parent
5e87e96a6f
commit
0bebfcc342
35 changed files with 2615 additions and 5 deletions
@ -0,0 +1,98 @@ |
||||
package org.springblade.lims.entry; |
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill; |
||||
import com.baomidou.mybatisplus.annotation.TableField; |
||||
import com.baomidou.mybatisplus.annotation.TableName; |
||||
import lombok.Data; |
||||
import org.springframework.data.annotation.Id; |
||||
|
||||
import java.io.Serializable; |
||||
import java.math.BigDecimal; |
||||
import java.util.Date; |
||||
|
||||
/** |
||||
* 预警记录 |
||||
* |
||||
* @author swj |
||||
*/ |
||||
@Data |
||||
@SuppressWarnings("all") |
||||
@TableName("f_alert_record") |
||||
public class AlertRecord implements Serializable { |
||||
|
||||
private static final long serialVersionUID = 1L; |
||||
|
||||
@Id |
||||
public Long id; |
||||
|
||||
/** |
||||
* 关联阈值配置ID |
||||
*/ |
||||
public Long thresholdConfigId; |
||||
|
||||
/** |
||||
* 预警级别: warning/danger |
||||
*/ |
||||
public String alertLevel; |
||||
|
||||
/** |
||||
* 指标编码 |
||||
*/ |
||||
public String indicatorCode; |
||||
|
||||
/** |
||||
* 指标名称 |
||||
*/ |
||||
public String indicatorName; |
||||
|
||||
/** |
||||
* 作用范围描述(如: 全省/济南市/口蹄疫) |
||||
*/ |
||||
public String scopeName; |
||||
|
||||
/** |
||||
* 触发时的当前值 |
||||
*/ |
||||
public BigDecimal currentValue; |
||||
|
||||
/** |
||||
* 触发时的阈值线值 |
||||
*/ |
||||
public BigDecimal thresholdValue; |
||||
|
||||
/** |
||||
* 触发时间 |
||||
*/ |
||||
public Date triggerTime; |
||||
|
||||
/** |
||||
* 状态: untreated/confirmed/processed (映射到数据库status列) |
||||
*/ |
||||
@TableField("status") |
||||
public String alertStatus; |
||||
|
||||
/** |
||||
* 备注/处理说明 |
||||
*/ |
||||
public String remark; |
||||
|
||||
/** 创建人 */ |
||||
@TableField(fill = FieldFill.INSERT) |
||||
public Long createUser; |
||||
|
||||
/** 创建部门 */ |
||||
@TableField(fill = FieldFill.INSERT) |
||||
public Long createDept; |
||||
|
||||
/** 创建时间 */ |
||||
@TableField(fill = FieldFill.INSERT) |
||||
public Date createTime; |
||||
|
||||
/** 修改人 */ |
||||
@TableField(fill = FieldFill.UPDATE) |
||||
public Long updateUser; |
||||
|
||||
/** 修改时间 */ |
||||
@TableField(fill = FieldFill.UPDATE) |
||||
public Date updateTime; |
||||
} |
||||
@ -0,0 +1,47 @@ |
||||
package org.springblade.lims.entry; |
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName; |
||||
import io.swagger.annotations.ApiModelProperty; |
||||
import lombok.Data; |
||||
import org.springblade.core.mp.base.BaseEntity; |
||||
import org.springframework.data.annotation.Id; |
||||
|
||||
import java.io.Serializable; |
||||
|
||||
@Data |
||||
@SuppressWarnings("all") |
||||
@TableName("f_assign_rule") |
||||
public class AssignRule extends BaseEntity implements Serializable { |
||||
|
||||
private static final long serialVersionUID = 1L; |
||||
|
||||
@Id |
||||
private Long id; |
||||
|
||||
@ApiModelProperty("规则名称") |
||||
public String name; |
||||
|
||||
@ApiModelProperty("条件配置(JSON)") |
||||
public String conditions; |
||||
|
||||
@ApiModelProperty("目标科室ID") |
||||
public Long targetDeptId; |
||||
|
||||
@ApiModelProperty("目标科室名称") |
||||
public String targetDeptName; |
||||
|
||||
@ApiModelProperty("触发频率") |
||||
public String frequency; |
||||
|
||||
@ApiModelProperty("科室负载阈值") |
||||
public Integer deptLoadThreshold; |
||||
|
||||
@ApiModelProperty("启用状态(0禁用 1启用)") |
||||
public Integer enabled; |
||||
|
||||
@ApiModelProperty("排序") |
||||
public Integer sort; |
||||
|
||||
@ApiModelProperty("未领取超时时间(小时),0表示不限制") |
||||
public Integer unclaimTimeoutHours; |
||||
} |
||||
@ -0,0 +1,47 @@ |
||||
package org.springblade.lims.entry; |
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName; |
||||
import io.swagger.annotations.ApiModelProperty; |
||||
import lombok.Data; |
||||
import org.springblade.core.mp.base.BaseEntity; |
||||
import org.springframework.data.annotation.Id; |
||||
|
||||
import java.io.Serializable; |
||||
|
||||
@Data |
||||
@TableName("f_assign_rule_log") |
||||
public class AssignRuleLog extends BaseEntity implements Serializable { |
||||
|
||||
private static final long serialVersionUID = 1L; |
||||
|
||||
@Id |
||||
@ApiModelProperty("主键ID") |
||||
private Long id; |
||||
|
||||
@ApiModelProperty("规则ID") |
||||
private Long ruleId; |
||||
|
||||
@ApiModelProperty("规则名称") |
||||
private String ruleName; |
||||
|
||||
@ApiModelProperty("Examine记录ID") |
||||
private Long examineId; |
||||
|
||||
@ApiModelProperty("检测编号") |
||||
private String experieNum; |
||||
|
||||
@ApiModelProperty("原科室ID") |
||||
private Long sourceDeptId; |
||||
|
||||
@ApiModelProperty("目标科室ID") |
||||
private Long targetDeptId; |
||||
|
||||
@ApiModelProperty("目标科室名称") |
||||
private String targetDeptName; |
||||
|
||||
@ApiModelProperty("状态(1=成功 0=跳过)") |
||||
private Integer status; |
||||
|
||||
@ApiModelProperty("匹配描述") |
||||
private String matchDesc; |
||||
} |
||||
@ -0,0 +1,70 @@ |
||||
package org.springblade.lims.entry; |
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName; |
||||
import lombok.Data; |
||||
import org.springblade.core.mp.base.BaseEntity; |
||||
import org.springframework.data.annotation.Id; |
||||
|
||||
import java.io.Serializable; |
||||
import java.math.BigDecimal; |
||||
|
||||
/** |
||||
* 阈值配置 |
||||
* |
||||
* @author swj |
||||
*/ |
||||
@Data |
||||
@SuppressWarnings("all") |
||||
@TableName("f_threshold_config") |
||||
public class ThresholdConfig extends BaseEntity implements Serializable { |
||||
|
||||
private static final long serialVersionUID = 1L; |
||||
|
||||
@Id |
||||
public Long id; |
||||
|
||||
/** |
||||
* 指标编码: group_positive_rate(群体阳性率), monthly_positive_growth(月度阳性增长率), farm_positive_count(单场阳性数), region_new_cases(区域新增病例) |
||||
*/ |
||||
public String indicatorCode; |
||||
|
||||
/** |
||||
* 指标名称 |
||||
*/ |
||||
public String indicatorName; |
||||
|
||||
/** |
||||
* 警示线(触发warning级别) |
||||
*/ |
||||
public BigDecimal warningLine; |
||||
|
||||
/** |
||||
* 预警线(触发danger级别) |
||||
*/ |
||||
public BigDecimal alertLine; |
||||
|
||||
/** |
||||
* 作用范围类型: all(全省)/city(某市)/disease(某病种) |
||||
*/ |
||||
public String scopeType; |
||||
|
||||
/** |
||||
* 范围具体值: scope_type=city时为blade_region.code |
||||
*/ |
||||
public String scopeValue; |
||||
|
||||
/** |
||||
* 关联病种ID(t_examine_item.id) |
||||
*/ |
||||
public Long diseaseId; |
||||
|
||||
/** |
||||
* 启用状态: 0=禁用 1=启用 |
||||
*/ |
||||
public Integer enabled; |
||||
|
||||
/** |
||||
* 备注 |
||||
*/ |
||||
public String remark; |
||||
} |
||||
@ -0,0 +1,47 @@ |
||||
package org.springblade.lims.api; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import java.io.Serializable; |
||||
|
||||
/** |
||||
* API 传输 DTO — 导入样品解析结果行 |
||||
* |
||||
* @author swj |
||||
*/ |
||||
@Data |
||||
public class SimpleImportRow implements Serializable { |
||||
|
||||
private static final long serialVersionUID = 1L; |
||||
|
||||
// 样品编号(必填)
|
||||
private String experieNum; |
||||
|
||||
// 样品名称
|
||||
private String simpleName; |
||||
|
||||
// 样品数量
|
||||
private Integer count; |
||||
|
||||
// 采样日期
|
||||
private String samplingDate; |
||||
|
||||
// 送检单位
|
||||
private String customerName; |
||||
|
||||
// 原始编号
|
||||
private String originalNum; |
||||
|
||||
// 送检人
|
||||
private String submittedBy; |
||||
|
||||
// 验证状态: valid / warning / error
|
||||
private String status; |
||||
|
||||
// 验证消息
|
||||
private String message; |
||||
|
||||
// 原始行号
|
||||
private Integer rowIndex; |
||||
|
||||
} |
||||
@ -0,0 +1,155 @@ |
||||
package org.springblade.lims.controller; |
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
||||
import com.baomidou.mybatisplus.core.metadata.IPage; |
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; |
||||
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.Scheduled.GlobalScheduledTasks; |
||||
import org.springblade.lims.entry.AlertRecord; |
||||
import org.springblade.lims.service.IAlertRecordService; |
||||
import org.springframework.web.bind.annotation.*; |
||||
|
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* 预警记录 控制器 |
||||
* |
||||
* @author swj |
||||
* @since 2026-05-26 |
||||
*/ |
||||
@RestController |
||||
@AllArgsConstructor |
||||
@RequestMapping("/alertRecord") |
||||
public class AlertRecordController extends BladeController { |
||||
|
||||
private final IAlertRecordService service; |
||||
private final GlobalScheduledTasks scheduledTasks; |
||||
|
||||
/** |
||||
* 分页列表 |
||||
*/ |
||||
@GetMapping("/list") |
||||
@ApiOperationSupport(order = 2) |
||||
@ApiOperation(value = "分页", notes = "分页,支持keyword模糊搜索indicator_name和scope_name") |
||||
public R<IPage<AlertRecord>> list(AlertRecord entry, Query query, |
||||
@RequestParam(required = false) String keyword) { |
||||
QueryWrapper<AlertRecord> wrapper = Condition.getQueryWrapper(entry); |
||||
if (Func.isNotBlank(keyword)) { |
||||
wrapper.and(w -> w.like("indicator_name", keyword).or().like("scope_name", keyword)); |
||||
} |
||||
IPage<AlertRecord> page = service.page(Condition.getPage(query), wrapper); |
||||
return R.data(page); |
||||
} |
||||
|
||||
/** |
||||
* 手动触发预警检测 |
||||
*/ |
||||
@PostMapping("/triggerAll") |
||||
@ApiOperationSupport(order = 9) |
||||
@ApiOperation(value = "手动触发预警检测", notes = "手动触发预警检测") |
||||
public R triggerAll() { |
||||
try { |
||||
scheduledTasks.manualCheck(); |
||||
return R.success("预警检测已触发,请稍后刷新列表查看结果"); |
||||
} catch (Exception e) { |
||||
return R.fail("预警检测触发失败: " + e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 详情 |
||||
*/ |
||||
@GetMapping("/detail") |
||||
@ApiOperationSupport(order = 3) |
||||
@ApiOperation(value = "详情", notes = "详情") |
||||
public R<AlertRecord> detail(@ApiParam(value = "主键", required = true) @RequestParam Long id) { |
||||
AlertRecord detail = service.getById(id); |
||||
return R.data(detail); |
||||
} |
||||
|
||||
/** |
||||
* 新增 |
||||
*/ |
||||
@PostMapping("/insert") |
||||
@ApiOperationSupport(order = 4) |
||||
@ApiOperation(value = "新增", notes = "新增") |
||||
public R insert(@RequestBody AlertRecord entry) { |
||||
return R.data(service.save(entry)); |
||||
} |
||||
|
||||
/** |
||||
* 修改 |
||||
*/ |
||||
@PostMapping("/update") |
||||
@ApiOperationSupport(order = 5) |
||||
@ApiOperation(value = "修改", notes = "修改") |
||||
public R update(@RequestBody AlertRecord entry) { |
||||
return R.data(service.updateById(entry)); |
||||
} |
||||
|
||||
/** |
||||
* 删除 |
||||
*/ |
||||
@PostMapping("/deleteByIds") |
||||
@ApiOperationSupport(order = 6) |
||||
@ApiOperation(value = "逻辑删除", notes = "传入ids") |
||||
public R deleteByIds(@ApiParam(value = "主键集合", required = true) @RequestParam String ids) { |
||||
return R.status(service.deleteLogic(Func.toLongList(ids))); |
||||
} |
||||
|
||||
/** |
||||
* 更新状态 |
||||
*/ |
||||
@PostMapping("/updateStatus") |
||||
@ApiOperationSupport(order = 7) |
||||
@ApiOperation(value = "更新状态", notes = "更新状态: untreated/confirmed/processed") |
||||
public R updateStatus(@RequestParam Long id, @RequestParam String status) { |
||||
AlertRecord record = service.getById(id); |
||||
if (record == null) { |
||||
return R.fail("记录不存在"); |
||||
} |
||||
record.setAlertStatus(status); |
||||
return R.status(service.updateById(record)); |
||||
} |
||||
|
||||
/** |
||||
* 统计摘要 |
||||
*/ |
||||
@GetMapping("/summary") |
||||
@ApiOperationSupport(order = 8) |
||||
@ApiOperation(value = "统计摘要", notes = "统计摘要") |
||||
public R<Map<String, Object>> summary() { |
||||
Map<String, Object> result = new HashMap<>(3); |
||||
|
||||
// 活跃预警: status='untreated' AND alert_level='danger'
|
||||
QueryWrapper<AlertRecord> activeWrapper = new QueryWrapper<>(); |
||||
activeWrapper.eq("status", "untreated"); |
||||
activeWrapper.eq("alert_level", "danger"); |
||||
int activeAlert = service.count(activeWrapper); |
||||
result.put("activeAlert", activeAlert); |
||||
|
||||
// 本月触发: trigger_time >= 本月1日
|
||||
QueryWrapper<AlertRecord> monthlyWrapper = new QueryWrapper<>(); |
||||
java.time.LocalDate now = java.time.LocalDate.now(); |
||||
java.time.LocalDate firstDay = now.withDayOfMonth(1); |
||||
monthlyWrapper.ge("trigger_time", java.sql.Date.valueOf(firstDay)); |
||||
int monthlyTriggered = service.count(monthlyWrapper); |
||||
result.put("monthlyTriggered", monthlyTriggered); |
||||
|
||||
// 已处理: status='processed'
|
||||
QueryWrapper<AlertRecord> processedWrapper = new QueryWrapper<>(); |
||||
processedWrapper.eq("status", "processed"); |
||||
int processed = service.count(processedWrapper); |
||||
result.put("processed", processed); |
||||
|
||||
return R.data(result); |
||||
} |
||||
} |
||||
@ -0,0 +1,329 @@ |
||||
|
||||
package org.springblade.lims.controller; |
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
||||
import com.baomidou.mybatisplus.core.metadata.IPage; |
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; |
||||
import io.swagger.annotations.ApiOperation; |
||||
import io.swagger.annotations.ApiParam; |
||||
import lombok.AllArgsConstructor; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springblade.core.boot.ctrl.BladeController; |
||||
import org.springblade.core.excel.util.ExcelUtil; |
||||
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.api.SimpleImportRow; |
||||
import org.springblade.lims.entry.AssignRule; |
||||
import org.springblade.lims.entry.Examine; |
||||
import org.springblade.lims.excel.SimpleImportExcel; |
||||
import org.springblade.lims.service.IAssignRuleService; |
||||
import org.springblade.lims.service.IExamineService; |
||||
import org.springblade.system.feign.ISysClient; |
||||
import org.springframework.web.bind.annotation.*; |
||||
import org.springframework.web.multipart.MultipartFile; |
||||
|
||||
import javax.servlet.http.HttpServletResponse; |
||||
import java.text.DecimalFormat; |
||||
import java.text.ParseException; |
||||
import java.text.SimpleDateFormat; |
||||
import java.util.*; |
||||
|
||||
/** |
||||
* |
||||
* |
||||
* @author swj |
||||
* @since 2022年6月1日19:49:10 |
||||
*/ |
||||
@RestController |
||||
@AllArgsConstructor |
||||
@Slf4j |
||||
@RequestMapping("/assignRule") |
||||
public class AssignRuleController extends BladeController { |
||||
|
||||
private final IAssignRuleService service; |
||||
|
||||
private final IExamineService examineService; |
||||
|
||||
private final ISysClient sysClient; |
||||
|
||||
/** |
||||
* 分页 |
||||
*/ |
||||
@GetMapping("/list") |
||||
@ApiOperationSupport(order = 2) |
||||
public R<IPage<AssignRule>> list(AssignRule entry, Query query) { |
||||
IPage<AssignRule> page = service.page(Condition.getPage(query), Condition.getQueryWrapper(entry)); |
||||
return R.data(page); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
@PostMapping("/insert") |
||||
@ApiOperation(value = "新增", notes = "新增分配规则") |
||||
public R insert(@RequestBody AssignRule entry) { |
||||
entry.setCreateTime(new Date()); |
||||
entry.setUpdateTime(new Date()); |
||||
return R.data(service.save(entry)); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
@PostMapping("/update") |
||||
@ApiOperation(value = "更改", notes = "更改分配规则") |
||||
public R update(@RequestBody AssignRule entry) { |
||||
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))); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
@GetMapping("/detail") |
||||
@ApiOperation(value = "详情", notes = "查询单条") |
||||
public R detail(@RequestParam Long id) { |
||||
return R.data(service.getById(id)); |
||||
} |
||||
|
||||
/** |
||||
* 执行率分析 |
||||
*/ |
||||
@GetMapping("/analysis") |
||||
@ApiOperation(value = "执行率分析", notes = "按科室统计任务执行率") |
||||
public R<Map<String, Object>> analysis( |
||||
@RequestParam(required = false) Long deptId, |
||||
@RequestParam(required = false) String examineBy, |
||||
@RequestParam(required = false) String startTime, |
||||
@RequestParam(required = false) String endTime |
||||
) throws ParseException { |
||||
Map<String, Object> result = new HashMap<>(8); |
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
||||
|
||||
// 时间范围:有传参用传参,否则本月
|
||||
Date rangeStart, rangeEnd, lastStart = null, lastEnd = null; |
||||
boolean useDefaultRange; |
||||
if (Func.isNotBlank(startTime) && Func.isNotBlank(endTime)) { |
||||
rangeStart = sdf.parse(startTime); |
||||
rangeEnd = sdf.parse(endTime); |
||||
useDefaultRange = false; |
||||
} else { |
||||
Calendar cal = Calendar.getInstance(); |
||||
cal.set(Calendar.DAY_OF_MONTH, 1); |
||||
cal.set(Calendar.HOUR_OF_DAY, 0); |
||||
cal.set(Calendar.MINUTE, 0); |
||||
cal.set(Calendar.SECOND, 0); |
||||
cal.set(Calendar.MILLISECOND, 0); |
||||
rangeStart = cal.getTime(); |
||||
|
||||
cal.add(Calendar.MONTH, 1); |
||||
cal.add(Calendar.DAY_OF_MONTH, -1); |
||||
cal.set(Calendar.HOUR_OF_DAY, 23); |
||||
cal.set(Calendar.MINUTE, 59); |
||||
cal.set(Calendar.SECOND, 59); |
||||
rangeEnd = cal.getTime(); |
||||
|
||||
// 上月时间范围(仅默认本月时计算较上月)
|
||||
lastStart = new Date(rangeStart.getTime()); |
||||
lastStart.setMonth(lastStart.getMonth() - 1); |
||||
lastEnd = new Date(rangeEnd.getTime()); |
||||
lastEnd.setMonth(lastEnd.getMonth() - 1); |
||||
useDefaultRange = true; |
||||
} |
||||
|
||||
// ===== 总分配任务 =====
|
||||
LambdaQueryWrapper<Examine> base = baseWrapper(deptId, examineBy, rangeStart, rangeEnd); |
||||
int totalAssigned = examineService.count(base); |
||||
|
||||
// ===== 已完成 =====
|
||||
base = baseWrapper(deptId, examineBy, rangeStart, rangeEnd); |
||||
base.eq(Examine::getIsFinished, "1"); |
||||
int totalFinished = examineService.count(base); |
||||
|
||||
// ===== 平均领取时长 =====
|
||||
base = baseWrapper(deptId, examineBy, rangeStart, rangeEnd); |
||||
base.isNotNull(Examine::getReceiveTime); |
||||
double avgHours = Double.parseDouble(new DecimalFormat("#0.0").format(calcAvgReceiveHours(examineService.list(base)))); |
||||
|
||||
// ===== 上月平均领取时长 =====
|
||||
double changeRate = 0; |
||||
if (useDefaultRange) { |
||||
LambdaQueryWrapper<Examine> lastBase = baseWrapper(deptId, examineBy, lastStart, lastEnd); |
||||
lastBase.isNotNull(Examine::getReceiveTime); |
||||
double lastAvg = Double.parseDouble(new DecimalFormat("#0.0").format(calcAvgReceiveHours(examineService.list(lastBase)))); |
||||
if (lastAvg > 0) { |
||||
changeRate = Double.parseDouble(new DecimalFormat("#0.0").format((avgHours - lastAvg) / lastAvg * 100)); |
||||
} |
||||
} |
||||
|
||||
// ===== 各科室统计 =====
|
||||
QueryWrapper<Examine> deptQuery = new QueryWrapper<>(); |
||||
deptQuery.select("DISTINCT dept_id").eq("is_distribute", 1); |
||||
if (deptId != null) deptQuery.eq("dept_id", deptId); |
||||
if (Func.isNotBlank(examineBy)) deptQuery.in("examine_by", (Object[]) examineBy.split(",")); |
||||
if (rangeStart != null) deptQuery.ge("create_time", rangeStart); |
||||
if (rangeEnd != null) deptQuery.le("create_time", rangeEnd); |
||||
List<Object> deptIds = examineService.getBaseMapper().selectObjs(deptQuery); |
||||
|
||||
DecimalFormat df = new DecimalFormat("#0.0"); |
||||
List<Map<String, Object>> deptStats = new ArrayList<>(); |
||||
for (Object obj : deptIds) { |
||||
Long dId = (Long) obj; |
||||
if (dId == null) continue; |
||||
|
||||
LambdaQueryWrapper<Examine> dw = baseWrapper(dId, examineBy, rangeStart, rangeEnd); |
||||
int deptTotal = examineService.count(dw); |
||||
|
||||
dw = baseWrapper(dId, examineBy, rangeStart, rangeEnd); |
||||
dw.eq(Examine::getIsFinished, "1"); |
||||
int deptFinished = examineService.count(dw); |
||||
|
||||
dw = baseWrapper(dId, examineBy, rangeStart, rangeEnd); |
||||
dw.isNotNull(Examine::getReceiveTime); |
||||
double deptAvgHours = Double.parseDouble(df.format(calcAvgReceiveHours(examineService.list(dw)))); |
||||
|
||||
String deptName = "未知"; |
||||
try { |
||||
deptName = sysClient.getDeptName(dId).getData(); |
||||
} catch (Exception e) { |
||||
log.warn("获取科室名称失败: deptId={}", dId); |
||||
} |
||||
|
||||
Map<String, Object> deptInfo = new HashMap<>(6); |
||||
deptInfo.put("deptId", dId); |
||||
deptInfo.put("deptName", deptName); |
||||
deptInfo.put("total", deptTotal); |
||||
deptInfo.put("finished", deptFinished); |
||||
deptInfo.put("rate", deptTotal > 0 ? Math.round((double) deptFinished / deptTotal * 100) : 0); |
||||
deptInfo.put("avgHours", deptAvgHours); |
||||
deptStats.add(deptInfo); |
||||
} |
||||
|
||||
deptStats.sort(Comparator.comparing(m -> (Long) m.get("deptId"))); |
||||
|
||||
result.put("totalAssigned", totalAssigned); |
||||
result.put("totalFinished", totalFinished); |
||||
result.put("finishRate", totalAssigned > 0 ? Double.parseDouble(df.format((double) totalFinished / totalAssigned * 100)) : 0); |
||||
result.put("avgHours", avgHours); |
||||
result.put("avgHoursChange", changeRate); |
||||
result.put("deptStats", deptStats); |
||||
|
||||
return R.data(result); |
||||
} |
||||
|
||||
/** |
||||
* 构建基础查询条件 |
||||
*/ |
||||
private LambdaQueryWrapper<Examine> baseWrapper(Long deptId, String examineBy, Date start, Date end) { |
||||
LambdaQueryWrapper<Examine> w = new LambdaQueryWrapper<>(); |
||||
w.eq(Examine::getIsDistribute, 1); |
||||
if (deptId != null) w.eq(Examine::getDeptId, deptId); |
||||
if (Func.isNotBlank(examineBy)) w.in(Examine::getExamineBy, (Object[]) examineBy.split(",")); |
||||
if (start != null) w.ge(Examine::getCreateTime, start); |
||||
if (end != null) w.le(Examine::getCreateTime, end); |
||||
return w; |
||||
} |
||||
|
||||
/** |
||||
* 计算平均领取时长(小时) |
||||
*/ |
||||
private double calcAvgReceiveHours(List<Examine> list) { |
||||
if (list == null || list.isEmpty()) return 0; |
||||
long totalMillis = 0; |
||||
for (Examine e : list) { |
||||
if (e.getReceiveTime() != null && e.getCreateTime() != null) { |
||||
totalMillis += e.getReceiveTime().getTime() - e.getCreateTime().getTime(); |
||||
} |
||||
} |
||||
if (totalMillis == 0) return 0; |
||||
return (double) totalMillis / list.size() / (1000 * 60 * 60); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
@PostMapping("/trigger") |
||||
@ApiOperation(value = "手动触发分配", notes = "执行规则匹配并分配任务") |
||||
public R trigger() { |
||||
log.info("===== 手动触发分配开始 ====="); |
||||
try { |
||||
Map<String, Object> result = service.triggerAssignment(); |
||||
return R.data(result); |
||||
} catch (Exception e) { |
||||
log.error("手动触发分配异常", e); |
||||
return R.fail("触发失败: " + e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 导入Excel解析 |
||||
*/ |
||||
@PostMapping("/importExcel/parse") |
||||
@ApiOperation(value = "导入Excel解析", notes = "上传Excel文件,解析并验证样品数据") |
||||
public R<List<SimpleImportRow>> importParseExcel(@ApiParam(value = "Excel文件", required = true) MultipartFile file) { |
||||
try { |
||||
List<SimpleImportRow> rows = service.importParseExcel(file); |
||||
return R.data(rows); |
||||
} catch (Exception e) { |
||||
log.error("导入Excel解析失败", e); |
||||
return R.fail(e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 批量保存导入样品 |
||||
*/ |
||||
@PostMapping("/importExcel/save") |
||||
@ApiOperation(value = "批量保存导入样品", notes = "保存用户确认后的导入样品数据") |
||||
public R<Map<String, Object>> importSaveSimples(@RequestBody List<SimpleImportRow> rows) { |
||||
try { |
||||
Map<String, Object> result = service.importSaveSimples(rows); |
||||
return R.data(result); |
||||
} catch (Exception e) { |
||||
log.error("批量保存导入样品失败", e); |
||||
return R.fail(e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 下载导入模板 |
||||
*/ |
||||
@GetMapping("/importExcel/template") |
||||
@ApiOperation(value = "下载导入模板", notes = "下载样品导入Excel模板") |
||||
public void downloadImportTemplate(HttpServletResponse response) { |
||||
List<SimpleImportExcel> list = new ArrayList<>(); |
||||
SimpleImportExcel example = new SimpleImportExcel(); |
||||
example.setExperieNum("JC2026001"); |
||||
example.setSimpleName("示例样品"); |
||||
example.setCount(1); |
||||
example.setSamplingDate("2026-05-26"); |
||||
example.setCustomerName("示例送检单位"); |
||||
example.setOriginalNum("YS001"); |
||||
example.setSubmittedBy("张三"); |
||||
list.add(example); |
||||
ExcelUtil.export(response, "样品导入模板", "样品数据", list, SimpleImportExcel.class); |
||||
} |
||||
|
||||
@lombok.Data |
||||
public static class ManualAssignDTO { |
||||
private Long entrustId; |
||||
private List<DeptAssignment> assignments; |
||||
} |
||||
|
||||
@lombok.Data |
||||
public static class DeptAssignment { |
||||
private Long deptId; |
||||
private List<Long> examineIds; |
||||
} |
||||
} |
||||
@ -0,0 +1,70 @@ |
||||
package org.springblade.lims.controller; |
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage; |
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; |
||||
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.ThresholdConfig; |
||||
import org.springblade.lims.service.IThresholdConfigService; |
||||
import org.springframework.web.bind.annotation.*; |
||||
|
||||
/** |
||||
* 阈值配置 控制器 |
||||
* |
||||
* @author swj |
||||
* @since 2026-05-26 |
||||
*/ |
||||
@RestController |
||||
@AllArgsConstructor |
||||
@RequestMapping("/thresholdConfig") |
||||
public class ThresholdConfigController extends BladeController { |
||||
|
||||
private final IThresholdConfigService service; |
||||
|
||||
/** |
||||
* 分页列表 |
||||
*/ |
||||
@GetMapping("/list") |
||||
@ApiOperationSupport(order = 2) |
||||
@ApiOperation(value = "分页", notes = "分页") |
||||
public R<IPage<ThresholdConfig>> list(ThresholdConfig entry, Query query) { |
||||
IPage<ThresholdConfig> page = service.page(Condition.getPage(query), Condition.getQueryWrapper(entry)); |
||||
return R.data(page); |
||||
} |
||||
|
||||
/** |
||||
* 新增 |
||||
*/ |
||||
@PostMapping("/insert") |
||||
@ApiOperationSupport(order = 3) |
||||
@ApiOperation(value = "新增", notes = "新增") |
||||
public R insert(@RequestBody ThresholdConfig entry) { |
||||
return R.data(service.save(entry)); |
||||
} |
||||
|
||||
/** |
||||
* 修改 |
||||
*/ |
||||
@PostMapping("/update") |
||||
@ApiOperationSupport(order = 4) |
||||
@ApiOperation(value = "修改", notes = "修改") |
||||
public R update(@RequestBody ThresholdConfig entry) { |
||||
return R.data(service.updateById(entry)); |
||||
} |
||||
|
||||
/** |
||||
* 删除 |
||||
*/ |
||||
@PostMapping("/deleteByIds") |
||||
@ApiOperationSupport(order = 5) |
||||
@ApiOperation(value = "逻辑删除", notes = "传入ids") |
||||
public R deleteByIds(@ApiParam(value = "主键集合", required = true) @RequestParam String ids) { |
||||
return R.status(service.deleteLogic(Func.toLongList(ids))); |
||||
} |
||||
} |
||||
@ -0,0 +1,167 @@ |
||||
package org.springblade.lims.drools; |
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException; |
||||
import com.fasterxml.jackson.databind.JsonNode; |
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.kie.api.KieServices; |
||||
import org.kie.api.builder.KieBuilder; |
||||
import org.kie.api.builder.KieFileSystem; |
||||
import org.kie.api.builder.Message; |
||||
import org.kie.api.runtime.KieContainer; |
||||
import org.kie.api.runtime.KieSession; |
||||
import org.springblade.lims.entry.AssignRule; |
||||
import org.springblade.lims.entry.Examine; |
||||
import org.springblade.lims.entry.ExamineItem; |
||||
import org.springblade.lims.service.IExamineItemService; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
import java.util.*; |
||||
import java.util.stream.Collectors; |
||||
import java.util.stream.StreamSupport; |
||||
|
||||
/** |
||||
* Drools规则引擎服务 — 从DB规则JSON动态生成DRL并执行匹配 |
||||
*/ |
||||
@Slf4j |
||||
@Service |
||||
public class AssignRuleDroolsService { |
||||
|
||||
private static final String DRL_PACKAGE = "org.springblade.lims.drools"; |
||||
private static final String DRL_TEMPLATE = "" + |
||||
"package " + DRL_PACKAGE + ";\n" + |
||||
"import org.springblade.lims.entry.Examine;\n" + |
||||
"import java.util.Set;\n" + |
||||
"global java.util.Set matchedIds;\n" + |
||||
"\n" + |
||||
"rule \"%s\"\n" + |
||||
" when\n" + |
||||
" $e: Examine(isDistribute == null || isDistribute == 0%s)\n" + |
||||
" then\n" + |
||||
" matchedIds.add($e.getId());\n" + |
||||
"end\n"; |
||||
|
||||
private final ObjectMapper objectMapper; |
||||
private final IExamineItemService examineItemService; |
||||
|
||||
public AssignRuleDroolsService(ObjectMapper objectMapper, IExamineItemService examineItemService) { |
||||
this.objectMapper = objectMapper; |
||||
this.examineItemService = examineItemService; |
||||
} |
||||
|
||||
/** |
||||
* 从规则条件JSON生成DRL文本 |
||||
* |
||||
* @param rule 分配规则(含conditions JSON) |
||||
* @param ruleId 规则ID(用于DRL中的规则名称) |
||||
* @return DRL文本 |
||||
*/ |
||||
public String generateDrl(AssignRule rule, String ruleId) { |
||||
String conditions = rule.getConditions(); |
||||
String constraints = ""; |
||||
|
||||
if (conditions != null && !conditions.trim().isEmpty()) { |
||||
try { |
||||
JsonNode root = objectMapper.readTree(conditions); |
||||
constraints = buildConstraints(root); |
||||
} catch (JsonProcessingException e) { |
||||
log.warn("规则[{}]条件JSON解析失败: {}, 将匹配所有未分配记录", ruleId, conditions); |
||||
} |
||||
} |
||||
|
||||
return String.format(DRL_TEMPLATE, "rule-" + ruleId, constraints); |
||||
} |
||||
|
||||
/** |
||||
* 从JSON节点构建DRL约束条件(返回内容插入Examine()括号内,逗号分隔) |
||||
*/ |
||||
private String buildConstraints(JsonNode root) { |
||||
List<String> parts = new ArrayList<>(); |
||||
|
||||
// examineItem: 检测项目ID匹配(JSON存名称,需查表转ID)
|
||||
JsonNode examineItem = root.get("examineItem"); |
||||
if (examineItem != null && examineItem.isArray() && examineItem.size() > 0) { |
||||
List<String> names = StreamSupport.stream(examineItem.spliterator(), false) |
||||
.map(JsonNode::asText) |
||||
.filter(n -> n != null && !n.isEmpty()) |
||||
.collect(Collectors.toList()); |
||||
if (!names.isEmpty()) { |
||||
List<ExamineItem> items = examineItemService.lambdaQuery() |
||||
.in(ExamineItem::getName, names) |
||||
.list(); |
||||
if (!items.isEmpty()) { |
||||
String values = items.stream() |
||||
.map(item -> item.getId() + "L") |
||||
.collect(Collectors.joining(", ")); |
||||
parts.add("examineItemId in (" + values + ")"); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// sampleCountThreshold: 样品数量阈值
|
||||
JsonNode threshold = root.get("sampleCountThreshold"); |
||||
if (threshold != null && threshold.isNumber() && threshold.asInt() > 0) { |
||||
parts.add("simpleCount != null && simpleCount >= " + threshold.asInt()); |
||||
} |
||||
|
||||
if (parts.isEmpty()) return ""; |
||||
return ", " + String.join(", ", parts); |
||||
} |
||||
|
||||
/** |
||||
* 执行DRL规则,返回匹配的Examine记录ID列表 |
||||
* |
||||
* @param drl DRL文本 |
||||
* @param candidates 候选Examine记录 |
||||
* @return 匹配的Examine ID列表(异常时返回空列表) |
||||
*/ |
||||
public List<Long> executeDrl(String drl, List<Examine> candidates) { |
||||
if (candidates == null || candidates.isEmpty()) { |
||||
return Collections.emptyList(); |
||||
} |
||||
|
||||
try { |
||||
KieServices ks = KieServices.Factory.get(); |
||||
KieFileSystem kfs = ks.newKieFileSystem(); |
||||
kfs.write("src/main/resources/" + DRL_PACKAGE.replace('.', '/') + "/rules.drl", drl); |
||||
|
||||
KieBuilder kb = ks.newKieBuilder(kfs).buildAll(); |
||||
|
||||
// 检查编译错误
|
||||
if (kb.getResults().hasMessages(Message.Level.ERROR)) { |
||||
StringBuilder errors = new StringBuilder("DRL编译失败:"); |
||||
for (Message msg : kb.getResults().getMessages(Message.Level.ERROR)) { |
||||
errors.append("\n ").append(msg.getText()); |
||||
} |
||||
log.error(errors.toString()); |
||||
if (log.isDebugEnabled()) { |
||||
log.debug("失败的DRL内容:\n{}", drl); |
||||
} |
||||
return Collections.emptyList(); |
||||
} |
||||
|
||||
KieContainer kc = ks.newKieContainer(kb.getKieModule().getReleaseId()); |
||||
KieSession kSession = kc.newKieSession(); |
||||
|
||||
try { |
||||
Set<Long> matchedIds = new HashSet<>(); |
||||
kSession.setGlobal("matchedIds", matchedIds); |
||||
|
||||
for (Examine candidate : candidates) { |
||||
kSession.insert(candidate); |
||||
} |
||||
|
||||
kSession.fireAllRules(); |
||||
return new ArrayList<>(matchedIds); |
||||
} finally { |
||||
kSession.dispose(); |
||||
} |
||||
} catch (Exception e) { |
||||
log.error("Drools执行异常", e); |
||||
if (log.isDebugEnabled()) { |
||||
log.debug("失败的DRL内容:\n{}", drl); |
||||
} |
||||
return Collections.emptyList(); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,52 @@ |
||||
package org.springblade.lims.excel; |
||||
|
||||
import com.alibaba.excel.annotation.ExcelProperty; |
||||
import com.alibaba.excel.annotation.write.style.ColumnWidth; |
||||
import com.alibaba.excel.annotation.write.style.ContentRowHeight; |
||||
import com.alibaba.excel.annotation.write.style.HeadRowHeight; |
||||
import lombok.Data; |
||||
|
||||
import java.io.Serializable; |
||||
|
||||
/** |
||||
* EasyExcel 模型 — 数据导入模板 |
||||
* |
||||
* @author swj |
||||
*/ |
||||
@Data |
||||
@ColumnWidth(25) |
||||
@HeadRowHeight(20) |
||||
@ContentRowHeight(18) |
||||
public class SimpleImportExcel implements Serializable { |
||||
|
||||
private static final long serialVersionUID = 1L; |
||||
|
||||
@ColumnWidth(20) |
||||
@ExcelProperty("样品编号") |
||||
private String experieNum; |
||||
|
||||
@ColumnWidth(20) |
||||
@ExcelProperty("样品名称") |
||||
private String simpleName; |
||||
|
||||
@ColumnWidth(15) |
||||
@ExcelProperty("样品数量") |
||||
private Integer count; |
||||
|
||||
@ColumnWidth(20) |
||||
@ExcelProperty("采样日期") |
||||
private String samplingDate; |
||||
|
||||
@ColumnWidth(25) |
||||
@ExcelProperty("送检单位") |
||||
private String customerName; |
||||
|
||||
@ColumnWidth(20) |
||||
@ExcelProperty("原始编号") |
||||
private String originalNum; |
||||
|
||||
@ColumnWidth(15) |
||||
@ExcelProperty("送检人") |
||||
private String submittedBy; |
||||
|
||||
} |
||||
@ -0,0 +1,14 @@ |
||||
package org.springblade.lims.mapper; |
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
||||
import org.springblade.lims.entry.AlertRecord; |
||||
|
||||
/** |
||||
* 预警记录 Mapper |
||||
* |
||||
* @author swj |
||||
* @since 2026-05-26 |
||||
*/ |
||||
public interface AlertRecordMapper extends BaseMapper<AlertRecord> { |
||||
|
||||
} |
||||
@ -0,0 +1,10 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
<mapper namespace="org.springblade.lims.mapper.AlertRecordMapper"> |
||||
|
||||
<!-- 通用查询映射结果 --> |
||||
<resultMap id="baseResultMap" type="org.springblade.lims.entry.AlertRecord"> |
||||
<id column="id" property="id"/> |
||||
</resultMap> |
||||
|
||||
</mapper> |
||||
@ -0,0 +1,7 @@ |
||||
package org.springblade.lims.mapper; |
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
||||
import org.springblade.lims.entry.AssignRuleLog; |
||||
|
||||
public interface AssignRuleLogMapper extends BaseMapper<AssignRuleLog> { |
||||
} |
||||
@ -0,0 +1,14 @@ |
||||
|
||||
package org.springblade.lims.mapper; |
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
||||
import org.springblade.lims.entry.AssignRule; |
||||
|
||||
/** |
||||
* |
||||
* @author swj |
||||
* @since 2026-05-22 |
||||
*/ |
||||
public interface AssignRuleMapper extends BaseMapper<AssignRule> { |
||||
|
||||
} |
||||
@ -0,0 +1,14 @@ |
||||
package org.springblade.lims.mapper; |
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
||||
import org.springblade.lims.entry.ThresholdConfig; |
||||
|
||||
/** |
||||
* 阈值配置 Mapper |
||||
* |
||||
* @author swj |
||||
* @since 2026-05-26 |
||||
*/ |
||||
public interface ThresholdConfigMapper extends BaseMapper<ThresholdConfig> { |
||||
|
||||
} |
||||
@ -0,0 +1,10 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
<mapper namespace="org.springblade.lims.mapper.ThresholdConfigMapper"> |
||||
|
||||
<!-- 通用查询映射结果 --> |
||||
<resultMap id="baseResultMap" type="org.springblade.lims.entry.ThresholdConfig"> |
||||
<id column="id" property="id"/> |
||||
</resultMap> |
||||
|
||||
</mapper> |
||||
@ -0,0 +1,24 @@ |
||||
package org.springblade.lims.service; |
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService; |
||||
import org.springblade.lims.entry.AlertRecord; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* 预警记录 服务类 |
||||
* |
||||
* @author swj |
||||
* @since 2026-05-26 |
||||
*/ |
||||
public interface IAlertRecordService extends IService<AlertRecord> { |
||||
|
||||
/** |
||||
* 逻辑删除 |
||||
* |
||||
* @param ids 主键集合 |
||||
* @return boolean |
||||
*/ |
||||
boolean deleteLogic(List<Long> ids); |
||||
|
||||
} |
||||
@ -0,0 +1,9 @@ |
||||
package org.springblade.lims.service; |
||||
|
||||
import org.springblade.core.mp.base.BaseService; |
||||
import org.springblade.lims.entry.AssignRuleLog; |
||||
|
||||
public interface IAssignRuleLogService extends BaseService<AssignRuleLog> { |
||||
|
||||
void saveLog(AssignRuleLog log); |
||||
} |
||||
@ -0,0 +1,40 @@ |
||||
|
||||
package org.springblade.lims.service; |
||||
|
||||
import org.springblade.core.mp.base.BaseService; |
||||
import org.springblade.lims.api.SimpleImportRow; |
||||
import org.springblade.lims.entry.AssignRule; |
||||
import org.springframework.web.multipart.MultipartFile; |
||||
|
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* |
||||
* @author swj |
||||
* @since 2026-05-22 |
||||
*/ |
||||
public interface IAssignRuleService extends BaseService<AssignRule> { |
||||
|
||||
List<AssignRule> listEnabledRules(); |
||||
|
||||
/** |
||||
* 触发规则分配 — 执行Drools匹配并分配任务到目标科室 |
||||
* @return 匹配结果(totalMatched, totalAssigned, ruleCount) |
||||
*/ |
||||
Map<String, Object> triggerAssignment(); |
||||
|
||||
/** |
||||
* 解析导入Excel — 读取文件、逐行验证、返回解析结果 |
||||
* @param file Excel文件 |
||||
* @return 解析后的行列表(含验证状态) |
||||
*/ |
||||
List<SimpleImportRow> importParseExcel(MultipartFile file); |
||||
|
||||
/** |
||||
* 批量保存导入样品 — 创建Entrust + 批量插入Simple记录 |
||||
* @param rows 用户确认后的导入行 |
||||
* @return 结果Map(totalRows, savedCount, skippedCount, entrustId, entrustNum) |
||||
*/ |
||||
Map<String, Object> importSaveSimples(List<SimpleImportRow> rows); |
||||
} |
||||
@ -0,0 +1,14 @@ |
||||
package org.springblade.lims.service; |
||||
|
||||
import org.springblade.core.mp.base.BaseService; |
||||
import org.springblade.lims.entry.ThresholdConfig; |
||||
|
||||
/** |
||||
* 阈值配置 服务类 |
||||
* |
||||
* @author swj |
||||
* @since 2026-05-26 |
||||
*/ |
||||
public interface IThresholdConfigService extends BaseService<ThresholdConfig> { |
||||
|
||||
} |
||||
@ -0,0 +1,24 @@ |
||||
package org.springblade.lims.service.impl; |
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
||||
import org.springblade.lims.entry.AlertRecord; |
||||
import org.springblade.lims.mapper.AlertRecordMapper; |
||||
import org.springblade.lims.service.IAlertRecordService; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* 预警记录 服务实现类 |
||||
* |
||||
* @author swj |
||||
* @since 2026-05-26 |
||||
*/ |
||||
@Service |
||||
public class AlertRecordServiceImpl extends ServiceImpl<AlertRecordMapper, AlertRecord> implements IAlertRecordService { |
||||
|
||||
@Override |
||||
public boolean deleteLogic(List<Long> ids) { |
||||
return baseMapper.deleteBatchIds(ids) > 0; |
||||
} |
||||
} |
||||
@ -0,0 +1,16 @@ |
||||
package org.springblade.lims.service.impl; |
||||
|
||||
import org.springblade.core.mp.base.BaseServiceImpl; |
||||
import org.springblade.lims.entry.AssignRuleLog; |
||||
import org.springblade.lims.mapper.AssignRuleLogMapper; |
||||
import org.springblade.lims.service.IAssignRuleLogService; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
@Service |
||||
public class AssignRuleLogServiceImpl extends BaseServiceImpl<AssignRuleLogMapper, AssignRuleLog> implements IAssignRuleLogService { |
||||
|
||||
@Override |
||||
public void saveLog(AssignRuleLog log) { |
||||
save(log); |
||||
} |
||||
} |
||||
@ -0,0 +1,473 @@ |
||||
package org.springblade.lims.service.impl; |
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
import com.fasterxml.jackson.core.JsonProcessingException; |
||||
import com.fasterxml.jackson.databind.JsonNode; |
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import lombok.AllArgsConstructor; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springblade.core.excel.util.ExcelUtil; |
||||
import org.springblade.core.mp.base.BaseServiceImpl; |
||||
import org.springblade.core.tool.api.R; |
||||
import org.springblade.lims.api.SimpleImportRow; |
||||
import org.springblade.lims.drools.AssignRuleDroolsService; |
||||
import org.springblade.lims.entry.AssignRule; |
||||
import org.springblade.lims.entry.AssignRuleLog; |
||||
import org.springblade.lims.entry.ETask; |
||||
import org.springblade.lims.entry.Entrust; |
||||
import org.springblade.lims.entry.Examine; |
||||
import org.springblade.lims.entry.Simple; |
||||
import org.springblade.lims.entry.TaskBlueprint; |
||||
import org.springblade.lims.excel.SimpleImportExcel; |
||||
import org.springblade.lims.mapper.AssignRuleMapper; |
||||
import org.springblade.lims.service.IAssignRuleLogService; |
||||
import org.springblade.lims.service.IAssignRuleService; |
||||
import org.springblade.lims.service.IEntrtrustService; |
||||
import org.springblade.lims.service.IExamineService; |
||||
import org.springblade.lims.service.IETaskService; |
||||
import org.springblade.lims.service.ISimpleService; |
||||
import org.springblade.lims.service.ITaskBlueprintService; |
||||
import org.springblade.system.entity.Dept; |
||||
import org.springblade.system.feign.ISysClient; |
||||
import org.apache.commons.lang3.RandomUtils; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Service; |
||||
import org.springframework.transaction.annotation.Transactional; |
||||
import org.springframework.web.multipart.MultipartFile; |
||||
|
||||
import java.util.*; |
||||
import java.util.stream.Collectors; |
||||
import java.util.stream.StreamSupport; |
||||
|
||||
/** |
||||
* |
||||
* @author swj |
||||
* @since 2026-05-22 |
||||
*/ |
||||
@Slf4j |
||||
@Service |
||||
@AllArgsConstructor |
||||
public class AssignRuleServiceImpl extends BaseServiceImpl<AssignRuleMapper, AssignRule> implements IAssignRuleService { |
||||
|
||||
private final AssignRuleDroolsService droolsService; |
||||
private final IExamineService examineService; |
||||
private final IAssignRuleLogService assignRuleLogService; |
||||
private final ObjectMapper objectMapper; |
||||
private final ISimpleService simpleService; |
||||
private final IETaskService etaskService; |
||||
private final ITaskBlueprintService taskBlueprintService; |
||||
private final ISysClient sysClient; |
||||
private final IEntrtrustService entrtrustService; |
||||
|
||||
@Override |
||||
public List<AssignRule> listEnabledRules() { |
||||
return lambdaQuery().eq(AssignRule::getEnabled, 1).orderByAsc(AssignRule::getSort).list(); |
||||
} |
||||
|
||||
@Override |
||||
public Map<String, Object> triggerAssignment() { |
||||
log.info("===== 开始执行规则分配 ====="); |
||||
|
||||
// 1. 加载启用的规则(按sort排序)
|
||||
List<AssignRule> rules = listEnabledRules(); |
||||
if (rules.isEmpty()) { |
||||
log.info("没有启用的分配规则"); |
||||
Map<String, Object> result = new HashMap<>(); |
||||
result.put("totalMatched", 0); |
||||
result.put("totalAssigned", 0); |
||||
result.put("ruleCount", 0); |
||||
return result; |
||||
} |
||||
|
||||
log.info("找到 {} 条启用规则", rules.size()); |
||||
|
||||
// 2. 超时未领重置:将超时未领取的Examine记录重置为未分配,使其可被重新分配
|
||||
int totalTimeoutReset = 0; |
||||
Set<Long> timeoutResetEntrustIds = new HashSet<>(); |
||||
for (AssignRule rule : rules) { |
||||
if (rule.getUnclaimTimeoutHours() != null && rule.getUnclaimTimeoutHours() > 0) { |
||||
Calendar cal = Calendar.getInstance(); |
||||
cal.add(Calendar.HOUR_OF_DAY, -rule.getUnclaimTimeoutHours()); |
||||
Date threshold = cal.getTime(); |
||||
|
||||
List<Examine> timedOut = examineService.list( |
||||
new LambdaQueryWrapper<Examine>() |
||||
.eq(Examine::getDeptId, rule.getTargetDeptId()) |
||||
.eq(Examine::getIsDistribute, 1) |
||||
.isNull(Examine::getReceiveTime) |
||||
.lt(Examine::getUpdateTime, threshold) |
||||
); |
||||
|
||||
if (!timedOut.isEmpty()) { |
||||
log.info("规则[{}]超时未领{}条, 重置为未分配(超时{}h)", rule.getName(), timedOut.size(), rule.getUnclaimTimeoutHours()); |
||||
for (Examine e : timedOut) { |
||||
if (e.getEntrustId() != null) { |
||||
timeoutResetEntrustIds.add(e.getEntrustId()); |
||||
} |
||||
e.setIsDistribute(0); |
||||
e.setDeptId(null); |
||||
e.setLockBy(null); |
||||
e.setLockTime(null); |
||||
} |
||||
examineService.updateBatchById(timedOut); |
||||
totalTimeoutReset += timedOut.size(); |
||||
} |
||||
} |
||||
} |
||||
if (totalTimeoutReset > 0) { |
||||
log.info("超时未领重置完成, 共重置{}条", totalTimeoutReset); |
||||
} |
||||
|
||||
int totalMatched = 0; |
||||
int totalAssigned = 0; |
||||
Set<Long> assignedEntrustIds = new HashSet<>(); |
||||
|
||||
// 3. 逐条执行规则(按sort顺序,第一条匹配即分配)
|
||||
for (AssignRule rule : rules) { |
||||
try { |
||||
// a) 检查科室负载阈值
|
||||
int currentLoad = examineService.count( |
||||
new LambdaQueryWrapper<Examine>() |
||||
.eq(Examine::getDeptId, rule.getTargetDeptId()) |
||||
.eq(Examine::getIsDistribute, 1) |
||||
.ne(Examine::getIsFinished, "1") |
||||
); |
||||
if (rule.getDeptLoadThreshold() != null && currentLoad >= rule.getDeptLoadThreshold()) { |
||||
log.info("规则[{}]跳过: 目标科室负载{}已达阈值{}", |
||||
rule.getName(), currentLoad, rule.getDeptLoadThreshold()); |
||||
|
||||
// 记录跳过日志
|
||||
AssignRuleLog skipLog = new AssignRuleLog(); |
||||
skipLog.setRuleId(rule.getId()); |
||||
skipLog.setRuleName(rule.getName()); |
||||
skipLog.setStatus(0); |
||||
skipLog.setMatchDesc("科室负载" + currentLoad + "已达阈值" + rule.getDeptLoadThreshold()); |
||||
assignRuleLogService.saveLog(skipLog); |
||||
continue; |
||||
} |
||||
|
||||
// b) 查询未分配的Examine记录(isDistribute IS NULL 或 0,且 deptId 为空)
|
||||
List<Examine> candidates = examineService.list( |
||||
new LambdaQueryWrapper<Examine>() |
||||
.eq(Examine::getIsDeleted, 0) |
||||
.and(w -> w.eq(Examine::getIsDistribute, 0) |
||||
.or().isNull(Examine::getIsDistribute)) |
||||
.isNull(Examine::getDeptId) |
||||
); |
||||
if (candidates.isEmpty()) { |
||||
log.info("规则[{}]无待分配记录", rule.getName()); |
||||
continue; |
||||
} |
||||
|
||||
// b2) 检测类型过滤(条件JSON中的investigativeType对应Entrust.investigativeType,Java层预过滤)
|
||||
try { |
||||
String condStr = rule.getConditions(); |
||||
if (condStr != null && !condStr.trim().isEmpty()) { |
||||
JsonNode root = objectMapper.readTree(condStr); |
||||
JsonNode invTypeNode = root.get("investigativeType"); |
||||
if (invTypeNode != null && invTypeNode.isArray() && invTypeNode.size() > 0) { |
||||
Set<String> types = StreamSupport.stream(invTypeNode.spliterator(), false) |
||||
.map(JsonNode::asText) |
||||
.collect(Collectors.toSet()); |
||||
List<Long> entrustIds = candidates.stream() |
||||
.map(Examine::getEntrustId) |
||||
.filter(Objects::nonNull) |
||||
.distinct() |
||||
.collect(Collectors.toList()); |
||||
if (!entrustIds.isEmpty()) { |
||||
List<Entrust> entrusts = entrtrustService.listByIds(entrustIds); |
||||
Map<Long, String> entrustTypeMap = entrusts.stream() |
||||
.collect(Collectors.toMap(Entrust::getId, Entrust::getInvestigativeType, (a, b) -> a)); |
||||
int before = candidates.size(); |
||||
candidates = candidates.stream() |
||||
.filter(e -> e.getEntrustId() != null |
||||
&& types.contains(entrustTypeMap.get(e.getEntrustId()))) |
||||
.collect(Collectors.toList()); |
||||
log.info("规则[{}]检测类型过滤: {}条→{}条", rule.getName(), before, candidates.size()); |
||||
if (candidates.isEmpty()) { |
||||
log.info("规则[{}]无匹配检测类型的待分配记录", rule.getName()); |
||||
continue; |
||||
} |
||||
} else { |
||||
log.info("规则[{}]候选记录无委托单ID, 跳过", rule.getName()); |
||||
continue; |
||||
} |
||||
} |
||||
} |
||||
} catch (JsonProcessingException e) { |
||||
log.warn("规则[{}]条件JSON解析失败: {}", rule.getName(), rule.getConditions()); |
||||
} |
||||
|
||||
// c) 生成DRL并执行Drools匹配
|
||||
String drl = droolsService.generateDrl(rule, String.valueOf(rule.getId())); |
||||
if (log.isDebugEnabled()) { |
||||
log.debug("规则[{}]生成DRL:\n{}", rule.getName(), drl); |
||||
} |
||||
|
||||
List<Long> matchedIds = droolsService.executeDrl(drl, candidates); |
||||
if (matchedIds.isEmpty()) { |
||||
log.info("规则[{}]无匹配记录", rule.getName()); |
||||
continue; |
||||
} |
||||
|
||||
totalMatched += matchedIds.size(); |
||||
|
||||
// d) 更新匹配的Examine记录
|
||||
List<Examine> toUpdate = candidates.stream() |
||||
.filter(e -> matchedIds.contains(e.getId())) |
||||
.collect(Collectors.toList()); |
||||
|
||||
for (Examine examine : toUpdate) { |
||||
examine.setDeptId(rule.getTargetDeptId()); |
||||
examine.setIsDistribute(0); |
||||
examine.setSimpleCurrPlace("接样室"); |
||||
examine.setIsFinished("-1"); |
||||
if (examine.getEntrustId() != null) { |
||||
assignedEntrustIds.add(examine.getEntrustId()); |
||||
} |
||||
|
||||
// 写分配日志
|
||||
AssignRuleLog logEntry = new AssignRuleLog(); |
||||
logEntry.setRuleId(rule.getId()); |
||||
logEntry.setRuleName(rule.getName()); |
||||
logEntry.setExamineId(examine.getId()); |
||||
logEntry.setExperieNum(examine.getExperieNum()); |
||||
logEntry.setSourceDeptId(examine.getDeptId()); |
||||
logEntry.setTargetDeptId(rule.getTargetDeptId()); |
||||
logEntry.setTargetDeptName(rule.getTargetDeptName()); |
||||
logEntry.setStatus(1); |
||||
logEntry.setMatchDesc("Drools匹配成功"); |
||||
assignRuleLogService.saveLog(logEntry); |
||||
} |
||||
|
||||
// e) 创建/查找TaskBlueprint和ETask基础设施
|
||||
Map<Long, List<Examine>> byEntrust = toUpdate.stream() |
||||
.filter(e -> e.getEntrustId() != null) |
||||
.collect(Collectors.groupingBy(Examine::getEntrustId)); |
||||
for (Map.Entry<Long, List<Examine>> entry : byEntrust.entrySet()) { |
||||
Long entrustId = entry.getKey(); |
||||
List<Examine> entrustExamines = entry.getValue(); |
||||
|
||||
// e1) 查找或创建TaskBlueprint
|
||||
TaskBlueprint blueprint = taskBlueprintService.lambdaQuery() |
||||
.eq(TaskBlueprint::getEntrustId, entrustId) |
||||
.one(); |
||||
if (blueprint == null) { |
||||
blueprint = new TaskBlueprint(); |
||||
blueprint.setId(RandomUtils.nextLong()); |
||||
blueprint.setEntrustId(entrustId); |
||||
blueprint.setIsDeleted(0); |
||||
blueprint.setStatus(0); |
||||
blueprint.setCreateTime(new Date()); |
||||
blueprint.setUpdateTime(new Date()); |
||||
taskBlueprintService.save(blueprint); |
||||
} |
||||
|
||||
// e2) 查找或创建ETask (按科室)
|
||||
ETask eTask = etaskService.lambdaQuery() |
||||
.eq(ETask::getTaskBlueprintId, blueprint.getId()) |
||||
.eq(ETask::getDeptId, rule.getTargetDeptId()) |
||||
.one(); |
||||
if (eTask == null) { |
||||
eTask = new ETask(); |
||||
eTask.setId(RandomUtils.nextLong()); |
||||
eTask.setTaskBlueprintId(blueprint.getId()); |
||||
eTask.setDeptId(rule.getTargetDeptId()); |
||||
eTask.setStatus(0); |
||||
eTask.setIsDeleted(0); |
||||
eTask.setCreateTime(new Date()); |
||||
eTask.setUpdateTime(new Date()); |
||||
try { |
||||
R<Dept> deptR = sysClient.getDept(rule.getTargetDeptId()); |
||||
if (deptR != null && deptR.getData() != null) { |
||||
eTask.setWeight(deptR.getData().getSort()); |
||||
} |
||||
} catch (Exception ex) { |
||||
log.warn("获取部门{}排序失败: {}", rule.getTargetDeptId(), ex.getMessage()); |
||||
} |
||||
etaskService.save(eTask); |
||||
} |
||||
|
||||
// e3) 关联Examine到ETask
|
||||
for (Examine examine : entrustExamines) { |
||||
examine.setETaskId(eTask.getId()); |
||||
} |
||||
} |
||||
|
||||
examineService.updateBatchById(toUpdate); |
||||
totalAssigned += toUpdate.size(); |
||||
|
||||
log.info("规则[{}]匹配{}条, 分配{}条", rule.getName(), matchedIds.size(), toUpdate.size()); |
||||
|
||||
} catch (Exception e) { |
||||
log.error("规则[{}]执行异常: {}", rule.getName(), e.getMessage(), e); |
||||
// 单条规则异常不影响后续规则
|
||||
} |
||||
} |
||||
|
||||
// 4. 更新受影响委托单状态:全部已分配→检测中(000),仍有未分配→待计划(2)
|
||||
Set<Long> allAffectedEntrustIds = new HashSet<>(); |
||||
allAffectedEntrustIds.addAll(timeoutResetEntrustIds); |
||||
allAffectedEntrustIds.addAll(assignedEntrustIds); |
||||
|
||||
if (!allAffectedEntrustIds.isEmpty()) { |
||||
for (Long entrustId : allAffectedEntrustIds) { |
||||
long unassignedCount = examineService.count( |
||||
new LambdaQueryWrapper<Examine>() |
||||
.eq(Examine::getEntrustId, entrustId) |
||||
.ne(Examine::getStatus, -1) |
||||
.and(w -> w.eq(Examine::getIsDistribute, 0) |
||||
.or().isNull(Examine::getIsDistribute)) |
||||
); |
||||
Entrust entrust = new Entrust(); |
||||
entrust.setId(entrustId); |
||||
if (unassignedCount > 0) { |
||||
entrust.setEntrustStatus("2"); |
||||
log.info("委托单[{}]仍有{}条未分配, 状态保持待计划", entrustId, unassignedCount); |
||||
} else { |
||||
entrust.setEntrustStatus("000"); |
||||
log.info("委托单[{}]全部已分配, 状态推进至检测中(000)", entrustId); |
||||
} |
||||
entrtrustService.updateById(entrust); |
||||
} |
||||
} |
||||
|
||||
Map<String, Object> result = new HashMap<>(); |
||||
result.put("totalMatched", totalMatched); |
||||
result.put("totalAssigned", totalAssigned); |
||||
result.put("ruleCount", rules.size()); |
||||
log.info("===== 规则分配完成: 匹配{}条, 分配{}条, 规则{}条 =====", |
||||
totalMatched, totalAssigned, rules.size()); |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public List<SimpleImportRow> importParseExcel(MultipartFile file) { |
||||
List<SimpleImportRow> resultRows = new ArrayList<>(); |
||||
try { |
||||
List<SimpleImportExcel> excelList = ExcelUtil.read(file, SimpleImportExcel.class); |
||||
if (excelList == null || excelList.isEmpty()) { |
||||
return resultRows; |
||||
} |
||||
for (int i = 0; i < excelList.size(); i++) { |
||||
SimpleImportExcel row = excelList.get(i); |
||||
SimpleImportRow dto = new SimpleImportRow(); |
||||
dto.setRowIndex(i); |
||||
dto.setExperieNum(row.getExperieNum()); |
||||
dto.setSimpleName(row.getSimpleName()); |
||||
dto.setCount(row.getCount()); |
||||
dto.setSamplingDate(row.getSamplingDate()); |
||||
dto.setCustomerName(row.getCustomerName()); |
||||
dto.setOriginalNum(row.getOriginalNum()); |
||||
dto.setSubmittedBy(row.getSubmittedBy()); |
||||
// 验证
|
||||
String status = "valid"; |
||||
String message = ""; |
||||
if (row.getExperieNum() == null || row.getExperieNum().trim().isEmpty()) { |
||||
status = "error"; |
||||
message = "样品编号不能为空"; |
||||
} else { |
||||
StringBuilder warnMsg = new StringBuilder(); |
||||
if (row.getSimpleName() == null || row.getSimpleName().trim().isEmpty()) { |
||||
warnMsg.append("样品名称未填写; "); |
||||
} |
||||
if (row.getCustomerName() == null || row.getCustomerName().trim().isEmpty()) { |
||||
warnMsg.append("送检单位未填写; "); |
||||
} |
||||
if (row.getSubmittedBy() == null || row.getSubmittedBy().trim().isEmpty()) { |
||||
warnMsg.append("送检人未填写; "); |
||||
} |
||||
if (row.getSamplingDate() == null || row.getSamplingDate().trim().isEmpty()) { |
||||
warnMsg.append("采样日期未填写; "); |
||||
} |
||||
if (warnMsg.length() > 0) { |
||||
status = "warning"; |
||||
message = warnMsg.substring(0, warnMsg.length() - 2); |
||||
} |
||||
} |
||||
dto.setStatus(status); |
||||
dto.setMessage(message); |
||||
resultRows.add(dto); |
||||
} |
||||
} catch (Exception e) { |
||||
log.error("Excel解析失败", e); |
||||
throw new RuntimeException("Excel文件格式错误,请下载模板后重试", e); |
||||
} |
||||
return resultRows; |
||||
} |
||||
|
||||
@Override |
||||
@Transactional(rollbackFor = Exception.class) |
||||
public Map<String, Object> importSaveSimples(List<SimpleImportRow> rows) { |
||||
Map<String, Object> result = new HashMap<>(6); |
||||
if (rows == null || rows.isEmpty()) { |
||||
result.put("totalRows", 0); |
||||
result.put("savedCount", 0); |
||||
result.put("skippedCount", 0); |
||||
result.put("entrustId", null); |
||||
result.put("entrustNum", null); |
||||
return result; |
||||
} |
||||
|
||||
int totalRows = rows.size(); |
||||
// 过滤出非error的行
|
||||
List<SimpleImportRow> validRows = new ArrayList<>(); |
||||
int skippedCount = 0; |
||||
for (SimpleImportRow row : rows) { |
||||
if ("error".equals(row.getStatus())) { |
||||
skippedCount++; |
||||
} else { |
||||
validRows.add(row); |
||||
} |
||||
} |
||||
|
||||
if (validRows.isEmpty()) { |
||||
result.put("totalRows", totalRows); |
||||
result.put("savedCount", 0); |
||||
result.put("skippedCount", skippedCount); |
||||
result.put("entrustId", null); |
||||
result.put("entrustNum", null); |
||||
return result; |
||||
} |
||||
|
||||
// 创建Entrust
|
||||
Entrust entrust = new Entrust(); |
||||
entrust.setEntrustType(99); |
||||
entrust.setAcceptanceNum("IMPORT-" + System.currentTimeMillis()); |
||||
entrust.setEntrustCustomerName(validRows.get(0).getCustomerName()); |
||||
entrust.setSubmittedBy(validRows.get(0).getSubmittedBy()); |
||||
entrust.setSimpleCount(validRows.size()); |
||||
entrust.setEntrustStatus("000"); |
||||
entrust.setCreateTime(new Date()); |
||||
entrust.setUpdateTime(new Date()); |
||||
entrtrustService.save(entrust); |
||||
Long entrustId = entrust.getId(); |
||||
|
||||
// 创建Simple记录
|
||||
List<Simple> simpleList = new ArrayList<>(); |
||||
for (SimpleImportRow row : validRows) { |
||||
Simple simple = new Simple(); |
||||
simple.setEntrustId(entrustId); |
||||
simple.setExperieNum(row.getExperieNum()); |
||||
simple.setSimpleName(row.getSimpleName()); |
||||
simple.setOriginalNum(row.getOriginalNum()); |
||||
if (row.getSamplingDate() != null && !row.getSamplingDate().trim().isEmpty()) { |
||||
try { |
||||
simple.setSamplingDate(new java.text.SimpleDateFormat("yyyy-MM-dd").parse(row.getSamplingDate())); |
||||
} catch (Exception ignored) { |
||||
// 日期解析失败则跳过
|
||||
} |
||||
} |
||||
simple.setIsDistribution(0); |
||||
simpleList.add(simple); |
||||
} |
||||
simpleService.saveBatch(simpleList); |
||||
|
||||
result.put("totalRows", totalRows); |
||||
result.put("savedCount", validRows.size()); |
||||
result.put("skippedCount", skippedCount); |
||||
result.put("entrustId", entrustId); |
||||
result.put("entrustNum", entrust.getAcceptanceNum()); |
||||
return result; |
||||
} |
||||
} |
||||
@ -0,0 +1,20 @@ |
||||
package org.springblade.lims.service.impl; |
||||
|
||||
import lombok.AllArgsConstructor; |
||||
import org.springblade.core.mp.base.BaseServiceImpl; |
||||
import org.springblade.lims.entry.ThresholdConfig; |
||||
import org.springblade.lims.mapper.ThresholdConfigMapper; |
||||
import org.springblade.lims.service.IThresholdConfigService; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
/** |
||||
* 阈值配置 服务实现类 |
||||
* |
||||
* @author swj |
||||
* @since 2026-05-26 |
||||
*/ |
||||
@Service |
||||
@AllArgsConstructor |
||||
public class ThresholdConfigServiceImpl extends BaseServiceImpl<ThresholdConfigMapper, ThresholdConfig> implements IThresholdConfigService { |
||||
|
||||
} |
||||
@ -0,0 +1,122 @@ |
||||
package org.springblade.lims; |
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.springblade.lims.controller.AssignRuleController; |
||||
import org.springblade.lims.entry.AssignRule; |
||||
import org.springblade.lims.service.IAssignRuleService; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; |
||||
import org.springframework.boot.test.mock.mockito.MockBean; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.test.web.servlet.MockMvc; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.Mockito.when; |
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; |
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; |
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||
|
||||
@WebMvcTest(AssignRuleController.class) |
||||
class AssignRuleControllerTest { |
||||
|
||||
@Autowired |
||||
private MockMvc mockMvc; |
||||
|
||||
@Autowired |
||||
private ObjectMapper objectMapper; |
||||
|
||||
@MockBean |
||||
private IAssignRuleService service; |
||||
|
||||
@Test |
||||
void testInsert() throws Exception { |
||||
AssignRule rule = new AssignRule(); |
||||
rule.setName("测试规则"); |
||||
rule.setTargetDeptId(1L); |
||||
rule.setEnabled(1); |
||||
|
||||
when(service.save(any())).thenReturn(true); |
||||
|
||||
mockMvc.perform(post("/assignRule/insert") |
||||
.contentType(MediaType.APPLICATION_JSON) |
||||
.content(objectMapper.writeValueAsString(rule))) |
||||
.andExpect(status().isOk()) |
||||
.andExpect(jsonPath("$.code").value(200)); |
||||
} |
||||
|
||||
@Test |
||||
void testList() throws Exception { |
||||
when(service.page(any(), any())).thenReturn(new Page<>()); |
||||
|
||||
mockMvc.perform(get("/assignRule/list") |
||||
.param("current", "1") |
||||
.param("size", "10")) |
||||
.andExpect(status().isOk()) |
||||
.andExpect(jsonPath("$.code").value(200)) |
||||
.andExpect(jsonPath("$.data.records").isArray()); |
||||
} |
||||
|
||||
@Test |
||||
void testUpdate() throws Exception { |
||||
AssignRule rule = new AssignRule(); |
||||
rule.setId(1L); |
||||
rule.setName("更新规则"); |
||||
|
||||
when(service.updateById(any())).thenReturn(true); |
||||
|
||||
mockMvc.perform(post("/assignRule/update") |
||||
.contentType(MediaType.APPLICATION_JSON) |
||||
.content(objectMapper.writeValueAsString(rule))) |
||||
.andExpect(status().isOk()) |
||||
.andExpect(jsonPath("$.code").value(200)); |
||||
} |
||||
|
||||
@Test |
||||
void testDelete() throws Exception { |
||||
when(service.deleteLogic(any())).thenReturn(true); |
||||
|
||||
mockMvc.perform(post("/assignRule/deleteByIds") |
||||
.param("ids", "1")) |
||||
.andExpect(status().isOk()) |
||||
.andExpect(jsonPath("$.code").value(200)); |
||||
} |
||||
|
||||
@Test |
||||
void testDetail() throws Exception { |
||||
AssignRule rule = new AssignRule(); |
||||
rule.setId(1L); |
||||
rule.setName("测试规则"); |
||||
|
||||
when(service.getById(1L)).thenReturn(rule); |
||||
|
||||
mockMvc.perform(get("/assignRule/detail") |
||||
.param("id", "1")) |
||||
.andExpect(status().isOk()) |
||||
.andExpect(jsonPath("$.code").value(200)) |
||||
.andExpect(jsonPath("$.data.id").value(1)); |
||||
} |
||||
|
||||
@Test |
||||
void testTrigger() throws Exception { |
||||
Map<String, Object> mockResult = new HashMap<>(); |
||||
mockResult.put("totalMatched", 5); |
||||
mockResult.put("totalAssigned", 3); |
||||
mockResult.put("ruleCount", 2); |
||||
|
||||
when(service.triggerAssignment()).thenReturn(mockResult); |
||||
|
||||
mockMvc.perform(post("/assignRule/trigger")) |
||||
.andExpect(status().isOk()) |
||||
.andExpect(jsonPath("$.code").value(200)) |
||||
.andExpect(jsonPath("$.data.totalMatched").value(5)) |
||||
.andExpect(jsonPath("$.data.totalAssigned").value(3)) |
||||
.andExpect(jsonPath("$.data.ruleCount").value(2)); |
||||
} |
||||
} |
||||
@ -0,0 +1,58 @@ |
||||
package org.springblade.lims; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.boot.test.context.SpringBootTest; |
||||
import org.springframework.boot.test.web.client.TestRestTemplate; |
||||
import org.springframework.http.ResponseEntity; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* 规则触发集成测试 — 验证端到端触发流程 |
||||
* <p> |
||||
* 场景: |
||||
* 1. 无规则场景: DB中没有启用规则,返回 totalMatched=0, totalAssigned=0, ruleCount=0 |
||||
* 2. 基础触发: 调用 /assignRule/trigger 返回标准响应结构 |
||||
* 3. 重复触发的幂等性: 连续调用两次返回结构一致 |
||||
* <p> |
||||
* 注意: 需要完整Spring上下文 + 数据库环境才能真实执行分配逻辑。 |
||||
* 在无数据库的测试环境中,默认走"无规则"或空查询路径。 |
||||
*/ |
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) |
||||
class AssignRuleTriggerIntegrationTest { |
||||
|
||||
@Autowired |
||||
private TestRestTemplate restTemplate; |
||||
|
||||
@Test |
||||
void testTrigger_basicInvocation_shouldReturnResultStructure() { |
||||
ResponseEntity<String> response = restTemplate.postForEntity("/assignRule/trigger", null, String.class); |
||||
|
||||
assertThat(response.getStatusCodeValue()).isEqualTo(200); |
||||
assertThat(response.getBody()).contains("totalMatched"); |
||||
assertThat(response.getBody()).contains("totalAssigned"); |
||||
assertThat(response.getBody()).contains("ruleCount"); |
||||
} |
||||
|
||||
@Test |
||||
void testTrigger_noEnabledRules_shouldReturnZeroCounts() { |
||||
// 无数据库环境或数据库中无启用规则 → 返回0
|
||||
ResponseEntity<String> response = restTemplate.postForEntity("/assignRule/trigger", null, String.class); |
||||
|
||||
assertThat(response.getStatusCodeValue()).isEqualTo(200); |
||||
assertThat(response.getBody()).contains("totalMatched"); |
||||
assertThat(response.getBody()).contains("totalAssigned"); |
||||
} |
||||
|
||||
@Test |
||||
void testTrigger_idempotent_shouldNotThrowOnRepeatedCall() { |
||||
// 幂等性: 连续触发不抛异常
|
||||
ResponseEntity<String> first = restTemplate.postForEntity("/assignRule/trigger", null, String.class); |
||||
ResponseEntity<String> second = restTemplate.postForEntity("/assignRule/trigger", null, String.class); |
||||
|
||||
assertThat(first.getStatusCodeValue()).isEqualTo(200); |
||||
assertThat(second.getStatusCodeValue()).isEqualTo(200); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,14 @@ |
||||
package org.springblade.lims; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; |
||||
import org.springframework.boot.test.context.SpringBootTest; |
||||
|
||||
@SpringBootTest |
||||
@AutoConfigureMockMvc |
||||
public class BaseLimsTest { |
||||
@Test |
||||
public void contextLoads() { |
||||
assert true; |
||||
} |
||||
} |
||||
@ -0,0 +1,193 @@ |
||||
package org.springblade.lims.drools; |
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.springblade.lims.entry.AssignRule; |
||||
import org.springblade.lims.entry.Examine; |
||||
import org.springblade.lims.service.IExamineItemService; |
||||
import org.springframework.boot.test.mock.mockito.MockBean; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* AssignRuleDroolsService 单元测试 |
||||
* <p> |
||||
* 覆盖6个场景: |
||||
* 1. generateDrl null条件 |
||||
* 2. generateDrl 空JSON |
||||
* 3. generateDrl sampleSource条件 |
||||
* 4. generateDrl 全部条件 |
||||
* 5. executeDrl 匹配命中 |
||||
* 6. executeDrl 无匹配 |
||||
*/ |
||||
class AssignRuleDroolsServiceTest { |
||||
|
||||
private AssignRuleDroolsService service; |
||||
|
||||
@MockBean |
||||
private IExamineItemService examineItemService; |
||||
|
||||
@BeforeEach |
||||
void setUp() { |
||||
service = new AssignRuleDroolsService(new ObjectMapper(), examineItemService); |
||||
} |
||||
|
||||
// ── DRL生成测试 ──
|
||||
|
||||
@Test |
||||
void generateDrl_nullConditions_shouldProduceBasicDrl() { |
||||
AssignRule rule = new AssignRule(); |
||||
rule.setConditions(null); |
||||
|
||||
String drl = service.generateDrl(rule, "1"); |
||||
|
||||
assertThat(drl).contains("rule \"rule-1\""); |
||||
assertThat(drl).contains("$e: Examine(isDistribute == null || isDistribute == 0)"); |
||||
assertThat(drl).contains("matchedIds.add($e.getId())"); |
||||
// 不应包含额外约束
|
||||
assertThat(drl).doesNotContain("simpleSource in"); |
||||
assertThat(drl).doesNotContain("examineItemId in"); |
||||
assertThat(drl).doesNotContain("simpleCount"); |
||||
} |
||||
|
||||
@Test |
||||
void generateDrl_emptyConditions_shouldProduceBasicDrl() { |
||||
AssignRule rule = new AssignRule(); |
||||
rule.setConditions("{}"); |
||||
|
||||
String drl = service.generateDrl(rule, "2"); |
||||
|
||||
assertThat(drl).contains("rule \"rule-2\""); |
||||
assertThat(drl).contains("$e: Examine(isDistribute == null || isDistribute == 0)"); |
||||
assertThat(drl).doesNotContain("sampleSource in"); |
||||
} |
||||
|
||||
@Test |
||||
void generateDrl_sampleSourceCondition_shouldIncludeSimpleSourceConstraint() { |
||||
AssignRule rule = new AssignRule(); |
||||
rule.setConditions("{\"sampleSource\":[\"血清\",\"全血\"]}"); |
||||
|
||||
String drl = service.generateDrl(rule, "3"); |
||||
|
||||
// 约束现在在Examine()括号内,使用字段名(无$e.前缀)
|
||||
assertThat(drl).contains("Examine(isDistribute == null || isDistribute == 0, simpleSource in (\"血清\", \"全血\"))"); |
||||
assertThat(drl).doesNotContain("examineItemId in"); |
||||
assertThat(drl).doesNotContain("simpleCount"); |
||||
} |
||||
|
||||
@Test |
||||
void generateDrl_allConditions_shouldIncludeAllConstraints() { |
||||
AssignRule rule = new AssignRule(); |
||||
rule.setConditions("{\"sampleSource\":[\"血清\",\"尿样\"],\"examineItem\":[1,2,3],\"sampleCountThreshold\":5}"); |
||||
|
||||
String drl = service.generateDrl(rule, "4"); |
||||
|
||||
assertThat(drl).contains("Examine(isDistribute == null || isDistribute == 0, simpleSource in (\"血清\", \"尿样\"), examineItemId in (1L, 2L, 3L), simpleCount != null && simpleCount >= 5)"); |
||||
} |
||||
|
||||
// ── DRL执行测试 ──
|
||||
|
||||
@Test |
||||
void executeDrl_withMatchingRecords_shouldReturnMatchedIds() { |
||||
// 规则: 匹配所有未分配记录
|
||||
String drl = "" + |
||||
"package org.springblade.lims.drools;\n" + |
||||
"import org.springblade.lims.entry.Examine;\n" + |
||||
"import java.util.Set;\n" + |
||||
"global java.util.Set matchedIds;\n" + |
||||
"\n" + |
||||
"rule \"all-unassigned\"\n" + |
||||
" when\n" + |
||||
" $e: Examine(isDistribute == null || isDistribute == 0)\n" + |
||||
" then\n" + |
||||
" matchedIds.add($e.getId());\n" + |
||||
"end\n"; |
||||
|
||||
Examine matched1 = new Examine(); |
||||
matched1.setId(100L); |
||||
matched1.setIsDistribute(0); |
||||
Examine matched2 = new Examine(); |
||||
matched2.setId(200L); |
||||
matched2.setIsDistribute(null); |
||||
Examine unmatched = new Examine(); |
||||
unmatched.setId(300L); |
||||
unmatched.setIsDistribute(1); |
||||
|
||||
List<Examine> candidates = Arrays.asList(matched1, matched2, unmatched); |
||||
|
||||
List<Long> result = service.executeDrl(drl, candidates); |
||||
|
||||
assertThat(result).containsExactlyInAnyOrder(100L, 200L); |
||||
assertThat(result).doesNotContain(300L); |
||||
} |
||||
|
||||
@Test |
||||
void executeDrl_withNoMatchingRecords_shouldReturnEmpty() { |
||||
String drl = "" + |
||||
"package org.springblade.lims.drools;\n" + |
||||
"import org.springblade.lims.entry.Examine;\n" + |
||||
"import java.util.Set;\n" + |
||||
"global java.util.Set matchedIds;\n" + |
||||
"\n" + |
||||
"rule \"no-match\"\n" + |
||||
" when\n" + |
||||
" $e: Examine(isDistribute == 2)\n" + |
||||
" then\n" + |
||||
" matchedIds.add($e.getId());\n" + |
||||
"end\n"; |
||||
|
||||
Examine e = new Examine(); |
||||
e.setId(1L); |
||||
e.setIsDistribute(0); |
||||
|
||||
List<Long> result = service.executeDrl(drl, Collections.singletonList(e)); |
||||
|
||||
assertThat(result).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void executeDrl_withEmptyCandidates_shouldReturnEmpty() { |
||||
String drl = "" + |
||||
"package org.springblade.lims.drools;\n" + |
||||
"import org.springblade.lims.entry.Examine;\n" + |
||||
"import java.util.Set;\n" + |
||||
"global java.util.Set matchedIds;\n" + |
||||
"\n" + |
||||
"rule \"any\"\n" + |
||||
" when\n" + |
||||
" $e: Examine()\n" + |
||||
" then\n" + |
||||
" matchedIds.add($e.getId());\n" + |
||||
"end\n"; |
||||
|
||||
List<Long> result = service.executeDrl(drl, new ArrayList<>()); |
||||
|
||||
assertThat(result).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void executeDrl_withNullCandidates_shouldReturnEmpty() { |
||||
String drl = "" + |
||||
"package org.springblade.lims.drools;\n" + |
||||
"import org.springblade.lims.entry.Examine;\n" + |
||||
"import java.util.Set;\n" + |
||||
"global java.util.Set matchedIds;\n" + |
||||
"\n" + |
||||
"rule \"any\"\n" + |
||||
" when\n" + |
||||
" $e: Examine()\n" + |
||||
" then\n" + |
||||
" matchedIds.add($e.getId());\n" + |
||||
"end\n"; |
||||
|
||||
List<Long> result = service.executeDrl(drl, null); |
||||
|
||||
assertThat(result).isEmpty(); |
||||
} |
||||
} |
||||
@ -0,0 +1,8 @@ |
||||
spring: |
||||
main: |
||||
allow-bean-definition-overriding: true |
||||
autoconfigure: |
||||
exclude: |
||||
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration |
||||
- org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration |
||||
- org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration |
||||
Loading…
Reference in new issue