接入外部接口,最容易被低估的风险不是超时和限流,是字段的静默变更:类型漂移、单位漂移、可空性漂移。它们不抛错、不留日志,只在参与运算或对账时露出后果。一次真实事故:上游把金额字段从「元,两位小数」改成「分,整数字符串」,本地两百多条用例全绿,灰度第二天结算金额放大一百倍。
用例为什么全绿。 三个常见成因:响应样例是历史快照,上游变更不会同步进本地文件;断言只验「非空」,字符串与数字在断言层面等价;解析层宽松转换吞掉了本应抛出的异常。三者共同点是——测试验证的是「代码以为自己对接的世界」,而不是「现在真实的世界」。
四类静默破坏: 类型变(数字改字符串)、单位变(元改分)、可空性变(字段缺失接默认 0)、精度变(小数位截断)。其中单位变最难发现:格式与长度都可能不变,只有量级错,单笔不显眼、汇总放大。
三道闸的实现要点:
第一道,入口契约校验。每个接口维护一张显式契约表(字段名、期望类型、单位约束),响应进来先逐项核对,违规即拦。校验之外再加一个结构指纹——把「字段名:类型」排序后取哈希,指纹与基线不一致就通知。校验拦「当前这一次」,指纹管「下一次会变」。
CONTRACT = {"amount": (str, "单位=分,纯数字字符串")} def check_contract(payload: dict) -> list: problems = [] for field, (want_type, note) in CONTRACT.items(): value = payload.get(field) if value is None: problems.append(f"{field} 缺失或为 null(约定 {note})") elif not isinstance(value, want_type): problems.append(f"{field} 类型漂移:期望 {want_type.__name__},实得 {type(value).__name__}") elif field == "amount" and not value.isdigit(): problems.append(f"amount 含非数字字符:{value!r}") return problems
第二道,解析层无兜底。金额一律 Decimal,单位进字段名(amount_cent),缺失即抛错。默认值只允许给「业务上确实可为空」的字段,不允许给「解析失败」——混用后事故永远查不到根。
第三道,隔离区。不合格响应连同原始报文落 raw_quarantine 表并告警,不直接丢弃。保留样本是排障速度的保证:上述事故靠隔离表十分钟定位。
主动探针补被动校验的盲区。 低频接口一天没人调,变更不会被发现。给每个上游接口配定时探针:每小时拉一笔固定样例,跑契约校验与指纹比对,异常即通知。多家货源平台并行对接时,每家的字段约定独立监控,混在一起查就无法定位是哪一家改的。
验收标准:
- 手工改字段类型:落库前被拦,告警报出字段名与期望类型。
- 删必填字段:进隔离表,业务表零新增。
- 单位改元:对账报差异,不静默通过。
- 改字段名等一小时:探针发出指纹变更通知。
契约文档是给人看的,契约校验才是给程序看的。前者保证理解一致,后者保证理解不一致时系统先知道。