775 lines
35 KiB
Python
775 lines
35 KiB
Python
# -*- 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/<domain>/{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()
|