feat(trade): pay_trade 冗余支付通道(channel)字段, 按接入通道维度看资金免JOIN

- PayTrade 新增 channel 字段(冗余自容器 product→channel, 创建即终值)
- 下单链路写入 channel(PayTradeInitUtil/NormalPayAssistService/GatewayPayHandleService)
- PayTradeQuery 开放 channel 筛选
- table.sql 建表加列, update-tables.sql 升级脚本+存量回填
- 与 TransferTrade 的 channel+provider 双冗余口径对齐
This commit is contained in:
daxpay
2026-08-05 11:56:22 +08:00
parent 4cb6eb0397
commit 2b838bb7f8
7 changed files with 52 additions and 11 deletions

View File

@@ -3266,6 +3266,7 @@ CREATE TABLE "public"."pay_trade" (
"version" int4 NOT NULL DEFAULT 0,
"deleted" bool NOT NULL DEFAULT false,
"provider" varchar(32) COLLATE "pg_catalog"."default",
"channel" varchar(32) COLLATE "pg_catalog"."default",
"title" varchar(255) COLLATE "pg_catalog"."default"
)
;
@@ -3294,6 +3295,7 @@ COMMENT ON COLUMN "public"."pay_trade"."last_modified_time" IS '最后修改时
COMMENT ON COLUMN "public"."pay_trade"."version" IS '乐观锁版本';
COMMENT ON COLUMN "public"."pay_trade"."deleted" IS '逻辑删除';
COMMENT ON COLUMN "public"."pay_trade"."provider" IS '支付渠道(冗余自容器, 支付成功sync后回填; 权威在容器 provider)';
COMMENT ON COLUMN "public"."pay_trade"."channel" IS '支付通道(冗余自容器 product→channel; 创建即终值; 权威在容器 channel)';
COMMENT ON COLUMN "public"."pay_trade"."title" IS '订单标题';
COMMENT ON TABLE "public"."pay_trade" IS '资金交易凭证';

View File

@@ -17,3 +17,25 @@ CREATE UNIQUE INDEX IF NOT EXISTS "uk_adapay_direct_key_config_sandbox" ON "publ
"sandbox" "pg_catalog"."bool_ops" ASC NULLS LAST
) WHERE deleted = false;
COMMENT ON INDEX "public"."uk_adapay_direct_key_config_sandbox" IS '同一通道商户同一环境密钥唯一';
-- ===== pay_trade 增加支付通道冗余字段 =====
-- 冗余自容器 product→channel, 用于按接入通道维度看资金/报表免 JOIN; 与 pay_transfer_trade 口径对齐
ALTER TABLE "public"."pay_trade" ADD COLUMN "channel" varchar(32) COLLATE "pg_catalog"."default";
COMMENT ON COLUMN "public"."pay_trade"."channel" IS '支付通道(冗余自容器 product→channel; 创建即终值; 权威在容器 channel)';
-- 存量数据回填: 从业务容器反向填充 channel
-- 普通支付容器(normal)
UPDATE "public"."pay_trade" t
SET "channel" = c."channel"
FROM "public"."pay_normal_order" c
WHERE t."container_id" = c."id"
AND t."trade_type" = 'normal'
AND t."channel" IS NULL;
-- 网关支付容器(gateway)
UPDATE "public"."pay_trade" t
SET "channel" = c."channel"
FROM "public"."pay_gateway_order" c
WHERE t."container_id" = c."id"
AND t."trade_type" = 'gateway'
AND t."channel" IS NULL;

View File

@@ -17,11 +17,12 @@ import java.time.OffsetDateTime;
/// 统一资金交易表,记录每一笔资金动作(普通支付/预授权冻结/预授权捕获/周期代扣/合单子单)
/// 与容器层(业务单/协议)分离,通过 trade_type + 容器关联字段建立联系。
/// 保留资金动作固有属性、通道反查命脉字段,以及 **轻量组织冗余**
///source / channelMchNo / storeNo / provider便于资金列表与汇总免 JOIN 容器;
///source / channel / channelMchNo / storeNo / provider便于资金列表与汇总免 JOIN 容器;
/// 完整业务上下文/路由细节/payBody/回执仍以容器为准。
/// 过期时间只在容器,本表不存 expiredTime。
/// 注意: 本表不冗余 channel(支付通道), 通道反查用 channelMchNo;
/// 渠道分布报表/资金列表筛选走 provider(支付渠道)。
/// channel(接入通道, B端机构维度) 与 provider(支付渠道, C端钱包维度) 是正交两个维度:
/// channel 用于按接入通道(拉卡拉/银联商务等聚合)维度看资金, provider 用于按付款钱包(微信/支付宝等)维度看资金;
/// 与 [cn.daxpay.open.payment.trade.transfer.entity.TransferTrade] 的 channel+provider 双冗余口径对齐。
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@@ -78,6 +79,10 @@ public class PayTrade extends MchBaseEntity {
/// @see cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum
private String provider;
/// 支付通道(冗余自容器 product→[ChannelEnum]; 创建即终值, 与 provider 不同无需成功路径兜底)
/// @see cn.daxpay.open.platform.core.enums.pay.channel.ChannelEnum
private String channel;
/// 门店号(冗余自业务容器, 可空; 权威在容器 storeNo)
private String storeNo;

View File

@@ -46,7 +46,13 @@ public class PayTradeQuery {
@Schema(description = "交易形态")
private String tradeType;
// 注意: pay_trade 表无 channel/method/product 列,禁止在此加无效筛选
/// 支付通道
/// @see cn.daxpay.open.platform.core.enums.pay.channel.ChannelEnum
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
@Schema(description = "支付通道")
private String channel;
// 注意: pay_trade 表无 method/product 列,禁止在此加无效筛选
/// 通道商户号
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)

View File

@@ -166,7 +166,7 @@ public class GatewayPayHandleService {
}
// 默认上送网关业务单号; 特殊通道返回后可覆盖
// channelMchNo 取自路由后的 payParam; storeNo 冗余自预下单容器; provider 由 method 派生
// channelMchNo 取自路由后的 payParam; storeNo 冗余自预下单容器; provider 由 method 派生; channel 由 product 派生
String provider = PayTradeProviderUtil.resolveProviderByMethod(payParam.getMethod());
PayTrade trade = PayTradeInitUtil.initProcessing(
order.getAppId(),
@@ -180,7 +180,8 @@ public class GatewayPayHandleService {
payParam.getChannelMchNo(),
order.getStoreNo(),
order.getTitle(),
provider);
provider,
channel);
payTradeManager.save(trade);
this.fillRouteOnOrder(order, payParam, clientEnv, device);

View File

@@ -142,7 +142,7 @@ public class NormalPayAssistService {
}
/// 组装资金交易(未落库); relationOrderNo 默认=orderNo, 特殊通道支付返回后可覆盖
/// channelMchNo / storeNo / provider 冗余自容器,资金列表与渠道报表免 JOIN
/// channel / channelMchNo / storeNo / provider 冗余自容器,资金列表与通道/渠道报表免 JOIN
private PayTrade buildPayTrade(NormalPayParam payParam, NormalPayOrder normalOrder,
String tradeNo, String orderNo, String source) {
return PayTradeInitUtil.initProcessing(
@@ -157,7 +157,8 @@ public class NormalPayAssistService {
normalOrder.getChannelMchNo(),
normalOrder.getStoreNo(),
normalOrder.getTitle(),
normalOrder.getProvider());
normalOrder.getProvider(),
normalOrder.getChannel());
}
/// 注册超时关单延时消息

View File

@@ -7,7 +7,7 @@ import cn.daxpay.open.platform.core.enums.pay.channel.CurrencyEnum;
/// # 资金交易初始化工具
///
/// PROCESSING 态资金凭证公共字段的单一事实源。
/// 调用方负责 tradeType / container / source / channelMchNo / storeNo / provider 等业务差异,本工具只填
/// 调用方负责 tradeType / container / source / channel / channelMchNo / storeNo / provider 等业务差异,本工具只填
/// 「待处理结算类交易」公共规则:
/// - currency = 传入币种(可空, 缺省 cny)
/// - postedAmount = 0成功后由 [cn.daxpay.open.payment.trade.runtime.service.pay.common.PayUniHandleService]
@@ -33,6 +33,7 @@ public final class PayTradeInitUtil {
/// @param storeNo 门店号(冗余自容器,可空)
/// @param title 订单标题(冗余自容器,资金列表/工作台免JOIN
/// @param provider 支付渠道(由 method 派生,可空;权威亦可在容器 provider
/// @param channel 支付通道(由 product→channel 派生,创建即终值;权威在容器 channel
public static PayTrade initProcessing(
String appId,
String tradeNo,
@@ -45,7 +46,8 @@ public final class PayTradeInitUtil {
String channelMchNo,
String storeNo,
String title,
String provider) {
String provider,
String channel) {
// 币种: 显式透传优先, 缺省 cny(向后兼容国内通道)
String resolvedCurrency = (currency == null || currency.isBlank())
? CurrencyEnum.CNY.getCode() : currency;
@@ -67,6 +69,8 @@ public final class PayTradeInitUtil {
.setChannelMchNo(channelMchNo)
.setStoreNo(storeNo)
// 支付渠道: 下单时由 method 派生, 渠道分布报表免 JOIN
.setProvider(provider);
.setProvider(provider)
// 支付通道: 由 product 派生, 创建即终值, 按接入通道维度看资金免 JOIN
.setChannel(channel);
}
}