Flask酒类购物系统毕业设计:从数据库设计到订单库存核心实现
每年毕业设计季各种“XX购物系统”都是Web方向出现频率最高的题目。而在这些大同小异的商城系统里酒类购物系统是比较特别的一种它本质上是一条完整的电商交易链路但因为商品是酒所以在商品属性、库存管理、下单流程上比普通的“图书商城”“服装商城”多了一层业务细节。本文就拿这套基于Flask实现、可获取完整源码的酒类购物系统作为实例从技术选型、数据库设计、核心功能落地到答辩前最容易翻车的细节完整拆开讲一遍。很多同学拿到题目后的第一反应是去网上找一套现成源码能跑起来就开始写文档。但真正到了开题和答辩阶段老师追问的往往是“购物车数据存在哪里订单状态怎么流转库存扣减遇到并发怎么处理”如果只把源码跑通这些问题完全答不上来分数会非常吃亏。这篇文章的目的就是帮你把每一个可能被追问的点都摊开做成一份能经得起提问的毕设项目。适合有一定Python基础、想快速理解Web全栈项目、并希望在其中加入自己改动的同学。1. 为什么酒类购物系统适合用Flask来做毕设先给结论如果选题允许自由选择技术栈Flask在这个题目上是一个性价比很高的选择。不是说Django或者SpringBoot不好而是从毕业设计的评分逻辑来看Flask更有利于你展示“我真正理解了每个环节”。1.1 毕设场景下轻量灵活比全家桶更重要Flask核心只包含路由、模板、请求和响应数据库ORM、登录认证、表单验证这些能力全靠扩展一点一点组装起来。这样做的好处是你在写系统时可以逐个引入Flask-SQLAlchemy、Flask-Login、Flask-WTF每一个都是在做主动选型而不是框架自动帮你包办。酒类购物系统的功能规模大概就是几种角色、几十张页面、几个核心流程完全在Flask的甜区范围内。用蓝图拆分模块之后代码量不大但层次清楚。万一后面想扩展“猜你喜欢”“销售统计图表”Flask周边生态也能接得上不会把自己卡死。1.2 Flask、Django、SpringBoot三选一怎么向导师解释对比项FlaskDjangoSpringBoot学习曲线低中较高内置功能少自由组合多全家桶多企业级项目体积轻较重较重答辩风险每个环节需要自己解释容易变成“框架在帮我做”适合Java方向的学生适合场景中小型Web、毕设、快速原型CMS、内容社区企业级服务、微服务如果课程体系以Python为主想用最短时间做一个逻辑完整、能讲清楚原理的系统Flask是优选。当然如果你是Java方向的学生那直接用SpringBoot更合适没必要为了本文去切换技术栈。技术选型本身没有绝对的对错能向导师讲清楚“为什么选它”就已经赢了一半。1.3 项目结构用工厂模式和蓝图把代码拆开我看到不少同学把几百行路由塞进一个app.py里最后文件两三千行改一个功能要滚动半天答辩也没法讲。这套系统采用的是应用工厂 蓝图结构示例目录如下flask_wine_shop/ ├── app/ │ ├── __init__.py # create_app 应用工厂 │ ├── models.py # 统一的数据模型入口 │ ├── extensions.py # db、login_manager 等扩展实例 │ ├── main/ # 首页与商品展示蓝图 │ │ ├── __init__.py │ │ └── views.py │ ├── user/ # 用户、购物车蓝图 │ ├── order/ # 订单蓝图 │ └── admin/ # 后台管理蓝图 ├── static/ │ ├── css/ │ ├── js/ │ └── uploads/ # 商品图片上传目录 ├── templates/ │ ├── main/ │ ├── user/ │ ├── order/ │ └── admin/ ├── config.py └── run.py使用create_app工厂函数来注册扩展和蓝图好处很直接项目可以方便地创建多个实例便于测试数据库迁移、单元测试、后续接入第三方接口也都顺畅。对答辩来说你还能顺势讲一句“我用了Flask的工厂模式”这就是一个真实的加分点。2. 业务功能拆解从用户注册到后台发货的完整链路这部分先不写代码先把系统的业务边界画清楚。明确一件事毕设项目不是功能越多越好而是核心链路越完整越好。酒类购物系统的核心链路可以概括为浏览商品 → 加入购物车 → 结算下单 → 支付 → 后台发货 → 确认收货。2.1 前台用户能做什么注册、登录、退出登录密码使用哈希存储商品分类浏览、关键词搜索、按价格/销量/新品排序商品详情页展示酒类特有属性酒精度、容量、产地、年份、香型等购物车管理加入、修改数量、删除、勾选结算下单选择收货地址、生成订单、模拟支付订单管理待支付、待发货、待收货、已完成、已取消个人中心资料修改、收货地址管理这里有一个很关键的差异点酒类商品的详情属性比普通商品复杂得多。白酒要看度数、香型葡萄酒要看产区、年份、葡萄品种啤酒要看原麦汁浓度。如果商品表里只有一个“description”字段页面就会非常单薄。设计商品表时预留扩展属性是这个题目的特色所在。2.2 后台管理员能做什么商品分类管理新增分类、修改排序、删除商品管理上架、下架、编辑库存、设置促销价订单管理查看订单明细、发货、处理退款和取消用户管理查看用户列表、禁用异常账号数据概览商品销量排行、订单金额汇总后台路由必须和前台隔离。所有以/admin/开头的视图都要做管理员权限校验模板也要单独使用一套后台布局不要和用户端混在一起。这个细节老师一般会注意到。2.3 核心业务状态流转订单状态不要用字符串满天飞用整数常量枚举更规范。我建议这样设计0待支付1待发货2待收货3已完成4已取消用户提交订单后是待支付模拟支付成功后变成待发货管理员发货后变成待收货用户确认收货后变成已完成。待支付状态的订单允许用户主动取消其它状态不能随便跳转。每个状态变更要记录更新时间比如paid_at、shipped_at、finished_at这样后面追溯谁在什么时间做了什么操作时一目了然。3. 数据库设计库存与订单的一致性是整个系统的心脏购物系统看似简单但最容易翻车的全在数据一致性上。我把核心表结构和几个关键设计思路拆开讲这些内容也是答辩时的高频问题。3.1 核心表结构先看用户表和商品表CREATE TABLE user ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password_hash varchar(255) NOT NULL, role tinyint NOT NULL DEFAULT 0, created_at datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ); CREATE TABLE product ( id int NOT NULL AUTO_INCREMENT, category_id int NOT NULL, name varchar(100) NOT NULL, subtitle varchar(255) DEFAULT NULL, price decimal(10,2) NOT NULL, stock int NOT NULL DEFAULT 0, sales int NOT NULL DEFAULT 0, image varchar(255) DEFAULT NULL, alcohol varchar(20) DEFAULT NULL, volume varchar(20) DEFAULT NULL, origin varchar(100) DEFAULT NULL, status tinyint NOT NULL DEFAULT 1, PRIMARY KEY (id) );不要小看这个表结构它已经在为业务做妥协了。酒精度、容量、产地都是商品维度的高频筛选条件直接做成独立字段比塞进一个JSON里方便查询得多。价格用decimal而不是float就是为了避免电商金额计算时出现浮点误差这个点也可以写进项目说明。3.2 为什么订单里要存一份商品快照很多初学者设计订单明细时只存一个product_id外键需要展示历史订单时再去查商品表。这个方案在演示时看不出问题但一旦商品下架或者价格调整历史订单显示就会错乱。正确的做法是在订单明细里冗余一份商品名称、图片和成交价这就是电商系统里常说的快照。CREATE TABLE order_item ( id int NOT NULL AUTO_INCREMENT, order_id int NOT NULL, product_id int NOT NULL, product_name varchar(100) NOT NULL, product_image varchar(255) DEFAULT NULL, price decimal(10,2) NOT NULL, quantity int NOT NULL DEFAULT 1, PRIMARY KEY (id) );product_id的作用只是追溯真正展示订单时使用的是product_name和price快照。和导师解释的时候可以说“订单是交易凭证必须反映下单那一刻的真实情况不能让后续的商品改动影响历史数据。”这句话本身就是高分回答。3.3 库存扣减用条件更新解决超卖假设库存只剩10件两个用户几乎同时下单。如果先查库存再扣库存两个请求都可能看到库存大于0最后各自扣减成功库存就变成负数了。这就是超卖问题。解决思路很简单扣库存时把“库存足够”作为条件写进更新语句# SQLAlchemy 风格的条件更新 result Product.query.filter( Product.id product_id, Product.stock quantity ).update({ stock: Product.stock - quantity }) db.session.commit() if result 0: # 受影响行数为0说明库存不足回滚整个下单流程 raise InventoryShortageErrorupdate返回的是受影响行数如果返回0说明库存不够或者商品不存在此时整个下单事务都应该停下来。这就是乐观锁的思路不需要引入Redis和分布式锁却能在演示和答辩中讲得明明白白。3.4 购物车与订单的数据转换下单本质上是把“临时数据”转化为“正式数据”。从购物车表取出当前用户勾选的商品校验商品状态和库存计算总金额写入order和order_item最后删除已结算的购物车项。整个过程必须在一个事务里任何一步失败都要整体回滚否则会出现“订单生成了但库存没扣”或者“购物车清空了但订单没生成”的脏数据。4. 核心功能实现从登录到下单的代码落地下面给出关键功能的代码片段都是可以直接抄到项目里的程度。4.1 登录与权限控制用户模块我强烈建议直接用Flask-Login扩展不要自己手写session。它处理了登录状态、会话管理和login_required装饰器代码会简洁很多。from flask_login import login_user, logout_user, login_required, current_user from werkzeug.security import check_password_hash app.route(/login, methods[GET, POST]) def login(): if request.method POST: user User.query.filter_by(usernamerequest.form[username]).first() if user and check_password_hash(user.password_hash, request.form[password]): login_user(user) return redirect(request.args.get(next) or url_for(main.index)) flash(用户名或密码错误) return render_template(user/login.html) app.route(/logout) login_required def logout(): logout_user() return redirect(url_for(main.index))注意密码不能明文存储注册时要用werkzeug的generate_password_hash生成哈希登录时用check_password_hash校验。管理员权限可以再写一个自定义装饰器检查current_user.role是否为管理员然后应用到所有后台视图上。4.2 商品列表与搜索分页商品列表页是前台流量最大的页面需要同时支持关键词搜索、分类筛选、排序和分页。app.route(/products) def product_list(): page request.args.get(page, 1, typeint) keyword request.args.get(keyword, , typestr) category_id request.args.get(category_id, 0, typeint) sort request.args.get(sort, default, typestr) query Product.query.filter(Product.status 1) if keyword: query query.filter(Product.name.contains(keyword)) if category_id: query query.filter(Product.category_id category_id) if sort price_asc: query query.order_by(Product.price.asc()) elif sort sales: query query.order_by(Product.sales.desc()) pagination query.paginate(pagepage, per_page12, error_outFalse) return render_template(main/product_list.html, paginationpagination)paginate是Flask-SQLAlchemy自带的分页方法返回pagination对象后模板里可以遍历items拿到当前页数据用iter_pages渲染页码。这里有一个常见的坑点击翻页后搜索和分类条件容易丢失下一节会专门讲。4.3 购物车为什么选择存数据库而不是Session购物车有两种存储方案存Session还是存数据库。存Session实现简单、不需要登录但用户换设备就丢而且后台完全没法统计用户行为。这个项目选的是数据库方案购物车表的核心字段是user_id、product_id、quantity、selected。def add_to_cart(product_id, quantity1): product Product.query.get(product_id) if product is None or product.status ! 1: raise ItemUnavailableError cart_item CartItem.query.filter_by( user_idcurrent_user.id, product_idproduct_id ).first() if cart_item: cart_item.quantity quantity else: cart_item CartItem( user_idcurrent_user.id, product_idproduct_id, quantityquantity ) db.session.add(cart_item) db.session.commit()数据库方案的另一个好处是可以在购物车页面直接关联商品表展示最新的价格和图片。唯一要注意的是商品下架后购物车里仍然可能残留数据此时结算前必须再次校验商品状态。4.4 下单事务订单号、扣库存、清购物车下单视图是整个项目最需要小心的地方我把核心逻辑列出来。login_required def create_order(): items CartItem.query.filter_by( user_idcurrent_user.id, selectedTrue ).all() if not items: flash(请选择商品) return redirect(url_for(user.cart)) order Order( order_nogenerate_order_no(), user_idcurrent_user.id, total_amount0, statusOrderStatus.UNPAID ) db.session.add(order) db.session.flush() # 先生成 order.id for item in items: product Product.query.filter( Product.id item.product_id, Product.stock item.quantity ).first() if product is None: db.session.rollback() flash(库存不足) return redirect(url_for(user.cart)) product.stock - item.quantity product.sales item.quantity db.session.add(OrderItem( order_idorder.id, product_idproduct.id, product_nameproduct.name, product_imageproduct.image, priceproduct.price, quantityitem.quantity )) order.total_amount product.price * item.quantity CartItem.query.filter(CartItem.id.in_([i.id for i in items]))\ .delete(synchronize_sessionFalse) db.session.commit() return redirect(url_for(order.pay, order_idorder.id))这里用db.session.flush()拿到order.id但还没有commit后续的所有插入和修改都还在同一个事务里。出现任何异常就rollback绝对不提交半成品数据。订单号可以简单用时间戳加用户ID加随机数生成确保唯一即可不需要做得太复杂。5. 踩坑实录这些细节不注意演示时当场翻车这一部分是我在实际调试这类项目时遇到最多的问题。代码逻辑没问题但演示效果十分糟糕全在细节上。5.1 图片上传中文文件名和目录权限商品图上传时用户上传的文件名可能是中文直接保存很容易出现浏览器访问乱码。解决方法是重命名文件不要保留原始文件名。import uuid from werkzeug.utils import secure_filename if file and allowed_file(file.filename): ext file.filename.rsplit(., 1)[-1].lower() filename f{uuid.uuid4().hex}.{ext} file.save(os.path.join(app.config[UPLOAD_FOLDER], filename))上传之后还要注意本地开发时改完图片可能因为浏览器缓存显示旧图这时候强制刷新或者给图片链接加上版本号即可。如果部署到云服务器千万不要忘记给static/uploads目录写权限否则后台一上传就报错现场很尴尬。5.2 分页翻页时条件丢失商品列表页选了“红酒”分类又输入了关键词点击第2页之后所有条件全部消失回到了全部分类。原因很简单翻页链接里只传了page参数。正确做法是把当前请求里的keyword、category_id、sort一起带过去a href{{ url_for(main.product_list, pagep, keywordrequest.args.get(keyword, ), category_idrequest.args.get(category_id, 0), sortrequest.args.get(sort, )) }}{{ p }}/a这个坑虽然不大但演示时一旦点到第二页整个体验瞬间垮掉。5.3 订单状态与重复提交用户快速点击两次“提交订单”可能生成两笔一模一样的订单。前端可以给按钮加一个loading状态防止重复点击后端也要做一层保护最简单的办法是下单前检查当前用户是否存在一秒钟内创建的相同金额订单有就直接返回提示。还有一个容易被忽略的点是模拟支付接口的幂等性。不管前端怎么重复请求更新订单状态的SQL都应该只有这一个逻辑UPDATE order SET status 1, paid_at NOW() WHERE id %s AND status 0;只有状态为待支付时才能更新为待发货重复回调对同一订单只能生效一次。这个技巧在答辩时讲出来老师会认为你考虑过真实业务场景。另外本地演示时建议直接用“模拟支付”按钮完成状态流转不要真去申请第三方支付沙箱。沙箱需要回调地址本地环境没有公网地址接入过程反而会消耗大量时间而且演示时容易出岔子。5.4 登录过期与回跳用户浏览很久后session过期点击“去结算”跳转登录页登录成功后却被带到了首页用户体验很差。解决方式是在拦截未登录请求时把当前地址保存到next参数登录成功后redirect到next。Flask-Login的login_required装饰器支持这个行为但要确保登录路由正确处理request.args.get(next)防止开放重定向漏洞。这个细节做得好不好直接决定了演示流畅度。我用无痕窗口模拟真实用户流程时因为next处理不当演示中断过两次后来花十分钟修好效果立刻不一样。6. 从源码到高分答辩演示路径与常见问题准备项目写完之后真正决定分数的是演示和答辩环节。这里给出一套完整的准备思路。6.1 准备一套干净的演示数据不要随便往里塞几十条无意义的数据数据要有业务感。我建议准备5个分类比如白酒、葡萄酒、啤酒、洋酒、果酒每个分类3到5个商品价格有高有低库存有充足也有接近售罄的。设置一个管理员账号和一个测试用户账号。演示顺序建议这样走注册一个全新用户进入首页浏览商品搜索关键词使用分类筛选点进葡萄酒详情页展示酒精度、产区、年份属性加入购物车修改数量再添加第二件商品勾选商品去结算填写地址提交订单点击模拟支付订单状态变为待发货切换到后台登录管理员账号找到订单并发货回到前台刷新订单列表用户确认收货整个过程一气呵成状态变化肉眼可见。演示前一定把数据库恢复成初始状态不要带着调试时的脏数据上场。6.2 把导师最可能问的问题提前写进项目说明根据我看到的答辩情况这几个问题几乎必问为什么选Flask而不是别的框架答轻量灵活项目模块边界清晰适合中小型应用技术上能讲清楚每个环节。密码怎么保证安全答不存明文用werkzeug哈希算法处理。库存超卖怎么解决答条件更新加事务只有库存足够时才能扣减成功。订单里为什么要冗余商品信息答保存下单瞬间的交易快照避免商品变化影响历史订单。购物车为什么不用localStorage答localStorage只在本地浏览器无法跨设备同步后台也无法统计数据库方案更符合真实电商场景。这些问题在本文前面的章节都有完整答案。把答案整理成一段段文字放进项目文档答辩时就不会慌。6.3 低成本加分扩展想提高项目完成度不需要堆功能挑一两个亮点就够。按工作量从小到大排序给商品详情页增加多图展示或轮播用Session记录最近浏览做“看了又看”推荐用Chart.js给后台增加一个简单的销量趋势图用Excel实现商品批量导入导出用Pillow生成简单验证码提升登录安全性每个扩展点的代码量都不大但都能让项目和“千篇一律的商城”区分开。注意做完扩展之后要把对应的技术名词写进项目说明比如“基于协同过滤的简单推荐”“销售数据可视化”这些就是标题里的亮点词汇。最后说一点我自己的体会毕业设计最怕的不是功能少而是跑起来全是“演示型Bug”。这套酒类购物系统做完之后建议在提交前导出一次干净数据库部署演示前再恢复一次确保库存数字和订单状态都可预期。到了答辩现场“这个功能怎么实现的”远比“页面多漂亮”更重要。宁可功能朴实一点也要把每一个按钮背后的逻辑讲透。希望这篇拆解能帮你在做Flask酒类购物系统时少走一些弯路。