diff --git a/lab-service-api/lab-dict-api/src/main/java/org/springblade/system/enums/DictBizEnum.java b/lab-service-api/lab-dict-api/src/main/java/org/springblade/system/enums/DictBizEnum.java index 5f5be0f..10ea81a 100644 --- a/lab-service-api/lab-dict-api/src/main/java/org/springblade/system/enums/DictBizEnum.java +++ b/lab-service-api/lab-dict-api/src/main/java/org/springblade/system/enums/DictBizEnum.java @@ -92,7 +92,11 @@ public enum DictBizEnum { /** * 定时任务 */ - SCHEDULED_TASK("scheduled_task"); + SCHEDULED_TASK("scheduled_task"), + /** + * 自动分配规则 + */ + ASSIGN_RULE_AUTO("assign_rule_auto"); final String name; diff --git a/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/AlertRecord.java b/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/AlertRecord.java new file mode 100644 index 0000000..f601761 --- /dev/null +++ b/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/AlertRecord.java @@ -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; +} diff --git a/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/AssignRule.java b/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/AssignRule.java new file mode 100644 index 0000000..f448aaf --- /dev/null +++ b/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/AssignRule.java @@ -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; +} diff --git a/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/AssignRuleLog.java b/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/AssignRuleLog.java new file mode 100644 index 0000000..caaab4d --- /dev/null +++ b/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/AssignRuleLog.java @@ -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; +} diff --git a/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/Entrust.java b/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/Entrust.java index cde90ed..0e2e419 100644 --- a/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/Entrust.java +++ b/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/Entrust.java @@ -59,6 +59,9 @@ public class Entrust extends BaseEntity implements Serializable { @ExcelProperty("城市") public String city; + // 3.行政区划代码(省市区三级联动最后一级的code) + public String regionCode; + // 3.受检单位地址 @ExcelProperty("养殖场地址(受检单位地址)") public String customeAddress; diff --git a/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/ThresholdConfig.java b/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/ThresholdConfig.java new file mode 100644 index 0000000..eeeccb6 --- /dev/null +++ b/lab-service-api/lab-lims-api/src/main/java/org/springblade/lims/entry/ThresholdConfig.java @@ -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; +} diff --git a/lab-service/lab-lims/pom.xml b/lab-service/lab-lims/pom.xml index fd4e30f..f1d939c 100644 --- a/lab-service/lab-lims/pom.xml +++ b/lab-service/lab-lims/pom.xml @@ -27,6 +27,23 @@ QLExpress 3.2.0 + + + + org.drools + drools-core + 7.73.0.Final + + + org.drools + drools-compiler + 7.73.0.Final + + + org.drools + drools-mvel + 7.73.0.Final + e-iceblue @@ -209,6 +226,18 @@ commons-logging 1.2 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/Scheduled/GlobalScheduledTasks.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/Scheduled/GlobalScheduledTasks.java index e22c66b..dc322c5 100644 --- a/lab-service/lab-lims/src/main/java/org/springblade/lims/Scheduled/GlobalScheduledTasks.java +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/Scheduled/GlobalScheduledTasks.java @@ -2,24 +2,35 @@ package org.springblade.lims.Scheduled; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j; +import org.springblade.lims.entry.AlertRecord; import org.springblade.lims.entry.Entrust; +import org.springblade.lims.entry.Examine; +import org.springblade.lims.entry.ExamineResult; +import org.springblade.lims.entry.ThresholdConfig; +import org.springblade.lims.service.IAlertRecordService; +import org.springblade.lims.service.IAssignRuleService; import org.springblade.lims.service.IEntrtrustService; +import org.springblade.lims.service.IExamineResultService; +import org.springblade.lims.service.IExamineService; +import org.springblade.lims.service.IThresholdConfigService; import org.springblade.system.cache.DictBizCache; import org.springblade.system.enums.DictBizEnum; - +import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.util.*; +import java.util.stream.Collectors; /** * @BelongsProject: project_husbandry_back * @BelongsPackage:org.springblade.lims.Scheduled * @Annotation:定时任务 * @Author:YangMaoFu - * @date :Created in 2023/7/6 14:18 + * @Date :Created in 2023/7/6 14:18 * @CreateTime: 2026-04-16 23:27 * @Description: TODO * @Version: 1.0 @@ -29,9 +40,23 @@ import java.util.*; @Component public class GlobalScheduledTasks { private final IEntrtrustService service; + private final IAssignRuleService assignRuleService; + private final IThresholdConfigService thresholdConfigService; + private final IAlertRecordService alertRecordService; + private final IExamineService examineService; + private final IExamineResultService examineResultService; - public GlobalScheduledTasks(IEntrtrustService service) { + public GlobalScheduledTasks(IEntrtrustService service, IAssignRuleService assignRuleService, + @Lazy IThresholdConfigService thresholdConfigService, + @Lazy IAlertRecordService alertRecordService, + @Lazy IExamineService examineService, + @Lazy IExamineResultService examineResultService) { this.service = service; + this.assignRuleService = assignRuleService; + this.thresholdConfigService = thresholdConfigService; + this.alertRecordService = alertRecordService; + this.examineService = examineService; + this.examineResultService = examineResultService; } /** @@ -93,4 +118,341 @@ public class GlobalScheduledTasks { log.error("===== 报告生成任务异常 =====: {}" ,LocalDateTime.now(), e); } } + + /** + * 每 30 分钟执行自动分配规则 + */ + @Scheduled(cron = "0 0/30 * * * ?") + public void autoAssignRules() { + // ===================== 核心:开关判断 ===================== + String enabled = DictBizCache.getKey(DictBizEnum.ASSIGN_RULE_AUTO.getName(), "openScheduled"); + if ("1".equals(enabled)) { + log.info("===== 自动分配规则任务已关闭,不执行 ====="); + return; + } + log.info("===== 开始执行自动分配规则任务 =====: {}", LocalDateTime.now()); + try { + Map result = assignRuleService.triggerAssignment(); + log.info("自动分配完成: 匹配{}条, 分配{}条, 规则{}条", + result.get("totalMatched"), result.get("totalAssigned"), result.get("ruleCount")); + log.info("===== 自动分配规则任务完成 =====: {}", LocalDateTime.now()); + } catch (Exception e) { + log.error("===== 自动分配规则任务异常 =====: {}", LocalDateTime.now(), e); + } + } + + // ================================================================= + // 预测预警 - 定时检测指标是否超过阈值 + // ================================================================= + + /** + * 每天凌晨2点执行预警检测(定时任务入口) + */ + @Scheduled(cron = "0 0 2 * * ?") + public void checkPredictionWarnings() { + // 开关判断:使用predictionWarning字典,默认关闭 + String enabled = DictBizCache.getKey("predictionWarning", "openScheduled"); + if ("1".equals(enabled)) { + log.info("===== 预测预警任务已关闭,不执行 ====="); + return; + } + log.info("===== 凌晨2点,开始执行预警检测 =====: {}", LocalDateTime.now()); + doCheck(); + } + + /** + * 手动触发预警检测(跳过开关判断) + */ + public void manualCheck() { + log.info("===== 手动触发预警检测 =====: {}", LocalDateTime.now()); + doCheck(); + } + + /** + * 预警检测核心逻辑 + */ + private void doCheck() { + try { + // 查询所有启用的阈值配置 + List configs = thresholdConfigService.lambdaQuery() + .eq(ThresholdConfig::getEnabled, 1) + .eq(ThresholdConfig::getIsDeleted, 0) + .list(); + + if (configs.isEmpty()) { + log.info("===== 没有启用的阈值配置,跳过 ===== "); + return; + } + log.info("查询到启用的阈值配置: {} 条", configs.size()); + + Calendar cal = Calendar.getInstance(); + Date now = cal.getTime(); + + // 本月开始 + 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); + Date monthStart = cal.getTime(); + + // 上月开始 + cal.add(Calendar.MONTH, -1); + Date prevMonthStart = cal.getTime(); + + // 7天前 + cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_YEAR, -7); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + Date sevenDaysAgo = cal.getTime(); + + for (ThresholdConfig config : configs) { + try { + BigDecimal currentValue = null; + switch (config.getIndicatorCode()) { + case "group_positive_rate": + currentValue = calcGroupPositiveRate(config, monthStart, now); + break; + case "monthly_positive_growth": + BigDecimal thisMonthRate = calcGroupPositiveRate(config, monthStart, now); + BigDecimal lastMonthRate = calcGroupPositiveRate(config, prevMonthStart, monthStart); + if (thisMonthRate != null && lastMonthRate != null) { + currentValue = thisMonthRate.subtract(lastMonthRate); + } + break; + case "farm_positive_count": + currentValue = calcFarmPositiveCount(config, monthStart, now); + break; + case "region_new_cases": + currentValue = calcRegionNewCases(config, sevenDaysAgo, now); + break; + default: + log.warn("未知指标编码: {}", config.getIndicatorCode()); + continue; + } + + if (currentValue == null) continue; + + // 判断预警级别 (先用预警线检查,预警线更高) + String alertLevel = null; + BigDecimal thresholdValue = null; + if (currentValue.compareTo(config.getAlertLine()) >= 0) { + alertLevel = "danger"; + thresholdValue = config.getAlertLine(); + } else if (currentValue.compareTo(config.getWarningLine()) >= 0) { + alertLevel = "warning"; + thresholdValue = config.getWarningLine(); + } + + if (alertLevel != null) { + // 构造作用范围名称 + String scopeName = getScopeName(config); + + // 检查当天是否已为同一配置+级别生成过预警记录 + Calendar todayStart = Calendar.getInstance(); + todayStart.set(Calendar.HOUR_OF_DAY, 0); + todayStart.set(Calendar.MINUTE, 0); + todayStart.set(Calendar.SECOND, 0); + todayStart.set(Calendar.MILLISECOND, 0); + + long existingCount = alertRecordService.lambdaQuery() + .eq(AlertRecord::getThresholdConfigId, config.getId()) + .eq(AlertRecord::getAlertLevel, alertLevel) + .ge(AlertRecord::getCreateTime, todayStart.getTime()) + .count(); + + if (existingCount > 0) { + log.info("配置[{}]级别[{}]已生成过预警,跳过", config.getIndicatorName(), alertLevel); + continue; + } + + // 生成预警记录 + AlertRecord record = new AlertRecord(); + record.setThresholdConfigId(config.getId()); + record.setAlertLevel(alertLevel); + record.setIndicatorCode(config.getIndicatorCode()); + record.setIndicatorName(config.getIndicatorName()); + record.setScopeName(scopeName); + record.setCurrentValue(currentValue); + record.setThresholdValue(thresholdValue); + record.setTriggerTime(now); + record.setAlertStatus("untreated"); + alertRecordService.save(record); + + log.info("生成预警记录: 指标[{}], 级别[{}], 当前值[{}], 阈值[{}]", + config.getIndicatorName(), alertLevel, currentValue, thresholdValue); + } + } catch (Exception e) { + log.error("计算指标[{}]异常: {}", config.getIndicatorCode(), e.getMessage(), e); + } + } + log.info("===== 预警检测完成 =====: {}", LocalDateTime.now()); + } catch (Exception e) { + log.error("===== 预警检测任务异常 =====: {}", LocalDateTime.now(), e); + } + } + + /** + * 计算群体阳性率 (%) + * positive_count / total_samples * 100 + */ + private BigDecimal calcGroupPositiveRate(ThresholdConfig config, Date startTime, Date endTime) { + // 构建查询:关联 ExamineResult → Examine → Entrust + List examines = queryExaminesByScope(config, startTime, endTime); + if (examines.isEmpty()) return null; + + Set examineIds = examines.stream().map(Examine::getId).collect(Collectors.toSet()); + if (examineIds.isEmpty()) return null; + + List results = examineResultService.lambdaQuery() + .in(ExamineResult::getExamineId, examineIds) + .list(); + if (results.isEmpty()) return null; + + // Map examineId -> simpleCount + java.util.Map simpleCountMap = examines.stream() + .collect(Collectors.toMap(Examine::getId, Examine::getSimpleCount, (a, b) -> a + b)); + + long totalPositive = 0; + long totalSamples = 0; + + for (ExamineResult result : results) { + // Count positive samples from comma-separated positive_num + String positiveNum = result.getPositiveNum(); + if (positiveNum != null && !positiveNum.trim().isEmpty()) { + String trimmed = positiveNum.trim(); + if (trimmed.endsWith(",")) { + trimmed = trimmed.substring(0, trimmed.length() - 1); + } + if (!trimmed.isEmpty()) { + totalPositive += trimmed.split(",").length; + } + } + + // Get total samples for this examine + Integer simpleCount = simpleCountMap.get(result.getExamineId()); + if (simpleCount != null) { + totalSamples += simpleCount; + } + } + + if (totalSamples == 0) return null; + + return BigDecimal.valueOf(totalPositive * 100.0 / totalSamples) + .setScale(2, BigDecimal.ROUND_HALF_UP); + } + + /** + * 计算单场阳性数 (单个委托单的最大阳性数) + */ + private BigDecimal calcFarmPositiveCount(ThresholdConfig config, Date startTime, Date endTime) { + List examines = queryExaminesByScope(config, startTime, endTime); + if (examines.isEmpty()) return null; + + Set examineIds = examines.stream().map(Examine::getId).collect(Collectors.toSet()); + List results = examineResultService.lambdaQuery() + .in(ExamineResult::getExamineId, examineIds) + .list(); + if (results.isEmpty()) return null; + + // Group by entrustId, sum positives per entrust + java.util.Map> examineByEntrust = examines.stream() + .collect(Collectors.groupingBy(Examine::getEntrustId)); + + long maxPositive = 0; + for (java.util.Map.Entry> entry : examineByEntrust.entrySet()) { + Set eIds = entry.getValue().stream().map(Examine::getId).collect(Collectors.toSet()); + long entrustPositive = 0; + for (ExamineResult result : results) { + if (eIds.contains(result.getExamineId())) { + String positiveNum = result.getPositiveNum(); + if (positiveNum != null && !positiveNum.trim().isEmpty()) { + String trimmed = positiveNum.trim(); + if (trimmed.endsWith(",")) trimmed = trimmed.substring(0, trimmed.length() - 1); + if (!trimmed.isEmpty()) { + entrustPositive += trimmed.split(",").length; + } + } + } + } + if (entrustPositive > maxPositive) { + maxPositive = entrustPositive; + } + } + + return BigDecimal.valueOf(maxPositive); + } + + /** + * 计算区域新增病例数 (最近7天新增阳性样本数) + */ + private BigDecimal calcRegionNewCases(ThresholdConfig config, Date startTime, Date endTime) { + List examines = queryExaminesByScope(config, startTime, endTime); + if (examines.isEmpty()) return BigDecimal.ZERO; + + Set examineIds = examines.stream().map(Examine::getId).collect(Collectors.toSet()); + List results = examineResultService.lambdaQuery() + .in(ExamineResult::getExamineId, examineIds) + .list(); + if (results.isEmpty()) return BigDecimal.ZERO; + + long total = 0; + for (ExamineResult result : results) { + String positiveNum = result.getPositiveNum(); + if (positiveNum != null && !positiveNum.trim().isEmpty()) { + String trimmed = positiveNum.trim(); + if (trimmed.endsWith(",")) trimmed = trimmed.substring(0, trimmed.length() - 1); + if (!trimmed.isEmpty()) { + total += trimmed.split(",").length; + } + } + } + return BigDecimal.valueOf(total); + } + + /** + * 根据作用范围查询Examine记录 + */ + private List queryExaminesByScope(ThresholdConfig config, Date startTime, Date endTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Examine::getIsDeleted, 0); + wrapper.between(Examine::getCreateTime, startTime, endTime); + + // 按作用范围过滤 + if ("city".equals(config.getScopeType()) && config.getScopeValue() != null) { + // 按区域过滤 (通过Entrust的region_code) + List entrusts = service.lambdaQuery() + .eq(Entrust::getRegionCode, config.getScopeValue()) + .eq(Entrust::getIsDeleted, 0) + .list(); + if (!entrusts.isEmpty()) { + Set entrustIds = entrusts.stream().map(Entrust::getId).collect(Collectors.toSet()); + wrapper.in(Examine::getEntrustId, entrustIds); + } else { + return Collections.emptyList(); + } + } else if ("disease".equals(config.getScopeType()) && config.getDiseaseId() != null) { + wrapper.eq(Examine::getExamineItemId, config.getDiseaseId()); + } + // "all" 不额外过滤 + + return examineService.list(wrapper); + } + + /** + * 获取作用范围显示名称 + */ + private String getScopeName(ThresholdConfig config) { + if ("all".equals(config.getScopeType()) || config.getScopeValue() == null) { + return "全省"; + } else if ("city".equals(config.getScopeType())) { + return "地市(" + config.getScopeValue() + ")"; + } else if ("disease".equals(config.getScopeType())) { + return "病种(" + config.getIndicatorName() + ")"; + } + return "未知"; + } } diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/api/SimpleImportRow.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/api/SimpleImportRow.java new file mode 100644 index 0000000..a39884a --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/api/SimpleImportRow.java @@ -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; + +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/controller/AlertRecordController.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/controller/AlertRecordController.java new file mode 100644 index 0000000..78698a6 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/controller/AlertRecordController.java @@ -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> list(AlertRecord entry, Query query, + @RequestParam(required = false) String keyword) { + QueryWrapper wrapper = Condition.getQueryWrapper(entry); + if (Func.isNotBlank(keyword)) { + wrapper.and(w -> w.like("indicator_name", keyword).or().like("scope_name", keyword)); + } + IPage 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 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> summary() { + Map result = new HashMap<>(3); + + // 活跃预警: status='untreated' AND alert_level='danger' + QueryWrapper 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 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 processedWrapper = new QueryWrapper<>(); + processedWrapper.eq("status", "processed"); + int processed = service.count(processedWrapper); + result.put("processed", processed); + + return R.data(result); + } +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/controller/AssignRuleController.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/controller/AssignRuleController.java new file mode 100644 index 0000000..5487464 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/controller/AssignRuleController.java @@ -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> list(AssignRule entry, Query query) { + IPage 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> analysis( + @RequestParam(required = false) Long deptId, + @RequestParam(required = false) String examineBy, + @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime + ) throws ParseException { + Map 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 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 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 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 deptIds = examineService.getBaseMapper().selectObjs(deptQuery); + + DecimalFormat df = new DecimalFormat("#0.0"); + List> deptStats = new ArrayList<>(); + for (Object obj : deptIds) { + Long dId = (Long) obj; + if (dId == null) continue; + + LambdaQueryWrapper 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 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 baseWrapper(Long deptId, String examineBy, Date start, Date end) { + LambdaQueryWrapper 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 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 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> importParseExcel(@ApiParam(value = "Excel文件", required = true) MultipartFile file) { + try { + List 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> importSaveSimples(@RequestBody List rows) { + try { + Map 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 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 assignments; + } + + @lombok.Data + public static class DeptAssignment { + private Long deptId; + private List examineIds; + } +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/controller/ThresholdConfigController.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/controller/ThresholdConfigController.java new file mode 100644 index 0000000..375ca83 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/controller/ThresholdConfigController.java @@ -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> list(ThresholdConfig entry, Query query) { + IPage 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))); + } +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/drools/AssignRuleDroolsService.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/drools/AssignRuleDroolsService.java new file mode 100644 index 0000000..259c5e7 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/drools/AssignRuleDroolsService.java @@ -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 parts = new ArrayList<>(); + + // examineItem: 检测项目ID匹配(JSON存名称,需查表转ID) + JsonNode examineItem = root.get("examineItem"); + if (examineItem != null && examineItem.isArray() && examineItem.size() > 0) { + List names = StreamSupport.stream(examineItem.spliterator(), false) + .map(JsonNode::asText) + .filter(n -> n != null && !n.isEmpty()) + .collect(Collectors.toList()); + if (!names.isEmpty()) { + List 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 executeDrl(String drl, List 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 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(); + } + } +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/excel/SimpleImportExcel.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/excel/SimpleImportExcel.java new file mode 100644 index 0000000..3c4edcc --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/excel/SimpleImportExcel.java @@ -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; + +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/AlertRecordMapper.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/AlertRecordMapper.java new file mode 100644 index 0000000..2c58b3d --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/AlertRecordMapper.java @@ -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 { + +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/AlertRecordMapper.xml b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/AlertRecordMapper.xml new file mode 100644 index 0000000..4d47ed2 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/AlertRecordMapper.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/AssignRuleLogMapper.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/AssignRuleLogMapper.java new file mode 100644 index 0000000..0b1d676 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/AssignRuleLogMapper.java @@ -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 { +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/AssignRuleMapper.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/AssignRuleMapper.java new file mode 100644 index 0000000..794b9cb --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/AssignRuleMapper.java @@ -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 { + +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/ThresholdConfigMapper.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/ThresholdConfigMapper.java new file mode 100644 index 0000000..d693007 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/ThresholdConfigMapper.java @@ -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 { + +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/ThresholdConfigMapper.xml b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/ThresholdConfigMapper.xml new file mode 100644 index 0000000..0cf5360 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/mapper/ThresholdConfigMapper.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/service/IAlertRecordService.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/IAlertRecordService.java new file mode 100644 index 0000000..6d38c7a --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/IAlertRecordService.java @@ -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 { + + /** + * 逻辑删除 + * + * @param ids 主键集合 + * @return boolean + */ + boolean deleteLogic(List ids); + +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/service/IAssignRuleLogService.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/IAssignRuleLogService.java new file mode 100644 index 0000000..fe29440 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/IAssignRuleLogService.java @@ -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 { + + void saveLog(AssignRuleLog log); +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/service/IAssignRuleService.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/IAssignRuleService.java new file mode 100644 index 0000000..b78a150 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/IAssignRuleService.java @@ -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 { + + List listEnabledRules(); + + /** + * 触发规则分配 — 执行Drools匹配并分配任务到目标科室 + * @return 匹配结果(totalMatched, totalAssigned, ruleCount) + */ + Map triggerAssignment(); + + /** + * 解析导入Excel — 读取文件、逐行验证、返回解析结果 + * @param file Excel文件 + * @return 解析后的行列表(含验证状态) + */ + List importParseExcel(MultipartFile file); + + /** + * 批量保存导入样品 — 创建Entrust + 批量插入Simple记录 + * @param rows 用户确认后的导入行 + * @return 结果Map(totalRows, savedCount, skippedCount, entrustId, entrustNum) + */ + Map importSaveSimples(List rows); +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/service/IThresholdConfigService.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/IThresholdConfigService.java new file mode 100644 index 0000000..fc9bddd --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/IThresholdConfigService.java @@ -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 { + +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/AlertRecordServiceImpl.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/AlertRecordServiceImpl.java new file mode 100644 index 0000000..41b3faa --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/AlertRecordServiceImpl.java @@ -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 implements IAlertRecordService { + + @Override + public boolean deleteLogic(List ids) { + return baseMapper.deleteBatchIds(ids) > 0; + } +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/AssignRuleLogServiceImpl.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/AssignRuleLogServiceImpl.java new file mode 100644 index 0000000..186efb6 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/AssignRuleLogServiceImpl.java @@ -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 implements IAssignRuleLogService { + + @Override + public void saveLog(AssignRuleLog log) { + save(log); + } +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/AssignRuleServiceImpl.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/AssignRuleServiceImpl.java new file mode 100644 index 0000000..5fb7cd1 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/AssignRuleServiceImpl.java @@ -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 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 listEnabledRules() { + return lambdaQuery().eq(AssignRule::getEnabled, 1).orderByAsc(AssignRule::getSort).list(); + } + + @Override + public Map triggerAssignment() { + log.info("===== 开始执行规则分配 ====="); + + // 1. 加载启用的规则(按sort排序) + List rules = listEnabledRules(); + if (rules.isEmpty()) { + log.info("没有启用的分配规则"); + Map 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 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 timedOut = examineService.list( + new LambdaQueryWrapper() + .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 assignedEntrustIds = new HashSet<>(); + + // 3. 逐条执行规则(按sort顺序,第一条匹配即分配) + for (AssignRule rule : rules) { + try { + // a) 检查科室负载阈值 + int currentLoad = examineService.count( + new LambdaQueryWrapper() + .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 candidates = examineService.list( + new LambdaQueryWrapper() + .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 types = StreamSupport.stream(invTypeNode.spliterator(), false) + .map(JsonNode::asText) + .collect(Collectors.toSet()); + List entrustIds = candidates.stream() + .map(Examine::getEntrustId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (!entrustIds.isEmpty()) { + List entrusts = entrtrustService.listByIds(entrustIds); + Map 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 matchedIds = droolsService.executeDrl(drl, candidates); + if (matchedIds.isEmpty()) { + log.info("规则[{}]无匹配记录", rule.getName()); + continue; + } + + totalMatched += matchedIds.size(); + + // d) 更新匹配的Examine记录 + List 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> byEntrust = toUpdate.stream() + .filter(e -> e.getEntrustId() != null) + .collect(Collectors.groupingBy(Examine::getEntrustId)); + for (Map.Entry> entry : byEntrust.entrySet()) { + Long entrustId = entry.getKey(); + List 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 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 allAffectedEntrustIds = new HashSet<>(); + allAffectedEntrustIds.addAll(timeoutResetEntrustIds); + allAffectedEntrustIds.addAll(assignedEntrustIds); + + if (!allAffectedEntrustIds.isEmpty()) { + for (Long entrustId : allAffectedEntrustIds) { + long unassignedCount = examineService.count( + new LambdaQueryWrapper() + .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 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 importParseExcel(MultipartFile file) { + List resultRows = new ArrayList<>(); + try { + List 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 importSaveSimples(List rows) { + Map 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 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 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; + } +} diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/TaskBlueprintServiceImpl.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/TaskBlueprintServiceImpl.java index 55bff6e..feaf2f0 100644 --- a/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/TaskBlueprintServiceImpl.java +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/TaskBlueprintServiceImpl.java @@ -335,9 +335,21 @@ public class TaskBlueprintServiceImpl extends BaseServiceImpl unassignedCheck = new LambdaQueryWrapper<>(); + unassignedCheck.eq(Examine::getEntrustId, taskBlueprint.getEntrustId()); + unassignedCheck.isNull(Examine::getDeptId); + unassignedCheck.ne(Examine::getStatus, -1); + long unassignedCount = examineService.count(unassignedCheck); + Entrust entrust = new Entrust(); entrust.setId(taskBlueprint.getEntrustId()); - entrust.setEntrustStatus("111"); + if (unassignedCount > 0) { + entrust.setEntrustStatus("2"); // 待计划(部分检验未分配科室) + } else { + entrust.setEntrustStatus("000"); // 检测中(全部已分配,待领取) + } return entrtrustService.updateById(entrust); } diff --git a/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/ThresholdConfigServiceImpl.java b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/ThresholdConfigServiceImpl.java new file mode 100644 index 0000000..6efdeb2 --- /dev/null +++ b/lab-service/lab-lims/src/main/java/org/springblade/lims/service/impl/ThresholdConfigServiceImpl.java @@ -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 implements IThresholdConfigService { + +} diff --git a/lab-service/lab-lims/src/test/java/org/springblade/lims/AssignRuleControllerTest.java b/lab-service/lab-lims/src/test/java/org/springblade/lims/AssignRuleControllerTest.java new file mode 100644 index 0000000..2dc8904 --- /dev/null +++ b/lab-service/lab-lims/src/test/java/org/springblade/lims/AssignRuleControllerTest.java @@ -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 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)); + } +} diff --git a/lab-service/lab-lims/src/test/java/org/springblade/lims/AssignRuleTriggerIntegrationTest.java b/lab-service/lab-lims/src/test/java/org/springblade/lims/AssignRuleTriggerIntegrationTest.java new file mode 100644 index 0000000..5b48dd5 --- /dev/null +++ b/lab-service/lab-lims/src/test/java/org/springblade/lims/AssignRuleTriggerIntegrationTest.java @@ -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; + +/** + * 规则触发集成测试 — 验证端到端触发流程 + *

+ * 场景: + * 1. 无规则场景: DB中没有启用规则,返回 totalMatched=0, totalAssigned=0, ruleCount=0 + * 2. 基础触发: 调用 /assignRule/trigger 返回标准响应结构 + * 3. 重复触发的幂等性: 连续调用两次返回结构一致 + *

+ * 注意: 需要完整Spring上下文 + 数据库环境才能真实执行分配逻辑。 + * 在无数据库的测试环境中,默认走"无规则"或空查询路径。 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class AssignRuleTriggerIntegrationTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void testTrigger_basicInvocation_shouldReturnResultStructure() { + ResponseEntity 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 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 first = restTemplate.postForEntity("/assignRule/trigger", null, String.class); + ResponseEntity second = restTemplate.postForEntity("/assignRule/trigger", null, String.class); + + assertThat(first.getStatusCodeValue()).isEqualTo(200); + assertThat(second.getStatusCodeValue()).isEqualTo(200); + } + +} diff --git a/lab-service/lab-lims/src/test/java/org/springblade/lims/BaseLimsTest.java b/lab-service/lab-lims/src/test/java/org/springblade/lims/BaseLimsTest.java new file mode 100644 index 0000000..403ece0 --- /dev/null +++ b/lab-service/lab-lims/src/test/java/org/springblade/lims/BaseLimsTest.java @@ -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; + } +} diff --git a/lab-service/lab-lims/src/test/java/org/springblade/lims/drools/AssignRuleDroolsServiceTest.java b/lab-service/lab-lims/src/test/java/org/springblade/lims/drools/AssignRuleDroolsServiceTest.java new file mode 100644 index 0000000..79d94f0 --- /dev/null +++ b/lab-service/lab-lims/src/test/java/org/springblade/lims/drools/AssignRuleDroolsServiceTest.java @@ -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 单元测试 + *

+ * 覆盖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 candidates = Arrays.asList(matched1, matched2, unmatched); + + List 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 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 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 result = service.executeDrl(drl, null); + + assertThat(result).isEmpty(); + } +} diff --git a/lab-service/lab-lims/src/test/resources/application-test.yml b/lab-service/lab-lims/src/test/resources/application-test.yml new file mode 100644 index 0000000..17dc4b7 --- /dev/null +++ b/lab-service/lab-lims/src/test/resources/application-test.yml @@ -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 diff --git a/lab-service/lab-system/src/main/java/org/springblade/system/controller/RegionController.java b/lab-service/lab-system/src/main/java/org/springblade/system/controller/RegionController.java index bcdbe73..fe79a12 100644 --- a/lab-service/lab-system/src/main/java/org/springblade/system/controller/RegionController.java +++ b/lab-service/lab-system/src/main/java/org/springblade/system/controller/RegionController.java @@ -148,6 +148,40 @@ public class RegionController extends BladeController { return R.data(list); } + /** + * 根据区划编码批量查询 + */ + @GetMapping("/list-by-codes") + @ApiOperationSupport(order = 10) + @ApiOperation(value = "批量查询", notes = "传入codes(逗号分隔)") + public R> listByCodes(@RequestParam String codes) { + List list = regionService.list(Wrappers.query().lambda().in(Region::getCode, codes.split(","))); + return R.data(list); + } + + /** + * 获取区划编码的祖先链 + */ + @GetMapping("/ancestor-codes") + @ApiOperationSupport(order = 11) + @ApiOperation(value = "祖先链", notes = "传入code") + public R> ancestorCodes(@RequestParam String code) { + Region region = regionService.getOne(Wrappers.query().lambda().eq(Region::getCode, code)); + if (region == null) { + return R.data(new ArrayList<>()); + } + String ancestors = region.getAncestors(); + String[] parts = ancestors.split(","); + List result = new ArrayList<>(); + for (String part : parts) { + if (!"0".equals(part) && !part.isEmpty()) { + result.add(part); + } + } + result.add(code); + return R.data(result); + } + /** * 导入行政区划数据 */