开发指南数据契约规范
时间与日期规范
业务时间统一使用 Unix epoch 毫秒,格式化字符串只用于显示和外部协议适配。
百积木平台控制的业务时间和系统时间统一使用 Unix epoch 毫秒整数。这一规则覆盖 API、模块方法、Connector 业务载荷、事件 payload、工作流、任务、缓存和数据库。
标准表示
| 位置 | 类型 | 示例 |
|---|---|---|
| JSON / Connector / 事件 payload | integer | 1785984572000 |
CModel DataType | integer | { "@type": "DataType", "type": "integer" } |
| Java | long / Long | 1785984572000L |
| Rust | i64 | 1785984572000_i64 |
| JavaScript / TypeScript | 安全整数 number | 1785984572000 |
| PostgreSQL / MySQL | BIGINT | 1785984572000 |
字段名使用业务语义,例如 createdAt、updatedAt、lastTimestamp、deliveryTime。单位由全局契约确定,不需要在每个时间点字段后追加 Millis。持续时间不是时间点,必须显式标明单位,例如 timeoutMs、durationMs。
禁止的做法
- 不得在同一字段中同时接受秒、毫秒和 RFC 3339 字符串。
- 不得根据数值位数或字符串形态静默猜测单位并自动兼容。
- 不得把
Date、Instant、DateTime或数据库时间类型的默认 JSON 序列化结果当成公共契约。 - 不得同时提供
lastTimestamp和lastOccurredAt两套同义字段。 - 不得在后端和数据库保存格式化后的本地时间字符串。
入参类型或单位错误时应立即拒绝,并返回 CModel 结构化校验错误。只接受现代业务时间的接口还应按业务范围校验,防止秒级值误入;历史日期场景则应使用适合该领域的范围,不能用一个全局阈值排除合法历史时间。
为什么数据库也存 BIGINT
数据库使用 TIMESTAMP/TIMESTAMPTZ 虽然便于日期函数计算,但 ORM 和驱动通常会把它序列化为格式化字符串,导致服务边界出现多种表示、隐式时区转换和精度差异。使用 BIGINT 可以让持久化、消息和接口保持同一份原始值,避免来回转换后改变语义。
排序、范围过滤和索引对 BIGINT 同样有效。确实需要按自然日、月份或时区做分析时,在查询或分析视图中显式转换,例如 PostgreSQL:
to_timestamp(created_at / 1000.0)显示时区属于调用方或报表层。数据库中的原始时间点不带显示时区;格式化时必须显式选择用户或工作区时区。
日期和显示文本
不是所有看起来像时间的字段都是时间点:
- 仅表示自然日的
reportDate、birthday可以使用 ISO 日期字符串YYYY-MM-DD,其 CModel 语义应明确为 date-only。 - 面向人阅读的“明天下午”“预计半小时内”等内容是显示文本,应使用
etaText、scheduleDisplayText等名称,不能命名为etaAt。 - UI 的
datetime-local、中文日期和 RFC 3339 都是显示或输入格式。前端必须在组件边界转换为毫秒,提交给服务后不再携带格式化副本。 - 外部标准强制使用 RFC 3339 时,只能在边界适配器转换;进入内部模型后立即恢复为毫秒。
迁移顺序
存量字段迁移必须一次完成生产者、消费者和数据:
- 发布结构化 CModel 契约和新主版本。
- 将数据库列确定性转换为
BIGINT,保留空值和毫秒精度。 - 更新服务模型、SQL 默认值和响应 envelope。
- 更新 Connector、前端、工作流和 Bundle Manifest。
- 部署后验证毫秒输入成功,秒级与字符串输入返回结构化错误。
不要长期保留“秒或毫秒都接受”的兼容分支。它会把契约错误隐藏到更深层,并让同一数据在不同链路产生不同含义。