feat(booking): add safe retries and operational metrics
This commit is contained in:
1 parent
c826b6e574
commit
7330ac853b
26 files changed
+749
-31
No files matched your search
@@ -680,6 +680,10 @@ export default {
|
||||
refreshing: 'Refreshing…',
|
||||
source: 'Source message',
|
||||
revision: 'Revision',
|
||||
retryCount: 'Retry count',
|
||||
retry: 'Retry processing',
|
||||
retrying: 'Retrying…',
|
||||
retrySuccess: 'The run was returned to the processing queue and will refresh automatically.',
|
||||
disposition: 'Disposition',
|
||||
status: 'Current status',
|
||||
timeline: 'Processing progress',
|
||||
|
||||
@@ -680,6 +680,10 @@ export default {
|
||||
refreshing: 'กำลังรีเฟรช…',
|
||||
source: 'อีเมลต้นทาง',
|
||||
revision: 'ฉบับแก้ไข',
|
||||
retryCount: 'จำนวนครั้งที่ลองใหม่',
|
||||
retry: 'ประมวลผลอีกครั้ง',
|
||||
retrying: 'กำลังประมวลผลอีกครั้ง…',
|
||||
retrySuccess: 'ส่งกลับเข้าคิวประมวลผลแล้ว และสถานะจะรีเฟรชอัตโนมัติ',
|
||||
disposition: 'ผลการจัดการ',
|
||||
status: 'สถานะปัจจุบัน',
|
||||
timeline: 'ความคืบหน้าการประมวลผล',
|
||||
|
||||
@@ -679,6 +679,10 @@ export default {
|
||||
refreshing: '刷新中…',
|
||||
source: '来源邮件',
|
||||
revision: '修订',
|
||||
retryCount: '重试次数',
|
||||
retry: '重新处理',
|
||||
retrying: '正在重新处理…',
|
||||
retrySuccess: '已重新进入处理队列,将自动刷新状态。',
|
||||
disposition: '处理结果',
|
||||
status: '当前状态',
|
||||
timeline: '处理进度',
|
||||
|
||||
@@ -63,6 +63,13 @@ export async function fetchBookingProcessingRun(runId: string): Promise<BookingP
|
||||
)
|
||||
}
|
||||
|
||||
export async function retryBookingProcessingRun(runId: string): Promise<BookingProcessingRunView> {
|
||||
return sendJson<BookingProcessingRunView>(
|
||||
`/api/reservation/booking-processing-runs/${encodeURIComponent(runId)}/retry`,
|
||||
'POST',
|
||||
)
|
||||
}
|
||||
|
||||
export async function confirmBookingProcessingRun(
|
||||
runId: string,
|
||||
request: BookingConfirmationRequest,
|
||||
|
||||
@@ -13,6 +13,7 @@ vi.mock('@/services/reservationService', async (importOriginal) => {
|
||||
...actual,
|
||||
fetchBookingProcessingRun: vi.fn(),
|
||||
confirmBookingProcessingRun: vi.fn(),
|
||||
retryBookingProcessingRun: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -22,6 +23,7 @@ describe('ReservationBookingProcessingRunView', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(service.fetchBookingProcessingRun).mockReset()
|
||||
vi.mocked(service.confirmBookingProcessingRun).mockReset()
|
||||
vi.mocked(service.retryBookingProcessingRun).mockReset()
|
||||
})
|
||||
|
||||
it('shows the safe confirmation projection and freezes corrected values without a PMS action', async () => {
|
||||
@@ -64,6 +66,38 @@ describe('ReservationBookingProcessingRunView', () => {
|
||||
expect(wrapper.text()).toContain('参数已冻结')
|
||||
expect(wrapper.text()).toContain('已确认')
|
||||
})
|
||||
|
||||
it('retries only a failed processing run and resumes safe polling', async () => {
|
||||
const failed = {
|
||||
...awaitingRun(),
|
||||
status: 'FAILED' as const,
|
||||
result_disposition: 'RISK_NOTIFICATION' as const,
|
||||
retry_count: 0,
|
||||
error_code: 'BOOKING_ORCHESTRATION_FAILED',
|
||||
error_summary: 'Booking processing failed before confirmation.',
|
||||
confirmation_projection: null,
|
||||
}
|
||||
const retried = {
|
||||
...failed,
|
||||
status: 'CONTEXT_READY' as const,
|
||||
retry_count: 1,
|
||||
error_code: null,
|
||||
error_summary: null,
|
||||
}
|
||||
vi.mocked(service.fetchBookingProcessingRun).mockResolvedValue(failed)
|
||||
vi.mocked(service.retryBookingProcessingRun).mockResolvedValue(retried)
|
||||
const wrapper = await mountView()
|
||||
await flushPromises()
|
||||
|
||||
const retryButton = wrapper.findAll('button').find((button) => button.text().includes('重新处理'))
|
||||
expect(retryButton).toBeDefined()
|
||||
await retryButton!.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(service.retryBookingProcessingRun).toHaveBeenCalledWith('booking-001')
|
||||
expect(wrapper.text()).toContain('上下文已组装')
|
||||
expect(wrapper.text()).toContain('已重新进入处理队列')
|
||||
})
|
||||
})
|
||||
|
||||
async function mountView() {
|
||||
|
||||
@@ -64,6 +64,10 @@
|
||||
<dt>{{ t('bookingProcessingRun.revision') }}</dt>
|
||||
<dd>{{ run.revision_number }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('bookingProcessingRun.retryCount') }}</dt>
|
||||
<dd>{{ run.retry_count }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('bookingProcessingRun.disposition') }}</dt>
|
||||
<dd>{{ run.result_disposition ?? '—' }}</dd>
|
||||
@@ -105,6 +109,15 @@
|
||||
>
|
||||
{{ run.error_code ?? 'BOOKING_PROCESSING_ERROR' }} · {{ run.error_summary }}
|
||||
</p>
|
||||
<button
|
||||
v-if="canRetry"
|
||||
type="button"
|
||||
class="secondary-button"
|
||||
:disabled="retrying"
|
||||
@click="retryRun"
|
||||
>
|
||||
{{ retrying ? t('bookingProcessingRun.retrying') : t('bookingProcessingRun.retry') }}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section
|
||||
@@ -258,6 +271,7 @@ import { ApiError } from '@/services/httpClient'
|
||||
import {
|
||||
confirmBookingProcessingRun,
|
||||
fetchBookingProcessingRun,
|
||||
retryBookingProcessingRun,
|
||||
} from '@/services/reservationService'
|
||||
import type {
|
||||
BookingConfirmationField,
|
||||
@@ -282,6 +296,7 @@ const route = useRoute()
|
||||
const run = ref<BookingProcessingRunView | null>(null)
|
||||
const loading = ref(false)
|
||||
const confirming = ref(false)
|
||||
const retrying = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const successMessage = ref('')
|
||||
const draftValues = ref<Record<string, Record<string, string>>>({})
|
||||
@@ -299,6 +314,7 @@ const projectionIssues = computed(() => projection.value?.issues ?? [])
|
||||
const evidenceCount = computed(() => projection.value?.evidence_refs.length ?? 0)
|
||||
const canSubmit = computed(() => run.value?.status === 'AWAITING_CONFIRMATION'
|
||||
&& confirmationItems.value.length > 0)
|
||||
const canRetry = computed(() => run.value?.status === 'FAILED')
|
||||
|
||||
onMounted(() => {
|
||||
void loadRun(true)
|
||||
@@ -401,6 +417,26 @@ async function confirmRun(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function retryRun(): Promise<void> {
|
||||
if (!run.value || !canRetry.value || retrying.value) {
|
||||
return
|
||||
}
|
||||
retrying.value = true
|
||||
errorMessage.value = ''
|
||||
successMessage.value = ''
|
||||
try {
|
||||
const nextRun = await retryBookingProcessingRun(run.value.processing_run_id)
|
||||
run.value = nextRun
|
||||
hydrate(nextRun)
|
||||
schedulePolling(nextRun)
|
||||
successMessage.value = t('bookingProcessingRun.retrySuccess')
|
||||
} catch (error) {
|
||||
errorMessage.value = apiErrorMessage(error)
|
||||
} finally {
|
||||
retrying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function itemFields(item: BookingConfirmationProjection['confirmation_items'][number]): BookingConfirmationField[] {
|
||||
const byCode = new Map<string, BookingConfirmationField>()
|
||||
for (const field of item.display_fields) {
|
||||
|
||||
Reference in new issue
Block a user