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

155 lines
12 KiB
Markdown
Raw Normal View History

# 抖音 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 注释警告。
## 双端 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、支付接口或商户配置变更。