mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-12 15:35:38 +08:00
feat(channel): 微信/抖音直连商户应用支付能力绑定
- 微信直连(商户级)+服务商(全局级)能力绑定: entity/dao/convert/param/result/service/controller 全套 - 抖音直连(商户级)能力绑定: 同上(抖音仅直连模式) - resolveApp: 显式配置 > appType推导 > 通道商户首个兜底 - 微信直连应用补全 appType 字段+枚举, 应用删除级联清理关联 - SQL: wechat_isv/wechat_direct/douyin_direct_app_capability 三表 - i18n: error/enum 中英文同步
This commit is contained in:
@@ -1,7 +1,98 @@
|
||||
-- 支付同步: pay_normal_order 冗余通道路由参数, 供同步时凭证解析使用
|
||||
-- ============================================================
|
||||
-- 升级建表脚本
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE pay_normal_order ADD COLUMN IF NOT EXISTS channel_mch_no varchar(64);
|
||||
ALTER TABLE pay_normal_order ADD COLUMN IF NOT EXISTS capability varchar(64);
|
||||
|
||||
COMMENT ON COLUMN pay_normal_order.channel_mch_no IS '通道商户号(路由回填, 同步时用于解析通道应用凭证)';
|
||||
COMMENT ON COLUMN pay_normal_order.capability IS '支付能力编码(路由回填, 同步时用于解析通道应用凭证)';
|
||||
-- ------------------------------------------------------------
|
||||
-- 微信服务商应用支付能力关联(全局维度,一个能力绑定一个服务商应用)
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS wechat_isv_app_capability (
|
||||
id bigserial PRIMARY KEY,
|
||||
capability varchar(64) NOT NULL,
|
||||
wechat_isv_app_id bigint NOT NULL,
|
||||
creator bigint,
|
||||
create_time timestamptz(6) DEFAULT now(),
|
||||
last_modifier bigint,
|
||||
last_modified_time timestamptz(6),
|
||||
version integer NOT NULL DEFAULT 0,
|
||||
deleted boolean NOT NULL DEFAULT false,
|
||||
CONSTRAINT uk_wechat_isv_app_capability UNIQUE (capability)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE wechat_isv_app_capability IS '微信服务商应用支付能力关联';
|
||||
|
||||
COMMENT ON COLUMN wechat_isv_app_capability.id IS '主键';
|
||||
COMMENT ON COLUMN wechat_isv_app_capability.capability IS '支付能力编码';
|
||||
COMMENT ON COLUMN wechat_isv_app_capability.wechat_isv_app_id IS '关联微信服务商应用ID';
|
||||
COMMENT ON COLUMN wechat_isv_app_capability.creator IS '创建人';
|
||||
COMMENT ON COLUMN wechat_isv_app_capability.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN wechat_isv_app_capability.last_modifier IS '最后修改人';
|
||||
COMMENT ON COLUMN wechat_isv_app_capability.last_modified_time IS '最后修改时间';
|
||||
COMMENT ON COLUMN wechat_isv_app_capability.version IS '乐观锁版本号';
|
||||
COMMENT ON COLUMN wechat_isv_app_capability.deleted IS '逻辑删除标志';
|
||||
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 微信直连商户应用支付能力关联(商户维度,同一通道商户下一个能力绑定一个应用)
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS wechat_direct_app_capability (
|
||||
id bigserial PRIMARY KEY,
|
||||
mch_no varchar(32) NOT NULL,
|
||||
channel_mch_no varchar(32) NOT NULL,
|
||||
capability varchar(64) NOT NULL,
|
||||
wechat_direct_app_id bigint NOT NULL,
|
||||
creator bigint,
|
||||
create_time timestamptz(6) DEFAULT now(),
|
||||
last_modifier bigint,
|
||||
last_modified_time timestamptz(6),
|
||||
version integer NOT NULL DEFAULT 0,
|
||||
deleted boolean NOT NULL DEFAULT false,
|
||||
CONSTRAINT uk_wechat_direct_app_capability UNIQUE (channel_mch_no, capability)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE wechat_direct_app_capability IS '微信直连商户应用支付能力关联';
|
||||
|
||||
COMMENT ON COLUMN wechat_direct_app_capability.id IS '主键';
|
||||
COMMENT ON COLUMN wechat_direct_app_capability.mch_no IS '商户号';
|
||||
COMMENT ON COLUMN wechat_direct_app_capability.channel_mch_no IS '通道商户号';
|
||||
COMMENT ON COLUMN wechat_direct_app_capability.capability IS '支付能力编码';
|
||||
COMMENT ON COLUMN wechat_direct_app_capability.wechat_direct_app_id IS '关联微信直连应用ID';
|
||||
COMMENT ON COLUMN wechat_direct_app_capability.creator IS '创建人';
|
||||
COMMENT ON COLUMN wechat_direct_app_capability.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN wechat_direct_app_capability.last_modifier IS '最后修改人';
|
||||
COMMENT ON COLUMN wechat_direct_app_capability.last_modified_time IS '最后修改时间';
|
||||
COMMENT ON COLUMN wechat_direct_app_capability.version IS '乐观锁版本号';
|
||||
COMMENT ON COLUMN wechat_direct_app_capability.deleted IS '逻辑删除标志';
|
||||
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 抖音直连商户应用支付能力关联
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS douyin_direct_app_capability (
|
||||
id bigserial PRIMARY KEY,
|
||||
mch_no varchar(32) NOT NULL,
|
||||
channel_mch_no varchar(32) NOT NULL,
|
||||
capability varchar(64) NOT NULL,
|
||||
douyin_direct_app_id bigint NOT NULL,
|
||||
creator bigint,
|
||||
create_time timestamptz(6) DEFAULT now(),
|
||||
last_modifier bigint,
|
||||
last_modified_time timestamptz(6),
|
||||
version integer NOT NULL DEFAULT 0,
|
||||
deleted boolean NOT NULL DEFAULT false,
|
||||
CONSTRAINT uk_douyin_direct_app_capability UNIQUE (channel_mch_no, capability)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE douyin_direct_app_capability IS '抖音直连商户应用支付能力关联';
|
||||
|
||||
COMMENT ON COLUMN douyin_direct_app_capability.id IS '主键';
|
||||
COMMENT ON COLUMN douyin_direct_app_capability.mch_no IS '商户号';
|
||||
COMMENT ON COLUMN douyin_direct_app_capability.channel_mch_no IS '通道商户号';
|
||||
COMMENT ON COLUMN douyin_direct_app_capability.capability IS '支付能力编码';
|
||||
COMMENT ON COLUMN douyin_direct_app_capability.douyin_direct_app_id IS '关联抖音直连应用ID';
|
||||
COMMENT ON COLUMN douyin_direct_app_capability.creator IS '创建人';
|
||||
COMMENT ON COLUMN douyin_direct_app_capability.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN douyin_direct_app_capability.last_modifier IS '最后修改人';
|
||||
COMMENT ON COLUMN douyin_direct_app_capability.last_modified_time IS '最后修改时间';
|
||||
COMMENT ON COLUMN douyin_direct_app_capability.version IS '乐观锁版本号';
|
||||
COMMENT ON COLUMN douyin_direct_app_capability.deleted IS '逻辑删除标志';
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.daxpay.open.channel.douyin.code;
|
||||
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.PayCapabilityEnum;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/// # 抖音应用类型与支付能力映射
|
||||
///
|
||||
/// 定义抖音直连商户应用([cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectApp].appType)的常量,
|
||||
/// 以及支付能力(PayCapabilityEnum)到应用类型的默认推导关系。
|
||||
///
|
||||
/// 当未显式配置「能力→应用」关联时,[cn.daxpay.open.channel.douyin.service.direct.DouyinDirectAppCapabilityService]
|
||||
/// 会依据本映射按能力自动推导出合适的 appType,进而在应用列表中匹配对应应用。
|
||||
///
|
||||
public final class DouyinAppTypeCode {
|
||||
|
||||
private DouyinAppTypeCode() {
|
||||
}
|
||||
|
||||
/// 应用类型: 小程序
|
||||
public static final String MINI_PROGRAM = "mini_program";
|
||||
|
||||
/// 应用类型: 移动应用
|
||||
public static final String MOBILE_APP = "mobile_app";
|
||||
|
||||
/// 应用类型: 网站应用
|
||||
public static final String WEB_APP = "web_app";
|
||||
|
||||
/// 支付能力 → 应用类型的默认推导映射
|
||||
///
|
||||
/// - 小程序支付 → 小程序
|
||||
/// - APP支付 → 移动应用
|
||||
/// - 扫码/H5 → 网站应用
|
||||
private static final Map<String, String> CAPABILITY_APP_TYPE_MAP = Map.of(
|
||||
PayCapabilityEnum.DOUYIN_JSAPI.getCode(), MINI_PROGRAM,
|
||||
PayCapabilityEnum.DOUYIN_APP.getCode(), MOBILE_APP,
|
||||
PayCapabilityEnum.DOUYIN_QR.getCode(), WEB_APP,
|
||||
PayCapabilityEnum.DOUYIN_H5.getCode(), WEB_APP);
|
||||
|
||||
/// 根据支付能力编码推导默认应用类型;未知能力返回 null
|
||||
public static String resolveAppType(String capabilityCode) {
|
||||
if (capabilityCode == null) {
|
||||
return null;
|
||||
}
|
||||
return CAPABILITY_APP_TYPE_MAP.get(capabilityCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.daxpay.open.channel.douyin.code;
|
||||
|
||||
import cn.daxpay.open.platform.core.i18n.I18nSupport;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/// # 抖音直连商户应用类型
|
||||
///
|
||||
/// 定义抖音直连商户应用([cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectApp].appType)的取值范围。
|
||||
///
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum DouyinDirectAppTypeEnum implements I18nSupport {
|
||||
/// 小程序
|
||||
MINI_PROGRAM("mini_program"),
|
||||
/// 移动应用
|
||||
MOBILE_APP("mobile_app"),
|
||||
/// 网站应用
|
||||
WEB_APP("web_app"),
|
||||
;
|
||||
|
||||
private final String code;
|
||||
|
||||
/// 翻译 key 前缀
|
||||
@Override
|
||||
public String getI18nPrefix() {
|
||||
return "enum.douyin_direct_app_type";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package cn.daxpay.open.channel.douyin.controller.direct;
|
||||
|
||||
import cn.daxpay.open.channel.douyin.param.direct.DouyinDirectAppCapabilityBatchParam;
|
||||
import cn.daxpay.open.channel.douyin.result.DouyinCapabilityOption;
|
||||
import cn.daxpay.open.channel.douyin.result.direct.DouyinDirectAppCapabilityResult;
|
||||
import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectAppCapabilityService;
|
||||
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:douyin:app")
|
||||
@Validated
|
||||
@Tag(name = "抖音直连商户应用支付能力关联管理")
|
||||
@RestController
|
||||
@RequestMapping("/admin/douyin/mch-app/capability")
|
||||
@RequiredArgsConstructor
|
||||
public class DouyinDirectAppCapabilityController {
|
||||
|
||||
private final DouyinDirectAppCapabilityService douyinDirectAppCapabilityService;
|
||||
|
||||
@PermCode(code = "view", nameCn = "通道商户查看", nameEn = "Channel Merchant View")
|
||||
@Operation(summary = "查询通道商户的能力应用关联列表")
|
||||
@GetMapping("/list-by-channel-mch-no")
|
||||
public Result<List<DouyinDirectAppCapabilityResult>> listByChannelMchNo(
|
||||
@NotBlank(message = "{validation.field.mchNo.notBlank}") String mchNo,
|
||||
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) {
|
||||
return Res.ok(douyinDirectAppCapabilityService.listByChannelMchNo(mchNo, channelMchNo));
|
||||
}
|
||||
|
||||
@PermCode(code = "manage", nameCn = "通道商户管理", nameEn = "Channel Merchant Manage")
|
||||
@Operation(summary = "全量保存能力应用关联")
|
||||
@PostMapping("/save-batch")
|
||||
public Result<Void> saveBatch(@RequestBody @Validated DouyinDirectAppCapabilityBatchParam param) {
|
||||
douyinDirectAppCapabilityService.saveBatch(param);
|
||||
return Res.ok();
|
||||
}
|
||||
|
||||
@PermCode(code = "view", nameCn = "通道商户查看", nameEn = "Channel Merchant View")
|
||||
@Operation(summary = "查询抖音直连支持的支付能力候选")
|
||||
@GetMapping("/list-supported-capabilities")
|
||||
public Result<List<DouyinCapabilityOption>> listSupportedCapabilities() {
|
||||
return Res.ok(douyinDirectAppCapabilityService.listSupportedCapabilities());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.daxpay.open.channel.douyin.convert.direct;
|
||||
|
||||
import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectAppCapability;
|
||||
import cn.daxpay.open.channel.douyin.result.direct.DouyinDirectAppCapabilityResult;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/// # 抖音直连商户应用支付能力关联转换
|
||||
///
|
||||
/// MapStruct转换器,负责关联记录在实体与返回结果之间的转换。
|
||||
///
|
||||
@Mapper
|
||||
public interface DouyinDirectAppCapabilityConvert {
|
||||
|
||||
DouyinDirectAppCapabilityConvert CONVERT = Mappers.getMapper(DouyinDirectAppCapabilityConvert.class);
|
||||
|
||||
/// 转换为返回对象
|
||||
DouyinDirectAppCapabilityResult toResult(DouyinDirectAppCapability entity);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cn.daxpay.open.channel.douyin.dao.direct;
|
||||
|
||||
import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectAppCapability;
|
||||
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 DouyinDirectAppCapabilityManager extends BaseManager<DouyinDirectAppCapabilityMapper, DouyinDirectAppCapability> {
|
||||
|
||||
/// 根据通道商户号查询全部关联(按创建时间升序)
|
||||
public List<DouyinDirectAppCapability> listByChannelMchNo(String channelMchNo) {
|
||||
return lambdaQuery()
|
||||
.eq(DouyinDirectAppCapability::getChannelMchNo, channelMchNo)
|
||||
.orderByAsc(DouyinDirectAppCapability::getCreateTime)
|
||||
.orderByAsc(DouyinDirectAppCapability::getId)
|
||||
.list();
|
||||
}
|
||||
|
||||
/// 根据通道商户号与支付能力查询单条关联(支付时调用)
|
||||
public Optional<DouyinDirectAppCapability> findOne(String channelMchNo, String capability) {
|
||||
return lambdaQuery()
|
||||
.eq(DouyinDirectAppCapability::getChannelMchNo, channelMchNo)
|
||||
.eq(DouyinDirectAppCapability::getCapability, capability)
|
||||
.oneOpt();
|
||||
}
|
||||
|
||||
/// 根据通道商户号删除全部关联(批量保存时先清后插)
|
||||
public void deleteByChannelMchNo(String channelMchNo) {
|
||||
lambdaUpdate()
|
||||
.eq(DouyinDirectAppCapability::getChannelMchNo, channelMchNo)
|
||||
.remove();
|
||||
}
|
||||
|
||||
/// 根据应用ID删除关联(应用被删除时级联清理,避免悬空引用)
|
||||
public void deleteByDouyinDirectAppId(Long douyinDirectAppId) {
|
||||
lambdaUpdate()
|
||||
.eq(DouyinDirectAppCapability::getDouyinDirectAppId, douyinDirectAppId)
|
||||
.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.daxpay.open.channel.douyin.dao.direct;
|
||||
|
||||
import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectAppCapability;
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/// # 抖音直连商户应用支付能力关联 Mapper
|
||||
///
|
||||
/// 继承 MPJBaseMapper 支持多表联查。
|
||||
///
|
||||
@Mapper
|
||||
public interface DouyinDirectAppCapabilityMapper extends MPJBaseMapper<DouyinDirectAppCapability> {
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/// # 抖音直连商户应用
|
||||
///
|
||||
@@ -32,4 +33,25 @@ public class DouyinDirectAppManager extends BaseManager<DouyinDirectAppMapper, D
|
||||
.ne(excludeId != null, DouyinDirectApp::getId, excludeId)
|
||||
.exists();
|
||||
}
|
||||
|
||||
/// 根据通道商户号取首个应用(支付能力推导兜底)
|
||||
public Optional<DouyinDirectApp> findFirstByChannelMchNo(String channelMchNo) {
|
||||
return lambdaQuery()
|
||||
.eq(DouyinDirectApp::getChannelMchNo, channelMchNo)
|
||||
.orderByAsc(DouyinDirectApp::getCreateTime)
|
||||
.orderByAsc(DouyinDirectApp::getId)
|
||||
.last("limit 1")
|
||||
.oneOpt();
|
||||
}
|
||||
|
||||
/// 根据通道商户号与应用类型取首个应用(appType自动推导时调用)
|
||||
public Optional<DouyinDirectApp> findFirstByChannelMchNoAndAppType(String channelMchNo, String appType) {
|
||||
return lambdaQuery()
|
||||
.eq(DouyinDirectApp::getChannelMchNo, channelMchNo)
|
||||
.eq(DouyinDirectApp::getAppType, appType)
|
||||
.orderByAsc(DouyinDirectApp::getCreateTime)
|
||||
.orderByAsc(DouyinDirectApp::getId)
|
||||
.last("limit 1")
|
||||
.oneOpt();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package cn.daxpay.open.channel.douyin.entity.direct;
|
||||
|
||||
import cn.daxpay.open.channel.douyin.convert.direct.DouyinDirectAppCapabilityConvert;
|
||||
import cn.daxpay.open.channel.douyin.result.direct.DouyinDirectAppCapabilityResult;
|
||||
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;
|
||||
|
||||
/// # 抖音直连商户应用支付能力关联
|
||||
///
|
||||
/// 建立通道商户维度下「支付能力 → 直连应用」的绑定关系,供支付时选择正确的抖音应用(AppId)。
|
||||
/// 同一通道商户下,一个支付能力只能绑定一个应用(由唯一约束 channel_mch_no + capability 保证)。
|
||||
///
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "douyin_direct_app_capability", autoResultMap = true)
|
||||
public class DouyinDirectAppCapability extends MchBaseEntity implements ToResult<DouyinDirectAppCapabilityResult> {
|
||||
|
||||
/// 通道商户号
|
||||
@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(指向 douyin_direct_app.id)
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private Long douyinDirectAppId;
|
||||
|
||||
/// 转换
|
||||
@Override
|
||||
public DouyinDirectAppCapabilityResult toResult() {
|
||||
return DouyinDirectAppCapabilityConvert.CONVERT.toResult(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.daxpay.open.channel.douyin.param.direct;
|
||||
|
||||
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 DouyinDirectAppCapabilityBatchParam {
|
||||
|
||||
@NotBlank(message = "{validation.field.mchNo.notBlank}")
|
||||
@Schema(description = "商户号")
|
||||
private String mchNo;
|
||||
|
||||
@NotBlank(message = "{validation.field.channelMchNo.notBlank}")
|
||||
@Schema(description = "通道商户号")
|
||||
private String channelMchNo;
|
||||
|
||||
@Valid
|
||||
@Schema(description = "支付能力关联应用列表")
|
||||
private List<DouyinDirectAppCapabilityItem> items;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.daxpay.open.channel.douyin.param.direct;
|
||||
|
||||
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 DouyinDirectAppCapabilityItem {
|
||||
|
||||
@NotBlank(message = "{validation.field.capability.notBlank}")
|
||||
@Schema(description = "支付能力编码")
|
||||
private String capability;
|
||||
|
||||
@NotNull(message = "{validation.field.douyinDirectAppId.notNull}")
|
||||
@Schema(description = "关联抖音直连应用ID")
|
||||
private Long douyinDirectAppId;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package cn.daxpay.open.channel.douyin.result;
|
||||
|
||||
/// # 抖音支付能力候选项
|
||||
///
|
||||
/// 用于展示抖音直连产品支持的支付能力编码及其国际化名称。
|
||||
///
|
||||
public record DouyinCapabilityOption(String code, String name) {
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.daxpay.open.channel.douyin.result.direct;
|
||||
|
||||
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 填充)。
|
||||
///
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "抖音直连商户应用支付能力关联")
|
||||
public class DouyinDirectAppCapabilityResult extends MchBaseResult {
|
||||
|
||||
@Schema(description = "通道商户号")
|
||||
private String channelMchNo;
|
||||
|
||||
@Schema(description = "支付能力编码")
|
||||
private String capability;
|
||||
|
||||
@Schema(description = "关联抖音直连应用ID")
|
||||
private Long douyinDirectAppId;
|
||||
|
||||
@Schema(description = "应用名称(冗余展示)")
|
||||
private String appName;
|
||||
|
||||
@Schema(description = "抖音应用AppId(冗余展示)")
|
||||
private String douyinAppId;
|
||||
|
||||
@Schema(description = "应用类型(冗余展示): mini_program/mobile_app/web_app")
|
||||
private String appType;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package cn.daxpay.open.channel.douyin.service.direct;
|
||||
|
||||
import cn.daxpay.open.channel.douyin.code.DouyinAppTypeCode;
|
||||
import cn.daxpay.open.channel.douyin.convert.direct.DouyinDirectAppCapabilityConvert;
|
||||
import cn.daxpay.open.channel.douyin.dao.direct.DouyinDirectAppCapabilityManager;
|
||||
import cn.daxpay.open.channel.douyin.dao.direct.DouyinDirectAppManager;
|
||||
import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectApp;
|
||||
import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectAppCapability;
|
||||
import cn.daxpay.open.channel.douyin.param.direct.DouyinDirectAppCapabilityBatchParam;
|
||||
import cn.daxpay.open.channel.douyin.param.direct.DouyinDirectAppCapabilityItem;
|
||||
import cn.daxpay.open.channel.douyin.result.DouyinCapabilityOption;
|
||||
import cn.daxpay.open.channel.douyin.result.direct.DouyinDirectAppCapabilityResult;
|
||||
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.common.util.PaymentStrategyFactory;
|
||||
import cn.daxpay.open.payment.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;
|
||||
|
||||
/// # 抖音直连商户应用支付能力关联
|
||||
///
|
||||
/// 管理通道商户维度下「支付能力 → 直连应用」的绑定关系。
|
||||
/// 支付时通过 [resolveApp] 解析当前能力对应的应用:显式配置 > appType自动推导 > 通道商户首个兜底。
|
||||
///
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DouyinDirectAppCapabilityService {
|
||||
|
||||
private final DouyinDirectAppCapabilityManager capabilityManager;
|
||||
private final DouyinDirectAppManager douyinDirectAppManager;
|
||||
|
||||
/// 查询通道商户下的能力关联列表,并填充应用展示信息(应用名称/AppId/类型)
|
||||
public List<DouyinDirectAppCapabilityResult> listByChannelMchNo(String mchNo, String channelMchNo) {
|
||||
List<DouyinDirectAppCapability> rels = capabilityManager.listByChannelMchNo(channelMchNo);
|
||||
if (rels.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
// 批量查询该通道商户下全部应用,构建 id → app 映射用于填充展示字段
|
||||
Map<Long, DouyinDirectApp> appMap = douyinDirectAppManager.listByMchNoAndChannelMchNo(mchNo, channelMchNo)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(DouyinDirectApp::getId, Function.identity()));
|
||||
return rels.stream()
|
||||
.map(rel -> fillResult(rel, appMap.get(rel.getDouyinDirectAppId())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 全量保存能力关联(先清后插),校验应用归属与能力唯一
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveBatch(DouyinDirectAppCapabilityBatchParam param) {
|
||||
String mchNo = param.getMchNo();
|
||||
String channelMchNo = param.getChannelMchNo();
|
||||
List<DouyinDirectAppCapabilityItem> items = param.getItems();
|
||||
// 先清空旧关联
|
||||
capabilityManager.deleteByChannelMchNo(channelMchNo);
|
||||
if (CollUtil.isEmpty(items)) {
|
||||
return;
|
||||
}
|
||||
// 查询该通道商户下全部应用,校验应用归属
|
||||
List<DouyinDirectApp> apps = douyinDirectAppManager.listByMchNoAndChannelMchNo(mchNo, channelMchNo);
|
||||
Map<Long, DouyinDirectApp> appMap = apps.stream()
|
||||
.collect(Collectors.toMap(DouyinDirectApp::getId, Function.identity()));
|
||||
HashSet<String> capabilitySet = new HashSet<>();
|
||||
for (DouyinDirectAppCapabilityItem item : items) {
|
||||
// 能力不可重复
|
||||
if (!capabilitySet.add(item.getCapability())) {
|
||||
// 抖音: 支付能力[{0}]重复配置
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.douyin.capabilityDuplicate", item.getCapability());
|
||||
}
|
||||
// 应用归属校验
|
||||
if (!appMap.containsKey(item.getDouyinDirectAppId())) {
|
||||
// 抖音: 直连商户应用不存在
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.douyin.mchAppNotFound");
|
||||
}
|
||||
var entity = new DouyinDirectAppCapability()
|
||||
.setChannelMchNo(channelMchNo)
|
||||
.setCapability(item.getCapability())
|
||||
.setDouyinDirectAppId(item.getDouyinDirectAppId());
|
||||
entity.setMchNo(mchNo);
|
||||
capabilityManager.save(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用被删除时级联清理关联,避免悬空引用
|
||||
public void deleteByDouyinDirectAppId(Long douyinDirectAppId) {
|
||||
capabilityManager.deleteByDouyinDirectAppId(douyinDirectAppId);
|
||||
}
|
||||
|
||||
/// 支付时解析当前支付能力对应的应用:显式配置 > appType自动推导 > 通道商户首个兜底
|
||||
///
|
||||
/// @param channelMchNo 通道商户号
|
||||
/// @param capability 支付能力编码
|
||||
/// @return 命中的应用;三者均未命中返回 empty,由调用方兜底回退
|
||||
public Optional<DouyinDirectApp> resolveApp(String channelMchNo, String capability) {
|
||||
if (StrUtil.hasBlank(channelMchNo, capability)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
// 1. 显式配置优先
|
||||
var rel = capabilityManager.findOne(channelMchNo, capability);
|
||||
if (rel.isPresent()) {
|
||||
return douyinDirectAppManager.findById(rel.get().getDouyinDirectAppId());
|
||||
}
|
||||
// 2. 未配置则按能力 → appType 自动推导
|
||||
String appType = DouyinAppTypeCode.resolveAppType(capability);
|
||||
if (appType != null) {
|
||||
Optional<DouyinDirectApp> byType = douyinDirectAppManager.findFirstByChannelMchNoAndAppType(channelMchNo, appType);
|
||||
if (byType.isPresent()) {
|
||||
return byType;
|
||||
}
|
||||
}
|
||||
// 3. 最终兜底:按通道商户号取首个应用
|
||||
return douyinDirectAppManager.findFirstByChannelMchNo(channelMchNo);
|
||||
}
|
||||
|
||||
/// 查询抖音直连产品支持的支付能力候选列表(含国际化名称)
|
||||
public List<DouyinCapabilityOption> listSupportedCapabilities() {
|
||||
AbsProductStrategy strategy = PaymentStrategyFactory.createByProduct(
|
||||
ProductEnum.DOUYIN_PAY.getCode(), AbsProductStrategy.class);
|
||||
return strategy.methodCapabilityMapping().values().stream()
|
||||
.flatMap(List::stream)
|
||||
.distinct()
|
||||
.map(cap -> new DouyinCapabilityOption(cap.getCode(), I18nUtil.getEnumName(cap)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 填充关联结果的应用展示字段
|
||||
private DouyinDirectAppCapabilityResult fillResult(DouyinDirectAppCapability rel, DouyinDirectApp app) {
|
||||
DouyinDirectAppCapabilityResult result = DouyinDirectAppCapabilityConvert.CONVERT.toResult(rel);
|
||||
if (app != null) {
|
||||
result.setAppName(app.getAppName())
|
||||
.setDouyinAppId(app.getDouyinAppId())
|
||||
.setAppType(app.getAppType());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.daxpay.open.channel.douyin.service.direct;
|
||||
|
||||
import cn.daxpay.open.channel.douyin.code.DouyinDirectAppTypeEnum;
|
||||
import cn.daxpay.open.channel.douyin.convert.direct.DouyinDirectAppConvert;
|
||||
import cn.daxpay.open.channel.douyin.dao.direct.DouyinDirectAppManager;
|
||||
import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectApp;
|
||||
@@ -15,6 +16,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/// # 抖音直连商户应用管理
|
||||
@@ -28,6 +30,7 @@ public class DouyinDirectAppService {
|
||||
|
||||
private final DouyinDirectAppManager douyinDirectAppManager;
|
||||
private final DouyinDirectAppAuthConfigService douyinDirectAppAuthConfigService;
|
||||
private final DouyinDirectAppCapabilityService douyinDirectAppCapabilityService;
|
||||
|
||||
/// 根据商户号和通道商户号查询应用列表
|
||||
public List<DouyinDirectAppResult> listByMchNoAndChannelMchNo(String mchNo, String channelMchNo) {
|
||||
@@ -54,6 +57,7 @@ public class DouyinDirectAppService {
|
||||
/// 新增抖音直连商户应用
|
||||
public void add(DouyinDirectAppParam param) {
|
||||
this.assertDouyinAppIdUnique(param.getMchNo(), param.getChannelMchNo(), param.getDouyinAppId(), null);
|
||||
this.validateAppType(param.getAppType());
|
||||
var entity = DouyinDirectAppConvert.CONVERT.toEntity(param);
|
||||
douyinDirectAppManager.save(entity);
|
||||
}
|
||||
@@ -65,6 +69,7 @@ public class DouyinDirectAppService {
|
||||
.orElseThrow(() -> new DataNotExistException("error.channel.douyin.mchAppNotFound"));
|
||||
this.assertScopeMatch(entity, param.getMchNo(), param.getChannelMchNo());
|
||||
this.assertDouyinAppIdUnique(entity.getMchNo(), entity.getChannelMchNo(), param.getDouyinAppId(), param.getId());
|
||||
this.validateAppType(param.getAppType());
|
||||
DouyinDirectAppConvert.CONVERT.copy(param, entity);
|
||||
douyinDirectAppManager.updateById(entity);
|
||||
}
|
||||
@@ -76,6 +81,8 @@ public class DouyinDirectAppService {
|
||||
// 抖音: 直连商户应用不存在
|
||||
.orElseThrow(() -> new DataNotExistException("error.channel.douyin.mchAppNotFound"));
|
||||
douyinDirectAppAuthConfigService.deleteByDouyinDirectAppId(id);
|
||||
// 级联清理支付能力关联,避免悬空引用
|
||||
douyinDirectAppCapabilityService.deleteByDouyinDirectAppId(id);
|
||||
douyinDirectAppManager.deleteById(id);
|
||||
}
|
||||
|
||||
@@ -93,4 +100,13 @@ public class DouyinDirectAppService {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.douyin.mchAppNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
/// 校验应用类型
|
||||
private void validateAppType(String appType) {
|
||||
boolean valid = Arrays.stream(DouyinDirectAppTypeEnum.values())
|
||||
.anyMatch(item -> item.getCode().equals(appType));
|
||||
if (!valid) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.douyin.appTypeInvalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package cn.daxpay.open.channel.wechat.code;
|
||||
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.PayCapabilityEnum;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/// # 微信应用类型与支付能力映射
|
||||
///
|
||||
/// 定义微信应用类型([cn.daxpay.open.channel.wechat.entity.isv.WechatIsvApp].appType)常量,
|
||||
/// 以及支付能力(PayCapabilityEnum)到应用类型的默认推导关系。
|
||||
///
|
||||
/// 当未显式配置「能力→应用」关联时,[cn.daxpay.open.channel.wechat.service.isv.WechatIsvAppCapabilityService]
|
||||
/// 会依据本映射按能力自动推导出合适的 appType,进而在应用列表中匹配对应应用。
|
||||
///
|
||||
public final class WechatAppTypeCode {
|
||||
|
||||
private WechatAppTypeCode() {
|
||||
}
|
||||
|
||||
/// 应用类型: 公众号
|
||||
public static final String OFFICIAL_ACCOUNT = "official_account";
|
||||
|
||||
/// 应用类型: 小程序
|
||||
public static final String MINI_PROGRAM = "mini_program";
|
||||
|
||||
/// 应用类型: 移动应用
|
||||
public static final String MOBILE_APP = "mobile_app";
|
||||
|
||||
/// 支付能力 → 应用类型的默认推导映射
|
||||
///
|
||||
/// - JSAPI支付 → 公众号
|
||||
/// - 小程序支付 → 小程序
|
||||
/// - APP支付 → 移动应用
|
||||
/// - 扫码/H5/付款码 → 公众号(微信扫码/H5/付款码通常使用公众号应用)
|
||||
private static final Map<String, String> CAPABILITY_APP_TYPE_MAP = Map.of(
|
||||
PayCapabilityEnum.WECHAT_JSAPI.getCode(), OFFICIAL_ACCOUNT,
|
||||
PayCapabilityEnum.WECHAT_QR.getCode(), OFFICIAL_ACCOUNT,
|
||||
PayCapabilityEnum.WECHAT_H5.getCode(), OFFICIAL_ACCOUNT,
|
||||
PayCapabilityEnum.WECHAT_BARCODE.getCode(), OFFICIAL_ACCOUNT,
|
||||
PayCapabilityEnum.WECHAT_MINI.getCode(), MINI_PROGRAM,
|
||||
PayCapabilityEnum.WECHAT_APP.getCode(), MOBILE_APP);
|
||||
|
||||
/// 根据支付能力编码推导默认应用类型;未知能力返回 null
|
||||
public static String resolveAppType(String capabilityCode) {
|
||||
if (capabilityCode == null) {
|
||||
return null;
|
||||
}
|
||||
return CAPABILITY_APP_TYPE_MAP.get(capabilityCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.daxpay.open.channel.wechat.code;
|
||||
|
||||
import cn.daxpay.open.platform.core.i18n.I18nSupport;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/// # 微信直连商户应用类型
|
||||
///
|
||||
/// 定义微信直连商户应用([cn.daxpay.open.channel.wechat.entity.direct.WechatDirectApp].appType)的取值范围,
|
||||
/// 与微信服务商应用类型([WechatIsvAppTypeEnum])取值保持一致,便于能力与应用类型统一推导。
|
||||
///
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum WechatDirectAppTypeEnum implements I18nSupport {
|
||||
/// 公众号
|
||||
OFFICIAL_ACCOUNT("official_account"),
|
||||
/// 小程序
|
||||
MINI_PROGRAM("mini_program"),
|
||||
/// 移动应用
|
||||
MOBILE_APP("mobile_app"),
|
||||
;
|
||||
|
||||
private final String code;
|
||||
|
||||
/// 翻译 key 前缀
|
||||
@Override
|
||||
public String getI18nPrefix() {
|
||||
return "enum.wechat_direct_app_type";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package cn.daxpay.open.channel.wechat.controller.direct;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.param.direct.WechatDirectAppCapabilityBatchParam;
|
||||
import cn.daxpay.open.channel.wechat.result.WechatCapabilityOption;
|
||||
import cn.daxpay.open.channel.wechat.result.direct.WechatDirectAppCapabilityResult;
|
||||
import cn.daxpay.open.channel.wechat.service.direct.WechatDirectAppCapabilityService;
|
||||
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/mch-app/capability")
|
||||
@RequiredArgsConstructor
|
||||
public class WechatDirectAppCapabilityController {
|
||||
|
||||
private final WechatDirectAppCapabilityService wechatDirectAppCapabilityService;
|
||||
|
||||
@PermCode(code = "view", nameCn = "通道商户查看", nameEn = "Channel Merchant View")
|
||||
@Operation(summary = "查询通道商户的能力应用关联列表")
|
||||
@GetMapping("/list-by-channel-mch-no")
|
||||
public Result<List<WechatDirectAppCapabilityResult>> listByChannelMchNo(
|
||||
@NotBlank(message = "{validation.field.mchNo.notBlank}") String mchNo,
|
||||
@NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) {
|
||||
return Res.ok(wechatDirectAppCapabilityService.listByChannelMchNo(mchNo, channelMchNo));
|
||||
}
|
||||
|
||||
@PermCode(code = "manage", nameCn = "通道商户管理", nameEn = "Channel Merchant Manage")
|
||||
@Operation(summary = "全量保存能力应用关联")
|
||||
@PostMapping("/save-batch")
|
||||
public Result<Void> saveBatch(@RequestBody @Validated WechatDirectAppCapabilityBatchParam param) {
|
||||
wechatDirectAppCapabilityService.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(wechatDirectAppCapabilityService.listSupportedCapabilities());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cn.daxpay.open.channel.wechat.controller.isv;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.param.isv.WechatIsvAppCapabilityBatchParam;
|
||||
import cn.daxpay.open.channel.wechat.result.WechatCapabilityOption;
|
||||
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvAppCapabilityResult;
|
||||
import cn.daxpay.open.channel.wechat.service.isv.WechatIsvAppCapabilityService;
|
||||
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 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 = "payment:wechat:isv")
|
||||
@Validated
|
||||
@Tag(name = "微信服务商应用支付能力关联管理")
|
||||
@RestController
|
||||
@RequestMapping("/admin/wechat/isv-app/capability")
|
||||
@RequiredArgsConstructor
|
||||
public class WechatIsvAppCapabilityController {
|
||||
|
||||
private final WechatIsvAppCapabilityService wechatIsvAppCapabilityService;
|
||||
|
||||
@PermCode(code = "view", nameCn = "微信服务商查看", nameEn = "WeChat ISV View")
|
||||
@Operation(summary = "查询能力应用关联列表")
|
||||
@GetMapping("/list-all")
|
||||
public Result<List<WechatIsvAppCapabilityResult>> listAll() {
|
||||
return Res.ok(wechatIsvAppCapabilityService.listAll());
|
||||
}
|
||||
|
||||
@PermCode(code = "manage", nameCn = "微信服务商管理", nameEn = "WeChat ISV Manage")
|
||||
@Operation(summary = "全量保存能力应用关联")
|
||||
@PostMapping("/save-batch")
|
||||
public Result<Void> saveBatch(@RequestBody @Validated WechatIsvAppCapabilityBatchParam param) {
|
||||
wechatIsvAppCapabilityService.saveBatch(param);
|
||||
return Res.ok();
|
||||
}
|
||||
|
||||
@PermCode(code = "view", nameCn = "微信服务商查看", nameEn = "WeChat ISV View")
|
||||
@Operation(summary = "查询微信服务商支持的支付能力候选")
|
||||
@GetMapping("/list-supported-capabilities")
|
||||
public Result<List<WechatCapabilityOption>> listSupportedCapabilities() {
|
||||
return Res.ok(wechatIsvAppCapabilityService.listSupportedCapabilities());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.daxpay.open.channel.wechat.convert.direct;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectAppCapability;
|
||||
import cn.daxpay.open.channel.wechat.result.direct.WechatDirectAppCapabilityResult;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/// # 微信直连商户应用支付能力关联转换
|
||||
///
|
||||
/// MapStruct转换器,负责关联记录在实体与返回结果之间的转换。
|
||||
///
|
||||
@Mapper
|
||||
public interface WechatDirectAppCapabilityConvert {
|
||||
|
||||
WechatDirectAppCapabilityConvert CONVERT = Mappers.getMapper(WechatDirectAppCapabilityConvert.class);
|
||||
|
||||
/// 转换为返回对象
|
||||
WechatDirectAppCapabilityResult toResult(WechatDirectAppCapability entity);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.daxpay.open.channel.wechat.convert.isv;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvAppCapability;
|
||||
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvAppCapabilityResult;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/// # 微信服务商应用支付能力关联转换
|
||||
///
|
||||
/// MapStruct转换器,负责关联记录在实体与返回结果之间的转换。
|
||||
///
|
||||
@Mapper
|
||||
public interface WechatIsvAppCapabilityConvert {
|
||||
|
||||
WechatIsvAppCapabilityConvert CONVERT = Mappers.getMapper(WechatIsvAppCapabilityConvert.class);
|
||||
|
||||
/// 转换为返回对象
|
||||
WechatIsvAppCapabilityResult toResult(WechatIsvAppCapability entity);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cn.daxpay.open.channel.wechat.dao.direct;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectAppCapability;
|
||||
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 WechatDirectAppCapabilityManager extends BaseManager<WechatDirectAppCapabilityMapper, WechatDirectAppCapability> {
|
||||
|
||||
/// 根据通道商户号查询全部关联(按创建时间升序)
|
||||
public List<WechatDirectAppCapability> listByChannelMchNo(String channelMchNo) {
|
||||
return lambdaQuery()
|
||||
.eq(WechatDirectAppCapability::getChannelMchNo, channelMchNo)
|
||||
.orderByAsc(WechatDirectAppCapability::getCreateTime)
|
||||
.orderByAsc(WechatDirectAppCapability::getId)
|
||||
.list();
|
||||
}
|
||||
|
||||
/// 根据通道商户号与支付能力查询单条关联(支付时调用)
|
||||
public Optional<WechatDirectAppCapability> findOne(String channelMchNo, String capability) {
|
||||
return lambdaQuery()
|
||||
.eq(WechatDirectAppCapability::getChannelMchNo, channelMchNo)
|
||||
.eq(WechatDirectAppCapability::getCapability, capability)
|
||||
.oneOpt();
|
||||
}
|
||||
|
||||
/// 根据通道商户号删除全部关联(批量保存时先清后插)
|
||||
public void deleteByChannelMchNo(String channelMchNo) {
|
||||
lambdaUpdate()
|
||||
.eq(WechatDirectAppCapability::getChannelMchNo, channelMchNo)
|
||||
.remove();
|
||||
}
|
||||
|
||||
/// 根据应用ID删除关联(应用被删除时级联清理,避免悬空引用)
|
||||
public void deleteByWechatDirectAppId(Long wechatDirectAppId) {
|
||||
lambdaUpdate()
|
||||
.eq(WechatDirectAppCapability::getWechatDirectAppId, wechatDirectAppId)
|
||||
.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.daxpay.open.channel.wechat.dao.direct;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectAppCapability;
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/// # 微信直连商户应用支付能力关联 Mapper
|
||||
///
|
||||
/// 继承 MPJBaseMapper 支持多表联查。
|
||||
///
|
||||
@Mapper
|
||||
public interface WechatDirectAppCapabilityMapper extends MPJBaseMapper<WechatDirectAppCapability> {
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/// # 微信直连商户应用
|
||||
///
|
||||
@@ -23,6 +24,23 @@ public class WechatDirectAppManager extends BaseManager<WechatDirectAppMapper, W
|
||||
.list();
|
||||
}
|
||||
|
||||
/// 按通道商户号查询首个应用(能力解析兜底时使用)
|
||||
public Optional<WechatDirectApp> findFirstByChannelMchNo(String channelMchNo) {
|
||||
return firstOpt(q -> q
|
||||
.eq(WechatDirectApp::getChannelMchNo, channelMchNo)
|
||||
.orderByAsc(WechatDirectApp::getCreateTime)
|
||||
.orderByAsc(WechatDirectApp::getId));
|
||||
}
|
||||
|
||||
/// 按通道商户号与应用类型查询首个应用(能力→应用类型推导时使用)
|
||||
public Optional<WechatDirectApp> findFirstByChannelMchNoAndAppType(String channelMchNo, String appType) {
|
||||
return firstOpt(q -> q
|
||||
.eq(WechatDirectApp::getChannelMchNo, channelMchNo)
|
||||
.eq(WechatDirectApp::getAppType, appType)
|
||||
.orderByAsc(WechatDirectApp::getCreateTime)
|
||||
.orderByAsc(WechatDirectApp::getId));
|
||||
}
|
||||
|
||||
/// 校验同一通道商户下wxAppId是否已存在(排除自身)
|
||||
public boolean existsByChannelMchNoAndWxAppId(String mchNo, String channelMchNo, String wxAppId, Long excludeId) {
|
||||
return lambdaQuery()
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.daxpay.open.channel.wechat.dao.isv;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvAppCapability;
|
||||
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 WechatIsvAppCapabilityManager extends BaseManager<WechatIsvAppCapabilityMapper, WechatIsvAppCapability> {
|
||||
|
||||
/// 查询全部关联(按创建时间升序)
|
||||
public List<WechatIsvAppCapability> listAll() {
|
||||
return lambdaQuery()
|
||||
.orderByAsc(WechatIsvAppCapability::getCreateTime)
|
||||
.orderByAsc(WechatIsvAppCapability::getId)
|
||||
.list();
|
||||
}
|
||||
|
||||
/// 根据支付能力查询单条关联(支付时调用)
|
||||
public Optional<WechatIsvAppCapability> findOne(String capability) {
|
||||
return lambdaQuery()
|
||||
.eq(WechatIsvAppCapability::getCapability, capability)
|
||||
.oneOpt();
|
||||
}
|
||||
|
||||
/// 全量删除(批量保存时先清后插)
|
||||
public void deleteAll() {
|
||||
lambdaUpdate().remove();
|
||||
}
|
||||
|
||||
/// 根据应用ID删除关联(应用被删除时级联清理,避免悬空引用)
|
||||
public void deleteByWechatIsvAppId(Long wechatIsvAppId) {
|
||||
lambdaUpdate()
|
||||
.eq(WechatIsvAppCapability::getWechatIsvAppId, wechatIsvAppId)
|
||||
.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.daxpay.open.channel.wechat.dao.isv;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvAppCapability;
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/// # 微信服务商应用支付能力关联 Mapper
|
||||
///
|
||||
/// 继承 MPJBaseMapper 支持多表联查。
|
||||
///
|
||||
@Mapper
|
||||
public interface WechatIsvAppCapabilityMapper extends MPJBaseMapper<WechatIsvAppCapability> {
|
||||
}
|
||||
@@ -31,6 +31,14 @@ public class WechatIsvAppManager extends BaseManager<WechatIsvAppMapper, WechatI
|
||||
.orderByAsc(WechatIsvApp::getId));
|
||||
}
|
||||
|
||||
/// 按应用类型查询首个应用(能力→应用类型推导时使用)
|
||||
public Optional<WechatIsvApp> findFirstByAppType(String appType) {
|
||||
return firstOpt(q -> q
|
||||
.eq(WechatIsvApp::getAppType, appType)
|
||||
.orderByAsc(WechatIsvApp::getCreateTime)
|
||||
.orderByAsc(WechatIsvApp::getId));
|
||||
}
|
||||
|
||||
/// 校验微信应用AppId是否已存在(排除自身)
|
||||
public boolean existsByWxAppId(String wxAppId, Long excludeId) {
|
||||
return lambdaQuery()
|
||||
|
||||
@@ -28,6 +28,10 @@ public class WechatDirectApp extends MchBaseEntity implements ToResult<WechatDir
|
||||
/// 应用名称
|
||||
private String appName;
|
||||
|
||||
/// 应用类型
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private String appType;
|
||||
|
||||
/// 微信应用AppId
|
||||
private String wxAppId;
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package cn.daxpay.open.channel.wechat.entity.direct;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.convert.direct.WechatDirectAppCapabilityConvert;
|
||||
import cn.daxpay.open.channel.wechat.result.direct.WechatDirectAppCapabilityResult;
|
||||
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;
|
||||
|
||||
/// # 微信直连商户应用支付能力关联
|
||||
///
|
||||
/// 建立通道商户维度下「支付能力 → 直连应用」的绑定关系,供支付时选择正确的微信应用(AppId)。
|
||||
/// 同一通道商户下,一个支付能力只能绑定一个应用(由唯一约束 channel_mch_no + capability 保证)。
|
||||
///
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "wechat_direct_app_capability", autoResultMap = true)
|
||||
public class WechatDirectAppCapability extends MchBaseEntity implements ToResult<WechatDirectAppCapabilityResult> {
|
||||
|
||||
/// 通道商户号
|
||||
@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_direct_app.id)
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private Long wechatDirectAppId;
|
||||
|
||||
/// 转换
|
||||
@Override
|
||||
public WechatDirectAppCapabilityResult toResult() {
|
||||
return WechatDirectAppCapabilityConvert.CONVERT.toResult(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cn.daxpay.open.channel.wechat.entity.isv;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.convert.isv.WechatIsvAppCapabilityConvert;
|
||||
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvAppCapabilityResult;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.base.MpBaseEntity;
|
||||
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;
|
||||
|
||||
/// # 微信服务商应用支付能力关联
|
||||
///
|
||||
/// 建立全局维度下「支付能力 → 服务商应用」的绑定关系,供支付时选择正确的服务商应用(微信AppId)。
|
||||
/// 微信服务商应用全局共享(密钥全局唯一),故本表不挂通道商户号,同一支付能力全局只能绑定一个应用。
|
||||
///
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "wechat_isv_app_capability", autoResultMap = true)
|
||||
public class WechatIsvAppCapability extends MpBaseEntity implements ToResult<WechatIsvAppCapabilityResult> {
|
||||
|
||||
/// 支付能力编码
|
||||
/// @see cn.daxpay.open.platform.core.enums.pay.channel.PayCapabilityEnum
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private String capability;
|
||||
|
||||
/// 关联微信服务商应用ID(指向 wechat_isv_app.id)
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private Long wechatIsvAppId;
|
||||
|
||||
/// 转换
|
||||
@Override
|
||||
public WechatIsvAppCapabilityResult toResult() {
|
||||
return WechatIsvAppCapabilityConvert.CONVERT.toResult(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.daxpay.open.channel.wechat.param.direct;
|
||||
|
||||
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 WechatDirectAppCapabilityBatchParam {
|
||||
|
||||
@NotBlank(message = "{validation.field.mchNo.notBlank}")
|
||||
@Schema(description = "商户号")
|
||||
private String mchNo;
|
||||
|
||||
@NotBlank(message = "{validation.field.channelMchNo.notBlank}")
|
||||
@Schema(description = "通道商户号")
|
||||
private String channelMchNo;
|
||||
|
||||
@Valid
|
||||
@Schema(description = "支付能力关联应用列表")
|
||||
private List<WechatDirectAppCapabilityItem> items;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.daxpay.open.channel.wechat.param.direct;
|
||||
|
||||
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 WechatDirectAppCapabilityItem {
|
||||
|
||||
@NotBlank(message = "{validation.field.capability.notBlank}")
|
||||
@Schema(description = "支付能力编码")
|
||||
private String capability;
|
||||
|
||||
@NotNull(message = "{validation.field.wechatDirectAppId.notNull}")
|
||||
@Schema(description = "关联微信直连应用ID")
|
||||
private Long wechatDirectAppId;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import lombok.experimental.Accessors;
|
||||
|
||||
/// # 微信直连商户应用保存参数
|
||||
///
|
||||
/// 保存/更新微信直连商户应用时接收的请求参数,含商户号、通道商户号、应用名称和微信应用AppId。
|
||||
/// 保存/更新微信直连商户应用时接收的请求参数,含商户号、通道商户号、应用名称、应用类型和微信应用AppId。
|
||||
///
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@@ -32,6 +32,10 @@ public class WechatDirectAppParam {
|
||||
@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;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package cn.daxpay.open.channel.wechat.param.isv;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// # 微信服务商支付能力关联应用批量保存参数
|
||||
///
|
||||
/// 全量覆盖「支付能力 → 服务商应用」的绑定关系:先清除旧记录,再按 items 批量插入。
|
||||
/// items 为空表示清空所有绑定。
|
||||
///
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "微信服务商支付能力关联应用批量保存参数")
|
||||
public class WechatIsvAppCapabilityBatchParam {
|
||||
|
||||
@Valid
|
||||
@Schema(description = "支付能力关联应用列表")
|
||||
private List<WechatIsvAppCapabilityItem> items;
|
||||
}
|
||||
@@ -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 WechatIsvAppCapabilityItem {
|
||||
|
||||
@NotBlank(message = "{validation.field.capability.notBlank}")
|
||||
@Schema(description = "支付能力编码")
|
||||
private String capability;
|
||||
|
||||
@NotNull(message = "{validation.field.wechatIsvAppId.notNull}")
|
||||
@Schema(description = "关联微信服务商应用ID")
|
||||
private Long wechatIsvAppId;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package cn.daxpay.open.channel.wechat.result;
|
||||
|
||||
/// # 微信支付能力候选项
|
||||
///
|
||||
/// 用于展示微信产品(直连/服务商)支持的支付能力编码及其国际化名称。
|
||||
/// 直连与服务商共用本结构。
|
||||
///
|
||||
public record WechatCapabilityOption(String code, String name) {
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.daxpay.open.channel.wechat.result.direct;
|
||||
|
||||
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 填充)。
|
||||
///
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "微信直连商户应用支付能力关联")
|
||||
public class WechatDirectAppCapabilityResult extends MchBaseResult {
|
||||
|
||||
@Schema(description = "通道商户号")
|
||||
private String channelMchNo;
|
||||
|
||||
@Schema(description = "支付能力编码")
|
||||
private String capability;
|
||||
|
||||
@Schema(description = "关联微信直连应用ID")
|
||||
private Long wechatDirectAppId;
|
||||
|
||||
@Schema(description = "应用名称(冗余展示)")
|
||||
private String appName;
|
||||
|
||||
@Schema(description = "微信应用AppId(冗余展示)")
|
||||
private String wxAppId;
|
||||
|
||||
@Schema(description = "应用类型(冗余展示): official_account/mini_program/mobile_app")
|
||||
private String appType;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import lombok.experimental.Accessors;
|
||||
|
||||
/// # 微信直连商户应用
|
||||
///
|
||||
/// 微信直连商户应用的返回结果对象,包含通道商户号、应用名称和微信应用AppId等信息。
|
||||
/// 微信直连商户应用的返回结果对象,包含通道商户号、应用名称、应用类型和微信应用AppId等信息。
|
||||
///
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@@ -22,6 +22,9 @@ public class WechatDirectAppResult extends MchTradeBaseResult {
|
||||
@Schema(description = "应用名称")
|
||||
private String appName;
|
||||
|
||||
@Schema(description = "应用类型")
|
||||
private String appType;
|
||||
|
||||
@Schema(description = "微信应用AppId")
|
||||
private String wxAppId;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.daxpay.open.channel.wechat.result.isv;
|
||||
|
||||
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;
|
||||
|
||||
/// # 微信服务商应用支付能力关联
|
||||
///
|
||||
/// 展示各支付能力绑定的服务商应用信息,含应用名称/AppId/类型等冗余字段(由 Service 填充)。
|
||||
///
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "微信服务商应用支付能力关联")
|
||||
public class WechatIsvAppCapabilityResult extends BaseResult {
|
||||
|
||||
@Schema(description = "支付能力编码")
|
||||
private String capability;
|
||||
|
||||
@Schema(description = "关联微信服务商应用ID")
|
||||
private Long wechatIsvAppId;
|
||||
|
||||
@Schema(description = "应用名称(冗余展示)")
|
||||
private String appName;
|
||||
|
||||
@Schema(description = "微信应用AppId(冗余展示)")
|
||||
private String wxAppId;
|
||||
|
||||
@Schema(description = "应用类型(冗余展示): official_account/mini_program/mobile_app")
|
||||
private String appType;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package cn.daxpay.open.channel.wechat.service.direct;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.code.WechatAppTypeCode;
|
||||
import cn.daxpay.open.channel.wechat.convert.direct.WechatDirectAppCapabilityConvert;
|
||||
import cn.daxpay.open.channel.wechat.dao.direct.WechatDirectAppCapabilityManager;
|
||||
import cn.daxpay.open.channel.wechat.dao.direct.WechatDirectAppManager;
|
||||
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectApp;
|
||||
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectAppCapability;
|
||||
import cn.daxpay.open.channel.wechat.param.direct.WechatDirectAppCapabilityBatchParam;
|
||||
import cn.daxpay.open.channel.wechat.param.direct.WechatDirectAppCapabilityItem;
|
||||
import cn.daxpay.open.channel.wechat.result.WechatCapabilityOption;
|
||||
import cn.daxpay.open.channel.wechat.result.direct.WechatDirectAppCapabilityResult;
|
||||
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.common.util.PaymentStrategyFactory;
|
||||
import cn.daxpay.open.payment.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;
|
||||
|
||||
/// # 微信直连商户应用支付能力关联
|
||||
///
|
||||
/// 管理通道商户维度下「支付能力 → 直连应用」的绑定关系。
|
||||
/// 支付时通过 [resolveApp] 解析当前能力对应的应用:显式配置 > appType自动推导 > 通道商户首个兜底。
|
||||
///
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WechatDirectAppCapabilityService {
|
||||
|
||||
private final WechatDirectAppCapabilityManager capabilityManager;
|
||||
private final WechatDirectAppManager wechatDirectAppManager;
|
||||
|
||||
/// 查询通道商户下的能力关联列表,并填充应用展示信息(应用名称/AppId/类型)
|
||||
public List<WechatDirectAppCapabilityResult> listByChannelMchNo(String mchNo, String channelMchNo) {
|
||||
List<WechatDirectAppCapability> rels = capabilityManager.listByChannelMchNo(channelMchNo);
|
||||
if (rels.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
// 批量查询该通道商户下全部应用,构建 id → app 映射用于填充展示字段
|
||||
Map<Long, WechatDirectApp> appMap = wechatDirectAppManager.listByMchNoAndChannelMchNo(mchNo, channelMchNo)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(WechatDirectApp::getId, Function.identity()));
|
||||
return rels.stream()
|
||||
.map(rel -> fillResult(rel, appMap.get(rel.getWechatDirectAppId())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 全量保存能力关联(先清后插),校验应用归属与能力唯一
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveBatch(WechatDirectAppCapabilityBatchParam param) {
|
||||
String mchNo = param.getMchNo();
|
||||
String channelMchNo = param.getChannelMchNo();
|
||||
List<WechatDirectAppCapabilityItem> items = param.getItems();
|
||||
// 先清空旧关联
|
||||
capabilityManager.deleteByChannelMchNo(channelMchNo);
|
||||
if (CollUtil.isEmpty(items)) {
|
||||
return;
|
||||
}
|
||||
// 查询该通道商户下全部应用,校验应用归属
|
||||
List<WechatDirectApp> apps = wechatDirectAppManager.listByMchNoAndChannelMchNo(mchNo, channelMchNo);
|
||||
Map<Long, WechatDirectApp> appMap = apps.stream()
|
||||
.collect(Collectors.toMap(WechatDirectApp::getId, Function.identity()));
|
||||
HashSet<String> capabilitySet = new HashSet<>();
|
||||
for (WechatDirectAppCapabilityItem 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.getWechatDirectAppId())) {
|
||||
// 微信: 直连商户应用不存在
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.wechat.mchAppNotFound");
|
||||
}
|
||||
var entity = new WechatDirectAppCapability()
|
||||
.setChannelMchNo(channelMchNo)
|
||||
.setCapability(item.getCapability())
|
||||
.setWechatDirectAppId(item.getWechatDirectAppId());
|
||||
entity.setMchNo(mchNo);
|
||||
capabilityManager.save(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用被删除时级联清理关联,避免悬空引用
|
||||
public void deleteByWechatDirectAppId(Long wechatDirectAppId) {
|
||||
capabilityManager.deleteByWechatDirectAppId(wechatDirectAppId);
|
||||
}
|
||||
|
||||
/// 支付时解析当前支付能力对应的应用:显式配置 > appType自动推导 > 通道商户首个兜底
|
||||
///
|
||||
/// @param channelMchNo 通道商户号
|
||||
/// @param capability 支付能力编码
|
||||
/// @return 命中的应用;三者均未命中返回 empty,由调用方兜底回退
|
||||
public Optional<WechatDirectApp> resolveApp(String channelMchNo, String capability) {
|
||||
if (StrUtil.hasBlank(channelMchNo, capability)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
// 1. 显式配置优先
|
||||
var rel = capabilityManager.findOne(channelMchNo, capability);
|
||||
if (rel.isPresent()) {
|
||||
return wechatDirectAppManager.findById(rel.get().getWechatDirectAppId());
|
||||
}
|
||||
// 2. 未配置则按能力 → appType 自动推导
|
||||
String appType = WechatAppTypeCode.resolveAppType(capability);
|
||||
if (appType != null) {
|
||||
Optional<WechatDirectApp> byType = wechatDirectAppManager.findFirstByChannelMchNoAndAppType(channelMchNo, appType);
|
||||
if (byType.isPresent()) {
|
||||
return byType;
|
||||
}
|
||||
}
|
||||
// 3. 最终兜底:按通道商户号取首个应用
|
||||
return wechatDirectAppManager.findFirstByChannelMchNo(channelMchNo);
|
||||
}
|
||||
|
||||
/// 查询微信直连产品支持的支付能力候选列表(含国际化名称)
|
||||
public List<WechatCapabilityOption> listSupportedCapabilities() {
|
||||
AbsProductStrategy strategy = PaymentStrategyFactory.createByProduct(
|
||||
ProductEnum.WECHAT_PAY.getCode(), AbsProductStrategy.class);
|
||||
return strategy.methodCapabilityMapping().values().stream()
|
||||
.flatMap(List::stream)
|
||||
.distinct()
|
||||
.map(cap -> new WechatCapabilityOption(cap.getCode(), I18nUtil.getEnumName(cap)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 填充关联结果的应用展示字段
|
||||
private WechatDirectAppCapabilityResult fillResult(WechatDirectAppCapability rel, WechatDirectApp app) {
|
||||
WechatDirectAppCapabilityResult result = WechatDirectAppCapabilityConvert.CONVERT.toResult(rel);
|
||||
if (app != null) {
|
||||
result.setAppName(app.getAppName())
|
||||
.setWxAppId(app.getWxAppId())
|
||||
.setAppType(app.getAppType());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package cn.daxpay.open.channel.wechat.service.direct;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.code.WechatDirectAppTypeEnum;
|
||||
import cn.daxpay.open.channel.wechat.convert.direct.WechatDirectAppConvert;
|
||||
import cn.daxpay.open.channel.wechat.dao.direct.WechatDirectAppManager;
|
||||
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectApp;
|
||||
import cn.daxpay.open.channel.wechat.param.direct.WechatDirectAppParam;
|
||||
import cn.daxpay.open.channel.wechat.result.direct.WechatDirectAppResult;
|
||||
import cn.daxpay.open.channel.wechat.service.direct.WechatDirectAppAuthConfigService;
|
||||
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;
|
||||
@@ -16,11 +16,12 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/// # 微信直连商户应用管理
|
||||
///
|
||||
/// 提供直连商户应用的增删改查功能,包含同一通道商户下应用ID的唯一性校验和操作范围校验(防止跨商户操作)。
|
||||
/// 提供直连商户应用的增删改查功能,包含同一通道商户下应用ID的唯一性校验、应用类型校验和操作范围校验(防止跨商户操作)。
|
||||
///
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -29,6 +30,7 @@ public class WechatDirectAppService {
|
||||
|
||||
private final WechatDirectAppManager wechatDirectAppManager;
|
||||
private final WechatDirectAppAuthConfigService wechatDirectAppAuthConfigService;
|
||||
private final WechatDirectAppCapabilityService wechatDirectAppCapabilityService;
|
||||
|
||||
/// 根据商户号和通道商户号查询应用列表
|
||||
public List<WechatDirectAppResult> listByMchNoAndChannelMchNo(String mchNo, String channelMchNo) {
|
||||
@@ -55,6 +57,7 @@ public class WechatDirectAppService {
|
||||
/// 新增微信直连商户应用
|
||||
public void add(WechatDirectAppParam param) {
|
||||
this.assertWxAppIdUnique(param.getMchNo(), param.getChannelMchNo(), param.getWxAppId(), null);
|
||||
this.validateAppType(param.getAppType());
|
||||
var entity = WechatDirectAppConvert.CONVERT.toEntity(param);
|
||||
wechatDirectAppManager.save(entity);
|
||||
}
|
||||
@@ -66,17 +69,19 @@ public class WechatDirectAppService {
|
||||
.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());
|
||||
WechatDirectAppConvert.CONVERT.copy(param, entity);
|
||||
wechatDirectAppManager.updateById(entity);
|
||||
}
|
||||
|
||||
/// 删除应用(级联删除授权认证配置)
|
||||
/// 删除应用(级联删除授权认证配置与能力关联)
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long id) {
|
||||
wechatDirectAppManager.findById(id)
|
||||
// 微信: 直连商户应用不存在
|
||||
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.mchAppNotFound"));
|
||||
wechatDirectAppAuthConfigService.deleteByWechatDirectAppId(id);
|
||||
wechatDirectAppCapabilityService.deleteByWechatDirectAppId(id);
|
||||
wechatDirectAppManager.deleteById(id);
|
||||
}
|
||||
|
||||
@@ -94,4 +99,13 @@ public class WechatDirectAppService {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.wechat.mchAppNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
/// 校验应用类型
|
||||
private void validateAppType(String appType) {
|
||||
boolean valid = Arrays.stream(WechatDirectAppTypeEnum.values())
|
||||
.anyMatch(item -> item.getCode().equals(appType));
|
||||
if (!valid) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.wechat.appTypeInvalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package cn.daxpay.open.channel.wechat.service.isv;
|
||||
|
||||
import cn.daxpay.open.channel.wechat.code.WechatAppTypeCode;
|
||||
import cn.daxpay.open.channel.wechat.convert.isv.WechatIsvAppCapabilityConvert;
|
||||
import cn.daxpay.open.channel.wechat.dao.isv.WechatIsvAppCapabilityManager;
|
||||
import cn.daxpay.open.channel.wechat.dao.isv.WechatIsvAppManager;
|
||||
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvApp;
|
||||
import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvAppCapability;
|
||||
import cn.daxpay.open.channel.wechat.param.isv.WechatIsvAppCapabilityBatchParam;
|
||||
import cn.daxpay.open.channel.wechat.param.isv.WechatIsvAppCapabilityItem;
|
||||
import cn.daxpay.open.channel.wechat.result.WechatCapabilityOption;
|
||||
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvAppCapabilityResult;
|
||||
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.common.util.PaymentStrategyFactory;
|
||||
import cn.daxpay.open.payment.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;
|
||||
|
||||
/// # 微信服务商应用支付能力关联
|
||||
///
|
||||
/// 管理全局维度下「支付能力 → 服务商应用」的绑定关系(微信服务商应用全局共享)。
|
||||
/// 支付时通过 [resolveApp] 解析当前能力对应的应用:显式配置 > appType自动推导 > 取首个兜底。
|
||||
///
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WechatIsvAppCapabilityService {
|
||||
|
||||
private final WechatIsvAppCapabilityManager capabilityManager;
|
||||
private final WechatIsvAppManager wechatIsvAppManager;
|
||||
|
||||
/// 查询能力关联列表,并填充应用展示信息(应用名称/AppId/类型)
|
||||
public List<WechatIsvAppCapabilityResult> listAll() {
|
||||
List<WechatIsvAppCapability> rels = capabilityManager.listAll();
|
||||
if (rels.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
// 批量查询全部服务商应用,构建 id → app 映射用于填充展示字段
|
||||
Map<Long, WechatIsvApp> appMap = wechatIsvAppManager.listAll().stream()
|
||||
.collect(Collectors.toMap(WechatIsvApp::getId, Function.identity()));
|
||||
return rels.stream()
|
||||
.map(rel -> fillResult(rel, appMap.get(rel.getWechatIsvAppId())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 全量保存能力关联(先清后插),校验应用存在与能力唯一
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveBatch(WechatIsvAppCapabilityBatchParam param) {
|
||||
List<WechatIsvAppCapabilityItem> items = param.getItems();
|
||||
// 先清空旧关联
|
||||
capabilityManager.deleteAll();
|
||||
if (CollUtil.isEmpty(items)) {
|
||||
return;
|
||||
}
|
||||
// 查询全部服务商应用,校验应用存在
|
||||
Map<Long, WechatIsvApp> appMap = wechatIsvAppManager.listAll().stream()
|
||||
.collect(Collectors.toMap(WechatIsvApp::getId, Function.identity()));
|
||||
HashSet<String> capabilitySet = new HashSet<>();
|
||||
for (WechatIsvAppCapabilityItem 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.getWechatIsvAppId())) {
|
||||
// 微信: 服务商应用不存在
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.wechat.appNotFound");
|
||||
}
|
||||
var entity = new WechatIsvAppCapability()
|
||||
.setCapability(item.getCapability())
|
||||
.setWechatIsvAppId(item.getWechatIsvAppId());
|
||||
capabilityManager.save(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用被删除时级联清理关联,避免悬空引用
|
||||
public void deleteByWechatIsvAppId(Long wechatIsvAppId) {
|
||||
capabilityManager.deleteByWechatIsvAppId(wechatIsvAppId);
|
||||
}
|
||||
|
||||
/// 支付时解析当前支付能力对应的应用:显式配置 > appType自动推导 > 取首个兜底
|
||||
///
|
||||
/// @param capability 支付能力编码
|
||||
/// @return 命中的应用;三者均未命中返回 empty,由调用方兜底回退
|
||||
public Optional<WechatIsvApp> resolveApp(String capability) {
|
||||
if (StrUtil.isBlank(capability)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
// 1. 显式配置优先
|
||||
var rel = capabilityManager.findOne(capability);
|
||||
if (rel.isPresent()) {
|
||||
return wechatIsvAppManager.findById(rel.get().getWechatIsvAppId());
|
||||
}
|
||||
// 2. 未配置则按能力 → appType 自动推导
|
||||
String appType = WechatAppTypeCode.resolveAppType(capability);
|
||||
if (appType != null) {
|
||||
Optional<WechatIsvApp> byType = wechatIsvAppManager.findFirstByAppType(appType);
|
||||
if (byType.isPresent()) {
|
||||
return byType;
|
||||
}
|
||||
}
|
||||
// 3. 最终兜底:取首个服务商应用
|
||||
return wechatIsvAppManager.findFirst();
|
||||
}
|
||||
|
||||
/// 查询微信服务商产品支持的支付能力候选列表(含国际化名称)
|
||||
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 WechatIsvAppCapabilityResult fillResult(WechatIsvAppCapability rel, WechatIsvApp app) {
|
||||
WechatIsvAppCapabilityResult result = WechatIsvAppCapabilityConvert.CONVERT.toResult(rel);
|
||||
if (app != null) {
|
||||
result.setAppName(app.getAppName())
|
||||
.setWxAppId(app.getWxAppId())
|
||||
.setAppType(app.getAppType());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvApp;
|
||||
import cn.daxpay.open.channel.wechat.code.WechatIsvAppTypeEnum;
|
||||
import cn.daxpay.open.channel.wechat.param.isv.WechatIsvAppParam;
|
||||
import cn.daxpay.open.channel.wechat.result.isv.WechatIsvAppResult;
|
||||
import cn.daxpay.open.channel.wechat.service.isv.WechatIsvAppAuthConfigService;
|
||||
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;
|
||||
@@ -29,6 +28,7 @@ public class WechatIsvAppService {
|
||||
|
||||
private final WechatIsvAppManager wechatIsvAppManager;
|
||||
private final WechatIsvAppAuthConfigService wechatIsvAppAuthConfigService;
|
||||
private final WechatIsvAppCapabilityService wechatIsvAppCapabilityService;
|
||||
|
||||
/// 查询全部应用列表
|
||||
public List<WechatIsvAppResult> listAll() {
|
||||
@@ -71,16 +71,18 @@ public class WechatIsvAppService {
|
||||
// 微信: 服务商应用不存在
|
||||
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.appNotFound"));
|
||||
this.assertWxAppIdUnique(param.getWxAppId(), param.getId());
|
||||
this.validateAppType(param.getAppType());
|
||||
WechatIsvAppConvert.CONVERT.copy(param, entity);
|
||||
wechatIsvAppManager.updateById(entity);
|
||||
}
|
||||
|
||||
/// 删除应用
|
||||
/// 删除应用(级联删除授权认证配置与能力关联)
|
||||
public void delete(Long id) {
|
||||
wechatIsvAppManager.findById(id)
|
||||
// 微信: 服务商应用不存在
|
||||
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.appNotFound"));
|
||||
wechatIsvAppAuthConfigService.deleteByWechatIsvAppId(id);
|
||||
wechatIsvAppCapabilityService.deleteByWechatIsvAppId(id);
|
||||
wechatIsvAppManager.deleteById(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"mini_program": "Mini Program",
|
||||
"mobile_app": "Mobile App",
|
||||
"web_app": "Web App"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"official_account": "Official Account",
|
||||
"mini_program": "Mini Program",
|
||||
"mobile_app": "Mobile App"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"official_account": "Official Account",
|
||||
"mini_program": "Mini Program",
|
||||
"mobile_app": "Mobile App"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"mchAppNotFound": "Douyin channel merchant application not found",
|
||||
"appIdDuplicate": "Douyin AppId already exists under this channel merchant",
|
||||
"directMchDuplicate": "This Douyin direct merchant already exists under the merchant",
|
||||
"appTypeInvalid": "Invalid Douyin application type",
|
||||
"capabilityDuplicate": "Payment capability [{0}] is duplicated under this channel merchant"
|
||||
}
|
||||
@@ -44,5 +44,6 @@
|
||||
"publicKeyRequired": "Payment public key is required",
|
||||
"privateCertRequired": "API certificate is required",
|
||||
"subMerchantDuplicate": "This sub-merchant already exists under the WeChat application",
|
||||
"directMchDuplicate": "The WeChat merchant already exists under this merchant"
|
||||
"directMchDuplicate": "The WeChat merchant already exists under this merchant",
|
||||
"capabilityDuplicate": "WeChat payment capability [{0}] is duplicated"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"mini_program": "小程序",
|
||||
"mobile_app": "移动应用",
|
||||
"web_app": "网站应用"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"official_account": "公众号",
|
||||
"mini_program": "小程序",
|
||||
"mobile_app": "移动应用"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"official_account": "公众号",
|
||||
"mini_program": "小程序",
|
||||
"mobile_app": "移动应用"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"mchAppNotFound": "抖音通道商户应用不存在",
|
||||
"appIdDuplicate": "同一通道商户下抖音应用ID已存在",
|
||||
"directMchDuplicate": "同一商户下该抖音直连商户已存在",
|
||||
"appTypeInvalid": "抖音应用类型无效",
|
||||
"capabilityDuplicate": "同一通道商户下支付能力[{0}]重复配置"
|
||||
}
|
||||
@@ -44,5 +44,6 @@
|
||||
"publicKeyRequired": "支付公钥未配置",
|
||||
"privateCertRequired": "API证书未配置",
|
||||
"subMerchantDuplicate": "该微信应用下已存在此特约商户号",
|
||||
"directMchDuplicate": "同一商户下该微信商户已存在"
|
||||
"directMchDuplicate": "同一商户下该微信商户已存在",
|
||||
"capabilityDuplicate": "微信支付能力[{0}]重复配置"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user