From 98bc5b3183338947cb745084f1b138b99924a65a Mon Sep 17 00:00:00 2001 From: mediabot-pt <295750538+mediabot-pt@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:50:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=88=E5=B9=B6=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=8C=85=20-=20publish=20=E6=97=B6=E7=94=9F=E6=88=90=20bundle?= =?UTF-8?q?=EF=BC=8Cmediabot=20=E4=B8=8B=E8=BD=BD=E4=BF=9D=E8=AF=81?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../par/api/controller/AdminController.java | 26 ++++- .../par/api/controller/ConfigController.java | 110 +++++++++++++++++- 2 files changed, 133 insertions(+), 3 deletions(-) diff --git a/par-api/src/main/java/com/par/api/controller/AdminController.java b/par-api/src/main/java/com/par/api/controller/AdminController.java index da93b5a..e07ecee 100644 --- a/par-api/src/main/java/com/par/api/controller/AdminController.java +++ b/par-api/src/main/java/com/par/api/controller/AdminController.java @@ -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> 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>) 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()); } } diff --git a/par-api/src/main/java/com/par/api/controller/ConfigController.java b/par-api/src/main/java/com/par/api/controller/ConfigController.java index 103097c..c44e815 100644 --- a/par-api/src/main/java/com/par/api/controller/ConfigController.java +++ b/par-api/src/main/java/com/par/api/controller/ConfigController.java @@ -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 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 buildBundle(String siteId) { + Map 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 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() + .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> 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; + } }