carrier-contract.php
A. 注册表 fail-closed
✅ A1 easyparcel 已注册
✅ A2 未知 code 不认
✅ A3 未知 code 抛 ERR_UNKNOWN_CARRIER
✅ A4 SELF_PRINT 不是 adapter
✅ A5 labels() 不含 SELF_PRINT
B. 能力声明(Open API 六个能力齐全)
✅ B1 支持取号
✅ B2 支持取面单
✅ B3 支持余额
✅ B4 🔴 支持取消(换代后)
✅ B5 未知承运商不支持任何能力
✅ B6 服务档位是动态的(返回 null)
✅ B7 支持询价
✅ B8 支持轨迹
✅ B9 🔴 声明的每个能力都有对应实现
✅ B10 configFields 由 adapter 声明
C. 州名 → 州代码(Appendix III)
✅ C-Johor
✅ C-Kedah
✅ C-Kelantan
✅ C-Melaka
✅ C-Negeri Sembilan
✅ C-Pahang
✅ C-Pulau Pinang
✅ C-Perak
✅ C-Perlis
✅ C-Selangor
✅ C-Terengganu
✅ C-Sabah
✅ C-Sarawak
✅ C-Kuala Lumpur
✅ C-Labuan
✅ C-Putra Jaya
✅ C-别名 Penang
✅ C-别名 Malacca
✅ C-别名 Putrajaya
✅ C-联邦直辖区全称
✅ C-大小写与空格归一
✅ C-🔴 十六个州码互不相同
✅ C-🔴 库里真实存在的 Choose State 被拒
✅ C-🔴 Others 被拒
✅ C-🔴 Singapore 被拒
✅ C-空州名被拒
D. 长度与必填(超长抛错而不是截断)
✅ D1 正常值原样返回
✅ D2 首尾空白被 trim
✅ D3 恰好等于上限放行
✅ D4 超一个字节即拒
✅ D5 必填为空即拒
✅ D6 非必填为空返回空串
✅ D7 🔴 中文按字节算(12 字 = 36 字节 → 拒)
✅ D8 中文 11 字 = 33 字节 → 放行
✅ D9 重量为 0 被拒
✅ D10 重量缺失被拒
✅ D11 重量负数被拒
✅ D12 重量格式化两位小数
E. 两层错误判定
✅ E1 L1 都是 200 不抛
✅ E2 L1 401 → AUTH
✅ E3 L1 403 → AUTH
✅ E4 L1 400 按 data.errors 细分
✅ E5 🔴 L1 429 → RATE_LIMITED(不是余额不足)
✅ E6 🔴 body 说 200 但 HTTP 500 → 仍然抛
✅ E7 🔴 HTTP 200 但 body 说 401 → 仍然抛
✅ E8 L1 缺 status_code 时按 HTTP 判
✅ E9 L2 余额不足 → INSUFFICIENT
✅ E10 L2 地址不可达 → ADDRESS
✅ E11 L2 邮编问题 → ADDRESS
✅ E12 L2 缺必填 → BAD_PARAM
✅ E13 L2 已取消 → NOT_CANCELLABLE
✅ E14 L2 过了取消期限 → NOT_CANCELLABLE
✅ E15 L2 认不出的归 UPSTREAM,不猜
✅ E16 L2 空错误也不当成功
✅ E17a 🔴 返回值是已登记的错误码
✅ E17b 🔴 返回值不含收件人姓名
✅ E17c 🔴 返回值不含电话
✅ E17d 🔴 返回值不含地址
P. 电话规范化(只做最确定的两件事)
✅ P1 剥 +60 国际前缀
✅ P2 剥前导 0
✅ P3 去掉分隔符
✅ P4 已经是裸号的原样
✅ P5 🔴 未知国家不猜拨号码
✅ P6 新加坡拨号码
✅ P7 国家码统一大写
✅ P8 国家为空回落 MY
L. 面单地址挑选(A6 优先)
✅ L1 🔴 有 A6 优先 A6
✅ L2 没有 A6 退 A4
✅ L3 都没有则用 awb_url
✅ L4 🔴 全空返回空串(面单还没生成)
F. 出参形状(error_hint 不含上游文本)
✅ F1 fail.ok=false
✅ F2 fail.data=null
✅ F3 fail.error_code 原样
✅ F4 fail.error_hint 非空
✅ F5 🔴 error_hint 不含上游原文
✅ F6 ok.ok=true
✅ F7 ok.error_code 为空
✅ F8 shipment 运单号
✅ F9 shipment 真实承运商
✅ F10 shipment 未给的键有默认值
✅ F11 label 归一成字节
G. 配置 name 生成
✅ G1 nameFor
✅ G2 senderNameFor
✅ G3-client_id 🔴 归密文桶
✅ G4-client_id 不在明文桶
✅ G3-client_secret 🔴 归密文桶
✅ G4-client_secret 不在明文桶
✅ G3-access_token 🔴 归密文桶
✅ G4-access_token 不在明文桶
✅ G3-refresh_token 🔴 归密文桶
✅ G4-refresh_token 不在明文桶
✅ G5 🔴 token_expire 归明文桶
✅ G6 寄件人字段含必填的邮编
✅ G7-carrier_easyparcel_token_expire 🔴 在 common.php 里也被当成明文
✅ G7-carrier_easyparcel_account_currency 🔴 在 common.php 里也被当成明文
✅ G7-carrier_easyparcel_env 🔴 在 common.php 里也被当成明文
✅ G7-carrier_easyparcel_account_id 🔴 在 common.php 里也被当成明文
✅ G8 managed 字段清单
✅ G9 🔴 put 拒绝未声明的字段
I. 技术描述与用户提示分开
✅ I1 州名错有用户提示
✅ I2 用户提示里带上是哪个州
✅ I3 🔴 技术错误不产生用户提示
✅ I4 超长提示用中文字段名
✅ I5 🔴 超长提示不含内部字段名
✅ I6 fieldLabel 映射
✅ I6b 🔴 寄件人侧提示的是寄件人
✅ I6c 🔴 两侧的提示不相同
✅ I7 fieldLabel 认不出时原样返回
✅ I8 技术描述仍在 getMessage 供排障
H. 守卫:正常场景没被误伤
✅ H1 合法州名照常通过
✅ H2 合法长度照常通过
✅ H3 合法重量照常通过
✅ H4 成功响应不抛
✅ H7 合法电话照常通过
✅ H8 正常错误消息不会被误判成"不能取消"
✅ H5 已注册承运商照常解析得到
✅ H6 能力表非空
S. 🔴 审计打点的源码形态
✅ S1 🔴 没有 $this->_logAdminOp(它是全局函数)
✅ S2 🔴 没有 \app\AdminOpCode(类在全局命名空间)
✅ S3 🔴 _logAdminOp 第 2 个参数不是数组字面量
✅ S4 守卫:确实扫到了 _logAdminOp 调用
✅ S5 守卫:本次新增的那个 op_code 真的在用
T. 🔴 面单 202 的处置
✅ T1-202 🔴 HTTP 202 判成 LABEL_PENDING(不是 upstream)
✅ T1-400 🔴 HTTP 400 判成 LABEL_PENDING(不是 upstream)
✅ T1-404 🔴 HTTP 404 判成 LABEL_PENDING(不是 upstream)
✅ T1b-401 守卫:401 没被塞进 pending 那一档
✅ T1b-403 守卫:403 没被塞进 pending 那一档
✅ T1b-500 守卫:500 没被塞进 pending 那一档
✅ T1b-502 守卫:502 没被塞进 pending 那一档
✅ T2 🔴 而且排在"非 200 即错误"那一条之前(排后面等于没写)
✅ T3 守卫:确实扫到了 fetchLabel 的正文
✅ T3b 🔴 非 200 的 detail 带上了上游原话
✅ T3c upstreamNote 抠不出结构化字段时原样截,不返回空
✅ T3d upstreamNote 认得 message
✅ T3e 守卫:空体还是空(别拼一个只有分隔符的后缀)
✅ T4 嵌套 {message,code} 能拍平
✅ T5 扁平字符串数组照旧
✅ T6 守卫:空的还是空的(别拍出一个空串条目)
✅ T7 🔴 不再出现字面量 Array
U. 占位面单
✅ U1 姓名打码
✅ U2 电话留前 3 后 2
✅ U3 🔴 详细地址整段打码(门牌号本身就能定位到人)
✅ U4 城市/州保留(否则这张纸看不出发往哪,失去"像面单"的意义)
✅ U5 邮编保留
✅ U6 守卫:打码后仍有内容
✅ U7 非 ASCII 变星号(镜像里没有中文字体,见 DebugLabel 文件头)
✅ U8 PDF 里有特殊含义的括号被转义(否则内容流会断)
✅ U9 是合法 PDF 开头
✅ U10 有 EOF 尾(阅读器认它)
✅ U11 🔴 纸面写着 TEST LABEL
✅ U12 🔴 纸面写着不会被揽收/派送
✅ U13 🔴 纸面写着别贴到真包裹上
✅ U14 带上了对方的真实状态(说明这张纸为什么会存在)
✅ U15 🔴 明文电话没出现在 PDF 里
✅ U16 🔴 明文门牌地址没出现在 PDF 里
✅ U17 🔴 没有条码(扫得动的假面单最危险)
✅ U18 🔴 正式账号下 isEnabled 恒 false
✅ U19 🔴 isEnabled 同时要求 FF_DEBUG_MODE 与沙箱(两者是与,不是或)
✅ U20 守卫:确实扫到了 DebugLabel 的正文
PASS=174 FAIL=0