From 604ae47b4f844c75eb81a21a120e2210866ebc0e Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 20 Oct 2026 14:43:51 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=AF=BC=E5=85=A5=E8=BF=90?= =?UTF-8?q?=E5=8D=95=E6=94=AF=E6=8C=81=E6=9C=AA=E7=BB=93=E7=AE=97=E6=95=B4?= =?UTF-8?q?=E6=89=B9=E5=88=A0=E9=99=A4=E5=B9=B6=E8=B0=83=E6=95=B4=E7=BB=93?= =?UTF-8?q?=E7=AE=97=E5=8D=95=E5=AF=BC=E5=85=A5=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../pojo/vo/WaybillImportBatchVO.java | 6 + .../excel/SettlementImportTemplateExcel.java | 6 +- .../SettlementImportTemplateExporter.java | 56 +++--- .../SettlementImportBatchServiceImpl.java | 32 +--- .../impl/WaybillImportBatchServiceImpl.java | 180 +++++++++++++++++- 5 files changed, 212 insertions(+), 68 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportBatchVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportBatchVO.java index 841236aa..b30c1973 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportBatchVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportBatchVO.java @@ -20,6 +20,12 @@ public class WaybillImportBatchVO extends WaybillImportBatch { private static final long serialVersionUID = 1L; private String importTypeName; private String statusName; + /** 结算状态:unsettled/partial/settled */ + @Schema(description = "结算状态") + private String settlementStatus; + /** 结算状态名称:未结算/部分结算/已结算 */ + @Schema(description = "结算状态名称") + private String settlementStatusName; private String createUserName; private String updateUserName; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExcel.java index 871599eb..8ed3f35a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExcel.java @@ -37,7 +37,7 @@ import java.io.Serializable; import java.math.BigDecimal; /** - * 导入结算单 Excel 模板(固定列 + 运输单价 + *运输费 + 示例费用项) + * 导入结算单 Excel 固定列模型(失败明细兜底导出;费用项由用户在原模板中自行增列)。 * * @author Chill */ @@ -96,10 +96,6 @@ public class SettlementImportTemplateExcel implements Serializable { private BigDecimal unitPrice; @ExcelProperty("*运输费") private BigDecimal transportFee; - @ExcelProperty("装卸费") - private BigDecimal loadingFee; - @ExcelProperty("过路费") - private BigDecimal tollFee; /** 模板已去掉结算金额列;仅失败明细内部保留,不导出为表头 */ @ExcelIgnore private BigDecimal settlementAmount; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExporter.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExporter.java index 91eb317e..cee7d476 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExporter.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExporter.java @@ -47,12 +47,12 @@ import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; /** - * 导入结算单模板导出:填写说明 + 数据表头 + 运输方式下拉 + 系统费用项列。 + * 导入结算单模板导出:填写说明 + 固定数据表头 + 运输方式下拉。 + *

+ * 模板不预置费用项列;用户可在「*运输费」与后续固定列之间自行插入系统已启用的费用项列。 * * @author Chill */ @@ -93,15 +93,18 @@ public final class SettlementImportTemplateExporter { "同一运单标识号" ); - private static final List DEFAULT_FEE_ITEMS = List.of("装卸费", "过路费"); - private SettlementImportTemplateExporter() { } public static void export(HttpServletResponse response, List transportTypeOptions, List feeItemNames) { - List headers = buildHeaders(feeItemNames); - List transportOptions = defaultTransportOptions(transportTypeOptions); + List headers = buildHeaders(); + // 运输方式由调用方传入业务字典 transport_type 的字典名称,禁止写死 + List transportOptions = transportTypeOptions == null ? List.of() : transportTypeOptions.stream() + .filter(item -> item != null && !item.isBlank()) + .map(String::trim) + .distinct() + .toList(); try (Workbook workbook = new XSSFWorkbook()) { writeInstructionSheet(workbook, transportOptions, feeItemNames); Sheet dataSheet = workbook.createSheet("导入结算单模板"); @@ -127,20 +130,9 @@ public final class SettlementImportTemplateExporter { } } - private static List buildHeaders(List feeItemNames) { + /** 固定列模板:不预置费用项列,由用户自行插入。 */ + private static List buildHeaders() { List headers = new ArrayList<>(FIXED_HEADERS_BEFORE_FEE); - Set feeNames = new LinkedHashSet<>(); - if (feeItemNames != null) { - for (String name : feeItemNames) { - if (name != null && !name.isBlank()) { - feeNames.add(name.trim()); - } - } - } - if (feeNames.isEmpty()) { - feeNames.addAll(DEFAULT_FEE_ITEMS); - } - headers.addAll(feeNames); headers.addAll(FIXED_HEADERS_AFTER_FEE); return headers; } @@ -154,18 +146,24 @@ public final class SettlementImportTemplateExporter { titleStyle.setFont(titleFont); List lines = new ArrayList<>(); lines.add("导入结算单填写说明"); - lines.add("1. 请不要修改「导入结算单模板」工作表的表头;带 * 号为必填项。"); - lines.add("2. 运输方式请从下拉项中选择,须与系统字典一致。"); + lines.add("1. 请不要修改「导入结算单模板」工作表的固定表头;带 * 号为必填项。"); + lines.add("2. 运输方式请从下拉项中选择,须与业务字典 transport_type 的字典名称一致。"); lines.add("3. 日期请填写 yyyy-MM-dd(如 2026-01-01);Excel 日期单元格也可直接选择。"); lines.add("4. 里程(km) 可为空;填写时须为正数。"); - lines.add("5. 「*运输费」之后、「*实际发货时间」之前的列为费用项,列名须与系统费用项主数据名称完全一致;金额不为 0 时若不存在或已停用则无法导入。"); + lines.add("5. 「*运输费」之后、「*实际发货时间」之前可按需自行插入费用项列,列名须与系统已启用费用项名称完全一致;金额不为 0 时若不存在或已停用则无法导入。"); lines.add("6. 模板不含结算金额列;导入后系统按「运输费 + 各费用项」自动合计结算金额。"); - lines.add("7. 当前可选运输方式:" + String.join("、", transportOptions) + "。"); + if (transportOptions != null && !transportOptions.isEmpty()) { + lines.add("7. 当前可选运输方式(业务字典 transport_type 名称):" + + String.join("、", transportOptions) + "。"); + } else { + lines.add("7. 运输方式请从业务字典 transport_type 维护的名称中选择。"); + } if (feeItemNames != null && !feeItemNames.isEmpty()) { - lines.add("8. 当前启用费用项示例:" + String.join("、", feeItemNames.stream().limit(30).toList()) + lines.add("8. 费用项请按实际需要在模板中自行增列,列名须与系统已启用费用项一致;当前系统已启用费用项:" + + String.join("、", feeItemNames.stream().limit(30).toList()) + (feeItemNames.size() > 30 ? " 等" : "") + "。"); } else { - lines.add("8. 模板默认包含装卸费、过路费列,可按系统费用项主数据增删费用列。"); + lines.add("8. 费用项请按实际需要在模板中自行增列,列名须与系统已启用的费用项主数据名称完全一致。"); } for (int index = 0; index < lines.size(); index++) { Row row = sheet.createRow(index); @@ -201,10 +199,4 @@ public final class SettlementImportTemplateExporter { return style; } - private static List defaultTransportOptions(List transportTypeOptions) { - if (transportTypeOptions != null && !transportTypeOptions.isEmpty()) { - return transportTypeOptions; - } - return List.of("公路运输", "铁路运输", "水路运输", "航空运输"); - } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java index 6270dbc4..31f0a8b0 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java @@ -41,7 +41,6 @@ import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.DictBizCache; -import org.springblade.system.cache.DictCache; import org.springblade.system.cache.UserCache; import org.springblade.system.feign.ISysClient; import org.springblade.system.pojo.entity.DictBiz; @@ -824,7 +823,7 @@ public class SettlementImportBatchServiceImpl for (Map.Entry entry : feeItems.entrySet()) { String name = normalizeFeeName(entry.getKey()); BigDecimal amount = entry.getValue() == null ? BigDecimal.ZERO : entry.getValue(); - // 模板自带的装卸费、过路费等列未填写时金额为 0,不要求费用项主数据存在同名档案 + // 用户自行增列的费用项金额为 0 时跳过校验;金额不为 0 须匹配系统已启用费用项 if (Func.isEmpty(name) || amount.compareTo(BigDecimal.ZERO) == 0) { continue; } @@ -1149,9 +1148,6 @@ public class SettlementImportBatchServiceImpl excel.setMileage(decimalValue(row, "mileage", "里程(km)", "里程")); excel.setUnitPrice(decimalValue(row, "unitPrice", "运输单价", "单价")); excel.setTransportFee(decimalValue(row, "transportFee", "*运输费", "运输费")); - Map feeItems = extractFeeItems(row, List.of("装卸费", "过路费")); - excel.setLoadingFee(feeItems.get("装卸费")); - excel.setTollFee(feeItems.get("过路费")); excel.setSettlementAmount(decimalValue(row, "settlementAmount", "结算金额")); excel.setActualStartDate(stringValue(row, "actualStartDate", "*实际发货时间", "实际发货时间")); excel.setActualEndDate(stringValue(row, "actualEndDate", "*实际完成时间", "实际完成时间")); @@ -1330,34 +1326,26 @@ public class SettlementImportBatchServiceImpl return sanitized; } + /** + * 运输方式取业务字典 transport_type 的字典名称(dictValue),不写死枚举。 + */ private List loadTransportTypeLabels() { LinkedHashSet labels = new LinkedHashSet<>(); try { List dictBizList = DictBizCache.getList("transport_type"); if (Func.isNotEmpty(dictBizList)) { dictBizList.stream() - .map(DictBiz::getDictValue) + .filter(dict -> dict != null && Func.isNotEmpty(dict.getDictValue()) + && !"-1".equals(Func.toStr(dict.getDictKey()))) + .map(dict -> dict.getDictValue().trim()) .filter(Func::isNotEmpty) - .map(String::trim) .forEach(labels::add); } - } catch (Exception ignored) { - // 业务字典不可用时继续读取系统字典 - } - try { - List dictList = DictCache.getList("transport_type"); - if (Func.isNotEmpty(dictList)) { - dictList.stream() - .map(org.springblade.system.pojo.entity.Dict::getDictValue) - .filter(Func::isNotEmpty) - .map(String::trim) - .forEach(labels::add); - } - } catch (Exception ignored) { - // 忽略 + } catch (Exception exception) { + throw new ServiceException("业务字典 transport_type 读取失败,请检查业务字典配置"); } if (labels.isEmpty()) { - return List.of("公路运输", "铁路运输", "水路运输", "航空运输"); + throw new ServiceException("业务字典 transport_type 未配置运输方式,请先维护字典名称"); } return new ArrayList<>(labels); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java index debfa16d..61604120 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java @@ -30,7 +30,9 @@ import org.springblade.transport.excel.WaybillImportBatchExcel; import org.springblade.transport.mapper.WaybillImportBatchMapper; import org.springblade.transport.pojo.dto.WaybillImportBatchRequest; import org.springblade.transport.pojo.entity.CustomerArchive; +import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.entity.ProjectApply; +import org.springblade.transport.pojo.entity.ReceivablePayableDetail; import org.springblade.transport.pojo.entity.TransportPlan; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.entity.WaybillImportBatch; @@ -76,6 +78,13 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl entityPage = page(page, queryWrapper); List records = entityPage.getRecords().stream().map(this::toVO).toList(); + fillSettlementStatus(records); Page resultPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal()); resultPage.setRecords(records); return resultPage; @@ -389,18 +399,33 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl batches = listByIds(idList).stream() .filter(batch -> !Objects.equals(batch.getIsDeleted(), 1)) .toList(); - // 与前端一致:仅草稿可删;选中含已完成/进行中时整批拒绝 - boolean hasNonDraft = batches.stream() - .anyMatch(batch -> !STATUS_DRAFT.equals(batch.getImportStatus())); - if (hasNonDraft) { - throw new ServiceException("已完成运单不可删除"); + if (Func.isEmpty(batches)) { + throw new ServiceException("运单批次不存在或已删除"); + } + Map> waybillsByBatchId = listWaybillsByBatchIds( + batches.stream().map(WaybillImportBatch::getId).toList()); + List allWaybillIds = waybillsByBatchId.values().stream() + .flatMap(List::stream) + .map(Waybill::getId) + .filter(Objects::nonNull) + .toList(); + Set settledWaybillIds = receivablePayableDetailService.settlementLinkedWaybillIds(allWaybillIds); + // 仅未结算批次可整批删除;选中含部分结算/已结算时整批拒绝 + for (WaybillImportBatch batch : batches) { + List waybills = waybillsByBatchId.getOrDefault(batch.getId(), List.of()); + String settlementStatus = resolveSettlementStatus(waybills, settledWaybillIds); + if (!SETTLEMENT_UNSETTLED.equals(settlementStatus)) { + throw new ServiceException("仅未结算的运单批次可以删除"); + } } BusinessRemoveResultVO result = new BusinessRemoveResultVO(); for (WaybillImportBatch batch : batches) { - List waybills = waybillService.list(Wrappers.lambdaQuery() - .eq(Waybill::getImportBatchId, batch.getId()).eq(Waybill::getIsDeleted, 0)); - if (Func.isNotEmpty(waybills)) { - waybillService.deleteLogic(waybills.stream().map(Waybill::getId).toList()); + List waybills = waybillsByBatchId.getOrDefault(batch.getId(), List.of()); + List waybillIds = waybills.stream().map(Waybill::getId).filter(Objects::nonNull).toList(); + if (Func.isNotEmpty(waybillIds)) { + clearUnsettledReceivableDetails(waybillIds); + clearImportedLoadings(waybills); + waybillService.deleteLogic(waybillIds); } deleteLogic(List.of(batch.getId())); result.setSuccessCount(result.getSuccessCount() + 1); @@ -671,11 +696,148 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl records) { + if (Func.isEmpty(records)) { + return; + } + List batchIds = records.stream().map(WaybillImportBatchVO::getId).filter(Objects::nonNull).toList(); + Map> waybillsByBatchId = listWaybillsByBatchIds(batchIds); + List allWaybillIds = waybillsByBatchId.values().stream() + .flatMap(List::stream) + .map(Waybill::getId) + .filter(Objects::nonNull) + .toList(); + Set settledWaybillIds = receivablePayableDetailService.settlementLinkedWaybillIds(allWaybillIds); + for (WaybillImportBatchVO record : records) { + List waybills = waybillsByBatchId.getOrDefault(record.getId(), List.of()); + String settlementStatus = resolveSettlementStatus(waybills, settledWaybillIds); + record.setSettlementStatus(settlementStatus); + record.setSettlementStatusName(settlementStatusName(settlementStatus)); + } + } + + private Map> listWaybillsByBatchIds(List batchIds) { + if (Func.isEmpty(batchIds)) { + return Map.of(); + } + List waybills = waybillService.list(Wrappers.lambdaQuery() + .select(Waybill::getId, Waybill::getImportBatchId, Waybill::getLoadingNo) + .in(Waybill::getImportBatchId, batchIds) + .eq(Waybill::getIsDeleted, 0)); + return waybills.stream() + .filter(waybill -> waybill.getImportBatchId() != null) + .collect(Collectors.groupingBy(Waybill::getImportBatchId)); + } + + private String resolveSettlementStatus(List waybills, Set settledWaybillIds) { + if (Func.isEmpty(waybills)) { + return SETTLEMENT_UNSETTLED; + } + int settledCount = 0; + for (Waybill waybill : waybills) { + if (waybill.getId() != null && settledWaybillIds.contains(waybill.getId())) { + settledCount++; + } + } + if (settledCount <= 0) { + return SETTLEMENT_UNSETTLED; + } + if (settledCount >= waybills.size()) { + return SETTLEMENT_SETTLED; + } + return SETTLEMENT_PARTIAL; + } + + private String settlementStatusName(String settlementStatus) { + if (SETTLEMENT_SETTLED.equals(settlementStatus)) { + return "已结算"; + } + if (SETTLEMENT_PARTIAL.equals(settlementStatus)) { + return "部分结算"; + } + return "未结算"; + } + + /** 删除批次前清理尚未进入预/正式结算的应收应付明细,避免残留待结算数据。 */ + private void clearUnsettledReceivableDetails(List waybillIds) { + if (Func.isEmpty(waybillIds)) { + return; + } + List details = receivablePayableDetailService.list( + Wrappers.lambdaQuery() + .in(ReceivablePayableDetail::getWaybillId, waybillIds) + .eq(ReceivablePayableDetail::getIsDeleted, 0) + .and(wrapper -> wrapper.isNull(ReceivablePayableDetail::getPreSettlementNo) + .or().eq(ReceivablePayableDetail::getPreSettlementNo, "")) + .and(wrapper -> wrapper.isNull(ReceivablePayableDetail::getFormalSettlementNo) + .or().eq(ReceivablePayableDetail::getFormalSettlementNo, ""))); + if (Func.isNotEmpty(details)) { + receivablePayableDetailService.deleteLogic( + details.stream().map(ReceivablePayableDetail::getId).toList()); + } + } + + /** 清理本批次运单关联的批量导入配载单(配载单运单均属本批时整单删除)。 */ + private void clearImportedLoadings(List waybills) { + if (Func.isEmpty(waybills)) { + return; + } + Set batchWaybillIds = waybills.stream() + .map(Waybill::getId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(HashSet::new)); + Set loadingNos = waybills.stream() + .map(Waybill::getLoadingNo) + .filter(Func::isNotEmpty) + .collect(Collectors.toCollection(HashSet::new)); + if (Func.isEmpty(loadingNos)) { + return; + } + List loadings = loadingManageService.list(Wrappers.lambdaQuery() + .in(LoadingManage::getLoadingNo, loadingNos) + .eq(LoadingManage::getDataSource, LOADING_DATA_SOURCE_IMPORT) + .eq(LoadingManage::getIsDeleted, 0)); + List deleteLoadingIds = new ArrayList<>(); + for (LoadingManage loading : loadings) { + List loadingWaybillIds = parseLoadingWaybillIds(loading.getWaybillIdsJson()); + if (Func.isEmpty(loadingWaybillIds) || batchWaybillIds.containsAll(loadingWaybillIds)) { + deleteLoadingIds.add(loading.getId()); + } + } + if (Func.isNotEmpty(deleteLoadingIds)) { + loadingManageService.deleteLogic(deleteLoadingIds); + } + } + + private List parseLoadingWaybillIds(String waybillIdsJson) { + if (Func.isEmpty(waybillIdsJson)) { + return List.of(); + } + try { + Object parsed = JsonUtil.parse(waybillIdsJson, List.class); + if (parsed instanceof List list) { + return list.stream() + .filter(Objects::nonNull) + .map(item -> Long.valueOf(String.valueOf(item))) + .toList(); + } + } catch (Exception ignored) { + // 兼容历史逗号分隔存储 + } + return Func.toLongList(waybillIdsJson); + } + @Override public String nextBatchNo() { return nextCode();