From 511e4b8ff33dbec06aeaffa3e58d9234a01c22fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Thu, 2 Jul 2026 22:40:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(pay):=20=E6=94=AF=E4=BB=98=E6=88=90?= =?UTF-8?q?=E5=8A=9F=E5=90=8E=E8=87=AA=E5=8A=A8=E8=B7=B3=E8=BD=AC=E5=88=B0?= =?UTF-8?q?=E5=B7=B2=E8=B4=AD=E4=BA=A7=E5=93=81=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支付成功后显示成功提示 - 延迟1.5秒自动跳转到已购产品页面 - 处理跳转失败时回退到用户首页 - 优化用户支付完成后的使用体验 --- .workbuddy/memory/2026-07-02.md | 101 ++++++++++++++++++++++++++++++++ src/passport/pay/index.tsx | 8 +++ 2 files changed, 109 insertions(+) create mode 100644 .workbuddy/memory/2026-07-02.md diff --git a/.workbuddy/memory/2026-07-02.md b/.workbuddy/memory/2026-07-02.md new file mode 100644 index 0000000..8ee842f --- /dev/null +++ b/.workbuddy/memory/2026-07-02.md @@ -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`,从 `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` 注释消除警告。 diff --git a/src/passport/pay/index.tsx b/src/passport/pay/index.tsx index c05240e..417f678 100644 --- a/src/passport/pay/index.tsx +++ b/src/passport/pay/index.tsx @@ -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)