refactor: 合并三级缓存为统一缓存 + 重命名 saveDraft → savePreview

1. 三个 Caffeine Cache 合并为一个 jsonCache,key 用 :published/:draft 后缀区分
2. saveDraft → savePreview,更准确反映「预览版」语义(非临时草稿)
3. getPreviewBundleJson 去掉独立缓存,直接查 DB(preview 是 draft+published 逻辑并集,缓存意义不大)
4. 前端按钮「保存草稿」→「保存预览版」,API 路径 /save-draft → /save-preview
This commit is contained in:
mediabot-pt
2026-07-07 11:45:04 +08:00
parent 3cc7b932a8
commit ebfb6395db
4 changed files with 65 additions and 81 deletions

View File

@@ -17,20 +17,20 @@ public interface SiteSnapshotService {
Map<String, Object> buildSnapshot(String siteId);
/**
* 更新站点的 draft 快照(任意配置变更后调用)
* 更新站点的预览版快照(任意配置变更后调用)
* 内容未变化(checksum 相同)则不产生新版本
* @return 新快照或 null(内容未变)
*/
SiteSnapshot updateSnapshot(String siteId);
/**
* 手动保存草稿快照(管理员触发,强制生成新 draft)
* 手动保存预览版快照(管理员触发)
* @return 新快照或 null
*/
SiteSnapshot saveDraft(String siteId);
SiteSnapshot savePreview(String siteId);
/**
* 发布指定站点的快照(draft → published)
* 发布指定站点的快照(预览版 → 正式版)
* 同时上传到七牛云 CDN 作为灾备
*/
SiteSnapshot publish(String siteId);
@@ -41,22 +41,21 @@ public interface SiteSnapshotService {
SiteSnapshot getActive(String siteId);
/**
* 获取站点当前生效快照的预序列化 JSON(带 Caffeine 缓存)
* 获取站点当前生效快照的预序列化 JSON(带缓存)
* 供 SnapshotController 使用,避免重复序列化
* @return JSON 字符串,空串表示无快照
*/
String getActiveBundleJson(String siteId);
/**
* 获取站点最新 draft 快照的预序列化 JSON(预览版,带 Caffeine 缓存)
* 获取站点最新预览版快照的预序列化 JSON(带缓存)
* @param siteId 站点标识
* @return JSON 字符串,空串表示无 draft
* @return JSON 字符串,空串表示无预览版
*/
String getDraftBundleJson(String siteId);
/**
* 获取预览版快照 JSON(draft 优先,无 draft 时 fallback 到 published)
* 即:正式版 + 预览版 的并集
* 获取预览快照 JSON(预览版优先,无预览版时 fallback 到正式版)
* @param siteId 站点标识
* @return JSON 字符串,空串表示无任何快照
*/

View File

@@ -43,23 +43,14 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
private final CachedStorageService storageService;
private static final ObjectMapper om = new ObjectMapper();
/** 已发布快照 JSON 缓存:key=siteId, value=预序列化的 bundle JSON */
private final Cache<String, String> bundleJsonCache = Caffeine.newBuilder()
/** 统一快照 JSON 缓存:key = siteId + ":published" | siteId + ":draft",value = 预序列化 bundle JSON */
private final Cache<String, String> jsonCache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(10))
.maximumSize(500)
.maximumSize(1000)
.build();
/** draft 快照 JSON 缓存:key=siteId, value=预序列化的 bundle JSON */
private final Cache<String, String> draftJsonCache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(5))
.maximumSize(500)
.build();
/** preview 快照 JSON 缓存:draft 优先 fallback published */
private final Cache<String, String> previewJsonCache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(5))
.maximumSize(500)
.build();
private static String pubKey(String siteId) { return siteId + ":published"; }
private static String draftKey(String siteId) { return siteId + ":draft"; }
@Override
public Map<String, Object> buildSnapshot(String siteId) {
@@ -134,18 +125,15 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
}
try {
// 计算 checksum
String contentJson = om.writeValueAsString(data);
String checksum = HmacUtil.sha256(contentJson);
// 查询最新 draft
SiteSnapshot latestDraft = snapshotMapper.selectLatestDraft(siteId);
if (latestDraft != null && checksum.equals(latestDraft.getChecksum())) {
log.debug("站点 {} 配置未变化,跳过快照生成", siteId);
return null;
}
// 确定新版本号
int newVersion = (latestDraft != null) ? latestDraft.getVersion() + 1 : 1;
SiteSnapshot snapshot = new SiteSnapshot();
@@ -157,10 +145,9 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
snapshotMapper.insert(snapshot);
// 刷新 draft 和 preview 缓存(审批通过后异步生成 draft,需立即可被预览查询到)
// 刷新缓存:审批通过后异步生成的预览版,需立即可被 ?preview=true 查询到
String bundleJson = toBundleJson(snapshot);
draftJsonCache.put(siteId, bundleJson);
previewJsonCache.put(siteId, bundleJson);
jsonCache.put(draftKey(siteId), bundleJson);
log.info("快照已更新: siteId={}, version={}, checksum={}", siteId, newVersion, checksum.substring(0, 8));
return snapshot;
@@ -172,10 +159,10 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
@Override
@Transactional
public SiteSnapshot saveDraft(String siteId) {
public SiteSnapshot savePreview(String siteId) {
Map<String, Object> data = buildSnapshot(siteId);
if (data == null) {
log.warn("站点 {} 无有效配置,无法保存草稿", siteId);
log.warn("站点 {} 无有效配置,无法保存预览版", siteId);
return null;
}
@@ -186,7 +173,7 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
// 查询最新 draft,内容相同则跳过
SiteSnapshot latestDraft = snapshotMapper.selectLatestDraft(siteId);
if (latestDraft != null && checksum.equals(latestDraft.getChecksum())) {
log.info("站点 {} 草稿内容未变化,跳过保存", siteId);
log.info("站点 {} 预览版内容未变化,跳过保存", siteId);
return latestDraft;
}
@@ -201,15 +188,14 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
snapshotMapper.insert(snapshot);
// 刷新 draft 和 preview 缓存
// 刷新缓存
String bundleJson = toBundleJson(snapshot);
draftJsonCache.put(siteId, bundleJson);
previewJsonCache.put(siteId, bundleJson);
jsonCache.put(draftKey(siteId), bundleJson);
log.info("草稿已保存: siteId={}, version={}, checksum={}", siteId, newVersion, checksum.substring(0, 8));
log.info("预览版已保存: siteId={}, version={}, checksum={}", siteId, newVersion, checksum.substring(0, 8));
return snapshot;
} catch (Exception e) {
log.error("保存草稿失败 siteId={}", siteId, e);
log.error("保存预览版失败 siteId={}", siteId, e);
return null;
}
}
@@ -237,7 +223,7 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
public SiteSnapshot publish(String siteId) {
SiteSnapshot draft = snapshotMapper.selectLatestDraft(siteId);
if (draft == null) {
log.warn("站点 {} 无 draft 快照可发布", siteId);
log.warn("站点 {} 无预览版快照可发布", siteId);
return null;
}
@@ -255,10 +241,9 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
// 序列化并刷新缓存
String bundleJson = toBundleJson(draft);
bundleJsonCache.put(siteId, bundleJson);
// 发布后 draft/preview 缓存已过时
draftJsonCache.invalidate(siteId);
previewJsonCache.invalidate(siteId);
jsonCache.put(pubKey(siteId), bundleJson);
// 发布后 draft 缓存已过时(该记录不再是 draft)
jsonCache.invalidate(draftKey(siteId));
// 上传到七牛云 CDN 作为灾备(复用已序列化的 JSON)
try {
@@ -285,42 +270,42 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
@Override
public String getActiveBundleJson(String siteId) {
return bundleJsonCache.get(siteId, key -> {
SiteSnapshot active = snapshotMapper.selectLatestPublished(key);
return jsonCache.get(pubKey(siteId), key -> {
SiteSnapshot active = snapshotMapper.selectLatestPublished(siteId);
if (active == null) {
log.debug("站点 {} 无 published 快照", key);
log.debug("站点 {} 无 published 快照", siteId);
return ""; // 空字符串表示无数据,Caffeine 会缓存
}
log.debug("缓存未命中,序列化 siteId={}", key);
log.debug("缓存未命中,序列化 siteId={}", siteId);
return toBundleJson(active);
});
}
@Override
public String getDraftBundleJson(String siteId) {
return draftJsonCache.get(siteId, key -> {
SiteSnapshot draft = snapshotMapper.selectLatestDraft(key);
return jsonCache.get(draftKey(siteId), key -> {
SiteSnapshot draft = snapshotMapper.selectLatestDraft(siteId);
if (draft == null) {
log.debug("站点 {} 无 draft 快照", key);
return ""; // 空字符串表示无数据,Caffeine 会缓存
log.debug("站点 {} 无预览版快照", siteId);
return "";
}
log.debug("draft 缓存未命中,序列化 siteId={}", key);
log.debug("预览版缓存未命中 siteId={}", siteId);
return toBundleJson(draft);
});
}
@Override
public String getPreviewBundleJson(String siteId) {
// draft 优先,无 draft 则取 published — 一次查询
return previewJsonCache.get(siteId, key -> {
SiteSnapshot snapshot = snapshotMapper.selectLatestWithDraft(key);
if (snapshot == null) {
log.debug("站点 {} 无任何快照", key);
return "";
}
log.debug("预览缓存未命中 siteId={} status={}", key, snapshot.getStatus());
return toBundleJson(snapshot);
});
// draft 优先,无 draft 则 fallback 到 published — 一次查询
// preview 不使用独立缓存,因为它是 draft + published 的逻辑并集;
// 当 draft 或 published 任一变化时,需要同时感知,直接查 DB 更准确
SiteSnapshot snapshot = snapshotMapper.selectLatestWithDraft(siteId);
if (snapshot == null) {
log.debug("站点 {} 无任何快照", siteId);
return "";
}
log.debug("预览查询 siteId={} status={}", siteId, snapshot.getStatus());
return toBundleJson(snapshot);
}
@Override