Merge remote-tracking branch 'refs/remotes/origin/master' into feature/payment-apply-bfi-sync

# Conflicts:
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
This commit is contained in:
刘泉佳
2026-10-01 13:47:00 +08:00
206 changed files with 4885 additions and 4913 deletions
@@ -128,10 +128,28 @@ public class AccidentRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入事故记录", description = "传入excel")
public R importAccidentRecord(MultipartFile file, HttpServletResponse response) {
List<AccidentRecordExcel> failureList = accidentRecordService.importAccidentRecord(ExcelUtil.read(file, AccidentRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "事故记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, AccidentRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
accidentRecordService.importAccidentRecord(ExcelUtil.read(file, AccidentRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "事故记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), AccidentRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "事故记录导入失败明细" + DateUtil.time(), "导入失败明细", file, AccidentRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -125,10 +125,28 @@ public class AnnualInspectionRecordController extends BladeController {
@ApiOperationSupport(order = 6)
@Operation(summary = "导入年检记录", description = "传入excel")
public R importAnnualInspectionRecord(MultipartFile file, HttpServletResponse response) {
List<AnnualInspectionRecordExcel> failureList = annualInspectionRecordService.importAnnualInspectionRecord(ExcelUtil.read(file, AnnualInspectionRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "年检记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, AnnualInspectionRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
annualInspectionRecordService.importAnnualInspectionRecord(ExcelUtil.read(file, AnnualInspectionRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "年检记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), AnnualInspectionRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "年检记录导入失败明细" + DateUtil.time(), "导入失败明细", file, AnnualInspectionRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -33,11 +33,9 @@ import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
@@ -46,6 +44,7 @@ import org.springblade.transport.pojo.entity.CommonAddress;
import org.springblade.transport.pojo.vo.CommonAddressRemoveResultVO;
import org.springblade.transport.pojo.vo.CommonAddressVO;
import org.springblade.transport.service.ICommonAddressService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -54,7 +53,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Objects;
/**
* 常用地址 控制器
@@ -164,15 +162,9 @@ public class CommonAddressController extends BladeController {
if (Func.isNotEmpty(ids)) {
queryWrapper.in(CommonAddress::getId, Func.toLongList(ids));
}
if (Objects.equals(commonAddress.getAllDept(), 1)) {
if (!AuthUtil.isAdministrator()) {
throw new ServiceException("无权导出全部组织常用地址");
}
} else {
queryWrapper.eq(CommonAddress::getDeptId, currentDeptId());
}
if (Objects.equals(commonAddress.getAllDept(), 1) && Func.isNotEmpty(commonAddress.getDeptId())) {
queryWrapper.eq(CommonAddress::getDeptId, commonAddress.getDeptId());
List<Long> deptIdList = TransportBusinessSupport.resolveDeptFilterIds(commonAddress.getAllDept(), commonAddress.getDeptId(), "常用地址");
if (Func.isNotEmpty(deptIdList)) {
queryWrapper.eq(CommonAddress::getDeptId, deptIdList.get(0));
}
if (Func.isNotEmpty(commonAddress.getAddressName())) {
queryWrapper.like(CommonAddress::getAddressName, commonAddress.getAddressName().trim());
@@ -198,12 +190,4 @@ public class CommonAddressController extends BladeController {
return queryWrapper;
}
private Long currentDeptId() {
Long deptId = Func.firstLong(AuthUtil.getDeptId());
if (Func.isEmpty(deptId) || deptId <= 0) {
throw new ServiceException("当前用户所属组织为空,无法导出常用地址");
}
return deptId;
}
}
@@ -38,9 +38,9 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.transport.excel.CommonCargoExcel;
import org.springblade.transport.excel.CommonCargoExportExcel;
import org.springblade.transport.excel.CommonCargoImportFailureExcel;
import org.springframework.web.multipart.MultipartFile;
import org.springblade.transport.pojo.entity.CommonCargo;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
@@ -110,14 +110,41 @@ public class CommonCargoController extends BladeController {
@ApiOperationSupport(order = 6)
@Operation(summary = "导入常用货物", description = "传入excel")
public R importCommonCargo(MultipartFile file, HttpServletResponse response) {
List<CommonCargoImportFailureExcel> failureList = commonCargoService.importCommonCargo(ExcelUtil.read(file, CommonCargoExcel.class));
if (Func.isNotEmpty(failureList)) {
ImportFailureExcelUtil.export(response, "常用货物导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CommonCargoImportFailureExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
commonCargoService.importCommonCargo(ExcelUtil.read(file, CommonCargoExcel.class));
} catch (ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
exportFailure(response, exception.getFailureList());
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "常用货物导入失败明细" + DateUtil.time(), "导入失败明细", file, CommonCargoExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("导入数据成功");
}
/**
* 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。
* <p>
* 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。
*/
private void exportFailure(HttpServletResponse response, List<?> failureList) {
ImportFailureExcelUtil.exportFailureReasonOnly(response, "常用货物导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CommonCargoExcel.class);
}
@GetMapping("/export-template")
@ApiOperationSupport(order = 7)
@Operation(summary = "导出模板")
@@ -38,6 +38,7 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.transport.excel.CommonRouteExportExcel;
import org.springblade.transport.excel.CommonRouteImportExcel;
import org.springblade.transport.pojo.entity.CommonRoute;
@@ -109,18 +110,34 @@ public class CommonRouteController extends BladeController {
@ApiOperationSupport(order = 6)
@Operation(summary = "导入常用线路", description = "传入excel")
public R importCommonRoute(MultipartFile file, HttpServletResponse response) {
List<CommonRouteImportExcel> failureList = commonRouteService.importCommonRoute(
ExcelUtil.read(file, CommonRouteImportExcel.class)
);
if (Func.isNotEmpty(failureList)) {
ImportFailureExcelUtil.export(
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
commonRouteService.importCommonRoute(ExcelUtil.read(file, CommonRouteImportExcel.class));
} catch (ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
ImportFailureExcelUtil.exportFailureReasonOnly(
response,
"常用线路导入失败明细" + DateUtil.time(),
"导入失败明细",
failureList,
exception.getFailureList(),
CommonRouteImportExcel.class
);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "常用线路导入失败明细" + DateUtil.time(), "导入失败明细", file, CommonRouteImportExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("导入数据成功");
}
@@ -138,10 +138,28 @@ public class CreditScoreQuantificationController extends BladeController {
@ApiOperationSupport(order = 7)
@Operation(summary = "导入评分量化表", description = "传入excel")
public R importCreditScoreQuantification(MultipartFile file, HttpServletResponse response) {
List<CreditScoreQuantificationExcel> failureList = creditScoreQuantificationService.importCreditScoreQuantification(ExcelUtil.read(file, CreditScoreQuantificationExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "评分量化表导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CreditScoreQuantificationExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
creditScoreQuantificationService.importCreditScoreQuantification(ExcelUtil.read(file, CreditScoreQuantificationExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一分组失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "评分量化表导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), CreditScoreQuantificationExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "评分量化表导入失败明细" + DateUtil.time(), "导入失败明细", file, CreditScoreQuantificationExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -38,6 +38,7 @@ import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
@@ -220,10 +221,12 @@ public class CustomerArchiveController extends BladeController {
LambdaQueryWrapper<CustomerArchive> queryWrapper = Wrappers.<CustomerArchive>lambdaQuery()
.eq(CustomerArchive::getIsDeleted, 0)
.orderByDesc(CustomerArchive::getCreateTime);
Long userId = AuthUtil.getUserId();
queryWrapper.and(wrapper -> wrapper
.exists("SELECT 1 FROM blade_user_customer_scope bucs WHERE bucs.user_id = " + userId
+ " AND bucs.customer_id = blade_customer_archive.id"));
if (!TransportBusinessSupport.isAdministrator()) {
Long userId = AuthUtil.getUserId();
queryWrapper.and(wrapper -> wrapper
.exists("SELECT 1 FROM blade_user_customer_scope bucs WHERE bucs.user_id = " + userId
+ " AND bucs.customer_id = blade_customer_archive.id"));
}
if (Func.isNotEmpty(ids)) {
queryWrapper.in(CustomerArchive::getId, Func.toLongList(ids));
}
@@ -30,12 +30,16 @@ 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.log.exception.ServiceException;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
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.core.tool.api.R;
import org.springblade.process.feign.IBusinessProcessClient;
import org.springblade.process.pojo.vo.MkFormBindVO;
import org.springblade.transport.pojo.vo.CustomerArchiveVO;
import org.springblade.transport.pojo.vo.CustomerChangeRecordVO;
import org.springblade.transport.service.ICustomerArchiveService;
@@ -65,15 +69,34 @@ public class CustomerArchivePublicController {
private final ICustomerArchiveService customerArchiveService;
private final CustomerArchivePublicProcessService customerArchivePublicProcessService;
private final IBusinessProcessClient businessProcessClient;
/**
* 公开详情
*/
@GetMapping("/detail")
@ApiOperationSupport(order = 1)
@Operation(summary = "公开详情", description = "传入id,无需登录")
public R<CustomerArchiveVO> detail(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.data(customerArchiveService.publicDetail(id));
@Operation(summary = "公开详情", description = "传入表单实例id,无需登录;内部反查业务主键后加载详情")
public R<CustomerArchiveVO> detail(@Parameter(description = "表单实例id", required = true) @RequestParam Long id) {
return R.data(customerArchiveService.publicDetail(resolveBizId(id)));
}
/**
* 公开页传入的 id 为 formInstanceId,反查业务主键;查不到时兼容历史(id 即 bizId)
*/
private Long resolveBizId(Long formInstanceId) {
if (formInstanceId == null) {
throw new ServiceException("表单实例id不能为空");
}
try {
FR<MkFormBindVO> result = businessProcessClient.resolveByFormInstanceId(formInstanceId);
if (result != null && result.isSuccess() && result.getData() != null && result.getData().getBizId() != null) {
return result.getData().getBizId();
}
} catch (Exception e) {
log.warn("按表单实例id解析客商业务主键失败,回退按业务主键查询 formInstanceId={}", formInstanceId, e);
}
return formInstanceId;
}
/**
@@ -90,10 +90,28 @@ public class EquipmentLedgerController extends BladeController {
@ApiOperationSupport(order = 6)
@Operation(summary = "导入设备台账")
public R importEquipmentLedger(MultipartFile file, HttpServletResponse response) {
List<EquipmentLedgerExcel> failureList = equipmentLedgerService.importEquipmentLedger(ExcelUtil.read(file, EquipmentLedgerExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "设备台账导入失败明细" + DateUtil.time(), "导入失败明细", failureList, EquipmentLedgerExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
equipmentLedgerService.importEquipmentLedger(ExcelUtil.read(file, EquipmentLedgerExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "设备台账导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), EquipmentLedgerExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "设备台账导入失败明细" + DateUtil.time(), "导入失败明细", file, EquipmentLedgerExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -94,10 +94,28 @@ public class EtcRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入ETC记录", description = "传入excel")
public R importEtcRecord(MultipartFile file, HttpServletResponse response) {
List<EtcRecordExcel> failureList = etcRecordService.importEtcRecord(ExcelUtil.read(file, EtcRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "ETC记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, EtcRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
etcRecordService.importEtcRecord(ExcelUtil.read(file, EtcRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "ETC记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), EtcRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "ETC记录导入失败明细" + DateUtil.time(), "导入失败明细", file, EtcRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -129,10 +129,28 @@ public class InsuranceRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入保险记录", description = "传入excel")
public R importInsuranceRecord(MultipartFile file, HttpServletResponse response) {
List<InsuranceRecordExcel> failureList = insuranceRecordService.importInsuranceRecord(ExcelUtil.read(file, InsuranceRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保险记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, InsuranceRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
insuranceRecordService.importInsuranceRecord(ExcelUtil.read(file, InsuranceRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保险记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), InsuranceRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "保险记录导入失败明细" + DateUtil.time(), "导入失败明细", file, InsuranceRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -128,10 +128,28 @@ public class MaintenancePlanController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入保养记录", description = "传入excel")
public R importMaintenancePlan(MultipartFile file, HttpServletResponse response) {
List<MaintenancePlanExcel> failureList = maintenancePlanService.importMaintenancePlan(ExcelUtil.read(file, MaintenancePlanExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保养记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, MaintenancePlanExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
maintenancePlanService.importMaintenancePlan(ExcelUtil.read(file, MaintenancePlanExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保养计划导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MaintenancePlanExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "保养计划导入失败明细" + DateUtil.time(), "导入失败明细", file, MaintenancePlanExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -128,10 +128,28 @@ public class MaintenanceRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入维修记录", description = "传入excel")
public R importMaintenanceRecord(MultipartFile file, HttpServletResponse response) {
List<MaintenanceRecordExcel> failureList = maintenanceRecordService.importMaintenanceRecord(ExcelUtil.read(file, MaintenanceRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "维修记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, MaintenanceRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
maintenanceRecordService.importMaintenanceRecord(ExcelUtil.read(file, MaintenanceRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "维修记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MaintenanceRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "维修记录导入失败明细" + DateUtil.time(), "导入失败明细", file, MaintenanceRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -116,10 +116,28 @@ public class MileageRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入里程记录", description = "传入excel")
public R importMileageRecord(MultipartFile file, HttpServletResponse response) {
List<MileageRecordExcel> failureList = mileageRecordService.importMileageRecord(ExcelUtil.read(file, MileageRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "里程记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, MileageRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
mileageRecordService.importMileageRecord(ExcelUtil.read(file, MileageRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "里程记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MileageRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "里程记录导入失败明细" + DateUtil.time(), "导入失败明细", file, MileageRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -7,10 +7,14 @@ 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.log.exception.ServiceException;
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.core.tool.api.R;
import org.springblade.process.feign.IBusinessProcessClient;
import org.springblade.process.pojo.vo.MkFormBindVO;
import org.springblade.transport.mk.MkProcessMessageService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -35,13 +39,33 @@ import java.util.Map;
public class MkProcessPublicController {
private final MkProcessMessageService mkProcessMessageService;
private final IBusinessProcessClient businessProcessClient;
@GetMapping("/{bizType}/detail")
@ApiOperationSupport(order = 1)
@Operation(summary = "公开详情", description = "无需登录")
@Operation(summary = "公开详情", description = "无需登录;id 为 MK 表单实例id,内部反查业务主键后加载详情")
public R detail(@PathVariable String bizType,
@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.data(mkProcessMessageService.handler(bizType).publicDetail(id));
@Parameter(description = "表单实例id", required = true) @RequestParam Long id) {
Long bizId = resolveBizId(id);
return R.data(mkProcessMessageService.handler(bizType).publicDetail(bizId));
}
/**
* 公开页传入的 id 为 formInstanceId,反查业务主键;查不到时兼容历史(id 即 bizId)
*/
private Long resolveBizId(Long formInstanceId) {
if (formInstanceId == null) {
throw new ServiceException("表单实例id不能为空");
}
try {
FR<MkFormBindVO> result = businessProcessClient.resolveByFormInstanceId(formInstanceId);
if (result != null && result.isSuccess() && result.getData() != null && result.getData().getBizId() != null) {
return result.getData().getBizId();
}
} catch (Exception e) {
log.warn("按表单实例id解析业务主键失败,回退按业务主键查询 formInstanceId={}", formInstanceId, e);
}
return formInstanceId;
}
@PostMapping("/{bizType}/process-message")
@@ -94,10 +94,28 @@ public class OilElectricRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入油电记录", description = "传入excel")
public R importOilElectricRecord(MultipartFile file, HttpServletResponse response) {
List<OilElectricRecordExcel> failureList = oilElectricRecordService.importOilElectricRecord(ExcelUtil.read(file, OilElectricRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "油电记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, OilElectricRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
oilElectricRecordService.importOilElectricRecord(ExcelUtil.read(file, OilElectricRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "油电记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), OilElectricRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "油电记录导入失败明细" + DateUtil.time(), "导入失败明细", file, OilElectricRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -94,10 +94,28 @@ public class OtherExpenseRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入其他费用记录", description = "传入excel")
public R importOtherExpenseRecord(MultipartFile file, HttpServletResponse response) {
List<OtherExpenseRecordExcel> failureList = otherExpenseRecordService.importOtherExpenseRecord(ExcelUtil.read(file, OtherExpenseRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "其他费用记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, OtherExpenseRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
otherExpenseRecordService.importOtherExpenseRecord(ExcelUtil.read(file, OtherExpenseRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "其他费用记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), OtherExpenseRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "其他费用记录导入失败明细" + DateUtil.time(), "导入失败明细", file, OtherExpenseRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -51,7 +51,9 @@ import org.springblade.transport.pojo.entity.VoucherManage;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.ProcessConfigVO;
import org.springblade.transport.pojo.vo.VoucherFolderVO;
import org.springblade.transport.service.IProcessConfigService;
import org.springblade.transport.service.IVoucherManageService;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
@@ -80,6 +82,7 @@ import java.util.concurrent.TimeUnit;
public class ProcessConfigController extends BladeController {
private final IProcessConfigService processConfigService;
private final IVoucherManageService voucherManageService;
private final WaybillMapper waybillMapper;
private final VoucherFileMapper voucherFileMapper;
private final VoucherImageMapper voucherImageMapper;
@@ -88,11 +91,13 @@ public class ProcessConfigController extends BladeController {
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
private String minioBucketName;
public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper,
public ProcessConfigController(IProcessConfigService processConfigService, IVoucherManageService voucherManageService,
WaybillMapper waybillMapper,
VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper,
VoucherManageMapper voucherManageMapper,
ObjectProvider<MinioClient> minioClientProvider) {
this.processConfigService = processConfigService;
this.voucherManageService = voucherManageService;
this.waybillMapper = waybillMapper;
this.voucherFileMapper = voucherFileMapper;
this.voucherImageMapper = voucherImageMapper;
@@ -128,6 +133,23 @@ public class ProcessConfigController extends BladeController {
return R.data(detail);
}
@GetMapping("/voucher-folders")
@ApiOperationSupport(order = 2)
@Operation(summary = "查询运单执行凭证文件夹")
public R<List<VoucherFolderVO>> voucherFolders(
@Parameter(description = "运单主键", required = true) @RequestParam Long waybillId) {
return R.data(voucherManageService.listWaybillFolders(waybillId));
}
@GetMapping("/voucher-folder-detail")
@ApiOperationSupport(order = 2)
@Operation(summary = "查询运单执行凭证文件夹详情")
public R<VoucherFolderVO> voucherFolderDetail(
@Parameter(description = "凭证批次主键", required = true) @RequestParam Long voucherId,
@Parameter(description = "车牌号", required = true) @RequestParam String plateNo) {
return R.data(voucherManageService.folderDetail(voucherId, plateNo));
}
@GetMapping("/voucher-images")
@ApiOperationSupport(order = 2)
@Operation(summary = "查询运单已关联凭证图片")
@@ -128,10 +128,28 @@ public class TireReplacementRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入换胎记录", description = "传入excel")
public R importTireReplacementRecord(MultipartFile file, HttpServletResponse response) {
List<TireReplacementRecordExcel> failureList = tireReplacementRecordService.importTireReplacementRecord(ExcelUtil.read(file, TireReplacementRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "换胎记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, TireReplacementRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
tireReplacementRecordService.importTireReplacementRecord(ExcelUtil.read(file, TireReplacementRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "换胎记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TireReplacementRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "换胎记录导入失败明细" + DateUtil.time(), "导入失败明细", file, TireReplacementRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -113,10 +113,28 @@ public class TransportChangeRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入变更记录", description = "传入excel")
public R importTransportChangeRecord(MultipartFile file, HttpServletResponse response) {
List<TransportChangeRecordExcel> failureList = transportChangeRecordService.importTransportChangeRecord(ExcelUtil.read(file, TransportChangeRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "变更记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, TransportChangeRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
transportChangeRecordService.importTransportChangeRecord(ExcelUtil.read(file, TransportChangeRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "运输变更记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TransportChangeRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "运输变更记录导入失败明细" + DateUtil.time(), "导入失败明细", file, TransportChangeRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -147,11 +147,29 @@ public class TransportPlanController extends BladeController {
public R importTransportPlan(MultipartFile file, @RequestParam Long projectId, @RequestParam String projectName,
@RequestParam Long contractId, @RequestParam String contractName, @RequestParam String customerName,
HttpServletResponse response) {
List<TransportPlanImportExcel> failureList = transportPlanService.importTransportPlan(
ExcelUtil.read(file, TransportPlanImportExcel.class), projectId, projectName, contractId, contractName, customerName);
if (Func.isNotEmpty(failureList)) {
ImportFailureExcelUtil.export(response, "运输计划导入失败明细" + DateUtil.time(), "导入失败明细", failureList, TransportPlanImportExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
transportPlanService.importTransportPlan(
ExcelUtil.read(file, TransportPlanImportExcel.class), projectId, projectName, contractId, contractName, customerName);
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
ImportFailureExcelUtil.export(response, "运输计划导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TransportPlanImportExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "运输计划导入失败明细" + DateUtil.time(), "导入失败明细", file, TransportPlanImportExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("导入数据成功");
}
@@ -19,6 +19,7 @@ import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.transport.excel.CargoReconciliationExcel;
import org.springblade.transport.excel.CargoReconciliationFeeReader;
@@ -126,13 +127,32 @@ public class TransportReconciliationController extends BladeController {
@ApiOperationSupport(order = 6)
@Operation(summary = "导入整车总额外部账单")
public R importVehicle(@RequestParam Long id, MultipartFile file, HttpServletResponse response) {
List<VehicleReconciliationExcel> rows = ExcelUtil.read(file, VehicleReconciliationExcel.class);
List<Map<String, BigDecimal>> feeItems = VehicleReconciliationFeeReader.read(file);
for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index));
List<VehicleReconciliationFailureExcel> failures = reconciliationService.importVehicles(id, rows);
if (!failures.isEmpty()) {
ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, VehicleReconciliationFailureExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
List<VehicleReconciliationExcel> rows = ExcelUtil.read(file, VehicleReconciliationExcel.class);
List<Map<String, BigDecimal>> feeItems = VehicleReconciliationFeeReader.read(file);
for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index));
reconciliationService.importVehicles(id, rows);
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 按原表真实表头导出全量列(含动态费用),修正后可直接重新导入
ImportFailureExcelUtil.exportOriginalWithFailures(response, "运输对账导入失败明细" + DateUtil.time(),
"导入失败明细", file, exception.getFailureList(), VehicleReconciliationExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", file, VehicleReconciliationExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("导入数据成功");
}
@@ -141,13 +161,32 @@ public class TransportReconciliationController extends BladeController {
@ApiOperationSupport(order = 7)
@Operation(summary = "导入货物明细外部账单")
public R importCargo(@RequestParam Long id, MultipartFile file, HttpServletResponse response) {
List<CargoReconciliationExcel> rows = ExcelUtil.read(file, CargoReconciliationExcel.class);
List<Map<String, BigDecimal>> feeItems = CargoReconciliationFeeReader.read(file);
for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index));
List<CargoReconciliationFailureExcel> failures = reconciliationService.importCargoes(id, rows);
if (!failures.isEmpty()) {
ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, CargoReconciliationFailureExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
List<CargoReconciliationExcel> rows = ExcelUtil.read(file, CargoReconciliationExcel.class);
List<Map<String, BigDecimal>> feeItems = CargoReconciliationFeeReader.read(file);
for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index));
reconciliationService.importCargoes(id, rows);
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 按原表真实表头导出全量列(含动态费用),修正后可直接重新导入
ImportFailureExcelUtil.exportOriginalWithFailures(response, "运输对账导入失败明细" + DateUtil.time(),
"导入失败明细", file, exception.getFailureList(), CargoReconciliationExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", file, CargoReconciliationExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("导入数据成功");
}
@@ -127,10 +127,28 @@ public class ViolationRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入违章记录", description = "传入excel")
public R importViolationRecord(MultipartFile file, HttpServletResponse response) {
List<ViolationRecordImportExcel> failureList = violationRecordService.importViolationRecord(ExcelUtil.read(file, ViolationRecordImportExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "违章记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, ViolationRecordImportExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
violationRecordService.importViolationRecord(ExcelUtil.read(file, ViolationRecordImportExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "违章记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), ViolationRecordImportExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "违章记录导入失败明细" + DateUtil.time(), "导入失败明细", file, ViolationRecordImportExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -34,6 +34,7 @@ import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.apache.poi.ss.usermodel.CellStyle;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.core.mp.support.Condition;
@@ -53,6 +54,7 @@ import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.LoadingManageVO;
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
import org.springblade.transport.pojo.vo.WaybillLocateVO;
import org.springblade.transport.pojo.vo.WaybillTrackVO;
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
@@ -191,15 +193,22 @@ public class WaybillController extends BladeController {
@PostMapping("/import-batch/validate")
@ApiOperationSupport(order = 6)
@Operation(summary = "校验运单批量导入数据")
public void validateImportBatch(@RequestBody WaybillImportBatchRequest request, HttpServletResponse response) {
waybillImportBatchService.validate(request, response);
public R<WaybillImportValidateResultVO> validateImportBatch(@RequestBody WaybillImportBatchRequest request) {
return R.data(waybillImportBatchService.validate(request));
}
@PostMapping("/import-batch/export-failures")
@ApiOperationSupport(order = 6)
@Operation(summary = "导出运单批量导入失败明细")
public void exportImportFailures(@RequestBody WaybillImportBatchRequest request, HttpServletResponse response) {
waybillImportBatchService.exportFailures(request, response);
}
@PostMapping("/import-batch/confirm")
@ApiOperationSupport(order = 7)
@Operation(summary = "确认运单批量导入")
public void confirmImportBatch(@RequestBody WaybillImportBatchRequest request, HttpServletResponse response) {
waybillImportBatchService.confirm(request, response);
public R<WaybillImportBatchVO> confirmImportBatch(@RequestBody WaybillImportBatchRequest request) {
return R.data(waybillImportBatchService.confirm(request));
}
@PostMapping("/import-batch/remove")
@@ -242,10 +251,28 @@ public class WaybillController extends BladeController {
@ApiOperationSupport(order = 13)
@Operation(summary = "导入运单管理", description = "传入excel")
public R importWaybill(MultipartFile file, HttpServletResponse response) {
List<WaybillExcel> failureList = waybillService.importWaybill(ExcelUtil.read(file, WaybillExcel.class));
if (Func.isNotEmpty(failureList)) {
ImportFailureExcelUtil.export(response, "运单管理导入失败明细" + DateUtil.time(), "导入失败明细", failureList, WaybillExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
waybillService.importWaybill(ExcelUtil.read(file, WaybillExcel.class));
} catch (ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
ImportFailureExcelUtil.exportFailureReasonOnly(response, "运单管理导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), WaybillExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "运单管理导入失败明细" + DateUtil.time(), "导入失败明细", file, WaybillExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("导入数据成功");
}
@@ -81,6 +81,9 @@ public class AccidentRecordExportExcel implements Serializable {
@ExcelProperty("事故原因及损坏情况")
private String accidentReasonDamage;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -81,6 +81,9 @@ public class AnnualInspectionRecordExportExcel implements Serializable {
@ExcelProperty("评定(复核)单位")
private String assessmentUnit;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -13,6 +13,7 @@ import cn.idev.excel.FastExcel;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.event.AnalysisEventListener;
import cn.idev.excel.metadata.data.ReadCellData;
import org.springblade.common.excel.ImportCellReadException;
import org.springblade.core.log.exception.ServiceException;
import org.springframework.web.multipart.MultipartFile;
@@ -60,10 +61,11 @@ public final class CargoReconciliationFeeReader {
@Override
public void invoke(Map<Integer, ReadCellData<?>> row, AnalysisContext context) {
Map<String, BigDecimal> values = new LinkedHashMap<>();
Integer excelRowIndex = context.readRowHolder() == null ? null : context.readRowHolder().getRowIndex();
if (freightColumn >= 0 && settlementColumn > freightColumn) {
for (int column = freightColumn + 1; column < settlementColumn; column++) {
String name = headers.get(column);
if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column)));
if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column), name.trim(), excelRowIndex));
}
}
feeItems.add(values);
@@ -80,7 +82,7 @@ public final class CargoReconciliationFeeReader {
.findFirst().orElse(-1);
}
private BigDecimal decimal(ReadCellData<?> cellData) {
private BigDecimal decimal(ReadCellData<?> cellData, String columnName, Integer excelRowIndex) {
if (cellData == null) return BigDecimal.ZERO.setScale(2);
Object value = cellData.getData();
if (value == null) {
@@ -95,7 +97,10 @@ public final class CargoReconciliationFeeReader {
try {
return new BigDecimal(String.valueOf(value).trim()).setScale(2);
} catch (NumberFormatException exception) {
return BigDecimal.ZERO.setScale(2);
// 这里绝不能静默返回 0:费用列会进 feeItemsJson 参与结算金额与自动匹配,
// 把「一百元」这类写法悄悄变成 0.00 等于往库里写脏数据。
// 改为抛带位置信息的异常,由 Controller 的读取阶段兜底导出原表全量明细。
throw new ImportCellReadException(excelRowIndex, columnName, String.valueOf(value).trim());
}
}
@@ -88,6 +88,9 @@ public class CommonAddressExportExcel implements Serializable {
@ExcelProperty("备注")
private String remark;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@@ -73,7 +73,7 @@ public class CommonCargoExcel implements Serializable {
@ExcelProperty("规格")
private String specification;
@ExcelProperty("型号")
@ExcelProperty("*型号")
private String model;
@ExcelProperty("单价")
@@ -93,6 +93,9 @@ public class CommonCargoExportExcel implements Serializable {
@ExcelProperty("组织")
private String deptName;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@@ -93,6 +93,9 @@ public class CustomerArchiveExcel implements Serializable {
@ExcelProperty("法人/负责人")
private String legalPerson;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
private Date createTime;
@@ -35,6 +35,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Date;
/**
* 司机管理 Excel
@@ -128,4 +129,10 @@ public class DriverExcel implements Serializable {
@ExcelProperty("所属组织 *")
private String organizationName;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
private Date createTime;
}
@@ -7,10 +7,12 @@ package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 设备台账 Excel
@@ -29,5 +31,7 @@ public class EquipmentLedgerExcel implements Serializable {
@ExcelProperty("规格型号") private String specificationModel;
@ExcelProperty(value = "出厂日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) private String factoryDate;
@ExcelProperty("备注") private String remark;
@ExcelProperty("创建人") private String createUserName;
@ExcelProperty("创建时间") @DateTimeFormat("yyyy-MM-dd HH:mm:ss") private Date createTime;
@ExcelIgnore private String errorMessage;
}
@@ -49,6 +49,9 @@ public class EtcRecordExportExcel implements Serializable {
@ExcelProperty("出口站")
private String exitStation;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -81,6 +81,9 @@ public class InsuranceRecordExportExcel implements Serializable {
@ExcelProperty("开票日期")
private LocalDate invoiceDate;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -34,7 +34,7 @@ public class LoadingManageExcel implements Serializable {
private String loadingNo;
@ExcelProperty("配载子单号")
private String loadingSubNos;
@ExcelProperty("车牌号/航班号/船号/班列号")
@ExcelProperty("车牌号/船班号/船次号/班次号")
private String vehicleNo;
@ExcelProperty("司机")
private String driverName;
@@ -54,6 +54,8 @@ public class LoadingManageExcel implements Serializable {
private String transportType;
@ExcelProperty("数据来源")
private String dataSource;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -24,14 +24,17 @@ public class MaintenancePlanExportExcel extends MaintenancePlanExcel {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "创建时间", index = 12)
@ExcelProperty(value = "创建人", index = 12)
private String createUserName;
@ExcelProperty(value = "创建时间", index = 13)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@ExcelProperty(value = "更新人", index = 13)
@ExcelProperty(value = "更新人", index = 14)
private String updateUserName;
@ExcelProperty(value = "更新时间", index = 14)
@ExcelProperty(value = "更新时间", index = 15)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
@@ -96,6 +96,9 @@ public class MaintenanceRecordExcel implements Serializable {
@NumberFormat("0.00")
private BigDecimal mileage;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -59,7 +59,7 @@ public class MasterOrderWaybillExcel {
private String carrierName;
@ExcelProperty("司机")
private String driverName;
@ExcelProperty("车牌号/航班号/船号/班列号")
@ExcelProperty("车牌号/船班号/船次号/班次号")
private String vehicleNo;
@ExcelProperty("开始日期")
private LocalDate startDate;
@@ -69,6 +69,10 @@ public class MasterOrderWaybillExcel {
private String businessStatus;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("更新时间")
private Date updateTime;
@ExcelProperty("创建时间")
private Date createTime;
}
@@ -70,6 +70,9 @@ public class MileageRecordExportExcel implements Serializable {
@NumberFormat("0.00")
private BigDecimal totalMileage;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -60,6 +60,9 @@ public class OilElectricRecordExportExcel implements Serializable {
@ExcelProperty("站点")
private String station;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -41,6 +41,9 @@ public class OtherExpenseRecordExportExcel implements Serializable {
@ExcelProperty("金额")
private BigDecimal amount;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -74,6 +74,9 @@ public class TireReplacementRecordExportExcel implements Serializable {
@ExcelProperty("换胎说明")
private String replacementDescription;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -55,6 +55,9 @@ public class TransportChangeRecordExportExcel implements Serializable {
@ExcelProperty("变更内容")
private String changeContent;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -36,6 +36,7 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Date;
/**
* 船舶管理 Excel
@@ -144,4 +145,10 @@ public class TransportShipExcel implements Serializable {
@ExcelProperty("船舶经营人")
private String shipOperator;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
private Date createTime;
}
@@ -35,6 +35,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Date;
/**
* 车辆管理 Excel
@@ -83,4 +84,10 @@ public class TransportVehicleExcel implements Serializable {
@ExcelProperty("年审有效期")
private LocalDate annualReviewEndDate;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
private Date createTime;
}
@@ -13,6 +13,7 @@ import cn.idev.excel.FastExcel;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.event.AnalysisEventListener;
import cn.idev.excel.metadata.data.ReadCellData;
import org.springblade.common.excel.ImportCellReadException;
import org.springblade.core.log.exception.ServiceException;
import org.springframework.web.multipart.MultipartFile;
@@ -60,10 +61,11 @@ public final class VehicleReconciliationFeeReader {
@Override
public void invoke(Map<Integer, ReadCellData<?>> row, AnalysisContext context) {
Map<String, BigDecimal> values = new LinkedHashMap<>();
Integer excelRowIndex = context.readRowHolder() == null ? null : context.readRowHolder().getRowIndex();
if (freightColumn >= 0 && settlementColumn > freightColumn) {
for (int column = freightColumn + 1; column < settlementColumn; column++) {
String name = headers.get(column);
if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column)));
if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column), name.trim(), excelRowIndex));
}
}
feeItems.add(values);
@@ -80,7 +82,7 @@ public final class VehicleReconciliationFeeReader {
.findFirst().orElse(-1);
}
private BigDecimal decimal(ReadCellData<?> cellData) {
private BigDecimal decimal(ReadCellData<?> cellData, String columnName, Integer excelRowIndex) {
if (cellData == null) return BigDecimal.ZERO.setScale(2);
Object value = cellData.getData();
if (value == null) {
@@ -95,7 +97,10 @@ public final class VehicleReconciliationFeeReader {
try {
return new BigDecimal(String.valueOf(value).trim()).setScale(2);
} catch (NumberFormatException exception) {
return BigDecimal.ZERO.setScale(2);
// 这里绝不能静默返回 0:费用列会进 feeItemsJson 参与结算金额与自动匹配,
// 把「一百元」这类写法悄悄变成 0.00 等于往库里写脏数据。
// 改为抛带位置信息的异常,由 Controller 的读取阶段兜底导出原表全量明细。
throw new ImportCellReadException(excelRowIndex, columnName, String.valueOf(value).trim());
}
}
@@ -94,6 +94,9 @@ public class ViolationRecordExcel implements Serializable {
@ExcelProperty("处理结果")
private String processResult;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -98,7 +98,7 @@ public class WaybillExcel implements Serializable {
private String driverName;
@ExcelProperty("司机手机号")
private String driverPhone;
@ExcelProperty("车/船/航班/班列号")
@ExcelProperty("车牌号/船班号/船次号/班次号")
private String vehicleNo;
@ExcelProperty("挂车车牌号")
private String trailerVehicleNo;
@@ -28,7 +28,7 @@ public class WaybillImportBatchExcel implements Serializable {
private String originalNo;
@ExcelProperty("配载标识号")
private String loadingIdentifier;
@ExcelProperty("*车牌号/航班号/船号/班列号")
@ExcelProperty("*车牌号/船班号/船次号/班次号")
private String vehicleNo;
@ExcelProperty("*运输方式")
private String transportType;
@@ -42,8 +42,8 @@
accident_location,
accident_nature,
accident_responsibility,
CASE WHEN direct_economic_loss &lt; 0 THEN 0 ELSE direct_economic_loss END AS direct_economic_loss,
CASE WHEN insurance_claim_amount &lt; 0 THEN 0 ELSE insurance_claim_amount END AS insurance_claim_amount,
CASE WHEN direct_economic_loss = -1 THEN NULL WHEN direct_economic_loss &lt; 0 THEN 0 ELSE direct_economic_loss END AS direct_economic_loss,
CASE WHEN insurance_claim_amount = -1 THEN NULL WHEN insurance_claim_amount &lt; 0 THEN 0 ELSE insurance_claim_amount END AS insurance_claim_amount,
accident_reason_damage,
attachments,
remark
@@ -88,7 +88,7 @@
valid_until_date,
passenger_type_level,
inspection_unit,
CASE WHEN fee &lt; 0 THEN 0 ELSE fee END AS fee,
CASE WHEN fee = -1 THEN NULL WHEN fee &lt; 0 THEN 0 ELSE fee END AS fee,
assessment_unit,
attachments,
remark
@@ -44,8 +44,8 @@
policy_no,
start_date,
end_date,
CASE WHEN insured_amount &lt; 0 THEN 0 ELSE insured_amount END AS insured_amount,
CASE WHEN premium &lt; 0 THEN 0 ELSE premium END AS premium,
CASE WHEN insured_amount = -1 THEN NULL ELSE insured_amount END AS insured_amount,
CASE WHEN premium = -1 THEN NULL ELSE premium END AS premium,
invoice_no,
invoice_date,
ocr_template,
@@ -44,7 +44,7 @@
vehicle_no,
maintainer,
maintenance_time,
CASE WHEN mileage &lt; 0 THEN 0 ELSE mileage END AS mileage,
CASE WHEN mileage = -1 THEN NULL WHEN mileage &lt; 0 THEN 0 ELSE mileage END AS mileage,
mileage_unit,
maintenance_item,
cost,
@@ -52,7 +52,7 @@
contact_phone,
address,
next_maintenance_time,
CASE WHEN next_maintenance_mileage &lt; 0 THEN 0 ELSE next_maintenance_mileage END AS next_maintenance_mileage,
CASE WHEN next_maintenance_mileage = -1 THEN NULL WHEN next_maintenance_mileage &lt; 0 THEN 0 ELSE next_maintenance_mileage END AS next_maintenance_mileage,
attachments,
remark
FROM
@@ -36,10 +36,10 @@
is_deleted,
vehicle_type,
vehicle_no,
CASE WHEN previous_month_mileage &lt; 0 THEN 0 ELSE previous_month_mileage END AS previous_month_mileage,
CASE WHEN current_month_mileage &lt; 0 THEN 0 ELSE current_month_mileage END AS current_month_mileage,
CASE WHEN monthly_mileage &lt; 0 THEN 0 ELSE monthly_mileage END AS monthly_mileage,
CASE WHEN total_mileage &lt; 0 THEN 0 ELSE total_mileage END AS total_mileage,
CASE WHEN previous_month_mileage = -1 THEN NULL WHEN previous_month_mileage &lt; 0 THEN 0 ELSE previous_month_mileage END AS previous_month_mileage,
CASE WHEN current_month_mileage = -1 THEN NULL WHEN current_month_mileage &lt; 0 THEN 0 ELSE current_month_mileage END AS current_month_mileage,
CASE WHEN monthly_mileage = -1 THEN NULL WHEN monthly_mileage &lt; 0 THEN 0 ELSE monthly_mileage END AS monthly_mileage,
CASE WHEN total_mileage = -1 THEN NULL WHEN total_mileage &lt; 0 THEN 0 ELSE total_mileage END AS total_mileage,
mileage_unit,
attachments,
remark
@@ -48,10 +48,10 @@
vehicle_no,
card_holder,
data_source,
CASE WHEN quantity &lt; 0 THEN 0 ELSE quantity END AS quantity,
CASE WHEN unit_price &lt; 0 THEN 0 ELSE unit_price END AS unit_price,
CASE WHEN transaction_amount &lt; 0 THEN 0 ELSE transaction_amount END AS transaction_amount,
CASE WHEN balance &lt; 0 THEN 0 ELSE balance END AS balance,
CASE WHEN quantity = -1 THEN NULL WHEN quantity &lt; 0 THEN 0 ELSE quantity END AS quantity,
CASE WHEN unit_price = -1 THEN NULL WHEN unit_price &lt; 0 THEN 0 ELSE unit_price END AS unit_price,
CASE WHEN transaction_amount = -1 THEN NULL WHEN transaction_amount &lt; 0 THEN 0 ELSE transaction_amount END AS transaction_amount,
CASE WHEN balance = -1 THEN NULL WHEN balance &lt; 0 THEN 0 ELSE balance END AS balance,
station,
attachments,
remark
@@ -38,8 +38,8 @@
handler,
replacement_time,
tire_brand,
CASE WHEN tire_quantity &lt; 0 THEN 0 ELSE tire_quantity END AS tire_quantity,
CASE WHEN replacement_cost &lt; 0 THEN 0 ELSE replacement_cost END AS replacement_cost,
CASE WHEN tire_quantity = -1 THEN NULL WHEN tire_quantity &lt; 0 THEN 0 ELSE tire_quantity END AS tire_quantity,
CASE WHEN replacement_cost = -1 THEN NULL WHEN replacement_cost &lt; 0 THEN 0 ELSE replacement_cost END AS replacement_cost,
replacement_description,
attachments,
remark
@@ -46,8 +46,8 @@
violation_item,
violation_time,
location,
CASE WHEN fine_amount &lt; 0 THEN 0 ELSE fine_amount END AS fine_amount,
CASE WHEN deduct_points &lt; 0 THEN 0 ELSE deduct_points END AS deduct_points,
CASE WHEN fine_amount = -1 THEN NULL WHEN fine_amount &lt; 0 THEN 0 ELSE fine_amount END AS fine_amount,
CASE WHEN deduct_points = -1 THEN NULL WHEN deduct_points &lt; 0 THEN 0 ELSE deduct_points END AS deduct_points,
penalty_unit,
process_status,
process_description,
@@ -3,19 +3,26 @@ 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.dto.BillPaymentStatusRequest;
import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest;
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
import org.springblade.transport.pojo.entity.BillPayment;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.InvoiceApplication;
import org.springblade.transport.pojo.entity.InvoiceReceipt;
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.IBillPaymentService;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IInvoiceApplicationService;
import org.springblade.transport.service.IInvoiceReceiptService;
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;
@@ -198,57 +205,6 @@ public final class MkProcessBizHandlers {
}
}
@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.publicDetail(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
@@ -436,4 +392,229 @@ public final class MkProcessBizHandlers {
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class InvoiceApplicationHandler implements IMkProcessBizHandler {
private final IInvoiceApplicationService service;
@Override
public String bizType() {
return "invoice-application";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
InvoiceApplication 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) {
InvoiceApplication entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
InvoiceApplicationStatusRequest request = new InvoiceApplicationStatusRequest();
request.setId(id);
service.approve(request);
InvoiceApplication updated = service.getById(id);
if (updated != null) {
updated.setCurrentNode("审核通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(updated);
}
return true;
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
InvoiceApplication entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
InvoiceApplicationStatusRequest request = new InvoiceApplicationStatusRequest();
request.setId(id);
request.setReason("MK流程驳回");
service.returnBill(request);
InvoiceApplication updated = service.getById(id);
if (updated != null) {
updated.setCurrentNode("审核不通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(updated);
}
return true;
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class InvoiceReceiptHandler implements IMkProcessBizHandler {
private final IInvoiceReceiptService service;
@Override
public String bizType() {
return "invoice-receipt";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
InvoiceReceipt 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) {
InvoiceReceipt entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
InvoiceReceiptStatusRequest request = new InvoiceReceiptStatusRequest();
request.setId(id);
service.approve(request);
InvoiceReceipt updated = service.getById(id);
if (updated != null) {
updated.setCurrentNode("审核通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(updated);
}
return true;
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
InvoiceReceipt entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
InvoiceReceiptStatusRequest request = new InvoiceReceiptStatusRequest();
request.setId(id);
request.setReason("MK流程驳回");
service.returnBill(request);
InvoiceReceipt updated = service.getById(id);
if (updated != null) {
updated.setCurrentNode("审核不通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(updated);
}
return true;
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class BillPaymentHandler implements IMkProcessBizHandler {
private final IBillPaymentService service;
@Override
public String bizType() {
return "bill-payment";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
BillPayment 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) {
BillPayment entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
BillPaymentStatusRequest request = new BillPaymentStatusRequest();
request.setId(id);
service.approve(request);
BillPayment updated = service.getById(id);
if (updated != null) {
updated.setCurrentNode("审核通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(updated);
}
return true;
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
BillPayment entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
BillPaymentStatusRequest request = new BillPaymentStatusRequest();
request.setId(id);
request.setReason("MK流程驳回");
service.returnBill(request);
BillPayment updated = service.getById(id);
if (updated != null) {
updated.setCurrentNode("审核不通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(updated);
}
return true;
}
}
}
@@ -10,7 +10,6 @@ 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;
@@ -129,14 +128,12 @@ public class MkProcessMessageService {
if (!"20".equals(firstText(info, "fdProcessStatus"))) {
return false;
}
if (hasItems(info.get("currentHandlers"))) {
Object historyHandlerInfos = asMap(info.get("fdTaskInfo")).get("historyHandlerInfos");
if (!(historyHandlerInfos instanceof List<?> historyList) || historyList.isEmpty()) {
return false;
}
return !hasItems(asMap(info.get("fdTaskInfo")).get("handlerInfos"));
}
private static boolean hasItems(Object value) {
return value instanceof Collection<?> collection && !collection.isEmpty();
Object lastHandler = historyList.get(historyList.size() - 1);
return "N6".equals(firstText(asMap(lastHandler), "nodeNumber"));
}
private void applyProcessResult(IMkProcessBizHandler handler, String bizType, String bizIdText,
@@ -154,8 +151,18 @@ public class MkProcessMessageService {
} else {
handler.rejectFromProcess(bizId, processorName);
}
try {
FR<Boolean> markResult = businessProcessClient.markProcessFinished(processId, approved);
log.info("公开页标记业务流程完结 bizType={} processId={} approved={} result={}",
bizType, processId, approved, JSON.toJSONString(markResult));
} catch (Exception markException) {
log.error("公开页标记业务流程完结失败 bizType={} processId={} approved={}",
bizType, processId, approved, markException);
}
} catch (NumberFormatException e) {
log.warn("流程{}但业务id格式不正确 bizType={} id={}", action, bizType, bizIdText);
} catch (Exception e) {
log.error("流程{}同步业务状态失败 bizType={} id={} processId={}", action, bizType, bizIdText, processId, e);
}
}
@@ -19,6 +19,7 @@ public interface IVoucherManageService extends BaseService<VoucherManage> {
VoucherManageVO detail(Long id);
IPage<VoucherFolderVO> folderPage(IPage<?> page, Long voucherId, String plateNo, Integer matched);
VoucherFolderVO folderDetail(Long voucherId, String plateNo);
List<VoucherFolderVO> listWaybillFolders(Long waybillId);
void replaceFolder(Long voucherId, String plateNo, MultipartFile file);
void replaceFolderByObject(Long voucherId, String plateNo, String objectKey, String fileName, Long size, String contentType);
void removeFolder(Long voucherId, String plateNo);
@@ -7,12 +7,14 @@ import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
import org.springblade.transport.pojo.entity.WaybillImportBatch;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
/** 运单批次服务。 */
public interface IWaybillImportBatchService extends BaseService<WaybillImportBatch> {
WaybillImportBatch saveDraft(WaybillImportBatchRequest request);
void validate(WaybillImportBatchRequest request, HttpServletResponse response);
void confirm(WaybillImportBatchRequest request, HttpServletResponse response);
WaybillImportValidateResultVO validate(WaybillImportBatchRequest request);
void exportFailures(WaybillImportBatchRequest request, HttpServletResponse response);
WaybillImportBatchVO confirm(WaybillImportBatchRequest request);
IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request);
BusinessRemoveResultVO removeBatches(String ids);
String nextBatchNo();
@@ -61,6 +61,8 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl<AccidentRecordMap
private static final int REMARK_MAX_LENGTH = 200;
private static final int ATTACHMENTS_MAX_LENGTH = 1000;
private static final int MONEY_SCALE = 2;
/** 非必填金额未填写时的历史占位值,统一转为 null */
private static final BigDecimal EMPTY_AMOUNT_SENTINEL = BigDecimal.valueOf(-1);
private static final String VEHICLE = "车辆";
private static final String SHIP = "船舶";
private static final Set<String> ACCIDENT_NATURES = Set.of("重大事故", "一般事故", "轻微事故", "其他");
@@ -69,7 +71,12 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl<AccidentRecordMap
@Override
public IPage<AccidentRecordVO> selectAccidentRecordPage(IPage<AccidentRecordVO> page, AccidentRecordVO accidentRecord) {
List<AccidentRecordVO> records = baseMapper.selectAccidentRecordPage(page, accidentRecord);
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
records.forEach(record -> {
record.setDirectEconomicLoss(normalizeEmptyAmount(record.getDirectEconomicLoss()));
record.setInsuranceClaimAmount(normalizeEmptyAmount(record.getInsuranceClaimAmount()));
record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
});
return page.setRecords(records);
}
@@ -86,7 +93,7 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl<AccidentRecordMap
@Transactional(rollbackFor = Exception.class)
public List<AccidentRecordExcel> importAccidentRecord(List<AccidentRecordExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<AccidentRecordExcel> errorList = new ArrayList<>();
List<AccidentRecord> accidentRecordList = new ArrayList<>();
@@ -111,8 +118,9 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl<AccidentRecordMap
}
}
if (Func.isNotEmpty(errorList)) {
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (AccidentRecord accidentRecord : accidentRecordList) {
if (!save(accidentRecord)) {
@@ -152,8 +160,9 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl<AccidentRecordMap
public List<AccidentRecordExportExcel> exportAccidentRecord(Wrapper<AccidentRecord> queryWrapper) {
return list(queryWrapper).stream().map(accidentRecord -> {
AccidentRecordExportExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(accidentRecord, AccidentRecordExportExcel.class));
excel.setDirectEconomicLoss(nonNegative(accidentRecord.getDirectEconomicLoss()));
excel.setInsuranceClaimAmount(nonNegative(accidentRecord.getInsuranceClaimAmount()));
excel.setDirectEconomicLoss(normalizeEmptyAmount(accidentRecord.getDirectEconomicLoss()));
excel.setInsuranceClaimAmount(normalizeEmptyAmount(accidentRecord.getInsuranceClaimAmount()));
excel.setCreateUserName(UserCache.getUserRealName(accidentRecord.getCreateUser()));
excel.setUpdateUserName(UserCache.getUserRealName(accidentRecord.getUpdateUser()));
return excel;
}).toList();
@@ -168,6 +177,9 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl<AccidentRecordMap
accidentRecord.setAccidentReasonDamage(trimToNull(accidentRecord.getAccidentReasonDamage()));
accidentRecord.setAttachments(trimToNull(accidentRecord.getAttachments()));
accidentRecord.setRemark(trimToNull(accidentRecord.getRemark()));
// 非必填金额未填写禁止落库 -1 占位值
accidentRecord.setDirectEconomicLoss(normalizeEmptyAmount(accidentRecord.getDirectEconomicLoss()));
accidentRecord.setInsuranceClaimAmount(normalizeEmptyAmount(accidentRecord.getInsuranceClaimAmount()));
}
private void validate(AccidentRecord accidentRecord) {
@@ -237,11 +249,8 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl<AccidentRecordMap
}
}
private BigDecimal nonNegative(BigDecimal value) {
if (Func.isEmpty(value) || value.compareTo(BigDecimal.ZERO) >= 0) {
return value;
}
return BigDecimal.ZERO;
private BigDecimal normalizeEmptyAmount(BigDecimal value) {
return Func.isNotEmpty(value) && value.compareTo(EMPTY_AMOUNT_SENTINEL) == 0 ? null : value;
}
private String normalizeVehicleType(String vehicleType) {
@@ -255,7 +264,7 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl<AccidentRecordMap
private String trimToNull(String value) {
String trimValue = trimToEmpty(value);
return trimValue.isEmpty() ? null : trimValue;
return trimValue.isEmpty() || "-1".equals(trimValue) ? null : trimValue;
}
}
@@ -63,6 +63,8 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl<AnnualIns
private static final int REMARK_MAX_LENGTH = 200;
private static final int ATTACHMENTS_MAX_LENGTH = 1000;
private static final int MONEY_SCALE = 2;
/** 费用等数值未填写时的历史占位值,统一转为 null */
private static final BigDecimal EMPTY_AMOUNT_SENTINEL = BigDecimal.valueOf(-1);
private static final String VEHICLE = "车辆";
private static final String SHIP = "船舶";
private static final Set<String> PASSENGER_TYPE_LEVELS = Set.of("一级", "二级", "其他");
@@ -70,7 +72,11 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl<AnnualIns
@Override
public IPage<AnnualInspectionRecordVO> selectAnnualInspectionRecordPage(IPage<AnnualInspectionRecordVO> page, AnnualInspectionRecordVO annualInspectionRecord) {
List<AnnualInspectionRecordVO> records = baseMapper.selectAnnualInspectionRecordPage(page, annualInspectionRecord);
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
records.forEach(record -> {
record.setFee(normalizeEmptyAmount(record.getFee()));
record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
});
return page.setRecords(records);
}
@@ -105,7 +111,7 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl<AnnualIns
@Transactional(rollbackFor = Exception.class)
public List<AnnualInspectionRecordExcel> importAnnualInspectionRecord(List<AnnualInspectionRecordExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<AnnualInspectionRecordExcel> errorList = new ArrayList<>();
List<AnnualInspectionRecord> annualInspectionRecordList = new ArrayList<>();
@@ -131,8 +137,9 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl<AnnualIns
}
}
if (Func.isNotEmpty(errorList)) {
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (AnnualInspectionRecord annualInspectionRecord : annualInspectionRecordList) {
if (!save(annualInspectionRecord)) {
@@ -175,7 +182,8 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl<AnnualIns
AnnualInspectionRecordExportExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(annualInspectionRecord, AnnualInspectionRecordExportExcel.class));
excel.setVehicleTechnicalLevel(VEHICLE.equals(annualInspectionRecord.getVehicleType()) ? annualInspectionRecord.getVehicleTechnicalLevel() : null);
excel.setShipInspectionType(SHIP.equals(annualInspectionRecord.getVehicleType()) ? annualInspectionRecord.getShipInspectionType() : null);
excel.setFee(nonNegative(annualInspectionRecord.getFee()));
excel.setFee(normalizeEmptyAmount(annualInspectionRecord.getFee()));
excel.setCreateUserName(UserCache.getUserRealName(annualInspectionRecord.getCreateUser()));
excel.setUpdateUserName(UserCache.getUserRealName(annualInspectionRecord.getUpdateUser()));
return excel;
}).toList();
@@ -191,6 +199,8 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl<AnnualIns
annualInspectionRecord.setAssessmentUnit(trimToNull(annualInspectionRecord.getAssessmentUnit()));
annualInspectionRecord.setAttachments(trimToNull(annualInspectionRecord.getAttachments()));
annualInspectionRecord.setRemark(trimToNull(annualInspectionRecord.getRemark()));
// 费用必填,-1 占位转 null 后由校验拦截;展示侧同步为空
annualInspectionRecord.setFee(normalizeEmptyAmount(annualInspectionRecord.getFee()));
if (VEHICLE.equals(annualInspectionRecord.getVehicleType())) {
annualInspectionRecord.setShipInspectionType(null);
} else {
@@ -267,11 +277,8 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl<AnnualIns
}
}
private BigDecimal nonNegative(BigDecimal value) {
if (Func.isEmpty(value) || value.compareTo(BigDecimal.ZERO) >= 0) {
return value;
}
return BigDecimal.ZERO;
private BigDecimal normalizeEmptyAmount(BigDecimal value) {
return Func.isNotEmpty(value) && value.compareTo(EMPTY_AMOUNT_SENTINEL) == 0 ? null : value;
}
private String normalizeVehicleType(String vehicleType) {
@@ -285,7 +292,7 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl<AnnualIns
private String trimToNull(String value) {
String trimValue = trimToEmpty(value);
return trimValue.isEmpty() ? null : trimValue;
return trimValue.isEmpty() || "-1".equals(trimValue) ? null : trimValue;
}
}
@@ -38,6 +38,7 @@ import org.springblade.transport.pojo.entity.CommonAddress;
import org.springblade.transport.pojo.vo.CommonAddressRemoveResultVO;
import org.springblade.transport.pojo.vo.CommonAddressVO;
import org.springblade.transport.service.ICommonAddressService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -160,18 +161,14 @@ public class CommonAddressServiceImpl extends BaseServiceImpl<CommonAddressMappe
return list(queryWrapper).stream().map(address -> {
CommonAddressExportExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(address, CommonAddressExportExcel.class));
excel.setSiteCodeDisplay(TYPE_NORMAL.equals(address.getAddressType()) ? NORMAL_SITE_CODE : address.getSiteCode());
excel.setCreateUserName(UserCache.getUserRealName(address.getCreateUser()));
return excel;
}).toList();
}
private void prepareQuery(CommonAddressVO address) {
if (Objects.equals(address.getAllDept(), 1)) {
if (!AuthUtil.isAdministrator()) {
throw new ServiceException("无权查看全部组织常用地址");
}
} else {
address.setDeptId(currentDeptId());
}
List<Long> deptIdList = TransportBusinessSupport.resolveDeptFilterIds(address.getAllDept(), address.getDeptId(), "常用地址");
address.setDeptId(Func.isEmpty(deptIdList) ? null : deptIdList.get(0));
address.setAddressName(trimToNull(address.getAddressName()));
address.setAddressType(trimToNull(address.getAddressType()));
address.setDetailAddress(trimToNull(address.getDetailAddress()));
@@ -300,14 +297,14 @@ public class CommonAddressServiceImpl extends BaseServiceImpl<CommonAddressMappe
}
private void validateCurrentDept(CommonAddress address) {
if (!Objects.equals(address.getDeptId(), currentDeptId())) {
if (!TransportBusinessSupport.isAdministrator() && !Objects.equals(address.getDeptId(), currentDeptId())) {
throw new ServiceException("无权操作其他组织常用地址");
}
}
private void fillDisplay(CommonAddressVO address) {
address.setSiteCodeDisplay(TYPE_NORMAL.equals(address.getAddressType()) ? NORMAL_SITE_CODE : address.getSiteCode());
address.setReadonly(!Objects.equals(address.getDeptId(), currentDeptId()));
address.setReadonly(!TransportBusinessSupport.isAdministrator() && !Objects.equals(address.getDeptId(), currentDeptId()));
address.setCreateUserName(UserCache.getUserRealName(address.getCreateUser()));
address.setUpdateUserName(UserCache.getUserRealName(address.getUpdateUser()));
}
@@ -25,10 +25,12 @@ package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.transport.excel.CommonCargoExcel;
import org.springblade.transport.excel.CommonCargoExportExcel;
@@ -70,10 +72,6 @@ public class CommonCargoServiceImpl extends BaseServiceImpl<CommonCargoMapper, C
@Override
@Transactional(rollbackFor = Exception.class)
public boolean submit(CommonCargo commonCargo) {
return submit(commonCargo, true);
}
private boolean submit(CommonCargo commonCargo, boolean requireModel) {
boolean created = Func.isEmpty(commonCargo.getId());
if (!created) {
CommonCargo oldRecord = loadEditable(commonCargo.getId(), true);
@@ -81,7 +79,7 @@ public class CommonCargoServiceImpl extends BaseServiceImpl<CommonCargoMapper, C
commonCargo.setDeptName(oldRecord.getDeptName());
}
prepare(commonCargo);
validate(commonCargo, requireModel);
validate(commonCargo);
return saveOrUpdate(commonCargo);
}
@@ -121,6 +119,7 @@ public class CommonCargoServiceImpl extends BaseServiceImpl<CommonCargoMapper, C
BeanUtil.copyProperties(record, excel);
excel.setCargoCodeSuffix(cargoCodeSuffix(record));
excel.setCargoValue(normalizeCargoValue(record.getCargoValue()));
excel.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
return excel;
}).toList();
}
@@ -141,8 +140,12 @@ public class CommonCargoServiceImpl extends BaseServiceImpl<CommonCargoMapper, C
@Transactional(rollbackFor = Exception.class)
public List<CommonCargoImportFailureExcel> importCommonCargo(List<CommonCargoExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
// 注意:本方法虽已标注 @Transactional,但循环内把异常吞掉后正常 return,
// Spring 会认为一切成功并提交事务——注解救不了被吞掉的异常。
// 必须把错误明细收集完后抛异常,回滚才会真正发生。
List<CommonCargoImportFailureExcel> failureList = new ArrayList<>();
for (int index = 0; index < data.size(); index++) {
CommonCargoExcel excel = data.get(index);
@@ -168,14 +171,23 @@ public class CommonCargoServiceImpl extends BaseServiceImpl<CommonCargoMapper, C
commonCargo.setDescriptionOne(TransportBusinessSupport.trimToNull(excel.getDescriptionOne()));
commonCargo.setRemark(TransportBusinessSupport.trimToNull(excel.getRemark()));
commonCargo.setDataSource("批量导入");
submit(commonCargo, false);
submit(commonCargo);
} catch (Exception exception) {
// 除收集到 failureList 外,还必须把原因写回原行对象:
// 抛出异常时携带的是原表 data,导出工具从行对象的 errorMessage 字段取原因,
// 只写在副本上会导致失败明细里错误行的原因为空。
excel.setErrorMessage(exception.getMessage());
CommonCargoImportFailureExcel failureExcel = new CommonCargoImportFailureExcel();
BeanUtil.copyProperties(excel, failureExcel);
failureExcel.setErrorMessage(exception.getMessage());
failureList.add(failureExcel);
}
}
if (Func.isNotEmpty(failureList)) {
// 抛出携带失败明细的异常,触发事务回滚。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new ImportFailureException(data);
}
return failureList;
}
@@ -201,12 +213,14 @@ public class CommonCargoServiceImpl extends BaseServiceImpl<CommonCargoMapper, C
}
private LambdaQueryWrapper<CommonCargo> buildQuery(CommonCargoVO commonCargo) {
TransportBusinessSupport.validateAllDept(commonCargo.getAllDept(), "常用货物");
LambdaQueryWrapper<CommonCargo> queryWrapper = Wrappers.<CommonCargo>lambdaQuery().eq(CommonCargo::getIsDeleted, 0);
if (!Objects.equals(commonCargo.getAllDept(), 1)) {
queryWrapper.eq(CommonCargo::getDeptId, TransportBusinessSupport.currentDeptId("常用货物"));
} else if (Func.isNotEmpty(commonCargo.getDeptId())) {
queryWrapper.eq(CommonCargo::getDeptId, commonCargo.getDeptId());
List<Long> deptIdList = TransportBusinessSupport.resolveDeptFilterIds(commonCargo.getAllDept(), commonCargo.getDeptId(), "常用货物");
if (Func.isNotEmpty(deptIdList)) {
if (deptIdList.size() == 1) {
queryWrapper.eq(CommonCargo::getDeptId, deptIdList.get(0));
} else {
queryWrapper.in(CommonCargo::getDeptId, deptIdList);
}
}
if (Func.isNotEmpty(commonCargo.getFirstCargoTypeId())) {
queryWrapper.eq(CommonCargo::getFirstCargoTypeId, commonCargo.getFirstCargoTypeId());
@@ -301,14 +315,15 @@ public class CommonCargoServiceImpl extends BaseServiceImpl<CommonCargoMapper, C
return cargoValue != null && cargoValue.compareTo(BigDecimal.ONE.negate()) == 0 ? null : cargoValue;
}
private void validate(CommonCargo commonCargo, boolean requireModel) {
private void validate(CommonCargo commonCargo) {
TransportBusinessSupport.validateRequired(commonCargo.getFirstCargoTypeName(), "请选择一级货物类型");
TransportBusinessSupport.validateRequired(commonCargo.getSecondCargoTypeCode(), "请选择二级货物类型");
TransportBusinessSupport.validateRequired(commonCargo.getCargoName(), "请输入货物名称");
TransportBusinessSupport.validateRequired(commonCargo.getCargoCode(), "请输入货物编号");
if (requireModel) {
TransportBusinessSupport.validateRequired(commonCargo.getModel(), "请输入型号");
}
// 型号必填:新增/编辑入口(/submit)一直要求,导入侧此前用 requireModel=false
// 放过了它,导致「页面上存不进去、导入却能写进库」,与根文档
// docs/import-spec.md 的「导入校验必须与新增/编辑一致」冲突,故统一为必填。
TransportBusinessSupport.validateRequired(commonCargo.getModel(), "请输入型号");
TransportBusinessSupport.validateLength(commonCargo.getFirstCargoTypeName(), 100, "一级货物类型不能超过100字");
TransportBusinessSupport.validateLength(commonCargo.getFirstCargoTypeCode(), 100, "一级货物类型编码不能超过100字");
TransportBusinessSupport.validateLength(commonCargo.getSecondCargoTypeName(), 100, "二级货物类型不能超过100字");
@@ -25,6 +25,7 @@ package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.tool.utils.BeanUtil;
@@ -128,8 +129,12 @@ public class CommonRouteServiceImpl extends BaseServiceImpl<CommonRouteMapper, C
@Transactional(rollbackFor = Exception.class)
public List<CommonRouteImportExcel> importCommonRoute(List<CommonRouteImportExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
// 注意:本方法虽已标注 @Transactional,但循环内把异常吞掉后正常 return,
// Spring 会认为一切成功并提交事务——注解救不了被吞掉的异常。
// 必须把错误明细收集完后抛异常,回滚才会真正发生。
List<CommonRouteImportExcel> failureList = new ArrayList<>();
for (int index = 0; index < data.size(); index++) {
CommonRouteImportExcel excel = data.get(index);
@@ -142,16 +147,19 @@ public class CommonRouteServiceImpl extends BaseServiceImpl<CommonRouteMapper, C
failureList.add(excel);
}
}
if (Func.isNotEmpty(failureList)) {
// 抛出携带失败明细的异常,触发事务回滚。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new ImportFailureException(data);
}
return failureList;
}
private LambdaQueryWrapper<CommonRoute> buildQuery(CommonRouteVO commonRoute) {
TransportBusinessSupport.validateAllDept(commonRoute.getAllDept(), "常用线路");
LambdaQueryWrapper<CommonRoute> queryWrapper = Wrappers.<CommonRoute>lambdaQuery().eq(CommonRoute::getIsDeleted, 0);
if (!Objects.equals(commonRoute.getAllDept(), 1)) {
queryWrapper.eq(CommonRoute::getDeptId, TransportBusinessSupport.currentDeptId("常用线路"));
} else if (Func.isNotEmpty(commonRoute.getDeptId())) {
queryWrapper.eq(CommonRoute::getDeptId, commonRoute.getDeptId());
List<Long> deptIdList = TransportBusinessSupport.resolveDeptFilterIds(commonRoute.getAllDept(), commonRoute.getDeptId(), "常用线路");
if (Func.isNotEmpty(deptIdList)) {
queryWrapper.eq(CommonRoute::getDeptId, deptIdList.get(0));
}
if (Func.isNotEmpty(commonRoute.getRouteCode())) {
queryWrapper.eq(CommonRoute::getRouteCode, commonRoute.getRouteCode());
@@ -416,7 +416,11 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
private LambdaQueryWrapper<ContractManage> buildQuery(ContractManageVO contractManage) {
TransportBusinessSupport.validateAllDept(contractManage.getAllDept(), "合同管理");
LambdaQueryWrapper<ContractManage> queryWrapper = Wrappers.<ContractManage>lambdaQuery().eq(ContractManage::getIsDeleted, 0);
if (!Objects.equals(contractManage.getAllDept(), 1)) {
if (TransportBusinessSupport.isAdministrator()) {
if (Objects.equals(contractManage.getAllDept(), 1) && Func.isNotEmpty(contractManage.getOrganizationId())) {
queryWrapper.eq(ContractManage::getOrganizationId, contractManage.getOrganizationId());
}
} else if (!Objects.equals(contractManage.getAllDept(), 1)) {
Long currentDeptId = TransportBusinessSupport.currentDeptId("合同管理");
Long currentUserId = AuthUtil.getUserId();
queryWrapper.and(wrapper -> wrapper.eq(ContractManage::getOrganizationId, currentDeptId)
@@ -1088,6 +1092,9 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
}
private boolean canOperateInCurrentScope(ContractManage contractManage) {
if (TransportBusinessSupport.isAdministrator()) {
return true;
}
Long currentDeptId = TransportBusinessSupport.currentDeptId("合同管理");
Long currentUserId = AuthUtil.getUserId();
return Objects.equals(contractManage.getOrganizationId(), currentDeptId)
@@ -35,6 +35,7 @@ import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.excel.CreditScoreQuantificationExcel;
import org.springblade.transport.mapper.CreditRatingStandardMapper;
import org.springblade.transport.mapper.CreditScoreCategoryMapper;
@@ -119,6 +120,7 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl<Credit
throw new ServiceException("评分量化表不存在");
}
CreditScoreQuantificationVO detail = Objects.requireNonNull(BeanUtil.copyProperties(quantification, CreditScoreQuantificationVO.class));
detail.setCreateUserName(UserCache.getUserRealName(quantification.getCreateUser()));
detail.setCategories(loadCategories(id));
detail.setStandards(loadStandards(id));
return detail;
@@ -196,7 +198,7 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl<Credit
@Transactional(rollbackFor = Exception.class)
public List<CreditScoreQuantificationExcel> importCreditScoreQuantification(List<CreditScoreQuantificationExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
Map<String, List<CreditScoreQuantificationExcel>> dataMap = data.stream()
.filter(row -> Func.isNotEmpty(trimToNull(row.getTableName())))
@@ -214,6 +216,13 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl<Credit
errorList.addAll(rows);
}
});
if (Func.isNotEmpty(errorList)) {
// 抛出携带失败明细的异常,触发事务回滚:任一分组失败时,
// 失败分组**之前已 submit 的正确分组**也必须一起退掉。
// 注意不能只往 errorList 里塞数据就 return——异常被吞掉后方法会正常返回,
// Spring 会照常提交事务,@Transactional 起不到回滚作用。
throw new org.springblade.common.excel.ImportFailureException(data);
}
return errorList;
}
@@ -15,9 +15,9 @@ 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.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -120,14 +120,12 @@ public class CustomerArchivePublicProcessService {
if (!"20".equals(firstText(info, "fdProcessStatus"))) {
return false;
}
if (hasItems(info.get("currentHandlers"))) {
Object historyHandlerInfos = asMap(info.get("fdTaskInfo")).get("historyHandlerInfos");
if (!(historyHandlerInfos instanceof List<?> historyList) || historyList.isEmpty()) {
return false;
}
return !hasItems(asMap(info.get("fdTaskInfo")).get("handlerInfos"));
}
private static boolean hasItems(Object value) {
return value instanceof Collection<?> collection && !collection.isEmpty();
Object lastHandler = historyList.get(historyList.size() - 1);
return "N6".equals(firstText(asMap(lastHandler), "nodeNumber"));
}
private void applyProcessResult(String customerIdText, String processId, Map<String, Object> formValues,
@@ -145,8 +143,17 @@ public class CustomerArchivePublicProcessService {
} else {
customerArchiveService.rejectFromProcess(customerId, processorName);
}
try {
FR<Boolean> markResult = businessProcessClient.markProcessFinished(processId, approved);
log.info("客商公开页标记业务流程完结 processId={} approved={} result={}",
processId, approved, JSON.toJSONString(markResult));
} catch (Exception markException) {
log.error("客商公开页标记业务流程完结失败 processId={} approved={}", processId, approved, markException);
}
} catch (NumberFormatException e) {
log.warn("流程{}但客商id格式不正确:{}", action, customerIdText);
} catch (Exception e) {
log.error("流程{}同步客商状态失败 id={} processId={}", action, customerIdText, processId, e);
}
}
@@ -34,6 +34,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil;
@@ -48,6 +49,7 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiCustomerVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.thirdparty.bfi.util.BfiUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.excel.CustomerArchiveExcel;
import org.springblade.transport.mapper.CreditRatingStandardMapper;
import org.springblade.transport.mapper.CreditScoreCategoryMapper;
@@ -229,7 +231,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
@Override
public IPage<CustomerArchiveVO> selectCustomerArchivePage(IPage<CustomerArchiveVO> page, CustomerArchiveVO customer) {
List<CustomerArchiveVO> records = baseMapper.selectCustomerArchivePage(page, customer, AuthUtil.getUserId());
List<CustomerArchiveVO> records = baseMapper.selectCustomerArchivePage(page, customer, currentScopeUserId());
records.forEach(record -> record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser())));
fillFundUseRisk(records);
return page.setRecords(records);
}
@@ -409,7 +412,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
log.info("流程结束审核通过跳过,客商已是审核通过 id={}", id);
return true;
}
validateReviewCompleted(buildDetail(before));
// MK 流程已结束(fdProcessStatus=30),直接同步为审核通过,不再校验复评
CustomerArchive after = copyCustomer(before);
after.setAccessType(ACCESS_FORMAL);
after.setApprovalStatus(APPROVAL_APPROVED);
@@ -1658,6 +1661,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
private CustomerArchiveVO buildDetail(CustomerArchive customer) {
CustomerArchiveVO detail = Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchiveVO.class));
detail.setCreateUserName(UserCache.getUserRealName(customer.getCreateUser()));
Long id = customer.getId();
detail.setContacts(loadContacts(id));
detail.setReceiptAccounts(loadReceiptAccounts(id));
@@ -1680,12 +1684,20 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
private CustomerArchive ensureCustomerAccessible(Long id) {
CustomerArchive customer = getExistingCustomer(id);
if (baseMapper.selectCustomerPermissionCount(id, AuthUtil.getUserId()) == 0) {
Long userId = currentScopeUserId();
if (userId != null && baseMapper.selectCustomerPermissionCount(id, userId) == 0) {
throw new ServiceException("无权访问该客商档案");
}
return customer;
}
/**
* 客商可见范围用户。超管返回空,表示不按用户客商授权表过滤。
*/
private Long currentScopeUserId() {
return TransportBusinessSupport.isAdministrator() ? null : AuthUtil.getUserId();
}
private CustomerArchive copyCustomer(CustomerArchive customer) {
return Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchive.class));
}
@@ -2050,6 +2062,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
excel.setApplyCreditLimit(customer.getApplyCreditLimit());
excel.setContactPhone(customer.getContactPhone());
excel.setLegalPerson(customer.getLegalPerson());
excel.setCreateUserName(UserCache.getUserRealName(customer.getCreateUser()));
excel.setCreateTime(customer.getCreateTime());
excel.setKingdeeStatusName(kingdeeStatusName(customer.getKingdeeStatus()));
return excel;
@@ -34,6 +34,7 @@ import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.feign.ISysClient;
import org.springblade.system.feign.IUserClient;
@@ -82,7 +83,9 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> imp
@Override
public IPage<DriverVO> selectDriverPage(IPage<DriverVO> page, DriverVO driver) {
prepareQuery(driver);
return page.setRecords(baseMapper.selectDriverPage(page, driver));
List<DriverVO> records = baseMapper.selectDriverPage(page, driver);
records.forEach(record -> record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser())));
return page.setRecords(records);
}
@Override
@@ -133,6 +136,8 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> imp
public List<DriverExcel> exportDriver(Wrapper<Driver> queryWrapper) {
return list(queryWrapper).stream().map(driver -> {
DriverExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(driver, DriverExcel.class));
excel.setCreateUserName(UserCache.getUserRealName(driver.getCreateUser()));
excel.setCreateTime(driver.getCreateTime());
excel.setAddress((driver.getAddressRegion() == null ? "" : driver.getAddressRegion())
+ (driver.getAddress() == null ? "" : driver.getAddress()));
return excel;
@@ -338,7 +343,7 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> imp
User existing = findExistingUser(driver, tenantId, mobile);
if (existing == null || Func.isEmpty(existing.getId())) {
// 司机未关联有效用户时新建;密码为手机号后6位
// 司机未关联有效用户时新建;初始密码由系统用户保存逻辑按默认参数/强度规则填充
createAndLinkDriverUser(driver, tenantId, mobile, roleId, deptId);
} else {
updateAndLinkDriverUser(driver, existing, tenantId, mobile, roleId, deptId);
@@ -373,7 +378,7 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> imp
private void createAndLinkDriverUser(Driver driver, String tenantId, String mobile, String roleId, String deptId) {
User user = buildSyncUser(null, tenantId, mobile, driver.getDriverName(), driver.getGender(), roleId, deptId);
user.setPassword(mobile.substring(mobile.length() - 6));
// 不传密码:交由 UserServiceImpl.saveUser 使用系统默认初始密码或自动生成合规密码
user.setUserType(UserType.WEB.getCategory());
R<Boolean> saveResult = null;
@@ -59,7 +59,10 @@ public class EquipmentLedgerServiceImpl extends BaseServiceImpl<EquipmentLedgerM
@Override
public IPage<EquipmentLedgerVO> selectEquipmentLedgerPage(IPage<EquipmentLedgerVO> page, EquipmentLedgerVO equipmentLedger) {
List<EquipmentLedgerVO> records = baseMapper.selectEquipmentLedgerPage(page, equipmentLedger);
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
records.forEach(record -> {
record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
});
return page.setRecords(records);
}
@@ -79,7 +82,7 @@ public class EquipmentLedgerServiceImpl extends BaseServiceImpl<EquipmentLedgerM
@Transactional(rollbackFor = Exception.class)
public List<EquipmentLedgerExcel> importEquipmentLedger(List<EquipmentLedgerExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<EquipmentLedgerExcel> errorList = new ArrayList<>();
List<EquipmentLedger> equipmentLedgerList = new ArrayList<>();
@@ -114,8 +117,9 @@ public class EquipmentLedgerServiceImpl extends BaseServiceImpl<EquipmentLedgerM
}
}
if (Func.isNotEmpty(errorList)) {
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (EquipmentLedger equipmentLedger : equipmentLedgerList) {
if (!save(equipmentLedger)) {
@@ -206,7 +210,12 @@ public class EquipmentLedgerServiceImpl extends BaseServiceImpl<EquipmentLedgerM
@Override
public List<EquipmentLedgerExcel> exportEquipmentLedger(Wrapper<EquipmentLedger> queryWrapper) {
return list(queryWrapper).stream()
.map(item -> Objects.requireNonNull(BeanUtil.copyProperties(item, EquipmentLedgerExcel.class)))
.map(item -> {
EquipmentLedgerExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(item, EquipmentLedgerExcel.class));
excel.setCreateUserName(UserCache.getUserRealName(item.getCreateUser()));
excel.setCreateTime(item.getCreateTime());
return excel;
})
.toList();
}
@@ -45,7 +45,10 @@ public class EtcRecordServiceImpl extends BaseServiceImpl<EtcRecordMapper, EtcRe
@Override
public IPage<EtcRecordVO> selectEtcRecordPage(IPage<EtcRecordVO> page, EtcRecordVO etcRecord) {
List<EtcRecordVO> records = baseMapper.selectEtcRecordPage(page, etcRecord);
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
records.forEach(record -> {
record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
});
return page.setRecords(records);
}
@@ -61,7 +64,7 @@ public class EtcRecordServiceImpl extends BaseServiceImpl<EtcRecordMapper, EtcRe
@Transactional(rollbackFor = Exception.class)
public List<EtcRecordExcel> importEtcRecord(List<EtcRecordExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<EtcRecordExcel> errorList = new ArrayList<>();
List<EtcRecord> etcRecordList = new ArrayList<>();
@@ -85,8 +88,9 @@ public class EtcRecordServiceImpl extends BaseServiceImpl<EtcRecordMapper, EtcRe
}
}
if (Func.isNotEmpty(errorList)) {
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (EtcRecord etcRecord : etcRecordList) {
if (!save(etcRecord)) {
@@ -123,6 +127,7 @@ public class EtcRecordServiceImpl extends BaseServiceImpl<EtcRecordMapper, EtcRe
return list(queryWrapper).stream().map(etcRecord -> {
EtcRecordExportExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(etcRecord, EtcRecordExportExcel.class));
excel.setTransactionAmount(nonNegative(etcRecord.getTransactionAmount()));
excel.setCreateUserName(UserCache.getUserRealName(etcRecord.getCreateUser()));
excel.setUpdateUserName(UserCache.getUserRealName(etcRecord.getUpdateUser()));
return excel;
}).toList();
@@ -82,6 +82,7 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
private static final int OCR_TEMPLATE_MAX_LENGTH = 100;
private static final int POLICY_FILE_MAX_LENGTH = 1000;
private static final int REMARK_MAX_LENGTH = 200;
private static final BigDecimal EMPTY_AMOUNT_SENTINEL = BigDecimal.valueOf(-1);
private static final Set<String> INSURANCE_TYPES = Set.of("交强险", "商业险", "承运人责任险", "货运险", "船舶险");
private static final Set<String> SUPPORT_FILE_TYPES = Set.of("jpg", "jpeg", "png", "bmp");
private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4})[-/.年](\\d{1,2})[-/.月](\\d{1,2})日?");
@@ -93,7 +94,12 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
@Override
public IPage<InsuranceRecordVO> selectInsuranceRecordPage(IPage<InsuranceRecordVO> page, InsuranceRecordVO insuranceRecord) {
List<InsuranceRecordVO> records = baseMapper.selectInsuranceRecordPage(page, insuranceRecord);
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
records.forEach(record -> {
record.setInsuredAmount(normalizeEmptyAmount(record.getInsuredAmount()));
record.setPremium(normalizeEmptyAmount(record.getPremium()));
record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
});
return page.setRecords(records);
}
@@ -110,7 +116,7 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
@Transactional(rollbackFor = Exception.class)
public List<InsuranceRecordExcel> importInsuranceRecord(List<InsuranceRecordExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<InsuranceRecordExcel> errorList = new ArrayList<>();
List<InsuranceRecord> insuranceRecordList = new ArrayList<>();
@@ -142,8 +148,9 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
}
}
if (Func.isNotEmpty(errorList)) {
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (InsuranceRecord insuranceRecord : insuranceRecordList) {
if (!save(insuranceRecord)) {
@@ -190,8 +197,9 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
public List<InsuranceRecordExportExcel> exportInsuranceRecord(Wrapper<InsuranceRecord> queryWrapper) {
return list(queryWrapper).stream().map(insuranceRecord -> {
InsuranceRecordExportExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(insuranceRecord, InsuranceRecordExportExcel.class));
excel.setInsuredAmount(nonNegative(insuranceRecord.getInsuredAmount()));
excel.setPremium(nonNegative(insuranceRecord.getPremium()));
excel.setInsuredAmount(normalizeEmptyAmount(insuranceRecord.getInsuredAmount()));
excel.setPremium(normalizeEmptyAmount(insuranceRecord.getPremium()));
excel.setCreateUserName(UserCache.getUserRealName(insuranceRecord.getCreateUser()));
excel.setUpdateUserName(UserCache.getUserRealName(insuranceRecord.getUpdateUser()));
return excel;
}).toList();
@@ -336,6 +344,9 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
insuranceRecord.setOcrTemplate(trimToNull(insuranceRecord.getOcrTemplate()));
insuranceRecord.setPolicyFile(trimToNull(insuranceRecord.getPolicyFile()));
insuranceRecord.setRemark(trimToNull(insuranceRecord.getRemark()));
// 未填写金额禁止落库 -1 占位值,统一存 null
insuranceRecord.setInsuredAmount(normalizeEmptyAmount(insuranceRecord.getInsuredAmount()));
insuranceRecord.setPremium(normalizeEmptyAmount(insuranceRecord.getPremium()));
}
private void validate(InsuranceRecord insuranceRecord) {
@@ -421,11 +432,8 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
}
}
private BigDecimal nonNegative(BigDecimal value) {
if (Func.isEmpty(value) || value.compareTo(BigDecimal.ZERO) >= 0) {
return value;
}
return BigDecimal.ZERO;
private BigDecimal normalizeEmptyAmount(BigDecimal value) {
return Func.isNotEmpty(value) && value.compareTo(EMPTY_AMOUNT_SENTINEL) == 0 ? null : value;
}
private String normalizeVehicleType(String vehicleType) {
@@ -14,6 +14,7 @@ import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache;
import org.springblade.system.cache.DictCache;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.transport.excel.LoadingManageExcel;
@@ -42,6 +43,7 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -176,7 +178,11 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
.eq(LoadingManage::getLoadingNo, normalizedLoadingNo)
.eq(LoadingManage::getIsDeleted, 0), false);
if (existing != null) {
throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo);
if (!"批量导入".equals(existing.getDataSource())) {
throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo);
}
appendImportedWaybills(existing, waybills, normalizedLoadingNo);
return;
}
Waybill first = waybills.get(0);
Waybill last = waybills.get(waybills.size() - 1);
@@ -221,7 +227,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
loadingManage.setTaskInfoJson(first.getTaskInfoJson());
loadingManage.setDeptId(first.getDeptId());
loadingManage.setDeptName(first.getDeptName());
loadingManage.setBusinessStatus(STATUS_COMPLETED);
loadingManage.setBusinessStatus(STATUS_RUNNING.equals(first.getBusinessStatus()) ? STATUS_RUNNING : STATUS_COMPLETED);
save(loadingManage);
waybillMapper.update(null, Wrappers.<Waybill>lambdaUpdate()
@@ -229,6 +235,27 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
.set(Waybill::getLoadingNo, normalizedLoadingNo));
}
/**
* 同一配载标识号再次导入时,把新运单补进已有的导入配载单。
*/
private void appendImportedWaybills(LoadingManage existing, List<Waybill> waybills, String loadingNo) {
Set<Long> waybillIds = new LinkedHashSet<>(waybillIds(existing.getWaybillIdsJson()));
waybills.stream().map(Waybill::getId).filter(Objects::nonNull).forEach(waybillIds::add);
List<Long> mergedWaybillIds = new ArrayList<>(waybillIds);
List<Waybill> linkedWaybills = waybillMapper.selectList(Wrappers.<Waybill>lambdaQuery()
.in(Waybill::getId, mergedWaybillIds)
.eq(Waybill::getIsDeleted, 0));
existing.setWaybillIdsJson(JsonUtil.toJson(mergedWaybillIds));
existing.setLoadingSubNos(linkedWaybills.stream()
.map(Waybill::getWaybillNo)
.filter(Func::isNotEmpty)
.collect(Collectors.joining(",")));
updateById(existing);
waybillMapper.update(null, Wrappers.<Waybill>lambdaUpdate()
.in(Waybill::getId, mergedWaybillIds)
.set(Waybill::getLoadingNo, loadingNo));
}
/**
* 根据导入运单构建配载单的途经地:
* 组内运单按导入顺序串联路线,上一票的到货地址与下一票的发货地址相同(中途卸货点)时只保留一个点,
@@ -300,6 +327,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
LoadingManageExcel excel = new LoadingManageExcel();
BeanUtil.copyProperties(record, excel);
excel.setTransportType(transportTypeName(record.getTransportType()));
excel.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
excel.setCreateTime(record.getCreateTime());
excel.setUpdateTime(record.getUpdateTime());
excel.setBusinessStatus(LoadingManageWrapper.businessStatusName(record.getBusinessStatus()));
@@ -644,7 +672,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
return;
}
TransportBusinessSupport.validateRequired(loadingManage.getCarrierType(), "承运类型不能为空");
TransportBusinessSupport.validateRequired(loadingManage.getVehicleNo(), "车/船/航班/班列不能为空");
TransportBusinessSupport.validateRequired(loadingManage.getVehicleNo(), "车牌号/船班号/船次号/班次号不能为空");
TransportBusinessSupport.validateRequired(loadingManage.getDepartureAddress(), "发货地不能为空");
TransportBusinessSupport.validateRequired(loadingManage.getArrivalAddress(), "到货地不能为空");
if (Objects.equals(loadingManage.getCarrierType(), "承运商")
@@ -663,7 +691,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
TransportBusinessSupport.validateLength(loadingManage.getTransportType(), 100, "运输类型不能超过100字");
TransportBusinessSupport.validateLength(loadingManage.getCargoType(), 100, "货物类型不能超过100字");
TransportBusinessSupport.validateLength(loadingManage.getCargoName(), 100, "货物名称不能超过100字");
TransportBusinessSupport.validateLength(loadingManage.getVehicleNo(), 100, "车/船/航班/班列不能超过100字");
TransportBusinessSupport.validateLength(loadingManage.getVehicleNo(), 100, "车牌号/船班号/船次号/班次号不能超过100字");
TransportBusinessSupport.validateLength(loadingManage.getDriverName(), 100, "司机不能超过100字");
TransportBusinessSupport.validateLength(loadingManage.getDriverPhone(), 50, "联系电话不能超过50字");
TransportBusinessSupport.validateLength(loadingManage.getTrailerVehicleNo(), 100, "挂车车牌号不能超过100字");
@@ -60,11 +60,18 @@ public class MaintenancePlanServiceImpl extends BaseServiceImpl<MaintenancePlanM
private static final int MAINTENANCE_ITEM_MAX_LENGTH = 100;
private static final int ADDRESS_MAX_LENGTH = 100;
private static final int REMARK_MAX_LENGTH = 500;
/** 非必填里程未填写时的历史占位值,统一转为 null */
private static final BigDecimal EMPTY_MILEAGE_SENTINEL = BigDecimal.valueOf(-1);
@Override
public IPage<MaintenancePlanVO> selectMaintenancePlanPage(IPage<MaintenancePlanVO> page, MaintenancePlanVO maintenancePlan) {
List<MaintenancePlanVO> records = baseMapper.selectMaintenancePlanPage(page, maintenancePlan);
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
records.forEach(record -> {
record.setMileage(normalizeEmptyMileage(record.getMileage()));
record.setNextMaintenanceMileage(normalizeEmptyMileage(record.getNextMaintenanceMileage()));
record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
});
return page.setRecords(records);
}
@@ -80,7 +87,7 @@ public class MaintenancePlanServiceImpl extends BaseServiceImpl<MaintenancePlanM
@Transactional(rollbackFor = Exception.class)
public List<MaintenancePlanExcel> importMaintenancePlan(List<MaintenancePlanExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<MaintenancePlanExcel> errorList = new ArrayList<>();
List<MaintenancePlan> maintenancePlanList = new ArrayList<>();
@@ -103,8 +110,9 @@ public class MaintenancePlanServiceImpl extends BaseServiceImpl<MaintenancePlanM
}
}
if (Func.isNotEmpty(errorList)) {
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (MaintenancePlan maintenancePlan : maintenancePlanList) {
if (!save(maintenancePlan)) {
@@ -145,9 +153,10 @@ public class MaintenancePlanServiceImpl extends BaseServiceImpl<MaintenancePlanM
public List<MaintenancePlanExportExcel> exportMaintenancePlan(Wrapper<MaintenancePlan> queryWrapper) {
return list(queryWrapper).stream().map(maintenancePlan -> {
MaintenancePlanExportExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(maintenancePlan, MaintenancePlanExportExcel.class));
excel.setCreateUserName(UserCache.getUserRealName(maintenancePlan.getCreateUser()));
excel.setUpdateUserName(UserCache.getUserRealName(maintenancePlan.getUpdateUser()));
excel.setMileage(nonNegative(maintenancePlan.getMileage()));
excel.setNextMaintenanceMileage(nonNegative(maintenancePlan.getNextMaintenanceMileage()));
excel.setMileage(normalizeEmptyMileage(maintenancePlan.getMileage()));
excel.setNextMaintenanceMileage(normalizeEmptyMileage(maintenancePlan.getNextMaintenanceMileage()));
if (Func.isEmpty(excel.getMileageUnit())) {
excel.setMileageUnit(defaultMileageUnit(maintenancePlan.getVehicleType()));
}
@@ -164,6 +173,9 @@ public class MaintenancePlanServiceImpl extends BaseServiceImpl<MaintenancePlanM
maintenancePlan.setContactPhone(trimToNull(maintenancePlan.getContactPhone()));
maintenancePlan.setAddress(trimToNull(maintenancePlan.getAddress()));
maintenancePlan.setRemark(trimToNull(maintenancePlan.getRemark()));
// 非必填里程未填写禁止落库 -1 占位值
maintenancePlan.setMileage(normalizeEmptyMileage(maintenancePlan.getMileage()));
maintenancePlan.setNextMaintenanceMileage(normalizeEmptyMileage(maintenancePlan.getNextMaintenanceMileage()));
if (Func.isEmpty(maintenancePlan.getMileageUnit())) {
maintenancePlan.setMileageUnit(defaultMileageUnit(maintenancePlan.getVehicleType()));
}
@@ -218,11 +230,8 @@ public class MaintenancePlanServiceImpl extends BaseServiceImpl<MaintenancePlanM
}
}
private BigDecimal nonNegative(BigDecimal value) {
if (Func.isEmpty(value) || value.compareTo(BigDecimal.ZERO) >= 0) {
return value;
}
return BigDecimal.ZERO;
private BigDecimal normalizeEmptyMileage(BigDecimal value) {
return Func.isNotEmpty(value) && value.compareTo(EMPTY_MILEAGE_SENTINEL) == 0 ? null : value;
}
private String defaultMileageUnit(String vehicleType) {
@@ -235,7 +244,7 @@ public class MaintenancePlanServiceImpl extends BaseServiceImpl<MaintenancePlanM
private String trimToNull(String value) {
String trimValue = trimToEmpty(value);
return trimValue.isEmpty() ? null : trimValue;
return trimValue.isEmpty() || "-1".equals(trimValue) ? null : trimValue;
}
}
@@ -66,7 +66,10 @@ public class MaintenanceRecordServiceImpl extends BaseServiceImpl<MaintenanceRec
@Override
public IPage<MaintenanceRecordVO> selectMaintenanceRecordPage(IPage<MaintenanceRecordVO> page, MaintenanceRecordVO maintenanceRecord) {
List<MaintenanceRecordVO> records = baseMapper.selectMaintenanceRecordPage(page, maintenanceRecord);
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
records.forEach(record -> {
record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
});
return page.setRecords(records);
}
@@ -82,7 +85,7 @@ public class MaintenanceRecordServiceImpl extends BaseServiceImpl<MaintenanceRec
@Transactional(rollbackFor = Exception.class)
public List<MaintenanceRecordExcel> importMaintenanceRecord(List<MaintenanceRecordExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<MaintenanceRecordExcel> errorList = new ArrayList<>();
List<MaintenanceRecord> maintenanceRecordList = new ArrayList<>();
@@ -107,8 +110,9 @@ public class MaintenanceRecordServiceImpl extends BaseServiceImpl<MaintenanceRec
}
}
if (Func.isNotEmpty(errorList)) {
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (MaintenanceRecord maintenanceRecord : maintenanceRecordList) {
if (!save(maintenanceRecord)) {
@@ -143,6 +147,7 @@ public class MaintenanceRecordServiceImpl extends BaseServiceImpl<MaintenanceRec
MaintenanceRecordExportExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(maintenanceRecord, MaintenanceRecordExportExcel.class));
excel.setCost(scaleAmount(maintenanceRecord.getCost()));
excel.setMileage(scaleAmount(nonNegative(normalizeMileage(maintenanceRecord.getMileage()))));
excel.setCreateUserName(UserCache.getUserRealName(maintenanceRecord.getCreateUser()));
excel.setUpdateUserName(UserCache.getUserRealName(maintenanceRecord.getUpdateUser()));
if (Func.isEmpty(excel.getMileageUnit())) {
excel.setMileageUnit(defaultMileageUnit(maintenanceRecord.getVehicleType()));
@@ -28,6 +28,8 @@ import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.ITransportPlanService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.WaybillProcessSupport;
import org.springblade.transport.wrapper.WaybillWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -92,16 +94,42 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
@Transactional(rollbackFor = Exception.class)
public MasterOrderVO submit(MasterOrderVO request, boolean draft) {
boolean created = Func.isEmpty(request.getId());
MasterOrder target = created ? new MasterOrder() : getRequired(request.getId());
if (!created && "completed".equals(target.getBusinessStatus())) {
assertRestrictedEdit(target, request);
MasterOrder oldRecord = created ? null : getRequired(request.getId());
MasterOrder target = created ? new MasterOrder()
: Objects.requireNonNull(BeanUtil.copyProperties(oldRecord, MasterOrder.class));
String previousStatus = oldRecord == null ? null : oldRecord.getBusinessStatus();
if (!created && "closed".equals(previousStatus)) {
throw new ServiceException("调度关闭的总单不能编辑");
}
// 非草稿编辑:基本信息与路线不可改
if (!created && !"draft".equals(previousStatus)) {
assertRestrictedEdit(oldRecord, request);
}
if (!created && "closed".equals(target.getBusinessStatus())) throw new ServiceException("调度关闭的总单不能编辑");
copyWritable(request, target);
if (created) target.setMasterNo(nextCode());
if (!created && !"draft".equals(previousStatus)) {
restoreBasicAndRoute(target, oldRecord);
}
if (created) {
target.setMasterNo(nextCode());
}
target.setTransportOrganizationType("多式联运");
target.setBusinessStatus(draft ? "draft" : ("dispatching".equals(target.getBusinessStatus()) ? "dispatching" : "waiting_dispatch"));
if (draft) {
// 仅新建或原草稿可暂存为 draft;已进入待调度及之后状态的编辑保持原状态
if (created || Func.isEmpty(previousStatus) || "draft".equals(previousStatus)) {
target.setBusinessStatus("draft");
} else {
target.setBusinessStatus(previousStatus);
}
} else if ("dispatching".equals(previousStatus) || "completed".equals(previousStatus)) {
// 调度中 / 调度完成编辑后保持原状态,避免误回退为待调度
target.setBusinessStatus(previousStatus);
} else {
target.setBusinessStatus("waiting_dispatch");
}
validate(target, draft);
if (!created) {
validateGoodsAgainstDispatched(oldRecord, target);
}
saveOrUpdate(target);
return detail(target.getId());
}
@@ -210,8 +238,15 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
row.setVehicleNo(waybill.getVehicleNo());
row.setStartDate(waybill.getStartDate());
row.setEndDate(waybill.getEndDate());
row.setBusinessStatus(waybill.getBusinessStatus());
// 与前端运单状态展示一致:先按接单配置校正,再转中文
row.setBusinessStatus(WaybillWrapper.businessStatusName(
WaybillProcessSupport.normalizeBusinessStatus(
waybill.getBusinessStatus(),
waybill.getProcessJson(),
waybill.getDriverAcceptStatus())));
row.setRemark(waybill.getRemark());
row.setCreateUserName(UserCache.getUserRealName(waybill.getCreateUser()));
row.setUpdateTime(waybill.getUpdateTime());
row.setCreateTime(waybill.getCreateTime());
result.add(row);
}
@@ -302,6 +337,7 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
List<Waybill> boundWaybills = waybillsByMasterNo(entity.getMasterNo());
vo.setBoundWaybills(boundWaybills);
vo.setRouteProgress(buildProgress(entity, vo.getRoutes(), boundWaybills));
vo.setDispatchedGoods(maxDispatchedGoodsByCategory(entity));
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
return vo;
}
@@ -343,7 +379,89 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
}
private void assertRestrictedEdit(MasterOrder oldRecord, MasterOrderVO request) {
if (!Objects.equals(oldRecord.getProjectId(), request.getProjectId()) || !Objects.equals(oldRecord.getRouteJson(), JsonUtil.toJson(request.getRoutes()))) throw new ServiceException("调度完成的总单不能修改基本信息和路线");
boolean basicChanged = !Objects.equals(oldRecord.getProjectId(), request.getProjectId())
|| !Objects.equals(oldRecord.getContractId(), request.getContractId())
|| !Objects.equals(normalizeText(oldRecord.getRemark()), normalizeText(request.getRemark()));
boolean routeChanged = !Objects.equals(normalizeText(oldRecord.getDepartureName()), normalizeText(request.getDepartureName()))
|| !Objects.equals(normalizeText(oldRecord.getDepartureAddress()), normalizeText(request.getDepartureAddress()))
|| !Objects.equals(normalizeText(oldRecord.getDepartureContact()), normalizeText(request.getDepartureContact()))
|| !Objects.equals(normalizeText(oldRecord.getDeparturePhone()), normalizeText(request.getDeparturePhone()))
|| !Objects.equals(normalizeText(oldRecord.getArrivalName()), normalizeText(request.getArrivalName()))
|| !Objects.equals(normalizeText(oldRecord.getArrivalAddress()), normalizeText(request.getArrivalAddress()))
|| !Objects.equals(normalizeText(oldRecord.getArrivalContact()), normalizeText(request.getArrivalContact()))
|| !Objects.equals(normalizeText(oldRecord.getArrivalPhone()), normalizeText(request.getArrivalPhone()))
|| !Objects.equals(oldRecord.getPlanStartTime(), request.getPlanStartTime())
|| !Objects.equals(oldRecord.getPlanEndTime(), request.getPlanEndTime())
|| !Objects.equals(normalizeText(oldRecord.getRouteJson()), normalizeText(JsonUtil.toJson(request.getRoutes())));
if (basicChanged || routeChanged) {
throw new ServiceException("当前总单不能修改基本信息和路线");
}
}
/** 非草稿编辑时强制保留库中基本信息与路线,避免前端绕过禁用项提交 */
private void restoreBasicAndRoute(MasterOrder target, MasterOrder oldRecord) {
target.setProjectId(oldRecord.getProjectId());
target.setProjectName(oldRecord.getProjectName());
target.setContractId(oldRecord.getContractId());
target.setContractName(oldRecord.getContractName());
target.setCustomerName(oldRecord.getCustomerName());
target.setRemark(oldRecord.getRemark());
target.setDepartureName(oldRecord.getDepartureName());
target.setDepartureAddress(oldRecord.getDepartureAddress());
target.setDepartureContact(oldRecord.getDepartureContact());
target.setDeparturePhone(oldRecord.getDeparturePhone());
target.setArrivalName(oldRecord.getArrivalName());
target.setArrivalAddress(oldRecord.getArrivalAddress());
target.setArrivalContact(oldRecord.getArrivalContact());
target.setArrivalPhone(oldRecord.getArrivalPhone());
target.setPlanStartTime(oldRecord.getPlanStartTime());
target.setPlanEndTime(oldRecord.getPlanEndTime());
target.setRouteJson(oldRecord.getRouteJson());
}
/**
* 编辑时货物数量不得低于该品类已调度量(各分段已调度量取最大值)。
*/
private void validateGoodsAgainstDispatched(MasterOrder oldRecord, MasterOrder target) {
Map<String, BigDecimal> maxDispatched = maxDispatchedGoodsByCategory(oldRecord);
if (maxDispatched.isEmpty()) {
return;
}
Map<String, BigDecimal> newAvailable = availableGoods(target);
for (Map.Entry<String, BigDecimal> entry : maxDispatched.entrySet()) {
BigDecimal dispatched = entry.getValue();
if (dispatched == null || dispatched.compareTo(BigDecimal.ZERO) <= 0) {
continue;
}
BigDecimal newQuantity = newAvailable.getOrDefault(entry.getKey(), BigDecimal.ZERO);
if (newQuantity.compareTo(dispatched) < 0) {
String cargoName = entry.getKey().contains("\u0000")
? entry.getKey().substring(0, entry.getKey().indexOf('\u0000'))
: entry.getKey();
throw new ServiceException("货物【" + cargoName + "】数量不能低于已调度量("
+ dispatched.stripTrailingZeros().toPlainString() + ")");
}
}
}
/** 各品类已调度量:同一品类取各分段已调度量的最大值 */
private Map<String, BigDecimal> maxDispatchedGoodsByCategory(MasterOrder masterOrder) {
Map<String, BigDecimal> result = new LinkedHashMap<>();
List<Map<String, Object>> routes = parseArray(masterOrder.getRouteJson());
if (routes.isEmpty()) {
return dispatchedGoods(masterOrder.getMasterNo(), null);
}
for (Map<String, Object> route : routes) {
Map<String, BigDecimal> dispatched = dispatchedGoods(masterOrder.getMasterNo(), string(route, "segmentNo"));
for (Map.Entry<String, BigDecimal> entry : dispatched.entrySet()) {
result.merge(entry.getKey(), entry.getValue(), BigDecimal::max);
}
}
return result;
}
private String normalizeText(String value) {
return value == null ? "" : value.trim();
}
private void validate(MasterOrder masterOrder, boolean draft) {
@@ -403,10 +521,13 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
String segmentNo = string(route, "segmentNo");
BigDecimal requestQuantity = requestQuantityBySegment.get(segmentNo);
if (requestQuantity == null) continue;
BigDecimal dispatchTotal = routeDispatchedQuantity(masterOrder, route).add(requestQuantity);
BigDecimal previousCompletedQuantity = completedQuantity(masterOrder.getMasterNo(), routes.get(index - 1));
if (previousCompletedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException(segmentNo + "需等上一段运单完成到达后才能调度");
}
BigDecimal dispatchTotal = routeDispatchedQuantity(masterOrder, route).add(requestQuantity);
if (dispatchTotal.compareTo(previousCompletedQuantity) > 0) {
throw new ServiceException(segmentNo + "调度总量不能大于上一段已完成数量(" + previousCompletedQuantity.stripTrailingZeros().toPlainString() + ")");
throw new ServiceException(segmentNo + "调度总量不能大于上一段已到达数量(" + previousCompletedQuantity.stripTrailingZeros().toPlainString() + ")");
}
}
}
@@ -58,13 +58,22 @@ public class MileageRecordServiceImpl extends BaseServiceImpl<MileageRecordMappe
private static final int REMARK_MAX_LENGTH = 200;
private static final int ATTACHMENTS_MAX_LENGTH = 1000;
private static final int MILEAGE_SCALE = 2;
/** 非必填里程未填写时的历史占位值,统一转为 null */
private static final BigDecimal EMPTY_MILEAGE_SENTINEL = BigDecimal.valueOf(-1);
private static final String VEHICLE_TYPE_VEHICLE = "车辆";
private static final String MILEAGE_UNIT = "公里";
@Override
public IPage<MileageRecordVO> selectMileageRecordPage(IPage<MileageRecordVO> page, MileageRecordVO mileageRecord) {
List<MileageRecordVO> records = baseMapper.selectMileageRecordPage(page, mileageRecord);
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
records.forEach(record -> {
record.setPreviousMonthMileage(normalizeEmptyMileage(record.getPreviousMonthMileage()));
record.setCurrentMonthMileage(normalizeEmptyMileage(record.getCurrentMonthMileage()));
record.setMonthlyMileage(normalizeEmptyMileage(record.getMonthlyMileage()));
record.setTotalMileage(normalizeEmptyMileage(record.getTotalMileage()));
record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
});
return page.setRecords(records);
}
@@ -81,7 +90,7 @@ public class MileageRecordServiceImpl extends BaseServiceImpl<MileageRecordMappe
@Transactional(rollbackFor = Exception.class)
public List<MileageRecordExcel> importMileageRecord(List<MileageRecordExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<MileageRecordExcel> errorList = new ArrayList<>();
List<MileageRecord> mileageRecordList = new ArrayList<>();
@@ -105,8 +114,9 @@ public class MileageRecordServiceImpl extends BaseServiceImpl<MileageRecordMappe
}
}
if (Func.isNotEmpty(errorList)) {
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (MileageRecord mileageRecord : mileageRecordList) {
if (!save(mileageRecord)) {
@@ -144,10 +154,11 @@ public class MileageRecordServiceImpl extends BaseServiceImpl<MileageRecordMappe
MileageRecordExportExcel excel = Objects.requireNonNull(
BeanUtil.copyProperties(mileageRecord, MileageRecordExportExcel.class)
);
excel.setPreviousMonthMileage(nonNegative(mileageRecord.getPreviousMonthMileage()));
excel.setCurrentMonthMileage(nonNegative(mileageRecord.getCurrentMonthMileage()));
excel.setMonthlyMileage(nonNegative(mileageRecord.getMonthlyMileage()));
excel.setTotalMileage(nonNegative(mileageRecord.getTotalMileage()));
excel.setPreviousMonthMileage(normalizeEmptyMileage(mileageRecord.getPreviousMonthMileage()));
excel.setCurrentMonthMileage(normalizeEmptyMileage(mileageRecord.getCurrentMonthMileage()));
excel.setMonthlyMileage(normalizeEmptyMileage(mileageRecord.getMonthlyMileage()));
excel.setTotalMileage(normalizeEmptyMileage(mileageRecord.getTotalMileage()));
excel.setCreateUserName(UserCache.getUserRealName(mileageRecord.getCreateUser()));
excel.setUpdateUserName(UserCache.getUserRealName(mileageRecord.getUpdateUser()));
return excel;
}).toList();
@@ -159,6 +170,11 @@ public class MileageRecordServiceImpl extends BaseServiceImpl<MileageRecordMappe
mileageRecord.setMileageUnit(MILEAGE_UNIT);
mileageRecord.setRemark(trimToNull(mileageRecord.getRemark()));
mileageRecord.setAttachments(trimToNull(mileageRecord.getAttachments()));
// 非必填里程未填写禁止落库 -1 占位值
mileageRecord.setPreviousMonthMileage(normalizeEmptyMileage(mileageRecord.getPreviousMonthMileage()));
mileageRecord.setCurrentMonthMileage(normalizeEmptyMileage(mileageRecord.getCurrentMonthMileage()));
mileageRecord.setMonthlyMileage(normalizeEmptyMileage(mileageRecord.getMonthlyMileage()));
mileageRecord.setTotalMileage(normalizeEmptyMileage(mileageRecord.getTotalMileage()));
if (mileageRecord.getMonthlyMileage() == null
&& mileageRecord.getPreviousMonthMileage() != null
&& mileageRecord.getCurrentMonthMileage() != null) {
@@ -234,11 +250,8 @@ public class MileageRecordServiceImpl extends BaseServiceImpl<MileageRecordMappe
}
}
private BigDecimal nonNegative(BigDecimal value) {
if (Func.isEmpty(value) || value.compareTo(BigDecimal.ZERO) >= 0) {
return value;
}
return BigDecimal.ZERO;
private BigDecimal normalizeEmptyMileage(BigDecimal value) {
return Func.isNotEmpty(value) && value.compareTo(EMPTY_MILEAGE_SENTINEL) == 0 ? null : value;
}
private String normalizeVehicleNo(String vehicleNo) {
@@ -251,7 +264,7 @@ public class MileageRecordServiceImpl extends BaseServiceImpl<MileageRecordMappe
private String trimToNull(String value) {
String trimValue = trimToEmpty(value);
return trimValue.isEmpty() ? null : trimValue;
return trimValue.isEmpty() || "-1".equals(trimValue) ? null : trimValue;
}
}
@@ -45,6 +45,8 @@ public class OilElectricRecordServiceImpl extends BaseServiceImpl<OilElectricRec
private static final int ATTACHMENTS_MAX_LENGTH = 1000;
private static final int MONEY_SCALE = 2;
private static final int QUANTITY_SCALE = 2;
/** 非必填数值未填写时的历史占位值,统一转为 null */
private static final BigDecimal EMPTY_AMOUNT_SENTINEL = BigDecimal.valueOf(-1);
private static final String VEHICLE = "车辆";
private static final String SHIP = "船舶";
private static final String FUEL = "加油";
@@ -54,7 +56,14 @@ public class OilElectricRecordServiceImpl extends BaseServiceImpl<OilElectricRec
@Override
public IPage<OilElectricRecordVO> selectOilElectricRecordPage(IPage<OilElectricRecordVO> page, OilElectricRecordVO oilElectricRecord) {
List<OilElectricRecordVO> records = baseMapper.selectOilElectricRecordPage(page, oilElectricRecord);
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
records.forEach(record -> {
record.setQuantity(normalizeEmptyAmount(record.getQuantity()));
record.setUnitPrice(normalizeEmptyAmount(record.getUnitPrice()));
record.setTransactionAmount(normalizeEmptyAmount(record.getTransactionAmount()));
record.setBalance(normalizeEmptyAmount(record.getBalance()));
record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
});
return page.setRecords(records);
}
@@ -70,7 +79,7 @@ public class OilElectricRecordServiceImpl extends BaseServiceImpl<OilElectricRec
@Transactional(rollbackFor = Exception.class)
public List<OilElectricRecordExcel> importOilElectricRecord(List<OilElectricRecordExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<OilElectricRecordExcel> errorList = new ArrayList<>();
List<OilElectricRecord> oilElectricRecordList = new ArrayList<>();
@@ -94,8 +103,9 @@ public class OilElectricRecordServiceImpl extends BaseServiceImpl<OilElectricRec
}
}
if (Func.isNotEmpty(errorList)) {
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (OilElectricRecord oilElectricRecord : oilElectricRecordList) {
if (!save(oilElectricRecord)) {
@@ -137,10 +147,11 @@ public class OilElectricRecordServiceImpl extends BaseServiceImpl<OilElectricRec
public List<OilElectricRecordExportExcel> exportOilElectricRecord(Wrapper<OilElectricRecord> queryWrapper) {
return list(queryWrapper).stream().map(oilElectricRecord -> {
OilElectricRecordExportExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(oilElectricRecord, OilElectricRecordExportExcel.class));
excel.setQuantity(nonNegative(oilElectricRecord.getQuantity()));
excel.setUnitPrice(nonNegative(oilElectricRecord.getUnitPrice()));
excel.setTransactionAmount(nonNegative(oilElectricRecord.getTransactionAmount()));
excel.setBalance(nonNegative(oilElectricRecord.getBalance()));
excel.setQuantity(normalizeEmptyAmount(oilElectricRecord.getQuantity()));
excel.setUnitPrice(normalizeEmptyAmount(oilElectricRecord.getUnitPrice()));
excel.setTransactionAmount(normalizeEmptyAmount(oilElectricRecord.getTransactionAmount()));
excel.setBalance(normalizeEmptyAmount(oilElectricRecord.getBalance()));
excel.setCreateUserName(UserCache.getUserRealName(oilElectricRecord.getCreateUser()));
excel.setUpdateUserName(UserCache.getUserRealName(oilElectricRecord.getUpdateUser()));
return excel;
}).toList();
@@ -160,6 +171,11 @@ public class OilElectricRecordServiceImpl extends BaseServiceImpl<OilElectricRec
oilElectricRecord.setDataSource(defaultDataSource(oilElectricRecord.getDataSource()));
oilElectricRecord.setAttachments(trimToNull(oilElectricRecord.getAttachments()));
oilElectricRecord.setRemark(trimToNull(oilElectricRecord.getRemark()));
// 非必填数值未填写禁止落库 -1;交易金额必填,-1 转 null 后由校验拦截
oilElectricRecord.setQuantity(normalizeEmptyAmount(oilElectricRecord.getQuantity()));
oilElectricRecord.setUnitPrice(normalizeEmptyAmount(oilElectricRecord.getUnitPrice()));
oilElectricRecord.setTransactionAmount(normalizeEmptyAmount(oilElectricRecord.getTransactionAmount()));
oilElectricRecord.setBalance(normalizeEmptyAmount(oilElectricRecord.getBalance()));
if (ELECTRIC.equals(oilElectricRecord.getFeeType())) {
oilElectricRecord.setOilProduct(null);
}
@@ -222,11 +238,8 @@ public class OilElectricRecordServiceImpl extends BaseServiceImpl<OilElectricRec
}
}
private BigDecimal nonNegative(BigDecimal value) {
if (Func.isEmpty(value) || value.compareTo(BigDecimal.ZERO) >= 0) {
return value;
}
return BigDecimal.ZERO;
private BigDecimal normalizeEmptyAmount(BigDecimal value) {
return Func.isNotEmpty(value) && value.compareTo(EMPTY_AMOUNT_SENTINEL) == 0 ? null : value;
}
private String defaultDataSource(String dataSource) {
@@ -245,7 +258,7 @@ public class OilElectricRecordServiceImpl extends BaseServiceImpl<OilElectricRec
private String trimToNull(String value) {
String trimValue = trimToEmpty(value);
return trimValue.isEmpty() ? null : trimValue;
return trimValue.isEmpty() || "-1".equals(trimValue) ? null : trimValue;
}
}
@@ -51,7 +51,10 @@ public class OtherExpenseRecordServiceImpl extends BaseServiceImpl<OtherExpenseR
@Override
public IPage<OtherExpenseRecordVO> selectOtherExpenseRecordPage(IPage<OtherExpenseRecordVO> page, OtherExpenseRecordVO otherExpenseRecord) {
List<OtherExpenseRecordVO> records = baseMapper.selectOtherExpenseRecordPage(page, otherExpenseRecord);
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
records.forEach(record -> {
record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
});
return page.setRecords(records);
}
@@ -67,7 +70,7 @@ public class OtherExpenseRecordServiceImpl extends BaseServiceImpl<OtherExpenseR
@Transactional(rollbackFor = Exception.class)
public List<OtherExpenseRecordExcel> importOtherExpenseRecord(List<OtherExpenseRecordExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<OtherExpenseRecordExcel> errorList = new ArrayList<>();
List<OtherExpenseRecord> otherExpenseRecordList = new ArrayList<>();
@@ -92,8 +95,9 @@ public class OtherExpenseRecordServiceImpl extends BaseServiceImpl<OtherExpenseR
}
}
if (Func.isNotEmpty(errorList)) {
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (OtherExpenseRecord otherExpenseRecord : otherExpenseRecordList) {
if (!save(otherExpenseRecord)) {
@@ -129,6 +133,7 @@ public class OtherExpenseRecordServiceImpl extends BaseServiceImpl<OtherExpenseR
return list(queryWrapper).stream().map(otherExpenseRecord -> {
OtherExpenseRecordExportExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(otherExpenseRecord, OtherExpenseRecordExportExcel.class));
excel.setAmount(nonNegative(otherExpenseRecord.getAmount()));
excel.setCreateUserName(UserCache.getUserRealName(otherExpenseRecord.getCreateUser()));
excel.setUpdateUserName(UserCache.getUserRealName(otherExpenseRecord.getUpdateUser()));
return excel;
}).toList();
@@ -252,12 +252,10 @@ public class ProcessConfigServiceImpl extends BaseServiceImpl<ProcessConfigMappe
}
private LambdaQueryWrapper<ProcessConfig> buildQuery(ProcessConfigVO processConfig) {
TransportBusinessSupport.validateAllDept(processConfig.getAllDept(), "过程配置");
LambdaQueryWrapper<ProcessConfig> queryWrapper = Wrappers.<ProcessConfig>lambdaQuery().eq(ProcessConfig::getIsDeleted, 0);
if (!Objects.equals(processConfig.getAllDept(), 1)) {
queryWrapper.eq(ProcessConfig::getDeptId, TransportBusinessSupport.currentDeptId("过程配置"));
} else if (Func.isNotEmpty(processConfig.getDeptId())) {
queryWrapper.eq(ProcessConfig::getDeptId, processConfig.getDeptId());
List<Long> deptIdList = TransportBusinessSupport.resolveDeptFilterIds(processConfig.getAllDept(), processConfig.getDeptId(), "过程配置");
if (Func.isNotEmpty(deptIdList)) {
queryWrapper.eq(ProcessConfig::getDeptId, deptIdList.get(0));
}
if (Func.isNotEmpty(processConfig.getConfigCode())) {
queryWrapper.eq(ProcessConfig::getConfigCode, processConfig.getConfigCode());
@@ -337,7 +337,12 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
private LambdaQueryWrapper<ProjectApply> buildQuery(ProjectApplyVO projectApply) {
TransportBusinessSupport.validateAllDept(projectApply.getAllDept(), "项目立项");
LambdaQueryWrapper<ProjectApply> queryWrapper = Wrappers.<ProjectApply>lambdaQuery().eq(ProjectApply::getIsDeleted, 0);
if (!Objects.equals(projectApply.getAllDept(), 1)) {
if (TransportBusinessSupport.isAdministrator()) {
// 超管默认可查看全部未删除数据;仅在明确指定承办组织时按组织精确筛选。
if (Objects.equals(projectApply.getAllDept(), 1) && Func.isNotEmpty(projectApply.getUndertakeDeptId())) {
queryWrapper.eq(ProjectApply::getUndertakeDeptId, projectApply.getUndertakeDeptId());
}
} else if (!Objects.equals(projectApply.getAllDept(), 1)) {
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目立项");
Long currentUserId = AuthUtil.getUserId();
queryWrapper.and(wrapper -> wrapper.eq(ProjectApply::getUndertakeDeptId, currentDeptId)
@@ -946,7 +951,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
}
private boolean canCurrentUserOperate(ProjectApply projectApply) {
if (AuthUtil.isAdministrator()) {
if (TransportBusinessSupport.isAdministrator()) {
return true;
}
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目立项");
@@ -173,7 +173,7 @@ public class ShippingTemplateServiceImpl extends BaseServiceImpl<ShippingTemplat
@Override
public List<ShippingTemplateGoodsExcel> importGoods(List<ShippingTemplateGoodsExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<ShippingTemplateGoodsExcel> failures = new ArrayList<>();
for (int index = 0; index < data.size(); index++) {
@@ -185,7 +185,11 @@ public class TemporaryCreditLimitServiceImpl extends BaseServiceImpl<TemporaryCr
private LambdaQueryWrapper<TemporaryCreditLimit> buildQuery(TemporaryCreditLimitVO temporaryCreditLimit) {
TransportBusinessSupport.validateAllDept(temporaryCreditLimit.getAllDept(), "临时额度管理");
LambdaQueryWrapper<TemporaryCreditLimit> queryWrapper = Wrappers.<TemporaryCreditLimit>lambdaQuery().eq(TemporaryCreditLimit::getIsDeleted, 0);
if (!Objects.equals(temporaryCreditLimit.getAllDept(), 1)) {
if (TransportBusinessSupport.isAdministrator()) {
if (Objects.equals(temporaryCreditLimit.getAllDept(), 1) && Func.isNotEmpty(temporaryCreditLimit.getApplyDeptId())) {
queryWrapper.eq(TemporaryCreditLimit::getApplyDeptId, temporaryCreditLimit.getApplyDeptId());
}
} else if (!Objects.equals(temporaryCreditLimit.getAllDept(), 1)) {
queryWrapper.eq(TemporaryCreditLimit::getApplyDeptId, TransportBusinessSupport.currentDeptId("临时额度管理"));
} else if (Func.isNotEmpty(temporaryCreditLimit.getApplyDeptId())) {
queryWrapper.eq(TemporaryCreditLimit::getApplyDeptId, temporaryCreditLimit.getApplyDeptId());
@@ -62,11 +62,19 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
private static final int REMARK_MAX_LENGTH = 500;
private static final int ATTACHMENTS_MAX_LENGTH = 1000;
private static final int MONEY_SCALE = 2;
/** 非必填数量/金额未填写时的历史占位值,统一转为 null */
private static final BigDecimal EMPTY_AMOUNT_SENTINEL = BigDecimal.valueOf(-1);
private static final Integer EMPTY_INTEGER_SENTINEL = -1;
@Override
public IPage<TireReplacementRecordVO> selectTireReplacementRecordPage(IPage<TireReplacementRecordVO> page, TireReplacementRecordVO tireReplacementRecord) {
List<TireReplacementRecordVO> records = baseMapper.selectTireReplacementRecordPage(page, tireReplacementRecord);
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
records.forEach(record -> {
record.setTireQuantity(normalizeEmptyInteger(record.getTireQuantity()));
record.setReplacementCost(normalizeEmptyAmount(record.getReplacementCost()));
record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
});
return page.setRecords(records);
}
@@ -82,7 +90,7 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
@Transactional(rollbackFor = Exception.class)
public List<TireReplacementRecordExcel> importTireReplacementRecord(List<TireReplacementRecordExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<TireReplacementRecordExcel> errorList = new ArrayList<>();
List<TireReplacementRecord> tireReplacementRecordList = new ArrayList<>();
@@ -107,8 +115,9 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
}
}
if (Func.isNotEmpty(errorList)) {
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (TireReplacementRecord tireReplacementRecord : tireReplacementRecordList) {
if (!save(tireReplacementRecord)) {
@@ -152,8 +161,9 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
public List<TireReplacementRecordExportExcel> exportTireReplacementRecord(Wrapper<TireReplacementRecord> queryWrapper) {
return list(queryWrapper).stream().map(tireReplacementRecord -> {
TireReplacementRecordExportExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(tireReplacementRecord, TireReplacementRecordExportExcel.class));
excel.setTireQuantity(validQuantity(tireReplacementRecord.getTireQuantity()));
excel.setReplacementCost(nonNegative(tireReplacementRecord.getReplacementCost()));
excel.setTireQuantity(normalizeEmptyInteger(tireReplacementRecord.getTireQuantity()));
excel.setReplacementCost(normalizeEmptyAmount(tireReplacementRecord.getReplacementCost()));
excel.setCreateUserName(UserCache.getUserRealName(tireReplacementRecord.getCreateUser()));
excel.setUpdateUserName(UserCache.getUserRealName(tireReplacementRecord.getUpdateUser()));
return excel;
}).toList();
@@ -166,6 +176,9 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
tireReplacementRecord.setReplacementDescription(trimToNull(tireReplacementRecord.getReplacementDescription()));
tireReplacementRecord.setAttachments(trimToNull(tireReplacementRecord.getAttachments()));
tireReplacementRecord.setRemark(trimToNull(tireReplacementRecord.getRemark()));
// 非必填数量及金额占位值 -1 统一存 null(费用必填,-1 转 null 后由校验拦截)
tireReplacementRecord.setTireQuantity(normalizeEmptyInteger(tireReplacementRecord.getTireQuantity()));
tireReplacementRecord.setReplacementCost(normalizeEmptyAmount(tireReplacementRecord.getReplacementCost()));
}
private void validate(TireReplacementRecord tireReplacementRecord) {
@@ -213,18 +226,12 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
}
}
private BigDecimal nonNegative(BigDecimal value) {
if (Func.isEmpty(value) || value.compareTo(BigDecimal.ZERO) >= 0) {
return value;
}
return BigDecimal.ZERO;
private BigDecimal normalizeEmptyAmount(BigDecimal value) {
return Func.isNotEmpty(value) && value.compareTo(EMPTY_AMOUNT_SENTINEL) == 0 ? null : value;
}
private Integer validQuantity(Integer value) {
if (Func.isEmpty(value) || value > 0) {
return value;
}
return null;
private Integer normalizeEmptyInteger(Integer value) {
return Func.isNotEmpty(value) && value.equals(EMPTY_INTEGER_SENTINEL) ? null : value;
}
private String trimToEmpty(String value) {
@@ -233,7 +240,7 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
private String trimToNull(String value) {
String trimValue = trimToEmpty(value);
return trimValue.isEmpty() ? null : trimValue;
return trimValue.isEmpty() || "-1".equals(trimValue) ? null : trimValue;
}
}
@@ -65,7 +65,10 @@ public class TransportChangeRecordServiceImpl extends BaseServiceImpl<TransportC
@Override
public IPage<TransportChangeRecordVO> selectTransportChangeRecordPage(IPage<TransportChangeRecordVO> page, TransportChangeRecordVO transportChangeRecord) {
List<TransportChangeRecordVO> records = baseMapper.selectTransportChangeRecordPage(page, transportChangeRecord);
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
records.forEach(record -> {
record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
});
return page.setRecords(records);
}
@@ -81,7 +84,7 @@ public class TransportChangeRecordServiceImpl extends BaseServiceImpl<TransportC
@Transactional(rollbackFor = Exception.class)
public List<TransportChangeRecordExcel> importTransportChangeRecord(List<TransportChangeRecordExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<TransportChangeRecordExcel> errorList = new ArrayList<>();
List<TransportChangeRecord> transportChangeRecordList = new ArrayList<>();
@@ -104,8 +107,9 @@ public class TransportChangeRecordServiceImpl extends BaseServiceImpl<TransportC
}
}
if (Func.isNotEmpty(errorList)) {
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (TransportChangeRecord transportChangeRecord : transportChangeRecordList) {
if (!save(transportChangeRecord)) {
@@ -134,6 +138,7 @@ public class TransportChangeRecordServiceImpl extends BaseServiceImpl<TransportC
return list(queryWrapper).stream()
.map(transportChangeRecord -> {
TransportChangeRecordExportExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(transportChangeRecord, TransportChangeRecordExportExcel.class));
excel.setCreateUserName(UserCache.getUserRealName(transportChangeRecord.getCreateUser()));
excel.setUpdateUserName(UserCache.getUserRealName(transportChangeRecord.getUpdateUser()));
return excel;
})
@@ -191,7 +196,7 @@ public class TransportChangeRecordServiceImpl extends BaseServiceImpl<TransportC
private String trimToNull(String value) {
String trimValue = trimToEmpty(value);
return trimValue.isEmpty() ? null : trimValue;
return trimValue.isEmpty() || "-1".equals(trimValue) ? null : trimValue;
}
}
@@ -198,7 +198,7 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
@Override
public List<TransportPlanImportExcel> validateTransportPlan(List<TransportPlanImportExcel> data, Long projectId, String projectName, Long contractId, String contractName, String customerName) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
if (Func.isEmpty(projectId)) {
throw new ServiceException("项目不能为空");
@@ -250,7 +250,7 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
@Transactional(rollbackFor = Exception.class)
public List<TransportPlanImportExcel> importTransportPlan(List<TransportPlanImportExcel> data, Long projectId, String projectName, Long contractId, String contractName, String customerName) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
if (Func.isEmpty(projectId)) {
throw new ServiceException("项目不能为空");
@@ -294,8 +294,9 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
// 如果有校验错误,回滚事务
if (Func.isNotEmpty(errorMap)) {
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return new ArrayList<>(errorMap.values());
// 任一行失败即整批回滚:抛异常触发事务回滚(替代旧的 setRollbackOnly),
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
// 第二阶段:批量导入
@@ -453,12 +454,14 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
}
}
// 5. 校验数量和里程为正数
// 5. 校验数量和里程非负。
// 文案与规则必须一致:条件只拦 < 0(即 0 是合法值),就不能写「必须为正数」。
// 新增/编辑走 TransportBusinessSupport.validateNonNegative,文案是「不能小于0」。
if (excel.getQuantity() != null && excel.getQuantity().compareTo(BigDecimal.ZERO) < 0) {
errors.add("数量必须为正数");
errors.add("数量不能小于0");
}
if (excel.getMileage() != null && excel.getMileage().compareTo(BigDecimal.ZERO) < 0) {
errors.add("里程必须为正数");
errors.add("里程不能小于0");
}
// 6. 校验计量单位(如果有值)
@@ -290,7 +290,7 @@ public class TransportReconciliationServiceImpl
@Override
@Transactional(rollbackFor = Exception.class)
public List<VehicleReconciliationFailureExcel> importVehicles(Long id, List<VehicleReconciliationExcel> rows) {
if (Func.isEmpty(rows)) throw new ServiceException("导入数据不能为空");
if (Func.isEmpty(rows)) throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
TransportReconciliation bill = editable(id);
if (!VEHICLE.equals(bill.getReconciliationMode())) throw new ServiceException("当前对账模式不是整车总额对账");
resetExternal(id);
@@ -300,22 +300,33 @@ public class TransportReconciliationServiceImpl
VehicleReconciliationExcel row = rows.get(index);
List<String> validationErrors = validateImportVehicle(row);
if (Func.isNotEmpty(validationErrors)) {
// 原因同时写回原行:异常携带的是原表 rows,
// 导出工具从行对象的 errorMessage 字段取原因,只写副本会丢原因。
row.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(validationErrors));
VehicleReconciliationFailureExcel failure = new VehicleReconciliationFailureExcel();
BeanUtil.copyProperties(row, failure);
failure.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(validationErrors));
failure.setErrorMessage(row.getErrorMessage());
failures.add(failure);
continue;
}
try {
validExternals.add(buildVehicleExternal(id, index, row));
} catch (Exception exception) {
String message = org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(List.of(
exception instanceof ServiceException ? exception.getMessage() : "导入失败"));
row.setErrorMessage(message);
VehicleReconciliationFailureExcel failure = new VehicleReconciliationFailureExcel();
BeanUtil.copyProperties(row, failure);
failure.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(List.of(
exception instanceof ServiceException ? exception.getMessage() : "导入失败")));
failure.setErrorMessage(message);
failures.add(failure);
}
}
if (Func.isNotEmpty(failures)) {
// 任一行失败即整批回滚:抛异常触发事务回滚,使前置的 resetExternal
// 清空与本批正确行的插入一并退掉;否则会出现「旧外部账单被清空、
// 正确行却已入库」的数据损坏。
throw new org.springblade.common.excel.ImportFailureException(rows);
}
for (TransportReconciliationExternal external : validExternals) {
if (externalMapper.insert(external) <= 0) throw new ServiceException("外部账单保存失败");
}
@@ -408,7 +419,7 @@ public class TransportReconciliationServiceImpl
@Override
@Transactional(rollbackFor = Exception.class)
public List<CargoReconciliationFailureExcel> importCargoes(Long id, List<CargoReconciliationExcel> rows) {
if (Func.isEmpty(rows)) throw new ServiceException("导入数据不能为空");
if (Func.isEmpty(rows)) throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
TransportReconciliation bill = editable(id);
if (!CARGO.equals(bill.getReconciliationMode())) throw new ServiceException("当前对账模式不是货物明细对账");
resetExternal(id);
@@ -418,22 +429,33 @@ public class TransportReconciliationServiceImpl
CargoReconciliationExcel row = rows.get(index);
List<String> validationErrors = validateImportCargo(row);
if (Func.isNotEmpty(validationErrors)) {
// 原因同时写回原行:异常携带的是原表 rows,
// 导出工具从行对象的 errorMessage 字段取原因,只写副本会丢原因。
row.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(validationErrors));
CargoReconciliationFailureExcel failure = new CargoReconciliationFailureExcel();
BeanUtil.copyProperties(row, failure);
failure.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(validationErrors));
failure.setErrorMessage(row.getErrorMessage());
failures.add(failure);
continue;
}
try {
validExternals.add(buildCargoExternal(id, index, row));
} catch (Exception exception) {
String message = org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(List.of(
exception instanceof ServiceException ? exception.getMessage() : "导入失败"));
row.setErrorMessage(message);
CargoReconciliationFailureExcel failure = new CargoReconciliationFailureExcel();
BeanUtil.copyProperties(row, failure);
failure.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(List.of(
exception instanceof ServiceException ? exception.getMessage() : "导入失败")));
failure.setErrorMessage(message);
failures.add(failure);
}
}
if (Func.isNotEmpty(failures)) {
// 任一行失败即整批回滚:抛异常触发事务回滚,使前置的 resetExternal
// 清空与本批正确行的插入一并退掉;否则会出现「旧外部账单被清空、
// 正确行却已入库」的数据损坏。
throw new org.springblade.common.excel.ImportFailureException(rows);
}
for (TransportReconciliationExternal external : validExternals) {
if (externalMapper.insert(external) <= 0) throw new ServiceException("外部账单保存失败");
}
@@ -465,7 +487,7 @@ public class TransportReconciliationServiceImpl
private List<String> validateImportCargo(CargoReconciliationExcel row) {
List<String> validationErrors = new ArrayList<>();
if (row == null) {
validationErrors.add("导入数据不能为空");
validationErrors.add("未读取到数据行,请确认表头下方已填写内容");
return validationErrors;
}
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors,
@@ -31,6 +31,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.excel.TransportShipExcel;
import org.springblade.transport.mapper.TransportShipMapper;
@@ -66,7 +67,9 @@ public class TransportShipServiceImpl extends BaseServiceImpl<TransportShipMappe
@Override
public IPage<TransportShipVO> selectTransportShipPage(IPage<TransportShipVO> page, TransportShipVO ship) {
prepareQuery(ship);
return page.setRecords(baseMapper.selectTransportShipPage(page, ship));
List<TransportShipVO> records = baseMapper.selectTransportShipPage(page, ship);
records.forEach(record -> record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser())));
return page.setRecords(records);
}
@Override
@@ -111,6 +114,8 @@ public class TransportShipServiceImpl extends BaseServiceImpl<TransportShipMappe
public List<TransportShipExcel> exportTransportShip(Wrapper<TransportShip> queryWrapper) {
return list(queryWrapper).stream().map(ship -> {
TransportShipExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(ship, TransportShipExcel.class));
excel.setCreateUserName(UserCache.getUserRealName(ship.getCreateUser()));
excel.setCreateTime(ship.getCreateTime());
excel.setConstructionDateRange(formatDateRange(
ship.getKeelLayingDate(), ship.getBuildCompletionDate()
));
@@ -32,6 +32,7 @@ import lombok.RequiredArgsConstructor;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.excel.TransportVehicleExcel;
import org.springblade.transport.mapper.DriverMapper;
@@ -81,6 +82,7 @@ public class TransportVehicleServiceImpl extends BaseServiceImpl<TransportVehicl
public IPage<TransportVehicleVO> selectTransportVehiclePage(IPage<TransportVehicleVO> page, TransportVehicleVO vehicle) {
prepareQuery(vehicle);
List<TransportVehicleVO> records = baseMapper.selectTransportVehiclePage(page, vehicle);
records.forEach(record -> record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser())));
fillBoundDrivers(records);
return page.setRecords(records);
}
@@ -188,6 +190,8 @@ public class TransportVehicleServiceImpl extends BaseServiceImpl<TransportVehicl
public List<TransportVehicleExcel> exportTransportVehicle(Wrapper<TransportVehicle> queryWrapper) {
return list(queryWrapper).stream().map(vehicle -> {
TransportVehicleExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(vehicle, TransportVehicleExcel.class));
excel.setCreateUserName(UserCache.getUserRealName(vehicle.getCreateUser()));
excel.setCreateTime(vehicle.getCreateTime());
excel.setStatusName(vehicle.getStatus() != null && vehicle.getStatus() == 2 ? "停用" : "启用");
return excel;
}).toList();
@@ -69,11 +69,17 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
private static final String UNPROCESSED = "未处理";
private static final String CAR = "车辆";
private static final String SHIP = "船舶";
/** 非必填数值未填写时的历史占位值,统一转为 null 落库/展示 */
private static final BigDecimal EMPTY_AMOUNT_SENTINEL = BigDecimal.valueOf(-1);
private static final Integer EMPTY_INTEGER_SENTINEL = -1;
@Override
public IPage<ViolationRecordVO> selectViolationRecordPage(IPage<ViolationRecordVO> page, ViolationRecordVO violationRecord) {
List<ViolationRecordVO> records = baseMapper.selectViolationRecordPage(page, violationRecord);
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
records.forEach(record -> {
record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
});
return page.setRecords(records);
}
@@ -91,7 +97,7 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
@Transactional(rollbackFor = Exception.class)
public List<ViolationRecordImportExcel> importViolationRecord(List<ViolationRecordImportExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<ViolationRecordImportExcel> errorList = new ArrayList<>();
List<ViolationRecord> violationRecordList = new ArrayList<>();
@@ -116,8 +122,11 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
}
}
if (Func.isNotEmpty(errorList)) {
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
// 注意不能只往 errorList 里塞数据就 return——异常被吞掉后方法会正常返回,
// Spring 会照常提交事务,@Transactional 起不到回滚作用。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (ViolationRecord violationRecord : violationRecordList) {
if (!save(violationRecord)) {
@@ -165,8 +174,9 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
public List<ViolationRecordExcel> exportViolationRecord(Wrapper<ViolationRecord> queryWrapper) {
return list(queryWrapper).stream().map(violationRecord -> {
ViolationRecordExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(violationRecord, ViolationRecordExcel.class));
excel.setFineAmount(nonNegative(violationRecord.getFineAmount()));
excel.setDeductPoints(validDeductPoints(violationRecord.getDeductPoints()));
excel.setFineAmount(normalizeEmptyAmount(violationRecord.getFineAmount()));
excel.setDeductPoints(validDeductPoints(normalizeEmptyInteger(violationRecord.getDeductPoints())));
excel.setCreateUserName(UserCache.getUserRealName(violationRecord.getCreateUser()));
excel.setUpdateUserName(UserCache.getUserRealName(violationRecord.getUpdateUser()));
return excel;
}).toList();
@@ -184,6 +194,9 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
violationRecord.setProcessDescription(trimToEmpty(violationRecord.getProcessDescription()));
violationRecord.setProcessResult(trimToNull(violationRecord.getProcessResult()));
violationRecord.setAttachments(trimToNull(violationRecord.getAttachments()));
// 非必填数值未填写禁止落库 -1 占位值
violationRecord.setFineAmount(normalizeEmptyAmount(violationRecord.getFineAmount()));
violationRecord.setDeductPoints(normalizeEmptyInteger(violationRecord.getDeductPoints()));
if (UNPROCESSED.equals(violationRecord.getProcessStatus())) {
violationRecord.setProcessResult(null);
}
@@ -287,11 +300,12 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
}
}
private BigDecimal nonNegative(BigDecimal value) {
if (Func.isEmpty(value) || value.compareTo(BigDecimal.ZERO) >= 0) {
return value;
}
return BigDecimal.ZERO;
private BigDecimal normalizeEmptyAmount(BigDecimal value) {
return Func.isNotEmpty(value) && value.compareTo(EMPTY_AMOUNT_SENTINEL) == 0 ? null : value;
}
private Integer normalizeEmptyInteger(Integer value) {
return Func.isNotEmpty(value) && value.equals(EMPTY_INTEGER_SENTINEL) ? null : value;
}
private Integer validDeductPoints(Integer value) {
@@ -320,7 +334,7 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
private String trimToNull(String value) {
String trimValue = trimToEmpty(value);
return trimValue.isEmpty() ? null : trimValue;
return trimValue.isEmpty() || "-1".equals(trimValue) ? null : trimValue;
}
}
@@ -7,8 +7,10 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.mapper.LoadingManageMapper;
import org.springblade.transport.mapper.VoucherManageMapper;
import org.springblade.transport.mapper.VoucherImageMapper;
import org.springblade.transport.mapper.VoucherFileMapper;
@@ -18,6 +20,7 @@ import org.springblade.transport.pojo.dto.VoucherManageChangeBatchRequest;
import org.springblade.transport.pojo.dto.VoucherManageSubmitRequest;
import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest;
import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest;
import org.springblade.transport.pojo.entity.LoadingManage;
import org.springblade.transport.pojo.entity.VoucherManage;
import org.springblade.transport.pojo.entity.VoucherWaybillBatch;
import org.springblade.transport.pojo.entity.VoucherImage;
@@ -66,6 +69,7 @@ import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.springframework.web.multipart.MultipartFile;
@@ -79,6 +83,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
private final IWaybillService waybillService;
private final VoucherImageMapper voucherImageMapper;
private final VoucherFileMapper voucherFileMapper;
private final LoadingManageMapper loadingManageMapper;
private final ApplicationEventPublisher eventPublisher;
private final ObjectProvider<MinioClient> minioClientProvider;
private final String minioBucketName;
@@ -87,6 +92,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService,
IWaybillService waybillService,
VoucherImageMapper voucherImageMapper, VoucherFileMapper voucherFileMapper,
LoadingManageMapper loadingManageMapper,
ApplicationEventPublisher eventPublisher, ObjectProvider<MinioClient> minioClientProvider,
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") String minioBucketName,
@Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory) {
@@ -95,6 +101,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
this.waybillService = waybillService;
this.voucherImageMapper = voucherImageMapper;
this.voucherFileMapper = voucherFileMapper;
this.loadingManageMapper = loadingManageMapper;
this.eventPublisher = eventPublisher;
this.minioClientProvider = minioClientProvider;
this.minioBucketName = minioBucketName;
@@ -104,7 +111,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
@Override
public IPage<VoucherManageVO> selectPage(IPage<VoucherManage> page, VoucherManageVO query) {
IPage<VoucherManageVO> result = VoucherManageWrapper.build().pageVO(page(page, buildQuery(query)));
// 凭证数量按文件夹(车牌目录)统计,兼容历史记录中仍保存图片数量的旧数据。
// 凭证数量、已关联数量、未关联数量都按压缩包中的车牌文件夹统计,不按图片张数。
if (result.getRecords() != null) {
result.getRecords().forEach(item -> {
VoucherFolderCounts folderCounts = countVoucherFolderRelations(item.getTenantId(), item.getId());
@@ -182,12 +189,22 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
if (Func.isEmpty(fileName)) throw new ServiceException("凭证文件名称不能为空");
boolean isZipFile = fileName.toLowerCase().endsWith(".zip");
if (!isZipFile && !isImageFile(fileName)) throw new ServiceException("仅支持 jpg、jpeg、png、bmp、webp 格式的凭证图片或 zip 压缩包");
List<VoucherFile> oldFiles = voucherFileMapper.selectList(Wrappers.<VoucherFile>lambdaQuery()
.eq(VoucherFile::getTenantId, voucher.getTenantId()).eq(VoucherFile::getVoucherId, voucherId)
.eq(VoucherFile::getPlateNo, normalizedPlateNo).eq(VoucherFile::getIsDeleted, 0));
List<VoucherImage> oldImages = voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
.eq(VoucherImage::getTenantId, voucher.getTenantId()).eq(VoucherImage::getVoucherId, voucherId)
.eq(VoucherImage::getPlateNo, normalizedPlateNo).eq(VoucherImage::getIsDeleted, 0));
if (isZipFile) {
try {
// 压缩包里只要有一个文件夹名对上已有车牌或运单车牌,就替换对应凭证,不必整包都匹配。
if (replaceZipFoldersByName(voucher, normalizedPlateNo, uploadFile)) {
refreshVoucherCounts(voucher);
return;
}
} catch (ServiceException exception) {
throw exception;
} catch (Exception exception) {
log.error("按文件夹替换车牌凭证失败 voucherId={}, plateNo={}", voucherId, normalizedPlateNo, exception);
throw new ServiceException("凭证文件上传失败");
}
}
List<VoucherFile> oldFiles = listNormalizedPlateFiles(voucher, normalizedPlateNo);
List<VoucherImage> oldImages = listNormalizedPlateImages(voucher, normalizedPlateNo);
if (oldFiles.isEmpty() && oldImages.isEmpty()) throw new ServiceException("车牌凭证不存在");
if (oldFiles.stream().anyMatch(item -> Objects.equals(item.getMatched(), 1))
|| oldImages.stream().anyMatch(item -> Objects.equals(item.getMatched(), 1))) {
@@ -228,11 +245,10 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
// 分片上传完成后对象已存在于 MinIO。先下载到临时文件,避免直接使用远程流导致
// ZIP 解压与 MinIO 分片上传之间的流状态冲突。
archivePath = downloadMinioObject(objectKey);
String effectivePlateNo = resolveReplacementPlateNo(voucher, plateNo, originalFileName, archivePath);
MultipartFile uploadedFile = localArchiveMultipartFile(archivePath, originalFileName, contentType);
replaceFolder(voucherId, effectivePlateNo, uploadedFile, false);
log.info("[分片上传] 压缩包下载、解压并匹配完成 voucherId={}, requestedPlateNo={}, matchedPlateNo={}, objectKey={}",
voucherId, plateNo, effectivePlateNo, objectKey);
replaceFolder(voucherId, plateNo, uploadedFile, false);
log.info("[分片上传] 压缩包下载、解压并匹配完成 voucherId={}, requestedPlateNo={}, objectKey={}",
voucherId, plateNo, objectKey);
} catch (ServiceException exception) {
throw exception;
} catch (Exception exception) {
@@ -381,9 +397,108 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
folders.get(index).setVoucherNo(voucher.getVoucherBatchNo() + "-" + String.format("%04d", index + 1));
if (!includeFiles) folders.get(index).setFiles(null);
}
fillFolderLoadingNo(folders);
return folders;
}
@Override
public List<VoucherFolderVO> listWaybillFolders(Long waybillId) {
if (waybillId == null) return List.of();
Waybill waybill = waybillService.getById(waybillId);
if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) return List.of();
Set<Long> voucherIds = new HashSet<>();
voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
.eq(VoucherImage::getWaybillId, waybillId)
.eq(VoucherImage::getIsDeleted, 0))
.forEach(image -> {
if (image.getVoucherId() != null) voucherIds.add(image.getVoucherId());
});
voucherFileMapper.selectList(Wrappers.<VoucherFile>lambdaQuery()
.eq(VoucherFile::getWaybillId, waybillId)
.eq(VoucherFile::getIsDeleted, 0))
.forEach(file -> {
if (file.getVoucherId() != null) voucherIds.add(file.getVoucherId());
});
if (voucherIds.isEmpty() && Func.isNotEmpty(waybill.getBatchNo())) {
list(Wrappers.<VoucherManage>lambdaQuery()
.eq(VoucherManage::getTenantId, waybill.getTenantId())
.eq(VoucherManage::getIsDeleted, 0))
.stream()
.filter(voucher -> containsWaybillBatchNo(voucher.getWaybillBatchNo(), waybill.getBatchNo()))
.forEach(voucher -> voucherIds.add(voucher.getId()));
}
List<VoucherFolderVO> folders = new ArrayList<>();
for (Long voucherId : voucherIds) {
VoucherManage voucher = getById(voucherId);
if (!visibleWaybillVoucher(voucher)) continue;
folderRecords(voucher, false).stream()
.filter(folder -> folderBelongsToWaybill(folder, waybill))
.forEach(folders::add);
}
folders.sort(Comparator.comparing(VoucherFolderVO::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder())));
return folders;
}
private boolean folderBelongsToWaybill(VoucherFolderVO folder, Waybill waybill) {
if (Objects.equals(folder.getWaybillId(), waybill.getId())) return true;
if (folder.getWaybillId() != null) return false;
String plateNo = normalizePlateNo(folder.getPlateNo());
return Func.isNotEmpty(plateNo) && waybillPlateNumbers(waybill).contains(plateNo);
}
/**
* 承运商上传的凭证只在审核通过后展示到运单执行详情;内部上传保持原展示规则。
*/
private boolean visibleWaybillVoucher(VoucherManage voucher) {
return voucher != null && !Objects.equals(voucher.getIsDeleted(), 1)
&& (!"承运商".equals(voucher.getUploadSource()) || "审核通过".equals(voucher.getAuditStatus()));
}
private boolean containsWaybillBatchNo(String batchNos, String batchNo) {
return Func.isNotEmpty(batchNos) && Func.isNotEmpty(batchNo)
&& Arrays.stream(batchNos.split(",")).map(String::trim).anyMatch(batchNo::equals);
}
private void fillFolderLoadingNo(List<VoucherFolderVO> folders) {
List<Long> waybillIds = folders.stream().map(VoucherFolderVO::getWaybillId).filter(Objects::nonNull).distinct().toList();
if (waybillIds.isEmpty()) return;
Map<Long, Waybill> waybillMap = waybillService.listByIds(waybillIds).stream()
.collect(Collectors.toMap(Waybill::getId, item -> item, (left, right) -> left));
Map<Long, String> loadingNoMap = new HashMap<>();
for (Long waybillId : waybillIds) {
Waybill waybill = waybillMap.get(waybillId);
String loadingNo = waybill == null ? null : waybill.getLoadingNo();
if (Func.isEmpty(loadingNo)) {
loadingNo = findLoadingNo(waybill == null ? null : waybill.getTenantId(), waybillId);
}
if (Func.isNotEmpty(loadingNo)) loadingNoMap.put(waybillId, loadingNo);
}
folders.forEach(folder -> {
if (folder.getWaybillId() != null) folder.setLoadingNo(loadingNoMap.get(folder.getWaybillId()));
});
}
private String findLoadingNo(String tenantId, Long waybillId) {
if (waybillId == null) return null;
String waybillIdText = String.valueOf(waybillId);
LambdaQueryWrapper<LoadingManage> query = Wrappers.<LoadingManage>lambdaQuery()
.like(LoadingManage::getWaybillIdsJson, waybillIdText)
.orderByDesc(LoadingManage::getCreateTime)
.last("limit 20");
if (Func.isNotEmpty(tenantId)) query.eq(LoadingManage::getTenantId, tenantId);
return loadingManageMapper.selectList(query).stream()
.filter(item -> containsJsonId(item.getWaybillIdsJson(), waybillIdText))
.map(LoadingManage::getLoadingNo)
.filter(Func::isNotEmpty)
.findFirst()
.orElse(null);
}
private boolean containsJsonId(String json, String idText) {
if (Func.isEmpty(json) || Func.isEmpty(idText)) return false;
return Arrays.stream(json.split("\\D+")).anyMatch(idText::equals);
}
private void addFolderFile(Map<String, VoucherFolderVO> grouped, VoucherManage voucher, VoucherFileVO file, java.util.Date createTime, java.util.Date updateTime) {
String plate = normalizePlateNo(file.getPlateNo());
if (Func.isEmpty(plate)) plate = "未识别车牌";
@@ -398,6 +513,174 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
if (folder.getFileName() == null) folder.setFileName(file.getFileName());
}
/**
* 按压缩包内文件夹名替换凭证。文件夹名与当前车牌、已有凭证车牌或关联运单车牌任一一致即替换。
*
* @return 是否至少替换了一个匹配文件夹
*/
private boolean replaceZipFoldersByName(VoucherManage voucher, String requestedPlateNo, MultipartFile zipFile) throws Exception {
Path archivePath = Files.createTempFile("voucher-replace-", ".zip");
try (InputStream source = zipFile.getInputStream()) {
Files.copy(source, archivePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
try {
Map<String, Waybill> waybillByPlate = new HashMap<>();
for (Waybill waybill : listRelatedWaybills(voucher)) {
for (String waybillPlateNo : waybillPlateNumbers(waybill)) {
waybillByPlate.putIfAbsent(waybillPlateNo, waybill);
}
}
Set<String> existingPlates = new HashSet<>();
voucherFileMapper.selectList(Wrappers.<VoucherFile>lambdaQuery()
.eq(VoucherFile::getTenantId, voucher.getTenantId()).eq(VoucherFile::getVoucherId, voucher.getId())
.eq(VoucherFile::getIsDeleted, 0)).forEach(file -> addPlateNumber(existingPlates, file.getPlateNo()));
voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
.eq(VoucherImage::getTenantId, voucher.getTenantId()).eq(VoucherImage::getVoucherId, voucher.getId())
.eq(VoucherImage::getIsDeleted, 0)).forEach(image -> addPlateNumber(existingPlates, image.getPlateNo()));
Charset archiveCharset = detectArchiveCharset(archivePath);
Set<String> matchedPlates = new HashSet<>();
try (InputStream source = Files.newInputStream(archivePath);
ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(source), archiveCharset)) {
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
if (entry.isDirectory()) continue;
String plateNo = plateFromZipEntry(entry.getName(), requestedPlateNo, existingPlates, waybillByPlate);
if (Func.isNotEmpty(plateNo)) matchedPlates.add(plateNo);
}
}
if (matchedPlates.isEmpty()) return false;
log.info("[单个上传] 压缩包文件夹命中,准备替换 voucherId={}, requestedPlateNo={}, matchedPlates={}",
voucher.getId(), requestedPlateNo, matchedPlates);
matchedPlates.forEach(plateNo -> deletePlateVoucherFiles(voucher, plateNo));
int fileCount = 0;
int imageCount = 0;
try (InputStream source = Files.newInputStream(archivePath);
ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(source), archiveCharset)) {
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
if (entry.isDirectory()) continue;
String entryName = entry.getName().replace('\\', '/');
String plateNo = plateFromZipEntry(entryName, requestedPlateNo, existingPlates, waybillByPlate);
if (!matchedPlates.contains(plateNo)) continue;
List<String> pathParts = Arrays.stream(entryName.split("/")).filter(Func::isNotEmpty).toList();
if (pathParts.isEmpty()) continue;
String rawFileName = pathParts.get(pathParts.size() - 1);
boolean imageFile = isImageFile(rawFileName);
String storedFileName = safeFileName(rawFileName);
if (Func.isEmpty(storedFileName)) continue;
Waybill matchedWaybill = waybillByPlate.get(plateNo);
if (saveZipVoucherFile(voucher, plateNo, matchedWaybill, entryName, storedFileName, imageFile, entry, zipInputStream)) {
fileCount++;
if (imageFile) imageCount++;
}
}
}
if (fileCount == 0) throw new ServiceException("压缩包内没有找到有效的文件");
log.info("[单个上传] 按文件夹替换完成 voucherId={}, matchedPlates={}, fileCount={}, imageCount={}",
voucher.getId(), matchedPlates, fileCount, imageCount);
return true;
} finally {
deleteTempArchive(archivePath);
}
}
/**
* 从压缩包路径的每一层目录识别车牌。车牌文件夹外面还套了其它目录时,也不能漏掉。
* 优先当前重新上传的车牌,其次关联运单车牌,再其次批次里已有车牌。
*/
private String plateFromZipEntry(String entryName, String requestedPlateNo, Set<String> existingPlates, Map<String, Waybill> waybillByPlate) {
if (Func.isEmpty(entryName)) return null;
String normalizedName = entryName.replace('\\', '/');
if (normalizedName.equals("__MACOSX") || normalizedName.startsWith("__MACOSX/")) return null;
List<String> pathParts = Arrays.stream(normalizedName.split("/")).filter(Func::isNotEmpty).toList();
if (pathParts.size() <= 1) return null;
List<String> directories = new ArrayList<>();
for (int index = 0; index < pathParts.size() - 1; index++) {
String plateNo = normalizePlateNo(pathParts.get(index));
if (Func.isEmpty(plateNo) || "凭证导入".equals(plateNo) || plateNo.startsWith("__MACOSX") || ".".equals(plateNo)) continue;
directories.add(plateNo);
}
if (directories.isEmpty()) return null;
if (Func.isNotEmpty(requestedPlateNo) && directories.contains(requestedPlateNo)) return requestedPlateNo;
for (String plateNo : directories) {
if (waybillByPlate.containsKey(plateNo)) return plateNo;
}
for (String plateNo : directories) {
if (existingPlates.contains(plateNo)) return plateNo;
}
return null;
}
private List<VoucherFile> listNormalizedPlateFiles(VoucherManage voucher, String plateNo) {
String normalizedPlateNo = normalizePlateNo(plateNo);
return voucherFileMapper.selectList(Wrappers.<VoucherFile>lambdaQuery()
.eq(VoucherFile::getTenantId, voucher.getTenantId())
.eq(VoucherFile::getVoucherId, voucher.getId())
.eq(VoucherFile::getIsDeleted, 0))
.stream()
.filter(file -> normalizedPlateNo.equals(normalizePlateNo(file.getPlateNo())))
.toList();
}
private List<VoucherImage> listNormalizedPlateImages(VoucherManage voucher, String plateNo) {
String normalizedPlateNo = normalizePlateNo(plateNo);
return voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
.eq(VoucherImage::getTenantId, voucher.getTenantId())
.eq(VoucherImage::getVoucherId, voucher.getId())
.eq(VoucherImage::getIsDeleted, 0))
.stream()
.filter(image -> normalizedPlateNo.equals(normalizePlateNo(image.getPlateNo())))
.toList();
}
private void deletePlateVoucherFiles(VoucherManage voucher, String plateNo) {
List<VoucherFile> oldFiles = listNormalizedPlateFiles(voucher, plateNo);
List<VoucherImage> oldImages = listNormalizedPlateImages(voucher, plateNo);
oldFiles.forEach(item -> voucherFileMapper.deleteById(item.getId()));
oldImages.forEach(item -> voucherImageMapper.deleteById(item.getId()));
java.util.stream.Stream.concat(oldFiles.stream().map(VoucherFile::getObjectKey), oldImages.stream().map(VoucherImage::getObjectKey))
.filter(Func::isNotEmpty).distinct().forEach(this::deleteObjectQuietly);
}
private boolean saveZipVoucherFile(VoucherManage voucher, String plateNo, Waybill matchedWaybill, String entryName,
String fileName, boolean imageFile, ZipEntry entry, ZipInputStream zipInputStream) throws Exception {
String waybillNo = matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo());
String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, entryName);
String contentType = contentType(fileName);
minioClient().putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
.stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
.contentType(contentType).build());
VoucherFile file = new VoucherFile();
file.setVoucherId(voucher.getId());
file.setVoucherBatchNo(voucher.getVoucherBatchNo());
file.setWaybillId(matchedWaybill == null ? null : matchedWaybill.getId());
file.setWaybillNo(matchedWaybill == null ? null : matchedWaybill.getWaybillNo());
file.setPlateNo(plateNo);
file.setFolderName(plateNo);
file.setEntryName(entryName);
file.setFileName(fileName);
file.setObjectKey(objectKey);
file.setFileSize(entry.getSize() < 0 ? null : entry.getSize());
file.setContentType(contentType);
file.setFileType(imageFile ? "image" : "file");
file.setMatched(matchedWaybill == null ? 0 : 1);
file.setTenantId(voucher.getTenantId());
voucherFileMapper.insert(file);
if (!imageFile) return true;
VoucherImage image = new VoucherImage();
image.setVoucherId(voucher.getId());
image.setVoucherBatchNo(voucher.getVoucherBatchNo());
image.setWaybillId(matchedWaybill == null ? null : matchedWaybill.getId());
image.setWaybillNo(matchedWaybill == null ? null : matchedWaybill.getWaybillNo());
image.setPlateNo(plateNo);
image.setImageName(fileName);
image.setObjectKey(objectKey);
image.setMatched(matchedWaybill == null ? 0 : 1);
image.setTenantId(voucher.getTenantId());
voucherImageMapper.insert(image);
return true;
}
private void replaceFolderWithZip(VoucherManage voucher, String plateNo, Waybill matchedWaybill, MultipartFile zipFile) throws Exception {
log.info("[单个上传] 开始解压替换 voucherId={}, plateNo={}, fileName={}", voucher.getId(), plateNo, zipFile.getOriginalFilename());
Charset zipCharset = StandardCharsets.UTF_8;
@@ -583,23 +866,82 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
return countVoucherFolderRelations(voucher.getTenantId(), voucher.getId()).total();
}
/**
* 按压缩包中的车牌文件夹统计。凭证数量是车牌文件夹个数;已关联、未关联是这些文件夹是否匹配到运单。
* 同一车牌下的多张图片只计 1 个文件夹,图片张数不计入。
*/
private VoucherFolderCounts countVoucherFolderRelations(String tenantId, Long voucherId) {
VoucherManage voucher = getById(voucherId);
Set<String> waybillPlates = new HashSet<>();
if (voucher != null) {
for (Waybill waybill : listRelatedWaybills(voucher)) {
waybillPlates.addAll(waybillPlateNumbers(waybill));
}
}
List<VoucherFile> files = voucherFileMapper.selectList(Wrappers.<VoucherFile>lambdaQuery()
.eq(VoucherFile::getTenantId, tenantId).eq(VoucherFile::getVoucherId, voucherId).eq(VoucherFile::getIsDeleted, 0));
Map<String, Boolean> relatedByFolder = new LinkedHashMap<>();
if (!files.isEmpty()) {
Set<String> folders = files.stream().map(item -> folderKey(item.getPlateNo())).collect(Collectors.toSet());
Set<String> relatedFolders = files.stream().filter(item -> Objects.equals(item.getMatched(), 1))
.map(item -> folderKey(item.getPlateNo())).collect(Collectors.toSet());
return new VoucherFolderCounts(folders.size(), relatedFolders.size(), folders.size() - relatedFolders.size());
for (VoucherFile file : files) {
accumulatePlateFolder(relatedByFolder, waybillPlates, file.getPlateNo(), file.getFolderName(), file.getEntryName(), file.getMatched());
}
} else {
List<VoucherImage> images = voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
.eq(VoucherImage::getTenantId, tenantId).eq(VoucherImage::getVoucherId, voucherId).eq(VoucherImage::getIsDeleted, 0));
for (VoucherImage image : images) {
accumulatePlateFolder(relatedByFolder, waybillPlates, image.getPlateNo(), image.getPlateNo(), null, image.getMatched());
}
}
List<VoucherImage> images = voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
.eq(VoucherImage::getTenantId, tenantId).eq(VoucherImage::getVoucherId, voucherId).eq(VoucherImage::getIsDeleted, 0));
Set<String> folders = images.stream().map(item -> folderKey(item.getPlateNo())).collect(Collectors.toSet());
Set<String> relatedFolders = images.stream().filter(item -> Objects.equals(item.getMatched(), 1))
.map(item -> folderKey(item.getPlateNo())).collect(Collectors.toSet());
return new VoucherFolderCounts(folders.size(), relatedFolders.size(), folders.size() - relatedFolders.size());
int related = (int) relatedByFolder.values().stream().filter(Boolean::booleanValue).count();
int total = relatedByFolder.size();
return new VoucherFolderCounts(total, related, total - related);
}
private void accumulatePlateFolder(Map<String, Boolean> relatedByFolder, Set<String> waybillPlates,
String plateNo, String folderName, String entryName, Integer matched) {
String plateFolder = countedPlateFolder(plateNo, folderName, entryName, waybillPlates);
if (Func.isEmpty(plateFolder)) return;
boolean related = Objects.equals(matched, 1) || waybillPlates.contains(plateFolder);
relatedByFolder.merge(plateFolder, related, (left, right) -> left || right);
}
/**
* 从车牌字段和压缩包路径中识别车牌文件夹。外层业务目录、内层图片目录都不计入凭证数量。
*/
private String countedPlateFolder(String plateNo, String folderName, String entryName, Set<String> waybillPlates) {
List<String> candidates = new ArrayList<>();
addPlateCandidate(candidates, plateNo);
addPlateCandidate(candidates, folderName);
if (Func.isNotEmpty(entryName)) {
List<String> pathParts = Arrays.stream(entryName.replace('\\', '/').split("/")).filter(Func::isNotEmpty).toList();
int directoryCount = Math.max(pathParts.size() - 1, 0);
for (int index = 0; index < directoryCount; index++) {
addPlateCandidate(candidates, pathParts.get(index));
}
}
for (String candidate : candidates) {
if (waybillPlates.contains(candidate)) return candidate;
}
for (String candidate : candidates) {
if (isPlateFolderName(candidate)) return candidate;
}
return null;
}
private void addPlateCandidate(List<String> candidates, String plateNo) {
String normalizedPlateNo = normalizePlateNo(plateNo);
if (Func.isNotEmpty(normalizedPlateNo) && !"未识别车牌".equals(normalizedPlateNo) && !candidates.contains(normalizedPlateNo)) {
candidates.add(normalizedPlateNo);
}
}
private boolean isPlateFolderName(String plateNo) {
return Func.isNotEmpty(plateNo) && PLATE_FOLDER_PATTERN.matcher(plateNo).matches();
}
private static final Pattern PLATE_FOLDER_PATTERN = Pattern.compile(
"^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-Z0-9]{4,6}(?:挂|学|警|港|澳)?$");
private record VoucherFolderCounts(int total, int related, int unrelated) {}
private String folderKey(String plateNo) {
@@ -927,19 +1269,19 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
@Override
public IPage<Map<String, Object>> selectableWaybillBatches(IPage<?> page, String batchNo, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd) {
return voucherWaybillBatchMapper.selectVoucherWaybillBatchPage(page, AuthUtil.getTenantId(), batchNo, createUser, waybillCount, createTimeStart, createTimeEnd,
!AuthUtil.isAdministrator(), currentCarrierName());
!TransportBusinessSupport.isAdministrator(), currentCarrierName());
}
private List<Map<String, Object>> selectableWaybillBatchesByIds(List<Long> ids) {
if (Func.isEmpty(ids)) return List.of();
return voucherWaybillBatchMapper.selectWaybillBatchesByIds(AuthUtil.getTenantId(), ids, !AuthUtil.isAdministrator(), currentCarrierName());
return voucherWaybillBatchMapper.selectWaybillBatchesByIds(AuthUtil.getTenantId(), ids, !TransportBusinessSupport.isAdministrator(), currentCarrierName());
}
/**
* 普通用户只能关联承运商名称与其当前组织名称一致的运单批次;超级管理员不受此限制。
*/
private String currentCarrierName() {
if (AuthUtil.isAdministrator()) return null;
if (TransportBusinessSupport.isAdministrator()) return null;
Long currentDeptId = Func.firstLong(AuthUtil.getDeptId());
return getOrganizationName(currentDeptId);
}
@@ -1020,13 +1362,19 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
if (pathParts.size() <= 1) {
return null;
}
for (String pathPart : pathParts) {
String normalizedPart = normalizePlateNo(pathPart);
for (int index = 0; index < pathParts.size() - 1; index++) {
String normalizedPart = normalizePlateNo(pathParts.get(index));
if (Func.isNotEmpty(normalizedPart) && waybillByPlate.containsKey(normalizedPart)) {
return safeArchiveSegment(pathPart);
return safeArchiveSegment(pathParts.get(index));
}
}
// 未匹配车牌时仍保留最接近文件名的目录,便于前端展示和后续人工替换。
for (int index = 0; index < pathParts.size() - 1; index++) {
String normalizedPart = normalizePlateNo(pathParts.get(index));
if (isPlateFolderName(normalizedPart)) {
return safeArchiveSegment(pathParts.get(index));
}
}
// 未识别到车牌目录时仍保留最接近文件名的目录,便于前端展示和后续人工替换。
return safeArchiveSegment(pathParts.get(pathParts.size() - 2));
}
@@ -18,7 +18,6 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
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.DictBizCache;
@@ -36,6 +35,8 @@ import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.entity.WaybillImportBatch;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
import org.springblade.transport.pojo.vo.WaybillImportRowErrorVO;
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IReceivablePayableDetailService;
@@ -44,11 +45,10 @@ import org.springblade.transport.service.ITransportPlanService;
import org.springblade.transport.service.IWaybillImportBatchService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.WaybillProcessSupport;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@@ -69,8 +69,9 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImportBatchMapper, WaybillImportBatch> implements IWaybillImportBatchService {
/** 批量导入状态仅保留草稿与导入完成两种。 */
/** 保存草稿仍使用 draft;确认导入的状态与运单业务状态一致。 */
private static final String STATUS_DRAFT = "draft";
private static final String STATUS_RUNNING = "running";
private static final String STATUS_COMPLETED = "completed";
private static final String IMPORT_TYPE_WAYBILL = "waybill";
private static final String IMPORT_TYPE_SETTLEMENT = "settlement";
@@ -98,89 +99,97 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
}
@Override
public void validate(WaybillImportBatchRequest request, HttpServletResponse response) {
public WaybillImportValidateResultVO validate(WaybillImportBatchRequest request) {
if (Func.isEmpty(request.getRows())) throw new ServiceException("请上传至少一条运单明细");
// 确定导入状态
String importStatus = STATUS_DRAFT.equals(request.getStatus()) ? STATUS_DRAFT : STATUS_COMPLETED;
WaybillImportValidateResultVO result = new WaybillImportValidateResultVO();
String importStatus = resolveImportStatus(request.getStatus());
boolean draft = STATUS_DRAFT.equals(importStatus);
// 草稿状态不校验,直接返回成功
if (draft) {
WebUtil.renderJson(response, R.success("校验通过"));
return;
result.setSuccess(Boolean.TRUE);
return result;
}
// 执行校验
List<Map<String, Object>> rows = request.getRows();
Map<Integer, String> validationErrors = validateImportRows(rows, importStatus);
// 如果有校验错误,导出错误明细Excel
if (!validationErrors.isEmpty()) {
List<WaybillImportBatchExcel> failureList = new ArrayList<>();
for (int i = 0; i < rows.size(); i++) {
WaybillImportBatchExcel excel = mapToExcel(rows.get(i));
String errorMessage = validationErrors.get(i);
excel.setErrorMessage(Func.isNotEmpty(errorMessage) ? errorMessage : "");
failureList.add(excel);
}
ImportFailureExcelUtil.export(response, "运单导入失败明细" + DateUtil.time(), "导入失败明细", failureList, WaybillImportBatchExcel.class);
return;
List<WaybillImportRowErrorVO> errors = new ArrayList<>();
for (Map.Entry<Integer, String> entry : validationErrors.entrySet()) {
WaybillImportRowErrorVO error = new WaybillImportRowErrorVO();
error.setRowIndex(entry.getKey());
error.setErrorMessage(entry.getValue());
errors.add(error);
}
result.setErrors(errors);
result.setSuccess(errors.isEmpty());
return result;
}
// 校验通过,返回成功响应
WebUtil.renderJson(response, R.success("校验通过"));
@Override
public void exportFailures(WaybillImportBatchRequest request, HttpServletResponse response) {
if (Func.isEmpty(request.getRows())) throw new ServiceException("请至少提供一条失败明细");
List<WaybillImportBatchExcel> failureList = new ArrayList<>();
for (Map<String, Object> row : request.getRows()) {
WaybillImportBatchExcel excel = mapToExcel(row);
excel.setErrorMessage(stringValue(row, "errorMessage", "导入失败原因", "失败原因"));
failureList.add(excel);
}
ImportFailureExcelUtil.export(response, "运单导入失败明细" + DateUtil.time(), "导入失败明细", failureList, WaybillImportBatchExcel.class);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void confirm(WaybillImportBatchRequest request, HttpServletResponse response) {
public WaybillImportBatchVO confirm(WaybillImportBatchRequest request) {
if (Func.isEmpty(request.getRows())) throw new ServiceException("请上传至少一条运单明细");
// 执行校验
String importStatus = STATUS_DRAFT.equals(request.getStatus()) ? STATUS_DRAFT : STATUS_COMPLETED;
String importStatus = resolveImportStatus(request.getStatus());
boolean draft = STATUS_DRAFT.equals(importStatus);
if (!draft) {
List<Map<String, Object>> rows = request.getRows();
Map<Integer, String> validationErrors = validateImportRows(rows, importStatus);
// 如果有校验错误,导出错误明细Excel
Map<Integer, String> validationErrors = validateImportRows(request.getRows(), importStatus);
if (!validationErrors.isEmpty()) {
List<WaybillImportBatchExcel> failureList = new ArrayList<>();
for (int i = 0; i < rows.size(); i++) {
WaybillImportBatchExcel excel = mapToExcel(rows.get(i));
String errorMessage = validationErrors.get(i);
excel.setErrorMessage(Func.isNotEmpty(errorMessage) ? errorMessage : "");
failureList.add(excel);
}
ImportFailureExcelUtil.export(response, "运单导入失败明细" + DateUtil.time(), "导入失败明细", failureList, WaybillImportBatchExcel.class);
return;
String firstError = validationErrors.values().iterator().next();
Integer firstIndex = validationErrors.keySet().iterator().next();
throw new ServiceException("第" + (firstIndex + 1) + "行校验失败:" + firstError);
}
}
// 校验通过,执行导入
WaybillImportBatch batch = persist(request, importStatus);
// 返回成功响应
WebUtil.renderJson(response, R.success("操作成功"));
return toVO(batch);
}
/** 草稿与确认导入共用落库流程,差异仅在于运单是否走校验以及是否生成应收应付明细。 */
private WaybillImportBatch persist(WaybillImportBatchRequest request, String importStatus) {
boolean draft = STATUS_DRAFT.equals(importStatus);
WaybillImportBatch batch = buildBatch(request, importStatus);
if (Func.isNotEmpty(request.getId())) {
boolean append = Boolean.TRUE.equals(request.getAppend());
WaybillImportBatch batch;
int existingWaybillCount = 0;
if (append) {
if (Func.isEmpty(request.getId())) {
throw new ServiceException("分批追加导入必须指定批次ID");
}
WaybillImportBatch oldBatch = getById(request.getId());
if (oldBatch == null || Objects.equals(oldBatch.getIsDeleted(), 1)) throw new ServiceException("运单批次不存在");
if (!STATUS_DRAFT.equals(oldBatch.getImportStatus())) throw new ServiceException("仅草稿状态的批次允许编辑");
batch.setId(oldBatch.getId());
batch.setBatchNo(oldBatch.getBatchNo());
if (oldBatch == null || Objects.equals(oldBatch.getIsDeleted(), 1)) {
throw new ServiceException("运单批次不存在");
}
batch = oldBatch;
existingWaybillCount = Func.toInt(oldBatch.getWaybillCount(), 0);
} else {
batch = buildBatch(request, importStatus);
if (Func.isNotEmpty(request.getId())) {
WaybillImportBatch oldBatch = getById(request.getId());
if (oldBatch == null || Objects.equals(oldBatch.getIsDeleted(), 1)) throw new ServiceException("运单批次不存在");
if (!STATUS_DRAFT.equals(oldBatch.getImportStatus())) throw new ServiceException("仅草稿状态的批次允许编辑");
batch.setId(oldBatch.getId());
batch.setBatchNo(oldBatch.getBatchNo());
}
batch.setWaybillCount(0);
saveOrUpdate(batch);
// 重新落库前清理批次已生成的运单,避免草稿反复保存产生重复运单。
clearBatchWaybills(batch.getId());
}
batch.setWaybillCount(0);
saveOrUpdate(batch);
// 重新落库前清理批次已生成的运单,避免草稿反复保存产生重复运单。
clearBatchWaybills(batch.getId());
List<Map<String, Object>> rows = Func.isEmpty(request.getRows()) ? List.of() : request.getRows();
@@ -199,32 +208,62 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
mergeMultiCargoRows(rows, rowWaybills, waybillByRow, skipRows);
List<Waybill> waybills = new ArrayList<>();
Map<String, List<Waybill>> loadingWaybills = new TreeMap<>();
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 {
if (draft) waybillService.saveDraft(waybill); else waybillService.submit(waybill);
} catch (Exception exception) {
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);
// 进行中导入要保持运单为进行中,避免过程接单配置在查询时把状态改回待执行。
if (STATUS_RUNNING.equals(importStatus)) {
waybillService.update(Wrappers.<Waybill>lambdaUpdate()
.eq(Waybill::getId, waybill.getId())
.set(Waybill::getBusinessStatus, STATUS_RUNNING)
.set(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_ACCEPTED));
waybill.setBusinessStatus(STATUS_RUNNING);
waybill.setDriverAcceptStatus(WaybillProcessSupport.ACCEPT_ACCEPTED);
}
waybills.add(waybill);
}
loadingWaybills.forEach(loadingManageService::createFromImportedWaybills);
batch.setWaybillCount(waybills.size());
updateById(batch);
// 导入完成且导入类型为运单时,按合同费用生成模式(系统生成)同步生成应收应付明细。
// 同一配载标识号有多条运单时,生成一张配载单并回写运单关联。
if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType())) {
createLoadingsForSharedIdentifiers(batch);
}
batch.setWaybillCount(existingWaybillCount + waybills.size());
if (append && !draft) {
batch.setImportStatus(importStatus);
}
updateById(batch);
// 导入状态为已完成时,按合同费用生成模式(系统生成)同步生成应收应付明细。
if (STATUS_COMPLETED.equals(importStatus) && IMPORT_TYPE_WAYBILL.equals(batch.getImportType())) {
receivablePayableDetailService.generateForImportedWaybills(waybills);
}
return batch;
}
/**
* 按批次内已落库运单的配载标识号分组。标识号相同且不少于两条时创建或补齐配载单。
*/
private void createLoadingsForSharedIdentifiers(WaybillImportBatch batch) {
List<Waybill> batchWaybills = waybillService.list(Wrappers.<Waybill>lambdaQuery()
.eq(Waybill::getImportBatchId, batch.getId())
.eq(Waybill::getIsDeleted, 0)
.orderByAsc(Waybill::getCreateTime)
.orderByAsc(Waybill::getId));
Map<String, List<Waybill>> loadingWaybills = new java.util.LinkedHashMap<>();
for (Waybill waybill : batchWaybills) {
if (Func.isEmpty(waybill.getLoadingIdentifier())) continue;
loadingWaybills.computeIfAbsent(waybill.getLoadingIdentifier(), key -> new ArrayList<>()).add(waybill);
}
loadingWaybills.forEach((loadingIdentifier, groupedWaybills) -> {
if (groupedWaybills.size() > 1) {
loadingManageService.createFromImportedWaybills(loadingIdentifier, groupedWaybills);
}
});
}
private void clearBatchWaybills(Long batchId) {
if (Func.isEmpty(batchId)) return;
List<Waybill> waybills = waybillService.list(Wrappers.<Waybill>lambdaQuery()
@@ -301,6 +340,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
public IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request) {
LambdaQueryWrapper<WaybillImportBatch> queryWrapper = Wrappers.<WaybillImportBatch>lambdaQuery()
.eq(WaybillImportBatch::getIsDeleted, 0)
.eq(request.getId() != null, WaybillImportBatch::getId, request.getId())
.like(Func.isNotEmpty(request.getBatchNo()), WaybillImportBatch::getBatchNo, request.getBatchNo())
.apply(Func.isNotEmpty(request.getCarrierId()), "FIND_IN_SET({0}, carrier_ids)", request.getCarrierId())
.eq(Func.isNotEmpty(request.getCarrierName()), WaybillImportBatch::getCarrierName, request.getCarrierName())
@@ -331,6 +371,23 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
return result;
}
/**
* 确认导入只接受进行中、已完成,并原样写入运单业务状态;保存草稿仍为 draft。
*/
private String resolveImportStatus(String status) {
if (STATUS_DRAFT.equals(status)) return STATUS_DRAFT;
if (STATUS_RUNNING.equals(status) || "进行中".equals(status)) return STATUS_RUNNING;
if (STATUS_COMPLETED.equals(status) || "已完成".equals(status)) return STATUS_COMPLETED;
throw new ServiceException("导入状态仅支持进行中或已完成");
}
private String importStatusName(String importStatus) {
if (STATUS_DRAFT.equals(importStatus)) return "草稿";
if (STATUS_RUNNING.equals(importStatus)) return "进行中";
if (STATUS_COMPLETED.equals(importStatus)) return "已完成";
return Func.toStr(importStatus);
}
private WaybillImportBatch buildBatch(WaybillImportBatchRequest request, String importStatus) {
if (Func.isEmpty(request.getProjectId())) throw new ServiceException("请选择项目");
if (Func.isEmpty(request.getContractId())) throw new ServiceException("请选择客户合同");
@@ -349,17 +406,28 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
}
batch.setProjectName(project.getProjectName());
if (Func.isEmpty(batch.getCustomerName())) batch.setCustomerName(project.getCustomerNames());
TransportPlan plan = Func.isEmpty(request.getPlanId()) ? null : transportPlanService.getById(request.getPlanId());
if (plan != null) batch.setPlanName(plan.getPlanName());
// BladeX 数字 null 序列化为 -1;未选计划时不得落库占位值
Long planId = normalizeOptionalId(request.getPlanId());
batch.setPlanId(planId);
TransportPlan plan = planId == null ? null : transportPlanService.getById(planId);
if (plan != null) {
batch.setPlanName(plan.getPlanName());
} else {
batch.setPlanName(null);
}
return batch;
}
private Long normalizeOptionalId(Long id) {
return id == null || id <= 0L ? null : id;
}
private Waybill buildWaybill(Map<String, Object> row, WaybillImportBatch batch, Long carrierContractId, boolean draft) {
if (row == null) throw new ServiceException("运单数据不正确");
Waybill waybill = new Waybill();
waybill.setOriginalNo(stringValue(row, "originalNo"));
waybill.setLoadingNo(stringValue(row, "loadingIdentifier", "配载标识号"));
waybill.setVehicleNo(stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号"));
waybill.setLoadingIdentifier(stringValue(row, "loadingIdentifier", "配载标识号"));
waybill.setVehicleNo(stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号"));
waybill.setDriverId(longValue(row, "driverId", "司机ID"));
waybill.setDriverName(stringValue(row, "driverName", "司机/船长姓名", "司机/船长"));
waybill.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号"));
@@ -404,8 +472,9 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
waybill.setCarrierId(firstCarrierId(batch.getCarrierIds()));
waybill.setCarrierName(batch.getCarrierName());
waybill.setCarrierContractId(carrierContractId);
waybill.setPlanId(batch.getPlanId());
waybill.setPlanName(batch.getPlanName());
Long planId = normalizeOptionalId(batch.getPlanId());
waybill.setPlanId(planId);
waybill.setPlanName(planId == null ? null : batch.getPlanName());
waybill.setImportBatchId(batch.getId());
waybill.setBatchNo(batch.getBatchNo());
waybill.setDataSource("批量导入");
@@ -497,7 +566,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
WaybillImportBatchVO vo = BeanUtil.copyProperties(batch, WaybillImportBatchVO.class);
if (vo == null) throw new ServiceException("运单批次数据转换失败");
vo.setImportTypeName(IMPORT_TYPE_SETTLEMENT.equals(batch.getImportType()) ? "结算单" : "运单");
vo.setStatusName(STATUS_DRAFT.equals(batch.getImportStatus()) ? "草稿" : "导入完成");
vo.setStatusName(importStatusName(batch.getImportStatus()));
vo.setCreateUserName(UserCache.getUserRealName(batch.getCreateUser()));
vo.setUpdateUserName(UserCache.getUserRealName(batch.getUpdateUser()));
return vo;
@@ -627,12 +696,12 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
}
// 检查同一配载标识号下车牌号是否一致
String currentVehicleNo = stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号");
String currentVehicleNo = stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
for (Integer otherRowIndex : sameIdentifierRows) {
if (otherRowIndex.equals(rowIndex)) {
continue;
}
String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo", "车牌号/航班号/船号/班列号");
String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
if (Func.isNotEmpty(currentVehicleNo) && Func.isNotEmpty(otherVehicleNo)
&& !currentVehicleNo.equals(otherVehicleNo)) {
errors.add("同一配载标识号下,车牌号不一致");
@@ -643,14 +712,14 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
}
private void validateVehicleNo(Map<String, Object> row, List<String> errors) {
String vehicleNo = stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号");
String vehicleNo = stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
String transportType = stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式");
if (Func.isEmpty(transportType)) {
return;
}
// 公路运输必须填写车牌号;其他运输方式的该字段可填写航班号、船号或班列号。
// 公路运输必须填写车牌号;其他运输方式的该字段可填写船班号、船次号或班次号。
if (!isRoadTransport(transportType)) {
return;
}
@@ -887,12 +956,12 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
}
// 检查同一运单标识号下车牌号是否一致
String currentVehicleNo = stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号");
String currentVehicleNo = stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
for (Integer otherRowIndex : sameIdentifierRows) {
if (otherRowIndex.equals(rowIndex)) {
continue;
}
String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo", "车牌号/航班号/船号/班列号");
String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
if (Func.isNotEmpty(currentVehicleNo) && Func.isNotEmpty(otherVehicleNo)
&& !currentVehicleNo.equals(otherVehicleNo)) {
errors.add("同一运单标识号下,车牌号必须一致");
@@ -1032,7 +1101,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
WaybillImportBatchExcel excel = new WaybillImportBatchExcel();
excel.setOriginalNo(stringValue(row, "originalNo"));
excel.setLoadingIdentifier(stringValue(row, "loadingIdentifier", "配载标识号"));
excel.setVehicleNo(stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号"));
excel.setVehicleNo(stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号"));
excel.setTransportType(stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式"));
excel.setDriverName(stringValue(row, "driverName", "司机/船长姓名", "司机/船长"));
excel.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号"));
@@ -685,6 +685,10 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
@Transactional(rollbackFor = Exception.class)
public boolean submit(Waybill waybill) {
boolean created = Func.isEmpty(waybill.getId());
// 正式提交须脱离草稿:draft 在过程校正中被视为终态不会提升,需先清空再按接单配置校正为 pending/running
if (STATUS_DRAFT.equals(waybill.getBusinessStatus())) {
waybill.setBusinessStatus(null);
}
prepareForSave(waybill);
validate(waybill);
boolean result = saveOrUpdate(waybill);
@@ -711,6 +715,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
assertNotLoaded(oldRecord);
waybill.setWaybillNo(oldRecord.getWaybillNo());
waybill.setLoadingNo(oldRecord.getLoadingNo());
waybill.setLoadingIdentifier(oldRecord.getLoadingIdentifier());
waybill.setDeptId(oldRecord.getDeptId());
waybill.setDeptName(oldRecord.getDeptName());
waybill.setMileageRemark(oldRecord.getMileageRemark());
@@ -901,7 +906,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
@Transactional(rollbackFor = Exception.class)
public List<WaybillExcel> importWaybill(List<WaybillExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<WaybillExcel> errorList = new ArrayList<>();
for (int index = 0; index < data.size(); index++) {
@@ -922,6 +927,13 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
errorList.add(excel);
}
}
if (Func.isNotEmpty(errorList)) {
// 抛出携带失败明细的异常,触发事务回滚:submit 内部一次写运单主表+
// 货物+配载路线等多张关联表,回滚边界必须覆盖全部关联表。
// 注意不能只往 errorList 里塞数据就 return——异常被吞掉后方法会正常返回,
// Spring 会照常提交事务,@Transactional 起不到回滚作用。
throw new org.springblade.common.excel.ImportFailureException(data);
}
return errorList;
}
@@ -982,6 +994,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
target.setPlanName(source.getPlanName());
target.setMasterNo(null);
target.setLoadingNo(null);
target.setLoadingIdentifier(null);
target.setBatchNo(source.getBatchNo());
target.setRelationNo(source.getRelationNo());
target.setCurrentProcessNode(source.getCurrentProcessNode());
@@ -1302,7 +1315,16 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
queryWrapper.like(Waybill::getOriginalNo, waybill.getOriginalNo());
}
if (Func.isNotEmpty(waybill.getBusinessStatus())) {
queryWrapper.eq(Waybill::getBusinessStatus, waybill.getBusinessStatus());
List<String> businessStatuses = Arrays.stream(waybill.getBusinessStatus().split("[,,]"))
.map(String::trim)
.filter(Func::isNotEmpty)
.distinct()
.toList();
if (businessStatuses.size() == 1) {
queryWrapper.eq(Waybill::getBusinessStatus, businessStatuses.get(0));
} else if (!businessStatuses.isEmpty()) {
queryWrapper.in(Waybill::getBusinessStatus, businessStatuses);
}
}
if (Func.isNotEmpty(waybill.getDataSource())) {
queryWrapper.eq(Waybill::getDataSource, waybill.getDataSource());
@@ -1396,8 +1418,16 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
waybill.setBusinessStatus(TransportBusinessSupport.trimToNull(waybill.getBusinessStatus()));
waybill.setDataSource(TransportBusinessSupport.normalizeWaybillDataSource(waybill.getDataSource()));
waybill.setPlanName(TransportBusinessSupport.trimToNull(waybill.getPlanName()));
// BladeX 数字 null 序列化为 -1;未选计划时归一为空,避免占位值落库/回显
if (waybill.getPlanId() != null && waybill.getPlanId() <= 0L) {
waybill.setPlanId(null);
}
if (waybill.getPlanId() == null || "-1".equals(waybill.getPlanName())) {
waybill.setPlanName(null);
}
waybill.setMasterNo(TransportBusinessSupport.trimToNull(waybill.getMasterNo()));
waybill.setLoadingNo(TransportBusinessSupport.trimToNull(waybill.getLoadingNo()));
waybill.setLoadingIdentifier(TransportBusinessSupport.trimToNull(waybill.getLoadingIdentifier()));
waybill.setBatchNo(TransportBusinessSupport.trimToNull(waybill.getBatchNo()));
waybill.setRelationNo(TransportBusinessSupport.trimToNull(waybill.getRelationNo()));
waybill.setCurrentProcessNode(TransportBusinessSupport.trimToNull(waybill.getCurrentProcessNode()));
@@ -1470,7 +1500,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
TransportBusinessSupport.validateRequired(waybill.getQuantityUnit(), "数量单位不能为空");
TransportBusinessSupport.validateRequired(waybill.getCarrierType(), "承运类型不能为空");
TransportBusinessSupport.validateRequired(
waybill.getVehicleNo(), isRoadTransport(waybill.getTransportType()) ? "车牌号不能为空" : "船/航/班列号不能为空"
waybill.getVehicleNo(), isRoadTransport(waybill.getTransportType()) ? "车牌号不能为空" : "船班号/船次号/班次号不能为空"
);
if (isRoadTransport(waybill.getTransportType())) {
if ("承运商".equals(waybill.getCarrierType())) {
@@ -1507,7 +1537,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
TransportBusinessSupport.validateLength(waybill.getCarrierName(), 255, "承运商名称不能超过255字");
TransportBusinessSupport.validateLength(waybill.getDriverName(), 255, "司机姓名不能超过255字");
TransportBusinessSupport.validateLength(waybill.getDriverPhone(), 50, "司机手机号不能超过50字");
TransportBusinessSupport.validateLength(waybill.getVehicleNo(), 255, "车/船/航班/班列号不能超过255字");
TransportBusinessSupport.validateLength(waybill.getVehicleNo(), 255, "车牌号/船班号/船次号/班次号不能超过255字");
TransportBusinessSupport.validateLength(waybill.getCaptainName(), 20, "船长不能超过20字");
TransportBusinessSupport.validateLength(waybill.getCabinNo(), 30, "舱位不能超过30字");
TransportBusinessSupport.validateLength(waybill.getContainerNo(), 30, "箱号不能超过30字");
@@ -1617,7 +1647,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
if (isRoadTransport(waybill.getTransportType())) {
return;
}
TransportBusinessSupport.validateLength(waybill.getVehicleNo(), 30, "船/航/班列号不能超过30字");
TransportBusinessSupport.validateLength(waybill.getVehicleNo(), 30, "船班号/船次号/班次号不能超过30字");
TransportBusinessSupport.validateLength(waybill.getCaptainName(), 20, "船长不能超过20字");
TransportBusinessSupport.validateLength(waybill.getCabinNo(), 30, "舱位不能超过30字");
TransportBusinessSupport.validateLength(waybill.getContainerNo(), 30, "箱号不能超过30字");
@@ -25,12 +25,15 @@ package org.springblade.transport.support;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.constant.RoleConstant;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.SysCache;
import org.springblade.system.pojo.entity.Dept;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
@@ -55,6 +58,24 @@ public final class TransportBusinessSupport {
private TransportBusinessSupport() {
}
/**
* 是否超级管理员:当前用户任一角色别名等于 administrator 即为超级管理员。
*
* @return true-超级管理员
*/
public static boolean isAdministrator() {
String userRole = AuthUtil.getUserRole();
if (Func.isBlank(userRole)) {
return false;
}
for (String roleAlias : Func.toStrArray(userRole)) {
if (RoleConstant.ADMINISTRATOR.equals(Func.toStr(roleAlias).trim())) {
return true;
}
}
return false;
}
public static Long currentDeptId(String moduleName) {
Long deptId = Func.firstLong(AuthUtil.getDeptId());
if (Func.isEmpty(deptId) || deptId <= 0) {
@@ -78,11 +99,40 @@ public final class TransportBusinessSupport {
}
public static void validateAllDept(Integer allDept, String moduleName) {
if (Objects.equals(allDept, 1) && !AuthUtil.isAdministrator()) {
if (Objects.equals(allDept, 1) && !isAdministrator()) {
throw new ServiceException("无权查看全部组织" + moduleName);
}
}
/**
* 解析列表组织筛选 ID。
* <p>
* 普通用户 allDept!=1 时强制当前组织;超管默认不按组织过滤,仅在 allDept=1 且传入 deptId 时精确匹配该组织。
*
* @param allDept 是否查看全部组织
* @param deptId 筛选组织 ID
* @param moduleName 模块名称
* @return 需要过滤的组织 ID 集合;空集合表示不过滤组织
*/
public static List<Long> resolveDeptFilterIds(Integer allDept, Long deptId, String moduleName) {
validateAllDept(allDept, moduleName);
if (isAdministrator()) {
if (Objects.equals(allDept, 1) && Func.isNotEmpty(deptId) && deptId > 0) {
return Collections.singletonList(deptId);
}
return Collections.emptyList();
}
if (!Objects.equals(allDept, 1)) {
return Collections.singletonList(currentDeptId(moduleName));
}
// Jackson 空值可能被反序列化为 -1,这里与空值同等对待,避免误过滤。
if (Func.isEmpty(deptId) || deptId <= 0) {
return Collections.emptyList();
}
// 列表组织筛选项为单选,按所选组织精确过滤,不自动展开下级。
return Collections.singletonList(deptId);
}
public static String trimToEmpty(String value) {
return value == null ? "" : value.trim();
}
@@ -31,6 +31,7 @@ import org.springblade.system.cache.UserCache;
import org.springblade.transport.pojo.entity.AccidentRecord;
import org.springblade.transport.pojo.vo.AccidentRecordVO;
import java.math.BigDecimal;
import java.util.Objects;
/**
@@ -40,6 +41,8 @@ import java.util.Objects;
*/
public class AccidentRecordWrapper extends BaseEntityWrapper<AccidentRecord, AccidentRecordVO> {
private static final BigDecimal EMPTY_AMOUNT_SENTINEL = BigDecimal.valueOf(-1);
public static AccidentRecordWrapper build() {
return new AccidentRecordWrapper();
}
@@ -47,8 +50,16 @@ public class AccidentRecordWrapper extends BaseEntityWrapper<AccidentRecord, Acc
@Override
public AccidentRecordVO entityVO(AccidentRecord accidentRecord) {
AccidentRecordVO accidentRecordVO = Objects.requireNonNull(BeanUtil.copyProperties(accidentRecord, AccidentRecordVO.class));
// 未填写金额的占位值 -1 对外展示为空
accidentRecordVO.setDirectEconomicLoss(normalizeEmptyAmount(accidentRecordVO.getDirectEconomicLoss()));
accidentRecordVO.setInsuranceClaimAmount(normalizeEmptyAmount(accidentRecordVO.getInsuranceClaimAmount()));
accidentRecordVO.setCreateUserName(UserCache.getUserRealName(accidentRecord.getCreateUser()));
accidentRecordVO.setUpdateUserName(UserCache.getUserRealName(accidentRecord.getUpdateUser()));
return accidentRecordVO;
}
private BigDecimal normalizeEmptyAmount(BigDecimal value) {
return value != null && value.compareTo(EMPTY_AMOUNT_SENTINEL) == 0 ? null : value;
}
}
@@ -31,6 +31,7 @@ import org.springblade.system.cache.UserCache;
import org.springblade.transport.pojo.entity.AnnualInspectionRecord;
import org.springblade.transport.pojo.vo.AnnualInspectionRecordVO;
import java.math.BigDecimal;
import java.util.Objects;
/**
@@ -40,6 +41,8 @@ import java.util.Objects;
*/
public class AnnualInspectionRecordWrapper extends BaseEntityWrapper<AnnualInspectionRecord, AnnualInspectionRecordVO> {
private static final BigDecimal EMPTY_AMOUNT_SENTINEL = BigDecimal.valueOf(-1);
public static AnnualInspectionRecordWrapper build() {
return new AnnualInspectionRecordWrapper();
}
@@ -47,8 +50,15 @@ public class AnnualInspectionRecordWrapper extends BaseEntityWrapper<AnnualInspe
@Override
public AnnualInspectionRecordVO entityVO(AnnualInspectionRecord annualInspectionRecord) {
AnnualInspectionRecordVO annualInspectionRecordVO = Objects.requireNonNull(BeanUtil.copyProperties(annualInspectionRecord, AnnualInspectionRecordVO.class));
// 费用占位值 -1 对外展示为空
annualInspectionRecordVO.setFee(normalizeEmptyAmount(annualInspectionRecordVO.getFee()));
annualInspectionRecordVO.setCreateUserName(UserCache.getUserRealName(annualInspectionRecord.getCreateUser()));
annualInspectionRecordVO.setUpdateUserName(UserCache.getUserRealName(annualInspectionRecord.getUpdateUser()));
return annualInspectionRecordVO;
}
private BigDecimal normalizeEmptyAmount(BigDecimal value) {
return value != null && value.compareTo(EMPTY_AMOUNT_SENTINEL) == 0 ? null : value;
}
}
@@ -24,6 +24,7 @@ package org.springblade.transport.wrapper;
import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.pojo.entity.CreditScoreQuantification;
import org.springblade.transport.pojo.vo.CreditScoreQuantificationVO;
@@ -42,7 +43,9 @@ public class CreditScoreQuantificationWrapper extends BaseEntityWrapper<CreditSc
@Override
public CreditScoreQuantificationVO entityVO(CreditScoreQuantification quantification) {
return Objects.requireNonNull(BeanUtil.copyProperties(quantification, CreditScoreQuantificationVO.class));
CreditScoreQuantificationVO vo = Objects.requireNonNull(BeanUtil.copyProperties(quantification, CreditScoreQuantificationVO.class));
vo.setCreateUserName(UserCache.getUserRealName(quantification.getCreateUser()));
return vo;
}
}

Some files were not shown because too many files have changed in this diff Show More