diff --git a/_config/sql/update-tables.sql b/_config/sql/update-tables.sql index 44eb5924d..c88069e51 100644 --- a/_config/sql/update-tables.sql +++ b/_config/sql/update-tables.sql @@ -206,3 +206,15 @@ CREATE INDEX IF NOT EXISTS idx_normal_order_mch_store ON pay_normal_order (mch_n CREATE INDEX IF NOT EXISTS idx_gateway_order_mch_store ON pay_gateway_order (mch_no, store_no); CREATE INDEX IF NOT EXISTS idx_refund_order_mch_store ON pay_refund_order (mch_no, store_no); +-- ============================================================ +-- pay_trade 轻量组织冗余: channel_mch_no + store_no +-- ============================================================ +ALTER TABLE pay_trade ADD COLUMN IF NOT EXISTS channel_mch_no varchar(64); +COMMENT ON COLUMN pay_trade.channel_mch_no IS '通道商户号(冗余自业务容器, 路由确定后写入)'; + +ALTER TABLE pay_trade ADD COLUMN IF NOT EXISTS store_no varchar(32); +COMMENT ON COLUMN pay_trade.store_no IS '门店号(冗余自业务容器, 可空; 权威在容器)'; + +CREATE INDEX IF NOT EXISTS idx_pay_trade_mch_channel ON pay_trade (mch_no, channel_mch_no); +CREATE INDEX IF NOT EXISTS idx_pay_trade_mch_store ON pay_trade (mch_no, store_no); + diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/entity/PayTrade.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/entity/PayTrade.java index 939881cc0..9184d9eda 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/entity/PayTrade.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/entity/PayTrade.java @@ -16,7 +16,9 @@ import java.time.OffsetDateTime; /// /// 统一资金交易表,记录每一笔资金动作(普通支付/预授权冻结/预授权捕获/周期代扣/合单子单) /// 与容器层(业务单/协议)分离,通过 trade_type + 容器关联字段建立联系。 -/// 仅保留资金动作固有属性与通道反查命脉字段;业务上下文/路由/payBody/回执统一归容器。 +/// 保留资金动作固有属性、通道反查命脉字段,以及 **轻量组织冗余** +///(source / channelMchNo / storeNo),便于资金列表与汇总免 JOIN 容器; +/// 完整业务上下文/路由细节/payBody/回执仍以容器为准。 /// 过期时间只在容器,本表不存 expiredTime。 @Data @EqualsAndHashCode(callSuper = true) @@ -64,6 +66,12 @@ public class PayTrade extends MchBaseEntity { /// @see cn.daxpay.open.platform.core.enums.pay.trade.TradeSourceEnum private String source; + /// 通道商户号(冗余自业务容器, 路由确定后写入; 权威在容器 channelMchNo) + private String channelMchNo; + + /// 门店号(冗余自业务容器, 可空; 权威在容器 storeNo) + private String storeNo; + /// 通道订单号(三方通道返回的订单号,通道 close/sync 调用参数 + 回调反查索引) private String outOrderNo; diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/param/PayTradeQuery.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/param/PayTradeQuery.java index 8b7c3e8c6..d02891bc3 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/param/PayTradeQuery.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/param/PayTradeQuery.java @@ -63,6 +63,16 @@ public class PayTradeQuery { @Schema(description = "支付产品") private String product; + /// 通道商户号 + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + @Schema(description = "通道商户号") + private String channelMchNo; + + /// 门店号 + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + @Schema(description = "门店号") + private String storeNo; + /// 关联容器ID @QueryParam(type = QueryParam.CompareTypeEnum.EQ) @Schema(description = "关联容器ID") diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/result/PayTradeResult.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/result/PayTradeResult.java index 761bbe6c8..063a7ae8f 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/result/PayTradeResult.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/result/PayTradeResult.java @@ -103,6 +103,14 @@ public class PayTradeResult extends BaseResult { @Schema(description = "订单来源") private String source; + /// 通道商户号(冗余自业务容器) + @Schema(description = "通道商户号") + private String channelMchNo; + + /// 门店号(冗余自业务容器, 可空) + @Schema(description = "门店号") + private String storeNo; + /// 通道订单号 @Schema(description = "通道订单号") private String outOrderNo; diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayPayHandleService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayPayHandleService.java index 8858a0d7c..bbe0bc654 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayPayHandleService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayPayHandleService.java @@ -143,6 +143,7 @@ public class GatewayPayHandleService { } // 默认上送网关业务单号; 特殊通道返回后可覆盖 + // channelMchNo 取自路由后的 payParam; storeNo 冗余自预下单容器 PayTrade trade = PayTradeInitUtil.initProcessing( order.getAppId(), TradeNoGenerateUtil.pay(), @@ -150,7 +151,9 @@ public class GatewayPayHandleService { order.getId(), order.getAmount(), order.getOrderNo(), - source); + source, + payParam.getChannelMchNo(), + order.getStoreNo()); payTradeManager.save(trade); this.fillRouteOnOrder(order, payParam, clientEnv, device); diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/normal/NormalPayAssistService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/normal/NormalPayAssistService.java index 4e290ab2b..967cd9b3b 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/normal/NormalPayAssistService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/normal/NormalPayAssistService.java @@ -9,11 +9,13 @@ import cn.daxpay.open.platform.core.exception.BizInfoException; import cn.daxpay.open.platform.core.code.CommonErrorCode; import cn.daxpay.open.platform.core.util.DateTimeUtil; import cn.daxpay.open.platform.core.util.TradeNoGenerateUtil; +import cn.daxpay.open.payment.common.context.PaymentContext; +import cn.daxpay.open.payment.common.util.PayUtil; +import cn.daxpay.open.payment.merchant.service.store.MchStoreInfoService; +import cn.daxpay.open.payment.strategy.pay.PayStrategyContext; import cn.daxpay.open.payment.trade.enums.NormalPayOrderStatusEnum; import cn.daxpay.open.payment.trade.enums.PayFundStatusEnum; import cn.daxpay.open.payment.trade.enums.PayTradeTypeEnum; -import cn.daxpay.open.payment.common.util.PayUtil; -import cn.daxpay.open.payment.strategy.pay.PayStrategyContext; import cn.daxpay.open.payment.trade.order.dao.NormalPayOrderManager; import cn.daxpay.open.payment.trade.order.entity.NormalPayOrder; import cn.daxpay.open.payment.trade.order.dao.PayTradeManager; @@ -44,6 +46,8 @@ public class NormalPayAssistService { private final NormalPayOrderManager payNormalOrderManager; private final PayTradeManager payTradeManager; private final ArtemisTemplateService artemisTemplateService; + private final MchStoreInfoService mchStoreInfoService; + private final PaymentContext paymentContext; /// 创建支付订单(容器 + 资金交易),填充到 context /// 调用方需保证 appId 和 product 已解析完毕 @@ -51,12 +55,15 @@ public class NormalPayAssistService { @Transactional(rollbackFor = Exception.class) public void createOrder(NormalPayParam payParam, PayStrategyContext context) { OffsetDateTime expiredTime = this.getExpiredTime(payParam.getExpiredTime()); + // 门店号(线下归属, 可空; 有值则校验存在/归属/启用) + String storeNo = payParam.getTerminal() != null ? payParam.getTerminal().getStoreNo() : null; + mchStoreInfoService.validateStoreForPay(storeNo, paymentContext.getMchNo()); // 双号独立生成 String orderNo = TradeNoGenerateUtil.order(); String tradeNo = TradeNoGenerateUtil.pay(); String source = resolveSource(payParam); - NormalPayOrder normalOrder = buildNormalOrder(payParam, orderNo, expiredTime, source); + NormalPayOrder normalOrder = buildNormalOrder(payParam, orderNo, expiredTime, source, storeNo); payNormalOrderManager.save(normalOrder); PayTrade trade = buildPayTrade(payParam, normalOrder, tradeNo, orderNo, source); @@ -77,7 +84,7 @@ public class NormalPayAssistService { /// 组装普通支付业务容器(未落库) private NormalPayOrder buildNormalOrder(NormalPayParam payParam, String orderNo, - OffsetDateTime expiredTime, String source) { + OffsetDateTime expiredTime, String source, String storeNo) { // 从产品编码派生通道编码 var productEnum = ProductEnum.findByCode(payParam.getProduct()); String channel = productEnum.getChannel(); @@ -111,8 +118,10 @@ public class NormalPayAssistService { normalOrder.setCapability(payParam.getCapability()); // 通道应用 AppId: doBeforePay 已将解析后的实际值回填到 payParam normalOrder.setChannelAppId(payParam.getChannelAppId()); - // --- 终端与客户端 --- + // --- 终端 / 门店 / 客户端 --- normalOrder.setTerminalNo(terminalNo); + // 门店号: 线下经营归属权威落容器 + normalOrder.setStoreNo(storeNo); // 客户端IP(支付入口已兜底, 作为单一事实源供退款/关单等后续流程取用) normalOrder.setClientIp(payParam.getClientIp()); // --- 回调与扩展 --- @@ -126,6 +135,7 @@ public class NormalPayAssistService { } /// 组装资金交易(未落库); relationOrderNo 默认=orderNo, 特殊通道支付返回后可覆盖 + /// channelMchNo / storeNo 冗余自容器,资金列表免 JOIN private PayTrade buildPayTrade(NormalPayParam payParam, NormalPayOrder normalOrder, String tradeNo, String orderNo, String source) { return PayTradeInitUtil.initProcessing( @@ -135,7 +145,9 @@ public class NormalPayAssistService { normalOrder.getId(), payParam.getAmount(), orderNo, - source); + source, + normalOrder.getChannelMchNo(), + normalOrder.getStoreNo()); } /// 注册超时关单延时消息 diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/util/PayTradeInitUtil.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/util/PayTradeInitUtil.java index fa9006bf2..3db4ff855 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/util/PayTradeInitUtil.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/util/PayTradeInitUtil.java @@ -7,7 +7,8 @@ import cn.daxpay.open.platform.core.enums.pay.channel.CurrencyEnum; /// # 资金交易初始化工具 /// /// PROCESSING 态资金凭证公共字段的单一事实源。 -/// 调用方负责 tradeType / container / source 等业务差异,本工具只填「待处理结算类交易」公共规则: +/// 调用方负责 tradeType / container / source / channelMchNo / storeNo 等业务差异,本工具只填 +/// 「待处理结算类交易」公共规则: /// - currency = CNY /// - postedAmount = 0(成功后由 [cn.daxpay.open.payment.trade.runtime.service.pay.common.PayUniHandleService] /// / [PayTradeAmountUtil] 按规则回写) @@ -27,6 +28,8 @@ public final class PayTradeInitUtil { /// @param amount 交易金额 /// @param relationOrderNo 实际上送串(默认与业务单号对齐,特殊通道可后覆盖) /// @param source 订单来源(冗余自容器) + /// @param channelMchNo 通道商户号(冗余自容器/路由结果,可空仅当路由未定时) + /// @param storeNo 门店号(冗余自容器,可空) public static PayTrade initProcessing( String appId, String tradeNo, @@ -34,7 +37,9 @@ public final class PayTradeInitUtil { Long containerId, Long amount, String relationOrderNo, - String source) { + String source, + String channelMchNo, + String storeNo) { return new PayTrade() .setAppId(appId) .setTradeNo(tradeNo) @@ -47,6 +52,9 @@ public final class PayTradeInitUtil { .setRefundableBalance(amount) .setStatus(PayFundStatusEnum.PROCESSING.getCode()) .setRelationOrderNo(relationOrderNo) - .setSource(source); + .setSource(source) + // 轻量组织冗余: 权威在业务容器 + .setChannelMchNo(channelMchNo) + .setStoreNo(storeNo); } }