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,53 @@
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.common.response import ResponseSchema, 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 .schema import AuditLogOutSchema, AuditLogQueryParam
from .service import AuditLogService
AuditRouter = APIRouter(route_class=OperationLogRoute, prefix="/audit", tags=["育种审计日志"])
@AuditRouter.get("/list", summary="分页查询审计日志", response_model=ResponseSchema[PageResultSchema[AuditLogOutSchema]])
async def get_audit__list_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:audit:query"]))],
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[AuditLogQueryParam, Query()],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = AuditLogService(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="查询审计日志列表成功")
@AuditRouter.get("/detail/{id}", summary="获取审计日志详情", response_model=ResponseSchema[AuditLogOutSchema])
async def get_audit__detail_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:audit:query"]))],
id: Annotated[int, Path(description="审计日志ID")],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = AuditLogService(auth, db)
result_dict = await service.detail(id=id)
return SuccessResponse(data=result_dict, msg="获取审计日志详情成功")
@AuditRouter.get("/options", summary="审计日志筛选选项")
async def get_audit__options_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_bre:audit:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = AuditLogService(auth, db)
options = await service.options()
return SuccessResponse(data=options, msg="获取审计日志选项成功")
@@ -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 AuditLogModel
class BreedingAuditCRUD(CRUDBase[AuditLogModel, Any, Any]):
"""审计日志 CRUD —— 复用 CRUDBase 的查询/分页(自动带数据权限过滤)。"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(AuditLogModel, auth, db)
audit_crud = BreedingAuditCRUD
@@ -0,0 +1,44 @@
"""育种审计日志 数据模型(§3.10;写操作全程可追溯切面)"""
from sqlalchemy import ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import MappedBase, ModelMixin, UserMixin
class AuditLogModel(ModelMixin, UserMixin, MappedBase):
"""审计日志。
entity_type=业务表名(__tablename__);CREATE 记录级 / UPDATE 字段级(每变更字段一行)/
DELETE 记录级 / IMPORT 汇总级。operator_id→bre_personnel 仅当育种人员姓名与操作用户名
一致时填充(默认只填 created_id→sys_user)。
"""
__tablename__ = "bre_audit_log"
__table_args__ = () # 表无 status 列,不生成 ModelMixin 的 status 索引
entity_type: Mapped[str | None] = mapped_column(
String(32), index=True, nullable=True, comment="实体类型(业务表名)", default=None
)
entity_id: Mapped[int | None] = mapped_column(
Integer, index=True, nullable=True, comment="实体ID", default=None
)
action: Mapped[str | None] = mapped_column(
String(32), index=True, nullable=True, comment="操作类型(CREATE/UPDATE/DELETE/IMPORT)", default=None
)
field_name: Mapped[str | None] = mapped_column(
String(64), nullable=True, comment="变更字段名(UPDATE 字段级)", default=None
)
old_value: Mapped[str | None] = mapped_column(
String(512), nullable=True, comment="旧值", default=None
)
new_value: Mapped[str | None] = mapped_column(
String(512), nullable=True, comment="新值", default=None
)
operator_id: Mapped[int | None] = mapped_column(
Integer,
ForeignKey("bre_personnel.id", ondelete="CASCADE"),
nullable=True,
comment="操作人(育种人员,命中 personnel.name==username 才填)",
default=None,
)
@@ -0,0 +1,33 @@
"""育种审计日志 —— Pydantic 校验/序列化模型。"""
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
from app.core.base_schema import CommonSchema
class AuditLogOutSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
uuid: str
entity_type: str | None = None
entity_id: int | None = None
action: str | None = None
field_name: str | None = None
old_value: str | None = None
new_value: str | None = None
operator_id: int | None = None
operator_name: str | None = None # 由 Service 层联表填充
created_time: datetime | None = None
updated_time: datetime | None = None
created_by: CommonSchema | None = None
updated_by: CommonSchema | None = None
class AuditLogQueryParam(BaseModel):
entity_type: str | None = Field(default=None, description="实体类型(业务表名)", json_schema_extra={"q": "like"})
entity_id: int | None = Field(default=None, description="实体ID", json_schema_extra={"q": "eq"})
action: str | None = Field(default=None, description="操作类型", json_schema_extra={"q": "eq"})
operator_id: int | None = Field(default=None, description="操作人", json_schema_extra={"q": "eq"})
created_time: list[datetime] | None = Field(default=None, description="创建时间范围(_time 后缀由 search_to_dict 合并为 between)")
@@ -0,0 +1,124 @@
"""育种审计日志 模块服务层(§3.10;写操作全程可追溯)"""
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema, PageResultSchema
from app.core.exceptions import CustomException
from .crud import BreedingAuditCRUD
from .model import AuditLogModel
from .schema import AuditLogOutSchema, AuditLogQueryParam
# 供 Service 层在导入汇总/业务路径直接落审计行(不触发 CRUDBase 递归审计)
_ALLOWED_ACTIONS = {"CREATE", "UPDATE", "DELETE", "IMPORT"}
def _fmt(v: Any) -> str | None:
if v is None:
return None
s = str(v)
return s[:500] if len(s) > 500 else s
class AuditLogService:
"""审计日志 —— 只读查询 + 静态写入。写入走 CRUDBase 自动注入,不对外暴露增删改。"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
self.auth = auth
self.db = db
@staticmethod
async def write(
db: AsyncSession,
*,
entity_type: str,
entity_id: int | None,
action: str,
field_name: str | None = None,
old_value: Any = None,
new_value: Any = None,
created_id: int | None = None,
operator_id: int | None = None,
) -> int:
"""直接落一条审计行(不经 CRUDBase,防递归)。返回新行 id。"""
if action not in _ALLOWED_ACTIONS:
raise CustomException(msg=f"非法审计操作类型: {action}")
row = AuditLogModel(
entity_type=entity_type[:32] if entity_type else entity_type,
entity_id=entity_id,
action=action,
field_name=(field_name[:64] if field_name else field_name),
old_value=_fmt(old_value),
new_value=_fmt(new_value),
created_id=created_id,
operator_id=operator_id,
)
db.add(row)
await db.flush()
return int(row.id)
async def page(
self,
page_no: int,
page_size: int,
search: AuditLogQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> PageResultSchema[AuditLogOutSchema]:
from app.utils.common_util import search_to_dict
result = await BreedingAuditCRUD(self.auth, self.db).page(
offset=(page_no - 1) * page_size,
limit=page_size,
order_by=order_by or [{"id": "desc"}],
search=search_to_dict(search, {}),
out_schema=AuditLogOutSchema,
)
await self._attach_labels(result.items)
return result
async def get_list(self, search: AuditLogQueryParam | None = None) -> list[AuditLogOutSchema]:
from app.utils.common_util import search_to_dict
objs = await BreedingAuditCRUD(self.auth, self.db).get_list(
search=search_to_dict(search, {}), order_by=[{"id": "desc"}]
)
outs = [AuditLogOutSchema.model_validate(o) for o in objs]
await self._attach_labels(outs)
return outs
async def detail(self, id: int) -> AuditLogOutSchema:
obj = await BreedingAuditCRUD(self.auth, self.db).get(id=id)
if not obj:
raise CustomException(msg="该审计日志不存在")
out = AuditLogOutSchema.model_validate(obj)
await self._attach_labels([out])
return out
async def _attach_labels(self, items: list[AuditLogOutSchema]) -> None:
op_ids = {getattr(it, "operator_id") for it in items if getattr(it, "operator_id")}
if op_ids:
from app.api.v1.module_bre.personnel.model import PersonnelModel
rows = (await self.db.execute(
select(PersonnelModel.id, PersonnelModel.name).where(PersonnelModel.id.in_(op_ids))
)).all()
op_map = {r.id: r.name for r in rows}
for it in items:
it.operator_name = op_map.get(getattr(it, "operator_id"))
async def options(self) -> dict[str, Any]:
"""前端筛选下拉:实体类型 / 操作类型 去重集合。"""
etypes = (await self.db.execute(
select(AuditLogModel.entity_type)
.where(AuditLogModel.entity_type.isnot(None))
.distinct()
)).scalars().all()
actions = (await self.db.execute(
select(AuditLogModel.action)
.where(AuditLogModel.action.isnot(None))
.distinct()
)).scalars().all()
return {"entity_types": sorted(etypes), "actions": sorted(actions)}