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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user