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

@@ -4,7 +4,7 @@ import com.par.core.dto.ApiResponse;
import com.par.core.dto.SiteConfigDTO; import com.par.core.dto.SiteConfigDTO;
import com.par.core.entity.SiteConfig; import com.par.core.entity.SiteConfig;
import com.par.core.service.ConfigService; import com.par.core.service.ConfigService;
import com.par.core.storage.StorageService; import com.par.core.storage.CachedStorageService;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -24,9 +24,9 @@ import java.util.Map;
public class ConfigController { public class ConfigController {
private final ConfigService configService; private final ConfigService configService;
private final StorageService storageService; private final CachedStorageService storageService;
public ConfigController(ConfigService configService, StorageService storageService) { public ConfigController(ConfigService configService, CachedStorageService storageService) {
this.configService = configService; this.configService = configService;
this.storageService = storageService; this.storageService = storageService;
} }

View File

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

View File

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

View File

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