实现 V4 卡片确认和来源通知确认
This commit is contained in:
@@ -10,5 +10,6 @@ public enum ReservationV4ReadonlyReasonCode {
|
||||
PRIOR_CARD_NOT_CONFIRMED,
|
||||
PRIOR_ORDER_TASK_NOT_COMPLETED,
|
||||
COMMAND_API_PENDING,
|
||||
ACK_API_PENDING
|
||||
ACK_API_PENDING,
|
||||
REVIEW_API_PENDING
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.request;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
* Reservation V4 卡片确认请求。confirmed_payload 为空时后端使用当前展示 payload 作为确认快照。
|
||||
*
|
||||
* @param version 前端读取到的卡片乐观锁版本
|
||||
* @param confirmedPayload 用户确认后的卡片 payload
|
||||
*/
|
||||
public record ReservationV4CardConfirmRequest(
|
||||
Long version,
|
||||
@JsonProperty("confirmed_payload")
|
||||
JsonNode confirmedPayload
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.request;
|
||||
|
||||
/**
|
||||
* Reservation V4 来源通知确认请求。第一版只要求带上前端读取到的 version。
|
||||
*
|
||||
* @param version 来源通知乐观锁版本
|
||||
* @param reason 可选处理说明,当前只写入审计摘要
|
||||
*/
|
||||
public record ReservationV4SourceNotificationAckRequest(
|
||||
Long version,
|
||||
String reason
|
||||
) {
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Reservation V4 查询侧动作可用性。CP5 只读查询阶段不开放写接口,因此 confirmable / ackable 默认由后续 CP 打开。
|
||||
* Reservation V4 查询侧动作可用性。用于前端判断卡片确认、来源通知确认和只读原因。
|
||||
*
|
||||
* @param blocked 是否被前置订单任务或卡片阻塞
|
||||
* @param readOnly 当前是否只读
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
ReservationTaskController.class,
|
||||
ReservationFrontendQueryController.class,
|
||||
ReservationV4QueryController.class,
|
||||
ReservationV4CommandController.class,
|
||||
ReservationDemoDataController.class,
|
||||
ReservationInvoiceGenerationController.class,
|
||||
ReservationRoomingListGenerationController.class
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.control;
|
||||
|
||||
import cn.nianxx.thhotel.platform.access.common.enums.PlatformPermissionCode;
|
||||
import cn.nianxx.thhotel.platform.security.common.dto.AuthenticatedUserContext;
|
||||
import cn.nianxx.thhotel.platform.security.service.FrontendAuthorizationService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4CardConfirmRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4SourceNotificationAckRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4OrderTaskDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4SourceNotificationDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationV4CommandService;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Reservation V4 前端写操作 Controller。只暴露卡片确认和来源通知确认入口。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/reservation")
|
||||
public class ReservationV4CommandController {
|
||||
|
||||
private final ReservationV4CommandService commandService;
|
||||
private final FrontendAuthorizationService authorizationService;
|
||||
|
||||
/**
|
||||
* 注入 V4 命令服务和前端鉴权服务,Controller 不直接访问 Repository。
|
||||
*/
|
||||
public ReservationV4CommandController(
|
||||
ReservationV4CommandService commandService,
|
||||
FrontendAuthorizationService authorizationService) {
|
||||
this.commandService = commandService;
|
||||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认 V4 订单任务下的一张业务卡;Basic Information 未确认时业务卡不可确认。
|
||||
*/
|
||||
@PostMapping(
|
||||
value = "/order-tasks/{orderTaskId}/cards/{cardId}/confirm",
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ReservationV4OrderTaskDetailResult confirmTaskCard(
|
||||
@PathVariable Long orderTaskId,
|
||||
@PathVariable Long cardId,
|
||||
@RequestBody(required = false) ReservationV4CardConfirmRequest request) {
|
||||
AuthenticatedUserContext actor = authorizationService.requirePermission(
|
||||
PlatformPermissionCode.RESERVATION_TASK_CONFIRM.name());
|
||||
return commandService.confirmTaskCard(orderTaskId, cardId, request, actor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认 S10/S99 来源通知已读或已处理;不创建订单、不参与订单阻塞。
|
||||
*/
|
||||
@PostMapping(
|
||||
value = "/source-notifications/{notificationId}/ack",
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ReservationV4SourceNotificationDetailResult ackSourceNotification(
|
||||
@PathVariable Long notificationId,
|
||||
@RequestBody(required = false) ReservationV4SourceNotificationAckRequest request) {
|
||||
AuthenticatedUserContext actor = authorizationService.requirePermission(
|
||||
PlatformPermissionCode.RESERVATION_TASK_CONFIRM.name());
|
||||
return commandService.ackSourceNotification(notificationId, request, actor);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTask
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationPageSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4OrderTaskQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationV4OrderTaskEntity;
|
||||
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationV4TaskCardEntity;
|
||||
@@ -282,6 +283,51 @@ public class MybatisReservationV4WorkflowRepository implements ReservationV4Work
|
||||
return updated == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 version 乐观锁确认 V4 任务卡,同时写入用户确认 payload、确认人和确认时间。
|
||||
*/
|
||||
@Override
|
||||
public boolean confirmTaskCardWithVersion(
|
||||
String hotelId,
|
||||
Long taskCardId,
|
||||
Long expectedVersion,
|
||||
String confirmedPayloadJson,
|
||||
String confirmedBy,
|
||||
LocalDateTime confirmedAt) {
|
||||
int updated = taskCardMapper.update(Wrappers.<ReservationV4TaskCardEntity>lambdaUpdate()
|
||||
.set(ReservationV4TaskCardEntity::getCardStatus, ReservationV4CardStatus.CONFIRMED.name())
|
||||
.set(ReservationV4TaskCardEntity::getConfirmedPayloadJson, confirmedPayloadJson)
|
||||
.set(ReservationV4TaskCardEntity::getConfirmedBy, confirmedBy)
|
||||
.set(ReservationV4TaskCardEntity::getConfirmedAt, confirmedAt)
|
||||
.set(ReservationV4TaskCardEntity::getUpdatedAt, confirmedAt)
|
||||
.setSql("version = version + 1")
|
||||
.eq(ReservationV4TaskCardEntity::getHotelId, hotelId)
|
||||
.eq(ReservationV4TaskCardEntity::getId, taskCardId)
|
||||
.eq(ReservationV4TaskCardEntity::getVersion, expectedVersion)
|
||||
.eq(ReservationV4TaskCardEntity::getCardStatus, ReservationV4CardStatus.PENDING_CONFIRM.name())
|
||||
.isNull(ReservationV4TaskCardEntity::getLogicDeletedAt));
|
||||
return updated == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 V4 订单任务派生状态,用于卡片确认后刷新 OPEN / COMPLETED。
|
||||
*/
|
||||
@Override
|
||||
public boolean updateOrderTaskStatus(
|
||||
String hotelId,
|
||||
Long orderTaskId,
|
||||
String orderTaskStatus,
|
||||
LocalDateTime now) {
|
||||
int updated = orderTaskMapper.update(Wrappers.<ReservationV4OrderTaskEntity>lambdaUpdate()
|
||||
.set(ReservationV4OrderTaskEntity::getOrderTaskStatus, orderTaskStatus)
|
||||
.set(ReservationV4OrderTaskEntity::getUpdatedAt, now)
|
||||
.setSql("version = version + 1")
|
||||
.eq(ReservationV4OrderTaskEntity::getHotelId, hotelId)
|
||||
.eq(ReservationV4OrderTaskEntity::getId, orderTaskId)
|
||||
.isNull(ReservationV4OrderTaskEntity::getLogicDeletedAt));
|
||||
return updated == 1;
|
||||
}
|
||||
|
||||
private ReservationV4OrderTaskSnapshot toOrderTaskSnapshot(ReservationV4OrderTaskEntity entity) {
|
||||
return new ReservationV4OrderTaskSnapshot(
|
||||
entity.getId(),
|
||||
|
||||
@@ -86,4 +86,24 @@ public interface ReservationV4WorkflowRepository {
|
||||
Long expectedVersion,
|
||||
String cardStatus,
|
||||
LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 按 version 乐观锁确认 V4 任务卡,同时写入用户确认 payload、确认人和确认时间。
|
||||
*/
|
||||
boolean confirmTaskCardWithVersion(
|
||||
String hotelId,
|
||||
Long taskCardId,
|
||||
Long expectedVersion,
|
||||
String confirmedPayloadJson,
|
||||
String confirmedBy,
|
||||
LocalDateTime confirmedAt);
|
||||
|
||||
/**
|
||||
* 更新 V4 订单任务派生状态,用于卡片确认后刷新 OPEN / COMPLETED。
|
||||
*/
|
||||
boolean updateOrderTaskStatus(
|
||||
String hotelId,
|
||||
Long orderTaskId,
|
||||
String orderTaskStatus,
|
||||
LocalDateTime now);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service;
|
||||
|
||||
import cn.nianxx.thhotel.platform.security.common.dto.AuthenticatedUserContext;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4CardConfirmRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4SourceNotificationAckRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4OrderTaskDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4SourceNotificationDetailResult;
|
||||
|
||||
/**
|
||||
* Reservation V4 命令服务。承接卡片确认和 S10/S99 来源通知确认写操作。
|
||||
*/
|
||||
public interface ReservationV4CommandService {
|
||||
|
||||
/**
|
||||
* 确认指定 V4 任务卡,并返回刷新后的订单任务详情。
|
||||
*/
|
||||
ReservationV4OrderTaskDetailResult confirmTaskCard(
|
||||
Long orderTaskId,
|
||||
Long cardId,
|
||||
ReservationV4CardConfirmRequest request,
|
||||
AuthenticatedUserContext actor);
|
||||
|
||||
/**
|
||||
* 确认 S10/S99 来源通知已读或已处理,并返回刷新后的来源通知详情。
|
||||
*/
|
||||
ReservationV4SourceNotificationDetailResult ackSourceNotification(
|
||||
Long notificationId,
|
||||
ReservationV4SourceNotificationAckRequest request,
|
||||
AuthenticatedUserContext actor);
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.security.common.dto.AuthenticatedUserContext;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4NotificationStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4OrderTaskStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4CardConfirmRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4SourceNotificationAckRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4OrderTaskDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4SourceNotificationDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4SourceNotificationRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4WorkflowRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationV4CommandService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationV4QueryService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Reservation V4 命令服务实现。负责卡片确认、来源通知确认、状态刷新和业务审计。
|
||||
*/
|
||||
@Service
|
||||
public class ReservationV4CommandServiceImpl implements ReservationV4CommandService {
|
||||
|
||||
private static final String ACTOR_TYPE_USER = "USER";
|
||||
private static final String ACTION_V4_CARD_CONFIRM = "V4_CARD_CONFIRM";
|
||||
private static final String ACTION_V4_SOURCE_NOTIFICATION_ACK = "V4_SOURCE_NOTIFICATION_ACK";
|
||||
|
||||
private final ReservationV4WorkflowRepository workflowRepository;
|
||||
private final ReservationV4SourceNotificationRepository sourceNotificationRepository;
|
||||
private final ReservationAiWorkflowRepository auditRepository;
|
||||
private final ReservationV4QueryService queryService;
|
||||
private final HotelContextService hotelContextService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 注入 V4 Repository、查询服务、酒店上下文和审计边界,命令服务不直接依赖 Mapper。
|
||||
*/
|
||||
public ReservationV4CommandServiceImpl(
|
||||
ReservationV4WorkflowRepository workflowRepository,
|
||||
ReservationV4SourceNotificationRepository sourceNotificationRepository,
|
||||
ReservationAiWorkflowRepository auditRepository,
|
||||
ReservationV4QueryService queryService,
|
||||
HotelContextService hotelContextService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.workflowRepository = workflowRepository;
|
||||
this.sourceNotificationRepository = sourceNotificationRepository;
|
||||
this.auditRepository = auditRepository;
|
||||
this.queryService = queryService;
|
||||
this.hotelContextService = hotelContextService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认指定 V4 任务卡,并返回刷新后的订单任务详情。
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public ReservationV4OrderTaskDetailResult confirmTaskCard(
|
||||
Long orderTaskId,
|
||||
Long cardId,
|
||||
ReservationV4CardConfirmRequest request,
|
||||
AuthenticatedUserContext actor) {
|
||||
Long expectedVersion = requireVersion(request == null ? null : request.version());
|
||||
LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
|
||||
ReservationV4OrderTaskSnapshot orderTask = requireOrderTask(orderTaskId);
|
||||
requireHotelAccess(orderTask.hotelId());
|
||||
ReservationV4TaskCardSnapshot card = requireTaskCard(orderTask.hotelId(), cardId, orderTask.id());
|
||||
validateCardConfirmable(orderTask, card);
|
||||
ensureNoPriorOrderTaskBlocking(orderTask);
|
||||
ensureBasicInformationConfirmed(orderTask, card);
|
||||
|
||||
String confirmedPayloadJson = confirmedPayloadJson(card, request == null ? null : request.confirmedPayload());
|
||||
String actorId = actorIdentifier(actor);
|
||||
boolean updated = workflowRepository.confirmTaskCardWithVersion(
|
||||
orderTask.hotelId(),
|
||||
card.id(),
|
||||
expectedVersion,
|
||||
confirmedPayloadJson,
|
||||
actorId,
|
||||
now);
|
||||
if (!updated) {
|
||||
handleCardConfirmRace(orderTask.hotelId(), card.id());
|
||||
}
|
||||
refreshOrderTaskStatus(orderTask, now);
|
||||
writeCardConfirmAudit(orderTask, card, actorId, confirmedPayloadJson, now);
|
||||
return queryService.getOrderTaskDetail(orderTask.hotelId(), orderTask.id());
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认 S10/S99 来源通知已读或已处理,并返回刷新后的来源通知详情。
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public ReservationV4SourceNotificationDetailResult ackSourceNotification(
|
||||
Long notificationId,
|
||||
ReservationV4SourceNotificationAckRequest request,
|
||||
AuthenticatedUserContext actor) {
|
||||
Long expectedVersion = requireVersion(request == null ? null : request.version());
|
||||
LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
|
||||
ReservationV4SourceNotificationSnapshot notification = requireSourceNotification(notificationId);
|
||||
requireHotelAccess(notification.hotelId());
|
||||
if (ReservationV4NotificationStatus.ACKED.name().equals(notification.notificationStatus())) {
|
||||
return queryService.getSourceNotificationDetail(notification.hotelId(), notification.id());
|
||||
}
|
||||
if (!ReservationV4NotificationStatus.ACK_REQUIRED.name().equals(notification.notificationStatus())) {
|
||||
throw error(HttpStatus.CONFLICT, "V4_SOURCE_NOTIFICATION_NOT_ACKABLE", "当前来源通知状态不允许确认。");
|
||||
}
|
||||
String actorId = actorIdentifier(actor);
|
||||
boolean updated = sourceNotificationRepository.updateNotificationStatusWithVersion(
|
||||
notification.hotelId(),
|
||||
notification.id(),
|
||||
expectedVersion,
|
||||
ReservationV4NotificationStatus.ACKED.name(),
|
||||
actorId,
|
||||
now);
|
||||
if (!updated) {
|
||||
ReservationV4SourceNotificationSnapshot latest = requireSourceNotification(notificationId);
|
||||
if (ReservationV4NotificationStatus.ACKED.name().equals(latest.notificationStatus())) {
|
||||
return queryService.getSourceNotificationDetail(latest.hotelId(), latest.id());
|
||||
}
|
||||
throw error(HttpStatus.CONFLICT, "V4_SOURCE_NOTIFICATION_VERSION_CONFLICT", "来源通知版本已变化,请刷新后重试。");
|
||||
}
|
||||
writeSourceNotificationAckAudit(notification, actorId, request == null ? null : request.reason(), now);
|
||||
return queryService.getSourceNotificationDetail(notification.hotelId(), notification.id());
|
||||
}
|
||||
|
||||
private void validateCardConfirmable(
|
||||
ReservationV4OrderTaskSnapshot orderTask,
|
||||
ReservationV4TaskCardSnapshot card) {
|
||||
if (ReservationV4CardType.SOURCE_MESSAGE_DISPLAY.name().equals(card.cardType())) {
|
||||
throw error(HttpStatus.CONFLICT, "V4_CARD_NOT_CONFIRMABLE", "来源邮件展示卡不允许确认。");
|
||||
}
|
||||
if (ReservationV4CardStatus.CONFIRMED.name().equals(card.cardStatus())) {
|
||||
throw error(HttpStatus.CONFLICT, "V4_CARD_ALREADY_CONFIRMED", "该卡片已经确认,不能重复确认。");
|
||||
}
|
||||
if (ReservationV4CardStatus.REVIEW_REQUIRED.name().equals(card.cardStatus())) {
|
||||
throw error(HttpStatus.CONFLICT, "V4_CARD_REVIEW_REQUIRED", "该卡片需要先完成人工复核。");
|
||||
}
|
||||
if (!ReservationV4CardStatus.PENDING_CONFIRM.name().equals(card.cardStatus())) {
|
||||
throw error(HttpStatus.CONFLICT, "V4_CARD_NOT_CONFIRMABLE", "当前卡片状态不允许确认。");
|
||||
}
|
||||
if (ReservationV4OrderTaskStatus.COMPLETED.name().equals(orderTask.orderTaskStatus())) {
|
||||
throw error(HttpStatus.CONFLICT, "V4_ORDER_TASK_ALREADY_COMPLETED", "该 V4 订单任务已经完成。");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureBasicInformationConfirmed(
|
||||
ReservationV4OrderTaskSnapshot orderTask,
|
||||
ReservationV4TaskCardSnapshot card) {
|
||||
if (ReservationV4CardType.BASIC_INFORMATION.name().equals(card.cardType())) {
|
||||
return;
|
||||
}
|
||||
ReservationV4TaskCardSnapshot basicCard = workflowRepository.findTaskCardsByOrderTaskId(
|
||||
orderTask.hotelId(),
|
||||
orderTask.id()).stream()
|
||||
.filter(candidate -> ReservationV4CardType.BASIC_INFORMATION.name().equals(candidate.cardType()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> error(
|
||||
HttpStatus.CONFLICT,
|
||||
"V4_BASIC_INFORMATION_MISSING",
|
||||
"V4 订单任务缺少 Basic Information 卡。"));
|
||||
if (!ReservationV4CardStatus.CONFIRMED.name().equals(basicCard.cardStatus())) {
|
||||
throw error(
|
||||
HttpStatus.CONFLICT,
|
||||
"V4_BASIC_INFORMATION_NOT_CONFIRMED",
|
||||
"Basic Information 卡未确认前,业务卡不可确认。");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureNoPriorOrderTaskBlocking(ReservationV4OrderTaskSnapshot orderTask) {
|
||||
if (orderTask.orderId() == null) {
|
||||
return;
|
||||
}
|
||||
List<ReservationV4OrderTaskSnapshot> sameOrderTasks = workflowRepository.findOrderTasksByOrderIds(
|
||||
orderTask.hotelId(),
|
||||
List.of(orderTask.orderId()));
|
||||
for (ReservationV4OrderTaskSnapshot candidate : sameOrderTasks) {
|
||||
if (Objects.equals(candidate.id(), orderTask.id())) {
|
||||
return;
|
||||
}
|
||||
if (!ReservationV4OrderTaskStatus.COMPLETED.name().equals(candidate.orderTaskStatus())) {
|
||||
throw error(
|
||||
HttpStatus.CONFLICT,
|
||||
"V4_PRIOR_ORDER_TASK_NOT_COMPLETED",
|
||||
"同一本地订单下存在更早未完成的 V4 订单任务。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshOrderTaskStatus(ReservationV4OrderTaskSnapshot orderTask, LocalDateTime now) {
|
||||
List<ReservationV4TaskCardSnapshot> cards = workflowRepository.findTaskCardsByOrderTaskId(
|
||||
orderTask.hotelId(),
|
||||
orderTask.id());
|
||||
boolean hasUnfinishedCard = cards.stream()
|
||||
.filter(card -> !ReservationV4CardType.SOURCE_MESSAGE_DISPLAY.name().equals(card.cardType()))
|
||||
.anyMatch(card -> !ReservationV4CardStatus.CONFIRMED.name().equals(card.cardStatus()));
|
||||
String nextStatus = hasUnfinishedCard
|
||||
? ReservationV4OrderTaskStatus.OPEN.name()
|
||||
: ReservationV4OrderTaskStatus.COMPLETED.name();
|
||||
if (!Objects.equals(orderTask.orderTaskStatus(), nextStatus)) {
|
||||
boolean updated = workflowRepository.updateOrderTaskStatus(orderTask.hotelId(), orderTask.id(), nextStatus, now);
|
||||
if (!updated) {
|
||||
throw error(
|
||||
HttpStatus.CONFLICT,
|
||||
"V4_ORDER_TASK_STATUS_REFRESH_FAILED",
|
||||
"V4 订单任务状态刷新失败,请刷新后重试。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCardConfirmRace(String hotelId, Long cardId) {
|
||||
ReservationV4TaskCardSnapshot latest = workflowRepository.findTaskCardById(hotelId, cardId)
|
||||
.orElseThrow(() -> notFound("V4_TASK_CARD_NOT_FOUND", "V4 任务卡不存在。"));
|
||||
if (ReservationV4CardStatus.CONFIRMED.name().equals(latest.cardStatus())) {
|
||||
throw error(HttpStatus.CONFLICT, "V4_CARD_ALREADY_CONFIRMED", "该卡片已经确认,不能重复确认。");
|
||||
}
|
||||
throw error(HttpStatus.CONFLICT, "V4_CARD_VERSION_CONFLICT", "任务卡版本已变化,请刷新后重试。");
|
||||
}
|
||||
|
||||
private String confirmedPayloadJson(ReservationV4TaskCardSnapshot card, JsonNode confirmedPayload) {
|
||||
if (confirmedPayload != null && !confirmedPayload.isNull() && !confirmedPayload.isMissingNode()) {
|
||||
return toJson(confirmedPayload);
|
||||
}
|
||||
if (hasText(card.displayPayloadJson())) {
|
||||
return card.displayPayloadJson();
|
||||
}
|
||||
return "{}";
|
||||
}
|
||||
|
||||
private ReservationV4OrderTaskSnapshot requireOrderTask(Long orderTaskId) {
|
||||
return workflowRepository.findOrderTaskById(orderTaskId)
|
||||
.orElseThrow(() -> notFound("V4_ORDER_TASK_NOT_FOUND", "V4 订单任务不存在。"));
|
||||
}
|
||||
|
||||
private ReservationV4TaskCardSnapshot requireTaskCard(String hotelId, Long cardId, Long orderTaskId) {
|
||||
ReservationV4TaskCardSnapshot card = workflowRepository.findTaskCardById(hotelId, cardId)
|
||||
.orElseThrow(() -> notFound("V4_TASK_CARD_NOT_FOUND", "V4 任务卡不存在。"));
|
||||
if (!Objects.equals(orderTaskId, card.v4OrderTaskId())) {
|
||||
throw notFound("V4_TASK_CARD_NOT_FOUND", "V4 任务卡不存在。");
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
private ReservationV4SourceNotificationSnapshot requireSourceNotification(Long notificationId) {
|
||||
return sourceNotificationRepository.findSourceNotificationById(notificationId)
|
||||
.orElseThrow(() -> notFound("V4_SOURCE_NOTIFICATION_NOT_FOUND", "V4 来源通知不存在。"));
|
||||
}
|
||||
|
||||
private void requireHotelAccess(String hotelId) {
|
||||
try {
|
||||
hotelContextService.requireAccessibleHotel(hotelId);
|
||||
} catch (HotelContextException exception) {
|
||||
throw new ReservationTaskWorkflowException(
|
||||
exception.getStatus(),
|
||||
exception.getErrorCode(),
|
||||
exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Long requireVersion(Long version) {
|
||||
if (version == null) {
|
||||
throw error(HttpStatus.BAD_REQUEST, "V4_VERSION_REQUIRED", "请求必须携带当前 version。");
|
||||
}
|
||||
if (version < 0) {
|
||||
throw error(HttpStatus.BAD_REQUEST, "V4_VERSION_INVALID", "version 不能小于 0。");
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
private String actorIdentifier(AuthenticatedUserContext actor) {
|
||||
if (actor == null) {
|
||||
return "unknown-user";
|
||||
}
|
||||
if (hasText(actor.username())) {
|
||||
return actor.username();
|
||||
}
|
||||
return actor.userId() == null ? "unknown-user" : actor.userId().toString();
|
||||
}
|
||||
|
||||
private void writeCardConfirmAudit(
|
||||
ReservationV4OrderTaskSnapshot orderTask,
|
||||
ReservationV4TaskCardSnapshot card,
|
||||
String actorId,
|
||||
String confirmedPayloadJson,
|
||||
LocalDateTime now) {
|
||||
Map<String, Object> beforeSnapshot = new LinkedHashMap<>();
|
||||
beforeSnapshot.put("v4_order_task_id", orderTask.id().toString());
|
||||
beforeSnapshot.put("v4_task_card_id", card.id().toString());
|
||||
beforeSnapshot.put("card_type", card.cardType());
|
||||
beforeSnapshot.put("card_status", card.cardStatus());
|
||||
beforeSnapshot.put("version", card.version());
|
||||
beforeSnapshot.put("has_confirmed_payload", hasText(card.confirmedPayloadJson()));
|
||||
Map<String, Object> afterSnapshot = new LinkedHashMap<>();
|
||||
afterSnapshot.put("v4_order_task_id", orderTask.id().toString());
|
||||
afterSnapshot.put("v4_task_card_id", card.id().toString());
|
||||
afterSnapshot.put("card_type", card.cardType());
|
||||
afterSnapshot.put("card_status", ReservationV4CardStatus.CONFIRMED.name());
|
||||
afterSnapshot.put("confirmed_by", actorId);
|
||||
afterSnapshot.put("confirmed_payload_keys", payloadFieldNames(confirmedPayloadJson));
|
||||
auditRepository.insertAuditLog(new ReservationAuditLogDraft(
|
||||
orderTask.hotelId(),
|
||||
orderTask.orderId(),
|
||||
null,
|
||||
null,
|
||||
ACTOR_TYPE_USER,
|
||||
actorId,
|
||||
ACTION_V4_CARD_CONFIRM,
|
||||
null,
|
||||
toJson(beforeSnapshot),
|
||||
toJson(afterSnapshot),
|
||||
now));
|
||||
}
|
||||
|
||||
private void writeSourceNotificationAckAudit(
|
||||
ReservationV4SourceNotificationSnapshot notification,
|
||||
String actorId,
|
||||
String reason,
|
||||
LocalDateTime now) {
|
||||
Map<String, Object> beforeSnapshot = new LinkedHashMap<>();
|
||||
beforeSnapshot.put("v4_source_notification_id", notification.id().toString());
|
||||
beforeSnapshot.put("route_code", notification.routeCode());
|
||||
beforeSnapshot.put("notification_status", notification.notificationStatus());
|
||||
beforeSnapshot.put("version", notification.version());
|
||||
Map<String, Object> afterSnapshot = new LinkedHashMap<>();
|
||||
afterSnapshot.put("v4_source_notification_id", notification.id().toString());
|
||||
afterSnapshot.put("route_code", notification.routeCode());
|
||||
afterSnapshot.put("notification_status", ReservationV4NotificationStatus.ACKED.name());
|
||||
afterSnapshot.put("ack_by", actorId);
|
||||
auditRepository.insertAuditLog(new ReservationAuditLogDraft(
|
||||
notification.hotelId(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
ACTOR_TYPE_USER,
|
||||
actorId,
|
||||
ACTION_V4_SOURCE_NOTIFICATION_ACK,
|
||||
trimToNull(reason),
|
||||
toJson(beforeSnapshot),
|
||||
toJson(afterSnapshot),
|
||||
now));
|
||||
}
|
||||
|
||||
private List<String> payloadFieldNames(String json) {
|
||||
if (!hasText(json)) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
JsonNode node = objectMapper.readTree(json);
|
||||
if (node == null || !node.isObject()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> fieldNames = new ArrayList<>();
|
||||
Iterator<String> iterator = node.fieldNames();
|
||||
while (iterator.hasNext()) {
|
||||
fieldNames.add(iterator.next());
|
||||
}
|
||||
return fieldNames;
|
||||
} catch (Exception exception) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private String toJson(Object value) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(value);
|
||||
} catch (Exception exception) {
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
private ReservationTaskWorkflowException notFound(String errorCode, String message) {
|
||||
return new ReservationTaskWorkflowException(HttpStatus.NOT_FOUND, errorCode, message);
|
||||
}
|
||||
|
||||
private ReservationTaskWorkflowException error(HttpStatus status, String errorCode, String message) {
|
||||
return new ReservationTaskWorkflowException(status, errorCode, message);
|
||||
}
|
||||
|
||||
private boolean hasText(String value) {
|
||||
return trimToNull(value) != null;
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -423,14 +423,14 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
}
|
||||
return new ReservationV4ActionAvailabilityResult(
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ReservationV4ReadonlyReasonCode.COMMAND_API_PENDING.name(),
|
||||
ReservationV4ReadonlyReasonCode.PROCESSABLE.name(),
|
||||
null,
|
||||
null,
|
||||
"V4 卡片确认和复核写接口将在后续 checkpoint 开放。");
|
||||
null);
|
||||
}
|
||||
|
||||
private ReservationV4ActionAvailabilityResult cardAvailability(
|
||||
@@ -467,18 +467,29 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
if (ReservationV4CardStatus.CONFIRMED.name().equals(card.cardStatus())) {
|
||||
return readOnlyAvailability(ReservationV4ReadonlyReasonCode.CARD_LOCKED.name(), null, null);
|
||||
}
|
||||
if (ReservationV4CardStatus.PENDING_CONFIRM.name().equals(card.cardStatus())
|
||||
|| ReservationV4CardStatus.REVIEW_REQUIRED.name().equals(card.cardStatus())) {
|
||||
if (ReservationV4CardStatus.PENDING_CONFIRM.name().equals(card.cardStatus())) {
|
||||
return new ReservationV4ActionAvailabilityResult(
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
ReservationV4ReadonlyReasonCode.PROCESSABLE.name(),
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
if (ReservationV4CardStatus.REVIEW_REQUIRED.name().equals(card.cardStatus())) {
|
||||
return new ReservationV4ActionAvailabilityResult(
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ReservationV4ReadonlyReasonCode.COMMAND_API_PENDING.name(),
|
||||
ReservationV4ReadonlyReasonCode.REVIEW_API_PENDING.name(),
|
||||
null,
|
||||
null,
|
||||
"V4 卡片确认和复核写接口将在后续 checkpoint 开放。");
|
||||
"V4 人工复核写接口将在后续 checkpoint 开放。");
|
||||
}
|
||||
return readOnlyAvailability(ReservationV4ReadonlyReasonCode.CARD_LOCKED.name(), null, null);
|
||||
}
|
||||
@@ -489,15 +500,15 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
return readOnlyAvailability(ReservationV4ReadonlyReasonCode.CARD_LOCKED.name(), null, null);
|
||||
}
|
||||
return new ReservationV4ActionAvailabilityResult(
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ReservationV4ReadonlyReasonCode.ACK_API_PENDING.name(),
|
||||
ReservationV4ReadonlyReasonCode.PROCESSABLE.name(),
|
||||
null,
|
||||
null,
|
||||
"V4 来源通知确认写接口将在后续 checkpoint 开放。");
|
||||
null);
|
||||
}
|
||||
|
||||
private ReservationV4ActionAvailabilityResult readOnlyAvailability(
|
||||
|
||||
Reference in New Issue
Block a user