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
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
文档更新: 已完成
代码实现: 待实施