案例研究 · 平台工程。Meta Dados 方法论(Genba · Kanso · Omotenashi)。
摘要。在服务领域的多租户 SaaS 中,同一个人会以客户、员工和企业主的身份出现——有时还分布在多个门店中。我们将这一问题建模为锚定式身份图:一个全局实体(即由 CPF 锚定的个人),多个按租户去范式化的关联关系向其汇聚。一项受控实验在生产数据库上以 ROLLBACK 事务方式执行,结果表明身份能够通过 CPF 正确统一,但去范式化的数据会在各表之间悄然发生偏离,而某一类以空 CPF 创建“内联”登录账号的入口,会将一个人分裂成两个身份。我们描述了两项已验证的干预措施,并推导出可复用的原则。
1. 引言
预约类平台将三种实体视为一等公民——客户、员工和门店——每一种都有其自然键:客户以电话号码识别;员工要求提供 CPF;门店以 CNPJ + 电子邮箱识别(个体从业者则以 CPF 识别)。问题在于这三者并非互斥:一位客户可能被聘用而成为员工;一位客户可能自己开店创业;一位企业主可能作为客户去另一家沙龙预约。系统需要让信息在各表之间流转并保持一致,避免同一关键数据在不同记录中相互偏离。
我们以Marcaê平台作为案例研究,但该模型与研究结论具有普遍性:适用于任何一个个体随时间推移、跨租户承担异质角色的 SaaS。
2. 模型:锚定式身份
核心思想是将身份与角色分离。个人是一个全局实体——不属于任何租户,由一个强键(规范化后的 CPF)锚定。每一条关联关系(客户、员工、用户)都携带一份关键字段的去范式化副本,以及一个指向该个人的外键 person_id。一个数据库触发器(vincular_pessoa_por_cpf)执行查找或创建:在写入一条带有效 CPF 的关联关系时,它会关联(或创建)该 CPF 对应的个人并固定 person_id。若没有有效的 CPF,身份即局限于该租户。
3. 不变式与偏离度指标
该模型必须保证两个不变式。第一个是身份统一(I1):两条具有相同有效 CPF 的关联关系指向同一个人。第二个是角色完整性(I2):该个人知晓由其关联关系派生出的所有角色(租户 + 角色)。
为衡量关键数据(姓名、电子邮箱、电话)的一致性,我们定义了一个偏离率 δ:在锚点及其关联关系之间,某个关键字段存在多个非空值的个人所占比例。δ = 0 是目标——个人是唯一的真相来源。
4. 身份分裂缺陷
有一类入口在(连同客户一起)“内联”创建登录账号时,采用了一种忽略 CPF 的锚定方式——实际上,它总是创建一个新的个人。如果一个人经过了一个执行锚定的入口,又经过了一个不执行锚定的入口,那么他就会被两个个人所表示——从而违反 I1。分裂率 σ 随着不执行锚定的入口的使用而成比例增长。这是严重性最高的缺陷,因为身份一旦分裂,就只能通过人工/SQL 操作来重新对齐。
5. 方法论与结果
我们采用 Genba——深入现场:不去推演预期中的模型,而是运行真实模型。有两种技术:(1)穷尽式地阅读产生或转化客户/员工/用户/企业主的 15 个入口,记录每个入口写入什么、是否基于 CPF 锚定;(2)事务化模拟——我们直接在生产数据库中、于 BEGIN … ROLLBACK 内实例化各种身份历程:锚定触发器会真实触发,但不会持久化任何内容。
结果印证了该模型,并标出了它失效之处:
- 在有锚定之处 I1 成立——一个人在 2 个租户中经历 5 种角色,都写入了相同的 person_id。
- 关键数据发生偏离——锚点只保留它最先看到的内容:员工后来收到的电子邮箱从未回传到个人;姓名在各表之间出现偏离。
- 在受控场景中 σ = 0.5——2 个人中有 1 个被内联创建登录账号的入口分裂成两个身份。
6. 干预措施
将去重作为等价关系。旧规则仅凭电话号码就合并两份记录;新规则要求规范化后的电话号码与姓名同时匹配。这样,共用一个电话号码的两个不同的人(用自己的手机为孩子预约的母亲)就不会再被错误地合并。
将合并作为逐字段的 fold。当两份记录确实属于同一个人时,合并不再是“选一份、弃一份”,而是逐字段地取舍:一张差异表将每个存在偏离的字段并排呈现,由操作员选择保留在幸存记录中的值。顺序很重要——必须在应用取舍之前先重新挂接引用并归档被移除的记录——否则部分唯一索引会发生冲突。丢失数据的合并是一种倒退;逐字段的合并则保留每份记录中最优的内容。
7. 讨论与原则
该案例揭示了一个反复出现的权衡:在关联关系中去范式化关键字段,能让每个界面既快速又自足,代价是一致性;而将个人作为唯一真相来源则能消除偏离,代价是读取时的耦合。以下是适用于任何具有共享身份的多租户平台的可复用原则:
- 一个锚点,一个键。选定一个全局实体和一个强键(CPF/CNPJ),并让每一个入口都基于它锚定。哪怕只有一个入口以空值锚定,也会破坏该保证。
- 锚定是状态的不变式,而非角色的不变式。将角色建模为状态;客户→员工→企业主的转变必须保留 person_id。“转化”一个角色是增加一条关联关系,而绝非创建一个新的身份。
- 按字段决定一致性。电子邮箱和电话需要唯一来源;本地备注则可以驻留在关联关系中。指标 δ 衡量每项决策的代价。
- 去重是一种等价关系——请明确地选定它。“相同电话”与“相同电话且相同姓名”会产生不同的划分。
- 对齐是 fold,而非丢弃。
8. 结论
当存在 CPF 时,Marcaê 的身份能够正确统一——锚点有效,且对角色转变具有稳健性。那一类会导致身份分裂的入口已通过数据库触发器封堵(对齐机制确保账号跟随 CPF 对应的个人,合并不再产生孤立的登录账号,锚点也开始学习它此前缺失的关键数据)。剩下的只有完整的唯一真相来源——这是一项设计决策,而非缺陷。所带来的收益不仅是技术层面的:它保证了一位转为员工的客户,或一位转为客户的企业主,在整个系统中始终是同一个人。
常见问题
为什么不能只用电话号码来识别个人?
因为电话号码并不是识别个人的强键:两个不同的人会共用一个号码(用自己的手机为孩子预约的母亲),而一个人也会更换号码。以 CPF(强键,每人唯一)锚定,并仅将电话+姓名作为去重关系,可避免错误地合并不同的人。
这只适用于预约场景,还是适用于任何 SaaS?
该模型具有普遍性。它适用于任何一个个体随时间推移、跨租户承担异质角色的多租户 SaaS——市场平台、医疗、教育、物业管理平台等。“一个锚点,一个键;角色是状态,而非身份”这一原则可在各个领域间迁移;变化的只是强键和关键字段的集合。