Files
websopy-taro/.workbuddy/memory/2026-07-02.md
赵忠林 511e4b8ff3 feat(pay): 支付成功后自动跳转到已购产品页面
- 支付成功后显示成功提示
- 延迟1.5秒自动跳转到已购产品页面
- 处理跳转失败时回退到用户首页
- 优化用户支付完成后的使用体验
2026-07-02 22:40:08 +08:00

102 lines
10 KiB
Markdown
Raw 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.

# 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` 注释消除警告。