feat(pay): 支付成功后自动跳转到已购产品页面

- 支付成功后显示成功提示
- 延迟1.5秒自动跳转到已购产品页面
- 处理跳转失败时回退到用户首页
- 优化用户支付完成后的使用体验
This commit is contained in:
2026-07-02 22:40:08 +08:00
parent 1d5bb5b9b1
commit 511e4b8ff3
2 changed files with 109 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
# 2026-07-02 工作日志
## 小程序扫码支付"获取订单失败"问题排查与修复
- **问题**:用户扫小程序码后进入小程序支付页,提示"获取订单失败"
- **根因**:后端 Spring Security 的 SecurityConfig 中未将支付相关接口加入白名单(permitAll),扫码进入时用户未登录态,请求 `/api/app/subscription/detail-by-no/**` 被 401 拦截
- **修复**:在 SecurityConfig.java 的 antMatchers 中添加了以下接口白名单:
- `/api/app/subscription/detail-by-no/**` — 根据订阅号查询订单详情
- `/api/app/subscription/mp-prepay/**` — 创建小程序预支付订单
- `/api/app/subscription/mp-confirm/**` — 支付成功确认
- `/api/app/subscription/wx-notify/**` — 微信支付回调通知
- `/api/wx-login/**` — 小程序码生成等微信登录相关接口(之前只覆盖了 `/api/shop/wx-login/**`,漏掉了 `/api/wx-login/**`
- **前端代码位置**`src/passport/pay/index.tsx`,支付页面从 scene/subscriptionNo 获取订单号,调 detail-by-no 接口
- **后端代码位置**`AppSubscriptionController.java`@RequestMapping("/api/app/subscription"))`WxLoginController.java`@RequestMapping("/api/wx-login"))
- **注意**:生成小程序码时 `envVersion` 默认为 `trial`(体验版),上线前需改为 `release`
### 后续排查:后端白名单加了仍报"获取订单失败"
- **二次根因**:前端 `src/passport/pay/index.tsx``fetchDetail` 存在逻辑 bug
- `request()` 默认 `returnRaw=false`,响应拦截器对 `code===0` 的响应已自动拆包,只返回 `data` 部分(订阅对象)
-`fetchDetail` 又检查 `res?.code === 200 || res?.code === 0`,而拆包后的 `res`(订阅对象)没有 `code` 字段
- 因此条件永远为 `false`,走进 `setErrorMsg` 分支,显示"获取订单失败"
- **修复**:移除多余的 `code` 检查,`request` 成功即代表业务成功,直接使用返回的 `data` 对象;错误由 catch 处理
- **附加**`getSubscriptionNo` 加了 `console.log` 打印页面参数和解析结果,方便真机调试排查 scene 传入问题
### 五次排查getOpenId 返回 success:true 但 openid 为 undefined
- **根因**:前端 `src/api/passport/wx-login/index.ts``getOpenId` 函数错误地假设后端返回的是 `{ openid, unionid, session_key }` 对象,直接取 `res.data.openid`;但后端 `/wx-login/getOpenId` 实际返回的是 `LoginResult` 结构 `{ access_token, user }`,真正的 openid 在 `user.openid`
- **修复**:更新 `getOpenId` 类型为 `ApiResult<WxLoginResult>`,从 `res.data.user?.openid` 提取 openid同时把 `access_token``user` 一起返回给调用方复用
- **后续影响**`handlePay` 中拿到 openid 后,因为 `getOpenId` 已经自动注册/登录(后端会注册新用户并签发 token理论上用户已经自动登录可能无需再调 `loginByOpenId`;但当前代码保留双保险
- **根因**`handlePay` 流程有 bug——调了 `Taro.login()` 获取 code但没有用 code 去换 openid直接从 `Taro.getStorageSync('openid')` 读取扫码用户可能没登录过storage 里没存)
- **前端修复**`src/passport/pay/index.tsx`
1. 引入 `getOpenId``loginByOpenId` API
2. `handlePay` 中:`Taro.login()``getOpenId(code)` 换取 openid → 缓存 openid
3. 尝试 `loginByOpenId` 自动登录获取 token如果用户未注册弹出引导登录对话框
4. `mp-prepay` 请求也加了 `returnRaw: true` 和调试日志
- **后端修复**`AppSubscriptionController.java`
1. `mp-prepay`:将 `userId == null || !userId.equals(sub.getUserId())` 改为 `userId != null && !userId.equals(sub.getUserId())`,允许未登录用户发起支付
2. `mp-confirm`:同理,未登录时跳过 userId 校验,用 `effectiveUserId = userId != null ? userId : sub.getUserId()` 替代原 `userId`
- **修改点1**`src/passport/pay/index.tsx``fetchDetail` 改为 `returnRaw: true`,拿到接口完整响应(含 code/message/data手动判断业务状态码`code === 0 || code === 200`),不再依赖拦截器拆包
- **修改点2**`fetchDetail``getSubscriptionNo` 加了详细 console.log/console.error打印
- 原始页面参数scene、subscriptionNo
- 请求 URL 和当前 token
- 接口完整返回 JSON
- catch 中的 err.name/type/code/message/data
- **修改点3**`src/utils/request.ts` 响应拦截器去掉了 `process.env.NODE_ENV === 'development'` 条件限制,所有环境都打印日志,方便真机调试 Console 查看
### 六次排查getOpenId 返回 success:true 但 openid 为 null
- **现象**:前端 `getOpenId` 返回 `success: true, access_token: 有值, user.userId: 35619`,但 `openid: null, unionid: null`
- **根因**:后端 `/wx-login/getOpenId` 虽然通过微信 `jscode2session` 换到了 openid但**没有把 openid 设置到 `UserParam` 中**。
- 后续 `userService.getByOauthId(userParam)` 查找用户时userParam.openid 为空,可能无法命中已有用户;
- 新用户注册时走 `addUser(userParam)`,而 `addUser` 只在 `userParam.openid` 非空时才会写 `User.openid`,所以新用户 openid 也为空;
- 对于已存在但 openid 为空的老用户如手机号注册直接返回openid 仍然是 null。
- **修复**`WxLoginController.java``/getOpenId` 方法):
1. 从微信返回中拿到 `openid`/`unionid` 后,立即 `userParam.setOpenid(openid)``userParam.setUnionid(unionid)`
2. 用户已存在但 `user.openid` 为空时,把 openid/unionid 更新到数据库并返回最新 user
3. 这样 `LoginResult.user.openid` 一定有值,前端就能正确获取并传给 `mp-prepay`
### 七次排查mp-prepay 报 SIGN_ERROR签名错误
- **现象**:前端调用 `/api/app/subscription/mp-prepay/{id}` 时,后端返回 `code: 1, message: "微信支付服务异常: 微信错误码: SIGN_ERROR, 签名错误"`
- **排查**
1. 当前小程序 JSAPI 支付使用 `WxNativePayUtil.getConfig` 构建微信支付 Config模式为 `RSAPublicKeyConfig`(公钥模式),但生产环境 `wechatpay-public-key-id` 为空。
2. 读取项目下所有证书文件,用 openssl 检查各证书对应的商户号,发现:
- `/wechat/websopy/apiclient_cert.pem`:商户号 `1557418831`(与小程序无关联)
- `/wechat/10398/apiclient_cert.pem`:商户号 `1246610101`(与小程序已关联,见截图)
3. 配置中的 `mch-id``1246610101`,但配置的 `private-key-relative-path` 指向 `wechat/websopy/`,导致证书与商户号不匹配,微信返回 `SIGN_ERROR`
4. 检查 `10398` 目录证书与私钥匹配性:公钥哈希一致,证书序列号为 `48749613B40AA8F1D768583FC352358E13EB5AF0`
- **修复**
1. 修改 `WxNativePayUtil.java`:将 `RSAPublicKeyConfig`(公钥模式)改为 `RSAAutoCertificateConfig`(自动证书模式),不再依赖 `wechatpay-public-key-id``wechatpay-cert-relative-path`
2. 修正 `application-dev.yml``application-prod.yml`
- `mch-id``1246610101`(保持不变,这是正确的)
- `merchant-serial-number``48749613B40AA8F1D768583FC352358E13EB5AF0`(对应 10398 目录证书)
- `private-key-relative-path``wechat/10398/apiclient_key.pem`
- `wechatpay-cert-relative-path` 置空(自动证书模式)
- **待确认**:当前保留的 `api-v3-key: "zGufUcqa7ovgxRL0kF5OlPr482EZwtn9"` 是否属于商户号 `1246610101`,需要用户在微信支付商户后台确认。如果仍报签名错误,需要替换为正确的 APIv3 密钥。
- **待处理**:生产环境需将 `/Users/gxwebsoft/JAVA/websopy-java/src/main/resources/wechat/10398/apiclient_key.pem` 上传到 `/www/wwwroot/file.ws/wechat/10398/apiclient_key.pem`,并确保该文件可被 Java 进程读取。之前错误上传 `wechat/websopy/` 目录的证书给 1246610101 使用,这是 `SIGN_ERROR` 的真正原因。
### 八次排查SUB202607022056385942 提示"订阅不存在"
- **现象**:调用 `/api/_app/subscription/check-status/SUB202607022056385942``/api/_app/subscription/detail-by-no/SUB202607022056385942` 均返回"订阅不存在"
- **根因**`getOrderQRCodeUnlimited` 接口WxLoginController.java 第 441 行)**只生成小程序码,并不创建订阅记录**。如果生成小程序码时没有先调用 `subscribe` 接口,`subscriptionNo` 就只是个随机字符串,没有对应的数据库记录
- **修复(方案 1**:新增 `POST /api/app/subscription/generate-pay-qrcode` 接口,在一个请求中完成:
1. 创建订阅记录(复用 `subscribe` 的核心逻辑,写入数据库)
2.`subscriptionNo` 调用 `WxMiniprogramUtil.generateMiniprogramQrCode` 生成小程序码
3. 返回 `subscriptionNo` + Base64 图片
- **关联修改**
- `AppPayProperties.java`:新增 `miniAppSecret` 字段(用于生成小程序码获取 access_token
- `application-dev.yml` / `application-prod.yml`:添加 `mini-app-secret` 配置项(当前为占位符,需用户填写真实秘钥)
- `AppSubscriptionController.java` `mp-prepay`:已改为允许跨用户支付(`userId != null && !userId.equals(sub.getUserId())`
- `AppSubscriptionController.java` `mp-confirm`:已改为未登录时跳过 userId 校验
- **下一步**
1. 到微信公众平台获取小程序 `AppSecret`,填写到 `application-dev.yml``application-prod.yml``mini-app-secret` 字段
2. 重新构建部署后端
3. 调用新接口生成支付小程序码:`POST /api/app/subscription/generate-pay-qrcode` Body: `{"productId": 1, "subscriptionPeriod": "month", "envVersion": "trial"}`
4. 用返回的 `subscriptionNo` 测试 `check-status``detail-by-no` 接口,确认订阅记录已存在
### 补充修复
- `AppSubscriptionController.java``generate-pay-qrcode` 接口中,`switch (subscriptionPeriod)``case "month":` 空 fall through 触发 IDE 红色警告。已添加 `// fall through to default` 注释消除警告。

View File

@@ -229,6 +229,14 @@ const PayPage: React.FC = () => {
setPaid(true)
Taro.showToast({ title: '支付成功', icon: 'success' })
// 8. 延迟自动跳转到已购产品页面
setTimeout(() => {
Taro.navigateTo({ url: '/user/apps/index' }).catch(() => {
// 如果 navigateTo 失败(比如在 tab 页),回退到用户页
Taro.switchTab({ url: '/pages/user/user' })
})
}, 1500)
} catch (err: any) {
console.error('[PayPage] 支付失败:', err)
console.error('[PayPage] err.name:', err?.name, 'err.type:', err?.type)