微信支付配置支持四级优先级并简化部署方式

- 微信支付配置支持按 门店 > 代理商 > 渠道 > 平台全局 四级优先级查找,不配置则兜底全局默认
- 支付回调解析优化为多商户号匹配,遍历所有ACTIVE配置逐个尝试验签解密
- 下单/查询/关单按订单组织信息查找对应支付配置
- docker-compose.yml 改为jar挂载方式部署,服务器不再需要源码和Dockerfile
This commit is contained in:
zhaogd
2026-08-01 13:15:18 +08:00
parent f55eb001ae
commit ff0729cb81
8 changed files with 188 additions and 70 deletions

View File

@@ -181,7 +181,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
// 7. 微信 Native 支付下单
if (wechatPayService.isConfigured()) {
try {
String codeUrl = wechatPayService.createNativePay(orderNo, "天卡订单", amount);
String codeUrl = wechatPayService.createNativePay(orderNo, "天卡订单", amount, channelOrgId, agentOrgId, storeOrgId);
order.setCodeUrl(codeUrl);
order.setPayChannel("WECHAT");
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
@@ -316,7 +316,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
// 6.5 微信 Native 支付下单
if (wechatPayService.isConfigured()) {
try {
String codeUrl = wechatPayService.createNativePay(orderNo, "天卡订单", amount);
String codeUrl = wechatPayService.createNativePay(orderNo, "天卡订单", amount, channelOrgId, agentOrgId, storeOrgId);
order.setCodeUrl(codeUrl);
order.setPayChannel("WECHAT");
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();

View File

@@ -70,7 +70,8 @@ public class PaymentController {
}
String codeUrl = wechatPayService.createNativePay(
order.getOrderNo(), request.getDescription(), order.getAmount());
order.getOrderNo(), request.getDescription(), order.getAmount(),
order.getChannelOrgId(), order.getAgentOrgId(), order.getStoreOrgId());
// 将 code_url 和支付渠道写入订单
order.setCodeUrl(codeUrl);

View File

@@ -20,9 +20,18 @@ public class WechatPayConfigEntity implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
/** 合作伙伴ID(空=全局默认) */
/** 合作伙伴ID(空=非合作伙伴专属,兼容保留) */
private Long partnerId;
/** 渠道组织ID(空=非渠道专属) */
private Long channelOrgId;
/** 代理商组织ID(空=非代理商专属) */
private Long agentOrgId;
/** 门店组织ID(空=非门店专属) */
private Long storeOrgId;
/** 微信应用ID */
private String appId;

View File

@@ -35,6 +35,16 @@ public interface WechatPayConfigService extends IService<WechatPayConfigEntity>
*/
WechatPayConfigEntity getActiveConfig(Long partnerId);
/**
* 获取有效配置(四级优先级):门店 > 代理商 > 渠道 > 平台全局
*
* @param channelOrgId 渠道组织ID(可空)
* @param agentOrgId 代理商组织ID(可空)
* @param storeOrgId 门店组织ID(可空)
* @return 有效配置实体,不存在返回 null
*/
WechatPayConfigEntity getActiveConfig(Long channelOrgId, Long agentOrgId, Long storeOrgId);
/**
* 新增支付配置
*

View File

@@ -17,12 +17,15 @@ public interface WechatPayService {
/**
* 创建 Native 支付订单(扫码支付)
*
* @param orderNo 商户订单号
* @param description 商品描述
* @param amount 支付金额(元)
* @param orderNo 商户订单号
* @param description 商品描述
* @param amount 支付金额(元)
* @param channelOrgId 渠道组织ID(可空,用于匹配渠道级支付配置)
* @param agentOrgId 代理商组织ID(可空)
* @param storeOrgId 门店组织ID(可空,用于匹配门店级支付配置)
* @return 微信支付二维码链接 code_url
*/
String createNativePay(String orderNo, String description, BigDecimal amount);
String createNativePay(String orderNo, String description, BigDecimal amount, Long channelOrgId, Long agentOrgId, Long storeOrgId);
/**
* 解析微信支付回调通知(验签 + 解密)

View File

@@ -25,11 +25,17 @@ public class WechatPayConfigServiceImpl extends ServiceImpl<WechatPayConfigDao,
public PageUtils queryPage(Map<String, Object> params) {
String partnerId = (String) params.get("partnerId");
String status = (String) params.get("status");
String channelOrgId = (String) params.get("channelOrgId");
String agentOrgId = (String) params.get("agentOrgId");
String storeOrgId = (String) params.get("storeOrgId");
IPage<WechatPayConfigEntity> page = this.page(
new Query<WechatPayConfigEntity>().getPage(params, "created_at", false),
new QueryWrapper<WechatPayConfigEntity>()
.eq(StringUtils.isNotBlank(partnerId), "partner_id", partnerId)
.eq(StringUtils.isNotBlank(channelOrgId), "channel_org_id", channelOrgId)
.eq(StringUtils.isNotBlank(agentOrgId), "agent_org_id", agentOrgId)
.eq(StringUtils.isNotBlank(storeOrgId), "store_org_id", storeOrgId)
.eq(StringUtils.isNotBlank(status), "status", status)
.eq("delete_flag", "UNDEL")
);
@@ -48,27 +54,51 @@ public class WechatPayConfigServiceImpl extends ServiceImpl<WechatPayConfigDao,
@Override
public WechatPayConfigEntity getActiveConfig(Long partnerId) {
// 优先 partner 专属配置
if (partnerId != null) {
WechatPayConfigEntity config = this.getOne(
new QueryWrapper<WechatPayConfigEntity>()
.eq("partner_id", partnerId)
.eq("status", "ACTIVE")
.eq("delete_flag", "UNDEL")
.last("LIMIT 1")
);
if (config != null) {
return config;
}
return getActiveConfig(null, null, null);
}
@Override
public WechatPayConfigEntity getActiveConfig(Long channelOrgId, Long agentOrgId, Long storeOrgId) {
// 1. 门店级
if (storeOrgId != null) {
WechatPayConfigEntity config = findActive(null, null, storeOrgId);
if (config != null) return config;
}
// 回退全局默认(partner_id 为空)
return this.getOne(
new QueryWrapper<WechatPayConfigEntity>()
.isNull("partner_id")
// 2. 代理商级
if (agentOrgId != null) {
WechatPayConfigEntity config = findActive(null, agentOrgId, null);
if (config != null) return config;
}
// 3. 渠道级
if (channelOrgId != null) {
WechatPayConfigEntity config = findActive(channelOrgId, null, null);
if (config != null) return config;
}
// 4. 平台全局默认
return findActive(null, null, null);
}
/** 按精确维度查一条有效配置(组织维度均为 IS NULL 或 =,partner_id 不参与) */
private WechatPayConfigEntity findActive(Long channelOrgId, Long agentOrgId, Long storeOrgId) {
QueryWrapper<WechatPayConfigEntity> wrapper = new QueryWrapper<WechatPayConfigEntity>()
.eq("status", "ACTIVE")
.eq("delete_flag", "UNDEL")
.last("LIMIT 1")
);
.eq("delete_flag", "UNDEL");
if (channelOrgId != null) {
wrapper.eq("channel_org_id", channelOrgId);
} else {
wrapper.isNull("channel_org_id");
}
if (agentOrgId != null) {
wrapper.eq("agent_org_id", agentOrgId);
} else {
wrapper.isNull("agent_org_id");
}
if (storeOrgId != null) {
wrapper.eq("store_org_id", storeOrgId);
} else {
wrapper.isNull("store_org_id");
}
return this.getOne(wrapper.last("LIMIT 1"));
}
@Override

View File

@@ -34,6 +34,7 @@ import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@@ -64,6 +65,9 @@ public class WechatPayServiceImpl implements WechatPayService {
@Autowired
private WechatPayConfigService wechatPayConfigService;
@Autowired
private JdbcTemplate jdbcTemplate;
/** SDK 配置缓存:按 mchId+certSerialNo+apiV3Key+privateKeyPath 缓存 */
private final ConcurrentHashMap<String, Config> sdkConfigCache = new ConcurrentHashMap<>();
@@ -75,16 +79,16 @@ public class WechatPayServiceImpl implements WechatPayService {
* <p>设置 SettleInfo.profitSharing=true 标记订单支持分账</p>
*/
@Override
public String createNativePay(String orderNo, String description, BigDecimal amount) {
public String createNativePay(String orderNo, String description, BigDecimal amount, Long channelOrgId, Long agentOrgId, Long storeOrgId) {
if (StringUtils.isBlank(orderNo)) {
throw new ASException("订单号不能为空");
}
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new ASException("支付金额必须大于0");
}
validateConfig();
validateConfig(channelOrgId, agentOrgId, storeOrgId);
WechatPayConfigEntity dbCfg = getActiveDbConfig();
WechatPayConfigEntity dbCfg = getActiveDbConfig(channelOrgId, agentOrgId, storeOrgId);
int totalFen = yuanToFen(amount);
logger.info("创建微信Native支付订单: orderNo={}, amount={}元({}分), description={}", orderNo, amount, totalFen, description);
@@ -130,7 +134,7 @@ public class WechatPayServiceImpl implements WechatPayService {
/**
* 解析微信支付回调通知
* <p>使用 wechatpay-java SDK 的 NotificationParser 完成验签 + 解密</p>
* <p>多商户号场景:遍历所有 ACTIVE 配置逐个尝试解析(不同商户 apiV3Key 不同),全局默认优先。</p>
*/
@Override
public WechatPayResult parseNotify(String body, Map<String, String> headers) {
@@ -139,40 +143,71 @@ public class WechatPayServiceImpl implements WechatPayService {
}
logger.info("收到微信支付回调: body={}", body);
try {
Config config = getOrCreateConfig();
NotificationParser parser = new NotificationParser(
(com.wechat.pay.java.core.notification.NotificationConfig) config);
// 获取所有 ACTIVE 配置
List<WechatPayConfigEntity> configs = wechatPayConfigService.lambdaQuery()
.eq(WechatPayConfigEntity::getStatus, "ACTIVE")
.eq(WechatPayConfigEntity::getDeleteFlag, "UNDEL")
.list();
// 全局默认(组织字段全空)排前面优先尝试
configs.sort((a, b) -> {
int aGlobal = (a.getChannelOrgId() == null && a.getAgentOrgId() == null && a.getStoreOrgId() == null) ? 0 : 1;
int bGlobal = (b.getChannelOrgId() == null && b.getAgentOrgId() == null && b.getStoreOrgId() == null) ? 0 : 1;
return aGlobal - bGlobal;
});
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(headers.get("Wechatpay-Serial"))
.nonce(headers.get("Wechatpay-Nonce"))
.signature(headers.get("Wechatpay-Signature"))
.timestamp(headers.get("Wechatpay-Timestamp"))
.body(body)
.build();
Transaction transaction = parser.parse(requestParam, Transaction.class);
String transactionId = transaction.getTransactionId();
String outTradeNo = transaction.getOutTradeNo();
String tradeState = transaction.getTradeState() != null
? String.valueOf(transaction.getTradeState()) : null;
Integer totalFen = transaction.getAmount() != null
? transaction.getAmount().getTotal() : null;
logger.info("微信支付回调解析结果: outTradeNo={}, transactionId={}, tradeState={}, total={}分",
outTradeNo, transactionId, tradeState, totalFen);
if (TRADE_STATE_SUCCESS.equals(tradeState)) {
BigDecimal amountYuan = totalFen != null ? fenToYuan(totalFen) : null;
return WechatPayResult.ok(outTradeNo, transactionId, amountYuan);
} else {
return WechatPayResult.fail("交易状态非 SUCCESS: " + tradeState);
// 逐个配置尝试解析
Exception lastError = null;
for (WechatPayConfigEntity cfg : configs) {
try {
return doParseNotify(body, headers, cfg);
} catch (Exception e) {
lastError = e;
logger.debug("用配置[id={}]解析回调失败: {}", cfg.getId(), e.getMessage());
}
} catch (Exception e) {
logger.error("解析微信支付回调异常", e);
return WechatPayResult.fail("回调解析异常: " + e.getMessage());
}
// DB 无配置,尝试 yml 配置
if (configs.isEmpty()) {
try {
return doParseNotify(body, headers, null);
} catch (Exception e) {
lastError = e;
}
}
logger.error("解析微信支付回调异常(所有配置均失败)", lastError);
return WechatPayResult.fail("回调解析异常: " + (lastError != null ? lastError.getMessage() : "无可用配置"));
}
/** 用指定配置验签 + 解密回调 */
private WechatPayResult doParseNotify(String body, Map<String, String> headers, WechatPayConfigEntity cfg) {
Config config = getOrCreateConfig(cfg);
NotificationParser parser = new NotificationParser(
(com.wechat.pay.java.core.notification.NotificationConfig) config);
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(headers.get("Wechatpay-Serial"))
.nonce(headers.get("Wechatpay-Nonce"))
.signature(headers.get("Wechatpay-Signature"))
.timestamp(headers.get("Wechatpay-Timestamp"))
.body(body)
.build();
Transaction transaction = parser.parse(requestParam, Transaction.class);
String transactionId = transaction.getTransactionId();
String outTradeNo = transaction.getOutTradeNo();
String tradeState = transaction.getTradeState() != null
? String.valueOf(transaction.getTradeState()) : null;
Integer totalFen = transaction.getAmount() != null
? transaction.getAmount().getTotal() : null;
logger.info("微信支付回调解析成功: configId={}, outTradeNo={}, transactionId={}, tradeState={}, total={}分",
cfg != null ? cfg.getId() : "yml", outTradeNo, transactionId, tradeState, totalFen);
if (TRADE_STATE_SUCCESS.equals(tradeState)) {
BigDecimal amountYuan = totalFen != null ? fenToYuan(totalFen) : null;
return WechatPayResult.ok(outTradeNo, transactionId, amountYuan);
} else {
return WechatPayResult.fail("交易状态非 SUCCESS: " + tradeState);
}
}
@@ -186,12 +221,12 @@ public class WechatPayServiceImpl implements WechatPayService {
if (StringUtils.isBlank(orderNo)) {
return WechatPayResult.fail("订单号不能为空");
}
validateConfig();
validateConfig(null, null, null);
logger.info("查询微信支付订单: orderNo={}", orderNo);
try {
WechatPayConfigEntity dbCfg = getActiveDbConfig();
WechatPayConfigEntity dbCfg = resolveConfigByOrderNo(orderNo);
Config config = getOrCreateConfig(dbCfg);
NativePayService service = new NativePayService.Builder().config(config).build();
@@ -233,7 +268,7 @@ public class WechatPayServiceImpl implements WechatPayService {
}
logger.info("微信关单: orderNo={}", orderNo);
try {
WechatPayConfigEntity dbCfg = getActiveDbConfig();
WechatPayConfigEntity dbCfg = resolveConfigByOrderNo(orderNo);
Config config = getOrCreateConfig(dbCfg);
NativePayService service = new NativePayService.Builder().config(config).build();
CloseOrderRequest request = new CloseOrderRequest();
@@ -260,6 +295,7 @@ public class WechatPayServiceImpl implements WechatPayService {
logger.info("微信退款: orderNo={}, refundNo={}, refundAmount={}", orderNo, refundNo, refundAmount);
try {
// TODO: 接入 wechatpay-java RefundService
// TODO: 接入SDK后用 resolveConfigByOrderNo(orderNo) 获取配置
logger.info("微信退款成功(待接入SDK): orderNo={}, refundNo={}", orderNo, refundNo);
return WechatPayResult.ok(orderNo, refundNo, refundAmount);
} catch (Exception e) {
@@ -498,7 +534,11 @@ public class WechatPayServiceImpl implements WechatPayService {
* 校验微信支付配置是否完整(DB 配置优先,回退 yml 环境变量)
*/
private void validateConfig() {
WechatPayConfigEntity dbCfg = getActiveDbConfig();
validateConfig(null, null, null);
}
private void validateConfig(Long channelOrgId, Long agentOrgId, Long storeOrgId) {
WechatPayConfigEntity dbCfg = getActiveDbConfig(channelOrgId, agentOrgId, storeOrgId);
if (StringUtils.isBlank(getAppId(dbCfg))) {
throw new ASException("微信支付配置缺少 appId");
}
@@ -523,14 +563,36 @@ public class WechatPayServiceImpl implements WechatPayService {
* 获取 DB 中的有效支付配置(全局默认),不存在返回 null
*/
private WechatPayConfigEntity getActiveDbConfig() {
return getActiveDbConfig(null, null, null);
}
private WechatPayConfigEntity getActiveDbConfig(Long channelOrgId, Long agentOrgId, Long storeOrgId) {
try {
return wechatPayConfigService.getActiveConfig(null);
return wechatPayConfigService.getActiveConfig(channelOrgId, agentOrgId, storeOrgId);
} catch (Exception e) {
logger.debug("获取DB支付配置失败,将回退到yml配置", e);
return null;
}
}
/** 从订单号反查订单的组织信息,再按四级优先级查找支付配置 */
private WechatPayConfigEntity resolveConfigByOrderNo(String orderNo) {
if (StringUtils.isBlank(orderNo)) {
return getActiveDbConfig(null, null, null);
}
try {
Map<String, Object> row = jdbcTemplate.queryForMap(
"SELECT channel_org_id, agent_org_id, store_org_id FROM day_card_order WHERE order_no = ? AND delete_flag = 'UNDEL' LIMIT 1", orderNo);
Long channelOrgId = row.get("channel_org_id") != null ? ((Number) row.get("channel_org_id")).longValue() : null;
Long agentOrgId = row.get("agent_org_id") != null ? ((Number) row.get("agent_org_id")).longValue() : null;
Long storeOrgId = row.get("store_org_id") != null ? ((Number) row.get("store_org_id")).longValue() : null;
return getActiveDbConfig(channelOrgId, agentOrgId, storeOrgId);
} catch (Exception e) {
logger.debug("从订单号反查支付配置失败,回退全局默认: orderNo={}", orderNo, e);
return getActiveDbConfig(null, null, null);
}
}
/**
* 获取或创建 SDK 配置实例(无参版本,使用 DB 有效配置)
*/