feat(admin): 新增备份管理页

This commit is contained in:
yml2213
2026-10-30 17:26:11 +08:00
parent 5fd880aa7a
commit c9b122d4c8
5 changed files with 238 additions and 0 deletions
+6
View File
@@ -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() {
<Route path="payment-configs" element={<AdminPaymentConfigsPage />} />
<Route path="roles" element={<AdminRolesPage />} />
<Route path="admin-users" element={<AdminStaffAccountsPage />} />
<Route path="backups" element={<AdminBackupsPage />} />
<Route path="manual-entries" element={<AdminManualEntriesPage />} />
<Route path="payments" element={<AdminPaymentsPage />} />
<Route path="pickup" element={<AdminPickupsPage />} />
@@ -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<AdminBackupSchedule>()
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 (
<div className="admin-page">
<AdminPageHeader eyebrow="JIEYOU OPS · 系统" title="备份管理" />
<Alert type="warning" showIcon message="当前账号没有备份查看权限(backup:view)" />
</div>
)
}
const status = statusQuery.data
const statusUnavailable =
statusQuery.isError &&
/尚未生成|不可用/.test(
String((statusQuery.error as { response?: { data?: { message?: string } } })?.response?.data?.message ?? '')
)
return (
<div className="admin-page">
<AdminPageHeader
eyebrow="JIEYOU OPS · 系统"
title="备份管理"
description="查看数据库全量/增量备份状态,并维护定时备份计划。"
meta={
<Space size={8} wrap>
{status?.health ? (
<Tag color={status.health === 'ok' ? 'green' : 'red'}>健康状态 {status.health}</Tag>
) : (
<Tag>状态未生成</Tag>
)}
<Tag color="geekblue">计划 {scheduleQuery.data?.enabled ? '启用' : '停用'}</Tag>
</Space>
}
extra={
<Button
icon={<ReloadOutlined />}
loading={statusQuery.isFetching || scheduleQuery.isFetching}
onClick={() => {
void statusQuery.refetch()
void scheduleQuery.refetch()
}}
>
刷新
</Button>
}
/>
{statusUnavailable ? (
<Alert
type="info"
showIcon
message="备份状态尚未生成"
description="备份监控脚本尚未在服务器写入状态文件;配置好定时任务后会出现在这里。"
/>
) : statusQuery.isError ? (
<Alert type="error" showIcon message="备份状态读取失败" description={errorText(statusQuery.error)} />
) : null}
<Card title="最近备份" className="admin-table-card">
<Descriptions column={2} size="middle" bordered>
<Descriptions.Item label="状态生成时间">{formatText(status?.generated_at)}</Descriptions.Item>
<Descriptions.Item label="健康状态">{formatText(status?.health)}</Descriptions.Item>
<Descriptions.Item label="最近全量(本地)">
{formatText(status?.last_full?.local_complete)}
</Descriptions.Item>
<Descriptions.Item label="最近全量(远端)">
{formatText(status?.last_full?.remote_complete)}
</Descriptions.Item>
<Descriptions.Item label="最近 binlog 文件">
{formatText(status?.last_binlog?.file)}
</Descriptions.Item>
<Descriptions.Item label="最近 binlog 远端目录">
{formatText(status?.last_binlog?.remote_dir)}
</Descriptions.Item>
<Descriptions.Item label="最近任务" span={2}>
{formatText(status?.last_job?.type)} · {formatText(status?.last_job?.status)} ·{' '}
{formatText(status?.last_job?.at)}
</Descriptions.Item>
<Descriptions.Item label="任务信息" span={2}>
{formatText(status?.last_job?.message)}
</Descriptions.Item>
</Descriptions>
</Card>
<Card title="定时备份计划" className="admin-table-card" style={{ marginTop: 16 }}>
<Form form={form} layout="inline" style={{ gap: 12, flexWrap: 'wrap' }}>
<Form.Item name="enabled" label="启用" valuePropName="checked">
<Switch disabled={!canUpdate} />
</Form.Item>
<Form.Item name="full_hour" label="全量小时">
<InputNumber min={0} max={23} style={{ width: 90 }} disabled={!canUpdate} />
</Form.Item>
<Form.Item name="full_minute" label="全量分钟">
<InputNumber min={0} max={59} style={{ width: 90 }} disabled={!canUpdate} />
</Form.Item>
<Form.Item name="binlog_interval_minutes" label="binlog 间隔(分钟)">
<InputNumber min={1} max={1440} style={{ width: 120 }} disabled={!canUpdate} />
</Form.Item>
<Form.Item>
<Button
type="primary"
loading={saving}
disabled={!canUpdate}
onClick={async () => {
const values = await form.validateFields()
setSaving(true)
try {
await updateAdminBackupSchedule(values)
message.success('备份计划已更新')
await queryClient.invalidateQueries({ queryKey: ['admin-backup-schedule'] })
} catch (error) {
message.error(errorText(error))
} finally {
setSaving(false)
}
}}
>
保存计划
</Button>
</Form.Item>
</Form>
<Typography.Paragraph type="secondary" style={{ margin: '10px 0 0' }}>
全量备份每天在指定时间执行;binlog 按间隔增量归档到远端。修改后需等待备份脚本下次读取配置。
</Typography.Paragraph>
</Card>
</div>
)
}
+22
View File
@@ -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<ApiResponse<AdminBackupStatus>>('/admin/backups/status')
return data.data
}
export async function fetchAdminBackupSchedule() {
const { data } = await adminClient.get<ApiResponse<AdminBackupSchedule>>(
'/admin/backups/schedule'
)
return data.data
}
export async function updateAdminBackupSchedule(payload: AdminBackupSchedule) {
const { data } = await adminClient.put<ApiResponse<AdminBackupSchedule>>(
'/admin/backups/schedule',
payload
)
return data.data
}
+21
View File
@@ -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
}
+2
View File
@@ -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<string, { title: string; description: string }> = {
},
'/system-configs': { title: '系统配置', description: '维护首页、发布、订单与支付配置。' },
'/payment-configs': { title: '支付配置', description: '维护主站支付商户、渠道与回调参数。' },
'/backups': { title: '备份管理', description: '查看备份健康状态并维护定时备份计划。' },
'/audit-logs': { title: '审计日志', description: '查看高风险操作与变更记录。' },
}