This commit is contained in:
2026-08-27 11:07:57 +08:00
parent b4c46280de
commit 4612181ab0
19 changed files with 424 additions and 68 deletions
@@ -1,6 +1,7 @@
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.warehouse.dto.PalletRequests;
@@ -10,6 +11,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Size;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
@@ -21,7 +24,6 @@ import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.List;
/** 内部托盘主数据只读接口。 */
@Validated
@@ -41,7 +43,8 @@ public class InternalPalletController {
}
@GetMapping("/{palletId}/snapshot")
@Operation(summary = "查询托盘只读快照", description = "固定按平台公共作用域校验仓库归属,不返回动态状态或可用性")
@Operation(summary = "查询当前可用于称重的托盘快照",
description = "固定按平台公共作用域校验启用中的国内仓归属;托盘必须当前未删除")
public ApiResponse<PalletRequests.Snapshot> snapshot(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@PathVariable @Min(1) long palletId, @RequestParam @Min(1) long warehouseId) {
@@ -50,12 +53,17 @@ public class InternalPalletController {
}
@GetMapping("/snapshots")
@Operation(summary = "查询仓库托盘快照列表", description = "返回该仓全部未删除托盘主数据,不按动态状态或占用过滤")
public ApiResponse<List<PalletRequests.Snapshot>> snapshots(
@Operation(summary = "分页查询当前可用于称重的托盘候选",
description = "固定按平台公共作用域、启用中的国内仓和当前未删除资格过滤;托盘可复用,不按预占或占用过滤")
public ApiResponse<PageResult<PalletRequests.Snapshot>> snapshots(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@RequestParam @Min(1) long warehouseId) {
@RequestParam @Min(1) long warehouseId,
@RequestParam(defaultValue = "1") @Min(1) long page,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) long size,
@RequestParam(required = false) @Size(max = 50) String keyword) {
verify(token);
return ApiResponse.success(service.snapshots(0L, warehouseId));
return ApiResponse.success(service.snapshotCandidates(
0L, warehouseId, new PalletRequests.SnapshotPageQuery(page, size, keyword)));
}
private void verify(String token) {
@@ -84,4 +84,21 @@ public final class PalletRequests {
@Schema(description = "皮重,单位kg") BigDecimal tareWeightKg
) {
}
@Schema(name = "InternalPalletCandidatePageQuery", description = "称重托盘候选分页查询")
public record SnapshotPageQuery(
@Min(1) @Schema(description = "页码", defaultValue = "1") long page,
@Min(1) @Max(100) @Schema(description = "每页条数", defaultValue = "20", maximum = "100") long size,
@Size(max = 50) @Schema(description = "关键词;匹配托盘编码或类型") String keyword
) {
public SnapshotPageQuery {
if (page == 0) {
page = 1;
}
if (size == 0) {
size = 20;
}
keyword = keyword == null ? null : keyword.trim();
}
}
}
@@ -46,12 +46,18 @@ public class PalletRepository {
return mapper.selectIncludingDeleted(tenantId, warehouseId, id);
}
public List<PalletEntity> findAllByWarehouse(long tenantId, long warehouseId) {
return mapper.selectList(new LambdaQueryWrapper<PalletEntity>()
public PageResult<PalletEntity> candidatePage(long tenantId, long warehouseId,
PalletRequests.SnapshotPageQuery query) {
LambdaQueryWrapper<PalletEntity> wrapper = new LambdaQueryWrapper<PalletEntity>()
.eq(PalletEntity::getTenantId, tenantId)
.eq(PalletEntity::getWarehouseId, warehouseId)
.and(StringUtils.hasText(query.keyword()), nested -> nested
.like(PalletEntity::getPalletCode, query.keyword())
.or()
.like(PalletEntity::getPalletType, query.keyword()))
.orderByAsc(PalletEntity::getPalletCode)
.orderByAsc(PalletEntity::getId));
.orderByAsc(PalletEntity::getId);
return PageResult.from(mapper.selectPage(new Page<>(query.page(), query.size()), wrapper));
}
public boolean existsCode(long tenantId, long warehouseId, String code, Long excludedId) {
@@ -125,16 +125,19 @@ public class PalletApplicationService {
}
public PalletRequests.Snapshot snapshot(long tenantId, long warehouseId, long palletId) {
requireWarehouse(tenantId, warehouseId, true);
PalletEntity entity = owned(tenantId, warehouseId, palletId);
return new PalletRequests.Snapshot(entity.getId(), entity.getPalletCode(), entity.getPalletType(),
entity.getCapacityCbm(), entity.getWeightKg());
}
public List<PalletRequests.Snapshot> snapshots(long tenantId, long warehouseId) {
requireWarehouse(tenantId, warehouseId, false);
return repository.findAllByWarehouse(tenantId, warehouseId).stream()
public PageResult<PalletRequests.Snapshot> snapshotCandidates(
long tenantId, long warehouseId, PalletRequests.SnapshotPageQuery query) {
requireWarehouse(tenantId, warehouseId, true);
PageResult<PalletEntity> source = repository.candidatePage(tenantId, warehouseId, query);
return new PageResult<>(source.records().stream()
.map(entity -> new PalletRequests.Snapshot(entity.getId(), entity.getPalletCode(), entity.getPalletType(),
entity.getCapacityCbm(), entity.getWeightKg()))
.toList();
.toList(), source.page(), source.size(), source.total(), source.pages());
}
}
@@ -2,12 +2,12 @@ openapi: 3.0.3
info:
title: Warehouse Pallet API
version: 1.0.0
description: 托盘是可复用标识和称重主数据,不承载 AVAILABLE、RESERVED 或 OCCUPIED 动态状态。
description: 托盘是可复用标识和称重主数据;称重候选按当前启用国内仓、同仓归属和未删除资格过滤,不按预占或占用过滤。
paths:
/internal/warehouse/v1/pallets/{palletId}/snapshot:
get:
summary: 查询单条托盘主数据快照
description: 固定使用平台公共作用域 tenant_id=0,并按 warehouseId 校验归属;不返回状态或可用性。
summary: 查询单条当前称重可用托盘快照
description: 固定使用平台公共作用域 tenant_id=0,并校验启用国内仓、warehouseId 归属和当前未删除资格。
parameters:
- $ref: '#/components/parameters/InternalToken'
- { in: path, name: palletId, required: true, schema: { type: integer, format: int64, minimum: 1 } }
@@ -18,15 +18,18 @@ paths:
content: { application/json: { schema: { $ref: '#/components/schemas/PalletSnapshotResponse' } } }
/internal/warehouse/v1/pallets/snapshots:
get:
summary: 查询仓库全部托盘主数据快照
description: 固定使用平台公共作用域 tenant_id=0,返回该仓全部未删除主数据,不按状态或占用过滤。
summary: 分页查询仓库当前称重托盘候选
description: 固定使用平台公共作用域 tenant_id=0,按启用国内仓、同仓归属和当前未删除资格过滤;托盘可复用,不按预占或占用过滤。
parameters:
- $ref: '#/components/parameters/InternalToken'
- { in: query, name: warehouseId, required: true, schema: { type: integer, format: int64, minimum: 1 } }
- { in: query, name: page, required: false, schema: { type: integer, format: int64, minimum: 1, default: 1 } }
- { in: query, name: size, required: false, schema: { type: integer, format: int64, minimum: 1, maximum: 100, default: 20 } }
- { in: query, name: keyword, required: false, description: 匹配托盘编码或类型, schema: { type: string, maxLength: 50 } }
responses:
'200':
description: 业务响应
content: { application/json: { schema: { $ref: '#/components/schemas/PalletSnapshotListResponse' } } }
content: { application/json: { schema: { $ref: '#/components/schemas/PalletSnapshotPageResponse' } } }
components:
parameters:
InternalToken:
@@ -61,12 +64,21 @@ components:
required: [data]
properties:
data: { $ref: '#/components/schemas/PalletSnapshot' }
PalletSnapshotListResponse:
PalletSnapshotPageResponse:
allOf:
- { $ref: '#/components/schemas/ApiEnvelope' }
- type: object
required: [data]
properties:
data:
type: array
items: { $ref: '#/components/schemas/PalletSnapshot' }
type: object
required: [records, page, size, total, pages]
properties:
records:
type: array
maxItems: 100
items: { $ref: '#/components/schemas/PalletSnapshot' }
page: { type: integer, format: int64, minimum: 1 }
size: { type: integer, format: int64, minimum: 1, maximum: 100 }
total: { type: integer, format: int64, minimum: 0 }
pages: { type: integer, format: int64, minimum: 0 }
@@ -1,6 +1,7 @@
package com.baiyue.warehouse.controller;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.warehouse.dto.PalletRequests;
import com.baiyue.warehouse.service.PalletApplicationService;
import org.junit.jupiter.api.BeforeEach;
@@ -39,17 +40,20 @@ class InternalPalletControllerTest {
}
@Test
void listsAllWarehouseSnapshotsFromPlatformScope() {
void pagesWarehouseCandidatesFromPlatformScope() {
List<PalletRequests.Snapshot> expected = List.of(snapshot(30L, "TP-001"), snapshot(31L, "TP-002"));
when(service.snapshots(0L, 20L)).thenReturn(expected);
PalletRequests.SnapshotPageQuery query = new PalletRequests.SnapshotPageQuery(2, 20, "TP");
PageResult<PalletRequests.Snapshot> page = new PageResult<>(expected, 2, 20, 1000, 50);
when(service.snapshotCandidates(0L, 20L, query)).thenReturn(page);
assertThat(controller.snapshots(TOKEN, 20L).data()).isEqualTo(expected);
verify(service).snapshots(0L, 20L);
assertThat(controller.snapshots(TOKEN, 20L, 2, 20, "TP").data()).isEqualTo(page);
verify(service).snapshotCandidates(0L, 20L, query);
}
@Test
void rejectsInvalidInternalToken() {
assertThatThrownBy(() -> controller.snapshots("invalid", 20L)).isInstanceOf(BizException.class);
assertThatThrownBy(() -> controller.snapshots("invalid", 20L, 1, 20, null))
.isInstanceOf(BizException.class);
verifyNoInteractions(service);
}
@@ -16,6 +16,10 @@ class PalletOpenApiContractTest {
assertThat(yaml)
.contains("/internal/warehouse/v1/pallets/{palletId}/snapshot:")
.contains("/internal/warehouse/v1/pallets/snapshots:")
.contains("name: keyword")
.contains("maximum: 100")
.contains("maxItems: 100")
.contains("PalletSnapshotPageResponse:")
.contains("required: [palletId, palletCode, palletType, capacityCbm, tareWeightKg]")
.contains("additionalProperties: false")
.doesNotContain("tenantId:")
@@ -64,6 +64,26 @@ class PalletRepositoryTest {
.containsExactlyInAnyOrder(10001L, 20L, 30L);
}
@Test
void candidatePageIsWarehouseScopedKeywordFilteredAndBounded() {
when(mapper.selectPage(any(Page.class), any())).thenAnswer(invocation -> invocation.getArgument(0));
repository.candidatePage(0L, 20L,
new PalletRequests.SnapshotPageQuery(3, 100, " TP-9 "));
@SuppressWarnings("unchecked")
ArgumentCaptor<Page<PalletEntity>> page = ArgumentCaptor.forClass(Page.class);
@SuppressWarnings("unchecked")
ArgumentCaptor<LambdaQueryWrapper<PalletEntity>> wrapper =
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(mapper).selectPage(page.capture(), wrapper.capture());
wrapper.getValue().getSqlSegment();
assertThat(page.getValue().getCurrent()).isEqualTo(3);
assertThat(page.getValue().getSize()).isEqualTo(100);
assertThat(wrapper.getValue().getParamNameValuePairs().values())
.contains(0L, 20L, "%TP-9%");
}
private Collection<Object> queryValues(Long tenantId, Long warehouseId) {
when(mapper.selectPage(any(Page.class), any())).thenAnswer(invocation -> invocation.getArgument(0));
repository.page(tenantId,
@@ -18,6 +18,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.util.List;
@SpringJUnitConfig(PalletApplicationService.class)
class PalletApplicationServiceTest {
@@ -64,21 +65,42 @@ class PalletApplicationServiceTest {
}
@Test
void returnsReusablePalletSnapshotsWithoutResourceStatus() {
void returnsControlledReusablePalletCandidatePage() {
WarehouseEntity warehouse = new WarehouseEntity();
warehouse.setWarehouseType("DOMESTIC");
warehouse.setStatus("ACTIVE");
when(warehouseRepository.findOwned(0L, 20L)).thenReturn(warehouse);
PalletEntity entity = pallet();
entity.setPalletCode("TP-001");
entity.setPalletType("STANDARD");
entity.setCapacityCbm(new BigDecimal("1.200000"));
entity.setWeightKg(new BigDecimal("8.000"));
when(repository.findAllByWarehouse(0L, 20L)).thenReturn(java.util.List.of(entity));
PalletRequests.SnapshotPageQuery query = new PalletRequests.SnapshotPageQuery(1, 20, "TP-0");
when(repository.candidatePage(0L, 20L, query)).thenReturn(
new PageResult<>(List.of(entity), 1, 20, 1000, 50));
org.assertj.core.api.Assertions.assertThat(service.snapshots(0L, 20L)).singleElement().satisfies(snapshot -> {
PageResult<PalletRequests.Snapshot> result = service.snapshotCandidates(0L, 20L, query);
org.assertj.core.api.Assertions.assertThat(result.records()).singleElement().satisfies(snapshot -> {
org.assertj.core.api.Assertions.assertThat(snapshot.palletId()).isEqualTo(30L);
org.assertj.core.api.Assertions.assertThat(snapshot.tareWeightKg()).isEqualByComparingTo("8.000");
});
org.assertj.core.api.Assertions.assertThat(result.total()).isEqualTo(1000);
org.assertj.core.api.Assertions.assertThat(result.records()).hasSize(1);
verify(repository).candidatePage(0L, 20L, query);
}
@Test
void rejectsCandidatesFromInactiveWarehouse() {
WarehouseEntity warehouse = new WarehouseEntity();
warehouse.setWarehouseType("DOMESTIC");
warehouse.setStatus("INACTIVE");
when(warehouseRepository.findOwned(0L, 20L)).thenReturn(warehouse);
assertThatThrownBy(() -> service.snapshotCandidates(
0L, 20L, new PalletRequests.SnapshotPageQuery(1, 20, null)))
.isInstanceOf(BizException.class)
.hasMessageContaining("启用中");
}
private static PalletEntity pallet() {