From 33efce85f989e78796bb06cdc8da5072e47a2334 Mon Sep 17 00:00:00 2001 From: luoweijian <1329394916@qq.com> Date: Tue, 25 Aug 2026 15:31:23 +0800 Subject: [PATCH] =?UTF-8?q?docs(dict):=20=E7=AB=8B=E7=A5=A8=2009=20?= =?UTF-8?q?=E5=AD=97=E5=85=B8=E9=A1=B9=E4=B8=A4=E7=BA=A7=E6=A0=91=E5=BD=A2?= =?UTF-8?q?=E8=83=BD=E5=8A=9B=20(DictItem=20=E5=8A=A0=20parent=5Fid)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 行业改全系统通用两级树 (产品 20260825), 商机选一级, 需扩 crm-dict 模型 - 票列 7 个待 grill 不变式: 层级上限/父子约束/停用删除级联/树形查询API/存量安全/客户侧选几级/商机落库形态 - 附产品给定行业全树 (9 一级 + 全二级) 待拍板后种 - opportunity map.md 同步: industry 挂起改为已立票 --- .../issues/09-hierarchical-item.md | 66 +++++++++++++++++++ .scratch/opportunity-module/map.md | 12 ++-- 2 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 .scratch/data-dictionary/issues/09-hierarchical-item.md diff --git a/.scratch/data-dictionary/issues/09-hierarchical-item.md b/.scratch/data-dictionary/issues/09-hierarchical-item.md new file mode 100644 index 0000000..8606549 --- /dev/null +++ b/.scratch/data-dictionary/issues/09-hierarchical-item.md @@ -0,0 +1,66 @@ +# 09 — 字典项两级树形能力(DictItem 加 parent_id) + +**Type:** grilling → task(本票先 grill 拍板不变式,再落地) + +**Status:** needs-triage + +**Blocked by:** 04 + +## Problem Statement + +现有 `DictItem` 是**扁平模型**(`group_id + code + name + value + sort_no + status`,无 `parent_id`)。ADR-0015 spec 开篇即写「两级结构(分组 → 字典项)」——即层级止步于「分组含一批平铺项」,**字典项自身之间没有父子关系**。 + +商机模块(20260825 产品拍板)要求「行业」字典是**多级树形**:一级 9 个大类(政府机关 / 政法国防 / 教育 / 医疗健康 / 金融 / 交通 / 能源与制造 / 文化旅游 / 园区与基建),每个大类下挂若干二级细分(如 政府机关 → 人大政协 / 应急局 / 烟草局 / …)。且明确: + +- **全系统通用**:客户、商机共用同一套 `industry`(不单建 `industry_code`)——顺带解掉此前挂起的「路 A/B」之争,选路 A 的反面:**复用并覆盖现有 `industry`**。 +- **商机只选到一级**:商机主表 `industry_code` 字段存一级 code。 +- 客户侧是否选到二级 = **待拍板**(见 Q6)。 + +现有 `industry` 分组种子是 8 项旧口径(制造业 / 信息技术 / 金融 / 医疗健康 / 教育 / 零售 / 房地产 / 其他),与新树完全不同口径,**覆盖会造成客户存量数据 code 悬空**(见 Q5)。 + +## 为什么必须先 grill(不能直接种) + +给 `DictItem` 加 `parent_id` 是 **crm-dict 基础模型扩展**,影响面远超「种一批值」: + +- 实体 + DDL(现有行 parent_id=null=顶层) +- `DictItemDTO`(加 parentId / 可能 children 树形出参) +- `DictItemServiceImpl` 校验不变式(见下) +- `DictQueryService` 新增树形查询 + Caffeine 缓存结构调整 +- 现有 45 个 crm-dict 测试 + 全系统扁平消费方(lead / customer / opportunity)回归 +- 前端字典管理页树形录入/展示契约 + +砸坏一个被全系统依赖的模块风险太高,故按仓库 issue-tracker 规矩先立票 grill。 + +## 待拍板不变式(grill 逐条) + +- **Q1 层级上限**:两级封顶?还是允许任意深度树?(商机只需两级,但字典是通用能力——限死两级更简单、更可控;开放多级更通用但校验/查询复杂。) +- **Q2 父子约束**:二级项的 `parent_id` 必须指向**同分组内**的另一项;禁止跨组;禁止自引用;禁止循环。一级项 `parent_id = null`。是否要求「有子项的项不能自己再当别人的子项」(即强制恰好两层)? +- **Q3 停用/删除级联**: + - 停用一级项时,其二级子项是否连带不可选(对齐分组停用→整组不可选的既有语义)? + - 删除一级项:若其下有存活子项,**拦截**(提示先删子项)还是**级联软删**?(建议拦截,与「删分组需组内无项」的既有保守语义一致。) +- **Q4 树形查询 API**:`DictQueryService` 加什么?`listTree(groupCode)` 返回嵌套?`listChildren(groupCode, parentCode)`?`getItem` 扁平接口保持不变(向后兼容所有现有消费方)。缓存怎么建(现 Caffeine 按 groupCode→扁平 list,加树需另建 key 还是复用 list 内存组树)。 +- **Q5 存量数据安全**:现有 `industry` 8 项(manufacturing/it/finance/…)**是否已有客户业务数据引用**?覆盖成新树前必须确认——若有存量,旧 code 会悬空(客户列表行业列显示空)。需要 DBA/产品确认,或提供 code 映射迁移。 +- **Q6 客户侧选到几级**:商机存一级 code 已定。客户模块 `industry` 字段是选一级还是可选到二级?决定二级项是否只是「商机的分类维度」还是「客户可直接选的值」。 +- **Q7 商机存一级的落库形态**:商机 `industry_code` 存一级项 code(如 `industry_gov`)。二级仅作字典维护/展示层级,不进商机主表?确认。 + +## 实现范围(Q 拍板后) + +1. `DictItem` 加 `parentId`(nullable,顶层为 null);DDL 自动加列。 +2. `DictItemDTO` 加 `parentId`(+ 视 Q4 决定是否加 children)。 +3. `DictItemServiceImpl.saveItem` 增校验:父项存在且同组、层级上限(Q1)、无自引用/循环(Q2);`doDeleteItem` 增级联策略(Q3)。 +4. `DictQueryService` 增树形查询(Q4)+ 缓存。 +5. 种子:`industry` 分组按新树重种(9 一级 + 全部二级),处理存量覆盖(Q5)。 +6. 测试:树形 CRUD / 父子约束 / 级联 / 树形查询;回归现有 45 扁平测试。 + +## 附:产品给定的行业全树(20260825,待 Q 拍板后种) + +一级(9)→ 二级: +- 政府机关:人大政协 / 应急局 / 烟草局 / 疾控中心 / 大数据中心 / 税务局 / 教育局 / 民政局 / 人社局 / 财政局 / 国土局 / 水利局 / 林业局 / 商务局 / 文体旅局 / 气象局 / 退役军人事务部 / 融媒体中心 / 人防办 +- 政法国防:公安 / 检察院 / 法院 / 司法厅 / 监狱 / 海关-边检站 / 武警 / 特警 / 消防 +- 教育:普教 / 职校 / 党校 +- 医疗健康:医院 / 康养中心 +- 金融:银行 / 保险公司 +- 交通:轨道交通 / 机场 / 高铁 / 高速公路 +- 能源与制造:石化-能源-矿业 / 发电站-核电站 / 电力实训基地 / 工厂 +- 文化旅游:文旅 / 酒店 / 展馆-博物馆 +- 园区与基建:智慧园区 / 集团大楼 / 商业中心 / 智算中心 / 低空飞行基地 diff --git a/.scratch/opportunity-module/map.md b/.scratch/opportunity-module/map.md index 057ff2b..4737740 100644 --- a/.scratch/opportunity-module/map.md +++ b/.scratch/opportunity-module/map.md @@ -27,12 +27,12 @@ | 08 | 现场勘察 | ✅ 已落(`OpportunitySiteSurvey` + Mapper,结构) | | 09 | 子表族(跟进/附件/工作计划/操作日志) | ✅ 已落(四实体 + Mapper + 3 枚举,结构) | | 10 | 四视图查询口径 | ✅ 已落(底座三表 + ViewType/PageParam/DTO + ViewQuery 六视图 filter + 分页 + 计算列节点停留/最近跟进/方案卡状态/预算 + RECENT view_log 排序 + followed + Controller `/api/opportunity/page`;**读侧回显已补全**:字典名 opp_source/industry_code(PRD §10 契约)+ 部门名 owner_dept_id + 区划名 province/city + 当前/下一节点名(依 crm-rule IOpportunityStageTemplateService,customNodeName 优先/opp_stage 字典名兜底);132 全绿) | -| 11 | createCmd customerId | ⛔ 卡 A4 | -| 12 | 商机侧新增入口关联线索校验 | ⛔ 实质卡(建商机入口 Service 未建=依赖票 02/11 → A4) | -| 13 | 商机-客户关联子表 | ⚠ 部分(实体 `OpportunityCustomer`+约束已落;读写入口/下拉选客户卡 A4) | +| 11 | createCmd customerId | ✅ 已落(走法甲:customerId 可空占位,待 A4 下拉就绪收紧必填) | +| 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 全绿) | -**前沿小结**:商机模块**结构层已全部落地**(主表 + 状态机 + 三族规则 + 六子表 + 视图数据源三表 team/focus/view_log)。剩余:① **票 10 查询层 + 票 14 视图组织/自定义视图 = 可独立开工的读侧大票(不卡 A4)** → 建议每票清上下文单独 `/implement`(底座表已备);② **票 02/11/12/13 读写入口 = 卡 A4 客户模块**(建商机核心逻辑需写关联客户主标记子表 customer_id),挂起等 A4。 +**前沿小结**:商机模块**结构层已全部落地**(主表 + 状态机 + 三族规则 + 六子表 + 视图数据源三表 team/focus/view_log)。剩余:① **票 10 查询层 + 票 14 视图组织/自定义视图 = 可独立开工的读侧大票(不卡 A4)** → 已全部落地 ✅;② **票 11/12/13 转商机写入口(走法甲)已落地** ✅:实现 `OpportunityCreationPortImpl`(唯一性预检 + 建商机 + customerId 非空建主要意向客户子表刷冗余);crm-opportunity 新依赖 crm-lead(**ADR-0029 技术债**:下游反向依赖上游,埋循环雷,同库单体阶段可接受);**仅剩卡 A4 的**:转商机弹窗「下拉选客户」的**数据源**(customerId 现可空占位,A4 就绪后收紧必填)+ 票 02 port 消费侧。**遗留 TODO**:省市拆分(cmd 单 regionCode → 主表 province/city 两列 not null,暂同填占位,待专票)。 ### 交付物清单(对齐线索地图的阅读顺序) @@ -58,7 +58,7 @@ ## Decisions so far -- **⚠ 行业字典口径待产品拍板(20260825 挂起,未落)** — PRD §10 商机主表 `industry_code`「行业」字段需字典,但存在冲突:crm-dict 已有 `industry` 分组(**客户模块在用**,8 项:制造业/信息技术/金融/医疗健康/教育/零售/房地产/其他,宽泛行业大类);产品 20260825 给的 26 项(普教/高校/党校/公安/检察院/法院…)实为**细分客户类型/项目场景**,与现有 8 项两套口径,不可简单复用/覆盖。**两条待选路**:**路 A**(建议)= 商机单建 `industry_code` 分组种 26 项,客户 `industry` 不动,回显代码查 `industry_code` 天然对上;**路 B** = 26 项覆盖进 `industry` 且商机改查 `industry`,风险=客户存量数据 code 悬空。**另一层疑点**:26 项名义「行业」实为「客户类型」,字段命名与取值口径可能本身错位——需产品确认「是否挂 `industry_code`」。**结论:不擅自落,等产品确认路 A/B + 挂载字段后再种。** 现状:`OpportunityViewQueryImpl.fillDictNames` 查 `industry_code`(分组暂不存在→行业名回显为空,getItem 优雅降级、不报错)。 +- **⚠ 行业字典口径待产品拍板(20260825 挂起,未落)** — PRD §10 商机主表 `industry_code`「行业」字段需字典,但存在冲突:crm-dict 已有 `industry` 分组(**客户模块在用**,8 项:制造业/信息技术/金融/医疗健康/教育/零售/房地产/其他,宽泛行业大类);产品 20260825 给的 26 项(普教/高校/党校/公安/检察院/法院…)实为**细分客户类型/项目场景**,与现有 8 项两套口径,不可简单复用/覆盖。**两条待选路**:**路 A**(建议)= 商机单建 `industry_code` 分组种 26 项,客户 `industry` 不动,回显代码查 `industry_code` 天然对上;**路 B** = 26 项覆盖进 `industry` 且商机改查 `industry`,风险=客户存量数据 code 悬空。**另一层疑点**:26 项名义「行业」实为「客户类型」,字段命名与取值口径可能本身错位——需产品确认「是否挂 `industry_code`」。**结论:不擅自落,等产品确认路 A/B + 挂载字段后再种。** - **状态/阶段、角色、流转动作、模块边界已定稿(票 01 resolved)** — 商机=独立聚合;两轴 `oppStatus`/`oppStage`;领取人+团队成员角色(按钮可见即可点、与领取人无关);流转动作 = 暂缓/取消暂缓/抛公海/关闭/重启/移交/转项目,**「中止」不存在**(原型活数据 V1.0-20260815 核实);商机来源与来源线索两回事。产出 `crm-opportunity/CONTEXT.md`。详见 `issues/01` + `research-close-pause-actions.md`。 @@ -94,7 +94,7 @@ - **客户关联子表的形态** — 已钉死为 **票 13**(见下 Decisions);仅“下拉选客户”数据源 + 客户联系人是否本子表冗余仍挂 **A4 客户模块(未建)** 边界(票 11 占位)。 - **商机批量操作与统计接口** — 对称线索 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→21、item 97→163,45 全绿)。⚠ **`industry_code` 行业挂起**(口径冲突待产品拍板,见 Decisions「行业字典口径」)。**仅剩 1 组缺值**:`attachment_type` 商机资料类型(票09 附件分类,产品未给值)。 +- **商机相关字典项清单**(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。 ## Out of scope