feat(system): 平台配置新增站点展示配置与OSS/端点连通性检查

- 新增网站显示配置(系统名/Logo/备案/版权等)website 配置类型
- OSS 配置增加连通性检查接口, 支持合并库中密钥探测
- 端点配置增加连通性检查, 后端走 /echo 严格校验
- StorageClientService 新增 invalidate 方法, 配置更新后丢弃缓存客户端
- S3 预签名器补 pathStyleAccess 配置, 修正 MinIO 预签名 URL
- 新增 oss/url 连通性检查中英文 i18n 错误消息
This commit is contained in:
DaxPay Dev
2026-07-14 08:15:03 +08:00
parent 14b06b583b
commit a54450a998
22 changed files with 689 additions and 1 deletions

View File

@@ -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();
}
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
},

View File

@@ -4,5 +4,7 @@
"security_session": "会话管理配置",
"security_two_factor_auth": "双因素认证配置",
"url": "系统访问地址配置",
"website": "站点显示内容配置",
"wechat_notify": "微信消息通知模板配置"
}

View File

@@ -0,0 +1,8 @@
{
"incomplete": "OSS 配置不完整, 请填写服务端点、存储桶与密钥",
"checkSuccess": "OSS 连通性检查通过",
"checkFailed": "OSS 连通性检查失败, 状态码: {0}",
"authFailed": "OSS 认证失败, 请检查 AccessKey / SecretKey",
"bucketNotFound": "存储桶不存在, 请检查公开/私有桶名称",
"networkError": "无法连接对象存储, 请检查端点与网络"
}

View File

@@ -0,0 +1,7 @@
{
"notConfigured": "访问地址未配置",
"checkSuccess": "连通性检查通过",
"checkFailed": "连通性检查失败, HTTP 状态码: {0}",
"echoMismatch": "连通性检查失败, 后端 echo 响应不符合预期",
"networkError": "网络不通, 请检查地址是否正确或服务是否可达"
}

View File

@@ -430,6 +430,9 @@
"privateBucket": {
"notBlank": "私有存储桶不能为空"
},
"urlType": {
"notBlank": "端点类型不能为空"
},
"dictId": {
"notNull": "字典ID不可为空"
},

View File

@@ -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")

View File

@@ -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")

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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");

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -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;
};
}
}

View File

@@ -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);
}
}