13 KiB
线索业务(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(要点是「四视图读 + 展示拼装」一块进一个深模块)
批量结果:
批量操作(批量领取/分配/释放/激活/删除)的统一返回体,通用骨架 BatchResult<F> 落 crm-base(total/successCount/failCount/failures,对失败项内部结构无感知),线索域失败项 LeadBatchFailItem(leadId + LeadBatchFailReason + message)落 crm-lead。批量非原子、逐条 CAS、部分成功:逐条套单条守卫,行数=0 或上限校验不过记一条失败,不拖垮整批。失败原因是语义枚举 LeadBatchFailReason(ALREADY_CONVERTED / CONCURRENT_MODIFIED / OVER_HOLD_LIMIT / OVER_DAILY_LIMIT / STATUS_NOT_ALLOWED / NOT_OWNER,各带 code 回指 ResultCode 65xxx),供前端按类型聚合展示「成功 N 条 / 失败 M 条(各类型明细)」。
Avoid: 批量响应、原子批处理、事务批量(要点是「非原子部分成功 + 结构化失败原因」)
视图统计:
四视图顶部统计卡片的数据来源 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 是权限树概念)、全量四菜单挂载(已推翻)