智能Excel映射引擎:告别繁琐数据对接

85 阅读9分钟

功能概述(实体类与数据库设计👉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. 点击"新增"按钮
  2. 选择医院
  3. 设置读取行数(默认为1)
  4. 上传Excel文件
  5. 系统自动解析并显示映射界面
  6. 调整映射关系
  7. 点击"确定"保存

4.2 修改映射配置

  1. 选择已有配置,点击"修改"
  2. 系统加载已保存的映射关系
  3. 调整映射
  4. 保存更新

4.3 前端映射数据示例

// excelLie (Excel列名数组)
["条码号", "姓名", "性别", "年龄", "科室", "标本类型"]

// mappedFields (映射结果数组)
["《医院条码 唯一》", "《病患姓名》", "病人性别 1 男 2 女 0 未知", "病人年龄", "《申请科室》", "《标本类型》 使用,,+分割"]

// autoMatchedIndexes (自动匹配索引)
[1, 3]  // 表示"姓名"和"年龄"是自动匹配的

// mapping (最终映射对象)
{
  "条码号": "《医院条码 唯一》",
  "姓名": "《病患姓名》",
  "性别": "病人性别 1 男 2 女 0 未知",
  "年龄": "病人年龄",
  "科室": "《申请科室》",
  "标本类型": "《标本类型》 使用,,+分割"
}

// form.excelTemplate (JSON字符串)
'{"条码号":"《医院条码 唯一》","姓名":"《病患姓名》",...}'