feat(wechat): 微信服务商子商户应用功能 + 授权配置基类修复

- 新增微信服务商通道商户应用(WechatIsvMchApp)子商户自己的微信应用管理,
  含应用/能力绑定/授权配置三表及配套 controller/service/dao/convert/param/result
- 修复 WechatIsvAppAuthConfig 基类错误(MchBaseEntity→MpBaseEntity):
  服务商应用为平台级全局配置, 不挂商户维度, 移除冗余的 mchNo/channelMchNo 字段,
  修复 channel_mch_no / mch_no 列不存在的运行时异常
- 支付宝支付异步通知地址改用平台端点配置(backendBaseUrl)生成
- 微信直连凭证组装从通道商户绑定表取真实 wxMchId(channelMchNo 为系统雪花号)
- 支付方式枚举与 i18n 文案统一精简(去掉冗余'支付'后缀, 统一大小写)
- 校验消息 key 修正(wechatIsvAppId / wechatIsvMchAppId)
This commit is contained in:
DaxPay Dev
2026-07-05 11:00:11 +08:00
parent 4083e47109
commit 3ce790ab99
38 changed files with 1382 additions and 67 deletions

View File

@@ -12,7 +12,7 @@
Target Server Version : 160014 (160014)
File Encoding : 65001
Date: 03/07/2026 18:32:43
Date: 05/07/2026 10:35:00
*/
@@ -155,6 +155,7 @@ INSERT INTO "public"."iam_perm_menu" VALUES (4040116, 401, 'payment:platform:pro
INSERT INTO "public"."iam_perm_menu" VALUES (309, 308, 'system:notify:notice', 'admin', 'SystemNotify', '通知公告', 'Notification', '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', '通知中心', 'Notification Center', '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 (603, 6, 'payment:refund', 'admin', 'RefundOrderList', '退款订单', 'Refund Orders', 'menu.trade.refundOrder', 'lucide:rotate-ccw', 'f', 'f', '/payment/order/RefundOrderList', '/trade/refund-order', NULL, 3, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-03 16:00:00+00', '2026-07-03 16:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (4040119, 40401, 'channel:wechat:app', 'admin', 'WechatIsvMchAppManage', '微信服务商通道商户应用', 'WeChat ISV Channel Merchant App', 'menu.payment.merchant.channelMerchant.wechatIsvMchApp', NULL, 't', 'f', '/payment/channel/wechat/manage/mch/isv-app/WechatIsvMchAppManage', '/payment/merchant/channel-merchant/wechat-isv-mch-app-manage', NULL, 14, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-05 01:49:32.557374+00', '2026-07-05 01:49:32.557374+00');
-- ----------------------------
-- Primary Key structure for table iam_perm_menu

View File

@@ -3,3 +3,44 @@ INSERT INTO "public"."iam_perm_menu" VALUES (4040118, 40401, 'merchant:notify_co
-- 退款订单: 菜单(权限码 payment:refund)
INSERT INTO "public"."iam_perm_menu" VALUES (603, 6, 'payment:refund', 'admin', 'RefundOrderList', '退款订单', 'Refund Orders', 'menu.trade.refundOrder', 'lucide:rotate-ccw', 'f', 'f', '/payment/order/RefundOrderList', '/trade/refund-order', NULL, 3, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-03 16:00:00+00', '2026-07-03 16:00:00+00');
-- 微信服务商通道商户应用(子商户应用): 菜单(权限码 channel:wechat:app)
INSERT INTO "public"."iam_perm_menu" VALUES (4040119, 40401, 'channel:wechat:app', 'admin', 'WechatIsvMchAppManage', '微信服务商通道商户应用', 'WeChat ISV Channel Merchant App', 'menu.payment.merchant.channelMerchant.wechatIsvMchApp', NULL, 't', 'f', '/payment/channel/wechat/manage/mch/isv-app/WechatIsvMchAppManage', '/payment/merchant/channel-merchant/wechat-isv-mch-app-manage', NULL, 14, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-05 00:00:00+00', '2026-07-05 00:00:00+00');
-- ============================================================
-- 菜单标题与 i18n_key 统一重命名
-- ============================================================
-- 支付产品详情 → 产品配置详情
UPDATE "public"."iam_perm_menu" SET title_cn='产品配置详情', title_en='Product Config Detail',
i18n_key='menu.payment.config.detail' WHERE id=40501;
-- 支付宝服务商应用: i18n_key 归入 config.*
UPDATE "public"."iam_perm_menu" SET i18n_key='menu.payment.config.alipayApp' WHERE id=40502;
-- 微信服务商应用: i18n_key 归入 config.*
UPDATE "public"."iam_perm_menu" SET i18n_key='menu.payment.config.wechatApp' WHERE id=40503;
-- 商户信息(列表入口) → 商户列表
UPDATE "public"."iam_perm_menu" SET title_cn='商户列表', title_en='Merchant List',
i18n_key='menu.payment.merchant.list' WHERE id=40401;
-- 商户管理(工作台) → 商户工作台
UPDATE "public"."iam_perm_menu" SET title_cn='商户工作台', title_en='Merchant Workbench',
i18n_key='menu.payment.merchant.workbench' WHERE id=4040101;
-- 对接配置: i18n_key 去掉中间 manage 层
UPDATE "public"."iam_perm_menu" SET i18n_key='menu.payment.merchant.credential' WHERE id=4040102;
-- 商户信息(详情) → 商户资料
UPDATE "public"."iam_perm_menu" SET title_cn='商户资料', title_en='Merchant Profile',
i18n_key='menu.payment.merchant.profile' WHERE id=4040103;
-- 通道路由: i18n_key 从 app.* 移出(实为商户级配置)
UPDATE "public"."iam_perm_menu" SET i18n_key='menu.payment.merchant.payRoute' WHERE id=4040111;
-- 通道商户详情 → 通道商户管理(静态通用名)
UPDATE "public"."iam_perm_menu" SET title_cn='通道商户管理', title_en='Channel Merchant Management'
WHERE id=4040112;
-- 微信通道商户应用 → 微信应用
UPDATE "public"."iam_perm_menu" SET title_cn='微信应用', title_en='WeChat App' WHERE id=4040113;
-- 支付宝通道商户应用 → 支付宝应用
UPDATE "public"."iam_perm_menu" SET title_cn='支付宝应用', title_en='Alipay App' WHERE id=4040114;
-- 抖音通道商户应用 → 抖音应用
UPDATE "public"."iam_perm_menu" SET title_cn='抖音应用', title_en='Douyin App' WHERE id=4040115;
-- 微信服务商通道商户应用 → 微信服务商子商户应用
UPDATE "public"."iam_perm_menu" SET title_cn='微信服务商子商户应用', title_en='WeChat ISV Sub-merchant App',
i18n_key='menu.payment.merchant.channelMerchant.wechatIsvApp' WHERE id=4040119;

View File

@@ -29,3 +29,101 @@ COMMENT ON COLUMN "public"."iam_user_dashboard_preference"."deleted" IS '逻辑
CREATE UNIQUE INDEX IF NOT EXISTS "uk_user_das_pref_user_client"
ON "public"."iam_user_dashboard_preference" ("user_id", "client_code")
WHERE "deleted" = false;
-- ============================================================
-- 微信服务商子商户应用 / 能力绑定 / 授权配置
-- ============================================================
-- 微信服务商通道商户应用(子商户自己的微信应用, channelMchNo 维度)
CREATE TABLE IF NOT EXISTS "public"."wechat_isv_mch_app" (
"id" bigint NOT NULL PRIMARY KEY,
"mch_no" varchar(32) NOT NULL,
"channel_mch_no" varchar(64) NOT NULL,
"app_name" varchar(128) NOT NULL,
"app_type" varchar(32) NOT NULL,
"wx_app_id" varchar(64) NOT NULL,
"creator" bigint,
"create_time" timestamptz(6),
"last_modifier" bigint,
"last_modified_time" timestamptz(6),
"version" integer NOT NULL DEFAULT 0,
"deleted" boolean NOT NULL DEFAULT false
);
COMMENT ON TABLE "public"."wechat_isv_mch_app" IS '微信服务商通道商户应用(子商户自己的微信应用)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app"."id" IS '主键';
COMMENT ON COLUMN "public"."wechat_isv_mch_app"."mch_no" IS '商户号';
COMMENT ON COLUMN "public"."wechat_isv_mch_app"."channel_mch_no" IS '通道商户号(服务商特约商户)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app"."app_name" IS '应用名称';
COMMENT ON COLUMN "public"."wechat_isv_mch_app"."app_type" IS '应用类型(official_account/mini_program/mobile_app)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app"."wx_app_id" IS '微信应用AppId(对应微信支付sub_appid)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app"."creator" IS '创建人ID';
COMMENT ON COLUMN "public"."wechat_isv_mch_app"."create_time" IS '创建时间(UTC)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app"."last_modifier" IS '最后修改人ID';
COMMENT ON COLUMN "public"."wechat_isv_mch_app"."last_modified_time" IS '最后修改时间(UTC)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app"."version" IS '乐观锁版本号';
COMMENT ON COLUMN "public"."wechat_isv_mch_app"."deleted" IS '逻辑删除标志';
CREATE UNIQUE INDEX IF NOT EXISTS "uk_wechat_isv_mch_app_channel_wxappid"
ON "public"."wechat_isv_mch_app" ("channel_mch_no", "wx_app_id")
WHERE "deleted" = false;
-- 微信服务商通道商户应用支付能力关联(channelMchNo 维度, 仅存子商户显式选自己应用的记录)
CREATE TABLE IF NOT EXISTS "public"."wechat_isv_mch_app_capability" (
"id" bigint NOT NULL PRIMARY KEY,
"mch_no" varchar(32) NOT NULL,
"channel_mch_no" varchar(64) NOT NULL,
"capability" varchar(64) NOT NULL,
"wechat_isv_mch_app_id" bigint NOT NULL,
"creator" bigint,
"create_time" timestamptz(6),
"last_modifier" bigint,
"last_modified_time" timestamptz(6),
"version" integer NOT NULL DEFAULT 0,
"deleted" boolean NOT NULL DEFAULT false
);
COMMENT ON TABLE "public"."wechat_isv_mch_app_capability" IS '微信服务商通道商户应用支付能力关联';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_capability"."id" IS '主键';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_capability"."mch_no" IS '商户号';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_capability"."channel_mch_no" IS '通道商户号(服务商特约商户)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_capability"."capability" IS '支付能力编码';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_capability"."wechat_isv_mch_app_id" IS '关联服务商通道商户应用ID(指向wechat_isv_mch_app.id)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_capability"."creator" IS '创建人ID';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_capability"."create_time" IS '创建时间(UTC)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_capability"."last_modifier" IS '最后修改人ID';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_capability"."last_modified_time" IS '最后修改时间(UTC)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_capability"."version" IS '乐观锁版本号';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_capability"."deleted" IS '逻辑删除标志';
CREATE UNIQUE INDEX IF NOT EXISTS "uk_wechat_isv_mch_app_cap_channel_cap"
ON "public"."wechat_isv_mch_app_capability" ("channel_mch_no", "capability")
WHERE "deleted" = false;
-- 微信服务商通道商户应用授权配置(appSecret等, 一对一)
CREATE TABLE IF NOT EXISTS "public"."wechat_isv_mch_app_auth_config" (
"id" bigint NOT NULL PRIMARY KEY,
"mch_no" varchar(32) NOT NULL,
"channel_mch_no" varchar(64) NOT NULL,
"wechat_isv_mch_app_id" bigint NOT NULL,
"app_secret" varchar(512),
"auth_callback_url" varchar(512),
"creator" bigint,
"create_time" timestamptz(6),
"last_modifier" bigint,
"last_modified_time" timestamptz(6),
"version" integer NOT NULL DEFAULT 0,
"deleted" boolean NOT NULL DEFAULT false
);
COMMENT ON TABLE "public"."wechat_isv_mch_app_auth_config" IS '微信服务商通道商户应用授权配置';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_auth_config"."id" IS '主键';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_auth_config"."mch_no" IS '商户号';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_auth_config"."channel_mch_no" IS '通道商户号(服务商特约商户)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_auth_config"."wechat_isv_mch_app_id" IS '关联服务商通道商户应用ID';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_auth_config"."app_secret" IS '应用密钥(加密存储)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_auth_config"."auth_callback_url" IS '授权回调地址(仅公众号)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_auth_config"."creator" IS '创建人ID';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_auth_config"."create_time" IS '创建时间(UTC)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_auth_config"."last_modifier" IS '最后修改人ID';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_auth_config"."last_modified_time" IS '最后修改时间(UTC)';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_auth_config"."version" IS '乐观锁版本号';
COMMENT ON COLUMN "public"."wechat_isv_mch_app_auth_config"."deleted" IS '逻辑删除标志';
CREATE UNIQUE INDEX IF NOT EXISTS "uk_wechat_isv_mch_app_auth_channel_app"
ON "public"."wechat_isv_mch_app_auth_config" ("channel_mch_no", "wechat_isv_mch_app_id")
WHERE "deleted" = false;

View File

@@ -15,6 +15,7 @@ import cn.daxpay.open.platform.core.code.DaxPayErrorCode;
import cn.daxpay.open.platform.core.enums.pay.channel.PayMethodEnum;
import cn.daxpay.open.platform.core.enums.unipay.PayBodyTypeEnum;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import cn.daxpay.open.platform.system.service.config.PlatformUrlConfigService;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -30,6 +31,7 @@ import org.springframework.stereotype.Service;
public class AlipayPayService {
private final AlipayChannelClient alipayChannelClient;
private final PlatformUrlConfigService platformUrlConfigService;
/// 执行支付宝支付
///
@@ -50,6 +52,8 @@ public class AlipayPayService {
// 付款码(BARCODE) / 买家标识(JSAPI) 通道专属参数透传
req.setAuthCode(payParam.getAuthCode());
req.setOpenId(payParam.getOpenId());
// 通道通知地址: 始终使用平台生成的回调地址(支付宝→平台), 不使用 payParam.notifyUrl(语义为平台→商户)
req.setNotifyUrl(this.buildNotifyUrl(order));
req.setCredential(credential);
// 调用子应用
@@ -61,6 +65,21 @@ public class AlipayPayService {
return toPayResult(result.getData());
}
/// 生成支付宝支付异步通知地址(支付宝→平台)
///
/// 沿用商业版旧版路径约定: `{backendBaseUrl}/unipay/callback/{mchNo}/{appId}/alipay`
/// backendBaseUrl 来自平台端点配置 [PlatformUrlConfig], 与社交登录回调地址同源
/// 注: 支付宝支付/退款共用同一回调端点(旧版设计), 无 /pay 后缀, 与微信路径不同
private String buildNotifyUrl(PayTrade order) {
String base = platformUrlConfigService.getUrlConfig().getBackendBaseUrl();
if (StrUtil.isBlank(base)) {
// backendBaseUrl 未配置时抛清晰异常, 避免 null 透传到支付宝导致无异步回调
throw new IllegalStateException("平台后端访问地址(backendBaseUrl)未配置, 无法生成支付宝回调地址");
}
return StrUtil.format("{}/unipay/callback/{}/{}/alipay",
base, order.getMchNo(), order.getAppId());
}
/// 平台支付方式([PayMethodEnum] code) -> 支付宝通道支付方式
private static AlipayPayMethod mapMethod(String methodCode) {
PayMethodEnum m = PayMethodEnum.findByCode(methodCode);

View File

@@ -97,7 +97,7 @@ public class WechatIsvAppController {
@Operation(summary = "查询应用授权认证配置")
@GetMapping("/find-auth-config-by-app-id")
public Result<WechatIsvAppAuthConfigResult> findAuthConfigByAppId(
@NotNull(message = "{validation.field.id.notNull}") Long wechatIsvAppId) {
@NotNull(message = "{validation.field.wechatIsvAppId.notNull}") Long wechatIsvAppId) {
var config = wechatIsvAppAuthConfigService.findByWechatIsvAppId(wechatIsvAppId);
return Res.ok(WechatIsvAppAuthConfigConvert.CONVERT.toResult(config));
}

View File

@@ -0,0 +1,60 @@
package cn.daxpay.open.channel.wechat.controller.isv;
import cn.daxpay.open.channel.wechat.param.isv.WechatIsvMchAppCapabilityBatchParam;
import cn.daxpay.open.channel.wechat.result.WechatCapabilityOption;
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvMchAppCapabilityResult;
import cn.daxpay.open.channel.wechat.service.isv.WechatIsvMchAppCapabilityService;
import cn.daxpay.open.platform.core.annotation.PermCode;
import cn.daxpay.open.platform.core.rest.Res;
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.NotBlank;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/// # 微信服务商通道商户应用支付能力关联管理
///
/// 提供通道商户(特约商户)维度下「支付能力 → 子商户应用」绑定关系的查询、批量保存及能力候选查询。
///
@PermCode(menuCode = "channel:wechat:app")
@Validated
@Tag(name = "微信服务商通道商户应用支付能力关联管理")
@RestController
@RequestMapping("/admin/wechat/isv-mch-app/capability")
@RequiredArgsConstructor
public class WechatIsvMchAppCapabilityController {
private final WechatIsvMchAppCapabilityService wechatIsvMchAppCapabilityService;
@PermCode(code = "view", nameCn = "通道商户查看", nameEn = "Channel Merchant View")
@Operation(summary = "查询通道商户的能力应用关联列表")
@GetMapping("/list-by-channel-mch-no")
public Result<List<WechatIsvMchAppCapabilityResult>> listByChannelMchNo(
@NotBlank(message = "{validation.field.mchNo.notBlank}") String mchNo,
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) {
return Res.ok(wechatIsvMchAppCapabilityService.listByChannelMchNo(mchNo, channelMchNo));
}
@PermCode(code = "manage", nameCn = "通道商户管理", nameEn = "Channel Merchant Manage")
@Operation(summary = "全量保存能力应用关联")
@PostMapping("/save-batch")
public Result<Void> saveBatch(@RequestBody @Validated WechatIsvMchAppCapabilityBatchParam param) {
wechatIsvMchAppCapabilityService.saveBatch(param);
return Res.ok();
}
@PermCode(code = "view", nameCn = "通道商户查看", nameEn = "Channel Merchant View")
@Operation(summary = "查询微信服务商支持的支付能力候选")
@GetMapping("/list-supported-capabilities")
public Result<List<WechatCapabilityOption>> listSupportedCapabilities() {
return Res.ok(wechatIsvMchAppCapabilityService.listSupportedCapabilities());
}
}

View File

@@ -0,0 +1,123 @@
package cn.daxpay.open.channel.wechat.controller.isv;
import cn.daxpay.open.channel.wechat.param.isv.WechatIsvMchAppParam;
import cn.daxpay.open.channel.wechat.param.isv.WechatIsvMchAppAuthConfigParam;
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvMchAppAuthConfigResult;
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvMchAppResult;
import cn.daxpay.open.channel.wechat.service.isv.WechatIsvMchAppAuthConfigService;
import cn.daxpay.open.channel.wechat.service.isv.WechatIsvMchAppService;
import cn.daxpay.open.platform.core.annotation.PermCode;
import cn.daxpay.open.platform.core.rest.Res;
import cn.daxpay.open.platform.core.rest.result.Result;
import cn.daxpay.open.platform.core.util.ValidationUtil;
import cn.daxpay.open.platform.core.validation.ValidationGroup;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotBlank;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/// # 微信服务商通道商户应用管理
///
/// 提供服务商通道商户应用(子商户应用)及其授权认证配置的 REST API,支持按商户号和通道商户号查询列表。
///
@PermCode(menuCode = "channel:wechat:app")
@Validated
@Tag(name = "微信服务商通道商户应用管理")
@RestController
@RequestMapping("/admin/wechat/isv-mch-app")
@RequiredArgsConstructor
public class WechatIsvMchAppController {
private final WechatIsvMchAppService wechatIsvMchAppService;
private final WechatIsvMchAppAuthConfigService wechatIsvMchAppAuthConfigService;
@PermCode(code = "view", nameCn = "通道商户查看", nameEn = "Channel Merchant View")
@Operation(summary = "根据商户号和通道商户号查询应用列表")
@GetMapping("/list-by-channel-mch-no")
public Result<List<WechatIsvMchAppResult>> listByChannelMchNo(
@NotBlank(message = "{validation.field.mchNo.notBlank}") String mchNo,
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) {
return Res.ok(wechatIsvMchAppService.listByMchNoAndChannelMchNo(mchNo, channelMchNo));
}
@PermCode(code = "view", nameCn = "通道商户查看", nameEn = "Channel Merchant View")
@Operation(summary = "查询应用详情")
@GetMapping("/find-by-id")
public Result<WechatIsvMchAppResult> findById(
@NotNull(message = "{validation.field.id.notNull}") Long id) {
return Res.ok(wechatIsvMchAppService.findById(id));
}
@PermCode(code = "view", nameCn = "通道商户查看", nameEn = "Channel Merchant View")
@Operation(summary = "同一通道商户下微信应用ID是否已存在")
@GetMapping("/exists-wx-app-id-by-channel")
public Result<Boolean> existsWxAppIdByChannel(
@NotBlank(message = "{validation.field.mchNo.notBlank}") String mchNo,
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo,
@NotBlank(message = "{validation.field.wxAppId.notBlank}") String wxAppId) {
return Res.ok(wechatIsvMchAppService.existsWxAppIdByChannel(mchNo, channelMchNo, wxAppId, null));
}
@PermCode(code = "view", nameCn = "通道商户查看", nameEn = "Channel Merchant View")
@Operation(summary = "同一通道商户下微信应用ID是否已存在(排除自身)")
@GetMapping("/exists-wx-app-id-by-channel-not-id")
public Result<Boolean> existsWxAppIdByChannelNotId(
@NotBlank(message = "{validation.field.mchNo.notBlank}") String mchNo,
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo,
@NotBlank(message = "{validation.field.wxAppId.notBlank}") String wxAppId,
@NotNull(message = "{validation.field.id.notNull}") Long id) {
return Res.ok(wechatIsvMchAppService.existsWxAppIdByChannel(mchNo, channelMchNo, wxAppId, id));
}
@PermCode(code = "manage", nameCn = "通道商户管理", nameEn = "Channel Merchant Manage")
@Operation(summary = "新增服务商通道商户应用")
@PostMapping("/add")
public Result<Void> add(@RequestBody @Validated(ValidationGroup.add.class) WechatIsvMchAppParam param) {
ValidationUtil.validateParam(param, ValidationGroup.add.class);
wechatIsvMchAppService.add(param);
return Res.ok();
}
@PermCode(code = "manage", nameCn = "通道商户管理", nameEn = "Channel Merchant Manage")
@Operation(summary = "修改服务商通道商户应用")
@PostMapping("/update")
public Result<Void> update(@RequestBody @Validated(ValidationGroup.edit.class) WechatIsvMchAppParam param) {
ValidationUtil.validateParam(param, ValidationGroup.edit.class);
wechatIsvMchAppService.update(param);
return Res.ok();
}
@PermCode(code = "manage", nameCn = "通道商户管理", nameEn = "Channel Merchant Manage")
@Operation(summary = "删除服务商通道商户应用")
@PostMapping("/delete")
public Result<Void> delete(@NotNull(message = "{validation.field.id.notNull}") Long id) {
wechatIsvMchAppService.delete(id);
return Res.ok();
}
@PermCode(code = "view", nameCn = "通道商户查看", nameEn = "Channel Merchant View")
@Operation(summary = "查询应用授权认证配置")
@GetMapping("/find-auth-config-by-app-id")
public Result<WechatIsvMchAppAuthConfigResult> findAuthConfigByAppId(
@NotNull(message = "{validation.field.wechatIsvMchAppId.notNull}") Long wechatIsvMchAppId) {
var config = wechatIsvMchAppAuthConfigService.findByWechatIsvMchAppId(wechatIsvMchAppId);
return Res.ok(config.toResult());
}
@PermCode(code = "manage", nameCn = "通道商户管理", nameEn = "Channel Merchant Manage")
@Operation(summary = "保存应用授权认证配置")
@PostMapping("/save-auth-config")
public Result<Void> saveAuthConfig(@RequestBody @Validated WechatIsvMchAppAuthConfigParam param) {
wechatIsvMchAppAuthConfigService.save(param);
return Res.ok();
}
}

View File

@@ -0,0 +1,30 @@
package cn.daxpay.open.channel.wechat.convert.isv;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchAppAuthConfig;
import cn.daxpay.open.channel.wechat.param.isv.WechatIsvMchAppAuthConfigParam;
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvMchAppAuthConfigResult;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
/// # 微信服务商通道商户应用授权认证配置转换
///
/// MapStruct转换器,负责微信服务商通道商户应用授权认证配置在实体、参数和返回结果之间的转换,更新时空值不覆盖。
///
@Mapper
public interface WechatIsvMchAppAuthConfigConvert {
WechatIsvMchAppAuthConfigConvert CONVERT = Mappers.getMapper(WechatIsvMchAppAuthConfigConvert.class);
/// 转换为返回对象
WechatIsvMchAppAuthConfigResult toResult(WechatIsvMchAppAuthConfig entity);
/// 转换为实体
WechatIsvMchAppAuthConfig toEntity(WechatIsvMchAppAuthConfigParam param);
/// 更新源数据到实体(空值不覆盖)
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void copy(WechatIsvMchAppAuthConfigParam param, @MappingTarget WechatIsvMchAppAuthConfig entity);
}

View File

@@ -0,0 +1,19 @@
package cn.daxpay.open.channel.wechat.convert.isv;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchAppCapability;
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvMchAppCapabilityResult;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/// # 微信服务商通道商户应用支付能力关联转换
///
/// MapStruct转换器,负责关联记录在实体与返回结果之间的转换。
///
@Mapper
public interface WechatIsvMchAppCapabilityConvert {
WechatIsvMchAppCapabilityConvert CONVERT = Mappers.getMapper(WechatIsvMchAppCapabilityConvert.class);
/// 转换为返回对象
WechatIsvMchAppCapabilityResult toResult(WechatIsvMchAppCapability entity);
}

View File

@@ -0,0 +1,27 @@
package cn.daxpay.open.channel.wechat.convert.isv;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchApp;
import cn.daxpay.open.channel.wechat.param.isv.WechatIsvMchAppParam;
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvMchAppResult;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
/// # 微信服务商通道商户应用转换
///
/// MapStruct转换器,负责微信服务商通道商户应用在实体、参数和返回结果之间的相互转换。
///
@Mapper
public interface WechatIsvMchAppConvert {
WechatIsvMchAppConvert CONVERT = Mappers.getMapper(WechatIsvMchAppConvert.class);
/// 转换为返回对象
WechatIsvMchAppResult toResult(WechatIsvMchApp entity);
/// 转换为实体
WechatIsvMchApp toEntity(WechatIsvMchAppParam param);
/// 更新源数据到实体(忽略空值)
void copy(WechatIsvMchAppParam param, @MappingTarget WechatIsvMchApp entity);
}

View File

@@ -0,0 +1,29 @@
package cn.daxpay.open.channel.wechat.dao.isv;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchAppAuthConfig;
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/// # 微信服务商通道商户应用授权认证配置 Manager
///
/// 服务商通道商户应用授权认证配置数据访问管理器,提供按应用ID查询和删除授权认证配置的方法。
///
@Repository
public class WechatIsvMchAppAuthConfigManager extends BaseManager<WechatIsvMchAppAuthConfigMapper, WechatIsvMchAppAuthConfig> {
/// 根据应用ID查询授权认证配置
public Optional<WechatIsvMchAppAuthConfig> findByWechatIsvMchAppId(Long wechatIsvMchAppId) {
return lambdaQuery()
.eq(WechatIsvMchAppAuthConfig::getWechatIsvMchAppId, wechatIsvMchAppId)
.oneOpt();
}
/// 根据应用ID删除授权认证配置
public void deleteByWechatIsvMchAppId(Long wechatIsvMchAppId) {
lambdaUpdate()
.eq(WechatIsvMchAppAuthConfig::getWechatIsvMchAppId, wechatIsvMchAppId)
.remove();
}
}

View File

@@ -0,0 +1,13 @@
package cn.daxpay.open.channel.wechat.dao.isv;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchAppAuthConfig;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
/// # 微信服务商通道商户应用授权认证配置
///
/// 微信服务商通道商户应用授权认证配置 MyBatis-Plus Mapper,继承 MPJBaseMapper 支持多表联查。
///
@Mapper
public interface WechatIsvMchAppAuthConfigMapper extends MPJBaseMapper<WechatIsvMchAppAuthConfig> {
}

View File

@@ -0,0 +1,51 @@
package cn.daxpay.open.channel.wechat.dao.isv;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchAppCapability;
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/// # 微信服务商通道商户应用支付能力关联 Manager
///
/// 关联记录数据访问管理器,提供按通道商户号查询/删除、按能力查应用、按应用清理等方法。
///
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatIsvMchAppCapabilityManager extends BaseManager<WechatIsvMchAppCapabilityMapper, WechatIsvMchAppCapability> {
/// 根据通道商户号查询全部关联(按创建时间升序)
public List<WechatIsvMchAppCapability> listByChannelMchNo(String channelMchNo) {
return lambdaQuery()
.eq(WechatIsvMchAppCapability::getChannelMchNo, channelMchNo)
.orderByAsc(WechatIsvMchAppCapability::getCreateTime)
.orderByAsc(WechatIsvMchAppCapability::getId)
.list();
}
/// 根据通道商户号与支付能力查询单条关联(支付时调用)
public Optional<WechatIsvMchAppCapability> findOne(String channelMchNo, String capability) {
return lambdaQuery()
.eq(WechatIsvMchAppCapability::getChannelMchNo, channelMchNo)
.eq(WechatIsvMchAppCapability::getCapability, capability)
.oneOpt();
}
/// 根据通道商户号删除全部关联(批量保存时先清后插)
public void deleteByChannelMchNo(String channelMchNo) {
lambdaUpdate()
.eq(WechatIsvMchAppCapability::getChannelMchNo, channelMchNo)
.remove();
}
/// 根据应用ID删除关联(应用被删除时级联清理,避免悬空引用)
public void deleteByWechatIsvMchAppId(Long wechatIsvMchAppId) {
lambdaUpdate()
.eq(WechatIsvMchAppCapability::getWechatIsvMchAppId, wechatIsvMchAppId)
.remove();
}
}

View File

@@ -0,0 +1,13 @@
package cn.daxpay.open.channel.wechat.dao.isv;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchAppCapability;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
/// # 微信服务商通道商户应用支付能力关联
///
/// 微信服务商通道商户应用支付能力关联 MyBatis-Plus Mapper,继承 MPJBaseMapper 支持多表联查。
///
@Mapper
public interface WechatIsvMchAppCapabilityMapper extends MPJBaseMapper<WechatIsvMchAppCapability> {
}

View File

@@ -0,0 +1,54 @@
package cn.daxpay.open.channel.wechat.dao.isv;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchApp;
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/// # 微信服务商通道商户应用 Manager
///
/// 服务商通道商户应用(子商户应用)数据访问管理器,提供按商户号和通道商户号查询列表、
/// 首个应用查询、应用类型推导查询及同一通道下应用AppId唯一性校验等方法。
///
@Repository
public class WechatIsvMchAppManager extends BaseManager<WechatIsvMchAppMapper, WechatIsvMchApp> {
/// 根据商户号和通道商户号查询应用列表(按创建时间升序)
public List<WechatIsvMchApp> listByMchNoAndChannelMchNo(String mchNo, String channelMchNo) {
return lambdaQuery()
.eq(WechatIsvMchApp::getMchNo, mchNo)
.eq(WechatIsvMchApp::getChannelMchNo, channelMchNo)
.orderByAsc(WechatIsvMchApp::getCreateTime)
.orderByAsc(WechatIsvMchApp::getId)
.list();
}
/// 按通道商户号查询首个应用(能力解析兜底时使用)
public Optional<WechatIsvMchApp> findFirstByChannelMchNo(String channelMchNo) {
return firstOpt(q -> q
.eq(WechatIsvMchApp::getChannelMchNo, channelMchNo)
.orderByAsc(WechatIsvMchApp::getCreateTime)
.orderByAsc(WechatIsvMchApp::getId));
}
/// 按通道商户号与应用类型查询首个应用(能力→应用类型推导时使用)
public Optional<WechatIsvMchApp> findFirstByChannelMchNoAndAppType(String channelMchNo, String appType) {
return firstOpt(q -> q
.eq(WechatIsvMchApp::getChannelMchNo, channelMchNo)
.eq(WechatIsvMchApp::getAppType, appType)
.orderByAsc(WechatIsvMchApp::getCreateTime)
.orderByAsc(WechatIsvMchApp::getId));
}
/// 校验同一通道商户下wxAppId是否已存在(排除自身)
public boolean existsByChannelMchNoAndWxAppId(String mchNo, String channelMchNo, String wxAppId, Long excludeId) {
return lambdaQuery()
.eq(WechatIsvMchApp::getMchNo, mchNo)
.eq(WechatIsvMchApp::getChannelMchNo, channelMchNo)
.eq(WechatIsvMchApp::getWxAppId, wxAppId)
.ne(excludeId != null, WechatIsvMchApp::getId, excludeId)
.exists();
}
}

View File

@@ -0,0 +1,13 @@
package cn.daxpay.open.channel.wechat.dao.isv;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchApp;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
/// # 微信服务商通道商户应用
///
/// 微信服务商通道商户应用 MyBatis-Plus Mapper,继承 MPJBaseMapper 支持多表联查。
///
@Mapper
public interface WechatIsvMchAppMapper extends MPJBaseMapper<WechatIsvMchApp> {
}

View File

@@ -2,7 +2,7 @@ package cn.daxpay.open.channel.wechat.entity.isv;
import cn.daxpay.open.channel.wechat.convert.isv.WechatIsvAppAuthConfigConvert;
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvAppAuthConfigResult;
import cn.daxpay.open.payment.common.entity.merchant.MchBaseEntity;
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;
@@ -14,17 +14,14 @@ import lombok.experimental.Accessors;
/// # 微信服务商应用授权认证配置
///
/// 继承商户基础实体,自动填充商户号用于多商户隔离。
/// 服务商应用(WechatIsvApp)本身为平台级全局配置, 不挂商户维度,
/// 其授权认证配置(appSecret/回调地址)同样为全局配置。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName(value = "wechat_isv_app_auth_config", autoResultMap = true)
public class WechatIsvAppAuthConfig extends MchBaseEntity implements ToResult<WechatIsvAppAuthConfigResult> {
/// 通道商户号
@TableField(updateStrategy = FieldStrategy.NEVER)
private String channelMchNo;
public class WechatIsvAppAuthConfig extends MpBaseEntity implements ToResult<WechatIsvAppAuthConfigResult> {
/// 微信服务商应用ID
@TableField(updateStrategy = FieldStrategy.NEVER)

View File

@@ -0,0 +1,44 @@
package cn.daxpay.open.channel.wechat.entity.isv;
import cn.daxpay.open.channel.wechat.convert.isv.WechatIsvMchAppConvert;
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvMchAppResult;
import cn.daxpay.open.payment.common.entity.merchant.MchBaseEntity;
import cn.daxpay.open.platform.common.mybatisplus.function.ToResult;
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;
/// # 微信服务商通道商户应用
///
/// 表示微信服务商模式下特约商户(子商户)注册的微信应用实体,挂在通道商户号(channelMchNo)维度下。
/// 每个应用对应一个微信AppId,支付时作为 sub_appid 使用。同一通道商户下微信应用AppId唯一。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName(value = "wechat_isv_mch_app", autoResultMap = true)
public class WechatIsvMchApp extends MchBaseEntity implements ToResult<WechatIsvMchAppResult> {
/// 通道商户号(服务商特约商户)
@TableField(updateStrategy = FieldStrategy.NEVER)
private String channelMchNo;
/// 应用名称
private String appName;
/// 应用类型
@TableField(updateStrategy = FieldStrategy.NEVER)
private String appType;
/// 微信应用AppId(对应微信支付sub_appid)
private String wxAppId;
/// 转换
@Override
public WechatIsvMchAppResult toResult() {
return WechatIsvMchAppConvert.CONVERT.toResult(this);
}
}

View File

@@ -0,0 +1,46 @@
package cn.daxpay.open.channel.wechat.entity.isv;
import cn.daxpay.open.channel.wechat.convert.isv.WechatIsvMchAppAuthConfigConvert;
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvMchAppAuthConfigResult;
import cn.daxpay.open.payment.common.entity.merchant.MchBaseEntity;
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;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # 微信服务商通道商户应用授权认证配置
///
/// 配置服务商通道商户应用(子商户应用)的应用密钥和授权回调地址,用于微信OAuth授权流程中的身份验证与回调跳转。
/// 一个应用对应一份授权配置(由唯一约束 channel_mch_no + wechat_isv_mch_app_id 保证)。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName(value = "wechat_isv_mch_app_auth_config", autoResultMap = true)
public class WechatIsvMchAppAuthConfig extends MchBaseEntity implements ToResult<WechatIsvMchAppAuthConfigResult> {
/// 通道商户号(服务商特约商户)
@TableField(updateStrategy = FieldStrategy.NEVER)
private String channelMchNo;
/// 关联服务商通道商户应用ID
@TableField(updateStrategy = FieldStrategy.NEVER)
private Long wechatIsvMchAppId;
/// 应用密钥(加密存储)
@TableField(typeHandler = DataEncryptTypeHandler.class)
private String appSecret;
/// 授权回调地址
private String authCallbackUrl;
/// 转换
@Override
public WechatIsvMchAppAuthConfigResult toResult() {
return WechatIsvMchAppAuthConfigConvert.CONVERT.toResult(this);
}
}

View File

@@ -0,0 +1,44 @@
package cn.daxpay.open.channel.wechat.entity.isv;
import cn.daxpay.open.channel.wechat.convert.isv.WechatIsvMchAppCapabilityConvert;
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvMchAppCapabilityResult;
import cn.daxpay.open.payment.common.entity.merchant.MchBaseEntity;
import cn.daxpay.open.platform.common.mybatisplus.function.ToResult;
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;
/// # 微信服务商通道商户应用支付能力关联
///
/// 建立通道商户维度下「支付能力 → 子商户应用」的绑定关系,供支付时选择正确的微信应用(sub_appid)。
/// 本表只存子商户显式选择自己应用的记录,某能力未配置时自动回退到全局服务商应用配置。
/// 同一通道商户下,一个支付能力只能绑定一个应用(由唯一约束 channel_mch_no + capability 保证)。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName(value = "wechat_isv_mch_app_capability", autoResultMap = true)
public class WechatIsvMchAppCapability extends MchBaseEntity implements ToResult<WechatIsvMchAppCapabilityResult> {
/// 通道商户号(服务商特约商户)
@TableField(updateStrategy = FieldStrategy.NEVER)
private String channelMchNo;
/// 支付能力编码
/// @see cn.daxpay.open.platform.core.enums.pay.channel.PayCapabilityEnum
@TableField(updateStrategy = FieldStrategy.NEVER)
private String capability;
/// 关联微信服务商通道商户应用ID(指向 wechat_isv_mch_app.id)
@TableField(updateStrategy = FieldStrategy.NEVER)
private Long wechatIsvMchAppId;
/// 转换
@Override
public WechatIsvMchAppCapabilityResult toResult() {
return WechatIsvMchAppCapabilityConvert.CONVERT.toResult(this);
}
}

View File

@@ -13,7 +13,7 @@ import lombok.experimental.Accessors;
@Schema(title = "微信服务商应用授权认证配置保存参数")
public class WechatIsvAppAuthConfigParam {
@NotNull(message = "{validation.field.id.notNull}")
@NotNull(message = "{validation.field.wechatIsvAppId.notNull}")
@Schema(description = "微信服务商应用ID")
private Long wechatIsvAppId;

View File

@@ -0,0 +1,37 @@
package cn.daxpay.open.channel.wechat.param.isv;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 微信服务商通道商户应用授权认证配置保存参数
///
/// 保存/更新微信服务商通道商户应用授权认证配置时接收的请求参数,含商户号、通道商户号、应用密钥和授权回调地址。
///
@Data
@Accessors(chain = true)
@Schema(title = "微信服务商通道商户应用授权认证配置保存参数")
public class WechatIsvMchAppAuthConfigParam {
@NotNull(message = "{validation.field.wechatIsvMchAppId.notNull}")
@Schema(description = "关联应用ID")
private Long wechatIsvMchAppId;
@NotBlank(message = "{validation.field.mchNo.notBlank}")
@Schema(description = "商户号")
private String mchNo;
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}")
@Schema(description = "通道商户号")
private String channelMchNo;
@Schema(description = "应用密钥")
private String appSecret;
@Size(max = 512, message = "{validation.field.authCallbackUrl.size}")
@Schema(description = "授权回调地址(仅公众号,非必填)")
private String authCallbackUrl;
}

View File

@@ -0,0 +1,33 @@
package cn.daxpay.open.channel.wechat.param.isv;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/// # 微信服务商通道商户支付能力关联应用批量保存参数
///
/// 全量覆盖某通道商户下「支付能力 → 子商户应用」的绑定关系:先清除旧记录,再按 items 批量插入。
/// items 为空表示清空该通道商户下所有绑定(该能力的应用将自动回退到全局服务商配置)。
/// 仅存子商户显式选择自己应用的记录,选「服务商默认应用」的能力不在此参数中。
///
@Data
@Accessors(chain = true)
@Schema(title = "微信服务商通道商户支付能力关联应用批量保存参数")
public class WechatIsvMchAppCapabilityBatchParam {
@NotBlank(message = "{validation.field.mchNo.notBlank}")
@Schema(description = "商户号")
private String mchNo;
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}")
@Schema(description = "通道商户号")
private String channelMchNo;
@Valid
@Schema(description = "支付能力关联应用列表(仅含选子商户应用的项,选服务商默认的不传)")
private List<WechatIsvMchAppCapabilityItem> items;
}

View File

@@ -0,0 +1,25 @@
package cn.daxpay.open.channel.wechat.param.isv;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 微信服务商通道商户支付能力关联应用单项
///
/// 用于批量保存时,声明单个支付能力绑定的服务商通道商户应用(子商户应用)。
///
@Data
@Accessors(chain = true)
@Schema(title = "微信服务商通道商户支付能力关联应用单项")
public class WechatIsvMchAppCapabilityItem {
@NotBlank(message = "{validation.field.capability.notBlank}")
@Schema(description = "支付能力编码")
private String capability;
@NotNull(message = "{validation.field.wechatIsvMchAppId.notNull}")
@Schema(description = "关联微信服务商通道商户应用ID")
private Long wechatIsvMchAppId;
}

View File

@@ -0,0 +1,42 @@
package cn.daxpay.open.channel.wechat.param.isv;
import cn.daxpay.open.platform.core.validation.ValidationGroup;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 微信服务商通道商户应用保存参数
///
/// 保存/更新微信服务商通道商户应用(子商户应用)时接收的请求参数,含商户号、通道商户号、应用名称、应用类型和微信应用AppId。
///
@Data
@Accessors(chain = true)
@Schema(title = "微信服务商通道商户应用保存参数")
public class WechatIsvMchAppParam {
@NotNull(message = "{validation.field.id.notNull}", groups = ValidationGroup.edit.class)
@Schema(description = "主键,新增时不传")
private Long id;
@NotBlank(message = "{validation.field.mchNo.notBlank}")
@Schema(description = "商户号")
private String mchNo;
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}")
@Schema(description = "通道商户号")
private String channelMchNo;
@NotBlank(message = "{validation.field.appName.notBlank}")
@Schema(description = "应用名称")
private String appName;
@NotBlank(message = "{validation.field.appType.notBlank}")
@Schema(description = "应用类型")
private String appType;
@NotBlank(message = "{validation.field.wxAppId.notBlank}")
@Schema(description = "微信应用AppId")
private String wxAppId;
}

View File

@@ -15,12 +15,6 @@ import lombok.experimental.Accessors;
@Schema(title = "微信服务商应用授权认证配置")
public class WechatIsvAppAuthConfigResult extends BaseResult {
@Schema(description = "商户号")
private String mchNo;
@Schema(description = "通道商户号")
private String channelMchNo;
@Schema(description = "微信服务商应用ID")
private Long wechatIsvAppId;

View File

@@ -0,0 +1,36 @@
package cn.daxpay.open.channel.wechat.result.isv;
import cn.daxpay.open.platform.common.json.sensitive.SensitiveInfo;
import cn.daxpay.open.platform.core.result.BaseResult;
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 WechatIsvMchAppAuthConfigResult extends BaseResult {
@Schema(description = "商户号")
private String mchNo;
@Schema(description = "通道商户号")
private String channelMchNo;
@Schema(description = "关联应用ID")
private Long wechatIsvMchAppId;
// 应用密钥(序列化时脱敏)
@SensitiveInfo
@Schema(description = "应用密钥(已脱敏)")
private String appSecret;
@Schema(description = "授权回调地址")
private String authCallbackUrl;
}

View File

@@ -0,0 +1,37 @@
package cn.daxpay.open.channel.wechat.result.isv;
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;
/// # 微信服务商通道商户应用支付能力关联
///
/// 展示通道商户(特约商户)下各支付能力绑定的子商户应用信息,含应用名称/AppId/类型等冗余字段(由 Service 填充)。
/// 注:「服务商默认应用」回退信息由前端另行调用全局 wechat_isv_app_capability 接口获取,不在本结果中冗余。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "微信服务商通道商户应用支付能力关联")
public class WechatIsvMchAppCapabilityResult extends MchBaseResult {
@Schema(description = "通道商户号")
private String channelMchNo;
@Schema(description = "支付能力编码")
private String capability;
@Schema(description = "关联微信服务商通道商户应用ID")
private Long wechatIsvMchAppId;
@Schema(description = "应用名称(冗余展示)")
private String appName;
@Schema(description = "微信应用AppId(冗余展示)")
private String wxAppId;
@Schema(description = "应用类型(冗余展示): official_account/mini_program/mobile_app")
private String appType;
}

View File

@@ -0,0 +1,30 @@
package cn.daxpay.open.channel.wechat.result.isv;
import cn.daxpay.open.payment.common.result.MchTradeBaseResult;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # 微信服务商通道商户应用
///
/// 微信服务商通道商户应用(子商户应用)的返回结果对象,含通道商户号、应用名称、应用类型和微信应用AppId等信息。
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "微信服务商通道商户应用")
public class WechatIsvMchAppResult extends MchTradeBaseResult {
@Schema(description = "通道商户号")
private String channelMchNo;
@Schema(description = "应用名称")
private String appName;
@Schema(description = "应用类型")
private String appType;
@Schema(description = "微信应用AppId")
private String wxAppId;
}

View File

@@ -2,7 +2,9 @@ package cn.daxpay.open.channel.wechat.service.direct;
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
import cn.daxpay.open.channel.wechat.dao.direct.WechatDirectAppManager;
import cn.daxpay.open.channel.wechat.dao.direct.WechatDirectChannelMerchantManager;
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectApp;
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectChannelMerchant;
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectKeyConfig;
import cn.daxpay.open.payment.masterdata.constants.product.dao.PayProductConfigManager;
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
@@ -28,6 +30,7 @@ import org.springframework.stereotype.Service;
public class WechatDirectConfigAssembler {
private final WechatDirectAppManager wechatDirectAppManager;
private final WechatDirectChannelMerchantManager wechatDirectChannelMerchantManager;
private final WechatDirectKeyConfigService wechatDirectKeyConfigService;
private final WechatDirectAppCapabilityService wechatDirectAppCapabilityService;
private final PayProductConfigManager payProductConfigManager;
@@ -35,16 +38,20 @@ public class WechatDirectConfigAssembler {
/// 组装直连商户的通道调用凭证(下发给子应用)
///
/// @param mchNo 商户号(兜底定位应用)
/// @param channelMchNo 通道商户号(微信商户号, 密钥查询与应用定位主键)
/// @param channelMchNo 通道商户号(系统雪花号, 密钥查询与应用定位主键, 不等于微信商户号)
/// @param capability 支付能力编码(用于选择匹配的应用)
/// @return 微信 SDK 凭证, 字段对齐子应用 WechatSdkCredential
public WechatSdkCredential buildConfig(String mchNo, String channelMchNo, String capability) {
WechatDirectApp app = resolveApp(mchNo, channelMchNo, capability);
WechatDirectKeyConfig keyConfig = wechatDirectKeyConfigService.findByChannelMchNo(channelMchNo);
// 从通道商户绑定表取真实微信商户号(channelMchNo 是系统雪花号, 不等于 wxMchId)
WechatDirectChannelMerchant channelMerchant = wechatDirectChannelMerchantManager.findByChannelMchNo(channelMchNo)
// 微信: 通道商户配置不存在
.orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist"));
WechatSdkCredential credential = new WechatSdkCredential();
// 微信商户号 = 通道商户号
credential.setWxMchId(app.getChannelMchNo());
credential.setWxMchId(channelMerchant.getWxMchId());
credential.setWxAppId(app.getWxAppId());
// 密钥与证书
credential.setApiKeyV3(keyConfig.getApiKeyV3());

View File

@@ -0,0 +1,66 @@
package cn.daxpay.open.channel.wechat.service.isv;
import cn.daxpay.open.channel.wechat.convert.isv.WechatIsvMchAppAuthConfigConvert;
import cn.daxpay.open.channel.wechat.dao.isv.WechatIsvMchAppAuthConfigManager;
import cn.daxpay.open.channel.wechat.dao.isv.WechatIsvMchAppManager;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchApp;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchAppAuthConfig;
import cn.daxpay.open.channel.wechat.param.isv.WechatIsvMchAppAuthConfigParam;
import cn.daxpay.open.platform.core.code.CommonErrorCode;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import cn.daxpay.open.platform.core.exception.DataNotExistException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/// # 微信服务商通道商户应用授权认证配置
///
/// 管理服务商通道商户应用(子商户应用)的授权认证配置,查询时不存在则创建默认记录,保存时校验应用归属关系。
///
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatIsvMchAppAuthConfigService {
private final WechatIsvMchAppAuthConfigManager wechatIsvMchAppAuthConfigManager;
private final WechatIsvMchAppManager wechatIsvMchAppManager;
/// 根据应用ID查询授权认证配置, 不存在则创建默认记录
@Transactional(rollbackFor = Exception.class)
public WechatIsvMchAppAuthConfig findByWechatIsvMchAppId(Long wechatIsvMchAppId) {
var existing = wechatIsvMchAppAuthConfigManager.findByWechatIsvMchAppId(wechatIsvMchAppId);
if (existing.isPresent()) {
return existing.get();
}
var app = wechatIsvMchAppManager.findById(wechatIsvMchAppId)
// 微信: 服务商通道商户应用不存在
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.mchAppNotFound"));
var config = new WechatIsvMchAppAuthConfig()
.setChannelMchNo(app.getChannelMchNo())
.setWechatIsvMchAppId(wechatIsvMchAppId);
config.setMchNo(app.getMchNo());
wechatIsvMchAppAuthConfigManager.save(config);
return config;
}
/// 保存应用授权认证配置(更新)
@Transactional(rollbackFor = Exception.class)
public void save(WechatIsvMchAppAuthConfigParam param) {
var app = wechatIsvMchAppManager.findById(param.getWechatIsvMchAppId())
// 微信: 服务商通道商户应用不存在
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.mchAppNotFound"));
if (!app.getMchNo().equals(param.getMchNo()) || !app.getChannelMchNo().equals(param.getChannelMchNo())) {
// 微信: 服务商通道商户应用不存在或商户号归属不匹配
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.wechat.mchAppNotFound");
}
var config = this.findByWechatIsvMchAppId(param.getWechatIsvMchAppId());
WechatIsvMchAppAuthConfigConvert.CONVERT.copy(param, config);
wechatIsvMchAppAuthConfigManager.updateById(config);
}
/// 删除应用授权认证配置
public void deleteByWechatIsvMchAppId(Long wechatIsvMchAppId) {
wechatIsvMchAppAuthConfigManager.deleteByWechatIsvMchAppId(wechatIsvMchAppId);
}
}

View File

@@ -0,0 +1,162 @@
package cn.daxpay.open.channel.wechat.service.isv;
import cn.daxpay.open.channel.wechat.code.WechatAppTypeCode;
import cn.daxpay.open.channel.wechat.convert.isv.WechatIsvMchAppCapabilityConvert;
import cn.daxpay.open.channel.wechat.dao.isv.WechatIsvMchAppCapabilityManager;
import cn.daxpay.open.channel.wechat.dao.isv.WechatIsvMchAppManager;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchApp;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchAppCapability;
import cn.daxpay.open.channel.wechat.param.isv.WechatIsvMchAppCapabilityBatchParam;
import cn.daxpay.open.channel.wechat.param.isv.WechatIsvMchAppCapabilityItem;
import cn.daxpay.open.channel.wechat.result.WechatCapabilityOption;
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvMchAppCapabilityResult;
import cn.daxpay.open.platform.common.i18n.util.I18nUtil;
import cn.daxpay.open.platform.core.code.CommonErrorCode;
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import cn.daxpay.open.payment.core.strategy.PaymentStrategyFactory;
import cn.daxpay.open.payment.core.strategy.product.AbsProductStrategy;
import cn.hutool.core.collection.CollUtil;
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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
/// # 微信服务商通道商户应用支付能力关联
///
/// 管理通道商户(特约商户)维度下「支付能力 → 子商户应用」的绑定关系。
/// 本表只存子商户显式选择自己应用的记录;某能力未配置时,支付解析返回 empty,
/// 由调用方(未来的微信服务商支付策略/配置组装器)回退到全局服务商应用配置
/// ([cn.daxpay.open.channel.wechat.service.isv.WechatIsvAppCapabilityService])。
///
/// 支付时通过 [resolveApp] 解析当前能力对应的应用:显式配置 > appType自动推导 > 通道商户首个兜底,
/// 三者均未命中返回 empty(语义为"使用全局服务商配置")。
///
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatIsvMchAppCapabilityService {
private final WechatIsvMchAppCapabilityManager capabilityManager;
private final WechatIsvMchAppManager wechatIsvMchAppManager;
/// 查询通道商户下的能力关联列表,并填充子商户应用展示信息(应用名称/AppId/类型)
public List<WechatIsvMchAppCapabilityResult> listByChannelMchNo(String mchNo, String channelMchNo) {
List<WechatIsvMchAppCapability> rels = capabilityManager.listByChannelMchNo(channelMchNo);
if (rels.isEmpty()) {
return List.of();
}
// 批量查询该通道商户下全部子商户应用,构建 id → app 映射用于填充展示字段
Map<Long, WechatIsvMchApp> appMap = wechatIsvMchAppManager.listByMchNoAndChannelMchNo(mchNo, channelMchNo)
.stream()
.collect(Collectors.toMap(WechatIsvMchApp::getId, Function.identity()));
return rels.stream()
.map(rel -> fillResult(rel, appMap.get(rel.getWechatIsvMchAppId())))
.toList();
}
/// 全量保存能力关联(先清后插),校验应用归属与能力唯一
///
/// items 仅含子商户显式选择自己应用的项;选「服务商默认应用」的能力不在此参数中,
/// 先清后插时会自然移除其历史记录,支付时自动回退到全局服务商配置。
@Transactional(rollbackFor = Exception.class)
public void saveBatch(WechatIsvMchAppCapabilityBatchParam param) {
String mchNo = param.getMchNo();
String channelMchNo = param.getChannelMchNo();
List<WechatIsvMchAppCapabilityItem> items = param.getItems();
// 先清空旧关联
capabilityManager.deleteByChannelMchNo(channelMchNo);
if (CollUtil.isEmpty(items)) {
return;
}
// 查询该通道商户下全部子商户应用,校验应用归属
List<WechatIsvMchApp> apps = wechatIsvMchAppManager.listByMchNoAndChannelMchNo(mchNo, channelMchNo);
Map<Long, WechatIsvMchApp> appMap = apps.stream()
.collect(Collectors.toMap(WechatIsvMchApp::getId, Function.identity()));
HashSet<String> capabilitySet = new HashSet<>();
for (WechatIsvMchAppCapabilityItem item : items) {
// 能力不可重复
if (!capabilitySet.add(item.getCapability())) {
// 微信: 支付能力[{0}]重复配置
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.wechat.capabilityDuplicate", item.getCapability());
}
// 应用归属校验
if (!appMap.containsKey(item.getWechatIsvMchAppId())) {
// 微信: 服务商通道商户应用不存在
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
"error.channel.wechat.mchAppNotFound");
}
var entity = new WechatIsvMchAppCapability()
.setChannelMchNo(channelMchNo)
.setCapability(item.getCapability())
.setWechatIsvMchAppId(item.getWechatIsvMchAppId());
entity.setMchNo(mchNo);
capabilityManager.save(entity);
}
}
/// 应用被删除时级联清理关联,避免悬空引用
public void deleteByWechatIsvMchAppId(Long wechatIsvMchAppId) {
capabilityManager.deleteByWechatIsvMchAppId(wechatIsvMchAppId);
}
/// 支付时解析当前支付能力对应的子商户应用:显式配置 > appType自动推导 > 通道商户首个兜底
///
/// 三者均未命中返回 empty,语义为"该能力未在子商户维度配置,请回退到全局服务商应用配置"。
/// 调用方(未来的微信服务商配置组装器)应在此基础上回退 [WechatIsvAppCapabilityService.resolveApp]。
///
/// @param channelMchNo 通道商户号(服务商特约商户)
/// @param capability 支付能力编码
/// @return 命中的子商户应用;未命中返回 empty(应回退全局服务商配置)
public Optional<WechatIsvMchApp> resolveApp(String channelMchNo, String capability) {
if (StrUtil.hasBlank(channelMchNo, capability)) {
return Optional.empty();
}
// 1. 显式配置优先
var rel = capabilityManager.findOne(channelMchNo, capability);
if (rel.isPresent()) {
return wechatIsvMchAppManager.findById(rel.get().getWechatIsvMchAppId());
}
// 2. 未配置则按能力 → appType 自动推导
String appType = WechatAppTypeCode.resolveAppType(capability);
if (appType != null) {
Optional<WechatIsvMchApp> byType = wechatIsvMchAppManager.findFirstByChannelMchNoAndAppType(channelMchNo, appType);
if (byType.isPresent()) {
return byType;
}
}
// 3. 最终兜底:按通道商户号取首个子商户应用;仍未命中返回 empty(回退全局)
return wechatIsvMchAppManager.findFirstByChannelMchNo(channelMchNo);
}
/// 查询微信服务商产品支持的支付能力候选列表(含国际化名称)
public List<WechatCapabilityOption> listSupportedCapabilities() {
AbsProductStrategy strategy = PaymentStrategyFactory.createByProduct(
ProductEnum.WECHAT_ISV.getCode(), AbsProductStrategy.class);
return strategy.methodCapabilityMapping().values().stream()
.flatMap(List::stream)
.distinct()
.map(cap -> new WechatCapabilityOption(cap.getCode(), I18nUtil.getEnumName(cap)))
.toList();
}
/// 填充关联结果的应用展示字段
private WechatIsvMchAppCapabilityResult fillResult(WechatIsvMchAppCapability rel, WechatIsvMchApp app) {
WechatIsvMchAppCapabilityResult result = WechatIsvMchAppCapabilityConvert.CONVERT.toResult(rel);
if (app != null) {
result.setAppName(app.getAppName())
.setWxAppId(app.getWxAppId())
.setAppType(app.getAppType());
}
return result;
}
}

View File

@@ -0,0 +1,112 @@
package cn.daxpay.open.channel.wechat.service.isv;
import cn.daxpay.open.channel.wechat.code.WechatIsvAppTypeEnum;
import cn.daxpay.open.channel.wechat.convert.isv.WechatIsvMchAppConvert;
import cn.daxpay.open.channel.wechat.dao.isv.WechatIsvMchAppManager;
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchApp;
import cn.daxpay.open.channel.wechat.param.isv.WechatIsvMchAppParam;
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvMchAppResult;
import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil;
import cn.daxpay.open.platform.core.code.CommonErrorCode;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import cn.daxpay.open.platform.core.exception.DataNotExistException;
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.Arrays;
import java.util.List;
/// # 微信服务商通道商户应用管理
///
/// 提供服务商通道商户应用(子商户应用)的增删改查功能,包含同一通道商户下应用AppId的唯一性校验、
/// 应用类型校验、操作范围校验(防止跨商户操作),以及删除时级联清理授权配置与能力关联。
///
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatIsvMchAppService {
private final WechatIsvMchAppManager wechatIsvMchAppManager;
private final WechatIsvMchAppAuthConfigService wechatIsvMchAppAuthConfigService;
private final WechatIsvMchAppCapabilityService wechatIsvMchAppCapabilityService;
/// 根据商户号和通道商户号查询应用列表
public List<WechatIsvMchAppResult> listByMchNoAndChannelMchNo(String mchNo, String channelMchNo) {
var list = wechatIsvMchAppManager.listByMchNoAndChannelMchNo(mchNo, channelMchNo);
return MpUtil.toListResult(list);
}
/// 根据ID查询应用详情
public WechatIsvMchAppResult findById(Long id) {
return wechatIsvMchAppManager.findById(id)
.map(WechatIsvMchApp::toResult)
// 微信: 服务商通道商户应用不存在
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.mchAppNotFound"));
}
/// 判断同一通道商户下微信应用AppId是否已存在
public boolean existsWxAppIdByChannel(String mchNo, String channelMchNo, String wxAppId, Long excludeId) {
if (StrUtil.hasBlank(mchNo, channelMchNo, wxAppId)) {
return false;
}
return wechatIsvMchAppManager.existsByChannelMchNoAndWxAppId(mchNo, channelMchNo, wxAppId, excludeId);
}
/// 新增服务商通道商户应用
public void add(WechatIsvMchAppParam param) {
this.assertWxAppIdUnique(param.getMchNo(), param.getChannelMchNo(), param.getWxAppId(), null);
this.validateAppType(param.getAppType());
var entity = WechatIsvMchAppConvert.CONVERT.toEntity(param);
wechatIsvMchAppManager.save(entity);
}
/// 更新服务商通道商户应用
public void update(WechatIsvMchAppParam param) {
var entity = wechatIsvMchAppManager.findById(param.getId())
// 微信: 服务商通道商户应用不存在
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.mchAppNotFound"));
this.assertScopeMatch(entity, param.getMchNo(), param.getChannelMchNo());
this.assertWxAppIdUnique(entity.getMchNo(), entity.getChannelMchNo(), param.getWxAppId(), param.getId());
this.validateAppType(param.getAppType());
WechatIsvMchAppConvert.CONVERT.copy(param, entity);
wechatIsvMchAppManager.updateById(entity);
}
/// 删除应用(级联删除授权配置与能力关联)
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
wechatIsvMchAppManager.findById(id)
// 微信: 服务商通道商户应用不存在
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.mchAppNotFound"));
wechatIsvMchAppAuthConfigService.deleteByWechatIsvMchAppId(id);
wechatIsvMchAppCapabilityService.deleteByWechatIsvMchAppId(id);
wechatIsvMchAppManager.deleteById(id);
}
/// 校验同一通道商户下应用AppId唯一
private void assertWxAppIdUnique(String mchNo, String channelMchNo, String wxAppId, Long excludeId) {
if (this.existsWxAppIdByChannel(mchNo, channelMchNo, wxAppId, excludeId)) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.wechat.appIdDuplicate");
}
}
/// 校验操作范围与记录归属一致
private void assertScopeMatch(WechatIsvMchApp entity, String mchNo, String channelMchNo) {
if (!entity.getMchNo().equals(mchNo) || !entity.getChannelMchNo().equals(channelMchNo)) {
// 微信: 服务商通道商户应用不存在或商户号归属不匹配
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.wechat.mchAppNotFound");
}
}
/// 校验应用类型(复用服务商应用类型枚举)
private void validateAppType(String appType) {
boolean valid = Arrays.stream(WechatIsvAppTypeEnum.values())
.anyMatch(item -> item.getCode().equals(appType));
if (!valid) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.wechat.appTypeInvalid");
}
}
}

View File

@@ -1,13 +1,13 @@
{
"aggregate_pay_qrcode": "Aggregate QR Code Payment",
"aggregate_pay_barcode": "Aggregate Barcode Payment",
"aggregate_pay_qrcode": "Aggregate QR Code",
"aggregate_pay_barcode": "Aggregate Barcode",
"wechat_cashier": "WeChat Mini Program Cashier",
"wechat_qr": "WeChat Native",
"wechat_jsapi": "WeChat JSAPI",
"wechat_mini": "WeChat Mini Program",
"wechat_h5": "WeChat H5",
"wechat_app": "WeChat App",
"wechat_barcode": "WeChat Barcode Payment",
"wechat_barcode": "WeChat Barcode",
"alipay_qr": "Alipay QR",
"alipay_order_qr": "Alipay Order QR",
"alipay_jsapi": "Alipay JSAPI",
@@ -15,15 +15,15 @@
"alipay_pc": "Alipay PC",
"alipay_h5": "Alipay H5",
"alipay_app": "Alipay App",
"alipay_barcode": "Alipay Barcode Payment",
"alipay_barcode": "Alipay Barcode",
"union_qr": "UnionPay QR",
"union_jsapi": "UnionPay JSAPI",
"union_h5": "UnionPay H5",
"union_pay_barcode": "UnionPay Barcode Payment",
"douyin_qr": "Douyin QR Pay",
"douyin_jsapi": "Douyin JSAPI Pay",
"douyin_h5": "Douyin H5 Pay",
"douyin_app": "Douyin APP Pay",
"union_pay_barcode": "UnionPay Barcode",
"douyin_qr": "Douyin QR",
"douyin_jsapi": "Douyin JSAPI",
"douyin_h5": "Douyin H5",
"douyin_app": "Douyin App",
"visa_card_gateway": "Visa Gateway",
"visa_card_present": "Visa Card Present",
"mastercard_card_gateway": "Mastercard Gateway",

View File

@@ -103,6 +103,9 @@
"alipayDirectAppId": {
"notNull": "Alipay direct app id cannot be null"
},
"wechatIsvAppId": {
"notNull": "WeChat ISV app id cannot be null"
},
"account": {
"notBlank": "Account cannot be blank",
"size": "Account length must be between 6 and 20 characters",
@@ -508,6 +511,9 @@
"wechatDirectAppId": {
"notNull": "WeChat direct app ID cannot be blank"
},
"wechatIsvMchAppId": {
"notNull": "WeChat ISV channel merchant app ID cannot be blank"
},
"appSecret": {
"size": "App secret cannot exceed 512 characters"
},

View File

@@ -1,32 +1,32 @@
{
"aggregate_pay_qrcode": "聚合扫码支付",
"aggregate_pay_barcode": "聚合付款码支付",
"aggregate_pay_qrcode": "聚合扫码",
"aggregate_pay_barcode": "聚合付款码",
"wechat_cashier": "微信小程序收银台",
"wechat_qr": "Native 支付",
"wechat_jsapi": "微信jsapi",
"wechat_qr": "微信Native",
"wechat_jsapi": "微信JSAPI",
"wechat_mini": "微信小程序",
"wechat_h5": "微信H5",
"wechat_app": "微信应用支付",
"wechat_barcode": "微信付款码支付",
"wechat_app": "微信APP",
"wechat_barcode": "微信付款码",
"alipay_qr": "支付宝扫码",
"alipay_order_qr": "支付宝订单码",
"alipay_jsapi": "支付宝jsapi",
"alipay_jsapi": "支付宝JSAPI",
"alipay_mini": "支付宝小程序",
"alipay_pc": "支付宝电脑支付",
"alipay_pc": "支付宝PC",
"alipay_h5": "支付宝H5",
"alipay_app": "支付宝应用支付",
"alipay_barcode": "支付宝付款码支付",
"alipay_app": "支付宝APP",
"alipay_barcode": "支付宝付款码",
"union_qr": "银联扫码",
"union_jsapi": "银联jsapi",
"union_jsapi": "银联JSAPI",
"union_h5": "银联H5",
"union_pay_barcode": "银联付款码支付",
"douyin_qr": "抖音扫码支付",
"douyin_jsapi": "抖音JSAPI支付",
"douyin_h5": "抖音H5支付",
"douyin_app": "抖音APP支付",
"visa_card_gateway": "Visa 网关支付",
"visa_card_present": "Visa 刷卡支付",
"mastercard_card_gateway": "万事达网关支付",
"mastercard_card_present": "万事达刷卡支付",
"other": "其他支付方式"
"union_pay_barcode": "银联付款码",
"douyin_qr": "抖音扫码",
"douyin_jsapi": "抖音JSAPI",
"douyin_h5": "抖音H5",
"douyin_app": "抖音APP",
"visa_card_gateway": "Visa网关",
"visa_card_present": "Visa刷卡",
"mastercard_card_gateway": "万事达网关",
"mastercard_card_present": "万事达刷卡",
"other": "其他"
}

View File

@@ -103,6 +103,9 @@
"alipayDirectAppId": {
"notNull": "支付宝直连应用ID不可为空"
},
"wechatIsvAppId": {
"notNull": "微信服务商应用ID不可为空"
},
"account": {
"notBlank": "登录账号不可为空",
"size": "账号长度为6-20个字符",
@@ -511,6 +514,9 @@
"wechatDirectAppId": {
"notNull": "微信直连应用ID不可为空"
},
"wechatIsvMchAppId": {
"notNull": "微信服务商通道商户应用ID不可为空"
},
"appSecret": {
"size": "应用密钥不可超过512位"
},

View File

@@ -17,23 +17,23 @@ import java.util.Objects;
public enum PayMethodEnum implements I18nSupport {
/* 聚合支付 */
/// 聚合扫码支付
/// 聚合扫码
AGGREGATE_PAY_QRCODE("aggregate_pay_qrcode", PayProviderEnum.AGGREGATE_PAY),
/// 聚合付款码支付
/// 聚合付款码
AGGREGATE_PAY_BARCODE("aggregate_pay_barcode", PayProviderEnum.AGGREGATE_PAY),
/* 微信 */
/// 微信小程序收银台
WECHAT_CASHIER("wechat_cashier", PayProviderEnum.WECHAT),
/// 微信扫码
/// 微信Native
WECHAT_QR("wechat_qr", PayProviderEnum.WECHAT),
/// 微信jsapi
/// 微信JSAPI
WECHAT_JSAPI("wechat_jsapi", PayProviderEnum.WECHAT),
/// 微信小程序
WECHAT_MINI("wechat_mini", PayProviderEnum.WECHAT),
/// 微信H5
WECHAT_H5("wechat_h5", PayProviderEnum.WECHAT),
/// 微信应用支付
/// 微信APP
WECHAT_APP("wechat_app", PayProviderEnum.WECHAT),
/// 微信付款码
WECHAT_BARCODE("wechat_barcode", PayProviderEnum.WECHAT),
@@ -43,15 +43,15 @@ public enum PayMethodEnum implements I18nSupport {
ALIPAY_QR("alipay_qr", PayProviderEnum.ALIPAY),
/// 支付宝订单码
ALIPAY_ORDER_QR("alipay_order_qr", PayProviderEnum.ALIPAY),
/// 支付宝jsapi
/// 支付宝JSAPI
ALIPAY_JSAPI("alipay_jsapi", PayProviderEnum.ALIPAY),
/// 支付宝小程序
ALIPAY_MINI("alipay_mini", PayProviderEnum.ALIPAY),
/// 支付宝电脑支付
/// 支付宝PC
ALIPAY_PC("alipay_pc", PayProviderEnum.ALIPAY),
/// 支付宝H5
ALIPAY_H5("alipay_h5", PayProviderEnum.ALIPAY),
/// 支付宝应用支付
/// 支付宝APP
ALIPAY_APP("alipay_app", PayProviderEnum.ALIPAY),
/// 支付宝付款码
ALIPAY_BARCODE("alipay_barcode", PayProviderEnum.ALIPAY),
@@ -59,7 +59,7 @@ public enum PayMethodEnum implements I18nSupport {
/* 银联 */
/// 银联扫码
UNION_QR("union_qr", PayProviderEnum.UNION_PAY),
/// 银联jsapi
/// 银联JSAPI
UNION_JSAPI("union_jsapi", PayProviderEnum.UNION_PAY),
/// 银联H5
UNION_H5("union_h5", PayProviderEnum.UNION_PAY),
@@ -67,26 +67,26 @@ public enum PayMethodEnum implements I18nSupport {
UNION_PAY_BARCODE("union_pay_barcode", PayProviderEnum.UNION_PAY),
/* 抖音 */
/// 抖音扫码支付
/// 抖音扫码
DOUYIN_QR("douyin_qr", PayProviderEnum.DOUYIN),
/// 抖音JSAPI支付
/// 抖音JSAPI
DOUYIN_JSAPI("douyin_jsapi", PayProviderEnum.DOUYIN),
/// 抖音H5支付
/// 抖音H5
DOUYIN_H5("douyin_h5", PayProviderEnum.DOUYIN),
/// 抖音APP支付
/// 抖音APP
DOUYIN_APP("douyin_app", PayProviderEnum.DOUYIN),
/* 卡组 */
/// Visa 网关支付
/// Visa网关
VISA_CARD_GATEWAY("visa_card_gateway", PayProviderEnum.VISA),
/// Visa 刷卡支付
/// Visa刷卡
VISA_CARD_PRESENT("visa_card_present", PayProviderEnum.VISA),
/// 万事达网关支付
/// 万事达网关
MASTERCARD_CARD_GATEWAY("mastercard_card_gateway", PayProviderEnum.MASTERCARD),
/// 万事达刷卡支付
/// 万事达刷卡
MASTERCARD_CARD_PRESENT("mastercard_card_present", PayProviderEnum.MASTERCARD),
/// 其他支付方式(无固定渠道归属)
/// 其他(无固定渠道归属)
OTHER("other", null),
;