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); }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")) {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();}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) { throw 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 {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<>();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();}}byte[] 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);}}}}}}byte[] 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);}}}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;}
}