feat: TCM中医模块前端页面 + 数据库表修复 + ESB表补全

- TCM方剂管理前端页面(方剂列表/新增/详情)
- TCM体质辨识前端页面(评估/查询)
- 修复TcmPrescription PostgreSQL reserved word 'usage'字段映射
- 修复HisBaseEntity deleteFlag冲突
- 创建V39迁移(TCM+ESB共9张表)
- ESB sys_esb_message补全delete_flag列
- 所有API测试通过
This commit is contained in:
2026-06-07 18:07:57 +08:00
parent 5afeece809
commit 50a73cc626
6 changed files with 401 additions and 19 deletions

View File

@@ -15,22 +15,22 @@ public class TcmAppServiceImpl implements ITcmAppService {
public List<TcmPrescription> getPrescriptions(String type) {
return prescriptionService.list(new LambdaQueryWrapper<TcmPrescription>()
.eq(type != null, TcmPrescription::getPrescriptionType, type)
.eq(TcmPrescription::getEnabled, "1").eq(TcmPrescription::getDelFlag, "0"));
.eq(TcmPrescription::getEnabled, "1").eq(TcmPrescription::getDeleteFlag, "0"));
}
@Override
public TcmPrescription savePrescription(TcmPrescription p) { p.setDelFlag("0"); p.setEnabled("1"); prescriptionService.save(p); return p; }
public TcmPrescription savePrescription(TcmPrescription p) { p.setDeleteFlag("0"); p.setEnabled("1"); prescriptionService.save(p); return p; }
@Override
public TcmConstitutionAssessment assess(TcmConstitutionAssessment a) { a.setDelFlag("0"); assessmentService.save(a); return a; }
public TcmConstitutionAssessment assess(TcmConstitutionAssessment a) { a.setDeleteFlag("0"); assessmentService.save(a); return a; }
@Override
public List<TcmConstitutionAssessment> getAssessmentsByEncounter(Long encounterId) {
return assessmentService.list(new LambdaQueryWrapper<TcmConstitutionAssessment>()
.eq(TcmConstitutionAssessment::getEncounterId, encounterId).eq(TcmConstitutionAssessment::getDelFlag, "0"));
.eq(TcmConstitutionAssessment::getEncounterId, encounterId).eq(TcmConstitutionAssessment::getDeleteFlag, "0"));
}
@Override
public Map<String, Object> getStatistics() {
Map<String, Object> r = new HashMap<>();
r.put("totalPrescriptions", prescriptionService.count(new LambdaQueryWrapper<TcmPrescription>().eq(TcmPrescription::getDelFlag, "0")));
r.put("totalAssessments", assessmentService.count(new LambdaQueryWrapper<TcmConstitutionAssessment>().eq(TcmConstitutionAssessment::getDelFlag, "0")));
r.put("totalPrescriptions", prescriptionService.count(new LambdaQueryWrapper<TcmPrescription>().eq(TcmPrescription::getDeleteFlag, "0")));
r.put("totalAssessments", assessmentService.count(new LambdaQueryWrapper<TcmConstitutionAssessment>().eq(TcmConstitutionAssessment::getDeleteFlag, "0")));
return r;
}
}

View File

@@ -0,0 +1,166 @@
-- 中医方剂表
CREATE TABLE IF NOT EXISTS healthlink_his.tcm_prescription (
id BIGSERIAL PRIMARY KEY,
prescription_name VARCHAR(200) NOT NULL,
prescription_type VARCHAR(50),
herbs TEXT,
dosage VARCHAR(500),
usage VARCHAR(500),
indication TEXT,
source VARCHAR(200),
enabled CHAR(1) DEFAULT '1',
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- 中医体质辨识表
CREATE TABLE IF NOT EXISTS healthlink_his.tcm_constitution_assessment (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT,
patient_id BIGINT,
constitution_type VARCHAR(50),
score INTEGER,
recommendation TEXT,
assessor_id BIGINT,
assessment_time TIMESTAMP,
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- ESB消息表
CREATE TABLE IF NOT EXISTS healthlink_his.sys_esb_message (
id BIGSERIAL PRIMARY KEY,
message_id VARCHAR(100) NOT NULL,
message_type VARCHAR(50),
source_system VARCHAR(100),
target_system VARCHAR(100),
message_content TEXT,
message_format VARCHAR(50),
status VARCHAR(20) DEFAULT 'PENDING',
retry_count INTEGER DEFAULT 0,
error_message TEXT,
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- ESB服务注册表
CREATE TABLE IF NOT EXISTS healthlink_his.sys_esb_service_registry (
id BIGSERIAL PRIMARY KEY,
service_name VARCHAR(200) NOT NULL,
service_version VARCHAR(50),
service_endpoint VARCHAR(500),
service_description TEXT,
service_status VARCHAR(20) DEFAULT 'ACTIVE',
protocol VARCHAR(50),
timeout_ms INTEGER DEFAULT 5000,
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- ESB监控统计表
CREATE TABLE IF NOT EXISTS healthlink_his.esb_monitor_stats (
id BIGSERIAL PRIMARY KEY,
stat_hour TIMESTAMP,
source_system VARCHAR(100),
target_system VARCHAR(100),
total_count INTEGER DEFAULT 0,
success_count INTEGER DEFAULT 0,
fail_count INTEGER DEFAULT 0,
retry_count INTEGER DEFAULT 0,
avg_duration_ms INTEGER DEFAULT 0,
tenant_id INTEGER DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ESB死信队列表
CREATE TABLE IF NOT EXISTS healthlink_his.esb_dead_letter (
id BIGSERIAL PRIMARY KEY,
message_id VARCHAR(100) NOT NULL,
source_system VARCHAR(100),
target_system VARCHAR(100),
message_type VARCHAR(50),
message_content TEXT,
error_message TEXT,
retry_count INTEGER DEFAULT 0,
max_retry INTEGER DEFAULT 3,
first_fail_time TIMESTAMP,
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- ESB CDA文档表
CREATE TABLE IF NOT EXISTS healthlink_his.esb_cda_document (
id BIGSERIAL PRIMARY KEY,
document_type VARCHAR(50),
document_title VARCHAR(200),
encounter_id BIGINT,
patient_id BIGINT,
cda_xml TEXT,
status VARCHAR(20) DEFAULT 'DRAFT',
version_id INTEGER DEFAULT 1,
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- ESB编码映射表
CREATE TABLE IF NOT EXISTS healthlink_his.esb_code_mapping (
id BIGSERIAL PRIMARY KEY,
source_system VARCHAR(100),
source_code VARCHAR(100),
target_system VARCHAR(100),
target_code VARCHAR(100),
mapping_type VARCHAR(50),
description TEXT,
tenant_id INTEGER DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ESB FHIR资源表
CREATE TABLE IF NOT EXISTS healthlink_his.esb_fhir_resource (
id BIGSERIAL PRIMARY KEY,
resource_type VARCHAR(50),
resource_id VARCHAR(100),
encounter_id BIGINT,
patient_id BIGINT,
resource_json TEXT,
status VARCHAR(20) DEFAULT 'ACTIVE',
version_id INTEGER DEFAULT 1,
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- 索引
CREATE INDEX IF NOT EXISTS idx_tcm_prescription_type ON healthlink_his.tcm_prescription(prescription_type);
CREATE INDEX IF NOT EXISTS idx_tcm_constitution_encounter ON healthlink_his.tcm_constitution_assessment(encounter_id);
CREATE INDEX IF NOT EXISTS idx_esb_message_status ON healthlink_his.sys_esb_message(status);
CREATE INDEX IF NOT EXISTS idx_esb_message_source ON healthlink_his.sys_esb_message(source_system);
CREATE INDEX IF NOT EXISTS idx_esb_dead_letter_msg ON healthlink_his.esb_dead_letter(message_id);
CREATE INDEX IF NOT EXISTS idx_esb_monitor_hour ON healthlink_his.esb_monitor_stats(stat_hour);

View File

@@ -3,13 +3,23 @@ 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 lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
@Data @TableName("tcm_constitution_assessment") @Accessors(chain = true) @EqualsAndHashCode(callSuper = false)
@Data
@TableName("tcm_constitution_assessment")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class TcmConstitutionAssessment extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private Long encounterId; private Long patientId;
private String constitutionType; private Integer score;
private String recommendation; private Long assessorId;
private Date assessmentTime; private String delFlag;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long encounterId;
private Long patientId;
private String constitutionType;
private Integer score;
private String recommendation;
private Long assessorId;
private Date assessmentTime;
}

View File

@@ -1,13 +1,27 @@
package com.healthlink.his.tcm.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors;
@Data @TableName("tcm_prescription") @Accessors(chain = true) @EqualsAndHashCode(callSuper = false)
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@Data
@TableName("tcm_prescription")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class TcmPrescription extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private String prescriptionName; private String prescriptionType;
private String herbs; private String dosage; private String usage;
private String indication; private String source; private String enabled; private String delFlag;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String prescriptionName;
private String prescriptionType;
private String herbs;
private String dosage;
@TableField("\"usage\"")
private String usage;
private String indication;
private String source;
private String enabled;
}

View File

@@ -0,0 +1,88 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px">
<el-form-item label="就诊号" prop="encounterId">
<el-input v-model="queryParams.encounterId" placeholder="请输入就诊号" clearable style="width: 180px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
<el-button type="success" icon="Plus" @click="handleAdd">新增评估</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="assessmentList" border>
<el-table-column label="患者" align="center" prop="patientName" width="120" />
<el-table-column label="就诊号" align="center" prop="encounterId" width="140" />
<el-table-column label="体质类型" align="center" prop="constitutionType" width="140">
<template #default="scope"><el-tag>{{ constitutionText(scope.row.constitutionType) }}</el-tag></template>
</el-table-column>
<el-table-column label="评估得分" align="center" prop="score" width="100" />
<el-table-column label="评估医生" align="center" prop="assessorName" width="120" />
<el-table-column label="评估时间" align="center" prop="assessTime" width="180" />
<el-table-column label="调理建议" align="center" prop="suggestion" min-width="200" show-overflow-tooltip />
</el-table>
<el-dialog title="中医体质辨识" v-model="addVisible" 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.encounterId" placeholder="请输入就诊号" /></el-form-item>
<el-form-item label="体质类型">
<el-select v-model="formData.constitutionType" placeholder="请选择">
<el-option v-for="item in constitutionTypes" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="评估得分"><el-input-number v-model="formData.score" :min="0" :max="100" /></el-form-item>
<el-form-item label="调理建议"><el-input v-model="formData.suggestion" type="textarea" :rows="3" placeholder="请输入调理建议" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="addVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="TcmConstitution" lang="js">
import { ref, reactive, toRefs, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
const assessmentList = ref([])
const loading = ref(false)
const addVisible = ref(false)
const constitutionTypes = [
{ value: 'PINGHE', label: '平和质' },
{ value: 'QIXU', label: '气虚质' },
{ value: 'YANGXU', label: '阳虚质' },
{ value: 'YINXU', label: '阴虚质' },
{ value: 'TANSHI', label: '痰湿质' },
{ value: 'SHIRE', label: '湿热质' },
{ value: 'XUEYU', label: '血瘀质' },
{ value: 'QIYU', label: '气郁质' },
{ value: 'TEBING', label: '特禀质' }
]
const data = reactive({
queryParams: { encounterId: undefined },
formData: { patientName: '', encounterId: '', constitutionType: 'PINGHE', score: 0, suggestion: '' }
})
const { queryParams, formData } = toRefs(data)
function constitutionText(t) {
const m = { PINGHE: '平和质', QIXU: '气虚质', YANGXU: '阳虚质', YINXU: '阴虚质', TANSHI: '痰湿质', SHIRE: '湿热质', XUEYU: '血瘀质', QIYU: '气郁质', TEBING: '特禀质' }
return m[t] || t
}
function handleQuery() {
loading.value = true
const url = queryParams.value.encounterId
? `/api/v1/tcm/constitution/encounter/${queryParams.value.encounterId}`
: '/api/v1/tcm/statistics'
request({ url, method: 'get' }).then(res => {
assessmentList.value = Array.isArray(res.data) ? res.data : (res.data?.records || [])
}).catch(() => { assessmentList.value = [] }).finally(() => { loading.value = false })
}
function handleAdd() {
formData.value = { patientName: '', encounterId: '', constitutionType: 'PINGHE', score: 0, suggestion: '' }
addVisible.value = true
}
function submitForm() {
request({ url: '/api/v1/tcm/constitution', method: 'post', data: formData.value }).then(() => {
ElMessage.success('评估完成'); addVisible.value = false; handleQuery()
})
}
onMounted(() => { handleQuery() })
</script>

View File

@@ -0,0 +1,104 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px">
<el-form-item label="方剂类型" prop="type">
<el-select v-model="queryParams.type" placeholder="请选择" clearable style="width: 160px">
<el-option label="经典方剂" value="CLASSIC" />
<el-option label="经验方" value="EXPERIENCE" />
<el-option label="协定方" value="AGREEMENT" />
<el-option label="壮医方" value="ZHUANG" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
<el-button type="success" icon="Plus" @click="handleAdd">新增方剂</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="prescriptionList" border>
<el-table-column label="方剂名称" align="center" prop="name" min-width="160" show-overflow-tooltip />
<el-table-column label="方剂类型" align="center" prop="type" width="120">
<template #default="scope">
<el-tag>{{ typeText(scope.row.type) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="组成" align="center" prop="composition" min-width="200" show-overflow-tooltip />
<el-table-column label="功效" align="center" prop="effect" min-width="150" show-overflow-tooltip />
<el-table-column label="主治" align="center" prop="indication" min-width="150" show-overflow-tooltip />
<el-table-column label="用法用量" align="center" prop="dosage" width="120" />
<el-table-column label="操作" align="center" width="80" fixed="right">
<template #default="scope">
<el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="方剂详情" v-model="detailVisible" width="650px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="方剂名称">{{ detailData.name }}</el-descriptions-item>
<el-descriptions-item label="方剂类型">{{ typeText(detailData.type) }}</el-descriptions-item>
<el-descriptions-item label="组成" :span="2">{{ detailData.composition }}</el-descriptions-item>
<el-descriptions-item label="功效" :span="2">{{ detailData.effect }}</el-descriptions-item>
<el-descriptions-item label="主治" :span="2">{{ detailData.indication }}</el-descriptions-item>
<el-descriptions-item label="用法用量">{{ detailData.dosage }}</el-descriptions-item>
<el-descriptions-item label="方解">{{ detailData.analysis }}</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="detailVisible = false">关闭</el-button></template>
</el-dialog>
<el-dialog title="新增方剂" v-model="addVisible" width="600px" append-to-body>
<el-form :model="formData" label-width="100px">
<el-form-item label="方剂名称"><el-input v-model="formData.name" placeholder="请输入方剂名称" /></el-form-item>
<el-form-item label="方剂类型">
<el-select v-model="formData.type" placeholder="请选择">
<el-option label="经典方剂" value="CLASSIC" />
<el-option label="经验方" value="EXPERIENCE" />
<el-option label="协定方" value="AGREEMENT" />
<el-option label="壮医方" value="ZHUANG" />
</el-select>
</el-form-item>
<el-form-item label="组成"><el-input v-model="formData.composition" type="textarea" :rows="3" placeholder="请输入药物组成" /></el-form-item>
<el-form-item label="功效"><el-input v-model="formData.effect" placeholder="请输入功效" /></el-form-item>
<el-form-item label="主治"><el-input v-model="formData.indication" placeholder="请输入主治病症" /></el-form-item>
<el-form-item label="用法用量"><el-input v-model="formData.dosage" placeholder="请输入用法用量" /></el-form-item>
<el-form-item label="方解"><el-input v-model="formData.analysis" type="textarea" :rows="2" placeholder="请输入方解" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="addVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="TcmPrescription" lang="js">
import { ref, reactive, toRefs, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
const prescriptionList = ref([])
const loading = ref(false)
const detailVisible = ref(false)
const addVisible = ref(false)
const detailData = ref({})
const data = reactive({
queryParams: { type: undefined },
formData: { name: '', type: 'CLASSIC', composition: '', effect: '', indication: '', dosage: '', analysis: '' }
})
const { queryParams, formData } = toRefs(data)
function typeText(t) {
const m = { CLASSIC: '经典方剂', EXPERIENCE: '经验方', AGREEMENT: '协定方', ZHUANG: '壮医方' }
return m[t] || t
}
function handleQuery() {
loading.value = true
request({ url: '/api/v1/tcm/prescriptions', method: 'get', params: queryParams.value }).then(res => {
prescriptionList.value = res.data || []
}).finally(() => { loading.value = false })
}
function handleReset() { queryParams.value.type = undefined; handleQuery() }
function handleDetail(row) { detailData.value = row; detailVisible.value = true }
function handleAdd() { formData.value = { name: '', type: 'CLASSIC', composition: '', effect: '', indication: '', dosage: '', analysis: '' }; addVisible.value = true }
function submitForm() {
request({ url: '/api/v1/tcm/prescription', method: 'post', data: formData.value }).then(() => {
ElMessage.success('新增成功'); addVisible.value = false; handleQuery()
})
}
onMounted(() => { handleQuery() })
</script>