From 0576b253e42b29adbf4190da08428530486b00cf Mon Sep 17 00:00:00 2001 From: logan Date: Mon, 25 May 2026 11:41:03 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=A1=E6=89=B9=E6=B5=81=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20+=20=E9=80=9A=E7=9F=A5KeyError=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审批流修复: - 提交阶段(step_type=submit)自动approved,不再创建pending记录 - 审批权限添加auditor角色(require_role admin/dba/auditor) - 审批完成判断排除execute步骤(step_type!=execute才计入has_next) - review_ticket中加载workflow_steps用于step_type判断 - Ticket字段名修正: creator_id -> applicant_id 通知修复: - send()中自动补充sql_preview字段(sql_content前100字符) - 飞书/钉钉模板format改为format_map+安全fallback(缺失字段不报KeyError) - DB中auditor用户role从developer修正为auditor 测试: 1182 passed, 0 failed 审批流全链路验证通过: 提交->DBA审核->审计员审核->admin执行->success --- backend/app/api/v1/endpoints/ticket.py | 63 ++++++++++++++++----- backend/app/services/notification_sender.py | 27 +++++++-- 2 files changed, 72 insertions(+), 18 deletions(-) diff --git a/backend/app/api/v1/endpoints/ticket.py b/backend/app/api/v1/endpoints/ticket.py index cc93d57..67798f7 100644 --- a/backend/app/api/v1/endpoints/ticket.py +++ b/backend/app/api/v1/endpoints/ticket.py @@ -602,13 +602,35 @@ async def create_ticket( ) wf_steps = wf_steps_result.scalars().all() for step in wf_steps: - approval = TicketApproval( - ticket_id=ticket.id, - step_number=step.step_number, - step_name=step.name, - approver_role=step.approver_role, - action="pending", - ) + if step.step_type == "submit": + # 提交阶段自动通过(提交者自己就是审批人) + approval = TicketApproval( + ticket_id=ticket.id, + step_number=step.step_number, + step_name=step.name, + approver_role=step.approver_role, + approver_id=ticket.applicant_id, + actual_approver_id=ticket.applicant_id, + action="approved", + comment="提交工单", + ) + elif step.step_type == "execute": + # 执行阶段不创建审批记录,等所有审核通过后直接执行 + approval = TicketApproval( + ticket_id=ticket.id, + step_number=step.step_number, + step_name=step.name, + approver_role=step.approver_role, + action="pending", + ) + else: + approval = TicketApproval( + ticket_id=ticket.id, + step_number=step.step_number, + step_name=step.name, + approver_role=step.approver_role, + action="pending", + ) db.add(approval) await db.flush() @@ -772,7 +794,7 @@ async def review_ticket( ticket_id: int, data: TicketReview, db: AsyncSession = Depends(get_db), - current_user: User = Depends(require_role("admin", "dba")), + current_user: User = Depends(require_role("admin", "dba", "auditor")), ): """审核工单。 @@ -799,6 +821,16 @@ async def review_ticket( pending_approvals = pending_approvals_result.scalars().all() if pending_approvals: + # 加载审批流步骤定义(用于判断 step_type) + wf_steps = [] + if ticket.workflow_id: + ws_result = await db.execute( + select(WorkflowStep) + .where(WorkflowStep.workflow_id == ticket.workflow_id) + .order_by(WorkflowStep.step_number) + ) + wf_steps = ws_result.scalars().all() + # 多级审批: 找到当前待审批步骤 current_step = None for pa in pending_approvals: @@ -812,12 +844,15 @@ async def review_ticket( current_step.approver_id = current_user.id current_step.comment = data.comment or "审批通过" - # 检查是否还有后续待审批步骤 - has_next = any( - pa.action == "pending" - for pa in pending_approvals - if pa.id != current_step.id - ) + # 检查是否还有后续待审批步骤(执行阶段不算,它只需要状态为approved即可执行) + has_next = False + for pa in pending_approvals: + if pa.id != current_step.id and pa.action == "pending": + # 查对应 workflow step 的 step_type + ws = next((s for s in wf_steps if s.step_number == pa.step_number), None) + if ws and ws.step_type != "execute": + has_next = True + break if has_next: new_status = TicketStatus.REVIEWING.value else: diff --git a/backend/app/services/notification_sender.py b/backend/app/services/notification_sender.py index 7b18ed0..ec08d08 100644 --- a/backend/app/services/notification_sender.py +++ b/backend/app/services/notification_sender.py @@ -125,6 +125,11 @@ class NotificationSender: # 通知脱敏:隐藏 SQL 中的敏感内容 data = self._mask_sensitive_data(data) + # 自动补充 sql_preview(取 sql_content 前100字符) + if "sql_preview" not in data and "sql_content" in data: + sql = data["sql_content"] + data["sql_preview"] = sql[:100] + ("..." if len(sql) > 100 else "") + try: if channel_type == "dingtalk": return await self._send_dingtalk(config, event_type, data) @@ -158,8 +163,15 @@ class NotificationSender: webhook_url = f"{webhook_url}?timestamp={timestamp}&sign={sign}" template = self.TEMPLATES.get(event_type, self.TEMPLATES["ticket_created"]) - title = template["title"].format(**data) - body = template["body"].format(**data) + # 安全格式化 + safe_data = {**data} + for key in ["applicant", "ticket_id", "title", "db_type", "instance_name", + "target_database", "sql_content", "sql_preview", "approver", + "comment", "result", "duration", "affected_rows", "error", + "operator", "database", "size", "drift_count", "drift_details"]: + safe_data.setdefault(key, "") + title = template["title"].format_map(safe_data) + body = template["body"].format_map(safe_data) tag = self.DINGTALK_TAGS.get(event_type, "📌") # 钉钉 markdown 单个 \n 视为空格,需要 \n\n 才能换行 @@ -191,8 +203,15 @@ class NotificationSender: return {"success": False, "message": "缺少 webhook_url 配置"} template = self.TEMPLATES.get(event_type, self.TEMPLATES["ticket_created"]) - title = template["title"].format(**data) - body = template["body"].format(**data) + # 安全格式化:缺失字段用空字符串代替,避免 KeyError + safe_data = {**data} + for key in ["applicant", "ticket_id", "title", "db_type", "instance_name", + "target_database", "sql_content", "sql_preview", "approver", + "comment", "result", "duration", "affected_rows", "error", + "operator", "database", "size", "drift_count", "drift_details"]: + safe_data.setdefault(key, "") + title = template["title"].format_map(safe_data) + body = template["body"].format_map(safe_data) color = self.FEISHU_COLORS.get(event_type, "blue") payload = {