# 设计文档审查意见

> 审查日期: 2026-04-16
> 审查范围: design-docs 目录下全部15份文档

---

## 1. 整体评价

文档体系**非常成熟**，覆盖了从产品定位、技术架构、数据库设计、API规范到部署运维的完整链路。吸收三个开源项目（Toonflow、AIComicBuilder、waoowaoo）的策略清晰，有明确的技术选型和分阶段实施路线图。

**优势**：
- 架构设计完整，三层Agent、异步任务、计费系统设计合理
- 技术选型务实（Spring AI Alibaba + Redis Stack + RabbitMQ）
- 实施路线图清晰，每个里程碑都有明确的完成标准
- 吸收开源项目策略明确，避免了重复造轮子

**待改进**：主要问题集中在原型与文档不同步、部分TODO需要明确实现时机、细节补充等方面。

---

## 2. 需要修正/补充的问题

### 2.1 数据不一致问题

| 文档 | 原型实际情况 | 需要修正 |
|------|------------|---------|
| **doc 02 风格数量** | 写6种 | 原型截图显示9+种（2D日漫/韩漫都市/3D国风/仙侠/韩漫/乙女/国漫/CG/卡通...），需更新 |
| **doc 11 购买页时机** | M7阶段 | 原型已有购买页（pic 32），可提前到M1或M2阶段 |

### 2.2 API补充

**doc 07** 需要补充以下API（原型已实现但文档缺失）：

```http
# 子账号管理（来自原型pic 5）
GET    /admin/accounts
POST   /admin/accounts
PATCH  /admin/accounts/:id/authorize
DELETE /admin/accounts/:id

# 资产批量操作
POST   /assets/batch-clone
POST   /assets/batch-delete

# 资源包购买
POST   /billing/packages/purchase  # 10万/100万/1000万档位
GET    /billing/packages           # 资源包列表
POST   /billing/coupons/apply      # 代金券使用
```

### 2.3 数据库设计遗漏

**doc 06** 可能需要补充的表结构：

```sql
-- 子账号表（原型有子账号授权登录机制）
CREATE TABLE sub_accounts (
  id BIGINT PK,
  tenant_id BIGINT,
  parent_user_id BIGINT,
  username VARCHAR(64),
  password_hash VARCHAR(100),
  permissions JSON,           -- 允许的操作范围
  status ENUM('ACTIVE','DISABLED'),
  created_at DATETIME
);

-- 资源包表（原型有10万/100万/1000万档位）
CREATE TABLE credit_packages (
  id BIGINT PK,
  name VARCHAR(64),
  credits INT,
  price DECIMAL(10,2),
  currency VARCHAR(8) DEFAULT 'CNY',
  is_active BOOLEAN,
  sort_order INT
);

-- 订单表（购买记录）
CREATE TABLE orders (
  id BIGINT PK,
  tenant_id BIGINT,
  user_id BIGINT,
  type ENUM('RECHARGE','PACKAGE','SUBSCRIPTION'),
  amount DECIMAL(10,2),
  credits INT,
  payment_provider VARCHAR(32),
  external_order_id VARCHAR(128),
  status ENUM('PENDING','PAID','FAILED','REFUNDED'),
  paid_at DATETIME,
  created_at DATETIME
);
```

### 2.4 MinIO实现TODO

**doc 05** 和 **doc 10** 都提到MinIO作为TODO备选，但本地开发可能需要完全离线环境。

**建议**：在 M0 阶段明确实现 MinIOProvider

```java
// yaoai-storage 模块新增实现
@Component
@ConditionalOnProperty(name = "yaoai.storage.provider", havingValue = "minio")
public class MinioProvider implements StorageProvider {

    @Override
    public String getProviderId() {
        return "minio";
    }

    @Override
    public void upload(String key, InputStream data, String contentType, long size) {
        // 使用 MinIO Java SDK
        minioClient.putObject(
            PutObjectArgs.builder()
                .bucket(bucketName)
                .object(key)
                .stream(data, size, -1)
                .contentType(contentType)
                .build()
        );
    }

    // 实现其他方法...
}
```

并在 **doc 10** 的 Docker Compose 中补充 MinIO 服务：

```yaml
minio:
  image: minio/minio:latest
  command: server /data --console-address ":9001"
  ports: ["9000:9000", "9001:9001"]
  environment:
    MINIO_ROOT_USER: minioadmin
    MINIO_ROOT_PASSWORD: minioadmin123
  volumes: ["minio_data:/data"]
```

### 2.5 前端状态管理迁移计划

**doc 02** 建议用 Zustand + TanStack Query，但原型目前是纯 useState。建议在 **doc 11** 的 M0.8 中明确迁移步骤：

```
M0.8 前端接入层改造（补充）
- [ ] 建立 src/stores/ 目录（Zustand store）
  - [ ] src/stores/authStore.ts
  - [ ] src/stores/projectStore.ts
  - [ ] src/stores/uiStore.ts
- [ ] 建立 src/services/api/ 目录（Axios封装）
  - [ ] src/services/api/client.ts（baseURL + token拦截器）
  - [ ] src/services/api/projects.ts
  - [ ] src/services/api/characters.ts
  - [ ] ...
- [ ] 引入 @tanstack/react-query
  - [ ] 建立 src/hooks/useQuery.ts 封装
  - [ ] 建立 src/hooks/useMutation.ts 封装
- [ ] 逐页迁移：
  - [ ] Dashboard（去mock，接入真实API）
  - [ ] NewProject
  - [ ] StyleSelection
  - [ ] ...
```

### 2.6 模型配置页截图

**doc 14** 提到 ModelSettings 已新增，但截图未包含。建议补充以下截图：

```
需补充截图：
- 语言模型配置页
  - 模型选择器：通义千问 Max / Plus / Turbo
  - API Key 配置
  - 默认模型设置
- 图片模型配置页
  - 模型选择器：通义万相 2.1 / DALL-E 3 / Gemini Imagen
  - 供应商管理（添加/编辑/删除）
- 视频模型配置页
  - 模型选择器：通义万相视频 / Seedance 2.0 / Kling v3
  - 时长选项（5s/8s/15s）
  - 质量选项（draft/standard/high）
- 供应商管理弹窗
  - 名称
  - 协议（OpenAI兼容/DashScope/Seedance/Kling）
  - Base URL
  - API Key / Secret Key
  - 模型列表（手动添加/自动获取）
```

---

## 3. 优化建议

### 3.1 数据库索引优化

**doc 06** 的表建议补充复合索引以提高查询性能：

```sql
-- tasks 表补充索引
CREATE INDEX idx_tenant_status_type ON tasks(tenant_id, status, type);
CREATE INDEX idx_project_status ON tasks(project_id, status);
CREATE INDEX idx_user_created ON tasks(user_id, created_at DESC);

-- balance_freezes 表补充索引
CREATE INDEX idx_freeze_expires ON balance_freezes(expires_at, status);
CREATE INDEX idx_freeze_idempotency ON balance_freezes(idempotency_key);

-- storyboards 表补充索引
CREATE INDEX idx_episode_seq ON storyboards(episode_id, sequence);
CREATE INDEX idx_episode_status ON storyboards(episode_id, status);

-- usage_records 表补充索引
CREATE INDEX idx_usage_tenant_type_time ON usage_records(tenant_id, type, created_at DESC);

-- audit_logs 表补充索引
CREATE INDEX idx_audit_tenant_time ON audit_logs(tenant_id, created_at DESC);
CREATE INDEX idx_audit_user_action ON audit_logs(user_id, action, created_at DESC);

-- global_assets 表补充索引
CREATE INDEX idx_asset_lib_type ON global_assets(library, type, created_at DESC);
CREATE INDEX idx_asset_tags ON global_assets(tags(50));  -- MySQL 5.7+ 支持JSON索引
```

### 3.2 计费积分精度问题

**doc 01** 计费单位用积分（如视频生成50积分），TTS为"0.1 积分/字符"。但 **doc 06** 的 `user_balances` 用 `DECIMAL(18,4)`。

**建议**：明确积分精度规则

```java
// 积分精度统一为 2 位小数
// 最小单位：0.01 积分

// 计费表调整
user_balances.balance        DECIMAL(10,2)  -- 足够存储：999,999.99 积分
balance_freezes.amount      DECIMAL(10,2)
balance_transactions.amount DECIMAL(10,2)

// 成本估算调整
SCRIPT_PARSE      -> 5.00 积分
OUTLINE_GEN       -> 10.00 积分
CHARACTER_IMAGE   -> 20.00 积分
SCENE_IMAGE       -> 10.00 积分
FRAME_GEN         -> 15.00 积分
TTS               -> 0.01 积分/字符
```

### 3.3 SSE连接数管理

**doc 08** 用SSE推送任务进度，但未考虑连接数管理。建议补充：

```java
// doc 08 新增章节: SSE连接管理

@Service
public class SseConnectionManager {
    // 1. 单用户最多3个SSE连接（防止多标签页累积）
    private final Map<Long, Set<SseEmitter>> userConnections = new ConcurrentHashMap<>();

    public boolean canAddConnection(Long userId) {
        return userConnections.getOrDefault(userId, Set.of()).size() < 3;
    }

    // 2. 连接超时自动关闭（5分钟无心跳）
    @Scheduled(fixedRate = 60000)
    public void cleanStaleConnections() {
        // 检查最后心跳时间，超时则关闭
    }

    // 3. Redis存储活跃SSE连接，用于断线重连
    public void registerConnection(Long userId, String connectionId, SseEmitter emitter) {
        redisTemplate.opsForValue().set(
            "sse:connection:" + connectionId,
            userId.toString(),
            Duration.ofMinutes(5)
        );
    }
}

// API层添加限制
@GetMapping("/projects/{id}/events")
public Flux<ServerSentEvent<String>> streamProjectEvents(
        @PathVariable Long id,
        @AuthenticationPrincipal UserPrincipal user) {
    if (!sseManager.canAddConnection(user.getId())) {
        throw new TooManyConnectionsException("超过SSE连接数限制");
    }
    // ...
}
```

### 3.4 视频生成并发控制

**doc 08** 的队列配置视频并发=10，但实际可能需要更细粒度控制：

```java
// doc 08 补充任务优先级和并发控制

// 1. 租户级配额配置
@Data
@TableName("tenant_quotas")
public class TenantQuota {
    @TableId
    Long tenantId;
    Integer maxConcurrentVideoTasks;  // PRO=5, TEAM=20
    Integer maxConcurrentImageTasks;  // PRO=10, TEAM=50
}

// 2. 用户级并发控制
@Service
public class ConcurrencyGate {
    public boolean canSubmitTask(Long userId, Long tenantId, TaskType type) {
        String key = String.format("concurrent:%s:%s:%s", tenantId, userId, type);
        Long current = redisTemplate.opsForValue().increment(key);
        if (current > getQuota(tenantId, type)) {
            redisTemplate.opsForValue().decrement(key);
            return false;
        }
        redisTemplate.expire(key, Duration.ofMinutes(30));  // 30分钟超时
        return true;
    }

    public void releaseTask(Long userId, Long tenantId, TaskType type) {
        String key = String.format("concurrent:%s:%s:%s", tenantId, userId, type);
        redisTemplate.opsForValue().decrement(key);
    }
}

// 3. 队列按优先级路由
// video.generate.high    (VIP用户)
// video.generate.normal  (默认)
// video.generate.low     (免费用户，降级处理）
```

### 3.5 通知系统细化

**doc 08** 提到通知系统但细节不足。建议补充：

```java
// doc 08 补充通知类型定义

public enum NotificationType {
    // 任务相关
    TASK_COMPLETED("任务完成", "您的任务已完成"),
    TASK_FAILED("任务失败", "任务执行失败"),
    TASK_TIMEOUT("任务超时", "任务执行超时"),

    // 计费相关
    BALANCE_LOW("余额不足", "您的积分余额不足"),
    PAYMENT_SUCCESS("支付成功", "充值成功"),
    SUBSCRIPTION_EXPIRED("订阅到期", "订阅即将到期"),

    // 项目相关
    PROJECT_INVITATION("项目邀请", "您被邀请加入项目"),
    PROJECT_ARCHIVED("项目归档", "项目已归档"),

    // 协作相关
    MEMBER_JOINED("成员加入", "有新成员加入项目"),
    COMMENT_MENTION("提及你", "有人提到了你"),

    // 系统相关
    SYSTEM_MAINTENANCE("系统维护", "系统即将维护"),
    SYSTEM_ANNOUNCEMENT("系统公告", "系统公告");
}

// 通知渠道
public enum NotificationChannel {
    WEB,       // 站内通知
    EMAIL,     // 邮件
    SMS,       // 短信
    WEBPUSH,   // 浏览器推送
    WECHAT     // 微信推送（可选）
}

// 用户通知偏好设置
@Data
@TableName("notification_preferences")
public class NotificationPreference {
    Long userId;
    NotificationType type;
    Map<NotificationChannel, Boolean> channels;  // {WEB: true, EMAIL: false}
}
```

### 3.6 前端错误边界

**doc 02** 技术债务提到无错误边界，建议在 **M0阶段** 补充：

```
M0.8 补充任务：
- [ ] 安装依赖
  npm install react-error-boundary

- [ ] 创建错误边界组件
  // src/components/ErrorBoundary.tsx
  import { ErrorBoundary } from 'react-error-boundary';

  function FallbackComponent({ error, resetErrorBoundary }) {
    return (
      <div role="alert" className="p-4 bg-red-50 text-red-800 rounded">
        <p className="font-bold">出现错误</p>
        <pre className="mt-2 text-sm">{error.message}</pre>
        <button
          onClick={resetErrorBoundary}
          className="mt-2 px-4 py-2 bg-red-600 text-white rounded"
        >
          重试
        </button>
      </div>
    );
  }

  export function AppErrorBoundary({ children }) {
    return (
      <ErrorBoundary FallbackComponent={FallbackComponent}>
        {children}
      </ErrorBoundary>
    );
  }

- [ ] 在 App.tsx 根节点包裹
  <AppErrorBoundary>
    <Router>
      {/* ... */}
    </Router>
  </AppErrorBoundary>

- [ ] 分页错误边界
  - StoryboardWorkspace（视频生成可能出错）
  - VideoGeneration（播放器可能出错）
```

### 3.7 API版本化

**doc 07** BaseURL 用 `/v1` 但未说明版本策略。建议补充：

```http
# API版本化策略

## 版本规则
- 主版本号：breaking change（/v1, /v2）
- 次版本号：新增特性，向后兼容（/v1.1）
- 补丁号：bug修复（/v1.1.2）

## 版本路径
https://api.yaoai.com/v1/...
https://api.yaoai.com/v2/...

## 当前版本
- 稳定版：/v1
- 实验性API：/v1.0.2-beta

## 废弃策略
- 废弃API保留至少2个大版本
- 响应头标示：X-API-Deprecated: true
- 废弃API返回警告：Warning: Deprecated, use /v2/xxx instead

## OpenAPI自动区分版本
- springdoc-openapi 按版本分组
- /v3/api-docs/v1
- /v3/api-docs/v1.1.2-beta
```

### 3.8 文档同步机制

**doc 14** 已发现多个需要更新的文档，建议建立变更追踪：

```markdown
# DESIGN_DOCS_CHANGELOG.md

## 2026-04-16

### 同步原型差异
- **doc 02**: 风格数量 6 → 9+ 种（来自原型 pic 7）
- **doc 02**: 补充积分购买页描述（原型 pic 32）
- **doc 02**: 补充子账号管理详情（原型 pic 5）
- **doc 07**: 补充子账号 CRUD API
- **doc 07**: 补充资源包购买 API
- **doc 07**: 补充资产批量操作 API
- **doc 09**: 购买页提前（M7 → M1）
- **doc 11**: 调整里程碑时间线

### 补充遗漏内容
- **doc 06**: 补充 sub_accounts 表
- **doc 06**: 补充 credit_packages 表
- **doc 06**: 补充 orders 表
- **doc 08**: 补充 SSE 连接管理
- **doc 08**: 补充视频生成并发控制

### 待确认项
- **doc 14**: ModelSettings 页截图（待补充）
- **doc 02**: TTS 音色选择交互细节（待确认）
- **doc 02**: 角色服装变体 UI 位置（待确认）
```

---

## 4. 可选增强功能

### 4.1 角色四视图的变体机制

原型中角色有服装变体（如"林若曦·白色晚礼服"），建议在数据库设计中明确用途：

```sql
-- 建议明确 character_appearances 的使用场景

CREATE TABLE character_appearances (
  id BIGINT PK,
  character_id BIGINT,
  name VARCHAR(128),              -- 变体名称（如"白色晚礼服"、"战损"）
  type ENUM('COSTUME','STATE','AGE'),  -- 变体类型
  description TEXT,
  prompt TEXT,                     -- 生成提示词
  image_url VARCHAR(500),          -- 变体图片
  turnaround_image_url VARCHAR(500), -- 变体四视图
  version INT,
  is_active BOOLEAN,
  created_at DATETIME
);

-- 变体类型说明
-- COSTUME: 服装变体（常服/礼服/战袍/校服等）
-- STATE: 状态变体（受伤/愤怒/正常等）
-- AGE: 年龄变体（少年/成年/老年等）
```

### 4.2 视频质量等级

原型中有模型选择（Doubao-Seedance-2.0 / fast），建议在后端支持质量分级：

```java
// doc 05 新增 VideoQuality 枚举

public enum VideoQuality {
    DRAFT("快速预览", 720, 5, 1.0, 30),      // 5s, 最低质量
    STANDARD("标准质量", 1080, 8, 1.0, 50),  // 8s, 标准质量
    HIGH("高清质量", 1080, 15, 1.5, 120),   // 15s, 高质量

    // 参数：名称、分辨率、时长、倍数倍率、积分基数
    private final String name;
    private final int resolution;
    private final int duration;
    private final double rateMultiplier;
    private final int baseCredits;

    public int calculateCredits() {
        return (int) (baseCredits * rateMultiplier);
    }
}

// 前端用户选择
<select>
  <option value="DRAFT">快速预览 (5s, 30积分)</option>
  <option value="STANDARD" selected>标准质量 (8s, 50积分)</option>
  <option value="HIGH">高清质量 (15s, 120积分)</option>
</select>
```

### 4.3 任务优先级队列

重要用户/付费用户任务应该优先处理：

```java
// doc 08 补充任务优先级

public enum TaskPriority {
    HIGH,   // VIP用户、紧急任务
    NORMAL, // 默认优先级
    LOW     // 免费用户、非紧急任务
}

// RabbitMQ 按优先级路由
@RabbitListener(queues = "video.generate.high")
public void handleHighPriorityVideo(String taskId) { /* 优先处理 */ }

@RabbitListener(queues = "video.generate.normal")
public void handleNormalPriorityVideo(String taskId) { /* 正常处理 */ }

@RabbitListener(queues = "video.generate.low")
public void handleLowPriorityVideo(String taskId) { /* 降级处理 */ }

// 提交任务时根据租户套餐选择队列
String queueName = switch (tenant.getPlan()) {
    case ENTERPRISE -> "video.generate.high";
    case TEAM, PRO -> "video.generate.normal";
    case FREE -> "video.generate.low";
};
```

### 4.4 AI模型热切换

当前设计需要重启服务才能切换模型，建议支持运行时热切换：

```java
// doc 05 补充模型热切换

@Service
public class ModelConfigService {
    @Autowired RedisTemplate<String, String> redis;

    // 运行时更新模型配置
    public void updateModelConfig(String providerId, ModelConfig config) {
        String key = String.format("model:config:%s", providerId);
        redis.opsForValue().set(key, JsonUtils.toJson(config), Duration.ofHours(1));
    }

    // 读取模型配置（缓存优先）
    public ModelConfig getModelConfig(String providerId) {
        String key = String.format("model:config:%s", providerId);
        String cached = redis.opsForValue().get(key);
        if (cached != null) return JsonUtils.fromJson(cached, ModelConfig.class);
        return loadFromDatabase(providerId);
    }
}

// 前端实时更新模型配置
// 无需重启服务，立即生效
```

### 4.5 项目模板系统

为常见短剧类型提供预设模板：

```sql
-- 项目模板表
CREATE TABLE project_templates (
  id BIGINT PK,
  name VARCHAR(128),           -- "都市甜宠"、"仙侠修仙"
  category VARCHAR(64),        -- 都市/仙侠/古装/悬疑
  description TEXT,
  art_style VARCHAR(64),       -- 默认艺术风格
  default_characters JSON,      -- 预设角色
  default_scenes JSON,         -- 预设场景
  default_props JSON,           -- 预设道具
  thumbnail_url VARCHAR(500),
  is_public BOOLEAN,
  created_at DATETIME
);

-- API
GET /templates               -- 模板列表
GET /templates/:id           -- 模板详情
POST /projects/from-template -- 从模板创建项目
```

---

## 5. 总结

### 5.1 需要立即处理（M1前完成）

1. **doc 02** 更新风格数量（6→9+）
2. **doc 07** 补充子账号管理API、购买API
3. **doc 06** 补充数据库表（sub_accounts、credit_packages、orders）
4. **doc 11** 调整购买页时机（M7→M1）
5. **doc 10** 补充MinIO Docker Compose配置

### 5.2 建议在M0阶段完成

1. 实现MinIOProvider（支持本地开发离线环境）
2. 前端建立Zustand + TanStack Query脚手架
3. 补充前端错误边界组件
4. 数据库补充复合索引

### 5.3 可以延后处理（M2+）

1. SSE连接数管理
2. 视频生成并发控制
3. 通知系统细化
4. API版本化
5. 任务优先级队列

### 5.4 可选增强（v2+）

1. 角色四视图变体机制细化
2. 视频质量等级
3. AI模型热切换
4. 项目模板系统

---

## 附录：文档同步清单

| 文档 | 需要同步的内容 | 优先级 |
|------|--------------|-------|
| doc 02 | 风格数量、购买页、子账号管理 | P0 |
| doc 06 | 数据库表补充、复合索引 | P0 |
| doc 07 | API补充 | P0 |
| doc 08 | SSE管理、并发控制 | P1 |
| doc 09 | 购买页提前 | P0 |
| doc 10 | MinIO配置 | P0 |
| doc 11 | 前端迁移计划细化 | P1 |

---

*审查完成时间: 2026-04-16*
