# 06 · 数据库设计

## 1. 存储选型

| 存储 | 用途 |
|------|------|
| MySQL 8 | 核心业务数据(ACID) |
| **Redis Stack 7.2** | 缓存、分布式锁、限流、任务去重、SSE 订阅、**Agent 记忆向量检索(RediSearch)** |
| ~~Elasticsearch~~ | M8 后按需引入,MVP 用 MySQL FULLTEXT 代替 |
| **阿里云 OSS / 火山引擎 TOS**(生产) | 媒体文件(图/视频/音频),默认私有 Bucket,预签名 URL 对外 |
| MinIO(备选 TODO) | 本地开发 / 私有化部署 |

## 2. MySQL 表设计 (核心表 ~35 张)

### 2.1 账号与租户

```sql
-- 租户(组织)
CREATE TABLE tenants (
  id BIGINT PK AUTO_INCREMENT,
  name VARCHAR(128) NOT NULL,
  slug VARCHAR(64) UNIQUE,
  plan ENUM('FREE','PRO','TEAM','ENTERPRISE') DEFAULT 'FREE',
  status ENUM('ACTIVE','SUSPENDED','DELETED') DEFAULT 'ACTIVE',
  owner_user_id BIGINT,
  created_at DATETIME, updated_at DATETIME
);

-- 用户
CREATE TABLE users (
  id BIGINT PK,
  email VARCHAR(255) UNIQUE,
  phone VARCHAR(32) UNIQUE,
  username VARCHAR(64) UNIQUE,
  password_hash VARCHAR(100),
  avatar_url VARCHAR(500),
  locale VARCHAR(16) DEFAULT 'zh-CN',
  status ENUM('ACTIVE','DISABLED','DELETED'),
  last_login_at DATETIME,
  created_at DATETIME
);

-- 用户-租户关联(多租户成员)
CREATE TABLE tenant_members (
  id BIGINT PK,
  tenant_id BIGINT,
  user_id BIGINT,
  role ENUM('OWNER','ADMIN','MEMBER'),
  joined_at DATETIME,
  UNIQUE KEY (tenant_id, user_id)
);

-- 企业级员工与小组(对应前端 /team 页)
CREATE TABLE employees (
  id BIGINT PK, tenant_id BIGINT, user_id BIGINT,
  position VARCHAR(128), status VARCHAR(16),
  created_at DATETIME
);
CREATE TABLE groups (
  id BIGINT PK, tenant_id BIGINT, name VARCHAR(128),
  description TEXT, permissions JSON
);
CREATE TABLE group_members (
  group_id BIGINT, employee_id BIGINT, PRIMARY KEY(group_id, employee_id)
);
```

### 2.2 项目与剧本

```sql
CREATE TABLE projects (
  id BIGINT PK,
  tenant_id BIGINT, owner_user_id BIGINT, group_id BIGINT NULL,
  title VARCHAR(255), description TEXT, cover_url VARCHAR(500),
  aspect_ratio ENUM('16:9','9:16','1:1'),
  resolution ENUM('720P','1080P','2K'),
  art_style VARCHAR(64),        -- 2d_anime / chinese_guofeng / ...
  world_setting TEXT,
  color_palette VARCHAR(255),
  target_duration INT,           -- 分钟
  target_episodes INT,
  status ENUM('DRAFT','OUTLINE','STORYBOARD','VIDEO','COMPLETED','ARCHIVED'),
  progress INT DEFAULT 0,        -- 0-100
  tags JSON,
  text_model VARCHAR(64), image_model VARCHAR(64), video_model VARCHAR(64), tts_model VARCHAR(64),
  watermark JSON,
  final_video_url VARCHAR(500),
  created_at DATETIME, updated_at DATETIME,
  INDEX idx_tenant (tenant_id), INDEX idx_status (status)
);

CREATE TABLE project_members (
  id BIGINT PK, project_id BIGINT, user_id BIGINT,
  role ENUM('OWNER','ADMIN','EDITOR','VIEWER'),
  joined_at DATETIME,
  UNIQUE KEY (project_id, user_id)
);

CREATE TABLE project_invitations (
  id BIGINT PK, project_id BIGINT, email VARCHAR(255),
  role VARCHAR(16), token VARCHAR(128), expires_at DATETIME,
  status ENUM('PENDING','ACCEPTED','EXPIRED','REVOKED')
);

CREATE TABLE scripts (
  id BIGINT PK, project_id BIGINT,
  original_filename VARCHAR(255), file_url VARCHAR(500),
  raw_text LONGTEXT, word_count INT,
  created_at DATETIME
);

CREATE TABLE novel_events (
  id BIGINT PK, project_id BIGINT, chapter_index INT,
  chapter_title VARCHAR(255), event_data JSON, processed BOOLEAN,
  created_at DATETIME
);

CREATE TABLE outlines (
  id BIGINT PK, project_id BIGINT,
  title VARCHAR(255), genre VARCHAR(64), summary TEXT,
  themes JSON, arcs JSON, story_skeleton JSON,
  version INT, created_at DATETIME
);

CREATE TABLE episodes (
  id BIGINT PK, project_id BIGINT, sequence INT,
  title VARCHAR(255), description TEXT,
  duration VARCHAR(32), keywords JSON,
  generation_mode ENUM('KEYFRAME','REFERENCE'),
  color_palette VARCHAR(255), bgm_url VARCHAR(500),
  status VARCHAR(32), final_video_url VARCHAR(500),
  created_at DATETIME,
  UNIQUE KEY (project_id, sequence)
);
```

### 2.3 资产(角色/场景/道具)

```sql
CREATE TABLE characters (
  id BIGINT PK, project_id BIGINT,
  name VARCHAR(128), role_type VARCHAR(32),  -- 女主/男主/反派/配角
  gender ENUM('MALE','FEMALE','OTHER'),
  age VARCHAR(32), personality TEXT, background TEXT,
  costume TEXT, voice_id VARCHAR(64),
  visual_hint TEXT, performance_style TEXT,
  height_cm INT, body_type VARCHAR(32),
  reference_image_url VARCHAR(500),  -- 四视图
  turnaround_history JSON,            -- 历史版本
  scope ENUM('MAIN','GUEST'),
  created_at DATETIME, updated_at DATETIME,
  INDEX idx_project (project_id)
);

CREATE TABLE character_appearances (
  id BIGINT PK, character_id BIGINT,
  name VARCHAR(128),              -- 常服/礼服/战损...
  prompt TEXT, image_url VARCHAR(500),
  version INT, is_active BOOLEAN,
  created_at DATETIME
);

CREATE TABLE character_relations (
  id BIGINT PK, project_id BIGINT,
  character_a_id BIGINT, character_b_id BIGINT,
  relation_type VARCHAR(64), description TEXT
);

CREATE TABLE episode_characters (
  episode_id BIGINT, character_id BIGINT,
  PRIMARY KEY (episode_id, character_id)
);

CREATE TABLE scenes (
  id BIGINT PK, project_id BIGINT,
  name VARCHAR(128), type ENUM('INDOOR','OUTDOOR'),
  description TEXT, usage TEXT,
  image_url VARCHAR(500), reference_image_url VARCHAR(500),
  version INT, created_at DATETIME
);

CREATE TABLE props (
  id BIGINT PK, project_id BIGINT,
  name VARCHAR(128), category VARCHAR(64),
  description TEXT, scenes JSON,
  image_url VARCHAR(500), reference_image_url VARCHAR(500),
  created_at DATETIME
);
```

### 2.4 分镜与生成资产(核心)

```sql
CREATE TABLE storyboards (
  id BIGINT PK, project_id BIGINT, episode_id BIGINT,
  sequence INT, scene_number VARCHAR(16),     -- "001","002"
  short_description TEXT, detailed_description LONGTEXT,
  location_id BIGINT,                         -- 引用 scenes.id
  characters JSON,                            -- [{id, name, costumeOverride, voice}]
  dialogues JSON,                             -- [{character, text, emotion}]
  props JSON,                                 -- 道具名称/id 数组
  transition_in VARCHAR(32), transition_out VARCHAR(32),
  camera_direction VARCHAR(64), composition_guide VARCHAR(64),
  focal_point VARCHAR(128), depth_of_field VARCHAR(16),
  duration INT,                               -- 秒
  motion_script TEXT, video_script TEXT,
  start_frame_prompt TEXT, end_frame_prompt TEXT,
  sound_design VARCHAR(255), music_cue VARCHAR(255),
  notes TEXT,
  status ENUM('DRAFT','REVIEW','APPROVED','GENERATING','GENERATED','FAILED'),
  credit_cost INT,
  created_at DATETIME, updated_at DATETIME,
  INDEX idx_episode_seq (episode_id, sequence)
);

-- 统一资产表(借鉴 AIComicBuilder shot_assets)
CREATE TABLE shot_assets (
  id BIGINT PK, storyboard_id BIGINT,
  type ENUM('FIRST_FRAME','LAST_FRAME','REFERENCE','KEYFRAME_VIDEO','REFERENCE_VIDEO','AUDIO','LIPSYNC_VIDEO'),
  sequence_in_type INT,
  asset_version INT,
  is_active BOOLEAN,
  prompt TEXT,
  media_id BIGINT,                             -- 引用 media_objects(始终存自有 OSS/TOS 的 object key,禁止存第三方模型的临时 URL)
  status VARCHAR(32),
  characters JSON,
  model_provider VARCHAR(64), model_id VARCHAR(64),
  meta JSON,
  created_at DATETIME,
  INDEX idx_sb_type_active (storyboard_id, type, is_active)
);
```

### 2.5 统一媒体表(借鉴 waoowaoo MediaObject)

```sql
CREATE TABLE media_objects (
  id BIGINT PK,
  public_id VARCHAR(64) UNIQUE,       -- ULID 对外暴露
  tenant_id BIGINT, user_id BIGINT,
  storage_key VARCHAR(500),            -- OSS 对象 key
  sha256 CHAR(64),                     -- 内容哈希,去重
  mime_type VARCHAR(64),
  size_bytes BIGINT,
  width INT, height INT, duration_ms INT,
  kind ENUM('IMAGE','VIDEO','AUDIO','FILE'),
  created_at DATETIME,
  UNIQUE KEY uk_sha256_tenant (tenant_id, sha256)
);
```

### 2.6 任务与工作流

```sql
CREATE TABLE tasks (
  id BIGINT PK, task_id VARCHAR(32) UNIQUE,   -- ULID
  tenant_id BIGINT, user_id BIGINT, project_id BIGINT,
  type VARCHAR(64),                            -- SCRIPT_PARSE, IMAGE_GEN, ...
  status ENUM('QUEUED','PROCESSING','COMPLETED','FAILED','CANCELLED','DEAD'),
  payload JSON, result JSON, error TEXT,
  attempt INT DEFAULT 0, max_attempts INT DEFAULT 3,
  heartbeat_at DATETIME,
  external_id VARCHAR(128),                    -- 第三方任务 id
  idempotency_key VARCHAR(128) UNIQUE,
  freeze_id BIGINT,                            -- 关联 balance_freezes
  parent_task_id BIGINT,                       -- 子任务关系
  billing_info JSON,
  created_at DATETIME, started_at DATETIME, finished_at DATETIME,
  INDEX idx_status_type (status, type),
  INDEX idx_heartbeat (heartbeat_at)
);

CREATE TABLE task_events (
  id BIGINT PK, task_id VARCHAR(32),
  event_type VARCHAR(64), data JSON,
  created_at DATETIME
);

-- 三层 Agent 的工作流运行
CREATE TABLE graph_runs (
  id BIGINT PK, run_id VARCHAR(32) UNIQUE,
  project_id BIGINT, agent_type VARCHAR(32),   -- SCRIPT / PRODUCTION
  status VARCHAR(32), input JSON, output JSON,
  created_at DATETIME, finished_at DATETIME
);
CREATE TABLE graph_steps (
  id BIGINT PK, run_id VARCHAR(32),
  step_name VARCHAR(64), layer ENUM('DECISION','EXECUTION','SUPERVISION'),
  status VARCHAR(32), input JSON, output JSON,
  started_at DATETIME, finished_at DATETIME, retry_count INT
);
```

### 2.7 计费(借鉴 waoowaoo)

```sql
CREATE TABLE user_balances (
  user_id BIGINT PK,
  tenant_id BIGINT,
  balance DECIMAL(18,4) DEFAULT 0,
  frozen_amount DECIMAL(18,4) DEFAULT 0,
  total_spent DECIMAL(18,4) DEFAULT 0,
  updated_at DATETIME,
  version BIGINT                               -- 乐观锁
);

CREATE TABLE balance_freezes (
  id BIGINT PK, user_id BIGINT, tenant_id BIGINT,
  amount DECIMAL(18,4),
  status ENUM('PENDING','CONFIRMED','ROLLED_BACK','EXPIRED'),
  idempotency_key VARCHAR(128) UNIQUE,
  task_id VARCHAR(32),
  expires_at DATETIME, created_at DATETIME, metadata JSON
);

CREATE TABLE balance_transactions (
  id BIGINT PK, user_id BIGINT, tenant_id BIGINT,
  type ENUM('RECHARGE','CONSUME','REFUND','GIFT','ADJUST'),
  amount DECIMAL(18,4), balance_after DECIMAL(18,4),
  currency VARCHAR(8) DEFAULT 'CREDIT',
  external_order_id VARCHAR(128),
  project_id BIGINT, episode_id BIGINT, task_id VARCHAR(32),
  task_type VARCHAR(64), billing_meta JSON,
  created_at DATETIME,
  INDEX idx_user_time (user_id, created_at)
);

CREATE TABLE subscriptions (
  id BIGINT PK, tenant_id BIGINT,
  plan ENUM('FREE','PRO','TEAM','ENTERPRISE'),
  status ENUM('ACTIVE','TRIALING','PAST_DUE','CANCELED'),
  started_at DATETIME, renewed_at DATETIME, expires_at DATETIME,
  monthly_credits INT, auto_renew BOOLEAN,
  external_subscription_id VARCHAR(128)
);

CREATE TABLE usage_records (
  id BIGINT PK, tenant_id BIGINT, user_id BIGINT, project_id BIGINT, group_id BIGINT,
  type VARCHAR(32),                            -- character/scene/props/video/text/tts
  credits INT, task_id VARCHAR(32),
  created_at DATETIME,
  INDEX idx_tenant_time (tenant_id, created_at)
);
```

### 2.8 提示词(Skill)

```sql
CREATE TABLE prompt_templates (
  id BIGINT PK,
  prompt_key VARCHAR(64),                      -- script_generate, shot_split, ...
  slot_key VARCHAR(64),                        -- role_definition / output_format / NULL(全文)
  scope ENUM('GLOBAL','TENANT','PROJECT'),
  tenant_id BIGINT, project_id BIGINT,
  content LONGTEXT,
  version INT, is_active BOOLEAN,
  created_by BIGINT, created_at DATETIME,
  UNIQUE KEY (prompt_key, slot_key, scope, tenant_id, project_id, version)
);

CREATE TABLE prompt_versions (
  id BIGINT PK, template_id BIGINT, content LONGTEXT,
  version INT, created_by BIGINT, created_at DATETIME
);

CREATE TABLE prompt_presets (
  id BIGINT PK, tenant_id BIGINT, user_id BIGINT,
  name VARCHAR(128), prompt_key VARCHAR(64),
  slots JSON, is_public BOOLEAN
);
```

### 2.9 资产库(个人/团队)

```sql
CREATE TABLE global_assets (
  id BIGINT PK, tenant_id BIGINT, owner_user_id BIGINT,
  library ENUM('PERSONAL','TEAM','MARKET'),
  type ENUM('CHARACTER','SCENE','PROP','VOICE'),
  name VARCHAR(128), description TEXT,
  tags JSON, style VARCHAR(64),
  thumbnail_url VARCHAR(500),
  full_data JSON,                              -- 完整对象(角色四视图/描述等)
  favorite_count INT DEFAULT 0,
  created_at DATETIME, updated_at DATETIME,
  INDEX idx_tenant_lib_type (tenant_id, library, type)
);

CREATE TABLE asset_favorites (
  user_id BIGINT, asset_id BIGINT, PRIMARY KEY (user_id, asset_id)
);
```

### 2.10 通知与审计

```sql
CREATE TABLE notifications (
  id BIGINT PK, user_id BIGINT,
  type VARCHAR(32), title VARCHAR(255), body TEXT,
  data JSON, read_at DATETIME, created_at DATETIME
);

CREATE TABLE audit_logs (
  id BIGINT PK, tenant_id BIGINT, user_id BIGINT,
  action VARCHAR(64), resource_type VARCHAR(32), resource_id VARCHAR(64),
  ip VARCHAR(45), user_agent VARCHAR(255),
  request_id VARCHAR(64), meta JSON,
  created_at DATETIME,
  INDEX idx_tenant_time (tenant_id, created_at)
);
```

## 3. Redis Stack 向量索引设计 (RediSearch)

**双实例部署**: 业务缓存 Redis 与向量 Redis 物理分离,避免大向量拖慢热 key。

### 3.1 `agent_memory` 索引(Agent 跨会话记忆)

```
FT.CREATE idx:agent_memory
  ON HASH PREFIX 1 memory:
  SCHEMA
    project_id   TAG SEPARATOR ","
    agent_type   TAG              -- script | production
    role         TAG              -- user | assistant:decision | assistant:skeleton ...
    content      TEXT WEIGHT 1.0
    summarized   TAG              -- 0 | 1
    created_at   NUMERIC SORTABLE
    embedding    VECTOR HNSW 6
                 TYPE FLOAT32 DIM 1024
                 DISTANCE_METRIC COSINE
                 M 16 EF_CONSTRUCTION 200
```

HSET 示例:
```
HSET memory:{ulid}
  project_id P001
  agent_type script
  role "assistant:skeleton"
  content "三幕骨架..."
  summarized 0
  created_at 1713234567
  embedding <1024 float32 binary>
```

### 3.2 `asset_semantic` 索引(资产语义检索,可选)

```
FT.CREATE idx:asset_semantic
  ON HASH PREFIX 1 asset:
  SCHEMA
    tenant_id    TAG
    asset_type   TAG              -- character | scene | prop
    asset_id     NUMERIC
    embedding    VECTOR HNSW 6
                 TYPE FLOAT32 DIM 1024
                 DISTANCE_METRIC COSINE
```

### 3.3 检索示例

```
FT.SEARCH idx:agent_memory
  "(@project_id:{P001} @agent_type:{script})=>[KNN 3 @embedding $vec AS score]"
  PARAMS 2 vec <query_vec_bytes>
  SORTBY score
  DIALECT 2
```

Spring AI 的 `RedisVectorStore` 自动生成上面的 FT 语法,业务代码只调 `similaritySearch()` 即可。

### 3.4 持久化与规模

- AOF `everysec` + RDB 每小时双持久化
- HNSW 索引参数: `M=16, EF_CONSTRUCTION=200, EF_RUNTIME=50`
- 预估: 1 万活跃项目 × 500 条记忆/项目 × (1KB 文本 + 4KB 向量) ≈ **25GB 内存**
- 超过 64GB 再迁 Milvus(Spring AI VectorStore 接口兼容,零改动)

## 4. Redis 业务 Key 规范(业务缓存实例)

```
auth:token:{jti}                  # JWT 黑名单
auth:login:ip:{ip}                # 登录限流
task:dedupe:{idempotencyKey}      # 任务去重
task:lock:{taskId}                # 分布式锁
sse:project:{projectId}           # SSE 订阅通道(pub/sub)
cache:project:{id}                # 项目详情缓存 TTL 60s
cache:skill:{key}:{tenantId}      # Skill 模板缓存 TTL 300s
rate:user:{userId}:{api}          # 按用户限流
```

## 5. 索引与分区策略

- `tasks` 按 `created_at` 月分区(3~6 月前归档)
- `balance_transactions` 按 `created_at` 季分区
- `audit_logs` 按 `created_at` 月分区 + 冷热分层(S3)
- `usage_records` 同上
- `storyboards` 大项目(1000+ 分镜)按 `project_id` 哈希分库(水平扩展)
