fix: 物料图片上传纳入保存互斥

This commit is contained in:
文睿 张 2026-10-09 14:34:43 +08:00
parent 89368ba3f8
commit dff5420a31
2 changed files with 59 additions and 2 deletions

View File

@ -26,6 +26,17 @@ vi.mock('../stores/auth', () => ({ useAuthStore: () => ({ isAdmin: true }) }))
vi.mock('../composables/useOrganizationSwitchGuard', () => ({ vi.mock('../composables/useOrganizationSwitchGuard', () => ({
useOrganizationSwitchGuard: vi.fn() useOrganizationSwitchGuard: vi.fn()
})) }))
// 图片上传等直接写请求走 useSavingGuard 的保存互斥;这里替身掉,避免测试环境需要 active Pinia。
const savingGuard = vi.hoisted(() => ({ runWithSaving: vi.fn(task => task()) }))
vi.mock('../composables/useSavingGuard', () => ({
useSavingGuard: () => savingGuard
}))
beforeEach(() => {
// restoreAllMocks 会清掉 vi.fn 的实现,这里每个用例重新装上直通实现。
savingGuard.runWithSaving = vi.fn(task => task())
})
vi.mock('../api/masterData', () => ({ vi.mock('../api/masterData', () => ({
materialCategoriesApi: { list: vi.fn() }, materialCategoriesApi: { list: vi.fn() },
unitsApi: { list: vi.fn() }, unitsApi: { list: vi.fn() },
@ -556,3 +567,43 @@ describe('物料页无可用分类时的配置入口提示', () => {
wrapper.unmount() wrapper.unmount()
}) })
}) })
describe('物料图片上传的保存互斥', () => {
beforeEach(() => {
materialCategoriesApi.list.mockResolvedValue({ data: categories })
unitsApi.list.mockResolvedValue({ data: [] })
codeRulesApi.next.mockReset()
codeRulesApi.next.mockResolvedValue({ data: { available: true, code: 'A010201' } })
materialsStore.createMaterial.mockReset().mockResolvedValue({ data: {} })
vi.spyOn(ElMessage, 'success').mockImplementation(() => {})
})
// 上传绕开 axios、直接 fetch,是保存物料流程里的直接写请求。必须走保存互斥,
// 否则上传飞行期间组织 store 的 saving 仍为 false,能切公司,归属不明确。
test('带图片保存时,上传请求走保存互斥', async () => {
const wrapper = mountMaterials()
await flushPromises()
await openAddDialog(wrapper)
await pickCategories(wrapper)
setupState(wrapper).form.name = '带图物料'
setupState(wrapper).imageInput = { files: [new File(['x'], 'a.png', { type: 'image/png' })] }
const originalFetch = global.fetch
const fetchSpy = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ data: { url: '/uploads/a.png' } })
})
global.fetch = fetchSpy
try {
await setupState(wrapper).handleSave()
} finally {
global.fetch = originalFetch
}
expect(fetchSpy).toHaveBeenCalled()
expect(materialsStore.createMaterial).toHaveBeenCalled()
expect(savingGuard.runWithSaving).toHaveBeenCalledTimes(1)
wrapper.unmount()
})
})

View File

@ -171,6 +171,7 @@ import { computed, ref, reactive, onMounted } from 'vue'
import { useMaterialsStore } from '../stores/materials' import { useMaterialsStore } from '../stores/materials'
import { useAuthStore } from '../stores/auth' import { useAuthStore } from '../stores/auth'
import { useOrganizationSwitchGuard } from '../composables/useOrganizationSwitchGuard' import { useOrganizationSwitchGuard } from '../composables/useOrganizationSwitchGuard'
import { useSavingGuard } from '../composables/useSavingGuard'
import BaseTable from '../components/business/BaseTable.vue' import BaseTable from '../components/business/BaseTable.vue'
import DeleteConfirm from '../components/business/DeleteConfirm.vue' import DeleteConfirm from '../components/business/DeleteConfirm.vue'
import AlertTip from '../components/business/AlertTip.vue' import AlertTip from '../components/business/AlertTip.vue'
@ -194,6 +195,9 @@ const MATERIAL_DATE_OPTIONS = {
const materialsStore = useMaterialsStore() const materialsStore = useMaterialsStore()
const authStore = useAuthStore() const authStore = useAuthStore()
// 图片上传是本页唯一直接发起的写请求(其余写请求都走 materialsStore 自身的保存互斥);
// 上传要用保存互斥包住,飞行期间禁止切换公司。
const { runWithSaving } = useSavingGuard()
const searchKeyword = ref('') const searchKeyword = ref('')
const materialSearchText = ref('') const materialSearchText = ref('')
@ -421,14 +425,16 @@ async function handleSave() {
uploadForm.append('image', file) uploadForm.append('image', file)
// 这里绕开了 axios 拦截器,组织头得手工补上,否则图片上传会缺少 X-Organization-ID。 // 这里绕开了 axios 拦截器,组织头得手工补上,否则图片上传会缺少 X-Organization-ID。
const organizationId = getOrganizationId() const organizationId = getOrganizationId()
const res = await fetch('/api/upload', { // 上传是保存物料流程中的直接写请求:包进保存互斥,飞行期间禁止切换公司;
// runWithSaving 内部是 try/finally,上传失败也会复位,不会把 busy 永久卡住。
const res = await runWithSaving(() => fetch('/api/upload', {
method: 'POST', method: 'POST',
headers: { headers: {
'Authorization': `Bearer ${authStore.token}`, 'Authorization': `Bearer ${authStore.token}`,
...(organizationId ? { 'X-Organization-ID': String(organizationId) } : {}) ...(organizationId ? { 'X-Organization-ID': String(organizationId) } : {})
}, },
body: uploadForm body: uploadForm
}) }))
if (!res.ok) { if (!res.ok) {
ElMessage.error('图片上传失败') ElMessage.error('图片上传失败')
return return