实现Rooming List确认自动置DEF

This commit is contained in:
andy
2026-07-21 09:16:05 +07:00
parent 8d181ee8ad
commit b44ab94113
5 changed files with 558 additions and 11 deletions

View File

@@ -342,6 +342,28 @@ public class MybatisReservationV4WorkflowRepository implements ReservationV4Work
return updated == 1;
}
/**
* 按 version 乐观锁更新已确认 V4 任务卡的确认快照,用于后续业务卡触发的订单事实派生修正。
*/
@Override
public boolean updateConfirmedTaskCardPayloadWithVersion(
String hotelId,
Long taskCardId,
Long expectedVersion,
String confirmedPayloadJson,
LocalDateTime updatedAt) {
int updated = taskCardMapper.update(Wrappers.<ReservationV4TaskCardEntity>lambdaUpdate()
.set(ReservationV4TaskCardEntity::getConfirmedPayloadJson, confirmedPayloadJson)
.set(ReservationV4TaskCardEntity::getUpdatedAt, updatedAt)
.setSql("version = version + 1")
.eq(ReservationV4TaskCardEntity::getHotelId, hotelId)
.eq(ReservationV4TaskCardEntity::getId, taskCardId)
.eq(ReservationV4TaskCardEntity::getVersion, expectedVersion)
.eq(ReservationV4TaskCardEntity::getCardStatus, ReservationV4CardStatus.CONFIRMED.name())
.isNull(ReservationV4TaskCardEntity::getLogicDeletedAt));
return updated == 1;
}
/**
* 在复核场景确认 V4 订单任务归属,只更新当前订单任务的本地订单绑定和归属状态。
*/

View File

@@ -110,6 +110,16 @@ public interface ReservationV4WorkflowRepository {
String confirmedBy,
LocalDateTime confirmedAt);
/**
* 按 version 乐观锁更新已确认 V4 任务卡的确认快照,用于后续业务卡触发的订单事实派生修正。
*/
boolean updateConfirmedTaskCardPayloadWithVersion(
String hotelId,
Long taskCardId,
Long expectedVersion,
String confirmedPayloadJson,
LocalDateTime updatedAt);
/**
* 在复核场景确认 V4 订单任务归属,只更新当前订单任务的本地订单绑定和归属状态。
*/

View File

@@ -61,6 +61,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
private static final String ACTION_V4_CARD_CONFIRM = "V4_CARD_CONFIRM";
private static final String ACTION_V4_CARD_REVIEW_RESOLVE = "V4_CARD_REVIEW_RESOLVE";
private static final String ACTION_V4_SOURCE_NOTIFICATION_ACK = "V4_SOURCE_NOTIFICATION_ACK";
private static final String ACTION_V4_ROOMING_LIST_AUTO_DEF = "V4_ROOMING_LIST_AUTO_DEF";
private static final String EVENT_NEW_BOOKING = "NEW_BOOKING";
private static final String EVENT_UPDATE_BOOKING = "UPDATE_BOOKING";
private static final String EVENT_CANCEL_BOOKING = "CANCEL_BOOKING";
@@ -68,6 +69,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
private static final String BOOKING_TYPE_FIT = "FIT";
private static final String LOCATOR_TYPE_GROUP_CODE = "GROUP_CODE";
private static final String GROUP_BOOKING_STATUS_TEN = "TEN";
private static final String GROUP_BOOKING_STATUS_DEF = "DEF";
private static final Map<String, String> GROUP_BOOKING_STATUS_LABELS = Map.of(
"TEN", "TEN-Tentative",
"DEF", "DEF-Definite",
@@ -183,6 +185,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
}
refreshOrderTaskStatus(orderTask, now);
writeCardConfirmAudit(orderTask, card, actorId, confirmedPayloadJson, now);
applyRoomingListGroupDefIfNeeded(orderTask, card, actorId, now);
return queryService.getOrderTaskDetail(orderTask.hotelId(), orderTask.id());
}
@@ -258,6 +261,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
}
refreshOrderTaskStatus(latestOrderTask, now);
writeCardReviewResolveAudit(orderTask, card, actorId, normalizedOverrides, confirmedOrderId, request, now);
applyRoomingListGroupDefIfNeeded(latestOrderTask, card, actorId, now);
return queryService.getOrderTaskDetail(orderTask.hotelId(), orderTask.id());
}
@@ -1186,6 +1190,134 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
return roomInformationDisplayValues(orderTask.targetBookingType(), current, true);
}
/**
* Rooming List 确认后Group 订单需要自动把 Group Booking Status 收口为 DEF。
*/
private void applyRoomingListGroupDefIfNeeded(
ReservationV4OrderTaskSnapshot orderTask,
ReservationV4TaskCardSnapshot card,
String actorId,
LocalDateTime now) {
if (!ReservationV4CardType.ROOMING_LIST.name().equals(card.cardType())
|| !BOOKING_TYPE_GROUP.equals(orderTask.targetBookingType())) {
return;
}
ReservationV4TaskCardSnapshot roomInformationCard = latestConfirmedRoomInformationCard(orderTask);
if (roomInformationCard == null) {
writeRoomingListAutoDefAudit(
orderTask,
card,
null,
actorId,
null,
"SKIPPED_NO_ROOM_INFORMATION_PROJECTION",
now);
return;
}
ObjectNode payload = confirmedPayloadObject(roomInformationCard.confirmedPayloadJson());
JsonNode finalValuesNode = payload.path("room_information").path("final_values");
if (!finalValuesNode.isObject()) {
writeRoomingListAutoDefAudit(
orderTask,
card,
roomInformationCard,
actorId,
null,
"SKIPPED_ROOM_INFORMATION_FINAL_VALUES_MISSING",
now);
return;
}
ObjectNode finalValues = (ObjectNode) finalValuesNode;
String previousStatus = textAt(finalValues, "group_booking_status");
finalValues.put("group_booking_status", GROUP_BOOKING_STATUS_DEF);
normalizeRoomInformationDerivedFields(BOOKING_TYPE_GROUP, finalValues);
payload.withObject("/room_information").set(
"change_summary",
changeSummary(payload.path("room_information").path("current_values"), finalValues));
boolean updated = workflowRepository.updateConfirmedTaskCardPayloadWithVersion(
roomInformationCard.hotelId(),
roomInformationCard.id(),
roomInformationCard.version(),
toJson(payload),
now);
writeRoomingListAutoDefAudit(
orderTask,
card,
roomInformationCard,
actorId,
previousStatus,
updated ? "UPDATED" : "SKIPPED_VERSION_CHANGED",
now);
}
/**
* 查找同订单最新一张已确认 Room Information 卡,作为 Rooming List 自动 DEF 的可更新订单投影。
*/
private ReservationV4TaskCardSnapshot latestConfirmedRoomInformationCard(ReservationV4OrderTaskSnapshot orderTask) {
if (orderTask.orderId() == null) {
return null;
}
List<ReservationV4OrderTaskSnapshot> orderTasks = workflowRepository.findOrderTasksByOrderIds(
orderTask.hotelId(),
List.of(orderTask.orderId()));
List<Long> updatableOrderTaskIds = new ArrayList<>();
for (ReservationV4OrderTaskSnapshot candidate : orderTasks) {
updatableOrderTaskIds.add(candidate.id());
if (Objects.equals(candidate.id(), orderTask.id())) {
break;
}
}
if (updatableOrderTaskIds.isEmpty()) {
return null;
}
ReservationV4TaskCardSnapshot latest = null;
for (ReservationV4TaskCardSnapshot candidate : workflowRepository.findTaskCardsByOrderTaskIds(
orderTask.hotelId(),
updatableOrderTaskIds)) {
if (!isUpdatableConfirmedRoomInformationCard(candidate)) {
continue;
}
if (latest == null || isNewerTaskCard(candidate, latest)) {
latest = candidate;
}
}
return latest;
}
/**
* 判断任务卡是否是已确认且具备稳定 final_values 的 Room Information 快照。
*/
private boolean isUpdatableConfirmedRoomInformationCard(ReservationV4TaskCardSnapshot card) {
if (card == null
|| !ReservationV4CardType.ROOM_INFORMATION.name().equals(card.cardType())
|| !ReservationV4CardStatus.CONFIRMED.name().equals(card.cardStatus())) {
return false;
}
ObjectNode confirmedPayload = confirmedPayloadObject(card.confirmedPayloadJson());
return BOOKING_TYPE_GROUP.equals(textAt(confirmedPayload.path("room_information"), "booking_type"))
&& confirmedPayload.path("room_information").path("final_values").isObject();
}
/**
* 在候选 Room Information 快照中选择最近一次更新的卡片。
*/
private boolean isNewerTaskCard(ReservationV4TaskCardSnapshot candidate, ReservationV4TaskCardSnapshot current) {
LocalDateTime candidateTime = candidate.updatedAt() == null ? candidate.createdAt() : candidate.updatedAt();
LocalDateTime currentTime = current.updatedAt() == null ? current.createdAt() : current.updatedAt();
if (candidateTime != null && currentTime != null && !candidateTime.equals(currentTime)) {
return candidateTime.isAfter(currentTime);
}
return candidate.id() != null && current.id() != null && candidate.id() > current.id();
}
/**
* 读取确认快照为可变 ObjectNode解析失败或非对象时降级为空对象。
*/
private ObjectNode confirmedPayloadObject(String confirmedPayloadJson) {
JsonNode payload = parseJson(confirmedPayloadJson);
return payload.isObject() ? ((ObjectNode) payload).deepCopy() : objectMapper.createObjectNode();
}
private ObjectNode confirmedRoomInformationFinalValues(String confirmedPayloadJson) {
JsonNode payload = parseJson(confirmedPayloadJson);
JsonNode finalValues = payload.path("room_information").path("final_values");
@@ -1944,6 +2076,47 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
now));
}
/**
* 写入 Rooming List 确认触发 Group Booking Status 自动 DEF 的安全审计摘要。
*/
private void writeRoomingListAutoDefAudit(
ReservationV4OrderTaskSnapshot orderTask,
ReservationV4TaskCardSnapshot roomingListCard,
ReservationV4TaskCardSnapshot roomInformationCard,
String actorId,
String previousStatus,
String updateStatus,
LocalDateTime now) {
Map<String, Object> beforeSnapshot = new LinkedHashMap<>();
beforeSnapshot.put("trigger", "ROOMING_LIST_CONFIRM");
beforeSnapshot.put("v4_order_task_id", orderTask.id().toString());
beforeSnapshot.put("rooming_list_card_id", roomingListCard.id().toString());
beforeSnapshot.put("room_information_card_id",
roomInformationCard == null ? null : roomInformationCard.id().toString());
beforeSnapshot.put("previous_group_booking_status", previousStatus);
Map<String, Object> afterSnapshot = new LinkedHashMap<>();
afterSnapshot.put("trigger", "ROOMING_LIST_CONFIRM");
afterSnapshot.put("v4_order_task_id", orderTask.id().toString());
afterSnapshot.put("rooming_list_card_id", roomingListCard.id().toString());
afterSnapshot.put("room_information_card_id",
roomInformationCard == null ? null : roomInformationCard.id().toString());
afterSnapshot.put("update_status", updateStatus);
afterSnapshot.put("target_group_booking_status", GROUP_BOOKING_STATUS_DEF);
afterSnapshot.put("target_group_booking_status_label", GROUP_BOOKING_STATUS_LABELS.get(GROUP_BOOKING_STATUS_DEF));
auditRepository.insertAuditLog(new ReservationAuditLogDraft(
orderTask.hotelId(),
orderTask.orderId(),
null,
null,
ACTOR_TYPE_USER,
actorId,
ACTION_V4_ROOMING_LIST_AUTO_DEF,
"Rooming List 确认触发 Group Booking Status 自动置 DEF。",
toJson(beforeSnapshot),
toJson(afterSnapshot),
now));
}
private void writeCardReviewResolveAudit(
ReservationV4OrderTaskSnapshot orderTask,
ReservationV4TaskCardSnapshot card,