接入前端P0页面真实接口
This commit is contained in:
6
client/src/services/healthService.ts
Normal file
6
client/src/services/healthService.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { getJson } from '@/services/httpClient'
|
||||
import type { BackendHealthResult } from '@/types/reservation'
|
||||
|
||||
export async function fetchBackendHealth(): Promise<BackendHealthResult> {
|
||||
return getJson<BackendHealthResult>('/api/health')
|
||||
}
|
||||
54
client/src/services/httpClient.ts
Normal file
54
client/src/services/httpClient.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? ''
|
||||
|
||||
export class ApiError extends Error {
|
||||
readonly status: number
|
||||
readonly details: unknown
|
||||
|
||||
constructor(message: string, status: number, details: unknown) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
this.status = status
|
||||
this.details = details
|
||||
}
|
||||
}
|
||||
|
||||
export async function getJson<T>(path: string): Promise<T> {
|
||||
return requestJson<T>(path, {
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendJson<T>(path: string, method: 'POST' | 'PUT', body?: unknown): Promise<T> {
|
||||
return requestJson<T>(path, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
})
|
||||
}
|
||||
|
||||
async function requestJson<T>(path: string, init: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${apiBaseUrl}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...init.headers,
|
||||
},
|
||||
})
|
||||
|
||||
const payload = await readResponsePayload(response)
|
||||
if (!response.ok) {
|
||||
throw new ApiError(`Request failed with status ${response.status}`, response.status, payload)
|
||||
}
|
||||
|
||||
return payload as T
|
||||
}
|
||||
|
||||
async function readResponsePayload(response: Response): Promise<unknown> {
|
||||
const contentType = response.headers.get('content-type') ?? ''
|
||||
if (!contentType.includes('application/json')) {
|
||||
return response.text()
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
201
client/src/services/reservationService.ts
Normal file
201
client/src/services/reservationService.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import {
|
||||
reservationTaskAudits,
|
||||
reservationTaskDetails,
|
||||
} from '@/fixtures/reservation'
|
||||
import { getJson, sendJson } from '@/services/httpClient'
|
||||
import type {
|
||||
ReservationOperaOperationResult,
|
||||
ReservationOperaSimulationRequest,
|
||||
ReservationOrderDetailResult,
|
||||
ReservationOrderListFilters,
|
||||
ReservationOrderListResult,
|
||||
ReservationTaskAuditListResult,
|
||||
ReservationTaskDetailResult,
|
||||
ReservationTaskListFilters,
|
||||
ReservationTaskListResult,
|
||||
ReservationTaskPayloadMutationRequest,
|
||||
ReservationTaskPayloadMutationResult,
|
||||
SourceMessageConversationResult,
|
||||
} from '@/types/reservation'
|
||||
|
||||
const useReservationFixtures = import.meta.env.VITE_RESERVATION_USE_FIXTURES === 'true'
|
||||
|
||||
export class EndpointPendingError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'EndpointPendingError'
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchReservationOrders(
|
||||
filters: ReservationOrderListFilters = {},
|
||||
): Promise<ReservationOrderListResult> {
|
||||
void filters
|
||||
throw new EndpointPendingError('GET /api/reservation/orders is pending backend implementation.')
|
||||
}
|
||||
|
||||
export async function fetchReservationTaskList(
|
||||
filters: ReservationTaskListFilters = {},
|
||||
): Promise<ReservationTaskListResult> {
|
||||
return getJson<ReservationTaskListResult>(withQuery('/api/reservation/tasks', filters))
|
||||
}
|
||||
|
||||
export async function fetchReservationOrderDetail(orderId: string): Promise<ReservationOrderDetailResult> {
|
||||
return getJson<ReservationOrderDetailResult>(
|
||||
withQuery(`/api/reservation/orders/${encodeURIComponent(orderId)}`, {
|
||||
include_tasks: true,
|
||||
include_source_summary: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export async function fetchSourceMessageConversation(
|
||||
sourceMessageId: string,
|
||||
): Promise<SourceMessageConversationResult> {
|
||||
throw new EndpointPendingError(
|
||||
`GET /api/source-messages/${sourceMessageId}/conversation is pending backend implementation.`,
|
||||
)
|
||||
}
|
||||
|
||||
export async function fetchReservationTaskDetail(taskId: string): Promise<ReservationTaskDetailResult> {
|
||||
if (useReservationFixtures) {
|
||||
return fixtureTaskDetail(taskId)
|
||||
}
|
||||
return getJson<ReservationTaskDetailResult>(`/api/reservation/tasks/${taskId}`)
|
||||
}
|
||||
|
||||
export async function saveReservationTaskDraft(
|
||||
taskId: string,
|
||||
request: ReservationTaskPayloadMutationRequest,
|
||||
): Promise<ReservationTaskPayloadMutationResult> {
|
||||
if (useReservationFixtures) {
|
||||
return fixtureTaskMutation(taskId, request, false)
|
||||
}
|
||||
return sendJson<ReservationTaskPayloadMutationResult>(`/api/reservation/tasks/${taskId}/draft`, 'PUT', request)
|
||||
}
|
||||
|
||||
export async function confirmReservationTask(
|
||||
taskId: string,
|
||||
request: ReservationTaskPayloadMutationRequest,
|
||||
): Promise<ReservationTaskPayloadMutationResult> {
|
||||
if (useReservationFixtures) {
|
||||
return fixtureTaskMutation(taskId, request, true)
|
||||
}
|
||||
return sendJson<ReservationTaskPayloadMutationResult>(`/api/reservation/tasks/${taskId}/confirm`, 'POST', request)
|
||||
}
|
||||
|
||||
export async function executeReservationOperaOperation(
|
||||
taskId: string,
|
||||
operationId: string,
|
||||
request: ReservationOperaSimulationRequest,
|
||||
): Promise<ReservationOperaOperationResult> {
|
||||
if (useReservationFixtures) {
|
||||
return fixtureOperaOperation(taskId, operationId, request, 'execute')
|
||||
}
|
||||
return sendJson<ReservationOperaOperationResult>(
|
||||
`/api/reservation/tasks/${taskId}/opera-operations/${operationId}/execute`,
|
||||
'POST',
|
||||
request,
|
||||
)
|
||||
}
|
||||
|
||||
export async function retryReservationOperaOperation(
|
||||
taskId: string,
|
||||
operationId: string,
|
||||
request: ReservationOperaSimulationRequest,
|
||||
): Promise<ReservationOperaOperationResult> {
|
||||
if (useReservationFixtures) {
|
||||
return fixtureOperaOperation(taskId, operationId, request, 'retry')
|
||||
}
|
||||
return sendJson<ReservationOperaOperationResult>(
|
||||
`/api/reservation/tasks/${taskId}/opera-operations/${operationId}/retry`,
|
||||
'POST',
|
||||
request,
|
||||
)
|
||||
}
|
||||
|
||||
export async function fetchReservationTaskAudits(taskId: string): Promise<ReservationTaskAuditListResult> {
|
||||
if (useReservationFixtures) {
|
||||
return Promise.resolve(
|
||||
reservationTaskAudits.find((audit) => audit.task_id === taskId) ?? {
|
||||
task_id: taskId,
|
||||
items: [],
|
||||
},
|
||||
)
|
||||
}
|
||||
return getJson<ReservationTaskAuditListResult>(`/api/reservation/tasks/${taskId}/audits`)
|
||||
}
|
||||
|
||||
function withQuery(path: string, params: object): string {
|
||||
const searchParams = new URLSearchParams()
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
!['string', 'number', 'boolean'].includes(typeof value)
|
||||
) {
|
||||
return
|
||||
}
|
||||
searchParams.set(key, String(value))
|
||||
})
|
||||
const query = searchParams.toString()
|
||||
return query ? `${path}?${query}` : path
|
||||
}
|
||||
|
||||
function fixtureTaskDetail(taskId: string): Promise<ReservationTaskDetailResult> {
|
||||
const detail = reservationTaskDetails.find((item) => item.task_id === taskId)
|
||||
if (!detail) {
|
||||
throw new Error(`Reservation task ${taskId} was not found in frontend fixture data.`)
|
||||
}
|
||||
return Promise.resolve(structuredClone(detail))
|
||||
}
|
||||
|
||||
function fixtureTaskMutation(
|
||||
taskId: string,
|
||||
request: ReservationTaskPayloadMutationRequest,
|
||||
confirmed: boolean,
|
||||
): Promise<ReservationTaskPayloadMutationResult> {
|
||||
return fixtureTaskDetail(taskId).then((detail) => ({
|
||||
task_id: detail.task_id,
|
||||
order_id: detail.order_id,
|
||||
task_status: confirmed ? 'READY' : detail.task_status,
|
||||
draft_payload: request.field_values,
|
||||
confirmed_payload: confirmed ? request.field_values : detail.confirmed_payload,
|
||||
opera_operations: confirmed ? detail.opera_operations : [],
|
||||
}))
|
||||
}
|
||||
|
||||
async function fixtureOperaOperation(
|
||||
taskId: string,
|
||||
operationId: string,
|
||||
request: ReservationOperaSimulationRequest,
|
||||
action: 'execute' | 'retry',
|
||||
): Promise<ReservationOperaOperationResult> {
|
||||
const detail = await fixtureTaskDetail(taskId)
|
||||
const operation = detail.opera_operations.find((item) => item.operation_id === operationId)
|
||||
if (!operation) {
|
||||
throw new Error(`Reservation OPERA operation ${operationId} was not found in frontend fixture data.`)
|
||||
}
|
||||
|
||||
const success = request.simulate_success !== false
|
||||
const attemptNumber = operation.attempt_count + 1
|
||||
return {
|
||||
...operation,
|
||||
operation_status: success ? 'SUCCEEDED' : 'FAILED',
|
||||
attempt_count: attemptNumber,
|
||||
last_error_message: success ? null : request.failure_message ?? 'Frontend simulated OPERA failure',
|
||||
attempts: [
|
||||
...operation.attempts,
|
||||
{
|
||||
attempt_id: `${operationId}-${action}-${attemptNumber}`,
|
||||
attempt_number: attemptNumber,
|
||||
attempt_status: success ? 'SUCCEEDED' : 'FAILED',
|
||||
response_payload: success ? { simulated: true } : null,
|
||||
error_message: success ? null : request.failure_message ?? 'Frontend simulated OPERA failure',
|
||||
started_at: new Date().toISOString(),
|
||||
finished_at: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user