- 补齐 tool_call 和 edge 验证链路的 duration_ms 计算与返回 - 任务详情和任务报告新增 result_summary_detail 结构化摘要 - 摘要中补充最终状态、失败原因、software-a 摘要、审批摘要、验证摘要 - 软件A层术语统一为“最小能力实现” - 同步更新 README、当前进度总结和相关设计文档 - 补充并通过对应自动化测试
165 lines
6.8 KiB
Python
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 最小能力实现执行。"
|
|
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)
|