docs: 添加快手电子凭证对接技术方案
覆盖五大阶段、四个完整业务链路(发码/核销/过期销毁/用户退款)、 双向签名协议、卡券状态流转、16类常见问题和注意事项、 上线前 checklist、配置参考
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
# 快手电子凭证对接 技术方案
|
||||
|
||||
## 一、对接流程总览
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ 五大阶段 │
|
||||
├──────────┬──────────┬──────────┬──────────┬────────────────────┤
|
||||
│ 一、入驻 │ 二、测试 │ 三、上线 │ 四、验证 │ 五、运维 │
|
||||
│ 运营拉群 │ 技术评估 │ 提交数据 │ 功能验证 │ 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
|
||||
```
|
||||
|
||||
```json
|
||||
// 签名源码示例
|
||||
"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 本地功能测试
|
||||
|
||||
```bash
|
||||
cd apps/backend
|
||||
|
||||
# 直接调 handler(不需启动 HTTP 服务)
|
||||
npm run test:industry
|
||||
|
||||
# HTTP 端到端测试(9 项检查)
|
||||
npm run test:industry:http -- --baseUrl=http://127.0.0.1
|
||||
```
|
||||
|
||||
### 5.2 远程联调测试
|
||||
|
||||
```bash
|
||||
# 测试线上环境
|
||||
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 压力测试
|
||||
|
||||
```bash
|
||||
# 发码/销毁 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 已加入快手生产环境白名单
|
||||
|
||||
## 七、配置参考
|
||||
|
||||
```bash
|
||||
# .env 生产配置
|
||||
KUASHOU_INDUSTRY_APP_KEY=ks660621772091030245
|
||||
KUASHOU_INDUSTRY_SIGN_SECRET=OZR3AUW2...
|
||||
KUASHOU_INDUSTRY_ACCESS_TOKEN=ChFvYXV0aC5hY2Nlc3NUb2tlbh... # OAuth 获取
|
||||
KUASHOU_INDUSTRY_SEND_CALLBACK_ENABLED=true
|
||||
KUASHOU_INDUSTRY_RATE_LIMIT_MAX=60000 # 足够大的限流值
|
||||
```
|
||||
Reference in New Issue
Block a user