172 lines
7.1 KiB
Python
172 lines
7.1 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,
|
|
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)
|
|
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,
|
|
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)
|