diff --git a/.scratch/opportunity-module/issues/15-default-stage-template-seed.md b/.scratch/opportunity-module/issues/15-default-stage-template-seed.md new file mode 100644 index 0000000..41aa9f1 --- /dev/null +++ b/.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`);本票只种「所有商机」默认兜底一条。 diff --git a/.scratch/opportunity-module/issues/16-conversion-txn-stage-oplog-team.md b/.scratch/opportunity-module/issues/16-conversion-txn-stage-oplog-team.md new file mode 100644 index 0000000..bfb793e --- /dev/null +++ b/.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 侧回填),不为此建跨模块查询。 diff --git a/.scratch/opportunity-module/map.md b/.scratch/opportunity-module/map.md index b10e362..c011c56 100644 --- a/.scratch/opportunity-module/map.md +++ b/.scratch/opportunity-module/map.md @@ -18,7 +18,7 @@ | 票 | 主题 | 实现状态 | |---|---|---| | 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) | | 04 | 状态机 | ✅ 已落(`OpportunityTransition` + 历史快照 + convert port) | | 05 | 公海规则 | ✅ 已落(crm-rule 实体/状态机 + Job + 匹配引擎 + 待发通知表) | @@ -31,8 +31,10 @@ | 12 | 商机侧新增入口关联线索校验 | ✅ 已落(source_lead_id 唯一性预检 + DB uk 并发防线,零 A4 依赖;原标「卡 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 全绿) | +| 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 个真实样本=商机,第三个菜单出现时再抽才知道缝在哪)。 - **客户关联子表的形态** — 已钉死为 **票 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(批量领取/分配/关注、列表统计卡片)。等主表 + 四视图定稿后再切成票。 - **商机相关字典项清单**(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。