docs: 适配单机场景,简化存储与认证方案
- 数据库:MySQL → SQLite,去掉连接池配置和 golang-migrate - 文件存储:OSS 对象存储 → 本地文件系统,统一 Gin 静态文件服务 - ID 生成:Sonyflake → 前缀+随机字符串 - 认证:Bearer Token → httpOnly Cookie,WebSocket 同步改为 Cookie 认证 - 质检重试:PromptBuilder 重试时注入 RejectReason 改进提示词 - 合并 multi-agent-pipeline.md 到 backend.md,消除文档重复 - 移除缓存管理 API(暴露内部实现细节)
This commit is contained in:
+100
-111
@@ -4,12 +4,12 @@
|
||||
|
||||
| 用途 | 方案 | 说明 |
|
||||
|------|------|------|
|
||||
| 持久化存储 | MySQL 8.0+ | 工程、任务、素材元数据、风格配置全部落库 |
|
||||
| 文件存储 | OSS 对象存储 | 生成的图片与 spritesheet 文件上传至 S3 兼容存储桶,通过 CDN URL 访问 |
|
||||
| 认证 | JWT | 简单注册登录,Bearer Token 鉴权 |
|
||||
| 缓存 / 去重 | MySQL + 应用层内存 | 请求去重通过数据库唯一约束 + 应用层 sync.Map 实现短期去重窗口,不引入 Redis |
|
||||
| 持久化存储 | SQLite 3 | 工程、任务、素材元数据、风格配置全部落库,单文件部署 |
|
||||
| 文件存储 | 本地文件系统 | 生成的图片与 spritesheet 文件保存到本地目录,通过 Gin 静态文件服务访问 |
|
||||
| 认证 | JWT + Cookie | 简单注册登录,httpOnly Cookie 鉴权 |
|
||||
| 缓存 / 去重 | 应用层内存 | 请求去重通过应用层 `sync.Map` 实现短期去重窗口,无需外部依赖 |
|
||||
|
||||
**不用 Redis 的理由**:gen2d 是单实例部署的工具型应用,并发量有限,任务状态轮询即可满足实时性需求。MySQL 完全能覆盖缓存、去重、队列语义,引入 Redis 增加运维复杂度但收益不大。
|
||||
**选用 SQLite 的理由**:gen2d 是单实例部署的工具型应用,SQLite 零配置、单文件、性能足够,无需额外数据库服务。
|
||||
|
||||
---
|
||||
|
||||
@@ -18,32 +18,30 @@
|
||||
### 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;
|
||||
CREATE TABLE user (
|
||||
id TEXT NOT NULL PRIMARY KEY, -- 用户 ID,如 user_a1B2c3
|
||||
username TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL, -- bcrypt 哈希
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_username ON user (username);
|
||||
CREATE UNIQUE INDEX uk_email ON user (email);
|
||||
```
|
||||
|
||||
### 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;
|
||||
CREATE TABLE project (
|
||||
id TEXT NOT NULL PRIMARY KEY, -- 项目 ID,如 proj_abc123
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_project_user ON project (user_id, created_at);
|
||||
```
|
||||
|
||||
### project_style — 工程风格
|
||||
@@ -51,58 +49,55 @@ CREATE TABLE `project` (
|
||||
工程级键值对,保证同一工程下所有素材风格一致。每个工程恰好一行。
|
||||
|
||||
```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;
|
||||
CREATE TABLE project_style (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
kv_pairs TEXT NOT NULL, -- JSON: {"artStyle":"pixel","palette":"warm",...}
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES project (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_style_project ON project_style (project_id);
|
||||
```
|
||||
|
||||
### 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;
|
||||
CREATE TABLE task (
|
||||
id TEXT NOT NULL PRIMARY KEY, -- 任务 ID,如 task_xyz789
|
||||
project_id TEXT NOT NULL,
|
||||
prompt TEXT NOT NULL, -- 用户原始文本
|
||||
asset_type TEXT NOT NULL, -- sprite / background / ui / animation
|
||||
task_style TEXT, -- JSON: 任务级风格覆盖,可为 NULL
|
||||
params TEXT NOT NULL DEFAULT '{}', -- JSON: {"resolution":64,"frames":{...},"format":"spritesheet"}
|
||||
status TEXT NOT NULL DEFAULT 'pending', -- pending / running / completed / failed
|
||||
stage TEXT, -- 当前管线阶段
|
||||
progress INTEGER NOT NULL DEFAULT 0, -- 0-100
|
||||
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||
error TEXT, -- 失败原因
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES project (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_task_project ON task (project_id, created_at);
|
||||
CREATE INDEX idx_task_status ON task (status);
|
||||
```
|
||||
|
||||
### 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;
|
||||
CREATE TABLE asset (
|
||||
id TEXT NOT NULL PRIMARY KEY, -- 素材 ID,如 asset_001
|
||||
task_id TEXT NOT NULL,
|
||||
url TEXT NOT NULL, -- 相对路径,如 tasks/task_xyz/output/spritesheet.png
|
||||
format TEXT NOT NULL DEFAULT 'png', -- png / json
|
||||
width INTEGER NOT NULL DEFAULT 0,
|
||||
height INTEGER NOT NULL DEFAULT 0,
|
||||
metadata TEXT, -- JSON: {"frameWidth":64,"frameHeight":64,"frameCount":4,"directions":1}
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (task_id) REFERENCES task (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_asset_task ON asset (task_id);
|
||||
```
|
||||
|
||||
---
|
||||
@@ -122,26 +117,28 @@ task 1 ── N asset
|
||||
|
||||
## ID 生成
|
||||
|
||||
使用 **Sonyflake**(或类似分布式 ID 方案)生成 20 字符的字符串 ID,格式为 `{前缀}_{base62}`:
|
||||
使用前缀 + 随机字符串生成 ID,格式为 `{prefix}_{random}`:
|
||||
|
||||
| 实体 | 前缀 | 示例 |
|
||||
|------|------|------|
|
||||
| user | `user` | `user_a1B2c3D4` |
|
||||
| project | `proj` | `proj_3kF9a2Bc` |
|
||||
| project_style | `sty` | `sty_7xM2pQ1d` |
|
||||
| task | `task` | `task_9bN4vR8e` |
|
||||
| asset | `asset` | `asset_2wK6tY5f` |
|
||||
| user | `user` | `user_a1B2c3` |
|
||||
| project | `proj` | `proj_3kF9a2` |
|
||||
| project_style | `sty` | `sty_7xM2pQ` |
|
||||
| task | `task` | `task_9bN4vR` |
|
||||
| asset | `asset` | `asset_2wK6tY` |
|
||||
|
||||
随机部分使用 `crypto/rand` 生成 6 字节 base62 编码,兼顾可读性与唯一性。
|
||||
|
||||
---
|
||||
|
||||
## 文件存储
|
||||
|
||||
使用 S3 兼容对象存储(如阿里云 OSS、MinIO),用户生成的素材持久化保存,便于重复利用。
|
||||
生成的素材文件保存到本地文件系统,通过 Gin 静态文件服务访问。
|
||||
|
||||
### OSS Key 结构
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
{bucket}/
|
||||
{dataDir}/
|
||||
└── users/
|
||||
└── {userId}/
|
||||
└── projects/
|
||||
@@ -158,15 +155,22 @@ task 1 ── N asset
|
||||
└── pipeline.log # 管线执行日志
|
||||
```
|
||||
|
||||
`dataDir` 通过环境变量 `GEN2D_DATA_DIR` 配置,默认 `./data`。
|
||||
|
||||
### 访问方式
|
||||
|
||||
- **开发环境**:Gin 静态文件服务,路由 `/files/*` 映射到本地 `data/` 目录,无需 OSS。
|
||||
- **生产环境**:文件上传至 OSS 存储桶,`asset.url` 存储完整 CDN URL,前端直接访问。
|
||||
Gin 静态文件服务,路由 `/files/*` 映射到 `{dataDir}/` 目录:
|
||||
|
||||
```go
|
||||
router.Static("/files", dataDir)
|
||||
```
|
||||
|
||||
### 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`。
|
||||
存储相对路径,前端通过 `/files/{url}` 拼接访问:
|
||||
|
||||
- 如 `users/user_a1B2c3/projects/proj_abc/tasks/task_xyz/output/spritesheet.png`
|
||||
- 前端拼接为 `/files/users/user_a1B2c3/projects/proj_abc/tasks/task_xyz/output/spritesheet.png`
|
||||
|
||||
---
|
||||
|
||||
@@ -180,8 +184,6 @@ task 1 ── N asset
|
||||
- 若不存在,写入内存并提交任务。
|
||||
3. 任务完成或失败后,从内存中清除(可设置 TTL 过期兜底)。
|
||||
|
||||
不依赖数据库唯一约束做去重,因为相同输入在不同时间点应该允许重新生成。
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
@@ -191,17 +193,10 @@ task 1 ── N asset
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|----------|--------|------|
|
||||
| `GEN2D_SERVER_PORT` | `8080` | HTTP 服务监听端口 |
|
||||
| `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_DB_PATH` | `./data/gen2d.db` | SQLite 数据库文件路径 |
|
||||
| `GEN2D_DATA_DIR` | `./data` | 本地文件存储根目录 |
|
||||
| `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` 中扩展字段即可,无需额外依赖。
|
||||
|
||||
@@ -209,23 +204,17 @@ task 1 ── N asset
|
||||
|
||||
## 迁移
|
||||
|
||||
使用 [golang-migrate](https://github.com/golang-migrate/migrate) 管理 schema 版本:
|
||||
应用启动时自动执行建表 SQL,保证 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
|
||||
```go
|
||||
// 启动时执行
|
||||
db.Exec(createUserTableSQL)
|
||||
db.Exec(createProjectTableSQL)
|
||||
db.Exec(createTaskTableSQL)
|
||||
db.Exec(createAssetTableSQL)
|
||||
```
|
||||
|
||||
启动时自动执行 `migrate.Up()`,保证 schema 与代码版本一致。
|
||||
|
||||
> 注:`project_style` 表的创建包含在 `000002_init_project.up.sql` 中,与 `project` 表同批迁移。
|
||||
使用 `CREATE TABLE IF NOT EXISTS` 保证幂等性。
|
||||
|
||||
---
|
||||
|
||||
@@ -235,8 +224,8 @@ backend/internal/migrations/
|
||||
|----|------|------|
|
||||
| `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)` | 任务下素材列表查询 |
|
||||
| `project` | `idx_project_user (user_id, created_at)` | 用户下工程列表分页查询 |
|
||||
| `project_style` | `uk_style_project (project_id)` | 一个工程一个风格,唯一约束 |
|
||||
| `task` | `idx_task_project (project_id, created_at)` | 工程下任务列表分页查询 |
|
||||
| `task` | `idx_task_status (status)` | 按状态筛选任务 |
|
||||
| `asset` | `idx_asset_task (task_id)` | 任务下素材列表查询 |
|
||||
|
||||
Reference in New Issue
Block a user