You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

308 lines
15 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

from app.api.v1.module_common.file.crud import FileCRUD
from app.api.v1.module_common.file.service import FileService
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, PageResultSchema
from app.core.exceptions import CustomException
from .crud import EnergyReportReviewCRUD, EnergyReportReviewLogCRUD
from .schema import (
EnergyReportReviewCreateSchema,
EnergyReportReviewLogCreateSchema,
EnergyReportReviewLogOutSchema,
EnergyReportReviewLogQueryParam,
EnergyReportReviewOutSchema,
EnergyReportReviewQueryParam,
EnergyReportReviewStatsSchema,
EnergyReportReviewUpdateSchema,
)
ENERGY_REPORT_REVIEW_BUSINESS_TYPE = "energy_report_review"
REVIEW_LOG_ACTION_NAMES = {
"create_task": "创建审查任务",
"start_review": "开始审查",
"review_pass": "已审查",
"review_reject": "审查失败",
}
class EnergyReportReviewService:
"""能源报告审核任务服务。"""
def __init__(self, auth: AuthSchema) -> None:
self.auth = auth
async def detail(self, id: int) -> EnergyReportReviewOutSchema:
review = await EnergyReportReviewCRUD(self.auth).get_or_404(id=id)
return self._to_out_schema(review)
async def page(
self,
page_no: int,
page_size: int,
search: EnergyReportReviewQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> dict:
search_dict = vars(search) if search else None
stats = await self._stats(search_dict=self._build_stats_search(search_dict))
offset = (page_no - 1) * page_size
page_result = await EnergyReportReviewCRUD(self.auth).page(
offset=offset,
limit=page_size,
order_by=order_by or [{"id": "desc"}],
search=search_dict,
out_schema=EnergyReportReviewOutSchema,
)
items = [self._fill_out_dict(item) for item in page_result.items]
return {
"page_no": page_result.page_no,
"page_size": page_result.page_size,
"total": page_result.total,
"has_next": page_result.has_next,
"stats": stats.model_dump(),
"items": items,
}
async def create(self, data: EnergyReportReviewCreateSchema) -> EnergyReportReviewOutSchema:
await self._validate_template(template_id=data.template_id, report_type=data.report_type)
await self._validate_file(file_id=data.file_id)
attachment_file = await self._validate_attachment_file(
attachment_file_id=data.attachment_file_id,
file_id=data.file_id,
)
await self._validate_reviewer(reviewer_id=data.reviewer_id)
create_data = data.model_dump()
create_data["attachment_url"] = attachment_file.file_url if attachment_file else None
review = await EnergyReportReviewCRUD(self.auth).create(data=create_data)
await self._bind_file(file_id=data.file_id, review_id=review.id)
if data.attachment_file_id:
await self._bind_file(file_id=data.attachment_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:
old_review = await EnergyReportReviewCRUD(self.auth).get_or_404(id=id, msg="更新失败,能源报告审核任务不存在")
await self._validate_template(template_id=data.template_id, report_type=data.report_type)
await self._validate_file(file_id=data.file_id, current_review_id=id)
if old_review.attachment_file_id and old_review.attachment_file_id == data.file_id:
raise CustomException(msg="更新失败,上传文档不能使用原附件文件")
if old_review.file_id == data.attachment_file_id:
raise CustomException(msg="更新失败,附件不能使用原审核文档文件")
attachment_file = await self._validate_attachment_file(
attachment_file_id=data.attachment_file_id,
file_id=data.file_id,
current_review_id=id,
)
await self._validate_reviewer(reviewer_id=data.reviewer_id)
update_data = data.model_dump(exclude_unset=True)
update_data["attachment_url"] = attachment_file.file_url if attachment_file else None
review = await EnergyReportReviewCRUD(self.auth).update(id=id, data=update_data)
if old_review.file_id != data.file_id:
await self._delete_file_record(file_id=old_review.file_id)
await self._bind_file(file_id=data.file_id, review_id=id)
if data.attachment_file_id:
await self._bind_file(file_id=data.attachment_file_id, review_id=id)
if old_review.attachment_file_id and old_review.attachment_file_id != data.attachment_file_id:
await self._delete_file_record(file_id=old_review.attachment_file_id)
return self._to_out_schema(review)
async def delete(self, ids: list[int]) -> None:
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
reviews = await EnergyReportReviewCRUD(self.auth).get_list(search={"id": ("in", ids)})
review_map = {review.id: review for review in reviews}
for review_id in ids:
if review_id not in review_map:
raise CustomException(msg="删除失败,能源报告审核任务不存在")
for review in reviews:
await self._unbind_file(file_id=review.file_id, review_id=review.id)
await EnergyReportReviewCRUD(self.auth).delete(ids=ids)
async def _stats(self, search_dict: dict | None = None) -> EnergyReportReviewStatsSchema:
records = await EnergyReportReviewCRUD(self.auth).get_list(search=search_dict or {})
return EnergyReportReviewStatsSchema(
submitted_count=len(records),
reviewing_count=len([record for record in records if record.status == 1]),
reviewed_count=len([record for record in records if record.status == 2]),
pending_count=len([record for record in records if record.status == 0]),
failed_count=len([record for record in records if record.status == 3]),
)
def _build_stats_search(self, search_dict: dict | None = None) -> dict:
if not search_dict:
return {}
stats_keys = {"created_time", "updated_time", "tenant_id", "created_id", "updated_id"}
return {key: value for key, value in search_dict.items() if key in stats_keys and value is not None}
def _to_out_schema(self, review) -> EnergyReportReviewOutSchema:
review_out = EnergyReportReviewOutSchema.model_validate(review)
if review_out.template:
review_out.template_name = review_out.template.template_name
if review_out.file:
review_out.file_url = review_out.file.file_url
review_out.original_name = review_out.file.original_name
if review_out.attachment_file:
review_out.attachment_url = review_out.attachment_file.file_url
review_out.attachment_original_name = review_out.attachment_file.original_name
if hasattr(review, "reviewer") and review.reviewer:
review_out.reviewer_name = review.reviewer.name
return review_out
def _fill_out_dict(self, item: dict) -> dict:
template_info = item.get("template") or {}
file_info = item.get("file") or {}
attachment_file_info = item.get("attachment_file") or {}
reviewer_info = item.get("reviewer") or {}
item["template_name"] = template_info.get("template_name")
item["file_url"] = file_info.get("file_url")
item["original_name"] = file_info.get("original_name")
item["attachment_url"] = attachment_file_info.get("file_url") or item.get("attachment_url")
item["attachment_original_name"] = attachment_file_info.get("original_name")
item["reviewer_name"] = reviewer_info.get("name")
return item
async def _validate_template(self, template_id: int, report_type: int) -> None:
template = await EnergyReportTemplateCRUD(self.auth).get(id=template_id)
if not template:
raise CustomException(msg="能源报告模板不存在")
if template.report_type != report_type:
raise CustomException(msg="报告类型与模板类型不一致")
async def _validate_file(self, file_id: int, current_review_id: int | None = None) -> None:
file_record = await FileCRUD(self.auth).get(id=file_id)
if not file_record:
raise CustomException(msg="文件记录不存在")
if file_record.business_type and file_record.business_type != ENERGY_REPORT_REVIEW_BUSINESS_TYPE:
raise CustomException(msg="文件已绑定到其他业务类型")
if file_record.business_id and file_record.business_id != current_review_id:
raise CustomException(msg="文件已绑定到其他能源报告审核任务")
async def _validate_attachment_file(
self,
attachment_file_id: int | None,
file_id: int,
current_review_id: int | None = None,
):
if attachment_file_id is None:
return None
if attachment_file_id == file_id:
raise CustomException(msg="附件文件不能和上传文档相同")
file_record = await FileCRUD(self.auth).get(id=attachment_file_id)
if not file_record:
raise CustomException(msg="附件文件记录不存在")
if file_record.business_type and file_record.business_type != ENERGY_REPORT_REVIEW_BUSINESS_TYPE:
raise CustomException(msg="附件文件已绑定到其他业务类型")
if file_record.business_id and file_record.business_id != current_review_id:
raise CustomException(msg="附件文件已绑定到其他能源报告审核任务")
return file_record
async def _validate_reviewer(self, reviewer_id: int | None) -> None:
if reviewer_id is None:
return
reviewer = await UserCRUD(self.auth).get(id=reviewer_id)
if not reviewer:
raise CustomException(msg="审查人不存在")
async def _bind_file(self, file_id: int, review_id: int) -> None:
await FileCRUD(self.auth).set(
ids=[file_id],
business_type=ENERGY_REPORT_REVIEW_BUSINESS_TYPE,
business_id=review_id,
)
async def _unbind_file(self, file_id: int, review_id: int) -> None:
file_record = await FileCRUD(self.auth).get(id=file_id)
if not file_record:
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)
async def _delete_file_record(self, file_id: int) -> None:
await FileService.delete_service(auth=self.auth, ids=[file_id], delete_object=True)
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}"