# 用户昵称字段设计方案 > **方案名称**:新增 nickname 字段,优化用户身份体系 > **创建时间**:2025-01-14 > **版本**:v1.0 --- ## 1. 背景与问题 ### 原有设计 - `username`:唯一标识,首次登录自动生成,仅允许修改一次 - `username_changed`:布尔字段,标记是否已修改过用户名 ### 存在的问题 1. 缺少灵活的显示名称字段 2. username 既作为唯一标识,又作为显示名称,职责不清 3. 用户希望能随时修改显示名称,但 username 限制修改次数 --- ## 2. 设计决策 ### 关于 `username_changed` 字段 **决策:✅ 保留** **理由**: 1. **性能最优**:单表查询 `WHERE username_changed = false`,无需 JOIN 2. **语义清晰**:布尔值直接表达业务含义 3. **成本极低**:仅占用 1 字节存储 4. **逻辑可靠**:不依赖字符串匹配或外部表 **替代方案分析**: - ❌ 方案 B(正则匹配):用户可能修改成类似格式导致误判 - ❌ 方案 C(历史表):过度设计,增加查询复杂度 ### 关于 `nickname` 字段 **决策:✅ 新增** **设计规格**: - **类型**:TEXT - **约束**:允许 NULL - **唯一性**:无需唯一约束(允许重复) - **默认值**:NULL - **长度限制**:1-50 字符(应用层校验) - **修改限制**:无限制,可随时修改 - **索引**:不需要唯一索引 --- ## 3. 字段体系对比 | 字段 | 用途 | 唯一性 | 修改限制 | 典型场景 | | ------------------ | -------- | -------- | ---------- | -------------------- | | `username` | 账号标识 | 必须唯一 | 只能改一次 | 登录、@提及、URL | | `nickname` | 显示名称 | 可重复 | 随时修改 | 评论、个人资料、社交 | | `username_changed` | 修改标记 | - | 系统维护 | 业务逻辑判断 | --- ## 4. 数据库变更 ### 4.1 表结构变更 ```sql -- 在 users 表中新增 nickname 字段 ALTER TABLE users ADD COLUMN nickname TEXT; -- 添加注释 COMMENT ON COLUMN users.nickname IS '用户昵称(可重复、可随时修改)'; ``` ### 4.2 数据迁移(可选) ```sql -- 为现有用户初始化 nickname(复制 username) UPDATE users SET nickname = username WHERE nickname IS NULL AND deleted_at IS NULL; ``` ### 4.3 完整表结构 ```sql CREATE TABLE users ( user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- 登录方式 phone TEXT, country_code TEXT DEFAULT '+86', phone_verified BOOLEAN NOT NULL DEFAULT false, wechat_openid TEXT, wechat_unionid TEXT, wechat_platform TEXT CHECK (wechat_platform IN ('mp', 'open')), -- 基本信息 email TEXT, username TEXT NOT NULL, username_changed BOOLEAN NOT NULL DEFAULT false, nickname TEXT, -- 新增字段 password_hash TEXT, avatar_url TEXT, avatar_id BIGINT REFERENCES attachments(attachment_id) ON DELETE SET NULL, -- 算力积分信息 ai_credits_balance INTEGER NOT NULL DEFAULT 0, total_recharged_amount NUMERIC(10, 2) NOT NULL DEFAULT 0.00, total_credits_earned INTEGER NOT NULL DEFAULT 0, total_credits_consumed INTEGER NOT NULL DEFAULT 0, -- 时间戳 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ, -- 唯一约束 CONSTRAINT users_phone_unique UNIQUE (phone, country_code) NULLS NOT DISTINCT, CONSTRAINT users_email_unique UNIQUE (email) NULLS NOT DISTINCT, CONSTRAINT users_username_unique UNIQUE (username) NULLS NOT DISTINCT, CONSTRAINT users_wechat_openid_unique UNIQUE (wechat_openid, wechat_platform) NULLS NOT DISTINCT ); ``` --- ## 5. 代码实现 ### 5.1 数据模型 ```python # app/models/user.py from sqlalchemy import Column, BigInteger, String, Boolean, Text from app.core.database import Base class User(Base): __tablename__ = "users" user_id = Column(BigInteger, primary_key=True, autoincrement=True) # 基本信息 email = Column(String(255)) username = Column(String(255), nullable=False) username_changed = Column(Boolean, default=False) nickname = Column(Text) # 新增 password_hash = Column(String(255)) avatar_url = Column(String(500)) # ... 其他字段 ``` ### 5.2 Schema 定义 ```python # app/schemas/user.py from pydantic import BaseModel, Field from typing import Optional class UserUpdate(BaseModel): """更新用户信息(支持修改 nickname)""" nickname: Optional[str] = Field(None, min_length=1, max_length=50) avatar_url: Optional[str] = None class UsernameUpdate(BaseModel): """修改用户名(仅允许一次)""" username: str = Field(..., min_length=3, max_length=50, pattern="^[a-zA-Z0-9_\u4e00-\u9fa5-]+$") class UserResponse(BaseModel): """用户信息响应""" user_id: int phone: Optional[str] email: Optional[str] username: str nickname: Optional[str] # 新增 avatar_url: Optional[str] ai_credits_balance: int total_recharged_amount: float username_changed: bool created_at: datetime class Config: from_attributes = True ``` ### 5.3 Service 层 ```python # app/services/user_service.py class UserService: async def update_user( self, user_id: int, user_data: UserUpdate ) -> User: """更新用户信息(包括 nickname)""" user = await self.repository.get_by_id(user_id) if not user: raise NotFoundError("用户不存在") update_data = user_data.dict(exclude_unset=True) # nickname 可随时修改,无需额外校验 return await self.repository.update(user_id, update_data) async def update_username( self, user_id: int, username: str ) -> User: """修改用户名(仅允许一次)""" user = await self.repository.get_by_id(user_id) if not user: raise NotFoundError("用户不存在") # 检查是否已修改过(使用 username_changed 字段) if user.username_changed: raise ValidationError("用户名只能修改一次") # 检查用户名是否已被使用 existing_user = await self.repository.get_by_username(username) if existing_user: raise ValidationError("用户名已被使用") return await self.repository.update(user_id, { 'username': username, 'username_changed': True }) ``` --- ## 6. API 接口 ### 6.1 更新用户信息(支持修改 nickname) ``` PUT /api/v1/users/me ``` **请求体**: ```json { "nickname": "新昵称" } ``` **响应**: ```json { "user_id": 123, "username": "my_username", "nickname": "新昵称", "avatar_url": "https://storage.jointo.ai/avatars/123.jpg", "username_changed": false } ``` ### 6.2 修改用户名(仅允许一次) ``` PUT /api/v1/users/me/username ``` **请求体**: ```json { "username": "new_username" } ``` **响应**: ```json { "message": "用户名修改成功", "user": { "user_id": 123, "username": "new_username", "username_changed": true } } ``` **错误响应**: ```json { "error": "用户名只能修改一次" } ``` --- ## 7. 业务逻辑流程 ### 7.1 用户注册流程 ``` 1. 用户首次登录(手机号/微信) 2. 系统自动生成 username(格式:user_{timestamp}_{random4}) 3. nickname 初始为 NULL 4. username_changed = false ``` ### 7.2 修改 nickname 流程 ``` 1. 用户调用 PUT /api/v1/users/me 2. 传入 nickname 参数 3. 系统直接更新 nickname 字段 4. 无需校验 username_changed 5. 可随时修改,无次数限制 ``` ### 7.3 修改 username 流程 ``` 1. 用户调用 PUT /api/v1/users/me/username 2. 系统检查 username_changed 字段 3. 如果 username_changed = true,返回错误 4. 如果 username_changed = false,允许修改 5. 修改成功后,设置 username_changed = true 6. 后续无法再次修改 ``` --- ## 8. 优势总结 ### 8.1 用户体验 - ✅ username 作为稳定的账号标识 - ✅ nickname 作为灵活的显示名称 - ✅ 用户可随时修改昵称,无心理负担 ### 8.2 技术实现 - ✅ `username_changed` 字段性能最优 - ✅ 单表查询,无需 JOIN - ✅ 逻辑清晰,易于维护 ### 8.3 扩展性 - ✅ 未来可支持 nickname 历史记录 - ✅ 可添加 nickname 敏感词过滤 - ✅ 可支持 nickname 搜索功能 --- ## 9. 注意事项 ### 9.1 数据迁移 - 现有用户的 nickname 初始为 NULL - 可选择性地将 username 复制到 nickname - 建议在应用层处理默认值 ### 9.2 前端展示 - 优先显示 nickname(如果存在) - nickname 为空时,回退显示 username - 示例:`display_name = user.nickname || user.username` ### 9.3 敏感词过滤 - nickname 允许重复,但需过滤敏感词 - 建议在应用层实现敏感词检测 - 可使用第三方服务或自建词库 --- ## 10. 测试用例 ### 10.1 修改 nickname ```python # 测试:可随时修改 nickname def test_update_nickname(): # 第一次修改 response = client.put("/api/v1/users/me", json={"nickname": "昵称1"}) assert response.status_code == 200 assert response.json()["nickname"] == "昵称1" # 第二次修改(应该成功) response = client.put("/api/v1/users/me", json={"nickname": "昵称2"}) assert response.status_code == 200 assert response.json()["nickname"] == "昵称2" ``` ### 10.2 修改 username ```python # 测试:只能修改一次 username def test_update_username_once(): # 第一次修改 response = client.put("/api/v1/users/me/username", json={"username": "new_name"}) assert response.status_code == 200 assert response.json()["user"]["username_changed"] == True # 第二次修改(应该失败) response = client.put("/api/v1/users/me/username", json={"username": "another_name"}) assert response.status_code == 400 assert "只能修改一次" in response.json()["error"] ``` --- ## 11. 迁移脚本 ### 11.1 数据库迁移(Alembic) ```python # migrations/versions/xxxx_add_nickname_field.py """add nickname field to users table Revision ID: xxxx Revises: yyyy Create Date: 2025-01-14 """ from alembic import op import sqlalchemy as sa def upgrade(): # 新增 nickname 字段 op.add_column('users', sa.Column('nickname', sa.Text(), nullable=True)) # 可选:为现有用户初始化 nickname op.execute(""" UPDATE users SET nickname = username WHERE nickname IS NULL AND deleted_at IS NULL """) def downgrade(): # 回滚:删除 nickname 字段 op.drop_column('users', 'nickname') ``` --- ## 12. 相关文档 - [用户管理服务](../需求/backend/04-services/user/user-service.md) - [数据库设计](../需求/backend/04-database-design.md) - [API 设计规范](../需求/backend/05-api-design.md) --- **方案版本**:v1.0 **最后更新**:2025-01-14