实现M002 V4持久化基线

This commit is contained in:
andy
2026-07-19 00:12:40 +07:00
parent 3d9055dd6b
commit e6fbd7a111
29 changed files with 1563 additions and 33 deletions

View File

@@ -0,0 +1,36 @@
package cn.nianxx.thhotel.workflows.reservation.common.dto;
import java.time.LocalDateTime;
/**
* Reservation V4 订单任务创建草稿。用于按 source_message_id + order_ref 幂等创建订单任务聚合。
*/
public record ReservationV4OrderTaskDraft(
/** 酒店 ID。 */
String hotelId,
/** SourceMessage Inbox 内部 ID。 */
Long sourceMessageId,
/** AI 回调批次 ID。 */
Long aiBatchId,
/** V4 包内订单引用。 */
String orderRef,
/** order_contexts[] 中的一基序号。 */
Integer orderContextIndex,
/** 绑定的本地订单投影 ID归属未解决时可为空。 */
Long orderId,
/** 目标订单业务类型,例如 GROUP、FIT。 */
String targetBookingType,
/** 目标订单定位类型。 */
String targetLocatorType,
/** 目标订单定位值。 */
String targetLocatorValue,
/** 订单归属解析状态。 */
String targetResolutionStatus,
/** 订单任务状态快照。 */
String orderTaskStatus,
/** 来源邮件接收 UTC 时间。 */
LocalDateTime sourceReceivedAt,
/** 当前写入 UTC 时间。 */
LocalDateTime now
) {
}

View File

@@ -0,0 +1,42 @@
package cn.nianxx.thhotel.workflows.reservation.common.dto;
import java.time.LocalDateTime;
/**
* Reservation V4 订单任务快照。屏蔽 Entity 细节供 Service 和后续查询接口使用。
*/
public record ReservationV4OrderTaskSnapshot(
/** V4 订单任务 ID。 */
Long id,
/** 酒店 ID。 */
String hotelId,
/** SourceMessage Inbox 内部 ID。 */
Long sourceMessageId,
/** AI 回调批次 ID。 */
Long aiBatchId,
/** V4 包内订单引用。 */
String orderRef,
/** order_contexts[] 中的一基序号。 */
Integer orderContextIndex,
/** 绑定的本地订单投影 ID归属未解决时可为空。 */
Long orderId,
/** 目标订单业务类型。 */
String targetBookingType,
/** 目标订单定位类型。 */
String targetLocatorType,
/** 目标订单定位值。 */
String targetLocatorValue,
/** 订单归属解析状态。 */
String targetResolutionStatus,
/** 订单任务状态快照。 */
String orderTaskStatus,
/** 来源邮件接收 UTC 时间。 */
LocalDateTime sourceReceivedAt,
/** 乐观锁版本。 */
Long version,
/** 记录创建 UTC 时间。 */
LocalDateTime createdAt,
/** 记录更新 UTC 时间。 */
LocalDateTime updatedAt
) {
}

View File

@@ -0,0 +1,28 @@
package cn.nianxx.thhotel.workflows.reservation.common.dto;
import java.time.LocalDateTime;
/**
* Reservation V4 来源通知创建草稿。第一版用于 S10 纯通知入库。
*/
public record ReservationV4SourceNotificationDraft(
/** 酒店 ID。 */
String hotelId,
/** SourceMessage Inbox 内部 ID。 */
Long sourceMessageId,
/** AI 回调批次 ID。 */
Long aiBatchId,
/** AI transition IDS10 第一版可为空。 */
Long aiTransitionId,
/** 包级路由码,第一版用于 S10。 */
String routeCode,
/** 通知状态。 */
String notificationStatus,
/** 来源通知原始 AI 片段或包级摘要 JSON。 */
String rawPayloadJson,
/** 来源邮件接收 UTC 时间。 */
LocalDateTime sourceReceivedAt,
/** 当前写入 UTC 时间。 */
LocalDateTime now
) {
}

View File

@@ -0,0 +1,38 @@
package cn.nianxx.thhotel.workflows.reservation.common.dto;
import java.time.LocalDateTime;
/**
* Reservation V4 来源通知快照。用于后续 S10 通知详情和确认接口。
*/
public record ReservationV4SourceNotificationSnapshot(
/** V4 来源通知 ID。 */
Long id,
/** 酒店 ID。 */
String hotelId,
/** SourceMessage Inbox 内部 ID。 */
Long sourceMessageId,
/** AI 回调批次 ID。 */
Long aiBatchId,
/** AI transition IDS10 第一版可为空。 */
Long aiTransitionId,
/** 包级路由码。 */
String routeCode,
/** 通知状态。 */
String notificationStatus,
/** 来源通知原始 AI 片段或包级摘要 JSON。 */
String rawPayloadJson,
/** 确认处理人标识。 */
String ackBy,
/** 确认处理 UTC 时间。 */
LocalDateTime ackAt,
/** 来源邮件接收 UTC 时间。 */
LocalDateTime sourceReceivedAt,
/** 乐观锁版本。 */
Long version,
/** 记录创建 UTC 时间。 */
LocalDateTime createdAt,
/** 记录更新 UTC 时间。 */
LocalDateTime updatedAt
) {
}

View File

@@ -0,0 +1,38 @@
package cn.nianxx.thhotel.workflows.reservation.common.dto;
import java.time.LocalDateTime;
/**
* Reservation V4 任务卡创建草稿。非 event 卡 source_event_index 允许为空,由 Repository 固定写入 0。
*/
public record ReservationV4TaskCardDraft(
/** 酒店 ID。 */
String hotelId,
/** V4 订单任务 ID。 */
Long v4OrderTaskId,
/** SourceMessage Inbox 内部 ID。 */
Long sourceMessageId,
/** AI transition ID非 event 卡可为空。 */
Long aiTransitionId,
/** 卡片类型。 */
String cardType,
/** 业务事件类型,非 event 卡为空。 */
String eventType,
/** message_events[] 中的一基序号;非 event 卡为空并写入 0。 */
Integer sourceEventIndex,
/** 同一订单任务内卡片排序。 */
Integer cardSortOrder,
/** 卡片状态。 */
String cardStatus,
/** 人工复核状态,非复核卡为空。 */
String reviewStatus,
/** AI 原始卡片 payload JSON 快照。 */
String aiPayloadJson,
/** 前端展示用 payload JSON 快照。 */
String displayPayloadJson,
/** 第一版后端校验错误 JSON。 */
String validationErrorsJson,
/** 当前写入 UTC 时间。 */
LocalDateTime now
) {
}

View File

@@ -0,0 +1,52 @@
package cn.nianxx.thhotel.workflows.reservation.common.dto;
import java.time.LocalDateTime;
/**
* Reservation V4 任务卡快照。保存 AI 原始卡、展示卡、确认卡和复核结果的当前数据库视图。
*/
public record ReservationV4TaskCardSnapshot(
/** V4 任务卡 ID。 */
Long id,
/** 酒店 ID。 */
String hotelId,
/** V4 订单任务 ID。 */
Long v4OrderTaskId,
/** SourceMessage Inbox 内部 ID。 */
Long sourceMessageId,
/** AI transition ID非 event 卡可为空。 */
Long aiTransitionId,
/** 卡片类型。 */
String cardType,
/** 业务事件类型,非 event 卡为空。 */
String eventType,
/** message_events[] 中的一基序号;非 event 卡固定为 0。 */
Integer sourceEventIndex,
/** 同一订单任务内卡片排序。 */
Integer cardSortOrder,
/** 卡片状态。 */
String cardStatus,
/** 人工复核状态,非复核卡为空。 */
String reviewStatus,
/** AI 原始卡片 payload JSON 快照。 */
String aiPayloadJson,
/** 前端展示用 payload JSON 快照。 */
String displayPayloadJson,
/** 用户最终确认 payload JSON。 */
String confirmedPayloadJson,
/** 人工复核解阻结果 JSON。 */
String reviewResolutionJson,
/** 第一版后端校验错误 JSON。 */
String validationErrorsJson,
/** 最终确认人标识。 */
String confirmedBy,
/** 最终确认 UTC 时间。 */
LocalDateTime confirmedAt,
/** 乐观锁版本。 */
Long version,
/** 记录创建 UTC 时间。 */
LocalDateTime createdAt,
/** 记录更新 UTC 时间。 */
LocalDateTime updatedAt
) {
}

View File

@@ -0,0 +1,15 @@
package cn.nianxx.thhotel.workflows.reservation.common.enums;
/**
* Reservation V4 任务卡状态。
*/
public enum ReservationV4CardStatus {
/** 只读展示卡,不允许用户编辑或确认。 */
READONLY,
/** 待用户确认的业务卡。 */
PENDING_CONFIRM,
/** 待人工复核解阻的业务卡。 */
REVIEW_REQUIRED,
/** 已由用户确认完成的业务卡。 */
CONFIRMED
}

View File

@@ -0,0 +1,19 @@
package cn.nianxx.thhotel.workflows.reservation.common.enums;
/**
* Reservation V4 任务卡类型。
*/
public enum ReservationV4CardType {
/** 来源邮件展示卡。 */
SOURCE_MESSAGE_DISPLAY,
/** 订单级 Basic Information 卡。 */
BASIC_INFORMATION,
/** 房型、房晚和房数等客房信息卡。 */
ROOM_INFORMATION,
/** 预订备注追踪卡。 */
TRACE_RESERVATION_NOTES,
/** Rooming List 业务卡。 */
ROOMING_LIST,
/** Payment 业务卡。 */
PAYMENT
}

View File

@@ -0,0 +1,11 @@
package cn.nianxx.thhotel.workflows.reservation.common.enums;
/**
* Reservation V4 来源通知状态。
*/
public enum ReservationV4NotificationStatus {
/** 需要用户确认已读或已处理。 */
ACK_REQUIRED,
/** 用户已经确认处理。 */
ACKED
}

View File

@@ -0,0 +1,11 @@
package cn.nianxx.thhotel.workflows.reservation.common.enums;
/**
* Reservation V4 订单任务状态快照。
*/
public enum ReservationV4OrderTaskStatus {
/** 订单任务仍有待处理卡片。 */
OPEN,
/** 订单任务下所有需要处理的卡片均已完成。 */
COMPLETED
}

View File

@@ -0,0 +1,13 @@
package cn.nianxx.thhotel.workflows.reservation.common.enums;
/**
* Reservation V4 订单归属解析状态。
*/
public enum ReservationV4TargetResolutionStatus {
/** 已明确匹配到目标订单或可创建的新订单上下文。 */
RESOLVED,
/** 缺少足够信息,等待人工确认订单归属。 */
UNRESOLVED,
/** 匹配到多个候选订单,需要人工处理冲突。 */
CONFLICT
}

View File

@@ -0,0 +1,88 @@
package cn.nianxx.thhotel.workflows.reservation.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.LocalDateTime;
/**
* Reservation V4 订单任务实体。承载同一 SourceMessage + order_ref 下的订单任务聚合。
*/
@TableName("workflow_reservation_v4_order_task")
public class ReservationV4OrderTaskEntity {
/** V4 订单任务 ID。 */
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 酒店 ID。 */
private String hotelId;
/** SourceMessage Inbox 内部 ID。 */
private Long sourceMessageId;
/** AI 回调批次 ID。 */
private Long aiBatchId;
/** V4 包内订单引用。 */
private String orderRef;
/** order_contexts[] 中的一基序号。 */
private Integer orderContextIndex;
/** 绑定的本地订单投影 ID归属未解决时可为空。 */
private Long orderId;
/** 目标订单业务类型,例如 GROUP、FIT。 */
private String targetBookingType;
/** 目标订单定位类型。 */
private String targetLocatorType;
/** 目标订单定位值。 */
private String targetLocatorValue;
/** 订单归属解析状态。 */
private String targetResolutionStatus;
/** 订单任务状态快照。 */
private String orderTaskStatus;
/** 来源邮件接收 UTC 时间。 */
private LocalDateTime sourceReceivedAt;
/** 乐观锁版本。 */
private Long version;
/** 记录创建 UTC 时间。 */
private LocalDateTime createdAt;
/** 记录更新 UTC 时间。 */
private LocalDateTime updatedAt;
/** 逻辑删除 UTC 时间。 */
private LocalDateTime logicDeletedAt;
/** 逻辑删除原因。 */
private String logicDeletedReason;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getHotelId() { return hotelId; }
public void setHotelId(String hotelId) { this.hotelId = hotelId; }
public Long getSourceMessageId() { return sourceMessageId; }
public void setSourceMessageId(Long sourceMessageId) { this.sourceMessageId = sourceMessageId; }
public Long getAiBatchId() { return aiBatchId; }
public void setAiBatchId(Long aiBatchId) { this.aiBatchId = aiBatchId; }
public String getOrderRef() { return orderRef; }
public void setOrderRef(String orderRef) { this.orderRef = orderRef; }
public Integer getOrderContextIndex() { return orderContextIndex; }
public void setOrderContextIndex(Integer orderContextIndex) { this.orderContextIndex = orderContextIndex; }
public Long getOrderId() { return orderId; }
public void setOrderId(Long orderId) { this.orderId = orderId; }
public String getTargetBookingType() { return targetBookingType; }
public void setTargetBookingType(String targetBookingType) { this.targetBookingType = targetBookingType; }
public String getTargetLocatorType() { return targetLocatorType; }
public void setTargetLocatorType(String targetLocatorType) { this.targetLocatorType = targetLocatorType; }
public String getTargetLocatorValue() { return targetLocatorValue; }
public void setTargetLocatorValue(String targetLocatorValue) { this.targetLocatorValue = targetLocatorValue; }
public String getTargetResolutionStatus() { return targetResolutionStatus; }
public void setTargetResolutionStatus(String targetResolutionStatus) { this.targetResolutionStatus = targetResolutionStatus; }
public String getOrderTaskStatus() { return orderTaskStatus; }
public void setOrderTaskStatus(String orderTaskStatus) { this.orderTaskStatus = orderTaskStatus; }
public LocalDateTime getSourceReceivedAt() { return sourceReceivedAt; }
public void setSourceReceivedAt(LocalDateTime sourceReceivedAt) { this.sourceReceivedAt = sourceReceivedAt; }
public Long getVersion() { return version; }
public void setVersion(Long version) { this.version = version; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getLogicDeletedAt() { return logicDeletedAt; }
public void setLogicDeletedAt(LocalDateTime logicDeletedAt) { this.logicDeletedAt = logicDeletedAt; }
public String getLogicDeletedReason() { return logicDeletedReason; }
public void setLogicDeletedReason(String logicDeletedReason) { this.logicDeletedReason = logicDeletedReason; }
}

View File

@@ -0,0 +1,80 @@
package cn.nianxx.thhotel.workflows.reservation.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.LocalDateTime;
/**
* Reservation V4 来源通知实体。第一版用于保存 S10 纯通知,不挂订单任务。
*/
@TableName("workflow_reservation_v4_source_notification")
public class ReservationV4SourceNotificationEntity {
/** V4 来源通知 ID。 */
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 酒店 ID。 */
private String hotelId;
/** SourceMessage Inbox 内部 ID。 */
private Long sourceMessageId;
/** AI 回调批次 ID。 */
private Long aiBatchId;
/** AI transition IDS10 第一版可为空。 */
private Long aiTransitionId;
/** 包级路由码。 */
private String routeCode;
/** 通知状态。 */
private String notificationStatus;
/** 来源通知原始 AI 片段或包级摘要 JSON。 */
private String rawPayloadJson;
/** 确认处理人标识。 */
private String ackBy;
/** 确认处理 UTC 时间。 */
private LocalDateTime ackAt;
/** 来源邮件接收 UTC 时间。 */
private LocalDateTime sourceReceivedAt;
/** 乐观锁版本。 */
private Long version;
/** 记录创建 UTC 时间。 */
private LocalDateTime createdAt;
/** 记录更新 UTC 时间。 */
private LocalDateTime updatedAt;
/** 逻辑删除 UTC 时间。 */
private LocalDateTime logicDeletedAt;
/** 逻辑删除原因。 */
private String logicDeletedReason;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getHotelId() { return hotelId; }
public void setHotelId(String hotelId) { this.hotelId = hotelId; }
public Long getSourceMessageId() { return sourceMessageId; }
public void setSourceMessageId(Long sourceMessageId) { this.sourceMessageId = sourceMessageId; }
public Long getAiBatchId() { return aiBatchId; }
public void setAiBatchId(Long aiBatchId) { this.aiBatchId = aiBatchId; }
public Long getAiTransitionId() { return aiTransitionId; }
public void setAiTransitionId(Long aiTransitionId) { this.aiTransitionId = aiTransitionId; }
public String getRouteCode() { return routeCode; }
public void setRouteCode(String routeCode) { this.routeCode = routeCode; }
public String getNotificationStatus() { return notificationStatus; }
public void setNotificationStatus(String notificationStatus) { this.notificationStatus = notificationStatus; }
public String getRawPayloadJson() { return rawPayloadJson; }
public void setRawPayloadJson(String rawPayloadJson) { this.rawPayloadJson = rawPayloadJson; }
public String getAckBy() { return ackBy; }
public void setAckBy(String ackBy) { this.ackBy = ackBy; }
public LocalDateTime getAckAt() { return ackAt; }
public void setAckAt(LocalDateTime ackAt) { this.ackAt = ackAt; }
public LocalDateTime getSourceReceivedAt() { return sourceReceivedAt; }
public void setSourceReceivedAt(LocalDateTime sourceReceivedAt) { this.sourceReceivedAt = sourceReceivedAt; }
public Long getVersion() { return version; }
public void setVersion(Long version) { this.version = version; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getLogicDeletedAt() { return logicDeletedAt; }
public void setLogicDeletedAt(LocalDateTime logicDeletedAt) { this.logicDeletedAt = logicDeletedAt; }
public String getLogicDeletedReason() { return logicDeletedReason; }
public void setLogicDeletedReason(String logicDeletedReason) { this.logicDeletedReason = logicDeletedReason; }
}

View File

@@ -0,0 +1,108 @@
package cn.nianxx.thhotel.workflows.reservation.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.LocalDateTime;
/**
* Reservation V4 任务卡实体。承载来源邮件展示卡、Basic Information 卡和具体业务卡。
*/
@TableName("workflow_reservation_v4_task_card")
public class ReservationV4TaskCardEntity {
/** V4 任务卡 ID。 */
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 酒店 ID。 */
private String hotelId;
/** V4 订单任务 ID。 */
private Long v4OrderTaskId;
/** SourceMessage Inbox 内部 ID。 */
private Long sourceMessageId;
/** AI transition ID非 event 卡可为空。 */
private Long aiTransitionId;
/** 卡片类型。 */
private String cardType;
/** 业务事件类型,非 event 卡为空。 */
private String eventType;
/** message_events[] 中的一基序号;非 event 卡固定为 0。 */
private Integer sourceEventIndex;
/** 同一订单任务内卡片排序。 */
private Integer cardSortOrder;
/** 卡片状态。 */
private String cardStatus;
/** 人工复核状态,非复核卡为空。 */
private String reviewStatus;
/** AI 原始卡片 payload JSON 快照。 */
private String aiPayloadJson;
/** 前端展示用 payload JSON 快照。 */
private String displayPayloadJson;
/** 用户最终确认 payload JSON。 */
private String confirmedPayloadJson;
/** 人工复核解阻结果 JSON。 */
private String reviewResolutionJson;
/** 第一版后端校验错误 JSON。 */
private String validationErrorsJson;
/** 最终确认人标识。 */
private String confirmedBy;
/** 最终确认 UTC 时间。 */
private LocalDateTime confirmedAt;
/** 乐观锁版本。 */
private Long version;
/** 记录创建 UTC 时间。 */
private LocalDateTime createdAt;
/** 记录更新 UTC 时间。 */
private LocalDateTime updatedAt;
/** 逻辑删除 UTC 时间。 */
private LocalDateTime logicDeletedAt;
/** 逻辑删除原因。 */
private String logicDeletedReason;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getHotelId() { return hotelId; }
public void setHotelId(String hotelId) { this.hotelId = hotelId; }
public Long getV4OrderTaskId() { return v4OrderTaskId; }
public void setV4OrderTaskId(Long v4OrderTaskId) { this.v4OrderTaskId = v4OrderTaskId; }
public Long getSourceMessageId() { return sourceMessageId; }
public void setSourceMessageId(Long sourceMessageId) { this.sourceMessageId = sourceMessageId; }
public Long getAiTransitionId() { return aiTransitionId; }
public void setAiTransitionId(Long aiTransitionId) { this.aiTransitionId = aiTransitionId; }
public String getCardType() { return cardType; }
public void setCardType(String cardType) { this.cardType = cardType; }
public String getEventType() { return eventType; }
public void setEventType(String eventType) { this.eventType = eventType; }
public Integer getSourceEventIndex() { return sourceEventIndex; }
public void setSourceEventIndex(Integer sourceEventIndex) { this.sourceEventIndex = sourceEventIndex; }
public Integer getCardSortOrder() { return cardSortOrder; }
public void setCardSortOrder(Integer cardSortOrder) { this.cardSortOrder = cardSortOrder; }
public String getCardStatus() { return cardStatus; }
public void setCardStatus(String cardStatus) { this.cardStatus = cardStatus; }
public String getReviewStatus() { return reviewStatus; }
public void setReviewStatus(String reviewStatus) { this.reviewStatus = reviewStatus; }
public String getAiPayloadJson() { return aiPayloadJson; }
public void setAiPayloadJson(String aiPayloadJson) { this.aiPayloadJson = aiPayloadJson; }
public String getDisplayPayloadJson() { return displayPayloadJson; }
public void setDisplayPayloadJson(String displayPayloadJson) { this.displayPayloadJson = displayPayloadJson; }
public String getConfirmedPayloadJson() { return confirmedPayloadJson; }
public void setConfirmedPayloadJson(String confirmedPayloadJson) { this.confirmedPayloadJson = confirmedPayloadJson; }
public String getReviewResolutionJson() { return reviewResolutionJson; }
public void setReviewResolutionJson(String reviewResolutionJson) { this.reviewResolutionJson = reviewResolutionJson; }
public String getValidationErrorsJson() { return validationErrorsJson; }
public void setValidationErrorsJson(String validationErrorsJson) { this.validationErrorsJson = validationErrorsJson; }
public String getConfirmedBy() { return confirmedBy; }
public void setConfirmedBy(String confirmedBy) { this.confirmedBy = confirmedBy; }
public LocalDateTime getConfirmedAt() { return confirmedAt; }
public void setConfirmedAt(LocalDateTime confirmedAt) { this.confirmedAt = confirmedAt; }
public Long getVersion() { return version; }
public void setVersion(Long version) { this.version = version; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getLogicDeletedAt() { return logicDeletedAt; }
public void setLogicDeletedAt(LocalDateTime logicDeletedAt) { this.logicDeletedAt = logicDeletedAt; }
public String getLogicDeletedReason() { return logicDeletedReason; }
public void setLogicDeletedReason(String logicDeletedReason) { this.logicDeletedReason = logicDeletedReason; }
}

View File

@@ -0,0 +1,12 @@
package cn.nianxx.thhotel.workflows.reservation.mapper;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationV4OrderTaskEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* Reservation V4 订单任务 Mapper。自带 CRUD 由 MyBatis-Plus 提供。
*/
@Mapper
public interface ReservationV4OrderTaskMapper extends BaseMapper<ReservationV4OrderTaskEntity> {
}

View File

@@ -0,0 +1,12 @@
package cn.nianxx.thhotel.workflows.reservation.mapper;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationV4SourceNotificationEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* Reservation V4 来源通知 Mapper。自带 CRUD 由 MyBatis-Plus 提供。
*/
@Mapper
public interface ReservationV4SourceNotificationMapper extends BaseMapper<ReservationV4SourceNotificationEntity> {
}

View File

@@ -0,0 +1,12 @@
package cn.nianxx.thhotel.workflows.reservation.mapper;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationV4TaskCardEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* Reservation V4 任务卡 Mapper。自带 CRUD 由 MyBatis-Plus 提供。
*/
@Mapper
public interface ReservationV4TaskCardMapper extends BaseMapper<ReservationV4TaskCardEntity> {
}

View File

@@ -0,0 +1,157 @@
package cn.nianxx.thhotel.workflows.reservation.repository;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationSnapshot;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationV4SourceNotificationEntity;
import cn.nianxx.thhotel.workflows.reservation.mapper.ReservationV4SourceNotificationMapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Repository;
/**
* Reservation V4 来源通知 MyBatis-Plus 持久化实现。
*/
@Repository
public class MybatisReservationV4SourceNotificationRepository implements ReservationV4SourceNotificationRepository {
private final ReservationV4SourceNotificationMapper sourceNotificationMapper;
/**
* 注入来源通知 MapperRepository 负责幂等和快照转换。
*/
public MybatisReservationV4SourceNotificationRepository(
ReservationV4SourceNotificationMapper sourceNotificationMapper) {
this.sourceNotificationMapper = sourceNotificationMapper;
}
/**
* 按来源通知 ID 查询当前快照。
*/
@Override
public Optional<ReservationV4SourceNotificationSnapshot> findSourceNotificationById(
String hotelId,
Long notificationId) {
ReservationV4SourceNotificationEntity entity = sourceNotificationMapper.selectOne(
Wrappers.<ReservationV4SourceNotificationEntity>lambdaQuery()
.eq(ReservationV4SourceNotificationEntity::getHotelId, hotelId)
.eq(ReservationV4SourceNotificationEntity::getId, notificationId)
.isNull(ReservationV4SourceNotificationEntity::getLogicDeletedAt)
.last("LIMIT 1"));
return Optional.ofNullable(entity).map(this::toSnapshot);
}
/**
* 按 SourceMessage 和 AI 批次查询来源通知,用于幂等判断。
*/
@Override
public Optional<ReservationV4SourceNotificationSnapshot> findSourceNotificationBySourceMessageAndBatch(
String hotelId,
Long sourceMessageId,
Long aiBatchId) {
ReservationV4SourceNotificationEntity entity = sourceNotificationMapper.selectOne(
Wrappers.<ReservationV4SourceNotificationEntity>lambdaQuery()
.eq(ReservationV4SourceNotificationEntity::getHotelId, hotelId)
.eq(ReservationV4SourceNotificationEntity::getSourceMessageId, sourceMessageId)
.eq(ReservationV4SourceNotificationEntity::getAiBatchId, aiBatchId)
.isNull(ReservationV4SourceNotificationEntity::getLogicDeletedAt)
.last("LIMIT 1"));
return Optional.ofNullable(entity).map(this::toSnapshot);
}
/**
* 按 SourceMessage 查询来源通知列表,用于后续邮件通知详情和工作台展示。
*/
@Override
public List<ReservationV4SourceNotificationSnapshot> findSourceNotificationsBySourceMessageId(
String hotelId,
Long sourceMessageId) {
return sourceNotificationMapper.selectList(Wrappers.<ReservationV4SourceNotificationEntity>lambdaQuery()
.eq(ReservationV4SourceNotificationEntity::getHotelId, hotelId)
.eq(ReservationV4SourceNotificationEntity::getSourceMessageId, sourceMessageId)
.isNull(ReservationV4SourceNotificationEntity::getLogicDeletedAt)
.orderByAsc(ReservationV4SourceNotificationEntity::getSourceReceivedAt)
.orderByAsc(ReservationV4SourceNotificationEntity::getId))
.stream()
.map(this::toSnapshot)
.toList();
}
/**
* 幂等创建 S10 来源通知;并发重复插入时返回已存在记录。
*/
@Override
public ReservationV4SourceNotificationSnapshot findOrCreateSourceNotification(
ReservationV4SourceNotificationDraft draft) {
Optional<ReservationV4SourceNotificationSnapshot> existing =
findSourceNotificationBySourceMessageAndBatch(draft.hotelId(), draft.sourceMessageId(), draft.aiBatchId());
if (existing.isPresent()) {
return existing.get();
}
ReservationV4SourceNotificationEntity entity = new ReservationV4SourceNotificationEntity();
entity.setHotelId(draft.hotelId());
entity.setSourceMessageId(draft.sourceMessageId());
entity.setAiBatchId(draft.aiBatchId());
entity.setAiTransitionId(draft.aiTransitionId());
entity.setRouteCode(draft.routeCode());
entity.setNotificationStatus(draft.notificationStatus());
entity.setRawPayloadJson(draft.rawPayloadJson());
entity.setSourceReceivedAt(draft.sourceReceivedAt());
entity.setVersion(0L);
entity.setCreatedAt(draft.now());
entity.setUpdatedAt(draft.now());
try {
sourceNotificationMapper.insert(entity);
return toSnapshot(entity);
} catch (DuplicateKeyException ex) {
return findSourceNotificationBySourceMessageAndBatch(
draft.hotelId(),
draft.sourceMessageId(),
draft.aiBatchId()).orElseThrow(() -> ex);
}
}
/**
* 按 version 乐观锁更新来源通知状态,常用于 S10 确认已读 / 已处理。
*/
@Override
public boolean updateNotificationStatusWithVersion(
String hotelId,
Long notificationId,
Long expectedVersion,
String notificationStatus,
String ackBy,
LocalDateTime ackAt) {
int updated = sourceNotificationMapper.update(Wrappers.<ReservationV4SourceNotificationEntity>lambdaUpdate()
.set(ReservationV4SourceNotificationEntity::getNotificationStatus, notificationStatus)
.set(ReservationV4SourceNotificationEntity::getAckBy, ackBy)
.set(ReservationV4SourceNotificationEntity::getAckAt, ackAt)
.set(ReservationV4SourceNotificationEntity::getUpdatedAt, ackAt)
.setSql("version = version + 1")
.eq(ReservationV4SourceNotificationEntity::getHotelId, hotelId)
.eq(ReservationV4SourceNotificationEntity::getId, notificationId)
.eq(ReservationV4SourceNotificationEntity::getVersion, expectedVersion)
.isNull(ReservationV4SourceNotificationEntity::getLogicDeletedAt));
return updated == 1;
}
private ReservationV4SourceNotificationSnapshot toSnapshot(ReservationV4SourceNotificationEntity entity) {
return new ReservationV4SourceNotificationSnapshot(
entity.getId(),
entity.getHotelId(),
entity.getSourceMessageId(),
entity.getAiBatchId(),
entity.getAiTransitionId(),
entity.getRouteCode(),
entity.getNotificationStatus(),
entity.getRawPayloadJson(),
entity.getAckBy(),
entity.getAckAt(),
entity.getSourceReceivedAt(),
entity.getVersion(),
entity.getCreatedAt(),
entity.getUpdatedAt());
}
}

View File

@@ -0,0 +1,274 @@
package cn.nianxx.thhotel.workflows.reservation.repository;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardSnapshot;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationV4OrderTaskEntity;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationV4TaskCardEntity;
import cn.nianxx.thhotel.workflows.reservation.mapper.ReservationV4OrderTaskMapper;
import cn.nianxx.thhotel.workflows.reservation.mapper.ReservationV4TaskCardMapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Repository;
/**
* Reservation V4 订单任务和多卡 MyBatis-Plus 持久化实现。
*/
@Repository
public class MybatisReservationV4WorkflowRepository implements ReservationV4WorkflowRepository {
private static final int NON_EVENT_SOURCE_EVENT_INDEX = 0;
private final ReservationV4OrderTaskMapper orderTaskMapper;
private final ReservationV4TaskCardMapper taskCardMapper;
/**
* 注入 V4 MapperRepository 负责幂等、乐观锁和快照转换。
*/
public MybatisReservationV4WorkflowRepository(
ReservationV4OrderTaskMapper orderTaskMapper,
ReservationV4TaskCardMapper taskCardMapper) {
this.orderTaskMapper = orderTaskMapper;
this.taskCardMapper = taskCardMapper;
}
/**
* 按 SourceMessage 和 order_ref 查询 V4 订单任务,用于幂等创建和回调重放判断。
*/
@Override
public Optional<ReservationV4OrderTaskSnapshot> findOrderTaskBySourceMessageAndOrderRef(
String hotelId,
Long sourceMessageId,
String orderRef) {
ReservationV4OrderTaskEntity entity = orderTaskMapper.selectOne(
Wrappers.<ReservationV4OrderTaskEntity>lambdaQuery()
.eq(ReservationV4OrderTaskEntity::getHotelId, hotelId)
.eq(ReservationV4OrderTaskEntity::getSourceMessageId, sourceMessageId)
.eq(ReservationV4OrderTaskEntity::getOrderRef, orderRef)
.isNull(ReservationV4OrderTaskEntity::getLogicDeletedAt)
.last("LIMIT 1"));
return Optional.ofNullable(entity).map(this::toOrderTaskSnapshot);
}
/**
* 按订单任务 ID 查询 V4 订单任务。
*/
@Override
public Optional<ReservationV4OrderTaskSnapshot> findOrderTaskById(String hotelId, Long orderTaskId) {
ReservationV4OrderTaskEntity entity = orderTaskMapper.selectOne(
Wrappers.<ReservationV4OrderTaskEntity>lambdaQuery()
.eq(ReservationV4OrderTaskEntity::getHotelId, hotelId)
.eq(ReservationV4OrderTaskEntity::getId, orderTaskId)
.isNull(ReservationV4OrderTaskEntity::getLogicDeletedAt)
.last("LIMIT 1"));
return Optional.ofNullable(entity).map(this::toOrderTaskSnapshot);
}
/**
* 按 SourceMessage 查询全部 V4 订单任务,保留同一邮件内多个 order_context 的顺序。
*/
@Override
public List<ReservationV4OrderTaskSnapshot> findOrderTasksBySourceMessageId(String hotelId, Long sourceMessageId) {
return orderTaskMapper.selectList(Wrappers.<ReservationV4OrderTaskEntity>lambdaQuery()
.eq(ReservationV4OrderTaskEntity::getHotelId, hotelId)
.eq(ReservationV4OrderTaskEntity::getSourceMessageId, sourceMessageId)
.isNull(ReservationV4OrderTaskEntity::getLogicDeletedAt)
.orderByAsc(ReservationV4OrderTaskEntity::getOrderContextIndex)
.orderByAsc(ReservationV4OrderTaskEntity::getId))
.stream()
.map(this::toOrderTaskSnapshot)
.toList();
}
/**
* 幂等创建 V4 订单任务;唯一键冲突时返回已存在记录。
*/
@Override
public ReservationV4OrderTaskSnapshot findOrCreateOrderTask(ReservationV4OrderTaskDraft draft) {
Optional<ReservationV4OrderTaskSnapshot> existing = findOrderTaskBySourceMessageAndOrderRef(
draft.hotelId(),
draft.sourceMessageId(),
draft.orderRef());
if (existing.isPresent()) {
return existing.get();
}
ReservationV4OrderTaskEntity entity = new ReservationV4OrderTaskEntity();
entity.setHotelId(draft.hotelId());
entity.setSourceMessageId(draft.sourceMessageId());
entity.setAiBatchId(draft.aiBatchId());
entity.setOrderRef(draft.orderRef());
entity.setOrderContextIndex(requirePositiveOrderContextIndex(draft.orderContextIndex()));
entity.setOrderId(draft.orderId());
entity.setTargetBookingType(draft.targetBookingType());
entity.setTargetLocatorType(draft.targetLocatorType());
entity.setTargetLocatorValue(draft.targetLocatorValue());
entity.setTargetResolutionStatus(draft.targetResolutionStatus());
entity.setOrderTaskStatus(draft.orderTaskStatus());
entity.setSourceReceivedAt(draft.sourceReceivedAt());
entity.setVersion(0L);
entity.setCreatedAt(draft.now());
entity.setUpdatedAt(draft.now());
try {
orderTaskMapper.insert(entity);
return toOrderTaskSnapshot(entity);
} catch (DuplicateKeyException ex) {
return findOrderTaskBySourceMessageAndOrderRef(
draft.hotelId(),
draft.sourceMessageId(),
draft.orderRef()).orElseThrow(() -> ex);
}
}
/**
* 创建 V4 任务卡;非 event 卡 source_event_index 固定写入 0。
*/
@Override
public ReservationV4TaskCardSnapshot insertTaskCard(ReservationV4TaskCardDraft draft) {
ReservationV4TaskCardEntity entity = new ReservationV4TaskCardEntity();
entity.setHotelId(draft.hotelId());
entity.setV4OrderTaskId(draft.v4OrderTaskId());
entity.setSourceMessageId(draft.sourceMessageId());
entity.setAiTransitionId(draft.aiTransitionId());
entity.setCardType(draft.cardType());
entity.setEventType(draft.eventType());
entity.setSourceEventIndex(normalizeSourceEventIndex(draft));
entity.setCardSortOrder(draft.cardSortOrder());
entity.setCardStatus(draft.cardStatus());
entity.setReviewStatus(draft.reviewStatus());
entity.setAiPayloadJson(draft.aiPayloadJson());
entity.setDisplayPayloadJson(draft.displayPayloadJson());
entity.setValidationErrorsJson(draft.validationErrorsJson());
entity.setVersion(0L);
entity.setCreatedAt(draft.now());
entity.setUpdatedAt(draft.now());
taskCardMapper.insert(entity);
return toTaskCardSnapshot(entity);
}
/**
* 按任务卡 ID 查询 V4 任务卡。
*/
@Override
public Optional<ReservationV4TaskCardSnapshot> findTaskCardById(String hotelId, Long taskCardId) {
ReservationV4TaskCardEntity entity = taskCardMapper.selectOne(
Wrappers.<ReservationV4TaskCardEntity>lambdaQuery()
.eq(ReservationV4TaskCardEntity::getHotelId, hotelId)
.eq(ReservationV4TaskCardEntity::getId, taskCardId)
.isNull(ReservationV4TaskCardEntity::getLogicDeletedAt)
.last("LIMIT 1"));
return Optional.ofNullable(entity).map(this::toTaskCardSnapshot);
}
/**
* 查询订单任务下全部 V4 任务卡,并按卡片排序和 event index 返回。
*/
@Override
public List<ReservationV4TaskCardSnapshot> findTaskCardsByOrderTaskId(String hotelId, Long orderTaskId) {
return taskCardMapper.selectList(Wrappers.<ReservationV4TaskCardEntity>lambdaQuery()
.eq(ReservationV4TaskCardEntity::getHotelId, hotelId)
.eq(ReservationV4TaskCardEntity::getV4OrderTaskId, orderTaskId)
.isNull(ReservationV4TaskCardEntity::getLogicDeletedAt)
.orderByAsc(ReservationV4TaskCardEntity::getCardSortOrder)
.orderByAsc(ReservationV4TaskCardEntity::getSourceEventIndex)
.orderByAsc(ReservationV4TaskCardEntity::getId))
.stream()
.map(this::toTaskCardSnapshot)
.toList();
}
/**
* 按 version 乐观锁更新 V4 任务卡状态。
*/
@Override
public boolean updateTaskCardStatusWithVersion(
String hotelId,
Long taskCardId,
Long expectedVersion,
String cardStatus,
LocalDateTime now) {
int updated = taskCardMapper.update(Wrappers.<ReservationV4TaskCardEntity>lambdaUpdate()
.set(ReservationV4TaskCardEntity::getCardStatus, cardStatus)
.set(ReservationV4TaskCardEntity::getUpdatedAt, now)
.setSql("version = version + 1")
.eq(ReservationV4TaskCardEntity::getHotelId, hotelId)
.eq(ReservationV4TaskCardEntity::getId, taskCardId)
.eq(ReservationV4TaskCardEntity::getVersion, expectedVersion)
.isNull(ReservationV4TaskCardEntity::getLogicDeletedAt));
return updated == 1;
}
private ReservationV4OrderTaskSnapshot toOrderTaskSnapshot(ReservationV4OrderTaskEntity entity) {
return new ReservationV4OrderTaskSnapshot(
entity.getId(),
entity.getHotelId(),
entity.getSourceMessageId(),
entity.getAiBatchId(),
entity.getOrderRef(),
entity.getOrderContextIndex(),
entity.getOrderId(),
entity.getTargetBookingType(),
entity.getTargetLocatorType(),
entity.getTargetLocatorValue(),
entity.getTargetResolutionStatus(),
entity.getOrderTaskStatus(),
entity.getSourceReceivedAt(),
entity.getVersion(),
entity.getCreatedAt(),
entity.getUpdatedAt());
}
private Integer requirePositiveOrderContextIndex(Integer orderContextIndex) {
if (orderContextIndex == null || orderContextIndex <= 0) {
throw new IllegalArgumentException("V4 order_context_index 必须是一基正整数。");
}
return orderContextIndex;
}
private Integer normalizeSourceEventIndex(ReservationV4TaskCardDraft draft) {
boolean eventCard = draft.aiTransitionId() != null || hasText(draft.eventType());
Integer sourceEventIndex = draft.sourceEventIndex();
if (eventCard) {
if (sourceEventIndex == null || sourceEventIndex <= 0) {
throw new IllegalArgumentException("V4 event 卡 source_event_index 必须是一基正整数。");
}
return sourceEventIndex;
}
if (sourceEventIndex != null && !sourceEventIndex.equals(NON_EVENT_SOURCE_EVENT_INDEX)) {
throw new IllegalArgumentException("V4 非 event 卡 source_event_index 必须固定为 0。");
}
return NON_EVENT_SOURCE_EVENT_INDEX;
}
private boolean hasText(String value) {
return value != null && !value.isBlank();
}
private ReservationV4TaskCardSnapshot toTaskCardSnapshot(ReservationV4TaskCardEntity entity) {
return new ReservationV4TaskCardSnapshot(
entity.getId(),
entity.getHotelId(),
entity.getV4OrderTaskId(),
entity.getSourceMessageId(),
entity.getAiTransitionId(),
entity.getCardType(),
entity.getEventType(),
entity.getSourceEventIndex(),
entity.getCardSortOrder(),
entity.getCardStatus(),
entity.getReviewStatus(),
entity.getAiPayloadJson(),
entity.getDisplayPayloadJson(),
entity.getConfirmedPayloadJson(),
entity.getReviewResolutionJson(),
entity.getValidationErrorsJson(),
entity.getConfirmedBy(),
entity.getConfirmedAt(),
entity.getVersion(),
entity.getCreatedAt(),
entity.getUpdatedAt());
}
}

View File

@@ -0,0 +1,51 @@
package cn.nianxx.thhotel.workflows.reservation.repository;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationSnapshot;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* Reservation V4 来源通知持久化边界。第一版用于 S10 纯通知的幂等创建、查询和确认。
*/
public interface ReservationV4SourceNotificationRepository {
/**
* 按来源通知 ID 查询当前快照。
*/
Optional<ReservationV4SourceNotificationSnapshot> findSourceNotificationById(
String hotelId,
Long notificationId);
/**
* 按 SourceMessage 和 AI 批次查询来源通知,用于幂等判断。
*/
Optional<ReservationV4SourceNotificationSnapshot> findSourceNotificationBySourceMessageAndBatch(
String hotelId,
Long sourceMessageId,
Long aiBatchId);
/**
* 按 SourceMessage 查询来源通知列表,用于后续邮件通知详情和工作台展示。
*/
List<ReservationV4SourceNotificationSnapshot> findSourceNotificationsBySourceMessageId(
String hotelId,
Long sourceMessageId);
/**
* 幂等创建 S10 来源通知;并发重复插入时返回已存在记录。
*/
ReservationV4SourceNotificationSnapshot findOrCreateSourceNotification(ReservationV4SourceNotificationDraft draft);
/**
* 按 version 乐观锁更新来源通知状态,常用于 S10 确认已读 / 已处理。
*/
boolean updateNotificationStatusWithVersion(
String hotelId,
Long notificationId,
Long expectedVersion,
String notificationStatus,
String ackBy,
LocalDateTime ackAt);
}

View File

@@ -0,0 +1,63 @@
package cn.nianxx.thhotel.workflows.reservation.repository;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardSnapshot;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* Reservation V4 订单任务和多卡持久化边界。Service 不直接依赖 Mapper 或 Entity。
*/
public interface ReservationV4WorkflowRepository {
/**
* 按 SourceMessage 和 order_ref 查询 V4 订单任务,用于幂等创建和回调重放判断。
*/
Optional<ReservationV4OrderTaskSnapshot> findOrderTaskBySourceMessageAndOrderRef(
String hotelId,
Long sourceMessageId,
String orderRef);
/**
* 按订单任务 ID 查询 V4 订单任务。
*/
Optional<ReservationV4OrderTaskSnapshot> findOrderTaskById(String hotelId, Long orderTaskId);
/**
* 按 SourceMessage 查询全部 V4 订单任务,保留同一邮件内多个 order_context 的顺序。
*/
List<ReservationV4OrderTaskSnapshot> findOrderTasksBySourceMessageId(String hotelId, Long sourceMessageId);
/**
* 幂等创建 V4 订单任务;唯一键冲突时返回已存在记录。
*/
ReservationV4OrderTaskSnapshot findOrCreateOrderTask(ReservationV4OrderTaskDraft draft);
/**
* 创建 V4 任务卡;非 event 卡 source_event_index 固定写入 0。
*/
ReservationV4TaskCardSnapshot insertTaskCard(ReservationV4TaskCardDraft draft);
/**
* 按任务卡 ID 查询 V4 任务卡。
*/
Optional<ReservationV4TaskCardSnapshot> findTaskCardById(String hotelId, Long taskCardId);
/**
* 查询订单任务下全部 V4 任务卡,并按卡片排序和 event index 返回。
*/
List<ReservationV4TaskCardSnapshot> findTaskCardsByOrderTaskId(String hotelId, Long orderTaskId);
/**
* 按 version 乐观锁更新 V4 任务卡状态。
*/
boolean updateTaskCardStatusWithVersion(
String hotelId,
Long taskCardId,
Long expectedVersion,
String cardStatus,
LocalDateTime now);
}