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.
11 KiB
11 KiB
用户昵称字段设计方案
方案名称:新增 nickname 字段,优化用户身份体系
创建时间:2025-01-14
版本:v1.0
1. 背景与问题
原有设计
username:唯一标识,首次登录自动生成,仅允许修改一次username_changed:布尔字段,标记是否已修改过用户名
存在的问题
- 缺少灵活的显示名称字段
- username 既作为唯一标识,又作为显示名称,职责不清
- 用户希望能随时修改显示名称,但 username 限制修改次数
2. 设计决策
关于 username_changed 字段
决策:✅ 保留
理由:
- 性能最优:单表查询
WHERE username_changed = false,无需 JOIN - 语义清晰:布尔值直接表达业务含义
- 成本极低:仅占用 1 字节存储
- 逻辑可靠:不依赖字符串匹配或外部表
替代方案分析:
- ❌ 方案 B(正则匹配):用户可能修改成类似格式导致误判
- ❌ 方案 C(历史表):过度设计,增加查询复杂度
关于 nickname 字段
决策:✅ 新增
设计规格:
- 类型:TEXT
- 约束:允许 NULL
- 唯一性:无需唯一约束(允许重复)
- 默认值:NULL
- 长度限制:1-50 字符(应用层校验)
- 修改限制:无限制,可随时修改
- 索引:不需要唯一索引
3. 字段体系对比
| 字段 | 用途 | 唯一性 | 修改限制 | 典型场景 |
|---|---|---|---|---|
username |
账号标识 | 必须唯一 | 只能改一次 | 登录、@提及、URL |
nickname |
显示名称 | 可重复 | 随时修改 | 评论、个人资料、社交 |
username_changed |
修改标记 | - | 系统维护 | 业务逻辑判断 |
4. 数据库变更
4.1 表结构变更
-- 在 users 表中新增 nickname 字段
ALTER TABLE users
ADD COLUMN nickname TEXT;
-- 添加注释
COMMENT ON COLUMN users.nickname IS '用户昵称(可重复、可随时修改)';
4.2 数据迁移(可选)
-- 为现有用户初始化 nickname(复制 username)
UPDATE users
SET nickname = username
WHERE nickname IS NULL AND deleted_at IS NULL;
4.3 完整表结构
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 数据模型
# 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 定义
# 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 层
# 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
请求体:
{
"nickname": "新昵称"
}
响应:
{
"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
请求体:
{
"username": "new_username"
}
响应:
{
"message": "用户名修改成功",
"user": {
"user_id": 123,
"username": "new_username",
"username_changed": true
}
}
错误响应:
{
"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
# 测试:可随时修改 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
# 测试:只能修改一次 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)
# 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. 相关文档
方案版本:v1.0
最后更新:2025-01-14