实现 M002 CP5 任务确认与 OPERA 模拟骨架

This commit is contained in:
andy
2026-07-07 16:24:17 +08:00
parent f7d77b45bd
commit bd7b791bee
37 changed files with 2548 additions and 57 deletions

View File

@@ -0,0 +1,22 @@
package cn.nianxx.thhotel.workflows.reservation.common.dto;
import java.time.LocalDateTime;
/**
* 审计日志快照。用于后端审计列表接口,不暴露数据库 Entity。
*/
public record ReservationAuditLogSnapshot(
Long id,
String hotelId,
Long orderId,
Long taskId,
Long operationId,
String actorType,
String actorId,
String action,
String reason,
String beforeSnapshotJson,
String afterSnapshotJson,
LocalDateTime occurredAt
) {
}

View File

@@ -0,0 +1,21 @@
package cn.nianxx.thhotel.workflows.reservation.common.dto;
import java.time.LocalDateTime;
/**
* OPERA 模拟操作 attempt 入库草稿。每次执行或重试生成一条。
*/
public record ReservationOperaOperationAttemptDraft(
String hotelId,
Long orderId,
Long taskId,
Long operationId,
Integer attemptNumber,
String attemptStatus,
String requestPayloadJson,
String responsePayloadJson,
String errorMessage,
LocalDateTime startedAt,
LocalDateTime finishedAt
) {
}

View File

@@ -0,0 +1,22 @@
package cn.nianxx.thhotel.workflows.reservation.common.dto;
import java.time.LocalDateTime;
/**
* OPERA 模拟操作 attempt 快照。用于执行结果和重试历史展示。
*/
public record ReservationOperaOperationAttemptSnapshot(
Long id,
String hotelId,
Long orderId,
Long taskId,
Long operationId,
Integer attemptNumber,
String attemptStatus,
String requestPayloadJson,
String responsePayloadJson,
String errorMessage,
LocalDateTime startedAt,
LocalDateTime finishedAt
) {
}

View File

@@ -0,0 +1,19 @@
package cn.nianxx.thhotel.workflows.reservation.common.dto;
import java.time.LocalDateTime;
/**
* OPERA 模拟操作入库草稿。任务确认后由系统固定生成两条。
*/
public record ReservationOperaOperationDraft(
String hotelId,
Long orderId,
Long taskId,
Integer operationSequence,
String operationCode,
String operationName,
String operationStatus,
String requestPayloadJson,
LocalDateTime now
) {
}

View File

@@ -0,0 +1,24 @@
package cn.nianxx.thhotel.workflows.reservation.common.dto;
import java.time.LocalDateTime;
/**
* OPERA 模拟操作快照。Service 使用该对象展示操作状态,不暴露 Entity。
*/
public record ReservationOperaOperationSnapshot(
Long id,
String hotelId,
Long orderId,
Long taskId,
Integer operationSequence,
String operationCode,
String operationName,
String operationStatus,
String requestPayloadJson,
Integer attemptCount,
Long lastAttemptId,
String lastErrorMessage,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
}

View File

@@ -1,7 +1,9 @@
package cn.nianxx.thhotel.workflows.reservation.common.dto;
import java.time.LocalDateTime;
/**
* 任务卡快照。保存字段契约版本AI 原始快照,供详情接口按矩阵组装展示字段
* 任务卡快照。保存字段契约版本AI 原始快照、用户草稿和最终确认 payload
*/
public record ReservationTaskCardSnapshot(
Long id,
@@ -9,6 +11,10 @@ public record ReservationTaskCardSnapshot(
Long taskId,
String taskCardType,
String fieldContractVersion,
String aiPayloadJson
String aiPayloadJson,
String draftPayloadJson,
String confirmedPayloadJson,
String confirmedBy,
LocalDateTime confirmedAt
) {
}

View File

@@ -1,7 +1,9 @@
package cn.nianxx.thhotel.workflows.reservation.common.dto;
import java.time.LocalDateTime;
/**
* 任务快照。用于任务详情、队列可处理状态人工复核转换,不向上暴露 Entity。
* 任务快照。用于任务详情、队列可处理状态人工复核转换和任务确认,不向上暴露 Entity。
*/
public record ReservationTaskSnapshot(
Long id,
@@ -20,6 +22,7 @@ public record ReservationTaskSnapshot(
Long parentTaskId,
Integer parentSourceEventIndex,
String linkedTaskGroupId,
Boolean blockedUntilParentCompleted
Boolean blockedUntilParentCompleted,
LocalDateTime confirmedAt
) {
}

View File

@@ -0,0 +1,9 @@
package cn.nianxx.thhotel.workflows.reservation.common.enums;
/**
* OPERA 模拟 attempt 状态。每次执行或重试只能成功或失败。
*/
public enum ReservationOperaAttemptStatus {
SUCCEEDED,
FAILED
}

View File

@@ -0,0 +1,10 @@
package cn.nianxx.thhotel.workflows.reservation.common.enums;
/**
* OPERA 模拟操作状态。第一版只表达待执行、成功和失败。
*/
public enum ReservationOperaOperationStatus {
PENDING,
SUCCEEDED,
FAILED
}

View File

@@ -0,0 +1,14 @@
package cn.nianxx.thhotel.workflows.reservation.common.request;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* OPERA 模拟执行请求。第一版只用开关控制模拟成功或失败,不接真实 OPERA。
*/
public record ReservationOperaSimulationRequest(
@JsonProperty("simulate_success")
Boolean simulateSuccess,
@JsonProperty("failure_message")
String failureMessage
) {
}

View File

@@ -0,0 +1,13 @@
package cn.nianxx.thhotel.workflows.reservation.common.request;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
/**
* 任务卡草稿保存和最终确认请求。第一版 field_values 使用矩阵 field_path 作为 key不使用 write_path 生成 OPERA 参数。
*/
public record ReservationTaskPayloadMutationRequest(
@JsonProperty("field_values")
Map<String, Object> fieldValues
) {
}

View File

@@ -0,0 +1,24 @@
package cn.nianxx.thhotel.workflows.reservation.common.result;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* OPERA 模拟操作 attempt 响应。用于展示每次执行或重试的结果。
*/
public record ReservationOperaOperationAttemptResult(
@JsonProperty("attempt_id")
String attemptId,
@JsonProperty("attempt_number")
Integer attemptNumber,
@JsonProperty("attempt_status")
String attemptStatus,
@JsonProperty("response_payload")
Object responsePayload,
@JsonProperty("error_message")
String errorMessage,
@JsonProperty("started_at")
String startedAt,
@JsonProperty("finished_at")
String finishedAt
) {
}

View File

@@ -0,0 +1,28 @@
package cn.nianxx.thhotel.workflows.reservation.common.result;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/**
* OPERA 模拟操作响应。包含当前操作状态和 attempt 历史。
*/
public record ReservationOperaOperationResult(
@JsonProperty("operation_id")
String operationId,
@JsonProperty("task_id")
String taskId,
@JsonProperty("operation_sequence")
Integer operationSequence,
@JsonProperty("operation_code")
String operationCode,
@JsonProperty("operation_name")
String operationName,
@JsonProperty("operation_status")
String operationStatus,
@JsonProperty("attempt_count")
Integer attemptCount,
@JsonProperty("last_error_message")
String lastErrorMessage,
List<ReservationOperaOperationAttemptResult> attempts
) {
}

View File

@@ -0,0 +1,14 @@
package cn.nianxx.thhotel.workflows.reservation.common.result;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/**
* 任务审计列表响应。第一版按任务维度返回审计流水。
*/
public record ReservationTaskAuditListResult(
@JsonProperty("task_id")
String taskId,
List<ReservationTaskAuditLogResult> items
) {
}

View File

@@ -0,0 +1,30 @@
package cn.nianxx.thhotel.workflows.reservation.common.result;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 任务审计列表单条响应。只返回审计摘要,不返回完整客户原始报文。
*/
public record ReservationTaskAuditLogResult(
@JsonProperty("audit_id")
String auditId,
@JsonProperty("order_id")
String orderId,
@JsonProperty("task_id")
String taskId,
@JsonProperty("operation_id")
String operationId,
@JsonProperty("actor_type")
String actorType,
@JsonProperty("actor_id")
String actorId,
String action,
String reason,
@JsonProperty("before_snapshot")
Object beforeSnapshot,
@JsonProperty("after_snapshot")
Object afterSnapshot,
@JsonProperty("occurred_at")
String occurredAt
) {
}

View File

@@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/**
* 任务详情结果。第一版提供队列可处理状态和按矩阵生成的字段列表。
* 任务详情结果。提供队列可处理状态、任务卡 payload 和按矩阵生成的字段列表。
*/
public record ReservationTaskDetailResult(
@JsonProperty("task_id")
@@ -21,7 +21,13 @@ public record ReservationTaskDetailResult(
String taskStatus,
@JsonProperty("field_contract_version")
String fieldContractVersion,
@JsonProperty("draft_payload")
Object draftPayload,
@JsonProperty("confirmed_payload")
Object confirmedPayload,
ReservationTaskAvailabilityResult availability,
List<ReservationTaskFieldResult> fields
List<ReservationTaskFieldResult> fields,
@JsonProperty("opera_operations")
List<ReservationOperaOperationResult> operaOperations
) {
}

View File

@@ -0,0 +1,23 @@
package cn.nianxx.thhotel.workflows.reservation.common.result;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/**
* 任务卡草稿保存或最终确认结果。payload 为第一版 field_path 简单结构,供前端回显和后续 OPERA 转换使用。
*/
public record ReservationTaskPayloadMutationResult(
@JsonProperty("task_id")
String taskId,
@JsonProperty("order_id")
String orderId,
@JsonProperty("task_status")
String taskStatus,
@JsonProperty("draft_payload")
Object draftPayload,
@JsonProperty("confirmed_payload")
Object confirmedPayload,
@JsonProperty("opera_operations")
List<ReservationOperaOperationResult> operaOperations
) {
}

View File

@@ -1,13 +1,19 @@
package cn.nianxx.thhotel.workflows.reservation.control;
import cn.nianxx.thhotel.workflows.reservation.common.request.ManualReviewConversionRequest;
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationOperaSimulationRequest;
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationTaskPayloadMutationRequest;
import cn.nianxx.thhotel.workflows.reservation.common.result.ManualReviewConversionResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOperaOperationResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskAuditListResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskDetailResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskPayloadMutationResult;
import cn.nianxx.thhotel.workflows.reservation.service.ReservationTaskWorkflowService;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -48,4 +54,66 @@ public class ReservationTaskController {
@RequestBody(required = false) ManualReviewConversionRequest request) {
return taskWorkflowService.convertManualReviewTask(taskId, request);
}
/**
* 保存任务卡草稿。第一版 field_values 使用矩阵 field_path不直接生成 OPERA 参数。
*/
@PutMapping(
value = "/{taskId}/draft",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ReservationTaskPayloadMutationResult saveDraft(
@PathVariable Long taskId,
@RequestBody(required = false) ReservationTaskPayloadMutationRequest request) {
return taskWorkflowService.saveTaskDraft(taskId, request);
}
/**
* 最终确认任务卡字段;校验通过后任务进入 READY等待后续 OPERA 阶段处理。
*/
@PostMapping(
value = "/{taskId}/confirm",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ReservationTaskPayloadMutationResult confirm(
@PathVariable Long taskId,
@RequestBody(required = false) ReservationTaskPayloadMutationRequest request) {
return taskWorkflowService.confirmTask(taskId, request);
}
/**
* 执行一条 OPERA 模拟操作;第一版只写入模拟 attempt不调用真实 OPERA。
*/
@PostMapping(
value = "/{taskId}/opera-operations/{operationId}/execute",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ReservationOperaOperationResult executeOperaOperation(
@PathVariable Long taskId,
@PathVariable Long operationId,
@RequestBody(required = false) ReservationOperaSimulationRequest request) {
return taskWorkflowService.executeOperaOperation(taskId, operationId, request);
}
/**
* 重试失败的 OPERA 模拟操作;重试会追加 attempt 历史。
*/
@PostMapping(
value = "/{taskId}/opera-operations/{operationId}/retry",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ReservationOperaOperationResult retryOperaOperation(
@PathVariable Long taskId,
@PathVariable Long operationId,
@RequestBody(required = false) ReservationOperaSimulationRequest request) {
return taskWorkflowService.retryOperaOperation(taskId, operationId, request);
}
/**
* 查询任务审计流水,供前端后续展示人工确认和 OPERA 模拟操作轨迹。
*/
@GetMapping(value = "/{taskId}/audits", produces = MediaType.APPLICATION_JSON_VALUE)
public ReservationTaskAuditListResult listAudits(@PathVariable Long taskId) {
return taskWorkflowService.listTaskAudits(taskId);
}
}

View File

@@ -2,7 +2,6 @@ package cn.nianxx.thhotel.workflows.reservation.control;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationWorkflowErrorResponse;
import cn.nianxx.thhotel.workflows.reservation.service.impl.ReservationTaskWorkflowException;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@@ -20,6 +19,9 @@ public class ReservationTaskControllerAdvice {
public ResponseEntity<ReservationWorkflowErrorResponse> handleTaskWorkflowException(
ReservationTaskWorkflowException exception) {
return ResponseEntity.status(exception.getStatus())
.body(new ReservationWorkflowErrorResponse(exception.getErrorCode(), exception.getMessage(), List.of()));
.body(new ReservationWorkflowErrorResponse(
exception.getErrorCode(),
exception.getMessage(),
exception.getDetails()));
}
}

View File

@@ -0,0 +1,68 @@
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 OPERA 模拟操作 attempt 实体。记录每次执行或重试的请求、响应和结果。
*/
@TableName("workflow_reservation_opera_operation_attempt")
public class ReservationOperaOperationAttemptEntity {
/** OPERA 模拟操作 attempt ID。 */
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 酒店或业务上下文 ID。 */
private String hotelId;
/** 关联订单 ID。 */
private Long orderId;
/** 关联任务 ID。 */
private Long taskId;
/** 关联 OPERA 模拟操作 ID。 */
private Long operationId;
/** 同一操作内第几次 attempt从 1 开始。 */
private Integer attemptNumber;
/** attempt 状态SUCCEEDED、FAILED。 */
private String attemptStatus;
/** 本次模拟请求 JSON。 */
private String requestPayloadJson;
/** 本次模拟响应 JSON。 */
private String responsePayloadJson;
/** 失败原因摘要。 */
private String errorMessage;
/** attempt 开始 UTC 时间。 */
private LocalDateTime startedAt;
/** attempt 结束 UTC 时间。 */
private LocalDateTime finishedAt;
/** 记录创建 UTC 时间。 */
private LocalDateTime createdAt;
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 getOrderId() { return orderId; }
public void setOrderId(Long orderId) { this.orderId = orderId; }
public Long getTaskId() { return taskId; }
public void setTaskId(Long taskId) { this.taskId = taskId; }
public Long getOperationId() { return operationId; }
public void setOperationId(Long operationId) { this.operationId = operationId; }
public Integer getAttemptNumber() { return attemptNumber; }
public void setAttemptNumber(Integer attemptNumber) { this.attemptNumber = attemptNumber; }
public String getAttemptStatus() { return attemptStatus; }
public void setAttemptStatus(String attemptStatus) { this.attemptStatus = attemptStatus; }
public String getRequestPayloadJson() { return requestPayloadJson; }
public void setRequestPayloadJson(String requestPayloadJson) { this.requestPayloadJson = requestPayloadJson; }
public String getResponsePayloadJson() { return responsePayloadJson; }
public void setResponsePayloadJson(String responsePayloadJson) { this.responsePayloadJson = responsePayloadJson; }
public String getErrorMessage() { return errorMessage; }
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
public LocalDateTime getStartedAt() { return startedAt; }
public void setStartedAt(LocalDateTime startedAt) { this.startedAt = startedAt; }
public LocalDateTime getFinishedAt() { return finishedAt; }
public void setFinishedAt(LocalDateTime finishedAt) { this.finishedAt = finishedAt; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}

View File

@@ -0,0 +1,72 @@
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 OPERA 模拟操作实体。保存任务确认后生成的固定模拟操作及当前状态。
*/
@TableName("workflow_reservation_opera_operation")
public class ReservationOperaOperationEntity {
/** OPERA 模拟操作 ID。 */
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 酒店或业务上下文 ID。 */
private String hotelId;
/** 关联订单 ID。 */
private Long orderId;
/** 关联任务 ID。 */
private Long taskId;
/** 任务内模拟操作顺序,从 1 开始。 */
private Integer operationSequence;
/** 模拟操作代码,第一版固定两类。 */
private String operationCode;
/** 模拟操作展示名称。 */
private String operationName;
/** 操作状态PENDING、SUCCEEDED、FAILED。 */
private String operationStatus;
/** 模拟请求 JSON来源于确认 payload 的低耦合摘要。 */
private String requestPayloadJson;
/** 已执行 attempt 次数。 */
private Integer attemptCount;
/** 最近一次 attempt ID。 */
private Long lastAttemptId;
/** 最近一次失败原因摘要。 */
private String lastErrorMessage;
/** 记录创建 UTC 时间。 */
private LocalDateTime createdAt;
/** 记录更新 UTC 时间。 */
private LocalDateTime updatedAt;
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 getOrderId() { return orderId; }
public void setOrderId(Long orderId) { this.orderId = orderId; }
public Long getTaskId() { return taskId; }
public void setTaskId(Long taskId) { this.taskId = taskId; }
public Integer getOperationSequence() { return operationSequence; }
public void setOperationSequence(Integer operationSequence) { this.operationSequence = operationSequence; }
public String getOperationCode() { return operationCode; }
public void setOperationCode(String operationCode) { this.operationCode = operationCode; }
public String getOperationName() { return operationName; }
public void setOperationName(String operationName) { this.operationName = operationName; }
public String getOperationStatus() { return operationStatus; }
public void setOperationStatus(String operationStatus) { this.operationStatus = operationStatus; }
public String getRequestPayloadJson() { return requestPayloadJson; }
public void setRequestPayloadJson(String requestPayloadJson) { this.requestPayloadJson = requestPayloadJson; }
public Integer getAttemptCount() { return attemptCount; }
public void setAttemptCount(Integer attemptCount) { this.attemptCount = attemptCount; }
public Long getLastAttemptId() { return lastAttemptId; }
public void setLastAttemptId(Long lastAttemptId) { this.lastAttemptId = lastAttemptId; }
public String getLastErrorMessage() { return lastErrorMessage; }
public void setLastErrorMessage(String lastErrorMessage) { this.lastErrorMessage = lastErrorMessage; }
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; }
}

View File

@@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
import java.time.LocalDateTime;
/**
* Reservation 任务卡实体。CP1-3 只保存 AI 原始快照,不承载用户编辑确认 payload。
* Reservation 任务卡实体。保存 AI 原始快照、用户草稿和最终确认 payload。
*/
@TableName("workflow_reservation_task_card")
public class ReservationTaskCardEntity {
@@ -24,6 +24,14 @@ public class ReservationTaskCardEntity {
private String fieldContractVersion;
/** 任务卡使用的 AI 原始 JSON 快照。 */
private String aiPayloadJson;
/** 用户保存的任务卡草稿 JSON第一版按矩阵 field_path 保存。 */
private String draftPayloadJson;
/** 用户最终确认后的任务卡 JSON第一版按矩阵 field_path 合并保存。 */
private String confirmedPayloadJson;
/** 确认人标识,用户身份权限接入前使用本地占位。 */
private String confirmedBy;
/** 任务卡最终确认 UTC 时间。 */
private LocalDateTime confirmedAt;
/** 记录创建 UTC 时间。 */
private LocalDateTime createdAt;
/** 记录更新 UTC 时间。 */
@@ -41,6 +49,14 @@ public class ReservationTaskCardEntity {
public void setFieldContractVersion(String fieldContractVersion) { this.fieldContractVersion = fieldContractVersion; }
public String getAiPayloadJson() { return aiPayloadJson; }
public void setAiPayloadJson(String aiPayloadJson) { this.aiPayloadJson = aiPayloadJson; }
public String getDraftPayloadJson() { return draftPayloadJson; }
public void setDraftPayloadJson(String draftPayloadJson) { this.draftPayloadJson = draftPayloadJson; }
public String getConfirmedPayloadJson() { return confirmedPayloadJson; }
public void setConfirmedPayloadJson(String confirmedPayloadJson) { this.confirmedPayloadJson = confirmedPayloadJson; }
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 LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }

View File

@@ -48,6 +48,8 @@ public class ReservationTaskEntity {
private Boolean blockedUntilParentCompleted;
/** 最近失败原因摘要。 */
private String lastFailureReason;
/** 用户最终确认任务字段和订单归属的 UTC 时间。 */
private LocalDateTime confirmedAt;
/** 任务完成 UTC 时间。 */
private LocalDateTime completedAt;
/** 乐观锁版本。 */
@@ -93,6 +95,8 @@ public class ReservationTaskEntity {
public void setBlockedUntilParentCompleted(Boolean blockedUntilParentCompleted) { this.blockedUntilParentCompleted = blockedUntilParentCompleted; }
public String getLastFailureReason() { return lastFailureReason; }
public void setLastFailureReason(String lastFailureReason) { this.lastFailureReason = lastFailureReason; }
public LocalDateTime getConfirmedAt() { return confirmedAt; }
public void setConfirmedAt(LocalDateTime confirmedAt) { this.confirmedAt = confirmedAt; }
public LocalDateTime getCompletedAt() { return completedAt; }
public void setCompletedAt(LocalDateTime completedAt) { this.completedAt = completedAt; }
public Long getVersion() { return version; }

View File

@@ -0,0 +1,12 @@
package cn.nianxx.thhotel.workflows.reservation.mapper;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationOperaOperationAttemptEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* Reservation OPERA 模拟操作 attempt Mapper只负责 attempt 表持久化访问。
*/
@Mapper
public interface ReservationOperaOperationAttemptMapper extends BaseMapper<ReservationOperaOperationAttemptEntity> {
}

View File

@@ -0,0 +1,12 @@
package cn.nianxx.thhotel.workflows.reservation.mapper;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationOperaOperationEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* Reservation OPERA 模拟操作 Mapper只负责操作表持久化访问。
*/
@Mapper
public interface ReservationOperaOperationMapper extends BaseMapper<ReservationOperaOperationEntity> {
}

View File

@@ -4,6 +4,11 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchDraf
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationAttemptDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationAttemptSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationSnapshot;
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.ReservationTaskCardDraft;
@@ -11,15 +16,20 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskCardSna
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationTaskStatus;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationAiBatchEntity;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationAiTransitionEntity;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationAuditLogEntity;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationOperaOperationAttemptEntity;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationOperaOperationEntity;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationOrderEntity;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationTaskCardEntity;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationTaskEntity;
import cn.nianxx.thhotel.workflows.reservation.mapper.ReservationAiBatchMapper;
import cn.nianxx.thhotel.workflows.reservation.mapper.ReservationAiTransitionMapper;
import cn.nianxx.thhotel.workflows.reservation.mapper.ReservationAuditLogMapper;
import cn.nianxx.thhotel.workflows.reservation.mapper.ReservationOperaOperationAttemptMapper;
import cn.nianxx.thhotel.workflows.reservation.mapper.ReservationOperaOperationMapper;
import cn.nianxx.thhotel.workflows.reservation.mapper.ReservationOrderMapper;
import cn.nianxx.thhotel.workflows.reservation.mapper.ReservationTaskCardMapper;
import cn.nianxx.thhotel.workflows.reservation.mapper.ReservationTaskMapper;
@@ -40,6 +50,8 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
private final ReservationOrderMapper orderMapper;
private final ReservationTaskMapper taskMapper;
private final ReservationTaskCardMapper taskCardMapper;
private final ReservationOperaOperationMapper operaOperationMapper;
private final ReservationOperaOperationAttemptMapper operaOperationAttemptMapper;
private final ReservationAuditLogMapper auditLogMapper;
/**
@@ -51,12 +63,16 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
ReservationOrderMapper orderMapper,
ReservationTaskMapper taskMapper,
ReservationTaskCardMapper taskCardMapper,
ReservationOperaOperationMapper operaOperationMapper,
ReservationOperaOperationAttemptMapper operaOperationAttemptMapper,
ReservationAuditLogMapper auditLogMapper) {
this.batchMapper = batchMapper;
this.transitionMapper = transitionMapper;
this.orderMapper = orderMapper;
this.taskMapper = taskMapper;
this.taskCardMapper = taskCardMapper;
this.operaOperationMapper = operaOperationMapper;
this.operaOperationAttemptMapper = operaOperationAttemptMapper;
this.auditLogMapper = auditLogMapper;
}
@@ -339,6 +355,169 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
.eq(ReservationTaskCardEntity::getTaskId, taskId));
}
/**
* 保存任务卡草稿 payload。
*/
@Override
public boolean updateTaskDraftPayload(String hotelId, Long taskId, String draftPayloadJson, LocalDateTime now) {
return taskCardMapper.update(null, Wrappers.<ReservationTaskCardEntity>lambdaUpdate()
.set(ReservationTaskCardEntity::getDraftPayloadJson, draftPayloadJson)
.set(ReservationTaskCardEntity::getUpdatedAt, now)
.eq(ReservationTaskCardEntity::getHotelId, hotelId)
.eq(ReservationTaskCardEntity::getTaskId, taskId)) > 0;
}
/**
* 保存最终确认 payload并将任务状态从 PENDING_CONFIRM 流转为 READY。
*/
@Override
public boolean confirmTaskPayload(
String hotelId,
Long taskId,
String confirmedPayloadJson,
String confirmedBy,
LocalDateTime confirmedAt) {
int updatedTaskCount = taskMapper.update(null, Wrappers.<ReservationTaskEntity>lambdaUpdate()
.set(ReservationTaskEntity::getTaskStatus, ReservationTaskStatus.READY.name())
.set(ReservationTaskEntity::getConfirmedAt, confirmedAt)
.set(ReservationTaskEntity::getUpdatedAt, confirmedAt)
.setSql("version = version + 1")
.eq(ReservationTaskEntity::getHotelId, hotelId)
.eq(ReservationTaskEntity::getId, taskId)
.eq(ReservationTaskEntity::getTaskStatus, ReservationTaskStatus.PENDING_CONFIRM.name()));
if (updatedTaskCount == 0) {
return false;
}
int updatedCardCount = taskCardMapper.update(null, Wrappers.<ReservationTaskCardEntity>lambdaUpdate()
.set(ReservationTaskCardEntity::getConfirmedPayloadJson, confirmedPayloadJson)
.set(ReservationTaskCardEntity::getConfirmedBy, confirmedBy)
.set(ReservationTaskCardEntity::getConfirmedAt, confirmedAt)
.set(ReservationTaskCardEntity::getUpdatedAt, confirmedAt)
.eq(ReservationTaskCardEntity::getHotelId, hotelId)
.eq(ReservationTaskCardEntity::getTaskId, taskId));
return updatedCardCount > 0;
}
/**
* 插入 OPERA 模拟操作。
*/
@Override
public Long insertOperaOperation(ReservationOperaOperationDraft draft) {
ReservationOperaOperationEntity entity = new ReservationOperaOperationEntity();
entity.setHotelId(draft.hotelId());
entity.setOrderId(draft.orderId());
entity.setTaskId(draft.taskId());
entity.setOperationSequence(draft.operationSequence());
entity.setOperationCode(draft.operationCode());
entity.setOperationName(draft.operationName());
entity.setOperationStatus(draft.operationStatus());
entity.setRequestPayloadJson(draft.requestPayloadJson());
entity.setAttemptCount(0);
entity.setCreatedAt(draft.now());
entity.setUpdatedAt(draft.now());
operaOperationMapper.insert(entity);
return entity.getId();
}
/**
* 查询任务下 OPERA 模拟操作,按任务内顺序返回。
*/
@Override
public List<ReservationOperaOperationSnapshot> findOperaOperationsByTaskId(String hotelId, Long taskId) {
return operaOperationMapper.selectList(Wrappers.<ReservationOperaOperationEntity>lambdaQuery()
.eq(ReservationOperaOperationEntity::getHotelId, hotelId)
.eq(ReservationOperaOperationEntity::getTaskId, taskId)
.orderByAsc(ReservationOperaOperationEntity::getOperationSequence))
.stream()
.map(this::toOperaOperationSnapshot)
.toList();
}
/**
* 按操作 ID 查询 OPERA 模拟操作。
*/
@Override
public Optional<ReservationOperaOperationSnapshot> findOperaOperationById(String hotelId, Long operationId) {
ReservationOperaOperationEntity entity = operaOperationMapper.selectOne(
Wrappers.<ReservationOperaOperationEntity>lambdaQuery()
.eq(ReservationOperaOperationEntity::getHotelId, hotelId)
.eq(ReservationOperaOperationEntity::getId, operationId)
.last("LIMIT 1"));
return Optional.ofNullable(entity).map(this::toOperaOperationSnapshot);
}
/**
* 查询 OPERA 模拟操作 attempt 历史。
*/
@Override
public List<ReservationOperaOperationAttemptSnapshot> findOperaOperationAttempts(String hotelId, Long operationId) {
return operaOperationAttemptMapper.selectList(Wrappers.<ReservationOperaOperationAttemptEntity>lambdaQuery()
.eq(ReservationOperaOperationAttemptEntity::getHotelId, hotelId)
.eq(ReservationOperaOperationAttemptEntity::getOperationId, operationId)
.orderByAsc(ReservationOperaOperationAttemptEntity::getAttemptNumber))
.stream()
.map(this::toOperaOperationAttemptSnapshot)
.toList();
}
/**
* 插入 OPERA 模拟操作 attempt。
*/
@Override
public Long insertOperaOperationAttempt(ReservationOperaOperationAttemptDraft draft) {
ReservationOperaOperationAttemptEntity entity = new ReservationOperaOperationAttemptEntity();
entity.setHotelId(draft.hotelId());
entity.setOrderId(draft.orderId());
entity.setTaskId(draft.taskId());
entity.setOperationId(draft.operationId());
entity.setAttemptNumber(draft.attemptNumber());
entity.setAttemptStatus(draft.attemptStatus());
entity.setRequestPayloadJson(draft.requestPayloadJson());
entity.setResponsePayloadJson(draft.responsePayloadJson());
entity.setErrorMessage(draft.errorMessage());
entity.setStartedAt(draft.startedAt());
entity.setFinishedAt(draft.finishedAt());
entity.setCreatedAt(draft.startedAt());
operaOperationAttemptMapper.insert(entity);
return entity.getId();
}
/**
* 更新 OPERA 模拟操作当前状态和最近 attempt 摘要。
*/
@Override
public boolean updateOperaOperationAfterAttempt(
String hotelId,
Long operationId,
String operationStatus,
Long lastAttemptId,
String lastErrorMessage,
LocalDateTime now) {
return operaOperationMapper.update(null, Wrappers.<ReservationOperaOperationEntity>lambdaUpdate()
.set(ReservationOperaOperationEntity::getOperationStatus, operationStatus)
.set(ReservationOperaOperationEntity::getLastAttemptId, lastAttemptId)
.set(ReservationOperaOperationEntity::getLastErrorMessage, lastErrorMessage)
.set(ReservationOperaOperationEntity::getUpdatedAt, now)
.setSql("attempt_count = attempt_count + 1")
.eq(ReservationOperaOperationEntity::getHotelId, hotelId)
.eq(ReservationOperaOperationEntity::getId, operationId)) > 0;
}
/**
* 更新任务状态。
*/
@Override
public boolean updateTaskStatus(String hotelId, Long taskId, String taskStatus, LocalDateTime now) {
return taskMapper.update(null, Wrappers.<ReservationTaskEntity>lambdaUpdate()
.set(ReservationTaskEntity::getTaskStatus, taskStatus)
.set(ReservationTaskEntity::getCompletedAt,
ReservationTaskStatus.COMPLETED.name().equals(taskStatus) ? now : null)
.set(ReservationTaskEntity::getUpdatedAt, now)
.setSql("version = version + 1")
.eq(ReservationTaskEntity::getHotelId, hotelId)
.eq(ReservationTaskEntity::getId, taskId)) > 0;
}
/**
* 统计订单下全部任务数量。
*/
@@ -387,6 +566,20 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
return entity.getId();
}
/**
* 查询任务审计流水,按发生时间正序返回。
*/
@Override
public List<ReservationAuditLogSnapshot> findAuditLogsByTaskId(String hotelId, Long taskId) {
return auditLogMapper.selectList(Wrappers.<ReservationAuditLogEntity>lambdaQuery()
.eq(ReservationAuditLogEntity::getHotelId, hotelId)
.eq(ReservationAuditLogEntity::getTaskId, taskId)
.orderByAsc(ReservationAuditLogEntity::getOccurredAt))
.stream()
.map(this::toAuditLogSnapshot)
.toList();
}
/**
* 转换批次实体为快照。
*/
@@ -435,7 +628,8 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
entity.getParentTaskId(),
entity.getParentSourceEventIndex(),
entity.getLinkedTaskGroupId(),
entity.getBlockedUntilParentCompleted());
entity.getBlockedUntilParentCompleted(),
entity.getConfirmedAt());
}
/**
@@ -448,6 +642,70 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
entity.getTaskId(),
entity.getTaskCardType(),
entity.getFieldContractVersion(),
entity.getAiPayloadJson());
entity.getAiPayloadJson(),
entity.getDraftPayloadJson(),
entity.getConfirmedPayloadJson(),
entity.getConfirmedBy(),
entity.getConfirmedAt());
}
/**
* 转换 OPERA 模拟操作实体为快照。
*/
private ReservationOperaOperationSnapshot toOperaOperationSnapshot(ReservationOperaOperationEntity entity) {
return new ReservationOperaOperationSnapshot(
entity.getId(),
entity.getHotelId(),
entity.getOrderId(),
entity.getTaskId(),
entity.getOperationSequence(),
entity.getOperationCode(),
entity.getOperationName(),
entity.getOperationStatus(),
entity.getRequestPayloadJson(),
entity.getAttemptCount(),
entity.getLastAttemptId(),
entity.getLastErrorMessage(),
entity.getCreatedAt(),
entity.getUpdatedAt());
}
/**
* 转换 OPERA 模拟操作 attempt 实体为快照。
*/
private ReservationOperaOperationAttemptSnapshot toOperaOperationAttemptSnapshot(
ReservationOperaOperationAttemptEntity entity) {
return new ReservationOperaOperationAttemptSnapshot(
entity.getId(),
entity.getHotelId(),
entity.getOrderId(),
entity.getTaskId(),
entity.getOperationId(),
entity.getAttemptNumber(),
entity.getAttemptStatus(),
entity.getRequestPayloadJson(),
entity.getResponsePayloadJson(),
entity.getErrorMessage(),
entity.getStartedAt(),
entity.getFinishedAt());
}
/**
* 转换审计实体为快照。
*/
private ReservationAuditLogSnapshot toAuditLogSnapshot(ReservationAuditLogEntity entity) {
return new ReservationAuditLogSnapshot(
entity.getId(),
entity.getHotelId(),
entity.getOrderId(),
entity.getTaskId(),
entity.getOperationId(),
entity.getActorType(),
entity.getActorId(),
entity.getAction(),
entity.getReason(),
entity.getBeforeSnapshotJson(),
entity.getAfterSnapshotJson(),
entity.getOccurredAt());
}
}

View File

@@ -4,6 +4,11 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchDraf
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationAttemptDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationAttemptSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationSnapshot;
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.ReservationTaskCardDraft;
@@ -106,6 +111,62 @@ public interface ReservationAiWorkflowRepository {
Integer executionOrder,
LocalDateTime now);
/**
* 保存任务卡草稿 payload。
*/
boolean updateTaskDraftPayload(String hotelId, Long taskId, String draftPayloadJson, LocalDateTime now);
/**
* 保存最终确认 payload并将任务状态从 PENDING_CONFIRM 流转为 READY。
*/
boolean confirmTaskPayload(
String hotelId,
Long taskId,
String confirmedPayloadJson,
String confirmedBy,
LocalDateTime confirmedAt);
/**
* 新增 OPERA 模拟操作,并返回操作 ID。
*/
Long insertOperaOperation(ReservationOperaOperationDraft draft);
/**
* 查询任务下全部 OPERA 模拟操作。
*/
List<ReservationOperaOperationSnapshot> findOperaOperationsByTaskId(String hotelId, Long taskId);
/**
* 按操作 ID 查询 OPERA 模拟操作。
*/
Optional<ReservationOperaOperationSnapshot> findOperaOperationById(String hotelId, Long operationId);
/**
* 查询 OPERA 模拟操作的 attempt 历史。
*/
List<ReservationOperaOperationAttemptSnapshot> findOperaOperationAttempts(String hotelId, Long operationId);
/**
* 新增 OPERA 模拟操作 attempt并返回 attempt ID。
*/
Long insertOperaOperationAttempt(ReservationOperaOperationAttemptDraft draft);
/**
* 更新 OPERA 模拟操作的当前状态和最近 attempt 摘要。
*/
boolean updateOperaOperationAfterAttempt(
String hotelId,
Long operationId,
String operationStatus,
Long lastAttemptId,
String lastErrorMessage,
LocalDateTime now);
/**
* 更新任务状态,用于 OPERA 模拟操作全部成功后完成任务。
*/
boolean updateTaskStatus(String hotelId, Long taskId, String taskStatus, LocalDateTime now);
/**
* 统计订单当前剩余任务数量,用于判断临时订单能否逻辑删除。
*/
@@ -120,4 +181,9 @@ public interface ReservationAiWorkflowRepository {
* 新增订单任务审计记录。
*/
Long insertAuditLog(ReservationAuditLogDraft draft);
/**
* 查询任务审计流水。
*/
List<ReservationAuditLogSnapshot> findAuditLogsByTaskId(String hotelId, Long taskId);
}

View File

@@ -1,11 +1,16 @@
package cn.nianxx.thhotel.workflows.reservation.service;
import cn.nianxx.thhotel.workflows.reservation.common.request.ManualReviewConversionRequest;
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationOperaSimulationRequest;
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationTaskPayloadMutationRequest;
import cn.nianxx.thhotel.workflows.reservation.common.result.ManualReviewConversionResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOperaOperationResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskAuditListResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskDetailResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskPayloadMutationResult;
/**
* Reservation 任务工作流服务。提供任务详情、队列可处理状态人工复核转换能力。
* Reservation 任务工作流服务。提供任务详情、队列可处理状态人工复核转换和任务确认能力。
*/
public interface ReservationTaskWorkflowService {
@@ -18,4 +23,35 @@ public interface ReservationTaskWorkflowService {
* 将 Fallback / manual_review 任务人工转换为 New、Update 或 Cancel。
*/
ManualReviewConversionResult convertManualReviewTask(Long taskId, ManualReviewConversionRequest request);
/**
* 保存任务卡草稿;只允许当前可编辑的 PENDING_CONFIRM 任务调用。
*/
ReservationTaskPayloadMutationResult saveTaskDraft(Long taskId, ReservationTaskPayloadMutationRequest request);
/**
* 最终确认任务卡字段;校验通过后任务从 PENDING_CONFIRM 流转为 READY。
*/
ReservationTaskPayloadMutationResult confirmTask(Long taskId, ReservationTaskPayloadMutationRequest request);
/**
* 执行一条 OPERA 模拟操作;只允许前置模拟操作已成功的操作执行。
*/
ReservationOperaOperationResult executeOperaOperation(
Long taskId,
Long operationId,
ReservationOperaSimulationRequest request);
/**
* 重试失败的 OPERA 模拟操作;成功后继续保留 attempt 历史。
*/
ReservationOperaOperationResult retryOperaOperation(
Long taskId,
Long operationId,
ReservationOperaSimulationRequest request);
/**
* 查询任务审计流水。
*/
ReservationTaskAuditListResult listTaskAudits(Long taskId);
}

View File

@@ -1,19 +1,30 @@
package cn.nianxx.thhotel.workflows.reservation.service.impl;
import java.util.List;
import org.springframework.http.HttpStatus;
/**
* Reservation 任务工作流受控异常。用于详情、队列状态人工转换接口。
* Reservation 任务工作流受控异常。用于详情、队列状态人工转换和任务确认接口。
*/
public class ReservationTaskWorkflowException extends RuntimeException {
private final HttpStatus status;
private final String errorCode;
private final List<String> details;
public ReservationTaskWorkflowException(HttpStatus status, String errorCode, String message) {
this(status, errorCode, message, List.of());
}
public ReservationTaskWorkflowException(
HttpStatus status,
String errorCode,
String message,
List<String> details) {
super(message);
this.status = status;
this.errorCode = errorCode;
this.details = details == null ? List.of() : List.copyOf(details);
}
public HttpStatus getStatus() {
@@ -23,4 +34,8 @@ public class ReservationTaskWorkflowException extends RuntimeException {
public String getErrorCode() {
return errorCode;
}
public List<String> getDetails() {
return details;
}
}

View File

@@ -0,0 +1,15 @@
-- M002 CP5任务草稿和最终确认 payload。第一版按矩阵 field_path 保存,不直接生成 OPERA 参数。
ALTER TABLE workflow_reservation_task_card
ADD COLUMN draft_payload_json LONGTEXT NULL COMMENT '用户保存的任务卡草稿 JSON第一版按矩阵 field_path 作为 key 保存';
ALTER TABLE workflow_reservation_task_card
ADD COLUMN confirmed_payload_json LONGTEXT NULL COMMENT '用户最终确认后的任务卡 JSON第一版按矩阵 field_path 合并保存,后续 OPERA 参数组装时再转换';
ALTER TABLE workflow_reservation_task_card
ADD COLUMN confirmed_by VARCHAR(128) NULL COMMENT '确认人标识,用户身份权限接入前使用本地占位';
ALTER TABLE workflow_reservation_task_card
ADD COLUMN confirmed_at DATETIME(6) NULL COMMENT '任务卡最终确认 UTC 时间';
ALTER TABLE workflow_reservation_task
ADD COLUMN confirmed_at DATETIME(6) NULL COMMENT '任务最终确认 UTC 时间';

View File

@@ -0,0 +1,42 @@
-- M002 OPERA 模拟骨架:每个已确认任务固定生成两条模拟操作。
CREATE TABLE workflow_reservation_opera_operation (
id BIGINT NOT NULL COMMENT 'OPERA 模拟操作 ID',
hotel_id VARCHAR(64) NOT NULL COMMENT '酒店或业务上下文 ID',
order_id BIGINT NOT NULL COMMENT '关联订单 ID',
task_id BIGINT NOT NULL COMMENT '关联任务 ID',
operation_sequence INT NOT NULL COMMENT '任务内模拟操作顺序,从 1 开始',
operation_code VARCHAR(64) NOT NULL COMMENT '模拟操作代码,第一版固定两类',
operation_name VARCHAR(128) NOT NULL COMMENT '模拟操作展示名称',
operation_status VARCHAR(32) NOT NULL COMMENT '操作状态PENDING、SUCCEEDED、FAILED',
request_payload_json LONGTEXT NULL COMMENT '模拟请求 JSON来源于 confirmed_payload_json 的低耦合摘要',
attempt_count INT NOT NULL DEFAULT 0 COMMENT '已执行 attempt 次数',
last_attempt_id BIGINT NULL COMMENT '最近一次 attempt ID',
last_error_message VARCHAR(512) NULL COMMENT '最近一次失败原因摘要',
created_at DATETIME(6) NOT NULL COMMENT '记录创建 UTC 时间',
updated_at DATETIME(6) NOT NULL COMMENT '记录更新 UTC 时间',
PRIMARY KEY (id),
UNIQUE KEY uk_reservation_opera_operation_sequence (hotel_id, task_id, operation_sequence),
KEY idx_reservation_opera_operation_task_status (hotel_id, task_id, operation_status),
KEY idx_reservation_opera_operation_order (hotel_id, order_id, operation_sequence)
) COMMENT='Reservation OPERA 模拟操作表,第一版每个任务固定两条操作';
-- M002 OPERA 模拟骨架:每次执行或重试都会生成一条 attempt。
CREATE TABLE workflow_reservation_opera_operation_attempt (
id BIGINT NOT NULL COMMENT 'OPERA 模拟操作 attempt ID',
hotel_id VARCHAR(64) NOT NULL COMMENT '酒店或业务上下文 ID',
order_id BIGINT NOT NULL COMMENT '关联订单 ID',
task_id BIGINT NOT NULL COMMENT '关联任务 ID',
operation_id BIGINT NOT NULL COMMENT '关联 OPERA 模拟操作 ID',
attempt_number INT NOT NULL COMMENT '同一操作内第几次 attempt从 1 开始',
attempt_status VARCHAR(32) NOT NULL COMMENT 'attempt 状态SUCCEEDED、FAILED',
request_payload_json LONGTEXT NULL COMMENT '本次模拟请求 JSON',
response_payload_json LONGTEXT NULL COMMENT '本次模拟响应 JSON',
error_message VARCHAR(512) NULL COMMENT '失败原因摘要',
started_at DATETIME(6) NOT NULL COMMENT 'attempt 开始 UTC 时间',
finished_at DATETIME(6) NOT NULL COMMENT 'attempt 结束 UTC 时间',
created_at DATETIME(6) NOT NULL COMMENT '记录创建 UTC 时间',
PRIMARY KEY (id),
UNIQUE KEY uk_reservation_opera_attempt_number (hotel_id, operation_id, attempt_number),
KEY idx_reservation_opera_attempt_task (hotel_id, task_id, created_at),
KEY idx_reservation_opera_attempt_operation (hotel_id, operation_id, attempt_number)
) COMMENT='Reservation OPERA 模拟操作 attempt 表,记录执行和重试结果';