feat(channel): 微信服务商通道管理迁移(后端)

This commit is contained in:
DaxPay Dev
2026-06-14 01:36:55 +08:00
parent 38b6810559
commit b1df4c3211
27 changed files with 1138 additions and 0 deletions

View File

@@ -210,3 +210,21 @@ ON CONFLICT (id) DO UPDATE SET
menu_type = EXCLUDED.menu_type,
last_modified_time = EXCLUDED.last_modified_time;
-- ----------------------------
-- 微信服务商相关菜单
-- ----------------------------
-- 微信服务商应用管理(隐藏,从微信服务商总览页卡片进入)
INSERT INTO "public"."iam_perm_menu" VALUES (40503, 405, 'payment:wechat:isvApp', 'admin', 'WechatIsvAppManage', '微信服务商应用', 'WeChat ISV Application', 'menu.payment.wechat.isvApp', NULL, 't', 'f', '/payment/channel/wechat/manage/app/WechatIsvAppManage', '/payment/config/product/wechat-app-manage', NULL, 4, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-14 00:00:00+00', '2026-06-14 00:00:00+00')
ON CONFLICT (id) DO UPDATE SET
pid = EXCLUDED.pid,
menu_code = EXCLUDED.menu_code,
name = EXCLUDED.name,
title_cn = EXCLUDED.title_cn,
title_en = EXCLUDED.title_en,
i18n_key = EXCLUDED.i18n_key,
component = EXCLUDED.component,
path = EXCLUDED.path,
sort_no = EXCLUDED.sort_no,
menu_type = EXCLUDED.menu_type,
last_modified_time = EXCLUDED.last_modified_time;

View File

@@ -0,0 +1,102 @@
-- ----------------------------
-- 微信服务商应用表
-- ----------------------------
DROP TABLE IF EXISTS wechat_isv_app;
CREATE TABLE wechat_isv_app (
id int8 NOT NULL,
app_name varchar(64) COLLATE pg_catalog.default,
app_type varchar(32) COLLATE pg_catalog.default,
wx_app_id varchar(64) COLLATE pg_catalog.default,
creator int8,
create_time timestamptz(6),
last_modifier int8,
last_modified_time timestamptz(6),
version int4 DEFAULT 0,
deleted bool NOT NULL DEFAULT false
);
COMMENT ON TABLE wechat_isv_app IS '微信服务商应用表';
COMMENT ON COLUMN wechat_isv_app.id IS '主键';
COMMENT ON COLUMN wechat_isv_app.app_name IS '应用名称';
COMMENT ON COLUMN wechat_isv_app.app_type IS '应用类型';
COMMENT ON COLUMN wechat_isv_app.wx_app_id IS '微信应用AppId';
COMMENT ON COLUMN wechat_isv_app.creator IS '创建者ID';
COMMENT ON COLUMN wechat_isv_app.create_time IS '创建时间';
COMMENT ON COLUMN wechat_isv_app.last_modifier IS '最后修改ID';
COMMENT ON COLUMN wechat_isv_app.last_modified_time IS '最后修改时间';
COMMENT ON COLUMN wechat_isv_app.version IS '版本号';
COMMENT ON COLUMN wechat_isv_app.deleted IS '删除标志';
ALTER TABLE wechat_isv_app ADD CONSTRAINT pk_wechat_isv_app PRIMARY KEY (id);
-- ----------------------------
-- 微信服务商密钥配置表(平台为唯一服务商,密钥全局唯一,按 product 区分)
-- ----------------------------
DROP TABLE IF EXISTS wechat_isv_key_config;
CREATE TABLE wechat_isv_key_config (
id int8 NOT NULL,
product varchar(32) COLLATE pg_catalog.default,
wx_mch_id varchar(64) COLLATE pg_catalog.default,
api_key_v3 text COLLATE pg_catalog.default,
public_key text COLLATE pg_catalog.default,
public_key_id varchar(128) COLLATE pg_catalog.default,
private_key text COLLATE pg_catalog.default,
private_cert text COLLATE pg_catalog.default,
cert_serial_no varchar(128) COLLATE pg_catalog.default,
creator int8,
create_time timestamptz(6),
last_modifier int8,
last_modified_time timestamptz(6),
version int4 DEFAULT 0,
deleted bool NOT NULL DEFAULT false
);
COMMENT ON TABLE wechat_isv_key_config IS '微信服务商密钥配置表';
COMMENT ON COLUMN wechat_isv_key_config.id IS '主键';
COMMENT ON COLUMN wechat_isv_key_config.product IS '产品编码';
COMMENT ON COLUMN wechat_isv_key_config.wx_mch_id IS '微信服务商商户号';
COMMENT ON COLUMN wechat_isv_key_config.api_key_v3 IS 'APIv3密钥';
COMMENT ON COLUMN wechat_isv_key_config.public_key IS '支付公钥';
COMMENT ON COLUMN wechat_isv_key_config.public_key_id IS '支付公钥ID';
COMMENT ON COLUMN wechat_isv_key_config.private_key IS 'API证书私钥';
COMMENT ON COLUMN wechat_isv_key_config.private_cert IS 'API证书';
COMMENT ON COLUMN wechat_isv_key_config.cert_serial_no IS '证书序列号';
COMMENT ON COLUMN wechat_isv_key_config.creator IS '创建者ID';
COMMENT ON COLUMN wechat_isv_key_config.create_time IS '创建时间';
COMMENT ON COLUMN wechat_isv_key_config.last_modifier IS '最后修改ID';
COMMENT ON COLUMN wechat_isv_key_config.last_modified_time IS '最后修改时间';
COMMENT ON COLUMN wechat_isv_key_config.version IS '版本号';
COMMENT ON COLUMN wechat_isv_key_config.deleted IS '删除标志';
ALTER TABLE wechat_isv_key_config ADD CONSTRAINT pk_wechat_isv_key_config PRIMARY KEY (id);
-- ----------------------------
-- 微信服务商应用授权认证配置表
-- ----------------------------
DROP TABLE IF EXISTS wechat_isv_app_auth_config;
CREATE TABLE wechat_isv_app_auth_config (
id int8 NOT NULL,
app_id int8,
app_secret text COLLATE pg_catalog.default,
auth_callback_url varchar(512) COLLATE pg_catalog.default,
creator int8,
create_time timestamptz(6),
last_modifier int8,
last_modified_time timestamptz(6),
version int4 DEFAULT 0,
deleted bool NOT NULL DEFAULT false
);
COMMENT ON TABLE wechat_isv_app_auth_config IS '微信服务商应用授权认证配置表';
COMMENT ON COLUMN wechat_isv_app_auth_config.id IS '主键';
COMMENT ON COLUMN wechat_isv_app_auth_config.app_id IS '关联应用ID';
COMMENT ON COLUMN wechat_isv_app_auth_config.app_secret IS '应用密钥';
COMMENT ON COLUMN wechat_isv_app_auth_config.auth_callback_url IS '授权回调URL';
COMMENT ON COLUMN wechat_isv_app_auth_config.creator IS '创建者ID';
COMMENT ON COLUMN wechat_isv_app_auth_config.create_time IS '创建时间';
COMMENT ON COLUMN wechat_isv_app_auth_config.last_modifier IS '最后修改ID';
COMMENT ON COLUMN wechat_isv_app_auth_config.last_modified_time IS '最后修改时间';
COMMENT ON COLUMN wechat_isv_app_auth_config.version IS '版本号';
COMMENT ON COLUMN wechat_isv_app_auth_config.deleted IS '删除标志';
ALTER TABLE wechat_isv_app_auth_config ADD CONSTRAINT pk_wechat_isv_app_auth_config PRIMARY KEY (id);

View File

@@ -0,0 +1,110 @@
package org.dromara.daxpay.channel.wechat.controller.app;
import org.dromara.daxpay.platform.core.annotation.PermCode;
import org.dromara.daxpay.platform.core.rest.Res;
import org.dromara.daxpay.platform.core.rest.result.Result;
import org.dromara.daxpay.platform.core.util.ValidationUtil;
import org.dromara.daxpay.platform.core.validation.ValidationGroup;
import org.dromara.daxpay.channel.wechat.param.app.WechatIsvAppParam;
import org.dromara.daxpay.channel.wechat.param.config.WechatIsvAppAuthConfigParam;
import org.dromara.daxpay.channel.wechat.result.app.WechatIsvAppResult;
import org.dromara.daxpay.channel.wechat.result.config.WechatIsvAppAuthConfigResult;
import org.dromara.daxpay.channel.wechat.service.app.WechatIsvAppService;
import org.dromara.daxpay.channel.wechat.service.config.WechatIsvAppAuthConfigService;
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.*;
import java.util.List;
/// # 微信服务商应用管理
///
@PermCode(menuCode = "payment:wechat:isv")
@Validated
@Tag(name = "微信服务商应用管理")
@RestController
@RequestMapping("/admin/wechat/isv-app")
@RequiredArgsConstructor
public class WechatIsvAppController {
private final WechatIsvAppService wechatIsvAppService;
private final WechatIsvAppAuthConfigService wechatIsvAppAuthConfigService;
@PermCode(code = "view", nameCn = "微信服务商查看", nameEn = "WeChat ISV View")
@Operation(summary = "查询服务商应用列表")
@GetMapping("/list-all")
public Result<List<WechatIsvAppResult>> listAll() {
return Res.ok(wechatIsvAppService.listAll());
}
@PermCode(code = "view", nameCn = "微信服务商查看", nameEn = "WeChat ISV View")
@Operation(summary = "查询应用详情")
@GetMapping("/find-by-id")
public Result<WechatIsvAppResult> findById(
@NotNull(message = "{validation.field.id.notNull}") Long id) {
return Res.ok(wechatIsvAppService.findById(id));
}
@PermCode(code = "view", nameCn = "微信服务商查看", nameEn = "WeChat ISV View")
@Operation(summary = "微信应用AppId是否已存在")
@GetMapping("/exists-wx-app-id")
public Result<Boolean> existsWxAppId(
@NotBlank(message = "{validation.field.wxAppId.notBlank}") String wxAppId) {
return Res.ok(wechatIsvAppService.existsWxAppId(wxAppId, null));
}
@PermCode(code = "view", nameCn = "微信服务商查看", nameEn = "WeChat ISV View")
@Operation(summary = "微信应用AppId是否已存在(排除自身)")
@GetMapping("/exists-wx-app-id-not-id")
public Result<Boolean> existsWxAppIdNotId(
@NotBlank(message = "{validation.field.wxAppId.notBlank}") String wxAppId,
@NotNull(message = "{validation.field.id.notNull}") Long id) {
return Res.ok(wechatIsvAppService.existsWxAppId(wxAppId, id));
}
@PermCode(code = "add", nameCn = "微信服务商新增", nameEn = "WeChat ISV Add")
@Operation(summary = "新增服务商应用")
@PostMapping("/add")
public Result<Void> add(@RequestBody @Validated(ValidationGroup.add.class) WechatIsvAppParam param) {
ValidationUtil.validateParam(param, ValidationGroup.add.class);
wechatIsvAppService.add(param);
return Res.ok();
}
@PermCode(code = "edit", nameCn = "微信服务商编辑", nameEn = "WeChat ISV Edit")
@Operation(summary = "修改服务商应用")
@PostMapping("/update")
public Result<Void> update(@RequestBody @Validated(ValidationGroup.edit.class) WechatIsvAppParam param) {
ValidationUtil.validateParam(param, ValidationGroup.edit.class);
wechatIsvAppService.update(param);
return Res.ok();
}
@PermCode(code = "edit", nameCn = "微信服务商编辑", nameEn = "WeChat ISV Edit")
@Operation(summary = "删除服务商应用")
@PostMapping("/delete")
public Result<Void> delete(@NotNull(message = "{validation.field.id.notNull}") Long id) {
wechatIsvAppService.delete(id);
return Res.ok();
}
@PermCode(code = "view", nameCn = "微信服务商查看", nameEn = "WeChat ISV View")
@Operation(summary = "查询应用授权认证配置")
@GetMapping("/find-auth-config-by-app-id")
public Result<WechatIsvAppAuthConfigResult> findAuthConfigByAppId(
@NotNull(message = "{validation.field.id.notNull}") Long appId) {
return Res.ok(wechatIsvAppAuthConfigService.findByAppId(appId));
}
@PermCode(code = "edit", nameCn = "微信服务商编辑", nameEn = "WeChat ISV Edit")
@Operation(summary = "保存应用授权认证配置")
@PostMapping("/save-auth-config")
public Result<Void> saveAuthConfig(@RequestBody @Validated WechatIsvAppAuthConfigParam param) {
wechatIsvAppAuthConfigService.save(param);
return Res.ok();
}
}

View File

@@ -0,0 +1,43 @@
package org.dromara.daxpay.channel.wechat.controller.config;
import org.dromara.daxpay.platform.core.annotation.PermCode;
import org.dromara.daxpay.platform.core.rest.Res;
import org.dromara.daxpay.platform.core.rest.result.Result;
import org.dromara.daxpay.channel.wechat.param.config.WechatIsvKeyConfigParam;
import org.dromara.daxpay.channel.wechat.result.config.WechatIsvKeyConfigResult;
import org.dromara.daxpay.channel.wechat.service.config.WechatIsvKeyConfigService;
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.*;
/// # 微信服务商密钥配置
///
@PermCode(menuCode = "payment:wechat:isv")
@Validated
@Tag(name = "微信服务商密钥配置")
@RestController
@RequestMapping("/admin/wechat/isv-key-config")
@RequiredArgsConstructor
public class WechatIsvKeyConfigController {
private final WechatIsvKeyConfigService wechatIsvKeyConfigService;
@PermCode(code = "view", nameCn = "微信服务商查看", nameEn = "WeChat ISV View")
@Operation(summary = "查询微信服务商密钥配置")
@GetMapping("/find-config")
public Result<WechatIsvKeyConfigResult> findConfig(
@NotBlank(message = "{validation.field.product.notBlank}") String product) {
return Res.ok(wechatIsvKeyConfigService.findByProduct(product));
}
@PermCode(code = "edit", nameCn = "微信服务商编辑", nameEn = "WeChat ISV Edit")
@Operation(summary = "保存微信服务商密钥配置")
@PostMapping("/save-config")
public Result<Void> saveConfig(@RequestBody @Validated WechatIsvKeyConfigParam param) {
wechatIsvKeyConfigService.saveConfig(param);
return Res.ok();
}
}

View File

@@ -0,0 +1,28 @@
package org.dromara.daxpay.channel.wechat.convert;
import org.dromara.daxpay.channel.wechat.entity.config.WechatIsvAppAuthConfig;
import org.dromara.daxpay.channel.wechat.param.config.WechatIsvAppAuthConfigParam;
import org.dromara.daxpay.channel.wechat.result.config.WechatIsvAppAuthConfigResult;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
/// # 微信服务商应用授权认证配置转换
///
@Mapper
public interface WechatIsvAppAuthConfigConvert {
WechatIsvAppAuthConfigConvert CONVERT = Mappers.getMapper(WechatIsvAppAuthConfigConvert.class);
/// 转换为返回对象
WechatIsvAppAuthConfigResult toResult(WechatIsvAppAuthConfig entity);
/// 转换为实体
WechatIsvAppAuthConfig toEntity(WechatIsvAppAuthConfigParam param);
/// 更新源数据到实体(空值不覆盖)
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void copy(WechatIsvAppAuthConfigParam param, @MappingTarget WechatIsvAppAuthConfig entity);
}

View File

@@ -0,0 +1,25 @@
package org.dromara.daxpay.channel.wechat.convert;
import org.dromara.daxpay.channel.wechat.entity.app.WechatIsvApp;
import org.dromara.daxpay.channel.wechat.param.app.WechatIsvAppParam;
import org.dromara.daxpay.channel.wechat.result.app.WechatIsvAppResult;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
/// # 微信服务商应用转换
///
@Mapper
public interface WechatIsvAppConvert {
WechatIsvAppConvert CONVERT = Mappers.getMapper(WechatIsvAppConvert.class);
/// 转换为返回对象
WechatIsvAppResult toResult(WechatIsvApp entity);
/// 转换为实体
WechatIsvApp toEntity(WechatIsvAppParam param);
/// 更新源数据到实体(忽略空值)
void copy(WechatIsvAppParam param, @MappingTarget WechatIsvApp entity);
}

View File

@@ -0,0 +1,28 @@
package org.dromara.daxpay.channel.wechat.convert;
import org.dromara.daxpay.channel.wechat.entity.config.WechatIsvKeyConfig;
import org.dromara.daxpay.channel.wechat.param.config.WechatIsvKeyConfigParam;
import org.dromara.daxpay.channel.wechat.result.config.WechatIsvKeyConfigResult;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
/// # 微信服务商密钥配置转换
///
@Mapper
public interface WechatIsvKeyConfigConvert {
WechatIsvKeyConfigConvert CONVERT = Mappers.getMapper(WechatIsvKeyConfigConvert.class);
/// 转换为返回对象
WechatIsvKeyConfigResult toResult(WechatIsvKeyConfig entity);
/// 转换为实体
WechatIsvKeyConfig toEntity(WechatIsvKeyConfigParam param);
/// 更新源数据到实体(空值不覆盖,密钥/证书为空时保留原值)
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void copy(WechatIsvKeyConfigParam param, @MappingTarget WechatIsvKeyConfig entity);
}

View File

@@ -0,0 +1,43 @@
package org.dromara.daxpay.channel.wechat.dao.app;
import org.dromara.daxpay.platform.common.mybatisplus.impl.BaseManager;
import org.dromara.daxpay.channel.wechat.entity.app.WechatIsvApp;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/// # 微信服务商应用
///
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatIsvAppManager extends BaseManager<WechatIsvAppMapper, WechatIsvApp> {
/// 查询全部应用(按创建时间升序,先创建的在前)
public List<WechatIsvApp> listAll() {
return lambdaQuery()
.orderByAsc(WechatIsvApp::getCreateTime)
.orderByAsc(WechatIsvApp::getId)
.list();
}
/// 查询第一个应用(运行时默认使用)
public Optional<WechatIsvApp> findFirst() {
return lambdaQuery()
.orderByAsc(WechatIsvApp::getCreateTime)
.orderByAsc(WechatIsvApp::getId)
.last("LIMIT 1")
.oneOpt();
}
/// 校验微信应用AppId是否已存在(排除自身)
public boolean existsByWxAppId(String wxAppId, Long excludeId) {
return lambdaQuery()
.eq(WechatIsvApp::getWxAppId, wxAppId)
.ne(excludeId != null, WechatIsvApp::getId, excludeId)
.exists();
}
}

View File

@@ -0,0 +1,11 @@
package org.dromara.daxpay.channel.wechat.dao.app;
import org.dromara.daxpay.channel.wechat.entity.app.WechatIsvApp;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
/// # 微信服务商应用
///
@Mapper
public interface WechatIsvAppMapper extends MPJBaseMapper<WechatIsvApp> {
}

View File

@@ -0,0 +1,31 @@
package org.dromara.daxpay.channel.wechat.dao.config;
import org.dromara.daxpay.platform.common.mybatisplus.impl.BaseManager;
import org.dromara.daxpay.channel.wechat.entity.config.WechatIsvAppAuthConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Optional;
/// # 微信服务商应用授权认证配置
///
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatIsvAppAuthConfigManager extends BaseManager<WechatIsvAppAuthConfigMapper, WechatIsvAppAuthConfig> {
/// 根据应用ID查询授权认证配置
public Optional<WechatIsvAppAuthConfig> findByAppId(Long appId) {
return lambdaQuery()
.eq(WechatIsvAppAuthConfig::getAppId, appId)
.oneOpt();
}
/// 根据应用ID删除授权认证配置
public void deleteByAppId(Long appId) {
lambdaUpdate()
.eq(WechatIsvAppAuthConfig::getAppId, appId)
.remove();
}
}

View File

@@ -0,0 +1,11 @@
package org.dromara.daxpay.channel.wechat.dao.config;
import org.dromara.daxpay.channel.wechat.entity.config.WechatIsvAppAuthConfig;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
/// # 微信服务商应用授权认证配置
///
@Mapper
public interface WechatIsvAppAuthConfigMapper extends MPJBaseMapper<WechatIsvAppAuthConfig> {
}

View File

@@ -0,0 +1,24 @@
package org.dromara.daxpay.channel.wechat.dao.config;
import org.dromara.daxpay.platform.common.mybatisplus.impl.BaseManager;
import org.dromara.daxpay.channel.wechat.entity.config.WechatIsvKeyConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Optional;
/// # 微信服务商密钥配置
///
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatIsvKeyConfigManager extends BaseManager<WechatIsvKeyConfigMapper, WechatIsvKeyConfig> {
/// 根据产品编码查询(平台为唯一服务商,密钥全局唯一)
public Optional<WechatIsvKeyConfig> findByProduct(String product) {
return lambdaQuery()
.eq(WechatIsvKeyConfig::getProduct, product)
.oneOpt();
}
}

View File

@@ -0,0 +1,11 @@
package org.dromara.daxpay.channel.wechat.dao.config;
import org.dromara.daxpay.channel.wechat.entity.config.WechatIsvKeyConfig;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
/// # 微信服务商密钥配置
///
@Mapper
public interface WechatIsvKeyConfigMapper extends MPJBaseMapper<WechatIsvKeyConfig> {
}

View File

@@ -0,0 +1,37 @@
package org.dromara.daxpay.channel.wechat.entity.app;
import org.dromara.daxpay.channel.wechat.convert.WechatIsvAppConvert;
import org.dromara.daxpay.channel.wechat.result.app.WechatIsvAppResult;
import org.dromara.daxpay.platform.common.mybatisplus.base.MpBaseEntity;
import org.dromara.daxpay.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;
/// # 微信服务商应用
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName(value = "wechat_isv_app", autoResultMap = true)
public class WechatIsvApp extends MpBaseEntity implements ToResult<WechatIsvAppResult> {
/// 应用名称
private String appName;
/// 应用类型
@TableField(updateStrategy = FieldStrategy.NEVER)
private String appType;
/// 微信应用AppId
private String wxAppId;
/// 转换
@Override
public WechatIsvAppResult toResult() {
return WechatIsvAppConvert.CONVERT.toResult(this);
}
}

View File

@@ -0,0 +1,39 @@
package org.dromara.daxpay.channel.wechat.entity.config;
import org.dromara.daxpay.channel.wechat.convert.WechatIsvAppAuthConfigConvert;
import org.dromara.daxpay.channel.wechat.result.config.WechatIsvAppAuthConfigResult;
import org.dromara.daxpay.platform.common.mybatisplus.base.MpBaseEntity;
import org.dromara.daxpay.platform.common.mybatisplus.function.ToResult;
import org.dromara.daxpay.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;
/// # 微信服务商应用授权认证配置
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName(value = "wechat_isv_app_auth_config", autoResultMap = true)
public class WechatIsvAppAuthConfig extends MpBaseEntity implements ToResult<WechatIsvAppAuthConfigResult> {
/// 微信服务商应用ID
@TableField(updateStrategy = FieldStrategy.NEVER)
private Long appId;
/// 应用密钥(加密存储)
@TableField(typeHandler = DataEncryptTypeHandler.class)
private String appSecret;
/// 授权回调地址
private String authCallbackUrl;
/// 转换
@Override
public WechatIsvAppAuthConfigResult toResult() {
return WechatIsvAppAuthConfigConvert.CONVERT.toResult(this);
}
}

View File

@@ -0,0 +1,55 @@
package org.dromara.daxpay.channel.wechat.entity.config;
import org.dromara.daxpay.channel.wechat.convert.WechatIsvKeyConfigConvert;
import org.dromara.daxpay.channel.wechat.result.config.WechatIsvKeyConfigResult;
import org.dromara.daxpay.platform.common.mybatisplus.base.MpBaseEntity;
import org.dromara.daxpay.platform.common.mybatisplus.function.ToResult;
import org.dromara.daxpay.platform.common.mybatisplus.handler.encrypt.DataEncryptTypeHandler;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # 微信服务商密钥配置
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName(value = "wechat_isv_key_config", autoResultMap = true)
public class WechatIsvKeyConfig extends MpBaseEntity implements ToResult<WechatIsvKeyConfigResult> {
/// 产品编码
private String product;
/// 微信服务商商户号
private String wxMchId;
/// APIv3密钥(加密存储)
@TableField(typeHandler = DataEncryptTypeHandler.class)
private String apiKeyV3;
/// 支付公钥(加密存储)
@TableField(typeHandler = DataEncryptTypeHandler.class)
private String publicKey;
/// 支付公钥ID
private String publicKeyId;
/// apiclient_key证书(加密存储)
@TableField(typeHandler = DataEncryptTypeHandler.class)
private String privateKey;
/// apiclient_cert证书(加密存储)
@TableField(typeHandler = DataEncryptTypeHandler.class)
private String privateCert;
/// 证书序列号
private String certSerialNo;
/// 转换
@Override
public WechatIsvKeyConfigResult toResult() {
return WechatIsvKeyConfigConvert.CONVERT.toResult(this);
}
}

View File

@@ -0,0 +1,25 @@
package org.dromara.daxpay.channel.wechat.enums;
import org.dromara.daxpay.platform.core.i18n.I18nSupport;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/// # 微信OpenId认证方式
///
@Getter
@RequiredArgsConstructor
public enum WechatAuthTypeEnum implements I18nSupport {
/// 服务商用户标识
SP("sp"),
/// 子商户应用用户标识
SUB("sub"),
;
private final String code;
/// 翻译 key 前缀
@Override
public String getI18nPrefix() {
return "enum.wechat_auth_type";
}
}

View File

@@ -0,0 +1,27 @@
package org.dromara.daxpay.channel.wechat.enums;
import org.dromara.daxpay.platform.core.i18n.I18nSupport;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/// # 微信服务商应用类型
///
@Getter
@RequiredArgsConstructor
public enum WechatIsvAppTypeEnum 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_isv_app_type";
}
}

View File

@@ -0,0 +1,32 @@
package org.dromara.daxpay.channel.wechat.param.app;
import org.dromara.daxpay.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;
/// # 微信服务商应用保存参数
///
@Data
@Accessors(chain = true)
@Schema(title = "微信服务商应用保存参数")
public class WechatIsvAppParam {
@NotNull(message = "{validation.field.id.notNull}", groups = ValidationGroup.edit.class)
@Schema(description = "主键,新增时不传")
private Long id;
@NotBlank(message = "{validation.field.appName.notBlank}")
@Schema(description = "应用名称")
private String appName;
@NotBlank(message = "{validation.field.appType.notBlank}", groups = ValidationGroup.add.class)
@Schema(description = "应用类型")
private String appType;
@NotBlank(message = "{validation.field.wxAppId.notBlank}")
@Schema(description = "微信应用AppId")
private String wxAppId;
}

View File

@@ -0,0 +1,26 @@
package org.dromara.daxpay.channel.wechat.param.config;
import io.swagger.v3.oas.annotations.media.Schema;
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 WechatIsvAppAuthConfigParam {
@NotNull(message = "{validation.field.id.notNull}")
@Schema(description = "微信服务商应用ID")
private Long appId;
@Schema(description = "应用密钥(加密存储),所有应用类型首次保存时必填")
private String appSecret;
@Size(max = 512, message = "{validation.field.authCallbackUrl.size}")
@Schema(description = "授权回调地址(仅公众号应用需要)")
private String authCallbackUrl;
}

View File

@@ -0,0 +1,40 @@
package org.dromara.daxpay.channel.wechat.param.config;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 微信服务商密钥配置保存参数
///
@Data
@Accessors(chain = true)
@Schema(title = "微信服务商密钥配置保存参数")
public class WechatIsvKeyConfigParam {
@NotBlank(message = "{validation.field.product.notBlank}")
@Schema(description = "产品编码")
private String product;
@NotBlank(message = "{validation.field.wxMchId.notBlank}")
@Schema(description = "微信服务商商户号")
private String wxMchId;
@Schema(description = "APIv3密钥(加密存储)")
private String apiKeyV3;
@Schema(description = "支付公钥(加密存储)")
private String publicKey;
@Schema(description = "支付公钥ID")
private String publicKeyId;
@Schema(description = "apiclient_key证书(加密存储)")
private String privateKey;
@Schema(description = "apiclient_cert证书(加密存储)")
private String privateCert;
@Schema(description = "证书序列号")
private String certSerialNo;
}

View File

@@ -0,0 +1,25 @@
package org.dromara.daxpay.channel.wechat.result.app;
import org.dromara.daxpay.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 WechatIsvAppResult extends BaseResult {
@Schema(description = "应用名称")
private String appName;
@Schema(description = "应用类型")
private String appType;
@Schema(description = "微信应用AppId")
private String wxAppId;
}

View File

@@ -0,0 +1,30 @@
package org.dromara.daxpay.channel.wechat.result.config;
import org.dromara.daxpay.platform.common.json.sensitive.SensitiveInfo;
import org.dromara.daxpay.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 WechatIsvAppAuthConfigResult extends BaseResult {
@Schema(description = "微信服务商应用ID")
private Long appId;
@SensitiveInfo(front = 12, end = 12)
@Schema(description = "应用密钥(加密存储)")
private String appSecret;
@Schema(description = "是否已配置应用密钥")
private Boolean appSecretConfigured;
@Schema(description = "授权回调地址(仅公众号)")
private String authCallbackUrl;
}

View File

@@ -0,0 +1,45 @@
package org.dromara.daxpay.channel.wechat.result.config;
import org.dromara.daxpay.platform.common.json.sensitive.SensitiveInfo;
import org.dromara.daxpay.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 WechatIsvKeyConfigResult extends BaseResult {
@Schema(description = "产品编码")
private String product;
@Schema(description = "微信服务商商户号")
private String wxMchId;
@SensitiveInfo(front = 12, end = 12)
@Schema(description = "APIv3密钥(加密存储)")
private String apiKeyV3;
@SensitiveInfo(front = 12, end = 12)
@Schema(description = "支付公钥(加密存储)")
private String publicKey;
@Schema(description = "支付公钥ID")
private String publicKeyId;
@SensitiveInfo(front = 12, end = 12)
@Schema(description = "apiclient_key证书(加密存储)")
private String privateKey;
@SensitiveInfo(front = 12, end = 12)
@Schema(description = "apiclient_cert证书(加密存储)")
private String privateCert;
@Schema(description = "证书序列号")
private String certSerialNo;
}

View File

@@ -0,0 +1,99 @@
package org.dromara.daxpay.channel.wechat.service.app;
import org.dromara.daxpay.channel.wechat.convert.WechatIsvAppConvert;
import org.dromara.daxpay.channel.wechat.dao.app.WechatIsvAppManager;
import org.dromara.daxpay.channel.wechat.entity.app.WechatIsvApp;
import org.dromara.daxpay.channel.wechat.enums.WechatIsvAppTypeEnum;
import org.dromara.daxpay.channel.wechat.param.app.WechatIsvAppParam;
import org.dromara.daxpay.channel.wechat.result.app.WechatIsvAppResult;
import org.dromara.daxpay.channel.wechat.service.config.WechatIsvAppAuthConfigService;
import org.dromara.daxpay.platform.common.mybatisplus.util.MpUtil;
import org.dromara.daxpay.platform.core.code.CommonErrorCode;
import org.dromara.daxpay.platform.core.exception.BizInfoException;
import org.dromara.daxpay.platform.core.exception.DataNotExistException;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
/// # 微信服务商应用管理
///
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatIsvAppService {
private final WechatIsvAppManager wechatIsvAppManager;
private final WechatIsvAppAuthConfigService wechatIsvAppAuthConfigService;
/// 查询全部应用列表
public List<WechatIsvAppResult> listAll() {
List<WechatIsvApp> list = wechatIsvAppManager.listAll();
return MpUtil.toListResult(list);
}
/// 根据ID查询应用详情
public WechatIsvAppResult findById(Long id) {
return wechatIsvAppManager.findById(id)
.map(WechatIsvApp::toResult)
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.appNotFound"));
}
/// 查询第一个应用(运行时默认使用)
public Optional<WechatIsvApp> findFirst() {
return wechatIsvAppManager.findFirst();
}
/// 判断微信应用AppId是否已存在
public boolean existsWxAppId(String wxAppId, Long excludeId) {
if (StrUtil.isBlank(wxAppId)) {
return false;
}
return wechatIsvAppManager.existsByWxAppId(wxAppId, excludeId);
}
/// 新增微信服务商应用
public void add(WechatIsvAppParam param) {
this.assertWxAppIdUnique(param.getWxAppId(), null);
this.validateAppType(param.getAppType());
WechatIsvApp entity = WechatIsvAppConvert.CONVERT.toEntity(param);
wechatIsvAppManager.save(entity);
}
/// 更新微信服务商应用
public void update(WechatIsvAppParam param) {
WechatIsvApp entity = wechatIsvAppManager.findById(param.getId())
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.appNotFound"));
this.assertWxAppIdUnique(param.getWxAppId(), param.getId());
WechatIsvAppConvert.CONVERT.copy(param, entity);
wechatIsvAppManager.updateById(entity);
}
/// 删除应用
public void delete(Long id) {
wechatIsvAppManager.findById(id)
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.appNotFound"));
wechatIsvAppAuthConfigService.deleteByAppId(id);
wechatIsvAppManager.deleteById(id);
}
/// 校验应用AppId唯一
private void assertWxAppIdUnique(String wxAppId, Long excludeId) {
if (this.existsWxAppId(wxAppId, excludeId)) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.wechat.appIdDuplicate");
}
}
/// 校验应用类型
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

@@ -0,0 +1,88 @@
package org.dromara.daxpay.channel.wechat.service.config;
import org.dromara.daxpay.channel.wechat.convert.WechatIsvAppAuthConfigConvert;
import org.dromara.daxpay.channel.wechat.dao.app.WechatIsvAppManager;
import org.dromara.daxpay.channel.wechat.dao.config.WechatIsvAppAuthConfigManager;
import org.dromara.daxpay.channel.wechat.entity.app.WechatIsvApp;
import org.dromara.daxpay.channel.wechat.entity.config.WechatIsvAppAuthConfig;
import org.dromara.daxpay.channel.wechat.enums.WechatIsvAppTypeEnum;
import org.dromara.daxpay.channel.wechat.param.config.WechatIsvAppAuthConfigParam;
import org.dromara.daxpay.channel.wechat.result.config.WechatIsvAppAuthConfigResult;
import org.dromara.daxpay.platform.core.code.CommonErrorCode;
import org.dromara.daxpay.platform.core.exception.BizInfoException;
import org.dromara.daxpay.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.Optional;
/// # 微信服务商应用授权认证配置
///
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatIsvAppAuthConfigService {
private final WechatIsvAppAuthConfigManager wechatIsvAppAuthConfigManager;
private final WechatIsvAppManager wechatIsvAppManager;
/// 根据应用ID查询授权认证配置
public WechatIsvAppAuthConfigResult findByAppId(Long appId) {
WechatIsvApp app = wechatIsvAppManager.findById(appId)
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.appNotFound"));
return wechatIsvAppAuthConfigManager.findByAppId(appId)
.map(config -> WechatIsvAppAuthConfigConvert.CONVERT.toResult(config)
.setAppSecretConfigured(StrUtil.isNotBlank(config.getAppSecret())))
.orElseGet(() -> new WechatIsvAppAuthConfigResult()
.setAppId(appId)
.setAppSecretConfigured(false));
}
/// 保存应用授权认证配置
@Transactional(rollbackFor = Exception.class)
public void save(WechatIsvAppAuthConfigParam param) {
WechatIsvApp app = wechatIsvAppManager.findById(param.getAppId())
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.appNotFound"));
String authCallbackUrl = this.resolveAuthCallbackUrl(app, param.getAuthCallbackUrl());
Optional<WechatIsvAppAuthConfig> existing = wechatIsvAppAuthConfigManager.findByAppId(param.getAppId());
if (existing.isPresent()) {
WechatIsvAppAuthConfig config = existing.get();
if (StrUtil.isNotBlank(param.getAppSecret())) {
config.setAppSecret(param.getAppSecret());
} else if (StrUtil.isBlank(config.getAppSecret())) {
// 所有应用类型均须配置 AppSecret
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.wechat.appSecretRequired");
}
config.setAuthCallbackUrl(authCallbackUrl);
wechatIsvAppAuthConfigManager.updateById(config);
} else {
if (StrUtil.isBlank(param.getAppSecret())) {
// 所有应用类型首次保存均须填写 AppSecret
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.wechat.appSecretRequired");
}
WechatIsvAppAuthConfig config = WechatIsvAppAuthConfigConvert.CONVERT.toEntity(param);
config.setAppId(param.getAppId());
config.setAuthCallbackUrl(authCallbackUrl);
wechatIsvAppAuthConfigManager.save(config);
}
}
/// 仅公众号保留授权回调地址,其他类型清空
private String resolveAuthCallbackUrl(WechatIsvApp app, String authCallbackUrl) {
if (!WechatIsvAppTypeEnum.OFFICIAL_ACCOUNT.getCode().equals(app.getAppType())) {
return null;
}
if (StrUtil.isBlank(authCallbackUrl)) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "validation.field.authCallbackUrl.notBlank");
}
return authCallbackUrl.trim();
}
/// 删除应用授权认证配置
public void deleteByAppId(Long appId) {
wechatIsvAppAuthConfigManager.deleteByAppId(appId);
}
}

View File

@@ -0,0 +1,85 @@
package org.dromara.daxpay.channel.wechat.service.config;
import org.dromara.daxpay.channel.wechat.convert.WechatIsvKeyConfigConvert;
import org.dromara.daxpay.channel.wechat.dao.config.WechatIsvKeyConfigManager;
import org.dromara.daxpay.channel.wechat.entity.config.WechatIsvKeyConfig;
import org.dromara.daxpay.channel.wechat.param.config.WechatIsvKeyConfigParam;
import org.dromara.daxpay.channel.wechat.result.config.WechatIsvKeyConfigResult;
import org.dromara.daxpay.platform.core.code.CommonErrorCode;
import org.dromara.daxpay.platform.core.exception.BizInfoException;
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.Optional;
/// # 微信服务商密钥配置
///
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatIsvKeyConfigService {
private final WechatIsvKeyConfigManager wechatIsvKeyConfigManager;
/// 根据产品编码查询微信服务商密钥配置(平台为唯一服务商,密钥全局唯一)
/// 注意: 微信服务商模式不支持沙箱环境
public WechatIsvKeyConfigResult findByProduct(String product) {
Optional<WechatIsvKeyConfig> config = wechatIsvKeyConfigManager.findByProduct(product);
return config.map(WechatIsvKeyConfigConvert.CONVERT::toResult).orElse(null);
}
/// 保存微信服务商密钥配置
/// 注意: 微信服务商模式不支持沙箱环境
@Transactional(rollbackFor = Exception.class)
public void saveConfig(WechatIsvKeyConfigParam param) {
Optional<WechatIsvKeyConfig> existing = wechatIsvKeyConfigManager.findByProduct(param.getProduct());
WechatIsvKeyConfig config;
if (existing.isPresent()) {
config = existing.get();
config.setWxMchId(param.getWxMchId());
if (StrUtil.isNotBlank(param.getApiKeyV3())) {
config.setApiKeyV3(param.getApiKeyV3());
}
if (StrUtil.isNotBlank(param.getPublicKey())) {
config.setPublicKey(param.getPublicKey());
}
if (StrUtil.isNotBlank(param.getPublicKeyId())) {
config.setPublicKeyId(param.getPublicKeyId());
}
if (StrUtil.isNotBlank(param.getPrivateKey())) {
config.setPrivateKey(param.getPrivateKey());
}
if (StrUtil.isNotBlank(param.getPrivateCert())) {
config.setPrivateCert(param.getPrivateCert());
}
if (StrUtil.isNotBlank(param.getCertSerialNo())) {
config.setCertSerialNo(param.getCertSerialNo());
}
wechatIsvKeyConfigManager.updateById(config);
} else {
this.validateForCreate(param);
config = WechatIsvKeyConfigConvert.CONVERT.toEntity(param);
config.setProduct(param.getProduct());
wechatIsvKeyConfigManager.save(config);
}
}
/// 新增时的必填校验
private void validateForCreate(WechatIsvKeyConfigParam param) {
if (StrUtil.isBlank(param.getPrivateKey())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.wechat.privateKeyRequired");
}
if (StrUtil.isBlank(param.getApiKeyV3())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.wechat.apiV3KeyRequired");
}
if (StrUtil.isBlank(param.getPublicKey())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.wechat.publicKeyRequired");
}
if (StrUtil.isBlank(param.getPrivateCert())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.channel.wechat.privateCertRequired");
}
}
}