2026-08-20 20:10:20 +08:00
|
|
|
import { reactive } from 'vue'
|
2026-08-25 00:24:57 +08:00
|
|
|
import { OVERSEAS_API_ROOT } from '../config/runtime.js'
|
2026-08-25 10:39:50 +08:00
|
|
|
import { resetReceivingSession } from './receiving.js'
|
2026-09-01 14:23:55 +08:00
|
|
|
import { resetFulfillmentSession } from './fulfillment.js'
|
2026-08-20 20:10:20 +08:00
|
|
|
|
|
|
|
|
const SESSION_KEY = 'overseas-pda-auth-session'
|
2026-08-28 10:39:02 +08:00
|
|
|
export const PDA_ROLE = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR'
|
2026-08-25 00:24:57 +08:00
|
|
|
export const PDA_AUTHORITY = 'lastleg:pda:operate'
|
2026-08-20 20:10:20 +08:00
|
|
|
|
2026-08-24 15:25:36 +08:00
|
|
|
export const session = reactive({ currentUser: null, token: '', domesticToken: '', refreshToken: '', tenantId: '', apiBase: OVERSEAS_API_ROOT })
|
2026-08-20 20:10:20 +08:00
|
|
|
|
|
|
|
|
function decodeBase64Url(value) {
|
|
|
|
|
const normalized = value.replace(/-/g, '+').replace(/_/g, '/')
|
|
|
|
|
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=')
|
|
|
|
|
if (typeof atob === 'function') return decodeURIComponent(Array.from(atob(padded), (char) => `%${char.charCodeAt(0).toString(16).padStart(2, '0')}`).join(''))
|
|
|
|
|
throw new Error('当前运行环境无法解析登录令牌')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function decodeAccessToken(token) {
|
|
|
|
|
const payload = String(token || '').split('.')[1]
|
|
|
|
|
if (!payload) throw new Error('登录响应中的 accessToken 格式无效')
|
|
|
|
|
try {
|
2026-08-25 00:24:57 +08:00
|
|
|
const decoded = decodeBase64Url(payload)
|
|
|
|
|
// Preserve snowflake IDs before JSON.parse rounds unquoted JWT numbers beyond Number.MAX_SAFE_INTEGER.
|
|
|
|
|
const safeLongClaims = decoded.replace(
|
|
|
|
|
/("(?:user_id|membership_id|tenant_id|overseas_warehouse_id)"\s*:\s*)(-?\d{16,})(?=\s*[,}])/g,
|
|
|
|
|
'$1"$2"'
|
|
|
|
|
)
|
|
|
|
|
return JSON.parse(safeLongClaims)
|
2026-08-20 20:10:20 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
if (error.message === '当前运行环境无法解析登录令牌') throw error
|
|
|
|
|
throw new Error('登录响应中的 accessToken 无法解析')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-25 00:24:57 +08:00
|
|
|
function claimId(claims, name) {
|
|
|
|
|
const value = claims?.[name]
|
|
|
|
|
return value === null || value === undefined ? '' : String(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function includesClaim(values, expected) {
|
|
|
|
|
return Array.isArray(values) && values.map(String).includes(expected)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function requireActiveClaims(claims, label) {
|
|
|
|
|
if (claims.exp && Number(claims.exp) * 1000 <= Date.now()) throw new Error(`${label}已过期`)
|
|
|
|
|
if (claims.app_code !== 'PLATFORM' || claimId(claims, 'tenant_id') !== '0') {
|
|
|
|
|
throw new Error('当前账号不属于平台仓库作业端')
|
|
|
|
|
}
|
|
|
|
|
if (!claimId(claims, 'user_id') || !claimId(claims, 'membership_id')) {
|
|
|
|
|
throw new Error(`${label}缺少完整作业主体`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function validatePdaTokenPair(accessToken, overseasAccessToken) {
|
|
|
|
|
const domestic = decodeAccessToken(accessToken)
|
|
|
|
|
const overseas = decodeAccessToken(overseasAccessToken)
|
|
|
|
|
requireActiveClaims(domestic, '登录状态')
|
|
|
|
|
requireActiveClaims(overseas, '海外作业令牌')
|
|
|
|
|
for (const claim of ['user_id', 'membership_id', 'tenant_id', 'app_code', 'overseas_warehouse_id']) {
|
|
|
|
|
if (claimId(domestic, claim) !== claimId(overseas, claim)) throw new Error('国内与海外令牌作业主体不一致')
|
|
|
|
|
}
|
|
|
|
|
const warehouseId = claimId(overseas, 'overseas_warehouse_id')
|
|
|
|
|
if (!/^\d+$/.test(warehouseId) || warehouseId === '0') throw new Error('海外PDA账号未绑定有效作业仓库')
|
2026-08-28 10:39:02 +08:00
|
|
|
if (!includesClaim(overseas.roles, PDA_ROLE)) throw new Error('当前账号不是海外仓 PDA 作业员')
|
2026-08-25 00:24:57 +08:00
|
|
|
if (!includesClaim(overseas.authorities, PDA_AUTHORITY)) throw new Error(`当前账号缺少${PDA_AUTHORITY}权限`)
|
|
|
|
|
return { domestic, overseas, warehouseId }
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 16:50:25 +08:00
|
|
|
function userFromClaims(pair, saved = {}) {
|
|
|
|
|
const savedWarehouseId = String(saved?.warehouseId || '')
|
|
|
|
|
const sameWarehouse = savedWarehouseId === pair.warehouseId
|
2026-08-25 00:24:57 +08:00
|
|
|
return {
|
|
|
|
|
id: claimId(pair.domestic, 'user_id'),
|
|
|
|
|
name: pair.domestic.username || '仓库作业员',
|
|
|
|
|
role: PDA_ROLE,
|
|
|
|
|
roles: [...pair.overseas.roles],
|
2026-08-30 17:38:15 +08:00
|
|
|
warehouse: sameWarehouse && saved.warehouseName ? saved.warehouseName : '',
|
2026-08-26 16:50:25 +08:00
|
|
|
warehouseId: pair.warehouseId,
|
|
|
|
|
warehouseCode: sameWarehouse ? (saved.warehouseCode || '') : '',
|
|
|
|
|
warehouseName: sameWarehouse ? (saved.warehouseName || '') : ''
|
2026-08-25 00:24:57 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 20:10:20 +08:00
|
|
|
function storageAvailable() {
|
|
|
|
|
return typeof uni !== 'undefined' && typeof uni.setStorageSync === 'function' && typeof uni.getStorageSync === 'function'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function persistSession() {
|
|
|
|
|
if (!storageAvailable()) return
|
|
|
|
|
uni.setStorageSync(SESSION_KEY, {
|
|
|
|
|
currentUser: session.currentUser,
|
|
|
|
|
token: session.token,
|
2026-08-24 15:25:36 +08:00
|
|
|
domesticToken: session.domesticToken,
|
2026-08-20 20:10:20 +08:00
|
|
|
refreshToken: session.refreshToken,
|
|
|
|
|
tenantId: session.tenantId,
|
|
|
|
|
apiBase: session.apiBase
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function signIn(account, warehouse, options = {}) {
|
2026-08-26 16:50:25 +08:00
|
|
|
session.currentUser = { id: String(account.id), name: account.name, role: account.role, roles: account.roles || [], warehouse,
|
|
|
|
|
warehouseId: account.warehouseId || null, warehouseCode: account.warehouseCode || '', warehouseName: account.warehouseName || '' }
|
2026-08-20 20:10:20 +08:00
|
|
|
session.token = options.token || account.token || ''
|
2026-08-24 15:25:36 +08:00
|
|
|
session.domesticToken = options.domesticToken || account.domesticToken || ''
|
2026-08-20 20:10:20 +08:00
|
|
|
session.refreshToken = options.refreshToken || account.refreshToken || ''
|
|
|
|
|
session.tenantId = options.tenantId || account.tenantId || ''
|
|
|
|
|
session.apiBase = options.apiBase || account.apiBase || OVERSEAS_API_ROOT
|
|
|
|
|
persistSession()
|
|
|
|
|
return session.currentUser
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function signInWithTokens(tokens) {
|
|
|
|
|
if (!tokens?.accessToken) throw new Error('登录响应缺少 accessToken')
|
2026-08-24 15:25:36 +08:00
|
|
|
if (!tokens?.overseasAccessToken) throw new Error('登录响应缺少 overseasAccessToken')
|
2026-08-25 00:24:57 +08:00
|
|
|
const pair = validatePdaTokenPair(tokens.accessToken, tokens.overseasAccessToken)
|
|
|
|
|
const user = userFromClaims(pair)
|
|
|
|
|
return signIn(user, user.warehouse, {
|
2026-08-24 15:25:36 +08:00
|
|
|
token: tokens.overseasAccessToken,
|
|
|
|
|
domesticToken: tokens.accessToken,
|
2026-08-20 20:10:20 +08:00
|
|
|
refreshToken: tokens.refreshToken || '',
|
2026-08-25 00:24:57 +08:00
|
|
|
tenantId: claimId(pair.domestic, 'tenant_id')
|
2026-08-20 20:10:20 +08:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 15:25:36 +08:00
|
|
|
export function replaceTokens(tokens) {
|
|
|
|
|
if (!tokens?.accessToken || !tokens?.overseasAccessToken) throw new Error('刷新响应缺少双区域访问令牌')
|
2026-08-25 00:24:57 +08:00
|
|
|
const pair = validatePdaTokenPair(tokens.accessToken, tokens.overseasAccessToken)
|
2026-08-26 16:50:25 +08:00
|
|
|
const user = userFromClaims(pair, session.currentUser)
|
2026-08-25 00:24:57 +08:00
|
|
|
const nextRefreshToken = tokens.refreshToken || session.refreshToken
|
|
|
|
|
// Validate the complete replacement before mutating reactive or persisted session state.
|
2026-08-24 15:25:36 +08:00
|
|
|
session.domesticToken = tokens.accessToken
|
|
|
|
|
session.token = tokens.overseasAccessToken
|
2026-08-25 00:24:57 +08:00
|
|
|
session.refreshToken = nextRefreshToken
|
|
|
|
|
session.tenantId = claimId(pair.domestic, 'tenant_id')
|
|
|
|
|
session.currentUser = user
|
2026-08-24 15:25:36 +08:00
|
|
|
persistSession()
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 16:50:25 +08:00
|
|
|
export function applyWarehouseProfile(profile) {
|
|
|
|
|
if (!session.currentUser) throw new Error('当前登录会话不存在')
|
|
|
|
|
const warehouseId = String(profile?.warehouseId || '')
|
|
|
|
|
if (!warehouseId || warehouseId !== String(session.currentUser.warehouseId)) throw new Error('仓库主档与当前登录仓库不一致')
|
|
|
|
|
session.currentUser = {
|
|
|
|
|
...session.currentUser,
|
2026-08-30 17:38:15 +08:00
|
|
|
warehouse: profile.warehouseName || '',
|
2026-08-26 16:50:25 +08:00
|
|
|
warehouseCode: String(profile.warehouseCode || ''),
|
|
|
|
|
warehouseName: String(profile.warehouseName || '')
|
|
|
|
|
}
|
|
|
|
|
persistSession()
|
|
|
|
|
return session.currentUser
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 20:10:20 +08:00
|
|
|
export function restoreSession() {
|
|
|
|
|
if (!storageAvailable()) return false
|
|
|
|
|
const saved = uni.getStorageSync(SESSION_KEY)
|
2026-08-25 00:24:57 +08:00
|
|
|
if (!saved?.token || !saved?.domesticToken || !saved?.refreshToken || !saved?.currentUser) {
|
|
|
|
|
signOut()
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-08-20 20:10:20 +08:00
|
|
|
try {
|
2026-08-25 00:24:57 +08:00
|
|
|
const pair = validatePdaTokenPair(saved.domesticToken, saved.token)
|
|
|
|
|
Object.assign(session, saved, {
|
2026-08-26 16:50:25 +08:00
|
|
|
currentUser: userFromClaims(pair, saved.currentUser), tenantId: claimId(pair.domestic, 'tenant_id'),
|
2026-08-26 15:24:51 +08:00
|
|
|
apiBase: OVERSEAS_API_ROOT
|
2026-08-25 00:24:57 +08:00
|
|
|
})
|
2026-08-20 20:10:20 +08:00
|
|
|
return true
|
|
|
|
|
} catch (_) {
|
2026-08-25 00:24:57 +08:00
|
|
|
signOut()
|
2026-08-20 20:10:20 +08:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function signOut() {
|
|
|
|
|
session.currentUser = null
|
|
|
|
|
session.token = ''
|
2026-08-24 15:25:36 +08:00
|
|
|
session.domesticToken = ''
|
2026-08-20 20:10:20 +08:00
|
|
|
session.refreshToken = ''
|
|
|
|
|
session.tenantId = ''
|
|
|
|
|
session.apiBase = OVERSEAS_API_ROOT
|
|
|
|
|
if (typeof uni !== 'undefined' && typeof uni.removeStorageSync === 'function') uni.removeStorageSync(SESSION_KEY)
|
2026-08-25 10:39:50 +08:00
|
|
|
resetReceivingSession()
|
2026-09-01 14:23:55 +08:00
|
|
|
resetFulfillmentSession()
|
2026-08-20 20:10:20 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function requireSession() {
|
|
|
|
|
if (session.currentUser || restoreSession()) return true
|
|
|
|
|
uni.reLaunch({ url: '/pages/login/login' })
|
|
|
|
|
return false
|
|
|
|
|
}
|