diff --git a/AGENTS.md b/AGENTS.md index eee16e9..fc0fd56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,6 +93,8 @@ - 生成代码前必须先参考 `AGENTS.md`、当前项目后端规范和既有代码习惯;遇到类职责或目录归属不确定时,先询问再继续,不能先写完再统一重构。 - 不写魔法值,稳定业务代码使用常量或枚举。 - 集合、空值、字符串、时间、金额和 `BigDecimal` 使用安全写法。 +- 后端业务时间点统一按 UTC 处理:数据库 `LocalDateTime` 默认表示 UTC,API 返回时间点字段必须使用带 `Z` 的 ISO 8601 UTC 时间;新增响应 DTO 优先使用 `OffsetDateTime`,从数据库快照输出时使用 `UtcTimeFormatter` 统一转换。 +- 入住日期、离店日期、酒店营业日等酒店本地业务日期不得和 UTC 时间点混用,应使用 `LocalDate` 或明确酒店时区语义的字段。 - 异常不吞掉,日志有上下文但不输出 Secret 或个人敏感信息。 - 写操作考虑幂等、并发版本、事务边界、审计和失败恢复。 - 复杂业务规则、外部字段映射、脱敏和幂等逻辑必须有必要中文注释。 diff --git a/docs/project/backend-development-guidelines.md b/docs/project/backend-development-guidelines.md index b122c93..3da8249 100644 --- a/docs/project/backend-development-guidelines.md +++ b/docs/project/backend-development-guidelines.md @@ -210,6 +210,7 @@ groupCode - 注释必须说明业务含义、来源或代码值范围。 - 禁止使用“字段1”“备用字段”等模糊注释。 - 业务时间点以 UTC 写入数据库,API 层负责返回带 `Z` 的 ISO 8601 UTC 时间,例如 `2026-07-08T03:00:00Z`。 +- 新增 API 响应 DTO 中的时间点字段优先使用 `OffsetDateTime`;从数据库 `LocalDateTime` 快照输出到接口时,应通过 `UtcTimeFormatter` 转换,禁止直接把无时区 `LocalDateTime` 暴露给前端或 SuperAgent。 - 酒店本地业务日期,例如入住日期、离店日期、营业日,优先使用 `LocalDate` 或明确酒店时区语义的字段,不和 UTC 时间点混用。 - JSON 字段只用于扩展元数据,不替代需要查询、约束或索引的正式列。 diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskAvailabilityResolver.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskAvailabilityResolver.java index d148f4d..8049d8c 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskAvailabilityResolver.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskAvailabilityResolver.java @@ -31,6 +31,9 @@ public class ReservationTaskAvailabilityResolver { * 计算任务可处理状态。FAILED 和 COMPLETED 视为前置任务结束,不阻塞后续任务。 */ public ReservationTaskAvailabilityResult calculateAvailability(ReservationTaskSnapshot task) { + if (!Boolean.TRUE.equals(task.queueParticipation())) { + return calculateAvailability(task, null); + } ReservationTaskSnapshot blockingTask = workflowRepository .findQueueTasksBefore(task.hotelId(), task.orderId(), task.executionOrder()) .stream() diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskAvailabilityResolverTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskAvailabilityResolverTest.java new file mode 100644 index 0000000..502d257 --- /dev/null +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskAvailabilityResolverTest.java @@ -0,0 +1,60 @@ +package cn.nianxx.thhotel.workflows.reservation.service.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskSnapshot; +import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationTaskStatus; +import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskAvailabilityResult; +import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ReservationTaskAvailabilityResolverTest { + + @Test + void shouldReturnReadOnlyForNonQueueTaskWithoutQueryingPreviousTasks() { + ReservationAiWorkflowRepository workflowRepository = mock(ReservationAiWorkflowRepository.class); + when(workflowRepository.findQueueTasksBefore(anyString(), anyLong(), nullable(Integer.class))) + .thenReturn(List.of()); + ReservationTaskAvailabilityResolver resolver = new ReservationTaskAvailabilityResolver(workflowRepository); + ReservationTaskSnapshot nonQueueTask = task(false, null, ReservationTaskStatus.PENDING_CONFIRM.name()); + + ReservationTaskAvailabilityResult result = resolver.calculateAvailability(nonQueueTask); + + assertThat(result.blocked()).isFalse(); + assertThat(result.readOnly()).isTrue(); + assertThat(result.editable()).isFalse(); + assertThat(result.confirmable()).isFalse(); + assertThat(result.executable()).isFalse(); + verify(workflowRepository, never()).findQueueTasksBefore(anyString(), anyLong(), nullable(Integer.class)); + } + + private ReservationTaskSnapshot task(Boolean queueParticipation, Integer executionOrder, String taskStatus) { + return new ReservationTaskSnapshot( + 930000000000001001L, + "HOTEL-TEST", + 930000000000001101L, + 930000000000001201L, + 930000000000001301L, + "message_notification", + "Message Notification", + "MESSAGE_NOTIFICATION", + "MESSAGE_NOTIFICATION", + "message_notification", + taskStatus, + queueParticipation, + executionOrder, + null, + null, + null, + false, + null); + } +}