from __future__ import annotations import json from uuid import uuid4 from sqlalchemy.orm import Session from app.core.constants import ( APPROVAL_STATUS_APPROVED, APPROVAL_STATUS_PENDING, APPROVAL_STATUS_REJECTED, DECISION_APPROVED, DECISION_REJECTED, ERROR_CODE_CONFLICT, ERROR_CODE_NOT_FOUND, TASK_STATUS_CANCELLED, TASK_STATUS_PENDING_APPROVAL, TASK_STATUS_RUNNING, ) from app.core.time import format_now from app.models.audit_log import AuditLog from app.models.approval import ApprovalRequest from app.repositories.audit_repository import AuditRepository from app.repositories.approval_repository import ApprovalRepository from app.repositories.task_repository import TaskRepository from app.schemas.approval import ApprovalDecisionRequest, CreateApprovalRequest class ApprovalConflictError(Exception): code = ERROR_CODE_CONFLICT class ApprovalNotFoundError(Exception): code = ERROR_CODE_NOT_FOUND class ApprovalService: def __init__(self, db: Session, timezone_name: str) -> None: self.db = db self.timezone_name = timezone_name self.repository = ApprovalRepository(db) self.audit_repository = AuditRepository(db) self.task_repository = TaskRepository(db) def create_request(self, payload: CreateApprovalRequest) -> ApprovalRequest: current_time = format_now(self.timezone_name) approval = ApprovalRequest( approval_id=f"ap-{uuid4().hex[:12]}", task_id=payload.task_id, approval_status=APPROVAL_STATUS_PENDING, risk_level=payload.risk_level, operator_user_id=payload.operator.user_id, operator_user_name=payload.operator.user_name, approver_user_ids_json=json.dumps(payload.approvers, ensure_ascii=False), reason=payload.reason, created_at=current_time, updated_at=current_time, ) created_approval = self.repository.add(approval) self._write_audit_log( task_id=payload.task_id, request_id=None, action="APPROVAL_REQUEST_CREATED", result="PENDING", target=payload.target.app_code, operator_user_id=payload.operator.user_id, operator_user_name=payload.operator.user_name, detail={ "approval_id": created_approval.approval_id, "approvers": payload.approvers, "reason": payload.reason, }, ) return created_approval def get_request(self, approval_id: str) -> ApprovalRequest: approval = self.repository.get_by_approval_id(approval_id) if not approval: raise ApprovalNotFoundError() return approval def list_pending(self, approver_user_id: str | None = None) -> list[ApprovalRequest]: return self.repository.list_pending(approver_user_id) def decide(self, approval_id: str, payload: ApprovalDecisionRequest, request_id: str | None = None) -> ApprovalRequest: approval = self.repository.get_by_approval_id(approval_id) if not approval: raise ApprovalNotFoundError() if approval.approval_status != APPROVAL_STATUS_PENDING: raise ApprovalConflictError("当前审批单状态不允许重复决策。") task = self.task_repository.get_by_task_id(approval.task_id) if not task: raise ApprovalConflictError("审批关联任务不存在,无法继续决策。") if task.task_status != TASK_STATUS_PENDING_APPROVAL or task.approval_status != APPROVAL_STATUS_PENDING: raise ApprovalConflictError("审批关联任务已不处于待审批状态。") current_time = format_now(self.timezone_name) if payload.decision == DECISION_APPROVED: approval.approval_status = APPROVAL_STATUS_APPROVED self._update_task_after_decision(approval.task_id, TASK_STATUS_RUNNING, APPROVAL_STATUS_APPROVED, request_id=request_id) elif payload.decision == DECISION_REJECTED: approval.approval_status = APPROVAL_STATUS_REJECTED self._update_task_after_decision(approval.task_id, TASK_STATUS_CANCELLED, APPROVAL_STATUS_REJECTED, request_id=request_id) else: raise ApprovalConflictError("decision 仅支持 APPROVED 或 REJECTED。") approval.updated_at = current_time updated_approval = self.repository.update(approval) self._write_audit_log( task_id=updated_approval.task_id, request_id=request_id, action="APPROVAL_DECISION", result=updated_approval.approval_status, target=payload.operator.user_name, operator_user_id=payload.operator.user_id, operator_user_name=payload.operator.user_name, detail={"approval_id": updated_approval.approval_id, "comment": payload.comment}, ) return updated_approval def _update_task_after_decision(self, task_id: str, task_status: str, approval_status: str, request_id: str | None = None) -> None: task = self.task_repository.get_by_task_id(task_id) if not task: raise ApprovalConflictError("审批关联任务不存在,无法更新任务状态。") if task.task_status != TASK_STATUS_PENDING_APPROVAL or task.approval_status != APPROVAL_STATUS_PENDING: raise ApprovalConflictError("审批关联任务已不处于待审批状态。") task.task_status = task_status task.approval_status = approval_status task.updated_at = format_now(self.timezone_name) if approval_status == APPROVAL_STATUS_APPROVED: task.summary = "审批已通过,准备调用 software-a 最小能力实现执行。" self.task_repository.update(task) from app.services.task_service import TaskService TaskService(self.db, self.timezone_name).execute_task(task_id, request_id=request_id) else: task.summary = "审批已驳回,任务已取消。" self.task_repository.update(task) def _write_audit_log( self, task_id: str, request_id: str | None, action: str, result: str, target: str | None, operator_user_id: str | None, operator_user_name: str | None, detail: dict, ) -> AuditLog: audit_log = AuditLog( audit_id=f"audit-{uuid4().hex[:12]}", task_id=task_id, request_id=request_id, action=action, operator_user_id=operator_user_id, operator_user_name=operator_user_name, target=target, result=result, detail_json=json.dumps(detail, ensure_ascii=False), timestamp=format_now(self.timezone_name), ) return self.audit_repository.add(audit_log)