diff --git a/.workbuddy/memory/2026-04-16.md b/.workbuddy/memory/2026-04-16.md new file mode 100644 index 0000000..cf3c159 --- /dev/null +++ b/.workbuddy/memory/2026-04-16.md @@ -0,0 +1,45 @@ +# 2026-04-16 工作记录 + +## 支付回调状态不更新问题诊断与修复 + +**问题接口**: `POST /api/shop/shop-order/notify/{tenantId}` + +### 发现的 Bug + +1. **根因 Bug**:`ShopOrderServiceImpl.updateByOutTradeNo()` 第837行有 `order.setExpirationTime(null)`,强制覆盖了 Controller 中设置的 `expirationTime`(`LocalDateTime.now().plusYears(10)`),导致 XML 中 expirationTime 条件不生效。**已修复**:删除了该行。 + +2. **XML 缺少 `update_time`**:`ShopOrderMapper.xml` 的 `updateByOutTradeNo` SQL 的 `` 块中没有 `update_time = NOW()` 和 `expiration_time` 字段。**已修复**:新增了这两个字段更新。 + +3. **回调地址路由问题**:Controller 路由为 `/notify/{tenantId}`,但测试访问的 `/notify`(无 tenantId)不存在,返回 fail。正确回调地址格式为 `https://glt-api.websoft.top/api/shop/shop-order/notify/{tenantId}`(需带租户ID)。**待检查**:数据库 Payment 表的 `notify_url` 字段是否正确配置了带 tenantId 的完整路径。 + +### 修复文件 +- `src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java` +- `src/main/java/com/gxwebsoft/shop/mapper/xml/ShopOrderMapper.xml` + +--- + +## 支付回调签名验证失败:Transaction 类错误(00:29修复) + +**错误日志关键信息**: +``` +signature verification failed, signType[WECHATPAY2-SHA256-RSA2048] +serial[test] message[test\ntest\n{"test":"test"}] sign[test] +``` + +### 根本原因(最致命) + +`ShopOrderController.java` 导入了 **服务商模式** 的 Transaction 类: +```java +// 错误(服务商模式) +import com.wechat.pay.java.service.partnerpayments.jsapi.model.Transaction; +``` +而 `ShopOrderServiceImpl.java` 下单时用的是**直连商户模式**: +```java +// 正确(直连商户模式) +import com.wechat.pay.java.service.payments.model.Transaction; +``` +两个 Transaction 包路径不同,字段结构有差异(服务商 Transaction 有 spAppid/spMchid 等字段),用错误的类解析回调会导致字段映射失败,交易状态无法正确读取。**已修复**:改为正确的直连商户模式 Transaction。 + +### 次要原因 + +`RSAAutoCertificateConfig` 每次回调都重新 `build()`,SDK 内部会发一次 `serial=test` 的探测验签,网络问题或并发场景下可能导致首次回调失败。**已优化**:添加 `notifyConfigCache`(ConcurrentHashMap)按 mchId 缓存 config,避免重复初始化。 diff --git a/.workbuddy/settings.local.json b/.workbuddy/settings.local.json new file mode 100644 index 0000000..7a6d5dd --- /dev/null +++ b/.workbuddy/settings.local.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "modern-webapp@cb_teams_marketplace": true + } +} \ No newline at end of file diff --git a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java b/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java index 1bd1795..d156858 100644 --- a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java +++ b/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java @@ -35,7 +35,7 @@ import com.wechat.pay.java.core.notification.NotificationConfig; import com.wechat.pay.java.core.notification.NotificationParser; import com.wechat.pay.java.core.notification.RequestParam; import com.wechat.pay.java.core.RSAAutoCertificateConfig; -import com.wechat.pay.java.service.partnerpayments.jsapi.model.Transaction; +import com.wechat.pay.java.service.payments.model.Transaction; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.Operation; @@ -53,6 +53,7 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; /** * 订单控制器 @@ -65,6 +66,8 @@ import java.util.Objects; @RequestMapping("/api/shop/shop-order") public class ShopOrderController extends BaseController { private static final Logger logger = LoggerFactory.getLogger(ShopOrderController.class); + /** 按商户号缓存 NotificationConfig,避免每次回调都重新拉取平台证书 */ + private final ConcurrentHashMap notifyConfigCache = new ConcurrentHashMap<>(); @Resource private ShopOrderService shopOrderService; @Resource @@ -791,90 +794,100 @@ public class ShopOrderController extends BaseController { .body(body) .build(); - // 创建通知配置 - 使用与下单方法相同的证书配置逻辑 + // 创建通知配置 - 使用与下单方法相同的证书配置逻辑(按 mchId 缓存,避免重复拉取平台证书) NotificationConfig config; - try { - if (active.equals("dev")) { - // 开发环境 - 构建包含租户号的私钥路径 - String tenantCertPath = "dev/wechat/" + tenantId; - String privateKeyPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getPrivateKeyFile(); + final String mchId = payment.getMchId(); + config = notifyConfigCache.get(mchId); + if (config == null) { + try { + NotificationConfig newConfig; + if (active.equals("dev")) { + // 开发环境 - 构建包含租户号的私钥路径 + String tenantCertPath = "dev/wechat/" + tenantId; + String privateKeyPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getPrivateKeyFile(); - logger.info("开发环境异步通知证书路径: {}", privateKeyPath); - logger.info("租户ID: {}, 证书目录: {}", tenantId, tenantCertPath); + logger.info("开发环境异步通知证书路径: {}", privateKeyPath); + logger.info("租户ID: {}, 证书目录: {}", tenantId, tenantCertPath); - // 检查证书文件是否存在 - if (!certificateLoader.certificateExists(privateKeyPath)) { - logger.error("证书文件不存在: {}", privateKeyPath); - throw new RuntimeException("证书文件不存在: " + privateKeyPath); + // 检查证书文件是否存在 + if (!certificateLoader.certificateExists(privateKeyPath)) { + logger.error("证书文件不存在: {}", privateKeyPath); + throw new RuntimeException("证书文件不存在: " + privateKeyPath); + } + + String privateKey = certificateLoader.loadCertificatePath(privateKeyPath); + + // 使用验证器获取有效的 APIv3 密钥 + String apiV3Key = wechatPayConfigValidator.getValidApiV3Key(payment); + + logger.info("私钥文件加载成功: {}", privateKey); + logger.info("使用APIv3密钥来源: {}", payment.getApiKey() != null && !payment.getApiKey().trim().isEmpty() ? "数据库配置" : "配置文件默认"); + logger.info("APIv3密钥长度: {}", apiV3Key != null ? apiV3Key.length() : 0); + logger.info("商户证书序列号: {}", payment.getMerchantSerialNumber()); + + // 使用自动证书配置 + newConfig = new RSAAutoCertificateConfig.Builder() + .merchantId(mchId) + .privateKeyFromPath(privateKey) + .merchantSerialNumber(payment.getMerchantSerialNumber()) + .apiV3Key(apiV3Key) + .build(); + + logger.info("✅ 开发环境使用自动证书配置创建通知解析器成功"); + } else { + // 生产环境 - 使用自动证书配置 + final String certRootPath = certConfig.getCertRootPath(); + logger.info("生产环境证书根路径: {}", certRootPath); + + String privateKeyRelativePath = payment.getApiclientKey(); + logger.info("数据库中的私钥相对路径: {}", privateKeyRelativePath); + + // 生产环境已经没有/file目录,所有路径都直接拼接到根路径 + String privateKeyFullPath; + // 处理数据库中可能存在的历史路径格式 + String cleanPath = privateKeyRelativePath; + if (privateKeyRelativePath.startsWith("/file/")) { + // 去掉历史的 /file/ 前缀 + cleanPath = privateKeyRelativePath.substring(6); + } else if (privateKeyRelativePath.startsWith("file/")) { + // 去掉历史的 file/ 前缀 + cleanPath = privateKeyRelativePath.substring(5); + } + // 确保路径以 / 开头 + if (!cleanPath.startsWith("/")) { + cleanPath = "/" + cleanPath; + } + privateKeyFullPath = certRootPath + cleanPath; + + logger.info("生产环境私钥完整路径: {}", privateKeyFullPath); + String privateKey = certificateLoader.loadCertificatePath(privateKeyFullPath); + String apiV3Key = payment.getApiKey(); + + // 使用自动证书配置 + newConfig = new RSAAutoCertificateConfig.Builder() + .merchantId(mchId) + .privateKeyFromPath(privateKey) + .merchantSerialNumber(payment.getMerchantSerialNumber()) + .apiV3Key(apiV3Key) + .build(); + + logger.info("✅ 生产环境使用自动证书配置创建通知解析器成功"); } - - String privateKey = certificateLoader.loadCertificatePath(privateKeyPath); - - // 使用验证器获取有效的 APIv3 密钥 - String apiV3Key = wechatPayConfigValidator.getValidApiV3Key(payment); - - logger.info("私钥文件加载成功: {}", privateKey); - logger.info("使用APIv3密钥来源: {}", payment.getApiKey() != null && !payment.getApiKey().trim().isEmpty() ? "数据库配置" : "配置文件默认"); - logger.info("APIv3密钥长度: {}", apiV3Key != null ? apiV3Key.length() : 0); - logger.info("商户证书序列号: {}", payment.getMerchantSerialNumber()); - - // 使用自动证书配置 - config = new RSAAutoCertificateConfig.Builder() - .merchantId(payment.getMchId()) - .privateKeyFromPath(privateKey) - .merchantSerialNumber(payment.getMerchantSerialNumber()) - .apiV3Key(apiV3Key) - .build(); - - logger.info("✅ 开发环境使用自动证书配置创建通知解析器成功"); - } else { - // 生产环境 - 使用自动证书配置 - final String certRootPath = certConfig.getCertRootPath(); - logger.info("生产环境证书根路径: {}", certRootPath); - - String privateKeyRelativePath = payment.getApiclientKey(); - logger.info("数据库中的私钥相对路径: {}", privateKeyRelativePath); - - // 生产环境已经没有/file目录,所有路径都直接拼接到根路径 - String privateKeyFullPath; - // 处理数据库中可能存在的历史路径格式 - String cleanPath = privateKeyRelativePath; - if (privateKeyRelativePath.startsWith("/file/")) { - // 去掉历史的 /file/ 前缀 - cleanPath = privateKeyRelativePath.substring(6); - } else if (privateKeyRelativePath.startsWith("file/")) { - // 去掉历史的 file/ 前缀 - cleanPath = privateKeyRelativePath.substring(5); - } - // 确保路径以 / 开头 - if (!cleanPath.startsWith("/")) { - cleanPath = "/" + cleanPath; - } - privateKeyFullPath = certRootPath + cleanPath; - - logger.info("生产环境私钥完整路径: {}", privateKeyFullPath); - String privateKey = certificateLoader.loadCertificatePath(privateKeyFullPath); - String apiV3Key = payment.getApiKey(); - - // 使用自动证书配置 - config = new RSAAutoCertificateConfig.Builder() - .merchantId(payment.getMchId()) - .privateKeyFromPath(privateKey) - .merchantSerialNumber(payment.getMerchantSerialNumber()) - .apiV3Key(apiV3Key) - .build(); - - logger.info("✅ 生产环境使用自动证书配置创建通知解析器成功"); + // 放入缓存 + notifyConfigCache.putIfAbsent(mchId, newConfig); + config = notifyConfigCache.get(mchId); + } catch (Exception e) { + logger.error("❌ 创建通知配置失败 - 租户ID: {}, 商户号: {}", tenantId, mchId, e); + logger.error("🔍 错误详情: {}", e.getMessage()); + logger.error("💡 请检查:"); + logger.error("1. 证书文件是否存在且路径正确"); + logger.error("2. APIv3密钥是否配置正确"); + logger.error("3. 商户证书序列号是否正确"); + logger.error("4. 网络连接是否正常"); + throw new RuntimeException("微信支付通知配置失败: " + e.getMessage(), e); } - } catch (Exception e) { - logger.error("❌ 创建通知配置失败 - 租户ID: {}, 商户号: {}", tenantId, payment.getMchId(), e); - logger.error("🔍 错误详情: {}", e.getMessage()); - logger.error("💡 请检查:"); - logger.error("1. 证书文件是否存在且路径正确"); - logger.error("2. APIv3密钥是否配置正确"); - logger.error("3. 商户证书序列号是否正确"); - logger.error("4. 网络连接是否正常"); - throw new RuntimeException("微信支付通知配置失败: " + e.getMessage(), e); + } else { + logger.info("✅ 使用缓存的通知配置 - 商户号: {}", mchId); } // 初始化 NotificationParser diff --git a/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopOrderMapper.xml b/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopOrderMapper.xml index 0c28dbf..fb70756 100644 --- a/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopOrderMapper.xml +++ b/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopOrderMapper.xml @@ -329,6 +329,10 @@ UPDATE shop_order + update_time = NOW(), + + expiration_time = #{param.expirationTime}, + pay_type = #{param.payType}, diff --git a/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java b/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java index da73b60..06a1b3a 100644 --- a/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java +++ b/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java @@ -834,7 +834,6 @@ public class ShopOrderServiceImpl extends ServiceImpl