diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/infection/appservice/IOutbreakWarningAppService.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/infection/appservice/IOutbreakWarningAppService.java new file mode 100644 index 000000000..aaf6946b2 --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/infection/appservice/IOutbreakWarningAppService.java @@ -0,0 +1,11 @@ +package com.healthlink.his.web.infection.appservice; + +import com.healthlink.his.infection.domain.OutbreakWarning; + +import java.util.List; +import java.util.Map; + +public interface IOutbreakWarningAppService { + Map checkOutbreak(Map params); + List getWarnings(Map params); +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/infection/appservice/impl/OutbreakWarningAppServiceImpl.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/infection/appservice/impl/OutbreakWarningAppServiceImpl.java new file mode 100644 index 000000000..61be598c7 --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/infection/appservice/impl/OutbreakWarningAppServiceImpl.java @@ -0,0 +1,146 @@ +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.domain.OutbreakWarning; +import com.healthlink.his.infection.service.IHirInfectionCaseService; +import com.healthlink.his.infection.service.IOutbreakWarningService; +import com.healthlink.his.web.infection.appservice.IOutbreakWarningAppService; +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 OutbreakWarningAppServiceImpl implements IOutbreakWarningAppService { + + private final IHirInfectionCaseService infectionCaseService; + private final IOutbreakWarningService outbreakWarningService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Map checkOutbreak(Map params) { + log.info("开始暴发预警检测"); + int timeRangeDays = parseInt(params.get("timeRangeDays"), 7); + int yellowThreshold = parseInt(params.get("yellowThreshold"), 3); + int redThreshold = parseInt(params.get("redThreshold"), 10); + + Date startDate = new Date(System.currentTimeMillis() - (long) timeRangeDays * 24 * 60 * 60 * 1000); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.ge(HirInfectionCase::getReportTime, startDate); + List recentCases = infectionCaseService.list(wrapper); + + Map> grouped = recentCases.stream() + .filter(c -> c.getInfectionType() != null) + .collect(Collectors.groupingBy(c -> c.getInfectionType())); + + List newWarnings = new ArrayList<>(); + int checkedCombinations = 0; + + for (Map.Entry> entry : grouped.entrySet()) { + String infectionType = entry.getKey(); + List cases = entry.getValue(); + int caseCount = cases.size(); + + if (caseCount < yellowThreshold) { + continue; + } + + Map> byDept = cases.stream() + .collect(Collectors.groupingBy(c -> + c.getReporterName() != null ? c.getReporterName() : "未知")); + + for (Map.Entry> deptEntry : byDept.entrySet()) { + checkedCombinations++; + int deptCount = deptEntry.getValue().size(); + if (deptCount < yellowThreshold) { + continue; + } + + String level = deptCount >= redThreshold ? "RED" : "YELLOW"; + + boolean alreadyExists = outbreakWarningService.list( + new LambdaQueryWrapper() + .eq(OutbreakWarning::getInfectionType, infectionType) + .eq(OutbreakWarning::getStatus, 0) + .apply("create_time > NOW() - INTERVAL '{0} days'", timeRangeDays) + ).stream().anyMatch(w -> deptEntry.getKey().equals(w.getDepartmentName())); + + if (alreadyExists) { + continue; + } + + OutbreakWarning warning = new OutbreakWarning(); + warning.setInfectionType(infectionType); + warning.setCaseCount(deptCount); + warning.setWarningLevel(level); + warning.setTimeRangeDays(timeRangeDays); + warning.setThresholdCount(yellowThreshold); + warning.setStatus(0); + warning.setDepartmentName(deptEntry.getKey()); + warning.setHandleResult("自动检测生成"); + warning.setCreateTime(new Date()); + outbreakWarningService.save(warning); + newWarnings.add(warning); + } + } + + Map result = new HashMap<>(); + result.put("totalRecentCases", recentCases.size()); + result.put("checkedCombinations", checkedCombinations); + result.put("newWarningCount", newWarnings.size()); + result.put("newWarnings", newWarnings); + result.put("checkTime", new Date()); + result.put("timeRangeDays", timeRangeDays); + result.put("yellowThreshold", yellowThreshold); + result.put("redThreshold", redThreshold); + + log.info("暴发预警检测完成: 近{}天{}例病例, 生成{}条新预警", + timeRangeDays, recentCases.size(), newWarnings.size()); + return result; + } + + @Override + public List getWarnings(Map params) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + String level = getStr(params, "warningLevel"); + if (level != null && !level.isEmpty()) { + wrapper.eq(OutbreakWarning::getWarningLevel, level); + } + Integer status = params.get("status") != null ? parseInt(params.get("status"), null) : null; + if (status != null) { + wrapper.eq(OutbreakWarning::getStatus, status); + } + String deptName = getStr(params, "departmentName"); + if (deptName != null && !deptName.isEmpty()) { + wrapper.like(OutbreakWarning::getDepartmentName, deptName); + } + wrapper.orderByDesc(OutbreakWarning::getCreateTime); + + return outbreakWarningService.list(wrapper); + } + + private int parseInt(Object val, int defaultVal) { + if (val == null) return defaultVal; + try { return Integer.parseInt(val.toString()); } + catch (NumberFormatException e) { return defaultVal; } + } + + private Integer parseInt(Object val, Integer defaultVal) { + if (val == null) return defaultVal; + try { return Integer.parseInt(val.toString()); } + catch (NumberFormatException e) { return defaultVal; } + } + + private String getStr(Map params, String key) { + Object v = params.get(key); + return v != null ? v.toString() : null; + } +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/infection/appservice/impl/TargetedSurveillanceAppServiceImpl.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/infection/appservice/impl/TargetedSurveillanceAppServiceImpl.java new file mode 100644 index 000000000..13d98601c --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/infection/appservice/impl/TargetedSurveillanceAppServiceImpl.java @@ -0,0 +1,115 @@ +package com.healthlink.his.web.infection.appservice.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.healthlink.his.infection.domain.TargetedSurveillance; +import com.healthlink.his.infection.service.ITargetedSurveillanceService; +import com.healthlink.his.web.infection.appservice.ITargetedSurveillanceAppService; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +@AllArgsConstructor +public class TargetedSurveillanceAppServiceImpl implements ITargetedSurveillanceAppService { + + private final ITargetedSurveillanceService surveillanceService; + + @Override + @Transactional(rollbackFor = Exception.class) + public TargetedSurveillance recordSurveillance(Map params) { + log.info("记录目标性监测数据, 参数: {}", params); + + TargetedSurveillance sv = new TargetedSurveillance(); + sv.setSurveillanceType(getStr(params, "surveillanceType")); + sv.setDepartmentId(params.get("departmentId") != null ? Long.valueOf(params.get("departmentId").toString()) : null); + sv.setDepartmentName(getStr(params, "departmentName")); + sv.setMonitorObject(getStr(params, "monitorObject")); + sv.setMonitorItem(getStr(params, "monitorItem")); + sv.setStartDate(params.get("startDate") != null ? parseDate(params.get("startDate").toString()) : null); + sv.setEndDate(params.get("endDate") != null ? parseDate(params.get("endDate").toString()) : null); + sv.setTotalCases(parseInt(params.get("totalCases"), 0)); + sv.setInfectionCases(parseInt(params.get("infectionCases"), 0)); + sv.setStatus(0); + sv.setReportContent(getStr(params, "reportContent")); + sv.setCreateTime(new Date()); + + if (sv.getTotalCases() > 0) { + sv.setInfectionRate(BigDecimal.valueOf(sv.getInfectionCases()) + .divide(BigDecimal.valueOf(sv.getTotalCases()), 4, RoundingMode.HALF_UP) + .multiply(BigDecimal.valueOf(100)) + .setScale(2, RoundingMode.HALF_UP)); + } else { + sv.setInfectionRate(BigDecimal.ZERO); + } + + surveillanceService.save(sv); + log.info("目标性监测记录已保存, ID: {}", sv.getId()); + return sv; + } + + @Override + public Map getSurveillanceStats(Map params) { + log.info("查询目标性监测统计, 参数: {}", params); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + String surveillanceType = getStr(params, "surveillanceType"); + if (surveillanceType != null && !surveillanceType.isEmpty()) { + wrapper.eq(TargetedSurveillance::getSurveillanceType, surveillanceType); + } + String deptName = getStr(params, "departmentName"); + if (deptName != null && !deptName.isEmpty()) { + wrapper.like(TargetedSurveillance::getDepartmentName, deptName); + } + wrapper.orderByDesc(TargetedSurveillance::getStartDate); + + List list = surveillanceService.list(wrapper); + + Map stats = new HashMap<>(); + stats.put("total", list.size()); + + int totalCases = 0, infectionCases = 0; + Map typeCount = new LinkedHashMap<>(); + for (TargetedSurveillance sv : list) { + totalCases += sv.getTotalCases() != null ? sv.getTotalCases() : 0; + infectionCases += sv.getInfectionCases() != null ? sv.getInfectionCases() : 0; + String type = sv.getSurveillanceType() != null ? sv.getSurveillanceType() : "未知"; + typeCount.merge(type, 1, Integer::sum); + } + + stats.put("totalCases", totalCases); + stats.put("infectionCases", infectionCases); + stats.put("overallRate", totalCases > 0 ? + BigDecimal.valueOf(infectionCases) + .divide(BigDecimal.valueOf(totalCases), 4, RoundingMode.HALF_UP) + .multiply(BigDecimal.valueOf(100)) + .setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO); + stats.put("typeDistribution", typeCount); + + return stats; + } + + private Date parseDate(String s) { + try { + java.time.LocalDate ld = java.time.LocalDate.parse(s, java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd")); + return java.util.Date.from(ld.atStartOfDay(java.time.ZoneId.systemDefault()).toInstant()); + } catch (Exception e) { return null; } + } + + private int parseInt(Object val, int defaultVal) { + if (val == null) return defaultVal; + try { return Integer.parseInt(val.toString()); } + catch (NumberFormatException e) { return defaultVal; } + } + + private String getStr(Map params, String key) { + Object v = params.get(key); + return v != null ? v.toString() : null; + } +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/infection/controller/OutbreakWarningController.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/infection/controller/OutbreakWarningController.java new file mode 100644 index 000000000..26463738d --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/infection/controller/OutbreakWarningController.java @@ -0,0 +1,59 @@ +package com.healthlink.his.web.infection.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.core.common.core.domain.R; +import com.healthlink.his.infection.domain.OutbreakWarning; +import com.healthlink.his.infection.service.IOutbreakWarningService; +import com.healthlink.his.web.infection.appservice.IOutbreakWarningAppService; +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.util.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Tag(name = "院感暴发预警") +@RestController +@RequestMapping("/infection/outbreak") +@Slf4j +@AllArgsConstructor +public class OutbreakWarningController { + + private final IOutbreakWarningAppService outbreakAppService; + private final IOutbreakWarningService outbreakWarningService; + + @Operation(summary = "执行暴发预警检测") + @PreAuthorize("@ss.hasPermi('infection:infection:edit')") + @PostMapping("/check") + public R checkOutbreak(@RequestBody Map params) { + log.info("触发暴发预警检测"); + return R.ok(outbreakAppService.checkOutbreak(params)); + } + + @Operation(summary = "查询暴发预警列表") + @PreAuthorize("@ss.hasPermi('infection:infection:list')") + @GetMapping("/list") + public R getWarnings( + @RequestParam(value = "warningLevel", required = false) String warningLevel, + @RequestParam(value = "status", required = false) Integer status, + @RequestParam(value = "departmentName", required = false) String departmentName, + @RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (StringUtils.hasText(warningLevel)) { + wrapper.eq(OutbreakWarning::getWarningLevel, warningLevel); + } + if (status != null) { + wrapper.eq(OutbreakWarning::getStatus, status); + } + if (StringUtils.hasText(departmentName)) { + wrapper.like(OutbreakWarning::getDepartmentName, departmentName); + } + wrapper.orderByDesc(OutbreakWarning::getCreateTime); + return R.ok(outbreakWarningService.page(new Page<>(pageNo, pageSize), wrapper)); + } +} diff --git a/healthlink-his-ui/src/views/infection/outbreak/api.js b/healthlink-his-ui/src/views/infection/outbreak/api.js new file mode 100644 index 000000000..9d6602986 --- /dev/null +++ b/healthlink-his-ui/src/views/infection/outbreak/api.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +export function checkOutbreak(data) { + return request({ url: '/infection/outbreak/check', method: 'post', data }) +} + +export function getOutbreakWarnings(params) { + return request({ url: '/infection/outbreak/list', method: 'get', params }) +} diff --git a/healthlink-his-ui/src/views/infection/outbreak/index.vue b/healthlink-his-ui/src/views/infection/outbreak/index.vue new file mode 100644 index 000000000..e5bffd896 --- /dev/null +++ b/healthlink-his-ui/src/views/infection/outbreak/index.vue @@ -0,0 +1,232 @@ + + + + +