feat(pay): pay_trade 冗余 channelMchNo 与 storeNo

资金凭证增加通道商户号、门店号轻量组织冗余,建单时从容器/路由结果拷贝,便于资金列表与店级汇总免 JOIN;同步 DDL 与查询返回字段,并补齐普通支付门店校验落库。
This commit is contained in:
DaxPay Dev
2026-07-15 14:49:40 +08:00
parent 4bde734c0b
commit 55ca3173fc
7 changed files with 72 additions and 11 deletions

View File

@@ -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);

View File

@@ -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;

View File

@@ -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")

View File

@@ -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;

View File

@@ -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);

View File

@@ -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());
}
/// 注册超时关单延时消息

View File

@@ -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);
}
}