diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx
index 69322f2..ce0e02b 100644
--- a/frontend/src/app/AppRouter.tsx
+++ b/frontend/src/app/AppRouter.tsx
@@ -276,6 +276,11 @@ const AdminStaffAccountsPage = lazy(() =>
default: module.AdminStaffAccountsPage,
}))
)
+const AdminBackupsPage = lazy(() =>
+ import('@/features/admin/AdminBackupsPage').then(module => ({
+ default: module.AdminBackupsPage,
+ }))
+)
const AdminManualEntriesPage = lazy(() =>
import('@/features/admin/AdminManualEntriesPage').then(module => ({
default: module.AdminManualEntriesPage,
@@ -489,6 +494,7 @@ export function AppRouter() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/frontend/src/features/admin/AdminBackupsPage.tsx b/frontend/src/features/admin/AdminBackupsPage.tsx
new file mode 100644
index 0000000..5471f30
--- /dev/null
+++ b/frontend/src/features/admin/AdminBackupsPage.tsx
@@ -0,0 +1,187 @@
+import { useEffect, useState } from 'react'
+import { useQuery, useQueryClient } from '@tanstack/react-query'
+import {
+ Alert,
+ App,
+ Button,
+ Card,
+ Descriptions,
+ Form,
+ InputNumber,
+ Space,
+ Switch,
+ Tag,
+ Typography,
+} from 'antd'
+import { ReloadOutlined } from '@ant-design/icons'
+
+import {
+ fetchAdminBackupSchedule,
+ fetchAdminBackupStatus,
+ updateAdminBackupSchedule,
+} from '@/features/admin/api'
+import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
+import type { AdminBackupSchedule } from '@/features/admin/types'
+import { useAdminSessionStore } from '@/stores/adminSession'
+import { formatText } from '@/shared/utils/format'
+
+function errorText(error: unknown) {
+ const response = (error as { response?: { data?: { message?: string } } })?.response
+ return response?.data?.message || (error instanceof Error ? error.message : '请求失败')
+}
+
+export function AdminBackupsPage() {
+ const { message } = App.useApp()
+ const queryClient = useQueryClient()
+ const [form] = Form.useForm()
+ const [saving, setSaving] = useState(false)
+ const permissions = useAdminSessionStore(state => state.admin?.permissions)
+ const canView = Boolean(permissions?.includes('*') || permissions?.includes('backup:view'))
+ const canUpdate = Boolean(
+ permissions?.includes('*') || permissions?.includes('backup:schedule:update')
+ )
+
+ const statusQuery = useQuery({
+ queryKey: ['admin-backup-status'],
+ queryFn: fetchAdminBackupStatus,
+ enabled: canView,
+ retry: false,
+ })
+ const scheduleQuery = useQuery({
+ queryKey: ['admin-backup-schedule'],
+ queryFn: fetchAdminBackupSchedule,
+ enabled: canView,
+ })
+
+ useEffect(() => {
+ if (!scheduleQuery.data) return
+ form.setFieldsValue(scheduleQuery.data)
+ }, [scheduleQuery.data, form])
+
+ if (!canView) {
+ return (
+
+ )
+ }
+
+ const status = statusQuery.data
+ const statusUnavailable =
+ statusQuery.isError &&
+ /尚未生成|不可用/.test(
+ String((statusQuery.error as { response?: { data?: { message?: string } } })?.response?.data?.message ?? '')
+ )
+
+ return (
+
+
+ {status?.health ? (
+ 健康状态 {status.health}
+ ) : (
+ 状态未生成
+ )}
+ 计划 {scheduleQuery.data?.enabled ? '启用' : '停用'}
+
+ }
+ extra={
+ }
+ loading={statusQuery.isFetching || scheduleQuery.isFetching}
+ onClick={() => {
+ void statusQuery.refetch()
+ void scheduleQuery.refetch()
+ }}
+ >
+ 刷新
+
+ }
+ />
+
+ {statusUnavailable ? (
+
+ ) : statusQuery.isError ? (
+
+ ) : null}
+
+
+
+ {formatText(status?.generated_at)}
+ {formatText(status?.health)}
+
+ {formatText(status?.last_full?.local_complete)}
+
+
+ {formatText(status?.last_full?.remote_complete)}
+
+
+ {formatText(status?.last_binlog?.file)}
+
+
+ {formatText(status?.last_binlog?.remote_dir)}
+
+
+ {formatText(status?.last_job?.type)} · {formatText(status?.last_job?.status)} ·{' '}
+ {formatText(status?.last_job?.at)}
+
+
+ {formatText(status?.last_job?.message)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 全量备份每天在指定时间执行;binlog 按间隔增量归档到远端。修改后需等待备份脚本下次读取配置。
+
+
+
+ )
+}
diff --git a/frontend/src/features/admin/api.ts b/frontend/src/features/admin/api.ts
index b20d9b7..0609829 100644
--- a/frontend/src/features/admin/api.ts
+++ b/frontend/src/features/admin/api.ts
@@ -127,6 +127,8 @@ import type {
AdminInvitationRelationPage,
AdminInviteRebatePage,
AdminInviteRebateSummary,
+ AdminBackupStatus,
+ AdminBackupSchedule,
ManualDisbursementPayload,
OperatingExpensePage,
OperatingExpensePayload,
@@ -1878,3 +1880,23 @@ export async function fetchAdminInviteRebateSummary(query: {
)
return data.data
}
+
+export async function fetchAdminBackupStatus() {
+ const { data } = await adminClient.get>('/admin/backups/status')
+ return data.data
+}
+
+export async function fetchAdminBackupSchedule() {
+ const { data } = await adminClient.get>(
+ '/admin/backups/schedule'
+ )
+ return data.data
+}
+
+export async function updateAdminBackupSchedule(payload: AdminBackupSchedule) {
+ const { data } = await adminClient.put>(
+ '/admin/backups/schedule',
+ payload
+ )
+ return data.data
+}
diff --git a/frontend/src/features/admin/types.ts b/frontend/src/features/admin/types.ts
index ce867d4..3075364 100644
--- a/frontend/src/features/admin/types.ts
+++ b/frontend/src/features/admin/types.ts
@@ -2039,3 +2039,24 @@ export interface AdminTrackingOverview {
+
+export interface AdminBackupStatus {
+ generated_at: string
+ health: string
+ schedule: {
+ enabled: string
+ full_hour: string
+ full_minute: string
+ binlog_interval_minutes: string
+ }
+ last_full: { local_complete: string; remote_complete: string }
+ last_binlog: { file: string; remote_dir: string }
+ last_job: { type: string; status: string; at: string; message: string }
+}
+
+export interface AdminBackupSchedule {
+ enabled: boolean
+ full_hour: number
+ full_minute: number
+ binlog_interval_minutes: number
+}
diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx
index 7c8d9f4..d1b3a32 100644
--- a/frontend/src/layouts/AdminLayout.tsx
+++ b/frontend/src/layouts/AdminLayout.tsx
@@ -163,6 +163,7 @@ const allNavGroups: NavGroup[] = [
{ key: '/roles', label: '角色权限', permission: 'role:manage' },
{ key: '/admin-users', label: '管理员账号', permission: 'admin_user:manage' },
{ key: '/payment-configs', label: '支付配置', permission: 'payment_config:list' },
+ { key: '/backups', label: '备份管理', permission: 'backup:view' },
{ key: '/audit-logs', label: '审计日志', permission: 'audit_log:view' },
],
},
@@ -231,6 +232,7 @@ const sectionMeta: Record = {
},
'/system-configs': { title: '系统配置', description: '维护首页、发布、订单与支付配置。' },
'/payment-configs': { title: '支付配置', description: '维护主站支付商户、渠道与回调参数。' },
+ '/backups': { title: '备份管理', description: '查看备份健康状态并维护定时备份计划。' },
'/audit-logs': { title: '审计日志', description: '查看高风险操作与变更记录。' },
}