feat: 合并配置包 - publish 时生成 bundle,mediabot 下载保证版本一致

This commit is contained in:
mediabot-pt
2026-07-07 10:50:37 +08:00
parent 223528817e
commit 98bc5b3183
2 changed files with 133 additions and 3 deletions

View File

@@ -52,6 +52,7 @@ public class AdminController {
private final com.par.core.mapper.RiskRuleMapper riskRuleMapper;
private final com.par.core.service.SitePagesService sitePagesService;
private final com.par.core.service.RiskRuleService riskRuleService;
private final ConfigController configController;
/**
* 获取所有用户列表
@@ -337,18 +338,39 @@ public class AdminController {
// ===================== 站点审核 =====================
/** 发布站点配置到七牛云(生成 manifest/manifest.json) */
/** 发布站点配置到七牛云(生成 manifest/manifest.json + 各站点 bundles/) */
@PostMapping("/sites/publish")
public ApiResponse<Map<String,Object>> publishManifest(HttpServletRequest request) {
checkAdmin(request);
try {
// 1. 生成并发布 manifest
var manifest = buildManifest();
String json = new com.fasterxml.jackson.databind.ObjectMapper()
.writerWithDefaultPrettyPrinter().writeValueAsString(manifest);
storageService.store("manifest/manifest.json", json);
log.info("Manifest published: {} sites", ((List<?>)manifest.get("sites")).size());
// 2. 为每个站点生成合并后的 bundle 文件,保证脚本+页面版本一致
@SuppressWarnings("unchecked")
var sites = (List<Map<String, Object>>) manifest.get("sites");
int bundleCount = 0;
for (var site : sites) {
String siteId = (String) site.get("siteId");
try {
var bundle = configController.buildBundle(siteId);
if (bundle != null) {
String bundleJson = new com.fasterxml.jackson.databind.ObjectMapper()
.writeValueAsString(bundle);
storageService.store("bundles/" + siteId + ".json", bundleJson);
bundleCount++;
}
} catch (Exception e) {
log.warn("生成 bundle 失败 siteId={}: {}", siteId, e.getMessage());
}
}
log.info("Bundles generated: {} sites", bundleCount);
return ApiResponse.success(Map.of("version", manifest.get("version"),
"sites", ((List<?>)manifest.get("sites")).size()));
"sites", sites.size(), "bundles", bundleCount));
} catch (Exception e) { return ApiResponse.error(500, "发布失败: " + e.getMessage()); }
}

View File

@@ -1,18 +1,22 @@
package com.par.api.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.par.core.dto.ApiResponse;
import com.par.core.dto.SiteConfigDTO;
import com.par.core.entity.SiteConfig;
import com.par.core.adapter.GroovyScriptEngine;
import com.par.core.mapper.SiteMapper;
import com.par.core.service.ConfigService;
import com.par.core.service.SitePagesService;
import com.par.core.storage.CachedStorageService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -28,11 +32,18 @@ public class ConfigController {
private final ConfigService configService;
private final CachedStorageService storageService;
private final SiteMapper siteMapper;
private final SitePagesService sitePagesService;
private final com.par.core.mapper.RiskRuleMapper riskRuleMapper;
private static final ObjectMapper objectMapper = new ObjectMapper();
public ConfigController(ConfigService configService, CachedStorageService storageService, SiteMapper siteMapper) {
public ConfigController(ConfigService configService, CachedStorageService storageService,
SiteMapper siteMapper, SitePagesService sitePagesService,
com.par.core.mapper.RiskRuleMapper riskRuleMapper) {
this.configService = configService;
this.storageService = storageService;
this.siteMapper = siteMapper;
this.sitePagesService = sitePagesService;
this.riskRuleMapper = riskRuleMapper;
}
/**
@@ -138,4 +149,101 @@ public class ConfigController {
SiteConfig config = configService.submitConfig(siteId, version, groovyScript, submitterId);
return ApiResponse.success(configService.toDTO(config));
}
/**
* 下载合并配置包(供 mediabot 使用)
* 返回 JSON 包含:脚本、页面配置、风险规则,保证版本一致性
* 优先从云存储读取已发布的 bundle,无则实时合并
*/
@GetMapping("/{siteId}/bundle")
public ResponseEntity<?> downloadBundle(@PathVariable("siteId") String siteId) {
// 1. 尝试从云存储读取已发布的 bundle
String bundlePath = "bundles/" + siteId + ".json";
try {
String cached = storageService.read(bundlePath);
if (cached != null && !cached.isBlank()) {
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(cached);
}
} catch (Exception ignored) {}
// 2. 实时合并(兜底)
Map<String, Object> bundle = buildBundle(siteId);
if (bundle == null) return ResponseEntity.notFound().build();
try {
String json = objectMapper.writeValueAsString(bundle);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(json);
} catch (Exception e) {
log.error("Failed to serialize bundle for siteId={}", siteId, e);
return ResponseEntity.status(500).body("{\"error\":\"serialization failed\"}");
}
}
/**
* 实时构建站点配置包(脚本 + 页面配置 + 风险规则)
*/
Map<String, Object> buildBundle(String siteId) {
Map<String, Object> bundle = new LinkedHashMap<>();
bundle.put("siteId", siteId);
bundle.put("generatedAt", java.time.LocalDateTime.now().toString());
// 脚本
var site = siteMapper.selectBySiteId(siteId);
String siteType = site != null && site.getSiteType() != null ? site.getSiteType().getValue() : null;
SiteConfig latest = configService.getLatestConfig(siteId);
if (latest != null && latest.getScriptStoragePath() != null && !latest.getScriptStoragePath().isBlank()) {
try {
String script = storageService.read(latest.getScriptStoragePath());
if (script != null && !script.isBlank()) {
bundle.put("script", script);
bundle.put("scriptVersion", latest.getVersion());
bundle.put("scriptSource", "custom");
}
} catch (Exception e) { log.warn("读取脚本失败 siteId={}: {}", siteId, e.getMessage()); }
}
if (!bundle.containsKey("script") && siteType != null) {
String defScript = GroovyScriptEngine.loadDefaultByType(siteType);
if (defScript != null) {
bundle.put("script", defScript);
bundle.put("scriptSource", "default");
}
}
// 页面配置(优先自定义,回退默认模板)
Map<String, Object> pages = sitePagesService.getPages(siteId);
if (pages == null && siteType != null) {
pages = sitePagesService.getPages("default:" + siteType);
}
if (pages != null) {
bundle.put("pages", pages.get("entries"));
bundle.put("pagesVersion", pages.get("version"));
bundle.put("pagesSource", pages.containsKey("source") ? pages.get("source") : "custom");
}
// 风险规则(通过 siteType 查找)
if (siteType != null) {
var rules = riskRuleMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.par.core.entity.RiskRule>()
.eq(com.par.core.entity.RiskRule::getSiteType, siteType)
.eq(com.par.core.entity.RiskRule::getDeleted, 0)
.orderByDesc(com.par.core.entity.RiskRule::getId));
if (!rules.isEmpty()) {
var latestRule = rules.get(0);
try {
@SuppressWarnings("unchecked")
List<Map<String, Object>> rulesList = objectMapper.readValue(
latestRule.getRulesJson(), List.class);
bundle.put("riskRules", rulesList);
bundle.put("riskRulesVersion", latestRule.getVersion());
} catch (Exception ignored) {}
}
}
return bundle.containsKey("script") ? bundle : null;
}
}