Files
order_site/docs/行业电子凭证/技术方案.md
T
yml2213 ccb5b6f4d6 docs: 清理过时文档(以代码为准)
删除(整篇已被代码淘汰 / 重复副本 / 与仓库无关):
- cloudflared_tunnel.txt:全仓无 cloudflared 配置,个人终端记录,无任何引用
- 履约配置/工程债-lewan拆分与短链.md:文中 services/kuaishou-cloud 与 kuaishou-cloud-executor 已删除
- 履约配置/快手Cloud领取mock.md:mock:claim / mock:feifei 与 dev-mock 卡片已移除(只剩 mock:open91)
- 行业电子凭证/解决方案/技术方案/1.基础技术.md(+ 其孤立 image.png):0.基础技术.md 的 75 行截断副本
- 寄卖库存功能设计与实施方案.md:手工入库已 409 停用、收集链接表已随 079 删除、统计看板未实现
- 服务器图片迁移到阿里云OSS.md:MinIO→OSS 迁移已完成(生产 STORAGE_MODE=oss)
- pgBackRest备份方案分析.md:全仓无 pgBackRest 落地痕迹,文档自述暂缓实施
- finance-implementation-plan.md:财务模块已落地(072 迁移 / RBAC / 页面接口),已实现部分与代码重复
  (未落地的二期三期路线图可从本提交的历史版本取回)

改写为与代码一致(已下线通道不再当作现行能力):
- 履约配置/多发货平台与电子凭证整体链路.md:kuaishou-cloud / kuaishou-feifei 标注为已下线 legacy stub,
  现行自动通道 affiliate-dash、人工 manual_dispatch;核销时机与券表按实现校正
- 履约配置/{UID领取与旧单兼容, open91请求, TypeScript类型收紧}.md:lewan/feifei/cloudtentacles 表述改为
  现行 affiliate-dash 链路;normalize 具名类型 KuaishouCloudFlow/FeifeiFlow → AffiliateDashFlow
-  affiliate_dash_发货平台/affiliate-dash对接.md:§13/§14/§19/§21/§24/§25 的已删文件与已下线通道引用校正,
  §21/§22 加「当时快照 / 落地后现状」横幅,回调去重表按 013_affiliate_dash_webhook_events.sql 改写
- 91卡券/{3.异步卡密下单, 5.当前项目对接配置, 6.开发设计}.md:删掉不存在的后台入口、
  cloudtentacles/kuaishou_ct_assisted 依赖、cardsEncoding 与 .js 代码落点校正
- 行业电子凭证/技术方案.md:checklist 指向不存在的 测试用例.md → gen-test-cases.ts + 5.测试指南.md
- 履约配置/cloudtentacles发货平台.md:补「本仓库未接入该通道」说明(保持外部参考)
- 部署启动与数据库重建.md:补齐生产必需变量 APP_DOMAIN / CORS_ALLOWED_ORIGINS / STORAGE_MODE / STORAGE_OSS_*

其它:
- README.md:迁移序号范围 002~009 → 002~080
- 寄卖系统迁移079上库前检查.sql:第 2 段注释按批次 Y 的自动去重更新;新增 2.1「重复退款风险」只读排查
  (已在一次性 PostgreSQL 16 上跑通 001→080 迁移:整份脚本 exit=0,脏数据下 2/2.1 均按预期报警)
- 寄卖系统优化方案.md:删掉链路图里已下线的「客服手工录入」,去掉指向已删设计稿的引用
2026-09-17 16:57:27 +08:00

10 KiB
Raw 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 与测试环境独立配置
  • 测试用例已用 apps/backend/scripts/gen-test-cases.ts(npm run test:industry:gen)生成并整理完整,可提交快手审核(测试步骤见 docs/行业电子凭证/5.测试指南.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                           # 足够大的限流值