优化
This commit is contained in:
+11
-1
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.firstleg.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -16,8 +17,17 @@ import java.util.List;
|
||||
url = "${baiyue.clients.warehouse.url:}", path = "/internal/warehouse/v1")
|
||||
public interface WarehousePalletLocationClient {
|
||||
@GetMapping("/pallets/snapshots")
|
||||
ApiResponse<List<PalletSnapshot>> palletSnapshots(
|
||||
ApiResponse<PageResult<PalletSnapshot>> palletSnapshots(
|
||||
@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestParam("warehouseId") long warehouseId,
|
||||
@RequestParam("page") long page,
|
||||
@RequestParam("size") long size,
|
||||
@RequestParam(value = "keyword", required = false) String keyword);
|
||||
|
||||
@GetMapping("/pallets/{palletId}/snapshot")
|
||||
ApiResponse<PalletSnapshot> palletSnapshot(
|
||||
@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@org.springframework.web.bind.annotation.PathVariable("palletId") long palletId,
|
||||
@RequestParam("warehouseId") long warehouseId);
|
||||
|
||||
@PostMapping("/pallet-locations/reservations")
|
||||
|
||||
+13
-5
@@ -4,6 +4,7 @@ import jakarta.annotation.Resource;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
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;
|
||||
@@ -18,8 +19,12 @@ import com.baiyue.firstleg.service.MeasurementSubmissionService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Size;
|
||||
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.GetMapping;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -33,10 +38,10 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 平台后台入库测量录入、提交和 OCR 重量结果维护接口。 */
|
||||
@Tag(name = "平台入库测量", description = "维护测量草稿、箱规明细和可追溯 OCR 重量结果。")
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/first-leg/inbounds/{inboundId}")
|
||||
public class PlatformInboundMeasurementController {
|
||||
@@ -56,16 +61,19 @@ public class PlatformInboundMeasurementController {
|
||||
}
|
||||
|
||||
@Operation(summary = "查询称重可选托盘快照",
|
||||
description = "按入库单所属仓库返回未删除的托盘只读快照,不校验托盘状态或占用资格。")
|
||||
description = "按入库单所属仓库分页返回当前有效托盘候选,支持编码或类型关键词;托盘可复用,不按预占或占用过滤。")
|
||||
@GetMapping("/weight-pallet-options")
|
||||
@PreAuthorize("hasAnyAuthority('platform:firstleg:inbound:measure:view',"
|
||||
+ " 'platform:firstleg:inbound:weight:edit')")
|
||||
public ApiResponse<List<InboundMeasurementResponses.PalletSnapshot>> weightPalletOptions(
|
||||
public ApiResponse<PageResult<InboundMeasurementResponses.PalletSnapshot>> weightPalletOptions(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@PathVariable long inboundId) {
|
||||
@PathVariable long inboundId,
|
||||
@RequestParam(defaultValue = "1") @Min(1) long page,
|
||||
@RequestParam(defaultValue = "20") @Min(1) @Max(100) long size,
|
||||
@RequestParam(required = false) @Size(max = 50) String keyword) {
|
||||
requirePlatformTenant(user, tenantId);
|
||||
return ApiResponse.success(service.weightPalletOptions(tenantId, inboundId));
|
||||
return ApiResponse.success(service.weightPalletOptions(tenantId, inboundId, page, size, keyword));
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "INBOUND_MEASUREMENT_DRAFT_SAVE", resourceType = "INBOUND",
|
||||
|
||||
@@ -4,6 +4,7 @@ import jakarta.annotation.Resource;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.OrderCargoClient;
|
||||
import com.baiyue.firstleg.client.MeasurementImageClient;
|
||||
@@ -73,14 +74,16 @@ public class InboundMeasurementService {
|
||||
return toDetail(inbound, measurement, items, weights, review, declaration);
|
||||
}
|
||||
|
||||
public List<InboundMeasurementResponses.PalletSnapshot> weightPalletOptions(
|
||||
long tenantId, long inboundId) {
|
||||
public PageResult<InboundMeasurementResponses.PalletSnapshot> weightPalletOptions(
|
||||
long tenantId, long inboundId, long page, long size, String keyword) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
return loadPalletSnapshots(inbound).stream()
|
||||
PageResult<WarehousePalletLocationClient.PalletSnapshot> source =
|
||||
loadPalletSnapshotPage(inbound, page, size, keyword);
|
||||
return new PageResult<>(source.records().stream()
|
||||
.map(snapshot -> new InboundMeasurementResponses.PalletSnapshot(
|
||||
snapshot.palletId(), snapshot.palletCode(), snapshot.palletType(),
|
||||
snapshot.capacityCbm(), snapshot.tareWeightKg()))
|
||||
.toList();
|
||||
.toList(), source.page(), source.size(), source.total(), source.pages());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -175,11 +178,8 @@ public class InboundMeasurementService {
|
||||
if (repository.findWeightByFile(tenantId, inboundId, request.fileId()) != null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "同一称重图片不能重复计重");
|
||||
}
|
||||
WarehousePalletLocationClient.PalletSnapshot pallet = loadPalletSnapshots(inbound).stream()
|
||||
.filter(snapshot -> snapshot.palletId() == request.palletId())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"所选托盘不属于当前入库仓库或已被删除"));
|
||||
WarehousePalletLocationClient.PalletSnapshot pallet = loadPalletSnapshot(
|
||||
inbound, request.palletId());
|
||||
validatePalletSnapshot(pallet);
|
||||
String unit = rawOcrUnit(request.ocrWeightUnit());
|
||||
InboundWeightResultEntity weight = new InboundWeightResultEntity();
|
||||
@@ -527,23 +527,43 @@ public class InboundMeasurementService {
|
||||
weight.getAdjustedAt(), weight.getConfirmedBy(), weight.getConfirmedAt(), weight.getVersion());
|
||||
}
|
||||
|
||||
private List<WarehousePalletLocationClient.PalletSnapshot> loadPalletSnapshots(
|
||||
InboundOrderEntity inbound) {
|
||||
private PageResult<WarehousePalletLocationClient.PalletSnapshot> loadPalletSnapshotPage(
|
||||
InboundOrderEntity inbound, long page, long size, String keyword) {
|
||||
if (inbound.getWarehouseId() == null || inbound.getWarehouseId() <= 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "入库单未绑定有效仓库,无法选择称重托盘");
|
||||
}
|
||||
try {
|
||||
ApiResponse<List<WarehousePalletLocationClient.PalletSnapshot>> response =
|
||||
warehousePallets.palletSnapshots(internalToken, inbound.getWarehouseId());
|
||||
ApiResponse<PageResult<WarehousePalletLocationClient.PalletSnapshot>> response =
|
||||
warehousePallets.palletSnapshots(internalToken, inbound.getWarehouseId(), page, size, keyword);
|
||||
if (response == null || !response.successful() || response.data() == null) {
|
||||
String message = response == null || !StringUtils.hasText(response.message())
|
||||
? "托盘快照查询失败" : response.message();
|
||||
? "托盘候选查询失败" : response.message();
|
||||
throw new BizException(response == null ? ResultCode.SERVICE_UNAVAILABLE
|
||||
: ResultCode.of(response.code()), message);
|
||||
}
|
||||
return response.data();
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓库服务暂不可用,无法查询托盘快照");
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓库服务暂不可用,无法查询托盘候选");
|
||||
}
|
||||
}
|
||||
|
||||
private WarehousePalletLocationClient.PalletSnapshot loadPalletSnapshot(
|
||||
InboundOrderEntity inbound, long palletId) {
|
||||
if (inbound.getWarehouseId() == null || inbound.getWarehouseId() <= 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "入库单未绑定有效仓库,无法选择称重托盘");
|
||||
}
|
||||
try {
|
||||
ApiResponse<WarehousePalletLocationClient.PalletSnapshot> response = warehousePallets.palletSnapshot(
|
||||
internalToken, palletId, inbound.getWarehouseId());
|
||||
if (response == null || !response.successful() || response.data() == null) {
|
||||
String message = response == null || !StringUtils.hasText(response.message())
|
||||
? "所选托盘当前不可用于该入库仓库" : response.message();
|
||||
throw new BizException(response == null ? ResultCode.SERVICE_UNAVAILABLE
|
||||
: ResultCode.of(response.code()), message);
|
||||
}
|
||||
return response.data();
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓库服务暂不可用,无法校验托盘资格");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -755,11 +755,14 @@ paths:
|
||||
get:
|
||||
summary: 查询称重可选托盘快照
|
||||
description: >-
|
||||
需要入库测量查看或称重编辑权限。Firstleg 按入库单 warehouseId 查询 Warehouse 公共托盘快照;
|
||||
仅限定同仓且未删除,不校验托盘状态或占用资格,Admin 不需要 platform:pallet:list。
|
||||
需要入库测量查看或称重编辑权限。Firstleg 按入库单 warehouseId 分页查询 Warehouse 当前称重托盘候选;
|
||||
Warehouse 校验平台公共作用域、启用国内仓、同仓归属和当前未删除资格。托盘可复用,不按预占或占用过滤,Admin 不需要 platform:pallet:list。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: page, in: query, required: false, schema: {type: integer, format: int64, minimum: 1, default: 1}}
|
||||
- {name: size, in: query, required: false, schema: {type: integer, format: int64, minimum: 1, maximum: 100, default: 20}}
|
||||
- {name: keyword, in: query, required: false, description: 匹配托盘编码或类型, schema: {type: string, maxLength: 50}}
|
||||
responses:
|
||||
'200':
|
||||
description: 成功或业务失败,按响应体 code 判断
|
||||
@@ -770,7 +773,7 @@ paths:
|
||||
properties:
|
||||
code: {type: string}
|
||||
message: {type: string}
|
||||
data: {type: array, items: {$ref: '#/components/schemas/WeightPalletSnapshot'}}
|
||||
data: {$ref: '#/components/schemas/WeightPalletSnapshotPage'}
|
||||
'403': {description: 无入库测量查看或称重编辑权限}
|
||||
/api/platform/v1/first-leg/inbounds/{inboundId}/weight-results:
|
||||
post:
|
||||
@@ -1294,6 +1297,18 @@ components:
|
||||
palletType: {type: string}
|
||||
capacityCbm: {type: number, format: double, minimum: 0.0001}
|
||||
tareWeightKg: {type: number, format: double, minimum: 0, multipleOf: 0.001}
|
||||
WeightPalletSnapshotPage:
|
||||
type: object
|
||||
required: [records, page, size, total, pages]
|
||||
properties:
|
||||
records:
|
||||
type: array
|
||||
maxItems: 100
|
||||
items: {$ref: '#/components/schemas/WeightPalletSnapshot'}
|
||||
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}
|
||||
CreateWeightResultRequest:
|
||||
type: object
|
||||
required: [fileId, palletId]
|
||||
|
||||
+17
-1
@@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@@ -16,11 +17,26 @@ class WarehousePalletLocationClientContractTest {
|
||||
assertThat(client.path()).isEqualTo("/internal/warehouse/v1");
|
||||
|
||||
Method method = WarehousePalletLocationClient.class.getDeclaredMethod(
|
||||
"palletSnapshots", String.class, long.class);
|
||||
"palletSnapshots", String.class, long.class, long.class, long.class, String.class);
|
||||
assertThat(method.getAnnotation(GetMapping.class).value())
|
||||
.containsExactly("/pallets/snapshots");
|
||||
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).value())
|
||||
.isEqualTo("warehouseId");
|
||||
assertThat(method.getParameters()[2].getAnnotation(RequestParam.class).value())
|
||||
.isEqualTo("page");
|
||||
assertThat(method.getParameters()[3].getAnnotation(RequestParam.class).value())
|
||||
.isEqualTo("size");
|
||||
assertThat(method.getParameters()[4].getAnnotation(RequestParam.class).value())
|
||||
.isEqualTo("keyword");
|
||||
|
||||
Method exact = WarehousePalletLocationClient.class.getDeclaredMethod(
|
||||
"palletSnapshot", String.class, long.class, long.class);
|
||||
assertThat(exact.getAnnotation(GetMapping.class).value())
|
||||
.containsExactly("/pallets/{palletId}/snapshot");
|
||||
assertThat(exact.getParameters()[1].getAnnotation(PathVariable.class).value())
|
||||
.isEqualTo("palletId");
|
||||
assertThat(exact.getParameters()[2].getAnnotation(RequestParam.class).value())
|
||||
.isEqualTo("warehouseId");
|
||||
|
||||
var snapshot = new WarehousePalletLocationClient.PalletSnapshot(
|
||||
101L, "PLT-101", "STANDARD", new java.math.BigDecimal("2.0000"),
|
||||
|
||||
+17
-1
@@ -6,10 +6,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -17,6 +20,19 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
@DisplayName("平台入库测量接口权限契约")
|
||||
class PlatformInboundMeasurementControllerContractTest {
|
||||
@Test
|
||||
void palletCandidatesArePaginatedAndBoundedInOpenApi() throws IOException {
|
||||
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(openapi)
|
||||
.contains("/api/platform/v1/first-leg/inbounds/{inboundId}/weight-pallet-options:")
|
||||
.contains("name: keyword")
|
||||
.contains("maximum: 100")
|
||||
.contains("maxItems: 100")
|
||||
.contains("data: {$ref: '#/components/schemas/WeightPalletSnapshotPage'}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void draftRequestOnlyAcceptsEditableGoodsNameAndMeasurementItems() {
|
||||
assertThat(Arrays.stream(InboundMeasurementRequests.SaveDraftRequest.class.getRecordComponents())
|
||||
@@ -41,7 +57,7 @@ class PlatformInboundMeasurementControllerContractTest {
|
||||
assertAuthority("weightPalletOptions",
|
||||
"hasAnyAuthority('platform:firstleg:inbound:measure:view',"
|
||||
+ " 'platform:firstleg:inbound:weight:edit')",
|
||||
AuthenticatedUser.class, long.class, long.class);
|
||||
AuthenticatedUser.class, long.class, long.class, long.class, long.class, String.class);
|
||||
assertAuthority("saveDraft", "hasAuthority('platform:firstleg:inbound:measure:edit')",
|
||||
AuthenticatedUser.class, long.class, long.class,
|
||||
InboundMeasurementRequests.SaveDraftRequest.class);
|
||||
|
||||
+38
-6
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.firstleg.client.OrderCargoClient;
|
||||
import com.baiyue.firstleg.client.MeasurementImageClient;
|
||||
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
|
||||
@@ -59,8 +60,8 @@ class InboundMeasurementServiceTest {
|
||||
@BeforeEach
|
||||
void stubWarehousePalletSnapshots() {
|
||||
when(repository.updateInboundWeightSummary(any())).thenReturn(true);
|
||||
when(warehousePallets.palletSnapshots("internal-token", 100L)).thenReturn(
|
||||
ApiResponse.success(List.of(palletSnapshot())));
|
||||
when(warehousePallets.palletSnapshot("internal-token", 501L, 100L)).thenReturn(
|
||||
ApiResponse.success(palletSnapshot()));
|
||||
when(orderCargoClient.measurementDeclaration("internal-token", 1L, 80L)).thenReturn(
|
||||
ApiResponse.success(new OrderCargoClient.MeasurementDeclarationSnapshot(
|
||||
"REF-50", new BigDecimal("2000"), new BigDecimal("20"), "123123")));
|
||||
@@ -238,20 +239,28 @@ class InboundMeasurementServiceTest {
|
||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
|
||||
List<InboundMeasurementResponses.PalletSnapshot> result =
|
||||
service.weightPalletOptions(1L, 10L);
|
||||
when(warehousePallets.palletSnapshots("internal-token", 100L, 1L, 20L, "TP-5"))
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(palletSnapshot()), 1, 20, 1000, 50)));
|
||||
|
||||
assertThat(result).singleElement().satisfies(snapshot -> {
|
||||
PageResult<InboundMeasurementResponses.PalletSnapshot> result =
|
||||
service.weightPalletOptions(1L, 10L, 1L, 20L, "TP-5");
|
||||
|
||||
assertThat(result.records()).singleElement().satisfies(snapshot -> {
|
||||
assertThat(snapshot.palletId()).isEqualTo(501L);
|
||||
assertThat(snapshot.tareWeightKg()).isEqualByComparingTo("18.500");
|
||||
});
|
||||
verify(warehousePallets).palletSnapshots("internal-token", 100L);
|
||||
assertThat(result.total()).isEqualTo(1000);
|
||||
assertThat(result.records()).hasSize(1);
|
||||
verify(warehousePallets).palletSnapshots("internal-token", 100L, 1L, 20L, "TP-5");
|
||||
}
|
||||
|
||||
@Test
|
||||
void creatingWeightResultRejectsPalletOutsideTheInboundWarehouse() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(warehousePallets.palletSnapshot("internal-token", 999L, 100L)).thenReturn(
|
||||
ApiResponse.failure(com.baiyue.common.core.api.ResultCode.NOT_FOUND,
|
||||
"所选托盘不属于当前入库仓库或当前不可用", null, null));
|
||||
|
||||
assertThatThrownBy(() -> service.createWeightResult(1L, 10L, 9L,
|
||||
new InboundMeasurementRequests.CreateWeightResultRequest(
|
||||
@@ -263,6 +272,29 @@ class InboundMeasurementServiceTest {
|
||||
verify(repository, never()).insertWeight(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailUsesStoredPalletSnapshotAfterCandidateBecomesUnavailable() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_SPLIT");
|
||||
InboundWeightResultEntity historical = weight(50L, "30.000", "KG", "CONFIRMED", 2);
|
||||
historical.setPalletCode("HISTORICAL-501");
|
||||
historical.setPalletType("LEGACY-STANDARD");
|
||||
historical.setPalletTareWeightKg(new BigDecimal("18.500"));
|
||||
historical.setGrossWeightKg(new BigDecimal("30.000"));
|
||||
historical.setNetWeightKg(new BigDecimal("11.500"));
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of(historical));
|
||||
|
||||
InboundMeasurementResponses.Detail result = service.detail(1L, 10L);
|
||||
|
||||
assertThat(result.weightResults()).singleElement().satisfies(weight -> {
|
||||
assertThat(weight.palletCode()).isEqualTo("HISTORICAL-501");
|
||||
assertThat(weight.palletType()).isEqualTo("LEGACY-STANDARD");
|
||||
assertThat(weight.palletTareWeightKg()).isEqualByComparingTo("18.500");
|
||||
});
|
||||
verify(warehousePallets, never()).palletSnapshot(any(), anyLong(), anyLong());
|
||||
verify(warehousePallets, never()).palletSnapshots(any(), anyLong(), anyLong(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmingLbWeightConvertsToKgAndRecalculatesOnlyConfirmedImages() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||
|
||||
@@ -5,7 +5,10 @@ import test from 'node:test'
|
||||
import {
|
||||
confirmedNetSummary,
|
||||
normalizedGrossKg,
|
||||
palletOptionsPath,
|
||||
selectWeightPallet,
|
||||
WEIGHT_PALLET_OPTIONS_PAGE_SIZE,
|
||||
weightPalletOptionRecords,
|
||||
workspaceWeightMatchesBaseline,
|
||||
} from './inbound-weight-ocr.mjs'
|
||||
|
||||
@@ -42,7 +45,15 @@ test('cleanup restores a non-empty carton fallback when there were no confirmed
|
||||
|
||||
test('HTTP scenario uses pallet option and net weight contracts without legacy final weight', () => {
|
||||
const source = readFileSync(new URL('inbound-weight-ocr.mjs', import.meta.url), 'utf8')
|
||||
assert.match(source, /\/weight-pallet-options\?tenantId=/)
|
||||
const optionsPath = palletOptionsPath('10001', '20001', ' TP-09 ')
|
||||
assert.match(optionsPath, /\/weight-pallet-options\?tenantId=10001&(?:page=1&size=50|size=50&page=1)/)
|
||||
assert.match(optionsPath, /keyword=TP-09/)
|
||||
assert.equal(WEIGHT_PALLET_OPTIONS_PAGE_SIZE, 50)
|
||||
assert.deepEqual(weightPalletOptionRecords({
|
||||
records: [{ palletId: '1' }], page: 1, size: 50, total: 1000, pages: 20,
|
||||
}), [{ palletId: '1' }])
|
||||
assert.equal(weightPalletOptionRecords([{ palletId: '1' }]), null)
|
||||
assert.equal(weightPalletOptionRecords({ records: [], page: 1, size: 1000 }), null)
|
||||
assert.ok(source.indexOf('const palletOptionsResponse') < source.lastIndexOf('const uploadPath'))
|
||||
assert.match(source, /form\.append\('file'/)
|
||||
assert.doesNotMatch(source, /form\.append\('palletId'/)
|
||||
|
||||
@@ -4,6 +4,7 @@ import process from 'node:process'
|
||||
|
||||
const MODULE = 'inbound-weight-ocr'
|
||||
const SCENARIO = 'weight-image-recognition-and-confirmation'
|
||||
export const WEIGHT_PALLET_OPTIONS_PAGE_SIZE = 50
|
||||
const DEFAULT_IMAGE = 'C:/Users/BY-OY/Desktop/上传图片包/测量图片/测量图片/orig_1cf27695-b2c3-44b3-98e3-c0be8b80f79a.JPG'
|
||||
const EDITABLE_STATUSES = new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'])
|
||||
const OCR_STATUSES = new Set(['RECOGNIZED', 'FAILED'])
|
||||
@@ -202,8 +203,24 @@ function deletePath(tenantId, inboundId, resultId, version) {
|
||||
return `${resultPath(inboundId, resultId)}?tenantId=${encodeURIComponent(tenantId)}&version=${version}`
|
||||
}
|
||||
|
||||
function palletOptionsPath(tenantId, inboundId) {
|
||||
return `/api/platform/v1/first-leg/inbounds/${encodeURIComponent(inboundId)}/weight-pallet-options?tenantId=${encodeURIComponent(tenantId)}`
|
||||
export function palletOptionsPath(tenantId, inboundId, keyword = '') {
|
||||
const params = new URLSearchParams({
|
||||
tenantId: id(tenantId),
|
||||
page: '1',
|
||||
size: String(WEIGHT_PALLET_OPTIONS_PAGE_SIZE),
|
||||
})
|
||||
const normalizedKeyword = String(keyword || '').trim()
|
||||
if (normalizedKeyword) params.set('keyword', normalizedKeyword)
|
||||
return `/api/platform/v1/first-leg/inbounds/${encodeURIComponent(inboundId)}/weight-pallet-options?${params.toString()}`
|
||||
}
|
||||
|
||||
export function weightPalletOptionRecords(page) {
|
||||
if (!page || Array.isArray(page) || !Array.isArray(page.records)) return null
|
||||
if (!Number.isInteger(Number(page.page)) || Number(page.page) < 1) return null
|
||||
if (!Number.isInteger(Number(page.size)) || Number(page.size) < 1
|
||||
|| Number(page.size) > WEIGHT_PALLET_OPTIONS_PAGE_SIZE) return null
|
||||
if (page.records.length > Number(page.size)) return null
|
||||
return page.records
|
||||
}
|
||||
|
||||
async function createUploadedResult(context, target, pallet, image, overrides = {}) {
|
||||
@@ -382,13 +399,14 @@ export async function run(context) {
|
||||
const palletOptionsResponse = await context.request(
|
||||
'GET', palletOptionsPath(target.tenantId, target.inboundId),
|
||||
)
|
||||
const palletOptions = context.apiSuccess(MODULE, '读取同仓称重托盘候选', palletOptionsResponse,
|
||||
const palletOptionsPage = context.apiSuccess(MODULE, '分页读取同仓称重托盘候选', palletOptionsResponse,
|
||||
`inboundId=${target.inboundId}`, NEW_CONTRACT_OPTIONS)
|
||||
const palletOptions = weightPalletOptionRecords(palletOptionsPage)
|
||||
const palletOptionsComplete = Array.isArray(palletOptions) && palletOptions.length > 0
|
||||
&& palletOptions.every(palletSnapshotComplete)
|
||||
context.assert(MODULE, '托盘候选包含稳定快照字段且无需状态或占用资格', palletOptionsResponse,
|
||||
context.assert(MODULE, '托盘候选分页受控且包含稳定快照字段', palletOptionsResponse,
|
||||
palletOptionsComplete,
|
||||
`count=${Array.isArray(palletOptions) ? palletOptions.length : 'invalid'}, fields=palletId/palletCode/palletType/capacityCbm/tareWeightKg`,
|
||||
`page=${palletOptionsPage?.page ?? 'missing'}, size=${palletOptionsPage?.size ?? 'missing'}, count=${Array.isArray(palletOptions) ? palletOptions.length : 'invalid'}, maxSize=${WEIGHT_PALLET_OPTIONS_PAGE_SIZE}, fields=palletId/palletCode/palletType/capacityCbm/tareWeightKg`,
|
||||
NEW_CONTRACT_OPTIONS)
|
||||
const pallet = palletOptionsComplete ? selectWeightPallet(palletOptions) : null
|
||||
if (!pallet) {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url), 'utf8')
|
||||
|
||||
const controller = read('../../firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java')
|
||||
const service = read('../../firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java')
|
||||
const warehouseClient = read('../../firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePalletLocationClient.java')
|
||||
const openapi = read('../../firstleg/src/main/resources/openapi/firstleg-v1.yaml')
|
||||
const adminApi = read('../../../apps/admin-web/src/modules/inbounds/weight/api/weightOcr.ts')
|
||||
const adminPanel = read('../../../apps/admin-web/src/modules/inbounds/weight/components/WeightOcrPanel.vue')
|
||||
const adminBehavior = read('../../../apps/admin-web/src/modules/inbounds/weight/components/WeightOcrPanel.behavior.test.ts')
|
||||
|
||||
function endpointBlock(source, marker, nextMarker) {
|
||||
const start = source.indexOf(marker)
|
||||
assert.notEqual(start, -1, `missing contract marker: ${marker}`)
|
||||
const end = source.indexOf(nextMarker, start + marker.length)
|
||||
return source.slice(start, end === -1 ? source.length : end)
|
||||
}
|
||||
|
||||
test('backend Controller exposes a keyword-filtered bounded page instead of an unbounded array', () => {
|
||||
const block = endpointBlock(controller, '@GetMapping("/weight-pallet-options")', '@BusinessAudit')
|
||||
assert.match(block, /ApiResponse<PageResult<InboundMeasurementResponses\.PalletSnapshot>>/)
|
||||
assert.doesNotMatch(block, /ApiResponse<List<InboundMeasurementResponses\.PalletSnapshot>>/)
|
||||
assert.match(block, /\bpage\b/)
|
||||
assert.match(block, /\bsize\b/)
|
||||
assert.match(block, /\bkeyword\b/)
|
||||
|
||||
const boundedSize = [...block.matchAll(/@Max\((\d+)\)/g)].map((match) => Number(match[1]))
|
||||
assert.ok(boundedSize.some((maximum) => maximum > 0 && maximum <= 200),
|
||||
'size must have a finite Controller maximum no greater than 200')
|
||||
})
|
||||
|
||||
test('Firstleg forwards the bound to Warehouse instead of loading the full warehouse and slicing in memory', () => {
|
||||
const clientBlock = endpointBlock(warehouseClient, '@GetMapping("/pallets/snapshots")', '@PostMapping')
|
||||
assert.match(clientBlock, /ApiResponse<PageResult<PalletSnapshot>>/)
|
||||
assert.doesNotMatch(clientBlock, /ApiResponse<List<PalletSnapshot>>/)
|
||||
for (const parameter of ['page', 'size', 'keyword']) assert.match(clientBlock, new RegExp(`"${parameter}"`))
|
||||
|
||||
const serviceBlock = endpointBlock(service,
|
||||
'public PageResult<InboundMeasurementResponses.PalletSnapshot> weightPalletOptions(',
|
||||
'@Transactional')
|
||||
assert.match(serviceBlock, /loadPalletSnapshotPage\(inbound, page, size, keyword\)/)
|
||||
assert.match(service, /palletSnapshots\([\s\S]{0,300}?page[\s\S]{0,80}?size[\s\S]{0,80}?keyword/)
|
||||
assert.doesNotMatch(serviceBlock, /\.stream\(\)[\s\S]*?\.skip\(/)
|
||||
})
|
||||
|
||||
test('OpenAPI publishes page, size, keyword and a finite maximum without an array data contract', () => {
|
||||
const block = endpointBlock(openapi,
|
||||
' /api/platform/v1/first-leg/inbounds/{inboundId}/weight-pallet-options:',
|
||||
' /api/platform/v1/first-leg/inbounds/{inboundId}/weight-results:')
|
||||
for (const parameter of ['page', 'size', 'keyword']) {
|
||||
assert.match(block, new RegExp(`name: ${parameter}`))
|
||||
}
|
||||
const maxima = [...block.matchAll(/maximum:\s*(\d+)/g)].map((match) => Number(match[1]))
|
||||
assert.ok(maxima.some((maximum) => maximum > 0 && maximum <= 200),
|
||||
'OpenAPI size parameter must declare a finite maximum no greater than 200')
|
||||
const dataLine = block.match(/data:\s*\{[^\n]+/)?.[0] || ''
|
||||
assert.doesNotMatch(dataLine, /type:\s*array/)
|
||||
assert.match(block, /(?:records|WeightPalletOptionPage|PageResult)/)
|
||||
})
|
||||
|
||||
test('Admin API requests a bounded remote-search page and does not type the response as an array', () => {
|
||||
const block = endpointBlock(adminApi, 'export async function getWeightPalletOptions', 'export async function uploadWeightImage')
|
||||
assert.match(block, /\bpage\b/)
|
||||
assert.match(block, /\bsize\b/)
|
||||
assert.match(block, /\bkeyword\b/)
|
||||
assert.doesNotMatch(block, /ApiEnvelope<WeightPalletOption\[\]>/)
|
||||
assert.match(block, /(?:records|PageResult|PageResource|PageResponse)/)
|
||||
})
|
||||
|
||||
test('Admin skips candidates without weight edit permission or an editable inbound state', () => {
|
||||
const loader = endpointBlock(adminPanel, 'async function loadPalletOptions', 'function chooseFile')
|
||||
const gate = endpointBlock(adminPanel, 'const canLoadPalletOptions', 'const virtualPalletOptions')
|
||||
assert.match(gate, /canEdit\.value/)
|
||||
assert.match(gate, /editable\.value/)
|
||||
assert.match(loader, /!canLoadPalletOptions\.value/)
|
||||
})
|
||||
|
||||
test('Admin behavior tests prove both permission and inbound-state request suppression', () => {
|
||||
const cases = [...adminBehavior.matchAll(/it\((['"`])([\s\S]*?)\1,\s*async\s*\(\)\s*=>\s*\{([\s\S]*?)\n\s*\}\)/g)]
|
||||
const suppressionCases = cases.filter(([, , title, body]) => /getWeightPalletOptions/.test(body)
|
||||
&& /not\.toHaveBeenCalled/.test(body))
|
||||
assert.ok(suppressionCases.some(([, , title]) => /edit|permission|权限/i.test(title)),
|
||||
'missing behavior case: no weight:edit must not request pallet candidates')
|
||||
assert.ok(suppressionCases.some(([, , title]) => /status|editable|状态/i.test(title)),
|
||||
'missing behavior case: non-editable inbound status must not request pallet candidates')
|
||||
})
|
||||
|
||||
test('Admin remote search is bounded and never accumulates every candidate', () => {
|
||||
assert.match(adminPanel, /\bremote\b/)
|
||||
assert.match(adminPanel, /:remote-method=/)
|
||||
const sizes = [...adminPanel.matchAll(/(?:PALLET|pallet).{0,40}(?:PAGE_SIZE|pageSize)\s*=\s*(\d+)/g)]
|
||||
.map((match) => Number(match[1]))
|
||||
assert.ok(sizes.some((size) => size > 0 && size <= 200),
|
||||
'Admin remote search page size must be finite and no greater than 200')
|
||||
assert.doesNotMatch(adminPanel, /palletOptions\.value\.(?:push|unshift)\s*\(/)
|
||||
assert.doesNotMatch(adminPanel, /palletOptions\.value\s*=\s*\[\s*\.\.\.palletOptions\.value/)
|
||||
assert.match(adminBehavior, /(?:remote search|远程搜索|keyword).{0,220}(?:size|page)/is)
|
||||
})
|
||||
|
||||
test('Admin uses a virtualized selector so a large response cannot create one DOM option per candidate', () => {
|
||||
assert.match(adminPanel, /<el-select-v2\b/)
|
||||
assert.doesNotMatch(adminPanel, /<el-option[\s\S]*?v-for="option in palletOptions"/)
|
||||
})
|
||||
|
||||
test('historical pallet snapshot remains read-only visible when it is no longer eligible', () => {
|
||||
const cards = endpointBlock(adminPanel, '<article v-for="(row, index) in rows"', '</article>')
|
||||
assert.match(cards, /row\.palletCode/)
|
||||
assert.match(cards, /row\.palletType/)
|
||||
assert.match(cards, /row\.palletTareWeightKg/)
|
||||
assert.match(adminBehavior, /(?:historical|history|历史|失去候选|no longer eligible).{0,220}(?:palletCode|托盘编码|TP-)/is)
|
||||
})
|
||||
+14
-6
@@ -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 }
|
||||
|
||||
+9
-5
@@ -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,
|
||||
|
||||
+25
-3
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user