feat(epidemic): add autoScreen/saveReport/getReportStats endpoints, enhance EpidemicReport entity with screen fields, and create EpidemicReport.vue

This commit is contained in:
2026-06-18 15:57:16 +08:00
parent f3aac08c4e
commit 5ee15b348b
6 changed files with 300 additions and 0 deletions

View File

@@ -7,4 +7,7 @@ public interface IEpidemicAppService {
void confirmReport(Long id, String cdcNo);
List<EpidemicReport> getReports(String status);
Map<String, Object> getStatistics(String startDate, String endDate);
EpidemicReport autoScreen(EpidemicReport r);
EpidemicReport saveReport(EpidemicReport r);
Map<String, Object> getReportStats(String startDate, String endDate);
}

View File

@@ -9,6 +9,18 @@ import java.util.*;
@Service
public class EpidemicAppServiceImpl implements IEpidemicAppService {
@Autowired private IEpidemicReportService reportService;
private static final Set<String> NOTIFIABLE_DISEASES = Set.of(
"鼠疫", "霍乱", "传染性非典型肺炎", "艾滋病", "病毒性肝炎", "脊髓灰质炎",
"人感染高致病性禽流感", "麻疹", "流行性出血热", "狂犬病", "流行性乙型脑炎",
"登革热", "炭疽", "细菌性和阿米巴性痢疾", "肺结核", "伤寒和副伤寒",
"流行性脑脊髓膜炎", "百日咳", "白喉", "新生儿破伤风", "猩红热",
"布鲁氏菌病", "淋病", "梅毒", "钩端螺旋体病", "血吸虫病", "疟疾",
"手足口病", "流行性感冒", "流行性腮腺炎", "风疹", "急性出血性结膜炎",
"麻风病", "流行性和地方性斑疹伤寒", "黑热病", "包虫病", "丝虫病",
"感染性腹泻", "甲型H1N1流感", "新型冠状病毒肺炎"
);
@Override
public EpidemicReport report(EpidemicReport r) { r.setStatus("PENDING"); r.setDelFlag("0"); r.setReportDate(new Date()); reportService.save(r); return r; }
@Override
@@ -27,4 +39,40 @@ public class EpidemicAppServiceImpl implements IEpidemicAppService {
r.put("confirmed", reportService.count(new LambdaQueryWrapper<EpidemicReport>().eq(EpidemicReport::getStatus, "CONFIRMED")));
return r;
}
@Override
public EpidemicReport autoScreen(EpidemicReport r) {
boolean match = r.getDiseaseName() != null && NOTIFIABLE_DISEASES.stream()
.anyMatch(d -> r.getDiseaseName().contains(d));
r.setScreenResult(match ? "MATCHED" : "NOT_MATCHED");
r.setScreenLevel(match ? "LEVEL_A" : "NORMAL");
r.setScreenTime(new Date());
if (match && (r.getStatus() == null || "DRAFT".equals(r.getStatus()))) {
r.setStatus("PENDING");
}
r.setDelFlag("0");
r.setReportDate(new Date());
reportService.save(r);
return r;
}
@Override
public EpidemicReport saveReport(EpidemicReport r) {
if (r.getId() == null) {
r.setStatus("DRAFT"); r.setDelFlag("0"); r.setReportDate(new Date());
reportService.save(r);
} else {
reportService.updateById(r);
}
return r;
}
@Override
public Map<String, Object> getReportStats(String startDate, String endDate) {
Map<String, Object> r = new HashMap<>();
LambdaQueryWrapper<EpidemicReport> base = new LambdaQueryWrapper<EpidemicReport>().eq(EpidemicReport::getDelFlag, "0");
r.put("total", reportService.count(base));
r.put("pending", reportService.count(new LambdaQueryWrapper<EpidemicReport>().eq(EpidemicReport::getStatus, "PENDING").eq(EpidemicReport::getDelFlag, "0")));
r.put("confirmed", reportService.count(new LambdaQueryWrapper<EpidemicReport>().eq(EpidemicReport::getStatus, "CONFIRMED").eq(EpidemicReport::getDelFlag, "0")));
r.put("screenMatched", reportService.count(new LambdaQueryWrapper<EpidemicReport>().eq(EpidemicReport::getScreenResult, "MATCHED").eq(EpidemicReport::getDelFlag, "0")));
r.put("screenNormal", reportService.count(new LambdaQueryWrapper<EpidemicReport>().eq(EpidemicReport::getScreenResult, "NOT_MATCHED").eq(EpidemicReport::getDelFlag, "0")));
return r;
}
}

View File

@@ -5,16 +5,32 @@ import com.healthlink.his.web.epidemic.appservice.IEpidemicAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@Tag(name = "传染病直报") @RestController @RequestMapping("/api/v1/epidemic")
public class EpidemicController {
@Autowired private IEpidemicAppService epidemicAppService;
@Operation(summary = "上报") @PostMapping("/report")
@PreAuthorize("hasAuthority('epidemic:edit')")
public AjaxResult report(@RequestBody EpidemicReport r) { return AjaxResult.success(epidemicAppService.report(r)); }
@Operation(summary = "确认") @PutMapping("/confirm/{id}")
@PreAuthorize("hasAuthority('epidemic:edit')")
public AjaxResult confirm(@PathVariable Long id, @RequestParam String cdcNo) { epidemicAppService.confirmReport(id, cdcNo); return AjaxResult.success(); }
@Operation(summary = "列表") @GetMapping("/list")
@PreAuthorize("hasAuthority('epidemic:list')")
public AjaxResult list(@RequestParam(required = false) String status) { return AjaxResult.success(epidemicAppService.getReports(status)); }
@Operation(summary = "统计") @GetMapping("/statistics")
@PreAuthorize("hasAuthority('epidemic:list')")
public AjaxResult statistics(@RequestParam(required = false) String s, @RequestParam(required = false) String e) { return AjaxResult.success(epidemicAppService.getStatistics(s, e)); }
@Operation(summary = "自动筛查") @PostMapping("/auto-screen")
@PreAuthorize("hasAuthority('epidemic:edit')")
public AjaxResult autoScreen(@RequestBody EpidemicReport r) { return AjaxResult.success(epidemicAppService.autoScreen(r)); }
@Operation(summary = "保存报告") @PostMapping("/save")
@PreAuthorize("hasAuthority('epidemic:edit')")
public AjaxResult saveReport(@RequestBody EpidemicReport r) { return AjaxResult.success(epidemicAppService.saveReport(r)); }
@Operation(summary = "报告统计") @GetMapping("/report-stats")
@PreAuthorize("hasAuthority('epidemic:list')")
public AjaxResult reportStats(@RequestParam(required = false) String s, @RequestParam(required = false) String e) { return AjaxResult.success(epidemicAppService.getReportStats(s, e)); }
}

View File

@@ -0,0 +1,6 @@
-- Add auto-screen fields to epidemic_report
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS screen_result TEXT;
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS screen_time TIMESTAMP;
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS screen_level VARCHAR(20);
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS address VARCHAR(500);
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS contact_phone VARCHAR(50);

View File

@@ -13,4 +13,6 @@ public class EpidemicReport extends HisBaseEntity {
private String reporterId; private String reporterName;
private Date reportDate; private String status;
private String cdcConfirmNo; private String delFlag;
private String screenResult; private Date screenTime; private String screenLevel;
private String address; private String contactPhone;
}

View File

@@ -0,0 +1,225 @@
<template>
<div class="app-container">
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane label="传染病报告" name="list">
<el-form :inline="true" :model="queryParams" label-width="100px">
<el-form-item label="报告状态">
<el-select v-model="queryParams.status" placeholder="请选择" clearable style="width:160px">
<el-option label="待上报" value="PENDING" />
<el-option label="已确认" value="CONFIRMED" />
<el-option label="草稿" value="DRAFT" />
<el-option label="已退回" value="RETURNED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="loadReports">查询</el-button>
<el-button type="success" icon="Plus" @click="showAddDialog">新增报告</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="reportList" border>
<el-table-column label="患者姓名" align="center" prop="patientName" width="120" />
<el-table-column label="疾病名称" align="center" prop="diseaseName" min-width="160" show-overflow-tooltip />
<el-table-column label="疾病编码" align="center" prop="diseaseCode" width="120" />
<el-table-column label="报告类型" align="center" prop="reportType" width="100">
<template #default="{ row }">
<el-tag>{{ reportTypeText(row.reportType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="上报人" align="center" prop="reporterName" width="100" />
<el-table-column label="上报日期" align="center" prop="reportDate" width="160" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template #default="{ row }">
<el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="筛查结果" align="center" prop="screenResult" width="110">
<template #default="{ row }">
<el-tag v-if="row.screenResult" :type="row.screenResult === 'MATCHED' ? 'danger' : 'success'">
{{ row.screenResult === 'MATCHED' ? '命中' : '未命中' }}
</el-tag>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" icon="View" @click="handleDetail(row)">详情</el-button>
<el-button v-if="row.status === 'PENDING'" link type="success" icon="Check" @click="handleConfirm(row)">确认</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="自动筛查" name="screen">
<el-form :model="screenForm" label-width="110px" style="max-width:700px">
<el-form-item label="患者姓名">
<el-input v-model="screenForm.patientName" placeholder="请输入患者姓名" />
</el-form-item>
<el-form-item label="疾病名称">
<el-input v-model="screenForm.diseaseName" placeholder="请输入诊断疾病名称" />
</el-form-item>
<el-form-item label="疾病编码">
<el-input v-model="screenForm.diseaseCode" placeholder="请输入疾病编码" />
</el-form-item>
<el-form-item label="报告类型">
<el-select v-model="screenForm.reportType" placeholder="请选择">
<el-option label="初次报告" value="INITIAL" />
<el-option label="订正报告" value="CORRECTION" />
</el-select>
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="screenForm.contactPhone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="现住址">
<el-input v-model="screenForm.address" placeholder="请输入现住址" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleAutoScreen">开始筛查</el-button>
</el-form-item>
</el-form>
<el-divider v-if="screenResult" />
<el-alert v-if="screenResult" :title="screenResultTitle" :type="screenResult.screenResult === 'MATCHED' ? 'error' : 'success'" show-icon :closable="false" style="margin-bottom:16px" />
<el-descriptions v-if="screenResult" :column="2" border>
<el-descriptions-item label="筛查结果">{{ screenResult.screenResult === 'MATCHED' ? '命中法定传染病' : '未命中' }}</el-descriptions-item>
<el-descriptions-item label="筛查等级">{{ screenResult.screenLevel === 'LEVEL_A' ? '甲类' : '正常' }}</el-descriptions-item>
<el-descriptions-item label="筛查时间">{{ screenResult.screenTime }}</el-descriptions-item>
<el-descriptions-item label="报告状态">{{ statusText(screenResult.status) }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="报告统计" name="stats">
<el-descriptions :column="3" border>
<el-descriptions-item label="报告总数">{{ reportStats.total || 0 }}</el-descriptions-item>
<el-descriptions-item label="待上报">{{ reportStats.pending || 0 }}</el-descriptions-item>
<el-descriptions-item label="已确认">{{ reportStats.confirmed || 0 }}</el-descriptions-item>
<el-descriptions-item label="筛查命中">{{ reportStats.screenMatched || 0 }}</el-descriptions-item>
<el-descriptions-item label="筛查未命中">{{ reportStats.screenNormal || 0 }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
</el-tabs>
<el-dialog v-model="addDialogVisible" title="新增传染病报告" width="650px" append-to-body>
<el-form :model="formData" label-width="110px">
<el-form-item label="患者姓名">
<el-input v-model="formData.patientName" placeholder="请输入患者姓名" />
</el-form-item>
<el-form-item label="疾病名称">
<el-input v-model="formData.diseaseName" placeholder="请输入疾病名称" />
</el-form-item>
<el-form-item label="疾病编码">
<el-input v-model="formData.diseaseCode" placeholder="请输入疾病编码" />
</el-form-item>
<el-form-item label="报告类型">
<el-select v-model="formData.reportType" placeholder="请选择">
<el-option label="初次报告" value="INITIAL" />
<el-option label="订正报告" value="CORRECTION" />
</el-select>
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="formData.contactPhone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="现住址">
<el-input v-model="formData.address" placeholder="请输入现住址" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="addDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="detailVisible" title="传染病报告详情" width="650px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="患者姓名">{{ detailData.patientName }}</el-descriptions-item>
<el-descriptions-item label="疾病名称">{{ detailData.diseaseName }}</el-descriptions-item>
<el-descriptions-item label="疾病编码">{{ detailData.diseaseCode }}</el-descriptions-item>
<el-descriptions-item label="报告类型">{{ reportTypeText(detailData.reportType) }}</el-descriptions-item>
<el-descriptions-item label="上报人">{{ detailData.reporterName }}</el-descriptions-item>
<el-descriptions-item label="上报日期">{{ detailData.reportDate }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="statusType(detailData.status)">{{ statusText(detailData.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="CDC确认号">{{ detailData.cdcConfirmNo || '—' }}</el-descriptions-item>
<el-descriptions-item label="筛查结果">{{ detailData.screenResult === 'MATCHED' ? '命中' : (detailData.screenResult || '—') }}</el-descriptions-item>
<el-descriptions-item label="筛查等级">{{ detailData.screenLevel === 'LEVEL_A' ? '甲类' : (detailData.screenLevel || '—') }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ detailData.contactPhone || '—' }}</el-descriptions-item>
<el-descriptions-item label="现住址">{{ detailData.address || '—' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="EpidemicReport" lang="js">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
const activeTab = ref('list')
const loading = ref(false)
const reportList = ref([])
const addDialogVisible = ref(false)
const detailVisible = ref(false)
const detailData = ref({})
const reportStats = ref({})
const screenResult = ref(null)
const queryParams = reactive({ status: undefined })
const formData = reactive({ patientName: '', diseaseName: '', diseaseCode: '', reportType: 'INITIAL', contactPhone: '', address: '' })
const screenForm = reactive({ patientName: '', diseaseName: '', diseaseCode: '', reportType: 'INITIAL', contactPhone: '', address: '' })
const screenResultTitle = computed(() =>
screenResult.value?.screenResult === 'MATCHED' ? '已命中法定传染病,请及时上报' : '未命中法定传染病'
)
function reportTypeText(t) { return { INITIAL: '初次报告', CORRECTION: '订正报告' }[t] || t }
function statusText(s) { return { PENDING: '待上报', CONFIRMED: '已确认', DRAFT: '草稿', RETURNED: '已退回' }[s] || s }
function statusType(s) { return { PENDING: 'warning', CONFIRMED: 'success', DRAFT: 'info', RETURNED: 'danger' }[s] || 'info' }
function loadReports() {
loading.value = true
request({ url: '/api/v1/epidemic/list', method: 'get', params: queryParams }).then(res => {
reportList.value = res.data || []
}).finally(() => { loading.value = false })
}
function loadStats() {
request({ url: '/api/v1/epidemic/report-stats', method: 'get' }).then(res => {
reportStats.value = res.data || {}
})
}
function showAddDialog() {
Object.assign(formData, { patientName: '', diseaseName: '', diseaseCode: '', reportType: 'INITIAL', contactPhone: '', address: '' })
addDialogVisible.value = true
}
function handleDetail(row) { detailData.value = row; detailVisible.value = true }
function handleConfirm(row) {
request({ url: `/api/v1/epidemic/confirm/${row.id}`, method: 'put', params: { cdcNo: 'CDC-' + Date.now() } }).then(() => {
ElMessage.success('确认成功'); loadReports()
})
}
function handleAutoScreen() {
request({ url: '/api/v1/epidemic/auto-screen', method: 'post', data: screenForm }).then(res => {
screenResult.value = res.data
ElMessage.success('筛查完成')
})
}
function submitForm() {
request({ url: '/api/v1/epidemic/save', method: 'post', data: formData }).then(() => {
ElMessage.success('报告保存成功'); addDialogVisible.value = false; loadReports()
})
}
function handleTabChange() {
if (activeTab.value === 'list') loadReports()
else if (activeTab.value === 'stats') loadStats()
}
onMounted(() => { loadReports(); loadStats() })
</script>