# 04 商机归属流转状态机(轴1) Type: grilling Status: resolved Blocked by: 01, 03 ## Question 定商机的**归属流转状态机**(轴1,商机主表单字段 `opp_status`,与轴2 业务阶段 `oppStage` 正交、互不干扰)。 从原型主流程图(A3 流程说明)与相关操作页(活数据 V1.0-20260815,见 `research-close-pause-actions.md`)抽到的态与迁移,charting 期「暂缓/中止/关闭」已纠偏为「暂缓/抛公海/关闭」,**「中止」不存在**(票 01 已核实)。 产出:状态机全图(态 × 迁移边 × 守卫)+ 终态清单。 ## Answer 三部分拍板:**① 状态取值域**(`opp_status` tinyint 枚举,落票 03 的 B2)、**② 迁移边全图 + 守卫 + 侧效 + 日志**、**③ 与轴2/移交/CAS/DataScope 的关系**。对称线索 `LeadTransition`(sealed `TransitionCmd` 体系 + CAS 乐观锁 + history INSERT),但**商机去掉「操作人==领取人」guard**(票 01:按钮可见即可点,权限全走权限点,与领取人无关)。 ### 一、`opp_status` 取值域(轴1,落票 03 B2) | code | 常量 | 中文 | 语义 | 是否终态 | 有领取人 | |---|---|---|---|---|---| | 1 | `STATUS_IN_POOL` | 待领取(在公海) | 无人持有,躺在商机公海等领取 | ✕ | ✕ | | 2 | `STATUS_ADVANCING` | 推进中 | 有领取人、正常推进(默认活跃态) | ✕ | ✅ | | 3 | `STATUS_PAUSED` | 暂缓中 | 短期推不动但有价值,挂起 | ✕ | ✅(保留) | | 4 | `STATUS_CLOSED` | 已关闭 | 明确无价值,只读锁定(可重启) | 半终态¹ | ✅(保留,只读) | | 5 | `STATUS_CONVERTED` | 已转项目 | 转成项目,锁死不可回退 | **终态** | ✅(保留,只读) | - 对称线索 7 态,商机 5 态:商机无「未分发/失效」概念(无导入分发、无失效计时器——见「五、计时器」),公海侧只有「待领取」。 - **默认活跃态 = 推进中(2)**:新商机(线索转入 or 直接创建、领取、分配、取消暂缓、重启)落地即「推进中」(票 02:新商机=状态「推进中」+阶段「模板第一步」)。 - ¹ **已关闭 = 半终态**:原型明确「已关闭可重新开启」(重启),故非绝对终态,但列为「受限态」——写跟进/推进节点/推送方案/暂缓/抛公海/转项目均禁(研究页规则 3)。唯一出边 = 重启。 - **唯一绝对终态 = 已转项目(5)**:无任何出边,锁死(票 01/CONTEXT「已转项目」)。 ### 二、迁移边全图(态 × 边 × 守卫 × 侧效 × 日志) ``` ┌──────────────── 移交(换 owner,不改 status,团队成员页) │ (2/3 自环,见下) ▼ [1 待领取]──领取/分配──▶[2 推进中]◀──取消暂缓──[3 暂缓中] ▲ │ │ ▲ ▲ │ 抛公海 │ │ └──── 暂缓 ────────┘ └────────────────────┘ │ (2→3;3 到期不自动恢复) │ ┌── 关闭 ────────┼──── 关闭 ──┐ │(2→4) │(3→4) │ ▼ │ ▼ [4 已关闭]──重启─────┘ [4 已关闭] │(4→2) ▼ [2 推进中]──转项目──▶[5 已转项目](终态,无出边) ``` 逐条边(迁移命令 = sealed `OpportunityTransitionCmd` 子类型,对称 `TransitionCmd`): | # | 动作 | Cmd | 起始态 | 目标态 | owner 侧效 | 其他侧效 | 日志(P6) | |---|---|---|---|---|---|---|---| | E1 | 领取 | `ClaimCmd` | 1 | 2 | 设 owner=当前用户 + 部门快照 + 姓名快照,设 `claim_time`;刷新 `last_valid_follow_time`=now(领取视为一次归属起点,票 05) | ~~`pool_id`~~ 票 05 删(无池实体) | ✅ 领取 | | E2 | 分配 | `AssignToUserCmd` | 1 | 2 | 设 owner=指派对象 + 部门/姓名快照,设 `claim_time` | — | ✅ 分配(记指派人+对象) | | E3 | 抛公海 | `ReleaseToPoolCmd` | 2 | 1 | **清 owner**(user_id/姓名/部门快照置空),清 `claim_time`;**记 `pool_reason`**(销售主动释放/特殊原因暂停/领导释放;票 10);**快照入池前工作节点**(保留 `current_stage_id` 供公海展示) | **不动团队成员**(票 01);无回池目标——公海=`opp_status=1` 逻辑视图,`owner_dept_id` 保留定可见范围(票 05);入口受规则 `allow_manual_pool` 开关控制;**「领导释放」= 管理侧抛公海**(操作人≠owner,管理页 A3-2-1,票 10) | ✅ 抛公海 | | E4 | 暂缓 | `PauseCmd` | 2 | 3 | 保留 owner + 团队(票 01) | 记暂缓原因(字典)+预计重启日期(必填≥今天)+补充说明;保留暂缓前阶段 | ✅ 暂缓(原因/重启日期/说明) | | E5 | 取消暂缓 | `ResumeCmd` | 3 | 2 | 保留 owner | 回到暂缓前阶段(阶段未动,无需恢复);清暂缓字段 | ✅ 取消暂缓 | | E6 | 关闭 | `CloseCmd` | 2, 3 | 4 | 保留 owner(只读锁定) | 记关闭原因(字典)+补充说明(选「其他」必填)+确认勾选;保留关闭前 status²/阶段 | ✅ 关闭(原因/说明) | | E7 | 重启 | `ReopenCmd` | 4 | 2 | 保留 owner | 记重开原因(必填);回「推进中」³ | ✅ 重启(重开原因) | | E8 | 转项目 | `ConvertToProjectCmd` | 2 | 5 | 保留 owner(只读锁定) | 转项目端口占位(A5,票外);锁死 | ✅ 转项目 | | E9 | 移交 | `HandoverCmd` | 2, 3 | **不变**(自环) | **换 owner**=移交对象 + 部门/姓名快照(票 01:移交只换 owner_user_id) | 团队成员页触发(A3-1-1-1-8);`claim_time` 不动⁴ | ✅ 移交(原 owner→新 owner) | - ² 关闭需存「关闭前 status」以便重启恢复?**否**——原型「恢复至暂缓前节点(阶段)」指轴2,轴1 重启统一回「推进中(2)」(E7)。关闭前 status 只可能是 2 或 3;暂缓中(3)被关闭后重启回 2 即可,不需要精确回 3(产品口径:重启 = 恢复推进)。若产品后续要求「暂缓中关闭→重启回暂缓」,再补 `status_before_close` 字段,本票**不建**。 - ³ 已关闭态下「暂缓/抛公海」受限(研究页),故 4 的唯一出边是 E7 重启。 - ⁴ 移交不重置计时(`claim_time`/`last_valid_follow_time` 保留原值)——移交是换人不是重新领取。 **受限态出边约束**(守卫层硬禁,研究页规则): - 态 4(已关闭):仅允许 E7(重启);E4/E6/E3/E8 及写跟进/推进节点 全禁。 - 态 5(已转项目):**无任何出边**,全禁(编辑/暂缓/抛公海/关闭/移交/转项目)。 - 态 3(暂缓中):允许 E5(取消暂缓)/ E6(关闭)/ E9(移交);推进节点/推送方案/转项目 受限(研究页规则 5,业务权限配置决定,非硬禁——本票按「转项目从暂缓中禁」处理,需先取消暂缓)。 ### 三、守卫模型(对称线索但去 owner guard) - **无「操作人==领取人」guard**(票 01 拍板:详情页顶部操作区动作可见即可点,权限全由权限点决定)。→ 对称线索 `TransitionCmd.requiresOwner()` 在商机侧**恒为 false / 不建此维度**。 - **移交 E9 的权限**:不是额外 guard,而是入口位置——移交在团队成员页(A3-1-1-1-8),团队管理权随负责人(原型「只能维护其权限范围内的团队成员」)。状态机层不校验「谁能移交」,只做 CAS + 换 owner;权限在 Controller/Service 层由团队成员页的权限点拦。 - **合法起始态集合**:每条 Cmd 自带 `allowedFromStatuses()`(对称线索 sealed 体系),`OpportunityTransition.execute` 分派前跑一次友好前置(非法起始态抛 `CODE_STATUS_NOT_ALLOWED`)。 - **DataScope 叠加**:状态机 execute 不管可见性;可见性由 `@DataScope(module="opportunity", ...)`(票 03 C2)+ 团队成员行级白名单在查询/入口层解决。状态机是「能不能改这条」的最后一道 CAS,不是「能不能看到」。 ### 四、CAS / 并发(对称线索 ADR-0021) - 每条迁移 = `UPDATE opportunity SET ... WHERE id=? AND opp_status IN (合法起始态)`;**行数=0 即失败**抛 `CODE_CAS_FAIL`(并发下已被他人改态)。 - 领取 E1 的并发:两人同时领 → CAS `WHERE opp_status=1` 只有一人成功,另一人行数=0 友好提示「已被领取」。对称线索领取。 - 移交 E9 CAS:`WHERE id=? AND opp_status IN (2,3) AND owner_user_id=?原owner`(防并发移交/抛公海竞态)。 ### 五、计时器(对称线索但更少) - **无「失效计时 M」自动 Job(票 05 已定)**——线索有回收/失效两把计时器;商机只有自动回收(无失效态)。回收锚点 = `last_valid_follow_time`(最近有效跟进时间,票 05 改;非 claim_time+N)。**E3 抛公海是人工动作**(受 `allow_manual_pool` 开关控制);批量自动回收 = 新增边 `executeRecycle`(对称 `LeadTransition.executeRecycle`):`WHERE opp_status=2 AND now − last_valid_follow_time ≥ rule.recycle_days → 1`,清 owner,`owner_dept_id` 保留定公海可见范围。**暂缓中(3) 不参与回收,回收计时暂停**(票 05 产品拍板)——`executeRecycle` 只扫推进中(2);取消暂缓回推进中后按剩余天数续算(重置只发生在有效跟进)。**自动回收亦记 `pool_reason`=超期回收 + 快照入池前节点**(票 10)。 - **暂缓到期不自动恢复**(研究页规则 7 + 票 01):预计重启日期到 → 系统发提醒(异步),**状态不动**,领取人人工选 E5/继续暂缓/E6。「继续暂缓」= 重填预计重启日期(更新 pause 字段,status 仍 3,不算状态迁移,走普通 update)。 - **本票主表计时锚点**:`claim_time`(票 03 B4)+ `last_valid_follow_time`(票 05 回填至票 03 B4,回收计时锚点)。~~`recycle_deadline`~~ 票 05 撤销(改实时算 now−锚点)。 ### 六、接口形态(spec,实现留给落地方,对称 `LeadTransition`) ```java /** 商机状态机——轴1 归属流转全部迁移的唯一入口。深模块,implementation 吸收 CAS/侧效/日志。 */ public interface OpportunityTransition { /** 单条迁移;CAS 失败抛 CODE_CAS_FAIL,非法起始态抛 CODE_STATUS_NOT_ALLOWED。 */ void execute(Long oppId, OpportunityTransitionCmd cmd); /** 批量回收(票 05):推进中(2) 且 now−last_valid_follow_time ≥ recycle_days → 待领取(1),清 owner,保留 owner_dept_id。 */ int executeRecycle(java.time.LocalDateTime cutoff); } public sealed interface OpportunityTransitionCmd permits ClaimCmd, AssignToUserCmd, ReleaseToPoolCmd, PauseCmd, ResumeCmd, CloseCmd, ReopenCmd, ConvertToProjectCmd, HandoverCmd { Long operatorId(); Set allowedFromStatuses(); // 无 requiresOwner():商机不建 owner guard(票 01) } ``` 调用方(OpportunityServiceImpl)负责:预计算快照/deadline、业务前置(领取规则/公海归属票 05)、权限点校验、outbound port(转项目,A5)。状态机对 crm-rule/crm-auth 零依赖(对称线索)。 ### 传出决策 / 不阻塞本票 - **超时回收计时器**(锚点 `last_valid_follow_time` + 批量边 `executeRecycle`;实时算 now−锚点≥recycle_days)→ 票 05 已定,回填本票「五/六」。~~`recycle_deadline`~~ 撤销。 - **抛公海回哪个池**:票 05 定稿——商机域无池实体,抛公海=清 owner + opp_status=1,`owner_dept_id` 保留定可见范围,无回池目标。 - **转项目端口契约** → A5 项目模块(out of scope,票 01)。 - **`status_before_close` 精确恢复** → 本票**不建**,产品若要求再补。 - **状态×阶段联动**:轴2 阶段推进是独立动作(不走本状态机),暂缓/关闭时阶段冻结、重启/取消暂缓时阶段不动(回原阶段)。阶段模板版本锁 → 票 06。 ### 已核实事实 - 状态相关动作 = 暂缓(A3-1-1-2-6)/抛公海(A3-1-1-2-7)/关闭(A3-1-1-2-9) + 公海侧领取(A3-3-1-1-2)/分配(A3-3-1-1-4);「中止」不存在(票 01 + research-close-pause-actions.md)。 - 关闭 = 半终态可重启;已转项目 = 绝对终态(票 01/CONTEXT)。 - 移交只换 owner、入口在团队成员页、无独立转让动作(票 01)。 - 抛公海只清 owner 不动团队;暂缓保留 owner+团队不进公海(票 01 + research)。 - 无 owner guard,可见即可点(票 01)。 - CAS 乐观锁 + history INSERT 对称线索 `LeadTransition`(ADR-0021)。