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

@@ -43,6 +43,7 @@ def _row_to_response(row) -> ScheduleResponse:
"""将数据库行转换为 ScheduleResponse。"""
task_config = row[4] if isinstance(row[4], dict) else json.loads(row[4])
schedule_config = row[5] if isinstance(row[5], dict) else json.loads(row[5])
min_run_intervals = row[14] if isinstance(row[14], dict) else json.loads(row[14]) if row[14] else {}
return ScheduleResponse(
id=str(row[0]),
site_id=row[1],
@@ -55,8 +56,12 @@ def _row_to_response(row) -> ScheduleResponse:
next_run_at=row[8],
run_count=row[9],
last_status=row[10],
created_at=row[11],
updated_at=row[12],
min_run_interval_value=row[11] or 0,
min_run_interval_unit=row[12] or "minutes",
last_success_at=row[13],
min_run_intervals=min_run_intervals,
created_at=row[15],
updated_at=row[16],
)
@@ -64,6 +69,8 @@ def _row_to_response(row) -> ScheduleResponse:
_SELECT_COLS = """
id, site_id, name, task_codes, task_config, schedule_config,
enabled, last_run_at, next_run_at, run_count, last_status,
min_run_interval_value, min_run_interval_unit, last_success_at,
min_run_intervals,
created_at, updated_at
"""
@@ -107,8 +114,9 @@ async def create_schedule(
cur.execute(
f"""
INSERT INTO scheduled_tasks
(site_id, name, task_codes, task_config, schedule_config, enabled, next_run_at)
VALUES (%s, %s, %s, %s, %s, %s, %s)
(site_id, name, task_codes, task_config, schedule_config, enabled, next_run_at,
min_run_interval_value, min_run_interval_unit, min_run_intervals)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING {_SELECT_COLS}
""",
(
@@ -119,6 +127,9 @@ async def create_schedule(
body.schedule_config.model_dump_json(),
body.schedule_config.enabled,
next_run,
body.min_run_interval_value,
body.min_run_interval_unit,
json.dumps({k: v.model_dump() for k, v in body.min_run_intervals.items()}) if body.min_run_intervals else "{}",
),
)
row = cur.fetchone()
@@ -174,6 +185,16 @@ async def update_schedule(
set_parts.append("next_run_at = %s")
params.append(next_run)
if body.min_run_interval_value is not None:
set_parts.append("min_run_interval_value = %s")
params.append(body.min_run_interval_value)
if body.min_run_interval_unit is not None:
set_parts.append("min_run_interval_unit = %s")
params.append(body.min_run_interval_unit)
if body.min_run_intervals is not None:
set_parts.append("min_run_intervals = %s")
params.append(json.dumps({k: v.model_dump() for k, v in body.min_run_intervals.items()}))
if not set_parts:
raise HTTPException(
status_code=422,
@@ -314,17 +335,22 @@ async def toggle_schedule(
@router.post("/{schedule_id}/run")
async def run_schedule_now(
schedule_id: str,
force: bool = Query(False),
user: CurrentUser = Depends(get_current_user),
) -> dict:
"""手动触发调度任务执行一次,不更新 last_run_at / next_run_at / run_count。
读取调度任务的 task_config构造 TaskConfigSchema 后入队执行。
force=true 时绕过并发和间隔检查,直接入队。
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"SELECT task_config, site_id FROM scheduled_tasks WHERE id = %s AND site_id = %s",
"""SELECT task_config, site_id,
min_run_interval_value, min_run_interval_unit,
last_run_at, last_status, min_run_intervals
FROM scheduled_tasks WHERE id = %s AND site_id = %s""",
(schedule_id, user.site_id),
)
row = cur.fetchone()
@@ -338,6 +364,42 @@ async def run_schedule_now(
detail="调度任务不存在",
)
# force=false 时执行并发和间隔检查
if not force:
last_status = row[5]
if last_status == "running":
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="任务正在运行中,无法重复执行",
)
min_interval_value = row[2] or 0
min_interval_unit = row[3] or "minutes"
last_run_at = row[4]
min_run_intervals_raw = row[6] if isinstance(row[6], dict) else json.loads(row[6]) if row[6] else {}
# 计算有效间隔per-task 最大值 vs schedule 级别,取较大者
effective_interval_seconds = 0
multipliers = {"minutes": 60, "hours": 3600, "days": 86400}
if min_interval_value > 0:
effective_interval_seconds = min_interval_value * multipliers.get(min_interval_unit, 60)
for _task_code, interval_cfg in min_run_intervals_raw.items():
if isinstance(interval_cfg, dict):
v = interval_cfg.get("value", 0) or 0
u = interval_cfg.get("unit", "minutes")
task_seconds = v * multipliers.get(u, 60) if v > 0 else 0
if task_seconds > effective_interval_seconds:
effective_interval_seconds = task_seconds
if effective_interval_seconds > 0 and last_run_at is not None:
now = datetime.now(timezone.utc)
elapsed = (now - last_run_at).total_seconds()
if elapsed < effective_interval_seconds:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="未达到最小运行间隔,请稍后再试",
)
task_config_raw = row[0] if isinstance(row[0], dict) else json.loads(row[0])
config = TaskConfigSchema(**task_config_raw)
config = config.model_copy(update={"store_id": user.site_id})