☰
Vue+Flask前后端分离实战:个人穿衣搭配网上衣橱系统开发
2026/10/14 12:42:51 网站建设 项目流程

前几天帮一位做服装工作室的朋友落地了一个“个人穿衣搭配分享网上衣橱系统”,项目全称就叫“基于 Vue 与 Python Flask 的个人穿衣搭配分享网上衣橱系统”。说白了,就是把“衣柜”搬到网上,既能管理自己有哪些衣服,又能把衣服组合成搭配方案,还能公开发布分享、让其他用户收藏参考。这个项目对服装专业的学生、独立穿搭博主、想练手前后端分离开发的人都很合适。它不算大,但把用户系统、文件上传、推荐逻辑、社区互动全串起来了,做完之后你对“一个真实业务系统是怎么从需求变成代码”的理解会有质的提升。

1. 项目概述与需求拆解

1.1 这个系统到底解决什么问题

很多人的衣柜里其实堆了三五十件衣服,但每天早上还是觉得“没衣服穿”。原因很简单:你记不清自己有哪些单品,更忙不过来去考虑它们之间怎么组合。市面上的衣橱管理工具要么只是记账式的清单,要么偏电商导购,真正把“整理衣柜”和“搭配分享”两端接起来的并不多。

这个项目做的,就是让用户先把衣服拍照录入,给衣服打上分类、颜色、季节、场合等标签,然后在一个虚拟衣橱里自由组合出穿搭方案。方案保存后,既可以作为一种“自己的搭配备忘”,也可以发布到社区的分享广场,其他用户能浏览、点赞、收藏。朋友的需求里还有一条:希望以后能加“AI 推荐搭配”,所以技术选型上必须给算法扩展留空间。Python 生态里 Pillow 处理图片、后续接机器学习模型都顺手,这就成了选 Flask 而不是其他后端的重要原因。

1.2 功能拆分与角色视角

系统按用户身份简单分两类:普通注册用户和管理员。

普通用户最关心的功能是“录入衣服要快,搭配要直观”。所以前端要有一个清晰的“添加衣物”表单,支持拍照上传、自动生成缩略图;衣橱页面要能按分类筛选;搭配操作应该像“把卡片拖到一起”那样直观,而不是在表格里一条条填数据。分享功能则要把搭配方案中的单品格子图拼成一张分享图,配合一段穿搭说明一起发布。

管理员侧的核心是内容审核和基础数据维护。社区分享一旦开放,就会出现广告、违禁图片等问题,管理员需要在后台看到最新分享列表,能下架违规内容。另外就是标签管理:衣服分类、场景标签这些数据由管理员统一维护,用户在录入衣服时只能从已有标签中选择,这样后续做筛选和推荐才有干净的数据基础。

1.3 功能模块清单

模块子功能说明
用户模块注册、登录、个人资料使用 JWT 做登录态,支持修改头像和昵称
衣橱管理衣物增删改查、图片上传支持分类筛选、关键字搜索
搭配模块创建搭配、拖拽组合、保存方案搭配由多个衣物记录组成,可设置主题
分享社区发布穿搭帖、点赞、收藏帖子关联搭配方案,展示统一分享图
后台管理内容审核、标签管理、用户禁用管理员独立后端页面
系统基础接口鉴权、全局异常、日志Flask 蓝图模块化搭建

这套模块拆分的核心思想是“衣橱是基础数据,搭配是业务数据,分享是展示数据”。三层各司其职,数据库表也好设计,后续加推荐算法时,只需要在衣物标签和搭配历史记录上做文章。

2. 技术选型与架构设计

2.1 为什么是 Flask + Vue,而不是其他组合

选型时我们对比过 Django + Django模板、Flask + Vue、Spring Boot + Vue 三套方案。Spring Boot 太重,一个衣橱系统没必要请出 Java 全家桶;Django 自带 Admin 后台虽然方便,但它的模板体系和 Vue 配合反而膈应,前后端耦合度也不理想。最终选了 Flask + Vue,理由很实在:

  • Flask 轻量,一个工厂函数就能创建应用,蓝图模块化后代码结构非常清晰。
  • Python 写后端,后续做图片识别、搭配推荐可以直接复用同一套环境。
  • Vue 组件化适合“衣物卡片”“搭配预览”这类界面,数据驱动更新,拖拽交互也有现成库可用。
  • 前后端分离后,后端只要提供稳定 API,前端页面随便改,不影响业务逻辑。

2.2 前后端数据流设计

前端是 Vue 单页应用,后端只提供 JSON 接口。用户打开页面后,前端通过 Axios 调用后端 API,带上 JWT Token 标识身份。后端校验通过后返回数据,前端把数据渲染成组件。

这里有个容易踩坑的点:跨域。开发环境下前端跑在 8080 端口,后端跑在 5000 端口,浏览器默认会拦截跨域请求。我们在 Flask 里统一挂了 flask-cors,并且明确允许 Authorization 请求头,这样登录后才能正常请求受保护的接口。生产部署时前后端同域,通过 Nginx 把/api转发到 Flask,前端静态文件由 Nginx 直接托管,跨域问题自然消失。

2.3 后端工程结构

Flask 项目不能全写在一个 app.py 里,否则项目一到 3000 行代码就没法维护。我用的结构是:

app/ __init__.py # 应用工厂 extensions.py # db, jwt, cors 实例 models/ # SQLAlchemy 模型 api/ auth.py wardrobe.py outfit.py share.py admin.py utils/ response.py # 统一返回格式 upload.py # 图片处理 config.py run.py

应用工厂模式的好处是,测试环境和生产环境可以创建不同的 app 实例,配置也互不干扰。刚开始的朋友容易犯的毛病是“全局变量一把梭”,结果写单元测试时没法隔离数据库。用工厂模式以后,这个问题从一开始就规避了。

2.4 前端工程结构

前端用 Vue CLI 创建项目,我把页面按业务拆成几个目录:

src/ api/ # 封装 axios 请求 assets/ components/ # 衣物卡片、搭配面板等 router/ store/ # Pinia 状态管理 views/ Home.vue Wardrobe.vue Outfit.vue Share.vue Admin.vue

页面组件只负责“怎么展示”,业务状态放在 Pinia 里。比如用户登录信息存到 Pinia,配合 localStorage 持久化,刷新页面也不会丢登录态。所有请求的加载状态、错误弹窗,放到 axios 拦截器里统一处理,每个页面就不用重复写一堆try/catch。

3. 核心业务与数据库设计

3.1 实体关系梳理

整个系统离不开四个核心实体:用户、衣物、搭配、分享帖。它们的关系是:

  • 一个用户拥有多件衣物,衣物属于某个用户。
  • 一个搭配方案包含多件衣物,一件衣物也可以出现在多个搭配中,所以搭配和衣物是多对多关系。
  • 一个分享帖对应一个搭配方案,用户在搭配保存后可以将其发布为分享帖。
  • 分享帖可以被其他用户点赞和收藏,点赞和收藏都是多对多关系。

为了维护“衣物到搭配”的多对多关系,我建了一张关联表outfit_items,里面只存 outfit_id 和 item_id。这样既不破坏衣物的独立性,又能让搭配做到“组合衣柜里已有的衣服”,而不是复制一份衣服数据过来。

3.2 核心表结构设计

下面是几张比较关键的表,字段已经裁剪到够用级别。

表名字段说明
usersid, username, password_hash, nickname, avatar_url, role, status, created_atrole 区分 user/admin,status 控制封禁
itemsid, user_id, name, category, color, season, occasion, image_url, note, created_atcategory/season/occasion 存标签标识
outfitsid, user_id, title, description, cover_image_url, is_public, created_atis_public 控制是否可被他人看到
outfit_itemsid, outfit_id, item_id, sort_order记录搭配内单品的排列顺序
sharesid, outfit_id, author_id, content, like_count, favorite_count, status, created_atstatus 标记审核状态
tagsid, type, name, sort类型区分分类、季节、场合

这里要特别说一下sort_order。搭配里的衣服顺序不是随意排的,用户拖拽调整顺序后,前端会把顺序一起提交,后端按 sort_order 排序返回,这样分享图中单品的格子顺序就和用户看到的一致。我之前偷懒没设计这个字段,结果保存的搭配总是随机顺序,后来加了排序字段才好。

3.3 图片存储方案

衣物图片是整个系统的门面,图片存储设计不好,系统用起来就很痛苦。图片数据不适合直接塞进 MySQL,也不适合用 base64 字符串存到数据库里。项目开发阶段我把图片放到后端的uploads/items/目录,数据库里只存相对路径;生产部署时把该目录交给 Nginx 托管。

图片处理上,上传接口会同时生成原图和缩略图。缩略图统一压缩到 300x300 像素左右,衣橱网格页面全部渲染缩略图,只有查看大图时才加载原图。这样做的收益非常明显:缩略图加载快,流量消耗小,页面滚动也流畅。生成缩略图我用的是 Pillow,代码很简单,但要注意保持宽高比,裁剪时用居中裁剪,否则图片会变形。

3.4 权限控制简析

普通用户只能操作自己的衣物和搭配,管理员可以审核删除他人的分享。实现上就是两个层次:

  • 接口层:登录接口校验 JWT,获取当前用户。
  • 数据层:查询时强制带上current_user.id,管理员调用审核接口时校验role == 'admin'。

权限控制不要只在前端写“这个按钮只有管理员可见”,后端每个接口都必须校验。很多人会忽略这个,结果前端把按钮藏了,但直接调用 API 还是能绕过,这是上线前必须检查的漏洞点。

4. 后端 Flask 核心实现

4.1 应用工厂与蓝图注册

后端入口文件只做一件事:创建 app,初始化扩展,注册蓝图。

# run.py from app import create_app app = create_app() if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)
# app/__init__.py from flask import Flask from .extensions import db, jwt, cors from .api import auth, wardrobe, outfit, share, admin def create_app(config_name='default'): app = Flask(__name__) app.config.from_object(config[config_name]) db.init_app(app) jwt.init_app(app) cors.init_app(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True) app.register_blueprint(auth.bp, url_prefix='/api/auth') app.register_blueprint(wardrobe.bp, url_prefix='/api/wardrobe') app.register_blueprint(outfit.bp, url_prefix='/api/outfits') app.register_blueprint(share.bp, url_prefix='/api/shares') app.register_blueprint(admin.bp, url_prefix='/api/admin') with app.app_context(): db.create_all() return app

蓝图配上统一 url_prefix,接口路径清晰,前端也可以按前缀做接口分组。

4.2 统一返回结构与错误处理

前后端联调时最怕接口返回格式不统一。有的接口出错返回字符串,有的返回 JSON,前端就没法统一解析。我从一开始就固定了返回格式:

def ok(data=None, message='success'): return {'code': 0, 'message': message, 'data': data} def fail(message='error', code=1): return {'code': code, 'message': message, 'data': None}

同时用 Flask 的@app.errorhandler捕获全局异常,数据库错误、文件上传错误、请求参数错误都返回统一的 JSON 结构。前端只看code就能判断成功失败,错误信息直接弹 toast。

4.3 衣物上传接口实现

衣物上传是用户操作频率最高的接口,实现时要同时处理文件和表单数据。我建议把文件字段和衣物信息一次请求搞定,不要让前端先传文件拿到 URL 再提交表单——那样要多一个请求不说,还会产生大量没用到的孤儿图片。

核心代码如下:

# app/api/wardrobe.py import os, uuid from flask import Blueprint, request, current_app from werkzeug.utils import secure_filename from PIL import Image from ..models import db, Item from ..utils.auth import login_required from ..utils.response import ok, fail bp = Blueprint('wardrobe', __name__) ALLOWED_EXT = {'png', 'jpg', 'jpeg', 'webp'} def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXT @bp.route('', methods=['POST']) @login_required def add_item(current_user): file = request.files.get('file') if not file or not allowed_file(file.filename): return fail('图片格式不支持') # 秒传文件名用 uuid,避免中文名和重名问题 ext = file.filename.rsplit('.', 1)[1].lower() filename = f'{uuid.uuid4().hex}.{ext}' upload_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'items') os.makedirs(upload_dir, exist_ok=True) filepath = os.path.join(upload_dir, filename) file.save(filepath) # 生成缩略图 thumb_name = f'thumb_{filename}' thumb_path = os.path.join(upload_dir, thumb_name) img = Image.open(filepath) img.thumbnail((300, 300)) img.save(thumb_path, quality=85) item = Item( user_id=current_user.id, name=request.form.get('name'), category=request.form.get('category'), color=request.form.get('color'), season=request.form.get('season'), occasion=request.form.get('occasion'), image_url=f'/uploads/items/{filename}', thumb_url=f'/uploads/items/{thumb_name}', note=request.form.get('note', '') ) db.session.add(item) db.session.commit() return ok(item.to_dict())

这里有个小细节:缩略图文件名加thumb_前缀,而不是子目录。因为如果缩略图放在不同子目录,前端拼接 URL 时要多加一层逻辑,直接用前缀区分最省事,Nginx 配置/uploads/一个 location 就能覆盖。

4.4 搭配推荐接口的逻辑

朋友特别想要“给我推荐一套今天的穿搭”功能。但我们没有训练任何模型,直接用标签规则引擎就能达到不错的效果。规则如下:

  • 根据当日温度区间选择季节:高于 28 度算夏季,20-28 度算春秋,低于 10 度算冬季。
  • 在该季节衣物里,优先选择最近 30 天没穿过的上衣、下装、外套各一件。
  • 如果用户设置了场合(通勤/约会/运动),优先匹配场合标签。
from datetime import datetime, timedelta def recommend_outfit(user_id): today = datetime.today() season = judge_season(today.month, today.temperature) # 简化示意 query = Item.query.filter_by(user_id=user_id, season=season, status='active') top = query.filter_by(category='上衣').order_by(Item.last_worn_at.asc()).first() bottom = query.filter_by(category='下装').order_by(Item.last_worn_at.asc()).first() outer = query.filter_by(category='外套').order_by(Item.last_worn_at.asc()).first() outfit = create_outfit(user_id, [top, bottom, outer]) return outfit

“最近没穿过”这个规则用last_worn_at排序就能实现,非常朴素但有效。真实推荐系统再复杂,核心也是先缩小候选集,再按用户偏好排序。衣橱推荐用这种轻量逻辑已经能让用户感受到“智能”。

4.5 JWT 登录鉴权细节

登录接口用 Flask-JWT-Extended 实现。注册时密码用generate_password_hash加盐哈希,登录时用check_password_hash校验,成功后返回 access_token。

from flask_jwt_extended import create_access_token @bp.route('/login', methods=['POST']) def login(): data = request.get_json() user = User.query.filter_by(username=data.get('username')).first() if not user or not check_password_hash(user.password_hash, data.get('password')): return fail('用户名或密码错误') if user.status != 'active': return fail('账号已被禁用') token = create_access_token(identity=str(user.id), expires_delta=timedelta(days=7)) return ok({'token': token, 'user': user.to_dict()})

注意identity我传的是字符串形式的 user_id,而不是直接传 user 对象。JWT 的 payload 会被序列化,遇到复杂对象容易出现 JSON 序列化错误。获取当前用户用get_jwt_identity(),再用这个 ID 去数据库查用户,避免把整个用户信息塞进 token。

5. 前端 Vue 核心实现

5.1 衣橱页面的组件拆解

衣橱页面是整个前端交互最重的部分。我把它拆成三个组件:

  • ItemCard.vue:展示单件衣物,包括缩略图、名称、分类、季节标签。
  • FilterBar.vue:分类、季节、场合筛选按钮。
  • AddItemDialog.vue:上传衣物图片的表单弹窗。

ItemCard接收一个item对象作为 prop,内部的图片懒加载用 Vue 的v-lazy指令实现,列表很长时不会卡顿。筛选逻辑放在 Pinia store 里,组件内只对 store 里的过滤结果进行渲染。

5.2 搭配工作台实现

搭配页的体验核心是“把衣物卡片加进来,实时看到组合效果”。我用了左右布局:左侧是可选的衣物列表,右侧是画布区。用户点击左侧单品,右侧就多一个格子;调整格子顺序时,使用原生拖拽事件记录每次改变后的item_id顺序。

保存搭配时,前端把整个单品列表发给后端:

async function saveOutfit() { const outfitData = { title: outfitTitle.value, description: outfitDesc.value, item_ids: canvasItems.value.map(item => item.id) }; const resp = await api.post('/api/outfits', outfitData); // 根据返回的 outfit_id 生成分享图 }

分享图生成我放在前端做。利用canvas把单品缩略图按顺序拼成一张长图,再把标题和描述文字画到底部。这样后端不用引入图像合成库,前端实时生成也方便用户预览。

5.3 状态管理与请求封装

登录状态和用户信息放进 Pinia:

// src/store/user.js export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null') }), actions: { setLogin(token, userInfo) { this.token = token; this.userInfo = userInfo; localStorage.setItem('token', token); localStorage.setItem('userInfo', JSON.stringify(userInfo)); }, logout() { this.token = ''; this.userInfo = null; localStorage.removeItem('token'); localStorage.removeItem('userInfo'); } } });

Axios 拦截器统一带上 token:

api.interceptors.request.use(config => { const store = useUserStore(); if (store.token) { config.headers.Authorization = `Bearer ${store.token}`; } return config; }); api.interceptors.response.use( response => { const res = response.data; if (res.code !== 0) { ElMessage.error(res.message); return Promise.reject(new Error(res.message)); } return res; }, error => { if (error.response && error.response.status === 401) { ElMessage.error('登录已过期,请重新登录'); useUserStore().logout(); router.push('/login'); } else { ElMessage.error('网络请求失败,请稍后重试'); } return Promise.reject(error); } );

401 拦截这个逻辑很关键,否则 token 过期后用户会看到一堆莫名其妙的数据加载失败,而不是被引导回登录页。

6. 实现中的常见问题与排查记录

6.1 跨域请求被拦截

开发联调时前端控制台报CORS policy: No 'Access-Control-Allow-Origin'。原因是我在 Flask 里虽然挂了 CORS,但默认只允许简单请求,前端发的是带Authorization头的复杂请求,需要服务器明确允许该请求头。

解决方法是在 flask-cors 初始化时加上allow_headers=["Content-Type", "Authorization"],并且对/api/*全部开放。生产环境同域部署后不需要这个配置,但开发环境必须有。

6.2 图片上传后访问 404

有个同事问“图片明明上传成功了,怎么打开链接是 404”,排查后发现是 Flask 的静态目录没配对。开发环境下我用send_from_directory提供图片访问服务:

from flask import send_from_directory @app.route('/uploads/<path:filename>') def uploads(filename): return send_from_directory(current_app.config['UPLOAD_FOLDER'], filename)

生产环境则由 Nginx 直接托管/uploads/目录,不再走 Flask。两个环境行为一致,前端代码不需要区分。

6.3 列表刷新后顺序乱跳

衣橱列表按时间倒序显示,但用户刷新后顺序偶尔不一致。原因是数据库里多条记录创建时间相同,排序没有次级条件。解决方法是排序时加上 id 倒序:

Item.query.filter_by(user_id=uid).order_by(Item.created_at.desc(), Item.id.desc())

这种细节不影响功能,但很影响体验。凡是列表页,排序字段一定要加唯一性兜底。

6.4 上传图片尺寸过大

用户手机拍的照片动不动就是 5MB,上传慢还占空间。除了生成缩略图之外,我在前端还加了一层压缩处理:拿到 File 对象后,先通过 canvas 把图片宽度缩到 1200px 以下,再转成 JPEG Blob 上传。这样原图体积控制在 300KB 左右,上传速度明显提升,服务器也轻松很多。

6.5 常见问题速查表

问题表现排查思路
跨域报错浏览器拦截接口响应检查 flask-cors 和 Authorization 头配置
上传图片 404图片文件存在但访问不到检查静态目录映射、文件名编码
登录后接口 401带 token 请求仍无权限检查拦截器是否设置 Bearer Token
列表顺序乱刷新后顺序变化排序字段加唯一兜底字段
图片加载慢衣橱网格卡顿使用缩略图并压缩上传图片

7. 部署与维护经验

7.1 部署架构

这个项目部署并不复杂,一套最朴素的方案就能稳定跑起来:

  • 前端构建:npm run build产出静态文件到dist/。
  • 后端运行:gunicorn 启动 Flask 应用,监听 127.0.0.1:5000。
  • 反向代理:Nginx 托管前端静态文件,/api和/uploads两个路径转发或映射到后端。
  • 进程守护:用 supervisor 守护 gunicorn 进程,崩溃自动拉起。

唯一要提醒的是:Flask 自带的开发服务器(app.run)不能用于生产,它性能差且不具备并发处理能力,必须交给 gunicorn。启动命令大致是:

gunicorn -w 4 -b 127.0.0.1:5000 "app:create_app()"

-w 4表示 4 个 worker 进程,普通服务器足够用。

7.2 上线前检查清单

我每次上线前都会逐项过一遍:

  • SECRET_KEY 是否更换成随机长字符串,不要用代码里的默认值。
  • 数据库连接是否改为生产库,开发库的测试数据必须清除。
  • 图片上传目录是否存在且有写权限。
  • 用户密码是否全部使用哈希存储,数据库里绝不能出现明文密码。
  • 前端接口地址是否改为生产域名或同域相对路径。
  • 管理员账号是否清理默认密码。

7.3 安全加固要点

安全是很多个人项目容易忽视的点。这个系统里我做了几项基础但必要的加固:

  • 密码存储用werkzeug.security的哈希函数,不存明文。
  • 登录接口增加简单的失败次数限制,防止暴力破解。
  • ORM 查询使用参数绑定,不拼 SQL 字符串。
  • 文件上传严格校验扩展名,并对上传目录禁止执行脚本。
  • 分享内容做简单敏感词过滤,管理员后台可下架。

对一个小型衣橱系统来说,这些措施已经能挡住绝大多数常见问题。不要想着一次做到企业级安全,先守住基础,再根据实际使用情况迭代。

8. 项目复盘与后续扩展

8.1 哪些地方做得对

用 Flask + Vue 而不是一个全家桶框架,回头看是对的。前后端分离让两个部分都能独立测试,语法检查、接口调试都方便很多。模块化拆分也让开发节奏很舒服,先完成后端 API,再慢慢调前端页面,两边可以并行。

数据库设计上提前预留了is_public、status这类状态字段,后来加审核功能时几乎没有改表。做业务系统,状态字段一定要提前设计,不然后期只能靠代码硬撑,数据库层面没法表达业务状态。

8.2 哪些地方一开始想复杂了

刚开始我考虑上一套 Redis 做缓存,后来发现没必要。衣橱系统的数据量很小,数据库顺序查询完全够快,加了 Redis 反而要处理缓存失效问题,全是额外负担。

搭配推荐也想用协同过滤算法,做了一半发现训练数据根本不够。最后换成基于规则的推荐,反而效果好、可解释性强。给个人项目上“高大上”的方案前,先问自己:当前数据量够吗?用户基数够吗?如果答案是否定的,优先用简单方案。

8.3 还能怎么扩展

这个系统的地基已经足够稳,后续可以加的方向不少:

  • 接入温度天气接口,搭配推荐自动跟随天气调整。
  • 增加“免费送闲置”模块,让用户把不需要的衣服转赠/交换。
  • 支持多张搭配图,社区帖子变成图片墙。
  • 将前端改造成小程序版,衣橱场景很适合手机端随手拍随手搭。

我个人的建议是:先重点优化拍照上传体验和衣橱浏览流畅度,这两个点直接决定用户会不会长期使用。功能再多,录入衣服太麻烦,用户还是会流失。

做这类系统的最大体会有两点:一是架构别过度设计,越简单的东西越容易维护;二是用户数据要提前设计好状态和归属关系,否则后续加功能时就会四处打补丁。如果你也想做一个类似的个人项目,照着这个方案去落地,绝对能绕开我踩过的那些坑,把精力省下来放在真正有意思的搭配推荐上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询