feat: 上游 Key 唯一化、分组导入跳过、账号导入平台识别&远端校验&base_url 注入
- 上游 Key 命名改为 {prefix}-{upstream.id}-{safe_group_name}-{group_id}
- 唯一约束 (upstream_id, group_id, managed_prefix) 加 managed_prefix 列
- 上游检测成功时同步 Key 状态,远端已删/分组已删自动清理
- 重复分组导入跳过,目标网站已存在同名分组返回 exists
- 账号导入平台自动识别(auto/manual 模式)
- 全选可导入 Key 按钮 + 目标分组自动匹配
- 导入幂等:已导入过的 Key 校验远端账号,不存在则重建
- 新增同步接口 POST /sync-imported-upstream-keys
- account_exists() 通过拉取账号列表判断,避免 404 误判
- credentials.base_url 注入来源上游地址,避免 401
- 前端导入弹窗自动同步+刷新按钮+并发/优先级设置
- 新增 12 个测试覆盖同步、幂等、远端删除、校验失败路径
This commit is contained in:
@@ -1,7 +1,28 @@
|
||||
import axios from 'axios'
|
||||
import axiosRetry from 'axios-retry'
|
||||
import router from '@/router'
|
||||
import { authStorageKeys } from '@/authStorage'
|
||||
|
||||
/** 标记是否正在处理 401,防多个并发 */
|
||||
let isHandlingUnauthorized = false
|
||||
|
||||
/** 统一的 401 处理:清登录态 + 提示 + 跳转 /login */
|
||||
async function handleUnauthorized() {
|
||||
if (isHandlingUnauthorized) return
|
||||
isHandlingUnauthorized = true
|
||||
try {
|
||||
const { useAuthStore } = await import('@/stores/auth')
|
||||
useAuthStore().clear()
|
||||
|
||||
const { ElMessage } = await import('element-plus')
|
||||
ElMessage.warning('登录已过期,请重新登录')
|
||||
|
||||
if (router.currentRoute.value.path !== '/login') {
|
||||
await router.replace('/login')
|
||||
}
|
||||
} finally {
|
||||
isHandlingUnauthorized = false
|
||||
}
|
||||
}
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: '/',
|
||||
@@ -27,10 +48,13 @@ axiosRetry(api, {
|
||||
api.interceptors.response.use(
|
||||
(r) => r,
|
||||
(err) => {
|
||||
// 跳过登录接口的 401(密码错误等正常登录失败场景)
|
||||
const requestPath = new URL(err.config?.url || '', window.location.origin).pathname
|
||||
if (err.response?.status === 401 && requestPath === '/api/auth/login') {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
if (err.response?.status === 401) {
|
||||
localStorage.removeItem(authStorageKeys.token)
|
||||
localStorage.removeItem(authStorageKeys.email)
|
||||
router.push('/login')
|
||||
void handleUnauthorized()
|
||||
}
|
||||
return Promise.reject(err)
|
||||
}
|
||||
@@ -84,6 +108,34 @@ export interface UpstreamForm {
|
||||
balance_divisor: number
|
||||
}
|
||||
|
||||
export interface GeneratedUpstreamKey {
|
||||
id: number | null
|
||||
upstream_id: number
|
||||
group_id: string
|
||||
group_name: string
|
||||
key_id: string | null
|
||||
key_name: string
|
||||
key_value: string | null
|
||||
masked_key: string
|
||||
status: string
|
||||
error: string | null
|
||||
imported_website_id: number | null
|
||||
imported_account_id: string | null
|
||||
imported_at: string | null
|
||||
created_at: string | null
|
||||
}
|
||||
|
||||
export interface GenerateKeysByGroupsForm {
|
||||
group_ids: string[]
|
||||
name_prefix: string
|
||||
quota: number
|
||||
expires_in_days?: number | null
|
||||
rate_limit_5h: number
|
||||
rate_limit_1d: number
|
||||
rate_limit_7d: number
|
||||
endpoint: string
|
||||
}
|
||||
|
||||
export const upstreamsApi = {
|
||||
list: () => api.get<UpstreamData[]>('/api/upstreams'),
|
||||
create: (data: UpstreamForm) => api.post<UpstreamData>('/api/upstreams', data),
|
||||
@@ -91,6 +143,9 @@ export const upstreamsApi = {
|
||||
delete: (id: number) => api.delete(`/api/upstreams/${id}`),
|
||||
test: (id: number) => api.post<{ success: boolean; message: string; detail?: string }>(`/api/upstreams/${id}/test`),
|
||||
checkNow: (id: number) => api.post<{ success: boolean; message: string }>(`/api/upstreams/${id}/check-now`),
|
||||
generatedKeys: (id: number) => api.get<GeneratedUpstreamKey[]>(`/api/upstreams/${id}/generated-keys`),
|
||||
generateKeysByGroups: (id: number, data: GenerateKeysByGroupsForm) =>
|
||||
api.post<{ success: boolean; message: string; items: GeneratedUpstreamKey[] }>(`/api/upstreams/${id}/keys/generate-by-groups`, data),
|
||||
latestSnapshot: (id: number) => api.get(`/api/upstreams/${id}/snapshots/latest`),
|
||||
listSnapshots: (id: number, limit = 20, offset = 0) =>
|
||||
api.get<any[]>(`/api/upstreams/${id}/snapshots`, { params: { limit, offset } }),
|
||||
@@ -185,6 +240,30 @@ export interface WebsiteSyncLog {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ImportGroupItem {
|
||||
source_group_id: string
|
||||
source_group_name: string
|
||||
target_group_id: string | null
|
||||
target_group_name: string
|
||||
status: string
|
||||
message: string
|
||||
raw: Record<string, any>
|
||||
}
|
||||
|
||||
export interface ImportAccountItem {
|
||||
upstream_key_id: number
|
||||
source_group_id: string
|
||||
source_group_name: string
|
||||
target_group_id: string | null
|
||||
account_id: string | null
|
||||
account_name: string
|
||||
platform: string
|
||||
upstream_base_url: string
|
||||
status: string
|
||||
message: string
|
||||
raw: Record<string, any>
|
||||
}
|
||||
|
||||
export const websitesApi = {
|
||||
list: () => api.get<WebsiteData[]>('/api/websites'),
|
||||
create: (data: WebsiteForm) => api.post<WebsiteData>('/api/websites', data),
|
||||
@@ -192,6 +271,19 @@ export const websitesApi = {
|
||||
delete: (id: number) => api.delete(`/api/websites/${id}`),
|
||||
test: (id: number) => api.post<{ success: boolean; message: string; detail?: string }>(`/api/websites/${id}/test`),
|
||||
groups: (id: number) => api.get<WebsiteGroup[]>(`/api/websites/${id}/groups`),
|
||||
importGroupsFromUpstream: (id: number, upstreamId: number, data: { group_ids: string[]; name_prefix: string }) =>
|
||||
api.post<{ success: boolean; message: string; items: ImportGroupItem[] }>(`/api/websites/${id}/groups/import-from-upstream/${upstreamId}`, data),
|
||||
syncImportedUpstreamKeys: (id: number, data: { upstream_id: number }) =>
|
||||
api.post<{ success: boolean; message: string; items: ImportAccountItem[] }>(`/api/websites/${id}/accounts/sync-imported-upstream-keys`, data),
|
||||
importAccountsFromUpstreamKeys: (id: number, data: {
|
||||
upstream_key_ids: number[]
|
||||
target_group_map: Record<string, string>
|
||||
account_name_prefix: string
|
||||
default_platform: string
|
||||
platform_mode?: string
|
||||
concurrency?: number
|
||||
priority?: number
|
||||
}) => api.post<{ success: boolean; message: string; items: ImportAccountItem[] }>(`/api/websites/${id}/accounts/import-upstream-keys`, data),
|
||||
listBindings: () => api.get<GroupBindingData[]>('/api/group-bindings'),
|
||||
createBinding: (data: GroupBindingForm) => api.post<GroupBindingData>('/api/group-bindings', data),
|
||||
updateBinding: (id: number, data: Partial<GroupBindingForm>) => api.put<GroupBindingData>(`/api/group-bindings/${id}`, data),
|
||||
|
||||
Reference in New Issue
Block a user