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.
 
 
 
 
 

13 KiB

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_timepool_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

/** 商机状态机——轴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<Integer> 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)。