parent
0bebfcc342
commit
59b54db9f6
11 changed files with 944 additions and 0 deletions
@ -0,0 +1,37 @@ |
||||
package org.springblade.lims.entry; |
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField; |
||||
import com.baomidou.mybatisplus.annotation.TableName; |
||||
import lombok.Data; |
||||
import org.springblade.core.mp.base.BaseEntity; |
||||
|
||||
import java.io.Serializable; |
||||
|
||||
/** |
||||
* 试剂公式 |
||||
* |
||||
* @author blade |
||||
* @since 2026-05-27 |
||||
*/ |
||||
@Data |
||||
@SuppressWarnings("all") |
||||
@TableName("f_reagent_formula") |
||||
public class ReagentFormula extends BaseEntity implements Serializable { |
||||
|
||||
private Long id; |
||||
|
||||
private Long reagentId; |
||||
|
||||
private String name; |
||||
|
||||
private String expression; |
||||
|
||||
private String description; |
||||
|
||||
private Integer enabled; |
||||
|
||||
@TableField(exist = false) |
||||
private String reagentName; |
||||
|
||||
private static final long serialVersionUID = 1L; |
||||
} |
||||
@ -0,0 +1,255 @@ |
||||
package org.springblade.lims.controller; |
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
import com.baomidou.mybatisplus.core.metadata.IPage; |
||||
import io.swagger.annotations.Api; |
||||
import io.swagger.annotations.ApiOperation; |
||||
import io.swagger.annotations.ApiParam; |
||||
import lombok.AllArgsConstructor; |
||||
import org.springblade.core.boot.ctrl.BladeController; |
||||
import org.springblade.core.mp.support.Condition; |
||||
import org.springblade.core.mp.support.Query; |
||||
import org.springblade.core.tool.api.R; |
||||
import org.springblade.core.tool.utils.Func; |
||||
import org.springblade.lims.entry.Reagent; |
||||
import org.springblade.lims.entry.ReagentFormula; |
||||
import org.springblade.lims.service.IReagentFormulaService; |
||||
import org.springblade.lims.service.IReagentService; |
||||
import org.springblade.lims.utils.FormulaValidationTool; |
||||
import org.springframework.web.bind.annotation.*; |
||||
|
||||
import java.util.*; |
||||
import java.util.regex.Matcher; |
||||
import java.util.regex.Pattern; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import static java.util.stream.Collectors.toMap; |
||||
|
||||
/** |
||||
* 试剂公式维护控制器 |
||||
* |
||||
* @author blade |
||||
* @since 2026-05-27 |
||||
*/ |
||||
@RestController |
||||
@AllArgsConstructor |
||||
@RequestMapping("/reagentFormula") |
||||
@Api(value = "试剂公式维护", tags = "试剂公式维护") |
||||
public class ReagentFormulaController extends BladeController { |
||||
|
||||
private final IReagentFormulaService service; |
||||
private final IReagentService reagentService; |
||||
|
||||
/** |
||||
* 分页查询 |
||||
*/ |
||||
@GetMapping("/list") |
||||
@ApiOperation(value = "分页查询", notes = "分页查询试剂公式") |
||||
public R<IPage<ReagentFormula>> list(ReagentFormula entry, Query query) { |
||||
LambdaQueryWrapper<ReagentFormula> wrapper = new LambdaQueryWrapper<>(); |
||||
if (entry.getName() != null && !entry.getName().isEmpty()) { |
||||
wrapper.like(ReagentFormula::getName, entry.getName()); |
||||
} |
||||
wrapper.orderByDesc(ReagentFormula::getCreateTime); |
||||
IPage<ReagentFormula> page = service.page(Condition.getPage(query), wrapper); |
||||
|
||||
// populate reagent names
|
||||
List<ReagentFormula> records = page.getRecords(); |
||||
if (records != null && !records.isEmpty()) { |
||||
Set<Long> reagentIds = records.stream() |
||||
.map(ReagentFormula::getReagentId) |
||||
.filter(Objects::nonNull) |
||||
.collect(Collectors.toSet()); |
||||
if (!reagentIds.isEmpty()) { |
||||
Map<Long, String> nameMap = reagentService.listByIds(reagentIds).stream() |
||||
.collect(toMap(Reagent::getId, Reagent::getName, (a, b) -> a)); |
||||
records.forEach(f -> f.setReagentName(nameMap.get(f.getReagentId()))); |
||||
} |
||||
} |
||||
|
||||
return R.data(page); |
||||
} |
||||
|
||||
/** |
||||
* 新增 |
||||
*/ |
||||
@PostMapping("/insert") |
||||
@ApiOperation(value = "新增", notes = "新增试剂公式") |
||||
public R insert(@RequestBody ReagentFormula entry) { |
||||
entry.setCreateTime(new Date()); |
||||
entry.setUpdateTime(new Date()); |
||||
return R.data(service.save(entry)); |
||||
} |
||||
|
||||
/** |
||||
* 修改 |
||||
*/ |
||||
@PostMapping("/update") |
||||
@ApiOperation(value = "修改", notes = "修改试剂公式") |
||||
public R update(@RequestBody ReagentFormula entry) { |
||||
entry.setUpdateTime(new Date()); |
||||
return R.data(service.updateById(entry)); |
||||
} |
||||
|
||||
/** |
||||
* 逻辑删除 |
||||
*/ |
||||
@PostMapping("/deleteByIds") |
||||
@ApiOperation(value = "逻辑删除", notes = "传入ids逻辑删除") |
||||
public R deleteByIds(@ApiParam(value = "主键集合", required = true) @RequestParam String ids) { |
||||
return R.status(service.deleteLogic(Func.toLongList(ids))); |
||||
} |
||||
|
||||
/** |
||||
* 公式校验 |
||||
*/ |
||||
@PostMapping("/validate") |
||||
@ApiOperation(value = "公式校验", notes = "使用Aviator引擎校验公式语法和安全") |
||||
public R validateFormula(@RequestBody Map<String, String> body) { |
||||
String expression = body.get("expression"); |
||||
if (expression == null || expression.trim().isEmpty()) { |
||||
return R.fail("公式表达式不能为空"); |
||||
} |
||||
FormulaValidationTool.ValidationResult result = FormulaValidationTool.validateSafe(expression); |
||||
Map<String, Object> data = new HashMap<>(3); |
||||
data.put("valid", result.isValid()); |
||||
data.put("message", result.getMessage()); |
||||
data.put("variables", result.getVariables()); |
||||
return R.data(data); |
||||
} |
||||
|
||||
/** |
||||
* 获取试剂关联的可用变量 |
||||
* 从 Reagent.resultDeterminationMethod 中语义提取 |
||||
*/ |
||||
@GetMapping("/variables") |
||||
@ApiOperation(value = "获取可用变量", notes = "根据试剂ID获取公式可用变量") |
||||
public R getVariables(@ApiParam(value = "试剂ID") @RequestParam Long reagentId) { |
||||
Reagent reagent = reagentService.getById(reagentId); |
||||
if (reagent == null) { |
||||
return R.data(new ArrayList<>()); |
||||
} |
||||
|
||||
String method = reagent.getResultDeterminationMethod(); |
||||
List<Map<String, Object>> variables = extractVariables(method); |
||||
|
||||
return R.data(variables); |
||||
} |
||||
|
||||
/** |
||||
* 获取试剂列表(下拉选择用) |
||||
*/ |
||||
@GetMapping("/getReagentList") |
||||
@ApiOperation(value = "获取试剂列表", notes = "获取全部试剂用于下拉选择") |
||||
public R<List<Reagent>> getReagentList() { |
||||
LambdaQueryWrapper<Reagent> wrapper = new LambdaQueryWrapper<>(); |
||||
wrapper.eq(Reagent::getIsDeleted, 0); |
||||
wrapper.orderByAsc(Reagent::getName); |
||||
List<Reagent> list = reagentService.list(wrapper); |
||||
return R.data(list); |
||||
} |
||||
|
||||
/** |
||||
* 从结果判定方法文本中提取变量 |
||||
*/ |
||||
private List<Map<String, Object>> extractVariables(String methodText) { |
||||
List<Map<String, Object>> variables = new ArrayList<>(); |
||||
if (methodText == null || methodText.isEmpty()) { |
||||
// 返回默认的常用变量
|
||||
return getDefaultVariables(); |
||||
} |
||||
|
||||
// 尝试解析为JSON
|
||||
try { |
||||
com.alibaba.fastjson.JSONObject json = com.alibaba.fastjson.JSON.parseObject(methodText); |
||||
if (json.containsKey("variables")) { |
||||
return json.getJSONArray("variables").stream() |
||||
.map(obj -> { |
||||
com.alibaba.fastjson.JSONObject v = (com.alibaba.fastjson.JSONObject) obj; |
||||
Map<String, Object> map = new HashMap<>(3); |
||||
map.put("name", v.getString("name")); |
||||
map.put("description", v.getString("description")); |
||||
map.put("type", v.getString("type")); |
||||
return map; |
||||
}) |
||||
.collect(Collectors.toList()); |
||||
} |
||||
} catch (Exception ignored) { |
||||
// 不是JSON格式,继续文本解析
|
||||
} |
||||
|
||||
// 从文本中提取常见参数名
|
||||
Set<String> foundNames = new LinkedHashSet<>(); |
||||
for (String[] var : COMMON_METHOD_VARIABLES) { |
||||
if (methodText.contains(var[0]) || methodText.contains(var[1])) { |
||||
foundNames.add(var[0]); |
||||
} |
||||
} |
||||
|
||||
// 额外用正则匹配变量模式
|
||||
Pattern pattern = Pattern.compile("([ODSPKQ]+[值率比]?|阻断率|[^,。;:\n\r]+?值)"); |
||||
Matcher matcher = pattern.matcher(methodText); |
||||
while (matcher.find()) { |
||||
String match = matcher.group().trim(); |
||||
if (match.length() <= 15 && !match.matches(".*[0-9]{2,}.*")) { |
||||
foundNames.add(match); |
||||
} |
||||
} |
||||
|
||||
if (!foundNames.isEmpty()) { |
||||
for (String name : foundNames) { |
||||
Map<String, Object> v = new HashMap<>(3); |
||||
v.put("name", name); |
||||
v.put("description", name); |
||||
v.put("type", "number"); |
||||
variables.add(v); |
||||
} |
||||
return variables; |
||||
} |
||||
|
||||
return getDefaultVariables(); |
||||
} |
||||
|
||||
/** |
||||
* 常用方法参数对照表 |
||||
*/ |
||||
private static final String[][] COMMON_METHOD_VARIABLES = { |
||||
{"OD值", "OD450"}, |
||||
{"S_P值", "S/P"}, |
||||
{"PI值", "阻断率"}, |
||||
{"阳性数", "阳性"}, |
||||
{"检测总数", "总数"}, |
||||
{"浓度", "浓度"}, |
||||
{"稀释倍数", "稀释"}, |
||||
{"阈值", "阈值"}, |
||||
{"S_N值", "S/N"}, |
||||
{"KQ值", "KQ"}, |
||||
{"阴性对照OD", "阴性对照"}, |
||||
{"阳性对照OD", "阳性对照"}, |
||||
}; |
||||
|
||||
/** |
||||
* 默认变量列表 |
||||
*/ |
||||
private List<Map<String, Object>> getDefaultVariables() { |
||||
List<Map<String, Object>> variables = new ArrayList<>(); |
||||
String[][] defaults = { |
||||
{"OD值", "光密度值", "number"}, |
||||
{"S_P值", "S/P比值", "number"}, |
||||
{"PI值", "抑制百分比", "number"}, |
||||
{"阳性数", "阳性样品数", "number"}, |
||||
{"检测总数", "检测样品总数", "number"}, |
||||
{"浓度", "样品浓度", "number"}, |
||||
{"稀释倍数", "样品稀释倍数", "number"}, |
||||
{"阈值", "自定义阈值", "number"}, |
||||
}; |
||||
for (String[] def : defaults) { |
||||
Map<String, Object> v = new HashMap<>(3); |
||||
v.put("name", def[0]); |
||||
v.put("description", def[1]); |
||||
v.put("type", def[2]); |
||||
variables.add(v); |
||||
} |
||||
return variables; |
||||
} |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
package org.springblade.lims.mapper; |
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
||||
import org.springblade.lims.entry.ReagentFormula; |
||||
|
||||
/** |
||||
* 试剂公式 Mapper |
||||
* |
||||
* @author blade |
||||
* @since 2026-05-27 |
||||
*/ |
||||
public interface ReagentFormulaMapper extends BaseMapper<ReagentFormula> { |
||||
} |
||||
@ -0,0 +1,21 @@ |
||||
package org.springblade.lims.service; |
||||
|
||||
import org.springblade.core.mp.base.BaseService; |
||||
import org.springblade.lims.entry.ReagentFormula; |
||||
|
||||
/** |
||||
* 试剂公式 Service 接口 |
||||
* |
||||
* @author blade |
||||
* @since 2026-05-27 |
||||
*/ |
||||
public interface IReagentFormulaService extends BaseService<ReagentFormula> { |
||||
|
||||
/** |
||||
* 根据试剂ID查找已启用的公式 |
||||
* |
||||
* @param reagentId 试剂ID |
||||
* @return 启用的公式,不存在返回 null |
||||
*/ |
||||
ReagentFormula findEnabledByReagentId(Long reagentId); |
||||
} |
||||
@ -0,0 +1,35 @@ |
||||
package org.springblade.lims.service.impl; |
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
import lombok.AllArgsConstructor; |
||||
import org.springblade.core.mp.base.BaseServiceImpl; |
||||
import org.springblade.lims.entry.ReagentFormula; |
||||
import org.springblade.lims.mapper.ReagentFormulaMapper; |
||||
import org.springblade.lims.service.IReagentFormulaService; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
/** |
||||
* 试剂公式 Service 实现 |
||||
* |
||||
* @author blade |
||||
* @since 2026-05-27 |
||||
*/ |
||||
@Service |
||||
@AllArgsConstructor |
||||
public class ReagentFormulaServiceImpl extends BaseServiceImpl<ReagentFormulaMapper, ReagentFormula> implements IReagentFormulaService { |
||||
|
||||
private final ReagentFormulaMapper mapper; |
||||
|
||||
@Override |
||||
public ReagentFormula findEnabledByReagentId(Long reagentId) { |
||||
if (reagentId == null) { |
||||
return null; |
||||
} |
||||
LambdaQueryWrapper<ReagentFormula> wrapper = new LambdaQueryWrapper<>(); |
||||
wrapper.eq(ReagentFormula::getReagentId, reagentId); |
||||
wrapper.eq(ReagentFormula::getEnabled, 1); |
||||
wrapper.eq(ReagentFormula::getIsDeleted, 0); |
||||
wrapper.last("LIMIT 1"); |
||||
return mapper.selectOne(wrapper); |
||||
} |
||||
} |
||||
@ -0,0 +1,239 @@ |
||||
package org.springblade.lims.utils; |
||||
|
||||
import org.springblade.lims.excel.BiochemicalIdentificationExcel; |
||||
import org.springblade.lims.excel.ExamineTemplate2Excel; |
||||
import org.springblade.lims.excel.XN2Excel; |
||||
|
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* 数据结构适配器 |
||||
* <p> |
||||
* 将不同 parser(ptExcel、blsExcel、xnExcel、ktyExcel、shjdExcel)返回的异构数据结构 |
||||
* 统一为「遍历每个样品 → 执行回调」的模式,用于公式注入。 |
||||
* <p> |
||||
* 每种 parser 的返回值结构: |
||||
* <pre> |
||||
* ptExcel (inputMode=1,4) → Map<String, List<Map<String, Map<String, Object>>>> |
||||
* blsExcel (inputMode=2,6,7) → List<ExamineTemplate2Excel> |
||||
* ktyExcel (inputMode=3) → List<Map<String, Object>> |
||||
* xnExcel (inputMode=5) → List<XN2Excel> |
||||
* shjdExcel (inputMode=8) → List<BiochemicalIdentificationExcel> |
||||
* </pre> |
||||
* |
||||
* @author blade |
||||
* @since 2026-05-27 |
||||
*/ |
||||
public final class DataStructureAdapter { |
||||
|
||||
private DataStructureAdapter() { |
||||
} |
||||
|
||||
/** |
||||
* 样品回调接口。 |
||||
* <p> |
||||
* 实现类接收该样品的 primary value 和完整的上下文 env, |
||||
* 返回公式计算后的 result 字符串(如 "阳性"、"阴性"、"可疑")。 |
||||
*/ |
||||
@FunctionalInterface |
||||
public interface SampleCallback { |
||||
/** |
||||
* @param value 样品主值(OD、CT、titer、凝集值等) |
||||
* @param env 公式可用的变量环境,包含 value 和其他上下文 |
||||
* @return 计算结果字符串,返回 null 或空字符串则保留原值 |
||||
*/ |
||||
String apply(String value, Map<String, Object> env); |
||||
} |
||||
|
||||
// ===================== 统一入口 =====================
|
||||
|
||||
/** |
||||
* 根据 inputMode 分发到对应的处理器。 |
||||
* |
||||
* @param data parser 返回的原始数据结构 |
||||
* @param inputMode 检测模式(ExamineWay.inputMode) |
||||
* @param callback 样品回调 |
||||
* @param <T> 数据结构泛型 |
||||
*/ |
||||
@SuppressWarnings({"unchecked", "rawtypes"}) |
||||
public static <T> void visitSamples(T data, int inputMode, SampleCallback callback) { |
||||
switch (inputMode) { |
||||
case 1: // ELISA 常规板
|
||||
case 4: // PCR / 荧光PCR
|
||||
visitPtExcel((Map<String, List<Map<String, Map<String, Object>>>>) data, callback); |
||||
break; |
||||
case 2: // 虎红平板凝集
|
||||
case 6: // 试管凝集(微量法)
|
||||
case 7: // 虎红平板(另一种)
|
||||
visitBlsExcel((List) data, callback); |
||||
break; |
||||
case 3: // 口蹄疫
|
||||
visitKtyExcel((List<Map<String, Object>>) data, callback); |
||||
break; |
||||
case 5: // 血凝
|
||||
visitXnExcel((List) data, callback); |
||||
break; |
||||
case 8: // 菌落生长
|
||||
visitShjdExcel((List) data, callback); |
||||
break; |
||||
default: |
||||
// 未知 inputMode,不做处理
|
||||
break; |
||||
} |
||||
} |
||||
|
||||
// ===================== ptExcel (inputMode=1, 4) =====================
|
||||
|
||||
/** |
||||
* ptExcel 返回结构: |
||||
* Map<组名, List<Map<孔位, Map<字段, 值>>>> |
||||
* <p> |
||||
* 每个样品是 Map<String, Object>,包含 keys: value, originResult, result, num, order |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
private static void visitPtExcel(Map<String, List<Map<String, Map<String, Object>>>> data, SampleCallback callback) { |
||||
if (data == null) return; |
||||
|
||||
for (Map.Entry<String, List<Map<String, Map<String, Object>>>> groupEntry : data.entrySet()) { |
||||
List<Map<String, Map<String, Object>>> rows = groupEntry.getValue(); |
||||
if (rows == null) continue; |
||||
|
||||
for (Map<String, Map<String, Object>> row : rows) { |
||||
if (row == null) continue; |
||||
|
||||
for (Map.Entry<String, Map<String, Object>> wellEntry : row.entrySet()) { |
||||
Map<String, Object> wellData = wellEntry.getValue(); |
||||
if (wellData == null) continue; |
||||
|
||||
Object valueObj = wellData.get("value"); |
||||
if (valueObj == null) continue; |
||||
|
||||
String value = valueObj.toString(); |
||||
if (value.isEmpty() || "/".equals(value)) continue; |
||||
|
||||
// 提供完整的 wellData 作为公式 env
|
||||
Map<String, Object> env = new HashMap<>(wellData); |
||||
env.put("value", value); |
||||
|
||||
String result = callback.apply(value, env); |
||||
if (result != null && !result.isEmpty()) { |
||||
wellData.put("result", result); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// ===================== blsExcel (inputMode=2, 6, 7) =====================
|
||||
|
||||
/** |
||||
* blsExcel 返回 List<ExamineTemplate2Excel> |
||||
* 每个元素有: experieNum, value, result |
||||
*/ |
||||
private static void visitBlsExcel(List<ExamineTemplate2Excel> data, SampleCallback callback) { |
||||
if (data == null) return; |
||||
|
||||
for (ExamineTemplate2Excel item : data) { |
||||
if (item == null) continue; |
||||
|
||||
String value = item.getValue(); |
||||
if (value == null || value.isEmpty()) continue; |
||||
|
||||
Map<String, Object> env = new HashMap<>(); |
||||
env.put("experieNum", item.getExperieNum()); |
||||
env.put("value", value); |
||||
|
||||
String result = callback.apply(value, env); |
||||
if (result != null && !result.isEmpty()) { |
||||
item.setResult(result); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// ===================== ktyExcel (inputMode=3) =====================
|
||||
|
||||
/** |
||||
* ktyExcel(单板模式)返回 List<Map<String, Object>> |
||||
* 每个 Map 包含 keys: log2, result, order, experieNum 等 |
||||
* <p> |
||||
* 变量名使用 "log2"(与 ptExcel 的 "value" 区分) |
||||
*/ |
||||
private static void visitKtyExcel(List<Map<String, Object>> data, SampleCallback callback) { |
||||
if (data == null) return; |
||||
|
||||
for (Map<String, Object> item : data) { |
||||
if (item == null) continue; |
||||
|
||||
// ktyExcel 主值是 log2
|
||||
Object valueObj = item.get("log2"); |
||||
if (valueObj == null) continue; |
||||
|
||||
String value = valueObj.toString(); |
||||
if (value.isEmpty()) continue; |
||||
|
||||
Map<String, Object> env = new HashMap<>(item); |
||||
// 确保 env 中有 value 别名
|
||||
env.put("value", value); |
||||
|
||||
String result = callback.apply(value, env); |
||||
if (result != null && !result.isEmpty()) { |
||||
item.put("result", result); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// ===================== xnExcel (inputMode=5) =====================
|
||||
|
||||
/** |
||||
* xnExcel 返回 List<XN2Excel> |
||||
* 每个元素有: experieNum, ctValue, result |
||||
*/ |
||||
private static void visitXnExcel(List<XN2Excel> data, SampleCallback callback) { |
||||
if (data == null) return; |
||||
|
||||
for (XN2Excel item : data) { |
||||
if (item == null) continue; |
||||
|
||||
String ctValue = item.getCtValue(); |
||||
if (ctValue == null || ctValue.isEmpty()) continue; |
||||
|
||||
Map<String, Object> env = new HashMap<>(); |
||||
env.put("experieNum", item.getExperieNum()); |
||||
env.put("ctValue", ctValue); |
||||
env.put("value", ctValue); // 兼容 value 变量名
|
||||
|
||||
String result = callback.apply(ctValue, env); |
||||
if (result != null && !result.isEmpty()) { |
||||
item.setResult(result); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// ===================== shjdExcel (inputMode=8) =====================
|
||||
|
||||
/** |
||||
* shjdExcel 返回 List<BiochemicalIdentificationExcel> |
||||
* 每个元素有: experieNum, value, result |
||||
*/ |
||||
private static void visitShjdExcel(List<BiochemicalIdentificationExcel> data, SampleCallback callback) { |
||||
if (data == null) return; |
||||
|
||||
for (BiochemicalIdentificationExcel item : data) { |
||||
if (item == null) continue; |
||||
|
||||
String value = item.getValue(); |
||||
if (value == null || value.isEmpty()) continue; |
||||
|
||||
Map<String, Object> env = new HashMap<>(); |
||||
env.put("experieNum", item.getExperieNum()); |
||||
env.put("value", value); |
||||
|
||||
String result = callback.apply(value, env); |
||||
if (result != null && !result.isEmpty()) { |
||||
item.setResult(result); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,234 @@ |
||||
package org.springblade.lims.utils; |
||||
|
||||
import com.googlecode.aviator.AviatorEvaluator; |
||||
import com.googlecode.aviator.AviatorEvaluatorInstance; |
||||
import com.googlecode.aviator.Expression; |
||||
|
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.concurrent.CountDownLatch; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
/** |
||||
* Aviator 公式校验工具类 |
||||
* <p> |
||||
* 提供公式语法校验和安全校验功能: |
||||
* - 语法检查(编译) |
||||
* - 安全校验(长度限制、危险类调用阻止) |
||||
* - 变量提取 |
||||
* |
||||
* @author blade |
||||
* @since 2026-05-27 |
||||
*/ |
||||
public class FormulaValidationTool { |
||||
|
||||
/** |
||||
* 公式最大长度 |
||||
*/ |
||||
private static final int MAX_EXPRESSION_LENGTH = 500; |
||||
|
||||
/** |
||||
* 危险关键词列表,禁止在公式中出现 |
||||
*/ |
||||
private static final String[] DANGEROUS_KEYWORDS = { |
||||
"Runtime", "Process", "System", "ClassLoader", "Thread", "Socket", |
||||
"ProcessBuilder", "File", "FileOutputStream", "FileInputStream", |
||||
"exec", "Runtime.getRuntime", "ProcessHandle" |
||||
}; |
||||
|
||||
/** |
||||
* 校验结果 |
||||
*/ |
||||
public static class ValidationResult { |
||||
private boolean valid; |
||||
private String message; |
||||
private List<String> variables; |
||||
|
||||
public ValidationResult() {} |
||||
|
||||
public ValidationResult(boolean valid, String message, List<String> variables) { |
||||
this.valid = valid; |
||||
this.message = message; |
||||
this.variables = variables; |
||||
} |
||||
|
||||
public boolean isValid() { return valid; } |
||||
public void setValid(boolean valid) { this.valid = valid; } |
||||
public String getMessage() { return message; } |
||||
public void setMessage(String message) { this.message = message; } |
||||
public List<String> getVariables() { return variables; } |
||||
public void setVariables(List<String> variables) { this.variables = variables; } |
||||
} |
||||
|
||||
/** |
||||
* 公式语法校验(仅编译检查,不执行) |
||||
* |
||||
* @param expression 公式表达式 |
||||
* @return 校验结果 |
||||
*/ |
||||
public static ValidationResult validate(String expression) { |
||||
ValidationResult result = new ValidationResult(); |
||||
try { |
||||
// 编译表达式检查语法
|
||||
Expression compiledExpr = AviatorEvaluator.compile(expression); |
||||
|
||||
// 提取变量名
|
||||
List<String> varNames = compiledExpr.getVariableNames(); |
||||
result.setVariables(varNames); |
||||
|
||||
result.setValid(true); |
||||
result.setMessage("公式语法校验通过"); |
||||
} catch (Exception e) { |
||||
result.setValid(false); |
||||
result.setMessage("公式语法错误: " + e.getMessage()); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* 安全校验(长度、危险类 + 语法检查) |
||||
* |
||||
* @param expression 公式表达式 |
||||
* @return 校验结果 |
||||
*/ |
||||
public static ValidationResult validateSafe(String expression) { |
||||
// 1. 长度校验
|
||||
if (expression == null || expression.isEmpty()) { |
||||
ValidationResult r = new ValidationResult(); |
||||
r.setValid(false); |
||||
r.setMessage("公式不能为空"); |
||||
return r; |
||||
} |
||||
|
||||
if (expression.length() > MAX_EXPRESSION_LENGTH) { |
||||
ValidationResult r = new ValidationResult(); |
||||
r.setValid(false); |
||||
r.setMessage("公式超过最大长度限制(" + MAX_EXPRESSION_LENGTH + ")"); |
||||
return r; |
||||
} |
||||
|
||||
// 2. 危险关键词校验
|
||||
for (String keyword : DANGEROUS_KEYWORDS) { |
||||
if (expression.contains(keyword)) { |
||||
ValidationResult r = new ValidationResult(); |
||||
r.setValid(false); |
||||
r.setMessage("公式包含危险类调用(" + keyword + "),已禁止"); |
||||
return r; |
||||
} |
||||
} |
||||
|
||||
// 3. 语法校验
|
||||
final ValidationResult[] result = {null}; |
||||
final String[] errorMsg = {null}; |
||||
CountDownLatch latch = new CountDownLatch(1); |
||||
|
||||
// 使用独立线程执行编译,防止死循环或长时间阻塞
|
||||
Thread compileThread = new Thread(() -> { |
||||
try { |
||||
result[0] = validate(expression); |
||||
} catch (Exception e) { |
||||
errorMsg[0] = e.getMessage(); |
||||
} finally { |
||||
latch.countDown(); |
||||
} |
||||
}); |
||||
compileThread.setDaemon(true); |
||||
compileThread.start(); |
||||
|
||||
try { |
||||
// 设置超时时间 5 秒
|
||||
boolean completed = latch.await(5, TimeUnit.SECONDS); |
||||
if (!completed) { |
||||
compileThread.interrupt(); |
||||
ValidationResult r = new ValidationResult(); |
||||
r.setValid(false); |
||||
r.setMessage("公式编译超时,请检查公式逻辑"); |
||||
return r; |
||||
} |
||||
} catch (InterruptedException e) { |
||||
Thread.currentThread().interrupt(); |
||||
ValidationResult r = new ValidationResult(); |
||||
r.setValid(false); |
||||
r.setMessage("公式校验被中断"); |
||||
return r; |
||||
} |
||||
|
||||
if (result[0] != null) { |
||||
return result[0]; |
||||
} |
||||
|
||||
ValidationResult r = new ValidationResult(); |
||||
r.setValid(false); |
||||
r.setMessage("公式校验失败: " + errorMsg[0]); |
||||
return r; |
||||
} |
||||
|
||||
// ========== Formula Execution ==========
|
||||
|
||||
/** |
||||
* 执行公式(直接执行,不带超时保护) |
||||
* |
||||
* @param expression 公式表达式 |
||||
* @param env 变量环境 |
||||
* @return 公式执行结果(通常是 String 或 Number) |
||||
*/ |
||||
public static Object execute(String expression, Map<String, Object> env) { |
||||
try { |
||||
Expression compiledExpr = AviatorEvaluator.compile(expression); |
||||
return compiledExpr.execute(env); |
||||
} catch (Exception e) { |
||||
throw new RuntimeException("公式执行失败: " + e.getMessage(), e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 执行公式(带超时保护,用于生产环境) |
||||
* |
||||
* @param expression 公式表达式 |
||||
* @param env 变量环境 |
||||
* @param timeoutMs 超时时间(毫秒),默认 5000 |
||||
* @return 公式执行结果(通常是 String 或 Number) |
||||
*/ |
||||
public static Object executeSafe(String expression, Map<String, Object> env, long timeoutMs) { |
||||
final Object[] result = {null}; |
||||
final String[] errorMsg = {null}; |
||||
CountDownLatch latch = new CountDownLatch(1); |
||||
|
||||
Thread execThread = new Thread(() -> { |
||||
try { |
||||
Expression compiledExpr = AviatorEvaluator.compile(expression); |
||||
result[0] = compiledExpr.execute(env); |
||||
} catch (Exception e) { |
||||
errorMsg[0] = e.getMessage(); |
||||
} finally { |
||||
latch.countDown(); |
||||
} |
||||
}); |
||||
execThread.setDaemon(true); |
||||
execThread.start(); |
||||
|
||||
try { |
||||
boolean completed = latch.await(timeoutMs, TimeUnit.MILLISECONDS); |
||||
if (!completed) { |
||||
execThread.interrupt(); |
||||
throw new RuntimeException("公式执行超时(" + timeoutMs + "ms)"); |
||||
} |
||||
} catch (InterruptedException e) { |
||||
Thread.currentThread().interrupt(); |
||||
throw new RuntimeException("公式执行被中断", e); |
||||
} |
||||
|
||||
if (errorMsg[0] != null) { |
||||
throw new RuntimeException("公式执行失败: " + errorMsg[0]); |
||||
} |
||||
|
||||
return result[0]; |
||||
} |
||||
|
||||
/** |
||||
* 执行公式(带默认 5 秒超时保护) |
||||
*/ |
||||
public static Object executeSafe(String expression, Map<String, Object> env) { |
||||
return executeSafe(expression, env, 5000L); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue