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

@@ -380,22 +380,22 @@ public class AdminController {
} catch (Exception e) { return ApiResponse.error(500, "发布失败: " + e.getMessage()); }
}
/** 保存站点配置草稿(预览版,不上传 CDN) */
@PostMapping("/sites/{siteId}/save-draft")
public ApiResponse<Map<String, Object>> saveDraft(@PathVariable("siteId") String siteId,
/** 保存站点配置预览版(不上传 CDN) */
@PostMapping("/sites/{siteId}/save-preview")
public ApiResponse<Map<String, Object>> savePreview(@PathVariable("siteId") String siteId,
HttpServletRequest request) {
checkAdmin(request);
try {
var draft = snapshotService.saveDraft(siteId);
if (draft == null) {
return ApiResponse.error(400, "该站点无有效配置,无法保存草稿");
var preview = snapshotService.savePreview(siteId);
if (preview == null) {
return ApiResponse.error(400, "该站点无有效配置,无法保存预览版");
}
return ApiResponse.success(Map.of(
"siteId", draft.getSiteId(),
"version", draft.getVersion(),
"checksum", draft.getChecksum().substring(0, 8)));
"siteId", preview.getSiteId(),
"version", preview.getVersion(),
"checksum", preview.getChecksum().substring(0, 8)));
} catch (Exception e) {
return ApiResponse.error(500, "保存草稿失败: " + e.getMessage());
return ApiResponse.error(500, "保存预览版失败: " + e.getMessage());
}
}
@@ -407,7 +407,7 @@ public class AdminController {
try {
var published = snapshotService.publish(siteId);
if (published == null) {
return ApiResponse.error(400, "该站点无 draft 快照可发布");
return ApiResponse.error(400, "该站点无预览版快照可发布");
}
return ApiResponse.success(Map.of(
"siteId", published.getSiteId(),

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);
// draft 优先,无 draft 则 fallback 到 published — 一次查询
// preview 不使用独立缓存,因为它是 draft + published 的逻辑并集;
// 当 draft 或 published 任一变化时,需要同时感知,直接查 DB 更准确
SiteSnapshot snapshot = snapshotMapper.selectLatestWithDraft(siteId);
if (snapshot == null) {
log.debug("站点 {} 无任何快照", key);
log.debug("站点 {} 无任何快照", siteId);
return "";
}
log.debug("预览缓存未命中 siteId={} status={}", key, snapshot.getStatus());
log.debug("预览查询 siteId={} status={}", siteId, snapshot.getStatus());
return toBundleJson(snapshot);
});
}
@Override

View File

@@ -57,7 +57,7 @@
<div style="display:flex;gap:8px">
<el-button size="small" @click="editSiteId=''">返回列表</el-button>
<el-button size="small" @click="saveSite" :loading="saving">保存站点</el-button>
<el-button size="small" @click="saveDraft" :loading="savingDraft">保存草稿</el-button>
<el-button size="small" @click="savePreview" :loading="savingPreview">保存预览版</el-button>
<el-button size="small" type="success" @click="publishSite" :loading="publishing">发布正式版</el-button>
</div>
</div>
@@ -108,7 +108,7 @@
<script setup>
import api from '../api'
const loading = ref(false), saving = ref(false), savingDraft = ref(false), publishing = ref(false), pendingCount = ref(0)
const loading = ref(false), saving = ref(false), savingPreview = ref(false), publishing = ref(false), pendingCount = ref(0)
const sites = ref([]), editSiteId = ref(''), editorJson = ref('{}'), jsonError = ref(false)
const form = reactive({ siteId:'', namesStr:'', urlsStr:'', tagsStr:'', desc:'', siteType:'nexusphp', status:'active' })
let syncing = false
@@ -155,13 +155,13 @@ const newSite = () => {
editorJson.value = JSON.stringify({siteId:'',names:[''],urls:[''],siteType:'nexusphp',status:'active'}, null, 2)
}
const saveDraft = async () => {
savingDraft.value = true
const savePreview = async () => {
savingPreview.value = true
try {
await api.raw.post('/admin/sites/' + editSiteId.value + '/save-draft')
ElMessage.success('草稿已保存(预览版)')
} catch { ElMessage.error('保存草稿失败') }
finally { savingDraft.value = false }
await api.raw.post('/admin/sites/' + editSiteId.value + '/save-preview')
ElMessage.success('预览版已保存')
} catch { ElMessage.error('保存预览版失败') }
finally { savingPreview.value = false }
}
const publishSite = async () => {