feat: Manifest 接口支持 ETag 条件请求

- ETag = SHA256(所有 siteId + configHash 拼接)
- 客户端传 If-None-Match 命中返回 304 无数据
- 替代新增 /latest 接口,同一端点高效轮询
This commit is contained in:
mediabot-pt
2026-06-29 17:21:22 +08:00
parent 092cf22d7a
commit cbf5684abc

View File

@@ -1,12 +1,16 @@
package com.par.api.controller;
import com.par.common.util.HmacUtil;
import com.par.core.dto.ApiResponse;
import com.par.core.entity.Site;
import com.par.core.entity.SiteConfig;
import com.par.core.mapper.SiteConfigMapper;
import com.par.core.mapper.SiteMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -17,7 +21,7 @@ import java.util.stream.Collectors;
/**
* Manifest 控制器
* 返回所有站点最新配置的索引
* 返回所有站点最新配置的索引,支持 ETag 条件请求
*/
@RestController
@RequestMapping("/api/v1/manifest")
@@ -28,15 +32,20 @@ public class ManifestController {
private final SiteConfigMapper siteConfigMapper;
/**
* 获取全局 Manifest
* 获取全局 Manifest(支持 ETag)
* 客户端传 If-None-Match 避免重复传输
*/
@GetMapping
public ApiResponse<Map<String, Object>> getManifest() {
public ResponseEntity<ApiResponse<Map<String, Object>>> getManifest(
@RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) {
List<Site> sites = siteMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Site>()
.eq(Site::getDeleted, 0)
);
// 构建 entries 并计算 ETag
StringBuilder etagBuilder = new StringBuilder();
List<Map<String, Object>> entries = sites.stream().map(site -> {
Map<String, Object> entry = new HashMap<>();
entry.put("siteId", site.getSiteId());
@@ -45,21 +54,31 @@ public class ManifestController {
entry.put("schemaVersion", site.getSchemaVersion());
entry.put("status", site.getStatus() != null ? site.getStatus().getValue() : null);
// 获取最新已批准配置
SiteConfig latest = siteConfigMapper.selectLatestBySiteId(site.getSiteId());
if (latest != null) {
entry.put("latestVersion", latest.getVersion());
entry.put("configHash", latest.getConfigHash());
entry.put("updatedAt", latest.getCreatedAt());
}
// 参与 ETag 计算
etagBuilder.append(site.getSiteId())
.append(latest != null ? latest.getConfigHash() : "none")
.append(";");
return entry;
}).collect(Collectors.toList());
String etag = HmacUtil.sha256(etagBuilder.toString());
// ETag 匹配 → 304
if (ifNoneMatch != null && ifNoneMatch.equals(etag)) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).eTag(etag).build();
}
Map<String, Object> manifest = new HashMap<>();
manifest.put("version", "1.0");
manifest.put("sites", entries);
return ApiResponse.success(manifest);
return ResponseEntity.ok().eTag(etag).body(ApiResponse.success(manifest));
}
}