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.
 

8.3 KiB

Changelog: 剧本管理服务枚举类型重构

日期: 2026-01-22
类型: 重构
影响范围: 剧本管理模块(screenplays, screenplay_characters, screenplay_scenes, screenplay_props)


概述

将剧本管理服务中所有 PostgreSQL ENUM 类型改为 SMALLINT + Python IntEnum 实现方式,与项目模块保持架构一致性。

变更内容

数据库层

移除的 ENUM 类型

-- 移除以下 ENUM 类型定义
DROP TYPE IF EXISTS screenplay_type;
DROP TYPE IF EXISTS screenplay_status;
DROP TYPE IF EXISTS character_role_type;
DROP TYPE IF EXISTS time_of_day_type;
DROP TYPE IF EXISTS prop_importance_type;

改为 SMALLINT 字段

表名 字段名 旧类型 新类型 默认值
screenplays type screenplay_type SMALLINT 1
screenplays status screenplay_status SMALLINT 1
screenplay_characters role_type character_role_type SMALLINT 2
screenplay_scenes time_of_day time_of_day_type SMALLINT NULL
screenplay_props importance prop_importance_type SMALLINT 2

添加列注释

COMMENT ON COLUMN screenplays.type IS '剧本类型:1=text(文本), 2=file(文件)';
COMMENT ON COLUMN screenplays.status IS '剧本状态:1=draft(草稿), 2=review(审核中), 3=approved(已批准), 4=archived(已归档)';
COMMENT ON COLUMN screenplay_characters.role_type IS '角色类型:1=main(主角), 2=supporting(配角), 3=extra(群演)';
COMMENT ON COLUMN screenplay_scenes.time_of_day IS '时间段:1=dawn(黎明), 2=morning(上午), 3=noon(中午), 4=afternoon(下午), 5=dusk(黄昏), 6=night(夜晚)';
COMMENT ON COLUMN screenplay_props.importance IS '重要性:1=key(关键道具), 2=normal(普通道具), 3=background(背景道具)';

Python 模型层

新增 IntEnum 类

from enum import IntEnum

class ScreenplayType(IntEnum):
    TEXT = 1
    FILE = 2
    
    @classmethod
    def from_string(cls, value: str) -> int:
        mapping = {'text': cls.TEXT, 'file': cls.FILE}
        return mapping.get(value.lower(), cls.TEXT)
    
    @classmethod
    def to_string(cls, value: int) -> str:
        mapping = {cls.TEXT: 'text', cls.FILE: 'file'}
        return mapping.get(value, 'text')


class ScreenplayStatus(IntEnum):
    DRAFT = 1
    REVIEW = 2
    APPROVED = 3
    ARCHIVED = 4
    
    @classmethod
    def from_string(cls, value: str) -> int:
        mapping = {
            'draft': cls.DRAFT,
            'review': cls.REVIEW,
            'approved': cls.APPROVED,
            'archived': cls.ARCHIVED
        }
        return mapping.get(value.lower(), cls.DRAFT)
    
    @classmethod
    def to_string(cls, value: int) -> str:
        mapping = {
            cls.DRAFT: 'draft',
            cls.REVIEW: 'review',
            cls.APPROVED: 'approved',
            cls.ARCHIVED: 'archived'
        }
        return mapping.get(value, 'draft')


class CharacterRoleType(IntEnum):
    MAIN = 1
    SUPPORTING = 2
    EXTRA = 3
    
    @classmethod
    def from_string(cls, value: str) -> int:
        mapping = {'main': cls.MAIN, 'supporting': cls.SUPPORTING, 'extra': cls.EXTRA}
        return mapping.get(value.lower(), cls.SUPPORTING)
    
    @classmethod
    def to_string(cls, value: int) -> str:
        mapping = {cls.MAIN: 'main', cls.SUPPORTING: 'supporting', cls.EXTRA: 'extra'}
        return mapping.get(value, 'supporting')


class TimeOfDay(IntEnum):
    DAWN = 1
    MORNING = 2
    NOON = 3
    AFTERNOON = 4
    DUSK = 5
    NIGHT = 6
    
    @classmethod
    def from_string(cls, value: str) -> int:
        mapping = {
            'dawn': cls.DAWN,
            'morning': cls.MORNING,
            'noon': cls.NOON,
            'afternoon': cls.AFTERNOON,
            'dusk': cls.DUSK,
            'night': cls.NIGHT
        }
        return mapping.get(value.lower(), cls.MORNING)
    
    @classmethod
    def to_string(cls, value: int) -> str:
        mapping = {
            cls.DAWN: 'dawn',
            cls.MORNING: 'morning',
            cls.NOON: 'noon',
            cls.AFTERNOON: 'afternoon',
            cls.DUSK: 'dusk',
            cls.NIGHT: 'night'
        }
        return mapping.get(value, 'morning')


class PropImportance(IntEnum):
    KEY = 1
    NORMAL = 2
    BACKGROUND = 3
    
    @classmethod
    def from_string(cls, value: str) -> int:
        mapping = {'key': cls.KEY, 'normal': cls.NORMAL, 'background': cls.BACKGROUND}
        return mapping.get(value.lower(), cls.NORMAL)
    
    @classmethod
    def to_string(cls, value: int) -> str:
        mapping = {cls.KEY: 'key', cls.NORMAL: 'normal', cls.BACKGROUND: 'background'}
        return mapping.get(value, 'normal')

模型字段更新

# 修改前
type = Column(Enum(ScreenplayType), nullable=False)
status = Column(Enum(ScreenplayStatus), default=ScreenplayStatus.DRAFT)

# 修改后
type = Column(SmallInteger, nullable=False)
status = Column(SmallInteger, nullable=False, default=ScreenplayStatus.DRAFT)

# 添加 property 方法
@property
def type_str(self) -> str:
    return ScreenplayType.to_string(self.type)

@property
def status_str(self) -> str:
    return ScreenplayStatus.to_string(self.status)

API 层保持兼容

API 接口继续使用字符串,对外无影响:

# Schema 层
class ScreenplayCreate(BaseModel):
    type: str = Field(..., pattern="^(text|file)$")
    status: str = Field(default="draft", pattern="^(draft|review|approved|archived)$")

# Response 层
class ScreenplayResponse(BaseModel):
    type: str  # 返回 'text' 或 'file'
    status: str  # 返回 'draft', 'review', 'approved', 'archived'
    
    @classmethod
    def from_orm(cls, obj):
        return cls(
            type=obj.type_str,  # 使用 property 方法
            status=obj.status_str,
            ...
        )

枚举值映射表

枚举类型 数值 字符串值 说明
screenplay_type 1 text 文本剧本
2 file 文件剧本
screenplay_status 1 draft 草稿
2 review 审核中
3 approved 已批准
4 archived 已归档
character_role_type 1 main 主角
2 supporting 配角
3 extra 群演
time_of_day 1 dawn 黎明
2 morning 上午
3 noon 中午
4 afternoon 下午
5 dusk 黄昏
6 night 夜晚
prop_importance 1 key 关键道具
2 normal 普通道具
3 background 背景道具

优势

1. 架构一致性

  • 与项目模块(ProjectType, ProjectStatus, FolderCategory)保持一致
  • 统一的代码模式,降低维护成本
  • 新开发者更容易理解

2. 简化数据库

  • 无需 ENUM 类型定义
  • 字段类型统一为 SMALLINT
  • 迁移脚本更简洁

3. 扩展灵活

  • 添加新枚举值无需 ALTER TYPE
  • 可以动态扩展(如添加新的剧本类型、状态)
  • 便于版本管理

4. 性能提升

  • SMALLINT 比 TEXT 更高效(2 字节 vs 可变长度)
  • 索引性能更好
  • 存储空间更小

5. 跨数据库兼容

  • SMALLINT 是标准 SQL 类型
  • 便于迁移到其他数据库
  • 减少数据库特定依赖

6. 向后兼容

  • API 接口保持不变
  • 对外仍使用字符串
  • 客户端无需修改

影响评估

Breaking Changes

  • 无破坏性变更(API 接口保持不变)

数据库迁移

  • ⚠️ 需要数据迁移脚本(如果已有数据)
  • ⚠️ 开发环境可以删除重建
  • ⚠️ 生产环境需要编写迁移脚本

代码变更

  • 模型层:使用 IntEnum 和 SmallInteger
  • Service 层:添加枚举转换逻辑
  • Schema 层:保持使用字符串,添加验证器
  • Repository 层:查询时使用数字

后续工作

  • 创建数据库迁移脚本
  • 实现 Python 模型层
  • 更新 Service 层转换逻辑
  • 更新 Schema 层验证器
  • 编写单元测试
  • 编写集成测试
  • 更新 API 文档

相关文档


变更完成时间: 2026-01-22
文档更新: 已完成
代码实现: 待实施