init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
@@ -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)}
|
||||
Reference in New Issue
Block a user