docs: 新增用户认证、OSS 存储、异步任务设计
- database.md: 新增 user 表,project 关联 user_id,恢复 OSS 对象存储, 移除 60 分钟文件过期,新增 JWT/OSS 环境变量配置 - api.md: 新增认证章节和错误码表,补充用户注册/登录/修改密码接口, 工程管理新增列表/详情/删除接口,各接口补充错误响应说明 - async-tasks.md: 完善任务队列设计、Worker 流程、并发控制、管线进度、 质检重试与任务级重试策略、进度推送方式
This commit is contained in:
+250
-20
@@ -6,6 +6,39 @@
|
||||
{ "code": 0, "message": "ok", "data": {} }
|
||||
```
|
||||
|
||||
## 认证
|
||||
|
||||
除健康检查和注册登录外,所有接口需在请求头携带 Bearer Token:
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
Token 通过登录接口获取,过期时间由 `GEN2D_JWT_EXPIRE` 控制(默认 7200 秒)。Token 过期或无效时返回:
|
||||
|
||||
```json
|
||||
{ "code": 401, "message": "未登录或 Token 已过期", "data": null }
|
||||
```
|
||||
|
||||
权限不足时返回:
|
||||
|
||||
```json
|
||||
{ "code": 403, "message": "无权访问该资源", "data": null }
|
||||
```
|
||||
|
||||
## 错误码
|
||||
|
||||
| code | 含义 | 场景 |
|
||||
|------|------|------|
|
||||
| 0 | 成功 | — |
|
||||
| 400 | 请求参数错误 | 缺少必填字段、格式不合法 |
|
||||
| 401 | 未认证 | Token 缺失、过期、无效 |
|
||||
| 403 | 无权访问 | 访问不属于自己的资源 |
|
||||
| 404 | 资源不存在 | 工程/任务/素材 ID 不存在 |
|
||||
| 409 | 冲突 | 用户名或邮箱已注册 |
|
||||
| 429 | 请求过于频繁 | 触发速率限制 |
|
||||
| 500 | 服务器内部错误 | 未预期异常 |
|
||||
|
||||
## 状态说明
|
||||
|
||||
- [x] 已完成
|
||||
@@ -19,14 +52,157 @@
|
||||
|------|------|------|------|
|
||||
| [x] | GET | `/api/v1/health` | 健康检查 |
|
||||
|
||||
## 工程管理
|
||||
## 用户认证
|
||||
|
||||
| 状态 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| [ ] | POST | `/api/v1/auth/register` | 用户注册 |
|
||||
| [ ] | POST | `/api/v1/auth/login` | 用户登录 |
|
||||
| [ ] | GET | `/api/v1/auth/me` | 获取当前用户信息 |
|
||||
| [ ] | PUT | `/api/v1/auth/password` | 修改密码 |
|
||||
|
||||
### POST /api/v1/auth/register
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "player1",
|
||||
"email": "player1@example.com",
|
||||
"password": "s3cretP@ss"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `username` | string | 是 | 3-64 字符,字母数字下划线 |
|
||||
| `email` | string | 是 | 邮箱地址 |
|
||||
| `password` | string | 是 | 8-128 字符 |
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"id": "user_a1B2c3D4",
|
||||
"username": "player1",
|
||||
"email": "player1@example.com",
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"expiresAt": "2026-05-24T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/auth/login
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "player1",
|
||||
"password": "s3cretP@ss"
|
||||
}
|
||||
```
|
||||
|
||||
支持 `username` 或 `email` 登录:
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "player1@example.com",
|
||||
"password": "s3cretP@ss"
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"id": "user_a1B2c3D4",
|
||||
"username": "player1",
|
||||
"email": "player1@example.com",
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"expiresAt": "2026-05-24T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/v1/auth/me
|
||||
|
||||
需认证。响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"id": "user_a1B2c3D4",
|
||||
"username": "player1",
|
||||
"email": "player1@example.com",
|
||||
"createdAt": "2026-05-24T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PUT /api/v1/auth/password
|
||||
|
||||
需认证。
|
||||
|
||||
```json
|
||||
{
|
||||
"oldPassword": "s3cretP@ss",
|
||||
"newPassword": "n3wS3cretP@ss"
|
||||
}
|
||||
```
|
||||
|
||||
错误响应:
|
||||
|
||||
| 场景 | code | message |
|
||||
|------|------|---------|
|
||||
| 原密码错误 | 400 | 原密码不正确 |
|
||||
| 用户名已存在 | 409 | 用户名已被注册 |
|
||||
| 邮箱已存在 | 409 | 邮箱已被注册 |
|
||||
|
||||
## 工程管理
|
||||
|
||||
需认证。以下接口均需携带 Bearer Token,工程归属于当前登录用户。
|
||||
|
||||
| 状态 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| [ ] | GET | `/api/v1/projects` | 获取当前用户的工程列表 |
|
||||
| [ ] | POST | `/api/v1/projects` | 创建工程 |
|
||||
| [ ] | GET | `/api/v1/projects/:projectId` | 获取工程信息 |
|
||||
| [ ] | DELETE | `/api/v1/projects/:projectId` | 删除工程(级联删除任务和素材) |
|
||||
| [ ] | GET | `/api/v1/projects/:projectId/tasks` | 获取工程下的任务列表 |
|
||||
|
||||
### GET /api/v1/projects
|
||||
|
||||
获取当前用户的工程列表。
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `page` | int | 页码,默认 1 |
|
||||
| `pageSize` | int | 每页条数,默认 20 |
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"total": 3,
|
||||
"projects": [
|
||||
{
|
||||
"id": "proj_abc123",
|
||||
"name": "我的像素游戏",
|
||||
"createdAt": "2026-05-24T10:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/projects
|
||||
|
||||
```json
|
||||
@@ -50,6 +226,44 @@
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/v1/projects/:projectId
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"id": "proj_abc123",
|
||||
"name": "我的像素游戏",
|
||||
"createdAt": "2026-05-24T10:00:00Z",
|
||||
"updatedAt": "2026-05-24T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
错误响应:
|
||||
|
||||
| 场景 | code | message |
|
||||
|------|------|---------|
|
||||
| projectId 不存在 | 404 | 工程不存在 |
|
||||
| 工程不属于当前用户 | 403 | 无权访问该工程 |
|
||||
|
||||
### DELETE /api/v1/projects/:projectId
|
||||
|
||||
级联删除工程下的所有任务、素材及 OSS 文件。
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/v1/projects/:projectId/tasks
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
@@ -80,6 +294,8 @@
|
||||
|
||||
## 工程风格
|
||||
|
||||
需认证。风格归属于工程,仅工程所属用户可操作。
|
||||
|
||||
| 状态 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| [ ] | GET | `/api/v1/projects/:projectId/style` | 获取工程风格 |
|
||||
@@ -100,6 +316,8 @@
|
||||
|
||||
## 素材生成
|
||||
|
||||
需认证。任务归属于当前用户的工程,跨用户访问返回 403。
|
||||
|
||||
| 状态 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| [ ] | POST | `/api/v1/generate` | 提交生成任务,返回 taskId |
|
||||
@@ -153,6 +371,15 @@
|
||||
}
|
||||
```
|
||||
|
||||
去重:相同 `prompt + assetType + params` 的并发请求返回已有 taskId,不重复创建任务。
|
||||
|
||||
错误响应:
|
||||
|
||||
| 场景 | code | message |
|
||||
|------|------|---------|
|
||||
| projectId 不存在或不属于当前用户 | 403 | 无权访问该工程 |
|
||||
| prompt 为空 | 400 | prompt 不能为空 |
|
||||
|
||||
### GET /api/v1/tasks/:taskId
|
||||
|
||||
响应:
|
||||
@@ -166,39 +393,40 @@
|
||||
"projectId": "proj_abc123",
|
||||
"prompt": "一个拿剑的小人",
|
||||
"assetType": "sprite",
|
||||
"taskStyle": null,
|
||||
"params": { "resolution": 64, "format": "spritesheet" },
|
||||
"status": "running",
|
||||
"stage": "asset_generator",
|
||||
"progress": 45,
|
||||
"retryCount": 0,
|
||||
"error": null,
|
||||
"createdAt": "2026-05-24T10:05:00Z",
|
||||
"updatedAt": "2026-05-24T10:05:30Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
任务状态机:
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `status` | string | 任务状态:`pending` / `running` / `completed` / `failed` |
|
||||
| `stage` | string | 当前管线阶段,`running` 时有值 |
|
||||
| `progress` | int | 0-100,整体进度 |
|
||||
| `retryCount` | int | 质检重试次数 |
|
||||
| `error` | string | 失败原因,仅 `failed` 时有值 |
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> pending
|
||||
pending --> running : 管线开始执行
|
||||
running --> completed : 四阶段全部通过
|
||||
running --> failed : 节点执行失败 / 超过重试次数
|
||||
completed --> [*]
|
||||
failed --> [*]
|
||||
```
|
||||
状态机与管线阶段详见 [异步任务](async-tasks.md)。
|
||||
|
||||
管线阶段(对应 Eino Graph 节点):
|
||||
错误响应:
|
||||
|
||||
| 阶段 | 说明 |
|
||||
|------|------|
|
||||
| `prompt_builder` | 合并风格,生成三段式提示词 |
|
||||
| `asset_generator` | 调用 AI 推理 API 出图 |
|
||||
| `quality_supervisor` | 视觉模型质检(可能触发重试回到 prompt_builder) |
|
||||
| `format_adapter` | 格式转换、spritesheet 打包 |
|
||||
| 场景 | code | message |
|
||||
|------|------|---------|
|
||||
| taskId 不存在 | 404 | 任务不存在 |
|
||||
| 任务不属于当前用户 | 403 | 无权访问该任务 |
|
||||
|
||||
### GET /api/v1/tasks/:taskId/assets
|
||||
|
||||
任务状态为 `completed` 时返回素材列表,其他状态返回空数组。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
@@ -207,7 +435,7 @@ stateDiagram-v2
|
||||
"assets": [
|
||||
{
|
||||
"id": "asset_001",
|
||||
"url": "/files/tasks/task_xyz789/spritesheet.png",
|
||||
"url": "https://cdn.example.com/users/user_a1B2c3D4/projects/proj_abc123/tasks/task_xyz789/output/spritesheet.png",
|
||||
"format": "png",
|
||||
"width": 256,
|
||||
"height": 64,
|
||||
@@ -225,7 +453,7 @@ stateDiagram-v2
|
||||
|
||||
### WebSocket 消息格式
|
||||
|
||||
连接路径:`ws://host/api/v1/tasks/:taskId/ws`
|
||||
连接路径:`ws://host/api/v1/tasks/:taskId/ws?token=<jwt>`(通过 query 参数传递 Token)
|
||||
|
||||
```typescript
|
||||
interface PipelineProgress {
|
||||
@@ -267,6 +495,8 @@ interface PipelineProgress {
|
||||
|
||||
## 缓存管理
|
||||
|
||||
需认证。
|
||||
|
||||
| 状态 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| [ ] | DELETE | `/api/v1/cache/:key` | 清除特定缓存 |
|
||||
|
||||
+191
-1
@@ -1,3 +1,193 @@
|
||||
# 异步任务
|
||||
|
||||
> 待补充:任务队列设计、状态机、并发控制、失败重试策略。
|
||||
## 概述
|
||||
|
||||
生成任务采用「提交-异步执行-轮询/推送」模式:API 同步返回 taskId,后端异步执行四阶段管线,前端通过轮询或 WebSocket 获取进度与结果。
|
||||
|
||||
## 任务生命周期
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> pending : POST /api/v1/generate
|
||||
pending --> running : Worker 领取任务
|
||||
running --> completed : 四阶段全部通过
|
||||
running --> failed : 节点执行失败 / 超过重试次数
|
||||
completed --> [*]
|
||||
failed --> [*]
|
||||
```
|
||||
|
||||
| 状态 | 说明 | 持久化 |
|
||||
|------|------|--------|
|
||||
| `pending` | 已入队,等待 Worker 领取 | `task.status = 'pending'` |
|
||||
| `running` | 管线执行中,`task.stage` 记录当前阶段 | `task.status = 'running'` |
|
||||
| `completed` | 管线完成,素材已上传至 OSS | `task.status = 'completed'`, 写入 `asset` 表 |
|
||||
| `failed` | 执行失败或超过重试上限 | `task.status = 'failed'`, `task.error` 记录原因 |
|
||||
|
||||
## 任务队列
|
||||
|
||||
采用内存 Channel 队列(非 Redis),适合单实例部署场景:
|
||||
|
||||
```go
|
||||
// jobqueue.go
|
||||
type JobQueue struct {
|
||||
ch chan string // taskId channel
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewJobQueue(size int) *JobQueue {
|
||||
return &JobQueue{
|
||||
ch: make(chan string, size),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (q *JobQueue) Push(taskId string) {
|
||||
q.ch <- taskId
|
||||
}
|
||||
|
||||
func (q *JobQueue) Pop() (string, bool) {
|
||||
select {
|
||||
case taskId := <-q.ch:
|
||||
return taskId, true
|
||||
case <-q.done:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 提交流程
|
||||
|
||||
```
|
||||
POST /api/v1/generate
|
||||
│
|
||||
├── 1. 去重检查(sync.Map,hash(prompt + assetType + params))
|
||||
├── 2. 写入 task 表(status=pending)
|
||||
├── 3. 推入 JobQueue
|
||||
└── 4. 同步返回 taskId
|
||||
```
|
||||
|
||||
### Worker 流程
|
||||
|
||||
```
|
||||
JobQueue.Pop() 取出 taskId
|
||||
│
|
||||
├── 1. 更新 task.status = running
|
||||
├── 2. 查询 task + project_style
|
||||
├── 3. 构造 PipelineInput
|
||||
├── 4. RunPipeline(ctx, input)
|
||||
│ ├── PromptBuilder → stage 更新
|
||||
│ ├── AssetGenerator → stage 更新
|
||||
│ ├── QualitySupervisor → stage 更新(可能触发重试分支)
|
||||
│ └── FormatAdapter → stage 更新
|
||||
├── 5. 上传素材至 OSS,写入 asset 表
|
||||
├── 6. 更新 task.status = completed
|
||||
└── 7. 异常时更新 task.status = failed, task.error = 错误信息
|
||||
```
|
||||
|
||||
### 并发控制
|
||||
|
||||
通过固定数量的 Worker goroutine 控制并发:
|
||||
|
||||
```go
|
||||
func (s *TaskService) StartWorkers(n int) {
|
||||
for i := 0; i < n; i++ {
|
||||
go func(workerID int) {
|
||||
for {
|
||||
taskId, ok := s.queue.Pop()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
s.processTask(context.Background(), taskId)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| 队列容量 | 100 | 内存 channel 缓冲大小 |
|
||||
| Worker 数 | 3 | 并发执行任务数 |
|
||||
|
||||
## 管线阶段与进度
|
||||
|
||||
每个阶段对应 Eino Graph 的一个节点,执行过程中更新 `task.stage` 和 `task.progress`:
|
||||
|
||||
| 阶段 | stage 值 | 进度范围 | 说明 |
|
||||
|------|----------|---------|------|
|
||||
| 提示词构建 | `prompt_builder` | 0-20% | 合并风格,生成三段式提示词 |
|
||||
| 素材生成 | `asset_generator` | 20-60% | 调用 AI 推理 API 出图 |
|
||||
| 质量检查 | `quality_supervisor` | 60-80% | 视觉模型质检 |
|
||||
| 格式适配 | `format_adapter` | 80-100% | 格式转换、spritesheet 打包、上传 OSS |
|
||||
|
||||
进度更新通过 WebSocket 实时推送给前端(参见 [API 设计 - WebSocket 消息格式](api.md))。
|
||||
|
||||
## 重试策略
|
||||
|
||||
### 质检重试(管线内重试)
|
||||
|
||||
QualitySupervisor 质检不通过时,Eino Graph 分支回到 PromptBuilder 重新生成:
|
||||
|
||||
```
|
||||
QualitySupervisor -- fail --> PromptBuilder --> AssetGenerator --> QualitySupervisor
|
||||
QualitySupervisor -- pass --> FormatAdapter
|
||||
```
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| 最大重试次数 | 3 | `task.retry_count` 达到上限后降级输出 |
|
||||
| 重试触发条件 | 质检不通过 | 视觉模型判定风格不一致 |
|
||||
|
||||
超过重试次数后,跳过质检直接进入 FormatAdapter 输出(降级策略,保证任务不会无限循环)。
|
||||
|
||||
### 任务级重试(管线外重试)
|
||||
|
||||
管线执行过程中发生不可恢复的错误(如 AI API 超时、OSS 上传失败):
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| 最大重试次数 | 1 | 仅重试一次 |
|
||||
| 重试间隔 | 5 秒 | 固定间隔 |
|
||||
|
||||
重试时重新执行完整管线,不保留上次中间状态。
|
||||
|
||||
## 进度推送
|
||||
|
||||
前端可通过两种方式获取任务进度:
|
||||
|
||||
### 轮询
|
||||
|
||||
```
|
||||
GET /api/v1/tasks/:taskId
|
||||
```
|
||||
|
||||
前端定时调用(建议间隔 2-3 秒),简单可靠,适合不需要实时性的场景。
|
||||
|
||||
### WebSocket
|
||||
|
||||
```
|
||||
ws://host/api/v1/tasks/:taskId/ws?token=<jwt>
|
||||
```
|
||||
|
||||
长连接实时推送,每个阶段的状态变更立即通知前端。消息格式参见 [API 设计](api.md)。
|
||||
|
||||
## 去重
|
||||
|
||||
相同 `prompt + assetType + params` 的并发请求,通过应用层去重避免重复生成:
|
||||
|
||||
1. 计算 `hash(prompt + assetType + params)` 作为去重键
|
||||
2. `sync.Map` 中查找:若存在且任务仍在运行中,直接返回已有 taskId
|
||||
3. 任务完成或失败后从内存中清除
|
||||
|
||||
去重范围为当前实例内存,不跨实例共享。相同输入在不同时间点允许重新生成。
|
||||
|
||||
## 文件存储
|
||||
|
||||
任务完成后,FormatAdapter 将素材上传至 OSS:
|
||||
|
||||
- **开发环境**:写入本地 `data/` 目录,通过 Gin 静态文件服务访问
|
||||
- **生产环境**:上传至 OSS 存储桶,`asset.url` 存储完整 CDN URL
|
||||
|
||||
OSS Key 结构:`users/{userId}/projects/{projectId}/tasks/{taskId}/output/`
|
||||
|
||||
素材持久化保存,随工程生命周期管理,删除工程时级联删除 OSS 文件。
|
||||
|
||||
+237
-1
@@ -1,3 +1,239 @@
|
||||
# 数据存储
|
||||
|
||||
> 待补充:数据库选型、表结构设计、素材文件存储方案。
|
||||
## 选型
|
||||
|
||||
| 用途 | 方案 | 说明 |
|
||||
|------|------|------|
|
||||
| 持久化存储 | MySQL 8.0+ | 工程、任务、素材元数据、风格配置全部落库 |
|
||||
| 文件存储 | OSS 对象存储 | 生成的图片与 spritesheet 文件上传至 S3 兼容存储桶,通过 CDN URL 访问 |
|
||||
| 认证 | JWT | 简单注册登录,Bearer Token 鉴权 |
|
||||
| 缓存 / 去重 | MySQL + 应用层内存 | 请求去重通过数据库唯一约束 + 应用层 sync.Map 实现短期去重窗口,不引入 Redis |
|
||||
|
||||
**不用 Redis 的理由**:gen2d 是单实例部署的工具型应用,并发量有限,任务状态轮询即可满足实时性需求。MySQL 完全能覆盖缓存、去重、队列语义,引入 Redis 增加运维复杂度但收益不大。
|
||||
|
||||
---
|
||||
|
||||
## 表结构
|
||||
|
||||
### user — 用户
|
||||
|
||||
```sql
|
||||
CREATE TABLE `user` (
|
||||
`id` CHAR(20) NOT NULL, -- 用户 ID,如 user_a1B2c3
|
||||
`username` VARCHAR(64) NOT NULL, -- 登录用户名
|
||||
`email` VARCHAR(128) NOT NULL, -- 邮箱
|
||||
`password_hash` VARCHAR(128) NOT NULL, -- bcrypt 哈希
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_username` (`username`),
|
||||
UNIQUE KEY `uk_email` (`email`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
### project — 工程
|
||||
|
||||
```sql
|
||||
CREATE TABLE `project` (
|
||||
`id` CHAR(20) NOT NULL, -- 项目 ID,如 proj_abc123
|
||||
`user_id` CHAR(20) NOT NULL, -- 所属用户
|
||||
`name` VARCHAR(128) NOT NULL, -- 工程名称
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user` (`user_id`, `created_at`),
|
||||
CONSTRAINT `fk_project_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
### project_style — 工程风格
|
||||
|
||||
工程级键值对,保证同一工程下所有素材风格一致。每个工程恰好一行。
|
||||
|
||||
```sql
|
||||
CREATE TABLE `project_style` (
|
||||
`id` CHAR(20) NOT NULL,
|
||||
`project_id` CHAR(20) NOT NULL,
|
||||
`kv_pairs` JSON NOT NULL, -- {"artStyle":"pixel","palette":"warm",...}
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_project` (`project_id`),
|
||||
CONSTRAINT `fk_style_project` FOREIGN KEY (`project_id`) REFERENCES `project` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
### task — 生成任务
|
||||
|
||||
```sql
|
||||
CREATE TABLE `task` (
|
||||
`id` CHAR(20) NOT NULL, -- 任务 ID,如 task_xyz789
|
||||
`project_id` CHAR(20) NOT NULL,
|
||||
`prompt` TEXT NOT NULL, -- 用户原始文本
|
||||
`asset_type` VARCHAR(32) NOT NULL, -- sprite / background / ui / animation
|
||||
`task_style` JSON NULL, -- 任务级风格覆盖,可为 NULL
|
||||
`params` JSON NOT NULL DEFAULT '{}', -- {"resolution":64,"frames":{...},"format":"spritesheet"}
|
||||
`status` VARCHAR(16) NOT NULL DEFAULT 'pending', -- pending / running / completed / failed
|
||||
`stage` VARCHAR(32) NULL, -- 当前管线阶段:prompt_builder / asset_generator / quality_supervisor / format_adapter
|
||||
`progress` TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0-100
|
||||
`retry_count` TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`error` TEXT NULL, -- 失败原因
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_project` (`project_id`, `created_at`),
|
||||
KEY `idx_status` (`status`),
|
||||
CONSTRAINT `fk_task_project` FOREIGN KEY (`project_id`) REFERENCES `project` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
### asset — 生成素材
|
||||
|
||||
```sql
|
||||
CREATE TABLE `asset` (
|
||||
`id` CHAR(20) NOT NULL, -- 素材 ID,如 asset_001
|
||||
`task_id` CHAR(20) NOT NULL,
|
||||
`url` VARCHAR(512) NOT NULL, -- OSS 完整 URL 或相对路径
|
||||
`format` VARCHAR(16) NOT NULL DEFAULT 'png', -- png / json
|
||||
`width` INT NOT NULL DEFAULT 0,
|
||||
`height` INT NOT NULL DEFAULT 0,
|
||||
`metadata` JSON NULL, -- {"frameWidth":64,"frameHeight":64,"frameCount":4,"directions":1}
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_task` (`task_id`),
|
||||
CONSTRAINT `fk_asset_task` FOREIGN KEY (`task_id`) REFERENCES `task` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ER 关系
|
||||
|
||||
```
|
||||
user 1 ── N project
|
||||
project 1 ── 1 project_style
|
||||
project 1 ── N task
|
||||
task 1 ── N asset
|
||||
```
|
||||
|
||||
对应 [后端工程](backend.md) 中的关键实体定义,所有一对多关系通过外键 + CASCADE 删除维护。
|
||||
|
||||
---
|
||||
|
||||
## ID 生成
|
||||
|
||||
使用 **Sonyflake**(或类似分布式 ID 方案)生成 20 字符的字符串 ID,格式为 `{前缀}_{base62}`:
|
||||
|
||||
| 实体 | 前缀 | 示例 |
|
||||
|------|------|------|
|
||||
| user | `user` | `user_a1B2c3D4` |
|
||||
| project | `proj` | `proj_3kF9a2Bc` |
|
||||
| project_style | `sty` | `sty_7xM2pQ1d` |
|
||||
| task | `task` | `task_9bN4vR8e` |
|
||||
| asset | `asset` | `asset_2wK6tY5f` |
|
||||
|
||||
---
|
||||
|
||||
## 文件存储
|
||||
|
||||
使用 S3 兼容对象存储(如阿里云 OSS、MinIO),用户生成的素材持久化保存,便于重复利用。
|
||||
|
||||
### OSS Key 结构
|
||||
|
||||
```
|
||||
{bucket}/
|
||||
└── users/
|
||||
└── {userId}/
|
||||
└── projects/
|
||||
└── {projectId}/
|
||||
└── tasks/
|
||||
└── {taskId}/
|
||||
├── raw/ # AssetGenerator 输出的原始图片
|
||||
│ ├── 0.png
|
||||
│ ├── 1.png
|
||||
│ └── ...
|
||||
├── output/ # FormatAdapter 输出的最终素材
|
||||
│ ├── spritesheet.png
|
||||
│ └── metadata.json
|
||||
└── pipeline.log # 管线执行日志
|
||||
```
|
||||
|
||||
### 访问方式
|
||||
|
||||
- **开发环境**:Gin 静态文件服务,路由 `/files/*` 映射到本地 `data/` 目录,无需 OSS。
|
||||
- **生产环境**:文件上传至 OSS 存储桶,`asset.url` 存储完整 CDN URL,前端直接访问。
|
||||
|
||||
### asset 表的 url 字段
|
||||
|
||||
- 开发环境:相对路径,如 `projects/proj_abc/tasks/task_xyz/output/spritesheet.png`,前端通过 `/files/{url}` 拼接。
|
||||
- 生产环境:完整 URL,如 `https://cdn.example.com/users/user_xxx/projects/proj_abc/tasks/task_xyz/output/spritesheet.png`。
|
||||
|
||||
---
|
||||
|
||||
## 去重策略
|
||||
|
||||
相同 prompt + assetType + params 的并发请求,通过应用层去重避免重复生成:
|
||||
|
||||
1. 请求到达时,计算 `hash(prompt + assetType + params)` 作为去重键。
|
||||
2. 应用内存(`sync.Map`)中查找该键:
|
||||
- 若存在且任务仍在运行中,直接返回已有 taskId。
|
||||
- 若不存在,写入内存并提交任务。
|
||||
3. 任务完成或失败后,从内存中清除(可设置 TTL 过期兜底)。
|
||||
|
||||
不依赖数据库唯一约束做去重,因为相同输入在不同时间点应该允许重新生成。
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
数据库连接信息通过环境变量注入:
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|----------|--------|------|
|
||||
| `GEN2D_DB_DSN` | `root:@tcp(127.0.0.1:3306)/gen2d?charset=utf8mb4&parseTime=True&loc=Local` | MySQL DSN |
|
||||
| `GEN2D_DB_MAX_OPEN` | `25` | 最大连接数 |
|
||||
| `GEN2D_DB_MAX_IDLE` | `5` | 最大空闲连接数 |
|
||||
| `GEN2D_DB_MAX_LIFETIME` | `300` | 连接最大存活时间(秒) |
|
||||
| `GEN2D_JWT_SECRET` | — | JWT 签名密钥(必填) |
|
||||
| `GEN2D_JWT_EXPIRE` | `7200` | Token 过期时间(秒) |
|
||||
| `GEN2D_OSS_ENDPOINT` | — | OSS Endpoint(生产环境必填) |
|
||||
| `GEN2D_OSS_BUCKET` | — | 存储桶名称 |
|
||||
| `GEN2D_OSS_ACCESS_KEY` | — | Access Key ID |
|
||||
| `GEN2D_OSS_SECRET_KEY` | — | Access Key Secret |
|
||||
| `GEN2D_OSS_CDN_DOMAIN` | — | CDN 域名(可选,用于拼接公开访问 URL) |
|
||||
|
||||
在 `config.go` 中扩展字段即可,无需额外依赖。
|
||||
|
||||
---
|
||||
|
||||
## 迁移
|
||||
|
||||
使用 [golang-migrate](https://github.com/golang-migrate/migrate) 管理 schema 版本:
|
||||
|
||||
```
|
||||
backend/internal/migrations/
|
||||
├── 000001_init_user.up.sql
|
||||
├── 000001_init_user.down.sql
|
||||
├── 000002_init_project.up.sql
|
||||
├── 000002_init_project.down.sql
|
||||
├── 000003_init_task.up.sql
|
||||
├── 000003_init_task.down.sql
|
||||
├── 000004_init_asset.up.sql
|
||||
└── 000004_init_asset.down.sql
|
||||
```
|
||||
|
||||
启动时自动执行 `migrate.Up()`,保证 schema 与代码版本一致。
|
||||
|
||||
---
|
||||
|
||||
## 索引说明
|
||||
|
||||
| 表 | 索引 | 用途 |
|
||||
|----|------|------|
|
||||
| `user` | `uk_username (username)` | 用户名唯一约束 |
|
||||
| `user` | `uk_email (email)` | 邮箱唯一约束 |
|
||||
| `project` | `idx_user (user_id, created_at)` | 用户下工程列表分页查询 |
|
||||
| `project_style` | `uk_project (project_id)` | 一个工程一个风格,唯一约束 |
|
||||
| `task` | `idx_project (project_id, created_at)` | 工程下任务列表分页查询 |
|
||||
| `task` | `idx_status (status)` | 按状态筛选任务(如查找所有 pending 任务) |
|
||||
| `asset` | `idx_task (task_id)` | 任务下素材列表查询 |
|
||||
|
||||
Reference in New Issue
Block a user