From ddaa6f2c45ac8fa1b16cb9533a2466e3d6d13a73 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sun, 18 Oct 2026 20:35:28 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=AE=8C=E5=96=84=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E7=BB=93=E7=AE=97=E5=8D=95=E6=A8=A1=E6=9D=BF=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E3=80=81=E8=8D=89=E7=A8=BF=E6=98=8E=E7=BB=86=E4=B8=8E?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=BF=90=E5=8D=95=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../pojo/entity/SettlementImportBatch.java | 3 + .../pojo/vo/SettlementImportBatchVO.java | 4 + .../SettlementImportController.java | 15 ++ .../excel/SettlementImportFeeReader.java | 100 +++++++- .../SettlementImportTemplateExporter.java | 211 ++++++++++++++++ .../ISettlementImportBatchService.java | 8 + .../SettlementImportBatchServiceImpl.java | 239 ++++++++++++++++-- ...blade_settlement_import_batch_20261015.sql | 10 +- ...settlement_import_batch_alter_20261018.sql | 7 + 9 files changed, 573 insertions(+), 24 deletions(-) create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExporter.java create mode 100644 doc/sql/transport/blade_settlement_import_batch_alter_20261018.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementImportBatch.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementImportBatch.java index 88a3bcd4..9cecf4af 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementImportBatch.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementImportBatch.java @@ -107,6 +107,9 @@ public class SettlementImportBatch extends TenantEntity { @Schema(description = "动态费用列名JSON") private String feeItemNamesJson; + @Schema(description = "导入明细行JSON") + private String rowsJson; + @Schema(description = "备注") private String remark; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementImportBatchVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementImportBatchVO.java index 3f856a62..6887da79 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementImportBatchVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementImportBatchVO.java @@ -32,6 +32,7 @@ import org.springblade.transport.pojo.entity.SettlementImportBatch; import java.io.Serial; import java.util.List; +import java.util.Map; /** * 导入结算单批次视图 @@ -66,4 +67,7 @@ public class SettlementImportBatchVO extends SettlementImportBatch { @Schema(description = "动态费用列名") private List feeItemNames; + + @Schema(description = "导入明细行") + private List> rows; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementImportController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementImportController.java index cfc6fe69..3fac3e24 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementImportController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementImportController.java @@ -144,4 +144,19 @@ public class SettlementImportController extends BladeController { settlementImportBatchService.voidBatch(request); return R.success("作废成功"); } + + @PostMapping("/remove") + @ApiOperationSupport(order = 12) + @Operation(summary = "删除草稿批次") + public R remove(@RequestParam Long id) { + settlementImportBatchService.removeDraft(id); + return R.success("删除成功"); + } + + @GetMapping("/history-waybills") + @ApiOperationSupport(order = 13) + @Operation(summary = "历史运单可选列表") + public R>> historyWaybills(SettlementImportBatchRequest request) { + return R.data(settlementImportBatchService.historyWaybills(request)); + } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportFeeReader.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportFeeReader.java index be771e3c..992db31c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportFeeReader.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportFeeReader.java @@ -26,7 +26,9 @@ package org.springblade.transport.excel; import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.DateUtil; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; @@ -38,11 +40,16 @@ import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; /** * 导入结算单 Excel 读取器(Apache POI)。 @@ -54,6 +61,11 @@ import java.util.Map; public final class SettlementImportFeeReader { private static final DataFormatter DATA_FORMATTER = new DataFormatter(Locale.CHINA); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final Set DATE_FIELDS = Set.of( + "actualStartDate", "actualEndDate", "planStartDate", "planEndDate" + ); + private static final Set OPTIONAL_DECIMAL_FIELDS = Set.of("mileage", "unitPrice"); private SettlementImportFeeReader() { } @@ -193,23 +205,26 @@ public final class SettlementImportFeeReader { for (Map.Entry entry : headers.entrySet()) { int column = entry.getKey(); String trimmedHeader = entry.getValue(); - String cell = cellString(row.getCell(column)); + Cell poiCell = row.getCell(column); if (feeColumns.contains(column)) { - feeItems.put(trimmedHeader, decimal(cell, trimmedHeader, excelRowIndex)); + feeItems.put(trimmedHeader, decimal(cellString(poiCell), trimmedHeader, excelRowIndex)); continue; } String field = FIXED_HEADER_MAP.get(trimmedHeader); if (field == null) { continue; } - if ("transportFee".equals(field) || "unitPrice".equals(field) - || "quantity".equals(field) || "mileage".equals(field) + if (DATE_FIELDS.contains(field)) { + mapped.put(field, dateCellString(poiCell)); + } else if (OPTIONAL_DECIMAL_FIELDS.contains(field)) { + mapped.put(field, optionalDecimal(cellString(poiCell), trimmedHeader, excelRowIndex)); + } else if ("transportFee".equals(field) || "quantity".equals(field) || "settlementAmount".equals(field)) { - mapped.put(field, decimal(cell, trimmedHeader, excelRowIndex)); + mapped.put(field, decimal(cellString(poiCell), trimmedHeader, excelRowIndex)); } else if ("serialNumber".equals(field)) { - mapped.put(field, intValue(cell)); + mapped.put(field, intValue(cellString(poiCell))); } else { - mapped.put(field, blankToNull(cell)); + mapped.put(field, blankToNull(cellString(poiCell))); } mapped.put(trimmedHeader, mapped.get(field)); } @@ -278,6 +293,69 @@ public final class SettlementImportFeeReader { return text.isEmpty() ? null : text; } + /** + * 读取日期单元格:Excel 日期序列优先格式化为 yyyy-MM-dd; + * 文本短日期(如 1/1/26、10/10/2026)归一为四位年,供后续 LenientDateParser 识别。 + */ + private static String dateCellString(Cell cell) { + if (cell == null) { + return null; + } + CellType cellType = cell.getCellType(); + if (cellType == CellType.FORMULA) { + cellType = cell.getCachedFormulaResultType(); + } + if (cellType == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) { + Date date = cell.getDateCellValue(); + if (date == null) { + return null; + } + LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + return DATE_FORMATTER.format(localDate); + } + String text = cellString(cell); + return normalizeExcelShortDate(text); + } + + /** 将 Excel 常见短日期文本归一为 yyyy-MM-dd;无法识别时原样返回交由校验层报错。 */ + private static String normalizeExcelShortDate(String text) { + if (text == null || text.isBlank()) { + return null; + } + String normalized = text.trim().replace('.', '/').replace('-', '/'); + String[] parts = normalized.split("[/\\s]+"); + if (parts.length < 3) { + return text.trim(); + } + try { + int first = Integer.parseInt(parts[0]); + int second = Integer.parseInt(parts[1]); + int third = Integer.parseInt(parts[2].replaceAll("[^0-9].*$", "")); + int year; + int month; + int day; + if (parts[0].length() == 4) { + year = first; + month = second; + day = third; + } else if (parts[2].length() == 4 || parts[2].length() == 2) { + // M/d/yy 或 M/d/yyyy(Excel 中文本/短日期常见) + month = first; + day = second; + year = parts[2].length() == 2 ? 2000 + third : third; + if (parts[2].length() == 2 && third >= 100) { + return text.trim(); + } + } else { + return text.trim(); + } + LocalDate localDate = LocalDate.of(year, month, day); + return DATE_FORMATTER.format(localDate); + } catch (Exception exception) { + return text.trim(); + } + } + private static BigDecimal decimal(String cell, String columnName, Integer excelRowIndex) { if (cell == null || cell.isBlank()) { return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); @@ -290,6 +368,14 @@ public final class SettlementImportFeeReader { } } + /** 可选数值:空白返回 null,避免空里程被当成 0 触发「必须为正数」。 */ + private static BigDecimal optionalDecimal(String cell, String columnName, Integer excelRowIndex) { + if (cell == null || cell.isBlank()) { + return null; + } + return decimal(cell, columnName, excelRowIndex); + } + private static Integer intValue(String cell) { if (cell == null || cell.isBlank()) { return null; 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 new file mode 100644 index 00000000..a33dbb6c --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExporter.java @@ -0,0 +1,211 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.excel; + +import jakarta.servlet.http.HttpServletResponse; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.DataValidationConstraint; +import org.apache.poi.ss.usermodel.DataValidationHelper; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springblade.core.log.exception.ServiceException; + +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 + */ +public final class SettlementImportTemplateExporter { + + private static final List FIXED_HEADERS_BEFORE_FEE = List.of( + "序号", + "原始单号", + "配载标识号", + "*车牌号/航班号/船号/班列号", + "*运输方式", + "司机/船长姓名", + "司机/船长手机号", + "*发货地址", + "发货联系人", + "发货联系人电话", + "*到货地址", + "收货联系人", + "收货联系人电话", + "*货物名称", + "*货物类型", + "包装", + "*数量", + "*数量单位", + "规格", + "型号", + "里程(km)", + "运输单价", + "*运输费" + ); + + private static final List FIXED_HEADERS_AFTER_FEE = List.of( + "结算金额", + "*实际发货时间", + "*实际完成时间", + "预计发货时间", + "预计完成时间", + "备注", + "同一运单标识号" + ); + + 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); + try (Workbook workbook = new XSSFWorkbook()) { + writeInstructionSheet(workbook, transportOptions, feeItemNames); + Sheet dataSheet = workbook.createSheet("导入结算单模板"); + CellStyle headerStyle = createHeaderStyle(workbook); + Row headerRow = dataSheet.createRow(0); + for (int index = 0; index < headers.size(); index++) { + Cell cell = headerRow.createCell(index); + cell.setCellValue(headers.get(index)); + cell.setCellStyle(headerStyle); + dataSheet.setColumnWidth(index, Math.min(28, Math.max(12, headers.get(index).length() + 4)) * 256); + } + int transportTypeColumn = headers.indexOf("*运输方式"); + if (transportTypeColumn >= 0 && !transportOptions.isEmpty()) { + addDropdown(dataSheet, transportTypeColumn, transportOptions); + } + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setHeader("Content-disposition", + "attachment;filename=" + URLEncoder.encode("导入结算单模板", StandardCharsets.UTF_8) + ".xlsx"); + workbook.write(response.getOutputStream()); + } catch (IOException exception) { + throw new ServiceException("导出导入结算单模板失败:" + exception.getMessage()); + } + } + + private static List buildHeaders(List feeItemNames) { + 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; + } + + private static void writeInstructionSheet(Workbook workbook, List transportOptions, + List feeItemNames) { + Sheet sheet = workbook.createSheet("填写说明"); + CellStyle titleStyle = workbook.createCellStyle(); + Font titleFont = workbook.createFont(); + titleFont.setBold(true); + titleStyle.setFont(titleFont); + List lines = new ArrayList<>(); + lines.add("导入结算单填写说明"); + lines.add("1. 请不要修改「导入结算单模板」工作表的表头;带 * 号为必填项。"); + lines.add("2. 运输方式请从下拉项中选择,须与系统字典一致。"); + lines.add("3. 日期请填写 yyyy-MM-dd(如 2026-01-01);Excel 日期单元格也可直接选择。"); + lines.add("4. 里程(km) 可为空;填写时须为正数。"); + lines.add("5. 「*运输费」之后、「结算金额」之前的列为费用项,列名须与系统费用项主数据名称完全一致;金额不为 0 时若不存在或已停用则无法导入。"); + lines.add("6. 结算金额 = 运输费 + 各费用项合计,系统会按此重算。"); + lines.add("7. 当前可选运输方式:" + String.join("、", transportOptions) + "。"); + if (feeItemNames != null && !feeItemNames.isEmpty()) { + lines.add("8. 当前启用费用项示例:" + String.join("、", feeItemNames.stream().limit(30).toList()) + + (feeItemNames.size() > 30 ? " 等" : "") + "。"); + } else { + lines.add("8. 模板默认包含装卸费、过路费列,可按系统费用项主数据增删费用列。"); + } + for (int index = 0; index < lines.size(); index++) { + Row row = sheet.createRow(index); + Cell cell = row.createCell(0); + cell.setCellValue(lines.get(index)); + if (index == 0) { + cell.setCellStyle(titleStyle); + } + } + sheet.setColumnWidth(0, 120 * 256); + } + + private static void addDropdown(Sheet sheet, int columnIndex, List options) { + DataValidationHelper helper = sheet.getDataValidationHelper(); + String[] values = options.toArray(new String[0]); + DataValidationConstraint constraint = helper.createExplicitListConstraint(values); + CellRangeAddressList addressList = new CellRangeAddressList(1, 2000, columnIndex, columnIndex); + DataValidation validation = helper.createValidation(constraint, addressList); + validation.setEmptyCellAllowed(true); + validation.setShowErrorBox(true); + sheet.addValidationData(validation); + } + + private static CellStyle createHeaderStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font font = workbook.createFont(); + font.setBold(true); + style.setFont(font); + 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/ISettlementImportBatchService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ISettlementImportBatchService.java index f045a015..5d81be02 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ISettlementImportBatchService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ISettlementImportBatchService.java @@ -66,4 +66,12 @@ public interface ISettlementImportBatchService extends BaseService> historyWaybills(SettlementImportBatchRequest request); } 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 634814ab..9e21cddc 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 @@ -32,7 +32,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springblade.common.excel.ImportFailureExcelUtil; -import org.springblade.core.excel.util.ExcelUtil; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.secure.utils.AuthUtil; @@ -41,12 +40,16 @@ import org.springblade.core.tool.jackson.JsonUtil; 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; import org.springblade.system.pojo.entity.FeeItem; import org.springblade.transport.constant.TransportBusiConstant; import org.springblade.transport.excel.SettlementImportFeeReader; import org.springblade.transport.excel.SettlementImportTemplateExcel; +import org.springblade.transport.excel.SettlementImportTemplateExporter; import org.springblade.transport.mapper.SettlementImportBatchMapper; import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest; import org.springblade.transport.pojo.dto.FormalSettlementStatusRequest; @@ -186,6 +189,7 @@ public class SettlementImportBatchServiceImpl .eq(SettlementImportBatch::getIsDeleted, 0) .like(Func.isNotEmpty(request.getBatchNo()), SettlementImportBatch::getBatchNo, request.getBatchNo()) .eq(Func.isNotEmpty(request.getSettlementType()), SettlementImportBatch::getSettlementType, request.getSettlementType()) + .eq(Func.isNotEmpty(request.getImportStatus()), SettlementImportBatch::getImportStatus, request.getImportStatus()) .like(Func.isNotEmpty(request.getProjectName()), SettlementImportBatch::getProjectName, request.getProjectName()) .like(Func.isNotEmpty(request.getCustomerName()), SettlementImportBatch::getCustomerName, request.getCustomerName()) .like(Func.isNotEmpty(request.getCarrierName()), SettlementImportBatch::getCarrierName, request.getCarrierName()) @@ -204,13 +208,18 @@ public class SettlementImportBatchServiceImpl if (batch == null || Objects.equals(batch.getIsDeleted(), 1)) { throw new ServiceException("导入结算单批次不存在"); } - return toVO(batch); + SettlementImportBatchVO vo = toVO(batch); + List> rows = parseRowsJson(batch.getRowsJson()); + if (Func.isEmpty(rows) && !STATUS_DRAFT.equals(batch.getImportStatus())) { + rows = buildRowsFromWaybills(listBatchWaybills(batch)); + } + vo.setRows(rows); + return vo; } @Override public void exportTemplate(HttpServletResponse response) { - List sample = new ArrayList<>(); - ExcelUtil.export(response, "导入结算单模板", "导入结算单模板", sample, SettlementImportTemplateExcel.class); + SettlementImportTemplateExporter.export(response, loadTransportTypeLabels(), loadEnabledFeeItemNames()); } @Override @@ -270,6 +279,7 @@ public class SettlementImportBatchServiceImpl @Override @Transactional(rollbackFor = Exception.class) public SettlementImportBatchVO saveDraft(SettlementImportBatchRequest request) { + prepareSettlementRows(request); SettlementImportBatch batch = buildDraftBatch(request); if (Func.isNotEmpty(request.getId())) { SettlementImportBatch oldBatch = getById(request.getId()); @@ -282,10 +292,14 @@ public class SettlementImportBatchServiceImpl batch.setId(oldBatch.getId()); batch.setBatchNo(oldBatch.getBatchNo()); } - batch.setWaybillCount(0); - batch.setDetailCount(0); + List> draftRows = sanitizeRowsForStorage(request.getRows()); + batch.setRowsJson(JsonUtil.toJson(draftRows)); + batch.setWaybillCount(primaryRows(draftRows).size()); + batch.setDetailCount(draftRows.size()); saveOrUpdate(batch); - return toVO(batch); + SettlementImportBatchVO vo = toVO(batch); + vo.setRows(draftRows); + return vo; } @Override @@ -399,8 +413,73 @@ public class SettlementImportBatchServiceImpl } batch.setImportStatus(STATUS_COMPLETED); batch.setFeeItemNamesJson(JsonUtil.toJson(resolveFeeItemNames(request))); + batch.setRowsJson(JsonUtil.toJson(sanitizeRowsForStorage(request.getRows()))); updateById(batch); - return toVO(batch); + SettlementImportBatchVO vo = toVO(batch); + vo.setRows(parseRowsJson(batch.getRowsJson())); + return vo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeDraft(Long id) { + if (id == null) { + throw new ServiceException("请选择需要删除的批次"); + } + SettlementImportBatch batch = getById(id); + if (batch == null || Objects.equals(batch.getIsDeleted(), 1)) { + throw new ServiceException("导入结算单批次不存在"); + } + if (!STATUS_DRAFT.equals(batch.getImportStatus())) { + throw new ServiceException("仅草稿状态的批次允许删除"); + } + deleteLogic(List.of(id)); + } + + @Override + public List> historyWaybills(SettlementImportBatchRequest request) { + if (request == null || Func.isEmpty(request.getProjectId()) + || Func.isEmpty(request.getCustomerContractId()) + || Func.isEmpty(request.getCarrierContractId())) { + throw new ServiceException("请先选择项目、客户合同和承运商合同"); + } + String settlementType = Func.isEmpty(request.getSettlementType()) + ? TYPE_RECEIVABLE : request.getSettlementType(); + if (!TYPE_RECEIVABLE.equals(settlementType) && !TYPE_PAYABLE.equals(settlementType)) { + throw new ServiceException("结算类型不正确"); + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(Waybill::getIsDeleted, 0) + .eq(Waybill::getProjectId, request.getProjectId()) + .eq(Waybill::getContractId, request.getCustomerContractId()) + .eq(Waybill::getCarrierContractId, request.getCarrierContractId()) + .apply("not exists (select 1 from blade_receivable_payable_detail d" + + " where d.waybill_id = blade_waybill.id and d.settlement_type = {0}" + + " and d.is_deleted = 0)", settlementType) + .orderByDesc(Waybill::getCreateTime) + .last("limit 500"); + List waybills = waybillService.list(wrapper); + List> result = new ArrayList<>(); + for (Waybill waybill : waybills) { + Map item = new LinkedHashMap<>(); + item.put("id", waybill.getId()); + item.put("waybillNo", waybill.getWaybillNo()); + item.put("vehicleNo", waybill.getVehicleNo()); + item.put("transportType", waybill.getTransportType()); + item.put("departureAddress", waybill.getDepartureAddress()); + item.put("arrivalAddress", waybill.getArrivalAddress()); + item.put("cargoName", waybill.getCargoName()); + item.put("cargoType", waybill.getCargoType()); + item.put("quantity", waybill.getQuantity()); + item.put("quantityUnit", waybill.getQuantityUnit()); + item.put("model", waybill.getModel()); + item.put("mileage", waybill.getMileage()); + item.put("actualStartDate", waybill.getStartDate()); + item.put("actualEndDate", waybill.getEndDate()); + item.put("driverName", waybill.getDriverName()); + result.add(item); + } + return result; } @Override @@ -608,9 +687,9 @@ public class SettlementImportBatchServiceImpl .eq(Waybill::getEndDate, endDate) .eq(Waybill::getDepartureAddress, departureAddress) .eq(Waybill::getArrivalAddress, arrivalAddress) - .eq(Waybill::getCargoName, cargoName) - .orderByDesc(Waybill::getCreateTime) - .last("limit 3"); + .eq(Waybill::getCargoName, cargoName); + applyUnsettledWaybillFilter(wrapper, request.getSettlementType()); + wrapper.orderByDesc(Waybill::getCreateTime).last("limit 3"); List matches = waybillService.list(wrapper); if (Func.isEmpty(matches)) { throw new ServiceException("未匹配到系统已有运单(项目+车号+运输完成日期+客户+承运商+起运地+目的地+货物名称)"); @@ -623,16 +702,17 @@ public class SettlementImportBatchServiceImpl /** 手工指定运单号时,在当前项目/客户/承运商范围内精确匹配。 */ private Waybill matchHistoryWaybillByNo(SettlementImportBatchRequest request, String waybillNo) { - List matches = waybillService.list(Wrappers.lambdaQuery() + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .eq(Waybill::getIsDeleted, 0) .eq(Waybill::getProjectId, request.getProjectId()) .eq(Waybill::getContractId, request.getCustomerContractId()) .eq(Waybill::getCarrierContractId, request.getCarrierContractId()) - .eq(Waybill::getWaybillNo, waybillNo) - .orderByDesc(Waybill::getCreateTime) - .last("limit 2")); + .eq(Waybill::getWaybillNo, waybillNo); + applyUnsettledWaybillFilter(wrapper, request.getSettlementType()); + wrapper.orderByDesc(Waybill::getCreateTime).last("limit 2"); + List matches = waybillService.list(wrapper); if (Func.isEmpty(matches)) { - throw new ServiceException("未匹配到运单号「" + waybillNo + "」对应的系统已有运单"); + throw new ServiceException("未匹配到运单号「" + waybillNo + "」对应的未结算运单"); } if (matches.size() > 1) { throw new ServiceException("运单号「" + waybillNo + "」匹配到多条运单,请核对后重试"); @@ -640,6 +720,14 @@ public class SettlementImportBatchServiceImpl return matches.get(0); } + /** 仅保留尚未生成对应结算类型应收/应付明细的运单。 */ + private void applyUnsettledWaybillFilter(LambdaQueryWrapper wrapper, String settlementType) { + String type = Func.isEmpty(settlementType) ? TYPE_RECEIVABLE : settlementType; + wrapper.apply("not exists (select 1 from blade_receivable_payable_detail d" + + " where d.waybill_id = blade_waybill.id and d.settlement_type = {0}" + + " and d.is_deleted = 0)", type); + } + private void prepareSettlementRows(SettlementImportBatchRequest request) { if (request == null || Func.isEmpty(request.getRows())) { return; @@ -1161,4 +1249,123 @@ public class SettlementImportBatchServiceImpl String merged = Func.isEmpty(original) ? "作废原因:" + reason : original + ";作废原因:" + reason; return limit(merged, 200); } + + @SuppressWarnings("unchecked") + private List> parseRowsJson(String rowsJson) { + if (Func.isEmpty(rowsJson)) { + return List.of(); + } + List rawList = JsonUtil.parseArray(rowsJson, Map.class); + if (rawList == null || rawList.isEmpty()) { + return List.of(); + } + List> rows = new ArrayList<>(); + for (Map raw : rawList) { + if (raw == null) { + continue; + } + rows.add(new LinkedHashMap<>(raw)); + } + return rows; + } + + private List> sanitizeRowsForStorage(List> rows) { + if (Func.isEmpty(rows)) { + return List.of(); + } + List> sanitized = new ArrayList<>(); + for (Map row : rows) { + if (row == null) { + continue; + } + Map copy = new LinkedHashMap<>(); + for (Map.Entry entry : row.entrySet()) { + String key = entry.getKey(); + if (key == null || key.startsWith("_")) { + continue; + } + copy.put(key, entry.getValue()); + } + sanitized.add(copy); + } + return sanitized; + } + + private List loadTransportTypeLabels() { + LinkedHashSet labels = new LinkedHashSet<>(); + try { + List dictBizList = DictBizCache.getList("transport_type"); + if (Func.isNotEmpty(dictBizList)) { + dictBizList.stream() + .map(DictBiz::getDictValue) + .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) { + // 忽略 + } + if (labels.isEmpty()) { + return List.of("公路运输", "铁路运输", "水路运输", "航空运输"); + } + return new ArrayList<>(labels); + } + + private List loadEnabledFeeItemNames() { + R> response = sysClient.getFeeItems(false); + if (response == null || !response.isSuccess() || response.getData() == null) { + return List.of(); + } + return response.getData().stream() + .filter(item -> item != null && (item.getStatus() == null || Objects.equals(item.getStatus(), 1))) + .map(FeeItem::getName) + .filter(Func::isNotEmpty) + .map(String::trim) + .distinct() + .toList(); + } + + /** 历史已完成批次无 rows_json 时,按关联运单回填查看明细。 */ + private List> buildRowsFromWaybills(List waybills) { + if (Func.isEmpty(waybills)) { + return List.of(); + } + List> rows = new ArrayList<>(); + int serial = 1; + for (Waybill waybill : waybills) { + Map row = new LinkedHashMap<>(); + row.put("serialNumber", serial++); + row.put("waybillNo", waybill.getWaybillNo()); + row.put("vehicleNo", waybill.getVehicleNo()); + row.put("transportType", waybill.getTransportType()); + row.put("driverName", waybill.getDriverName()); + row.put("departureAddress", waybill.getDepartureAddress()); + row.put("arrivalAddress", waybill.getArrivalAddress()); + row.put("cargoName", waybill.getCargoName()); + row.put("cargoType", waybill.getCargoType()); + row.put("quantity", waybill.getQuantity()); + row.put("quantityUnit", waybill.getQuantityUnit()); + row.put("specification", waybill.getSpecification()); + row.put("model", waybill.getModel()); + row.put("mileage", waybill.getMileage()); + row.put("unitPrice", waybill.getUnitPrice()); + row.put("actualStartDate", waybill.getStartDate()); + row.put("actualEndDate", waybill.getEndDate()); + row.put("remark", waybill.getRemark()); + rows.add(row); + } + return rows; + } } diff --git a/doc/sql/transport/blade_settlement_import_batch_20261015.sql b/doc/sql/transport/blade_settlement_import_batch_20261015.sql index 6578be14..89c7b872 100644 --- a/doc/sql/transport/blade_settlement_import_batch_20261015.sql +++ b/doc/sql/transport/blade_settlement_import_batch_20261015.sql @@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS `blade_settlement_import_batch` ( `pre_settlement_id` bigint DEFAULT NULL COMMENT '预结算单ID', `pre_settlement_no` varchar(100) DEFAULT NULL COMMENT '预结算单号', `fee_item_names_json` longtext COMMENT '动态费用列名JSON数组', + `rows_json` longtext COMMENT '导入明细行JSON数组', `remark` varchar(200) DEFAULT NULL COMMENT '备注', `waybill_import_batch_id` bigint DEFAULT NULL COMMENT '关联底层运单导入批次,可空', `create_user` bigint DEFAULT NULL COMMENT '创建人', @@ -45,6 +46,7 @@ ALTER TABLE `blade_settlement_import_batch` ADD COLUMN `settlement_bill_type` va ALTER TABLE `blade_settlement_import_batch` ADD COLUMN `pre_settlement_id` bigint DEFAULT NULL COMMENT '预结算单ID' AFTER `formal_settlement_no`; ALTER TABLE `blade_settlement_import_batch` ADD COLUMN `pre_settlement_no` varchar(100) DEFAULT NULL COMMENT '预结算单号' AFTER `pre_settlement_id`; ALTER TABLE `blade_settlement_import_batch` ADD KEY `idx_settlement_import_pre` (`pre_settlement_id`); +ALTER TABLE `blade_settlement_import_batch` ADD COLUMN `rows_json` longtext COMMENT '导入明细行JSON数组' AFTER `fee_item_names_json`; -- 运单关联导入结算批次(可选,若列已存在可忽略报错) ALTER TABLE `blade_waybill` ADD COLUMN `settlement_import_batch_id` bigint DEFAULT NULL COMMENT '导入结算单批次ID'; @@ -56,5 +58,11 @@ INSERT INTO `blade_menu` (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source` (2090000000000002082,2090000000000002080,'settlement_import_add','新增','settlement_import_add','','',2,2,0,1,NULL,'',0), (2090000000000002083,2090000000000002080,'settlement_import_detail','详情','settlement_import_detail','','',3,2,0,1,NULL,'',0), (2090000000000002084,2090000000000002080,'settlement_import_void','作废','settlement_import_void','','',4,2,0,1,NULL,'',0), -(2090000000000002085,2090000000000002080,'settlement_import_template','下载模板','settlement_import_template','','',5,2,0,1,NULL,'',0) +(2090000000000002085,2090000000000002080,'settlement_import_template','下载模板','settlement_import_template','','',5,2,0,1,NULL,'',0), +(2090000000000002086,2090000000000002080,'settlement_import_delete','删除','settlement_import_delete','','',6,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`path`=VALUES(`path`),`is_deleted`=0; + +-- 已建表环境增量菜单(已存在可忽略) +INSERT INTO `blade_menu` (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) VALUES +(2090000000000002086,2090000000000002080,'settlement_import_delete','删除','settlement_import_delete','','',6,2,0,1,NULL,'',0) ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`path`=VALUES(`path`),`is_deleted`=0; diff --git a/doc/sql/transport/blade_settlement_import_batch_alter_20261018.sql b/doc/sql/transport/blade_settlement_import_batch_alter_20261018.sql new file mode 100644 index 00000000..218ed51e --- /dev/null +++ b/doc/sql/transport/blade_settlement_import_batch_alter_20261018.sql @@ -0,0 +1,7 @@ +-- 导入结算单:草稿明细落库 + 删除菜单 +ALTER TABLE `blade_settlement_import_batch` + ADD COLUMN `rows_json` longtext COMMENT '导入明细行JSON数组' AFTER `fee_item_names_json`; + +INSERT INTO `blade_menu` (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) VALUES +(2090000000000002086,2090000000000002080,'settlement_import_delete','删除','settlement_import_delete','','',6,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`path`=VALUES(`path`),`is_deleted`=0;