feat: 管理端聚合页、小程序/抖音埋点与统计、飞书线索 webhook、API 迁移与路由
- admin:OrdersHub/UsersHub、Commerce/Ops/Enterprise Hub、MpAnalytics、Feishu/小程序配置面板、鉴权存储 - api:Analytics、DataMigration、FeishuLeadWebhook、mp 事件迁移 SQL - 微信/抖音小程序:analytics 上报与相关页面调整 - 开发文档与 scripts 补充 Made-with: Cursor
This commit is contained in:
88
admin/src/utils/authStorage.ts
Normal file
88
admin/src/utils/authStorage.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 管理端与超管端使用独立 localStorage,避免互相覆盖 Token。
|
||||
* 兼容旧键 authToken / userRole:首次加载时迁移到新键后删除旧键。
|
||||
*/
|
||||
|
||||
export const ADMIN_TOKEN_KEY = 'adminAuthToken'
|
||||
export const ADMIN_ROLE_KEY = 'adminUserRole'
|
||||
export const ADMIN_USER_ID_KEY = 'adminUserId'
|
||||
|
||||
export const SUPERADMIN_TOKEN_KEY = 'superadminAuthToken'
|
||||
export const SUPERADMIN_ROLE_KEY = 'superadminUserRole'
|
||||
export const SUPERADMIN_USER_ID_KEY = 'superadminUserId'
|
||||
|
||||
const LEGACY_TOKEN = 'authToken'
|
||||
const LEGACY_ROLE = 'userRole'
|
||||
const LEGACY_USER_ID = 'userId'
|
||||
|
||||
/** 应用启动时调用一次 */
|
||||
export function migrateLegacyAuthStorage(): void {
|
||||
if (typeof localStorage === 'undefined') return
|
||||
|
||||
const legacyToken = localStorage.getItem(LEGACY_TOKEN)
|
||||
const legacyRole = localStorage.getItem(LEGACY_ROLE)
|
||||
if (!legacyToken || !legacyRole) return
|
||||
|
||||
const hasSplit =
|
||||
localStorage.getItem(ADMIN_TOKEN_KEY) || localStorage.getItem(SUPERADMIN_TOKEN_KEY)
|
||||
if (hasSplit) {
|
||||
localStorage.removeItem(LEGACY_TOKEN)
|
||||
localStorage.removeItem(LEGACY_ROLE)
|
||||
localStorage.removeItem(LEGACY_USER_ID)
|
||||
return
|
||||
}
|
||||
|
||||
const legacyUserId = localStorage.getItem(LEGACY_USER_ID)
|
||||
if (legacyRole === 'superadmin') {
|
||||
localStorage.setItem(SUPERADMIN_TOKEN_KEY, legacyToken)
|
||||
localStorage.setItem(SUPERADMIN_ROLE_KEY, legacyRole)
|
||||
if (legacyUserId) localStorage.setItem(SUPERADMIN_USER_ID_KEY, legacyUserId)
|
||||
} else if (['admin', 'enterprise_admin'].includes(legacyRole)) {
|
||||
localStorage.setItem(ADMIN_TOKEN_KEY, legacyToken)
|
||||
localStorage.setItem(ADMIN_ROLE_KEY, legacyRole)
|
||||
if (legacyUserId) localStorage.setItem(ADMIN_USER_ID_KEY, legacyUserId)
|
||||
}
|
||||
|
||||
localStorage.removeItem(LEGACY_TOKEN)
|
||||
localStorage.removeItem(LEGACY_ROLE)
|
||||
localStorage.removeItem(LEGACY_USER_ID)
|
||||
}
|
||||
|
||||
export function getAdminToken(): string | null {
|
||||
return localStorage.getItem(ADMIN_TOKEN_KEY)
|
||||
}
|
||||
|
||||
export function getAdminRole(): string | null {
|
||||
return localStorage.getItem(ADMIN_ROLE_KEY)
|
||||
}
|
||||
|
||||
export function getSuperadminToken(): string | null {
|
||||
return localStorage.getItem(SUPERADMIN_TOKEN_KEY)
|
||||
}
|
||||
|
||||
export function getSuperadminRole(): string | null {
|
||||
return localStorage.getItem(SUPERADMIN_ROLE_KEY)
|
||||
}
|
||||
|
||||
/** axios / 上传:按当前页面路径选择 Bearer */
|
||||
export function getBearerTokenForCurrentApp(): string | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
if (window.location.pathname.startsWith('/superadmin')) {
|
||||
return getSuperadminToken()
|
||||
}
|
||||
return getAdminToken()
|
||||
}
|
||||
|
||||
export function clearAdminAuthKeys(): void {
|
||||
localStorage.removeItem(ADMIN_TOKEN_KEY)
|
||||
localStorage.removeItem(ADMIN_ROLE_KEY)
|
||||
localStorage.removeItem(ADMIN_USER_ID_KEY)
|
||||
localStorage.removeItem('adminLoggedIn')
|
||||
}
|
||||
|
||||
export function clearSuperadminAuthKeys(): void {
|
||||
localStorage.removeItem(SUPERADMIN_TOKEN_KEY)
|
||||
localStorage.removeItem(SUPERADMIN_ROLE_KEY)
|
||||
localStorage.removeItem(SUPERADMIN_USER_ID_KEY)
|
||||
localStorage.removeItem('superAdminLoggedIn')
|
||||
}
|
||||
@@ -1,118 +1,127 @@
|
||||
import axios from 'axios'
|
||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 获取API基础URL
|
||||
const getBaseURL = (): string => {
|
||||
const envURL = import.meta.env.VITE_API_BASE_URL
|
||||
if (envURL) {
|
||||
// 如果环境变量是完整URL,拼接 /api/v1
|
||||
return envURL.endsWith('/') ? `${envURL}api/v1` : `${envURL}/api/v1`
|
||||
}
|
||||
// 默认使用相对路径
|
||||
return '/api/v1'
|
||||
}
|
||||
|
||||
// 创建 axios 实例
|
||||
const service: AxiosInstance = axios.create({
|
||||
baseURL: getBaseURL(),
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
// 可以在这里添加 token
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
console.error('请求错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const res = response.data
|
||||
|
||||
// 如果返回的状态码不是 200,则认为是错误
|
||||
if (res.code && res.code !== 200) {
|
||||
ElMessage.error(res.message || '请求失败')
|
||||
return Promise.reject(new Error(res.message || '请求失败'))
|
||||
}
|
||||
|
||||
return res
|
||||
},
|
||||
(error) => {
|
||||
console.error('响应错误:', error)
|
||||
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
|
||||
if (status === 401) {
|
||||
ElMessage.error('未授权,请重新登录')
|
||||
// 清除所有登录状态和Token
|
||||
localStorage.removeItem('authToken')
|
||||
localStorage.removeItem('userRole')
|
||||
localStorage.removeItem('userId')
|
||||
localStorage.removeItem('adminLoggedIn')
|
||||
localStorage.removeItem('superAdminLoggedIn')
|
||||
|
||||
// 根据当前路径跳转到对应登录页
|
||||
if (window.location.pathname.startsWith('/superadmin')) {
|
||||
window.location.href = '/superadmin/login'
|
||||
} else {
|
||||
window.location.href = '/admin/login'
|
||||
}
|
||||
} else if (status === 403) {
|
||||
ElMessage.error('拒绝访问')
|
||||
} else if (status === 404) {
|
||||
ElMessage.error('请求地址不存在')
|
||||
} else if (status === 500) {
|
||||
ElMessage.error('服务器错误')
|
||||
} else {
|
||||
ElMessage.error(data?.message || '请求失败')
|
||||
}
|
||||
} else if (error.request) {
|
||||
ElMessage.error('网络错误,请检查网络连接')
|
||||
} else {
|
||||
ElMessage.error('请求配置错误')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 导出请求方法
|
||||
export const request = {
|
||||
get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.get(url, config)
|
||||
},
|
||||
|
||||
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.post(url, data, config)
|
||||
},
|
||||
|
||||
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.put(url, data, config)
|
||||
},
|
||||
|
||||
delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.delete(url, config)
|
||||
},
|
||||
|
||||
patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.patch(url, data, config)
|
||||
}
|
||||
}
|
||||
|
||||
export default service
|
||||
|
||||
import axios from 'axios'
|
||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
getBearerTokenForCurrentApp,
|
||||
clearAdminAuthKeys,
|
||||
clearSuperadminAuthKeys
|
||||
} from '@/utils/authStorage'
|
||||
|
||||
let lastBizErrorKey = ''
|
||||
let lastBizErrorAt = 0
|
||||
function showBizErrorOnce(message: string) {
|
||||
const key = message || '请求失败'
|
||||
const now = Date.now()
|
||||
if (key === lastBizErrorKey && now - lastBizErrorAt < 1800) return
|
||||
lastBizErrorKey = key
|
||||
lastBizErrorAt = now
|
||||
ElMessage.error(key)
|
||||
}
|
||||
|
||||
// 获取API基础URL(开发环境留空 VITE_API_BASE_URL 时走同源 /api/v1,由 Vite 代理到本机后端)
|
||||
const getBaseURL = (): string => {
|
||||
const raw = import.meta.env.VITE_API_BASE_URL as string | undefined
|
||||
const envURL = typeof raw === 'string' ? raw.trim() : ''
|
||||
if (envURL) {
|
||||
return envURL.endsWith('/') ? `${envURL}api/v1` : `${envURL}/api/v1`
|
||||
}
|
||||
return '/api/v1'
|
||||
}
|
||||
|
||||
// 创建 axios 实例
|
||||
const service: AxiosInstance = axios.create({
|
||||
baseURL: getBaseURL(),
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
// 可以在这里添加 token
|
||||
const token = getBearerTokenForCurrentApp()
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
console.error('请求错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const res = response.data
|
||||
|
||||
// 如果返回的状态码不是 200,则认为是错误
|
||||
if (res.code && res.code !== 200) {
|
||||
showBizErrorOnce(res.message || '请求失败')
|
||||
return Promise.reject(new Error(res.message || '请求失败'))
|
||||
}
|
||||
|
||||
return res
|
||||
},
|
||||
(error) => {
|
||||
console.error('响应错误:', error)
|
||||
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
|
||||
if (status === 401) {
|
||||
ElMessage.error('未授权,请重新登录')
|
||||
if (window.location.pathname.startsWith('/superadmin')) {
|
||||
clearSuperadminAuthKeys()
|
||||
window.location.href = '/superadmin/login'
|
||||
} else {
|
||||
clearAdminAuthKeys()
|
||||
window.location.href = '/admin/login'
|
||||
}
|
||||
} else if (status === 403) {
|
||||
showBizErrorOnce(data?.message || '拒绝访问')
|
||||
} else if (status === 404) {
|
||||
ElMessage.error('请求地址不存在')
|
||||
} else if (status === 500) {
|
||||
ElMessage.error('服务器错误')
|
||||
} else {
|
||||
ElMessage.error(data?.message || '请求失败')
|
||||
}
|
||||
} else if (error.request) {
|
||||
ElMessage.error('网络错误,请检查网络连接')
|
||||
} else {
|
||||
ElMessage.error('请求配置错误')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 导出请求方法
|
||||
export const request = {
|
||||
get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.get(url, config)
|
||||
},
|
||||
|
||||
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.post(url, data, config)
|
||||
},
|
||||
|
||||
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.put(url, data, config)
|
||||
},
|
||||
|
||||
delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.delete(url, config)
|
||||
},
|
||||
|
||||
patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
return service.patch(url, data, config)
|
||||
}
|
||||
}
|
||||
|
||||
export default service
|
||||
|
||||
|
||||
Reference in New Issue
Block a user