rebuild(stage-14): deliver migration and recovery tooling

This commit is contained in:
leefer
2026-07-30 08:29:37 +08:00
parent 9eb7b548f3
commit fa7a8dde06
17 changed files with 1397 additions and 3 deletions
+65
View File
@@ -0,0 +1,65 @@
# 新系统部署与回退
本说明只适用于 `next/` 重建版。旧容器在最终人工确认前继续运行,不得原地覆盖。
## 首次准备
1.`.env.example` 复制为 `.env`,写入固定的 Fernet `APP_ENCRYPTION_KEY`。该密钥必须与迁移旧数据时使用的密钥一致。
2. 创建 `data/private-mentor-skills/`,把不公开的 Skill 放在该持久化目录;镜像内只含公开 Skill。
3. 先在独立目录运行迁移,并查看报告中的迁移数、主动跳过项、完整性和校验和。
4. 使用尚未占用的端口启动新容器并完成验收。不要停止旧容器。
```powershell
$env:APP_ENCRYPTION_KEY='<现有固定密钥>'
python -m tools.legacy_migration `
--source ..\data\review.db `
--target .\data\xiaobai.db `
--report .\data\migration-report.json
docker compose config
docker compose build
docker compose up -d
```
`legacy_migration` 以只读 URI 打开旧库,可重复执行;会保留用户编号和密码散列,不迁移会话。旧的每用户 LLM 配置和可由正式数据源重建的原始因子表会明确记录为主动跳过,不会进入新运行时。
## 备份
SQLite 必须通过在线备份 API 取得一致快照,不能在运行中直接复制数据库文件。生产备份应把数据库、环境/密钥文件与私有 Skill 放入同一归档,并把归档存放在容器数据卷之外。
```powershell
python -m tools.backup `
--database .\data\xiaobai.db `
--output D:\backups\xiaobai-2026-07-30.zip `
--private-skills .\data\private-mentor-skills `
--environment-file .\.env `
--apply-retention
```
保留策略至少为最近 7 份日备份与 4 个不同周的周备份。密钥随归档备份意味着归档自身必须保存到受控、加密的 NAS 位置。
## 恢复演练
恢复默认拒绝覆盖,且在写入前校验清单、逐文件 SHA-256、路径安全、SQLite 完整性。
```powershell
python -m tools.restore `
--archive D:\backups\xiaobai-2026-07-30.zip `
--database .\data\restore-drill.db `
--private-skills .\data\restore-private-skills `
--environment-file .\data\restore.env `
--confirm-restore
```
恢复后必须抽验:原账号登录、共享行情快照、自选与复盘、会员状态、模型配置、问天历史、选股历史和跟踪记录。
## 升级与回退
1. 升级前生成一致备份,记录当前镜像标签与数据库 migration 版本。
2. 用新镜像启动独立验收容器;健康检查通过后再切换反向代理或宿主端口。
3. 代码回退只切换至旧镜像,不自动降级数据库。
4. 若新 migration 与旧镜像不兼容,使用升级前的整套备份恢复到独立目录,再启动旧镜像。
5. NAS 正式切换属于单独人工确认项,本仓库脚本不会自动执行切换。
## 本机验证边界
开发机未安装 Docker 时仍可执行全部 Python、前端构建、迁移和备份恢复测试,但不能声称镜像已经实际构建或容器健康检查已经运行。最终切换前应在有 Docker 的 NAS 或预发布机补做 `docker compose build`、只读文件系统、数据卷写入和重启恢复验证。
+51
View File
@@ -0,0 +1,51 @@
# 阶段 14 验收
## 交付边界
- 旧系统只作为只读来源;真实演练源为 `data/review.db`,目标为新建的 `next/data/stage14-migrated.db`
- 迁移逻辑仅存在于一次性工具 `tools/legacy_migration.py`,正常应用不导入旧系统代码或旧数据库。
- 新 Docker、备份和恢复资产仅位于 `next/`,没有停止、修改或替换 NAS 正式容器。
## 真实迁移结果
- 源库 SHA-256`b7c444359c3b9b201a47ce6b23e55ad7ca4eba3d9dd67dfc8544b575d43f4924`
- SQLite `integrity_check=ok`,外键违规 0。
- 3 个账号与会员状态、1 份出生资料、2 项系统数据凭据、3 个模型配置均已转换。
- 6 条自选、3 条复盘、2 条提醒、28 条问师消息、45 条问师偏好、31 条问天历史均已迁移并保留账号归属。
- 196 次历史选股与 16 条手动策略跟踪已迁移;每个历史交易日使用明确标注的归档因子快照,不伪造旧因子覆盖率。
- 5,534 个股票目录、261 个交易日、24 份市场摘要、5,567 份最近 90 根日 K 展示归档和 24 份兼容市场洞察已迁移。
- 真实账号密码验证、管理员与永久会员状态、模型选择、问天历史和选股历史抽验通过。
- 逐项机器报告见 `real-migration-report.json`,报告不含明文令牌或模型密钥。
## 主动删除或重建
- 会话不迁移,切换后所有账号重新登录。
- 已取消的用户自主 LLM 配置不迁移。
- 旧原始因子表不复制成兼容表;由新系统受治理的数据同步和因子任务重建。
- 旧指数行仅有收盘价,缺少新图表契约要求的 OHLC,未伪造成指数 K 线。
- 日 K 展示归档每个标的保留最近 90 根;旧全量数据库和一致性备份继续作为审计资产保留。
## 备份恢复演练
- 使用 SQLite 在线备份 API 创建一致快照,归档同时包含环境密钥材料与 9 个私有 Skill 文件,共 11 个文件。
- 恢复前验证安全路径、逐文件 SHA-256 和 SQLite 完整性;默认拒绝覆盖已有数据库、环境、密钥和私有 Skill 目录。
- 恢复副本 `integrity_check=ok`,账号数 3、历史选股数 196,与迁移目标一致。
- 单元测试确认篡改数据库内容会因校验和不一致被拒绝。
## 生产与安全抽验
- Vite 生产资源由 FastAPI 同端口提供;SPA 深链接可返回入口文件,未知 `/api/*` 保持统一 JSON 404。
- 生产登录 Cookie 含 `HttpOnly``Secure``SameSite=Lax`;生产环境缺少加密密钥会拒绝启动。
- 真实迁移库副本的行情摘要读取 20 次:中位数 20.90msP95 23.55ms;日 K 返回 90 个点。
- 容器使用多阶段构建、非 root 用户、只读根文件系统、持久化数据卷、健康检查、移除 Linux capabilities 和 `no-new-privileges`
- 本机未安装 Docker,因此未声称镜像已实构建;NAS 切换前必须在有 Docker 的预发布环境补做构建、健康检查、只读根文件系统和重启恢复验证。
## 质量门
- Ruff:通过。
- pytest96 项通过。
- Vue 类型检查:通过。
- Vitest:3 个文件、7 项通过。
- Vite 生产构建:通过,CSS 89.25KBJS 279.23KB,均为构建前原始体积。
- Playwright17 项通过,单 worker,耗时 53.6 秒。
- `git diff --check` 与已知令牌/密码扫描:通过。
@@ -0,0 +1,51 @@
{
"source": "C:\\Users\\MoBai\\Documents\\gupiaofupan\\webapp\\data\\review.db",
"target": "C:\\Users\\MoBai\\Documents\\gupiaofupan\\webapp\\next\\data\\stage14-migrated.db",
"source_sha256": "b7c444359c3b9b201a47ce6b23e55ad7ca4eba3d9dd67dfc8544b575d43f4924",
"target_sha256": "9b0ca7faed7a15b4099bd09ae7a0c94b52a0c8a7dbfedb9d2b26bdf47e34d5d2",
"integrity": "ok",
"foreign_key_violations": 0,
"migrated": {
"alerts": 2,
"birth_profiles": 1,
"chart_series": 5567,
"heaven_readings": 31,
"llm_models": 3,
"llm_usage_daily": 9,
"market_entities": 5534,
"market_insight_snapshots": 24,
"market_summaries": 24,
"memberships": 3,
"mentor_messages": 28,
"mentor_preferences": 45,
"review_notes": 3,
"screener_runs": 196,
"strategy_tracks": 16,
"system_credentials": 2,
"trading_days": 261,
"users": 3,
"watchlist_entries": 6
},
"target_counts": {
"users": 3,
"memberships": 3,
"market_entities": 5537,
"market_summaries": 24,
"chart_series": 5567,
"watchlist_entries": 6,
"review_notes": 3,
"trade_entries": 0,
"alerts": 2,
"mentor_messages": 28,
"heaven_readings": 31,
"screener_runs": 196,
"custom_screener_strategies": 0,
"strategy_tracks": 16
},
"intentionally_skipped": {
"sessions": "sessions are intentionally invalidated during cutover",
"user_credentials": "per-user LLM configuration was removed from the product",
"raw_factor_tables": "reproducible provider inputs are rebuilt by governed sync jobs",
"benchmark_bars": "legacy rows lack OHLC values required by the chart contract"
}
}