Files
frontend-app/docs/douyin-pay-integration.md

168 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 抖音 App 支付接入与打包
2026-09-09,基于 develop / b350f1c 修复。
## 本次故障
普通 uni-app App 中调用 uni.requestPayment({ provider: "toutiao" }) 无法找到该通道,
会报 requestPayment:fail service not found。配置 Scheme 白名单不会注册支付 SDK。
后端 prepayId 不能由前端直接当成 pay_token 拼接收银台 URL。
## 统一调用
订单页、订单补付、充值使用 utils/douyin-pay.js 的 appDypayFun(payment),传入
/order/payment 等接口的 data 对象(含 orderNum、douyin),不要传整个 HTTP response。
utils/payUtils.js 保留 douyinPayFun(douyin, orderId, orderNum) 兼容入口,
也委托给同一原生调用与查单逻辑。
适配器支持 douyin 的旧扁平字段以及新 orderInfo 对象/JSON,
最终传给 SDK 的字段为 appid、partnerid、prepayid、package、noncestr、timestamp、sign。
签名仍由服务端产生,前端不自行签名或改写签名字段。
uni_modules/tb-douyin-pay 是本地源码 UTS 插件,不是 uni.requestPayment provider;
无需将它添加成 manifest 的 toutiao 支付通道。
Android 原生依赖由 utssdk/app-android/config.json 声明;
iOS SDK 位于 utssdk/app-ios/Frameworks/DypaySDK.xcframework。
需要将整个插件目录纳入版本控制,不能只提交 utils 和页面。
## 打包与验收
1. 在支持插件配置的 HBuilderX(插件声明最低 4.36)打开 frontend-app。
2. 安装对应 UTS 编译与 App 打包扩展,Android 打包需能解析插件 Maven 依赖。
3. 制作包含本插件的自定义调试基座或完整 Android/iOS 安装包,使用平台登记的包名和签名。
4. 安装新的原生包。单独更新 WGT 或服务器部署无法给旧安装包加入 SDK。
5. 从真实订单调用支付;测试页也可粘贴 /order/payment 成功返回 JSON。
6. 覆盖成功、取消、网络失败、重复点击、回前台、冷启动恢复场景。
仅服务端 /payment/queryResult 返回 state=2 才提示支付成功。
SDK 成功或跳回 App 本身不作为到账依据。结果未知时保留待查订单并阻止重复发起。
人工续付接口返回 paid 时仍走原查单接口,以便补同步业务订单;closed 只解除本地待查记录,不标记成功。
测试页不再提供模拟 pay_token 链接,不在页面日志输出完整签名。
H5/小程序不会调用原生 App SDK。
## 验证边界
2026-09-09 实测:14 项自动化测试通过,6 个 Vue 文件的 SFC/脚本/模板检查通过。
HBuilderX 5.24 执行 App appResource 导出成功,产物位于 unpackage/resources,
包含 tb-douyin-pay 的 Android 原生源码/配置和 iOS SDK 源码/框架。
编译有既有 CSS Autoprefixer 注释警告,不影响此次资源导出。
这次没有构建、安装完整 APK/IPA,也没有进行真机交易。
npm run test:douyin 覆盖参数适配、旧调用入口、服务端确认、重复支付及异常恢复。
这些自动化测试使用模拟 SDK/接口,不会产生真实交易,不能替代完整原生包与真机验收。
## 关闭 App 后继续未支付订单(2026-09-09 补丁)
旧实现把保存的订单一直当作“结果未知”,只查单不允许重新打开收银台。
现在冷启动仍然只查单;用户再次点击抖音支付时,调用
`POST /payment/douyin/resume`(表单参数 `orderNum`),由当前登录身份校验订单归属。
- 服务端确认 `NOTPAY` 且本地订单仍可支付、未过期:复用原支付订单号、金额、到期时间,
获取新的 SDK 签名参数;用户确认“继续上一笔支付”后打开收银台。
- 返回 `paid` 或 `closed`:不调 SDK,只清理该账户对应的本地待查记录。
- `USERPAYING`、未知渠道状态、查单失败、续付接口不可用:仍保留记录,不创建新支付。
- 取消弹窗、账号切换、重复点击:不会发起第二笔支付。
- 只保存原订单号,不缓存签名;兼容修复前已保存的订单号,覆盖购物及充值。
接口返回 `data = { state, orderNum, payment }`,其中 state 为 unpaid/paid/closed/pending,
只有 unpaid 才含 payment(与原 /order/payment 的 SDK 参数结构一致)。
续付接口不创建支付订单、不重新扣抵扣券,也不会自动取消、退款或更改订单到期时间。
**发布顺序:先部署后端续付接口,再更新 App。只更新 App 无法解除旧后端的锁定。**
本次无 SQL、无新增密钥配置。现有原生插件未变化,但仍需使用已包含 SDK 的安装包。
本补丁涉及 App 的 `utils/douyin-pay.js`、`tests/douyin-integration.test.cjs` 及本文档;
后端涉及 `DouyinPaymentResumeService`、`DouyinPayResumeDto`、`DouyinPayService`、
`QueryOrderResult`、`PayController` 和 `DouyinPaymentResumeServiceTest`。
本地验证:`npm run test:douyin` 的 22 项测试通过;后端
`mvn -o -pl hashmall-frontend -am test -Dtest=DouyinPaymentResumeServiceTest,DouyinPayInfoDtoTest,DouyinAppPaySignerTest -Dsurefire.failIfNoSpecifiedTests=false`
编译成功,所选 10 项测试通过(含新增 7 项续付测试)。
提交目标:后端 hashmall/hashmall 的 dev_douyin_tmp,App 为 charles/frontend-app 的 dev_codex。
尚未部署或进行真机实付;以上测试均使用模拟渠道,不产生真实交易。
## 过期关单及支付安全修复(2026-09-09 第二次补丁)
本节覆盖前文旧续付补丁对“过期仍 pending”和“不关单”的行为描述。
- 后端先查单,过期且 NOTPAY 时请求渠道关单、再次确认,成功后结束本地支付与商品/充值订单。查单失败不放行。
- `/payment/douyin/resume` 新增 reason:QUERY_FAILED、CLOSE_UNCONFIRMED、RECONCILIATION_REQUIRED、CHANNEL_PENDING;App 显示相应提示,不暴露后台密钥或完整渠道响应。
- closed 解除缓存的旧支付单,用户确认后按当前页面重新请求支付;如果原商品订单已取消,需返回商品页重新下单。
- 原生错误码不再被完全隐藏,但 SDK 错误/取消不能单独用于判断渠道订单已关闭。
- 插件版本 0.2.0 新增 resetDypay。在服务端确认 paid/closed/unpaid 后重置 UTS/Swift 锁,恢复原生回调丢失场景。迟到回调不作为支付成功依据。
- 续付请求超时 60 秒,查单 30 秒;其他请求默认值不变。
**本次修改了原生插件,必须重新打包 APK/IPA 或自定义基座;不要只对旧基座发布 WGT。**
先更新后端 frontend/paycenter/job/mqconsumer 及实际承载退款的服务,再更新 App。
无 SQL 迁移。历史已关闭但已收款的异常单保留人工核对,不自动恢复发货或重复付款。
前端 `npm run test:douyin` 新增过期旧单放行、取消/切换账号、明确诊断、原生锁恢复等用例;测试不产生真实交易。
最终验证:27 项前端支付测试全部通过;后端 41 项选定支付测试通过,包含真实 H2 事务回滚验证。
HBuilderX 5.24 App 资源编译及导出成功(有既有 CSS 注释警告)。
资源导出通过不代表 Android/iOS 原生构建或真机交易已验证,发布前仍须完成验收。
## 与现有支付交互对齐(2026-09-09 第三次补丁)
本节取代前文“显式确认/继续支付/重新支付弹窗”的交互说明,遵循用户要求,不新增确认弹窗。
- 用户点击支付即继续:原单有效时直接续付;渠道确认旧单关闭后,直接继续当前页面的下单请求。保留服务端防重复支付校验。
- 微信、支付宝和抖音共用现有支付成功页 `pages/order_package/order_submit_ok/order_submit_ok`,不新建成功页,不仅显示成功 Toast。
- 抖音正常回调、再次点击查到已支付、冷启动查到已支付,均在服务端确认后跳转成功页。
- 从抖音返回 App 时,即使 SDK 回调丢失,也允许 onShow 查单结束等待;与迟到 SDK 回调竞争时只处理一次。查单失败或 pending 不跳成功、不解锁。
- 页面栈满时使用 redirectTo 进入同一成功页;两种跳转均失败则保留待查记录,下次重试跳转,不再次付款。
- 本次仅修改前端 JavaScript、测试及文档,不改后端、接口、SQL、原生插件和支付页面样式。仍依赖上一补丁的后端与原生插件 0.2.0。
本地 33 项支付测试通过,涵盖无弹窗、成功跳转、丢失回调、回调竞争、账号切换、页面栈满以及微信/支付宝成功页回归。尚未进行真机实付。
HBuilderX 5.24 App 资源编译及导出成功,仅有既有 CSS Autoprefixer 注释警告。
## 失败与杀进程恢复页面对齐(2026-09-09 第四次补丁)
按用户要求直接复用微信、支付宝的支付失败页及其“确认”操作,不增加弹窗,不新增页面。
- SDK 取消/调用失败后,先查询支付结果;已确认支付成功仍进入成功页,其余本次未完成的支付进入现有失败页。
- 支付时杀死 App,重新打开后读取原订单并查单:已支付进入成功页;已关闭/未完成进入失败页,不自动唤起 SDK。
- 旧单关闭的点击直接结束在失败页,不在失败页背后同一次操作再创建新单;后续用户主动点击才继续新支付。
- 页面跳转只结束本次客户端支付交互,不代表服务端订单被置为失败。未知/网络异常仍保留原订单号并用已有 Toast 提示待确认,后续到账仍可查到成功;不自动关单、退款或丢弃待查记录。
- 同一进程中同一未完成订单只自动展示一次失败页,避免每次回前台又强制跳回。新的主动支付尝试重新计数。
- 成功/失败共用导航封装,页面栈满时尝试 redirectTo;跳转失败保留恢复记录。微信、支付宝仍使用原来的成功/失败页面。
39 项前端测试通过,包含杀进程后新运行上下文恢复、取消/原生错误、离线恢复、失败跳转重试、迟到成功及微信/支付宝失败页回归。
本次只改 JavaScript、测试和文档,不改后端、原生插件、SQL、接口或页面样式。尚未真机实付。
HBuilderX 5.24 App 资源编译与导出成功,只有既有 CSS Autoprefixer 注释警告。
## 当前订单与缓存旧单隔离(2026-09-10)
本节覆盖前文“点击支付先恢复账号缓存旧单”的说明。原逻辑会在取消 200 元订单后,购买其他 0.2 元商品时再次唤起 200 元旧单,必须移除。
- 与微信/支付宝一致,`prepareDouyinPay` 仅检查 App、登录、客户端可用性及正在进行的调用,不从账号缓存中选订单,不续付、不关单、不跳旧单结果页。
- 商品结算、充值、订单详情仍分别调用现有业务接口,以本次接口返回的数据调用 `appDypayFun`,旧单不再拦截当前下单。
- 只有当前响应 `payment.orderNum` 与缓存订单号完全一致时才查单续付;缓存属于另一单时,验证当前签名参数,释放残留客户端回调,再调用当前订单的 SDK 参数。金额和签名不在客户端改写。
- 成功/失败继续使用微信/支付宝的现有结果页,不新增弹窗。实际尚在运行的 SDK 调用仍防重复点击;迟到的旧回调不得处理新订单。
- 缓存只用于最近一次支付的 App 恢复,不是用户待支付订单清单。新支付发起后恢复记录指向新订单;旧订单仍保留在服务端,由回调、定时查单、订单详情处理,不自动关单或标记失败。若新参数无效/初始化失败,旧恢复记录不覆盖。
验证:`node --test tests/douyin-integration.test.cjs tests/douyin-uts.test.cjs`(需设置下节所述 HBUILDERX_HOME)51 项通过。新增测试经过真实前端 API 包装函数验证商品结算、充值和订单补付的当前响应,SDK/服务端均使用模拟数据,不产生真实交易。HBuilderX 5.24 App 资源编译、导出通过,仅有既有 CSS 注释警告。
本次仅修改 `utils/douyin-pay.js`、回归测试和文档,无后端、SQL、接口、原生插件变更。仍需真机验收“200 元取消 → 0.2 元支付”的收银台金额与订单归属;不能把模拟测试当作实付验证。
## 双端 UTS 编译兼容(2026-09-10,插件 0.2.1)
基于用户确认的远端 `dev_codex` / `3e2e182`,不改变支付页面或后端逻辑。
- 修复 `common.uts` 在 iOS 生成 Swift 时局部变量被推断为 `PayInfo?`:先判空,再明确声明 `PayInfo`;字段值先收集为非空字符串后验证,不修改签名原文。
- `JSON.stringify` 的可空返回值在加锁前处理;序列化失败直接结束,不把 `String?` 传入原生支付接口,也不会占用支付锁。
- 使用显式 `new Array<string>`,避免生成依赖较新 Android 运行时的 `_uA` 数组辅助函数,同时让 Swift 元素类型明确。
- Android 与 iOS 共用修复。Android 的 DyPay 类及方法已通过官方 Maven `com.bytedance.caijing:dy-pay-sdk-tob:1.1.0.9` 的真实 classes.jar 核验,不改 SDK 包名、不使用反射绕过错误。
验证:
```powershell
# 使用实际安装路径;资源导出与 Kotlin 依赖可以来自不同安装目录。
$env:HBUILDERX_HOME = '你的 HBuilderX 安装目录'
npm run test:douyin
npm run test:douyin:uts
# 先通过 HBuilderX 导出 APP 资源,再使用已安装 Android UTS 扩展的目录。
# SdkJar 是从上述官方 AAR 中解压出的 classes.jar。
./tests/compile-douyin-android.ps1 -HBuilderXHome '含 Android UTS 扩展的 HBuilderX 目录' -SdkJar 'SDK classes.jar 的绝对路径'
```
本地结果:39 项页面/业务回归 + 5 项实际共享 UTS 逻辑测试全部通过;HBuilderX 5.24 App 资源导出通过;生成的 Kotlin 与原始 `TbDouyinPayNative.kt` 使用真实 Android/UTS/DyPay 依赖编译成功。Kotlin 中跨平台空值兜底产生的“左侧非空”提示是警告。
验证边界:UTS 逻辑测试转译为 JS,不等于原生测试;Kotlin 编译不等于完整 APK 打包。iOS 已检查生成 Swift 的 `PayInfo` 和 `String` 类型,但 Windows 无 Xcode,未完成 IPA 原生编译或真机交易。仍须重新云打包 Android/iOS,验收正常支付、取消、杀进程恢复及成功/失败跳转。插件 0.2.1 必须随完整 APK/IPA 或自定义基座发布,不能仅用 WGT 更新旧插件。无后端、SQL、支付接口或商户配置变更。