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 e2b2817..d3309aa 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 @@ -7,6 +7,7 @@ import com.par.core.entity.SiteConfig; import com.par.core.enums.SiteStatus; import com.par.core.mapper.SiteConfigMapper; import com.par.core.mapper.SiteMapper; +import com.par.core.service.SiteService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -31,6 +32,7 @@ public class ManifestController { private final SiteMapper siteMapper; private final SiteConfigMapper siteConfigMapper; + private final SiteService siteService; /** * 获取全局 Manifest(支持 ETag) @@ -49,9 +51,11 @@ public class ManifestController { // 构建 entries 并计算 ETag StringBuilder etagBuilder = new StringBuilder(); List> entries = sites.stream().map(site -> { + var dto = siteService.toDTO(site); Map entry = new HashMap<>(); entry.put("siteId", site.getSiteId()); - entry.put("name", site.getName()); + entry.put("name", dto.getNames() != null && !dto.getNames().isEmpty() ? dto.getNames().get(0) : site.getSiteId()); + entry.put("baseUrl", dto.getUrls() != null && !dto.getUrls().isEmpty() ? dto.getUrls().get(0) : null); entry.put("siteType", site.getSiteType() != null ? site.getSiteType().getValue() : null); entry.put("schemaVersion", site.getSchemaVersion()); entry.put("status", site.getStatus() != null ? site.getStatus().getValue() : null); diff --git a/par-core/src/main/java/com/par/core/dto/SiteDTO.java b/par-core/src/main/java/com/par/core/dto/SiteDTO.java index 26cd38d..23cc842 100644 --- a/par-core/src/main/java/com/par/core/dto/SiteDTO.java +++ b/par-core/src/main/java/com/par/core/dto/SiteDTO.java @@ -5,6 +5,7 @@ import com.par.core.enums.SiteType; import lombok.Data; import java.time.LocalDateTime; +import java.util.List; /** * 站点信息 DTO @@ -14,9 +15,10 @@ public class SiteDTO { private Long id; private String siteId; - private String name; + private List names; private String description; - private String baseUrl; + private String notes; + private List urls; private String iconUrl; private SiteType siteType; private String schemaVersion; diff --git a/par-core/src/main/java/com/par/core/entity/Site.java b/par-core/src/main/java/com/par/core/entity/Site.java index 50bdf26..8fc4bad 100644 --- a/par-core/src/main/java/com/par/core/entity/Site.java +++ b/par-core/src/main/java/com/par/core/entity/Site.java @@ -20,15 +20,26 @@ public class Site { @TableField("site_id") private String siteId; + /** @deprecated 字段保留用于 Flyway 数据迁移,业务逻辑改用 names */ @TableField("name") private String name; + @TableField("names") + private String namesJson; + @TableField("description") private String description; + @TableField("notes") + private String notes; + + /** @deprecated 字段保留用于 Flyway 数据迁移,业务逻辑改用 urls */ @TableField("base_url") private String baseUrl; + @TableField("urls") + private String urlsJson; + @TableField("icon_url") private String iconUrl; diff --git a/par-core/src/main/java/com/par/core/service/impl/SiteServiceImpl.java b/par-core/src/main/java/com/par/core/service/impl/SiteServiceImpl.java index 21f1525..68ab140 100644 --- a/par-core/src/main/java/com/par/core/service/impl/SiteServiceImpl.java +++ b/par-core/src/main/java/com/par/core/service/impl/SiteServiceImpl.java @@ -1,6 +1,7 @@ package com.par.core.service.impl; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.par.core.dto.SiteDTO; import com.par.core.entity.Site; @@ -12,14 +13,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.util.LinkedHashMap; +import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; -/** - * 站点服务实现 - */ @Slf4j @Service @RequiredArgsConstructor @@ -28,6 +25,31 @@ public class SiteServiceImpl implements SiteService { private final SiteMapper siteMapper; private static final ObjectMapper objectMapper = new ObjectMapper(); + // ==================== 工具方法 ==================== + + private String toJsonArray(List list) { + if (list == null || list.isEmpty()) return null; + try { return objectMapper.writeValueAsString(list); } catch (JsonProcessingException e) { return null; } + } + + private List fromJsonArray(String json) { + if (json == null || json.isBlank()) return new ArrayList<>(); + try { return objectMapper.readValue(json, new TypeReference>() {}); } + catch (Exception e) { return new ArrayList<>(); } + } + + private String firstName(Site site) { + List names = fromJsonArray(site.getNamesJson()); + return names.isEmpty() ? site.getSiteId() : names.get(0); + } + + private String firstUrl(Site site) { + List urls = fromJsonArray(site.getUrlsJson()); + return urls.isEmpty() ? "" : urls.get(0); + } + + // ==================== 业务方法 ==================== + @Override public List listActiveSites() { return siteMapper.selectList( @@ -48,9 +70,10 @@ public class SiteServiceImpl implements SiteService { SiteDTO dto = new SiteDTO(); dto.setId(site.getId()); dto.setSiteId(site.getSiteId()); - dto.setName(site.getName()); + dto.setNames(fromJsonArray(site.getNamesJson())); dto.setDescription(site.getDescription()); - dto.setBaseUrl(site.getBaseUrl()); + dto.setNotes(site.getNotes()); + dto.setUrls(fromJsonArray(site.getUrlsJson())); dto.setIconUrl(site.getIconUrl()); dto.setSiteType(site.getSiteType()); dto.setSchemaVersion(site.getSchemaVersion()); @@ -63,29 +86,32 @@ public class SiteServiceImpl implements SiteService { @Override public Site submitSite(String siteId, String name, String description, String baseUrl, String iconUrl, String siteType, Long submitterId) { - // Phase 1: 简化实现,直接创建/更新 Site existing = siteMapper.selectBySiteId(siteId); if (existing != null) { - existing.setName(name); - existing.setDescription(description); - existing.setBaseUrl(baseUrl); - existing.setIconUrl(iconUrl); - if (siteType != null) { - existing.setSiteType(com.par.core.enums.SiteType.fromValue(siteType)); + if (name != null && !name.isBlank()) { + List names = fromJsonArray(existing.getNamesJson()); + if (!names.contains(name)) { names.add(0, name); existing.setNamesJson(toJsonArray(names)); } } + if (baseUrl != null && !baseUrl.isBlank()) { + List urls = fromJsonArray(existing.getUrlsJson()); + if (!urls.contains(baseUrl)) { urls.add(0, baseUrl); existing.setUrlsJson(toJsonArray(urls)); } + } + if (description != null) existing.setDescription(description); + if (iconUrl != null) existing.setIconUrl(iconUrl); + if (siteType != null) existing.setSiteType(SiteType.fromValue(siteType)); siteMapper.updateById(existing); return existing; } Site site = new Site(); site.setSiteId(siteId); - site.setName(name); + site.setNamesJson(toJsonArray(name != null ? List.of(name) : List.of())); + site.setUrlsJson(toJsonArray(baseUrl != null ? List.of(baseUrl) : List.of())); site.setDescription(description); - site.setBaseUrl(baseUrl); site.setIconUrl(iconUrl); - site.setSiteType(siteType != null ? com.par.core.enums.SiteType.fromValue(siteType) : com.par.core.enums.SiteType.UNKNOWN); + site.setSiteType(siteType != null ? SiteType.fromValue(siteType) : SiteType.UNKNOWN); site.setSchemaVersion("1.0.0"); - site.setStatus(SiteStatus.PENDING); // 新站点需审核 + site.setStatus(SiteStatus.PENDING); site.setSubmitterId(submitterId); siteMapper.insert(site); return site; @@ -94,42 +120,13 @@ public class SiteServiceImpl implements SiteService { @Override public Site updateSiteInfo(String siteId, String status, List names, List urls, String notes, Long submitterId) { - // 映射状态:正常→ACTIVE, 关停/已迁移→DEPRECATED, 维护中→ACTIVE(带备注) - SiteStatus siteStatus; - if (status == null) { - siteStatus = SiteStatus.ACTIVE; - } else { - switch (status) { - case "关停": - case "已迁移": - siteStatus = SiteStatus.DEPRECATED; - break; - case "维护中": - case "正常": - default: - siteStatus = SiteStatus.ACTIVE; - break; - } - } - - // 构建额外信息 JSON - Map extraInfo = new LinkedHashMap<>(); - if (names != null && !names.isEmpty()) extraInfo.put("names", names); - if (urls != null && !urls.isEmpty()) extraInfo.put("urls", urls); - if (notes != null && !notes.isBlank()) extraInfo.put("notes", notes); - String extraJson = null; - try { - if (!extraInfo.isEmpty()) extraJson = objectMapper.writeValueAsString(extraInfo); - } catch (JsonProcessingException ignored) {} + SiteStatus siteStatus = mapStatus(status); Site existing = siteMapper.selectBySiteId(siteId); if (existing != null) { - // 更新已有站点 - if (names != null && !names.isEmpty()) existing.setName(names.get(0)); - if (urls != null && !urls.isEmpty()) existing.setBaseUrl(urls.get(0)); - if (extraJson != null) existing.setDescription( - existing.getDescription() != null && !existing.getDescription().isBlank() - ? existing.getDescription() : extraJson); + if (names != null && !names.isEmpty()) existing.setNamesJson(toJsonArray(names)); + if (urls != null && !urls.isEmpty()) existing.setUrlsJson(toJsonArray(urls)); + if (notes != null && !notes.isBlank()) existing.setNotes(notes); existing.setStatus(siteStatus); existing.setSubmitterId(submitterId != null ? submitterId : existing.getSubmitterId()); siteMapper.updateById(existing); @@ -137,12 +134,11 @@ public class SiteServiceImpl implements SiteService { return existing; } - // 创建新站点(PENDING 状态) Site site = new Site(); site.setSiteId(siteId); - site.setName(names != null && !names.isEmpty() ? names.get(0) : siteId); - site.setBaseUrl(urls != null && !urls.isEmpty() ? urls.get(0) : ""); - site.setDescription(extraJson); + site.setNamesJson(toJsonArray(names != null && !names.isEmpty() ? names : List.of(siteId))); + site.setUrlsJson(toJsonArray(urls != null && !urls.isEmpty() ? urls : List.of())); + site.setNotes(notes); site.setSiteType(SiteType.UNKNOWN); site.setSchemaVersion("1.0.0"); site.setStatus(SiteStatus.PENDING); @@ -156,19 +152,26 @@ public class SiteServiceImpl implements SiteService { public Site updateSite(String siteId, String name, String description, String baseUrl, String siteType, Long operatorId) { Site site = siteMapper.selectBySiteId(siteId); - if (site == null) { - throw new IllegalArgumentException("站点不存在: " + siteId); + if (site == null) throw new IllegalArgumentException("站点不存在: " + siteId); + + if (name != null && !name.isBlank()) { + List names = fromJsonArray(site.getNamesJson()); + if (names.isEmpty()) names.add(name); + else names.set(0, name); + site.setNamesJson(toJsonArray(names)); + } + if (baseUrl != null && !baseUrl.isBlank()) { + List urls = fromJsonArray(site.getUrlsJson()); + if (urls.isEmpty()) urls.add(baseUrl); + else urls.set(0, baseUrl); + site.setUrlsJson(toJsonArray(urls)); } - if (name != null && !name.isBlank()) site.setName(name); - if (baseUrl != null && !baseUrl.isBlank()) site.setBaseUrl(baseUrl); if (description != null) site.setDescription(description); if (siteType != null && !siteType.isBlank()) { - try { - site.setSiteType(SiteType.fromValue(siteType)); - } catch (Exception ignored) {} + try { site.setSiteType(SiteType.fromValue(siteType)); } catch (Exception ignored) {} } siteMapper.updateById(site); - log.info("站点编辑: siteId={} name={}", siteId, name); + log.info("站点编辑: siteId={}", siteId); return site; } @@ -199,6 +202,14 @@ public class SiteServiceImpl implements SiteService { @Override public void rejectSite(Long id) { - siteMapper.deleteById(id); // 物理删除 + siteMapper.deleteById(id); + } + + private static SiteStatus mapStatus(String status) { + if (status == null) return SiteStatus.ACTIVE; + return switch (status) { + case "关停", "已迁移" -> SiteStatus.DEPRECATED; + default -> SiteStatus.ACTIVE; + }; } } diff --git a/par-core/src/main/resources/db/migration/V6__site_multi_names_urls.sql b/par-core/src/main/resources/db/migration/V6__site_multi_names_urls.sql new file mode 100644 index 0000000..9e97649 --- /dev/null +++ b/par-core/src/main/resources/db/migration/V6__site_multi_names_urls.sql @@ -0,0 +1,9 @@ +-- 支持多站点名称和多地址 +ALTER TABLE sites + ADD COLUMN names TEXT COMMENT '站点名称列表(JSON数组)' AFTER name, + ADD COLUMN urls TEXT COMMENT '站点地址列表(JSON数组)' AFTER base_url, + ADD COLUMN notes TEXT COMMENT '备注/其他信息' AFTER description; + +-- 迁移现有单值数据到JSON数组 +UPDATE sites SET names = JSON_ARRAY(name) WHERE name IS NOT NULL AND name != ''; +UPDATE sites SET urls = JSON_ARRAY(base_url) WHERE base_url IS NOT NULL AND base_url != ''; diff --git a/web/src/views/Sites.vue b/web/src/views/Sites.vue index a4f63ff..9b85ec9 100644 --- a/web/src/views/Sites.vue +++ b/web/src/views/Sites.vue @@ -4,8 +4,16 @@ - - + + + + + +