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:
2026-07-08 14:08:03 +08:00
parent 92f490b97e
commit 19e59e83cc
59 changed files with 3271 additions and 247 deletions

View File

@@ -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,