From 766100a55cd215271dd1d9eb986eaf93413f62ca Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 19 Oct 2026 23:28:50 +0800 Subject: [PATCH 1/4] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E8=BD=A6?= =?UTF-8?q?=E8=BE=86=E9=95=BF=E6=9C=9F=E6=9C=89=E6=95=88=E3=80=81=E7=BB=93?= =?UTF-8?q?=E7=AE=97=E5=AF=BC=E5=85=A5=E4=B8=8E=E8=B0=83=E5=BA=A6=E7=AB=AF?= =?UTF-8?q?=E8=BD=A8=E8=BF=B9=E9=89=B4=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 车辆勾选长期有效不再强校验有效期;结算导入模板去掉结算金额并自动合计,取消运费合计勾稽;调度端所属组织为空不拦截,并新增仅登录态的 manage/track。 Co-authored-by: Cursor --- .../controller/ManageWaybillController.java | 11 +++++ .../excel/SettlementImportFeeReader.java | 40 ++++++++++++++----- .../excel/SettlementImportTemplateExcel.java | 3 +- .../SettlementImportTemplateExporter.java | 5 +-- .../impl/DriverWaybillServiceImpl.java | 6 +++ .../impl/ManageWaybillServiceImpl.java | 13 ++++-- .../impl/TransportVehicleServiceImpl.java | 4 +- .../impl/WaybillImportBatchServiceImpl.java | 40 +++---------------- .../impl/ManageWaybillOrgScopeTest.java | 12 +++--- 9 files changed, 75 insertions(+), 59 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ManageWaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ManageWaybillController.java index ee10e0b6..67c2d15c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ManageWaybillController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ManageWaybillController.java @@ -39,6 +39,7 @@ import org.springblade.transport.pojo.vo.AdminVehicleOptionVO; import org.springblade.transport.pojo.vo.AdminWaybillCardVO; import org.springblade.transport.pojo.vo.AdminWaybillDetailVO; import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO; +import org.springblade.transport.pojo.vo.WaybillTrackVO; import org.springblade.transport.service.IManageWaybillService; import org.springblade.transport.service.IWaybillService; import org.springframework.web.bind.annotation.GetMapping; @@ -159,4 +160,14 @@ public class ManageWaybillController extends BladeController { return R.status(waybillService.savePunchRecord(request)); } + @PostMapping("/track") + @ApiOperationSupport(order = 11) + @Operation(summary = "车辆历史轨迹", description = "小程序调度端:仅需登录态,按运单绑定车牌和日期区间查询 LBS 轨迹") + public R track( + @Parameter(description = "运单ID", required = true) @RequestParam Long id, + @Parameter(description = "开始日期 YYYY-MM-DD", required = true) @RequestParam String startDate, + @Parameter(description = "结束日期 YYYY-MM-DD", required = true) @RequestParam String endDate) { + return R.data(waybillService.trackVehicle(id, startDate, endDate)); + } + } 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 cc43d6e1..12b44580 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 @@ -54,7 +54,8 @@ import java.util.Set; /** * 导入结算单 Excel 读取器(Apache POI)。 *

- * 定位「*运输费」或「运输费」列,其后直到「结算金额」为动态费用列。 + * 定位「*运输费」或「运输费」列,其后直到日期/备注等固定列(兼容旧模板中的「结算金额」)为动态费用列。 + * 结算金额不由模板填写,解析时按「运输费 + 各费用项」自动合计。 * * @author Chill */ @@ -66,6 +67,14 @@ public final class SettlementImportFeeReader { "actualStartDate", "actualEndDate", "planStartDate", "planEndDate" ); private static final Set OPTIONAL_DECIMAL_FIELDS = Set.of("mileage", "unitPrice"); + /** 费用列结束边界:其后为固定业务列;含旧模板「结算金额」以便兼容历史文件 */ + private static final Set FEE_END_HEADERS = Set.of( + "结算金额", + "*实际发货时间", "实际发货时间", + "*实际完成时间", "实际完成时间", + "预计发货时间", "预计完成时间", + "备注", "同一运单标识号" + ); private SettlementImportFeeReader() { } @@ -119,15 +128,12 @@ public final class SettlementImportFeeReader { if (transportFeeColumn < 0) { throw new ServiceException("未找到「*运输费」或「运输费」列"); } - int settlementAmountColumn = findColumnExact(headers, "结算金额"); List feeColumns = new ArrayList<>(); List feeItemNames = new ArrayList<>(); - int endColumn = settlementAmountColumn > transportFeeColumn - ? settlementAmountColumn - : headers.keySet().stream().mapToInt(Integer::intValue).max().orElse(transportFeeColumn) + 1; + int endColumn = findFeeEndColumn(headers, transportFeeColumn); for (int column = transportFeeColumn + 1; column < endColumn; column++) { String name = headers.get(column); - if (name == null || name.isBlank() || "结算金额".equals(name)) { + if (name == null || name.isBlank() || FEE_END_HEADERS.contains(name)) { continue; } feeColumns.add(column); @@ -229,7 +235,8 @@ public final class SettlementImportFeeReader { } else if ("transportFee".equals(field) || "quantity".equals(field)) { mapped.put(field, decimal(cellString(poiCell), trimmedHeader, excelRowIndex)); } else if ("settlementAmount".equals(field)) { - mapped.put(field, optionalDecimal(cellString(poiCell), trimmedHeader, excelRowIndex)); + // 旧模板可能仍含结算金额列,读入后忽略,统一按运输费+费用项自动合计 + continue; } else if ("serialNumber".equals(field)) { mapped.put(field, intValue(cellString(poiCell))); } else { @@ -255,9 +262,7 @@ public final class SettlementImportFeeReader { BigDecimal feeItemTotal = feeItems.values().stream() .reduce(BigDecimal.ZERO, BigDecimal::add) .setScale(2, RoundingMode.HALF_UP); - BigDecimal settlementAmount = mapped.get("settlementAmount") == null - ? transportFee.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP) - : toDecimal(mapped.get("settlementAmount")); + BigDecimal settlementAmount = transportFee.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP); mapped.put("settlementAmount", settlementAmount); mapped.put("结算金额", settlementAmount); mapped.put("feeItems", feeItems); @@ -284,6 +289,21 @@ public final class SettlementImportFeeReader { return findColumnExact(headers, "运输费"); } + /** 费用列右边界:取运输费之后第一个固定结束表头列下标;无则取表头最大下标 + 1。 */ + private static int findFeeEndColumn(Map headers, int transportFeeColumn) { + int endColumn = headers.keySet().stream().mapToInt(Integer::intValue).max().orElse(transportFeeColumn) + 1; + for (Map.Entry entry : headers.entrySet()) { + int column = entry.getKey(); + if (column <= transportFeeColumn) { + continue; + } + if (FEE_END_HEADERS.contains(entry.getValue()) && column < endColumn) { + endColumn = column; + } + } + return endColumn; + } + private static int findColumnExact(Map headers, String header) { return headers.entrySet().stream() .filter(entry -> header.equals(entry.getValue())) 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 a060b15e..871599eb 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 @@ -100,7 +100,8 @@ public class SettlementImportTemplateExcel implements Serializable { private BigDecimal loadingFee; @ExcelProperty("过路费") private BigDecimal tollFee; - @ExcelProperty("结算金额") + /** 模板已去掉结算金额列;仅失败明细内部保留,不导出为表头 */ + @ExcelIgnore private BigDecimal settlementAmount; @ExcelProperty("*实际发货时间") private String actualStartDate; 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 a33dbb6c..91eb317e 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 @@ -85,7 +85,6 @@ public final class SettlementImportTemplateExporter { ); private static final List FIXED_HEADERS_AFTER_FEE = List.of( - "结算金额", "*实际发货时间", "*实际完成时间", "预计发货时间", @@ -159,8 +158,8 @@ public final class SettlementImportTemplateExporter { 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("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()) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java index 59e02bb0..534f043d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java @@ -349,9 +349,14 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { /** * 按打卡记录 / 计划时间推导轨迹查询起止日期 + *

+ * TODO(临时):当前仅查当天,便于联调 LBS;恢复正式逻辑时删掉本段 early-return。 */ private LocalDate[] resolveTrackDateRange(Waybill waybill) { LocalDate today = LocalDate.now(); + // TEMP: 行驶轨迹时间段临时改为仅当天 + return new LocalDate[]{today, today}; + /* List punchDates = new ArrayList<>(); nodePunchMapper.selectList(Wrappers.lambdaQuery() .eq(WaybillNodePunch::getWaybillId, waybill.getId()) @@ -383,6 +388,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { return new LocalDate[]{waybill.getStartDate(), today}; } return new LocalDate[]{today.minusDays(3), today}; + */ } @Override diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java index f8701641..4b1e5c93 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java @@ -886,11 +886,18 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { /** * 调度端可见组织:普通用户(含各级角色)只出自己所属组织,超级管理员不过滤。 *

- * 与后台运单列表同一条规则({@link TransportBusinessSupport#resolveDeptFilterIds}), - * 小程序端不提供「全部组织」开关,故两个入参固定传空;详情与重新派单不按组织收口。 + * 小程序不校验「所属组织不能为空」:账号没有组织时不按组织收口,避免首页、待处理等接口直接报错。 + * 详情与重新派单本身不走这里。 */ private List manageableDeptIds() { - return TransportBusinessSupport.resolveDeptFilterIds(null, null, "运单管理"); + if (TransportBusinessSupport.isAdministrator()) { + return List.of(); + } + Long deptId = Func.firstLong(AuthUtil.getDeptId()); + if (Func.isEmpty(deptId) || deptId <= 0) { + return List.of(); + } + return List.of(deptId); } /** diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java index e28d7329..d9e1c734 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java @@ -291,13 +291,13 @@ public class TransportVehicleServiceImpl extends BaseServiceImpl row, List errors) { - String freightTotalStr = stringValue(row, "freightTotal", "运费合计"); - if (Func.isEmpty(freightTotalStr) || isImportOptionalNumberEmpty(freightTotalStr)) { - return; - } - - try { - BigDecimal freightTotal = new BigDecimal(freightTotalStr); - String freightStr = stringValue(row, "freight", "运费"); - String otherFeeTotalStr = stringValue(row, "otherFeeTotal", "其他费用合计"); - - BigDecimal freight = Func.isNotEmpty(freightStr) && !isImportOptionalNumberEmpty(freightStr) - ? new BigDecimal(freightStr) : BigDecimal.ZERO; - BigDecimal otherFeeTotal = Func.isNotEmpty(otherFeeTotalStr) && !isImportOptionalNumberEmpty(otherFeeTotalStr) - ? new BigDecimal(otherFeeTotalStr) : BigDecimal.ZERO; - - BigDecimal calculatedTotal = freight.add(otherFeeTotal); - if (freightTotal.compareTo(calculatedTotal) != 0) { - errors.add("运费合计应等于运费+其他费用合计"); - } - } catch (NumberFormatException e) { - errors.add("运费相关字段格式不正确"); - } - } - private void validateActualStartDate(Map row, boolean isDraft, List errors) { Object actualStartDate = row.get("actualStartDate"); if (actualStartDate == null || String.valueOf(actualStartDate).isBlank()) { diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillOrgScopeTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillOrgScopeTest.java index 15a31a27..95346246 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillOrgScopeTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillOrgScopeTest.java @@ -52,7 +52,7 @@ import static org.mockito.Mockito.when; * 三个取数入口必须同一条规则——列表、待处理、首页三档统计各走一次查询,漏一处就会出现 * 「统计说 46 张、列表只出 18 张」这种对不上的数;待重新派单另走库层分组查询,也要单独带上。 *

- * 组织取不到时报错而不是放行:放行等于把全租户的运单发给一个没有组织的账号。 + * 所属组织为空时不拦截:小程序调度端不要求账号必须挂组织。 */ class ManageWaybillOrgScopeTest { @@ -190,13 +190,13 @@ class ManageWaybillOrgScopeTest { } @Test - @DisplayName("组织取不到时报错,不退回不带组织条件") - void blankDeptFailsClosed() { + @DisplayName("组织取不到时不拦截,小程序接口不按组织收口") + void blankDeptDoesNotFail() { authUtil.when(AuthUtil::getDeptId).thenReturn(""); - assertThrows(ServiceException.class, - () -> service.pageList(1, 10, null, null, null, null, null, null), - "没有组织的账号不能拿到全租户运单"); + service.pageList(1, 10, null, null, null, null, null, null); + + assertTrue(capturedPageQuery().getDeptIds().isEmpty(), "没有组织时不报错、不按组织过滤"); } // ------------------------------------------------------------ 夹具 From 87222f2e8cbd05d1f6d75434287d373bbc12f114 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 20 Oct 2026 01:50:55 +0800 Subject: [PATCH 2/4] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E9=A2=84=E4=BB=98=E9=A9=B3=E5=9B=9E=E9=87=8D=E6=8F=90?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E6=A0=A1=E6=AD=A3=E7=BB=93=E7=AE=97=E8=B4=B9?= =?UTF-8?q?=E7=94=A8=E4=B8=8E=E8=BF=90=E8=BE=93=E8=AE=A1=E5=88=92=E7=BB=84?= =?UTF-8?q?=E7=BB=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../dto/PaymentApplicationSaveRequest.java | 2 + .../controller/BillLedgerController.java | 11 +- .../transport/service/IBillLedgerService.java | 5 +- .../service/impl/BillLedgerServiceImpl.java | 47 ++++-- .../impl/PaymentApplicationServiceImpl.java | 131 +++++++++++++--- .../impl/PreSettlementServiceImpl.java | 147 ++++++++++-------- .../ReceivablePayableDetailServiceImpl.java | 38 +++-- .../impl/TransportPlanServiceImpl.java | 32 +++- 8 files changed, 302 insertions(+), 111 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java index d608d7cb..d69690f8 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java @@ -36,6 +36,8 @@ public class PaymentApplicationSaveRequest implements Serializable { private List settlementIds; private Long preSettlementId; private List preSettlementIds; + /** 预结算单号。驳回重提时用于在主键失真后找回预结算单 */ + private String preSettlementNo; private Long projectId; private String projectName; private Long deptId; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/BillLedgerController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/BillLedgerController.java index 24d695ca..7cce3ffc 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/BillLedgerController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/BillLedgerController.java @@ -61,8 +61,9 @@ public class BillLedgerController extends BladeController { @ApiOperationSupport(order = 4) @Operation(summary = "付款申请可用汇票") public R> availableOptions(@RequestParam(required = false) String keyword, - @RequestParam(required = false) Long deptId, @RequestParam(required = false) Long selectedId) { - return R.data(billLedgerService.availableOptions(keyword, deptId, selectedId)); + @RequestParam(required = false) Long deptId, @RequestParam(required = false) Long selectedId, + @RequestParam(required = false) String payerName, @RequestParam(required = false) String payeeName) { + return R.data(billLedgerService.availableOptions(keyword, deptId, selectedId, payerName, payeeName)); } @GetMapping("/available-page") @@ -70,8 +71,10 @@ public class BillLedgerController extends BladeController { @Operation(summary = "付款申请可用汇票分页") public R> availablePage(Query pageQuery, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long deptId, - @RequestParam(required = false) Long selectedId) { - return R.data(billLedgerService.availablePage(Condition.getPage(pageQuery), keyword, deptId, selectedId)); + @RequestParam(required = false) Long selectedId, @RequestParam(required = false) String payerName, + @RequestParam(required = false) String payeeName) { + return R.data(billLedgerService.availablePage(Condition.getPage(pageQuery), keyword, deptId, selectedId, + payerName, payeeName)); } @PostMapping("/submit") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IBillLedgerService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IBillLedgerService.java index abd6e5d2..c46522e3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IBillLedgerService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IBillLedgerService.java @@ -18,8 +18,9 @@ public interface IBillLedgerService extends BaseService { IPage selectPage(IPage page, BillLedgerVO query); BillLedgerVO detail(Long id); Map expiryCounts(); - List availableOptions(String keyword, Long deptId, Long selectedId); - IPage availablePage(IPage page, String keyword, Long deptId, Long selectedId); + List availableOptions(String keyword, Long deptId, Long selectedId, String payerName, String payeeName); + IPage availablePage(IPage page, String keyword, Long deptId, Long selectedId, + String payerName, String payeeName); Long submit(BillLedgerSaveRequest request); void removeLedger(Long id); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java index a2728da7..a20a4e0e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java @@ -86,14 +86,15 @@ public class BillLedgerServiceImpl extends BaseServiceImpl availableOptions(String keyword, Long deptId, Long selectedId) { - return availableList(keyword, deptId, selectedId); + public List availableOptions(String keyword, Long deptId, Long selectedId, String payerName, + String payeeName) { + return availableList(keyword, deptId, selectedId, payerName, payeeName); } @Override public IPage availablePage(IPage page, String keyword, Long deptId, - Long selectedId) { - List available = availableList(keyword, deptId, selectedId); + Long selectedId, String payerName, String payeeName) { + List available = availableList(keyword, deptId, selectedId, payerName, payeeName); long current = Math.max(page.getCurrent(), 1); long size = Math.max(page.getSize(), 1); long from = Math.min((current - 1) * size, available.size()); @@ -103,27 +104,50 @@ public class BillLedgerServiceImpl extends BaseServiceImpl availableList(String keyword, Long deptId, Long selectedId) { + private List availableList(String keyword, Long deptId, Long selectedId, String payerName, + String payeeName) { LocalDate today = LocalDate.now(); - // 付款申请仅可选:收票类型、未到期、有可用余额;已选中的汇票始终带回以便回显 + // 未到期且有余额;收票且部门可用,或出票/收票单位与结算收付款方一致(开票、收票都可) return list(Wrappers.lambdaQuery() .and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(BillLedger::getBillNo, keyword) .or().like(BillLedger::getIssuerName, keyword) .or().like(BillLedger::getReceiverName, keyword)) .and(wrapper -> wrapper .and(available -> available - .eq(BillLedger::getBillType, "received") .ge(BillLedger::getMaturityDate, today) .gt(BillLedger::getAvailableBalance, BigDecimal.ZERO)) .or(selectedId != null, child -> child.eq(BillLedger::getId, selectedId))) .orderByAsc(BillLedger::getMaturityDate) .orderByDesc(BillLedger::getCreateTime)).stream() .filter(item -> selectedId != null && Objects.equals(item.getId(), selectedId) - || departmentAvailable(item, deptId)) + || selectableBill(item, deptId, payerName, payeeName)) .map(item -> BillLedgerWrapper.build().entityVO(item)) .toList(); } + /** + * 收票且当前部门在可用范围内,或出票单位、收票单位与结算付款方、收款方一致(顺序不限)。 + */ + private boolean selectableBill(BillLedger ledger, Long deptId, String payerName, String payeeName) { + boolean receivedAndDept = "received".equals(ledger.getBillType()) && departmentAvailable(ledger, deptId); + return receivedAndDept || partiesMatch(ledger.getIssuerName(), ledger.getReceiverName(), payerName, payeeName); + } + + private boolean partiesMatch(String issuerName, String receiverName, String payerName, String payeeName) { + String issuer = trimName(issuerName); + String receiver = trimName(receiverName); + String payer = trimName(payerName); + String payee = trimName(payeeName); + if (issuer.isEmpty() || receiver.isEmpty() || payer.isEmpty() || payee.isEmpty()) { + return false; + } + return (issuer.equals(payer) && receiver.equals(payee)) || (issuer.equals(payee) && receiver.equals(payer)); + } + + private String trimName(String value) { + return value == null ? "" : value.trim(); + } + @Override @Transactional(rollbackFor = Exception.class) public Long submit(BillLedgerSaveRequest request) { @@ -141,6 +165,11 @@ public class BillLedgerServiceImpl extends BaseServiceImpl candidates = new java.util.ArrayList<>(); + if (request.getPreSettlementId() != null) { + candidates.add(request.getPreSettlementId()); + } + if (request.getPreSettlementIds() != null) { + request.getPreSettlementIds().stream().filter(Objects::nonNull).forEach(candidates::add); + } + if (request.getSettlementId() != null) { + candidates.add(request.getSettlementId()); + } + if (request.getSettlementIds() != null) { + request.getSettlementIds().stream().filter(Objects::nonNull).forEach(candidates::add); + } + if (entity != null && entity.getPreSettlementId() != null) { + candidates.add(entity.getPreSettlementId()); + } + for (Long candidate : candidates) { + PreSettlement preSettlement = preSettlementMapper.selectById(candidate); + if (preSettlement != null && !Objects.equals(preSettlement.getIsDeleted(), 1)) { + return preSettlement.getId(); + } + } + String preSettlementNo = request.getPreSettlementNo(); + if (Func.isEmpty(preSettlementNo) && entity != null) { + preSettlementNo = entity.getPreSettlementNo(); + } + if (Func.isNotEmpty(preSettlementNo)) { + String settlementNo = preSettlementNo.split("、")[0].trim(); + PreSettlement preSettlement = preSettlementMapper.selectOne(Wrappers.lambdaQuery() + .eq(PreSettlement::getPreSettlementNo, settlementNo) + .eq(PreSettlement::getIsDeleted, 0) + .last("limit 1")); + if (preSettlement != null) { + return preSettlement.getId(); + } + } + return null; + } + private void fillReference(PaymentApplication entity, PaymentApplicationSaveRequest request) { - if ("project_advance".equals(request.getPaymentType())) { + if ("progress_advance".equals(request.getPaymentType())) { + Long preSettlementId = resolveProgressPreSettlementId(request, entity); + request.setPreSettlementId(preSettlementId); + if (preSettlementId == null) { + throw new ServiceException("进度预付必须关联预结算单"); + } + request.setSettlementId(null); + request.setSettlementIds(null); + fillPreSettlementReference(entity, requireApprovedPreSettlement(request.getPreSettlementId())); + } else if ("project_advance".equals(request.getPaymentType())) { ProjectApply project = projectApplyMapper.selectById(request.getProjectId()); if (project == null || Objects.equals(project.getIsDeleted(), 1)) throw new ServiceException("所属项目不存在"); entity.setSettlementId(null); entity.setSettlementNo(null); entity.setPreSettlementId(null); entity.setPreSettlementNo(null); @@ -1981,6 +2037,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 0) throw new ServiceException("申请付款金额超过汇票可用余额"); - if (!departmentAvailable(ledger, deptId)) throw new ServiceException("当前使用部门不在汇票可用部门范围内"); + if (!partyMatched && !departmentAvailable(ledger, deptId)) throw new ServiceException("当前使用部门不在汇票可用部门范围内"); + } + + /** 出票单位、收票单位与结算付款方、收款方一致(顺序不限) */ + private boolean settlementPartiesMatch(BillLedger ledger, Long settlementId, Long preSettlementId) { + String payerName = null; + String payeeName = null; + if (settlementId != null) { + FormalSettlement settlement = formalSettlementMapper.selectById(settlementId); + if (settlement != null && !Objects.equals(settlement.getIsDeleted(), 1)) { + payerName = settlement.getPayerName(); + payeeName = settlement.getPayeeName(); + } + } + if ((payerName == null || payeeName == null) && preSettlementId != null) { + PreSettlement settlement = preSettlementMapper.selectById(preSettlementId); + if (settlement != null && !Objects.equals(settlement.getIsDeleted(), 1)) { + payerName = settlement.getPayerName(); + payeeName = settlement.getPayeeName(); + } + } + String issuer = ledger.getIssuerName() == null ? "" : ledger.getIssuerName().trim(); + String receiver = ledger.getReceiverName() == null ? "" : ledger.getReceiverName().trim(); + String payer = payerName == null ? "" : payerName.trim(); + String payee = payeeName == null ? "" : payeeName.trim(); + if (issuer.isEmpty() || receiver.isEmpty() || payer.isEmpty() || payee.isEmpty()) { + return false; + } + return (issuer.equals(payer) && receiver.equals(payee)) || (issuer.equals(payee) && receiver.equals(payer)); } private void useBillBalance(PaymentApplication entity) { BillLedger ledger = lockedBill(entity.getBillLedgerId()); - validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, null); + validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, null, + entity.getSettlementId(), entity.getPreSettlementId()); BillLedgerUsage exists = lockedBillUsage(entity.getId()); if (exists != null && TransportBusiConstant.APPROVED.equals(exists.getUsageStatus())) { throw new ServiceException("该付款申请已生成汇票使用记录"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java index 2b98c34a..f828826f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -206,6 +206,9 @@ public class PreSettlementServiceImpl extends BaseServiceImpllambdaQuery() .eq(PreSettlementChangeRecord::getPreSettlementId, id) .eq(PreSettlementChangeRecord::getIsDeleted, 0) + .and(wrapper -> wrapper.isNull(PreSettlementChangeRecord::getChangeContent) + .or() + .ne(PreSettlementChangeRecord::getChangeContent, "预结算审批通过")) .orderByDesc(PreSettlementChangeRecord::getChangeTime))); vo.setPrintTemplates(resolvePrintTemplates(settlement)); return vo; @@ -355,8 +358,10 @@ public class PreSettlementServiceImpl extends BaseServiceImpl summaryBefore = created ? new ArrayList<>() : listSummaryFees(settlement.getId()); rebuildSummaryFees(settlement.getId(), true); applySummaryRequest(settlement.getId(), contractChanged ? List.of() : request.getSummaryFees()); + recordSummaryFeeChanges(settlement.getId(), summaryBefore); refreshSettlementAmount(settlement); projectAdvanceWriteOffService.replaceWriteOffs( TransportBusiConstant.SETTLEMENT_BILL_PRE, @@ -425,7 +430,6 @@ public class PreSettlementServiceImpl extends BaseServiceImpl beforeData = new LinkedHashMap<>(); - Map afterData = new LinkedHashMap<>(); - beforeData.put("费用项", row.getFeeItem()); - beforeData.put("调整金额", before); - beforeData.put("结算金额", money(row.getOriginalAmount().add(before))); - afterData.put("费用项", row.getFeeItem()); - afterData.put("调整金额", row.getAdjustAmount()); - afterData.put("结算金额", money(row.getSettlementAmount())); - saveChange(settlementId, "合计费用项", row.getLineNo(), "调整", - "【" + row.getFeeItem() + "-调整金额】从【" + before + "】调整为【" + row.getAdjustAmount() + "】", - "", beforeData, afterData); - } } for (PreSettlementSummaryFee manualRow : manualRows) { if (!retainedManualIds.contains(manualRow.getId())) { summaryFeeMapper.deleteById(manualRow.getId()); - Map beforeData = new LinkedHashMap<>(); - Map afterData = new LinkedHashMap<>(); - beforeData.put("费用类型", manualRow.getFeeType()); - beforeData.put("费用项", manualRow.getFeeItem()); - beforeData.put("调整金额", money(manualRow.getAdjustAmount())); - beforeData.put("结算金额", money(manualRow.getSettlementAmount())); - beforeData.put("备注", manualRow.getRemark()); - afterData.put("费用类型", ""); - afterData.put("费用项", ""); - afterData.put("调整金额", BigDecimal.ZERO.setScale(2)); - afterData.put("结算金额", BigDecimal.ZERO.setScale(2)); - afterData.put("备注", ""); - saveChange(settlementId, "合计费用项", manualRow.getLineNo(), "删除", - "删除合计费用项【" + manualRow.getFeeItem() + "】金额【" + money(manualRow.getSettlementAmount()) + "】", - "", beforeData, afterData); } } for (PreSettlementSummaryFee generatedRow : existingGeneratedRows) { @@ -1563,38 +1531,87 @@ public class PreSettlementServiceImpl extends BaseServiceImpl changes = new ArrayList<>(); + private void recordSummaryFeeChanges(Long settlementId, List beforeRows) { + List> beforeLines = summaryFeeLines(beforeRows); + List> afterLines = summaryFeeLines(listSummaryFees(settlementId)); + if (beforeLines.equals(afterLines)) { + return; + } + String content = summaryFeeChangeContent(beforeLines, afterLines); + if (Func.isEmpty(content)) { + return; + } Map beforeData = new LinkedHashMap<>(); Map afterData = new LinkedHashMap<>(); - if (creating) { - changes.add("新增合计费用项【" + row.getFeeItem() + "】金额【" + money(row.getSettlementAmount()) + "】"); - beforeData.put("费用类型", ""); - beforeData.put("费用项", ""); - beforeData.put("调整金额", BigDecimal.ZERO.setScale(2)); - beforeData.put("结算金额", BigDecimal.ZERO.setScale(2)); - beforeData.put("备注", ""); - afterData.put("费用类型", row.getFeeType()); - afterData.put("费用项", row.getFeeItem()); - afterData.put("调整金额", money(row.getAdjustAmount())); - afterData.put("结算金额", money(row.getSettlementAmount())); - afterData.put("备注", row.getRemark()); - saveChange(settlementId, "合计费用项", row.getLineNo(), "新增", String.join(";", changes), "", - beforeData, afterData); - return; + beforeData.put("结算合计", beforeLines); + afterData.put("结算合计", afterLines); + String operationType = beforeLines.isEmpty() ? "新增" : afterLines.isEmpty() ? "删除" : "调整"; + saveChange(settlementId, "结算合计", null, operationType, content, "", beforeData, afterData); + } + + private List> summaryFeeLines(List rows) { + Map categoryNames = DictBizCache.getList("fee_category").stream() + .collect(Collectors.toMap(DictBiz::getDictKey, DictBiz::getDictValue, (first, second) -> first)); + List> lines = new ArrayList<>(); + for (PreSettlementSummaryFee row : rows) { + String feeType = row.getFeeType() == null ? "" : row.getFeeType(); + Map line = new LinkedHashMap<>(); + line.put("来源", Integer.valueOf(1).equals(row.getManualFlag()) ? "手工新增" : "系统归集"); + line.put("费用类型", categoryNames.getOrDefault(feeType, feeType)); + line.put("费用项", row.getFeeItem() == null ? "" : row.getFeeItem()); + line.put("原金额", money(row.getOriginalAmount())); + line.put("调整金额", money(row.getAdjustAmount())); + line.put("结算金额", money(row.getSettlementAmount())); + line.put("备注", row.getRemark() == null ? "" : row.getRemark()); + lines.add(line); } - appendTextChange(changes, beforeData, afterData, "费用类型", beforeFeeType, row.getFeeType()); - appendTextChange(changes, beforeData, afterData, "费用项", beforeFeeItem, row.getFeeItem()); - appendChange(changes, beforeData, afterData, "调整金额", beforeAdjustAmount, row.getAdjustAmount()); - appendChange(changes, beforeData, afterData, "结算金额", beforeSettlementAmount, row.getSettlementAmount()); - appendTextChange(changes, beforeData, afterData, "备注", beforeRemark, row.getRemark()); - if (changes.isEmpty()) { - return; + return lines; + } + + private String summaryFeeChangeContent(List> beforeLines, List> afterLines) { + Map> beforeMap = indexSummaryLines(beforeLines); + Map> afterMap = indexSummaryLines(afterLines); + List changes = new ArrayList<>(); + afterMap.forEach((key, afterLine) -> { + Map beforeLine = beforeMap.get(key); + String label = summaryLineLabel(afterLine); + if (beforeLine == null) { + changes.add("新增结算合计【" + label + "】结算金额【" + afterLine.get("结算金额") + "】"); + return; + } + if (!Objects.equals(beforeLine.get("原金额"), afterLine.get("原金额")) + || !Objects.equals(beforeLine.get("调整金额"), afterLine.get("调整金额")) + || !Objects.equals(beforeLine.get("结算金额"), afterLine.get("结算金额")) + || !Objects.equals(String.valueOf(beforeLine.get("备注")), String.valueOf(afterLine.get("备注")))) { + changes.add("调整结算合计【" + label + "】结算金额从【" + beforeLine.get("结算金额") + + "】调整为【" + afterLine.get("结算金额") + "】"); + } + }); + beforeMap.forEach((key, beforeLine) -> { + if (!afterMap.containsKey(key)) { + changes.add("删除结算合计【" + summaryLineLabel(beforeLine) + "】结算金额【" + beforeLine.get("结算金额") + "】"); + } + }); + return String.join(";", changes); + } + + private Map> indexSummaryLines(List> lines) { + Map> indexed = new LinkedHashMap<>(); + Map counts = new HashMap<>(); + for (Map line : lines) { + String base = String.valueOf(line.get("来源")) + "|" + line.get("费用类型") + "|" + line.get("费用项"); + int index = counts.merge(base, 1, Integer::sum); + indexed.put(base + "#" + index, line); } - saveChange(settlementId, "合计费用项", row.getLineNo(), "调整", String.join(";", changes), "", - beforeData, afterData); + return indexed; + } + + private String summaryLineLabel(Map line) { + String feeType = String.valueOf(line.getOrDefault("费用类型", "")); + String feeItem = String.valueOf(line.getOrDefault("费用项", "")); + String source = String.valueOf(line.getOrDefault("来源", "")); + String name = feeType.isBlank() ? feeItem : feeType + "-" + feeItem; + return source + " " + name; } private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType, diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java index f27388f7..8ce2f5f7 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java @@ -424,9 +424,7 @@ public class ReceivablePayableDetailServiceImpl } }); ReceivablePayableFeeDetailVO result = buildFeeDetail(rows); - LinkedHashSet feeItemNames = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId())); - feeItemNames.addAll(result.getFeeItemNames()); - result.setFeeItemNames(new ArrayList<>(feeItemNames)); + // 只展示本明细已生成的费用项。合同上其他计费方案的费用项不能叠到当前明细。 return result; } @@ -1968,21 +1966,35 @@ public class ReceivablePayableDetailServiceImpl } private List calculatedFees(Waybill waybill, ContractManage contract, String planId) { - return calculatedFees(waybill, contract, planId, false); + return calculatedFees(waybill, contract, planId, false, false); } - private List calculatedFees(Waybill waybill, ContractManage contract, String planId, boolean matchOnly) { + private List calculatedFees(Waybill waybill, ContractManage contract, String planId, + boolean matchOnly) { + return calculatedFees(waybill, contract, planId, matchOnly, false); + } + + /** + * @param strictPlan 更新费用时为 true:选定方案有规则但未命中时,不再回退运单历史费用,避免旧费用项叠加 + */ + private List calculatedFees(Waybill waybill, ContractManage contract, String planId, + boolean matchOnly, boolean strictPlan) { List> plans = parseList(contract == null ? null : contract.getBillingPlanJson()); Map plan = "__matched__".equals(planId) ? resolveDefaultBillingPlan(plans, waybill.getTransportType()) : resolveBillingPlan(plans, planId); - return calculatedFees(waybill, plan, matchOnly); + return calculatedFees(waybill, plan, matchOnly, strictPlan); } private List calculatedFees(Waybill waybill, Map plan, boolean matchOnly) { + return calculatedFees(waybill, plan, matchOnly, false); + } + + private List calculatedFees(Waybill waybill, Map plan, + boolean matchOnly, boolean strictPlan) { if (plan == null || !(plan.get("rules") instanceof List)) { - return matchOnly ? List.of() : buildCargoFees(waybill); + return matchOnly || strictPlan ? List.of() : buildCargoFees(waybill); } Map, ReceivablePayableCargoFee> feesByCargo = new LinkedHashMap<>(); Map, Map> feeItemsByCargo = new LinkedHashMap<>(); @@ -2024,7 +2036,11 @@ public class ReceivablePayableDetailServiceImpl ReceivablePayableCargoFee fee = result.get(index); fee.setLineNo(String.format("%04d", index + 1)); } - return result.isEmpty() && !matchOnly ? buildCargoFees(waybill) : result; + // 已选定计费方案时,只保留该方案算出的费用项,不用运单上的历史费用回填。 + if (strictPlan || matchOnly) { + return result; + } + return result.isEmpty() ? buildCargoFees(waybill) : result; } private ReceivablePayableCargoFee buildCalculatedCargoFee(Waybill waybill, Waybill feeWaybill, @@ -2934,7 +2950,7 @@ public class ReceivablePayableDetailServiceImpl throw new ServiceException("关联运单不存在"); } ContractManage contract = contractManageService.getById(detail.getContractId()); - List fees = calculatedFees(waybill, contract, billingPlanId); + List fees = calculatedFees(waybill, contract, billingPlanId, false, true); cargoFeeMapper.delete(Wrappers.lambdaQuery().eq(ReceivablePayableCargoFee::getDetailId, detail.getId())); fees.forEach(fee -> { fee.setDetailId(detail.getId()); cargoFeeMapper.insert(fee); }); BigDecimal freight = fees.stream().map(fee -> money(fee.getFreightAmount())) @@ -2959,7 +2975,7 @@ public class ReceivablePayableDetailServiceImpl ContractManage contract = contractManageService.getById(detail.getContractId()); List masterGoods = masterOrderGoods(masterOrder); List fees = masterGoods.stream() - .flatMap(goods -> calculatedFees(goods, contract, billingPlanId).stream()).toList(); + .flatMap(goods -> calculatedFees(goods, contract, billingPlanId, false, true).stream()).toList(); normalizeMasterFeeLines(fees); cargoFeeMapper.delete(Wrappers.lambdaQuery() .eq(ReceivablePayableCargoFee::getDetailId, detail.getId())); @@ -2995,7 +3011,7 @@ public class ReceivablePayableDetailServiceImpl } ContractManage contract = contractManageService.getById(detail.getContractId()); List fees = waybills.stream() - .flatMap(waybill -> calculatedFees(waybill, contract, billingPlanId).stream()).toList(); + .flatMap(waybill -> calculatedFees(waybill, contract, billingPlanId, false, true).stream()).toList(); normalizeWaybillFeeLines(fees); cargoFeeMapper.delete(Wrappers.lambdaQuery() .eq(ReceivablePayableCargoFee::getDetailId, detail.getId())); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java index 5e0fb473..34d9d35f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java @@ -81,6 +81,8 @@ public class TransportPlanServiceImpl extends BaseServiceImpl selectTransportPlanPage(IPage page, TransportPlanVO transportPlan) { + // 历史手工创建未写入 dept_id,allDept=0 按组织过滤时会被排除 + backfillMissingDept(transportPlan); IPage entityPage = page(page, buildQuery(transportPlan)); IPage resultPage = TransportPlanWrapper.build().pageVO(entityPage); fillCustomerNamesFromContracts(resultPage.getRecords()); @@ -151,6 +153,12 @@ public class TransportPlanServiceImpl extends BaseServiceImpllambdaUpdate() + .set(TransportPlan::getDeptId, dept.getId()) + .set(TransportPlan::getDeptName, dept.getDeptName()) + .isNull(TransportPlan::getDeptId) + .eq(TransportPlan::getCreateDept, dept.getId()) + .eq(TransportPlan::getIsDeleted, 0)); + } + private LambdaQueryWrapper buildQuery(TransportPlanVO transportPlan) { TransportBusinessSupport.validateAllDept(transportPlan.getAllDept(), "运输计划"); LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery().eq(TransportPlan::getIsDeleted, 0); if (!Objects.equals(transportPlan.getAllDept(), 1)) { - queryWrapper.eq(TransportPlan::getDeptId, TransportBusinessSupport.currentDeptId("运输计划")); + Long currentDeptId = TransportBusinessSupport.currentDeptId("运输计划"); + // dept_id 为空的历史数据按创建组织归入当前组织 + queryWrapper.and(wrapper -> wrapper.eq(TransportPlan::getDeptId, currentDeptId) + .or(item -> item.isNull(TransportPlan::getDeptId).eq(TransportPlan::getCreateDept, currentDeptId))); } else if (Func.isNotEmpty(transportPlan.getDeptId())) { queryWrapper.eq(TransportPlan::getDeptId, transportPlan.getDeptId()); } From ccb68b08531b51dd5018bfd242c60ce456e96614 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 20 Oct 2026 14:43:51 +0800 Subject: [PATCH 3/4] =?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(); From af396965722ca3f25ee39661639259ec8bf74d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Tue, 20 Oct 2026 18:48:00 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20HCS=20=E5=90=88=E8=A7=84=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=20Mock=20=E7=AB=AF=E7=82=B9,=E4=B8=B4=E6=97=B6?= =?UTF-8?q?=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/HcsMockController.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsMockController.java diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsMockController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsMockController.java new file mode 100644 index 00000000..247f0e87 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsMockController.java @@ -0,0 +1,123 @@ +/** + * 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.controller; + +import cn.hutool.core.util.StrUtil; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.tenant.annotation.TenantIgnore; +import org.springblade.core.tool.api.R; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; +import java.util.Objects; + +/** + * HCS 合规回调 Mock 端点 + *

+ * 合同法务审批结果回调(D103)、付款法务审批结果回调(D104)、项目金蝶同步结果回调(D902)、 + * 合同金蝶同步结果回传接收(D903)四个入站回调的纯 mock 实现:仅校验来源系统标识后返回固定结果, + * 不读写业务数据、不联动状态。真实处理逻辑保留在对应 Support 中,联调时把端点切回 + * {@link } 并调用 Support 方法即可。 + * + * @author Chill + */ +@Slf4j +@RestController +@TenantIgnore +@RequestMapping("hcs") +@Tag(name = "HCS合规回调Mock", description = "HCS合规回调Mock端点(仅自测,返回固定 mock 数据)") +public class HcsMockController { + + /** + * 来源系统标识(合规方约定值,与 third-party.hcs.src-system 默认值一致) + */ + private static final String SOURCE_SYSTEM = "WLTMS"; + + /** + * 合同法务审批结果回调(D103) + */ + @PostMapping("/contractLegal/result") + @ApiOperationSupport(order = 15) + @Operation(summary = "合同法务审批结果回调Mock", description = "D103 合规推送合同特批(金蝶同步)结果,仅校验来源系统并返回 mock 数据") + public R contractLegalResult(@RequestBody(required = false) Map param) { + this.checkSourceSystem("合同法务审批结果", param); + return R.data("mock特批结果已处理:approved"); + } + + /** + * 付款法务审批结果回调(D104) + */ + @PostMapping("/paymentLegal/result") + @ApiOperationSupport(order = 16) + @Operation(summary = "付款法务审批结果回调Mock", description = "D104 合规推送付款特批结果,仅校验来源系统并返回 mock 数据") + public R paymentLegalResult(@RequestBody(required = false) Map param) { + this.checkSourceSystem("付款法务审批结果", param); + return R.data("mock付款特批结果接收已处理:approved"); + } + + /** + * 项目金蝶同步结果回调(D902) + */ + @PostMapping("/projectLegal/result") + @ApiOperationSupport(order = 18) + @Operation(summary = "项目金蝶同步结果回调Mock", description = "D902 合规推送项目金蝶同步结果,仅校验来源系统并返回 mock 数据") + public R projectLegalResult(@RequestBody(required = false) Map param) { + this.checkSourceSystem("项目金蝶同步结果", param); + return R.data("mock项目金蝶同步结果接收已处理:approved"); + } + + /** + * 合同金蝶同步结果回传接收(D903) + */ + @PostMapping("/contractKingdee/result") + @ApiOperationSupport(order = 19) + @Operation(summary = "合同金蝶同步结果回传接收Mock", description = "D903 合规推送合同金蝶最终同步结果,仅校验来源系统并返回 mock 数据") + public R contractKingdeeResult(@RequestBody(required = false) Map param) { + this.checkSourceSystem("合同金蝶同步结果", param); + return R.data("mock合同金蝶同步结果接收处理:approved"); + } + + /** + * 来源系统校验:仅接受合规方约定的来源标识,防伪造回传 + * + * @param bizName 业务名称(日志用) + * @param param 回调报文 + */ + private void checkSourceSystem(String bizName, Map param) { + String sourceSystem = param == null ? null : Objects.toString(param.get("sourceSystem"), null); + if (!StrUtil.equalsIgnoreCase(sourceSystem, SOURCE_SYSTEM)) { + log.warn("{}推送来源系统标识错误:sourceSystem={}", bizName, sourceSystem); + throw new ServiceException("来源系统标识错误"); + } + } +}