当前位置: 首页 > news >正文

08_Excel 导入 - 用户信息批量导入

08_Excel 导入 - 用户信息批量导入

1. VO 类

java复制编辑@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfoBatch4ExcelReq {@ExcelProperty(value = "用户姓名")@Schema(description = "用户姓名")private String userName;@ExcelProperty(value = "用户性别(男/女)")@Schema(description = "用户性别")private String gender;@ExcelProperty(value = "出生日期")@Schema(description = "出生日期")private String birthDate;@ExcelProperty(value = "电子邮箱")@Schema(description = "电子邮箱")private String email;@ExcelProperty(value = "联系电话")@Schema(description = "联系电话")private String phoneNumber;@ExcelProperty(value = "地址")@Schema(description = "地址")private String address;@ExcelProperty(value = "职位")@Schema(description = "职位")private String jobTitle;@ExcelProperty(value = "部门")@Schema(description = "部门")private String department;@ExcelProperty(value = "入职日期")@Schema(description = "入职日期")private String joinDate;@ExcelProperty(value = "是否为管理员(是/否)")@Schema(description = "是否为管理员")private String isAdmin;@ExcelIgnore@Schema(description = "用户账号")private String account;@ExcelIgnore@Schema(description = "用户密码")private String password;
}

说明:

  • @ExcelProperty:Excel 表格中列的标题。
  • @Schema:为 Swagger 或其他文档生成工具提供字段描述。

2. DO 类

java复制编辑@EqualsAndHashCode(callSuper = true)
@Data
@TableName(value = "user_info", autoResultMap = true)
@NoArgsConstructor
@AllArgsConstructor
public class UserInfoVo extends BaseVo {@ExcelProperty(value = "序号", index = 0)@TableId(value = "id", type = IdType.AUTO)@Schema(description = "主键id")private Long id;@ExcelProperty(value = "用户姓名", index = 1)@Schema(description = "用户姓名")private String userName;@ExcelProperty(value = "用户性别", index = 2)@Schema(description = "用户性别")private String gender;@ExcelProperty(value = "出生日期", index = 3)@Schema(description = "出生日期")private String birthDate;@ExcelProperty(value = "电子邮箱", index = 4)@Schema(description = "电子邮箱")private String email;@ExcelProperty(value = "联系电话", index = 5)@Schema(description = "联系电话")private String phoneNumber;@ExcelProperty(value = "地址", index = 6)@Schema(description = "地址")private String address;@ExcelProperty(value = "职位", index = 7)@Schema(description = "职位")private String jobTitle;@ExcelProperty(value = "部门", index = 8)@Schema(description = "部门")private String department;@ExcelProperty(value = "入职日期", index = 9)@Schema(description = "入职日期")private String joinDate;@ExcelProperty(value = "是否为管理员", index = 10)@Schema(description = "是否为管理员")private String isAdmin;@ExcelIgnore@Schema(description = "用户账号")private String account;@ExcelIgnore@Schema(description = "用户密码")private String password;@ExcelIgnore@Schema(description = "是否删除")private Boolean isDeleted;
}

说明:

  • 该类用于将导入的 Excel 数据转换为持久化存储对象(UserInfoVo)进行数据库操作。

3. Controller 层

java复制编辑@Tag(name = "用户管理")
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {private final UserInfoService userInfoService;@Operation(summary = "批量用户信息导入")@RequestMapping(value = "/import", method = RequestMethod.POST)public CommonResult<Long> userInfoBatchInsert4Excel(@RequestParam("file") MultipartFile file) {return CommonResult.success(userInfoService.userInfoBatchInsert4Excel(file));}
}

说明:

  • @RequestMapping 用于映射 HTTP 请求。
  • @Operation 描述接口功能。
  • MultipartFile 用于接收上传的 Excel 文件。

4. Service 接口

java复制编辑public interface UserInfoService {Long userInfoBatchInsert4Excel(MultipartFile file);
}

说明:

  • 定义了一个方法用于批量导入用户信息。

5. Service 实现类

java复制编辑@Service
@RequiredArgsConstructor
public class UserInfoServiceImpl implements UserInfoService {private final UserMapper userMapper;private final FileService fileService;private final ExcelImageUtil excelImageUtil;private final ThreadPoolTaskExecutor threadPoolTaskExecutor;@Overridepublic Long userInfoBatchInsert4Excel(MultipartFile file) {try {if (file.getSize() > 52428800) {throw new BusinessException(ResultCode.FAILED, "导入失败,用户批量导入Excel文件大小最多为50MB");}byte[] excelBytes = file.getBytes();Map<String, byte[]> imageData = excelImageUtil.extractImages(excelBytes);UserInfoExcelListener listener = new UserInfoExcelListener(threadPoolTaskExecutor,userMapper,imageData,fileService);FastExcel.read(new ByteArrayInputStream(excelBytes), UserInfoBatch4ExcelReq.class, listener).sheet().doRead();return listener.getSuccessCount();} catch (Exception e) {throw new BusinessException(ResultCode.FAILED, "导入失败,请检查 Excel 文件是否正确!");}}
}

说明:

  • 通过 FastExcel 读取并解析 Excel 文件。
  • 进行图片提取并处理。
  • 使用自定义的 UserInfoExcelListener 进行批量导入。

6. Listener 监听类

@Slf4j
@RequiredArgsConstructor
@Getter
public class UserInfoExcelListener extends AnalysisEventListener<UserInfoBatch4ExcelReq> {private final ThreadPoolTaskExecutor threadPoolTaskExecutor;private final UserMapper userMapper;private final FileService fileService;private final Map<String, byte[]> imageData;private Integer rowIndex = 1;private Long successCount = 0L;@Overridepublic void invoke(UserInfoBatch4ExcelReq data, AnalysisContext context) {rowIndex = context.readRowHolder().getRowIndex();// 校验数据并进行转换validateAndConvert(data);try {// 如果包含图片字段,解析图片if (data.getProfileImage() != null && !data.getProfileImage().isEmpty()) {String imageUrl = resolveImageUrlFromDispImg(data.getProfileImage());data.setProfileImage(imageUrl); // 将解析后的图片URL赋值回去}// 没问题,插入数据UserInfoVo userInfoVo = new UserInfoVo();BeanUtils.copyProperties(data, userInfoVo);// 插入用户信息userMapper.insert(userInfoVo);successCount++;} catch (Exception e) {String msg = e.getMessage();if (msg != null && msg.contains("Duplicate entry")) {// 解析出 "xx用户id-用户名-1(是否参与联合唯一索引校验)"Pattern pattern = Pattern.compile("Duplicate entry '(.+?)' for key");Matcher matcher = pattern.matcher(msg);if (matcher.find()) {String dupKey = matcher.group(1);String[] parts = dupKey.split("-", 3);if (parts.length == 3) {String userIdStr = parts[0];String userName = parts[1];// 构造具体的错误信息String errorMessage = String.format("当前用户存在相同用户信息,重复用户名:《%s》,请移除后再重新提交!",userName);throw new BusinessException(ResultCode.FAILED, buildImportErrorMessage(rowIndex, errorMessage, null));}}}throw new BusinessException(ResultCode.FAILED, "导入失败,请检查格式是否正确,如尝试多次仍然报此错误,请联系管理员!");}}private void validateAndConvert(UserInfoBatch4ExcelReq data) {// ========== 基础校验 ============================if (StringUtils.isNullOrEmpty(data.getUserName())) {throw new BusinessException(ResultCode.FAILED, buildImportErrorMessage(rowIndex, "用户姓名不能为空", null));}if (StringUtils.isNullOrEmpty(data.getGender())) {throw new BusinessException(ResultCode.FAILED, buildImportErrorMessage(rowIndex, "用户性别不能为空", null));}if (StringUtils.isNullOrEmpty(data.getBirthDate())) {throw new BusinessException(ResultCode.FAILED, buildImportErrorMessage(rowIndex, "出生日期不能为空", null));}if (StringUtils.isNullOrEmpty(data.getEmail())) {throw new BusinessException(ResultCode.FAILED, buildImportErrorMessage(rowIndex, "电子邮箱不能为空", null));}if (StringUtils.isNullOrEmpty(data.getPhoneNumber())) {throw new BusinessException(ResultCode.FAILED, buildImportErrorMessage(rowIndex, "联系电话不能为空", null));}if (StringUtils.isNullOrEmpty(data.getAddress())) {throw new BusinessException(ResultCode.FAILED, buildImportErrorMessage(rowIndex, "地址不能为空", null));}if (StringUtils.isNullOrEmpty(data.getJobTitle())) {throw new BusinessException(ResultCode.FAILED, buildImportErrorMessage(rowIndex, "职位不能为空", null));}if (StringUtils.isNullOrEmpty(data.getDepartment())) {throw new BusinessException(ResultCode.FAILED, buildImportErrorMessage(rowIndex, "部门不能为空", null));}if (StringUtils.isNullOrEmpty(data.getJoinDate())) {throw new BusinessException(ResultCode.FAILED, buildImportErrorMessage(rowIndex, "入职日期不能为空", null));}if (StringUtils.isNullOrEmpty(data.getIsAdmin())) {throw new BusinessException(ResultCode.FAILED, buildImportErrorMessage(rowIndex, "是否为管理员不能为空", null));}}private String buildImportErrorMessage(int rowIndex, String reason, String correctFormat) {StringBuilder sb = new StringBuilder();sb.append("导入失败,已成功导入 ").append(successCount).append(" 条记录。");sb.append("异常位置:第").append(rowIndex).append("行数据处理异常。");sb.append("异常原因:").append(reason);if (!StringUtils.isNullOrEmpty(correctFormat)) {sb.append("。正确格式:").append(correctFormat).append("。");}return sb.toString();}/*** 如果字段是类似 =DISPIMG("ID_293ECFFE4A5D4C11996580C8502E1816",1)* 则尝试提取ID,从imageData里取图片上传返回url* 否则直接返回原字符串*/private String resolveImageUrlFromDispImg(String fieldValue) {if (fieldValue == null || !fieldValue.contains("DISPIMG")) {throw new BusinessException(ResultCode.FAILED, buildImportErrorMessage(rowIndex, "图片解析失败,请检查 Excel 中插入的图片", null));}String imgId = extractDispImgId(fieldValue);if (imgId == null || !imageData.containsKey(imgId)) {throw new BusinessException(ResultCode.FAILED, buildImportErrorMessage(rowIndex, "图片解析失败,请检查 Excel 中插入的图片", null));}return uploadImage(imageData.get(imgId));}private String extractDispImgId(String formula) {if (formula != null) {formula = formula.trim();if (formula.startsWith("=")) {formula = formula.substring(1); // 去掉前面等号}if (formula.startsWith("DISPIMG")) {int start = formula.indexOf("\"");int end = formula.lastIndexOf("\"");if (start >= 0 && end > start) {return formula.substring(start + 1, end);}}}return null;}private String uploadImage(byte[] imgBytes) {if (imgBytes.length > 2097152) { // 最大 2MBthrow new BusinessException(ResultCode.FAILED, buildImportErrorMessage(rowIndex, "图片大小不能超过2MB", null));}String extension = FileTypeUtil.getImageExtension(imgBytes);String fileName = UUID.randomUUID() + extension;MultipartFile multipartFile = new MockMultipartFile(fileName, fileName, "image/" + extension.replace(".", ""), imgBytes);return fileService.uploadFile(multipartFile, null);}
}

说明:

  • 监听器负责逐行处理 Excel 数据,进行字段校验、数据转换并保存。
  • 支持图片字段的解析和上传。

7. WPS excel 图片提取工具类

Component
public class ExcelImageUtil {/*** 提取 WPS 表格中的内嵌图片(=DISPIMG(...))。** @param excelData Excel 文件的字节数组* @return 图片映射,键为图片 ID,值为图片字节数组* @throws IOException 如果读取失败*/public Map<String, byte[]> extractImages(byte[] excelData) throws IOException {Map<String, String> idToRidMap = new HashMap<>();Map<String, String> ridToTargetMap = new HashMap<>();Map<String, byte[]> imageMap = new HashMap<>();// 1. 解析 cellimages.xml 和其 rels 文件Map<String, byte[]> zipEntryMap = new HashMap<>();try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(excelData))) {ZipEntry zipEntry;while ((zipEntry = zipInputStream.getNextEntry()) != null) {String entryName = zipEntry.getName();if (entryName.equals("xl/cellimages.xml") || entryName.equals("xl/_rels/cellimages.xml.rels") || entryName.startsWith("xl/media/")) {byte[] entryData = IOUtils.toByteArray(zipInputStream);zipEntryMap.put(entryName, entryData);}zipInputStream.closeEntry();}}// 2. 解析 cellimages.xmlbyte[] cellImagesData = zipEntryMap.get("xl/cellimages.xml");if (cellImagesData != null) {JSONObject json = XML.toJSONObject(new String(cellImagesData, "UTF-8"));JSONObject cellImages = json.getJSONObject("etc:cellImages");if (cellImages != null) {Object cellImageObj = cellImages.get("etc:cellImage");JSONArray cellImageArray = new JSONArray();if (cellImageObj instanceof JSONArray) {cellImageArray = (JSONArray) cellImageObj;} else if (cellImageObj instanceof JSONObject) {cellImageArray.add(cellImageObj);}for (int i = 0; i < cellImageArray.size(); i++) {JSONObject cellImage = cellImageArray.getJSONObject(i);JSONObject pic = cellImage.getJSONObject("xdr:pic");if (pic != null) {JSONObject nvPicPr = pic.getJSONObject("xdr:nvPicPr");if (nvPicPr != null) {JSONObject cNvPr = nvPicPr.getJSONObject("xdr:cNvPr");String name = cNvPr.getStr("name");JSONObject blipFill = pic.getJSONObject("xdr:blipFill");if (blipFill != null) {JSONObject blip = blipFill.getJSONObject("a:blip");String embed = blip.getStr("r:embed");idToRidMap.put(name, embed);}}}}}}// 3. 解析 cellimages.xml.relsbyte[] relsData = zipEntryMap.get("xl/_rels/cellimages.xml.rels");if (relsData != null) {JSONObject json = XML.toJSONObject(new String(relsData, "UTF-8"));JSONObject relationships = json.getJSONObject("Relationships");if (relationships != null) {Object relationshipObj = relationships.get("Relationship");JSONArray relationshipArray = new JSONArray();if (relationshipObj instanceof JSONArray) {relationshipArray = (JSONArray) relationshipObj;} else if (relationshipObj instanceof JSONObject) {relationshipArray.add(relationshipObj);}for (int i = 0; i < relationshipArray.size(); i++) {JSONObject rel = relationshipArray.getJSONObject(i);String id = rel.getStr("Id");String target = rel.getStr("Target");ridToTargetMap.put(id, target);}}}// 4. 根据 rId -> target 映射找到 media 文件for (Map.Entry<String, String> entry : idToRidMap.entrySet()) {String dispImgId = entry.getKey();String rid = entry.getValue();String targetPath = "xl/" + ridToTargetMap.getOrDefault(rid, "");byte[] imageData = zipEntryMap.get(targetPath);if (imageData != null) {imageMap.put(dispImgId, imageData);}}return imageMap;}
}
http://www.dtcms.com/a/265299.html

相关文章:

  • [ linux-系统 ] 软硬链接与动静态库
  • 基于Java+SpringBoot的图书管理系统
  • 【字节跳动】数据挖掘面试题0002:从转发数据中求原视频用户以及转发的最长深度和二叉排序树指定值
  • Scala 安装使用教程
  • windows系统下将Docker Desktop安装到除了C盘的其它盘中
  • 前端可视化——Canvas实战篇
  • Docker Compose 基础——AI教你学Docker
  • 《寻北技术的全面剖析与应用前景研究报告》
  • 【4】 Deployment深入简出实战演练
  • 无代码自动化测试工具介绍
  • Java中创建线程方法以及线程池参数配置
  • (LeetCode ) 13. 罗马数字转整数 (哈希表)
  • 跨越十年的C++演进:C++20新特性全解析
  • 软件反调试(3)- 基于父进程的检测
  • Python 高光谱分析工具(PyHAT)
  • 【数字后端】- 什么是AOI、OAI cell?它们后面数字的含义
  • seaborn又一个扩展heatmapz
  • 利用tcp转发搭建私有云笔记
  • Java--多态--向上转型--动态绑定机制--断点调试--向下转型
  • IO进程线程 (进程)
  • 旋转不变子空间( ESPRIT) 算法
  • 算法笔记上机训练实战指南刷题
  • pytorch学习-9.多分类问题
  • WSL2 + Docker Desktop 环境中查看本地镜像
  • 基于SpringBoot的场地预定管理系统
  • Springboot开发常见注解一览
  • 记一次finallshell.exe打开无法应的处理
  • 【卡尔曼滤波第二期】一维无过程噪声的卡尔曼滤波
  • 红黑树:高效平衡的秘密
  • 声网支持弱网对抗保障直播不卡不花屏