feat(trade): 下单写入 provider 并修复报表口径

资金凭证创建时由 method 派生 provider,成功路径兜底补齐;SQL 回填历史 posted_amount/provider;时段分布改为日均。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
DaxPay Dev
2026-07-22 21:14:25 +08:00
parent ca85e2641f
commit 365cb6ed5f
13 changed files with 274 additions and 52 deletions

View File

@@ -12,7 +12,7 @@
Target Server Version : 160014 (160014)
File Encoding : 65001
Date: 21/07/2026 14:12:39
Date: 22/07/2026 18:36:13
*/
@@ -127,7 +127,9 @@ INSERT INTO "public"."iam_perm_menu" VALUES (4040111, 4040130, 'merchant:app:rou
INSERT INTO "public"."iam_perm_menu" VALUES (4040109, 4040131, 'channel:merchant:create', 'admin', 'ChannelMerchantCreate', 'menu.payment.merchant.channelMerchant.create', NULL, 't', 'f', '/payment/merchant/channel-merchant/ChannelMerchantCreate', '/payment/merchant/channel-merchant/create', NULL, 9, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-05-05 16:00:00+00', '2026-07-11 03:37:15.428444+00');
INSERT INTO "public"."iam_perm_menu" VALUES (4040117, 4040130, 'merchant:store', 'admin', 'MchStoreInfoList', 'menu.payment.merchant.store', NULL, 't', 'f', '/payment/merchant/store/MchStoreInfoList', '/payment/merchant/store', NULL, 15, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-24 16:00:00+00', '2026-07-11 03:37:15.424776+00');
INSERT INTO "public"."iam_perm_menu" VALUES (602, 6, 'trade:fund', 'admin', 'PayTradeList', 'menu.trade.payTrade', 'lucide:arrow-left-right', 'f', 'f', '/payment/order/PayTradeList', '/trade/pay-trade', NULL, 1, 'f', 't', 'f', NULL, NULL, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-01 15:09:04.769906+00', '2026-07-17 06:18:15.474015+00');
INSERT INTO "public"."iam_perm_menu" VALUES (901, 4, 'device:qrcode', 'admin', 'DeviceQrCode', 'menu.device.qrcode', 'lucide:qr-code', 'f', 'f', '/payment/device/qrcode/DeviceQrCode', '/device/qrcode', NULL, 3, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-25 01:21:34.634639+00', '2026-07-21 06:30:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (8, NULL, 'develop', 'admin', 'Develop', 'menu.develop', 'lucide:wrench', 'f', 'f', NULL, '/develop', NULL, 5, 'f', 't', 'f', 1, 1, 0, 'f', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-23 16:00:00+00', '2026-06-23 16:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (9, NULL, 'device', 'admin', 'DeviceManagement', 'menu.device', 'lucide:qr-code', 'f', 'f', NULL, '/device', NULL, 4.2, 'f', 't', 'f', 1, 1, 1, 't', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-25 01:21:34.631155+00', '2026-07-21 06:30:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (204, 2, 'demos:artemis', 'admin', 'ArtemisDemo', 'menu.demos.artemis', 'lucide:send', 'f', 'f', '/demos/artemis/ArtemisDemo', '/demos/artemis', NULL, 4, 'f', 't', 'f', 0, NULL, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-18 00:00:00+00', '2026-06-18 00:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (900001, 304, 'iam:social', 'admin', 'ThirdPlatform', 'menu.system.config.thirdPlatform', 'lucide:share-2', 'f', 'f', '/system/config/third-platform/ThirdPlatform', '/system/config/third-platform', NULL, 5, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-13 02:15:41.505142+00');
INSERT INTO "public"."iam_perm_menu" VALUES (4040103, 4040130, 'merchant:profile', 'admin', 'MchInfoManage', 'menu.payment.merchant.profile', NULL, 't', 'f', '/payment/merchant/manage/info/MchInfoManage', '/payment/merchant/manage/info', NULL, 1, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-11 03:37:15.424776+00');
@@ -143,7 +145,6 @@ INSERT INTO "public"."iam_perm_menu" VALUES (603, 6, 'trade:refund', 'admin', 'R
INSERT INTO "public"."iam_perm_menu" VALUES (309, 308, 'system:notify:notice', 'admin', 'SystemNotify', 'menu.system.notify.notice', 'lucide:megaphone', 'f', 'f', '/system/notify/notice/NoticeList', '/system/notify/notice', NULL, 20, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, '2026-06-24 16:00:00+00', '2026-06-24 16:00:00+00', NULL);
INSERT INTO "public"."iam_perm_menu" VALUES (308, 3, 'system:notify', 'admin', 'SystemNotice', 'menu.system.notify', 'lucide:bell', 'f', 'f', NULL, '/system/notify', '/system/notify/notice', 20, 'f', 't', 'f', 1, 1, 0, 'f', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-01 07:16:30.927645+00', '2026-07-01 07:16:30.927645+00');
INSERT INTO "public"."iam_perm_menu" VALUES (40502, 40508, 'payment:isv', 'admin', 'AlipayIsvAppManage', 'menu.payment.config.alipayApp', NULL, 't', 'f', '/payment/channel/alipay/manage/app/AlipayIsvAppManage', '/payment/config/product/app-manage', NULL, 3, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-14 00:00:00+00', '2026-07-11 03:37:15.43231+00');
INSERT INTO "public"."iam_perm_menu" VALUES (901, 4, 'device:qrcode', 'admin', 'DeviceQrCode', 'menu.device.qrcode', 'lucide:qr-code', 'f', 'f', '/payment/device/qrcode/DeviceQrCode', '/device/qrcode', NULL, 3, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-25 01:21:34.634639+00', '2026-06-25 12:51:25.24304+00');
INSERT INTO "public"."iam_perm_menu" VALUES (4040119, 4040132, 'channel:app', 'admin', 'WechatIsvMchAppManage', 'menu.payment.merchant.channelMerchant.wechatIsvApp', NULL, 't', 'f', '/payment/channel/wechat/manage/mch/isv-app/WechatIsvMchAppManage', '/payment/merchant/channel-merchant/wechat-isv-mch-app-manage', NULL, 14, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-05 01:49:32.557374+00', '2026-07-11 03:37:15.430392+00');
INSERT INTO "public"."iam_perm_menu" VALUES (30403, 301, 'system:protocol', 'admin', 'UserProtocol', 'menu.system.basic.protocol', 'lucide:file-text', 'f', 'f', '/system/basic/protocol/UserProtocolList', '/system/basic/protocol', NULL, 2, 'f', 't', 'f', 1, 1, 1, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-24 10:32:13.183371+00', '2026-07-16 08:43:35.038937+00');
INSERT INTO "public"."iam_perm_menu" VALUES (803, 8, 'develop:auth', 'admin', 'ChannelAuth', 'menu.develop.auth', 'lucide:key-round', 'f', 'f', '/payment/develop/auth/ChannelAuth', '/develop/auth', NULL, 3, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-09 16:00:00+00', '2026-07-09 16:00:00+00');

View File

@@ -313,3 +313,83 @@ DROP INDEX IF EXISTS "idx_pay_callback_record_channel";
ALTER TABLE "public"."pay_callback_record" DROP COLUMN IF EXISTS "channel";
CREATE INDEX IF NOT EXISTS "idx_pay_callback_record_product" ON "public"."pay_callback_record" USING btree ("product");
-- ----------------------------
-- 数据修复: 成功结算类资金凭证补齐 posted_amount
-- 报表成交口径为 status=success AND posted_amount>0若历史成功单未走
-- PayUniHandleService 回写导致 posted_amount 仍为 0工作台/分析页金额会全 0。
-- 预授权冻结(trade_type=authorize) 入账金额恒为 0故意排除。
-- 可重复执行(幂等)。
-- ----------------------------
UPDATE "public"."pay_trade"
SET "posted_amount" = "amount"
WHERE "status" = 'success'
AND "trade_type" <> 'authorize'
AND COALESCE("posted_amount", 0) = 0
AND COALESCE("amount", 0) > 0;
-- ----------------------------
-- 数据修复: 补齐支付渠道 provider(渠道分布/成功率报表依赖)
-- 报表 SQL 强制 provider IS NOT NULL历史单创建时未写 provider 会导致四张渠道图全空。
-- 优先用目录表 pay_md_provider_method(method→provider) 回填容器, 再同步到 pay_trade。
-- 可重复执行(幂等)。
-- ----------------------------
-- 1) 普通支付容器: method → provider
UPDATE "public"."pay_normal_order" o
SET "provider" = m."provider"
FROM "public"."pay_md_provider_method" m
WHERE o."provider" IS NULL
AND o."method" IS NOT NULL
AND o."method" = m."method"
AND m."deleted" = false
AND m."provider" IS NOT NULL;
-- 2) 网关支付容器: method → provider
UPDATE "public"."pay_gateway_order" o
SET "provider" = m."provider"
FROM "public"."pay_md_provider_method" m
WHERE o."provider" IS NULL
AND o."method" IS NOT NULL
AND o."method" = m."method"
AND m."deleted" = false
AND m."provider" IS NOT NULL;
-- 3) 资金凭证: 从普通支付容器同步
UPDATE "public"."pay_trade" t
SET "provider" = o."provider"
FROM "public"."pay_normal_order" o
WHERE t."trade_type" = 'normal'
AND t."container_id" = o."id"
AND t."provider" IS NULL
AND o."provider" IS NOT NULL;
-- 4) 资金凭证: 从网关支付容器同步
UPDATE "public"."pay_trade" t
SET "provider" = o."provider"
FROM "public"."pay_gateway_order" o
WHERE t."trade_type" = 'gateway'
AND t."container_id" = o."id"
AND t."provider" IS NULL
AND o."provider" IS NOT NULL;
-- 5) 资金凭证兜底: 容器仍无 provider 时直接 JOIN 目录表(按容器 method)
UPDATE "public"."pay_trade" t
SET "provider" = m."provider"
FROM "public"."pay_normal_order" o
JOIN "public"."pay_md_provider_method" m
ON m."method" = o."method" AND m."deleted" = false AND m."provider" IS NOT NULL
WHERE t."trade_type" = 'normal'
AND t."container_id" = o."id"
AND t."provider" IS NULL
AND o."method" IS NOT NULL;
UPDATE "public"."pay_trade" t
SET "provider" = m."provider"
FROM "public"."pay_gateway_order" o
JOIN "public"."pay_md_provider_method" m
ON m."method" = o."method" AND m."deleted" = false AND m."provider" IS NOT NULL
WHERE t."trade_type" = 'gateway'
AND t."container_id" = o."id"
AND t."provider" IS NULL
AND o."method" IS NOT NULL;

View File

@@ -113,7 +113,7 @@ public class DashboardTradeController {
// ===== 时段 =====
@Operation(summary = "24小时时段分布(指定天数或自定义区间)")
@Operation(summary = "时段分布(日均): 区间内按小时汇总后除以天数, 补齐 0-23")
@GetMapping("/hourly-dist")
public Result<List<HourlyDistItemResult>> hourlyDist(
@RequestParam(defaultValue = "7") int days,

View File

@@ -184,15 +184,19 @@ public class DashboardTradeService {
// ===== 时段分布 =====
/// 24 小时时段分布(指定天数), 补齐 0-23 缺失小时
/// 时段分布(日均, 指定天数): 区间内按小时汇总后 ÷ 天数, 补齐 0-23
public List<HourlyDistItemResult> hourlyDist(int days) {
OffsetDateTime[] range = daysRange(days);
return fillHourlyDist(tradeReportMapper.hourlyDist(range[0], range[1]));
int daysSpan = clampDays(days);
return toDailyAverage(fillHourlyDist(tradeReportMapper.hourlyDist(range[0], range[1])), daysSpan);
}
/// 24 小时时段分布(自定义区间)
/// 时段分布(日均, 自定义区间): 区间内按小时汇总后 ÷ 天数, 补齐 0-23
public List<HourlyDistItemResult> hourlyDist(String startStr, String endStr) {
return fillHourlyDist(tradeReportMapper.hourlyDist(parseDateStart(startStr), parseDateEndExclusive(endStr)));
OffsetDateTime start = parseDateStart(startStr);
OffsetDateTime end = parseDateEndExclusive(endStr);
long daysSpan = Math.max(1L, java.time.Duration.between(start, end).toDays());
return toDailyAverage(fillHourlyDist(tradeReportMapper.hourlyDist(start, end)), daysSpan);
}
// ===== 金额区间分桶 =====
@@ -303,7 +307,7 @@ public class DashboardTradeService {
return result;
}
/// 时段分布补齐 0-23 缺失小时
/// 时段分布补齐 0-23 缺失小时(此时仍为区间合计, 日均见 [toDailyAverage])
private List<HourlyDistItemResult> fillHourlyDist(List<HourlyDistItemResult> rows) {
Map<Integer, HourlyDistItemResult> indexed = new HashMap<>(rows.size());
for (HourlyDistItemResult row : rows) {
@@ -313,14 +317,33 @@ public class DashboardTradeService {
for (int h = 0; h < 24; h++) {
HourlyDistItemResult row = indexed.get(h);
if (row != null) {
if (row.getCount() == null) {
row.setCount(0D);
}
if (row.getAmount() == null) {
row.setAmount(0L);
}
result.add(row);
} else {
result.add(new HourlyDistItemResult().setHour(h).setAmount(0L).setCount(0L));
result.add(new HourlyDistItemResult().setHour(h).setAmount(0L).setCount(0D));
}
}
return result;
}
/// 将区间内按小时汇总的金额/笔数转为日均(天数至少为 1)
/// 笔数保留 1 位小数; 金额(分)四舍五入为 Long
private List<HourlyDistItemResult> toDailyAverage(List<HourlyDistItemResult> rows, long daysSpan) {
long days = Math.max(1L, daysSpan);
for (HourlyDistItemResult row : rows) {
long amount = row.getAmount() == null ? 0L : row.getAmount();
double count = row.getCount() == null ? 0D : row.getCount();
row.setAmount(Math.round((double) amount / days));
row.setCount(Math.round(count * 10.0 / days) / 10.0);
}
return rows;
}
/// 金额区间补齐 5 个缺失桶(保持固定顺序)
private List<AmountRangeItemResult> fillAmountRange(List<AmountRangeItemResult> rows) {
Map<String, Long> indexed = new HashMap<>(rows.size());

View File

@@ -2,9 +2,7 @@ package cn.daxpay.open.payment.strategy;
import cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum;
import cn.daxpay.open.platform.core.exception.business.UnsupportedAbilityException;
import cn.daxpay.open.payment.strategy.PaymentStrategy;
import cn.daxpay.open.payment.strategy.product.AbsProductStrategy;
import cn.daxpay.open.payment.strategy.ProductStrategySupport;
import cn.hutool.extra.spring.SpringUtil;
import lombok.experimental.UtilityClass;

View File

@@ -74,7 +74,7 @@ public class PayTrade extends MchBaseEntity {
/// 通道商户号(冗余自业务容器, 路由确定后写入; 权威在容器 channelMchNo)
private String channelMchNo;
/// 支付渠道(冗余自业务容器, 支付成功sync后回填; 权威在容器 provider)
/// 支付渠道(冗余自业务容器; 下单时由 method→[PayProviderEnum] 写入, 成功路径兜底补齐)
/// @see cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum
private String provider;

View File

@@ -136,8 +136,8 @@ public interface TradeReportMapper extends MPJBaseMapper<PayTrade> {
// ===== 时段分布 =====
/// 24 小时时段分布: 按 CST 小时(0-23)分组, 返回各时段成交金额(分)与笔数
/// 仅含有成交的小时(Service 补齐 0-23 缺失时段)
/// 24 小时时段分布: 按 CST 小时(0-23)分组, 返回各时段成交金额(分)与笔数(区间合计)
/// 仅含有成交的小时(Service 补齐 0-23 缺失时段后, 再除以区间天数得到日均)
@Select("SELECT EXTRACT(HOUR FROM pay_time AT TIME ZONE 'Asia/Shanghai')::int AS hour, "
+ "COALESCE(SUM(posted_amount), 0)::bigint AS amount, "
+ "COUNT(*)::bigint AS count "

View File

@@ -6,22 +6,22 @@ import lombok.experimental.Accessors;
/// # 时段分布单项结果
///
/// 一小时区间内的成交金额与笔数(按业务时区 Asia/Shanghai 取 HOUR).
/// 按业务时区 Asia/Shanghai 取 HOUR(0-23) 在所选区间内汇总后, 由 Service 除以区间天数得到**日均**。
/// Service 层负责补齐 0-23 缺失小时, 保证前端柱图完整。
@Data
@Accessors(chain = true)
@Schema(title = "时段分布单项")
@Schema(title = "时段分布单项(日均)")
public class HourlyDistItemResult {
/// 小时(0-23)
@Schema(description = "小时(0-23)")
private Integer hour;
/// 成交金额(分)
@Schema(description = "成交金额(分)")
/// 日均成交金额(分, 四舍五入)
@Schema(description = "日均成交金额(分)")
private Long amount;
/// 成交笔数
@Schema(description = "成交笔数")
private Long count;
/// 日均成交笔数(保留 1 位小数)
@Schema(description = "日均成交笔数")
private Double count;
}

View File

@@ -15,7 +15,9 @@ import cn.daxpay.open.payment.trade.order.entity.PayTrade;
import cn.daxpay.open.payment.trade.notice.service.TradeNoticeBridge;
import cn.daxpay.open.payment.trade.runtime.service.plugin.PayPluginAssistService;
import cn.daxpay.open.payment.trade.util.PayTradeAmountUtil;
import cn.daxpay.open.payment.trade.util.PayTradeProviderUtil;
import cn.daxpay.open.platform.core.enums.pay.notice.NoticeEventEnum;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -47,10 +49,11 @@ public class PayUniHandleService {
if (result.getRelationOrderNo() != null) {
trade.setRelationOrderNo(result.getRelationOrderNo());
}
// 按 status/tradeType 回写入账金额后再落库
updateTradeWithPosted(trade);
if (isGateway(trade)) {
GatewayPayOrder order = gatewayPayOrderManager.findById(trade.getContainerId()).orElse(null);
// 渠道分布报表依赖 trade.provider: 空则从容器/method 兜底
applyProviderFallback(trade, order);
updateTradeWithPosted(trade);
if (order != null) {
applyGatewayReceipts(order, result);
if (Objects.equals(trade.getStatus(), PayFundStatusEnum.SUCCESS.getCode())) {
@@ -61,6 +64,8 @@ public class PayUniHandleService {
}
} else {
NormalPayOrder order = payNormalOrderManager.findById(trade.getContainerId()).orElse(null);
applyProviderFallback(trade, order);
updateTradeWithPosted(trade);
if (order != null) {
applyNormalReceipts(order, result);
if (Objects.equals(trade.getStatus(), PayFundStatusEnum.SUCCESS.getCode())) {
@@ -80,13 +85,14 @@ public class PayUniHandleService {
/// 支付成功后续处理(同步/回调路径), 含通道回执写容器
public void paySuccess(PayTrade trade, PaySyncResultBo syncResult) {
updateTradeWithPosted(trade);
if (isGateway(trade)) {
GatewayPayOrder order = gatewayPayOrderManager.findById(trade.getContainerId()).orElse(null);
applyGatewaySyncReceipts(trade, order, syncResult);
applyProviderFallback(trade, order);
updateTradeWithPosted(trade);
if (order != null) {
order.setStatus(GatewayOrderStatusEnum.PAID.getCode());
order.setPayTime(trade.getPayTime());
applyGatewaySyncReceipts(trade, order, syncResult);
gatewayPayOrderManager.updateById(order);
}
// 商户出站通知(系统协议)
@@ -95,10 +101,12 @@ public class PayUniHandleService {
return;
}
NormalPayOrder order = payNormalOrderManager.findById(trade.getContainerId()).orElse(null);
applyNormalSyncReceipts(trade, order, syncResult);
applyProviderFallback(trade, order);
updateTradeWithPosted(trade);
if (order != null) {
order.setStatus(NormalPayOrderStatusEnum.PAID.getCode());
order.setPayTime(trade.getPayTime());
applyNormalSyncReceipts(trade, order, syncResult);
payNormalOrderManager.updateById(order);
}
// 商户出站通知(系统协议)
@@ -108,8 +116,17 @@ public class PayUniHandleService {
/// 支付成功后续处理(回调路径, 无回执详情)
public void paySuccess(PayTrade trade) {
updateTradeWithPosted(trade);
markContainerPaid(trade);
if (isGateway(trade)) {
GatewayPayOrder order = gatewayPayOrderManager.findById(trade.getContainerId()).orElse(null);
applyProviderFallback(trade, order);
updateTradeWithPosted(trade);
markContainerPaid(trade, order);
} else {
NormalPayOrder order = payNormalOrderManager.findById(trade.getContainerId()).orElse(null);
applyProviderFallback(trade, order);
updateTradeWithPosted(trade);
markContainerPaid(trade, order);
}
// 商户出站通知(系统协议)
tradeNoticeBridge.dispatchPay(trade, NoticeEventEnum.PAY_SUCCESS);
payPluginAssistService.paySuccess(trade);
@@ -176,22 +193,29 @@ public class PayUniHandleService {
gatewayPayOrderManager.updateById(order);
}
private void markContainerPaid(PayTrade trade) {
if (isGateway(trade)) {
GatewayPayOrder order = gatewayPayOrderManager.findById(trade.getContainerId()).orElse(null);
if (order != null) {
order.setStatus(GatewayOrderStatusEnum.PAID.getCode());
order.setPayTime(trade.getPayTime());
gatewayPayOrderManager.updateById(order);
}
/// 容器置已支付; 若已加载实体则复用, 避免重复查询, 并同步 provider
private void markContainerPaid(PayTrade trade, GatewayPayOrder order) {
if (order == null) {
return;
}
NormalPayOrder normalOrder = payNormalOrderManager.findById(trade.getContainerId()).orElse(null);
if (normalOrder != null) {
normalOrder.setStatus(NormalPayOrderStatusEnum.PAID.getCode());
normalOrder.setPayTime(trade.getPayTime());
payNormalOrderManager.updateById(normalOrder);
order.setStatus(GatewayOrderStatusEnum.PAID.getCode());
order.setPayTime(trade.getPayTime());
if (StrUtil.isBlank(order.getProvider()) && StrUtil.isNotBlank(trade.getProvider())) {
order.setProvider(trade.getProvider());
}
gatewayPayOrderManager.updateById(order);
}
private void markContainerPaid(PayTrade trade, NormalPayOrder order) {
if (order == null) {
return;
}
order.setStatus(NormalPayOrderStatusEnum.PAID.getCode());
order.setPayTime(trade.getPayTime());
if (StrUtil.isBlank(order.getProvider()) && StrUtil.isNotBlank(trade.getProvider())) {
order.setProvider(trade.getProvider());
}
payNormalOrderManager.updateById(order);
}
/// 支付失败: 容器 FAILED
@@ -292,9 +316,15 @@ public class PayUniHandleService {
return;
}
if (Objects.nonNull(syncResult.getProvider())) {
order.setProvider(syncResult.getProvider().getCode());
String providerCode = syncResult.getProvider().getCode();
if (order != null) {
order.setProvider(providerCode);
}
// 冗余至资金凭证, 渠道分布报表/资金列表免 JOIN 容器
trade.setProvider(syncResult.getProvider().getCode());
trade.setProvider(providerCode);
}
if (order == null) {
return;
}
order.setBuyerId(syncResult.getBuyerId());
order.setTradeProduct(syncResult.getTradeProduct());
@@ -309,9 +339,15 @@ public class PayUniHandleService {
return;
}
if (Objects.nonNull(syncResult.getProvider())) {
order.setProvider(syncResult.getProvider().getCode());
String providerCode = syncResult.getProvider().getCode();
if (order != null) {
order.setProvider(providerCode);
}
// 冗余至资金凭证, 渠道分布报表/资金列表免 JOIN 容器
trade.setProvider(syncResult.getProvider().getCode());
trade.setProvider(providerCode);
}
if (order == null) {
return;
}
order.setBuyerId(syncResult.getBuyerId());
order.setTradeProduct(syncResult.getTradeProduct());
@@ -321,6 +357,33 @@ public class PayUniHandleService {
order.setErrorMsg(null);
}
/// trade.provider 为空时从容器 provider / method 兜底, 并回写容器空 provider
private void applyProviderFallback(PayTrade trade, NormalPayOrder order) {
String containerProvider = order != null ? order.getProvider() : null;
String method = order != null ? order.getMethod() : null;
String provider = PayTradeProviderUtil.coalesceProvider(trade.getProvider(), containerProvider, method);
if (StrUtil.isBlank(provider)) {
return;
}
trade.setProvider(provider);
if (order != null && StrUtil.isBlank(order.getProvider())) {
order.setProvider(provider);
}
}
private void applyProviderFallback(PayTrade trade, GatewayPayOrder order) {
String containerProvider = order != null ? order.getProvider() : null;
String method = order != null ? order.getMethod() : null;
String provider = PayTradeProviderUtil.coalesceProvider(trade.getProvider(), containerProvider, method);
if (StrUtil.isBlank(provider)) {
return;
}
trade.setProvider(provider);
if (order != null && StrUtil.isBlank(order.getProvider())) {
order.setProvider(provider);
}
}
private boolean isGateway(PayTrade trade) {
return Objects.equals(trade.getTradeType(), PayTradeTypeEnum.GATEWAY.getCode());
}

View File

@@ -27,6 +27,7 @@ import cn.daxpay.open.platform.core.util.TradeNoGenerateUtil;
import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformPaySecurityConfig;
import cn.daxpay.open.platform.system.service.config.security.PlatformSecurityConfigService;
import cn.daxpay.open.payment.trade.util.PayTradeInitUtil;
import cn.daxpay.open.payment.trade.util.PayTradeProviderUtil;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -57,6 +58,7 @@ public class GatewayPayHandleService {
private final PlatformSecurityConfigService platformSecurityConfigService;
/// 自注入,保证 [GatewayPayHandleService#createTrade] / [GatewayPayHandleService#paySuccess] 走 Spring 事务代理
@Lazy
private final GatewayPayHandleService self;
/// 发起网关支付
@@ -163,7 +165,8 @@ public class GatewayPayHandleService {
}
// 默认上送网关业务单号; 特殊通道返回后可覆盖
// channelMchNo 取自路由后的 payParam; storeNo 冗余自预下单容器
// channelMchNo 取自路由后的 payParam; storeNo 冗余自预下单容器; provider 由 method 派生
String provider = PayTradeProviderUtil.resolveProviderByMethod(payParam.getMethod());
PayTrade trade = PayTradeInitUtil.initProcessing(
order.getAppId(),
TradeNoGenerateUtil.pay(),
@@ -174,13 +177,16 @@ public class GatewayPayHandleService {
source,
payParam.getChannelMchNo(),
order.getStoreNo(),
order.getTitle());
order.getTitle(),
provider);
payTradeManager.save(trade);
this.fillRouteOnOrder(order, payParam, clientEnv, device);
order.setStatus(GatewayOrderStatusEnum.PAYING.getCode());
order.setChannel(channel);
order.setMethod(payParam.getMethod());
// 支付渠道: 与 trade 同步, 渠道分布报表免 JOIN
order.setProvider(provider);
order.setLimitPay(payParam.getLimitPay() != null
? String.join(",", payParam.getLimitPay()) : null);
order.setOpenid(payParam.getOpenId());

View File

@@ -23,6 +23,7 @@ import cn.daxpay.open.payment.trade.order.entity.PayTrade;
import cn.daxpay.open.payment.trade.runtime.mq.NormalPayTimeoutMessage;
import cn.daxpay.open.payment.trade.runtime.mq.PayArtemisConstants;
import cn.daxpay.open.payment.trade.util.PayTradeInitUtil;
import cn.daxpay.open.payment.trade.util.PayTradeProviderUtil;
import cn.daxpay.open.payment.unipay.param.trade.pay.NormalPayParam;
import cn.daxpay.open.payment.unipay.result.trade.pay.NormalPayResult;
import cn.hutool.core.util.StrUtil;
@@ -109,6 +110,8 @@ public class NormalPayAssistService {
// --- 支付路由 ---
normalOrder.setChannel(channel);
normalOrder.setMethod(payParam.getMethod());
// 支付渠道: 由 method 派生, 渠道分布报表/详情展示用
normalOrder.setProvider(PayTradeProviderUtil.resolveProviderByMethod(payParam.getMethod()));
normalOrder.setLimitPay(payParam.getLimitPay() != null
? String.join(",", payParam.getLimitPay()) : null);
normalOrder.setOpenid(payParam.getOpenId());
@@ -136,7 +139,7 @@ public class NormalPayAssistService {
}
/// 组装资金交易(未落库); relationOrderNo 默认=orderNo, 特殊通道支付返回后可覆盖
/// channelMchNo / storeNo 冗余自容器,资金列表免 JOIN
/// channelMchNo / storeNo / provider 冗余自容器,资金列表与渠道报表免 JOIN
private PayTrade buildPayTrade(NormalPayParam payParam, NormalPayOrder normalOrder,
String tradeNo, String orderNo, String source) {
return PayTradeInitUtil.initProcessing(
@@ -149,7 +152,8 @@ public class NormalPayAssistService {
source,
normalOrder.getChannelMchNo(),
normalOrder.getStoreNo(),
normalOrder.getTitle());
normalOrder.getTitle(),
normalOrder.getProvider());
}
/// 注册超时关单延时消息

View File

@@ -7,7 +7,7 @@ import cn.daxpay.open.platform.core.enums.pay.channel.CurrencyEnum;
/// # 资金交易初始化工具
///
/// PROCESSING 态资金凭证公共字段的单一事实源。
/// 调用方负责 tradeType / container / source / channelMchNo / storeNo 等业务差异,本工具只填
/// 调用方负责 tradeType / container / source / channelMchNo / storeNo / provider 等业务差异,本工具只填
/// 「待处理结算类交易」公共规则:
/// - currency = CNY
/// - postedAmount = 0成功后由 [cn.daxpay.open.payment.trade.runtime.service.pay.common.PayUniHandleService]
@@ -31,6 +31,7 @@ public final class PayTradeInitUtil {
/// @param channelMchNo 通道商户号(冗余自容器/路由结果,可空仅当路由未定时)
/// @param storeNo 门店号(冗余自容器,可空)
/// @param title 订单标题(冗余自容器,资金列表/工作台免JOIN
/// @param provider 支付渠道(由 method 派生,可空;权威亦可在容器 provider
public static PayTrade initProcessing(
String appId,
String tradeNo,
@@ -41,7 +42,8 @@ public final class PayTradeInitUtil {
String source,
String channelMchNo,
String storeNo,
String title) {
String title,
String provider) {
return new PayTrade()
.setAppId(appId)
.setTradeNo(tradeNo)
@@ -58,6 +60,8 @@ public final class PayTradeInitUtil {
.setSource(source)
// 轻量组织冗余: 权威在业务容器
.setChannelMchNo(channelMchNo)
.setStoreNo(storeNo);
.setStoreNo(storeNo)
// 支付渠道: 下单时由 method 派生, 渠道分布报表免 JOIN
.setProvider(provider);
}
}

View File

@@ -0,0 +1,43 @@
package cn.daxpay.open.payment.trade.util;
import cn.daxpay.open.platform.core.enums.pay.channel.PayMethodEnum;
import cn.daxpay.open.platform.core.enums.pay.channel.PayProviderEnum;
import cn.hutool.core.util.StrUtil;
import java.util.Arrays;
import java.util.Objects;
/// # 资金凭证支付渠道解析
///
/// 报表/资金列表依赖 [cn.daxpay.open.payment.trade.order.entity.PayTrade#getProvider]
/// 渠道在下单时即可由支付方式确定,不依赖三方 sync 透传。
/// [PayMethodEnum#OTHER] 无固定渠道归属时返回 null。
public final class PayTradeProviderUtil {
private PayTradeProviderUtil() {
}
/// 由支付方式编码解析支付渠道编码method 空/未知/OTHER 返回 null不抛异常
public static String resolveProviderByMethod(String methodCode) {
if (StrUtil.isBlank(methodCode)) {
return null;
}
return Arrays.stream(PayMethodEnum.values())
.filter(o -> Objects.equals(o.getCode(), methodCode))
.findFirst()
.map(PayMethodEnum::getProvider)
.map(PayProviderEnum::getCode)
.orElse(null);
}
/// 优先保留已有 provider否则用容器已存值再否则由 method 派生
public static String coalesceProvider(String tradeProvider, String containerProvider, String methodCode) {
if (StrUtil.isNotBlank(tradeProvider)) {
return tradeProvider;
}
if (StrUtil.isNotBlank(containerProvider)) {
return containerProvider;
}
return resolveProviderByMethod(methodCode);
}
}