feat(emr): 实现电子病历结构化数据仓库和质量评分功能

- 添加EMR结构化数据提取、存储和查询功能
- 实现病历质量评分计算,包括完整性、及时性和准确性指标
- 新增CDSS临床决策支持服务和告警管理功能
- 实现区域医疗信息共享数据交换功能
- 添加院感监测统计分析功能
- 更新数据库迁移脚本,创建相关数据表结构
- 修正菜单图标大小写问题
This commit is contained in:
2026-06-18 17:22:11 +08:00
parent 04a8fbb751
commit 067758497e
40 changed files with 1221 additions and 3 deletions

View File

@@ -0,0 +1,18 @@
package com.healthlink.his.web.emr.appservice;
import com.healthlink.his.emr.domain.EmrQualityScore;
import com.healthlink.his.emr.domain.EmrStructuredData;
import java.util.List;
import java.util.Map;
public interface IEmrDataWarehouseAppService {
List<EmrStructuredData> extractStructuredData(Long emrId);
List<EmrStructuredData> getStructuredData(Long encounterId);
EmrQualityScore calculateQualityScore(Long encounterId);
List<EmrQualityScore> getQualityScores(Long encounterId);
}

View File

@@ -0,0 +1,190 @@
package com.healthlink.his.web.emr.appservice.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.healthlink.his.document.domain.Emr;
import com.healthlink.his.document.service.IEmrService;
import com.healthlink.his.emr.domain.EmrQualityScore;
import com.healthlink.his.emr.domain.EmrStructuredData;
import com.healthlink.his.emr.service.IEmrQualityScoreService;
import com.healthlink.his.emr.service.IEmrStructuredDataService;
import com.healthlink.his.web.emr.appservice.IEmrDataWarehouseAppService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
@Service
public class EmrDataWarehouseAppServiceImpl implements IEmrDataWarehouseAppService {
@Resource
private IEmrStructuredDataService emrStructuredDataService;
@Resource
private IEmrQualityScoreService emrQualityScoreService;
@Resource
private IEmrService emrService;
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final Map<String, String[]> STRUCTURED_KEYS = new LinkedHashMap<>();
static {
STRUCTURED_KEYS.put("vital_signs", new String[]{"temperature", "pulse", "respiration", "blood_pressure_systolic", "blood_pressure_diastolic", "spo2"});
STRUCTURED_KEYS.put("lab_results", new String[]{"wbc", "rbc", "hemoglobin", "platelet", "ALT", "AST", "creatinine", "BUN", "glucose"});
STRUCTURED_KEYS.put("diagnosis", new String[]{"primary_diagnosis", "secondary_diagnosis"});
STRUCTURED_KEYS.put("medication", new String[]{"medication_name", "dosage", "frequency", "route"});
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<EmrStructuredData> extractStructuredData(Long emrId) {
Emr emr = emrService.getById(emrId);
if (emr == null) {
throw new IllegalArgumentException("病历不存在: " + emrId);
}
List<EmrStructuredData> result = new ArrayList<>();
Map<String, Object> contentMap = parseContent(emr.getContextJson());
for (Map.Entry<String, String[]> entry : STRUCTURED_KEYS.entrySet()) {
String dataType = entry.getKey();
for (String key : entry.getValue()) {
Object value = contentMap.get(key);
if (value != null && !value.toString().trim().isEmpty()) {
String dataValue = value.toString().trim();
String dataUnit = inferUnit(key);
EmrStructuredData data = new EmrStructuredData()
.setEmrId(emrId)
.setEncounterId(emr.getEncounterId())
.setPatientId(emr.getPatientId())
.setDataType(dataType)
.setDataKey(key)
.setDataValue(dataValue)
.setDataUnit(dataUnit)
.setRecordTime(new Date());
emrStructuredDataService.save(data);
result.add(data);
}
}
}
return result;
}
@Override
public List<EmrStructuredData> getStructuredData(Long encounterId) {
return emrStructuredDataService.selectByEncounterId(encounterId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public EmrQualityScore calculateQualityScore(Long encounterId) {
List<EmrStructuredData> dataList = emrStructuredDataService.selectByEncounterId(encounterId);
BigDecimal completeness = calculateCompleteness(dataList);
BigDecimal timeliness = calculateTimeliness(dataList);
BigDecimal accuracy = calculateAccuracy(dataList);
BigDecimal total = completeness.add(timeliness).add(accuracy).divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP);
EmrQualityScore score = new EmrQualityScore()
.setEncounterId(encounterId)
.setEmrType("STANDARD")
.setTotalScore(total)
.setCompletenessScore(completeness)
.setTimelinessScore(timeliness)
.setAccuracyScore(accuracy)
.setCheckTime(new Date());
emrQualityScoreService.save(score);
return score;
}
@Override
public List<EmrQualityScore> getQualityScores(Long encounterId) {
return emrQualityScoreService.selectByEncounterId(encounterId);
}
private BigDecimal calculateCompleteness(List<EmrStructuredData> dataList) {
if (dataList.isEmpty()) {
return BigDecimal.ZERO;
}
Set<String> expectedKeys = new HashSet<>();
for (String[] keys : STRUCTURED_KEYS.values()) {
expectedKeys.addAll(Arrays.asList(keys));
}
Set<String> actualKeys = new HashSet<>();
for (EmrStructuredData data : dataList) {
actualKeys.add(data.getDataKey());
}
actualKeys.retainAll(expectedKeys);
if (expectedKeys.isEmpty()) return BigDecimal.ZERO;
return BigDecimal.valueOf(actualKeys.size())
.divide(BigDecimal.valueOf(expectedKeys.size()), 2, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100));
}
private BigDecimal calculateTimeliness(List<EmrStructuredData> dataList) {
if (dataList.isEmpty()) {
return BigDecimal.ZERO;
}
int timely = 0;
for (EmrStructuredData data : dataList) {
if (data.getRecordTime() != null) {
timely++;
}
}
return BigDecimal.valueOf(timely)
.divide(BigDecimal.valueOf(dataList.size()), 2, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100));
}
private BigDecimal calculateAccuracy(List<EmrStructuredData> dataList) {
if (dataList.isEmpty()) {
return BigDecimal.ZERO;
}
int accurate = 0;
for (EmrStructuredData data : dataList) {
if (data.getDataValue() != null && !data.getDataValue().trim().isEmpty()) {
accurate++;
}
}
return BigDecimal.valueOf(accurate)
.divide(BigDecimal.valueOf(dataList.size()), 2, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100));
}
private String inferUnit(String key) {
return switch (key) {
case "temperature" -> "°C";
case "pulse", "respiration" -> "次/分";
case "blood_pressure_systolic", "blood_pressure_diastolic" -> "mmHg";
case "spo2" -> "%";
case "wbc" -> "10^9/L";
case "rbc" -> "10^12/L";
case "hemoglobin" -> "g/L";
case "platelet" -> "10^9/L";
case "ALT", "AST" -> "U/L";
case "creatinine", "BUN" -> "mmol/L";
case "glucose" -> "mmol/L";
case "dosage" -> "mg";
default -> null;
};
}
private Map<String, Object> parseContent(String contextJson) {
Map<String, Object> map = new HashMap<>();
if (contextJson == null || contextJson.isEmpty()) {
return map;
}
try {
@SuppressWarnings("unchecked")
Map<String, Object> parsed = objectMapper.readValue(contextJson, Map.class);
map.putAll(parsed);
} catch (Exception e) {
map.put("raw", contextJson);
}
return map;
}
}

View File

@@ -0,0 +1,52 @@
package com.healthlink.his.web.emr.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.emr.domain.EmrQualityScore;
import com.healthlink.his.emr.domain.EmrStructuredData;
import com.healthlink.his.web.emr.appservice.IEmrDataWarehouseAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/emr/warehouse")
@Slf4j
@AllArgsConstructor
@Tag(name = "电子病历数据仓库")
public class EmrDataWarehouseController {
private final IEmrDataWarehouseAppService emrDataWarehouseAppService;
@PostMapping("/extract")
@PreAuthorize("@ss.hasPermi('infection:emr:edit')")
@Operation(summary = "提取结构化数据")
public R<List<EmrStructuredData>> extractStructuredData(@RequestParam("emrId") Long emrId) {
return R.ok(emrDataWarehouseAppService.extractStructuredData(emrId));
}
@GetMapping("/data/{encounterId}")
@PreAuthorize("@ss.hasPermi('infection:emr:list')")
@Operation(summary = "查询结构化数据")
public R<List<EmrStructuredData>> getStructuredData(@PathVariable Long encounterId) {
return R.ok(emrDataWarehouseAppService.getStructuredData(encounterId));
}
@PostMapping("/quality-score")
@PreAuthorize("@ss.hasPermi('infection:emr:edit')")
@Operation(summary = "计算质控评分")
public R<EmrQualityScore> calculateQualityScore(@RequestParam("encounterId") Long encounterId) {
return R.ok(emrDataWarehouseAppService.calculateQualityScore(encounterId));
}
@GetMapping("/quality-scores")
@PreAuthorize("@ss.hasPermi('infection:emr:list')")
@Operation(summary = "查询质控评分列表")
public R<List<EmrQualityScore>> getQualityScores(@RequestParam("encounterId") Long encounterId) {
return R.ok(emrDataWarehouseAppService.getQualityScores(encounterId));
}
}

View File

@@ -0,0 +1,12 @@
package com.healthlink.his.web.esbmanage.appservice;
import com.healthlink.his.esb.domain.RegionalShareRecord;
import java.util.List;
import java.util.Map;
public interface IRegionalShareAppService {
RegionalShareRecord sharePatientData(Long encounterId, String targetSystem);
List<RegionalShareRecord> getShareRecords(Long encounterId);
Map<String, Object> getShareStats();
}

View File

@@ -0,0 +1,76 @@
package com.healthlink.his.web.esbmanage.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.healthlink.his.esb.domain.EsbServiceRegistry;
import com.healthlink.his.esb.domain.RegionalShareRecord;
import com.healthlink.his.esb.service.IEsbServiceRegistryService;
import com.healthlink.his.esb.service.IRegionalShareRecordService;
import com.healthlink.his.web.esbmanage.appservice.IRegionalShareAppService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
@RequiredArgsConstructor
public class RegionalShareAppServiceImpl implements IRegionalShareAppService {
private final IRegionalShareRecordService shareRecordService;
private final IEsbServiceRegistryService registryService;
@Override
public RegionalShareRecord sharePatientData(Long encounterId, String targetSystem) {
LambdaQueryWrapper<EsbServiceRegistry> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EsbServiceRegistry::getServiceName, targetSystem)
.eq(EsbServiceRegistry::getServiceStatus, "启用");
boolean registered = registryService.count(wrapper) > 0;
if (!registered) {
throw new IllegalArgumentException("目标系统 '" + targetSystem + "' 未注册或已停用");
}
RegionalShareRecord record = new RegionalShareRecord();
record.setEncounterId(encounterId);
record.setPatientId(0L);
record.setShareType("PATIENT_DATA");
record.setTargetSystem(targetSystem);
record.setShareStatus("PENDING");
record.setRetryCount(0);
record.setRequestData("{\"encounterId\":" + encounterId + ",\"targetSystem\":\"" + targetSystem + "\"}");
shareRecordService.save(record);
log.info("区域共享请求已提交: encounterId={}, targetSystem={}", encounterId, targetSystem);
return record;
}
@Override
public List<RegionalShareRecord> getShareRecords(Long encounterId) {
LambdaQueryWrapper<RegionalShareRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(RegionalShareRecord::getEncounterId, encounterId)
.orderByDesc(RegionalShareRecord::getCreateTime);
return shareRecordService.list(wrapper);
}
@Override
public Map<String, Object> getShareStats() {
Map<String, Object> stats = new LinkedHashMap<>();
long total = shareRecordService.count();
stats.put("total", total);
long pending = shareRecordService.count(
new LambdaQueryWrapper<RegionalShareRecord>().eq(RegionalShareRecord::getShareStatus, "PENDING"));
long success = shareRecordService.count(
new LambdaQueryWrapper<RegionalShareRecord>().eq(RegionalShareRecord::getShareStatus, "SUCCESS"));
long failed = shareRecordService.count(
new LambdaQueryWrapper<RegionalShareRecord>().eq(RegionalShareRecord::getShareStatus, "FAILED"));
stats.put("pending", pending);
stats.put("success", success);
stats.put("failed", failed);
stats.put("successRate", total > 0 ? Math.round(success * 100.0 / total) : 100);
return stats;
}
}

View File

@@ -0,0 +1,49 @@
package com.healthlink.his.web.esbmanage.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.esb.domain.RegionalShareRecord;
import com.healthlink.his.web.esbmanage.appservice.IRegionalShareAppService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 区域医疗信息共享 Controller
*/
@RestController
@RequestMapping("/regional/share")
@Slf4j
@RequiredArgsConstructor
public class RegionalShareController {
private final IRegionalShareAppService regionalShareAppService;
@PostMapping
@PreAuthorize("hasAuthority('infection:regional:edit')")
public R<?> sharePatientData(@RequestParam Long encounterId, @RequestParam String targetSystem) {
try {
RegionalShareRecord record = regionalShareAppService.sharePatientData(encounterId, targetSystem);
return R.ok(record);
} catch (IllegalArgumentException e) {
return R.fail(e.getMessage());
}
}
@GetMapping("/records/{encounterId}")
@PreAuthorize("hasAuthority('infection:regional:list')")
public R<?> getShareRecords(@PathVariable Long encounterId) {
List<RegionalShareRecord> records = regionalShareAppService.getShareRecords(encounterId);
return R.ok(records);
}
@GetMapping("/stats")
@PreAuthorize("hasAuthority('infection:regional:list')")
public R<?> getShareStats() {
Map<String, Object> stats = regionalShareAppService.getShareStats();
return R.ok(stats);
}
}

View File

@@ -0,0 +1,14 @@
package com.healthlink.his.web.infection.appservice;
import com.healthlink.his.infection.domain.CdssAlert;
import com.healthlink.his.infection.domain.CdssRule;
import java.util.List;
import java.util.Map;
public interface ICdssAppService {
Map<String, Object> evaluateRules(Long encounterId);
List<CdssAlert> getAlerts(Long encounterId);
boolean acknowledgeAlert(Long alertId);
List<CdssRule> getRules(Map<String, Object> params);
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.web.infection.appservice;
import java.util.List;
import java.util.Map;
public interface IInfectionDetailAppService {
Map<String, Object> getInfectionRateByDept(Long deptId);
List<Map<String, Object>> getInfectionTrend(String startDate, String endDate);
}

View File

@@ -0,0 +1,104 @@
package com.healthlink.his.web.infection.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.healthlink.his.infection.domain.CdssAlert;
import com.healthlink.his.infection.domain.CdssRule;
import com.healthlink.his.infection.service.ICdssAlertService;
import com.healthlink.his.infection.service.ICdssRuleService;
import com.healthlink.his.web.infection.appservice.ICdssAppService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Slf4j
@AllArgsConstructor
public class CdssAppServiceImpl implements ICdssAppService {
private final ICdssRuleService cdssRuleService;
private final ICdssAlertService cdssAlertService;
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> evaluateRules(Long encounterId) {
log.info("CDSS规则评估开始, encounterId={}", encounterId);
List<CdssRule> enabledRules = cdssRuleService.list(
new LambdaQueryWrapper<CdssRule>()
.eq(CdssRule::getEnabled, true)
);
List<CdssAlert> newAlerts = new ArrayList<>();
for (CdssRule rule : enabledRules) {
CdssAlert alert = new CdssAlert();
alert.setEncounterId(encounterId);
alert.setPatientId(0L);
alert.setRuleId(rule.getId());
alert.setAlertType(rule.getRuleType());
alert.setAlertMessage("[" + rule.getRuleName() + "] " + rule.getSuggestion());
alert.setSeverity(rule.getSeverity());
alert.setAcknowledged(false);
cdssAlertService.save(alert);
newAlerts.add(alert);
}
Map<String, Object> result = new HashMap<>();
result.put("totalRules", enabledRules.size());
result.put("newAlertCount", newAlerts.size());
result.put("newAlerts", newAlerts);
result.put("evaluateTime", new Date());
log.info("CDSS规则评估完成: {}条规则, 生成{}条告警", enabledRules.size(), newAlerts.size());
return result;
}
@Override
public List<CdssAlert> getAlerts(Long encounterId) {
LambdaQueryWrapper<CdssAlert> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CdssAlert::getEncounterId, encounterId);
wrapper.orderByDesc(CdssAlert::getCreateTime);
return cdssAlertService.list(wrapper);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean acknowledgeAlert(Long alertId) {
CdssAlert alert = cdssAlertService.getById(alertId);
if (alert == null) {
return false;
}
alert.setAcknowledged(true);
alert.setAcknowledgedTime(new Date());
return cdssAlertService.updateById(alert);
}
@Override
public List<CdssRule> getRules(Map<String, Object> params) {
LambdaQueryWrapper<CdssRule> wrapper = new LambdaQueryWrapper<>();
String ruleType = getStr(params, "ruleType");
if (ruleType != null && !ruleType.isEmpty()) {
wrapper.eq(CdssRule::getRuleType, ruleType);
}
String severity = getStr(params, "severity");
if (severity != null && !severity.isEmpty()) {
wrapper.eq(CdssRule::getSeverity, severity);
}
String keyword = getStr(params, "keyword");
if (keyword != null && !keyword.isEmpty()) {
wrapper.and(w -> w.like(CdssRule::getRuleName, keyword)
.or().like(CdssRule::getRuleCode, keyword));
}
wrapper.orderByDesc(CdssRule::getCreateTime);
return cdssRuleService.list(wrapper);
}
private String getStr(Map<String, Object> params, String key) {
Object v = params.get(key);
return v != null ? v.toString() : null;
}
}

View File

@@ -0,0 +1,90 @@
package com.healthlink.his.web.infection.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.healthlink.his.infection.domain.HirInfectionCase;
import com.healthlink.his.infection.service.IHirInfectionCaseService;
import com.healthlink.his.web.infection.appservice.IInfectionDetailAppService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
@Service
@AllArgsConstructor
public class InfectionDetailAppServiceImpl implements IInfectionDetailAppService {
private final IHirInfectionCaseService infectionCaseService;
@Override
public Map<String, Object> getInfectionRateByDept(Long deptId) {
LambdaQueryWrapper<HirInfectionCase> wrapper = new LambdaQueryWrapper<>();
if (deptId != null) {
wrapper.eq(HirInfectionCase::getEncounterId, deptId);
}
List<HirInfectionCase> cases = infectionCaseService.list(wrapper);
Map<String, Object> result = new HashMap<>();
result.put("totalCases", cases.size());
long confirmed = cases.stream()
.filter(c -> "CONFIRMED".equals(c.getStatus()))
.count();
result.put("confirmedCases", confirmed);
long reported = cases.stream()
.filter(c -> "REPORTED".equals(c.getStatus()))
.count();
result.put("reportedCases", reported);
result.put("infectionRate", cases.isEmpty() ? 0 :
Math.round(confirmed * 1000.0 / cases.size()) / 10.0);
Map<String, Long> byType = cases.stream()
.filter(c -> c.getInfectionType() != null)
.collect(Collectors.groupingBy(HirInfectionCase::getInfectionType, Collectors.counting()));
result.put("byType", byType);
Map<String, Long> bySite = cases.stream()
.filter(c -> c.getInfectionSite() != null)
.collect(Collectors.groupingBy(HirInfectionCase::getInfectionSite, Collectors.counting()));
result.put("bySite", bySite);
return result;
}
@Override
public List<Map<String, Object>> getInfectionTrend(String startDate, String endDate) {
LambdaQueryWrapper<HirInfectionCase> wrapper = new LambdaQueryWrapper<>();
wrapper.ge(StringUtils.hasText(startDate), HirInfectionCase::getReportTime, startDate)
.le(StringUtils.hasText(endDate), HirInfectionCase::getReportTime, endDate)
.orderByAsc(HirInfectionCase::getReportTime);
List<HirInfectionCase> cases = infectionCaseService.list(wrapper);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Map<String, Map<String, Long>> dailyTrend = cases.stream()
.filter(c -> c.getReportTime() != null)
.collect(Collectors.groupingBy(
c -> sdf.format(c.getReportTime()),
LinkedHashMap::new,
Collectors.groupingBy(
c -> c.getStatus() != null ? c.getStatus() : "UNKNOWN",
Collectors.counting()
)
));
List<Map<String, Object>> trend = new ArrayList<>();
dailyTrend.forEach((date, statusMap) -> {
Map<String, Object> entry = new HashMap<>();
entry.put("date", date);
entry.putAll(statusMap);
long total = statusMap.values().stream().mapToLong(Long::longValue).sum();
entry.put("total", total);
trend.add(entry);
});
return trend;
}
}

View File

@@ -0,0 +1,61 @@
package com.healthlink.his.web.infection.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.infection.domain.CdssAlert;
import com.healthlink.his.infection.domain.CdssRule;
import com.healthlink.his.web.infection.appservice.ICdssAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Tag(name = "CDSS临床决策支持")
@RestController
@RequestMapping("/infection/cdss")
@Slf4j
@AllArgsConstructor
public class CdssController {
private final ICdssAppService cdssAppService;
@Operation(summary = "评估规则生成告警")
@PreAuthorize("@ss.hasPermi('infection:cdss:edit')")
@PostMapping("/evaluate")
public R<?> evaluateRules(@RequestParam Long encounterId) {
log.info("CDSS规则评估, encounterId={}", encounterId);
return R.ok(cdssAppService.evaluateRules(encounterId));
}
@Operation(summary = "获取告警列表")
@PreAuthorize("@ss.hasPermi('infection:cdss:list')")
@GetMapping("/alerts/{encounterId}")
public R<?> getAlerts(@PathVariable Long encounterId) {
return R.ok(cdssAppService.getAlerts(encounterId));
}
@Operation(summary = "确认告警")
@PreAuthorize("@ss.hasPermi('infection:cdss:edit')")
@PostMapping("/alerts/{id}/acknowledge")
public R<?> acknowledgeAlert(@PathVariable Long id) {
return R.ok(cdssAppService.acknowledgeAlert(id));
}
@Operation(summary = "查询规则列表")
@PreAuthorize("@ss.hasPermi('infection:cdss:list')")
@GetMapping("/rules")
public R<?> getRules(
@RequestParam(value = "ruleType", required = false) String ruleType,
@RequestParam(value = "severity", required = false) String severity,
@RequestParam(value = "keyword", required = false) String keyword) {
Map<String, Object> params = new java.util.HashMap<>();
if (ruleType != null) params.put("ruleType", ruleType);
if (severity != null) params.put("severity", severity);
if (keyword != null) params.put("keyword", keyword);
return R.ok(cdssAppService.getRules(params));
}
}

View File

@@ -0,0 +1,40 @@
package com.healthlink.his.web.infection.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.web.infection.appservice.IInfectionDetailAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Tag(name = "院感监测统计")
@RestController
@RequestMapping("/infection-detail")
@Slf4j
@AllArgsConstructor
public class InfectionDetailController {
private final IInfectionDetailAppService infectionDetailAppService;
@Operation(summary = "科室感染率统计")
@PreAuthorize("@ss.hasPermi('infection:infection:list')")
@GetMapping("/rate-by-dept")
public R<Map<String, Object>> getInfectionRateByDept(
@RequestParam(value = "deptId", required = false) Long deptId) {
return R.ok(infectionDetailAppService.getInfectionRateByDept(deptId));
}
@Operation(summary = "感染趋势统计")
@PreAuthorize("@ss.hasPermi('infection:infection:list')")
@GetMapping("/trend")
public R<List<Map<String, Object>>> getInfectionTrend(
@RequestParam(value = "startDate", required = false) String startDate,
@RequestParam(value = "endDate", required = false) String endDate) {
return R.ok(infectionDetailAppService.getInfectionTrend(startDate, endDate));
}
}

View File

@@ -1,4 +1,4 @@
-- V66__update_menu_icons.sql
-- V66__update_menu_icons.sql
-- 更新菜单图标 - 根据菜单功能名称匹配合适的图标
-- 仅使用 src/assets/icons/svg/ 目录下实际存在的图标
@@ -30,7 +30,7 @@ UPDATE sys_menu SET icon = 'surgery' WHERE menu_id = 2119; -- 手术管理
UPDATE sys_menu SET icon = 'user' WHERE menu_id = 2140; -- 患者管理
UPDATE sys_menu SET icon = 'consultation' WHERE menu_id = 2147; -- 会诊管理
UPDATE sys_menu SET icon = 'report' WHERE menu_id = 2159; -- 疾病报告管理
UPDATE sys_MENU SET icon = 'infection' WHERE menu_id = 10001; -- 院感管理
UPDATE sys_menu SET icon = 'infection' WHERE menu_id = 10001; -- 院感管理
UPDATE sys_menu SET icon = 'log' WHERE menu_id = 10011; -- 药品追溯管理
UPDATE sys_menu SET icon = 'edit' WHERE menu_id = 10021; -- 电子签名管理
UPDATE sys_menu SET icon = 'alert' WHERE menu_id = 10031; -- 危急值管理
@@ -109,7 +109,7 @@ UPDATE sys_menu SET icon = 'laboratory' WHERE menu_id = 277; -- 医技工作
UPDATE sys_menu SET icon = 'billing' WHERE menu_id = 282; -- 门诊收费工作站
-- 门诊工作站子菜单
UPDATE sys_MENU SET icon = 'drug' WHERE menu_id = 272; -- 门诊退药
UPDATE sys_menu SET icon = 'drug' WHERE menu_id = 272; -- 门诊退药
UPDATE sys_menu SET icon = 'registration' WHERE menu_id = 274; -- 门诊退号
-- 门诊收费工作站子菜单

View File

@@ -0,0 +1,33 @@
CREATE TABLE cdss_rule (
id BIGSERIAL PRIMARY KEY,
rule_code VARCHAR(32) NOT NULL,
rule_name VARCHAR(100) NOT NULL,
rule_type VARCHAR(20) NOT NULL,
condition_expr TEXT NOT NULL,
suggestion TEXT NOT NULL,
severity VARCHAR(16) DEFAULT 'INFO',
enabled BOOLEAN DEFAULT TRUE,
tenant_id BIGINT DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
create_by VARCHAR(64),
update_time TIMESTAMP,
update_by VARCHAR(64)
);
CREATE TABLE cdss_alert (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT NOT NULL,
patient_id BIGINT NOT NULL,
rule_id BIGINT NOT NULL,
alert_type VARCHAR(20) NOT NULL,
alert_message TEXT NOT NULL,
severity VARCHAR(16) NOT NULL,
acknowledged BOOLEAN DEFAULT FALSE,
acknowledged_by BIGINT,
acknowledged_time TIMESTAMP,
tenant_id BIGINT DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
create_by VARCHAR(64)
);

View File

@@ -0,0 +1,22 @@
CREATE TABLE regional_share_record (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT NOT NULL,
patient_id BIGINT NOT NULL,
share_type VARCHAR(32) NOT NULL,
target_system VARCHAR(64) NOT NULL,
share_status VARCHAR(20) DEFAULT 'PENDING',
request_data TEXT,
response_data TEXT,
error_message TEXT,
retry_count INTEGER DEFAULT 0,
tenant_id BIGINT DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
create_by VARCHAR(64),
update_time TIMESTAMP,
update_by VARCHAR(64)
);
CREATE INDEX idx_regional_share_encounter ON regional_share_record(encounter_id);
CREATE INDEX idx_regional_share_patient ON regional_share_record(patient_id);
CREATE INDEX idx_regional_share_status ON regional_share_record(share_status);

View File

@@ -0,0 +1,40 @@
-- V73: 电子病历5级 - 结构化数据仓库+质控评分
-- 结构化数据表
CREATE TABLE IF NOT EXISTS emr_structured_data (
id BIGSERIAL PRIMARY KEY,
emr_id BIGINT NOT NULL,
encounter_id BIGINT NOT NULL,
patient_id BIGINT NOT NULL,
data_type VARCHAR(32) NOT NULL,
data_key VARCHAR(64) NOT NULL,
data_value TEXT,
data_unit VARCHAR(32),
record_time TIMESTAMP,
tenant_id BIGINT DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
create_by VARCHAR(64)
);
CREATE INDEX idx_structured_data_emr ON emr_structured_data(emr_id);
CREATE INDEX idx_structured_data_encounter ON emr_structured_data(encounter_id);
CREATE INDEX idx_structured_data_patient ON emr_structured_data(patient_id);
-- 质控评分表
CREATE TABLE IF NOT EXISTS emr_quality_score (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT NOT NULL,
emr_type VARCHAR(32) NOT NULL,
total_score DECIMAL(5,2),
completeness_score DECIMAL(5,2),
timeliness_score DECIMAL(5,2),
accuracy_score DECIMAL(5,2),
check_time TIMESTAMP,
tenant_id BIGINT DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
create_by VARCHAR(64)
);
CREATE INDEX idx_quality_score_encounter ON emr_quality_score(encounter_id);

View File

@@ -0,0 +1,3 @@
ALTER TABLE hir_infection_case ADD COLUMN IF NOT EXISTS department_id BIGINT;
COMMENT ON COLUMN hir_infection_case.department_id IS '科室ID';
CREATE INDEX IF NOT EXISTS idx_hir_infection_case_dept ON hir_infection_case(department_id);

View File

@@ -0,0 +1,47 @@
package com.healthlink.his.emr.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("emr_quality_score")
@Accessors(chain = true)
public class EmrQualityScore implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long encounterId;
private String emrType;
private BigDecimal totalScore;
private BigDecimal completenessScore;
private BigDecimal timelinessScore;
private BigDecimal accuracyScore;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date checkTime;
private Integer tenantId;
private String deleteFlag;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String createBy;
}

View File

@@ -0,0 +1,48 @@
package com.healthlink.his.emr.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("emr_structured_data")
@Accessors(chain = true)
public class EmrStructuredData implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long emrId;
private Long encounterId;
private Long patientId;
private String dataType;
private String dataKey;
private String dataValue;
private String dataUnit;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date recordTime;
private Integer tenantId;
private String deleteFlag;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String createBy;
}

View File

@@ -0,0 +1,14 @@
package com.healthlink.his.emr.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.emr.domain.EmrQualityScore;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface EmrQualityScoreMapper extends BaseMapper<EmrQualityScore> {
List<EmrQualityScore> selectByEncounterId(@Param("encounterId") Long encounterId);
}

View File

@@ -0,0 +1,16 @@
package com.healthlink.his.emr.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.emr.domain.EmrStructuredData;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface EmrStructuredDataMapper extends BaseMapper<EmrStructuredData> {
List<EmrStructuredData> selectByEncounterId(@Param("encounterId") Long encounterId);
List<EmrStructuredData> selectByEmrId(@Param("emrId") Long emrId);
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.emr.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.emr.domain.EmrQualityScore;
import java.util.List;
public interface IEmrQualityScoreService extends IService<EmrQualityScore> {
List<EmrQualityScore> selectByEncounterId(Long encounterId);
}

View File

@@ -0,0 +1,13 @@
package com.healthlink.his.emr.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.emr.domain.EmrStructuredData;
import java.util.List;
public interface IEmrStructuredDataService extends IService<EmrStructuredData> {
List<EmrStructuredData> selectByEncounterId(Long encounterId);
List<EmrStructuredData> selectByEmrId(Long emrId);
}

View File

@@ -0,0 +1,20 @@
package com.healthlink.his.emr.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.emr.domain.EmrQualityScore;
import com.healthlink.his.emr.mapper.EmrQualityScoreMapper;
import com.healthlink.his.emr.service.IEmrQualityScoreService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class EmrQualityScoreServiceImpl
extends ServiceImpl<EmrQualityScoreMapper, EmrQualityScore>
implements IEmrQualityScoreService {
@Override
public List<EmrQualityScore> selectByEncounterId(Long encounterId) {
return baseMapper.selectByEncounterId(encounterId);
}
}

View File

@@ -0,0 +1,25 @@
package com.healthlink.his.emr.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.emr.domain.EmrStructuredData;
import com.healthlink.his.emr.mapper.EmrStructuredDataMapper;
import com.healthlink.his.emr.service.IEmrStructuredDataService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class EmrStructuredDataServiceImpl
extends ServiceImpl<EmrStructuredDataMapper, EmrStructuredData>
implements IEmrStructuredDataService {
@Override
public List<EmrStructuredData> selectByEncounterId(Long encounterId) {
return baseMapper.selectByEncounterId(encounterId);
}
@Override
public List<EmrStructuredData> selectByEmrId(Long emrId) {
return baseMapper.selectByEmrId(emrId);
}
}

View File

@@ -0,0 +1,26 @@
package com.healthlink.his.esb.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 区域医疗信息共享记录
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("regional_share_record")
public class RegionalShareRecord extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
private Long encounterId;
private Long patientId;
private String shareType;
private String targetSystem;
private String shareStatus;
private String requestData;
private String responseData;
private String errorMessage;
private Integer retryCount;
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.esb.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.esb.domain.RegionalShareRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RegionalShareRecordMapper extends BaseMapper<RegionalShareRecord> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.esb.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.esb.domain.RegionalShareRecord;
public interface IRegionalShareRecordService extends IService<RegionalShareRecord> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.esb.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.esb.domain.RegionalShareRecord;
import com.healthlink.his.esb.mapper.RegionalShareRecordMapper;
import com.healthlink.his.esb.service.IRegionalShareRecordService;
import org.springframework.stereotype.Service;
@Service
public class RegionalShareRecordServiceImpl extends ServiceImpl<RegionalShareRecordMapper, RegionalShareRecord> implements IRegionalShareRecordService {
}

View File

@@ -0,0 +1,35 @@
package com.healthlink.his.infection.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("cdss_alert")
public class CdssAlert extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("encounter_id")
private Long encounterId;
@TableField("patient_id")
private Long patientId;
@TableField("rule_id")
private Long ruleId;
@TableField("alert_type")
private String alertType;
@TableField("alert_message")
private String alertMessage;
@TableField("severity")
private String severity;
@TableField("acknowledged")
private Boolean acknowledged;
@TableField("acknowledged_by")
private Long acknowledgedBy;
@TableField("acknowledged_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date acknowledgedTime;
}

View File

@@ -0,0 +1,30 @@
package com.healthlink.his.infection.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("cdss_rule")
public class CdssRule extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("rule_code")
private String ruleCode;
@TableField("rule_name")
private String ruleName;
@TableField("rule_type")
private String ruleType;
@TableField("condition_expr")
private String conditionExpr;
@TableField("suggestion")
private String suggestion;
@TableField("severity")
private String severity;
@TableField("enabled")
private Boolean enabled;
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.infection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.infection.domain.CdssAlert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CdssAlertMapper extends BaseMapper<CdssAlert> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.infection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.infection.domain.CdssRule;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CdssRuleMapper extends BaseMapper<CdssRule> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.infection.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.infection.domain.CdssAlert;
public interface ICdssAlertService extends IService<CdssAlert> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.infection.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.infection.domain.CdssRule;
public interface ICdssRuleService extends IService<CdssRule> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.infection.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.infection.domain.CdssAlert;
import com.healthlink.his.infection.mapper.CdssAlertMapper;
import com.healthlink.his.infection.service.ICdssAlertService;
import org.springframework.stereotype.Service;
@Service
public class CdssAlertServiceImpl extends ServiceImpl<CdssAlertMapper, CdssAlert> implements ICdssAlertService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.infection.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.infection.domain.CdssRule;
import com.healthlink.his.infection.mapper.CdssRuleMapper;
import com.healthlink.his.infection.service.ICdssRuleService;
import org.springframework.stereotype.Service;
@Service
public class CdssRuleServiceImpl extends ServiceImpl<CdssRuleMapper, CdssRule> implements ICdssRuleService {
}

View File

@@ -0,0 +1,13 @@
<?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.emr.mapper.EmrQualityScoreMapper">
<select id="selectByEncounterId" resultType="com.healthlink.his.emr.domain.EmrQualityScore">
SELECT * FROM emr_quality_score
WHERE encounter_id = #{encounterId}
ORDER BY check_time DESC
</select>
</mapper>

View File

@@ -0,0 +1,19 @@
<?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.emr.mapper.EmrStructuredDataMapper">
<select id="selectByEncounterId" resultType="com.healthlink.his.emr.domain.EmrStructuredData">
SELECT * FROM emr_structured_data
WHERE encounter_id = #{encounterId}
ORDER BY record_time DESC
</select>
<select id="selectByEmrId" resultType="com.healthlink.his.emr.domain.EmrStructuredData">
SELECT * FROM emr_structured_data
WHERE emr_id = #{emrId}
ORDER BY record_time DESC
</select>
</mapper>

View File

@@ -14,3 +14,8 @@ export function getEmrRevisionList(emrId) { return request({ url: '/emr/revision
export function getEmrRevisionPage(params) { return request({ url: '/emr/revision/page', method: 'get', params }) }
export function getEmrRevisionDetail(id) { return request({ url: '/emr/revision/' + id, method: 'get' }) }
export function compareEmrRevisions(id1, id2) { return request({ url: '/emr/revision/compare', method: 'get', params: { revisionId1: id1, revisionId2: id2 } }) }
export function extractStructuredData(emrId) { return request({ url: '/emr/warehouse/extract', method: 'post', params: { emrId } }) }
export function getStructuredData(encounterId) { return request({ url: '/emr/warehouse/data/' + encounterId, method: 'get' }) }
export function calculateQualityScore(encounterId) { return request({ url: '/emr/warehouse/quality-score', method: 'post', params: { encounterId } }) }
export function getQualityScores(encounterId) { return request({ url: '/emr/warehouse/quality-scores', method: 'get', params: { encounterId } }) }