mirror of
https://github.com/BgaSol/sol-cloud
synced 2026-05-23 17:18:44 +00:00
refactor(common-base-web): Extract import and export functions into separate classes
This commit is contained in:
parent
ea20afefc2
commit
7408258faa
6 changed files with 264 additions and 215 deletions
|
|
@ -6,16 +6,11 @@ import com.bgasol.common.core.base.dto.BaseUpdateDto;
|
|||
import com.bgasol.common.core.base.entity.BaseEntity;
|
||||
import com.bgasol.common.core.base.service.BaseService;
|
||||
import com.bgasol.common.core.base.vo.BaseVo;
|
||||
import com.bgasol.common.core.base.vo.ImportResult;
|
||||
import com.bgasol.common.core.base.vo.PageVo;
|
||||
import com.bgasol.model.system.role.entity.RoleEntity;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Validated
|
||||
|
|
@ -57,13 +52,4 @@ public abstract class BaseController<
|
|||
List<ENTITY> all = commonBaseService().findAll();
|
||||
return BaseVo.success(all);
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> downloadImportTemplate() {
|
||||
return commonBaseService().generateImportTemplateResponse();
|
||||
}
|
||||
|
||||
public BaseVo<ImportResult> importFromExcel( MultipartFile file) throws IOException {
|
||||
ImportResult importResult = commonBaseService().importFromExcel(file);
|
||||
return BaseVo.success(importResult, "导入成功"); }
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package com.bgasol.common.core.base.controller;
|
||||
|
||||
|
||||
import com.bgasol.common.core.base.dto.BaseCreateDto;
|
||||
import com.bgasol.common.core.base.dto.BasePageDto;
|
||||
import com.bgasol.common.core.base.dto.BaseUpdateDto;
|
||||
import com.bgasol.common.core.base.entity.BaseEntity;
|
||||
import com.bgasol.common.core.base.service.BasePoiService;
|
||||
import com.bgasol.common.core.base.vo.BaseVo;
|
||||
import com.bgasol.common.core.base.vo.ImportResult;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public abstract class BasePoiController<
|
||||
ENTITY extends BaseEntity,
|
||||
PAGE_DTO extends BasePageDto<ENTITY>,
|
||||
CREATE_DTO extends BaseCreateDto<ENTITY>,
|
||||
UPDATE_DTO extends BaseUpdateDto<ENTITY>
|
||||
> extends BaseController<ENTITY, PAGE_DTO, CREATE_DTO, UPDATE_DTO> {
|
||||
abstract public BasePoiService<ENTITY, PAGE_DTO, CREATE_DTO, UPDATE_DTO> commonBaseService();
|
||||
|
||||
public ResponseEntity<byte[]> downloadImportTemplate() {
|
||||
return commonBaseService().generateImportTemplateResponse();
|
||||
}
|
||||
|
||||
public BaseVo<ImportResult> importFromExcel(MultipartFile file) throws IOException {
|
||||
ImportResult importResult = commonBaseService().importFromExcel(file);
|
||||
return BaseVo.success(importResult, "导入成功");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
package com.bgasol.common.core.base.service;
|
||||
|
||||
import cn.idev.excel.EasyExcel;
|
||||
import com.bgasol.common.core.base.dto.BaseCreateDto;
|
||||
import com.bgasol.common.core.base.dto.BasePageDto;
|
||||
import com.bgasol.common.core.base.dto.BaseUpdateDto;
|
||||
import com.bgasol.common.core.base.entity.BaseEntity;
|
||||
import com.bgasol.common.core.base.exception.BaseException;
|
||||
import com.bgasol.common.core.base.listener.BaseExcelImportListener;
|
||||
import com.bgasol.common.core.base.vo.ImportResult;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Transactional
|
||||
@Slf4j
|
||||
@Service
|
||||
public abstract class BasePoiService<
|
||||
ENTITY extends BaseEntity,
|
||||
PAGE_DTO extends BasePageDto<ENTITY>,
|
||||
CREATE_DTO extends BaseCreateDto<ENTITY>,
|
||||
UPDATE_DTO extends BaseUpdateDto<ENTITY>> extends BaseService<ENTITY, PAGE_DTO> {
|
||||
// DTO类缓存,避免重复反射和类加载
|
||||
private volatile Class<? extends BaseCreateDto<ENTITY>> cachedCreateDtoClass;
|
||||
|
||||
// 常量定义
|
||||
private static final String ENTITY_PACKAGE_SUFFIX = ".entity.";
|
||||
private static final String DTO_PACKAGE_SUFFIX = ".dto.";
|
||||
private static final String ENTITY_CLASS_SUFFIX = "Entity";
|
||||
private static final String CREATE_DTO_CLASS_SUFFIX = "CreateDto";
|
||||
|
||||
/**
|
||||
* 创建模板对应的 DTO 类型(自动推断)。
|
||||
* 规则:将 ENTITY 的包路径由 ".entity." 替换为 ".dto.",类名后缀由 "Entity" 改为 "CreateDto"。
|
||||
* 例如:com.bgasol.model.system.role.entity.RoleEntity -> com.bgasol.model.system.role.dto.RoleCreateDto。
|
||||
* 若推断失败或类不存在,则抛出异常。
|
||||
*
|
||||
* @return 对应的CreateDto类
|
||||
* @throws BaseException 当无法获取实体类类型或推断DTO类失败时
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Class<? extends BaseCreateDto<ENTITY>> commonCreateDtoClass() {
|
||||
if (cachedCreateDtoClass == null) {
|
||||
synchronized (this) {
|
||||
if (cachedCreateDtoClass == null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<? extends BaseCreateDto<ENTITY>> resolved = (Class<? extends BaseCreateDto<ENTITY>>) (Class<?>) resolveDtoClass(BaseCreateDto.class, CREATE_DTO_CLASS_SUFFIX);
|
||||
cachedCreateDtoClass = resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cachedCreateDtoClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用DTO解析:基于实体类全名,将 .entity. 替换为 .dto.,并用指定后缀替换/追加类名。
|
||||
* 例如:RoleEntity -> RoleCreateDto / RoleUpdateDto 等。
|
||||
*
|
||||
* @param dtoSuperType 目标DTO的父类型(用于类型校验)
|
||||
* @param targetSuffix 目标DTO类名后缀,例如 "CreateDto"
|
||||
* @return 解析并加载后的 DTO Class
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <T> Class<? extends T> resolveDtoClass(Class<T> dtoSuperType, String targetSuffix) {
|
||||
Class<ENTITY> entityClass = commonBaseEntityClass();
|
||||
if (entityClass == null) {
|
||||
throw new BaseException("无法获取实体类类型,请检查泛型参数配置");
|
||||
}
|
||||
|
||||
String entityClassName = entityClass.getName();
|
||||
String dtoClassName = inferDtoClassName(entityClassName, targetSuffix);
|
||||
|
||||
try {
|
||||
ClassLoader classLoader = entityClass.getClassLoader();
|
||||
Class<?> dtoClass = Class.forName(dtoClassName, false, classLoader);
|
||||
if (!dtoSuperType.isAssignableFrom(dtoClass)) {
|
||||
throw new BaseException(String.format("推断的类 %s 不是 %s 的子类", dtoClassName, dtoSuperType.getSimpleName()));
|
||||
}
|
||||
return (Class<? extends T>) dtoClass;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new BaseException(String.format("无法找到DTO类: %s(由实体类 %s 推断)", dtoClassName, entityClassName));
|
||||
} catch (Exception e) {
|
||||
throw new BaseException(String.format("解析DTO类失败: %s(实体类: %s),原因: %s", dtoClassName, entityClassName, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private String inferDtoClassName(String entityClassName, String targetSuffix) {
|
||||
String dtoClassName = entityClassName.replace(ENTITY_PACKAGE_SUFFIX, DTO_PACKAGE_SUFFIX);
|
||||
if (dtoClassName.endsWith(ENTITY_CLASS_SUFFIX)) {
|
||||
int suffixIndex = dtoClassName.length() - ENTITY_CLASS_SUFFIX.length();
|
||||
dtoClassName = dtoClassName.substring(0, suffixIndex) + targetSuffix;
|
||||
} else {
|
||||
dtoClassName += targetSuffix;
|
||||
}
|
||||
return dtoClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接以下载响应的形式返回导入模板
|
||||
*/
|
||||
public ResponseEntity<byte[]> generateImportTemplateResponse() {
|
||||
try {
|
||||
Schema classSchema = commonBaseEntityClass().getAnnotation(Schema.class);
|
||||
String templateName = (classSchema != null && StringUtils.isNotBlank(classSchema.description()))
|
||||
? classSchema.description().replace("实体", "")
|
||||
: commonBaseEntityClass().getSimpleName().replace("Entity", "");
|
||||
|
||||
List<List<String>> head = buildExcelHeadFromEntity();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
EasyExcel.write(outputStream)
|
||||
.head(head)
|
||||
.sheet("导入模板")
|
||||
.doWrite(new ArrayList<>());
|
||||
|
||||
byte[] bytes = outputStream.toByteArray();
|
||||
String fileName = templateName + "_导入模板.xlsx";
|
||||
String encoded = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
|
||||
headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded);
|
||||
headers.setContentLength(bytes.length);
|
||||
|
||||
return ResponseEntity.ok().headers(headers).body(bytes);
|
||||
} catch (Exception e) {
|
||||
log.error("生成导入模板失败", e);
|
||||
throw new BaseException("生成导入模板失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于实体类 @Schema(description) 注解构建 Excel 单层表头
|
||||
* 规则:
|
||||
* - 从字段的 @Schema(description) 读取列名
|
||||
* - 保持字段声明顺序;忽略无效或空标题字段(不允许重复列名)
|
||||
*/
|
||||
protected List<List<String>> buildExcelHeadFromEntity() {
|
||||
Class<?> createDtoClass = commonCreateDtoClass();
|
||||
List<Field> fields = FieldUtils.getAllFieldsList(createDtoClass);
|
||||
|
||||
List<String> titles = fields.stream()
|
||||
.map(field -> field.getAnnotation(Schema.class))
|
||||
.filter(Objects::nonNull)
|
||||
.map(Schema::description)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.toList();
|
||||
|
||||
if (titles.isEmpty()) {
|
||||
throw new BaseException("未能从实体类生成任何列,请检查实体字段与 @Schema(description) 注解配置");
|
||||
}
|
||||
|
||||
return titles.stream()
|
||||
.map(Collections::singletonList)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用导入:根据表头与字段或 @Schema(description) 名称匹配,逐行转换并保存实体
|
||||
*/
|
||||
public ImportResult importFromExcel(MultipartFile file) throws IOException {
|
||||
Class<? extends BaseCreateDto<ENTITY>> dtoClass = commonCreateDtoClass();
|
||||
BaseExcelImportListener<BaseCreateDto<ENTITY>, ENTITY> listener = BaseExcelImportListener.ofDto(
|
||||
importBatchSize(),
|
||||
// 目前还是单次插入
|
||||
batch -> batch.forEach(this::save),
|
||||
this::validateImportedEntity,
|
||||
(e, rowIndex) -> log.warn("导入行异常 row={}, ex={}", rowIndex, e.toString())
|
||||
);
|
||||
|
||||
EasyExcel.read(file.getInputStream(), dtoClass, listener)
|
||||
.headRowNumber(1)
|
||||
.sheet()
|
||||
.doRead();
|
||||
|
||||
return ImportResult.builder()
|
||||
.totalRows(listener.getCurrentRowIndex())
|
||||
.successRows(listener.getSuccessRows())
|
||||
.errorRows(listener.getErrors().size())
|
||||
.errors(listener.getErrors())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 实体级导入校验,返回是否通过。默认通过。
|
||||
* 可将错误记录到 errors 列表中(如果需要外部收集,可在子类维护)。
|
||||
*/
|
||||
protected boolean validateImportedEntity(ENTITY entity, int rowIndex, List<String> errors) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批大小,默认 200
|
||||
*/
|
||||
protected int importBatchSize() {
|
||||
return 200;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
package com.bgasol.common.core.base.service;
|
||||
|
||||
import cn.idev.excel.EasyExcel;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.bgasol.common.core.base.dto.BaseCreateDto;
|
||||
import com.bgasol.common.core.base.listener.BaseExcelImportListener;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
|
@ -12,17 +9,11 @@ import com.bgasol.common.core.base.entity.BaseEntity;
|
|||
import com.bgasol.common.core.base.entity.BaseTreeEntity;
|
||||
import com.bgasol.common.core.base.exception.BaseException;
|
||||
import com.bgasol.common.core.base.mapper.MyBaseMapper;
|
||||
import com.bgasol.common.core.base.vo.ImportResult;
|
||||
import com.bgasol.common.core.base.vo.PageVo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import jakarta.persistence.JoinTable;
|
||||
import jakarta.persistence.Transient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.redisson.api.RMapCache;
|
||||
import org.redisson.api.RedissonClient;
|
||||
|
|
@ -30,15 +21,10 @@ import org.springframework.beans.factory.annotation.Value;
|
|||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static com.bgasol.common.constant.value.RedisConfigValues.DEFAULT_TIME_UNIT;
|
||||
import static com.bgasol.common.constant.value.RedisConfigValues.randomizeTtl;
|
||||
|
|
@ -67,82 +53,6 @@ public abstract class BaseService<ENTITY extends BaseEntity, PAGE_DTO extends Ba
|
|||
return (Class<ENTITY>) ResolvableType.forClass(getClass()).as(BaseService.class).getGeneric(0).resolve();
|
||||
}
|
||||
|
||||
// DTO类缓存,避免重复反射和类加载
|
||||
private volatile Class<? extends BaseCreateDto<ENTITY>> cachedCreateDtoClass;
|
||||
|
||||
// 常量定义
|
||||
private static final String ENTITY_PACKAGE_SUFFIX = ".entity.";
|
||||
private static final String DTO_PACKAGE_SUFFIX = ".dto.";
|
||||
private static final String ENTITY_CLASS_SUFFIX = "Entity";
|
||||
private static final String CREATE_DTO_CLASS_SUFFIX = "CreateDto";
|
||||
|
||||
/**
|
||||
* 创建模板对应的 DTO 类型(自动推断)。
|
||||
* 规则:将 ENTITY 的包路径由 ".entity." 替换为 ".dto.",类名后缀由 "Entity" 改为 "CreateDto"。
|
||||
* 例如:com.bgasol.model.system.role.entity.RoleEntity -> com.bgasol.model.system.role.dto.RoleCreateDto。
|
||||
* 若推断失败或类不存在,则抛出异常。
|
||||
*
|
||||
* @return 对应的CreateDto类
|
||||
* @throws BaseException 当无法获取实体类类型或推断DTO类失败时
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Class<? extends BaseCreateDto<ENTITY>> commonCreateDtoClass() {
|
||||
if (cachedCreateDtoClass == null) {
|
||||
synchronized (this) {
|
||||
if (cachedCreateDtoClass == null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<? extends BaseCreateDto<ENTITY>> resolved = (Class<? extends BaseCreateDto<ENTITY>>) (Class<?>) resolveDtoClass(BaseCreateDto.class, CREATE_DTO_CLASS_SUFFIX);
|
||||
cachedCreateDtoClass = resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cachedCreateDtoClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用DTO解析:基于实体类全名,将 .entity. 替换为 .dto.,并用指定后缀替换/追加类名。
|
||||
* 例如:RoleEntity -> RoleCreateDto / RoleUpdateDto 等。
|
||||
*
|
||||
* @param dtoSuperType 目标DTO的父类型(用于类型校验)
|
||||
* @param targetSuffix 目标DTO类名后缀,例如 "CreateDto"
|
||||
* @return 解析并加载后的 DTO Class
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <T> Class<? extends T> resolveDtoClass(Class<T> dtoSuperType, String targetSuffix) {
|
||||
Class<ENTITY> entityClass = commonBaseEntityClass();
|
||||
if (entityClass == null) {
|
||||
throw new BaseException("无法获取实体类类型,请检查泛型参数配置");
|
||||
}
|
||||
|
||||
String entityClassName = entityClass.getName();
|
||||
String dtoClassName = inferDtoClassName(entityClassName, targetSuffix);
|
||||
|
||||
try {
|
||||
ClassLoader classLoader = entityClass.getClassLoader();
|
||||
Class<?> dtoClass = Class.forName(dtoClassName, false, classLoader);
|
||||
if (!dtoSuperType.isAssignableFrom(dtoClass)) {
|
||||
throw new BaseException(String.format("推断的类 %s 不是 %s 的子类", dtoClassName, dtoSuperType.getSimpleName()));
|
||||
}
|
||||
return (Class<? extends T>) dtoClass;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new BaseException(String.format("无法找到DTO类: %s(由实体类 %s 推断)", dtoClassName, entityClassName));
|
||||
} catch (Exception e) {
|
||||
throw new BaseException(String.format("解析DTO类失败: %s(实体类: %s),原因: %s", dtoClassName, entityClassName, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private String inferDtoClassName(String entityClassName, String targetSuffix) {
|
||||
String dtoClassName = entityClassName.replace(ENTITY_PACKAGE_SUFFIX, DTO_PACKAGE_SUFFIX);
|
||||
if (dtoClassName.endsWith(ENTITY_CLASS_SUFFIX)) {
|
||||
int suffixIndex = dtoClassName.length() - ENTITY_CLASS_SUFFIX.length();
|
||||
dtoClassName = dtoClassName.substring(0, suffixIndex) + targetSuffix;
|
||||
} else {
|
||||
dtoClassName += targetSuffix;
|
||||
}
|
||||
return dtoClassName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 保存实体
|
||||
* 如果实体有中间表,也会保存中间表
|
||||
|
|
@ -558,107 +468,4 @@ public abstract class BaseService<ENTITY extends BaseEntity, PAGE_DTO extends Ba
|
|||
mapCache.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接以下载响应的形式返回导入模板
|
||||
*/
|
||||
public ResponseEntity<byte[]> generateImportTemplateResponse() {
|
||||
try {
|
||||
Schema classSchema = commonBaseEntityClass().getAnnotation(Schema.class);
|
||||
String templateName = (classSchema != null && StringUtils.isNotBlank(classSchema.description()))
|
||||
? classSchema.description().replace("实体", "")
|
||||
: commonBaseEntityClass().getSimpleName().replace("Entity", "");
|
||||
|
||||
List<List<String>> head = buildExcelHeadFromEntity();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
EasyExcel.write(outputStream)
|
||||
.head(head)
|
||||
.sheet("导入模板")
|
||||
.doWrite(new ArrayList<>());
|
||||
|
||||
byte[] bytes = outputStream.toByteArray();
|
||||
String fileName = templateName + "_导入模板.xlsx";
|
||||
String encoded = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
|
||||
headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded);
|
||||
headers.setContentLength(bytes.length);
|
||||
|
||||
return ResponseEntity.ok().headers(headers).body(bytes);
|
||||
} catch (Exception e) {
|
||||
log.error("生成导入模板失败", e);
|
||||
throw new BaseException("生成导入模板失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于实体类 @Schema(description) 注解构建 Excel 单层表头
|
||||
* 规则:
|
||||
* - 从字段的 @Schema(description) 读取列名
|
||||
* - 保持字段声明顺序;忽略无效或空标题字段(不允许重复列名)
|
||||
*/
|
||||
protected List<List<String>> buildExcelHeadFromEntity() {
|
||||
Class<?> createDtoClass = commonCreateDtoClass();
|
||||
List<Field> fields = FieldUtils.getAllFieldsList(createDtoClass);
|
||||
|
||||
List<String> titles = fields.stream()
|
||||
.map(field -> field.getAnnotation(Schema.class))
|
||||
.filter(Objects::nonNull)
|
||||
.map(Schema::description)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.toList();
|
||||
|
||||
if (titles.isEmpty()) {
|
||||
throw new BaseException("未能从实体类生成任何列,请检查实体字段与 @Schema(description) 注解配置");
|
||||
}
|
||||
|
||||
return titles.stream()
|
||||
.map(Collections::singletonList)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用导入:根据表头与字段或 @Schema(description) 名称匹配,逐行转换并保存实体
|
||||
*/
|
||||
public ImportResult importFromExcel(MultipartFile file) throws IOException {
|
||||
Class<? extends BaseCreateDto<ENTITY>> dtoClass = commonCreateDtoClass();
|
||||
BaseExcelImportListener<BaseCreateDto<ENTITY>, ENTITY> listener = BaseExcelImportListener.ofDto(
|
||||
importBatchSize(),
|
||||
// 目前还是单次插入
|
||||
batch -> batch.forEach(this::save),
|
||||
this::validateImportedEntity,
|
||||
(e, rowIndex) -> log.warn("导入行异常 row={}, ex={}", rowIndex, e.toString())
|
||||
);
|
||||
|
||||
EasyExcel.read(file.getInputStream(), dtoClass, listener)
|
||||
.headRowNumber(1)
|
||||
.sheet()
|
||||
.doRead();
|
||||
|
||||
return ImportResult.builder()
|
||||
.totalRows(listener.getCurrentRowIndex())
|
||||
.successRows(listener.getSuccessRows())
|
||||
.errorRows(listener.getErrors().size())
|
||||
.errors(listener.getErrors())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 实体级导入校验,返回是否通过。默认通过。
|
||||
* 可将错误记录到 errors 列表中(如果需要外部收集,可在子类维护)。
|
||||
*/
|
||||
protected boolean validateImportedEntity(ENTITY entity, int rowIndex, List<String> errors) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批大小,默认 200
|
||||
*/
|
||||
protected int importBatchSize() {
|
||||
return 200;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.bgasol.web.system.role.controller;
|
|||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.bgasol.common.core.base.controller.BaseController;
|
||||
import com.bgasol.common.core.base.controller.BasePoiController;
|
||||
import com.bgasol.common.core.base.dto.BasePageDto;
|
||||
import com.bgasol.common.core.base.vo.BaseVo;
|
||||
import com.bgasol.common.core.base.vo.ImportResult;
|
||||
|
|
@ -26,7 +27,7 @@ import java.util.List;
|
|||
@Tag(name = "角色管理")
|
||||
@RequestMapping("/role")
|
||||
@Validated
|
||||
public class RoleController extends BaseController<
|
||||
public class RoleController extends BasePoiController<
|
||||
RoleEntity,
|
||||
BasePageDto<RoleEntity>,
|
||||
RoleCreateDto,
|
||||
|
|
@ -80,14 +81,14 @@ public class RoleController extends BaseController<
|
|||
|
||||
@GetMapping("/template-download")
|
||||
@Operation(summary = "下载角色导入模板", operationId = "downloadRoleImportTemplate")
|
||||
// @SaCheckPermission("role:downloadTemplate")
|
||||
@SaCheckPermission("role:downloadImportTemplate")
|
||||
public ResponseEntity<byte[]> downloadImportTemplate() {
|
||||
return super.downloadImportTemplate();
|
||||
}
|
||||
|
||||
@PostMapping(value = "/import", consumes = {"multipart/form-data"})
|
||||
@Operation(summary = "导入角色", operationId = "importRole")
|
||||
// @SaCheckPermission("role:import")
|
||||
@SaCheckPermission("role:importExcel")
|
||||
public BaseVo<ImportResult> importExcel(@RequestPart("file") MultipartFile file) throws IOException {
|
||||
return super.importFromExcel(file);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
package com.bgasol.web.system.role.service;
|
||||
|
||||
import com.bgasol.common.core.base.dto.BasePageDto;
|
||||
import com.bgasol.common.core.base.service.BaseService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.bgasol.common.core.base.dto.BasePageDto;
|
||||
import com.bgasol.common.core.base.entity.BaseEntity;
|
||||
import com.bgasol.common.core.base.service.BasePoiService;
|
||||
import com.bgasol.model.system.menu.entity.MenuEntity;
|
||||
import com.bgasol.model.system.permission.entity.PermissionEntity;
|
||||
import com.bgasol.model.system.role.dto.RoleCreateDto;
|
||||
import com.bgasol.model.system.role.dto.RoleUpdateDto;
|
||||
import com.bgasol.model.system.role.entity.RoleEntity;
|
||||
import com.bgasol.web.system.menu.service.MenuService;
|
||||
import com.bgasol.web.system.permission.service.PermissionService;
|
||||
import com.bgasol.web.system.role.mapper.RoleMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
|
@ -17,13 +21,14 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import com.bgasol.common.core.base.entity.BaseEntity;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class RoleService extends BaseService<RoleEntity, BasePageDto<RoleEntity>> {
|
||||
public class RoleService extends BasePoiService<RoleEntity,
|
||||
BasePageDto<RoleEntity>,
|
||||
RoleCreateDto,
|
||||
RoleUpdateDto> {
|
||||
private final RoleMapper roleMapper;
|
||||
|
||||
private final MenuService menuService;
|
||||
|
|
|
|||
Loading…
Reference in a new issue