refactor: 默认模板服务端+统一导出导入+页面配置版本历史+前端

This commit is contained in:
mediabot-pt
2026-07-05 19:55:51 +08:00
parent 4ac61a1868
commit e0e61a0afc
5 changed files with 150 additions and 67 deletions

View File

@@ -402,43 +402,67 @@ public class AdminController {
return ApiResponse.success();
}
// ==================== 脚本批量导入导出 ====================
// ==================== 全配置批量导入导出 ====================
/**
* 导出所有站点脚本为 ZIP:每个站点一个 .groovy 文件
* 导出所有配置为 ZIP:scripts/{siteId}.groovy, pages/{siteId}.json, rules/{type}.json
*/
@GetMapping("/scripts/export")
public ResponseEntity<byte[]> exportScripts(HttpServletRequest request) {
checkAdmin(request);
List<SiteConfig> configs = siteConfigMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SiteConfig>()
.eq(SiteConfig::getDeleted, 0)
.eq(SiteConfig::getIsLatest, true)
.isNotNull(SiteConfig::getScriptStoragePath));
try {
var baos = new java.io.ByteArrayOutputStream();
try (var zos = new java.util.zip.ZipOutputStream(baos)) {
for (SiteConfig sc : configs) {
// 1. 脚本
for (SiteConfig sc : siteConfigMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SiteConfig>()
.eq(SiteConfig::getDeleted, 0).eq(SiteConfig::getIsLatest, true)
.isNotNull(SiteConfig::getScriptStoragePath))) {
String script = storageService.read(sc.getScriptStoragePath());
if (script == null || script.isBlank()) continue;
var entry = new java.util.zip.ZipEntry(sc.getSiteId() + ".groovy");
zos.putNextEntry(entry);
zos.write(script.getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
addZipEntry(zos, "scripts/" + sc.getSiteId() + ".groovy", script);
}
// 2. 页面配置(去重取最新版本)
Set<String> seenPages = new HashSet<>();
for (var sp : ((com.par.core.service.impl.SitePagesServiceImpl) siteService).mapper
.selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.par.core.entity.SitePages>()
.eq(com.par.core.entity.SitePages::getDeleted, 0)
.orderByDesc(com.par.core.entity.SitePages::getId))) {
if (seenPages.add(sp.getSiteId())) {
addZipEntry(zos, "pages/" + sp.getSiteId() + ".json", sp.getEntriesJson());
}
}
// 3. 风险规则(去重取最新版本)
Set<String> seenRules = new HashSet<>();
var ruleMapper = new com.par.core.mapper.RiskRuleMapper() {};
for (var rr : ruleMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.par.core.entity.RiskRule>()
.eq(com.par.core.entity.RiskRule::getDeleted, 0)
.orderByDesc(com.par.core.entity.RiskRule::getId))) {
if (seenRules.add(rr.getSiteType())) {
addZipEntry(zos, "rules/" + rr.getSiteType() + ".json", rr.getRulesJson());
}
}
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=scripts.zip")
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=par-config.zip")
.contentType(MediaType.parseMediaType("application/zip"))
.body(baos.toByteArray());
} catch (Exception e) {
log.error("导出脚本失败", e);
log.error("导出失败", e);
return ResponseEntity.status(500).build();
}
}
private void addZipEntry(java.util.zip.ZipOutputStream zos, String name, String content) throws Exception {
var entry = new java.util.zip.ZipEntry(name);
zos.putNextEntry(entry);
zos.write(content.getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}
/**
* 导入脚本 ZIP:每个 .groovy 文件名 = siteId,保存到本地
* 导入 ZIP:scripts/*.groovy, pages/*.json, rules/*.json
*/
@PostMapping("/scripts/import")
public ApiResponse<Map<String, Object>> importScripts(
@@ -449,37 +473,40 @@ public class AdminController {
try (var zis = new java.util.zip.ZipInputStream(file.getInputStream())) {
java.util.zip.ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.isDirectory() || !entry.getName().endsWith(".groovy")) continue;
String siteId = entry.getName().replace(".groovy", "");
String script = new String(zis.readAllBytes(), StandardCharsets.UTF_8);
if (script.isBlank()) continue;
if (entry.isDirectory()) continue;
String name = entry.getName();
String content = new String(zis.readAllBytes(), StandardCharsets.UTF_8);
if (content.isBlank()) continue;
try {
int nextVersion = 1;
List<SiteConfig> existing = siteConfigMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SiteConfig>()
.eq(SiteConfig::getSiteId, siteId)
.eq(SiteConfig::getDeleted, 0)
.orderByDesc(SiteConfig::getVersion)
.last("limit 1"));
if (!existing.isEmpty()) {
try { nextVersion = Integer.parseInt(existing.get(0).getVersion()) + 1; }
catch (NumberFormatException ignored) {}
if (name.startsWith("scripts/") && name.endsWith(".groovy")) {
String sid = name.replace("scripts/", "").replace(".groovy", "");
configService.submitConfig(sid, String.valueOf(System.currentTimeMillis()), content, null);
} else if (name.startsWith("pages/") && name.endsWith(".json")) {
String sid = name.replace("pages/", "").replace(".json", "");
siteService.savePages(sid, parseJson(content));
} else if (name.startsWith("rules/") && name.endsWith(".json")) {
String type = name.replace("rules/", "").replace(".json", "");
((com.par.core.service.RiskRuleService) riskService).saveRules(type, parseJson(content));
}
configService.submitConfig(siteId, String.valueOf(nextVersion), script, null);
ok++;
} catch (Exception ex) {
log.error("导入脚本失败 siteId={}: {}", siteId, ex.getMessage());
fail++;
}
} catch (Exception ex) { log.error("导入失败: {}", name, ex); fail++; }
zis.closeEntry();
}
} catch (Exception e) {
log.error("解压导入ZIP失败", e);
return ApiResponse.error(500, "导入失败: " + e.getMessage());
}
} catch (Exception e) { return ApiResponse.error(500, "导入失败: " + e.getMessage()); }
return ApiResponse.success(Map.of("ok", ok, "fail", fail));
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> parseJson(String json) {
try { return new com.fasterxml.jackson.databind.ObjectMapper().readValue(json, java.util.List.class); }
catch (Exception e) { return java.util.List.of(); }
}
private final com.par.core.service.SitePagesService siteService;
private final com.par.core.service.RiskRuleService riskService;
// ==================== 旧脚本导入导出(保留兼容) ====================
private void checkAdmin(HttpServletRequest request) {
TrustLevel level = (TrustLevel) request.getAttribute("trustLevel");
if (level == null || level != TrustLevel.ADMIN) {

View File

@@ -111,6 +111,26 @@ public class RiskRuleController {
return ApiResponse.success(result);
}
/** 获取或保存默认风险规则模板 */
@GetMapping("/api/v1/admin/default-rules/{type}")
public ApiResponse<Map<String, Object>> getDefaultRules(@PathVariable String type, HttpServletRequest request) {
checkAdmin(request);
String key = "default:" + type;
Map<String, Object> data = riskRuleService.getRules(key);
if (data == null) data = Map.of("version", 0, "rules", List.of());
return ApiResponse.success(data);
}
@PostMapping("/api/v1/admin/default-rules/{type}")
public ApiResponse<Void> saveDefaultRules(
@PathVariable String type, @RequestBody Map<String, Object> payload, HttpServletRequest request) {
checkAdmin(request);
@SuppressWarnings("unchecked")
var rules = (List<Map<String, Object>>) payload.get("rules");
riskRuleService.saveRules("default:" + type, rules != null ? rules : List.of());
return ApiResponse.success();
}
private void checkAdmin(HttpServletRequest request) {
var level = request.getAttribute("trustLevel");
if (level == null || !"ADMIN".equals(level.toString())) {

View File

@@ -116,6 +116,28 @@ public class SitePagesController {
return ApiResponse.success(result);
}
/** 获取或保存默认页面模板 */
@GetMapping("/api/v1/admin/default-pages/{type}")
public ApiResponse<Map<String, Object>> getDefaultPages(@PathVariable String type, HttpServletRequest request) {
checkAdmin(request);
String defaultId = "default:" + type;
Map<String, Object> data = sitePagesService.getPages(defaultId);
if (data == null) {
data = Map.of("siteId", defaultId, "version", "0", "entries", List.of());
}
return ApiResponse.success(data);
}
@PutMapping("/api/v1/admin/default-pages/{type}")
public ApiResponse<Void> saveDefaultPages(
@PathVariable String type, @RequestBody Map<String, Object> payload, HttpServletRequest request) {
checkAdmin(request);
@SuppressWarnings("unchecked")
var entries = (List<Map<String, Object>>) payload.get("entries");
sitePagesService.savePages("default:" + type, entries != null ? entries : List.of());
return ApiResponse.success();
}
private void checkAdmin(HttpServletRequest request) {
var level = request.getAttribute("trustLevel");
if (level == null || !"ADMIN".equals(level.toString())) {