Merge remote-tracking branch 'origin/master' into feature/payment-apply-bfi-sync
# Conflicts: # blade-common/src/main/java/org/springblade/common/config/BladeCommonConfiguration.java # blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java # blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java # blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java # blade-service/blade-transport/pom.xml # blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml # blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java # blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java # blade-service/blade-transport/src/main/resources/application-dev.yml # blade-third-party-api/pom.xml # doc/nacos/blade-dev.yaml # doc/nacos/blade-prod.yaml # doc/nacos/third-party-api.yaml # pom.xml
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.common.aspect;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springblade.core.log.annotation.ApiLog;
|
||||
import org.springblade.core.log.constant.EventConstant;
|
||||
import org.springblade.core.log.event.ApiLogEvent;
|
||||
import org.springblade.core.log.model.LogApi;
|
||||
import org.springblade.core.log.utils.LogAbstractUtil;
|
||||
import org.springblade.core.tool.constant.BladeConstant;
|
||||
import org.springblade.core.tool.utils.SpringUtil;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.core.tool.utils.WebUtil;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 增删改操作日志切面
|
||||
* 自动拦截 Controller 中的 save/submit/update/remove/delete 等方法,写入 blade_log_api
|
||||
* 已标注 {@link ApiLog} 的方法跳过,避免重复记录
|
||||
*
|
||||
* @author BladeX
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
public class OperationApiLogAspect {
|
||||
|
||||
@Around("""
|
||||
execution(* org.springblade..controller..*.save*(..))
|
||||
|| execution(* org.springblade..controller..*.submit*(..))
|
||||
|| execution(* org.springblade..controller..*.update*(..))
|
||||
|| execution(* org.springblade..controller..*.remove*(..))
|
||||
|| execution(* org.springblade..controller..*.delete*(..))
|
||||
""")
|
||||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
if (!shouldRecord(method)) {
|
||||
return point.proceed();
|
||||
}
|
||||
|
||||
String className = point.getTarget().getClass().getName();
|
||||
String methodName = method.getName();
|
||||
String title = resolveTitle(point.getTarget().getClass(), method);
|
||||
long beginTime = System.currentTimeMillis();
|
||||
Object result = point.proceed();
|
||||
long time = System.currentTimeMillis() - beginTime;
|
||||
try {
|
||||
publishEvent(methodName, className, title, time);
|
||||
} catch (Exception e) {
|
||||
log.warn("记录操作日志失败: {}#{} - {}", className, methodName, e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean shouldRecord(Method method) {
|
||||
if (method.getAnnotation(ApiLog.class) != null) {
|
||||
return false;
|
||||
}
|
||||
HttpServletRequest request = WebUtil.getRequest();
|
||||
if (request == null) {
|
||||
return true;
|
||||
}
|
||||
String httpMethod = request.getMethod();
|
||||
return !"GET".equalsIgnoreCase(httpMethod)
|
||||
&& !"HEAD".equalsIgnoreCase(httpMethod)
|
||||
&& !"OPTIONS".equalsIgnoreCase(httpMethod);
|
||||
}
|
||||
|
||||
private String resolveTitle(Class<?> targetClass, Method method) {
|
||||
Tag tag = AnnotationUtils.findAnnotation(targetClass, Tag.class);
|
||||
Operation operation = AnnotationUtils.findAnnotation(method, Operation.class);
|
||||
String module = tag == null ? null : firstNonBlank(tag.name(), tag.description());
|
||||
String action = operation == null ? null : firstNonBlank(operation.summary(), operation.description());
|
||||
if (StringUtil.isNotBlank(module) && StringUtil.isNotBlank(action)) {
|
||||
return module + "-" + action;
|
||||
}
|
||||
if (StringUtil.isNotBlank(action)) {
|
||||
return action;
|
||||
}
|
||||
String simpleName = targetClass.getSimpleName().replace("Controller", "");
|
||||
return simpleName + "-" + method.getName();
|
||||
}
|
||||
|
||||
private String firstNonBlank(String... values) {
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
for (String value : values) {
|
||||
if (StringUtil.isNotBlank(value)) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void publishEvent(String methodName, String methodClass, String title, long time) {
|
||||
HttpServletRequest request = WebUtil.getRequest();
|
||||
LogApi logApi = new LogApi();
|
||||
logApi.setType(BladeConstant.LOG_NORMAL_TYPE);
|
||||
logApi.setTitle(title);
|
||||
logApi.setTime(String.valueOf(time));
|
||||
logApi.setMethodClass(methodClass);
|
||||
logApi.setMethodName(methodName);
|
||||
LogAbstractUtil.addRequestInfoToLog(request, logApi);
|
||||
Map<String, Object> event = new HashMap<>(16);
|
||||
event.put(EventConstant.EVENT_LOG, logApi);
|
||||
SpringUtil.publishEvent(new ApiLogEvent(event));
|
||||
}
|
||||
|
||||
}
|
||||
+15
-2
@@ -26,6 +26,12 @@
|
||||
package org.springblade.common.config;
|
||||
|
||||
|
||||
import org.springblade.common.aspect.OperationApiLogAspect;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import feign.Logger;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.common.feign.CustomFeignLogger;
|
||||
@@ -39,10 +45,17 @@ import org.springframework.context.annotation.Primary;
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@AllArgsConstructor
|
||||
@AutoConfiguration
|
||||
public class BladeCommonConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||
@ConditionalOnClass(name = "org.springblade.core.log.event.ApiLogEvent")
|
||||
@ConditionalOnProperty(value = "blade.log.operation.enabled", havingValue = "true", matchIfMissing = true)
|
||||
public OperationApiLogAspect operationApiLogAspect() {
|
||||
return new OperationApiLogAspect();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnClass(name = "feign.Logger")
|
||||
|
||||
+133
-5
@@ -46,9 +46,11 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 导入失败明细 Excel 导出工具类
|
||||
@@ -95,7 +97,7 @@ public class ImportFailureExcelUtil {
|
||||
* @param excelClass 原导入 Excel 类型
|
||||
*/
|
||||
public static void export(HttpServletResponse response, String fileName, String sheetName, List<?> data, Class<?> excelClass) {
|
||||
exportFailureReasonOnly(response, fileName, sheetName, data, excelClass);
|
||||
export(response, fileName, sheetName, data, excelClass, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,6 +111,16 @@ public class ImportFailureExcelUtil {
|
||||
*/
|
||||
public static void exportFailureReasonOnly(HttpServletResponse response, String fileName, String sheetName,
|
||||
List<?> data, Class<?> excelClass) {
|
||||
export(response, fileName, sheetName, data, excelClass, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出导入失败明细
|
||||
*
|
||||
* @param markErrorColumns 是否按失败原因里出现的列名,标红对应字段单元格
|
||||
*/
|
||||
private static void export(HttpServletResponse response, String fileName, String sheetName,
|
||||
List<?> data, Class<?> excelClass, boolean markErrorColumns) {
|
||||
response.setContentType("application/vnd.ms-excel");
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
|
||||
@@ -118,7 +130,7 @@ public class ImportFailureExcelUtil {
|
||||
List<List<Object>> rows = buildRows(data, excelFields);
|
||||
try {
|
||||
FastExcel.write(response.getOutputStream())
|
||||
.registerWriteHandler(new ImportFailureCellStyleHandler(excelFields.size()))
|
||||
.registerWriteHandler(new ImportFailureCellStyleHandler(excelFields, rows, markErrorColumns))
|
||||
.head(head)
|
||||
.sheet(sheetName)
|
||||
.doWrite(rows);
|
||||
@@ -204,15 +216,59 @@ public class ImportFailureExcelUtil {
|
||||
throw new NoSuchFieldException(String.join(",", fieldNames));
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本归一化:去掉空白、星号与中英文标点并统一小写
|
||||
* <p>
|
||||
* 目的是让「失败原因里写的列名」与「表头列名」能直接做包含匹配,
|
||||
* 不受「*」「/」「:」等写法差异影响。
|
||||
*/
|
||||
private static String normalize(String value) {
|
||||
return value == null ? "" : value.replaceAll("[\\s*_::,,。;;()()\\[\\]【】<>《》//、-]", "").toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取某一列用于定位的候选关键词
|
||||
* <p>
|
||||
* 包含表头列名、字段名,以及列名按分隔符拆出的片段
|
||||
* (如「车牌号/船号」拆出「车牌号」「船号」,失败原因只写其中一段时也能定位)。
|
||||
*/
|
||||
private static List<String> columnKeywords(Field field) {
|
||||
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
|
||||
String[] value = excelProperty.value();
|
||||
String columnName = value.length == 0 ? field.getName() : value[0];
|
||||
List<String> keywords = new ArrayList<>();
|
||||
keywords.add(columnName);
|
||||
keywords.add(field.getName());
|
||||
keywords.addAll(Arrays.asList(columnName.replace("*", "").split("[//、()()\\s]+")));
|
||||
return keywords.stream()
|
||||
.map(ImportFailureExcelUtil::normalize)
|
||||
.filter(keyword -> keyword.length() >= 2)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static class ImportFailureCellStyleHandler implements CellWriteHandler {
|
||||
|
||||
private final int failureReasonColumnIndex;
|
||||
private final boolean markErrorColumns;
|
||||
private final List<List<Object>> rows;
|
||||
private final Map<Integer, Set<Integer>> redColumnsByRow = new HashMap<>();
|
||||
private final Map<Short, CellStyle> redStyleCache = new HashMap<>();
|
||||
private final Map<Short, CellStyle> noWrapStyleCache = new HashMap<>();
|
||||
private final Map<Integer, Integer> columnWidthCache = new HashMap<>();
|
||||
|
||||
private ImportFailureCellStyleHandler(int failureReasonColumnIndex) {
|
||||
this.failureReasonColumnIndex = failureReasonColumnIndex;
|
||||
private ImportFailureCellStyleHandler(List<Field> excelFields, List<List<Object>> rows, boolean markErrorColumns) {
|
||||
this.failureReasonColumnIndex = excelFields.size();
|
||||
this.markErrorColumns = markErrorColumns;
|
||||
this.rows = rows;
|
||||
if (markErrorColumns) {
|
||||
List<List<String>> columnKeywords = excelFields.stream()
|
||||
.map(ImportFailureExcelUtil::columnKeywords)
|
||||
.toList();
|
||||
for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
|
||||
redColumnsByRow.put(rowIndex, resolveRedColumns(rows.get(rowIndex), columnKeywords));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -230,11 +286,83 @@ public class ImportFailureExcelUtil {
|
||||
return;
|
||||
}
|
||||
adjustColumnWidth(cell);
|
||||
if (cell.getColumnIndex() == failureReasonColumnIndex) {
|
||||
if (relativeRowIndex == null || relativeRowIndex < 0 || relativeRowIndex >= rows.size()) {
|
||||
return;
|
||||
}
|
||||
if (shouldMarkRed(relativeRowIndex, cell.getColumnIndex())) {
|
||||
markRed(cell);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算某一行需要标红的列
|
||||
* <p>
|
||||
* 失败原因里出现的列名即视为出错列;但当某个命中片段被另一列更长的命中
|
||||
* 片段完全覆盖时(如原因「车船类型不能为空」同时命中「车船类型」和它的
|
||||
* 子串「类型」),只保留更长的那一列,避免把无关列一起标红。
|
||||
*/
|
||||
private Set<Integer> resolveRedColumns(List<Object> row, List<List<String>> columnKeywords) {
|
||||
Set<Integer> redColumns = new LinkedHashSet<>();
|
||||
redColumns.add(failureReasonColumnIndex);
|
||||
String failureReason = normalize(String.valueOf(row.get(failureReasonColumnIndex)));
|
||||
if (failureReason.isEmpty()) {
|
||||
return redColumns;
|
||||
}
|
||||
List<int[]> matches = new ArrayList<>();
|
||||
for (int column = 0; column < columnKeywords.size(); column++) {
|
||||
for (String keyword : columnKeywords.get(column)) {
|
||||
int fromIndex = 0;
|
||||
while (true) {
|
||||
int index = failureReason.indexOf(keyword, fromIndex);
|
||||
if (index < 0) {
|
||||
break;
|
||||
}
|
||||
matches.add(new int[]{index, index + keyword.length(), column});
|
||||
fromIndex = index + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int[] match : matches) {
|
||||
if (!isSubsumed(match, matches)) {
|
||||
redColumns.add(match[2]);
|
||||
}
|
||||
}
|
||||
return redColumns;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断该命中片段是否被另一列更长的命中片段完全覆盖
|
||||
*/
|
||||
private boolean isSubsumed(int[] match, List<int[]> matches) {
|
||||
int matchLength = match[1] - match[0];
|
||||
for (int[] other : matches) {
|
||||
if (other[2] == match[2]) {
|
||||
continue;
|
||||
}
|
||||
if ((other[1] - other[0]) > matchLength && other[0] <= match[0] && other[1] >= match[1]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean shouldMarkRed(int rowIndex, int columnIndex) {
|
||||
if (columnIndex == failureReasonColumnIndex) {
|
||||
return true;
|
||||
}
|
||||
if (!markErrorColumns) {
|
||||
return false;
|
||||
}
|
||||
Set<Integer> redColumns = redColumnsByRow.get(rowIndex);
|
||||
return redColumns != null && redColumns.contains(columnIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标红单元格
|
||||
* <p>
|
||||
* 统一只改字体颜色,不加底纹;空单元格的红色字体在屏幕上不可见,
|
||||
* 这类错误依靠失败原因列的文字定位。
|
||||
*/
|
||||
private void markRed(Cell cell) {
|
||||
CellStyle currentStyle = cell.getCellStyle();
|
||||
CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> {
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.common.excel;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 批量导入宽容日期解析器(口径见根工作区 docs/import-spec.md)。
|
||||
* <p>
|
||||
* 解析规则:
|
||||
* <ul>
|
||||
* <li>分隔符 {@code -}、{@code /}、{@code .} 均接受;补零与否均可(2026-08-02 ≡ 2026-8-2)</li>
|
||||
* <li>日期时间:日期部分同上,时间 HH:mm:ss,可省略秒或秒+分(2026-8-2 12:3 可解析)</li>
|
||||
* <li>拒绝:两位年份(26-8-2)、日在前(2/8/2026)、无分隔符(20260802)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class LenientDateParser {
|
||||
|
||||
/**
|
||||
* 日期部分:4 位年 + 分隔符 + 1~2 位月 + 同一分隔符 + 1~2 位日(年在前,拒绝两位年份与日在前)。
|
||||
*/
|
||||
private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4})([-/.])(\\d{1,2})\\2(\\d{1,2})");
|
||||
|
||||
/**
|
||||
* 时间部分:1~2 位时[:1~2 位分[:1~2 位秒]],逐级可省略。
|
||||
*/
|
||||
private static final Pattern TIME_PATTERN = Pattern.compile("(\\d{1,2})(?::(\\d{1,2})(?::(\\d{1,2}))?)?");
|
||||
|
||||
private LenientDateParser() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期文本,失败返回 {@code null}。
|
||||
*
|
||||
* @param value 单元格原始文本
|
||||
* @return 日期;无法识别时返回 null
|
||||
*/
|
||||
public static LocalDate parseDateOrNull(String value) {
|
||||
String normalized = normalize(value);
|
||||
if (normalized == null) {
|
||||
return null;
|
||||
}
|
||||
Matcher matcher = DATE_PATTERN.matcher(normalized);
|
||||
if (!matcher.matches()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.of(Integer.parseInt(matcher.group(1)),
|
||||
Integer.parseInt(matcher.group(3)), Integer.parseInt(matcher.group(4)));
|
||||
} catch (NumberFormatException | java.time.DateTimeException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期文本;允许携带合法时间部分并截断(如 2026-9-1 8:0:0 按 2026-09-01 解析,
|
||||
* 与 fastexcel 内置 LocalDate 转换的既有宽容度保持一致),失败返回 {@code null}。
|
||||
*
|
||||
* @param value 单元格原始文本
|
||||
* @return 日期;无法识别时返回 null
|
||||
*/
|
||||
public static LocalDate parseDateLenientlyOrNull(String value) {
|
||||
LocalDate date = parseDateOrNull(value);
|
||||
if (date != null) {
|
||||
return date;
|
||||
}
|
||||
LocalDateTime dateTime = parseDateTimeOrNull(value);
|
||||
return dateTime == null ? null : dateTime.toLocalDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期时间文本;纯日期按当日零点处理,失败返回 {@code null}。
|
||||
*
|
||||
* @param value 单元格原始文本
|
||||
* @return 日期时间;无法识别时返回 null
|
||||
*/
|
||||
public static LocalDateTime parseDateTimeOrNull(String value) {
|
||||
String normalized = normalize(value);
|
||||
if (normalized == null) {
|
||||
return null;
|
||||
}
|
||||
// 拆出日期与时间两部分;中间允许 1 个及以上空白或小写 t(2026-8-2t12:3 亦接受)。
|
||||
String[] parts = normalized.split("[ \\t]+|(?<=\\d)t", 2);
|
||||
if (parts.length == 0) {
|
||||
return null;
|
||||
}
|
||||
LocalDate date = parseDateOrNull(parts[0]);
|
||||
if (date == null) {
|
||||
return null;
|
||||
}
|
||||
if (parts.length == 1) {
|
||||
return date.atStartOfDay();
|
||||
}
|
||||
Matcher matcher = TIME_PATTERN.matcher(parts[1]);
|
||||
if (!matcher.matches()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
int hour = Integer.parseInt(matcher.group(1));
|
||||
int minute = matcher.group(2) == null ? 0 : Integer.parseInt(matcher.group(2));
|
||||
int second = matcher.group(3) == null ? 0 : Integer.parseInt(matcher.group(3));
|
||||
return LocalDateTime.of(date, LocalTime.of(hour, minute, second));
|
||||
} catch (NumberFormatException | java.time.DateTimeException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期文本,失败抛出携带口径文案的 {@link org.springblade.core.log.exception.ServiceException}。
|
||||
* 空白与 {@code null} 返回 {@code null}(可选字段由业务校验决定是否必填)。
|
||||
*
|
||||
* @param value 单元格原始文本
|
||||
* @param columnName 导入模板列名(用于失败原因文案与失败明细标红定位)
|
||||
* @return 日期
|
||||
*/
|
||||
public static LocalDate parseDate(String value, String columnName) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
LocalDate date = parseDateLenientlyOrNull(value);
|
||||
if (date == null) {
|
||||
throw unrecognizedDate(value, columnName);
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期时间文本,失败抛出携带口径文案的 {@link org.springblade.core.log.exception.ServiceException}。
|
||||
* 空白与 {@code null} 返回 {@code null}(可选字段由业务校验决定是否必填)。
|
||||
*
|
||||
* @param value 单元格原始文本
|
||||
* @param columnName 导入模板列名(用于失败原因文案与失败明细标红定位)
|
||||
* @return 日期时间
|
||||
*/
|
||||
public static LocalDateTime parseDateTime(String value, String columnName) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
LocalDateTime dateTime = parseDateTimeOrNull(value);
|
||||
if (dateTime == null) {
|
||||
throw unrecognizedDate(value, columnName);
|
||||
}
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按口径构造「日期格式无法识别」错误。
|
||||
*
|
||||
* @param value 单元格原始文本
|
||||
* @param columnName 导入模板列名
|
||||
* @return 业务异常
|
||||
*/
|
||||
public static org.springblade.core.log.exception.ServiceException unrecognizedDate(String value, String columnName) {
|
||||
String displayName = columnName == null || columnName.isBlank() ? "日期" : columnName;
|
||||
return new org.springblade.core.log.exception.ServiceException(
|
||||
displayName + " 日期格式无法识别:" + (value == null ? "" : value.trim()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化输入:去首尾空白,跳过空白值。
|
||||
*/
|
||||
private static String normalize(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String normalized = value.trim();
|
||||
return normalized.isEmpty() ? null : normalized;
|
||||
}
|
||||
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.common.excel;
|
||||
|
||||
import cn.idev.excel.converters.Converter;
|
||||
import cn.idev.excel.enums.CellDataTypeEnum;
|
||||
import cn.idev.excel.metadata.GlobalConfiguration;
|
||||
import cn.idev.excel.metadata.data.ReadCellData;
|
||||
import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||
import cn.idev.excel.util.DateUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 批量导入宽容日期转换器(String 承载):Excel 数值日期(序列号)转 ISO 文本,文本原样透传。
|
||||
* <p>
|
||||
* 文本的宽容解析由服务层调用 {@link LenientDateParser} 完成,不在此处抛错——
|
||||
* 转换器阶段抛出的异常会被 fastexcel 包装成 ExcelDataConvertException 直接中断读取,
|
||||
* 无法进入导入失败明细流程。口径见根工作区 docs/import-spec.md。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class LenientDateStringConverter implements Converter<String> {
|
||||
|
||||
@Override
|
||||
public Class<?> supportJavaTypeKey() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
return CellDataTypeEnum.STRING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
if (cellData.getType() == CellDataTypeEnum.NUMBER) {
|
||||
LocalDate date = DateUtils.getLocalDateTime(cellData.getNumberValue().doubleValue(),
|
||||
globalConfiguration.getUse1904windowing()).toLocalDate();
|
||||
return date.toString();
|
||||
}
|
||||
return cellData.getStringValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
return new WriteCellData<>(value);
|
||||
}
|
||||
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.common.excel;
|
||||
|
||||
import cn.idev.excel.converters.Converter;
|
||||
import cn.idev.excel.enums.CellDataTypeEnum;
|
||||
import cn.idev.excel.metadata.GlobalConfiguration;
|
||||
import cn.idev.excel.metadata.data.ReadCellData;
|
||||
import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||
import cn.idev.excel.util.DateUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 批量导入宽容日期时间转换器(String 承载):Excel 数值日期(序列号)转
|
||||
* {@code yyyy-MM-dd HH:mm:ss} 文本,文本原样透传。
|
||||
* <p>
|
||||
* 文本的宽容解析由服务层调用 {@link LenientDateParser} 完成,不在此处抛错——
|
||||
* 转换器阶段抛出的异常会被 fastexcel 包装成 ExcelDataConvertException 直接中断读取,
|
||||
* 无法进入导入失败明细流程。口径见根工作区 docs/import-spec.md。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class LenientDateTimeStringConverter implements Converter<String> {
|
||||
|
||||
/**
|
||||
* 数值日期序列号转文本的输出格式,与导入模板展示格式保持一致。
|
||||
*/
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public Class<?> supportJavaTypeKey() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
return CellDataTypeEnum.STRING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
if (cellData.getType() == CellDataTypeEnum.NUMBER) {
|
||||
LocalDateTime dateTime = DateUtils.getLocalDateTime(cellData.getNumberValue().doubleValue(),
|
||||
globalConfiguration.getUse1904windowing());
|
||||
return dateTime.format(DATE_TIME_FORMATTER);
|
||||
}
|
||||
return cellData.getStringValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
return new WriteCellData<>(value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.common.utils;
|
||||
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 登录密码强度规则工具
|
||||
* 规则:大于8位,同时包含字母、数字、特殊字符(.!@#$%^&*)
|
||||
*
|
||||
* @author BladeX
|
||||
*/
|
||||
public final class PasswordRuleUtil {
|
||||
|
||||
/**
|
||||
* 允许的特殊字符集合
|
||||
*/
|
||||
public static final String SPECIAL_CHARS = ".!@#$%^&*";
|
||||
|
||||
/**
|
||||
* 最小长度(大于 8 位 => 至少 9 位)
|
||||
*/
|
||||
public static final int MIN_LENGTH = 9;
|
||||
|
||||
/**
|
||||
* 自动生成密码长度
|
||||
*/
|
||||
public static final int GENERATE_LENGTH = 10;
|
||||
|
||||
/**
|
||||
* 规则提示文案
|
||||
*/
|
||||
public static final String RULE_MESSAGE = "密码须大于8位,且同时包含字母、数字和特殊字符(.!@#$%^&*)";
|
||||
|
||||
private static final String LETTERS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
private static final String DIGITS = "23456789";
|
||||
private static final String ALL_CHARS = LETTERS + DIGITS + SPECIAL_CHARS;
|
||||
private static final Pattern PASSWORD_PATTERN = Pattern.compile(
|
||||
"^(?=.*[A-Za-z])(?=.*\\d)(?=.*[.!@#$%^&*]).{" + MIN_LENGTH + ",}$"
|
||||
);
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
private PasswordRuleUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验密码是否符合强度规则
|
||||
*
|
||||
* @param password 明文密码
|
||||
* @return true-符合
|
||||
*/
|
||||
public static boolean isValid(String password) {
|
||||
return StringUtil.isNotBlank(password) && PASSWORD_PATTERN.matcher(password).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成符合规则的随机密码
|
||||
*
|
||||
* @return 随机密码
|
||||
*/
|
||||
public static String generate() {
|
||||
List<Character> chars = new ArrayList<>(GENERATE_LENGTH);
|
||||
chars.add(LETTERS.charAt(SECURE_RANDOM.nextInt(LETTERS.length())));
|
||||
chars.add(DIGITS.charAt(SECURE_RANDOM.nextInt(DIGITS.length())));
|
||||
chars.add(SPECIAL_CHARS.charAt(SECURE_RANDOM.nextInt(SPECIAL_CHARS.length())));
|
||||
for (int i = chars.size(); i < GENERATE_LENGTH; i++) {
|
||||
chars.add(ALL_CHARS.charAt(SECURE_RANDOM.nextInt(ALL_CHARS.length())));
|
||||
}
|
||||
Collections.shuffle(chars, SECURE_RANDOM);
|
||||
StringBuilder password = new StringBuilder(GENERATE_LENGTH);
|
||||
for (Character ch : chars) {
|
||||
password.append(ch);
|
||||
}
|
||||
return password.toString();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user