批量导入预约数据-笔记
- 前端
- 场馆列表
- 后端
前端
场馆列表
该列表进入出现的是这样的,这儿是列表操作
<el-table-column label="操作" align="center" width="220px"><template #default="scope"><el-buttonlinktype="primary"@click="openForm('update', scope.row.id, scope.row.name)"v-hasPermi="['sports:venue:update']">编辑</el-button><el-buttonlinktype="primary"@click="openImportForm('update', scope.row.id, scope.row.name, scope.row.capacity)"v-hasPermi="['sports:venue:update']">批量导入预约</el-button><el-buttonlinktype="danger"@click="handleDelete(scope.row.id)"v-hasPermi="['sports:venue:delete']">删除</el-button></template></el-table-column>
批量导入操作在这儿
<el-button
link
type="primary"
@
click="openImportForm('update', scope.row.id, scope.row.name, scope.row.capacity)"
v-hasPermi="['sports:venue:update']"
>
批量导入预约
<
/el-button>
<
!-- 批量导入 -->
<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)
}
这是导入的弹窗
<template><Dialog v-model="dialogVisible" title="批量导入场馆预约" width="480"><el-form-item class="form-item"><el-date-pickerv-model="bookingDates"type="date"value-format="YYYY-MM-DD"placeholder="请选择预约的日期":disabled-date="disabledDate":clearable="true"class="custom-date-picker"/></el-form-item><el-uploadref="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"dragclass="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) // 用于存储场馆IDconst venueName = ref('') // 用于存储场馆名称// 用于打开导入对话框并传入参数const open = (type: string, id: number, name: string) =>{dialogVisible.value = truevenueId.value = id // 设置 venueIdvenueName.value = name // 设置 venueNameupdateSupport.value = 0fileList.value = []bookingDates.value = [] // 重置日期范围resetForm()}defineExpose({open})// 限制选择日期的函数(例如,不能选择过去的日期)const disabledDate = (date: Date) =>{return date.getTime() < Date.now() // 禁止选择过去的日期}// 提交表单const submitForm =async () =>{if (!validateForm())returnuploadHeaders.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) // 场馆 IDformData.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 = falsereturn}// 完成后恢复状态formLoading.value = falsedialogVisible.value = false}// 重置表单const resetForm =async (): Promise<void>=>{formLoading.value = falseawait 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>
// 下载用户导入模板
export
const importUserTemplate = (
) =>
{
return request.download({
url: '/sports/venue-booking/get-import-template'
}
)
}
下载用户模板插件
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
后端
用户模板下载
@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);}
@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;
}
导入模板
@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;}
@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;
}
}
/**
* 用户 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;
}
@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()));}