收紧 V4 复核解阻边界

This commit is contained in:
andy
2026-07-19 11:56:12 +07:00
parent d0d26c7fbe
commit b4f1b3c856
6 changed files with 160 additions and 7 deletions

View File

@@ -161,6 +161,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
ReservationV4TaskCardSnapshot card = requireTaskCard(orderTask.hotelId(), cardId, orderTask.id());
validateCardReviewable(orderTask, card);
Long confirmedOrderId = parseConfirmedOrderId(request == null ? null : request.confirmedOrderId());
ensureReviewOrderRebindAllowed(orderTask, confirmedOrderId);
Long targetOrderId = resolveReviewTargetOrderId(orderTask, confirmedOrderId);
ensureNoPriorOrderTaskBlocking(orderTask, targetOrderId);
ensureBasicInformationConfirmed(orderTask, card);
@@ -495,7 +496,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
String pointer = trimToNull(override == null ? null : override.fieldPointer());
List<String> segments = decodeJsonPointer(pointer);
JsonNode value = override == null || override.value() == null ? objectMapper.nullNode() : override.value();
ensureReviewPointerWritable(card, confirmedPayload, segments, value);
ensureReviewPointerWritable(card, confirmedPayload, pointer, segments, value);
if (!seenPointers.add(pointer)) {
throw error(HttpStatus.BAD_REQUEST, "V4_REVIEW_POINTER_DUPLICATED", "复核字段不能重复提交。");
}
@@ -546,6 +547,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
private void ensureReviewPointerWritable(
ReservationV4TaskCardSnapshot card,
ObjectNode confirmedPayload,
String pointer,
List<String> segments,
JsonNode value) {
if (segments.isEmpty()) {
@@ -565,6 +567,9 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
if (current.isContainerNode()) {
throw error(HttpStatus.BAD_REQUEST, "V4_REVIEW_POINTER_NOT_ALLOWED", "复核字段只能指向当前卡允许编辑的叶子字段。");
}
if (!isReviewPointerAllowedForResolution(confirmedPayload, pointer, current)) {
throw error(HttpStatus.BAD_REQUEST, "V4_REVIEW_POINTER_NOT_ALLOWED", "复核字段不在当前卡允许编辑字段内。");
}
}
private void ensureReviewPointerInsideEditableContainer(
@@ -655,6 +660,69 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
}
}
/**
* 判断复核字段是否属于本次允许修正的字段范围,优先使用显式缺失字段清单。
*/
private boolean isReviewPointerAllowedForResolution(
ObjectNode confirmedPayload,
String pointer,
JsonNode current) {
Set<String> explicitPointers = collectExplicitReviewFieldPointers(confirmedPayload);
if (!explicitPointers.isEmpty()) {
return explicitPointers.contains(pointer);
}
return isUnresolvedReviewLeaf(current);
}
/**
* 从当前卡展示 payload 中收集兼容用的 missing_fields 指针清单。
*/
private Set<String> collectExplicitReviewFieldPointers(JsonNode node) {
Set<String> pointers = new HashSet<>();
collectExplicitReviewFieldPointers(node, pointers);
return pointers;
}
/**
* 递归收集嵌套对象里的 missing_fields避免前端只能处理某一种历史摆放位置。
*/
private void collectExplicitReviewFieldPointers(JsonNode node, Set<String> pointers) {
if (node == null || node.isMissingNode()) {
return;
}
if (node.isObject()) {
JsonNode missingFields = node.get("missing_fields");
if (missingFields != null && missingFields.isArray()) {
for (JsonNode missingField : missingFields) {
String pointer = trimToNull(missingField == null ? null : missingField.asText());
if (pointer != null && pointer.startsWith("/")) {
pointers.add(pointer);
}
}
}
Iterator<Map.Entry<String, JsonNode>> fields = node.fields();
while (fields.hasNext()) {
collectExplicitReviewFieldPointers(fields.next().getValue(), pointers);
}
return;
}
if (node.isArray()) {
for (JsonNode item : node) {
collectExplicitReviewFieldPointers(item, pointers);
}
}
}
/**
* 判断当前叶子字段是否仍是未解决值,第一版只允许修正 null 或空字符串。
*/
private boolean isUnresolvedReviewLeaf(JsonNode current) {
if (current == null || current.isNull()) {
return true;
}
return current.isTextual() && !hasText(current.asText());
}
private Long parseConfirmedOrderId(String confirmedOrderId) {
String normalized = trimToNull(confirmedOrderId);
if (normalized == null) {
@@ -671,6 +739,22 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
}
}
/**
* 阻止已确认归属的 V4 订单任务借复核接口切换到其它订单。
*/
private void ensureReviewOrderRebindAllowed(
ReservationV4OrderTaskSnapshot orderTask,
Long confirmedOrderId) {
if (confirmedOrderId == null) {
return;
}
boolean alreadyResolvedToOrder = orderTask.orderId() != null
&& ReservationV4TargetResolutionStatus.RESOLVED.name().equals(orderTask.targetResolutionStatus());
if (alreadyResolvedToOrder && !Objects.equals(orderTask.orderId(), confirmedOrderId)) {
throw error(HttpStatus.CONFLICT, "V4_ORDER_REBIND_NOT_ALLOWED", "已确认归属的 V4 订单任务不能通过复核接口切换到其他订单。");
}
}
private Long resolveReviewTargetOrderId(ReservationV4OrderTaskSnapshot orderTask, Long confirmedOrderId) {
boolean ownershipUnresolved = orderTask.orderId() == null
|| !ReservationV4TargetResolutionStatus.RESOLVED.name().equals(orderTask.targetResolutionStatus());