Files
2026-07-07 08:51:23 +08:00

10 KiB
Raw Permalink Blame History

快手电子凭证对接 技术方案

一、对接流程总览

┌────────────────────────────────────────────────────────────────┐
│                        五大阶段                                 │
├──────────┬──────────┬──────────┬──────────┬────────────────────┤
│ 一、入驻  │ 二、测试  │ 三、上线  │ 四、验证  │ 五、运维            │
│ 运营拉群  │ 技术评估  │ 提交数据  │ 功能验证  │ 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&param={\"oid\":\"2319200000001216\",...}&signMethod=MD5&timestamp=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                           # 足够大的限流值