Compare commits

...

2 Commits

Author SHA1 Message Date
fa5260d583 fix(order): 修改配送员提成直接入账逻辑
- 配送员提成由先入冻结金额 freeze_money 改为直接加入可提现余额 money
- 更新两个 LambdaUpdateWrapper SQL 语句,修改相关字段及注释
- total_money 字段保持累计不变
- 修复 Transaction 类路径和字段结构导致的回调字段映射失败问题
- 优化回调通知配置缓存,避免重复初始化带来的网络请求失败风险
2026-04-16 01:17:23 +08:00
0c4bdc3031 fix(shop-order): 修复支付回调签名验证失败及状态更新问题
- 修正导入 Transaction 类为直连商户模式路径,解决签名验证失败
- 新增按 mchId 缓存 NotificationConfig,避免重复拉取平台证书和重复初始化
- 更新 ShopOrderMapper.xml,增加 update_time 和 expiration_time 字段更新
- 删除 ShopOrderServiceImpl.updateByOutTradeNo 中重置 expirationTime 的代码,确保回调传递值生效
- 补充日志,完善异步通知证书配置流程监控
2026-04-16 00:33:20 +08:00
7 changed files with 162 additions and 86 deletions

View File

@@ -0,0 +1,55 @@
# 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 的 `<set>` 块中没有 `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。
---
## 配送员提成直接入账01:15修改
**文件**`src/main/java/com/gxwebsoft/glt/service/impl/GltTicketOrderServiceImpl.java`
**变更**配送员提成ticketOrderId 关联送水订单)从进入 `freeze_money` 改为直接进入 `money`(可提现余额)。修改了 2 处 `LambdaUpdateWrapper` SQL`freeze_money``money`),注释同步更新。`total_money` 不变(仍累计)。
---
### 次要原因
`RSAAutoCertificateConfig` 每次回调都重新 `build()`SDK 内部会发一次 `serial=test` 的探测验签,网络问题或并发场景下可能导致首次回调失败。**已优化**:添加 `notifyConfigCache`ConcurrentHashMap按 mchId 缓存 config避免重复初始化。

View File

@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"modern-webapp@cb_teams_marketplace": true
}
}

View File

@@ -4,10 +4,10 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>com.gxwebsoft</groupId> <groupId>com.gxwebsoft</groupId>
<artifactId>glt-api</artifactId> <artifactId>mp-api</artifactId>
<version>1.0</version> <version>1.0</version>
<name>glt-api</name> <name>mp-api</name>
<description>WebSoftApi project for Spring Boot</description> <description>WebSoftApi project for Spring Boot</description>
<parent> <parent>

View File

@@ -845,13 +845,13 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
return; return;
} }
// 送水订单提成:先入冻结金额 freeze_money与分销订单佣金一致 // 送水订单提成:直接入账可提现余额 money配送员提成即时可用无需冻结
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
boolean updated = shopDealerUserService.update( boolean updated = shopDealerUserService.update(
new LambdaUpdateWrapper<ShopDealerUser>() new LambdaUpdateWrapper<ShopDealerUser>()
.eq(ShopDealerUser::getTenantId, tenantId) .eq(ShopDealerUser::getTenantId, tenantId)
.eq(ShopDealerUser::getUserId, riderId) .eq(ShopDealerUser::getUserId, riderId)
.setSql("freeze_money = IFNULL(freeze_money,0) + " + money.toPlainString()) .setSql("money = IFNULL(money,0) + " + money.toPlainString())
.setSql("total_money = IFNULL(total_money,0) + " + money.toPlainString()) .setSql("total_money = IFNULL(total_money,0) + " + money.toPlainString())
.set(ShopDealerUser::getUpdateTime, now) .set(ShopDealerUser::getUpdateTime, now)
); );
@@ -895,7 +895,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
new LambdaUpdateWrapper<ShopDealerUser>() new LambdaUpdateWrapper<ShopDealerUser>()
.eq(ShopDealerUser::getTenantId, tenantId) .eq(ShopDealerUser::getTenantId, tenantId)
.eq(ShopDealerUser::getUserId, riderId) .eq(ShopDealerUser::getUserId, riderId)
.setSql("freeze_money = IFNULL(freeze_money,0) + " + money.toPlainString()) .setSql("money = IFNULL(money,0) + " + money.toPlainString())
.setSql("total_money = IFNULL(total_money,0) + " + money.toPlainString()) .setSql("total_money = IFNULL(total_money,0) + " + money.toPlainString())
.set(ShopDealerUser::getUpdateTime, now) .set(ShopDealerUser::getUpdateTime, now)
); );

View File

@@ -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.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam; import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.core.RSAAutoCertificateConfig; 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.tags.Tag;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -53,6 +53,7 @@ import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* 订单控制器 * 订单控制器
@@ -65,6 +66,8 @@ import java.util.Objects;
@RequestMapping("/api/shop/shop-order") @RequestMapping("/api/shop/shop-order")
public class ShopOrderController extends BaseController { public class ShopOrderController extends BaseController {
private static final Logger logger = LoggerFactory.getLogger(ShopOrderController.class); private static final Logger logger = LoggerFactory.getLogger(ShopOrderController.class);
/** 按商户号缓存 NotificationConfig避免每次回调都重新拉取平台证书 */
private final ConcurrentHashMap<String, NotificationConfig> notifyConfigCache = new ConcurrentHashMap<>();
@Resource @Resource
private ShopOrderService shopOrderService; private ShopOrderService shopOrderService;
@Resource @Resource
@@ -791,9 +794,13 @@ public class ShopOrderController extends BaseController {
.body(body) .body(body)
.build(); .build();
// 创建通知配置 - 使用与下单方法相同的证书配置逻辑 // 创建通知配置 - 使用与下单方法相同的证书配置逻辑(按 mchId 缓存,避免重复拉取平台证书)
NotificationConfig config; NotificationConfig config;
final String mchId = payment.getMchId();
config = notifyConfigCache.get(mchId);
if (config == null) {
try { try {
NotificationConfig newConfig;
if (active.equals("dev")) { if (active.equals("dev")) {
// 开发环境 - 构建包含租户号的私钥路径 // 开发环境 - 构建包含租户号的私钥路径
String tenantCertPath = "dev/wechat/" + tenantId; String tenantCertPath = "dev/wechat/" + tenantId;
@@ -819,8 +826,8 @@ public class ShopOrderController extends BaseController {
logger.info("商户证书序列号: {}", payment.getMerchantSerialNumber()); logger.info("商户证书序列号: {}", payment.getMerchantSerialNumber());
// 使用自动证书配置 // 使用自动证书配置
config = new RSAAutoCertificateConfig.Builder() newConfig = new RSAAutoCertificateConfig.Builder()
.merchantId(payment.getMchId()) .merchantId(mchId)
.privateKeyFromPath(privateKey) .privateKeyFromPath(privateKey)
.merchantSerialNumber(payment.getMerchantSerialNumber()) .merchantSerialNumber(payment.getMerchantSerialNumber())
.apiV3Key(apiV3Key) .apiV3Key(apiV3Key)
@@ -857,8 +864,8 @@ public class ShopOrderController extends BaseController {
String apiV3Key = payment.getApiKey(); String apiV3Key = payment.getApiKey();
// 使用自动证书配置 // 使用自动证书配置
config = new RSAAutoCertificateConfig.Builder() newConfig = new RSAAutoCertificateConfig.Builder()
.merchantId(payment.getMchId()) .merchantId(mchId)
.privateKeyFromPath(privateKey) .privateKeyFromPath(privateKey)
.merchantSerialNumber(payment.getMerchantSerialNumber()) .merchantSerialNumber(payment.getMerchantSerialNumber())
.apiV3Key(apiV3Key) .apiV3Key(apiV3Key)
@@ -866,8 +873,11 @@ public class ShopOrderController extends BaseController {
logger.info("✅ 生产环境使用自动证书配置创建通知解析器成功"); logger.info("✅ 生产环境使用自动证书配置创建通知解析器成功");
} }
// 放入缓存
notifyConfigCache.putIfAbsent(mchId, newConfig);
config = notifyConfigCache.get(mchId);
} catch (Exception e) { } catch (Exception e) {
logger.error("❌ 创建通知配置失败 - 租户ID: {}, 商户号: {}", tenantId, payment.getMchId(), e); logger.error("❌ 创建通知配置失败 - 租户ID: {}, 商户号: {}", tenantId, mchId, e);
logger.error("🔍 错误详情: {}", e.getMessage()); logger.error("🔍 错误详情: {}", e.getMessage());
logger.error("💡 请检查:"); logger.error("💡 请检查:");
logger.error("1. 证书文件是否存在且路径正确"); logger.error("1. 证书文件是否存在且路径正确");
@@ -876,6 +886,9 @@ public class ShopOrderController extends BaseController {
logger.error("4. 网络连接是否正常"); logger.error("4. 网络连接是否正常");
throw new RuntimeException("微信支付通知配置失败: " + e.getMessage(), e); throw new RuntimeException("微信支付通知配置失败: " + e.getMessage(), e);
} }
} else {
logger.info("✅ 使用缓存的通知配置 - 商户号: {}", mchId);
}
// 初始化 NotificationParser // 初始化 NotificationParser
NotificationParser parser = new NotificationParser(config); NotificationParser parser = new NotificationParser(config);

View File

@@ -329,6 +329,10 @@
<update id="updateByOutTradeNo" parameterType="com.gxwebsoft.cms.entity.CmsWebsite"> <update id="updateByOutTradeNo" parameterType="com.gxwebsoft.cms.entity.CmsWebsite">
UPDATE shop_order UPDATE shop_order
<set> <set>
update_time = NOW(),
<if test="param.expirationTime != null">
expiration_time = #{param.expirationTime},
</if>
<if test="param.payType != null"> <if test="param.payType != null">
pay_type = #{param.payType}, pay_type = #{param.payType},
</if> </if>

View File

@@ -834,7 +834,6 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
@Override @Override
public void updateByOutTradeNo(ShopOrder order) { public void updateByOutTradeNo(ShopOrder order) {
order.setExpirationTime(null);
baseMapper.updateByOutTradeNo(order); baseMapper.updateByOutTradeNo(order);
// 处理支付成功后的业务逻辑 // 处理支付成功后的业务逻辑