前言
个人信息管理是用户系统中非常重要的模块。本文详细介绍智学领航项目中个人信息管理功能的实现,包括用户基础信息修改、密码管理和学生档案管理。
功能模块总览
┌─────────────────────────────────────────────────────────────┐
│ 个人信息管理 │
├──────────────────┬──────────────────┬───────────────────────┤
│ 用户基础信息 │ 密码管理 │ 学生档案 │
│ ───────────── │ ────────── │ ───────── │
│ • 昵称修改 │ • 修改密码 │ • 学业信息 │
│ • 头像上传 │ • 旧密码验证 │ • 联系方式 │
│ • 性别设置 │ • 新密码设置 │ • 发展意向 │
│ • 个人简介 │ │ • 求职意向 │
└──────────────────┴──────────────────┴───────────────────────┘
API 接口一览
| 接口 | 方法 | 说明 |
|---|---|---|
/api/user/info | GET | 获取用户信息 |
/api/user/update | PUT | 更新用户信息 |
/api/user/password | PUT | 修改密码 |
/api/user/profile | GET | 获取学生档案 |
/api/user/profile | POST | 创建学生档案 |
/api/user/profile | PUT | 更新学生档案 |
/api/user/avatar/upload | POST | 上传头像 |
一、用户基础信息管理
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": "热爱编程的大学生"
}
}
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"
}
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 页面有 🔐 修改密码 按钮
四、学生档案管理
学生档案是智学领航项目的特色功能,用于存储用户的学业规划相关信息。
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 表
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | INT | PK, AUTO_INCREMENT | 学生信息ID |
| user_id | INT | FK -> user.id, NOT NULL | 用户ID |
| name | VARCHAR(32) | NULL | 姓名 |
| gender | INT | NULL | 性别 (1 男 / 2 女) |
| age | INT | NULL | 年龄 |
| school | VARCHAR(64) | NULL | 学校名称 |
| college | VARCHAR(64) | NULL | 学院 |
| major | VARCHAR(100) | NULL | 专业 |
| grade | VARCHAR(20) | NULL | 年级 |
| degree | INT | NULL | 学历 (1 本科 / 2 硕士) |
| gpa | VARCHAR(10) | NULL | 成绩 |
| has_internship | INT | NULL | 实习经历 (0 无 / 1 有) |
| interests | VARCHAR(500) | NULL | 兴趣方向 |
| career_goals | VARCHAR(500) | NULL | 职业意向 |
| skills | VARCHAR(500) | NULL | 技能基础 |
| phone | VARCHAR(16) | NULL | 手机号 |
| VARCHAR(64) | NULL | 邮箱 | |
| job_intention | VARCHAR(64) | NULL | 求职意向 |
| created_at | DATETIME | DEFAULT | 创建时间 |
| updated_at | DATETIME | DEFAULT | 更新时间 |
六、前端调用示例
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'
});
七、安全注意事项
当前实现的待优化点:
- 密码明文存储:建议使用 passlib 等库进行加密存储
- 密码强度校验:添加密码强度验证规则
- 旧密码验证:可增加旧密码正确性校验
- 文件上传限制:增加文件大小限制和病毒扫描
- 手机号格式校验:使用正则表达式验证手机号格式
- 邮箱格式校验:验证邮箱格式合法性
总结
本文详细介绍了智学领航项目中个人信息管理系统的完整实现,包括:
- 用户基础信息:昵称、头像、性别、个人简介
- 头像上传:文件类型验证、唯一文件名生成
- 密码管理:旧密码验证、新密码设置
- 学生档案:学业信息、联系方式、发展意向、求职意向
所有接口均通过 Depends(get_current_user) 进行认证拦截,确保用户信息的安全性。