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();