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.pswp.cn/web/87953.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/87953.shtml
英文地址,请注明出处:http://en.pswp.cn/web/87953.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【深度学习新浪潮】什么是世界模型?

世界模型(World Model)是人工智能领域中一类通过构建环境的抽象表示来理解和预测外部世界的系统。它通过整合多模态数据(如视觉、语言、传感器信号)形成对环境的动态认知,并支持智能体在复杂场景中进行决策与规划。以下从核心概念、解决的问题、关键研究、技术路线、现状与…

React + Express 传输加密以及不可逆加密

一、传输加密这里用 对称加密模式 ASE实现。React 前端const CryptoJS require("crypto-js");// 示例1&#xff1a;ECB模式&#xff08;无需IV&#xff09; const encryptECB (plainText, key) > {return CryptoJS.AES.encrypt(plainText, key, {mode: CryptoJS…

浏览器(Chrome /Edge)高效使用 - 内部命令/快捷键/启动参数

今天在CSDN上传文件,提交总是提示续传失败,重试了五六次才想到获取是科学上网的问题,这个时候其实只要重启浏览器即可,但如果手动关闭浏览器再次打开,浏览器不会恢复之前的多开窗口(会恢复最后一个窗口内多开的标签页,但不会恢复其他窗口)。想了想记得 Chrome 流行的时…

【PTA数据结构 | C语言版】连续子序列最大和

本专栏持续输出数据结构题目集&#xff0c;欢迎订阅。 文章目录 题目代码 题目 给定 n 个整数组成的序列 { a1 ,a2 ,⋯,an }&#xff0c;“连续子序列”被定义为 { ai ,ai1 ,⋯,aj }&#xff0c;其中 1≤i≤j≤n。“连续子序列最大和”则被定义为所有连续子序列元素的和中最大…

Vrrp配置和原理

Vrrp配置和原理 文章目录Vrrp配置和原理概述物理与逻辑拓扑重点vrid虚拟路由器虚拟IP地址及虚拟MAC地址超时时间计算-MASTER_DOWNvip 管理员手动指定方法Master路由器Backup路由器PriorityVRRP报文格式VRRP状态机从Backup到masterVRRP协议状态二.优先级一样比较接口IPVRRP优先级…

可编辑59页PPT | 某大型集团人工智能数字化转型SAP解决方案

荐言摘要&#xff1a;某大型集团人工智能数字化转型中&#xff0c;SAP解决方案扮演着智能中枢角色&#xff0c;深度融合AI技术与核心业务场景&#xff0c;破解传统系统“数据孤岛流程僵化”双重困局。针对集团跨产业、多业态特点&#xff0c;方案以SAP S/4HANA为数据底座&#…

【RK3568 驱动开发:实现一个最基础的网络设备】

RK3568 驱动开发&#xff1a;实现一个最基础的网络设备一、引言二、编写网络设备驱动代码1. 核心数据结构与接口2. 核心功能实现3. 网络命名空间管理4.源代码三、编译与验证1.加载模块2.验证网络四、注意事项一、引言 RK3568 作为一款高性能 ARM 架构处理器&#xff0c;广泛应…

CAIDCP系列对话:AI 驱动安全

数字时代&#xff0c;AI浪潮翻涌&#xff0c;网络安全攻防战已悄然升级&#xff1a; 某工业控制系统遭AI驱动勒索攻击&#xff1a;攻击者借 AI 精准捕捉异常网络扫描、远程 PowerShell 痕迹&#xff0c;瞬间加密文件索要赎金&#xff1b; 另一边&#xff0c;某大型科技公司用AI…

ARMv8 没开mmu执行memset引起的非对齐访问异常

最近在haps上验证一个新的芯片&#xff0c;记录一下memset访问出错的问题。在没开mmu和cache的情况下&#xff0c;对全局变量指针进行memset清零操作&#xff0c;发现每次都会出现异常。最后发现是没开mmu导致出现了数据非对齐访问导致报错。排查EC区域发现是0x25&#xff0c;产…

基于LiveKit Go 实现腾讯云实时音视频功能

详细的生产部署建议&#xff0c;适用于 LiveKit Go 服务器 Web 客户端 TURN/HTTPS。 1. 服务器准备 推荐使用云服务器&#xff08;如阿里云、腾讯云、AWS、Azure等&#xff09;&#xff0c;公网IP&#xff0c;带宽建议≥10Mbps。系统推荐 Ubuntu 20.04/22.04 或 CentOS 7/8&…

三位一体:Ovis-U1如何以30亿参数重构多模态AI格局?

1. 时代命题&#xff1a;多模态统一模型的破局之战当GPT-4o以万亿级参数构建多模态帝国时&#xff0c;中国AI军团正在书写另一种答案。Ovis-U1用30亿参数证明&#xff1a;参数量并非决定性因素&#xff0c;架构创新与训练策略的化学反应&#xff0c;同样能催生出改变游戏规则的…

图像处理基础:镜像、缩放与矫正

在图像处理中&#xff0c;镜像、缩放和矫正操作是常见的图像变换手段。这些操作可以帮助我们对图像进行调整&#xff0c;以满足不同的需求。本文将详细介绍这三种操作的原理和实现方法&#xff0c;并通过代码示例展示它们的实际应用。一、图片镜像旋转1.1 什么是镜像旋转&#…

「Java案例」猜数游戏

案例实现 猜数字游戏 设计一个三位数的猜数游戏,三位数随机生成。程序提示用户输入一个三位的数字,依照以下的规则决定赢取多少奖金:1) 如果用户输入的数字和随机数字完全一致,输出:“恭喜恭喜!完全猜对了!获得三个赞!”2) 如果用户输入的数字覆盖了随机生成的所有数…

创客匠人解析创始人 IP 内卷:知识变现时代的生存逻辑与破局路径

当知识付费行业进入 “存量竞争” 阶段&#xff0c;创始人 IP 的 “内卷” 已非选择而是必然。创客匠人在服务数万知识创业者的实践中发现&#xff0c;那些实现逆势增长的案例&#xff0c;其核心差异往往在于创始人是否具备 “从幕后走到台前” 的决心与能力 —— 这种内卷并非…

250705-Debian12-sudo apt update加速+配置RDP远程桌面环境+设置FRP服务为开机启动项

A. 实现sudo apt update加速 在 Debian 12 上运行 sudo apt update 很慢的常见原因包括&#xff1a; &#x1f50d; 一、常见原因分析 使用了国外的软件源 默认 Debian 安装源多数是国际服务器&#xff0c;国内访问会非常慢。 DNS 解析慢或失败 软件源地址解析时间长&#xf…

数学视频动画引擎Python库 -- Manim Voiceover 语音服务 Speech Services

文中内容仅限技术学习与代码实践参考&#xff0c;市场存在不确定性&#xff0c;技术分析需谨慎验证&#xff0c;不构成任何投资建议。 Manim Voiceover 是一个为 Manim 打造的专注于语音旁白的插件&#xff1a; 直接在 Python 中添加语音旁白&#xff1a; 无需使用视频编辑器&…

C++11 forward_list 从基础到精通:原理、实践与性能优化

文章目录一、为什么需要 forward_list&#xff1f;二、基础篇&#xff1a;forward_list 的核心特性与接口2.1 数据结构与迭代器2.2 常用接口速览2.3 基础操作示例&#xff1a;从初始化到遍历2.3.1 初始化与遍历2.3.2 插入与删除&#xff1a;before_begin 的关键作用三、进阶篇&…

物联网技术的核心组件与发展趋势(截至2025年)

一、物联网技术的核心组件物联网&#xff08;IoT&#xff09;技术体系由感知层、网络层、平台层、应用层和安全层构成&#xff0c;各层技术协同工作&#xff0c;实现物理世界与数字世界的深度融合。1. 感知层&#xff1a;数据采集与交互传感器技术&#xff1a;类型&#xff1a;…

面试中常见的问题:JavaScript 宏任务与微任务,包教包会

事件循环Event Loop 我们都知道&#xff0c;JavaScript 是一种单线程的编程语言&#xff0c;简单的说就是&#xff1a;js只有一条通道&#xff0c;那么在任务多的情况下&#xff0c;就会出现拥挤的情况&#xff0c;这种情况下就产生了 ‘多线程’ &#xff0c;但是这种“多线程…

【LeetCode102.二叉树的层序遍历】vs.【LeetCode103.二叉树的锯齿形层序遍历】

题目链接 LeetCode102.二叉树的层序遍历&#xff1a;102. 二叉树的层序遍历 - 力扣&#xff08;LeetCode&#xff09;LeetCode103.二叉树的锯齿形层序遍历&#xff1a;103. 二叉树的锯齿形层序遍历 - 力扣&#xff08;LeetCode&#xff09; 实现思路 定义一个队列&#xff0…