diff --git a/par-api/src/main/java/com/par/api/controller/SnapshotController.java b/par-api/src/main/java/com/par/api/controller/SnapshotController.java index 5afbd27..ed36218 100644 --- a/par-api/src/main/java/com/par/api/controller/SnapshotController.java +++ b/par-api/src/main/java/com/par/api/controller/SnapshotController.java @@ -1,5 +1,6 @@ package com.par.api.controller; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.par.core.dto.ApiResponse; import com.par.core.entity.SiteSnapshot; @@ -32,7 +33,7 @@ public class SnapshotController { /** * 获取站点当前生效配置快照(供 mediabot 使用) * 支持 ETag (If-None-Match) 实现增量更新 - * 优先从数据库读取,数据库不可用时回退到七牛云 CDN + * 优先从 Caffeine 缓存读取,缓存未命中查数据库并回退七牛云 CDN */ @GetMapping("/{siteId}") public ResponseEntity getSnapshot( @@ -40,9 +41,9 @@ public class SnapshotController { @RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) { try { - SiteSnapshot active = snapshotService.getActive(siteId); + String bundleJson = snapshotService.getActiveBundleJson(siteId); - if (active == null) { + if (bundleJson == null || bundleJson.isEmpty()) { // 尝试从云存储 CDN 回退 String cached = storageService.read("snapshots/" + siteId + ".json"); if (cached != null && !cached.isBlank()) { @@ -53,18 +54,18 @@ public class SnapshotController { return ResponseEntity.notFound().build(); } - // ETag 检查 - if (ifNoneMatch != null && ifNoneMatch.equals(active.getChecksum())) { + // ETag 检查:从 JSON 中提取 checksum + String etag = extractChecksum(bundleJson); + if (ifNoneMatch != null && etag != null && ifNoneMatch.equals(etag)) { return ResponseEntity.status(304) - .header(HttpHeaders.ETAG, active.getChecksum()) + .header(HttpHeaders.ETAG, etag) .build(); } - String json = toJson(active); return ResponseEntity.ok() - .header(HttpHeaders.ETAG, active.getChecksum()) + .header(HttpHeaders.ETAG, etag) .contentType(MediaType.APPLICATION_JSON) - .body(json); + .body(bundleJson); } catch (Exception e) { log.error("获取快照失败 siteId={}", siteId, e); @@ -99,39 +100,13 @@ public class SnapshotController { return ApiResponse.success(result); } - private String toJson(SiteSnapshot snapshot) { + /** 从 bundle JSON 中提取 checksum 字段,用于 ETag 比较 */ + private String extractChecksum(String bundleJson) { try { - Map bundle = new LinkedHashMap<>(); - bundle.put("siteId", snapshot.getSiteId()); - bundle.put("generatedAt", snapshot.getPublishedAt() != null - ? snapshot.getPublishedAt().toString() - : java.time.LocalDateTime.now().toString()); - - if (snapshot.getScript() != null) { - bundle.put("script", snapshot.getScript()); - bundle.put("scriptVersion", snapshot.getScriptVersion()); - bundle.put("scriptSource", snapshot.getScriptSource()); - } - - if (snapshot.getPages() != null) { - @SuppressWarnings("unchecked") - var pages = om.readValue(snapshot.getPages(), java.util.List.class); - bundle.put("pages", pages); - bundle.put("pagesVersion", snapshot.getPagesVersion()); - bundle.put("pagesSource", snapshot.getPagesSource()); - } - - if (snapshot.getRules() != null) { - @SuppressWarnings("unchecked") - var rules = om.readValue(snapshot.getRules(), java.util.List.class); - bundle.put("riskRules", rules); - bundle.put("riskRulesVersion", snapshot.getRulesVersion()); - } - - return om.writeValueAsString(bundle); + JsonNode node = om.readTree(bundleJson); + return node.has("checksum") ? node.get("checksum").asText() : null; } catch (Exception e) { - log.error("序列化快照失败 siteId={}", snapshot.getSiteId(), e); - return "{}"; + return null; } } } diff --git a/par-core/src/main/java/com/par/core/service/SiteSnapshotService.java b/par-core/src/main/java/com/par/core/service/SiteSnapshotService.java index af2712b..e5d3548 100644 --- a/par-core/src/main/java/com/par/core/service/SiteSnapshotService.java +++ b/par-core/src/main/java/com/par/core/service/SiteSnapshotService.java @@ -34,6 +34,13 @@ public interface SiteSnapshotService { */ SiteSnapshot getActive(String siteId); + /** + * 获取站点当前生效快照的预序列化 JSON(带 Caffeine 缓存) + * 供 SnapshotController 使用,避免重复序列化 + * @return JSON 字符串,或 null 表示无快照 + */ + String getActiveBundleJson(String siteId); + /** * 按 checksum 判断是否需要更新 */ diff --git a/par-core/src/main/java/com/par/core/service/impl/SiteSnapshotServiceImpl.java b/par-core/src/main/java/com/par/core/service/impl/SiteSnapshotServiceImpl.java index 6756a15..6f0f4a9 100644 --- a/par-core/src/main/java/com/par/core/service/impl/SiteSnapshotServiceImpl.java +++ b/par-core/src/main/java/com/par/core/service/impl/SiteSnapshotServiceImpl.java @@ -43,8 +43,8 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService { private final CachedStorageService storageService; private static final ObjectMapper om = new ObjectMapper(); - /** 已发布快照本地缓存:key=siteId, value=SiteSnapshot */ - private final Cache activeSnapshotCache = Caffeine.newBuilder() + /** 已发布快照 JSON 缓存:key=siteId, value=预序列化的 bundle JSON */ + private final Cache bundleJsonCache = Caffeine.newBuilder() .expireAfterWrite(Duration.ofMinutes(10)) .maximumSize(500) .build(); @@ -193,12 +193,12 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService { draft.setPublishedAt(LocalDateTime.now()); snapshotMapper.updateById(draft); - // 刷新本地缓存 - activeSnapshotCache.put(siteId, draft); + // 序列化并刷新缓存 + String bundleJson = toBundleJson(draft); + bundleJsonCache.put(siteId, bundleJson); - // 上传到七牛云 CDN 作为灾备 + // 上传到七牛云 CDN 作为灾备(复用已序列化的 JSON) try { - String bundleJson = toBundleJson(draft); String cdnPath = "snapshots/" + siteId + ".json"; storageService.store(cdnPath, bundleJson); // 也保存版本化副本 @@ -217,9 +217,19 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService { @Override public SiteSnapshot getActive(String siteId) { - return activeSnapshotCache.get(siteId, key -> { - log.debug("缓存未命中,查询数据库 siteId={}", siteId); - return snapshotMapper.selectLatestPublished(siteId); + return snapshotMapper.selectLatestPublished(siteId); + } + + @Override + public String getActiveBundleJson(String siteId) { + return bundleJsonCache.get(siteId, key -> { + SiteSnapshot active = snapshotMapper.selectLatestPublished(key); + if (active == null) { + log.debug("站点 {} 无 published 快照", key); + return ""; // 空字符串表示无数据,Caffeine 会缓存 + } + log.debug("缓存未命中,序列化 siteId={}", key); + return toBundleJson(active); }); } @@ -253,6 +263,7 @@ public class SiteSnapshotServiceImpl implements SiteSnapshotService { try { Map bundle = new LinkedHashMap<>(); bundle.put("siteId", snapshot.getSiteId()); + bundle.put("checksum", snapshot.getChecksum()); bundle.put("generatedAt", snapshot.getPublishedAt() != null ? snapshot.getPublishedAt().toString() : LocalDateTime.now().toString());