Browse Source

docs(opportunity): 拆票15/16 并回写 map(票15 已落 resolved)

票15 全局默认阶段模板兜底 seed 已实现落地;票16 转商机同步事务补齐
(阶段落位+初始日志+团队成员)blocked by 15 现已解除,待开工。
master
luoweijian 2 weeks ago
parent
commit
49cbab687c
  1. 53
      .scratch/opportunity-module/issues/15-default-stage-template-seed.md
  2. 60
      .scratch/opportunity-module/issues/16-conversion-txn-stage-oplog-team.md
  3. 12
      .scratch/opportunity-module/map.md

53
.scratch/opportunity-module/issues/15-default-stage-template-seed.md

@ -0,0 +1,53 @@
# 15 全局默认阶段模板兜底 seed(crm-rule)
Type: task
Status: resolved
Blocked by: 06
## Question
系统需预置一条**发布中的全局默认阶段模板**,作转商机时 `resolveBindingVersion` 的命中兜底。
产品拍板(20260826)默认阶段轨道:**客户圈定 → 关系摸排 → 资料采集 → 方案卡 → 已转项目**。
这不是新发明——**方案卡模板已有此范式**(`SchemeFieldDefInitializer` §2「兜底模板种子化」,grill 拍板 20260821:植入一条全公司通用发布中默认模板作建卡兜底)。
阶段模板漏了对称的这一条,本票补齐。
## 已核实事实(本票零 A4 依赖、零字典改动)
- **`opp_stage` 字典 5 项已种好**(`crm-dict/DictDataInitializer`,code 完全对上):
`OPP_STAGE_01 客户圈定` / `02 关系摸排` / `03 资料采集` / `04 方案卡` / `05 已转项目`
→ 本票**不碰字典**,只把这 5 项组装成一个 V-CONFIG 模板版本行。
- **范式模板** = `crm-rule/config/SchemeFieldDefInitializer`(`CommandLineRunner` + `@Order` + 按 code 幂等判存,含已停用不复活)。
- **实体**:`OpportunityStageTemplate`(头:`template_code`/`template_name`/`version_no`/`status`/`apply_scope`/`is_default`/`template_desc`)
+ `OpportunityStageNode`(节点:`template_id`/`seq_no`/`stage_dict_code`/`custom_node_name`/`work_goal`/`is_fixed`)。
- **常量**(`OpportunityRuleConstants`)已有:`STAGE_TPL_CODE_PREFIX="OPP_STAGE_TPL_"` / `FIXED_STAGE_DICT_CODE="OPP_STAGE_05"` / `FIXED_STAGE_NAME="已转项目"`
/ `VERSION_STATUS_PUBLISHED=2` / `APPLY_SCOPE_ALL=1` / `FLAG_YES=1`
**缺** `SEED_STAGE_TPL_CODE` / `SEED_STAGE_TPL_NAME`(对称方案卡的 `SEED_SCHEME_TPL_CODE/NAME`)。
- **消费缝**:`IOpportunityStageTemplateService.resolveBindingVersion(deptId)` javadoc 明写「无任何发布中模板可匹配时返回 null」——seed 保证常态命中,票 16 的「容忍空」退化为极端兜底(管理员停用全部模板才触发)。
## 实现范围
1. **常量**:`OpportunityRuleConstants` 加 `SEED_STAGE_TPL_CODE = STAGE_TPL_CODE_PREFIX + "01"`(= `OPP_STAGE_TPL_01`)+ `SEED_STAGE_TPL_NAME`(如「全公司通用阶段模板」)。
2. **`StageTemplateInitializer`**(`crm-rule/config`,照 `SchemeFieldDefInitializer` 范式):
- `CommandLineRunner`,`@Order` 排在阶段模板依赖就绪之后。
- 幂等键 = `template_code`:同 code 行存在(含已停用)即跳过,管理员停用后重启不复活。
- 首装植入:
- **1 头**:`template_code=SEED_STAGE_TPL_CODE`、`version_no="V1.0"`、`status=发布中`、`apply_scope=所有商机`、`is_default=1`、`template_name=SEED_STAGE_TPL_NAME`、`template_desc` 说明系统初始化兜底。
- **5 节点**(`template_id=` 回填的头 id,`seq_no` 1-5):
| seq | stage_dict_code | is_fixed |
|---|---|---|
| 1 | OPP_STAGE_01(客户圈定) | 0 |
| 2 | OPP_STAGE_02(关系摸排) | 0 |
| 3 | OPP_STAGE_03(资料采集) | 0 |
| 4 | OPP_STAGE_04(方案卡) | 0 |
| 5 | OPP_STAGE_05(已转项目) | **1**(固定末节点,票 06 契约)|
- `custom_node_name` 留空(带入字典名),`work_goal` 留空。
3. **测试** `StageTemplateInitializerTest`(照 `SchemeFieldDefInitializerTest` 范式,Mockito + `TableInfoHelper.initTableInfo`):
- 首装:种 1 头(发布中/全公司通用/默认/V1.0/固定编码)+ 5 节点(seq 1-5、末节点 `is_fixed=1``stage_dict_code=OPP_STAGE_05`、前 4 节点 `is_fixed=0`)。
- 幂等重跑:模板按 code 判存(含已停用),插入总数不变(1 头 5 节点各补 0)。
## 边界
- **不碰 `opp_stage` 字典**(已种)。
- **不改 `resolveBindingVersion` 服务逻辑**(已实现,本票只喂它数据)。
- 不做「指定部门」范围模板(`apply_scope=2`);本票只种「所有商机」默认兜底一条。

60
.scratch/opportunity-module/issues/16-conversion-txn-stage-oplog-team.md

@ -0,0 +1,60 @@
# 16 票 02 转商机同步事务补齐:阶段落位 + 初始操作日志 + 团队成员
Type: task
Status: open
Blocked by: 15
## Question
票 02 底稿「簇 D — 事务边界 + 转商机时的子表」要求转商机时**同步事务内**做 5 件事,
`OpportunityCreationPortImpl` 现只落了其中 3 项(幂等检查、建主表、建关联客户),**缺 3 步**:
| 步 | 底稿要求(票 02 簇 D) | 现状 |
|---|---|---|
| 1 | 幂等检查(查 source_lead_id) | ✅ 已落(票 12 预检 + DB uk 兜底)|
| 2 | 建商机主表行 + **初始状态/阶段** | ⚠️ 主表 ✅,**阶段落位缺**(`current_stage_id` 未设)|
| 3 | 写一条初始操作日志(「由线索转入创建」)| ❌ **缺** |
| 4 | 建团队成员一条(领取人=商机负责人)| ❌ **缺** |
| 5 | 建关联客户一条(主要意向客户)| ✅ 已落(customerId 非空时)|
本票补齐第 2(半)/3/4 步。**三步全是商机域内部写入,零 A4 依赖。**
## 决策(20260826 拍板)
- **阶段落位「无发布中模板」= 容忍空(走法 A)**:`resolveBindingVersion` 返 null 时 `current_stage_id=null`,商机照建,不阻塞转商机。
票 15 已预置全局默认阶段模板,常态必命中,「容忍空」退化为极端兜底(管理员停用全部模板才触发)。
与模块「customerId 可空占位、不倒退」哲学一致。
## 已核实事实(基础设施齐备)
- **阶段落位缝**:`IOpportunityStageTemplateService.resolveBindingVersion(deptId)` → 命中取版本行
`listNodesOfVersion(versionId)``seq_no` 首节点。写法可抄 `OpportunityConvertPortImpl`(转项目端口已用同款缝驱动 `current_stage_id`)。
- **操作日志**:`OpportunityOplog` + `OpportunityOplogMapper`;枚举 `OplogKind.ROW_ADD` / `OplogLogType.ROW_CHANGE` / `opSource="SYSTEM"`
- **团队成员**:`OpportunityTeam` + `OpportunityTeamMapper`(实体 javadoc 已点名「转商机时自动插一条:领取人=商机负责人(票 02)」);
`TeamMemberPermission.READ_WRITE`;`project_role_01`(crm-dict `project_role` 分组「商机负责人」,已核实)。
- **主表阶段字段**(票 03/06):`current_stage_id`(节点 id)+ `stage_template_id`(版本行 id 锁版本)+ `stage_template_version`(varchar 冗余版本号)。
## 实现范围(`crm-opportunity/OpportunityCreationPortImpl`)
1. **注入** `IOpportunityStageTemplateService` + `OpportunityOplogMapper` + `OpportunityTeamMapper`
2. **阶段落位**(第 2 步补齐,插主表前):
- `resolveBindingVersion(cmd.ownerDeptId())` → 命中:`stage_template_id=版本行 id`、`stage_template_version=版本号`、
`current_stage_id=listNodesOfVersion 首节点 id`
- 返 null:三字段留空(容忍空,走法 A)。
3. **初始操作日志**(第 3 步,插主表后):`opp_id=新id`、`opKind=ROW_ADD`、`logType=ROW_CHANGE`、
`entityName="商机"`、`opDesc="由线索转入创建"`、`opSource="SYSTEM"`、`opTime=now`、`opUserId=cmd.ownerUserId()`。
4. **团队成员**(第 4 步,插主表后):`opportunity_id=新id`、`userId=cmd.ownerUserId()`、
`projectRole="project_role_01"`、`permission=READ_WRITE`、`deleteKey=0`、`userNameSnapshot` 从 cmd 若有(否则留空,A4/auth 侧补)。
5. **单测**扩 `OpportunityCreationPortImplTest`(Mockito):
- 命中模板 → 断言 `current_stage_id`/`stage_template_id`/`version` 落位为首节点/版本行。
- **无模板(resolveBindingVersion 返 null)→ 三字段空,商机仍建成功**(容忍空用例)。
- 断言写了 1 条初始 oplog(ROW_ADD/ROW_CHANGE/SYSTEM/「由线索转入创建」)。
- 断言插了 1 条团队成员(owner/project_role_01/READ_WRITE)。
- 现有幂等/客户子表/并发用例保持绿。
## 边界
- **事务内 vs 异步**:本 3 步全属票 02 簇 D「必须同步」清单,与主表同事务;通知/推送仍异步(不在本票)。
- **不改状态/阶段快照表**:`opportunity_status_history`/`opportunity_stage_history` 的初始快照是否要写,
票 02 簇 D 未列(只列 oplog)——本票**不写快照**,与簇 D 对齐;若后续需初始快照另开票。
- 不动 A4:`userNameSnapshot` 缺失容忍(auth/A4 侧回填),不为此建跨模块查询。

12
.scratch/opportunity-module/map.md

@ -18,7 +18,7 @@
| 票 | 主题 | 实现状态 | | 票 | 主题 | 实现状态 |
|---|---|---| |---|---|---|
| 01 | 领域词汇/CONTEXT | ✅ 已落 | | 01 | 领域词汇/CONTEXT | ✅ 已落 |
| 02 | lead→opp port 消费侧实现 | ⛔ 卡 A4(建商机核心逻辑需写关联客户主标记子表,需 customer_id) | | 02 | lead→opp port 消费侧实现 | ✅ 已落(`OpportunityCreationPortImpl`:唯一性预检 + 建商机 + customerId 非空建主要意向客户子表刷冗余)。**不再卡 A4**:走 customer_id 契约占位(见下「customer_id 契约占位」决策)——customerId 可空贯通(cmd/param 无 @NotNull 已核实),空则跳过建子表不倒退;唯一悬空 = 转商机弹窗「下拉选客户」的**数据源**(A4 未建,前端 mock/传 null),非商机侧实现缺口 |
| 03 | 主表 | ✅ 已落(`Opportunity` + Mapper) | | 03 | 主表 | ✅ 已落(`Opportunity` + Mapper) |
| 04 | 状态机 | ✅ 已落(`OpportunityTransition` + 历史快照 + convert port) | | 04 | 状态机 | ✅ 已落(`OpportunityTransition` + 历史快照 + convert port) |
| 05 | 公海规则 | ✅ 已落(crm-rule 实体/状态机 + Job + 匹配引擎 + 待发通知表) | | 05 | 公海规则 | ✅ 已落(crm-rule 实体/状态机 + Job + 匹配引擎 + 待发通知表) |
@ -31,8 +31,10 @@
| 12 | 商机侧新增入口关联线索校验 | ✅ 已落(source_lead_id 唯一性预检 + DB uk 并发防线,零 A4 依赖;原标「卡 A4」为误判已纠) | | 12 | 商机侧新增入口关联线索校验 | ✅ 已落(source_lead_id 唯一性预检 + DB uk 并发防线,零 A4 依赖;原标「卡 A4」为误判已纠) |
| 13 | 商机-客户关联子表 | ✅ 已落(实体 `OpportunityCustomer`+约束 + 转商机时建 `is_primary_intended=1` 主要意向客户子表 + 刷主表冗余;仅「下拉选客户」数据源卡 A4) | | 13 | 商机-客户关联子表 | ✅ 已落(实体 `OpportunityCustomer`+约束 + 转商机时建 `is_primary_intended=1` 主要意向客户子表 + 刷主表冗余;仅「下拉选客户」数据源卡 A4) |
| 14 | 自定义视图 + 内置视图族 | ✅ 已落(底座三表 team/focus/view_log + `TeamMemberPermission` 枚举;**内置六视图**在 crm-opportunity 枚举/ViewQuery 硬编码;**自定义视图 = crm-preference 平台能力**:`UserSavedView` 实体 + Mapper + `SavedView`/`SavedViewCondition` DTO + `SavedViewService` list/save/delete/setDefault + Impl(默认单值互斥) + Controller `/api/preference/view` + 16 单测;**商机侧接线已完成**:`SavedViewOperator` 六操作符枚举 + `OpportunitySavedViewFilter` 把 filter_json 条件翻译成 wrapper(字段池白名单=票10可筛选列,未知字段/操作符跳过)+ PageParam.savedViewId + ViewQueryImpl 叠加;读默认视图=前端调 /view/list 取 isDefault。144 全绿) | | 14 | 自定义视图 + 内置视图族 | ✅ 已落(底座三表 team/focus/view_log + `TeamMemberPermission` 枚举;**内置六视图**在 crm-opportunity 枚举/ViewQuery 硬编码;**自定义视图 = crm-preference 平台能力**:`UserSavedView` 实体 + Mapper + `SavedView`/`SavedViewCondition` DTO + `SavedViewService` list/save/delete/setDefault + Impl(默认单值互斥) + Controller `/api/preference/view` + 16 单测;**商机侧接线已完成**:`SavedViewOperator` 六操作符枚举 + `OpportunitySavedViewFilter` 把 filter_json 条件翻译成 wrapper(字段池白名单=票10可筛选列,未知字段/操作符跳过)+ PageParam.savedViewId + ViewQueryImpl 叠加;读默认视图=前端调 /view/list 取 isDefault。144 全绿) |
| 15 | 全局默认阶段模板兜底 seed | ✅ 已落(crm-rule `StageTemplateInitializer`:植入 1 头 5 节点发布中默认模板,客户圈定→关系摸排→资料采集→方案卡→已转项目;对称方案卡兜底范式;零 A4/零字典改动)——见 `issues/15` |
| 16 | 票 02 转商机同步事务补齐(阶段落位+初始日志+团队成员) | 🔨 开发中(票 02 簇 D 同步事务 5 步现差 3 步:阶段落位(resolveBindingVersion 首节点,无模板容忍空走法 A)+ 初始操作日志(ROW_ADD「由线索转入创建」)+ 团队成员(领取人=商机负责人 project_role_01/READ_WRITE);零 A4 依赖)——见 `issues/16`,blocked by 15 |
**前沿小结**:商机模块**结构层已全部落地**(主表 + 状态机 + 三族规则 + 六子表 + 视图数据源三表 team/focus/view_log)。剩余:① **票 10 查询层 + 票 14 视图组织/自定义视图 = 可独立开工的读侧大票(不卡 A4)** → 已全部落地 ✅;② **票 11/12/13 转商机写入口(走法甲)已落地** ✅:实现 `OpportunityCreationPortImpl`(唯一性预检 + 建商机 + customerId 非空建主要意向客户子表刷冗余);crm-opportunity 新依赖 crm-lead(**ADR-0029 技术债**:下游反向依赖上游,埋循环雷,同库单体阶段可接受);**仅剩卡 A4 的**:转商机弹窗「下拉选客户」的**数据源**(customerId 现可空占位,A4 就绪后收紧必填)+ 票 02 port 消费侧。**省市地区:已修**(原“省市拆分 TODO”实为命令契约丢字段 bug,CreateOpportunityCmd 已补 provinceCode+cityCode 两级,删同填占位)。 **前沿小结**:商机模块**结构层已全部落地**(主表 + 状态机 + 三族规则 + 六子表 + 视图数据源三表 team/focus/view_log)。剩余:① **票 10 查询层 + 票 14 视图组织/自定义视图 = 可独立开工的读侧大票(不卡 A4)** → 已全部落地 ✅;② **票 11/12/13 转商机写入口(走法甲)已落地** ✅:实现 `OpportunityCreationPortImpl`(唯一性预检 + 建商机 + customerId 非空建主要意向客户子表刷冗余);crm-opportunity 新依赖 crm-lead(**ADR-0029 技术债**:下游反向依赖上游,埋循环雷,同库单体阶段可接受);**仅剩卡 A4 的**:转商机弹窗「下拉选客户」的**数据源**(customerId 现可空占位,A4 就绪后收紧必填)。**票 02 port 消费侧已实现**(`OpportunityCreationPortImpl`),不再卡 A4;**商机全面开发不阻塞** — 详见下「customer_id 契约占位」决策(写入口按 id 入参 + 名快照 + 不做跨模块存在性校验)。**省市地区:已修**(原“省市拆分 TODO”实为命令契约丢字段 bug,CreateOpportunityCmd 已补 provinceCode+cityCode 两级,删同填占位)。
### 交付物清单(对齐线索地图的阅读顺序) ### 交付物清单(对齐线索地图的阅读顺序)
@ -93,6 +95,12 @@
- **自定义检索推广到其他菜单(追踪锤,非物当前票)** — 平台能力已就绪、菜单无关(`user_saved_view` + `SavedViewService` + `/api/preference/view``scope_key` 分组,crm-preference 不校验字段)。产品拍板“每个菜单都应支持这种能力”,但**不自己建空 ticket**。**新菜单接入三步**(手册,已回写 crm-preference/CONTEXT.md 责任边界段):① 约定一个稳定 `scope_key`(如 `lead.list` / `customer.list`);② 在**本菜单模块**写一个 `XxxSavedViewFilter`(filter_json→本表查询,字段池白名单 = 本菜单可筛选列,参 `OpportunitySavedViewFilter`)+ 给本菜单 PageParam 加 `savedViewId` 接线;③ 前端复用 `/api/preference/view/*`,无需改 crm-preference。**建带验收 ticket 的时机** = 有明确的第二个菜单要上时(那时验收 = 该菜单 saved-view 端到端可用)。**不提前抽共享翻译器缝**(rule of three:目前只 1 个真实样本=商机,第三个菜单出现时再抽才知道缝在哪)。 - **自定义检索推广到其他菜单(追踪锤,非物当前票)** — 平台能力已就绪、菜单无关(`user_saved_view` + `SavedViewService` + `/api/preference/view``scope_key` 分组,crm-preference 不校验字段)。产品拍板“每个菜单都应支持这种能力”,但**不自己建空 ticket**。**新菜单接入三步**(手册,已回写 crm-preference/CONTEXT.md 责任边界段):① 约定一个稳定 `scope_key`(如 `lead.list` / `customer.list`);② 在**本菜单模块**写一个 `XxxSavedViewFilter`(filter_json→本表查询,字段池白名单 = 本菜单可筛选列,参 `OpportunitySavedViewFilter`)+ 给本菜单 PageParam 加 `savedViewId` 接线;③ 前端复用 `/api/preference/view/*`,无需改 crm-preference。**建带验收 ticket 的时机** = 有明确的第二个菜单要上时(那时验收 = 该菜单 saved-view 端到端可用)。**不提前抽共享翻译器缝**(rule of three:目前只 1 个真实样本=商机,第三个菜单出现时再抽才知道缝在哪)。
- **客户关联子表的形态** — 已钉死为 **票 13**(见下 Decisions);仅“下拉选客户”数据源 + 客户联系人是否本子表冗余仍挂 **A4 客户模块(未建)** 边界(票 11 占位)。 - **客户关联子表的形态** — 已钉死为 **票 13**(见下 Decisions);仅“下拉选客户”数据源 + 客户联系人是否本子表冗余仍挂 **A4 客户模块(未建)** 边界(票 11 占位)。
- **customer_id 契约占位(跨商机写入口的统一约定,20260825 拍板)** — 产品定:**商机模块先全面开发完,后做 A4 客户模块**。为不阻塞,商机侧**所有碰客户的写入口统一按下列三条写**(已核实商机现有代码均符合,下文未写入口建时照此执行):
1. **存形态 = 引用 id + 名快照并存**:凡落客户处(`opportunity_customer.customer_id`+`customer_name_snapshot`、主表冗余 `primary_customer_id`+`primary_customer_name_snapshot`)均存 **id(bigint 引用)+ 名快照**。名快照让列表/详情免 join 就能回显客户名,A4 建不建都能跑。
2. **写入口按 `customer_id` 入参,不做跨模块存在性校验**:前端传 id 进来,商机侧只做**本地校验**(如“是否本商机已关联”、一客户一卡 UNIQUE),**不**去 A4 查“这个 id 是否真存在”;A4 就绪后再加存在性校验(留 TODO)。方案卡挂客户已如此:只查本商机 `opportunity_customer` 子表,不碰 A4。
3. **customerId 可空占位,不强制非空**:`CreateOpportunityCmd.customerId` / `ConvertOpportunityParam.customerId` 均无 `@NotNull`(已核实);空时跳过建关联子表/不刷冗余,不倒退现有自由文本转商机;A4 下拉就绪后收紧为必填(票 11 终态)。
- **唯一真空洞 = 客户“下拉数据源”**:`customer_id` 从哪来靠上游传入,商机侧全程不需 A4。商机全面开发时这个下拉**前端 mock 几个假 customer_id 或传 null**;功能能全开,只“选真客户”这一步是空的。商机侧不为此建任何 A4 占位/mock 代码(YAGNI,A4 那张图自己建时提供)。
- **商机批量操作与统计接口** — 对称线索 G1–G5(批量领取/分配/关注、列表统计卡片)。等主表 + 四视图定稿后再切成票。 - **商机批量操作与统计接口** — 对称线索 G1–G5(批量领取/分配/关注、列表统计卡片)。等主表 + 四视图定稿后再切成票。
- **商机相关字典项清单**(A7-3-2-6)— ✅ **14 组已种(crm-dict `DictDataInitializer`,产品录入 20260825)**:`opp_source` / `opp_type` / `bid_form` / `result_tag` / `design_config` / `pause_reason` / `pool_reason` / `project_role` / `close_reason` / `locality_type` 项目属地 / `follow_way` 跟进方式 / `survey_seq` 勘察次数 / `apply_way` 申请方式 / `customer_role` 客户角色(分组 7→22、item 97→168,45 全绿)。⚠ **`industry` 行业树形化改造已立票**(20260825 产品定:行业改为全系统通用两级树、商机选到一级;需给 `DictItem``parent_id`——见 `.scratch/data-dictionary/issues/09-hierarchical-item.md`,待 grill 拍板不变式后实现)。**缺值已清零**(attachment_type 已补)。 - **商机相关字典项清单**(A7-3-2-6)— ✅ **14 组已种(crm-dict `DictDataInitializer`,产品录入 20260825)**:`opp_source` / `opp_type` / `bid_form` / `result_tag` / `design_config` / `pause_reason` / `pool_reason` / `project_role` / `close_reason` / `locality_type` 项目属地 / `follow_way` 跟进方式 / `survey_seq` 勘察次数 / `apply_way` 申请方式 / `customer_role` 客户角色(分组 7→22、item 97→168,45 全绿)。⚠ **`industry` 行业树形化改造已立票**(20260825 产品定:行业改为全系统通用两级树、商机选到一级;需给 `DictItem``parent_id`——见 `.scratch/data-dictionary/issues/09-hierarchical-item.md`,待 grill 拍板不变式后实现)。**缺值已清零**(attachment_type 已补)。
- **方案预算 `scheme_budget`**(方案卡字段)— 商机列表「方案预算」列 = 查主要意向客户(票13)那张方案卡的 budget,主表不建。字段规格归票07,取值锚点归票13。 - **方案预算 `scheme_budget`**(方案卡字段)— 商机列表「方案预算」列 = 查主要意向客户(票13)那张方案卡的 budget,主表不建。字段规格归票07,取值锚点归票13。

Loading…
Cancel
Save