1、调整凭证
2、调整运单
This commit is contained in:
+6
@@ -145,4 +145,10 @@ public class TransportPlan extends TenantEntity {
|
|||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "里程(km)")
|
||||||
|
private java.math.BigDecimal mileage;
|
||||||
|
|
||||||
|
@Schema(description = "同一计划标识号")
|
||||||
|
private String planGroupId;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -26,6 +26,7 @@ public class VoucherManage extends TenantEntity {
|
|||||||
private Long fileTaskId;
|
private Long fileTaskId;
|
||||||
private String uploadSource;
|
private String uploadSource;
|
||||||
private String carrierName;
|
private String carrierName;
|
||||||
|
private Long carrierId;
|
||||||
private String processStatus;
|
private String processStatus;
|
||||||
private Integer voucherCount;
|
private Integer voucherCount;
|
||||||
private Integer relatedWaybillCount;
|
private Integer relatedWaybillCount;
|
||||||
|
|||||||
+3
@@ -4,6 +4,7 @@ import lombok.Data;
|
|||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 凭证解压文件详情。
|
* 凭证解压文件详情。
|
||||||
@@ -29,4 +30,6 @@ public class VoucherFileVO implements Serializable {
|
|||||||
private String fileType;
|
private String fileType;
|
||||||
private Integer matched;
|
private Integer matched;
|
||||||
private String url;
|
private String url;
|
||||||
|
private Date createTime;
|
||||||
|
private Date updateTime;
|
||||||
}
|
}
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.pojo.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 执行凭证车牌文件夹详情。 */
|
||||||
|
@Data
|
||||||
|
public class VoucherFolderVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long voucherId;
|
||||||
|
private String voucherBatchNo;
|
||||||
|
private String voucherNo;
|
||||||
|
private String plateNo;
|
||||||
|
private String folderName;
|
||||||
|
private Long waybillId;
|
||||||
|
private String waybillNo;
|
||||||
|
private Integer matched;
|
||||||
|
private String processStatus;
|
||||||
|
private String fileName;
|
||||||
|
private Date createTime;
|
||||||
|
private Date updateTime;
|
||||||
|
private List<VoucherFileVO> files;
|
||||||
|
}
|
||||||
+14
@@ -155,6 +155,13 @@ public class TransportReconciliationController extends BladeController {
|
|||||||
@Operation(summary = "自动匹配内部账单")
|
@Operation(summary = "自动匹配内部账单")
|
||||||
public R match(@RequestParam Long id) { reconciliationService.autoMatch(id); return R.success("匹配完成"); }
|
public R match(@RequestParam Long id) { reconciliationService.autoMatch(id); return R.success("匹配完成"); }
|
||||||
|
|
||||||
|
@PostMapping("/match-preview")
|
||||||
|
@ApiOperationSupport(order = 9)
|
||||||
|
@Operation(summary = "预览自动匹配内部账单")
|
||||||
|
public R<TransportReconciliationVO> matchPreview(@RequestBody TransportReconciliationVO request) {
|
||||||
|
return R.data(reconciliationService.matchPreview(request));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/manual-match")
|
@PostMapping("/manual-match")
|
||||||
@ApiOperationSupport(order = 10)
|
@ApiOperationSupport(order = 10)
|
||||||
@Operation(summary = "人工匹配账单明细")
|
@Operation(summary = "人工匹配账单明细")
|
||||||
@@ -179,4 +186,11 @@ public class TransportReconciliationController extends BladeController {
|
|||||||
@ApiOperationSupport(order = 14)
|
@ApiOperationSupport(order = 14)
|
||||||
@Operation(summary = "完成运输对账")
|
@Operation(summary = "完成运输对账")
|
||||||
public R complete(@RequestParam Long id) { reconciliationService.complete(id); return R.success("对账单确认完成"); }
|
public R complete(@RequestParam Long id) { reconciliationService.complete(id); return R.success("对账单确认完成"); }
|
||||||
|
|
||||||
|
@PostMapping("/complete-with-data")
|
||||||
|
@ApiOperationSupport(order = 14)
|
||||||
|
@Operation(summary = "保存明细并完成运输对账")
|
||||||
|
public R<TransportReconciliationVO> completeWithData(@RequestBody TransportReconciliationVO request) {
|
||||||
|
return R.data(reconciliationService.completeWithData(request));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
@@ -15,6 +15,8 @@ import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest;
|
|||||||
import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest;
|
import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest;
|
||||||
import org.springblade.transport.pojo.entity.VoucherManage;
|
import org.springblade.transport.pojo.entity.VoucherManage;
|
||||||
import org.springblade.transport.pojo.vo.VoucherManageVO;
|
import org.springblade.transport.pojo.vo.VoucherManageVO;
|
||||||
|
import org.springblade.transport.pojo.vo.VoucherFolderVO;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springblade.transport.service.IVoucherManageService;
|
import org.springblade.transport.service.IVoucherManageService;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -46,6 +48,34 @@ public class VoucherManageController extends BladeController {
|
|||||||
@Operation(summary = "凭证详情")
|
@Operation(summary = "凭证详情")
|
||||||
public R<VoucherManageVO> detail(@RequestParam Long id) { return R.data(voucherManageService.detail(id)); }
|
public R<VoucherManageVO> detail(@RequestParam Long id) { return R.data(voucherManageService.detail(id)); }
|
||||||
|
|
||||||
|
@GetMapping("/folder-page")
|
||||||
|
@Operation(summary = "执行凭证文件夹分页")
|
||||||
|
public R<IPage<VoucherFolderVO>> folderPage(@RequestParam Long voucherId, Query query,
|
||||||
|
@RequestParam(required = false) String plateNo, @RequestParam(required = false) Integer matched) {
|
||||||
|
return R.data(voucherManageService.folderPage(Condition.getPage(query), voucherId, plateNo, matched));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/folder-detail")
|
||||||
|
@Operation(summary = "执行凭证文件夹详情")
|
||||||
|
public R<VoucherFolderVO> folderDetail(@RequestParam Long voucherId, @RequestParam String plateNo) {
|
||||||
|
return R.data(voucherManageService.folderDetail(voucherId, plateNo));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/folder-replace", consumes = "multipart/form-data")
|
||||||
|
@Operation(summary = "替换单个车牌凭证")
|
||||||
|
public R replaceFolder(@RequestParam Long voucherId, @RequestParam String plateNo,
|
||||||
|
@RequestParam("file") MultipartFile file) {
|
||||||
|
voucherManageService.replaceFolder(voucherId, plateNo, file);
|
||||||
|
return R.success("上传成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/folder-remove")
|
||||||
|
@Operation(summary = "删除车牌凭证")
|
||||||
|
public R removeFolder(@RequestParam Long voucherId, @RequestParam String plateNo) {
|
||||||
|
voucherManageService.removeFolder(voucherId, plateNo);
|
||||||
|
return R.success("删除成功");
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/submit")
|
@PostMapping("/submit")
|
||||||
@ApiOperationSupport(order = 3)
|
@ApiOperationSupport(order = 3)
|
||||||
@Operation(summary = "上传凭证或更换运输批次")
|
@Operation(summary = "上传凭证或更换运输批次")
|
||||||
@@ -85,4 +115,20 @@ public class VoucherManageController extends BladeController {
|
|||||||
@RequestParam(required = false) String createTimeStart, @RequestParam(required = false) String createTimeEnd) {
|
@RequestParam(required = false) String createTimeStart, @RequestParam(required = false) String createTimeEnd) {
|
||||||
return R.data(voucherManageService.selectableWaybillBatches(Condition.getPage(query), batchNo, createUser, waybillCount, createTimeStart, createTimeEnd));
|
return R.data(voucherManageService.selectableWaybillBatches(Condition.getPage(query), batchNo, createUser, waybillCount, createTimeStart, createTimeEnd));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/audit-pass")
|
||||||
|
@ApiOperationSupport(order = 7)
|
||||||
|
@Operation(summary = "审核通过")
|
||||||
|
public R auditPass(@RequestParam Long id) {
|
||||||
|
voucherManageService.auditPass(id);
|
||||||
|
return R.success("审核通过");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/audit-reject")
|
||||||
|
@ApiOperationSupport(order = 8)
|
||||||
|
@Operation(summary = "审核驳回")
|
||||||
|
public R auditReject(@RequestParam Long id, @RequestParam(required = false) String rejectReason) {
|
||||||
|
voucherManageService.auditReject(id, rejectReason);
|
||||||
|
return R.success("审核驳回");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-24
@@ -49,46 +49,44 @@ public class TransportPlanImportExcel implements Serializable {
|
|||||||
|
|
||||||
@ExcelProperty("*计划名称")
|
@ExcelProperty("*计划名称")
|
||||||
private String planName;
|
private String planName;
|
||||||
@ExcelProperty("*运输方式")
|
@ExcelProperty("*运输类型")
|
||||||
private String transportType;
|
private String transportType;
|
||||||
@ExcelProperty("*计划开始日期")
|
@ExcelProperty("*发货地址")
|
||||||
private String planStartDate;
|
private String departureAddress;
|
||||||
@ExcelProperty("*计划结束日期")
|
@ExcelProperty("发货联系人")
|
||||||
private String planEndDate;
|
private String departureContact;
|
||||||
|
@ExcelProperty("发货联系人电话")
|
||||||
|
private String departurePhone;
|
||||||
|
@ExcelProperty("*到货地址")
|
||||||
|
private String arrivalAddress;
|
||||||
|
@ExcelProperty("收货联系人")
|
||||||
|
private String arrivalContact;
|
||||||
|
@ExcelProperty("收货联系人电话")
|
||||||
|
private String arrivalPhone;
|
||||||
@ExcelProperty("货物名称")
|
@ExcelProperty("货物名称")
|
||||||
private String cargoName;
|
private String cargoName;
|
||||||
@ExcelProperty("*货物类型")
|
@ExcelProperty("*货物类型")
|
||||||
private String cargoType;
|
private String cargoType;
|
||||||
@ExcelProperty("数量")
|
@ExcelProperty("数量")
|
||||||
private BigDecimal quantity;
|
private BigDecimal quantity;
|
||||||
@ExcelProperty("数量单位")
|
@ExcelProperty("计量单位")
|
||||||
private String quantityUnit;
|
private String quantityUnit;
|
||||||
@ExcelProperty("包装")
|
@ExcelProperty("包装")
|
||||||
private String packageType;
|
private String packageType;
|
||||||
@ExcelProperty("品牌")
|
|
||||||
private String brand;
|
|
||||||
@ExcelProperty("规格")
|
@ExcelProperty("规格")
|
||||||
private String specification;
|
private String specification;
|
||||||
@ExcelProperty("型号")
|
@ExcelProperty("型号")
|
||||||
private String model;
|
private String model;
|
||||||
@ExcelProperty("物料编码")
|
@ExcelProperty("里程(km)")
|
||||||
private String materialCode;
|
private BigDecimal mileage;
|
||||||
@ExcelProperty("设备编码")
|
@ExcelProperty("计划开始时间")
|
||||||
private String deviceCode;
|
private String planStartDate;
|
||||||
@ExcelProperty("*发货地址")
|
@ExcelProperty("计划结束时间")
|
||||||
private String departureAddress;
|
private String planEndDate;
|
||||||
@ExcelProperty("发货联系人")
|
|
||||||
private String departureContact;
|
|
||||||
@ExcelProperty("发货联系方式")
|
|
||||||
private String departurePhone;
|
|
||||||
@ExcelProperty("*收货地址")
|
|
||||||
private String arrivalAddress;
|
|
||||||
@ExcelProperty("收货联系人")
|
|
||||||
private String arrivalContact;
|
|
||||||
@ExcelProperty("收货联系方式")
|
|
||||||
private String arrivalPhone;
|
|
||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
@ExcelProperty("同一计划标识号")
|
||||||
|
private String planGroupId;
|
||||||
|
|
||||||
@ExcelIgnore
|
@ExcelIgnore
|
||||||
private String errorMessage;
|
private String errorMessage;
|
||||||
|
|||||||
+5
-6
@@ -34,8 +34,7 @@ import java.io.Serial;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 运单管理 Excel
|
* 运单管理 Excel
|
||||||
@@ -151,12 +150,12 @@ public class WaybillExcel implements Serializable {
|
|||||||
private String createUserName;
|
private String createUserName;
|
||||||
@ExcelProperty("更新人")
|
@ExcelProperty("更新人")
|
||||||
private String updateUserName;
|
private String updateUserName;
|
||||||
@ExcelProperty(value = "创建时间", converter = MaintenancePlanDateTimeConverter.class)
|
@ExcelProperty("创建时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private Date createTime;
|
||||||
@ExcelProperty(value = "更新时间", converter = MaintenancePlanDateTimeConverter.class)
|
@ExcelProperty("更新时间")
|
||||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@ExcelIgnore
|
@ExcelIgnore
|
||||||
private String errorMessage;
|
private String errorMessage;
|
||||||
|
|||||||
+44
-22
@@ -22,45 +22,67 @@ public class WaybillImportBatchExcel implements Serializable {
|
|||||||
@Serial
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ExcelProperty("序号")
|
||||||
|
private Integer serialNumber;
|
||||||
@ExcelProperty("原始单号")
|
@ExcelProperty("原始单号")
|
||||||
private String originalNo;
|
private String originalNo;
|
||||||
|
@ExcelProperty("配载标识号")
|
||||||
|
private String loadingIdentifier;
|
||||||
@ExcelProperty("*车牌号/航班号/船号/班列号")
|
@ExcelProperty("*车牌号/航班号/船号/班列号")
|
||||||
private String vehicleNo;
|
private String vehicleNo;
|
||||||
@ExcelProperty("*司机/船长")
|
@ExcelProperty("*运输方式")
|
||||||
private String driverName;
|
|
||||||
@ExcelProperty("*运输类型")
|
|
||||||
private String transportType;
|
private String transportType;
|
||||||
|
@ExcelProperty("司机/船长姓名")
|
||||||
|
private String driverName;
|
||||||
|
@ExcelProperty("司机/船长手机号")
|
||||||
|
private String driverPhone;
|
||||||
|
@ExcelProperty("*发货地址")
|
||||||
|
private String departureAddress;
|
||||||
|
@ExcelProperty("发货联系人")
|
||||||
|
private String departureContact;
|
||||||
|
@ExcelProperty("发货联系人电话")
|
||||||
|
private String departurePhone;
|
||||||
|
@ExcelProperty("*到货地址")
|
||||||
|
private String arrivalAddress;
|
||||||
|
@ExcelProperty("收货联系人")
|
||||||
|
private String arrivalContact;
|
||||||
|
@ExcelProperty("收货联系人电话")
|
||||||
|
private String arrivalPhone;
|
||||||
@ExcelProperty("*货物名称")
|
@ExcelProperty("*货物名称")
|
||||||
private String cargoName;
|
private String cargoName;
|
||||||
@ExcelProperty("*货物类型")
|
@ExcelProperty("*货物类型")
|
||||||
private String cargoType;
|
private String cargoType;
|
||||||
@ExcelProperty("重量")
|
@ExcelProperty("包装")
|
||||||
|
private String packageType;
|
||||||
|
@ExcelProperty("*数量")
|
||||||
private BigDecimal quantity;
|
private BigDecimal quantity;
|
||||||
@ExcelProperty("*发货地址")
|
@ExcelProperty("*数量单位")
|
||||||
private String departureAddress;
|
private String quantityUnit;
|
||||||
@ExcelProperty("*发货联系人")
|
@ExcelProperty("规格")
|
||||||
private String departureContact;
|
private String specification;
|
||||||
@ExcelProperty("*发货联系人电话")
|
@ExcelProperty("型号")
|
||||||
private String departurePhone;
|
private String model;
|
||||||
@ExcelProperty("*到货地址")
|
@ExcelProperty("里程(km)")
|
||||||
private String arrivalAddress;
|
private BigDecimal mileage;
|
||||||
@ExcelProperty("*到货联系人")
|
@ExcelProperty("单价")
|
||||||
private String arrivalContact;
|
|
||||||
@ExcelProperty("*收货联系人电话")
|
|
||||||
private String arrivalPhone;
|
|
||||||
@ExcelProperty("*开始时间")
|
|
||||||
private String startDate;
|
|
||||||
@ExcelProperty("*结束时间")
|
|
||||||
private String endDate;
|
|
||||||
@ExcelProperty("*单价")
|
|
||||||
private BigDecimal unitPrice;
|
private BigDecimal unitPrice;
|
||||||
@ExcelProperty("*运费")
|
@ExcelProperty("运费")
|
||||||
private BigDecimal freight;
|
private BigDecimal freight;
|
||||||
@ExcelProperty("其他费用合计")
|
@ExcelProperty("其他费用合计")
|
||||||
private BigDecimal otherFeeTotal;
|
private BigDecimal otherFeeTotal;
|
||||||
@ExcelProperty("运费合计")
|
@ExcelProperty("运费合计")
|
||||||
private BigDecimal freightTotal;
|
private BigDecimal freightTotal;
|
||||||
|
@ExcelProperty("*实际发货时间")
|
||||||
|
private String actualStartDate;
|
||||||
|
@ExcelProperty("*实际完成时间")
|
||||||
|
private String actualEndDate;
|
||||||
|
@ExcelProperty("预计发货时间")
|
||||||
|
private String planStartDate;
|
||||||
|
@ExcelProperty("预计完成时间")
|
||||||
|
private String planEndDate;
|
||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
@ExcelProperty("同一运单标识号")
|
||||||
|
private String waybillIdentifier;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+17
@@ -15,4 +15,21 @@ public interface VoucherManageMapper extends BaseMapper<VoucherManage> {
|
|||||||
+ "FROM blade_voucher_manage WHERE tenant_id = #{tenantId} "
|
+ "FROM blade_voucher_manage WHERE tenant_id = #{tenantId} "
|
||||||
+ "AND voucher_batch_no LIKE CONCAT(#{prefix}, '%')")
|
+ "AND voucher_batch_no LIKE CONCAT(#{prefix}, '%')")
|
||||||
Long selectMaxDailySequence(@Param("tenantId") String tenantId, @Param("prefix") String prefix);
|
Long selectMaxDailySequence(@Param("tenantId") String tenantId, @Param("prefix") String prefix);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询组织的顶级父组织ID(递归查询到根节点)
|
||||||
|
*/
|
||||||
|
@Select("WITH RECURSIVE dept_tree AS ( "
|
||||||
|
+ "SELECT id, parent_id, dept_name FROM blade_dept WHERE id = #{deptId} AND is_deleted = 0 "
|
||||||
|
+ "UNION ALL "
|
||||||
|
+ "SELECT d.id, d.parent_id, d.dept_name FROM blade_dept d "
|
||||||
|
+ "INNER JOIN dept_tree dt ON d.id = dt.parent_id WHERE d.is_deleted = 0 "
|
||||||
|
+ ") SELECT id FROM dept_tree WHERE parent_id = 0 OR parent_id IS NULL LIMIT 1")
|
||||||
|
Long selectTopDeptId(@Param("deptId") Long deptId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询组织名称
|
||||||
|
*/
|
||||||
|
@Select("SELECT dept_name FROM blade_dept WHERE id = #{deptId} AND is_deleted = 0")
|
||||||
|
String selectDeptName(@Param("deptId") Long deptId);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -30,6 +30,8 @@ public interface ITransportReconciliationService extends BaseService<TransportRe
|
|||||||
void removeDraft(Long id);
|
void removeDraft(Long id);
|
||||||
List<VehicleReconciliationFailureExcel> importVehicles(Long id, List<VehicleReconciliationExcel> rows);
|
List<VehicleReconciliationFailureExcel> importVehicles(Long id, List<VehicleReconciliationExcel> rows);
|
||||||
List<CargoReconciliationFailureExcel> importCargoes(Long id, List<CargoReconciliationExcel> rows);
|
List<CargoReconciliationFailureExcel> importCargoes(Long id, List<CargoReconciliationExcel> rows);
|
||||||
|
TransportReconciliationVO matchPreview(TransportReconciliationVO request);
|
||||||
|
TransportReconciliationVO completeWithData(TransportReconciliationVO request);
|
||||||
void autoMatch(Long id);
|
void autoMatch(Long id);
|
||||||
void manualMatch(TransportReconciliationManualMatchRequest request);
|
void manualMatch(TransportReconciliationManualMatchRequest request);
|
||||||
void unmatch(Long internalId);
|
void unmatch(Long internalId);
|
||||||
|
|||||||
+8
@@ -7,6 +7,8 @@ import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest;
|
|||||||
import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest;
|
import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest;
|
||||||
import org.springblade.transport.pojo.entity.VoucherManage;
|
import org.springblade.transport.pojo.entity.VoucherManage;
|
||||||
import org.springblade.transport.pojo.vo.VoucherManageVO;
|
import org.springblade.transport.pojo.vo.VoucherManageVO;
|
||||||
|
import org.springblade.transport.pojo.vo.VoucherFolderVO;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -14,6 +16,10 @@ import java.util.Map;
|
|||||||
public interface IVoucherManageService extends BaseService<VoucherManage> {
|
public interface IVoucherManageService extends BaseService<VoucherManage> {
|
||||||
IPage<VoucherManageVO> selectPage(IPage<VoucherManage> page, VoucherManageVO query);
|
IPage<VoucherManageVO> selectPage(IPage<VoucherManage> page, VoucherManageVO query);
|
||||||
VoucherManageVO detail(Long id);
|
VoucherManageVO detail(Long id);
|
||||||
|
IPage<VoucherFolderVO> folderPage(IPage<?> page, Long voucherId, String plateNo, Integer matched);
|
||||||
|
VoucherFolderVO folderDetail(Long voucherId, String plateNo);
|
||||||
|
void replaceFolder(Long voucherId, String plateNo, MultipartFile file);
|
||||||
|
void removeFolder(Long voucherId, String plateNo);
|
||||||
void submit(VoucherManageSubmitRequest request);
|
void submit(VoucherManageSubmitRequest request);
|
||||||
VoucherManage createUploadDraft(VoucherUploadDraftRequest request);
|
VoucherManage createUploadDraft(VoucherUploadDraftRequest request);
|
||||||
void completeUploadFile(VoucherFileCompleteRequest request);
|
void completeUploadFile(VoucherFileCompleteRequest request);
|
||||||
@@ -21,4 +27,6 @@ public interface IVoucherManageService extends BaseService<VoucherManage> {
|
|||||||
void reprocessUploadedVoucher(Long voucherId);
|
void reprocessUploadedVoucher(Long voucherId);
|
||||||
void removeVoucher(Long id);
|
void removeVoucher(Long id);
|
||||||
IPage<Map<String, Object>> selectableWaybillBatches(IPage<?> page, String batchNo, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd);
|
IPage<Map<String, Object>> selectableWaybillBatches(IPage<?> page, String batchNo, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd);
|
||||||
|
void auditPass(Long id);
|
||||||
|
void auditReject(Long id, String rejectReason);
|
||||||
}
|
}
|
||||||
|
|||||||
+233
-50
@@ -59,11 +59,14 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.TreeMap;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 运输计划 服务实现类
|
* 运输计划 服务实现类
|
||||||
@@ -207,40 +210,56 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
|||||||
throw new ServiceException("客户合同不能为空");
|
throw new ServiceException("客户合同不能为空");
|
||||||
}
|
}
|
||||||
TransportBusinessSupport.validateRequired(contractName, "客户合同不能为空");
|
TransportBusinessSupport.validateRequired(contractName, "客户合同不能为空");
|
||||||
List<TransportPlanImportExcel> errorList = new ArrayList<>();
|
|
||||||
|
// 第一阶段:全部校验
|
||||||
|
Map<Integer, TransportPlanImportExcel> errorMap = new TreeMap<>();
|
||||||
|
Map<String, Integer> planNameCountMap = buildImportPlanNameCountMap(data);
|
||||||
|
Map<String, List<Integer>> planGroupMap = buildImportPlanGroupMap(data);
|
||||||
|
Long currentDeptId = TransportBusinessSupport.currentDept("运输计划").getId();
|
||||||
|
|
||||||
|
List<TransportPlan> importPlans = new ArrayList<>();
|
||||||
for (int index = 0; index < data.size(); index++) {
|
for (int index = 0; index < data.size(); index++) {
|
||||||
TransportPlanImportExcel excel = data.get(index);
|
TransportPlanImportExcel excel = data.get(index);
|
||||||
try {
|
try {
|
||||||
LocalDate planStartDate = parseImportDate(excel.getPlanStartDate(), "计划开始日期");
|
LocalDate planStartDate = Func.isNotEmpty(excel.getPlanStartDate())
|
||||||
LocalDate planEndDate = parseImportDate(excel.getPlanEndDate(), "计划结束日期");
|
? parseImportDate(excel.getPlanStartDate(), "计划开始时间") : null;
|
||||||
validateImportExcel(excel, planStartDate, planEndDate);
|
LocalDate planEndDate = Func.isNotEmpty(excel.getPlanEndDate())
|
||||||
TransportPlan transportPlan = new TransportPlan();
|
? parseImportDate(excel.getPlanEndDate(), "计划结束时间") : null;
|
||||||
transportPlan.setProjectId(projectId);
|
|
||||||
transportPlan.setProjectName(projectName);
|
List<String> validationErrors = validateImportExcel(excel, planStartDate, planEndDate, planNameCountMap, planGroupMap, currentDeptId);
|
||||||
transportPlan.setContractId(contractId);
|
if (Func.isNotEmpty(validationErrors)) {
|
||||||
transportPlan.setContractName(contractName);
|
excel.setErrorMessage(formatImportErrorMessage(validationErrors));
|
||||||
transportPlan.setCustomerName(customerName);
|
errorMap.put(index, excel);
|
||||||
transportPlan.setPlanName(excel.getPlanName());
|
continue;
|
||||||
transportPlan.setTransportType(excel.getTransportType());
|
}
|
||||||
transportPlan.setPlanStartDate(planStartDate);
|
|
||||||
transportPlan.setPlanEndDate(planEndDate);
|
TransportPlan transportPlan = buildImportTransportPlan(excel, projectId, projectName, contractId, contractName, customerName, planStartDate, planEndDate);
|
||||||
transportPlan.setDepartureAddress(excel.getDepartureAddress());
|
importPlans.add(transportPlan);
|
||||||
transportPlan.setDepartureContact(excel.getDepartureContact());
|
|
||||||
transportPlan.setDeparturePhone(excel.getDeparturePhone());
|
|
||||||
transportPlan.setArrivalAddress(excel.getArrivalAddress());
|
|
||||||
transportPlan.setArrivalContact(excel.getArrivalContact());
|
|
||||||
transportPlan.setArrivalPhone(excel.getArrivalPhone());
|
|
||||||
transportPlan.setRemark(excel.getRemark());
|
|
||||||
transportPlan.setGoodsJson(JsonUtil.toJson(List.of(importGoods(excel))));
|
|
||||||
transportPlan.setDataSource("批量导入");
|
|
||||||
transportPlan.setBusinessStatus("waiting_dispatch");
|
|
||||||
submit(transportPlan);
|
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
excel.setErrorMessage(exception.getMessage());
|
String message = exception instanceof ServiceException ? exception.getMessage() : "数据解析失败";
|
||||||
errorList.add(excel);
|
excel.setErrorMessage(formatImportErrorMessage(List.of(message)));
|
||||||
|
errorMap.put(index, excel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errorList;
|
|
||||||
|
// 如果有校验错误,回滚事务
|
||||||
|
if (Func.isNotEmpty(errorMap)) {
|
||||||
|
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
||||||
|
return new ArrayList<>(errorMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二阶段:批量导入
|
||||||
|
for (TransportPlan transportPlan : importPlans) {
|
||||||
|
try {
|
||||||
|
prepare(transportPlan);
|
||||||
|
validate(transportPlan);
|
||||||
|
save(transportPlan);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new ServiceException("数据保存失败:" + exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private LinkedHashMap<String, Object> importGoods(TransportPlanImportExcel excel) {
|
private LinkedHashMap<String, Object> importGoods(TransportPlanImportExcel excel) {
|
||||||
@@ -250,36 +269,197 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
|||||||
goods.put("quantity", excel.getQuantity());
|
goods.put("quantity", excel.getQuantity());
|
||||||
goods.put("quantityUnit", excel.getQuantityUnit());
|
goods.put("quantityUnit", excel.getQuantityUnit());
|
||||||
goods.put("packageType", excel.getPackageType());
|
goods.put("packageType", excel.getPackageType());
|
||||||
goods.put("brand", excel.getBrand());
|
|
||||||
goods.put("specification", excel.getSpecification());
|
goods.put("specification", excel.getSpecification());
|
||||||
goods.put("model", excel.getModel());
|
goods.put("model", excel.getModel());
|
||||||
goods.put("materialCode", excel.getMaterialCode());
|
|
||||||
goods.put("deviceCode", excel.getDeviceCode());
|
|
||||||
return goods;
|
return goods;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateImportExcel(TransportPlanImportExcel excel, LocalDate planStartDate, LocalDate planEndDate) {
|
private Map<String, Integer> buildImportPlanNameCountMap(List<TransportPlanImportExcel> data) {
|
||||||
TransportBusinessSupport.validateRequired(excel.getPlanName(), "计划名称不能为空");
|
Map<String, Integer> countMap = new HashMap<>();
|
||||||
TransportBusinessSupport.validateRequired(excel.getTransportType(), "运输方式不能为空");
|
for (TransportPlanImportExcel excel : data) {
|
||||||
TransportBusinessSupport.validateRequired(excel.getCargoType(), "货物类型不能为空");
|
String planName = trimToEmpty(excel.getPlanName());
|
||||||
TransportBusinessSupport.validateRequired(excel.getDepartureAddress(), "发货地址不能为空");
|
if (Func.isNotEmpty(planName)) {
|
||||||
TransportBusinessSupport.validateRequired(excel.getArrivalAddress(), "收货地址不能为空");
|
countMap.merge(planName, 1, Integer::sum);
|
||||||
validateImportLength(excel.getPlanName(), 50, "计划名称");
|
}
|
||||||
validateImportLength(excel.getDepartureAddress(), 255, "发货地址");
|
}
|
||||||
validateImportLength(excel.getArrivalAddress(), 255, "收货地址");
|
return countMap;
|
||||||
validateImportLength(excel.getDepartureContact(), 50, "发货联系人");
|
}
|
||||||
validateImportLength(excel.getArrivalContact(), 50, "收货联系人");
|
|
||||||
validateImportLength(excel.getDeparturePhone(), 50, "发货联系方式");
|
private Map<String, List<Integer>> buildImportPlanGroupMap(List<TransportPlanImportExcel> data) {
|
||||||
validateImportLength(excel.getArrivalPhone(), 50, "收货联系方式");
|
Map<String, List<Integer>> groupMap = new HashMap<>();
|
||||||
validateImportLength(excel.getRemark(), 200, "备注");
|
for (int index = 0; index < data.size(); index++) {
|
||||||
TransportBusinessSupport.validatePhone(excel.getDeparturePhone(), "发货联系方式格式不正确");
|
TransportPlanImportExcel excel = data.get(index);
|
||||||
TransportBusinessSupport.validatePhone(excel.getArrivalPhone(), "收货联系方式格式不正确");
|
String planGroupId = trimToEmpty(excel.getPlanGroupId());
|
||||||
TransportBusinessSupport.validateNonNegative(excel.getQuantity(), "数量");
|
if (Func.isNotEmpty(planGroupId)) {
|
||||||
TransportBusinessSupport.validateDateRange(planStartDate, planEndDate, "计划结束日期不能早于计划开始日期");
|
groupMap.computeIfAbsent(planGroupId, k -> new ArrayList<>()).add(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groupMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransportPlan buildImportTransportPlan(TransportPlanImportExcel excel, Long projectId, String projectName,
|
||||||
|
Long contractId, String contractName, String customerName, LocalDate planStartDate, LocalDate planEndDate) {
|
||||||
|
TransportPlan transportPlan = new TransportPlan();
|
||||||
|
transportPlan.setProjectId(projectId);
|
||||||
|
transportPlan.setProjectName(projectName);
|
||||||
|
transportPlan.setContractId(contractId);
|
||||||
|
transportPlan.setContractName(contractName);
|
||||||
|
transportPlan.setCustomerName(customerName);
|
||||||
|
transportPlan.setPlanName(excel.getPlanName());
|
||||||
|
transportPlan.setTransportType(excel.getTransportType());
|
||||||
|
transportPlan.setPlanStartDate(planStartDate);
|
||||||
|
transportPlan.setPlanEndDate(planEndDate);
|
||||||
|
transportPlan.setDepartureAddress(excel.getDepartureAddress());
|
||||||
|
transportPlan.setDepartureContact(excel.getDepartureContact());
|
||||||
|
transportPlan.setDeparturePhone(excel.getDeparturePhone());
|
||||||
|
transportPlan.setArrivalAddress(excel.getArrivalAddress());
|
||||||
|
transportPlan.setArrivalContact(excel.getArrivalContact());
|
||||||
|
transportPlan.setArrivalPhone(excel.getArrivalPhone());
|
||||||
|
transportPlan.setMileage(excel.getMileage());
|
||||||
|
transportPlan.setPlanGroupId(excel.getPlanGroupId());
|
||||||
|
transportPlan.setRemark(excel.getRemark());
|
||||||
|
transportPlan.setGoodsJson(JsonUtil.toJson(List.of(importGoods(excel))));
|
||||||
|
transportPlan.setDataSource("批量导入");
|
||||||
|
transportPlan.setBusinessStatus("waiting_dispatch");
|
||||||
|
return transportPlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatImportErrorMessage(List<String> validationErrors) {
|
||||||
|
if (Func.isEmpty(validationErrors)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
StringBuilder errorMessage = new StringBuilder();
|
||||||
|
for (int index = 0; index < validationErrors.size(); index++) {
|
||||||
|
if (index > 0) {
|
||||||
|
errorMessage.append(System.lineSeparator());
|
||||||
|
}
|
||||||
|
errorMessage.append(index + 1).append(". ").append(validationErrors.get(index));
|
||||||
|
}
|
||||||
|
return errorMessage.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimToEmpty(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> validateImportExcel(TransportPlanImportExcel excel, LocalDate planStartDate, LocalDate planEndDate,
|
||||||
|
Map<String, Integer> planNameCountMap, Map<String, List<Integer>> planGroupMap, Long currentDeptId) {
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
|
||||||
|
// 1. 校验必填字段
|
||||||
|
if (Func.isEmpty(trimToEmpty(excel.getPlanName()))) {
|
||||||
|
errors.add("计划名称不能为空");
|
||||||
|
}
|
||||||
|
if (Func.isEmpty(trimToEmpty(excel.getTransportType()))) {
|
||||||
|
errors.add("运输类型不能为空");
|
||||||
|
}
|
||||||
|
if (Func.isEmpty(trimToEmpty(excel.getDepartureAddress()))) {
|
||||||
|
errors.add("发货地址不能为空");
|
||||||
|
}
|
||||||
|
if (Func.isEmpty(trimToEmpty(excel.getArrivalAddress()))) {
|
||||||
|
errors.add("到货地址不能为空");
|
||||||
|
}
|
||||||
|
if (Func.isEmpty(trimToEmpty(excel.getCargoType()))) {
|
||||||
|
errors.add("货物类型不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 校验运输类型枚举值
|
||||||
|
String transportType = trimToEmpty(excel.getTransportType());
|
||||||
|
if (Func.isNotEmpty(transportType)) {
|
||||||
|
List<String> validTransportTypes = List.of("公路整车", "公路配载/零担", "铁路运输", "水路运输", "跨境海运", "航空运输");
|
||||||
|
if (!validTransportTypes.contains(transportType)) {
|
||||||
|
errors.add("运输类型需系统枚举值(公路整车、公路配载/零担、铁路运输、水路运输、跨境海运、航空运输)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 校验计划名称唯一性(当前组织下不重复)
|
||||||
|
String planName = trimToEmpty(excel.getPlanName());
|
||||||
|
if (Func.isNotEmpty(planName)) {
|
||||||
|
// 检查本次导入中的重复
|
||||||
|
if (planNameCountMap.getOrDefault(planName, 0) > 1) {
|
||||||
|
errors.add("计划名称在导入数据中重复");
|
||||||
|
}
|
||||||
|
// 检查数据库中的重复
|
||||||
|
long existingCount = count(Wrappers.<TransportPlan>lambdaQuery()
|
||||||
|
.eq(TransportPlan::getPlanName, planName)
|
||||||
|
.eq(TransportPlan::getDeptId, currentDeptId)
|
||||||
|
.eq(TransportPlan::getIsDeleted, 0));
|
||||||
|
if (existingCount > 0) {
|
||||||
|
errors.add("计划名称在当前组织下已存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 校验联系电话格式(11位数字)
|
||||||
|
String departurePhone = trimToEmpty(excel.getDeparturePhone());
|
||||||
|
if (Func.isNotEmpty(departurePhone)) {
|
||||||
|
if (!departurePhone.matches("^\\d{11}$")) {
|
||||||
|
errors.add("发货联系人电话格式不正确(需11位数字)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String arrivalPhone = trimToEmpty(excel.getArrivalPhone());
|
||||||
|
if (Func.isNotEmpty(arrivalPhone)) {
|
||||||
|
if (!arrivalPhone.matches("^\\d{11}$")) {
|
||||||
|
errors.add("收货联系人电话格式不正确(需11位数字)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 校验数量和里程为正数
|
||||||
|
if (excel.getQuantity() != null && excel.getQuantity().compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
errors.add("数量必须为正数");
|
||||||
|
}
|
||||||
|
if (excel.getMileage() != null && excel.getMileage().compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
errors.add("里程必须为正数");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 校验计量单位(如果有值)
|
||||||
|
String quantityUnit = trimToEmpty(excel.getQuantityUnit());
|
||||||
|
if (Func.isNotEmpty(quantityUnit)) {
|
||||||
|
// 这里应该查询字典表,暂时使用常见的单位列表
|
||||||
|
List<String> validUnits = List.of("吨", "千克", "立方米", "件", "车", "箱", "托盘", "个", "套", "台");
|
||||||
|
if (!validUnits.contains(quantityUnit)) {
|
||||||
|
errors.add("计量单位不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 校验时间格式和逻辑
|
||||||
|
if (planStartDate != null && planEndDate != null) {
|
||||||
|
if (planEndDate.isBefore(planStartDate)) {
|
||||||
|
errors.add("计划结束时间不得早于计划开始时间");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 校验同一计划标识号的一致性
|
||||||
|
String planGroupId = trimToEmpty(excel.getPlanGroupId());
|
||||||
|
if (Func.isNotEmpty(planGroupId)) {
|
||||||
|
List<Integer> groupIndexes = planGroupMap.get(planGroupId);
|
||||||
|
if (groupIndexes != null && groupIndexes.size() > 1) {
|
||||||
|
// 校验同一标识号下的所有记录的关键字段是否一致
|
||||||
|
// 这里简化处理,实际应该在所有数据收集后再校验
|
||||||
|
errors.add("注意:同一计划标识号应保持计划名称、运输类型、发货地址、到货地址一致");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 校验字段长度
|
||||||
|
if (Func.isNotEmpty(planName) && planName.length() > 255) {
|
||||||
|
errors.add("计划名称不能超过255个字符");
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(excel.getDepartureAddress()) && excel.getDepartureAddress().length() > 255) {
|
||||||
|
errors.add("发货地址不能超过255个字符");
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(excel.getArrivalAddress()) && excel.getArrivalAddress().length() > 255) {
|
||||||
|
errors.add("到货地址不能超过255个字符");
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(excel.getRemark()) && excel.getRemark().length() > 500) {
|
||||||
|
errors.add("备注不能超过500个字符");
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
private LocalDate parseImportDate(String value, String fieldName) {
|
private LocalDate parseImportDate(String value, String fieldName) {
|
||||||
TransportBusinessSupport.validateRequired(value, fieldName + "不能为空");
|
if (Func.isEmpty(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return LocalDate.parse(value.trim(), DateTimeFormatter.ISO_LOCAL_DATE);
|
return LocalDate.parse(value.trim(), DateTimeFormatter.ISO_LOCAL_DATE);
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
@@ -511,6 +691,7 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
|||||||
transportPlan.setDataSource(TransportBusinessSupport.normalizeTransportPlanDataSource(transportPlan.getDataSource()));
|
transportPlan.setDataSource(TransportBusinessSupport.normalizeTransportPlanDataSource(transportPlan.getDataSource()));
|
||||||
transportPlan.setBusinessStatus(TransportBusinessSupport.trimToNull(transportPlan.getBusinessStatus()));
|
transportPlan.setBusinessStatus(TransportBusinessSupport.trimToNull(transportPlan.getBusinessStatus()));
|
||||||
transportPlan.setDeptName(TransportBusinessSupport.trimToNull(transportPlan.getDeptName()));
|
transportPlan.setDeptName(TransportBusinessSupport.trimToNull(transportPlan.getDeptName()));
|
||||||
|
transportPlan.setPlanGroupId(TransportBusinessSupport.trimToNull(transportPlan.getPlanGroupId()));
|
||||||
transportPlan.setRemark(TransportBusinessSupport.trimToNull(transportPlan.getRemark()));
|
transportPlan.setRemark(TransportBusinessSupport.trimToNull(transportPlan.getRemark()));
|
||||||
if (Func.isEmpty(transportPlan.getDeptId())) {
|
if (Func.isEmpty(transportPlan.getDeptId())) {
|
||||||
Dept dept = TransportBusinessSupport.currentDept("运输计划");
|
Dept dept = TransportBusinessSupport.currentDept("运输计划");
|
||||||
@@ -548,7 +729,9 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
|||||||
TransportBusinessSupport.validateLength(transportPlan.getDataSource(), 255, "数据来源不能超过255字");
|
TransportBusinessSupport.validateLength(transportPlan.getDataSource(), 255, "数据来源不能超过255字");
|
||||||
TransportBusinessSupport.validateLength(transportPlan.getBusinessStatus(), 255, "业务状态不能超过255字");
|
TransportBusinessSupport.validateLength(transportPlan.getBusinessStatus(), 255, "业务状态不能超过255字");
|
||||||
TransportBusinessSupport.validateLength(transportPlan.getDeptName(), 255, "所属组织不能超过255字");
|
TransportBusinessSupport.validateLength(transportPlan.getDeptName(), 255, "所属组织不能超过255字");
|
||||||
|
TransportBusinessSupport.validateLength(transportPlan.getPlanGroupId(), 255, "同一计划标识号不能超过255字");
|
||||||
TransportBusinessSupport.validateLength(transportPlan.getRemark(), 500, "备注不能超过500字");
|
TransportBusinessSupport.validateLength(transportPlan.getRemark(), 500, "备注不能超过500字");
|
||||||
|
TransportBusinessSupport.validateNonNegative(transportPlan.getMileage(), "里程");
|
||||||
TransportBusinessSupport.validateDateRange(transportPlan.getPlanStartDate(), transportPlan.getPlanEndDate(), "计划开始日期不能晚于计划结束日期");
|
TransportBusinessSupport.validateDateRange(transportPlan.getPlanStartDate(), transportPlan.getPlanEndDate(), "计划开始日期不能晚于计划结束日期");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+158
-29
@@ -55,17 +55,13 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.text.Collator;
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.format.DateTimeParseException;
|
import java.time.format.DateTimeParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -399,6 +395,124 @@ public class TransportReconciliationServiceImpl
|
|||||||
return validationErrors;
|
return validationErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransportReconciliationVO matchPreview(TransportReconciliationVO request) {
|
||||||
|
if (request == null || request.getId() == null) throw new ServiceException("运输对账单不存在");
|
||||||
|
editable(request.getId());
|
||||||
|
List<TransportReconciliationInternal> internals = request.getInternalDetails();
|
||||||
|
List<TransportReconciliationExternal> externals = request.getExternalDetails();
|
||||||
|
if (internals == null) internals = internalRows(request.getId());
|
||||||
|
if (externals == null) externals = externalRows(request.getId());
|
||||||
|
if (externals.isEmpty()) throw new ServiceException("请先导入外部账单");
|
||||||
|
resetPreviewMatches(internals, externals);
|
||||||
|
Map<String, List<TransportReconciliationExternal>> externalGroups = externals.stream()
|
||||||
|
.collect(Collectors.groupingBy(this::matchKey));
|
||||||
|
Map<String, List<TransportReconciliationInternal>> internalGroups = internals.stream()
|
||||||
|
.collect(Collectors.groupingBy(this::matchKey));
|
||||||
|
for (Map.Entry<String, List<TransportReconciliationExternal>> entry : externalGroups.entrySet()) {
|
||||||
|
List<TransportReconciliationExternal> externalGroup = entry.getValue();
|
||||||
|
List<TransportReconciliationInternal> internalGroup = internalGroups.getOrDefault(entry.getKey(), List.of());
|
||||||
|
if (externalGroup.size() == 1 && internalGroup.size() == 1) {
|
||||||
|
linkPreview(internalGroup.get(0), externalGroup.get(0));
|
||||||
|
} else if (externalGroup.size() > 1) {
|
||||||
|
for (TransportReconciliationExternal external : externalGroup) {
|
||||||
|
external.setSuspectedDuplicate(true);
|
||||||
|
external.setMatchStatus(DUPLICATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TransportReconciliationVO result = detail(request.getId());
|
||||||
|
result.setInternalDetails(internals);
|
||||||
|
result.setExternalDetails(externals);
|
||||||
|
refreshPreviewStats(result, internals, externals);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public TransportReconciliationVO completeWithData(TransportReconciliationVO request) {
|
||||||
|
if (request == null || request.getId() == null) throw new ServiceException("运输对账单不存在");
|
||||||
|
TransportReconciliation bill = editable(request.getId());
|
||||||
|
applySnapshots(bill, request.getInternalDetails(), request.getExternalDetails());
|
||||||
|
if (request.getReconciliationDate() != null) bill.setReconciliationDate(request.getReconciliationDate());
|
||||||
|
bill.setRemark(limit(request.getRemark(), 200));
|
||||||
|
updateById(bill);
|
||||||
|
refreshStats(bill.getId());
|
||||||
|
complete(bill.getId());
|
||||||
|
return detail(bill.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySnapshots(TransportReconciliation bill,
|
||||||
|
List<TransportReconciliationInternal> incomingInternals,
|
||||||
|
List<TransportReconciliationExternal> incomingExternals) {
|
||||||
|
if (incomingInternals == null || incomingExternals == null) {
|
||||||
|
throw new ServiceException("对账明细不能为空");
|
||||||
|
}
|
||||||
|
Map<Long, TransportReconciliationInternal> internalMap = internalRows(bill.getId()).stream()
|
||||||
|
.collect(Collectors.toMap(TransportReconciliationInternal::getId, Function.identity()));
|
||||||
|
Map<Long, TransportReconciliationExternal> externalMap = externalRows(bill.getId()).stream()
|
||||||
|
.collect(Collectors.toMap(TransportReconciliationExternal::getId, Function.identity()));
|
||||||
|
if (incomingInternals.size() != internalMap.size() || incomingExternals.size() != externalMap.size()) {
|
||||||
|
throw new ServiceException("对账明细已发生变化,请刷新后重试");
|
||||||
|
}
|
||||||
|
for (TransportReconciliationInternal incoming : incomingInternals) {
|
||||||
|
TransportReconciliationInternal existing = internalMap.get(incoming.getId());
|
||||||
|
if (existing == null) throw new ServiceException("内部账单明细不存在");
|
||||||
|
applyInternalSnapshot(existing, incoming);
|
||||||
|
internalMapper.updateById(existing);
|
||||||
|
}
|
||||||
|
for (TransportReconciliationExternal incoming : incomingExternals) {
|
||||||
|
TransportReconciliationExternal existing = externalMap.get(incoming.getId());
|
||||||
|
if (existing == null) throw new ServiceException("外部账单明细不存在");
|
||||||
|
applyExternalSnapshot(existing, incoming);
|
||||||
|
externalMapper.updateById(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyInternalSnapshot(TransportReconciliationInternal target, TransportReconciliationInternal source) {
|
||||||
|
if (source.getReconciliationId() != null && !Objects.equals(source.getReconciliationId(), target.getReconciliationId())) {
|
||||||
|
throw new ServiceException("内部账单明细不属于当前对账单");
|
||||||
|
}
|
||||||
|
target.setTransportQuantity(nonNegative(source.getTransportQuantity(), "运输量"));
|
||||||
|
target.setUnitPrice(nonNegative(source.getUnitPrice(), "运输单价"));
|
||||||
|
target.setMileage(source.getMileage());
|
||||||
|
target.setFreightAmount(source.getFreightAmount() == null ? null : nonNegative(source.getFreightAmount(), "运输费"));
|
||||||
|
target.setFeeItemsJson(source.getFeeItemsJson());
|
||||||
|
target.setSettlementAmount(nonNegative(source.getSettlementAmount(), "结算金额"));
|
||||||
|
target.setMatchedExternalId(source.getMatchedExternalId());
|
||||||
|
target.setMatchedExternalLineNo(source.getMatchedExternalLineNo());
|
||||||
|
target.setMatchResult(source.getMatchResult());
|
||||||
|
target.setUpdateResult(source.getUpdateResult());
|
||||||
|
target.setUpdateMessage(limit(source.getUpdateMessage(), 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyExternalSnapshot(TransportReconciliationExternal target, TransportReconciliationExternal source) {
|
||||||
|
if (source.getReconciliationId() != null && !Objects.equals(source.getReconciliationId(), target.getReconciliationId())) {
|
||||||
|
throw new ServiceException("外部账单明细不属于当前对账单");
|
||||||
|
}
|
||||||
|
target.setVehicleNo(source.getVehicleNo());
|
||||||
|
target.setDepartureAddress(source.getDepartureAddress());
|
||||||
|
target.setArrivalAddress(source.getArrivalAddress());
|
||||||
|
target.setActualDepartureTime(source.getActualDepartureTime());
|
||||||
|
target.setActualCompletionTime(source.getActualCompletionTime());
|
||||||
|
target.setTransportType(source.getTransportType());
|
||||||
|
target.setCargoName(source.getCargoName());
|
||||||
|
target.setCargoType(source.getCargoType());
|
||||||
|
target.setSpecification(source.getSpecification());
|
||||||
|
target.setModel(source.getModel());
|
||||||
|
target.setTransportQuantity(nonNegative(source.getTransportQuantity(), "运输量"));
|
||||||
|
target.setQuantityUnit(source.getQuantityUnit());
|
||||||
|
target.setMileage(source.getMileage());
|
||||||
|
target.setBatchNo(source.getBatchNo());
|
||||||
|
target.setUnitPrice(source.getUnitPrice() == null ? null : nonNegative(source.getUnitPrice(), "运输单价"));
|
||||||
|
target.setFreightAmount(source.getFreightAmount() == null ? null : nonNegative(source.getFreightAmount(), "运输费"));
|
||||||
|
target.setFeeItemsJson(source.getFeeItemsJson());
|
||||||
|
target.setSettlementAmount(nonNegative(source.getSettlementAmount(), "结算费用合计"));
|
||||||
|
target.setMatchedInternalId(source.getMatchedInternalId());
|
||||||
|
target.setMatchStatus(source.getMatchStatus());
|
||||||
|
target.setSuspectedDuplicate(Boolean.TRUE.equals(source.getSuspectedDuplicate()));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void autoMatch(Long id) {
|
public void autoMatch(Long id) {
|
||||||
@@ -407,16 +521,14 @@ public class TransportReconciliationServiceImpl
|
|||||||
List<TransportReconciliationExternal> externals = externalRows(id);
|
List<TransportReconciliationExternal> externals = externalRows(id);
|
||||||
if (externals.isEmpty()) throw new ServiceException("请先导入外部账单");
|
if (externals.isEmpty()) throw new ServiceException("请先导入外部账单");
|
||||||
resetMatches(internals, externals);
|
resetMatches(internals, externals);
|
||||||
Function<TransportReconciliationExternal, String> externalKey = CARGO.equals(bill.getReconciliationMode())
|
Map<String, List<TransportReconciliationExternal>> externalGroups = externals.stream()
|
||||||
? this::cargoKey : this::vehicleKey;
|
.collect(Collectors.groupingBy(this::matchKey));
|
||||||
Map<String, List<TransportReconciliationExternal>> externalGroups = externals.stream().collect(Collectors.groupingBy(externalKey));
|
Map<String, List<TransportReconciliationInternal>> internalGroups = internals.stream()
|
||||||
Map<String, List<TransportReconciliationInternal>> internalGroups = internals.stream().collect(Collectors.groupingBy(
|
.collect(Collectors.groupingBy(this::matchKey));
|
||||||
CARGO.equals(bill.getReconciliationMode()) ? this::cargoKey : this::vehicleKey));
|
|
||||||
for (Map.Entry<String, List<TransportReconciliationExternal>> entry : externalGroups.entrySet()) {
|
for (Map.Entry<String, List<TransportReconciliationExternal>> entry : externalGroups.entrySet()) {
|
||||||
List<TransportReconciliationExternal> externalGroup = entry.getValue();
|
List<TransportReconciliationExternal> externalGroup = entry.getValue();
|
||||||
List<TransportReconciliationInternal> internalGroup = internalGroups.getOrDefault(entry.getKey(), List.of());
|
List<TransportReconciliationInternal> internalGroup = internalGroups.getOrDefault(entry.getKey(), List.of());
|
||||||
if (externalGroup.size() == 1 && internalGroup.size() == 1
|
if (externalGroup.size() == 1 && internalGroup.size() == 1) {
|
||||||
&& equalMoney(externalGroup.get(0).getSettlementAmount(), internalGroup.get(0).getSettlementAmount())) {
|
|
||||||
link(internalGroup.get(0), externalGroup.get(0));
|
link(internalGroup.get(0), externalGroup.get(0));
|
||||||
} else if (externalGroup.size() > 1) {
|
} else if (externalGroup.size() > 1) {
|
||||||
for (TransportReconciliationExternal external : externalGroup) {
|
for (TransportReconciliationExternal external : externalGroup) {
|
||||||
@@ -664,6 +776,13 @@ public class TransportReconciliationServiceImpl
|
|||||||
TransportReconciliation bill = existing(id);
|
TransportReconciliation bill = existing(id);
|
||||||
List<TransportReconciliationInternal> internals = internalRows(id);
|
List<TransportReconciliationInternal> internals = internalRows(id);
|
||||||
List<TransportReconciliationExternal> externals = externalRows(id);
|
List<TransportReconciliationExternal> externals = externalRows(id);
|
||||||
|
refreshPreviewStats(bill, internals, externals);
|
||||||
|
updateById(bill);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshPreviewStats(TransportReconciliation bill,
|
||||||
|
List<TransportReconciliationInternal> internals,
|
||||||
|
List<TransportReconciliationExternal> externals) {
|
||||||
int matched = (int) internals.stream().filter(item -> MATCHED.equals(item.getMatchResult())).count();
|
int matched = (int) internals.stream().filter(item -> MATCHED.equals(item.getMatchResult())).count();
|
||||||
bill.setInternalBillCount(internals.size()); bill.setExternalBillCount(externals.size());
|
bill.setInternalBillCount(internals.size()); bill.setExternalBillCount(externals.size());
|
||||||
int internalUnmatched = (int) internals.stream().filter(item -> !MATCHED.equals(item.getMatchResult())).count();
|
int internalUnmatched = (int) internals.stream().filter(item -> !MATCHED.equals(item.getMatchResult())).count();
|
||||||
@@ -675,7 +794,6 @@ public class TransportReconciliationServiceImpl
|
|||||||
bill.setInternalAmount(sumInternalAmount(internals)); bill.setExternalAmount(sumExternalAmount(externals));
|
bill.setInternalAmount(sumInternalAmount(internals)); bill.setExternalAmount(sumExternalAmount(externals));
|
||||||
bill.setDifferenceAmount(bill.getInternalAmount().subtract(bill.getExternalAmount()).abs());
|
bill.setDifferenceAmount(bill.getInternalAmount().subtract(bill.getExternalAmount()).abs());
|
||||||
bill.setMatchStatus(internals.size() > 0 && internals.size() == externals.size() && matched == internals.size() ? MATCHED : matched > 0 ? "partial" : UNMATCHED);
|
bill.setMatchStatus(internals.size() > 0 && internals.size() == externals.size() && matched == internals.size() ? MATCHED : matched > 0 ? "partial" : UNMATCHED);
|
||||||
updateById(bill);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void link(TransportReconciliationInternal internal, TransportReconciliationExternal external) {
|
private void link(TransportReconciliationInternal internal, TransportReconciliationExternal external) {
|
||||||
@@ -696,6 +814,28 @@ public class TransportReconciliationServiceImpl
|
|||||||
for (TransportReconciliationExternal external : externals) { external.setMatchedInternalId(null); external.setMatchStatus(UNMATCHED); external.setSuspectedDuplicate(false); externalMapper.updateById(external); }
|
for (TransportReconciliationExternal external : externals) { external.setMatchedInternalId(null); external.setMatchStatus(UNMATCHED); external.setSuspectedDuplicate(false); externalMapper.updateById(external); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void resetPreviewMatches(List<TransportReconciliationInternal> internals, List<TransportReconciliationExternal> externals) {
|
||||||
|
for (TransportReconciliationInternal internal : internals) {
|
||||||
|
internal.setMatchedExternalId(null);
|
||||||
|
internal.setMatchedExternalLineNo(null);
|
||||||
|
internal.setMatchResult(UNMATCHED);
|
||||||
|
}
|
||||||
|
for (TransportReconciliationExternal external : externals) {
|
||||||
|
external.setMatchedInternalId(null);
|
||||||
|
external.setMatchStatus(UNMATCHED);
|
||||||
|
external.setSuspectedDuplicate(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void linkPreview(TransportReconciliationInternal internal, TransportReconciliationExternal external) {
|
||||||
|
internal.setMatchedExternalId(external.getId());
|
||||||
|
internal.setMatchedExternalLineNo(external.getExternalLineNo());
|
||||||
|
internal.setMatchResult(MATCHED);
|
||||||
|
external.setMatchedInternalId(internal.getId());
|
||||||
|
external.setMatchStatus(MATCHED);
|
||||||
|
external.setSuspectedDuplicate(false);
|
||||||
|
}
|
||||||
|
|
||||||
private void resetExternal(Long id) {
|
private void resetExternal(Long id) {
|
||||||
List<TransportReconciliationInternal> internals = internalRows(id);
|
List<TransportReconciliationInternal> internals = internalRows(id);
|
||||||
for (TransportReconciliationInternal internal : internals) { internal.setMatchedExternalId(null); internal.setMatchedExternalLineNo(null); internal.setMatchResult(UNMATCHED); internalMapper.updateById(internal); }
|
for (TransportReconciliationInternal internal : internals) { internal.setMatchedExternalId(null); internal.setMatchedExternalLineNo(null); internal.setMatchResult(UNMATCHED); internalMapper.updateById(internal); }
|
||||||
@@ -768,10 +908,12 @@ public class TransportReconciliationServiceImpl
|
|||||||
throw new ServiceException(field + "格式应为yyyy-MM-dd HH:mm:ss或yyyy-MM-dd");
|
throw new ServiceException(field + "格式应为yyyy-MM-dd HH:mm:ss或yyyy-MM-dd");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String vehicleKey(TransportReconciliationInternal row) { return key(row.getVehicleNo(), sortedCargoNames(row.getCargoName()), row.getActualDepartureTime(), row.getBatchNo(), row.getTransportQuantity()); }
|
private String matchKey(TransportReconciliationInternal row) {
|
||||||
private String vehicleKey(TransportReconciliationExternal row) { return key(row.getVehicleNo(), sortedCargoNames(row.getCargoName()), row.getActualDepartureTime(), row.getBatchNo(), row.getTransportQuantity()); }
|
return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(), row.getCargoName(), row.getCargoType());
|
||||||
private String cargoKey(TransportReconciliationInternal row) { return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(), row.getCargoName(), row.getActualDepartureTime(), row.getTransportQuantity()); }
|
}
|
||||||
private String cargoKey(TransportReconciliationExternal row) { return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(), row.getCargoName(), row.getActualDepartureTime(), row.getTransportQuantity()); }
|
private String matchKey(TransportReconciliationExternal row) {
|
||||||
|
return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(), row.getCargoName(), row.getCargoType());
|
||||||
|
}
|
||||||
private String key(Object... values) { StringBuilder builder = new StringBuilder(); for (Object value : values) builder.append(normal(value)).append('|'); return builder.toString(); }
|
private String key(Object... values) { StringBuilder builder = new StringBuilder(); for (Object value : values) builder.append(normal(value)).append('|'); return builder.toString(); }
|
||||||
private String normal(Object value) {
|
private String normal(Object value) {
|
||||||
if (value == null) return "";
|
if (value == null) return "";
|
||||||
@@ -779,23 +921,10 @@ public class TransportReconciliationServiceImpl
|
|||||||
if (value instanceof List<?> list) return JsonUtil.toJson(list);
|
if (value instanceof List<?> list) return JsonUtil.toJson(list);
|
||||||
return value.toString().trim().replaceAll("\\s+", "").toLowerCase();
|
return value.toString().trim().replaceAll("\\s+", "").toLowerCase();
|
||||||
}
|
}
|
||||||
private List<String> sortedCargoNames(String cargoName) {
|
|
||||||
if (Func.isEmpty(cargoName)) return List.of();
|
|
||||||
Collator collator = Collator.getInstance(Locale.CHINA);
|
|
||||||
return Arrays.stream(cargoName.split("、"))
|
|
||||||
.map(String::trim)
|
|
||||||
.filter(Func::isNotEmpty)
|
|
||||||
.sorted((left, right) -> {
|
|
||||||
int initialResult = collator.compare(left.substring(0, 1), right.substring(0, 1));
|
|
||||||
return initialResult == 0 ? collator.compare(left, right) : initialResult;
|
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
private String firstNotEmpty(String... values) {
|
private String firstNotEmpty(String... values) {
|
||||||
for (String value : values) if (Func.isNotEmpty(value)) return value;
|
for (String value : values) if (Func.isNotEmpty(value)) return value;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
private boolean equalMoney(BigDecimal left, BigDecimal right) { return money(left).compareTo(money(right)) == 0; }
|
|
||||||
private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; }
|
private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; }
|
||||||
private BigDecimal nonNegative(BigDecimal value, String field) { if (value == null || value.compareTo(BigDecimal.ZERO) < 0) throw new ServiceException(field + "不能小于0"); return value; }
|
private BigDecimal nonNegative(BigDecimal value, String field) { if (value == null || value.compareTo(BigDecimal.ZERO) < 0) throw new ServiceException(field + "不能小于0"); return value; }
|
||||||
private String limit(String value, int max) { if (value != null && value.length() > max) throw new ServiceException("备注不能超过" + max + "个字"); return value; }
|
private String limit(String value, int max) { if (value != null && value.length() > max) throw new ServiceException("备注不能超过" + max + "个字"); return value; }
|
||||||
|
|||||||
+456
-8
@@ -2,6 +2,7 @@ package org.springblade.transport.service.impl;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import org.springblade.core.log.exception.ServiceException;
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||||
@@ -24,6 +25,7 @@ import org.springblade.transport.pojo.entity.Waybill;
|
|||||||
import org.springblade.transport.pojo.entity.ProjectApply;
|
import org.springblade.transport.pojo.entity.ProjectApply;
|
||||||
import org.springblade.transport.pojo.vo.VoucherManageVO;
|
import org.springblade.transport.pojo.vo.VoucherManageVO;
|
||||||
import org.springblade.transport.pojo.vo.VoucherFileVO;
|
import org.springblade.transport.pojo.vo.VoucherFileVO;
|
||||||
|
import org.springblade.transport.pojo.vo.VoucherFolderVO;
|
||||||
import org.springblade.transport.service.IVoucherManageService;
|
import org.springblade.transport.service.IVoucherManageService;
|
||||||
import org.springblade.transport.service.IProjectApplyService;
|
import org.springblade.transport.service.IProjectApplyService;
|
||||||
import org.springblade.transport.service.IWaybillService;
|
import org.springblade.transport.service.IWaybillService;
|
||||||
@@ -35,6 +37,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import io.minio.MinioClient;
|
import io.minio.MinioClient;
|
||||||
import io.minio.GetPresignedObjectUrlArgs;
|
import io.minio.GetPresignedObjectUrlArgs;
|
||||||
import io.minio.PutObjectArgs;
|
import io.minio.PutObjectArgs;
|
||||||
|
import io.minio.RemoveObjectArgs;
|
||||||
import io.minio.http.Method;
|
import io.minio.http.Method;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -45,6 +48,8 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
@@ -60,6 +65,7 @@ import java.util.HashSet;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -107,10 +113,326 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
.eq(VoucherFile::getVoucherId, record.getId())
|
.eq(VoucherFile::getVoucherId, record.getId())
|
||||||
.eq(VoucherFile::getIsDeleted, 0)
|
.eq(VoucherFile::getIsDeleted, 0)
|
||||||
.orderByAsc(VoucherFile::getEntryName));
|
.orderByAsc(VoucherFile::getEntryName));
|
||||||
result.setVoucherFiles(files.stream().map(this::toVoucherFileVO).toList());
|
if (files.isEmpty()) {
|
||||||
|
List<VoucherImage> images = voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
|
||||||
|
.eq(VoucherImage::getTenantId, record.getTenantId())
|
||||||
|
.eq(VoucherImage::getVoucherId, record.getId())
|
||||||
|
.eq(VoucherImage::getIsDeleted, 0)
|
||||||
|
.orderByAsc(VoucherImage::getImageName));
|
||||||
|
result.setVoucherFiles(images.stream().map(this::toVoucherFileVO).toList());
|
||||||
|
} else {
|
||||||
|
result.setVoucherFiles(files.stream().map(this::toVoucherFileVO).toList());
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IPage<VoucherFolderVO> folderPage(IPage<?> page, Long voucherId, String plateNo, Integer matched) {
|
||||||
|
VoucherManage voucher = getById(voucherId);
|
||||||
|
if (voucher == null || Objects.equals(voucher.getIsDeleted(), 1)) throw new ServiceException("凭证批次不存在");
|
||||||
|
List<VoucherFolderVO> folders = folderRecords(voucher, false).stream()
|
||||||
|
.filter(item -> Func.isEmpty(plateNo) || item.getPlateNo().contains(plateNo))
|
||||||
|
.filter(item -> matched == null || Objects.equals(item.getMatched(), matched))
|
||||||
|
.sorted(Comparator.comparing(VoucherFolderVO::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder())))
|
||||||
|
.toList();
|
||||||
|
long current = page.getCurrent();
|
||||||
|
long size = page.getSize();
|
||||||
|
int from = (int) Math.min((current - 1) * size, folders.size());
|
||||||
|
int to = (int) Math.min((long) from + size, folders.size());
|
||||||
|
Page<VoucherFolderVO> result = new Page<>(current, size, folders.size());
|
||||||
|
result.setRecords(folders.subList(from, to));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VoucherFolderVO folderDetail(Long voucherId, String plateNo) {
|
||||||
|
return folderRecords(getVoucher(voucherId), true).stream()
|
||||||
|
.filter(item -> Objects.equals(item.getPlateNo(), normalizePlateNo(plateNo)))
|
||||||
|
.findFirst().orElseThrow(() -> new ServiceException("车牌凭证不存在"));
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void replaceFolder(Long voucherId, String plateNo, MultipartFile uploadFile) {
|
||||||
|
VoucherManage voucher = getVoucher(voucherId);
|
||||||
|
if (uploadFile == null || uploadFile.isEmpty()) throw new ServiceException("请选择凭证文件");
|
||||||
|
if (uploadFile.getSize() > 50L * 1024 * 1024) throw new ServiceException("凭证文件大小不能超过50M");
|
||||||
|
String normalizedPlateNo = normalizePlateNo(plateNo);
|
||||||
|
if (Func.isEmpty(normalizedPlateNo)) throw new ServiceException("车牌号不能为空");
|
||||||
|
validateMinioConfig();
|
||||||
|
Waybill matchedWaybill = listRelatedWaybills(voucher).stream()
|
||||||
|
.filter(item -> waybillPlateNumbers(item).contains(normalizedPlateNo)).findFirst().orElse(null);
|
||||||
|
String fileName = safeFileName(uploadFile.getOriginalFilename());
|
||||||
|
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 (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))) {
|
||||||
|
throw new ServiceException("已关联运单的凭证不能重新上传");
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
try {
|
||||||
|
if (isZipFile) {
|
||||||
|
replaceFolderWithZip(voucher, normalizedPlateNo, matchedWaybill, uploadFile);
|
||||||
|
} else {
|
||||||
|
replaceFolderWithImage(voucher, normalizedPlateNo, matchedWaybill, uploadFile, fileName);
|
||||||
|
}
|
||||||
|
} catch (ServiceException exception) {
|
||||||
|
throw exception;
|
||||||
|
} catch (Exception exception) {
|
||||||
|
log.error("替换车牌凭证失败 voucherId={}, plateNo={}", voucherId, normalizedPlateNo, exception);
|
||||||
|
throw new ServiceException("凭证文件上传失败");
|
||||||
|
}
|
||||||
|
refreshVoucherCounts(voucher);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void removeFolder(Long voucherId, String plateNo) {
|
||||||
|
VoucherManage voucher = getVoucher(voucherId);
|
||||||
|
String normalizedPlateNo = normalizePlateNo(plateNo);
|
||||||
|
List<VoucherFile> files = voucherFileMapper.selectList(Wrappers.<VoucherFile>lambdaQuery().eq(VoucherFile::getTenantId, voucher.getTenantId()).eq(VoucherFile::getVoucherId, voucherId).eq(VoucherFile::getPlateNo, normalizedPlateNo).eq(VoucherFile::getIsDeleted, 0));
|
||||||
|
List<VoucherImage> images = voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery().eq(VoucherImage::getTenantId, voucher.getTenantId()).eq(VoucherImage::getVoucherId, voucherId).eq(VoucherImage::getPlateNo, normalizedPlateNo).eq(VoucherImage::getIsDeleted, 0));
|
||||||
|
files.forEach(item -> { voucherFileMapper.deleteById(item.getId()); deleteObjectQuietly(item.getObjectKey()); });
|
||||||
|
images.forEach(item -> { voucherImageMapper.deleteById(item.getId()); if (files.stream().noneMatch(file -> Objects.equals(file.getObjectKey(), item.getObjectKey()))) deleteObjectQuietly(item.getObjectKey()); });
|
||||||
|
if (files.isEmpty() && images.isEmpty()) throw new ServiceException("车牌凭证不存在");
|
||||||
|
refreshVoucherCounts(voucher);
|
||||||
|
}
|
||||||
|
|
||||||
|
private VoucherManage getVoucher(Long voucherId) {
|
||||||
|
VoucherManage voucher = getById(voucherId);
|
||||||
|
if (voucher == null || Objects.equals(voucher.getIsDeleted(), 1)) throw new ServiceException("凭证批次不存在");
|
||||||
|
return voucher;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<VoucherFolderVO> folderRecords(VoucherManage voucher, boolean includeFiles) {
|
||||||
|
List<VoucherFile> files = voucherFileMapper.selectList(Wrappers.<VoucherFile>lambdaQuery().eq(VoucherFile::getTenantId, voucher.getTenantId()).eq(VoucherFile::getVoucherId, voucher.getId()).eq(VoucherFile::getIsDeleted, 0));
|
||||||
|
Map<String, VoucherFolderVO> grouped = new LinkedHashMap<>();
|
||||||
|
if (!files.isEmpty()) {
|
||||||
|
files.forEach(file -> addFolderFile(grouped, voucher, toVoucherFileVO(file, includeFiles), file.getCreateTime(), file.getUpdateTime()));
|
||||||
|
} else {
|
||||||
|
voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery().eq(VoucherImage::getTenantId, voucher.getTenantId()).eq(VoucherImage::getVoucherId, voucher.getId()).eq(VoucherImage::getIsDeleted, 0))
|
||||||
|
.forEach(image -> addFolderFile(grouped, voucher, toVoucherFileVO(image, includeFiles), image.getCreateTime(), image.getUpdateTime()));
|
||||||
|
}
|
||||||
|
List<VoucherFolderVO> folders = new ArrayList<>(grouped.values());
|
||||||
|
folders.sort(Comparator.comparing(VoucherFolderVO::getPlateNo));
|
||||||
|
for (int index = 0; index < folders.size(); index++) {
|
||||||
|
folders.get(index).setVoucherNo(voucher.getVoucherBatchNo() + "-" + String.format("%04d", index + 1));
|
||||||
|
if (!includeFiles) folders.get(index).setFiles(null);
|
||||||
|
}
|
||||||
|
return folders;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = "未识别车牌";
|
||||||
|
VoucherFolderVO folder = grouped.computeIfAbsent(plate, key -> {
|
||||||
|
VoucherFolderVO value = new VoucherFolderVO(); value.setVoucherId(voucher.getId()); value.setVoucherBatchNo(voucher.getVoucherBatchNo()); value.setVoucherNo(voucher.getVoucherBatchNo()); value.setPlateNo(key); value.setFolderName(key); value.setFiles(new ArrayList<>()); value.setProcessStatus(voucher.getProcessStatus()); return value;
|
||||||
|
});
|
||||||
|
folder.getFiles().add(file); if (Objects.equals(file.getMatched(), 1)) { folder.setMatched(1); folder.setWaybillId(file.getWaybillId()); folder.setWaybillNo(file.getWaybillNo()); } else if (folder.getMatched() == null) folder.setMatched(0);
|
||||||
|
java.util.Date effectiveCreateTime = createTime == null ? voucher.getCreateTime() : createTime;
|
||||||
|
java.util.Date effectiveUpdateTime = updateTime == null ? voucher.getUpdateTime() : updateTime;
|
||||||
|
if (folder.getCreateTime() == null || (effectiveCreateTime != null && effectiveCreateTime.before(folder.getCreateTime()))) folder.setCreateTime(effectiveCreateTime);
|
||||||
|
if (folder.getUpdateTime() == null || (effectiveUpdateTime != null && effectiveUpdateTime.after(folder.getUpdateTime()))) folder.setUpdateTime(effectiveUpdateTime);
|
||||||
|
if (folder.getFileName() == null) folder.setFileName(file.getFileName());
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
int fileCount = 0;
|
||||||
|
int imageCount = 0;
|
||||||
|
try (InputStream source = zipFile.getInputStream();
|
||||||
|
ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(source), zipCharset)) {
|
||||||
|
ZipEntry entry;
|
||||||
|
while ((entry = zipInputStream.getNextEntry()) != null) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String entryName = entry.getName().replace('\\', '/');
|
||||||
|
if (entryName.equals("__MACOSX") || entryName.startsWith("__MACOSX/")) {
|
||||||
|
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 fileName = safeFileName(rawFileName);
|
||||||
|
if (Func.isEmpty(fileName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
fileCount++;
|
||||||
|
if (imageFile) {
|
||||||
|
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);
|
||||||
|
imageCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
log.warn("[单个上传] 压缩包使用 UTF-8 解析失败,尝试 GB18030 voucherId={}", voucher.getId());
|
||||||
|
zipCharset = Charset.forName("GB18030");
|
||||||
|
try (InputStream source = zipFile.getInputStream();
|
||||||
|
ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(source), zipCharset)) {
|
||||||
|
ZipEntry entry;
|
||||||
|
while ((entry = zipInputStream.getNextEntry()) != null) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String entryName = entry.getName().replace('\\', '/');
|
||||||
|
if (entryName.equals("__MACOSX") || entryName.startsWith("__MACOSX/")) {
|
||||||
|
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 fileName = safeFileName(rawFileName);
|
||||||
|
if (Func.isEmpty(fileName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
fileCount++;
|
||||||
|
if (imageFile) {
|
||||||
|
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);
|
||||||
|
imageCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fileCount == 0) {
|
||||||
|
throw new ServiceException("压缩包内没有找到有效的文件");
|
||||||
|
}
|
||||||
|
log.info("[单个上传] 解压完成 voucherId={}, plateNo={}, fileCount={}, imageCount={}", voucher.getId(), plateNo, fileCount, imageCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void replaceFolderWithImage(VoucherManage voucher, String plateNo, Waybill matchedWaybill, MultipartFile imageFile, String fileName) throws Exception {
|
||||||
|
String entryName = plateNo + "/" + System.currentTimeMillis() + "_" + fileName;
|
||||||
|
String objectKey = buildObjectKey(voucher.getId(), matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo()), plateNo, entryName);
|
||||||
|
try (InputStream inputStream = imageFile.getInputStream()) {
|
||||||
|
minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||||
|
.stream(inputStream, imageFile.getSize(), 10 * 1024 * 1024)
|
||||||
|
.contentType(Func.isEmpty(imageFile.getContentType()) ? contentType(fileName) : imageFile.getContentType()).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(imageFile.getSize());
|
||||||
|
file.setContentType(Func.isEmpty(imageFile.getContentType()) ? contentType(fileName) : imageFile.getContentType());
|
||||||
|
file.setFileType("image");
|
||||||
|
file.setMatched(matchedWaybill == null ? 0 : 1);
|
||||||
|
file.setTenantId(voucher.getTenantId());
|
||||||
|
voucherFileMapper.insert(file);
|
||||||
|
VoucherImage image = new VoucherImage();
|
||||||
|
image.setVoucherId(voucher.getId());
|
||||||
|
image.setVoucherBatchNo(voucher.getVoucherBatchNo());
|
||||||
|
image.setWaybillId(file.getWaybillId());
|
||||||
|
image.setWaybillNo(file.getWaybillNo());
|
||||||
|
image.setPlateNo(plateNo);
|
||||||
|
image.setImageName(fileName);
|
||||||
|
image.setObjectKey(objectKey);
|
||||||
|
image.setMatched(file.getMatched());
|
||||||
|
image.setTenantId(voucher.getTenantId());
|
||||||
|
voucherImageMapper.insert(image);
|
||||||
|
log.info("[单个上传] 图片上传完成 voucherId={}, plateNo={}, fileName={}", voucher.getId(), plateNo, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshVoucherCounts(VoucherManage voucher) {
|
||||||
|
List<VoucherImage> images = voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
|
||||||
|
.eq(VoucherImage::getTenantId, voucher.getTenantId()).eq(VoucherImage::getVoucherId, voucher.getId()).eq(VoucherImage::getIsDeleted, 0));
|
||||||
|
long relatedWaybillCount = images.stream().filter(item -> Objects.equals(item.getMatched(), 1))
|
||||||
|
.map(VoucherImage::getWaybillId).filter(Objects::nonNull).distinct().count();
|
||||||
|
int waybillCount = listRelatedWaybills(voucher).size();
|
||||||
|
VoucherManage update = new VoucherManage(); update.setId(voucher.getId()); update.setVoucherCount(images.size());
|
||||||
|
update.setRelatedWaybillCount((int) relatedWaybillCount); update.setUnRelatedWaybillCount(Math.max(waybillCount - (int) relatedWaybillCount, 0));
|
||||||
|
update.setProcessStatus("处理完成"); updateById(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteObjectQuietly(String objectKey) { try { minioClient.removeObject(RemoveObjectArgs.builder().bucket(minioBucketName).object(objectKey).build()); } catch (Exception exception) { log.warn("删除凭证对象失败 objectKey={}", objectKey, exception); } }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public VoucherManage createUploadDraft(VoucherUploadDraftRequest request) {
|
public VoucherManage createUploadDraft(VoucherUploadDraftRequest request) {
|
||||||
@@ -151,7 +473,19 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
voucher.setFileName(request.getFileName());
|
voucher.setFileName(request.getFileName());
|
||||||
voucher.setFileUrl(request.getFileUrl());
|
voucher.setFileUrl(request.getFileUrl());
|
||||||
voucher.setFileTaskId(request.getFileTaskId());
|
voucher.setFileTaskId(request.getFileTaskId());
|
||||||
voucher.setUploadSource("内部");
|
|
||||||
|
// 判断是否为承运商上传:如果用户组织的顶级组织是"外部组织",则为承运商
|
||||||
|
Long currentDeptId = Func.firstLong(AuthUtil.getDeptId());
|
||||||
|
if (currentDeptId != null && isExternalOrganization(currentDeptId)) {
|
||||||
|
voucher.setUploadSource("承运商");
|
||||||
|
voucher.setCarrierId(currentDeptId);
|
||||||
|
voucher.setCarrierName(getOrganizationName(currentDeptId));
|
||||||
|
} else {
|
||||||
|
voucher.setUploadSource("内部");
|
||||||
|
voucher.setCarrierId(null);
|
||||||
|
voucher.setCarrierName(null);
|
||||||
|
}
|
||||||
|
|
||||||
voucher.setProcessStatus("上传中");
|
voucher.setProcessStatus("上传中");
|
||||||
voucher.setAuditStatus("-");
|
voucher.setAuditStatus("-");
|
||||||
voucher.setVoucherCount(0);
|
voucher.setVoucherCount(0);
|
||||||
@@ -338,6 +672,8 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
update.setRelatedWaybillCount(relatedWaybillIds.size());
|
update.setRelatedWaybillCount(relatedWaybillIds.size());
|
||||||
update.setUnRelatedWaybillCount(Math.max(waybills.size() - relatedWaybillIds.size(), 0));
|
update.setUnRelatedWaybillCount(Math.max(waybills.size() - relatedWaybillIds.size(), 0));
|
||||||
update.setProcessStatus("处理完成");
|
update.setProcessStatus("处理完成");
|
||||||
|
// 处理完成后自动设置为待审核状态
|
||||||
|
update.setAuditStatus("待审核");
|
||||||
updateById(update);
|
updateById(update);
|
||||||
log.info("[凭证处理] 进度 100%:处理完成 voucherId={}, voucherBatchNo={}, fileCount={}, imageCount={}, relatedWaybillCount={}, unrelatedWaybillCount={}",
|
log.info("[凭证处理] 进度 100%:处理完成 voucherId={}, voucherBatchNo={}, fileCount={}, imageCount={}, relatedWaybillCount={}, unrelatedWaybillCount={}",
|
||||||
voucherId, voucher.getVoucherBatchNo(), fileCount, imageCount, relatedWaybillIds.size(), Math.max(waybills.size() - relatedWaybillIds.size(), 0));
|
voucherId, voucher.getVoucherBatchNo(), fileCount, imageCount, relatedWaybillIds.size(), Math.max(waybills.size() - relatedWaybillIds.size(), 0));
|
||||||
@@ -482,6 +818,10 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private VoucherFileVO toVoucherFileVO(VoucherFile file) {
|
private VoucherFileVO toVoucherFileVO(VoucherFile file) {
|
||||||
|
return toVoucherFileVO(file, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private VoucherFileVO toVoucherFileVO(VoucherFile file, boolean includeUrl) {
|
||||||
VoucherFileVO result = new VoucherFileVO();
|
VoucherFileVO result = new VoucherFileVO();
|
||||||
result.setId(file.getId());
|
result.setId(file.getId());
|
||||||
result.setVoucherId(file.getVoucherId());
|
result.setVoucherId(file.getVoucherId());
|
||||||
@@ -497,15 +837,47 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
result.setContentType(file.getContentType());
|
result.setContentType(file.getContentType());
|
||||||
result.setFileType(file.getFileType());
|
result.setFileType(file.getFileType());
|
||||||
result.setMatched(file.getMatched());
|
result.setMatched(file.getMatched());
|
||||||
try {
|
result.setCreateTime(file.getCreateTime());
|
||||||
result.setUrl(minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
result.setUpdateTime(file.getUpdateTime());
|
||||||
.method(Method.GET).bucket(minioBucketName).object(file.getObjectKey()).expiry(1, java.util.concurrent.TimeUnit.HOURS).build()));
|
if (includeUrl) result.setUrl(buildVoucherFileUrl(file.getId(), file.getObjectKey()));
|
||||||
} catch (Exception exception) {
|
|
||||||
log.warn("生成凭证文件预览地址失败 voucherFileId={}, objectKey={}", file.getId(), file.getObjectKey(), exception);
|
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private VoucherFileVO toVoucherFileVO(VoucherImage image) {
|
||||||
|
return toVoucherFileVO(image, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private VoucherFileVO toVoucherFileVO(VoucherImage image, boolean includeUrl) {
|
||||||
|
VoucherFileVO result = new VoucherFileVO();
|
||||||
|
result.setId(image.getId());
|
||||||
|
result.setVoucherId(image.getVoucherId());
|
||||||
|
result.setVoucherBatchNo(image.getVoucherBatchNo());
|
||||||
|
result.setWaybillId(image.getWaybillId());
|
||||||
|
result.setWaybillNo(image.getWaybillNo());
|
||||||
|
result.setPlateNo(image.getPlateNo());
|
||||||
|
result.setFolderName(image.getPlateNo());
|
||||||
|
result.setEntryName(image.getImageName());
|
||||||
|
result.setFileName(image.getImageName());
|
||||||
|
result.setObjectKey(image.getObjectKey());
|
||||||
|
result.setContentType(contentType(image.getImageName()));
|
||||||
|
result.setFileType("image");
|
||||||
|
result.setMatched(image.getMatched());
|
||||||
|
result.setCreateTime(image.getCreateTime());
|
||||||
|
result.setUpdateTime(image.getUpdateTime());
|
||||||
|
if (includeUrl) result.setUrl(buildVoucherFileUrl(image.getId(), image.getObjectKey()));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildVoucherFileUrl(Long fileId, String objectKey) {
|
||||||
|
try {
|
||||||
|
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||||
|
.method(Method.GET).bucket(minioBucketName).object(objectKey).expiry(1, java.util.concurrent.TimeUnit.HOURS).build());
|
||||||
|
} catch (Exception exception) {
|
||||||
|
log.warn("生成凭证文件预览地址失败 voucherFileId={}, objectKey={}", fileId, objectKey, exception);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String normalizePlateNo(String plateNo) {
|
private String normalizePlateNo(String plateNo) {
|
||||||
return plateNo == null ? null : plateNo.replaceAll("[\\s-]", "").toUpperCase();
|
return plateNo == null ? null : plateNo.replaceAll("[\\s-]", "").toUpperCase();
|
||||||
}
|
}
|
||||||
@@ -570,6 +942,14 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
.like(Func.isNotEmpty(query.getCarrierName()), VoucherManage::getCarrierName, query.getCarrierName());
|
.like(Func.isNotEmpty(query.getCarrierName()), VoucherManage::getCarrierName, query.getCarrierName());
|
||||||
if (query.getCreateTimeStart() != null) wrapper.ge(VoucherManage::getCreateTime, query.getCreateTimeStart());
|
if (query.getCreateTimeStart() != null) wrapper.ge(VoucherManage::getCreateTime, query.getCreateTimeStart());
|
||||||
if (query.getCreateTimeEnd() != null) wrapper.le(VoucherManage::getCreateTime, query.getCreateTimeEnd());
|
if (query.getCreateTimeEnd() != null) wrapper.le(VoucherManage::getCreateTime, query.getCreateTimeEnd());
|
||||||
|
|
||||||
|
// 权限控制:如果当前用户所属组织的顶级组织是"外部组织",则只能看到自己上传的数据
|
||||||
|
Long currentUserId = AuthUtil.getUserId();
|
||||||
|
Long currentDeptId = Func.firstLong(AuthUtil.getDeptId());
|
||||||
|
if (currentDeptId != null && isExternalOrganization(currentDeptId)) {
|
||||||
|
wrapper.eq(VoucherManage::getCreateUser, currentUserId);
|
||||||
|
}
|
||||||
|
|
||||||
return wrapper.orderByDesc(VoucherManage::getCreateTime);
|
return wrapper.orderByDesc(VoucherManage::getCreateTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,4 +958,72 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
Long maxSequence = baseMapper.selectMaxDailySequence(AuthUtil.getTenantId(), prefix);
|
Long maxSequence = baseMapper.selectMaxDailySequence(AuthUtil.getTenantId(), prefix);
|
||||||
return prefix + String.format("%03d", (maxSequence == null ? 0 : maxSequence) + 1);
|
return prefix + String.format("%03d", (maxSequence == null ? 0 : maxSequence) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void auditPass(Long id) {
|
||||||
|
VoucherManage voucher = getById(id);
|
||||||
|
if (voucher == null || Objects.equals(voucher.getIsDeleted(), 1)) {
|
||||||
|
throw new ServiceException("凭证批次不存在");
|
||||||
|
}
|
||||||
|
if (!"待审核".equals(voucher.getAuditStatus())) {
|
||||||
|
throw new ServiceException("仅待审核状态的凭证可以审核通过");
|
||||||
|
}
|
||||||
|
if (!"处理完成".equals(voucher.getProcessStatus())) {
|
||||||
|
throw new ServiceException("仅处理完成的凭证可以审核");
|
||||||
|
}
|
||||||
|
voucher.setAuditStatus("审核通过");
|
||||||
|
updateById(voucher);
|
||||||
|
log.info("凭证批次审核通过 id={}, voucherBatchNo={}, operator={}", id, voucher.getVoucherBatchNo(), AuthUtil.getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void auditReject(Long id, String rejectReason) {
|
||||||
|
VoucherManage voucher = getById(id);
|
||||||
|
if (voucher == null || Objects.equals(voucher.getIsDeleted(), 1)) {
|
||||||
|
throw new ServiceException("凭证批次不存在");
|
||||||
|
}
|
||||||
|
if (!"待审核".equals(voucher.getAuditStatus())) {
|
||||||
|
throw new ServiceException("仅待审核状态的凭证可以审核驳回");
|
||||||
|
}
|
||||||
|
if (!"处理完成".equals(voucher.getProcessStatus())) {
|
||||||
|
throw new ServiceException("仅处理完成的凭证可以审核");
|
||||||
|
}
|
||||||
|
voucher.setAuditStatus("审核驳回");
|
||||||
|
updateById(voucher);
|
||||||
|
log.info("凭证批次审核驳回 id={}, voucherBatchNo={}, operator={}, reason={}",
|
||||||
|
id, voucher.getVoucherBatchNo(), AuthUtil.getUserId(), rejectReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为外部组织:递归查找顶级组织,判断是否为"外部组织"
|
||||||
|
*/
|
||||||
|
private boolean isExternalOrganization(Long deptId) {
|
||||||
|
if (deptId == null) return false;
|
||||||
|
try {
|
||||||
|
// 查询组织的顶级父组织
|
||||||
|
Long topDeptId = baseMapper.selectTopDeptId(deptId);
|
||||||
|
if (topDeptId == null) topDeptId = deptId;
|
||||||
|
// 查询顶级组织名称
|
||||||
|
String topDeptName = baseMapper.selectDeptName(topDeptId);
|
||||||
|
return "外部组织".equals(topDeptName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("判断外部组织失败 deptId={}", deptId, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取组织名称
|
||||||
|
*/
|
||||||
|
private String getOrganizationName(Long deptId) {
|
||||||
|
if (deptId == null) return null;
|
||||||
|
try {
|
||||||
|
return baseMapper.selectDeptName(deptId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取组织名称失败 deptId={}", deptId, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+391
-13
@@ -32,15 +32,19 @@ import org.springblade.transport.service.IWaybillService;
|
|||||||
import org.springblade.transport.support.TransportBusinessSupport;
|
import org.springblade.transport.support.TransportBusinessSupport;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.transaction.interceptor.TransactionAspectSupport;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.format.DateTimeParseException;
|
import java.time.format.DateTimeParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/** 运单批次服务实现。 */
|
/** 运单批次服务实现。 */
|
||||||
@@ -54,6 +58,11 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
|||||||
private static final String STATUS_COMPLETED = "completed";
|
private static final String STATUS_COMPLETED = "completed";
|
||||||
private static final String IMPORT_TYPE_WAYBILL = "waybill";
|
private static final String IMPORT_TYPE_WAYBILL = "waybill";
|
||||||
private static final String IMPORT_TYPE_SETTLEMENT = "settlement";
|
private static final String IMPORT_TYPE_SETTLEMENT = "settlement";
|
||||||
|
/** 车牌号校验正则:首位汉字,次位大写字母,总长度7或8位 */
|
||||||
|
private static final Pattern VEHICLE_NO_PATTERN = Pattern.compile("^[一-龥][A-Z][A-Z0-9]{5,6}$");
|
||||||
|
/** 手机号校验正则:11位数字 */
|
||||||
|
private static final Pattern PHONE_PATTERN = Pattern.compile("^\\d{11}$");
|
||||||
|
|
||||||
private final IWaybillService waybillService;
|
private final IWaybillService waybillService;
|
||||||
private final ICustomerArchiveService customerArchiveService;
|
private final ICustomerArchiveService customerArchiveService;
|
||||||
private final IProjectApplyService projectApplyService;
|
private final IProjectApplyService projectApplyService;
|
||||||
@@ -90,6 +99,21 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
|||||||
clearBatchWaybills(batch.getId());
|
clearBatchWaybills(batch.getId());
|
||||||
|
|
||||||
List<Map<String, Object>> rows = Func.isEmpty(request.getRows()) ? List.of() : request.getRows();
|
List<Map<String, Object>> rows = Func.isEmpty(request.getRows()) ? List.of() : request.getRows();
|
||||||
|
|
||||||
|
// 执行批量数据校验
|
||||||
|
Map<Integer, String> validationErrors = new TreeMap<>();
|
||||||
|
if (!draft) {
|
||||||
|
validationErrors = validateImportRows(rows, importStatus);
|
||||||
|
if (!validationErrors.isEmpty()) {
|
||||||
|
// 将所有错误信息合并抛出
|
||||||
|
StringBuilder errorMessage = new StringBuilder("数据校验失败:\n");
|
||||||
|
validationErrors.forEach((index, error) ->
|
||||||
|
errorMessage.append("第").append(index + 1).append("行:").append(error).append("\n")
|
||||||
|
);
|
||||||
|
throw new ServiceException(errorMessage.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<Waybill> waybills = new ArrayList<>();
|
List<Waybill> waybills = new ArrayList<>();
|
||||||
for (int index = 0; index < rows.size(); index++) {
|
for (int index = 0; index < rows.size(); index++) {
|
||||||
try {
|
try {
|
||||||
@@ -179,17 +203,18 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
|||||||
if (row == null) throw new ServiceException("运单数据不正确");
|
if (row == null) throw new ServiceException("运单数据不正确");
|
||||||
Waybill waybill = new Waybill();
|
Waybill waybill = new Waybill();
|
||||||
waybill.setOriginalNo(stringValue(row, "originalNo"));
|
waybill.setOriginalNo(stringValue(row, "originalNo"));
|
||||||
|
waybill.setLoadingNo(stringValue(row, "loadingIdentifier", "配载标识号"));
|
||||||
waybill.setVehicleNo(stringValue(row, "vehicleNo"));
|
waybill.setVehicleNo(stringValue(row, "vehicleNo"));
|
||||||
waybill.setDriverId(longValue(row, "driverId", "司机ID"));
|
waybill.setDriverId(longValue(row, "driverId", "司机ID"));
|
||||||
waybill.setDriverName(stringValue(row, "driverName"));
|
waybill.setDriverName(stringValue(row, "driverName", "司机/船长姓名", "司机/船长"));
|
||||||
waybill.setDriverPhone(stringValue(row, "driverPhone"));
|
waybill.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号"));
|
||||||
waybill.setTransportType(stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式"));
|
waybill.setTransportType(stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式"));
|
||||||
waybill.setCargoName(stringValue(row, "cargoName"));
|
waybill.setCargoName(stringValue(row, "cargoName"));
|
||||||
waybill.setCargoType(stringValue(row, "cargoType"));
|
waybill.setCargoType(stringValue(row, "cargoType"));
|
||||||
waybill.setSpecification(stringValue(row, "specification"));
|
waybill.setSpecification(stringValue(row, "specification", "规格"));
|
||||||
waybill.setModel(stringValue(row, "model"));
|
waybill.setModel(stringValue(row, "model", "型号"));
|
||||||
waybill.setQuantity(decimalValue(row, "quantity", "重量"));
|
waybill.setQuantity(decimalValue(row, "quantity", "数量", "重量"));
|
||||||
waybill.setQuantityUnit(stringValue(row, "quantityUnit"));
|
waybill.setQuantityUnit(stringValue(row, "quantityUnit", "数量单位"));
|
||||||
waybill.setDepartureAddressId(longValue(row, "departureAddressId", "发货地址ID"));
|
waybill.setDepartureAddressId(longValue(row, "departureAddressId", "发货地址ID"));
|
||||||
waybill.setDepartureName(stringValue(row, "departureName"));
|
waybill.setDepartureName(stringValue(row, "departureName"));
|
||||||
waybill.setDepartureAddress(stringValue(row, "departureAddress"));
|
waybill.setDepartureAddress(stringValue(row, "departureAddress"));
|
||||||
@@ -198,8 +223,8 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
|||||||
waybill.setArrivalAddressId(longValue(row, "arrivalAddressId", "收货地址ID"));
|
waybill.setArrivalAddressId(longValue(row, "arrivalAddressId", "收货地址ID"));
|
||||||
waybill.setArrivalName(stringValue(row, "arrivalName"));
|
waybill.setArrivalName(stringValue(row, "arrivalName"));
|
||||||
waybill.setArrivalAddress(stringValue(row, "arrivalAddress"));
|
waybill.setArrivalAddress(stringValue(row, "arrivalAddress"));
|
||||||
waybill.setArrivalContact(stringValue(row, "arrivalContact"));
|
waybill.setArrivalContact(stringValue(row, "arrivalContact", "收货联系人"));
|
||||||
waybill.setArrivalPhone(stringValue(row, "arrivalPhone"));
|
waybill.setArrivalPhone(stringValue(row, "arrivalPhone", "收货联系人电话"));
|
||||||
waybill.setCaptainName(stringValue(row, "captainName"));
|
waybill.setCaptainName(stringValue(row, "captainName"));
|
||||||
waybill.setCabinNo(stringValue(row, "cabinNo"));
|
waybill.setCabinNo(stringValue(row, "cabinNo"));
|
||||||
waybill.setContainerNo(stringValue(row, "containerNo"));
|
waybill.setContainerNo(stringValue(row, "containerNo"));
|
||||||
@@ -207,12 +232,13 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
|||||||
waybill.setEscortName(stringValue(row, "escortName"));
|
waybill.setEscortName(stringValue(row, "escortName"));
|
||||||
waybill.setEscortPhone(stringValue(row, "escortPhone"));
|
waybill.setEscortPhone(stringValue(row, "escortPhone"));
|
||||||
waybill.setMileage(normalizeImportMileage(decimalValue(
|
waybill.setMileage(normalizeImportMileage(decimalValue(
|
||||||
row, "mileage", "里程", "里程", "里程(km)", "里程(公里)"
|
row, "mileage", "里程", "里程(km)", "里程(公里)"
|
||||||
)));
|
)));
|
||||||
waybill.setUnitPrice(decimalValue(row, "unitPrice", "单价"));
|
waybill.setUnitPrice(decimalValue(row, "unitPrice", "单价"));
|
||||||
waybill.setPriceUnit(stringValue(row, "priceUnit"));
|
waybill.setPriceUnit(stringValue(row, "priceUnit"));
|
||||||
waybill.setOtherFeeTotal(decimalValue(row, "otherFeeTotal", "其他费用合计", "其他费用合计"));
|
waybill.setOtherFeeTotal(decimalValue(row, "otherFeeTotal", "其他费用合计"));
|
||||||
waybill.setRemark(stringValue(row, "remark"));
|
waybill.setRemark(stringValue(row, "remark"));
|
||||||
|
waybill.setRelationNo(stringValue(row, "waybillIdentifier", "同一运单标识号"));
|
||||||
waybill.setProjectId(batch.getProjectId());
|
waybill.setProjectId(batch.getProjectId());
|
||||||
waybill.setProjectName(batch.getProjectName());
|
waybill.setProjectName(batch.getProjectName());
|
||||||
waybill.setCustomerName(batch.getCustomerName());
|
waybill.setCustomerName(batch.getCustomerName());
|
||||||
@@ -232,8 +258,10 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
|||||||
waybill.setQuantityUnit(Func.isEmpty(waybill.getQuantityUnit()) ? "吨" : waybill.getQuantityUnit());
|
waybill.setQuantityUnit(Func.isEmpty(waybill.getQuantityUnit()) ? "吨" : waybill.getQuantityUnit());
|
||||||
waybill.setPriceUnit(Func.isEmpty(waybill.getPriceUnit()) ? "吨" : waybill.getPriceUnit());
|
waybill.setPriceUnit(Func.isEmpty(waybill.getPriceUnit()) ? "吨" : waybill.getPriceUnit());
|
||||||
// 草稿允许明细不完整,时间为空时不阻断保存。
|
// 草稿允许明细不完整,时间为空时不阻断保存。
|
||||||
waybill.setStartDate(parseDate(row.get("startDate"), "开始时间", !draft));
|
waybill.setStartDate(parseDate(row.get("actualStartDate"), "实际发货时间", !draft, "startDate", "开始时间"));
|
||||||
waybill.setEndDate(parseDate(row.get("endDate"), "结束时间", !draft));
|
waybill.setEndDate(parseDate(row.get("actualEndDate"), "实际完成时间", !draft, "endDate", "结束时间"));
|
||||||
|
waybill.setEstimatedStartTime(parseDate(row.get("planStartDate"), "预计发货时间", false, "estimatedStartTime"));
|
||||||
|
waybill.setEstimatedEndTime(parseDate(row.get("planEndDate"), "预计完成时间", false, "estimatedEndTime"));
|
||||||
return waybill;
|
return waybill;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +303,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
|||||||
return mileage != null && mileage.compareTo(BigDecimal.ONE.negate()) == 0 ? null : mileage;
|
return mileage != null && mileage.compareTo(BigDecimal.ONE.negate()) == 0 ? null : mileage;
|
||||||
}
|
}
|
||||||
|
|
||||||
private LocalDate parseDate(Object value, String fieldName, boolean required) {
|
private LocalDate parseDate(Object value, String fieldName, boolean required, String... aliases) {
|
||||||
if (value == null || String.valueOf(value).isBlank()) {
|
if (value == null || String.valueOf(value).isBlank()) {
|
||||||
if (!required) return null;
|
if (!required) return null;
|
||||||
throw new ServiceException(fieldName + "不能为空");
|
throw new ServiceException(fieldName + "不能为空");
|
||||||
@@ -323,4 +351,354 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
|||||||
long count = count(Wrappers.<WaybillImportBatch>lambdaQuery().likeRight(WaybillImportBatch::getBatchNo, prefix));
|
long count = count(Wrappers.<WaybillImportBatch>lambdaQuery().likeRight(WaybillImportBatch::getBatchNo, prefix));
|
||||||
return prefix + String.format("%04d", count + 1);
|
return prefix + String.format("%04d", count + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量校验导入数据
|
||||||
|
* @param rows 导入的行数据
|
||||||
|
* @param importStatus 导入状态
|
||||||
|
* @return 错误信息映射,key为行号,value为错误信息
|
||||||
|
*/
|
||||||
|
private Map<Integer, String> validateImportRows(List<Map<String, Object>> rows, String importStatus) {
|
||||||
|
Map<Integer, String> errorMap = new TreeMap<>();
|
||||||
|
boolean isDraft = STATUS_DRAFT.equals(importStatus);
|
||||||
|
|
||||||
|
// 加载系统枚举值
|
||||||
|
List<String> transportTypeOptions = loadTransportTypeOptions();
|
||||||
|
List<String> quantityUnitOptions = loadQuantityUnitOptions();
|
||||||
|
|
||||||
|
// 构建配载标识号和同一运单标识号的映射
|
||||||
|
Map<String, List<Integer>> loadingIdentifierMap = new HashMap<>();
|
||||||
|
Map<String, List<Integer>> waybillIdentifierMap = new HashMap<>();
|
||||||
|
Map<String, Integer> loadingIdentifierCountMap = new HashMap<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
|
Map<String, Object> row = rows.get(i);
|
||||||
|
String loadingIdentifier = stringValue(row, "loadingIdentifier", "配载标识号");
|
||||||
|
String waybillIdentifier = stringValue(row, "waybillIdentifier", "同一运单标识号");
|
||||||
|
|
||||||
|
if (Func.isNotEmpty(loadingIdentifier)) {
|
||||||
|
loadingIdentifierMap.computeIfAbsent(loadingIdentifier, k -> new ArrayList<>()).add(i);
|
||||||
|
loadingIdentifierCountMap.merge(loadingIdentifier, 1, Integer::sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Func.isNotEmpty(waybillIdentifier)) {
|
||||||
|
waybillIdentifierMap.computeIfAbsent(waybillIdentifier, k -> new ArrayList<>()).add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逐行校验
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
Map<String, Object> row = rows.get(i);
|
||||||
|
|
||||||
|
// 1. 配载标识号校验
|
||||||
|
validateLoadingIdentifier(row, i, loadingIdentifierMap, rows, errors);
|
||||||
|
|
||||||
|
// 2. 车牌号校验
|
||||||
|
validateVehicleNo(row, errors);
|
||||||
|
|
||||||
|
// 3. 运输方式校验
|
||||||
|
validateTransportType(row, transportTypeOptions, errors);
|
||||||
|
|
||||||
|
// 4. 手机号校验
|
||||||
|
validatePhoneNumber(row, "driverPhone", "司机/船长手机号", errors);
|
||||||
|
validatePhoneNumber(row, "departurePhone", "发货联系人电话", errors);
|
||||||
|
validatePhoneNumber(row, "arrivalPhone", "收货联系人电话", errors);
|
||||||
|
|
||||||
|
// 5. 货物类型校验(暂时跳过,需要货物类型数据)
|
||||||
|
|
||||||
|
// 6. 数量校验
|
||||||
|
validatePositiveNumber(row, "quantity", "数量", errors);
|
||||||
|
|
||||||
|
// 7. 数量单位校验
|
||||||
|
validateQuantityUnit(row, quantityUnitOptions, errors);
|
||||||
|
|
||||||
|
// 8. 里程校验
|
||||||
|
validatePositiveNumber(row, "mileage", "里程(km)", errors);
|
||||||
|
|
||||||
|
// 9. 运费合计校验
|
||||||
|
validateFreightTotal(row, errors);
|
||||||
|
|
||||||
|
// 10. 实际发货时间校验
|
||||||
|
validateActualStartDate(row, isDraft, errors);
|
||||||
|
|
||||||
|
// 11. 实际完成时间校验
|
||||||
|
validateActualEndDate(row, isDraft, errors);
|
||||||
|
|
||||||
|
// 12. 预计发货时间校验
|
||||||
|
validatePlanStartDate(row, errors);
|
||||||
|
|
||||||
|
// 13. 预计完成时间校验
|
||||||
|
validatePlanEndDate(row, errors);
|
||||||
|
|
||||||
|
// 14. 同一运单标识号校验
|
||||||
|
validateWaybillIdentifier(row, i, waybillIdentifierMap, rows, errors);
|
||||||
|
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
errorMap.put(i, String.join("; ", errors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateLoadingIdentifier(Map<String, Object> row, int rowIndex,
|
||||||
|
Map<String, List<Integer>> loadingIdentifierMap, List<Map<String, Object>> allRows, List<String> errors) {
|
||||||
|
String loadingIdentifier = stringValue(row, "loadingIdentifier", "配载标识号");
|
||||||
|
if (Func.isEmpty(loadingIdentifier)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Integer> sameIdentifierRows = loadingIdentifierMap.get(loadingIdentifier);
|
||||||
|
if (sameIdentifierRows == null || sameIdentifierRows.size() <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查同一配载标识号下车牌号是否一致
|
||||||
|
String currentVehicleNo = stringValue(row, "vehicleNo");
|
||||||
|
for (Integer otherRowIndex : sameIdentifierRows) {
|
||||||
|
if (otherRowIndex.equals(rowIndex)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo");
|
||||||
|
if (Func.isNotEmpty(currentVehicleNo) && Func.isNotEmpty(otherVehicleNo)
|
||||||
|
&& !currentVehicleNo.equals(otherVehicleNo)) {
|
||||||
|
errors.add("同一配载标识号下,车牌号不一致");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查配载标识号是否重复
|
||||||
|
if (sameIdentifierRows.size() > 1 && sameIdentifierRows.indexOf(rowIndex) > 0) {
|
||||||
|
errors.add("配载标识号重复");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateVehicleNo(Map<String, Object> row, List<String> errors) {
|
||||||
|
String vehicleNo = stringValue(row, "vehicleNo");
|
||||||
|
String transportType = stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式");
|
||||||
|
|
||||||
|
if (Func.isEmpty(vehicleNo) || Func.isEmpty(transportType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当运输类型为公路整车或公路配载/零担时,校验车牌号格式
|
||||||
|
if (transportType.contains("公路整车") || transportType.contains("公路配载") || transportType.contains("公路零担")) {
|
||||||
|
if (!VEHICLE_NO_PATTERN.matcher(vehicleNo).matches()) {
|
||||||
|
errors.add("车牌号格式不正确,应为首位汉字、次位大写字母、总长度7或8位");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateTransportType(Map<String, Object> row, List<String> transportTypeOptions, List<String> errors) {
|
||||||
|
String transportType = stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式");
|
||||||
|
if (Func.isEmpty(transportType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transportTypeOptions.contains(transportType)) {
|
||||||
|
errors.add("运输方式必须为系统枚举值之一");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePhoneNumber(Map<String, Object> row, String field, String fieldName, List<String> errors) {
|
||||||
|
String phone = stringValue(row, field, fieldName);
|
||||||
|
if (Func.isEmpty(phone)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PHONE_PATTERN.matcher(phone).matches()) {
|
||||||
|
errors.add(fieldName + "格式不正确,应为11位数字");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePositiveNumber(Map<String, Object> row, String field, String fieldName, List<String> errors) {
|
||||||
|
String value = stringValue(row, field, fieldName);
|
||||||
|
if (Func.isEmpty(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
BigDecimal number = new BigDecimal(value);
|
||||||
|
if (number.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
errors.add(fieldName + "必须为正数");
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
errors.add(fieldName + "格式不正确");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateQuantityUnit(Map<String, Object> row, List<String> quantityUnitOptions, List<String> errors) {
|
||||||
|
String quantityUnit = stringValue(row, "quantityUnit", "数量单位");
|
||||||
|
if (Func.isEmpty(quantityUnit)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!quantityUnitOptions.contains(quantityUnit)) {
|
||||||
|
errors.add("数量单位必须为系统枚举值之一");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateFreightTotal(Map<String, Object> row, List<String> errors) {
|
||||||
|
String freightTotalStr = stringValue(row, "freightTotal", "运费合计");
|
||||||
|
if (Func.isEmpty(freightTotalStr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
BigDecimal freightTotal = new BigDecimal(freightTotalStr);
|
||||||
|
String freightStr = stringValue(row, "freight", "运费");
|
||||||
|
String otherFeeTotalStr = stringValue(row, "otherFeeTotal", "其他费用合计");
|
||||||
|
|
||||||
|
BigDecimal freight = Func.isNotEmpty(freightStr) ? new BigDecimal(freightStr) : BigDecimal.ZERO;
|
||||||
|
BigDecimal otherFeeTotal = Func.isNotEmpty(otherFeeTotalStr) ? new BigDecimal(otherFeeTotalStr) : BigDecimal.ZERO;
|
||||||
|
|
||||||
|
BigDecimal calculatedTotal = freight.add(otherFeeTotal);
|
||||||
|
if (freightTotal.compareTo(calculatedTotal) != 0) {
|
||||||
|
errors.add("运费合计应等于运费+其他费用合计");
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
errors.add("运费相关字段格式不正确");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateActualStartDate(Map<String, Object> row, boolean isDraft, List<String> errors) {
|
||||||
|
Object actualStartDate = row.get("actualStartDate");
|
||||||
|
if (actualStartDate == null || String.valueOf(actualStartDate).isBlank()) {
|
||||||
|
if (!isDraft) {
|
||||||
|
errors.add("实际发货时间不能为空");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate startDate = parseDateForValidation(actualStartDate, "实际发货时间", errors);
|
||||||
|
if (startDate == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object actualEndDate = row.get("actualEndDate");
|
||||||
|
if (actualEndDate != null && !String.valueOf(actualEndDate).isBlank()) {
|
||||||
|
LocalDate endDate = parseDateForValidation(actualEndDate, "实际完成时间", errors);
|
||||||
|
if (endDate != null && startDate.isAfter(endDate)) {
|
||||||
|
errors.add("实际发货时间不能晚于实际完成时间");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateActualEndDate(Map<String, Object> row, boolean isDraft, List<String> errors) {
|
||||||
|
Object actualEndDate = row.get("actualEndDate");
|
||||||
|
if (actualEndDate == null || String.valueOf(actualEndDate).isBlank()) {
|
||||||
|
if (!isDraft) {
|
||||||
|
errors.add("实际完成时间不能为空");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate endDate = parseDateForValidation(actualEndDate, "实际完成时间", errors);
|
||||||
|
if (endDate == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object actualStartDate = row.get("actualStartDate");
|
||||||
|
if (actualStartDate != null && !String.valueOf(actualStartDate).isBlank()) {
|
||||||
|
LocalDate startDate = parseDateForValidation(actualStartDate, "实际发货时间", errors);
|
||||||
|
if (startDate != null && endDate.isBefore(startDate)) {
|
||||||
|
errors.add("实际完成时间不能早于实际发货时间");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePlanStartDate(Map<String, Object> row, List<String> errors) {
|
||||||
|
Object planStartDate = row.get("planStartDate");
|
||||||
|
if (planStartDate == null || String.valueOf(planStartDate).isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate startDate = parseDateForValidation(planStartDate, "预计发货时间", errors);
|
||||||
|
if (startDate == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object planEndDate = row.get("planEndDate");
|
||||||
|
if (planEndDate != null && !String.valueOf(planEndDate).isBlank()) {
|
||||||
|
LocalDate endDate = parseDateForValidation(planEndDate, "预计完成时间", errors);
|
||||||
|
if (endDate != null && startDate.isAfter(endDate)) {
|
||||||
|
errors.add("预计发货时间不能晚于预计完成时间");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePlanEndDate(Map<String, Object> row, List<String> errors) {
|
||||||
|
Object planEndDate = row.get("planEndDate");
|
||||||
|
if (planEndDate == null || String.valueOf(planEndDate).isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate endDate = parseDateForValidation(planEndDate, "预计完成时间", errors);
|
||||||
|
if (endDate == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object planStartDate = row.get("planStartDate");
|
||||||
|
if (planStartDate != null && !String.valueOf(planStartDate).isBlank()) {
|
||||||
|
LocalDate startDate = parseDateForValidation(planStartDate, "预计发货时间", errors);
|
||||||
|
if (startDate != null && endDate.isBefore(startDate)) {
|
||||||
|
errors.add("预计完成时间不能早于预计发货时间");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateWaybillIdentifier(Map<String, Object> row, int rowIndex,
|
||||||
|
Map<String, List<Integer>> waybillIdentifierMap, List<Map<String, Object>> allRows, List<String> errors) {
|
||||||
|
String waybillIdentifier = stringValue(row, "waybillIdentifier", "同一运单标识号");
|
||||||
|
if (Func.isEmpty(waybillIdentifier)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Integer> sameIdentifierRows = waybillIdentifierMap.get(waybillIdentifier);
|
||||||
|
if (sameIdentifierRows == null || sameIdentifierRows.size() <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查同一运单标识号下车牌号是否一致
|
||||||
|
String currentVehicleNo = stringValue(row, "vehicleNo");
|
||||||
|
for (Integer otherRowIndex : sameIdentifierRows) {
|
||||||
|
if (otherRowIndex.equals(rowIndex)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo");
|
||||||
|
if (Func.isNotEmpty(currentVehicleNo) && Func.isNotEmpty(otherVehicleNo)
|
||||||
|
&& !currentVehicleNo.equals(otherVehicleNo)) {
|
||||||
|
errors.add("同一运单标识号下,车牌号必须一致");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDate parseDateForValidation(Object value, String fieldName, List<String> errors) {
|
||||||
|
if (value == null || String.valueOf(value).isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String text = String.valueOf(value).trim();
|
||||||
|
try {
|
||||||
|
return text.length() == 10 ? LocalDate.parse(text) : LocalDate.parse(text, DATE_TIME_FORMATTER);
|
||||||
|
} catch (DateTimeParseException exception) {
|
||||||
|
errors.add(fieldName + "格式必须为日期格式(YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> loadTransportTypeOptions() {
|
||||||
|
// 常见运输方式枚举值
|
||||||
|
return List.of(
|
||||||
|
"公路整车", "公路配载/零担", "铁路整车", "铁路零担",
|
||||||
|
"水路", "航空", "多式联运", "管道运输", "其他"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> loadQuantityUnitOptions() {
|
||||||
|
// 常见数量单位
|
||||||
|
return List.of("吨", "千克", "立方米", "件", "箱", "台", "个", "升", "米", "平方米");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -252,6 +252,9 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
|||||||
BeanUtil.copyProperties(record, excel);
|
BeanUtil.copyProperties(record, excel);
|
||||||
excel.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
|
excel.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
|
||||||
excel.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
|
excel.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
|
||||||
|
excel.setCreateTime(record.getCreateTime());
|
||||||
|
excel.setUpdateTime(record.getUpdateTime());
|
||||||
|
excel.setBusinessStatus(WaybillWrapper.businessStatusName(record.getBusinessStatus()));
|
||||||
return excel;
|
return excel;
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ public class WaybillWrapper extends BaseEntityWrapper<Waybill, WaybillVO> {
|
|||||||
return waybillVO;
|
return waybillVO;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String businessStatusName(String status) {
|
public static String businessStatusName(String status) {
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
return "未知";
|
return "未知";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- 运输计划表新增字段:里程和同一计划标识号
|
||||||
|
-- 日期:2026-09-08
|
||||||
|
|
||||||
|
-- 添加里程字段
|
||||||
|
ALTER TABLE `blade_transport_plan`
|
||||||
|
ADD COLUMN `mileage` decimal(10,2) DEFAULT NULL COMMENT '里程(km)' AFTER `remark`;
|
||||||
|
|
||||||
|
-- 添加同一计划标识号字段
|
||||||
|
ALTER TABLE `blade_transport_plan`
|
||||||
|
ADD COLUMN `plan_group_id` varchar(100) DEFAULT NULL COMMENT '同一计划标识号' AFTER `mileage`;
|
||||||
|
|
||||||
|
-- 添加索引
|
||||||
|
ALTER TABLE `blade_transport_plan`
|
||||||
|
ADD INDEX `idx_transport_plan_group_id` (`plan_group_id`) USING BTREE;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- 凭证管理审核功能增强
|
||||||
|
-- 日期:2026-09-08
|
||||||
|
|
||||||
|
-- 1. 添加承运商ID字段
|
||||||
|
ALTER TABLE `blade_voucher_manage`
|
||||||
|
ADD COLUMN `carrier_id` bigint DEFAULT NULL COMMENT '承运商ID(组织ID)' AFTER `carrier_name`;
|
||||||
|
|
||||||
|
-- 2. 添加索引
|
||||||
|
ALTER TABLE `blade_voucher_manage`
|
||||||
|
ADD KEY `idx_voucher_manage_carrier_id` (`carrier_id`),
|
||||||
|
ADD KEY `idx_voucher_manage_audit_status` (`audit_status`);
|
||||||
|
|
||||||
|
-- 3. 新增审核菜单权限
|
||||||
|
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`)
|
||||||
|
VALUES
|
||||||
|
(2090000000000000905, 2090000000000000900, 'voucher_manage_audit_pass', '审核通过', 'voucher_manage_audit_pass', '', '', 5, 2, 0, 1, NULL, '', 0),
|
||||||
|
(2090000000000000906, 2090000000000000900, 'voucher_manage_audit_reject', '审核驳回', 'voucher_manage_audit_reject', '', '', 6, 2, 0, 1, NULL, '', 0)
|
||||||
|
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `is_deleted` = 0;
|
||||||
Reference in New Issue
Block a user