批量导入预约数据-笔记
前端
场馆列表
该列表进入出现的是这样的,这儿是列表操作
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()));
}