mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-11 23:25:33 +08:00
feat(system): 平台配置新增站点展示配置与OSS/端点连通性检查
- 新增网站显示配置(系统名/Logo/备案/版权等)website 配置类型 - OSS 配置增加连通性检查接口, 支持合并库中密钥探测 - 端点配置增加连通性检查, 后端走 /echo 严格校验 - StorageClientService 新增 invalidate 方法, 配置更新后丢弃缓存客户端 - S3 预签名器补 pathStyleAccess 配置, 修正 MinIO 预签名 URL - 新增 oss/url 连通性检查中英文 i18n 错误消息
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -4,5 +4,7 @@
|
||||
"security_session": "会话管理配置",
|
||||
"security_two_factor_auth": "双因素认证配置",
|
||||
"url": "系统访问地址配置",
|
||||
"website": "站点显示内容配置",
|
||||
"wechat_notify": "微信消息通知模板配置"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"incomplete": "OSS 配置不完整, 请填写服务端点、存储桶与密钥",
|
||||
"checkSuccess": "OSS 连通性检查通过",
|
||||
"checkFailed": "OSS 连通性检查失败, 状态码: {0}",
|
||||
"authFailed": "OSS 认证失败, 请检查 AccessKey / SecretKey",
|
||||
"bucketNotFound": "存储桶不存在, 请检查公开/私有桶名称",
|
||||
"networkError": "无法连接对象存储, 请检查端点与网络"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"notConfigured": "访问地址未配置",
|
||||
"checkSuccess": "连通性检查通过",
|
||||
"checkFailed": "连通性检查失败, HTTP 状态码: {0}",
|
||||
"echoMismatch": "连通性检查失败, 后端 echo 响应不符合预期",
|
||||
"networkError": "网络不通, 请检查地址是否正确或服务是否可达"
|
||||
}
|
||||
@@ -430,6 +430,9 @@
|
||||
"privateBucket": {
|
||||
"notBlank": "私有存储桶不能为空"
|
||||
},
|
||||
"urlType": {
|
||||
"notBlank": "端点类型不能为空"
|
||||
},
|
||||
"dictId": {
|
||||
"notNull": "字典ID不可为空"
|
||||
},
|
||||
|
||||
@@ -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<ConnectivityCheckResult> 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")
|
||||
|
||||
@@ -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<ConnectivityCheckResult> 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")
|
||||
|
||||
@@ -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<PlatformWebsiteConfigResult> getWebsiteConfig() {
|
||||
return Res.ok(platformWebsiteConfigService.findWebsiteConfig());
|
||||
}
|
||||
|
||||
@PermCode(code = PermCodes.Action.MANAGE, nameCn = "平台配置管理", nameEn = "Platform Config Manage")
|
||||
@Operation(summary = "更新站点配置")
|
||||
@PostMapping("/update")
|
||||
public Result<Void> updateWebsiteConfig(@RequestBody @Validated PlatformWebsiteConfigParam param) {
|
||||
platformWebsiteConfigService.updateWebsiteConfig(param);
|
||||
return Res.ok();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<StorageClientService> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user