auto_agent/backend/app/services/approval_service.py

165 lines
6.8 KiB
Python

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 demo 执行。"
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)