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={ + + } + /> + + {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: '查看高风险操作与变更记录。' }, }