免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Flask+Vue前后端分离实战:校园二手交易卖家端开发与部署

Flask+Vue前后端分离实战:校园二手交易卖家端开发与部署 1. 为什么这个项目最终用了 FlaskVue而不是 Django 全家桶1.1 技术选型的分歧点需求方想要的“卖家端”到底该怎么做这个项目名字叫python-flask校园二手交易市场卖家_xa1i4接过来的时候我就发现旧代码里既有 Flask 的启动文件又残留了一些 Django 的 app 配置。后来跟需求方对需求才弄清楚他们早期在技术选型上摇摆过一部分人觉得 Django 自带 Admin 后台、ORM 和认证模块开发效率更高另一部分人觉得项目主体只是校园二手交易平台里的卖家端没必要上全套Flask 足够。最终定下来的方案是卖家端的所有 API 用 Flask 写前端用 Vue 做单页应用PyCharm 作为主开发环境Django 的代码只保留在项目里供后续数据统计服务复用。我自己的判断是这个选择对于“卖家端”这个场景是合理的。卖家端本质上是一个管理后台核心功能是商品上架、编辑、下架、订单状态更新、交易数据汇总交互频繁但业务深度不算大Flask 的蓝图和扩展生态完全覆盖得住。1.2 前后端分离与 Django MTV 模式的对比很多刚入门的朋友会纠结Django 的 MTV 模式不是自带模板引擎直接渲染 HTML 不香吗这里关键要看页面交互的复杂度。校园二手交易市场需要买家端、卖家端、管理员端三个入口页面跳转频繁还要支持图片上传、订单状态实时刷新这类功能。如果用 Django 模板渲染前后端代码会耦合在一个工程里后续想给卖家端单独做一套移动端 H5 或者小程序API 部分又要重写。所以我把卖家端做成了前后端分离Vue 负责页面和交互Flask 只返回 JSON。这样做的好处是同一个 Flask 接口可以同时服务买家端和卖家端Vue 打包后的 dist 目录也可以直接让 Flask 托管部署时不用额外开一个 Node 服务。对比下来Django 的 MTV 模式更适合快速出整站Flask Vue 更适合这种以接口为中心、多端共用的场景。1.3 最终技术栈和工程目录这个项目实际使用的核心依赖如下模块选型说明后端框架Flask 2.3提供 REST APIORMFlask-SQLAlchemy 3.x操作 MySQL身份认证Flask-JWT-Extended签发和校验 token跨域Flask-CORS开发环境联调用前端框架Vue 3 Vite构建卖家端 SPAUI 组件Element Plus表格、表单、上传组件状态管理Pinia存储登录态和卖家信息HTTP 客户端Axios调用后端接口后端目录结构我从一开始就按业务模块拆好避免所有路由堆在 app.py 里secondhand/ ├── app.py # 应用入口 ├── config.py # 配置 ├── extensions.py # db, jwt, cors 实例 ├── models/ │ ├── user.py │ ├── product.py │ └── order.py ├── api/ │ ├── __init__.py │ ├── auth.py # 登录/注册 │ ├── product.py # 商品管理 │ ├── order.py # 订单管理 │ └── upload.py # 图片上传 └── frontend/ # Vue 项目目录 └── src/这样的目录结构后面维护起来很舒服新增一个模块只需要加一个蓝图不会动到其他代码。2. 环境准备PyCharm 里最容易翻车的三个细节2.1 Python 虚拟环境与解释器选择项目拿到手第一件事不是在 PyCharm 里直接打开跑而是先创建一个干净的虚拟环境。这个步骤略过的话后面装 Flask、Flask-SQLAlchemy 很容易把系统 Python 环境搞乱尤其在同一台机器上还要跑 Django 旧代码的时候依赖冲突会让你怀疑人生。我习惯用命令创建虚拟环境cd secondhand python -m venv venv然后打开 PyCharm 的 Settings - Project - Python Interpreter选择 venv 里的解释器。这里有个小坑PyCharm 有时候会默认帮你选中全局的 Python 3.x导致终端里明明激活了 venv运行flask run却提示找不到模块。装完依赖后建议先执行pip list确认 flask、flask_sqlalchemy 都出现在当前环境里再往下写代码。2.2 Flask 和 Flask-SQLAlchemy 版本不匹配问题今年新开项目的人大概率会装到 Flask 3.x这时候 Flask-SQLAlchemy 必须用 3.x 版本初始化方式跟旧版不太一样。旧写法是db SQLAlchemy(app)直接把 app 传进去新版推荐用扩展实例加init_app的方式不然在蓝图里导入 db 会出现上下文报错。我的写法是在extensions.py里统一初始化from flask_sqlalchemy import SQLAlchemy db SQLAlchemy()然后在app.py里调用from extensions import db from flask_migrate import Migrate db.init_app(app) migrate Migrate(app, db)这个习惯是从 Django 迁移过来的思考方式Django 的 ORM 也是先把应用配置好再统一调用Flask 扩展的init_app模式本质上是在解决循环引用问题一开始就按这个模式来后面写模型和蓝图都会顺利很多。2.3 Vue 项目初始化与 npm 依赖安装的坑前端部分我用 Vite 而不是 Vue CLI主要是 Vite 创建项目速度快、热更新快。执行npm create vitelatest frontend -- --template vue cd frontend npm install这里有个很常见的坑Node 版本太低会导致 Vite 启动失败。我用的是 Node 18 LTS如果你的机器上还是 Node 14建议先升级或者用 nvm 切换版本否则会报Error: require() of ES Module这类的错误。npm install装到一半失败也是家常便饭多半是网络问题。可以临时切换镜像源npm install --registryhttps://registry.npmmirror.com装完后再装项目需要的组件npm install axios element-plus pinia vue-router2.4 前后端联调的 CORS 配置前后端分离开发时Flask 跑在 5000 端口Vite 默认跑在 5173 端口只要前端页面发请求到后端就会触发跨域。一定要在 Flask 后端配好 CORS否则前端控制台会一直报Access-Control-Allow-Origin错误。最简单的方式是from flask_cors import CORS CORS(app, resources{r/api/*: {origins: [http://localhost:5173]}})生产环境里如果前后端同域名部署这个配置可以去掉靠 Nginx 反代解决。3. 卖家端后端接口设计从商品上架到订单发货3.1 数据模型设计把卖家、商品、订单的关系一次想清楚校园二手交易平台的卖家端最核心的实体是三个卖家账号、商品、订单。设计数据模型的时候我特意参考了 Django ORM 里常用的外键关联方式再迁移到 Flask-SQLAlchemyclass SellerUser(db.Model): __tablename__ seller_user id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(64), uniqueTrue, nullableFalse) password_hash db.Column(db.String(128), nullableFalse) shop_name db.Column(db.String(64)) created_at db.Column(db.DateTime, defaultdatetime.now) def set_password(self, password): self.password_hash generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password)class Product(db.Model): __tablename__ product id db.Column(db.Integer, primary_keyTrue) seller_id db.Column(db.Integer, db.ForeignKey(seller_user.id)) title db.Column(db.String(128), nullableFalse) description db.Column(db.Text) price db.Column(db.Numeric(10, 2), nullableFalse) cover_url db.Column(db.String(256)) status db.Column(db.String(16), defaulton_sale) # on_sale / off_sale / sold created_at db.Column(db.DateTime, defaultdatetime.now)订单模型里我保留了订单编号和买家 ID因为一个卖家可能会收到多个买家订单订单本身还要记录状态流转class Order(db.Model): __tablename__ order id db.Column(db.Integer, primary_keyTrue) order_no db.Column(db.String(64), uniqueTrue, nullableFalse) product_id db.Column(db.Integer, db.ForeignKey(product.id)) buyer_id db.Column(db.Integer) buyer_name db.Column(db.String(64)) status db.Column(db.String(16), defaultpending) created_at db.Column(db.DateTime, defaultdatetime.now)3.2 商品上架接口为什么用 multipart/form-data 而不是纯 JSON商品上架时前端要同时传商品信息和封面图我选择用multipart/form-data的方式提交。接口接收参数后把图片保存到本地再把访问路径写进数据库bp.route(/products, methods[POST]) jwt_required() def create_product(): seller_id get_jwt_identity() title request.form.get(title) price request.form.get(price) description request.form.get(description) cover request.files.get(cover) if not title or not price: return jsonify({message: 标题和价格不能为空}), 400 if cover and allowed_file(cover.filename): ext cover.filename.rsplit(., 1)[1].lower() filename f{uuid4().hex}.{ext} save_path os.path.join(current_app.config[UPLOAD_FOLDER], filename) cover.save(save_path) cover_url f/uploads/{filename} else: cover_url None product Product( seller_idseller_id, titletitle, priceprice, descriptiondescription, cover_urlcover_url ) db.session.add(product) db.session.commit() return jsonify({message: 商品发布成功, product_id: product.id}), 201这里有一个细节图片文件名我选择用 UUID 重命名而不是保留原始文件名。原因是校园用户上传的图片经常叫IMG_9527.jpg如果两个用户传了同名文件后传的会覆盖先传的。用 UUID 基本可以避免这个问题。3.3 订单状态流转逻辑与接口设计卖家端订单管理不能只提供一个“列表查询”接口还需要处理接单、发货、完成、取消这些动作。我设计订单状态时统一用字符串常量方便前后端读代码时直接理解pending买家已下单等待卖家确认accepted卖家已接单shipped卖家已发货completed交易完成cancelled订单取消订单发货接口的典型代码长这样bp.route(/orders/string:order_no/ship, methods[POST]) jwt_required() def ship_order(order_no): seller_id get_jwt_identity() order Order.query.filter_by(order_noorder_no).first() if not order: return jsonify({message: 订单不存在}), 404 product Product.query.get(order.product_id) if product.seller_id ! seller_id: return jsonify({message: 无权操作该订单}), 403 if order.status ! accepted: return jsonify({message: 当前状态不能发货}), 400 order.status shipped db.session.commit() return jsonify({message: 发货成功})写这种接口最需要注意的是状态校验。因为订单状态是一个递进关系如果买家或者恶意用户直接请求发货接口传一个已取消的订单号后端必须拦住不能只依赖前端按钮的禁用状态。3.4 文件下载与 Content-Disposition 的细节卖家端还有订单导出功能把已完成的订单列表导成 CSV。用 Flask 实现这个功能很简单from flask import send_file bp.route(/orders/export, methods[GET]) jwt_required() def export_orders(): seller_id get_jwt_identity() # 查询订单并生成 CSV 文件 csv_buffer generate_orders_csv(seller_id) return send_file( csv_buffer, mimetypetext/csv, download_nameforders_{date.today()}.csv, as_attachmentTrue )如果当初项目留在 Django 那套技术栈里文件下载通常会用StreamingHttpResponse并且要同时设置content_type和content_disposition两个参数。Flask 这里用mimetype和download_name两者思路是一样的本质都是让浏览器知道返回内容类型和下载文件名。我建议做这类功能时统一走“服务端生成文件 浏览器下载”的模式不要在前端用字符串拼 CSV容易把逗号和换行符处理错。4. Vue 卖家工作台实战路由守卫、状态管理和 Axios 封装4.1 登录态与路由守卫的实现前端卖家工作台不是一个单纯展示页面用户必须登录才能看到商品管理和订单管理。我选择了 Vue Router 的全局前置守卫来判断登录状态核心逻辑是如果没有 token就直接跳转到登录页并且把目标路由带过去登录成功后还能回跳。router.beforeEach((to, from, next) { const userStore useUserStore() if (to.meta.requiresAuth !userStore.token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })这里有一个容易忽略的点不要只在localStorage里存 token还需要在 Pinia 里维护一份当前用户信息。刷新页面时 Pinia 里的数据会丢失所以我在 store 初始化时会把localStorage里的 token 同步回来再通过调用/api/auth/me获取用户资料避免路由守卫判断到 token 存在但用户信息为空的情况。4.2 Axios 请求封装与统一错误处理Vue 项目中如果每个页面都直接用axios.get写请求代码会非常散。我把 Axios 封装成了一个统一实例这样拦截器可以统一处理 token 注入和 401 跳转import axios from axios const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { const userStore useUserStore() userStore.logout() router.push(/login) } return Promise.reject(error) } ) export default service这个封装有几个好处后端返回的 JSON 统一被response.data解包页面里拿到的就是业务数据不用每次写两层.data接口过期或未登录时自动清理登录态并回登录页避免用户看到一个半加载的空白页面。4.3 商品管理页列表、分页、编辑、下架卖家工作台的主界面分为三个区域统计卡片、商品表格、操作按钮。商品列表我用 Element Plus 的el-table渲染数据来自/api/products?page1page_size10这个分页接口。表格里“状态”列我用标签展示如果商品已经售出则不允许再编辑。需要注意前端显示的按钮状态永远是不可信的后端接口里也要判断商品状态不然用户手动改请求参数就能把已售出的商品重新上架。商品下架操作是一个典型的“状态翻转”接口前端只需要发起 POST 请求后端负责校验状态并更新const offShelf async (row) { await request.post(/products/${row.id}/off) ElMessage.success(已下架) loadProducts() }4.4 图片上传组件的兼容处理Element Plus 的el-upload组件默认会用 iframe 或 XHR 发送文件我一般设成自定义请求方式方便统一加 token 和错误处理el-upload action/api/upload :headersuploadHeaders :on-successhandleUploadSuccess namefile el-button上传封面图/el-button /el-upload上传成功后后端返回的是图片访问路径前端把路径回填到表单隐藏字段里提交商品信息时一起提交。这样比直接把文件转 base64 塞进表单要高效得多。有一个坑是 Vite 开发环境的代理/uploads路径也要在vite.config.js里代理到 Flask 服务否则上传成功的图片在页面上显示不出来。5. 打包部署与踩坑记录Waitress Nginx 托起 FlaskVue5.1 为什么生产环境不用 Flask 自带的开发服务器Flask 自带的app.run()启动的是 Werkzeug 开发服务器跑起来会显示一句 “WARNING: This is a development server”它不适合直接暴露到公网。原因在于它的并发能力一般而且默认没有做安全加固加上它是单进程模型遇到稍微大一点的并发请求就会卡住。生产环境我用了 Waitress这是一个纯 Python 实现的 WSGI 服务器Windows 和 Linux 都能跑不需要额外装 C 扩展。启动代码非常简单from waitress import serve from app import create_app app create_app() serve(app, host0.0.0.0, port5000, threads8)threads8表示最多同时处理 8 个请求校园场景完全够用。5.2 Vue 打包后 Flask 托管静态文件的正确姿势Vue 项目执行npm run build后会在frontend/dist生成静态文件。为了让 Flask 直接托管这份文件需要把 Flask 的静态目录指向 distfrom flask import send_from_directory DIST_DIR os.path.abspath(os.path.join(os.path.dirname(__file__), ../frontend/dist)) app.route(/, defaults{path: }) app.route(/path:path) def serve_frontend(path): if path and os.path.exists(os.path.join(DIST_DIR, path)): return send_from_directory(DIST_DIR, path) return send_from_directory(DIST_DIR, index.html)这段代码的重点是最后一行Vue Router 使用 history 模式时前端路由/seller/products在后端没有对应文件所以必须统一返回index.html让前端路由接管。如果你漏掉这个 fallback部署后一刷新商品管理页就会 404。5.3 Nginx 反向代理与静态资源缓存虽然 Flask 可以托管静态文件我还是更推荐在前面套一层 Nginx。Nginx 处理静态文件性能更好同时可以把/api/请求反向代理给 Waitress 进程。下面是我这个项目的 Nginx 配置节选server { listen 80; server_name your-domain.com; root /opt/secondhand/frontend/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /uploads/ { alias /opt/secondhand/uploads/; } location / { try_files $uri $uri/ /index.html; } location ~* \.(js|css|png|jpg|svg|woff2)$ { expires 7d; add_header Cache-Control public; } }这里几个细节值得注意/api/转发到http://127.0.0.1:5000时proxy_pass 末尾没有/这样请求路径里的/api前缀会保留后端路由不需要额外调整。/uploads/单独用 alias 指向真实图片目录避免经过 Flask 时还要处理一次静态文件读取。静态资源加 7 天缓存因为打包后的文件名通常带 hash缓存不会影响更新。5.4 部署后常见问题定位接口 404、刷新白屏、图片打不开部署完以后我连续排查过几个问题这里记录一下排查链路免得大家重复踩。第一个现象是页面能打开但登录接口返回 404。我先确认 Flask 进程是否监听在 5000 端口再确认 Nginx 的 location 匹配。执行curl http://127.0.0.1:5000/api/auth/login能通但通过域名访问 404基本就是 Nginx 配置里/api/的 proxy_pass 写错了。最常见的是 proxy_pass 后面多写了一个子路径导致转发后路径被改写。第二个现象是刷新/seller/products页面变白屏。这在前后端分离项目里很典型原因是 Flask 的 catch-all 路由没有配置好或者 Nginx 的 try_files 没有写。只要把静态文件目录配置正确、让不存在的路径回退到 index.html 就能解决。第三个现象是商品图片打不开。多数时候不是代码问题而是图片访问路径没有代理到上传目录。排查顺序是先看数据库里的 cover_url再在浏览器直接访问/uploads/xxx.jpg如果 Nginx 返回 404检查 alias 路径是否写对目录是否有读权限。5.5 安全加固JWT、密码哈希、文件类型校验校园项目看起来内部使用但一旦部署到外网安全问题不能忽视。这个项目里我至少做了三层基础加固。第一层是密码存储。所有用户密码都通过 Werkzeug 的generate_password_hash转成哈希再入库登录时用check_password_hash校验。数据库泄露也不会直接暴露明文密码。第二层是 JWT token。后端用 Flask-JWT-Extended 签发 token有效期我设为 24 小时卖家端每次请求都携带Authorization: Bearer token接口用jwt_required()保护。这里注意不要简单地把 token 放在 URL 参数里会被 Nginx access log 记录存在泄露风险。第三层是文件上传校验。图片保存前要检查扩展名不能只判断文件名的.jpg后缀因为攻击者完全可以伪造。我还会进一步判断文件大小超过 5MB 直接拒绝ALLOWED_EXTENSIONS {jpg, jpeg, png, gif, webp} MAX_CONTENT_LENGTH 5 * 1024 * 1024 if not allowed_file(cover.filename): return jsonify({message: 图片格式不支持}), 400 if not cover.content_type.startswith(image/): return jsonify({message: 文件不是图片}), 400Werkzeug 的secure_filename在保存文件时也可以用一下防止路径穿越。文件名最终用 UUID 生成已经天然规避了这个风险但双重保险总不是坏事。6. 从 Code 到上线我对这个卖家端项目的几点补充体会项目整体跑通以后我又回头复盘了一些细节。比如卖家端统计卡片里的“今日订单数”“在售商品数”这类数据一开始我是直接在列表接口里返回的后来发现前端多个页面都要展示就单独拆了一个/api/dashboard聚合接口用一条 SQL 同时查出多个统计值。这样接口语义更清晰前端也不用重复请求。另外开发过程中我尽量保持了 Flask 和 Django 两套代码的边界。标题里之所以还有 django是因为后期有一个数据统计服务用 Django 写更方便它不直接参与卖家端的商品订单流程。如果你也在做类似的项目我的建议是不要在一个进程里同时跑 Flask 和 Django 两个应用哪怕它们用的是同一个数据库也尽量把任务拆开分开部署。不然两个框架的上下文和依赖很容易互相干扰部署时也会变成一场灾难。图片上传和生产部署两个环节是我这次踩坑最多的地方。最后一次在服务器上跑起来的时候我用curl测了登录、商品列表、下单、发货、导出订单这条完整链路又让一个同学在手机浏览器上操作了一遍卖家端确认没有白屏和接口报错才收工。如果让我给后来者一句总结我会说做这类校园二手交易系统的卖家端技术栈本身不是瓶颈真正花时间的是接口边界划分、状态流转校验和部署细节。把这些环节想清楚Flask Vue 完全能支撑一个小型交易平台的稳定运行。
返回列表