本文要点
- 保留 rawAddress,不要只存解析后字段
- 邮编和门牌号使用文本类型
- 行政区域使用可选多层字段而非固定 State
- 订单应保存下单时的地址快照
作者与审核
查看编辑与审核原则- 作者
- Random Address Generator 编辑团队
- 审核
- 数据模型与 QA 审核组
- 最近实质更新
- 从国际化、原始值保留、邮编类型和地址版本化角度重写。
先区分用户输入、解析值和展示值
用户输入是原始证据,解析字段用于搜索和业务逻辑,展示值则按国家顺序组合。只保存其中一种会让后续无法核对自动修改,也无法在解析器升级后重新处理。
建议保留 rawAddressLines、parsedComponents、normalizedAddress 和 normalizationProvider/version。归一化不成功也应能保存原始输入,而不是把整条记录丢弃。
| 字段 | 类型 | 目的 |
|---|---|---|
| countryCode | CHAR(2) | ISO 国家代码 |
| addressLines | JSON / TEXT[] | 保留可变行数 |
| adminArea1..3 | TEXT nullable | 多级行政区域 |
| locality | TEXT nullable | 城市、suburb 或邮政城镇 |
| postalCode | TEXT nullable | 保留前导零和字母 |
| rawAddress | JSON | 审计和重新解析 |
不要把看起来像数字的字段存成数字
ZIP Code、Postcode、PLZ 和门牌号是标识符。它们可以有前导零、字母、空格、连字符或范围。用整数类型会丢失信息,并且让不同国家需要不同补丁。
所有文本字段应使用 Unicode 编码,长度限制来自真实数据和业务边界,而不是美文样本的平均长度。
type PostalAddress = {
countryCode: string;
addressLines: string[];
adminArea1?: string;
adminArea2?: string;
adminArea3?: string;
locality?: string;
postalCode?: string;
rawAddress: unknown;
};空字符串、null 和未提供不要混用
Address Line 2 可以没有值。在 API 契约中明确它应被省略、传 null 还是传空字符串,并在入库层统一。否则查询、去重和导出会将同一状态当成三种值。
不要用 N/A 或横线填充缺失地址行;这些占位符会进入标签和数据导出。
用快照保护订单和审计记录
用户的地址簿可以继续更新,但已创建订单、发票和发货记录应保留当时的地址快照。如果订单只引用当前 addressId,用户修改地址后历史记录也会改变。
快照应包含展示行、结构化字段、当时验证状态和时间,而不要只保存一条格式化字符串。
- 订单地址不随地址簿更新
- 记录归一化提供者与版本
- 验证状态含时间戳
- 敏感日志不输出完整真实地址
用跨国固定样本验证模式迁移
每次数据库迁移都用包含英国房屋名、加拿大字母邮编、德语字符、日文原文和前导零的固定样本做往返测试。迁移前后比较语义字段和原始 JSON,而不是只比较行数。
迁移预演还要记录被截断、无法转码或从空字符串变为 null 的记录数量。如果数据库提示迁移成功,但应用读回时无法重建原始行,这次迁移仍然失败。在上线前抽取新旧版本的结构化差异,并为不可逆更改准备恢复或重放方案。
- 比较迁移前后的 Unicode 字符串和 JSON 结构
- 统计截断、编码失败和 null 转换
- 从应用层重新读取而非只查数据库成功状态
- 为已保存的 rawAddress 准备重放工具
常见问题
地址表应该有固定的 Address Line 1、2、3 吗?
前端可以展示固定数量,但底层使用 addressLines 数组更能容纳不同国家的行数。
Postal Code 应该用 VARCHAR 吗?
是的。邮编可有字母、空格、连字符和前导零,不适合数值类型。
为什么要保留 rawAddress?
它用于审计自动修改、排查解析错误,并在解析器升级后重新处理。
