1、调整凭证
2、调整运单
This commit is contained in:
+6
@@ -145,4 +145,10 @@ public class TransportPlan extends TenantEntity {
|
||||
@Schema(description = "备注")
|
||||
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 String uploadSource;
|
||||
private String carrierName;
|
||||
private Long carrierId;
|
||||
private String processStatus;
|
||||
private Integer voucherCount;
|
||||
private Integer relatedWaybillCount;
|
||||
|
||||
+3
@@ -4,6 +4,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 凭证解压文件详情。
|
||||
@@ -29,4 +30,6 @@ public class VoucherFileVO implements Serializable {
|
||||
private String fileType;
|
||||
private Integer matched;
|
||||
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 = "自动匹配内部账单")
|
||||
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")
|
||||
@ApiOperationSupport(order = 10)
|
||||
@Operation(summary = "人工匹配账单明细")
|
||||
@@ -179,4 +186,11 @@ public class TransportReconciliationController extends BladeController {
|
||||
@ApiOperationSupport(order = 14)
|
||||
@Operation(summary = "完成运输对账")
|
||||
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.entity.VoucherManage;
|
||||
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.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -46,6 +48,34 @@ public class VoucherManageController extends BladeController {
|
||||
@Operation(summary = "凭证详情")
|
||||
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")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "上传凭证或更换运输批次")
|
||||
@@ -85,4 +115,20 @@ public class VoucherManageController extends BladeController {
|
||||
@RequestParam(required = false) String createTimeStart, @RequestParam(required = false) String 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("*计划名称")
|
||||
private String planName;
|
||||
@ExcelProperty("*运输方式")
|
||||
@ExcelProperty("*运输类型")
|
||||
private String transportType;
|
||||
@ExcelProperty("*计划开始日期")
|
||||
private String planStartDate;
|
||||
@ExcelProperty("*计划结束日期")
|
||||
private String planEndDate;
|
||||
@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("货物名称")
|
||||
private String cargoName;
|
||||
@ExcelProperty("*货物类型")
|
||||
private String cargoType;
|
||||
@ExcelProperty("数量")
|
||||
private BigDecimal quantity;
|
||||
@ExcelProperty("数量单位")
|
||||
@ExcelProperty("计量单位")
|
||||
private String quantityUnit;
|
||||
@ExcelProperty("包装")
|
||||
private String packageType;
|
||||
@ExcelProperty("品牌")
|
||||
private String brand;
|
||||
@ExcelProperty("规格")
|
||||
private String specification;
|
||||
@ExcelProperty("型号")
|
||||
private String model;
|
||||
@ExcelProperty("物料编码")
|
||||
private String materialCode;
|
||||
@ExcelProperty("设备编码")
|
||||
private String deviceCode;
|
||||
@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("里程(km)")
|
||||
private BigDecimal mileage;
|
||||
@ExcelProperty("计划开始时间")
|
||||
private String planStartDate;
|
||||
@ExcelProperty("计划结束时间")
|
||||
private String planEndDate;
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
@ExcelProperty("同一计划标识号")
|
||||
private String planGroupId;
|
||||
|
||||
@ExcelIgnore
|
||||
private String errorMessage;
|
||||
|
||||
+5
-6
@@ -34,8 +34,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 运单管理 Excel
|
||||
@@ -151,12 +150,12 @@ public class WaybillExcel implements Serializable {
|
||||
private String createUserName;
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty(value = "创建时间", converter = MaintenancePlanDateTimeConverter.class)
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
@ExcelProperty(value = "更新时间", converter = MaintenancePlanDateTimeConverter.class)
|
||||
private Date createTime;
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelIgnore
|
||||
private String errorMessage;
|
||||
|
||||
+44
-22
@@ -22,45 +22,67 @@ public class WaybillImportBatchExcel implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty("序号")
|
||||
private Integer serialNumber;
|
||||
@ExcelProperty("原始单号")
|
||||
private String originalNo;
|
||||
@ExcelProperty("配载标识号")
|
||||
private String loadingIdentifier;
|
||||
@ExcelProperty("*车牌号/航班号/船号/班列号")
|
||||
private String vehicleNo;
|
||||
@ExcelProperty("*司机/船长")
|
||||
private String driverName;
|
||||
@ExcelProperty("*运输类型")
|
||||
@ExcelProperty("*运输方式")
|
||||
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("*货物名称")
|
||||
private String cargoName;
|
||||
@ExcelProperty("*货物类型")
|
||||
private String cargoType;
|
||||
@ExcelProperty("重量")
|
||||
@ExcelProperty("包装")
|
||||
private String packageType;
|
||||
@ExcelProperty("*数量")
|
||||
private BigDecimal quantity;
|
||||
@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("*开始时间")
|
||||
private String startDate;
|
||||
@ExcelProperty("*结束时间")
|
||||
private String endDate;
|
||||
@ExcelProperty("*单价")
|
||||
@ExcelProperty("*数量单位")
|
||||
private String quantityUnit;
|
||||
@ExcelProperty("规格")
|
||||
private String specification;
|
||||
@ExcelProperty("型号")
|
||||
private String model;
|
||||
@ExcelProperty("里程(km)")
|
||||
private BigDecimal mileage;
|
||||
@ExcelProperty("单价")
|
||||
private BigDecimal unitPrice;
|
||||
@ExcelProperty("*运费")
|
||||
@ExcelProperty("运费")
|
||||
private BigDecimal freight;
|
||||
@ExcelProperty("其他费用合计")
|
||||
private BigDecimal otherFeeTotal;
|
||||
@ExcelProperty("运费合计")
|
||||
private BigDecimal freightTotal;
|
||||
@ExcelProperty("*实际发货时间")
|
||||
private String actualStartDate;
|
||||
@ExcelProperty("*实际完成时间")
|
||||
private String actualEndDate;
|
||||
@ExcelProperty("预计发货时间")
|
||||
private String planStartDate;
|
||||
@ExcelProperty("预计完成时间")
|
||||
private String planEndDate;
|
||||
@ExcelProperty("备注")
|
||||
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} "
|
||||
+ "AND voucher_batch_no LIKE CONCAT(#{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);
|
||||
List<VehicleReconciliationFailureExcel> importVehicles(Long id, List<VehicleReconciliationExcel> rows);
|
||||
List<CargoReconciliationFailureExcel> importCargoes(Long id, List<CargoReconciliationExcel> rows);
|
||||
TransportReconciliationVO matchPreview(TransportReconciliationVO request);
|
||||
TransportReconciliationVO completeWithData(TransportReconciliationVO request);
|
||||
void autoMatch(Long id);
|
||||
void manualMatch(TransportReconciliationManualMatchRequest request);
|
||||
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.entity.VoucherManage;
|
||||
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.Map;
|
||||
@@ -14,6 +16,10 @@ import java.util.Map;
|
||||
public interface IVoucherManageService extends BaseService<VoucherManage> {
|
||||
IPage<VoucherManageVO> selectPage(IPage<VoucherManage> page, VoucherManageVO query);
|
||||
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);
|
||||
VoucherManage createUploadDraft(VoucherUploadDraftRequest request);
|
||||
void completeUploadFile(VoucherFileCompleteRequest request);
|
||||
@@ -21,4 +27,6 @@ public interface IVoucherManageService extends BaseService<VoucherManage> {
|
||||
void reprocessUploadedVoucher(Long voucherId);
|
||||
void removeVoucher(Long id);
|
||||
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);
|
||||
}
|
||||
|
||||
+226
-43
@@ -59,11 +59,14 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 运输计划 服务实现类
|
||||
@@ -207,13 +210,95 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
throw new ServiceException("客户合同不能为空");
|
||||
}
|
||||
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++) {
|
||||
TransportPlanImportExcel excel = data.get(index);
|
||||
try {
|
||||
LocalDate planStartDate = parseImportDate(excel.getPlanStartDate(), "计划开始日期");
|
||||
LocalDate planEndDate = parseImportDate(excel.getPlanEndDate(), "计划结束日期");
|
||||
validateImportExcel(excel, planStartDate, planEndDate);
|
||||
LocalDate planStartDate = Func.isNotEmpty(excel.getPlanStartDate())
|
||||
? parseImportDate(excel.getPlanStartDate(), "计划开始时间") : null;
|
||||
LocalDate planEndDate = Func.isNotEmpty(excel.getPlanEndDate())
|
||||
? parseImportDate(excel.getPlanEndDate(), "计划结束时间") : null;
|
||||
|
||||
List<String> validationErrors = validateImportExcel(excel, planStartDate, planEndDate, planNameCountMap, planGroupMap, currentDeptId);
|
||||
if (Func.isNotEmpty(validationErrors)) {
|
||||
excel.setErrorMessage(formatImportErrorMessage(validationErrors));
|
||||
errorMap.put(index, excel);
|
||||
continue;
|
||||
}
|
||||
|
||||
TransportPlan transportPlan = buildImportTransportPlan(excel, projectId, projectName, contractId, contractName, customerName, planStartDate, planEndDate);
|
||||
importPlans.add(transportPlan);
|
||||
} catch (Exception exception) {
|
||||
String message = exception instanceof ServiceException ? exception.getMessage() : "数据解析失败";
|
||||
excel.setErrorMessage(formatImportErrorMessage(List.of(message)));
|
||||
errorMap.put(index, excel);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有校验错误,回滚事务
|
||||
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) {
|
||||
LinkedHashMap<String, Object> goods = new LinkedHashMap<>();
|
||||
goods.put("cargoName", excel.getCargoName());
|
||||
goods.put("cargoType", excel.getCargoType());
|
||||
goods.put("quantity", excel.getQuantity());
|
||||
goods.put("quantityUnit", excel.getQuantityUnit());
|
||||
goods.put("packageType", excel.getPackageType());
|
||||
goods.put("specification", excel.getSpecification());
|
||||
goods.put("model", excel.getModel());
|
||||
return goods;
|
||||
}
|
||||
|
||||
private Map<String, Integer> buildImportPlanNameCountMap(List<TransportPlanImportExcel> data) {
|
||||
Map<String, Integer> countMap = new HashMap<>();
|
||||
for (TransportPlanImportExcel excel : data) {
|
||||
String planName = trimToEmpty(excel.getPlanName());
|
||||
if (Func.isNotEmpty(planName)) {
|
||||
countMap.merge(planName, 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
return countMap;
|
||||
}
|
||||
|
||||
private Map<String, List<Integer>> buildImportPlanGroupMap(List<TransportPlanImportExcel> data) {
|
||||
Map<String, List<Integer>> groupMap = new HashMap<>();
|
||||
for (int index = 0; index < data.size(); index++) {
|
||||
TransportPlanImportExcel excel = data.get(index);
|
||||
String planGroupId = trimToEmpty(excel.getPlanGroupId());
|
||||
if (Func.isNotEmpty(planGroupId)) {
|
||||
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);
|
||||
@@ -230,56 +315,151 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
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");
|
||||
submit(transportPlan);
|
||||
} catch (Exception exception) {
|
||||
excel.setErrorMessage(exception.getMessage());
|
||||
errorList.add(excel);
|
||||
}
|
||||
}
|
||||
return errorList;
|
||||
return transportPlan;
|
||||
}
|
||||
|
||||
private LinkedHashMap<String, Object> importGoods(TransportPlanImportExcel excel) {
|
||||
LinkedHashMap<String, Object> goods = new LinkedHashMap<>();
|
||||
goods.put("cargoName", excel.getCargoName());
|
||||
goods.put("cargoType", excel.getCargoType());
|
||||
goods.put("quantity", excel.getQuantity());
|
||||
goods.put("quantityUnit", excel.getQuantityUnit());
|
||||
goods.put("packageType", excel.getPackageType());
|
||||
goods.put("brand", excel.getBrand());
|
||||
goods.put("specification", excel.getSpecification());
|
||||
goods.put("model", excel.getModel());
|
||||
goods.put("materialCode", excel.getMaterialCode());
|
||||
goods.put("deviceCode", excel.getDeviceCode());
|
||||
return goods;
|
||||
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 void validateImportExcel(TransportPlanImportExcel excel, LocalDate planStartDate, LocalDate planEndDate) {
|
||||
TransportBusinessSupport.validateRequired(excel.getPlanName(), "计划名称不能为空");
|
||||
TransportBusinessSupport.validateRequired(excel.getTransportType(), "运输方式不能为空");
|
||||
TransportBusinessSupport.validateRequired(excel.getCargoType(), "货物类型不能为空");
|
||||
TransportBusinessSupport.validateRequired(excel.getDepartureAddress(), "发货地址不能为空");
|
||||
TransportBusinessSupport.validateRequired(excel.getArrivalAddress(), "收货地址不能为空");
|
||||
validateImportLength(excel.getPlanName(), 50, "计划名称");
|
||||
validateImportLength(excel.getDepartureAddress(), 255, "发货地址");
|
||||
validateImportLength(excel.getArrivalAddress(), 255, "收货地址");
|
||||
validateImportLength(excel.getDepartureContact(), 50, "发货联系人");
|
||||
validateImportLength(excel.getArrivalContact(), 50, "收货联系人");
|
||||
validateImportLength(excel.getDeparturePhone(), 50, "发货联系方式");
|
||||
validateImportLength(excel.getArrivalPhone(), 50, "收货联系方式");
|
||||
validateImportLength(excel.getRemark(), 200, "备注");
|
||||
TransportBusinessSupport.validatePhone(excel.getDeparturePhone(), "发货联系方式格式不正确");
|
||||
TransportBusinessSupport.validatePhone(excel.getArrivalPhone(), "收货联系方式格式不正确");
|
||||
TransportBusinessSupport.validateNonNegative(excel.getQuantity(), "数量");
|
||||
TransportBusinessSupport.validateDateRange(planStartDate, planEndDate, "计划结束日期不能早于计划开始日期");
|
||||
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) {
|
||||
TransportBusinessSupport.validateRequired(value, fieldName + "不能为空");
|
||||
if (Func.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(value.trim(), DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
} catch (Exception exception) {
|
||||
@@ -511,6 +691,7 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
transportPlan.setDataSource(TransportBusinessSupport.normalizeTransportPlanDataSource(transportPlan.getDataSource()));
|
||||
transportPlan.setBusinessStatus(TransportBusinessSupport.trimToNull(transportPlan.getBusinessStatus()));
|
||||
transportPlan.setDeptName(TransportBusinessSupport.trimToNull(transportPlan.getDeptName()));
|
||||
transportPlan.setPlanGroupId(TransportBusinessSupport.trimToNull(transportPlan.getPlanGroupId()));
|
||||
transportPlan.setRemark(TransportBusinessSupport.trimToNull(transportPlan.getRemark()));
|
||||
if (Func.isEmpty(transportPlan.getDeptId())) {
|
||||
Dept dept = TransportBusinessSupport.currentDept("运输计划");
|
||||
@@ -548,7 +729,9 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
TransportBusinessSupport.validateLength(transportPlan.getDataSource(), 255, "数据来源不能超过255字");
|
||||
TransportBusinessSupport.validateLength(transportPlan.getBusinessStatus(), 255, "业务状态不能超过255字");
|
||||
TransportBusinessSupport.validateLength(transportPlan.getDeptName(), 255, "所属组织不能超过255字");
|
||||
TransportBusinessSupport.validateLength(transportPlan.getPlanGroupId(), 255, "同一计划标识号不能超过255字");
|
||||
TransportBusinessSupport.validateLength(transportPlan.getRemark(), 500, "备注不能超过500字");
|
||||
TransportBusinessSupport.validateNonNegative(transportPlan.getMileage(), "里程");
|
||||
TransportBusinessSupport.validateDateRange(transportPlan.getPlanStartDate(), transportPlan.getPlanEndDate(), "计划开始日期不能晚于计划结束日期");
|
||||
}
|
||||
|
||||
|
||||
+158
-29
@@ -55,17 +55,13 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.Collator;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
@@ -399,6 +395,124 @@ public class TransportReconciliationServiceImpl
|
||||
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
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void autoMatch(Long id) {
|
||||
@@ -407,16 +521,14 @@ public class TransportReconciliationServiceImpl
|
||||
List<TransportReconciliationExternal> externals = externalRows(id);
|
||||
if (externals.isEmpty()) throw new ServiceException("请先导入外部账单");
|
||||
resetMatches(internals, externals);
|
||||
Function<TransportReconciliationExternal, String> externalKey = CARGO.equals(bill.getReconciliationMode())
|
||||
? this::cargoKey : this::vehicleKey;
|
||||
Map<String, List<TransportReconciliationExternal>> externalGroups = externals.stream().collect(Collectors.groupingBy(externalKey));
|
||||
Map<String, List<TransportReconciliationInternal>> internalGroups = internals.stream().collect(Collectors.groupingBy(
|
||||
CARGO.equals(bill.getReconciliationMode()) ? this::cargoKey : this::vehicleKey));
|
||||
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
|
||||
&& equalMoney(externalGroup.get(0).getSettlementAmount(), internalGroup.get(0).getSettlementAmount())) {
|
||||
if (externalGroup.size() == 1 && internalGroup.size() == 1) {
|
||||
link(internalGroup.get(0), externalGroup.get(0));
|
||||
} else if (externalGroup.size() > 1) {
|
||||
for (TransportReconciliationExternal external : externalGroup) {
|
||||
@@ -664,6 +776,13 @@ public class TransportReconciliationServiceImpl
|
||||
TransportReconciliation bill = existing(id);
|
||||
List<TransportReconciliationInternal> internals = internalRows(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();
|
||||
bill.setInternalBillCount(internals.size()); bill.setExternalBillCount(externals.size());
|
||||
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.setDifferenceAmount(bill.getInternalAmount().subtract(bill.getExternalAmount()).abs());
|
||||
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) {
|
||||
@@ -696,6 +814,28 @@ public class TransportReconciliationServiceImpl
|
||||
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) {
|
||||
List<TransportReconciliationInternal> internals = internalRows(id);
|
||||
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");
|
||||
}
|
||||
|
||||
private String vehicleKey(TransportReconciliationInternal row) { return key(row.getVehicleNo(), sortedCargoNames(row.getCargoName()), row.getActualDepartureTime(), row.getBatchNo(), row.getTransportQuantity()); }
|
||||
private String vehicleKey(TransportReconciliationExternal row) { return key(row.getVehicleNo(), sortedCargoNames(row.getCargoName()), row.getActualDepartureTime(), row.getBatchNo(), row.getTransportQuantity()); }
|
||||
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(TransportReconciliationInternal row) {
|
||||
return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(), row.getCargoName(), row.getCargoType());
|
||||
}
|
||||
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 normal(Object value) {
|
||||
if (value == null) return "";
|
||||
@@ -779,23 +921,10 @@ public class TransportReconciliationServiceImpl
|
||||
if (value instanceof List<?> list) return JsonUtil.toJson(list);
|
||||
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) {
|
||||
for (String value : values) if (Func.isNotEmpty(value)) return value;
|
||||
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 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; }
|
||||
|
||||
+454
-6
@@ -2,6 +2,7 @@ package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
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.vo.VoucherManageVO;
|
||||
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.IProjectApplyService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
@@ -35,6 +37,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import io.minio.MinioClient;
|
||||
import io.minio.GetPresignedObjectUrlArgs;
|
||||
import io.minio.PutObjectArgs;
|
||||
import io.minio.RemoveObjectArgs;
|
||||
import io.minio.http.Method;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -45,6 +48,8 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.io.BufferedInputStream;
|
||||
@@ -60,6 +65,7 @@ import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -107,10 +113,326 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
.eq(VoucherFile::getVoucherId, record.getId())
|
||||
.eq(VoucherFile::getIsDeleted, 0)
|
||||
.orderByAsc(VoucherFile::getEntryName));
|
||||
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;
|
||||
}
|
||||
|
||||
@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
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public VoucherManage createUploadDraft(VoucherUploadDraftRequest request) {
|
||||
@@ -151,7 +473,19 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
voucher.setFileName(request.getFileName());
|
||||
voucher.setFileUrl(request.getFileUrl());
|
||||
voucher.setFileTaskId(request.getFileTaskId());
|
||||
|
||||
// 判断是否为承运商上传:如果用户组织的顶级组织是"外部组织",则为承运商
|
||||
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.setAuditStatus("-");
|
||||
voucher.setVoucherCount(0);
|
||||
@@ -338,6 +672,8 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
update.setRelatedWaybillCount(relatedWaybillIds.size());
|
||||
update.setUnRelatedWaybillCount(Math.max(waybills.size() - relatedWaybillIds.size(), 0));
|
||||
update.setProcessStatus("处理完成");
|
||||
// 处理完成后自动设置为待审核状态
|
||||
update.setAuditStatus("待审核");
|
||||
updateById(update);
|
||||
log.info("[凭证处理] 进度 100%:处理完成 voucherId={}, voucherBatchNo={}, fileCount={}, imageCount={}, relatedWaybillCount={}, unrelatedWaybillCount={}",
|
||||
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) {
|
||||
return toVoucherFileVO(file, true);
|
||||
}
|
||||
|
||||
private VoucherFileVO toVoucherFileVO(VoucherFile file, boolean includeUrl) {
|
||||
VoucherFileVO result = new VoucherFileVO();
|
||||
result.setId(file.getId());
|
||||
result.setVoucherId(file.getVoucherId());
|
||||
@@ -497,15 +837,47 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
result.setContentType(file.getContentType());
|
||||
result.setFileType(file.getFileType());
|
||||
result.setMatched(file.getMatched());
|
||||
try {
|
||||
result.setUrl(minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
.method(Method.GET).bucket(minioBucketName).object(file.getObjectKey()).expiry(1, java.util.concurrent.TimeUnit.HOURS).build()));
|
||||
} catch (Exception exception) {
|
||||
log.warn("生成凭证文件预览地址失败 voucherFileId={}, objectKey={}", file.getId(), file.getObjectKey(), exception);
|
||||
}
|
||||
result.setCreateTime(file.getCreateTime());
|
||||
result.setUpdateTime(file.getUpdateTime());
|
||||
if (includeUrl) result.setUrl(buildVoucherFileUrl(file.getId(), file.getObjectKey()));
|
||||
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) {
|
||||
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());
|
||||
if (query.getCreateTimeStart() != null) wrapper.ge(VoucherManage::getCreateTime, query.getCreateTimeStart());
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -578,4 +958,72 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
Long maxSequence = baseMapper.selectMaxDailySequence(AuthUtil.getTenantId(), prefix);
|
||||
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.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.interceptor.TransactionAspectSupport;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
import java.util.regex.Pattern;
|
||||
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 IMPORT_TYPE_WAYBILL = "waybill";
|
||||
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 ICustomerArchiveService customerArchiveService;
|
||||
private final IProjectApplyService projectApplyService;
|
||||
@@ -90,6 +99,21 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
clearBatchWaybills(batch.getId());
|
||||
|
||||
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<>();
|
||||
for (int index = 0; index < rows.size(); index++) {
|
||||
try {
|
||||
@@ -179,17 +203,18 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
if (row == null) throw new ServiceException("运单数据不正确");
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setOriginalNo(stringValue(row, "originalNo"));
|
||||
waybill.setLoadingNo(stringValue(row, "loadingIdentifier", "配载标识号"));
|
||||
waybill.setVehicleNo(stringValue(row, "vehicleNo"));
|
||||
waybill.setDriverId(longValue(row, "driverId", "司机ID"));
|
||||
waybill.setDriverName(stringValue(row, "driverName"));
|
||||
waybill.setDriverPhone(stringValue(row, "driverPhone"));
|
||||
waybill.setDriverName(stringValue(row, "driverName", "司机/船长姓名", "司机/船长"));
|
||||
waybill.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号"));
|
||||
waybill.setTransportType(stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式"));
|
||||
waybill.setCargoName(stringValue(row, "cargoName"));
|
||||
waybill.setCargoType(stringValue(row, "cargoType"));
|
||||
waybill.setSpecification(stringValue(row, "specification"));
|
||||
waybill.setModel(stringValue(row, "model"));
|
||||
waybill.setQuantity(decimalValue(row, "quantity", "重量"));
|
||||
waybill.setQuantityUnit(stringValue(row, "quantityUnit"));
|
||||
waybill.setSpecification(stringValue(row, "specification", "规格"));
|
||||
waybill.setModel(stringValue(row, "model", "型号"));
|
||||
waybill.setQuantity(decimalValue(row, "quantity", "数量", "重量"));
|
||||
waybill.setQuantityUnit(stringValue(row, "quantityUnit", "数量单位"));
|
||||
waybill.setDepartureAddressId(longValue(row, "departureAddressId", "发货地址ID"));
|
||||
waybill.setDepartureName(stringValue(row, "departureName"));
|
||||
waybill.setDepartureAddress(stringValue(row, "departureAddress"));
|
||||
@@ -198,8 +223,8 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
waybill.setArrivalAddressId(longValue(row, "arrivalAddressId", "收货地址ID"));
|
||||
waybill.setArrivalName(stringValue(row, "arrivalName"));
|
||||
waybill.setArrivalAddress(stringValue(row, "arrivalAddress"));
|
||||
waybill.setArrivalContact(stringValue(row, "arrivalContact"));
|
||||
waybill.setArrivalPhone(stringValue(row, "arrivalPhone"));
|
||||
waybill.setArrivalContact(stringValue(row, "arrivalContact", "收货联系人"));
|
||||
waybill.setArrivalPhone(stringValue(row, "arrivalPhone", "收货联系人电话"));
|
||||
waybill.setCaptainName(stringValue(row, "captainName"));
|
||||
waybill.setCabinNo(stringValue(row, "cabinNo"));
|
||||
waybill.setContainerNo(stringValue(row, "containerNo"));
|
||||
@@ -207,12 +232,13 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
waybill.setEscortName(stringValue(row, "escortName"));
|
||||
waybill.setEscortPhone(stringValue(row, "escortPhone"));
|
||||
waybill.setMileage(normalizeImportMileage(decimalValue(
|
||||
row, "mileage", "里程", "里程", "里程(km)", "里程(公里)"
|
||||
row, "mileage", "里程", "里程(km)", "里程(公里)"
|
||||
)));
|
||||
waybill.setUnitPrice(decimalValue(row, "unitPrice", "单价"));
|
||||
waybill.setPriceUnit(stringValue(row, "priceUnit"));
|
||||
waybill.setOtherFeeTotal(decimalValue(row, "otherFeeTotal", "其他费用合计", "其他费用合计"));
|
||||
waybill.setOtherFeeTotal(decimalValue(row, "otherFeeTotal", "其他费用合计"));
|
||||
waybill.setRemark(stringValue(row, "remark"));
|
||||
waybill.setRelationNo(stringValue(row, "waybillIdentifier", "同一运单标识号"));
|
||||
waybill.setProjectId(batch.getProjectId());
|
||||
waybill.setProjectName(batch.getProjectName());
|
||||
waybill.setCustomerName(batch.getCustomerName());
|
||||
@@ -232,8 +258,10 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
waybill.setQuantityUnit(Func.isEmpty(waybill.getQuantityUnit()) ? "吨" : waybill.getQuantityUnit());
|
||||
waybill.setPriceUnit(Func.isEmpty(waybill.getPriceUnit()) ? "吨" : waybill.getPriceUnit());
|
||||
// 草稿允许明细不完整,时间为空时不阻断保存。
|
||||
waybill.setStartDate(parseDate(row.get("startDate"), "开始时间", !draft));
|
||||
waybill.setEndDate(parseDate(row.get("endDate"), "结束时间", !draft));
|
||||
waybill.setStartDate(parseDate(row.get("actualStartDate"), "实际发货时间", !draft, "startDate", "开始时间"));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -275,7 +303,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
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 (!required) return null;
|
||||
throw new ServiceException(fieldName + "不能为空");
|
||||
@@ -323,4 +351,354 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
long count = count(Wrappers.<WaybillImportBatch>lambdaQuery().likeRight(WaybillImportBatch::getBatchNo, prefix));
|
||||
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);
|
||||
excel.setCreateUserName(UserCache.getUserRealName(record.getCreateUser()));
|
||||
excel.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()));
|
||||
excel.setCreateTime(record.getCreateTime());
|
||||
excel.setUpdateTime(record.getUpdateTime());
|
||||
excel.setBusinessStatus(WaybillWrapper.businessStatusName(record.getBusinessStatus()));
|
||||
return excel;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ public class WaybillWrapper extends BaseEntityWrapper<Waybill, WaybillVO> {
|
||||
return waybillVO;
|
||||
}
|
||||
|
||||
private String businessStatusName(String status) {
|
||||
public static String businessStatusName(String status) {
|
||||
if (status == null) {
|
||||
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