fix(firstleg): enforce strict manual node progression

This commit is contained in:
2026-08-26 15:44:05 +08:00
parent 2ebea9e789
commit 57a2f6ef28
5 changed files with 22 additions and 60 deletions
@@ -98,7 +98,7 @@ public class PlatformCarrierScheduleQueryController {
@PostMapping("/{queryId}/manual-nodes")
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:manual')")
@Operation(summary = "人工补录预计或实际标准船节点",
description = "预计节点不推进实际进度;同级当前节点可追加审计事实并修正实际时间,更早节点返回 A0409。")
description = "预计节点不推进实际进度;人工实际节点必须严格晚于当前最新实际节点,同级或更早节点返回 A0409。")
@BusinessAudit(event = "CARRIER_SCHEDULE_QUERY_MANUAL_NODE", resourceType = "CARRIER_SCHEDULE_QUERY",
resourceId = "#queryId", description = "人工补录船节点事实")
public ApiResponse<Detail> manualNode(@AuthenticationPrincipal AuthenticatedUser user,
@@ -205,7 +205,7 @@ public class CarrierScheduleQueryService {
}
CarrierScheduleQueryEntity value = requiredLockedVersion(id, request.expectedVersion());
CarrierScheduleNode node = CarrierScheduleNode.require(request.standardNode());
requireManualActualNotEarlier(value, node, request.actualAt());
requireManualActualAdvance(value, node, request.actualAt());
CarrierScheduleQueryEntity before = copy(value);
String providerEventId = sha256("MANUAL|" + id + '|' + key);
CarrierScheduleNodeFactEntity fact = addNode(id, "MANUAL", providerEventId, node.name(), node,
@@ -215,7 +215,7 @@ public class CarrierScheduleQueryService {
if (fact == null) {
return complete(id, operatorId, key, detail(value));
}
boolean advanced = advanceManualActualNode(value, node, request.actualAt(), request.observedAt(),
boolean advanced = advanceActualNode(value, node, request.actualAt(), request.observedAt(),
operatorId, operatorName);
if (!advanced && repository.updateState(value, request.expectedVersion(), operatorId, operatorName) != 1) {
throw concurrent();
@@ -227,34 +227,15 @@ public class CarrierScheduleQueryService {
return complete(id, operatorId, key, detail(after));
}
private void requireManualActualNotEarlier(CarrierScheduleQueryEntity value, CarrierScheduleNode node,
LocalDateTime actualAt) {
private void requireManualActualAdvance(CarrierScheduleQueryEntity value, CarrierScheduleNode node,
LocalDateTime actualAt) {
if (actualAt == null || value.getLatestStandardNode() == null) return;
CarrierScheduleNode current = CarrierScheduleNode.require(value.getLatestStandardNode());
if (node.sequence() < current.sequence()) {
throw conflict("人工实际节点不得早于当前最新实际节点:" + current.name());
if (node.sequence() <= current.sequence()) {
throw conflict("人工实际节点必须严格晚于当前最新实际节点");
}
}
private boolean advanceManualActualNode(CarrierScheduleQueryEntity value, CarrierScheduleNode node,
LocalDateTime actualAt, LocalDateTime observedAt,
long operatorId, String operatorName) {
if (actualAt == null) return false;
CarrierScheduleNode current = value.getLatestStandardNode() == null ? null
: CarrierScheduleNode.require(value.getLatestStandardNode());
if (current == node) {
value.setLatestNodeObservedAt(actualAt); value.setLastSuccessAt(now());
value.setSourceUpdatedAt(observedAt); value.setLastAttemptStatus("SUCCESS");
value.setLastErrorCode(null); value.setLastErrorMessage(null);
if (!"STOPPED".equals(value.getStatus())) {
value.setStatus("READY"); value.setNextAllowedAt(CarrierScheduleQuerySlots.nextSlot(now()));
}
if (repository.updateState(value, value.getVersion(), operatorId, operatorName) != 1) throw concurrent();
return true;
}
return advanceActualNode(value, node, actualAt, observedAt, operatorId, operatorName);
}
private boolean advanceActualNode(CarrierScheduleQueryEntity value, CarrierScheduleNode node,
LocalDateTime actualAt, LocalDateTime observedAt,
long operatorId, String operatorName) {
@@ -82,9 +82,9 @@ paths:
schema: {$ref: '#/components/schemas/ManualNodeRequest'}
responses:
'200':
description: Idempotent fact append; estimated facts never advance actual progress; the current latestActualNode may append an audited correction fact and replace latestActualAt; a later actual node advances progress and actual EMPTY_RETURNED stops
description: Idempotent fact append; estimated facts never advance actual progress, while a manual actual fact must have a strictly greater standard-node sequence than latestActualNode and actual EMPTY_RETURNED stops
'409':
description: A0409 when expectedVersion is stale or a manual actual node is earlier than latestActualNode
description: A0409 when expectedVersion is stale or a manual actual node is equal to or earlier than latestActualNode
content: {application/json: {schema: {$ref: '#/components/schemas/DetailEnvelope'}}}
components:
parameters:
@@ -124,7 +124,7 @@ components:
ManualNodeRequest:
type: object
required: [expectedVersion, standardNode, observedAt, reason]
description: estimatedAt and actualAt cannot both be null; estimatedAt never advances the latest actual node; when actualAt is present, standardNode may equal latestActualNode to correct latestActualAt through a new immutable audit fact, but cannot be earlier.
description: estimatedAt and actualAt cannot both be null; estimatedAt never advances the latest actual node; when actualAt is present, standardNode must have a strictly greater sequence than latestActualNode.
properties:
expectedVersion: {type: integer, minimum: 0}
standardNode: {$ref: '#/components/schemas/StandardNode'}