refactor: 存储模式动态检查环境变量,改PAR_STORAGE_TYPE无需重启

- CachedStorageService 每次调用读 PAR_STORAGE_TYPE 环境变量
- QiniuStorageService 首次调用懒初始化,读 QINIU_* 环境变量
- 去掉 @ConditionalOnProperty/@ConditionalOnMissingBean
This commit is contained in:
mediabot-pt
2026-06-29 23:13:38 +08:00
parent cd78cfbf88
commit d093c5eb28
5 changed files with 72 additions and 87 deletions

View File

@@ -6,9 +6,8 @@ import com.par.core.entity.SiteConfig;
import com.par.core.enums.ConfigStatus;
import com.par.core.mapper.SiteConfigMapper;
import com.par.core.service.ConfigService;
import com.par.core.storage.StorageService;
import com.par.core.storage.CachedStorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
@@ -20,14 +19,12 @@ import java.util.stream.Collectors;
public class ConfigServiceImpl implements ConfigService {
private final SiteConfigMapper siteConfigMapper;
private final StorageService storageService;
private final CachedStorageService storageService;
public ConfigServiceImpl(SiteConfigMapper siteConfigMapper,
@Autowired(required = false) StorageService storageService) {
CachedStorageService storageService) {
this.siteConfigMapper = siteConfigMapper;
// 未注入 StorageService 时(极端情况),回退到本地文件存储
this.storageService = storageService != null ? storageService
: new com.par.core.storage.LocalStorageService("./data/static");
this.storageService = storageService;
}
@Override

View File

@@ -1,61 +1,58 @@
package com.par.core.storage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Service;
/**
* 缓存存储:七牛(主) + 本地(读缓存/备份)。
* <p>
* 写:先写七牛,成功后再缓存到本地
* 读:本地缓存命中 → 返回;未命中 → 七牛拉取 → 缓存本地 → 返回
* 动态存储:运行时判断 PAR_STORAGE_TYPE 环境变量。
* qiniu → 七牛主存储 + 本地缓存,否则 → 纯本地存储。
* 修改环境变量后无需重启,下次请求即生效。
*/
@Slf4j
@Service
@ConditionalOnBean(QiniuStorageService.class)
public class CachedStorageService implements StorageService {
private final QiniuStorageService primary;
private final StorageService cache;
private final QiniuStorageService qiniu;
private final LocalStorageService local;
public CachedStorageService(QiniuStorageService primary, LocalStorageService cache) {
this.primary = primary;
this.cache = cache;
log.info("缓存存储就绪: 七牛(主) + 本地(缓存)");
public CachedStorageService(QiniuStorageService qiniu, LocalStorageService local) {
this.qiniu = qiniu;
this.local = local;
log.info("动态存储就绪: {} 模式", isQiniuMode() ? "qiniu+本地缓存" : "本地");
}
private boolean isQiniuMode() {
return "qiniu".equalsIgnoreCase(System.getenv("PAR_STORAGE_TYPE"));
}
@Override
public void store(String path, String content) {
// 先写七牛
primary.store(path, content);
// 再写本地缓存(失败不影响)
try {
cache.store(path, content);
} catch (Exception e) {
log.warn("写本地缓存失败 {}: {}", path, e.getMessage());
if (isQiniuMode()) {
qiniu.store(path, content);
try { local.store(path, content); } catch (Exception ignored) {}
} else {
local.store(path, content);
}
}
@Override
public String read(String path) {
// 本地缓存命中
String cached = cache.read(path);
if (cached != null) return cached;
// 从七牛拉取
String remote = primary.read(path);
if (remote != null) {
// 回写本地缓存
try {
cache.store(path, remote);
} catch (Exception ignored) {}
// qiniu 模式:本地缓存优先
if (isQiniuMode()) {
String cached = local.read(path);
if (cached != null) return cached;
String remote = qiniu.read(path);
if (remote != null) {
try { local.store(path, remote); } catch (Exception ignored) {}
}
return remote;
}
return null;
return local.read(path);
}
@Override
public boolean exists(String path) {
return cache.exists(path) || primary.exists(path);
if (isQiniuMode()) return local.exists(path) || qiniu.exists(path);
return local.exists(path);
}
}

View File

@@ -3,7 +3,6 @@ package com.par.core.storage;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.stereotype.Service;
import java.io.IOException;
@@ -18,7 +17,6 @@ import java.nio.file.Paths;
*/
@Slf4j
@Service
@ConditionalOnMissingBean(QiniuStorageService.class)
public class LocalStorageService implements StorageService {
private final Path baseDir;

View File

@@ -7,8 +7,6 @@ import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.util.Auth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.net.URI;
@@ -18,46 +16,48 @@ import java.net.http.HttpResponse;
import java.time.Duration;
/**
* 七牛云存储(主存储)。
* 读文件通过公开 URL 直接 HTTP GET,不依赖 SDK 内部 API。
* 七牛云存储。
* 每次调用时从环境变量读取配置,支持热更新无需重启。
*/
@Slf4j
@Service
@ConditionalOnProperty(name = "par.storage.type", havingValue = "qiniu")
public class QiniuStorageService implements StorageService {
private final UploadManager uploadManager;
private final BucketManager bucketManager;
private final Auth auth;
private final String bucket;
private final String cdnDomain;
private final HttpClient httpClient;
private final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
public QiniuStorageService(
@Value("${par.storage.qiniu.access-key:}") String ak,
@Value("${par.storage.qiniu.secret-key:}") String sk,
@Value("${par.storage.qiniu.bucket:par-configs}") String bucket,
@Value("${par.storage.qiniu.region:z2}") String region,
@Value("${par.storage.qiniu.domain:}") String domain) {
private UploadManager uploadManager;
private BucketManager bucketManager;
private Auth auth;
private String bucket;
private String cdnDomain;
this.bucket = bucket;
private synchronized void ensureInit() {
if (bucket != null) return;
String ak = env("QINIU_AK");
String sk = env("QINIU_SK");
String region = env("QINIU_REGION", "z2");
this.bucket = env("QINIU_BUCKET", "par-configs");
this.cdnDomain = env("QINIU_DOMAIN", "http://" + bucket + ".qiniucdn.com");
this.auth = Auth.create(ak, sk);
this.cdnDomain = domain != null && !domain.isBlank()
? domain : "http://" + bucket + ".qiniucdn.com";
Configuration cfg = new Configuration(getRegion(region));
Configuration cfg = new Configuration(switch (region) {
case "z0" -> Region.region0();
case "z1" -> Region.region1();
case "z2" -> Region.region2();
case "na0" -> Region.regionNa0();
case "as0" -> Region.regionAs0();
default -> Region.autoRegion();
});
this.uploadManager = new UploadManager(cfg);
this.bucketManager = new BucketManager(auth, cfg);
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
log.info("Qiniu 存储就绪: bucket={}, domain={}", bucket, cdnDomain);
}
@Override
public void store(String path, String content) {
ensureInit();
try {
uploadManager.put(content.getBytes(), path, auth.uploadToken(bucket));
log.debug("Qiniu 上传: {}", path);
@@ -69,16 +69,13 @@ public class QiniuStorageService implements StorageService {
@Override
public String read(String path) {
ensureInit();
try {
String url = cdnDomain + "/" + path;
var req = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(10))
.GET().build();
var req = HttpRequest.newBuilder().uri(URI.create(url))
.timeout(Duration.ofSeconds(10)).GET().build();
var resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() == 200) {
return resp.body();
}
if (resp.statusCode() == 200) return resp.body();
log.warn("Qiniu 读取失败 {}: HTTP {}", path, resp.statusCode());
} catch (Exception e) {
log.warn("Qiniu 读取失败 {}: {}", path, e.getMessage());
@@ -88,6 +85,7 @@ public class QiniuStorageService implements StorageService {
@Override
public boolean exists(String path) {
ensureInit();
try {
return bucketManager.stat(bucket, path) != null;
} catch (QiniuException e) {
@@ -95,14 +93,9 @@ public class QiniuStorageService implements StorageService {
}
}
private Region getRegion(String region) {
return switch (region) {
case "z0" -> Region.region0();
case "z1" -> Region.region1();
case "z2" -> Region.region2();
case "na0" -> Region.regionNa0();
case "as0" -> Region.regionAs0();
default -> Region.autoRegion();
};
private static String env(String key) { return System.getenv(key); }
private static String env(String key, String def) {
String v = System.getenv(key);
return v != null && !v.isEmpty() ? v : def;
}
}