新增海外仓阶梯费用
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
| `iam/` | 租户、商户成员、平台统一用户管理、企业资料/资质、RBAC 与授权快照 | `PlatformUserManagementController.java`、`PlatformTenantController.java`、`ProfileResourceController.java`、`RbacController.java` |
|
||||
| `order/` | 订单主数据、租户运单数、持久化幂等、状态编排、快照、货物明细内部查询与 Outbox | `PlatformFirstLegOrderController.java`、`OrderApplicationService.java`、`InternalOrderCargoController.java`、`README-ORDER.md` |
|
||||
| `firstleg/` | 履约任务与分配;二期入库单、测量/OCR 结果、审核、FBA 计费和自动完成入库 | 履约:`PlatformFulfillmentAssignmentController.java`、`FulfillmentTaskService.java`;入库:`controller/PlatformInbound*.java`、`service/Inbound*.java`、`service/Fba*.java` |
|
||||
| `warehouse/` | 仓库、地址、服务能力、往来单位、快递公司;二期托盘、库位、预占和占用 | 主数据:`controller/`、`PlatformPartnerQueryService.java`;二期:`PlatformPalletController.java`、`PlatformLocationController.java`、`PalletLocationAllocationService.java` |
|
||||
| `warehouse/` | 仓库、地址、服务能力、往来单位、快递公司、海外仓阶梯费用;二期托盘、库位、预占和占用 | 主数据:`controller/`、`PlatformPartnerQueryService.java`、`PlatformOverseasStorageFeeController.java`;二期:`PlatformPalletController.java`、`PlatformLocationController.java`、`PalletLocationAllocationService.java` |
|
||||
| `file/` | 企业资质文件、公共导出/下载中心;入库测量图片与重量 OCR 协作 | 资质:`QualificationFileController.java`;导出:`PlatformExportTaskController.java`、`ExportStateService.java`;测量图片:`PlatformMeasurementImageController.java`、`InternalMeasurementImageController.java`、`MeasurementImageService.java` |
|
||||
| `print/` | 无数据库的无状态 PDF 渲染;固定入仓标签模板、二维码、字体嵌入和资源保护 | `InternalPdfController.java`、`InboundMarkPdfService.java`、`InboundMarkV1Renderer.java`、`src/main/resources/openapi/print-internal-v1.yaml` |
|
||||
| `audit-service/` | 审计事件接收与持久化 | `AuditIngestController.java`、`AuditIngestService.java`;当前接收接口未实现内部令牌保护 |
|
||||
@@ -39,6 +39,7 @@
|
||||
| 平台代客户上传或替换资质 | `file/.../controller/PlatformQualificationFileController.java` → `client/IamQualificationClient.java` → `iam/.../controller/InternalTenantController.java` 的 `/platform-qualifications` |
|
||||
| 往来单位多类型 | `warehouse/.../service/PlatformPartnerQueryService.java`、`entity/PartnerTypeEntity.java`、`src/main/resources/db/warehouse-partner.sql`,消费者看 `order/.../client/PartnerClient.java` |
|
||||
| 往来单位/快递公司批量导入 | `warehouse/.../service/PartnerImportService.java`、`ExpressCompanyImportService.java`、两个平台 Controller 的 `/import-template` 与 `/import` |
|
||||
| 海外仓阶梯费用 | `warehouse/.../controller/PlatformOverseasStorageFeeController.java` → `service/OverseasStorageFeeService.java` → `repository/OverseasStorageFeeRepository.java`;表为 `wh_overseas_storage_fee_rule`、`wh_overseas_storage_fee_tier` |
|
||||
| 入仓标签 PDF 生成 | 调用方先读 `print/README.md`;实现为 `print/.../controller/InternalPdfController.java` → `service/InboundMarkPdfService.java` → `renderer/InboundMarkV1Renderer.java`;机器契约见 `print/src/main/resources/openapi/print-internal-v1.yaml`,仅供携带内部令牌的服务调用,不加入 Gateway |
|
||||
| HTTP 200、body code、成功码 `"200"` | `docs/统一响应约定改造方案-HTTP200.md`、`common-core/ResultCode.java`、`common-web/GlobalExceptionHandler.java` |
|
||||
| 单服务 Jenkins 构建或部署 | 总控 `Jenkinsfile`、子 Job `Jenkinsfile.service`、模块/JAR 映射 `deploy/service-map.yml` |
|
||||
@@ -56,7 +57,7 @@
|
||||
| 托盘、库位与预占/占用 | 平台 CRUD:`warehouse/.../controller/PlatformPalletController.java`、`PlatformLocationController.java`;内部事务接口:`InternalPalletLocationController.java` | `service/PalletApplicationService.java`、`LocationApplicationService.java`、`PalletLocationAllocationService.java`;表与迁移在 `warehouse/src/main/resources/db/phase2-*.sql` |
|
||||
| 称重图片、OCR 重试与第三方测量证据 | 平台入口 `file/.../controller/PlatformMeasurementImageController.java`;Firstleg 内部入口 `InternalMeasurementImageController.java` | `service/MeasurementImageService.java`、`MeasurementImageStorage.java`、`client/WeightOcrClient.java`;重量识别服务入口 `integration-service/.../controller/WeightOcrController.java` |
|
||||
| 入库状态、幂等、历史与 Outbox | `firstleg/.../domain/InboundProcessStatus.java`、`service/InboundIdempotencyService.java`、`repository/InboundWorkflowRepository.java` | 表结构与增量在 `firstleg/src/main/resources/db/phase2-*.sql`;SQL 契约测试 `repository/InboundWorkflowSqlContractTest.java` |
|
||||
| 二期数据库部署 | 测试环境/存量环境统一入口 `deploy/sql/2026-08-04-二期入库新增表字段-MySQL8一键执行.sql` | 覆盖 `baiyue_file`、`baiyue_firstleg`、`baiyue_warehouse`;前置、可重复执行边界和末尾 `PASS` 核验见脚本头及 `deploy/sql/README.md`,不得在未核对目标库和备份的情况下执行 |
|
||||
| 数据库部署 | 空库/存量环境唯一入口 `deploy/sql/头程数据库统一增量-MySQL8.sql` | 覆盖全部一期、二期及后续增量;可重复执行边界和末尾 `PASS` 核验见脚本头及 `deploy/sql/README.md`,历史日期脚本不得再单独执行 |
|
||||
|
||||
- 上表中的 `模块/.../` 从对应模块的 `src/main/java/com/baiyue/<domain>/` 起算;二期链路跨 `order`、`firstleg`、`warehouse`、`file`、`integration-service`,修改 DTO、内部令牌、状态或表结构时必须逐个检查调用方与被调用方。
|
||||
- 二期入库主状态归 `firstleg`,订单货物主数据仍归 `order`,托盘/库位资源归 `warehouse`,文件对象归 `file`,OCR 供应商适配归 `integration-service`;禁止通过跨库查询绕过内部 API。
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.RedisTokenKeys;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.SetOperations;
|
||||
@@ -28,7 +29,7 @@ import java.util.List;
|
||||
*/
|
||||
@Component
|
||||
public class TokenSessionStore {
|
||||
@Resource
|
||||
@Autowired
|
||||
private StringRedisTemplate redisTemplate;
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@@ -78,3 +78,10 @@ SELECT COUNT(*) = 8 AS scheduler_columns_ready
|
||||
- 当前固定入口 `头程数据库统一增量-MySQL8.sql` 已内联该增量以及订单船司字段、二期 28 项 IAM 权限和最新仓储资源租户字段。
|
||||
- 新增 `external_request_hash`、`inbound_detail_id`、`external_detail_no` 和第三方明细唯一索引,用于一条回传绑定一条订单货物明细及一张文件中心图片。
|
||||
- 部署 Firstleg 新版本前必须完整执行固定入口,并确认末尾总体验证为 `PASS`。
|
||||
|
||||
## 海外仓阶梯费用增量
|
||||
|
||||
- `2026-08-05` 增量已追加到固定入口末尾,不创建新的日期部署脚本。
|
||||
- Warehouse 新增 `wh_overseas_storage_fee_rule` 和 `wh_overseas_storage_fee_tier`,包含客户唯一索引、分页索引、阶梯起始天唯一索引和规则明细查询索引。
|
||||
- IAM 新增 `platform:overseas-storage-fee:list/create/edit/delete` 四项权限,默认授予平台系统管理员。
|
||||
- Warehouse Service 启动前必须先执行固定入口;末尾只有两张表、五个索引和四项权限均存在才返回 `PASS`。
|
||||
|
||||
@@ -3342,5 +3342,123 @@ DELIMITER ;
|
||||
CALL unified_assert_permission_count();
|
||||
DROP PROCEDURE IF EXISTS unified_assert_permission_count;
|
||||
|
||||
-- ============================================================================
|
||||
-- 2026-08-05:海外仓阶梯费用基础资料 CRUD 增量
|
||||
-- ============================================================================
|
||||
USE baiyue_warehouse;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wh_overseas_storage_fee_rule (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '海外仓阶梯费用规则ID(雪花ID)',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户账号ID,沿用tenant_id物理字段',
|
||||
customer_name VARCHAR(128) NOT NULL COMMENT '客户公司名称快照',
|
||||
currency CHAR(3) NOT NULL COMMENT 'ISO 4217币种三字码',
|
||||
created_by BIGINT UNSIGNED NULL COMMENT '创建人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_by BIGINT UNSIGNED NULL COMMENT '最后修改人ID',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
|
||||
ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
active_customer_id BIGINT UNSIGNED
|
||||
GENERATED ALWAYS AS (CASE WHEN deleted = 0 THEN tenant_id ELSE NULL END) STORED
|
||||
COMMENT '未删除规则的客户唯一键',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_overseas_fee_active_customer (active_customer_id),
|
||||
KEY idx_overseas_fee_page (deleted, updated_at, id),
|
||||
KEY idx_overseas_fee_customer_name (customer_name, deleted)
|
||||
) ENGINE=InnoDB COMMENT='客户维度海外仓阶梯仓储费规则';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wh_overseas_storage_fee_tier (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '海外仓阶梯费用明细ID(雪花ID)',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户账号ID,沿用tenant_id物理字段',
|
||||
rule_id BIGINT UNSIGNED NOT NULL COMMENT '阶梯费用规则ID',
|
||||
start_day SMALLINT UNSIGNED NOT NULL COMMENT '起始天,含边界',
|
||||
end_day SMALLINT UNSIGNED NOT NULL COMMENT '结束天,含边界',
|
||||
unit_price DECIMAL(18,6) NOT NULL COMMENT '仓储费标准',
|
||||
sort_no SMALLINT UNSIGNED NOT NULL COMMENT '阶梯排序号',
|
||||
created_by BIGINT UNSIGNED NULL COMMENT '创建人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_by BIGINT UNSIGNED NULL COMMENT '最后修改人ID',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
|
||||
ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
active_start_day SMALLINT UNSIGNED
|
||||
GENERATED ALWAYS AS (CASE WHEN deleted = 0 THEN start_day ELSE NULL END) STORED
|
||||
COMMENT '未删除阶梯的起始天唯一键',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_overseas_fee_tier_start (rule_id, active_start_day),
|
||||
KEY idx_overseas_fee_tier_rule (tenant_id, rule_id, deleted, sort_no, id),
|
||||
CONSTRAINT chk_overseas_fee_tier_days CHECK (start_day >= 1 AND end_day >= start_day),
|
||||
CONSTRAINT chk_overseas_fee_tier_price CHECK (unit_price > 0)
|
||||
) ENGINE=InnoDB COMMENT='海外仓阶梯仓储费明细';
|
||||
|
||||
USE baiyue_iam;
|
||||
INSERT INTO iam_permission (
|
||||
id, tenant_id, app_code, permission_type, parent_id, permission_code,
|
||||
permission_name, route_path, http_method, sort_no, status, built_in, deleted
|
||||
)
|
||||
VALUES
|
||||
(5100030, 0, 'PLATFORM', 'MENU', 5100000,
|
||||
'platform:overseas-storage-fee:list', '海外仓阶梯费用',
|
||||
'/customers/overseas-storage-fees', 'GET', 20, 'ACTIVE', 1, 0),
|
||||
(5100031, 0, 'PLATFORM', 'BUTTON', 5100030,
|
||||
'platform:overseas-storage-fee:create', '新增海外仓阶梯费用',
|
||||
NULL, NULL, 10, 'ACTIVE', 1, 0),
|
||||
(5100032, 0, 'PLATFORM', 'BUTTON', 5100030,
|
||||
'platform:overseas-storage-fee:edit', '编辑海外仓阶梯费用',
|
||||
NULL, NULL, 20, 'ACTIVE', 1, 0),
|
||||
(5100033, 0, 'PLATFORM', 'BUTTON', 5100030,
|
||||
'platform:overseas-storage-fee:delete', '删除海外仓阶梯费用',
|
||||
NULL, NULL, 30, 'ACTIVE', 1, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_type = VALUES(permission_type), parent_id = VALUES(parent_id),
|
||||
permission_name = VALUES(permission_name), route_path = VALUES(route_path),
|
||||
http_method = VALUES(http_method), sort_no = VALUES(sort_no),
|
||||
status = 'ACTIVE', built_in = 1, deleted = 0;
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000001, id, 0
|
||||
FROM iam_permission
|
||||
WHERE tenant_id = 0 AND app_code = 'PLATFORM'
|
||||
AND permission_code IN (
|
||||
'platform:overseas-storage-fee:list',
|
||||
'platform:overseas-storage-fee:create',
|
||||
'platform:overseas-storage-fee:edit',
|
||||
'platform:overseas-storage-fee:delete'
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
DROP PROCEDURE IF EXISTS unified_assert_overseas_storage_fee;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE unified_assert_overseas_storage_fee()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM information_schema.tables
|
||||
WHERE table_schema = 'baiyue_warehouse'
|
||||
AND table_name IN ('wh_overseas_storage_fee_rule', 'wh_overseas_storage_fee_tier')) <> 2 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '统一迁移失败:海外仓阶梯费用表未完整创建';
|
||||
END IF;
|
||||
IF (SELECT COUNT(DISTINCT index_name) FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_warehouse'
|
||||
AND index_name IN ('uk_overseas_fee_active_customer', 'idx_overseas_fee_page',
|
||||
'idx_overseas_fee_customer_name', 'uk_overseas_fee_tier_start',
|
||||
'idx_overseas_fee_tier_rule')) <> 5 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '统一迁移失败:海外仓阶梯费用索引未完整创建';
|
||||
END IF;
|
||||
IF (SELECT COUNT(DISTINCT permission_code) FROM baiyue_iam.iam_permission
|
||||
WHERE tenant_id = 0 AND status = 'ACTIVE' AND deleted = 0
|
||||
AND permission_code IN (
|
||||
'platform:overseas-storage-fee:list', 'platform:overseas-storage-fee:create',
|
||||
'platform:overseas-storage-fee:edit', 'platform:overseas-storage-fee:delete')) <> 4 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '统一迁移失败:海外仓阶梯费用权限未完整创建';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL unified_assert_overseas_storage_fee();
|
||||
DROP PROCEDURE IF EXISTS unified_assert_overseas_storage_fee;
|
||||
|
||||
SELECT 'PASS' AS migration_status,
|
||||
'一期与二期统一增量执行完成,关键结构与二期权限校验通过' AS message;
|
||||
'统一增量执行完成,海外仓阶梯费用表、索引和权限校验通过' AS message;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.baiyue.firstleg.security;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -11,7 +10,7 @@ import java.time.Duration;
|
||||
@Component
|
||||
public class RedisNonceReplayGuard implements NonceReplayGuard {
|
||||
private static final String KEY_PREFIX = "baiyue:firstleg:measurement-openapi:nonce:";
|
||||
@Resource
|
||||
@Autowired
|
||||
private StringRedisTemplate redisTemplate;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -57,7 +57,7 @@ spring:
|
||||
- id: warehouse-service
|
||||
uri: lb://warehouse-service
|
||||
predicates:
|
||||
- Path=/api/merchant/v1/warehouses/**,/api/merchant/v1/partners/**,/api/platform/v1/warehouses/**,/api/platform/v1/warehouse-admin/**,/api/platform/v1/partners/**,/api/platform/v1/express-companies/**,/api/platform/v1/pallets/**,/api/platform/v1/locations/**
|
||||
- Path=/api/merchant/v1/warehouses/**,/api/merchant/v1/partners/**,/api/platform/v1/warehouses/**,/api/platform/v1/warehouse-admin/**,/api/platform/v1/partners/**,/api/platform/v1/express-companies/**,/api/platform/v1/pallets/**,/api/platform/v1/locations/**,/api/platform/v1/overseas-storage-fees/**
|
||||
- id: file-service
|
||||
uri: lb://file-service
|
||||
predicates:
|
||||
|
||||
+2
-1
@@ -48,7 +48,8 @@ class GatewayFileRouteConfigurationTest {
|
||||
String paths = String.join(",", predicate.getArgs().values());
|
||||
assertThat(paths)
|
||||
.contains("/api/platform/v1/pallets/**")
|
||||
.contains("/api/platform/v1/locations/**");
|
||||
.contains("/api/platform/v1/locations/**")
|
||||
.contains("/api/platform/v1/overseas-storage-fees/**");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
| 平台快递公司增改查、启停与 XLSX 导入 | `controller/PlatformExpressCompanyController.java`、`service/ExpressCompanyQueryService.java`、`ExpressCompanyImportService.java`;模板 `/import-template`,导入 `/import` |
|
||||
| 平台托盘增改查 | `controller/PlatformPalletController.java` → `service/PalletApplicationService.java` → `repository/PalletRepository.java`;体积分托盘规则在 `service/PalletVolumeSplitter.java` |
|
||||
| 平台库位增改查、可用库位与容量 | `controller/PlatformLocationController.java` → `service/LocationApplicationService.java`、`LocationResourceService.java` → `repository/LocationRepository.java` |
|
||||
| 海外仓阶梯费用 CRUD | `controller/PlatformOverseasStorageFeeController.java` → `service/OverseasStorageFeeService.java` → `repository/OverseasStorageFeeRepository.java`;客户主记录与阶梯明细分别落 `wh_overseas_storage_fee_rule`、`wh_overseas_storage_fee_tier` |
|
||||
| Firstleg 托盘库位预占、确认和释放 | `controller/InternalPalletLocationController.java`(`/reservations`、`/confirm`、`/release`)→ `service/PalletLocationAllocationService.java`;只允许内部令牌调用 |
|
||||
| 服务与数据库结构状态 | `controller/WarehouseHealthController.java`、`service/WarehouseSchemaReadinessService.java` |
|
||||
| 数据访问异常映射 | `controller/WarehouseDataAccessExceptionHandler.java`;数据库不可用或结构漂移统一返回 `S0503/503` |
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package com.baiyue.warehouse.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.warehouse.dto.OverseasStorageFeeDtos;
|
||||
import com.baiyue.warehouse.service.OverseasStorageFeeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 平台海外仓阶梯费用管理接口。 */
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/overseas-storage-fees")
|
||||
@Tag(name = "海外仓阶梯费用", description = "按客户维护海外仓仓储费阶梯")
|
||||
public class PlatformOverseasStorageFeeController {
|
||||
@Resource
|
||||
private OverseasStorageFeeService service;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:overseas-storage-fee:list')")
|
||||
@Operation(summary = "分页查询海外仓阶梯费用")
|
||||
public ApiResponse<PageResult<OverseasStorageFeeDtos.View>> page(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @ParameterObject OverseasStorageFeeDtos.PageQuery query) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.page(query));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-storage-fee:list')")
|
||||
@Operation(summary = "查询海外仓阶梯费用详情")
|
||||
public ApiResponse<OverseasStorageFeeDtos.View> detail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Min(1) long id) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.detail(id));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('platform:overseas-storage-fee:create')")
|
||||
@Operation(summary = "新增海外仓阶梯费用")
|
||||
public ApiResponse<Long> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody OverseasStorageFeeDtos.CreateRequest request) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.create(request));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-storage-fee:edit')")
|
||||
@Operation(summary = "编辑海外仓阶梯费用")
|
||||
public ApiResponse<Void> update(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Min(1) long id,
|
||||
@Valid @RequestBody OverseasStorageFeeDtos.UpdateRequest request) {
|
||||
requirePlatform(user);
|
||||
service.update(id, request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-storage-fee:delete')")
|
||||
@Operation(summary = "删除海外仓阶梯费用")
|
||||
public ApiResponse<Void> delete(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Min(1) long id,
|
||||
@RequestParam @Min(0) int version) {
|
||||
requirePlatform(user);
|
||||
service.delete(id, version);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
private static void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || !"PLATFORM".equals(user.appCode())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.baiyue.warehouse.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Digits;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** 海外仓阶梯费用接口契约。 */
|
||||
public final class OverseasStorageFeeDtos {
|
||||
private OverseasStorageFeeDtos() {
|
||||
}
|
||||
|
||||
@Schema(name = "OverseasStorageFeePageQuery", description = "海外仓阶梯费用分页条件")
|
||||
public record PageQuery(
|
||||
@Min(1) long page,
|
||||
@Min(1) @Max(200) long size,
|
||||
@Schema(description = "客户ID,不传时查询全部客户") @Positive Long customerId,
|
||||
@Schema(description = "客户公司名称关键字") @Size(max = 128) String customerName
|
||||
) {
|
||||
public PageQuery {
|
||||
if (page == 0) {
|
||||
page = 1;
|
||||
}
|
||||
if (size == 0) {
|
||||
size = 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "OverseasStorageFeeTierRequest", description = "仓储费阶梯")
|
||||
public record TierRequest(
|
||||
@NotNull @Min(1) @Max(36500) Integer startDay,
|
||||
@NotNull @Min(1) @Max(36500) Integer endDay,
|
||||
@NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 12, fraction = 6)
|
||||
BigDecimal unitPrice
|
||||
) {
|
||||
}
|
||||
|
||||
@Schema(name = "OverseasStorageFeeCreateRequest", description = "新增海外仓阶梯费用")
|
||||
public record CreateRequest(
|
||||
@NotNull @Positive Long customerId,
|
||||
@NotBlank @Size(max = 128) String customerName,
|
||||
@NotBlank @Pattern(regexp = "^[A-Z]{3}$") String currency,
|
||||
@NotEmpty @Size(max = 100) List<@Valid TierRequest> tiers
|
||||
) {
|
||||
}
|
||||
|
||||
@Schema(name = "OverseasStorageFeeUpdateRequest", description = "编辑海外仓阶梯费用")
|
||||
public record UpdateRequest(
|
||||
@NotNull @Positive Long customerId,
|
||||
@NotBlank @Size(max = 128) String customerName,
|
||||
@NotBlank @Pattern(regexp = "^[A-Z]{3}$") String currency,
|
||||
@NotEmpty @Size(max = 100) List<@Valid TierRequest> tiers,
|
||||
@NotNull @Min(0) Integer version
|
||||
) {
|
||||
}
|
||||
|
||||
@Schema(name = "OverseasStorageFeeTierView", description = "仓储费阶梯明细")
|
||||
public record TierView(Long id, Integer startDay, Integer endDay, BigDecimal unitPrice, Integer sortNo) {
|
||||
}
|
||||
|
||||
@Schema(name = "OverseasStorageFeeView", description = "海外仓阶梯费用详情")
|
||||
public record View(
|
||||
Long id,
|
||||
Long customerId,
|
||||
String customerName,
|
||||
String currency,
|
||||
List<TierView> tiers,
|
||||
Integer version,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.warehouse.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
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;
|
||||
|
||||
/** 客户维度的海外仓阶梯仓储费主记录。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("wh_overseas_storage_fee_rule")
|
||||
public class OverseasStorageFeeRuleEntity extends BaseEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private String customerName;
|
||||
private String currency;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.warehouse.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
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
|
||||
@Setter
|
||||
@TableName("wh_overseas_storage_fee_tier")
|
||||
public class OverseasStorageFeeTierEntity extends BaseEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long ruleId;
|
||||
private Integer startDay;
|
||||
private Integer endDay;
|
||||
private BigDecimal unitPrice;
|
||||
private Integer sortNo;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.baiyue.warehouse.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.warehouse.entity.OverseasStorageFeeRuleEntity;
|
||||
|
||||
/** 海外仓阶梯费用主记录 Mapper。 */
|
||||
public interface OverseasStorageFeeRuleMapper extends BaseMapper<OverseasStorageFeeRuleEntity> {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.baiyue.warehouse.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.warehouse.entity.OverseasStorageFeeTierEntity;
|
||||
|
||||
/** 海外仓阶梯费用明细 Mapper。 */
|
||||
public interface OverseasStorageFeeTierMapper extends BaseMapper<OverseasStorageFeeTierEntity> {
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.baiyue.warehouse.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.warehouse.dto.OverseasStorageFeeDtos;
|
||||
import com.baiyue.warehouse.entity.OverseasStorageFeeRuleEntity;
|
||||
import com.baiyue.warehouse.entity.OverseasStorageFeeTierEntity;
|
||||
import com.baiyue.warehouse.mapper.OverseasStorageFeeRuleMapper;
|
||||
import com.baiyue.warehouse.mapper.OverseasStorageFeeTierMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/** 海外仓阶梯费用持久化仓储。 */
|
||||
@Repository
|
||||
public class OverseasStorageFeeRepository {
|
||||
@Resource
|
||||
private OverseasStorageFeeRuleMapper ruleMapper;
|
||||
@Resource
|
||||
private OverseasStorageFeeTierMapper tierMapper;
|
||||
|
||||
public PageResult<OverseasStorageFeeRuleEntity> page(OverseasStorageFeeDtos.PageQuery query) {
|
||||
LambdaQueryWrapper<OverseasStorageFeeRuleEntity> wrapper =
|
||||
new LambdaQueryWrapper<OverseasStorageFeeRuleEntity>()
|
||||
.eq(query.customerId() != null, OverseasStorageFeeRuleEntity::getTenantId, query.customerId())
|
||||
.like(StringUtils.hasText(query.customerName()),
|
||||
OverseasStorageFeeRuleEntity::getCustomerName, query.customerName())
|
||||
.orderByDesc(OverseasStorageFeeRuleEntity::getUpdatedAt)
|
||||
.orderByDesc(OverseasStorageFeeRuleEntity::getId);
|
||||
return PageResult.from(ruleMapper.selectPage(new Page<>(query.page(), query.size()), wrapper));
|
||||
}
|
||||
|
||||
public OverseasStorageFeeRuleEntity find(long id) {
|
||||
return ruleMapper.selectById(id);
|
||||
}
|
||||
|
||||
public boolean existsCustomer(long customerId, Long excludedId) {
|
||||
return ruleMapper.selectCount(new LambdaQueryWrapper<OverseasStorageFeeRuleEntity>()
|
||||
.eq(OverseasStorageFeeRuleEntity::getTenantId, customerId)
|
||||
.ne(excludedId != null, OverseasStorageFeeRuleEntity::getId, excludedId)) > 0;
|
||||
}
|
||||
|
||||
public List<OverseasStorageFeeTierEntity> findTiers(Collection<Long> ruleIds) {
|
||||
if (ruleIds == null || ruleIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return tierMapper.selectList(new LambdaQueryWrapper<OverseasStorageFeeTierEntity>()
|
||||
.in(OverseasStorageFeeTierEntity::getRuleId, ruleIds)
|
||||
.orderByAsc(OverseasStorageFeeTierEntity::getRuleId)
|
||||
.orderByAsc(OverseasStorageFeeTierEntity::getSortNo)
|
||||
.orderByAsc(OverseasStorageFeeTierEntity::getId));
|
||||
}
|
||||
|
||||
public void insertRule(OverseasStorageFeeRuleEntity entity) {
|
||||
ruleMapper.insert(entity);
|
||||
}
|
||||
|
||||
public int updateRule(OverseasStorageFeeRuleEntity entity) {
|
||||
return ruleMapper.updateById(entity);
|
||||
}
|
||||
|
||||
public void insertTiers(List<OverseasStorageFeeTierEntity> tiers) {
|
||||
tiers.forEach(tierMapper::insert);
|
||||
}
|
||||
|
||||
public void deleteTiers(long ruleId) {
|
||||
tierMapper.delete(new LambdaQueryWrapper<OverseasStorageFeeTierEntity>()
|
||||
.eq(OverseasStorageFeeTierEntity::getRuleId, ruleId));
|
||||
}
|
||||
|
||||
public int deleteRule(long id, int version) {
|
||||
return ruleMapper.update(null, new LambdaUpdateWrapper<OverseasStorageFeeRuleEntity>()
|
||||
.eq(OverseasStorageFeeRuleEntity::getId, id)
|
||||
.eq(OverseasStorageFeeRuleEntity::getVersion, version)
|
||||
.set(OverseasStorageFeeRuleEntity::getDeleted, true)
|
||||
.set(OverseasStorageFeeRuleEntity::getVersion, version + 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.warehouse.dto.OverseasStorageFeeDtos;
|
||||
import com.baiyue.warehouse.entity.OverseasStorageFeeRuleEntity;
|
||||
import com.baiyue.warehouse.entity.OverseasStorageFeeTierEntity;
|
||||
import com.baiyue.warehouse.repository.OverseasStorageFeeRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 海外仓阶梯费用管理服务。 */
|
||||
@Service
|
||||
public class OverseasStorageFeeService {
|
||||
@Resource
|
||||
private OverseasStorageFeeRepository repository;
|
||||
|
||||
public PageResult<OverseasStorageFeeDtos.View> page(OverseasStorageFeeDtos.PageQuery query) {
|
||||
PageResult<OverseasStorageFeeRuleEntity> source = repository.page(query);
|
||||
Map<Long, List<OverseasStorageFeeTierEntity>> tiers = repository.findTiers(
|
||||
source.records().stream().map(OverseasStorageFeeRuleEntity::getId).toList())
|
||||
.stream().collect(Collectors.groupingBy(OverseasStorageFeeTierEntity::getRuleId));
|
||||
return new PageResult<>(source.records().stream()
|
||||
.map(rule -> view(rule, tiers.getOrDefault(rule.getId(), List.of())))
|
||||
.toList(), source.page(), source.size(), source.total(), source.pages());
|
||||
}
|
||||
|
||||
public OverseasStorageFeeDtos.View detail(long id) {
|
||||
OverseasStorageFeeRuleEntity rule = required(id);
|
||||
return view(rule, repository.findTiers(List.of(id)));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "OVERSEAS_STORAGE_FEE_CREATE", resourceType = "OVERSEAS_STORAGE_FEE",
|
||||
description = "新增海外仓阶梯费用", captureBefore = false)
|
||||
public Long create(OverseasStorageFeeDtos.CreateRequest request) {
|
||||
ensureCustomerUnique(request.customerId(), null);
|
||||
List<OverseasStorageFeeDtos.TierRequest> tiers = normalize(request.tiers());
|
||||
OverseasStorageFeeRuleEntity rule = new OverseasStorageFeeRuleEntity();
|
||||
copy(rule, request.customerId(), request.customerName(), request.currency());
|
||||
try {
|
||||
repository.insertRule(rule);
|
||||
repository.insertTiers(tierEntities(rule.getId(), request.customerId(), tiers));
|
||||
} catch (DataIntegrityViolationException exception) {
|
||||
throw new BizException(ResultCode.CONFLICT, "该客户已配置海外仓阶梯费用或阶梯区间重复");
|
||||
}
|
||||
return rule.getId();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "OVERSEAS_STORAGE_FEE_UPDATE", resourceType = "OVERSEAS_STORAGE_FEE",
|
||||
resourceId = "#id", description = "编辑海外仓阶梯费用")
|
||||
public void update(long id, OverseasStorageFeeDtos.UpdateRequest request) {
|
||||
OverseasStorageFeeRuleEntity rule = required(id);
|
||||
if (!rule.getVersion().equals(request.version())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "费用配置已被修改,请刷新后重试");
|
||||
}
|
||||
ensureCustomerUnique(request.customerId(), id);
|
||||
List<OverseasStorageFeeDtos.TierRequest> tiers = normalize(request.tiers());
|
||||
copy(rule, request.customerId(), request.customerName(), request.currency());
|
||||
try {
|
||||
if (repository.updateRule(rule) != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "费用配置已被并发修改,请刷新后重试");
|
||||
}
|
||||
repository.deleteTiers(id);
|
||||
repository.insertTiers(tierEntities(id, request.customerId(), tiers));
|
||||
} catch (DataIntegrityViolationException exception) {
|
||||
throw new BizException(ResultCode.CONFLICT, "该客户已配置海外仓阶梯费用或阶梯区间重复");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "OVERSEAS_STORAGE_FEE_DELETE", resourceType = "OVERSEAS_STORAGE_FEE",
|
||||
resourceId = "#id", description = "删除海外仓阶梯费用")
|
||||
public void delete(long id, int version) {
|
||||
required(id);
|
||||
repository.deleteTiers(id);
|
||||
if (repository.deleteRule(id, version) != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "费用配置已被并发修改,请刷新后重试");
|
||||
}
|
||||
}
|
||||
|
||||
private OverseasStorageFeeRuleEntity required(long id) {
|
||||
OverseasStorageFeeRuleEntity rule = repository.find(id);
|
||||
if (rule == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "海外仓阶梯费用不存在");
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
private void ensureCustomerUnique(long customerId, Long excludedId) {
|
||||
if (repository.existsCustomer(customerId, excludedId)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "该客户已配置海外仓阶梯费用");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<OverseasStorageFeeDtos.TierRequest> normalize(
|
||||
List<OverseasStorageFeeDtos.TierRequest> source) {
|
||||
if (source == null || source.isEmpty() || source.size() > 100) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "阶梯费用明细必须为1至100组");
|
||||
}
|
||||
if (source.stream().anyMatch(tier -> tier == null || tier.startDay() == null
|
||||
|| tier.endDay() == null || tier.unitPrice() == null
|
||||
|| tier.unitPrice().signum() <= 0)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "阶梯天数和仓储费标准必须为有效正数");
|
||||
}
|
||||
List<OverseasStorageFeeDtos.TierRequest> tiers = new ArrayList<>(source);
|
||||
tiers.sort(Comparator.comparingInt(OverseasStorageFeeDtos.TierRequest::startDay));
|
||||
int expectedStart = 1;
|
||||
for (OverseasStorageFeeDtos.TierRequest tier : tiers) {
|
||||
if (tier.startDay() != expectedStart || tier.endDay() < tier.startDay()) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"阶梯必须从第1天开始,且区间连续、不能重叠或倒置");
|
||||
}
|
||||
expectedStart = tier.endDay() + 1;
|
||||
}
|
||||
return List.copyOf(tiers);
|
||||
}
|
||||
|
||||
private static void copy(OverseasStorageFeeRuleEntity rule, long customerId,
|
||||
String customerName, String currency) {
|
||||
rule.setTenantId(customerId);
|
||||
rule.setCustomerName(customerName.trim());
|
||||
rule.setCurrency(currency);
|
||||
}
|
||||
|
||||
private static List<OverseasStorageFeeTierEntity> tierEntities(
|
||||
long ruleId, long customerId, List<OverseasStorageFeeDtos.TierRequest> source) {
|
||||
List<OverseasStorageFeeTierEntity> result = new ArrayList<>(source.size());
|
||||
for (int index = 0; index < source.size(); index++) {
|
||||
OverseasStorageFeeDtos.TierRequest request = source.get(index);
|
||||
OverseasStorageFeeTierEntity tier = new OverseasStorageFeeTierEntity();
|
||||
tier.setTenantId(customerId);
|
||||
tier.setRuleId(ruleId);
|
||||
tier.setStartDay(request.startDay());
|
||||
tier.setEndDay(request.endDay());
|
||||
tier.setUnitPrice(request.unitPrice());
|
||||
tier.setSortNo(index + 1);
|
||||
result.add(tier);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static OverseasStorageFeeDtos.View view(OverseasStorageFeeRuleEntity rule,
|
||||
List<OverseasStorageFeeTierEntity> tiers) {
|
||||
return new OverseasStorageFeeDtos.View(rule.getId(), rule.getTenantId(), rule.getCustomerName(),
|
||||
rule.getCurrency(), tiers.stream().map(tier -> new OverseasStorageFeeDtos.TierView(
|
||||
tier.getId(), tier.getStartDay(), tier.getEndDay(), tier.getUnitPrice(), tier.getSortNo()))
|
||||
.toList(), rule.getVersion(), rule.getCreatedAt(), rule.getUpdatedAt());
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -22,7 +22,9 @@ public class WarehouseSchemaReadinessService {
|
||||
"SELECT id, tenant_id, company_code, company_name, status, deleted FROM wh_express_company LIMIT 0",
|
||||
"SELECT id, tenant_id, warehouse_id, zone_code, zone_name, location_code, location_type, max_weight_kg, shared_overflow, status, deleted, version FROM wh_location LIMIT 0",
|
||||
"SELECT id, tenant_id, warehouse_id, pallet_id, location_id, shared_overflow, reservation_status, expires_at, version FROM wh_location_reservation LIMIT 0",
|
||||
"SELECT id, tenant_id, warehouse_id, pallet_id, location_id, shared_overflow, occupancy_status, active_location_occupancy, active_pallet_occupancy, version FROM wh_location_occupancy LIMIT 0"
|
||||
"SELECT id, tenant_id, warehouse_id, pallet_id, location_id, shared_overflow, occupancy_status, active_location_occupancy, active_pallet_occupancy, version FROM wh_location_occupancy LIMIT 0",
|
||||
"SELECT id, tenant_id, customer_name, currency, version FROM wh_overseas_storage_fee_rule LIMIT 0",
|
||||
"SELECT id, tenant_id, rule_id, start_day, end_day, unit_price, sort_no, version FROM wh_overseas_storage_fee_tier LIMIT 0"
|
||||
);
|
||||
|
||||
@Resource
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.baiyue.warehouse.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PlatformOverseasStorageFeeControllerContractTest {
|
||||
@Test
|
||||
void exposesCrud() {
|
||||
assertThat(PlatformOverseasStorageFeeController.class.getAnnotation(RequestMapping.class).value())
|
||||
.containsExactly("/api/platform/v1/overseas-storage-fees");
|
||||
assertMethod("page", GetMapping.class, "platform:overseas-storage-fee:list");
|
||||
assertMethod("detail", GetMapping.class, "platform:overseas-storage-fee:list");
|
||||
assertMethod("create", PostMapping.class, "platform:overseas-storage-fee:create");
|
||||
assertMethod("update", PutMapping.class, "platform:overseas-storage-fee:edit");
|
||||
assertMethod("delete", DeleteMapping.class, "platform:overseas-storage-fee:delete");
|
||||
}
|
||||
|
||||
private static void assertMethod(String name, Class<? extends Annotation> mapping, String authority) {
|
||||
Method method = Arrays.stream(PlatformOverseasStorageFeeController.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals(name)).findFirst().orElseThrow();
|
||||
assertThat(method.getAnnotation(mapping)).isNotNull();
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('" + authority + "')");
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.warehouse.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class OverseasStorageFeeSqlContractTest {
|
||||
@Test
|
||||
void containsSchemaAndPermissions() throws IOException {
|
||||
String sql = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"));
|
||||
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS wh_overseas_storage_fee_rule")
|
||||
.contains("CREATE TABLE IF NOT EXISTS wh_overseas_storage_fee_tier")
|
||||
.contains("uk_overseas_fee_active_customer")
|
||||
.contains("idx_overseas_fee_page")
|
||||
.contains("idx_overseas_fee_customer_name")
|
||||
.contains("uk_overseas_fee_tier_start")
|
||||
.contains("idx_overseas_fee_tier_rule")
|
||||
.contains("platform:overseas-storage-fee:list")
|
||||
.contains("platform:overseas-storage-fee:create")
|
||||
.contains("platform:overseas-storage-fee:edit")
|
||||
.contains("platform:overseas-storage-fee:delete")
|
||||
.contains("CALL unified_assert_overseas_storage_fee()")
|
||||
.contains("海外仓阶梯费用表、索引和权限校验通过");
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.warehouse.dto.OverseasStorageFeeDtos;
|
||||
import com.baiyue.warehouse.entity.OverseasStorageFeeRuleEntity;
|
||||
import com.baiyue.warehouse.entity.OverseasStorageFeeTierEntity;
|
||||
import com.baiyue.warehouse.repository.OverseasStorageFeeRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(OverseasStorageFeeService.class)
|
||||
class OverseasStorageFeeServiceTest {
|
||||
@MockBean
|
||||
private OverseasStorageFeeRepository repository;
|
||||
@Resource
|
||||
private OverseasStorageFeeService service;
|
||||
|
||||
@Test
|
||||
void createsRule() {
|
||||
doAnswer(invocation -> {
|
||||
OverseasStorageFeeRuleEntity rule = invocation.getArgument(0);
|
||||
rule.setId(10L);
|
||||
return null;
|
||||
}).when(repository).insertRule(any());
|
||||
|
||||
Long id = service.create(new OverseasStorageFeeDtos.CreateRequest(
|
||||
10001L, "示例客户", "USD", List.of(tier(8, 30, "0.8"), tier(1, 7, "0.5"))));
|
||||
|
||||
assertThat(id).isEqualTo(10L);
|
||||
verify(repository).insertTiers(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsTierGap() {
|
||||
OverseasStorageFeeDtos.CreateRequest request = new OverseasStorageFeeDtos.CreateRequest(
|
||||
10001L, "示例客户", "USD", List.of(tier(1, 7, "0.5"), tier(9, 30, "0.8")));
|
||||
|
||||
assertThatThrownBy(() -> service.create(request)).isInstanceOf(BizException.class);
|
||||
verify(repository, never()).insertRule(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDuplicateCustomer() {
|
||||
when(repository.existsCustomer(10001L, null)).thenReturn(true);
|
||||
OverseasStorageFeeDtos.CreateRequest request = new OverseasStorageFeeDtos.CreateRequest(
|
||||
10001L, "示例客户", "USD", List.of(tier(1, 30, "0.8")));
|
||||
|
||||
assertThatThrownBy(() -> service.create(request)).isInstanceOf(BizException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pagesWithTiers() {
|
||||
OverseasStorageFeeRuleEntity rule = rule();
|
||||
OverseasStorageFeeTierEntity tier = new OverseasStorageFeeTierEntity();
|
||||
tier.setId(20L);
|
||||
tier.setRuleId(10L);
|
||||
tier.setStartDay(1);
|
||||
tier.setEndDay(30);
|
||||
tier.setUnitPrice(new BigDecimal("0.8"));
|
||||
tier.setSortNo(1);
|
||||
OverseasStorageFeeDtos.PageQuery query = new OverseasStorageFeeDtos.PageQuery(1, 20, null, null);
|
||||
when(repository.page(query)).thenReturn(new PageResult<>(List.of(rule), 1, 20, 1, 1));
|
||||
when(repository.findTiers(List.of(10L))).thenReturn(List.of(tier));
|
||||
|
||||
OverseasStorageFeeDtos.View result = service.page(query).records().get(0);
|
||||
|
||||
assertThat(result.customerId()).isEqualTo(10001L);
|
||||
assertThat(result.tiers()).singleElement().extracting(OverseasStorageFeeDtos.TierView::unitPrice)
|
||||
.isEqualTo(new BigDecimal("0.8"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsStaleUpdate() {
|
||||
OverseasStorageFeeRuleEntity rule = rule();
|
||||
when(repository.find(10L)).thenReturn(rule);
|
||||
OverseasStorageFeeDtos.UpdateRequest request = new OverseasStorageFeeDtos.UpdateRequest(
|
||||
10001L, "示例客户", "USD", List.of(tier(1, 30, "0.8")), 2);
|
||||
|
||||
assertThatThrownBy(() -> service.update(10L, request)).isInstanceOf(BizException.class);
|
||||
verify(repository, never()).updateRule(any());
|
||||
}
|
||||
|
||||
private static OverseasStorageFeeDtos.TierRequest tier(int start, int end, String price) {
|
||||
return new OverseasStorageFeeDtos.TierRequest(start, end, new BigDecimal(price));
|
||||
}
|
||||
|
||||
private static OverseasStorageFeeRuleEntity rule() {
|
||||
OverseasStorageFeeRuleEntity rule = new OverseasStorageFeeRuleEntity();
|
||||
rule.setId(10L);
|
||||
rule.setTenantId(10001L);
|
||||
rule.setCustomerName("示例客户");
|
||||
rule.setCurrency("USD");
|
||||
rule.setVersion(1);
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user