智学领航 个人信息管理系统详解

0 阅读9分钟

前言

个人信息管理是用户系统中非常重要的模块。本文详细介绍智学领航项目中个人信息管理功能的实现,包括用户基础信息修改、密码管理和学生档案管理。

功能模块总览

┌─────────────────────────────────────────────────────────────┐
│                     个人信息管理                             │
├──────────────────┬──────────────────┬───────────────────────┤
│  用户基础信息     │   密码管理        │    学生档案           │
│  ─────────────   │   ──────────     │    ─────────          │
│  • 昵称修改      │   • 修改密码      │    • 学业信息          │
│  • 头像上传      │   • 旧密码验证    │    • 联系方式          │
│  • 性别设置      │   • 新密码设置    │    • 发展意向          │ 
│  • 个人简介      │                  │    • 求职意向          │
└──────────────────┴──────────────────┴───────────────────────┘

API 接口一览

接口方法说明
/api/user/infoGET获取用户信息
/api/user/updatePUT更新用户信息
/api/user/passwordPUT修改密码
/api/user/profileGET获取学生档案
/api/user/profilePOST创建学生档案
/api/user/profilePUT更新学生档案
/api/user/avatar/uploadPOST上传头像

一、用户基础信息管理

1.1 数据模型 (Schema)

class UserUpdateRequest(BaseModel):
    """更新用户信息的请求模型"""
    nickname: Optional[str] = Field(None, max_length=50, description="昵称")
    avatar: Optional[str] = Field(None, max_length=255, description="头像URL")
    gender: Optional[str] = Field(None, description="性别 (male/female/unknown)")
    bio: Optional[str] = Field(None, max_length=500, description="个人简介")
    phone: Optional[str] = Field(None, max_length=20, description="手机号")


class UserInfoResponse(BaseModel):
    """用户信息响应模型"""
    id: int
    username: Optional[str] = None
    phone: Optional[str] = None
    nickname: Optional[str] = None
    avatar: Optional[str] = None
    gender: Optional[str] = None
    bio: Optional[str] = None

    model_config = ConfigDict(from_attributes=True)

1.2 获取用户信息

路由处理

@router.get("/info")
async def get_user_info(user: User = Depends(get_current_user)):
    """获取当前登录用户的信息"""
    return success_response(
        message="获取用户信息成功",
        data=UserInfoResponse.model_validate(user)
    )

响应示例

{
    "code": 200,
    "message": "获取用户信息成功",
    "data": {
        "id": 1,
        "username": "demo",
        "phone": "13800138000",
        "nickname": "小明同学",
        "avatar": "/static/uploads/avatar.jpg",
        "gender": "male",
        "bio": "热爱编程的大学生"
    }
}

image.png

1.3 更新用户信息

路由处理

@router.put("/update")
async def update_user_info(
    user_data: UserUpdateRequest,
    user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    """更新用户信息"""
    updated_user = await users.update_user(db, user.username, user_data)
    return success_response(
        message="更新用户信息成功",
        data=UserInfoResponse.model_validate(updated_user)
    )

CRUD 实现

async def update_user(db: AsyncSession, username: str, user_data: UserUpdateRequest):
    """更新用户信息"""
    # 只更新非空字段
    query = update(User).where(User.username == username).values(
        **user_data.model_dump(exclude_unset=True, exclude_none=True)
    )
    result = await db.execute(query)
    await db.commit()

    if result.rowcount == 0:
        raise HTTPException(status_code=404, detail="用户不存在")

    updated_user = await get_user_by_username(db, username)
    return updated_user

请求示例

{
    "nickname": "新昵称",
    "bio": "更新后的个人简介",
    "gender": "female"
}

二、头像上传功能

2.1 路由处理

UPLOAD_DIR = "static/uploads"
ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "gif", "webp"}


def allowed_file(filename: str) -> bool:
    """检查文件类型是否允许"""
    return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS


@router.post("/avatar/upload")
async def upload_avatar(
    file: UploadFile = File(...),
    user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    """上传用户头像"""

    # 1. 检查文件类型
    if not allowed_file(file.filename):
        raise HTTPException(
            status_code=400,
            detail="只允许上传图片文件 (jpg, jpeg, png, gif, webp)"
        )

    # 2. 生成唯一文件名
    file_extension = file.filename.rsplit(".", 1)[1].lower()
    unique_filename = f"{uuid.uuid4()}.{file_extension}"
    file_path = os.path.join(UPLOAD_DIR, unique_filename)

    # 3. 保存文件
    try:
        contents = await file.read()
        with open(file_path, "wb") as f:
            f.write(contents)

        # 4. 更新用户头像 URL
        avatar_url = f"/static/uploads/{unique_filename}"
        update_data = UserUpdateRequest(avatar=avatar_url)
        updated_user = await users.update_user(db, user.username, update_data)

        return success_response(
            message="头像上传成功",
            data={
                "avatar": avatar_url,
                "userInfo": UserInfoResponse.model_validate(updated_user)
            }
        )
    except Exception as e:
        # 清理已上传的文件
        if os.path.exists(file_path):
            os.remove(file_path)
        raise HTTPException(
            status_code=500,
            detail=f"文件上传失败: {str(e)}"
        )

2.2 上传流程

1. 前端:选择图片文件
        ↓
2. 检查文件类型 (jpg/jpeg/png/gif/webp)
        ↓
3. 生成 UUID 文件名避免冲突
        ↓
4. 保存文件到 static/uploads/
        ↓
5. 更新用户表的 avatar 字段
        ↓
6. 返回新的头像 URL

三、密码修改功能

3.1 数据模型

class UserChangePasswordRequest(BaseModel):
    """修改密码请求模型"""
    old_password: str = Field(..., alias="oldPassword", description="旧密码")
    new_password: str = Field(..., min_length=6, alias="newPassword", description="新密码")

3.2 路由处理

@router.put("/password")
async def update_password(
    password_data: UserChangePasswordRequest,
    user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    """修改密码"""
    res_change_pwd = await users.change_password(
        db,
        user,
        password_data.old_password,
        password_data.new_password
    )

    if not res_change_pwd:
        raise HTTPException(
            status_code=500,
            detail="修改密码失败,请稍后再试"
        )

    return success_response(message="修改密码成功")

3.3 CRUD 实现

async def change_password(
    db: AsyncSession,
    user: User,
    old_password: str,
    new_password: str
):
    """修改密码"""
    # 验证旧密码(当前为明文比较)
    if old_password != user.password:
        return False

    # 更新密码
    user.password = new_password
    db.add(user)
    await db.commit()
    await db.refresh(user)
    return True

请求示例

{
    "oldPassword": "旧密码123",
    "newPassword": "新密码456"
}

image.png

3.4 前端密码修改页面

项目提供了独立的密码修改页面 password.html,具有以下特性:

页面功能

  • 🔐 密码强度实时指示器(弱/中等/良好/强)
  • ✅ 新密码与确认密码实时比对
  • 📋 密码安全建议提示
  • 🔄 修改成功后自动跳转登录页

密码强度检测算法

function checkPasswordStrength(password) {
    let strength = 0;
    if (password.length >= 8) strength++;
    if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
    if (/\d/.test(password)) strength++;
    if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) strength++;
    return strength;
}

强度等级说明

等级强度颜色
0灰色
1红色
2中等橙色
3良好绿色
4深绿

3.5 前端验证逻辑

document.getElementById('passwordForm').addEventListener('submit', async (e) => {
    e.preventDefault();
    const oldPassword = form.oldPassword.value;
    const newPassword = form.newPassword.value;
    const confirmPassword = form.confirmPassword.value;

    if (newPassword.length < 6) {
        showMessage('新密码至少需要6位', 'error');
        return;
    }
    if (newPassword !== confirmPassword) {
        showMessage('两次输入的新密码不一致', 'error');
        return;
    }
    if (oldPassword === newPassword) {
        showMessage('新密码不能与当前密码相同', 'error');
        return;
    }

    const res = await fetch('/api/user/password', {
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}`
        },
        body: JSON.stringify({ oldPassword, newPassword })
    });

    if (res.ok) {
        showMessage('密码修改成功!请重新登录', 'success');
        setTimeout(() => {
            localStorage.removeItem('token');
            location.href = '/login';
        }, 2000);
    }
});

3.6 页面入口

路由GET /password

入口:在 /user 页面有 🔐 修改密码 按钮


四、学生档案管理

学生档案是智学领航项目的特色功能,用于存储用户的学业规划相关信息。

image.png

4.1 数据模型

StudentProfileRequest (请求)

class StudentProfileRequest(BaseModel):
    """学生档案请求模型"""
    name: Optional[str] = Field(None, max_length=32, description="姓名")
    gender: Optional[int] = Field(None, description="性别 (1 男 / 2 女)")
    age: Optional[int] = Field(None, description="年龄")
    school: Optional[str] = Field(None, max_length=64, description="学校名称")
    college: Optional[str] = Field(None, max_length=64, description="学院")
    major: Optional[str] = Field(None, max_length=100, description="专业")
    grade: Optional[str] = Field(None, max_length=20, description="年级(如:大一、研一)")
    degree: Optional[int] = Field(None, description="学历 (1 本科 / 2 硕士)")
    gpa: Optional[str] = Field(None, max_length=10, description="成绩(如:3.5/4.0)")
    has_internship: Optional[int] = Field(None, description="是否有实习经历 (0 无 / 1 有)")
    interests: Optional[str] = Field(None, max_length=500, description="兴趣方向")
    career_goals: Optional[str] = Field(None, max_length=500, description="职业意向")
    skills: Optional[str] = Field(None, max_length=500, description="技能基础")
    phone: Optional[str] = Field(None, max_length=16, description="手机号")
    email: Optional[str] = Field(None, max_length=64, description="邮箱")
    job_intention: Optional[str] = Field(None, max_length=64, description="求职意向")

StudentProfile 数据库模型

class StudentProfile(Base):
    __tablename__ = 'student_profile'

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    user_id: Mapped[int] = mapped_column(Integer, ForeignKey(User.id), nullable=False)
    name: Mapped[Optional[str]] = mapped_column(String(32))
    gender: Mapped[Optional[int]] = mapped_column(Integer)
    age: Mapped[Optional[int]] = mapped_column(Integer)
    school: Mapped[Optional[str]] = mapped_column(String(64))
    college: Mapped[Optional[str]] = mapped_column(String(64))
    major: Mapped[Optional[str]] = mapped_column(String(100))
    grade: Mapped[Optional[str]] = mapped_column(String(20))
    degree: Mapped[Optional[int]] = mapped_column(Integer)
    gpa: Mapped[Optional[str]] = mapped_column(String(10))
    has_internship: Mapped[Optional[int]] = mapped_column(Integer)
    interests: Mapped[Optional[str]] = mapped_column(String(500))
    career_goals: Mapped[Optional[str]] = mapped_column(String(500))
    skills: Mapped[Optional[str]] = mapped_column(String(500))
    phone: Mapped[Optional[str]] = mapped_column(String(16))
    email: Mapped[Optional[str]] = mapped_column(String(64))
    job_intention: Mapped[Optional[str]] = mapped_column(String(64))
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now())
    updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now(), onupdate=datetime.now())

4.2 获取学生档案

@router.get("/profile")
async def get_student_profile(
    user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    """获取学生信息"""
    profile = await users.get_student_profile(db, user.id)
    if not profile:
        return success_response(message="未填写学生信息", data=None)
    return success_response(
        message="获取学生信息成功",
        data=StudentProfileResponse.model_validate(profile)
    )

响应示例

{
    "code": 200,
    "message": "获取学生信息成功",
    "data": {
        "id": 1,
        "user_id": 1,
        "name": "张三",
        "gender": 1,
        "age": 20,
        "school": "XX大学",
        "college": "计算机学院",
        "major": "计算机科学与技术",
        "grade": "大三",
        "degree": 1,
        "gpa": "3.5/4.0",
        "has_internship": 1,
        "interests": "人工智能、软件开发",
        "career_goals": "后端开发工程师",
        "skills": "Java、Python、MySQL",
        "phone": "13800138000",
        "email": "zhangsan@example.com",
        "job_intention": "互联网公司后端开发"
    }
}

4.3 创建/更新学生档案

@router.post("/profile")
async def create_student_profile(
    profile_data: StudentProfileRequest,
    user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    """创建或更新学生信息"""
    profile = await users.create_or_update_student_profile(db, user.id, profile_data)
    return success_response(
        message="保存学生信息成功",
        data=StudentProfileResponse.model_validate(profile)
    )


@router.put("/profile")
async def update_student_profile(
    profile_data: StudentProfileRequest,
    user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    """更新学生信息"""
    profile = await users.create_or_update_student_profile(db, user.id, profile_data)
    return success_response(
        message="更新学生信息成功",
        data=StudentProfileResponse.model_validate(profile)
    )

4.4 CRUD 实现

async def get_student_profile(db: AsyncSession, user_id: int):
    """获取学生档案"""
    query = select(StudentProfile).where(StudentProfile.user_id == user_id)
    result = await db.execute(query)
    return result.scalar_one_or_none()


async def create_or_update_student_profile(
    db: AsyncSession,
    user_id: int,
    profile_data: StudentProfileRequest
):
    """创建或更新学生档案"""
    # 1. 查询是否已存在
    profile = await get_student_profile(db, user_id)

    if profile:
        # 2. 存在则更新(只更新非空字段)
        for key, value in profile_data.model_dump(
            exclude_unset=True,
            exclude_none=True
        ).items():
            setattr(profile, key, value)
    else:
        # 3. 不存在则创建
        profile = StudentProfile(
            user_id=user_id,
            **profile_data.model_dump(exclude_unset=True, exclude_none=True)
        )
        db.add(profile)

    await db.commit()
    await db.refresh(profile)
    return profile

五、数据库表结构

5.1 student_profile 表

字段名类型约束说明
idINTPK, AUTO_INCREMENT学生信息ID
user_idINTFK -> user.id, NOT NULL用户ID
nameVARCHAR(32)NULL姓名
genderINTNULL性别 (1 男 / 2 女)
ageINTNULL年龄
schoolVARCHAR(64)NULL学校名称
collegeVARCHAR(64)NULL学院
majorVARCHAR(100)NULL专业
gradeVARCHAR(20)NULL年级
degreeINTNULL学历 (1 本科 / 2 硕士)
gpaVARCHAR(10)NULL成绩
has_internshipINTNULL实习经历 (0 无 / 1 有)
interestsVARCHAR(500)NULL兴趣方向
career_goalsVARCHAR(500)NULL职业意向
skillsVARCHAR(500)NULL技能基础
phoneVARCHAR(16)NULL手机号
emailVARCHAR(64)NULL邮箱
job_intentionVARCHAR(64)NULL求职意向
created_atDATETIMEDEFAULT创建时间
updated_atDATETIMEDEFAULT更新时间

六、前端调用示例

6.1 更新用户信息

async function updateUserInfo(data) {
    const token = localStorage.getItem('token');

    const response = await fetch('/api/user/update', {
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}`
        },
        body: JSON.stringify(data)
    });

    const result = await response.json();
    if (result.code === 200) {
        console.log('更新成功:', result.data);
    } else {
        console.error('更新失败:', result.message);
    }
}

// 调用
updateUserInfo({
    nickname: '新昵称',
    bio: '新的个人简介'
});

6.2 上传头像

async function uploadAvatar(file) {
    const token = localStorage.getItem('token');
    const formData = new FormData();
    formData.append('file', file);

    const response = await fetch('/api/user/avatar/upload', {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${token}`
        },
        body: formData
    });

    const result = await response.json();
    if (result.code === 200) {
        console.log('头像上传成功:', result.data.avatar);
    }
}

// 调用
const fileInput = document.getElementById('avatarInput');
uploadAvatar(fileInput.files[0]);

6.3 修改密码

async function changePassword(oldPwd, newPwd) {
    const token = localStorage.getItem('token');

    const response = await fetch('/api/user/password', {
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}`
        },
        body: JSON.stringify({
            oldPassword: oldPwd,
            newPassword: newPwd
        })
    });

    const result = await response.json();
    if (result.code === 200) {
        console.log('密码修改成功');
    }
}

6.4 保存学生档案

async function saveStudentProfile(profileData) {
    const token = localStorage.getItem('token');

    const response = await fetch('/api/user/profile', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}`
        },
        body: JSON.stringify(profileData)
    });

    const result = await response.json();
    if (result.code === 200) {
        console.log('档案保存成功');
    }
}

// 调用
saveStudentProfile({
    name: '张三',
    school: 'XX大学',
    major: '计算机科学与技术',
    grade: '大三',
    gpa: '3.5/4.0',
    interests: '人工智能',
    career_goals: '后端开发工程师',
    skills: 'Java, Python, MySQL'
});

七、安全注意事项

当前实现的待优化点:

  1. 密码明文存储:建议使用 passlib 等库进行加密存储
  2. 密码强度校验:添加密码强度验证规则
  3. 旧密码验证:可增加旧密码正确性校验
  4. 文件上传限制:增加文件大小限制和病毒扫描
  5. 手机号格式校验:使用正则表达式验证手机号格式
  6. 邮箱格式校验:验证邮箱格式合法性

总结

本文详细介绍了智学领航项目中个人信息管理系统的完整实现,包括:

  • 用户基础信息:昵称、头像、性别、个人简介
  • 头像上传:文件类型验证、唯一文件名生成
  • 密码管理:旧密码验证、新密码设置
  • 学生档案:学业信息、联系方式、发展意向、求职意向

所有接口均通过 Depends(get_current_user) 进行认证拦截,确保用户信息的安全性。