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

@@ -22,6 +22,8 @@ from ..schemas.schedules import ScheduleConfigSchema
from ..schemas.tasks import TaskConfigSchema
from .task_queue import task_queue
from app.trace.decorators import trace_service
logger = logging.getLogger(__name__)
# 调度器轮询间隔(秒)
@@ -34,6 +36,23 @@ def _parse_time(time_str: str) -> tuple[int, int]:
return int(parts[0]), int(parts[1])
def _convert_interval_to_seconds(value: int, unit: str) -> int:
"""将间隔值转换为秒数。
Args:
value: 间隔数值0 = 无限制)
unit: 间隔单位,支持 "minutes""hours""days"
Returns:
对应的秒数value <= 0 时返回 0
"""
if value <= 0:
return 0
multipliers = {"minutes": 60, "hours": 3600, "days": 86400}
return value * multipliers.get(unit, 60)
@trace_service(description_zh="calculate_next_run", description_en="Calculate Next Run")
def calculate_next_run(
schedule_config: ScheduleConfigSchema,
now: datetime | None = None,
@@ -188,7 +207,9 @@ class Scheduler:
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, site_id, task_config, schedule_config
SELECT id, site_id, task_config, schedule_config,
min_run_interval_value, min_run_interval_unit,
last_run_at, last_status, min_run_intervals
FROM scheduled_tasks
WHERE enabled = TRUE
AND next_run_at IS NOT NULL
@@ -198,11 +219,32 @@ class Scheduler:
)
rows = cur.fetchall()
now = datetime.now(timezone.utc)
for row in rows:
task_id = str(row[0])
site_id = row[1]
task_config_raw = row[2] if isinstance(row[2], dict) else json.loads(row[2])
schedule_config_raw = row[3] if isinstance(row[3], dict) else json.loads(row[3])
min_interval_value = row[4] or 0
min_interval_unit = row[5] or "minutes"
last_run_at = row[6]
last_status = row[7]
# per-task 间隔:取所有任务中最大的间隔作为有效间隔
min_run_intervals_raw = row[8] if isinstance(row[8], dict) else json.loads(row[8]) if row[8] else {}
# 计算有效间隔per-task 最大值 vs schedule 级别,取较大者
effective_interval_seconds = _convert_interval_to_seconds(
min_interval_value, min_interval_unit
)
for _task_code, interval_cfg in min_run_intervals_raw.items():
if isinstance(interval_cfg, dict):
task_seconds = _convert_interval_to_seconds(
interval_cfg.get("value", 0),
interval_cfg.get("unit", "minutes"),
)
if task_seconds > effective_interval_seconds:
effective_interval_seconds = task_seconds
try:
config = TaskConfigSchema(**task_config_raw)
@@ -211,7 +253,44 @@ class Scheduler:
logger.exception("调度任务 [%s] 配置反序列化失败,跳过", task_id)
continue
# 入队
# 1. 并发检查:上次仍在运行中 → 跳过
if last_status == "running":
logger.warning(
"调度任务 [%s] skipped_concurrent上次执行仍在运行中",
task_id,
)
continue
# 2. 间隔检查:最小运行间隔未到 → 跳过并推进 next_run_at
if effective_interval_seconds > 0 and last_run_at is not None:
elapsed = (now - last_run_at).total_seconds()
if elapsed < effective_interval_seconds:
# 推进 next_run_at = last_run_at + interval
next_run_at_pushed = last_run_at + timedelta(
seconds=effective_interval_seconds
)
with conn.cursor() as cur:
cur.execute(
"""
UPDATE scheduled_tasks
SET next_run_at = %s,
updated_at = NOW()
WHERE id = %s
""",
(next_run_at_pushed, task_id),
)
conn.commit()
logger.info(
"调度任务 [%s] skipped_interval最小间隔未到"
"(已过 %.0fs / 需 %dsnext_run_at 推进至 %s",
task_id,
elapsed,
effective_interval_seconds,
next_run_at_pushed,
)
continue
# 3. 正常入队
try:
queue_id = task_queue.enqueue(config, site_id, schedule_id=task_id)
logger.info(
@@ -224,7 +303,6 @@ class Scheduler:
continue
# 更新调度任务状态
now = datetime.now(timezone.utc)
next_run = calculate_next_run(schedule_cfg, now)
with conn.cursor() as cur:
@@ -269,6 +347,9 @@ class Scheduler:
# 在线程池中执行同步数据库操作,避免阻塞事件循环
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, self.check_and_enqueue)
# CHANGE 2026-03-23 | 同时检查 trigger_jobs 中到期的 cron/interval 任务
from app.services.trigger_scheduler import check_scheduled_jobs
await loop.run_in_executor(None, check_scheduled_jobs)
except Exception:
logger.exception("Scheduler 循环迭代异常")