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.
 
 
 
 
 

8.7 KiB

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.mdCONTEXT-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 对外)

public interface ColumnPreferenceService {
    /** 读某用户在某 scope 的偏好;无记录返回 empty(业务方决定回退默认) */
    Optional<ColumnPreference> get(Long userId, String scopeKey);

    /** 保存某用户在某 scope 的偏好;upsert 语义;一次写入显隐+顺序 */
    void save(Long userId, String scopeKey, ColumnPreference preference);
}

public record ColumnPreference(List<String> visibleKeys, List<String> 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 模块基线落表)。