2026-09-04 10:20:40 +08:00

167 lines
6.8 KiB
Python

"""Risk item submission, pre-review, and approval workflow."""
from datetime import UTC, datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.exceptions import AppException
from app.models.entities import AnomalyCase, OperationAudit, ReviewRecord, RiskItem, UserAccount
from app.schemas.risk_item import ReviewRecordData, RiskItemCreate, RiskItemData, RiskItemUpdate
class RiskItemService:
def __init__(self, session: AsyncSession) -> None:
self.session = session
async def submit(self, payload: RiskItemCreate, user: UserAccount) -> RiskItemData:
pre_result, pre_note = self._pre_review(payload)
item = RiskItem(
source_type=payload.source_type,
source_file_id=payload.source_file_id,
product_name=payload.product_name,
product_category=payload.product_category,
supplier=payload.supplier,
factory=payload.factory,
stage_new=payload.stage_new,
description=payload.description,
defect_level=payload.defect_level,
solution=payload.solution,
photos=payload.photos,
submitted_by=user.emp_no,
submitted_at=datetime.now(UTC),
status="待审核" if pre_result == "通过" else "待预审",
)
self.session.add(item)
await self.session.flush()
self.session.add(
ReviewRecord(
risk_item_id=item.id,
pre_review_result=pre_result,
pre_review_note=pre_note,
)
)
self.session.add(
OperationAudit(
actor_user_id=user.id,
actor_emp_no=user.emp_no,
action="risk_item.submit",
resource_type="risk_item",
resource_id=str(item.id),
audit_metadata={"source_type": payload.source_type, "pre_review": pre_result},
)
)
await self.session.flush()
return self.to_data(item)
async def list_pending(
self,
limit: int = 100,
*,
source_type: str = "all",
status: str = "待审核",
) -> list[RiskItemData]:
filters = []
if status != "all":
filters.append(RiskItem.status == status)
if source_type != "all":
filters.append(RiskItem.source_type == source_type)
statement = select(RiskItem).where(*filters).order_by(RiskItem.created_at.desc()).limit(limit)
return [self.to_data(item) for item in await self.session.scalars(statement)]
async def update(self, item_id: int, payload: RiskItemUpdate, editor: UserAccount) -> RiskItemData:
item = await self.session.get(RiskItem, item_id)
if item is None:
raise AppException("风险项不存在", code=404, status_code=404)
if item.status not in {"待审核", "待预审"}:
raise AppException("只有待处理风险项可以编辑", code=409, status_code=409)
changes = payload.model_dump(exclude_unset=True)
if "defect_level" in changes and changes["defect_level"] not in {None, "A", "B", "C"}:
raise AppException("缺陷等级只能为 A、B 或 C", code=422, status_code=422)
for key, value in changes.items():
setattr(item, key, value)
if item.status == "待预审" and len((item.description or "").strip()) >= 5:
item.status = "待审核"
record = await self.session.scalar(
select(ReviewRecord).where(ReviewRecord.risk_item_id == item_id).order_by(ReviewRecord.id.desc())
)
if record:
record.pre_review_result = "通过"
record.pre_review_note = None
self.session.add(
OperationAudit(
actor_user_id=editor.id,
actor_emp_no=editor.emp_no,
action="risk_item.edit",
resource_type="risk_item",
resource_id=str(item.id),
audit_metadata={"fields": list(changes)},
)
)
await self.session.flush()
return self.to_data(item)
async def review(self, item_id: int, action: str, note: str, reviewer: UserAccount) -> ReviewRecordData:
item = await self.session.get(RiskItem, item_id)
if item is None:
raise AppException("风险项不存在", code=404, status_code=404)
if item.status != "待审核":
raise AppException("该风险项当前不可审核", code=409, status_code=409)
if item.submitted_by == reviewer.emp_no:
raise AppException("审核人不能审核自己提交的风险项", code=403, status_code=403)
record = await self.session.scalar(
select(ReviewRecord)
.where(ReviewRecord.risk_item_id == item_id)
.order_by(ReviewRecord.id.desc())
)
if record is None:
raise AppException("审核记录不存在", code=409, status_code=409)
record.reviewer = reviewer.emp_no
record.review_action = action
record.review_note = note
record.reviewed_at = datetime.now(UTC)
if action == "通过":
case = AnomalyCase(
product_name=item.product_name,
product_category=item.product_category,
supplier=item.supplier,
factory=item.factory,
stage_new=item.stage_new,
description=item.description,
defect_level=item.defect_level,
solution=item.solution,
source_type=item.source_type,
import_fingerprint=f"risk-item-{item.id}",
)
self.session.add(case)
await self.session.flush()
record.case_id = case.id
item.status = "已通过"
else:
item.status = "已驳回"
self.session.add(
OperationAudit(
actor_user_id=reviewer.id,
actor_emp_no=reviewer.emp_no,
action="risk_item.review",
resource_type="risk_item",
resource_id=str(item.id),
audit_metadata={"review_action": action},
)
)
await self.session.flush()
return ReviewRecordData.model_validate(record, from_attributes=True)
@staticmethod
def _pre_review(payload: RiskItemCreate) -> tuple[str, str | None]:
if len(payload.description.strip()) < 5:
return "打回", "异常描述不能少于 5 个字符"
if payload.defect_level and payload.defect_level not in {"A", "B", "C"}:
return "打回", "缺陷等级只能为 A、B 或 C"
return "通过", None
@staticmethod
def to_data(item: RiskItem) -> RiskItemData:
return RiskItemData.model_validate(item, from_attributes=True)