diff --git a/app/api/v1/module_energy/report_review/controller.py b/app/api/v1/module_energy/report_review/controller.py index 9b26531..a1f1718 100644 --- a/app/api/v1/module_energy/report_review/controller.py +++ b/app/api/v1/module_energy/report_review/controller.py @@ -5,18 +5,21 @@ from fastapi.responses import JSONResponse from app.common.response import ResponseSchema, SuccessResponse from app.core.base_params import PaginationQueryParam -from app.core.base_schema import AuthSchema +from app.core.base_schema import AuthSchema, PageResultSchema from app.core.dependencies import AuthPermission from app.core.router_class import OperationLogRoute from .schema import ( EnergyReportReviewCreateSchema, + EnergyReportReviewLogCreateSchema, + EnergyReportReviewLogOutSchema, + EnergyReportReviewLogQueryParam, EnergyReportReviewOutSchema, EnergyReportReviewPageOutSchema, EnergyReportReviewQueryParam, EnergyReportReviewUpdateSchema, ) -from .service import EnergyReportReviewService +from .service import EnergyReportReviewLogService, EnergyReportReviewService EnergyReportReviewRouter = APIRouter(route_class=OperationLogRoute, prefix="/report-review", tags=["能源报告审核"]) @@ -91,3 +94,61 @@ async def delete_energy_report_review_controller( ) -> JSONResponse: await EnergyReportReviewService(auth).delete(ids=ids) return SuccessResponse(msg="删除能源报告审核任务成功") + + +@EnergyReportReviewRouter.get( + "/log/list", + summary="查询能源报告审核操作记录列表", + response_model=ResponseSchema[PageResultSchema[EnergyReportReviewLogOutSchema]], +) +async def get_energy_report_review_log_list_controller( + page: Annotated[PaginationQueryParam, Depends()], + search: Annotated[EnergyReportReviewLogQueryParam, Depends()], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:report_review:query"]))], +) -> JSONResponse: + result = await EnergyReportReviewLogService(auth).page( + page_no=page.page_no, + page_size=page.page_size, + search=search, + order_by=page.order_by or [{"id": "desc"}], + ) + return SuccessResponse(data=result, msg="查询能源报告审核操作记录列表成功") + + +@EnergyReportReviewRouter.get( + "/log/detail/{id}", + summary="查询能源报告审核操作记录详情", + response_model=ResponseSchema[EnergyReportReviewLogOutSchema], +) +async def get_energy_report_review_log_detail_controller( + id: Annotated[int, Path(description="能源报告审核操作记录ID")], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:report_review:query"]))], +) -> JSONResponse: + result = await EnergyReportReviewLogService(auth).detail(id=id) + return SuccessResponse(data=result, msg="查询能源报告审核操作记录详情成功") + + +@EnergyReportReviewRouter.post( + "/log/create", + summary="创建能源报告审核操作记录", + response_model=ResponseSchema[EnergyReportReviewLogOutSchema], +) +async def create_energy_report_review_log_controller( + data: EnergyReportReviewLogCreateSchema, + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:report_review:update"]))], +) -> JSONResponse: + result = await EnergyReportReviewLogService(auth).create(data=data) + return SuccessResponse(data=result, msg="创建能源报告审核操作记录成功") + + +@EnergyReportReviewRouter.delete( + "/log/delete", + summary="删除能源报告审核操作记录", + response_model=ResponseSchema[None], +) +async def delete_energy_report_review_log_controller( + ids: Annotated[list[int], Body(description="ID列表")], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:report_review:delete"]))], +) -> JSONResponse: + await EnergyReportReviewLogService(auth).delete(ids=ids) + return SuccessResponse(msg="删除能源报告审核操作记录成功") diff --git a/app/api/v1/module_energy/report_review/crud.py b/app/api/v1/module_energy/report_review/crud.py index 7ce5814..7907977 100644 --- a/app/api/v1/module_energy/report_review/crud.py +++ b/app/api/v1/module_energy/report_review/crud.py @@ -1,8 +1,12 @@ from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema -from .model import EnergyReportReviewModel -from .schema import EnergyReportReviewCreateSchema, EnergyReportReviewUpdateSchema +from .model import EnergyReportReviewLogModel, EnergyReportReviewModel +from .schema import ( + EnergyReportReviewCreateSchema, + EnergyReportReviewLogCreateSchema, + EnergyReportReviewUpdateSchema, +) class EnergyReportReviewCRUD(CRUDBase[EnergyReportReviewModel, EnergyReportReviewCreateSchema, EnergyReportReviewUpdateSchema]): @@ -10,3 +14,12 @@ class EnergyReportReviewCRUD(CRUDBase[EnergyReportReviewModel, EnergyReportRevie def __init__(self, auth: AuthSchema) -> None: super().__init__(model=EnergyReportReviewModel, auth=auth) + + +class EnergyReportReviewLogCRUD( + CRUDBase[EnergyReportReviewLogModel, EnergyReportReviewLogCreateSchema, EnergyReportReviewLogCreateSchema] +): + """能源报告审核操作记录数据层。""" + + def __init__(self, auth: AuthSchema) -> None: + super().__init__(model=EnergyReportReviewLogModel, auth=auth) diff --git a/app/api/v1/module_energy/report_review/model.py b/app/api/v1/module_energy/report_review/model.py index 7291eb8..529abef 100644 --- a/app/api/v1/module_energy/report_review/model.py +++ b/app/api/v1/module_energy/report_review/model.py @@ -47,3 +47,36 @@ class EnergyReportReviewModel(ModelMixin, TenantMixin, UserMixin): template = relationship("EnergyReportTemplateModel", lazy="selectin", foreign_keys=[template_id], uselist=False) file = relationship("FileModel", lazy="selectin", foreign_keys=[file_id], uselist=False) reviewer = relationship("UserModel", lazy="selectin", foreign_keys=[reviewer_id], uselist=False) + + +class EnergyReportReviewLogModel(ModelMixin, TenantMixin, UserMixin): + """能源报告审核操作记录模型。""" + + __tablename__: str = "energy_report_review_log" + __table_args__: dict[str, str] = {"comment": "能源报告审核操作记录表"} + __loader_options__: list[str] = [ + "review", + "created_by", + "updated_by", + "deleted_by", + "tenant_by", + ] + + report_review_id: Mapped[int] = mapped_column( + Integer, + ForeignKey("energy_report_review.id", ondelete="CASCADE", onupdate="CASCADE"), + nullable=False, + index=True, + comment="能源报告审核任务ID", + ) + task_name: Mapped[str] = mapped_column(String(128), nullable=False, comment="任务名称快照", index=True) + action_type: Mapped[str] = mapped_column( + String(32), + nullable=False, + index=True, + comment="操作类型:create_task/start_review/review_pass/review_reject", + ) + action_name: Mapped[str] = mapped_column(String(64), nullable=False, comment="操作名称") + description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="操作描述") + + review = relationship("EnergyReportReviewModel", lazy="selectin", foreign_keys=[report_review_id], uselist=False) diff --git a/app/api/v1/module_energy/report_review/schema.py b/app/api/v1/module_energy/report_review/schema.py index 80dd99f..b0994c0 100644 --- a/app/api/v1/module_energy/report_review/schema.py +++ b/app/api/v1/module_energy/report_review/schema.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Literal from fastapi import Query from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -92,3 +93,44 @@ class EnergyReportReviewQueryParam(BaseQueryParam, UserByQueryParam, TenantByQue self.template_id = (QueueEnum.eq.value, self.template_id) if isinstance(self.reviewer_id, int): self.reviewer_id = (QueueEnum.eq.value, self.reviewer_id) + + +ReportReviewLogActionType = Literal["create_task", "start_review", "review_pass", "review_reject"] + + +class EnergyReportReviewLogCreateSchema(BaseModel): + """能源报告审核操作记录创建模型。""" + + report_review_id: int = Field(..., ge=1, description="能源报告审核任务ID") + action_type: ReportReviewLogActionType = Field(..., description="操作类型") + description: str | None = Field(default=None, max_length=500, description="操作描述") + task_name: str = Field(default="", description="任务名称,由后端填充") + action_name: str = Field(default="", description="操作名称,由后端填充") + + +class EnergyReportReviewLogOutSchema(EnergyReportReviewLogCreateSchema, BaseSchema, UserBySchema, TenantBySchema): + """能源报告审核操作记录响应模型。""" + + model_config = ConfigDict(from_attributes=True) + + task_name: str = Field(description="任务名称") + action_name: str = Field(description="操作名称") + operator_id: int | None = Field(default=None, description="操作人ID") + operator_name: str | None = Field(default=None, description="操作人名称") + + +@dataclass +class EnergyReportReviewLogQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam): + """能源报告审核操作记录查询参数。""" + + report_review_id: int | None = Query(None, ge=1, description="能源报告审核任务ID") + task_name: str | None = Query(None, description="任务名称") + action_type: str | None = Query(None, description="操作类型") + + def __post_init__(self) -> None: + if isinstance(self.report_review_id, int): + self.report_review_id = (QueueEnum.eq.value, self.report_review_id) + if self.task_name: + self.task_name = (QueueEnum.like.value, self.task_name) + if self.action_type: + self.action_type = (QueueEnum.eq.value, self.action_type) diff --git a/app/api/v1/module_energy/report_review/service.py b/app/api/v1/module_energy/report_review/service.py index e44d7a1..984c363 100644 --- a/app/api/v1/module_energy/report_review/service.py +++ b/app/api/v1/module_energy/report_review/service.py @@ -1,12 +1,15 @@ from app.api.v1.module_common.file.crud import FileCRUD from app.api.v1.module_energy.report_template.crud import EnergyReportTemplateCRUD from app.api.v1.module_system.user.crud import UserCRUD -from app.core.base_schema import AuthSchema +from app.core.base_schema import AuthSchema, PageResultSchema from app.core.exceptions import CustomException -from .crud import EnergyReportReviewCRUD +from .crud import EnergyReportReviewCRUD, EnergyReportReviewLogCRUD from .schema import ( EnergyReportReviewCreateSchema, + EnergyReportReviewLogCreateSchema, + EnergyReportReviewLogOutSchema, + EnergyReportReviewLogQueryParam, EnergyReportReviewOutSchema, EnergyReportReviewQueryParam, EnergyReportReviewStatsSchema, @@ -15,6 +18,13 @@ from .schema import ( ENERGY_REPORT_REVIEW_BUSINESS_TYPE = "energy_report_review" +REVIEW_LOG_ACTION_NAMES = { + "create_task": "创建审查任务", + "start_review": "开始审查", + "review_pass": "审核通过", + "review_reject": "审核不通过", +} + class EnergyReportReviewService: """能源报告审核任务服务。""" @@ -59,6 +69,12 @@ class EnergyReportReviewService: await self._validate_reviewer(reviewer_id=data.reviewer_id) review = await EnergyReportReviewCRUD(self.auth).create(data=data) await self._bind_file(file_id=data.file_id, review_id=review.id) + await EnergyReportReviewLogService(self.auth).create_system_log( + report_review_id=review.id, + task_name=review.task_name, + action_type="create_task", + description=f"创建审查任务:{review.task_name}", + ) return self._to_out_schema(review) async def update(self, id: int, data: EnergyReportReviewUpdateSchema) -> EnergyReportReviewOutSchema: @@ -154,3 +170,83 @@ class EnergyReportReviewService: return if file_record.business_type == ENERGY_REPORT_REVIEW_BUSINESS_TYPE and file_record.business_id == review_id: await FileCRUD(self.auth).set(ids=[file_id], business_id=None) + + +class EnergyReportReviewLogService: + """能源报告审核操作记录服务。""" + + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + async def detail(self, id: int) -> EnergyReportReviewLogOutSchema: + log = await EnergyReportReviewLogCRUD(self.auth).get_or_404(id=id, msg="操作记录不存在") + return self._to_out_schema(log) + + async def page( + self, + page_no: int, + page_size: int, + search: EnergyReportReviewLogQueryParam | None = None, + order_by: list[dict[str, str]] | None = None, + ) -> PageResultSchema[EnergyReportReviewLogOutSchema]: + search_dict = vars(search) if search else {} + page_result = await EnergyReportReviewLogCRUD(self.auth).page( + offset=(page_no - 1) * page_size, + limit=page_size, + order_by=order_by or [{"id": "desc"}], + search=search_dict, + out_schema=EnergyReportReviewLogOutSchema, + ) + page_result.items = [self._fill_out_dict(item) for item in page_result.items] + return page_result + + async def create(self, data: EnergyReportReviewLogCreateSchema) -> EnergyReportReviewLogOutSchema: + review = await EnergyReportReviewCRUD(self.auth).get_or_404( + id=data.report_review_id, + msg="新增操作记录失败,能源报告审核任务不存在", + ) + log = await self.create_system_log( + report_review_id=review.id, + task_name=review.task_name, + action_type=data.action_type, + description=data.description or self._default_description(review.task_name, data.action_type), + ) + return self._to_out_schema(log) + + async def delete(self, ids: list[int]) -> None: + if len(ids) < 1: + raise CustomException(msg="删除失败,删除对象不能为空") + await EnergyReportReviewLogCRUD(self.auth).delete(ids=ids) + + async def create_system_log(self, report_review_id: int, task_name: str, action_type: str, description: str | None = None): + action_name = REVIEW_LOG_ACTION_NAMES.get(action_type) + if not action_name: + raise CustomException(msg="操作类型不支持") + return await EnergyReportReviewLogCRUD(self.auth).create( + data=EnergyReportReviewLogCreateSchema( + report_review_id=report_review_id, + action_type=action_type, # type: ignore[arg-type] + description=description, + task_name=task_name, + action_name=action_name, + ), + ) + + def _to_out_schema(self, log) -> EnergyReportReviewLogOutSchema: + log_out = EnergyReportReviewLogOutSchema.model_validate(log) + log_out.operator_id = log_out.created_id + if getattr(log, "created_by", None): + log_out.operator_name = log.created_by.name + log_out.action_name = REVIEW_LOG_ACTION_NAMES.get(log.action_type, log.action_type) + return log_out + + def _fill_out_dict(self, item: dict) -> dict: + created_by = item.get("created_by") or {} + item["operator_id"] = item.get("created_id") + item["operator_name"] = created_by.get("name") + item["action_name"] = REVIEW_LOG_ACTION_NAMES.get(item.get("action_type"), item.get("action_name")) + return item + + def _default_description(self, task_name: str, action_type: str) -> str: + action_name = REVIEW_LOG_ACTION_NAMES.get(action_type, action_type) + return f"{action_name}:{task_name}" diff --git a/app/api/v1/module_energy/report_template/service.py b/app/api/v1/module_energy/report_template/service.py index 4d90633..c570e10 100644 --- a/app/api/v1/module_energy/report_template/service.py +++ b/app/api/v1/module_energy/report_template/service.py @@ -1,8 +1,10 @@ -from app.api.v1.module_common.file.crud import FileCRUD +from app.api.v1.module_common.file.crud import FileCRUD from app.core.base_schema import AuthSchema from app.core.exceptions import CustomException +from sqlalchemy import select from .crud import EnergyReportTemplateCRUD +from .model import EnergyReportTemplateModel from .schema import ( EnergyReportTemplateCreateSchema, EnergyReportTemplateOutSchema, @@ -14,7 +16,7 @@ ENERGY_REPORT_TEMPLATE_BUSINESS_TYPE = "energy_report_template" class EnergyReportTemplateService: - """能源报告模板服务。""" + """鑳芥簮鎶ュ憡妯℃澘鏈嶅姟銆?"" def __init__(self, auth: AuthSchema) -> None: self.auth = auth @@ -53,7 +55,7 @@ class EnergyReportTemplateService: return self._to_out_schema(template) async def update(self, id: int, data: EnergyReportTemplateUpdateSchema) -> EnergyReportTemplateOutSchema: - old_template = await EnergyReportTemplateCRUD(self.auth).get_or_404(id=id, msg="更新失败,能源报告模板不存在") + old_template = await EnergyReportTemplateCRUD(self.auth).get_or_404(id=id, msg="鏇存柊澶辫触锛岃兘婧愭姤鍛婃ā鏉夸笉瀛樺湪") await self._validate_unique_name(template_name=data.template_name, exclude_id=id) await self._validate_file(file_id=data.file_id, current_template_id=id) template = await EnergyReportTemplateCRUD(self.auth).update(id=id, data=data) @@ -64,12 +66,12 @@ class EnergyReportTemplateService: async def delete(self, ids: list[int]) -> None: if len(ids) < 1: - raise CustomException(msg="删除失败,删除对象不能为空") + raise CustomException(msg="鍒犻櫎澶辫触锛屽垹闄ゅ璞′笉鑳戒负绌?) templates = await EnergyReportTemplateCRUD(self.auth).get_list(search={"id": ("in", ids)}) template_map = {template.id: template for template in templates} for template_id in ids: if template_id not in template_map: - raise CustomException(msg="删除失败,能源报告模板不存在") + raise CustomException(msg="鍒犻櫎澶辫触锛岃兘婧愭姤鍛婃ā鏉夸笉瀛樺湪") for template in templates: await self._unbind_file(file_id=template.file_id, template_id=template.id) await EnergyReportTemplateCRUD(self.auth).delete(ids=ids) @@ -85,16 +87,16 @@ class EnergyReportTemplateService: async def _validate_unique_name(self, template_name: str, exclude_id: int | None = None) -> None: template = await EnergyReportTemplateCRUD(self.auth).get(template_name=template_name) if template and template.id != exclude_id: - raise CustomException(msg="能源报告模板名称已存在") + raise CustomException(msg="鑳芥簮鎶ュ憡妯℃澘鍚嶇О宸插瓨鍦?) async def _validate_file(self, file_id: int, current_template_id: int | None = None) -> None: file_record = await FileCRUD(self.auth).get(id=file_id) if not file_record: - raise CustomException(msg="文件记录不存在") + raise CustomException(msg="鏂囦欢璁板綍涓嶅瓨鍦?) if file_record.business_type and file_record.business_type != ENERGY_REPORT_TEMPLATE_BUSINESS_TYPE: - raise CustomException(msg="文件已绑定到其他业务类型") + raise CustomException(msg="鏂囦欢宸茬粦瀹氬埌鍏朵粬涓氬姟绫诲瀷") if file_record.business_id and file_record.business_id != current_template_id: - raise CustomException(msg="文件已绑定到其他能源报告模板") + raise CustomException(msg="鏂囦欢宸茬粦瀹氬埌鍏朵粬鑳芥簮鎶ュ憡妯℃澘") async def _bind_file(self, file_id: int, template_id: int) -> None: await FileCRUD(self.auth).set( diff --git a/app/scripts/initialize.py b/app/scripts/initialize.py index 921fea3..65e14c4 100644 --- a/app/scripts/initialize.py +++ b/app/scripts/initialize.py @@ -16,7 +16,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.api.v1.module_common.file.model import FileModel from app.api.v1.module_energy.chapter.model import EnergyReportChapterModel -from app.api.v1.module_energy.report_review.model import EnergyReportReviewModel +from app.api.v1.module_energy.report_review.model import EnergyReportReviewLogModel, EnergyReportReviewModel from app.api.v1.module_energy.report_template.model import EnergyReportTemplateModel from app.api.v1.module_energy.variable.model import EnergyReportVariableModel from app.api.v1.module_platform.email.model import EmailConfigModel, EmailTemplateModel @@ -86,6 +86,7 @@ class InitializeData: EnergyReportChapterModel, EnergyReportVariableModel, EnergyReportReviewModel, + EnergyReportReviewLogModel, # ── 日志表(追加写入) ── LoginLogModel, OperationLogModel,