feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本

包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Neo
2026-04-06 00:03:48 +08:00
parent 70324d8542
commit 6f8f12314f
515 changed files with 76604 additions and 7456 deletions

View File

@@ -14,6 +14,8 @@ import logging
from datetime import datetime, timedelta, timezone
from typing import Any, Callable
from app.trace.decorators import trace_service
logger = logging.getLogger(__name__)
@@ -26,11 +28,13 @@ def _get_connection():
_JOB_REGISTRY: dict[str, Callable] = {}
@trace_service(description_zh="register_job", description_en="Register Job")
def register_job(job_type: str, handler: Callable) -> None:
"""注册 job_type 对应的执行函数。"""
_JOB_REGISTRY[job_type] = handler
@trace_service(description_zh="update_job_last_run_at", description_en="Update Job Last Run At")
def update_job_last_run_at(cur, job_id: int) -> None:
"""
在 handler 的事务内更新 last_run_at。
@@ -45,6 +49,7 @@ def update_job_last_run_at(cur, job_id: int) -> None:
)
@trace_service(description_zh="触发调度事件", description_en="Fire scheduler event")
def fire_event(event_name: str, payload: dict[str, Any] | None = None) -> int:
"""
触发事件驱动型任务。
@@ -94,6 +99,7 @@ def fire_event(event_name: str, payload: dict[str, Any] | None = None) -> int:
return executed
@trace_service(description_zh="检查定时任务", description_en="Check scheduled jobs")
def check_scheduled_jobs() -> int:
"""
检查 cron/interval 类型的到期 job 并执行。
@@ -138,16 +144,30 @@ def check_scheduled_jobs() -> int:
cur.execute(
"""
UPDATE biz.trigger_jobs
SET last_run_at = NOW(), next_run_at = %s
SET last_run_at = NOW(), next_run_at = %s, last_error = NULL
WHERE id = %s
""",
(next_run, job_id),
)
conn.commit()
executed += 1
except Exception:
except Exception as exc:
logger.exception("触发器 %s 执行失败", job_name)
conn.rollback()
# 记录错误到 last_error 字段
try:
with conn.cursor() as cur:
cur.execute(
"UPDATE biz.trigger_jobs SET last_error = %s WHERE id = %s",
(str(exc)[:500], job_id),
)
conn.commit()
except Exception:
logger.debug("记录 last_error 失败", exc_info=True)
try:
conn.rollback()
except Exception:
pass
finally:
conn.close()
@@ -179,3 +199,151 @@ def _calculate_next_run(
trigger_config.get("cron_expression", "0 7 * * *"), now
)
return None # event 类型无 next_run_at
def check_startup_jobs() -> list[dict]:
"""
启动时检查 cron/interval 类型任务今天是否执行过。
返回未执行的任务列表,供启动横幅提示。
不自动执行,由用户通过管理页面手动确认。
"""
from datetime import date
conn = _get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, job_name, trigger_condition, trigger_config,
last_run_at, description
FROM biz.trigger_jobs
WHERE status = 'enabled'
AND trigger_condition IN ('cron', 'interval')
ORDER BY id
"""
)
rows = cur.fetchall()
conn.commit()
today = date.today()
pending = []
for row in rows:
job_id, job_name, trigger_condition, trigger_config, last_run_at, description = row
ran_today = False
if last_run_at is not None:
# last_run_at 可能带时区,取 date 部分比较
ran_today = last_run_at.date() == today if hasattr(last_run_at, 'date') else False
if not ran_today:
pending.append({
"id": job_id,
"job_name": job_name,
"trigger_condition": trigger_condition,
"description": description or job_name,
"last_run_at": str(last_run_at) if last_run_at else "从未执行",
})
return pending
finally:
conn.close()
def run_job_by_id(job_id: int) -> dict:
"""
手动触发指定 job通过管理页面调用
返回 {"success": bool, "message": str}。
"""
conn = _get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, job_type, job_name, trigger_condition, trigger_config
FROM biz.trigger_jobs
WHERE id = %s
""",
(job_id,),
)
row = cur.fetchone()
conn.commit()
if not row:
return {"success": False, "message": f"任务 {job_id} 不存在"}
_, job_type, job_name, trigger_condition, trigger_config = row
handler = _JOB_REGISTRY.get(job_type)
if not handler:
return {"success": False, "message": f"任务 {job_name} 未注册处理器"}
try:
handler()
# 更新 last_run_at 和 next_run_at
next_run = _calculate_next_run(trigger_condition, trigger_config)
with conn.cursor() as cur:
cur.execute(
"""
UPDATE biz.trigger_jobs
SET last_run_at = NOW(), next_run_at = %s, last_error = NULL
WHERE id = %s
""",
(next_run, job_id),
)
conn.commit()
return {"success": True, "message": f"任务 {job_name} 执行成功"}
except Exception as exc:
logger.exception("手动触发 %s 失败", job_name)
conn.rollback()
try:
with conn.cursor() as cur:
cur.execute(
"UPDATE biz.trigger_jobs SET last_error = %s WHERE id = %s",
(str(exc)[:500], job_id),
)
conn.commit()
except Exception:
try:
conn.rollback()
except Exception:
pass
return {"success": False, "message": f"任务 {job_name} 执行失败: {str(exc)[:200]}"}
finally:
conn.close()
def list_trigger_jobs() -> list[dict]:
"""
获取所有 trigger_jobs 列表(管理页面用)。
"""
conn = _get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, job_type, job_name, trigger_condition, trigger_config,
last_run_at, next_run_at, status, description, last_error,
created_at
FROM biz.trigger_jobs
ORDER BY id
"""
)
rows = cur.fetchall()
conn.commit()
result = []
for row in rows:
result.append({
"id": row[0],
"job_type": row[1],
"job_name": row[2],
"trigger_condition": row[3],
"trigger_config": row[4],
"last_run_at": row[5].isoformat() if row[5] else None,
"next_run_at": row[6].isoformat() if row[6] else None,
"status": row[7],
"description": row[8],
"last_error": row[9],
"created_at": row[10].isoformat() if row[10] else None,
})
return result
finally:
conn.close()