资讯动态

Flask+Vue前后端分离:网上书店商城系统完整开发实战

发布时间:2026/10/20 9:37:17 来源:尧图企业网站定制
网上书店的图书销售商城几乎是每个学Python Web开发的人都会遇到的一个练手项目。前后端分离的组合很常见Vue负责页面交互Flask提供接口PyCharm作为日常开发环境数据库这边按项目规模在SQLite和MySQL之间选。它覆盖面广但又不是那种“企业级重型架构”正好能把用户注册、图书检索、分类导航、购物车、下单支付流程、后台图书管理和订单管理这一条电商核心链路完整走一遍。本文按我实际做这个项目的顺序把数据库设计、API开发、Vue页面联调、常见坑位都整理出来给准备动手的读者一份可复现的路线图。1. 项目整体拆解书店商城到底要做什么做项目之前先别急着写代码把“要做什么”一条条列清楚后面所有表结构和接口都从这里长出来。这个图书销售商城从角色上天然分成两边普通用户看到的商城前台管理员操作的后台管理。1.1 核心功能需求前台的逛购链路与后台的管理闭环前台是给买书人用的完整链路包括注册登录、浏览首页、按分类找书、搜索书名或作者、进入详情页看库存和简介、加入购物车、提交订单、模拟支付、查看我的订单。后台是给运营人员用的核心是图书维护、库存调整、上下架、订单状态流转、用户列表。两边功能合起来才构成一个能自圆其说的商城系统。第一次做这类项目我建议把功能收敛一点别一上来就加优惠券、秒杀、评论、积分。先把“商品—购物车—订单—支付状态—后台发货”这条主链路打通就是一个完整的销售闭环。后面想加分项再往上面叠加。前台后台的边界要提前划分清楚。前台页面是首页、图书列表、详情、购物车、结算、订单列表后台页面是图书管理、分类管理、订单管理、用户管理、数据概览。前端路由和菜单跟着这个划分走后端接口也按/api/user/...、/api/admin/...分开权限控制才有落脚点。1.2 技术选型的现实考量Flask和Django怎么选很多同学纠结标题里同时出现Flask和Django的问题。我的看法是两个都能完成这个项目但选择逻辑完全不同。Flask是一个轻量级框架什么东西都要自己组装路由用装饰器蓝图Blueprints做模块化ORM用独立的Flask-SQLAlchemy鉴权接第三方的Flask-JWT-Extended。好处是你对每一层都有掌控感坏处是前期配置多。Django的哲学相反自带Admin后台、自带ORM、自带认证体系开发速度确实快尤其是后台管理可以直接用Django Admin快速生成一个能增删改查的界面。但Django的“全家桶”风格让很多初学者容易照着文档敲完却不理解内部机制。我最终选择Flask是因为这个项目的核心目标就是亲手把电商业务链路走一遍框架越简单底层逻辑暴露得越充分。如果你希望一周内出成品给导师看效果Django是好选择如果你想扎扎实实弄清楚接口、鉴权、事务、联调这些细节Flask更合适。技术栈的完整选型可以用一张表说清楚层次选型说明开发工具PyCharm Professional调试、数据库面板、前端文件编辑一站搞定后端框架Python 3.8 Flask 2.x轻量适合教学和中小型项目数据库MySQL 8.0生产常用前期也可以用SQLite快速起步ORMFlask-SQLAlchemy Flask-Migrate模型映射与表结构迁移认证方式Flask-JWT-ExtendedToken鉴权前后端分离标准做法前端框架Vue 2 Element UI教程多、稳定Vue 3 Element Plus也没问题状态管理Vuex购物车和登录态全局共享HTTP客户端Axios统一处理请求和响应拦截开发环境还有个细节PyCharm专业版能直接连MySQL数据库查看表结构、执行SQL还能一键配置Flask运行环境社区版虽然也能写代码但少了数据库面板和一键启动。没有专业版授权也不用担心手动创建虚拟环境、命令行启动Flask、用DataGrip或Navicat看库效果一样只是步骤多一点。1.3 目录结构规划前后端分仓库互不干扰前后端分离的第一个好处就是目录干净。我习惯把后端和前端分成两个独立目录后端只关注API和数据库前端只关注页面和状态。后端结构长这样bookstore-backend/ ├── app.py # 应用工厂与蓝本注册 ├── config.py # 数据库连接、密钥等配置 ├── requirements.txt # 依赖清单 ├── models/ │ ├── __init__.py # 初始化SQLAlchemy实例 │ ├── user.py # 用户模型 │ ├── book.py # 图书与分类模型 │ ├── cart.py # 购物车模型 │ └── order.py # 订单与订单明细模型 ├── api/ │ ├── __init__.py │ ├── auth.py # 注册登录接口 │ ├── books.py # 图书列表、详情、分类 │ ├── cart.py # 购物车接口 │ ├── order.py # 订单创建、支付、列表 │ └── admin.py # 后台管理接口 └── utils/ ├── response.py # 统一响应格式 └── decorators.py # 管理员鉴权装饰器前端目录按Vue官方习惯组织bookstore-frontend/ ├── public/ ├── src/ │ ├── main.js # 入口文件 │ ├── App.vue # 根组件 │ ├── router/index.js # 路由配置 │ ├── store/index.js # Vuex状态 │ ├── api/ # axios接口封装 │ │ ├── request.js │ │ ├── auth.js │ │ ├── book.js │ │ └── order.js │ ├── views/ │ │ ├── Home.vue # 首页 │ │ ├── BookList.vue # 图书列表 │ │ ├── BookDetail.vue # 图书详情 │ │ ├── Cart.vue # 购物车 │ │ ├── Checkout.vue # 结算页 │ │ ├── Login.vue │ │ ├── Register.vue │ │ ├── OrderList.vue │ │ └── admin/ # 后台页面 │ └── components/ ├── vue.config.js # 开发代理配置 └── package.json实际开发顺序我建议先建数据库表 → 写后端接口并全部用Postman验证 → 再开发前端页面。不要前后端同时开工否则两边出问题你分不清是接口的锅还是页面的锅。2. 数据库设计从业务到表的映射数据库是电商系统的地基。表结构设计不合理后面写接口会处处别扭。这个项目共六张核心表用户表、分类表、图书表、购物车表、订单表、订单明细表。2.1 实体关系谁和谁是一对多先把关系理清楚一个用户能下多个订单一个订单包含多个订单明细一个分类下有多个图书一个用户购物车里有多个购物车记录每个购物车记录对应一本图书。用文字描述就是一对多关系的组合。用户表存账号密码和角色标识图书表存书籍的标题、作者、ISBN、价格、库存、销量、封面、简介分类表存图书类别购物车表存用户和图书的关联以及数量订单表存一次购买的汇总信息订单明细表存订单里每一本书的快照数据。为什么订单明细要单独一张表而不是在订单表里存一堆书因为一个订单可能有多种书每种书的数量、价格都不一样如果不拆明细表数据直接没法存。订单明细表里的book_title和price是快照字段这个设计很关键后面细说。2.2 核心模型代码SQLAlchemy建模用户模型from datetime import datetime from models import db class User(db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) username db.Column(db.String(64), uniqueTrue, nullableFalse, indexTrue) password_hash db.Column(db.String(128), nullableFalse) email db.Column(db.String(120), uniqueTrue) phone db.Column(db.String(20)) role db.Column(db.Integer, default0) # 0普通用户 1管理员 status db.Column(db.Boolean, defaultTrue) # 是否可用 created_at db.Column(db.DateTime, defaultdatetime.now)图书和分类模型from models import db class Category(db.Model): __tablename__ categories id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(50), uniqueTrue, nullableFalse) class Book(db.Model): __tablename__ books id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(200), nullableFalse, indexTrue) author db.Column(db.String(100)) isbn db.Column(db.String(20), uniqueTrue, indexTrue) price db.Column(db.Numeric(10, 2), nullableFalse) # 用Decimal存金额 original_price db.Column(db.Numeric(10, 2)) category_id db.Column(db.Integer, db.ForeignKey(categories.id)) stock db.Column(db.Integer, default0) sales db.Column(db.Integer, default0) cover_url db.Column(db.String(255)) description db.Column(db.Text) status db.Column(db.Boolean, defaultTrue) # True上架 False下架 created_at db.Column(db.DateTime, defaultdatetime.now)购物车与订单模型class Cart(db.Model): __tablename__ carts id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(users.id), nullableFalse) book_id db.Column(db.Integer, db.ForeignKey(books.id), nullableFalse) quantity db.Column(db.Integer, default1) created_at db.Column(db.DateTime, defaultdatetime.now) # 同一个用户同一本书只保留一条购物车记录 __table_args__ (db.UniqueConstraint(user_id, book_id),) class Order(db.Model): __tablename__ orders id db.Column(db.Integer, primary_keyTrue) order_no db.Column(db.String(32), uniqueTrue, nullableFalse, indexTrue) user_id db.Column(db.Integer, db.ForeignKey(users.id), nullableFalse) total_amount db.Column(db.Numeric(10, 2), nullableFalse) status db.Column(db.Integer, default0) # 0待支付 1已支付 2已发货 3已完成 4已取消 receiver_name db.Column(db.String(50)) receiver_phone db.Column(db.String(20)) receiver_address db.Column(db.String(255)) created_at db.Column(db.DateTime, defaultdatetime.now) paid_at db.Column(db.DateTime) class OrderItem(db.Model): __tablename__ order_items id db.Column(db.Integer, primary_keyTrue) order_id db.Column(db.Integer, db.ForeignKey(orders.id), nullableFalse) book_id db.Column(db.Integer, nullableFalse) book_title db.Column(db.String(200)) # 快照下单时的书名 cover_url db.Column(db.String(255)) # 快照封面 price db.Column(db.Numeric(10, 2)) # 快照下单时的单价 quantity db.Column(db.Integer, default1)2.3 字段设计里的几个坑金额字段必须用Numeric(10, 2)而不是Float。浮点数存价格会出现精度问题比如 19.9 实际存成 19.899999...累计到订单总价时误差会被放大。数据库层面设计对了后面接口计算价格才不会埋雷。订单明细里的书名、封面、单价用快照字段这是电商系统里的标准做法。用户下单之后管理员完全可能改书名、调价格历史订单必须展示下单那一刻的信息。如果订单明细直接关联books表书籍一改价历史订单总对不上账。删除策略要设计成软删除而不是物理删除。图书“下架”就是把status置为 False而不是直接DELETE。用户购物车里可能还有下架的书历史订单也要关联快照数据物理删除会造成外键约束报错或者查询时到处判空。这个坑我见过太多次了。用户表的role字段决定了怎么区分普通用户和管理员后台管理的接口在前端路由守卫和后端装饰器两层都要做校验不能只在前端藏个按钮就算权限控制。后面接口部分会单独讲。3. 后端APIFlask怎么支撑一个商城后端接口是整个系统的发动机。这一部分把Flask的初始化、蓝图拆分、认证鉴权、图书查询、购物车与订单实现细节全部过一遍。3.1 应用工厂、蓝图与统一响应Flask的蓝图机制很适合这种多模块项目。在app.py中创建应用实例并注册各功能蓝本from flask import Flask from flask_cors import CORS from flask_jwt_extended import JWTManager from models import db from api.auth import auth_bp from api.books import books_bp from api.cart import cart_bp from api.order import order_bp from api.admin import admin_bp def create_app(): app Flask(__name__) app.config.from_object(config.Config) db.init_app(app) CORS(app, resources{r/api/*: {origins: http://localhost:8080}}) JWTManager(app) app.register_blueprint(auth_bp, url_prefix/api/auth) app.register_blueprint(books_bp, url_prefix/api/books) app.register_blueprint(cart_bp, url_prefix/api/cart) app.register_blueprint(order_bp, url_prefix/api/orders) app.register_blueprint(admin_bp, url_prefix/api/admin) return app app create_app() if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)统一响应格式这一步一定要做。我见过不少接口一会儿返回列表直接当作JSON一会儿返回{code: 1}前端被迫到处写分支判断。所有接口统一返回{code, msg, data}前端处理逻辑会简单很多# utils/response.py from flask import jsonify def ok(dataNone, msgsuccess): return jsonify(code200, msgmsg, datadata) def fail(msgerror, code400): return jsonify(codecode, msgmsg, dataNone)依赖版本方面建议先锁定一套能直接跑通的组合。我用的版本是 Flask 2.2.5、Flask-SQLAlchemy 2.5.1、Flask-JWT-Extended 4.5.2、Flask-Cors 4.0.0、PyMySQL 1.0.2。如果用最新版Flask 3.x部分扩展API有变动出问题先检查版本兼容性。3.2 注册登录与JWT鉴权注册接口的核心是密码不能明文入库。用werkzeug自带的generate_password_hash对密码加盐哈希校验时用check_password_hash。登录成功后签发JWT Token前端把Token存到localStorage每次请求在请求头带Authorization: Bearer token。登录接口的大致逻辑from werkzeug.security import check_password_hash from flask_jwt_extended import create_access_token from models import db from models.user import User from utils.response import ok, fail auth_bp.route(/login, methods[POST]) def login(): data request.get_json() username data.get(username, ).strip() password data.get(password, ) user User.query.filter_by(usernameusername).first() if not user or not check_password_hash(user.password_hash, password): return fail(用户名或密码错误) if not user.status: return fail(账号已被禁用) token create_access_token(identityuser.id) return ok(data{ token: token, user: {id: user.id, username: user.username, role: user.role} })Token里只放用户ID不把整个用户对象塞进去。每次请求需要通过Token拿用户信息时再查一次数据库。这样做的好处是用户被封禁后 Token 立即失效。后台管理接口需要管理员权限写一个装饰器统一校验from functools import wraps from flask_jwt_extended import verify_jwt_in_request, get_jwt_identity from models.user import User from utils.response import fail def admin_required(fn): wraps(fn) def wrapper(*args, **kwargs): verify_jwt_in_request() user_id get_jwt_identity() user User.query.get(user_id) if not user or user.role ! 1: return fail(无权限访问, code403) return fn(*args, **kwargs) return wrapperPyCharm里配Flask启动也很省事专业版新建Flask项目模板Community版就在终端python app.py。我习惯在PyCharm的Run Configuration里设置环境变量FLASK_ENVdevelopment这样改代码保存后自动重启调试效率明显高。3.3 图书列表多条件查询与分页图书列表接口是前台入口的重头。它要支持按分类筛选、按关键词搜索、按价格排序、分页。核心代码如下books_bp.route(, methods[GET]) def get_books(): page int(request.args.get(page, 1)) page_size int(request.args.get(page_size, 12)) keyword request.args.get(keyword, ).strip() category_id request.args.get(category_id, typeint) sort request.args.get(sort, default) # price_asc / price_desc / sales query Book.query.filter(Book.status True) if keyword: # 书名、作者、ISBN都参与搜索 query query.filter(db.or_( Book.title.like(f%{keyword}%), Book.author.like(f%{keyword}%), Book.isbn.like(f%{keyword}%) )) if category_id: query query.filter(Book.category_id category_id) if sort price_asc: query query.order_by(Book.price.asc()) elif sort price_desc: query query.order_by(Book.price.desc()) elif sort sales: query query.order_by(Book.sales.desc()) else: query query.order_by(Book.created_at.desc()) # 分页 pagination query.paginate(pagepage, per_pagepage_size, error_outFalse) items [{ id: b.id, title: b.title, author: b.author, price: float(b.price), cover_url: b.cover_url, sales: b.sales, stock: b.stock, category_name: b.category.name if b.category else } for b in pagination.items] return ok(data{ list: items, total: pagination.total, page: page, page_size: page_size })分页返回total字段是给前端分页组件算总页数用的。price从Decimal转成float再返回因为JSON原生不支持Decimal。转换格式的细节后面第五部分还会专门讲序列化问题。用like做模糊搜索数据量小没问题。如果以后图书量到了几十万本搜索引擎就得上ES或者MySQL全文索引但当前阶段完全够用别过度设计。3.4 购物车与订单事务、库存扣减和快照购物车接口的逻辑要点是同书合并。用户反复点击“加入购物车”时如果购物车已有这本书应该把数量加一而不是插两条记录。我用唯一约束(user_id, book_id)保证业务层并发也不会产生重复数据然后在接口里先查再更新cart_bp.route(, methods[POST]) jwt_required() def add_cart(): user_id get_jwt_identity() data request.get_json() book_id data.get(book_id) book Book.query.get(book_id) if not book or not book.status: return fail(图书不存在或已下架) cart_item Cart.query.filter_by(user_iduser_id, book_idbook_id).first() if cart_item: cart_item.quantity 1 else: cart_item Cart(user_iduser_id, book_idbook_id, quantity1) db.session.add(cart_item) db.session.commit() return ok()订单创建是整个系统最需要严谨的地方涉及多个表的状态变更必须放在一个数据库事务里。一个订单的创建要完成五件事校验库存、计算总价、生成订单主表、生成订单明细快照、扣减库存并清空购物车。任何一步失败所有操作都要一起回滚。from decimal import Decimal from datetime import datetime import random def generate_order_no(): now datetime.now() return f{now.strftime(%Y%m%d%H%M%S)}{random.randint(1000, 9999)} order_bp.route(, methods[POST]) jwt_required() def create_order(): user_id get_jwt_identity() data request.get_json() cart_items Cart.query.filter_by(user_iduser_id).all() if not cart_items: return fail(购物车为空) total Decimal(0) for item in cart_items: book Book.query.get(item.book_id) if not book or not book.status: return fail(f商品 {item.book_title} 已下架) if book.stock item.quantity: return fail(f《{book.title}》库存不足) order Order( order_nogenerate_order_no(), user_iduser_id, total_amounttotal, status0, receiver_namedata.get(receiver_name), receiver_phonedata.get(receiver_phone), receiver_addressdata.get(receiver_address) ) db.session.add(order) db.session.flush() # 先拿到order.id for item in cart_items: book Book.query.get(item.book_id) total book.price * item.quantity db.session.add(OrderItem( order_idorder.id, book_idbook.id, book_titlebook.title, cover_urlbook.cover_url, pricebook.price, quantityitem.quantity )) # 扣库存、加销量 book.stock - item.quantity book.sales item.quantity order.total_amount total # 清空购物车 for item in cart_items: db.session.delete(item) db.session.commit() return ok(data{order_id: order.id, order_no: order.order_no})注意这个写法在单进程开发环境下完全没问题但并发下存在超卖风险。两个用户同时买同一本书都先读到 stock1然后都执行减一最后库存变成 -1。解决思路有两个一是用条件更新代替“读后再写”二是用SELECT ... FOR UPDATE加行锁。建议先跑通基础版本再把库存扣减改成条件更新我会在第五部分给出具体写法。模拟支付接口更简单把订单状态从0改成1记录支付时间order_bp.route(/int:order_id/pay, methods[POST]) jwt_required() def pay_order(order_id): user_id get_jwt_identity() order Order.query.filter_by(idorder_id, user_iduser_id).first() if not order: return fail(订单不存在) if order.status ! 0: return fail(订单状态不支持支付) order.status 1 order.paid_at datetime.now() db.session.commit() return ok()订单编号用时间戳加随机数够用真实电商系统还会加店铺标识和用户ID片段但核心思想就是保持唯一且不易猜测。4. 前端Vue页面开发与联调前端部分的工作量其实比后端大。页面多、交互多、状态管理复杂尤其是购物车和订单流程非常考验Vuex和路由设计的功底。4.1 工程搭建与开发代理用Vue CLI创建项目vue create bookstore-frontend npm install element-ui vue-router vuex axios在main.js里挂载Element UI和路由、状态管理。开发阶段最舒服的配置是vue.config.js里的代理把前端8080端口的/api请求转发到后端的5000端口这样前端写请求就直接写/api/...不用操心跨域问题module.exports { devServer: { proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } }配置了代理之后即使后端还有CORS配置两者并存也不会冲突。如果哪天前端直接访问后端地址CORS会生效平时走代理请求头里看到的还是同源完全没有跨域提示。4.2 axios封装与登录态处理Axios一定要封装。我见过所有接口调用处直接写axios.get的代码改一个baseURL或者加一个Token拦截器能改到崩溃。统一封装后页面组件里只关心业务数据// api/request.js import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) Message.error(登录已过期请重新登录) router.push(/login) } return Promise.reject(error) } ) export default service请求拦截器统一加Token响应拦截器统一判断错误码和401跳转。前端页面所有接口调用都走这个封装逻辑会非常干净。路由的前置守卫同样处理登录和权限判断。后台管理页要求用户必须管理员身份否则直接跳回首页。判断角色最简单的方法是在登录时把用户信息存到localStorage或者Vuex里路由守卫里读出来比较。4.3 首页、图书列表与详情页首页通常是轮播图、分类导航、热销图书列表。图书列表页的筛选交互要跟后端接口的query参数一一对应前端把keyword、category_id、sort、page这些参数通过axios传给后端后端返回total后前端El-pagination组件算总页数。图书详情页的关键是这本书的库存展示。库存为零或已下架时加入购物车按钮要禁用并提示“暂时缺货”。前端做一层拦截后端接口再拦截一次两边都判断才算安全。我习惯在api/book.js里把接口按资源组织// api/book.js import request from ./request export function getBooks(params) { return request.get(/books, { params }) } export function getBookDetail(id) { return request.get(/books/${id}) } export function addToCart(bookId, quantity) { return request.post(/cart, { book_id: bookId, quantity }) }4.4 购物车状态管理与下单结算购物车用Vuex做全局状态管理因为同一本书可能在不同页面触发加购操作而且购物车角标要跟着变化。Vuex的action里调用后端接口成功后再commit mutation更新本地state。购物车页面的计算属性是整个交互的核心选中、全选、合计价格都基于这个数据computed: { selectedItems() { return this.cartList.filter(item item.selected) }, totalPrice() { return this.selectedItems.reduce((sum, item) { return sum item.price * item.quantity }, 0).toFixed(2) } }下单结算页要填写收货人、电话、地址点击提交调用创建订单接口。订单创建成功之后前端要清空购物车本地状态并且跳转到订单列表页。之前踩过一个坑订单创建成功但购物车没清掉用户回到购物车页发现刚才买的书还在体验很糟。订单列表页根据状态展示标签待支付订单要提供“去支付”和“取消订单”两个操作按钮已支付订单显示“等待发货”。状态字段和后端数字一一对应建议前端定义一个订单状态映射常量不要遍地写魔法数字。4.5 后台管理页面后台页面用Element UI的el-table配合el-dialog做图书增删改查图片封面上传可以用两种方式一种是本地文件上传后存到静态目录另一种是直接填图片URL。国内图书封面图往往可以从公开图片接口拿但对练手项目来说我更建议实现本地文件上传因为这是常规开发必会的技能点。图书表单的校验要做扎实价格必须是大于0的数字库存必须是整数ISBN唯一性由后端数据库约束保证前端表单校验提前拦截不合法数据减少后端压力。订单管理页面本质上就是把订单状态从“待发货”改成“已发货”再改成“已完成”每个状态变更对应一个后端接口。5. 联调中的常见问题与排查实录前后端联调阶段才是真正涨经验的时候。我把实际开发中出现频率最高的问题整理成一个速查表每一个都是踩过坑之后总结出来的。问题现象根因解决思路前端请求报CORS跨域后端未配置跨域或前端口径不一致开发用代理生产用Nginx反向代理接口返回的中文变成\uXXXXFlask响应JSON时默认ASCII转义设置JSON_AS_ASCII False订单总价精度不对价格用Float存储数据库用Numeric后端用Decimaldatetime不能序列化默认JSONEncoder不认识datetime自定义Encoder统一格式化后台修改图书后前端不生效没有清理缓存或在浏览器强缓存接口加版本参数或配置no-cache路由直接访问刷新404history模式没有服务端fallbackNginx配置try_files库存偶尔变负数读到旧库存后并发扣减条件更新或行锁5.1 中文乱码与时间序列化Flask在旧版本里返回JSON时会把中文转成Unicode编码看起来像\u56fe\u4e66。解决方式是在配置里关闭ASCII转义class Config: JSON_AS_ASCII False如果项目用了Flask 2.3以上这个配置的写法有变化需要改用app.json.ensure_ascii False这也是升级框架时最容易踩的兼容性坑。时间序列化问题也需要统一处理。默认情况下datetime对象不能直接JSON序列化自定义一个Encoder最省事from flask.json import JSONEncoder from datetime import datetime from decimal import Decimal class CustomJSONEncoder(JSONEncoder): def default(self, obj): if isinstance(obj, datetime): return obj.strftime(%Y-%m-%d %H:%M:%S) if isinstance(obj, Decimal): return float(obj) return super().default(obj) app.json_encoder CustomJSONEncoder这样后端所有接口返回的时间、金额都能直接序列化不需要在每个接口里手动转一遍。5.2 图片封面上传的路径问题图书封面上传后存在后端static/uploads目录返回给前端的URL怎么写是个容易糊涂的地方。前端开发服务器的端口是8080后端的静态文件端口是5000如果前端直接拼/static/uploads/xxx.jpg浏览器会在8080端口找文件自然404。我惯用的做法是在后端返回完整URL。上传接口保存文件后拼一个由配置项控制的完整地址from flask import current_app cover_url f{current_app.config[SERVER_HOST]}/static/uploads/{filename}配置里SERVER_HOST开发时是http://localhost:5000上线部署时改成正式域名。这样一个变量控制所有静态资源的访问地址比前端到处拼URL要稳定得多。5.3 库存超卖的并发方案前面订单创建接口用的是“先查库存再扣减”单机教学环境够用但聊到并发和实际部署就必须要升级。改成条件更新是成本最低的解法result Book.query.filter( Book.id book_id, Book.stock quantity ).update({ stock: Book.stock - quantity, sales: Book.sales quantity }) # result为0说明库存不足或图书已下架 if result 0: db.session.rollback() return fail(库存不足)这段代码把“查库存”和“扣库存”合并成一条带条件的UPDATE数据库层面保证只有库存充足时才扣减并发场景下不会出现负库存。注意这里要在事务里执行扣减失败时整个订单都要回滚。5.4 前端history路由刷新404前端开发时一切正常一旦发布到服务器的静态目录用户站在/books/123这个页面按F5刷新Nginx会去磁盘找books/123这个文件找不到就报404。Vue Router的history模式本质上是纯前端路由服务端需要把未知路径都回退到入口HTMLlocation / { try_files $uri $uri/ /index.html; }用了这段配置后任何路径刷新都先尝试找真实文件找不到就加载index.html再由前端路由接管。这是前后端分离项目上线必须配置的一步。5.5 JWT过期与401跳转Token过期是联调时最隐蔽的问题。接口返回401前端如果只弹一个“网络错误”用户完全不知道发生了什么。正确做法是把401统一识别为登录失效清掉本地Token跳转到登录页同时给出明确提示。我在axios响应拦截器里已经写了这段逻辑。还有一个小细节并发请求时多个接口同时401会触发多次跳转和多余提示。可以加一个标志位只对第一个401做跳转处理后续401直接忽略避免页面反复弹窗。6. 从零上手的开发顺序与实操心得如果你打算照着这篇文章做一遍我给你一条经过验证的实施路线先花半天把后端工程搭好、建好表、写好图书列表接口用Postman验证通了再动前端。前后端同时开工最容易出现的问题是前端等接口后端等前端测互相堵着。一个接口跑通再做下一个。我的具体执行顺序是这样的。第一周做数据库设计和图书模块包括分类、图书列表、详情、搜索、分页。第二周做认证与购物车用户注册登录、JWT鉴权、购物车增删改。第三周做订单流程下单支付、订单列表、状态流转。第四周做后台管理图书增删改查、分类管理、订单管理。最后两天完善部署和文档。上线部署时选最简单的组合后端用Gunicorn跑Flask前端构建产物用Nginx托管MySQL单独一台服务。Gunicorn适合这个量级的并发Nginx负责静态文件和反向代理配置也不复杂。最后分享几个实际的体会。第一统一响应格式和统一的序列化处理一定要提前做这是我这个项目里收益最大的决定。第二订单相关代码里事务边界一定要画清楚db.session.flush()和db.session.commit()的区别要弄清楚否则会出现拿不到自增ID或者数据写到一半报错的问题。第三不要迷信代码量这个系统两千行后端、三千行前端已经很完整了真正的价值在于你亲手把一条电商业务链路从数据表到接口再到页面完整串起来。真遇到卡住的时候最有效的排查方式不是盯着代码看而是打开PyCharm的调试器在接口入口处打断点跟着请求走一遍。数据流在哪个环节断的、哪个字段是None、哪个返回值不对一目了然。这套排查习惯比任何快捷键和插件都管用。

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

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

免费获取报价 →
↑