From a54450a998aa1c06cafa97eb61a2c3716173c180 Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Tue, 14 Jul 2026 08:15:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(system):=20=E5=B9=B3=E5=8F=B0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=B0=E5=A2=9E=E7=AB=99=E7=82=B9=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=B8=8EOSS/=E7=AB=AF=E7=82=B9=E8=BF=9E?= =?UTF-8?q?=E9=80=9A=E6=80=A7=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增网站显示配置(系统名/Logo/备案/版权等)website 配置类型 - OSS 配置增加连通性检查接口, 支持合并库中密钥探测 - 端点配置增加连通性检查, 后端走 /echo 严格校验 - StorageClientService 新增 invalidate 方法, 配置更新后丢弃缓存客户端 - S3 预签名器补 pathStyleAccess 配置, 修正 MinIO 预签名 URL - 新增 oss/url 连通性检查中英文 i18n 错误消息 --- .../file/service/StorageClientService.java | 34 +++- .../i18n/en-US/enum/platform_config_type.json | 2 + .../i18n/en-US/error/system/oss.json | 8 + .../i18n/en-US/error/system/url.json | 7 + .../main/resources/i18n/en-US/validation.json | 3 + .../i18n/zh-CN/enum/platform_config_type.json | 2 + .../i18n/zh-CN/error/system/oss.json | 8 + .../i18n/zh-CN/error/system/url.json | 7 + .../main/resources/i18n/zh-CN/validation.json | 3 + .../infra/PlatformOssConfigController.java | 8 + .../infra/PlatformUrlConfigController.java | 9 ++ .../PlatformWebsiteConfigController.java | 45 ++++++ .../infra/PlatformWebsiteConfigConvert.java | 25 +++ .../platform/infra/PlatformWebsiteConfig.java | 62 ++++++++ .../system/enums/PlatformConfigTypeEnum.java | 2 + .../config/infra/PlatformUrlCheckParam.java | 23 +++ .../infra/PlatformWebsiteConfigParam.java | 63 ++++++++ .../config/infra/ConnectivityCheckResult.java | 49 ++++++ .../infra/PlatformWebsiteConfigResult.java | 61 +++++++ .../infra/PlatformOssConfigService.java | 150 ++++++++++++++++++ .../infra/PlatformUrlConfigService.java | 76 +++++++++ .../infra/PlatformWebsiteConfigService.java | 43 +++++ 22 files changed, 689 insertions(+), 1 deletion(-) create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/system/oss.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/system/url.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/system/oss.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/system/url.json create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/infra/PlatformWebsiteConfigController.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/convert/config/infra/PlatformWebsiteConfigConvert.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/infra/PlatformWebsiteConfig.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/infra/PlatformUrlCheckParam.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/infra/PlatformWebsiteConfigParam.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/infra/ConnectivityCheckResult.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/infra/PlatformWebsiteConfigResult.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/infra/PlatformWebsiteConfigService.java diff --git a/daxpay-platform/daxpay-platform-capability/capability-file/src/main/java/cn/daxpay/open/platform/capability/file/service/StorageClientService.java b/daxpay-platform/daxpay-platform-capability/capability-file/src/main/java/cn/daxpay/open/platform/capability/file/service/StorageClientService.java index 88955fc86..cd51cec59 100644 --- a/daxpay-platform/daxpay-platform-capability/capability-file/src/main/java/cn/daxpay/open/platform/capability/file/service/StorageClientService.java +++ b/daxpay-platform/daxpay-platform-capability/capability-file/src/main/java/cn/daxpay/open/platform/capability/file/service/StorageClientService.java @@ -10,6 +10,7 @@ import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import java.net.URI; @@ -92,6 +93,31 @@ public class StorageClientService { return s3Presigner; } + /// 丢弃缓存的 S3 客户端与预签名器 + /// + /// OSS 配置更新后调用, 下次访问按新配置重建客户端. + public void invalidate() { + synchronized (this) { + if (s3Client != null) { + try { + s3Client.close(); + } catch (Exception e) { + log.warn("关闭 S3Client 失败: {}", e.getMessage()); + } + s3Client = null; + } + if (s3Presigner != null) { + try { + s3Presigner.close(); + } catch (Exception e) { + log.warn("关闭 S3Presigner 失败: {}", e.getMessage()); + } + s3Presigner = null; + } + log.info("已使 S3 客户端缓存失效"); + } + } + /// 创建S3客户端 /// /// 根据配置创建S3客户端,支持自定义endpoint(用于MinIO等兼容存储)。 @@ -148,10 +174,16 @@ public class StorageClientService { builder.endpointOverride(URI.create(config.getEndpoint())); } - log.info("创建S3预签名器"); + // 与 S3Client 保持一致: MinIO 等需 path-style, 否则预签名 URL 主机形态不正确 + builder.serviceConfiguration(S3Configuration.builder() + .pathStyleAccessEnabled(config.isPathStyleAccess()) + .build()); + + log.info("创建S3预签名器: pathStyleAccess={}", config.isPathStyleAccess()); return builder.build(); } } + diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/platform_config_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/platform_config_type.json index 5c40855be..bee75c7a8 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/platform_config_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/platform_config_type.json @@ -4,5 +4,7 @@ "security_session": "Session management", "security_two_factor_auth": "Two-factor authentication", "url": "System URL settings", + "website": "Website display settings", "wechat_notify": "WeChat message notify template config" } + diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/system/oss.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/system/oss.json new file mode 100644 index 000000000..63e6ace84 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/system/oss.json @@ -0,0 +1,8 @@ +{ + "incomplete": "OSS configuration is incomplete. Please fill endpoint, buckets and credentials", + "checkSuccess": "OSS connectivity check passed", + "checkFailed": "OSS connectivity check failed, status: {0}", + "authFailed": "OSS authentication failed, please check AccessKey / SecretKey", + "bucketNotFound": "Bucket not found, please check public/private bucket names", + "networkError": "Cannot reach object storage, please check endpoint and network" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/system/url.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/system/url.json new file mode 100644 index 000000000..3648f9c79 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/system/url.json @@ -0,0 +1,7 @@ +{ + "notConfigured": "Access URL is not configured", + "checkSuccess": "Connectivity check passed", + "checkFailed": "Connectivity check failed, HTTP status: {0}", + "echoMismatch": "Connectivity check failed, backend echo response mismatch", + "networkError": "Network unreachable, please verify the URL and service availability" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json index 0787d1b52..4971ec12a 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json @@ -427,6 +427,9 @@ "privateBucket": { "notBlank": "Private bucket cannot be blank" }, + "urlType": { + "notBlank": "URL type cannot be blank" + }, "dictId": { "notNull": "Dictionary ID cannot be blank" }, diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/platform_config_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/platform_config_type.json index 4ff6c6f6d..131b4ed3f 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/platform_config_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/platform_config_type.json @@ -4,5 +4,7 @@ "security_session": "会话管理配置", "security_two_factor_auth": "双因素认证配置", "url": "系统访问地址配置", + "website": "站点显示内容配置", "wechat_notify": "微信消息通知模板配置" } + diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/system/oss.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/system/oss.json new file mode 100644 index 000000000..666ca973f --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/system/oss.json @@ -0,0 +1,8 @@ +{ + "incomplete": "OSS 配置不完整, 请填写服务端点、存储桶与密钥", + "checkSuccess": "OSS 连通性检查通过", + "checkFailed": "OSS 连通性检查失败, 状态码: {0}", + "authFailed": "OSS 认证失败, 请检查 AccessKey / SecretKey", + "bucketNotFound": "存储桶不存在, 请检查公开/私有桶名称", + "networkError": "无法连接对象存储, 请检查端点与网络" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/system/url.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/system/url.json new file mode 100644 index 000000000..a0a2287ab --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/system/url.json @@ -0,0 +1,7 @@ +{ + "notConfigured": "访问地址未配置", + "checkSuccess": "连通性检查通过", + "checkFailed": "连通性检查失败, HTTP 状态码: {0}", + "echoMismatch": "连通性检查失败, 后端 echo 响应不符合预期", + "networkError": "网络不通, 请检查地址是否正确或服务是否可达" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json index b1f63f431..457c60875 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json @@ -430,6 +430,9 @@ "privateBucket": { "notBlank": "私有存储桶不能为空" }, + "urlType": { + "notBlank": "端点类型不能为空" + }, "dictId": { "notNull": "字典ID不可为空" }, diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/infra/PlatformOssConfigController.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/infra/PlatformOssConfigController.java index 560293c9e..a4b734aea 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/infra/PlatformOssConfigController.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/infra/PlatformOssConfigController.java @@ -6,6 +6,7 @@ import cn.daxpay.open.platform.core.annotation.PermCode; import cn.daxpay.open.platform.core.rest.Res; import cn.daxpay.open.platform.core.rest.result.Result; import cn.daxpay.open.platform.system.param.config.infra.PlatformOssConfigParam; +import cn.daxpay.open.platform.system.result.config.infra.ConnectivityCheckResult; import cn.daxpay.open.platform.system.result.config.infra.PlatformOssConfigResult; import cn.daxpay.open.platform.system.service.config.infra.PlatformOssConfigService; import io.swagger.v3.oas.annotations.Operation; @@ -33,6 +34,13 @@ public class PlatformOssConfigController { return Res.ok(platformOssConfigService.findOssConfig()); } + @PermCode(code = PermCodes.Action.VIEW, nameCn = "OSS配置查看", nameEn = "OSS Config View") + @Operation(summary = "检查OSS连通性") + @PostMapping("/check") + public Result checkOss(@RequestBody(required = false) PlatformOssConfigParam param) { + return Res.ok(platformOssConfigService.checkOss(param)); + } + @PermCode(code = PermCodes.Action.MANAGE, nameCn = "OSS配置管理", nameEn = "OSS Config Manage") @Operation(summary = "更新OSS配置") @PostMapping("/update") diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/infra/PlatformUrlConfigController.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/infra/PlatformUrlConfigController.java index d2bc67071..5cb0099f0 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/infra/PlatformUrlConfigController.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/infra/PlatformUrlConfigController.java @@ -5,7 +5,9 @@ import cn.daxpay.open.platform.core.code.PermCodes; import cn.daxpay.open.platform.core.annotation.PermCode; import cn.daxpay.open.platform.core.rest.Res; import cn.daxpay.open.platform.core.rest.result.Result; +import cn.daxpay.open.platform.system.param.config.infra.PlatformUrlCheckParam; import cn.daxpay.open.platform.system.param.config.infra.PlatformUrlConfigParam; +import cn.daxpay.open.platform.system.result.config.infra.ConnectivityCheckResult; import cn.daxpay.open.platform.system.result.config.infra.PlatformUrlConfigResult; import cn.daxpay.open.platform.system.service.config.infra.PlatformUrlConfigService; import io.swagger.v3.oas.annotations.Operation; @@ -33,6 +35,13 @@ public class PlatformUrlConfigController { return Res.ok(platformUrlConfigService.findUrlConfig()); } + @PermCode(code = PermCodes.Action.VIEW, nameCn = "平台配置查看", nameEn = "Platform Config View") + @Operation(summary = "检查端点连通性") + @PostMapping("/check") + public Result checkUrl(@RequestBody @Validated PlatformUrlCheckParam param) { + return Res.ok(platformUrlConfigService.checkUrl(param)); + } + @PermCode(code = PermCodes.Action.MANAGE, nameCn = "平台配置管理", nameEn = "Platform Config Manage") @Operation(summary = "更新端点配置") @PostMapping("/update") diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/infra/PlatformWebsiteConfigController.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/infra/PlatformWebsiteConfigController.java new file mode 100644 index 000000000..80a9af077 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/infra/PlatformWebsiteConfigController.java @@ -0,0 +1,45 @@ +package cn.daxpay.open.platform.system.controller.config.infra; + +import cn.daxpay.open.platform.core.annotation.IgnoreAuth; +import cn.daxpay.open.platform.core.annotation.PermCode; +import cn.daxpay.open.platform.core.code.PermCodes; +import cn.daxpay.open.platform.core.rest.Res; +import cn.daxpay.open.platform.core.rest.result.Result; +import cn.daxpay.open.platform.system.param.config.infra.PlatformWebsiteConfigParam; +import cn.daxpay.open.platform.system.result.config.infra.PlatformWebsiteConfigResult; +import cn.daxpay.open.platform.system.service.config.infra.PlatformWebsiteConfigService; +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.*; + +/// # 平台站点配置 +/// +/// 管理系统名称、Logo、备案与版权等展示配置 +@PermCode(menuCode = PermCodes.System.PlatformConfig.MENU) +@Validated +@Tag(name = "平台站点配置") +@RestController +@RequestMapping("/platform/config/website") +@RequiredArgsConstructor +public class PlatformWebsiteConfigController { + + private final PlatformWebsiteConfigService platformWebsiteConfigService; + + /// 获取站点配置(免登录, 供登录页品牌展示) + @IgnoreAuth + @Operation(summary = "获取站点配置") + @GetMapping("/get") + public Result getWebsiteConfig() { + return Res.ok(platformWebsiteConfigService.findWebsiteConfig()); + } + + @PermCode(code = PermCodes.Action.MANAGE, nameCn = "平台配置管理", nameEn = "Platform Config Manage") + @Operation(summary = "更新站点配置") + @PostMapping("/update") + public Result updateWebsiteConfig(@RequestBody @Validated PlatformWebsiteConfigParam param) { + platformWebsiteConfigService.updateWebsiteConfig(param); + return Res.ok(); + } +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/convert/config/infra/PlatformWebsiteConfigConvert.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/convert/config/infra/PlatformWebsiteConfigConvert.java new file mode 100644 index 000000000..7352bbafa --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/convert/config/infra/PlatformWebsiteConfigConvert.java @@ -0,0 +1,25 @@ +package cn.daxpay.open.platform.system.convert.config.infra; + +import cn.daxpay.open.platform.system.entity.config.platform.infra.PlatformWebsiteConfig; +import cn.daxpay.open.platform.system.param.config.infra.PlatformWebsiteConfigParam; +import cn.daxpay.open.platform.system.result.config.infra.PlatformWebsiteConfigResult; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +/// # 平台站点配置转换 +/// +@Mapper +public interface PlatformWebsiteConfigConvert { + PlatformWebsiteConfigConvert CONVERT = Mappers.getMapper(PlatformWebsiteConfigConvert.class); + + PlatformWebsiteConfigResult toResult(PlatformWebsiteConfig data); + + PlatformWebsiteConfig convert(PlatformWebsiteConfigParam param); + + /// 站点字段允许清空(传 null 覆盖原值) + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_NULL) + void copy(PlatformWebsiteConfigParam param, @MappingTarget PlatformWebsiteConfig data); +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/infra/PlatformWebsiteConfig.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/infra/PlatformWebsiteConfig.java new file mode 100644 index 000000000..c0a061be8 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/infra/PlatformWebsiteConfig.java @@ -0,0 +1,62 @@ +package cn.daxpay.open.platform.system.entity.config.platform.infra; + +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 平台站点显示内容配置 +/// +/// 系统名称、公司信息、Logo、备案与版权等展示配置. +/// 全局唯一, 通过 [PlatformConfigTypeEnum.WEBSITE] 以 JSON 存于 `system_platform_config`. +/// +@Data +@Accessors(chain = true) +public class PlatformWebsiteConfig { + + /// 系统名称 + private String systemName; + + /// 公司全称 + private String companyName; + + /// 公司电话 + private String companyPhone; + + /// 公司邮箱 + private String companyEmail; + + /// 系统完整 logo(公开文件 filename) + private String wholeLogo; + + /// 系统简化 logo(公开文件 filename) + private String simpleLogo; + + /// 工信部 ICP 备案信息 + private String icpInfo; + + /// 工信部 ICP 链接地址 + private String icpLink; + + /// 公网安备案信息 + private String mpsInfo; + + /// 公网安备案链接地址 + private String mpsLink; + + /// 中国支付清算协会备案信息 + private String pcacInfo; + + /// 中国支付清算协会备案链接地址 + private String pcacLink; + + /// 电信增值业务许可信息 + private String icpPlusInfo; + + /// 电信增值业务许可链接地址 + private String icpPlusLink; + + /// 版权信息 + private String copyright; + + /// 版权信息链接 + private String copyrightLink; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/enums/PlatformConfigTypeEnum.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/enums/PlatformConfigTypeEnum.java index 70abf3e27..eab73a3d0 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/enums/PlatformConfigTypeEnum.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/enums/PlatformConfigTypeEnum.java @@ -23,6 +23,8 @@ public enum PlatformConfigTypeEnum implements I18nSupport { SECURITY_TWO_FACTOR_AUTH("security_two_factor_auth"), /// 系统访问地址配置 URL("url"), + /// 站点显示内容配置(系统名/Logo/备案/版权等) + WEBSITE("website"), /// 微信消息通知模板配置(非敏感, 仅模板Id) WECHAT_NOTIFY("wechat_notify"); diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/infra/PlatformUrlCheckParam.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/infra/PlatformUrlCheckParam.java new file mode 100644 index 000000000..9adac8271 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/infra/PlatformUrlCheckParam.java @@ -0,0 +1,23 @@ +package cn.daxpay.open.platform.system.param.config.infra; + +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 PlatformUrlCheckParam { + + /// 端点类型: admin / merchant / paymentGateway / backend + @NotBlank(message = "{validation.field.urlType.notBlank}") + @Schema(description = "端点类型: admin / merchant / paymentGateway / backend") + private String urlType; + + /// 待检查地址(可选; 为空则使用已保存配置) + @Schema(description = "待检查地址, 为空则使用已保存配置") + private String url; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/infra/PlatformWebsiteConfigParam.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/infra/PlatformWebsiteConfigParam.java new file mode 100644 index 000000000..6feaa7f79 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/infra/PlatformWebsiteConfigParam.java @@ -0,0 +1,63 @@ +package cn.daxpay.open.platform.system.param.config.infra; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 平台站点配置参数 +/// +/// 全部选填, 未配置的字段前端展示时回落默认品牌文案. +/// +@Data +@Accessors(chain = true) +@Schema(title = "平台站点配置参数") +public class PlatformWebsiteConfigParam { + + @Schema(description = "系统名称") + private String systemName; + + @Schema(description = "公司全称") + private String companyName; + + @Schema(description = "公司电话") + private String companyPhone; + + @Schema(description = "公司邮箱") + private String companyEmail; + + @Schema(description = "系统完整 logo") + private String wholeLogo; + + @Schema(description = "系统简化 logo") + private String simpleLogo; + + @Schema(description = "工信部 ICP 备案信息") + private String icpInfo; + + @Schema(description = "工信部 ICP 链接地址") + private String icpLink; + + @Schema(description = "公网安备案信息") + private String mpsInfo; + + @Schema(description = "公网安备案链接地址") + private String mpsLink; + + @Schema(description = "中国支付清算协会备案信息") + private String pcacInfo; + + @Schema(description = "中国支付清算协会备案链接地址") + private String pcacLink; + + @Schema(description = "电信增值业务许可信息") + private String icpPlusInfo; + + @Schema(description = "电信增值业务许可链接地址") + private String icpPlusLink; + + @Schema(description = "版权信息") + private String copyright; + + @Schema(description = "版权信息链接") + private String copyrightLink; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/infra/ConnectivityCheckResult.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/infra/ConnectivityCheckResult.java new file mode 100644 index 000000000..0661df5e6 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/infra/ConnectivityCheckResult.java @@ -0,0 +1,49 @@ +package cn.daxpay.open.platform.system.result.config.infra; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 连通性检查结果 +/// +@Data +@Accessors(chain = true) +@Schema(title = "连通性检查结果") +public class ConnectivityCheckResult { + + /// 是否成功 + @Schema(description = "是否成功") + private boolean success; + + /// 提示信息(已国际化) + @Schema(description = "提示信息") + private String message; + + /// HTTP 状态码(端点探测时有值) + @Schema(description = "HTTP 状态码") + private Integer statusCode; + + /// 耗时毫秒 + @Schema(description = "耗时毫秒") + private Long latencyMs; + + public static ConnectivityCheckResult ok(String message, Long latencyMs, Integer statusCode) { + return new ConnectivityCheckResult() + .setSuccess(true) + .setMessage(message) + .setLatencyMs(latencyMs) + .setStatusCode(statusCode); + } + + public static ConnectivityCheckResult fail(String message, Long latencyMs, Integer statusCode) { + return new ConnectivityCheckResult() + .setSuccess(false) + .setMessage(message) + .setLatencyMs(latencyMs) + .setStatusCode(statusCode); + } + + public static ConnectivityCheckResult fail(String message) { + return fail(message, null, null); + } +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/infra/PlatformWebsiteConfigResult.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/infra/PlatformWebsiteConfigResult.java new file mode 100644 index 000000000..8e3265d64 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/infra/PlatformWebsiteConfigResult.java @@ -0,0 +1,61 @@ +package cn.daxpay.open.platform.system.result.config.infra; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 平台站点配置返回结果 +/// +@Data +@Accessors(chain = true) +@Schema(title = "平台站点配置") +public class PlatformWebsiteConfigResult { + + @Schema(description = "系统名称") + private String systemName; + + @Schema(description = "公司全称") + private String companyName; + + @Schema(description = "公司电话") + private String companyPhone; + + @Schema(description = "公司邮箱") + private String companyEmail; + + @Schema(description = "系统完整 logo") + private String wholeLogo; + + @Schema(description = "系统简化 logo") + private String simpleLogo; + + @Schema(description = "工信部 ICP 备案信息") + private String icpInfo; + + @Schema(description = "工信部 ICP 链接地址") + private String icpLink; + + @Schema(description = "公网安备案信息") + private String mpsInfo; + + @Schema(description = "公网安备案链接地址") + private String mpsLink; + + @Schema(description = "中国支付清算协会备案信息") + private String pcacInfo; + + @Schema(description = "中国支付清算协会备案链接地址") + private String pcacLink; + + @Schema(description = "电信增值业务许可信息") + private String icpPlusInfo; + + @Schema(description = "电信增值业务许可链接地址") + private String icpPlusLink; + + @Schema(description = "版权信息") + private String copyright; + + @Schema(description = "版权信息链接") + private String copyrightLink; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/infra/PlatformOssConfigService.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/infra/PlatformOssConfigService.java index 041de9d57..43c527d5e 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/infra/PlatformOssConfigService.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/infra/PlatformOssConfigService.java @@ -1,14 +1,28 @@ package cn.daxpay.open.platform.system.service.config.infra; +import cn.daxpay.open.platform.capability.file.service.StorageClientService; +import cn.daxpay.open.platform.common.i18n.util.I18nUtil; import cn.daxpay.open.platform.system.convert.config.infra.PlatformOssConfigConvert; import cn.daxpay.open.platform.system.entity.config.platform.infra.PlatformOssConfig; import cn.daxpay.open.platform.system.enums.EncryptPlatformConfigTypeEnum; import cn.daxpay.open.platform.system.param.config.infra.PlatformOssConfigParam; +import cn.daxpay.open.platform.system.result.config.infra.ConnectivityCheckResult; import cn.daxpay.open.platform.system.result.config.infra.PlatformOssConfigResult; import cn.daxpay.open.platform.system.service.config.SystemPlatformEncryptConfigService; +import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.NoSuchBucketException; +import software.amazon.awssdk.services.s3.model.S3Exception; + +import java.net.URI; /// # 平台OSS配置服务 /// @@ -19,6 +33,11 @@ import org.springframework.stereotype.Service; public class PlatformOssConfigService { private final SystemPlatformEncryptConfigService encryptConfigService; + /// ObjectProvider 延迟获取, 避免与 StorageClientService → OssConfigProvider → 本类 循环依赖 + private final ObjectProvider storageClientServiceProvider; + + /// S3 兼容存储默认区域 + private static final String DEFAULT_REGION = "us-east-1"; /// 获取OSS配置 public PlatformOssConfig getOssConfig() { @@ -37,5 +56,136 @@ public class PlatformOssConfigService { PlatformOssConfig data = this.getOssConfig(); PlatformOssConfigConvert.CONVERT.copy(param, data); encryptConfigService.updateConfig(EncryptPlatformConfigTypeEnum.OSS, data); + // 配置变更后丢弃缓存的 S3 客户端, 下次访问按新配置重建 + StorageClientService storageClientService = storageClientServiceProvider.getIfAvailable(); + if (storageClientService != null) { + storageClientService.invalidate(); + } + } + + /// 检查 OSS 连通性 + /// + /// 请求体可带表单中的非敏感字段; accessKey/secretKey 为空时使用库中已存密钥. + /// 使用临时 S3Client, 不影响全局缓存. + public ConnectivityCheckResult checkOss(PlatformOssConfigParam param) { + PlatformOssConfig saved = this.getOssConfig(); + PlatformOssConfig probe = mergeForCheck(saved, param); + + if (StrUtil.hasBlank(probe.getEndpoint(), probe.getPublicBucket(), probe.getPrivateBucket(), + probe.getAccessKey(), probe.getSecretKey())) { + return ConnectivityCheckResult.fail(I18nUtil.get("error.system.oss.incomplete")); + } + + long start = System.currentTimeMillis(); + try (S3Client client = buildTempClient(probe)) { + headBucket(client, probe.getPublicBucket()); + headBucket(client, probe.getPrivateBucket()); + long latency = System.currentTimeMillis() - start; + return ConnectivityCheckResult.ok( + I18nUtil.get("error.system.oss.checkSuccess"), + latency, + null); + } catch (NoSuchBucketException e) { + long latency = System.currentTimeMillis() - start; + log.warn("OSS 桶不存在: {}", e.getMessage()); + return ConnectivityCheckResult.fail( + I18nUtil.get("error.system.oss.bucketNotFound"), + latency, + e.statusCode()); + } catch (S3Exception e) { + long latency = System.currentTimeMillis() - start; + log.warn("OSS 连通性检查失败: status={}, msg={}", e.statusCode(), e.getMessage()); + if (e.statusCode() == 403 || e.statusCode() == 401) { + return ConnectivityCheckResult.fail( + I18nUtil.get("error.system.oss.authFailed"), + latency, + e.statusCode()); + } + return ConnectivityCheckResult.fail( + I18nUtil.get("error.system.oss.checkFailed", String.valueOf(e.statusCode())), + latency, + e.statusCode()); + } catch (Exception e) { + long latency = System.currentTimeMillis() - start; + log.warn("OSS 连通性检查网络异常: {}", e.getMessage()); + return ConnectivityCheckResult.fail( + I18nUtil.get("error.system.oss.networkError"), + latency, + null); + } + } + + /// 合并探测用配置: 请求非空字段覆盖库配置; 密钥仅在请求显式传入时覆盖 + private PlatformOssConfig mergeForCheck(PlatformOssConfig saved, PlatformOssConfigParam param) { + PlatformOssConfig probe = new PlatformOssConfig(); + if (saved != null) { + probe.setEndpoint(saved.getEndpoint()) + .setRegion(saved.getRegion()) + .setPublicBucket(saved.getPublicBucket()) + .setPrivateBucket(saved.getPrivateBucket()) + .setPublicBaseUrl(saved.getPublicBaseUrl()) + .setPrivateBaseUrl(saved.getPrivateBaseUrl()) + .setAccessKey(saved.getAccessKey()) + .setSecretKey(saved.getSecretKey()) + .setPathStyleAccess(saved.getPathStyleAccess()) + .setUploadExpireMinutes(saved.getUploadExpireMinutes()) + .setDownloadExpireHours(saved.getDownloadExpireHours()) + .setBasePath(saved.getBasePath()); + } + if (param == null) { + return probe; + } + if (StrUtil.isNotBlank(param.getEndpoint())) { + probe.setEndpoint(param.getEndpoint()); + } + if (param.getRegion() != null) { + probe.setRegion(param.getRegion()); + } + if (StrUtil.isNotBlank(param.getPublicBucket())) { + probe.setPublicBucket(param.getPublicBucket()); + } + if (StrUtil.isNotBlank(param.getPrivateBucket())) { + probe.setPrivateBucket(param.getPrivateBucket()); + } + if (param.getPublicBaseUrl() != null) { + probe.setPublicBaseUrl(param.getPublicBaseUrl()); + } + if (param.getPrivateBaseUrl() != null) { + probe.setPrivateBaseUrl(param.getPrivateBaseUrl()); + } + if (param.getPathStyleAccess() != null) { + probe.setPathStyleAccess(param.getPathStyleAccess()); + } + if (param.getBasePath() != null) { + probe.setBasePath(param.getBasePath()); + } + // 密钥: 仅当请求携带非空明文时覆盖(前端未改密钥时不传) + if (StrUtil.isNotBlank(param.getAccessKey())) { + probe.setAccessKey(param.getAccessKey()); + } + if (StrUtil.isNotBlank(param.getSecretKey())) { + probe.setSecretKey(param.getSecretKey()); + } + return probe; + } + + private S3Client buildTempClient(PlatformOssConfig config) { + AwsBasicCredentials credentials = AwsBasicCredentials.create( + config.getAccessKey(), + config.getSecretKey() + ); + String region = StrUtil.blankToDefault(config.getRegion(), DEFAULT_REGION); + var builder = S3Client.builder() + .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .region(Region.of(region)); + if (StrUtil.isNotBlank(config.getEndpoint())) { + builder.endpointOverride(URI.create(config.getEndpoint())); + } + builder.forcePathStyle(Boolean.TRUE.equals(config.getPathStyleAccess())); + return builder.build(); + } + + private void headBucket(S3Client client, String bucket) { + client.headBucket(HeadBucketRequest.builder().bucket(bucket).build()); } } diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/infra/PlatformUrlConfigService.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/infra/PlatformUrlConfigService.java index 2c8c46e36..bde7bf6e6 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/infra/PlatformUrlConfigService.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/infra/PlatformUrlConfigService.java @@ -1,11 +1,17 @@ package cn.daxpay.open.platform.system.service.config.infra; +import cn.daxpay.open.platform.common.i18n.util.I18nUtil; import cn.daxpay.open.platform.system.convert.config.infra.PlatformUrlConfigConvert; import cn.daxpay.open.platform.system.entity.config.platform.infra.PlatformUrlConfig; import cn.daxpay.open.platform.system.enums.PlatformConfigTypeEnum; +import cn.daxpay.open.platform.system.param.config.infra.PlatformUrlCheckParam; import cn.daxpay.open.platform.system.param.config.infra.PlatformUrlConfigParam; +import cn.daxpay.open.platform.system.result.config.infra.ConnectivityCheckResult; import cn.daxpay.open.platform.system.result.config.infra.PlatformUrlConfigResult; import cn.daxpay.open.platform.system.service.config.SystemPlatformConfigService; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -20,6 +26,9 @@ public class PlatformUrlConfigService { private final SystemPlatformConfigService systemConfigService; + /// HTTP 探测超时(毫秒) + private static final int CHECK_TIMEOUT_MS = 5000; + /// 获取端点配置 public PlatformUrlConfig getUrlConfig() { return systemConfigService.getOrCreateConfig(PlatformConfigTypeEnum.URL, @@ -38,4 +47,71 @@ public class PlatformUrlConfigService { PlatformUrlConfigConvert.CONVERT.copy(param, data); systemConfigService.updateConfig(PlatformConfigTypeEnum.URL, data); } + + /// 检查端点连通性 + /// + /// @param param 端点类型与可选 URL + /// @return 探测结果 + public ConnectivityCheckResult checkUrl(PlatformUrlCheckParam param) { + String urlType = StrUtil.trim(param.getUrlType()); + String targetUrl = StrUtil.blankToDefault(StrUtil.trim(param.getUrl()), resolveSavedUrl(urlType)); + if (StrUtil.isBlank(targetUrl)) { + return ConnectivityCheckResult.fail(I18nUtil.get("error.system.url.notConfigured")); + } + targetUrl = StrUtil.removeSuffix(targetUrl, "/"); + + // 后端 API 走 /echo 严格校验; 前端 SPA 仅校验 HTTP 可达 + boolean backendEcho = "backend".equalsIgnoreCase(urlType); + String probeUrl = backendEcho ? targetUrl + "/echo" : targetUrl; + + long start = System.currentTimeMillis(); + try (HttpResponse response = HttpRequest.get(probeUrl) + .timeout(CHECK_TIMEOUT_MS) + .setFollowRedirects(true) + .execute()) { + long latency = System.currentTimeMillis() - start; + int status = response.getStatus(); + if (status < 200 || status >= 400) { + return ConnectivityCheckResult.fail( + I18nUtil.get("error.system.url.checkFailed", String.valueOf(status)), + latency, + status); + } + if (backendEcho) { + String body = StrUtil.nullToEmpty(response.body()); + if (!body.startsWith("echo")) { + return ConnectivityCheckResult.fail( + I18nUtil.get("error.system.url.echoMismatch"), + latency, + status); + } + } + return ConnectivityCheckResult.ok( + I18nUtil.get("error.system.url.checkSuccess"), + latency, + status); + } catch (Exception e) { + long latency = System.currentTimeMillis() - start; + log.warn("端点连通性检查失败: type={}, url={}, err={}", urlType, probeUrl, e.getMessage()); + return ConnectivityCheckResult.fail( + I18nUtil.get("error.system.url.networkError"), + latency, + null); + } + } + + /// 从已保存配置解析对应端点地址 + private String resolveSavedUrl(String urlType) { + PlatformUrlConfig config = this.getUrlConfig(); + if (config == null || StrUtil.isBlank(urlType)) { + return null; + } + return switch (urlType.toLowerCase()) { + case "admin" -> config.getAdminBaseUrl(); + case "merchant" -> config.getMerchantBaseUrl(); + case "paymentgateway" -> config.getPaymentGatewayBaseUrl(); + case "backend" -> config.getBackendBaseUrl(); + default -> null; + }; + } } diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/infra/PlatformWebsiteConfigService.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/infra/PlatformWebsiteConfigService.java new file mode 100644 index 000000000..89b9ee7b5 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/infra/PlatformWebsiteConfigService.java @@ -0,0 +1,43 @@ +package cn.daxpay.open.platform.system.service.config.infra; + +import cn.daxpay.open.platform.system.convert.config.infra.PlatformWebsiteConfigConvert; +import cn.daxpay.open.platform.system.entity.config.platform.infra.PlatformWebsiteConfig; +import cn.daxpay.open.platform.system.enums.PlatformConfigTypeEnum; +import cn.daxpay.open.platform.system.param.config.infra.PlatformWebsiteConfigParam; +import cn.daxpay.open.platform.system.result.config.infra.PlatformWebsiteConfigResult; +import cn.daxpay.open.platform.system.service.config.SystemPlatformConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 平台站点配置服务 +/// +/// 管理系统名称、Logo、备案与版权等展示配置, 存于 `system_platform_config`. +@Slf4j +@Service +@RequiredArgsConstructor +public class PlatformWebsiteConfigService { + + private final SystemPlatformConfigService systemConfigService; + + /// 获取站点配置 + public PlatformWebsiteConfig getWebsiteConfig() { + return systemConfigService.getOrCreateConfig(PlatformConfigTypeEnum.WEBSITE, + PlatformWebsiteConfig.class, + new PlatformWebsiteConfig()); + } + + /// 获取站点配置 + public PlatformWebsiteConfigResult findWebsiteConfig() { + return PlatformWebsiteConfigConvert.CONVERT.toResult(this.getWebsiteConfig()); + } + + /// 更新站点配置(整包覆盖, 允许清空字段) + public void updateWebsiteConfig(PlatformWebsiteConfigParam param) { + PlatformWebsiteConfig data = PlatformWebsiteConfigConvert.CONVERT.convert(param); + if (data == null) { + data = new PlatformWebsiteConfig(); + } + systemConfigService.updateConfig(PlatformConfigTypeEnum.WEBSITE, data); + } +}