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

- 微信支付配置支持按 门店 > 代理商 > 渠道 > 平台全局 四级优先级查找,不配置则兜底全局默认
- 支付回调解析优化为多商户号匹配,遍历所有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

@@ -2,11 +2,14 @@ version: "3.8"
services: services:
daycard-openapi: daycard-openapi:
build: . image: eclipse-temurin:17-jre-alpine
image: daycard-openapi:latest
container_name: daycard-openapi container_name: daycard-openapi
ports: ports:
- "8084:8084" - "8084:8084"
volumes:
- ./daycard-openapi.jar:/app/daycard-openapi.jar:ro
working_dir: /app
command: java -jar daycard-openapi.jar
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
environment: environment:

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,16 @@ public interface WechatPayConfigService extends IService<WechatPayConfigEntity>
*/ */
WechatPayConfigEntity getActiveConfig(Long partnerId); 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 支付订单(扫码支付) * 创建 Native 支付订单(扫码支付)
* *
* @param orderNo 商户订单号 * @param orderNo 商户订单号
* @param description 商品描述 * @param description 商品描述
* @param amount 支付金额(元) * @param amount 支付金额(元)
* @param channelOrgId 渠道组织ID(可空,用于匹配渠道级支付配置)
* @param agentOrgId 代理商组织ID(可空)
* @param storeOrgId 门店组织ID(可空,用于匹配门店级支付配置)
* @return 微信支付二维码链接 code_url * @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) { public PageUtils queryPage(Map<String, Object> params) {
String partnerId = (String) params.get("partnerId"); String partnerId = (String) params.get("partnerId");
String status = (String) params.get("status"); 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( IPage<WechatPayConfigEntity> page = this.page(
new Query<WechatPayConfigEntity>().getPage(params, "created_at", false), new Query<WechatPayConfigEntity>().getPage(params, "created_at", false),
new QueryWrapper<WechatPayConfigEntity>() new QueryWrapper<WechatPayConfigEntity>()
.eq(StringUtils.isNotBlank(partnerId), "partner_id", partnerId) .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(StringUtils.isNotBlank(status), "status", status)
.eq("delete_flag", "UNDEL") .eq("delete_flag", "UNDEL")
); );
@@ -48,27 +54,51 @@ public class WechatPayConfigServiceImpl extends ServiceImpl<WechatPayConfigDao,
@Override @Override
public WechatPayConfigEntity getActiveConfig(Long partnerId) { public WechatPayConfigEntity getActiveConfig(Long partnerId) {
// 优先 partner 专属配置 return getActiveConfig(null, null, null);
if (partnerId != null) { }
WechatPayConfigEntity config = this.getOne(
new QueryWrapper<WechatPayConfigEntity>() @Override
.eq("partner_id", partnerId) public WechatPayConfigEntity getActiveConfig(Long channelOrgId, Long agentOrgId, Long storeOrgId) {
.eq("status", "ACTIVE") // 1. 门店级
.eq("delete_flag", "UNDEL") if (storeOrgId != null) {
.last("LIMIT 1") WechatPayConfigEntity config = findActive(null, null, storeOrgId);
); if (config != null) return config;
if (config != null) {
return config;
}
} }
// 回退全局默认(partner_id 为空) // 2. 代理商级
return this.getOne( if (agentOrgId != null) {
new QueryWrapper<WechatPayConfigEntity>() WechatPayConfigEntity config = findActive(null, agentOrgId, null);
.isNull("partner_id") 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("status", "ACTIVE")
.eq("delete_flag", "UNDEL") .eq("delete_flag", "UNDEL");
.last("LIMIT 1") 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 @Override

View File

@@ -34,6 +34,7 @@ import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -64,6 +65,9 @@ public class WechatPayServiceImpl implements WechatPayService {
@Autowired @Autowired
private WechatPayConfigService wechatPayConfigService; private WechatPayConfigService wechatPayConfigService;
@Autowired
private JdbcTemplate jdbcTemplate;
/** SDK 配置缓存:按 mchId+certSerialNo+apiV3Key+privateKeyPath 缓存 */ /** SDK 配置缓存:按 mchId+certSerialNo+apiV3Key+privateKeyPath 缓存 */
private final ConcurrentHashMap<String, Config> sdkConfigCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, Config> sdkConfigCache = new ConcurrentHashMap<>();
@@ -75,16 +79,16 @@ public class WechatPayServiceImpl implements WechatPayService {
* <p>设置 SettleInfo.profitSharing=true 标记订单支持分账</p> * <p>设置 SettleInfo.profitSharing=true 标记订单支持分账</p>
*/ */
@Override @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)) { if (StringUtils.isBlank(orderNo)) {
throw new ASException("订单号不能为空"); throw new ASException("订单号不能为空");
} }
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new ASException("支付金额必须大于0"); throw new ASException("支付金额必须大于0");
} }
validateConfig(); validateConfig(channelOrgId, agentOrgId, storeOrgId);
WechatPayConfigEntity dbCfg = getActiveDbConfig(); WechatPayConfigEntity dbCfg = getActiveDbConfig(channelOrgId, agentOrgId, storeOrgId);
int totalFen = yuanToFen(amount); int totalFen = yuanToFen(amount);
logger.info("创建微信Native支付订单: orderNo={}, amount={}元({}分), description={}", orderNo, amount, totalFen, description); 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 @Override
public WechatPayResult parseNotify(String body, Map<String, String> headers) { public WechatPayResult parseNotify(String body, Map<String, String> headers) {
@@ -139,40 +143,71 @@ public class WechatPayServiceImpl implements WechatPayService {
} }
logger.info("收到微信支付回调: body={}", body); logger.info("收到微信支付回调: body={}", body);
try { // 获取所有 ACTIVE 配置
Config config = getOrCreateConfig(); List<WechatPayConfigEntity> configs = wechatPayConfigService.lambdaQuery()
NotificationParser parser = new NotificationParser( .eq(WechatPayConfigEntity::getStatus, "ACTIVE")
(com.wechat.pay.java.core.notification.NotificationConfig) config); .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")) Exception lastError = null;
.nonce(headers.get("Wechatpay-Nonce")) for (WechatPayConfigEntity cfg : configs) {
.signature(headers.get("Wechatpay-Signature")) try {
.timestamp(headers.get("Wechatpay-Timestamp")) return doParseNotify(body, headers, cfg);
.body(body) } catch (Exception e) {
.build(); lastError = e;
logger.debug("用配置[id={}]解析回调失败: {}", cfg.getId(), e.getMessage());
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);
} }
} catch (Exception e) { }
logger.error("解析微信支付回调异常", e); // DB 无配置,尝试 yml 配置
return WechatPayResult.fail("回调解析异常: " + e.getMessage()); 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)) { if (StringUtils.isBlank(orderNo)) {
return WechatPayResult.fail("订单号不能为空"); return WechatPayResult.fail("订单号不能为空");
} }
validateConfig(); validateConfig(null, null, null);
logger.info("查询微信支付订单: orderNo={}", orderNo); logger.info("查询微信支付订单: orderNo={}", orderNo);
try { try {
WechatPayConfigEntity dbCfg = getActiveDbConfig(); WechatPayConfigEntity dbCfg = resolveConfigByOrderNo(orderNo);
Config config = getOrCreateConfig(dbCfg); Config config = getOrCreateConfig(dbCfg);
NativePayService service = new NativePayService.Builder().config(config).build(); NativePayService service = new NativePayService.Builder().config(config).build();
@@ -233,7 +268,7 @@ public class WechatPayServiceImpl implements WechatPayService {
} }
logger.info("微信关单: orderNo={}", orderNo); logger.info("微信关单: orderNo={}", orderNo);
try { try {
WechatPayConfigEntity dbCfg = getActiveDbConfig(); WechatPayConfigEntity dbCfg = resolveConfigByOrderNo(orderNo);
Config config = getOrCreateConfig(dbCfg); Config config = getOrCreateConfig(dbCfg);
NativePayService service = new NativePayService.Builder().config(config).build(); NativePayService service = new NativePayService.Builder().config(config).build();
CloseOrderRequest request = new CloseOrderRequest(); CloseOrderRequest request = new CloseOrderRequest();
@@ -260,6 +295,7 @@ public class WechatPayServiceImpl implements WechatPayService {
logger.info("微信退款: orderNo={}, refundNo={}, refundAmount={}", orderNo, refundNo, refundAmount); logger.info("微信退款: orderNo={}, refundNo={}, refundAmount={}", orderNo, refundNo, refundAmount);
try { try {
// TODO: 接入 wechatpay-java RefundService // TODO: 接入 wechatpay-java RefundService
// TODO: 接入SDK后用 resolveConfigByOrderNo(orderNo) 获取配置
logger.info("微信退款成功(待接入SDK): orderNo={}, refundNo={}", orderNo, refundNo); logger.info("微信退款成功(待接入SDK): orderNo={}, refundNo={}", orderNo, refundNo);
return WechatPayResult.ok(orderNo, refundNo, refundAmount); return WechatPayResult.ok(orderNo, refundNo, refundAmount);
} catch (Exception e) { } catch (Exception e) {
@@ -498,7 +534,11 @@ public class WechatPayServiceImpl implements WechatPayService {
* 校验微信支付配置是否完整(DB 配置优先,回退 yml 环境变量) * 校验微信支付配置是否完整(DB 配置优先,回退 yml 环境变量)
*/ */
private void validateConfig() { 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))) { if (StringUtils.isBlank(getAppId(dbCfg))) {
throw new ASException("微信支付配置缺少 appId"); throw new ASException("微信支付配置缺少 appId");
} }
@@ -523,14 +563,36 @@ public class WechatPayServiceImpl implements WechatPayService {
* 获取 DB 中的有效支付配置(全局默认),不存在返回 null * 获取 DB 中的有效支付配置(全局默认),不存在返回 null
*/ */
private WechatPayConfigEntity getActiveDbConfig() { private WechatPayConfigEntity getActiveDbConfig() {
return getActiveDbConfig(null, null, null);
}
private WechatPayConfigEntity getActiveDbConfig(Long channelOrgId, Long agentOrgId, Long storeOrgId) {
try { try {
return wechatPayConfigService.getActiveConfig(null); return wechatPayConfigService.getActiveConfig(channelOrgId, agentOrgId, storeOrgId);
} catch (Exception e) { } catch (Exception e) {
logger.debug("获取DB支付配置失败,将回退到yml配置", e); logger.debug("获取DB支付配置失败,将回退到yml配置", e);
return null; 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 有效配置) * 获取或创建 SDK 配置实例(无参版本,使用 DB 有效配置)
*/ */