From bf9ad010a1b4887733599b75ef135f7891731d66 Mon Sep 17 00:00:00 2001 From: mediabot-pt <295750538+mediabot-pt@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:01:49 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=80=82=E9=85=8D=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E6=94=B9=E4=B8=BA=E7=AB=99=E7=82=B9=E6=B1=87=E6=80=BB?= =?UTF-8?q?+=E9=80=82=E9=85=8D=E5=99=A8=E9=A1=B5=E9=9D=A2=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E6=A0=A1=E9=AA=8C=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AccessResultController.java | 28 +- .../par/core/dto/AccessPageSummaryDTO.java | 18 ++ .../par/core/dto/SiteAccessSummaryDTO.java | 21 ++ .../par/core/mapper/AccessResultMapper.java | 19 ++ .../par/core/service/AccessResultService.java | 11 + .../service/impl/AccessResultServiceImpl.java | 76 ++++- web/src/api/index.js | 2 + web/src/router/index.js | 6 + web/src/views/AccessResultCheck.vue | 299 ++++++++++++++++++ web/src/views/AccessResults.vue | 222 ++----------- 10 files changed, 499 insertions(+), 203 deletions(-) create mode 100644 par-core/src/main/java/com/par/core/dto/AccessPageSummaryDTO.java create mode 100644 par-core/src/main/java/com/par/core/dto/SiteAccessSummaryDTO.java create mode 100644 web/src/views/AccessResultCheck.vue diff --git a/par-api/src/main/java/com/par/api/controller/AccessResultController.java b/par-api/src/main/java/com/par/api/controller/AccessResultController.java index f6ee7e9..67a3167 100644 --- a/par-api/src/main/java/com/par/api/controller/AccessResultController.java +++ b/par-api/src/main/java/com/par/api/controller/AccessResultController.java @@ -2,6 +2,7 @@ package com.par.api.controller; import com.par.core.dto.AccessResultDTO; import com.par.core.dto.ApiResponse; +import com.par.core.dto.SiteAccessSummaryDTO; import com.par.core.entity.AccessResult; import com.par.core.service.AccessResultService; import jakarta.servlet.http.HttpServletRequest; @@ -57,6 +58,14 @@ public class AccessResultController { // ==================== 管理员查看接口(JWT 鉴权) ==================== + /** + * 获取所有站点访问结果汇总(按URL分组,用于适配校验列表页) + */ + @GetMapping("/api/v1/admin/access-results/summary") + public ApiResponse> listSiteSummaries() { + return ApiResponse.success(accessResultService.listSiteSummaries()); + } + /** * 获取所有访问结果列表 */ @@ -86,7 +95,7 @@ public class AccessResultController { } /** - * 管理员验证适配结果 + * 管理员验证适配结果(单个) */ @PostMapping("/api/v1/admin/access-results/{id}/validate") public ApiResponse validate( @@ -107,4 +116,21 @@ public class AccessResultController { return ApiResponse.error(404, e.getMessage()); } } + + /** + * 批量验证:标记站点某个URL的所有未验证结果 + */ + @PostMapping("/api/v1/admin/access-results/batch-validate") + public ApiResponse> batchValidateUrl(@RequestBody Map payload) { + String siteId = (String) payload.get("siteId"); + String url = (String) payload.get("url"); + Boolean isValid = payload.get("isValid") instanceof Boolean b ? b : null; + + if (siteId == null || url == null || isValid == null) { + return ApiResponse.error(400, "siteId, url, isValid are required"); + } + + int affected = accessResultService.batchValidateUrl(siteId, url, isValid); + return ApiResponse.success(Map.of("affected", affected)); + } } diff --git a/par-core/src/main/java/com/par/core/dto/AccessPageSummaryDTO.java b/par-core/src/main/java/com/par/core/dto/AccessPageSummaryDTO.java new file mode 100644 index 0000000..e37764c --- /dev/null +++ b/par-core/src/main/java/com/par/core/dto/AccessPageSummaryDTO.java @@ -0,0 +1,18 @@ +package com.par.core.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 单个页面的访问结果汇总 + */ +@Data +public class AccessPageSummaryDTO { + + private String url; + private int count; + private int validCount; + private int invalidCount; + private LocalDateTime lastSubmitted; +} diff --git a/par-core/src/main/java/com/par/core/dto/SiteAccessSummaryDTO.java b/par-core/src/main/java/com/par/core/dto/SiteAccessSummaryDTO.java new file mode 100644 index 0000000..993511b --- /dev/null +++ b/par-core/src/main/java/com/par/core/dto/SiteAccessSummaryDTO.java @@ -0,0 +1,21 @@ +package com.par.core.dto; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 站点访问结果汇总(按页面分组) + */ +@Data +public class SiteAccessSummaryDTO { + + private String siteId; + private int totalResults; + private int validCount; + private int invalidCount; + private int uncheckedCount; + private LocalDateTime lastSubmitted; + private List pages; +} diff --git a/par-core/src/main/java/com/par/core/mapper/AccessResultMapper.java b/par-core/src/main/java/com/par/core/mapper/AccessResultMapper.java index 5511a63..6136200 100644 --- a/par-core/src/main/java/com/par/core/mapper/AccessResultMapper.java +++ b/par-core/src/main/java/com/par/core/mapper/AccessResultMapper.java @@ -5,8 +5,10 @@ import com.par.core.entity.AccessResult; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; import java.util.List; +import java.util.Map; /** * 站点访问结果 Mapper @@ -19,4 +21,21 @@ public interface AccessResultMapper extends BaseMapper { @Select("SELECT * FROM access_results ORDER BY created_at DESC") List selectAll(); + + /** + * 按站点+页面聚合统计访问结果 + */ + @Select("SELECT site_id, url, " + + "COUNT(*) AS cnt, " + + "SUM(CASE WHEN is_valid = 1 THEN 1 ELSE 0 END) AS valid_cnt, " + + "SUM(CASE WHEN is_valid = 0 THEN 1 ELSE 0 END) AS invalid_cnt, " + + "MAX(created_at) AS last_at " + + "FROM access_results GROUP BY site_id, url ORDER BY site_id, cnt DESC") + List> selectPageSummary(); + + /** + * 批量标记站点某个URL的所有结果为有效/无效 + */ + @Update("UPDATE access_results SET is_valid = #{isValid} WHERE site_id = #{siteId} AND url = #{url} AND is_valid IS NULL") + int batchValidateUrl(@Param("siteId") String siteId, @Param("url") String url, @Param("isValid") Boolean isValid); } diff --git a/par-core/src/main/java/com/par/core/service/AccessResultService.java b/par-core/src/main/java/com/par/core/service/AccessResultService.java index 305dddf..cbd69f9 100644 --- a/par-core/src/main/java/com/par/core/service/AccessResultService.java +++ b/par-core/src/main/java/com/par/core/service/AccessResultService.java @@ -1,6 +1,7 @@ package com.par.core.service; import com.par.core.dto.AccessResultDTO; +import com.par.core.dto.SiteAccessSummaryDTO; import com.par.core.entity.AccessResult; import java.util.List; @@ -37,6 +38,16 @@ public interface AccessResultService { */ AccessResult validate(Long id, Boolean isValid, String note); + /** + * 获取所有站点的访问结果汇总(按URL分组统计) + */ + List listSiteSummaries(); + + /** + * 批量标记站点某个URL的所有未验证结果为有效/无效 + */ + int batchValidateUrl(String siteId, String url, Boolean isValid); + /** * 实体转 DTO(不含内容) */ diff --git a/par-core/src/main/java/com/par/core/service/impl/AccessResultServiceImpl.java b/par-core/src/main/java/com/par/core/service/impl/AccessResultServiceImpl.java index 13c227f..2b93e2a 100644 --- a/par-core/src/main/java/com/par/core/service/impl/AccessResultServiceImpl.java +++ b/par-core/src/main/java/com/par/core/service/impl/AccessResultServiceImpl.java @@ -1,6 +1,8 @@ package com.par.core.service.impl; +import com.par.core.dto.AccessPageSummaryDTO; import com.par.core.dto.AccessResultDTO; +import com.par.core.dto.SiteAccessSummaryDTO; import com.par.core.entity.AccessResult; import com.par.core.mapper.AccessResultMapper; import com.par.core.service.AccessResultService; @@ -9,8 +11,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.util.List; -import java.util.UUID; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.*; import java.util.stream.Collectors; @Slf4j @@ -103,4 +106,73 @@ public class AccessResultServiceImpl implements AccessResultService { dto.setCreatedAt(entity.getCreatedAt()); return dto; } + + // ==================== 站点汇总 ==================== + + @Override + public List listSiteSummaries() { + List> rows = mapper.selectPageSummary(); + // 按 siteId 分组聚合 + Map siteMap = new LinkedHashMap<>(); + for (Map row : rows) { + String siteId = (String) row.get("site_id"); + String url = (String) row.get("url"); + int cnt = toInt(row.get("cnt")); + int validCnt = toInt(row.get("valid_cnt")); + int invalidCnt = toInt(row.get("invalid_cnt")); + LocalDateTime lastAt = toLocalDateTime(row.get("last_at")); + + SiteAccessSummaryDTO site = siteMap.computeIfAbsent(siteId, k -> { + SiteAccessSummaryDTO s = new SiteAccessSummaryDTO(); + s.setSiteId(k); + s.setPages(new ArrayList<>()); + s.setTotalResults(0); + s.setValidCount(0); + s.setInvalidCount(0); + s.setUncheckedCount(0); + return s; + }); + + site.setTotalResults(site.getTotalResults() + cnt); + site.setValidCount(site.getValidCount() + validCnt); + site.setInvalidCount(site.getInvalidCount() + invalidCnt); + if (site.getLastSubmitted() == null || (lastAt != null && lastAt.isAfter(site.getLastSubmitted()))) { + site.setLastSubmitted(lastAt); + } + + AccessPageSummaryDTO page = new AccessPageSummaryDTO(); + page.setUrl(url); + page.setCount(cnt); + page.setValidCount(validCnt); + page.setInvalidCount(invalidCnt); + page.setLastSubmitted(lastAt); + site.getPages().add(page); + } + + // 计算未验证数量 + for (SiteAccessSummaryDTO site : siteMap.values()) { + site.setUncheckedCount(site.getTotalResults() - site.getValidCount() - site.getInvalidCount()); + } + + return new ArrayList<>(siteMap.values()); + } + + @Override + public int batchValidateUrl(String siteId, String url, Boolean isValid) { + int affected = mapper.batchValidateUrl(siteId, url, isValid); + log.info("Batch validated: siteId={}, url={}, isValid={}, affected={}", siteId, url, isValid, affected); + return affected; + } + + private int toInt(Object val) { + if (val instanceof Number n) return n.intValue(); + if (val instanceof String s) return Integer.parseInt(s); + return 0; + } + + private LocalDateTime toLocalDateTime(Object val) { + if (val instanceof LocalDateTime ldt) return ldt; + if (val instanceof Timestamp ts) return ts.toLocalDateTime(); + return null; + } } diff --git a/web/src/api/index.js b/web/src/api/index.js index 523a3ef..9c5dad0 100644 --- a/web/src/api/index.js +++ b/web/src/api/index.js @@ -67,10 +67,12 @@ export default { pathStats: () => api.get('/admin/logs/stats/path'), }, accessResults: { + summary: () => api.get('/admin/access-results/summary'), list: () => api.get('/admin/access-results'), bySite: (siteId) => api.get(`/admin/access-results/by-site/${siteId}`), get: (id) => api.get(`/admin/access-results/${id}`), validate: (id, isValid, note) => api.post(`/admin/access-results/${id}/validate`, { isValid, note: note || '' }), + batchValidate: (siteId, url, isValid) => api.post('/admin/access-results/batch-validate', { siteId, url, isValid }), }, health: () => api.get('/health', { baseURL: '' }) } diff --git a/web/src/router/index.js b/web/src/router/index.js index 152e4fa..2166ed5 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -59,6 +59,12 @@ const routes = [ name: 'AccessResults', component: () => import('../views/AccessResults.vue'), meta: { title: '适配验证' } + }, + { + path: 'access-results/check/:siteId', + name: 'AccessResultCheck', + component: () => import('../views/AccessResultCheck.vue'), + meta: { title: '适配校验详情' } } ] } diff --git a/web/src/views/AccessResultCheck.vue b/web/src/views/AccessResultCheck.vue new file mode 100644 index 0000000..1d79a94 --- /dev/null +++ b/web/src/views/AccessResultCheck.vue @@ -0,0 +1,299 @@ + + + + + diff --git a/web/src/views/AccessResults.vue b/web/src/views/AccessResults.vue index 4836de8..6a40dda 100644 --- a/web/src/views/AccessResults.vue +++ b/web/src/views/AccessResults.vue @@ -2,101 +2,45 @@

适配验证

-
-
- - - - - - - - -
+
刷新
- - + - + - + - + - + - + - - - - - - - - - - - - {{ detail.siteId }} - {{ detail.configVersion || '-' }} - {{ detail.clientId || '-' }} - {{ detail.contentType }} - {{ detail.url || '-' }} - - {{ detail.statusCode }} - - {{ detail.responseTimeMs != null ? detail.responseTimeMs + 'ms' : '-' }} - - 有效 - 无效 - 未验证 - - {{ detail.validationNote || '-' }} - {{ detail.createdAt }} - - -
返回内容:
-

-      

-
-      
-    
@@ -105,138 +49,16 @@ import api from '../api' import { Refresh } from '@element-plus/icons-vue' const loading = ref(false) -const list = ref([]) -const filterSiteId = ref('') -const filterValid = ref('') -const siteOptions = ref([]) - -const detailVisible = ref(false) -const detail = ref({}) - -const filteredList = computed(() => { - let items = list.value - if (filterSiteId.value) { - items = items.filter(r => r.siteId === filterSiteId.value) - } - if (filterValid.value === '1') { - items = items.filter(r => r.isValid === true) - } else if (filterValid.value === '0') { - items = items.filter(r => r.isValid === false) - } else if (filterValid.value === 'null') { - items = items.filter(r => r.isValid == null) - } - return items -}) - -const highlightedJson = computed(() => { - try { - const json = typeof detail.value.content === 'string' ? JSON.parse(detail.value.content) : detail.value.content - return syntaxHighlight(JSON.stringify(json, null, 2)) - } catch { - return escapeHtml(detail.value.content || '') - } -}) - -const highlightedHtml = computed(() => { - return escapeHtml(detail.value.content || '') - .replace(/(<\/?)(\w+)([\s\S]*?)(\/?>)/g, - '$1$2$3$4') - .replace(/(\w+)=(".*?")/g, - '$1=$2') -}) +const sites = ref([]) const fetchAll = async () => { loading.value = true try { - const r = await api.accessResults.list() - list.value = r.data || [] - // 提取去重的站点列表用于筛选 - const siteIds = [...new Set(list.value.map(r => r.siteId))] - siteOptions.value = siteIds.map(id => ({ siteId: id })) - } catch { ElMessage.error('获取访问结果失败') } + const r = await api.accessResults.summary() + sites.value = r.data || [] + } catch { ElMessage.error('获取汇总失败') } finally { loading.value = false } } -const handleView = async (row) => { - // 详情需从服务端获取完整内容 - detailVisible.value = true - try { - const r = await api.accessResults.get(row.id) - detail.value = r.data || {} - } catch { - ElMessage.error('获取详情失败') - detail.value = { ...row } - } -} - -const handleValidate = async (row, isValid) => { - try { - await api.accessResults.validate(row.id, isValid) - ElMessage.success(`已标记为${isValid ? '有效' : '无效'}`) - fetchAll() - } catch { ElMessage.error('操作失败') } -} - -const statusTag = (code) => { - if (code >= 200 && code < 300) return 'success' - if (code >= 300 && code < 400) return 'warning' - return 'danger' -} - -const escapeHtml = (str) => { - if (!str) return '' - return str.replace(/&/g, '&').replace(//g, '>') -} - -const syntaxHighlight = (json) => escapeHtml(json).replace( - /("(\\u[a-fA-F0-9]{4}|\\[^u]|[^"\\])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, - (match) => { - let cls = 'number' - if (/^"/.test(match)) cls = /:$/.test(match) ? 'key' : 'string' - else if (/true|false/.test(match)) cls = 'boolean' - else if (/null/.test(match)) cls = 'null' - return `${match}` - } -) - onMounted(fetchAll) - -