资讯动态

FastAPI+Vue3全栈实战:电子相册管理系统开发

发布时间:2026/9/5 21:48:55 来源:尧图企业网站定制
基于 Python 的电子相册管理系统FastAPI Vue3是一个很典型的毕业设计或课程设计选题。表面需求只是“用户注册登录、创建相册、上传照片、查看照片”但真正写起来会同时牵扯文件上传、JWT 登录鉴权、数据表外键关系、前端 axios 封装、跨域配置和静态图片访问这一整条链路。很多同学直接把前端和后端代码堆在一起结果卡在最常见的几个地方后端接口在 Swagger 文档里能调用前端却 401图片上传成功页面上却显示不出来相册删除后照片文件还在磁盘上。这篇文章以一个可运行的网络相册管理系统为例子按照“功能边界 - 数据模型 - FastAPI 后端接口 - Vue3 前端页面 - 前后端联调 - 常见问题排查 - 生产化建议”的顺序推进。整个过程会给出可以直接用于本地的表结构、接口代码、前端请求封装和验证步骤。代码不是完整项目的每一个文件都贴出来但核心链路都会覆盖到读完能对照着补出属于自己的完整项目。1. 先拆需求这个相册系统到底要做什么1.1 一个完整的相册操作闭环网络相册系统不能只理解成“上传图片”。从用户视角看完整闭环应该是这样注册账号并登录登录成功后拿到 token。进入个人相册管理页创建一个相册例如“周末出行”。进入相册后选择本地图片上传上传完成后相册内出现缩略图或原图。点击图片可以预览删除不要的照片。退出相册管理页后再次访问受保护接口时要能识别当前用户。这个闭环决定了后端不能只写一组简单的增删改查接口。用户 A 创建相册后用户 B 不应该通过手工修改请求参数看到 A 的数据。所以每一个相册、照片接口都必须携带当前登录用户的身份并且在后端通过 user_id 做过滤。这里要注意前端把“删除按钮隐藏”并不算权限控制。用户只要拿到接口地址用浏览器或工具直接发送请求后端就会收到一个合法的用户 ID 和操作目标。所以权限校验必须放在后端不能只靠前端页面做限制。1.2 数据表拆分用户、相册、照片怎样关联相册系统最少有两种备选设计。一种是只用一张 photos 表每条记录里放 user_id用户登录后直接按 user_id 查询所有照片。另一种是拆成 users、albums、photos 三张表用外键建立层级关系。推荐后者。原因很直接照片一旦多起来需要按相册维度管理、删除、批量迁移或做封面。如果没有相册表未来加“把照片移动到另一个分类”这类功能时只能在照片表里加一个 category 字段分类和用户的关系会变得非常混乱。三张表的关系是一个用户拥有多个相册。一个相册属于一个用户。一个相册下有多张照片。一张照片属于一个相册也间接属于某个用户。对应的 SQLite 建表语句可以设计成下面这样。下面的字段能覆盖课程设计的大部分需求。CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, nickname VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE albums ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, name VARCHAR(100) NOT NULL, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE photos ( id INTEGER PRIMARY KEY AUTOINCREMENT, album_id INTEGER NOT NULL, user_id INTEGER NOT NULL, file_name VARCHAR(255) NOT NULL, stored_name VARCHAR(255) NOT NULL, url_path VARCHAR(255) NOT NULL, content_type VARCHAR(50), size_bytes INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (album_id) REFERENCES albums(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE );这里有几个字段值得单独说明字段作用说明username password_hash用户身份不能保存明文密码哈希后保存user_id数据归属所有查询和删除都要按这个字段过滤album_id照片所属相册照片列表总是先定位某个相册file_name原始文件名展示给用户看的名称stored_name磁盘存储文件名实际保存成 uuid避免文件名冲突url_path访问路径数据库只存相对路径域名由前端拼接photos 表里冗余保存了 user_id这个设计看起来会重复因为通过 album_id 已经可以关联到用户。冗余的好处是当需要查询“当前用户全部照片”时不需要先查出用户全部相册 ID再写 IN 查询另一个作用是防止在接口传参时只校验了相册 ID却没有校验相册归属。实际上接口在操作照片前一定会校验“照片的 user_id 是否等于当前用户”。1.3 功能边界和演示范围一个适合课程设计的范围可以收敛为用户模块注册、登录、退出登录前端清 token。相册模块创建相册、查询当前用户自己的相册列表、删除自己的相册。照片模块向指定相册上传照片、查询相册下照片列表、删除照片。图片访问后端以静态目录方式提供/uploads/图片名访问。暂不做的内容例如云对象存储、图片加水印、多用户共享相册、朋友圈式时间线等可以作为后续扩展写在论文或项目文档里反而更能体现你对工程边界的理解。2. 后端环境准备与 FastAPI 项目骨架2.1 Python 环境和依赖版本建议使用 Python 3.10 或更高版本因为新版本对类型标注和异步支持都更好。数据库先使用 SQLite不需要额外安装数据库服务适合课程设计和本地演示。创建虚拟环境并安装依赖mkdir photo-system cd photo-system python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install fastapi uvicorn[standard] sqlalchemy pydantic python-multipart PyJWT python-dotenv这里依赖选择有几个工程原因python-multipart是处理UploadFile和表单登录请求的必要依赖漏掉它会在上传图片或表单登录时收到 500 或 422 错误。密码哈希示例使用 Python 标准库hashlib实现 PBKDF2不额外引入bcrypt或passlib避免这两个库在部分 Python 版本上因为 OpenSSL 兼容问题产生的报错。JWT 使用PyJWT字段名和异常类型比较直观。如果原始项目描述里没有明确指定数据库这里采用 SQLite 是在降低搭建成本。如果后续要接 MySQL只需要改数据库连接字符串并把 SQLAlchemy 模型里的类型按 MySQL 要求微调。2.2 后端目录结构一个干净的目录结构能减少路由导入时踩坑。建议按下面结构组织后端backend ├── app │ ├── __init__.py │ ├── config.py │ ├── database.py │ ├── models.py │ ├── schemas.py │ ├── security.py │ ├── main.py │ └── routers │ ├── __init__.py │ ├── auth.py │ ├── albums.py │ └── photos.py ├── uploads ├── requirements.txt └── .envuploads目录用于存放上传图片。FastAPI 启动时会将这个目录挂载成静态资源目录相当于把磁盘目录暴露成 HTTP 可访问路径。这样做适合课程设计和本地演示。2.3 database.py 和 models.py 的实现database.py负责创建 SQLAlchemy 引擎、Session 和 Base 类。SQLite 连接需要加上check_same_threadFalse否则 FastAPI 在多个线程中访问同一个 SQLite 连接时会报错。# app/database.py from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, declarative_base SQLALCHEMY_DATABASE_URL sqlite:///./photo_system.db engine create_engine( SQLALCHEMY_DATABASE_URL, connect_args{check_same_thread: False} ) SessionLocal sessionmaker(autocommitFalse, autoflushFalse, bindengine) Base declarative_base() def get_db(): db SessionLocal() try: yield db finally: db.close()models.py使用 SQLAlchemy 的 Column 风格定义数据模型。虽然有更新一点的 Mapped 风格但 Column 风格在课程设计代码里更容易被阅读。# app/models.py from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, func from sqlalchemy.orm import relationship from .database import Base class User(Base): __tablename__ users id Column(Integer, primary_keyTrue, indexTrue) username Column(String(50), uniqueTrue, indexTrue, nullableFalse) password_hash Column(String(255), nullableFalse) nickname Column(String(50)) created_at Column(DateTime, server_defaultfunc.now()) albums relationship(Album, back_populatesowner, cascadeall, delete-orphan) class Album(Base): __tablename__ albums id Column(Integer, primary_keyTrue, indexTrue) user_id Column(Integer, ForeignKey(users.id), nullableFalse, indexTrue) name Column(String(100), nullableFalse) description Column(Text) created_at Column(DateTime, server_defaultfunc.now()) owner relationship(User, back_populatesalbums) photos relationship(Photo, back_populatesalbum, cascadeall, delete-orphan) class Photo(Base): __tablename__ photos id Column(Integer, primary_keyTrue, indexTrue) album_id Column(Integer, ForeignKey(albums.id), nullableFalse, indexTrue) user_id Column(Integer, ForeignKey(users.id), nullableFalse, indexTrue) file_name Column(String(255), nullableFalse) stored_name Column(String(255), nullableFalse) url_path Column(String(255), nullableFalse) content_type Column(String(50)) size_bytes Column(Integer) created_at Column(DateTime, server_defaultfunc.now()) album relationship(Album, back_populatesphotos)relationship里的cascadeall, delete-orphan实现 ORM 层面的级联删除。删除用户时他名下的相册能自动删除删除相册时相册下的照片数据库记录也会被删除。这个功能在代码里很顺手但磁盘上的图片文件不会自动删除需要业务代码单独处理后面会讲到。2.4 启动入口和静态目录挂载main.py是整个 FastAPI 应用的入口。需要完成四件事装配 CORS 中间件、注册路由、创建数据库表、挂载 uploads 静态目录。# app/main.py from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from .database import Base, engine from . import models # noqa: F401 from .config import UPLOAD_DIR from .routers import auth, albums, photos # 建表课程设计阶段够用 Base.metadata.create_all(bindengine) # 确保图片目录存在 UPLOAD_DIR.mkdir(parentsTrue, exist_okTrue) app FastAPI(title网络相册系统 API) # 本地联调开放 Vue3 开发服务器端口 app.add_middleware( CORSMiddleware, allow_origins[http://localhost:5173, http://127.0.0.1:5173], allow_credentialsTrue, allow_methods[*], allow_headers[*], ) app.include_router(auth.router) app.include_router(albums.router) app.include_router(photos.router) # 将 uploads 目录暴露为 http://127.0.0.1:8000/uploads/xxx.jpg app.mount(/uploads, StaticFiles(directoryUPLOAD_DIR), nameuploads)Base.metadata.create_all只在表不存在时创建表不会像迁移工具那样识别已有表的字段变化。修改模型字段后如果表已经存在可能需要删除本地 db 文件重新生成。这个能力只适合开发阶段生产环境应该使用 Alembic。在config.py中统一管理配置避免多个文件各自维护路径。# app/config.py from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent UPLOAD_DIR BASE_DIR / uploads SECRET_KEY change-this-in-production ALGORITHM HS256 ACCESS_TOKEN_EXPIRE_MINUTES 60 * 24 ALLOWED_IMAGE_EXTENSIONS {jpg, jpeg, png, gif, webp} MAX_UPLOAD_SIZE 10 * 1024 * 10243. FastAPI 关键接口认证、相册、照片上传与权限控制3.1 密码哈希与 JWT 工具函数密码不能明文存进数据库。示例使用标准库实现 PBKDF2 哈希每个用户生成随机盐再用固定迭代次数生成摘要。取盐和哈希值一起存进数据库。验证时从数据库取出原始字符串拆出盐后重新计算最后用hmac.compare_digest做常量时间比较。# app/security.py import hashlib import hmac import os from datetime import datetime, timedelta, timezone import jwt from .

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价