优化
This commit is contained in:
@@ -170,10 +170,7 @@ public class ContainerService {
|
||||
}
|
||||
return required(id);
|
||||
}
|
||||
ContainerBookingEntity current = repository.findForUpdate(id);
|
||||
if (current == null || !"ACTIVE".equals(current.getRecordStatus())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "货柜不存在");
|
||||
}
|
||||
ContainerBookingEntity current = required(id);
|
||||
requireVersion(current, request.version());
|
||||
if (status(current) == SmartLoadingStatus.COMPLETED) throw conflict("装柜完成后货柜全部只读,不能修改");
|
||||
replay = transportHistories == null ? null
|
||||
|
||||
@@ -317,17 +317,27 @@ public class SmartLoadingService {
|
||||
List<LoadingPlanItemEntity> others = new ArrayList<>(repository.items(planId));
|
||||
others.removeIf(x -> Objects.equals(x.getId(), itemId));
|
||||
checkRouteAndCapacity(booking, others, candidate, request.packageCount());
|
||||
String before = json(adjustSnapshot(item));
|
||||
LoadingPlanItemEntity desired = new LoadingPlanItemEntity();
|
||||
desired.setPackageCount(request.packageCount());
|
||||
desired.setGrossWeightKg(unit(candidate.unitGrossWeightKg()).multiply(BigDecimal.valueOf(request.packageCount())));
|
||||
desired.setNetWeightKg(unit(candidate.unitNetWeightKg()).multiply(BigDecimal.valueOf(request.packageCount())));
|
||||
desired.setVolumeCbm(unit(candidate.unitVolumeCbm()).multiply(BigDecimal.valueOf(request.packageCount())));
|
||||
desired.setPalletCount(estimatedPalletCount(candidate, request.packageCount()));
|
||||
applyCargo(desired, cargo, request.packageCount());
|
||||
if (sameAdjustSnapshot(item, desired)) {
|
||||
return view(plan, booking);
|
||||
}
|
||||
touch(plan, operatorId, operatorName);
|
||||
item.setPackageCount(request.packageCount());
|
||||
item.setGrossWeightKg(unit(candidate.unitGrossWeightKg()).multiply(BigDecimal.valueOf(request.packageCount())));
|
||||
item.setNetWeightKg(unit(candidate.unitNetWeightKg()).multiply(BigDecimal.valueOf(request.packageCount())));
|
||||
item.setVolumeCbm(unit(candidate.unitVolumeCbm()).multiply(BigDecimal.valueOf(request.packageCount())));
|
||||
item.setPalletCount(estimatedPalletCount(candidate, request.packageCount())); applyCargo(item, cargo, request.packageCount());
|
||||
item.setPackageCount(desired.getPackageCount());
|
||||
item.setGrossWeightKg(desired.getGrossWeightKg());
|
||||
item.setNetWeightKg(desired.getNetWeightKg());
|
||||
item.setVolumeCbm(desired.getVolumeCbm());
|
||||
item.setPalletCount(desired.getPalletCount()); applyCargo(item, cargo, request.packageCount());
|
||||
item.setUpdatedBy(operatorId); item.setUpdatedByName(operatorName);
|
||||
if (repository.update(item) != 1) throw conflict("装柜明细已被并发修改");
|
||||
history(planId, itemId, "ADJUST_ITEM", plan.getPlanStatus(), plan.getPlanStatus(), operation,
|
||||
json(values("version", request.expectedPlanVersion(), "packageCount", item.getPackageCount())),
|
||||
json(values("requestHash", requestHash, "packageCount", request.packageCount())),
|
||||
before, json(values("requestHash", requestHash, "business", adjustSnapshot(item))),
|
||||
operatorId, operatorName);
|
||||
loadingPickService.sync(planId, item);
|
||||
return view(repository.plan(planId), requireBooking(plan.getContainerBookingId()));
|
||||
@@ -483,6 +493,33 @@ public class SmartLoadingService {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean sameAdjustSnapshot(LoadingPlanItemEntity current, LoadingPlanItemEntity desired) {
|
||||
return Objects.equals(current.getPackageCount(), desired.getPackageCount())
|
||||
&& decimalEquals(current.getGrossWeightKg(), desired.getGrossWeightKg())
|
||||
&& decimalEquals(current.getNetWeightKg(), desired.getNetWeightKg())
|
||||
&& decimalEquals(current.getVolumeCbm(), desired.getVolumeCbm())
|
||||
&& Objects.equals(current.getPalletCount(), desired.getPalletCount())
|
||||
&& Objects.equals(current.getSkuCode(), desired.getSkuCode())
|
||||
&& Objects.equals(current.getProductNameEn(), desired.getProductNameEn())
|
||||
&& Objects.equals(current.getUnitsPerCarton(), desired.getUnitsPerCarton())
|
||||
&& Objects.equals(current.getProductQuantity(), desired.getProductQuantity())
|
||||
&& Objects.equals(current.getCustomerRemark(), desired.getCustomerRemark())
|
||||
&& Objects.equals(current.getInternalRemark(), desired.getInternalRemark());
|
||||
}
|
||||
|
||||
private static boolean decimalEquals(BigDecimal left, BigDecimal right) {
|
||||
return left == null ? right == null : right != null && left.compareTo(right) == 0;
|
||||
}
|
||||
|
||||
private static Map<String, Object> adjustSnapshot(LoadingPlanItemEntity item) {
|
||||
return values("packageCount", item.getPackageCount(), "grossWeightKg", item.getGrossWeightKg(),
|
||||
"netWeightKg", item.getNetWeightKg(), "volumeCbm", item.getVolumeCbm(),
|
||||
"palletCount", item.getPalletCount(), "skuCode", item.getSkuCode(),
|
||||
"productNameEn", item.getProductNameEn(), "unitsPerCarton", item.getUnitsPerCarton(),
|
||||
"productQuantity", item.getProductQuantity(), "customerRemark", item.getCustomerRemark(),
|
||||
"internalRemark", item.getInternalRemark(), "version", item.getVersion());
|
||||
}
|
||||
|
||||
private OrderCargoClient.CargoBinding loadCargo(Candidate candidate) {
|
||||
try {
|
||||
ApiResponse<OrderCargoClient.CargoBinding> response = orderCargoClient.detail(internalToken,
|
||||
|
||||
@@ -330,9 +330,10 @@ paths:
|
||||
/api/platform/v1/containers/{id}/transport:
|
||||
patch:
|
||||
summary: 修改集装箱号和航次
|
||||
description: 需要 platform:firstleg:container:edit 权限;LOADING 仅开放此白名单,COMPLETED 全部只读
|
||||
description: 需要 platform:firstleg:container:edit 权限;LOADING 仅开放此白名单,COMPLETED 全部只读;相同幂等键精确重放零新增历史,相同业务字段为无效变更且不递增版本、不写历史
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/ContainerBookingId'}
|
||||
- {$ref: '#/components/parameters/IdempotencyKey'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
|
||||
@@ -154,12 +154,63 @@ class ContainerServiceTest {
|
||||
when(repository.updateTransport(11L, "MSCU1234567", "V002", 2, 9L, "admin"))
|
||||
.thenReturn(1);
|
||||
|
||||
service.updateTransport(11L, 9L, "admin",
|
||||
service.updateTransport(11L, 9L, "admin", "transport-1",
|
||||
new TransportUpdateRequest("MSCU1234567", "V002", 2));
|
||||
}
|
||||
|
||||
verify(repository, org.mockito.Mockito.times(1))
|
||||
.updateTransport(11L, "MSCU1234567", "V002", 2, 9L, "admin");
|
||||
ArgumentCaptor<ContainerTransportHistoryEntity> history =
|
||||
ArgumentCaptor.forClass(ContainerTransportHistoryEntity.class);
|
||||
verify(transportHistories).insert(history.capture());
|
||||
assertThat(history.getValue().getBeforeContainerNo()).isEqualTo("MSCU1234567");
|
||||
assertThat(history.getValue().getAfterVoyageNo()).isEqualTo("V002");
|
||||
assertThat(history.getValue().getOperationId()).hasSize(64);
|
||||
assertThat(history.getValue().getRequestHash()).hasSize(64);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运输字段没有有效变化时不递增版本且不写历史")
|
||||
void ignoresTransportNoOp() {
|
||||
ContainerBookingEntity current = booking("LOADING", 2);
|
||||
current.setVoyageNo("V002");
|
||||
when(repository.find(11L)).thenReturn(current);
|
||||
|
||||
ContainerBookingEntity result = service.updateTransport(11L, 9L, "admin", "transport-noop",
|
||||
new TransportUpdateRequest(" MSCU1234567 ", " V002 ", 2));
|
||||
|
||||
assertThat(result).isSameAs(current);
|
||||
verify(repository, never()).updateTransport(anyLong(), any(), any(), anyInt(), anyLong(), any());
|
||||
verify(transportHistories, never()).insert(any(ContainerTransportHistoryEntity.class));
|
||||
verifyNoInteractions(loadingPickService);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运输字段精确幂等重放零新增")
|
||||
void replaysTransportWithoutSecondMutationOrHistory() {
|
||||
ContainerBookingEntity current = booking("LOADING", 2);
|
||||
ContainerBookingEntity updated = booking("LOADING", 3);
|
||||
updated.setVoyageNo("V002");
|
||||
when(repository.find(11L)).thenReturn(current, updated, updated);
|
||||
when(repository.updateTransport(11L, "MSCU1234567", "V002", 2, 9L, "admin"))
|
||||
.thenReturn(1);
|
||||
ArgumentCaptor<ContainerTransportHistoryEntity> history =
|
||||
ArgumentCaptor.forClass(ContainerTransportHistoryEntity.class);
|
||||
|
||||
TransportUpdateRequest request = new TransportUpdateRequest("MSCU1234567", "V002", 2);
|
||||
service.updateTransport(11L, 9L, "admin", "transport-replay", request);
|
||||
verify(transportHistories).insert(history.capture());
|
||||
when(transportHistories.selectOperation(eq(11L), eq(9L), anyString()))
|
||||
.thenReturn(history.getValue());
|
||||
|
||||
ContainerBookingEntity replay = service.updateTransport(
|
||||
11L, 9L, "admin", "transport-replay", request);
|
||||
|
||||
assertThat(replay.getVersion()).isEqualTo(3);
|
||||
verify(repository, org.mockito.Mockito.times(1))
|
||||
.updateTransport(11L, "MSCU1234567", "V002", 2, 9L, "admin");
|
||||
verify(transportHistories, org.mockito.Mockito.times(1))
|
||||
.insert(any(ContainerTransportHistoryEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -175,7 +226,7 @@ class ContainerServiceTest {
|
||||
.thenReturn(1);
|
||||
when(smartLoadingRepository.planByBooking(11L)).thenReturn(plan);
|
||||
|
||||
service.updateTransport(11L, 9L, "admin",
|
||||
service.updateTransport(11L, 9L, "admin", "transport-2",
|
||||
new TransportUpdateRequest("MSCU1234567", "V002", 2));
|
||||
|
||||
verify(loadingPickService).beginIfReady(11L, 9L, "admin");
|
||||
@@ -195,7 +246,7 @@ class ContainerServiceTest {
|
||||
when(smartLoadingRepository.planByBooking(11L)).thenReturn(plan);
|
||||
when(loadingPickRepository.taskByPlan(31L)).thenReturn(new LoadingPickTaskEntity());
|
||||
|
||||
service.updateTransport(11L, 9L, "admin",
|
||||
service.updateTransport(11L, 9L, "admin", "transport-3",
|
||||
new TransportUpdateRequest("MSCU7654321", "V002", 2));
|
||||
|
||||
verify(loadingPickService).beginIfReady(11L, 9L, "admin");
|
||||
@@ -211,7 +262,7 @@ class ContainerServiceTest {
|
||||
when(smartLoadingRepository.planByBooking(11L)).thenReturn(plan);
|
||||
when(loadingPickRepository.taskByPlan(31L)).thenReturn(new LoadingPickTaskEntity());
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(11L, 9L, "admin",
|
||||
assertThatThrownBy(() -> service.updateTransport(11L, 9L, "admin", "transport-4",
|
||||
new TransportUpdateRequest(null, "V002", 2)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("不能清空真实集装箱号");
|
||||
@@ -225,7 +276,7 @@ class ContainerServiceTest {
|
||||
void rejectsConfirmedUpdate() {
|
||||
when(repository.find(11L)).thenReturn(booking("COMPLETED", 2));
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(11L, 9L, "admin",
|
||||
assertThatThrownBy(() -> service.updateTransport(11L, 9L, "admin", "transport-5",
|
||||
new TransportUpdateRequest("MSCU1234567", "V002", 2)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("全部只读");
|
||||
|
||||
@@ -42,6 +42,7 @@ import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@DisplayName("预装与装柜清单状态服务")
|
||||
@@ -102,6 +103,25 @@ class LoadingListServiceTest {
|
||||
eq("operator"), eq("补录箱号"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotWritePlanOrHistoryWhenTransportFieldsAreUnchanged() {
|
||||
AggregateRow before = row("LOADING", 3);
|
||||
before.setContainerNo("CSLU1234567");
|
||||
before.setVoyageNo("023E");
|
||||
when(repository.aggregate(10L)).thenReturn(before);
|
||||
when(repository.preloadDetail(before)).thenReturn(
|
||||
new com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail(null, List.of(), List.of()));
|
||||
|
||||
service.updateTransport(10L, 100L, 9L, "operator", "noop-key",
|
||||
new TransportUpdateRequest(" CSLU1234567 ", " 023E ", 3, "重复保存"));
|
||||
|
||||
verify(repository, never()).updateTransport(anyLong(), org.mockito.ArgumentMatchers.anyInt(),
|
||||
any(), any(), anyLong(), any());
|
||||
verify(repository, never()).appendHistory(anyLong(), anyString(), anyString(), any(), any(),
|
||||
any(), any(), anyLong(), any(), any());
|
||||
verifyNoInteractions(loadingPickService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushedEditingPreloadCanReceiveContainerNumber() {
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
|
||||
@@ -503,6 +503,26 @@ class SmartLoadingServiceTest {
|
||||
verify(repository).updateWarehouseRemark(1L, 8L, 2, 0, "待核对外箱", 7L, "u");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unchangedWarehouseRemarkDoesNotTouchPlanItemOrHistory() {
|
||||
LoadingPlanEntity plan = plan("LOADING", 2);
|
||||
LoadingPlanItemEntity line = item(8, 9, 20, 1);
|
||||
line.setWarehouseRemark("待核对外箱");
|
||||
when(repository.plan(1L)).thenReturn(plan);
|
||||
when(repository.item(1L, 8L)).thenReturn(line);
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.items(1L)).thenReturn(List.of(line));
|
||||
|
||||
Plan result = service.updateWarehouseRemark(1L, 8L,
|
||||
new WarehouseRemarkRequest(" 待核对外箱 ", 2, 0, "warehouse-noop"), 7L, "u");
|
||||
|
||||
assertThat(result.version()).isEqualTo(2);
|
||||
verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), anyString());
|
||||
verify(repository, never()).updateWarehouseRemark(anyLong(), anyLong(), anyInt(), anyInt(),
|
||||
any(), anyLong(), anyString());
|
||||
verify(repository, never()).insert(any(LoadingPlanHistoryEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void warehouseRemarkConflictProducesNoHistory() {
|
||||
LoadingPlanEntity plan = plan("LOADING", 2);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import { isPureStatusNoop, visibleLoadingHistories } from './loading-history-noop.mjs'
|
||||
|
||||
const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url), 'utf8')
|
||||
const firstlegServiceDirectory = new URL('../../firstleg/src/main/java/com/baiyue/firstleg/service/', import.meta.url)
|
||||
const serviceSources = readdirSync(firstlegServiceDirectory)
|
||||
.filter((name) => name.endsWith('.java'))
|
||||
.map((name) => read(`../../firstleg/src/main/java/com/baiyue/firstleg/service/${name}`)).join('\n')
|
||||
const smartLoading = read('../../firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java')
|
||||
const smartLoadingTest = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java')
|
||||
const loadingDtos = read('../../firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java')
|
||||
const loadingRepository = read('../../firstleg/src/main/java/com/baiyue/firstleg/repository/LoadingListRepository.java')
|
||||
const openapi = read('../../firstleg/src/main/resources/openapi/loading-lists-v1.yaml')
|
||||
const adminApi = read('../../../apps/admin-web/src/modules/loading-lists/api/platformLoadingLists.ts')
|
||||
const adminDrawer = read('../../../apps/admin-web/src/modules/loading-lists/components/LoadingListDetailDrawer.vue')
|
||||
const adminTests = read('../../../apps/admin-web/src/modules/loading-lists/components/LoadingListsView.test.ts')
|
||||
|
||||
test('reference projection removes only pure STATUS_UPDATE no-ops', () => {
|
||||
assert.equal(isPureStatusNoop({ actionType: 'STATUS_UPDATE', fromStatus: 'EDITING', toStatus: 'EDITING',
|
||||
beforeSnapshot: '{"status":"EDITING","version":1}', afterSnapshot: '{"version":2,"status":"EDITING"}' }), true)
|
||||
assert.equal(isPureStatusNoop({ actionType: 'STATUS_UPDATE', fromStatus: 'EDITING', toStatus: 'LOADING' }), false)
|
||||
assert.equal(isPureStatusNoop({ actionType: 'STATUS_UPDATE', fromStatus: 'EDITING', toStatus: 'EDITING',
|
||||
beforeSnapshot: '{"containerNo":null}', afterSnapshot: '{"containerNo":"MSCU1"}' }), false)
|
||||
assert.equal(isPureStatusNoop({ actionType: 'PUSH_PRELOAD', fromStatus: 'EDITING', toStatus: 'EDITING' }), false)
|
||||
})
|
||||
|
||||
test('deduplication uses stable event identity and never identical display text', () => {
|
||||
const rows = visibleLoadingHistories([
|
||||
{ historyId: '1', operationId: 'push-1', actionType: 'PUSH_PRELOAD', operationReason: '推送预装' },
|
||||
{ historyId: '2', operationId: 'push-1', actionType: 'PUSH_PRELOAD', operationReason: '推送预装' },
|
||||
{ historyId: '3', operationId: 'push-2', actionType: 'PUSH_PRELOAD', operationReason: '推送预装' },
|
||||
{ historyId: '4', actionType: 'ADJUST_ITEM', operationReason: '相同文案' },
|
||||
{ historyId: '5', actionType: 'ADJUST_ITEM', operationReason: '相同文案' },
|
||||
])
|
||||
assert.deepEqual(rows.map((row) => row.historyId), ['1', '3', '4', '5'])
|
||||
})
|
||||
|
||||
test('backend never appends a generic STATUS_UPDATE and push replay is read-only', () => {
|
||||
assert.doesNotMatch(serviceSources, /(?:history|appendHistory)\([^;]{0,500}"STATUS_UPDATE"/s)
|
||||
assert.match(smartLoading, /if \(key\.equals\(plan\.getPushIdempotencyKey\(\)\)\)[\s\S]{0,300}return view\(plan, booking\)/)
|
||||
assert.match(smartLoading, /history\(planId, null, "PUSH_PRELOAD"/)
|
||||
assert.match(smartLoadingTest, /exactPushReplayIsReadOnly[\s\S]{0,1000}never\(\)\.insert/)
|
||||
assert.match(smartLoadingTest, /getActionType\(\)\)\.isEqualTo\("PUSH_PRELOAD"\)/)
|
||||
})
|
||||
|
||||
test('operationId is projected end-to-end for identity-based frontend deduplication', () => {
|
||||
assert.match(loadingDtos, /record History\([\s\S]{0,180}String operationId/)
|
||||
assert.match(loadingRepository, /new History\([\s\S]{0,180}getOperationId\(\)/)
|
||||
assert.match(openapi, /OperationHistory:[\s\S]{0,500}operationId:/)
|
||||
assert.match(adminApi, /interface LoadingHistory[\s\S]{0,260}operationId:\s*string/)
|
||||
assert.match(adminApi, /normalizeHistory[\s\S]{0,260}operationId:/)
|
||||
})
|
||||
|
||||
test('Admin counts and renders the filtered history projection', () => {
|
||||
assert.match(adminDrawer, /(?:visible|effective|filtered)Histories/)
|
||||
assert.doesNotMatch(adminDrawer, /detail\.histories\.length\s*\}\}\s*条/)
|
||||
assert.doesNotMatch(adminDrawer, /v-for="history in detail\.histories"/)
|
||||
assert.match(adminDrawer, /STATUS_UPDATE/)
|
||||
assert.match(adminDrawer, /operationId|eventId/)
|
||||
})
|
||||
|
||||
test('Admin behavior contract covers no-op, identity duplicate, count and same-text distinct events', () => {
|
||||
for (const marker of ['STATUS_UPDATE', 'operationId', 'PUSH_PRELOAD']) assert.match(adminTests, new RegExp(marker))
|
||||
assert.match(adminTests, /(?:same text|same wording|相同文案|同文案)/i)
|
||||
assert.match(adminTests, /(?:count|条)/i)
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
const VOLATILE_SNAPSHOT_FIELDS = new Set([
|
||||
'requestHash', 'version', 'updatedAt', 'operatorId', 'operatorName', 'occurredAt',
|
||||
])
|
||||
|
||||
function stableValue(value) {
|
||||
if (Array.isArray(value)) return value.map(stableValue)
|
||||
if (!value || typeof value !== 'object') return value
|
||||
return Object.fromEntries(Object.entries(value)
|
||||
.filter(([key]) => !VOLATILE_SNAPSHOT_FIELDS.has(key))
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, nested]) => [key, stableValue(nested)]))
|
||||
}
|
||||
|
||||
function snapshot(value) {
|
||||
if (value == null || value === '') return null
|
||||
try { return stableValue(typeof value === 'string' ? JSON.parse(value) : value) } catch { return value }
|
||||
}
|
||||
|
||||
function sameSnapshot(left, right) {
|
||||
return JSON.stringify(snapshot(left)) === JSON.stringify(snapshot(right))
|
||||
}
|
||||
|
||||
export function isPureStatusNoop(history) {
|
||||
if (history?.actionType !== 'STATUS_UPDATE') return false
|
||||
if ((history?.fromStatus ?? null) !== (history?.toStatus ?? null)) return false
|
||||
return sameSnapshot(history?.beforeSnapshot, history?.afterSnapshot)
|
||||
}
|
||||
|
||||
function stableEventKey(history) {
|
||||
if (history?.operationId) return `operation:${history.operationId}`
|
||||
if (history?.eventId) return `event:${history.eventId}`
|
||||
if (history?.historyId) return `history:${history.historyId}`
|
||||
return null
|
||||
}
|
||||
|
||||
export function visibleLoadingHistories(histories) {
|
||||
const seen = new Set()
|
||||
return (Array.isArray(histories) ? histories : []).filter((history) => {
|
||||
if (isPureStatusNoop(history)) return false
|
||||
const key = stableEventKey(history)
|
||||
if (!key) return true
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs'
|
||||
import { isPureStatusNoop, visibleLoadingHistories } from './loading-history-noop.mjs'
|
||||
import { createHash } from 'node:crypto'
|
||||
|
||||
const MODULE = 'phase3-loading'
|
||||
@@ -319,6 +320,12 @@ export async function run(context) {
|
||||
const pushedDetailResponse = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const pushedDetail = context.apiSuccess(MODULE, '无号推送后预装清单正式可见', pushedDetailResponse, '', OPTIONS)
|
||||
const pushedHistories = visibleLoadingHistories(pushedDetail?.histories)
|
||||
context.assert(MODULE, '首次 push 只保留一条 PUSH_PRELOAD 且无纯状态 no-op', pushedDetailResponse,
|
||||
pushedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1
|
||||
&& !(pushedDetail?.histories || []).some(isPureStatusNoop),
|
||||
`raw=${pushedDetail?.histories?.length ?? 'invalid'}, effective=${pushedHistories.length}, push=${pushedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length}`,
|
||||
OPTIONS)
|
||||
const pushedLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
|
||||
const pushedLinks = context.apiSuccess(MODULE, '无号推送自动建立 bridge 关联', pushedLinksResponse, '', OPTIONS)
|
||||
const link = Array.isArray(pushedLinks) ? pushedLinks.find((item) => id(item.inboundId) === inboundId) : null
|
||||
@@ -353,6 +360,12 @@ export async function run(context) {
|
||||
const replayDetailResponse = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const replayDetail = context.apiSuccess(MODULE, '重放后回查预装历史', replayDetailResponse, '', OPTIONS)
|
||||
const replayHistories = visibleLoadingHistories(replayDetail?.histories)
|
||||
context.assert(MODULE, 'push 同幂等请求重放历史零新增', replayDetailResponse,
|
||||
replayHistories.length === pushedHistories.length
|
||||
&& replayHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1,
|
||||
`before=${pushedHistories.length}, after=${replayHistories.length}, push=${replayHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length}`,
|
||||
OPTIONS)
|
||||
const replayLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
|
||||
const replayLinks = context.apiSuccess(MODULE, '重放后回查 bridge', replayLinksResponse, '', OPTIONS)
|
||||
const replayTaskResponse = await context.request('GET',
|
||||
@@ -380,6 +393,7 @@ export async function run(context) {
|
||||
const rejectedDetailResponse = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const rejectedDetail = context.apiSuccess(MODULE, '非法 push 后回查预装历史', rejectedDetailResponse, '', OPTIONS)
|
||||
const rejectedHistories = visibleLoadingHistories(rejectedDetail?.histories)
|
||||
const rejectedLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
|
||||
const rejectedLinks = context.apiSuccess(MODULE, '非法 push 后回查 bridge', rejectedLinksResponse, '', OPTIONS)
|
||||
const rejectedTaskResponse = await context.request('GET',
|
||||
@@ -391,7 +405,9 @@ export async function run(context) {
|
||||
const rejectedDiff = diffPushPreloadStability(replayBaseline, rejectedSnapshot)
|
||||
context.assert(MODULE, '两类非法 push 后计划版本历史关联和任务仍零漂移', rejectedTaskResponse,
|
||||
failure(sameKeyDifferentBody) && failure(newKeyDuplicate)
|
||||
&& rejectedDiff.length === 0 && codeOf(rejectedTaskResponse) === 'A0404',
|
||||
&& rejectedDiff.length === 0 && codeOf(rejectedTaskResponse) === 'A0404'
|
||||
&& rejectedHistories.length === pushedHistories.length
|
||||
&& rejectedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1,
|
||||
`changed=${rejectedDiff.join('|') || 'none'}; sameKey=${responseEvidence(sameKeyDifferentBody)}; newKey=${responseEvidence(newKeyDuplicate)}; task=${responseEvidence(rejectedTaskResponse)}`, OPTIONS)
|
||||
if (rejectedDiff.length !== 0 || codeOf(rejectedTaskResponse) !== 'A0404') return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user