244 lines
9.6 KiB
Markdown
244 lines
9.6 KiB
Markdown
# 迁移方案:废弃云端 `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` | 完全重写<br>• 保留:`POST /clone` / `GET /tasks/{id}` / `GET /clone/stream` / `POST /tasks/{id}/retry` / `DELETE /{id}` <br>• 删除:`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<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 从本地读取 |
|
||
| **创建克隆** | 流程变化:<br>1. 前端生成 `avatar_id`<br>2. `POST /avatar/clone` → 获取 `task_id`<br>3. 前端创建本地目录 + 写入初始 `meta.json` (`status=pending`)<br>4. SSE 监听进度<br>5. 完成后 → 前端把 `voice_id`/`element_id` 写入本地 `meta.json`<br>6. 完成 |
|
||
| **删除 Avatar** | 流程变化:<br>1. 前端调用 `DELETE /avatar/{avatar_id}`(后端负责删除 Kling 远程资源)<br>2. 后端记删除日志到接口日志<br>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*
|