diff --git a/docker-compose.yml b/docker-compose.yml index 6493c38..f179b86 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,11 +2,14 @@ version: "3.8" services: daycard-openapi: - build: . - image: daycard-openapi:latest + image: eclipse-temurin:17-jre-alpine container_name: daycard-openapi ports: - "8084:8084" + volumes: + - ./daycard-openapi.jar:/app/daycard-openapi.jar:ro + working_dir: /app + command: java -jar daycard-openapi.jar extra_hosts: - "host.docker.internal:host-gateway" environment: diff --git a/src/main/java/io/as/modules/daycard/service/impl/DayCardOrderServiceImpl.java b/src/main/java/io/as/modules/daycard/service/impl/DayCardOrderServiceImpl.java index e412287..5da3d37 100644 --- a/src/main/java/io/as/modules/daycard/service/impl/DayCardOrderServiceImpl.java +++ b/src/main/java/io/as/modules/daycard/service/impl/DayCardOrderServiceImpl.java @@ -181,7 +181,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl payUpdate = new UpdateWrapper<>(); @@ -316,7 +316,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl payUpdate = new UpdateWrapper<>(); diff --git a/src/main/java/io/as/modules/payment/controller/PaymentController.java b/src/main/java/io/as/modules/payment/controller/PaymentController.java index 1858cf2..9d7dec0 100644 --- a/src/main/java/io/as/modules/payment/controller/PaymentController.java +++ b/src/main/java/io/as/modules/payment/controller/PaymentController.java @@ -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); diff --git a/src/main/java/io/as/modules/payment/entity/WechatPayConfigEntity.java b/src/main/java/io/as/modules/payment/entity/WechatPayConfigEntity.java index 2d7df18..e60dd17 100644 --- a/src/main/java/io/as/modules/payment/entity/WechatPayConfigEntity.java +++ b/src/main/java/io/as/modules/payment/entity/WechatPayConfigEntity.java @@ -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; diff --git a/src/main/java/io/as/modules/payment/service/WechatPayConfigService.java b/src/main/java/io/as/modules/payment/service/WechatPayConfigService.java index 0ae47e3..ea06f19 100644 --- a/src/main/java/io/as/modules/payment/service/WechatPayConfigService.java +++ b/src/main/java/io/as/modules/payment/service/WechatPayConfigService.java @@ -35,6 +35,16 @@ public interface WechatPayConfigService extends IService */ WechatPayConfigEntity getActiveConfig(Long partnerId); + /** + * 获取有效配置(四级优先级):门店 > 代理商 > 渠道 > 平台全局 + * + * @param channelOrgId 渠道组织ID(可空) + * @param agentOrgId 代理商组织ID(可空) + * @param storeOrgId 门店组织ID(可空) + * @return 有效配置实体,不存在返回 null + */ + WechatPayConfigEntity getActiveConfig(Long channelOrgId, Long agentOrgId, Long storeOrgId); + /** * 新增支付配置 * diff --git a/src/main/java/io/as/modules/payment/service/WechatPayService.java b/src/main/java/io/as/modules/payment/service/WechatPayService.java index 7720fea..d9e717d 100644 --- a/src/main/java/io/as/modules/payment/service/WechatPayService.java +++ b/src/main/java/io/as/modules/payment/service/WechatPayService.java @@ -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); /** * 解析微信支付回调通知(验签 + 解密) diff --git a/src/main/java/io/as/modules/payment/service/impl/WechatPayConfigServiceImpl.java b/src/main/java/io/as/modules/payment/service/impl/WechatPayConfigServiceImpl.java index b6170ae..38e56eb 100644 --- a/src/main/java/io/as/modules/payment/service/impl/WechatPayConfigServiceImpl.java +++ b/src/main/java/io/as/modules/payment/service/impl/WechatPayConfigServiceImpl.java @@ -25,11 +25,17 @@ public class WechatPayConfigServiceImpl extends ServiceImpl 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 page = this.page( new Query().getPage(params, "created_at", false), new QueryWrapper() .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() - .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() - .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 wrapper = new QueryWrapper() .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 diff --git a/src/main/java/io/as/modules/payment/service/impl/WechatPayServiceImpl.java b/src/main/java/io/as/modules/payment/service/impl/WechatPayServiceImpl.java index 0f9bd61..8adac29 100644 --- a/src/main/java/io/as/modules/payment/service/impl/WechatPayServiceImpl.java +++ b/src/main/java/io/as/modules/payment/service/impl/WechatPayServiceImpl.java @@ -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 sdkConfigCache = new ConcurrentHashMap<>(); @@ -75,16 +79,16 @@ public class WechatPayServiceImpl implements WechatPayService { *

设置 SettleInfo.profitSharing=true 标记订单支持分账

*/ @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 { /** * 解析微信支付回调通知 - *

使用 wechatpay-java SDK 的 NotificationParser 完成验签 + 解密

+ *

多商户号场景:遍历所有 ACTIVE 配置逐个尝试解析(不同商户 apiV3Key 不同),全局默认优先。

*/ @Override public WechatPayResult parseNotify(String body, Map 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 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 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 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 有效配置) */