功能概述(实体类与数据库设计👉2.1与2.2小节)
在医疗信息化系统实施过程中,我们经常遇到这样的场景:
场景一:某三甲医院需要将积累多年的患者检验数据接入LIS系统,但数据格式五花八门——"患者姓名"有的写"姓名",有的写"病人名称",有的甚至用拼音缩写...
场景二:使用通用工具(如Navicat、Excel插件等)进行数据导入,但面对多样化的Excel模板格式,往往需要大量人工干预和格式调整,效率低下且容易出错。此外,这些工具通常无法满足自定义业务需求,在处理涉及多张表关联的复杂业务场景时存在明显局限性。
本功能专为解决上述痛点而设计,实现**"变"与"不变"的智能桥梁**——无论Excel模板如何变化,LIS系统标准字段保持不变,通过可视化映射建立连接关系。
该功能支持:
🟢 自动匹配标识:绿色标签清晰显示系统智能识别的字段
🟠 手动调整区域:下拉框提供所有可用选项
🔴 必填项提醒:红色边框高亮提示未映射的必填字段
总流程如下:
1. 用户选择医院
↓
2. 设置读取行数(从第几行开始读取数据)
↓
3. 上传Excel文件
↓
4. 后端解析Excel,提取所有列名
↓
5. 前端接收列名,自动进行智能匹配
↓
6. 用户手动调整未匹配或匹配错误的字段
↓
7. 保存映射关系到数据库(JSON格式)
在配置模板阶段,导入数据时。系统会保存模板:
当实际使用过程中导入数据时,系统会根据保存的模板自定义匹配值:
// 从 BSpecimenServiceImpl.java 中的核心逻辑
/*
1. 读取Excel文件数据
{
"条码号": "20250120001",
"患者姓名": "张三",
"性别": "男",
"年龄": "45",
"科室": "内科",
"标本类型": "血液",
"申请医生": "李医生",
"申请时间": "2025-01-20 08:30",
"采样时间": "2025-01-20 09:00"
},
{
"条码号": "20250120002",
"患者姓名": "李四",
"性别": "女",
"年龄": "32",
"科室": "外科",
"标本类型": "尿液",
"申请医生": "王医生",
"申请时间": "2025-01-20 09:15",
"采样时间": "2025-01-20 09:30"
}
*/
List<Map<String,String>> dataList = util.importExcelList(file.getInputStream(), 2);
/*
2. 获取该医院的映射配置模板
{
"条码号": "《医院条码 唯一》",
"患者姓名": "《病患姓名》",
"性别": "病人性别 1 男 2 女 0 未知",
"年龄": "病人年龄",
"科室": "《申请科室》",
"标本类型": "《标本类型》 使用,,+分割",
"申请医生": "申请医生1 申请医生1与申请医生2二选一",
"申请时间": "《申请时间》",
"采样时间": "《采样时间》"
}
*/
Map<String, String> excelTemplateMap = bHospitalExcels.get(0).getExcelTemplateMap();
/*
将Excel文件读取的2条数据通过 映射配置模板 解析到UploadHospitalInfo中。
*/
for (Map<String, String> rowData : dataList) {
UploadHospitalInfo resultLie = new UploadHospitalInfo();
for (Map.Entry<String, String> entry : rowData.entrySet()) {
String excelKey = entry.getKey(); // Excel列名
String value = entry.getValue(); // Excel中的值
String lisField = excelTemplateMap.get(excelKey); // 对应的LIS字段的值
if("《医院条码 唯一》".equals(lisField)) {
dto.setPlaceholderCode(value);
} else if("《病患姓名》".equals(lisField)) {
dto.setPatientName(value);
}
// ... 其他字段的映射处理
}
}
一、前端实现
1.1 核心数据结构
| 关键数据结构 | 功能说明 |
|---|---|
excelLie | 用户上传的Excel表头数组 (源数据列名) |
lisLie | 系统预定义的LIS字段池 (目标字段名) |
mappedFields[] | 存储映射结果, 索引对应 excelLie位置值为选中的 lisLie项 |
autoMatchedIndexes[] | 记录通过算法自动匹配成功的excelLie索引位置 |
mapping{} | 最终生成的映射字典:{Excel列名: LIS字段名} |
data() {
return {
// 上传加载状态
uploadLoading: false,
// 映射关系对象 {Excel列名: LIS字段名}
mapping: {},
// 映射结果数组,存储每个Excel列对应的LIS字段
mappedFields: [],
// 自动匹配的索引数组,记录哪些字段是自动匹配的
autoMatchedIndexes: [],
// Excel表格的列名数组
excelLie: [],
// LIS系统标准字段列表
lisLie: [
"《医院条码 唯一》",
"门诊号",
"身份证号",
"《病患姓名》",
"病人性别 1 男 2 女 0 未知",
"病人年龄",
"年龄单位 1 岁 2 月 3 日",
"病历号",
"病床号",
"《医院id》",
"病人类型 0 门诊 1 住院 2 体检 3 其他",
"《标本类型》 使用,,+分割",
"试管类型",
"《申请科室》",
"申请医生1 申请医生1与申请医生2二选一",
"申请医生2",
"《申请时间》",
"采样窗口",
"《采样时间》",
"优先标志",
"临床诊断",
"备注",
],
// 表单数据
form: {
hospitalId: null,
startRow: 1,
excelUrl: null,
excelTemplate: null // JSON字符串,存储映射关系
}
}
}
1.2 核心 HTML 模板
<el-form-item label="" prop="excelTemplate">
<div v-for="(item, index) in excelLie" style="display: flex; justify-content: center">
<!-- Excel列名显示 -->
<div style="width: 100px; text-align: right">{{item}}:</div>
<!-- LIS列选择器 -->
<el-select
v-model="mappedFields[index]"
filterable
clearable
placeholder="请选择LIS列"
:class="{ 'required-border': isRequired(item) && !mappedFields[index] }"
>
<!-- 可用选项(未被其他项选中的LIS列) -->
<el-option
v-for="opt in availableOptions(index)"
:key="opt"
:label="opt"
:value="opt"
>
{{ opt }}
</el-option>
</el-select>
<!-- 匹配状态显示 -->
<div class="match-status">
<el-tag v-if="autoMatchedIndexes.includes(index)" type="success">自动匹配</el-tag>
<el-tag v-else-if="mappedFields[index]" type="info">手动选择</el-tag>
<el-tag v-else type="danger">未匹配</el-tag>
</div>
</div>
</el-form-item>
1.3 核心方法
用户操作 → 触发事件 → 更新mappedFields → watch监听 → 生成mapping → 存储JSON
↑ ↓
└────────────────────── 实时反馈 ─────────────────────────────┘
1.3.1 计算可用选项 (computed)
computed: {
// 计算当前可用的LIS列选项
availableOptions() {
return (index) => {
// 当前选择的列
const selectedValue = this.mappedFields[index];
// 获取已被其他项选中的值
const usedValues = this.mappedFields.filter(
(val, i) => i !== index && !!val
);
// 返回未使用的选项 + 当前已选项
return this.lisLie.filter(opt =>
!usedValues.includes(opt) || opt === selectedValue
);
}
}
}
1.3.2 判断必填字段
// 判断是否为必填字段
isRequired(field) {
return field.includes('《') && field.includes('》');
}
1.3.3 初始化映射关系
// 初始化匹配
initMapping() {
this.autoMatchedIndexes = [];
this.mappedFields = this.excelLie.map((excelCol, index) => {
// 在LIS列中查找相同项
const matchedIndex = this.lisLie.findIndex(
lisCol => lisCol === excelCol
);
if (matchedIndex !== -1) {
// 记录自动匹配的索引
this.autoMatchedIndexes.push(index);
return this.lisLie[matchedIndex];
}
return ''; // 没有匹配则返回空
});
}
1.3.4 Excel 上传成功处理
//el-upload的回调函数
handleSuccess(response, file, fileList) {
if (response.code == 200) {
this.uploadLoading = false;
this.form.excelUrl = response.data.url;
this.excelLie = response.data.importSpecimen; // 获取Excel列名
this.initMapping(); // 初始化映射
this.msgSuccess("上传成功");
} else {
this.uploadLoading = false;
this.msgError(response.msg);
this.form.excelUrl = [];
this.$refs['upload'].clearFiles();
}
}
1.3.5 监听映射变化
watch: {
mappedFields(newVal) {
// 监听mappedFields变化,并保存映射关系
this.excelLie.forEach((key, index) => {
this.mapping[key] = newVal[index];
});
// 转换为JSON字符串存储
this.form.excelTemplate = JSON.stringify(this.mapping)
}
}
1.3.6 修改时加载映射关系
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const id = row.id || this.ids
getExcel(id).then(response => {
this.form = response.data;
// 从JSON字符串解析映射关系
this.mapping = JSON.parse(this.form.excelTemplate);
this.excelLie = Object.keys(this.mapping); // Excel列名
this.mappedFields = Object.values(this.mapping); // 对应的LIS字段
this.open = true;
this.title = "修改医院管理";
});
}
1.3.7 表单重置
// 表单重置
reset() {
this.excelLie = [];
this.mapping = {};
this.mappedFields = [];
this.autoMatchedIndexes = [];
this.form = {
id: null,
createTime: null,
createBy: null,
updateTime: null,
updateBy: null,
hospitalId: null,
excelTemplate: null,
excelUrl: null,
startRow: 1
};
this.resetForm("form");
}
1.4 样式定义
/* 必填字段未填写时的边框样式 */
.required-border .el-input__inner {
border-color: #f56c6c !important;
}
二、后端实现
2.1 数据库表结构
CREATE TABLE `b_hospital_excel` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人',
`hospital_id` BIGINT(20) NOT NULL COMMENT '医院ID',
`start_row` INT(11) DEFAULT 1 COMMENT '模板读取起始行数',
`excel_template` TEXT COMMENT 'Excel映射模板(JSON格式)',
`excel_url` VARCHAR(500) DEFAULT NULL COMMENT 'Excel文件URL',
PRIMARY KEY (`id`),
KEY `idx_hospital_id` (`hospital_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='医院Excel映射配置表';
2.2 实体类
2.2.1 主实体 - BHospitalExcel
package com.ruoyi.lis.domain;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import java.util.HashMap;
import java.util.Map;
/**
* 医院管理对象 b_hospital_excel
*
* @author lianChen
* @date 2025-06-14
*/
public class BHospitalExcel extends BaseEntity {
private static final long serialVersionUID = 1L;
/** id */
private Long id;
/** 医院Id */
@Excel(name = "医院Id")
private Long hospitalId;
/** 模板读取行数 */
@Excel(name = "模板读取行数")
private Integer startRow;
/** excel模板 - 存储JSON格式的映射关系字符串 */
@Excel(name = "excel模板")
private String excelTemplate;
/** excel模板映射对象 - Map<Excel列名, LIS字段名> */
private Map<String, String> excelTemplateMap;
/** excelUrl - 上传的Excel文件URL */
@Excel(name = "excelUrl")
private String excelUrl;
/**
* 设置映射模板
* 自动将JSON字符串反序列化为Map对象
*/
public void setExcelTemplate(String excelTemplate) {
this.excelTemplate = excelTemplate;
// 自动反序列化为Map
if (excelTemplate != null && !excelTemplate.isEmpty()) {
Map<String, String> maps = JSON.parseObject(excelTemplate, Map.class);
this.excelTemplateMap = maps;
} else {
this.excelTemplateMap = new HashMap<>();
}
}
}
2.2.2 上传返回实体 - BHospitalExcelUp
package com.ruoyi.lis.domain;
import org.apache.commons.lang3.builder.ToStringBuilder;
import java.util.Set;
/**
* 上传Excel导出所有列
* @author Liutime
*/
public class BHospitalExcelUp {
/**
* excel列名集合
*/
private Set<String> importSpecimen;
/**
* 文件URL
*/
private String url;
}
2.3 Controller 层
package com.ruoyi.lis.controller;
import java.util.List;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.lis.domain.BHospitalExcel;
import com.ruoyi.lis.service.IBHospitalExcelService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
import org.springframework.web.multipart.MultipartFile;
/**
* 医院ExcelController
*
* @author lianChen
* @date 2025-06-14
*/
@RestController
@RequestMapping("/lis/excel")
public class BHospitalExcelController extends BaseController {
/**
* 导入Excel文件,解析列名并返回
* 核心接口: 用于上传Excel并获取列名
*/
@PreAuthorize("@ss.hasPermi('lis:excel:import')")
@Log(title = " 医院Excel", businessType = BusinessType.IMPORT)
@PostMapping("/importSpecimen")
public AjaxResult importSpecimen(MultipartFile file,
@RequestParam("hospitalId") Long hospitalId,
@RequestParam("startRow") Integer startRow) {
if (!file.getOriginalFilename().contains(".xls") &&
!file.getOriginalFilename().contains(".csv")) {
return AjaxResult.error("只能上传excel/csv文件");
}
try {
return AjaxResult.success(bHospitalExcelService.importSpecimen(file, hospitalId, startRow));
} catch (Exception e) {
return AjaxResult.error("请检查模板是否正确,error:" + e.getMessage());
}
}
}
2.4 Service 层
package com.ruoyi.lis.service.impl;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.lis.domain.BHospitalExcelUp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.lis.mapper.BHospitalExcelMapper;
import com.ruoyi.lis.domain.BHospitalExcel;
import com.ruoyi.lis.service.IBHospitalExcelService;
import org.springframework.web.multipart.MultipartFile;
@Service
public class BHospitalExcelServiceImpl implements IBHospitalExcelService {
/**
* 导入Excel并解析列名
* 核心方法: 读取Excel文件的列名并返回
*/
@Override
public BHospitalExcelUp importSpecimen(MultipartFile file, Long hospitalId, Integer startRow) {
BHospitalExcelUp bHospitalExcelUp = new BHospitalExcelUp();
ExcelUtil util = new ExcelUtil();
Set<String> strings = new HashSet<>();
try {
// 读取Excel文件,从指定行开始读取
List<Map<String,Object>> resultList = util.importExcelList(file.getInputStream(), startRow);
// 获取第一行的所有列名(key集合)
strings = resultList.get(0).keySet();
} catch (IOException e) {
throw new RuntimeException(e);
}
// 设置Excel列名集合
bHospitalExcelUp.setImportSpecimen(strings);
return bHospitalExcelUp;
}
}
三、核心特性说明
3.1 自动匹配机制
当Excel列名与LIS字段名完全相同时,系统会自动建立映射关系,并标记为"自动匹配"(绿色标签)。
3.2 字段唯一性保证
通过 availableOptions 计算属性,确保:
- 每个LIS字段只能被映射一次
- 已选择的字段会从其他下拉框中隐藏
- 当前选择的字段在自己的下拉框中仍然可见
3.3 必填字段校验
- 使用《》符号标记必填字段
- 必填字段未映射时,下拉框会显示红色边框
- 匹配状态显示为红色"未匹配"标签
3.4 映射关系存储
映射关系以JSON格式存储在数据库中:
{
"条码号": "《医院条码 唯一》",
"姓名": "《病患姓名》",
"性别": "病人性别 1 男 2 女 0 未知",
"年龄": "病人年龄",
"科室": "《申请科室》",
"标本类型": "《标本类型》 使用,,+分割"
}
3.5 响应式更新
通过 watch 监听器,映射关系会实时同步到表单的 excelTemplate 字段,确保数据一致性。
四、使用示例
4.1 新增映射配置
- 点击"新增"按钮
- 选择医院
- 设置读取行数(默认为1)
- 上传Excel文件
- 系统自动解析并显示映射界面
- 调整映射关系
- 点击"确定"保存
4.2 修改映射配置
- 选择已有配置,点击"修改"
- 系统加载已保存的映射关系
- 调整映射
- 保存更新
4.3 前端映射数据示例
// excelLie (Excel列名数组)
["条码号", "姓名", "性别", "年龄", "科室", "标本类型"]
// mappedFields (映射结果数组)
["《医院条码 唯一》", "《病患姓名》", "病人性别 1 男 2 女 0 未知", "病人年龄", "《申请科室》", "《标本类型》 使用,,+分割"]
// autoMatchedIndexes (自动匹配索引)
[1, 3] // 表示"姓名"和"年龄"是自动匹配的
// mapping (最终映射对象)
{
"条码号": "《医院条码 唯一》",
"姓名": "《病患姓名》",
"性别": "病人性别 1 男 2 女 0 未知",
"年龄": "病人年龄",
"科室": "《申请科室》",
"标本类型": "《标本类型》 使用,,+分割"
}
// form.excelTemplate (JSON字符串)
'{"条码号":"《医院条码 唯一》","姓名":"《病患姓名》",...}'