5 Commits

Author SHA1 Message Date
b2894lxlx 0797785176 项目、合同、运单、预结算、正式结算、付款对接mk 2026-09-21 10:36:00 +08:00
b2894lxlx 86adb47392 调试mk 2026-09-20 19:03:13 +08:00
b2894lxlx 780cd56ffe 调试mk 2026-09-20 18:34:40 +08:00
b2894lxlx 8cf9140f56 调试mk 2026-09-20 18:12:45 +08:00
b2894lxlx 2f56ca07cb 调试mk 2026-09-20 17:35:59 +08:00
89 changed files with 3749 additions and 2888 deletions
-9
View File
@@ -22,15 +22,6 @@
<groupId>org.springblade</groupId> <groupId>org.springblade</groupId>
<artifactId>blade-starter-loadbalancer</artifactId> <artifactId>blade-starter-loadbalancer</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-log</artifactId>
<!-- providedLenientDateParser 编译期需要 ServiceException(位于 starter-log),
但 provided 不向下游传递——否则网关会经 common 拿到 blade-core-tool 的
BladeConverterConfigurationimplements WebMvcConfigurer),在无 spring-webmvc
的 WebFlux 网关里启动即崩。业务服务经 blade-core-boot 自带 starter-log,运行时不受影响。 -->
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springblade</groupId> <groupId>org.springblade</groupId>
<artifactId>blade-core-auto</artifactId> <artifactId>blade-core-auto</artifactId>
@@ -46,11 +46,9 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
/** /**
* 导入失败明细 Excel 导出工具类 * 导入失败明细 Excel 导出工具类
@@ -97,7 +95,7 @@ public class ImportFailureExcelUtil {
* @param excelClass 原导入 Excel 类型 * @param excelClass 原导入 Excel 类型
*/ */
public static void export(HttpServletResponse response, String fileName, String sheetName, List<?> data, Class<?> excelClass) { public static void export(HttpServletResponse response, String fileName, String sheetName, List<?> data, Class<?> excelClass) {
export(response, fileName, sheetName, data, excelClass, true); exportFailureReasonOnly(response, fileName, sheetName, data, excelClass);
} }
/** /**
@@ -111,16 +109,6 @@ public class ImportFailureExcelUtil {
*/ */
public static void exportFailureReasonOnly(HttpServletResponse response, String fileName, String sheetName, public static void exportFailureReasonOnly(HttpServletResponse response, String fileName, String sheetName,
List<?> data, Class<?> excelClass) { 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.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setCharacterEncoding(StandardCharsets.UTF_8.name());
String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8); String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
@@ -130,7 +118,7 @@ public class ImportFailureExcelUtil {
List<List<Object>> rows = buildRows(data, excelFields); List<List<Object>> rows = buildRows(data, excelFields);
try { try {
FastExcel.write(response.getOutputStream()) FastExcel.write(response.getOutputStream())
.registerWriteHandler(new ImportFailureCellStyleHandler(excelFields, rows, markErrorColumns)) .registerWriteHandler(new ImportFailureCellStyleHandler(excelFields.size()))
.head(head) .head(head)
.sheet(sheetName) .sheet(sheetName)
.doWrite(rows); .doWrite(rows);
@@ -216,59 +204,15 @@ public class ImportFailureExcelUtil {
throw new NoSuchFieldException(String.join(",", fieldNames)); 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 static class ImportFailureCellStyleHandler implements CellWriteHandler {
private final int failureReasonColumnIndex; 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> redStyleCache = new HashMap<>();
private final Map<Short, CellStyle> noWrapStyleCache = new HashMap<>(); private final Map<Short, CellStyle> noWrapStyleCache = new HashMap<>();
private final Map<Integer, Integer> columnWidthCache = new HashMap<>(); private final Map<Integer, Integer> columnWidthCache = new HashMap<>();
private ImportFailureCellStyleHandler(List<Field> excelFields, List<List<Object>> rows, boolean markErrorColumns) { private ImportFailureCellStyleHandler(int failureReasonColumnIndex) {
this.failureReasonColumnIndex = excelFields.size(); this.failureReasonColumnIndex = failureReasonColumnIndex;
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 @Override
@@ -286,83 +230,11 @@ public class ImportFailureExcelUtil {
return; return;
} }
adjustColumnWidth(cell); adjustColumnWidth(cell);
if (relativeRowIndex == null || relativeRowIndex < 0 || relativeRowIndex >= rows.size()) { if (cell.getColumnIndex() == failureReasonColumnIndex) {
return;
}
if (shouldMarkRed(relativeRowIndex, cell.getColumnIndex())) {
markRed(cell); 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) { private void markRed(Cell cell) {
CellStyle currentStyle = cell.getCellStyle(); CellStyle currentStyle = cell.getCellStyle();
CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> { CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> {
@@ -1,198 +0,0 @@
/**
* 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;
}
}
@@ -1,75 +0,0 @@
/**
* 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);
}
}
@@ -1,82 +0,0 @@
/**
* 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);
}
}
@@ -39,6 +39,7 @@ public interface IBusinessProcessClient {
String QUERY_BUSINESS_PROCESS_SNAPSHOT = API_PREFIX + "/queryBusinessProcessSnapshot"; String QUERY_BUSINESS_PROCESS_SNAPSHOT = API_PREFIX + "/queryBusinessProcessSnapshot";
String QUERY_APPROVED_RECORD_LIST = API_PREFIX + "/queryApprovedRecordsNoAttachments"; String QUERY_APPROVED_RECORD_LIST = API_PREFIX + "/queryApprovedRecordsNoAttachments";
String GET_CURRENT_NODES = API_PREFIX + "/getCurrentNodes"; String GET_CURRENT_NODES = API_PREFIX + "/getCurrentNodes";
String GET_PROCESS_INFO = API_PREFIX + "/getProcessInfo";
/** /**
* 提交业务流程 * 提交业务流程
@@ -114,4 +115,15 @@ public interface IBusinessProcessClient {
@GetMapping(GET_CURRENT_NODES) @GetMapping(GET_CURRENT_NODES)
FR<Object> getCurrentNodes(@RequestParam("processInstanceId") String processInstanceId, FR<Object> getCurrentNodes(@RequestParam("processInstanceId") String processInstanceId,
@RequestParam(value = "loginName", required = false) String loginName); @RequestParam(value = "loginName", required = false) String loginName);
/**
* 获取流程实例详情
*
* @param processInstanceId 流程实例id
* @param loginName MK登录名(手机号)
* @return 流程实例详情
*/
@GetMapping(GET_PROCESS_INFO)
FR<Object> getProcessInfo(@RequestParam("processInstanceId") String processInstanceId,
@RequestParam(value = "loginName", required = false) String loginName);
} }
@@ -0,0 +1,26 @@
package org.springblade.transport.feign;
import org.springblade.core.tool.api.FR;
import org.springblade.transport.pojo.dto.CustomerProcessNodeSyncDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* 客商档案 Feign接口
*/
@FeignClient(value = "blade-transport")
public interface ICustomerArchiveClient {
String API_PREFIX = "/feign/client/customerArchive";
String SYNC_PROCESS_NODE = API_PREFIX + "/syncProcessNode";
/**
* 按 MK 当前节点同步客商当前节点、当前处理人和审批状态
*
* @param param 同步参数
* @return 是否成功
*/
@PostMapping(SYNC_PROCESS_NODE)
FR<Boolean> syncProcessNode(@RequestBody CustomerProcessNodeSyncDTO param);
}
@@ -0,0 +1,20 @@
package org.springblade.transport.feign;
import org.springblade.core.tool.api.FR;
import org.springblade.transport.pojo.dto.MkProcessSyncDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* MK 业务流程同步 Feign
*/
@FeignClient(value = "blade-transport")
public interface IMkProcessClient {
String API_PREFIX = "/feign/client/mkProcess";
String APPLY = API_PREFIX + "/apply";
@PostMapping(APPLY)
FR<Boolean> apply(@RequestBody MkProcessSyncDTO param);
}
@@ -0,0 +1,33 @@
package org.springblade.transport.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 客商流程当前节点同步参数
*/
@Data
@Schema(description = "客商流程当前节点同步参数")
public class CustomerProcessNodeSyncDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "客商ID")
private Long id;
@Schema(description = "当前节点")
private String currentNode;
@Schema(description = "当前处理人")
private String currentProcessor;
@Schema(description = "审批状态")
private String approvalStatus;
@Schema(description = "MK 流程实例详情,传入后按 currentHandlers.fdName 和 handlerInfos.nodeName 回写")
private Object processInfo;
}
@@ -0,0 +1,33 @@
package org.springblade.transport.pojo.dto;
import io.swagger.v3.oas.annotations.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* MK 流程节点同步参数
*/
@Data
@Schema(description = "MK 流程节点同步参数")
public class MkProcessSyncDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "业务类型,如 project-apply")
private String bizType;
@Schema(description = "业务主键")
private Long id;
@Schema(description = "动作:sync / approve / reject")
private String action;
@Schema(description = "处理人姓名")
private String processorName;
@Schema(description = "MK 流程实例详情")
private Object processInfo;
}
+4
View File
@@ -62,6 +62,10 @@
<groupId>org.springblade</groupId> <groupId>org.springblade</groupId>
<artifactId>blade-mk-api</artifactId> <artifactId>blade-mk-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-transport-api</artifactId>
</dependency>
<!-- 其他依赖 --> <!-- 其他依赖 -->
<dependency> <dependency>
@@ -60,6 +60,12 @@ public class BusinessProcessController extends BladeController {
return R.data(businessProcessService.processSubmit(param)); return R.data(businessProcessService.processSubmit(param));
} }
@PostMapping("/processDelete")
@ApiOperationSupport(order = 7)
public R<Boolean> processDelete(@RequestBody MKProcessCreateDTO param) {
return R.data(businessProcessService.processDelete(param == null ? null : param.getFormInstanceId()));
}
@GetMapping("/isEditView") @GetMapping("/isEditView")
@ApiOperationSupport(order = 3) @ApiOperationSupport(order = 3)
@Operation(summary = "是否编辑页", description = "传入业务id") @Operation(summary = "是否编辑页", description = "传入业务id")
@@ -86,4 +86,12 @@ public class BusinessProcessClient implements IBusinessProcessClient {
@RequestParam(value = "loginName", required = false) String loginName) { @RequestParam(value = "loginName", required = false) String loginName) {
return FR.data(businessProcessService.getCurrentNodes(processInstanceId, loginName)); return FR.data(businessProcessService.getCurrentNodes(processInstanceId, loginName));
} }
@PreAuth(AuthConstant.PERMIT_ALL)
@GetMapping(GET_PROCESS_INFO)
@Override
public FR<Object> getProcessInfo(@RequestParam("processInstanceId") String processInstanceId,
@RequestParam(value = "loginName", required = false) String loginName) {
return FR.data(businessProcessService.getProcessInfo(processInstanceId, loginName));
}
} }
@@ -34,6 +34,14 @@ public interface IBusinessProcessService extends IService<BusinessProcess> {
*/ */
String processSubmit(MKProcessCreateDTO param); String processSubmit(MKProcessCreateDTO param);
/**
* 按业务表单实例 id 删除 MK 流程(不删除本地业务流程记录,供驳回后重新提交使用)
*
* @param formInstanceId 业务表单实例 id
* @return 是否成功
*/
boolean processDelete(String formInstanceId);
/** /**
* 获取流程当前节点详情 * 获取流程当前节点详情
* *
@@ -43,6 +51,15 @@ public interface IBusinessProcessService extends IService<BusinessProcess> {
*/ */
List<?> getCurrentNodes(String processInstanceId, String loginName); List<?> getCurrentNodes(String processInstanceId, String loginName);
/**
* 获取流程实例详情
*
* @param processInstanceId 流程实例id
* @param loginName MK登录名(手机号),可为空,为空时按流程发起人解析
* @return 流程实例详情
*/
Object getProcessInfo(String processInstanceId, String loginName);
/** /**
* 修改业务流程状态 * 修改业务流程状态
* @param param * @param param
@@ -15,6 +15,7 @@ import org.springblade.process.pojo.enums.ApproveStatusEnum;
import org.springblade.core.log.exception.ServiceException; import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.log.utils.AssertUtils; import org.springblade.core.log.utils.AssertUtils;
import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.FR;
import org.springblade.core.tool.utils.StringUtil; import org.springblade.core.tool.utils.StringUtil;
import org.springblade.process.convert.ApprovalConvert; import org.springblade.process.convert.ApprovalConvert;
import org.springblade.process.convert.BusinessProcessConvert; import org.springblade.process.convert.BusinessProcessConvert;
@@ -41,12 +42,17 @@ import org.springblade.thirdparty.mk.pojo.dto.approval.MKProcessDTO;
import org.springblade.thirdparty.mk.pojo.dto.sort.*; import org.springblade.thirdparty.mk.pojo.dto.sort.*;
import org.springblade.thirdparty.mk.pojo.vo.*; import org.springblade.thirdparty.mk.pojo.vo.*;
import org.springblade.thirdparty.mk.service.IMKService; import org.springblade.thirdparty.mk.service.IMKService;
import org.springblade.transport.feign.ICustomerArchiveClient;
import org.springblade.transport.feign.IMkProcessClient;
import org.springblade.transport.pojo.dto.CustomerProcessNodeSyncDTO;
import org.springblade.transport.pojo.dto.MkProcessSyncDTO;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.*; import java.util.*;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* 业务流程关联表 服务实现类 * 业务流程关联表 服务实现类
@@ -64,6 +70,8 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
private final MKProperties mkProperties; private final MKProperties mkProperties;
private final ApprovalConvert approvalConvert; private final ApprovalConvert approvalConvert;
private final IUserService userService; private final IUserService userService;
private final ICustomerArchiveClient customerArchiveClient;
private final IMkProcessClient mkProcessClient;
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@Override @Override
@@ -128,8 +136,17 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法提交审核流"); AssertUtils.notBlank(loginName, "当前用户手机号为空,无法提交审核流");
param.setSubmitIdentity(loginName); param.setSubmitIdentity(loginName);
param.setLoginName(loginName); param.setLoginName(loginName);
log.info("调用mk processSubmit 参数:{}", JSON.toJSONString(param)); String bizType = param.getBizType();
String processInstanceId = mkService.processSubmit(param); MKProcessCreateDTO mkParam = new MKProcessCreateDTO();
mkParam.setFormInstanceId(param.getFormInstanceId());
mkParam.setSubject(param.getSubject());
mkParam.setSubmitIdentity(loginName);
mkParam.setLoginName(loginName);
mkParam.setTemplateCode(param.getTemplateCode());
mkParam.setFormValues(param.getFormValues());
mkParam.setTempVarData(param.getTempVarData());
log.info("调用mk processSubmit 参数:{}", JSON.toJSONString(mkParam));
String processInstanceId = mkService.processSubmit(mkParam);
AssertUtils.notBlank(processInstanceId, "提交流程失败,未返回流程实例id"); AssertUtils.notBlank(processInstanceId, "提交流程失败,未返回流程实例id");
Long bizId; Long bizId;
@@ -164,9 +181,42 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
businessProcess.setApproveStatus(ApproveStatusEnum.APPROVING.getValue()); businessProcess.setApproveStatus(ApproveStatusEnum.APPROVING.getValue());
this.saveOrUpdate(businessProcess); this.saveOrUpdate(businessProcess);
this.getCurrentNodes(processInstanceId, loginName); this.getCurrentNodes(processInstanceId, loginName);
Object processInfo = this.getProcessInfo(processInstanceId, loginName);
this.syncBizFromProcessInfo(bizType, param.getFormInstanceId(), processInfo);
return processInstanceId; return processInstanceId;
} }
@Transactional(rollbackFor = Exception.class)
@Override
public boolean processDelete(String formInstanceId) {
AssertUtils.notBlank(formInstanceId, "表单实例id不能为空");
Long bizId;
try {
bizId = Long.valueOf(formInstanceId);
} catch (NumberFormatException e) {
throw new ServiceException("表单实例id格式不正确");
}
BusinessProcess businessProcess = this.getOne(Wrappers.<BusinessProcess>lambdaQuery()
.eq(BusinessProcess::getBizId, bizId)
);
if (businessProcess == null || StringUtil.isBlank(businessProcess.getProcessInstanceId())) {
log.warn("客商驳回后删除流程跳过,未找到流程实例 formInstanceId={}", formInstanceId);
return true;
}
String loginName = businessProcess.getPromoterLoginName();
if (StringUtil.isBlank(loginName)) {
loginName = resolveCurrentUserPhone();
}
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法删除审核流");
log.info("调用mk processDelete processInstanceId={} loginName={} formInstanceId={}",
businessProcess.getProcessInstanceId(), loginName, formInstanceId);
boolean deleted = mkService.processDelete(businessProcess.getProcessInstanceId(), loginName);
if (!deleted) {
throw new ServiceException("删除MK流程失败");
}
return true;
}
@Override @Override
public List<?> getCurrentNodes(String processInstanceId, String loginName) { public List<?> getCurrentNodes(String processInstanceId, String loginName) {
if (StringUtils.isBlank(processInstanceId)) { if (StringUtils.isBlank(processInstanceId)) {
@@ -193,6 +243,120 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
} }
} }
@Override
public Object getProcessInfo(String processInstanceId, String loginName) {
if (StringUtils.isBlank(processInstanceId)) {
log.warn("查询流程实例详情失败,processInstanceId为空");
return null;
}
String resolvedLoginName = loginName;
if (StringUtils.isBlank(resolvedLoginName)) {
BusinessProcess businessProcess = this.getBusinessProcessByProcessInstanceId(processInstanceId);
resolvedLoginName = resolvePromoterLoginName(businessProcess, null);
}
if (StringUtils.isBlank(resolvedLoginName)) {
log.warn("查询流程实例详情失败,loginName为空 processInstanceId={}", processInstanceId);
return null;
}
try {
Object processInfo = mkService.getProcessInfo(processInstanceId, resolvedLoginName);
log.info("获取流程实例详情 processInstanceId={} loginName={} result={}",
processInstanceId, resolvedLoginName, JSON.toJSONString(processInfo));
return processInfo;
} catch (Exception e) {
log.error("查询流程实例详情异常 processInstanceId={} loginName={}", processInstanceId, resolvedLoginName, e);
return null;
}
}
private void syncBizFromProcessInfo(String bizType, String formInstanceId, Object processInfo) {
if (StringUtils.isBlank(formInstanceId) || processInfo == null) {
log.warn("同步业务当前节点跳过,formInstanceId或流程实例详情为空 formInstanceId={}", formInstanceId);
return;
}
Long bizId;
try {
bizId = Long.valueOf(formInstanceId);
} catch (NumberFormatException e) {
log.warn("同步业务当前节点失败,表单实例id不是数字:{}", formInstanceId);
return;
}
String resolvedBizType = StringUtils.isBlank(bizType) ? "customer-archive" : bizType;
if ("customer-archive".equals(resolvedBizType)) {
this.syncCustomerArchiveFromProcessInfo(formInstanceId, processInfo);
return;
}
MkProcessSyncDTO param = new MkProcessSyncDTO();
param.setBizType(resolvedBizType);
param.setId(bizId);
param.setAction("sync");
param.setProcessInfo(processInfo);
try {
FR<Boolean> result = mkProcessClient.apply(param);
log.info("同步业务当前节点完成 bizType={} bizId={} result={}",
resolvedBizType, bizId, JSON.toJSONString(result));
} catch (Exception e) {
log.error("同步业务当前节点异常 bizType={} bizId={}", resolvedBizType, bizId, e);
}
}
private void syncCustomerArchiveFromProcessInfo(String formInstanceId, Object processInfo) {
if (StringUtils.isBlank(formInstanceId) || processInfo == null) {
log.warn("同步客商当前节点跳过,formInstanceId或流程实例详情为空 formInstanceId={}", formInstanceId);
return;
}
Long customerId;
try {
customerId = Long.valueOf(formInstanceId);
} catch (NumberFormatException e) {
log.warn("同步客商当前节点失败,表单实例id不是数字:{}", formInstanceId);
return;
}
CustomerProcessNodeSyncDTO param = new CustomerProcessNodeSyncDTO();
param.setId(customerId);
param.setProcessInfo(processInfo);
param.setApprovalStatus("reviewing");
try {
FR<Boolean> result = customerArchiveClient.syncProcessNode(param);
log.info("同步客商当前节点完成 customerId={} result={}", customerId, JSON.toJSONString(result));
} catch (Exception e) {
log.error("同步客商当前节点异常 customerId={}", customerId, e);
}
}
private String readMkNodeName(Object node) {
if (node instanceof MKNodeVO vo) {
return vo.getNodeName();
}
if (node instanceof Map<?, ?> map) {
Object value = map.get("nodeName");
return value == null ? null : String.valueOf(value);
}
return null;
}
private Stream<String> readMkHandlerNames(Object node) {
List<MKNodeHandlerVO> handlers = null;
if (node instanceof MKNodeVO vo) {
handlers = vo.getNodeHandlers();
} else if (node instanceof Map<?, ?> map && map.get("nodeHandlers") instanceof List<?> list) {
return list.stream().map(item -> {
if (item instanceof MKNodeHandlerVO handler) {
return handler.getHandlerName();
}
if (item instanceof Map<?, ?> handlerMap) {
Object value = handlerMap.get("handlerName");
return value == null ? null : String.valueOf(value);
}
return null;
});
}
if (CollectionUtil.isEmpty(handlers)) {
return Stream.empty();
}
return handlers.stream().map(MKNodeHandlerVO::getHandlerName);
}
/** /**
* 从当前登录用户实体读取真实手机号(绕过接口返回脱敏) * 从当前登录用户实体读取真实手机号(绕过接口返回脱敏)
*/ */
@@ -43,8 +43,6 @@ import org.springblade.core.tenant.annotation.NonDS;
import org.springblade.core.tool.api.R; import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.system.excel.ImportFailureException;
import org.springblade.system.excel.PortTerminalExcel; import org.springblade.system.excel.PortTerminalExcel;
import org.springblade.system.excel.PortTerminalExportExcel; import org.springblade.system.excel.PortTerminalExportExcel;
import org.springblade.system.excel.PortTerminalImporter; import org.springblade.system.excel.PortTerminalImporter;
@@ -177,25 +175,14 @@ public class PortTerminalController extends BladeController {
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件"); return R.fail("请上传 .xls,.xlsx 标准格式文件");
} }
try { List<PortTerminalExcel> failureList = portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class));
portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class)); if (Func.isNotEmpty(failureList)) {
} catch (ImportFailureException exception) { org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class);
// 全失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
exportFailure(response, exception.getFailureList());
return null; return null;
} }
return R.success("操作成功"); return R.success("操作成功");
} }
/**
* 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。
* <p>
* 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。
*/
private void exportFailure(HttpServletResponse response, List<?> failureList) {
ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class);
}
/** /**
* 导出港口码头主数据 * 导出港口码头主数据
*/ */
@@ -35,6 +35,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
/** /**
* 币种汇率 Excel * 币种汇率 Excel
@@ -61,8 +62,8 @@ public class CurrencyExcel implements Serializable {
@ExcelProperty("汇率") @ExcelProperty("汇率")
private BigDecimal exchangeRate; private BigDecimal exchangeRate;
@ExcelProperty(value = "生效日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) @ExcelProperty("生效日期")
private String effectiveDate; private LocalDate effectiveDate;
@ExcelProperty("状态") @ExcelProperty("状态")
private String statusName; private String statusName;
@@ -70,8 +71,8 @@ public class CurrencyExcel implements Serializable {
@ExcelProperty("来源") @ExcelProperty("来源")
private String dataSource; private String dataSource;
@ExcelProperty(value = "失效日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) @ExcelProperty("失效日期")
private String expiryDate; private LocalDate expiryDate;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -1,60 +0,0 @@
/**
* 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.system.excel;
import org.springblade.core.log.exception.ServiceException;
import java.io.Serial;
import java.util.List;
/**
* 导入失败异常,携带失败明细用于导出原表并标注错误。
* <p>
* 批量导入采用「全失败即整批回滚」语义:任一行校验失败都会抛出本异常触发事务回滚,
* 失败明细在抛异常前已收集完毕,因此回滚不影响明细的内容。
*
* @author Chill
*/
public class ImportFailureException extends ServiceException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 失败明细,包含原表全部数据,错误行已标注错误原因。
*/
private final transient List<?> failureList;
public ImportFailureException(List<?> failureList) {
super("导入失败,已回滚全部数据");
this.failureList = failureList;
}
public List<?> getFailureList() {
return failureList;
}
}
@@ -34,6 +34,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date;
/** /**
* UserExcel * UserExcel
@@ -101,7 +102,7 @@ public class UserExcel implements Serializable {
private String postName; private String postName;
@ColumnWidth(20) @ColumnWidth(20)
@ExcelProperty(value = "生日", converter = org.springblade.common.excel.LenientDateStringConverter.class) @ExcelProperty("生日")
private String birthday; private Date birthday;
} }
@@ -95,7 +95,7 @@
</select> </select>
<select id="exportUser" resultType="org.springblade.system.excel.UserExcel"> <select id="exportUser" resultType="org.springblade.system.excel.UserExcel">
SELECT id, tenant_id, user_type, account, name, real_name, email, phone, DATE_FORMAT(birthday, '%Y-%m-%d') AS birthday, role_id, dept_id, post_id FROM blade_user ${ew.customSqlSegment} SELECT id, tenant_id, user_type, account, name, real_name, email, phone, birthday, role_id, dept_id, post_id FROM blade_user ${ew.customSqlSegment}
</select> </select>
<select id="selectCustomerOptions" resultType="java.util.HashMap"> <select id="selectCustomerOptions" resultType="java.util.HashMap">
@@ -125,8 +125,6 @@ public class CurrencyServiceImpl extends BaseServiceImpl<CurrencyMapper, Currenc
CurrencyExcel excel = data.get(index); CurrencyExcel excel = data.get(index);
try { try {
Currency currency = Objects.requireNonNull(BeanUtil.copyProperties(excel, Currency.class)); Currency currency = Objects.requireNonNull(BeanUtil.copyProperties(excel, Currency.class));
currency.setEffectiveDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEffectiveDate(), "生效日期"));
currency.setExpiryDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getExpiryDate(), "失效日期"));
currency.setDataSource(SOURCE_BATCH); currency.setDataSource(SOURCE_BATCH);
currency.setStatus(STATUS_ENABLED); currency.setStatus(STATUS_ENABLED);
prepare(currency, SOURCE_BATCH); prepare(currency, SOURCE_BATCH);
@@ -35,7 +35,6 @@ import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache; import org.springblade.system.cache.UserCache;
import org.springblade.system.excel.ImportFailureException;
import org.springblade.system.excel.PortTerminalExcel; import org.springblade.system.excel.PortTerminalExcel;
import org.springblade.system.excel.PortTerminalExportExcel; import org.springblade.system.excel.PortTerminalExportExcel;
import org.springblade.system.mapper.PortTerminalMapper; import org.springblade.system.mapper.PortTerminalMapper;
@@ -46,19 +45,17 @@ import org.springblade.system.service.IPortTerminalService;
import org.springblade.system.service.IRegionService; import org.springblade.system.service.IRegionService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import java.io.Serial;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.TreeMap;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
@@ -72,10 +69,12 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
private static final String CATEGORY_PORT = "港口"; private static final String CATEGORY_PORT = "港口";
private static final String CATEGORY_TERMINAL = "码头"; private static final String CATEGORY_TERMINAL = "码头";
private static final String SOURCE_INITIAL = "初始化"; private static final String SOURCE_INITIAL = "初始化";
private static final String SOURCE_INITIAL_IMPORT = "初始化导入";
private static final String SOURCE_INITIAL_OLD = "初始导入"; private static final String SOURCE_INITIAL_OLD = "初始导入";
private static final String SOURCE_BATCH = "批量导入"; private static final String SOURCE_BATCH = "批量导入";
private static final String SOURCE_MANUAL = "工导"; private static final String SOURCE_MANUAL = "动录";
private static final String SOURCE_MANUAL_OLD = "手工导入";
private static final int STATUS_ENABLED = 1; private static final int STATUS_ENABLED = 1;
private static final int STATUS_DISABLED = 2; private static final int STATUS_DISABLED = 2;
private static final int CODE_MAX_LENGTH = 30; private static final int CODE_MAX_LENGTH = 30;
@@ -142,209 +141,270 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
if (Func.isEmpty(data)) { if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空"); throw new ServiceException("导入数据不能为空");
} }
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。 Map<Integer, PortTerminalExcel> errorMap = new TreeMap<>();
List<PortTerminalExcel> failureList = new ArrayList<>(); Map<String, Integer> codeCountMap = buildImportCodeCountMap(data);
// 批内已占用的编码,用于识别文件内重复数据。 importPortTerminalByCategory(data, codeCountMap, errorMap, true);
Map<String, Integer> occupiedCodeMap = new HashMap<>(); importPortTerminalByCategory(data, codeCountMap, errorMap, false);
// 本文件声明的一级数据(港口)编码集合。 if (Func.isNotEmpty(errorMap)) {
// 用于区分两种情况:父港口"漏填"与"父行自身校验失败"。 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
// 后者说明父行已被标红、用户只需改那一行,因此不再连带给码头行报错。
Set<String> declaredPortCodeSet = new HashSet<>();
// 两阶段导入:先落库一级数据(港口),再落库二级数据(码头),消除对 Excel 行序的依赖。
Map<String, PortTerminal> batchPortMap = new LinkedHashMap<>();
List<Integer> portIndexList = new ArrayList<>();
List<Integer> terminalIndexList = new ArrayList<>();
splitByCategory(data, portIndexList, terminalIndexList, occupiedCodeMap, declaredPortCodeSet, failureList);
importPorts(data, portIndexList, batchPortMap, failureList);
importTerminals(data, terminalIndexList, batchPortMap, declaredPortCodeSet, failureList);
if (Func.isNotEmpty(failureList)) {
// 抛出携带失败明细的异常,触发事务回滚。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new ImportFailureException(data);
} }
return failureList; return new ArrayList<>(errorMap.values());
} }
/** private void importPortTerminalByCategory(List<PortTerminalExcel> data, Map<String, Integer> codeCountMap,
* 按类型拆分数据行,并完成与阶段无关的基础校验(类型、编码格式、批内重复)。 Map<Integer, PortTerminalExcel> errorMap, boolean importPort) {
*
* @param data 导入数据
* @param portIndexList 港口行下标
* @param terminalIndexList 码头行下标
* @param occupiedCodeMap 编码占用情况,值为首次出现的下标
* @param declaredPortCodeSet 本文件声明的港口编码(含编码格式不合法的行)
* @param failureList 失败明细
*/
private void splitByCategory(List<PortTerminalExcel> data, List<Integer> portIndexList, List<Integer> terminalIndexList,
Map<String, Integer> occupiedCodeMap, Set<String> declaredPortCodeSet,
List<PortTerminalExcel> failureList) {
for (int index = 0; index < data.size(); index++) { for (int index = 0; index < data.size(); index++) {
PortTerminalExcel excel = data.get(index); PortTerminalExcel excel = data.get(index);
String category = trimToEmpty(excel.getCategory()); boolean isPort = CATEGORY_PORT.equals(trimToEmpty(excel.getCategory()));
boolean categoryValid = CATEGORY_PORT.equals(category) || CATEGORY_TERMINAL.equals(category); if (isPort != importPort) {
if (!categoryValid) {
failureList.add(buildFailure(data, index, "类型只能为港口或码头"));
continue; continue;
} }
// 码头行的「港口编码」既是编号前缀,也是"所属港口"的来源(见 importTerminals)。 PortTerminal portTerminal = buildImportPortTerminal(excel);
// 缺失时下游解析不到父港口,而该异常又会被"父行已声明"的抑制逻辑吞掉, portTerminal.setDataSource(SOURCE_BATCH);
// 导致码头被静默丢弃、接口却返回成功——所以必须在这里明确拦下。 portTerminal.setStatus(STATUS_ENABLED);
if (CATEGORY_TERMINAL.equals(category) && Func.isEmpty(trimToEmpty(excel.getPortCode()))) { normalizeImportPortTerminal(portTerminal);
failureList.add(buildFailure(data, index, "码头必须填写所属港口编码")); List<String> validationErrors = validateImportPortTerminal(excel, portTerminal, codeCountMap);
if (Func.isNotEmpty(validationErrors)) {
excel.setErrorMessage(formatImportErrorMessage(validationErrors));
errorMap.put(index, excel);
continue; continue;
} }
// 只要类型是港口就登记为"本文件已声明",即使它的编码格式不合法:
// 这样引用它的码头行不会被连带报错,用户只需修正这一个港口行。
if (CATEGORY_PORT.equals(category)) {
String declaredCode = resolveImportCode(excel);
if (Func.isNotEmpty(declaredCode)) {
declaredPortCodeSet.add(declaredCode);
}
}
String code = resolveImportCode(excel);
boolean codeValid = CATEGORY_PORT.equals(category)
? PORT_CODE_PATTERN.matcher(code).matches()
: TERMINAL_CODE_PATTERN.matcher(code).matches();
if (!codeValid) {
String message = CATEGORY_PORT.equals(category) ? "港口编码为5位大写字母" : "码头编码格式为港口编码-码头标识";
failureList.add(buildFailure(data, index, message));
continue;
}
// 批内重复:两行都需要标记,由用户决定保留哪一行。
if (occupiedCodeMap.containsKey(code)) {
markFailure(data, occupiedCodeMap.get(code), "编码 " + code + " 在文件中重复出现");
failureList.add(buildFailure(data, index, "编码 " + code + " 在文件中重复出现"));
continue;
}
occupiedCodeMap.put(code, index);
if (CATEGORY_PORT.equals(category)) {
portIndexList.add(index);
} else {
terminalIndexList.add(index);
}
}
}
/**
* 阶段一:导入港口(一级数据),同时登记到批次内存映射,供码头引用。
*/
private void importPorts(List<PortTerminalExcel> data, List<Integer> portIndexList, Map<String, PortTerminal> batchPortMap,
List<PortTerminalExcel> failureList) {
for (Integer index : portIndexList) {
PortTerminalExcel excel = data.get(index);
try { try {
PortTerminal portTerminal = Objects.requireNonNull(BeanUtil.copyProperties(excel, PortTerminal.class));
// 导入模板按"港口编码/码头编码"两列填写,需归并为实体编码。
portTerminal.setCode(resolveImportCode(excel));
portTerminal.setDataSource(SOURCE_BATCH);
portTerminal.setStatus(STATUS_ENABLED);
prepare(portTerminal, SOURCE_BATCH); prepare(portTerminal, SOURCE_BATCH);
validate(portTerminal); validate(portTerminal);
save(portTerminal); prepareImportTarget(portTerminal);
batchPortMap.put(portTerminal.getCode(), portTerminal); if (!saveOrUpdate(portTerminal)) {
} catch (Exception exception) { throw new ServiceException("港口码头保存失败");
failureList.add(buildFailure(data, index, resolveMessage(exception)));
}
}
}
/**
* 阶段二:导入码头(二级数据),上级港口优先取本批次新增的港口,其次回查数据库。
*
* @param declaredPortCodeSet 本文件声明的港口编码,用于避免连带误报
*/
private void importTerminals(List<PortTerminalExcel> data, List<Integer> terminalIndexList, Map<String, PortTerminal> batchPortMap,
Set<String> declaredPortCodeSet, List<PortTerminalExcel> failureList) {
for (Integer index : terminalIndexList) {
PortTerminalExcel excel = data.get(index);
try {
PortTerminal portTerminal = Objects.requireNonNull(BeanUtil.copyProperties(excel, PortTerminal.class));
// 导入模板按"港口编码/码头编码"两列填写,需归并为实体编码;上级港口取本行的港口编码列。
portTerminal.setCode(resolveImportCode(excel));
portTerminal.setParentCode(trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT));
portTerminal.setDataSource(SOURCE_BATCH);
portTerminal.setStatus(STATUS_ENABLED);
prepareTerminal(portTerminal, batchPortMap);
validate(portTerminal);
save(portTerminal);
} catch (ParentPortNotFoundException exception) {
// 上级港口就声明在本文件里,只是那一行自己校验失败(已被标红)。
// 此时码头行本身没有问题,不再连带报错,避免用户看到"两行都错"的假象。
// 注意这里必须与上面 setParentCode 取同一列(港口编码);
// 若改用「上级港口编码」,父港口取不到时会被误判为"已声明"而吞掉异常。
if (!declaredPortCodeSet.contains(trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT))) {
failureList.add(buildFailure(data, index, resolveMessage(exception)));
} }
} catch (Exception exception) { } catch (Exception exception) {
failureList.add(buildFailure(data, index, resolveMessage(exception))); String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败";
excel.setErrorMessage(formatImportErrorMessage(List.of(message)));
errorMap.put(index, excel);
} }
} }
} }
/** private void prepareImportTarget(PortTerminal portTerminal) {
* 码头导入:上级港口优先匹配本批次新增的港口,其次由 prepare 回查数据库。 if (Func.isNotEmpty(portTerminal.getId())) {
*/
private void prepareTerminal(PortTerminal portTerminal, Map<String, PortTerminal> batchPortMap) {
String parentCode = trimToEmpty(portTerminal.getParentCode()).toUpperCase(Locale.ROOT);
PortTerminal parent = Func.isEmpty(parentCode) ? null : batchPortMap.get(parentCode);
if (Func.isNotEmpty(parent)) {
applyParent(portTerminal, parent);
// 父信息已由本批次港口回填,无需再回查数据库。
prepare(portTerminal, SOURCE_BATCH, true);
return; return;
} }
portTerminal.setParentCode(parentCode); PortTerminal existingPortTerminal = baseMapper.selectByCodeIncludingDeleted(portTerminal.getCode());
prepare(portTerminal, SOURCE_BATCH); if (existingPortTerminal == null) {
return;
}
if (!Objects.equals(existingPortTerminal.getIsDeleted(), 1)) {
throw new ServiceException("该编码已存在");
}
baseMapper.restoreById(existingPortTerminal.getId());
portTerminal.setId(existingPortTerminal.getId());
portTerminal.setIsDeleted(0);
} }
/** private String formatImportErrorMessage(List<String> validationErrors) {
* 将上级港口信息回填到码头,保证码头不会出现没有港口的数据。 StringBuilder errorMessage = new StringBuilder();
*/ for (int index = 0; index < validationErrors.size(); index++) {
private void applyParent(PortTerminal portTerminal, PortTerminal parent) { if (index > 0) {
portTerminal.setParentId(parent.getId()); errorMessage.append(System.lineSeparator());
portTerminal.setParentCode(parent.getCode()); }
portTerminal.setParentName(parent.getName()); errorMessage.append(index + 1).append(". ").append(validationErrors.get(index));
// 与回查数据库保持一致:父港口区域信息为空时保留码头自身填写的值。
if (Func.isNotEmpty(parent.getCountry())) {
portTerminal.setCountry(parent.getCountry());
} }
if (Func.isNotEmpty(parent.getCity())) { return errorMessage.toString();
portTerminal.setCity(parent.getCity()); }
private Map<String, Integer> buildImportCodeCountMap(List<PortTerminalExcel> data) {
Map<String, Integer> codeCountMap = new HashMap<>();
for (PortTerminalExcel excel : data) {
String code = resolveImportCode(excel);
if (Func.isNotEmpty(code)) {
codeCountMap.merge(code, 1, Integer::sum);
}
} }
if (Func.isNotEmpty(parent.getDistrictCode())) { return codeCountMap;
portTerminal.setDistrictCode(parent.getDistrictCode()); }
portTerminal.setRegionCode(parent.getDistrictCode());
} else if (Func.isNotEmpty(portTerminal.getDistrictCode())) { private PortTerminal buildImportPortTerminal(PortTerminalExcel excel) {
portTerminal.setRegionCode(portTerminal.getDistrictCode()); PortTerminal portTerminal = new PortTerminal();
portTerminal.setCode(resolveImportCode(excel));
portTerminal.setName(excel.getName());
portTerminal.setCategory(excel.getCategory());
portTerminal.setParentName(excel.getParentName());
portTerminal.setParentCode(CATEGORY_TERMINAL.equals(trimToEmpty(excel.getCategory())) ? excel.getPortCode() : excel.getParentCode());
portTerminal.setCountry(excel.getCountry());
portTerminal.setProvinceName(excel.getProvinceName());
portTerminal.setCity(excel.getCity());
portTerminal.setDistrictName(excel.getDistrictName());
portTerminal.setDetailAddress(excel.getDetailAddress());
portTerminal.setLongitude(excel.getLongitude());
portTerminal.setLatitude(excel.getLatitude());
portTerminal.setRemark(excel.getRemark());
return portTerminal;
}
private String resolveImportCode(PortTerminalExcel excel) {
String portCode = trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT);
String terminalCode = trimToEmpty(excel.getTerminalCode()).toUpperCase(Locale.ROOT);
if (Func.isEmpty(terminalCode)) {
return portCode;
} }
if (Func.isNotEmpty(parent.getDistrictName())) { return Func.isEmpty(portCode) ? terminalCode : portCode + "-" + terminalCode;
portTerminal.setDistrictName(parent.getDistrictName()); }
private void normalizeImportPortTerminal(PortTerminal portTerminal) {
portTerminal.setCode(trimToEmpty(portTerminal.getCode()).toUpperCase(Locale.ROOT));
portTerminal.setCategory(trimToEmpty(portTerminal.getCategory()));
portTerminal.setName(trimToEmpty(portTerminal.getName()));
portTerminal.setParentCode(trimToNull(portTerminal.getParentCode()));
if (Func.isNotEmpty(portTerminal.getParentCode())) {
portTerminal.setParentCode(portTerminal.getParentCode().toUpperCase(Locale.ROOT));
}
portTerminal.setParentName(trimToNull(portTerminal.getParentName()));
portTerminal.setCountry(trimToEmpty(portTerminal.getCountry()));
portTerminal.setProvinceCode(trimToNull(portTerminal.getProvinceCode()));
portTerminal.setProvinceName(trimToNull(portTerminal.getProvinceName()));
portTerminal.setCity(trimToEmpty(portTerminal.getCity()));
portTerminal.setDistrictCode(trimToNull(portTerminal.getDistrictCode()));
portTerminal.setDistrictName(trimToNull(portTerminal.getDistrictName()));
portTerminal.setRegionCode(trimToNull(portTerminal.getRegionCode()));
if (Func.isEmpty(portTerminal.getDistrictCode()) && Func.isNotEmpty(portTerminal.getRegionCode())) {
portTerminal.setDistrictCode(portTerminal.getRegionCode());
}
portTerminal.setDetailAddress(trimToNull(portTerminal.getDetailAddress()));
portTerminal.setRemark(trimToNull(portTerminal.getRemark()));
}
private List<String> validateImportPortTerminal(PortTerminalExcel excel, PortTerminal portTerminal,
Map<String, Integer> codeCountMap) {
List<String> validationErrors = new ArrayList<>();
if (!CATEGORY_PORT.equals(portTerminal.getCategory()) && !CATEGORY_TERMINAL.equals(portTerminal.getCategory())) {
addValidationError(validationErrors, "类型只能为港口或码头");
}
validateImportCodeColumns(excel, portTerminal, validationErrors);
if (Func.isEmpty(portTerminal.getCode())) {
addValidationError(validationErrors, "编码不能为空");
} else {
validateImportLength(portTerminal.getCode(), CODE_MAX_LENGTH, "编码不能超过30字", validationErrors);
if (CATEGORY_PORT.equals(portTerminal.getCategory()) && !PORT_CODE_PATTERN.matcher(portTerminal.getCode()).matches()) {
addValidationError(validationErrors, "港口编码为5位大写字母");
}
if (CATEGORY_TERMINAL.equals(portTerminal.getCategory()) && !TERMINAL_CODE_PATTERN.matcher(portTerminal.getCode()).matches()) {
addValidationError(validationErrors, "码头编码格式为港口编码-码头标识");
}
if (codeCountMap.getOrDefault(portTerminal.getCode(), 0) > 1) {
addValidationError(validationErrors, "编码在本次导入中重复");
}
if (count(Wrappers.<PortTerminal>lambdaQuery()
.eq(PortTerminal::getCode, portTerminal.getCode())
.eq(PortTerminal::getIsDeleted, 0)) > 0L) {
addValidationError(validationErrors, "该编码已存在");
}
}
if (Func.isEmpty(portTerminal.getName())) {
addValidationError(validationErrors, "港口/码头名称不能为空");
}
validateImportLength(portTerminal.getName(), NAME_MAX_LENGTH, "港口/码头名称不能超过100字", validationErrors);
validateImportLength(portTerminal.getCountry(), REGION_MAX_LENGTH, "国家不能超过50字", validationErrors);
validateImportLength(portTerminal.getProvinceName(), REGION_MAX_LENGTH, "所属省份不能超过50字", validationErrors);
validateImportLength(portTerminal.getCity(), REGION_MAX_LENGTH, "城市不能超过50字", validationErrors);
validateImportLength(portTerminal.getDistrictName(), REGION_MAX_LENGTH, "区县不能超过50字", validationErrors);
validateImportLength(portTerminal.getRegionCode(), REGION_CODE_MAX_LENGTH, "行政区划编码不能超过32字", validationErrors);
if (Func.isEmpty(portTerminal.getDetailAddress())) {
addValidationError(validationErrors, "详细地址不能为空");
}
validateImportLength(portTerminal.getDetailAddress(), DETAIL_ADDRESS_MAX_LENGTH, "详细地址不能超过255字", validationErrors);
validateImportLength(portTerminal.getRemark(), REMARK_MAX_LENGTH, "备注不能超过200个字", validationErrors);
if (Func.isEmpty(portTerminal.getLongitude())) {
addValidationError(validationErrors, "经度不能为空");
} else if (!validRange(portTerminal.getLongitude(), MIN_LONGITUDE, MAX_LONGITUDE)) {
addValidationError(validationErrors, "经度范围为 -180 到 180");
}
if (Func.isEmpty(portTerminal.getLatitude())) {
addValidationError(validationErrors, "纬度不能为空");
} else if (!validRange(portTerminal.getLatitude(), MIN_LATITUDE, MAX_LATITUDE)) {
addValidationError(validationErrors, "纬度范围为 -90 到 90");
}
if (CATEGORY_PORT.equals(portTerminal.getCategory())) {
validateImportPortRegion(portTerminal, validationErrors);
}
if (CATEGORY_TERMINAL.equals(portTerminal.getCategory())) {
validateImportParentPort(portTerminal, validationErrors);
}
return validationErrors;
}
private void validateImportCodeColumns(PortTerminalExcel excel, PortTerminal portTerminal,
List<String> validationErrors) {
String portCode = trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT);
String terminalCode = trimToEmpty(excel.getTerminalCode()).toUpperCase(Locale.ROOT);
String parentCode = trimToEmpty(excel.getParentCode()).toUpperCase(Locale.ROOT);
if (Func.isEmpty(portCode)) {
addValidationError(validationErrors, "港口编码不能为空");
}
if (!CATEGORY_TERMINAL.equals(portTerminal.getCategory())) {
return;
}
if (Func.isEmpty(terminalCode)) {
addValidationError(validationErrors, "码头编码不能为空");
}
if (Func.isEmpty(portTerminal.getParentName())) {
addValidationError(validationErrors, "上级港口不能为空");
}
if (Func.isEmpty(parentCode)) {
addValidationError(validationErrors, "上级港口编码不能为空");
} else if (Func.isNotEmpty(portCode) && !portCode.equals(parentCode) && !portCode.endsWith(parentCode)) {
addValidationError(validationErrors, "港口编码与上级港口编码不匹配");
} }
} }
/** private void validateImportPortRegion(PortTerminal portTerminal, List<String> validationErrors) {
* 构造失败明细,行号按 Excel 中的实际行号(表头占第 1 行)推算。 if (Func.isEmpty(portTerminal.getCountry())) {
*/ addValidationError(validationErrors, "国家不能为空");
private PortTerminalExcel buildFailure(List<PortTerminalExcel> data, int index, String message) { }
PortTerminalExcel excel = data.get(index); if (Func.isEmpty(portTerminal.getCity())) {
markFailure(data, index, message); addValidationError(validationErrors, "城市不能为空");
return excel; }
} if (Func.isEmpty(portTerminal.getProvinceName())) {
addValidationError(validationErrors, "所属省份不能为空");
/** }
* 仅标注错误原因,不重复加入失败明细集合。 if (Func.isEmpty(portTerminal.getDistrictCode()) && Func.isEmpty(portTerminal.getDistrictName())) {
*/ addValidationError(validationErrors, "区县不能为空");
private void markFailure(List<PortTerminalExcel> data, int index, String message) { return;
if (index >= 0 && index < data.size()) { }
data.get(index).setErrorMessage("" + (index + 2) + "行:" + message); try {
fillRegion(portTerminal);
} catch (ServiceException exception) {
addValidationError(validationErrors, exception.getMessage());
} }
} }
/** private void validateImportParentPort(PortTerminal portTerminal, List<String> validationErrors) {
* 解析异常信息,非业务异常统一提示导入失败。 if (Func.isEmpty(portTerminal.getParentCode())) {
*/ addValidationError(validationErrors, "上级港口编码不能为空");
private String resolveMessage(Exception exception) { return;
return exception instanceof ServiceException ? exception.getMessage() : "导入失败"; }
PortTerminal parentPort = getOne(Wrappers.<PortTerminal>lambdaQuery()
.eq(PortTerminal::getCode, portTerminal.getParentCode())
.eq(PortTerminal::getCategory, CATEGORY_PORT)
.eq(PortTerminal::getIsDeleted, 0), false);
if (Func.isEmpty(parentPort)) {
addValidationError(validationErrors, "上级港口编码对应的港口不存在");
} else if (Func.isNotEmpty(portTerminal.getParentName()) && !Objects.equals(portTerminal.getParentName(), parentPort.getName())) {
addValidationError(validationErrors, "上级港口与上级港口编码不匹配");
}
if (Func.isNotEmpty(portTerminal.getCode()) && !portTerminal.getCode().startsWith(portTerminal.getParentCode() + "-")) {
addValidationError(validationErrors, "码头编码必须以上级港口编码开头");
}
}
private void validateImportLength(String value, int maxLength, String message, List<String> validationErrors) {
if (Func.isNotEmpty(value) && value.length() > maxLength) {
addValidationError(validationErrors, message);
}
}
private void addValidationError(List<String> validationErrors, String message) {
if (Func.isNotEmpty(message) && !validationErrors.contains(message)) {
validationErrors.add(message);
}
} }
@Override @Override
@@ -370,21 +430,12 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
} }
private void prepare(PortTerminal portTerminal, String defaultDataSource) { private void prepare(PortTerminal portTerminal, String defaultDataSource) {
prepare(portTerminal, defaultDataSource, false);
}
/**
* 整理并补全港口码头数据。
*
* @param portTerminal 港口码头
* @param defaultDataSource 默认数据来源
* @param parentResolved 上级港口是否已确定(批量导入时由批次内存匹配得到,无需回查数据库)
*/
private void prepare(PortTerminal portTerminal, String defaultDataSource, boolean parentResolved) {
portTerminal.setCode(trimToEmpty(portTerminal.getCode()).toUpperCase(Locale.ROOT)); portTerminal.setCode(trimToEmpty(portTerminal.getCode()).toUpperCase(Locale.ROOT));
portTerminal.setCategory(trimToEmpty(portTerminal.getCategory())); portTerminal.setCategory(trimToEmpty(portTerminal.getCategory()));
portTerminal.setName(trimToEmpty(portTerminal.getName())); portTerminal.setName(trimToEmpty(portTerminal.getName()));
portTerminal.setCountry(trimToEmpty(portTerminal.getCountry())); portTerminal.setCountry(trimToEmpty(portTerminal.getCountry()));
portTerminal.setProvinceCode(trimToNull(portTerminal.getProvinceCode()));
portTerminal.setProvinceName(trimToNull(portTerminal.getProvinceName()));
portTerminal.setCity(trimToEmpty(portTerminal.getCity())); portTerminal.setCity(trimToEmpty(portTerminal.getCity()));
portTerminal.setDistrictCode(trimToNull(portTerminal.getDistrictCode())); portTerminal.setDistrictCode(trimToNull(portTerminal.getDistrictCode()));
portTerminal.setDistrictName(trimToNull(portTerminal.getDistrictName())); portTerminal.setDistrictName(trimToNull(portTerminal.getDistrictName()));
@@ -405,9 +456,6 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
portTerminal.setParentName(null); portTerminal.setParentName(null);
return; return;
} }
if (parentResolved) {
return;
}
fillParentPort(portTerminal); fillParentPort(portTerminal);
} }
@@ -423,9 +471,7 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
.eq(PortTerminal::getIsDeleted, 0)); .eq(PortTerminal::getIsDeleted, 0));
} }
if (Func.isEmpty(parent)) { if (Func.isEmpty(parent)) {
// 用专用异常类型:调用方需要区分"父港口真的漏填"与"父行自身失败" throw new ServiceException("码头必须选择上级港口");
// 后者不应连带给码头行报错(见 importTerminals)。
throw new ParentPortNotFoundException("码头必须选择上级港口");
} }
if (!CATEGORY_PORT.equals(parent.getCategory())) { if (!CATEGORY_PORT.equals(parent.getCategory())) {
throw new ServiceException("上级港口类型不正确"); throw new ServiceException("上级港口类型不正确");
@@ -433,25 +479,31 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
portTerminal.setParentId(parent.getId()); portTerminal.setParentId(parent.getId());
portTerminal.setParentCode(parent.getCode()); portTerminal.setParentCode(parent.getCode());
portTerminal.setParentName(parent.getName()); portTerminal.setParentName(parent.getName());
// 父港口区域信息为空时保留码头自身填写的值,避免历史数据(区县为空)导致码头无法导入。 portTerminal.setCountry(inheritParentValue(parent.getCountry(), portTerminal.getCountry()));
if (Func.isNotEmpty(parent.getCountry())) { portTerminal.setProvinceCode(inheritParentValue(parent.getProvinceCode(), portTerminal.getProvinceCode()));
portTerminal.setCountry(parent.getCountry()); portTerminal.setProvinceName(inheritParentValue(parent.getProvinceName(), portTerminal.getProvinceName()));
} portTerminal.setCity(inheritParentValue(parent.getCity(), portTerminal.getCity()));
if (Func.isNotEmpty(parent.getCity())) { portTerminal.setDistrictCode(inheritParentValue(parent.getDistrictCode(), portTerminal.getDistrictCode()));
portTerminal.setCity(parent.getCity()); portTerminal.setDistrictName(inheritParentValue(parent.getDistrictName(), portTerminal.getDistrictName()));
} portTerminal.setRegionCode(inheritParentValue(parent.getDistrictCode(), portTerminal.getRegionCode()));
if (Func.isNotEmpty(parent.getDistrictCode())) { if (Func.isEmpty(portTerminal.getDistrictCode()) && Func.isNotEmpty(portTerminal.getDistrictName())) {
portTerminal.setDistrictCode(parent.getDistrictCode()); fillRegion(portTerminal);
portTerminal.setRegionCode(parent.getDistrictCode());
} else if (Func.isNotEmpty(portTerminal.getRegionCode())) {
portTerminal.setDistrictCode(portTerminal.getRegionCode());
}
if (Func.isNotEmpty(parent.getDistrictName())) {
portTerminal.setDistrictName(parent.getDistrictName());
} }
} }
private void fillRegion(PortTerminal portTerminal) { private void fillRegion(PortTerminal portTerminal) {
Region selectedProvince = null;
if (Func.isNotEmpty(portTerminal.getProvinceCode())) {
selectedProvince = regionService.getById(portTerminal.getProvinceCode());
}
if (Func.isEmpty(selectedProvince) && Func.isNotEmpty(portTerminal.getProvinceName())) {
selectedProvince = regionService.getOne(Wrappers.<Region>lambdaQuery()
.eq(Region::getName, portTerminal.getProvinceName())
.eq(Region::getRegionLevel, 1), false);
}
if (Func.isEmpty(selectedProvince)) {
throw new ServiceException("请选择所属省份");
}
Region district = null; Region district = null;
if (Func.isNotEmpty(portTerminal.getDistrictCode())) { if (Func.isNotEmpty(portTerminal.getDistrictCode())) {
district = regionService.getById(portTerminal.getDistrictCode()); district = regionService.getById(portTerminal.getDistrictCode());
@@ -460,6 +512,7 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
if (Func.isNotEmpty(portTerminal.getCity())) { if (Func.isNotEmpty(portTerminal.getCity())) {
List<Region> cityList = regionService.list(Wrappers.<Region>lambdaQuery() List<Region> cityList = regionService.list(Wrappers.<Region>lambdaQuery()
.eq(Region::getName, portTerminal.getCity()) .eq(Region::getName, portTerminal.getCity())
.eq(Region::getParentCode, selectedProvince.getCode())
.eq(Region::getRegionLevel, 2)); .eq(Region::getRegionLevel, 2));
for (Region city : cityList) { for (Region city : cityList) {
district = regionService.getOne(Wrappers.<Region>lambdaQuery() district = regionService.getOne(Wrappers.<Region>lambdaQuery()
@@ -483,9 +536,26 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
if (Func.isEmpty(city)) { if (Func.isEmpty(city)) {
throw new ServiceException("区县所属城市不存在"); throw new ServiceException("区县所属城市不存在");
} }
Region province = regionService.getById(city.getParentCode());
if (Func.isEmpty(province)) {
throw new ServiceException("城市所属省份不存在");
}
if (Func.isNotEmpty(portTerminal.getProvinceCode())
&& !Objects.equals(portTerminal.getProvinceCode(), province.getCode())) {
throw new ServiceException("城市与省份不匹配");
}
if (Func.isNotEmpty(portTerminal.getProvinceName())
&& !Objects.equals(portTerminal.getProvinceName(), province.getName())) {
throw new ServiceException("城市与省份不匹配");
}
if (!Objects.equals(selectedProvince.getCode(), province.getCode())) {
throw new ServiceException("城市与省份不匹配");
}
if (Func.isNotEmpty(portTerminal.getCity()) && !Objects.equals(portTerminal.getCity(), city.getName())) { if (Func.isNotEmpty(portTerminal.getCity()) && !Objects.equals(portTerminal.getCity(), city.getName())) {
throw new ServiceException("区县与城市不匹配"); throw new ServiceException("区县与城市不匹配");
} }
portTerminal.setProvinceCode(province.getCode());
portTerminal.setProvinceName(province.getName());
portTerminal.setCity(city.getName()); portTerminal.setCity(city.getName());
portTerminal.setDistrictCode(district.getCode()); portTerminal.setDistrictCode(district.getCode());
portTerminal.setDistrictName(district.getName()); portTerminal.setDistrictName(district.getName());
@@ -505,6 +575,7 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
validateLength(portTerminal.getCode(), CODE_MAX_LENGTH, "编码不能超过30字"); validateLength(portTerminal.getCode(), CODE_MAX_LENGTH, "编码不能超过30字");
validateLength(portTerminal.getName(), NAME_MAX_LENGTH, "港口/码头名称不能超过100字"); validateLength(portTerminal.getName(), NAME_MAX_LENGTH, "港口/码头名称不能超过100字");
validateLength(portTerminal.getCountry(), REGION_MAX_LENGTH, "国家不能超过50字"); validateLength(portTerminal.getCountry(), REGION_MAX_LENGTH, "国家不能超过50字");
validateLength(portTerminal.getProvinceName(), REGION_MAX_LENGTH, "所属省份不能超过50字");
validateLength(portTerminal.getCity(), REGION_MAX_LENGTH, "城市不能超过50字"); validateLength(portTerminal.getCity(), REGION_MAX_LENGTH, "城市不能超过50字");
validateLength(portTerminal.getDistrictName(), REGION_MAX_LENGTH, "区县不能超过50字"); validateLength(portTerminal.getDistrictName(), REGION_MAX_LENGTH, "区县不能超过50字");
validateLength(portTerminal.getRegionCode(), REGION_CODE_MAX_LENGTH, "行政区划编码不能超过32字"); validateLength(portTerminal.getRegionCode(), REGION_CODE_MAX_LENGTH, "行政区划编码不能超过32字");
@@ -525,9 +596,15 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
if (Func.isEmpty(portTerminal.getCity())) { if (Func.isEmpty(portTerminal.getCity())) {
throw new ServiceException("城市不能为空"); throw new ServiceException("城市不能为空");
} }
if (Func.isEmpty(portTerminal.getProvinceCode())) {
throw new ServiceException("请选择所属省份");
}
if (Func.isEmpty(portTerminal.getDistrictCode())) { if (Func.isEmpty(portTerminal.getDistrictCode())) {
throw new ServiceException("区县不能为空"); throw new ServiceException("区县不能为空");
} }
if (Func.isEmpty(portTerminal.getDetailAddress())) {
throw new ServiceException("详细地址不能为空");
}
validateDataSource(portTerminal.getDataSource()); validateDataSource(portTerminal.getDataSource());
validateStatus(portTerminal.getStatus()); validateStatus(portTerminal.getStatus());
validateRange(portTerminal.getLongitude(), MIN_LONGITUDE, MAX_LONGITUDE, "经度范围为 -180 到 180"); validateRange(portTerminal.getLongitude(), MIN_LONGITUDE, MAX_LONGITUDE, "经度范围为 -180 到 180");
@@ -578,21 +655,12 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
return Func.isEmpty(value) || (value.compareTo(min) >= 0 && value.compareTo(max) <= 0); return Func.isEmpty(value) || (value.compareTo(min) >= 0 && value.compareTo(max) <= 0);
} }
/**
* 归并导入行编码:仅填港口编码时取港口编码,填了码头编码时拼接为"港口编码-码头编码"。
*/
private String resolveImportCode(PortTerminalExcel excel) {
String portCode = trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT);
String terminalCode = trimToEmpty(excel.getTerminalCode()).toUpperCase(Locale.ROOT);
if (Func.isEmpty(terminalCode)) {
return portCode;
}
return Func.isEmpty(portCode) ? terminalCode : portCode + "-" + terminalCode;
}
private String normalizeDataSource(String dataSource) { private String normalizeDataSource(String dataSource) {
String value = trimToEmpty(dataSource); String value = trimToEmpty(dataSource);
return SOURCE_INITIAL_OLD.equals(value) ? SOURCE_INITIAL : value; if (SOURCE_INITIAL_IMPORT.equals(value) || SOURCE_INITIAL_OLD.equals(value)) {
return SOURCE_INITIAL;
}
return SOURCE_MANUAL_OLD.equals(value) ? SOURCE_MANUAL : value;
} }
private void validateEnabledTerminal(Long parentId) { private void validateEnabledTerminal(Long parentId) {
@@ -618,23 +686,9 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
return trimValue.isEmpty() ? null : trimValue; return trimValue.isEmpty() ? null : trimValue;
} }
/** private String inheritParentValue(String parentValue, String currentValue) {
* 上级港口找不到时抛出。 String normalizedParentValue = trimToNull(parentValue);
* <p> return normalizedParentValue == null ? currentValue : normalizedParentValue;
* 与普通业务异常区分开,是因为调用方需要判断:这个父港口到底是"用户漏填了",
* 还是"父港口那一行就在本文件里、只是它自己校验失败"。后者不该连带给码头行报错。
*
* @author Chill
*/
private static class ParentPortNotFoundException extends ServiceException {
@Serial
private static final long serialVersionUID = 1L;
ParentPortNotFoundException(String message) {
super(message);
}
} }
} }
@@ -689,9 +689,6 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
*/ */
private User buildImportUser(UserExcel userExcel, String tenantId) { private User buildImportUser(UserExcel userExcel, String tenantId) {
User user = Objects.requireNonNull(BeanUtil.copyProperties(userExcel, User.class)); User user = Objects.requireNonNull(BeanUtil.copyProperties(userExcel, User.class));
// 宽容解析生日文本(2026-8-2 等写法),User.birthday 为 java.util.Date 需转换
java.time.LocalDate birthday = org.springblade.common.excel.LenientDateParser.parseDate(userExcel.getBirthday(), "生日");
user.setBirthday(birthday == null ? null : java.sql.Date.valueOf(birthday));
user.setTenantId(tenantId); user.setTenantId(tenantId);
user.setUserType(Func.toInt(DictCache.getKey(DictEnum.USER_TYPE, userExcel.getUserTypeName()), 1)); user.setUserType(Func.toInt(DictCache.getKey(DictEnum.USER_TYPE, userExcel.getUserTypeName()), 1));
user.setDeptId(Func.toStrWithEmpty(SysCache.getDeptIds(tenantId, userExcel.getDeptName()), StringPool.EMPTY)); user.setDeptId(Func.toStrWithEmpty(SysCache.getDeptIds(tenantId, userExcel.getDeptName()), StringPool.EMPTY));
@@ -6,20 +6,14 @@ package org.springblade.transport.config;
import io.minio.MinioClient; import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
/** /**
* 凭证图片 MinIO 客户端配置。 * 凭证图片 MinIO 客户端配置。
* <p>
* 连接参数由 Nacos 的 file.storage.minio 配置提供。 * 连接参数由 Nacos 的 file.storage.minio 配置提供。
* 未配置 {@code file.storage.minio.endpoint} 时不注册该客户端,
* 以免凭证上传功能缺失配置导致整个 blade-transport 服务无法启动;
* 此时凭证相关接口会在调用时给出明确提示,而非启动即失败。
*/ */
@Configuration @Configuration
@ConditionalOnProperty(prefix = "file.storage.minio", name = "endpoint")
public class VoucherMinioConfig { public class VoucherMinioConfig {
@Bean @Bean
@@ -35,13 +35,11 @@ import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.constant.AuthConstant; import org.springblade.core.secure.constant.AuthConstant;
import org.springblade.core.tenant.annotation.TenantIgnore; import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.FR;
import org.springblade.core.tool.api.R; import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.process.feign.IBusinessProcessClient;
import org.springblade.transport.pojo.vo.CustomerArchiveVO; import org.springblade.transport.pojo.vo.CustomerArchiveVO;
import org.springblade.transport.pojo.vo.CustomerChangeRecordVO; import org.springblade.transport.pojo.vo.CustomerChangeRecordVO;
import org.springblade.transport.service.ICustomerArchiveService; import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.impl.CustomerArchivePublicProcessService;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@@ -49,8 +47,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
@@ -68,7 +64,7 @@ import java.util.Map;
public class CustomerArchivePublicController { public class CustomerArchivePublicController {
private final ICustomerArchiveService customerArchiveService; private final ICustomerArchiveService customerArchiveService;
private final IBusinessProcessClient businessProcessClient; private final CustomerArchivePublicProcessService customerArchivePublicProcessService;
/** /**
* 公开详情 * 公开详情
@@ -92,61 +88,15 @@ public class CustomerArchivePublicController {
} }
/** /**
* 公开接收流程页 postMessage 数据(当前仅打印,便于联调) * 公开接收流程页 postMessage 数据
*/ */
@PostMapping("/process-message") @PostMapping("/process-message")
@ApiOperationSupport(order = 3) @ApiOperationSupport(order = 3)
@Operation(summary = "公开接收流程消息", description = "无需登录,接收后查询当前节点并打印") @Operation(summary = "公开接收流程消息", description = "无需登录,立即查询当前节点5秒后查询流程实例详情并同步客商")
public R processMessage(@RequestBody Map<String, Object> body) { public R processMessage(@RequestBody Map<String, Object> body) {
log.info("客商公开页收到流程消息:{}", JSON.toJSONString(body)); log.info("客商公开页收到流程消息:{}", JSON.toJSONString(body));
Map<String, Object> formValues = asMap(body == null ? null : body.get("formValues")); customerArchivePublicProcessService.handleProcessMessage(body);
String processId = firstText(formValues, "processId");
if (StringUtil.isBlank(processId) && body != null) {
processId = firstText(body, "processId");
}
String loginName = firstText(formValues, "mkLoginName", "loginName");
if (StringUtil.isBlank(processId)) {
log.warn("客商公开页流程消息未找到 processId,跳过查询当前节点");
return R.success("ok");
}
try {
FR<Object> result = businessProcessClient.getCurrentNodes(processId, loginName);
log.info("客商公开页流程消息当前节点详情 processId={} loginName={} result={}",
processId, loginName, JSON.toJSONString(result == null ? null : result.getData()));
} catch (Exception e) {
log.error("客商公开页查询当前节点失败 processId={} loginName={}", processId, loginName, e);
}
return R.success("ok"); return R.success("ok");
} }
private static Map<String, Object> asMap(Object value) {
if (!(value instanceof Map<?, ?> map)) {
return Collections.emptyMap();
}
Map<String, Object> result = new HashMap<>();
map.forEach((key, nested) -> {
if (key != null) {
result.put(String.valueOf(key), nested);
}
});
return result;
}
private static String firstText(Map<String, Object> source, String... keys) {
if (source == null || keys == null) {
return null;
}
for (String key : keys) {
Object value = source.get(key);
if (value == null) {
continue;
}
String text = String.valueOf(value).trim();
if (StringUtil.isNotBlank(text) && !"null".equalsIgnoreCase(text)) {
return text;
}
}
return null;
}
} }
@@ -0,0 +1,55 @@
package org.springblade.transport.controller;
import com.alibaba.fastjson2.JSON;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.constant.AuthConstant;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.R;
import org.springblade.transport.mk.MkProcessMessageService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* MK 业务流程公开查看
*/
@Slf4j
@RestController
@AllArgsConstructor
@TenantIgnore
@PreAuth(AuthConstant.PERMIT_ALL)
@RequestMapping("/mk-process/public")
@Tag(name = "MK业务流程公开查看", description = "MK业务流程公开查看")
public class MkProcessPublicController {
private final MkProcessMessageService mkProcessMessageService;
@GetMapping("/{bizType}/detail")
@ApiOperationSupport(order = 1)
@Operation(summary = "公开详情", description = "无需登录")
public R detail(@PathVariable String bizType,
@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.data(mkProcessMessageService.handler(bizType).publicDetail(id));
}
@PostMapping("/{bizType}/process-message")
@ApiOperationSupport(order = 2)
@Operation(summary = "公开接收流程消息", description = "无需登录,立即查询当前节点,5秒后查询流程实例详情并同步业务")
public R processMessage(@PathVariable String bizType, @RequestBody Map<String, Object> body) {
log.info("公开页收到流程消息 bizType={} body={}", bizType, JSON.toJSONString(body));
mkProcessMessageService.handleProcessMessage(bizType, body);
return R.success("ok");
}
}
@@ -52,7 +52,6 @@ import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.ProcessConfigVO; import org.springblade.transport.pojo.vo.ProcessConfigVO;
import org.springblade.transport.service.IProcessConfigService; import org.springblade.transport.service.IProcessConfigService;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -84,34 +83,20 @@ public class ProcessConfigController extends BladeController {
private final VoucherFileMapper voucherFileMapper; private final VoucherFileMapper voucherFileMapper;
private final VoucherImageMapper voucherImageMapper; private final VoucherImageMapper voucherImageMapper;
private final VoucherManageMapper voucherManageMapper; private final VoucherManageMapper voucherManageMapper;
private final ObjectProvider<MinioClient> minioClientProvider; private final MinioClient minioClient;
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") @Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
private String minioBucketName; private String minioBucketName;
public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper, public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper,
VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper, VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper,
VoucherManageMapper voucherManageMapper, VoucherManageMapper voucherManageMapper,
ObjectProvider<MinioClient> minioClientProvider) { MinioClient minioClient) {
this.processConfigService = processConfigService; this.processConfigService = processConfigService;
this.waybillMapper = waybillMapper; this.waybillMapper = waybillMapper;
this.voucherFileMapper = voucherFileMapper; this.voucherFileMapper = voucherFileMapper;
this.voucherImageMapper = voucherImageMapper; this.voucherImageMapper = voucherImageMapper;
this.voucherManageMapper = voucherManageMapper; this.voucherManageMapper = voucherManageMapper;
this.minioClientProvider = minioClientProvider; this.minioClient = minioClient;
}
/**
* 获取 MinIO 客户端。
* <p>
* 未配置 file.storage.minio.endpoint 时该客户端不会被注册,
* 此时凭证预览地址无法生成,抛出明确提示而非启动即失败。
*/
private MinioClient minioClient() {
MinioClient minioClient = minioClientProvider.getIfAvailable();
if (minioClient == null) {
throw new IllegalStateException("Nacos 未配置 file.storage.minio.endpoint,凭证文件功能不可用");
}
return minioClient;
} }
@GetMapping("/detail") @GetMapping("/detail")
@@ -162,7 +147,7 @@ public class ProcessConfigController extends BladeController {
result.put("waybillNo", image.getWaybillNo()); result.put("waybillNo", image.getWaybillNo());
result.put("objectKey", image.getObjectKey()); result.put("objectKey", image.getObjectKey());
try { try {
result.put("url", minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET).bucket(minioBucketName).object(image.getObjectKey()) .method(Method.GET).bucket(minioBucketName).object(image.getObjectKey())
.expiry(1, TimeUnit.HOURS).build())); .expiry(1, TimeUnit.HOURS).build()));
} catch (Exception e) { } catch (Exception e) {
@@ -285,7 +270,7 @@ public class ProcessConfigController extends BladeController {
result.put("objectKey", image.objectKey()); result.put("objectKey", image.objectKey());
result.put("matched", image.matched()); result.put("matched", image.matched());
try { try {
result.put("url", minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET).bucket(minioBucketName).object(image.objectKey()) .method(Method.GET).bucket(minioBucketName).object(image.objectKey())
.expiry(1, TimeUnit.HOURS).build())); .expiry(1, TimeUnit.HOURS).build()));
} catch (Exception exception) { } catch (Exception exception) {
@@ -105,7 +105,7 @@ public class ProjectApplyController extends BladeController {
@ApiOperationSupport(order = 4) @ApiOperationSupport(order = 4)
@Operation(summary = "新增或修改", description = "传入projectApply") @Operation(summary = "新增或修改", description = "传入projectApply")
public R submit(@RequestBody ProjectApply projectApply) { public R submit(@RequestBody ProjectApply projectApply) {
return R.status(projectApplyService.submit(projectApply)); return projectApplyService.submit(projectApply) ? R.data(projectApply) : R.fail("保存失败");
} }
@PostMapping("/submit-approval") @PostMapping("/submit-approval")
@@ -213,7 +213,7 @@ public class WaybillController extends BladeController {
@ApiOperationSupport(order = 9) @ApiOperationSupport(order = 9)
@Operation(summary = "新增或修改", description = "传入waybill") @Operation(summary = "新增或修改", description = "传入waybill")
public R submit(@RequestBody Waybill waybill) { public R submit(@RequestBody Waybill waybill) {
return R.status(waybillService.submit(waybill)); return waybillService.submit(waybill) ? R.data(waybill) : R.fail("保存失败");
} }
@PostMapping("/save-draft") @PostMapping("/save-draft")
@@ -36,6 +36,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
/** /**
* 事故记录 Excel * 事故记录 Excel
@@ -59,8 +60,8 @@ public class AccidentRecordExcel implements Serializable {
@ExcelProperty("*车牌号/船号") @ExcelProperty("*车牌号/船号")
private String vehicleNo; private String vehicleNo;
@ExcelProperty(value = "*事故发生日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) @ExcelProperty("*事故发生日期")
private String accidentDate; private LocalDate accidentDate;
@ExcelProperty("事故发生地点") @ExcelProperty("事故发生地点")
private String accidentLocation; private String accidentLocation;
@@ -37,7 +37,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Date; import java.time.LocalDateTime;
/** /**
* 事故记录导出 Excel * 事故记录导出 Excel
@@ -83,14 +83,14 @@ public class AccidentRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private LocalDateTime createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private LocalDateTime updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -35,6 +35,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
/** /**
* 年检记录 Excel * 年检记录 Excel
@@ -58,11 +59,11 @@ public class AnnualInspectionRecordExcel implements Serializable {
@ExcelProperty("*车牌号/船号") @ExcelProperty("*车牌号/船号")
private String vehicleNo; private String vehicleNo;
@ExcelProperty(value = "*检测评定日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) @ExcelProperty("*检测评定日期")
private String inspectionAssessmentDate; private LocalDate inspectionAssessmentDate;
@ExcelProperty(value = "*有效期截止日", converter = org.springblade.common.excel.LenientDateStringConverter.class) @ExcelProperty("*有效期截止日")
private String validUntilDate; private LocalDate validUntilDate;
@ExcelProperty("*车辆技术等级") @ExcelProperty("*车辆技术等级")
private String vehicleTechnicalLevel; private String vehicleTechnicalLevel;
@@ -36,7 +36,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Date; import java.time.LocalDateTime;
/** /**
* 年检记录导出 Excel * 年检记录导出 Excel
@@ -83,14 +83,14 @@ public class AnnualInspectionRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private LocalDateTime createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private LocalDateTime updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -24,7 +24,6 @@ package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelIgnore; import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -89,11 +88,9 @@ public class CommonAddressExportExcel implements Serializable {
private String remark; private String remark;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private Date updateTime;
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private Date createTime;
} }
@@ -24,7 +24,6 @@
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.format.NumberFormat; import cn.idev.excel.annotation.format.NumberFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
@@ -94,11 +93,9 @@ public class CommonCargoExportExcel implements Serializable {
private String deptName; private String deptName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private Date updateTime;
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private Date createTime;
} }
@@ -23,7 +23,6 @@
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -76,10 +75,8 @@ public class CommonRouteExportExcel implements Serializable {
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private Date updateTime;
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private Date createTime;
} }
@@ -23,7 +23,6 @@
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.format.NumberFormat; import cn.idev.excel.annotation.format.NumberFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
@@ -34,7 +33,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Date; import java.time.LocalDateTime;
/** /**
* 合同管理 Excel * 合同管理 Excel
@@ -112,10 +111,8 @@ public class ContractManageExcel implements Serializable {
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;
private Date updateTime;
} }
@@ -7,10 +7,12 @@ package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelIgnore; import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import lombok.Data; import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDate;
/** /**
* 设备台账 Excel * 设备台账 Excel
@@ -27,7 +29,7 @@ public class EquipmentLedgerExcel implements Serializable {
@ExcelProperty("设备品牌") private String equipmentBrand; @ExcelProperty("设备品牌") private String equipmentBrand;
@ExcelProperty("设备类型") private String equipmentType; @ExcelProperty("设备类型") private String equipmentType;
@ExcelProperty("规格型号") private String specificationModel; @ExcelProperty("规格型号") private String specificationModel;
@ExcelProperty(value = "出厂日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) private String factoryDate; @ExcelProperty("出厂日期") @DateTimeFormat("yyyy-MM-dd") private LocalDate factoryDate;
@ExcelProperty("备注") private String remark; @ExcelProperty("备注") private String remark;
@ExcelIgnore private String errorMessage; @ExcelIgnore private String errorMessage;
} }
@@ -11,7 +11,6 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
/** /**
* ETC记录导出 Excel * ETC记录导出 Excel
@@ -51,14 +50,14 @@ public class EtcRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private LocalDateTime createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private LocalDateTime updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -35,6 +35,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
/** /**
* 保险记录 Excel * 保险记录 Excel
@@ -67,11 +68,11 @@ public class InsuranceRecordExcel implements Serializable {
@ExcelProperty("*保单号") @ExcelProperty("*保单号")
private String policyNo; private String policyNo;
@ExcelProperty(value = "*开始日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) @ExcelProperty("*开始日期")
private String startDate; private LocalDate startDate;
@ExcelProperty(value = "*结束日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) @ExcelProperty("*结束日期")
private String endDate; private LocalDate endDate;
@ExcelProperty("保额") @ExcelProperty("保额")
private BigDecimal insuredAmount; private BigDecimal insuredAmount;
@@ -82,8 +83,8 @@ public class InsuranceRecordExcel implements Serializable {
@ExcelProperty("发票号") @ExcelProperty("发票号")
private String invoiceNo; private String invoiceNo;
@ExcelProperty(value = "开票日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) @ExcelProperty("开票日期")
private String invoiceDate; private LocalDate invoiceDate;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -36,7 +36,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Date; import java.time.LocalDateTime;
/** /**
* 保险记录导出 Excel * 保险记录导出 Excel
@@ -83,14 +83,14 @@ public class InsuranceRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private LocalDateTime createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private LocalDateTime updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -33,20 +33,22 @@ import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty; import cn.idev.excel.metadata.property.ExcelContentProperty;
import cn.idev.excel.util.DateUtils; import cn.idev.excel.util.DateUtils;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
/** /**
* 保养记录日期转换器,兼容日期文本、日期时间文本和 Excel 数值日期。 * 保养记录日期转换器,兼容日期文本、日期时间文本和 Excel 数值日期。
* <p>
* 文本解析委托公共宽容解析器(支持 2026-8-2、2026/8/2 等写法,口径见根工作区
* docs/import-spec.md);无法识别的文本按既有行为抛出转换异常,
* 文案带原值,与「日期格式无法识别」口径一致。
* *
* @author Chill * @author Chill
*/ */
public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime> { public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime> {
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_TIME_MINUTE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT);
@Override @Override
public Class<?> supportJavaTypeKey() { public Class<?> supportJavaTypeKey() {
@@ -69,14 +71,16 @@ public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime
if (value == null || value.trim().isEmpty()) { if (value == null || value.trim().isEmpty()) {
return null; return null;
} }
LocalDateTime parsed = org.springblade.common.excel.LenientDateParser.parseDateTimeOrNull(value); String normalizedValue = value.trim();
if (parsed != null) { try {
return parsed; return LocalDateTime.parse(normalizedValue, DATE_TIME_FORMATTER);
} catch (DateTimeParseException ignored) {
try {
return LocalDateTime.parse(normalizedValue, DATE_TIME_MINUTE_FORMATTER);
} catch (DateTimeParseException ignoredMinute) {
return LocalDate.parse(normalizedValue, DATE_FORMATTER).atStartOfDay();
}
} }
// 无法识别的文本:与既有行为一致抛出转换异常(ExcelDataConvertException 是 RuntimeException
// 会带上本文案一路抛到 Controller,由全局异常处理返回给前端提示)。
throw new cn.idev.excel.exception.ExcelDataConvertException(-1, -1, cellData, contentProperty,
value.trim() + " 日期格式无法识别");
} }
@Override @Override
@@ -11,7 +11,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.io.Serial; import java.io.Serial;
import java.util.Date; import java.time.LocalDateTime;
/** /**
* 车辆保养记录导出 Excel * 车辆保养记录导出 Excel
@@ -26,12 +26,12 @@ public class MaintenancePlanExportExcel extends MaintenancePlanExcel {
@ExcelProperty(value = "创建时间", index = 12) @ExcelProperty(value = "创建时间", index = 12)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private LocalDateTime createTime;
@ExcelProperty(value = "更新人", index = 13) @ExcelProperty(value = "更新人", index = 13)
private String updateUserName; private String updateUserName;
@ExcelProperty(value = "更新时间", index = 14) @ExcelProperty(value = "更新时间", index = 14)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private LocalDateTime updateTime;
} }
@@ -38,7 +38,6 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
/** /**
* 车辆维修记录 Excel * 车辆维修记录 Excel
@@ -96,16 +95,16 @@ public class MaintenanceRecordExcel implements Serializable {
@NumberFormat("0.00") @NumberFormat("0.00")
private BigDecimal mileage; private BigDecimal mileage;
@ExcelProperty("创建时间") @ExcelProperty(value = "创建时间", converter = MaintenancePlanDateTimeConverter.class)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private LocalDateTime createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty(value = "更新时间", converter = MaintenancePlanDateTimeConverter.class)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private LocalDateTime updateTime;
@ExcelProperty("里程单位") @ExcelProperty("里程单位")
private String mileageUnit; private String mileageUnit;
@@ -25,6 +25,7 @@
*/ */
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -41,4 +42,7 @@ public class MaintenanceRecordExportExcel extends MaintenanceRecordExcel {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ExcelProperty(value = "导出失败原因", index = 17)
private String errorMessage;
} }
@@ -36,7 +36,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.time.LocalDateTime;
/** /**
* 里程记录导出 Excel * 里程记录导出 Excel
@@ -72,14 +72,14 @@ public class MileageRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private LocalDateTime createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private LocalDateTime updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -11,7 +11,6 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
/** /**
* 油电记录导出 Excel * 油电记录导出 Excel
@@ -62,14 +61,14 @@ public class OilElectricRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private LocalDateTime createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private LocalDateTime updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -16,6 +16,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
/** /**
* 其他费用记录 Excel * 其他费用记录 Excel
@@ -39,8 +40,8 @@ public class OtherExpenseRecordExcel implements Serializable {
@ExcelProperty("*车牌号/船号") @ExcelProperty("*车牌号/船号")
private String vehicleNo; private String vehicleNo;
@ExcelProperty(value = "*费用日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) @ExcelProperty("*费用日期")
private String expenseDate; private LocalDate expenseDate;
@ExcelProperty("*费用类型") @ExcelProperty("*费用类型")
private String expenseType; private String expenseType;
@@ -11,7 +11,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Date; import java.time.LocalDateTime;
/** /**
* 其他费用记录导出 Excel * 其他费用记录导出 Excel
@@ -43,14 +43,14 @@ public class OtherExpenseRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private LocalDateTime createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private LocalDateTime updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -23,7 +23,6 @@
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -70,10 +69,8 @@ public class ProcessConfigExportExcel implements Serializable {
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private Date updateTime;
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private Date createTime;
} }
@@ -23,7 +23,6 @@
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -34,7 +33,6 @@ import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
/** /**
* 项目立项 Excel * 项目立项 Excel
@@ -125,10 +123,8 @@ public class ProjectApplyExcel implements Serializable {
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;
private Date updateTime;
} }
@@ -23,7 +23,6 @@
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -31,7 +30,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.time.LocalDateTime;
/** /**
* 发货模板 Excel * 发货模板 Excel
@@ -60,10 +59,8 @@ public class ShippingTemplateExcel implements Serializable {
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;
private Date updateTime;
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime;
private Date createTime;
} }
@@ -23,7 +23,6 @@
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -33,7 +32,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Date; import java.time.LocalDateTime;
/** /**
* 临时额度申请 Excel * 临时额度申请 Excel
@@ -82,10 +81,8 @@ public class TemporaryCreditLimitExcel implements Serializable {
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;
private Date updateTime;
} }
@@ -38,10 +38,6 @@ import java.time.format.DateTimeFormatter;
/** /**
* 换胎日期文本转换器,保留非法日期原值并兼容 Excel 数值日期。 * 换胎日期文本转换器,保留非法日期原值并兼容 Excel 数值日期。
* <p>
* 数值日期序列号转 ISO 文本,文本原样透传;文本的宽容解析由服务层委托
* {@link org.springblade.common.excel.LenientDateParser} 完成(支持 2026-8-2 等写法,
* 口径见根工作区 docs/import-spec.md)。
* *
* @author Chill * @author Chill
*/ */
@@ -37,7 +37,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Date; import java.time.LocalDateTime;
/** /**
* 换胎记录导出 Excel * 换胎记录导出 Excel
@@ -76,14 +76,14 @@ public class TireReplacementRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private LocalDateTime createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private LocalDateTime updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -28,7 +28,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.time.LocalDateTime;
/** /**
* 变更记录导出 Excel * 变更记录导出 Excel
@@ -57,14 +57,14 @@ public class TransportChangeRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private LocalDateTime createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private LocalDateTime updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -23,7 +23,6 @@
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -32,7 +31,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Date; import java.time.LocalDateTime;
/** /**
* 运输计划 Excel * 运输计划 Excel
@@ -93,10 +92,8 @@ public class TransportPlanExcel implements Serializable {
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime;
private Date createTime;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;
private Date updateTime;
} }
@@ -37,7 +37,6 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
/** /**
* 违章记录 Excel * 违章记录 Excel
@@ -96,14 +95,14 @@ public class ViolationRecordExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private LocalDateTime createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private LocalDateTime updateTime;
@ExcelIgnore @ExcelIgnore
private String errorMessage; private String errorMessage;
@@ -60,11 +60,8 @@ public class ViolationRecordImportExcel implements Serializable {
@ExcelProperty("*驾驶人") @ExcelProperty("*驾驶人")
private String driverName; private String driverName;
@ExcelProperty("*类型") @ExcelProperty("*类型/事项")
private String violationType; private String violationTypeOrItem;
@ExcelProperty("*事项")
private String violationItem;
@ExcelProperty(value = "*时间", converter = MaintenancePlanDateTimeConverter.class) @ExcelProperty(value = "*时间", converter = MaintenancePlanDateTimeConverter.class)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@@ -108,10 +108,10 @@ public class WaybillExcel implements Serializable {
private String escortPhone; private String escortPhone;
@ExcelProperty("里程(km)") @ExcelProperty("里程(km)")
private BigDecimal mileage; private BigDecimal mileage;
@ExcelProperty(value = "预计发货日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) @ExcelProperty("预计发货日期")
private String estimatedStartTime; private LocalDate estimatedStartTime;
@ExcelProperty(value = "预计完成日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) @ExcelProperty("预计完成日期")
private String estimatedEndTime; private LocalDate estimatedEndTime;
@ExcelProperty("单价") @ExcelProperty("单价")
private BigDecimal unitPrice; private BigDecimal unitPrice;
@ExcelProperty("计价单位") @ExcelProperty("计价单位")
@@ -126,10 +126,10 @@ public class WaybillExcel implements Serializable {
private String businessStatus; private String businessStatus;
@ExcelProperty("数据来源") @ExcelProperty("数据来源")
private String dataSource; private String dataSource;
@ExcelProperty(value = "开始日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) @ExcelProperty("开始日期")
private String startDate; private LocalDate startDate;
@ExcelProperty(value = "结束日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) @ExcelProperty("结束日期")
private String endDate; private LocalDate endDate;
@ExcelProperty("计划名称") @ExcelProperty("计划名称")
private String planName; private String planName;
@ExcelProperty("多联总单") @ExcelProperty("多联总单")
@@ -0,0 +1,39 @@
package org.springblade.transport.feign;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.AllArgsConstructor;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.constant.AuthConstant;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.FR;
import org.springblade.transport.pojo.dto.CustomerProcessNodeSyncDTO;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* 客商档案 Feign实现
*/
@Hidden
@RestController
@AllArgsConstructor
public class CustomerArchiveClient implements ICustomerArchiveClient {
private final ICustomerArchiveService customerArchiveService;
@TenantIgnore
@PreAuth(AuthConstant.PERMIT_ALL)
@PostMapping(SYNC_PROCESS_NODE)
@Override
public FR<Boolean> syncProcessNode(@RequestBody CustomerProcessNodeSyncDTO param) {
if (param == null) {
return FR.data(false);
}
if (param.getProcessInfo() != null) {
return FR.data(customerArchiveService.syncProcessNodeFromProcessInfo(param.getId(), param.getProcessInfo()));
}
return FR.data(customerArchiveService.syncProcessNode(
param.getId(), param.getCurrentNode(), param.getCurrentProcessor(), param.getApprovalStatus()));
}
}
@@ -0,0 +1,42 @@
package org.springblade.transport.feign;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.AllArgsConstructor;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.constant.AuthConstant;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.FR;
import org.springblade.transport.mk.IMkProcessBizHandler;
import org.springblade.transport.mk.MkProcessMessageService;
import org.springblade.transport.pojo.dto.MkProcessSyncDTO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* MK 业务流程同步 Feign 实现
*/
@Hidden
@RestController
@AllArgsConstructor
public class MkProcessClient implements IMkProcessClient {
private final MkProcessMessageService mkProcessMessageService;
@TenantIgnore
@PreAuth(AuthConstant.PERMIT_ALL)
@PostMapping(APPLY)
@Override
public FR<Boolean> apply(@RequestBody MkProcessSyncDTO param) {
if (param == null || param.getId() == null) {
return FR.data(false);
}
IMkProcessBizHandler handler = mkProcessMessageService.handler(param.getBizType());
String action = param.getAction() == null ? "sync" : param.getAction();
return switch (action) {
case "approve" -> FR.data(handler.approveFromProcess(param.getId(), param.getProcessorName()));
case "reject" -> FR.data(handler.rejectFromProcess(param.getId(), param.getProcessorName()));
default -> FR.data(handler.syncFromProcessInfo(param.getId(), param.getProcessInfo()));
};
}
}
@@ -0,0 +1,17 @@
package org.springblade.transport.mk;
/**
* MK 业务流程处理器
*/
public interface IMkProcessBizHandler {
String bizType();
Object publicDetail(Long id);
boolean syncFromProcessInfo(Long id, Object processInfo);
boolean approveFromProcess(Long id, String processorName);
boolean rejectFromProcess(Long id, String processorName);
}
@@ -0,0 +1,439 @@
package org.springblade.transport.mk;
import lombok.RequiredArgsConstructor;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.entity.PreSettlement;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IPaymentApplicationService;
import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IWaybillService;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* 各业务 MK 流程处理器
*/
public final class MkProcessBizHandlers {
private MkProcessBizHandlers() {
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class CustomerHandler implements IMkProcessBizHandler {
private final ICustomerArchiveService service;
@Override
public String bizType() {
return "customer-archive";
}
@Override
public Object publicDetail(Long id) {
return service.publicDetail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
return service.syncProcessNodeFromProcessInfo(id, processInfo);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
return service.approveFromProcess(id, processorName);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
return service.rejectFromProcess(id, processorName);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class ProjectHandler implements IMkProcessBizHandler {
private final IProjectApplyService service;
@Override
public String bizType() {
return "project-apply";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
ProjectApply entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
if (!"change_reviewing".equals(entity.getApprovalStatus())) {
entity.setApprovalStatus("reviewing");
}
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
ProjectApply entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())
|| "change_approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
entity.setApprovalStatus(change ? "change_approved" : "approved");
entity.setEffectiveType("formal");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
entity.setApprovedTime(LocalDateTime.now());
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
ProjectApply entity = service.getById(id);
if (entity == null || "rejected".equals(entity.getApprovalStatus())
|| "change_rejected".equals(entity.getApprovalStatus())) {
return entity != null;
}
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
entity.setApprovalStatus(change ? "change_rejected" : "rejected");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class ContractHandler implements IMkProcessBizHandler {
private final IContractManageService service;
@Override
public String bizType() {
return "contract-manage";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
ContractManage entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
if (!"change_reviewing".equals(entity.getApprovalStatus())) {
entity.setApprovalStatus("reviewing");
}
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
ContractManage entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())
|| "change_approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
entity.setApprovalStatus(change ? "change_approved" : "approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
ContractManage entity = service.getById(id);
if (entity == null || "rejected".equals(entity.getApprovalStatus())
|| "change_rejected".equals(entity.getApprovalStatus())) {
return entity != null;
}
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
entity.setApprovalStatus(change ? "change_rejected" : "rejected");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class WaybillHandler implements IMkProcessBizHandler {
private final IWaybillService service;
@Override
public String bizType() {
return "waybill-manage";
}
@Override
public Object publicDetail(Long id) {
return service.getById(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
Waybill entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
if (Func.isEmpty(node)) {
return false;
}
entity.setCurrentProcessNode(node);
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
Waybill entity = service.getById(id);
if (entity == null) {
return false;
}
entity.setCurrentProcessNode("审核通过");
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
Waybill entity = service.getById(id);
if (entity == null) {
return false;
}
entity.setCurrentProcessNode("审核不通过");
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class PreSettlementHandler implements IMkProcessBizHandler {
private final IPreSettlementService service;
@Override
public String bizType() {
return "pre-settlement";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
PreSettlement entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
entity.setApprovalStatus("reviewing");
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
PreSettlement entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
entity.setApprovedTime(LocalDateTime.now());
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
PreSettlement entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("returned");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class FormalSettlementHandler implements IMkProcessBizHandler {
private final IFormalSettlementService service;
@Override
public String bizType() {
return "formal-settlement";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
FormalSettlement entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
entity.setApprovalStatus("reviewing");
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
FormalSettlement entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
entity.setApprovedTime(LocalDateTime.now());
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
FormalSettlement entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("returned");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class PaymentHandler implements IMkProcessBizHandler {
private final IPaymentApplicationService service;
@Override
public String bizType() {
return "payment-application";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
PaymentApplication entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
entity.setApprovalStatus("reviewing");
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
PaymentApplication entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
PaymentApplication entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("returned");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
}
@@ -0,0 +1,191 @@
package org.springblade.transport.mk;
import com.alibaba.fastjson2.JSON;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.FR;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.process.feign.IBusinessProcessClient;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 公开页流程消息:当前节点立即查询,流程实例详情延迟 5 秒查询后同步业务状态。
*/
@Slf4j
@Service
@TenantIgnore
@RequiredArgsConstructor
public class MkProcessMessageService {
private static final long PROCESS_INFO_DELAY_SECONDS = 5L;
private final IBusinessProcessClient businessProcessClient;
private final List<IMkProcessBizHandler> handlers;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2, runnable -> {
Thread thread = new Thread(runnable, "mk-process-info-" + THREAD_INDEX.incrementAndGet());
thread.setDaemon(true);
return thread;
});
private static final AtomicInteger THREAD_INDEX = new AtomicInteger();
@PreDestroy
public void shutdown() {
scheduler.shutdown();
}
public IMkProcessBizHandler handler(String bizType) {
Map<String, IMkProcessBizHandler> mapping = handlers.stream()
.collect(Collectors.toMap(IMkProcessBizHandler::bizType, Function.identity(), (a, b) -> a));
IMkProcessBizHandler handler = mapping.get(bizType);
if (handler == null) {
throw new IllegalArgumentException("不支持的MK业务类型:" + bizType);
}
return handler;
}
public void handleProcessMessage(String bizType, Map<String, Object> body) {
IMkProcessBizHandler handler = handler(bizType);
Map<String, Object> formValues = asMap(body == null ? null : body.get("formValues"));
String processId = firstText(formValues, "processId");
if (StringUtil.isBlank(processId) && body != null) {
processId = firstText(body, "processId");
}
String loginName = firstText(formValues, "mkLoginName", "loginName");
if (StringUtil.isBlank(processId)) {
log.warn("公开页流程消息未找到 processId,跳过查询当前节点 bizType={}", bizType);
return;
}
String bizIdText = firstText(asMap(body == null ? null : body.get("formData")), "id", "formInstanceId");
queryCurrentNodes(bizType, processId, loginName);
scheduleProcessInfo(handler, bizType, processId, loginName, bizIdText, formValues);
}
private void queryCurrentNodes(String bizType, String processId, String loginName) {
try {
FR<Object> result = businessProcessClient.getCurrentNodes(processId, loginName);
Object nodeData = result == null ? null : result.getData();
log.info("公开页流程消息当前节点详情 bizType={} processId={} loginName={} result={}",
bizType, processId, loginName, JSON.toJSONString(nodeData));
} catch (Exception e) {
log.error("公开页查询当前节点失败 bizType={} processId={} loginName={}", bizType, processId, loginName, e);
}
}
private void scheduleProcessInfo(IMkProcessBizHandler handler, String bizType, String processId,
String loginName, String bizIdText, Map<String, Object> formValues) {
Map<String, Object> formValuesCopy = new HashMap<>(formValues == null ? Map.of() : formValues);
log.info("公开页将在{}秒后查询流程实例详情 bizType={} processId={}", PROCESS_INFO_DELAY_SECONDS, bizType, processId);
scheduler.schedule(
() -> queryProcessInfo(handler, bizType, processId, loginName, bizIdText, formValuesCopy),
PROCESS_INFO_DELAY_SECONDS,
TimeUnit.SECONDS
);
}
private void queryProcessInfo(IMkProcessBizHandler handler, String bizType, String processId,
String loginName, String bizIdText, Map<String, Object> formValues) {
try {
FR<Object> processInfoResult = businessProcessClient.getProcessInfo(processId, loginName);
Object processInfo = processInfoResult == null ? null : processInfoResult.getData();
log.info("公开页流程消息流程实例详情 bizType={} processId={} loginName={} result={}",
bizType, processId, loginName, JSON.toJSONString(processInfo));
if (isProcessFinished(processInfo)) {
applyProcessResult(handler, bizType, bizIdText, processId, formValues, true);
} else if (isProcessRejected(processInfo)) {
applyProcessResult(handler, bizType, bizIdText, processId, formValues, false);
} else if (StringUtil.isNotBlank(bizIdText)) {
try {
handler.syncFromProcessInfo(Long.valueOf(bizIdText), processInfo);
} catch (NumberFormatException e) {
log.warn("公开页流程消息业务id格式不正确 bizType={} id={}", bizType, bizIdText);
}
}
} catch (Exception e) {
log.error("公开页查询流程实例详情失败 bizType={} processId={} loginName={}", bizType, processId, loginName, e);
}
}
private static boolean isProcessFinished(Object processInfo) {
return "30".equals(firstText(asMap(processInfo), "fdProcessStatus"));
}
private static boolean isProcessRejected(Object processInfo) {
Map<String, Object> info = asMap(processInfo);
if (!"20".equals(firstText(info, "fdProcessStatus"))) {
return false;
}
if (hasItems(info.get("currentHandlers"))) {
return false;
}
return !hasItems(asMap(info.get("fdTaskInfo")).get("handlerInfos"));
}
private static boolean hasItems(Object value) {
return value instanceof Collection<?> collection && !collection.isEmpty();
}
private void applyProcessResult(IMkProcessBizHandler handler, String bizType, String bizIdText,
String processId, Map<String, Object> formValues, boolean approved) {
String action = approved ? "审核通过" : "审核驳回";
if (StringUtil.isBlank(bizIdText)) {
log.warn("流程{}但未找到业务id,跳过同步 bizType={} processId={}", action, bizType, processId);
return;
}
try {
String processorName = firstText(formValues, "mkUserName", "mkLoginName");
Long bizId = Long.valueOf(bizIdText);
if (approved) {
handler.approveFromProcess(bizId, processorName);
} else {
handler.rejectFromProcess(bizId, processorName);
}
} catch (NumberFormatException e) {
log.warn("流程{}但业务id格式不正确 bizType={} id={}", action, bizType, bizIdText);
}
}
private static Map<String, Object> asMap(Object value) {
if (!(value instanceof Map<?, ?> map)) {
return Collections.emptyMap();
}
Map<String, Object> result = new HashMap<>();
map.forEach((key, nested) -> {
if (key != null) {
result.put(String.valueOf(key), nested);
}
});
return result;
}
private static String firstText(Map<String, Object> source, String... keys) {
if (source == null || keys == null) {
return null;
}
for (String key : keys) {
Object value = source.get(key);
if (value == null) {
continue;
}
String text = String.valueOf(value).trim();
if (StringUtil.isNotBlank(text) && !"null".equalsIgnoreCase(text)) {
return text;
}
}
return null;
}
}
@@ -0,0 +1,76 @@
package org.springblade.transport.mk;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.Func;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 解析 MK 流程实例详情中的当前节点、当前处理人
*/
public final class MkProcessNodeHelper {
private MkProcessNodeHelper() {
}
public static String currentProcessor(Object processInfo) {
return joinDistinct(extractTexts(asMap(processInfo).get("currentHandlers"), "fdName", "name"), "");
}
public static String currentNode(Object processInfo) {
Map<String, Object> taskInfo = asMap(asMap(processInfo).get("fdTaskInfo"));
return joinDistinct(extractTexts(taskInfo.get("handlerInfos"), "nodeName"), "");
}
public static String text(String value, String fallback) {
return Func.isEmpty(value) ? fallback : value.trim();
}
public static Map<String, Object> asMap(Object value) {
if (value instanceof Map<?, ?> map) {
Map<String, Object> result = new LinkedHashMap<>();
map.forEach((key, nested) -> {
if (key != null) {
result.put(String.valueOf(key), nested);
}
});
return result;
}
if (value == null) {
return Map.of();
}
try {
Map<String, Object> parsed = JsonUtil.toMap(JsonUtil.toJson(value));
return parsed == null ? Map.of() : parsed;
} catch (Exception ignored) {
return Map.of();
}
}
private static List<String> extractTexts(Object listObj, String... keys) {
List<String> result = new ArrayList<>();
if (!(listObj instanceof Collection<?> collection) || keys == null) {
return result;
}
for (Object item : collection) {
Map<String, Object> map = asMap(item);
for (String key : keys) {
String text = Func.toStr(map.get(key), "").trim();
if (Func.isNotEmpty(text)) {
result.add(text);
break;
}
}
}
return result;
}
private static String joinDistinct(List<String> values, String delimiter) {
return values.stream().filter(Func::isNotEmpty).distinct().collect(Collectors.joining(delimiter));
}
}
@@ -100,6 +100,53 @@ public interface ICustomerArchiveService extends BaseService<CustomerArchive> {
*/ */
boolean submitApproval(Long id); boolean submitApproval(Long id);
/**
* 按 MK 当前节点同步客商当前节点、当前处理人和审批状态
*
* @param id 客商ID
* @param currentNode 当前节点
* @param currentProcessor 当前处理人
* @param approvalStatus 审批状态,可为空,为空时保持为审核中
* @return 是否成功
*/
boolean syncProcessNode(Long id, String currentNode, String currentProcessor, String approvalStatus);
/**
* 按 MK getCurrentNodes 返回结果同步客商当前节点
*
* @param id 客商ID
* @param currentNodes MK 当前节点详情
* @return 是否成功
*/
boolean syncProcessNodeFromMk(Long id, Object currentNodes);
/**
* 按 MK 流程实例详情回写当前节点、当前处理人
*
* @param id 客商ID
* @param processInfo MK getProcessInfo 返回数据
* @return 是否成功
*/
boolean syncProcessNodeFromProcessInfo(Long id, Object processInfo);
/**
* MK 流程结束(fdProcessStatus=30)时将客商置为审核通过
*
* @param id 客商ID
* @param processorName 当前处理人,可为空
* @return 是否成功
*/
boolean approveFromProcess(Long id, String processorName);
/**
* MK 流程状态为 20 时将客商置为审核驳回
*
* @param id 客商ID
* @param processorName 当前处理人,可为空
* @return 是否成功
*/
boolean rejectFromProcess(Long id, String processorName);
/** /**
* 撤回审批并恢复草稿状态 * 撤回审批并恢复草稿状态
* *
@@ -94,7 +94,6 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl<AccidentRecordMap
AccidentRecordExcel excel = data.get(index); AccidentRecordExcel excel = data.get(index);
try { try {
AccidentRecord accidentRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AccidentRecord.class)); AccidentRecord accidentRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AccidentRecord.class));
accidentRecord.setAccidentDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getAccidentDate(), "事故发生日期"));
prepare(accidentRecord); prepare(accidentRecord);
List<String> validationErrors = validateImportAccidentRecord(accidentRecord); List<String> validationErrors = validateImportAccidentRecord(accidentRecord);
if (Func.isNotEmpty(validationErrors)) { if (Func.isNotEmpty(validationErrors)) {
@@ -113,8 +113,6 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl<AnnualIns
AnnualInspectionRecordExcel excel = data.get(index); AnnualInspectionRecordExcel excel = data.get(index);
try { try {
AnnualInspectionRecord annualInspectionRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AnnualInspectionRecord.class)); AnnualInspectionRecord annualInspectionRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AnnualInspectionRecord.class));
annualInspectionRecord.setInspectionAssessmentDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getInspectionAssessmentDate(), "检测评定日期"));
annualInspectionRecord.setValidUntilDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getValidUntilDate(), "有效期截止日"));
prepare(annualInspectionRecord); prepare(annualInspectionRecord);
List<String> validationErrors = validateImportAnnualInspectionRecord(annualInspectionRecord); List<String> validationErrors = validateImportAnnualInspectionRecord(annualInspectionRecord);
if (Func.isNotEmpty(validationErrors)) { if (Func.isNotEmpty(validationErrors)) {
@@ -0,0 +1,182 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.service.impl;
import com.alibaba.fastjson2.JSON;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.FR;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.process.feign.IBusinessProcessClient;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 客商公开页流程消息处理:当前节点立即查询,流程实例详情延迟查询。
*/
@Slf4j
@Service
@TenantIgnore
@RequiredArgsConstructor
public class CustomerArchivePublicProcessService {
private static final long PROCESS_INFO_DELAY_SECONDS = 5L;
private final ICustomerArchiveService customerArchiveService;
private final IBusinessProcessClient businessProcessClient;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2, runnable -> {
Thread thread = new Thread(runnable, "customer-archive-process-info-" + THREAD_INDEX.incrementAndGet());
thread.setDaemon(true);
return thread;
});
private static final AtomicInteger THREAD_INDEX = new AtomicInteger();
@PreDestroy
public void shutdown() {
scheduler.shutdown();
}
public void handleProcessMessage(Map<String, Object> body) {
Map<String, Object> formValues = asMap(body == null ? null : body.get("formValues"));
String processId = firstText(formValues, "processId");
if (StringUtil.isBlank(processId) && body != null) {
processId = firstText(body, "processId");
}
String loginName = firstText(formValues, "mkLoginName", "loginName");
if (StringUtil.isBlank(processId)) {
log.warn("客商公开页流程消息未找到 processId,跳过查询当前节点");
return;
}
String customerIdText = firstText(asMap(body == null ? null : body.get("formData")), "id");
queryCurrentNodes(processId, loginName);
scheduleProcessInfo(processId, loginName, customerIdText, formValues);
}
private void queryCurrentNodes(String processId, String loginName) {
try {
FR<Object> result = businessProcessClient.getCurrentNodes(processId, loginName);
Object nodeData = result == null ? null : result.getData();
log.info("客商公开页流程消息当前节点详情 processId={} loginName={} result={}",
processId, loginName, JSON.toJSONString(nodeData));
} catch (Exception e) {
log.error("客商公开页查询当前节点失败 processId={} loginName={}", processId, loginName, e);
}
}
private void scheduleProcessInfo(String processId, String loginName, String customerIdText,
Map<String, Object> formValues) {
Map<String, Object> formValuesCopy = new HashMap<>(formValues == null ? Map.of() : formValues);
log.info("客商公开页将在{}秒后查询流程实例详情 processId={}", PROCESS_INFO_DELAY_SECONDS, processId);
scheduler.schedule(
() -> queryProcessInfo(processId, loginName, customerIdText, formValuesCopy),
PROCESS_INFO_DELAY_SECONDS,
TimeUnit.SECONDS
);
}
private void queryProcessInfo(String processId, String loginName, String customerIdText,
Map<String, Object> formValues) {
try {
FR<Object> processInfoResult = businessProcessClient.getProcessInfo(processId, loginName);
Object processInfo = processInfoResult == null ? null : processInfoResult.getData();
log.info("客商公开页流程消息流程实例详情 processId={} loginName={} result={}",
processId, loginName, JSON.toJSONString(processInfo));
if (isProcessFinished(processInfo)) {
applyProcessResult(customerIdText, processId, formValues, true);
} else if (isProcessRejected(processInfo)) {
applyProcessResult(customerIdText, processId, formValues, false);
} else if (StringUtil.isNotBlank(customerIdText)) {
try {
customerArchiveService.syncProcessNodeFromProcessInfo(Long.valueOf(customerIdText), processInfo);
} catch (NumberFormatException e) {
log.warn("客商公开页流程消息客商id格式不正确:{}", customerIdText);
}
}
} catch (Exception e) {
log.error("客商公开页查询流程实例详情失败 processId={} loginName={}", processId, loginName, e);
}
}
private static boolean isProcessFinished(Object processInfo) {
return "30".equals(firstText(asMap(processInfo), "fdProcessStatus"));
}
private static boolean isProcessRejected(Object processInfo) {
Map<String, Object> info = asMap(processInfo);
if (!"20".equals(firstText(info, "fdProcessStatus"))) {
return false;
}
if (hasItems(info.get("currentHandlers"))) {
return false;
}
return !hasItems(asMap(info.get("fdTaskInfo")).get("handlerInfos"));
}
private static boolean hasItems(Object value) {
return value instanceof Collection<?> collection && !collection.isEmpty();
}
private void applyProcessResult(String customerIdText, String processId, Map<String, Object> formValues,
boolean approved) {
String action = approved ? "审核通过" : "审核驳回";
if (StringUtil.isBlank(customerIdText)) {
log.warn("流程{}但未找到客商id,跳过同步 processId={}", action, processId);
return;
}
try {
String processorName = firstText(formValues, "mkUserName", "mkLoginName");
Long customerId = Long.valueOf(customerIdText);
if (approved) {
customerArchiveService.approveFromProcess(customerId, processorName);
} else {
customerArchiveService.rejectFromProcess(customerId, processorName);
}
} catch (NumberFormatException e) {
log.warn("流程{}但客商id格式不正确:{}", action, customerIdText);
}
}
private static Map<String, Object> asMap(Object value) {
if (!(value instanceof Map<?, ?> map)) {
return Collections.emptyMap();
}
Map<String, Object> result = new HashMap<>();
map.forEach((key, nested) -> {
if (key != null) {
result.put(String.valueOf(key), nested);
}
});
return result;
}
private static String firstText(Map<String, Object> source, String... keys) {
if (source == null || keys == null) {
return null;
}
for (String key : keys) {
Object value = source.get(key);
if (value == null) {
continue;
}
String text = String.valueOf(value).trim();
if (StringUtil.isNotBlank(text) && !"null".equalsIgnoreCase(text)) {
return text;
}
}
return null;
}
}
@@ -29,6 +29,7 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException; import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.secure.utils.AuthUtil;
@@ -85,6 +86,7 @@ import java.math.RoundingMode;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@@ -101,6 +103,7 @@ import java.util.stream.Collectors;
* *
* @author Chill * @author Chill
*/ */
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveMapper, CustomerArchive> implements ICustomerArchiveService { public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveMapper, CustomerArchive> implements ICustomerArchiveService {
@@ -214,12 +217,151 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
CustomerArchive before = Objects.requireNonNull(BeanUtil.copyProperties(detail, CustomerArchive.class)); CustomerArchive before = Objects.requireNonNull(BeanUtil.copyProperties(detail, CustomerArchive.class));
CustomerArchive after = copyCustomer(before); CustomerArchive after = copyCustomer(before);
after.setApprovalStatus(APPROVAL_REVIEWING); after.setApprovalStatus(APPROVAL_REVIEWING);
after.setCurrentNode("客商准入审批"); if (Func.isEmpty(after.getCurrentNode()) || Objects.equals(after.getCurrentNode(), "草稿")) {
after.setCurrentProcessor("待处理"); after.setCurrentNode("客商准入审批");
after.setCurrentProcessor("待处理");
}
addChangeRecord(id, "提交客商准入审批", before, after); addChangeRecord(id, "提交客商准入审批", before, after);
return updateById(after); return updateById(after);
} }
@Override
@TenantIgnore
@Transactional(rollbackFor = Exception.class)
public boolean syncProcessNode(Long id, String currentNode, String currentProcessor, String approvalStatus) {
if (Func.isEmpty(id)) {
log.warn("同步客商当前节点失败,客商ID为空");
return false;
}
CustomerArchive customer = getById(id);
if (customer == null || Objects.equals(customer.getIsDeleted(), 1)) {
log.warn("同步客商当前节点失败,客商不存在 id={}", id);
return false;
}
String nextStatus = Func.isEmpty(approvalStatus) ? APPROVAL_REVIEWING : approvalStatus;
boolean updated = this.lambdaUpdate()
.eq(CustomerArchive::getId, id)
.set(Func.isNotEmpty(currentNode), CustomerArchive::getCurrentNode, currentNode)
.set(Func.isNotEmpty(currentProcessor), CustomerArchive::getCurrentProcessor, currentProcessor)
.set(Func.isNotEmpty(nextStatus), CustomerArchive::getApprovalStatus, nextStatus)
.update();
log.info("同步客商当前节点 id={} currentNode={} currentProcessor={} approvalStatus={} result={}",
id, currentNode, currentProcessor, nextStatus, updated);
return updated;
}
@Override
@TenantIgnore
@Transactional(rollbackFor = Exception.class)
public boolean syncProcessNodeFromMk(Long id, Object currentNodes) {
if (Func.isEmpty(id) || currentNodes == null) {
return false;
}
List<Map<String, Object>> nodes = normalizeMkNodeList(currentNodes);
if (nodes.isEmpty()) {
log.warn("同步客商当前节点跳过,节点详情为空 id={}", id);
return false;
}
List<String> nodeNames = new ArrayList<>();
List<String> handlerNames = new ArrayList<>();
for (Map<String, Object> node : nodes) {
String nodeName = Func.toStr(node.get("nodeName"), "").trim();
if (Func.isNotEmpty(nodeName)) {
nodeNames.add(nodeName);
}
Object handlers = node.get("nodeHandlers");
if (!(handlers instanceof Collection<?> handlerList)) {
continue;
}
for (Object handler : handlerList) {
Map<String, Object> handlerMap = asStringObjectMap(handler);
String handlerName = Func.toStr(handlerMap.get("handlerName"), "").trim();
if (Func.isNotEmpty(handlerName)) {
handlerNames.add(handlerName);
}
}
}
return syncProcessNode(id, joinDistinct(nodeNames), joinDistinct(handlerNames), APPROVAL_REVIEWING);
}
@Override
@TenantIgnore
@Transactional(rollbackFor = Exception.class)
public boolean syncProcessNodeFromProcessInfo(Long id, Object processInfo) {
if (Func.isEmpty(id) || processInfo == null) {
return false;
}
Map<String, Object> info = asStringObjectMap(processInfo);
if (info.isEmpty()) {
log.warn("同步客商当前节点跳过,流程实例详情为空 id={}", id);
return false;
}
String currentProcessor = joinDistinct(extractTexts(info.get("currentHandlers"), "fdName", "name"), "");
Map<String, Object> taskInfo = asStringObjectMap(info.get("fdTaskInfo"));
String currentNode = joinDistinct(extractTexts(taskInfo.get("handlerInfos"), "nodeName"), "");
if (Func.isEmpty(currentNode) && Func.isEmpty(currentProcessor)) {
log.warn("同步客商当前节点跳过,未解析到节点名称或处理人 id={}", id);
return false;
}
return syncProcessNode(id, currentNode, currentProcessor, APPROVAL_REVIEWING);
}
@Override
@TenantIgnore
@Transactional(rollbackFor = Exception.class)
public boolean approveFromProcess(Long id, String processorName) {
if (Func.isEmpty(id)) {
log.warn("流程结束审核通过失败,客商ID为空");
return false;
}
CustomerArchive before = getById(id);
if (before == null || Objects.equals(before.getIsDeleted(), 1)) {
log.warn("流程结束审核通过失败,客商不存在 id={}", id);
return false;
}
if (Objects.equals(before.getApprovalStatus(), APPROVAL_APPROVED)) {
log.info("流程结束审核通过跳过,客商已是审核通过 id={}", id);
return true;
}
CustomerArchive after = copyCustomer(before);
after.setAccessType(ACCESS_FORMAL);
after.setApprovalStatus(APPROVAL_APPROVED);
after.setCurrentNode("审核通过");
after.setCurrentProcessor(Func.isEmpty(processorName) ? "系统" : processorName.trim());
after.setApprovedTime(LocalDateTime.now());
addChangeRecord(id, "客商准入审核通过", before, after);
boolean updated = updateById(after);
log.info("流程结束审核通过 id={} processor={} result={}", id, after.getCurrentProcessor(), updated);
return updated;
}
@Override
@TenantIgnore
@Transactional(rollbackFor = Exception.class)
public boolean rejectFromProcess(Long id, String processorName) {
if (Func.isEmpty(id)) {
log.warn("流程审核驳回失败,客商ID为空");
return false;
}
CustomerArchive before = getById(id);
if (before == null || Objects.equals(before.getIsDeleted(), 1)) {
log.warn("流程审核驳回失败,客商不存在 id={}", id);
return false;
}
if (Objects.equals(before.getApprovalStatus(), APPROVAL_REJECTED)) {
log.info("流程审核驳回跳过,客商已是审核不通过 id={}", id);
return true;
}
CustomerArchive after = copyCustomer(before);
after.setApprovalStatus(APPROVAL_REJECTED);
after.setCurrentNode("审核不通过");
after.setCurrentProcessor(Func.isEmpty(processorName) ? "系统" : processorName.trim());
addChangeRecord(id, "客商准入审核不通过", before, after);
boolean updated = updateById(after);
log.info("流程审核驳回 id={} processor={} result={}", id, after.getCurrentProcessor(), updated);
return updated;
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean withdrawApproval(Long id) { public boolean withdrawApproval(Long id) {
@@ -1032,6 +1174,73 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
return Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchive.class)); return Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchive.class));
} }
@SuppressWarnings("unchecked")
private List<Map<String, Object>> normalizeMkNodeList(Object currentNodes) {
Object source = currentNodes;
if (source instanceof String text && Func.isNotEmpty(text.trim())) {
source = JsonUtil.parse(text, List.class);
}
if (source instanceof Collection<?> collection) {
List<Map<String, Object>> result = new ArrayList<>();
for (Object item : collection) {
Map<String, Object> map = asStringObjectMap(item);
if (!map.isEmpty()) {
result.add(map);
}
}
return result;
}
Map<String, Object> single = asStringObjectMap(source);
return single.isEmpty() ? List.of() : List.of(single);
}
private Map<String, Object> asStringObjectMap(Object value) {
if (value instanceof Map<?, ?> map) {
Map<String, Object> result = new LinkedHashMap<>();
map.forEach((key, nested) -> {
if (key != null) {
result.put(String.valueOf(key), nested);
}
});
return result;
}
if (value == null) {
return Map.of();
}
try {
Map<String, Object> parsed = JsonUtil.toMap(JsonUtil.toJson(value));
return parsed == null ? Map.of() : parsed;
} catch (Exception ignored) {
return Map.of();
}
}
private String joinDistinct(List<String> values) {
return joinDistinct(values, ",");
}
private String joinDistinct(List<String> values, String delimiter) {
return values.stream().filter(Func::isNotEmpty).distinct().collect(Collectors.joining(delimiter));
}
private List<String> extractTexts(Object listObj, String... keys) {
List<String> result = new ArrayList<>();
if (!(listObj instanceof Collection<?> collection) || keys == null) {
return result;
}
for (Object item : collection) {
Map<String, Object> map = asStringObjectMap(item);
for (String key : keys) {
String text = Func.toStr(map.get(key), "").trim();
if (Func.isNotEmpty(text)) {
result.add(text);
break;
}
}
}
return result;
}
private void addChangeRecord(Long customerId, String content, CustomerArchive before, CustomerArchive after) { private void addChangeRecord(Long customerId, String content, CustomerArchive before, CustomerArchive after) {
Map<String, Object> beforeSnapshot = customerSnapshot(before); Map<String, Object> beforeSnapshot = customerSnapshot(before);
Map<String, Object> afterSnapshot = customerSnapshot(after); Map<String, Object> afterSnapshot = customerSnapshot(after);
@@ -91,7 +91,6 @@ public class EquipmentLedgerServiceImpl extends BaseServiceImpl<EquipmentLedgerM
EquipmentLedgerExcel excel = data.get(index); EquipmentLedgerExcel excel = data.get(index);
try { try {
EquipmentLedger equipmentLedger = Objects.requireNonNull(BeanUtil.copyProperties(excel, EquipmentLedger.class)); EquipmentLedger equipmentLedger = Objects.requireNonNull(BeanUtil.copyProperties(excel, EquipmentLedger.class));
equipmentLedger.setFactoryDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getFactoryDate(), "出厂日期"));
prepare(equipmentLedger); prepare(equipmentLedger);
if (equipmentLedger.getId() == null && Func.isEmpty(equipmentLedger.getEquipmentCode())) { if (equipmentLedger.getId() == null && Func.isEmpty(equipmentLedger.getEquipmentCode())) {
equipmentLedger.setEquipmentCode(nextEquipmentCode(importEquipmentCodes)); equipmentLedger.setEquipmentCode(nextEquipmentCode(importEquipmentCodes));
@@ -119,9 +119,6 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
InsuranceRecordExcel excel = data.get(index); InsuranceRecordExcel excel = data.get(index);
try { try {
InsuranceRecord insuranceRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, InsuranceRecord.class)); InsuranceRecord insuranceRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, InsuranceRecord.class));
insuranceRecord.setStartDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getStartDate(), "开始日期"));
insuranceRecord.setEndDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEndDate(), "结束日期"));
insuranceRecord.setInvoiceDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getInvoiceDate(), "开票日期"));
prepare(insuranceRecord); prepare(insuranceRecord);
List<String> validationErrors = validateImportInsuranceRecord(insuranceRecord); List<String> validationErrors = validateImportInsuranceRecord(insuranceRecord);
if (Func.isNotEmpty(insuranceRecord.getVehicleType()) && Func.isNotEmpty(insuranceRecord.getInsuranceType()) && Func.isNotEmpty(insuranceRecord.getPolicyNo())) { if (Func.isNotEmpty(insuranceRecord.getVehicleType()) && Func.isNotEmpty(insuranceRecord.getInsuranceType()) && Func.isNotEmpty(insuranceRecord.getPolicyNo())) {
@@ -174,7 +174,6 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo); throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo);
} }
Waybill first = waybills.get(0); Waybill first = waybills.get(0);
Waybill last = waybills.get(waybills.size() - 1);
LoadingManage loadingManage = new LoadingManage(); LoadingManage loadingManage = new LoadingManage();
loadingManage.setLoadingNo(normalizedLoadingNo); loadingManage.setLoadingNo(normalizedLoadingNo);
loadingManage.setLoadingSubNos(waybills.stream() loadingManage.setLoadingSubNos(waybills.stream()
@@ -198,8 +197,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
loadingManage.setCarrierName(first.getCarrierName()); loadingManage.setCarrierName(first.getCarrierName());
loadingManage.setCarrierContractId(first.getCarrierContractId()); loadingManage.setCarrierContractId(first.getCarrierContractId());
loadingManage.setDepartureAddress(first.getDepartureAddress()); loadingManage.setDepartureAddress(first.getDepartureAddress());
loadingManage.setTransitAddress(buildImportedRouteTransitAddress(waybills)); loadingManage.setArrivalAddress(first.getArrivalAddress());
loadingManage.setArrivalAddress(last.getArrivalAddress());
loadingManage.setOriginalNo(first.getOriginalNo()); loadingManage.setOriginalNo(first.getOriginalNo());
loadingManage.setDataSource("批量导入"); loadingManage.setDataSource("批量导入");
loadingManage.setStartDate(first.getStartDate()); loadingManage.setStartDate(first.getStartDate());
@@ -224,39 +222,6 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
.set(Waybill::getLoadingNo, normalizedLoadingNo)); .set(Waybill::getLoadingNo, normalizedLoadingNo));
} }
/**
* 根据导入运单构建配载单的途经地
* 组内运单按导入顺序串联路线上一票的到货地址与下一票的发货地址相同中途卸货点时只保留一个点
* 最终形成首票发货地 途经点 末票到货地
*/
private String buildImportedRouteTransitAddress(List<Waybill> waybills) {
// 按顺序收集全部节点首票发货地每票到货地节点与相邻前一点相同则重合跳过
List<String> nodes = new ArrayList<>();
for (Waybill waybill : waybills) {
String departure = TransportBusinessSupport.trimToNull(waybill.getDepartureAddress());
String arrival = TransportBusinessSupport.trimToNull(waybill.getArrivalAddress());
appendRouteNode(nodes, departure);
appendRouteNode(nodes, arrival);
}
// 途经点 = 去掉首尾首票发货地末票到货地后的中间节点
List<String> transitNodes = nodes.size() > 2 ? nodes.subList(1, nodes.size() - 1) : List.of();
if (Func.isEmpty(transitNodes)) {
return null;
}
return String.join(" - ", transitNodes);
}
private void appendRouteNode(List<String> nodes, String address) {
if (Func.isEmpty(address)) {
return;
}
if (!nodes.isEmpty() && nodes.get(nodes.size() - 1).equals(address)) {
// 与上一节点相同视为同一地点重合不重复
return;
}
nodes.add(address);
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public BusinessRemoveResultVO removeLoadingManage(String ids) { public BusinessRemoveResultVO removeLoadingManage(String ids) {
@@ -75,7 +75,6 @@ public class OtherExpenseRecordServiceImpl extends BaseServiceImpl<OtherExpenseR
OtherExpenseRecordExcel excel = data.get(index); OtherExpenseRecordExcel excel = data.get(index);
try { try {
OtherExpenseRecord otherExpenseRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, OtherExpenseRecord.class)); OtherExpenseRecord otherExpenseRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, OtherExpenseRecord.class));
otherExpenseRecord.setExpenseDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getExpenseDate(), "费用日期"));
otherExpenseRecord.setDataSource("批量导入"); otherExpenseRecord.setDataSource("批量导入");
prepare(otherExpenseRecord); prepare(otherExpenseRecord);
List<String> validationErrors = validateImportOtherExpenseRecord(otherExpenseRecord); List<String> validationErrors = validateImportOtherExpenseRecord(otherExpenseRecord);
@@ -43,6 +43,8 @@ import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@@ -62,6 +64,7 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
private static final int REMARK_MAX_LENGTH = 500; private static final int REMARK_MAX_LENGTH = 500;
private static final int ATTACHMENTS_MAX_LENGTH = 1000; private static final int ATTACHMENTS_MAX_LENGTH = 1000;
private static final int MONEY_SCALE = 2; private static final int MONEY_SCALE = 2;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
@Override @Override
public IPage<TireReplacementRecordVO> selectTireReplacementRecordPage(IPage<TireReplacementRecordVO> page, TireReplacementRecordVO tireReplacementRecord) { public IPage<TireReplacementRecordVO> selectTireReplacementRecordPage(IPage<TireReplacementRecordVO> page, TireReplacementRecordVO tireReplacementRecord) {
@@ -141,9 +144,9 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
return null; return null;
} }
try { try {
return org.springblade.common.excel.LenientDateParser.parseDate(normalizedValue, "换胎时间"); return LocalDate.parse(normalizedValue, DATE_FORMATTER);
} catch (Exception exception) { } catch (DateTimeParseException exception) {
validationErrors.add(exception.getMessage()); validationErrors.add("换胎时间格式不正确,请使用yyyy-MM-dd格式并填写有效日期");
return null; return null;
} }
} }
@@ -416,9 +416,9 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
// 2. 校验运输类型枚举值 // 2. 校验运输类型枚举值
String transportType = trimToEmpty(excel.getTransportType()); String transportType = trimToEmpty(excel.getTransportType());
if (Func.isNotEmpty(transportType)) { if (Func.isNotEmpty(transportType)) {
List<String> validTransportTypes = List.of("公路运输", "铁路运输", "水路运输", "航空运输"); List<String> validTransportTypes = List.of("公路整车", "公路配载/零担", "铁路运输", "水路运输", "跨境海运", "航空运输");
if (!validTransportTypes.contains(transportType)) { if (!validTransportTypes.contains(transportType)) {
errors.add("运输类型需系统枚举值(公路运输、铁路运输、水路运输、航空运输)"); errors.add("运输类型需系统枚举值(公路整车、公路配载/零担、铁路运输、水路运输、跨境海运、航空运输)");
} }
} }
@@ -510,12 +510,11 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
if (Func.isEmpty(value)) { if (Func.isEmpty(value)) {
return null; return null;
} }
// 宽容解析接受 2026-8-22026/8/2 等写法口径见根工作区 docs/import-spec.md try {
LocalDate date = org.springblade.common.excel.LenientDateParser.parseDateLenientlyOrNull(value); return LocalDate.parse(value.trim(), DateTimeFormatter.ISO_LOCAL_DATE);
if (date == null) { } catch (Exception exception) {
throw new ServiceException(fieldName + " 日期格式无法识别:" + value.trim()); throw new ServiceException(fieldName + "格式必须为 YYYY-MM-DD");
} }
return date;
} }
private void validateImportLength(String value, int maxLength, String fieldName) { private void validateImportLength(String value, int maxLength, String fieldName) {
@@ -60,6 +60,7 @@ import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@@ -1345,12 +1346,14 @@ public class TransportReconciliationServiceImpl
private LocalDateTime parseTimeNullable(String value, String field) { private LocalDateTime parseTimeNullable(String value, String field) {
if (Func.isEmpty(value)) return null; if (Func.isEmpty(value)) return null;
// 宽容解析统一委托公共解析器接受 2026-8-2 12:3 等写法口径见根工作区 docs/import-spec.md for (String pattern : List.of("yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-M-d HH:mm:ss", "yyyy-M-d HH:mm",
LocalDateTime result = org.springblade.common.excel.LenientDateParser.parseDateTimeOrNull(value); "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/M/d HH:mm:ss", "yyyy/M/d HH:mm")) {
if (result == null) { try { return LocalDateTime.parse(value.trim(), DateTimeFormatter.ofPattern(pattern)); } catch (DateTimeParseException ignored) { }
throw new ServiceException(field + " 日期格式无法识别:" + value.trim());
} }
return result; for (String pattern : List.of("yyyy-MM-dd", "yyyy-M-d", "yyyy/MM/dd", "yyyy/M/d")) {
try { return LocalDate.parse(value.trim(), DateTimeFormatter.ofPattern(pattern)).atStartOfDay(); } catch (DateTimeParseException ignored) { }
}
throw new ServiceException(field + "格式应为yyyy-MM-dd HH:mm:ss或yyyy-MM-dd");
} }
private String matchKey(TransportReconciliationInternal row) { private String matchKey(TransportReconciliationInternal row) {
@@ -67,8 +67,6 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
private static final int MAX_DEDUCT_POINTS = 15; private static final int MAX_DEDUCT_POINTS = 15;
private static final String PROCESSED = "已处理"; private static final String PROCESSED = "已处理";
private static final String UNPROCESSED = "未处理"; private static final String UNPROCESSED = "未处理";
private static final String CAR = "车辆";
private static final String SHIP = "船舶";
@Override @Override
public IPage<ViolationRecordVO> selectViolationRecordPage(IPage<ViolationRecordVO> page, ViolationRecordVO violationRecord) { public IPage<ViolationRecordVO> selectViolationRecordPage(IPage<ViolationRecordVO> page, ViolationRecordVO violationRecord) {
@@ -83,7 +81,6 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
prepare(violationRecord); prepare(violationRecord);
validate(violationRecord); validate(violationRecord);
validateVehicleTypeImmutable(violationRecord); validateVehicleTypeImmutable(violationRecord);
clearIrrelevantField(violationRecord);
return saveOrUpdate(violationRecord); return saveOrUpdate(violationRecord);
} }
@@ -99,6 +96,11 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
ViolationRecordImportExcel excel = data.get(index); ViolationRecordImportExcel excel = data.get(index);
try { try {
ViolationRecord violationRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, ViolationRecord.class)); ViolationRecord violationRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, ViolationRecord.class));
if ("船舶".equals(trimToEmpty(excel.getVehicleType()))) {
violationRecord.setViolationItem(excel.getViolationTypeOrItem());
} else {
violationRecord.setViolationType(excel.getViolationTypeOrItem());
}
prepare(violationRecord); prepare(violationRecord);
List<String> validationErrors = validateImportViolationRecord(violationRecord); List<String> validationErrors = validateImportViolationRecord(violationRecord);
if (Func.isNotEmpty(validationErrors)) { if (Func.isNotEmpty(validationErrors)) {
@@ -107,7 +109,6 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
continue; continue;
} }
validateVehicleTypeImmutable(violationRecord); validateVehicleTypeImmutable(violationRecord);
clearIrrelevantField(violationRecord);
violationRecordList.add(violationRecord); violationRecordList.add(violationRecord);
} catch (Exception exception) { } catch (Exception exception) {
String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败";
@@ -135,8 +136,6 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getDriverName()), "驾驶人/船长不能为空"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getDriverName()), "驾驶人/船长不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, "车辆".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationType()), "类型不能为空"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, "车辆".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationType()), "类型不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, "船舶".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem()), "事项不能为空"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, "船舶".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem()), "事项不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, SHIP.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationType()), "船舶不适用于类型,该列应留空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, CAR.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationItem()), "车辆不适用于事项,该列应留空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getViolationTime()), "时间不能为空"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getViolationTime()), "时间不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getViolationTime()) && violationRecord.getViolationTime().isAfter(LocalDateTime.now()), "时间不能超过当前时间"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getViolationTime()) && violationRecord.getViolationTime().isAfter(LocalDateTime.now()), "时间不能超过当前时间");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getLocation()), "地址不能为空"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getLocation()), "地址不能为空");
@@ -184,30 +183,21 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
violationRecord.setProcessDescription(trimToEmpty(violationRecord.getProcessDescription())); violationRecord.setProcessDescription(trimToEmpty(violationRecord.getProcessDescription()));
violationRecord.setProcessResult(trimToNull(violationRecord.getProcessResult())); violationRecord.setProcessResult(trimToNull(violationRecord.getProcessResult()));
violationRecord.setAttachments(trimToNull(violationRecord.getAttachments())); violationRecord.setAttachments(trimToNull(violationRecord.getAttachments()));
if (UNPROCESSED.equals(violationRecord.getProcessStatus())) { if ("车辆".equals(violationRecord.getVehicleType())) {
violationRecord.setProcessResult(null);
}
}
/**
* 清空与车船类型不匹配的对侧字段
* <p>
* 必须在 validate 之后执行校验需要看到用户填了什么
* 若提前清空误填的内容会被静默丢弃用户无从察觉
*/
private void clearIrrelevantField(ViolationRecord violationRecord) {
if (CAR.equals(violationRecord.getVehicleType())) {
violationRecord.setViolationItem(null); violationRecord.setViolationItem(null);
} else { } else {
violationRecord.setViolationType(null); violationRecord.setViolationType(null);
} }
if (UNPROCESSED.equals(violationRecord.getProcessStatus())) {
violationRecord.setProcessResult(null);
}
} }
private void validate(ViolationRecord violationRecord) { private void validate(ViolationRecord violationRecord) {
if (Func.isEmpty(violationRecord.getVehicleType())) { if (Func.isEmpty(violationRecord.getVehicleType())) {
throw new ServiceException("车船类型不能为空"); throw new ServiceException("车船类型不能为空");
} }
if (!CAR.equals(violationRecord.getVehicleType()) && !SHIP.equals(violationRecord.getVehicleType())) { if (!"车辆".equals(violationRecord.getVehicleType()) && !"船舶".equals(violationRecord.getVehicleType())) {
throw new ServiceException("车船类型不正确"); throw new ServiceException("车船类型不正确");
} }
if (Func.isEmpty(violationRecord.getVehicleNo())) { if (Func.isEmpty(violationRecord.getVehicleNo())) {
@@ -216,18 +206,12 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
if (Func.isEmpty(violationRecord.getDriverName())) { if (Func.isEmpty(violationRecord.getDriverName())) {
throw new ServiceException("驾驶人/船长不能为空"); throw new ServiceException("驾驶人/船长不能为空");
} }
if (CAR.equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationType())) { if ("车辆".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationType())) {
throw new ServiceException("类型不能为空"); throw new ServiceException("类型不能为空");
} }
if (SHIP.equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem())) { if ("船舶".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem())) {
throw new ServiceException("事项不能为空"); throw new ServiceException("事项不能为空");
} }
if (SHIP.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationType())) {
throw new ServiceException("船舶不适用于类型,该列应留空");
}
if (CAR.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationItem())) {
throw new ServiceException("车辆不适用于事项,该列应留空");
}
if (Func.isEmpty(violationRecord.getViolationTime())) { if (Func.isEmpty(violationRecord.getViolationTime())) {
throw new ServiceException("时间不能为空"); throw new ServiceException("时间不能为空");
} }
@@ -306,7 +290,7 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
private String normalizeVehicleType(String vehicleType) { private String normalizeVehicleType(String vehicleType) {
String value = trimToEmpty(vehicleType); String value = trimToEmpty(vehicleType);
return value.isEmpty() ? CAR : value; return value.isEmpty() ? "车辆" : value;
} }
private String normalizeProcessStatus(String processStatus) { private String normalizeProcessStatus(String processStatus) {
@@ -42,7 +42,6 @@ import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs; import io.minio.RemoveObjectArgs;
import io.minio.http.Method; import io.minio.http.Method;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@@ -80,14 +79,14 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
private final VoucherImageMapper voucherImageMapper; private final VoucherImageMapper voucherImageMapper;
private final VoucherFileMapper voucherFileMapper; private final VoucherFileMapper voucherFileMapper;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
private final ObjectProvider<MinioClient> minioClientProvider; private final MinioClient minioClient;
private final String minioBucketName; private final String minioBucketName;
private final String minioRootDirectory; private final String minioRootDirectory;
public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService, public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService,
IWaybillService waybillService, IWaybillService waybillService,
VoucherImageMapper voucherImageMapper, VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper, VoucherFileMapper voucherFileMapper,
ApplicationEventPublisher eventPublisher, ObjectProvider<MinioClient> minioClientProvider, ApplicationEventPublisher eventPublisher, MinioClient minioClient,
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") String minioBucketName, @Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") String minioBucketName,
@Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory) { @Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory) {
this.voucherWaybillBatchMapper = voucherWaybillBatchMapper; this.voucherWaybillBatchMapper = voucherWaybillBatchMapper;
@@ -96,7 +95,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
this.voucherImageMapper = voucherImageMapper; this.voucherImageMapper = voucherImageMapper;
this.voucherFileMapper = voucherFileMapper; this.voucherFileMapper = voucherFileMapper;
this.eventPublisher = eventPublisher; this.eventPublisher = eventPublisher;
this.minioClientProvider = minioClientProvider; this.minioClient = minioClient;
this.minioBucketName = minioBucketName; this.minioBucketName = minioBucketName;
this.minioRootDirectory = minioRootDirectory; this.minioRootDirectory = minioRootDirectory;
} }
@@ -252,7 +251,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
List<String> candidateObjectKeys = resolvedObjectKey.equals(objectKey) List<String> candidateObjectKeys = resolvedObjectKey.equals(objectKey)
? List.of(objectKey) : List.of(resolvedObjectKey, objectKey); ? List.of(objectKey) : List.of(resolvedObjectKey, objectKey);
for (String candidateObjectKey : candidateObjectKeys) { for (String candidateObjectKey : candidateObjectKeys) {
try (InputStream source = minioClient().getObject(GetObjectArgs.builder().bucket(minioBucketName).object(candidateObjectKey).build()); try (InputStream source = minioClient.getObject(GetObjectArgs.builder().bucket(minioBucketName).object(candidateObjectKey).build());
OutputStream target = Files.newOutputStream(archivePath)) { OutputStream target = Files.newOutputStream(archivePath)) {
source.transferTo(target); source.transferTo(target);
return archivePath; return archivePath;
@@ -428,7 +427,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
String waybillNo = matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo()); String waybillNo = matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo());
String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, entryName); String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, entryName);
String contentType = contentType(fileName); String contentType = contentType(fileName);
minioClient().putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey) minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
.stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024) .stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
.contentType(contentType).build()); .contentType(contentType).build());
VoucherFile file = new VoucherFile(); VoucherFile file = new VoucherFile();
@@ -491,7 +490,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
String waybillNo = matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo()); String waybillNo = matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo());
String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, entryName); String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, entryName);
String contentType = contentType(fileName); String contentType = contentType(fileName);
minioClient().putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey) minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
.stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024) .stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
.contentType(contentType).build()); .contentType(contentType).build());
VoucherFile file = new VoucherFile(); VoucherFile file = new VoucherFile();
@@ -538,7 +537,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
String entryName = plateNo + "/" + System.currentTimeMillis() + "_" + fileName; String entryName = plateNo + "/" + System.currentTimeMillis() + "_" + fileName;
String objectKey = buildObjectKey(voucher.getId(), matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo()), plateNo, entryName); String objectKey = buildObjectKey(voucher.getId(), matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo()), plateNo, entryName);
try (InputStream inputStream = imageFile.getInputStream()) { try (InputStream inputStream = imageFile.getInputStream()) {
minioClient().putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey) minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
.stream(inputStream, imageFile.getSize(), 10 * 1024 * 1024) .stream(inputStream, imageFile.getSize(), 10 * 1024 * 1024)
.contentType(Func.isEmpty(imageFile.getContentType()) ? contentType(fileName) : imageFile.getContentType()).build()); .contentType(Func.isEmpty(imageFile.getContentType()) ? contentType(fileName) : imageFile.getContentType()).build());
} }
@@ -607,7 +606,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
return Func.isEmpty(normalizedPlateNo) ? "未识别车牌" : normalizedPlateNo; return Func.isEmpty(normalizedPlateNo) ? "未识别车牌" : normalizedPlateNo;
} }
private void deleteObjectQuietly(String objectKey) { try { minioClient().removeObject(RemoveObjectArgs.builder().bucket(minioBucketName).object(objectKey).build()); } catch (Exception exception) { log.warn("删除凭证对象失败 objectKey={}", objectKey, exception); } } private void deleteObjectQuietly(String objectKey) { try { minioClient.removeObject(RemoveObjectArgs.builder().bucket(minioBucketName).object(objectKey).build()); } catch (Exception exception) { log.warn("删除凭证对象失败 objectKey={}", objectKey, exception); } }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@@ -839,7 +838,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
String objectKey = buildObjectKey(voucher.getId(), waybillNo, String objectKey = buildObjectKey(voucher.getId(), waybillNo,
Func.isEmpty(plateNo) ? "root" : plateNo, entryName); Func.isEmpty(plateNo) ? "root" : plateNo, entryName);
String contentType = contentType(fileName); String contentType = contentType(fileName);
minioClient().putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey) minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
.stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024) .stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
.contentType(contentType).build()); .contentType(contentType).build());
VoucherFile file = new VoucherFile(); VoucherFile file = new VoucherFile();
@@ -1053,20 +1052,6 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
} }
} }
/**
* 获取 MinIO 客户端
* <p>
* 未配置 file.storage.minio.endpoint 时该客户端不会被注册
* 此处给出与 {@link #validateMinioConfig()} 一致的明确提示
*/
private MinioClient minioClient() {
MinioClient minioClient = minioClientProvider.getIfAvailable();
if (minioClient == null) {
throw new ServiceException("Nacos 未配置 file.storage.minio.endpoint,凭证文件功能不可用");
}
return minioClient;
}
private String buildObjectKey(Long voucherId, String waybillNo, String plateNo, String entryName) { private String buildObjectKey(Long voucherId, String waybillNo, String plateNo, String entryName) {
String objectKey = voucherId + "/" + waybillNo + "/" + safePathPart(plateNo) + "/" + safeArchivePath(entryName); String objectKey = voucherId + "/" + waybillNo + "/" + safePathPart(plateNo) + "/" + safeArchivePath(entryName);
if (Func.isEmpty(minioRootDirectory)) { if (Func.isEmpty(minioRootDirectory)) {
@@ -1142,7 +1127,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
private String buildVoucherFileUrl(Long fileId, String objectKey) { private String buildVoucherFileUrl(Long fileId, String objectKey) {
try { try {
return minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET).bucket(minioBucketName).object(objectKey).expiry(1, java.util.concurrent.TimeUnit.HOURS).build()); .method(Method.GET).bucket(minioBucketName).object(objectKey).expiry(1, java.util.concurrent.TimeUnit.HOURS).build());
} catch (Exception exception) { } catch (Exception exception) {
log.warn("生成凭证文件预览地址失败 voucherFileId={}, objectKey={}", fileId, objectKey, exception); log.warn("生成凭证文件预览地址失败 voucherFileId={}, objectKey={}", fileId, objectKey, exception);
@@ -19,7 +19,6 @@ import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.WebUtil; import org.springblade.core.tool.utils.WebUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.system.cache.DictCache; import org.springblade.system.cache.DictCache;
import org.springblade.system.cache.DictBizCache; import org.springblade.system.cache.DictBizCache;
import org.springblade.system.cache.UserCache; import org.springblade.system.cache.UserCache;
@@ -52,10 +51,10 @@ import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@@ -69,6 +68,7 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor @RequiredArgsConstructor
public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImportBatchMapper, WaybillImportBatch> implements IWaybillImportBatchService { public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImportBatchMapper, WaybillImportBatch> implements IWaybillImportBatchService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/** 批量导入状态仅保留草稿与导入完成两种。 */ /** 批量导入状态仅保留草稿与导入完成两种。 */
private static final String STATUS_DRAFT = "draft"; private static final String STATUS_DRAFT = "draft";
private static final String STATUS_COMPLETED = "completed"; private static final String STATUS_COMPLETED = "completed";
@@ -184,36 +184,20 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
List<Map<String, Object>> rows = Func.isEmpty(request.getRows()) ? List.of() : request.getRows(); List<Map<String, Object>> rows = Func.isEmpty(request.getRows()) ? List.of() : request.getRows();
// 逐行构建运单未落库构建失败时报出对应行号
List<Waybill> rowWaybills = new ArrayList<>();
for (int index = 0; index < rows.size(); index++) {
try {
rowWaybills.add(buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft));
} catch (Exception exception) {
throw new ServiceException("" + (index + 1) + "" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
}
}
// 同一运单标识号的多货物行合并为一条运单各行转为货物明细
Map<Integer, Waybill> waybillByRow = new HashMap<>();
Set<Integer> skipRows = new HashSet<>();
mergeMultiCargoRows(rows, rowWaybills, waybillByRow, skipRows);
List<Waybill> waybills = new ArrayList<>(); List<Waybill> waybills = new ArrayList<>();
Map<String, List<Waybill>> loadingWaybills = new TreeMap<>(); Map<String, List<Waybill>> loadingWaybills = new TreeMap<>();
for (int index = 0; index < rows.size(); index++) { for (int index = 0; index < rows.size(); index++) {
if (skipRows.contains(index)) continue;
Waybill waybill = waybillByRow.getOrDefault(index, rowWaybills.get(index));
// 配载标识号必须取自行数据运单落库prepareForSave会把新建运单的 loadingNo 置空
String loadingIdentifier = stringValue(rows.get(index), "loadingIdentifier", "配载标识号");
try { try {
String loadingIdentifier = stringValue(rows.get(index), "loadingIdentifier", "配载标识号");
Waybill waybill = buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft);
if (draft) waybillService.saveDraft(waybill); else waybillService.submit(waybill); if (draft) waybillService.saveDraft(waybill); else waybillService.submit(waybill);
waybills.add(waybill);
if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType()) && Func.isNotEmpty(loadingIdentifier)) {
loadingWaybills.computeIfAbsent(loadingIdentifier, key -> new ArrayList<>()).add(waybill);
}
} catch (Exception exception) { } catch (Exception exception) {
throw new ServiceException("" + (index + 1) + "" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage()); throw new ServiceException("" + (index + 1) + "" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
} }
waybills.add(waybill);
if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType()) && Func.isNotEmpty(loadingIdentifier)) {
loadingWaybills.computeIfAbsent(loadingIdentifier, key -> new ArrayList<>()).add(waybill);
}
} }
loadingWaybills.forEach(loadingManageService::createFromImportedWaybills); loadingWaybills.forEach(loadingManageService::createFromImportedWaybills);
batch.setWaybillCount(waybills.size()); batch.setWaybillCount(waybills.size());
@@ -234,69 +218,6 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
} }
} }
/**
* 将同一运单标识号的多货物行合并为一条运单
* 首行作为主行数值字段累加各行后续行转为货物明细追加到主行货物信息中
* merge 的行记入 skipRows落库时跳过
*/
private void mergeMultiCargoRows(List<Map<String, Object>> rows, List<Waybill> rowWaybills,
Map<Integer, Waybill> waybillByRow, Set<Integer> skipRows) {
Map<String, List<Integer>> groups = new LinkedHashMap<>();
for (int index = 0; index < rowWaybills.size(); index++) {
String relationNo = rowWaybills.get(index).getRelationNo();
if (Func.isEmpty(relationNo)) continue;
groups.computeIfAbsent(relationNo, key -> new ArrayList<>()).add(index);
}
for (List<Integer> groupRows : groups.values()) {
if (groupRows.size() <= 1) continue;
Waybill primary = rowWaybills.get(groupRows.get(0));
List<Map<String, Object>> goodsList = new ArrayList<>();
for (Integer rowIndex : groupRows) {
Waybill current = rowWaybills.get(rowIndex);
if (!rowIndex.equals(groupRows.get(0))) {
// 主行字段保留首行值数量等数值字段累加同组其余各行
mergeNumericFields(primary, current);
skipRows.add(rowIndex);
waybillByRow.remove(rowIndex);
}
goodsList.add(buildGoodsItem(rows.get(rowIndex), current));
}
primary.setGoodsJson(JsonUtil.toJson(goodsList));
}
}
/** 合并多货物行时累加数量、其他费用等可加数值字段;为空的字段跳过。 */
private void mergeNumericFields(Waybill primary, Waybill current) {
primary.setQuantity(sumNullable(primary.getQuantity(), current.getQuantity()));
primary.setOtherFeeTotal(sumNullable(primary.getOtherFeeTotal(), current.getOtherFeeTotal()));
}
private BigDecimal sumNullable(BigDecimal first, BigDecimal second) {
if (first == null) return second;
if (second == null) return first;
return first.add(second);
}
/** 由导入行构建一条货物明细,字段与运单详情货物编辑结构保持一致。 */
private Map<String, Object> buildGoodsItem(Map<String, Object> row, Waybill waybill) {
Map<String, Object> goods = new LinkedHashMap<>();
putIfNotEmpty(goods, "cargoName", waybill.getCargoName());
putIfNotEmpty(goods, "cargoType", waybill.getCargoType());
putIfNotEmpty(goods, "specification", waybill.getSpecification());
putIfNotEmpty(goods, "model", waybill.getModel());
putIfNotEmpty(goods, "packageType", stringValue(row, "packageType", "包装"));
if (waybill.getQuantity() != null) {
goods.put("quantity", waybill.getQuantity().toPlainString());
}
putIfNotEmpty(goods, "quantityUnit", waybill.getQuantityUnit());
putIfNotEmpty(goods, "remark", waybill.getRemark());
return goods;
}
private void putIfNotEmpty(Map<String, Object> goods, String key, String value) {
if (Func.isNotEmpty(value)) goods.put(key, value);
}
@Override @Override
public IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request) { public IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request) {
LambdaQueryWrapper<WaybillImportBatch> queryWrapper = Wrappers.<WaybillImportBatch>lambdaQuery() LambdaQueryWrapper<WaybillImportBatch> queryWrapper = Wrappers.<WaybillImportBatch>lambdaQuery()
@@ -465,12 +386,11 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
throw new ServiceException(fieldName + "不能为空"); throw new ServiceException(fieldName + "不能为空");
} }
String text = String.valueOf(value).trim(); String text = String.valueOf(value).trim();
// 宽容解析接受 2026-8-22026-9-1 8:0:0 等写法口径见根工作区 docs/import-spec.md try {
LocalDate date = org.springblade.common.excel.LenientDateParser.parseDateLenientlyOrNull(text); return text.length() == 10 ? LocalDate.parse(text) : LocalDate.parse(text, DATE_TIME_FORMATTER);
if (date == null) { } catch (DateTimeParseException exception) {
throw new ServiceException(fieldName + " 日期格式无法识别:" + text); throw new ServiceException(fieldName + "格式必须为 YYYY-MM-DD HH:mm:ss");
} }
return date;
} }
private BigDecimal defaultQuantity(BigDecimal quantity) { private BigDecimal defaultQuantity(BigDecimal quantity) {
@@ -603,9 +523,6 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
// 16. 同一运单标识号校验 // 16. 同一运单标识号校验
validateWaybillIdentifier(row, i, waybillIdentifierMap, rows, errors); validateWaybillIdentifier(row, i, waybillIdentifierMap, rows, errors);
// 17. 同一运单标识号组内一致性校验地址/配载标识号/数量单位多货物合并的前提
validateWaybillIdentifierConsistency(row, i, waybillIdentifierMap, rows, errors);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
errorMap.put(i, String.join("; ", errors)); errorMap.put(i, String.join("; ", errors));
} }
@@ -654,13 +571,21 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
if (!isRoadTransport(transportType)) { if (!isRoadTransport(transportType)) {
return; return;
} }
// 必填长度与格式统一为一条简短原因避免多条规则叠加导致失败原因过长 if (Func.isEmpty(vehicleNo)) {
// 文案保留车牌号字样导入失败明细仍能据此定位到车牌号列并标红 errors.add("公路运输时车牌号/航班号/船号/班列号不能为空");
// VEHICLE_NO_PATTERN 已约束总长度为 7 8 故不再单独判断长度 return;
if (Func.isEmpty(vehicleNo) }
|| !VEHICLE_PROVINCE_PATTERN.matcher(vehicleNo).lookingAt() if (vehicleNo.length() < 7) {
|| !VEHICLE_NO_PATTERN.matcher(vehicleNo).matches()) { errors.add("车牌号长度不能少于7位");
errors.add("车牌号校验不通过"); }
if (vehicleNo.length() > 8) {
errors.add("车牌号长度不能超过8位");
}
if (!VEHICLE_PROVINCE_PATTERN.matcher(vehicleNo).lookingAt()) {
errors.add("车牌号首位必须是省份简称,第二位必须是英文字母");
}
if (!VEHICLE_NO_PATTERN.matcher(vehicleNo).matches()) {
errors.add("车牌号格式不正确,应为首位省份简称、次位英文字母、总长度7或8位");
} }
} }
@@ -901,62 +826,18 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
} }
} }
/**
* 同一运单标识号组内的发货地址到货地址数量单位配载标识号必须一致
* 多货物行会合并为一条运单上述字段不一致时无法合并数量累加也要求单位相同
*/
private void validateWaybillIdentifierConsistency(Map<String, Object> row, int rowIndex,
Map<String, List<Integer>> waybillIdentifierMap, List<Map<String, Object>> allRows, List<String> errors) {
String waybillIdentifier = stringValue(row, "waybillIdentifier", "同一运单标识号");
if (Func.isEmpty(waybillIdentifier)) {
return;
}
List<Integer> sameIdentifierRows = waybillIdentifierMap.get(waybillIdentifier);
if (sameIdentifierRows == null || sameIdentifierRows.size() <= 1) {
return;
}
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "departureAddress", "发货地址", errors);
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "arrivalAddress", "到货地址", errors);
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "quantityUnit", "数量单位", errors);
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "loadingIdentifier", "配载标识号", errors);
}
/** 比较同一运单标识号组内其余行的字段值,不一致时记录错误;空值不参与比较。 */
private void compareGroupValue(int rowIndex, List<Integer> sameIdentifierRows, List<Map<String, Object>> allRows,
String field, String fieldName, List<String> errors) {
String currentValue = normalizeCompareValue(stringValue(allRows.get(rowIndex), field, fieldName));
if (Func.isEmpty(currentValue)) {
return;
}
for (Integer otherRowIndex : sameIdentifierRows) {
if (otherRowIndex <= rowIndex) {
continue;
}
String otherValue = normalizeCompareValue(stringValue(allRows.get(otherRowIndex), field, fieldName));
if (Func.isNotEmpty(otherValue) && !currentValue.equals(otherValue)) {
errors.add("同一运单标识号下," + fieldName + "必须一致");
return;
}
}
}
private String normalizeCompareValue(String value) {
return Func.isEmpty(value) ? null : value.replaceAll("\\s+", "");
}
private LocalDate parseDateForValidation(Object value, String fieldName, List<String> errors) { private LocalDate parseDateForValidation(Object value, String fieldName, List<String> errors) {
if (value == null || String.valueOf(value).isBlank()) { if (value == null || String.valueOf(value).isBlank()) {
return null; return null;
} }
String text = String.valueOf(value).trim(); String text = String.valueOf(value).trim();
// 宽容解析接受 2026-8-22026-9-1 8:0:0 等写法口径见根工作区 docs/import-spec.md try {
LocalDate date = org.springblade.common.excel.LenientDateParser.parseDateLenientlyOrNull(text); return text.length() == 10 ? LocalDate.parse(text) : LocalDate.parse(text, DATE_TIME_FORMATTER);
if (date == null) { } catch (DateTimeParseException exception) {
errors.add(fieldName + " 日期格式无法识别:" + text); errors.add(fieldName + "格式必须为日期格式(YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss");
return null; return null;
} }
return date;
} }
private Map<String, String> loadTransportTypeOptions() { private Map<String, String> loadTransportTypeOptions() {
@@ -980,8 +861,8 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
// 字典读取失败时使用默认值 // 字典读取失败时使用默认值
} }
if (options.isEmpty()) { if (options.isEmpty()) {
// 运输方式仅支持公路铁路水路航空四种 List<String> defaults = List.of("公路运输", "铁路运输", "水路运输", "航空运输",
List<String> defaults = List.of("公路运输", "铁路运输", "水路运输", "航空运输"); "公路整车", "公路配载/零担", "铁路整车", "铁路零担", "水路", "航空", "多式联运", "管道运输", "其他");
defaults.forEach(value -> options.put(value, value)); defaults.forEach(value -> options.put(value, value));
} }
return options; return options;
@@ -885,13 +885,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
WaybillExcel excel = data.get(index); WaybillExcel excel = data.get(index);
try { try {
Waybill waybill = Objects.requireNonNull(BeanUtil.copyProperties(excel, Waybill.class)); Waybill waybill = Objects.requireNonNull(BeanUtil.copyProperties(excel, Waybill.class));
waybill.setEstimatedStartTime(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEstimatedStartTime(), "预计发货日期"));
waybill.setEstimatedEndTime(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEstimatedEndTime(), "预计完成日期"));
waybill.setStartDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getStartDate(), "开始日期"));
waybill.setEndDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEndDate(), "结束日期"));
waybill.setDataSource("批量导入"); waybill.setDataSource("批量导入");
waybill.setCreateTime(null);
waybill.setUpdateTime(null);
waybill.setCarrierJson(buildImportCarrierJson(waybill)); waybill.setCarrierJson(buildImportCarrierJson(waybill));
submit(waybill); submit(waybill);
} catch (Exception exception) { } catch (Exception exception) {
@@ -65,6 +65,15 @@ public interface IMKClient {
@PostMapping("${thirdParty.mk.getCurrentNodesUrl:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getCurNodesInfo}") @PostMapping("${thirdParty.mk.getCurrentNodesUrl:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getCurNodesInfo}")
MKResultVO<List<MKNodeVO>> getCurrentNodes(@RequestBody MKCurrentNodesDTO param, @RequestParam("access_token") String accessToken); MKResultVO<List<MKNodeVO>> getCurrentNodes(@RequestBody MKCurrentNodesDTO param, @RequestParam("access_token") String accessToken);
/**
* 获取流程实例详情
* @param param
* @param accessToken
* @return
*/
@PostMapping("${thirdParty.mk.getProcessInfoUrl:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getProcessInfo}")
MKResultVO<Object> getProcessInfo(@RequestBody MKCurrentNodesDTO param, @RequestParam("access_token") String accessToken);
/** /**
* 获取节点处理人信息 * 获取节点处理人信息
* @param param * @param param
@@ -1,6 +1,7 @@
package org.springblade.thirdparty.mk.pojo.dto; package org.springblade.thirdparty.mk.pojo.dto;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data; import lombok.Data;
import java.io.Serial; import java.io.Serial;
@@ -44,4 +45,9 @@ public class MKProcessCreateDTO implements Serializable {
*/ */
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
private Object tempVarData; private Object tempVarData;
/**
* 业务类型 ERP 内部使用不传给 MK
*/
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String bizType;
} }
@@ -66,6 +66,14 @@ public interface IMKService {
*/ */
List<MKNodeVO> getCurrentNodes(String processInstanceId, String loginName); List<MKNodeVO> getCurrentNodes(String processInstanceId, String loginName);
/**
* 获取流程实例详情
* @param processInstanceId
* @param loginName
* @return
*/
Object getProcessInfo(String processInstanceId, String loginName);
/** /**
* 获取节点处理人信息 * 获取节点处理人信息
* @param processInstanceId * @param processInstanceId
@@ -246,6 +246,21 @@ public class MKServiceImpl implements IMKService {
return nodesResult.getData(); return nodesResult.getData();
} }
@Override
public Object getProcessInfo(String processInstanceId, String loginName) {
String accessToken = getToken();
MKCurrentNodesDTO param = new MKCurrentNodesDTO();
param.setProcessInstanceId(processInstanceId);
param.setLoginName(loginName);
MKResultVO<Object> processInfoResult = client.getProcessInfo(param, accessToken);
log.info("调用mk接口 获取流程实例详情 :{}", JSONUtil.toJsonStr(processInfoResult));
if (processInfoResult == null || !processInfoResult.isSuccess()) {
log.error("调用mk接口 获取流程实例详情异常:{}", JSONUtil.toJsonStr(processInfoResult));
return null;
}
return processInfoResult.getData();
}
@Override @Override
public MKAllHandlerVO getNodeHandlers(String processInstanceId, String loginName, String nodeId) { public MKAllHandlerVO getNodeHandlers(String processInstanceId, String loginName, String nodeId) {
String accessToken = getToken(); String accessToken = getToken();
+1
View File
@@ -118,6 +118,7 @@ thirdParty:
processExecuteUrl: ${MK_PROCESS_EXECUTE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/execute} processExecuteUrl: ${MK_PROCESS_EXECUTE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/execute}
processDeleteUrl: ${MK_PROCESS_DELETE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/delete} processDeleteUrl: ${MK_PROCESS_DELETE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/delete}
getCurrentNodesUrl: ${MK_CURRENT_NODES_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getCurNodesInfo} getCurrentNodesUrl: ${MK_CURRENT_NODES_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getCurNodesInfo}
getProcessInfoUrl: ${MK_PROCESS_INFO_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getProcessInfo}
getNodeHandlersUrl: ${MK_NODE_HANDLERS_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getNodeHandlerInfos} getNodeHandlersUrl: ${MK_NODE_HANDLERS_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getNodeHandlerInfos}
getManualNodeUrl: ${MK_MANUAL_NODE_URL:/openapi/sys-lbpm/sysLbpmTemplate/openSupport/getManualNode} getManualNodeUrl: ${MK_MANUAL_NODE_URL:/openapi/sys-lbpm/sysLbpmTemplate/openSupport/getManualNode}
pushOrgDeptUrl: ${MK_PUSH_ORG_DEPT_URL:/openapi/sys-org/v2/push/orgDept} pushOrgDeptUrl: ${MK_PUSH_ORG_DEPT_URL:/openapi/sys-org/v2/push/orgDept}
+1
View File
@@ -91,6 +91,7 @@ thirdParty:
processExecuteUrl: ${MK_PROCESS_EXECUTE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/execute} processExecuteUrl: ${MK_PROCESS_EXECUTE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/execute}
processDeleteUrl: ${MK_PROCESS_DELETE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/delete} processDeleteUrl: ${MK_PROCESS_DELETE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/delete}
getCurrentNodesUrl: ${MK_CURRENT_NODES_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getCurNodesInfo} getCurrentNodesUrl: ${MK_CURRENT_NODES_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getCurNodesInfo}
getProcessInfoUrl: ${MK_PROCESS_INFO_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getProcessInfo}
getNodeHandlersUrl: ${MK_NODE_HANDLERS_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getNodeHandlerInfos} getNodeHandlersUrl: ${MK_NODE_HANDLERS_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getNodeHandlerInfos}
getManualNodeUrl: ${MK_MANUAL_NODE_URL:/openapi/sys-lbpm/sysLbpmTemplate/openSupport/getManualNode} getManualNodeUrl: ${MK_MANUAL_NODE_URL:/openapi/sys-lbpm/sysLbpmTemplate/openSupport/getManualNode}
pushOrgDeptUrl: ${MK_PUSH_ORG_DEPT_URL:/openapi/sys-org/v2/push/orgDept} pushOrgDeptUrl: ${MK_PUSH_ORG_DEPT_URL:/openapi/sys-org/v2/push/orgDept}
+5
View File
@@ -218,9 +218,14 @@ blade:
- /blade-auth/oauth/logout/** - /blade-auth/oauth/logout/**
- /blade-transport/customer-archive/public/** - /blade-transport/customer-archive/public/**
- /customer-archive/public/** - /customer-archive/public/**
- /blade-transport/mk-process/public/**
- /mk-process/public/**
- /blade-openapi/openApi/mk/process/commonCallback - /blade-openapi/openApi/mk/process/commonCallback
- /openApi/mk/process/commonCallback - /openApi/mk/process/commonCallback
- /feign/client/businessProcess/getCurrentNodes - /feign/client/businessProcess/getCurrentNodes
- /feign/client/businessProcess/getProcessInfo
- /feign/client/customerArchive/syncProcessNode
- /feign/client/mkProcess/**
#授权认证配置 #授权认证配置
auth: auth:
- method: ALL - method: ALL