# -*- coding: utf-8 -*- """生成 breeding 剩余 10 个模块的后端文件 + 菜单 SQL。 运行: conda activate dpb && python _gen_breeding_be.py --force 无 --force 会被阻止(本生成器会整体覆写模块文件,丢波次 1-6 业务修复)。 依赖: _gen_specs.py 产出: backend/app/api/v1/module_bre//{model,schema,crud,service,controller}.py backend/app/api/v1/module_bre/__init__.py (重写, 注册所有 Router) backend/sql/bre_menu_extra.sql (已废弃, 仅占位说明) 说明: 菜单唯一权威种子为 backend/sql/bre_menu.sql(重构版菜单树 + 预留树幂等补灌)。 旧 900130 段菜单设计已废弃,本生成器不再输出菜单 DML。 """ import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _gen_specs import MOD, cls_of # noqa: E402 BASE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "backend") PKG = os.path.join(BASE, "app", "api", "v1", "module_bre") SQL_DIR = os.path.join(BASE, "sql") # ── 类型映射 ──────────────────────────────────────────────────────────────── def sa_py(t): if t == "str": return "String(100)", "str" if t == "text": return "Text", "str" if t == "select": return "String(50)", "str" if t == "date": return "String(20)", "str" if t == "int": return "Integer", "int" if t == "float": return "Float", "float" raise ValueError(t) def schema_py(f): if f["type"] in ("int", "fk"): return "int" if f["type"] == "float": return "float" return "str" def fk_out_name(field): # bre_target_id -> bre_target_name return field[:-3] + "_name" def ref_table_of(domain): return "bre_" + domain def ref_crud_cls(domain): return "Breeding" + cls_of(domain) + "CRUD" def ref_pkg(domain): # plot 模型/CRUD 寄居在 site 包下,FK 引用需从 site 导入 return "site" if domain == "plot" else domain def opt_label_field(m): names = ("target_name", "combination_code", "name", "plot_code", "cultivar_name") for f in m["fields"]: if f["name"] in names: return f["name"] for f in m["fields"]: if f["type"] in ("str", "text", "select", "date") and f["req"]: return f["name"] for f in m["fields"]: if f["type"] in ("str", "text", "select", "date"): return f["name"] return m["fields"][0]["name"] # ── model.py ──────────────────────────────────────────────────────────────── def gen_model(m): cls = cls_of(m["domain"]) table = m["table"] L = [] L.append('"""%s 数据模型"""' % m["title"]) L.append("from datetime import datetime") L.append("") L.append("from sqlalchemy import Float, ForeignKey, Index, Integer, String, Text") L.append("from sqlalchemy.orm import Mapped, mapped_column") L.append("") L.append("from app.core.base_model import MappedBase, ModelMixin") L.append("") L.append("") L.append("class %sModel(ModelMixin, MappedBase):" % cls) L.append(' """%s 主数据表。"""' % m["title"]) L.append("") L.append(' __tablename__ = "%s"' % table) L.append("") for f in m["fields"]: L.append(gen_model_field(f)) L.append("") L.append(" # 无 status 列:覆盖基类默认 ix_<表>_status_deleted 索引,") L.append(" # 仅保留 (created_time, is_deleted) 复合索引用于数据权限过滤。") L.append(" __table_args__ = (") L.append(' Index("ix_%s_created_deleted", "created_time", "is_deleted"),' % table) L.append(" )") return "\n".join(L) def gen_model_field(f): t = f["type"] name = f["name"] label = f["label"] if t == "fk": ref_table = ref_table_of(f["fk"][0]) null = "False" if f["req"] else "True" return ( " %s: Mapped[int] = mapped_column(\n" ' Integer, ForeignKey("%s.id", ondelete="CASCADE"),\n' ' index=True, nullable=%s, comment="%s"\n' " )" % (name, ref_table, null, label) ) sa, _ = sa_py(t) if f["req"]: mapped = "str" if t in ("str", "text", "select", "date") else t null = "False" extra = "" else: mapped = ("str" if t in ("str", "text", "select", "date") else t) + " | None" null = "True" extra = ", default=None" if t == "str": sa = "String(%d)" % f.get("length", 100) return ' %s: Mapped[%s] = mapped_column(%s, nullable=%s, comment="%s"%s)' % ( name, mapped, sa, null, label, extra, ) # ── schema.py ─────────────────────────────────────────────────────────────── def gen_schema(m): cls = cls_of(m["domain"]) L = [] L.append('"""%s —— Pydantic 校验/序列化模型。"""' % m["title"]) L.append("from datetime import datetime") L.append("") L.append("from pydantic import BaseModel, ConfigDict, Field") L.append("") L.append("") L.append("class %sBaseSchema(BaseModel):" % cls) L.append(" model_config = ConfigDict(from_attributes=True)") L.append("") for f in m["fields"]: dft = "..." if f["req"] else "default=None" L.append(' %s: %s = Field(%s, description="%s")' % (f["name"], schema_py(f), dft, f["label"])) L.append("") L.append("") L.append("class %sCreateSchema(%sBaseSchema):" % (cls, cls)) L.append(" pass") L.append("") L.append("") L.append("class %sUpdateSchema(%sBaseSchema):" % (cls, cls)) for f in m["fields"]: L.append(' %s: %s | None = Field(default=None, description="%s")' % (f["name"], schema_py(f), f["label"])) L.append("") L.append("") L.append("class %sOutSchema(%sBaseSchema):" % (cls, cls)) L.append(" id: int") L.append(" uuid: str") for f in m["fields"]: if f["type"] == "fk": L.append(" %s: str | None = None # 由 Service 层联表填充" % fk_out_name(f["name"])) else: L.append(" %s: %s | None = None" % (f["name"], schema_py(f))) L.append(" created_time: datetime | None = None") L.append(" updated_time: datetime | None = None") L.append(" created_by: dict | None = None") L.append(" updated_by: dict | None = None") L.append("") L.append("") qfields = [f for f in m["fields"] if f["search"]] L.append("class %sQueryParam(BaseModel):" % cls) if qfields: for f in qfields: L.append(' %s: %s | None = Field(default=None, description="%s")' % (f["name"], schema_py(f), f["label"])) L.append("") L.append(" model_config = ConfigDict(") L.append(' extra="ignore",') L.append(" json_schema_extra={") for f in qfields: op = "like" if f["search"] == "like" else "eq" L.append(' "%s": ("%s", "{param}"),' % (f["name"], op)) L.append(" },") L.append(" )") else: L.append(' model_config = ConfigDict(extra="ignore")') return "\n".join(L) # ── crud.py ───────────────────────────────────────────────────────────────── def gen_crud(m): cls = cls_of(m["domain"]) L = [] L.append("from typing import Any") L.append("") L.append("from sqlalchemy.ext.asyncio import AsyncSession") L.append("") L.append("from app.core.base_crud import CRUDBase") L.append("from app.core.base_schema import AuthSchema") L.append("") L.append("from .model import %sModel" % cls) L.append("") L.append("") L.append("class Breeding%sCRUD(CRUDBase[%sModel, Any, Any]):" % (cls, cls)) L.append(' """%s CRUD —— 直接复用 CRUDBase(已自动注入数据权限过滤)。"""' % m["title"]) L.append("") L.append(" def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:") L.append(" super().__init__(%sModel, auth, db)" % cls) L.append("") L.append("") L.append("%s_crud = Breeding%sCRUD" % (m["domain"], cls)) return "\n".join(L) # ── service.py ────────────────────────────────────────────────────────────── def gen_service(m): cls = cls_of(m["domain"]) domain = m["domain"] L = [] L.append("from typing import Any") L.append("") L.append("from fastapi import UploadFile") L.append("from sqlalchemy.ext.asyncio import AsyncSession") L.append("") L.append("from app.core.base_schema import AuthSchema, PageResultSchema") L.append("from app.core.exceptions import CustomException") L.append("from app.core.logger import logger") L.append("from app.utils.common_util import search_to_dict") L.append("from app.utils.excel_util import ExcelUtil") L.append("") L.append("from .crud import Breeding%sCRUD" % cls) L.append("from .schema import (") L.append(" %sCreateSchema," % cls) L.append(" %sOutSchema," % cls) L.append(" %sQueryParam," % cls) L.append(" %sUpdateSchema," % cls) L.append(")") # ref crud imports (dedup) ref_domains = [] for f in m["fields"]: if f["type"] == "fk" and f["fk"][0] not in ref_domains: ref_domains.append(f["fk"][0]) for rd in ref_domains: L.append("from app.api.v1.module_bre.%s.crud import %s" % (ref_pkg(rd), ref_crud_cls(rd))) L.append("") L.append("") # helpers L.append("def _is_blank(v: Any) -> bool:") L.append(" return v is None or (isinstance(v, str) and v.strip() == \"\")") L.append("") L.append("") L.append("def _none_if_blank(v: Any) -> Any:") L.append(" if _is_blank(v):") L.append(" return None") L.append(" return str(v).strip() if isinstance(v, str) else v") L.append("") L.append("") L.append("def _to_float(v: Any) -> float | None:") L.append(" if _is_blank(v):") L.append(" return None") L.append(" try:") L.append(" return float(v)") L.append(" except (TypeError, ValueError):") L.append(" return None") L.append("") L.append("") L.append("def _to_int(v: Any) -> int | None:") L.append(" if _is_blank(v):") L.append(" return None") L.append(" try:") L.append(" return int(float(v))") L.append(" except (TypeError, ValueError):") L.append(" return None") L.append("") L.append("") L.append("class %sService:" % cls) L.append(' """%s 模块服务层"""' % m["title"]) L.append("") L.append(" def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:") L.append(" self.auth = auth") L.append(" self.db = db") L.append("") # _attach_fk_labels fks = [f for f in m["fields"] if f["type"] == "fk"] L.append(" async def _attach_fk_labels(self, items: list[%sOutSchema]) -> None:" % cls) L.append(" if not items:") L.append(" return") L.append(" crud = Breeding%sCRUD(self.auth, self.db)" % cls) for f in fks: name = f["name"] ref_cls = ref_crud_cls(f["fk"][0]) label = f["fk"][1] oname = fk_out_name(name) L.append(" %s_ids = {getattr(it, \"%s\") for it in items if getattr(it, \"%s\")}" % (name, name, name)) L.append(" if %s_ids:" % name) L.append(" refs = await %s(self.auth, self.db).get_list(search={\"id\": (\"in\", list(%s_ids))})" % (ref_cls, name)) L.append(" ref_map = {r.id: getattr(r, \"%s\") for r in refs}" % label) L.append(" for it in items:") L.append(" it.%s = ref_map.get(getattr(it, \"%s\"))" % (oname, name)) L.append("") # detail L.append(" async def detail(self, id: int) -> %sOutSchema:" % cls) L.append(" obj = await Breeding%sCRUD(self.auth, self.db).get(id=id)" % cls) L.append(" if not obj:") L.append(' raise CustomException(msg="该%s不存在")' % m["title"]) L.append(" out = %sOutSchema.model_validate(obj)" % cls) L.append(" await self._attach_fk_labels([out])") L.append(" return out") L.append("") # get_list L.append(" async def get_list(") L.append(" self,") L.append(" search: %sQueryParam | None = None," % cls) L.append(" order_by: list[dict[str, str]] | None = None,") L.append(" ) -> list[%sOutSchema]:" % cls) L.append(" obj_list = await Breeding%sCRUD(self.auth, self.db).get_list(" % cls) L.append(" search=search_to_dict(search), order_by=order_by") L.append(" )") L.append(" outs = [%sOutSchema.model_validate(obj) for obj in obj_list]" % cls) L.append(" await self._attach_fk_labels(outs)") L.append(" return outs") L.append("") # page L.append(" async def page(") L.append(" self,") L.append(" page_no: int,") L.append(" page_size: int,") L.append(" search: %sQueryParam | None = None," % cls) L.append(" order_by: list[dict[str, str]] | None = None,") L.append(" ) -> PageResultSchema[%sOutSchema]:" % cls) L.append(" offset = (page_no - 1) * page_size") L.append(" result = await Breeding%sCRUD(self.auth, self.db).page(" % cls) L.append(" offset=offset,") L.append(" limit=page_size,") L.append(" order_by=order_by or [{\"id\": \"asc\"}],") L.append(" search=search_to_dict(search, {}),") L.append(" out_schema=%sOutSchema," % cls) L.append(" )") L.append(" await self._attach_fk_labels(result.items)") L.append(" return result") L.append("") # create L.append(" async def create(self, data: %sCreateSchema) -> %sOutSchema:" % (cls, cls)) for f in m["fields"]: if f["unique"]: L.append(" exist_obj = await Breeding%sCRUD(self.auth, self.db).get(%s=data.%s)" % (cls, f["name"], f["name"])) L.append(' if exist_obj:') L.append(' raise CustomException(msg="创建失败,%s已存在")' % f["label"]) L.append(" obj = await Breeding%sCRUD(self.auth, self.db).create(data=data)" % cls) L.append(" out = %sOutSchema.model_validate(obj)" % cls) if fks: L.append(" await self._attach_fk_labels([out])") L.append(" return out") L.append("") # update L.append(" async def update(self, id: int, data: %sUpdateSchema) -> %sOutSchema:" % (cls, cls)) L.append(" obj = await Breeding%sCRUD(self.auth, self.db).get(id=id)" % cls) L.append(" if not obj:") L.append(' raise CustomException(msg="更新失败,该%s不存在")' % m["title"]) for f in m["fields"]: if f["unique"]: L.append(" if data.%s is not None:" % f["name"]) L.append(" exist_obj = await Breeding%sCRUD(self.auth, self.db).get(%s=data.%s)" % (cls, f["name"], f["name"])) L.append(" if exist_obj and exist_obj.id != id:") L.append(' raise CustomException(msg="更新失败,%s重复")' % f["label"]) L.append(" obj = await Breeding%sCRUD(self.auth, self.db).update(id=id, data=data)" % cls) L.append(" out = %sOutSchema.model_validate(obj)" % cls) if fks: L.append(" await self._attach_fk_labels([out])") L.append(" return out") L.append("") # delete L.append(" async def delete(self, ids: list[int]) -> None:") L.append(' if not ids:') L.append(' raise CustomException(msg="删除失败,删除对象不能为空")') L.append(" objs = await Breeding%sCRUD(self.auth, self.db).get_list(search={\"id\": (\"in\", ids)})" % cls) L.append(" obj_map = {o.id: o for o in objs}") L.append(" for id_ in ids:") L.append(" if id_ not in obj_map:") L.append(' raise CustomException(msg="删除失败,该%s不存在")' % m["title"]) L.append(" await Breeding%sCRUD(self.auth, self.db).delete(ids=ids)" % cls) L.append("") # list_options opt_label = opt_label_field(m) L.append(" async def list_options(self) -> list[dict[str, Any]]:") L.append(' """供前端下拉选择使用:返回 [{value, label}]。"""') L.append(" obj_list = await Breeding%sCRUD(self.auth, self.db).get_list(order_by=[{\"id\": \"asc\"}])" % cls) L.append(" return [{\"value\": o.id, \"label\": o.%s} for o in obj_list]" % opt_label) L.append("") # batch_export L.append(" @staticmethod") L.append(" def batch_export(obj_list: list[dict[str, Any]]) -> bytes:") L.append(" mapping_dict = {") for f in m["fields"]: if f["type"] == "fk": L.append(' "%s": "%s",' % (fk_out_name(f["name"]), f["label"])) else: L.append(' "%s": "%s",' % (f["name"], f["label"])) L.append(' "created_time": "创建时间",') L.append(' "created_by": "创建者",') L.append(" }") L.append(" data = [dict(item) for item in obj_list]") L.append(" for item in data:") L.append(" creator = item.get(\"created_by\")") L.append(' item["created_by"] = creator.get("name", "未知") if isinstance(creator, dict) else "未知"') L.append(" return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)") L.append("") # batch_import L.append(" async def batch_import(self, file: UploadFile, update_support: bool = False) -> str:") L.append(" header_dict = {") for f in m["fields"]: L.append(' "%s": "%s",' % (f["label"], f["name"])) L.append(" }") L.append(" try:") L.append(" contents = await file.read()") L.append(" rows = ExcelUtil.read_excel_to_dicts(contents)") L.append(" await file.close()") L.append(' if not rows:') L.append(' raise CustomException(msg="导入文件为空")') L.append(" missing_headers = [h for h in header_dict if h not in rows[0]]") L.append(" if missing_headers:") L.append(' raise CustomException(msg=f"导入文件缺少必要的列: {\', \'.join(missing_headers)}")') # preload fk maps for f in fks: name = f["name"] ref_cls = ref_crud_cls(f["fk"][0]) label = f["fk"][1] L.append(" %s_refs = await %s(self.auth, self.db).get_list(order_by=[{\"id\": \"asc\"}])" % (name, ref_cls)) L.append(" %s_map = {getattr(r, \"%s\"): r.id for r in %s_refs}" % (name, label, name)) L.append(" mapped_rows = []") L.append(" for row in rows:") L.append(" mapped_rows.append({en: row.get(ch) for ch, en in header_dict.items()})") # required check req_fields = [f for f in m["fields"] if f["req"]] if req_fields: L.append(" required_fields = [%s]" % ", ".join('"%s"' % f["name"] for f in req_fields)) L.append(" errors = []") L.append(" for field in required_fields:") L.append(" missing_indices = [i + 1 for i, r in enumerate(mapped_rows) if _is_blank(r.get(field))]") L.append(" if missing_indices:") L.append(" field_name = next(k for k, v in header_dict.items() if v == field)") L.append(' rows_str = "、".join(str(i) for i in missing_indices)') L.append(' errors.append(f"{field_name}不能为空,第{rows_str}行")') L.append(" if errors:") L.append(' raise CustomException(msg=f"导入失败,以下行缺少必要字段:\\n{\'; \'.join(errors)}")') L.append(" error_msgs: list[str] = []") L.append(" success_count = 0") L.append(" crud = Breeding%sCRUD(self.auth, self.db)" % cls) L.append(" for i, row in enumerate(mapped_rows, start=1):") L.append(" try:") # build field dict fk_val_lines = [] for f in m["fields"]: if f["type"] == "fk": name = f["name"] fk_val_lines.append(" %s_val = %s_map.get(str(row.get(\"%s\")).strip()) if not _is_blank(row.get(\"%s\")) else None" % (name, name, f["label"], f["label"])) for line in fk_val_lines: L.append(line) L.append(" fields = {") for f in m["fields"]: name = f["name"] if f["type"] == "int": L.append(' "%s": _to_int(row.get("%s")),' % (name, name)) elif f["type"] == "float": L.append(' "%s": _to_float(row.get("%s")),' % (name, name)) elif f["type"] == "fk": L.append(' "%s": %s_val,' % (name, name)) else: L.append(' "%s": _none_if_blank(row.get("%s")),' % (name, name)) L.append(" }") if fks: # uniqueness per fk? skip; just create or update by natural unique pass uniq = [f for f in m["fields"] if f["unique"]] if uniq: L.append(" unique_kwargs = {%s}" % ", ".join('"%s": fields["%s"]' % (f["name"], f["name"]) for f in uniq)) L.append(" create_data = %sCreateSchema(**fields)" % cls) L.append(" exist_obj = await crud.get(**unique_kwargs)") L.append(" if exist_obj:") L.append(" if update_support:") L.append(" await crud.update(id=exist_obj.id, data=%sUpdateSchema(**fields))" % cls) L.append(" success_count += 1") L.append(" else:") _msg = "第{i}行: " + uniq[0]["label"] + " {fields['" + uniq[0]["name"] + "']} 已存在" L.append(' error_msgs.append(f"' + _msg + '")') L.append(" else:") L.append(" await crud.create(data=create_data)") L.append(" success_count += 1") else: L.append(" create_data = %sCreateSchema(**fields)" % cls) L.append(" await crud.create(data=create_data)") L.append(" success_count += 1") L.append(" except Exception as e:") L.append(" error_msgs.append(f\"第{i}行: {e!s}\")") L.append(" continue") L.append(" result = f\"成功导入 {success_count} 条数据\"") L.append(" if error_msgs:") L.append(' result += "\\n错误信息:\\n" + "\\n".join(error_msgs)') L.append(" return result") L.append(" except Exception as e:") L.append(' logger.error(f"批量导入%s失败: {e!s}")' % m["title"]) L.append(' raise CustomException(msg=f"导入失败: {e!s}")') L.append("") # import_template_download L.append(" @staticmethod") L.append(" def import_template_download() -> bytes:") L.append(" header_list = [") for f in m["fields"]: L.append(' "%s",' % f["label"]) L.append(" ]") L.append(" selector_header_list = [%s]" % ", ".join('"%s"' % f["label"] for f in m["fields"] if f["type"] == "select")) L.append(" option_list = [") for f in m["fields"]: if f["type"] == "select": opts = ", ".join('"%s"' % o[0] for o in f["options"]) L.append(" {%s: [%s]}," % ('"%s"' % f["label"], opts)) L.append(" ]") L.append(" return ExcelUtil.get_excel_template(") L.append(" header_list=header_list,") L.append(" selector_header_list=selector_header_list,") L.append(" option_list=option_list,") L.append(" )") return "\n".join(L) # ── controller.py ─────────────────────────────────────────────────────────── CTRL_TMPL = '''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 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 ( __SCHEMAS__ ) from .service import __SERVICE__ __ROUTER__ = APIRouter(route_class=OperationLogRoute, prefix="__PREFIX__", tags=["__TITLE__"]) @__ROUTER__.get("/detail/{id}", summary="获取__TITLE__详情", response_model=ResponseSchema[__OUT__]) async def get___DOMAIN____detail_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:__DOMAIN__:detail"]))], id: Annotated[int, Path(description="__TITLE__ID")], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: service = __SERVICE__(auth, db) result_dict = await service.detail(id=id) return SuccessResponse(data=result_dict, msg="获取__TITLE__详情成功") @__ROUTER__.get("/list", summary="分页查询__TITLE__", response_model=ResponseSchema[PageResultSchema[__OUT__]]) async def get___DOMAIN____list_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:__DOMAIN__:query"]))], page: Annotated[PaginationQueryParam, Depends()], search: Annotated[__QUERY__, Query()], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: service = __SERVICE__(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="查询__TITLE__列表成功") @__ROUTER__.get("/options", summary="__TITLE__下拉选项") async def get___DOMAIN____options_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:__DOMAIN__:query"]))], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: service = __SERVICE__(auth, db) options = await service.list_options() return SuccessResponse(data=options, msg="获取__TITLE__选项成功") @__ROUTER__.post("/create", summary="创建__TITLE__", response_model=ResponseSchema[__OUT__]) async def create___DOMAIN____controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:__DOMAIN__:create"]))], data: Annotated[__CREATE__, Body(description="创建参数")], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: service = __SERVICE__(auth, db) result_dict = await service.create(data=data) return SuccessResponse(data=result_dict, msg="创建__TITLE__成功") @__ROUTER__.put("/update/{id}", summary="修改__TITLE__", response_model=ResponseSchema[__OUT__]) async def update___DOMAIN____controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:__DOMAIN__:update"]))], id: Annotated[int, Path(description="__TITLE__ID")], data: Annotated[__UPDATE__, Body(description="修改参数")], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: service = __SERVICE__(auth, db) result_dict = await service.update(id=id, data=data) return SuccessResponse(data=result_dict, msg="修改__TITLE__成功") @__ROUTER__.delete("/delete", summary="删除__TITLE__", response_model=ResponseSchema[None]) async def delete___DOMAIN____controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:__DOMAIN__:delete"]))], ids: Annotated[list[int], Body(description="ID列表")], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: service = __SERVICE__(auth, db) await service.delete(ids=ids) return SuccessResponse(msg="删除__TITLE__成功") @__ROUTER__.post("/export", summary="导出__TITLE__") async def export___DOMAIN____controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:__DOMAIN__:export"]))], search: Annotated[__QUERY__, Query()], db: Annotated[AsyncSession, Depends(db_getter)], ) -> StreamingResponse: service = __SERVICE__(auth, db) result_dict_list = await service.get_list(search=search) export_result = __SERVICE__.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('__TITLE__管理.xlsx')}"}, ) @__ROUTER__.post("/import", summary="导入__TITLE__", response_model=ResponseSchema[str]) async def import___DOMAIN____controller( file: Annotated[UploadFile, File(description="导入文件")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:__DOMAIN__:import"]))], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: service = __SERVICE__(auth, db) batch_import_result = await service.batch_import(file=file, update_support=True) return SuccessResponse(data=batch_import_result, msg="导入__TITLE__成功") @__ROUTER__.post("/download/template", summary="获取__TITLE__导入模板", dependencies=[Depends(AuthPermission(["module_bre:__DOMAIN__:download"]))]) async def download___DOMAIN____template_controller() -> StreamingResponse: import_template_result = __SERVICE__.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('__TITLE__管理导入模板.xlsx')}", "Access-Control-Expose-Headers": "Content-Disposition", }, ) ''' def gen_controller(m): cls = cls_of(m["domain"]) domain = m["domain"] schemas = "\n ".join([ " %sCreateSchema," % cls, " %sOutSchema," % cls, " %sQueryParam," % cls, " %sUpdateSchema," % cls, ]) text = CTRL_TMPL text = text.replace("__SCHEMAS__", schemas) text = text.replace("__SERVICE__", cls + "Service") text = text.replace("__ROUTER__", cls + "Router") text = text.replace("__PREFIX__", "/" + domain) text = text.replace("__TITLE__", m["title"]) text = text.replace("__DOMAIN__", domain) text = text.replace("__OUT__", cls + "OutSchema") text = text.replace("__QUERY__", cls + "QueryParam") text = text.replace("__CREATE__", cls + "CreateSchema") text = text.replace("__UPDATE__", cls + "UpdateSchema") return text # ── __init__.py ───────────────────────────────────────────────────────────── def gen_init(): L = [] L.append('"""桃育种业务模块"""') L.append("") L.append("from fastapi import APIRouter") L.append("") L.append("from .site.controller import SiteRouter, PlotRouter") L.append("from .germplasm.controller import GermplasmRouter") L.append("from .statistics.controller import StatisticsRouter") L.append("") lines = [] lines.append("bre_router.include_router(SiteRouter)") lines.append("bre_router.include_router(PlotRouter)") lines.append("bre_router.include_router(GermplasmRouter)") lines.append("bre_router.include_router(StatisticsRouter)") for m in MOD: cls = cls_of(m["domain"]) L.append("from .%s.controller import %sRouter" % (m["domain"], cls)) lines.append("bre_router.include_router(%sRouter)" % cls) L.append("") L.append('bre_router = APIRouter(prefix="/bre")') for ln in lines: L.append(ln) return "\n".join(L) # ── menu SQL(已废弃,仅输出占位说明)─────────────────────────────────────── # 全系统菜单的唯一权威种子为 backend/sql/bre_menu.sql(重构版菜单树), # 拥有 900001-900199 + 900300-900499,并以 ON CONFLICT DO NOTHING 幂等补灌 # 900200-900299 预留树(分子育种/品种保护/AI增强)。 # 旧 900130 段(挂 900001)与重构版冲突,严禁再生成,否则重跑会摧毁线上菜单。 def gen_menu_sql(): return """-- ============================================================ -- 桃育种业务模块 · 扩展菜单种子(已废弃) -- 全系统菜单唯一权威种子见 backend/sql/bre_menu.sql。 -- 本文件由 _gen_breeding_be.py 生成器占位输出,仅作说明,不含任何 DDL/DML。 -- ============================================================ SELECT 1; """ # ── 写出 ──────────────────────────────────────────────────────────────────── # 危险提示:本生成器会【整体覆写】各模块 5 个文件。波次 1-6 已对手写 # service/schema 等做过大量业务修复(FK 校验、状态机、数值门禁、自动编号等), # 无参重跑会静默清空这些修复。必须以 --force 显式确认才允许重生成。 def main(): if "--force" not in sys.argv: print("!! 已阻止重生成:本生成器会覆写模块后端文件,丢失波次 1-6 的业务修复。") print(" 如确认要用模板骨架覆盖现有实现,请显式执行: python _gen_breeding_be.py --force") print(" (菜单种子已改为只输出占位说明,唯一权威见 backend/sql/bre_menu.sql)") sys.exit(1) for m in MOD: d = os.path.join(PKG, m["domain"]) os.makedirs(d, exist_ok=True) files = { "model.py": gen_model(m), "schema.py": gen_schema(m), "crud.py": gen_crud(m), "service.py": gen_service(m), "controller.py": gen_controller(m), } for fn, content in files.items(): with open(os.path.join(d, fn), "w", encoding="utf-8") as fh: fh.write(content + "\n") print("generated backend:", m["domain"]) # __init__.py with open(os.path.join(PKG, "__init__.py"), "w", encoding="utf-8") as fh: fh.write(gen_init() + "\n") print("rewrote module_bre/__init__.py") # menu sql(占位说明) with open(os.path.join(SQL_DIR, "bre_menu_extra.sql"), "w", encoding="utf-8") as fh: fh.write(gen_menu_sql() + "\n") print("wrote bre_menu_extra.sql (deprecation stub)") if __name__ == "__main__": main()