40 KiB
线索业务 PRD
版本:v1.1(chart 阶段;2026-08-13 续 grill:定时任务顺序、部门集合领取、分配上限、状态CAS、history展示、公海混合展示、转商机事务前提、B/D定稿) 范围:crm-lead(线索业务)· crm-rule/线索规则(公海池配置)· crm-preference(列偏好平台能力) 目的:把线索域端到端业务流程写成产品可验证的规格;不确定点用 ⚠ 待确认 标出。
0. 阅读约定
- ⚠ 待确认(编号):产品/UX 尚未拍板的点,见文末《待确认事项索引》。
- 术语沿用 crm-auth 既有:主部门、兼职部门、部门集合、数据范围档位。
- 实体名用于对齐:线索(Lead)、公海池(LeadPool)、反馈(Feedback)、关注(Follow)、附件(Attachment)、行历史(History)、区划(Region)。
1. 全景:线索一生
一条线索从进入系统到**转化为商机(或作废/失效)**的完整生命周期:
┌──────────── 管理员导入/新建 ────────────┐
▼ ▼
[未分发]──分配到池──▶[待领取] [销售新增]
│ │
▼ 领取/分配 ▼
[已领取]────首次反馈────▶[跟进中]
│ │ ▲
│ 释放 / 超时回收 │ │ 再次反馈(重置N)
▼ │ │
[待领取]◀────释放/回收 N ────┘ │
│
转商机(已领取/跟进中) ──────┘
│
▼
[已转商机] ← 终态
失效M计时(创建时起持续跑) ──到期──▶[过期失效]──管理员激活──▶恢复原态
四把标尺贯穿始终:
- 状态(7 态单字段,见 §3)
- 归属(哪个池 / 哪个部门 / 哪个销售,见 §4)
- 两把计时器(回收 N 天 / 失效 M 天,见 §5)
- 可见性(谁能看到、谁能操作,见 §9 三机制权限模型)
2. 模块架构(后端)
| 模块 | 职责 | 关键实体 |
|---|---|---|
| crm-lead | 线索业务:CRUD、领取/反馈/释放/转商机、四视图列表、统计、附件 | lead, lead_history, lead_follow, lead_attachment |
| crm-rule(线索规则子域) | 公海池配置与规则参数 | lead_pool, lead_pool_province, lead_pool_region, lead_pool_member |
| crm-preference(新平台模块) | 用户级 UI 列偏好(显隐 + 顺序) | user_column_preference |
| crm-auth(既有,消费) | 用户/部门/角色/数据范围/行级授权(新增,ticket 09) | sys_user, sys_dept, sys_role_data_scope, sys_row_grant(设计定稿) |
| crm-dict(既有,消费) | 字典:渠道/品牌/需求产品/需求场景 | dict_group, dict_item |
| crm-region(新建,02 定稿) | 行政区划:省/市/区,国标 code | sys_region |
| crm-file(既有,消费) | 附件文件 | file |
| 商机模块 | 不存在,crm-lead 通过 outbound port 契约声明依赖 | — |
依赖关系:crm-lead → crm-rule / crm-preference / crm-auth / crm-dict / crm-region / crm-file;单向,无回边。
3. 线索状态机(7 态单字段 leadStatus)
3.1 状态定义
| # | 状态 | 含义 | 有领取人 | 回收计时 N | 失效计时 M |
|---|---|---|---|---|---|
| 1 | 未分发 | 管理员新建/导入且未指定归属池;悬于池外 | 无 | 停 | 跑 |
| 2 | 待领取 | 已归池;池内成员可自领 | 无 | 停 | 跑 |
| 3 | 已领取 | 某销售持有,尚未反馈 | 有 | 跑 | 跑 |
| 4 | 跟进中 | 持有销售已产生至少一次反馈 | 有 | 跑(每次反馈重置) | 跑 |
| 5 | 已转商机 | 已通过转商机操作创建对应商机;终态 | 保留(历史) | 冻结 | 冻结 |
| 6 | 过期失效 | 失效计时 M 到期;可发生在任何非终态 | 保留(若失效前被持有) | 冻结 | 冻结(除激活重置) |
| 7 | 线索作废 | 反馈=无效自动进入;可恢复 | 进入时清空、分配后重写 | 停 | 跑(继续) |
反馈情况(有效 / 无效 / 未反馈)是独立字段 feedbackStatus,不进状态机;仅供筛选/展示。
3.2 合法迁移边
| # | from | 触发 | to | 副作用 |
|---|---|---|---|---|
| 1 | (无) | 管理员新建/导入且未指定池 | 未分发 | 启动 M(起点=创建时刻) |
| 2 | (无) | 销售在"我的线索"新增(自动带出本人池)或管理员指定池 | 待领取 | 启动 M |
| 3 | 待领取 | 销售新增时勾"是否领取" | 已领取 | 领取人=本人;启动 N |
| 4 | 未分发 | 管理员分配到池 | 待领取 | 归池,M 继续 |
| 5 | 未分发 / 待领取 | 管理员分配并指定销售 | 已领取 | 领取人=被指派销售;启动 N |
| 6 | 待领取 | 池内成员点【领取】 | 已领取 | 领取人=本人;启动 N |
| 7 | 已领取 | 首次反馈 | 跟进中 | 写反馈;N 重置(起点=反馈时刻) |
| 8 | 跟进中 | 再次反馈 | 跟进中(自环) | 写反馈;N 重置 |
| 9 | 已领取 / 跟进中 | 转商机 | 已转商机 | 调 OpportunityCreationPort;写 CONVERT 历史;冻结 N、M |
| 10 | 已领取 / 跟进中 | 手动【释放】 | 待领取 | 清领取人;N 停 |
| 11 | 已领取 / 跟进中 | 超时回收(N 到期无跟进) | 待领取 | 清领取人;N 停 |
| 12 | 未分发 / 待领取 / 已领取 / 跟进中 / 线索作废 | 失效计时 M 到期 | 过期失效 | 若被持有则保留领取人;只读(作废态失效后 status_before_expire=线索作废,激活恢复作废) |
| 13 | 过期失效 | 管理员【激活】(单条/批量) | 恢复失效前状态与原领取人 | N 和 M 均重置(起点=激活时刻,recycle_deadline = now + pool.N,expire_deadline = now + pool.M)。若失效前态=线索作废则恢复作废(等同无操作,见 §3.4) |
| 14 | 已领取 / 跟进中 | 提交反馈且反馈情况=无效 | 线索作废 | 清空 owner_user_id;M 继续跑;写 VOID 历史 |
| 15 | 线索作废 | 管理员分配给销售 | 线索作废(自环) | 写入新 owner_user_id;状态不变;写 ASSIGN 历史 |
| 16 | 线索作废 | 被分配销售提交反馈且反馈情况=有效 | 已领取 | owner=当前销售;启动 N;写 FEEDBACK 历史 |
3.3 状态 × 操作 权威矩阵
| 操作 | 未分发 | 待领取 | 已领取 | 跟进中 | 已转商机 | 过期失效 | 线索作废 |
|---|---|---|---|---|---|---|---|
| 详情 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 编辑 | ✓(管理) | ✓(管理) | ✓(持有人) | ✓(持有人) | — | — | ✓ |
| 领取 | — | ✓(池成员) | — | — | — | — | — |
| 分配 | ✓(管理) | ✓(管理) | — | — | — | — | ✓(管理) |
| 反馈 | — | — | ✓(持有人) | ✓(持有人) | — | — | ✓(持有人) |
| 转商机 | — | — | ✓(持有人) | ✓(持有人) | — | — | — |
| 释放 | — | — | ✓(持有人) | ✓(持有人) | — | — | — |
| 关注/取关 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 删除 | ✓(管理/创建人) | ✓(管理/创建人) | ✓(管理/创建人·强确认) | ✓(管理/创建人·强确认) | ✕ 禁止(终态) | ✓(管理/创建人·强确认) | ✓(管理/创建人·强确认) |
| 激活 | — | — | — | — | — | ✓(管理) | — |
「管理」= 具有该线索所属数据范围的管理权限;具体谁是"管理"随 §9 权限 PRD 定。
删除权限全局规则(2026-08-13 定稿):
删除 = (状态 ≠ 已转商机) AND (管理员 OR 创建人·强确认)。已转商机是唯一删除禁区(终态,下游商机source_lead_id依赖,删会悬空);过期失效/线索作废无下游依赖,仍可删。推翻了早期“持有人可删过期失效”的写法,改为“创建人可删”。
3.4 线索作废生命周期(2026-08-13 产品定稿,原 ⚠(A))
已领取/跟进中 ─[提交反馈,反馈情况=无效]→ 线索作废(清空 owner)
│ ▲
管理员分配给新销售(仍保持作废)
新销售提交反馈,反馈情况=有效 → 已领取
└──┘
- 触发:销售提交反馈且反馈情况=无效时自动进入,无需手动点按钮。
- 入口态:已领取 / 跟进中(能提交反馈的状态)。
- 可恢复:管理员可把作废线索分配给其他销售(分配后仍保持作废态);被分配销售提交反馈=有效时自动恢复到“已领取”。
- 归属:进入作废时清空
owner_user_id;分配时写入新 owner。 - 计时器:失效计时 M 在作废态下继续跑(不冻结)。
- 作废态可操作:详情 / 编辑 / 反馈(持有人)/ 分配(管理)/ 关注 / 删除(管理员 OR 创建人·强确认)。
- 激活与作废的关系(2026-08-13 拍板):【激活】是过期失效专属操作;作废线索不提供激活,复活只有“分配 → 新销售反馈=有效”一条路。
- 由于 M 在作废态继续跑,作废线索可能 M 到期进入【过期失效】,此时“失效前态=线索作废”。
- 对这种线索执行【激活】会恢复到“线索作废”(owner 仍为空)——等于无操作,实际不会/不应对作废线索点激活。复活仍走分配。
- 工程含义:状态机里“过期失效 → 激活”只恢复失效前态;不需为作废特制新边。
4. 归属模型
4.1 三层归属
一条线索的归属由三层表达,各有用途:
| 层 | 字段 | 由什么决定 | 变化时机 |
|---|---|---|---|
| 池归属 | pool_id |
建线索时指定(或后续分配) | 分配变更 |
| 部门归属 | dept_id(冗余 = 池的 dept_id) |
池所属部门;一部门一池 → 线索的 dept_id 恒等于池的 dept_id | 池不变则永不变 |
| 销售归属 | owner_user_id |
领取/分配到销售 | 领取/释放/回收/分配 |
4.2 快照策略
以下字段在 lead 主表以快照形式冗余,避免关联查询:
| 字段 | 快照源 | 刷新时机 |
|---|---|---|
pool_name_snapshot |
lead_pool.pool_name |
池改名时批量刷 |
dept_id |
池的 dept_id | 一部门一池 → 永不变 |
team_dept_id |
池的 team_dept_id | 同上 |
owner_name_snapshot |
领取人姓名 | 领取/分配时快照,释放时清空 |
owner_dept_id_snapshot |
领取时销售所属部门 | 领取/分配时快照(用于审计"这条线索历史属于哪个部门") |
feedback_*(5 列) |
最新一条反馈 | 每次反馈时刷 |
⚠ 待确认(B):销售换部门后,owner_dept_id_snapshot 是否需要刷新?当前定为不刷(快照即历史)。
4.1.1 公海池换部门时的 dept_id 联动
管理员可修改公海池的归属部门(如从 A 部门改挂到 B 部门)。此时线索 dept_id 的联动规则:
| 线索状态 | dept_id 是否跟池走 | 说明 |
|---|---|---|
| 未分发 / 待领取 | ✓ 跟池走 | 批量 UPDATE lead SET dept_id = new_dept_id WHERE pool_id=? AND status IN ('未分发','待领取'),新部门成员可查到、可领取 |
| 已领取 / 跟进中 | ✗ 不动 | dept_id 保持原值(=旧部门),已归属销售的私海线索不受池换部门影响 |
| 已转商机 / 过期失效 / 线索作废 | ✗ 不动 | 终态/失效线索不参与 |
前置校验:目标部门必须当前无池(UNIQUE(dept_id) 约束);源部门换出后无池不拦截。
改写 [I]:dept_id 不再是"恒定永不变动",而是"池归属部门变更时,待领取/未分发线索 dept_id 跟池走;已领取/跟进中线索 dept_id 不动"。
4.3 公海池(lead_pool,crm-rule 定义)
一个公海池 = 一个部门的线索容器 + 一组规则参数:
| 字段组 | 内容 |
|---|---|
| 归属 | dept_id(UNIQUE,一部门一池)、team_dept_id(冗余,团队 = 部门上溯节点) |
| 负责市场 | 多省(lead_pool_province)+ 多市/区(lead_pool_region);用国标 region_code 前缀 LIKE 支持祖先查询 |
| 人员 | lead_pool_member(pool_id, user_id, role_type) role=OWNER(1)/COLLABORATOR(多);部门成员 = 部门全体动态推导,不落库 |
| 规则参数 | recycle_days(N,默认 7)、expire_days(M,默认 180)、hold_limit(持有上限,默认 200)、daily_claim_limit(每日领取上限,默认 10,0=无限)、claim_rule(1=成员可领+管理员分配 / 2=仅管理员分配) |
池名全局 UNIQUE;删除软删且必须"池下无非终态线索"。
换部门:管理员可修改 dept_id,前置校验目标部门当前无池(UNIQUE(dept_id))。换部门后,待领取/未分发线索 dept_id 批量跟池走,已领取/跟进中线索 dept_id 不动(见 §4.1.1)。
5. 双计时器
两把独立计时器,各管各的(对齐产品最新口径):
5.1 回收计时 N(recycle_deadline)
- 计什么:销售领取到线索后,多久没跟进就退回公海。
- 起点:领取时刻。
- 重置:每次新增反馈时重置为
now + N。 - 停止:释放 / 回收 / 转商机 / 失效。
- 到期动作:定时任务扫
WHERE recycle_deadline < now AND status IN ('已领取','跟进中')→ 状态回到"待领取"、清领取人、写 RECYCLE 历史。 - 存储:
lead.recycle_deadline datetime,加索引。
5.2 失效计时 M(expire_deadline)
- 计什么:一条线索从入库起多久后自动失效。
- 起点:创建时刻,不随领取/释放变化。
- 重置:仅管理员激活时(
now + M)。 - 停止:转商机 / 已失效。
- 到期动作:定时任务扫
WHERE expire_deadline < now AND status IN ('未分发','待领取','已领取','跟进中','线索作废')→ 状态置"过期失效"(保留领取人若有)、写 EXPIRE 历史。含线索作废:作废态 M 继续跑(§3.4),到期同样置失效(status_before_expire=线索作废)。 - 存储:
lead.expire_deadline datetime,加索引。
5.3 池规则参数变更时的批量刷新
管理员改了池的 N 或 M:
- N 改动 → 分状态批量刷新:
- 已领取:
UPDATE lead SET recycle_deadline = claim_time + new_N WHERE pool_id=? AND status='已领取' - 跟进中:
UPDATE lead SET recycle_deadline = last_feedback_time + new_N WHERE pool_id=? AND status='跟进中'
- 已领取:
- M 改动 → 批量
UPDATE lead SET expire_deadline = create_time + new_M WHERE pool_id=? AND status != '已转商机'
当日不批量清算已超时的——只改截止时刻,实际状态转移交给次日定时任务自然执行(对齐原型 A7-3-1 口径)。
6. 关键业务流程
6.0 全局并发原则:状态 CAS 乐观锁(2026-08-13 grill #8 定稿,见 ADR-0021)
所有状态迁移的 UPDATE 一律带 AND status IN (合法起始态) 作 CAS;行数=0 即判定「已被他人操作」失败。
- 适用:领取(#6)/ 分配(#4/#5/#15)/ 激活(#13)/ 释放(#10)/ 反馈(#7/#8/#16)/ 转商机(#9)全部迁移。
- “部分成功”的“失败”有确切定义 = CAS 行数 0(目标已被并发改态,或被定时任务回收/失效)。批量领取/分配/激活均非原子,逐条 CAS,成功 N 条、失败 M 条弹提示。
- 例:两销售同时领取同一条待领取线索 →
UPDATE lead SET owner_user_id=? WHERE id=? AND status='待领取',后者行数=0,不会静默抢走。 - 定时任务(回收/失效)的
WHERE ... AND status IN (...)同属此原则,与人工操作天然互斥。
6.1 新增线索(销售侧 · a2-1-2-2)
入口:【我的线索】列表 → 【新增线索】按钮。
表单字段(必填标 *):
| 分组 | 字段 | 类型 | 校验 |
|---|---|---|---|
| 基础 | 线索名称 | 文本 | 无 |
| 联系电话 | 文本 | 手机/座机通用正则(单字段) | |
| 微信 | 文本 | 微信号正则;无效提示"无效微信号" | |
| 电子邮箱 | 文本 | 通用邮箱正则;无效提示"无效邮箱号" | |
| 省份 * | 下拉 | 联动城市 | |
| 城市 * | 下拉 | 依赖省份;未选省份不可选城市 | |
| 详细地址 | 文本 | 无 | |
| 业务 | 渠道 * | 字典 | dict group=lead.channel |
| 品牌 * | 字典 | dict group=lead.brand |
|
| 需求产品 * | 字典 | dict group=lead.product |
|
| 需求场景 | 字典 | dict group=lead.scene |
|
| 核心 | 咨询内容 * | 多行文本 | 无 |
| 只读回填 | 线索状态 | 展示"待领取" | 不可编辑 |
| 只读回填 | 线索公海池 | 自动带出本人所属池 | 不可编辑 |
| 销售团队 / 销售部门 / 销售人员 / 新增时间 | 自动回填 | 不可编辑 | |
| 标记 | 是否加急 | 单选是/否,默认否 | — |
| 是否领取 | 复选,勾则新增即领 | — | |
| 是否关注 | 复选,勾则加我的关注 | — |
底部:连续新增(复选)· 新增线索(主按钮)· 关闭(二次确认放弃)。
前置校验(2026-08-13 grill #6):
- 自动带池 = 本人主部门的池(不给选、不可编辑);即使兼职部门也有池,新增仍只落主部门池。跨部门归属走管理员【分配】。
- 主部门无池 → 报错拦截(“本部门尚未配置公海池,请联系管理员”),不允许销售建挂空池线索;“未分发/空池”是管理员导入专属中间态。
副作用:
- 落
lead行 → 状态 = 待领取(勾"是否领取"则一步转已领取,走边 #3) - 写
lead_historyCREATE 记录 - 勾"是否关注" → 落
lead_follow - 触发失效计时 M(起点=
create_time);若同步领取,触发回收计时 N
6.2 领取线索(池成员 · a2-1-1 系列)
入口:【线索公海】列表 → 单条【领取】 / 批量勾选后【批量领取】。 前置(应用层校验):
- 当前用户在池的可领取成员集合内(
claim_rule=1时=部门全员;claim_rule=2时销售不可自领,按钮隐藏)。“部门全员”按部门集合算(2026-08-13 grill #5):销售可领取其所属**任一部门(主+兼职)**公海池的线索,即pool.dept_id IN 用户部门集合。与 crm-auth 既有 ABACDEPT档口径一致(ADR-0005,部门集合=主+兼职并集),“能领的=能看的”自洽。 - 未超"每日领取上限"
daily_claim_limit(0=无限);"每日" = 自然日(00:00-23:59,DATE(op_time) = CURRENT_DATE) - 未超"个人持有上限"
hold_limit;计数口径 = 仅已领取 + 跟进中(已转商机、过期失效、线索作废不占名额) - 目标线索状态 = 待领取
副作用:设
owner_user_id;快照 owner 姓名/部门;状态转 已领取(边 #6);启动 N;写 CLAIM 历史。
6.3 分配线索(管理侧 · A7 相关,部分在 A2 管理视图)
入口:管理员在【线索管理】/【线索公海】的管理视图选中线索 → 【分配】。 两种分配深度:
- 仅分配到池(未分发 → 待领取,边 #4)
- 分配到销售(未分发/待领取 → 已领取,边 #5) 权限:仅管理员 + 池 OWNER + 池 COLLABORATOR 可执行(对齐 04-3 语义)。
上限校验(2026-08-13 grill #7):分配到销售时,hold_limit(个人持有上限)与 daily_claim_limit(每日领取上限)都校验,与自领同一套约束(分配也受限)。
- 额度归属:消耗的是被指派销售的持有数/当日领取计数(不是管理员),按目标线索所属池的上限值算。
- 批量分配部分成功:超限不拖垮整批——成功 N 条、失败 M 条(超限/已被他人操作)弹提示,非原子,与批量激活/批量领取口径统一。
- 仅分配到池(未分发→待领取,无 owner)不占任何上限。
6.4 反馈(持有人 · a2-1-2 系列)
入口:详情页 / 列表操作【反馈】按钮。
表单:反馈情况(有效/无效/未反馈)· 反馈内容 · 需求产品(可切换)· 附件(走 crm-file,存 lead_feedback.attachment_ids)。
草稿/提交双态(改写 [Q2=c],拆出独立 lead_feedback 表):
- 【保存草稿】:写/更新
lead_feedback行record_status='DRAFT',不校验必填,不改状态机,不写lead_history,不刷主表快照。草稿按(user_id, lead_id)唯一,重复保存为覆盖写。 - 【保存】(正式提交):
lead_feedback行record_status='SUBMITTED',同时:- 主表更新 5 列反馈快照(
feedback_status/content/time/by/product_code) - 状态:已领取 → 跟进中(边 #7);跟进中 → 跟进中(边 #8)
- 回收计时 N 重置(起点=
now) - 写
lead_historyFEEDBACK 记录(detail 引用lead_feedback.id,不冗余内容) - 清除同
(user_id, lead_id)的草稿行(转为 SUBMITTED)
- 主表更新 5 列反馈快照(
lead_feedback 表结构:
lead_feedback
├─ id, lead_id, user_id
├─ feedback_status, content, product_code
├─ attachment_ids json -- crm-file 文件 id 列表
├─ record_status tinyint -- 1=DRAFT, 2=SUBMITTED
├─ created_at, submitted_at
└─ INDEX(lead_id, user_id, record_status)
6.5 转商机(持有人 · a2-1-2 转商机弹窗)
入口:详情页 / 列表【转商机】按钮。
前置:status IN (已领取, 跟进中);当前用户=持有人;反馈情况不参与判定(对齐产品最新口径,⚠ 已推翻原型"仅有效/跟进中/已联系"文案,UX 修订清单见 §11)。
表单:商机名称* · 行业*(字典 opportunity.industry,⚠ 待商机模块创建)· 甲方 · 意向客户* · 地区*(region_code)· 备注。
**副作用(同一本地事务)**:
- 调
OpportunityCreationPort#createOpportunity(cmd)→ 拿商机 id UPDATE lead SET status='已转商机' WHERE id=? AND status IN ('已领取','跟进中');行数=0 抛并发异常- 写
lead_historyCONVERT 记录,detail 含opportunityId + opportunityName - 冻结 N/M(靠定时任务
WHERE status != '已转商机'天然实现,无需 UPDATE 截止字段) 幂等:状态终态保护(crm-lead 侧) +opportunity表UNIQUE(source_lead_id)(商机侧契约要求)。 失败:本地事务性回滚,线索状态不变。
⚠ 事务语义前提约束(2026-08-13 grill #10,见 ADR-0020):上述“同一本地事务”成立的前提是 OpportunityCreationPort 的首个实现与 crm-lead 同库、共享事务(商机模块尚未建,近期大概率同库)。此前提写入端口契约(ticket 08)。若未来商机模块独立库/独立部署,本节事务语义必须重新设计:改为“先本地 CAS 改态 → 再调端口建商机”的最终一致 + 补偿(靠 UNIQUE(source_lead_id) 幂等 + 对账收敛)。现阶段不过度设计。
6.6 释放(持有人 · a2-1-2)
前置:status IN (已领取, 跟进中);当前用户=持有人。
副作用:清 owner_user_id、owner_name_snapshot、owner_dept_id_snapshot、claim_time、recycle_deadline;状态转"待领取"(边 #10);N 停;写 RELEASE 历史。反馈快照 5 列不清空(保留上一位持有人的反馈,供下一位领取人参考)。线索回到池内待领取,原持有人失去所有非查看权限。
6.7 定时任务:超时回收 & 失效
回收 Job(每日凌晨 / 或按配置节奏):
SELECT id, pool_id, owner_user_id FROM lead
WHERE recycle_deadline < now() AND status IN ('已领取','跟进中');
逐行:清领取人,状态→待领取,写 RECYCLE 历史。
失效 Job(同上):
SELECT id, status FROM lead
WHERE expire_deadline < now() AND status IN ('未分发','待领取','已领取','跟进中','线索作废');
逐行:状态→过期失效(若在私海则保留 owner_user_id 作历史记录),写 EXPIRE 历史。
执行顺序(关键,2026-08-13 grill #4 定稿,见 ADR-0019):失效 Job 先跑,回收 Job 后跑。 当一条线索两个 deadline 同时过期时,失效优先:
- 失效把状态改为「过期失效」(保留 owner)后,回收 Job 的
status IN ('已领取','跟进中')天然扫不到它,两者不再竞争。 - 语义理由:失效是更强的终止语义,“谁持有”这个历史应被冻结保留;且失效可被管理员【激活】连 owner 一起恢复,而回收退公海会不可逆地丢 owner。先失效给管理员留一条“激活找回原持有人”的后路。
⚠ 待确认(D):两个 Job 的执行节奏(每日凌晨?每小时?按公海池独立?)。当前定为统一每日 02:00 全库扫(同一次扫描内先失效后回收)。
6.8 激活失效线索(管理员)
入口:过期失效线索详情 → 【激活】(单条);线索管理列表 → 勾选多条 → 【批量激活】。
副作用:状态恢复为失效前状态、owner 保留;expire_deadline = now + pool.expire_days(M 重置);recycle_deadline = now + pool.recycle_days(N 同步重置);写 ACTIVATE 历史。
批量激活:逐条执行单条激活逻辑,部分成功——成功 N 条、失败 M 条(已被他人操作),弹结果提示。非原子。
6.9 编辑线索(管理/持有人)
入口:列表操作【编辑】按钮,打开线索详情编辑页(含【线索详情】【历史记录】双 Tab)。
可编辑字段(= 新增线索表单的业务字段):
| 分组 | 可编辑字段 |
|---|---|
| 基础 | 线索名称、联系电话、微信、电子邮箱、省份、城市、详细地址 |
| 业务来源 | 渠道、品牌、需求产品、需求场景 |
| 核心 | 咨询内容、是否加急 |
不可编辑字段:状态、所属公海池、领取人、领取时刻、所有时效字段(recycle_deadline/expire_deadline)、所有快照字段(pool_name_snapshot/owner_*/feedback_*)——由状态机和业务流程管,不走编辑入口。
权限:
- 未分发 / 待领取 → 管理可编辑
- 已领取 / 跟进中 → 持有人可编辑
- 已转商机 / 过期失效 → 不可编辑(终态锁 / 只读)
- 线索作废 → 可编辑(业务字段,与已领取/跟进中同)
副作用:
- 写
lead_historyEDIT 记录,detail 含变更字段清单(旧值→新值) - 不改状态,不影响 N/M 计时器
6.10 操作日志 lead_history(2026-08-13 grill #3 定稿)
写入口径 = 全量记录(11 种类型),展示口径 = 仅 5 种(selected (a))。
lead_history.type 枚举(全量落库):
| type | 触发 | detail 关键内容 | 页面展示 |
|---|---|---|---|
| CREATE | 新建/导入(§6.1) | — | ✕ |
| CLAIM | 领取(边 #6) | — | ✅ 领取了该线索 |
| ASSIGN | 分配(边 #4/#5/#15) | 新 owner | ✕ |
| FEEDBACK | 反馈(边 #7/#8/#16) | 引用 lead_feedback.id |
✅ 添加了反馈,状态修改为【状态】,添加内容【反馈原文内容】 |
| CONVERT | 转商机(边 #9) | opportunityId + opportunityName | ✅ 将线索转化为项目【项目名称】,生成对应商机 |
| RELEASE | 释放(边 #10) | — | ✅ 将该线索释放回线索公海池 |
| RECYCLE | 超时回收(边 #11) | 原持有人 | ✕ |
| EXPIRE | 失效(边 #12) | — | ✕ |
| ACTIVATE | 激活(边 #13) | — | ✕ |
| VOID | 作废(边 #14) | 原持有人 owner_user_id(作废清 owner 前留痕,供审计) |
✕ |
| EDIT | 编辑(§6.9) | 变更字段清单(旧值→新值) | ✕ |
- 全量写:11 种动作全部落
lead_history,作为管理员审计与纠纷排查的唯一来源。 - 页面展示:详情页「历史记录」Tab 只
WHERE type IN (CLAIM, RELEASE, FEEDBACK, CONVERT, EDIT)过滤展示 5 种。 - 作废审计:VOID 记录 detail 保留原持有人;
lead_feedback.user_id(谁反馈无效)与lead_history均不受清 owner 影响,无主只是当前归属,历史全程可追溯。
7. 四视图(前端列表页)
| 视图 | 路径 | 数据集 | 关键操作 |
|---|---|---|---|
| 线索公海(a2-1-1) | /lead/pool | 池内非终态、非失效线索(待领取 + 已领取/跟进中 的只读观察态,防撞单),部门集合天花板 | 领取、关注 |
| 我的线索(a2-1-2) | /lead/mine | owner_user_id = 当前用户(含已转商机、过期失效) |
反馈、转商机、释放、编辑、删除 |
| 我的关注(a2-1-3) | /lead/follow | 我在 lead_follow 里关注的线索 |
取消关注、跳转详情 |
| 线索管理(a2-1-4) | /lead/manage | 数据可见范围内全部线索 | 分配、导入/导出、批量释放、删除、激活 |
列偏好:每视图独立一套字段显隐 + 顺序(scope_key 各不同:lead.public_pool / lead.my_lead / lead.my_follow / lead.manage),互不同步(对齐 07 定稿,⚠ 已推翻原型"共用一套"文案,UX 修订清单见 §11)。
✅ 已定稿(E)(2026-08-13 grill #9):线索公海视图混合展示——含「待领取」+「已被别人领取」(已领取/跟进中)的只读观察态,用于防撞单(领前确认同事是否已在跟)。展示范围作为可配置项,不硬编码(后续可能收窄为仅待领取,或按池/角色细分):后端以一个“公海可见状态集”参数(默认 {待领取, 已领取, 跟进中})驱动过滤,避免改规则时改代码。领取按钮仅对 status='待领取' 行可点,已被领走的行只读。
8. crm-preference 契约(列偏好,07 定稿)
表:user_column_preference(id, user_id, scope_key varchar(64), visible_keys json, column_order json, updated_at),UNIQUE(user_id, scope_key)。
接口(仅 2 方法):
Optional<ColumnPreference> get(Long userId, String scopeKey);
void save(Long userId, String scopeKey, ColumnPreference preference);
record ColumnPreference(List<String> visibleKeys, List<String> columnOrder) {}
scope_key:由业务方(crm-lead)约定的稳定字符串(不是路由、不是主键)。线索域 4 个:
lead.public_pool·lead.my_lead·lead.my_follow·lead.manage
字段池归属:业务方(crm-lead)拥有字段元数据、校验、未知 key 兜底;preference 只存"勾了哪些 key + 什么顺序",无感知语义。
保存时机:仅点【保存】按钮才落库;拖拽临时顺序仅前端本地保留,关页面/刷新即丢。
明确不做:跨用户默认模板、管理员强制列、多租户 scope 隔离、偏好版本历史、字段池注册表、scopeKey 注册表校验。
9. 数据可见性与权限
§9 已定稿(2026-08-13,详见 权限与数据可见性-PRD.md + issues/05 + issues/09):
三机制模型 RBAC + ABAC + ReBAC(升级动因:团队成员/线索池协作人这类"行级授权"必须能越过部门天花板,纯 AND 不够表达):
- RBAC 功能权限(既有):跟角色走;权限点控制按钮/API 开关。
- ABAC 数据可见范围(既有,四档天花板):页面业务语义(owner=我/dept=本部门)+ SELF / DEPT / DEPT_AND_CHILDREN / ALL,多角色取最宽。
- ReBAC 行级授权(通行证)(新增,见 ticket 09):
sys_row_grant(user_id, resource_type, resource_id, created_by, created_at)通用表;拦截器OR EXISTS(...)注入;跟用户不跟角色;只覆盖部门天花板,不覆盖页面业务语义。
组合公式:
能否看到某数据 = 页面业务条件(ABAC语义) AND (落在数据可见范围(ABAC天花板) OR 已被行级授权(ReBAC))
5 项产品拍板(PRD-1/2/3/D/E)已全部定稿,结论如下:
- PRD-1 触发动作清单:团队成员(商机/客户/项目,per-row)+ 公海池负责人/协作人(线索,池维度
resource_type=pool)。 - PRD-2 权限边界:被授权者只读;写权由权限点+service(owner/状态机)守,不分档。
- PRD-3 撤销立即失效:硬删(
DELETE FROM sys_row_grant),不做软删。 - PRD-D 线索不开按单条直接授权([D]):线索可见=owner=我 OR 池级授权;无
resource_type=lead;分配=改 owner_id。 - PRD-E 池配置不发通行证([E]):池配置可见/可改=功能权限×数据范围;负责人/协作人身份不授予池配置写权。
- [F] 关注不走行级授权:“我的关注”=
id IN 关注表AND 部门天花板,每请求实时算。
10. 依赖与集成
| 目标 | 依赖模块 | 依赖形态 |
|---|---|---|
| 用户/部门/角色 | crm-auth | 消费既有 API |
| 数据范围拦截 | crm-auth | @DataScope 注解 + 拦截器 |
| 行级授权 | crm-auth(已立项 ticket 09) | sys_row_grant 通用表 + 拦截器 OR EXISTS 注入(设计定稿,待实现) |
| 字典 | crm-dict | 4 个 group:lead.channel/brand/product/scene |
| 行政区划 | crm-region(新建,02 定稿) | sys_region.code 国标前缀 LIKE 查询 |
| 附件 | crm-file | 关联表 lead_attachment(lead_id, file_id) |
| 列偏好 | crm-preference(新建,07 定稿) | 2-方法接口 |
| 商机模块 | 不存在 | crm-lead 定 outbound port OpportunityCreationPort,实现待商机模块落地 |
11. 待确认事项索引
11.1 产品拍板类(阻塞 spec 收尾)
| 编号 | 事项 | 阻塞对象 | 优先级 |
|---|---|---|---|
| — | — | ||
owner_dept_id_snapshot 是否刷新? |
— | — | |
lead_feedback.attachment_ids(本轮 grill #3) |
— | — | |
| — | — | ||
| — | — | ||
| ⚠ PRD-1 | 行级授权触发动作清单 | ✅ 定稿:团队成员+池负责人/协作人 | |
| ⚠ PRD-2 | 被授权者权限边界 | ✅ 定稿:只读,写权归 service | |
| ⚠ PRD-3 | 移除授权后即时失效 | ✅ 定稿:硬删,立即失效 | |
| ⚠ PRD-D | 线索是否跨部门逐条指派 | ✅ 定稿:不开 lead 直接通道,分配=改 owner | |
| ⚠ PRD-E | 池负责人/协作人的授权范围 | ✅ 定稿:仅池内线索可见,不兼池配置写权 |
11.2 UX 修订类(不阻塞后端,但上线前必改,详见 待确认事项清单.md)
| 编号 | 事项 | 处数 |
|---|---|---|
| ⚠ UX-1 | 列表字段"共用一套/同步生效" → "每菜单独立" | 7 处 |
| ⚠ UX-2 | "拖拽实时保存" → "点保存才落库" | 2 处 |
| ⚠ UX-3 | 公海池"江苏 1/2/3" → 合并为 1 池 | 数据行 |
| ⚠ UX-4 | 转商机触发条件"跟进中/已联系/有效" → "已领取/跟进中" | 3 处 4 行 |
12. 附:字段清单速查
见各 ticket Answer:
lead主表 →issues/06-线索实体与字段模型.mdlead_feedback(独立反馈实体,DRAFT/SUBMITTED) → 本 PRD §6.4(本轮 grill #3 改写 [Q2=c])lead_pool+ 3 关联表 →issues/04-公海池实体与归属建模.mdsys_region→issues/02-行政区划数据源调研.mduser_column_preference→issues/07-preference列偏好scope模型与契约.mdOpportunityCreationPort→issues/08-转商机outbound-port契约.mdsys_row_grant(行级授权底座) →issues/09-行级授权sys_row_grant能力.md- 权限三机制完整推导 →
权限与数据可见性-PRD.md+issues/05-线索域数据隔离落地.md - 模块领域语言 →
crm-lead/CONTEXT.md、crm-rule/CONTEXT.md、crm-preference/CONTEXT.md
12.1 grill 决策清单(2026-08-12)
| # | 决策 | 影响章节 |
|---|---|---|
| 1 | 池换部门允许;待领取/未分发 dept_id 跟池走,已领取/跟进中不动;目标部门须无池 |
§4.1.1、§4.3,改写 [I] |
| 2 | 线索合并不做,Out of scope | map.md |
| 3 | 拆出 lead_feedback 独立表(DRAFT/SUBMITTED),改写 [Q2=c] |
§6.4 |
| 4 | 过期失效线索可删(管理/持有人·强确认) | §3.3 矩阵 |
| 5 | 释放/回收时反馈快照不清空 | §6.6 |
| 6 | 激活时 N 和 M 均重置 | §3.2 边 #13、§6.8 |
| 7 | hold_limit 只算已领取 + 跟进中 |
§6.2 |
| 8 | 批量激活,部分成功 | §6.8 |
| 9 | daily_claim_limit "每日" = 自然日 |
§6.2 |
| 10 | 导入线索不做,留 fog | map.md |
| 11 | 销售可删自己过期失效线索(持有人·强确认) | §3.3 矩阵 |
| 12 | 编辑范围 = 业务字段可改,状态/池/归属/时效/快照不可改 | §6.9 |
12.2 grill 决策清单(2026-08-13,本轮收口)
| # | 决策 | 影响章节 |
|---|---|---|
| 13 | 线索作废:反馈=无效自动触发,入口=已领取/跟进中,清空 owner,可恢复(分配+反馈=有效→已领取) | §3.1/3.2/3.3/3.4 |
| 14 | 删除权限全局修正 = (状态≠已转商机) AND (管理员 OR 创建人·强确认);已转商机为删除禁区 | §3.3 |
| 15 | 权限三机制 RBAC+ABAC+ReBAC 定稿;ReBAC 跟用户不跟角色、只覆盖天花板 | §9 |
| 16 | PRD-1~E 全部拍板;sys_row_grant 立项为 ticket 09 |
§9、§11.1 |
| 17 | 激活是过期失效专属;作废无激活(只有分配复活);作废→失效→激活=恢复作废(等同无操作)(prototype-permission / prototype-statemachine 验出) | §3.2#13、§3.4、§6.8 |
| 18 | 删除“创建人可删”对普通销售在“自建且自持有(owner=我)”场景由“我的线索”行使;流走后归属变,删除权由新持有方/管理员承接(自洽,无需改) | §3.3 |
12.3 grill 决策清单(2026-08-13 本轮续 grill)
| # | 决策 | 影响章节 |
|---|---|---|
| G1 | 作废态反馈/编辑的“持有人”不矛盾(视角问题):“我的线索”里的作废线索 owner 必=我;“线索管理”无反馈按钮。不拆子状态 | §3.3/3.4 |
| G2 | 草稿选“无效”不触发作废(仅正式提交触发);作废清 owner 不影响审计留痕 | §6.4/6.10 |
| G3 | lead_history 全量写 11 种,页面只展示 5 种(领取/释放/反馈/转商机/编辑);VOID 记原持有人 |
§6.10 |
| G4 | 定时任务失效先跑、回收后跑,两者不再竞争 | §6.7 |
| G5 | 领取/可见按部门集合(主+兼职);与 crm-auth ABAC DEPT 档既有实现对齐(ADR-0005) | §6.2、CONTEXT |
| G6 | 新增自动带主部门池不给选;主部门无池→报错拦截 | §6.1 |
| G7 | hold_limit+daily_claim_limit 分配时也校验;额度按被指派人+目标池算;批量部分成功 |
§6.3 |
| G8 | 新增§6.0 全局并发原则:状态 CAS 乐观锁,行数=0=失败 | §6.0 |
| G9 | ✅(E) 线索公海混合展示(防撞单),展示范围可配置不硬编码 | §7 |
| G10 | 转商机现按同库同事务定,显式写入前提约束;商机独立时重设计为最终一致+补偿 | §6.5、CONTEXT |
| G11 | ✅(B) 不刷 owner_dept_id_snapshot;✅(D) 每日 02:00 全库扫 | §11.1 |