From d0fa20e80017f47609742e337eb9a48edb431e5d Mon Sep 17 00:00:00 2001 From: Hamm Date: Wed, 3 Jul 2024 21:11:02 +0800 Subject: [PATCH 1/6] feat(excel-export): add ExcelColumn annotation and related export functionality Introduce the ExcelColumn annotation to mark fields for Excel export. Enhance the RootEntity and UserEntity classes with this annotation, indicating their inclusion in the Excel export process. Extend the RootEntityController and RootService to support export functionality, initiating asynchronous tasks for data export and handling the associated queries. BREAKING CHANGE: The addition of export functionality may affect existing clients who do not expect these new methods or the ExcelColumn annotation. Signed-off-by: Hamm --- .../hamm/airpower/annotation/ExcelColumn.java | 42 ++++++ .../airpower/enums/DateTimeFormatter.java | 66 +++++++++ .../airpower/model/query/QueryExport.java | 18 +++ .../cn/hamm/airpower/root/RootEntity.java | 7 + .../airpower/root/RootEntityController.java | 27 ++++ .../cn/hamm/airpower/root/RootService.java | 137 +++++++++++++++++- .../cn/hamm/airpower/util/DateTimeUtil.java | 68 +++++++++ .../java/cn/hamm/airpower/util/Utils.java | 10 +- 8 files changed, 373 insertions(+), 2 deletions(-) create mode 100644 airpower-core/src/main/java/cn/hamm/airpower/annotation/ExcelColumn.java create mode 100644 airpower-core/src/main/java/cn/hamm/airpower/enums/DateTimeFormatter.java create mode 100644 airpower-core/src/main/java/cn/hamm/airpower/model/query/QueryExport.java create mode 100644 airpower-core/src/main/java/cn/hamm/airpower/util/DateTimeUtil.java diff --git a/airpower-core/src/main/java/cn/hamm/airpower/annotation/ExcelColumn.java b/airpower-core/src/main/java/cn/hamm/airpower/annotation/ExcelColumn.java new file mode 100644 index 0000000..5576714 --- /dev/null +++ b/airpower-core/src/main/java/cn/hamm/airpower/annotation/ExcelColumn.java @@ -0,0 +1,42 @@ +package cn.hamm.airpower.annotation; + +import java.lang.annotation.*; + +/** + *

Excel导出列

+ * + * @author Hamm.cn + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface ExcelColumn { + /** + *

类型

+ */ + Type value() default Type.TEXT; + + + enum Type { + /** + *

普通文本

+ */ + TEXT, + + /** + *

时间日期

+ */ + DATETIME, + + /** + *

数字

+ */ + NUMBER, + + /** + *

布尔值

+ */ + BOOLEAN + } +} diff --git a/airpower-core/src/main/java/cn/hamm/airpower/enums/DateTimeFormatter.java b/airpower-core/src/main/java/cn/hamm/airpower/enums/DateTimeFormatter.java new file mode 100644 index 0000000..d982b83 --- /dev/null +++ b/airpower-core/src/main/java/cn/hamm/airpower/enums/DateTimeFormatter.java @@ -0,0 +1,66 @@ +package cn.hamm.airpower.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + *

格式化模板

+ * + * @author Hamm.cn + */ +@Getter +@AllArgsConstructor +public enum DateTimeFormatter { + /** + *

+ */ + YEAR("yyyy"), + + /** + *

+ */ + MONTH("MM"), + + /** + *

+ */ + DAY("dd"), + + /** + *

+ */ + HOUR("HH"), + + /** + *

+ */ + MINUTE("mm"), + + /** + *

+ */ + SECOND("ss"), + + /** + *

年月日

+ */ + FULL_DATE(YEAR + "-" + MONTH + "-" + DAY), + + /** + *

时分秒

+ */ + FULL_TIME(HOUR + ":" + MINUTE + ":" + SECOND), + + /** + *

年月日时分秒

+ */ + FULL_DATETIME(FULL_DATE + " " + FULL_TIME), + + /** + *

月日时分

+ */ + SHORT_DATETIME(MONTH + "-" + DAY + " " + HOUR + ":" + MINUTE), + ; + + private final String value; +} diff --git a/airpower-core/src/main/java/cn/hamm/airpower/model/query/QueryExport.java b/airpower-core/src/main/java/cn/hamm/airpower/model/query/QueryExport.java new file mode 100644 index 0000000..ea8c82b --- /dev/null +++ b/airpower-core/src/main/java/cn/hamm/airpower/model/query/QueryExport.java @@ -0,0 +1,18 @@ +package cn.hamm.airpower.model.query; + +import cn.hamm.airpower.root.RootModel; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + *

查询导出结果模型

+ * + * @author Hamm.cn + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class QueryExport extends RootModel { + @NotBlank(message = "文件Code不能为空") + private String fileCode; +} diff --git a/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntity.java b/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntity.java index 3e04e7c..d4c59b6 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntity.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntity.java @@ -45,6 +45,7 @@ public class RootEntity> extends RootModel @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(nullable = false, columnDefinition = "bigint UNSIGNED comment 'ID'") @Min(value = 0, message = "ID必须大于{value}") + @ExcelColumn(ExcelColumn.Type.NUMBER) @NotNull(groups = {WhenUpdate.class, WhenIdRequired.class}, message = "ID不能为空") private Long id; @@ -53,6 +54,7 @@ public class RootEntity> extends RootModel @Column(columnDefinition = "text comment '备注'") @Length(max = 1000, message = "备注最多允许{max}个字符") @Exclude(filters = {WhenPayLoad.class}) + @ExcelColumn private String remark; @Description("是否禁用") @@ -60,30 +62,35 @@ public class RootEntity> extends RootModel @Search(Search.Mode.EQUALS) @Column(columnDefinition = "tinyint UNSIGNED default 0 comment '是否禁用'") @Exclude(filters = {WhenPayLoad.class}) + @ExcelColumn(ExcelColumn.Type.BOOLEAN) private Boolean isDisabled; @Description("创建时间") @ReadOnly @Column(columnDefinition = "bigint UNSIGNED default 0 comment '创建时间'") @Exclude(filters = {WhenPayLoad.class}) + @ExcelColumn(ExcelColumn.Type.DATETIME) private Long createTime; @Description("创建人ID") @ReadOnly @Column(columnDefinition = "bigint UNSIGNED default 0 comment '创建人ID'") @Exclude(filters = {WhenPayLoad.class}) + @ExcelColumn(ExcelColumn.Type.NUMBER) private Long createUserId; @Description("修改人ID") @ReadOnly @Column(columnDefinition = "bigint UNSIGNED default 0 comment '修改人ID'") @Exclude(filters = {WhenPayLoad.class}) + @ExcelColumn(ExcelColumn.Type.NUMBER) private Long updateUserId; @Description("修改时间") @ReadOnly @Column(columnDefinition = "bigint UNSIGNED default 0 comment '修改时间'") @Exclude(filters = {WhenPayLoad.class}) + @ExcelColumn(ExcelColumn.Type.DATETIME) private Long updateTime; @Transient diff --git a/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntityController.java b/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntityController.java index e987ad8..bba8f98 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntityController.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntityController.java @@ -10,6 +10,7 @@ import cn.hamm.airpower.enums.ServiceError; import cn.hamm.airpower.exception.ServiceException; import cn.hamm.airpower.interfaces.IEntityAction; import cn.hamm.airpower.model.Json; +import cn.hamm.airpower.model.query.QueryExport; import cn.hamm.airpower.model.query.QueryPageRequest; import cn.hamm.airpower.model.query.QueryPageResponse; import cn.hamm.airpower.model.query.QueryRequest; @@ -42,6 +43,32 @@ public class RootEntityController< @Autowired protected S service; + /** + *

导出

+ */ + @Description("异步导出") + @RequestMapping("export") + public Json export(@RequestBody QueryRequest queryRequest) { + return Json.data(service.createExportTask(queryRequest), "导出任务创建成功"); + } + + @RequestMapping("tests") + @Permission(login = false) + public Json tests(@RequestBody QueryRequest queryRequest) { + List list = service.getList(queryRequest); + return Json.success(service.saveExportFile(list)); + } + + /** + *

导出

+ */ + @Description("异步导出") + @RequestMapping("queryExport") + @Permission(authorize = false) + public Json queryExport(@RequestBody @Validated QueryExport queryExport) { + return Json.data(service.queryExport(queryExport), "请下载导出的文件"); + } + /** *

添加一条新数据接口

* diff --git a/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java b/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java index 43ecc0e..47e1ec5 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java @@ -1,16 +1,20 @@ package cn.hamm.airpower.root; +import cn.hamm.airpower.annotation.ExcelColumn; import cn.hamm.airpower.annotation.Search; import cn.hamm.airpower.config.Configs; import cn.hamm.airpower.config.Constant; import cn.hamm.airpower.config.MessageConstant; import cn.hamm.airpower.enums.ServiceError; import cn.hamm.airpower.exception.ServiceException; +import cn.hamm.airpower.model.Json; import cn.hamm.airpower.model.Page; import cn.hamm.airpower.model.Sort; +import cn.hamm.airpower.model.query.QueryExport; import cn.hamm.airpower.model.query.QueryPageRequest; import cn.hamm.airpower.model.query.QueryPageResponse; import cn.hamm.airpower.model.query.QueryRequest; +import cn.hamm.airpower.util.ReflectUtil; import cn.hamm.airpower.util.Utils; import jakarta.persistence.Column; import jakarta.persistence.criteria.*; @@ -31,6 +35,8 @@ import org.springframework.util.StringUtils; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; import java.util.function.BiFunction; @@ -50,6 +56,135 @@ public class RootService, R extends RootRepository> { @Autowired protected R repository; + /** + *

导出任务缓存前缀

+ */ + private static final String EXPORT_TASK_PREFIX = "export_task_"; + + /** + *

创建导出任务

+ * + * @param queryRequest 请求查询的参数 + * @return 导出任务ID + */ + public final String createExportTask(QueryRequest queryRequest) { + String fileCode = Utils.getRandomUtil().randomString().toLowerCase(); + final String fileCacheKey = EXPORT_TASK_PREFIX + fileCode; + Object object = Utils.getRedisUtil().get(fileCacheKey); + if (Objects.nonNull(object)) { + return createExportTask(queryRequest); + } + Utils.getRedisUtil().set(fileCacheKey, ""); + Utils.getTaskUtil().runAsync(() -> { + // 查数据 写文件 + List list = getList(queryRequest); + list = beforeExport(list); + String url = saveExportFile(list); + Utils.getRedisUtil().set(fileCacheKey, url); + }); + return fileCode; + } + + /** + *

保存导出的数据到文件

+ * + * @param exportList 导出的数据 + * @return 存储的文件地址 + */ + protected String saveExportFile(List exportList) { + // 导出到csv并存出文件 + ReflectUtil reflectUtil = Utils.getReflectUtil(); + List fieldNameList = new ArrayList<>(); + + List headerList = new ArrayList<>(); + for (Field field : reflectUtil.getFieldList(getEntityClass())) { + ExcelColumn excelColumn = reflectUtil.getAnnotation(ExcelColumn.class, field); + if (Objects.isNull(excelColumn)) { + continue; + } + fieldNameList.add(field.getName()); + String fieldName = reflectUtil.getDescription(field); + headerList.add(fieldName); + } + + List rowList = new ArrayList<>(); + // 添加表头 + rowList.add(String.join(",", headerList)); + + String json = Json.toString(exportList); + List> mapList = Json.parse2MapList(json); + for (Map map : mapList) { + List columnList = new ArrayList<>(); + for (String fieldName : fieldNameList) { + Object value = map.get(fieldName); + if (Objects.isNull(value)) { + value = Constant.LINE; + } + if (!StringUtils.hasText(value.toString())) { + value = Constant.LINE; + } + + String breakLine = "\n"; + value = value.toString().replaceAll(Constant.COMMA, " ").replaceAll(breakLine, " "); + try { + Field field = getEntityClass().getField(fieldName); + ExcelColumn excelColumn = reflectUtil.getAnnotation(ExcelColumn.class, field); + if (Objects.nonNull(excelColumn)) { + switch (excelColumn.value()) { + case DATETIME: + value = Utils.getDateTimeUtil().format(Long.parseLong(value.toString())); + break; + case TEXT: + value = "\t" + value; + break; + default: + } + } + } catch (Exception exception) { + log.error(exception.getMessage(), exception); + } + columnList.add(value.toString()); + } + rowList.add(String.join(",", columnList)); + } + String content = String.join("\n", rowList); + final String prefix = "export_file_"; + final String suffix = ".csv"; + try { + // 创建临时文件 + Path tempFilePath = Files.createTempFile(prefix, suffix); + Files.writeString(tempFilePath, content, java.nio.charset.StandardCharsets.UTF_8); + return tempFilePath.getFileName().toString(); + } catch (Exception exception) { + log.error(exception.getMessage(), exception); + throw new ServiceException(exception); + } + } + + /** + *

导出前置方法

+ * + * @param exportList 导出的数据列表 + * @return 处理后的数据列表 + */ + protected List beforeExport(@NotNull List exportList) { + return exportList; + } + + /** + *

查询导出结果

+ * + * @param queryExportModel 查询导出模型 + * @return 导出文件地址 + */ + protected final String queryExport(@NotNull QueryExport queryExportModel) { + final String fileCacheKey = EXPORT_TASK_PREFIX + queryExportModel.getFileCode(); + Object object = Utils.getRedisUtil().get(fileCacheKey); + ServiceError.DATA_NOT_FOUND.whenNull(object, "错误的FileCode"); + ServiceError.DATA_NOT_FOUND.whenEmpty(object, "文件暂未准备完毕"); + return object.toString(); + } + /** *

🟢添加前置方法

* @@ -780,7 +915,7 @@ public class RootService, R extends RootRepository> { * @return 搜索条件 */ @SuppressWarnings("AlibabaSwitchStatement") - private @NotNull List getPredicateList( + private @NotNull List getPredicateList( @NotNull From root, @NotNull CriteriaBuilder builder, @NotNull Object search, boolean isEqual ) { List predicateList = new ArrayList<>(); diff --git a/airpower-core/src/main/java/cn/hamm/airpower/util/DateTimeUtil.java b/airpower-core/src/main/java/cn/hamm/airpower/util/DateTimeUtil.java new file mode 100644 index 0000000..d2290d0 --- /dev/null +++ b/airpower-core/src/main/java/cn/hamm/airpower/util/DateTimeUtil.java @@ -0,0 +1,68 @@ +package cn.hamm.airpower.util; + +import cn.hamm.airpower.enums.DateTimeFormatter; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +/** + *

时间日期格式化

+ * + * @author Hamm.cn + */ +@Component +public class DateTimeUtil { + /** + *

默认时区

+ */ + private static final String ASIA_CHONGQING = "Asia/Chongqing"; + + /** + *

格式化时间

+ * + * @param milliSecond 毫秒 + * @return 格式化后的时间 + */ + public final @NotNull String format(long milliSecond) { + return format(milliSecond, DateTimeFormatter.FULL_DATETIME.getValue()); + } + + /** + *

格式化时间

+ * + * @param milliSecond 毫秒 + * @param formatter 格式化模板 + * @return 格式化后的时间 + */ + public final @NotNull String format(long milliSecond, DateTimeFormatter formatter) { + return format(milliSecond, formatter.getValue()); + } + + /** + *

格式化时间

+ * + * @param milliSecond 毫秒 + * @param formatter 格式化模板 + * @return 格式化后的时间 + */ + public final @NotNull String format(long milliSecond, String formatter) { + return format(milliSecond, formatter, ASIA_CHONGQING); + } + + /** + *

格式化时间

+ * + * @param milliSecond 毫秒 + * @param formatter 格式化模板 + * @param zone 时区 + * @return 格式化后的时间 + */ + public final @NotNull String format(long milliSecond, String formatter, String zone) { + Instant instant = Instant.ofEpochMilli(milliSecond); + ZonedDateTime beijingTime = instant.atZone(ZoneId.of(zone)); + return beijingTime.format(java.time.format.DateTimeFormatter.ofPattern(formatter)); + } +} diff --git a/airpower-core/src/main/java/cn/hamm/airpower/util/Utils.java b/airpower-core/src/main/java/cn/hamm/airpower/util/Utils.java index f767c6e..c0bc63a 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/util/Utils.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/util/Utils.java @@ -168,6 +168,12 @@ public class Utils { @Getter private static TaskUtil taskUtil; + /** + *

日期时间工具

+ */ + @Getter + private static DateTimeUtil dateTimeUtil; + @Autowired Utils( RedisUtil redisUtil, @@ -194,7 +200,8 @@ public class Utils { StringUtil stringUtil, WebsocketUtil websocketUtil, AesUtil aesUtil, - TaskUtil taskUtil + TaskUtil taskUtil, + DateTimeUtil dateTimeUtil ) { Utils.redisUtil = redisUtil; Utils.emailUtil = emailUtil; @@ -221,6 +228,7 @@ public class Utils { Utils.websocketUtil = websocketUtil; Utils.aesUtil = aesUtil; Utils.taskUtil = taskUtil; + Utils.dateTimeUtil = dateTimeUtil; } /** -- Gitee From 483df9844db8379142a141c288a403033fcec0df Mon Sep 17 00:00:00 2001 From: Hamm Date: Wed, 3 Jul 2024 21:34:01 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(ReflectUtil):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=80=92=E5=BD=92=E8=8E=B7=E5=8F=96=E5=AD=97=E6=AE=B5=E7=9A=84?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hamm --- .../cn/hamm/airpower/util/ReflectUtil.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/airpower-core/src/main/java/cn/hamm/airpower/util/ReflectUtil.java b/airpower-core/src/main/java/cn/hamm/airpower/util/ReflectUtil.java index 07caf02..450e1a8 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/util/ReflectUtil.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/util/ReflectUtil.java @@ -353,4 +353,22 @@ public class ReflectUtil { } return getAnnotation(annotationClass, superMethod, superClass); } + + /** + *

递归获取字段

+ * + * @param fieldName 字段名 + * @param clazz 当前类 + * @return 字段 + */ + public final @Nullable Field getField(String fieldName, Class clazz) { + if (Objects.isNull(clazz) || Object.class.equals(clazz)) { + return null; + } + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + return getField(fieldName, clazz.getSuperclass()); + } + } } -- Gitee From 221e824e16f5d7b1c6d23197fea3ab2c558e0873 Mon Sep 17 00:00:00 2001 From: Hamm Date: Wed, 3 Jul 2024 21:39:31 +0800 Subject: [PATCH 3/6] refactor(core): streamline constants and format date-time patterns - Remove unnecessary LINE_BREAK and TAB constants from Constant.java.- Update DateTimeFormatter patterns to use simplified string literals. - Remove redundant Type enum from ExcelColumn.java. - Add utility method for Excel column value preparation in RootService.java. - Refactor file export logic to use constant CSV file suffix and prefix. Signed-off-by: Hamm --- .../hamm/airpower/annotation/ExcelColumn.java | 1 - .../cn/hamm/airpower/config/Constant.java | 10 +++ .../airpower/enums/DateTimeFormatter.java | 8 +- .../cn/hamm/airpower/root/RootService.java | 84 +++++++++++-------- 4 files changed, 64 insertions(+), 39 deletions(-) diff --git a/airpower-core/src/main/java/cn/hamm/airpower/annotation/ExcelColumn.java b/airpower-core/src/main/java/cn/hamm/airpower/annotation/ExcelColumn.java index 5576714..a0d41e6 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/annotation/ExcelColumn.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/annotation/ExcelColumn.java @@ -17,7 +17,6 @@ public @interface ExcelColumn { */ Type value() default Type.TEXT; - enum Type { /** *

普通文本

diff --git a/airpower-core/src/main/java/cn/hamm/airpower/config/Constant.java b/airpower-core/src/main/java/cn/hamm/airpower/config/Constant.java index ded1a3e..971fb60 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/config/Constant.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/config/Constant.java @@ -237,4 +237,14 @@ public class Constant { *

毫秒转秒

*/ public static final int MILLISECONDS_PER_SECOND = 1000; + + /** + *

换行

+ */ + public static final String LINE_BREAK = "\n"; + + /** + *

TAB

+ */ + public static final String TAB = "\t"; } diff --git a/airpower-core/src/main/java/cn/hamm/airpower/enums/DateTimeFormatter.java b/airpower-core/src/main/java/cn/hamm/airpower/enums/DateTimeFormatter.java index d982b83..0049772 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/enums/DateTimeFormatter.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/enums/DateTimeFormatter.java @@ -44,22 +44,22 @@ public enum DateTimeFormatter { /** *

年月日

*/ - FULL_DATE(YEAR + "-" + MONTH + "-" + DAY), + FULL_DATE("yyyy-MM-dd"), /** *

时分秒

*/ - FULL_TIME(HOUR + ":" + MINUTE + ":" + SECOND), + FULL_TIME("HH:mm:ss"), /** *

年月日时分秒

*/ - FULL_DATETIME(FULL_DATE + " " + FULL_TIME), + FULL_DATETIME("yyyy-MM-dd HH:mm:ss"), /** *

月日时分

*/ - SHORT_DATETIME(MONTH + "-" + DAY + " " + HOUR + ":" + MINUTE), + SHORT_DATETIME("MM-dd HH:mm"), ; private final String value; diff --git a/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java b/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java index 47e1ec5..c87b5a4 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java @@ -50,6 +50,8 @@ import java.util.function.BiFunction; @SuppressWarnings({"unchecked", "SpringJavaInjectionPointsAutowiringInspection"}) @Slf4j public class RootService, R extends RootRepository> { + public static final String EXPORT_FILE_PREFIX = "export_file_"; + public static final String EXPORT_FILE_CSV = ".csv"; /** *

数据源

*/ @@ -95,13 +97,16 @@ public class RootService, R extends RootRepository> { // 导出到csv并存出文件 ReflectUtil reflectUtil = Utils.getReflectUtil(); List fieldNameList = new ArrayList<>(); + List fieldList = new ArrayList<>(); List headerList = new ArrayList<>(); - for (Field field : reflectUtil.getFieldList(getEntityClass())) { + Class entityClass = getEntityClass(); + for (Field field : reflectUtil.getFieldList(entityClass)) { ExcelColumn excelColumn = reflectUtil.getAnnotation(ExcelColumn.class, field); if (Objects.isNull(excelColumn)) { continue; } + fieldList.add(field); fieldNameList.add(field.getName()); String fieldName = reflectUtil.getDescription(field); headerList.add(fieldName); @@ -109,7 +114,7 @@ public class RootService, R extends RootRepository> { List rowList = new ArrayList<>(); // 添加表头 - rowList.add(String.join(",", headerList)); + rowList.add(String.join(Constant.COMMA, headerList)); String json = Json.toString(exportList); List> mapList = Json.parse2MapList(json); @@ -117,42 +122,14 @@ public class RootService, R extends RootRepository> { List columnList = new ArrayList<>(); for (String fieldName : fieldNameList) { Object value = map.get(fieldName); - if (Objects.isNull(value)) { - value = Constant.LINE; - } - if (!StringUtils.hasText(value.toString())) { - value = Constant.LINE; - } - - String breakLine = "\n"; - value = value.toString().replaceAll(Constant.COMMA, " ").replaceAll(breakLine, " "); - try { - Field field = getEntityClass().getField(fieldName); - ExcelColumn excelColumn = reflectUtil.getAnnotation(ExcelColumn.class, field); - if (Objects.nonNull(excelColumn)) { - switch (excelColumn.value()) { - case DATETIME: - value = Utils.getDateTimeUtil().format(Long.parseLong(value.toString())); - break; - case TEXT: - value = "\t" + value; - break; - default: - } - } - } catch (Exception exception) { - log.error(exception.getMessage(), exception); - } + value = prepareExcelColumn(fieldName, value, fieldList); columnList.add(value.toString()); } - rowList.add(String.join(",", columnList)); + rowList.add(String.join(Constant.COMMA, columnList)); } - String content = String.join("\n", rowList); - final String prefix = "export_file_"; - final String suffix = ".csv"; + String content = String.join(Constant.LINE_BREAK, rowList); try { - // 创建临时文件 - Path tempFilePath = Files.createTempFile(prefix, suffix); + Path tempFilePath = Files.createTempFile(EXPORT_FILE_PREFIX, EXPORT_FILE_CSV); Files.writeString(tempFilePath, content, java.nio.charset.StandardCharsets.UTF_8); return tempFilePath.getFileName().toString(); } catch (Exception exception) { @@ -161,6 +138,45 @@ public class RootService, R extends RootRepository> { } } + /** + *

准备导出列

+ * + * @param fieldName 字段名 + * @param value 当前值 + * @param fieldList 字段列表 + * @return 处理后的值 + */ + private @NotNull Object prepareExcelColumn(String fieldName, Object value, List fieldList) { + if (Objects.isNull(value)) { + value = Constant.LINE; + } + if (!StringUtils.hasText(value.toString())) { + value = Constant.LINE; + } + ReflectUtil reflectUtil = Utils.getReflectUtil(); + // 替换逗号 换行 为空格 + value = value.toString().replaceAll(Constant.COMMA, Constant.SPACE).replaceAll(Constant.LINE_BREAK, Constant.SPACE); + try { + Field field = fieldList.stream().filter(item -> item.getName().equals(fieldName)).findFirst().orElse(null); + if (Objects.isNull(field)) { + return value; + } + ExcelColumn excelColumn = reflectUtil.getAnnotation(ExcelColumn.class, field); + if (Objects.isNull(excelColumn)) { + return value; + } + + return switch (excelColumn.value()) { + case DATETIME -> Utils.getDateTimeUtil().format(Long.parseLong(value.toString())); + case TEXT -> Constant.TAB + value; + default -> value; + }; + } catch (Exception exception) { + log.error(exception.getMessage(), exception); + return value; + } + } + /** *

导出前置方法

* -- Gitee From 55bef0e53d95b01cf65be67d7d02f8024ed2f1ff Mon Sep 17 00:00:00 2001 From: Hamm Date: Thu, 4 Jul 2024 01:28:43 +0800 Subject: [PATCH 4/6] feat(core): add dictionary support and enhance excel export functionality - Implement dictionary enumeration for improved data validation and consistency. - Add new constants for boolean values (YES, NO) and improve string manipulation in data export. - Refactor file naming and storage logic for exported CSV files.- Introduce afterExport method for customizable post-processing of exported data. - Extend RootEntityController and RootService with new export-related endpoints and methods. - Update PayAccountEntity with ExcelColumn annotations and dictionary support. BREAKING CHANGE: The addition of dictionary support and changes to the export logic may affect existing clients relying on the previous implementation. --- .../hamm/airpower/annotation/ExcelColumn.java | 7 ++ .../cn/hamm/airpower/config/Constant.java | 10 +++ .../hamm/airpower/config/ServiceConfig.java | 7 ++ .../airpower/root/RootEntityController.java | 7 -- .../cn/hamm/airpower/root/RootService.java | 90 ++++++++++++++++--- 5 files changed, 101 insertions(+), 20 deletions(-) diff --git a/airpower-core/src/main/java/cn/hamm/airpower/annotation/ExcelColumn.java b/airpower-core/src/main/java/cn/hamm/airpower/annotation/ExcelColumn.java index a0d41e6..5b1a35a 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/annotation/ExcelColumn.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/annotation/ExcelColumn.java @@ -1,5 +1,6 @@ package cn.hamm.airpower.annotation; +import cn.hamm.airpower.validate.dictionary.Dictionary; import java.lang.annotation.*; /** @@ -33,6 +34,12 @@ public @interface ExcelColumn { */ NUMBER, + /** + *

字典

+ * @apiNote 请确保同时标记了 @{@link Dictionary} + */ + DICTIONARY, + /** *

布尔值

*/ diff --git a/airpower-core/src/main/java/cn/hamm/airpower/config/Constant.java b/airpower-core/src/main/java/cn/hamm/airpower/config/Constant.java index 971fb60..8babbe7 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/config/Constant.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/config/Constant.java @@ -247,4 +247,14 @@ public class Constant { *

TAB

*/ public static final String TAB = "\t"; + + /** + *

+ */ + public static final String YES = "是"; + + /** + *

+ */ + public static final String NO = "否"; } diff --git a/airpower-core/src/main/java/cn/hamm/airpower/config/ServiceConfig.java b/airpower-core/src/main/java/cn/hamm/airpower/config/ServiceConfig.java index 802e9c7..f281ff5 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/config/ServiceConfig.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/config/ServiceConfig.java @@ -83,6 +83,13 @@ public class ServiceConfig { */ private String tenantHeader = "tenant-code"; + /** + *

导出文件的目录

+ * + * @apiNote 请不要使用 / 结尾 + */ + private String exportFilePath = ""; + /** *

是否开启调试模式

* diff --git a/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntityController.java b/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntityController.java index bba8f98..d7966f7 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntityController.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntityController.java @@ -52,13 +52,6 @@ public class RootEntityController< return Json.data(service.createExportTask(queryRequest), "导出任务创建成功"); } - @RequestMapping("tests") - @Permission(login = false) - public Json tests(@RequestBody QueryRequest queryRequest) { - List list = service.getList(queryRequest); - return Json.success(service.saveExportFile(list)); - } - /** *

导出

*/ diff --git a/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java b/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java index c87b5a4..51f2bdb 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java @@ -5,8 +5,10 @@ import cn.hamm.airpower.annotation.Search; import cn.hamm.airpower.config.Configs; import cn.hamm.airpower.config.Constant; import cn.hamm.airpower.config.MessageConstant; +import cn.hamm.airpower.enums.DateTimeFormatter; import cn.hamm.airpower.enums.ServiceError; import cn.hamm.airpower.exception.ServiceException; +import cn.hamm.airpower.interfaces.IDictionary; import cn.hamm.airpower.model.Json; import cn.hamm.airpower.model.Page; import cn.hamm.airpower.model.Sort; @@ -14,8 +16,10 @@ import cn.hamm.airpower.model.query.QueryExport; import cn.hamm.airpower.model.query.QueryPageRequest; import cn.hamm.airpower.model.query.QueryPageResponse; import cn.hamm.airpower.model.query.QueryRequest; +import cn.hamm.airpower.util.DateTimeUtil; import cn.hamm.airpower.util.ReflectUtil; import cn.hamm.airpower.util.Utils; +import cn.hamm.airpower.validate.dictionary.Dictionary; import jakarta.persistence.Column; import jakarta.persistence.criteria.*; import lombok.extern.slf4j.Slf4j; @@ -33,10 +37,12 @@ import org.springframework.data.jpa.domain.Specification; import org.springframework.util.StringUtils; import java.beans.PropertyDescriptor; +import java.io.File; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.*; import java.util.function.BiFunction; @@ -50,8 +56,7 @@ import java.util.function.BiFunction; @SuppressWarnings({"unchecked", "SpringJavaInjectionPointsAutowiringInspection"}) @Slf4j public class RootService, R extends RootRepository> { - public static final String EXPORT_FILE_PREFIX = "export_file_"; - public static final String EXPORT_FILE_CSV = ".csv"; + /** *

数据源

*/ @@ -59,9 +64,14 @@ public class RootService, R extends RootRepository> { protected R repository; /** - *

导出任务缓存前缀

+ *

导出文件前缀

*/ - private static final String EXPORT_TASK_PREFIX = "export_task_"; + public static final String EXPORT_FILE_PREFIX = "export_file_"; + + /** + *

导出文件后缀

+ */ + public static final String EXPORT_FILE_CSV = ".csv"; /** *

创建导出任务

@@ -71,7 +81,7 @@ public class RootService, R extends RootRepository> { */ public final String createExportTask(QueryRequest queryRequest) { String fileCode = Utils.getRandomUtil().randomString().toLowerCase(); - final String fileCacheKey = EXPORT_TASK_PREFIX + fileCode; + final String fileCacheKey = EXPORT_FILE_PREFIX + fileCode; Object object = Utils.getRedisUtil().get(fileCacheKey); if (Objects.nonNull(object)) { return createExportTask(queryRequest); @@ -92,9 +102,15 @@ public class RootService, R extends RootRepository> { * * @param exportList 导出的数据 * @return 存储的文件地址 + * @apiNote 支持完全重写导出逻辑 + * + *
    + *
  • 默认导出为 CSV 表格,如需自定义导出方式或格式,可直接重写此方法
  • + *
  • 如仅需自定义导出存储位置,可重写 {@link #afterExport(String)}
  • + *
*/ protected String saveExportFile(List exportList) { - // 导出到csv并存出文件 + // 导出到csv并存储文件 ReflectUtil reflectUtil = Utils.getReflectUtil(); List fieldNameList = new ArrayList<>(); List fieldList = new ArrayList<>(); @@ -123,15 +139,55 @@ public class RootService, R extends RootRepository> { for (String fieldName : fieldNameList) { Object value = map.get(fieldName); value = prepareExcelColumn(fieldName, value, fieldList); + value = value.toString().replaceAll(Constant.COMMA, Constant.SPACE).replaceAll(Constant.LINE_BREAK, Constant.SPACE); columnList.add(value.toString()); } rowList.add(String.join(Constant.COMMA, columnList)); } String content = String.join(Constant.LINE_BREAK, rowList); + return afterExport(content); + } + + /** + *

导出数据后置方法

+ * + * @param content 导出的CSV数据 + * @return 存储后的可访问路径 + * @apiNote 可存储至其他地方后返回可访问绝对路径 + */ + protected String afterExport(String content) { + final String separator = File.separator; + final String absolutePath = Configs.getServiceConfig().getExportFilePath() + separator; + ServiceError.SERVICE_ERROR.when(!StringUtils.hasText(absolutePath), "导出失败,未配置导出文件目录"); + + // 准备导出的相对路径 + String exportFilePath = ""; try { - Path tempFilePath = Files.createTempFile(EXPORT_FILE_PREFIX, EXPORT_FILE_CSV); - Files.writeString(tempFilePath, content, java.nio.charset.StandardCharsets.UTF_8); - return tempFilePath.getFileName().toString(); + DateTimeUtil dateTimeUtil = Utils.getDateTimeUtil(); + long milliSecond = System.currentTimeMillis(); + + // 追加今日文件夹 定时任务将按存储文件夹进行删除过时文件 + String todayDir = dateTimeUtil.format(milliSecond, + DateTimeFormatter.FULL_DATE.getValue() + .replaceAll(Constant.LINE, Constant.EMPTY_STRING) + ); + exportFilePath += todayDir + separator; + + if (!Files.exists(Paths.get(absolutePath + exportFilePath))) { + Files.createDirectory(Paths.get(absolutePath + exportFilePath)); + } + + // 存储的文件名 + final String fileName = todayDir + dateTimeUtil.format(milliSecond, + DateTimeFormatter.FULL_TIME.getValue() + .replaceAll(Constant.COLON, Constant.EMPTY_STRING) + ) + Utils.getRandomUtil().randomString() + EXPORT_FILE_CSV; + + // 拼接最终存储路径 + exportFilePath += fileName; + Path path = Paths.get(absolutePath + exportFilePath); + Files.writeString(path, content); + return exportFilePath; } catch (Exception exception) { log.error(exception.getMessage(), exception); throw new ServiceException(exception); @@ -154,8 +210,6 @@ public class RootService, R extends RootRepository> { value = Constant.LINE; } ReflectUtil reflectUtil = Utils.getReflectUtil(); - // 替换逗号 换行 为空格 - value = value.toString().replaceAll(Constant.COMMA, Constant.SPACE).replaceAll(Constant.LINE_BREAK, Constant.SPACE); try { Field field = fieldList.stream().filter(item -> item.getName().equals(fieldName)).findFirst().orElse(null); if (Objects.isNull(field)) { @@ -167,8 +221,18 @@ public class RootService, R extends RootRepository> { } return switch (excelColumn.value()) { - case DATETIME -> Utils.getDateTimeUtil().format(Long.parseLong(value.toString())); + case DATETIME -> Constant.TAB + Utils.getDateTimeUtil().format(Long.parseLong(value.toString())); case TEXT -> Constant.TAB + value; + case BOOLEAN -> (boolean) value ? Constant.YES : Constant.NO; + case DICTIONARY -> { + Dictionary dictionary = reflectUtil.getAnnotation(Dictionary.class, field); + if (Objects.isNull(dictionary)) { + yield value; + } else { + IDictionary dict = Utils.getDictionaryUtil().getDictionary(dictionary.value(), Integer.parseInt(value.toString())); + yield dict.getLabel(); + } + } default -> value; }; } catch (Exception exception) { @@ -194,7 +258,7 @@ public class RootService, R extends RootRepository> { * @return 导出文件地址 */ protected final String queryExport(@NotNull QueryExport queryExportModel) { - final String fileCacheKey = EXPORT_TASK_PREFIX + queryExportModel.getFileCode(); + final String fileCacheKey = EXPORT_FILE_PREFIX + queryExportModel.getFileCode(); Object object = Utils.getRedisUtil().get(fileCacheKey); ServiceError.DATA_NOT_FOUND.whenNull(object, "错误的FileCode"); ServiceError.DATA_NOT_FOUND.whenEmpty(object, "文件暂未准备完毕"); -- Gitee From 6ece2a1bfd83d9e78ca28bfb41061583a13c2240 Mon Sep 17 00:00:00 2001 From: Hamm Date: Thu, 4 Jul 2024 02:10:27 +0800 Subject: [PATCH 5/6] =?UTF-8?q?release(v2.1.1):=20=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E4=BA=86`v2.1.1`=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airpower-core/pom.xml | 4 ++-- .../main/java/cn/hamm/airpower/root/RootService.java | 10 ++++++---- pom.xml | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/airpower-core/pom.xml b/airpower-core/pom.xml index 4f3393d..6476c0d 100644 --- a/airpower-core/pom.xml +++ b/airpower-core/pom.xml @@ -5,10 +5,10 @@ cn.hamm airpower - 2.1.0 + 2.1.1 airpower-core - 2.1.0 + 2.1.1 airpower-core AirPower is a fast backend development tool based on SpringBoot3 and JPA. It's the core of AirPower. diff --git a/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java b/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java index 51f2bdb..dbbd417 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/root/RootService.java @@ -156,12 +156,14 @@ public class RootService, R extends RootRepository> { * @apiNote 可存储至其他地方后返回可访问绝对路径 */ protected String afterExport(String content) { + // 路径分隔符 final String separator = File.separator; + + // 准备导出的相对路径 + String exportFilePath = "export_"; final String absolutePath = Configs.getServiceConfig().getExportFilePath() + separator; ServiceError.SERVICE_ERROR.when(!StringUtils.hasText(absolutePath), "导出失败,未配置导出文件目录"); - // 准备导出的相对路径 - String exportFilePath = ""; try { DateTimeUtil dateTimeUtil = Utils.getDateTimeUtil(); long milliSecond = System.currentTimeMillis(); @@ -178,10 +180,10 @@ public class RootService, R extends RootRepository> { } // 存储的文件名 - final String fileName = todayDir + dateTimeUtil.format(milliSecond, + final String fileName = todayDir + Constant.UNDERLINE + dateTimeUtil.format(milliSecond, DateTimeFormatter.FULL_TIME.getValue() .replaceAll(Constant.COLON, Constant.EMPTY_STRING) - ) + Utils.getRandomUtil().randomString() + EXPORT_FILE_CSV; + ) + Constant.UNDERLINE + Utils.getRandomUtil().randomString() + EXPORT_FILE_CSV; // 拼接最终存储路径 exportFilePath += fileName; diff --git a/pom.xml b/pom.xml index 52066a2..178c8f6 100644 --- a/pom.xml +++ b/pom.xml @@ -4,11 +4,11 @@ 4.0.0 cn.hamm airpower - 2.1.0 + 2.1.1 airpower AirPower is a fast backend development tool based on SpringBoot3 and JPA. - 2.1.0 + 2.1.1 UTF-8 UTF-8 UTF-8 -- Gitee From 0f655ce4251442324dcc4f416f96234394c6ca9c Mon Sep 17 00:00:00 2001 From: Hamm Date: Thu, 4 Jul 2024 02:12:15 +0800 Subject: [PATCH 6/6] docs(root-entity): update comments for export related methods --- .../java/cn/hamm/airpower/root/RootEntityController.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntityController.java b/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntityController.java index d7966f7..841c3ec 100644 --- a/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntityController.java +++ b/airpower-core/src/main/java/cn/hamm/airpower/root/RootEntityController.java @@ -44,18 +44,18 @@ public class RootEntityController< protected S service; /** - *

导出

+ *

创建导出任务

*/ - @Description("异步导出") + @Description("创建导出任务") @RequestMapping("export") public Json export(@RequestBody QueryRequest queryRequest) { return Json.data(service.createExportTask(queryRequest), "导出任务创建成功"); } /** - *

导出

+ *

查询异步导出结果

*/ - @Description("异步导出") + @Description("查询异步导出结果") @RequestMapping("queryExport") @Permission(authorize = false) public Json queryExport(@RequestBody @Validated QueryExport queryExport) { -- Gitee