material_manage_system/frontend/src/stores/organization.js
文睿 张 ffcf7748fb fix: 补全公司切换的保存互斥与旧读请求中止
- 保存请求执行期间禁止切换:organization store 新增 saving/busy 与
  beginSaving/endSaving,切换守卫与切换器禁用条件均由 switching 改为 busy
- 切换提交时 abort 旧读请求:commitOrganizationId 内调用 abortReadRequests,
  不再只在 resetOrganizationContext 里取消
- validateCandidate 仅将 403 视为候选失效,网络异常/500 等向上抛出;
  initialize 置 loadError,App 据此区分“加载失败”与“请先选择公司”并支持重试
- 补竞态与中止测试;需要写请求时旧响应仍被版本丢弃的用例改用 POST 覆盖
2026-10-09 10:34:19 +08:00

200 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { authApi } from '../api/auth'
import {
captureOrganizationContext,
commitOrganizationId,
getOrganizationId,
resetOrganizationContext
} from '../utils/organizationContext'
// 本标签页上次选择的公司按用户分开存:同一浏览器不同账号登录时互不继承选择。
function selectionStorageKey(userId) {
return `wmms_organization_id:${userId ?? 'anonymous'}`
}
// 直接读 localStorage 里的用户信息拿 id,不导入 auth store,避免 store 之间相互依赖成环。
function currentUserId() {
try {
const user = JSON.parse(localStorage.getItem('wmms_user') || 'null')
return user?.id ?? null
} catch {
return null
}
}
function readStoredOrganizationId() {
const raw = sessionStorage.getItem(selectionStorageKey(currentUserId()))
const id = Number(raw)
return Number.isInteger(id) && id > 0 ? id : null
}
function writeStoredOrganizationId(id) {
sessionStorage.setItem(selectionStorageKey(currentUserId()), String(id))
}
export const useOrganizationStore = defineStore('organization', () => {
const organizations = ref([])
const currentOrganizationId = ref(null)
const currentWarehouseId = ref(null)
const currentLocationId = ref(null)
// 当前公司的仓库摘要(含库位),供切换器与页面展示,普通用户无需调用管理员仓库接口。
const currentWarehouse = ref(null)
const contextVersion = ref(0)
const switching = ref(false)
// 写请求(保存)进行中标记:业务 store 在发起保存前 beginSaving()、结束后 endSaving()。
// 保存飞行中若切换公司,保存的成功/失败响应会因版本变化被视为作废并静默丢弃,
// 用户既看不到成功也看不到失败,所以保存期间必须禁止切换。
const saving = ref(false)
// 任何会导致“公司上下文变更或归属不明”的操作进行中都算忙:切换中或保存中。
const busy = computed(() => switching.value || saving.value)
// 是否已完成一次公司列表加载,路由屏障据此决定是否需要先初始化。
const initialized = ref(false)
// 最近一次 initialize 是否因临时故障失败:用于把“加载失败”与“确实没有可用公司”区分开,
// 避免把网络异常误导成“请先选择要操作的公司”。
const loadError = ref(false)
// 把 store 内展示用的版本号同步成上下文工具里的真实版本,两者始终一致。
function syncVersion() {
contextVersion.value = captureOrganizationContext().version
}
// 仓库、库位的自动选择规则:只有唯一仓库启用时设置仓库 ID,其唯一库位也启用时才设置库位 ID。
// 缺配置、停用或失败时对应 ID 保持为空,绝不回退到旧公司的仓库或库位。
function applyWarehouseContext(warehouse) {
currentWarehouse.value = warehouse || null
// 先清空旧公司的仓库、库位,再按目标公司摘要设置。
currentWarehouseId.value = null
currentLocationId.value = null
if (warehouse && warehouse.status === 'active') {
currentWarehouseId.value = warehouse.id
const location = warehouse.location
if (location && location.status === 'active') {
currentLocationId.value = location.id
}
}
}
// 校验成功后才提交本地状态:先递增版本(同时取消旧读请求),再写公司 ID 和仓库、库位。
function applyContext(data) {
const organizationId = data?.organization_id ?? null
commitOrganizationId(organizationId)
currentOrganizationId.value = organizationId
applyWarehouseContext(data?.warehouse || null)
syncVersion()
if (organizationId) writeStoredOrganizationId(organizationId)
}
// 没有可用公司(或候选公司都失效)时进入空状态:公司 ID 保持为空,
// 拦截器不会给任何组织级请求写组织头,业务页面也不挂载。
function applyEmptyContext() {
commitOrganizationId(null)
currentOrganizationId.value = null
applyWarehouseContext(null)
syncVersion()
}
// 用切换校验接口验证候选公司:该接口会返回目标公司的仓库摘要,顺带完成授权校验。
async function validateCandidate(organizationId) {
try {
const res = await authApi.switchOrganizationContext(organizationId, getOrganizationId() ?? 0)
applyContext(res.data || {})
return true
} catch (error) {
// 只有 403(该用户对该候选公司确实无授权)才算“候选失效”,返回 false 交给调用方尝试下一个。
if (error?.response?.status === 403) return false
// 网络异常、500 等临时故障必须向上抛出:若被当成候选失效,initialize 会落入空状态并
// 提示“请先选择要操作的公司”,把一次性故障误导成需要重新选公司。
throw error
}
}
async function initialize() {
loadError.value = false
try {
const res = await authApi.listOrganizations()
const data = res.data || {}
organizations.value = data.organizations || []
if (organizations.value.length === 0) {
// 无授权公司:空状态,不发起任何校验请求。
applyEmptyContext()
initialized.value = true
return
}
// 本标签页上次选择优先,其次默认公司;两者都必须重新校验授权。
const defaultId = data.default_organization_id ?? null
const candidates = [readStoredOrganizationId(), defaultId]
const seen = new Set()
for (const candidate of candidates) {
if (candidate == null || seen.has(candidate)) continue
seen.add(candidate)
if (!organizations.value.some(org => org.id === candidate)) continue
if (await validateCandidate(candidate)) {
initialized.value = true
return
}
}
// 候选都失效但仍有授权公司:保留列表,要求用户重新选择。
applyEmptyContext()
initialized.value = true
} catch (error) {
// 临时故障(列表拉取失败或候选校验非 403)不降级成“无公司”空状态:
// 标记加载失败供 App 显示“加载失败”而非“请先选择公司”,并向上抛出交由调用方重试。
loadError.value = true
throw error
}
}
// 业务 store 复用的“写请求进行中”开关:发起保存前置位、结束后复位(务必放在 finally)。
function beginSaving() {
saving.value = true
}
function endSaving() {
saving.value = false
}
async function switchOrganization(organizationId) {
// 切换或保存进行中禁止再次切换:避免并发切换互相覆盖,以及写请求归属不明确。
// 保存飞行中切换会让保存的响应被版本判定作废并被静默吞掉,用户既看不到成功也看不到失败。
if (busy.value) return
if (!organizationId || organizationId === currentOrganizationId.value) return
switching.value = true
try {
const res = await authApi.switchOrganizationContext(
organizationId,
currentOrganizationId.value ?? 0
)
// 校验成功后才提交;抛错时保持旧公司不变,由调用方决定是否提示。
applyContext(res.data || {})
} finally {
switching.value = false
}
}
// 登出或切换账号时清空:上下文、公司列表、仓库库位与初始化标记全部复位。
function reset() {
resetOrganizationContext()
organizations.value = []
currentOrganizationId.value = null
currentWarehouse.value = null
currentWarehouseId.value = null
currentLocationId.value = null
switching.value = false
saving.value = false
initialized.value = false
loadError.value = false
syncVersion()
}
return {
organizations, currentOrganizationId, currentWarehouseId, currentLocationId,
currentWarehouse, contextVersion, switching, saving, busy, initialized, loadError,
beginSaving, endSaving,
initialize, switchOrganization, reset
}
})