# 10 — 客户规则族「超期未跟进提醒」(crm-rule 全局单例) Type: research Status: resolved Blocked by: ## 问题 在 `crm-rule` 落客户规则族第三个 tab(线索/商机/客户并列),本图**只做「超期未跟进提醒」**一项,**全局单例配置**(不套 V-CONFIG 版本化,chart D4)。「客户查重设置」不做(chart D6)。 ## 一手证据(蓝湖缓存 txt) - `a7-3-3-1_客户管理设置_超期未跟进提醒_.txt` — 全部 spec 都在这里 - `a7-3-3-2_客户管理设置_客户查重设置_.txt` — **划出不读**(查重规则写死在新增流程,票 04) ## 已知事实(原型明文) - **全局单例**:非按阶段/按部门分别配,仅后台管理面板可见。 - **顶部总开关**:控制整个超期未跟进提醒功能启用/禁用。 - **两配置项**: - **首次触发时间**:客户超过 N 天未跟进→触发首次提醒(默认 30 天);含独立启用开关。 - **二次提醒间隔时间**:首次后每隔 M 天再提醒(默认 7 天);含独立启用开关。 - **提醒推送渠道**:钉钉推送 + 应用内通知(对称商机票 05 的通知落点)。 - **交互**:保存/取消(取消恢复上次保存的配置)。 ## 待拍板点 1. **数据模型**: - **推荐 A(单行配置表)** `customer_reminder_rule`(id=1 fixed / master_enabled / first_trigger_enabled / first_trigger_days / second_interval_enabled / second_interval_days / updater / update_time)。 - **推荐 B(键值配置)** 复用某个 platform_config 表按 key 存。 - 我倾向 **A**(单行表,与商机 V-CONFIG 表族对称位置摆,只是不做版本,操作更直观)。 2. **触发口径**:「超过 N 天未跟进」=`now - last_valid_follow_time ≥ N`;`last_valid_follow_time` = 客户下最新一条 `customer_follow.create_time`(或字段编辑时间?→ 推荐**仅跟进记录**驱动,字段编辑不算——对齐商机票 05 的 last_valid_follow_time 但商机含字段编辑;这里更保守)。 3. **计时锚点写入**:`customer.last_valid_follow_time` 冗余到主表(新写跟进时刷新);对称商机主表 `last_valid_follow_time`。→ **回填票 02** 加此字段。 4. **调度**:定时任务(Quartz / Spring Schedule)每天跑一次;命中的客户写「待发通知」表让通知模块消费(对称商机票 05 的 remind 待发通知)。 5. **通知去重**:一个客户当天只触发一次(首次或二次,取其一)。 6. **无 owner 的客户**(在公海)**不参与**超期提醒(对谁提醒?→ 无接收人)。 7. **禁用后**:定时任务扫到总开关=禁用 → 跳过整轮;启用后从下一轮开始。 8. **规则生效时机**:chart Q4-补 用户特意强调「规则在客户新增时生效」——我的解读是**新增客户时就把 `last_valid_follow_time` 初始化为 create_time**(客户创建后开始计时,30 天没跟进就首次提醒)。**待和用户确认**这个解读——见 grill 挂号。 ## Answer **Status: resolved** ### A. 结论速览 客户「超期未跟进提醒」= **crm-rule 全局单例配置 + crm-customer 待发通知 + 调度 Job**,三层对称商机「回收提醒」范式,但配置形态是**单例**(不套 V-CONFIG 版本化,chart D4)。「客户查重设置」不做(chart D6)。 | 层 | 落点 | 对称对象 | |---|---|---| | 配置 | `crm-rule` `customer_reminder_rule`(单行表,id 固定=1) | `opportunity_pool_rule`(但非版本化) | | 待发通知 | `crm-customer` `customer_pending_notice` | `opportunity_pending_notice` | | 调度 | `crm-customer` `CustomerReminderJob`(@Scheduled) | `OpportunityMaintenanceJob` | --- ### B. 配置表 `customer_reminder_rule`(crm-rule) **全局单例**:单行表,`id` 固定为 1,种子初始化器 `CustomerReminderRuleInitializer` 植入默认行(对称 `StageTemplateInitializer` 兜底范式)。无版本号、无状态机、无 rule_code。 | Java 字段 | 列名 | 类型 | 默认 | 说明 | |---|---|---|---|---| | id | id | bigint | 固定 1 | 单例行主键 | | masterEnabled | master_enabled | tinyint | 1 | 总开关:1启用 0禁用(禁用后 Job 整轮跳过) | | firstTriggerEnabled | first_trigger_enabled | tinyint | 1 | 首次触发独立开关 | | firstTriggerDays | first_trigger_days | int | 30 | 首次触发天数(>0) | | secondIntervalEnabled | second_interval_enabled | tinyint | 1 | 二次提醒独立开关 | | secondIntervalDays | second_interval_days | int | 7 | 二次提醒间隔天数(>0) | | updateBy / updateTime | update_by / update_time | — | — | BaseEntity 继承 | > ⚠ **孤立单大写字母检查**(AGENTS.md):`masterEnabled`→`master_enabled`、`firstTriggerDays`→`first_trigger_days`、`secondIntervalDays`→`second_interval_days`——均为「完整词 + 完整词」,**无需 `@Column(name=...)`**。 **校验规则**(服务层,对称商机规则): - `firstTriggerDays > 0`、`secondIntervalDays > 0`。 - 保存 = 全量覆盖(无草稿/发布态,保存即生效)。 - 只后台管理面板可见(RBAC 功能权限 `customer.reminder.rule`)。 **契约**: ``` GET /api/rule/customer/reminder → 读当前配置(单行) PUT /api/rule/customer/reminder → 保存配置(全量覆盖,记录操作日志) ``` --- ### C. 触发口径(只前进、确定性提醒链) **计时锚点** = `customer.last_valid_follow_time`(票 02 已建;新建客户时初始化为 `create_time`,每次写跟进记录刷新——票 07)。 **提醒链**(确定性,可从锚点直接推导,无需存「已发首次」状态): ``` 第 0 次(首次) plan = anchor + first_trigger_days 第 1 次(二次) plan = anchor + first_trigger_days + 1 × second_interval_days 第 k 次(二次) plan = anchor + first_trigger_days + k × second_interval_days ``` - 幂等键 = `(customer_id, notice_type, plan_notify_time)`,同计划时刻永不重写(对称商机 `uk_pending_notice_dedupe`)。 - Job 每轮对每个客户只补**下一条未建的到期提醒**(不批量堆历史)。 - **跟进刷新锚点** → 旧锚点的未发提醒**置已失效**(对称商机回收时 invalidate 语义);新锚点重新从首次开始计时。 **参与范围**(扫表条件): - `archive_status = 有效(1)`(归档/合并不提醒) - `owner_user_id IS NOT NULL`(公海无接收人不提醒,chart 票 10 待拍板点 6) - `last_valid_follow_time IS NOT NULL`(防御脏数据) - `master_enabled = 1` 且(`first_trigger_enabled=1` 或 `second_interval_enabled=1`) --- ### D. 待发通知表 `customer_pending_notice`(crm-customer) 对称 `OpportunityPendingNotice`,字段集一致: | Java 字段 | 列名 | 类型 | 说明 | |---|---|---|---| | id | id | bigint | ASSIGN_ID 主键 | | customerId | customer_id | bigint | 客户 ID | | noticeType | notice_type | varchar(32) | `OVERDUE_REMIND`(本期唯一类型) | | targetUserId | target_user_id | bigint | 应通知对象 = 客户销售负责人(快照) | | planNotifyTime | plan_notify_time | datetime | 应发提醒时刻(= 提醒链推导时刻) | | payloadJson | payload_json | text | 上下文 JSON(见下) | | noticeStatus | notice_status | tinyint | 0待发 1已发 2已失效 | - **唯一约束**:`UNIQUE(customer_id, notice_type, plan_notify_time)`。 - **索引**:`idx(customer_id, notice_status)` 供消费者扫待发;`idx(notice_status, plan_notify_time)` 供补发调度。 - **payload JSON**(对称商机无「剩余天数」伪状态):`{ remindSeq(0=首次/k=第k次二次), overdueDays(已无跟进天数), anchor(last_valid_follow_time), firstTriggerDays, secondIntervalDays }`。 > 本期**只落待发记录,不直接发送**;平台通知能力(钉钉 + 应用内通知)spin-out,就绪后由另一消费者扫 `notice_status=待发` 补发(对称商机票 05 拍板)。 --- ### E. 调度 Job `CustomerReminderJob`(crm-customer) - **cron**:`${crm.customer.reminder-cron:0 0 4 * * ?}`(默认凌晨 4 点,错开线索 2 点 / 商机 3 点)。 - **流程**(对称 `OpportunityMaintenanceJob`,但无回收分支): 1. 读 `customer_reminder_rule` 单行;`master_enabled=0` 或无配置 → 整轮跳过。 2. 扫参与范围客户(见 C)。 3. 对每个客户:算**下一条未建的到期提醒**(`plan ≤ now`),若有则落 `customer_pending_notice`(幂等:已存在同 plan 时刻跳过)。 4. 锚点已被刷新的客户:把旧锚点未发的 OVERDUE_REMIND 置已失效。 - **不发送**:只落待发记录(E 末尾同)。 --- ### F. 规则生效时机(chart Q4-补 用户强调,已确认) > 用户原话「规则在客户新增的时候生效」——最终解读:**新建客户时把 `last_valid_follow_time` 初始化为 `create_time`**,客户创建后即开始计时,30 天(首次触发默认值)无跟进 → 首次提醒。**回填票 02**(已落 `last_valid_follow_time` 字段)+ **票 07**(新建跟进时刷新)。 --- ### G. 回填 / 下游影响 - **票 02**:`last_valid_follow_time` 已含(✅ 已落)。 - **票 07**:跟进记录写入时刷新 `last_valid_follow_time`。 - **票 01**:CONTEXT.md 增术语「超期未跟进提醒」。 - **crm-rule/CONTEXT.md**:增客户规则族段(对称商机规则段)。