3. 数据契约的 Schema 设计:字段定义、数据类型、约束条件、版本管理

好,咱们进入正题。上一章聊了数据契约是什么、为什么能保障安全,这一章咱们得落地了——Schema 怎么设计。

说白了,数据契约的 Schema 就是一份“数据接口的说明书”。你想想看,跨团队协作最怕什么?怕 A 团队说“我传了个 userId”,B 团队理解成“用户昵称”。Schema 就是用来消灭这种歧义的。

我个人习惯把 Schema 设计拆成四个维度:字段定义、数据类型、约束条件、版本管理。咱们一个一个来。

3.1 字段定义:名字要见名知意

字段名是契约的第一道防线。我在项目中遇到过最离谱的事——有个字段叫 f1,文档里写“字段1”,没人知道它存什么。后来查了三个月前的邮件才知道是“首次登录时间”。

所以我的建议是:

  • 全小写 + 下划线:比如 user_id、created_at,别用驼峰,也别用拼音。
  • 命名要自解释:is_active 比 status 好,phone_number 比 phone 好。
  • 加前缀区分业务域:比如 order_、user_、payment_,一眼就知道归属。

核心原则:字段名就是契约的“身份证”,别让下游团队猜。

3.2 数据类型:别让“字符串”背所有锅

很多团队图省事,所有字段都写成 string。嗯,这里要注意——数据类型越模糊,安全风险越大。

举个例子:一个“年龄”字段,你用 string 存,下游可能传进来“abc”,解析直接崩。用 int 就天然过滤掉了。

我常用的数据类型清单:

类型 适用场景 避坑点
string 名称、描述、备注 必须限制最大长度,否则可能被注入超长文本
int ID、数量、年龄 注意范围,比如 uint32 还是 int64
float/double 金额、百分比 金额建议用 decimal 避免精度丢失
boolean 开关、状态标记 别用 0/1 代替,语义不清晰
timestamp 时间点 统一用 UTC,别用本地时间
enum 有限状态集合 必须显式列出所有枚举值

小技巧:我习惯在契约里加一个 example 字段,写一个真实示例值。比如 "phone_number": "13800138000",比任何文档都管用。

3.3 约束条件:给数据上“紧箍咒”

约束条件就是告诉下游:这个字段能是什么、不能是什么。我把它分成三类:

3.3.1 非空约束

这个最简单,但最容易漏。我曾经遇到一个支付系统,amount 字段没设非空,结果某次上游没传,下游直接扣了 0 元——嗯,那天的工单我到现在还记得。

  • 用 required: true 标记必填字段
  • 关键字段如 user_id、order_id、amount 必须非空

3.3.2 唯一约束

唯一约束不只是数据库的事,契约里也要声明。比如 email 字段,如果两个用户传了同一个邮箱,业务逻辑会乱套。

  • 用 unique: true 标记
  • 注意:唯一约束通常只在“主键”或“业务标识”上使用

3.3.3 枚举约束

这个我特别想多说两句。很多团队用 string 存状态,然后文档里写“取值:0-待支付,1-已支付,2-已退款”。结果下游传了个“3”,系统直接崩了。

正确的做法:在契约里显式定义枚举。

// 错误示范
"status": {
  "type": "string",
  "description": "订单状态"
}

// 正确示范
"status": {
  "type": "string",
  "enum": ["pending", "paid", "refunded", "cancelled"],
  "description": "订单状态:pending-待支付,paid-已支付,refunded-已退款,cancelled-已取消"
}

警告:枚举值一旦发布,不要删除!只能新增。否则下游还在用旧值,上游突然不认了——这就是典型的“契约断裂”。

3.4 版本管理:让契约“活”起来

Schema 不是写一次就完事的。业务在变,字段在加,类型在改。没有版本管理,契约就是一纸空文。

我推荐语义化版本:MAJOR.MINOR.PATCH。

版本号变化 含义 示例
MAJOR 增加 不兼容变更 删除字段、修改类型、重命名字段
MINOR 增加 向后兼容的新增 新增可选字段、新增枚举值
PATCH 增加 内部修复 修正描述、调整示例值

我个人习惯在契约文件头部加一个版本块:

{
  "schema_version": "1.2.0",
  "last_updated": "2025-03-15",
  "changelog": [
    "1.2.0 - 新增 phone_number 字段(可选)",
    "1.1.0 - 新增 status 枚举值 'cancelled'",
    "1.0.0 - 初始版本"
  ]
}

避坑指南:我曾经见过一个团队,版本号从 1.0.0 直接跳到 2.0.0,结果下游还在用 1.x 的客户端,全线报错。记住:MAJOR 版本升级必须通知所有下游,并给过渡期。

3.5 一张图看懂 Schema 设计

说了这么多,咱们用一张图把整个流程串起来:

数据契约 Schema 设计流程 数据契约 Schema 设计流程 Schema 设计 字段定义 数据类型 约束条件 版本管理 全小写 + 下划线 自解释命名 业务域前缀 string / int / float boolean / timestamp enum 显式枚举 非空约束 (required) 唯一约束 (unique) 枚举约束 (enum) MAJOR 不兼容变更 MINOR 新增兼容 PATCH 内部修复 核心原则:字段清晰 + 类型严格 + 约束明确 + 版本可追溯 —— 消灭歧义,保障跨团队数据安全

3.6 实战建议:从“能用”到“好用”

最后,分享几个我在实战中沉淀下来的习惯:

  1. 先写契约,再写代码:我习惯在项目启动时就把 Schema 定下来,哪怕后面改,也比“先写代码再补契约”好一百倍。
  2. 契约要自动化校验:别靠人工 review。用工具(比如 JSON Schema、Protobuf)自动检查字段类型、约束是否合规。
  3. 版本变更要留痕:每次改 Schema,必须写 changelog。我曾经靠 changelog 定位过一次线上事故——原来是某个字段从 int 改成了 string,下游没适配。
  4. 给下游留缓冲期:MAJOR 版本升级,至少提前 2 个迭代通知。别搞“突然死亡”。

一句话总结:好的 Schema 设计,能让跨团队协作从“猜谜游戏”变成“按图索骥”。字段定义是地图,数据类型是路标,约束条件是护栏,版本管理是导航——缺一个,都可能翻车。