```
feat: 添加数据库迁移脚本并更新Dockerfile配置 - 在Dockerfile中添加alembic配置文件和目录的复制指令 - 更新alembic/env.py注册新的模块模型:events、workflows、writebacks - 生成完整的初始数据库schema迁移脚本,包含以下表: - approval_requests, attendance_records, audit_logs, domain_events - expenses, feishu_event_receipts, fund_accounts, legacy_sync_runs - official_writeback_runs, performance_metrics, policies, procurements - projects, report_push_runs, risk_event_actions, risk_events - standards, suppliers, work_reports, work_tasks, workflow_actions - workflow_instances等21个数据表结构定义 - 在API路由器中添加新模块的路由:events、workflows、writebacks、observability ```
This commit is contained in:
@@ -64,6 +64,8 @@ class FeishuResponseKey(StrEnum):
|
||||
RESULT = "result"
|
||||
CHALLENGE = "challenge"
|
||||
PROVIDER_RESPONSE = "provider_response"
|
||||
STATUS = "status"
|
||||
APPROVER = "approver"
|
||||
|
||||
|
||||
class FeishuCommandResultKey(StrEnum):
|
||||
@@ -96,6 +98,24 @@ class FeishuEventReceiptKey(StrEnum):
|
||||
MESSAGE_ID = "message_id"
|
||||
|
||||
|
||||
class FeishuApprovalAction(StrEnum):
|
||||
APPROVE = "approve"
|
||||
REJECT = "reject"
|
||||
|
||||
|
||||
class FeishuApprovalValueKey(StrEnum):
|
||||
TICKET_ID = "ticket_id"
|
||||
DECISION = "decision"
|
||||
ACTION = "action"
|
||||
COMMENT = "comment"
|
||||
|
||||
|
||||
class FeishuCardKey(StrEnum):
|
||||
ACTIONS = "actions"
|
||||
BUTTON_TYPE = "type"
|
||||
VALUE = "value"
|
||||
|
||||
|
||||
FEISHU_TENANT_TOKEN_PATH = "/auth/v3/tenant_access_token/internal"
|
||||
FEISHU_MESSAGE_PATH = "/im/v1/messages"
|
||||
FEISHU_DEFAULT_OPEN_API_DOMAIN = "https://open.feishu.cn"
|
||||
@@ -103,6 +123,10 @@ FEISHU_AUTH_MISSING = "Feishu app credentials are not configured"
|
||||
FEISHU_VERIFICATION_TOKEN_REQUIRED = "FEISHU_VERIFICATION_TOKEN is required"
|
||||
FEISHU_INVALID_TOKEN = "Invalid Feishu token"
|
||||
FEISHU_RECEIVE_ID_MISSING = "receive_id or FEISHU_DEFAULT_CHAT_ID is required"
|
||||
FEISHU_APPROVAL_ACTION_INVALID = "Invalid Feishu approval action payload"
|
||||
FEISHU_APPROVAL_APPROVER_IDS_REQUIRED = "FEISHU_APPROVAL_APPROVER_IDS is required"
|
||||
FEISHU_APPROVER_NOT_ALLOWED = "Feishu approver is not allowed"
|
||||
FEISHU_APPROVAL_CARD_ACTION_TARGET = "approval_card_action"
|
||||
FEISHU_SUCCESS_CODE = 0
|
||||
FEISHU_DEFAULT_TOKEN_EXPIRE_SECONDS = 7200
|
||||
FEISHU_TOKEN_EXPIRE_SAFETY_SECONDS = 300
|
||||
|
||||
@@ -6,11 +6,19 @@ from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.constants import ActorValue
|
||||
from app.core.config import get_settings
|
||||
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
|
||||
from app.modules.feishu.constants import (
|
||||
FEISHU_APPROVAL_ACTION_INVALID,
|
||||
FEISHU_APPROVAL_APPROVER_IDS_REQUIRED,
|
||||
FEISHU_APPROVAL_CARD_ACTION_TARGET,
|
||||
FEISHU_APPROVER_NOT_ALLOWED,
|
||||
FeishuApprovalAction,
|
||||
FeishuApprovalValueKey,
|
||||
FeishuCardKey,
|
||||
FeishuCommandKey,
|
||||
FeishuEventReceiptKey,
|
||||
FeishuEventSource,
|
||||
@@ -87,15 +95,38 @@ class FeishuEventService:
|
||||
|
||||
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"}:
|
||||
ticket_id = str(value.get(FeishuApprovalValueKey.TICKET_ID) or "").strip()
|
||||
decision = str(
|
||||
value.get(FeishuApprovalValueKey.DECISION)
|
||||
or value.get(FeishuApprovalValueKey.ACTION)
|
||||
or ""
|
||||
).lower()
|
||||
if not ticket_id or decision not in set(FeishuApprovalAction):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="Invalid Feishu approval action payload",
|
||||
detail=FEISHU_APPROVAL_ACTION_INVALID,
|
||||
)
|
||||
comment = value.get("comment")
|
||||
comment = value.get(FeishuApprovalValueKey.COMMENT)
|
||||
actor = _approval_operator(payload)
|
||||
try:
|
||||
_ensure_approval_operator_allowed(actor)
|
||||
except HTTPException as exc:
|
||||
self.feishu.audit.log(
|
||||
AuditLogCreate(
|
||||
actor=actor,
|
||||
source=AuditSource.FEISHU,
|
||||
action=AuditAction.FEISHU_WEBHOOK_EVENT,
|
||||
target_type=FEISHU_APPROVAL_CARD_ACTION_TARGET,
|
||||
target_id=ticket_id,
|
||||
request_payload=payload,
|
||||
response_payload={
|
||||
FeishuResponseKey.OK: False,
|
||||
"status_code": exc.status_code,
|
||||
"detail": exc.detail,
|
||||
},
|
||||
)
|
||||
)
|
||||
raise
|
||||
event_identity = _approval_event_identity(payload, ticket_id, decision, actor)
|
||||
if not self._register_event(event_identity):
|
||||
ticket = ApprovalService(self.db).get_by_ticket(ticket_id)
|
||||
@@ -104,15 +135,15 @@ class FeishuEventService:
|
||||
FeishuResponseKey.HANDLED: True,
|
||||
FeishuResponseKey.DUPLICATE: True,
|
||||
FeishuResponseKey.RESULT: {
|
||||
"ticket_id": ticket.ticket_id,
|
||||
"status": ticket.status,
|
||||
"approver": ticket.approver,
|
||||
FeishuApprovalValueKey.TICKET_ID: ticket.ticket_id,
|
||||
FeishuResponseKey.STATUS: ticket.status,
|
||||
FeishuResponseKey.APPROVER: ticket.approver,
|
||||
},
|
||||
}
|
||||
ticket = ApprovalService(self.db).decide(
|
||||
ticket_id,
|
||||
actor,
|
||||
approved=decision == "approve",
|
||||
approved=decision == FeishuApprovalAction.APPROVE,
|
||||
comment=str(comment) if comment is not None else None,
|
||||
)
|
||||
self.feishu.audit.log(
|
||||
@@ -120,19 +151,22 @@ class FeishuEventService:
|
||||
actor=actor,
|
||||
source=AuditSource.FEISHU,
|
||||
action=AuditAction.FEISHU_WEBHOOK_EVENT,
|
||||
target_type="approval_card_action",
|
||||
target_type=FEISHU_APPROVAL_CARD_ACTION_TARGET,
|
||||
target_id=ticket_id,
|
||||
request_payload=payload,
|
||||
response_payload={"status": ticket.status, "decision": decision},
|
||||
response_payload={
|
||||
FeishuResponseKey.STATUS: ticket.status,
|
||||
FeishuApprovalValueKey.DECISION: decision,
|
||||
},
|
||||
)
|
||||
)
|
||||
return {
|
||||
FeishuResponseKey.OK: True,
|
||||
FeishuResponseKey.HANDLED: True,
|
||||
FeishuResponseKey.RESULT: {
|
||||
"ticket_id": ticket.ticket_id,
|
||||
"status": ticket.status,
|
||||
"approver": ticket.approver,
|
||||
FeishuApprovalValueKey.TICKET_ID: ticket.ticket_id,
|
||||
FeishuResponseKey.STATUS: ticket.status,
|
||||
FeishuResponseKey.APPROVER: ticket.approver,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -183,10 +217,15 @@ def _event_identity(
|
||||
|
||||
|
||||
def _approval_action_value(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
action = payload.get("action") or {}
|
||||
action = payload.get(FeishuApprovalValueKey.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 {}
|
||||
event_action = event.get(FeishuApprovalValueKey.ACTION) or {}
|
||||
value = (
|
||||
action.get(FeishuCardKey.VALUE)
|
||||
or event_action.get(FeishuCardKey.VALUE)
|
||||
or payload.get(FeishuCardKey.VALUE)
|
||||
or {}
|
||||
)
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
@@ -210,6 +249,24 @@ def _approval_operator(payload: dict[str, Any]) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _ensure_approval_operator_allowed(actor: str) -> None:
|
||||
allowed_ids = {
|
||||
item.strip()
|
||||
for item in get_settings().feishu_approval_approver_ids
|
||||
if item.strip()
|
||||
}
|
||||
if not allowed_ids:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=FEISHU_APPROVAL_APPROVER_IDS_REQUIRED,
|
||||
)
|
||||
if actor not in allowed_ids:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=FEISHU_APPROVER_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
|
||||
def _approval_event_identity(
|
||||
payload: dict[str, Any],
|
||||
ticket_id: str,
|
||||
|
||||
@@ -14,6 +14,9 @@ from app.modules.feishu.constants import (
|
||||
FEISHU_EMPTY_CARD_TEXT,
|
||||
FEISHU_INVALID_TOKEN,
|
||||
FEISHU_VERIFICATION_TOKEN_REQUIRED,
|
||||
FeishuApprovalAction,
|
||||
FeishuApprovalValueKey,
|
||||
FeishuCardKey,
|
||||
FeishuPayloadKey,
|
||||
FeishuReceiveIdType,
|
||||
)
|
||||
@@ -119,16 +122,19 @@ class FeishuService:
|
||||
card = FeishuService.build_basic_card(title, lines)
|
||||
card[FeishuPayloadKey.ELEMENTS].append(
|
||||
{
|
||||
FeishuPayloadKey.TAG: "action",
|
||||
"actions": [
|
||||
FeishuPayloadKey.TAG: FeishuApprovalValueKey.ACTION,
|
||||
FeishuCardKey.ACTIONS: [
|
||||
{
|
||||
FeishuPayloadKey.TAG: "button",
|
||||
FeishuPayloadKey.TEXT: {
|
||||
FeishuPayloadKey.TAG: FeishuPayloadKey.PLAIN_TEXT,
|
||||
FeishuPayloadKey.CONTENT: "批准",
|
||||
},
|
||||
"type": "primary",
|
||||
"value": {"ticket_id": ticket_id, "decision": "approve"},
|
||||
FeishuCardKey.BUTTON_TYPE: "primary",
|
||||
FeishuCardKey.VALUE: {
|
||||
FeishuApprovalValueKey.TICKET_ID: ticket_id,
|
||||
FeishuApprovalValueKey.DECISION: FeishuApprovalAction.APPROVE,
|
||||
},
|
||||
},
|
||||
{
|
||||
FeishuPayloadKey.TAG: "button",
|
||||
@@ -136,8 +142,11 @@ class FeishuService:
|
||||
FeishuPayloadKey.TAG: FeishuPayloadKey.PLAIN_TEXT,
|
||||
FeishuPayloadKey.CONTENT: "拒绝",
|
||||
},
|
||||
"type": "danger",
|
||||
"value": {"ticket_id": ticket_id, "decision": "reject"},
|
||||
FeishuCardKey.BUTTON_TYPE: "danger",
|
||||
FeishuCardKey.VALUE: {
|
||||
FeishuApprovalValueKey.TICKET_ID: ticket_id,
|
||||
FeishuApprovalValueKey.DECISION: FeishuApprovalAction.REJECT,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user