"""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, occurred_on=payload.occurred_on, product_name=payload.product_name, product_category=payload.product_category, material_type=payload.material_type, 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, recognition_confidence=payload.recognition_confidence, ocr_payload=payload.ocr_payload, 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) else: filters.append(RiskItem.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 withdraw(self, item_id: int, operator: UserAccount) -> None: 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) previous_status = item.status item.status = "已撤回" self.session.add( OperationAudit( actor_user_id=operator.id, actor_emp_no=operator.emp_no, action="risk_item.withdraw", resource_type="risk_item", resource_id=str(item.id), audit_metadata={"previous_status": previous_status}, ) ) await self.session.flush() 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, date=item.occurred_on, product_category=item.product_category, material_type=item.material_type, 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, source_file=item.source_file_id, 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)