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.4 KiB
7.4 KiB
Changelog: 文件夹枚举字段重构为 SMALLINT
日期: 2026-01-23
类型: 重构
影响范围: 后端 - 文件夹模块
关联 RFC: RFC 133
变更概述
将文件夹相关的枚举字段从 PostgreSQL ENUM 类型重构为 SMALLINT 类型,提升性能和扩展性。
变更内容
1. 数据库层
folder_members 表
变更前:
role member_role NOT NULL DEFAULT 'viewer'
-- PostgreSQL ENUM: 'owner', 'editor', 'viewer'
变更后:
role SMALLINT NOT NULL DEFAULT 3 CHECK (role IN (1, 2, 3))
-- 1: owner, 2: editor, 3: viewer
新增表
folder_export_jobs (导出任务表):
status SMALLINT NOT NULL DEFAULT 1 CHECK (status IN (1, 2, 3, 4, 5))
-- 1: pending, 2: processing, 3: completed, 4: failed, 5: cancelled
folder_shares (分享表):
role SMALLINT CHECK (role IN (1, 2, 3))
access_level SMALLINT CHECK (access_level IN (1, 2, 3))
2. 代码层
Model 层 (app/models/folder.py)
变更前:
class MemberRole(str, Enum):
OWNER = "owner"
EDITOR = "editor"
VIEWER = "viewer"
role: MemberRole = Field(
sa_column=Column(SQLEnum(MemberRole, name="member_role"), ...)
)
变更后:
class MemberRole(IntEnum):
OWNER = 1
EDITOR = 2
VIEWER = 3
@classmethod
def from_string(cls, value: str) -> "MemberRole":
"""从字符串转换"""
mapping = {"owner": cls.OWNER, "editor": cls.EDITOR, "viewer": cls.VIEWER}
return mapping.get(value.lower(), cls.VIEWER)
def to_string(self) -> str:
"""转换为字符串"""
mapping = {self.OWNER: "owner", self.EDITOR: "editor", self.VIEWER: "viewer"}
return mapping[self]
role: int = Field(
sa_column=Column(SmallInteger, nullable=False, default=MemberRole.VIEWER)
)
新增 ExportStatus IntEnum:
class ExportStatus(IntEnum):
PENDING = 1
PROCESSING = 2
COMPLETED = 3
FAILED = 4
CANCELLED = 5
Schema 层 (app/schemas/folder.py)
保持 API 层面使用字符串枚举,添加转换逻辑:
class FolderMemberCreate(BaseModel):
role: MemberRoleEnum = Field(default=MemberRoleEnum.VIEWER)
@field_validator('role', mode='before')
@classmethod
def convert_role_to_int(cls, v):
"""API 输入:字符串 → 整数"""
from app.models.folder import MemberRole
if isinstance(v, str):
return MemberRole.from_string(v).value
return v
class FolderMemberResponse(BaseModel):
role: str
@field_serializer('role')
def serialize_role(self, value: int) -> str:
"""API 输出:整数 → 字符串"""
from app.models.folder import MemberRole
if isinstance(value, int):
return MemberRole(value).to_string()
return value
Service 层 (app/services/folder_service.py)
简化角色映射逻辑:
变更前:
role_map = {
'viewer': MemberRole.VIEWER,
'editor': MemberRole.EDITOR,
'owner': MemberRole.OWNER
}
role_enum = role_map.get(role, MemberRole.VIEWER)
变更后:
role_enum = MemberRole.from_string(role)
3. 数据库迁移
创建迁移脚本 006_folder_enum_to_smallint.py:
- 删除旧的 PostgreSQL ENUM 类型
- 重建
folder_members表(使用 SMALLINT) - 创建
folder_export_jobs表 - 创建
folder_shares表
优势
- 性能提升: SMALLINT 查询和索引性能优于 ENUM
- 易于扩展: 添加新值无需 ALTER TYPE,只需更新 CHECK 约束
- 一致性: 与
folder_category等字段保持统一 - 向后兼容: API 层仍使用字符串,客户端无感知
- 类型安全: Python IntEnum 提供编译时类型检查
枚举值映射表
MemberRole(成员角色)
| 值 | 名称 | 字符串 | 说明 |
|---|---|---|---|
| 1 | OWNER | "owner" | 所有者 |
| 2 | EDITOR | "editor" | 编辑者 |
| 3 | VIEWER | "viewer" | 查看者 |
ExportStatus(导出状态)
| 值 | 名称 | 字符串 | 说明 |
|---|---|---|---|
| 1 | PENDING | "pending" | 等待处理 |
| 2 | PROCESSING | "processing" | 处理中 |
| 3 | COMPLETED | "completed" | 已完成 |
| 4 | FAILED | "failed" | 失败 |
| 5 | CANCELLED | "cancelled" | 已取消 |
影响范围
受影响的文件
- ✅
server/app/models/folder.py- 更新枚举定义 - ✅
server/app/schemas/folder.py- 添加转换逻辑 - ✅
server/app/services/folder_service.py- 简化映射逻辑 - ✅
server/app/repositories/folder_repository.py- 无需修改(自动适配) - ✅
server/app/migrations/006_folder_enum_to_smallint.py- 新增迁移脚本
API 兼容性
✅ 完全向后兼容 - API 层面仍使用字符串格式:
请求示例:
{
"userId": "01936d8f-1234-7890-abcd-ef1234567890",
"role": "editor"
}
响应示例:
{
"id": "01936d8f-5678-7890-abcd-ef1234567890",
"role": "editor",
"inherited": false
}
部署说明
前置条件
- 数据库可以清除重建(开发环境)
- 已备份重要数据(生产环境需要数据迁移脚本)
部署步骤
- 停止后端服务
- 执行数据库迁移:
cd server python run_migration.py 006 - 重启后端服务
- 验证 API 功能正常
验证清单
- 创建文件夹成员(role: "viewer", "editor", "owner")
- 查询文件夹成员列表(role 字段返回字符串)
- 更新成员角色
- 权限检查功能正常
- 数据库中 role 字段存储为整数(1, 2, 3)
回滚方案
由于数据库可以清除重建,如需回滚:
- 恢复旧版本代码
- 删除数据库
- 重新运行旧版本迁移脚本
相关文档
测试建议
单元测试
def test_member_role_conversion():
"""测试角色转换"""
assert MemberRole.from_string("owner") == MemberRole.OWNER
assert MemberRole.OWNER.to_string() == "owner"
assert MemberRole.OWNER.value == 1
def test_member_role_comparison():
"""测试角色优先级比较"""
assert MemberRole.OWNER > MemberRole.EDITOR
assert MemberRole.EDITOR > MemberRole.VIEWER
集成测试
async def test_add_folder_member_with_role():
"""测试添加成员(API 使用字符串)"""
response = await client.post(
f"/api/v1/folders/{folder_id}/members",
json={"userId": user_id, "role": "editor"}
)
assert response.status_code == 200
assert response.json()["role"] == "editor"
# 验证数据库存储为整数
member = await db.get(FolderMember, response.json()["id"])
assert member.role == 2 # EDITOR = 2
注意事项
- 数据库迁移: 确保在开发环境测试通过后再部署到生产环境
- API 兼容性: 客户端代码无需修改,仍使用字符串格式
- 性能监控: 部署后监控查询性能,预期有所提升
- 扩展性: 未来添加新角色只需更新 IntEnum 和 CHECK 约束
变更作者: System
审核状态: ✅ 已验证
部署状态: ⏳ 待部署