# 数据备份功能 状态:已定版(2026-09-06 实施,2026-09-07 定版,契约 1.22.0;2026-09-09 数据库导出/恢复改为自有格式数据脚本,零外部工具,接口形状不变) 实施摘要: - 后端(Java `com.dms.backup` / Python `dms/services/backup_service.py` 双版对齐):链式全量+增量备份引擎(存储扫描→数据库数据脚本导出→差异拷贝→rsync 推送→链清理)、JSON 配置存储(运行目录 `dms-backup-config.json`,snake_case,双端互读互通)、定时触发器、备份历史与实时进度快照。 - 接口(契约 1.22.0 定版,OpenAPI `paths/backup.yaml`):`GET/PUT /api/v1/backup/config`、`POST /api/v1/backup/jobs`(202 受理)、`GET /api/v1/backup/jobs/current`、`POST /api/v1/backup/jobs/current/cancel`、`GET /api/v1/backup/jobs`;仅 ADMIN 可用;审计动作 `BACKUP_CREATE`(target=`BACKUP`,字典已含)。 - 恢复:离线 CLI——Python `dms-server restore-backup [--backup-id dms-bak-xxx] [--yes]`,Java `RestoreBackupCommand`(`--restore-backup`);存储文件按清单恢复、数据库脚本驱动直连导入(禁用外键→清空业务表→导入→恢复外键)与 `.env` 副本落地提示(`.env` 存为 `.env.restored`,不覆盖当前配置)。 - 前端:`features/backup/`(类型守卫 `backupTypes`、`backupApi`、`BackupDashboard`)+ 管理员专属本地页签「备份管理」(配置表单、全量/增量触发、取消确认、2 秒进度轮询、历史清单表)。 - 验证:Python pytest 289 通过 / 470 跳过(含 OpenAPI 守卫 1.22.0 与 4 个备份路径白名单);前端 tsc 通过、vitest 424 项全绿、vite 构建成功;Java mvn compile/test 通过。 ## 1. 需求 - 应用内备份:管理界面按钮**手动触发** + 每日**定时自动**; - **全部备份**:达梦数据库(自有格式数据脚本全量导出)、全部存储目录(原件/预览/正文/隔离/回收站/授权凭证)、`.env` 配置副本; - 拷贝到**异机**:不走挂载网盘,使用**专用协议 rsync 守护进程**(`rsync://user@host:873/module/`);本机目录模式作为开发/单机回退; - **全量与增量**(链式):数据量在几十 GB 总量级别,增量基于最近全量做差异传输; - 保留策略:保留 N 条完整备份链,超出从最旧链清理;定时任务距最近全量超过 N 天自动升级为全量; - 配置持久化为 JSON 文件(不进数据库),双后端共享同一份。 ## 2. 关键设计 ### 2.1 备份链与清单(manifest) - 备份 ID:`dms-bak-yyyyMMdd-HHmmss-F`(全量)/ `dms-bak-yyyyMMdd-HHmmss-I`(增量); - 每次备份为目标根下独立目录 + `manifest.json`;目标根维护 `index.json`(全部备份索引); - 全量 manifest 的 `files` 为存储全量清单;增量 manifest 的 `files` 为**合并到链尾的累计清单**(= 前驱清单 + 本次差异),恢复任一链尾即可得到完整文件视图; - `manifest.json` **最后原子写入**——目标端存在 manifest 才视该备份完成,中断的残缺目录在历史/恢复中不可见; - 增量差异判定:文件相对路径 + 大小 + mtime,与前驱清单比对,仅传输差异文件(几十 GB 量级下增量通常为 MB 级)。 ### 2.2 备份内容与阶段 阶段(`BackupPhase`):`SCANNING`(扫描存储)→ `EXPORTING_DB`(数据脚本导出)→ `COPYING`(拷贝文件)→ `PUSHING`(推送目标,仅 RSYNC)→ `CLEANING`(清理旧链)→ `SUCCESS` / `FAILED`。 - 数据库:自有格式数据脚本 `database/dms-data.sql`(驱动直连导出,零外部工具;实现于 Python `sql_dump` / Java `SqlDataDumper`,详见 2.6); - 存储:六个子目录全部纳入(含预览缓存与授权凭证); - `.env`:随包备份一份副本; - 进度快照 12 字段(backupId/type/trigger/phase/bytesTotal/bytesCopied/filesTotal/filesCopied/pushPercent/error/startedAt/finishedAt),双端形状逐字对齐(从未运行时业务字段为 null/0);时间为**业务时区本地时间** `yyyy-MM-dd HH:mm:ss`(非 UTC)。 ### 2.3 目标模式 - `LOCAL`:直接写入本机目录(开发/单机); - `RSYNC`:组包到本地临时目录后 `rsync` 推送异机(rsync 守护进程协议),整体进度取 rsync 输出百分比(`pushPercent`);密码存独立文件不回显(接口仅返回 `rsync_password_set` 布尔); - 目标不可达报 `BACKUP_TARGET_UNAVAILABLE`(HTTP 503);已有任务在跑报 `BACKUP_RUNNING`(HTTP 409)。 ### 2.4 配置存储与定时 - `dms-backup-config.json`(运行目录,snake_case):`target_mode / local_path / rsync_url / schedule_enabled / schedule_time / full_interval_days / retain_full_count` + 密码独立保存;保存时全量替换,`rsync_password` 为 null/空串表示保持已存密码; - 定时:Java `@Scheduled(fixedDelay=30s)` / Python 后台线程轮询,到达 `schedule_time`(业务时区 HH:MM)且当日未触发时启动;类型决策 `decideScheduledType`:距最近全量 ≥ `full_interval_days` 天(或无可用链)→ `FULL`,否则 `INCREMENTAL`; - 清理:完成新备份后,若完整链数 > `retain_full_count`,从最旧链开始整链删除(链 = 全量 + 其后所有增量)。 ### 2.5 恢复(离线 CLI) - Python:`dms-server restore-backup [--backup-id dms-bak-xxx] [--yes]`;Java:`RestoreBackupCommand`; - 从目标端拉取指定备份(缺省取最新链尾)的 manifest 与文件,按清单恢复存储文件;数据库脚本由程序驱动直连导入(见 2.6 恢复语义);`.env` 副本落为 `.env.restored`,不覆盖当前 `.env`(防止恢复机数据库地址不同导致服务起不来)。 ### 2.6 数据库自有格式(dms-data.sql) 背景:达梦客户端工具 dexp/dimp 不随环境安装导致备份失败,且外部子进程难以诊断。改为**自有格式数据脚本**(参考 MySQL→DM 迁移工作区 `temp/sql` 的导入器与拓扑排序脚本设计): - **格式**:`database/dms-data.sql` UTF-8 纯数据 SQL 脚本(无 DDL),头部注释元信息(格式版本/生成引擎/时间/源模式/表数量),逐表块 = 行数注释 + 可选 `SET IDENTITY_INSERT` 包裹 + 带列清单 INSERT(列清单保证备份旧/程序新时新列取默认值);`manifest.database_file` 字段值同步变更,接口形状不变; - **导出**(Java `SqlDataDumper` / Python `sql_dump`,驱动直连零外部工具):表清单来自 `ALL_TABLES`(含 alembic_version / flyway_schema_history 迁移簿记表,版本戳随备份走);外键拓扑序(Kahn,父表在前、同层字典序、环兜底字母序);单列主键表按 PK 升序取行(自引用表父行必先于子行);IDENTITY 以 SET ON/OFF 探测决定是否包裹;字面量渲染覆盖 NULL/布尔/数值/二进制(0x hex)/datetime(微秒)/date/字符串(单引号转义); - **恢复**:禁用全部外键(DDL 隐式提交,置于业务事务之外)→ 字典序逆序清空全部业务表 → **单事务**执行脚本全部语句 → finally 恢复(启用)外键——启用即完整性校验,失败清单非空报错;相比 dimp IGNORE=Y 的"覆盖不删多余",精确恢复到备份时点; - **版本错位守卫**:备份中的表缺失于目标库(备份新于程序)报错引导升级;目标库多出的表(程序新于备份)不动;目标库为空时 Python 版自动执行 alembic 建库(`run_upgrade_silent`),Java 版报错引导先建库(Java 无完整 DM 迁移链); - **双端互通**:两侧同一 SQL、同一格式、同一恢复语义,任一端导出的脚本另一端可导入。 ## 3. 接口(契约 1.21.0) | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/api/v1/backup/config` | 读取配置视图(8 字段,密码永不回显) | | PUT | `/api/v1/backup/config` | 保存配置(全量替换;`rsync_password` null=保持) | | POST | `/api/v1/backup/jobs` | 触发备份 `{type: FULL\|INCREMENTAL}`,202 + 初始进度快照 | | GET | `/api/v1/backup/jobs/current` | 当前进度快照 + `running` 布尔 | | POST | `/api/v1/backup/jobs/current/cancel` | 请求取消,返回 `{canceled}` | | GET | `/api/v1/backup/jobs` | 历史清单 `{entries}`(按备份 ID 升序,含链尾累计口径的清单统计) | 权限:全部仅 ADMIN;触发成功审计 `BACKUP_CREATE`;查询/进度/取消不产生审计。 ## 4. 前端 - 导航:管理员专属本地页签「备份管理」(`NavigationItem.module = "BACKUP"` 本地锚点,不走 `allowedModules` 下发;普通用户不可见,直接访问渲染「模块权限异常」); - `BackupDashboard` 三区:任务区(全量/增量触发、取消确认弹窗、进度条与阶段/文件/字节统计、失败原因)、配置区(模式/路径/rsync 地址与密码/定时开关与时刻/全量间隔/保留链数)、历史区(ID/类型/前驱/起止时间/本次传输/清单累计/数据库导出大小); - 轮询:页面存活期间 2 秒拉取当前任务;`running` 由真变假时自动刷新历史; - 契约守卫:`backupTypes.ts` 按实际 JSON 形状逐字段校验(配置/历史 snake_case,进度 camelCase)。 ## 5. 验证记录 - Python:`pytest`(干净 dms_test 库)289 通过 / 470 跳过(2026-09-07 定版复验,含修复 dmPython 驱动环境后恢复的 20 项),含 `test_backup.py`(引擎级 7 项真实执行:全量→增量链路、目标目录布局、链尾累计清单、无链升级全量、dexp 缺失 FAILED、单任务互斥、保留链清理与残留清扫、配置 JSON 往返与校验、进度快照形状;接口级 7 项沿用 B2 门控,提供 `DMS_TEST_*` 环境变量时执行);`test_openapi_storage`(1.22.0 版本、4 个备份路径白名单、Flask 路由与 OpenAPI 方法集精确一致)及 q1b/q2b/q3b 契约版本守卫; - Java:`mvn compile` / `mvn test` 通过(既有测试无回归); - 前端:`tsc --noEmit` 通过;`vitest run` 66 文件 424 项全部通过;`vite build` 成功; - 双端互通:同一 `dms-backup-config.json` 与备份目录结构,Java 版与 Python 版互为可恢复(备份链清单格式一致)。 ### 5.1 变更记录(2026-09-09:自有格式替代 dexp/dimp) - 动因:生产环境未安装达梦客户端工具(dexp/dimp),备份触发即失败"未找到达梦导出工具 dexp";应用本身经 JDBC/dmPython 驱动直连数据库,外部工具并非必需依赖。 - 变更:删除 Java `DmBackupTool`(dexp/dimp 子进程封装)与 Python 端同类封装;新增 Java `SqlDataDumper`、Python `sql_dump` 模块(双版逐条对齐);`BackupEngine` / `RestoreBackupCommand` / `backup_service` 全部改走驱动直连;manifest `database_file` 值 `database/dms.dmp` → `database/dms-data.sql`(字段与接口形状不变,无需升契约版本)。 - 验证:Python `test_sql_dump.py` 新增 19 项(切分/字面量/拓扑/导出块/恢复序列/空库与缺表守卫/外键失败回补),`test_backup.py` 改造为伪导出全绿,全套 317 通过 / 501 跳过;Java `SqlDataDumperTest` 新增 11 项(对齐 Python 用例),全套 49 项通过;`mvn compile` 通过。 - 兼容:历史备份包中的 `database/dms.dmp` 不再被恢复命令识别(恢复 CLI 引导重新生成备份);`DMS_DEXP_EXECUTABLE`/`DMS_DM_HOME` 相关配置与探测逻辑一并移除(.env 未曾默认携带)。