简介:基于Flask、Python与MySQL构建的电子商城项目源码,面向计算机相关专业在校学生和初级开发者,尤其适合作为毕业设计、课程设计或课程作业的完整参考。代码涵盖前端页面展示、后端业务逻辑与数据库交互,并附带SQL脚本与使用说明,便于快速部署和调试。压缩包共2000个文件,约85.92MB,其中以1891个Python文件为主体,辅以少量C源码、头文件、文本说明等类型,分别承担核心功能、底层扩展与文档说明,目录结构清晰,便于按模块检索。目前已有874人学习下载。项目经测试运行稳定,既可完整用于项目交付,也可在商品管理、订单处理等模块上进行二次开发,或作为项目初期立项演示模板。对于希望深入理解Web开发全流程、数据库设计及系统部署的用户,是一份实用的参考代码。
1. 基于 Flask + Python + MySQL 的电子商城:一份能直接跑的课程设计源码
先说结论:这套基于 Flask + Python + MySQL 的电子商城源码,不是那种只能看不能跑的半成品,而是把用户端、后台管理、订单流程和 SQL 初始化脚本都打包好的完整项目。对于正在做 Python 课程设计、毕业设计的人来说,它最值钱的地方在于数据库表结构是现成的、前台后台逻辑是通的、登录注册购物车下单这些核心闭环都做了,拿到手改改就能用。
这套商城覆盖了典型的 B2C 电商场景:前台有商品列表、商品详情、购物车、订单确认,后台有商品管理、订单管理、分类管理,权限上区分了普通用户和管理员。技术栈就是 Flask + PyMySQL + MySQL,没有引入 Redis、Celery 这些重组件,对刚入门或者急着交作业的人非常友好,跑起来不挑机器。如果你是第一次接触 Flask 商城项目,或者想找一个结构清楚、能讲明白设计思路的课设源码,这份资源值得花时间拆一遍。
2. 技术选型与数据库设计:为什么这个组合最适合课设
2.1 Flask 轻量框架的定位
Flask 在 Python Web 框架里的定位是「微内核 + 自由拼装」。它不像 Django 那样把 ORM、Admin、表单、认证全部内置,而是只给你路由、模板渲染和请求响应这套最小核心,其他的通过扩展自己组装。对于电子商城这类中小型项目,这个特性反而是优势:你能在代码里看清楚每个环节是怎么接上去的,不会像 Django 那样被「自动生成的后台」掩盖了实现细节。
商城里最常用的几个 Flask 组件——flask_sqlalchemy管 ORM、flask_wtf管表单、flask_login管会话——通过pip install装进来后,项目的结构依然是你可控的。课程设计答辩的时候,老师问你某个功能怎么实现的,你打开自己写的视图函数就能讲清楚,不需要去翻框架源码。这是选 Flask 而不是 Django 的核心理由。
2.2 MySQL 与 Flask 的配合方式
项目的数据层没有用 MySQL 官方的mysql-connector-python,而是用了PyMySQL。原因很实在:PyMySQL是纯 Python 实现的,不需要编底层 C 扩展,装完就能连,在 Windows 和 Linux 上的表现一致。配合flask_sqlalchemy的连接串写法如下:
import pymysql pymysql.install_as_MySQLdb() # config.py 中配置 SQLAlchemy class Config: SECRET_KEY = 'your-secret-key-here' SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:your_password@127.0.0.1:3306/shop_db?charset=utf8mb4' SQLALCHEMY_TRACK_MODIFICATIONS = False这段配置里,install_as_MySQLdb()是因为 SQLAlchemy 默认找MySQLdb这个驱动,PyMySQL 通过这个函数把自己伪装成它,从而避免额外安装mysqlclient时的编译报错。连接串中的charset=utf8mb4很关键,只有用它才能存表情符号这类四字节字符。TRACK_MODIFICATIONS设为 False 是为了省内存,因为每次请求结束 SQLAlchemy 都要对比对象变更,在商城这种高频读写场景下没必要。
2.3 数据库表结构与电商核心流程的映射
这份资源里的 SQL 文件建了五张核心表:用户表(user)、商品表(goods)、分类表(category)、购物车表(cart)、订单表(order)。订单和商品之间还有一张订单明细表(order_item),用来记录每个订单里买了哪些商品、单价多少、数量多少。这个设计遵循了电商系统最基本的范式:订单主表存总金额和收货信息,明细表存快照数据。
为什么要存快照?因为商品价格是会变的。如果订单表只关联商品 ID,商品涨价后你的历史订单金额就跟着变了,这属于严重的业务事故。所以下单时要把当时的商品名、单价、数量复制到order_item表里,之后商品随便涨价降价,都不影响已经生成的订单。电商系统的表设计,绝大多数都遵循「主表 + 明细表」的模式,你把这个结构讲清楚,课设答辩基本就稳了。
CREATE TABLE `order` ( `id` int NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` int NOT NULL COMMENT '下单用户ID', `total_price` decimal(10,2) NOT NULL COMMENT '订单总金额', `receiver_name` varchar(50) NOT NULL COMMENT '收货人姓名', `receiver_phone` varchar(20) NOT NULL COMMENT '收货人电话', `receiver_address` varchar(200) NOT NULL COMMENT '收货地址', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待付款 1已付款 2已发货 3已完成', `create_time` datetime NOT NULL, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';设计要点在于order_no用独立字段而不是直接拿自增主键当订单号,这是因为订单号在很多场景下需要暴露给用户和物流系统,自增 ID 会泄露平台的订单量。业务上一般用时间戳加随机数生成。total_price用decimal(10,2)而不是float,是因为二进制浮点数在计算金额时会有精度丢失,0.1 加 0.2 不等于 0.3 的经典问题在电商金额上是绝对不能出现的。
2.4 分页查询与防刷设计的实现方式
商城商品列表必须分页,否则商品一多页面加载就卡死。项目里用 Flask-SQLAlchemy 的paginate方法:
page = request.args.get('page', 1, type=int) per_page = 12 # 商品列表分页 paginate = Goods.query.filter_by(category_id=category_id, is_on_sale=True)\ .order_by(Goods.sales.desc())\ .paginate(page=page, per_page=per_page, error_out=False) goods_list = paginate.items total_pages = paginate.pageserror_out=False的意思是当请求的页码超出范围时,不抛 404 而是返回空列表——用户在地址栏乱改?page=999也不会把页面打崩。order_by(Goods.sales.desc())让销量高的商品排在前面,这是商城默认排序逻辑里最关键的一行,直接影响了用户的购买转化。在真实项目中,这一步通常会改成 Redis 缓存里的实时热销榜,但在课设场景下数据库直接排序已经足够。
3. 源码运行与部署:从零把商城跑起来的完整流程
3.1 环境准备与依赖安装
运行这套源码前,先确认本地环境:Python 版本建议 3.8 到 3.10,MySQL 5.7 或 8.0 均可。太老的 Python 3.6 有些依赖会装不上,太新的 Python 3.12 某些 Flask 版本可能有兼容问题。建议用虚拟环境把依赖隔离,不要直接往系统 Python 里装包,不然将来做别的项目时依赖冲突会很头疼。
依赖的核心清单在requirements.txt里,装起来很简单:
# 创建并激活虚拟环境(Windows) python -m venv venv venv\Scripts\activate # 创建并激活虚拟环境(macOS / Linux) python3 -m venv venv source venv/bin/activate # 安装项目依赖 pip install -r requirements.txt这套资源对虚拟环境兼容性做得不错,requirements.txt里没有用那种带有严格版本号的==锁定全部依赖,只锁定了 Flask 和 SQLAlchemy 的主版本范围,这样在不同机器上安装时不太容易碰上二进制包缺失的坑。装完之后用pip list检查一下,确认 Flask、PyMySQL、flask_sqlalchemy 都在列表里。
3.2 本地 MySQL 建库与 SQL 脚本导入
接下来导入 SQL 脚本。打开 MySQL 命令行或 Navicat,先创建数据库,然后再导入。顺序不能反——不存在就先执行CREATE TABLE会直接报No database selected错误。
# 方式一:命令行导入 mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS shop_db DEFAULT CHARSET utf8mb4;" mysql -u root -p shop_db < shop.sql # 方式二:如果表结构有更新,用 source 方式更直观 mysql -u root -p mysql> source /path/to/your/shop.sql;导入完成后,来验证一下数据是否完整。重点看三件事:表数量对不对、关键表有没有数据、自增主键是否正常。用下面这条 SQL 做一次全面体检:
-- 检查所有表是否存在 SHOW TABLES; -- 检查商品表是否有数据 SELECT COUNT(*) AS goods_count FROM goods; -- 检查分类表是否有数据 SELECT COUNT(*) AS category_count FROM category; -- 检查管理员账号是否存在 SELECT * FROM user WHERE role = 'admin';正常情况下商品表里应该有几十条测试商品,分类表里至少有手机、电脑、服饰等几个大类,管理员账号的密码是 MD5 加密存储的。如果goods_count是 0,说明 SQL 导入时可能漏了 INSERT 语句,重新导入一遍就好。
导入后要留意一个细节:SQL 文件里的user表创建语句,如果建表时指定了DEFAULT CHARSET=utf8mb4,那在 MySQL 5.7 上一切正常,但在 MySQL 8.0 上要注意字符集校验规则。8.0 默认的utf8mb4_0900_ai_ci和 5.7 的utf8mb4_general_ci存在排序规则差异,但项目 SQL 文件里如果明确指定了ENGINE=InnoDB DEFAULT CHARSET=utf8mb4,MySQL 会自动用当前版本的默认排序规则填充,这套源码的 SQL 是从实际课设中抽取的,你只需要保证导入时不报错即可。
3.3 连接配置修改与应用启动要点
SQL 导入成功后,打开config.py把数据库账号密码改成你自己的。这一步是新手翻车重灾区,十个人里有八个是卡在这里。改完配置后用下面的方式启动:
# 开发模式启动 python app.py # 指定端口启动(默认 5000 被占用时用) python app.py --port=5001启动成功后控制台会输出Running on http://127.0.0.1:5000,浏览器打开这个地址就能看到商城首页。前台可以看到商品列表和详情,注册一个普通账号后可以用购物车和下单功能。后台入口一般是/admin,用 SQL 文件里内置的管理员账号登录就能进入管理界面。
3.4 生产环境部署:Gunicorn + Nginx 的组合
课程设计如果只要求在本地跑,开发服务器就够了。但如果老师要求部署到云服务器上演示,用 Flask 自带的开发服务器是不行的——它是单进程单线程的,并发稍高就卡死,而且启动时会明确打印WARNING: This is a development server。正规的做法是用 Gunicorn 做 WSGI 服务器,Nginx 做反向代理。
# 安装 Gunicorn pip install gunicorn # 用 Gunicorn 启动(4 个 worker 进程) gunicorn -w 4 -b 127.0.0.1:8000 app:app # 后台运行并输出日志 nohup gunicorn -w 4 -b 127.0.0.1:8000 app:app > /var/log/shop.log 2>&1 &-w 4代表 4 个 worker 进程,每个进程独立处理请求。worker 数量不是越多越好,一般建议是 CPU 核数的 2 倍加 1。对于课设的并发量,4 个 worker 完全够用。app:app的第一个 app 是文件名,第二个 app 是 Flask 实例名,写错任何一个都会报ModuleNotFoundError。
Nginx 的配置核心是把端口 80 的请求转发到 Gunicorn 的 8000 端口:
server { listen 80; server_name your_domain_or_ip; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态文件由 Nginx 直接处理,不经过 Gunicorn location /static/ { alias /path/to/your/project/static/; } }proxy_set_header这三行不能省。特别是Host字段,如果不透传,Flask 生成绝对 URL 时会用错域名。X-Forwarded-For是用来记录真实客户端 IP 的,否则 Flask 里request.remote_addr拿到的永远是 Nginx 所在的 IP,将来做访问统计或者反爬限流的时候数据全是错的。
4. 避开那些常见的坑:从配置错误到编码问题的排查与修复
4.1 PyMySQL 驱动报错:ModuleNotFoundError
现象:启动项目时直接报ModuleNotFoundError: No module named 'MySQLdb'。
原因:SQLAlchemy 默认寻找的是 MySQLdb 驱动,但项目用的是 PyMySQL。虽然config.py里已经写了pymysql.install_as_MySQLdb(),但如果这个调用在 SQLAlchemy 初始化之后才执行,就来不及生效。
解决:把import pymysql和install_as_MySQLdb()移到项目入口文件的最顶部,确保在任何db = SQLAlchemy(app)创建之前完成。我的习惯是把这两个放到app.py的第一行和第二行,永远不让它出问题。
4.2 中文乱码和 SQL 导入报错
现象:SQL 文件执行到一半报Incorrect string value,或者导入成功但页面上商品名显示一堆???。
原因:建库时没有指定 utf8mb4 字符集。MySQL 默认的latin1存不了中文。虽然CREATE TABLE语句里指定了DEFAULT CHARSET=utf8mb4,但如果数据库本身用了 latin1,表创建时会被数据库的默认值覆盖,或者 Navicat 导入时用了错误的编码。
解决:删库重建,建库时显式指定:
DROP DATABASE IF EXISTS shop_db; CREATE DATABASE shop_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE shop_db; SOURCE /path/to/your/shop.sql;这里COLLATE utf8mb4_general_ci选得比较保守,兼容性最好。MySQL 8.0 默认的utf8mb4_0900_ai_ci在某些旧版 SQLAlchemy 下会有排序规则冲突,统一用general_ci就能避免。导入完成后用SHOW CREATE TABLE goods;看一眼建表语句里的CHARSET是否变成了utf8mb4,确认成功再往下走。
4.3 Navicat 导入 SQL 失败:这是因为默认 SQL 编码设置
现象:Navicat 里执行项目 SQL 文件,中途报错,检查后发现是注释乱码导致的语法中断。
原因:Navicat 导入大 SQL 文件时的默认编码如果设成GBK或ASCII,中文字段注释和商品名会变成乱码,乱码里的某些字节会破坏引号匹配,导致 SQL 解析失败。
解决:Navicat 中右键数据库,选择「运行 SQL 文件」,在编码选项里选择UTF-8。这一步容易被忽略,因为 Navicat 默认的「自动检测」在含中文的 UTF-8 文件上偶尔会猜错。
4.4 支付回调与事务一致性:课设最容易忽略的边界
现象:用户在商城点下单,数据库里订单状态变成「已付款」,但管理员后台看到的销量没有累加,或者库存扣减了但订单取消后库存没加回来。
原因:下单、扣库存、加销量这三个操作没有放在同一个数据库事务里。比如先插入了订单表,再更新库存时失败了,由于没有事务包裹,前面的 INSERT 不会回滚,数据就处于不一致状态。
解决:使用事务装饰器把关键操作包在一起:
from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() def place_order(user_id, goods_id, quantity): """下单操作:创建订单 + 扣库存 + 加销量,全部在一个事务里""" try: goods = Goods.query.filter_by(id=goods_id).with_for_update().first() if not goods or goods.stock < quantity: raise ValueError('库存不足') goods.stock -= quantity goods.sales += quantity order = Order( user_id=user_id, order_no=generate_order_no(), total_price=goods.price * quantity, status='0' ) db.session.add(order) db.session.commit() return True except Exception as e: db.session.rollback() print(f'下单失败,已回滚:{e}') return False关键点在with_for_update(),它给商品行加了行级锁。两个用户同时买同一个商品时,后到的人会等先到的人提交或回滚后才开始读取,这样就不会出现超卖。课设答辩时能说出这个设计,通常会让老师觉得你有生产环境的思维。
4.5 分页页码越界处理:直接报 404 的玄学
现象:商品列表页点击「下一页」翻到最后一页后,再手动改 URL 的?page=999,页面直接 404,后台日志显示IndexError。
原因:paginate默认error_out=True,页码越界时会主动抛 404 异常而不是返回空列表。
解决:在paginate调用中显式传error_out=False,页码越界时返回最后一页或空数据。这也是 2.4 节里那么写的原因。顺手把页码做了边界修剪:
page = request.args.get('page', 1, type=int) page = max(1, page) # 防止负数页码实战项目中,点击分页触发的请求永远来自自己生成的链接,越界概率很小。真正容易翻车的是爬虫乱扫 URL 参数、用户从收藏夹打开旧页码链接,这两种场景都能触发 404。加上error_out=False之后页面不报错,对 SEO 也比较友好。
5. 二次开发与进阶改造:三招把课设项目的含金量拉满
5.1 引入 Redis 缓存热点数据的改造
商品详情页通常是商城访问量最大的页面。数据库里同一个商品的查询可能一秒被执行几十次,虽然 MySQL 扛得住,但为了展示「缓存思想」,很多课设项目会选择引入 Redis。在商品详情页首次被访问时把数据写入 Redis,设置 5 分钟过期;后续请求直接从 Redis 拿数据,给 MySQL 减轻不少压力。
import json import redis r = redis.Redis(host='127.0.0.1', port=6379, db=0) def get_goods_detail(goods_id): """带缓存穿透保护的商品详情查询""" cache_key = f'goods_detail:{goods_id}' # 先查缓存 cached = r.get(cache_key) if cached: return json.loads(cached) # 缓存未命中,查数据库 goods = Goods.query.get(goods_id) if not goods: return None goods_data = { 'id': goods.id, 'name': goods.name, 'price': str(goods.price), 'stock': goods.stock, 'main_image': goods.main_image } # 写入缓存,设置 300 秒过期 r.setex(cache_key, 300, json.dumps(goods_data, ensure_ascii=False)) return goods_data这段代码里有个关键设计点:goods_data里的价格用了str(goods.price)而不是直接放decimal.Decimal对象。因为json.dumps序列化 Decimal 会报TypeError,电商金额值域敏感,不像普通数字那样可以用 float 随便转——转成字符串既保留了精度又绕过了序列化限制,读取时再转回 Decimal 做计算,这是生产环境里处理 Decimal 的常见做法。
5.2 登录状态保持与权限控制的优化
原项目如果用的是session['is_login']这种简化的登录判断,那有一个隐患:会话数据存在 Flask 默认的签名 cookie 里,虽然能防篡改,但如果SECRET_KEY泄露,攻击者可以直接伪造管理员身份。进阶改造可以换成flask_login的login_manager,它在服务端维护会话状态,比裸写 session 更安全规范。
from flask_login import LoginManager, login_user, login_required login_manager = LoginManager() login_manager.init_app(app) @login_manager.user_loader def load_user(user_id): """回调函数:根据用户 ID 加载用户对象""" return User.query.get(int(user_id)) @app.route('/admin') @login_required def admin_index(): """后台首页(需要登录)""" # 这里可以通过 current_user 获取当前登录用户 if not current_user.is_admin: return '无权限访问', 403 return render_template('admin/index.html')user_loader回调是flask_login的核心机制,每个请求进来时,框架会从 session 里取user_id字符串,传给这个函数加载出用户对象,然后挂到current_user上。如果不注册这个回调,任何受保护的页面都会直接报错。JSON Web Token 方案相比 Session 方案有一个好处:服务端不需要存储 session cookie,天然适合 REST API 化改造。如果你和 SSH、Celery、JWT 这些词打过照面,就知道这套商城原生的登录校验在大型服务架构里是跑不通的,但配合 JWT 改造后就能接进微服务体系。
5.3 将商城 API 化的改造思路
如果你打算把这套课设作为区块链毕设的「交互载体」——呃,说错了——如果你打算把它用在期末项目展示时对接微信小程序,最简单的方式是把路由改成 JSON 接口。小程序不支持 Jinja2 模板渲染,它需要后端返回结构化数据,前端自己画界面。改造本身不复杂,以商品列表为例:
@app.route('/api/goods') def api_goods_list(): """商品列表 API,返回 JSON 格式""" page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 10, type=int) goods = Goods.query.filter_by(is_on_sale=True)\ .order_by(Goods.sales.desc())\ .paginate(page=page, per_page=per_page, error_out=False) data = [{ 'id': g.id, 'name': g.name, 'price': str(g.price), 'sales': g.sales, 'image': g.main_image } for g in goods.items] return jsonify({ 'code': 0, 'data': data, 'total': goods.total, 'pages': goods.pages })注意把price转成str,这和第 5.1 节的处理是同一个逻辑。改成 API 之后,原来的模板渲染代码不要删,路由可以同时保留两套,只要 URL 前缀区分就行——这也就是这套源码结构设计合理的体现。从那以后,我再拿到任何 Flask 课设源码,第一步永远是先看配置文件和数据库脚本,再启动项目。这个习惯帮我避开了至少一半的翻车现场。希望这篇拆解对你有用。
本文还有配套的精品资源,点击获取