feat(aidiagnosis): add AI-assisted diagnosis suggestion module
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
-- V87: AI辅助诊疗 - AI诊断建议表
|
||||
|
||||
CREATE TABLE ai_diagnosis_suggestion (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
encounter_id BIGINT NOT NULL,
|
||||
patient_id BIGINT NOT NULL,
|
||||
symptom_text TEXT,
|
||||
diagnosis_suggestions TEXT,
|
||||
confidence_score DECIMAL(5,2),
|
||||
suggestion_source VARCHAR(32),
|
||||
accepted BOOLEAN DEFAULT FALSE,
|
||||
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)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ai_diagnosis_suggestion IS 'AI辅助诊疗建议';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.id IS '建议ID';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.encounter_id IS '就诊ID';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.patient_id IS '患者ID';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.symptom_text IS '症状描述';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.diagnosis_suggestions IS '诊断建议';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.confidence_score IS '置信度(0-100)';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.suggestion_source IS '建议来源(llm/rule/manual)';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.accepted IS '是否采纳';
|
||||
|
||||
CREATE INDEX idx_ai_diag_encounter ON ai_diagnosis_suggestion(encounter_id);
|
||||
CREATE INDEX idx_ai_diag_patient ON ai_diagnosis_suggestion(patient_id);
|
||||
CREATE INDEX idx_ai_diag_source ON ai_diagnosis_suggestion(suggestion_source);
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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.aidiagnosis.mapper.AiDiagnosisSuggestionMapper">
|
||||
|
||||
<resultMap type="com.healthlink.his.aidiagnosis.domain.AiDiagnosisSuggestion" id="AiDiagnosisSuggestionResult">
|
||||
<id column="id" property="id"/>
|
||||
<result column="encounter_id" property="encounterId"/>
|
||||
<result column="patient_id" property="patientId"/>
|
||||
<result column="symptom_text" property="symptomText"/>
|
||||
<result column="diagnosis_suggestions" property="diagnosisSuggestions"/>
|
||||
<result column="confidence_score" property="confidenceScore"/>
|
||||
<result column="suggestion_source" property="suggestionSource"/>
|
||||
<result column="accepted" property="accepted"/>
|
||||
<result column="tenant_id" property="tenantId"/>
|
||||
<result column="create_by" property="createBy"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="update_by" property="updateBy"/>
|
||||
<result column="update_time" property="updateTime"/>
|
||||
<result column="delete_flag" property="deleteFlag"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id, encounter_id, patient_id, symptom_text, diagnosis_suggestions,
|
||||
confidence_score, suggestion_source, accepted,
|
||||
tenant_id, create_by, create_time, update_by, update_time, delete_flag
|
||||
</sql>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.healthlink.his.aidiagnosis.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.core.common.core.domain.HisBaseEntity;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("ai_diagnosis_suggestion")
|
||||
public class AiDiagnosisSuggestion extends HisBaseEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long encounterId;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long patientId;
|
||||
|
||||
private String symptomText;
|
||||
|
||||
private String diagnosisSuggestions;
|
||||
|
||||
private BigDecimal confidenceScore;
|
||||
|
||||
private String suggestionSource;
|
||||
|
||||
private Boolean accepted;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.healthlink.his.aidiagnosis.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.healthlink.his.aidiagnosis.domain.AiDiagnosisSuggestion;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface AiDiagnosisSuggestionMapper extends BaseMapper<AiDiagnosisSuggestion> {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.healthlink.his.aidiagnosis.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.healthlink.his.aidiagnosis.domain.AiDiagnosisSuggestion;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IAiDiagnosisService extends IService<AiDiagnosisSuggestion> {
|
||||
|
||||
List<AiDiagnosisSuggestion> findByPatientId(Long patientId);
|
||||
|
||||
List<AiDiagnosisSuggestion> findByEncounterId(Long encounterId);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.healthlink.his.aidiagnosis.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.healthlink.his.aidiagnosis.domain.AiDiagnosisSuggestion;
|
||||
import com.healthlink.his.aidiagnosis.mapper.AiDiagnosisSuggestionMapper;
|
||||
import com.healthlink.his.aidiagnosis.service.IAiDiagnosisService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class AiDiagnosisServiceImpl extends ServiceImpl<AiDiagnosisSuggestionMapper, AiDiagnosisSuggestion> implements IAiDiagnosisService {
|
||||
|
||||
@Override
|
||||
public List<AiDiagnosisSuggestion> findByPatientId(Long patientId) {
|
||||
LambdaQueryWrapper<AiDiagnosisSuggestion> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiDiagnosisSuggestion::getPatientId, patientId)
|
||||
.orderByDesc(AiDiagnosisSuggestion::getCreateTime);
|
||||
return baseMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiDiagnosisSuggestion> findByEncounterId(Long encounterId) {
|
||||
LambdaQueryWrapper<AiDiagnosisSuggestion> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiDiagnosisSuggestion::getEncounterId, encounterId)
|
||||
.orderByDesc(AiDiagnosisSuggestion::getCreateTime);
|
||||
return baseMapper.selectList(wrapper);
|
||||
}
|
||||
}
|
||||
30
healthlink-his-ui/src/api/aidiagnosis/aiDiagnosis.js
Normal file
30
healthlink-his-ui/src/api/aidiagnosis/aiDiagnosis.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function aiDiagnosisSuggest(data) {
|
||||
return request({
|
||||
url: '/ai-diagnosis/suggest',
|
||||
method: 'post',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
|
||||
export function getAiDiagnosisHistory(patientId) {
|
||||
return request({
|
||||
url: '/ai-diagnosis/history/' + patientId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getAiDiagnosisHistoryByEncounter(encounterId) {
|
||||
return request({
|
||||
url: '/ai-diagnosis/history/encounter/' + encounterId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function acceptAiDiagnosis(id) {
|
||||
return request({
|
||||
url: '/ai-diagnosis/accept/' + id,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
186
healthlink-his-ui/src/views/aidiagnosis/AiDiagnosisSuggest.vue
Normal file
186
healthlink-his-ui/src/views/aidiagnosis/AiDiagnosisSuggest.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryFormRef" :inline="true" v-show="showSearch" label-width="100px">
|
||||
<el-form-item label="患者ID" prop="patientId">
|
||||
<el-input v-model="queryParams.patientId" placeholder="请输入患者ID" clearable style="width: 180px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">查询历史</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
<el-button type="success" icon="MagicStick" @click="handleNewSuggest">AI辅助诊断</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<vxe-table :data="historyList" :loading="loading" border stripe height="auto">
|
||||
<vxe-column type="seq" title="序号" width="70" />
|
||||
<vxe-column field="encounterId" title="就诊ID" width="100" />
|
||||
<vxe-column field="symptomText" title="症状描述" min-width="200" show-overflow />
|
||||
<vxe-column field="diagnosisSuggestions" title="AI诊断建议" min-width="300" show-overflow />
|
||||
<vxe-column field="confidenceScore" title="置信度" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="Number(row.confidenceScore) || 0" :stroke-width="12" :text-inside="true" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="suggestionSource" title="来源" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.suggestionSource === 'llm' ? 'success' : 'info'">{{ row.suggestionSource }}</el-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="accepted" title="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.accepted ? 'success' : 'warning'" size="small">{{ row.accepted ? '已采纳' : '待处理' }}</el-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="createTime" title="创建时间" width="170" />
|
||||
<vxe-column title="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="!row.accepted" link type="primary" icon="Check" v-hasPermi="['cdss:alert:acknowledge']" @click="handleAccept(row)">采纳</el-button>
|
||||
<span v-else class="text-gray">-</span>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
|
||||
<el-dialog title="AI辅助诊断" v-model="suggestDialogVisible" width="600px" append-to-body>
|
||||
<el-form :model="suggestForm" label-width="100px">
|
||||
<el-form-item label="就诊ID" required>
|
||||
<el-input v-model="suggestForm.encounterId" placeholder="请输入就诊ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="患者ID" required>
|
||||
<el-input v-model="suggestForm.patientId" placeholder="请输入患者ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="症状描述" required>
|
||||
<el-input v-model="suggestForm.symptomText" type="textarea" :rows="4" placeholder="请输入患者症状描述,如:发热3天,伴咳嗽" />
|
||||
</el-form-item>
|
||||
<el-form-item label="建议来源">
|
||||
<el-select v-model="suggestForm.source" placeholder="请选择" clearable>
|
||||
<el-option label="AI大模型" value="llm" />
|
||||
<el-option label="规则引擎" value="rule" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div v-if="suggestResult" class="suggest-result">
|
||||
<el-divider content-position="left">AI诊断结果</el-divider>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="诊断建议">{{ suggestResult.diagnosisSuggestions }}</el-descriptions-item>
|
||||
<el-descriptions-item label="置信度">
|
||||
<el-progress :percentage="Number(suggestResult.confidenceScore) || 0" :stroke-width="14" :text-inside="true" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="建议来源">{{ suggestResult.suggestionSource }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="suggestDialogVisible = false">关闭</el-button>
|
||||
<el-button v-if="!suggestResult" type="primary" :loading="suggestLoading" @click="submitSuggest">获取建议</el-button>
|
||||
<el-button v-if="suggestResult" type="success" @click="handleQuery">查看历史</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="AiDiagnosisSuggest">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { aiDiagnosisSuggest, getAiDiagnosisHistory, acceptAiDiagnosis } from '@/api/aidiagnosis/aiDiagnosis'
|
||||
|
||||
const historyList = ref([])
|
||||
const loading = ref(false)
|
||||
const showSearch = ref(true)
|
||||
|
||||
const queryParams = reactive({
|
||||
patientId: ''
|
||||
})
|
||||
|
||||
const suggestDialogVisible = ref(false)
|
||||
const suggestLoading = ref(false)
|
||||
const suggestResult = ref(null)
|
||||
const suggestForm = reactive({
|
||||
encounterId: '',
|
||||
patientId: '',
|
||||
symptomText: '',
|
||||
source: 'llm'
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
if (!queryParams.patientId) {
|
||||
historyList.value = []
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getAiDiagnosisHistory(queryParams.patientId)
|
||||
if (res.code === 200) {
|
||||
historyList.value = res.data || []
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryParams.patientId = ''
|
||||
historyList.value = []
|
||||
}
|
||||
|
||||
const handleNewSuggest = () => {
|
||||
suggestForm.encounterId = ''
|
||||
suggestForm.patientId = ''
|
||||
suggestForm.symptomText = ''
|
||||
suggestForm.source = 'llm'
|
||||
suggestResult.value = null
|
||||
suggestDialogVisible.value = true
|
||||
}
|
||||
|
||||
const submitSuggest = async () => {
|
||||
if (!suggestForm.encounterId || !suggestForm.patientId) {
|
||||
ElMessage.error('就诊ID和患者ID不能为空')
|
||||
return
|
||||
}
|
||||
if (!suggestForm.symptomText.trim()) {
|
||||
ElMessage.error('症状描述不能为空')
|
||||
return
|
||||
}
|
||||
suggestLoading.value = true
|
||||
try {
|
||||
const res = await aiDiagnosisSuggest({
|
||||
encounterId: suggestForm.encounterId,
|
||||
patientId: suggestForm.patientId,
|
||||
symptomText: suggestForm.symptomText,
|
||||
source: suggestForm.source
|
||||
})
|
||||
if (res.code === 200) {
|
||||
suggestResult.value = res.data
|
||||
ElMessage.success('AI诊断建议已生成')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '生成建议失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('生成建议失败')
|
||||
} finally {
|
||||
suggestLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAccept = async (row) => {
|
||||
try {
|
||||
const res = await acceptAiDiagnosis(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('已采纳该建议')
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.suggest-result {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user