feat: 页面配置入口地址添加验证按钮,解析失败自动提交错误日志

- 后端新增 POST /api/v1/admin/site-pages/verify 端点
  - 接收 siteId/url/pageKey,用对应 Groovy 脚本访问URL并解析
  - 解析失败(matched=false)时自动提交 error-log 到 PAR
  - 支持 HTTP_ERROR/EMPTY_RESPONSE/PARSE_FAILED/SCRIPT_ERROR 多种错误类型
- 前端 SitePages.vue 每条页面入口 URL 后添加验证按钮
  - 成功显示绿色 ✓,失败显示红色 ✗,并展示解析结果消息
  - 显示响应耗时
- api/index.js 新增 adapter.verifyPage 方法
This commit is contained in:
mediabot-pt
2026-07-06 13:39:32 +08:00
parent 355f586086
commit 8724774850
3 changed files with 183 additions and 1 deletions

View File

@@ -1,12 +1,18 @@
package com.par.api.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.par.core.adapter.GroovyScriptEngine;
import com.par.core.adapter.HttpUtils;
import com.par.core.dto.ApiResponse;
import com.par.core.mapper.SiteConfigMapper;
import com.par.core.service.ErrorLogService;
import com.par.core.service.SitePagesService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -18,6 +24,10 @@ public class SitePagesController {
private final SitePagesService sitePagesService;
private final com.par.core.mapper.SiteMapper siteMapper;
private final com.par.core.storage.CachedStorageService storageService;
private final GroovyScriptEngine groovyEngine;
private final SiteConfigMapper siteConfigMapper;
private final ErrorLogService errorLogService;
private static final ObjectMapper objectMapper = new ObjectMapper();
@GetMapping("/api/v1/sites/{siteId}/pages")
public ApiResponse<Map<String, Object>> getPages(@PathVariable String siteId) {
@@ -66,6 +76,138 @@ public class SitePagesController {
return ApiResponse.success();
}
/**
* 验证页面入口:用脚本访问指定URL并解析,解析失败时自动提交错误日志
*/
@PostMapping("/api/v1/admin/site-pages/verify")
public ApiResponse<Map<String, Object>> verifyPageEntry(@RequestBody Map<String, Object> payload,
HttpServletRequest request) {
checkAdmin(request);
String siteId = (String) payload.get("siteId");
String url = (String) payload.get("url");
String pageKey = (String) payload.get("pageKey");
if (siteId == null || siteId.isBlank()) return ApiResponse.error(400, "siteId is required");
if (url == null || url.isBlank()) return ApiResponse.error(400, "url is required");
Map<String, Object> result = new LinkedHashMap<>();
result.put("siteId", siteId);
result.put("url", url);
result.put("pageKey", pageKey != null ? pageKey : "");
// 1. 获取站点类型,加载对应 Groovy 脚本
var site = siteMapper.selectBySiteId(siteId);
String siteType = site != null && site.getSiteType() != null ? site.getSiteType().getValue() : null;
if (siteType == null) siteType = "nexusphp";
// 优先用站点自定义脚本,否则用类型默认脚本
String script = null;
String scriptSource = "default";
var latestConfig = siteConfigMapper.selectLatestBySiteId(siteId);
if (latestConfig != null && latestConfig.getScriptStoragePath() != null
&& !latestConfig.getScriptStoragePath().isBlank()) {
try {
script = storageService.read(latestConfig.getScriptStoragePath());
if (script != null && !script.isBlank()) scriptSource = "custom";
} catch (Exception e) {
log.warn("读取自定义脚本失败 siteId={}: {}", siteId, e.getMessage());
}
}
if (script == null || script.isBlank()) {
script = GroovyScriptEngine.loadDefaultByType(siteType);
scriptSource = "default";
}
if (script == null || script.isBlank()) {
result.put("success", false);
result.put("message", "未找到可用脚本(站点类型: " + siteType + ")");
return ApiResponse.success(result);
}
result.put("scriptSource", scriptSource);
// 2. 用 HttpUtils 访问目标 URL 获取 HTML 内容
String content;
long startMs = System.currentTimeMillis();
try {
content = HttpUtils.INSTANCE.get(url, null);
result.put("responseTimeMs", System.currentTimeMillis() - startMs);
if (content == null || content.isBlank()) {
result.put("success", false);
result.put("message", "页面返回空内容");
submitErrorLog(siteId, request, "EMPTY_RESPONSE", "页面返回空内容", null, url);
return ApiResponse.success(result);
}
result.put("contentLength", content.length());
} catch (Exception e) {
result.put("responseTimeMs", System.currentTimeMillis() - startMs);
result.put("success", false);
String errMsg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
result.put("message", "HTTP 请求失败: " + errMsg);
submitErrorLog(siteId, request, "HTTP_ERROR", errMsg, null, url);
return ApiResponse.success(result);
}
// 3. 用 Groovy 脚本解析内容
try {
Map<String, Object> parsed = groovyEngine.execute(script, content, url);
boolean matched = Boolean.TRUE.equals(parsed.get("matched"));
result.put("parsed", parsed);
result.put("matched", matched);
result.put("pageName", parsed.getOrDefault("pageName", ""));
result.put("rowCount", parsed.getOrDefault("rowCount", 0));
result.put("fieldCount", parsed.getOrDefault("fieldCount", 0));
if (matched) {
result.put("success", true);
result.put("message", "解析成功: " + parsed.getOrDefault("message", ""));
} else {
result.put("success", false);
String msg = parsed.get("message") != null ? parsed.get("message").toString() : "脚本返回 matched=false";
result.put("message", "解析失败: " + msg);
// 提交错误日志
submitErrorLog(siteId, request, "PARSE_FAILED", msg, null, url);
}
} catch (Exception e) {
result.put("success", false);
String errMsg = extractError(e);
result.put("message", "脚本执行异常: " + errMsg);
result.put("matched", false);
submitErrorLog(siteId, request, "SCRIPT_ERROR", errMsg, null, url);
}
return ApiResponse.success(result);
}
private void submitErrorLog(String siteId, HttpServletRequest request,
String errorType, String message, String stackTrace, String url) {
try {
String clientId = (String) request.getAttribute("clientId");
if (clientId == null || clientId.isBlank()) {
// 从 Bearer token 中提取 email 作为 clientId
Object accountId = request.getAttribute("accountId");
clientId = accountId != null ? "admin-" + accountId : "web-admin";
}
errorLogService.submit(siteId, clientId, errorType, message, stackTrace, null, url);
log.info("验证失败自动提交错误日志: siteId={}, errorType={}, url={}", siteId, errorType, url);
} catch (Exception e) {
log.warn("提交错误日志失败: {}", e.getMessage());
}
}
private String extractError(Exception e) {
StringBuilder sb = new StringBuilder();
sb.append(e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName());
for (StackTraceElement ste : e.getStackTrace()) {
String cn = ste.getClassName();
if (cn.startsWith("Script") || cn.contains("$_run_closure") || cn.contains("$__execute")) {
sb.append("\n at line ").append(ste.getLineNumber());
break;
}
}
if (e.getCause() != null && e.getCause() != e) {
sb.append("\nCaused by: ").append(e.getCause().getMessage());
}
return sb.toString();
}
@GetMapping("/api/v1/admin/site-pages")
public ApiResponse<List<Map<String, Object>>> adminList(HttpServletRequest request) {
checkAdmin(request);