refactor(payment): 通道应用类型解析改为多兼容类型遍历

- 微信/抖音/支付宝应用类型映射由单值改为有序兼容列表, 多个同类型应用时跳过并尝试下一个兼容类型
- 抖音 H5 验签 web_app 报错参数改用本地化名称(网站应用), 不再显示裸编码
- 补齐 wx_app_type 东盟四语(id-ID/vi-VN/th-TH/ms-MY)
- 抖音直连应用体系(douyin_direct_app*)下线, 迁移至 dy_mch_app/dy_channel_app_capability 统一模式
- 删除已废弃的抖音应用管理菜单, 清理通道子应用配置注释
This commit is contained in:
daxpay
2026-07-30 12:32:41 +08:00
parent 9f985e9ed0
commit 4ced54dfab
25 changed files with 180 additions and 265 deletions

View File

@@ -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');

View File

@@ -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
-- ----------------------------

View File

@@ -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;

View File

@@ -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";

View File

@@ -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<String, String> 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<String, List<String>> 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<String> 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<String> 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<String> types = resolveCompatibleAppTypes(capability);
if (types.isEmpty()) {
return true;
}
return expected.equals(appType);
return types.contains(appType);
}
}

View File

@@ -112,10 +112,10 @@ public class AlipayDirectAppCapabilityService {
/// 支付/回调解析应用:显式配置 > appType 自动推导(须已装载 mchNo租户内
///
/// appType 推导要求该通道商户下该类型应用唯一命中:
/// - 唯一命中:返回该应用
/// - 类型存在多个应用:抛 appNotUnique要求显式配置能力绑
/// - 该类型无应用:返回 empty由调用方走最终报错
/// appType 推导:遍历兼容类型(按优先级), 该通道商户下首个唯一命中即返回
/// - 某类型恰好 1 个:返回该应用
/// - 类型多个/无:跳过, 继续下一个兼容类型
/// - 全部兼容类型均未唯一命中:返回 empty, 由调用方走最终报错
///
/// 认证无上下文请用 [#resolveAppNotTenant]。
public Optional<AlipayDirectApp> 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<AlipayDirectApp> 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());
}

View File

@@ -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());
}
}
/// 删除应用(级联删除密钥配置、授权认证配置与能力关联)

View File

@@ -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<String, String> CAPABILITY_APP_TYPE_MAP = Map.of(
PayCapabilityEnum.WECHAT_JSAPI.getCode(), OFFICIAL_ACCOUNT,
PayCapabilityEnum.WECHAT_QR.getCode(), OFFICIAL_ACCOUNT,

View File

@@ -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<DyMchAppResult>
/// 应用类型
/// @see cn.daxpay.open.payment.douyin.enums.DyAppTypeEnum
@TableField(updateStrategy = FieldStrategy.NEVER)
private String appType;
/// 抖音应用AppId

View File

@@ -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<DyPlatformAp
/// 应用类型
/// @see cn.daxpay.open.payment.douyin.enums.DyAppTypeEnum
@TableField(updateStrategy = FieldStrategy.NEVER)
private String appType;
/// 抖音应用AppId

View File

@@ -6,6 +6,7 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -26,16 +27,16 @@ public enum DyAppTypeEnum implements I18nSupport {
private final String code;
/// 支付能力 → 应用类型的默认推导映射
/// 支付能力 → 兼容应用类型集合(有序, 首个为兜底推导优先级)
///
/// - JSAPI → 小程序
/// - JSAPI → 小程序(场景强绑 openId)
/// - APP支付 → 移动应用
/// - 扫码/H5 → 网站应用
private static final Map<String, String> 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<String, List<String>> 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<String> 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<String> 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<String> types = resolveCompatibleAppTypes(capability);
if (types.isEmpty()) {
return true;
}
return expected.equals(appType);
return types.contains(appType);
}
}

View File

@@ -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<DyPlatformApp> 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<DyPlatformApp> 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 解析:商户档优先, 平台档兜底

View File

@@ -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());
}
}
/// 删除应用(被通道能力绑定引用时拒删;级联删除授权认证配置)

View File

@@ -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());
}
}
/// 删除应用(被能力绑定引用时拒删;级联删除授权认证配置)

View File

@@ -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<WxMchAppResult>
/// 应用类型
/// @see cn.daxpay.open.payment.wx.enums.WxAppTypeEnum
@TableField(updateStrategy = FieldStrategy.NEVER)
private String appType;
/// 微信应用AppId

View File

@@ -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<WxPlatformAp
/// 应用类型
/// @see cn.daxpay.open.payment.wx.enums.WxAppTypeEnum
@TableField(updateStrategy = FieldStrategy.NEVER)
private String appType;
/// 微信应用AppId

View File

@@ -6,6 +6,7 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -26,19 +27,21 @@ public enum WxAppTypeEnum implements I18nSupport {
private final String code;
/// 支付能力 → 应用类型的默认推导映射
/// 支付能力 → 兼容应用类型集合(有序, 首个为兜底推导优先级)
///
/// - JSAPI/扫码/H5/付款码/收银台 → 公众号
/// - JSAPI/收银台 → 公众号(场景强绑 openId)
/// - 扫码/付款码 → 公众号优先, 兼容小程序/移动应用(不依赖 openId)
/// - H5 → 公众号优先, 兼容移动应用(微信 H5 不支持小程序 appid)
/// - 小程序支付 → 小程序
/// - APP支付 → 移动应用
private static final Map<String, String> 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<String, List<String>> 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<String> 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<String> 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<String> types = resolveCompatibleAppTypes(capability);
if (types.isEmpty()) {
return true;
}
return expected.equals(appType);
return types.contains(appType);
}
}

View File

@@ -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<WxPlatformApp> 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());
}

View File

@@ -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());
}
}
/// 删除应用(被通道能力绑定引用时拒删;级联删除授权认证配置)

View File

@@ -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());
}
}
/// 删除应用(被能力绑定引用时拒删;级联删除授权认证配置)

View File

@@ -0,0 +1,5 @@
{
"official_account": "Akun Resmi",
"mini_program": "Program Kecil",
"mobile_app": "Aplikasi Seluler"
}

View File

@@ -0,0 +1,5 @@
{
"official_account": "Akaun Rasmi",
"mini_program": "Program Mini",
"mobile_app": "Apl Mudah Alih"
}

View File

@@ -0,0 +1,5 @@
{
"official_account": "บัญชีอย่างเป็นทางการ",
"mini_program": "มินิโปรแกรม",
"mobile_app": "แอพมือถือ"
}

View File

@@ -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"
}

View File

@@ -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: 其他通道(未来扩展, 暂未启用)