feat: 添加仪表板路由和响应数据脱敏功能

- 添加了仪表板模块路由并集成到主路由器中
- 实现了敏感数据响应脱敏配置和功能
- 增加了 Feishu 审批卡片操作处理功能
- 支持通过任务队列异步推送日常简报和项目周报
- 添加了风险事件生成的任务队列支持
- 在 smoke 测试中增加了相关功能验证

refactor: 格式化模型注册模块导入列表

- 将单行导入列表改为多行格式以提高可读性
```
This commit is contained in:
2026-07-07 18:33:07 +08:00
parent fbd0aaa9e4
commit 4d09d8e2e3
21 changed files with 641 additions and 25 deletions

View File

@@ -1,9 +1,12 @@
import json
from typing import Any
from fastapi import HTTPException, status
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from app.core.constants import ActorValue
from app.modules.approvals.service import ApprovalService
from app.modules.audit.constants import AuditAction, AuditSource
from app.modules.audit.schemas import AuditLogCreate
from app.modules.feishu.commands import FeishuCommandService
@@ -79,6 +82,36 @@ class FeishuEventService:
FeishuResponseKey.RESULT: result,
}
def handle_approval_card_action(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle Feishu interactive-card approval button callbacks."""
self.feishu.verify_event(payload)
value = _approval_action_value(payload)
ticket_id = str(value.get("ticket_id") or "").strip()
decision = str(value.get("decision") or value.get("action") or "").lower()
if not ticket_id or decision not in {"approve", "reject"}:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid Feishu approval action payload",
)
comment = value.get("comment")
actor = _approval_operator(payload)
ticket = ApprovalService(self.db).decide(
ticket_id,
actor,
approved=decision == "approve",
comment=str(comment) if comment is not None else None,
)
return {
FeishuResponseKey.OK: True,
FeishuResponseKey.HANDLED: True,
FeishuResponseKey.RESULT: {
"ticket_id": ticket.ticket_id,
"status": ticket.status,
"approver": ticket.approver,
},
}
def _register_event(self, event_identity: dict[str, str | None]) -> bool:
receipt = FeishuEventReceipt(
event_key=str(event_identity[FeishuEventReceiptKey.EVENT_KEY]),
@@ -123,3 +156,31 @@ def _event_identity(
FeishuEventReceiptKey.EVENT_ID: str(event_id) if event_id else None,
FeishuEventReceiptKey.MESSAGE_ID: str(message_id) if message_id else None,
}
def _approval_action_value(payload: dict[str, Any]) -> dict[str, Any]:
action = payload.get("action") or {}
event = payload.get(FeishuPayloadKey.EVENT) or {}
event_action = event.get("action") or {}
value = action.get("value") or event_action.get("value") or payload.get("value") or {}
if isinstance(value, str):
try:
parsed = json.loads(value)
except json.JSONDecodeError:
return {}
return parsed if isinstance(parsed, dict) else {}
return value if isinstance(value, dict) else {}
def _approval_operator(payload: dict[str, Any]) -> str:
operator = payload.get("operator") or (payload.get(FeishuPayloadKey.EVENT) or {}).get(
"operator"
) or {}
operator_id = operator.get("operator_id") or {}
return (
operator_id.get(FeishuPayloadKey.OPEN_ID)
or operator_id.get(FeishuPayloadKey.USER_ID)
or operator.get(FeishuPayloadKey.OPEN_ID)
or operator.get(FeishuPayloadKey.USER_ID)
or ActorValue.FEISHU
)

View File

@@ -30,7 +30,18 @@ async def feishu_webhook(request: Request, db: Session = Depends(get_db)) -> dic
)
@router.post("/send-text", response_model=FeishuSendResult, dependencies=[Depends(require_api_key)])
@router.post("/approval-card-action")
async def feishu_approval_card_action(
request: Request,
db: Session = Depends(get_db),
) -> dict:
"""Handle Feishu interactive-card approval actions."""
payload = await request.json()
return FeishuEventService(db).handle_approval_card_action(payload)
@router.post("/send-text", response_model=FeishuSendResult)
def send_text(
payload: FeishuTextMessage,
db: Session = Depends(get_db),
@@ -48,7 +59,7 @@ def send_text(
}
@router.post("/send-card", response_model=FeishuSendResult, dependencies=[Depends(require_api_key)])
@router.post("/send-card", response_model=FeishuSendResult)
def send_card(
payload: FeishuCardMessage,
db: Session = Depends(get_db),
@@ -69,7 +80,6 @@ def send_card(
@router.post(
"/commands/preview",
response_model=FeishuCommandResult,
dependencies=[Depends(require_api_key)],
)
def preview_command(
payload: FeishuCommandRequest,

View File

@@ -109,3 +109,37 @@ class FeishuService:
}
],
}
@staticmethod
def build_approval_card(
title: str,
lines: list[str],
ticket_id: str,
) -> dict[str, Any]:
card = FeishuService.build_basic_card(title, lines)
card[FeishuPayloadKey.ELEMENTS].append(
{
FeishuPayloadKey.TAG: "action",
"actions": [
{
FeishuPayloadKey.TAG: "button",
FeishuPayloadKey.TEXT: {
FeishuPayloadKey.TAG: FeishuPayloadKey.PLAIN_TEXT,
FeishuPayloadKey.CONTENT: "批准",
},
"type": "primary",
"value": {"ticket_id": ticket_id, "decision": "approve"},
},
{
FeishuPayloadKey.TAG: "button",
FeishuPayloadKey.TEXT: {
FeishuPayloadKey.TAG: FeishuPayloadKey.PLAIN_TEXT,
FeishuPayloadKey.CONTENT: "拒绝",
},
"type": "danger",
"value": {"ticket_id": ticket_id, "decision": "reject"},
},
],
}
)
return card