feat(cdss): upgrade rule engine with priority, category, execution history and stats
This commit is contained in:
@@ -2,6 +2,9 @@ package com.healthlink.his.web.cdss.appservice;
|
|||||||
|
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public interface ICdssAppService {
|
public interface ICdssAppService {
|
||||||
|
|
||||||
R<?> evaluateRules(Long encounterId, Long patientId, String triggerType, Long departmentId);
|
R<?> evaluateRules(Long encounterId, Long patientId, String triggerType, Long departmentId);
|
||||||
@@ -11,4 +14,10 @@ public interface ICdssAppService {
|
|||||||
R<?> acknowledgeAlert(Long id, String remark);
|
R<?> acknowledgeAlert(Long id, String remark);
|
||||||
|
|
||||||
R<?> getRules(String ruleType, String severity, String keyword);
|
R<?> getRules(String ruleType, String severity, String keyword);
|
||||||
|
|
||||||
|
R<?> getRuleStats();
|
||||||
|
|
||||||
|
R<?> getExecutionHistory(Long ruleId, Long encounterId, Integer page, Integer size);
|
||||||
|
|
||||||
|
R<?> getRulesEnhanced(String ruleType, String severity, String keyword, String category, Integer priority);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package com.healthlink.his.web.cdss.appservice.impl;
|
package com.healthlink.his.web.cdss.appservice.impl;
|
||||||
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
import com.healthlink.his.cdss.domain.CdssAlert;
|
import com.healthlink.his.cdss.domain.CdssAlert;
|
||||||
import com.healthlink.his.cdss.domain.CdssRule;
|
import com.healthlink.his.cdss.domain.CdssRule;
|
||||||
|
import com.healthlink.his.cdss.domain.CdssRuleExecution;
|
||||||
import com.healthlink.his.cdss.service.ICdssAlertService;
|
import com.healthlink.his.cdss.service.ICdssAlertService;
|
||||||
|
import com.healthlink.his.cdss.service.ICdssRuleExecutionService;
|
||||||
import com.healthlink.his.cdss.service.ICdssRuleService;
|
import com.healthlink.his.cdss.service.ICdssRuleService;
|
||||||
import com.healthlink.his.web.cdss.appservice.ICdssAppService;
|
import com.healthlink.his.web.cdss.appservice.ICdssAppService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -23,10 +26,13 @@ public class CdssAppServiceImpl implements ICdssAppService {
|
|||||||
|
|
||||||
private final ICdssRuleService cdssRuleService;
|
private final ICdssRuleService cdssRuleService;
|
||||||
private final ICdssAlertService cdssAlertService;
|
private final ICdssAlertService cdssAlertService;
|
||||||
|
private final ICdssRuleExecutionService cdssRuleExecutionService;
|
||||||
|
|
||||||
public CdssAppServiceImpl(ICdssRuleService cdssRuleService, ICdssAlertService cdssAlertService) {
|
public CdssAppServiceImpl(ICdssRuleService cdssRuleService, ICdssAlertService cdssAlertService,
|
||||||
|
ICdssRuleExecutionService cdssRuleExecutionService) {
|
||||||
this.cdssRuleService = cdssRuleService;
|
this.cdssRuleService = cdssRuleService;
|
||||||
this.cdssAlertService = cdssAlertService;
|
this.cdssAlertService = cdssAlertService;
|
||||||
|
this.cdssRuleExecutionService = cdssRuleExecutionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -38,12 +44,36 @@ public class CdssAppServiceImpl implements ICdssAppService {
|
|||||||
List<CdssAlert> triggeredAlerts = new ArrayList<>();
|
List<CdssAlert> triggeredAlerts = new ArrayList<>();
|
||||||
|
|
||||||
for (CdssRule rule : activeRules) {
|
for (CdssRule rule : activeRules) {
|
||||||
if (matchRule(rule, encounterId, patientId)) {
|
long startTime = System.currentTimeMillis();
|
||||||
CdssAlert alert = buildAlert(rule, encounterId, patientId);
|
boolean matched = false;
|
||||||
cdssAlertService.save(alert);
|
String result = null;
|
||||||
triggeredAlerts.add(alert);
|
try {
|
||||||
log.info("CDSS rule triggered: ruleCode={}, encounterId={}", rule.getRuleCode(), encounterId);
|
matched = matchRule(rule, encounterId, patientId);
|
||||||
|
if (matched) {
|
||||||
|
CdssAlert alert = buildAlert(rule, encounterId, patientId);
|
||||||
|
cdssAlertService.save(alert);
|
||||||
|
triggeredAlerts.add(alert);
|
||||||
|
result = "MATCHED";
|
||||||
|
log.info("CDSS rule triggered: ruleCode={}, encounterId={}", rule.getRuleCode(), encounterId);
|
||||||
|
} else {
|
||||||
|
result = "NOT_MATCHED";
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
result = "ERROR: " + e.getMessage();
|
||||||
|
log.warn("CDSS rule execution error: ruleCode={}, error={}", rule.getRuleCode(), e.getMessage());
|
||||||
}
|
}
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
|
||||||
|
CdssRuleExecution execution = new CdssRuleExecution();
|
||||||
|
execution.setRuleId(rule.getId());
|
||||||
|
execution.setRuleCode(rule.getRuleCode());
|
||||||
|
execution.setEncounterId(encounterId);
|
||||||
|
execution.setPatientId(patientId);
|
||||||
|
execution.setMatched(matched);
|
||||||
|
execution.setExecutionTime(new Date());
|
||||||
|
execution.setExecutionResult(result);
|
||||||
|
execution.setDurationMs((int) duration);
|
||||||
|
cdssRuleExecutionService.save(execution);
|
||||||
}
|
}
|
||||||
|
|
||||||
return R.ok(Map.of(
|
return R.ok(Map.of(
|
||||||
@@ -96,6 +126,35 @@ public class CdssAppServiceImpl implements ICdssAppService {
|
|||||||
return R.ok(rules);
|
return R.ok(rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R<?> getRuleStats() {
|
||||||
|
return R.ok(cdssRuleService.getRuleStats());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R<?> getExecutionHistory(Long ruleId, Long encounterId, Integer page, Integer size) {
|
||||||
|
LambdaQueryWrapper<CdssRuleExecution> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (ruleId != null) {
|
||||||
|
wrapper.eq(CdssRuleExecution::getRuleId, ruleId);
|
||||||
|
}
|
||||||
|
if (encounterId != null) {
|
||||||
|
wrapper.eq(CdssRuleExecution::getEncounterId, encounterId);
|
||||||
|
}
|
||||||
|
wrapper.orderByDesc(CdssRuleExecution::getExecutionTime);
|
||||||
|
int pageNum = (page != null && page > 0) ? page : 1;
|
||||||
|
int pageSize = (size != null && size > 0) ? size : 20;
|
||||||
|
wrapper.last("LIMIT " + pageSize + " OFFSET " + (pageNum - 1) * pageSize);
|
||||||
|
List<CdssRuleExecution> history = cdssRuleExecutionService.list(wrapper);
|
||||||
|
return R.ok(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R<?> getRulesEnhanced(String ruleType, String severity, String keyword,
|
||||||
|
String category, Integer priority) {
|
||||||
|
List<CdssRule> rules = cdssRuleService.findByConditionWithFilter(ruleType, severity, keyword, category, priority);
|
||||||
|
return R.ok(rules);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean matchRule(CdssRule rule, Long encounterId, Long patientId) {
|
private boolean matchRule(CdssRule rule, Long encounterId, Long patientId) {
|
||||||
try {
|
try {
|
||||||
String conditionExpr = rule.getConditionExpr();
|
String conditionExpr = rule.getConditionExpr();
|
||||||
|
|||||||
@@ -54,4 +54,34 @@ public class CdssController {
|
|||||||
@RequestParam(value = "keyword", required = false) String keyword) {
|
@RequestParam(value = "keyword", required = false) String keyword) {
|
||||||
return cdssAppService.getRules(ruleType, severity, keyword);
|
return cdssAppService.getRules(ruleType, severity, keyword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "查询规则列表(增强版-支持优先级/分类)")
|
||||||
|
@PreAuthorize("@ss.hasPermi('infection:cdss:list')")
|
||||||
|
@GetMapping("/rules/enhanced")
|
||||||
|
public R<?> getRulesEnhanced(
|
||||||
|
@RequestParam(value = "ruleType", required = false) String ruleType,
|
||||||
|
@RequestParam(value = "severity", required = false) String severity,
|
||||||
|
@RequestParam(value = "keyword", required = false) String keyword,
|
||||||
|
@RequestParam(value = "category", required = false) String category,
|
||||||
|
@RequestParam(value = "priority", required = false) Integer priority) {
|
||||||
|
return cdssAppService.getRulesEnhanced(ruleType, severity, keyword, category, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取规则统计数据")
|
||||||
|
@PreAuthorize("@ss.hasPermi('infection:cdss:list')")
|
||||||
|
@GetMapping("/rules/stats")
|
||||||
|
public R<?> getRuleStats() {
|
||||||
|
return cdssAppService.getRuleStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取规则执行历史")
|
||||||
|
@PreAuthorize("@ss.hasPermi('infection:cdss:list')")
|
||||||
|
@GetMapping("/rules/history")
|
||||||
|
public R<?> getExecutionHistory(
|
||||||
|
@RequestParam(value = "ruleId", required = false) Long ruleId,
|
||||||
|
@RequestParam(value = "encounterId", required = false) Long encounterId,
|
||||||
|
@RequestParam(value = "page", required = false, defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
|
||||||
|
return cdssAppService.getExecutionHistory(ruleId, encounterId, page, size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
-- V86: CDSS规则引擎升级 - 添加优先级/分类字段 + 规则执行历史
|
||||||
|
|
||||||
|
ALTER TABLE cdss_rule ADD COLUMN IF NOT EXISTS priority INT NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE cdss_rule ADD COLUMN IF NOT EXISTS category VARCHAR(64);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN cdss_rule.priority IS '规则优先级(0普通 1紧急 2最高)';
|
||||||
|
COMMENT ON COLUMN cdss_rule.category IS '规则分类';
|
||||||
|
|
||||||
|
CREATE TABLE cdss_rule_execution (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
rule_id BIGINT NOT NULL,
|
||||||
|
rule_code VARCHAR(64) NOT NULL,
|
||||||
|
encounter_id BIGINT NOT NULL,
|
||||||
|
patient_id BIGINT NOT NULL,
|
||||||
|
matched BOOLEAN DEFAULT FALSE,
|
||||||
|
execution_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
execution_result TEXT,
|
||||||
|
duration_ms INT,
|
||||||
|
tenant_id BIGINT DEFAULT 0,
|
||||||
|
create_by VARCHAR(64),
|
||||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
delete_flag CHAR(1) DEFAULT '0'
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE cdss_rule_execution IS 'CDSS规则执行历史';
|
||||||
|
COMMENT ON COLUMN cdss_rule_execution.id IS '执行记录ID';
|
||||||
|
COMMENT ON COLUMN cdss_rule_execution.rule_id IS '规则ID';
|
||||||
|
COMMENT ON COLUMN cdss_rule_execution.rule_code IS '规则编码';
|
||||||
|
COMMENT ON COLUMN cdss_rule_execution.encounter_id IS '就诊ID';
|
||||||
|
COMMENT ON COLUMN cdss_rule_execution.patient_id IS '患者ID';
|
||||||
|
COMMENT ON COLUMN cdss_rule_execution.matched IS '是否命中';
|
||||||
|
COMMENT ON COLUMN cdss_rule_execution.execution_time IS '执行时间';
|
||||||
|
COMMENT ON COLUMN cdss_rule_execution.execution_result IS '执行结果';
|
||||||
|
COMMENT ON COLUMN cdss_rule_execution.duration_ms IS '执行耗时(毫秒)';
|
||||||
|
|
||||||
|
CREATE INDEX idx_cdss_exec_rule ON cdss_rule_execution(rule_id);
|
||||||
|
CREATE INDEX idx_cdss_exec_encounter ON cdss_rule_execution(encounter_id);
|
||||||
|
CREATE INDEX idx_cdss_exec_patient ON cdss_rule_execution(patient_id);
|
||||||
|
CREATE INDEX idx_cdss_exec_time ON cdss_rule_execution(execution_time);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?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="com.healthlink.his.cdss.mapper.CdssRuleExecutionMapper">
|
||||||
|
|
||||||
|
<resultMap type="com.healthlink.his.cdss.domain.CdssRuleExecution" id="CdssRuleExecutionResult">
|
||||||
|
<id column="id" property="id"/>
|
||||||
|
<result column="rule_id" property="ruleId"/>
|
||||||
|
<result column="rule_code" property="ruleCode"/>
|
||||||
|
<result column="encounter_id" property="encounterId"/>
|
||||||
|
<result column="patient_id" property="patientId"/>
|
||||||
|
<result column="matched" property="matched"/>
|
||||||
|
<result column="execution_time" property="executionTime"/>
|
||||||
|
<result column="execution_result" property="executionResult"/>
|
||||||
|
<result column="duration_ms" property="durationMs"/>
|
||||||
|
<result column="tenant_id" property="tenantId"/>
|
||||||
|
<result column="create_by" property="createBy"/>
|
||||||
|
<result column="create_time" property="createTime"/>
|
||||||
|
<result column="delete_flag" property="deleteFlag"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="Base_Column_List">
|
||||||
|
id, rule_id, rule_code, encounter_id, patient_id,
|
||||||
|
matched, execution_time, execution_result, duration_ms,
|
||||||
|
tenant_id, create_by, create_time, delete_flag
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
<result column="department_id" property="departmentId"/>
|
<result column="department_id" property="departmentId"/>
|
||||||
<result column="status" property="status"/>
|
<result column="status" property="status"/>
|
||||||
<result column="sort_order" property="sortOrder"/>
|
<result column="sort_order" property="sortOrder"/>
|
||||||
|
<result column="priority" property="priority"/>
|
||||||
|
<result column="category" property="category"/>
|
||||||
<result column="tenant_id" property="tenantId"/>
|
<result column="tenant_id" property="tenantId"/>
|
||||||
<result column="create_by" property="createBy"/>
|
<result column="create_by" property="createBy"/>
|
||||||
<result column="create_time" property="createTime"/>
|
<result column="create_time" property="createTime"/>
|
||||||
@@ -26,7 +28,8 @@
|
|||||||
<sql id="Base_Column_List">
|
<sql id="Base_Column_List">
|
||||||
id, rule_code, rule_name, rule_type, severity, trigger_type,
|
id, rule_code, rule_name, rule_type, severity, trigger_type,
|
||||||
condition_expr, action_expr, description, department_id,
|
condition_expr, action_expr, description, department_id,
|
||||||
status, sort_order, tenant_id, create_by, create_time,
|
status, sort_order, priority, category,
|
||||||
|
tenant_id, create_by, create_time,
|
||||||
update_by, update_time, delete_flag
|
update_by, update_time, delete_flag
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
|
|||||||
@@ -42,4 +42,8 @@ public class CdssRule extends HisBaseEntity {
|
|||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
private Integer sortOrder;
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
private Integer priority;
|
||||||
|
|
||||||
|
private String category;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.healthlink.his.cdss.domain;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.core.common.core.domain.HisBaseEntity;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@TableName("cdss_rule_execution")
|
||||||
|
public class CdssRuleExecution extends HisBaseEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long ruleId;
|
||||||
|
|
||||||
|
private String ruleCode;
|
||||||
|
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long encounterId;
|
||||||
|
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long patientId;
|
||||||
|
|
||||||
|
private Boolean matched;
|
||||||
|
|
||||||
|
private Date executionTime;
|
||||||
|
|
||||||
|
private String executionResult;
|
||||||
|
|
||||||
|
private Integer durationMs;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.healthlink.his.cdss.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.healthlink.his.cdss.domain.CdssRuleExecution;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface CdssRuleExecutionMapper extends BaseMapper<CdssRuleExecution> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.healthlink.his.cdss.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.healthlink.his.cdss.domain.CdssRuleExecution;
|
||||||
|
|
||||||
|
public interface ICdssRuleExecutionService extends IService<CdssRuleExecution> {
|
||||||
|
}
|
||||||
@@ -4,10 +4,16 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
|||||||
import com.healthlink.his.cdss.domain.CdssRule;
|
import com.healthlink.his.cdss.domain.CdssRule;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public interface ICdssRuleService extends IService<CdssRule> {
|
public interface ICdssRuleService extends IService<CdssRule> {
|
||||||
|
|
||||||
List<CdssRule> findActiveRules(String triggerType, Long departmentId);
|
List<CdssRule> findActiveRules(String triggerType, Long departmentId);
|
||||||
|
|
||||||
List<CdssRule> findByCondition(String ruleType, String severity, Integer status);
|
List<CdssRule> findByCondition(String ruleType, String severity, Integer status);
|
||||||
|
|
||||||
|
Map<String, Object> getRuleStats();
|
||||||
|
|
||||||
|
List<CdssRule> findByConditionWithFilter(String ruleType, String severity, String keyword,
|
||||||
|
String category, Integer priority);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.healthlink.his.cdss.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.healthlink.his.cdss.domain.CdssRuleExecution;
|
||||||
|
import com.healthlink.his.cdss.mapper.CdssRuleExecutionMapper;
|
||||||
|
import com.healthlink.his.cdss.service.ICdssRuleExecutionService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CdssRuleExecutionServiceImpl extends ServiceImpl<CdssRuleExecutionMapper, CdssRuleExecution> implements ICdssRuleExecutionService {
|
||||||
|
}
|
||||||
@@ -3,15 +3,25 @@ package com.healthlink.his.cdss.service.impl;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.healthlink.his.cdss.domain.CdssRule;
|
import com.healthlink.his.cdss.domain.CdssRule;
|
||||||
|
import com.healthlink.his.cdss.domain.CdssRuleExecution;
|
||||||
import com.healthlink.his.cdss.mapper.CdssRuleMapper;
|
import com.healthlink.his.cdss.mapper.CdssRuleMapper;
|
||||||
|
import com.healthlink.his.cdss.service.ICdssRuleExecutionService;
|
||||||
import com.healthlink.his.cdss.service.ICdssRuleService;
|
import com.healthlink.his.cdss.service.ICdssRuleService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class CdssRuleServiceImpl extends ServiceImpl<CdssRuleMapper, CdssRule> implements ICdssRuleService {
|
public class CdssRuleServiceImpl extends ServiceImpl<CdssRuleMapper, CdssRule> implements ICdssRuleService {
|
||||||
|
|
||||||
|
private final ICdssRuleExecutionService executionService;
|
||||||
|
|
||||||
|
public CdssRuleServiceImpl(ICdssRuleExecutionService executionService) {
|
||||||
|
this.executionService = executionService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<CdssRule> findActiveRules(String triggerType, Long departmentId) {
|
public List<CdssRule> findActiveRules(String triggerType, Long departmentId) {
|
||||||
LambdaQueryWrapper<CdssRule> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<CdssRule> wrapper = new LambdaQueryWrapper<>();
|
||||||
@@ -22,12 +32,38 @@ public class CdssRuleServiceImpl extends ServiceImpl<CdssRuleMapper, CdssRule> i
|
|||||||
if (departmentId != null) {
|
if (departmentId != null) {
|
||||||
wrapper.and(w -> w.isNull(CdssRule::getDepartmentId).or().eq(CdssRule::getDepartmentId, departmentId));
|
wrapper.and(w -> w.isNull(CdssRule::getDepartmentId).or().eq(CdssRule::getDepartmentId, departmentId));
|
||||||
}
|
}
|
||||||
wrapper.orderByAsc(CdssRule::getSortOrder);
|
wrapper.orderByDesc(CdssRule::getPriority).orderByAsc(CdssRule::getSortOrder);
|
||||||
return baseMapper.selectList(wrapper);
|
return baseMapper.selectList(wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<CdssRule> findByCondition(String ruleType, String severity, Integer status) {
|
public List<CdssRule> findByCondition(String ruleType, String severity, Integer status) {
|
||||||
|
return findByConditionWithFilter(ruleType, severity, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getRuleStats() {
|
||||||
|
long totalCount = baseMapper.selectCount(null);
|
||||||
|
long activeCount = baseMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<CdssRule>().eq(CdssRule::getStatus, 1));
|
||||||
|
long totalExecutions = executionService.count();
|
||||||
|
long matchedExecutions = executionService.count(
|
||||||
|
new LambdaQueryWrapper<CdssRuleExecution>().eq(CdssRuleExecution::getMatched, true));
|
||||||
|
|
||||||
|
Map<String, Object> stats = new HashMap<>();
|
||||||
|
stats.put("totalRules", totalCount);
|
||||||
|
stats.put("activeRules", activeCount);
|
||||||
|
stats.put("inactiveRules", totalCount - activeCount);
|
||||||
|
stats.put("totalExecutions", totalExecutions);
|
||||||
|
stats.put("matchedExecutions", matchedExecutions);
|
||||||
|
stats.put("hitRate", totalExecutions > 0
|
||||||
|
? Math.round(matchedExecutions * 10000.0 / totalExecutions) / 100.0 : 0.0);
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CdssRule> findByConditionWithFilter(String ruleType, String severity, String keyword,
|
||||||
|
String category, Integer priority) {
|
||||||
LambdaQueryWrapper<CdssRule> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<CdssRule> wrapper = new LambdaQueryWrapper<>();
|
||||||
if (ruleType != null && !ruleType.isEmpty()) {
|
if (ruleType != null && !ruleType.isEmpty()) {
|
||||||
wrapper.eq(CdssRule::getRuleType, ruleType);
|
wrapper.eq(CdssRule::getRuleType, ruleType);
|
||||||
@@ -35,10 +71,17 @@ public class CdssRuleServiceImpl extends ServiceImpl<CdssRuleMapper, CdssRule> i
|
|||||||
if (severity != null && !severity.isEmpty()) {
|
if (severity != null && !severity.isEmpty()) {
|
||||||
wrapper.eq(CdssRule::getSeverity, severity);
|
wrapper.eq(CdssRule::getSeverity, severity);
|
||||||
}
|
}
|
||||||
if (status != null) {
|
if (category != null && !category.isEmpty()) {
|
||||||
wrapper.eq(CdssRule::getStatus, status);
|
wrapper.eq(CdssRule::getCategory, category);
|
||||||
}
|
}
|
||||||
wrapper.orderByAsc(CdssRule::getSortOrder);
|
if (priority != null) {
|
||||||
|
wrapper.eq(CdssRule::getPriority, priority);
|
||||||
|
}
|
||||||
|
if (keyword != null && !keyword.isEmpty()) {
|
||||||
|
wrapper.and(w -> w.like(CdssRule::getRuleName, keyword)
|
||||||
|
.or().like(CdssRule::getRuleCode, keyword));
|
||||||
|
}
|
||||||
|
wrapper.orderByDesc(CdssRule::getPriority).orderByAsc(CdssRule::getSortOrder);
|
||||||
return baseMapper.selectList(wrapper);
|
return baseMapper.selectList(wrapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,29 @@ export function getCdssRuleList(query) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCdssRuleListEnhanced(query) {
|
||||||
|
return request({
|
||||||
|
url: '/infection/cdss/rules/enhanced',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCdssRuleStats() {
|
||||||
|
return request({
|
||||||
|
url: '/infection/cdss/rules/stats',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCdssRuleHistory(query) {
|
||||||
|
return request({
|
||||||
|
url: '/infection/cdss/rules/history',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function addCdssRule(data) {
|
export function addCdssRule(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/infection/cdss/rules',
|
url: '/infection/cdss/rules',
|
||||||
|
|||||||
@@ -18,16 +18,71 @@
|
|||||||
<el-option label="INFO" value="INFO" />
|
<el-option label="INFO" value="INFO" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="优先级" prop="priority">
|
||||||
|
<el-select v-model="queryParams.priority" placeholder="请选择" clearable style="width: 120px">
|
||||||
|
<el-option label="最高" :value="2" />
|
||||||
|
<el-option label="紧急" :value="1" />
|
||||||
|
<el-option label="普通" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分类" prop="category">
|
||||||
|
<el-input v-model="queryParams.category" placeholder="规则分类" clearable style="width: 140px" />
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="规则名称" prop="keyword">
|
<el-form-item label="规则名称" prop="keyword">
|
||||||
<el-input v-model="queryParams.keyword" placeholder="搜索规则名称" clearable style="width: 180px" @keyup.enter="handleQuery" />
|
<el-input v-model="queryParams.keyword" placeholder="搜索规则名称" clearable style="width: 180px" @keyup.enter="handleQuery" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||||
|
<el-button type="info" icon="DataAnalysis" @click="handleToggleStats">{{ showStats ? '返回列表' : '统计概览' }}</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<vxe-table :data="ruleList" :loading="loading" border stripe height="auto">
|
<div v-if="showStats" class="stats-panel">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" class="stat-card">
|
||||||
|
<div class="stat-value">{{ ruleStats.totalRules || 0 }}</div>
|
||||||
|
<div class="stat-label">规则总数</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" class="stat-card stat-active">
|
||||||
|
<div class="stat-value">{{ ruleStats.activeRules || 0 }}</div>
|
||||||
|
<div class="stat-label">启用规则</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" class="stat-card stat-exec">
|
||||||
|
<div class="stat-value">{{ ruleStats.totalExecutions || 0 }}</div>
|
||||||
|
<div class="stat-label">执行总次数</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" class="stat-card stat-hit">
|
||||||
|
<div class="stat-value">{{ ruleStats.hitRate || 0 }}%</div>
|
||||||
|
<div class="stat-label">命中率</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-divider content-position="left">执行历史</el-divider>
|
||||||
|
<vxe-table :data="executionHistory" :loading="historyLoading" border stripe height="auto" size="small">
|
||||||
|
<vxe-column type="seq" title="序号" width="60" />
|
||||||
|
<vxe-column field="ruleCode" title="规则编码" width="120" />
|
||||||
|
<vxe-column field="encounterId" title="就诊ID" width="100" />
|
||||||
|
<vxe-column field="matched" title="是否命中" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.matched ? 'success' : 'info'" size="small">{{ row.matched ? '命中' : '未命中' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</vxe-column>
|
||||||
|
<vxe-column field="executionResult" title="执行结果" min-width="150" show-overflow />
|
||||||
|
<vxe-column field="durationMs" title="耗时(ms)" width="100" align="center" />
|
||||||
|
<vxe-column field="executionTime" title="执行时间" width="170" />
|
||||||
|
</vxe-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<vxe-table v-else :data="ruleList" :loading="loading" border stripe height="auto">
|
||||||
<vxe-column type="seq" title="序号" width="70" />
|
<vxe-column type="seq" title="序号" width="70" />
|
||||||
<vxe-column field="ruleCode" title="规则编码" width="120" />
|
<vxe-column field="ruleCode" title="规则编码" width="120" />
|
||||||
<vxe-column field="ruleName" title="规则名称" min-width="180" show-overflow />
|
<vxe-column field="ruleName" title="规则名称" min-width="180" show-overflow />
|
||||||
@@ -41,6 +96,12 @@
|
|||||||
<el-tag :type="severityTagType(row.severity)" effect="dark">{{ row.severity }}</el-tag>
|
<el-tag :type="severityTagType(row.severity)" effect="dark">{{ row.severity }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</vxe-column>
|
</vxe-column>
|
||||||
|
<vxe-column field="priority" title="优先级" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="priorityTagType(row.priority)" size="small">{{ priorityLabel(row.priority) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</vxe-column>
|
||||||
|
<vxe-column field="category" title="分类" width="110" show-overflow />
|
||||||
<vxe-column field="conditionExpr" title="条件表达式" min-width="200" show-overflow />
|
<vxe-column field="conditionExpr" title="条件表达式" min-width="200" show-overflow />
|
||||||
<vxe-column field="actionExpr" title="执行动作" min-width="200" show-overflow />
|
<vxe-column field="actionExpr" title="执行动作" min-width="200" show-overflow />
|
||||||
<vxe-column field="status" title="状态" width="80" align="center">
|
<vxe-column field="status" title="状态" width="80" align="center">
|
||||||
@@ -55,16 +116,22 @@
|
|||||||
|
|
||||||
<script setup name="CdssRules">
|
<script setup name="CdssRules">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { getCdssRuleList } from '@/api/cdss/cdssRule'
|
import { getCdssRuleListEnhanced, getCdssRuleStats, getCdssRuleHistory } from '@/api/cdss/cdssRule'
|
||||||
|
|
||||||
const ruleList = ref([])
|
const ruleList = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const showSearch = ref(true)
|
const showSearch = ref(true)
|
||||||
|
const showStats = ref(false)
|
||||||
|
const ruleStats = ref({})
|
||||||
|
const executionHistory = ref([])
|
||||||
|
const historyLoading = ref(false)
|
||||||
|
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
ruleType: '',
|
ruleType: '',
|
||||||
severity: '',
|
severity: '',
|
||||||
keyword: ''
|
keyword: '',
|
||||||
|
category: '',
|
||||||
|
priority: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const severityTagType = (severity) => {
|
const severityTagType = (severity) => {
|
||||||
@@ -72,6 +139,16 @@ const severityTagType = (severity) => {
|
|||||||
return map[severity] || 'info'
|
return map[severity] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const priorityTagType = (priority) => {
|
||||||
|
const map = { 2: 'danger', 1: 'warning', 0: 'info' }
|
||||||
|
return map[priority] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const priorityLabel = (priority) => {
|
||||||
|
const map = { 2: '最高', 1: '紧急', 0: '普通' }
|
||||||
|
return map[priority] || '普通'
|
||||||
|
}
|
||||||
|
|
||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -79,7 +156,9 @@ const getList = async () => {
|
|||||||
if (queryParams.ruleType) params.ruleType = queryParams.ruleType
|
if (queryParams.ruleType) params.ruleType = queryParams.ruleType
|
||||||
if (queryParams.severity) params.severity = queryParams.severity
|
if (queryParams.severity) params.severity = queryParams.severity
|
||||||
if (queryParams.keyword) params.keyword = queryParams.keyword
|
if (queryParams.keyword) params.keyword = queryParams.keyword
|
||||||
const res = await getCdssRuleList(params)
|
if (queryParams.category) params.category = queryParams.category
|
||||||
|
if (queryParams.priority !== undefined && queryParams.priority !== '') params.priority = queryParams.priority
|
||||||
|
const res = await getCdssRuleListEnhanced(params)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
ruleList.value = res.data || []
|
ruleList.value = res.data || []
|
||||||
}
|
}
|
||||||
@@ -88,6 +167,29 @@ const getList = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStats = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getCdssRuleStats()
|
||||||
|
if (res.code === 200) {
|
||||||
|
ruleStats.value = res.data || {}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ruleStats.value = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHistory = async () => {
|
||||||
|
historyLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getCdssRuleHistory({ page: 1, size: 50 })
|
||||||
|
if (res.code === 200) {
|
||||||
|
executionHistory.value = res.data || []
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
historyLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleQuery = () => {
|
const handleQuery = () => {
|
||||||
getList()
|
getList()
|
||||||
}
|
}
|
||||||
@@ -96,10 +198,43 @@ const resetQuery = () => {
|
|||||||
queryParams.ruleType = ''
|
queryParams.ruleType = ''
|
||||||
queryParams.severity = ''
|
queryParams.severity = ''
|
||||||
queryParams.keyword = ''
|
queryParams.keyword = ''
|
||||||
|
queryParams.category = ''
|
||||||
|
queryParams.priority = undefined
|
||||||
handleQuery()
|
handleQuery()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleToggleStats = () => {
|
||||||
|
showStats.value = !showStats.value
|
||||||
|
if (showStats.value) {
|
||||||
|
getStats()
|
||||||
|
getHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getList()
|
getList()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stats-panel {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-card .stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.stat-card .stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.stat-active .stat-value { color: #67c23a; }
|
||||||
|
.stat-exec .stat-value { color: #409eff; }
|
||||||
|
.stat-hit .stat-value { color: #e6a23c; }
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user