diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/mapper/ReservationOrderMapper.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/mapper/ReservationOrderMapper.java index ddc81a2..a0b506c 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/mapper/ReservationOrderMapper.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/mapper/ReservationOrderMapper.java @@ -22,7 +22,7 @@ public interface ReservationOrderMapper extends BaseMapper AND o.order_status = #{orderStatus} @@ -56,7 +56,7 @@ public interface ReservationOrderMapper extends BaseMapper ORDER BY - COALESCE(o.latest_activity_at, o.updated_at) DESC, + o.latest_activity_at DESC, o.updated_at DESC, o.id DESC @@ -68,6 +68,6 @@ public interface ReservationOrderMapper extends BaseMapper sourceMessageIds); } diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/MybatisReservationAiWorkflowRepository.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/MybatisReservationAiWorkflowRepository.java index 5f301f6..c0f6035 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/MybatisReservationAiWorkflowRepository.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/MybatisReservationAiWorkflowRepository.java @@ -453,7 +453,7 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork trim(request.groupCode()), trim(request.confirmationNumber()), trim(request.keyword()), - ReservationOrderVisibility.HIDDEN_SYSTEM.name(), + ReservationOrderVisibility.VISIBLE.name(), sourceMessageIds); return new ReservationPageSnapshot<>( page.getRecords().stream().map(this::toAiQueryOrderSnapshot).toList(), @@ -788,11 +788,12 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork public boolean updateOperaOperationAfterAttempt( String hotelId, Long operationId, + Long orderId, String operationStatus, Long lastAttemptId, String lastErrorMessage, LocalDateTime now) { - return operaOperationMapper.update(null, Wrappers.lambdaUpdate() + boolean updated = operaOperationMapper.update(null, Wrappers.lambdaUpdate() .set(ReservationOperaOperationEntity::getOperationStatus, operationStatus) .set(ReservationOperaOperationEntity::getLastAttemptId, lastAttemptId) .set(ReservationOperaOperationEntity::getLastErrorMessage, lastErrorMessage) @@ -800,6 +801,10 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork .setSql("attempt_count = attempt_count + 1") .eq(ReservationOperaOperationEntity::getHotelId, hotelId) .eq(ReservationOperaOperationEntity::getId, operationId)) > 0; + if (updated) { + touchOrderLatestActivity(hotelId, orderId, now); + } + return updated; } /** diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/ReservationAiWorkflowRepository.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/ReservationAiWorkflowRepository.java index cd7a739..115e647 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/ReservationAiWorkflowRepository.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/ReservationAiWorkflowRepository.java @@ -248,6 +248,7 @@ public interface ReservationAiWorkflowRepository { boolean updateOperaOperationAfterAttempt( String hotelId, Long operationId, + Long orderId, String operationStatus, Long lastAttemptId, String lastErrorMessage, diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskWorkflowServiceImpl.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskWorkflowServiceImpl.java index 4c73723..b53ace7 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskWorkflowServiceImpl.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskWorkflowServiceImpl.java @@ -500,6 +500,7 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl workflowRepository.updateOperaOperationAfterAttempt( task.hotelId(), operation.id(), + task.orderId(), operationStatus, attemptId, errorMessage, diff --git a/server/src/main/resources/db/migration/V21__add_reservation_order_latest_activity.sql b/server/src/main/resources/db/migration/V21__add_reservation_order_latest_activity.sql index bf0f04d..65b012e 100644 --- a/server/src/main/resources/db/migration/V21__add_reservation_order_latest_activity.sql +++ b/server/src/main/resources/db/migration/V21__add_reservation_order_latest_activity.sql @@ -1,6 +1,6 @@ -- M002 前端订单列表排序优化:订单自身维护最近业务活动时间,避免列表查询每次聚合全量任务。 ALTER TABLE workflow_reservation_order - ADD COLUMN latest_activity_at DATETIME(6) NULL COMMENT '订单最近业务活动 UTC 时间,用于订单列表最新活动排序'; + ADD COLUMN latest_activity_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '订单最近业务活动 UTC 时间,用于订单列表最新活动排序'; -- 历史数据回填:优先取订单更新时间和所属任务最新来源/创建时间中的较大值。 UPDATE workflow_reservation_order @@ -19,4 +19,4 @@ SET latest_activity_at = GREATEST( -- 订单列表按可见性和最新活动时间倒序分页。 CREATE INDEX idx_reservation_order_visibility_activity - ON workflow_reservation_order (hotel_id, order_visibility, latest_activity_at, id); + ON workflow_reservation_order (hotel_id, order_visibility, latest_activity_at DESC, updated_at DESC, id DESC); diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationFrontendQueryControllerTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationFrontendQueryControllerTest.java index c6eaec6..01ff0d3 100644 --- a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationFrontendQueryControllerTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationFrontendQueryControllerTest.java @@ -248,6 +248,44 @@ class ReservationFrontendQueryControllerTest { .andExpect(jsonPath("$.page.total").value(2)); } + @Test + void shouldKeepOrderLatestActivityAtRequiredForIndexedSorting() { + String isNullable = jdbcTemplate.queryForObject(""" + SELECT IS_NULLABLE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'WORKFLOW_RESERVATION_ORDER' + AND COLUMN_NAME = 'LATEST_ACTIVITY_AT' + """, String.class); + + assertThat(isNullable).isEqualTo("NO"); + } + + @Test + void shouldOnlyReturnVisibleOrdersInFrontendOrderList() throws Exception { + SourceMessageCaptureResult visibleSource = captureSourceMessage( + "mail-frontend-order-visible-filter-001", + "Frontend Query Visible Filter"); + SourceMessageCaptureResult archivedSource = captureSourceMessage( + "mail-frontend-order-visible-filter-archived-001", + "Frontend Query Visible Filter Archived"); + Long visibleOrderId = 930000000000002201L; + Long archivedOrderId = 930000000000002202L; + insertGroupOrder(visibleOrderId, visibleSource.inboxId(), + "GRP-FRONTEND-VISIBLE-FILTER-001", "ACTIVE"); + insertGroupOrder(archivedOrderId, archivedSource.inboxId(), + "GRP-FRONTEND-VISIBLE-FILTER-ARCHIVED", "ACTIVE"); + updateOrderVisibility(archivedOrderId, "ARCHIVED_SYSTEM"); + + mockMvc.perform(get("/api/reservation/orders") + .param("hotel_id", HOTEL_ID) + .param("keyword", "GRP-FRONTEND-VISIBLE-FILTER-") + .param("page_num", "1") + .param("page_size", "20")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.items[0].order_id").value(visibleOrderId.toString())) + .andExpect(jsonPath("$.page.total").value(1)); + } + @Test void shouldTouchOrderLatestActivityWhenRepositoryInsertsTask() { SourceMessageCaptureResult source = captureSourceMessage( @@ -457,6 +495,14 @@ class ReservationFrontendQueryControllerTest { """, Timestamp.from(latestActivityAt), orderId); } + private void updateOrderVisibility(Long orderId, String orderVisibility) { + jdbcTemplate.update(""" + UPDATE workflow_reservation_order + SET order_visibility = ? + WHERE id = ? + """, orderVisibility, orderId); + } + private void insertActiveGroupOrder(Long orderId, Long sourceMessageId, String groupCode) { insertGroupOrder(orderId, sourceMessageId, groupCode, "ACTIVE"); } diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java index 79a8d71..b10a6d5 100644 --- a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java @@ -19,6 +19,7 @@ import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResu import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; +import java.sql.Timestamp; import java.time.Instant; import java.util.HexFormat; import java.util.List; @@ -1968,6 +1969,18 @@ class SuperAgentTaskResultControllerTest { "CNF-CP5-OPERA-RETRY-001"); String taskId = taskAndOperationIds[0]; String firstOperationId = taskAndOperationIds[1]; + Long orderId = jdbcTemplate.queryForObject(""" + SELECT order_id + FROM workflow_reservation_task + WHERE id = ? + """, Long.class, Long.valueOf(taskId)); + Timestamp staleLatestActivityAt = Timestamp.from(Instant.parse("2026-07-01T00:00:00Z")); + jdbcTemplate.update(""" + UPDATE workflow_reservation_order + SET latest_activity_at = ?, + updated_at = ? + WHERE id = ? + """, staleLatestActivityAt, staleLatestActivityAt, orderId); mockMvc.perform(post( "/api/reservation/tasks/{taskId}/opera-operations/{operationId}/execute", @@ -1984,6 +1997,12 @@ class SuperAgentTaskResultControllerTest { .andExpect(jsonPath("$.operation_status").value("FAILED")) .andExpect(jsonPath("$.attempt_count").value(1)) .andExpect(jsonPath("$.attempts[0].attempt_status").value("FAILED")); + Timestamp failedAttemptLatestActivityAt = jdbcTemplate.queryForObject(""" + SELECT latest_activity_at + FROM workflow_reservation_order + WHERE id = ? + """, Timestamp.class, orderId); + assertThat(failedAttemptLatestActivityAt.toInstant()).isAfter(staleLatestActivityAt.toInstant()); mockMvc.perform(post( "/api/reservation/tasks/{taskId}/opera-operations/{operationId}/retry",