【从前端到后端导入excel文件实现批量导入-笔记模仿芋道源码的《系统管理-用户管理-导入-批量导入》】

批量导入预约数据-笔记

前端

场馆列表

该列表进入出现的是这样的,这儿是列表操作

typescript 复制代码
      <el-table-column label="操作" align="center" width="220px">
        <template #default="scope">
          <el-button
            link
            type="primary"
            @click="openForm('update', scope.row.id, scope.row.name)"
            v-hasPermi="['sports:venue:update']"
          >
            编辑
          </el-button>
          <el-button
            link
            type="primary"
            @click="openImportForm('update', scope.row.id, scope.row.name, scope.row.capacity)"
            v-hasPermi="['sports:venue:update']"
          >
            批量导入预约
          </el-button>
          <el-button
            link
            type="danger"
            @click="handleDelete(scope.row.id)"
            v-hasPermi="['sports:venue:delete']"
          >
            删除
          </el-button>
        </template>
      </el-table-column>

批量导入操作在这儿

typescript 复制代码
          <el-button
            link
            type="primary"
            @click="openImportForm('update', scope.row.id, scope.row.name, scope.row.capacity)"
            v-hasPermi="['sports:venue:update']"
          >
            批量导入预约
          </el-button>
typescript 复制代码
  <!-- 批量导入 -->
  <UserImportForm ref="importFormRef" @success="getList" />
  import UserImportForm from './UserImportForm.vue'
  
  
  // 批量导入
	const importFormRef = ref()
	const openImportForm = (type: string, id?: number, name?: string, capacity: number) => {
  if (!importFormRef.value) {
    console.error('importFormRef 未正确绑定')
    return
  }
  	importFormRef.value.open(type, id, name)
}

这是导入的弹窗

typescript 复制代码
<template>
  <Dialog v-model="dialogVisible" title="批量导入场馆预约" width="480">
    <el-form-item class="form-item">
      <el-date-picker
        v-model="bookingDates"
        type="date"
        value-format="YYYY-MM-DD"
        placeholder="请选择预约的日期"
        :disabled-date="disabledDate"
        :clearable="true"
        class="custom-date-picker"
      />
    </el-form-item>
    <el-upload
      ref="uploadRef"
      v-model:file-list="fileList"
      :action="importUrl"
      :auto-upload="false"
      :disabled="formLoading"
      :headers="uploadHeaders"
      :limit="1"
      :on-error="submitFormError"
      :on-exceed="handleExceed"
      :on-success="submitFormSuccess"
      accept=".xlsx, .xls"
      drag
      class="upload-area"
    >
      <Icon icon="ep:upload" />
      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
      <template #tip>
        <div class="el-upload__tip text-center">
          <span>仅允许导入 xls、xlsx 格式文件。</span>
          <el-link
            :underline="false"
            style="font-size: 12px; vertical-align: baseline"
            type="primary"
            @click="importTemplate"
          >
            下载模板
          </el-link>
        </div>
      </template>
    </el-upload>
    <template #footer>
      <el-button :disabled="formLoading" type="primary" @click="submitForm" class="submit-button">
        确 定
      </el-button>
      <el-button @click="dialogVisible = false" class="cancel-button">取 消</el-button>
    </template>
  </Dialog>
</template>

<script lang="ts" setup>
import { ref, nextTick, defineExpose } from 'vue'
import axios from 'axios'
import { getAccessToken, getTenantId } from '@/utils/auth'
import * as VenueBookingApi from '@/api/sports/venuebooking'
import download from '@/utils/download'
import { getUserProfile } from '@/api/system/user/profile'
defineOptions({ name: 'VenueBookingImportForm' })

const message = useMessage()

const dialogVisible = ref(false)
const formLoading = ref(false)
const uploadRef = ref()
const importUrl =
  import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/sports/venue-booking/import'
const uploadHeaders = ref()
const fileList = ref([])
const updateSupport = ref(0)
const bookingDates = ref([]) // 日期范围
const venueId = ref(null) // 用于存储场馆ID
const venueName = ref('') // 用于存储场馆名称

// 用于打开导入对话框并传入参数
const open = (type: string, id: number, name: string) => {
  dialogVisible.value = true
  venueId.value = id // 设置 venueId
  venueName.value = name // 设置 venueName
  updateSupport.value = 0
  fileList.value = []
  bookingDates.value = [] // 重置日期范围
  
  resetForm()
}

defineExpose({ open })

// 限制选择日期的函数(例如,不能选择过去的日期)
const disabledDate = (date: Date) => {
  return date.getTime() < Date.now() // 禁止选择过去的日期
}

// 提交表单
const submitForm = async () => {
  if (!validateForm()) return

  uploadHeaders.value = {
    Authorization: 'Bearer ' + getAccessToken(),
    'tenant-id': getTenantId()
  }
  formLoading.value = true

  // 创建 FormData 对象,包含文件和其他参数
  const formData = new FormData()
  formData.append('file', fileList.value[0].raw)
  formData.append('venueId', venueId.value) // 场馆 ID
  formData.append('venueName', venueName.value) // 场馆名称
  formData.append('bookingTime', bookingDates.value) // 预约日期
  formData.append('updateSupport', updateSupport.value ? 'true' : 'false')
 // 获取用户资料并添加到 formData 中

  // 发起请求
  try {
    const res = await getUserProfile();  // 假设 getUserProfile 是异步函数
    formData.append('agent', res.nickname);  // 将 nickname 作为 agent 添加到 FormData 中
    formData.append('agentId', res.id);
    const response = await axios.post(importUrl, formData, {
      headers: uploadHeaders.value
    })

    // 检查接口返回的 response 格式,如果是包含 code 的结构
    if (response.data && response.data.code === 0) {
      // 上传成功
      message.alertSuccess('批量导入成功!!')
      submitFormSuccess(response.data)
    } else {
      // 上传失败,显示错误信息
      submitFormError(response.data)
    }
  } catch (error) {
    submitFormError(error) // 请求失败
  }
}

// 失败回调
const submitFormError = (error: any): void => {
  if (error && error.msg) {
    message.error(error.msg || '上传失败,请重新上传!')
  } else {
    message.error('上传失败,请重新上传!')
  }
  formLoading.value = false
}

// 成功回调
const submitFormSuccess = (response: any) => {
  if (response.code !== 0) {
    message.error(response.msg || '上传失败')
    formLoading.value = false
    return
  }
  // 完成后恢复状态
  formLoading.value = false
  dialogVisible.value = false
}

// 重置表单
const resetForm = async (): Promise<void> => {
  formLoading.value = false
  await nextTick()
  uploadRef.value?.clearFiles()
}

// 处理文件超出限制
const handleExceed = (): void => {
  message.error('最多只能上传一个文件!')
}

// 下载模板
const importTemplate = async () => {
  const res = await VenueBookingApi.importUserTemplate()
  download.excel(res, '用户导入模版.xls')
}

// 验证表单输入
const validateForm = () => {
  if (!fileList.value.length) {
    message.error('请上传文件')
    return false
  }

  if (!bookingDates.value || bookingDates.value.length === 0) {
    message.error('请先选择预约日期')
    return false
  }

  return true
}
</script>

<style scoped>
.form-item {
  margin-left: 110px;
}
</style>
typescript 复制代码
// 下载用户导入模板
export const importUserTemplate = () => {
  return request.download({ url: '/sports/venue-booking/get-import-template' })
}

下载用户模板插件

typescript 复制代码
const download0 = (data: Blob, fileName: string, mineType: string) => {
  // 创建 blob
  const blob = new Blob([data], { type: mineType })
  // 创建 href 超链接,点击进行下载
  window.URL = window.URL || window.webkitURL
  const href = URL.createObjectURL(blob)
  const downA = document.createElement('a')
  downA.href = href
  downA.download = fileName
  downA.click()
  // 销毁超连接
  window.URL.revokeObjectURL(href)
}

const download = {
  // 下载 Excel 方法
  excel: (data: Blob, fileName: string) => {
    download0(data, fileName, 'application/vnd.ms-excel')
  },
  // 下载 Word 方法
  word: (data: Blob, fileName: string) => {
    download0(data, fileName, 'application/msword')
  },
  // 下载 Zip 方法
  zip: (data: Blob, fileName: string) => {
    download0(data, fileName, 'application/zip')
  },
  // 下载 Html 方法
  html: (data: Blob, fileName: string) => {
    download0(data, fileName, 'text/html')
  },
  // 下载 Markdown 方法
  markdown: (data: Blob, fileName: string) => {
    download0(data, fileName, 'text/markdown')
  },
  // 下载图片(允许跨域)
  image: ({
    url,
    canvasWidth,
    canvasHeight,
    drawWithImageSize = true
  }: {
    url: string
    canvasWidth?: number // 指定画布宽度
    canvasHeight?: number // 指定画布高度
    drawWithImageSize?: boolean // 将图片绘制在画布上时带上图片的宽高值, 默认是要带上的
  }) => {
    const image = new Image()
    // image.setAttribute('crossOrigin', 'anonymous')
    image.src = url
    image.onload = () => {
      const canvas = document.createElement('canvas')
      canvas.width = canvasWidth || image.width
      canvas.height = canvasHeight || image.height
      const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
      ctx?.clearRect(0, 0, canvas.width, canvas.height)
      if (drawWithImageSize) {
        ctx.drawImage(image, 0, 0, image.width, image.height)
      } else {
        ctx.drawImage(image, 0, 0)
      }
      const url = canvas.toDataURL('image/png')
      const a = document.createElement('a')
      a.href = url
      a.download = 'image.png'
      a.click()
    }
  }
}

export default download

后端

用户模板下载

javascript 复制代码
    @GetMapping("/get-import-template")
    @Operation(summary = "获得导入用户模板")
    public void importTemplate(HttpServletResponse response) throws IOException {
        // 手动创建导出 demo
        List<UserImportDemoExcelVO> list = Arrays.asList(
                UserImportDemoExcelVO.builder().registrant("张三").phone("15601691300").idCard("522324198805060010").build());

        // 输出
        ExcelUtils.write(response, "预约用户导入模板.xls", "预约用户列表", UserImportDemoExcelVO.class, list);
    }
javascript 复制代码
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = false) // 设置 chain = false,避免用户导入有问题
public class UserImportDemoExcelVO {

//    @ExcelProperty("场馆id")
//    private Long venueId;
//
//    @ExcelProperty("场馆名称")
//    private String venueName;

    @ExcelProperty("预定人姓名")
    private String registrant;

    @ExcelProperty("预定人联系方式")
    private String phone;

    @ExcelProperty("预定人身份证号")
    private String idCard;

//    @ExcelProperty("预约时间")
//    private LocalDateTime bookingTime;

}

导入模板

javascript 复制代码
    @PostMapping("/import")
    @Operation(summary = "导入场馆预约数据")
    @Parameters({
            @Parameter(name = "file", description = "Excel 文件", required = true),
            @Parameter(name = "venueId", description = "场馆 ID", required = true),
            @Parameter(name = "venueName", description = "场馆名称", required = true),
            @Parameter(name = "bookingTime", description = "预约时间", required = true),
            @Parameter(name = "agent", description = "代理预约人", required = true),
            @Parameter(name = "agentId", description = "代理id", required = true),
            @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "false")
    })
    @PreAuthorize("@ss.hasPermission('sports:venue:booking:import')")
    public CommonResult<VenueBookingRespVO> importExcel(@RequestParam("file") MultipartFile file,
                                                        @RequestParam("venueId") Long venueId,
                                                        @RequestParam("agentId") Long agentId,
                                                        @RequestParam("venueName") String venueName,
                                                        @RequestParam("bookingTime") String bookingTime,
                                                        @RequestParam("agent") String agent,
                                                        @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception {

        // 解析 Excel 文件为对象列表
        List<UserImportExcelVO> list = ExcelUtils.read(file, UserImportExcelVO.class);

        // 调用 service 层进行导入,获取导入结果
        CommonResult<VenueBookingRespVO> result = venueBookingService.importVenueBookingList(list, venueId, venueName,agent,agentId, bookingTime, updateSupport);

        // 返回服务层的响应
        return result;
    }
javascript 复制代码
@Schema(description = "管理后台 - 观赛预约记录 Response VO")
@Data
@ExcelIgnoreUnannotated
public class VenueBookingRespVO {
    @ExcelProperty("代理预定人id")
    private Long id;
    @Schema(description = "预定人")
    @ExcelProperty("预定人")
    private String registrant;
    @Schema(description = "预定人")
    @ExcelProperty("代理预定人")
    private String agent;
    @Schema(description = "代理预定人id", example = "19070")
    @ExcelProperty("代理预定人id")
    private Long agentId;

    @Schema(description = "预定人员电话号码")
    @ExcelProperty("预定人员电话号码")
    private String phone;

    @Schema(description = "预定人员身份证")
    @ExcelProperty("预定人员身份证")
    private String idCard;

    @Schema(description = "预约时间")
    @ExcelProperty("预约时间")
    private LocalDateTime bookingTime;

    @Schema(description = "场馆id", example = "19070")
    @ExcelProperty("场馆id")
    private Long venueId;

    @Schema(description = "场馆名称")
    @ExcelProperty("场馆名称")
    private String venueName;

    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
    private String message;

    // 带有多个字段的构造方法
    public VenueBookingRespVO(String message, String registrant, String phone, String idCard,
                              LocalDateTime bookingTime, Long venueId, String venueName, LocalDateTime createTime) {
        this.message = message;
        this.registrant = registrant;
        this.phone = phone;
        this.idCard = idCard;
        this.bookingTime = bookingTime;
        this.venueId = venueId;
        this.venueName = venueName;
        this.createTime = createTime;
    }

}
javascript 复制代码
/**
 * 用户 Excel 导入 VO
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = false) // 设置 chain = false,避免用户导入有问题
public class UserImportExcelVO {

    @ExcelProperty("场馆id")
    private Long venueId;

    @ExcelProperty("场馆名称")
    private String venueName;

    @ExcelProperty("预定人姓名")
    private String registrant;

    @ExcelProperty("预定人联系方式")
    private String phone;

    @ExcelProperty("预定人身份证号")
    private String idCard;

    @ExcelProperty("预约时间")
    private LocalDateTime bookingTime;

}
javascript 复制代码
    @Override
    @Transactional(rollbackFor = Exception.class)
    public CommonResult<VenueBookingRespVO> importVenueBookingList(List<UserImportExcelVO> list, Long venueId, String venueName, String agent, Long agentId, String bookingTime, Boolean updateSupport) {
        if (list == null || list.isEmpty()) {
            return CommonResult.error(400, "导入的数据不能为空!");
        }

        List<String> errorMessages = new ArrayList<>();
        LocalDateTime bookingDateTime = LocalDateTime.parse(bookingTime + "T00:00:00");

        // 身份证号和手机号验证正则表达式
        String idCardRegex = "^[1-9]\\d{5}(?:18|19|20)\\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\\d|30|31)\\d{3}[\\dXx]$";
        Pattern idCardPattern = Pattern.compile(idCardRegex);
        String phoneRegex = "^1[3-9]\\d{9}$";
        Pattern phonePattern = Pattern.compile(phoneRegex);

        List<VenueBookingDO> venueBookingList = new ArrayList<>();
        Set<String> idCardSet = new HashSet<>();  // 用于存储已存在的身份证号,查重

        for (int i = 0; i < list.size(); i++) {
            UserImportExcelVO excelVO = list.get(i);
            errorMessages.clear();  // 清空错误信息列表

            // 验证身份证号格式
            Matcher idCardMatcher = idCardPattern.matcher(excelVO.getIdCard());
            if (!idCardMatcher.matches()) {
                errorMessages.add("第" + (i + 1) + "条记录:身份证号格式不正确");
            }

            // 验证手机号格式
            Matcher phoneMatcher = phonePattern.matcher(excelVO.getPhone());
            if (!phoneMatcher.matches()) {
                errorMessages.add("第" + (i + 1) + "条记录:手机号格式不正确");
            }

            // 检查身份证号是否重复
            if (idCardSet.contains(excelVO.getIdCard())) {
                errorMessages.add("第" + (i + 1) + "条记录:身份数据重复");
            } else {
                idCardSet.add(excelVO.getIdCard());  // 加入已存在身份证号集合
            }

            // 如果有错误,返回错误信息并终止插入
            if (!errorMessages.isEmpty()) {
                return CommonResult.error(400, String.join(",", errorMessages));  // 使用 error 方法返回错误信息
            }

            // 对身份证号进行加密处理
            String encryptedIdCard = Sm2Util.signMd5(excelVO.getIdCard());
            excelVO.setVenueId(venueId);
            excelVO.setVenueName(venueName);
            excelVO.setBookingTime(bookingDateTime);
            excelVO.setIdCard(encryptedIdCard);

            VenueBookingDO venueBookingDO = new VenueBookingDO();
            venueBookingDO.setVenueId(excelVO.getVenueId());
            venueBookingDO.setVenueName(excelVO.getVenueName());
            venueBookingDO.setBookingTime(excelVO.getBookingTime());
            venueBookingDO.setIdCard(excelVO.getIdCard());  // 设置加密后的身份证号
            venueBookingDO.setPhone(excelVO.getPhone());
            venueBookingDO.setRegistrant(excelVO.getRegistrant());
            venueBookingDO.setAgent(agent);
            venueBookingDO.setAgentId(agentId);
            venueBookingDO.setBookingStatus(0);

            venueBookingList.add(venueBookingDO);
        }

        // 批量插入数据
        if (!venueBookingList.isEmpty()) {
            venueBookingMapper.insertBatch(venueBookingList);
        }

        // 返回成功的响应,填充所有字段
        return CommonResult.success(new VenueBookingRespVO("导入成功", "registrantData", "12345678901", "123456789012345678", bookingDateTime, venueId, venueName, LocalDateTime.now()));
    }
相关推荐
wdfk_prog2 分钟前
lely-canopen-rtt:为什么推荐在 RT-Thread 上使用 Lely CANopen 构建主站
java·运维·缓存·docker·容器
Token掘金室7 分钟前
JSON模式结构化输出报错
java·服务器·json
秋饼13 分钟前
Spring AI 2.0 接入 DeepSeek V4.1 Flash 生产级实战
java·ai·技术分享·后端开发
IT枫斗者枫哥14 分钟前
Java 分批导出仍然 OOM?用 32 MiB 堆复现三种 CSV 写法
java
库玛西16 分钟前
深度解构高并发利器:纯事件驱动 Reactor 模式的设计哲学与工程实践
服务器·开发语言·c++·笔记·tcp/ip
shehuiyuelaiyuehao22 分钟前
算法43,外观数列,模拟算法+双指针
java·算法
泰晶科技29 分钟前
【晶振相位噪声对通信系统的影响:如何降低5G光模块信号误码率】
笔记·5g
妖孽白小白36 分钟前
el-select的内容续写实现
前端·vue.js·elementui
不可能片场1 小时前
Electron 主进程热补丁:单行 JS 安全打法
前端·electron