You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

46 KiB

线索业务 PRD

版本:v1.2(2026 grill-with-docs:接口完整性复核,补 §13 批量操作与统计接口 G1–G5;G6/G7/G8 维持 Out of scope) 历史: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计时(创建时起持续跑) ──到期──▶[过期失效]──管理员激活──▶恢复原态

四把标尺贯穿始终:

  1. 状态(7 态单字段,见 §3)
  2. 归属(哪个池 / 哪个部门 / 哪个销售,见 §4)
  3. 两把计时器(回收 N 天 / 失效 M 天,见 §5)
  4. 可见性(谁能看到、谁能操作,见 §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.Nexpire_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_history CREATE 记录
  • 勾"是否关注" → 落 lead_follow
  • 触发失效计时 M(起点=create_time);若同步领取,触发回收计时 N

6.2 领取线索(池成员 · a2-1-1 系列)

入口:【线索公海】列表 → 单条【领取】 / 批量勾选后【批量领取】。 前置(应用层校验):

  1. 当前用户在池的可领取成员集合内(claim_rule=1 时=部门全员;claim_rule=2销售不可自领,按钮隐藏)。“部门全员”按部门集合算(2026-08-13 grill #5):销售可领取其所属**任一部门(主+兼职)**公海池的线索,即 pool.dept_id IN 用户部门集合。与 crm-auth 既有 ABAC DEPT 档口径一致(ADR-0005,部门集合=主+兼职并集),“能领的=能看的”自洽。
  2. 未超"每日领取上限"daily_claim_limit(0=无限);"每日" = 自然日(00:00-23:59,DATE(op_time) = CURRENT_DATE
  3. 未超"个人持有上限"hold_limit计数口径 = 仅已领取 + 跟进中(已转商机、过期失效、线索作废不占名额)
  4. 目标线索状态 = 待领取 副作用:设 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_feedbackrecord_status='DRAFT'不校验必填,不改状态机,不写 lead_history,不刷主表快照。草稿按 (user_id, lead_id) 唯一,重复保存为覆盖写。
  • 【保存】(正式提交)lead_feedbackrecord_status='SUBMITTED',同时:
    • 主表更新 5 列反馈快照(feedback_status/content/time/by/product_code
    • 状态:已领取 → 跟进中(边 #7);跟进中 → 跟进中(边 #8)
    • 回收计时 N 重置(起点=now
    • lead_history FEEDBACK 记录(detail 引用 lead_feedback.id,不冗余内容)
    • 清除同 (user_id, lead_id) 的草稿行(转为 SUBMITTED)

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)· 备注。 **副作用(同一本地事务)**:

  1. OpportunityCreationPort#createOpportunity(cmd) → 拿商机 id
  2. UPDATE lead SET status='已转商机' WHERE id=? AND status IN ('已领取','跟进中');行数=0 抛并发异常
  3. lead_history CONVERT 记录,detail 含 opportunityId + opportunityName
  4. 冻结 N/M(靠定时任务 WHERE status != '已转商机' 天然实现,无需 UPDATE 截止字段) 幂等:状态终态保护(crm-lead 侧) + opportunityUNIQUE(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_idowner_name_snapshotowner_dept_id_snapshotclaim_timerecycle_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_history EDIT 记录,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 不够表达):

  1. RBAC 功能权限(既有):跟角色走;权限点控制按钮/API 开关。
  2. ABAC 数据可见范围(既有,四档天花板):页面业务语义(owner=我/dept=本部门)+ SELF / DEPT / DEPT_AND_CHILDREN / ALL,多角色取最宽。
  3. 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 收尾)

编号 事项 阻塞对象 优先级
⚠ (A) 线索作废 已定稿(2026-08-13):反馈=无效自动进入,入口=已领取/跟进中,清空 owner,可恢复(分配+反馈=有效→已领取)。见 §3.4
⚠ (B) 销售换部门后 owner_dept_id_snapshot 是否刷新? 定稿(2026-08-13 grill #11)不刷,快照即历史(审计用)。
⚠ (C) 反馈是否可上传附件? 已解决:反馈可上传附件,存 lead_feedback.attachment_ids(本轮 grill #3)
⚠ (D) 回收/失效 Job 执行节奏 定稿(2026-08-13 grill #11)每日 02:00 全库扫(失效先、回收后,见 §6.7)。
⚠ (E) 线索公海视图是否显示"已被别人领取"的线索 已定稿(2026-08-13):混合展示(防撞单),展示范围可配置不硬编码。见 §7
⚠ PRD-1 行级授权触发动作清单 05、§9 定稿:团队成员+池负责人/协作人
⚠ PRD-2 被授权者权限边界 05、§9 定稿:只读,写权归 service
⚠ PRD-3 移除授权后即时失效 05、§9 定稿:硬删,立即失效
⚠ PRD-D 线索是否跨部门逐条指派 05、§9 定稿:不开 lead 直接通道,分配=改 owner
⚠ PRD-E 池负责人/协作人的授权范围 05、§9 定稿:仅池内线索可见,不兼池配置写权

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-线索实体与字段模型.md
  • lead_feedback(独立反馈实体,DRAFT/SUBMITTED) → 本 PRD §6.4(本轮 grill #3 改写 [Q2=c])
  • lead_pool + 3 关联表 → issues/04-公海池实体与归属建模.md
  • sys_regionissues/02-行政区划数据源调研.md
  • user_column_preferenceissues/07-preference列偏好scope模型与契约.md
  • OpportunityCreationPortissues/08-转商机outbound-port契约.md
  • sys_row_grant(行级授权底座) → issues/09-行级授权sys_row_grant能力.md
  • 权限三机制完整推导 → 权限与数据可见性-PRD.md + issues/05-线索域数据隔离落地.md
  • 模块领域语言 → crm-lead/CONTEXT.mdcrm-rule/CONTEXT.mdcrm-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

13. 批量操作与统计接口(v1.2 补全,2026 grill-with-docs 定稿)

动因:接口完整性复核发现——原型(A2-1-1/A2-1-4/A7-3-1 + 分配/激活弹窗)大量要求批量操作顶部统计卡片,§6 的单条流转已实现但批量与统计接口缺失。本节把缺口 G1–G5 定稿为可实现契约。范围只含 G1–G5;G6(导入/导出)/G7(合并)/G8(池导入导出)维持既有「不做 / Out of scope」判定(见 map.md)。

13.1 批量操作契约(G1–G4)

统一语义:所有批量操作非原子、逐条 CAS、部分成功(沿用 §6.0 全局并发原则)。逐条套用对应单条操作的守卫 + CAS + 上限校验;行数=0 或校验不过即记一条失败,不拖垮整批。

批量接口全集(-batch 后缀,与单条动词对称)

接口 URL 入参 逐条委派 备注
批量领取 POST /api/lead/claim-batch ids: List<Long> claimLead 池成员自领,受每日/持有上限
批量分配到池 POST /api/lead/assign-pool-batch ids, poolId assignToPool 未分发→待领取,不占上限
批量分配到销售 POST /api/lead/assign-user-batch ids, userId assignToUser 统一分配给同一销售;受被指派人上限
批量释放 POST /api/lead/release-batch ids releaseLead 已领取/跟进中→待领取
批量激活 POST /api/lead/activate-batch ids activateLead 过期失效→恢复失效前态,重置 N/M
批量删除 POST /api/lead/delete-batch ids deleteLead 已转商机为删除禁区(记失败)
批量删除公海池 POST /api/rule/pool/delete-batch ids deletePool 池下有非终态线索则该条失败(池删除保护)

批量分配双深度:前端弹窗选了销售调 assign-user-batch,只选池未选人调 assign-pool-batch(对应原型「管理员可以不指定具体销售人员」)。销售人员单选——所有选中线索统一分配给同一 userId

统一返回 Result<BatchResult<LeadBatchFailItem>>

// crm-base/domain/result —— 通用批量结果,对失败项内部结构无感知(保持 crm-base 非业务纯净)
class BatchResult<F> {
    int total;              // 本次提交条数
    int successCount;
    int failCount;
    List<F> failures;       // 逐条失败明细
}

// crm-lead —— 线索域失败项
class LeadBatchFailItem {
    Long leadId;            // 池批量删除时为 poolId
    LeadBatchFailReason reason;
    String message;
}

// crm-lead —— 失败原因语义枚举(code 回指既有 ResultCode 65xxx)
enum LeadBatchFailReason {
    ALREADY_CONVERTED(65009, "已转商机,不可操作"),   // 删除/分配禁区
    CONCURRENT_MODIFIED(65010, "已被他人操作"),        // CAS 行数=0(抢占分配/被回收失效)
    OVER_HOLD_LIMIT(65007, "超出个人持有上限"),
    OVER_DAILY_LIMIT(65006, "超出每日领取上限"),
    STATUS_NOT_ALLOWED(65003, "当前状态不允许此操作"),
    NOT_OWNER(65004, "非本人持有,无权操作");
}

前端结果弹窗按 reason 聚合展示(原型「分配成功 XX 条 / 失败 XX 条 / 失败类型:已转商机 XX 条,抢占分配 XX 条」)。

13.2 统计卡片接口(G5)

接口POST /api/lead/stats,入参复用 LeadPageParam(同一套 viewType + 全部筛选 + @DataScope 部门天花板),忽略分页字段。返回 Result<LeadStatsDTO>

统计口径 = 当前视图数据集口径,不是全库statspage 吃完全相同的数据集条件(viewType + 筛选 + 数据范围),只把「取一页」换成「按 status 分组计数」。例:MY_LEAD 视图的 total = 我持有的线索总条数,claimed = 我的线索里 status IN(3,4) 的条数。每个视图都有自己的卡片;接口通用、全量返回 5 个计数,前端按需取。

class LeadStatsDTO {
    long total;          // 数据集内全部
    long claimed;        // 「已被领取」= status IN (3 已领取, 4 跟进中)——展示文案「已被领取」由前端渲染
    long converted;      // 已转商机 = status 5
    long todayNew;       // 今日新增 = DATE(create_time) = CURRENT_DATE(同样受视图+筛选+数据范围约束)
    long undistributed;  // 未分发 = status 1
}

13.3 复合卡片下钻:status 单值 → statusIn 多值(改写既有 LeadPageParam)

动因:统计卡片可点击下钻——点卡片把该卡的状态条件塞进 LeadPageParam 再调 /page。但「已被领取」是 status IN(3,4) 的并集,单值 Integer status 表达不了。

改写LeadPageParam.status(单值 Integer)→ statusInList<Integer>),/page/stats 共用;列表 SQL 用 status IN (...)。单状态卡传单元素列表,「已被领取」传 [3,4]。原型「全部状态」下拉本就可能多选,一并支持。此改动落在接口尚未联调/未发文档阶段,契约变更成本低。

13.4 决策清单(v1.2)

# 决策 出处
B1 本期只补 G1–G5;G6/G7/G8 维持 Out of scope §13、map
B2 批量返回 Result<BatchResult<F>> 完整体(total/success/fail/failures) §13.1
B3 BatchResult<F> 泛型放 crm-base;LeadBatchFailItem/LeadBatchFailReason 放 crm-lead §13.1
B4 批量接口 -batch 后缀;ids 表单多值传参 §13.1
B5 批量分配双深度(到池 / 到销售),与单条对称 §13.1
B6 LeadBatchFailReason 新造语义枚举,带 code 回指 65xxx §13.1
B7 /stats 复用 LeadPageParam;统计口径随视图(非全库);全量返回前端按需取 §13.2
B8 「已被领取」= status IN(3,4);后端字段 claimed,文案前端渲染 §13.2
B9 LeadPageParam.status 单值 → statusIn 多值,支撑复合卡下钻,page/stats 共用 §13.3