1、调整导入运单

2、调整对账
This commit is contained in:
2026-09-09 14:43:26 +08:00
parent 0ce701ab90
commit 63af35a333
13 changed files with 812 additions and 61 deletions
@@ -71,6 +71,7 @@ public interface ISysClient {
String PARAM_VALUE = API_PREFIX + "/param-value"; String PARAM_VALUE = API_PREFIX + "/param-value";
String REGION = API_PREFIX + "/region"; String REGION = API_PREFIX + "/region";
String FEE_ITEMS = API_PREFIX + "/fee-items"; String FEE_ITEMS = API_PREFIX + "/fee-items";
String CARGO_TYPES = API_PREFIX + "/cargo-types";
/** /**
* 获取菜单 * 获取菜单
@@ -301,4 +302,7 @@ public interface ISysClient {
@GetMapping(FEE_ITEMS) @GetMapping(FEE_ITEMS)
R<List<FeeItem>> getFeeItems(); R<List<FeeItem>> getFeeItems();
@GetMapping(CARGO_TYPES)
R<List<CargoType>> getCargoTypes();
} }
@@ -164,5 +164,9 @@ public class ISysClientFallback implements ISysClient {
return R.fail("获取数据失败"); return R.fail("获取数据失败");
} }
@Override
public R<List<CargoType>> getCargoTypes() {
return R.fail("获取数据失败");
}
} }
@@ -8,6 +8,7 @@ import org.springblade.transport.pojo.entity.TransportReconciliation;
import org.springblade.transport.pojo.entity.TransportReconciliationChangeRecord; import org.springblade.transport.pojo.entity.TransportReconciliationChangeRecord;
import org.springblade.transport.pojo.entity.TransportReconciliationExternal; import org.springblade.transport.pojo.entity.TransportReconciliationExternal;
import org.springblade.transport.pojo.entity.TransportReconciliationInternal; import org.springblade.transport.pojo.entity.TransportReconciliationInternal;
import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee;
import java.io.Serial; import java.io.Serial;
import java.util.List; import java.util.List;
@@ -25,5 +26,6 @@ public class TransportReconciliationVO extends TransportReconciliation {
@TableField(exist = false) private String matchStatusName; @TableField(exist = false) private String matchStatusName;
@TableField(exist = false) private List<TransportReconciliationInternal> internalDetails; @TableField(exist = false) private List<TransportReconciliationInternal> internalDetails;
@TableField(exist = false) private List<TransportReconciliationExternal> externalDetails; @TableField(exist = false) private List<TransportReconciliationExternal> externalDetails;
@TableField(exist = false) private List<FormalSettlementSummaryFee> feeSummary;
@TableField(exist = false) private List<TransportReconciliationChangeRecord> changeRecords; @TableField(exist = false) private List<TransportReconciliationChangeRecord> changeRecords;
} }
@@ -65,6 +65,7 @@ public class SysClient implements ISysClient {
private final IRegionService regionService; private final IRegionService regionService;
private final IFeeItemService feeItemService; private final IFeeItemService feeItemService;
private final ICargoTypeService cargoTypeService;
@Override @Override
@GetMapping(MENU) @GetMapping(MENU)
@@ -212,5 +213,12 @@ public class SysClient implements ISysClient {
.orderByAsc(FeeItem::getFeeCategory, FeeItem::getName))); .orderByAsc(FeeItem::getFeeCategory, FeeItem::getName)));
} }
@Override
@GetMapping(CARGO_TYPES)
public R<List<CargoType>> getCargoTypes() {
return R.data(cargoTypeService.list(Wrappers.<CargoType>lambdaQuery()
.eq(CargoType::getIsDeleted, 0)
.orderByAsc(CargoType::getCargoCode)));
}
} }
@@ -21,8 +21,10 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.excel.util.ExcelUtil; import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.transport.excel.CargoReconciliationExcel; import org.springblade.transport.excel.CargoReconciliationExcel;
import org.springblade.transport.excel.CargoReconciliationFeeReader;
import org.springblade.transport.excel.CargoReconciliationFailureExcel; import org.springblade.transport.excel.CargoReconciliationFailureExcel;
import org.springblade.transport.excel.VehicleReconciliationExcel; import org.springblade.transport.excel.VehicleReconciliationExcel;
import org.springblade.transport.excel.VehicleReconciliationFeeReader;
import org.springblade.transport.excel.VehicleReconciliationFailureExcel; import org.springblade.transport.excel.VehicleReconciliationFailureExcel;
import org.springblade.transport.excel.TransportReconciliationExportExcel; import org.springblade.transport.excel.TransportReconciliationExportExcel;
import org.springblade.transport.pojo.dto.TransportReconciliationManualMatchRequest; import org.springblade.transport.pojo.dto.TransportReconciliationManualMatchRequest;
@@ -40,10 +42,11 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Map;
/** 运输对账单控制器。 @author Chill */ /** 运输对账单控制器。 @author Chill */
@RestController @RestController
@@ -122,7 +125,10 @@ public class TransportReconciliationController extends BladeController {
@ApiOperationSupport(order = 6) @ApiOperationSupport(order = 6)
@Operation(summary = "导入整车总额外部账单") @Operation(summary = "导入整车总额外部账单")
public R importVehicle(@RequestParam Long id, MultipartFile file, HttpServletResponse response) { public R importVehicle(@RequestParam Long id, MultipartFile file, HttpServletResponse response) {
List<VehicleReconciliationFailureExcel> failures = reconciliationService.importVehicles(id, ExcelUtil.read(file, VehicleReconciliationExcel.class)); List<VehicleReconciliationExcel> rows = ExcelUtil.read(file, VehicleReconciliationExcel.class);
List<Map<String, BigDecimal>> feeItems = VehicleReconciliationFeeReader.read(file);
for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index));
List<VehicleReconciliationFailureExcel> failures = reconciliationService.importVehicles(id, rows);
if (!failures.isEmpty()) { if (!failures.isEmpty()) {
ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, VehicleReconciliationFailureExcel.class); ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, VehicleReconciliationFailureExcel.class);
return null; return null;
@@ -134,7 +140,10 @@ public class TransportReconciliationController extends BladeController {
@ApiOperationSupport(order = 7) @ApiOperationSupport(order = 7)
@Operation(summary = "导入货物明细外部账单") @Operation(summary = "导入货物明细外部账单")
public R importCargo(@RequestParam Long id, MultipartFile file, HttpServletResponse response) { public R importCargo(@RequestParam Long id, MultipartFile file, HttpServletResponse response) {
List<CargoReconciliationFailureExcel> failures = reconciliationService.importCargoes(id, ExcelUtil.read(file, CargoReconciliationExcel.class)); List<CargoReconciliationExcel> rows = ExcelUtil.read(file, CargoReconciliationExcel.class);
List<Map<String, BigDecimal>> feeItems = CargoReconciliationFeeReader.read(file);
for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index));
List<CargoReconciliationFailureExcel> failures = reconciliationService.importCargoes(id, rows);
if (!failures.isEmpty()) { if (!failures.isEmpty()) {
ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, CargoReconciliationFailureExcel.class); ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, CargoReconciliationFailureExcel.class);
return null; return null;
@@ -15,6 +15,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Map;
/** 货物明细对账导入模型。 @author Chill */ /** 货物明细对账导入模型。 @author Chill */
@Data @Data
@@ -34,8 +35,9 @@ public class CargoReconciliationExcel implements Serializable {
@ExcelProperty("运输单价") @NumberFormat("0.00") private BigDecimal unitPrice; @ExcelProperty("运输单价") @NumberFormat("0.00") private BigDecimal unitPrice;
@ExcelProperty("里程(KM") @NumberFormat("0.00") private BigDecimal mileage; @ExcelProperty("里程(KM") @NumberFormat("0.00") private BigDecimal mileage;
@ExcelProperty("运输费") @NumberFormat("0.00") private BigDecimal freightAmount; @ExcelProperty("运输费") @NumberFormat("0.00") private BigDecimal freightAmount;
@ExcelProperty("用项目名称1") @NumberFormat("0.00") private BigDecimal feeItemOne; @ExcelProperty("") @NumberFormat("0.00") private BigDecimal feeItemOne;
@ExcelProperty("费用项目名称2") @NumberFormat("0.00") private BigDecimal feeItemTwo; @ExcelProperty("罚款") @NumberFormat("0.00") private BigDecimal feeItemTwo;
@ExcelProperty("结算费用合计") @NumberFormat("0.00") private BigDecimal settlementAmount; @ExcelProperty("结算费用合计") @NumberFormat("0.00") private BigDecimal settlementAmount;
@ExcelIgnore private Map<String, BigDecimal> feeItems;
@ExcelIgnore private String errorMessage; @ExcelIgnore private String errorMessage;
} }
@@ -0,0 +1,106 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.excel;
import cn.idev.excel.FastExcel;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.event.AnalysisEventListener;
import cn.idev.excel.metadata.data.ReadCellData;
import org.springblade.core.log.exception.ServiceException;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** 货物明细对账动态费用读取器。 @author Chill */
public final class CargoReconciliationFeeReader {
private CargoReconciliationFeeReader() {
}
public static List<Map<String, BigDecimal>> read(MultipartFile file) {
try (InputStream inputStream = file.getInputStream()) {
FeeListener listener = new FeeListener();
FastExcel.read(inputStream)
.useDefaultListener(false)
.registerReadListener(listener)
.sheet()
.doRead();
return listener.getFeeItems();
} catch (IOException exception) {
throw new ServiceException("读取货物明细对账费用列失败");
}
}
private static final class FeeListener extends AnalysisEventListener<Map<Integer, ReadCellData<?>>> {
private final List<Map<String, BigDecimal>> feeItems = new ArrayList<>();
private Map<Integer, String> headers = Map.of();
private int freightColumn = -1;
private int settlementColumn = -1;
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
headers = headMap;
freightColumn = findColumn("运输费");
settlementColumn = findColumn("结算费用合计");
}
@Override
public void invoke(Map<Integer, ReadCellData<?>> row, AnalysisContext context) {
Map<String, BigDecimal> values = new LinkedHashMap<>();
if (freightColumn >= 0 && settlementColumn > freightColumn) {
for (int column = freightColumn + 1; column < settlementColumn; column++) {
String name = headers.get(column);
if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column)));
}
}
feeItems.add(values);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
private int findColumn(String header) {
return headers.entrySet().stream()
.filter(entry -> header.equals(entry.getValue()))
.mapToInt(Map.Entry::getKey)
.findFirst().orElse(-1);
}
private BigDecimal decimal(ReadCellData<?> cellData) {
if (cellData == null) return BigDecimal.ZERO.setScale(2);
Object value = cellData.getData();
if (value == null) {
value = switch (cellData.getType()) {
case NUMBER -> cellData.getNumberValue();
case STRING, DIRECT_STRING, ERROR -> cellData.getStringValue();
case BOOLEAN -> cellData.getBooleanValue();
default -> null;
};
}
if (value == null || String.valueOf(value).isBlank()) return BigDecimal.ZERO.setScale(2);
try {
return new BigDecimal(String.valueOf(value).trim()).setScale(2);
} catch (NumberFormatException exception) {
return BigDecimal.ZERO.setScale(2);
}
}
private List<Map<String, BigDecimal>> getFeeItems() {
return feeItems;
}
}
}
@@ -15,6 +15,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Map;
/** 整车总额对账导入模型。 @author Chill */ /** 整车总额对账导入模型。 @author Chill */
@Data @Data
@@ -36,5 +37,6 @@ public class VehicleReconciliationExcel implements Serializable {
@ExcelProperty("运输费") @NumberFormat("0.00") private BigDecimal freightAmount; @ExcelProperty("运输费") @NumberFormat("0.00") private BigDecimal freightAmount;
@ExcelProperty("费用项目1") @NumberFormat("0.00") private BigDecimal feeItemOne; @ExcelProperty("费用项目1") @NumberFormat("0.00") private BigDecimal feeItemOne;
@ExcelProperty("结算费用合计") @NumberFormat("0.00") private BigDecimal settlementAmount; @ExcelProperty("结算费用合计") @NumberFormat("0.00") private BigDecimal settlementAmount;
@ExcelIgnore private Map<String, BigDecimal> feeItems;
@ExcelIgnore private String errorMessage; @ExcelIgnore private String errorMessage;
} }
@@ -0,0 +1,106 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.excel;
import cn.idev.excel.FastExcel;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.event.AnalysisEventListener;
import cn.idev.excel.metadata.data.ReadCellData;
import org.springblade.core.log.exception.ServiceException;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** 整车总额对账动态费用读取器。 @author Chill */
public final class VehicleReconciliationFeeReader {
private VehicleReconciliationFeeReader() {
}
public static List<Map<String, BigDecimal>> read(MultipartFile file) {
try (InputStream inputStream = file.getInputStream()) {
FeeListener listener = new FeeListener();
FastExcel.read(inputStream)
.useDefaultListener(false)
.registerReadListener(listener)
.sheet()
.doRead();
return listener.getFeeItems();
} catch (IOException exception) {
throw new ServiceException("读取整车总额对账费用列失败");
}
}
private static final class FeeListener extends AnalysisEventListener<Map<Integer, ReadCellData<?>>> {
private final List<Map<String, BigDecimal>> feeItems = new ArrayList<>();
private Map<Integer, String> headers = Map.of();
private int freightColumn = -1;
private int settlementColumn = -1;
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
headers = headMap;
freightColumn = findColumn("运输费");
settlementColumn = findColumn("结算费用合计");
}
@Override
public void invoke(Map<Integer, ReadCellData<?>> row, AnalysisContext context) {
Map<String, BigDecimal> values = new LinkedHashMap<>();
if (freightColumn >= 0 && settlementColumn > freightColumn) {
for (int column = freightColumn + 1; column < settlementColumn; column++) {
String name = headers.get(column);
if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column)));
}
}
feeItems.add(values);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
private int findColumn(String header) {
return headers.entrySet().stream()
.filter(entry -> header.equals(entry.getValue()))
.mapToInt(Map.Entry::getKey)
.findFirst().orElse(-1);
}
private BigDecimal decimal(ReadCellData<?> cellData) {
if (cellData == null) return BigDecimal.ZERO.setScale(2);
Object value = cellData.getData();
if (value == null) {
value = switch (cellData.getType()) {
case NUMBER -> cellData.getNumberValue();
case STRING, DIRECT_STRING, ERROR -> cellData.getStringValue();
case BOOLEAN -> cellData.getBooleanValue();
default -> null;
};
}
if (value == null || String.valueOf(value).isBlank()) return BigDecimal.ZERO.setScale(2);
try {
return new BigDecimal(String.valueOf(value).trim()).setScale(2);
} catch (NumberFormatException exception) {
return BigDecimal.ZERO.setScale(2);
}
}
private List<Map<String, BigDecimal>> getFeeItems() {
return feeItems;
}
}
}
@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService; import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.excel.LoadingManageExcel; import org.springblade.transport.excel.LoadingManageExcel;
import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.entity.LoadingManage;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.LoadingCarrierContractVO; import org.springblade.transport.pojo.vo.LoadingCarrierContractVO;
import org.springblade.transport.pojo.vo.LoadingManageVO; import org.springblade.transport.pojo.vo.LoadingManageVO;
@@ -31,6 +32,14 @@ public interface ILoadingManageService extends BaseService<LoadingManage> {
boolean submit(LoadingManage loadingManage); boolean submit(LoadingManage loadingManage);
/**
* 根据导入运单创建配载单并建立运单关联,保留导入运单的业务状态。
*
* @param loadingNo 配载标识号
* @param waybills 已导入的运单
*/
void createFromImportedWaybills(String loadingNo, List<Waybill> waybills);
BusinessRemoveResultVO removeLoadingManage(String ids); BusinessRemoveResultVO removeLoadingManage(String ids);
List<LoadingManageExcel> exportLoadingManage(LoadingManageVO loadingManage, String ids); List<LoadingManageExcel> exportLoadingManage(LoadingManageVO loadingManage, String ids);
@@ -42,6 +42,7 @@ import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors;
/** /**
* 配载管理 服务实现类 * 配载管理 服务实现类
@@ -149,6 +150,68 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
return result; return result;
} }
@Override
@Transactional(rollbackFor = Exception.class)
public void createFromImportedWaybills(String loadingNo, List<Waybill> waybills) {
String normalizedLoadingNo = TransportBusinessSupport.trimToNull(loadingNo);
if (Func.isEmpty(normalizedLoadingNo) || Func.isEmpty(waybills)) {
return;
}
LoadingManage existing = getOne(Wrappers.<LoadingManage>lambdaQuery()
.eq(LoadingManage::getLoadingNo, normalizedLoadingNo)
.eq(LoadingManage::getIsDeleted, 0), false);
if (existing != null) {
throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo);
}
Waybill first = waybills.get(0);
LoadingManage loadingManage = new LoadingManage();
loadingManage.setLoadingNo(normalizedLoadingNo);
loadingManage.setLoadingSubNos(waybills.stream()
.map(Waybill::getWaybillNo)
.filter(Func::isNotEmpty)
.collect(Collectors.joining(",")));
loadingManage.setWaybillIdsJson(JsonUtil.toJson(waybills.stream().map(Waybill::getId).toList()));
loadingManage.setProjectId(first.getProjectId());
loadingManage.setProjectName(first.getProjectName());
loadingManage.setCustomerName(first.getCustomerName());
loadingManage.setTransportType(first.getTransportType());
loadingManage.setCargoType(first.getCargoType());
loadingManage.setCargoName(first.getCargoName());
loadingManage.setVehicleNo(first.getVehicleNo());
loadingManage.setTrailerVehicleNo(first.getTrailerVehicleNo());
loadingManage.setDriverName(first.getDriverName());
loadingManage.setDriverPhone(first.getDriverPhone());
loadingManage.setEscortName(first.getEscortName());
loadingManage.setEscortPhone(first.getEscortPhone());
loadingManage.setCarrierType(first.getCarrierType());
loadingManage.setCarrierName(first.getCarrierName());
loadingManage.setCarrierContractId(first.getCarrierContractId());
loadingManage.setDepartureAddress(first.getDepartureAddress());
loadingManage.setArrivalAddress(first.getArrivalAddress());
loadingManage.setOriginalNo(first.getOriginalNo());
loadingManage.setDataSource("批量导入");
loadingManage.setStartDate(first.getStartDate());
loadingManage.setEndDate(first.getEndDate());
loadingManage.setPlanName(first.getPlanName());
loadingManage.setBatchNo(first.getBatchNo());
loadingManage.setCurrentProcessNode(first.getCurrentProcessNode());
loadingManage.setMileage(first.getMileage());
loadingManage.setEstimatedStartDate(first.getEstimatedStartTime());
loadingManage.setEstimatedEndDate(first.getEstimatedEndTime());
loadingManage.setTaskRemark(first.getTaskRemark());
loadingManage.setGoodsJson(first.getGoodsJson());
loadingManage.setRouteJson(first.getRouteJson());
loadingManage.setTaskInfoJson(first.getTaskInfoJson());
loadingManage.setDeptId(first.getDeptId());
loadingManage.setDeptName(first.getDeptName());
loadingManage.setBusinessStatus(STATUS_COMPLETED);
save(loadingManage);
waybillMapper.update(null, Wrappers.<Waybill>lambdaUpdate()
.in(Waybill::getId, waybills.stream().map(Waybill::getId).toList())
.set(Waybill::getLoadingNo, normalizedLoadingNo));
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public BusinessRemoveResultVO removeLoadingManage(String ids) { public BusinessRemoveResultVO removeLoadingManage(String ids) {
@@ -22,6 +22,7 @@ import org.springblade.transport.excel.VehicleReconciliationExcel;
import org.springblade.transport.excel.VehicleReconciliationFailureExcel; import org.springblade.transport.excel.VehicleReconciliationFailureExcel;
import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper; import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper;
import org.springblade.transport.mapper.FormalSettlementDetailMapper; import org.springblade.transport.mapper.FormalSettlementDetailMapper;
import org.springblade.transport.mapper.FormalSettlementSummaryFeeMapper;
import org.springblade.transport.mapper.FormalSettlementMapper; import org.springblade.transport.mapper.FormalSettlementMapper;
import org.springblade.transport.mapper.FormalSettlementSourceMapper; import org.springblade.transport.mapper.FormalSettlementSourceMapper;
import org.springblade.transport.mapper.LoadingManageMapper; import org.springblade.transport.mapper.LoadingManageMapper;
@@ -38,6 +39,7 @@ import org.springblade.transport.pojo.dto.TransportReconciliationSaveRequest;
import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.FormalSettlementDetail; import org.springblade.transport.pojo.entity.FormalSettlementDetail;
import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; import org.springblade.transport.pojo.entity.FormalSettlementDetailFee;
import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee;
import org.springblade.transport.pojo.entity.FormalSettlementSource; import org.springblade.transport.pojo.entity.FormalSettlementSource;
import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.entity.LoadingManage;
import org.springblade.transport.pojo.entity.MasterOrder; import org.springblade.transport.pojo.entity.MasterOrder;
@@ -61,9 +63,13 @@ import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -91,6 +97,7 @@ public class TransportReconciliationServiceImpl
private final FormalSettlementSourceMapper formalSourceMapper; private final FormalSettlementSourceMapper formalSourceMapper;
private final FormalSettlementDetailMapper formalDetailMapper; private final FormalSettlementDetailMapper formalDetailMapper;
private final FormalSettlementDetailFeeMapper formalDetailFeeMapper; private final FormalSettlementDetailFeeMapper formalDetailFeeMapper;
private final FormalSettlementSummaryFeeMapper formalSummaryFeeMapper;
private final ReceivablePayableDetailMapper receivablePayableMapper; private final ReceivablePayableDetailMapper receivablePayableMapper;
private final ReceivablePayableCargoFeeMapper cargoFeeMapper; private final ReceivablePayableCargoFeeMapper cargoFeeMapper;
private final WaybillMapper waybillMapper; private final WaybillMapper waybillMapper;
@@ -141,8 +148,11 @@ public class TransportReconciliationServiceImpl
if (Func.isEmpty(vo.getCustomerName())) { if (Func.isEmpty(vo.getCustomerName())) {
vo.setCustomerName("receivable".equals(vo.getSettlementType()) ? vo.getPayerName() : vo.getPayeeName()); vo.setCustomerName("receivable".equals(vo.getSettlementType()) ? vo.getPayerName() : vo.getPayeeName());
} }
vo.setInternalDetails(internalRows(id)); List<TransportReconciliationInternal> internals = internalRows(id);
vo.setExternalDetails(externalRows(id)); List<TransportReconciliationExternal> externals = externalRows(id);
vo.setInternalDetails(internals);
vo.setExternalDetails(externals);
vo.setFeeSummary(feeSummary(vo.getFormalSettlementId(), externals));
vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.<TransportReconciliationChangeRecord>lambdaQuery() vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.<TransportReconciliationChangeRecord>lambdaQuery()
.eq(TransportReconciliationChangeRecord::getReconciliationId, id) .eq(TransportReconciliationChangeRecord::getReconciliationId, id)
.orderByDesc(TransportReconciliationChangeRecord::getChangeTime))); .orderByDesc(TransportReconciliationChangeRecord::getChangeTime)));
@@ -264,7 +274,12 @@ public class TransportReconciliationServiceImpl
external.setExternalLineNo(index + 1); external.setExternalLineNo(index + 1);
external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间")); external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间"));
external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间"));
external.setFeeItemsJson(JsonUtil.toJson(Map.of("费用项目1", money(row.getFeeItemOne())))); Map<String, BigDecimal> feeItems = row.getFeeItems();
if (feeItems == null || feeItems.isEmpty()) {
feeItems = new HashMap<>();
feeItems.put("费用项目1", money(row.getFeeItemOne()));
}
external.setFeeItemsJson(JsonUtil.toJson(feeItems));
external.setMatchStatus(UNMATCHED); external.setMatchStatus(UNMATCHED);
external.setSuspectedDuplicate(false); external.setSuspectedDuplicate(false);
external.setRawDataJson(JsonUtil.toJson(row)); external.setRawDataJson(JsonUtil.toJson(row));
@@ -274,9 +289,10 @@ public class TransportReconciliationServiceImpl
private List<String> validateImportVehicle(VehicleReconciliationExcel row) { private List<String> validateImportVehicle(VehicleReconciliationExcel row) {
List<String> validationErrors = new ArrayList<>(); List<String> validationErrors = new ArrayList<>();
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors,
Func.isEmpty(row.getVehicleNo()), "车牌号不能为空"); row == null || Func.isEmpty(row.getVehicleNo()), "车牌号不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors,
Func.isEmpty(row.getCargoName()), "货物名称不能为空"); row == null || Func.isEmpty(row.getCargoName()), "货物名称不能为空");
if (row == null) return validationErrors;
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors,
Func.isEmpty(row.getActualDepartureTime()), "实际发货时间不能为空"); Func.isEmpty(row.getActualDepartureTime()), "实际发货时间不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors,
@@ -296,7 +312,11 @@ public class TransportReconciliationServiceImpl
addImportDecimalErrors(validationErrors, row.getMileage(), "里程(KM", 2); addImportDecimalErrors(validationErrors, row.getMileage(), "里程(KM", 2);
addImportDecimalErrors(validationErrors, row.getUnitPrice(), "运输单价", 2); addImportDecimalErrors(validationErrors, row.getUnitPrice(), "运输单价", 2);
addImportDecimalErrors(validationErrors, row.getFreightAmount(), "运输费", 2); addImportDecimalErrors(validationErrors, row.getFreightAmount(), "运输费", 2);
if (row.getFeeItems() != null && !row.getFeeItems().isEmpty()) {
row.getFeeItems().forEach((name, value) -> addImportDecimalErrors(validationErrors, value, name, 2));
} else {
addImportDecimalErrors(validationErrors, row.getFeeItemOne(), "费用项目1", 2); addImportDecimalErrors(validationErrors, row.getFeeItemOne(), "费用项目1", 2);
}
addImportDecimalErrors(validationErrors, row.getSettlementAmount(), "结算费用合计", 2); addImportDecimalErrors(validationErrors, row.getSettlementAmount(), "结算费用合计", 2);
LocalDateTime departureTime = parseImportTime(row.getActualDepartureTime(), "实际发货时间", validationErrors); LocalDateTime departureTime = parseImportTime(row.getActualDepartureTime(), "实际发货时间", validationErrors);
@@ -372,7 +392,13 @@ public class TransportReconciliationServiceImpl
external.setExternalLineNo(index + 1); external.setExternalLineNo(index + 1);
external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间")); external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间"));
external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间"));
external.setFeeItemsJson(JsonUtil.toJson(Map.of("费用项目名称1", money(row.getFeeItemOne()), "费用项目名称2", money(row.getFeeItemTwo())))); Map<String, BigDecimal> feeItems = row.getFeeItems();
if (feeItems == null || feeItems.isEmpty()) {
feeItems = new HashMap<>();
feeItems.put("水费", money(row.getFeeItemOne()));
feeItems.put("罚款", money(row.getFeeItemTwo()));
}
external.setFeeItemsJson(JsonUtil.toJson(feeItems));
external.setMatchStatus(UNMATCHED); external.setMatchStatus(UNMATCHED);
external.setSuspectedDuplicate(false); external.setSuspectedDuplicate(false);
external.setRawDataJson(JsonUtil.toJson(row)); external.setRawDataJson(JsonUtil.toJson(row));
@@ -408,8 +434,12 @@ public class TransportReconciliationServiceImpl
addImportDecimalErrors(validationErrors, row.getMileage(), "里程(KM", 2); addImportDecimalErrors(validationErrors, row.getMileage(), "里程(KM", 2);
addImportDecimalErrors(validationErrors, row.getUnitPrice(), "运输单价", 2); addImportDecimalErrors(validationErrors, row.getUnitPrice(), "运输单价", 2);
addImportDecimalErrors(validationErrors, row.getFreightAmount(), "运输费", 2); addImportDecimalErrors(validationErrors, row.getFreightAmount(), "运输费", 2);
addImportDecimalErrors(validationErrors, row.getFeeItemOne(), "费用项目名称1", 2); if (row.getFeeItems() != null) {
addImportDecimalErrors(validationErrors, row.getFeeItemTwo(), "费用项目名称2", 2); row.getFeeItems().forEach((name, value) -> addImportDecimalErrors(validationErrors, value, name, 2));
} else {
addImportDecimalErrors(validationErrors, row.getFeeItemOne(), "水费", 2);
addImportDecimalErrors(validationErrors, row.getFeeItemTwo(), "罚款", 2);
}
addImportDecimalErrors(validationErrors, row.getSettlementAmount(), "结算费用合计", 2); addImportDecimalErrors(validationErrors, row.getSettlementAmount(), "结算费用合计", 2);
LocalDateTime departureTime = parseImportTime(row.getActualDepartureTime(), "实际发货时间", validationErrors); LocalDateTime departureTime = parseImportTime(row.getActualDepartureTime(), "实际发货时间", validationErrors);
@@ -420,6 +450,7 @@ public class TransportReconciliationServiceImpl
return validationErrors; return validationErrors;
} }
@Override @Override
public TransportReconciliationVO matchPreview(TransportReconciliationVO request) { public TransportReconciliationVO matchPreview(TransportReconciliationVO request) {
if (request == null || request.getId() == null) throw new ServiceException("运输对账单不存在"); if (request == null || request.getId() == null) throw new ServiceException("运输对账单不存在");
@@ -430,6 +461,7 @@ public class TransportReconciliationServiceImpl
if (externals == null) externals = externalRows(request.getId()); if (externals == null) externals = externalRows(request.getId());
if (externals.isEmpty()) throw new ServiceException("请先导入外部账单"); if (externals.isEmpty()) throw new ServiceException("请先导入外部账单");
resetPreviewMatches(internals, externals); resetPreviewMatches(internals, externals);
markSuspectedDuplicates(externals, false);
Map<String, List<TransportReconciliationExternal>> externalGroups = externals.stream() Map<String, List<TransportReconciliationExternal>> externalGroups = externals.stream()
.collect(Collectors.groupingBy(this::matchKey)); .collect(Collectors.groupingBy(this::matchKey));
Map<String, List<TransportReconciliationInternal>> internalGroups = internals.stream() Map<String, List<TransportReconciliationInternal>> internalGroups = internals.stream()
@@ -439,11 +471,6 @@ public class TransportReconciliationServiceImpl
List<TransportReconciliationInternal> internalGroup = internalGroups.getOrDefault(entry.getKey(), List.of()); List<TransportReconciliationInternal> internalGroup = internalGroups.getOrDefault(entry.getKey(), List.of());
if (externalGroup.size() == 1 && internalGroup.size() == 1) { if (externalGroup.size() == 1 && internalGroup.size() == 1) {
linkPreview(internalGroup.get(0), externalGroup.get(0)); 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()); TransportReconciliationVO result = detail(request.getId());
@@ -459,6 +486,9 @@ public class TransportReconciliationServiceImpl
if (request == null || request.getId() == null) throw new ServiceException("运输对账单不存在"); if (request == null || request.getId() == null) throw new ServiceException("运输对账单不存在");
TransportReconciliation bill = editable(request.getId()); TransportReconciliation bill = editable(request.getId());
applySnapshots(bill, request.getInternalDetails(), request.getExternalDetails()); applySnapshots(bill, request.getInternalDetails(), request.getExternalDetails());
if (request.getFeeSummary() != null) {
applyCompletionSummaryFees(bill.getFormalSettlementId(), request.getFeeSummary());
}
if (request.getReconciliationDate() != null) bill.setReconciliationDate(request.getReconciliationDate()); if (request.getReconciliationDate() != null) bill.setReconciliationDate(request.getReconciliationDate());
bill.setRemark(limit(request.getRemark(), 200)); bill.setRemark(limit(request.getRemark(), 200));
updateById(bill); updateById(bill);
@@ -467,6 +497,93 @@ public class TransportReconciliationServiceImpl
return detail(bill.getId()); return detail(bill.getId());
} }
/**
* 保存汇总对账弹窗中的费用行,并同步正式结算单金额。
*/
private void applyCompletionSummaryFees(Long formalSettlementId, List<FormalSettlementSummaryFee> incomingRows) {
if (formalSettlementId == null) throw new ServiceException("正式结算单不存在");
List<FormalSettlementSummaryFee> existingRows = formalSummaryFeeMapper.selectList(Wrappers.<FormalSettlementSummaryFee>lambdaQuery()
.eq(FormalSettlementSummaryFee::getFormalSettlementId, formalSettlementId)
.eq(FormalSettlementSummaryFee::getIsDeleted, 0));
Map<Long, FormalSettlementSummaryFee> existingById = existingRows.stream()
.filter(row -> row.getId() != null)
.collect(Collectors.toMap(FormalSettlementSummaryFee::getId, Function.identity()));
Set<Long> retainedManualIds = new HashSet<>();
for (FormalSettlementSummaryFee incoming : incomingRows) {
if (incoming == null) continue;
boolean manual = Integer.valueOf(1).equals(incoming.getManualFlag());
FormalSettlementSummaryFee row = incoming.getId() == null ? null : existingById.get(incoming.getId());
if (manual) {
if (row != null && !Integer.valueOf(1).equals(row.getManualFlag())) {
throw new ServiceException("系统生成费用行不允许改为手工费用");
}
if (Func.isEmpty(incoming.getFeeItem())) throw new ServiceException("手工费用项目不能为空");
BigDecimal adjustAmount = money(incoming.getAdjustAmount());
if ("补款".equals(incoming.getFeeItem()) && adjustAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("补款调整金额必须大于0");
}
if ("扣款".equals(incoming.getFeeItem()) && adjustAmount.compareTo(BigDecimal.ZERO) >= 0) {
throw new ServiceException("扣款调整金额必须小于0");
}
String feeType = Func.isEmpty(incoming.getFeeType()) ? "其他费用" : limit(incoming.getFeeType(), 50);
String feeItem = limit(incoming.getFeeItem(), 50);
if (row == null) {
row = new FormalSettlementSummaryFee();
row.setFormalSettlementId(formalSettlementId);
row.setLineNo(existingRows.size() + 1);
row.setFeeType(feeType);
row.setFeeItem(feeItem);
row.setManualFlag(1);
row.setOriginalAmount(BigDecimal.ZERO);
}
row.setFeeType(feeType);
row.setFeeItem(feeItem);
row.setOriginalAmount(BigDecimal.ZERO);
row.setAdjustAmount(adjustAmount);
row.setSettlementAmount(adjustAmount);
row.setRemark(limit(incoming.getRemark(), 200));
row.setManualFlag(1);
if (row.getId() == null) formalSummaryFeeMapper.insert(row);
else formalSummaryFeeMapper.updateById(row);
if (row.getId() != null) retainedManualIds.add(row.getId());
continue;
}
if (row == null || Integer.valueOf(1).equals(row.getManualFlag())) {
throw new ServiceException("系统生成费用行不存在");
}
BigDecimal originalAmount = money(row.getOriginalAmount());
BigDecimal adjustAmount = money(incoming.getAdjustAmount());
row.setOriginalAmount(originalAmount);
row.setAdjustAmount(adjustAmount);
row.setSettlementAmount(originalAmount.add(adjustAmount));
row.setRemark(limit(incoming.getRemark(), 200));
formalSummaryFeeMapper.updateById(row);
}
for (FormalSettlementSummaryFee row : existingRows) {
if (Integer.valueOf(1).equals(row.getManualFlag()) && !retainedManualIds.contains(row.getId())) {
formalSummaryFeeMapper.deleteById(row.getId());
}
}
List<FormalSettlementSummaryFee> finalRows = formalSummaryFeeMapper.selectList(Wrappers.<FormalSettlementSummaryFee>lambdaQuery()
.eq(FormalSettlementSummaryFee::getFormalSettlementId, formalSettlementId)
.eq(FormalSettlementSummaryFee::getIsDeleted, 0)
.orderByAsc(FormalSettlementSummaryFee::getLineNo));
BigDecimal total = BigDecimal.ZERO;
int lineNo = 1;
for (FormalSettlementSummaryFee row : finalRows) {
row.setLineNo(lineNo++);
row.setSettlementAmount(money(row.getOriginalAmount()).add(money(row.getAdjustAmount())));
total = total.add(money(row.getSettlementAmount()));
formalSummaryFeeMapper.updateById(row);
}
FormalSettlement formal = formalSettlementMapper.selectById(formalSettlementId);
if (formal == null) throw new ServiceException("正式结算单不存在");
formal.setSettlementAmount(total);
formal.setLocalSettlementAmount(total.multiply(formal.getExchangeRate() == null ? BigDecimal.ONE : formal.getExchangeRate()));
formal.setRemainingPayableAmount(total.subtract(money(formal.getPaidAmount())).max(BigDecimal.ZERO));
formalSettlementMapper.updateById(formal);
}
private void applySnapshots(TransportReconciliation bill, private void applySnapshots(TransportReconciliation bill,
List<TransportReconciliationInternal> incomingInternals, List<TransportReconciliationInternal> incomingInternals,
List<TransportReconciliationExternal> incomingExternals) { List<TransportReconciliationExternal> incomingExternals) {
@@ -516,7 +633,8 @@ public class TransportReconciliationServiceImpl
target.setUpdateMessage(limit(source.getUpdateMessage(), 200)); target.setUpdateMessage(limit(source.getUpdateMessage(), 200));
} }
private void applyExternalSnapshot(TransportReconciliationExternal target, TransportReconciliationExternal source) { private void applyExternalSnapshot(TransportReconciliationExternal target,
TransportReconciliationExternal source) {
if (source.getReconciliationId() != null && !Objects.equals(source.getReconciliationId(), target.getReconciliationId())) { if (source.getReconciliationId() != null && !Objects.equals(source.getReconciliationId(), target.getReconciliationId())) {
throw new ServiceException("外部账单明细不属于当前对账单"); throw new ServiceException("外部账单明细不属于当前对账单");
} }
@@ -551,6 +669,7 @@ public class TransportReconciliationServiceImpl
List<TransportReconciliationExternal> externals = externalRows(id); List<TransportReconciliationExternal> externals = externalRows(id);
if (externals.isEmpty()) throw new ServiceException("请先导入外部账单"); if (externals.isEmpty()) throw new ServiceException("请先导入外部账单");
resetMatches(internals, externals); resetMatches(internals, externals);
markSuspectedDuplicates(externals, true);
Map<String, List<TransportReconciliationExternal>> externalGroups = externals.stream() Map<String, List<TransportReconciliationExternal>> externalGroups = externals.stream()
.collect(Collectors.groupingBy(this::matchKey)); .collect(Collectors.groupingBy(this::matchKey));
Map<String, List<TransportReconciliationInternal>> internalGroups = internals.stream() Map<String, List<TransportReconciliationInternal>> internalGroups = internals.stream()
@@ -560,10 +679,6 @@ public class TransportReconciliationServiceImpl
List<TransportReconciliationInternal> internalGroup = internalGroups.getOrDefault(entry.getKey(), List.of()); List<TransportReconciliationInternal> internalGroup = internalGroups.getOrDefault(entry.getKey(), List.of());
if (externalGroup.size() == 1 && internalGroup.size() == 1) { if (externalGroup.size() == 1 && internalGroup.size() == 1) {
link(internalGroup.get(0), externalGroup.get(0)); link(internalGroup.get(0), externalGroup.get(0));
} else if (externalGroup.size() > 1) {
for (TransportReconciliationExternal external : externalGroup) {
external.setSuspectedDuplicate(true); external.setMatchStatus(DUPLICATE); externalMapper.updateById(external);
}
} }
} }
refreshStats(id); refreshStats(id);
@@ -632,6 +747,7 @@ public class TransportReconciliationServiceImpl
internalMapper.updateById(internal); internalMapper.updateById(internal);
updateWaybill(internal, external); updateWaybill(internal, external);
} }
refreshFormalSummaryFees(bill.getFormalSettlementId());
recalculateSettlement(bill); recalculateSettlement(bill);
bill.setBillUpdated(true); bill.setBillUpdated(true);
updateById(bill); updateById(bill);
@@ -643,6 +759,7 @@ public class TransportReconciliationServiceImpl
List<TransportReconciliationExternal> externals = externalRows(reconciliationId); List<TransportReconciliationExternal> externals = externalRows(reconciliationId);
if (externals.isEmpty()) throw new ServiceException("请先导入外部账单"); if (externals.isEmpty()) throw new ServiceException("请先导入外部账单");
resetMatches(internals, externals); resetMatches(internals, externals);
markSuspectedDuplicates(externals, true);
Map<String, List<TransportReconciliationExternal>> externalGroups = externals.stream() Map<String, List<TransportReconciliationExternal>> externalGroups = externals.stream()
.collect(Collectors.groupingBy(this::matchKey)); .collect(Collectors.groupingBy(this::matchKey));
Map<String, List<TransportReconciliationInternal>> internalGroups = internals.stream() Map<String, List<TransportReconciliationInternal>> internalGroups = internals.stream()
@@ -652,12 +769,6 @@ public class TransportReconciliationServiceImpl
List<TransportReconciliationInternal> internalGroup = internalGroups.getOrDefault(entry.getKey(), List.of()); List<TransportReconciliationInternal> internalGroup = internalGroups.getOrDefault(entry.getKey(), List.of());
if (externalGroup.size() == 1 && internalGroup.size() == 1) { if (externalGroup.size() == 1 && internalGroup.size() == 1) {
link(internalGroup.get(0), externalGroup.get(0)); link(internalGroup.get(0), externalGroup.get(0));
} else if (externalGroup.size() > 1) {
for (TransportReconciliationExternal external : externalGroup) {
external.setSuspectedDuplicate(true);
external.setMatchStatus(DUPLICATE);
externalMapper.updateById(external);
}
} }
} }
return assertAllMatched(existing(reconciliationId)); return assertAllMatched(existing(reconciliationId));
@@ -771,6 +882,19 @@ public class TransportReconciliationServiceImpl
ReceivablePayableCargoFee sourceFee = fee.getSourceFeeId() == null ? null : cargoFeeMapper.selectById(fee.getSourceFeeId()); ReceivablePayableCargoFee sourceFee = fee.getSourceFeeId() == null ? null : cargoFeeMapper.selectById(fee.getSourceFeeId());
if (sourceFee != null) { row.setSpecification(sourceFee.getSpecification()); row.setModel(sourceFee.getModel()); } if (sourceFee != null) { row.setSpecification(sourceFee.getSpecification()); row.setModel(sourceFee.getModel()); }
} else { } else {
List<FormalSettlementDetailFee> detailFees = formalDetailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
.eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId())
.eq(FormalSettlementDetailFee::getIsDeleted, 0));
if (!detailFees.isEmpty()) {
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
for (FormalSettlementDetailFee detailFee : detailFees) {
parseFeeItems(detailFee.getFeeItemsJson()).forEach((name, amount) ->
feeItems.merge(name, money(amount), BigDecimal::add));
}
row.setFeeItemsJson(JsonUtil.toJson(feeItems));
row.setFreightAmount(detailFees.stream().map(FormalSettlementDetailFee::getFreightAmount)
.map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add));
}
row.setSettlementAmount(detail.getSettlementAmountTax()); row.setSettlementAmount(detail.getSettlementAmountTax());
} }
internalMapper.insert(row); internalMapper.insert(row);
@@ -820,14 +944,19 @@ public class TransportReconciliationServiceImpl
private void applyAmount(TransportReconciliation bill, TransportReconciliationInternal internal, TransportReconciliationExternal external) { private void applyAmount(TransportReconciliation bill, TransportReconciliationInternal internal, TransportReconciliationExternal external) {
BigDecimal before = money(internal.getSettlementAmount()); BigDecimal before = money(internal.getSettlementAmount());
BigDecimal after = money(external.getSettlementAmount()); BigDecimal after = money(external.getSettlementAmount());
updateFormalSettlementHeader(internal, external, after);
if (internal.getFormalSettlementDetailFeeId() != null) { if (internal.getFormalSettlementDetailFeeId() != null) {
FormalSettlementDetailFee fee = formalDetailFeeMapper.selectById(internal.getFormalSettlementDetailFeeId()); FormalSettlementDetailFee fee = formalDetailFeeMapper.selectById(internal.getFormalSettlementDetailFeeId());
fee.setSettlementAmountTax(after); fee.setAdjustAmount(after.subtract(money(fee.getOriginalAmount()))); formalDetailFeeMapper.updateById(fee); if (fee != null) {
copyExternalToDetailFee(fee, external, after);
formalDetailFeeMapper.updateById(fee);
}
if (internal.getSourceCargoFeeId() != null) { if (internal.getSourceCargoFeeId() != null) {
ReceivablePayableCargoFee sourceFee = cargoFeeMapper.selectById(internal.getSourceCargoFeeId()); ReceivablePayableCargoFee sourceFee = cargoFeeMapper.selectById(internal.getSourceCargoFeeId());
if (sourceFee != null) { sourceFee.setAfterAmount(after); sourceFee.setAdjustAmount(after.subtract(money(sourceFee.getOriginalAmount()))); cargoFeeMapper.updateById(sourceFee); } if (sourceFee != null) { sourceFee.setAfterAmount(after); sourceFee.setAdjustAmount(after.subtract(money(sourceFee.getOriginalAmount()))); cargoFeeMapper.updateById(sourceFee); }
} }
} else { } else {
updateFormalSettlementDetailFees(internal, external, after);
FormalSettlementDetail detail = formalDetailMapper.selectById(internal.getFormalSettlementDetailId()); FormalSettlementDetail detail = formalDetailMapper.selectById(internal.getFormalSettlementDetailId());
List<FormalSettlementDetailFee> fees = formalDetailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery() List<FormalSettlementDetailFee> fees = formalDetailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
.eq(FormalSettlementDetailFee::getFormalSettlementDetailId, internal.getFormalSettlementDetailId())); .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, internal.getFormalSettlementDetailId()));
@@ -854,6 +983,154 @@ public class TransportReconciliationServiceImpl
record.setChangeReason("运输对账按匹配结果更新"); changeRecordMapper.insert(record); record.setChangeReason("运输对账按匹配结果更新"); changeRecordMapper.insert(record);
} }
private void updateFormalSettlementHeader(TransportReconciliationInternal internal,
TransportReconciliationExternal external, BigDecimal settlementAmount) {
if (internal.getFormalSettlementDetailId() == null) return;
FormalSettlementDetail detail = formalDetailMapper.selectById(internal.getFormalSettlementDetailId());
if (detail == null) return;
detail.setVehicleNo(external.getVehicleNo());
detail.setDepartureAddress(external.getDepartureAddress());
detail.setArrivalAddress(external.getArrivalAddress());
detail.setActualDepartureTime(external.getActualDepartureTime());
detail.setActualCompletionTime(external.getActualCompletionTime());
detail.setTransportType(external.getTransportType());
detail.setCargoName(external.getCargoName());
detail.setCargoType(external.getCargoType());
detail.setTransportQuantity(external.getTransportQuantity());
detail.setQuantityUnit(external.getQuantityUnit());
detail.setMileage(external.getMileage());
detail.setBatchNo(external.getBatchNo());
detail.setUnitPrice(external.getUnitPrice());
detail.setFreightAmount(external.getFreightAmount());
detail.setFeeItemsJson(external.getFeeItemsJson());
detail.setSettlementAmountTax(settlementAmount);
detail.setAdjustAmount(settlementAmount.subtract(money(detail.getOriginalAmount())));
formalDetailMapper.updateById(detail);
}
private void updateFormalSettlementDetailFees(TransportReconciliationInternal internal,
TransportReconciliationExternal external, BigDecimal settlementAmount) {
if (internal.getFormalSettlementDetailId() == null) return;
FormalSettlementDetail detail = formalDetailMapper.selectById(internal.getFormalSettlementDetailId());
if (detail == null) return;
List<FormalSettlementDetailFee> fees = formalDetailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
.eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId())
.eq(FormalSettlementDetailFee::getIsDeleted, 0)
.orderByAsc(FormalSettlementDetailFee::getLineNo));
if (fees.size() == 1) {
FormalSettlementDetailFee fee = fees.get(0);
copyExternalToDetailFee(fee, external, settlementAmount);
formalDetailFeeMapper.updateById(fee);
} else if (fees.size() > 1) {
Map<String, BigDecimal> externalFeeItems = parseFeeItems(external.getFeeItemsJson());
List<Map<String, BigDecimal>> matchedItems = new ArrayList<>();
List<BigDecimal> amounts = new ArrayList<>();
for (int index = 0; index < fees.size(); index++) {
FormalSettlementDetailFee fee = fees.get(index);
Map<String, BigDecimal> feeItems = parseFeeItems(fee.getFeeItemsJson());
Map<String, BigDecimal> matchedFeeItems = new LinkedHashMap<>();
feeItems.keySet().forEach(name -> {
BigDecimal amount = externalFeeItems.get(name);
if (amount != null) matchedFeeItems.put(name, money(amount));
});
matchedItems.add(matchedFeeItems);
amounts.add(matchedFeeItems.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add));
}
BigDecimal allocated = amounts.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal residual = settlementAmount.subtract(allocated);
if (!amounts.isEmpty()) amounts.set(0, amounts.get(0).add(residual));
for (int index = 0; index < fees.size(); index++) {
FormalSettlementDetailFee fee = fees.get(index);
copyExternalToDetailFee(fee, external, amounts.get(index), matchedItems.get(index), false);
formalDetailFeeMapper.updateById(fee);
}
}
}
private void copyExternalToDetailFee(FormalSettlementDetailFee fee,
TransportReconciliationExternal external, BigDecimal settlementAmount) {
copyExternalToDetailFee(fee, external, settlementAmount, parseFeeItems(external.getFeeItemsJson()));
}
private void copyExternalToDetailFee(FormalSettlementDetailFee fee,
TransportReconciliationExternal external, BigDecimal settlementAmount,
Map<String, BigDecimal> feeItems) {
copyExternalToDetailFee(fee, external, settlementAmount, feeItems, true);
}
private void copyExternalToDetailFee(FormalSettlementDetailFee fee,
TransportReconciliationExternal external, BigDecimal settlementAmount,
Map<String, BigDecimal> feeItems, boolean copyTransportFields) {
if (!copyTransportFields) {
fee.setFeeItemsJson(JsonUtil.toJson(feeItems));
fee.setSettlementAmountTax(settlementAmount);
fee.setAdjustAmount(settlementAmount.subtract(money(fee.getOriginalAmount())));
return;
}
fee.setTransportQuantity(external.getTransportQuantity());
fee.setQuantityUnit(external.getQuantityUnit());
fee.setMileage(external.getMileage());
fee.setUnitPrice(external.getUnitPrice());
fee.setFreightAmount(external.getFreightAmount());
fee.setFeeItemsJson(JsonUtil.toJson(feeItems));
fee.setSettlementAmountTax(settlementAmount);
fee.setAdjustAmount(settlementAmount.subtract(money(fee.getOriginalAmount())));
}
private List<FormalSettlementSummaryFee> feeSummary(Long formalSettlementId,
List<TransportReconciliationExternal> externals) {
if (formalSettlementId == null) return List.of();
Map<String, BigDecimal> externalFees = new LinkedHashMap<>();
for (TransportReconciliationExternal external : externals) {
externalFees.merge("运输费", money(external.getFreightAmount()), BigDecimal::add);
parseFeeItems(external.getFeeItemsJson()).forEach((name, amount) ->
externalFees.merge(name, money(amount), BigDecimal::add));
}
return formalSummaryFeeMapper.selectList(Wrappers.<FormalSettlementSummaryFee>lambdaQuery()
.eq(FormalSettlementSummaryFee::getFormalSettlementId, formalSettlementId)
.eq(FormalSettlementSummaryFee::getIsDeleted, 0)
.orderByAsc(FormalSettlementSummaryFee::getLineNo)).stream()
.peek(summary -> {
BigDecimal after = externalFees.get(summary.getFeeItem());
if (after != null) {
summary.setSettlementAmount(after);
summary.setAdjustAmount(after.subtract(money(summary.getOriginalAmount())));
}
}).toList();
}
private void refreshFormalSummaryFees(Long formalSettlementId) {
if (formalSettlementId == null) return;
Map<String, BigDecimal> currentAmounts = new LinkedHashMap<>();
List<FormalSettlementDetail> details = formalDetailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
.eq(FormalSettlementDetail::getFormalSettlementId, formalSettlementId));
for (FormalSettlementDetail detail : details) {
List<FormalSettlementDetailFee> fees = formalDetailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
.eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId())
.eq(FormalSettlementDetailFee::getIsDeleted, 0));
for (FormalSettlementDetailFee fee : fees) {
Map<String, BigDecimal> items = parseFeeItems(fee.getFeeItemsJson());
boolean containsFreight = items.keySet().stream().anyMatch(this::isFreightFeeItem);
if (!containsFreight) currentAmounts.merge("运输费", money(fee.getFreightAmount()), BigDecimal::add);
items.forEach((name, amount) -> currentAmounts.merge(name, money(amount), BigDecimal::add));
}
}
for (FormalSettlementSummaryFee summary : formalSummaryFeeMapper.selectList(Wrappers.<FormalSettlementSummaryFee>lambdaQuery()
.eq(FormalSettlementSummaryFee::getFormalSettlementId, formalSettlementId)
.eq(FormalSettlementSummaryFee::getIsDeleted, 0))) {
if (Integer.valueOf(1).equals(summary.getManualFlag())) continue;
BigDecimal amount = money(currentAmounts.get(summary.getFeeItem()));
summary.setSettlementAmount(amount);
summary.setAdjustAmount(amount.subtract(money(summary.getOriginalAmount())));
formalSummaryFeeMapper.updateById(summary);
}
}
private boolean isFreightFeeItem(String name) {
return name != null && (name.contains("运费") || name.contains("运输费"));
}
private void recalculateSettlement(TransportReconciliation bill) { private void recalculateSettlement(TransportReconciliation bill) {
List<FormalSettlementDetail> details = formalDetailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery() List<FormalSettlementDetail> details = formalDetailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
.eq(FormalSettlementDetail::getFormalSettlementId, bill.getFormalSettlementId())); .eq(FormalSettlementDetail::getFormalSettlementId, bill.getFormalSettlementId()));
@@ -1027,10 +1304,70 @@ public class TransportReconciliationServiceImpl
private String matchKey(TransportReconciliationExternal row) { private String matchKey(TransportReconciliationExternal row) {
return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(), row.getCargoName(), row.getCargoType()); return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(), row.getCargoName(), row.getCargoType());
} }
private String duplicateKey(TransportReconciliationExternal row) {
return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(),
row.getActualDepartureTime(), row.getActualCompletionTime(), row.getTransportType(),
row.getCargoName(), row.getCargoType(), row.getSpecification(), row.getModel(),
row.getTransportQuantity(), row.getQuantityUnit(), row.getMileage(), row.getBatchNo(),
row.getUnitPrice(), row.getFreightAmount(), duplicateFeeItemsKey(row.getFeeItemsJson()), row.getSettlementAmount());
}
private String duplicateFeeItemsKey(String feeItemsJson) {
if (Func.isEmpty(feeItemsJson)) return "";
try {
Map<String, Object> feeItems = JsonUtil.parse(feeItemsJson, Map.class);
StringJoiner joiner = new StringJoiner(",");
feeItems.entrySet().stream().sorted(Map.Entry.comparingByKey())
.forEach(entry -> joiner.add(normal(entry.getKey()) + "=" + normal(entry.getValue())));
return joiner.toString();
} catch (Exception exception) {
return normal(feeItemsJson);
}
}
@SuppressWarnings("unchecked")
private Map<String, BigDecimal> parseFeeItems(String feeItemsJson) {
if (Func.isEmpty(feeItemsJson)) return Map.of();
try {
Map<String, Object> source = JsonUtil.parse(feeItemsJson, Map.class);
Map<String, BigDecimal> result = new LinkedHashMap<>();
source.forEach((name, value) -> {
if (Func.isEmpty(name) || value == null) return;
try {
result.put(name, new BigDecimal(String.valueOf(value)));
} catch (NumberFormatException ignored) {
// 忽略无法转换的费用金额
}
});
return result;
} catch (Exception exception) {
return Map.of();
}
}
private void markSuspectedDuplicates(List<TransportReconciliationExternal> externals, boolean persist) {
Map<String, List<TransportReconciliationExternal>> duplicateGroups = externals.stream()
.collect(Collectors.groupingBy(this::duplicateKey));
for (List<TransportReconciliationExternal> group : duplicateGroups.values()) {
if (group.size() <= 1) continue;
for (TransportReconciliationExternal external : group) {
external.setSuspectedDuplicate(true);
external.setMatchStatus(DUPLICATE);
if (persist) externalMapper.updateById(external);
}
}
}
private String key(Object... values) { StringBuilder builder = new StringBuilder(); for (Object value : values) builder.append(normal(value)).append('|'); return builder.toString(); } private String key(Object... values) { StringBuilder builder = new StringBuilder(); for (Object value : values) builder.append(normal(value)).append('|'); return builder.toString(); }
private String normal(Object value) { private String normal(Object value) {
if (value == null) return ""; if (value == null) return "";
if (value instanceof BigDecimal decimal) return decimal.stripTrailingZeros().toPlainString(); if (value instanceof Number number) {
try {
return new BigDecimal(number.toString()).stripTrailingZeros().toPlainString();
} catch (NumberFormatException ignored) {
return number.toString();
}
}
if (value instanceof List<?> list) return JsonUtil.toJson(list); if (value instanceof List<?> list) return JsonUtil.toJson(list);
return value.toString().trim().replaceAll("\\s+", "").toLowerCase(); return value.toString().trim().replaceAll("\\s+", "").toLowerCase();
} }
@@ -21,7 +21,9 @@ import org.springblade.core.tool.utils.WebUtil;
import org.springblade.system.cache.DictCache; import org.springblade.system.cache.DictCache;
import org.springblade.system.cache.DictBizCache; import org.springblade.system.cache.DictBizCache;
import org.springblade.system.cache.UserCache; import org.springblade.system.cache.UserCache;
import org.springblade.system.feign.ISysClient;
import org.springblade.system.pojo.entity.DictBiz; import org.springblade.system.pojo.entity.DictBiz;
import org.springblade.system.pojo.entity.CargoType;
import org.springblade.transport.excel.WaybillImportBatchExcel; import org.springblade.transport.excel.WaybillImportBatchExcel;
import org.springblade.transport.mapper.WaybillImportBatchMapper; import org.springblade.transport.mapper.WaybillImportBatchMapper;
import org.springblade.transport.pojo.dto.WaybillImportBatchRequest; import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
@@ -35,6 +37,7 @@ import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
import org.springblade.transport.service.ICustomerArchiveService; import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.IProjectApplyService; import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.transport.service.ILoadingManageService;
import org.springblade.transport.service.ITransportPlanService; import org.springblade.transport.service.ITransportPlanService;
import org.springblade.transport.service.IWaybillImportBatchService; import org.springblade.transport.service.IWaybillImportBatchService;
import org.springblade.transport.service.IWaybillService; import org.springblade.transport.service.IWaybillService;
@@ -50,9 +53,11 @@ import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -70,14 +75,20 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
private static final String IMPORT_TYPE_SETTLEMENT = "settlement"; private static final String IMPORT_TYPE_SETTLEMENT = "settlement";
/** 车牌号校验正则:首位汉字,次位大写字母,总长度7或8位 */ /** 车牌号校验正则:首位汉字,次位大写字母,总长度7或8位 */
private static final Pattern VEHICLE_NO_PATTERN = Pattern.compile("^[一-龥][A-Z][A-Z0-9]{5,6}$"); private static final Pattern VEHICLE_NO_PATTERN = Pattern.compile("^[一-龥][A-Z][A-Z0-9]{5,6}$");
/** 中国机动车号牌省份简称。 */
private static final Pattern VEHICLE_PROVINCE_PATTERN = Pattern.compile("^[京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新][A-Z]");
/** 手机号校验正则:11位数字 */ /** 手机号校验正则:11位数字 */
private static final Pattern PHONE_PATTERN = Pattern.compile("^\\d{11}$"); private static final Pattern PHONE_PATTERN = Pattern.compile("^\\d{11}$");
/** 仅由行政区划名称组成的地址,例如“广西南宁市”或“浙江省/宁波市/北仑区”。 */
private static final Pattern REGION_ONLY_ADDRESS_PATTERN = Pattern.compile("^[\\u4e00-\\u9fa5]+(?:省|自治区|特别行政区|市|州|盟|地区|区|县|旗)+$");
private final IWaybillService waybillService; private final IWaybillService waybillService;
private final ICustomerArchiveService customerArchiveService; private final ICustomerArchiveService customerArchiveService;
private final IProjectApplyService projectApplyService; private final IProjectApplyService projectApplyService;
private final ITransportPlanService transportPlanService; private final ITransportPlanService transportPlanService;
private final IReceivablePayableDetailService receivablePayableDetailService; private final IReceivablePayableDetailService receivablePayableDetailService;
private final ILoadingManageService loadingManageService;
private final ISysClient sysClient;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@@ -173,15 +184,21 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
List<Map<String, Object>> rows = Func.isEmpty(request.getRows()) ? List.of() : request.getRows(); List<Map<String, Object>> rows = Func.isEmpty(request.getRows()) ? List.of() : request.getRows();
List<Waybill> waybills = new ArrayList<>(); List<Waybill> waybills = new ArrayList<>();
Map<String, List<Waybill>> loadingWaybills = new TreeMap<>();
for (int index = 0; index < rows.size(); index++) { for (int index = 0; index < rows.size(); index++) {
try { try {
String loadingIdentifier = stringValue(rows.get(index), "loadingIdentifier", "配载标识号");
Waybill waybill = buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft); Waybill waybill = buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft);
if (draft) waybillService.saveDraft(waybill); else waybillService.submit(waybill); if (draft) waybillService.saveDraft(waybill); else waybillService.submit(waybill);
waybills.add(waybill); waybills.add(waybill);
if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType()) && Func.isNotEmpty(loadingIdentifier)) {
loadingWaybills.computeIfAbsent(loadingIdentifier, key -> new ArrayList<>()).add(waybill);
}
} catch (Exception exception) { } catch (Exception exception) {
throw new ServiceException("" + (index + 1) + "" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage()); throw new ServiceException("" + (index + 1) + "" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
} }
} }
loadingWaybills.forEach(loadingManageService::createFromImportedWaybills);
batch.setWaybillCount(waybills.size()); batch.setWaybillCount(waybills.size());
updateById(batch); updateById(batch);
// 导入完成且导入类型为运单时,按合同费用生成模式(系统生成)同步生成应收应付明细。 // 导入完成且导入类型为运单时,按合同费用生成模式(系统生成)同步生成应收应付明细。
@@ -262,7 +279,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
Waybill waybill = new Waybill(); Waybill waybill = new Waybill();
waybill.setOriginalNo(stringValue(row, "originalNo")); waybill.setOriginalNo(stringValue(row, "originalNo"));
waybill.setLoadingNo(stringValue(row, "loadingIdentifier", "配载标识号")); waybill.setLoadingNo(stringValue(row, "loadingIdentifier", "配载标识号"));
waybill.setVehicleNo(stringValue(row, "vehicleNo")); waybill.setVehicleNo(stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号"));
waybill.setDriverId(longValue(row, "driverId", "司机ID")); waybill.setDriverId(longValue(row, "driverId", "司机ID"));
waybill.setDriverName(stringValue(row, "driverName", "司机/船长姓名", "司机/船长")); waybill.setDriverName(stringValue(row, "driverName", "司机/船长姓名", "司机/船长"));
waybill.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号")); waybill.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号"));
@@ -424,11 +441,11 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
// 加载系统枚举值 // 加载系统枚举值
Map<String, String> transportTypeOptions = loadTransportTypeOptions(); Map<String, String> transportTypeOptions = loadTransportTypeOptions();
List<String> quantityUnitOptions = loadQuantityUnitOptions(); List<String> quantityUnitOptions = loadQuantityUnitOptions();
Set<String> cargoTypeOptions = loadCargoTypeOptions();
// 构建配载标识号和同一运单标识号的映射 // 构建配载标识号和同一运单标识号的映射
Map<String, List<Integer>> loadingIdentifierMap = new HashMap<>(); Map<String, List<Integer>> loadingIdentifierMap = new HashMap<>();
Map<String, List<Integer>> waybillIdentifierMap = new HashMap<>(); Map<String, List<Integer>> waybillIdentifierMap = new HashMap<>();
Map<String, Integer> loadingIdentifierCountMap = new HashMap<>();
for (int i = 0; i < rows.size(); i++) { for (int i = 0; i < rows.size(); i++) {
Map<String, Object> row = rows.get(i); Map<String, Object> row = rows.get(i);
@@ -437,7 +454,6 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
if (Func.isNotEmpty(loadingIdentifier)) { if (Func.isNotEmpty(loadingIdentifier)) {
loadingIdentifierMap.computeIfAbsent(loadingIdentifier, k -> new ArrayList<>()).add(i); loadingIdentifierMap.computeIfAbsent(loadingIdentifier, k -> new ArrayList<>()).add(i);
loadingIdentifierCountMap.merge(loadingIdentifier, 1, Integer::sum);
} }
if (Func.isNotEmpty(waybillIdentifier)) { if (Func.isNotEmpty(waybillIdentifier)) {
@@ -464,33 +480,41 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
validatePhoneNumber(row, "departurePhone", "发货联系人电话", errors); validatePhoneNumber(row, "departurePhone", "发货联系人电话", errors);
validatePhoneNumber(row, "arrivalPhone", "收货联系人电话", errors); validatePhoneNumber(row, "arrivalPhone", "收货联系人电话", errors);
// 5. 货物类型校验(暂时跳过,需要货物类型数据) // 5. 地址详细程度校验
validateAddressDetail(row, "departureAddress", "发货地址", errors);
validateAddressDetail(row, "arrivalAddress", "到货地址", errors);
// 6. 数量校验 // 6. 备注长度校验
validateRemark(row, errors);
// 7. 货物名称与货物类型校验
validateCargoFields(row, cargoTypeOptions, errors);
// 8. 数量校验
validatePositiveNumber(row, "quantity", "数量", errors); validatePositiveNumber(row, "quantity", "数量", errors);
// 7. 数量单位校验 // 9. 数量单位校验
validateQuantityUnit(row, quantityUnitOptions, errors); validateQuantityUnit(row, quantityUnitOptions, errors);
// 8. 里程校验 // 10. 里程校验
validatePositiveNumber(row, "mileage", "里程(km)", errors); validatePositiveNumber(row, "mileage", "里程(km)", errors);
// 9. 运费合计校验 // 11. 运费合计校验
validateFreightTotal(row, errors); validateFreightTotal(row, errors);
// 10. 实际发货时间校验 // 12. 实际发货时间校验
validateActualStartDate(row, isDraft, errors); validateActualStartDate(row, isDraft, errors);
// 11. 实际完成时间校验 // 13. 实际完成时间校验
validateActualEndDate(row, isDraft, errors); validateActualEndDate(row, isDraft, errors);
// 12. 预计发货时间校验 // 14. 预计发货时间校验
validatePlanStartDate(row, errors); validatePlanStartDate(row, errors);
// 13. 预计完成时间校验 // 15. 预计完成时间校验
validatePlanEndDate(row, errors); validatePlanEndDate(row, errors);
// 14. 同一运单标识号校验 // 16. 同一运单标识号校验
validateWaybillIdentifier(row, i, waybillIdentifierMap, rows, errors); validateWaybillIdentifier(row, i, waybillIdentifierMap, rows, errors);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
@@ -514,12 +538,12 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
} }
// 检查同一配载标识号下车牌号是否一致 // 检查同一配载标识号下车牌号是否一致
String currentVehicleNo = stringValue(row, "vehicleNo"); String currentVehicleNo = stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号");
for (Integer otherRowIndex : sameIdentifierRows) { for (Integer otherRowIndex : sameIdentifierRows) {
if (otherRowIndex.equals(rowIndex)) { if (otherRowIndex.equals(rowIndex)) {
continue; continue;
} }
String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo"); String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo", "车牌号/航班号/船号/班列号");
if (Func.isNotEmpty(currentVehicleNo) && Func.isNotEmpty(otherVehicleNo) if (Func.isNotEmpty(currentVehicleNo) && Func.isNotEmpty(otherVehicleNo)
&& !currentVehicleNo.equals(otherVehicleNo)) { && !currentVehicleNo.equals(otherVehicleNo)) {
errors.add("同一配载标识号下,车牌号不一致"); errors.add("同一配载标识号下,车牌号不一致");
@@ -527,25 +551,86 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
} }
} }
// 检查配载标识号是否重复
if (sameIdentifierRows.size() > 1 && sameIdentifierRows.indexOf(rowIndex) > 0) {
errors.add("配载标识号重复");
}
} }
private void validateVehicleNo(Map<String, Object> row, List<String> errors) { private void validateVehicleNo(Map<String, Object> row, List<String> errors) {
String vehicleNo = stringValue(row, "vehicleNo"); String vehicleNo = stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号");
String transportType = stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式"); String transportType = stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式");
if (Func.isEmpty(vehicleNo) || Func.isEmpty(transportType)) { if (Func.isEmpty(transportType)) {
return; return;
} }
// 当运输类型为公路整车或公路配载/零担时,校验车牌号格式 // 公路运输必须填写车牌号;其他运输方式的该字段可填写航班号、船号或班列号。
if (transportType.contains("公路整车") || transportType.contains("公路配载") || transportType.contains("公路零担")) { if (!isRoadTransport(transportType)) {
if (!VEHICLE_NO_PATTERN.matcher(vehicleNo).matches()) { return;
errors.add("车牌号格式不正确,应为首位汉字、次位大写字母、总长度7或8位");
} }
if (Func.isEmpty(vehicleNo)) {
errors.add("公路运输时车牌号/航班号/船号/班列号不能为空");
return;
}
if (vehicleNo.length() < 7) {
errors.add("车牌号长度不能少于7位");
}
if (vehicleNo.length() > 8) {
errors.add("车牌号长度不能超过8位");
}
if (!VEHICLE_PROVINCE_PATTERN.matcher(vehicleNo).lookingAt()) {
errors.add("车牌号首位必须是省份简称,第二位必须是英文字母");
}
if (!VEHICLE_NO_PATTERN.matcher(vehicleNo).matches()) {
errors.add("车牌号格式不正确,应为首位省份简称、次位英文字母、总长度7或8位");
}
}
private boolean isRoadTransport(String transportType) {
String value = transportType == null ? "" : transportType.trim().toLowerCase();
return value.contains("公路") || value.contains("道路") || value.contains("road") || "gl".equals(value);
}
private void validateAddressDetail(Map<String, Object> row, String field, String fieldName, List<String> errors) {
String address = stringValue(row, field, fieldName);
if (Func.isEmpty(address)) {
errors.add(fieldName + "不能为空");
return;
}
String normalizedAddress = address.trim();
String[] addressParts = normalizedAddress.split("[\\s/,;|>]+", -1);
boolean hasDetailPart = addressParts.length > 3;
if (hasDetailPart) {
for (int index = 3; index < addressParts.length; index++) {
if (Func.isNotEmpty(addressParts[index])) {
return;
}
}
}
if (!REGION_ONLY_ADDRESS_PATTERN.matcher(normalizedAddress.replaceAll("[\\s/,;|>]+", "")).matches()) {
return;
}
errors.add(fieldName + "必须包含省市区以外的详细地址");
}
private void validateRemark(Map<String, Object> row, List<String> errors) {
String remark = stringValue(row, "remark", "备注");
if (Func.isNotEmpty(remark) && remark.length() > 200) {
errors.add("备注不能超过200个字");
}
}
private void validateCargoFields(Map<String, Object> row, Set<String> cargoTypeOptions, List<String> errors) {
String cargoName = stringValue(row, "cargoName", "货物名称");
if (Func.isEmpty(cargoName)) {
errors.add("货物名称不能为空");
}
String cargoType = stringValue(row, "cargoType", "货物类型");
if (Func.isEmpty(cargoType)) {
errors.add("货物类型不能为空");
return;
}
boolean exists = cargoTypeOptions.contains(cargoType.trim())
|| cargoTypeOptions.stream().anyMatch(value -> value.equalsIgnoreCase(cargoType.trim()));
if (!exists) {
errors.add("货物类型必须在/base/cargo-type中存在");
} }
} }
@@ -721,12 +806,12 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
} }
// 检查同一运单标识号下车牌号是否一致 // 检查同一运单标识号下车牌号是否一致
String currentVehicleNo = stringValue(row, "vehicleNo"); String currentVehicleNo = stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号");
for (Integer otherRowIndex : sameIdentifierRows) { for (Integer otherRowIndex : sameIdentifierRows) {
if (otherRowIndex.equals(rowIndex)) { if (otherRowIndex.equals(rowIndex)) {
continue; continue;
} }
String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo"); String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo", "车牌号/航班号/船号/班列号");
if (Func.isNotEmpty(currentVehicleNo) && Func.isNotEmpty(otherVehicleNo) if (Func.isNotEmpty(currentVehicleNo) && Func.isNotEmpty(otherVehicleNo)
&& !currentVehicleNo.equals(otherVehicleNo)) { && !currentVehicleNo.equals(otherVehicleNo)) {
errors.add("同一运单标识号下,车牌号必须一致"); errors.add("同一运单标识号下,车牌号必须一致");
@@ -777,6 +862,20 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
return options; return options;
} }
private Set<String> loadCargoTypeOptions() {
R<List<CargoType>> response = sysClient.getCargoTypes();
if (response == null || !response.isSuccess() || response.getData() == null) {
throw new ServiceException("货物类型基础数据读取失败,请稍后重试");
}
Set<String> options = new HashSet<>();
for (CargoType cargoType : response.getData()) {
if (cargoType == null) continue;
if (Func.isNotEmpty(cargoType.getCargoName())) options.add(cargoType.getCargoName().trim());
if (Func.isNotEmpty(cargoType.getCargoCode())) options.add(cargoType.getCargoCode().trim());
}
return options;
}
private void addTransportTypeOption(Map<String, String> options, String dictKey, String dictValue) { private void addTransportTypeOption(Map<String, String> options, String dictKey, String dictValue) {
if (Func.isNotEmpty(dictKey)) options.put(dictKey.trim(), dictKey.trim()); if (Func.isNotEmpty(dictKey)) options.put(dictKey.trim(), dictKey.trim());
if (Func.isNotEmpty(dictValue)) options.put(dictValue.trim(), Func.isNotEmpty(dictKey) ? dictKey.trim() : dictValue.trim()); if (Func.isNotEmpty(dictValue)) options.put(dictValue.trim(), Func.isNotEmpty(dictKey) ? dictKey.trim() : dictValue.trim());
@@ -808,7 +907,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
WaybillImportBatchExcel excel = new WaybillImportBatchExcel(); WaybillImportBatchExcel excel = new WaybillImportBatchExcel();
excel.setOriginalNo(stringValue(row, "originalNo")); excel.setOriginalNo(stringValue(row, "originalNo"));
excel.setLoadingIdentifier(stringValue(row, "loadingIdentifier", "配载标识号")); excel.setLoadingIdentifier(stringValue(row, "loadingIdentifier", "配载标识号"));
excel.setVehicleNo(stringValue(row, "vehicleNo")); excel.setVehicleNo(stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号"));
excel.setTransportType(stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式")); excel.setTransportType(stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式"));
excel.setDriverName(stringValue(row, "driverName", "司机/船长姓名", "司机/船长")); excel.setDriverName(stringValue(row, "driverName", "司机/船长姓名", "司机/船长"));
excel.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号")); excel.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号"));