
基于 Python 的电子相册管理系统后端采用 FastAPI前端采用 Vue3本质是一套前后端分离的 Web 相册应用。它解决的问题很具体把散落在本地文件夹里的照片变成可以在浏览器里按相册分类、在线预览、随时上传删除的图片资源库。如果你正在准备 Python 课程设计或毕业设计或者想通过一个完整项目把 FastAPI、Vue3、SQLAlchemy、axios 这些技术串起来这个项目从架构到编码都值得完整过一遍。下面按我实际搭建时的顺序整理成文尽量把每一步为什么这么做也讲清楚。1. 它到底是个什么项目值不值得拿来做课设1.1 核心功能先拆清楚一个电子相册管理系统最基础的功能不是界面好看而是能完成这几件事照片上传把本地图片传到服务器而不是只在浏览器里展示本地文件。相册组织可以新建相册、删除相册把照片归类到不同相册。列表与预览在网页里按相册查看照片点击后能看到大图。图片管理删除图片、调整所属相册部分场景还带简单检索。数据存储图片的元数据记录在数据库图片文件落在服务器磁盘。从项目标题看这个系统主要面向两类人一类是正在找 Python 毕业设计或者课程设计题目的同学另一类是想学 FastAPI Vue3 全栈开发的新手。我建议不要把它理解成网盘或云相册它的重点是把图片当成可管理资源完整跑通上传、存储、查询、展示、删除这条闭环。1.2 为什么是 FastAPI Vue3 这套组合后端选 FastAPI主要是三个原因开发效率高类型声明看过一遍就能上手适合课程设计周期。自带 /docs 交互式接口文档前后端联调时不用盲猜请求参数。支持异步处理后续扩展批量上传或文件压缩时有一定余地。前端选 Vue3核心是组件化思路。相册列表、照片卡片、上传按钮、预览弹窗都可以拆成独立组件再用 Vue Router 管理页面跳转用 axios 统一发接口请求整个项目结构非常清晰。选型判断标准很简单如果需要的是一套能演示、能继续改、结构讲得清楚的项目FastAPI Vue3 比传统 Django 模板渲染更像真正的前后端分离产品答辩时也更容易讲出层次。2. 环境准备先把 Python、Node 和依赖装明白2.1 Python 侧需要装什么建议给后端单独建一个虚拟环境避免本机已有的包把项目带乱。python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install fastapi uvicorn sqlalchemy pillow python-multipart最容易漏掉的是python-multipart。FastAPI 要接收图片上传依赖它解析表单数据。我第一次搭的时候没装项目启动后只要定义带File参数的接口启动阶段就会直接报缺少依赖光看报错提示不一定能马上反应过来是这种基础依赖问题。Pillow也可以现在就装。它在项目里的主要用途有两个读取图片尺寸生成缩略图。相册列表如果直接加载原图图片一多页面会明显变慢后端生成小尺寸缩略图后前端加载性能会改善不少。2.2 Node 侧准备 Vue3 工程前端需要 Node.js。先检查基础环境node -v npm -v创建 Vue3 项目用 Vite 脚手架最省事npm create vuelatest photo-front cd photo-front npm install npm install axios vue-router element-plusnpm create vue会问很多可选功能第一次跑可以全部选 No先把最小工程跑起来后面缺什么再加。这样生成的目录精简也更容易定位问题。如果 Node 版本太旧Vite 可能直接起不来这种情况不要怀疑代码先看版本是否在支持范围内。2.3 数据库和图片存储怎么做取舍课程设计和真实生产环境的差别往往不在代码写法而在存储设计。常见做法是用 SQLite 存储图片的元数据比如文件名、路径、所属相册、上传时间。原始图片文件直接写到磁盘上的 uploads 目录。数据库只保存图片的相对访问路径而不是把图片二进制塞进数据库。这样设计有三个好处数据库体积不会因为图片数量增加而快速膨胀。图片访问走静态文件服务读取效率高。备份时只需要同时备份数据库文件和 uploads 目录。我一般会建两张核心表相册表和图片表。如果后面要加用户登录再补 user 表并在图片表里加 user_id 字段。一个典型的 SQLAlchemy 模型如下from datetime import datetime from sqlalchemy import Column, Integer, String, DateTime, ForeignKey from sqlalchemy.orm import declarative_base Base declarative_base() class Album(Base): __tablename__ album id Column(Integer, primary_keyTrue, indexTrue) name Column(String(100), nullableFalse) created_at Column(DateTime, defaultdatetime.now) class Photo(Base): __tablename__ photo id Column(Integer, primary_keyTrue, indexTrue) album_id Column(Integer, ForeignKey(album.id)) file_name Column(String(255)) url Column(String(255)) created_at Column(DateTime, defaultdatetime.now)model 只做了最小字段够用。课设阶段不建议一上来设计十几张表先跑通主流程再根据需求补字段更稳妥。3. 后端接口怎么做上传、相册和图片服务3.1 项目目录不能全堆在 main.py 里一个只有几十行的 demo 放在 main.py 没问题但相册系统涉及接口、模型、配置、文件处理继续堆下去很难维护。课程设计阶段目录结构本身就是评分点之一。app/ main.py # FastAPI 实例、全局配置、路由注册 database.py # 数据库连接 models.py # ORM 模型 schemas.py # 请求和响应数据结构 routers/ album.py # 相册接口 photo.py # 图片上传与删除接口 uploads/ # 图片文件目录 requirements.txtmain.py 只负责创建应用、注册路由、挂载静态目录具体业务逻辑放在 routers 里。这样任何人拿到代码都能在几分钟内找到要改的地方。3.2 图片上传接口是核心链路上传逻辑本身不复杂但需要做好几件常规处理文件类型要过滤避免任意文件被传上来。文件名不能直接用用户上传的原名避免重名和特殊字符问题。保存路径和数据库记录要保持一致否则前端拿不到图片。文件大小要做限制防止大文件把磁盘占满。一个最小可用的上传接口大概是下面这样import uuid from pathlib import Path from fastapi import APIRouter, File, HTTPException, UploadFile router APIRouter(prefix/api/photo, tags[photo]) ALLOWED_EXT {.jpg, .jpeg, .png, .gif, .webp} UPLOAD_DIR Path(uploads) UPLOAD_DIR.mkdir(exist_okTrue) router.post(/upload) async def upload_photo(file: UploadFile File(...), album_id: int 0): ext Path(file.filename).suffix.lower() if ext not in ALLOWED_EXT: raise HTTPException(status_code400, detail不支持的图片格式) content await file.read() # 实际项目中要增加文件大小判断 # if len(content) 10 * 1024 * 1024: # raise HTTPException(status_code400, detail图片不能超过 10MB) new_name f{uuid.uuid4().hex}{ext} save_path UPLOAD_DIR / new_name save_path.write_bytes(content) # 这里再落一条数据库记录 return {url: f/uploads/{new_name}, size: len(content)}文件名用 uuid 生成核心原因是避免多用户同时上传同名文件时互相覆盖。真实上传场景里photo_1.jpg这种命名迟早会撞车uuid 方案省掉很多麻烦。如果只想上传图片建议做一层扩展名白名单校验。这里的判断很简单但能挡住不少非图片请求也给答辩增加一个安全细节。3.3 相册接口不要贪多有了上传还需要配套的相册接口和图片查询接口。比较核心的一组是方法接口说明POST/api/album新建相册GET/api/album获取相册列表POST/api/photo/upload上传图片GET/api/photo?album_idxxx获取相册下的图片列表DELETE/api/photo/{id}删除图片接口数量不要求多重点是每个接口都能讲清输入和输出。设计文档里如果能写清楚参数、返回字段、错误码答辩效果通常比做一堆页面更稳。删除图片时要注意数据库记录和磁盘文件一起删除。很多同学只删数据库记录不删磁盘文件磁盘空间会越用越大。课程设计里这种细节很容易被老师追问。3.4 图片文件怎么让前端访问图片上传到 uploads 目录后浏览器还不能直接访问目录需要在 FastAPI 中把 uploads 挂载成静态资源目录from pathlib import Path from fastapi import FastAPI from fastapi.staticfiles import StaticFiles Path(uploads).mkdir(exist_okTrue) app FastAPI(title电子相册管理系统) app.mount(/uploads, StaticFiles(directoryuploads), nameuploads)这里有个常见坑如果 uploads 目录不存在挂载静态目录时可能直接启动失败。所以要先执行Path(uploads).mkdir(exist_okTrue)再执行 mount。看起来是小细节但对第一次运行项目的人非常关键。数据库里保存的 URL 使用相对路径/uploads/xxx.jpg前端在页面里组合成完整地址访问。这样后端域名或端口变化时只要前端代理跟着变图片路径不用改。4. 前端页面相册列表、图片展示和上传交互4.1 页面路由怎么规划Vue3 前端通常分成几个视图页面相册列表页展示所有相册。相册详情页展示某个相册下的照片提供上传和删除操作。如果有登录功能再加登录页和注册页。用 Vue Router 配置时建议用动态导入做路由级代码分割import { createRouter, createWebHistory } from vue-router const routes [ { path: /, component: () import(/views/AlbumList.vue) }, { path: /album/:id, component: () import(/views/AlbumDetail.vue) } ] const router createRouter({ history: createWebHistory(), routes }) export default router动态导入后首屏不会一次性把所有页面代码都加载下来。这个优化不大但属于工程上看得见的细节。4.2 axios 封装和开发联调在开发环境前端默认跑在 5173 端口后端跑在 8000 端口两者端口不一样跨域问题就会出现。解决方式有两种后端加 CORS 中间件允许前端域名跨域访问。前端在 Vite 里配置代理把 /api 和 /uploads 转发给后端。我更推荐第二种因为代理方案更接近生产环境的同源部署// vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { proxy: { /api: http://127.0.0.1:8000, /uploads: http://127.0.0.1:8000 } } })这样前端代码里请求/api/albumVite 会自动转发到后端的http://127.0.0.1:8000/api/album。图片路径/uploads/xxx.jpg也能正常显示不会因为端口不同而 404。axios 建议统一封装一个实例设置超时时间并在响应拦截器里直接取出数据import axios from axios const request axios.create({ baseURL: /, timeout: 15000 }) request.interceptors.response.use( response response.data, error Promise.reject(error) ) export default request课程设计阶段不需要封装得非常复杂但统一入口的好处是后期加 token 认证时只需要改一个文件。4.3 上传组件实现与字段名对齐用 Element Plus 的 upload 组件体验好一些但很多课程项目里直接用原生 input 也可以。要点是 FormData 字段名必须和后端参数名一致。input typefile acceptimage/* multiple changehandleUpload /import request from /utils/request async function handleUpload(event: Event) { const input event.target as HTMLInputElement if (!input.files?.length) return const file input.files[0] const form new FormData() form.append(file, file) form.append(album_id, String(albumId.value)) const res await request.post(/api/photo/upload, form) // 上传成功后刷新图片列表 loadPhotos() }这里最容易翻车的点有两个后端参数名是file前端 FormData 里却写成了image接口直接返回 422。axios 里手动给 FormData 设置Content-Type: multipart/form-data可能导致 boundary 信息丢失后端解析不到文件。正确做法是前端把 FormData 直接交给 axios其他由浏览器处理。页面展示图片时直接把接口返回的/uploads/xxx.jpg放到img的 src 里只要前端代理配好图片就能正常加载。5. 联调验证顺序先单张再批量不要上来就一把梭5.1 第一步先启动后端用接口文档自测联调不要直接从前端页面开始我一般会先把后端跑起来uvicorn app.main:app --reload --host 0.0.0.0 --port 8000随后打开http://127.0.0.1:8000/docs用 Swagger UI 直接测试上传接口。这一步可以帮助确认后端独立可用把前后端问题隔离开。如果上传接口都通不过就不用急着看前端。判断标准很直接接口返回 JSON包含图片 URL。uploads 目录里出现一个新文件。数据库 photo 表新增一条记录。三步都满足后端链路就通了。5.2 第二步再启动前端看页面代理是否正常后端确认没问题后再启动前端cd photo-front npm run dev打开前端页面先做一次最小验证新建相册上传一张图片刷新页面确认图片显示。如果页面能调到接口但图片裂开问题大概率出在静态资源访问或代理配置上而不是图片没传上来。接口通了、图片却不显示的情况在联调里极其常见原因往往就是/uploads没有一起代理。所以 Vite 代理里要同时包含/api和/uploads。5.3 第三步从单张测试切到批量任务单张没问题不代表批量没问题。批量上传会暴露出几个单张时看不到的问题文件大小差异有些图片只有几十 KB有些是几 MB处理时间差异很大。重名问题不同目录下可能存在大量同名图片。失败重试网络抖动或文件损坏时某一张失败不能影响整批。并发压力一次性同时上传几十个文件后端内存和磁盘占用会明显上升。简单场景下前端不推荐用Promise.all一次性把所有请求都发出去。更稳的方式是一个接一个循环上传或者限制同时上传数量。我在课设里通常用顺序循环for (const file of files) { const form new FormData() form.append(file, file) form.append(album_id, String(albumId.value)) try { await request.post(/api/photo/upload, form) } catch (e) { // 记录失败文件名最后统一提示 failList.push(file.name) } }顺序上传速度会慢一点但实现简单不容易把后端打崩也方便定位哪张图片失败。课程设计阶段的演示场景顺序上传完全够用。5.4 批量测试时要盯住哪些信号不能只看界面上有没有弹成功提示。我建议同时观察四个信息后端控制台日志是否有异常抛出。uploads 目录文件数量是否和上传成功数量一致。服务器内存和磁盘占用是否出现明显异常。失败图片是集中在某个固定格式还是随机分布。如果所有 jpg 都成功、某种格式全部失败优先怀疑扩展名校验逻辑。如果图片大小差异导致超时优先处理前端超时时间和上传限制。6. 课程设计/毕业设计场景怎样把系统做得更完整6.1 加入用户登录和权限管理如果只做管理员一个角色硬编码账号也能演示但缺少完整度。更合理的做法是加 user 表实现注册、登录、JWT 认证注册时对密码做哈希存储不要明文存数据库。登录成功后签一个 token 返回前端。前端把 token 存在 localStorage请求时放到 Authorization 请求头。创建相册和上传图片时记录当前用户 ID只能访问自己的数据。FastAPI 侧做 JWT 并不复杂用 pyjwt 或 python-jose 签发和校验 token 都是常见方案。这一步做完系统的完整度会上升一个档次因为从“单机相册工具”变成了“多人可用的小型产品”。6.2 缩略图、搜索和图片信息展示相册详情页如果图片很多每次都加载原图会卡。后端可以在上传成功后用 Pillow 生成一张缩略图from PIL import Image im Image.open(save_path) im.thumbnail((400, 400)) thumb_path UPLOAD_DIR / f{uuid.uuid4().hex}_thumb.jpg im.save(thumb_path)数据库里增加缩略图 URL 字段列表页加载缩略图点击后再看原图。这个优化肉眼可见而且 Pillow 的用法是很常见的考点。搜索方面不需要做太复杂的检索引擎用 SQL 的 LIKE 模糊匹配相册名称和图片文件名就可以。答辩时能讲解清楚搜索条件和排序逻辑比炫技更重要。6.3 答辩材料里最该准备的四样东西技术项目做完只是第一步课程设计和毕业设计还需要能把过程讲清楚。我建议提前准备README 文档写清环境版本、安装步骤、启动命令和默认账号。架构说明画清前后端请求流程和目录结构不用画复杂系统架构图。接口表列出每个接口的方法、地址、输入参数、返回结果。测试记录把功能测试结果整理成一个表格包括测试操作、预期结果、实际结果。答辩时常见的追问是“为什么图片不直接存数据库”。照着第 2 节的存储设计思路讲就可以重点说明数据库存元数据、磁盘存文件、静态服务做访问这是主流做法。6.4 代码管理和配置分离的早期习惯很多课设项目到后期会变得很乱原因是不做代码管理。哪怕只有一个人写也建议从开始就使用 Git 提交代码每完成一个功能点就提交一次。后端依赖用 requirements.txt 固定前端依赖由 package.json 管理。把数据库连接、上传目录、端口、JWT 密钥这类配置放到单独文件或环境变量里不要在代码里到处写死。后面部署到其他机器时只需改一处配置。这些工程习惯在写代码时看不出价值等到答辩演示换一台机器跑项目时就会明白有多省事。7. 最容易翻车的五个点实测排查顺序7.1 后端能启动前端接口调不通出现这种问题按固定顺序排查看前端浏览器 Network 面板请求是否真的发出。看请求地址是相对路径还是绝对路径。看 Vite 代理是否覆盖了该接口路径。看后端控制台有没有收到请求。看是否存在 CORS 阻断。如果后端完全没收到前端请求重点查前端代理和后端端口如果后端收到但返回 403再考虑 CORS 配置是否正确。7.2 上传接口报 422422 在 FastAPI 里通常代表请求格式不符合接口定义。常见原因表单字段名不对后端要file前端传了image。请求没有使用 multipart/form-data。JSON 请求体中带了文件数据但后端声明的是 UploadFile。处理的唯一思路是先在 Swagger UI 里试一次确认接口本身没问题再回头检查前端代码。7.3 图片能上传但页面显示不出来这类问题最容易被误判成后端代码写错其实通常是访问链路的配置问题。直接在浏览器地址栏输入图片 URL看能否打开。如果打不开检查 StaticFiles 挂载路径和数据库保存的 URL 是否一致。如果开发环境打不开、上线后能打开检查 Vite 代理是否包含 /uploads。如果目录权限不对文件在但读不出来。排查时先把“文件存不存在”和“URL 对不对”分开问题会清楚很多。7.4 中文文件名和特殊字符导致异常用户上传的图片经常叫“旅行-2025-01.jpg”或直接叫“照片 1.jpg”如果直接拿原名落盘既可能重名也可能出现乱码。后端保存文件时改用 uuid 命名原本文件名只存数据库做展示安全问题也能少很多。7.5 批量上传时 SQLite 报错或任务卡住SQLite 在并发写入场景下偶尔会出现数据库锁异常。课程设计的数据量通常不大但也不要人为制造大量并发。批量上传时建议采用顺序写入或在写操作密集场景下减少高频并发提交。如果系统要真正服务很多人上传把数据库从 SQLite 切换成 MySQL 或 PostgreSQL 即可。SQLAlchemy 的好处是模型层改动很小只需改数据库连接配置这也是一种功能扩展思路。最后说一点实际体会。这类课设项目真正拉开差距的地方往往不在用了多新的框架而在于你有没有把上传、存储、展示、删除这条完整链路跑通并能在答辩时把每个环节为什么这样设计讲清楚。先让一张图片的数据完整走通再考虑用户、权限、批量、缩略图整个项目的复杂度会变得非常可控。我建议拿到项目后不要急着写页面先把后端上传接口、文件目录和数据库记录这三件事打通剩下的功能都是在给这条主干加分支。