fix(dispatch): reject stale loading document upload rounds
This commit is contained in:
@@ -10,7 +10,7 @@ vi.mock('@/core/auth/auth', () => ({ useAuthStore: () => ({ user: { id: 'operato
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) }))
|
||||
vi.mock('../api/dispatchLoadingDocuments', async importOriginal => ({ ...await importOriginal<typeof api>(), getLoadingDocuments: vi.fn(), uploadLoadingPhoto: vi.fn(), recoverLoadingPhoto: vi.fn(), bindLoadingPhoto: vi.fn(), readLoadingPhoto: vi.fn() }))
|
||||
const photo: api.LoadingPhoto = { slot: 'LOADING_SCENE', fileId: '101', fileName: 'loading.png', contentType: 'image/png', savedAt: null }
|
||||
const data = (extra: Partial<api.LoadingDocuments> = {}): api.LoadingDocuments => ({ dispatchNo: 'PC/1', recordVersion: '0', dispatchRecordVersion: '3', stage: 'AFTER_LOADING', editable: true, complete: false, photos: [], ...extra })
|
||||
const data = (extra: Partial<api.LoadingDocuments> = {}): api.LoadingDocuments => ({ dispatchNo: 'PC/1', recordVersion: '0', dispatchRecordVersion: '3', reviewRound: '1', stage: 'AFTER_LOADING', editable: true, complete: false, photos: [], ...extra })
|
||||
function setup() { return mount(DispatchLoadingDocuments, { props: { dispatchNo: 'PC/1' }, global: { plugins: [ElementPlus] } }) }
|
||||
async function upload(wrapper: ReturnType<typeof setup>, files = [new File(['image'], 'loading.png', { type: 'image/png' })]) {
|
||||
const input = wrapper.get('input[aria-label="上传司机驾驶证"]')
|
||||
@@ -51,13 +51,28 @@ it('offers five whole-dispatch slots and only reports saved after binding succee
|
||||
expect(table.findAll('thead th').map(cell => cell.text())).toEqual(['操作', '照片类别', '文件名称', '图片预览', '保存时间'])
|
||||
expect(table.findAll('tbody tr').map(row => row.findAll('td')[1]!.text())).toEqual(Object.values(api.loadingPhotoLabels).map(label => `${label}0/5 张`))
|
||||
await upload(wrapper)
|
||||
expect(api.uploadLoadingPhoto).toHaveBeenCalledWith('PC/1', 'LOADING_SCENE', expect.any(File), expect.any(String))
|
||||
expect(api.uploadLoadingPhoto).toHaveBeenCalledWith('PC/1', 'LOADING_SCENE', expect.any(File), expect.any(String), '1')
|
||||
expect(api.bindLoadingPhoto).toHaveBeenCalledWith('PC/1', 'LOADING_SCENE', expect.objectContaining({ recordVersion: '0', fileId: '101' }))
|
||||
expect(vi.mocked(api.bindLoadingPhoto).mock.calls[0]![2].uploadKey).toBe(vi.mocked(api.uploadLoadingPhoto).mock.calls[0]![3])
|
||||
expect(wrapper.emitted('saved')).toHaveLength(1)
|
||||
expect(wrapper.text()).toContain('图片读取失败')
|
||||
expect(table.get('tbody tr').text()).toContain('loading.png')
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('stops a batch when re-review changes the source during upload instead of rebasing old files', async () => {
|
||||
const wrapper = setup(); await flushPromises()
|
||||
vi.mocked(api.uploadLoadingPhoto).mockImplementationOnce(async () => {
|
||||
vi.mocked(api.getLoadingDocuments).mockResolvedValue(data({ recordVersion: '8', dispatchRecordVersion: '12', reviewRound: '2' }))
|
||||
return photo
|
||||
})
|
||||
await upload(wrapper, [new File(['a'], 'old-driver.png', { type: 'image/png' }), new File(['b'], 'old-driver-2.png', { type: 'image/png' })])
|
||||
expect(api.bindLoadingPhoto).not.toHaveBeenCalled()
|
||||
expect(api.uploadLoadingPhoto).toHaveBeenCalledTimes(1)
|
||||
expect(vi.mocked(api.uploadLoadingPhoto).mock.calls[0]![4]).toBe('1')
|
||||
expect(wrapper.text()).toContain('审核轮次已变化')
|
||||
expect(wrapper.emitted('saved')).toHaveLength(1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('shows an imported PDF license as an authenticated document and accepts an exact PDF replacement', async () => {
|
||||
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:license')
|
||||
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {})
|
||||
|
||||
@@ -44,20 +44,23 @@ function photosFor(slot: LoadingPhotoSlot) { return snapshot.value?.photos.filte
|
||||
async function upload(slot: LoadingPhotoSlot, event: Event, replaceFileId?: string) {
|
||||
const input = event.target as HTMLInputElement, files = Array.from(input.files || []); input.value = ''
|
||||
if (!files.length || busy.value || !editable.value) return
|
||||
const reviewRound = snapshot.value!.reviewRound
|
||||
const capacity = replaceFileId ? 1 : 5 - photosFor(slot).length
|
||||
if (files.length > capacity) { ElMessage.warning(`本次最多可选择${capacity}张图片`); return }
|
||||
if (files.some(file => !(['image/jpeg','image/png'].includes(file.type) || slot === 'LOADING_SCENE' && file.type === 'application/pdf') || !file.size || file.size > 10 * 1024 * 1024)) { ElMessage.warning('请选择不超过10MiB的JPG或PNG,司机驾驶证也支持PDF'); return }
|
||||
busy.value = true; error.value = null
|
||||
let saved = 0, reconciled = false, bindingFailed = false
|
||||
let saved = 0, reconciled = false, bindingFailed = false, roundChanged = false
|
||||
try {
|
||||
ensureScope()
|
||||
for (const file of files) {
|
||||
const photo = await uploadLoadingPhoto(number, slot, file, crypto.randomUUID()); ensureScope()
|
||||
const uploadKey = crypto.randomUUID()
|
||||
const photo = await uploadLoadingPhoto(number, slot, file, uploadKey, reviewRound); ensureScope()
|
||||
const latest = await getLoadingDocuments(number); accept(latest)
|
||||
if (latest.reviewRound !== reviewRound) { reconciled = true; roundChanged = true; throw new Error('审核轮次已变化,请核对新车辆、司机后重新选择资料') }
|
||||
if (!latest.editable) throw new Error('当前状态不允许修改照片')
|
||||
if (photo.slot !== slot || !photo.fileId) throw new Error('上传结果与照片类别不一致')
|
||||
try {
|
||||
accept(await bindLoadingPhoto(number, slot, { recordVersion: latest.recordVersion, idempotencyKey: crypto.randomUUID(), fileId: photo.fileId, ...(replaceFileId ? { replaceFileId } : {}) }))
|
||||
accept(await bindLoadingPhoto(number, slot, { recordVersion: latest.recordVersion, idempotencyKey: crypto.randomUUID(), fileId: photo.fileId, uploadKey, ...(replaceFileId ? { replaceFileId } : {}) }))
|
||||
} catch (cause) {
|
||||
// 保存可能已经提交;先核对当前事实,不重放请求或继续后续图片。
|
||||
bindingFailed = true
|
||||
@@ -73,7 +76,7 @@ async function upload(slot: LoadingPhotoSlot, event: Event, replaceFileId?: stri
|
||||
}
|
||||
ElMessage.success(`已保存${saved}张照片`)
|
||||
} catch (cause) {
|
||||
if (active) error.value = getApiErrorDetails(cause, bindingFailed
|
||||
if (active) error.value = getApiErrorDetails(cause, roundChanged ? '审核轮次已变化,请核对新车辆、司机后重新选择资料' : bindingFailed
|
||||
? needsRefresh.value ? '保存结果尚未核实,请重新读取资料' : '保存响应未确认,已读取最新资料,请核对已保存图片后继续'
|
||||
: '图片上传失败,已保存照片保留,请重新选择图片')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user