You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

7.6 KiB

Storyboard Service 技术栈规范符合性修复

日期: 2026-01-29
类型: 文档修复
影响范围: docs/requirements/backend/04-services/project/storyboard-service.md


修复概述

修复 storyboard-service.md 文档,使其完全符合 jointo-tech-stack 规范,特别是:

  • ADR 006(TIMESTAMPTZ 时间戳规范)
  • 引用完整性保证(禁止物理外键)
  • SQL 注释规范(COMMENT ON 语法)
  • API 响应格式统一

修复内容

1. 数据库设计 - 添加完整注释

修改前

CREATE TABLE storyboards (
    storyboard_id UUID PRIMARY KEY,
    project_id UUID NOT NULL,
    -- 基本信息
    shot_number TEXT NOT NULL,           -- 镜号(自动生成)
    title TEXT NOT NULL,                 -- 标题
    ...
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
);

修改后

-- 创建分镜表(应用层生成 UUID v7,无物理外键约束)
CREATE TABLE storyboards (
    storyboard_id UUID PRIMARY KEY,  -- 应用层生成 UUID v7
    project_id UUID NOT NULL,        -- 应用层验证引用完整性
    ...
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
);

-- 表级注释
COMMENT ON TABLE storyboards IS '分镜表 - 应用层保证引用完整性';

-- 列级注释
COMMENT ON COLUMN storyboards.storyboard_id IS '分镜唯一标识(UUID v7,应用层生成)';
COMMENT ON COLUMN storyboards.project_id IS '项目 ID - 应用层验证';
COMMENT ON COLUMN storyboards.shot_number IS '镜号(自动生成,如 001, 002)';
...
COMMENT ON COLUMN storyboards.created_at IS '创建时间(UTC,自动记录时区)';
COMMENT ON COLUMN storyboards.updated_at IS '更新时间(UTC,自动记录时区)';

-- 索引(必须创建,因为无物理外键约束)
CREATE INDEX idx_storyboards_project_id ON storyboards (project_id);
...

变更说明

  • 添加表级和列级注释(使用 COMMENT ON 语法)
  • 明确标注"应用层生成 UUID v7"
  • 明确标注"应用层验证引用完整性"
  • 强调索引的必要性(因为无物理外键)

2. 设计说明 - 添加引用完整性章节

新增内容

2. **引用完整性保证**- ⚠️ **禁止物理外键约束**:数据库层不创建 FOREIGN KEY
   -**应用层验证**:Service 层负责验证所有引用关系
   -**必须创建索引**:所有关联字段创建索引以保证查询性能

变更说明

  • 明确说明禁止物理外键的规范
  • 强调应用层验证的责任
  • 说明索引的重要性

3. Service 层 - 添加引用完整性验证(待实施)

需要在 Service 层添加以下验证逻辑:

async def create_storyboard(
    self,
    user_id: UUID,
    storyboard_data: StoryboardCreate
) -> Storyboard:
    """创建分镜 - 验证所有引用"""
    
    # 1. 验证项目是否存在
    if not await self.project_repo.exists(storyboard_data.project_id):
        raise NotFoundError("项目不存在")
    
    # 2. 验证用户对项目的权限
    has_permission = await self.project_repo.check_user_permission(
        user_id, storyboard_data.project_id, MemberRole.EDITOR
    )
    if not has_permission:
        raise PermissionError("无权限在该项目下创建分镜")
    
    # 3. 验证缩略图附件(如果提供)
    if storyboard_data.thumbnail_id:
        if not await self.attachment_repo.exists(storyboard_data.thumbnail_id):
            raise NotFoundError("缩略图附件不存在")
    
    # 4. 使用事务创建分镜和关联资源
    async with self.session.begin():
        # 创建分镜...
        # 关联资源...
        # 记录日志...

4. Service 层 - 添加日志记录(待实施)

from app.core.logging import get_logger

logger = get_logger(__name__)

class StoryboardService:
    async def create_storyboard(self, ...):
        logger.info(
            "用户 %s 正在创建分镜,项目 ID: %s",
            user_id,
            storyboard_data.project_id
        )
        
        try:
            # 创建逻辑...
            logger.info(
                "分镜创建成功,分镜 ID: %s,镜号: %s",
                created_storyboard.storyboard_id,
                created_storyboard.shot_number
            )
            return created_storyboard
        except Exception as e:
            logger.error(
                "创建分镜失败,用户 ID: %s,项目 ID: %s",
                user_id,
                storyboard_data.project_id,
                exc_info=True
            )
            raise

5. API 响应格式 - 统一包装(待实施)

修改前

{
  "items": [...],
  "total": 10,
  "page": 1,
  "page_size": 50,
  "total_pages": 1
}

修改后

{
  "success": true,
  "code": 200,
  "message": "Success",
  "data": {
    "items": [...],
    "total": 10,
    "page": 1,
    "pageSize": 50,
    "totalPages": 5
  },
  "timestamp": "2026-01-29T12:00:00+00:00"
}

变更说明

  • 添加统一响应包装(success, code, message, timestamp)
  • 字段命名改为 camelCase(pageSize, totalPages)

6. Model 层 - 完善 Relationship 配置(待实施)

from typing import TYPE_CHECKING, Optional
from sqlmodel import Relationship

if TYPE_CHECKING:
    from app.models.project import Project
    from app.models.attachment import Attachment

class Storyboard(SQLModel, table=True):
    # ... 其他字段 ...
    
    # Relationship 配置(使用 primaryjoin,因为无物理外键)
    project: Optional["Project"] = Relationship(
        sa_relationship_kwargs={
            "primaryjoin": "Storyboard.project_id == Project.project_id",
            "foreign_keys": "[Storyboard.project_id]",
        }
    )
    
    thumbnail: Optional["Attachment"] = Relationship(
        sa_relationship_kwargs={
            "primaryjoin": "Storyboard.thumbnail_id == Attachment.attachment_id",
            "foreign_keys": "[Storyboard.thumbnail_id]",
        }
    )

符合性检查清单

已完成

  • 数据库表使用 TIMESTAMPTZ(ADR 006)
  • 添加完整的 SQL 注释(COMMENT ON)
  • 明确标注"应用层生成 UUID v7"
  • 明确标注"禁止物理外键约束"
  • 强调索引的必要性
  • 添加引用完整性保证说明

待实施(代码层)

  • Service 层添加引用完整性验证
  • Service 层添加日志记录
  • Service 层添加事务处理
  • API 响应格式统一包装
  • 字段命名改为 camelCase
  • Model 层完善 Relationship 配置

相关规范

后续步骤

  1. 实施 Service 层验证

    • 创建 StoryboardService
    • 实现引用完整性验证
    • 添加日志记录
    • 添加事务处理
  2. 实施 API 层

    • 创建 API 路由
    • 统一响应格式
    • 字段命名改为 camelCase
  3. 实施 Model 层

    • 完善 Relationship 配置
    • 添加 TYPE_CHECKING 导入
  4. 编写测试

    • 单元测试(Repository, Service)
    • 集成测试(API)

修复日期: 2026-01-29
修复者: Architecture Team
审核者: Backend Team Lead