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.
 

10 KiB

用户服务重构 - 符合架构规范

日期: 2026-01-27
类型: 重构
影响范围: 用户管理模块(Models, Schemas, Repositories, Services, API)


变更概述

根据项目架构规范和用户服务文档(docs/requirements/backend/04-services/user/user-service.md v2.3),对用户管理模块进行全面重构,主要包括:

  1. 移除数据库外键约束,改为应用层验证
  2. 添加微信登录相关字段和枚举
  3. 完善积分管理字段
  4. 使用 Pydantic v2 序列化器
  5. 实现完整的业务逻辑和 API 接口

详细变更

1. Model 层 (app/models/user.py)

新增 WechatPlatform 枚举

class WechatPlatform(IntEnum):
    """微信平台枚举"""
    MP = 1    # 公众号
    OPEN = 2  # 开放平台
    
    @classmethod
    def from_string(cls, value: str) -> 'WechatPlatform':
        """从字符串转换为枚举值"""
        
    def to_string(self) -> str:
        """转换为字符串"""
        
    @classmethod
    def get_display_name(cls, value: int) -> str:
        """获取显示名称"""

User 模型变更

移除

  • foreign_key 约束
  • Relationship 关系定义
  • sessions 关系字段

新增字段

  • wechat_openid: 微信 openid
  • wechat_unionid: 微信 unionid
  • wechat_platform: 微信平台(SMALLINT,1=mp, 2=open)
  • password_hash: 密码哈希(可选)
  • avatar_id: 头像附件 ID(应用层验证)
  • total_recharged_amount: 累计充值金额(Numeric(10, 2))
  • total_credits_earned: 累计获得积分
  • total_credits_consumed: 累计消耗积分

字段调整

  • phone: 改为可选(Optional[str])
  • phone_verified: 默认值改为 False

UserSession 模型变更

移除

  • foreign_key="users.user_id" 约束
  • user 关系字段

新增字段

  • refresh_token: 刷新令牌

字段调整

  • user_id: 添加描述"应用层验证,关联 users.user_id"

2. Schema 层 (app/schemas/user.py)

新增枚举

class WechatPlatformEnum(str, Enum):
    """API 层微信平台枚举(字符串)"""
    MP = "mp"
    OPEN = "open"

新增 Schema

  • SendSmsRequest: 发送短信验证码请求
  • UsernameUpdate: 用户名修改请求
  • BindPhoneRequest: 绑定手机号请求
  • BindWechatRequest: 绑定微信请求
  • BindEmailRequest: 绑定邮箱请求
  • RefreshTokenRequest: 刷新 Token 请求
  • RefreshTokenResponse: 刷新 Token 响应
  • CreditsInfoResponse: 积分信息响应
  • WechatQrcodeResponse: 微信二维码响应
  • WechatLoginResultResponse: 微信登录结果响应

使用 Pydantic v2 序列化器

UserResponse 变更

@field_serializer('user_id')
def serialize_user_id(self, value: UUID) -> str:
    """UUID 序列化为字符串"""
    return str(value)

@field_serializer('wechat_platform')
def serialize_platform(self, value: Optional[int]) -> Optional[str]:
    """API 输出:整数 → 字符串"""
    if value is None:
        return None
    from app.models.user import WechatPlatform
    return WechatPlatform(value).to_string()

BindWechatRequest 变更

@field_validator('platform', mode='before')
@classmethod
def convert_platform_to_int(cls, v):
    """API 输入:字符串 → 整数"""
    from app.models.user import WechatPlatform
    if isinstance(v, str):
        return WechatPlatform.from_string(v).value
    return v

3. Repository 层 (app/repositories/user_repository.py)

新增方法

  • get_by_wechat_openid(openid, platform): 通过微信 openid 查询用户
  • get_session_by_refresh_token(refresh_token): 通过 refresh_token 查询会话
  • update_session(session_id, data): 更新会话

方法签名调整

所有涉及 UUID 的参数和返回值类型从 str 改为 UUID

  • delete_session(session_id: UUID)
  • delete_user_sessions(user_id: UUID)
  • update_session_last_used(session_id: UUID)

添加应用层级联删除注释

async def delete_user_sessions(self, user_id: UUID) -> int:
    """删除用户的所有会话(应用层级联删除)"""

4. Service 层 (app/services/user_service.py)

新增方法

登录相关

  • refresh_token(refresh_token): 刷新访问令牌

用户信息管理

  • update_username(user_id, username): 修改用户名(仅允许一次)
  • upload_avatar(user_id, file_content, filename): 上传头像

账号绑定

  • bind_phone(user_id, phone, country_code, code): 绑定手机号
  • bind_wechat(user_id, code, platform): 绑定微信(待实现)
  • bind_email(user_id, email): 绑定邮箱

积分查询

  • get_credits_info(user_id): 查询用户积分信息

私有方法

  • _create_session(...): 创建会话(应用层验证用户存在)

方法增强

login_with_phone

  • 添加 refresh_token 生成
  • 初始化完整的积分字段(balance, total_earned, total_consumed, total_recharged_amount)
  • 调用 _create_session 进行应用层验证

logout

  • 参数类型从 str 改为 UUID
  • 添加应用层级联删除注释

update_user

  • 添加邮箱唯一性检查
  • 邮箱自动转小写

5. API 层

app/api/v1/auth.py 新增接口

  • POST /auth/refresh: 刷新 Token
  • GET /auth/wechat/qrcode: 获取微信登录二维码(待实现)
  • GET /auth/wechat/result: 轮询微信登录结果(待实现)

app/api/v1/users.py 新增接口

  • PUT /users/me/username: 修改用户名
  • POST /users/me/avatar: 上传头像
  • POST /users/me/bind/phone: 绑定手机号
  • POST /users/me/bind/wechat: 绑定微信(待实现)
  • POST /users/me/bind/email: 绑定邮箱
  • GET /users/me/credits: 查询积分信息

架构改进

1. 移除数据库外键约束

原因

  • 提升性能和扩展性
  • 为分库分表做准备
  • 避免数据库层级联删除的复杂性

实现

  • 在 Service 层的 _create_session 方法中验证 user_id 是否存在
  • 在 Repository 层添加 delete_user_sessions 方法实现应用层级联删除

2. 枚举类型使用 SMALLINT

原因

  • 更好的性能(2 字节 vs 可变长度字符串)
  • 更容易扩展
  • 与项目其他模块保持一致

实现

  • 数据库层:wechat_platform SMALLINT
  • Model 层:WechatPlatform(IntEnum)
  • Schema 层:WechatPlatformEnum(str, Enum) + field_validator + field_serializer

3. Pydantic v2 序列化器

优势

  • 更好的性能
  • 更灵活的序列化控制
  • 类型安全

实现

  • 使用 @field_serializer 替代 @validator
  • 使用 @field_validator 替代 @validator
  • 支持 mode='before' 进行输入转换

数据库迁移

需要执行以下迁移脚本(待创建):

-- 1. 移除外键约束
ALTER TABLE user_sessions DROP CONSTRAINT IF EXISTS user_sessions_user_id_fkey;

-- 2. 添加微信相关字段
ALTER TABLE users ADD COLUMN IF NOT EXISTS wechat_openid VARCHAR(100);
ALTER TABLE users ADD COLUMN IF NOT EXISTS wechat_unionid VARCHAR(100);
ALTER TABLE users ADD COLUMN IF NOT EXISTS wechat_platform SMALLINT CHECK (wechat_platform IN (1, 2));

-- 3. 添加积分字段
ALTER TABLE users ADD COLUMN IF NOT EXISTS total_recharged_amount NUMERIC(10, 2) DEFAULT 0.00;
ALTER TABLE users ADD COLUMN IF NOT EXISTS total_credits_earned INTEGER DEFAULT 100;
ALTER TABLE users ADD COLUMN IF NOT EXISTS total_credits_consumed INTEGER DEFAULT 0;

-- 4. 添加其他字段
ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash VARCHAR(255);
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_id UUID;
ALTER TABLE user_sessions ADD COLUMN IF NOT EXISTS refresh_token VARCHAR(500);

-- 5. 修改字段约束
ALTER TABLE users ALTER COLUMN phone DROP NOT NULL;
ALTER TABLE users ALTER COLUMN phone_verified SET DEFAULT false;

-- 6. 创建索引
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_wechat_openid 
    ON users (wechat_openid, wechat_platform) 
    WHERE deleted_at IS NULL AND wechat_openid IS NOT NULL;

CREATE INDEX IF NOT EXISTS idx_users_avatar_id 
    ON users (avatar_id) 
    WHERE avatar_id IS NOT NULL;

-- 7. 添加注释
COMMENT ON TABLE users IS '用户表 - 应用层保证引用完整性';
COMMENT ON COLUMN users.avatar_id IS '头像附件ID - 应用层验证,关联 attachments.attachment_id';
COMMENT ON TABLE user_sessions IS '用户会话表 - 应用层保证引用完整性';
COMMENT ON COLUMN user_sessions.user_id IS '用户ID - 应用层验证,关联 users.user_id';

待实现功能

以下功能已定义接口但尚未实现:

  1. 微信登录

    • GET /auth/wechat/qrcode
    • GET /auth/wechat/result
    • bind_wechat 方法
  2. 文件上传

    • upload_avatar 方法需要集成存储服务
  3. 短信验证码

    • 当前使用固定验证码 "6666",需要集成短信服务

测试建议

1. 单元测试

  • 测试 WechatPlatform 枚举转换方法
  • 测试 Service 层的应用层验证逻辑
  • 测试 Schema 序列化器

2. 集成测试

  • 测试手机号登录流程
  • 测试 Token 刷新流程
  • 测试账号绑定流程
  • 测试积分查询

3. 性能测试

  • 测试无外键约束后的查询性能
  • 测试应用层级联删除的性能

兼容性说明

Breaking Changes

  1. API 响应格式变更

    • UserResponseuser_id 从 UUID 对象改为字符串
    • 新增 wechat_platform 字段(字符串格式)
    • 新增 total_recharged_amount 字段
  2. 数据库结构变更

    • 移除外键约束
    • 新增多个字段
    • phone 字段改为可选

向后兼容

  • 所有新增字段均为可选或有默认值
  • 现有 API 接口保持兼容
  • 数据库迁移脚本使用 IF NOT EXISTSIF EXISTS

相关文档


总结

本次重构全面对齐了用户服务文档规范,主要改进包括:

  1. 移除数据库外键,改为应用层验证
  2. 添加微信登录支持(字段和枚举)
  3. 完善积分管理字段
  4. 使用 Pydantic v2 序列化器
  5. 实现完整的业务逻辑和 API 接口
  6. 代码通过诊断检查,无错误

下一步需要:

  • 创建数据库迁移脚本
  • 实现微信登录功能
  • 集成存储服务(头像上传)
  • 集成短信服务(验证码)
  • 编写单元测试和集成测试