# 抖音 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`,避免生成依赖较新 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、支付接口或商户配置变更。