Compare commits
2 Commits
47ef45054a
...
fa5260d583
| Author | SHA1 | Date | |
|---|---|---|---|
| fa5260d583 | |||
| 0c4bdc3031 |
55
.workbuddy/memory/2026-04-16.md
Normal file
55
.workbuddy/memory/2026-04-16.md
Normal 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,避免重复初始化。
|
||||||
5
.workbuddy/settings.local.json
Normal file
5
.workbuddy/settings.local.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"enabledPlugins": {
|
||||||
|
"modern-webapp@cb_teams_marketplace": true
|
||||||
|
}
|
||||||
|
}
|
||||||
4
pom.xml
4
pom.xml
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,90 +794,100 @@ public class ShopOrderController extends BaseController {
|
|||||||
.body(body)
|
.body(body)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 创建通知配置 - 使用与下单方法相同的证书配置逻辑
|
// 创建通知配置 - 使用与下单方法相同的证书配置逻辑(按 mchId 缓存,避免重复拉取平台证书)
|
||||||
NotificationConfig config;
|
NotificationConfig config;
|
||||||
try {
|
final String mchId = payment.getMchId();
|
||||||
if (active.equals("dev")) {
|
config = notifyConfigCache.get(mchId);
|
||||||
// 开发环境 - 构建包含租户号的私钥路径
|
if (config == null) {
|
||||||
String tenantCertPath = "dev/wechat/" + tenantId;
|
try {
|
||||||
String privateKeyPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getPrivateKeyFile();
|
NotificationConfig newConfig;
|
||||||
|
if (active.equals("dev")) {
|
||||||
|
// 开发环境 - 构建包含租户号的私钥路径
|
||||||
|
String tenantCertPath = "dev/wechat/" + tenantId;
|
||||||
|
String privateKeyPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getPrivateKeyFile();
|
||||||
|
|
||||||
logger.info("开发环境异步通知证书路径: {}", privateKeyPath);
|
logger.info("开发环境异步通知证书路径: {}", privateKeyPath);
|
||||||
logger.info("租户ID: {}, 证书目录: {}", tenantId, tenantCertPath);
|
logger.info("租户ID: {}, 证书目录: {}", tenantId, tenantCertPath);
|
||||||
|
|
||||||
// 检查证书文件是否存在
|
// 检查证书文件是否存在
|
||||||
if (!certificateLoader.certificateExists(privateKeyPath)) {
|
if (!certificateLoader.certificateExists(privateKeyPath)) {
|
||||||
logger.error("证书文件不存在: {}", privateKeyPath);
|
logger.error("证书文件不存在: {}", privateKeyPath);
|
||||||
throw new RuntimeException("证书文件不存在: " + 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);
|
notifyConfigCache.putIfAbsent(mchId, newConfig);
|
||||||
|
config = notifyConfigCache.get(mchId);
|
||||||
// 使用验证器获取有效的 APIv3 密钥
|
} catch (Exception e) {
|
||||||
String apiV3Key = wechatPayConfigValidator.getValidApiV3Key(payment);
|
logger.error("❌ 创建通知配置失败 - 租户ID: {}, 商户号: {}", tenantId, mchId, e);
|
||||||
|
logger.error("🔍 错误详情: {}", e.getMessage());
|
||||||
logger.info("私钥文件加载成功: {}", privateKey);
|
logger.error("💡 请检查:");
|
||||||
logger.info("使用APIv3密钥来源: {}", payment.getApiKey() != null && !payment.getApiKey().trim().isEmpty() ? "数据库配置" : "配置文件默认");
|
logger.error("1. 证书文件是否存在且路径正确");
|
||||||
logger.info("APIv3密钥长度: {}", apiV3Key != null ? apiV3Key.length() : 0);
|
logger.error("2. APIv3密钥是否配置正确");
|
||||||
logger.info("商户证书序列号: {}", payment.getMerchantSerialNumber());
|
logger.error("3. 商户证书序列号是否正确");
|
||||||
|
logger.error("4. 网络连接是否正常");
|
||||||
// 使用自动证书配置
|
throw new RuntimeException("微信支付通知配置失败: " + e.getMessage(), e);
|
||||||
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("✅ 生产环境使用自动证书配置创建通知解析器成功");
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} else {
|
||||||
logger.error("❌ 创建通知配置失败 - 租户ID: {}, 商户号: {}", tenantId, payment.getMchId(), e);
|
logger.info("✅ 使用缓存的通知配置 - 商户号: {}", mchId);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化 NotificationParser
|
// 初始化 NotificationParser
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
// 处理支付成功后的业务逻辑
|
// 处理支付成功后的业务逻辑
|
||||||
|
|||||||
Reference in New Issue
Block a user