diff --git a/docs/api.md b/docs/api.md index e7a3897..a1ee06f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -6,6 +6,39 @@ { "code": 0, "message": "ok", "data": {} } ``` +## 认证 + +除健康检查和注册登录外,所有接口需在请求头携带 Bearer Token: + +``` +Authorization: Bearer +``` + +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=`(通过 query 参数传递 Token) ```typescript interface PipelineProgress { @@ -267,6 +495,8 @@ interface PipelineProgress { ## 缓存管理 +需认证。 + | 状态 | 方法 | 路径 | 说明 | |------|------|------|------| | [ ] | DELETE | `/api/v1/cache/:key` | 清除特定缓存 | diff --git a/docs/async-tasks.md b/docs/async-tasks.md index a110dec..00e9712 100644 --- a/docs/async-tasks.md +++ b/docs/async-tasks.md @@ -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= +``` + +长连接实时推送,每个阶段的状态变更立即通知前端。消息格式参见 [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 文件。 diff --git a/docs/database.md b/docs/database.md index 250c686..53d6569 100644 --- a/docs/database.md +++ b/docs/database.md @@ -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)` | 任务下素材列表查询 |