
手里有朋友让我帮他看看他的毕业设计题目基于Python Flask框架的蛋糕购物商城。这个题目很典型属于Python Web开发的入门级综合实战项目难度适中工作量饱满而且很适合用来展示你对Web开发全流程的理解。这几年但凡计算机相关专业的学生想做点拿得出手的东西选这个方向的人特别多。主要原因有三点第一Flask足够轻量学习曲线平缓不像Django那样上来就是全家桶对新手非常友好第二购物商城这个业务场景大家都很熟悉需求分析不费劲功能边界清晰用户、商品、购物车、订单、后台管理一套下来知识点覆盖非常全第三蛋糕这个品类有电商的一般性特征又有定制化、配送到家等个性化逻辑容易扩展出亮点功能在答辩时能讲的东西很多。这篇文章我就按做项目的实际流程把这个“蛋糕购物商城”从需求设计、数据库设计、核心功能实现到上线部署的完整思路过一遍。中间会穿插大量我实际开发中踩过的坑和调试经验希望你能少走弯路。1. 项目拆解先搞清楚蛋糕商城到底要做哪些事1.1 为什么选Flask而不是其他框架很多同学在选框架的时候会纠结Django功能全、自带admin后台、ORM强大为什么我要推荐Flask我的观点很直接如果你的目标是快速上手、搞懂Web开发的核心机制同时又要写一个能演示、可维护、代码量适中的项目Flask是最优选。Django虽然强大但它帮你做了太多事情你在Django里写一个项目很多时候是“在配置和约定中填空”而不是真正理解每个请求怎么流转、模板怎么渲染、session怎么管理。Flask恰恰相反它就像一个工具箱。你手里握着路由、模板、请求对象这三样东西剩下的都自己搭。写商城项目时你自己实现用户认证、购物车、订单状态管理这个过程对基本功的提升远大于“一行代码调用现成框架功能”。另外Flask的扩展生态足够成熟Flask-SQLAlchemy管数据库、Flask-Login管登录、Flask-WTF管表单组合起来既轻便又顺手对于中小规模项目完全够用。1.2 从“买菜思维”拆解商城的功能模块在动代码之前先把功能模块画清楚。商城类项目无论卖什么底层的用户流程都是一致的把这个逻辑捋顺了后面写代码心里才有底。我做商城项目时习惯用“买家视角”去推导功能新用户进入网站先得能注册账号、登录系统登录后浏览商品列表按蛋糕分类筛选查看商品详情喜欢就加入购物车能修改数量、删除确定下单后填写收货信息和备注然后“货到付款”或者模拟线上支付下单后能看到自己的订单列表和订单详情甚至可以取消订单管理员进入后台能管理商品分类、商品上下架、处理订单状态。这样一拆模块就出来了用户模块、商品模块、购物车模块、订单模块、后台管理模块。再往外扩展还可以有搜索、收藏、评论、优惠券、积分等。第一次做项目不要贪多先把核心五模块做扎实再挑一两个扩展点作为亮点比如“蛋糕定制备注”或者“按销量排序”都比功能堆砌要好。1.3 数据库设计给商城打地基数据库设计是这个项目里最考验功力的部分因为表结构一旦设计不合理后面写业务代码时就会处处别扭。商城系统核心表大概是这些用户表Userid、用户名、密码哈希、手机号、邮箱、收货地址、注册时间。分类表Categoryid、分类名、排序、父级id如果想做二级分类可以留parent_id字段。商品表Productid、分类id、蛋糕名称、主图路径、价格、原价、库存、销量、描述、上架状态。购物车表Cartid、用户id、商品id、数量。这里需要注意唯一约束同一个用户同一件商品只能有一条记录。订单表Orderid、订单号、用户id、总金额、收货信息快照、订单状态、创建时间。订单明细表OrderItemid、订单id、商品id、商品名称快照、单价、数量。这里有一个很重要的设计思想叫“快照”。什么意思用户下单之后商品名称、价格、图片这些信息不应该再去关联查询商品表而应该直接存在订单明细里。否则你过段时间改了商品价格历史订单显示的数据也会跟着变这在真实业务里是绝对不允许的。蛋糕这种商品还可能定制快照字段里还可以加一行“定制备注”保存用户在下单时填的“生日快乐”之类的蛋糕面文字。我建议表字段的注释写清楚类型要设置合理。比如价格字段很多新手会存成float。一般的电商项目价格要么用decimal类型做精确运算要么直接用整数存“分”。用“分”这个单位是最省心的因为整数运算永远不会遇到浮点精度问题前端显示时再除以100转成“元”。这个习惯你越早养成越好。2. 核心逻辑逐一击破从注册登录到下单选购2.1 密码不能明文存注册登录背后的安全细节注册登录这个功能看起来简单但里面有个最常见的坑——密码存储。你是绝对不能把用户密码明文写到数据库里的一旦数据库泄露用户的密码就全暴露了这是一个严重的责任问题。正确的做法是使用哈希算法对密码进行加密存储。Flask生态里最顺手的是Werkzeug自带的密码散列工具Flask本身就是依赖Werkzeug的所以不需要额外安装别的库。代码长这样# utils/security.py from werkzeug.security import generate_password_hash, check_password_hash def hash_password(password): # 默认使用pbkdf2算法可以显式指定方法 return generate_password_hash(password, methodpbkdf2:sha256) def verify_password(password_hash, password): return check_password_hash(password_hash, password)注册时调用hash_password生成哈希存到数据库的password字段登录时取出用户记录调用verify_password校验。这样就做到了“数据库里永远没有明文密码”。登录状态保持是另一个要点。Flask的session默认是客户端cookie内容会做签名但不会加密所以你千万不要往session里塞敏感信息。一般只存用户的id比如session[user_id] user.id然后在请求进来时根据这个id去查用户对象。这里要特别提醒Flask的SECRET_KEY要设置成一个足够随机、足够长的字符串否则session被逆向伪造登录态就可以被冒充后果很严重。千万不要用“123456”这种。如果你选用了Flask-Login扩展它做的事情本质上也是类似的把用户id存到session然后通过user_loader回调加载用户。区别在于它帮你封装了login_required装饰器、current_user上下文等方便的东西。我个人的看法第一次做项目手写session管理一次能帮你理解登录机制的本质理解了之后再引入Flask-Login你会用得比谁都明白。2.2 购物车用session还是数据库两种方案的对与错购物车是这个项目里最值得展开讲的一个模块因为它有两种典型实现方案很多教程各执一词你需要理解它们各自的使用场景。方案一不登录也能加购物车数据存session。这种适合“游客购物”场景用户没登录就可以把东西丢进购物车结账时再强制登录合并数据。优点是转化率高用户操作无门槛缺点是session存在cookie里数量和内容不能太多而且用户换设备或者清浏览器缓存购物车就丢了。方案二登录用户的购物车存数据库。购物车表关联用户id随时可以从数据库加载多端同步数据可靠而且可以方便地做“降价提醒”“购物车商品失效提醒”这类额外功能。缺点是需要登录之后才能加购物车体验上多了一步。我的建议是课程设计和毕业设计直接做方案二即数据库存购物车。理由很简单第一逻辑更清晰一个用户对应多条购物车记录增删改查都是对数据库操作好讲解第二你看代码的“量感”更足写起来也不复杂第三后续如果想把项目扩展成“游客购物车合并”核心代码用JSON存购物车结构改动也不算伤筋动骨。购物车的核心操作就四个加入购物车、修改数量、删除、计算总价。加入购物车时要注意“如果商品已经在购物车则数量增加而不是新增一条记录”这个逻辑用一条查询就能搞定。# routes/cart.py 核心片段 cart_item Cart.query.filter_by(user_idcurrent_user.id, product_idproduct_id).first() if cart_item: cart_item.quantity quantity else: cart_item Cart(user_idcurrent_user.id, product_idproduct_id, quantityquantity) db.session.add(cart_item) db.session.commit()还有一个细节是在购物车列表里要实时和商品表关联以便展示最新价格和最新库存。如果商品已经下架购物车条目应该显示“已失效”状态结算时自动跳过这个逻辑一定要写否则会出现用户买了一个已经下架商品的情况。2.3 下单流程库存、状态机、幂等性三件事下单是整个商城的枢纽环节也是我认为这个项目里最值得花心思设计的部分。一个健壮的下单流程至少要处理好三件事库存校验、订单状态流转、防重复提交。库存校验理论上是两步查询库存是否足够扣减库存。这中间有个隐患叫“并发超卖”——两个用户同时下单都看到库存还有1个结果都扣减成功库存变成-1。真实电商系统会借助数据库事务和行级锁来解决。在Flask里最简单的处理方式是下单选用了悲观锁的思想先查询并锁住商品行再判断库存再扣减。具体到SQLAlchemy可以用with_for_update()方法product Product.query.filter_by(idproduct_id).with_for_update().first() if product.stock quantity: # 库存不足回滚事务 return error_result(库存不足) product.stock - quantity db.session.commit()注意这个方法依赖底层数据库引擎支持行级锁MySQL的InnoDB没问题SQLite在并发场景下表现弱一些但作为学习项目完全可以接受。如果你觉得锁的概念太难理解那踏踏实实把事务写对也行——把库存扣减和订单创建放在同一个事务里一旦任何一步失败就整体回滚至少能保证数据一致性。再说订单状态机。一个订单从创建到完结状态流转应该是单向的、确定的。我推荐比较简单的状态设计待付款下单后未支付待发货已支付配送中已发货已完成确认收货已取消未支付时用户可以取消每个状态对应一组允许的操作。比如“待付款”可以取消可以支付“待发货”可以发货“配送中”可以确认收货。禁止跨状态跳转。这个设计在代码里其实就是一堆if判断但如果你用枚举或者常量类来定义状态可读性会好很多。# models/order.py from enum import Enum class OrderStatus(Enum): UNPAID 1 PAID 2 SHIPPED 3 COMPLETED 4 CANCELLED 5最后是防重复提交。这个坑非常常见用户在支付页面手抖点了两下“提交订单”结果生成了两个订单。最简单的防重手段是在前端按钮上做loading禁用但后端同样要防一手。常规做法是生成一个唯一的幂等键比如随机的uuid提交订单时带上服务端记录这个键已使用重复提交直接拒绝。对于课程设计还有一个更省事的方案创建订单前检查该用户是否已有一个“待付款”的订单如果有就不允许再创建直接跳回到原订单。这个做法对“蛋糕商城”这种低频C端场景足够用了。3. 从零搭起来的实操记录环境、代码、部署3.1 环境准备与项目初始化项目动手之前先把环境搞定。我推荐用Python 3.10及以上版本配合虚拟环境venv管理依赖。很多同学一上来就pip install之后全网乱装环境弄得一团糟后面排查问题苦不堪言。正确姿势是这样# 创建虚拟环境 python -m venv venv # 激活虚拟环境Windows venv\Scripts\activate # 激活虚拟环境macOS/Linux source venv/bin/activate # 安装依赖 pip install flask flask-sqlalchemy flask-wtf安装完成后建议把依赖导出一份到requirements.txt方便后面部署或者别人复现你的环境。pip freeze requirements.txt项目目录结构我用了很长时间这种分模块的组织方式非常清晰cake_shop/ ├── app/ │ ├── __init__.py # 创建app、注册蓝图与扩展 │ ├── models/ # 数据库模型 │ │ ├── __init__.py │ │ ├── user.py │ │ ├── product.py │ │ └── order.py │ ├── routes/ # 蓝图路由 │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── product.py │ │ └── cart.py │ ├── templates/ # HTML模板 │ └── static/ # CSS、JS、图片 ├── config.py # 配置文件 ├── run.py # 启动入口 └── requirements.txt用蓝图Blueprint拆分路由是Flask项目从“脚本”走向“应用”的关键一步。如果你把所有路由堆在一个app.py里项目到后期会变得又臭又长找一个路由要来回滚动很久。而用蓝图之后每个模块各管各的非常清爽。# app/routes/auth.py 蓝图示例 from flask import Blueprint auth_bp Blueprint(auth, __name__) auth_bp.route(/register, methods[GET, POST]) def register(): # 注册逻辑 pass然后在app/init.py里把蓝图注册到app上就完成挂载了。3.2 数据模型定义与数据库迁移数据模型定义是数据库设计的代码落地。以商品表为例SQLAlchemy模型写起来其实非常直白# app/models/product.py from datetime import datetime from app import db class Category(db.Model): __tablename__ category id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(64), nullableFalse, uniqueTrue) sort db.Column(db.Integer, default0) products db.relationship(Product, backrefcategory, lazyTrue) class Product(db.Model): __tablename__ product id db.Column(db.Integer, primary_keyTrue) category_id db.Column(db.Integer, db.ForeignKey(category.id)) name db.Column(db.String(128), nullableFalse) image_url db.Column(db.String(256)) price db.Column(db.Integer, nullableFalse) # 单位分 original_price db.Column(db.Integer) stock db.Column(db.Integer, default0) sales db.Column(db.Integer, default0) is_on_sale db.Column(db.Boolean, defaultTrue) description db.Column(db.Text) created_at db.Column(db.DateTime, defaultdatetime.now)这里有个常见的困扰模型定义好之后修改字段怎么同步到数据库新手最容易犯的错是“改了模型然后手动去数据库里ALTER TABLE”这样经常导致模型和表结构不一致后面查询报错都不知道错在哪里。正确的做法是用Flask-Migrate做数据库迁移。pip install flask-migrate# app/__init__.py 初始化迁移 from flask_migrate import Migrate migrate Migrate(app, db)然后按顺序执行三个命令flask db init # 初始化迁移仓库 flask db migrate -m init tables # 生成迁移脚本 flask db upgrade # 应用迁移到数据库以后每次改了模型重新执行migrate和upgrade两步就行。数据库迁移这套流程在真实项目里是标配毕设答辩时讲出来会让评委觉得你对项目的成熟度有认知而不是“写死”一个数据库。3.3 核心路由与模板渲染的联动路由是视图函数的集合模板是展示层的渲染。Flask用Jinja2模板引擎本身没什么学习成本只要会写HTML和简单的Python语法就能上手。这里我重点说两个容易忽略的点。第一个是模板继承。不要每个页面从头写一个完整的HTML而是做一个基础模板base.html把导航栏、页脚、公共样式放在里面再用block关键字定义内容区!-- templates/base.html -- !DOCTYPE html html langzh head meta charsetUTF-8 title{% block title %}烘焙小铺{% endblock %}/title /head body nav!-- 导航栏 --/nav main {% block content %}{% endblock %} /main footer!-- 页脚 --/footer /body /html子模板只需要extends继承重写content块就行了。这样所有页面共享导航栏和整体布局改动一处全局生效。很多新手忽略了模板继承每个页面复制粘贴一遍HTML后续改一个样式要全站搜索替换那叫一个酸爽。第二个是模板里的URL生成。永远不要在模板里硬编码URL地址而是用url_for函数根据视图函数名反向生成。a href{{ url_for(cart.view_cart) }}购物车/a这样即使你改了路由路径模板里的链接也能自动跟着变不会出现改了路由后一堆404找不到页面的情况。商品列表页面的核心逻辑就是分页和筛选。用Flask-SQLAlchemy的paginate方法可以很轻松地实现分页# app/routes/product.py from flask import request product_bp.route(/) def product_list(): category_id request.args.get(category_id, typeint) query Product.query.filter_by(is_on_saleTrue) if category_id: query query.filter_by(category_idcategory_id) # 按销量排序每页显示12个 pagination query.order_by(Product.sales.desc()).paginate( pagerequest.args.get(page, 1, typeint), per_page12, error_outFalse ) return render_template(product/list.html, paginationpagination)模板里渲染分页数据也非常直观{% for product in pagination.items %} div classproduct-card img src{{ product.image_url }} alt{{ product.name }} h3{{ product.name }}/h3 p classprice¥{{ product.price // 100 }}.{{ %02d % (product.price % 100) }}/p a href{{ url_for(product.detail, product_idproduct.id) }}查看详情/a /div {% endfor %}3.4 部署上线前的检查清单项目本地能跑通和真正部署上线是两回事。如果你打算把项目部署到云服务器做演示或上线有几件事必须提前检查。第一debug模式必须关闭否则出异常时会抛出完整堆栈代码这是严重的信息泄露风险。第二数据库要切换到MySQL这类生产级数据库SQLite在本地开发方便但并发能力和可靠性都撑不住线上场景。第三静态文件的路径要处理好。Flask默认静态文件位置是app/static上线后最好把图片上传目录独立出来并且把真实路径配置到上传时有权限写入的目录。部署方式我推荐用Gunicorn Nginx的组合。Gunicorn作为Python WSGI服务器负责运行Flask应用Nginx做反向代理和静态文件服务。命令大概是这样的# 安装Gunicorn pip install gunicorn # 启动应用4个worker进程 gunicorn -w 4 -b 127.0.0.1:8000 run:appNginx配置里把/路径代理到本地8000端口把/static路径直接指向项目静态文件目录这样静态资源不经过Python进程负载一下轻很多。如果你只是做课程设计或者毕设演示也可以先用PythonAnywhere、Railway这类免费/低价的PaaS平台它们对Flask项目的部署流程已经封装得很好了填一下项目根目录、启动命令就能把应用跑起来。但不管用哪种方式环境变量和密钥管理一定要提前准备好不要在代码里硬编码SECRET_KEY和数据库密码。4. 真实开发中踩过的坑问题排查与避坑指南4.1 表单提交500服务器内部错误和CSRF的坑我在做一个类似项目时遇到过一个问题用户注册表单一提交就报500错误控制台日志显示一堆类似“Bad Request The CSRF token is missing”的信息。原因是我接了Flask-WTF做表单处理Flask-WTF默认开启了CSRF保护但我的模板里没有渲染csrf_token。解决办法是确保模板里的每个表单都包含{{ form.csrf_token }}或者如果你不用Flask-WTF只是用原生form提交又开启了全局CSRF保护那就要在POST请求对应的会话里注入token再校验。对于学习项目最省心的做法是用了Flask-WTF就用它的render_form或者显示地渲染form.csrf_token字段别混用原生标签不渲染token。4.2 图片上传的路径坑商品要传蛋糕图片上传功能看似简单实际坑很多。我在开发时遇到过一个诡异的问题上传的图片马上能显示刷新之后就404了。排查下来发现是静态文件路径和上传目录配置不一致导致的。建议你在项目结构里单独建一个uploads目录并且让上传的保存路径和模板里读取时的URL路径严格对应。相对路径一定要记得基于当前工作目录或者配置里的绝对路径来拼接不要用“./upload”这种靠运行位置猜的写法否则换个目录启动项目图片全部裂掉。还有一种常见情况是Windows环境下路径分隔符的反斜杠被Flask路由当成转义字符解析出乱码最好统一用pathlib库来拼接路径。from pathlib import Path UPLOAD_DIR Path(app.root_path) / static / uploads def save_upload(file): filepath UPLOAD_DIR / file.filename file.save(filepath) url f/static/uploads/{file.filename} return url4.3 买东西关于Session和Cookie的那些事儿在调购物车和登录状态的时候我遇到过一种很隐蔽的bug部署到正式环境后用户登录偶尔会掉线加购物车偶尔会失败。后来排查发现是Cookie的域名作用域和Secure属性设置问题。开发环境用http没问题但线上如果开了HTTPSCookie必须带Secure属性否则浏览器不会在HTTPS请求中发送Cookie。还有SameSite属性如果设置不当跨域请求时Cookie也会被拦截。解决办法是在Flask的配置里明确设置app.config.update( SESSION_COOKIE_SECURETrue, # 线上开启HTTPS后必须置True SESSION_COOKIE_SAMESITELax, )另外session默认是签名但不加密如果你想把session内容加密可以考虑安装Flask-Session扩展并使用服务端session。但对于商城项目只要保证session里不存敏感信息默认方案是安全的。我的经验是session里只存用户id和登录状态标记其他用户信息每次请求从数据库里拿虽然多一次查询但数据永远是最新的。4.4 订单并发和库存超卖的补救方案前面讲了下单时用with_for_update()做行级锁但如果你用SQLite开发行级锁是不生效的高并发测试下库存就可能变负数。一个简单的补丁方案是“乐观锁”在商品表加一个version字段扣库存时带上版本号条件更新成功影响行数为1才算成功否则说明版本变了要重试。# 乐观锁方式扣库存 result Product.query.filter_by( idproduct_id, versionversion ).update({ stock: Product.stock - quantity, version: version 1 }) if result 0: # 冲突了要么商品不存在要么版本不对提醒用户重试 return error_result(库存变动中请稍后重试)这个方案虽然也有一定的局限但比无锁的状态好太多了。对于课程设计而言你能讲清楚“并发下可能存在超卖我用了乐观锁防止库存被扣成负数”这已经是亮点了。写在最后的小建议我做了这么多Flask项目总结下来最想告诉你的不是哪个框架牛而是心态。一个商城项目你在做的过程中会经历“需求设计—数据建模—逻辑实现—调试—部署”的全流程每走一步都会踩坑但每一步踩坑都有价值。做项目最忌讳的是“开开心心建环境愁眉苦脸调问题”。出现问题不可怕重要的是你有没有一套系统排查的思路比如先看日志、再分模块定位、最后搜索解决方案。针对蛋糕商城这个方向如果你学有余力我建议扩展方向考虑这几个一是接入真实或沙箱支付比如常见的支付沙箱环境把付款环节打通整个商城的完整度立刻高一个档次二是加一个后台数据统计页面用简单的图表展示订单量和销售额走势这个功能答辩时非常出效果还能顺带讲你用了哪些数据分析技巧三是购物车支持“游客模式”用JSON存cookie登录后合并数据这个能体现你对用户体验的思考。我在写自己的第一个商城项目时也曾在半夜因为购物车丢数据的问题焦头烂额。但等你把这些问题一个个处理完回头看那段时间你收获的东西是任何教程都给不了的。做出来的系统不在于大而全而在于稳而精。希望这篇内容可以帮你把蛋糕商城这个项目做扎实做出属于自己的节奏感。