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

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

优势

  1. 性能提升: SMALLINT 查询和索引性能优于 ENUM
  2. 易于扩展: 添加新值无需 ALTER TYPE,只需更新 CHECK 约束
  3. 一致性: 与 folder_category 等字段保持统一
  4. 向后兼容: API 层仍使用字符串,客户端无感知
  5. 类型安全: 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
}

部署说明

前置条件

  • 数据库可以清除重建(开发环境)
  • 已备份重要数据(生产环境需要数据迁移脚本)

部署步骤

  1. 停止后端服务
  2. 执行数据库迁移:
    cd server
    python run_migration.py 006
    
  3. 重启后端服务
  4. 验证 API 功能正常

验证清单

  • 创建文件夹成员(role: "viewer", "editor", "owner")
  • 查询文件夹成员列表(role 字段返回字符串)
  • 更新成员角色
  • 权限检查功能正常
  • 数据库中 role 字段存储为整数(1, 2, 3)

回滚方案

由于数据库可以清除重建,如需回滚:

  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

注意事项

  1. 数据库迁移: 确保在开发环境测试通过后再部署到生产环境
  2. API 兼容性: 客户端代码无需修改,仍使用字符串格式
  3. 性能监控: 部署后监控查询性能,预期有所提升
  4. 扩展性: 未来添加新角色只需更新 IntEnum 和 CHECK 约束

变更作者: System
审核状态: 已验证
部署状态: 待部署