统一V4待处理计数并补齐审计查询
This commit is contained in:
@@ -15,6 +15,7 @@ import java.time.OffsetDateTime;
|
||||
* @param confirmationNumber Confirmation No.,仅订单业务号类型为 CONFIRMATION_NUMBER 时返回
|
||||
* @param displayName 订单展示名
|
||||
* @param openTaskCount 未关闭任务数,排除 COMPLETED 和 FAILED
|
||||
* @param openWorkItemCount 订单列表统一待处理工作项数量,第一版按 V4 未完成订单任务数计算,不叠加旧任务
|
||||
* @param nextProcessableTaskId 下一条当前可处理任务 ID
|
||||
* @param nextV4OrderTaskId 当前订单下第一条仍需用户处理的 V4 订单任务 ID
|
||||
* @param nextV4ActionCardId 当前 V4 订单任务下第一张仍需确认或复核的卡片 ID
|
||||
@@ -43,6 +44,8 @@ public record ReservationOrderListItemResult(
|
||||
String displayName,
|
||||
@JsonProperty("open_task_count")
|
||||
Integer openTaskCount,
|
||||
@JsonProperty("open_work_item_count")
|
||||
Integer openWorkItemCount,
|
||||
@JsonProperty("next_processable_task_id")
|
||||
String nextProcessableTaskId,
|
||||
@JsonProperty("next_v4_order_task_id")
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* V4 订单任务审计列表响应。用于前端展示卡片确认和人工复核解阻流水。
|
||||
*/
|
||||
public record ReservationV4OrderTaskAuditListResult(
|
||||
/** V4 订单任务 ID。 */
|
||||
@JsonProperty("order_task_id")
|
||||
String orderTaskId,
|
||||
|
||||
/** 已脱敏的审计流水列表。 */
|
||||
List<ReservationTaskAuditLogResult> items
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* V4 来源通知审计列表响应。用于前端展示 S10/S99 已读确认流水。
|
||||
*/
|
||||
public record ReservationV4SourceNotificationAuditListResult(
|
||||
/** V4 来源通知 ID。 */
|
||||
@JsonProperty("notification_id")
|
||||
String notificationId,
|
||||
|
||||
/** 已脱敏的审计流水列表。 */
|
||||
List<ReservationTaskAuditLogResult> items
|
||||
) {
|
||||
}
|
||||
@@ -4,8 +4,10 @@ import cn.nianxx.thhotel.platform.access.common.enums.PlatformPermissionCode;
|
||||
import cn.nianxx.thhotel.platform.security.service.FrontendAuthorizationService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4OrderTaskQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4WorkbenchQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4OrderTaskAuditListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4OrderTaskDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4OrderTaskListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4SourceNotificationAuditListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4SourceNotificationDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4WorkbenchListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationV4QueryService;
|
||||
@@ -89,6 +91,17 @@ public class ReservationV4QueryController {
|
||||
return v4QueryService.getOrderTaskDetail(hotelId, orderTaskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 V4 订单任务审计流水,用于前端展示卡片确认和复核记录。
|
||||
*/
|
||||
@GetMapping(value = "/order-tasks/{orderTaskId}/audits", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ReservationV4OrderTaskAuditListResult listOrderTaskAudits(
|
||||
@PathVariable Long orderTaskId,
|
||||
@RequestParam(name = "hotel_id", required = false) String hotelId) {
|
||||
authorizationService.requirePermission(PlatformPermissionCode.RESERVATION_AUDIT_READ.name());
|
||||
return v4QueryService.listOrderTaskAudits(hotelId, orderTaskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 V4 S10/S99 来源通知详情,只返回安全邮件摘要和通知状态。
|
||||
*/
|
||||
@@ -99,4 +112,15 @@ public class ReservationV4QueryController {
|
||||
authorizationService.requirePermission(PlatformPermissionCode.RESERVATION_TASK_READ.name());
|
||||
return v4QueryService.getSourceNotificationDetail(hotelId, notificationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 V4 来源通知审计流水,用于前端展示 S10/S99 确认已读记录。
|
||||
*/
|
||||
@GetMapping(value = "/source-notifications/{notificationId}/audits", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ReservationV4SourceNotificationAuditListResult listSourceNotificationAudits(
|
||||
@PathVariable Long notificationId,
|
||||
@RequestParam(name = "hotel_id", required = false) String hotelId) {
|
||||
authorizationService.requirePermission(PlatformPermissionCode.RESERVATION_AUDIT_READ.name());
|
||||
return v4QueryService.listSourceNotificationAudits(hotelId, notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ import org.springframework.stereotype.Repository;
|
||||
@Repository
|
||||
public class MybatisReservationAiWorkflowRepository implements ReservationAiWorkflowRepository {
|
||||
|
||||
private static final List<String> V4_ORDER_TASK_AUDIT_ACTIONS = List.of(
|
||||
"V4_CARD_CONFIRM",
|
||||
"V4_CARD_REVIEW_RESOLVE");
|
||||
private static final String ACTION_V4_SOURCE_NOTIFICATION_ACK = "V4_SOURCE_NOTIFICATION_ACK";
|
||||
|
||||
private final ReservationAiBatchMapper batchMapper;
|
||||
private final ReservationAiTransitionMapper transitionMapper;
|
||||
private final ReservationOrderMapper orderMapper;
|
||||
@@ -900,6 +905,60 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 V4 订单任务审计流水。V4 审计写入时不占用旧 task_id,通过快照目标 ID 做第一版关联。
|
||||
*/
|
||||
@Override
|
||||
public List<ReservationAuditLogSnapshot> findAuditLogsByV4OrderTaskId(String hotelId, Long orderTaskId) {
|
||||
return findAuditLogsBySnapshotTarget(
|
||||
hotelId,
|
||||
V4_ORDER_TASK_AUDIT_ACTIONS,
|
||||
"v4_order_task_id",
|
||||
orderTaskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 V4 来源通知审计流水。第一版仅包含确认已读 / 已处理 ack 记录。
|
||||
*/
|
||||
@Override
|
||||
public List<ReservationAuditLogSnapshot> findAuditLogsByV4SourceNotificationId(String hotelId, Long notificationId) {
|
||||
return findAuditLogsBySnapshotTarget(
|
||||
hotelId,
|
||||
List.of(ACTION_V4_SOURCE_NOTIFICATION_ACK),
|
||||
"v4_source_notification_id",
|
||||
notificationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按审计快照中的目标字段过滤 V4 审计记录,避免旧 task_id 为空导致 V4 审计不可查。
|
||||
*/
|
||||
private List<ReservationAuditLogSnapshot> findAuditLogsBySnapshotTarget(
|
||||
String hotelId,
|
||||
List<String> actions,
|
||||
String targetFieldName,
|
||||
Long targetId) {
|
||||
if (targetId == null) {
|
||||
return List.of();
|
||||
}
|
||||
String targetFieldToken = "\"" + targetFieldName + "\"";
|
||||
String targetIdToken = "\"" + targetId + "\"";
|
||||
return auditLogMapper.selectList(Wrappers.<ReservationAuditLogEntity>lambdaQuery()
|
||||
.eq(ReservationAuditLogEntity::getHotelId, hotelId)
|
||||
.in(ReservationAuditLogEntity::getAction, actions)
|
||||
.and(wrapper -> wrapper
|
||||
.and(beforeSnapshot -> beforeSnapshot
|
||||
.like(ReservationAuditLogEntity::getBeforeSnapshotJson, targetFieldToken)
|
||||
.like(ReservationAuditLogEntity::getBeforeSnapshotJson, targetIdToken))
|
||||
.or(afterSnapshot -> afterSnapshot
|
||||
.like(ReservationAuditLogEntity::getAfterSnapshotJson, targetFieldToken)
|
||||
.like(ReservationAuditLogEntity::getAfterSnapshotJson, targetIdToken)))
|
||||
.orderByAsc(ReservationAuditLogEntity::getOccurredAt)
|
||||
.orderByAsc(ReservationAuditLogEntity::getId))
|
||||
.stream()
|
||||
.map(this::toAuditLogSnapshot)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按业务 key 查询 AI transition。该查询只用于补充待处理任务上下文,不替代订单查询。
|
||||
*/
|
||||
|
||||
@@ -283,4 +283,14 @@ public interface ReservationAiWorkflowRepository {
|
||||
* 查询任务审计流水。
|
||||
*/
|
||||
List<ReservationAuditLogSnapshot> findAuditLogsByTaskId(String hotelId, Long taskId);
|
||||
|
||||
/**
|
||||
* 按 V4 订单任务 ID 查询审计流水。V4 审计第一版通过快照中的目标 ID 关联。
|
||||
*/
|
||||
List<ReservationAuditLogSnapshot> findAuditLogsByV4OrderTaskId(String hotelId, Long orderTaskId);
|
||||
|
||||
/**
|
||||
* 按 V4 来源通知 ID 查询审计流水。第一版仅返回 S10/S99 ack 相关记录。
|
||||
*/
|
||||
List<ReservationAuditLogSnapshot> findAuditLogsByV4SourceNotificationId(String hotelId, Long notificationId);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package cn.nianxx.thhotel.workflows.reservation.service;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4OrderTaskQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4WorkbenchQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4OrderTaskDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4OrderTaskAuditListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4OrderTaskListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4SourceNotificationAuditListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4SourceNotificationDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4WorkbenchListResult;
|
||||
|
||||
@@ -27,8 +29,18 @@ public interface ReservationV4QueryService {
|
||||
*/
|
||||
ReservationV4OrderTaskDetailResult getOrderTaskDetail(String hotelId, Long orderTaskId);
|
||||
|
||||
/**
|
||||
* 查询 V4 订单任务审计流水,只返回前端可展示的脱敏审计摘要。
|
||||
*/
|
||||
ReservationV4OrderTaskAuditListResult listOrderTaskAudits(String hotelId, Long orderTaskId);
|
||||
|
||||
/**
|
||||
* 查询 V4 S10/S99 来源通知详情,不返回业务订单任务结构。
|
||||
*/
|
||||
ReservationV4SourceNotificationDetailResult getSourceNotificationDetail(String hotelId, Long notificationId);
|
||||
|
||||
/**
|
||||
* 查询 V4 来源通知审计流水,只返回 S10/S99 ack 相关脱敏审计摘要。
|
||||
*/
|
||||
ReservationV4SourceNotificationAuditListResult listSourceNotificationAudits(String hotelId, Long notificationId);
|
||||
}
|
||||
|
||||
@@ -574,6 +574,7 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
confirmationNumber(order),
|
||||
order.displayName(),
|
||||
openTaskCount(tasks),
|
||||
v4NextAction.openOrderTaskCount(),
|
||||
nextProcessableTaskId(tasks, availabilityByTaskId),
|
||||
v4NextAction.orderTaskId(),
|
||||
v4NextAction.cardId(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageSummaryResponse;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationPageSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4AccountCatalogItem;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskSnapshot;
|
||||
@@ -24,13 +25,16 @@ import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4Workb
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationAiTransitionDisplayResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderSummaryResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationPaginationResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskAuditLogResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4ActionAvailabilityResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4CardCountsResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4OrderTaskAuditListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4OrderTaskDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4OrderTaskListItemResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4OrderTaskListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4OrderTaskSummaryResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4SourceMessageSummaryResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4SourceNotificationAuditListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4SourceNotificationDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4SourceNotificationSummaryResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4TaskCardFieldResult;
|
||||
@@ -44,6 +48,7 @@ import cn.nianxx.thhotel.workflows.reservation.service.ReservationV4DirectorySer
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationV4QueryService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.NullNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -53,6 +58,7 @@ import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -251,6 +257,21 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
orderAvailability);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 V4 订单任务审计流水,按订单任务实际酒店做访问权校验后返回脱敏摘要。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public ReservationV4OrderTaskAuditListResult listOrderTaskAudits(String hotelId, Long orderTaskId) {
|
||||
ReservationV4OrderTaskSnapshot orderTask = requireOrderTask(orderTaskId, hotelId);
|
||||
List<ReservationTaskAuditLogResult> items = aiWorkflowRepository
|
||||
.findAuditLogsByV4OrderTaskId(orderTask.hotelId(), orderTask.id())
|
||||
.stream()
|
||||
.map(this::toAuditLogResult)
|
||||
.toList();
|
||||
return new ReservationV4OrderTaskAuditListResult(orderTask.id().toString(), items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 V4 S10/S99 来源通知详情,只返回安全邮件摘要和通知状态。
|
||||
*/
|
||||
@@ -269,6 +290,23 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
availability);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 V4 来源通知审计流水,按通知实际酒店做访问权校验后返回脱敏摘要。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public ReservationV4SourceNotificationAuditListResult listSourceNotificationAudits(
|
||||
String hotelId,
|
||||
Long notificationId) {
|
||||
ReservationV4SourceNotificationSnapshot notification = requireSourceNotification(notificationId, hotelId);
|
||||
List<ReservationTaskAuditLogResult> items = aiWorkflowRepository
|
||||
.findAuditLogsByV4SourceNotificationId(notification.hotelId(), notification.id())
|
||||
.stream()
|
||||
.map(this::toAuditLogResult)
|
||||
.toList();
|
||||
return new ReservationV4SourceNotificationAuditListResult(notification.id().toString(), items);
|
||||
}
|
||||
|
||||
private ReservationPageSnapshot<ReservationV4SourceNotificationSnapshot> querySourceNotificationPage(
|
||||
String hotelId,
|
||||
String notificationStatus,
|
||||
@@ -399,6 +437,89 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
availability);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 V4 审计快照为前端审计行,时间统一输出 UTC ISO 字符串。
|
||||
*/
|
||||
private ReservationTaskAuditLogResult toAuditLogResult(ReservationAuditLogSnapshot snapshot) {
|
||||
return new ReservationTaskAuditLogResult(
|
||||
snapshot.id().toString(),
|
||||
snapshot.orderId() == null ? null : snapshot.orderId().toString(),
|
||||
snapshot.taskId() == null ? null : snapshot.taskId().toString(),
|
||||
snapshot.operationId() == null ? null : snapshot.operationId().toString(),
|
||||
snapshot.actorType(),
|
||||
snapshot.actorId(),
|
||||
snapshot.action(),
|
||||
snapshot.reason(),
|
||||
auditPayloadToObject(snapshot.beforeSnapshotJson()),
|
||||
auditPayloadToObject(snapshot.afterSnapshotJson()),
|
||||
auditOccurredAt(snapshot));
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化审计发生时间,固定输出带秒的 UTC ISO 8601 字符串。
|
||||
*/
|
||||
private String auditOccurredAt(ReservationAuditLogSnapshot snapshot) {
|
||||
if (snapshot.occurredAt() == null) {
|
||||
return null;
|
||||
}
|
||||
return UtcTimeFormatter.toUtcOffsetDateTime(snapshot.occurredAt()).toInstant().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将审计 JSON 快照转换为安全对象,避免业务审计展示泄漏原始正文或敏感 token。
|
||||
*/
|
||||
private Object auditPayloadToObject(String payloadJson) {
|
||||
JsonNode payload = parseJson(payloadJson);
|
||||
if (payload == null || payload.isNull() || payload.isMissingNode()) {
|
||||
return null;
|
||||
}
|
||||
return objectMapper.convertValue(sanitizeAuditPayload(payload), Object.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归清理审计快照中的敏感字段,保留状态码、目标 ID 和普通业务摘要。
|
||||
*/
|
||||
private JsonNode sanitizeAuditPayload(JsonNode payload) {
|
||||
if (payload == null || payload.isNull() || payload.isMissingNode()) {
|
||||
return NullNode.getInstance();
|
||||
}
|
||||
if (payload.isObject()) {
|
||||
ObjectNode sanitized = objectMapper.createObjectNode();
|
||||
Iterator<Map.Entry<String, JsonNode>> iterator = payload.fields();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, JsonNode> entry = iterator.next();
|
||||
if (isSensitiveAuditField(entry.getKey())) {
|
||||
continue;
|
||||
}
|
||||
sanitized.set(entry.getKey(), sanitizeAuditPayload(entry.getValue()));
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
if (payload.isArray()) {
|
||||
ArrayNode sanitized = objectMapper.createArrayNode();
|
||||
for (JsonNode item : payload) {
|
||||
sanitized.add(sanitizeAuditPayload(item));
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断审计字段名是否属于前端不应展示的敏感信息。
|
||||
*/
|
||||
private boolean isSensitiveAuditField(String fieldName) {
|
||||
String normalized = fieldName == null ? "" : fieldName.toLowerCase(Locale.ROOT);
|
||||
return "raw".equals(normalized)
|
||||
|| normalized.endsWith("_raw")
|
||||
|| normalized.contains("payload_json")
|
||||
|| normalized.contains("private_url")
|
||||
|| normalized.contains("secret")
|
||||
|| normalized.contains("token")
|
||||
|| normalized.contains("body")
|
||||
|| normalized.contains("html");
|
||||
}
|
||||
|
||||
private ReservationV4CardCountsResult cardCounts(List<ReservationV4TaskCardSnapshot> cards) {
|
||||
List<ReservationV4TaskCardSnapshot> safeCards = cards == null ? List.of() : cards;
|
||||
return new ReservationV4CardCountsResult(
|
||||
|
||||
@@ -588,12 +588,16 @@ class ReservationFrontendQueryControllerTest {
|
||||
.value(contains("ACTIVE")))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + activeOrderId + "')].open_task_count")
|
||||
.value(contains(1)))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + activeOrderId + "')].open_work_item_count")
|
||||
.value(contains(0)))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + activeOrderId + "')].next_processable_task_id")
|
||||
.value(contains(pendingTaskId.toString())))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + endedOrderId + "')].order_status")
|
||||
.value(contains("ENDED")))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + endedOrderId + "')].open_task_count")
|
||||
.value(contains(0)))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + endedOrderId + "')].open_work_item_count")
|
||||
.value(contains(0)))
|
||||
.andExpect(jsonPath("$.page.total").value(2));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders")
|
||||
@@ -732,6 +736,8 @@ class ReservationFrontendQueryControllerTest {
|
||||
.value(contains(legacyTaskId.toString())))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + basicFirstOrderId + "')].v4_open_order_task_count")
|
||||
.value(contains(2)))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + basicFirstOrderId + "')].open_work_item_count")
|
||||
.value(contains(2)))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + basicFirstOrderId + "')].next_v4_order_task_id")
|
||||
.value(contains(basicFirstOrderTask.id().toString())))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + basicFirstOrderId + "')].next_v4_action_card_id")
|
||||
@@ -742,6 +748,8 @@ class ReservationFrontendQueryControllerTest {
|
||||
.value(contains("PENDING_CONFIRM")))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + reviewFirstOrderId + "')].v4_open_order_task_count")
|
||||
.value(contains(1)))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + reviewFirstOrderId + "')].open_work_item_count")
|
||||
.value(contains(1)))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + reviewFirstOrderId + "')].next_v4_order_task_id")
|
||||
.value(contains(reviewOrderTask.id().toString())))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + reviewFirstOrderId + "')].next_v4_action_card_id")
|
||||
@@ -760,6 +768,8 @@ class ReservationFrontendQueryControllerTest {
|
||||
.value(contains("PENDING_CONFIRM")))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + completedOnlyOrderId + "')].v4_open_order_task_count")
|
||||
.value(contains(0)))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + completedOnlyOrderId + "')].open_work_item_count")
|
||||
.value(contains(0)))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + completedOnlyOrderId + "')].next_v4_order_task_id")
|
||||
.value(contains(nullValue())))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + completedOnlyOrderId + "')].next_v4_action_type")
|
||||
@@ -768,6 +778,8 @@ class ReservationFrontendQueryControllerTest {
|
||||
.value(contains(nullValue())))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + noV4OrderId + "')].v4_open_order_task_count")
|
||||
.value(contains(0)))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + noV4OrderId + "')].open_work_item_count")
|
||||
.value(contains(0)))
|
||||
.andExpect(jsonPath("$.items[?(@.order_id=='" + noV4OrderId + "')].next_v4_action_type")
|
||||
.value(contains("NONE")));
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResu
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchDraft;
|
||||
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.ReservationV4OrderTaskDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationDraft;
|
||||
@@ -367,6 +368,58 @@ class ReservationV4QueryControllerTest {
|
||||
.andExpect(content().string(not(containsString("Sensitive raw notification body"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnV4OrderTaskAudits() throws Exception {
|
||||
ReservationV4OrderTaskSnapshot orderTask = seedOrderTask(
|
||||
"mail-v4-query-order-task-audits-001",
|
||||
Instant.parse("2026-07-18T04:10:00Z"));
|
||||
insertV4OrderTaskAudit(orderTask, "V4_CARD_CONFIRM", "v4-query-admin",
|
||||
Instant.parse("2026-07-18T04:11:00Z"));
|
||||
insertV4OrderTaskAudit(orderTask, "V4_CARD_REVIEW_RESOLVE", "v4-query-admin",
|
||||
Instant.parse("2026-07-18T04:12:00Z"));
|
||||
ReservationV4OrderTaskSnapshot otherOrderTask = seedOrderTask(
|
||||
"mail-v4-query-order-task-audits-other-001",
|
||||
Instant.parse("2026-07-18T04:13:00Z"));
|
||||
insertV4OrderTaskAudit(otherOrderTask, "V4_CARD_CONFIRM", "v4-query-admin",
|
||||
Instant.parse("2026-07-18T04:14:00Z"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}/audits",
|
||||
orderTask.id()).param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.order_task_id").value(orderTask.id().toString()))
|
||||
.andExpect(jsonPath("$.items.length()").value(2))
|
||||
.andExpect(jsonPath("$.items[0].action").value("V4_CARD_CONFIRM"))
|
||||
.andExpect(jsonPath("$.items[0].actor_id").value("v4-query-admin"))
|
||||
.andExpect(jsonPath("$.items[0].after_snapshot.v4_order_task_id").value(orderTask.id().toString()))
|
||||
.andExpect(jsonPath("$.items[1].action").value("V4_CARD_REVIEW_RESOLVE"))
|
||||
.andExpect(jsonPath("$.items[1].after_snapshot.v4_order_task_id").value(orderTask.id().toString()))
|
||||
.andExpect(jsonPath("$.items[0].occurred_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
|
||||
.andExpect(content().string(not(containsString(otherOrderTask.id().toString()))))
|
||||
.andExpect(content().string(not(containsString("Sensitive raw audit body"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnV4SourceNotificationAudits() throws Exception {
|
||||
ReservationV4SourceNotificationSnapshot notification = seedSourceNotification(
|
||||
"mail-v4-query-notification-audits-001",
|
||||
"S10",
|
||||
Instant.parse("2026-07-18T04:20:00Z"));
|
||||
insertV4SourceNotificationAudit(notification, "v4-query-admin",
|
||||
Instant.parse("2026-07-18T04:21:00Z"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/source-notifications/{notificationId}/audits",
|
||||
notification.id()).param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.notification_id").value(notification.id().toString()))
|
||||
.andExpect(jsonPath("$.items.length()").value(1))
|
||||
.andExpect(jsonPath("$.items[0].action").value("V4_SOURCE_NOTIFICATION_ACK"))
|
||||
.andExpect(jsonPath("$.items[0].actor_type").value("USER"))
|
||||
.andExpect(jsonPath("$.items[0].after_snapshot.v4_source_notification_id")
|
||||
.value(notification.id().toString()))
|
||||
.andExpect(jsonPath("$.items[0].occurred_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
|
||||
.andExpect(content().string(not(containsString("Sensitive raw audit body"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectV4WorkbenchItemsWithoutLogin() throws Exception {
|
||||
mockMvc.perform(get("/api/reservation/workbench-items")
|
||||
@@ -400,6 +453,14 @@ class ReservationV4QueryControllerTest {
|
||||
notification.id()).param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
|
||||
performAuthorized(mockMvc, noPermissionToken(), get("/api/reservation/order-tasks/{orderTaskId}/audits",
|
||||
orderTask.id()).param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
|
||||
performAuthorized(mockMvc, noPermissionToken(), get("/api/reservation/source-notifications/{notificationId}/audits",
|
||||
notification.id()).param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -427,6 +488,14 @@ class ReservationV4QueryControllerTest {
|
||||
notification.id()).param("hotel_id", OTHER_HOTEL_ID))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}/audits",
|
||||
orderTask.id()).param("hotel_id", OTHER_HOTEL_ID))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/source-notifications/{notificationId}/audits",
|
||||
notification.id()).param("hotel_id", OTHER_HOTEL_ID))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -670,4 +739,49 @@ class ReservationV4QueryControllerTest {
|
||||
"PAYMENT.attachment_ids 未匹配来源附件。",
|
||||
now));
|
||||
}
|
||||
|
||||
private void insertV4OrderTaskAudit(
|
||||
ReservationV4OrderTaskSnapshot orderTask,
|
||||
String action,
|
||||
String actorId,
|
||||
Instant occurredAt) {
|
||||
aiWorkflowRepository.insertAuditLog(new ReservationAuditLogDraft(
|
||||
HOTEL_ID,
|
||||
orderTask.orderId(),
|
||||
null,
|
||||
null,
|
||||
"USER",
|
||||
actorId,
|
||||
action,
|
||||
"前端查询 V4 审计测试。",
|
||||
"""
|
||||
{"v4_order_task_id":"%s","card_status":"PENDING_CONFIRM","raw":"Sensitive raw audit body"}
|
||||
""".formatted(orderTask.id()),
|
||||
"""
|
||||
{"v4_order_task_id":"%s","card_status":"CONFIRMED","field_pointers":["/basic_information/account_code"]}
|
||||
""".formatted(orderTask.id()),
|
||||
LocalDateTime.ofInstant(occurredAt, ZoneOffset.UTC)));
|
||||
}
|
||||
|
||||
private void insertV4SourceNotificationAudit(
|
||||
ReservationV4SourceNotificationSnapshot notification,
|
||||
String actorId,
|
||||
Instant occurredAt) {
|
||||
aiWorkflowRepository.insertAuditLog(new ReservationAuditLogDraft(
|
||||
HOTEL_ID,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"USER",
|
||||
actorId,
|
||||
"V4_SOURCE_NOTIFICATION_ACK",
|
||||
"确认已处理。",
|
||||
"""
|
||||
{"v4_source_notification_id":"%s","notification_status":"ACK_REQUIRED","raw":"Sensitive raw audit body"}
|
||||
""".formatted(notification.id()),
|
||||
"""
|
||||
{"v4_source_notification_id":"%s","notification_status":"ACKED"}
|
||||
""".formatted(notification.id()),
|
||||
LocalDateTime.ofInstant(occurredAt, ZoneOffset.UTC)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user