修复 V4 查询接口安全与筛选问题

This commit is contained in:
andy
2026-07-19 10:13:57 +07:00
parent dc6c8f48ab
commit 9404a0e5c1
7 changed files with 348 additions and 26 deletions

View File

@@ -19,6 +19,8 @@ import java.time.OffsetDateTime;
* @param orderTaskStatus 订单任务状态,仅 ORDER_TASK 返回
* @param displayStatus 前端展示状态
* @param readonlyReasonCode 只读原因稳定码
* @param createdAt 条目创建 UTC 时间,用于前端调试和稳定排序兜底
* @param updatedAt 条目最近更新 UTC 时间,用于同来源时间下的稳定排序
*/
public record ReservationV4WorkbenchItemResult(
@JsonProperty("item_type")
@@ -46,6 +48,10 @@ public record ReservationV4WorkbenchItemResult(
@JsonProperty("display_status")
String displayStatus,
@JsonProperty("readonly_reason_code")
String readonlyReasonCode
String readonlyReasonCode,
@JsonProperty("created_at")
OffsetDateTime createdAt,
@JsonProperty("updated_at")
OffsetDateTime updatedAt
) {
}

View File

@@ -36,6 +36,7 @@ public interface ReservationV4OrderTaskMapper extends BaseMapper<ReservationV4Or
WHERE c.hotel_id = ot.hotel_id
AND c.v4_order_task_id = ot.id
AND c.card_status = #{cardStatus}
AND c.card_type != 'SOURCE_MESSAGE_DISPLAY'
AND c.logic_deleted_at IS NULL
)
</if>

View File

@@ -5,10 +5,12 @@ import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
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.ReservationPageSnapshot;
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.ReservationAiSystemProcessCategory;
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;
@@ -18,6 +20,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4Workben
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4OrderTaskQueryRequest;
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4SourceNotificationQueryRequest;
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4WorkbenchQueryRequest;
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.ReservationV4ActionAvailabilityResult;
@@ -32,6 +35,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4Source
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4TaskCardResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4WorkbenchItemResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4WorkbenchListResult;
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.ReservationV4QueryService;
@@ -66,6 +70,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
private static final String DISPLAY_STATUS_OPEN = "OPEN";
private final ReservationV4WorkflowRepository workflowRepository;
private final ReservationAiWorkflowRepository aiWorkflowRepository;
private final ReservationV4SourceNotificationRepository sourceNotificationRepository;
private final SourceMessageQueryService sourceMessageQueryService;
private final HotelContextService hotelContextService;
@@ -76,11 +81,13 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
*/
public ReservationV4QueryServiceImpl(
ReservationV4WorkflowRepository workflowRepository,
ReservationAiWorkflowRepository aiWorkflowRepository,
ReservationV4SourceNotificationRepository sourceNotificationRepository,
SourceMessageQueryService sourceMessageQueryService,
HotelContextService hotelContextService,
ObjectMapper objectMapper) {
this.workflowRepository = workflowRepository;
this.aiWorkflowRepository = aiWorkflowRepository;
this.sourceNotificationRepository = sourceNotificationRepository;
this.sourceMessageQueryService = sourceMessageQueryService;
this.hotelContextService = hotelContextService;
@@ -215,7 +222,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
basicInformationCard,
businessCards,
cardCounts(cards),
List.of(),
adapterContractErrors(orderTask),
orderAvailability);
}
@@ -281,7 +288,9 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
null,
orderTask.orderTaskStatus(),
item.displayStatus(),
item.availability().readonlyReasonCode());
item.availability().readonlyReasonCode(),
orderTask.createdAt(),
orderTask.updatedAt());
}
private ReservationV4WorkbenchItemResult toWorkbenchSourceNotificationItem(
@@ -301,7 +310,9 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
notification.notificationStatus(),
null,
notification.notificationStatus(),
availability.readonlyReasonCode());
availability.readonlyReasonCode(),
UtcTimeFormatter.toUtcOffsetDateTime(notification.createdAt()),
UtcTimeFormatter.toUtcOffsetDateTime(notification.updatedAt()));
}
private ReservationV4TaskCardResult toCardResult(
@@ -573,7 +584,10 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
}
List<Long> uniqueIds = sourceMessageIds.stream().filter(Objects::nonNull).distinct().toList();
List<SourceMessageSummaryResponse> summaries = sourceMessageQueryService.getSummariesByIds(uniqueIds);
List<String> conversationIds = summaries.stream()
List<SourceMessageSummaryResponse> safeSummaries = summaries.stream()
.filter(summary -> Objects.equals(hotelId, summary.hotelId()))
.toList();
List<String> conversationIds = safeSummaries.stream()
.map(SourceMessageSummaryResponse::externalConversationId)
.filter(Objects::nonNull)
.distinct()
@@ -582,7 +596,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
hotelId,
conversationIds);
Map<Long, ReservationV4SourceMessageSummaryResult> result = new LinkedHashMap<>();
for (SourceMessageSummaryResponse summary : summaries) {
for (SourceMessageSummaryResponse summary : safeSummaries) {
Long sourceMessageId = Long.valueOf(summary.id());
result.put(sourceMessageId, new ReservationV4SourceMessageSummaryResult(
summary.id(),
@@ -610,6 +624,59 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
return result;
}
private List<ReservationAiTransitionDisplayResult> adapterContractErrors(ReservationV4OrderTaskSnapshot orderTask) {
if (orderTask.aiBatchId() == null) {
return List.of();
}
return aiWorkflowRepository.findTransitionsByBatchId(orderTask.hotelId(), orderTask.aiBatchId()).stream()
.filter(transition -> ReservationAiSystemProcessCategory.ADAPTER_CONTRACT_ERROR.name()
.equals(transition.systemProcessCategory()))
.map(this::toTransitionDisplayResult)
.toList();
}
private ReservationAiTransitionDisplayResult toTransitionDisplayResult(ReservationAiTransitionSnapshot transition) {
return new ReservationAiTransitionDisplayResult(
transition.id().toString(),
transition.sourceEventIndex(),
transition.arrayIndex(),
transition.resultType(),
transition.aiTaskType(),
transition.taskSubtype(),
transition.routeCode(),
transition.systemProcessCategory(),
transition.adapterErrorCode(),
transition.adapterErrorMessage(),
diagnosticPayloadFragment(transition.aiPayloadJson()));
}
private Object diagnosticPayloadFragment(String aiPayloadJson) {
if (!hasText(aiPayloadJson)) {
return null;
}
JsonNode payload = parseJson(aiPayloadJson);
Map<String, Object> fragment = new LinkedHashMap<>();
copyDiagnosticPayloadField(payload, fragment, "event_type");
copyDiagnosticPayloadField(payload, fragment, "source_event_index");
copyDiagnosticPayloadField(payload, fragment, "current_or_history");
copyDiagnosticPayloadField(payload, fragment, "contract_errors");
copyDiagnosticPayloadField(payload, fragment, "intent_type");
copyDiagnosticPayloadField(payload, fragment, "reason_code");
copyDiagnosticPayloadField(payload, fragment, "missing_fields");
copyDiagnosticPayloadField(payload, fragment, "parent_source_event_index");
copyDiagnosticPayloadField(payload, fragment, "linked_task_group_id");
copyDiagnosticPayloadField(payload, fragment, "blocked_until_parent_completed");
return fragment;
}
private void copyDiagnosticPayloadField(JsonNode payload, Map<String, Object> fragment, String fieldName) {
JsonNode value = payload == null ? null : payload.get(fieldName);
if (value == null || value.isNull() || value.isMissingNode()) {
return;
}
fragment.put(fieldName, objectMapper.convertValue(value, Object.class));
}
private List<Long> findSourceMessageIdsByKeyword(String hotelId, String keyword) {
if (!hasText(keyword)) {
return List.of();
@@ -725,7 +792,21 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
private Comparator<ReservationV4WorkbenchItemResult> workbenchItemComparator() {
return Comparator.comparing(ReservationV4WorkbenchItemResult::sourceReceivedAt,
Comparator.nullsLast(Comparator.reverseOrder()))
.thenComparing(ReservationV4WorkbenchItemResult::targetId, Comparator.reverseOrder());
.thenComparing(ReservationV4WorkbenchItemResult::updatedAt,
Comparator.nullsLast(Comparator.reverseOrder()))
.thenComparing(ReservationV4WorkbenchItemResult::createdAt,
Comparator.nullsLast(Comparator.reverseOrder()))
.thenComparing(this::targetIdAsLong, Comparator.nullsLast(Comparator.reverseOrder()))
.thenComparing(ReservationV4WorkbenchItemResult::targetId,
Comparator.nullsLast(Comparator.reverseOrder()));
}
private Long targetIdAsLong(ReservationV4WorkbenchItemResult item) {
try {
return item.targetId() == null ? null : Long.valueOf(item.targetId());
} catch (NumberFormatException exception) {
return null;
}
}
private ReservationV4WorkbenchQueryRequest normalizeWorkbenchRequest(ReservationV4WorkbenchQueryRequest request) {
@@ -748,13 +829,45 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
return new ReservationV4OrderTaskQueryRequest(
normalizeHotelId(request.hotelId()),
request.orderId(),
trimToNull(request.orderTaskStatus()),
trimToNull(request.cardStatus()),
normalizeOrderTaskStatus(request.orderTaskStatus()),
normalizeCardStatus(request.cardStatus()),
trimToNull(request.keyword()),
request.pageNum(),
request.pageSize());
}
private String normalizeOrderTaskStatus(String orderTaskStatus) {
String normalized = trimToNull(orderTaskStatus);
if (normalized == null) {
return null;
}
for (ReservationV4OrderTaskStatus status : ReservationV4OrderTaskStatus.values()) {
if (status.name().equals(normalized)) {
return normalized;
}
}
throw new ReservationTaskWorkflowException(
HttpStatus.BAD_REQUEST,
"V4_ORDER_TASK_STATUS_INVALID",
"V4 订单任务状态不支持。");
}
private String normalizeCardStatus(String cardStatus) {
String normalized = trimToNull(cardStatus);
if (normalized == null) {
return null;
}
for (ReservationV4CardStatus status : ReservationV4CardStatus.values()) {
if (status.name().equals(normalized)) {
return normalized;
}
}
throw new ReservationTaskWorkflowException(
HttpStatus.BAD_REQUEST,
"V4_CARD_STATUS_INVALID",
"V4 任务卡状态不支持。");
}
private String normalizeHotelId(String hotelId) {
try {
return hotelContextService.resolveCurrentHotelId(hotelId);