需要新增第 15 个设置 section backup,位于 /wp-admin/settings/backup。功能包括:定时自动备份(依赖 S3)、手动备份、备份文件列表/下载、从备份还原、手动上传还原。所有备份文件存储在 S3 的 backup/ 前缀下。
src/server/domains/settings/schema.ts — 追加 backupSchema:
export const backupSchema = z.object({
scheduled: z.object({
enabled: z.coerce.boolean(),
frequency: z.enum(['daily', 'weekly', 'monthly']).default('daily'),
hour: z.coerce.number().int().min(0).max(23).default(3),
minute: z.enum([0, 30] as const).default(0),
dayOfWeek: z.coerce.number().int().min(1).max(7).optional(), // 1=周一, 7=周日
dayOfMonth: z.coerce.number().int().min(1).max(28).optional(), // 1-28
}),
retention: z.object({
enabled: z.coerce.boolean().default(true),
days: z.coerce.number().int().min(1).max(365).default(30),
}),
})src/shared/config/blog.ts — 追加 BackupSettings 接口,在 BlogSettingsBundle 末尾加 backup: BackupSettings | null。
src/shared/config/settings.ts:
SETTINGS_SECTIONS末尾追加'backup'SECTION_TO_BUNDLE_KEY追加backup: 'backup'SECTION_DISPLAY追加:backup: { to: '/wp-admin/settings/backup', label: '备份与还原', description: '数据库自动备份、手动备份与还原' }
src/server/domains/settings/sections.ts:
- 导入
backupSchema SECTION_REGISTRY追加:backup: { scope: 'blog.backup', schema: backupSchema, key: 'backup', defaults: { scheduled: { enabled: false, frequency: 'daily', hour: 3, minute: 0 }, retention: { enabled: true, days: 30 }, }, }
src/server/domains/settings/snapshot.ts — PROBES 追加:
backup: (value) =>
typeof value.scheduled === 'object' && value.scheduled !== null &&
typeof (value.scheduled as Record<string, unknown>).enabled === 'boolean' &&
typeof value.retention === 'object' && value.retention !== null &&
typeof (value.retention as Record<string, unknown>).enabled === 'boolean',src/shared/contracts/settings.ts — blogSettingsBundleDto 追加 backup: sectionPayload。
src/routes.ts — settings layout 内追加 route('wp-admin/settings/backup', 'routes/wp-admin/settings/backup.tsx')。
src/server/domains/settings/service.ts — 无需修改(backup 无 secret 字段,走默认透传)。
还原后需要重启 Hono server,但不使用 process.exit(0)(依赖外部进程管理器),而是采用 Node.js cluster 模块 实现 nginx 风格的主进程 + 工作进程模型:
- 主进程(primary)永不退出,负责管理 worker 生命周期
- 工作进程(worker)运行实际的 Hono HTTP server
- 还原完成后,worker 通过 IPC 通知 primary 重启自己
- primary 杀掉旧 worker → fork 新 worker → 无缝恢复服务
在文件顶部加入 cluster 判断:
import cluster from 'node:cluster'
// Master-worker process model for graceful restart during DB restore.
// Primary process never exits; workers run the actual HTTP server.
// When a backup restore completes, the worker sends an IPC "restart"
// message and the primary forks a fresh worker.
if (cluster.isPrimary) {
const log = getLogger('cluster')
let worker = cluster.fork()
cluster.on('exit', (w, code, signal) => {
if (w.exitedAfterDisconnect) return // intentional kill
log.error('Worker crashed, restarting', { code, signal })
worker = cluster.fork()
})
// Listen for restart requests from the worker (e.g. after DB restore).
const handleMessage = (msg: unknown) => {
if (msg === 'restart') {
log.info('Restart requested — killing worker')
worker.kill()
worker = cluster.fork()
worker.on('message', handleMessage)
}
}
worker.on('message', handleMessage)
log.info('Primary started', { pid: process.pid, workerPid: worker.process.pid })
// Prevent primary from exiting
} else {
// ─── Worker: existing Hono server code ───
// (the rest of the current server.ts code stays here, unchanged)
}导出辅助函数供 restore 流程调用:
// In a shared module (e.g. src/server/infra/cluster.ts)
export function requestRestart(): void {
if (process.send) {
process.send('restart')
}
}src/server/domains/images/s3-client.ts — 追加 listS3Objects(prefix) 函数:
- 使用
ListObjectsV2Command分页列举 - 返回
{ key, size, lastModified }[]
新文件 src/server/domains/backup/backup-service.ts:
| 函数 | 职责 |
|---|---|
createBackup() |
execFile('pg_dump') → gzip → putImageObject({ key: 'backup/YYYY-MM-DD_HH-mm-ss.sql.gz', ... }) |
listBackups() |
listS3Objects('backup/') → 过滤排序返回 |
getBackupFile(key) |
getImageObject(key) → 返回 Buffer |
cleanupOldBackups(days) |
列举备份 → 删除超龄文件 |
restoreFromBackup(buffer) |
gunzip → execFile('psql', [DATABASE_URL], { input }) → requestRestart() |
pg_dump 参数:--no-owner --no-acl --clean --if-exists,输出 plain SQL 经 gzip 压缩后上传 S3。
还原完成后调用 requestRestart() 发送 IPC 消息给 primary,由 primary 杀掉当前 worker 并 fork 新的。
新文件 src/server/domains/backup/scheduler.ts:
- 遵循 analytics batcher 的
setTimeout+.unref()模式 - 读取
getBlogSettingsBundleSync().backup获取调度配置 - 根据频率/时间计算下次执行时刻,设定定时器
- 执行后自动清理超龄备份,然后重新调度
- 调度器在 S3 未开启或 scheduled.enabled=false 时以 5 分钟轮询检查配置变化
启动入口:在 src/server.ts 的 worker 分支中导入并调用 startBackupScheduler()(settings hydration 之后)。
新文件 src/server/http/controllers/admin/backup.controller.ts:
| Procedure | Method | Path | 职责 |
|---|---|---|---|
create |
POST | /admin/backup/create |
触发手动备份 |
list |
GET | /admin/backup/list |
列举 S3 备份文件 |
restore |
POST | /admin/backup/restore |
从指定 key 还原 |
全部使用 adminProc,create/restore 前置检查 S3 enabled。
src/server/http/api-router.ts — apiRouter.admin 追加 backup: adminBackupRouter。
新文件 src/server/http/resources/backup-download.ts:
- Hono route
GET /api/admin/backup/download/:key{.+} - 校验 admin session →
getBackupFile(key)→ 设置 Content-Type/Content-Disposition →c.body(buffer)
新文件 src/server/http/resources/backup-upload.ts:
- Hono route
POST /api/admin/backup/upload-restore - 校验 admin session → 解析 multipart → 读取 Buffer →
restoreFromBackup(buffer)
两个 resource route 在 src/server.ts 中注册。
新文件 src/routes/wp-admin/settings/backup.tsx:
- loader:requireRole('admin') → 检查
bundle.assets.storage.enabled→ 若 S3 已开则listBackups()→ 返回{ s3Enabled, backups } - component:读取
bundle.backup+ loaderData → 渲染<BackupView />
新文件 src/ui/admin/settings/BackupView.tsx:
分为四个区块:
- S3 未配置提示(s3Enabled=false 时)— 带跳转链接到 assets 设置页
- 定时备份配置 — 使用
useSettingsForm管理 scheduled/retention 表单状态:- 开关:
scheduled.enabled - 频率选择:daily / weekly / monthly
- 时间选择:hour (0-23) + minute (0 或 30)
- 周频率:星期选择 (1-7)
- 月频率:日期选择 (1-28)
- 保留策略开关 + 天数
- 底部
SettingsFormBar
- 开关:
- 备份文件列表 — 表格显示文件名/大小/时间,每行有下载和还原按钮:
- 左上角"手动备份"按钮(
useMutation调orpc.admin.backup.create) - 还原按钮弹出确认对话框(参考 cache 的
ConfirmClearDialog)
- 左上角"手动备份"按钮(
- 手动还原 — 文件上传 input,上传后调用 upload-restore endpoint
| 文件 | 操作 |
|---|---|
src/server/domains/settings/schema.ts |
修改:追加 backupSchema |
src/shared/config/blog.ts |
修改:追加 BackupSettings + 扩展 BlogSettingsBundle |
src/shared/config/settings.ts |
修改:追加 section 注册 |
src/shared/contracts/settings.ts |
修改:追加 backup 到 DTO |
src/server/domains/settings/sections.ts |
修改:追加 registry entry |
src/server/domains/settings/snapshot.ts |
修改:追加 probe |
src/server/domains/images/s3-client.ts |
修改:追加 listS3Objects |
src/server/http/api-router.ts |
修改:注册 backup router |
src/routes.ts |
修改:追加 backup route |
src/server.ts |
修改:cluster master-worker 包装 + 资源路由注册 |
src/server/infra/cluster.ts |
新建:requestRestart() IPC 辅助 |
src/server/domains/backup/backup-service.ts |
新建 |
src/server/domains/backup/scheduler.ts |
新建 |
src/server/http/controllers/admin/backup.controller.ts |
新建 |
src/server/http/resources/backup-download.ts |
新建 |
src/server/http/resources/backup-upload.ts |
新建 |
src/routes/wp-admin/settings/backup.tsx |
新建 |
src/ui/admin/settings/BackupView.tsx |
新建 |
vp check通过vp test run通过vp build成功/wp-admin/settings/backup页面正常渲染- S3 未开启时显示配置提示
- S3 开启后可配置定时备份、查看备份列表、手动创建备份