进入身份识别规则
入口建议放在 系统设置 / 数据管理 / 联系人字段管理 / 身份识别规则,延续当前字段主键配置心智。
将联系人身份识别从单字段 Email/Mobile 升级为管理员可配置的组合匹配键,解决酒店集团同邮箱不同会员卡导致属性覆盖的问题。
客户真正要解决的不是数据库物理联合主键,而是联系人身份识别规则从“单字段命中即同一人”升级为“多字段组合同时命中才同一人”。
| 验收项 | 明确定义 |
|---|---|
| 管理员配置 | 管理员可以在联系人字段管理中选择 2 到 5 个字段组成一套身份识别规则,例如 cardNo + email。 |
| 导入识别 | 导入同一 cardNo + email 时更新同一联系人;导入同邮箱但不同 cardNo 时创建/保留不同联系人。 |
| 属性不覆盖 | 酒店 A、酒店 B 的偏好字段、标签、分群结果不因邮箱相同而互相覆盖。 |
| 兼容存量 | 未启用组合规则的租户,现有 Email/Mobile 主键、导入、API、分群、旅程、退订查询行为不变。 |
| 冲突可见 | 当 cardNo 命中联系人 A、email 命中联系人 B 且无法判断同一人时,不静默合并,进入冲突池或返回明确错误。 |
| 可回退 | 规则启用前有预检,启用后可禁用或回滚到旧规则;历史冲突和规则变更有审计记录。 |
| 竞品 | 实现方式 | 优点 | 缺点 | DMTX 可借鉴之处 |
|---|---|---|---|---|
| HubSpot | 联系人默认以 Email 自动去重;API 支持 Record ID、Email 或自定义唯一属性 idProperty 读取/Upsert。导入缺少 Email 或其他唯一标识时会创建新联系人;多个联系人共享同一导入 Email 时会报错跳过。 | 规则清晰,默认去重易理解;自定义唯一属性适合集成。 | Contact Email 仍是强唯一语义,不适合“同邮箱多会员关系”。 | 提供明确唯一标识配置、导入预检、冲突提示,而不是静默合并。 |
| Klaviyo | Profile 至少包含 email、phone_number、external_id 之一;Create or Update Profile 可按标识创建或更新。官方建议优先用 email/phone,external_id 属高级用法,使用不一致会产生难清理重复 Profile。 | API 同步友好;明确提示 external_id 使用风险;提供 merge 能力。 | external_id 与 email/phone 使用不一致时可能产生重复,不自动兜底。 | 组合规则字段必须稳定、一致地随数据写入,并提供冲突/重复治理。 |
| Salesforce Marketing Cloud | 使用 Contact Key / Subscriber Key 作为跨 Email、SMS、Push 的稳定唯一身份;明确建议避免用 email 作为 Subscriber Key,同一 email 可以绑定多个 Subscriber Key。 | 企业级身份模型成熟,支持共享邮箱、多关系、多渠道。 | 对前期身份设计要求高,后期改 Key 成本大。 | 把组合主键定位为“稳定业务身份键”,不要把可变 email 当唯一事实源。 |
external_id 若使用不一致,会产生难清理重复 Profile。| 层级 | 证据文件 | 现状 | 改造含义 |
|---|---|---|---|
| 元数据 | MetadataFieldDTO.java:91AbsMetaDataService.java:109ContactMetaDataService.java:40 | 字段级 if_pk/ifPk 标记和 pkFieldMapping 缓存。 | 新增规则级模型,不能继续用多个布尔字段表达组合语义。 |
| Open API | OpenAttributeDTO.java:11OpenAttributeDTO.java:13openResource.java:87/124/202/236 | 只接收单个 name/value 并调用 getContactId(companyId, name, value)。 | 扩展为 keyAttributes,保留旧单字段兼容。 |
| 动态 SQL | ContactInfoService.java:1085ContactInfoService.java:1089ContactInfoService.java:1094 | 字段名拼接到 SQL,值参数化。 | 字段名必须经过元数据/规则白名单映射,避免非法字段和 SQL 注入风险。 |
| 导入合并 | ContacatImportDisposeService.java:49ContacatImportDisposeService.java:120ContacatImportDisposeService.java:266ContacatImportDisposeService.java:311 | 按 onlyEmail、onlyMobile、emailMobile 三桶处理;email/mobile 命中不同联系人时会合并并删除一条。 | 组合规则启用后,不能再因为 email 相同就合并酒店 A 和酒店 B 的会员。 |
| 物理表 | create_table_contact.sql:1create_table_contact.sql:3create_table_contact.sql:11create_table_contact.sql:12 | contact.id 是物理主键,contact_id/customer_id 是业务稳定 ID。 | 不建议把表物理主键改成 (cardNo, email),应新增组合业务身份索引。 |
| 受影响模块 | 主要文件/模块 | 影响说明 |
|---|---|---|
| 元数据 | MetadataFieldDTO、AbsMetaDataService、ContactMetaDataService、CustomerFieldListDTO | 当前只有字段级 ifPk,需新增规则级模型。 |
| 手工保存 | ContactInfoService.saveSingleContact、verifyEmail、verifyMobile、updateInOtherGroup | 从单字段重复校验升级为统一身份解析。 |
| 批量导入 | ContacatImportDisposeService、ContactGroupUploadService | Email/Mobile 三桶逻辑需替换为规则驱动匹配/新增/冲突。 |
| Open API | OpenAttributeDTO、openResource、ContactServiceOpenApiImpl、ContactServiceApiImpl | 单字段 name/value 需要兼容扩展为多字段 keyAttributes。 |
| 外部同步 | ScrmImportApiService、ELTApiService、UpdateContactFieldArrayService、CustomerInfoFieldControlService | SCRM、企微、标签、画像字段同步需明确稳定 ID 与组合键优先级。 |
| 下游消费 | rtjourney、scorerating、ContactSubscriptionService、报表/导出 | 不建议替换 customer_id,但必须回归验证统计、旅程、退订一致性。 |
入口建议放在 系统设置 / 数据管理 / 联系人字段管理 / 身份识别规则,延续当前字段主键配置心智。
酒店模板默认推荐 会员卡号 cardNo + 邮箱 email;预检展示可识别人数、缺失字段、重复组合键、部分冲突。
规则启用按钮必须在预检通过后才可点击;冲突数据进入冲突池,不自动合并。
| 配置项 | 默认值 | 说明 |
|---|---|---|
| 规则状态 | 未启用 | 存量租户不受影响。 |
| 规则模板 | 当前单字段主键兼容模式 | 初次进入时显示“当前仍按 Email/Mobile 等单字段识别”。 |
| 组合字段数 | 2 到 5 个字段 | v1 不建议超过 5 个,避免索引、缺失率和解释成本过高。 |
| 字段必填 | 组合字段全部必填 | v1 建议 all-match,缺字段进入“缺失主键字段”提示,不自动回退。 |
| 字符串规范化 | trim + 小写 email | 邮箱默认 trim/lowercase;手机号可 E.164;卡号默认 trim,保留大小写可配置。 |
| 冲突策略 | 阻断并进入冲突池 | 不做静默合并,保护客户数据。 |
| 规则变更 | 必须预检 | 变更字段前展示影响人数、冲突数、缺失数。 |
| 导入页指标 | 示例 | 用户看到的含义 |
|---|---|---|
| 本次识别规则 | cardNo + email | 本次不是单独按 email 合并。 |
| 预计新增 | 1,240 | 组合键不存在,将创建新联系人。 |
| 预计更新 | 8,700 | 组合键已存在,将更新同一联系人。 |
| 缺失主键字段 | 86 | 缺少 cardNo 或 email,需下载错误明细。 |
| 冲突 | 12 | 部分字段命中不同联系人,需管理员处理。 |
推荐采用 “稳定内部 ID + 管理员可配置组合匹配键” 的双层身份方案:继续使用 contact.id 作为物理主键,继续使用 customer_id/contact_id 作为 DMTX 内部稳定身份和下游关联键;新增规则表、规则字段表、身份索引表和冲突表;组合键只决定“找到谁、更新谁、新建谁、是否冲突”,不直接替换下游 customer_id。
email 只作为组合键的一部分,不再单独决定“同一联系人”。cardNo + email 同时一致才命中同一联系人。cardNo 命中 A、email 命中 B 时视为冲突,不自动合并。| 表 | 用途 | 关键字段 | 关键约束/说明 |
|---|---|---|---|
contact_identity_rule | 保存每个租户启用的联系人身份规则。 | company_id、object_type、rule_name、rule_code、match_mode、status、version、missing_key_strategy、conflict_strategy、legacy_fallback_enabled | 建议唯一启用规则:(company_id, object_type) where status='ENABLED'。 |
contact_identity_rule_field | 保存规则字段顺序、规范化方式和必填性。 | rule_id、field_name、column_name、field_type、ordinal、required、normalizer、blank_as_null | v1 默认所有组合字段必填,ordinal 参与 hash 生成。 |
contact_identity_value | 承载组合键到 contact_id/customer_id 的快速定位。 | company_id、rule_id、rule_version、contact_id、customer_id、composite_key_hash、key_payload_json、display_key、status、source | 建议唯一索引:(company_id, rule_id, composite_key_hash) where status='ACTIVE'。 |
contact_identity_conflict | 记录冲突和人工处理状态。 | conflict_type、incoming_payload_json、matched_contacts_json、status、resolution_action、resolved_by | 冲突类型包括 PARTIAL_MATCH_DIFFERENT_CONTACT、DUPLICATE_COMPOSITE_KEY、MISSING_REQUIRED_KEY。 |
CREATE UNIQUE INDEX ux_contact_identity_rule_enabled
ON contact_identity_rule(company_id, object_type)
WHERE status = 'ENABLED';
CREATE UNIQUE INDEX ux_contact_identity_value_active
ON contact_identity_value(company_id, rule_id, composite_key_hash)
WHERE status = 'ACTIVE';
GET /api/contact/identity-rules/current?companyId=10001
{
"code": 200,
"message": "success",
"data": {
"enabled": true,
"ruleId": 9001,
"ruleName": "酒店会员身份规则",
"matchMode": "ALL_FIELDS",
"status": "ENABLED",
"version": 3,
"fields": [
{ "fieldName": "cardNo", "displayName": "会员卡号", "required": true, "normalizer": "TRIM", "ordinal": 1 },
{ "fieldName": "email", "displayName": "邮箱", "required": true, "normalizer": "LOWER_TRIM", "ordinal": 2 }
],
"conflictStrategy": "BLOCK_AND_REVIEW",
"legacyFallbackEnabled": false
}
}
POST /api/contact/identity-rules/preview
{
"companyId": 10001,
"ruleName": "酒店会员身份规则",
"matchMode": "ALL_FIELDS",
"fields": [
{ "fieldName": "cardNo", "normalizer": "TRIM", "required": true, "ordinal": 1 },
{ "fieldName": "email", "normalizer": "LOWER_TRIM", "required": true, "ordinal": 2 }
],
"conflictStrategy": "BLOCK_AND_REVIEW"
}
{
"code": 200,
"message": "preview success",
"data": {
"totalContacts": 1200000,
"canBuildIdentity": 1130000,
"missingRequiredKey": 62000,
"duplicateCompositeKey": 180,
"partialConflict": 27,
"estimatedIndexRows": 1130000,
"canEnable": false,
"downloadConflictFileUrl": "/api/contact/identity-rules/preview/9001/conflicts.csv"
}
}
OpenAttributeDTOpublic class OpenAttributeDTO implements Serializable {
private Long userId;
private Long companyId;
// 旧单字段模式,继续兼容
private String name;
private String value;
// 新组合键模式
private Map<String, String> keyAttributes;
private Long identityRuleId;
}
{
"companyId": 10001,
"userId": 88,
"keyAttributes": {
"cardNo": "A001",
"email": "zhangsan@example.com"
}
}
{
"code": 409,
"message": "identity conflict",
"data": {
"conflictType": "PARTIAL_MATCH_DIFFERENT_CONTACT",
"conflictId": 778812,
"hint": "cardNo and email match different existing contacts; manual review required"
}
}
contact/src/main/java/com/qdum/dmtx/biz/contact/service/identity/
├── ContactIdentityRuleService.java
├── ContactIdentityResolverService.java
├── ContactIdentityNormalizer.java
├── ContactIdentityIndexService.java
├── ContactIdentityConflictService.java
├── dto/ContactIdentityRuleDTO.java
├── dto/ContactIdentityResolveRequest.java
├── dto/ContactIdentityResolveResult.java
└── enums/IdentityResolveStatus.java
| 返回状态 | 含义 | 写路径动作 |
|---|---|---|
MATCHED | 组合键唯一命中一条联系人。 | 更新该 contact_id/customer_id。 |
NO_MATCH | 组合键未命中。 | 新建联系人并写身份索引。 |
MISSING_REQUIRED_KEY | 缺少必填组合字段。 | 导入错误或 API 400。 |
CONFLICT | 部分字段或索引命中冲突。 | 写冲突池,API 409,不自动合并。 |
LEGACY_MATCHED | 未启用组合规则时按旧单键命中。 | 保持兼容。 |
导入改造后的目标流程:
读取启用规则
→ 对每行提取组合字段
→ 规范化并生成 composite_key_hash
→ 批量查 contact_identity_value
→ 按结果分桶:更新 / 新增 / 缺失字段 / 冲突
→ 批量写 contact 和 contact_identity_value
→ 生成导入结果报告
| 页面/组件 | 功能 |
|---|---|
| 联系人字段管理 / 身份识别规则 Tab | 展示当前规则、创建规则、启用/禁用、查看版本。 |
| 规则字段选择器 | 只能选择可参与身份识别的字段,展示字段类型、缺失率、重复率。 |
| 规则预检结果页 | 展示可启用、缺失字段、重复组合键、冲突数量。 |
| 导入预检摘要卡 | 在联系人导入页展示本次数据按当前规则的新增/更新/冲突统计。 |
| 冲突池列表 | 按冲突类型、导入批次、字段值筛选和处理。 |
| 场景 | 处理策略 | 优先级 |
|---|---|---|
| 同邮箱不同卡号 | 组合键不同,应保留为不同联系人,不覆盖属性。 | P0 |
| 同卡号同邮箱重复导入 | 命中同一组合键,更新同一联系人。 | P0 |
| 缺少 cardNo 或 email | v1 默认拒绝导入该行,导出错误明细;不自动回退 email 单键。 | P0 |
| cardNo 命中 A、email 命中 B | 写入冲突池并阻断,不静默合并。 | P0 |
| 存量数据已有重复组合键 | 预检时阻断启用,管理员先处理冲突或标记忽略。 | P0 |
| email 大小写/空格不同 | 邮箱默认 trim + lowercase,hash 使用规范化值。 | P1 |
| 卡号前导零 | 卡号按文本处理,默认 trim 但不转数字,避免 00123 变 123。 | P1 |
API 只传旧 name/value | 未启用组合规则时按旧逻辑;启用组合规则后建议返回提示或仅唯一命中时兼容。 | P1 |
| 性能问题 | 使用 contact_identity_value.composite_key_hash 唯一索引,批量导入按 hash 批查。 | P1 |
| 动态字段 SQL 注入 | 字段名必须从规则字段白名单映射到列名,禁止直接拼接外部 key。 | P0 |
| 回滚 | 禁用规则只影响后续写入;已创建/更新数据通过审计和冲突池处理,不自动删除。 | P1 |
| 模块 | 工作项 | 预估人天 |
|---|---|---|
| 产品/技术梳理 | 字段来源确认、酒店场景规则确认、存量数据预检口径、接口兼容策略。 | 3-5 |
| 数据模型与迁移 | 新增规则表、规则字段表、身份索引表、冲突表、DDL、索引、回填脚本。 | 4-7 |
| 后端核心能力 | ContactIdentityRuleService、ContactIdentityResolverService、规范化、hash、冲突判断、审计。 | 6-9 |
| 导入改造 | 替换 Email/Mobile 三桶逻辑,导入预检,批量写身份索引,错误明细。 | 6-9 |
| 手工/API 保存改造 | 改造 ContactInfoService.saveSingleContact、旧 verifyEmail/verifyMobile 兼容、字段白名单。 | 3-5 |
| Open API/内部 API | 扩展 OpenAttributeDTO,改造按主键查询属性、标签、评分、退订接口。 | 3-5 |
| SCRM/企微同步 | 明确稳定 ID 与组合键优先级,改造相关同步服务,回归外部链路。 | 5-8 |
| 前端管理端 | 身份规则页、字段选择、预检结果、冲突池入口、导入摘要卡。 | 5-8 |
| 测试 | 单元测试、导入集成测试、API 测试、回归矩阵、性能测试。 | 6-8 |
| 联调/灰度 | 酒店试点、数据回填校验、监控看板、灰度开关、上线支持。 | 4-6 |
| 合计 | 完整平台化可复用能力。 | 45-70 |
| MVP 模块 | 工作项 | 预估人天 |
|---|---|---|
| 后端规则与索引 | 固定组合规则、hash、身份索引、冲突表。 | 5-7 |
| 导入/手工保存 | 导入预检、写路径 resolver、禁用 email 单独合并。 | 5-7 |
| 查询/API 兼容 | keyAttributes 查询、旧接口兼容、字段白名单。 | 2-4 |
| 简化配置/实施脚本 | 后台配置、回填脚本、冲突导出。 | 3-4 |
| 测试/灰度 | 酒店样例、批量导入、回归、上线验证。 | 5-6 |
| 合计 | 单客户可交付 MVP。 | 20-28 |
推荐。组合主键实现为业务身份匹配规则,保留 contact.id 与 customer_id/contact_id,冲突阻断并审计。
ifPk=true不推荐。实现看似快,但语义不清,容易继续按单字段命中并合并。
不推荐。会影响大量外键、缓存、旅程、评分、导出、API 和历史数据。
| 维度 | 方案 A:多个 ifPk=true | 方案 B:规则对象 + 身份解析器 + 身份索引 | 方案 C:数据库联合主键 |
|---|---|---|---|
| 实现复杂度 | 低 | 中高 | 极高 |
| 用户体验 | 表面简单,但解释不清“或/且”关系。 | 管理员配置清晰,运营只看预检结果。 | 用户无感,但系统迁移风险巨大。 |
| 数据安全 | 高风险,容易继续按单字段合并。 | 可控,冲突阻断并审计。 | 高风险,历史关联和下游外键易断。 |
| 扩展性 | 差,只能堆字段布尔标记。 | 好,可支持模板、规则版本、冲突治理。 | 差,物理主键与业务规则强耦合。 |
| 工期 | 1-2 周但不可控。 | MVP 3-4 周,平台化 4-7 周。 | 2-3 月以上且风险高。 |
确认 cardNo 是真实列还是动态扩展字段;确认 cardNo + email 是否全部必填;确认同邮箱不同卡号必须允许重复邮箱。
新增规则表、规则字段表、身份索引表、冲突表;新增 ContactIdentityResolverService 和 normalizer;字段名校验收敛到元数据白名单。
改造 ContacatImportDisposeService.ContactImportSaveOrUpdate 和 ContactInfoService.saveSingleContact;组合规则启用后禁用 email 单独合并和 deleteContactIds 静默删除。
扩展 OpenAttributeDTO.keyAttributes;改造按主键查询属性、标签、评分、退订接口;旧 name/value 保留兼容。
SCRM/企微同步接入统一 resolver;明确 customer_id/contact_id/external_userid 高于组合键;回归分群、旅程、评分、退订、导出。
酒店客户灰度启用,监控导入命中率、冲突率、重复创建率、API 查询失败率;冲突池处理流程上线。
| 测试类型 | 重点用例 |
|---|---|
| 单元测试 | normalizer、hash 生成、字段白名单、resolver 状态判断。 |
| 导入集成测试 | 同 cardNo+email 更新;同 email 不同 cardNo 新增;缺字段报错;部分命中冲突。 |
| 手工保存测试 | 新增联系人、编辑组合字段、编辑非组合字段、旧租户兼容。 |
| Open API 测试 | 旧 name/value、新 keyAttributes、冲突 409、字段非法 400。 |
| SCRM/企微测试 | 有稳定 ID 优先、无稳定 ID 走组合键、冲突阻断。 |
| 下游回归 | 分群人数、旅程触发、评分记录、退订状态、标签读取、导出字段。 |
| 性能测试 | 10 万/100 万联系人身份索引回填;批量导入按 hash 查询;冲突查询分页。 |
| 安全测试 | 动态字段 SQL 注入、越权字段查询、冲突明细脱敏。 |
cardNo 是标准字段、扩展字段,还是客户导入时动态生成字段?cardNo 是否全集团唯一,还是只在酒店/品牌维度内唯一?如果只在酒店内唯一,组合键可能需要 hotelCode + cardNo + email。cardNo 但有 email 的数据,客户希望拒绝、进入错误表,还是按旧 email 逻辑导入?本方案建议 v1 拒绝。keyAttributes?如果不能,需要多长兼容期?| 维度 | 检查项 | 分值 | 自评 |
|---|---|---|---|
| A. 需求理解 | 客户原始需求已记录;验收标准明确。 | 4 | 4 |
| B. 竞品调研 | 至少 2 个竞品;有 DMTX 差异化策略。 | 4 | 4 |
| C. 用户体验 | 操作流清晰;核心步骤 ≤ 3;默认值合理。 | 6 | 6 |
| D. 技术完整度 | 数据模型;API;边界情况。 | 6 | 6 |
| E. 可行性 | 工作量;风险点。 | 4 | 4 |
| F. 附加分 | 多方案对比;扩展性。 | 2 | 2 |
| 总分 | 26 | 26 |
本需求应按“联系人身份治理能力”立项,而不是按“字段主键小优化”处理。建议采用两级范围:客户 MVP 先支持酒店 cardNo + email,预计 20-28 人天;平台能力沉淀为可配置组合身份规则、冲突池、规则版本和 Open API 兼容,预计 45-70 人天。
contact 表物理主键。ifPk=true 伪装组合主键。ContactIdentityResolverService。