9.6 KiB
9.6 KiB
迁移方案:废弃云端 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 结构
{
"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 命令:
/// 列出所有本地数字人(按创建时间倒序)
#[tauri::command]
pub fn list_avatars(app: AppHandle) -> Result<Vec<AvatarMeta>, 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<Option<AvatarMeta>, 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_id2. POST /avatar/clone → 获取 task_id3. 前端创建本地目录 + 写入初始 meta.json (status=pending)4. SSE 监听进度 5. 完成后 → 前端把 voice_id/element_id 写入本地 meta.json6. 完成 |
| 删除 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 开始处理后会计费。
存量数据迁移策略
渐进迁移(对用户友好)
- 保留云端表:
mjk_avatars保留不删除,存量数据继续存在 - 前端自动迁移:用户首次打开形象库时:
- 前端检查:如果后端有数据但本地没有 → 提示用户"将云端数字人同步到本地"
- 用户确认后,前端逐个拉取数据写入本地
- 同步完成后,后续只使用本地数据
- 下线旧表:稳定运行一段时间后,可在维护窗口物理删除
mjk_avatars表
回滚方案
- 迁移过程中如果出问题,随时切回原逻辑(表保留,代码只需恢复删除部分)
优缺点总结
| 优点 | 说明 |
|---|---|
| ✅ 完全符合需求 | 云端只存接口请求记录和消耗积分,不存用户业务数据 |
| ✅ 云端存储成本极低 | 只有接口日志,每条几KB,用户增长成本可控 |
| ✅ 后端代码大幅简化 | 删除了整个 Avatar CRUD、状态机管理、定时任务恢复,代码减少约 300 行 |
| ✅ 用户完全掌控数据 | 所有数字人元数据存储在用户本地磁盘 |
| ✅ 形象库展示更快 | 本地读取文件比查询数据库快很多 |
| ✅ 兼容存量数据 | 渐进迁移,可回滚 |
| 缺点 | 说明 | 应对 |
|---|---|---|
| 用户换电脑需要迁移 | 用户需要自行迁移数据,或重新克隆 | 后续可增加导出/导入功能解决 |
| 本地硬盘损坏数据丢失 | 这是"全本地"设计的必然结果 | 符合项目初始"轻量云+全本地"设计理念,用户自担数据安全 |
执行步骤(按顺序)
-
数据库
- 生成 Alembic 迁移:所有表重命名加
mjk_前缀 + 新建mjk_interface_request_logs - 修改所有 Python 模型中的
__tablename__
- 生成 Alembic 迁移:所有表重命名加
-
后端代码
- 新建
interface_request_logs模型和 CRUD - 重写
app/api/v1/avatar.py - 精简
app/tasks/avatar_tasks.py - 删除废弃文件
- 新建
-
Rust Tauri
- 在
persistence.rs新增 avatar 相关 IPC 命令 - 在
lib.rs注册命令
- 在
-
前端代码
- 修改形象库:从本地读取
- 修改创建流程:完成后写入本地
- 修改删除流程:删除云端 Kling 资源后删除本地
- 修改重命名:直接本地修改
-
测试验证
- 创建克隆 → 检查本地文件生成 → 检查接口日志写入
- 列表展示 → 删除 → 重命名 全流程测试
相关文档
版本:v1.0 创建日期:2026-04-16