From 4ced54dfab742a85e3c6653ea0f323fa025bfc18 Mon Sep 17 00:00:00 2001 From: daxpay Date: Thu, 30 Jul 2026 12:32:41 +0800 Subject: [PATCH] =?UTF-8?q?refactor(payment):=20=E9=80=9A=E9=81=93?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E7=B1=BB=E5=9E=8B=E8=A7=A3=E6=9E=90=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=A4=9A=E5=85=BC=E5=AE=B9=E7=B1=BB=E5=9E=8B=E9=81=8D?= =?UTF-8?q?=E5=8E=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 微信/抖音/支付宝应用类型映射由单值改为有序兼容列表, 多个同类型应用时跳过并尝试下一个兼容类型 - 抖音 H5 验签 web_app 报错参数改用本地化名称(网站应用), 不再显示裸编码 - 补齐 wx_app_type 东盟四语(id-ID/vi-VN/th-TH/ms-MY) - 抖音直连应用体系(douyin_direct_app*)下线, 迁移至 dy_mch_app/dy_channel_app_capability 统一模式 - 删除已废弃的抖音应用管理菜单, 清理通道子应用配置注释 --- _config/sql/iam_perm_menu.sql | 2 - _config/sql/table.sql | 133 +----------------- _config/sql/update-datas.sql | 29 ---- _config/sql/update-tables.sql | 7 + .../alipay/code/AlipayDirectAppTypeCode.java | 41 +++--- .../AlipayDirectAppCapabilityService.java | 18 +-- .../direct/AlipayDirectAppService.java | 11 ++ .../wechat/code/WechatAppTypeCode.java | 7 +- .../douyin/entity/merchant/DyMchApp.java | 2 - .../douyin/entity/platform/DyPlatformApp.java | 2 - .../payment/douyin/enums/DyAppTypeEnum.java | 37 +++-- .../douyin/service/DyAppResolveService.java | 25 ++-- .../service/merchant/DyMchAppService.java | 9 ++ .../platform/DyPlatformAppService.java | 10 ++ .../payment/wx/entity/merchant/WxMchApp.java | 2 - .../wx/entity/platform/WxPlatformApp.java | 2 - .../open/payment/wx/enums/WxAppTypeEnum.java | 43 +++--- .../wx/service/WxAppResolveService.java | 18 +-- .../wx/service/merchant/WxMchAppService.java | 9 ++ .../platform/WxPlatformAppService.java | 10 ++ .../i18n/id-ID/enum/wx_app_type.json | 5 + .../i18n/ms-MY/enum/wx_app_type.json | 5 + .../i18n/th-TH/enum/wx_app_type.json | 5 + .../i18n/vi-VN/enum/wx_app_type.json | 5 + .../src/main/resources/application-dev.yml | 8 +- 25 files changed, 180 insertions(+), 265 deletions(-) create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/wx_app_type.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/wx_app_type.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/wx_app_type.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/wx_app_type.json diff --git a/_config/sql/iam_perm_menu.sql b/_config/sql/iam_perm_menu.sql index 1d8999b45..ad464fc9c 100644 --- a/_config/sql/iam_perm_menu.sql +++ b/_config/sql/iam_perm_menu.sql @@ -142,7 +142,6 @@ INSERT INTO "public"."iam_perm_menu" VALUES (308, 3, 'system:notify', 'admin', ' INSERT INTO "public"."iam_perm_menu" VALUES (40502, 40508, 'payment:isv', 'admin', 'AlipayIsvAppManage', 'menu.payment.config.alipayApp', NULL, 't', 'f', '/payment/channel/alipay/manage/app/AlipayIsvAppManage', '/payment/config/product/app-manage', NULL, 3, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-14 00:00:00+00', '2026-07-11 03:37:15.43231+00'); INSERT INTO "public"."iam_perm_menu" VALUES (4040106, 4040131, 'channel:merchant', 'admin', 'ChannelMerchant', 'menu.payment.merchant.channelMerchant', NULL, 't', 'f', '/payment/global/channel-merchant/ChannelMerchantList', '/payment/global/channel-merchant', NULL, 6, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-05-03 16:00:00+00', '2026-07-22 13:50:46.61698+00'); INSERT INTO "public"."iam_perm_menu" VALUES (30403, 301, 'system:protocol', 'admin', 'UserProtocol', 'menu.system.basic.protocol', 'lucide:file-text', 'f', 'f', '/system/basic/protocol/UserProtocolList', '/system/basic/protocol', NULL, 2, 'f', 't', 'f', 1, 1, 1, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-24 10:32:13.183371+00', '2026-07-16 08:43:35.038937+00'); -INSERT INTO "public"."iam_perm_menu" VALUES (4040115, 4040132, 'channel:app', 'admin', 'DouyinMchAppManage', 'menu.payment.merchant.channelMerchant.douyinApp', NULL, 't', 'f', '/payment/channel/douyin/manage/app/DouyinMchAppManage', '/payment/global/channel-merchant/douyin-app-manage', NULL, 13, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-15 16:00:00+00', '2026-07-22 13:50:46.642335+00'); INSERT INTO "public"."iam_perm_menu" VALUES (802, 8, 'develop:sign', 'admin', 'DevelopSign', 'menu.develop.sign', 'lucide:pen-tool', 'f', 'f', '/payment/develop/sign/DevelopSign', '/develop/sign', NULL, 3, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-23 16:00:00+00', '2026-06-23 16:00:00+00'); INSERT INTO "public"."iam_perm_menu" VALUES (310, 308, 'system:notify:wechat-config', 'admin', 'WechatNotify', 'menu.system.notify.wechatConfig', 'lucide:message-circle', 'f', 'f', '/system/notify/wechat/index', '/system/notify/wechat', NULL, 30, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-09 00:00:00+00', '2026-07-09 00:00:00+00'); INSERT INTO "public"."iam_perm_menu" VALUES (4040121, 4040130, 'merchant:gateway-aggregate', 'admin', 'AggregateScanConfig', 'menu.payment.merchant.aggregateScan', NULL, 't', 'f', '/payment/merchant/aggregate/AggregateScanConfig', '/payment/merchant/aggregate', NULL, 3, 'f', 't', 'f', 1, 1, 1, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-11 03:00:13.374496+00', '2026-07-13 10:13:20.81126+00'); @@ -217,7 +216,6 @@ INSERT INTO "public"."iam_perm_menu" VALUES (91409, 91403, 'merchant:easypay', ' INSERT INTO "public"."iam_perm_menu" VALUES (91501, 91300, 'merchant:credential', 'merchant', 'MerchantCredentialConfig', 'menu.payment.merchant.credential', 'lucide:key', 'f', 'f', '/payment/merchant/credential/MerchantCredentialConfig', '/mch/credential', NULL, 5, 'f', 'f', 'f', 0, 1, 1, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-24 13:08:44.950986+00', '2026-07-24 13:08:44.950986+00'); INSERT INTO "public"."iam_perm_menu" VALUES (91305, 91300, 'merchant:wx-verify', 'merchant', 'MchWxDomainVerifyList', 'menu.payment.merchant.wxVerify', 'lucide:shield-check', 'f', 'f', '/payment/merchant/wx-verify/MchWxDomainVerifyList', '/mch/wx-verify', NULL, 6, 'f', 'f', 'f', 0, 1, 1, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-24 14:17:59.251093+00', '2026-07-24 14:17:59.251093+00'); INSERT INTO "public"."iam_perm_menu" VALUES (91421, 91402, 'channel:app', 'merchant', 'AlipayMchAppManage', 'menu.payment.merchant.channelMerchant.alipayApp', NULL, 't', 'f', '/payment/channel/alipay/manage/mch/app/AlipayMchAppManage', '/mch/channel-merchant/alipay-app-manage', NULL, 4, 'f', 'f', 'f', 0, 1, 1, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-25 01:53:14.928869+00', '2026-07-25 01:53:14.928869+00'); -INSERT INTO "public"."iam_perm_menu" VALUES (91422, 91402, 'channel:app', 'merchant', 'DouyinMchAppManage', 'menu.payment.merchant.channelMerchant.douyinApp', NULL, 't', 'f', '/payment/channel/douyin/manage/app/DouyinMchAppManage', '/mch/channel-merchant/douyin-app-manage', NULL, 5, 'f', 'f', 'f', 0, 1, 1, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-25 03:24:01.70476+00', '2026-07-25 03:24:01.70476+00'); INSERT INTO "public"."iam_perm_menu" VALUES (91601, 91600, 'develop:trade', 'merchant', 'DevelopTrade', 'menu.develop.trade', 'lucide:credit-card', 'f', 'f', '/payment/develop/trade/DevelopTrade', '/develop/trade', NULL, 1, 'f', 'f', 'f', 0, NULL, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-25 15:10:00+00', '2026-07-25 15:10:00+00'); INSERT INTO "public"."iam_perm_menu" VALUES (91602, 91600, 'develop:gateway', 'merchant', 'DevelopGateway', 'menu.develop.gateway', 'lucide:globe', 'f', 'f', '/payment/develop/gateway/DevelopGateway', '/develop/gateway', NULL, 2, 'f', 'f', 'f', 0, NULL, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-25 15:10:00+00', '2026-07-25 15:10:00+00'); INSERT INTO "public"."iam_perm_menu" VALUES (91603, 91600, 'develop:sign', 'merchant', 'DevelopSign', 'menu.develop.sign', 'lucide:pen-tool', 'f', 'f', '/payment/develop/sign/DevelopSign', '/develop/sign', NULL, 3, 'f', 'f', 'f', 0, NULL, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-25 15:10:00+00', '2026-07-25 15:10:00+00'); diff --git a/_config/sql/table.sql b/_config/sql/table.sql index fe9a5aa1d..7d59bd33e 100644 --- a/_config/sql/table.sql +++ b/_config/sql/table.sql @@ -2605,69 +2605,7 @@ COMMENT ON COLUMN "public"."douyin_direct_channel_merchant"."version" IS '乐观 COMMENT ON COLUMN "public"."douyin_direct_channel_merchant"."deleted" IS '逻辑删除标志'; COMMENT ON TABLE "public"."douyin_direct_channel_merchant" IS '抖音直连通道商户绑定'; --- ---------------------------- --- Table structure for douyin_direct_app_auth_config --- ---------------------------- -DROP TABLE IF EXISTS "public"."douyin_direct_app_auth_config"; -CREATE TABLE "public"."douyin_direct_app_auth_config" ( - "id" int8 NOT NULL, - "mch_no" varchar(32) COLLATE "pg_catalog"."default" NOT NULL, - "channel_mch_no" varchar(64) COLLATE "pg_catalog"."default" NOT NULL, - "douyin_direct_app_id" int8, - "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, - "app_secret" varchar(512) COLLATE "pg_catalog"."default" -) -; -COMMENT ON COLUMN "public"."douyin_direct_app_auth_config"."id" IS '主键'; -COMMENT ON COLUMN "public"."douyin_direct_app_auth_config"."mch_no" IS '商户号'; -COMMENT ON COLUMN "public"."douyin_direct_app_auth_config"."channel_mch_no" IS '通道商户号'; -COMMENT ON COLUMN "public"."douyin_direct_app_auth_config"."douyin_direct_app_id" IS '关联抖音直连应用 ID'; -COMMENT ON COLUMN "public"."douyin_direct_app_auth_config"."creator" IS '创建人ID'; -COMMENT ON COLUMN "public"."douyin_direct_app_auth_config"."create_time" IS '创建时间'; -COMMENT ON COLUMN "public"."douyin_direct_app_auth_config"."last_modifier" IS '最后修改人ID'; -COMMENT ON COLUMN "public"."douyin_direct_app_auth_config"."last_modified_time" IS '最后修改时间'; -COMMENT ON COLUMN "public"."douyin_direct_app_auth_config"."version" IS '乐观锁版本号'; -COMMENT ON COLUMN "public"."douyin_direct_app_auth_config"."deleted" IS '逻辑删除标志'; -COMMENT ON COLUMN "public"."douyin_direct_app_auth_config"."app_secret" IS '应用密钥(加密存储)'; -COMMENT ON TABLE "public"."douyin_direct_app_auth_config" IS '抖音直连商户应用授权认证配置'; - --- ---------------------------- --- Table structure for douyin_direct_app --- ---------------------------- -DROP TABLE IF EXISTS "public"."douyin_direct_app"; -CREATE TABLE "public"."douyin_direct_app" ( - "id" int8 NOT NULL, - "mch_no" varchar(32) COLLATE "pg_catalog"."default" NOT NULL, - "channel_mch_no" varchar(64) COLLATE "pg_catalog"."default" NOT NULL, - "app_name" varchar(64) COLLATE "pg_catalog"."default", - "douyin_app_id" varchar(64) COLLATE "pg_catalog"."default", - "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, - "app_type" varchar(32) COLLATE "pg_catalog"."default" -) -; -COMMENT ON COLUMN "public"."douyin_direct_app"."id" IS '主键'; -COMMENT ON COLUMN "public"."douyin_direct_app"."mch_no" IS '商户号'; -COMMENT ON COLUMN "public"."douyin_direct_app"."channel_mch_no" IS '通道商户号'; -COMMENT ON COLUMN "public"."douyin_direct_app"."app_name" IS '应用名称'; -COMMENT ON COLUMN "public"."douyin_direct_app"."douyin_app_id" IS '抖音应用AppId(APPID)'; -COMMENT ON COLUMN "public"."douyin_direct_app"."creator" IS '创建人ID'; -COMMENT ON COLUMN "public"."douyin_direct_app"."create_time" IS '创建时间'; -COMMENT ON COLUMN "public"."douyin_direct_app"."last_modifier" IS '最后修改人ID'; -COMMENT ON COLUMN "public"."douyin_direct_app"."last_modified_time" IS '最后修改时间'; -COMMENT ON COLUMN "public"."douyin_direct_app"."version" IS '乐观锁版本号'; -COMMENT ON COLUMN "public"."douyin_direct_app"."deleted" IS '逻辑删除标志'; -COMMENT ON COLUMN "public"."douyin_direct_app"."app_type" IS '应用类型(mini_program小程序/mobile_app移动应用/web_app网站应用)'; -COMMENT ON TABLE "public"."douyin_direct_app" IS '抖音直连商户应用'; +-- (douyin_direct_app / douyin_direct_app_auth_config 表已废弃, 改用 payment-core 跨通道通用应用模式, 详见 update-tables.sql) -- ---------------------------- -- Table structure for device_qr_code @@ -3856,36 +3794,7 @@ COMMENT ON COLUMN "public"."hkrt_isv_channel_merchant"."create_time" IS '创建 COMMENT ON COLUMN "public"."hkrt_isv_channel_merchant"."sandbox" IS '是否沙箱环境商户'; COMMENT ON TABLE "public"."hkrt_isv_channel_merchant" IS '海科融通通道商户绑定'; --- ---------------------------- --- Table structure for douyin_direct_app_capability --- ---------------------------- -DROP TABLE IF EXISTS "public"."douyin_direct_app_capability"; -CREATE TABLE "public"."douyin_direct_app_capability" ( - "id" int8 NOT NULL DEFAULT nextval('douyin_direct_app_capability_id_seq'::regclass), - "mch_no" varchar(32) COLLATE "pg_catalog"."default" NOT NULL, - "channel_mch_no" varchar(32) COLLATE "pg_catalog"."default" NOT NULL, - "capability" varchar(64) COLLATE "pg_catalog"."default" NOT NULL, - "douyin_direct_app_id" int8 NOT NULL, - "creator" int8, - "create_time" timestamptz(6) DEFAULT now(), - "last_modifier" int8, - "last_modified_time" timestamptz(6), - "version" int4 NOT NULL DEFAULT 0, - "deleted" bool NOT NULL DEFAULT false -) -; -COMMENT ON COLUMN "public"."douyin_direct_app_capability"."id" IS '主键'; -COMMENT ON COLUMN "public"."douyin_direct_app_capability"."mch_no" IS '商户号'; -COMMENT ON COLUMN "public"."douyin_direct_app_capability"."channel_mch_no" IS '通道商户号'; -COMMENT ON COLUMN "public"."douyin_direct_app_capability"."capability" IS '支付能力编码(如 DOUYIN_QR/DOUYIN_JSAPI 等)'; -COMMENT ON COLUMN "public"."douyin_direct_app_capability"."douyin_direct_app_id" IS '关联抖音直连应用 ID'; -COMMENT ON COLUMN "public"."douyin_direct_app_capability"."creator" IS '创建人ID'; -COMMENT ON COLUMN "public"."douyin_direct_app_capability"."create_time" IS '创建时间'; -COMMENT ON COLUMN "public"."douyin_direct_app_capability"."last_modifier" IS '最后修改人ID'; -COMMENT ON COLUMN "public"."douyin_direct_app_capability"."last_modified_time" IS '最后修改时间'; -COMMENT ON COLUMN "public"."douyin_direct_app_capability"."version" IS '乐观锁版本号'; -COMMENT ON COLUMN "public"."douyin_direct_app_capability"."deleted" IS '逻辑删除标志'; -COMMENT ON TABLE "public"."douyin_direct_app_capability" IS '抖音直连商户应用支付能力关联'; +-- (douyin_direct_app_capability 表已废弃, 改用 payment-core 跨通道通用应用模式, 详见 update-tables.sql) -- ---------------------------- -- Table structure for base_street @@ -5014,25 +4923,6 @@ COMMENT ON INDEX "public"."uk_douyin_direct_mch_dyid" IS '同一商户下抖音 -- ---------------------------- ALTER TABLE "public"."douyin_direct_channel_merchant" ADD CONSTRAINT "pk_douyin_direct_channel_merchant" PRIMARY KEY ("id"); --- ---------------------------- --- Primary Key structure for table douyin_direct_app_auth_config --- ---------------------------- -ALTER TABLE "public"."douyin_direct_app_auth_config" ADD CONSTRAINT "pk_douyin_direct_app_auth_config" PRIMARY KEY ("id"); - --- ---------------------------- --- Indexes structure for table douyin_direct_app --- ---------------------------- -CREATE UNIQUE INDEX "uk_douyin_direct_app_appid" ON "public"."douyin_direct_app" USING btree ( - "channel_mch_no" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST, - "douyin_app_id" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST -) WHERE deleted = false; -COMMENT ON INDEX "public"."uk_douyin_direct_app_appid" IS '同一通道商户下抖音应用 ID 唯一'; - --- ---------------------------- --- Primary Key structure for table douyin_direct_app --- ---------------------------- -ALTER TABLE "public"."douyin_direct_app" ADD CONSTRAINT "pk_douyin_direct_app" PRIMARY KEY ("id"); - -- ---------------------------- -- Indexes structure for table device_qr_code -- ---------------------------- @@ -5528,25 +5418,6 @@ CREATE UNIQUE INDEX "uk_hkrt_isv_key_prod_sandbox" ON "public"."hkrt_isv_key_con ) WHERE deleted = false; COMMENT ON INDEX "public"."uk_hkrt_isv_key_prod_sandbox" IS '同一产品同一环境密钥唯一'; --- ---------------------------- --- Indexes structure for table douyin_direct_app_capability --- ---------------------------- -CREATE UNIQUE INDEX "uk_douyin_direct_cap_cmchno_cap" ON "public"."douyin_direct_app_capability" USING btree ( - "channel_mch_no" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST, - "capability" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST -) WHERE deleted = false; -COMMENT ON INDEX "public"."uk_douyin_direct_cap_cmchno_cap" IS '同一通道商户下支付能力唯一'; - --- ---------------------------- --- Uniques structure for table douyin_direct_app_capability --- ---------------------------- -ALTER TABLE "public"."douyin_direct_app_capability" ADD CONSTRAINT "uk_douyin_direct_app_capability" UNIQUE ("channel_mch_no", "capability"); - --- ---------------------------- --- Primary Key structure for table douyin_direct_app_capability --- ---------------------------- -ALTER TABLE "public"."douyin_direct_app_capability" ADD CONSTRAINT "douyin_direct_app_capability_pkey" PRIMARY KEY ("id"); - -- ---------------------------- -- Indexes structure for table base_street -- ---------------------------- diff --git a/_config/sql/update-datas.sql b/_config/sql/update-datas.sql index c5fa9d091..e69de29bb 100644 --- a/_config/sql/update-datas.sql +++ b/_config/sql/update-datas.sql @@ -1,29 +0,0 @@ --- 微信通道旧应用体系下线: 删除已废弃的直连/服务商应用管理菜单 --- 4040113=WechatMchAppManage(直连商户应用)、4040119=WechatIsvMchAppManage(ISV子商户应用)、40503=WechatIsvAppManage(ISV服务商应用) --- 入口已由 WxAppHub(id=40106, menu.payment.wx.app)统一接管 - -DELETE FROM "public"."iam_perm_menu" WHERE "id" IN (4040113, 4040119, 40503); - --- 「移动端应用管理」菜单归属迁移: 系统管理>系统配置 → 支付管理(顶级) --- 40504=MobileAppConfig(移动端应用管理)、40505=MobileAppDetail(移动端应用详情) --- 原因: 该配置的 cashier(收银台小程序)凭证被支付核心 CashierAuthService 直接依赖, 属支付链路前置配置; --- 表 pay_platform_mobile_app 前缀亦属支付域。pid 由 304(系统配置) 改为 4(支付管理)。 --- 注: 405(支付配置 catalog) 在运营端已软删(deleted=t), 故直接挂顶级 4 下, 与"支付产品配置(40105)"平级。 - --- 40504 主菜单: pid 304→4, menu_code/i18n_key/component/path 支付域化, sort_no 调整到支付产品配置之后 -UPDATE "public"."iam_perm_menu" SET - "pid" = 4, - "menu_code" = 'payment:config:mobile-app', - "i18n_key" = 'menu.payment.config.mobileApp', - "component" = '/payment/config/mobile-app/MobileAppConfig', - "path" = '/payment/config/mobile-app', - "sort_no" = 2.7 -WHERE "id" = 40504; - --- 40505 详情子页: menu_code/i18n_key/component/path 支付域化 (pid 仍挂 40504) -UPDATE "public"."iam_perm_menu" SET - "menu_code" = 'payment:config:mobile-app-detail', - "i18n_key" = 'menu.payment.config.mobileAppDetail', - "component" = '/payment/config/mobile-app/detail/MobileAppDetail', - "path" = '/payment/config/mobile-app/detail/:appType' -WHERE "id" = 40505; diff --git a/_config/sql/update-tables.sql b/_config/sql/update-tables.sql index 7ff2935c1..8eb3dab63 100644 --- a/_config/sql/update-tables.sql +++ b/_config/sql/update-tables.sql @@ -14,3 +14,10 @@ DROP TABLE IF EXISTS "public"."wechat_isv_mch_app_capability"; -- 微信服务商通道商户: 移除冗余的认证应用类型字段 -- 实际 sp/sub 路由由通道能力绑定(WxChannelAppCapability)决定, 该字段不被任何业务逻辑消费 ALTER TABLE "public"."wechat_isv_channel_merchant" DROP COLUMN IF EXISTS "auth_app_type"; + +-- 抖音通道旧直连应用体系下线: 删除已废弃的直连应用/能力/授权配置表 +-- 抖音改用 payment-core 跨通道通用应用模式(对齐微信), 应用身份迁移至 dy_mch_app / dy_channel_app_capability 统一体系 +DROP TABLE IF EXISTS "public"."douyin_direct_app_auth_config"; +DROP TABLE IF EXISTS "public"."douyin_direct_app_capability"; +DROP TABLE IF EXISTS "public"."douyin_direct_app"; +DROP SEQUENCE IF EXISTS "public"."douyin_direct_app_capability_id_seq"; diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/code/AlipayDirectAppTypeCode.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/code/AlipayDirectAppTypeCode.java index 1ae12b198..818a0b0fa 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/code/AlipayDirectAppTypeCode.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/code/AlipayDirectAppTypeCode.java @@ -2,6 +2,7 @@ package cn.daxpay.open.channel.alipay.code; import cn.daxpay.open.platform.core.enums.pay.channel.PayCapabilityEnum; +import java.util.List; import java.util.Map; /// # 支付宝直连应用类型与支付能力映射 @@ -26,33 +27,39 @@ public final class AlipayDirectAppTypeCode { /// 应用类型: 网站应用 public static final String WEB_APP = "web_app"; - /// 支付能力 → 应用类型的默认推导映射 + /// 支付能力 → 兼容应用类型集合(有序, 首个为兜底推导优先级) /// - /// - JSAPI 支付(含小程序) → 小程序应用(官方 JSAPI_PAY 主场景) + /// - JSAPI → 小程序(场景强绑 buyer_id) /// - APP支付 → 移动应用 - /// - 其余支付宝能力(付款码/扫码/H5/PC) → 网站应用 - private static final Map CAPABILITY_APP_TYPE_MAP = Map.of( - PayCapabilityEnum.ALIPAY_JSAPI.getCode(), MINI_PROGRAM, - PayCapabilityEnum.ALIPAY_APP.getCode(), MOBILE_APP, - PayCapabilityEnum.ALIPAY_BARCODE.getCode(), WEB_APP, - PayCapabilityEnum.ALIPAY_QR.getCode(), WEB_APP, - PayCapabilityEnum.ALIPAY_H5.getCode(), WEB_APP, - PayCapabilityEnum.ALIPAY_PC.getCode(), WEB_APP); + /// - 付款码/扫码/H5/PC → 网站应用优先, 兼容小程序/移动应用(不依赖 buyer_id) + private static final Map> CAPABILITY_APP_TYPE_MAP = Map.of( + PayCapabilityEnum.ALIPAY_JSAPI.getCode(), List.of(MINI_PROGRAM), + PayCapabilityEnum.ALIPAY_APP.getCode(), List.of(MOBILE_APP), + PayCapabilityEnum.ALIPAY_BARCODE.getCode(), List.of(WEB_APP, MINI_PROGRAM, MOBILE_APP), + PayCapabilityEnum.ALIPAY_QR.getCode(), List.of(WEB_APP, MINI_PROGRAM, MOBILE_APP), + PayCapabilityEnum.ALIPAY_H5.getCode(), List.of(WEB_APP, MINI_PROGRAM, MOBILE_APP), + PayCapabilityEnum.ALIPAY_PC.getCode(), List.of(WEB_APP, MINI_PROGRAM, MOBILE_APP)); - /// 根据支付能力编码推导默认应用类型;未知能力返回 null - public static String resolveAppType(String capabilityCode) { + /// 根据支付能力推导全部兼容应用类型(有序, 首个为兜底优先级);未知能力返回空列表 + public static List resolveCompatibleAppTypes(String capabilityCode) { if (capabilityCode == null) { - return null; + return List.of(); } - return CAPABILITY_APP_TYPE_MAP.get(capabilityCode); + return CAPABILITY_APP_TYPE_MAP.getOrDefault(capabilityCode, List.of()); + } + + /// 根据支付能力编码推导默认(优先级最高)应用类型;未知能力返回 null + public static String resolveAppType(String capabilityCode) { + List types = resolveCompatibleAppTypes(capabilityCode); + return types.isEmpty() ? null : types.getFirst(); } /// 校验应用类型与支付能力是否兼容;未知能力不做强制约束 public static boolean isCompatible(String appType, String capability) { - String expected = resolveAppType(capability); - if (expected == null) { + List types = resolveCompatibleAppTypes(capability); + if (types.isEmpty()) { return true; } - return expected.equals(appType); + return types.contains(appType); } } diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectAppCapabilityService.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectAppCapabilityService.java index 93ed64e34..10265b8ff 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectAppCapabilityService.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectAppCapabilityService.java @@ -112,10 +112,10 @@ public class AlipayDirectAppCapabilityService { /// 支付/回调解析应用:显式配置 > appType 自动推导(须已装载 mchNo,租户内) /// - /// appType 推导要求该通道商户下该类型应用唯一命中: - /// - 唯一命中:返回该应用 - /// - 该类型存在多个应用:抛 appNotUnique,要求显式配置能力绑 - /// - 该类型无应用:返回 empty,由调用方走最终报错 + /// appType 推导:遍历兼容类型(按优先级), 该通道商户下首个唯一命中即返回: + /// - 某类型恰好 1 个:返回该应用 + /// - 某类型多个/无:跳过, 继续下一个兼容类型 + /// - 全部兼容类型均未唯一命中:返回 empty, 由调用方走最终报错 /// /// 认证无上下文请用 [#resolveAppNotTenant]。 public Optional resolveApp(String channelMchNo, String capability) { @@ -127,15 +127,9 @@ public class AlipayDirectAppCapabilityService { if (rel.isPresent()) { return alipayDirectAppManager.findById(rel.get().getAlipayDirectAppId()); } - // 2. appType 推导:要求该通道商户下该类型应用唯一命中,>1 拒绝猜测 - String appType = AlipayDirectAppTypeCode.resolveAppType(capability); - if (appType != null) { + // 2. appType 推导:遍历兼容类型(按优先级), 该通道商户下首个唯一命中即返回; 多个则跳过 + for (String appType : AlipayDirectAppTypeCode.resolveCompatibleAppTypes(capability)) { List apps = alipayDirectAppManager.listByChannelMchNoAndAppType(channelMchNo, appType); - if (apps.size() > 1) { - // 存在多个同类型应用,请显式配置能力绑定以明确选择 - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "error.channel.alipay.appNotUnique", appType); - } if (apps.size() == 1) { return Optional.of(apps.getFirst()); } diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectAppService.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectAppService.java index e20d3851e..0914caa02 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectAppService.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/direct/AlipayDirectAppService.java @@ -15,8 +15,10 @@ import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Objects; /// # 支付宝直连商户应用管理 /// @@ -62,14 +64,23 @@ public class AlipayDirectAppService { } /// 更新支付宝直连商户应用 + @Transactional(rollbackFor = Exception.class) public void update(AlipayDirectAppParam param) { var entity = alipayDirectAppManager.findById(param.getId()) // 支付宝: 直连商户应用不存在 .orElseThrow(() -> new DataNotExistException("error.channel.alipay.mchAppNotFound")); this.assertScopeMatch(entity, param.getMchNo(), param.getChannelMchNo()); this.assertAliAppIdUnique(entity.getMchNo(), entity.getChannelMchNo(), param.getAliAppId(), param.getId()); + // 记录原始应用类型, 用于判断是否需要联动清理支付能力绑定 + String oldAppType = entity.getAppType(); AlipayDirectAppConvert.CONVERT.copy(param, entity); alipayDirectAppManager.updateById(entity); + // 应用类型变更: 旧 appType 下的能力绑定对新类型不再兼容, 清理后由用户重新配置 + if (!Objects.equals(oldAppType, param.getAppType())) { + alipayDirectAppCapabilityService.deleteByAlipayDirectAppId(entity.getId()); + log.warn("支付宝直连商户应用[{}] appType 从[{}]变更为[{}], 已清理能力绑定", + entity.getId(), oldAppType, param.getAppType()); + } } /// 删除应用(级联删除密钥配置、授权认证配置与能力关联) diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/code/WechatAppTypeCode.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/code/WechatAppTypeCode.java index e5a92d88d..534a1289b 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/code/WechatAppTypeCode.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/code/WechatAppTypeCode.java @@ -26,12 +26,15 @@ public final class WechatAppTypeCode { /// 应用类型: 移动应用 public static final String MOBILE_APP = "mobile_app"; - /// 支付能力 → 应用类型的默认推导映射 + /// 支付能力 → 默认应用类型映射 + /// + /// 注意: 本类已废弃(运行时无调用), 完整多值兼容规则(扫码/付款码/H5 的兼容类型)统一以 + /// payment-core 的 [cn.daxpay.open.payment.wx.enums.WxAppTypeEnum#resolveCompatibleAppTypes] 为准。 /// /// - JSAPI支付 → 公众号 /// - 小程序支付 → 小程序 /// - APP支付 → 移动应用 - /// - 扫码/H5/付款码 → 公众号(微信扫码/H5/付款码通常使用公众号应用) + /// - 扫码/H5/付款码 → 公众号(默认, 实际兼容类型见 WxAppTypeEnum) private static final Map CAPABILITY_APP_TYPE_MAP = Map.of( PayCapabilityEnum.WECHAT_JSAPI.getCode(), OFFICIAL_ACCOUNT, PayCapabilityEnum.WECHAT_QR.getCode(), OFFICIAL_ACCOUNT, diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/entity/merchant/DyMchApp.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/entity/merchant/DyMchApp.java index 6caeb04f3..09030146b 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/entity/merchant/DyMchApp.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/entity/merchant/DyMchApp.java @@ -5,7 +5,6 @@ import cn.daxpay.open.payment.douyin.convert.merchant.DyMchAppConvert; import cn.daxpay.open.payment.douyin.result.merchant.DyMchAppResult; import cn.daxpay.open.platform.common.mybatisplus.function.ToResult; import cn.daxpay.open.platform.common.mybatisplus.handler.encrypt.DataEncryptTypeHandler; -import com.baomidou.mybatisplus.annotation.FieldStrategy; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @@ -27,7 +26,6 @@ public class DyMchApp extends MchBaseEntity implements ToResult /// 应用类型 /// @see cn.daxpay.open.payment.douyin.enums.DyAppTypeEnum - @TableField(updateStrategy = FieldStrategy.NEVER) private String appType; /// 抖音应用AppId diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/entity/platform/DyPlatformApp.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/entity/platform/DyPlatformApp.java index 66e188540..7eb0c6019 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/entity/platform/DyPlatformApp.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/entity/platform/DyPlatformApp.java @@ -5,7 +5,6 @@ import cn.daxpay.open.payment.douyin.result.platform.DyPlatformAppResult; import cn.daxpay.open.platform.common.mybatisplus.base.MpBaseEntity; import cn.daxpay.open.platform.common.mybatisplus.function.ToResult; import cn.daxpay.open.platform.common.mybatisplus.handler.encrypt.DataEncryptTypeHandler; -import com.baomidou.mybatisplus.annotation.FieldStrategy; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @@ -27,7 +26,6 @@ public class DyPlatformApp extends MpBaseEntity implements ToResult CAPABILITY_APP_TYPE_MAP = Map.of( - PayCapabilityEnum.DOUYIN_JSAPI.getCode(), MINI_PROGRAM.code, - PayCapabilityEnum.DOUYIN_APP.getCode(), MOBILE_APP.code, - PayCapabilityEnum.DOUYIN_QR.getCode(), WEB_APP.code, - PayCapabilityEnum.DOUYIN_H5.getCode(), WEB_APP.code); + /// - 扫码/H5 → 网站应用优先, 兼容小程序/移动应用(不依赖 openId) + private static final Map> CAPABILITY_APP_TYPE_MAP = Map.of( + PayCapabilityEnum.DOUYIN_JSAPI.getCode(), List.of(MINI_PROGRAM.code), + PayCapabilityEnum.DOUYIN_APP.getCode(), List.of(MOBILE_APP.code), + PayCapabilityEnum.DOUYIN_QR.getCode(), List.of(WEB_APP.code, MINI_PROGRAM.code, MOBILE_APP.code), + PayCapabilityEnum.DOUYIN_H5.getCode(), List.of(WEB_APP.code, MINI_PROGRAM.code, MOBILE_APP.code)); /// 翻译 key 前缀 @Override @@ -54,20 +55,26 @@ public enum DyAppTypeEnum implements I18nSupport { .orElse(null); } - /// 根据支付能力编码推导默认应用类型;未知能力返回 null - public static String resolveAppType(String capabilityCode) { + /// 根据支付能力推导全部兼容应用类型(有序, 首个为兜底优先级);未知能力返回空列表 + public static List resolveCompatibleAppTypes(String capabilityCode) { if (capabilityCode == null) { - return null; + return List.of(); } - return CAPABILITY_APP_TYPE_MAP.get(capabilityCode); + return CAPABILITY_APP_TYPE_MAP.getOrDefault(capabilityCode, List.of()); + } + + /// 根据支付能力编码推导默认(优先级最高)应用类型;未知能力返回 null + public static String resolveAppType(String capabilityCode) { + List types = resolveCompatibleAppTypes(capabilityCode); + return types.isEmpty() ? null : types.getFirst(); } /// 校验应用类型与支付能力是否兼容;未知能力不做强制约束 public static boolean isCompatible(String appType, String capability) { - String expected = resolveAppType(capability); - if (expected == null) { + List types = resolveCompatibleAppTypes(capability); + if (types.isEmpty()) { return true; } - return expected.equals(appType); + return types.contains(appType); } } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/service/DyAppResolveService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/service/DyAppResolveService.java index 5863c641d..b439c239f 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/service/DyAppResolveService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/service/DyAppResolveService.java @@ -10,6 +10,7 @@ import cn.daxpay.open.payment.douyin.enums.DyAppTypeEnum; import cn.daxpay.open.payment.douyin.facade.DouyinAppFacade; import cn.daxpay.open.payment.douyin.facade.DyAppView; import cn.daxpay.open.payment.douyin.facade.DyIsvAppPair; +import cn.daxpay.open.platform.common.i18n.util.I18nUtil; import cn.daxpay.open.platform.core.code.CommonErrorCode; import cn.daxpay.open.platform.core.exception.BizInfoException; import cn.daxpay.open.platform.core.exception.DataNotExistException; @@ -148,24 +149,18 @@ public class DyAppResolveService implements DouyinAppFacade { throw new DataNotExistException("error.payment.douyin.channelAppIdNotFound", channelAppId); } - /// appType 推导兜底:要求该类型平台应用唯一命中 + /// appType 推导兜底:按兼容类型优先级遍历, 首个唯一命中的平台应用返回 /// - /// - 唯一命中:返回该应用 - /// - 该类型存在多个平台应用:抛 notUnique,要求显式配置通道能力绑 - /// - 该类型无平台应用:返回 null,由调用方走最终报错 + /// - 某类型恰好 1 个:返回该应用 + /// - 某类型多个/无:跳过, 继续下一个兼容类型 + /// - 全部兼容类型均未唯一命中:返回 null, 由调用方走最终报错 private DyAppView resolvePlatformFallback(String capability) { if (StrUtil.isBlank(capability)) { return null; } - // appType 推导:要求该类型平台应用唯一命中,>1 拒绝猜测 - String appType = DyAppTypeEnum.resolveAppType(capability); - if (appType != null) { + // 遍历兼容 appType(按优先级), 首个唯一命中即返回; 某类型存在多个则跳过, 避免猜测 + for (String appType : DyAppTypeEnum.resolveCompatibleAppTypes(capability)) { List apps = dyPlatformAppManager.listByAppType(appType); - if (apps.size() > 1) { - // 存在多个同类型平台应用,请显式配置通道能力绑定以明确选择 - throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE, - "error.payment.douyin.appNotUnique", appType); - } if (apps.size() == 1) { return this.toPlatformView(apps.getFirst()); } @@ -203,17 +198,19 @@ public class DyAppResolveService implements DouyinAppFacade { } // 3. 平台档 web_app 唯一命中 List webApps = dyPlatformAppManager.listByAppType(DyAppTypeEnum.WEB_APP.getCode()); + // 网站应用类型本地化名称 + String webAppTypeLabel = I18nUtil.getEnumName(DyAppTypeEnum.WEB_APP); if (webApps.size() > 1) { // 存在多个同类型平台应用,请显式配置能力绑定以明确选择 throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE, - "error.payment.douyin.appNotUnique", DyAppTypeEnum.WEB_APP.getCode()); + "error.payment.douyin.appNotUnique", webAppTypeLabel); } if (webApps.size() == 1) { return this.toPlatformView(webApps.getFirst()); } // 抖音: 未配置 H5 验签所需的 web_app 类型应用 throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE, - "error.payment.douyin.appNotConfigured", DyAppTypeEnum.WEB_APP.getCode()); + "error.payment.douyin.appNotConfigured", webAppTypeLabel); } /// 按真实 douyinAppId 解析:商户档优先, 平台档兜底 diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/service/merchant/DyMchAppService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/service/merchant/DyMchAppService.java index 391b7b4e4..913d72559 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/service/merchant/DyMchAppService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/service/merchant/DyMchAppService.java @@ -19,6 +19,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Objects; /// # 商户抖音应用管理 /// @@ -75,10 +76,18 @@ public class DyMchAppService { String mchNo = entity.getMchNo(); this.assertDouyinAppIdUnique(mchNo, param.getDouyinAppId(), param.getId()); this.validateAppType(param.getAppType()); + // 记录原始应用类型, 用于判断是否需要联动清理支付能力绑定 + String oldAppType = entity.getAppType(); DyMchAppConvert.CONVERT.copy(param, entity); // 保持主表商户号不变 entity.setMchNo(mchNo); dyMchAppManager.updateById(entity); + // 应用类型变更: 旧 appType 下的能力绑定对新类型不再兼容, 清理后由用户重新配置 + if (!Objects.equals(oldAppType, param.getAppType())) { + dyChannelAppCapabilityManager.deleteByScopeAndRefId(AppScopeEnum.MERCHANT.getCode(), entity.getId()); + log.warn("商户抖音应用[{}] appType 从[{}]变更为[{}], 已清理通道能力绑定", + entity.getId(), oldAppType, param.getAppType()); + } } /// 删除应用(被通道能力绑定引用时拒删;级联删除授权认证配置) diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/service/platform/DyPlatformAppService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/service/platform/DyPlatformAppService.java index 4d55c55a4..3d81f941c 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/service/platform/DyPlatformAppService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/douyin/service/platform/DyPlatformAppService.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Objects; /// # 平台抖音应用管理 /// @@ -70,8 +71,17 @@ public class DyPlatformAppService { .orElseThrow(() -> new DataNotExistException("error.payment.douyin.appNotFound")); this.assertDouyinAppIdUnique(param.getDouyinAppId(), param.getId()); this.validateAppType(param.getAppType()); + // 记录原始应用类型, 用于判断是否需要联动清理支付能力绑定 + String oldAppType = entity.getAppType(); DyPlatformAppConvert.CONVERT.copy(param, entity); dyPlatformAppManager.updateById(entity); + // 应用类型变更: 旧 appType 下的能力绑定对新类型不再兼容, 清理后由用户重新配置 + if (!Objects.equals(oldAppType, param.getAppType())) { + dyPlatformAppCapabilityManager.deleteByDyPlatformAppId(entity.getId()); + dyChannelAppCapabilityManager.deleteByScopeAndRefId(AppScopeEnum.PLATFORM.getCode(), entity.getId()); + log.warn("平台抖音应用[{}] appType 从[{}]变更为[{}], 已清理平台默认能力绑定与通道能力绑定", + entity.getId(), oldAppType, param.getAppType()); + } } /// 删除应用(被能力绑定引用时拒删;级联删除授权认证配置) diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/entity/merchant/WxMchApp.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/entity/merchant/WxMchApp.java index 61e0b17b7..fee648d59 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/entity/merchant/WxMchApp.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/entity/merchant/WxMchApp.java @@ -5,7 +5,6 @@ import cn.daxpay.open.payment.wx.convert.merchant.WxMchAppConvert; import cn.daxpay.open.payment.wx.result.merchant.WxMchAppResult; import cn.daxpay.open.platform.common.mybatisplus.function.ToResult; import cn.daxpay.open.platform.common.mybatisplus.handler.encrypt.DataEncryptTypeHandler; -import com.baomidou.mybatisplus.annotation.FieldStrategy; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @@ -27,7 +26,6 @@ public class WxMchApp extends MchBaseEntity implements ToResult /// 应用类型 /// @see cn.daxpay.open.payment.wx.enums.WxAppTypeEnum - @TableField(updateStrategy = FieldStrategy.NEVER) private String appType; /// 微信应用AppId diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/entity/platform/WxPlatformApp.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/entity/platform/WxPlatformApp.java index 8a49e0e4b..82d1b5fb0 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/entity/platform/WxPlatformApp.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/entity/platform/WxPlatformApp.java @@ -5,7 +5,6 @@ import cn.daxpay.open.payment.wx.result.platform.WxPlatformAppResult; import cn.daxpay.open.platform.common.mybatisplus.base.MpBaseEntity; import cn.daxpay.open.platform.common.mybatisplus.function.ToResult; import cn.daxpay.open.platform.common.mybatisplus.handler.encrypt.DataEncryptTypeHandler; -import com.baomidou.mybatisplus.annotation.FieldStrategy; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @@ -27,7 +26,6 @@ public class WxPlatformApp extends MpBaseEntity implements ToResult CAPABILITY_APP_TYPE_MAP = Map.of( - PayCapabilityEnum.WECHAT_JSAPI.getCode(), OFFICIAL_ACCOUNT.code, - PayCapabilityEnum.WECHAT_QR.getCode(), OFFICIAL_ACCOUNT.code, - PayCapabilityEnum.WECHAT_H5.getCode(), OFFICIAL_ACCOUNT.code, - PayCapabilityEnum.WECHAT_BARCODE.getCode(), OFFICIAL_ACCOUNT.code, - PayCapabilityEnum.WECHAT_CASHIER.getCode(), OFFICIAL_ACCOUNT.code, - PayCapabilityEnum.WECHAT_MINI.getCode(), MINI_PROGRAM.code, - PayCapabilityEnum.WECHAT_APP.getCode(), MOBILE_APP.code); + private static final Map> CAPABILITY_APP_TYPE_MAP = Map.of( + PayCapabilityEnum.WECHAT_JSAPI.getCode(), List.of(OFFICIAL_ACCOUNT.code), + PayCapabilityEnum.WECHAT_QR.getCode(), List.of(OFFICIAL_ACCOUNT.code, MINI_PROGRAM.code, MOBILE_APP.code), + PayCapabilityEnum.WECHAT_H5.getCode(), List.of(OFFICIAL_ACCOUNT.code, MOBILE_APP.code), + PayCapabilityEnum.WECHAT_BARCODE.getCode(), List.of(OFFICIAL_ACCOUNT.code, MINI_PROGRAM.code, MOBILE_APP.code), + PayCapabilityEnum.WECHAT_CASHIER.getCode(), List.of(OFFICIAL_ACCOUNT.code), + PayCapabilityEnum.WECHAT_MINI.getCode(), List.of(MINI_PROGRAM.code), + PayCapabilityEnum.WECHAT_APP.getCode(), List.of(MOBILE_APP.code)); /// 翻译 key 前缀 @Override @@ -57,20 +60,26 @@ public enum WxAppTypeEnum implements I18nSupport { .orElse(null); } - /// 根据支付能力编码推导默认应用类型;未知能力返回 null - public static String resolveAppType(String capabilityCode) { + /// 根据支付能力推导全部兼容应用类型(有序, 首个为兜底优先级);未知能力返回空列表 + public static List resolveCompatibleAppTypes(String capabilityCode) { if (capabilityCode == null) { - return null; + return List.of(); } - return CAPABILITY_APP_TYPE_MAP.get(capabilityCode); + return CAPABILITY_APP_TYPE_MAP.getOrDefault(capabilityCode, List.of()); + } + + /// 根据支付能力编码推导默认(优先级最高)应用类型;未知能力返回 null + public static String resolveAppType(String capabilityCode) { + List types = resolveCompatibleAppTypes(capabilityCode); + return types.isEmpty() ? null : types.getFirst(); } /// 校验应用类型与支付能力是否兼容;未知能力不做强制约束 public static boolean isCompatible(String appType, String capability) { - String expected = resolveAppType(capability); - if (expected == null) { + List types = resolveCompatibleAppTypes(capability); + if (types.isEmpty()) { return true; } - return expected.equals(appType); + return types.contains(appType); } } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/service/WxAppResolveService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/service/WxAppResolveService.java index 3b74107ec..88745c331 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/service/WxAppResolveService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/service/WxAppResolveService.java @@ -176,24 +176,18 @@ public class WxAppResolveService implements WxAppFacade { throw new DataNotExistException("error.payment.wx.channelAppIdNotFound", channelAppId); } - /// appType 推导兜底:要求该类型平台应用唯一命中 + /// appType 推导兜底:按兼容类型优先级遍历, 首个唯一命中的平台应用返回 /// - /// - 唯一命中:返回该应用 - /// - 该类型存在多个平台应用:抛 notUnique,要求显式配置通道能力绑 - /// - 该类型无平台应用:返回 null,由调用方走最终报错 + /// - 某类型恰好 1 个:返回该应用 + /// - 某类型多个/无:跳过, 继续下一个兼容类型 + /// - 全部兼容类型均未唯一命中:返回 null, 由调用方走最终报错 private WxAppView resolvePlatformFallback(String capability) { if (StrUtil.isBlank(capability)) { return null; } - // appType 推导:要求该类型平台应用唯一命中,>1 拒绝猜测 - String appType = WxAppTypeEnum.resolveAppType(capability); - if (appType != null) { + // 遍历兼容 appType(按优先级), 首个唯一命中即返回; 某类型存在多个则跳过, 避免猜测 + for (String appType : WxAppTypeEnum.resolveCompatibleAppTypes(capability)) { List apps = wxPlatformAppManager.listByAppType(appType); - if (apps.size() > 1) { - // 存在多个同类型平台应用,请显式配置通道能力绑定以明确选择 - throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE, - "error.payment.wx.appNotUnique", appType); - } if (apps.size() == 1) { return this.toPlatformView(apps.getFirst()); } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/service/merchant/WxMchAppService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/service/merchant/WxMchAppService.java index 1d538d195..cb8a585d4 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/service/merchant/WxMchAppService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/service/merchant/WxMchAppService.java @@ -19,6 +19,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Objects; /// # 商户微信应用管理 /// @@ -75,10 +76,18 @@ public class WxMchAppService { String mchNo = entity.getMchNo(); this.assertWxAppIdUnique(mchNo, param.getWxAppId(), param.getId()); this.validateAppType(param.getAppType()); + // 记录原始应用类型, 用于判断是否需要联动清理支付能力绑定 + String oldAppType = entity.getAppType(); WxMchAppConvert.CONVERT.copy(param, entity); // 保持主表商户号不变 entity.setMchNo(mchNo); wxMchAppManager.updateById(entity); + // 应用类型变更: 旧 appType 下的能力绑定对新类型不再兼容, 清理后由用户重新配置 + if (!Objects.equals(oldAppType, param.getAppType())) { + wxChannelAppCapabilityManager.deleteByScopeAndRefId(AppScopeEnum.MERCHANT.getCode(), entity.getId()); + log.warn("商户微信应用[{}] appType 从[{}]变更为[{}], 已清理通道能力绑定", + entity.getId(), oldAppType, param.getAppType()); + } } /// 删除应用(被通道能力绑定引用时拒删;级联删除授权认证配置) diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/service/platform/WxPlatformAppService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/service/platform/WxPlatformAppService.java index b8c4a3d0a..5fcf9d747 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/service/platform/WxPlatformAppService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/wx/service/platform/WxPlatformAppService.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Objects; /// # 平台微信应用管理 /// @@ -70,8 +71,17 @@ public class WxPlatformAppService { .orElseThrow(() -> new DataNotExistException("error.payment.wx.appNotFound")); this.assertWxAppIdUnique(param.getWxAppId(), param.getId()); this.validateAppType(param.getAppType()); + // 记录原始应用类型, 用于判断是否需要联动清理支付能力绑定 + String oldAppType = entity.getAppType(); WxPlatformAppConvert.CONVERT.copy(param, entity); wxPlatformAppManager.updateById(entity); + // 应用类型变更: 旧 appType 下的能力绑定对新类型不再兼容, 清理后由用户重新配置 + if (!Objects.equals(oldAppType, param.getAppType())) { + wxPlatformAppCapabilityManager.deleteByWxPlatformAppId(entity.getId()); + wxChannelAppCapabilityManager.deleteByScopeAndRefId(AppScopeEnum.PLATFORM.getCode(), entity.getId()); + log.warn("平台微信应用[{}] appType 从[{}]变更为[{}], 已清理平台默认能力绑定与通道能力绑定", + entity.getId(), oldAppType, param.getAppType()); + } } /// 删除应用(被能力绑定引用时拒删;级联删除授权认证配置) diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/wx_app_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/wx_app_type.json new file mode 100644 index 000000000..acd3f99dc --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/wx_app_type.json @@ -0,0 +1,5 @@ +{ + "official_account": "Akun Resmi", + "mini_program": "Program Kecil", + "mobile_app": "Aplikasi Seluler" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/wx_app_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/wx_app_type.json new file mode 100644 index 000000000..525ecba76 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/wx_app_type.json @@ -0,0 +1,5 @@ +{ + "official_account": "Akaun Rasmi", + "mini_program": "Program Mini", + "mobile_app": "Apl Mudah Alih" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/wx_app_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/wx_app_type.json new file mode 100644 index 000000000..224b7bea1 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/wx_app_type.json @@ -0,0 +1,5 @@ +{ + "official_account": "บัญชีอย่างเป็นทางการ", + "mini_program": "มินิโปรแกรม", + "mobile_app": "แอพมือถือ" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/wx_app_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/wx_app_type.json new file mode 100644 index 000000000..064445c6f --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/wx_app_type.json @@ -0,0 +1,5 @@ +{ + "official_account": "Tài khoản chính thức", + "mini_program": "Chương trình nhỏ", + "mobile_app": "Ứng dụng di động" +} diff --git a/daxpay-start/src/main/resources/application-dev.yml b/daxpay-start/src/main/resources/application-dev.yml index f70f744de..72a171629 100644 --- a/daxpay-start/src/main/resources/application-dev.yml +++ b/daxpay-start/src/main/resources/application-dev.yml @@ -167,21 +167,17 @@ daxpay: # 关闭后不可切换到沙箱; 若启动时已有产品处于沙箱, 将自动重置为生产环境 sandbox-enabled: true # 通道适配子应用配置 - # 按支付通道拆分的独立部署子服务, 主服务通过 HTTP 调用对应子服务完成通道对接。 - # 拆分目的: 通道 SDK 依赖隔离(避免冲突) + 独立升级 + 弹性伸缩 - # 子应用编号(one/two/three)固定且不可改名, 与路由策略中的通道分配一一对应。 channel: # 子应用1: 支付宝/微信支付/银联商务/(已启用, 本机启动的 dax-pay-channel-one 服务) one: base-url: http://127.0.0.1:20100 - # 通道传输 AES 密钥(强制常开,恰好 32 字符;仅本地占位,勿用于生产) - # 须与子应用一致: dax-pay-channel-one application.yml / dax-pay-channel-one-go config.yaml 的 channel.transport-encrypt.key + # 通道传输 AES 密钥(强制常开) transport-encrypt: key: 0123456789abcdef0123456789abcdef # 子应用2: 其他支付通道 two: base-url: http://127.0.0.1:20200 - # 须与子应用一致: dax-pay-channel-two application.yml 的 channel.transport-encrypt.key + # 须与子应用一致 transport-encrypt: key: fedcba9876543210fedcba9876543210 # 子应用3: 其他通道(未来扩展, 暂未启用)