""" 特例审批 API 创建、查询、审批特例记录 """ import uuid from datetime import datetime, timezone from fastapi import APIRouter, Depends, Header, HTTPException, status from sqlalchemy import select, and_ from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models.tenant import Tenant from app.models.risk_exception import ( RiskException, RiskTargetType as DBRiskTargetType, RiskExceptionStatus as DBRiskExceptionStatus, ) from app.schemas.review import ( RiskExceptionCreateRequest, RiskExceptionRecord, RiskExceptionStatus, RiskExceptionDecisionRequest, RiskTargetType, ) router = APIRouter(prefix="/risk-exceptions", tags=["risk-exceptions"]) async def _ensure_tenant_exists(tenant_id: str, db: AsyncSession) -> Tenant: """确保租户存在,不存在则自动创建""" result = await db.execute( select(Tenant).where(Tenant.id == tenant_id) ) tenant = result.scalar_one_or_none() if not tenant: tenant = Tenant(id=tenant_id, name=f"租户-{tenant_id}") db.add(tenant) await db.flush() return tenant def _exception_to_response(record: RiskException) -> RiskExceptionRecord: """将数据库模型转换为响应模型""" return RiskExceptionRecord( record_id=record.id, applicant_id=record.applicant_id, apply_time=record.apply_time, target_type=RiskTargetType(record.target_type.value), target_id=record.target_id, risk_rule_id=record.risk_rule_id, status=RiskExceptionStatus(record.status.value), valid_start_time=record.valid_start_time, valid_end_time=record.valid_end_time, reason_category=record.reason_category, justification=record.justification, attachment_url=record.attachment_url, current_approver_id=record.current_approver_id, approval_chain_log=record.approval_chain_log or [], auto_rejected=record.auto_rejected, rejection_reason=record.rejection_reason, last_status_at=record.last_status_at, ) @router.post("", response_model=RiskExceptionRecord, status_code=status.HTTP_201_CREATED) async def create_exception( request: RiskExceptionCreateRequest, x_tenant_id: str = Header(..., alias="X-Tenant-ID"), db: AsyncSession = Depends(get_db), ) -> RiskExceptionRecord: """创建特例申请""" # 确保租户存在 await _ensure_tenant_exists(x_tenant_id, db) record_id = f"exc-{uuid.uuid4().hex[:12]}" now = datetime.now(timezone.utc) record = RiskException( id=record_id, tenant_id=x_tenant_id, applicant_id=request.applicant_id, apply_time=now, target_type=DBRiskTargetType(request.target_type.value), target_id=request.target_id, risk_rule_id=request.risk_rule_id, status=DBRiskExceptionStatus.PENDING, valid_start_time=request.valid_start_time, valid_end_time=request.valid_end_time, reason_category=request.reason_category, justification=request.justification, attachment_url=request.attachment_url, current_approver_id=request.current_approver_id, approval_chain_log=[], auto_rejected=False, rejection_reason=None, last_status_at=now, ) db.add(record) await db.flush() await db.refresh(record) return _exception_to_response(record) @router.get("/{record_id}", response_model=RiskExceptionRecord) async def get_exception( record_id: str, x_tenant_id: str = Header(..., alias="X-Tenant-ID"), db: AsyncSession = Depends(get_db), ) -> RiskExceptionRecord: """查询特例记录""" result = await db.execute( select(RiskException).where( and_( RiskException.id == record_id, RiskException.tenant_id == x_tenant_id, ) ) ) record = result.scalar_one_or_none() if not record: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"特例记录不存在: {record_id}", ) return _exception_to_response(record) @router.post("/{record_id}/approve", response_model=RiskExceptionRecord) async def approve_exception( record_id: str, request: RiskExceptionDecisionRequest, x_tenant_id: str = Header(..., alias="X-Tenant-ID"), db: AsyncSession = Depends(get_db), ) -> RiskExceptionRecord: """审批通过""" result = await db.execute( select(RiskException).where( and_( RiskException.id == record_id, RiskException.tenant_id == x_tenant_id, ) ) ) record = result.scalar_one_or_none() if not record: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"特例记录不存在: {record_id}", ) now = datetime.now(timezone.utc) record.status = DBRiskExceptionStatus.APPROVED record.last_status_at = now # 更新审批日志 approval_log = record.approval_chain_log or [] approval_log.append({ "approver_id": request.approver_id, "action": "approve", "comment": request.comment, "timestamp": now.isoformat(), }) record.approval_chain_log = approval_log await db.flush() await db.refresh(record) return _exception_to_response(record) @router.post("/{record_id}/reject", response_model=RiskExceptionRecord) async def reject_exception( record_id: str, request: RiskExceptionDecisionRequest, x_tenant_id: str = Header(..., alias="X-Tenant-ID"), db: AsyncSession = Depends(get_db), ) -> RiskExceptionRecord: """驳回申请""" result = await db.execute( select(RiskException).where( and_( RiskException.id == record_id, RiskException.tenant_id == x_tenant_id, ) ) ) record = result.scalar_one_or_none() if not record: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"特例记录不存在: {record_id}", ) # 驳回必须填写原因 if not request.comment: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="驳回必须填写原因", ) now = datetime.now(timezone.utc) record.status = DBRiskExceptionStatus.REJECTED record.rejection_reason = request.comment record.last_status_at = now # 更新审批日志 approval_log = record.approval_chain_log or [] approval_log.append({ "approver_id": request.approver_id, "action": "reject", "comment": request.comment, "timestamp": now.isoformat(), }) record.approval_chain_log = approval_log await db.flush() await db.refresh(record) return _exception_to_response(record)