✨ 完善导入结算单模板校验、草稿明细与历史运单选择
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+3
@@ -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;
|
||||
|
||||
|
||||
+4
@@ -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<String> feeItemNames;
|
||||
|
||||
@Schema(description = "导入明细行")
|
||||
private List<Map<String, Object>> rows;
|
||||
}
|
||||
|
||||
+15
@@ -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<List<Map<String, Object>>> historyWaybills(SettlementImportBatchRequest request) {
|
||||
return R.data(settlementImportBatchService.historyWaybills(request));
|
||||
}
|
||||
}
|
||||
|
||||
+93
-7
@@ -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<String> DATE_FIELDS = Set.of(
|
||||
"actualStartDate", "actualEndDate", "planStartDate", "planEndDate"
|
||||
);
|
||||
private static final Set<String> OPTIONAL_DECIMAL_FIELDS = Set.of("mileage", "unitPrice");
|
||||
|
||||
private SettlementImportFeeReader() {
|
||||
}
|
||||
@@ -193,23 +205,26 @@ public final class SettlementImportFeeReader {
|
||||
for (Map.Entry<Integer, String> 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;
|
||||
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* 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>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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<String> FIXED_HEADERS_BEFORE_FEE = List.of(
|
||||
"序号",
|
||||
"原始单号",
|
||||
"配载标识号",
|
||||
"*车牌号/航班号/船号/班列号",
|
||||
"*运输方式",
|
||||
"司机/船长姓名",
|
||||
"司机/船长手机号",
|
||||
"*发货地址",
|
||||
"发货联系人",
|
||||
"发货联系人电话",
|
||||
"*到货地址",
|
||||
"收货联系人",
|
||||
"收货联系人电话",
|
||||
"*货物名称",
|
||||
"*货物类型",
|
||||
"包装",
|
||||
"*数量",
|
||||
"*数量单位",
|
||||
"规格",
|
||||
"型号",
|
||||
"里程(km)",
|
||||
"运输单价",
|
||||
"*运输费"
|
||||
);
|
||||
|
||||
private static final List<String> FIXED_HEADERS_AFTER_FEE = List.of(
|
||||
"结算金额",
|
||||
"*实际发货时间",
|
||||
"*实际完成时间",
|
||||
"预计发货时间",
|
||||
"预计完成时间",
|
||||
"备注",
|
||||
"同一运单标识号"
|
||||
);
|
||||
|
||||
private static final List<String> DEFAULT_FEE_ITEMS = List.of("装卸费", "过路费");
|
||||
|
||||
private SettlementImportTemplateExporter() {
|
||||
}
|
||||
|
||||
public static void export(HttpServletResponse response, List<String> transportTypeOptions,
|
||||
List<String> feeItemNames) {
|
||||
List<String> headers = buildHeaders(feeItemNames);
|
||||
List<String> 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<String> buildHeaders(List<String> feeItemNames) {
|
||||
List<String> headers = new ArrayList<>(FIXED_HEADERS_BEFORE_FEE);
|
||||
Set<String> 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<String> transportOptions,
|
||||
List<String> feeItemNames) {
|
||||
Sheet sheet = workbook.createSheet("填写说明");
|
||||
CellStyle titleStyle = workbook.createCellStyle();
|
||||
Font titleFont = workbook.createFont();
|
||||
titleFont.setBold(true);
|
||||
titleStyle.setFont(titleFont);
|
||||
List<String> 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<String> 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<String> defaultTransportOptions(List<String> transportTypeOptions) {
|
||||
if (transportTypeOptions != null && !transportTypeOptions.isEmpty()) {
|
||||
return transportTypeOptions;
|
||||
}
|
||||
return List.of("公路运输", "铁路运输", "水路运输", "航空运输");
|
||||
}
|
||||
}
|
||||
+8
@@ -66,4 +66,12 @@ public interface ISettlementImportBatchService extends BaseService<SettlementImp
|
||||
SettlementImportBatchVO confirm(SettlementImportBatchRequest request);
|
||||
|
||||
void voidBatch(SettlementImportVoidRequest request);
|
||||
|
||||
/** 删除草稿批次(仅 draft 可删)。 */
|
||||
void removeDraft(Long id);
|
||||
|
||||
/**
|
||||
* 历史运单可选列表:相同项目、客户合同、承运商合同下,尚未生成对应应收/应付明细的运单。
|
||||
*/
|
||||
List<Map<String, Object>> historyWaybills(SettlementImportBatchRequest request);
|
||||
}
|
||||
|
||||
+223
-16
@@ -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<Map<String, Object>> 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<SettlementImportTemplateExcel> 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<Map<String, Object>> 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<Map<String, Object>> 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<Waybill> wrapper = Wrappers.<Waybill>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<Waybill> waybills = waybillService.list(wrapper);
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Waybill waybill : waybills) {
|
||||
Map<String, Object> 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<Waybill> 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<Waybill> matches = waybillService.list(Wrappers.<Waybill>lambdaQuery()
|
||||
LambdaQueryWrapper<Waybill> wrapper = Wrappers.<Waybill>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<Waybill> 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<Waybill> 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<Map<String, Object>> parseRowsJson(String rowsJson) {
|
||||
if (Func.isEmpty(rowsJson)) {
|
||||
return List.of();
|
||||
}
|
||||
List<Map> rawList = JsonUtil.parseArray(rowsJson, Map.class);
|
||||
if (rawList == null || rawList.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
for (Map raw : rawList) {
|
||||
if (raw == null) {
|
||||
continue;
|
||||
}
|
||||
rows.add(new LinkedHashMap<>(raw));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> sanitizeRowsForStorage(List<Map<String, Object>> rows) {
|
||||
if (Func.isEmpty(rows)) {
|
||||
return List.of();
|
||||
}
|
||||
List<Map<String, Object>> sanitized = new ArrayList<>();
|
||||
for (Map<String, Object> row : rows) {
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> copy = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, Object> 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<String> loadTransportTypeLabels() {
|
||||
LinkedHashSet<String> labels = new LinkedHashSet<>();
|
||||
try {
|
||||
List<DictBiz> 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<org.springblade.system.pojo.entity.Dict> 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<String> loadEnabledFeeItemNames() {
|
||||
R<List<FeeItem>> 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<Map<String, Object>> buildRowsFromWaybills(List<Waybill> waybills) {
|
||||
if (Func.isEmpty(waybills)) {
|
||||
return List.of();
|
||||
}
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
int serial = 1;
|
||||
for (Waybill waybill : waybills) {
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user