init: 初始化 dpb 桃育种系统代码库

前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
34047007@qq.com
2026-08-06 00:17:49 +08:00
commit b95053c52c
1469 changed files with 322298 additions and 0 deletions
@@ -0,0 +1,134 @@
import urllib.parse
from typing import Annotated
from fastapi import APIRouter, Body, Depends, File, Path, Query, UploadFile
from fastapi.responses import JSONResponse, StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam, ImportResultSchema
from app.core.dependencies import AuthPermission, db_getter
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
from .schema import (
PropagationCreateSchema,
PropagationOutSchema,
PropagationQueryParam,
PropagationUpdateSchema,
)
from .service import PropagationService
PropagationRouter = APIRouter(route_class=OperationLogRoute, prefix="/propagation", tags=["克隆扩繁批次"])
@PropagationRouter.get("/detail/{id}", summary="获取扩繁批次详情", response_model=ResponseSchema[PropagationOutSchema])
async def get_propagation__detail_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:propagation:detail"]))],
id: Annotated[int, Path(description="扩繁批次ID")],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = PropagationService(auth, db)
result_dict = await service.detail(id=id)
return SuccessResponse(data=result_dict, msg="获取扩繁批次详情成功")
@PropagationRouter.get("/list", summary="分页查询扩繁批次", response_model=ResponseSchema[PageResultSchema[PropagationOutSchema]])
async def get_propagation__list_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:propagation:query"]))],
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[PropagationQueryParam, Query()],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = PropagationService(auth, db)
result_dict = await service.page(
page_no=page.page_no,
page_size=page.page_size,
search=search,
order_by=page.order_by,
)
return SuccessResponse(data=result_dict, msg="查询扩繁批次列表成功")
@PropagationRouter.get("/options", summary="扩繁批次下拉选项")
async def get_propagation__options_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:propagation:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = PropagationService(auth, db)
options = await service.list_options()
return SuccessResponse(data=options, msg="获取扩繁批次选项成功")
@PropagationRouter.post("/create", summary="创建扩繁批次", response_model=ResponseSchema[PropagationOutSchema])
async def create_propagation__controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:propagation:create"]))],
data: Annotated[PropagationCreateSchema, Body(description="创建参数")],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = PropagationService(auth, db)
result_dict = await service.create(data=data)
return SuccessResponse(data=result_dict, msg="创建扩繁批次成功")
@PropagationRouter.put("/update/{id}", summary="修改扩繁批次", response_model=ResponseSchema[PropagationOutSchema])
async def update_propagation__controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:propagation:update"]))],
id: Annotated[int, Path(description="扩繁批次ID")],
data: Annotated[PropagationUpdateSchema, Body(description="修改参数")],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = PropagationService(auth, db)
result_dict = await service.update(id=id, data=data)
return SuccessResponse(data=result_dict, msg="修改扩繁批次成功")
@PropagationRouter.delete("/delete", summary="删除扩繁批次", response_model=ResponseSchema[None])
async def delete_propagation__controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:propagation:delete"]))],
ids: Annotated[list[int], Body(description="ID列表")],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = PropagationService(auth, db)
await service.delete(ids=ids)
return SuccessResponse(msg="删除扩繁批次成功")
@PropagationRouter.post("/export", summary="导出扩繁批次")
async def export_propagation__controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:propagation:export"]))],
search: Annotated[PropagationQueryParam, Query()],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> StreamingResponse:
service = PropagationService(auth, db)
result_dict_list = await service.get_list(search=search)
export_result = PropagationService.batch_export(obj_list=[item.model_dump() for item in result_dict_list])
return StreamResponse(
data=bytes2file_response(export_result),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={urllib.parse.quote('克隆扩繁批次.xlsx')}"},
)
@PropagationRouter.post("/import", summary="导入扩繁批次", response_model=ResponseSchema[ImportResultSchema])
async def import_propagation__controller(
file: Annotated[UploadFile, File(description="导入文件")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:propagation:import"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = PropagationService(auth, db)
batch_import_result = await service.batch_import(file=file, update_support=True)
return SuccessResponse(data=batch_import_result, msg="导入扩繁批次成功")
@PropagationRouter.post("/download/template", summary="获取扩繁批次导入模板", dependencies=[Depends(AuthPermission(["module_bre:propagation:download"]))])
async def download_propagation__template_controller() -> StreamingResponse:
import_template_result = PropagationService.import_template_download()
return StreamResponse(
data=bytes2file_response(import_template_result),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={
"Content-Disposition": f"attachment; filename={urllib.parse.quote('克隆扩繁批次导入模板.xlsx')}",
"Access-Control-Expose-Headers": "Content-Disposition",
},
)
@@ -0,0 +1,18 @@
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_crud import CRUDBase
from app.core.base_schema import AuthSchema
from .model import PropagationModel
class BreedingPropagationCRUD(CRUDBase[PropagationModel, Any, Any]):
"""克隆扩繁批次 CRUD —— 直接复用 CRUDBase(已自动注入数据权限过滤)。"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(PropagationModel, auth, db)
propagation_crud = BreedingPropagationCRUD
@@ -0,0 +1,62 @@
"""克隆扩繁(苗圃)批次 数据模型"""
from datetime import date
from sqlalchemy import Date, ForeignKey, Index, Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import MappedBase, ModelMixin, UserMixin
class PropagationModel(ModelMixin, UserMixin, MappedBase):
"""克隆扩繁批次(规格 §3.8;入选株→克隆种质→扩繁→新树闭环)。
A2 产出物为无性系(produced_clone_id→bre_clone),沿用原 clone 不新建;
A3 砧木为砧木字典(rootstock_id→bre_rootstock),不再直连种质。
"""
__tablename__ = "bre_propagation"
batch_code: Mapped[str] = mapped_column(String(64), nullable=False, comment="扩繁批次编号")
scion_source_type: Mapped[str | None] = mapped_column(
String(16), nullable=True, comment="接穗来源类型(germplasm/tree)", default=None
)
scion_source_id: Mapped[int | None] = mapped_column(
Integer, nullable=True, comment="接穗来源ID(按类型对应种质或单株)", default=None
)
produced_clone_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_clone.id"), index=True, nullable=True, comment="产出的无性系(沿用原clone)", default=None
)
rootstock_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_rootstock.id"), index=True, nullable=True, comment="砧木(砧木字典)", default=None
)
method: Mapped[str | None] = mapped_column(String(16), nullable=True, comment="繁殖方法(嫁接/扦插/压条)", default=None)
graft_date: Mapped[date | None] = mapped_column(Date, nullable=True, comment="嫁接日期", default=None)
nursery_site_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_site.id"), index=True, nullable=True, comment="育苗基地(试验基地)", default=None
)
operator_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("bre_personnel.id"), index=True, nullable=True, comment="操作人(育种人员)", default=None
)
scion_count: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="接穗数量", default=None)
grafted_count: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="嫁接数量", default=None)
survival_count: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="成活数量", default=None)
destination: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="去向(定植/试验/外送)", default=None)
remark: Mapped[str | None] = mapped_column(String(512), nullable=True, comment="备注", default=None)
__table_args__ = (
Index("ix_bre_propagation_created_deleted", "created_time", "is_deleted"),
Index("uq_bre_propagation_batch", "batch_code", unique=True),
)
@@ -0,0 +1,80 @@
"""克隆扩繁批次 —— Pydantic 校验/序列化模型。"""
from datetime import date, datetime
from pydantic import BaseModel, ConfigDict, Field
from app.core.base_schema import CommonSchema
class PropagationBaseSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
batch_code: str = Field(..., description="扩繁批次编号")
scion_source_type: str | None = Field(default=None, description="接穗来源类型(germplasm/tree)")
scion_source_id: int | None = Field(default=None, description="接穗来源ID")
produced_clone_id: int | None = Field(default=None, description="产出的无性系(沿用原clone)")
rootstock_id: int | None = Field(default=None, description="砧木(砧木字典)")
method: str | None = Field(default=None, description="繁殖方法")
graft_date: date | None = Field(default=None, description="嫁接日期")
nursery_site_id: int | None = Field(default=None, description="育苗基地(试验基地)")
operator_id: int | None = Field(default=None, description="操作人(育种人员)")
scion_count: int | None = Field(default=None, description="接穗数量")
grafted_count: int | None = Field(default=None, description="嫁接数量")
survival_count: int | None = Field(default=None, description="成活数量")
destination: str | None = Field(default=None, description="去向")
remark: str | None = Field(default=None, description="备注")
class PropagationCreateSchema(PropagationBaseSchema):
pass
class PropagationUpdateSchema(PropagationBaseSchema):
batch_code: str | None = Field(default=None, description="扩繁批次编号")
scion_source_type: str | None = Field(default=None, description="接穗来源类型(germplasm/tree)")
scion_source_id: int | None = Field(default=None, description="接穗来源ID")
produced_clone_id: int | None = Field(default=None, description="产出的无性系(沿用原clone)")
rootstock_id: int | None = Field(default=None, description="砧木(砧木字典)")
method: str | None = Field(default=None, description="繁殖方法")
graft_date: date | None = Field(default=None, description="嫁接日期")
nursery_site_id: int | None = Field(default=None, description="育苗基地(试验基地)")
operator_id: int | None = Field(default=None, description="操作人(育种人员)")
scion_count: int | None = Field(default=None, description="接穗数量")
grafted_count: int | None = Field(default=None, description="嫁接数量")
survival_count: int | None = Field(default=None, description="成活数量")
destination: str | None = Field(default=None, description="去向")
remark: str | None = Field(default=None, description="备注")
class PropagationOutSchema(PropagationBaseSchema):
id: int
uuid: str
batch_code: str | None = None
scion_source_type: str | None = None
scion_source_id: int | None = None
produced_clone_id: int | None = None
produced_clone_name: str | None = None # 由 Service 层联表填充
rootstock_id: int | None = None
rootstock_name: str | None = None # 由 Service 层联表填充
method: str | None = None
graft_date: date | None = None
nursery_site_id: int | None = None
site_name: str | None = None # 由 Service 层联表填充
operator_id: int | None = None
operator_name: str | None = None # 由 Service 层联表填充
scion_count: int | None = None
grafted_count: int | None = None
survival_count: int | None = None
destination: str | None = None
remark: str | None = None
created_time: datetime | None = None
updated_time: datetime | None = None
created_by: CommonSchema | None = None
updated_by: CommonSchema | None = None
class PropagationQueryParam(BaseModel):
batch_code: str | None = Field(default=None, description="扩繁批次编号", json_schema_extra={"q": "like"})
method: str | None = Field(default=None, description="繁殖方法", json_schema_extra={"q": "like"})
produced_clone_id: int | None = Field(default=None, description="产出的无性系", json_schema_extra={"q": "eq"})
nursery_site_id: int | None = Field(default=None, description="育苗基地", json_schema_extra={"q": "eq"})
@@ -0,0 +1,311 @@
from typing import Any
from fastapi import UploadFile
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema, PageResultSchema, ImportResultSchema
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.utils.common_util import search_to_dict
from app.utils.excel_util import ExcelUtil
from .crud import BreedingPropagationCRUD
from .schema import (
PropagationCreateSchema,
PropagationOutSchema,
PropagationQueryParam,
PropagationUpdateSchema,
)
from app.core.base_crud import assert_parents_exist
from app.api.v1.module_bre.clone.crud import BreedingCloneCRUD
from app.api.v1.module_bre.clone.model import CloneModel
from app.api.v1.module_bre.rootstock.crud import BreedingRootstockCRUD
from app.api.v1.module_bre.rootstock.model import RootstockModel
from app.api.v1.module_bre.site.crud import BreedingSiteCRUD
from app.api.v1.module_bre.site.model import BreedingSiteModel
from app.api.v1.module_bre.personnel.crud import BreedingPersonnelCRUD
from app.api.v1.module_bre.personnel.model import PersonnelModel
from app.api.v1.module_bre.germplasm.model import BreedingGermplasmModel
from app.api.v1.module_bre.tree.model import TreeModel
_SCION_SOURCE_TYPES = {"germplasm", "tree"}
def _is_blank(v: Any) -> bool:
return v is None or (isinstance(v, str) and v.strip() == "")
def _none_if_blank(v: Any) -> Any:
if _is_blank(v):
return None
return str(v).strip() if isinstance(v, str) else v
def _to_int(v: Any) -> int | None:
if _is_blank(v):
return None
try:
return int(float(v))
except (TypeError, ValueError):
return None
class PropagationService:
"""克隆扩繁批次 模块服务层"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
self.auth = auth
self.db = db
async def _attach_fk_labels(self, items: list[PropagationOutSchema]) -> None:
if not items:
return
clone_ids = {getattr(it, "produced_clone_id") for it in items if getattr(it, "produced_clone_id")}
if clone_ids:
refs = await BreedingCloneCRUD(self.auth, self.db).get_list(search={"id": ("in", list(clone_ids))})
ref_map = {r.id: getattr(r, "clone_code") for r in refs}
for it in items:
it.produced_clone_name = ref_map.get(getattr(it, "produced_clone_id"))
rootstock_ids = {getattr(it, "rootstock_id") for it in items if getattr(it, "rootstock_id")}
if rootstock_ids:
refs = await BreedingRootstockCRUD(self.auth, self.db).get_list(search={"id": ("in", list(rootstock_ids))})
ref_map = {r.id: getattr(r, "rootstock_name") for r in refs}
for it in items:
it.rootstock_name = ref_map.get(getattr(it, "rootstock_id"))
site_ids = {getattr(it, "nursery_site_id") for it in items if getattr(it, "nursery_site_id")}
if site_ids:
refs = await BreedingSiteCRUD(self.auth, self.db).get_list(search={"id": ("in", list(site_ids))})
ref_map = {r.id: getattr(r, "site_name") for r in refs}
for it in items:
it.site_name = ref_map.get(getattr(it, "nursery_site_id"))
operator_ids = {getattr(it, "operator_id") for it in items if getattr(it, "operator_id")}
if operator_ids:
refs = await BreedingPersonnelCRUD(self.auth, self.db).get_list(search={"id": ("in", list(operator_ids))})
ref_map = {r.id: getattr(r, "name") for r in refs}
for it in items:
it.operator_name = ref_map.get(getattr(it, "operator_id"))
async def _assert_scion_source(self, source_type: str | None, source_id: int | None) -> None:
"""接穗来源多态校验:类型合法且对应种质/单株存在。"""
if _is_blank(source_type) or _is_blank(source_id):
return
if source_type not in _SCION_SOURCE_TYPES:
raise CustomException(msg=f"接穗来源类型不合法: {source_type}(仅支持 germplasm/tree)", status_code=409)
model = BreedingGermplasmModel if source_type == "germplasm" else TreeModel
await assert_parents_exist(self.db, [(model, source_id, "接穗来源")])
async def detail(self, id: int) -> PropagationOutSchema:
obj = await BreedingPropagationCRUD(self.auth, self.db).get(id=id)
if not obj:
raise CustomException(msg="该扩繁批次不存在")
out = PropagationOutSchema.model_validate(obj)
await self._attach_fk_labels([out])
return out
async def get_list(
self,
search: PropagationQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> list[PropagationOutSchema]:
obj_list = await BreedingPropagationCRUD(self.auth, self.db).get_list(
search=search_to_dict(search), order_by=order_by
)
outs = [PropagationOutSchema.model_validate(obj) for obj in obj_list]
await self._attach_fk_labels(outs)
return outs
async def page(
self,
page_no: int,
page_size: int,
search: PropagationQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> PageResultSchema[PropagationOutSchema]:
offset = (page_no - 1) * page_size
result = await BreedingPropagationCRUD(self.auth, self.db).page(
offset=offset,
limit=page_size,
order_by=order_by or [{"id": "asc"}],
search=search_to_dict(search, {}),
out_schema=PropagationOutSchema,
)
await self._attach_fk_labels(result.items)
return result
async def create(self, data: PropagationCreateSchema) -> PropagationOutSchema:
if _is_blank(data.batch_code):
raise CustomException(msg="扩繁批次编号不能为空")
await assert_parents_exist(
self.db,
[
(CloneModel, data.produced_clone_id, '无性系'),
(RootstockModel, data.rootstock_id, '砧木'),
(BreedingSiteModel, data.nursery_site_id, '试验基地'),
(PersonnelModel, data.operator_id, '育种人员'),
],
)
await self._assert_scion_source(data.scion_source_type, data.scion_source_id)
obj = await BreedingPropagationCRUD(self.auth, self.db).create(data=data)
out = PropagationOutSchema.model_validate(obj)
await self._attach_fk_labels([out])
return out
async def update(self, id: int, data: PropagationUpdateSchema) -> PropagationOutSchema:
obj = await BreedingPropagationCRUD(self.auth, self.db).get(id=id)
if not obj:
raise CustomException(msg="更新失败,该扩繁批次不存在")
await assert_parents_exist(
self.db,
[
(CloneModel, data.produced_clone_id, '无性系'),
(RootstockModel, data.rootstock_id, '砧木'),
(BreedingSiteModel, data.nursery_site_id, '试验基地'),
(PersonnelModel, data.operator_id, '育种人员'),
],
)
await self._assert_scion_source(data.scion_source_type, data.scion_source_id)
obj = await BreedingPropagationCRUD(self.auth, self.db).update(id=id, data=data)
out = PropagationOutSchema.model_validate(obj)
await self._attach_fk_labels([out])
return out
async def delete(self, ids: list[int]) -> None:
if not ids:
raise CustomException(msg="删除失败,删除对象不能为空")
objs = await BreedingPropagationCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)})
obj_map = {o.id: o for o in objs}
for id_ in ids:
if id_ not in obj_map:
raise CustomException(msg="删除失败,该扩繁批次不存在")
await BreedingPropagationCRUD(self.auth, self.db).delete(ids=ids)
async def list_options(self) -> list[dict[str, Any]]:
"""供前端下拉选择使用:返回 [{value, label}]。"""
obj_list = await BreedingPropagationCRUD(self.auth, self.db).get_list(order_by=[{"id": "asc"}])
return [{"value": o.id, "label": o.batch_code} for o in obj_list]
@staticmethod
def batch_export(obj_list: list[dict[str, Any]]) -> bytes:
mapping_dict = {
"batch_code": "扩繁批次编号",
"scion_source_type": "接穗来源类型",
"scion_source_id": "接穗来源ID",
"produced_clone_name": "产出无性系",
"rootstock_name": "砧木",
"method": "繁殖方法",
"graft_date": "嫁接日期",
"site_name": "育苗基地",
"operator_name": "操作人",
"scion_count": "接穗数量",
"grafted_count": "嫁接数量",
"survival_count": "成活数量",
"destination": "去向",
"remark": "备注",
"created_time": "创建时间",
"created_by": "创建者",
}
data = [dict(item) for item in obj_list]
for item in data:
creator = item.get("created_by")
item["created_by"] = creator.get("name", "未知") if isinstance(creator, dict) else "未知"
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
async def batch_import(self, file: UploadFile, update_support: bool = False) -> ImportResultSchema:
header_dict = {
"扩繁批次编号": "batch_code",
"接穗来源类型": "scion_source_type",
"接穗来源ID": "scion_source_id",
"产出无性系": "produced_clone_id",
"砧木": "rootstock_id",
"繁殖方法": "method",
"嫁接日期": "graft_date",
"育苗基地": "nursery_site_id",
"操作人": "operator_id",
"接穗数量": "scion_count",
"嫁接数量": "grafted_count",
"成活数量": "survival_count",
"去向": "destination",
"备注": "remark",
}
try:
contents = await file.read()
rows = ExcelUtil.read_excel_to_dicts(contents)
await file.close()
if not rows:
raise CustomException(msg="导入文件为空")
missing_headers = [h for h in header_dict if h not in rows[0]]
if missing_headers:
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
clone_refs = await BreedingCloneCRUD(self.auth, self.db).get_list(order_by=[{"id": "asc"}])
clone_map = {getattr(r, "clone_code"): r.id for r in clone_refs}
rootstock_refs = await BreedingRootstockCRUD(self.auth, self.db).get_list(order_by=[{"id": "asc"}])
rootstock_map = {getattr(r, "rootstock_name"): r.id for r in rootstock_refs}
site_refs = await BreedingSiteCRUD(self.auth, self.db).get_list(order_by=[{"id": "asc"}])
site_map = {getattr(r, "site_name"): r.id for r in site_refs}
operator_refs = await BreedingPersonnelCRUD(self.auth, self.db).get_list(order_by=[{"id": "asc"}])
operator_map = {getattr(r, "name"): r.id for r in operator_refs}
error_msgs: list[str] = []
success_count = 0
crud = BreedingPropagationCRUD(self.auth, self.db)
for i, row in enumerate(rows, start=1):
try:
fields = {
"batch_code": _none_if_blank(row.get("batch_code")),
"scion_source_type": _none_if_blank(row.get("scion_source_type")),
"scion_source_id": _to_int(row.get("scion_source_id")),
"produced_clone_id": clone_map.get(str(row.get("produced_clone_id")).strip())
if not _is_blank(row.get("produced_clone_id")) else None,
"rootstock_id": rootstock_map.get(str(row.get("rootstock_id")).strip())
if not _is_blank(row.get("rootstock_id")) else None,
"method": _none_if_blank(row.get("method")),
"graft_date": _none_if_blank(row.get("graft_date")),
"nursery_site_id": site_map.get(str(row.get("nursery_site_id")).strip())
if not _is_blank(row.get("nursery_site_id")) else None,
"operator_id": operator_map.get(str(row.get("operator_id")).strip())
if not _is_blank(row.get("operator_id")) else None,
"scion_count": _to_int(row.get("scion_count")),
"grafted_count": _to_int(row.get("grafted_count")),
"survival_count": _to_int(row.get("survival_count")),
"destination": _none_if_blank(row.get("destination")),
"remark": _none_if_blank(row.get("remark")),
}
create_data = PropagationCreateSchema(**fields)
await crud.create(data=create_data)
success_count += 1
except Exception as e:
error_msgs.append(f"{i}行: {e!s}")
continue
return ImportResultSchema(
valid_count=success_count,
invalid_count=len(error_msgs),
message_list=error_msgs,
)
except Exception as e:
logger.error(f"批量导入克隆扩繁批次失败: {e!s}")
raise CustomException(msg=f"导入失败: {e!s}")
@staticmethod
def import_template_download() -> bytes:
header_list = [
"扩繁批次编号",
"接穗来源类型",
"接穗来源ID",
"产出无性系",
"砧木",
"繁殖方法",
"嫁接日期",
"育苗基地",
"操作人",
"接穗数量",
"嫁接数量",
"成活数量",
"去向",
"备注",
]
selector_header_list = []
option_list = []
return ExcelUtil.get_excel_template(
header_list=header_list,
selector_header_list=selector_header_list,
option_list=option_list,
)