优化订单任务列表排序并补充上线文档

This commit is contained in:
andy
2026-07-13 12:20:05 +08:00
parent 51ebc8e767
commit 4b98cf9491
10 changed files with 344 additions and 46 deletions

View File

@@ -38,6 +38,8 @@ public class ReservationOrderEntity {
private Long sourceMessageId;
/** 首次创建该订单的任务 IDCP1-3 允许为空。 */
private Long createdFromTaskId;
/** 订单最近业务活动 UTC 时间,用于订单列表按最新活动排序。 */
private LocalDateTime latestActivityAt;
/** 订单进入 ENDED 状态的 UTC 时间。 */
private LocalDateTime endedAt;
/** 逻辑删除 UTC 时间。 */
@@ -77,6 +79,8 @@ public class ReservationOrderEntity {
public void setSourceMessageId(Long sourceMessageId) { this.sourceMessageId = sourceMessageId; }
public Long getCreatedFromTaskId() { return createdFromTaskId; }
public void setCreatedFromTaskId(Long createdFromTaskId) { this.createdFromTaskId = createdFromTaskId; }
public LocalDateTime getLatestActivityAt() { return latestActivityAt; }
public void setLatestActivityAt(LocalDateTime latestActivityAt) { this.latestActivityAt = latestActivityAt; }
public LocalDateTime getEndedAt() { return endedAt; }
public void setEndedAt(LocalDateTime endedAt) { this.endedAt = endedAt; }
public LocalDateTime getLogicDeletedAt() { return logicDeletedAt; }

View File

@@ -2,11 +2,72 @@ package cn.nianxx.thhotel.workflows.reservation.mapper;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationOrderEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* Reservation 订单 Mapper只负责本表持久化访问。
*/
@Mapper
public interface ReservationOrderMapper extends BaseMapper<ReservationOrderEntity> {
/**
* 分页查询前端订单列表。排序按订单维护的最新业务活动时间倒序,避免每次列表查询聚合全量任务。
*/
@Select("""
<script>
SELECT o.*
FROM workflow_reservation_order o
WHERE o.hotel_id = #{hotelId}
AND o.order_visibility != #{hiddenSystemVisibility}
<if test="orderStatus != null and orderStatus != ''">
AND o.order_status = #{orderStatus}
</if>
<if test="groupCode != null and groupCode != ''">
AND (
o.order_business_key LIKE CONCAT('%', #{groupCode}, '%')
OR o.active_business_key LIKE CONCAT('%', #{groupCode}, '%')
OR o.display_name LIKE CONCAT('%', #{groupCode}, '%')
)
</if>
<if test="confirmationNumber != null and confirmationNumber != ''">
AND (
o.order_business_key LIKE CONCAT('%', #{confirmationNumber}, '%')
OR o.active_business_key LIKE CONCAT('%', #{confirmationNumber}, '%')
OR o.display_name LIKE CONCAT('%', #{confirmationNumber}, '%')
)
</if>
<if test="keyword != null and keyword != ''">
AND (
o.order_business_key LIKE CONCAT('%', #{keyword}, '%')
OR o.active_business_key LIKE CONCAT('%', #{keyword}, '%')
OR o.temporary_order_code LIKE CONCAT('%', #{keyword}, '%')
OR o.order_status LIKE CONCAT('%', #{keyword}, '%')
OR o.display_name LIKE CONCAT('%', #{keyword}, '%')
<if test="sourceMessageIds != null and sourceMessageIds.size() > 0">
OR o.source_message_id IN
<foreach collection="sourceMessageIds" item="sourceMessageIdItem" open="(" separator="," close=")">
#{sourceMessageIdItem}
</foreach>
</if>
)
</if>
ORDER BY
COALESCE(o.latest_activity_at, o.updated_at) DESC,
o.updated_at DESC,
o.id DESC
</script>
""")
Page<ReservationOrderEntity> selectFrontendOrderPage(
Page<ReservationOrderEntity> page,
@Param("hotelId") String hotelId,
@Param("orderStatus") String orderStatus,
@Param("groupCode") String groupCode,
@Param("confirmationNumber") String confirmationNumber,
@Param("keyword") String keyword,
@Param("hiddenSystemVisibility") String hiddenSystemVisibility,
@Param("sourceMessageIds") List<Long> sourceMessageIds);
}

View File

@@ -24,6 +24,9 @@ public interface ReservationTaskMapper extends BaseMapper<ReservationTaskEntity>
LEFT JOIN workflow_reservation_order o
ON o.hotel_id = t.hotel_id
AND o.id = t.order_id
LEFT JOIN platform_source_message_inbox sm
ON sm.hotel_id = t.hotel_id
AND sm.id = t.source_message_id
WHERE t.hotel_id = #{hotelId}
<if test="orderId != null">
AND t.order_id = #{orderId}
@@ -61,7 +64,14 @@ public interface ReservationTaskMapper extends BaseMapper<ReservationTaskEntity>
</if>
)
</if>
ORDER BY t.order_id ASC, t.execution_order ASC
<choose>
<when test="orderId != null">
ORDER BY t.execution_order ASC, t.id ASC
</when>
<otherwise>
ORDER BY COALESCE(sm.received_at, t.created_at) DESC, t.created_at DESC, t.id DESC
</otherwise>
</choose>
</script>
""")
Page<ReservationTaskEntity> selectFrontendTaskPage(

View File

@@ -204,6 +204,7 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
entity.setDisplayName(draft.displayName());
entity.setSourceMessageId(draft.sourceMessageId());
entity.setVersion(0L);
entity.setLatestActivityAt(draft.now());
entity.setCreatedAt(draft.now());
entity.setUpdatedAt(draft.now());
orderMapper.insert(entity);
@@ -249,6 +250,7 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
entity.setCreatedAt(draft.now());
entity.setUpdatedAt(draft.now());
taskMapper.insert(entity);
touchOrderLatestActivity(draft.hotelId(), draft.orderId(), draft.now());
return entity.getId();
}
@@ -444,41 +446,15 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
int pageNum,
int pageSize) {
List<Long> sourceMessageIds = keywordSourceMessageIds == null ? List.of() : keywordSourceMessageIds;
Page<ReservationOrderEntity> page = orderMapper.selectPage(Page.of(pageNum, pageSize),
Wrappers.<ReservationOrderEntity>lambdaQuery()
.eq(ReservationOrderEntity::getHotelId, request.hotelId())
.ne(ReservationOrderEntity::getOrderVisibility, ReservationOrderVisibility.HIDDEN_SYSTEM.name())
.eq(hasText(request.orderStatus()),
ReservationOrderEntity::getOrderStatus,
trim(request.orderStatus()))
.and(hasText(request.groupCode()), wrapper -> wrapper
.like(ReservationOrderEntity::getOrderBusinessKey, trim(request.groupCode()))
.or()
.like(ReservationOrderEntity::getActiveBusinessKey, trim(request.groupCode()))
.or()
.like(ReservationOrderEntity::getDisplayName, trim(request.groupCode())))
.and(hasText(request.confirmationNumber()), wrapper -> wrapper
.like(ReservationOrderEntity::getOrderBusinessKey, trim(request.confirmationNumber()))
.or()
.like(ReservationOrderEntity::getActiveBusinessKey, trim(request.confirmationNumber()))
.or()
.like(ReservationOrderEntity::getDisplayName, trim(request.confirmationNumber())))
.and(hasText(request.keyword()), wrapper -> wrapper
.like(ReservationOrderEntity::getOrderBusinessKey, trim(request.keyword()))
.or()
.like(ReservationOrderEntity::getActiveBusinessKey, trim(request.keyword()))
.or()
.like(ReservationOrderEntity::getTemporaryOrderCode, trim(request.keyword()))
.or()
.like(ReservationOrderEntity::getOrderStatus, trim(request.keyword()))
.or()
.like(ReservationOrderEntity::getDisplayName, trim(request.keyword()))
.or(!sourceMessageIds.isEmpty())
.in(!sourceMessageIds.isEmpty(),
ReservationOrderEntity::getSourceMessageId,
sourceMessageIds))
.orderByDesc(ReservationOrderEntity::getUpdatedAt)
.orderByDesc(ReservationOrderEntity::getId));
Page<ReservationOrderEntity> page = orderMapper.selectFrontendOrderPage(
Page.of(pageNum, pageSize),
request.hotelId(),
trim(request.orderStatus()),
trim(request.groupCode()),
trim(request.confirmationNumber()),
trim(request.keyword()),
ReservationOrderVisibility.HIDDEN_SYSTEM.name(),
sourceMessageIds);
return new ReservationPageSnapshot<>(
page.getRecords().stream().map(this::toAiQueryOrderSnapshot).toList(),
page.getTotal(),
@@ -592,6 +568,7 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
.set(ReservationOrderEntity::getOrderStatus, ReservationOrderStatus.ACTIVE.name())
.set(ReservationOrderEntity::getBusinessKeySource, businessKeySource)
.set(ReservationOrderEntity::getDisplayName, orderBusinessKey)
.set(ReservationOrderEntity::getLatestActivityAt, now)
.set(ReservationOrderEntity::getUpdatedAt, now)
.eq(ReservationOrderEntity::getHotelId, hotelId)
.eq(ReservationOrderEntity::getId, orderId)
@@ -618,7 +595,7 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
entity.setTaskCardType(taskCardType);
entity.setExecutionOrder(executionOrder);
entity.setUpdatedAt(now);
taskMapper.update(entity, Wrappers.<ReservationTaskEntity>lambdaUpdate()
int updatedTaskCount = taskMapper.update(entity, Wrappers.<ReservationTaskEntity>lambdaUpdate()
.eq(ReservationTaskEntity::getHotelId, hotelId)
.eq(ReservationTaskEntity::getId, taskId));
ReservationTaskCardEntity cardEntity = new ReservationTaskCardEntity();
@@ -627,6 +604,9 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
taskCardMapper.update(cardEntity, Wrappers.<ReservationTaskCardEntity>lambdaUpdate()
.eq(ReservationTaskCardEntity::getHotelId, hotelId)
.eq(ReservationTaskCardEntity::getTaskId, taskId));
if (updatedTaskCount > 0) {
touchOrderLatestActivity(hotelId, orderId, now);
}
}
/**
@@ -634,11 +614,15 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
*/
@Override
public boolean updateTaskDraftPayload(String hotelId, Long taskId, String draftPayloadJson, LocalDateTime now) {
return taskCardMapper.update(null, Wrappers.<ReservationTaskCardEntity>lambdaUpdate()
boolean updated = taskCardMapper.update(null, Wrappers.<ReservationTaskCardEntity>lambdaUpdate()
.set(ReservationTaskCardEntity::getDraftPayloadJson, draftPayloadJson)
.set(ReservationTaskCardEntity::getUpdatedAt, now)
.eq(ReservationTaskCardEntity::getHotelId, hotelId)
.eq(ReservationTaskCardEntity::getTaskId, taskId)) > 0;
if (updated) {
touchOrderLatestActivityByTaskId(hotelId, taskId, now);
}
return updated;
}
/**
@@ -669,6 +653,9 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
.set(ReservationTaskCardEntity::getUpdatedAt, confirmedAt)
.eq(ReservationTaskCardEntity::getHotelId, hotelId)
.eq(ReservationTaskCardEntity::getTaskId, taskId));
if (updatedCardCount > 0) {
touchOrderLatestActivityByTaskId(hotelId, taskId, confirmedAt);
}
return updatedCardCount > 0;
}
@@ -704,6 +691,9 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
.set(ReservationTaskCardEntity::getUpdatedAt, confirmedAt)
.eq(ReservationTaskCardEntity::getHotelId, hotelId)
.eq(ReservationTaskCardEntity::getTaskId, taskId));
if (updatedCardCount > 0) {
touchOrderLatestActivityByTaskId(hotelId, taskId, confirmedAt);
}
return updatedCardCount > 0;
}
@@ -817,7 +807,7 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
*/
@Override
public boolean updateTaskStatus(String hotelId, Long taskId, String taskStatus, LocalDateTime now) {
return taskMapper.update(null, Wrappers.<ReservationTaskEntity>lambdaUpdate()
boolean updated = taskMapper.update(null, Wrappers.<ReservationTaskEntity>lambdaUpdate()
.set(ReservationTaskEntity::getTaskStatus, taskStatus)
.set(ReservationTaskEntity::getCompletedAt,
ReservationTaskStatus.COMPLETED.name().equals(taskStatus) ? now : null)
@@ -825,6 +815,10 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
.setSql("version = version + 1")
.eq(ReservationTaskEntity::getHotelId, hotelId)
.eq(ReservationTaskEntity::getId, taskId)) > 0;
if (updated) {
touchOrderLatestActivityByTaskId(hotelId, taskId, now);
}
return updated;
}
/**
@@ -847,6 +841,7 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
.set(ReservationOrderEntity::getActiveBusinessKey, null)
.set(ReservationOrderEntity::getLogicDeletedAt, now)
.set(ReservationOrderEntity::getLogicDeletedReason, reason)
.set(ReservationOrderEntity::getLatestActivityAt, now)
.set(ReservationOrderEntity::getUpdatedAt, now)
.eq(ReservationOrderEntity::getHotelId, hotelId)
.eq(ReservationOrderEntity::getId, orderId)
@@ -916,6 +911,40 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
}));
}
/**
* 按任务定位订单并刷新订单最近业务活动时间,用于任务状态、确认和草稿等写操作。
*/
private void touchOrderLatestActivityByTaskId(String hotelId, Long taskId, LocalDateTime activityAt) {
if (taskId == null) {
return;
}
ReservationTaskEntity task = taskMapper.selectOne(Wrappers.<ReservationTaskEntity>lambdaQuery()
.eq(ReservationTaskEntity::getHotelId, hotelId)
.eq(ReservationTaskEntity::getId, taskId)
.last("LIMIT 1"));
if (task == null) {
return;
}
touchOrderLatestActivity(hotelId, task.getOrderId(), activityAt);
}
/**
* 刷新订单最近业务活动时间。只前进不回退,避免旧事件覆盖更新的用户操作。
*/
private void touchOrderLatestActivity(String hotelId, Long orderId, LocalDateTime activityAt) {
if (!hasText(hotelId) || orderId == null || activityAt == null) {
return;
}
orderMapper.update(null, Wrappers.<ReservationOrderEntity>lambdaUpdate()
.set(ReservationOrderEntity::getLatestActivityAt, activityAt)
.eq(ReservationOrderEntity::getHotelId, hotelId)
.eq(ReservationOrderEntity::getId, orderId)
.and(wrapper -> wrapper
.isNull(ReservationOrderEntity::getLatestActivityAt)
.or()
.lt(ReservationOrderEntity::getLatestActivityAt, activityAt)));
}
/**
* 将任务实体批量转换为 AI 查询任务快照,补充 transition 中的 Skill 元数据。
*/

View File

@@ -0,0 +1,22 @@
-- M002 前端订单列表排序优化:订单自身维护最近业务活动时间,避免列表查询每次聚合全量任务。
ALTER TABLE workflow_reservation_order
ADD COLUMN latest_activity_at DATETIME(6) NULL COMMENT '订单最近业务活动 UTC 时间,用于订单列表最新活动排序';
-- 历史数据回填:优先取订单更新时间和所属任务最新来源/创建时间中的较大值。
UPDATE workflow_reservation_order
SET latest_activity_at = GREATEST(
updated_at,
COALESCE((
SELECT MAX(COALESCE(sm.received_at, t.created_at))
FROM workflow_reservation_task t
LEFT JOIN platform_source_message_inbox sm
ON sm.hotel_id = t.hotel_id
AND sm.id = t.source_message_id
WHERE t.hotel_id = workflow_reservation_order.hotel_id
AND t.order_id = workflow_reservation_order.id
), updated_at)
);
-- 订单列表按可见性和最新活动时间倒序分页。
CREATE INDEX idx_reservation_order_visibility_activity
ON workflow_reservation_order (hotel_id, order_visibility, latest_activity_at, id);

View File

@@ -1,7 +1,8 @@
package cn.nianxx.thhotel.workflows.reservation.control;
import static org.hamcrest.Matchers.nullValue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.matchesPattern;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
@@ -18,8 +19,17 @@ import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCom
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOrderDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOrderSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskDraft;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderKeyType;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderVisibility;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationTaskStatus;
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -156,6 +166,138 @@ class ReservationFrontendQueryControllerTest {
.andExpect(jsonPath("$.page.total").value(1));
}
@Test
void shouldReturnTaskWorkbenchListWithLatestSourceMessageFirst() throws Exception {
SourceMessageCaptureResult oldSource = captureSourceMessage(
"mail-frontend-task-sort-old-001",
"Frontend Query Task Sort Old",
Instant.parse("2026-07-08T01:00:00Z"));
SourceMessageCaptureResult latestSource = captureSourceMessage(
"mail-frontend-task-sort-latest-001",
"Frontend Query Task Sort Latest",
Instant.parse("2026-07-10T09:30:00Z"));
Long oldOrderId = 930000000000001401L;
Long latestOrderId = 930000000000001402L;
Long oldTaskId = 930000000000001601L;
Long latestTaskId = 930000000000001602L;
insertGroupOrder(oldOrderId, oldSource.inboxId(), "GRP-FRONTEND-TASK-SORT-OLD", "ACTIVE");
insertGroupOrder(latestOrderId, latestSource.inboxId(), "GRP-FRONTEND-TASK-SORT-LATEST", "ACTIVE");
insertTransition(930000000000001501L, oldSource.inboxId(), 1, "GRP-FRONTEND-TASK-SORT-OLD",
"New Booking", "NEW_BOOKING", "NEW_BOOKING");
insertTransition(930000000000001502L, latestSource.inboxId(), 1, "GRP-FRONTEND-TASK-SORT-LATEST",
"Update Booking", "UPDATE_BOOKING", "UPDATE_BOOKING");
insertTask(oldTaskId, oldOrderId, oldSource.inboxId(), 930000000000001501L, "New Booking",
"NEW_BOOKING", "NEW_BOOKING", "PENDING_CONFIRM", 1);
insertTask(latestTaskId, latestOrderId, latestSource.inboxId(), 930000000000001502L, "Update Booking",
"UPDATE_BOOKING", "UPDATE_BOOKING", "PENDING_CONFIRM", 1);
mockMvc.perform(get("/api/reservation/tasks")
.param("hotel_id", HOTEL_ID)
.param("keyword", "GRP-FRONTEND-TASK-SORT-")
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[0].task_id").value(latestTaskId.toString()))
.andExpect(jsonPath("$.items[0].source_subject").value("Frontend Query Task Sort Latest"))
.andExpect(jsonPath("$.items[1].task_id").value(oldTaskId.toString()))
.andExpect(jsonPath("$.items[1].source_subject").value("Frontend Query Task Sort Old"))
.andExpect(jsonPath("$.page.total").value(2));
}
@Test
void shouldReturnOrderListWithLatestTaskActivityFirst() throws Exception {
SourceMessageCaptureResult oldSource = captureSourceMessage(
"mail-frontend-order-sort-old-001",
"Frontend Query Order Sort Old",
Instant.parse("2026-07-08T01:00:00Z"));
SourceMessageCaptureResult latestSource = captureSourceMessage(
"mail-frontend-order-sort-latest-001",
"Frontend Query Order Sort Latest",
Instant.parse("2026-07-10T09:30:00Z"));
Long staleButRecentlyUpdatedOrderId = 930000000000001701L;
Long latestActivityOrderId = 930000000000001702L;
Long staleTaskId = 930000000000001901L;
Long latestTaskId = 930000000000001902L;
insertGroupOrder(staleButRecentlyUpdatedOrderId, oldSource.inboxId(),
"GRP-FRONTEND-ORDER-SORT-STALE", "ACTIVE");
insertGroupOrder(latestActivityOrderId, latestSource.inboxId(),
"GRP-FRONTEND-ORDER-SORT-LATEST", "ACTIVE");
updateOrderUpdatedAt(staleButRecentlyUpdatedOrderId, Instant.parse("2026-07-09T00:00:00Z"));
updateOrderUpdatedAt(latestActivityOrderId, Instant.parse("2026-07-08T02:00:00Z"));
insertTransition(930000000000001801L, oldSource.inboxId(), 1, "GRP-FRONTEND-ORDER-SORT-STALE",
"New Booking", "NEW_BOOKING", "NEW_BOOKING");
insertTransition(930000000000001802L, latestSource.inboxId(), 1, "GRP-FRONTEND-ORDER-SORT-LATEST",
"Update Booking", "UPDATE_BOOKING", "UPDATE_BOOKING");
insertTask(staleTaskId, staleButRecentlyUpdatedOrderId, oldSource.inboxId(), 930000000000001801L,
"New Booking", "NEW_BOOKING", "NEW_BOOKING", "PENDING_CONFIRM", 1);
insertTask(latestTaskId, latestActivityOrderId, latestSource.inboxId(), 930000000000001802L,
"Update Booking", "UPDATE_BOOKING", "UPDATE_BOOKING", "PENDING_CONFIRM", 1);
updateOrderLatestActivityAt(staleButRecentlyUpdatedOrderId, Instant.parse("2026-07-09T00:00:00Z"));
updateOrderLatestActivityAt(latestActivityOrderId, Instant.parse("2026-07-10T09:30:00Z"));
mockMvc.perform(get("/api/reservation/orders")
.param("hotel_id", HOTEL_ID)
.param("keyword", "GRP-FRONTEND-ORDER-SORT-")
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[0].order_id").value(latestActivityOrderId.toString()))
.andExpect(jsonPath("$.items[0].group_code").value("GRP-FRONTEND-ORDER-SORT-LATEST"))
.andExpect(jsonPath("$.items[1].order_id").value(staleButRecentlyUpdatedOrderId.toString()))
.andExpect(jsonPath("$.items[1].group_code").value("GRP-FRONTEND-ORDER-SORT-STALE"))
.andExpect(jsonPath("$.page.total").value(2));
}
@Test
void shouldTouchOrderLatestActivityWhenRepositoryInsertsTask() {
SourceMessageCaptureResult source = captureSourceMessage(
"mail-frontend-order-activity-touch-001",
"Frontend Query Order Activity Touch",
Instant.parse("2026-07-08T01:00:00Z"));
LocalDateTime orderCreatedAt = LocalDateTime.parse("2026-07-08T01:00:00");
LocalDateTime taskCreatedAt = LocalDateTime.parse("2026-07-10T09:30:00");
ReservationOrderSnapshot order = workflowRepository.insertOrder(new ReservationOrderDraft(
HOTEL_ID,
ReservationOrderKeyType.GROUP_CODE.name(),
"GRP-FRONTEND-ORDER-ACTIVITY-TOUCH",
"GRP-FRONTEND-ORDER-ACTIVITY-TOUCH",
"TMP-FRONTEND-ORDER-ACTIVITY-TOUCH",
ReservationOrderStatus.ACTIVE.name(),
ReservationOrderVisibility.VISIBLE.name(),
"AI_CANDIDATE",
null,
"GRP-FRONTEND-ORDER-ACTIVITY-TOUCH",
source.inboxId(),
orderCreatedAt));
workflowRepository.insertTask(new ReservationTaskDraft(
HOTEL_ID,
order.id(),
source.inboxId(),
930000000000002001L,
"normal_task",
"Update Booking",
"UPDATE_BOOKING",
"UPDATE_BOOKING",
"frontend_query",
ReservationTaskStatus.PENDING_CONFIRM.name(),
true,
1,
null,
null,
false,
null,
taskCreatedAt));
Timestamp latestActivityAt = jdbcTemplate.queryForObject("""
SELECT latest_activity_at
FROM workflow_reservation_order
WHERE id = ?
""", Timestamp.class, order.id());
assertThat(latestActivityAt).isNotNull();
assertThat(latestActivityAt.toLocalDateTime()).isEqualTo(taskCreatedAt);
}
@Test
void shouldReturnOrderDetailWithTaskTimeline() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage(
@@ -272,6 +414,13 @@ class ReservationFrontendQueryControllerTest {
}
private SourceMessageCaptureResult captureSourceMessage(String externalMessageId, String subject) {
return captureSourceMessage(externalMessageId, subject, Instant.parse("2026-07-08T08:00:00Z"));
}
private SourceMessageCaptureResult captureSourceMessage(
String externalMessageId,
String subject,
Instant receivedAt) {
return captureService.capture(new CaptureSourceMessageCommand(
HOTEL_ID,
"AGENTBUS",
@@ -280,7 +429,8 @@ class ReservationFrontendQueryControllerTest {
"thread-" + externalMessageId,
"frame-" + externalMessageId,
"session-frontend-query",
Instant.parse("2026-07-08T08:00:00Z"),
receivedAt,
null,
"guest@example.test",
subject,
"Please handle booking message.",
@@ -291,6 +441,22 @@ class ReservationFrontendQueryControllerTest {
));
}
private void updateOrderUpdatedAt(Long orderId, Instant updatedAt) {
jdbcTemplate.update("""
UPDATE workflow_reservation_order
SET updated_at = ?
WHERE id = ?
""", Timestamp.from(updatedAt), orderId);
}
private void updateOrderLatestActivityAt(Long orderId, Instant latestActivityAt) {
jdbcTemplate.update("""
UPDATE workflow_reservation_order
SET latest_activity_at = ?
WHERE id = ?
""", Timestamp.from(latestActivityAt), orderId);
}
private void insertActiveGroupOrder(Long orderId, Long sourceMessageId, String groupCode) {
insertGroupOrder(orderId, sourceMessageId, groupCode, "ACTIVE");
}
@@ -300,10 +466,10 @@ class ReservationFrontendQueryControllerTest {
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
source_message_id, version, latest_activity_at, created_at, updated_at
)
VALUES (?, ?, 'GROUP_CODE', ?, ?, ?, ?, 'AI_CANDIDATE', ?, ?, 0,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", orderId, HOTEL_ID, groupCode, groupCode, "TMP-" + orderId, orderStatus, groupCode, sourceMessageId);
}