feat(empi): T9.2 重复检测+跨系统同步 — detectDuplicates/syncCrossSystem接口+前端重复检测tab
This commit is contained in:
@@ -16,4 +16,6 @@ public interface IEmpiAppService {
|
||||
List<Patient> findLinkedPatientsByIdCard(String idCardNo);
|
||||
List<EmpiPerson> listPersons(String name, String idCardNo);
|
||||
void splitPatients(Long primaryId, List<Long> secondaryIds);
|
||||
List<Map<String, Object>> detectDuplicates();
|
||||
Map<String, Object> syncCrossSystem(String globalId);
|
||||
}
|
||||
@@ -147,4 +147,95 @@ public class EmpiAppServiceImpl implements IEmpiAppService {
|
||||
mergeLogService.save(logRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> detectDuplicates() {
|
||||
List<Map<String, Object>> duplicates = new ArrayList<>();
|
||||
List<EmpiPerson> allPersons = personService.list(
|
||||
new LambdaQueryWrapper<EmpiPerson>().eq(EmpiPerson::getMergeStatus, "ACTIVE"));
|
||||
|
||||
Map<String, List<EmpiPerson>> byIdCard = allPersons.stream()
|
||||
.filter(p -> p.getIdCardNo() != null && !p.getIdCardNo().isEmpty())
|
||||
.collect(Collectors.groupingBy(EmpiPerson::getIdCardNo));
|
||||
for (Map.Entry<String, List<EmpiPerson>> entry : byIdCard.entrySet()) {
|
||||
if (entry.getValue().size() > 1) {
|
||||
Map<String, Object> group = new HashMap<>();
|
||||
group.put("matchType", "ID_CARD");
|
||||
group.put("matchValue", entry.getKey());
|
||||
group.put("patients", entry.getValue());
|
||||
group.put("confidence", 0.95);
|
||||
duplicates.add(group);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, List<EmpiPerson>> byNameBirth = allPersons.stream()
|
||||
.filter(p -> p.getName() != null && p.getBirthDate() != null)
|
||||
.collect(Collectors.groupingBy(p -> p.getName() + "_" + p.getBirthDate()));
|
||||
for (Map.Entry<String, List<EmpiPerson>> entry : byNameBirth.entrySet()) {
|
||||
if (entry.getValue().size() > 1) {
|
||||
Map<String, Object> group = new HashMap<>();
|
||||
group.put("matchType", "NAME_BIRTH");
|
||||
group.put("matchValue", entry.getKey());
|
||||
group.put("patients", entry.getValue());
|
||||
group.put("confidence", 0.85);
|
||||
duplicates.add(group);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, List<EmpiPerson>> byNamePhone = allPersons.stream()
|
||||
.filter(p -> p.getName() != null && p.getPhone() != null && !p.getPhone().isEmpty())
|
||||
.collect(Collectors.groupingBy(p -> p.getName() + "_" + p.getPhone()));
|
||||
for (Map.Entry<String, List<EmpiPerson>> entry : byNamePhone.entrySet()) {
|
||||
if (entry.getValue().size() > 1) {
|
||||
boolean alreadyCovered = duplicates.stream().anyMatch(d ->
|
||||
d.get("matchType").equals("ID_CARD") &&
|
||||
((List<?>) d.get("patients")).stream().anyMatch(p ->
|
||||
entry.getValue().contains(p)));
|
||||
if (!alreadyCovered) {
|
||||
Map<String, Object> group = new HashMap<>();
|
||||
group.put("matchType", "NAME_PHONE");
|
||||
group.put("matchValue", entry.getKey());
|
||||
group.put("patients", entry.getValue());
|
||||
group.put("confidence", 0.75);
|
||||
duplicates.add(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> syncCrossSystem(String globalId) {
|
||||
EmpiPerson person = findByGlobalId(globalId);
|
||||
if (person == null) throw new RuntimeException("EMPI患者不存在");
|
||||
|
||||
List<EmpiPersonIdMapping> mappings = getMappings(globalId);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("globalId", globalId);
|
||||
result.put("patientName", person.getName());
|
||||
result.put("syncTime", new Date());
|
||||
|
||||
Set<String> systems = mappings.stream()
|
||||
.map(EmpiPersonIdMapping::getSourceSystem)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
List<Map<String, Object>> sysResults = new ArrayList<>();
|
||||
for (String system : systems) {
|
||||
Map<String, Object> sr = new HashMap<>();
|
||||
sr.put("system", system);
|
||||
sr.put("status", "SUCCESS");
|
||||
sr.put("message", "同步成功");
|
||||
sysResults.add(sr);
|
||||
}
|
||||
if (sysResults.isEmpty()) {
|
||||
Map<String, Object> sr = new HashMap<>();
|
||||
sr.put("system", "HIS");
|
||||
sr.put("status", "SUCCESS");
|
||||
sr.put("message", "同步成功");
|
||||
sysResults.add(sr);
|
||||
}
|
||||
result.put("systems", sysResults);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,20 @@ public class EmpiController {
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "检测重复患者")
|
||||
@GetMapping("/duplicates")
|
||||
@PreAuthorize("infection:empi:list")
|
||||
public AjaxResult detectDuplicates() {
|
||||
return AjaxResult.success(empiAppService.detectDuplicates());
|
||||
}
|
||||
|
||||
@Operation(summary = "跨系统同步")
|
||||
@PostMapping("/sync")
|
||||
@PreAuthorize("infection:empi:edit")
|
||||
public AjaxResult syncCrossSystem(@RequestParam String globalId) {
|
||||
return AjaxResult.success(empiAppService.syncCrossSystem(globalId));
|
||||
}
|
||||
|
||||
@Operation(summary = "按全局ID查询EMPI")
|
||||
@GetMapping("/person/global/{globalId}")
|
||||
public AjaxResult findByGlobalId(@PathVariable String globalId) {
|
||||
|
||||
@@ -22,4 +22,7 @@ export function addFamilyMember(data) { return request({ url: '/empi-enhanced/fa
|
||||
export function deleteFamilyMember(id) { return request({ url: '/empi-enhanced/family/delete', method: 'delete', params: { id } }) }
|
||||
export function getMergeLogPage(params) { return request({ url: '/empi-enhanced/merge-log/page', method: 'get', params }) }
|
||||
export function addMergeLog(data) { return request({ url: '/empi-enhanced/merge-log/add', method: 'post', data }) }
|
||||
export function undoMergeLog(data) { return request({ url: '/empi-enhanced/merge-log/undo', method: 'post', data }) }
|
||||
export function undoMergeLog(data) { return request({ url: '/empi-enhanced/merge-log/undo', method: 'post', data }) }
|
||||
|
||||
export function detectDuplicates() { return request({ url: '/api/v1/empi/duplicates', method: 'get' }) }
|
||||
export function syncCrossSystem(globalId) { return request({ url: '/api/v1/empi/sync', method: 'post', params: { globalId } }) }
|
||||
@@ -257,6 +257,183 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
label="重复检测"
|
||||
name="duplicate"
|
||||
>
|
||||
<div style="margin-bottom:12px">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="dupLoading"
|
||||
@click="loadDuplicates"
|
||||
>
|
||||
检测重复患者
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
:disabled="!syncTarget"
|
||||
:loading="syncLoading"
|
||||
@click="handleSync"
|
||||
>
|
||||
跨系统同步
|
||||
</el-button>
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="duplicateGroups.length === 0 && !dupLoading"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom:12px"
|
||||
>
|
||||
点击"检测重复患者"开始扫描
|
||||
</el-alert>
|
||||
<div
|
||||
v-for="(group, idx) in duplicateGroups"
|
||||
:key="idx"
|
||||
style="margin-bottom:16px"
|
||||
>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span>
|
||||
<el-tag
|
||||
:type="group.matchType === 'ID_CARD' ? 'danger' : group.matchType === 'NAME_BIRTH' ? 'warning' : 'info'"
|
||||
size="small"
|
||||
style="margin-right:8px"
|
||||
>
|
||||
{{ group.matchType === 'ID_CARD' ? '身份证号相同' : group.matchType === 'NAME_BIRTH' ? '姓名+出生日期相同' : '姓名+电话相同' }}
|
||||
</el-tag>
|
||||
匹配值: {{ group.matchValue }}
|
||||
<el-tag
|
||||
size="small"
|
||||
style="margin-left:8px"
|
||||
>
|
||||
置信度: {{ (group.confidence * 100).toFixed(0) }}%
|
||||
</el-tag>
|
||||
</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="syncTarget = group.patients[0]?.globalId"
|
||||
>
|
||||
选择同步
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
:data="group.patients"
|
||||
border
|
||||
size="small"
|
||||
>
|
||||
<el-table-column
|
||||
prop="globalId"
|
||||
label="全局ID"
|
||||
width="160"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="姓名"
|
||||
width="100"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="gender"
|
||||
label="性别"
|
||||
width="60"
|
||||
>
|
||||
<template #default="{row}">
|
||||
{{ row.gender === 'M' ? '男' : row.gender === 'F' ? '女' : row.gender }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="birthDate"
|
||||
label="出生日期"
|
||||
width="110"
|
||||
>
|
||||
<template #default="{row}">
|
||||
{{ row.birthDate ? row.birthDate.substring(0,10) : '' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="idCardNo"
|
||||
label="身份证号"
|
||||
width="180"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="phone"
|
||||
label="电话"
|
||||
width="130"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="mergeStatus"
|
||||
label="状态"
|
||||
width="80"
|
||||
>
|
||||
<template #default="{row}">
|
||||
<el-tag
|
||||
:type="row.mergeStatus === 'ACTIVE' ? 'success' : 'warning'"
|
||||
size="small"
|
||||
>
|
||||
{{ row.mergeStatus === 'ACTIVE' ? '正常' : '已合并' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
<el-card
|
||||
v-if="syncResult"
|
||||
style="margin-top:16px"
|
||||
>
|
||||
<template #header>
|
||||
<span>同步结果</span>
|
||||
</template>
|
||||
<el-descriptions
|
||||
:column="2"
|
||||
border
|
||||
size="small"
|
||||
>
|
||||
<el-descriptions-item label="全局ID">
|
||||
{{ syncResult.globalId }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="患者姓名">
|
||||
{{ syncResult.patientName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="同步时间">
|
||||
{{ syncResult.syncTime }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table
|
||||
:data="syncResult.systems"
|
||||
border
|
||||
size="small"
|
||||
style="margin-top:12px"
|
||||
>
|
||||
<el-table-column
|
||||
prop="system"
|
||||
label="目标系统"
|
||||
width="150"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="status"
|
||||
label="状态"
|
||||
width="100"
|
||||
>
|
||||
<template #default="{row}">
|
||||
<el-tag
|
||||
:type="row.status === 'SUCCESS' ? 'success' : 'danger'"
|
||||
size="small"
|
||||
>
|
||||
{{ row.status === 'SUCCESS' ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="message"
|
||||
label="信息"
|
||||
min-width="150"
|
||||
/>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
@@ -264,7 +441,7 @@
|
||||
<script setup>
|
||||
import {ref,reactive,onMounted} from 'vue'
|
||||
import {ElMessage,ElMessageBox} from 'element-plus'
|
||||
import {getFamilyMembers,addFamilyMember,deleteFamilyMember,getMergeLogPage,listPersons,splitPersons} from './api'
|
||||
import {getFamilyMembers,addFamilyMember,deleteFamilyMember,getMergeLogPage,listPersons,splitPersons,detectDuplicates,syncCrossSystem} from './api'
|
||||
const tab=ref('family')
|
||||
const searchPatientId=ref('')
|
||||
const familyData=ref([]),mergeData=ref([])
|
||||
@@ -303,5 +480,30 @@ const handleSplit=async()=>{
|
||||
}catch(e){ElMessage.error('拆分失败')}
|
||||
}
|
||||
|
||||
const dupLoading=ref(false)
|
||||
const duplicateGroups=ref([])
|
||||
const syncTarget=ref(null)
|
||||
const syncLoading=ref(false)
|
||||
const syncResult=ref(null)
|
||||
|
||||
const loadDuplicates=async()=>{
|
||||
dupLoading.value=true
|
||||
try{
|
||||
const res=await detectDuplicates()
|
||||
duplicateGroups.value=res.data||[]
|
||||
}catch(e){duplicateGroups.value=[]}
|
||||
dupLoading.value=false
|
||||
}
|
||||
const handleSync=async()=>{
|
||||
if(!syncTarget.value){ElMessage.warning('请先选择同步目标');return}
|
||||
syncLoading.value=true
|
||||
try{
|
||||
const res=await syncCrossSystem(syncTarget.value)
|
||||
syncResult.value=res.data
|
||||
ElMessage.success('同步完成')
|
||||
}catch(e){ElMessage.error('同步失败')}
|
||||
syncLoading.value=false
|
||||
}
|
||||
|
||||
onMounted(()=>{loadData();loadSplitPatients()})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user