feat: Groovy 脚本解析引擎,脚本与配置一同管理,提交时支持上传 script 字段
This commit is contained in:
@@ -102,5 +102,11 @@
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Groovy 脚本引擎 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.groovy</groupId>
|
||||
<artifactId>groovy</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.par.core.adapter;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import groovy.lang.Binding;
|
||||
import groovy.lang.GroovyShell;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Groovy 脚本解析引擎。
|
||||
* 脚本可访问以下内置变量:
|
||||
* content - String,页面内容(HTML 或 JSON)
|
||||
* url - String,访问的 URL
|
||||
* jsoup - 可直接调用 Jsoup.parse() 等
|
||||
* json - ObjectMapper,可直接调用 readTree() 等
|
||||
* utils - 工具方法集合
|
||||
* 脚本应返回 Map<String, Object> 或 List<Map<String, Object>>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class GroovyScriptEngine {
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> execute(String groovyScript, String content, String url) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("engine", "groovy");
|
||||
try {
|
||||
Binding binding = new Binding();
|
||||
binding.setVariable("content", content);
|
||||
binding.setVariable("url", url);
|
||||
binding.setVariable("jsoup", new JsoupHelper());
|
||||
binding.setVariable("json", objectMapper);
|
||||
binding.setVariable("utils", new ScriptUtils());
|
||||
|
||||
GroovyShell shell = new GroovyShell(binding);
|
||||
Object scriptResult = shell.evaluate(groovyScript);
|
||||
|
||||
if (scriptResult instanceof Map) {
|
||||
result.put("matched", true);
|
||||
Map<String, Object> m = (Map<String, Object>) scriptResult;
|
||||
result.put("pageName", m.getOrDefault("pageName", "Groovy脚本"));
|
||||
result.put("fieldCount", m.containsKey("fields") ? ((Map<?, ?>) m.get("fields")).size() : 0);
|
||||
// 有 fields map → 展示为一行
|
||||
if (m.containsKey("fields")) {
|
||||
result.put("extracted", List.of(m.get("fields")));
|
||||
result.put("rowCount", 1);
|
||||
} else if (m.containsKey("extracted")) {
|
||||
result.put("extracted", m.get("extracted"));
|
||||
result.put("rowCount", m.getOrDefault("rowCount", ((List<?>) m.get("extracted")).size()));
|
||||
} else {
|
||||
result.put("extracted", List.of(m));
|
||||
result.put("rowCount", 1);
|
||||
}
|
||||
result.put("message", m.getOrDefault("message", "解析成功"));
|
||||
} else if (scriptResult instanceof List) {
|
||||
result.put("matched", true);
|
||||
result.put("pageName", "Groovy脚本");
|
||||
result.put("extracted", (List<?>) scriptResult);
|
||||
result.put("rowCount", ((List<?>) scriptResult).size());
|
||||
result.put("fieldCount", !((List<?>) scriptResult).isEmpty() && ((List<?>) scriptResult).get(0) instanceof Map
|
||||
? ((Map<?, ?>) ((List<?>) scriptResult).get(0)).size() : 0);
|
||||
result.put("message", "共 " + ((List<?>) scriptResult).size() + " 行");
|
||||
} else if (scriptResult != null) {
|
||||
result.put("matched", true);
|
||||
result.put("pageName", "Groovy脚本");
|
||||
result.put("extracted", List.of(Map.of("result", scriptResult)));
|
||||
result.put("rowCount", 1);
|
||||
result.put("message", String.valueOf(scriptResult));
|
||||
} else {
|
||||
result.put("matched", true);
|
||||
result.put("message", "脚本返回 null");
|
||||
result.put("extracted", List.of());
|
||||
result.put("rowCount", 0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
result.put("matched", false);
|
||||
result.put("message", "Groovy 脚本执行异常: " + e.getMessage());
|
||||
log.error("Groovy script error for url={}", url, e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Jsoup 辅助类 */
|
||||
public static class JsoupHelper {
|
||||
public Object parse(String html) { return Jsoup.parse(html); }
|
||||
public Object parse(String html, String baseUri) { return Jsoup.parse(html, baseUri); }
|
||||
}
|
||||
|
||||
/** 脚本工具方法 */
|
||||
public static class ScriptUtils {
|
||||
public String text(Object node) {
|
||||
try { return (String) node.getClass().getMethod("text").invoke(node); }
|
||||
catch (Exception e) { return null; }
|
||||
}
|
||||
public String attr(Object node, String attr) {
|
||||
try { return (String) node.getClass().getMethod("attr", String.class).invoke(node, attr); }
|
||||
catch (Exception e) { return null; }
|
||||
}
|
||||
public Object select(Object node, String cssQuery) {
|
||||
try { return node.getClass().getMethod("select", String.class).invoke(node, cssQuery); }
|
||||
catch (Exception e) { return null; }
|
||||
}
|
||||
public Object selectFirst(Object node, String cssQuery) {
|
||||
try {
|
||||
Object elements = select(node, cssQuery);
|
||||
if (elements instanceof List && !((List<?>) elements).isEmpty())
|
||||
return ((List<?>) elements).get(0);
|
||||
return null;
|
||||
} catch (Exception e) { return null; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,9 @@ public class SiteConfig {
|
||||
@TableField("storage_path")
|
||||
private String storagePath;
|
||||
|
||||
@TableField("script_storage_path")
|
||||
private String scriptStoragePath;
|
||||
|
||||
@TableField("file_size")
|
||||
private Long fileSize;
|
||||
|
||||
|
||||
@@ -31,10 +31,10 @@ public interface ConfigService {
|
||||
SiteConfig getConfig(String siteId, String version);
|
||||
|
||||
/**
|
||||
* 提交新配置(需审核)
|
||||
* 提交新配置(需审核),可选 Groovy 脚本
|
||||
*/
|
||||
SiteConfig submitConfig(String siteId, String version, String configJson,
|
||||
String schemaVersion, Long submitterId);
|
||||
String schemaVersion, String groovyScript, Long submitterId);
|
||||
|
||||
/**
|
||||
* 转换为 DTO
|
||||
|
||||
@@ -55,7 +55,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
|
||||
@Override
|
||||
public SiteConfig submitConfig(String siteId, String version, String configJson,
|
||||
String schemaVersion, Long submitterId) {
|
||||
String schemaVersion, String groovyScript, Long submitterId) {
|
||||
String configHash = HmacUtil.sha256(configJson);
|
||||
String storagePath = String.format("configs/%s/pending/%s.json", siteId, configHash.substring(0, 8));
|
||||
|
||||
@@ -70,7 +70,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
return sameHash;
|
||||
}
|
||||
|
||||
// 存储到抽象存储层
|
||||
// 存储配置
|
||||
try {
|
||||
storageService.store(storagePath, configJson);
|
||||
} catch (Exception e) {
|
||||
@@ -78,11 +78,19 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
throw new RuntimeException("存储配置文件失败: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 存储 Groovy 脚本(可选)
|
||||
String scriptPath = null;
|
||||
if (groovyScript != null && !groovyScript.isBlank()) {
|
||||
scriptPath = String.format("configs/%s/pending/%s.groovy", siteId, configHash.substring(0, 8));
|
||||
storageService.store(scriptPath, groovyScript);
|
||||
}
|
||||
|
||||
SiteConfig config = new SiteConfig();
|
||||
config.setSiteId(siteId);
|
||||
config.setVersion(version); // 客户端传入的版本,仅供参考;批准时会被自动分配的新版本号覆盖
|
||||
config.setVersion(version);
|
||||
config.setConfigHash(configHash);
|
||||
config.setStoragePath(storagePath);
|
||||
config.setScriptStoragePath(scriptPath);
|
||||
config.setFileSize((long) configJson.getBytes(StandardCharsets.UTF_8).length);
|
||||
config.setSchemaVersion(schemaVersion != null ? schemaVersion : "1.0.0");
|
||||
config.setStatus(ConfigStatus.PENDING);
|
||||
@@ -90,7 +98,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
config.setIsLatest(false);
|
||||
|
||||
siteConfigMapper.insert(config);
|
||||
log.info("Config submitted: siteId={}, version={}, id={}, hash={}", siteId, version, config.getId(), configHash);
|
||||
log.info("Config submitted: siteId={}, version={}, id={}, hash={}, hasScript={}", siteId, version, config.getId(), configHash, scriptPath != null);
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- PAR Flyway V14: site_configs 增加 script_storage_path 字段存储 Groovy 脚本路径
|
||||
ALTER TABLE site_configs ADD COLUMN script_storage_path VARCHAR(500) COMMENT 'Groovy 脚本存储路径(七牛云/本地)';
|
||||
Reference in New Issue
Block a user