From 2ced6f5dcb4206698990d95a67bf19e8d65f3dbc Mon Sep 17 00:00:00 2001 From: mediabot-pt <295750538+mediabot-pt@users.noreply.github.com> Date: Sat, 4 Jul 2026 23:35:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20mediabo=E6=8E=A5=E5=8F=A3=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0(pages/scripts/risk-rules)+DB=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/migration_v2.sql | 42 +++++++++ .../api/controller/ManifestController.java | 6 ++ .../api/controller/RiskRuleController.java | 74 +++++++++++++++ .../par/api/controller/ScriptController.java | 82 +++++++++++++++++ .../api/controller/SitePagesController.java | 76 ++++++++++++++++ .../java/com/par/core/entity/RiskRule.java | 36 ++++++++ .../java/com/par/core/entity/SitePages.java | 36 ++++++++ .../com/par/core/mapper/RiskRuleMapper.java | 9 ++ .../com/par/core/mapper/SitePagesMapper.java | 9 ++ .../com/par/core/service/RiskRuleService.java | 15 ++++ .../par/core/service/SitePagesService.java | 15 ++++ .../service/impl/RiskRuleServiceImpl.java | 83 +++++++++++++++++ .../service/impl/SitePagesServiceImpl.java | 89 +++++++++++++++++++ 13 files changed, 572 insertions(+) create mode 100644 docs/migration_v2.sql create mode 100644 par-api/src/main/java/com/par/api/controller/RiskRuleController.java create mode 100644 par-api/src/main/java/com/par/api/controller/ScriptController.java create mode 100644 par-api/src/main/java/com/par/api/controller/SitePagesController.java create mode 100644 par-core/src/main/java/com/par/core/entity/RiskRule.java create mode 100644 par-core/src/main/java/com/par/core/entity/SitePages.java create mode 100644 par-core/src/main/java/com/par/core/mapper/RiskRuleMapper.java create mode 100644 par-core/src/main/java/com/par/core/mapper/SitePagesMapper.java create mode 100644 par-core/src/main/java/com/par/core/service/RiskRuleService.java create mode 100644 par-core/src/main/java/com/par/core/service/SitePagesService.java create mode 100644 par-core/src/main/java/com/par/core/service/impl/RiskRuleServiceImpl.java create mode 100644 par-core/src/main/java/com/par/core/service/impl/SitePagesServiceImpl.java diff --git a/docs/migration_v2.sql b/docs/migration_v2.sql new file mode 100644 index 0000000..b1fddc5 --- /dev/null +++ b/docs/migration_v2.sql @@ -0,0 +1,42 @@ +-- PAR v2 新增表 +-- 站点页面配置(mediabot 页面入口) +CREATE TABLE IF NOT EXISTS site_pages ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + site_id VARCHAR(64) NOT NULL, + version VARCHAR(20) NOT NULL DEFAULT '1.0.0', + entries_json LONGTEXT NOT NULL, + deleted TINYINT NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_site_id (site_id, deleted) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 风险保号规则(按站点类型组织) +CREATE TABLE IF NOT EXISTS risk_rules ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + site_type VARCHAR(32) NOT NULL, + version INT NOT NULL DEFAULT 1, + rules_json LONGTEXT NOT NULL, + deleted TINYINT NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_site_type (site_type, deleted) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 示例数据:NexusPHP 页面入口 +INSERT INTO site_pages (site_id, version, entries_json) VALUES +('default-nexusphp', '1.0.0', '[ + {"key":"home", "label":"首页", "url":"{base}", "method":"GET"}, + {"key":"search", "label":"搜索页", "url":"{base}/torrents.php", "method":"GET"}, + {"key":"detail", "label":"种子详情", "url":"{base}/details.php?id=", "method":"GET"}, + {"key":"signIn", "label":"签到页", "url":"{base}/attendance.php", "method":"POST"}, + {"key":"user", "label":"用户详情", "url":"{base}/userdetails.php?id=","method":"GET"} +]'); + +-- 示例数据:NexusPHP 风险规则 +INSERT INTO risk_rules (site_type, version, rules_json) VALUES +('nexusphp', 1, '[ + {"type":"KILL","name":"登录间隔超过30天","metric":"daysSinceLastLogin","threshold":30,"unit":"DAY","condition":"GT","level":"CRITICAL"}, + {"type":"RATIO","name":"分享率低于0.3","metric":"shareRatio","threshold":0.3,"condition":"LT","level":"CRITICAL","applicable":{"daysSinceRegistration":30}}, + {"type":"TRAFFIC","name":"超过5天无流量","metric":"daysSinceLastTraffic","threshold":5,"unit":"DAY","condition":"GT","level":"WARN","applicable":{"daysSinceRegistration":30}} +]'); diff --git a/par-api/src/main/java/com/par/api/controller/ManifestController.java b/par-api/src/main/java/com/par/api/controller/ManifestController.java index b2b01e5..e150333 100644 --- a/par-api/src/main/java/com/par/api/controller/ManifestController.java +++ b/par-api/src/main/java/com/par/api/controller/ManifestController.java @@ -32,6 +32,7 @@ public class ManifestController { private final SiteMapper siteMapper; private final SiteConfigMapper siteConfigMapper; + private final com.par.core.mapper.SitePagesMapper sitePagesMapper; private final SiteService siteService; /** @@ -66,6 +67,11 @@ public class ManifestController { entry.put("hasScript", latest.getScriptStoragePath() != null && !latest.getScriptStoragePath().isBlank()); entry.put("updatedAt", latest.getCreatedAt()); } + // 是否配置了页面入口 + entry.put("hasPages", sitePagesMapper.selectCount( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(com.par.core.entity.SitePages::getSiteId, site.getSiteId()) + .eq(com.par.core.entity.SitePages::getDeleted, 0)) > 0); // 参与 ETag 计算 etagBuilder.append(site.getSiteId()) .append(latest != null ? latest.getScriptStoragePath() : "none") diff --git a/par-api/src/main/java/com/par/api/controller/RiskRuleController.java b/par-api/src/main/java/com/par/api/controller/RiskRuleController.java new file mode 100644 index 0000000..e863bca --- /dev/null +++ b/par-api/src/main/java/com/par/api/controller/RiskRuleController.java @@ -0,0 +1,74 @@ +package com.par.api.controller; + +import com.par.core.dto.ApiResponse; +import com.par.core.service.RiskRuleService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 风险规则接口 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +public class RiskRuleController { + + private final RiskRuleService riskRuleService; + + /** 获取风险规则 */ + @GetMapping("/api/v1/risk-rules/{siteType}") + public ResponseEntity getRules( + @PathVariable String siteType, + @RequestParam(defaultValue = "0") int version) { + int currentVer = riskRuleService.getVersion(siteType); + if (currentVer <= version) { + return ResponseEntity.status(304).build(); + } + Map data = riskRuleService.getRules(siteType); + if (data == null) return ResponseEntity.status(304).build(); + return ResponseEntity.ok(data); + } + + /** 提交风险规则 */ + @PostMapping("/api/v1/risk-rules/{siteType}") + public ApiResponse saveRules( + @PathVariable String siteType, + @RequestBody Map payload) { + @SuppressWarnings("unchecked") + List> rules = (List>) payload.get("rules"); + if (rules == null || rules.isEmpty()) { + return ApiResponse.error(400, "rules is required"); + } + riskRuleService.saveRules(siteType, rules); + return ApiResponse.success(); + } + + /** 管理员查看所有风险规则列表 */ + @GetMapping("/api/v1/admin/risk-rules") + public ApiResponse>> adminList(HttpServletRequest request) { + checkAdmin(request); + return ApiResponse.success(riskRuleService.listAll()); + } + + /** 管理员查看某个类型风险规则内容 */ + @GetMapping("/api/v1/admin/risk-rules/{siteType}") + public ApiResponse> adminGet(@PathVariable String siteType, HttpServletRequest request) { + checkAdmin(request); + Map data = riskRuleService.getRules(siteType); + if (data == null) return ApiResponse.error(404, "风险规则不存在"); + return ApiResponse.success(data); + } + + private void checkAdmin(HttpServletRequest request) { + var level = request.getAttribute("trustLevel"); + if (level == null || !"ADMIN".equals(level.toString())) { + throw new IllegalArgumentException("Admin permission required"); + } + } +} diff --git a/par-api/src/main/java/com/par/api/controller/ScriptController.java b/par-api/src/main/java/com/par/api/controller/ScriptController.java new file mode 100644 index 0000000..1358fa3 --- /dev/null +++ b/par-api/src/main/java/com/par/api/controller/ScriptController.java @@ -0,0 +1,82 @@ +package com.par.api.controller; + +import com.par.core.adapter.GroovyScriptEngine; +import com.par.core.dto.ApiResponse; +import com.par.core.entity.SiteConfig; +import com.par.core.entity.Site; +import com.par.core.mapper.SiteMapper; +import com.par.core.service.ConfigService; +import com.par.core.storage.CachedStorageService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * Groovy 脚本接口(mediabot 使用) + */ +@Slf4j +@RestController +@RequiredArgsConstructor +public class ScriptController { + + private final ConfigService configService; + private final CachedStorageService storageService; + private final SiteMapper siteMapper; + + /** 下载最新 Groovy 脚本 */ + @GetMapping("/api/v1/scripts/{siteId}") + public ResponseEntity downloadScript(@PathVariable String siteId) { + 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()) { + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_TYPE, "text/plain;charset=UTF-8") + .body(script); + } + } catch (Exception ignored) {} + } + // 回退到默认模板 + var site = siteMapper.selectBySiteId(siteId); + String siteType = site != null && site.getSiteType() != null + ? site.getSiteType().getValue() : null; + String defaultScript = GroovyScriptEngine.loadDefaultByType(siteType); + if (defaultScript != null) { + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_TYPE, "text/plain;charset=UTF-8") + .body(defaultScript); + } + return ResponseEntity.notFound().build(); + } + + /** 获取脚本版本 */ + @GetMapping("/api/v1/scripts/{siteId}/version") + public ApiResponse> getVersion(@PathVariable String siteId) { + SiteConfig latest = configService.getLatestConfig(siteId); + String v = latest != null ? latest.getVersion() : "0"; + return ApiResponse.success(Map.of("version", v)); + } + + /** 上传 Groovy 脚本(PSK 签名认证) */ + @PutMapping("/api/v1/scripts/{siteId}") + public ApiResponse uploadScript( + @PathVariable String siteId, + @RequestBody String script, + HttpServletRequest request) { + if (script == null || script.isBlank()) { + return ApiResponse.error(400, "script body is required"); + } + // 版本号由服务端管理 + Long submitterId = (Long) request.getAttribute("accountId"); + SiteConfig config = configService.submitConfig(siteId, "1", script, submitterId); + return ApiResponse.success(Map.of("version", config.getVersion())); + } +} diff --git a/par-api/src/main/java/com/par/api/controller/SitePagesController.java b/par-api/src/main/java/com/par/api/controller/SitePagesController.java new file mode 100644 index 0000000..31e8f8c --- /dev/null +++ b/par-api/src/main/java/com/par/api/controller/SitePagesController.java @@ -0,0 +1,76 @@ +package com.par.api.controller; + +import com.par.core.dto.ApiResponse; +import com.par.core.service.SitePagesService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 站点页面配置接口 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +public class SitePagesController { + + private final SitePagesService sitePagesService; + + /** 获取站点页面配置 */ + @GetMapping("/api/v1/sites/{siteId}/pages") + public ApiResponse> getPages(@PathVariable String siteId) { + Map data = sitePagesService.getPages(siteId); + if (data == null) return ApiResponse.error(404, "页面配置不存在"); + return ApiResponse.success(data); + } + + /** 获取页面配置版本 */ + @GetMapping("/api/v1/sites/{siteId}/pages/version") + public ApiResponse> getVersion(@PathVariable String siteId) { + String v = sitePagesService.getVersion(siteId); + return ApiResponse.success(Map.of("version", v != null ? v : "0")); + } + + /** 上传/更新页面配置(PSK 签名认证) */ + @PutMapping("/api/v1/sites/{siteId}/pages") + public ApiResponse savePages( + @PathVariable String siteId, + @RequestBody Map payload, + HttpServletRequest request) { + @SuppressWarnings("unchecked") + List> entries = (List>) payload.get("entries"); + if (entries == null || entries.isEmpty()) { + return ApiResponse.error(400, "entries is required"); + } + sitePagesService.savePages(siteId, entries); + return ApiResponse.success(); + } + + /** 管理员查看所有站点页面配置列表 */ + @GetMapping("/api/v1/admin/site-pages") + public ApiResponse>> adminList(HttpServletRequest request) { + checkAdmin(request); + return ApiResponse.success(sitePagesService.listAll()); + } + + /** 管理员查看某站点页面配置内容 */ + @GetMapping("/api/v1/admin/site-pages/{siteId}") + public ApiResponse> adminGet(@PathVariable String siteId, HttpServletRequest request) { + checkAdmin(request); + Map data = sitePagesService.getPages(siteId); + if (data == null) return ApiResponse.error(404, "页面配置不存在"); + return ApiResponse.success(data); + } + + private void checkAdmin(HttpServletRequest request) { + var level = request.getAttribute("trustLevel"); + if (level == null || !"ADMIN".equals(level.toString())) { + throw new IllegalArgumentException("Admin permission required"); + } + } +} diff --git a/par-core/src/main/java/com/par/core/entity/RiskRule.java b/par-core/src/main/java/com/par/core/entity/RiskRule.java new file mode 100644 index 0000000..4e5ec2e --- /dev/null +++ b/par-core/src/main/java/com/par/core/entity/RiskRule.java @@ -0,0 +1,36 @@ +package com.par.core.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 风险规则实体 + */ +@Data +@TableName("risk_rules") +public class RiskRule { + + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("site_type") + private String siteType; + + @TableField("version") + private Integer version; + + @TableField("rules_json") + private String rulesJson; + + @TableLogic + @TableField("deleted") + private Integer deleted; + + @TableField(value = "created_at", fill = FieldFill.INSERT) + private LocalDateTime createdAt; + + @TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedAt; +} diff --git a/par-core/src/main/java/com/par/core/entity/SitePages.java b/par-core/src/main/java/com/par/core/entity/SitePages.java new file mode 100644 index 0000000..43ccf57 --- /dev/null +++ b/par-core/src/main/java/com/par/core/entity/SitePages.java @@ -0,0 +1,36 @@ +package com.par.core.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 站点页面配置实体 + */ +@Data +@TableName("site_pages") +public class SitePages { + + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("site_id") + private String siteId; + + @TableField("version") + private String version; + + @TableField("entries_json") + private String entriesJson; + + @TableLogic + @TableField("deleted") + private Integer deleted; + + @TableField(value = "created_at", fill = FieldFill.INSERT) + private LocalDateTime createdAt; + + @TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedAt; +} diff --git a/par-core/src/main/java/com/par/core/mapper/RiskRuleMapper.java b/par-core/src/main/java/com/par/core/mapper/RiskRuleMapper.java new file mode 100644 index 0000000..ffbb59e --- /dev/null +++ b/par-core/src/main/java/com/par/core/mapper/RiskRuleMapper.java @@ -0,0 +1,9 @@ +package com.par.core.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.par.core.entity.RiskRule; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface RiskRuleMapper extends BaseMapper { +} diff --git a/par-core/src/main/java/com/par/core/mapper/SitePagesMapper.java b/par-core/src/main/java/com/par/core/mapper/SitePagesMapper.java new file mode 100644 index 0000000..4e9914a --- /dev/null +++ b/par-core/src/main/java/com/par/core/mapper/SitePagesMapper.java @@ -0,0 +1,9 @@ +package com.par.core.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.par.core.entity.SitePages; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SitePagesMapper extends BaseMapper { +} diff --git a/par-core/src/main/java/com/par/core/service/RiskRuleService.java b/par-core/src/main/java/com/par/core/service/RiskRuleService.java new file mode 100644 index 0000000..3697522 --- /dev/null +++ b/par-core/src/main/java/com/par/core/service/RiskRuleService.java @@ -0,0 +1,15 @@ +package com.par.core.service; + +import java.util.List; +import java.util.Map; + +public interface RiskRuleService { + /** 获取指定站点类型的风险规则 */ + Map getRules(String siteType); + /** 获取规则版本号 */ + int getVersion(String siteType); + /** 保存规则 */ + void saveRules(String siteType, List> rules); + /** 获取所有规则列表(管理员) */ + List> listAll(); +} diff --git a/par-core/src/main/java/com/par/core/service/SitePagesService.java b/par-core/src/main/java/com/par/core/service/SitePagesService.java new file mode 100644 index 0000000..205e098 --- /dev/null +++ b/par-core/src/main/java/com/par/core/service/SitePagesService.java @@ -0,0 +1,15 @@ +package com.par.core.service; + +import java.util.List; +import java.util.Map; + +public interface SitePagesService { + /** 获取站点页面配置 */ + Map getPages(String siteId); + /** 获取页面配置版本号 */ + String getVersion(String siteId); + /** 保存/更新页面配置 */ + void savePages(String siteId, List> entries); + /** 获取所有站点页面配置列表(管理员) */ + List> listAll(); +} diff --git a/par-core/src/main/java/com/par/core/service/impl/RiskRuleServiceImpl.java b/par-core/src/main/java/com/par/core/service/impl/RiskRuleServiceImpl.java new file mode 100644 index 0000000..821cc93 --- /dev/null +++ b/par-core/src/main/java/com/par/core/service/impl/RiskRuleServiceImpl.java @@ -0,0 +1,83 @@ +package com.par.core.service.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.par.core.entity.RiskRule; +import com.par.core.mapper.RiskRuleMapper; +import com.par.core.service.RiskRuleService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RiskRuleServiceImpl implements RiskRuleService { + + private final RiskRuleMapper mapper; + private static final ObjectMapper om = new ObjectMapper(); + + @Override + public Map getRules(String siteType) { + RiskRule rr = mapper.selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(RiskRule::getSiteType, siteType).eq(RiskRule::getDeleted, 0) + .orderByDesc(RiskRule::getId).last("limit 1")); + if (rr == null) return null; + try { + List> rules = om.readValue(rr.getRulesJson(), + new TypeReference>>() {}); + Map result = new LinkedHashMap<>(); + result.put("version", rr.getVersion()); + result.put("updatedAt", rr.getUpdatedAt()); + result.put("rules", rules); + return result; + } catch (Exception e) { + log.error("解析风险规则失败 siteType={}", siteType, e); + return null; + } + } + + @Override + public int getVersion(String siteType) { + RiskRule rr = mapper.selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(RiskRule::getSiteType, siteType).eq(RiskRule::getDeleted, 0) + .orderByDesc(RiskRule::getId).last("limit 1")); + return rr != null ? rr.getVersion() : 0; + } + + @Override + public void saveRules(String siteType, List> rules) { + try { + String json = om.writeValueAsString(rules); + int nextVer = getVersion(siteType) + 1; + RiskRule rr = new RiskRule(); + rr.setSiteType(siteType); + rr.setVersion(nextVer); + rr.setRulesJson(json); + mapper.insert(rr); + log.info("风险规则已保存: siteType={}, version={}", siteType, nextVer); + } catch (Exception e) { + log.error("保存风险规则失败 siteType={}", siteType, e); + throw new RuntimeException("保存失败: " + e.getMessage()); + } + } + + @Override + public List> listAll() { + List all = mapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(RiskRule::getDeleted, 0)); + List> result = new ArrayList<>(); + for (RiskRule rr : all) { + Map m = new LinkedHashMap<>(); + m.put("id", rr.getId()); + m.put("siteType", rr.getSiteType()); + m.put("version", rr.getVersion()); + m.put("updatedAt", rr.getUpdatedAt()); + result.add(m); + } + return result; + } +} diff --git a/par-core/src/main/java/com/par/core/service/impl/SitePagesServiceImpl.java b/par-core/src/main/java/com/par/core/service/impl/SitePagesServiceImpl.java new file mode 100644 index 0000000..cd5e209 --- /dev/null +++ b/par-core/src/main/java/com/par/core/service/impl/SitePagesServiceImpl.java @@ -0,0 +1,89 @@ +package com.par.core.service.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.par.core.entity.SitePages; +import com.par.core.mapper.SitePagesMapper; +import com.par.core.service.SitePagesService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SitePagesServiceImpl implements SitePagesService { + + private final SitePagesMapper mapper; + private static final ObjectMapper om = new ObjectMapper(); + + @Override + public Map getPages(String siteId) { + SitePages sp = mapper.selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(SitePages::getSiteId, siteId).eq(SitePages::getDeleted, 0) + .orderByDesc(SitePages::getId).last("limit 1")); + if (sp == null) return null; + try { + List> entries = om.readValue(sp.getEntriesJson(), + new TypeReference>>() {}); + Map result = new LinkedHashMap<>(); + result.put("siteId", siteId); + result.put("version", sp.getVersion()); + result.put("entries", entries); + result.put("updatedAt", sp.getUpdatedAt()); + return result; + } catch (Exception e) { + log.error("解析页面配置失败 siteId={}", siteId, e); + return null; + } + } + + @Override + public String getVersion(String siteId) { + SitePages sp = mapper.selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(SitePages::getSiteId, siteId).eq(SitePages::getDeleted, 0) + .orderByDesc(SitePages::getId).last("limit 1")); + return sp != null ? sp.getVersion() : null; + } + + @Override + public void savePages(String siteId, List> entries) { + try { + String json = om.writeValueAsString(entries); + // 版本号递增 + String currentVer = getVersion(siteId); + int nextVer = 1; + if (currentVer != null) { + try { nextVer = Integer.parseInt(currentVer) + 1; } catch (NumberFormatException e) { /* keep 1 */ } + } + SitePages sp = new SitePages(); + sp.setSiteId(siteId); + sp.setVersion(String.valueOf(nextVer)); + sp.setEntriesJson(json); + mapper.insert(sp); + log.info("页面配置已保存: siteId={}, version={}", siteId, nextVer); + } catch (Exception e) { + log.error("保存页面配置失败 siteId={}", siteId, e); + throw new RuntimeException("保存页面配置失败: " + e.getMessage()); + } + } + + @Override + public List> listAll() { + List all = mapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(SitePages::getDeleted, 0)); + List> result = new ArrayList<>(); + for (SitePages sp : all) { + Map m = new LinkedHashMap<>(); + m.put("id", sp.getId()); + m.put("siteId", sp.getSiteId()); + m.put("version", sp.getVersion()); + m.put("updatedAt", sp.getUpdatedAt()); + result.add(m); + } + return result; + } +}