Files
Neo-ZQYY/docs/prd/specs/P17-assistant-ownership-task-engine.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

517 lines
20 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.
# P17助教客户归属与任务生成引擎 — 商业逻辑 PRD
> 版本v1.0 | 日期2026-03-24 | 作者Neo
> 依赖P4核心业务层、ETL INDEX 层RS/OS/MS/ML/WBI/NCI
---
## 0. 文档背景与目标
### 0.1 问题来源
本文档源自 2026-03-24 的三次会话(#20 / #21 / #22)对任务生成器的持续审查,核心发现两大系统性问题:
**问题 A — 客户归属失控**:任务生成器在分配"召回类"任务时,仅凭"是否绑定微信"来圈定助教候选池,而不是依据助教与客户之间真实发生的服务关系。导致全店所有客户的召回任务都堆给唯一绑定了微信的助教,任务量严重失衡。
**问题 B — 客户转移无保护**:当召回连续失败后需要将客户转给其他助教跟进,但现行逻辑缺少任何保护机制:
- 没有"门店助教规模保护"——助教数量未达标时就启动转移,容易混乱
- 没有"入驻时间保护"——新助教未经历足够交互就被分配陌生客户
- 没有"服务关系门槛"——客户有可能被转给从未服务过他的助教,关系冷启动成本极高
### 0.2 类比:助教 = 台球厅的销售 + 客户运营
助教的角色本质上同时承担两件事:
| 职能 | 销售视角 | 客户运营视角 |
|------|---------|-------------|
| 核心工作 | 把"流失/新客"召回到店并成交 | 和已服务客户维持稳定关系 |
| 核心指标 | WBI流失风险分、NCI新客转化分 | RS关系强度、MS升温动量 |
| 任务类型 | 高优先召回、优先召回 | 关系构建、客户回访 |
| 客户归属逻辑 | 谁有机会承接该客户的召回 | 谁是该客户的主责助教 |
因此,**客户归属**和**任务分配**需要两套紧密联动但逻辑独立的规则。
### 0.3 本文档目标
1. 定义清晰、可落地的**客户归属算法**(基于 OS/RS 四象限模型)
2. 定义完整、可解释的**任务生成算法**(基于归属约束 + 指数门槛 + 四种任务类型)
3. 定义**客户转移保护机制**(三重保护:规模保护 + 时间保护 + 关系门槛)
4. 给出每个决策点的**参数化方案**,支持门店级配置调整
---
## 1. 概念词典
| 术语 | 通俗解释 | 技术对应 |
|------|---------|----------|
| **RS关系强度分** | 助教和客户之间服务关系的紧密程度上过的课越多、越近期分越高0-10分 | `rs_display`,来自 `dws_member_assistant_relation_index` |
| **OS归属份额** | 在服务过该客户的所有助教中,该助教占多大比重;决定"主责/共管/待认领"标签 | `os_label ∈ {MAIN, COMANAGE, POOL, UNASSIGNED}` |
| **MS升温动量分** | 最近服务是在增多还是减少升温则分高降温则分低0-10分 | `ms_display` |
| **ML付费关联分** | 客户的消费台账中有多少是由该助教直接带来的0-10分 | `ml_display` |
| **WBI流失风险分** | 这个客户有多久没来了、来的频率是否下降分越高越需要主动联系0-10分 | `display_score`,来自 `dws_member_winback_index` |
| **NCI新客转化分** | 新客户被转化为回头客的紧迫程度0-10分 | `display_score`,来自 `dws_member_newconv_index` |
| **客户转移** | 原主责助教召回失败超过阈值后,系统将该客户的召回任务扩展给其他有服务关系的助教 | task_generator 中的转移逻辑 |
| **门店规模保护** | 若店内绑定微信的在职助教比例不足50%,禁用客户转移功能 | `guard_assistant_coverage_ratio` |
| **入驻时间保护** | 助教绑定微信后10天内不接收转移客户 | `guard_new_assistant_days` |
| **服务关系门槛** | 只把客户转给曾经服务过该客户的助教 | `os_label ≠ UNASSIGNED` |
---
## 2. 客户归属算法
### 2.1 设计原则
客户归属解决的问题是:**一个客户应该由哪个(些)助教负责跟进?**
台球厅的实际场景是:一个客户可能被多个助教服务过,但服务次数和亲密度差别很大。归属算法需要把这种模糊关系量化为清晰的"主责/共管/待认领/未归属"四个层级。
> 参考 CRM 行业最佳实践Salesforce/HubSpot 的 Account Ownership 模型):
> 当多名销售都服务过同一客户时,最优解不是"谁先认领谁拥有",而是用历史交互深度加权判断,避免资深关系被新人抢占。
### 2.2 OS 归属标签定义
OS 由 ETL `RelationIndexTask` 已实现,输出 `os_label` 字段,定义如下:
```
MAIN — 主责助教:在服务过该客户的助教中,该助教的 os_share 显著高于其他人
COMANAGE — 共管助教:多名助教的 os_share 差距不大RS 相对差 < 50%),均视为负责人
POOL — 待认领:有过服务记录,但 os_share 较低,属于潜在接管候选
UNASSIGNED — 无关联:从未为该客户提供过服务记录
```
**RS 相对差公式(来自 PRD 审阅 Q3.2**
```
对于助教 Ars=8和助教 Brs=5
相对差 = (8 - 5) / 8 = 0.375 < 0.5
→ 两人共管该客户COMANAGE
对于助教 Ars=9和助教 Brs=4
相对差 = (9 - 4) / 9 = 0.556 > 0.5
→ 助教 A 为主责MAIN助教 B 降为 POOL
```
### 2.3 归属判定流程
```
输入:某客户的所有 (assistant_id, rs_display) 记录
Step 1 — 过滤无效记录
rs_display = 0 → 视为无有效服务os_label = UNASSIGNED
Step 2 — 排序
按 rs_display DESC 排列所有助教
Step 3 — 主责判定
取最高分助教 A
若 A 是唯一有效助教,或与第二名 B 的相对差 ≥ 50%
→ A 标记为 MAIN其余有效助教标记为 POOL
Step 4 — 共管判定
若 A 与 B 的相对差 < 50%
→ 继续对 B 与 C 执行同样判断
→ 所有满足"与最高分相对差 < 50%"的助教标记为 COMANAGE
→ 其余有效助教标记为 POOL
Step 5 — 输出
写入 os_label + os_share + os_rank 字段(由 ETL 层计算,本 PRD 只消费结果)
```
### 2.4 归属与任务分配的映射关系
| os_label | 召回类任务 | 关系构建任务 |
|----------|-----------|-------------|
| MAIN | ✅ 有资格接收 | ✅ 有资格接收 |
| COMANAGE | ✅ 有资格接收 | ✅ 有资格接收 |
| POOL | ❌ 常规不分配;仅在"客户转移"条件触发后分配召回 | ❌ 不分配 |
| UNASSIGNED | ❌ 永不分配 | ❌ 永不分配 |
>
**关键改变**:将 WBI/NCI 召回任务的候选池从「绑定微信的助教」改为「对该客户 os_label ∈ {MAIN, COMANAGE} 的助教」。这是修复"小燕任务爆炸"问题的核心。
---
## 3. 客户转移机制
### 3.1 触发条件
客户转移是召回失败后的兜底机制,类似销售中的「线索升级」:
```
触发条件:
某客户的主责/共管助教MAIN/COMANAGE对该客户的召回任务
在连续 N 个任务周期内均未完成status ≠ completed
且 WBI 或 NCI 持续高于门槛值
默认参数:
consecutive_recall_fail_cycles = 3 连续3个生成周期未完成
min_wbi_for_transfer = 5.0 WBI > 5 才触发转移)
```
### 3.2 三重保护机制
客户转移在触发前,必须通过三道检查。任一检查不通过,本次不转移。
#### 保护 1 — 门店助教规模保护
```
规则:
若 (店内绑定微信的在职助教数 / 店内全部在职助教总数) < 0.5
→ 客户转移功能全局禁用
业务意义:
门店大部分助教都没绑定小程序时,系统对助教团队的覆盖率太低,
此时启动转移会造成信息盲区(被转出的任务助教看不到)。
只有当绑定率超过 50% 时,才能保障转移链路有效。
参数guard_assistant_coverage_ratio = 0.5(可配置)
```
#### 保护 2 — 入驻时间保护
```
规则:
助教首次绑定微信的时间binding_created_at距今不足 10 天
→ 该助教本轮不参与转移候选池
业务意义:
新助教刚入驻,还没有建立足够的客户印象,
贸然分配陌生客户会降低召回成功率,也打击新助教积极性。
10 天保护期给新助教建立自己客户基础的空间。
参数guard_new_assistant_days = 10可配置
```
#### 保护 3 — 服务关系门槛
```
规则:
待转入的助教对该客户的 os_label 必须 ∈ {POOL}
(即曾经服务过该客户,但目前归属份额较低)
os_label = UNASSIGNED 的助教永远不参与转移候选
业务意义:
从未服务过该客户的助教,关系完全冷启动。
不论技术上可以转,业务上也不应该这样做。
转移只在"有过接触但目前不是主责"的助教之间发生,
最大化利用已有的关系温度。
参数transfer_eligible_labels = ['POOL'](固定,不可放开到 UNASSIGNED
```
### 3.3 转移候选排序
通过三重保护后,对候选助教按以下优先级排序,取得分最高的 1 名(或多名,取决于配置):
```
转移得分 = w_rs × rs_display + w_ms × ms_display + w_ml × ml_display
默认权重:
w_rs = 0.5 (关系强度,历史服务深度)
w_ms = 0.3 (升温动量,关系是在改善还是冷却)
w_ml = 0.2 (付费关联,客户是否在该助教服务期间消费)
业务意义:
优先把客户转给"之前有服务基础、且关系正在升温、且有消费记录"的助教,
而不是随机转给"历史最高分"。MS 权重确保选的是当下状态最好的关系,
而不是过去最好的关系。
```
### 3.4 转移后的归属处理
```
转移发生后:
新助教的任务状态 = active高优先召回 or 优先召回)
原主责助教的同类型召回任务 status = 'transferred'(新增任务状态)
—— 不关闭原任务,而是标记为"已转移",供审计和历史查询
若转移后新助教也失败(连续 consecutive_recall_fail_cycles 次),
且 POOL 中还有其他候选助教,可再次转移
但每个客户的累计转移次数上限 = max_transfer_count默认 2 次)
超过上限后,任务进入 PENDING_REVIEW 状态,等待人工介入
```
---
## 4. 任务生成算法(重新设计版)
### 4.1 总体流程
```
每日 07:00 任务生成器 run() 执行:
Step 1 — 确定全店有效助教池
查询 dws_member_assistant_relation_index
取 os_label ∈ {MAIN, COMANAGE} 的所有 (assistant_id, member_id) 对
(放弃以 user_assistant_binding 为入口的旧逻辑)
Step 2 — 读取指数
对每个 assistant_id 关联的 member_id 集合:
WBI = dws_member_winback_index.display_score按 member_id 查)
NCI = dws_member_newconv_index.display_score按 member_id 查)
RS = dws_member_assistant_relation_index.rs_display按 assistant_id + member_id 查)
OS = dws_member_assistant_relation_index.os_label同上
MS = dws_member_assistant_relation_index.ms_display同上
Step 3 — 归属过滤(新增)
对每个 (assistant_id, member_id) 对:
若 os_label ∉ {MAIN, COMANAGE} → 跳过召回类任务判断
POOL 助教只在"客户转移"触发后才参与)
Step 4 — 任务类型判定 determine_task_type()
见第 4.2 节
Step 5 — 任务状态检查与写入
见第 4.3 节
Step 6 — 客户转移检查(独立子流程)
见第 3 节
Step 7 — 更新 trigger_jobs 时间戳
```
### 4.2 任务类型判定算法(四级漏斗)
```
function determine_task_type(os_label, wbi, nci, rs, has_pending_recall, has_follow_up_note):
priority_score = max(wbi, nci)
-- 漏斗第一级:高优先召回
if priority_score > 7 AND os_label ∈ {MAIN, COMANAGE}:
return 'high_priority_recall'
-- 漏斗第二级:优先召回
if priority_score > 5 AND os_label ∈ {MAIN, COMANAGE}:
return 'priority_recall'
-- 漏斗第三级:客户回访
-- (召回已完成 ETL 确认,但助教尚未提交备注)
if has_pending_recall == True AND has_follow_up_note == False:
return 'follow_up_visit'
-- 漏斗第四级:关系构建
-- RS ≤ 1 视为无有效交互,不生成任务
if 1 < rs < 6 AND os_label ∈ {MAIN, COMANAGE}:
return 'relationship_building'
return None -- 不生成任务
```
**四级漏斗的业务逻辑说明**
| 级别 | 任务 | 触发信号 | 业务含义 |
|------|------|---------|----------|
| 1 | 高优先召回 | WBI 或 NCI > 7且本人是主责/共管 | 客户流失风险极高,必须今天联系 |
| 2 | 优先召回 | WBI 或 NCI > 5且本人是主责/共管 | 客户有流失迹象,本周内联系 |
| 3 | 客户回访 | 召回成功但未备注ETL 已确认到店) | 召回成功后的温度维护,不能凉掉 |
| 4 | 关系构建 | RS 在 1-6 之间,关系有提升空间 | 日常维护客情,升温关系 |
> 注意:漏斗是互斥优先的。一个客户-助教对同一时刻只生成一条最高优先级的任务。
### 4.3 任务状态检查与写入逻辑
```
对每个 (assistant_id, member_id, new_task_type)
Case A — 已存在相同类型的 active 任务:
→ 跳过skip不更新 created_at
stats['skipped'] += 1
Case B — 已存在不同类型的 active 任务:
→ 将旧任务 status 改为 'inactive'
→ 创建新任务status = 'active'
→ 记录 coach_task_history
stats['replaced'] += 1
Case C — 不存在 active 任务:
→ 直接创建新任务
stats['created'] += 1
Case D — new_task_type = None
→ 检查 follow_up_visit 是否超过48小时 → inactive
stats['skipped'] += 1
```
### 4.4 关系构建任务的 RS 门槛说明
| RS 区间 | 含义 | 是否生成任务 |
|---------|------|------------|
| RS = 0 | 无有效服务数据 | 否 |
| RS ≤ 1 | 仅 1 次以下交互,关系未建立 | 否 |
| 1 < RS < 6 | 有初步关系但未牢固,黄金维护窗口 | 是(关系构建) |
| RS ≥ 6 | 关系已牢固,无需系统催动 | 否 |
---
## 5. 参数总览与配置说明
所有参数存储于 `biz.cfg_task_generator_params`,支持按 `site_id` 级别覆盖。
| 参数名 | 默认值 | 说明 |
|--------|--------|------|
| `high_priority_recall_threshold` | 7.0 | max(WBI,NCI) 超过此值生成高优先召回 |
| `priority_recall_threshold` | 5.0 | max(WBI,NCI) 超过此值生成优先召回 |
| `rs_min_for_relationship` | 1.0 | RS ≤ 此值不生成关系构建 |
| `rs_max_for_relationship` | 6.0 | RS ≥ 此值不生成关系构建 |
| `consecutive_recall_fail_cycles` | 3 | 连续失败多少轮触发客户转移 |
| `min_wbi_for_transfer` | 5.0 | WBI 低于此值不触发转移 |
| `guard_assistant_coverage_ratio` | 0.5 | 绑定率低于此值禁用转移 |
| `guard_new_assistant_days` | 10 | 新助教入驻保护天数 |
| `transfer_score_w_rs` | 0.5 | 转移候选排序RS 权重 |
| `transfer_score_w_ms` | 0.3 | 转移候选排序MS 权重 |
| `transfer_score_w_ml` | 0.2 | 转移候选排序ML 权重 |
| `max_transfer_count` | 2 | 单客户最大累计转移次数 |
| `follow_up_visit_retention_hours` | 48 | 回访任务最低保留时长(小时) |
## 6. 数据库变更需求
### 6.1 新增字段:`biz.coach_tasks`
```sql
-- 新增任务状态枚举值
ALTER TYPE task_status ADD VALUE IF NOT EXISTS 'transferred';
ALTER TYPE task_status ADD VALUE IF NOT EXISTS 'pending_review';
-- 新增转移追踪字段
ALTER TABLE biz.coach_tasks
ADD COLUMN transfer_count INTEGER NOT NULL DEFAULT 0,
ADD COLUMN transferred_from BIGINT REFERENCES biz.coach_tasks(id),
ADD COLUMN transferred_at TIMESTAMPTZ;
```
### 6.2 新增表:`biz.cfg_task_generator_params`
```sql
CREATE TABLE biz.cfg_task_generator_params (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT, -- NULL 表示全局默认值
param_key VARCHAR(64) NOT NULL,
param_value NUMERIC NOT NULL,
description TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (site_id, param_key)
);
```
### 6.3 新增表:`biz.coach_task_transfer_log`
```sql
CREATE TABLE biz.coach_task_transfer_log (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
from_assistant_id BIGINT NOT NULL,
to_assistant_id BIGINT NOT NULL,
from_task_id BIGINT NOT NULL REFERENCES biz.coach_tasks(id),
to_task_id BIGINT REFERENCES biz.coach_tasks(id),
transfer_reason TEXT,
guard_checks JSONB, -- 三重保护检查结果
transfer_score NUMERIC,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
---
## 7. 核心流程伪代码(供开发参考)
### 7.1 任务生成器主流程(重写版)
```python
def run() -> dict:
stats = {"created": 0, "replaced": 0, "skipped": 0, "transferred": 0}
params = load_params() # 从 cfg_task_generator_params 加载
# Step 1: 以 OS 归属为入口(取代旧的 user_assistant_binding 入口)
ownership_pairs = query("""
SELECT assistant_id, member_id, os_label,
rs_display, ms_display, ml_display
FROM app.v_dws_member_assistant_relation_index
WHERE os_label IN ('MAIN', 'COMANAGE')
""")
# Step 2: 批量读取 WBI / NCI
member_ids = {p['member_id'] for p in ownership_pairs}
wbi_map = fetch_wbi(member_ids)
nci_map = fetch_nci(member_ids)
# Step 3: 逐对生成任务
for pair in ownership_pairs:
process_pair(pair, wbi_map, nci_map, params, stats)
# Step 4: 客户转移子流程
run_transfer_check(params, stats)
update_trigger_timestamp('task_generator')
return stats
```
### 7.2 客户转移子流程
```python
def run_transfer_check(params, stats):
# 保护 1: 门店规模检查
if coverage_ratio() < params['guard_assistant_coverage_ratio']:
return # 全局禁用
# 查找连续失败达阈值的 (member_id, assistant_id) 对
for candidate in find_failed_recall_candidates(params):
pool = get_pool_assistants(candidate['member_id'])
eligible = [
a for a in pool
if days_since_binding(a) >= params['guard_new_assistant_days'] # 保护 2
and a['os_label'] == 'POOL' # 保护 3
]
if not eligible:
continue
# 按转移得分选最优候选
best = max(eligible, key=lambda a:
params['w_rs'] * a['rs'] +
params['w_ms'] * a['ms'] +
params['w_ml'] * a['ml']
)
do_transfer(candidate, best, stats)
```
---
## 8. 验收标准Acceptance Criteria
| # | 验收项 | 判定方式 |
|---|--------|----------|
| AC1 | 召回任务只分配给 os_label ∈ {MAIN, COMANAGE} 的助教 | 数据库核查,无 UNASSIGNED/POOL 助教的召回任务 |
| AC2 | 关系构建任务 RS 门槛正确1 < RS < 6 | 检查 relationship_building 任务对应的 rs_display |
| AC3 | 客户转移通过三重保护 | transfer_log.guard_checks 全部 pass |
| AC4 | 新助教10天内不接收转移 | transfer_log 中 to_assistant binding 距转移时间 ≥ 10 天 |
| AC5 | 绑定率 < 50% 全局禁用转移 | 低覆盖率场景下 transfer_log 无新记录 |
| AC6 | 相同类型任务不重复生成 | 重复运行两次,第二次 skipped = 第一次 created |
| AC7 | 回访任务最低保留48小时 | 将 created_at 回拨49小时验证 expiry check |
| AC8 | 转移累计上限生效 | 第3次转移触发 pending_review 状态 |
---
## 9. 开放问题与后续讨论
| # | 问题 | 优先级 |
|---|------|--------|
| O1 | OS 标签每4小时更新是否足以支撑7:00任务生成需确认 ETL 完成时间窗口 | 高 |
| O2 | follow_up_visit 由召回完成检测器触发 vs 本 PRD Step 3 漏斗判定,两者需对齐触发逻辑 | 高 |
| O3 | POOL 助教何时晋升为 COMANAGE/MAINOS 算法是否有晋升路径,还是纯由 RS 数据自然演进 | 中 |
| O4 | pending_review 状态的任务如何人工干预需要管理后台支持P10 租户管理后台范畴) | 中 |
| O5 | 多门店场景下 cfg_task_generator_params 的继承逻辑(全局默认 → 门店覆盖) | 低 |
---
## 10. 与现有 PRD/代码的关系
| 文档/模块 | 关系说明 |
|-----------|----------|
| `docs/prd/specs/P4-miniapp-core-business.md` | 本 PRD 是 P4 中任务生成章节的细化和纠错版,以本文档为准 |
| `apps/backend/app/services/task_generator.py` | 需按本 PRD 重写 `run()``_process_assistant()`,主要改动是入口改为 OS 归属 |
| `apps/backend/app/services/fdw_queries.py` | 需新增 `get_ownership_pairs()` 查询方法 |
| `docs/prd/PRD审阅-Q&A.md` Q3.2 | RS 50% 相对差公式来源,本 PRD 已完整引用 |
| ETL `RelationIndexTask` | OS/RS/MS/ML 的计算源头,本 PRD 只消费其结果,不修改 ETL 层 |