From 9f9e027dfc629bb355a7d9ef48948539e6a935aa Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 19 Oct 2026 17:54:20 +0800 Subject: [PATCH] feat(firstleg): prefill editable default fee prices --- .../com/baiyue/firstleg/dto/FeeItemDtos.java | 24 +++-------- .../baiyue/firstleg/entity/FeeItemEntity.java | 6 +++ .../firstleg/service/FeeItemService.java | 2 + .../db/phase2-fee-item-and-inbound-detail.sql | 1 + .../main/resources/openapi/fee-item-v1.yaml | 2 + .../controller/FeeManagementContractTest.java | 4 ++ .../service/FeeManagementServiceTest.java | 42 +++++++++++++++++-- 7 files changed, 58 insertions(+), 23 deletions(-) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/FeeItemDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/FeeItemDtos.java index 019125cae..5f56392b0 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/FeeItemDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/FeeItemDtos.java @@ -4,6 +4,9 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import java.math.BigDecimal; /** 费用项目请求模型。 */ public final class FeeItemDtos { @@ -21,26 +24,9 @@ public final class FeeItemDtos { @NotNull Boolean warehousePayable, @NotNull Boolean warehouseReceivable, @NotNull Boolean allocationFee, - @Size(max = 500) String description + @Size(max = 500) String description, + @DecimalMin("0.00") @Digits(integer = 10, fraction = 2) BigDecimal defaultPrice ) { - public SaveRequest(String category, String tradeScope, String feeCode, - String feeTypeName, Boolean pickupPayable, - Boolean pickupReceivable, Boolean warehousePayable, - Boolean warehouseReceivable, Boolean allocationFee, - String description) { - this(category, tradeScope, feeCode, null, feeTypeName, pickupPayable, - pickupReceivable, warehousePayable, warehouseReceivable, - allocationFee, description); - } - - /** Backward-compatible constructor for callers predating the stable fee code. */ - public SaveRequest(String category, String tradeScope, String feeTypeName, - Boolean pickupPayable, Boolean pickupReceivable, - Boolean warehousePayable, Boolean warehouseReceivable, - Boolean allocationFee, String description) { - this(category, tradeScope, null, null, feeTypeName, pickupPayable, pickupReceivable, - warehousePayable, warehouseReceivable, allocationFee, description); - } } public record StatusRequest( diff --git a/firstleg/src/main/java/com/baiyue/firstleg/entity/FeeItemEntity.java b/firstleg/src/main/java/com/baiyue/firstleg/entity/FeeItemEntity.java index 2cc751bb3..414877558 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/entity/FeeItemEntity.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/entity/FeeItemEntity.java @@ -1,11 +1,14 @@ package com.baiyue.firstleg.entity; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baiyue.common.core.model.BaseEntity; import lombok.Getter; import lombok.Setter; +import java.math.BigDecimal; /** 费用项目主数据。 */ @Getter @@ -20,6 +23,9 @@ public class FeeItemEntity extends BaseEntity { private String feeCode; private String supplierCategory; private String feeTypeName; + /** 业务选费时带入的默认价格,可清空且不限制实际成交金额。 */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal defaultPrice; private Boolean pickupPayable; private Boolean pickupReceivable; private Boolean warehousePayable; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FeeItemService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FeeItemService.java index 6d7258f87..8948e0317 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/FeeItemService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FeeItemService.java @@ -101,6 +101,7 @@ public class FeeItemService { } } + /** 保存项目属性及可清空的默认价格,实际费用仍以业务录入金额为准。 */ private static void copy(FeeItemEntity value, SaveRequest request) { boolean payable = Boolean.TRUE.equals(request.pickupPayable()) || Boolean.TRUE.equals(request.warehousePayable()); @@ -111,6 +112,7 @@ public class FeeItemService { value.setSupplierCategory(payable ? FeeItemSupplierCategory.normalize(request.supplierCategory()) : null); value.setFeeTypeName(request.feeTypeName().trim()); + value.setDefaultPrice(request.defaultPrice()); value.setPickupPayable(request.pickupPayable()); value.setPickupReceivable(request.pickupReceivable()); value.setWarehousePayable(request.warehousePayable()); diff --git a/firstleg/src/main/resources/db/phase2-fee-item-and-inbound-detail.sql b/firstleg/src/main/resources/db/phase2-fee-item-and-inbound-detail.sql index 1338459d4..9b3d5e4ec 100644 --- a/firstleg/src/main/resources/db/phase2-fee-item-and-inbound-detail.sql +++ b/firstleg/src/main/resources/db/phase2-fee-item-and-inbound-detail.sql @@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS fl_fee_item ( fee_code VARCHAR(50) NOT NULL COMMENT '稳定费用编码;平台公共主数据固定编码', supplier_category VARCHAR(16) NULL COMMENT 'PAYABLE供应商类别约束;为空时可选任意ACTIVE供应商', fee_type_name VARCHAR(128) NOT NULL COMMENT '费用类型名称', + default_price DECIMAL(12,2) NULL COMMENT '默认价格CNY;仅选费带入,实际费用可修改', pickup_payable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应付是否参与', pickup_receivable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应收是否参与', warehouse_payable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '仓储应付是否参与', diff --git a/firstleg/src/main/resources/openapi/fee-item-v1.yaml b/firstleg/src/main/resources/openapi/fee-item-v1.yaml index 31ba1ec1d..b8f0e5097 100644 --- a/firstleg/src/main/resources/openapi/fee-item-v1.yaml +++ b/firstleg/src/main/resources/openapi/fee-item-v1.yaml @@ -102,6 +102,7 @@ components: feeCode: {type: string, maxLength: 50, pattern: '^[A-Za-z0-9_-]+$'} supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true, description: PAYABLE供应商类别约束;为空时可选任意ACTIVE Warehouse Supplier} feeTypeName: {type: string, maxLength: 128} + defaultPrice: {type: number, nullable: true, minimum: 0, maximum: 9999999999.99, multipleOf: 0.01, description: 默认价格CNY;为空不自动带价,业务录入时可修改,不追改已有费用} pickupPayable: {type: boolean} pickupReceivable: {type: boolean} warehousePayable: {type: boolean} @@ -118,6 +119,7 @@ components: feeCode: {type: string} supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true, description: PAYABLE供应商类别约束;为空时可选任意ACTIVE Warehouse Supplier} feeTypeName: {type: string} + defaultPrice: {type: number, nullable: true, minimum: 0, maximum: 9999999999.99, multipleOf: 0.01, description: 默认价格CNY;仅用于业务选择费用项目时带入} pickupPayable: {type: boolean} pickupReceivable: {type: boolean} warehousePayable: {type: boolean} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/FeeManagementContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/FeeManagementContractTest.java index 1f89e555e..d1e6b7f6b 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/FeeManagementContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/FeeManagementContractTest.java @@ -96,6 +96,10 @@ class FeeManagementContractTest { Map schemas = stringMap(stringMap(document.get("components")).get("schemas")); assertThat(schemas).containsKeys("FeeItemSaveRequest", "FeeItemResource", "FeeItemPage", "FeeItemApiResponse", "FeeItemPageApiResponse", "VoidApiResponse"); + for (String schema : List.of("FeeItemSaveRequest", "FeeItemResource")) { + assertThat(stringMap(properties(schemas, schema).get("defaultPrice"))) + .containsEntry("nullable", true).containsEntry("minimum", 0); + } for (String response : List.of("FeeItemApiResponse", "FeeItemPageApiResponse", "VoidApiResponse")) { assertThat(list(stringMap(schemas.get(response)).get("required"))).contains("timestamp"); assertThat(properties(schemas, response)).containsKeys( diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java index a0fdc7b00..1cf2d0953 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java @@ -110,6 +110,7 @@ class FeeManagementServiceTest { assertThat(created.getId()).isEqualTo(11L); assertThat(created.getAllocationFee()).isTrue(); + assertThat(created.getDefaultPrice()).isEqualByComparingTo("60.00"); assertThat(created.getWarehouseReceivable()).isTrue(); assertThat(created.getWarehousePayable()).isTrue(); assertThat(created.getSupplierCategory()).isNull(); @@ -122,7 +123,7 @@ class FeeManagementServiceTest { @DisplayName("自定义费用编码统一转为大写并检查客户内唯一性") void normalizesCustomFeeCode() { FeeItemDtos.SaveRequest request = new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL", - "storage_fee", "仓库租赁费", false, false, true, true, true, "应付仓库租赁费"); + "storage_fee", null, "仓库租赁费", false, false, true, true, true, "应付仓库租赁费", null); FeeItemEntity created = feeItems.create(9L, "admin", request); @@ -134,7 +135,7 @@ class FeeManagementServiceTest { @DisplayName("并发创建相同费用编码返回业务冲突") void rejectsConcurrentDuplicateFeeCode() { FeeItemDtos.SaveRequest request = new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL", - "STORAGE_FEE", "仓库租赁费", false, false, true, true, true, null); + "STORAGE_FEE", null, "仓库租赁费", false, false, true, true, true, null, null); doThrow(new DuplicateKeyException("duplicate fee code")).when(repository).insertItem(any()); assertThatThrownBy(() -> feeItems.create(9L, "admin", request)) @@ -156,6 +157,38 @@ class FeeManagementServiceTest { verify(repository, never()).itemCodeExists("STORAGE_FEE", 11L); } + /** 清空默认价只修改公共项目,不触碰已生成的费用。 */ + @Test + void clearsDefaultPrice() { + FeeItemEntity existing = item(true, true); + existing.setDefaultPrice(new BigDecimal("60.00")); + when(repository.findItem(11L)).thenReturn(existing); + when(repository.updateItem(any())).thenReturn(1); + FeeItemDtos.SaveRequest request = new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL", + null, null, "仓库租赁费", false, false, true, true, true, null, null); + + assertThat(feeItems.update(11L, 9L, "admin", request).getDefaultPrice()).isNull(); + verify(repository).updateItem(existing); + } + + /** 请求价格允许空值和零元,拒绝负数、过多小数和超出字段范围的金额。 */ + @Test + void validatesDefaultPrice() { + try (var factory = jakarta.validation.Validation.buildDefaultValidatorFactory()) { + var validator = factory.getValidator(); + for (String price : List.of("0", "60.25", "9999999999.99")) { + var request = new FeeItemDtos.SaveRequest("仓储成本", "DOMESTIC", null, null, + "操作费", false, true, false, false, false, null, new BigDecimal(price)); + assertThat(validator.validate(request)).isEmpty(); + } + for (String price : List.of("-1", "60.255", "10000000000")) { + var request = new FeeItemDtos.SaveRequest("仓储成本", "DOMESTIC", null, null, + "操作费", false, true, false, false, false, null, new BigDecimal(price)); + assertThat(validator.validate(request)).isNotEmpty(); + } + } + } + @Test @DisplayName("任一客户引用公共费用项目后都禁止删除") void rejectsDeletingItemBoundByAnyCustomer() { @@ -478,9 +511,10 @@ class FeeManagementServiceTest { eq(10001L), eq(21L), eq(31L), eq(1), any(LocalDateTime.class)); } + /** 构造带默认价格的公共费用项目。 */ private static FeeItemDtos.SaveRequest itemRequest() { - return new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL", "仓库租赁费", - false, false, true, true, true, "应付仓库租赁费"); + return new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL", null, null, "仓库租赁费", + false, false, true, true, true, "应付仓库租赁费", new BigDecimal("60.00")); } private static InboundFeeDetailDtos.SaveRequest detailRequest(String direction) {