2026山东大学项目实训项目博客(八)
宠声健康 AI:一个完整的前后端分离项目实战总结
前言
最近完成了「宠声健康 AI」项目的开发与优化,这是一个功能完整的宠物健康管理应用。整个项目采用了前后端分离的架构,前端使用 Vue 3 + Pinia + Vite,后端使用 Flask + SQLAlchemy + JWT,实现了从用户认证到宠物管理、文件上传等核心功能。
在这篇文章中,我将对整个项目进行一次系统的总结,分享架构设计、核心功能实现以及踩过的坑。
一、项目技术架构
技术栈一览
| 层级 | 技术选择 | 说明 |
|---|---|---|
| 前端 | Vue 3 + Pinia + Vite | 现代化前端技术栈,Composition API 写法 |
| 后端 | Flask + SQLAlchemy + JWT | 轻量级 Python Web 框架 |
| 认证 | JWT + localStorage | 无状态认证方案 |
| 存储 | SQLite | 轻量级关系型数据库 |
目录结构
travel_agent/ ├── backend/ # 后端服务 │ ├── routes/ # 路由模块 │ │ ├── auth.py # 认证相关 │ │ ├── pet.py # 宠物管理 │ │ └── ... │ ├── models.py # 数据模型 │ ├── utils/ # 工具函数 │ ├── static/ # 静态文件 │ └── app.py ├── frontend/ # 前端应用 │ ├── src/ │ │ ├── api/ # API 接口定义 │ │ ├── components/ # 公共组件 │ │ ├── stores/ # Pinia 状态管理 │ │ ├── utils/ # 工具函数 │ │ ├── views/ # 页面组件 │ │ └── main.js └── docs/ # 文档目录二、核心功能实现
1. 统一响应格式设计
前后端分离架构中,统一的响应格式是协作的基础。我在后端封装了api_response函数,实现了以下功能:
defsnake_to_camel(snake_str):components=snake_str.split('_')returncomponents[0]+''.join(x.title()forxincomponents[1:])defapi_response(data=None,message="操作成功",code=200,status="success"):converted_data=convert_dict_keys(data)ifdataisnotNoneelseNonereturnjsonify({"status":status,"code":code,"message":message,"data":converted_data})关键特性:
- 自动将 Python 下划线命名转换为前端习惯的驼峰命名
- 统一的响应结构:
status、code、message、data - 支持递归转换嵌套数据结构
2. 宠物头像上传功能
这是这次优化的核心功能之一,从最初仅支持 URL 改为支持本地文件上传。
前端实现
1. 模板结构
<divclass="avatar-upload"><divclass="avatar-preview"@click="triggerFileInput"><imgv-if="previewAvatarUrl":src="previewAvatarUrl"alt="头像预览"/><divv-elseclass="avatar-placeholder"><span>点击上传</span></div></div><inputref="fileInputRef"type="file"accept="image/*"@change="handleFileChange"style="display:none"/></div>2. 文件处理与预览
constfileInputRef=ref(null)constselectedFile=ref(null)constpreviewAvatarUrl=ref('')consthandleFileChange=(event)=>{constfile=event.target.files[0]if(!file)returnselectedFile.value=fileconstreader=newFileReader()reader.onload=(e)=>previewAvatarUrl.value=e.target.result reader.readAsDataURL(file)}3. 智能上传逻辑
constonSave=async()=>{if(selectedFile.value){constformData=newFormData()formData.append('petName',form.petName)formData.append('avatar',selectedFile.value)awaitaddPet(formData)}else{awaitaddPet({petName:form.petName,avatarUrl:form.avatarUrl})}closeModal()awaitfetchPets()}后端实现
1. 路由与配置
pet_bp=Blueprint('pet',__name__)UPLOAD_FOLDER=os.path.join(os.path.dirname(os.path.dirname(__file__)),'static','avatars')ALLOWED_EXTENSIONS={'png','jpg','jpeg','gif'}os.makedirs(UPLOAD_FOLDER,exist_ok=True)defallowed_file(filename):return'.'infilenameandfilename.rsplit('.',1)[1].lower()inALLOWED_EXTENSIONS2. 添加宠物接口
@pet_bp.route('/',methods=['POST'])@jwt_required()defadd_pet():user_id=int(get_jwt_identity())user=User.query.filter_by(user_id=user_id).first()data=request.get_json(silent=True)avatar_file=request.files.get('avatar')# 同时支持 FormData 和 JSON 两种格式pet_name=data.get('pet_name')ordata.get('petName')ifdataelse\ request.form.get('pet_name')orrequest.form.get('petName')# 文件处理final_avatar_url=avatar_urlifavatar_fileandallowed_file(avatar_file.filename):original_filename=secure_filename(avatar_file.filename)unique_filename=f"{uuid.uuid4().hex}_{original_filename}"file_path=os.path.join(UPLOAD_FOLDER,unique_filename)avatar_file.save(file_path)final_avatar_url=unique_filename# 保存宠物信息new_pet=Pet(user_id=user.user_id,pet_name=pet_name,avatar_url=final_avatar_url)db.session.add(new_pet)db.session.commit()returnapi_response(data={"petId":new_pet.pet_id,"avatarUrl":return_avatar_url},code=201),201request.js 适配
关键!FormData 不能手动设置 Content-Type,要让浏览器自动处理:
request.interceptors.request.use(config=>{consttoken=localStorage.getItem('token')if(token){config.headers.Authorization=`Bearer${token}`}if(!(config.datainstanceofFormData)){config.headers['Content-Type']='application/json'}returnconfig})3. 401 认证错误处理
Token 过期时,需要给用户友好的提示并自动跳转:
request.interceptors.response.use(response=>{constres=response.dataif(res.code===200||res.status==='success'){returnres.data!==undefined?res.data:res}toast.error(res.message||'请求失败')returnPromise.reject(newError(res.message||'请求失败'))},error=>{if(error.response&&error.response.status===401){localStorage.removeItem('token')localStorage.removeItem('user')if(window.location.pathname!=='/login'){toast.error('登录已过期,请重新登录')setTimeout(()=>window.location.href='/login',1000)}}else{toast.error(error.response?.data?.message||error.message)}returnPromise.reject(error)})4. Pinia 状态管理
采用 Pinia 的 setup 写法,模块化管理状态:
exportconstuseUserStore=defineStore('user',()=>{constuser=ref(null)constisLoggedIn=ref(false)constrestoreLogin=()=>{consttoken=localStorage.getItem('token')conststoredUser=localStorage.getItem('user')if(token&&storedUser){user.value=JSON.parse(storedUser)isLoggedIn.value=true}}constfetchUserInfo=async()=>{constuserData=awaitgetUserInfo()user.value=userData localStorage.setItem('user',JSON.stringify(userData))}return{user,isLoggedIn,restoreLogin,fetchUserInfo}})三、踩过的坑与经验总结
1. z-index 层级问题
问题:用户下拉菜单被页面其他元素遮挡。
解决:
- 使用
2147483647(浏览器最大值)作为关键元素的 z-index - 层级关系:Modal > Navbar > Content
- 确保有定位属性(relative/absolute/fixed)z-index 才生效
.navbar{position:relative;z-index:2147483647;}2. FormData 处理问题
问题:发送 FormData 时强制设置Content-Type: multipart/form-data,导致后端解析失败。
解决:FormData 不要手动设置 Content-Type,让浏览器自动处理(会自动加上正确的 boundary)。
3. 文件上传安全
踩坑:直接使用用户上传的文件名,可能存在路径遍历攻击风险。
解决:
- 使用
secure_filename处理文件名 - 配合 UUID 防止文件名冲突
4. 命名规范统一
经验:
- 后端使用 Python 下划线命名(pet_name)
- 前端使用 JavaScript 驼峰命名(petName)
- 通过统一响应格式函数自动转换,避免前后端不一致问题
四、项目收尾与成果
经过这轮优化,项目已经具备了:
完善的用户认证体系:登录、登出、Token 管理
便捷的宠物管理功能:增删改查、头像上传
优秀的文件上传体验:本地文件 + URL 两种方式
流畅的前后端交互:统一响应、错误处理完善
清晰的代码架构:模块化设计、易于维护
后续优化方向
虽然核心功能已经完成,但仍有优化空间:
- 图片优化:上传前压缩、裁剪、生成缩略图
- 性能优化:CDN 加速、请求缓存、异步处理
- 功能扩展:更多宠物健康相关功能
- 部署上线:生产环境配置、运维自动化
