fix: 审批流修复 + 通知KeyError修复
审批流修复: - 提交阶段(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
This commit is contained in:
1 parent
da3ed85295
commit
0576b253e4
2 files changed
+72
-18
No files matched your search
@@ -602,13 +602,35 @@ async def create_ticket(
|
|||||||
)
|
)
|
||||||
wf_steps = wf_steps_result.scalars().all()
|
wf_steps = wf_steps_result.scalars().all()
|
||||||
for step in wf_steps:
|
for step in wf_steps:
|
||||||
approval = TicketApproval(
|
if step.step_type == "submit":
|
||||||
ticket_id=ticket.id,
|
# 提交阶段自动通过(提交者自己就是审批人)
|
||||||
step_number=step.step_number,
|
approval = TicketApproval(
|
||||||
step_name=step.name,
|
ticket_id=ticket.id,
|
||||||
approver_role=step.approver_role,
|
step_number=step.step_number,
|
||||||
action="pending",
|
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)
|
db.add(approval)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|
||||||
@@ -772,7 +794,7 @@ async def review_ticket(
|
|||||||
ticket_id: int,
|
ticket_id: int,
|
||||||
data: TicketReview,
|
data: TicketReview,
|
||||||
db: AsyncSession = Depends(get_db),
|
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()
|
pending_approvals = pending_approvals_result.scalars().all()
|
||||||
|
|
||||||
if pending_approvals:
|
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
|
current_step = None
|
||||||
for pa in pending_approvals:
|
for pa in pending_approvals:
|
||||||
@@ -812,12 +844,15 @@ async def review_ticket(
|
|||||||
current_step.approver_id = current_user.id
|
current_step.approver_id = current_user.id
|
||||||
current_step.comment = data.comment or "审批通过"
|
current_step.comment = data.comment or "审批通过"
|
||||||
|
|
||||||
# 检查是否还有后续待审批步骤
|
# 检查是否还有后续待审批步骤(执行阶段不算,它只需要状态为approved即可执行)
|
||||||
has_next = any(
|
has_next = False
|
||||||
pa.action == "pending"
|
for pa in pending_approvals:
|
||||||
for pa in pending_approvals
|
if pa.id != current_step.id and pa.action == "pending":
|
||||||
if pa.id != current_step.id
|
# 查对应 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:
|
if has_next:
|
||||||
new_status = TicketStatus.REVIEWING.value
|
new_status = TicketStatus.REVIEWING.value
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -125,6 +125,11 @@ class NotificationSender:
|
|||||||
# 通知脱敏:隐藏 SQL 中的敏感内容
|
# 通知脱敏:隐藏 SQL 中的敏感内容
|
||||||
data = self._mask_sensitive_data(data)
|
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:
|
try:
|
||||||
if channel_type == "dingtalk":
|
if channel_type == "dingtalk":
|
||||||
return await self._send_dingtalk(config, event_type, data)
|
return await self._send_dingtalk(config, event_type, data)
|
||||||
@@ -158,8 +163,15 @@ class NotificationSender:
|
|||||||
webhook_url = f"{webhook_url}?timestamp={timestamp}&sign={sign}"
|
webhook_url = f"{webhook_url}?timestamp={timestamp}&sign={sign}"
|
||||||
|
|
||||||
template = self.TEMPLATES.get(event_type, self.TEMPLATES["ticket_created"])
|
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, "📌")
|
tag = self.DINGTALK_TAGS.get(event_type, "📌")
|
||||||
|
|
||||||
# 钉钉 markdown 单个 \n 视为空格,需要 \n\n 才能换行
|
# 钉钉 markdown 单个 \n 视为空格,需要 \n\n 才能换行
|
||||||
@@ -191,8 +203,15 @@ class NotificationSender:
|
|||||||
return {"success": False, "message": "缺少 webhook_url 配置"}
|
return {"success": False, "message": "缺少 webhook_url 配置"}
|
||||||
|
|
||||||
template = self.TEMPLATES.get(event_type, self.TEMPLATES["ticket_created"])
|
template = self.TEMPLATES.get(event_type, self.TEMPLATES["ticket_created"])
|
||||||
title = template["title"].format(**data)
|
# 安全格式化:缺失字段用空字符串代替,避免 KeyError
|
||||||
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)
|
||||||
color = self.FEISHU_COLORS.get(event_type, "blue")
|
color = self.FEISHU_COLORS.get(event_type, "blue")
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
|
|||||||
Reference in new issue
Block a user