微信支付配置支持四级优先级并简化部署方式
- 微信支付配置支持按 门店 > 代理商 > 渠道 > 平台全局 四级优先级查找,不配置则兜底全局默认 - 支付回调解析优化为多商户号匹配,遍历所有ACTIVE配置逐个尝试验签解密 - 下单/查询/关单按订单组织信息查找对应支付配置 - docker-compose.yml 改为jar挂载方式部署,服务器不再需要源码和Dockerfile
This commit is contained in:
@@ -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<>();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 新增支付配置
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 解析微信支付回调通知(验签 + 解密)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 有效配置)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user