# crm-preference 列偏好 scope 模型与最小契约 Type: grilling Status: resolved ## Question 定义 `crm-preference` 平台模块的核心抽象——用户级列表列偏好(显隐 + 拖拽排序),限定在"线索够用的最小通用深度",不追求全平台完美通用。 需在本 ticket 定清: 1. **scope 模型**:偏好按什么键存储?原型要求"公海/我的线索/我的关注共用一组,线索管理独立一组"。scope 是 `(userId, scopeKey)`?scopeKey 由消费方(crm-lead)自定义字符串(如 `clue.publicShared` / `clue.manage`)?共用组如何用同一个 scopeKey 表达。 2. **列定义来源**:偏好只存"显隐 + 顺序",那"有哪些列可选"(字段池)由谁提供?crm-preference 只存用户选择(列 key 列表 + 顺序),字段池的权威定义留在消费方(crm-lead)——确认此边界。 3. **最小契约接口**:`getColumns(userId, scopeKey)` / `saveColumns(userId, scopeKey, orderedVisibleKeys)`——够不够?至少保留 1 列的校验放消费方还是 preference? 4. **存储形态**:一张 `user_list_preference(user_id, scope_key, columns_json, updated_at)`? 5. **通用深度边界**:明确本次**不做**什么(如:不做跨用户默认模板、不做管理员强制列、不做多租户)——记入 fog/out-of-scope,避免通用性绑架线索进度。 产出:scope 模型 + 契约接口签名 + 存储表 + 通用深度边界,写入 ## Answer;术语进 `crm-preference/CONTEXT.md`,`CONTEXT-MAP.md` 加行。 ## Decisions(grill 过程中定稿) ### [scope 粒度] 每菜单独立(产品口头确认,推翻原型"共用一套") - **原型说法**(散见 a2-1-1:364, a2-1-2:1796/1814/1961, a2-1-3:333/351, a2-1-4:406):公海/我的线索/我的关注三处共用一套字段配置,"一处修改全模块同步生效";线索管理独立一套。→ 共 2 个 scope。 - **产品确认(口头)**:每个菜单独立 → 公海、我的线索、我的关注、线索管理**各自一个 scope**,互不同步。共 4 个 scope(线索域内)。 - **权威**:以产品口头为准;实现层不再合并共享组。原型"同步生效"文案作废。 - **对 scope 模型的影响**:`scopeKey` 一菜单一 key,命名如 `lead.publicPool` / `lead.myLead` / `lead.myFollow` / `lead.manage`。业务方自定字符串(Q1=a 未变)。 ## Decisions(grill 定稿) ### [scope 粒度] 每菜单一个 scope(产品确认,推翻原型"共用一套") - **原型说法**(7 处,散见 a2-1-1:364, a2-1-2:1796/1814/1961, a2-1-3:333/351, a2-1-4:406):公海/我的线索/我的关注三处共用一套字段配置,"一处修改全模块同步生效",线索管理独立。 - **产品确认**:每菜单独立,共 4 个 scope(线索域内):公海、我的线索、我的关注、线索管理,互不同步。原型"共用/同步生效"文案作废,UX 侧同步修订。 - **实现含义**:`scopeKey` 一菜单一 key,命名由业务方自定字符串(如 `lead.publicPool` / `lead.myLead` / `lead.myFollow` / `lead.manage`),crm-preference 无需感知语义。 ### [Q1 命名] scopeKey = 业务方约定的稳定 code - 形态:不可变字符串,业务方(crm-lead)定义并维护;如 `lead.public_pool` / `lead.my_lead` / `lead.my_follow` / `lead.manage`。 - 拒用路由字符串(改路由会丢偏好);拒用菜单表主键(换库/改主键会丢偏好)。crm-preference 完全不感知语义。 ### [Q2 字段池归属] 业务方(crm-lead)提供 - crm-preference 只存"用户勾了哪些 key、什么顺序"两份纯列表。 - "有哪些字段可选、字段中文名、默认显隐、每 scope 的字段池差异"全部由 crm-lead 提供元数据,前端向 crm-lead 拉取。 - 校验放业务方:至少保留 1 列、字段 key 合法性、未知 key 兜底策略均由 crm-lead 决定;crm-preference 不做业务校验。 ### [Q3 存储形态] 一张表两个 JSON 列 - 表结构:`user_column_preference(user_id, scope_key, visible_keys json, column_order json, updated_at)` - 主键/唯一约束:`(user_id, scope_key)` - 索引:`(user_id, scope_key)` 即唯一约束天然索引,读侧一次命中。 - 读一次拿全(显隐+顺序),写侧可分别更新任一列。 ### [Q4 保存时机] 一律点【保存】才落库;拖拽临时顺序仅前端本地 - **推翻原型"拖拽实时保存"文案**(a2-1-2:1813 "配置缓存至当前账号"、a2-1-2:1843 "拖拽表头、列表设置修改后实时保存"),UX 侧同步修订原型。 - 交互:表头拖拽后顺序仅存前端内存/localStorage,关页面/刷新即丢;用户在【列表设置】弹窗内明确点【保存】才把显隐+顺序一起落库;点【取消】或未保存关闭页面则本轮变更全部作废。 - 契约含义:只需**一个保存接口**(同时接收 visibleKeys 与 columnOrder),无需单独的"保存顺序"入口。 ### [Q5 字段池粒度] 每 scope 一套字段池,由 crm-lead 分别决定 - 4 个 scope 各自拥有独立字段池;线索管理字段池含管理员视角字段(关注/领取人数、销售人员等),公海不含"反馈"三件套等。 - 字段池差异由 crm-lead 在 ticket 06(线索字段模型)中定义;crm-preference 契约不为字段池负责。 ## Answer ### 一、scope 模型 - `scopeKey`:不可变字符串,业务方约定;每菜单一个;线索域 4 个:`lead.public_pool` / `lead.my_lead` / `lead.my_follow` / `lead.manage`。互不同步。 - `(userId, scopeKey)` 为唯一键——一个用户在一个 scope 下有且仅有一套偏好。 ### 二、职责边界(决定通用深度) | 关注点 | 归属 | |---|---| | scopeKey 命名 | 业务方 crm-lead | | 字段池元数据(有哪些字段、中文名、默认显隐) | 业务方 crm-lead | | 每 scope 的字段池差异 | 业务方 crm-lead | | 至少保留 1 列等业务校验 | 业务方 crm-lead | | 未知 key 的兜底策略 | 业务方 crm-lead | | **存 "用户勾了哪些、什么顺序"** | **crm-preference(本模块唯一职责)** | | 保存 UX(何时落库、拖拽临时态存放) | 前端 + 业务方 | ### 三、存储表 ``` user_column_preference ├─ id bigint PK ├─ user_id bigint NOT NULL ├─ scope_key varchar(64) NOT NULL -- 业务方约定的稳定 code ├─ visible_keys json NOT NULL -- 用户勾选的字段 key 列表(有序或无序均可,语义为"显示") ├─ column_order json NOT NULL -- 用户拖拽后的字段 key 顺序列表 ├─ updated_at datetime └─ UNIQUE (user_id, scope_key) ``` - 无 create_by / update_by(用户自己的偏好,author 恒为 user_id)。 - 物理删除即可(用户偏好非业务凭证);亦可继承 BaseEntity 走软删,视 crm-preference 模块基线定。 ### 四、最小契约(接口签名,crm-preference 对外) ```java public interface ColumnPreferenceService { /** 读某用户在某 scope 的偏好;无记录返回 empty(业务方决定回退默认) */ Optional get(Long userId, String scopeKey); /** 保存某用户在某 scope 的偏好;upsert 语义;一次写入显隐+顺序 */ void save(Long userId, String scopeKey, ColumnPreference preference); } public record ColumnPreference(List visibleKeys, List columnOrder) {} ``` - **只有两个方法**。无 delete(覆盖写等价重置)、无 list、无 admin API。 - 业务方在自身 service 拼装"字段池 × 用户偏好 → 最终展示列"。 ### 五、通用深度边界(本次明确不做) - 不做跨用户默认模板(业务方自己决定"未配置时给哪套默认")。 - 不做管理员强制列 / 锁定列(业务方自己在字段池元数据里标)。 - 不做多租户 scope 隔离(业务方在 scopeKey 里自行加租户前缀,如 `t1.lead.my_lead`)。 - 不做偏好版本 / 历史(覆盖写即可)。 - 不做字段池注册表(字段池权威在业务方)。 - 不做 scopeKey 注册表 / 校验(业务方自证)。 ### 六、需在 CONTEXT 中登记 - 新建 `crm-preference/CONTEXT.md`:术语(scopeKey / ColumnPreference / 字段池归属)+ 上述职责边界表 + 通用深度边界清单。 - `CONTEXT-MAP.md` 加行:`crm-preference` = 用户级 UI 偏好(当前仅列显隐+顺序)。 ### 七、原型待 UX 修订 - 原型 7 处"共用一套/同步生效"文案作废(a2-1-1:364, a2-1-2:1796/1814/1961, a2-1-3:333/351, a2-1-4:406)。 - 原型 2 处"拖拽实时保存"文案作废(a2-1-2:1813, a2-1-2:1843)。 ## Status transition open → claimed → **resolved**(本轮所有决策点已定,可交付;实施留给 ticket 06 定字段池、crm-preference 模块基线落表)。