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:
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user