diff --git a/_config/sql/iam_perm_menu.sql b/_config/sql/iam_perm_menu.sql index 057d657f8..29099c3c4 100644 --- a/_config/sql/iam_perm_menu.sql +++ b/_config/sql/iam_perm_menu.sql @@ -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; + diff --git a/_config/sql/wechat_isv_app.sql b/_config/sql/wechat_isv_app.sql new file mode 100644 index 000000000..9a0b04fe5 --- /dev/null +++ b/_config/sql/wechat_isv_app.sql @@ -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); diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/controller/app/WechatIsvAppController.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/controller/app/WechatIsvAppController.java new file mode 100644 index 000000000..55298f1a1 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/controller/app/WechatIsvAppController.java @@ -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> listAll() { + return Res.ok(wechatIsvAppService.listAll()); + } + + @PermCode(code = "view", nameCn = "微信服务商查看", nameEn = "WeChat ISV View") + @Operation(summary = "查询应用详情") + @GetMapping("/find-by-id") + public Result 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 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 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 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 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 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 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 saveAuthConfig(@RequestBody @Validated WechatIsvAppAuthConfigParam param) { + wechatIsvAppAuthConfigService.save(param); + return Res.ok(); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/controller/config/WechatIsvKeyConfigController.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/controller/config/WechatIsvKeyConfigController.java new file mode 100644 index 000000000..e1d16347b --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/controller/config/WechatIsvKeyConfigController.java @@ -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 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 saveConfig(@RequestBody @Validated WechatIsvKeyConfigParam param) { + wechatIsvKeyConfigService.saveConfig(param); + return Res.ok(); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/convert/WechatIsvAppAuthConfigConvert.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/convert/WechatIsvAppAuthConfigConvert.java new file mode 100644 index 000000000..278a6bbf7 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/convert/WechatIsvAppAuthConfigConvert.java @@ -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); +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/convert/WechatIsvAppConvert.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/convert/WechatIsvAppConvert.java new file mode 100644 index 000000000..f400adb98 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/convert/WechatIsvAppConvert.java @@ -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); +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/convert/WechatIsvKeyConfigConvert.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/convert/WechatIsvKeyConfigConvert.java new file mode 100644 index 000000000..e5c963e06 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/convert/WechatIsvKeyConfigConvert.java @@ -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); +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/app/WechatIsvAppManager.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/app/WechatIsvAppManager.java new file mode 100644 index 000000000..30b40c01e --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/app/WechatIsvAppManager.java @@ -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 { + + /// 查询全部应用(按创建时间升序,先创建的在前) + public List listAll() { + return lambdaQuery() + .orderByAsc(WechatIsvApp::getCreateTime) + .orderByAsc(WechatIsvApp::getId) + .list(); + } + + /// 查询第一个应用(运行时默认使用) + public Optional 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(); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/app/WechatIsvAppMapper.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/app/WechatIsvAppMapper.java new file mode 100644 index 000000000..add3cf820 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/app/WechatIsvAppMapper.java @@ -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 { +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/config/WechatIsvAppAuthConfigManager.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/config/WechatIsvAppAuthConfigManager.java new file mode 100644 index 000000000..22c0c6fe6 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/config/WechatIsvAppAuthConfigManager.java @@ -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 { + + /// 根据应用ID查询授权认证配置 + public Optional findByAppId(Long appId) { + return lambdaQuery() + .eq(WechatIsvAppAuthConfig::getAppId, appId) + .oneOpt(); + } + + /// 根据应用ID删除授权认证配置 + public void deleteByAppId(Long appId) { + lambdaUpdate() + .eq(WechatIsvAppAuthConfig::getAppId, appId) + .remove(); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/config/WechatIsvAppAuthConfigMapper.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/config/WechatIsvAppAuthConfigMapper.java new file mode 100644 index 000000000..0282c5699 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/config/WechatIsvAppAuthConfigMapper.java @@ -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 { +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/config/WechatIsvKeyConfigManager.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/config/WechatIsvKeyConfigManager.java new file mode 100644 index 000000000..9cba4a1a8 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/config/WechatIsvKeyConfigManager.java @@ -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 { + + /// 根据产品编码查询(平台为唯一服务商,密钥全局唯一) + public Optional findByProduct(String product) { + return lambdaQuery() + .eq(WechatIsvKeyConfig::getProduct, product) + .oneOpt(); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/config/WechatIsvKeyConfigMapper.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/config/WechatIsvKeyConfigMapper.java new file mode 100644 index 000000000..ac6805f45 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/dao/config/WechatIsvKeyConfigMapper.java @@ -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 { +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/entity/app/WechatIsvApp.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/entity/app/WechatIsvApp.java new file mode 100644 index 000000000..073fbcf93 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/entity/app/WechatIsvApp.java @@ -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 { + + /// 应用名称 + private String appName; + + /// 应用类型 + @TableField(updateStrategy = FieldStrategy.NEVER) + private String appType; + + /// 微信应用AppId + private String wxAppId; + + /// 转换 + @Override + public WechatIsvAppResult toResult() { + return WechatIsvAppConvert.CONVERT.toResult(this); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/entity/config/WechatIsvAppAuthConfig.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/entity/config/WechatIsvAppAuthConfig.java new file mode 100644 index 000000000..3f1be9112 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/entity/config/WechatIsvAppAuthConfig.java @@ -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 { + + /// 微信服务商应用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); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/entity/config/WechatIsvKeyConfig.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/entity/config/WechatIsvKeyConfig.java new file mode 100644 index 000000000..de56cdeca --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/entity/config/WechatIsvKeyConfig.java @@ -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 { + + /// 产品编码 + 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); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/enums/WechatAuthTypeEnum.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/enums/WechatAuthTypeEnum.java new file mode 100644 index 000000000..5e75a0582 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/enums/WechatAuthTypeEnum.java @@ -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"; + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/enums/WechatIsvAppTypeEnum.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/enums/WechatIsvAppTypeEnum.java new file mode 100644 index 000000000..596dc87c2 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/enums/WechatIsvAppTypeEnum.java @@ -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"; + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/param/app/WechatIsvAppParam.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/param/app/WechatIsvAppParam.java new file mode 100644 index 000000000..f06cdd3ab --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/param/app/WechatIsvAppParam.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/param/config/WechatIsvAppAuthConfigParam.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/param/config/WechatIsvAppAuthConfigParam.java new file mode 100644 index 000000000..e3426ab5a --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/param/config/WechatIsvAppAuthConfigParam.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/param/config/WechatIsvKeyConfigParam.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/param/config/WechatIsvKeyConfigParam.java new file mode 100644 index 000000000..6b169b739 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/param/config/WechatIsvKeyConfigParam.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/result/app/WechatIsvAppResult.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/result/app/WechatIsvAppResult.java new file mode 100644 index 000000000..97455573c --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/result/app/WechatIsvAppResult.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/result/config/WechatIsvAppAuthConfigResult.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/result/config/WechatIsvAppAuthConfigResult.java new file mode 100644 index 000000000..134c7c242 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/result/config/WechatIsvAppAuthConfigResult.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/result/config/WechatIsvKeyConfigResult.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/result/config/WechatIsvKeyConfigResult.java new file mode 100644 index 000000000..de75b6896 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/result/config/WechatIsvKeyConfigResult.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/service/app/WechatIsvAppService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/service/app/WechatIsvAppService.java new file mode 100644 index 000000000..44d015ebf --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/service/app/WechatIsvAppService.java @@ -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 listAll() { + List 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 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"); + } + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/service/config/WechatIsvAppAuthConfigService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/service/config/WechatIsvAppAuthConfigService.java new file mode 100644 index 000000000..f651b843b --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/service/config/WechatIsvAppAuthConfigService.java @@ -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 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); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/service/config/WechatIsvKeyConfigService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/service/config/WechatIsvKeyConfigService.java new file mode 100644 index 000000000..1a05f9c85 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/org/dromara/daxpay/channel/wechat/service/config/WechatIsvKeyConfigService.java @@ -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 config = wechatIsvKeyConfigManager.findByProduct(product); + return config.map(WechatIsvKeyConfigConvert.CONVERT::toResult).orElse(null); + } + + /// 保存微信服务商密钥配置 + /// 注意: 微信服务商模式不支持沙箱环境 + @Transactional(rollbackFor = Exception.class) + public void saveConfig(WechatIsvKeyConfigParam param) { + Optional 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"); + } + } +}