Files
Neo-ZQYY/docs/prd/specs/P18-admin-task-engine-dashboard.md
Neo 6f8f12314f 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>
2026-04-06 00:03:48 +08:00

744 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P18管理后台 — 任务引擎运营看板与参数管理
> 版本v2.1-reviewed | 日期2026-03-24 | 作者AI待评审
> 依赖P17助教客户归属与任务生成引擎、P10租户管理后台部分复用
> 状态:评审通过,可进入实施
---
## 0. 文档背景与目标
### 0.1 问题来源
P17 实现了助教客户归属、任务生成、客户转移三大引擎能力,但所有数据仅存在于数据库中,运营团队无法:
- 查看客户转移日志(谁被转给了谁、为什么、三重保护检查结果)
- 审核 `pending_review` 状态的任务(转移次数超限后需人工介入)
- 按门店调整任务生成参数(召回阈值、转移保护参数等)
- 监控任务生成器的运行健康度(生成/替换/跳过/转移统计)
同时,现有 `http://localhost:5173/trigger-jobs` 定时任务页面功能较基础(仅展示 + 手动执行),需要评估是否扩展为更完整的调度监控面板。
### 0.2 本文档目标
1. 定义 admin-web 中 P17 相关的三个新页面(转移日志、待审核任务、参数管理)
2. 评估现有 trigger-jobs 页面的扩展需求
3. 明确后端 API 需求(含 Pydantic schema 定义)
4. 给出优先级排序和实施建议
5. 明确技术实现方案(前端组件结构、路由注册、权限控制)
### 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_checks` JSONB 中追加指数快照字段,无需 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-10numeric |
| `priority_recall_threshold` | 5.0 | 优先召回阈值 | 0-10numeric |
| `rs_min_for_relationship` | 1.0 | 关系构建 RS 下限 | 0-10numeric |
| `rs_max_for_relationship` | 6.0 | 关系构建 RS 上限 | 0-10> rs_min |
| `consecutive_recall_fail_cycles` | 3 | 连续失败触发转移的轮数 | 1-10integer |
| `min_wbi_for_transfer` | 5.0 | 触发转移的最低 WBI | 0-10numeric |
| `guard_assistant_coverage_ratio` | 0.5 | 门店助教绑定率保护阈值 | 0-1numeric |
| `guard_new_assistant_days` | 10 | 新助教入驻保护天数 | 1-90integer |
| `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-5integer |
| `follow_up_visit_retention_hours` | 48 | 回访任务保留时长(小时) | 1-168integer |
**操作**
| 操作 | 说明 | 权限 |
|------|------|------|
| 编辑参数值 | 行内编辑,保存后立即生效 | 超级管理员 |
| 新增门店覆盖 | 选择门店 + 参数,设置覆盖值 | 超级管理员 |
| 删除门店覆盖 | 恢复使用全局默认值 | 超级管理员 |
| 重置为默认 | 将全局默认值恢复为 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 示例(转移日志列表)**
```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`(新建)
```python
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 面向门店管理员),但共享认证基础设施。
```python
# 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 前端实现
```typescript
// 在页面组件中检查权限,控制操作按钮显示
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` 数组中新增"任务引擎"菜单组:
```typescript
// 新增 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>` 中新增:
```tsx
<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` 前缀:
```tsx
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`
```typescript
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` 确认),无需重复添加。
```sql
-- 仅 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 表扩展
```sql
-- 记录修改人(审计追溯)
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 |
**建议实施顺序**
1. 后端:先建 router + schema 骨架(`admin_task_engine.py`),再逐个实现端点
2. 前端:先注册路由和导航,再逐个实现页面组件
3. 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` 中注册新路由:
```python
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 账号体系关系说明 |