修复同卡复核重复字段校验
This commit is contained in:
@@ -168,6 +168,7 @@ POST /api/auth/logout
|
|||||||
```
|
```
|
||||||
|
|
||||||
- `field_pointer` 必须是 RFC 6901 JSON Pointer,并且只能指向当前任务卡可编辑字段;后端会映射到矩阵 `field_path`。非法或只读字段会返回 `TASK_REVIEW_POINTER_INVALID`。
|
- `field_pointer` 必须是 RFC 6901 JSON Pointer,并且只能指向当前任务卡可编辑字段;后端会映射到矩阵 `field_path`。非法或只读字段会返回 `TASK_REVIEW_POINTER_INVALID`。
|
||||||
|
- 同一次请求不能重复提交同一字段;重复 `field_pointer` 或重复映射到同一 `field_path` 会返回 `TASK_REVIEW_POINTER_DUPLICATE`。
|
||||||
- `confirmed_order_id` 第一版必须等于当前任务的 `order_id`;如果前端需要选择其他订单,仍属于后续“复核场景订单归属选择”细化,不要复用普通任务切换订单能力。
|
- `confirmed_order_id` 第一版必须等于当前任务的 `order_id`;如果前端需要选择其他订单,仍属于后续“复核场景订单归属选择”细化,不要复用普通任务切换订单能力。
|
||||||
- 解阻成功后返回 `task_status=READY`、`review_status=RESOLVED`、`review_resolution.field_overrides[]`、`confirmed_payload` 和两条 `opera_operations[]`。前端应刷新任务详情并显示 OPERA 模拟操作入口。
|
- 解阻成功后返回 `task_status=READY`、`review_status=RESOLVED`、`review_resolution.field_overrides[]`、`confirmed_payload` 和两条 `opera_operations[]`。前端应刷新任务详情并显示 OPERA 模拟操作入口。
|
||||||
- 解阻过程不改写 `ai_payload_json`;用户修正值保存在 `review_resolution` 和 `confirmed_payload.field_values` 中。
|
- 解阻过程不改写 `ai_payload_json`;用户修正值保存在 `review_resolution` 和 `confirmed_payload.field_values` 中。
|
||||||
|
|||||||
@@ -560,6 +560,7 @@ Content-Type: application/json
|
|||||||
前端注意:
|
前端注意:
|
||||||
|
|
||||||
- `field_pointer` 必须是 RFC 6901 JSON Pointer,并且只能指向任务详情 `fields[]` 中当前可编辑字段;只读字段或未知字段会返回 `TASK_REVIEW_POINTER_INVALID`。
|
- `field_pointer` 必须是 RFC 6901 JSON Pointer,并且只能指向任务详情 `fields[]` 中当前可编辑字段;只读字段或未知字段会返回 `TASK_REVIEW_POINTER_INVALID`。
|
||||||
|
- 同一次请求不能重复提交同一字段;重复 `field_pointer` 或重复映射到同一 `field_path` 会返回 `TASK_REVIEW_POINTER_DUPLICATE`。
|
||||||
- `confirmed_order_id` 第一版必须等于当前任务 `order_id`;普通任务任意切换订单继续后置。
|
- `confirmed_order_id` 第一版必须等于当前任务 `order_id`;普通任务任意切换订单继续后置。
|
||||||
- type-known manual review 不能调用通用 `POST /api/reservation/tasks/{taskId}/confirm`;必须调用本节解阻接口,否则后端返回 `TASK_REVIEW_RESOLUTION_REQUIRED`。
|
- type-known manual review 不能调用通用 `POST /api/reservation/tasks/{taskId}/confirm`;必须调用本节解阻接口,否则后端返回 `TASK_REVIEW_RESOLUTION_REQUIRED`。
|
||||||
- `review_resolution.resolved_at` 是 UTC `Z` 时间点。
|
- `review_resolution.resolved_at` 是 UTC `Z` 时间点。
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ Agent payload 不可变。本系统在同一张卡上维护复核状态:
|
|||||||
- `missing_fields[]` 必须是 RFC 6901 JSON Pointer。
|
- `missing_fields[]` 必须是 RFC 6901 JSON Pointer。
|
||||||
- 入站阶段 `missing_fields[]` 不完整或不是 RFC 6901 Pointer 时,按 `adapter_contract_error` fail closed,不创建业务任务。
|
- 入站阶段 `missing_fields[]` 不完整或不是 RFC 6901 Pointer 时,按 `adapter_contract_error` fail closed,不创建业务任务。
|
||||||
- 解阻接口提交的 Pointer 必须能映射到该业务卡当前可展示且可编辑字段,否则返回 `TASK_REVIEW_POINTER_INVALID`。
|
- 解阻接口提交的 Pointer 必须能映射到该业务卡当前可展示且可编辑字段,否则返回 `TASK_REVIEW_POINTER_INVALID`。
|
||||||
|
- 同一次解阻请求中 `field_overrides[]` 不允许重复指向同一 `field_pointer` 或同一矩阵 `field_path`,否则返回 `TASK_REVIEW_POINTER_DUPLICATE`。
|
||||||
- 订单归属确认可作为复核解阻的一部分保存;当前第一版只允许确认当前任务所属订单,不开放普通任务随意切换订单能力。
|
- 订单归属确认可作为复核解阻的一部分保存;当前第一版只允许确认当前任务所属订单,不开放普通任务随意切换订单能力。
|
||||||
- 全部缺失字段、订单归属、目录值和依赖校验通过后,才进入 Preflight / READY。
|
- 全部缺失字段、订单归属、目录值和依赖校验通过后,才进入 Preflight / READY。
|
||||||
|
|
||||||
|
|||||||
@@ -807,9 +807,14 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
|
|||||||
Map<String, ReservationTaskCardFieldDefinition> definitionByPath = new LinkedHashMap<>();
|
Map<String, ReservationTaskCardFieldDefinition> definitionByPath = new LinkedHashMap<>();
|
||||||
activeDefinitions.forEach(definition -> definitionByPath.put(definition.fieldPath(), definition));
|
activeDefinitions.forEach(definition -> definitionByPath.put(definition.fieldPath(), definition));
|
||||||
List<ManualReviewResolutionFieldOverrideResult> results = new ArrayList<>();
|
List<ManualReviewResolutionFieldOverrideResult> results = new ArrayList<>();
|
||||||
|
Set<String> seenPointers = new LinkedHashSet<>();
|
||||||
|
Set<String> seenFieldPaths = new LinkedHashSet<>();
|
||||||
for (ManualReviewResolutionFieldOverrideRequest request : requests) {
|
for (ManualReviewResolutionFieldOverrideRequest request : requests) {
|
||||||
String fieldPointer = trimToNull(request == null ? null : request.fieldPointer());
|
String fieldPointer = trimToNull(request == null ? null : request.fieldPointer());
|
||||||
ReservationTaskCardFieldDefinition definition = findEditableDefinitionByPointer(definitionByPath, fieldPointer);
|
ReservationTaskCardFieldDefinition definition = findEditableDefinitionByPointer(definitionByPath, fieldPointer);
|
||||||
|
if (!seenPointers.add(fieldPointer) || !seenFieldPaths.add(definition.fieldPath())) {
|
||||||
|
throw reviewPointerDuplicateError(fieldPointer, definition.fieldPath());
|
||||||
|
}
|
||||||
results.add(new ManualReviewResolutionFieldOverrideResult(
|
results.add(new ManualReviewResolutionFieldOverrideResult(
|
||||||
fieldPointer,
|
fieldPointer,
|
||||||
definition.fieldPath(),
|
definition.fieldPath(),
|
||||||
@@ -818,6 +823,17 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建重复复核字段错误,避免 confirmed payload 后值覆盖前值但审计保留多条冲突记录。
|
||||||
|
*/
|
||||||
|
private ReservationTaskWorkflowException reviewPointerDuplicateError(String fieldPointer, String fieldPath) {
|
||||||
|
return new ReservationTaskWorkflowException(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"TASK_REVIEW_POINTER_DUPLICATE",
|
||||||
|
"复核字段指针重复。",
|
||||||
|
List.of(fieldPointer + ": 重复指向字段 " + fieldPath + "。"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 复核 pointer 白名单使用当前卡型的可编辑字段全集;展示条件由合并后的 activeDefinitions 再兜底校验。
|
* 复核 pointer 白名单使用当前卡型的可编辑字段全集;展示条件由合并后的 activeDefinitions 再兜底校验。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1912,6 +1912,53 @@ class SuperAgentTaskResultControllerTest {
|
|||||||
assertThat(sourceTaskCount).isEqualTo(1L);
|
assertThat(sourceTaskCount).isEqualTo(1L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectManualReviewResolutionWhenPointerIsDuplicated() throws Exception {
|
||||||
|
captureSourceMessage("mail-v3-manual-review-duplicate-pointer-001");
|
||||||
|
String body = typeKnownManualReviewBody(
|
||||||
|
"mail-v3-manual-review-duplicate-pointer-001",
|
||||||
|
"CNF-V3-MR-DUP-001",
|
||||||
|
"2026-09-05",
|
||||||
|
"2026-09-07");
|
||||||
|
MvcResult createResult = mockMvc.perform(signedPost(body, "nonce-v3-manual-review-duplicate-pointer-001"))
|
||||||
|
.andExpect(status().isCreated())
|
||||||
|
.andReturn();
|
||||||
|
String taskId = com.jayway.jsonpath.JsonPath.read(
|
||||||
|
createResult.getResponse().getContentAsString(),
|
||||||
|
"$.items[0].task_id");
|
||||||
|
String orderId = com.jayway.jsonpath.JsonPath.read(
|
||||||
|
createResult.getResponse().getContentAsString(),
|
||||||
|
"$.items[0].order_id");
|
||||||
|
|
||||||
|
mockMvc.perform(post("/api/reservation/tasks/{taskId}/manual-review-resolutions", taskId)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("""
|
||||||
|
{
|
||||||
|
"confirmed_order_id": "%s",
|
||||||
|
"field_overrides": [
|
||||||
|
{
|
||||||
|
"field_pointer": "/extracted_fields/pms_room_type_code",
|
||||||
|
"value": "RM3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field_pointer": "/extracted_fields/pms_room_type_code",
|
||||||
|
"value": "RM4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".formatted(orderId)))
|
||||||
|
.andExpect(status().isBadRequest())
|
||||||
|
.andExpect(jsonPath("$.error_code").value("TASK_REVIEW_POINTER_DUPLICATE"))
|
||||||
|
.andExpect(jsonPath("$.details[0]").value(containsString("/extracted_fields/pms_room_type_code")));
|
||||||
|
|
||||||
|
String taskStatus = jdbcTemplate.queryForObject("""
|
||||||
|
SELECT task_status
|
||||||
|
FROM workflow_reservation_task
|
||||||
|
WHERE id = ?
|
||||||
|
""", String.class, Long.valueOf(taskId));
|
||||||
|
assertThat(taskStatus).isEqualTo("PENDING_CONFIRM");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRejectManualReviewResolutionWhenMissingFieldsAreNotResolved() throws Exception {
|
void shouldRejectManualReviewResolutionWhenMissingFieldsAreNotResolved() throws Exception {
|
||||||
captureSourceMessage("mail-v3-manual-review-missing-001");
|
captureSourceMessage("mail-v3-manual-review-missing-001");
|
||||||
|
|||||||
Reference in New Issue
Block a user