feat(emr): 病历完整性检查

- 创建 IEmrCompletenessAppService + impl,实现 checkCompleteness() 和 getCheckResults()
- 创建 EmrCompletenessController,POST /emr/completeness/check 和 GET /emr/completeness/results/{emrId}
- 新建 EmrCompletenessCheck.vue 检查结果展示 + 不合格项提醒
- 添加前端 API 函数 checkCompleteness / getCompletenessResults
This commit is contained in:
2026-06-17 13:37:53 +08:00
parent f3a24a9129
commit 9673c0ed80
5 changed files with 322 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
package com.healthlink.his.web.emr.appservice;
import com.healthlink.his.emr.domain.EmrCompletenessCheck;
import java.util.List;
import java.util.Map;
public interface IEmrCompletenessAppService {
Map<String, Object> checkCompleteness(Long emrId, Long encounterId);
List<EmrCompletenessCheck> getCheckResults(Long emrId);
}

View File

@@ -0,0 +1,112 @@
package com.healthlink.his.web.emr.appservice.impl;
import com.healthlink.his.document.domain.Emr;
import com.healthlink.his.document.service.IEmrService;
import com.healthlink.his.emr.domain.EmrCompletenessCheck;
import com.healthlink.his.emr.service.IEmrCompletenessCheckService;
import com.healthlink.his.web.emr.appservice.IEmrCompletenessAppService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@Service
public class EmrCompletenessAppServiceImpl implements IEmrCompletenessAppService {
@Resource
private IEmrCompletenessCheckService emrCompletenessCheckService;
@Resource
private IEmrService emrService;
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> checkCompleteness(Long emrId, Long encounterId) {
Emr emr = emrService.getById(emrId);
if (emr == null) {
throw new IllegalArgumentException("病历不存在: " + emrId);
}
List<EmrCompletenessCheck> checks = new ArrayList<>();
int total = 0;
int requiredPassed = 0;
int requiredTotal = 0;
String[][] checkDefs = {
{"chief_complaint", "basic", "true", "主诉"},
{"medical_history", "basic", "true", "现病史"},
{"past_history", "basic", "false", "既往史"},
{"physical_exam", "basic", "true", "体格检查"},
{"auxiliary_exam", "examination", "false", "辅助检查"},
{"diagnosis", "diagnosis", "true", "诊断"},
{"treatment_plan", "treatment", "true", "治疗计划"},
{"signature", "signature", "false", "签名"}
};
Map<String, Object> contentMap = parseContent(emr.getContextJson());
for (String[] def : checkDefs) {
total++;
boolean isRequired = Boolean.parseBoolean(def[2]);
if (isRequired) requiredTotal++;
boolean hasValue = contentMap.containsKey(def[0])
&& contentMap.get(def[0]) != null
&& !contentMap.get(def[0]).toString().trim().isEmpty();
String result = hasValue ? "PASS" : "FAIL";
String detail = def[3] + (hasValue ? " - 已填写" : " - 未填写");
if (!isRequired && !hasValue) {
detail = def[3] + " - 未填写(选填项)";
}
if (isRequired && hasValue) requiredPassed++;
EmrCompletenessCheck check = new EmrCompletenessCheck()
.setEmrId(emrId)
.setEncounterId(encounterId)
.setCheckItem(def[0])
.setCheckCategory(def[1])
.setIsRequired(isRequired)
.setCheckResult(result)
.setCheckDetail(detail)
.setCheckTime(new Date());
emrCompletenessCheckService.save(check);
checks.add(check);
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("emrId", emrId);
result.put("encounterId", encounterId);
result.put("totalItems", total);
result.put("requiredTotal", requiredTotal);
result.put("requiredPassed", requiredPassed);
result.put("requiredFailed", requiredTotal - requiredPassed);
result.put("isComplete", requiredPassed == requiredTotal);
result.put("checks", checks);
return result;
}
@Override
public List<EmrCompletenessCheck> getCheckResults(Long emrId) {
return emrCompletenessCheckService.selectByEmrId(emrId);
}
private Map<String, Object> parseContent(String contextJson) {
Map<String, Object> map = new HashMap<>();
if (contextJson == null || contextJson.isEmpty()) {
return map;
}
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
@SuppressWarnings("unchecked")
Map<String, Object> parsed = mapper.readValue(contextJson, Map.class);
map.putAll(parsed);
} catch (Exception e) {
map.put("raw", contextJson);
}
return map;
}
}

View File

@@ -0,0 +1,38 @@
package com.healthlink.his.web.emr.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.web.emr.appservice.IEmrCompletenessAppService;
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.Map;
@RestController
@RequestMapping("/emr/completeness")
@Slf4j
@AllArgsConstructor
@Tag(name = "病历完整性检查")
public class EmrCompletenessController {
private final IEmrCompletenessAppService emrCompletenessAppService;
@PostMapping("/check")
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
@Operation(summary = "执行病历完整性检查")
public R<Map<String, Object>> checkCompleteness(
@RequestParam("emrId") Long emrId,
@RequestParam("encounterId") Long encounterId) {
return R.ok(emrCompletenessAppService.checkCompleteness(emrId, encounterId));
}
@GetMapping("/results/{emrId}")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
@Operation(summary = "获取完整性检查结果")
public R<?> getCheckResults(@PathVariable Long emrId) {
return R.ok(emrCompletenessAppService.getCheckResults(emrId));
}
}

View File

@@ -13,3 +13,6 @@ export function compareEmrRevisions(id1, id2) { return request({ url: "/emr/revi
export function saveEmrVersion(data) { return request({ url: "/emr/version/save", method: "post", data }) }
export function getEmrVersionList(emrId) { return request({ url: "/emr/version/list/" + emrId, method: "get" }) }
export function compareEmrVersions(id1, id2) { return request({ url: "/emr/version/compare", method: "get", params: { versionId1: id1, versionId2: id2 } }) }
export function checkCompleteness(emrId, encounterId) { return request({ url: "/emr/completeness/check", method: "post", params: { emrId, encounterId } }) }
export function getCompletenessResults(emrId) { return request({ url: "/emr/completeness/results/" + emrId, method: "get" }) }

View File

@@ -0,0 +1,156 @@
<template>
<div class="app-container">
<el-card shadow="hover" class="mb8">
<template #header>
<span>执行完整性检查</span>
</template>
<el-form :inline="true" :model="checkForm">
<el-form-item label="病历ID">
<el-input v-model="checkForm.emrId" placeholder="请输入病历ID" clearable />
</el-form-item>
<el-form-item label="就诊ID">
<el-input v-model="checkForm.encounterId" placeholder="请输入就诊ID" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" :loading="checkLoading" @click="handleCheck">
执行检查
</el-button>
</el-form-item>
</el-form>
</el-card>
<el-row v-if="checkResult" :gutter="20" class="mb8">
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="检查项总数" :value="checkResult.totalItems" />
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="必填项总数" :value="checkResult.requiredTotal" />
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="必填项通过" :value="checkResult.requiredPassed" />
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="是否完整" :value="checkResult.isComplete ? '是' : '否'" />
</el-card>
</el-col>
</el-row>
<el-card v-if="checkResult && checkResult.requiredFailed > 0" shadow="hover" class="mb8">
<template #header>
<span style="color: #e6a23c;">不合格项提醒</span>
</template>
<el-alert
v-for="item in failedItems"
:key="item.id"
:title="item.checkItem"
:description="item.checkDetail"
type="warning"
show-icon
class="mb4"
/>
</el-card>
<el-card shadow="hover">
<template #header>
<span>检查结果明细</span>
</template>
<el-table v-loading="resultLoading" :data="resultList">
<el-table-column label="检查项" prop="checkItem" width="150" />
<el-table-column label="分类" prop="checkCategory" width="120">
<template #default="scope">
<el-tag>{{ categoryMap[scope.row.checkCategory] || scope.row.checkCategory }}</el-tag>
</template>
</el-table-column>
<el-table-column label="是否必填" prop="isRequired" width="100">
<template #default="scope">
<el-tag :type="scope.row.isRequired ? 'danger' : 'info'">
{{ scope.row.isRequired ? '必填' : '选填' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="检查结果" prop="checkResult" width="120">
<template #default="scope">
<el-tag :type="scope.row.checkResult === 'PASS' ? 'success' : 'danger'">
{{ scope.row.checkResult === 'PASS' ? '通过' : '不合格' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="详情" prop="checkDetail" min-width="200" show-overflow-tooltip />
<el-table-column label="检查时间" prop="checkTime" width="180" />
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { checkCompleteness, getCompletenessResults } from '@/api/emr'
import { ElMessage } from 'element-plus'
const checkLoading = ref(false)
const resultLoading = ref(false)
const checkResult = ref(null)
const resultList = ref([])
const checkForm = reactive({ emrId: '', encounterId: '' })
const categoryMap = {
basic: '基本信息',
examination: '检查',
diagnosis: '诊断',
treatment: '治疗',
signature: '签名'
}
const failedItems = computed(() => {
if (!checkResult.value || !checkResult.value.checks) return []
return checkResult.value.checks.filter(c => c.checkResult === 'FAIL')
})
const handleCheck = async () => {
if (!checkForm.emrId) {
ElMessage.warning('请输入病历ID')
return
}
if (!checkForm.encounterId) {
ElMessage.warning('请输入就诊ID')
return
}
checkLoading.value = true
try {
const res = await checkCompleteness(checkForm.emrId, checkForm.encounterId)
checkResult.value = res.data || res
resultList.value = checkResult.value.checks || []
ElMessage.success(checkResult.value.isComplete ? '病历完整性检查通过' : '病历完整性检查未通过,存在不合格项')
} catch (e) {
ElMessage.error('执行检查失败')
} finally {
checkLoading.value = false
}
}
const loadResults = async () => {
if (!checkForm.emrId) return
resultLoading.value = true
try {
const res = await getCompletenessResults(checkForm.emrId)
resultList.value = res.data || res || []
} catch (e) {
ElMessage.error('获取检查结果失败')
} finally {
resultLoading.value = false
}
}
</script>
<style scoped>
.mb8 { margin-bottom: 8px; }
.mb4 { margin-bottom: 4px; }
</style>