feat: add local fallback for S3 writes

This commit is contained in:
237899745
2026-07-25 16:29:05 +08:00
parent 63744c6d2f
commit 0fe3d4ce8e
11 changed files with 169 additions and 58 deletions

View File

@@ -8,6 +8,8 @@ const auth = useAuthStore()
const router = useRouter()
const isLoggedIn = computed(() => auth.isLoggedIn)
const consoleRoute = computed(() => (auth.user?.role === 'admin' ? '/admin' : '/dashboard'))
const consoleLabel = computed(() => (auth.user?.role === 'admin' ? '管理后台' : '控制台'))
function logout() {
auth.logout()
@@ -35,10 +37,10 @@ function logout() {
<div class="flex items-center gap-3">
<template v-if="isLoggedIn">
<RouterLink
to="/dashboard"
:to="consoleRoute"
class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50"
>
控制台
{{ consoleLabel }}
</RouterLink>
<button
type="button"

View File

@@ -81,7 +81,7 @@ export function createAppRouter(pinia: Pinia) {
}
if ((to.name === 'login' || to.name === 'register') && auth.isLoggedIn) {
return { name: 'dashboard' }
return { name: auth.user?.role === 'admin' ? 'admin' : 'dashboard' }
}
if (to.meta?.requiresAdmin && auth.user?.role !== 'admin') {

View File

@@ -22,8 +22,10 @@ async function submit() {
const resp = await login(email.value.trim(), password.value)
auth.setAuth(resp.token, resp.user)
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/dashboard'
await router.push(redirect)
const defaultRoute = resp.user.role === 'admin' ? '/admin' : '/dashboard'
const requestedRoute = typeof route.query.redirect === 'string' ? route.query.redirect : null
const redirect = requestedRoute?.startsWith('/') && !requestedRoute.startsWith('//') ? requestedRoute : defaultRoute
await router.replace(redirect)
} catch (err) {
if (err instanceof ApiError) {
error.value = `[${err.code}] ${err.message}`

View File

@@ -20,6 +20,8 @@ const loading = ref(true)
const error = ref<string | null>(null)
const message = ref<string | null>(null)
const activeBackend = ref<'local' | 's3'>('local')
const localObjectCount = ref(0)
const localStoredBytes = ref(0)
const endpoints = ref<AdminStorageEndpoint[]>([])
const editingId = ref<string | null>(null)
const formOpen = ref(false)
@@ -59,6 +61,8 @@ async function loadEndpoints(clearError = true) {
const response = await listStorageEndpoints(auth.token)
endpoints.value = response.endpoints
activeBackend.value = response.active_backend
localObjectCount.value = response.local_object_count
localStoredBytes.value = response.local_stored_bytes
} catch (err) {
error.value = errorText(err, '加载存储配置失败')
} finally {
@@ -207,7 +211,7 @@ onMounted(loadEndpoints)
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-cyan-300">Storage control plane</p>
<h2 class="mt-3 text-2xl font-semibold">对象存储</h2>
<p class="mt-2 max-w-2xl text-sm leading-6 text-slate-300">
应用完成鉴权后返回短期签名地址图片与批量 ZIP 的下载流量直接由高带宽 S3 节点承担每个对象固定绑定写入端点后续切换或扩容不会影响旧文件
新对象优先写入活动 S3连接或写入失败时自动保存到应用服务器本地每个对象固定记录实际写入后端恢复 S3 后的新文件会自动重新走对象存储
</p>
</div>
<div class="rounded-xl border border-white/10 bg-white/5 p-4">
@@ -217,12 +221,16 @@ onMounted(loadEndpoints)
class="rounded-full px-3 py-1 text-xs font-semibold"
:class="activeBackend === 's3' ? 'bg-emerald-400/15 text-emerald-300' : 'bg-amber-400/15 text-amber-200'"
>
{{ activeBackend === 's3' ? 'S3 已启用' : '本地回退' }}
{{ activeBackend === 's3' ? 'S3 优先' : '本地' }}
</span>
</div>
<p class="mt-3 text-xs leading-5 text-slate-400">
推荐内部 Endpoint 走两台服务器之间的 WireGuard 地址公网 Endpoint 使用带 TLS 的下载域名签名默认 5 分钟有效
S3 故障只影响当次写入下载与到期清理会按对象记录的真实后端执行签名默认 5 分钟有效
</p>
<div class="mt-3 flex items-center justify-between border-t border-white/10 pt-3 text-xs">
<span class="text-slate-400">本地对象</span>
<span class="font-medium text-slate-200">{{ localObjectCount }} · {{ formatBytes(localStoredBytes) }}</span>
</div>
</div>
</div>
</section>
@@ -238,7 +246,7 @@ onMounted(loadEndpoints)
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="font-semibold text-slate-900">存储端点</h3>
<p class="mt-1 text-sm text-slate-500">同一时间只有一个端点接收新对象停用端点继续服务其历史对象</p>
<p class="mt-1 text-sm text-slate-500">同一时间只有一个首选 S3 端点写入失败自动回退本地停用端点继续服务其历史对象</p>
</div>
<button
class="rounded-lg bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800"

View File

@@ -669,6 +669,8 @@ export interface AdminStorageEndpoint {
export interface AdminStorageEndpointsResponse {
active_backend: 'local' | 's3'
local_object_count: number
local_stored_bytes: number
endpoints: AdminStorageEndpoint[]
}