10 KiB
10 KiB
快手电子凭证对接 技术方案
一、对接流程总览
┌────────────────────────────────────────────────────────────────┐
│ 五大阶段 │
├──────────┬──────────┬──────────┬──────────┬────────────────────┤
│ 一、入驻 │ 二、测试 │ 三、上线 │ 四、验证 │ 五、运维 │
│ 运营拉群 │ 技术评估 │ 提交数据 │ 功能验证 │ Token 管理 │
│ 发文档 │ 创建应用 │ 发布配置 │ 性能验证 │ 退款处理 │
│ │ 联调 │ │ │ 问题排查 │
└──────────┴──────────┴──────────┴──────────┴────────────────────┘
| 阶段 | 操作方 | 关键动作 |
|---|---|---|
| 入驻 | 快手运营 + 商家 | 业务评估、拉群、获取 appKey/appSecret/signSecret |
| 测试 | 商家技术 + 快手技术 | 技术评估、创建应用、白名单、接口对接、联调 |
| 上线 | 商家 + 快手技术 | 提交测试用例、快手审核、发布配置 |
| 验证 | 商家 | 功能验证、性能验证(100 TPS / 500 QPS) |
| 运维 | 商家 | Token 刷新(48h 过期)、退款、日常问题排查 |
二、完整业务链路
链路 1:正常购买 → 发码 → 核销
买家下单快手
│
▼
快手通知商家发码 (send-code) ← 商家接收
├─ oid, num, token, certExpireType, certActualStart/End
└─ result=1 收单成功
│
▼
商家发码回调 快手 (eticket/send) ← 商家主动
├─ oid, etickets[{id,num,validStart/End}], token
└─ result=1 或 result=2004「订单不存在」= 通过
│
▼
快手查询发码结果 (query-code) ← 快手轮询
├─ oid, eticketId(可选), sendType
└─ result=1, etickets[{status:UNUSED|CONSUMED|DESTROYED}]
│
▼
买家到店核销 / 线上核销
│
▼
快手通知核销 (consume-code) ← 商家接收
├─ oid, etickets[{id,num}], status, consumeType
├─ storeName/Address, seriallNum
└─ result=1 成功
│
▼
商家核销回调 快手 (eticket/consume) ← 商家主动
├─ oid, etickets[{id,num,goodsValue}], status, consumeType
├─ token, seriallNum, consumeTime
└─ result=1 或 result=2004 = 通过
链路 2:卡券过期销毁
卡券有效期到期(商家侧判断或快手通知)
│
▼
快手通知销毁 (destroy-code) ← 商家接收
├─ oid, reason=ETICKET_EXPIRED
├─ etickets[{id,num,goodsValue}] (可选)
└─ result=1 (不存在的订单也返回成功!)
│
▼
商家销毁回调 快手 (eticket/destroy) ← 商家主动
├─ oid, etickets[{id,num,goodsValue}], reason=ETICKET_EXPIRED
└─ result=1 或 result=2004 = 通过
链路 3:用户申请退款
买家在快手申请退款
│
▼
快手通知销毁 (destroy-code) ← 商家接收
├─ oid, reason=USER_APPLY_REFUND
├─ etickets[{id,num,goodsValue}]
└─ result=1 (订单不存在也返回成功!)
│
▼
商家销毁回调 快手 (eticket/destroy) ← 商家主动
├─ oid, etickets[{id,num,goodsValue}], reason=USER_APPLY_REFUND
└─ result=1
三、接口协议规范
3.1 双向接口清单
| 方向 | 数量 | 接口 | 路径 |
|---|---|---|---|
| 快手 → 商家 | 4 个 | 通知发码、查询发码、通知销毁、通知核销 | /api/v1/open/kuaishou-industry/* |
| 商家 → 快手 | 3 个 | 发码回调、核销回调、销毁回调 | openapi.kwaixiaodian.com/integration/callback/virtual/eticket/* |
3.2 签名算法(双向统一)
签名步骤:
1. 收集所有参数(appkey, method, version, param, timestamp, signMethod, access_token)
2. 排除 sign 和 signSecret
3. 按 key 字典序排序
4. 用 & 拼接 key=value
5. 末尾追加 &signSecret=xxx
6. MD5( whole_string ) 或 HMAC_SHA256 → sign
// 签名源码示例
"access_token=xxx&appkey=ks660621772091030245&method=integration.callback.virtual.eticket.send¶m={\"oid\":\"2319200000001216\",...}&signMethod=MD5×tamp=1782966971471&version=1&signSecret=OZR3AUW2..."
注意:param 内 JSON 对象的 key 也需要字典序排序。
3.3 错误码规范
| 错误码 | 含义 | 触发场景 |
|---|---|---|
| 1 | 成功 | 正常返回 |
| 4010003 | 系统异常/拒单 | 签名错误、参数缺失、appkey 不匹配 |
| 4012002 | 订单不存在 | 查询/核销不存在的订单 |
| 4012005 | 卡券不存在 | 查询不存在的卡券 ID |
3.4 卡券状态流转
┌─→ CONSUMED (已核销)
│
UNUSED (未使用) ──┼─→ DESTROYED (已销毁)
│
└─→ CANCELLED (订单关闭)
四、对接中遇到的问题和注意事项
4.1 签名问题
| 问题 | 原因 | 解决 |
|---|---|---|
| "签名验证失败" | param JSON 内部 key 未排序 | JSON.stringify 时按 key 排序 |
| "签名验证失败" | signSecret 夹带在请求参数中 | signSecret 只用于签名计算,不能传给快手 |
| "签名验证失败" | param 字段 encode 方式不一致 | 先签名再 URL encode,不是先 encode 再签名 |
| param 为空 | GET 请求 URL 过长被截断 | POST 请求 param 放 body 里 |
4.2 参数问题
| 问题 | 原因 | 解决 |
|---|---|---|
| "需要纯数字oid" | oid 传入了含字母的测试值 | oid 必须是快手生成的纯数字订单号(如 2319200000001216) |
| "appkey 不匹配" | 测试/生产环境 appKey 混用 | 两套环境 appKey 独立,不可混用 |
| "缺少 oid" | 必填参数缺失 | 校验每个接口的必填参数 |
| eticketId 类型不匹配 | 传字符串 vs 数据库 Long | 统一用 String 传递 |
4.3 Token / 鉴权问题
| 问题 | 原因 | 解决 |
|---|---|---|
| "access_token 过期" | token 有效期 48h | 实现 refreshToken 自动刷新机制 |
| "权限不足" | 子账号授权导致 sellerId 错误 | 必须用店主主账号授权 |
| 回调接口 401 | 测试环境 IP 未加白名单 | 联系快手对接人添加出口 IP |
| "fetch failed" | 代理/VPN 劫持了 DNS | 关闭代理或配置直连 openapi.kwaixiaodian.com |
4.4 业务逻辑问题
| 问题 | 原因 | 解决 |
|---|---|---|
| 销毁订单返回失败 | 快手要求"不存在的订单也返回成功" | 找不到订单时 result=1 而非错误码 |
| 重复发码创建重复任务 | 未做幂等校验 | 按 oid 查已有订单和任务数,只补建缺少的 |
| 发码时 num 与 etickets 数量不一致 | 并发或逻辑错误 | sendNum 必须等于 etickets[num] 之和 |
| 销毁已核销卡券 | 状态流转未校验 | 销毁前检查卡券状态,已核销不应允许销毁 |
4.5 环境问题
| 问题 | 原因 | 解决 |
|---|---|---|
| 测试环境调不通 | 测试环境需要出口 IP 白名单 | 提供 IP → 快手对接人(屈国庆/王哲/杨天问) |
| 测试环境 token 和生产混淆 | 两套独立 token | 测试用 gw-merchant-staging.test.gifshow.com,生产用 openapi.kwaixiaodian.com |
| 回调被限流 | 默认 120 req/min | 压测时设 KUASHOU_INDUSTRY_RATE_LIMIT_MAX=60000 |
4.6 重试策略
快手对所有回调失败按指数退避重试:
10s → 30s → 1min → 2min → 3min → 4min → 5min → 6min → 7min → 8min → 9min → 10min → 20min → 30min → 1h → 2h
最多 16 次
因此商家服务必须保证幂等性——同一订单重复通知不应产生副作用。
五、测试方法
5.1 本地功能测试
cd apps/backend
# 直接调 handler(不需启动 HTTP 服务)
npm run test:industry
# HTTP 端到端测试(9 项检查)
npm run test:industry:http -- --baseUrl=http://127.0.0.1
5.2 远程联调测试
# 测试线上环境
npm run test:industry:http -- --baseUrl=https://ks.khhao.com
# 生成快手回调 curl(手动测试)
npm run test:industry:curl # 发码回调
npm run test:industry:curl -- --mode=consume # 核销回调
npm run test:industry:curl -- --mode=destroy # 销毁回调
npm run test:industry:curl -- --mode=destroy --oid=2319200000001216
5.3 压力测试
# 发码/销毁 100 TPS
npm run test:industry:http -- --baseUrl=https://ks.khhao.com --load --tps=100 --duration=30
# 查询 500 QPS
npm run test:industry:http -- --baseUrl=https://ks.khhao.com --load --endpoint=query-code --tps=500 --duration=30
六、上线前 Checklist
- 所有接口错误码按快手规范返回(1 = 成功,4010003/4012002/4012005)
- 发码接口幂等——重复通知不重复创建任务
- 销毁接口——不存在的订单返回 result=1
- 查询接口——不存在订单返回 4012002,不存在卡券返回 4012005
- 三个出站回调(发码/核销/销毁)已实现并配置
KUASHOU_INDUSTRY_SEND_CALLBACK_ENABLED=true - access_token 已通过店主主账号 OAuth 获取,48h 自动刷新
- 生产环境 appKey / signSecret 与测试环境独立配置
- 测试用例文档(
测试用例.md)已填写完整提交快手审核 - 压力测试通过:发码 100 TPS、查询 500 QPS、销毁 100 TPS,平均耗时 < 100ms
- 所有错误场景(签名错/参数缺失/appkey 错)均能正确处理
- signSecret 未提交到 git 仓库,仅通过环境变量注入
- 出口 IP 已加入快手生产环境白名单
七、配置参考
# .env 生产配置
KUASHOU_INDUSTRY_APP_KEY=ks66xxx
KUASHOU_INDUSTRY_SIGN_SECRET=OZR3AUW2...
KUASHOU_INDUSTRY_ACCESS_TOKEN=ChFvYXV0aC5hY2Nlc3NUb2tlbh... # OAuth 获取
KUASHOU_INDUSTRY_SEND_CALLBACK_ENABLED=true
KUASHOU_INDUSTRY_RATE_LIMIT_MAX=60000 # 足够大的限流值