包含多个会话的累积代码变更: - 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>
30 KiB
P18:管理后台 — 任务引擎运营看板与参数管理
版本:v2.1-reviewed | 日期:2026-03-24 | 作者:AI(待评审) 依赖:P17(助教客户归属与任务生成引擎)、P10(租户管理后台,部分复用) 状态:评审通过,可进入实施
0. 文档背景与目标
0.1 问题来源
P17 实现了助教客户归属、任务生成、客户转移三大引擎能力,但所有数据仅存在于数据库中,运营团队无法:
- 查看客户转移日志(谁被转给了谁、为什么、三重保护检查结果)
- 审核
pending_review状态的任务(转移次数超限后需人工介入) - 按门店调整任务生成参数(召回阈值、转移保护参数等)
- 监控任务生成器的运行健康度(生成/替换/跳过/转移统计)
同时,现有 http://localhost:5173/trigger-jobs 定时任务页面功能较基础(仅展示 + 手动执行),需要评估是否扩展为更完整的调度监控面板。
0.2 本文档目标
- 定义 admin-web 中 P17 相关的三个新页面(转移日志、待审核任务、参数管理)
- 评估现有 trigger-jobs 页面的扩展需求
- 明确后端 API 需求(含 Pydantic schema 定义)
- 给出优先级排序和实施建议
- 明确技术实现方案(前端组件结构、路由注册、权限控制)
0.3 技术栈概要
| 层 | 技术 |
|---|---|
| 前端 | React 19 + Vite 6 + Ant Design 5 + axios + Zustand + react-router-dom 7 |
| API 客户端 | apps/admin-web/src/api/client.ts,baseURL = /api,JWT 自动附加 + 401 自动刷新 |
| 后端 | FastAPI + Pydantic v2 + asyncpg |
| 认证 | JWT(CurrentUser 含 user_id / site_id / roles),Depends(get_current_user) |
| 响应包装 | 全局中间件自动 { code: 0, data: <body> },前端 axios 拦截器自动解包 |
1. 现有 trigger-jobs 页面评估
1.1 当前能力
| 功能 | 状态 |
|---|---|
| 列出所有定时任务(名称、触发方式、配置、状态) | ✅ 已实现 |
| 展示上次/下次执行时间 | ✅ 已实现 |
| 展示最近错误信息 | ✅ 已实现 |
| 手动执行按钮(需确认) | ✅ 已实现 |
| 刷新按钮 | ✅ 已实现 |
1.2 缺失能力(待评估)
| 功能 | 优先级 | 说明 |
|---|---|---|
| 执行历史记录 | 中 | 当前只有 last_run_at,无法查看历史执行记录和每次的统计结果 |
| 任务启用/禁用开关 | 中 | 当前只能查看状态,无法在界面上切换 |
| 执行结果统计 | 高 | task_generator 的 created/replaced/skipped/transferred 统计应可视化 |
| Cron 表达式可编辑 | 低 | 当前触发配置只读,修改需直接改数据库 |
| 执行耗时监控 | 低 | 无执行耗时记录 |
1.3 建议
trigger-jobs 页面的核心定位是"调度监控",不宜过度膨胀。建议:
- 短期:在现有页面增加"最近执行结果"列(展示 task_generator 的 stats JSON)
- 中期:新增"执行历史"抽屉(点击任务名展开,展示最近 N 次执行记录)
- 长期:考虑是否需要独立的"调度中心"页面(类似 Airflow UI)
决策(O6):暂不新增
biz.trigger_job_execution_log表。短期通过在trigger_jobs表新增last_stats JSONB字段记录最近一次执行统计即可满足需求。中期再评估是否需要完整执行历史表。
2. 新增页面设计
2.1 页面一:客户转移日志(Transfer Log)
路由:/task-engine/transfer-log
目标用户:运营管理员(超级管理员看全部,门店管理员看本店)
数据源:biz.coach_task_transfer_log
展示内容:
| 列 | 数据来源 | 说明 |
|---|---|---|
| 转移时间 | created_at |
降序排列 |
| 门店 | site_id → biz.sites.site_name |
JOIN 门店表 |
| 客户 | member_id → 会员昵称/手机号 |
FDW 关联 ETL 库会员维度表 |
| 原助教 | from_assistant_id → 助教姓名 |
FDW 关联 ETL 库助教维度表 |
| 新助教 | to_assistant_id → 助教姓名 |
同上 |
| 转移原因 | transfer_reason |
文本展示 |
| 转移得分 | transfer_score |
数值,保留 2 位小数 |
| 保护检查 | guard_checks |
JSON → 三项检查结果(✅/❌) |
筛选条件:
- 门店(下拉选择,门店管理员自动锁定本店)
- 时间范围(日期选择器,默认最近 7 天)
- 助教(搜索框,支持原助教/新助教)
操作:
- 无写操作,纯查看
- 支持导出 CSV(后续迭代)
决策(O3):转移日志不关联 WBI/NCI/RS 快照。原因:(1) 转移时刻的指数值可通过
transfer_reason文本和transfer_score间接推断;(2) 存储快照会增加coach_task_transfer_log表宽度,且历史指数可从 DWS 层按日期回溯。如后续运营反馈需要,可在guard_checksJSONB 中追加指数快照字段,无需 DDL 变更。
2.2 页面二:待审核任务(Pending Review)
路由:/task-engine/pending-review
目标用户:运营管理员(超级管理员看全部,门店管理员看本店)
数据源:biz.coach_tasks WHERE status = 'pending_review'
展示内容:
| 列 | 数据来源 | 说明 |
|---|---|---|
| 创建时间 | created_at |
降序排列 |
| 门店 | site_id → 门店名称 |
|
| 客户 | member_id → 会员昵称 |
FDW 关联 |
| 当前助教 | assistant_id → 助教姓名 |
FDW 关联 |
| 任务类型 | task_type |
中文映射(见下方枚举表) |
| 累计转移次数 | transfer_count |
|
| 优先级分 | priority_score |
WBI/NCI 分值 |
任务类型中文映射:
| task_type | 中文 |
|---|---|
high_priority_recall |
高优先召回 |
priority_recall |
优先召回 |
follow_up_visit |
客户回访 |
relationship_building |
关系构建 |
操作:
| 操作 | 说明 |
|---|---|
| 重新分配 | 弹窗选择目标助教 → 调用 reassign API → 原任务标记 transferred,新任务 active |
| 关闭任务 | 弹窗填写关闭原因 → 调用 close API → 原任务标记 inactive |
| 查看转移历史 | 抽屉展示该客户的所有转移记录(复用转移日志 API) |
决策(O1):重新分配的候选助教列表获取方式 — 复用 P17 的转移候选逻辑。后端新增
GET /api/admin/task-engine/pending-review/{task_id}/candidates端点,内部调用fdw_queries.get_pool_assistants(member_id)获取 POOL 助教列表,按转移得分排序返回。前端展示为下拉选择框,显示助教姓名 + 转移得分。若 POOL 为空,候选列表返回空数组,界面提示"该客户暂无符合转移条件的助教,请联系 ETL 团队确认数据覆盖情况",不提供降级到全店助教的选项(P17 明确规定 UNASSIGNED 助教永远不分配)。若确实有紧急人工干预需求,可提供"强制指定"按钮,需额外二次确认弹窗,且操作记录中标注source: "manual_override",写入transfer_log.transfer_reason。
2.3 页面三:任务引擎参数管理(Task Engine Config)
路由:/task-engine/config
目标用户:超级管理员(门店管理员只读)
数据源:biz.cfg_task_generator_params
展示内容:
表格形式,按参数分组,每行展示全局默认值 + 各门店覆盖值:
| 列 | 说明 |
|---|---|
| 参数名 | param_key,中文描述 |
| 全局默认值 | site_id IS NULL 的记录 |
| 门店覆盖值 | 特定 site_id 的记录(无覆盖时显示"使用默认"灰色标签) |
| 说明 | description 字段 |
| 操作 | 编辑 / 删除覆盖 |
参数列表(来自 P17 第 5 节):
| 参数 | 默认值 | 中文说明 | 校验规则 |
|---|---|---|---|
high_priority_recall_threshold |
7.0 | 高优先召回阈值 | 0-10,numeric |
priority_recall_threshold |
5.0 | 优先召回阈值 | 0-10,numeric |
rs_min_for_relationship |
1.0 | 关系构建 RS 下限 | 0-10,numeric |
rs_max_for_relationship |
6.0 | 关系构建 RS 上限 | 0-10,> rs_min |
consecutive_recall_fail_cycles |
3 | 连续失败触发转移的轮数 | 1-10,integer |
min_wbi_for_transfer |
5.0 | 触发转移的最低 WBI | 0-10,numeric |
guard_assistant_coverage_ratio |
0.5 | 门店助教绑定率保护阈值 | 0-1,numeric |
guard_new_assistant_days |
10 | 新助教入驻保护天数 | 1-90,integer |
transfer_score_w_rs |
0.5 | 转移排序 RS 权重 | 0-1,三项之和 = 1 |
transfer_score_w_ms |
0.3 | 转移排序 MS 权重 | 0-1,三项之和 = 1 |
transfer_score_w_ml |
0.2 | 转移排序 ML 权重 | 0-1,三项之和 = 1 |
max_transfer_count |
2 | 单客户最大转移次数 | 1-5,integer |
follow_up_visit_retention_hours |
48 | 回访任务保留时长(小时) | 1-168,integer |
操作:
| 操作 | 说明 | 权限 |
|---|---|---|
| 编辑参数值 | 行内编辑,保存后立即生效 | 超级管理员 |
| 新增门店覆盖 | 选择门店 + 参数,设置覆盖值 | 超级管理员 |
| 删除门店覆盖 | 恢复使用全局默认值 | 超级管理员 |
| 重置为默认 | 将全局默认值恢复为 P17 定义的初始值 | 超级管理员 |
前端校验规则:
- 权重参数(
w_rs+w_ms+w_ml)之和必须 = 1.0(容差 0.001)。前端将三个权重参数合并为一个"权重配置"卡片,同时展示三个输入框,整体保存。后端 PUT 接口对权重类参数做联合校验:收到任一权重参数修改时,自动读取当前另外两个权重的值做求和校验 - 阈值参数范围 0-10(与指数分值范围一致)
rs_max_for_relationship必须 >rs_min_for_relationship- 修改后弹出确认对话框,展示变更前后对比
决策(O2):参数修改直接生效,不需要审批流程。原因:(1) 参数修改频率极低(预计每月 1-2 次);(2) 修改记录通过
updated_at字段和后端日志可追溯;(3) 引入审批流程会增加不必要的复杂度。后续如需审批,可在cfg_task_generator_params表新增updated_by字段记录操作人。
3. 后端 API 需求
3.1 转移日志 API
路由文件:apps/backend/app/routers/admin_task_engine.py(新建)
GET /api/admin/task-engine/transfer-log
Query: site_id?, from_date?, to_date?, assistant_id?, page=1, page_size=20
Response: { items: TransferLogItem[], total: int }
权限: get_current_user(门店管理员自动按 site_id 过滤)
GET /api/admin/task-engine/transfer-log/{member_id}/history
Response: { items: TransferLogItem[] }
权限: get_current_user
SQL 示例(转移日志列表):
SELECT
tl.id, tl.site_id, tl.member_id,
tl.from_assistant_id, tl.to_assistant_id,
tl.transfer_reason, tl.transfer_score,
tl.guard_checks, tl.created_at,
s.site_name
FROM biz.coach_task_transfer_log tl
JOIN biz.sites s ON s.site_id = tl.site_id
WHERE ($1::bigint IS NULL OR tl.site_id = $1)
AND ($2::date IS NULL OR tl.created_at >= $2)
AND ($3::date IS NULL OR tl.created_at < $3 + INTERVAL '1 day')
AND ($4::bigint IS NULL OR tl.from_assistant_id = $4 OR tl.to_assistant_id = $4)
ORDER BY tl.created_at DESC
LIMIT $5 OFFSET $6;
姓名关联实现方式:由于 postgres_fdw 不传递 GUC 参数,不能在 admin-web 后端直接用 FDW JOIN。实现上采用 Python 层批量合并:先从
biz.coach_task_transfer_log取分页数据,然后收集所有member_id和assistant_id,批量调用fdw_queries.get_member_names(member_ids)和新增的fdw_queries.get_assistant_names(assistant_ids)工具函数(通过_fdw_context()创建独立 ETL 连接),最后在 Python 层做字典合并。待审核任务列表同理。避免在 SQL 里做跨库 JOIN。
3.2 待审核任务 API
GET /api/admin/task-engine/pending-review
Query: site_id?, page=1, page_size=20
Response: { items: PendingReviewItem[], total: int }
权限: get_current_user
GET /api/admin/task-engine/pending-review/{task_id}/candidates
Response: { candidates: CandidateAssistant[] }
权限: get_current_user
说明: 返回可接收转移的候选助教列表(POOL 助教 + 降级全店助教)
POST /api/admin/task-engine/pending-review/{task_id}/reassign
Body: { to_assistant_id: int }
Response: { success: bool, new_task_id: int }
权限: get_current_user + roles 包含 'super_admin'
副作用: 原任务 status → 'transferred',新建 active 任务,写入 transfer_log
POST /api/admin/task-engine/pending-review/{task_id}/close
Body: { reason: str }
Response: { success: bool }
权限: get_current_user + roles 包含 'super_admin'
副作用: 任务 status → 'inactive',abandon_reason = reason
3.3 参数管理 API
GET /api/admin/task-engine/config
Query: site_id?(不传返回全局默认 + 所有门店覆盖)
Response: { params: ConfigParam[] }
权限: get_current_user
PUT /api/admin/task-engine/config/{param_id}
Body: { param_value: float }
Response: { success: bool }
权限: get_current_user + roles 包含 'super_admin'
POST /api/admin/task-engine/config
Body: { site_id: int, param_key: str, param_value: float }
Response: { success: bool, id: int }
权限: get_current_user + roles 包含 'super_admin'
DELETE /api/admin/task-engine/config/{param_id}
Response: { success: bool }
权限: get_current_user + roles 包含 'super_admin'
约束: 不允许删除全局默认值(site_id IS NULL),仅允许删除门店覆盖
4. Pydantic Schema 定义
文件位置:apps/backend/app/schemas/admin_task_engine.py(新建)
from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, Field
# ── 转移日志 ──
class TransferLogItem(BaseModel):
id: int
site_id: int
site_name: str = ""
member_id: int
member_name: str = "" # FDW 关联
from_assistant_id: int
from_assistant_name: str = "" # FDW 关联
to_assistant_id: int
to_assistant_name: str = "" # FDW 关联
transfer_reason: str | None = None
transfer_score: float | None = None
guard_checks: dict | None = None
created_at: datetime
class TransferLogPage(BaseModel):
items: list[TransferLogItem]
total: int
# ── 待审核任务 ──
class PendingReviewItem(BaseModel):
id: int
site_id: int
site_name: str = ""
member_id: int
member_name: str = ""
assistant_id: int
assistant_name: str = ""
task_type: str
task_type_label: str = "" # 中文映射
transfer_count: int = 0
priority_score: float | None = None
created_at: datetime
class PendingReviewPage(BaseModel):
items: list[PendingReviewItem]
total: int
class CandidateAssistant(BaseModel):
assistant_id: int
assistant_name: str = ""
rs_display: float = 0
ms_display: float = 0
ml_display: float = 0
transfer_score: float = 0 # w_rs*rs + w_ms*ms + w_ml*ml
source: str = "pool" # "pool" | "manual_override"(强制指定标记)
class CandidateListResponse(BaseModel):
candidates: list[CandidateAssistant]
class ReassignRequest(BaseModel):
to_assistant_id: int
class ReassignResponse(BaseModel):
success: bool
new_task_id: int | None = None
class CloseRequest(BaseModel):
reason: str = Field(..., min_length=1, max_length=500)
class CloseResponse(BaseModel):
success: bool
# ── 参数管理 ──
class ConfigParam(BaseModel):
id: int
site_id: int | None = None
site_name: str | None = None # site_id 非空时关联
param_key: str
param_value: float
description: str | None = None
updated_at: datetime
class ConfigParamList(BaseModel):
params: list[ConfigParam]
class ConfigParamUpdate(BaseModel):
param_value: float
class ConfigParamCreate(BaseModel):
site_id: int
param_key: str = Field(..., max_length=64)
param_value: float
class ConfigParamResponse(BaseModel):
success: bool
id: int | None = None
5. 权限控制方案
决策(O5):采用双层权限模型 — 超级管理员(
super_admin)全局访问 + 门店管理员(site_admin)本店只读。
5.1 权限矩阵
| 页面/操作 | super_admin | site_admin |
|---|---|---|
| 转移日志 — 查看全部 | ✅ | ❌ |
| 转移日志 — 查看本店 | ✅ | ✅ |
| 待审核任务 — 查看全部 | ✅ | ❌ |
| 待审核任务 — 查看本店 | ✅ | ✅ |
| 待审核任务 — 重新分配 | ✅ | ❌ |
| 待审核任务 — 关闭任务 | ✅ | ❌ |
| 参数管理 — 查看 | ✅ | ✅(只读) |
| 参数管理 — 编辑/新增/删除 | ✅ | ❌ |
5.2 后端实现
与 P10 账号体系的关系:P10 租户管理后台(
/tenant-admins)的门店管理员账号与本页面的site_admin角色使用同一套 JWT 体系,通过roles字段区分权限范围。site_admin在 P18 页面为只读访问,在 P10 页面可能有写操作权限。两者面向的用户群不同(P18 面向平台运营,P10 面向门店管理员),但共享认证基础设施。
# apps/backend/app/auth/dependencies.py 中已有 CurrentUser(roles=[...])
# 新增权限检查辅助函数:
from fastapi import HTTPException, status
def require_super_admin(user: CurrentUser) -> None:
"""写操作权限检查:仅超级管理员可执行。"""
if "super_admin" not in user.roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="仅超级管理员可执行此操作",
)
def filter_by_site(user: CurrentUser, query_site_id: int | None) -> int | None:
"""读操作门店过滤:门店管理员强制锁定本店。"""
if "super_admin" in user.roles:
return query_site_id # 超级管理员可查看任意门店
return user.site_id # 门店管理员强制本店
5.3 前端实现
// 在页面组件中检查权限,控制操作按钮显示
const isSuperAdmin = user.roles.includes('super_admin');
// 转移日志:门店筛选器
// super_admin → 显示全部门店下拉
// site_admin → 隐藏门店筛选器,API 自动按 site_id 过滤
// 待审核任务:操作列
// super_admin → 显示"重新分配"和"关闭"按钮
// site_admin → 不显示操作列
// 参数管理:编辑按钮
// super_admin → 显示编辑/新增/删除按钮
// site_admin → 隐藏所有写操作按钮
6. 前端实现方案
6.1 导航结构
在 apps/admin-web/src/App.tsx 的 NAV_ITEMS 数组中新增"任务引擎"菜单组:
// 新增 import
import { ApartmentOutlined } from "@ant-design/icons";
import TransferLog from "./pages/TransferLog";
import PendingReview from "./pages/PendingReview";
import TaskEngineConfig from "./pages/TaskEngineConfig";
// NAV_ITEMS 中新增(插入在"定时任务"之后)
{
key: "task-engine-group",
icon: <ApartmentOutlined />,
label: "任务引擎",
children: [
{ key: "/task-engine/transfer-log", label: "转移日志" },
{ key: "/task-engine/pending-review", label: "待审核任务" },
{ key: "/task-engine/config", label: "参数管理" },
],
},
最终侧边栏结构:
📋 任务配置 /
📋 任务管理 /task-manager
📊 ETL 状态 /etl-status
⏰ 定时任务 /trigger-jobs
🔀 任务引擎(新增)
├── 转移日志 /task-engine/transfer-log
├── 待审核任务 /task-engine/pending-review
└── 参数管理 /task-engine/config
💾 数据库 /db-viewer
📄 日志 /log-viewer
⚙️ 环境配置 /env-config
🖥️ 运维面板 /ops-panel
👥 租户管理员 /tenant-admins
🤖 AI 监控
├── 运行总览 /ai/dashboard
├── 调度状态 /ai/trigger-jobs
├── 调用明细 /ai/run-logs
└── 手动操作 /ai/operations
🐛 开发调试日志 /dev-trace
6.2 路由注册
在 AppLayout 的 <Routes> 中新增:
<Route path="/task-engine/transfer-log" element={<TransferLog />} />
<Route path="/task-engine/pending-review" element={<PendingReview />} />
<Route path="/task-engine/config" element={<TaskEngineConfig />} />
同时修改 <Menu> 的 defaultOpenKeys,补充 task-engine-group 前缀:
defaultOpenKeys={[
...(location.pathname.startsWith("/ai/") ? ["ai-group"] : []),
...(location.pathname.startsWith("/task-engine/") ? ["task-engine-group"] : []),
]}
6.3 API 模块
新建 apps/admin-web/src/api/taskEngine.ts:
import { apiClient } from "./client";
// ── 转移日志 ──
export interface TransferLogItem { /* 同 Pydantic schema */ }
export interface TransferLogPage { items: TransferLogItem[]; total: number; }
export async function fetchTransferLog(params: {
site_id?: number; from_date?: string; to_date?: string;
assistant_id?: number; page?: number; page_size?: number;
}): Promise<TransferLogPage> {
const { data } = await apiClient.get("/admin/task-engine/transfer-log", { params });
return data;
}
export async function fetchMemberTransferHistory(memberId: number): Promise<TransferLogItem[]> {
const { data } = await apiClient.get(`/admin/task-engine/transfer-log/${memberId}/history`);
return data.items;
}
// ── 待审核任务 ──
export interface PendingReviewItem { /* 同 Pydantic schema */ }
export async function fetchPendingReview(params: {
site_id?: number; page?: number; page_size?: number;
}): Promise<{ items: PendingReviewItem[]; total: number }> {
const { data } = await apiClient.get("/admin/task-engine/pending-review", { params });
return data;
}
export async function fetchCandidates(taskId: number) {
const { data } = await apiClient.get(`/admin/task-engine/pending-review/${taskId}/candidates`);
return data.candidates;
}
export async function reassignTask(taskId: number, toAssistantId: number) {
const { data } = await apiClient.post(`/admin/task-engine/pending-review/${taskId}/reassign`, {
to_assistant_id: toAssistantId,
});
return data;
}
export async function closeTask(taskId: number, reason: string) {
const { data } = await apiClient.post(`/admin/task-engine/pending-review/${taskId}/close`, {
reason,
});
return data;
}
// ── 参数管理 ──
export interface ConfigParam { /* 同 Pydantic schema */ }
export async function fetchConfig(siteId?: number): Promise<ConfigParam[]> {
const { data } = await apiClient.get("/admin/task-engine/config", {
params: siteId ? { site_id: siteId } : {},
});
return data.params;
}
export async function updateConfig(paramId: number, value: number) {
const { data } = await apiClient.put(`/admin/task-engine/config/${paramId}`, {
param_value: value,
});
return data;
}
export async function createConfig(siteId: number, paramKey: string, value: number) {
const { data } = await apiClient.post("/admin/task-engine/config", {
site_id: siteId, param_key: paramKey, param_value: value,
});
return data;
}
export async function deleteConfig(paramId: number) {
const { data } = await apiClient.delete(`/admin/task-engine/config/${paramId}`);
return data;
}
6.4 页面组件结构
每个页面遵循现有 TriggerJobs.tsx 的模式:useState + useCallback + useEffect + Ant Design Table。
| 文件 | 说明 |
|---|---|
apps/admin-web/src/pages/TransferLog.tsx |
转移日志页面 |
apps/admin-web/src/pages/PendingReview.tsx |
待审核任务页面 |
apps/admin-web/src/pages/TaskEngineConfig.tsx |
参数管理页面 |
apps/admin-web/src/api/taskEngine.ts |
API 调用模块 |
apps/backend/app/routers/admin_task_engine.py |
后端路由 |
apps/backend/app/schemas/admin_task_engine.py |
Pydantic schema |
7. 数据库变更需求
7.1 trigger_jobs 表扩展(P1 优先级)
last_error和description字段已存在于数据库中(已通过\d biz.trigger_jobs确认),无需重复添加。
-- 仅 last_stats 是真正新增的
ALTER TABLE biz.trigger_jobs
ADD COLUMN IF NOT EXISTS last_stats JSONB;
COMMENT ON COLUMN biz.trigger_jobs.last_stats
IS '最近一次执行的统计结果 JSON,如 {"created":5,"replaced":2,"skipped":10,"transferred":1}';
last_stats 由 task_generator.run() 在执行完成后写入,是运营监控任务引擎健康度的最直接手段。
7.2 cfg_task_generator_params 表扩展
-- 记录修改人(审计追溯)
ALTER TABLE biz.cfg_task_generator_params
ADD COLUMN IF NOT EXISTS updated_by BIGINT;
COMMENT ON COLUMN biz.cfg_task_generator_params.updated_by IS '最近修改人 user_id,用于审计追溯';
7.3 无需新建表
P18 不新增业务表。所有数据来源于 P17 已创建的:
biz.coach_task_transfer_log(转移日志)biz.cfg_task_generator_params(参数配置)biz.coach_tasks(任务表,含pending_review状态)
8. 优先级排序
| 优先级 | 功能 | 理由 | 预估工时 |
|---|---|---|---|
| P0 | 待审核任务页面 | pending_review 任务需要人工介入,无此页面则转移超限的任务无法处理 |
后端 4h + 前端 4h |
| P1 | 参数管理页面 | 门店级参数调整是日常运营需求,否则每次改参数都要直接改数据库 | 后端 3h + 前端 4h |
| P1 | 转移日志页面 | 运营需要追踪转移效果,但短期可通过数据库查询替代 | 后端 3h + 前端 3h |
| P1 | trigger-jobs last_stats 展示 | 任务引擎上线后运营最直接的监控手段,能看到 created/replaced/skipped/transferred 数字 | 后端 1h + 前端 1h |
| P2 | trigger-jobs 启用/禁用开关 | 低频操作,可通过数据库修改 | 后端 0.5h + 前端 0.5h |
建议实施顺序:
- 后端:先建 router + schema 骨架(
admin_task_engine.py),再逐个实现端点 - 前端:先注册路由和导航,再逐个实现页面组件
- P0 → P1(参数管理)→ P1(转移日志)→ P2 → P3
9. 开放问题决策汇总
| # | 问题 | 决策 | 理由 |
|---|---|---|---|
| O1 | 重新分配候选助教列表获取方式 | 复用 P17 转移候选逻辑(仅 POOL 助教),POOL 为空时返回空列表 + 提示,紧急情况提供"强制指定"(需二次确认 + manual_override 标记) | 严格遵守 P17 的 UNASSIGNED 永不分配约束 |
| O2 | 参数修改是否需要审批流程 | 直接生效,不需要审批 | 修改频率极低,updated_at + updated_by 可追溯 |
| O3 | 转移日志是否关联 WBI/NCI/RS 快照 | 不关联,通过 guard_checks JSONB 间接推断 |
避免表膨胀,历史指数可从 DWS 层回溯 |
| O4 | 是否需要运行总览 Dashboard | 暂不需要,后续根据运营反馈评估 | 当前数据量不足以支撑有意义的趋势图 |
| O5 | 权限模型 | 双层:super_admin 全局 + site_admin 本店只读 | 与现有 JWT roles 体系一致,实现成本最低 |
| O6 | trigger-jobs 执行历史是否需要新表 | 暂不新增,用 last_stats JSONB 字段过渡 |
短期够用,中期再评估完整历史表 |
决策(O4):暂不建设运行总览 Dashboard。原因:(1) P17 刚上线,历史数据不足以生成有意义的趋势图;(2) 三个功能页面已覆盖核心运营需求;(3) 后续积累 1-2 个月数据后,可作为 P19 或 P20 独立评估。
10. 验收标准
| # | 验收项 | 判定方式 |
|---|---|---|
| AC1 | 转移日志页面可按门店/时间/助教筛选 | 手动验证筛选条件组合 |
| AC2 | 门店管理员只能看到本店转移日志 | 用 site_admin 角色登录验证 |
| AC3 | 待审核任务页面展示所有 pending_review 任务 | 数据库插入测试数据后验证 |
| AC4 | 重新分配操作正确创建新任务并标记原任务 | 执行后检查 coach_tasks + transfer_log |
| AC5 | 关闭任务操作正确更新状态和原因 | 执行后检查 coach_tasks.status + abandon_reason |
| AC6 | 参数管理页面展示全局默认 + 门店覆盖 | 插入门店覆盖数据后验证 |
| AC7 | 权重参数之和校验生效 | 尝试设置不等于 1.0 的权重组合 |
| AC8 | 超级管理员可执行所有写操作 | 用 super_admin 角色验证 |
| AC9 | 门店管理员无法执行写操作 | 用 site_admin 角色验证 403 |
| AC10 | 不允许删除全局默认参数 | 尝试删除 site_id IS NULL 的记录 |
11. 与现有 PRD/模块的关系
| 文档/模块 | 关系说明 |
|---|---|
| P17 | 本 PRD 是 P17 的管理后台配套,P17 提供数据,P18 提供可视化和操作入口 |
| P10(租户管理后台) | P10 是面向门店管理员的独立应用,P18 是面向平台运营的 admin-web 扩展。两者共享 JWT 认证体系(同一套 roles 字段),site_admin 在 P18 为只读、在 P10 可能有写权限。详见第 5.2 节说明 |
| trigger-jobs 现有页面 | 本 PRD 评估其扩展需求,但不强制改动,保持现有功能稳定 |
| AI 监控模块 | 参考其 API 设计模式(JWT + admin 角色、分页列表 + 详情) |
附录 A:后端路由注册
在 apps/backend/app/main.py 中注册新路由:
from app.routers import admin_task_engine
app.include_router(admin_task_engine.router)
附录 B:变更历史
| 版本 | 日期 | 变更内容 |
|---|---|---|
| v1.0-draft | 2026-03-24 | 初始草稿,定义 3 个页面 + 9 个 API + 6 个开放问题 |
| v2.0-ready-for-review | 2026-03-24 | 关闭全部 6 个开放问题;补充 Pydantic schema、权限矩阵、前端实现方案、SQL 示例、校验规则、验收标准;升级为待评审 |
| v2.1-reviewed | 2026-03-24 | 评审修正:(1) DDL 移除已存在的 last_error/description,仅新增 last_stats;(2) defaultOpenKeys 补充 task-engine-group;(3) last_stats 优先级 P2→P1;(4) 候选助教降级方案改为空结果+提示+强制指定,不放开到全店助教;(5) 权重参数改为卡片整体编辑+联合校验;(6) 明确姓名关联走 Python 层批量合并;(7) 补充 site_admin 与 P10 账号体系关系说明 |