修复任务详情字段过滤契约

This commit is contained in:
andy
2026-07-13 08:50:06 +08:00
parent 890565d43f
commit b05001e481
7 changed files with 36 additions and 4 deletions

View File

@@ -242,6 +242,8 @@ Content-Type: application/json
后端已按 `docs/project/requirements/M002-task-field-control-contract-v1.md` 返回字段控件契约 V1。前端接入时注意 后端已按 `docs/project/requirements/M002-task-field-control-contract-v1.md` 返回字段控件契约 V1。前端接入时注意
- 任务详情 `fields[]` 已新增 `control_type``edit_scope``write_target``options_source``raw_readonly``control_hint` - 任务详情 `fields[]` 已新增 `control_type``edit_scope``write_target``options_source``raw_readonly``control_hint`
- 任务详情 `fields[]` 是后端按当前任务生效规则过滤后的字段集合,不是完整字段矩阵;前端不得自行补齐后端未返回的字段,也不要依赖固定 `fields.length`
- 未出现在 `fields[]` 的字段表示当前任务不展示、不校验、不提交。例如 `new_group_block` 不返回 FIT 专属 `case_keys.confirmation_number`,也不返回 Allotment 专属 `extracted_fields.child_room_items[]`;无附件时可以不返回 `attachments`
- 前端应优先按 `control_type` 渲染字段;旧 `input_editable``select_editable``date_picker``number_input``file_display``table_editable` 只作为兼容兜底。 - 前端应优先按 `control_type` 渲染字段;旧 `input_editable``select_editable``date_picker``number_input``file_display``table_editable` 只作为兼容兜底。
- `raw_readonly=true``edit_scope=never/system_only``write_target=none` 的字段不能展示普通编辑控件。 - `raw_readonly=true``edit_scope=never/system_only``write_target=none` 的字段不能展示普通编辑控件。
- `source_message`、邮件正文、附件引用、raw evidence、`event_type``source_event_index`、关系索引、`route_code``result_type``task_type``task_subtype``manual_review.reason_code` 等字段必须只读。 - `source_message`、邮件正文、附件引用、raw evidence、`event_type``source_event_index`、关系索引、`route_code``result_type``task_type``task_subtype``manual_review.reason_code` 等字段必须只读。

View File

@@ -473,6 +473,8 @@ GET /api/reservation/tasks/{taskId}
已完成的 `fields[]` 字段: 已完成的 `fields[]` 字段:
返回口径:`fields[]` 是后端按当前任务生效规则过滤后的字段集合,不是完整字段矩阵。后端已经应用 `visible``result_type``task_type``task_subtype``display_condition`;未返回字段对当前任务视为不展示、不校验、不提交,前端不要自行补齐,也不要依赖固定字段数量。
| 字段 | 说明 | | 字段 | 说明 |
| --- | --- | | --- | --- |
| `row_number` | 字段矩阵行号。 | | `row_number` | 字段矩阵行号。 |
@@ -643,6 +645,7 @@ Content-Type: application/json
中文说明: 中文说明:
- 如果前端只做“按后端字段直接渲染”,现有 `fields[]` 可以支撑第一版表单展示;本轮已经扩展 `ReservationTaskFieldResult`,避免前端维护第二套字段矩阵。 - 如果前端只做“按后端字段直接渲染”,现有 `fields[]` 可以支撑第一版表单展示;本轮已经扩展 `ReservationTaskFieldResult`,避免前端维护第二套字段矩阵。
- `fields[]` 已是当前任务生效字段集合,前端不能把导入 Excel 或历史矩阵里的其他字段自行合成到页面上;例如 Group Block 不应补出 FIT 专属 Confirmation No. 输入框。
- `result_type``task_type``task_subtype``default_value_source` 当前从后端字段矩阵定义透出。 - `result_type``task_type``task_subtype``default_value_source` 当前从后端字段矩阵定义透出。
- 如果后端已有更细的字段来源或适用场景元数据,可后续再扩展 `field_source``applicable_scenario`,不作为本轮 P0 阻塞项。 - 如果后端已有更细的字段来源或适用场景元数据,可后续再扩展 `field_source``applicable_scenario`,不作为本轮 P0 阻塞项。
- 前端默认不需要为 `GET /api/reservation/orders``GET /api/reservation/tasks``GET /api/reservation/orders/{orderId}` 自动拼 `hotel_id`;如已接入酒店选择器,可以传当前选中酒店,后端会校验访问权限。当前 `GET /api/reservation/tasks/{taskId}` 以及任务写操作 Controller 不接收 `hotel_id`;第一版先按 ID 定位,后续多酒店隔离 / 权限方案统一补齐。 - 前端默认不需要为 `GET /api/reservation/orders``GET /api/reservation/tasks``GET /api/reservation/orders/{orderId}` 自动拼 `hotel_id`;如已接入酒店选择器,可以传当前选中酒店,后端会校验访问权限。当前 `GET /api/reservation/tasks/{taskId}` 以及任务写操作 Controller 不接收 `hotel_id`;第一版先按 ID 定位,后续多酒店隔离 / 权限方案统一补齐。

View File

@@ -397,6 +397,7 @@ Content-Type: application/json
- Parent split 父事件展示为 `Cancel Allotment / cancel_allotment_control_block`,不再展示独立 Parent Cancel Booking 卡。 - Parent split 父事件展示为 `Cancel Allotment / cancel_allotment_control_block`,不再展示独立 Parent Cancel Booking 卡。
- 任务列表筛选不提供 `linked_parent_release_after_child_split`;新开发阶段只展示 `S10/S99` 筛选,不再展示旧 `S000/S999` 筛选项。 - 任务列表筛选不提供 `linked_parent_release_after_child_split`;新开发阶段只展示 `S10/S99` 筛选,不再展示旧 `S000/S999` 筛选项。
- 任务详情字段控件以 `M002-task-field-control-contract-v1.md` 为准:前端优先消费后端 `fields[]` 中的 `control_type``edit_scope``write_target``options_source``raw_readonly`,旧矩阵开关只作为兼容兜底。 - 任务详情字段控件以 `M002-task-field-control-contract-v1.md` 为准:前端优先消费后端 `fields[]` 中的 `control_type``edit_scope``write_target``options_source``raw_readonly`,旧矩阵开关只作为兼容兜底。
- 任务详情 `fields[]` 由后端按当前任务 active definition 过滤后返回,前端不得自行补齐完整字段矩阵;后端未返回的字段对当前任务视为不展示、不校验、不提交。
- type-known manual review 的 `missing_fields[]` 不应统一渲染为普通文本输入;前端应按 `field_pointer` 匹配 `fields[]`,复用对应字段控件并提交 `review_resolution.field_overrides[]` - type-known manual review 的 `missing_fields[]` 不应统一渲染为普通文本输入;前端应按 `field_pointer` 匹配 `fields[]`,复用对应字段控件并提交 `review_resolution.field_overrides[]`
- 前端不得自行硬编码 PMS 房型目录、Rate Code 全集、Fix Charge 枚举或 P1/P2 未冻结字段;选项来源未接入时按只读、容错或“目录待接入”处理。 - 前端不得自行硬编码 PMS 房型目录、Rate Code 全集、Fix Charge 枚举或 P1/P2 未冻结字段;选项来源未接入时按只读、容错或“目录待接入”处理。
@@ -447,6 +448,7 @@ V3 P0.1 不做以下事项:
- `POST /api/reservation/tasks/{taskId}/manual-review-resolutions` 支持字段修正、当前订单归属确认、JSON Pointer 到可编辑字段校验、READY 流转、confirmed payload 写入、两条 OPERA 模拟操作创建和审计记录。 - `POST /api/reservation/tasks/{taskId}/manual-review-resolutions` 支持字段修正、当前订单归属确认、JSON Pointer 到可编辑字段校验、READY 流转、confirmed payload 写入、两条 OPERA 模拟操作创建和审计记录。
- `POST /api/reservation/tasks/{taskId}/manual-review-resolutions``field_overrides[]` 已支持 `field_pointer``field_path` 两种定位方式;`field_path` 可为 P0 主路径或旧扁平路径,成功后响应归一化到 P0 主 `field_path` - `POST /api/reservation/tasks/{taskId}/manual-review-resolutions``field_overrides[]` 已支持 `field_pointer``field_path` 两种定位方式;`field_path` 可为 P0 主路径或旧扁平路径,成功后响应归一化到 P0 主 `field_path`
- 任务详情 `fields[]` 已新增 `control_type``edit_scope``write_target``options_source``raw_readonly``control_hint`,用于前端字段控件和同卡复核输入渲染。 - 任务详情 `fields[]` 已新增 `control_type``edit_scope``write_target``options_source``raw_readonly``control_hint`,用于前端字段控件和同卡复核输入渲染。
- 任务详情 `fields[]` 已与保存草稿、最终确认和复核解阻使用同一套 active definition 规则;已按 `visible``result_type``task_type``task_subtype``display_condition` 过滤。
- `room_items[0]` 字段第一版已返回 P0 JSON Pointer 和控件契约:房量为 `number`PMS 房型代码为 `select + active_pms_room_type_catalog`,房型原文 `room_type_raw` 作为 raw 证据只读。 - `room_items[0]` 字段第一版已返回 P0 JSON Pointer 和控件契约:房量为 `number`PMS 房型代码为 `select + active_pms_room_type_catalog`,房型原文 `room_type_raw` 作为 raw 证据只读。
- type-known manual review 的缺失字段返回 `edit_scope=manual_review_only``write_target=review_resolution.field_overrides`;只读诊断字段、源邮件通知卡和 route/result/task 字段不返回可编辑控件。 - type-known manual review 的缺失字段返回 `edit_scope=manual_review_only``write_target=review_resolution.field_overrides`;只读诊断字段、源邮件通知卡和 route/result/task 字段不返回可编辑控件。
- `CANCEL_ALLOTMENT` 第一版复用字段矩阵中的 `Cancel Booking 卡 / cancel_allotment_control_block` 规则Parent / Allotment 复核中 `/case_keys/block_code``case_keys.group_code` 别名处理,前端只需要提交任务详情 `fields[]` 暴露的 `case_keys.group_code`。后端会派生 `cancel_object_type=allotment_control_block` 并接受 `cancel_scope=entire_allotment_control_block` - `CANCEL_ALLOTMENT` 第一版复用字段矩阵中的 `Cancel Booking 卡 / cancel_allotment_control_block` 规则Parent / Allotment 复核中 `/case_keys/block_code``case_keys.group_code` 别名处理,前端只需要提交任务详情 `fields[]` 暴露的 `case_keys.group_code`。后端会派生 `cancel_object_type=allotment_control_block` 并接受 `cancel_scope=entire_allotment_control_block`

View File

@@ -46,6 +46,7 @@
当前后端 CP9 第一版已经补齐: 当前后端 CP9 第一版已经补齐:
- `fields[]` 已显式返回 `control_type``edit_scope``write_target``options_source``raw_readonly``control_hint` - `fields[]` 已显式返回 `control_type``edit_scope``write_target``options_source``raw_readonly``control_hint`
- `fields[]` 是后端按当前任务生效规则过滤后的字段集合,已应用 `visible``result_type``task_type``task_subtype``display_condition`;前端不得自行补齐未返回的完整字段矩阵。
- `room_items[0]` 主路径字段已返回 P0 JSON Pointer 和控件元数据。 - `room_items[0]` 主路径字段已返回 P0 JSON Pointer 和控件元数据。
- type-known manual review 的 `manual_review.missing_fields[]` 可匹配到对应 `fields[].field_pointer`,缺失字段返回 `edit_scope=manual_review_only``write_target=review_resolution.field_overrides` - type-known manual review 的 `manual_review.missing_fields[]` 可匹配到对应 `fields[].field_pointer`,缺失字段返回 `edit_scope=manual_review_only``write_target=review_resolution.field_overrides`
- `S10/S99` 源邮件只读通知卡不返回可编辑字段控件。 - `S10/S99` 源邮件只读通知卡不返回可编辑字段控件。
@@ -60,6 +61,8 @@
后端已在 `ReservationTaskFieldResult` 中新增以下字段,并在 `GET /api/reservation/tasks/{taskId}``fields[]` 中返回。 后端已在 `ReservationTaskFieldResult` 中新增以下字段,并在 `GET /api/reservation/tasks/{taskId}``fields[]` 中返回。
`fields[]` 的返回口径是“当前任务可展示 / 可编辑字段全集”,不是原始矩阵全集。后端在组装详情时复用保存草稿、最终确认和复核解阻同一套 active definition 规则;如果某个字段没有出现在 `fields[]`,前端应视为当前任务不展示、不校验、不提交。示例:`new_group_block` 不返回 FIT 专属 `case_keys.confirmation_number`,也不返回 Allotment 专属 `extracted_fields.child_room_items[]`;无附件时可以不返回 `attachments`
| 字段 | 类型 | 中文说明 | | 字段 | 类型 | 中文说明 |
| --- | --- | --- | | --- | --- | --- |
| `control_type` | string | 前端应使用的控件类型。 | | `control_type` | string | 前端应使用的控件类型。 |

View File

@@ -2257,7 +2257,21 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
ReservationTaskSnapshot task, ReservationTaskSnapshot task,
ReservationTaskCardSnapshot taskCard) { ReservationTaskCardSnapshot taskCard) {
JsonNode aiPayload = parseJson(taskCard.aiPayloadJson()); JsonNode aiPayload = parseJson(taskCard.aiPayloadJson());
return fieldDefinitionProvider.listDefinitions(task.taskCardType(), task.resultType()) List<ReservationTaskCardFieldDefinition> allDefinitions =
fieldDefinitionProvider.listDefinitions(task.taskCardType(), task.resultType());
Map<String, Object> confirmedValues = normalizeStoredFieldValues(
task,
allDefinitions,
fieldValuesFromPayloadJson(taskCard.confirmedPayloadJson()));
Map<String, Object> draftValues = normalizeStoredFieldValues(
task,
allDefinitions,
fieldValuesFromPayloadJson(taskCard.draftPayloadJson()));
Map<String, Object> effectiveValues = applySystemDerivedFieldDefaults(
task,
aiPayload,
trimToNull(taskCard.confirmedPayloadJson()) == null ? draftValues : confirmedValues);
return activeDefinitions(task, aiPayload, effectiveValues)
.stream() .stream()
.map(definition -> toFieldResult(task, definition, aiPayload)) .map(definition -> toFieldResult(task, definition, aiPayload))
.toList(); .toList();

View File

@@ -1713,7 +1713,7 @@ class SuperAgentTaskResultControllerTest {
.andExpect(jsonPath("$.external_conversation_id").value("thread-mail-detail-matrix-001")) .andExpect(jsonPath("$.external_conversation_id").value("thread-mail-detail-matrix-001"))
.andExpect(jsonPath("$.conversation_message_count").value(1)) .andExpect(jsonPath("$.conversation_message_count").value(1))
.andExpect(jsonPath("$.field_contract_version").value("20260711-p0")) .andExpect(jsonPath("$.field_contract_version").value("20260711-p0"))
.andExpect(jsonPath("$.fields.length()").value(18)) .andExpect(jsonPath("$.fields.length()").value(9))
.andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].display_name") .andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].display_name")
.value(contains("Confirmation No."))) .value(contains("Confirmation No.")))
.andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].result_type") .andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].result_type")
@@ -1729,7 +1729,11 @@ class SuperAgentTaskResultControllerTest {
.andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].write_path") .andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].write_path")
.value(contains("confirmed_payload_json.case_keys.confirmation_number"))) .value(contains("confirmed_payload_json.case_keys.confirmation_number")))
.andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].value") .andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].value")
.value(contains("CNF-MATRIX-001"))); .value(contains("CNF-MATRIX-001")))
.andExpect(jsonPath("$.fields[?(@.field_path=='attachments')].field_path")
.isEmpty())
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.child_room_items[]')].field_path")
.isEmpty());
} }
@Test @Test

View File

@@ -716,7 +716,11 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
.andExpect(jsonPath("$.fields[?(@.field_path=='relevant_message_excerpt')].write_target") .andExpect(jsonPath("$.fields[?(@.field_path=='relevant_message_excerpt')].write_target")
.value(contains("none"))) .value(contains("none")))
.andExpect(jsonPath("$.fields[?(@.field_path=='relevant_message_excerpt')].raw_readonly") .andExpect(jsonPath("$.fields[?(@.field_path=='relevant_message_excerpt')].raw_readonly")
.value(contains(true))); .value(contains(true)))
.andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].field_path")
.isEmpty())
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.child_room_items[]')].field_path")
.isEmpty());
mockMvc.perform(put("/api/reservation/tasks/{taskId}/draft", taskId) mockMvc.perform(put("/api/reservation/tasks/{taskId}/draft", taskId)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)