补齐前端P0查询接口

This commit is contained in:
andy
2026-07-08 16:34:27 +08:00
parent 66a613d6cd
commit 3c1649a567
34 changed files with 1662 additions and 39 deletions

View File

@@ -1,5 +1,6 @@
package cn.nianxx.thhotel.platform.message.control;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -125,6 +126,62 @@ class SourceMessageControllerTest {
org.assertj.core.api.Assertions.assertThat(auditCount).isEqualTo(1L);
}
@Test
void shouldReadConversationOriginalContentAndRelatedReservationSummaryWithoutFrontendKey() throws Exception {
SourceMessageCaptureResult first = captureService.capture(conversationCommand(
"mail-conversation-p0-001",
"conversation-p0-001",
"First full text body with booking context.",
"<html><body><p>First HTML body</p><img src=\"cid:image-001\" /></body></html>",
List.of(
media("INLINE_IMAGE", "image-001.png", "image/png", 100L,
"https://media.example.test/image-001.png?token=secret", "image-001"),
media("ATTACHMENT", "first.pdf", "application/pdf", 1000L,
"https://media.example.test/first.pdf?token=secret", "attachment-001")
)));
SourceMessageCaptureResult second = captureService.capture(conversationCommand(
"mail-conversation-p0-002",
"conversation-p0-001",
"Second full text body.",
"<html><body><p>Second HTML body</p></body></html>",
List.of()));
Long orderId = 940000000000000101L;
Long transitionId = 940000000000000201L;
Long taskId = 940000000000000301L;
insertRelatedOrder(orderId, first.inboxId(), "GRP-CONVERSATION-P0-001");
insertRelatedTransition(transitionId, first.inboxId(), "GRP-CONVERSATION-P0-001");
insertRelatedTask(taskId, orderId, first.inboxId(), transitionId);
mockMvc.perform(get("/api/source-messages/{sourceMessageId}/conversation", first.inboxId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.conversation.external_conversation_id").value("conversation-p0-001"))
.andExpect(jsonPath("$.conversation.message_count").value(2))
.andExpect(jsonPath("$.messages.length()").value(2))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].text_body")
.value(contains(containsString("First full text body"))))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_body")
.value(contains(containsString("First HTML body"))))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].inline_images[0].externalUrl")
.value(contains("https://media.example.test/image-001.png?token=secret")))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].attachments[0].externalUrl")
.value(contains("https://media.example.test/first.pdf?token=secret")))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].related_orders[0].order_id")
.value(contains(orderId.toString())))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].related_tasks[0].task_id")
.value(contains(taskId.toString())))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-002')].text_body")
.value(contains(containsString("Second full text body"))));
Long auditCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM platform_source_message_original_access_audit
WHERE inbox_id IN (?, ?)
AND actor_id = 'system:source-message-conversation'
AND access_scene = 'source-message-conversation'
""", Long.class, first.inboxId(), second.inboxId());
org.assertj.core.api.Assertions.assertThat(auditCount).isEqualTo(2L);
}
private CaptureSourceMessageCommand command(
String externalMessageId,
String externalConversationId,
@@ -155,4 +212,81 @@ class SourceMessageControllerTest {
"attachment-api-001"))
);
}
private CaptureSourceMessageCommand conversationCommand(
String externalMessageId,
String externalConversationId,
String textBody,
String htmlBody,
List<CaptureSourceMessageMedia> mediaItems) {
return new CaptureSourceMessageCommand(
"HOTEL-TEST",
"AGENTBUS",
"EMAIL",
externalMessageId,
externalConversationId,
"frame-" + externalMessageId,
"session-api",
Instant.parse("2026-07-06T10:00:00Z"),
"guest@example.test",
"Reservation update",
textBody,
htmlBody,
"{\"source\":{\"external_message_id\":\"" + externalMessageId + "\"}}",
"agentbus-outlook-v1",
mediaItems
);
}
private CaptureSourceMessageMedia media(
String mediaType,
String fileName,
String contentType,
Long sizeBytes,
String externalUrl,
String externalMediaId) {
return new CaptureSourceMessageMedia(mediaType, fileName, contentType, sizeBytes, externalUrl, externalMediaId);
}
private void insertRelatedOrder(Long orderId, Long sourceMessageId, String groupCode) {
jdbcTemplate.update("""
INSERT INTO workflow_reservation_order (
id, hotel_id, order_key_type, order_business_key, active_business_key,
temporary_order_code, order_status, business_key_source, display_name,
source_message_id, version, created_at, updated_at
)
VALUES (?, 'HOTEL-TEST', 'GROUP_CODE', ?, ?, ?, 'ACTIVE', 'AI_CANDIDATE', ?,
?, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", orderId, groupCode, groupCode, "TMP-" + orderId, groupCode, sourceMessageId);
}
private void insertRelatedTransition(Long transitionId, Long sourceMessageId, String groupCode) {
jdbcTemplate.update("""
INSERT INTO workflow_reservation_ai_transition (
id, hotel_id, batch_id, source_message_id, source_event_index, array_index,
execution_order, catalog_code, skill_id, result_type, ai_task_type,
system_task_type, task_card_type, task_subtype, current_or_history,
group_code, item_payload_sha256, item_idempotency_key, blocked_until_parent_completed,
ai_payload_json, case_keys_json, extracted_fields_json, created_at, updated_at
)
VALUES (?, 'HOTEL-TEST', ?, ?, 1, 1, 1, 'S02', 'conversation_skill',
'normal_task', 'New Booking', 'NEW_BOOKING', 'NEW_BOOKING', 'new_group_block',
'current', ?, ?, ?, 0, '{}', '{}', '{}', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", transitionId, transitionId - 1, sourceMessageId, groupCode, "1".repeat(64),
transitionId.toString());
}
private void insertRelatedTask(Long taskId, Long orderId, Long sourceMessageId, Long transitionId) {
jdbcTemplate.update("""
INSERT INTO workflow_reservation_task (
id, hotel_id, order_id, source_message_id, ai_transition_id,
result_type, ai_task_type, system_task_type, task_card_type, task_subtype,
task_status, queue_participation, execution_order, blocked_until_parent_completed,
version, created_at, updated_at
)
VALUES (?, 'HOTEL-TEST', ?, ?, ?, 'normal_task', 'New Booking',
'NEW_BOOKING', 'NEW_BOOKING', 'new_group_block', 'PENDING_CONFIRM',
1, 1, 0, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", taskId, orderId, sourceMessageId, transitionId);
}
}

View File

@@ -1,6 +1,7 @@
package cn.nianxx.thhotel.workflows.reservation.control;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.matchesPattern;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
@@ -90,10 +91,18 @@ class ReservationFrontendQueryControllerTest {
.andExpect(jsonPath("$.items[0].readonly_reason_code").value("PROCESSABLE"))
.andExpect(jsonPath("$.items[0].source_message_id").value(source.inboxId().toString()))
.andExpect(jsonPath("$.items[0].source_subject").value("Frontend Query List Smoke"))
.andExpect(jsonPath("$.items[0].source_sender_summary").value("g***@example.test"))
.andExpect(jsonPath("$.items[0].source_received_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
.andExpect(jsonPath("$.items[0].external_conversation_id")
.value("thread-mail-frontend-task-list-001"))
.andExpect(jsonPath("$.items[0].conversation_message_count").value(1))
.andExpect(jsonPath("$.items[0].created_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
.andExpect(jsonPath("$.items[0].updated_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
.andExpect(jsonPath("$.items[1].task_id").value(secondTaskId.toString()))
.andExpect(jsonPath("$.items[1].task_type").value("UPDATE_BOOKING"))
.andExpect(jsonPath("$.items[1].external_conversation_id")
.value("thread-mail-frontend-task-list-001"))
.andExpect(jsonPath("$.items[1].conversation_message_count").value(1))
.andExpect(jsonPath("$.items[1].can_process").value(false))
.andExpect(jsonPath("$.items[1].readonly_reason_code").value("PREVIOUS_TASK_NOT_FINISHED"))
.andExpect(jsonPath("$.page.page_num").value(1))
@@ -148,6 +157,13 @@ class ReservationFrontendQueryControllerTest {
.andExpect(jsonPath("$.order.updated_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
.andExpect(jsonPath("$.tasks[0].task_id").value(firstTaskId.toString()))
.andExpect(jsonPath("$.tasks[0].task_status").value("COMPLETED"))
.andExpect(jsonPath("$.tasks[0].source_message_id").value(source.inboxId().toString()))
.andExpect(jsonPath("$.tasks[0].source_subject").value("Frontend Query Detail Smoke"))
.andExpect(jsonPath("$.tasks[0].source_sender_summary").value("g***@example.test"))
.andExpect(jsonPath("$.tasks[0].source_received_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
.andExpect(jsonPath("$.tasks[0].external_conversation_id")
.value("thread-mail-frontend-order-detail-001"))
.andExpect(jsonPath("$.tasks[0].conversation_message_count").value(1))
.andExpect(jsonPath("$.tasks[0].readonly_reason_code").value("TASK_FINISHED"))
.andExpect(jsonPath("$.tasks[0].created_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
.andExpect(jsonPath("$.tasks[1].task_id").value(secondTaskId.toString()))
@@ -165,6 +181,62 @@ class ReservationFrontendQueryControllerTest {
.andExpect(jsonPath("$.tasks.length()").value(0));
}
@Test
void shouldReturnOrderListWithOpenTaskCountAndNextProcessableTask() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage(
"mail-frontend-order-list-001",
"Frontend Query Order List Smoke");
Long activeOrderId = 930000000000000701L;
Long endedOrderId = 930000000000000702L;
Long pendingTaskId = 930000000000000901L;
Long completedTaskId = 930000000000000902L;
Long failedTaskId = 930000000000000903L;
insertGroupOrder(activeOrderId, source.inboxId(), "GRP-FRONTEND-ORDERS-ACTIVE-001", "ACTIVE");
insertGroupOrder(endedOrderId, source.inboxId(), "GRP-FRONTEND-ORDERS-ENDED-001", "ENDED");
insertTransition(930000000000000801L, source.inboxId(), 1, "GRP-FRONTEND-ORDERS-ACTIVE-001",
"New Booking", "NEW_BOOKING", "NEW_BOOKING");
insertTransition(930000000000000802L, source.inboxId(), 2, "GRP-FRONTEND-ORDERS-ACTIVE-001",
"Update Booking", "UPDATE_BOOKING", "UPDATE_BOOKING");
insertTransition(930000000000000803L, source.inboxId(), 3, "GRP-FRONTEND-ORDERS-ACTIVE-001",
"Cancel Booking", "CANCEL_BOOKING", "CANCEL_BOOKING");
insertTask(pendingTaskId, activeOrderId, source.inboxId(), 930000000000000801L, "New Booking",
"NEW_BOOKING", "NEW_BOOKING", "PENDING_CONFIRM", 1);
insertTask(completedTaskId, activeOrderId, source.inboxId(), 930000000000000802L, "Update Booking",
"UPDATE_BOOKING", "UPDATE_BOOKING", "COMPLETED", 2);
insertTask(failedTaskId, activeOrderId, source.inboxId(), 930000000000000803L, "Cancel Booking",
"CANCEL_BOOKING", "CANCEL_BOOKING", "FAILED", 3);
mockMvc.perform(get("/api/reservation/orders")
.param("hotel_id", HOTEL_ID)
.param("keyword", "GRP-FRONTEND-ORDERS-")
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items.length()").value(2))
.andExpect(jsonPath("$.items[?(@.order_id=='" + activeOrderId + "')].order_status")
.value(contains("ACTIVE")))
.andExpect(jsonPath("$.items[?(@.order_id=='" + activeOrderId + "')].open_task_count")
.value(contains(1)))
.andExpect(jsonPath("$.items[?(@.order_id=='" + activeOrderId + "')].next_processable_task_id")
.value(contains(pendingTaskId.toString())))
.andExpect(jsonPath("$.items[?(@.order_id=='" + endedOrderId + "')].order_status")
.value(contains("ENDED")))
.andExpect(jsonPath("$.items[?(@.order_id=='" + endedOrderId + "')].open_task_count")
.value(contains(0)))
.andExpect(jsonPath("$.page.total").value(2));
mockMvc.perform(get("/api/reservation/orders")
.param("hotel_id", HOTEL_ID)
.param("order_status", "ENDED")
.param("keyword", "GRP-FRONTEND-ORDERS-")
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items.length()").value(1))
.andExpect(jsonPath("$.items[0].order_id").value(endedOrderId.toString()))
.andExpect(jsonPath("$.items[0].order_status").value("ENDED"));
}
private SourceMessageCaptureResult captureSourceMessage(String externalMessageId, String subject) {
return captureService.capture(new CaptureSourceMessageCommand(
HOTEL_ID,
@@ -186,15 +258,19 @@ class ReservationFrontendQueryControllerTest {
}
private void insertActiveGroupOrder(Long orderId, Long sourceMessageId, String groupCode) {
insertGroupOrder(orderId, sourceMessageId, groupCode, "ACTIVE");
}
private void insertGroupOrder(Long orderId, Long sourceMessageId, String groupCode, String orderStatus) {
jdbcTemplate.update("""
INSERT INTO workflow_reservation_order (
id, hotel_id, order_key_type, order_business_key, active_business_key,
temporary_order_code, order_status, business_key_source, display_name,
source_message_id, version, created_at, updated_at
)
VALUES (?, ?, 'GROUP_CODE', ?, ?, ?, 'ACTIVE', 'AI_CANDIDATE', ?, ?, 0,
VALUES (?, ?, 'GROUP_CODE', ?, ?, ?, ?, 'AI_CANDIDATE', ?, ?, 0,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", orderId, HOTEL_ID, groupCode, groupCode, "TMP-" + orderId, groupCode, sourceMessageId);
""", orderId, HOTEL_ID, groupCode, groupCode, "TMP-" + orderId, orderStatus, groupCode, sourceMessageId);
}
private void insertTransition(

View File

@@ -1,6 +1,8 @@
package cn.nianxx.thhotel.workflows.reservation.control;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.not;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -48,6 +50,7 @@ class SuperAgentTaskResultControllerTest {
private static final String ENDPOINT = "/api/integrations/superagent/task-results";
private static final String CLIENT_ID = "superagent-test-client";
private static final String SECRET = "test-superagent-secret";
private static final String UTC_INSTANT_PATTERN = "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$";
@Autowired
private MockMvc mockMvc;
@@ -519,16 +522,29 @@ class SuperAgentTaskResultControllerTest {
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.task_card_type").value("NEW_BOOKING"))
.andExpect(jsonPath("$.source_subject").value("M002 SuperAgent intake"))
.andExpect(jsonPath("$.source_sender_summary").value("g***@example.test"))
.andExpect(jsonPath("$.source_received_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
.andExpect(jsonPath("$.external_conversation_id").value("thread-mail-detail-matrix-001"))
.andExpect(jsonPath("$.conversation_message_count").value(1))
.andExpect(jsonPath("$.field_contract_version").value("code-v1"))
.andExpect(jsonPath("$.fields.length()").value(18))
.andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].display_name")
.value(org.hamcrest.Matchers.contains("Confirmation No.")))
.value(contains("Confirmation No.")))
.andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].result_type")
.value(contains("normal_task")))
.andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].task_type")
.value(contains("New Booking")))
.andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].task_subtype")
.value(contains("new_fit_reservation")))
.andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].default_value_source")
.value(contains("AI提取值")))
.andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].editable")
.value(org.hamcrest.Matchers.contains("")))
.value(contains("")))
.andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.confirmation_number')].write_path")
.value(org.hamcrest.Matchers.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")
.value(org.hamcrest.Matchers.contains("CNF-MATRIX-001")));
.value(contains("CNF-MATRIX-001")));
}
@Test