From 365cb6ed5f561898f47165250aff7d05b8b3bf44 Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Wed, 22 Jul 2026 21:14:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(trade):=20=E4=B8=8B=E5=8D=95=E5=86=99?= =?UTF-8?q?=E5=85=A5=20provider=20=E5=B9=B6=E4=BF=AE=E5=A4=8D=E6=8A=A5?= =?UTF-8?q?=E8=A1=A8=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 资金凭证创建时由 method 派生 provider,成功路径兜底补齐;SQL 回填历史 posted_amount/provider;时段分布改为日均。 Co-authored-by: Cursor --- _config/sql/iam_perm_menu.sql | 5 +- _config/sql/update-tables.sql | 80 +++++++++++++ .../dashboard/DashboardTradeController.java | 2 +- .../dashboard/DashboardTradeService.java | 35 +++++- .../strategy/PaymentStrategyFactory.java | 2 - .../payment/trade/order/entity/PayTrade.java | 2 +- .../trade/report/dao/TradeReportMapper.java | 4 +- .../report/result/HourlyDistItemResult.java | 14 +-- .../pay/common/PayUniHandleService.java | 111 ++++++++++++++---- .../pay/gateway/GatewayPayHandleService.java | 10 +- .../pay/normal/NormalPayAssistService.java | 8 +- .../payment/trade/util/PayTradeInitUtil.java | 10 +- .../trade/util/PayTradeProviderUtil.java | 43 +++++++ 13 files changed, 274 insertions(+), 52 deletions(-) create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/util/PayTradeProviderUtil.java diff --git a/_config/sql/iam_perm_menu.sql b/_config/sql/iam_perm_menu.sql index 879bce911..a32230005 100644 --- a/_config/sql/iam_perm_menu.sql +++ b/_config/sql/iam_perm_menu.sql @@ -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'); diff --git a/_config/sql/update-tables.sql b/_config/sql/update-tables.sql index ff817e9e5..9f40d19dc 100644 --- a/_config/sql/update-tables.sql +++ b/_config/sql/update-tables.sql @@ -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; + diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/dashboard/DashboardTradeController.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/dashboard/DashboardTradeController.java index 5a666b013..54cd93cdb 100644 --- a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/dashboard/DashboardTradeController.java +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/dashboard/DashboardTradeController.java @@ -113,7 +113,7 @@ public class DashboardTradeController { // ===== 时段 ===== - @Operation(summary = "24小时时段分布(指定天数或自定义区间)") + @Operation(summary = "时段分布(日均): 区间内按小时汇总后除以天数, 补齐 0-23") @GetMapping("/hourly-dist") public Result> hourlyDist( @RequestParam(defaultValue = "7") int days, diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/dashboard/DashboardTradeService.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/dashboard/DashboardTradeService.java index 679012502..a6463215b 100644 --- a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/dashboard/DashboardTradeService.java +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/dashboard/DashboardTradeService.java @@ -184,15 +184,19 @@ public class DashboardTradeService { // ===== 时段分布 ===== - /// 24 小时时段分布(指定天数), 补齐 0-23 缺失小时 + /// 时段分布(日均, 指定天数): 区间内按小时汇总后 ÷ 天数, 补齐 0-23 public List 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 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 fillHourlyDist(List rows) { Map 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 toDailyAverage(List 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 fillAmountRange(List rows) { Map indexed = new HashMap<>(rows.size()); diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/strategy/PaymentStrategyFactory.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/strategy/PaymentStrategyFactory.java index ce83ef494..5eab7356c 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/strategy/PaymentStrategyFactory.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/strategy/PaymentStrategyFactory.java @@ -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; 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 6f849d7e1..48252d505 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 @@ -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; diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/report/dao/TradeReportMapper.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/report/dao/TradeReportMapper.java index 4c3ca4b6b..837ed0c5f 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/report/dao/TradeReportMapper.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/report/dao/TradeReportMapper.java @@ -136,8 +136,8 @@ public interface TradeReportMapper extends MPJBaseMapper { // ===== 时段分布 ===== - /// 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 " diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/report/result/HourlyDistItemResult.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/report/result/HourlyDistItemResult.java index 93315fcf2..b342dc34a 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/report/result/HourlyDistItemResult.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/report/result/HourlyDistItemResult.java @@ -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; } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/common/PayUniHandleService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/common/PayUniHandleService.java index 904b3113f..ce0085465 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/common/PayUniHandleService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/common/PayUniHandleService.java @@ -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()); } 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 8f632338b..76b9af824 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 @@ -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()); 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 f226ace5a..846a674e3 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 @@ -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()); } /// 注册超时关单延时消息 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 a948acb92..ea4b78df3 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,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); } } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/util/PayTradeProviderUtil.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/util/PayTradeProviderUtil.java new file mode 100644 index 000000000..2619dbedc --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/util/PayTradeProviderUtil.java @@ -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); + } +}