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:
@@ -380,22 +380,22 @@ public class AdminController {
|
|||||||
} catch (Exception e) { return ApiResponse.error(500, "发布失败: " + e.getMessage()); }
|
} catch (Exception e) { return ApiResponse.error(500, "发布失败: " + e.getMessage()); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保存站点配置草稿(预览版,不上传 CDN) */
|
/** 保存站点配置预览版(不上传 CDN) */
|
||||||
@PostMapping("/sites/{siteId}/save-draft")
|
@PostMapping("/sites/{siteId}/save-preview")
|
||||||
public ApiResponse<Map<String, Object>> saveDraft(@PathVariable("siteId") String siteId,
|
public ApiResponse<Map<String, Object>> savePreview(@PathVariable("siteId") String siteId,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
checkAdmin(request);
|
checkAdmin(request);
|
||||||
try {
|
try {
|
||||||
var draft = snapshotService.saveDraft(siteId);
|
var preview = snapshotService.savePreview(siteId);
|
||||||
if (draft == null) {
|
if (preview == null) {
|
||||||
return ApiResponse.error(400, "该站点无有效配置,无法保存草稿");
|
return ApiResponse.error(400, "该站点无有效配置,无法保存预览版");
|
||||||
}
|
}
|
||||||
return ApiResponse.success(Map.of(
|
return ApiResponse.success(Map.of(
|
||||||
"siteId", draft.getSiteId(),
|
"siteId", preview.getSiteId(),
|
||||||
"version", draft.getVersion(),
|
"version", preview.getVersion(),
|
||||||
"checksum", draft.getChecksum().substring(0, 8)));
|
"checksum", preview.getChecksum().substring(0, 8)));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ApiResponse.error(500, "保存草稿失败: " + e.getMessage());
|
return ApiResponse.error(500, "保存预览版失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,7 +407,7 @@ public class AdminController {
|
|||||||
try {
|
try {
|
||||||
var published = snapshotService.publish(siteId);
|
var published = snapshotService.publish(siteId);
|
||||||
if (published == null) {
|
if (published == null) {
|
||||||
return ApiResponse.error(400, "该站点无 draft 快照可发布");
|
return ApiResponse.error(400, "该站点无预览版快照可发布");
|
||||||
}
|
}
|
||||||
return ApiResponse.success(Map.of(
|
return ApiResponse.success(Map.of(
|
||||||
"siteId", published.getSiteId(),
|
"siteId", published.getSiteId(),
|
||||||
|
|||||||
@@ -17,20 +17,20 @@ public interface SiteSnapshotService {
|
|||||||
Map<String, Object> buildSnapshot(String siteId);
|
Map<String, Object> buildSnapshot(String siteId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新站点的 draft 快照(任意配置变更后调用)
|
* 更新站点的预览版快照(任意配置变更后调用)
|
||||||
* 内容未变化(checksum 相同)则不产生新版本
|
* 内容未变化(checksum 相同)则不产生新版本
|
||||||
* @return 新快照或 null(内容未变)
|
* @return 新快照或 null(内容未变)
|
||||||
*/
|
*/
|
||||||
SiteSnapshot updateSnapshot(String siteId);
|
SiteSnapshot updateSnapshot(String siteId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手动保存草稿快照(管理员触发,强制生成新 draft)
|
* 手动保存预览版快照(管理员触发)
|
||||||
* @return 新快照或 null
|
* @return 新快照或 null
|
||||||
*/
|
*/
|
||||||
SiteSnapshot saveDraft(String siteId);
|
SiteSnapshot savePreview(String siteId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发布指定站点的快照(draft → published)
|
* 发布指定站点的快照(预览版 → 正式版)
|
||||||
* 同时上传到七牛云 CDN 作为灾备
|
* 同时上传到七牛云 CDN 作为灾备
|
||||||
*/
|
*/
|
||||||
SiteSnapshot publish(String siteId);
|
SiteSnapshot publish(String siteId);
|
||||||
@@ -41,22 +41,21 @@ public interface SiteSnapshotService {
|
|||||||
SiteSnapshot getActive(String siteId);
|
SiteSnapshot getActive(String siteId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取站点当前生效快照的预序列化 JSON(带 Caffeine 缓存)
|
* 获取站点当前生效快照的预序列化 JSON(带缓存)
|
||||||
* 供 SnapshotController 使用,避免重复序列化
|
* 供 SnapshotController 使用,避免重复序列化
|
||||||
* @return JSON 字符串,空串表示无快照
|
* @return JSON 字符串,空串表示无快照
|
||||||
*/
|
*/
|
||||||
String getActiveBundleJson(String siteId);
|
String getActiveBundleJson(String siteId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取站点最新 draft 快照的预序列化 JSON(预览版,带 Caffeine 缓存)
|
* 获取站点最新预览版快照的预序列化 JSON(带缓存)
|
||||||
* @param siteId 站点标识
|
* @param siteId 站点标识
|
||||||
* @return JSON 字符串,空串表示无 draft
|
* @return JSON 字符串,空串表示无预览版
|
||||||
*/
|
*/
|
||||||
String getDraftBundleJson(String siteId);
|
String getDraftBundleJson(String siteId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取预览版快照 JSON(draft 优先,无 draft 时 fallback 到 published)
|
* 获取预览快照 JSON(预览版优先,无预览版时 fallback 到正式版)
|
||||||
* 即:正式版 + 预览版 的并集
|
|
||||||
* @param siteId 站点标识
|
* @param siteId 站点标识
|
||||||
* @return JSON 字符串,空串表示无任何快照
|
* @return JSON 字符串,空串表示无任何快照
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -43,23 +43,14 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
|||||||
private final CachedStorageService storageService;
|
private final CachedStorageService storageService;
|
||||||
private static final ObjectMapper om = new ObjectMapper();
|
private static final ObjectMapper om = new ObjectMapper();
|
||||||
|
|
||||||
/** 已发布快照 JSON 缓存:key=siteId, value=预序列化的 bundle JSON */
|
/** 统一快照 JSON 缓存:key = siteId + ":published" | siteId + ":draft",value = 预序列化 bundle JSON */
|
||||||
private final Cache<String, String> bundleJsonCache = Caffeine.newBuilder()
|
private final Cache<String, String> jsonCache = Caffeine.newBuilder()
|
||||||
.expireAfterWrite(Duration.ofMinutes(10))
|
.expireAfterWrite(Duration.ofMinutes(10))
|
||||||
.maximumSize(500)
|
.maximumSize(1000)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
/** draft 快照 JSON 缓存:key=siteId, value=预序列化的 bundle JSON */
|
private static String pubKey(String siteId) { return siteId + ":published"; }
|
||||||
private final Cache<String, String> draftJsonCache = Caffeine.newBuilder()
|
private static String draftKey(String siteId) { return siteId + ":draft"; }
|
||||||
.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();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> buildSnapshot(String siteId) {
|
public Map<String, Object> buildSnapshot(String siteId) {
|
||||||
@@ -134,18 +125,15 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 计算 checksum
|
|
||||||
String contentJson = om.writeValueAsString(data);
|
String contentJson = om.writeValueAsString(data);
|
||||||
String checksum = HmacUtil.sha256(contentJson);
|
String checksum = HmacUtil.sha256(contentJson);
|
||||||
|
|
||||||
// 查询最新 draft
|
|
||||||
SiteSnapshot latestDraft = snapshotMapper.selectLatestDraft(siteId);
|
SiteSnapshot latestDraft = snapshotMapper.selectLatestDraft(siteId);
|
||||||
if (latestDraft != null && checksum.equals(latestDraft.getChecksum())) {
|
if (latestDraft != null && checksum.equals(latestDraft.getChecksum())) {
|
||||||
log.debug("站点 {} 配置未变化,跳过快照生成", siteId);
|
log.debug("站点 {} 配置未变化,跳过快照生成", siteId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确定新版本号
|
|
||||||
int newVersion = (latestDraft != null) ? latestDraft.getVersion() + 1 : 1;
|
int newVersion = (latestDraft != null) ? latestDraft.getVersion() + 1 : 1;
|
||||||
|
|
||||||
SiteSnapshot snapshot = new SiteSnapshot();
|
SiteSnapshot snapshot = new SiteSnapshot();
|
||||||
@@ -157,10 +145,9 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
|||||||
|
|
||||||
snapshotMapper.insert(snapshot);
|
snapshotMapper.insert(snapshot);
|
||||||
|
|
||||||
// 刷新 draft 和 preview 缓存(审批通过后异步生成 draft,需立即可被预览查询到)
|
// 刷新缓存:审批通过后异步生成的预览版,需立即可被 ?preview=true 查询到
|
||||||
String bundleJson = toBundleJson(snapshot);
|
String bundleJson = toBundleJson(snapshot);
|
||||||
draftJsonCache.put(siteId, bundleJson);
|
jsonCache.put(draftKey(siteId), bundleJson);
|
||||||
previewJsonCache.put(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;
|
return snapshot;
|
||||||
@@ -172,10 +159,10 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public SiteSnapshot saveDraft(String siteId) {
|
public SiteSnapshot savePreview(String siteId) {
|
||||||
Map<String, Object> data = buildSnapshot(siteId);
|
Map<String, Object> data = buildSnapshot(siteId);
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
log.warn("站点 {} 无有效配置,无法保存草稿", siteId);
|
log.warn("站点 {} 无有效配置,无法保存预览版", siteId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +173,7 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
|||||||
// 查询最新 draft,内容相同则跳过
|
// 查询最新 draft,内容相同则跳过
|
||||||
SiteSnapshot latestDraft = snapshotMapper.selectLatestDraft(siteId);
|
SiteSnapshot latestDraft = snapshotMapper.selectLatestDraft(siteId);
|
||||||
if (latestDraft != null && checksum.equals(latestDraft.getChecksum())) {
|
if (latestDraft != null && checksum.equals(latestDraft.getChecksum())) {
|
||||||
log.info("站点 {} 草稿内容未变化,跳过保存", siteId);
|
log.info("站点 {} 预览版内容未变化,跳过保存", siteId);
|
||||||
return latestDraft;
|
return latestDraft;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,15 +188,14 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
|||||||
|
|
||||||
snapshotMapper.insert(snapshot);
|
snapshotMapper.insert(snapshot);
|
||||||
|
|
||||||
// 刷新 draft 和 preview 缓存
|
// 刷新缓存
|
||||||
String bundleJson = toBundleJson(snapshot);
|
String bundleJson = toBundleJson(snapshot);
|
||||||
draftJsonCache.put(siteId, bundleJson);
|
jsonCache.put(draftKey(siteId), bundleJson);
|
||||||
previewJsonCache.put(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;
|
return snapshot;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("保存草稿失败 siteId={}", siteId, e);
|
log.error("保存预览版失败 siteId={}", siteId, e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,7 +223,7 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
|||||||
public SiteSnapshot publish(String siteId) {
|
public SiteSnapshot publish(String siteId) {
|
||||||
SiteSnapshot draft = snapshotMapper.selectLatestDraft(siteId);
|
SiteSnapshot draft = snapshotMapper.selectLatestDraft(siteId);
|
||||||
if (draft == null) {
|
if (draft == null) {
|
||||||
log.warn("站点 {} 无 draft 快照可发布", siteId);
|
log.warn("站点 {} 无预览版快照可发布", siteId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,10 +241,9 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
|||||||
|
|
||||||
// 序列化并刷新缓存
|
// 序列化并刷新缓存
|
||||||
String bundleJson = toBundleJson(draft);
|
String bundleJson = toBundleJson(draft);
|
||||||
bundleJsonCache.put(siteId, bundleJson);
|
jsonCache.put(pubKey(siteId), bundleJson);
|
||||||
// 发布后 draft/preview 缓存已过时
|
// 发布后 draft 缓存已过时(该记录不再是 draft)
|
||||||
draftJsonCache.invalidate(siteId);
|
jsonCache.invalidate(draftKey(siteId));
|
||||||
previewJsonCache.invalidate(siteId);
|
|
||||||
|
|
||||||
// 上传到七牛云 CDN 作为灾备(复用已序列化的 JSON)
|
// 上传到七牛云 CDN 作为灾备(复用已序列化的 JSON)
|
||||||
try {
|
try {
|
||||||
@@ -285,42 +270,42 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getActiveBundleJson(String siteId) {
|
public String getActiveBundleJson(String siteId) {
|
||||||
return bundleJsonCache.get(siteId, key -> {
|
return jsonCache.get(pubKey(siteId), key -> {
|
||||||
SiteSnapshot active = snapshotMapper.selectLatestPublished(key);
|
SiteSnapshot active = snapshotMapper.selectLatestPublished(siteId);
|
||||||
if (active == null) {
|
if (active == null) {
|
||||||
log.debug("站点 {} 无 published 快照", key);
|
log.debug("站点 {} 无 published 快照", siteId);
|
||||||
return ""; // 空字符串表示无数据,Caffeine 会缓存
|
return ""; // 空字符串表示无数据,Caffeine 会缓存
|
||||||
}
|
}
|
||||||
log.debug("缓存未命中,序列化 siteId={}", key);
|
log.debug("缓存未命中,序列化 siteId={}", siteId);
|
||||||
return toBundleJson(active);
|
return toBundleJson(active);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getDraftBundleJson(String siteId) {
|
public String getDraftBundleJson(String siteId) {
|
||||||
return draftJsonCache.get(siteId, key -> {
|
return jsonCache.get(draftKey(siteId), key -> {
|
||||||
SiteSnapshot draft = snapshotMapper.selectLatestDraft(key);
|
SiteSnapshot draft = snapshotMapper.selectLatestDraft(siteId);
|
||||||
if (draft == null) {
|
if (draft == null) {
|
||||||
log.debug("站点 {} 无 draft 快照", key);
|
log.debug("站点 {} 无预览版快照", siteId);
|
||||||
return ""; // 空字符串表示无数据,Caffeine 会缓存
|
return "";
|
||||||
}
|
}
|
||||||
log.debug("draft 缓存未命中,序列化 siteId={}", key);
|
log.debug("预览版缓存未命中 siteId={}", siteId);
|
||||||
return toBundleJson(draft);
|
return toBundleJson(draft);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getPreviewBundleJson(String siteId) {
|
public String getPreviewBundleJson(String siteId) {
|
||||||
// draft 优先,无 draft 则取 published — 一次查询
|
// draft 优先,无 draft 则 fallback 到 published — 一次查询
|
||||||
return previewJsonCache.get(siteId, key -> {
|
// preview 不使用独立缓存,因为它是 draft + published 的逻辑并集;
|
||||||
SiteSnapshot snapshot = snapshotMapper.selectLatestWithDraft(key);
|
// 当 draft 或 published 任一变化时,需要同时感知,直接查 DB 更准确
|
||||||
|
SiteSnapshot snapshot = snapshotMapper.selectLatestWithDraft(siteId);
|
||||||
if (snapshot == null) {
|
if (snapshot == null) {
|
||||||
log.debug("站点 {} 无任何快照", key);
|
log.debug("站点 {} 无任何快照", siteId);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
log.debug("预览缓存未命中 siteId={} status={}", key, snapshot.getStatus());
|
log.debug("预览查询 siteId={} status={}", siteId, snapshot.getStatus());
|
||||||
return toBundleJson(snapshot);
|
return toBundleJson(snapshot);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<div style="display:flex;gap:8px">
|
<div style="display:flex;gap:8px">
|
||||||
<el-button size="small" @click="editSiteId=''">返回列表</el-button>
|
<el-button size="small" @click="editSiteId=''">返回列表</el-button>
|
||||||
<el-button size="small" @click="saveSite" :loading="saving">保存站点</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>
|
<el-button size="small" type="success" @click="publishSite" :loading="publishing">发布正式版</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import api from '../api'
|
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 sites = ref([]), editSiteId = ref(''), editorJson = ref('{}'), jsonError = ref(false)
|
||||||
const form = reactive({ siteId:'', namesStr:'', urlsStr:'', tagsStr:'', desc:'', siteType:'nexusphp', status:'active' })
|
const form = reactive({ siteId:'', namesStr:'', urlsStr:'', tagsStr:'', desc:'', siteType:'nexusphp', status:'active' })
|
||||||
let syncing = false
|
let syncing = false
|
||||||
@@ -155,13 +155,13 @@ const newSite = () => {
|
|||||||
editorJson.value = JSON.stringify({siteId:'',names:[''],urls:[''],siteType:'nexusphp',status:'active'}, null, 2)
|
editorJson.value = JSON.stringify({siteId:'',names:[''],urls:[''],siteType:'nexusphp',status:'active'}, null, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveDraft = async () => {
|
const savePreview = async () => {
|
||||||
savingDraft.value = true
|
savingPreview.value = true
|
||||||
try {
|
try {
|
||||||
await api.raw.post('/admin/sites/' + editSiteId.value + '/save-draft')
|
await api.raw.post('/admin/sites/' + editSiteId.value + '/save-preview')
|
||||||
ElMessage.success('草稿已保存(预览版)')
|
ElMessage.success('预览版已保存')
|
||||||
} catch { ElMessage.error('保存草稿失败') }
|
} catch { ElMessage.error('保存预览版失败') }
|
||||||
finally { savingDraft.value = false }
|
finally { savingPreview.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
const publishSite = async () => {
|
const publishSite = async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user