# 迁移方案:废弃云端 `mjk_avatars` 表,数字人元数据全量迁移到本地存储 > **状态:方案已调整(2026-04-17)** — 原方案中提到的 Celery 架构已完全移除,形象克隆现由 `app/scheduler/handlers/avatar_handler.py`(Async Engine Scheduler)统一调度。云端仍保留 `avatars` 表作为形象克隆的持久化记录。 ## 方案目标 | 目标 | 说明 | |------|------| | ✅ 贯彻设计理念 | 真正做到**轻量云 + 全本地业务数据**,云端只记日志不存业务数据 | | ✅ 统一接口日志 | 所有接口请求统一记录到 `mjk_interface_request_logs`,按接口统计积分消耗 | | ✅ 简化后端代码 | 删除大量 CRUD、状态管理、定时任务代码,后端更干净 | | ✅ 用户掌控数据 | 所有数字人元数据存在用户本地,云端只记克隆请求的消耗积分 | --- ## 存储结构变化 ### 变化前(现状) ``` 云端 PostgreSQL: mjk_avatars └─ 存储所有数字人元数据 (name/voice_id/element_id/status 等) 前端本地: └─ 只做缓存,从云端同步 ``` ### 变化后(目标) ``` 云端 PostgreSQL: ├─ mjk_interface_request_logs ← 只记:avatar_clone 请求 + 消耗积分 + 状态 └─ mjk_avatars ← 废弃,不再写入新数据(存量可保留可删除) 用户本地磁盘: └─ ~/Documents/Meijiaka/avatars/{avatar_id}/ ├─ meta.json ← 完整数字人元数据(JSON) └─ source.mp4 ← 原始上传视频 ``` --- ## 本地存储结构定义 ### 目录结构 ``` ~/Documents/Meijiaka/ └── avatars/ └── {avatar_id}/ # avatar_id = avt_{16位随机hex} ├── meta.json # 元数据(JSON 格式) └── source.mp4 # 原始上传视频 ``` ### `meta.json` 结构 ```json { "id": "avt_xxxxxxxxxxxxxxxx", "name": "我的数字人", "voiceId": "klingai-voice-id-string", "elementId": 12345678, "voiceTaskId": "kling-task-id-string", "elementTaskId": "kling-task-id-string", "videoUrl": "https://domain.com/path/to/source.mp4", "trialUrl": "https://domain.com/path/to/trial.wav", "status": "succeed", "failReason": null, "createdAt": "2026-04-16T10:00:00.000Z", "updatedAt": "2026-04-16T10:05:00.000Z" } ``` --- ## 代码改动清单 ### 后端 Python | 操作 | 文件 | 改动说明 | |------|------|----------| | 🆕 新增 | `app/models/interface_request_logs.py` | SQLAlchemy 模型 `InterfaceRequestLogs` | | 🆕 新增 | `app/crud/interface_request_logs.py` | CRUD:create / update | | ✏️ 修改 | `app/models/__init__.py` | 删除 `Avatar` 导入,新增 `InterfaceRequestLogs` | | ✏️ 修改 | `app/api/v1/avatar.py` | 完全重写
• 保留:`POST /clone` / `GET /tasks/{id}` / `GET /clone/stream` / `POST /tasks/{id}/retry` / `DELETE /{id}`
• 删除:`GET /library` / `PATCH /{id}` / `/health` | | ✏️ 修改 | `app/scheduler/handlers/avatar_handler.py` | 精简:删除所有对 `mjk_avatars` 读写,只记接口日志,进度放 Redis Registry | | ❌ 删除 | `app/models/avatar.py` | 模型废弃,删除 | | ❌ 删除 | `app/crud/avatar.py` | CRUD 废弃,删除 | | ❌ 删除 | `app/tasks/avatar_clone.py` | 逻辑已合并到 avatar_handler,删除 | ### Rust Tauri(`tauri-app/src-tauri/src/persistence.rs`) 新增以下 IPC 命令: ```rust /// 列出所有本地数字人(按创建时间倒序) #[tauri::command] pub fn list_avatars(app: AppHandle) -> Result, String>; /// 保存数字人元数据 #[tauri::command] pub fn save_avatar(app: AppHandle, avatar_id: String, meta: AvatarMeta) -> Result<(), String>; /// 获取单个数字人元数据 #[tauri::command] pub fn get_avatar(app: AppHandle, avatar_id: String) -> Result, String>; /// 删除数字人(删除整个本地目录) #[tauri::command] pub fn delete_avatar(app: AppHandle, avatar_id: String) -> Result<(), String>; /// 更新数字人名称 #[tauri::command] pub fn update_avatar_name(app: AppHandle, avatar_id: String, name: String) -> Result<(), String>; ``` 在 `lib.rs` 注册新命令。 ### 前端 TypeScript | 模块 | 改动 | |------|------| | **Avatar 列表** | 原:`GET /avatar/library` 从后端获取 → 现在:调用 Tauri IPC 从本地读取 | | **创建克隆** | 流程变化:
1. 前端生成 `avatar_id`
2. `POST /avatar/clone` → 获取 `task_id`
3. 前端创建本地目录 + 写入初始 `meta.json` (`status=pending`)
4. SSE 监听进度
5. 完成后 → 前端把 `voice_id`/`element_id` 写入本地 `meta.json`
6. 完成 | | **删除 Avatar** | 流程变化:
1. 前端调用 `DELETE /avatar/{avatar_id}`(后端负责删除 Kling 远程资源)
2. 后端记删除日志到接口日志
3. 前端调用 IPC 删除本地目录 | | **重命名 Avatar** | 原:调用后端 PATCH → 现在:前端直接修改本地 `meta.json`,无需请求后端 | | **选择数字人生成视频** | 用法不变:从本地读取 `voice_id`/`element_id` → 传给后端视频生成接口 | --- ## 工作流对比 ### 改动前(当前) ``` 用户提交克隆 → POST /clone → 后端写 mjk_avatars (status=pending) → 派发任务 → Async Engine Scheduler (avatar_handler) 每一步都更新 `avatars` 表 → 前端 SSE 轮询读 `avatars` 表拿进度 → 完成后 Scheduler 更新 status=succeed 写入 voice_id/element_id → 前端从 `avatars` 表读结果 → 缓存到本地 → 列表从 `avatars` 表读取 ``` ### 改动后(目标) ``` 用户提交克隆 → 前端生成 avatar_id → 创建本地 meta.json (status=pending) → POST /clone → 后端: 1. 在 mjk_interface_request_logs 插入记录 interface_type=avatar_clone, status=pending, started_at=now, cost_credits=X 2. 注册到 Async Engine Scheduler (Redis Registry) 3. 返回 {task_id, avatar_id} → Async Engine Scheduler (avatar_handler) 执行: 1. 调用 Kling 创建音色 → 轮询 → 获取 voice_id 2. 调用 Kling 创建主体 → 轮询 → 获取 element_id 3. 更新 Redis Registry 状态为 completed,写入结果 4. 更新接口日志: status=success/failed, finished_at=now → 前端 SSE 从 TaskCache 获取结果 → 完成后前端将 voice_id/element_id 写入本地 meta.json → 列表展示直接从本地读取,不请求后端 ``` --- ## 接口日志记录规则 `mjk_interface_request_logs` 对 `avatar_clone` 的记录: | 时机 | 操作 | 字段值 | |------|------|--------| | 刚收到请求 | 插入新记录 | `interface_type=avatar_clone`, `status=pending`, `started_at=NOW`, `cost_credits` = 克隆一次所需积分 | | 任务完成成功 | 更新记录 | `status=success`, `finished_at=NOW` | | 任务失败 | 更新记录 | `status=failed`, `finished_at=NOW`, `error_message=错误原因` | > 积分在请求创建时即扣除,因为无论成功失败,KlingAI 开始处理后会计费。 --- ## 存量数据迁移策略 ### 渐进迁移(对用户友好) 1. **保留云端表**:`mjk_avatars` 保留不删除,存量数据继续存在 2. **前端自动迁移**:用户首次打开形象库时: - 前端检查:如果后端有数据但本地没有 → 提示用户"将云端数字人同步到本地" - 用户确认后,前端逐个拉取数据写入本地 - 同步完成后,后续只使用本地数据 3. **下线旧表**:稳定运行一段时间后,可在维护窗口物理删除 `mjk_avatars` 表 ### 回滚方案 - 迁移过程中如果出问题,随时切回原逻辑(表保留,代码只需恢复删除部分) --- ## 优缺点总结 | 优点 | 说明 | |------|------| | ✅ 完全符合需求 | 云端只存接口请求记录和消耗积分,不存用户业务数据 | | ✅ 云端存储成本极低 | 只有接口日志,每条几KB,用户增长成本可控 | | ✅ 后端代码大幅简化 | 删除了整个 Avatar CRUD、状态机管理、定时任务恢复,代码减少约 300 行 | | ✅ 用户完全掌控数据 | 所有数字人元数据存储在用户本地磁盘 | | ✅ 形象库展示更快 | 本地读取文件比查询数据库快很多 | | ✅ 兼容存量数据 | 渐进迁移,可回滚 | | 缺点 | 说明 | 应对 | |------|------|------| | 用户换电脑需要迁移 | 用户需要自行迁移数据,或重新克隆 | 后续可增加导出/导入功能解决 | | 本地硬盘损坏数据丢失 | 这是"全本地"设计的必然结果 | 符合项目初始"轻量云+全本地"设计理念,用户自担数据安全 | --- ## 执行步骤(按顺序) 1. **数据库** - 生成 Alembic 迁移:所有表重命名加 `mjk_` 前缀 + 新建 `mjk_interface_request_logs` - 修改所有 Python 模型中的 `__tablename__` 2. **后端代码** - 新建 `interface_request_logs` 模型和 CRUD - 重写 `app/api/v1/avatar.py` - 精简 `app/tasks/avatar_tasks.py` - 删除废弃文件 3. **Rust Tauri** - 在 `persistence.rs` 新增 avatar 相关 IPC 命令 - 在 `lib.rs` 注册命令 4. **前端代码** - 修改形象库:从本地读取 - 修改创建流程:完成后写入本地 - 修改删除流程:删除云端 Kling 资源后删除本地 - 修改重命名:直接本地修改 5. **测试验证** - 创建克隆 → 检查本地文件生成 → 检查接口日志写入 - 列表展示 → 删除 → 重命名 全流程测试 --- ## 相关文档 - [数据库设计规范](./database-design.md) - 完整的数据库命名规范和表结构 - [视频生成流程](./video-generation-flow.md) - 完整视频生成流程说明 --- *版本:v1.0* *创建日期:2026-04-16*