10 KiB
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存在逻辑 bugrequest()默认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):- 引入
getOpenId和loginByOpenIdAPI handlePay中:Taro.login()→getOpenId(code)换取 openid → 缓存 openid- 尝试
loginByOpenId自动登录获取 token(如果用户未注册,弹出引导登录对话框) mp-prepay请求也加了returnRaw: true和调试日志
- 引入
-
后端修复(
AppSubscriptionController.java):mp-prepay:将userId == null || !userId.equals(sub.getUserId())改为userId != null && !userId.equals(sub.getUserId()),允许未登录用户发起支付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方法):- 从微信返回中拿到
openid/unionid后,立即userParam.setOpenid(openid)和userParam.setUnionid(unionid); - 用户已存在但
user.openid为空时,把 openid/unionid 更新到数据库并返回最新 user; - 这样
LoginResult.user.openid一定有值,前端就能正确获取并传给mp-prepay。
- 从微信返回中拿到
七次排查:mp-prepay 报 SIGN_ERROR(签名错误)
- 现象:前端调用
/api/app/subscription/mp-prepay/{id}时,后端返回code: 1, message: "微信支付服务异常: 微信错误码: SIGN_ERROR, 签名错误"。 - 排查:
- 当前小程序 JSAPI 支付使用
WxNativePayUtil.getConfig构建微信支付 Config,模式为RSAPublicKeyConfig(公钥模式),但生产环境wechatpay-public-key-id为空。 - 读取项目下所有证书文件,用 openssl 检查各证书对应的商户号,发现:
/wechat/websopy/apiclient_cert.pem:商户号1557418831(与小程序无关联)/wechat/10398/apiclient_cert.pem:商户号1246610101(与小程序已关联,见截图)
- 配置中的
mch-id是1246610101,但配置的private-key-relative-path指向wechat/websopy/,导致证书与商户号不匹配,微信返回SIGN_ERROR。 - 检查
10398目录证书与私钥匹配性:公钥哈希一致,证书序列号为48749613B40AA8F1D768583FC352358E13EB5AF0。
- 当前小程序 JSAPI 支付使用
- 修复:
- 修改
WxNativePayUtil.java:将RSAPublicKeyConfig(公钥模式)改为RSAAutoCertificateConfig(自动证书模式),不再依赖wechatpay-public-key-id和wechatpay-cert-relative-path。 - 修正
application-dev.yml和application-prod.yml:mch-id:1246610101(保持不变,这是正确的)merchant-serial-number:48749613B40AA8F1D768583FC352358E13EB5AF0(对应 10398 目录证书)private-key-relative-path:wechat/10398/apiclient_key.pemwechatpay-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接口,在一个请求中完成:- 创建订阅记录(复用
subscribe的核心逻辑,写入数据库) - 用
subscriptionNo调用WxMiniprogramUtil.generateMiniprogramQrCode生成小程序码 - 返回
subscriptionNo+ Base64 图片
- 创建订阅记录(复用
- 关联修改:
AppPayProperties.java:新增miniAppSecret字段(用于生成小程序码获取 access_token)application-dev.yml/application-prod.yml:添加mini-app-secret配置项(当前为占位符,需用户填写真实秘钥)AppSubscriptionController.javamp-prepay:已改为允许跨用户支付(userId != null && !userId.equals(sub.getUserId()))AppSubscriptionController.javamp-confirm:已改为未登录时跳过 userId 校验
- 下一步:
- 到微信公众平台获取小程序
AppSecret,填写到application-dev.yml和application-prod.yml的mini-app-secret字段 - 重新构建部署后端
- 调用新接口生成支付小程序码:
POST /api/app/subscription/generate-pay-qrcodeBody:{"productId": 1, "subscriptionPeriod": "month", "envVersion": "trial"} - 用返回的
subscriptionNo测试check-status和detail-by-no接口,确认订阅记录已存在
- 到微信公众平台获取小程序
补充修复
AppSubscriptionController.java的generate-pay-qrcode接口中,switch (subscriptionPeriod)的case "month":空 fall through 触发 IDE 红色警告。已添加// fall through to default注释消除警告。