新增功能:试剂公式维护

feature-wangxilei-dev
wxl 2 weeks ago
parent 0bebfcc342
commit 59b54db9f6
  1. 37
      lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/ReagentFormula.java
  2. 7
      lab-service/lab-lims/pom.xml
  3. 12
      lab-service/lab-lims/src/main/java/org/springblade/lims/controller/ExamineResultController.java
  4. 255
      lab-service/lab-lims/src/main/java/org/springblade/lims/controller/ReagentFormulaController.java
  5. 13
      lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/ReagentFormulaMapper.java
  6. 17
      lab-service/lab-lims/src/main/java/org/springblade/lims/service/IExamineResultService.java
  7. 21
      lab-service/lab-lims/src/main/java/org/springblade/lims/service/IReagentFormulaService.java
  8. 74
      lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/ExamineResultServiceImpl.java
  9. 35
      lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/ReagentFormulaServiceImpl.java
  10. 239
      lab-service/lab-lims/src/main/java/org/springblade/lims/utils/DataStructureAdapter.java
  11. 234
      lab-service/lab-lims/src/main/java/org/springblade/lims/utils/FormulaValidationTool.java

@ -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;
}

@ -28,6 +28,13 @@
<version>3.2.0</version> <version>3.2.0</version>
</dependency> </dependency>
<!-- Aviator 表达式引擎(用于试剂公式校验) -->
<dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
<version>5.3.3</version>
</dependency>
<!-- Drools rule engine (Java 8 compatible) --> <!-- Drools rule engine (Java 8 compatible) -->
<dependency> <dependency>
<groupId>org.drools</groupId> <groupId>org.drools</groupId>

@ -79,6 +79,18 @@ public class ExamineResultController extends BladeController {
return service.excel(file, examineId, reagentId); return service.excel(file, examineId, reagentId);
} }
/**
* 解析数据 + 试剂公式自动计算结果
* <p>
* /excel 接口参数一致但会根据 reagentId 查找已启用的 ReagentFormula
* 使用 Aviator 引擎逐样品计算结果覆盖原始 result 字段
* 未配置公式时降级为 /excel 的原始计算逻辑
*/
@PostMapping("/excelWithFormula")
public R excelWithFormula(MultipartFile file, String examineId, String reagentId) throws Exception {
return service.excelWithFormula(file, examineId, reagentId);
}
/** /**
* PCRExcel解析数据 * PCRExcel解析数据
*/ */

@ -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> {
}

@ -28,6 +28,23 @@ public interface IExamineResultService extends BaseService<ExamineResult> {
R excel(MultipartFile file, String examineId, String reagentId) throws Exception; R excel(MultipartFile file, String examineId, String reagentId) throws Exception;
/**
* 解析Excel并执行试剂公式计算结果替换 /excel 的逻辑
* <p>
* 解析流程
* 1. 调用对应 parser 解析 Excel 原始数据
* 2. 根据 reagentId 查询启用的 ReagentFormula
* 3. 有公式 Aviator 逐样品计算结果覆盖 result 字段
* 4. 无公式 保留 parser 原始计算结果降级兼容
* 5. 将最终结果存入 f_examine_result
*
* @param file 上传的 Excel 文件
* @param examineId 检验ID
* @param reagentId 试剂ID用于查找公式
* @return 解析结果 /excel 返回结构一致
*/
R excelWithFormula(MultipartFile file, String examineId, String reagentId) throws Exception;
void exportCurrExamineTemplate(HttpServletResponse response, String id); void exportCurrExamineTemplate(HttpServletResponse response, String id);
String handleNum(String id, Integer number); String handleNum(String id, Integer number);

@ -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);
}

@ -17,6 +17,8 @@ import org.springblade.lims.entry.*;
import org.springblade.lims.excel.*; import org.springblade.lims.excel.*;
import org.springblade.lims.mapper.ExamineResultMapper; import org.springblade.lims.mapper.ExamineResultMapper;
import org.springblade.lims.service.*; import org.springblade.lims.service.*;
import org.springblade.lims.utils.DataStructureAdapter;
import org.springblade.lims.utils.FormulaValidationTool;
import org.springblade.resource.enums.SysTypeEnum; import org.springblade.resource.enums.SysTypeEnum;
import org.springblade.resource.feign.IMessageClient; import org.springblade.resource.feign.IMessageClient;
import org.springblade.system.feign.ISysClient; import org.springblade.system.feign.ISysClient;
@ -32,8 +34,11 @@ import java.text.DecimalFormat;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
@Slf4j
public class ExamineResultServiceImpl extends BaseServiceImpl<ExamineResultMapper, ExamineResult> implements IExamineResultService { public class ExamineResultServiceImpl extends BaseServiceImpl<ExamineResultMapper, ExamineResult> implements IExamineResultService {
private final IExamineService examineService; private final IExamineService examineService;
@ -64,6 +69,8 @@ public class ExamineResultServiceImpl extends BaseServiceImpl<ExamineResultMappe
private final IMessageClient messageClient; private final IMessageClient messageClient;
private final IReagentFormulaService reagentFormulaService;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void resultCommit(ExamineResult examineResult) throws Exception { public void resultCommit(ExamineResult examineResult) throws Exception {
@ -241,6 +248,73 @@ public class ExamineResultServiceImpl extends BaseServiceImpl<ExamineResultMappe
} }
} }
@Override
public R excelWithFormula(MultipartFile file, String examineId, String reagentId) throws Exception {
// 1. 解析Excel — 复用现有 parser 逻辑
R parseResult = excel(file, examineId, reagentId);
Object data = parseResult.getData();
if (data == null) {
return parseResult;
}
// 2. 查找试剂公式(未配置 → 降级,保持原有计算结果)
if (reagentId == null) {
return parseResult;
}
ReagentFormula formula = reagentFormulaService.findEnabledByReagentId(Long.valueOf(reagentId));
if (formula == null) {
log.info("试剂[{}]未配置公式,使用原有解析结果", reagentId);
return parseResult;
}
// 3. 确定检测模式
Examine examine = examineService.getById(examineId);
ExamineWay examineWay = examineWayService.getById(examine.getExamineWayId());
int inputMode = Integer.parseInt(examineWay.getInputMode());
String expression = formula.getExpression();
// 4. 使用 Aviator 公式逐样品计算结果,覆盖 result 字段
boolean formulaApplied = false;
try {
DataStructureAdapter.visitSamples(data, inputMode, (value, env) -> {
try {
Object result = FormulaValidationTool.executeSafe(expression, env);
return result != null ? result.toString() : null;
} catch (Exception e) {
log.error("公式执行失败: expr={}, value={}, env={}",
expression, value, env, e);
return null; // 单个样品失败 → 保留原值
}
});
formulaApplied = true;
} catch (Exception e) {
log.error("公式遍历处理异常: examineId={}, reagentId={}", examineId, reagentId, e);
}
// 5. 保存最终结果(含公式计算后的数据)到 f_examine_result
if (formulaApplied) {
String jsonData = JSON.toJSONString(data);
LambdaQueryWrapper<ExamineResult> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ExamineResult::getExamineId, examineId);
ExamineResult er = getOne(wrapper);
if (er != null) {
er.setOriginRecordData(jsonData);
er.setOriginRecordResult(jsonData);
er.setExamineDataArr(jsonData);
this.updateById(er);
} else {
ExamineResult newEr = new ExamineResult();
newEr.setExamineId(Long.valueOf(examineId));
newEr.setOriginRecordData(jsonData);
newEr.setOriginRecordResult(jsonData);
newEr.setExamineDataArr(jsonData);
this.save(newEr);
}
}
return parseResult;
}
@Override @Override
public void exportCurrExamineTemplate(HttpServletResponse response, String id) { public void exportCurrExamineTemplate(HttpServletResponse response, String id) {
Examine examine = examineService.getById(id); Examine examine = examineService.getById(id);

@ -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>
* 将不同 parserptExcelblsExcelxnExcelktyExcelshjdExcel返回的异构数据结构
* 统一为遍历每个样品 执行回调的模式用于公式注入
* <p>
* 每种 parser 的返回值结构
* <pre>
* ptExcel (inputMode=1,4) Map&lt;String, List&lt;Map&lt;String, Map&lt;String, Object&gt;&gt;&gt;&gt;
* blsExcel (inputMode=2,6,7) List&lt;ExamineTemplate2Excel&gt;
* ktyExcel (inputMode=3) List&lt;Map&lt;String, Object&gt;&gt;
* xnExcel (inputMode=5) List&lt;XN2Excel&gt;
* shjdExcel (inputMode=8) List&lt;BiochemicalIdentificationExcel&gt;
* </pre>
*
* @author blade
* @since 2026-05-27
*/
public final class DataStructureAdapter {
private DataStructureAdapter() {
}
/**
* 样品回调接口
* <p>
* 实现类接收该样品的 primary value 和完整的上下文 env
* 返回公式计算后的 result 字符串 "阳性""阴性""可疑"
*/
@FunctionalInterface
public interface SampleCallback {
/**
* @param value 样品主值ODCTtiter凝集值等
* @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&lt;组名, List&lt;Map&lt;孔位, Map&lt;字段, &gt;&gt;&gt;&gt;
* <p>
* 每个样品是 Map&lt;String, Object&gt;包含 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&lt;ExamineTemplate2Excel&gt;
* 每个元素有: 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&lt;Map&lt;String, Object&gt;&gt;
* 每个 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&lt;XN2Excel&gt;
* 每个元素有: 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&lt;BiochemicalIdentificationExcel&gt;
* 每个元素有: 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…
Cancel
Save