# 数据存储 ## 选型 | 用途 | 方案 | 说明 | |------|------|------| | 持久化存储 | SQLite 3 | 工程、任务、素材元数据、风格配置全部落库,单文件部署 | | 文件存储 | 本地文件系统 | 生成的图片与 spritesheet 文件保存到本地目录,通过 Gin 静态文件服务访问 | | 认证 | JWT + Cookie | 简单注册登录,httpOnly Cookie 鉴权 | | 缓存 / 去重 | 应用层内存 | 请求去重通过应用层 `sync.Map` 实现短期去重窗口,无需外部依赖 | **选用 SQLite 的理由**:gen2d 是单实例部署的工具型应用,SQLite 零配置、单文件、性能足够,无需额外数据库服务。 --- ## 表结构 ### user — 用户 ```sql 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 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 — 工程风格 工程级键值对,保证同一工程下所有素材风格一致。每个工程恰好一行。 ```sql 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 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 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); ``` --- ## ER 关系 ``` user 1 ── N project project 1 ── 1 project_style project 1 ── N task task 1 ── N asset ``` 对应 [后端工程](backend.md) 中的关键实体定义,所有一对多关系通过外键 + CASCADE 删除维护。 --- ## ID 生成 使用前缀 + 随机字符串生成 ID,格式为 `{prefix}_{random}`: | 实体 | 前缀 | 示例 | |------|------|------| | 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 编码,兼顾可读性与唯一性。 --- ## 文件存储 生成的素材文件保存到本地文件系统,通过 Gin 静态文件服务访问。 ### 目录结构 ``` {dataDir}/ └── users/ └── {userId}/ └── projects/ └── {projectId}/ └── tasks/ └── {taskId}/ ├── raw/ # AssetGenerator 输出的原始图片 │ ├── 0.png │ ├── 1.png │ └── ... ├── output/ # FormatAdapter 输出的最终素材 │ ├── spritesheet.png │ └── metadata.json └── pipeline.log # 管线执行日志 ``` `dataDir` 通过环境变量 `GEN2D_DATA_DIR` 配置,默认 `./data`。 ### 访问方式 Gin 静态文件服务,路由 `/files/*` 映射到 `{dataDir}/` 目录: ```go router.Static("/files", dataDir) ``` ### asset 表的 url 字段 存储相对路径,前端通过 `/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` --- ## 去重策略 相同 prompt + assetType + params 的并发请求,通过应用层去重避免重复生成: 1. 请求到达时,计算 `hash(prompt + assetType + params)` 作为去重键。 2. 应用内存(`sync.Map`)中查找该键: - 若存在且任务仍在运行中,直接返回已有 taskId。 - 若不存在,写入内存并提交任务。 3. 任务完成或失败后,从内存中清除(可设置 TTL 过期兜底)。 --- ## 配置 数据库连接信息通过环境变量注入: | 环境变量 | 默认值 | 说明 | |----------|--------|------| | `GEN2D_SERVER_PORT` | `8080` | HTTP 服务监听端口 | | `GEN2D_DB_PATH` | `./data/gen2d.db` | SQLite 数据库文件路径 | | `GEN2D_DATA_DIR` | `./data` | 本地文件存储根目录 | | `GEN2D_JWT_SECRET` | — | JWT 签名密钥(必填) | | `GEN2D_JWT_EXPIRE` | `7200` | Token 过期时间(秒) | 在 `config.go` 中扩展字段即可,无需额外依赖。 --- ## 迁移 应用启动时自动执行建表 SQL,保证 schema 与代码版本一致: ```go // 启动时执行 db.Exec(createUserTableSQL) db.Exec(createProjectTableSQL) db.Exec(createTaskTableSQL) db.Exec(createAssetTableSQL) ``` 使用 `CREATE TABLE IF NOT EXISTS` 保证幂等性。 --- ## 索引说明 | 表 | 索引 | 用途 | |----|------|------| | `user` | `uk_username (username)` | 用户名唯一约束 | | `user` | `uk_email (email)` | 邮箱唯一约束 | | `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)` | 任务下素材列表查询 |