## Answer **Status: resolved** ### A. 范围(定稿) - ✅ **做**:新增/编辑客户时的**内建查重校验** + **企查查外部服务契约 seam**。 - ❌ **不做**:A4-6 独立查重页 + 客户合并(chart D6);「客户查重设置」可配规则(chart D4);企查查服务内部实现(只定契约)。 ### B. 查重校验(内建,写死在新增/编辑流程) #### B1. 三层查重(定稿) | 层 | 时机 | 规则 | 结果 | |---|---|---|---| | L1 自动相似提示 | 客户名称**停输防抖**(~300ms) | 名称模糊匹配全部未删除客户 | 只提示不阻止;无权命中的脱敏为「存在匹配记录」 | | L2 保存时精确查重 | 点击确认提交 | ① 信用代码全局唯一(硬拦)② 名称相似(弹窗确认) | 信用代码同 → 禁止;名称似 → 弹窗「仍要创建/返回修改」 | | L3 终查(并发兜底) | 「仍要创建」后、写库前 | 再查一次信用代码 + 名称 | 仍无冲突才写库;DB UNIQUE 兜底 | #### B2. 相似判定算法 — 已拍板(grill Q11,seam 化) 原型明说「使用 CRM 后端统一查重服务,不在前端固化阈值」。**我的推荐(最简可行)**:用 **MySQL 内置 `FULLTEXT` 索引 + `MATCH ... AGAINST` 自然语言模式**,或更简单先上 **`LIKE '%name%'` + 编辑距离(Levenshtein) 后置过滤**。 - **✅ 已拍板(grill Q11,用户拍板 = seam 化)**:相似度判定**抽成接口/端口**(可替换策略,如 `CustomerDedupService` 内部策略接口或独立 `CustomerNameSimilarityPort`),一期只做**最简实现**,后续迭代可换算法。 - **一期最简实现**:`customer_name` 建 FULLTEXT 索引(ngram parser, token_size=2),`MATCH(customer_name) AGAINST(?)` 取 topN 候选,再算相似度判定「相似」;信用代码精确相等判定「重复」。**阈值从「客户查重设置」配置读取(70/80/90%)**,不再写死。 - **契约稳定、实现可插拔**:调用方不关心具体算法;后续可换 Levenshtein / Jaccard / 外部相似度服务,只替换端口实现。 - **统一查重服务**:抽 `CustomerDedupService`(crm-customer 内),后续 A4-6 若重启可复用;本期只被新增/编辑流程调用。 #### B3. 信用代码校验(定稿) - 格式:`^[0-9A-Z]{18}$`(18 位数字或大写字母,原型「18 位数字或字母组合」)。 - 唯一:`uk_unified_credit_code` 唯一索引(票 02 已建),**NULL 不参与唯一约束**(未填时允许)。 - 精确相等 → 硬拦禁止创建;命中无权查看的客户只提示「系统已存在该企业」,不显 name/编号/负责人。 #### B4. 联系人内联查重(定稿,回填票 06;D26 翻案后校准 20260902) - 同一客户内同表格**电话相同两行** → 禁止提交并定位重复行(表单级硬校验)。 - 无电话 → 按「客户 + 姓名 + 职务」提示疑似重复(只提示)。 - ~~全库按电话精确查重 → 相同禁止新建~~ **已翻案(D26 用户拍板 A,20260902)**:跨客户电话命中=**软提示疑似重复、不阻断**,受 crm-rule 客户查重设置「联系电话」开关控制(默认开、可关)。不建电话唯一索引。 ### C. 企查查外部服务契约 seam(定稿) #### C1. 端口(不做实现,留 mock) ```java // crm-customer 侧出站端口 public interface CompanyLookupPort { /** 按企业名查询工商候选列表 */ List lookup(String companyName); } record CompanyCandidate( String bizName, String unifiedCreditCode, String legalRepresentative, LocalDate establishedDate, String registeredCapital, String businessScope, String staffSize, String annualRevenue, String address ) {} ``` #### C2. 回填范围(定稿,原型 §3.3 明文) - **允许回填字段(9 项)**:工商名称 / 统一社会信用代码 / 法定代表人 / 成立时间 / 注册资本 / 经营范围 / 人员规模 / 年产值 / 工商注册地址。 - **只回填当前为空的字段**(首次选择);已填字段再次选择必须先弹「受影响字段确认」才覆盖;服务返回空值不覆盖原值。 - **永不覆盖**:客户类型 / 行业 / 所属地区 / 关系星级 / 客户星级 / 销售负责人 / 销售部门 / 协同人 / 人脉关系单位 / 联系人 / 备注。 - 回填后**立即用信用代码执行精确查重**(B1 L2 联动)。 #### C3. 异常语义(定稿) | 场景 | 行为 | |---|---| | 无结果 | 保留表单,提示「未查询到匹配企业」,可手工填写 | | 服务失败/超时 | 保留表单,提示重试或手工填写;**不阻断手工提交** | | 请求乱序 | 只处理最后一次有效请求,旧结果不覆盖当前表单 | ### D. 编辑客户(定稿,对称新增) - 编辑页复用新增表单结构 + 查重校验;编辑时**排除自身**出查重范围(改名字不和自己撞)。 - 历史停用枚举可回显不可重选(原型公共规则)。 ### E. 回填 / 下游 - **票 02**:`unified_credit_code` 唯一索引 + `customer_name` FULLTEXT(ngram) 索引(A5 索引表补一条 `idx_name_fulltext`)。 - **票 06**:联系人电话查重(B4)。 - **票 09**(导入):复用 `CustomerDedupService` 做导入查重。 ### F. 挂号 — 已随 grill 全部销号 1. ~~相似判定算法~~ **已拍板(grill Q11)**:seam 化接口,一期最简 = ngram FULLTEXT,后续可换算法。