From 754a74721fda71fd72f792bc38ef68f06c8a1b1c Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Tue, 21 Jul 2026 13:47:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(notice):=20=E6=96=B0=E5=A2=9E=E5=95=86?= =?UTF-8?q?=E6=88=B7=E5=87=BA=E7=AB=99=E9=80=9A=E7=9F=A5=E7=AE=A1=E7=BA=BF?= =?UTF-8?q?=EF=BC=88=E4=BB=BB=E5=8A=A1/=E9=87=8D=E8=AF=95/=E5=8D=8F?= =?UTF-8?q?=E8=AE=AESPI=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一订单级与应用级 webhook,接入支付/退款终态与易支付协议发送,并提供管理端任务查询与手动重发。 Co-authored-by: Cursor --- _config/sql/iam_perm_menu.sql | 1 + _config/sql/update-datas.sql | 4 + _config/sql/update-tables.sql | 108 ++++++++++++++ .../trade/MchNoticeAdminController.java | 65 ++++++++ .../service/trade/MchNoticeAdminService.java | 74 ++++++++++ .../common/result/DaxNoticeResult.java | 42 ++++++ .../entity/config/MchAppNotifyConfig.java | 2 +- .../notice/command/NoticeDispatchCommand.java | 44 ++++++ .../consumer/MchNoticeSendConsumer.java | 41 ++++++ .../convert/MchNoticeRecordConvert.java | 16 ++ .../notice/convert/MchNoticeTaskConvert.java | 16 ++ .../notice/dao/MchNoticeRecordManager.java | 25 ++++ .../notice/dao/MchNoticeRecordMapper.java | 11 ++ .../notice/dao/MchNoticeTaskManager.java | 40 +++++ .../trade/notice/dao/MchNoticeTaskMapper.java | 11 ++ .../trade/notice/entity/MchNoticeRecord.java | 60 ++++++++ .../trade/notice/entity/MchNoticeTask.java | 91 ++++++++++++ .../notice/param/MchNoticeRecordQuery.java | 26 ++++ .../notice/param/MchNoticeTaskQuery.java | 42 ++++++ .../notice/protocol/NoticeProtocolSender.java | 31 ++++ .../protocol/SystemHttpSignedSender.java | 79 ++++++++++ .../notice/result/MchNoticeRecordResult.java | 37 +++++ .../notice/result/MchNoticeTaskResult.java | 73 +++++++++ .../notice/service/NoticeDispatcher.java | 114 ++++++++++++++ .../notice/service/NoticeRetryPolicy.java | 48 ++++++ .../notice/service/NoticeSendEngine.java | 139 ++++++++++++++++++ .../service/NoticeTaskScheduleService.java | 45 ++++++ .../notice/service/TradeNoticeBridge.java | 99 +++++++++++++ .../trade/notice/util/NoticeTxUtil.java | 26 ++++ .../runtime/mq/MchNoticeSendMessage.java | 15 ++ .../trade/runtime/mq/PayArtemisConstants.java | 4 + .../pay/common/PayUniHandleService.java | 19 ++- .../service/refund/RefundSettleService.java | 9 ++ .../i18n/en-US/enum/callback_notice_type.json | 3 +- .../i18n/en-US/enum/notice_content_mode.json | 4 + .../i18n/en-US/enum/notice_event.json | 7 + .../i18n/en-US/enum/notice_protocol.json | 4 + .../i18n/en-US/enum/notice_source.json | 5 + .../i18n/id-ID/enum/callback_notice_type.json | 3 +- .../i18n/id-ID/enum/notice_content_mode.json | 4 + .../i18n/id-ID/enum/notice_event.json | 7 + .../i18n/id-ID/enum/notice_protocol.json | 4 + .../i18n/id-ID/enum/notice_source.json | 5 + .../i18n/ja-JP/enum/callback_notice_type.json | 3 +- .../i18n/ja-JP/enum/notice_content_mode.json | 4 + .../i18n/ja-JP/enum/notice_event.json | 7 + .../i18n/ja-JP/enum/notice_protocol.json | 4 + .../i18n/ja-JP/enum/notice_source.json | 5 + .../i18n/ko-KR/enum/callback_notice_type.json | 3 +- .../i18n/ko-KR/enum/notice_content_mode.json | 4 + .../i18n/ko-KR/enum/notice_event.json | 7 + .../i18n/ko-KR/enum/notice_protocol.json | 4 + .../i18n/ko-KR/enum/notice_source.json | 5 + .../i18n/ms-MY/enum/callback_notice_type.json | 3 +- .../i18n/ms-MY/enum/notice_content_mode.json | 4 + .../i18n/ms-MY/enum/notice_event.json | 7 + .../i18n/ms-MY/enum/notice_protocol.json | 4 + .../i18n/ms-MY/enum/notice_source.json | 5 + .../i18n/th-TH/enum/callback_notice_type.json | 3 +- .../i18n/th-TH/enum/notice_content_mode.json | 4 + .../i18n/th-TH/enum/notice_event.json | 7 + .../i18n/th-TH/enum/notice_protocol.json | 4 + .../i18n/th-TH/enum/notice_source.json | 5 + .../i18n/vi-VN/enum/callback_notice_type.json | 3 +- .../i18n/vi-VN/enum/notice_content_mode.json | 4 + .../i18n/vi-VN/enum/notice_event.json | 7 + .../i18n/vi-VN/enum/notice_protocol.json | 4 + .../i18n/vi-VN/enum/notice_source.json | 5 + .../i18n/zh-CN/enum/callback_notice_type.json | 3 +- .../i18n/zh-CN/enum/notice_content_mode.json | 4 + .../i18n/zh-CN/enum/notice_event.json | 7 + .../i18n/zh-CN/enum/notice_protocol.json | 4 + .../i18n/zh-CN/enum/notice_source.json | 5 + .../i18n/zh-HK/enum/callback_notice_type.json | 3 +- .../i18n/zh-HK/enum/notice_content_mode.json | 4 + .../i18n/zh-HK/enum/notice_event.json | 7 + .../i18n/zh-HK/enum/notice_protocol.json | 4 + .../i18n/zh-HK/enum/notice_source.json | 5 + .../i18n/zh-TW/enum/callback_notice_type.json | 3 +- .../i18n/zh-TW/enum/notice_content_mode.json | 4 + .../i18n/zh-TW/enum/notice_event.json | 7 + .../i18n/zh-TW/enum/notice_protocol.json | 4 + .../i18n/zh-TW/enum/notice_source.json | 5 + .../open/platform/core/code/PermCodes.java | 6 + .../pay/notice/CallbackNoticeTypeEnum.java | 4 +- .../pay/notice/NoticeContentModeEnum.java | 28 ++++ .../enums/pay/notice/NoticeEventEnum.java | 41 ++++++ .../enums/pay/notice/NoticeProtocolEnum.java | 29 ++++ .../enums/pay/notice/NoticeSourceEnum.java | 30 ++++ .../pay/notice/NotifyContentTypeEnum.java | 4 +- .../plugin/easypay/entity/EasyPayOrder.java | 2 +- .../easypay/notice/EasyPayNoticeSender.java | 130 ++++++++++++++++ .../api/v1/EasyPayCallbackV1Result.java | 44 ++++++ .../api/v2/EasyPayCallbackV2Result.java | 61 ++++++++ .../service/api/v2/EasyPayPayV2Service.java | 2 +- .../strategy/EasyPayPluginStrategy.java | 39 ++++- 96 files changed, 2089 insertions(+), 18 deletions(-) create mode 100644 daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/trade/MchNoticeAdminController.java create mode 100644 daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/trade/MchNoticeAdminService.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/result/DaxNoticeResult.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/command/NoticeDispatchCommand.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/consumer/MchNoticeSendConsumer.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/convert/MchNoticeRecordConvert.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/convert/MchNoticeTaskConvert.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeRecordManager.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeRecordMapper.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeTaskManager.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeTaskMapper.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/entity/MchNoticeRecord.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/entity/MchNoticeTask.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/param/MchNoticeRecordQuery.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/param/MchNoticeTaskQuery.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/protocol/NoticeProtocolSender.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/protocol/SystemHttpSignedSender.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/result/MchNoticeRecordResult.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/result/MchNoticeTaskResult.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeDispatcher.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeRetryPolicy.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeSendEngine.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeTaskScheduleService.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/TradeNoticeBridge.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/util/NoticeTxUtil.java create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/mq/MchNoticeSendMessage.java create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_content_mode.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_event.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_protocol.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_source.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_content_mode.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_event.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_protocol.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_source.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_content_mode.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_event.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_protocol.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_source.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_content_mode.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_event.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_protocol.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_source.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_content_mode.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_event.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_protocol.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_source.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_content_mode.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_event.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_protocol.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_source.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_content_mode.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_event.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_protocol.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_source.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_content_mode.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_event.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_protocol.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_source.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_content_mode.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_event.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_protocol.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_source.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_content_mode.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_event.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_protocol.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_source.json create mode 100644 daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeContentModeEnum.java create mode 100644 daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeEventEnum.java create mode 100644 daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeProtocolEnum.java create mode 100644 daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeSourceEnum.java create mode 100644 daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/notice/EasyPayNoticeSender.java create mode 100644 daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/result/api/v1/EasyPayCallbackV1Result.java create mode 100644 daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/result/api/v2/EasyPayCallbackV2Result.java diff --git a/_config/sql/iam_perm_menu.sql b/_config/sql/iam_perm_menu.sql index 60509fc1e..82ef78dab 100644 --- a/_config/sql/iam_perm_menu.sql +++ b/_config/sql/iam_perm_menu.sql @@ -141,6 +141,7 @@ INSERT INTO "public"."iam_perm_menu" VALUES (40503, 40508, 'payment:isv', 'admin INSERT INTO "public"."iam_perm_menu" VALUES (40105, 4, 'payment:config:product-config', 'admin', 'ProductConfig', 'menu.payment.config.productConfig', 'lucide:layout-grid', 'f', 'f', '/payment/config/product/ProductConfig', '/payment/config/product', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-13 00:00:00+00', '2026-07-13 13:02:20.703887+00'); INSERT INTO "public"."iam_perm_menu" VALUES (4040116, 401, 'payment:platform:provider', 'admin', 'PayProviderManage', 'menu.payment.platform.provider.manage', 'lucide:wallet', 'f', 'f', '/payment/masterdata/provider/PayProviderList', '/payment/platform/pay-provider', NULL, 0.5, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-17 10:00:31.640482+00', '2026-06-27 13:01:24.318649+00'); INSERT INTO "public"."iam_perm_menu" VALUES (603, 6, 'trade:refund', 'admin', 'RefundOrderList', 'menu.trade.refundOrder', 'lucide:rotate-ccw', 'f', 'f', '/payment/order/RefundOrderList', '/trade/refund-order', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-03 16:00:00+00', '2026-07-17 06:18:15.476169+00'); +INSERT INTO "public"."iam_perm_menu" VALUES (605, 6, 'trade:mch-notice', 'admin', 'MchNoticeTaskList', 'menu.trade.mchNotice', 'lucide:bell-ring', 'f', 'f', '/payment/notice/MchNoticeTaskList', '/trade/mch-notice', NULL, 4, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-21 03:00:00+00', '2026-07-21 03:00:00+00'); 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'); diff --git a/_config/sql/update-datas.sql b/_config/sql/update-datas.sql index e69de29bb..34a9c665b 100644 --- a/_config/sql/update-datas.sql +++ b/_config/sql/update-datas.sql @@ -0,0 +1,4 @@ +-- 增量数据脚本(PostgreSQL) +-- 商户出站通知菜单 +INSERT INTO "public"."iam_perm_menu" VALUES (605, 6, 'trade:mch-notice', 'admin', 'MchNoticeTaskList', 'menu.trade.mchNotice', 'lucide:bell-ring', 'f', 'f', '/payment/notice/MchNoticeTaskList', '/trade/mch-notice', NULL, 4, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-21 03:00:00+00', '2026-07-21 03:00:00+00') +ON CONFLICT ("id") DO NOTHING; diff --git a/_config/sql/update-tables.sql b/_config/sql/update-tables.sql index 0c64b071a..b38efdb1b 100644 --- a/_config/sql/update-tables.sql +++ b/_config/sql/update-tables.sql @@ -132,3 +132,111 @@ CREATE INDEX "idx_pay_risk_hit_hit_type_value" ON "public"."pay_risk_hit" USING CREATE INDEX "idx_pay_risk_hit_handle_status" ON "public"."pay_risk_hit" USING btree ("handle_status"); CREATE INDEX "idx_pay_risk_hit_trade_no" ON "public"."pay_risk_hit" USING btree ("trade_no"); CREATE INDEX "idx_pay_risk_hit_create_time" ON "public"."pay_risk_hit" USING btree ("create_time" DESC); + + +-- ---------------------------- +-- Table structure for mch_notice_task +-- 商户出站通知任务 +-- ---------------------------- +DROP TABLE IF EXISTS "public"."mch_notice_record"; +DROP TABLE IF EXISTS "public"."mch_notice_task"; +CREATE TABLE "public"."mch_notice_task" ( + "id" int8 NOT NULL, + "creator" int8, + "create_time" timestamptz(6), + "last_modifier" int8, + "last_modified_time" timestamptz(6), + "version" int4 NOT NULL DEFAULT 0, + "deleted" bool NOT NULL DEFAULT false, + "mch_no" varchar(32) NOT NULL, + "app_id" varchar(64) NOT NULL, + "biz_id" int8, + "biz_no" varchar(64) NOT NULL, + "event" varchar(64) NOT NULL, + "protocol" varchar(32) NOT NULL, + "source" varchar(16) NOT NULL, + "content_mode" varchar(16) NOT NULL, + "content" text, + "url" varchar(512) NOT NULL, + "success" bool NOT NULL DEFAULT false, + "send_count" int4 NOT NULL DEFAULT 0, + "delay_count" int4 NOT NULL DEFAULT 0, + "next_time" timestamptz(6), + "latest_time" timestamptz(6), + "error_msg" varchar(300) +) +; +COMMENT ON COLUMN "public"."mch_notice_task"."id" IS '主键'; +COMMENT ON COLUMN "public"."mch_notice_task"."creator" IS '创建者ID'; +COMMENT ON COLUMN "public"."mch_notice_task"."create_time" IS '创建时间'; +COMMENT ON COLUMN "public"."mch_notice_task"."last_modifier" IS '最后修改ID'; +COMMENT ON COLUMN "public"."mch_notice_task"."last_modified_time" IS '最后修改时间'; +COMMENT ON COLUMN "public"."mch_notice_task"."version" IS '版本号'; +COMMENT ON COLUMN "public"."mch_notice_task"."deleted" IS '删除标志'; +COMMENT ON COLUMN "public"."mch_notice_task"."mch_no" IS '商户号'; +COMMENT ON COLUMN "public"."mch_notice_task"."app_id" IS '应用号'; +COMMENT ON COLUMN "public"."mch_notice_task"."biz_id" IS '业务主键ID'; +COMMENT ON COLUMN "public"."mch_notice_task"."biz_no" IS '业务单号'; +COMMENT ON COLUMN "public"."mch_notice_task"."event" IS '通知事件码(如 pay.success / refund.close)'; +COMMENT ON COLUMN "public"."mch_notice_task"."protocol" IS '通知协议: system / easy_pay'; +COMMENT ON COLUMN "public"."mch_notice_task"."source" IS 'URL来源: order / app / protocol'; +COMMENT ON COLUMN "public"."mch_notice_task"."content_mode" IS '内容策略: snapshot / ref'; +COMMENT ON COLUMN "public"."mch_notice_task"."content" IS '通知内容(快照JSON或引用指针JSON)'; +COMMENT ON COLUMN "public"."mch_notice_task"."url" IS '商户接收地址'; +COMMENT ON COLUMN "public"."mch_notice_task"."success" IS '是否发送成功'; +COMMENT ON COLUMN "public"."mch_notice_task"."send_count" IS '已发送次数'; +COMMENT ON COLUMN "public"."mch_notice_task"."delay_count" IS '延迟重试次数'; +COMMENT ON COLUMN "public"."mch_notice_task"."next_time" IS '下次发送时间'; +COMMENT ON COLUMN "public"."mch_notice_task"."latest_time" IS '最后发送时间'; +COMMENT ON COLUMN "public"."mch_notice_task"."error_msg" IS '最近一次错误摘要'; +COMMENT ON TABLE "public"."mch_notice_task" IS '商户出站通知任务'; + +ALTER TABLE "public"."mch_notice_task" ADD CONSTRAINT "mch_notice_task_pkey" PRIMARY KEY ("id"); +CREATE UNIQUE INDEX "uk_mch_notice_task_biz" ON "public"."mch_notice_task" USING btree ("mch_no", "app_id", "event", "biz_no", "protocol", "source") WHERE deleted = false; +CREATE INDEX "idx_mch_notice_task_biz_no" ON "public"."mch_notice_task" USING btree ("biz_no"); +CREATE INDEX "idx_mch_notice_task_success" ON "public"."mch_notice_task" USING btree ("success"); +CREATE INDEX "idx_mch_notice_task_create_time" ON "public"."mch_notice_task" USING btree ("create_time" DESC); + + +-- ---------------------------- +-- Table structure for mch_notice_record +-- 商户出站通知发送记录 +-- ---------------------------- +CREATE TABLE "public"."mch_notice_record" ( + "id" int8 NOT NULL, + "creator" int8, + "create_time" timestamptz(6), + "last_modifier" int8, + "last_modified_time" timestamptz(6), + "version" int4 NOT NULL DEFAULT 0, + "deleted" bool NOT NULL DEFAULT false, + "mch_no" varchar(32) NOT NULL, + "task_id" int8 NOT NULL, + "req_count" int4 NOT NULL, + "send_type" varchar(16) NOT NULL, + "success" bool NOT NULL DEFAULT false, + "http_status" int4, + "error_msg" varchar(300), + "request_digest" varchar(500) +) +; +COMMENT ON COLUMN "public"."mch_notice_record"."id" IS '主键'; +COMMENT ON COLUMN "public"."mch_notice_record"."creator" IS '创建者ID'; +COMMENT ON COLUMN "public"."mch_notice_record"."create_time" IS '创建时间'; +COMMENT ON COLUMN "public"."mch_notice_record"."last_modifier" IS '最后修改ID'; +COMMENT ON COLUMN "public"."mch_notice_record"."last_modified_time" IS '最后修改时间'; +COMMENT ON COLUMN "public"."mch_notice_record"."version" IS '版本号'; +COMMENT ON COLUMN "public"."mch_notice_record"."deleted" IS '删除标志'; +COMMENT ON COLUMN "public"."mch_notice_record"."mch_no" IS '商户号'; +COMMENT ON COLUMN "public"."mch_notice_record"."task_id" IS '通知任务ID'; +COMMENT ON COLUMN "public"."mch_notice_record"."req_count" IS '本次对应的发送序号'; +COMMENT ON COLUMN "public"."mch_notice_record"."send_type" IS '发送类型: auto / manual'; +COMMENT ON COLUMN "public"."mch_notice_record"."success" IS '是否成功'; +COMMENT ON COLUMN "public"."mch_notice_record"."http_status" IS 'HTTP状态码'; +COMMENT ON COLUMN "public"."mch_notice_record"."error_msg" IS '错误摘要'; +COMMENT ON COLUMN "public"."mch_notice_record"."request_digest" IS '请求摘要(截断)'; +COMMENT ON TABLE "public"."mch_notice_record" IS '商户出站通知发送记录'; + +ALTER TABLE "public"."mch_notice_record" ADD CONSTRAINT "mch_notice_record_pkey" PRIMARY KEY ("id"); +CREATE INDEX "idx_mch_notice_record_task_id" ON "public"."mch_notice_record" USING btree ("task_id"); +CREATE INDEX "idx_mch_notice_record_create_time" ON "public"."mch_notice_record" USING btree ("create_time" DESC); diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/trade/MchNoticeAdminController.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/trade/MchNoticeAdminController.java new file mode 100644 index 000000000..826c947ef --- /dev/null +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/trade/MchNoticeAdminController.java @@ -0,0 +1,65 @@ +package cn.daxpay.open.payment.admin.controller.trade; + +import cn.daxpay.open.payment.admin.service.trade.MchNoticeAdminService; +import cn.daxpay.open.payment.trade.notice.param.MchNoticeRecordQuery; +import cn.daxpay.open.payment.trade.notice.param.MchNoticeTaskQuery; +import cn.daxpay.open.payment.trade.notice.result.MchNoticeRecordResult; +import cn.daxpay.open.payment.trade.notice.result.MchNoticeTaskResult; +import cn.daxpay.open.platform.core.annotation.PermCode; +import cn.daxpay.open.platform.core.code.PermCodes; +import cn.daxpay.open.platform.core.rest.Res; +import cn.daxpay.open.platform.core.rest.param.PageParam; +import cn.daxpay.open.platform.core.rest.result.PageResult; +import cn.daxpay.open.platform.core.rest.result.Result; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/// # 商户出站通知(管理) +/// +@PermCode(menuCode = PermCodes.Trade.Notice.MENU) +@Validated +@Tag(name = "商户出站通知(管理)") +@RestController +@RequestMapping("/admin/mch-notice") +@RequiredArgsConstructor +public class MchNoticeAdminController { + + private final MchNoticeAdminService mchNoticeAdminService; + + @PermCode(code = PermCodes.Action.VIEW) + @Operation(summary = "通知任务分页") + @GetMapping("/task/page") + public Result> pageTask(PageParam pageParam, MchNoticeTaskQuery query) { + return Res.ok(mchNoticeAdminService.pageTask(pageParam, query)); + } + + @PermCode(code = PermCodes.Action.VIEW) + @Operation(summary = "通知任务详情") + @GetMapping("/task/get-by-id") + public Result getTaskById( + @NotNull(message = "{validation.field.id.notNull}") Long id) { + return Res.ok(mchNoticeAdminService.findTaskById(id)); + } + + @PermCode(code = PermCodes.Action.MANAGE) + @Operation(summary = "手动重发通知") + @PostMapping("/task/resend") + public Result resend(@NotNull(message = "{validation.field.id.notNull}") Long id) { + mchNoticeAdminService.resend(id); + return Res.ok(); + } + + @PermCode(code = PermCodes.Action.VIEW) + @Operation(summary = "发送记录分页") + @GetMapping("/record/page") + public Result> pageRecord(PageParam pageParam, MchNoticeRecordQuery query) { + return Res.ok(mchNoticeAdminService.pageRecord(pageParam, query)); + } +} diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/trade/MchNoticeAdminService.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/trade/MchNoticeAdminService.java new file mode 100644 index 000000000..e7645f87b --- /dev/null +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/trade/MchNoticeAdminService.java @@ -0,0 +1,74 @@ +package cn.daxpay.open.payment.admin.service.trade; + +import cn.daxpay.open.payment.trade.notice.convert.MchNoticeRecordConvert; +import cn.daxpay.open.payment.trade.notice.convert.MchNoticeTaskConvert; +import cn.daxpay.open.payment.trade.notice.dao.MchNoticeRecordManager; +import cn.daxpay.open.payment.trade.notice.dao.MchNoticeTaskManager; +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeRecord; +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; +import cn.daxpay.open.payment.trade.notice.param.MchNoticeRecordQuery; +import cn.daxpay.open.payment.trade.notice.param.MchNoticeTaskQuery; +import cn.daxpay.open.payment.trade.notice.result.MchNoticeRecordResult; +import cn.daxpay.open.payment.trade.notice.result.MchNoticeTaskResult; +import cn.daxpay.open.payment.trade.notice.service.NoticeSendEngine; +import cn.daxpay.open.platform.common.translate.service.TransService; +import cn.daxpay.open.platform.core.exception.DataNotExistException; +import cn.daxpay.open.platform.core.rest.param.PageParam; +import cn.daxpay.open.platform.core.rest.result.PageResult; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 商户出站通知管理服务 +/// +@Slf4j +@Service +@RequiredArgsConstructor +public class MchNoticeAdminService { + + private final MchNoticeTaskManager taskManager; + private final MchNoticeRecordManager recordManager; + private final NoticeSendEngine noticeSendEngine; + private final TransService transService; + + /// 任务分页 + public PageResult pageTask(PageParam pageParam, MchNoticeTaskQuery query) { + Page page = taskManager.page(pageParam, query); + var records = page.getRecords().stream().map(MchNoticeTaskConvert.CONVERT::toResult).toList(); + PageResult pageResult = new PageResult() + .setRecords(records) + .setTotal(page.getTotal()) + .setSize(page.getSize()) + .setCurrent(page.getCurrent()); + transService.translate(pageResult); + return pageResult; + } + + /// 任务详情 + public MchNoticeTaskResult findTaskById(Long id) { + MchNoticeTask task = taskManager.findById(id) + .orElseThrow(() -> new DataNotExistException("pay.error.order.callbackTaskNotExist")); + MchNoticeTaskResult result = MchNoticeTaskConvert.CONVERT.toResult(task); + transService.translate(result); + return result; + } + + /// 手动重发 + public void resend(Long id) { + taskManager.findById(id) + .orElseThrow(() -> new DataNotExistException("pay.error.order.callbackTaskNotExist")); + noticeSendEngine.sendManual(id); + } + + /// 发送记录分页 + public PageResult pageRecord(PageParam pageParam, MchNoticeRecordQuery query) { + Page page = recordManager.page(pageParam, query); + var records = page.getRecords().stream().map(MchNoticeRecordConvert.CONVERT::toResult).toList(); + return new PageResult() + .setRecords(records) + .setTotal(page.getTotal()) + .setSize(page.getSize()) + .setCurrent(page.getCurrent()); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/result/DaxNoticeResult.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/result/DaxNoticeResult.java new file mode 100644 index 000000000..4f4c3274d --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/result/DaxNoticeResult.java @@ -0,0 +1,42 @@ +package cn.daxpay.open.payment.common.result; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/// # 商户出站通知通用报文 +/// +/// 在 [DaxResult] 基础上补充 event / protocol / mchNo / appId,便于商户区分业务事件 +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@Accessors(chain = true) +@Schema(title = "商户出站通知通用报文") +public class DaxNoticeResult extends DaxResult { + + /// 通知事件码(如 pay.success) + @Schema(description = "通知事件码") + private String event; + + /// 通知协议(system / easy_pay) + @Schema(description = "通知协议") + private String protocol; + + /// 商户号 + @Schema(description = "商户号") + private String mchNo; + + /// 应用ID + @Schema(description = "应用ID") + private String appId; + + public DaxNoticeResult(int successCode, T data, String msg) { + super(successCode, data, msg); + } + + public DaxNoticeResult(int successCode, String msg) { + super(successCode, msg); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/entity/config/MchAppNotifyConfig.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/entity/config/MchAppNotifyConfig.java index 34429b563..a4e700ab1 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/entity/config/MchAppNotifyConfig.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/entity/config/MchAppNotifyConfig.java @@ -27,7 +27,7 @@ public class MchAppNotifyConfig extends MchBaseEntity implements ToResult { + + /// 分页查询 + public Page page(PageParam pageParam, MchNoticeRecordQuery query) { + Page mpPage = MpUtil.getMpPage(pageParam); + QueryWrapper wrapper = QueryGenerator.generator(query); + wrapper.lambda().orderByDesc(MchNoticeRecord::getId); + return this.page(mpPage, wrapper); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeRecordMapper.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeRecordMapper.java new file mode 100644 index 000000000..3b924f11b --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeRecordMapper.java @@ -0,0 +1,11 @@ +package cn.daxpay.open.payment.trade.notice.dao; + +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeRecord; +import com.github.yulichang.base.MPJBaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/// # 商户出站通知发送记录 Mapper +/// +@Mapper +public interface MchNoticeRecordMapper extends MPJBaseMapper { +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeTaskManager.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeTaskManager.java new file mode 100644 index 000000000..90348f025 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeTaskManager.java @@ -0,0 +1,40 @@ +package cn.daxpay.open.payment.trade.notice.dao; + +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; +import cn.daxpay.open.payment.trade.notice.param.MchNoticeTaskQuery; +import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager; +import cn.daxpay.open.platform.common.mybatisplus.query.generator.QueryGenerator; +import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil; +import cn.daxpay.open.platform.core.rest.param.PageParam; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/// # 商户出站通知任务管理 +/// +@Repository +public class MchNoticeTaskManager extends BaseManager { + + /// 分页查询 + public Page page(PageParam pageParam, MchNoticeTaskQuery query) { + Page mpPage = MpUtil.getMpPage(pageParam); + QueryWrapper wrapper = QueryGenerator.generator(query); + wrapper.lambda().orderByDesc(MchNoticeTask::getId); + return this.page(mpPage, wrapper); + } + + /// 按幂等键查询已存在任务 + public Optional findByIdempotentKey(String mchNo, String appId, String event, + String bizNo, String protocol, String source) { + return lambdaQuery() + .eq(MchNoticeTask::getMchNo, mchNo) + .eq(MchNoticeTask::getAppId, appId) + .eq(MchNoticeTask::getEvent, event) + .eq(MchNoticeTask::getBizNo, bizNo) + .eq(MchNoticeTask::getProtocol, protocol) + .eq(MchNoticeTask::getSource, source) + .oneOpt(); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeTaskMapper.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeTaskMapper.java new file mode 100644 index 000000000..ead515328 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeTaskMapper.java @@ -0,0 +1,11 @@ +package cn.daxpay.open.payment.trade.notice.dao; + +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; +import com.github.yulichang.base.MPJBaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/// # 商户出站通知任务 Mapper +/// +@Mapper +public interface MchNoticeTaskMapper extends MPJBaseMapper { +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/entity/MchNoticeRecord.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/entity/MchNoticeRecord.java new file mode 100644 index 000000000..848c7e468 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/entity/MchNoticeRecord.java @@ -0,0 +1,60 @@ +package cn.daxpay.open.payment.trade.notice.entity; + +import cn.daxpay.open.payment.common.entity.MchBaseEntity; +import cn.daxpay.open.payment.trade.notice.convert.MchNoticeRecordConvert; +import cn.daxpay.open.payment.trade.notice.result.MchNoticeRecordResult; +import cn.daxpay.open.platform.common.mybatisplus.function.ToResult; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeSendTypeEnum; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/// # 商户出站通知发送记录 +/// +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("mch_notice_record") +public class MchNoticeRecord extends MchBaseEntity implements ToResult { + + /// 通知任务ID + private Long taskId; + + /// 本次对应的发送序号 + private Integer reqCount; + + /// 发送类型 + /// @see NoticeSendTypeEnum + private String sendType; + + /// 是否成功 + private boolean success; + + /// HTTP 状态码 + private Integer httpStatus; + + /// 错误摘要 + private String errorMsg; + + /// 请求摘要(截断) + private String requestDigest; + + /// 错误摘要截断 + public MchNoticeRecord setErrorMsg(String errorMsg) { + this.errorMsg = StrUtil.sub(errorMsg, 0, 300); + return this; + } + + /// 请求摘要截断 + public MchNoticeRecord setRequestDigest(String requestDigest) { + this.requestDigest = StrUtil.sub(requestDigest, 0, 500); + return this; + } + + @Override + public MchNoticeRecordResult toResult() { + return MchNoticeRecordConvert.CONVERT.toResult(this); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/entity/MchNoticeTask.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/entity/MchNoticeTask.java new file mode 100644 index 000000000..fe2b51aa0 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/entity/MchNoticeTask.java @@ -0,0 +1,91 @@ +package cn.daxpay.open.payment.trade.notice.entity; + +import cn.daxpay.open.payment.common.entity.MchBaseEntity; +import cn.daxpay.open.payment.trade.notice.convert.MchNoticeTaskConvert; +import cn.daxpay.open.payment.trade.notice.result.MchNoticeTaskResult; +import cn.daxpay.open.platform.common.mybatisplus.function.ToResult; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeContentModeEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeEventEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeProtocolEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeSourceEnum; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.time.OffsetDateTime; + +/// # 商户出站通知任务 +/// +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("mch_notice_task") +public class MchNoticeTask extends MchBaseEntity implements ToResult { + + /// 应用号 + @TableField(updateStrategy = FieldStrategy.NEVER, fill = FieldFill.INSERT) + private String appId; + + /// 业务主键ID + private Long bizId; + + /// 业务单号 + private String bizNo; + + /// 通知事件码 + /// @see NoticeEventEnum + private String event; + + /// 通知协议 + /// @see NoticeProtocolEnum + private String protocol; + + /// URL 来源 + /// @see NoticeSourceEnum + private String source; + + /// 内容策略 + /// @see NoticeContentModeEnum + private String contentMode; + + /// 通知内容(快照或引用指针) + private String content; + + /// 商户接收地址 + private String url; + + /// 是否发送成功 + private boolean success; + + /// 已发送次数 + private Integer sendCount; + + /// 延迟重试次数 + private Integer delayCount; + + /// 下次发送时间 + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private OffsetDateTime nextTime; + + /// 最后发送时间 + private OffsetDateTime latestTime; + + /// 最近一次错误摘要 + private String errorMsg; + + /// 错误摘要截断 + public MchNoticeTask setErrorMsg(String errorMsg) { + this.errorMsg = StrUtil.sub(errorMsg, 0, 300); + return this; + } + + @Override + public MchNoticeTaskResult toResult() { + return MchNoticeTaskConvert.CONVERT.toResult(this); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/param/MchNoticeRecordQuery.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/param/MchNoticeRecordQuery.java new file mode 100644 index 000000000..977261040 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/param/MchNoticeRecordQuery.java @@ -0,0 +1,26 @@ +package cn.daxpay.open.payment.trade.notice.param; + +import cn.daxpay.open.platform.core.annotation.QueryParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 商户出站通知发送记录查询 +/// +@Data +@Accessors(chain = true) +@Schema(title = "商户出站通知发送记录查询") +public class MchNoticeRecordQuery { + + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + @Schema(description = "任务ID") + private Long taskId; + + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + @Schema(description = "商户号") + private String mchNo; + + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + @Schema(description = "是否成功") + private Boolean success; +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/param/MchNoticeTaskQuery.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/param/MchNoticeTaskQuery.java new file mode 100644 index 000000000..c2328bf4e --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/param/MchNoticeTaskQuery.java @@ -0,0 +1,42 @@ +package cn.daxpay.open.payment.trade.notice.param; + +import cn.daxpay.open.platform.core.annotation.QueryParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 商户出站通知任务查询 +/// +@Data +@Accessors(chain = true) +@Schema(title = "商户出站通知任务查询") +public class MchNoticeTaskQuery { + + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + @Schema(description = "商户号") + private String mchNo; + + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + @Schema(description = "应用号") + private String appId; + + @QueryParam(type = QueryParam.CompareTypeEnum.LIKE) + @Schema(description = "业务单号") + private String bizNo; + + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + @Schema(description = "通知事件码") + private String event; + + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + @Schema(description = "通知协议") + private String protocol; + + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + @Schema(description = "URL来源") + private String source; + + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + @Schema(description = "是否发送成功") + private Boolean success; +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/protocol/NoticeProtocolSender.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/protocol/NoticeProtocolSender.java new file mode 100644 index 000000000..691e0bceb --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/protocol/NoticeProtocolSender.java @@ -0,0 +1,31 @@ +package cn.daxpay.open.payment.trade.notice.protocol; + +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 商户出站通知协议发送器 +/// +/// 按 protocol 精确路由;禁止 fan-out 遍历全部插件 +public interface NoticeProtocolSender { + + /// 协议编码(与 NoticeProtocolEnum.code 对齐) + String protocol(); + + /// 执行一次 HTTP/协议发送 + NoticeSendResult send(MchNoticeTask task); + + /// 单次发送结果 + @Data + @Accessors(chain = true) + class NoticeSendResult { + /// 是否业务 Ack 成功 + private boolean success; + /// HTTP 状态码(可空) + private Integer httpStatus; + /// 错误或非 SUCCESS 响应摘要 + private String errorMsg; + /// 请求摘要(便于排查) + private String requestDigest; + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/protocol/SystemHttpSignedSender.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/protocol/SystemHttpSignedSender.java new file mode 100644 index 000000000..05054d1aa --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/protocol/SystemHttpSignedSender.java @@ -0,0 +1,79 @@ +package cn.daxpay.open.payment.trade.notice.protocol; + +import cn.daxpay.open.payment.common.result.DaxNoticeResult; +import cn.daxpay.open.payment.common.util.JsonSignStrUtil; +import cn.daxpay.open.payment.common.util.PaySignUtil; +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; +import cn.daxpay.open.platform.common.config.properties.PlatformConfigProperties; +import cn.daxpay.open.platform.common.json.util.JacksonUtil; +import cn.daxpay.open.platform.core.code.CommonCode; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeProtocolEnum; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.ContentType; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +/// # 标准 DaxPay 签名 JSON 出站发送器 +/// +/// Ack 规则:HTTP 2xx 且 body trim 后忽略大小写等于 SUCCESS +@Slf4j +@Component +@RequiredArgsConstructor +public class SystemHttpSignedSender implements NoticeProtocolSender { + + private final PlatformConfigProperties platformConfigProperties; + + @Override + public String protocol() { + return NoticeProtocolEnum.SYSTEM.getCode(); + } + + @Override + public NoticeSendResult send(MchNoticeTask task) { + NoticeSendResult result = new NoticeSendResult(); + String body = null; + Integer httpStatus = null; + try { + var data = JsonSignStrUtil.buildSortedMap(task.getContent()); + var notice = new DaxNoticeResult<>(CommonCode.SUCCESS_CODE, data, CommonCode.SUCCESS_MSG) + .setEvent(task.getEvent()) + .setProtocol(task.getProtocol()) + .setMchNo(task.getMchNo()) + .setAppId(task.getAppId()); + notice.setResTime(OffsetDateTime.now(ZoneOffset.UTC)); + notice.setReqId(MDC.get(CommonCode.TRACE_ID)); + String privateKey = platformConfigProperties.getKeyConfig().getPrivateKey(); + notice.setSign(PaySignUtil.sign(notice, privateKey)); + String requestJson = JacksonUtil.toJson(notice); + result.setRequestDigest(StrUtil.sub(requestJson, 0, 500)); + HttpResponse response = HttpUtil.createPost(task.getUrl()) + .body(requestJson, ContentType.JSON.getValue()) + .timeout(15000) + .execute(); + httpStatus = response.getStatus(); + body = response.body(); + } catch (Exception e) { + log.error("系统协议通知发送失败, taskId={}, bizNo={}", task.getId(), task.getBizNo(), e); + result.setSuccess(false) + .setHttpStatus(httpStatus) + .setErrorMsg(e.getMessage()); + return result; + } + result.setHttpStatus(httpStatus); + boolean ack = httpStatus != null && httpStatus >= 200 && httpStatus < 300 + && StrUtil.equalsIgnoreCase(StrUtil.trim(body), "SUCCESS"); + result.setSuccess(ack); + if (!ack) { + result.setErrorMsg(StrUtil.blankToDefault(StrUtil.sub(body, 0, 300), + "httpStatus=" + httpStatus)); + } + return result; + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/result/MchNoticeRecordResult.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/result/MchNoticeRecordResult.java new file mode 100644 index 000000000..5f1836b13 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/result/MchNoticeRecordResult.java @@ -0,0 +1,37 @@ +package cn.daxpay.open.payment.trade.notice.result; + +import cn.daxpay.open.payment.common.result.MchBaseResult; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/// # 商户出站通知发送记录结果 +/// +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "商户出站通知发送记录") +public class MchNoticeRecordResult extends MchBaseResult { + + @Schema(description = "通知任务ID") + private Long taskId; + + @Schema(description = "发送序号") + private Integer reqCount; + + @Schema(description = "发送类型") + private String sendType; + + @Schema(description = "是否成功") + private Boolean success; + + @Schema(description = "HTTP状态码") + private Integer httpStatus; + + @Schema(description = "错误摘要") + private String errorMsg; + + @Schema(description = "请求摘要") + private String requestDigest; +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/result/MchNoticeTaskResult.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/result/MchNoticeTaskResult.java new file mode 100644 index 000000000..517d88279 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/result/MchNoticeTaskResult.java @@ -0,0 +1,73 @@ +package cn.daxpay.open.payment.trade.notice.result; + +import cn.daxpay.open.payment.common.result.MchBaseResult; +import cn.daxpay.open.payment.merchant.entity.info.MerchantInfo; +import cn.daxpay.open.platform.core.annotation.Trans; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.time.OffsetDateTime; + +/// # 商户出站通知任务结果 +/// +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "商户出站通知任务") +public class MchNoticeTaskResult extends MchBaseResult { + + /// 商户名称(由 mchNo 翻译) + @Trans( + entity = MerchantInfo.class, + source = MchBaseResult.Fields.mchNo, + result = MerchantInfo.Fields.mchName) + @Schema(description = "商户名称") + private String mchName; + + @Schema(description = "应用号") + private String appId; + + @Schema(description = "业务主键ID") + private Long bizId; + + @Schema(description = "业务单号") + private String bizNo; + + @Schema(description = "通知事件码") + private String event; + + @Schema(description = "通知协议") + private String protocol; + + @Schema(description = "URL来源") + private String source; + + @Schema(description = "内容策略") + private String contentMode; + + @Schema(description = "通知内容") + private String content; + + @Schema(description = "商户接收地址") + private String url; + + @Schema(description = "是否发送成功") + private Boolean success; + + @Schema(description = "已发送次数") + private Integer sendCount; + + @Schema(description = "延迟重试次数") + private Integer delayCount; + + @Schema(description = "下次发送时间") + private OffsetDateTime nextTime; + + @Schema(description = "最后发送时间") + private OffsetDateTime latestTime; + + @Schema(description = "最近一次错误摘要") + private String errorMsg; +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeDispatcher.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeDispatcher.java new file mode 100644 index 000000000..3054cdb8a --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeDispatcher.java @@ -0,0 +1,114 @@ +package cn.daxpay.open.payment.trade.notice.service; + +import cn.daxpay.open.payment.merchant.dao.config.MchAppNotifyConfigManager; +import cn.daxpay.open.payment.merchant.entity.config.MchAppNotifyConfig; +import cn.daxpay.open.payment.trade.notice.command.NoticeDispatchCommand; +import cn.daxpay.open.payment.trade.notice.dao.MchNoticeTaskManager; +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeContentModeEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeProtocolEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeSourceEnum; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.Objects; + +/// # 商户出站通知派发器 +/// +/// SYSTEM:尝试创建 order + app 两条任务;其它协议:仅 protocol 任务 +@Slf4j +@Service +@RequiredArgsConstructor +public class NoticeDispatcher { + + private final MchNoticeTaskManager taskManager; + private final MchAppNotifyConfigManager notifyConfigManager; + private final NoticeTaskScheduleService scheduleService; + + /// 派发通知意图 + public void dispatch(NoticeDispatchCommand command) { + if (command == null + || StrUtil.isBlank(command.getMchNo()) + || StrUtil.isBlank(command.getAppId()) + || StrUtil.isBlank(command.getEvent()) + || StrUtil.isBlank(command.getBizNo())) { + log.warn("出站通知命令缺少必要字段, skip"); + return; + } + NoticeProtocolEnum protocol = command.getProtocol() == null + ? NoticeProtocolEnum.SYSTEM : command.getProtocol(); + NoticeContentModeEnum contentMode = command.getContentMode() == null + ? NoticeContentModeEnum.SNAPSHOT : command.getContentMode(); + + if (protocol == NoticeProtocolEnum.SYSTEM) { + tryCreate(command, NoticeSourceEnum.ORDER, protocol, contentMode, command.getOrderNotifyUrl()); + tryCreateApp(command, contentMode); + return; + } + tryCreate(command, NoticeSourceEnum.PROTOCOL, protocol, contentMode, command.getProtocolNotifyUrl()); + } + + /// 应用级订阅 + private void tryCreateApp(NoticeDispatchCommand command, NoticeContentModeEnum contentMode) { + MchAppNotifyConfig config = notifyConfigManager.findByAppId(command.getAppId()).orElse(null); + if (config == null || !Boolean.TRUE.equals(config.getStatus()) || StrUtil.isBlank(config.getNotifyUrl())) { + return; + } + if (!matchSubscribed(config.getSubscribedEvents(), command.getEvent())) { + return; + } + tryCreate(command, NoticeSourceEnum.APP, NoticeProtocolEnum.SYSTEM, contentMode, config.getNotifyUrl()); + } + + /// 创建任务(幂等)并投递 + private void tryCreate(NoticeDispatchCommand command, NoticeSourceEnum source, + NoticeProtocolEnum protocol, NoticeContentModeEnum contentMode, String url) { + if (StrUtil.isBlank(url)) { + log.debug("出站通知跳过(无URL): event={}, bizNo={}, source={}", + command.getEvent(), command.getBizNo(), source.getCode()); + return; + } + var existing = taskManager.findByIdempotentKey( + command.getMchNo(), command.getAppId(), command.getEvent(), + command.getBizNo(), protocol.getCode(), source.getCode()); + if (existing.isPresent()) { + log.info("出站通知任务已存在, 跳过创建: event={}, bizNo={}, source={}", + command.getEvent(), command.getBizNo(), source.getCode()); + return; + } + MchNoticeTask task = new MchNoticeTask(); + // 运营端写 MchBaseEntity 必须显式 mchNo + task.setMchNo(command.getMchNo()); + task.setAppId(command.getAppId()) + .setBizId(command.getBizId()) + .setBizNo(command.getBizNo()) + .setEvent(command.getEvent()) + .setProtocol(protocol.getCode()) + .setSource(source.getCode()) + .setContentMode(contentMode.getCode()) + .setContent(command.getContentOrRef()) + .setUrl(url) + .setSuccess(false) + .setSendCount(0) + .setDelayCount(0); + taskManager.save(task); + scheduleService.scheduleImmediateAfterCommit(task.getId()); + log.info("注册出站通知: event={}, bizNo={}, protocol={}, source={}", + command.getEvent(), command.getBizNo(), protocol.getCode(), source.getCode()); + } + + /// 订阅匹配:精确事件码,或前缀(pay 匹配 pay.*) + public static boolean matchSubscribed(String subscribedEvents, String event) { + if (StrUtil.isBlank(subscribedEvents) || StrUtil.isBlank(event)) { + return false; + } + return Arrays.stream(subscribedEvents.split(",")) + .map(String::trim) + .filter(StrUtil::isNotBlank) + .anyMatch(token -> Objects.equals(token, event) + || event.startsWith(token + ".")); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeRetryPolicy.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeRetryPolicy.java new file mode 100644 index 000000000..e70ab3968 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeRetryPolicy.java @@ -0,0 +1,48 @@ +package cn.daxpay.open.payment.trade.notice.service; + +import org.springframework.stereotype.Component; + +import java.util.Map; + +/// # 商户出站通知重试策略 +/// +/// 仿微信通知节奏,约 16 次延时重试,合计约 24h+ +/// 间隔:15s/15s/30s/3m/10m/20m/30m×3/60m/3h×3/6h×… +@Component +public class NoticeRetryPolicy { + + /// 最大延时重试次数 + public static final int MAX_DELAY_COUNT = 16; + + /// key: 延时次数(1起), value: 下次间隔秒数 + private static final Map DELAY_SECONDS = Map.ofEntries( + Map.entry(1, 15), + Map.entry(2, 15), + Map.entry(3, 30), + Map.entry(4, 3 * 60), + Map.entry(5, 10 * 60), + Map.entry(6, 20 * 60), + Map.entry(7, 30 * 60), + Map.entry(8, 30 * 60), + Map.entry(9, 30 * 60), + Map.entry(10, 60 * 60), + Map.entry(11, 3 * 60 * 60), + Map.entry(12, 3 * 60 * 60), + Map.entry(13, 3 * 60 * 60), + Map.entry(14, 6 * 60 * 60), + Map.entry(15, 6 * 60 * 60), + Map.entry(16, 6 * 60 * 60) + ); + + /// 是否还可继续延时重试(delayCount 为已完成的延时次数) + public boolean canRetry(int delayCount) { + return delayCount < MAX_DELAY_COUNT; + } + + /// 获取下一次延时间隔秒数 + /// + /// @param nextDelayCount 即将执行的延时序号(1..16) + public int nextDelaySeconds(int nextDelayCount) { + return DELAY_SECONDS.getOrDefault(nextDelayCount, 6 * 60 * 60); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeSendEngine.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeSendEngine.java new file mode 100644 index 000000000..f0711a30c --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeSendEngine.java @@ -0,0 +1,139 @@ +package cn.daxpay.open.payment.trade.notice.service; + +import cn.daxpay.open.payment.trade.notice.dao.MchNoticeRecordManager; +import cn.daxpay.open.payment.trade.notice.dao.MchNoticeTaskManager; +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeRecord; +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; +import cn.daxpay.open.payment.trade.notice.protocol.NoticeProtocolSender; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeSendTypeEnum; +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/// # 商户出站通知发送引擎 +/// +/// 唯一负责:选 Sender、写流水、更新任务、排期重试 +@Slf4j +@Service +public class NoticeSendEngine { + + private final MchNoticeTaskManager taskManager; + private final MchNoticeRecordManager recordManager; + private final NoticeRetryPolicy retryPolicy; + private final NoticeTaskScheduleService scheduleService; + private final Map senderMap; + + public NoticeSendEngine(MchNoticeTaskManager taskManager, + MchNoticeRecordManager recordManager, + NoticeRetryPolicy retryPolicy, + NoticeTaskScheduleService scheduleService, + List senders) { + this.taskManager = taskManager; + this.recordManager = recordManager; + this.retryPolicy = retryPolicy; + this.scheduleService = scheduleService; + this.senderMap = senders.stream() + .collect(Collectors.toMap(NoticeProtocolSender::protocol, Function.identity(), (a, b) -> a)); + } + + /// 自动发送(消费端入口) + public void sendAuto(Long taskId) { + send(taskId, true); + } + + /// 手动重发 + public void sendManual(Long taskId) { + send(taskId, false); + } + + /// 执行发送 + public void send(Long taskId, boolean autoSend) { + MchNoticeTask task = taskManager.findById(taskId).orElse(null); + if (task == null) { + log.warn("出站通知任务不存在: taskId={}", taskId); + return; + } + if (task.isSuccess()) { + log.info("出站通知任务已成功, 跳过: taskId={}", taskId); + return; + } + NoticeProtocolSender sender = senderMap.get(task.getProtocol()); + OffsetDateTime sendTime = OffsetDateTime.now(ZoneOffset.UTC); + int reqCount = (task.getSendCount() == null ? 0 : task.getSendCount()) + 1; + MchNoticeRecord record = new MchNoticeRecord(); + // 运营端写 MchBaseEntity 必须显式 mchNo + record.setMchNo(task.getMchNo()); + record.setTaskId(task.getId()) + .setReqCount(reqCount) + .setSendType(autoSend ? NoticeSendTypeEnum.AUTO.getCode() : NoticeSendTypeEnum.MANUAL.getCode()); + + if (sender == null) { + log.error("未找到通知协议 Sender: protocol={}, taskId={}", task.getProtocol(), taskId); + record.setSuccess(false).setErrorMsg("protocol sender not found: " + task.getProtocol()); + failUpdate(task, sendTime, autoSend, record); + return; + } + + NoticeProtocolSender.NoticeSendResult sendResult; + try { + sendResult = sender.send(task); + } catch (Exception e) { + log.error("出站通知 Sender 异常: taskId={}", taskId, e); + record.setSuccess(false).setErrorMsg(e.getMessage()); + failUpdate(task, sendTime, autoSend, record); + return; + } + + record.setHttpStatus(sendResult.getHttpStatus()) + .setRequestDigest(sendResult.getRequestDigest()); + if (sendResult.isSuccess()) { + task.setSendCount(reqCount) + .setLatestTime(sendTime) + .setSuccess(true) + .setErrorMsg(null) + .setNextTime(null); + if (autoSend && task.getDelayCount() != null) { + // 自动路径成功时同步累计 delay 计数(与商业版一致,便于观测) + task.setDelayCount(task.getDelayCount()); + } + record.setSuccess(true); + taskManager.updateById(task); + recordManager.save(record); + return; + } + + record.setSuccess(false).setErrorMsg(sendResult.getErrorMsg()); + failUpdate(task, sendTime, autoSend, record); + } + + /// 失败:更新任务并按需排期重试 + private void failUpdate(MchNoticeTask task, OffsetDateTime sendTime, boolean autoSend, MchNoticeRecord record) { + int reqCount = record.getReqCount() == null ? 1 : record.getReqCount(); + task.setSendCount(reqCount).setLatestTime(sendTime); + if (StrUtil.isNotBlank(record.getErrorMsg())) { + task.setErrorMsg(record.getErrorMsg()); + } + if (autoSend && !task.isSuccess()) { + int delayCount = task.getDelayCount() == null ? 0 : task.getDelayCount(); + if (retryPolicy.canRetry(delayCount)) { + int next = delayCount + 1; + task.setDelayCount(next); + int delaySeconds = retryPolicy.nextDelaySeconds(next); + task.setNextTime(sendTime.plusSeconds(delaySeconds)); + taskManager.updateById(task); + recordManager.save(record); + scheduleService.scheduleRetry(task, delaySeconds); + return; + } + } + taskManager.updateById(task); + recordManager.save(record); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeTaskScheduleService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeTaskScheduleService.java new file mode 100644 index 000000000..d38a73cee --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeTaskScheduleService.java @@ -0,0 +1,45 @@ +package cn.daxpay.open.payment.trade.notice.service; + +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; +import cn.daxpay.open.payment.trade.notice.util.NoticeTxUtil; +import cn.daxpay.open.payment.trade.runtime.mq.MchNoticeSendMessage; +import cn.daxpay.open.payment.trade.runtime.mq.PayArtemisConstants; +import cn.daxpay.open.platform.common.artemis.service.ArtemisTemplateService; +import cn.daxpay.open.platform.common.json.util.JacksonUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 商户出站通知任务投递 +/// +@Slf4j +@Service +@RequiredArgsConstructor +public class NoticeTaskScheduleService { + + private final ArtemisTemplateService artemisTemplateService; + + /// 事务提交后立即投递(delay=0) + public void scheduleImmediateAfterCommit(Long taskId) { + NoticeTxUtil.afterCommit(() -> sendNow(taskId)); + } + + /// 立即投递 + public void sendNow(Long taskId) { + var msg = new MchNoticeSendMessage().setTaskId(taskId); + artemisTemplateService.send(PayArtemisConstants.MCH_NOTICE_SEND, JacksonUtil.toJson(msg)); + log.debug("出站通知已投递: taskId={}", taskId); + } + + /// 延时投递 + public void scheduleDelay(Long taskId, int delaySeconds) { + var msg = new MchNoticeSendMessage().setTaskId(taskId); + artemisTemplateService.sendDelay(PayArtemisConstants.MCH_NOTICE_SEND, JacksonUtil.toJson(msg), delaySeconds); + log.info("出站通知延时重试已投递: taskId={}, delaySeconds={}", taskId, delaySeconds); + } + + /// 根据任务排程下次重试 + public void scheduleRetry(MchNoticeTask task, int delaySeconds) { + scheduleDelay(task.getId(), delaySeconds); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/TradeNoticeBridge.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/TradeNoticeBridge.java new file mode 100644 index 000000000..f076d1f02 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/TradeNoticeBridge.java @@ -0,0 +1,99 @@ +package cn.daxpay.open.payment.trade.notice.service; + +import cn.daxpay.open.payment.trade.notice.command.NoticeDispatchCommand; +import cn.daxpay.open.payment.trade.order.convert.GatewayPayOrderConvert; +import cn.daxpay.open.payment.trade.order.convert.NormalPayOrderConvert; +import cn.daxpay.open.payment.trade.order.convert.RefundOrderConvert; +import cn.daxpay.open.payment.trade.order.dao.GatewayPayOrderManager; +import cn.daxpay.open.payment.trade.order.dao.NormalPayOrderManager; +import cn.daxpay.open.payment.trade.order.entity.GatewayPayOrder; +import cn.daxpay.open.payment.trade.order.entity.NormalPayOrder; +import cn.daxpay.open.payment.trade.order.entity.PayTrade; +import cn.daxpay.open.payment.trade.order.entity.RefundOrder; +import cn.daxpay.open.payment.trade.enums.PayTradeTypeEnum; +import cn.daxpay.open.platform.common.json.util.JacksonUtil; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeContentModeEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeEventEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeProtocolEnum; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +/// # 支付/退款域 → 出站通知桥接 +/// +/// 组装快照并调用 [NoticeDispatcher],避免业务服务直接依赖通知细节 +@Slf4j +@Service +@RequiredArgsConstructor +public class TradeNoticeBridge { + + private final NoticeDispatcher noticeDispatcher; + private final NormalPayOrderManager normalPayOrderManager; + private final GatewayPayOrderManager gatewayPayOrderManager; + + /// 支付终态通知 + public void dispatchPay(PayTrade trade, NoticeEventEnum event) { + if (trade == null || event == null) { + return; + } + if (isGateway(trade)) { + GatewayPayOrder order = gatewayPayOrderManager.findById(trade.getContainerId()).orElse(null); + if (order == null) { + log.warn("网关订单不存在, 跳过通知: containerId={}", trade.getContainerId()); + return; + } + String content = JacksonUtil.toJson(GatewayPayOrderConvert.CONVERT.toResult(order)); + noticeDispatcher.dispatch(new NoticeDispatchCommand() + .setMchNo(order.getMchNo()) + .setAppId(order.getAppId()) + .setEvent(event.getCode()) + .setBizId(order.getId()) + .setBizNo(order.getOrderNo()) + .setOrderNotifyUrl(order.getNotifyUrl()) + .setProtocol(NoticeProtocolEnum.SYSTEM) + .setContentMode(NoticeContentModeEnum.SNAPSHOT) + .setContentOrRef(content)); + return; + } + NormalPayOrder order = normalPayOrderManager.findById(trade.getContainerId()).orElse(null); + if (order == null) { + log.warn("普通支付订单不存在, 跳过通知: containerId={}", trade.getContainerId()); + return; + } + String content = JacksonUtil.toJson(NormalPayOrderConvert.CONVERT.toResult(order)); + noticeDispatcher.dispatch(new NoticeDispatchCommand() + .setMchNo(order.getMchNo()) + .setAppId(order.getAppId()) + .setEvent(event.getCode()) + .setBizId(order.getId()) + .setBizNo(order.getOrderNo()) + .setOrderNotifyUrl(order.getNotifyUrl()) + .setProtocol(NoticeProtocolEnum.SYSTEM) + .setContentMode(NoticeContentModeEnum.SNAPSHOT) + .setContentOrRef(content)); + } + + /// 退款终态通知 + public void dispatchRefund(RefundOrder refundOrder, NoticeEventEnum event) { + if (refundOrder == null || event == null) { + return; + } + String content = JacksonUtil.toJson(RefundOrderConvert.CONVERT.toResult(refundOrder)); + noticeDispatcher.dispatch(new NoticeDispatchCommand() + .setMchNo(refundOrder.getMchNo()) + .setAppId(refundOrder.getAppId()) + .setEvent(event.getCode()) + .setBizId(refundOrder.getId()) + .setBizNo(refundOrder.getRefundNo()) + .setOrderNotifyUrl(refundOrder.getNotifyUrl()) + .setProtocol(NoticeProtocolEnum.SYSTEM) + .setContentMode(NoticeContentModeEnum.SNAPSHOT) + .setContentOrRef(content)); + } + + 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/notice/util/NoticeTxUtil.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/util/NoticeTxUtil.java new file mode 100644 index 000000000..7471275c4 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/util/NoticeTxUtil.java @@ -0,0 +1,26 @@ +package cn.daxpay.open.payment.trade.notice.util; + +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/// # 事务提交后执行工具 +/// +public final class NoticeTxUtil { + + private NoticeTxUtil() { + } + + /// 有活跃事务则 afterCommit 执行,否则立即执行 + public static void afterCommit(Runnable runnable) { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + runnable.run(); + } + }); + } else { + runnable.run(); + } + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/mq/MchNoticeSendMessage.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/mq/MchNoticeSendMessage.java new file mode 100644 index 000000000..c63b2b905 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/mq/MchNoticeSendMessage.java @@ -0,0 +1,15 @@ +package cn.daxpay.open.payment.trade.runtime.mq; + +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 商户出站通知发送消息体 +/// +/// 消费端只信任 taskId 重新查库,不信任消息内业务状态 +@Data +@Accessors(chain = true) +public class MchNoticeSendMessage { + + /// 通知任务 ID + private Long taskId; +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/mq/PayArtemisConstants.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/mq/PayArtemisConstants.java index c3dd64148..b93a9d139 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/mq/PayArtemisConstants.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/mq/PayArtemisConstants.java @@ -19,4 +19,8 @@ public interface PayArtemisConstants { /// 网关支付超时关单队列 address /// 预下单时按容器过期时间投递,到期后由 [GatewayTimeoutConsumer] 消费。 String GATEWAY_TIMEOUT_QUEUE = "pay.gateway.timeout"; + + /// 商户出站通知发送队列 address + /// 任务落库后 afterCommit 投递(含失败延时重试),由 [cn.daxpay.open.payment.trade.notice.consumer.MchNoticeSendConsumer] 消费。 + String MCH_NOTICE_SEND = "mch.notice.send"; } 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 a198c84aa..904b3113f 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 @@ -12,8 +12,10 @@ import cn.daxpay.open.payment.trade.order.dao.PayTradeManager; import cn.daxpay.open.payment.trade.order.entity.GatewayPayOrder; import cn.daxpay.open.payment.trade.order.entity.NormalPayOrder; 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.platform.core.enums.pay.notice.NoticeEventEnum; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -35,6 +37,7 @@ public class PayUniHandleService { private final NormalPayOrderManager payNormalOrderManager; private final GatewayPayOrderManager gatewayPayOrderManager; private final PayPluginAssistService payPluginAssistService; + private final TradeNoticeBridge tradeNoticeBridge; /// 支付发起后处理 /// 不论是否完成都更新交易单; 仅资金状态为 SUCCESS 时同步容器为 PAID。 @@ -67,8 +70,10 @@ public class PayUniHandleService { payNormalOrderManager.updateById(order); } } - // 插件: 仅支付成功时广播 + // 出站通知 + 插件: 仅支付成功时 if (Objects.equals(trade.getStatus(), PayFundStatusEnum.SUCCESS.getCode())) { + // 商户出站通知(系统协议) + tradeNoticeBridge.dispatchPay(trade, NoticeEventEnum.PAY_SUCCESS); payPluginAssistService.paySuccess(trade); } } @@ -84,6 +89,8 @@ public class PayUniHandleService { applyGatewaySyncReceipts(trade, order, syncResult); gatewayPayOrderManager.updateById(order); } + // 商户出站通知(系统协议) + tradeNoticeBridge.dispatchPay(trade, NoticeEventEnum.PAY_SUCCESS); payPluginAssistService.paySuccess(trade); return; } @@ -94,6 +101,8 @@ public class PayUniHandleService { applyNormalSyncReceipts(trade, order, syncResult); payNormalOrderManager.updateById(order); } + // 商户出站通知(系统协议) + tradeNoticeBridge.dispatchPay(trade, NoticeEventEnum.PAY_SUCCESS); payPluginAssistService.paySuccess(trade); } @@ -101,6 +110,8 @@ public class PayUniHandleService { public void paySuccess(PayTrade trade) { updateTradeWithPosted(trade); markContainerPaid(trade); + // 商户出站通知(系统协议) + tradeNoticeBridge.dispatchPay(trade, NoticeEventEnum.PAY_SUCCESS); payPluginAssistService.paySuccess(trade); } @@ -111,6 +122,8 @@ public class PayUniHandleService { trade.setCloseTime(now); updateTradeWithPosted(trade); this.markContainerFailed(trade, now, errMsg); + // 商户出站通知(系统协议) + tradeNoticeBridge.dispatchPay(trade, NoticeEventEnum.PAY_FAIL); payPluginAssistService.payFail(trade); } @@ -124,6 +137,8 @@ public class PayUniHandleService { trade.setCloseTime(now); updateTradeWithPosted(trade); this.markContainerClosed(trade, now, false, null); + // 商户出站通知(系统协议) + tradeNoticeBridge.dispatchPay(trade, NoticeEventEnum.PAY_CLOSE); payPluginAssistService.payClose(trade); } @@ -134,6 +149,8 @@ public class PayUniHandleService { trade.setCloseTime(now); updateTradeWithPosted(trade); this.markContainerClosed(trade, now, true, null); + // 商户出站通知(系统协议) + tradeNoticeBridge.dispatchPay(trade, NoticeEventEnum.PAY_CLOSE); payPluginAssistService.payClose(trade); } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/RefundSettleService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/RefundSettleService.java index e24eaa83c..7836b73aa 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/RefundSettleService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/RefundSettleService.java @@ -5,10 +5,12 @@ import cn.daxpay.open.payment.trade.order.dao.PayTradeManager; import cn.daxpay.open.payment.trade.order.dao.RefundOrderManager; import cn.daxpay.open.payment.trade.order.entity.RefundOrder; 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.platform.core.code.CommonErrorCode; import cn.daxpay.open.platform.core.code.DaxPayErrorCode; import cn.daxpay.open.platform.common.redis.lock.LockExecutor; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeEventEnum; import cn.daxpay.open.platform.core.exception.BizInfoException; import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; @@ -40,6 +42,7 @@ public class RefundSettleService { private final PayTradeManager payTradeManager; private final LockExecutor lockExecutor; private final PayPluginAssistService payPluginAssistService; + private final TradeNoticeBridge tradeNoticeBridge; /// 构建退款结算锁键 public static String lockKey(String tradeNo) { @@ -92,6 +95,8 @@ public class RefundSettleService { refundOrder.setStatus(RefundOrderStatusEnum.SUCCESS.getCode()); refundOrder.setErrorMsg(null); refundOrderManager.updateById(refundOrder); + // 商户出站通知(系统协议) + tradeNoticeBridge.dispatchRefund(refundOrder, NoticeEventEnum.REFUND_SUCCESS); // 插件: 按原支付 tradeNo 回查资金单后广播退款成功 payTradeManager.findByTradeNo(refundOrder.getTradeNo()).ifPresent(trade -> payPluginAssistService.refundSuccess(trade, refundOrder)); @@ -156,6 +161,10 @@ public class RefundSettleService { refundOrder.setErrorMsg(StrUtil.maxLength(errorMsg, 500)); } refundOrderManager.updateById(refundOrder); + // 商户出站通知: 关闭终态发 refund.close(失败暂不推,避免与关闭语义混淆) + if (close) { + tradeNoticeBridge.dispatchRefund(refundOrder, NoticeEventEnum.REFUND_CLOSE); + } // 插件: 失败/关闭广播 payTradeManager.findByTradeNo(refundOrder.getTradeNo()).ifPresent(trade -> payPluginAssistService.refundClose(trade, refundOrder)); diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/callback_notice_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/callback_notice_type.json index 82b8df135..6f50ddc34 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/callback_notice_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/callback_notice_type.json @@ -1,3 +1,4 @@ { - "system": "System Callback" + "system": "System Callback", + "easy_pay": "EasyPay" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_content_mode.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_content_mode.json new file mode 100644 index 000000000..5a8b237a6 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_content_mode.json @@ -0,0 +1,4 @@ +{ + "snapshot": "Snapshot", + "ref": "Reference" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_event.json new file mode 100644 index 000000000..d13d7f922 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_event.json @@ -0,0 +1,7 @@ +{ + "pay.success": "Pay Success", + "pay.fail": "Pay Fail", + "pay.close": "Pay Close", + "refund.success": "Refund Success", + "refund.close": "Refund Close" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_protocol.json new file mode 100644 index 000000000..48eee3cc2 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_protocol.json @@ -0,0 +1,4 @@ +{ + "system": "System", + "easy_pay": "EasyPay" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_source.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_source.json new file mode 100644 index 000000000..e00f1b959 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_source.json @@ -0,0 +1,5 @@ +{ + "order": "Order", + "app": "App", + "protocol": "Protocol" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/callback_notice_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/callback_notice_type.json index b8f51f62f..6f50ddc34 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/callback_notice_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/callback_notice_type.json @@ -1,3 +1,4 @@ { - "system": "Panggilan Balik Sistem" + "system": "System Callback", + "easy_pay": "EasyPay" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_content_mode.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_content_mode.json new file mode 100644 index 000000000..5a8b237a6 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_content_mode.json @@ -0,0 +1,4 @@ +{ + "snapshot": "Snapshot", + "ref": "Reference" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_event.json new file mode 100644 index 000000000..d13d7f922 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_event.json @@ -0,0 +1,7 @@ +{ + "pay.success": "Pay Success", + "pay.fail": "Pay Fail", + "pay.close": "Pay Close", + "refund.success": "Refund Success", + "refund.close": "Refund Close" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_protocol.json new file mode 100644 index 000000000..48eee3cc2 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_protocol.json @@ -0,0 +1,4 @@ +{ + "system": "System", + "easy_pay": "EasyPay" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_source.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_source.json new file mode 100644 index 000000000..e00f1b959 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_source.json @@ -0,0 +1,5 @@ +{ + "order": "Order", + "app": "App", + "protocol": "Protocol" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/callback_notice_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/callback_notice_type.json index c2749b5ba..6f50ddc34 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/callback_notice_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/callback_notice_type.json @@ -1,3 +1,4 @@ { - "system": "システムコールバック" + "system": "System Callback", + "easy_pay": "EasyPay" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_content_mode.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_content_mode.json new file mode 100644 index 000000000..5a8b237a6 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_content_mode.json @@ -0,0 +1,4 @@ +{ + "snapshot": "Snapshot", + "ref": "Reference" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_event.json new file mode 100644 index 000000000..d13d7f922 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_event.json @@ -0,0 +1,7 @@ +{ + "pay.success": "Pay Success", + "pay.fail": "Pay Fail", + "pay.close": "Pay Close", + "refund.success": "Refund Success", + "refund.close": "Refund Close" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_protocol.json new file mode 100644 index 000000000..48eee3cc2 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_protocol.json @@ -0,0 +1,4 @@ +{ + "system": "System", + "easy_pay": "EasyPay" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_source.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_source.json new file mode 100644 index 000000000..e00f1b959 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_source.json @@ -0,0 +1,5 @@ +{ + "order": "Order", + "app": "App", + "protocol": "Protocol" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/callback_notice_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/callback_notice_type.json index b5e893c74..6f50ddc34 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/callback_notice_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/callback_notice_type.json @@ -1,3 +1,4 @@ { - "system": "시스템 콜백" + "system": "System Callback", + "easy_pay": "EasyPay" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_content_mode.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_content_mode.json new file mode 100644 index 000000000..5a8b237a6 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_content_mode.json @@ -0,0 +1,4 @@ +{ + "snapshot": "Snapshot", + "ref": "Reference" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_event.json new file mode 100644 index 000000000..d13d7f922 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_event.json @@ -0,0 +1,7 @@ +{ + "pay.success": "Pay Success", + "pay.fail": "Pay Fail", + "pay.close": "Pay Close", + "refund.success": "Refund Success", + "refund.close": "Refund Close" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_protocol.json new file mode 100644 index 000000000..48eee3cc2 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_protocol.json @@ -0,0 +1,4 @@ +{ + "system": "System", + "easy_pay": "EasyPay" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_source.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_source.json new file mode 100644 index 000000000..e00f1b959 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_source.json @@ -0,0 +1,5 @@ +{ + "order": "Order", + "app": "App", + "protocol": "Protocol" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/callback_notice_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/callback_notice_type.json index b8f51f62f..6f50ddc34 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/callback_notice_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/callback_notice_type.json @@ -1,3 +1,4 @@ { - "system": "Panggilan Balik Sistem" + "system": "System Callback", + "easy_pay": "EasyPay" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_content_mode.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_content_mode.json new file mode 100644 index 000000000..5a8b237a6 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_content_mode.json @@ -0,0 +1,4 @@ +{ + "snapshot": "Snapshot", + "ref": "Reference" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_event.json new file mode 100644 index 000000000..d13d7f922 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_event.json @@ -0,0 +1,7 @@ +{ + "pay.success": "Pay Success", + "pay.fail": "Pay Fail", + "pay.close": "Pay Close", + "refund.success": "Refund Success", + "refund.close": "Refund Close" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_protocol.json new file mode 100644 index 000000000..48eee3cc2 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_protocol.json @@ -0,0 +1,4 @@ +{ + "system": "System", + "easy_pay": "EasyPay" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_source.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_source.json new file mode 100644 index 000000000..e00f1b959 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_source.json @@ -0,0 +1,5 @@ +{ + "order": "Order", + "app": "App", + "protocol": "Protocol" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/callback_notice_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/callback_notice_type.json index 327acf0c0..6f50ddc34 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/callback_notice_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/callback_notice_type.json @@ -1,3 +1,4 @@ { - "system": "การเรียกกลับของระบบ" + "system": "System Callback", + "easy_pay": "EasyPay" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_content_mode.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_content_mode.json new file mode 100644 index 000000000..5a8b237a6 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_content_mode.json @@ -0,0 +1,4 @@ +{ + "snapshot": "Snapshot", + "ref": "Reference" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_event.json new file mode 100644 index 000000000..d13d7f922 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_event.json @@ -0,0 +1,7 @@ +{ + "pay.success": "Pay Success", + "pay.fail": "Pay Fail", + "pay.close": "Pay Close", + "refund.success": "Refund Success", + "refund.close": "Refund Close" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_protocol.json new file mode 100644 index 000000000..48eee3cc2 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_protocol.json @@ -0,0 +1,4 @@ +{ + "system": "System", + "easy_pay": "EasyPay" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_source.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_source.json new file mode 100644 index 000000000..e00f1b959 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_source.json @@ -0,0 +1,5 @@ +{ + "order": "Order", + "app": "App", + "protocol": "Protocol" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/callback_notice_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/callback_notice_type.json index c3ed3833c..6f50ddc34 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/callback_notice_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/callback_notice_type.json @@ -1,3 +1,4 @@ { - "system": "Gọi lại hệ thống" + "system": "System Callback", + "easy_pay": "EasyPay" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_content_mode.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_content_mode.json new file mode 100644 index 000000000..5a8b237a6 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_content_mode.json @@ -0,0 +1,4 @@ +{ + "snapshot": "Snapshot", + "ref": "Reference" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_event.json new file mode 100644 index 000000000..d13d7f922 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_event.json @@ -0,0 +1,7 @@ +{ + "pay.success": "Pay Success", + "pay.fail": "Pay Fail", + "pay.close": "Pay Close", + "refund.success": "Refund Success", + "refund.close": "Refund Close" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_protocol.json new file mode 100644 index 000000000..48eee3cc2 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_protocol.json @@ -0,0 +1,4 @@ +{ + "system": "System", + "easy_pay": "EasyPay" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_source.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_source.json new file mode 100644 index 000000000..e00f1b959 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_source.json @@ -0,0 +1,5 @@ +{ + "order": "Order", + "app": "App", + "protocol": "Protocol" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/callback_notice_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/callback_notice_type.json index 1cb066a17..5fb8120c6 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/callback_notice_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/callback_notice_type.json @@ -1,3 +1,4 @@ { - "system": "系统回调" + "system": "系统回调", + "easy_pay": "易支付" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_content_mode.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_content_mode.json new file mode 100644 index 000000000..4223d82a4 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_content_mode.json @@ -0,0 +1,4 @@ +{ + "snapshot": "快照", + "ref": "引用" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_event.json new file mode 100644 index 000000000..da90e149f --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_event.json @@ -0,0 +1,7 @@ +{ + "pay.success": "支付成功", + "pay.fail": "支付失败", + "pay.close": "支付关闭", + "refund.success": "退款成功", + "refund.close": "退款关闭" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_protocol.json new file mode 100644 index 000000000..672f53303 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_protocol.json @@ -0,0 +1,4 @@ +{ + "system": "系统协议", + "easy_pay": "易支付协议" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_source.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_source.json new file mode 100644 index 000000000..b7ff116ef --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_source.json @@ -0,0 +1,5 @@ +{ + "order": "订单级", + "app": "应用级", + "protocol": "协议适配" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/callback_notice_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/callback_notice_type.json index b82b13ca5..5fb8120c6 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/callback_notice_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/callback_notice_type.json @@ -1,3 +1,4 @@ { - "system": "系統回調" + "system": "系统回调", + "easy_pay": "易支付" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_content_mode.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_content_mode.json new file mode 100644 index 000000000..4223d82a4 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_content_mode.json @@ -0,0 +1,4 @@ +{ + "snapshot": "快照", + "ref": "引用" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_event.json new file mode 100644 index 000000000..bbdf62942 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_event.json @@ -0,0 +1,7 @@ +{ + "pay.success": "支付成功", + "pay.fail": "支付失敗", + "pay.close": "支付關閉", + "refund.success": "退款成功", + "refund.close": "退款關閉" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_protocol.json new file mode 100644 index 000000000..672f53303 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_protocol.json @@ -0,0 +1,4 @@ +{ + "system": "系统协议", + "easy_pay": "易支付协议" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_source.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_source.json new file mode 100644 index 000000000..b7ff116ef --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_source.json @@ -0,0 +1,5 @@ +{ + "order": "订单级", + "app": "应用级", + "protocol": "协议适配" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/callback_notice_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/callback_notice_type.json index 1eb523301..5fb8120c6 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/callback_notice_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/callback_notice_type.json @@ -1,3 +1,4 @@ { - "system": "系統回撥" + "system": "系统回调", + "easy_pay": "易支付" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_content_mode.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_content_mode.json new file mode 100644 index 000000000..4223d82a4 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_content_mode.json @@ -0,0 +1,4 @@ +{ + "snapshot": "快照", + "ref": "引用" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_event.json new file mode 100644 index 000000000..bbdf62942 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_event.json @@ -0,0 +1,7 @@ +{ + "pay.success": "支付成功", + "pay.fail": "支付失敗", + "pay.close": "支付關閉", + "refund.success": "退款成功", + "refund.close": "退款關閉" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_protocol.json new file mode 100644 index 000000000..672f53303 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_protocol.json @@ -0,0 +1,4 @@ +{ + "system": "系统协议", + "easy_pay": "易支付协议" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_source.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_source.json new file mode 100644 index 000000000..b7ff116ef --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_source.json @@ -0,0 +1,5 @@ +{ + "order": "订单级", + "app": "应用级", + "protocol": "协议适配" +} diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/code/PermCodes.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/code/PermCodes.java index 937d48edd..c46a72784 100644 --- a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/code/PermCodes.java +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/code/PermCodes.java @@ -238,6 +238,12 @@ public interface PermCodes { /// 菜单编码 String MENU = "trade:fund"; } + + /// 商户出站通知 menuCode=trade:mch-notice + interface Notice { + /// 菜单编码 + String MENU = "trade:mch-notice"; + } } /// IAM 身份与访问(导航挂系统下,码域独立) diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/CallbackNoticeTypeEnum.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/CallbackNoticeTypeEnum.java index d096bc4ef..4d5a967c6 100644 --- a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/CallbackNoticeTypeEnum.java +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/CallbackNoticeTypeEnum.java @@ -4,8 +4,10 @@ import cn.daxpay.open.platform.core.i18n.I18nSupport; import lombok.Getter; import lombok.RequiredArgsConstructor; -/// # 回调通知类型 +/// # 回调通知类型(已演进为 [NoticeProtocolEnum]) /// +/// @deprecated 使用 [NoticeProtocolEnum] +@Deprecated @Getter @RequiredArgsConstructor public enum CallbackNoticeTypeEnum implements I18nSupport { diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeContentModeEnum.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeContentModeEnum.java new file mode 100644 index 000000000..4a9fd9c83 --- /dev/null +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeContentModeEnum.java @@ -0,0 +1,28 @@ +package cn.daxpay.open.platform.core.enums.pay.notice; + +import cn.daxpay.open.platform.core.i18n.I18nSupport; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/// # 商户出站通知内容策略 +/// +/// 字典: notice_content_mode +@Getter +@RequiredArgsConstructor +public enum NoticeContentModeEnum implements I18nSupport { + + /// 注册时冻结 JSON 快照(重试内容稳定、可审计) + SNAPSHOT("snapshot"), + /// content 仅存业务指针,发送时实时组装 + REF("ref"), + ; + + /// 编码 + private final String code; + + /// 翻译 key 前缀 + @Override + public String getI18nPrefix() { + return "enum.notice_content_mode"; + } +} diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeEventEnum.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeEventEnum.java new file mode 100644 index 000000000..c403d482c --- /dev/null +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeEventEnum.java @@ -0,0 +1,41 @@ +package cn.daxpay.open.platform.core.enums.pay.notice; + +import cn.daxpay.open.platform.core.i18n.I18nSupport; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/// # 商户出站通知事件 +/// +/// 字典: notice_event +/// 与 [TradeTypeEnum] 解耦: 事件码为 `域.动作`,订阅配置可用前缀匹配(如 `pay` 匹配所有 `pay.*`) +@Getter +@RequiredArgsConstructor +public enum NoticeEventEnum implements I18nSupport { + + /// 支付成功 + PAY_SUCCESS("pay.success"), + /// 支付失败 + PAY_FAIL("pay.fail"), + /// 支付关闭 + PAY_CLOSE("pay.close"), + /// 退款成功 + REFUND_SUCCESS("refund.success"), + /// 退款关闭 + REFUND_CLOSE("refund.close"), + ; + + /// 编码 + private final String code; + + /// 翻译 key 前缀 + @Override + public String getI18nPrefix() { + return "enum.notice_event"; + } + + /// 事件所属业务前缀(pay / refund) + public String prefix() { + int dot = code.indexOf('.'); + return dot > 0 ? code.substring(0, dot) : code; + } +} diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeProtocolEnum.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeProtocolEnum.java new file mode 100644 index 000000000..3010e3b8d --- /dev/null +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeProtocolEnum.java @@ -0,0 +1,29 @@ +package cn.daxpay.open.platform.core.enums.pay.notice; + +import cn.daxpay.open.platform.core.i18n.I18nSupport; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/// # 商户出站通知协议 +/// +/// 字典: notice_protocol +/// 演进自原 CallbackNoticeTypeEnum,表示报文协议而非「回调/订阅」双轨 +@Getter +@RequiredArgsConstructor +public enum NoticeProtocolEnum implements I18nSupport { + + /// 标准 DaxPay 签名 JSON 回调 + SYSTEM("system"), + /// 易支付兼容协议 GET 回调 + EASY_PAY("easy_pay"), + ; + + /// 编码 + private final String code; + + /// 翻译 key 前缀 + @Override + public String getI18nPrefix() { + return "enum.notice_protocol"; + } +} diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeSourceEnum.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeSourceEnum.java new file mode 100644 index 000000000..4b8bae61c --- /dev/null +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeSourceEnum.java @@ -0,0 +1,30 @@ +package cn.daxpay.open.platform.core.enums.pay.notice; + +import cn.daxpay.open.platform.core.i18n.I18nSupport; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/// # 商户出站通知 URL 来源 +/// +/// 字典: notice_source +@Getter +@RequiredArgsConstructor +public enum NoticeSourceEnum implements I18nSupport { + + /// 订单级 notifyUrl + ORDER("order"), + /// 应用级订阅配置 + APP("app"), + /// 协议适配层自带 URL(如易支付) + PROTOCOL("protocol"), + ; + + /// 编码 + private final String code; + + /// 翻译 key 前缀 + @Override + public String getI18nPrefix() { + return "enum.notice_source"; + } +} diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NotifyContentTypeEnum.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NotifyContentTypeEnum.java index d9aa04fab..5d0c9c553 100644 --- a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NotifyContentTypeEnum.java +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NotifyContentTypeEnum.java @@ -4,9 +4,11 @@ import cn.daxpay.open.platform.core.i18n.I18nSupport; import lombok.Getter; import lombok.RequiredArgsConstructor; -/// # 客户通知内容类型 +/// # 客户通知内容类型(已演进为 [NoticeEventEnum]) /// /// 字典: notify_content_type +/// @deprecated 使用 [NoticeEventEnum];应用订阅可用前缀 pay/refund 匹配事件 +@Deprecated @Getter @RequiredArgsConstructor public enum NotifyContentTypeEnum implements I18nSupport { diff --git a/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/entity/EasyPayOrder.java b/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/entity/EasyPayOrder.java index 328bcba59..ac35cd522 100644 --- a/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/entity/EasyPayOrder.java +++ b/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/entity/EasyPayOrder.java @@ -62,7 +62,7 @@ public class EasyPayOrder extends MchBaseEntity implements ToResult map = JacksonUtil.toBean(JacksonUtil.toJson(callback), + new TypeReference>() {}); + String query = URLUtil.buildQuery(map, StandardCharsets.UTF_8); + String fullUrl = baseUrl.contains("?") ? baseUrl + "&" + query : baseUrl + "?" + query; + result.setRequestDigest(StrUtil.sub(fullUrl, 0, 500)); + HttpResponse response = HttpUtil.createGet(fullUrl).timeout(15000).execute(); + httpStatus = response.getStatus(); + body = response.body(); + } catch (Exception e) { + log.error("易支付通知发送失败, url={}", baseUrl, e); + return result.setSuccess(false).setHttpStatus(httpStatus).setErrorMsg(e.getMessage()); + } + result.setHttpStatus(httpStatus); + boolean ack = httpStatus != null && httpStatus >= 200 && httpStatus < 300 + && StrUtil.equalsIgnoreCase(StrUtil.trim(body), "SUCCESS"); + result.setSuccess(ack); + if (!ack) { + result.setErrorMsg(StrUtil.blankToDefault(StrUtil.sub(body, 0, 300), "httpStatus=" + httpStatus)); + } + return result; + } +} diff --git a/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/result/api/v1/EasyPayCallbackV1Result.java b/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/result/api/v1/EasyPayCallbackV1Result.java new file mode 100644 index 000000000..6c0709663 --- /dev/null +++ b/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/result/api/v1/EasyPayCallbackV1Result.java @@ -0,0 +1,44 @@ +package cn.daxpay.open.plugin.easypay.result.api.v1; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/// # 易支付 V1 回调报文 +/// +@Data +@Accessors(chain = true) +public class EasyPayCallbackV1Result implements Serializable { + + @JsonProperty("pid") + private Integer pid; + + @JsonProperty("trade_no") + private String tradeNo; + + @JsonProperty("out_trade_no") + private String outTradeNo; + + @JsonProperty("type") + private String type; + + @JsonProperty("name") + private String name; + + @JsonProperty("money") + private String money; + + @JsonProperty("trade_status") + private String tradeStatus; + + @JsonProperty("param") + private String param; + + @JsonProperty("sign") + private String sign; + + @JsonProperty("sign_type") + private String signType; +} diff --git a/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/result/api/v2/EasyPayCallbackV2Result.java b/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/result/api/v2/EasyPayCallbackV2Result.java new file mode 100644 index 000000000..a92f30be7 --- /dev/null +++ b/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/result/api/v2/EasyPayCallbackV2Result.java @@ -0,0 +1,61 @@ +package cn.daxpay.open.plugin.easypay.result.api.v2; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/// # 易支付 V2 回调报文 +/// +@Data +@Accessors(chain = true) +@Schema(title = "易支付V2回调报文") +public class EasyPayCallbackV2Result implements Serializable { + + @JsonProperty("pid") + private Integer pid; + + @JsonProperty("trade_no") + private String tradeNo; + + @JsonProperty("out_trade_no") + private String outTradeNo; + + @JsonProperty("api_trade_no") + private String apiTradeNo; + + @JsonProperty("type") + private String type; + + @JsonProperty("trade_status") + private String tradeStatus; + + @JsonProperty("addtime") + private String addTime; + + @JsonProperty("endtime") + private String endTime; + + @JsonProperty("name") + private String name; + + @JsonProperty("money") + private String money; + + @JsonProperty("param") + private String param; + + @JsonProperty("buyer") + private String buyer; + + @JsonProperty("timestamp") + private String timestamp; + + @JsonProperty("sign") + private String sign; + + @JsonProperty("sign_type") + private String signType; +} diff --git a/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/service/api/v2/EasyPayPayV2Service.java b/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/service/api/v2/EasyPayPayV2Service.java index 722090885..679f1129f 100644 --- a/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/service/api/v2/EasyPayPayV2Service.java +++ b/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/service/api/v2/EasyPayPayV2Service.java @@ -176,7 +176,7 @@ public class EasyPayPayV2Service { payParam.setOpenId(openId); payParam.setAuthCode(authCode); payParam.setClientIp(order.getClientIp()); - payParam.setNotifyUrl(order.getNotifyUrl()); + // 易支付 notifyUrl 仅落在协议单,不写内核订单,避免与 system 协议双发 payParam.setReturnUrl(order.getReturnUrl()); payParam.setAttach(order.getParam()); payParam.setSource(easyPayAssistService.sourceCode()); diff --git a/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/strategy/EasyPayPluginStrategy.java b/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/strategy/EasyPayPluginStrategy.java index 76d7b9a57..470d048ad 100644 --- a/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/strategy/EasyPayPluginStrategy.java +++ b/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/strategy/EasyPayPluginStrategy.java @@ -1,14 +1,23 @@ package cn.daxpay.open.plugin.easypay.strategy; import cn.daxpay.open.payment.strategy.plugin.AbsPayPluginStrategy; +import cn.daxpay.open.payment.trade.notice.command.NoticeDispatchCommand; +import cn.daxpay.open.payment.trade.notice.service.NoticeDispatcher; import cn.daxpay.open.payment.trade.order.entity.RefundOrder; import cn.daxpay.open.payment.trade.order.entity.PayTrade; +import cn.daxpay.open.platform.common.json.util.JacksonUtil; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeContentModeEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeEventEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeProtocolEnum; import cn.daxpay.open.platform.core.enums.pay.trade.TradeSourceEnum; +import cn.daxpay.open.plugin.easypay.entity.EasyPayOrder; import cn.daxpay.open.plugin.easypay.service.order.EasyPayOrderService; +import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.Map; import java.util.Objects; /// # 易支付插件生命周期策略 @@ -20,14 +29,16 @@ import java.util.Objects; public class EasyPayPluginStrategy implements AbsPayPluginStrategy { private final EasyPayOrderService easyPayOrderService; + private final NoticeDispatcher noticeDispatcher; - /// 支付成功:回写协议单状态 + /// 支付成功:回写协议单状态并注册易支付出站通知 @Override public void paySuccess(PayTrade trade) { if (!Objects.equals(TradeSourceEnum.EASY_PAY.getCode(), trade.getSource())) { return; } - easyPayOrderService.paySuccess(trade); + EasyPayOrder easyPayOrder = easyPayOrderService.paySuccess(trade); + registerEasyPayNotice(easyPayOrder); } /// 关单:一期仅日志 @@ -48,4 +59,28 @@ public class EasyPayPluginStrategy implements AbsPayPluginStrategy { long amount = refundOrder.getAmount() == null ? 0L : refundOrder.getAmount(); easyPayOrderService.refundSuccess(trade, amount); } + + /// 注册易支付协议出站(content 仅存 id 指针) + private void registerEasyPayNotice(EasyPayOrder easyPayOrder) { + if (easyPayOrder == null || StrUtil.isBlank(easyPayOrder.getNotifyUrl())) { + log.info("易支付订单无需回调, outTradeNo={}", + easyPayOrder == null ? null : easyPayOrder.getOutTradeNo()); + return; + } + String content = JacksonUtil.toJson(Map.of( + "id", easyPayOrder.getId(), + "pid", easyPayOrder.getPid() == null ? 0 : easyPayOrder.getPid(), + "remark", "ref-only; payload assembled at send time" + )); + noticeDispatcher.dispatch(new NoticeDispatchCommand() + .setMchNo(easyPayOrder.getMchNo()) + .setAppId(easyPayOrder.getAppId()) + .setEvent(NoticeEventEnum.PAY_SUCCESS.getCode()) + .setBizId(easyPayOrder.getId()) + .setBizNo(easyPayOrder.getTradeNo() != null ? easyPayOrder.getTradeNo() : easyPayOrder.getOutTradeNo()) + .setProtocol(NoticeProtocolEnum.EASY_PAY) + .setContentMode(NoticeContentModeEnum.REF) + .setContentOrRef(content) + .setProtocolNotifyUrl(easyPayOrder.getNotifyUrl())); + } }