# 线索业务(crm-lead) 线索的全生命周期业务域:线索实体与 7 状态机、四视图(公海 / 我的线索 / 我的关注 / 线索管理)、领取 / 分配 / 反馈 / 转商机 / 释放等流转,以及超时回收与失效两个定时任务。依赖方向单向 `crm-lead → crm-rule → (crm-auth / crm-dict)`;公海池实体归 `crm-rule`,本模块只消费。访问控制走三机制模型(详见 `.scratch/clue-module/权限与数据可见性-PRD.md`)。 ## Language **线索**: 一条待转化的销售机会记录,`crm-lead` 的核心聚合根。有单一状态字段 `leadStatus`(7 态),归属由 `owner_user_id`(领取人)+ `dept_id`(所属公海池归属部门)表达。全生命周期最终走向「已转商机」或被清理。 _Avoid_: clue、商机(商机是转化后的下游实体)、客户 **领取人**: 当前持有该线索的销售,存于 `owner_user_id`。只有「已领取 / 跟进中」态有领取人;释放、超时回收、进入作废时清空。领取人是「我的线索」视图的第一层业务过滤依据(`owner_user_id = 当前用户`)。 _Avoid_: 负责人(负责人是公海池概念)、持有者、拥有人 **创建人**: 录入该线索的用户,全生命周期不变。删除权限的判定依据之一——`删除 = (状态≠已转商机) AND (管理员 OR 创建人)`(强确认;已转商机为删除禁区)。 _Avoid_: 录入人、owner(owner 是领取人) **所属公海池归属部门**: 线索的 `dept_id`,恒等于其所属公海池的归属部门,全生命周期**不变动**(决策 [I])。是数据范围天花板过滤的唯一部门维度。销售可领取/可见其所属**部门集合(主+兼职)**的公海池线索(grill #5);领取口径与 crm-auth ABAC `DEPT` 档一致(都按部门集合算,ADR-0005),“能领=能看”自洽。 _Avoid_: 领取人部门、归属部门、owner_dept **反馈情况**: 线索的独立字段(有效 / 无效 / 未反馈),**不进状态机**,与 `leadStatus` 并存。是「反馈=无效自动作废」「反馈=有效使作废恢复」两条迁移边的触发内容,但字段本身独立记录。 _Avoid_: 反馈状态、跟进结果、线索质量 **反馈记录**: 销售对线索的一次跟进记录,存于独立表 `lead_feedback`(DRAFT / SUBMITTED 两态,草稿可暂存、可上传附件)。主表冗余最新一条的反馈情况值供列表展示;释放 / 回收时反馈快照不清空(供下一位参考)。 _Avoid_: 跟进记录、feedback(英文)、备注 **未分发**: 线索状态之一:管理员新建 / 导入但**未归属任何公海池**,悬于池外。仅管理员可将其分配到池。与「待领取」的区别是「是否已归池」。 _Avoid_: 未分配、待分配、草稿态 **待领取**: 线索状态之一:已分配进某公海池、尚无领取人,池内成员可自领。是领取 / 分配的来源态,也是释放 / 回收 / 激活作废的目标态。 _Avoid_: 公海线索、未领取、待认领 **已领取**: 线索状态之一:某销售持有、尚未产生反馈。领取后即可转商机,不强制先反馈。启动回收计时 N。 _Avoid_: 已认领、私海线索、跟进态 **跟进中**: 线索状态之一:持有销售已产生至少一次反馈。再次反馈为自环并续期回收计时 N。 _Avoid_: 联系中、有效(有效是反馈情况) **已转商机**: 线索**终态**:已调 `OpportunityCreationPort` 转化为商机。锁死——不可释放 / 回收 / 反馈 / 编辑,回收与失效两个计时器均冻结。 _Avoid_: 已成交、已转化、完成 **过期失效**: 线索状态之一:失效计时 M 到期(从创建时间起算)。可发生在任何非终态;失效时若被持有则保留领取人、只读。可被管理员激活(F2)恢复到失效前状态与原领取人,并重置 N 和 M。 _Avoid_: 过期、失效、废弃 **线索作废**: 线索状态之一:销售提交反馈且反馈情况=无效时**自动进入**(无需手动点按钮),入口态为「已领取 / 跟进中」。进入时清空 `owner_user_id`。**可恢复**——管理员可分配给新销售(仍保持作废态),被分配销售提交反馈=有效时恢复到「已领取」。失效计时 M 在作废态下继续跑。 _Avoid_: 作废、无效线索(无效是反馈情况)、删除 **回收计时 N**: 超时回收公海的计时器:仅线索被销售领取到私海(已领取 / 跟进中)后计时,N 天内无新反馈则自动退回公海(→待领取、清领取人)。有反馈则续期重计,退回公海则清零。天数取所属公海池的 `recycle_days`。 _Avoid_: 超时时间、回收天数、N 天 **失效计时 M**: 线索失效的计时器:从**创建时间**起全程持续跑,无论在公海或私海、不受回收操作影响、不清零。到期直接置「过期失效」。仅「已转商机」冻结、「管理员激活」重置。天数取所属公海池的 `expire_days`。 _Avoid_: 有效期、失效天数、M 天 **分配**: 管理员把线索指派出去的动作,是可选深度动作:可只分配到公海池(未分发→待领取),或一步指定到某销售(→已领取,领取人=被指派销售)。**分配 = 改 `owner_user_id`,不发行级授权通行证**(决策 [D])。作废线索也可被分配(分配后仍作废)。 _Avoid_: 指派、派发、授权 **释放**: 持有销售主动把线索退回公海的动作:已领取 / 跟进中 → 待领取,清领取人,回收计时 N 清零。与超时回收(系统自动)目标态相同,区别是触发方。 _Avoid_: 退回、放弃、回收(回收是系统动作) **激活**: 管理员对过期失效线索的恢复动作(F2):恢复到失效前状态与原领取人,重置 N 和 M。注意与「作废恢复」不同——作废靠「分配+反馈=有效」恢复,不走激活。 _Avoid_: 恢复、复活、重置 **关注**: 用户对线索的收藏关系,存于 `lead_follow`。「我的关注」视图 = `id IN 关注表` AND 部门天花板。**关注不走行级授权**(决策 [F])——可见性每请求按当前部门实时算,防止「人事误操作→关注→改回部门→长期越权」。关注总数现算不冗余。 _Avoid_: 收藏、订阅、star **操作日志**: 线索的统一操作留痕表 `lead_history`,**全量**记录 11 种动作(CREATE / CLAIM / ASSIGN / FEEDBACK / CONVERT / RELEASE / RECYCLE / EXPIRE / ACTIVATE / VOID / EDIT)。反馈记录在此只记引用(正文在 `lead_feedback`);作废(VOID)在 detail 保留原持有人供审计。是审计与纠纷排查的唯一来源,不靠授权表本身。详情页「历史记录」Tab **只展示 5 种**(领取 / 释放 / 反馈 / 转商机 / 编辑),其余为审计用不上页。 _Avoid_: 变更记录、审计表、log **转商机端口**: 线索转化为商机的出站契约 `OpportunityCreationPort`(商机模块尚不存在,本模块只定接口)。触发前置 = `status IN (已领取, 跟进中)`,反馈情况不参与判定。幂等 = 状态终态保护 + 商机侧 `UNIQUE(source_lead_id)`。**事务前提:首个实现必须与 crm-lead 同库共享事务**(本地事务性回滚);若未来商机独立部署,转商机需重设计为最终一致 + 补偿。 _Avoid_: 商机接口、转化服务、OpportunityService **池占用查询适配器**: 公海池删除守卫的入站实现 `PoolLeadOccupationAdapter`(crm-lead 实现 crm-rule 声明的 `PoolLeadOccupationPort`,ADR-0025)。查 lead 表 `pool_id` 匹配且 `status` 为非终态(1/2/3/4)的 count > 0。供 `LeadPoolServiceImpl.deletePool()` 调用——crm-rule 不依赖 crm-lead,通过依赖倒置跨模块查询。与「转商机端口」方向相反的同型 seam:转商机是 crm-lead 声明等外部实现,池占用是 crm-rule 声明等本模块实现。 _Avoid_: 占用检查服务、pool 查询服务(要点是「实现 crm-rule 的出站端口、跨模块查 lead 表」) **状态机守卫**: 状态迁移的统一前置校验(ADR-0022 候选 1),收在 `LeadTransition` 内的 `guard`(原散落在 service 与 transition 两处的 19 处判定)。每条 `TransitionCmd` 自带声明 `allowedFromStatuses()`(合法起始态集合)与 `requiresOwner()`(是否要求操作人=领取人,纯代码常量、不建表);`execute` 分派前统一跑一次,在 ADR-0021 的 CAS 之上给出友好前置——非法起始态抛 `CODE_STATUS_NOT_ALLOWED`、非领取人抛 `CODE_NOT_OWNER`。「起始态 × 命令内容」的组合子规则(作废+反馈无效、作废自环换 owner)仍留在各 `applyXxx`。ADMIN_ONLY 池规则、转商机端口调用时序等要查 crm-rule/port 的前置留在 `LeadServiceImpl`,守住 `LeadTransition` 对 crm-rule/crm-auth 的零依赖契约。 _Avoid_: 前置校验、参数校验、状态检查(要点是「声明式起始态集合 + 一处 guard」) **历史记录器**: `LeadHistoryRecorder`——线索模块内共用的深模块(ADR-0022 候选 2),单方法 `record(leadId, type, userId, detailKv…)` 藏起「组装 `LeadHistory` + kv 明细序列化为 JSON + 落库」整条机制,供 `LeadServiceImpl`(创建/编辑)与 `LeadTransition`(状态迁移)共用,消除两处逐字重复的 writeHistory/buildDetail。**空 kv 存 `null`**(保持历史行为,不写 `{}`);序列化失败告警并存 null。仅依赖 crm-lead 内部 mapper/实体/枚举,注入 `LeadTransition` 不破坏其零依赖契约。 _Avoid_: 日志记录器、审计器、logger(写的是「操作日志」表,不是应用日志) **线索读侧**: `LeadViewQuery`——与写侧命令编排分离的读模块(ADR-0022 候选 3),吃下四视图分页(PUBLIC_POOL / MY_LEAD / MY_FOLLOW / MANAGE)、详情、历史时间线,以及展示字段拼装(省市 code→name、关注总数、当前用户是否关注)。**独占读依赖**:`sysRegionService`(省市名,crm-rule)、`leadHistoryMapper`(历史读)、`leadFollowMapper`(关注读,与写侧共享同一 mapper bean);自持 `LeadMapper` 用 `selectPage`(不再借基类 `this.page()`)。`LeadServiceImpl` 的 `pageLeads`/`getLeadDetail`/`listHistory` 瘦成一行委派,controller 仍只认 `ILeadService` 契约(不直接碰读模块)。拆后 `LeadServiceImpl` 只剩写侧命令编排(创建/编辑/删除、领取/分配/反馈/转商机/释放/激活、关注写)。 _Avoid_: 查询服务、QueryService、read model(要点是「四视图读 + 展示拼装」一块进一个深模块) **创建规划**: `LeadCreationPlanner`——创建路径的深模块(ADR-0026),单方法 `plan(LeadDTO, LeadCreateContext)` 吃纯值上下文(操作人 / 是否管理员 / 选人即分配 / claimOnCreate),吐一个完成归池绑定、初始状态、快照、计时预算与领取仪式的**未保存** Lead。四分支规则树(销售自建强制主部门池 / 选人即分配 / 管理员建未分发 / 管理员指定池)、资格链(ADMIN_ONLY 判定 + ClaimLimitChecker)与两个仪式(归池 4 字段 / 领取 7 字段)独占于此——claimOnCreate 不可能再绕过校验链(P0-4 / P0-7 的结构性根治)。零静态依赖(角色与当前用户从 ctx 进入,不碰 SecurityUtils);validateLeadFields 留在 service(create / edit 共用);claimLead / assignToUser 的 CAS 版仪式仍在状态机——那是并发防线,不重复的是「规则」不是「锁」。 _Avoid_: 创建工厂、Creator、Builder(要点是「按角色分支规则完成归池与领取的规划」) **双计时器预算**: `LeadDeadlines`——双计时器规则的深模块(ADR-0027),4 方法接口吃下 N/M 的起算、重置、条件、锚点与池参数变更批量重算:`recycleOnPrivateEntry(pool, anchor)`(进入/维持私海的 N 预算,领取/分配/作废恢复/有效反馈共用)、`expireFromCreation(createTime, pool)`(M 锚创建时刻——创建时 `now` 与 assignToPool 的 `createTime` 是同一规则的方法名显式化)、`budgetForActivation(lead, pool, now)`(激活条件全收:失效前态是否私海决定 N、无池保留原 M)、`refreshForPoolChange(poolId, pool)`(§5.3 三段 DATE_ADD 批量重算,池参数变更联动)。返回 `DeadlineBudget` record(recycle/expire 两字段),生命周期止于调用方——解构后填 `TransitionCmd` 既有散字段,不穿越状态机 seam(ADR-0021 的「调用方预计算」契约不变,错的只是预计算无家)。**不收**:停止执行(清 null 与清 owner 同批 CAS 侧效,留守 `LeadTransition`)、到期扫描(ADR-0019 属地)、起始态分支(状态机语言留调用方)。边界判据:「调用方本来就要做的判断不收,只有计时器才要做的判断收」。 _Avoid_: 计时器服务、DeadlineService、Timer(要点是「N/M 规则的唯一家 + 预算值对象止于调用方」) **批量结果**: 批量操作(批量领取/分配/释放/激活/删除/关注/取关)的统一返回体,通用骨架 `BatchResult` 落 crm-base(total/successCount/failCount/failures,对失败项内部结构无感知),线索域失败项 `LeadBatchFailItem`(leadId + `LeadBatchFailReason` + message)落 crm-lead。批量**非原子、逐条 CAS、部分成功**:8 个批量方法一行委派 crm-base `BatchRunner.run(ids, self::单条方法, LeadBatchFailItem::bizOf, LeadBatchFailItem::unknownOf)`(ADR-0028)——逐条经 self 代理走独立事务,失败不拖垮整批;code→reason 翻译与兜底文案(「操作失败」)由 `LeadBatchFailItem` 翻译工厂自持。失败原因是语义枚举 `LeadBatchFailReason`(ALREADY_CONVERTED / CONCURRENT_MODIFIED / OVER_HOLD_LIMIT / OVER_DAILY_LIMIT / STATUS_NOT_ALLOWED / NOT_OWNER,各带 code 回指 ResultCode 65xxx),供前端按类型聚合展示「成功 N 条 / 失败 M 条(各类型明细)」。 _Avoid_: 批量响应、原子批处理、事务批量(要点是「非原子部分成功 + 结构化失败原因 + 编排委派 BatchRunner」) **视图统计**: 四视图顶部统计卡片的数据来源 `LeadStatsDTO`(total / claimed / converted / todayNew / undistributed)。**统计口径 = 当前视图数据集口径,非全库**——`/stats` 与 `/page` 吃完全相同的 viewType + 筛选 + `@DataScope` 部门天花板,只把「取一页」换成「按 status 分组计数」。`claimed`(「已被领取」,展示文案前端渲染)= status IN(已领取, 跟进中) 的并集。卡片可点击下钻:把该卡状态条件塞进 `LeadPageParam.statusIn`(单值 status 已升级为多值列表以表达「已被领取」这种复合状态)再调 `/page`。 _Avoid_: 全局统计、看板指标、汇总(要点是「口径随视图 + 复合状态用 statusIn 多值下钻」) **菜单**: 前端导航项(sys_menu type=MENU),seed 真相源 `LeadPermissionInitializer`:线索管理目录 → 公海 / 我的线索 / 我的关注 / 线索管理。**产品原型措辞「线索公海」与 seed「公海」不一致**,接口文档 tag 以原型为准(ADR-0024);seed 侧统一需数据迁移(seeder 按 name find-or-create,直接改字符串会 seed 出第二个菜单)。 _Avoid_: 页面、路由(菜单带 route,但菜单 ≠ 路由本身) **菜单 tag**: `@Operation(tags)` 声明的接口文档页面归属(bruno collection 文件夹),命名 = 原型菜单(含 / 按目录嵌套)。**每页实挂**(ADR-0024 修订 2026-08-17):接口只挂有真实页面入口的菜单,真相源 = 原型逐页交互矩阵;四视图接口(page/detail/history/stats/列偏好)四页各一份副本,按 `bruno-sync.config.json` 的 menuBindings 绑定专属参数值:线索公海 = viewType PUBLIC_POOL / scopeKey lead.public_pool · 我的线索 = MY_LEAD / lead.my_lead · 我的关注 = MY_FOLLOW / lead.my_follow · 线索管理 = MANAGE / lead.manage。 _Avoid_: 页面 tag(旧称)、分组、目录(catalog 是权限树概念)、全量四菜单挂载(已推翻)