feat: 快照改为预览版/正式版两阶段发布 - 新增 saveDraft 接口手动保存草稿快照 - 新增 getDraftBundleJson 方法读取预览版(Caffeine缓存) - SnapshotController 支持 ?preview=true 参数读取 draft - 前端编辑页改为'保存草稿'+'发布正式版'两个按钮 - AdminController 新增单站点 save-draft/publish 接口 - 复用 populateSnapshotFields 消除重复代码
This commit is contained in:
@@ -23,6 +23,12 @@ public interface SiteSnapshotService {
|
||||
*/
|
||||
SiteSnapshot updateSnapshot(String siteId);
|
||||
|
||||
/**
|
||||
* 手动保存草稿快照(管理员触发,强制生成新 draft)
|
||||
* @return 新快照或 null
|
||||
*/
|
||||
SiteSnapshot saveDraft(String siteId);
|
||||
|
||||
/**
|
||||
* 发布指定站点的快照(draft → published)
|
||||
* 同时上传到七牛云 CDN 作为灾备
|
||||
@@ -37,10 +43,17 @@ public interface SiteSnapshotService {
|
||||
/**
|
||||
* 获取站点当前生效快照的预序列化 JSON(带 Caffeine 缓存)
|
||||
* 供 SnapshotController 使用,避免重复序列化
|
||||
* @return JSON 字符串,或 null 表示无快照
|
||||
* @return JSON 字符串,空串表示无快照
|
||||
*/
|
||||
String getActiveBundleJson(String siteId);
|
||||
|
||||
/**
|
||||
* 获取站点最新 draft 快照的预序列化 JSON(预览版,带 Caffeine 缓存)
|
||||
* @param siteId 站点标识
|
||||
* @return JSON 字符串,空串表示无 draft
|
||||
*/
|
||||
String getDraftBundleJson(String siteId);
|
||||
|
||||
/**
|
||||
* 按 checksum 判断是否需要更新
|
||||
*/
|
||||
|
||||
@@ -49,6 +49,12 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
||||
.maximumSize(500)
|
||||
.build();
|
||||
|
||||
/** draft 快照 JSON 缓存:key=siteId, value=预序列化的 bundle JSON */
|
||||
private final Cache<String, String> draftJsonCache = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMinutes(5))
|
||||
.maximumSize(500)
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public Map<String, Object> buildSnapshot(String siteId) {
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
@@ -139,27 +145,7 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
||||
SiteSnapshot snapshot = new SiteSnapshot();
|
||||
snapshot.setSiteId(siteId);
|
||||
snapshot.setVersion(newVersion);
|
||||
|
||||
// 脚本
|
||||
if (data.containsKey("script")) {
|
||||
snapshot.setScript((String) data.get("script"));
|
||||
snapshot.setScriptVersion((String) data.get("scriptVersion"));
|
||||
snapshot.setScriptSource((String) data.get("scriptSource"));
|
||||
}
|
||||
|
||||
// 页面
|
||||
if (data.containsKey("pages")) {
|
||||
snapshot.setPages(om.writeValueAsString(data.get("pages")));
|
||||
snapshot.setPagesVersion((String) data.get("pagesVersion"));
|
||||
snapshot.setPagesSource((String) data.get("pagesSource"));
|
||||
}
|
||||
|
||||
// 规则
|
||||
if (data.containsKey("riskRules")) {
|
||||
snapshot.setRules(om.writeValueAsString(data.get("riskRules")));
|
||||
snapshot.setRulesVersion((Integer) data.get("riskRulesVersion"));
|
||||
}
|
||||
|
||||
populateSnapshotFields(snapshot, data);
|
||||
snapshot.setChecksum(checksum);
|
||||
snapshot.setStatus("draft");
|
||||
|
||||
@@ -172,6 +158,67 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public SiteSnapshot saveDraft(String siteId) {
|
||||
Map<String, Object> data = buildSnapshot(siteId);
|
||||
if (data == null) {
|
||||
log.warn("站点 {} 无有效配置,无法保存草稿", siteId);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
String contentJson = om.writeValueAsString(data);
|
||||
String checksum = HmacUtil.sha256(contentJson);
|
||||
|
||||
// 查询最新 draft,内容相同则跳过
|
||||
SiteSnapshot latestDraft = snapshotMapper.selectLatestDraft(siteId);
|
||||
if (latestDraft != null && checksum.equals(latestDraft.getChecksum())) {
|
||||
log.info("站点 {} 草稿内容未变化,跳过保存", siteId);
|
||||
return latestDraft;
|
||||
}
|
||||
|
||||
int newVersion = (latestDraft != null) ? latestDraft.getVersion() + 1 : 1;
|
||||
|
||||
SiteSnapshot snapshot = new SiteSnapshot();
|
||||
snapshot.setSiteId(siteId);
|
||||
snapshot.setVersion(newVersion);
|
||||
populateSnapshotFields(snapshot, data);
|
||||
snapshot.setChecksum(checksum);
|
||||
snapshot.setStatus("draft");
|
||||
|
||||
snapshotMapper.insert(snapshot);
|
||||
|
||||
// 刷新 draft 缓存
|
||||
String bundleJson = toBundleJson(snapshot);
|
||||
draftJsonCache.put(siteId, bundleJson);
|
||||
|
||||
log.info("草稿已保存: siteId={}, version={}, checksum={}", siteId, newVersion, checksum.substring(0, 8));
|
||||
return snapshot;
|
||||
} catch (Exception e) {
|
||||
log.error("保存草稿失败 siteId={}", siteId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 将 buildSnapshot 的数据填充到实体字段 */
|
||||
private void populateSnapshotFields(SiteSnapshot snapshot, Map<String, Object> data) throws Exception {
|
||||
if (data.containsKey("script")) {
|
||||
snapshot.setScript((String) data.get("script"));
|
||||
snapshot.setScriptVersion((String) data.get("scriptVersion"));
|
||||
snapshot.setScriptSource((String) data.get("scriptSource"));
|
||||
}
|
||||
if (data.containsKey("pages")) {
|
||||
snapshot.setPages(om.writeValueAsString(data.get("pages")));
|
||||
snapshot.setPagesVersion((String) data.get("pagesVersion"));
|
||||
snapshot.setPagesSource((String) data.get("pagesSource"));
|
||||
}
|
||||
if (data.containsKey("riskRules")) {
|
||||
snapshot.setRules(om.writeValueAsString(data.get("riskRules")));
|
||||
snapshot.setRulesVersion((Integer) data.get("riskRulesVersion"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public SiteSnapshot publish(String siteId) {
|
||||
@@ -196,6 +243,8 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
||||
// 序列化并刷新缓存
|
||||
String bundleJson = toBundleJson(draft);
|
||||
bundleJsonCache.put(siteId, bundleJson);
|
||||
// 发布后 draft 缓存已过时,清除
|
||||
draftJsonCache.invalidate(siteId);
|
||||
|
||||
// 上传到七牛云 CDN 作为灾备(复用已序列化的 JSON)
|
||||
try {
|
||||
@@ -233,6 +282,19 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDraftBundleJson(String siteId) {
|
||||
return draftJsonCache.get(siteId, key -> {
|
||||
SiteSnapshot draft = snapshotMapper.selectLatestDraft(key);
|
||||
if (draft == null) {
|
||||
log.debug("站点 {} 无 draft 快照", key);
|
||||
return ""; // 空字符串表示无数据,Caffeine 会缓存
|
||||
}
|
||||
log.debug("draft 缓存未命中,序列化 siteId={}", key);
|
||||
return toBundleJson(draft);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChanged(String siteId, String checksum) {
|
||||
if (checksum == null) return true;
|
||||
|
||||
Reference in New Issue
Block a user