ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python在线水果销售系统毕业设计全解析:数据库建模到源码实现

Python在线水果销售系统毕业设计全解析:数据库建模到源码实现 简介一份基于Python语言、采用Django框架与MySQL数据库开发的在线水果销售系统毕业设计项目面向计算机相关专业毕业生或需要快速搭建电商类课程设计的开发者。资源完整包含前台用户与后台管理两套子系统前台支持注册登录、水果推荐、购物车、在线下单及支付后台支持管理员对用户、水果分类、商品信息、订单、支付和评价的全面管理涵盖概要设计中的登录模块、用户管理模块、水果管理模块以及订单管理模块能清晰呈现系统分层结构与权限控制逻辑。压缩包共782个文件以JS、Vue、CSS等前端资源Python与Pyc后端代码SQL数据库脚本以及HTML页面和演示视频为主整体大小约42.57MB还附带安装与运行脚本可快速启动项目并对照学习。目前已有132人学习下载适合用于毕业设计参考、功能扩展或面试项目展示。1. 这个毕业设计题目到底在做什么看到「基于 python 在线水果销售系统毕业设计与实现源码数据库演示视频.zip」这个标题很多人的第一反应是又是一个商城项目。但真正动手做过的人会明白它和你印象里的电商系统完全是两码事它没有分布式、没有消息队列、没有复杂的推荐算法核心就四件事——商品管理、用户登录、购物车、订单结算。这是典型的 CRUD增删改查密集型课题恰好是毕业设计最稳妥的选题方向也是数据库课程设计、python 入门阶段最值得完整走一遍的项目类型。这套系统的价值不在于业务有多新颖而在于它把 Python Web 开发的完整链路串了起来从数据库表结构设计到后端接口实现再到前端页面联调最后到演示视频里的效果呈现。对正在做毕业设计或准备找 Python 相关工作的人来说弄懂源码里每个模块的数据库操作和参数设置比拿到压缩包本身更有意义。这篇文章就顺着这个标题把在线水果销售系统从数据库建模到源码落地的完整方案讲一遍重点放在参数怎么设、坑在哪、演示视频拍什么才能过答辩。2. 在线水果销售系统的数据库设计与建模2.1 为什么选 MySQL 而不是 SQLite做在线水果销售系统这类 python 毕业设计数据库选型一般就两个方向SQLite 和 MySQL。SQLite 胜在零配置Python 标准库自带 sqlite3解压即用很多偷懒的模板项目会用它。但答辩时老师大概率会问一句为什么不用 MySQL如果你答不上来印象分会受影响。我一般会建议直接用 MySQL理由有三个第一在线销售系统的核心是订单和库存这两个表天然需要事务支持MySQL 的 InnoDB 引擎在这方面比 SQLite 成熟第二源码包里的数据库文件通常是 .sql 脚本导入 MySQL 后用 Navicat 或 dbx 数据库工具查看表结构比在命令行里敲 sqlite3 直观得多第三MySQL 的索引优化、连接池配置这些知识点在你后续写简历和面试时是能直接拿出来讲的。如果你的开发机还没装好 Python 环境先按 python 安装教程把 3.8 版本配好然后执行下面的命令安装驱动pip install pymysql sqlalchemy cryptography说明pymysql是纯 Python 实现的 MySQL 驱动sqlalchemy是 ORM 框架cryptography是 MySQL 8.0 默认认证方式caching_sha2_password所需的依赖不装会报Authentication plugin caching_sha2_password cannot be loaded。2.2 水果销售系统的五张核心表在线水果销售系统的业务闭环是用户浏览水果 → 加购物车 → 下单 → 后台发货所以表设计围绕这个链路展开。常见做法是拆成五张表用户表、水果分类表、水果表、购物车表、订单表订单明细可以并入订单表或用独立表。这里的关键设计决策是水果表和订单明细表都要冗余存储当前价格。水果表里的价格是可变的后台改价后历史订单里记录的价格绝不能跟着变。如果订单明细只外键关联水果表改价会导致历史订单金额错乱这是答辩时很容易被追问的点。2.2.1 水果表与分类表的字段参数水果表是系统的核心数据源字段设计直接决定后面的代码复杂程度CREATE TABLE category ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名如热带水果、时令水果, sort INT DEFAULT 0 COMMENT 排序权重越大越靠前, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE fruit ( id INT NOT NULL AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(100) NOT NULL COMMENT 水果名称, price DECIMAL(10,2) NOT NULL COMMENT 当前售价单位元, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量, unit VARCHAR(10) DEFAULT 斤 COMMENT 销售单位, origin VARCHAR(50) DEFAULT NULL COMMENT 产地, description TEXT COMMENT 商品详情, image_url VARCHAR(255) DEFAULT NULL, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明价格必须用DECIMAL(10,2)而不是FLOAT这是电商系统的基础常识——FLOAT是近似值算订单总价时会出现 0.1 0.2 0.30000000000000004 这类问题打印到页面上很尴尬。status字段是做商品下架功能的如果源码里没有这个字段你后续想加下架水果在前台不可见的功能就得改表所以建表时一次性加上。idx_status这个索引很多人会忽略但前台商品列表的查询条件是WHERE status1没有索引时全表扫描数据量过万后响应时间会明显变长。2.3 订单表的状态机设计订单表是销售系统里最容易出 bug 的表问题主要集中在状态字段的设计上CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号前端展示用, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, receiver_name VARCHAR(50) NOT NULL, receiver_phone VARCHAR(20) NOT NULL, receiver_address VARCHAR(255) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL, fruit_id INT NOT NULL, fruit_name VARCHAR(100) NOT NULL COMMENT 冗余商品名防止水果改名后历史订单显示异常, price DECIMAL(10,2) NOT NULL COMMENT 下单时的快照价格, quantity INT NOT NULL, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意order_no上加了唯一索引。生成规则一般是时间戳加随机数比如time.strftime(%Y%m%d%H%M%S) str(random.randint(1000, 9999))并发下可能出现重复唯一索引能兜底。订单状态用 TINYINT 数字表示是常规做法对应关系写进注释里比用字符串状态码省空间也比散落在代码里的魔法数字好维护。2.4 SQLAlchemy 连接池的参数配置数据表设计好后Python 端连接数据库的这段配置决定了系统在并发访问时的稳定性。源码里如果用的是 SQLAlchemy核心是create_engine的参数from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker engine create_engine( mysqlpymysql://root:123456127.0.0.1:3306/fruit_shop?charsetutf8mb4, pool_size5, # 连接池保持的最小连接数 max_overflow10, # 连接池最多额外创建的连接数 pool_timeout10, # 获取连接的超时时间单位秒 pool_recycle3600, # 连接最大复用时间MySQL 默认 wait_timeout 是 8 小时 echoFalse ) SessionLocal sessionmaker(bindengine, autoflushFalse, expire_on_commitFalse)参数说明pool_recycle3600非常关键Python 写数据库服务最常见的坑就是MySQL server has gone away原因是 MySQL 服务端断开了空闲连接而客户端连接池不知道还在用旧连接。把复用时间设成 3600 秒小于 MySQL 的默认 8 小时 wait_timeout能规避大部分这类问题。expire_on_commitFalse是给后续查询用的——commit 之后对象属性不会自动过期否则你在事务提交后再读order.total_amount会触发一次额外查询。3. Python 后端核心源码实现登录、商品列表与后台增删改查3.1 项目目录结构与路由规划一个能过答辩的在线水果销售系统源码目录结构应该一眼能看懂。我见过很多同学交上来的压缩包所有代码堆在两个文件里数据库脚本和图片混在一起这种源码演示效果再好答辩时也容易被扣分。推荐的结构是这样fruit_shop/ ├── app.py # 应用入口Flask 实例化与蓝图注册 ├── models.py # SQLAlchemy 模型定义 ├── views/ │ ├── user.py # 登录注册 │ ├── fruit.py # 前台商品浏览 │ ├── cart.py # 购物车 │ └── admin.py # 后台管理 ├── templates/ # Jinja2 模板 ├── static/ # CSS/JS/图片 ├── fruit_shop.sql # 数据库脚本 └── requirements.txt路由规划要贴合业务场景/是首页水果列表/fruit/int:id是详情页/user/login和/user/register是认证/cart系列是购物车/order/confirm是结算/admin/开头的是后台管理。用 Flask 蓝图按views拆开每个文件只干一类事这本身就是答辩时能讲的项目分层设计。3.2 用户登录的密码存储与 Session 处理用户模块看起来简单但密码存储方式是源码里第一个会被老师盯上的点。明文存密码是绝对不能出现的——哪怕只是毕业设计也要用哈希。Python 标准库hashlib加盐即可不一定要引入 Flask-Loginimport hashlib import uuid from flask import session, request, redirect, url_for def hash_password(raw_password: str, salt: str None) - tuple: salt salt or uuid.uuid4().hex[:8] hashed hashlib.sha256((salt raw_password).encode()).hexdigest() return salt, hashed # 登录处理 def login(): username request.form.get(username) password request.form.get(password) user User.query.filter_by(usernameusername).first() if not user: return 用户不存在, 404 salt, hashed hash_password(password, user.salt) if hashed ! user.password: return 密码错误, 401 session[user_id] user.id session[username] user.username return redirect(url_for(fruit.index))逻辑说明uuid.uuid4().hex[:8]生成的随机盐存进用户表密码哈希值是sha256(salt password)的结果。这里不用加decode(utf-8)之类的折腾因为hexdigest()返回的就是字符串。登录成功后把user_id写进 Flask 的session注意 Flask 的 session 默认是签名的 Cookie不是服务端存储所以千万别往里面放大对象只放 ID 和用户名即可。3.3 前台商品列表的分页与条件筛选商品列表的查询是数据库增删改查里最常规但也最能体现水平的接口。直接fruit_list Fruit.query.all()然后传给模板数据量小时没问题但老师可能顺手问你如果有一万条水果记录怎么办。标准答案是分页加筛选from flask import request def index(): page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 12, typeint) category_id request.args.get(category_id, 0, typeint) keyword request.args.get(keyword, ).strip() query Fruit.query.filter(Fruit.status 1) if category_id: query query.filter(Fruit.category_id category_id) if keyword: query query.filter(Fruit.name.like(f%{keyword}%)) # 按上架时间倒序新到水果排前面 query query.order_by(Fruit.create_time.desc()) pagination query.paginate( pagepage, per_pageper_page, error_outFalse ) return render_template(index.html, paginationpagination)参数说明request.args.get(page, 1, typeint)里的typeint是 Flask 内置的类型转换避免前端传了个pageabc过来导致程序抛异常。per_page12是栅格系统下每行 4 个、共 3 行比较美观的默认值。error_outFalse表示当页码超出范围时返回空列表而不是 404用户体验更好。分页对象上pagination.items是当前页数据pagination.iter_pages()返回页码序列模板里做翻页按钮用的就是它。3.4 后台水果管理的源码实现要点后台管理是毕业设计演示视频里最重要的部分因为老师通常会要求现场添加一条水果记录看效果。核心是新增和修改的请求处理这里有个隐藏坑图片上传和水果信息要分两步处理。import os from werkzeug.utils import secure_filename ALLOWED_EXTENSIONS {png, jpg, jpeg, gif} def save_image(file) - str: if not file or file.filename : return filename secure_filename(file.filename) ext filename.rsplit(., 1)[-1].lower() if ext not in ALLOWED_EXTENSIONS: raise ValueError(不支持的图片格式) # 用时间戳重命名防止重名覆盖 new_name f{int(time.time())}_{uuid.uuid4().hex[:6]}.{ext} file.save(os.path.join(static/upload, new_name)) return f/static/upload/{new_name}说明secure_filename是 werkzeug 提供的安全函数会自动过滤掉文件名中的路径分隔符和危险字符不处理的话如果有人上传名为../../etc/passwd的文件保存路径就被篡改了。重命名策略是用时间戳加随机后缀这样同一张图片重复上传也不会互相覆盖。返回的路径直接存到水果表的image_url字段模板里用img src{{ fruit.image_url }}渲染。后台的删除操作要注意常规做法是逻辑删除把status置 0而不是物理删除因为水果表被order_item外键引用物理删掉后历史订单明细就悬空了。如果源码里用的是db.session.delete(fruit)答辩时建议改成逻辑删除并解释保留历史数据完整性的理由这比功能本身更容易拿分。4. 购物车与订单结算的源码联调4.1 购物车用数据库表还是 Cookie购物车实现有两种思路存在数据库表里或者存 Flask session签名 Cookie里。毕业设计源码里常见的是数据库方案因为要写到演示里让老师看到加入购物车后刷新页面数据还在。数据库方案需要两张表的配合——购物车表记录user_id、fruit_id、quantity操作接口就是标准的增删改查。这里要处理的细节是数量变更。加入购物车时如果商品已存在应该累加数量而不是插入新记录def add_to_cart(fruit_id: int, quantity: int 1): user_id session.get(user_id) if not user_id: return redirect(url_for(user.login)) cart_item CartItem.query.filter_by( user_iduser_id, fruit_idfruit_id ).first() if cart_item: cart_item.quantity quantity else: cart_item CartItem( user_iduser_id, fruit_idfruit_id, quantityquantity ) db.session.add(cart_item) db.session.commit() return redirect(url_for(cart.list_cart))逻辑说明这段代码的关键是用filter_by(user_id, fruit_id)先查一次命中则更新数量否则插入。事务单位是单次请求commit放在最后避免多次提交带来的一致性问题。要注意的是这里没有做库存校验——库存校验应该放在订单确认时而不是加购物车时否则用户购物车里放了一天水果库存变了也不知道。4.2 订单提交的事务与库存扣减订单结算是整个系统技术含量最高的部分涉及多个表的同时更新插入订单表、插入订单明细、扣减库存、清空购物车。这四步必须在一个事务里完成任何一个失败都要整体回滚否则会出现订单创建了但库存没扣或库存扣了但订单没生成的数据不一致。from sqlalchemy.exc import IntegrityError db.session.no_autoflush def create_order(user_id: int, receiver: dict, items: list): try: total 0 order orders(user_iduser_id, status0, **receiver) for item in items: fruit db.session.execute( select(Fruit).where(Fruit.id item[fruit_id]) ).scalar_one() if fruit.stock item[quantity]: raise ValueError(f{fruit.name} 库存不足) total fruit.price * item[quantity] order.item.append(order_item( fruit_idfruit.id, fruit_namefruit.name, pricefruit.price, quantityitem[quantity] )) # 扣减库存 fruit.stock - item[quantity] order.total_amount total db.session.add(order) db.session.commit() return order except Exception as e: db.session.rollback() raise e参数说明db.session.no_autoflush是 SQLAlchemy 的一个坑点——默认在查询时会先自动 flush 未提交的改动如果order还没生成主键就执行fruit.stock - quantity的赋值可能触发意外的提前写库。加上这个装饰器后查询不会再隐式提交事务控制完全由手动commit掌握。rollback()后 session 里的对象状态会失效所以上层捕获异常后直接提示下单失败请重试即可。4.3 演示视频里订单模块的拍摄脚本压缩包里带的演示视频很多人是边操作边录录完才发现关键场景没覆盖。订单模块建议至少录这四个场景登录 → 挑选水果加购物车 → 修改购物车数量 → 提交订单并到后台查看订单状态变更。每次操作前先在页面上展示数据库对应表的数据切换窗口后再操作操作完回到数据库刷新记录这个前后对比的镜头是答辩视频最有力的部分。录制时注意把屏幕分辨率调低一些代码编辑器字号放大到 18 以上数据库用 Navicat 或 dbx 数据库工具打开表结构树展开在左侧。视频里如果能出现下单前后库存从 50 变成 48的对比比任何讲解都有说服力。5. 验证数据正确性与提升答辩通过率的落地技巧5.1 三条 SQL 验证业务闭环系统做完后别急着录视频先用三条 SQL 把核心数据链路验证一遍。打开查询数据库的终端依次执行-- 1. 验证购物车和用户关联正确 SELECT c.id, u.username, f.name, c.quantity FROM cart_item c JOIN user u ON c.user_id u.id JOIN fruit f ON c.fruit_id f.id WHERE u.id 1; -- 2. 验证订单金额一致性订单总额等于明细价格乘数量之和 SELECT o.order_no, o.total_amount, SUM(oi.price * oi.quantity) AS item_sum FROM orders o JOIN order_item oi ON o.id oi.order_id GROUP BY o.id, o.order_no, o.total_amount HAVING o.total_amount ! item_sum; -- 3. 验证库存和已售数量对得上账 SELECT f.name, f.stock, IFNULL(SUM(oi.quantity), 0) AS sold FROM fruit f LEFT JOIN order_item oi ON f.id oi.fruit_id GROUP BY f.id, f.name, f.stock;逻辑说明第二条查询如果返回了任何记录说明订单金额计算有 bug优先检查Fruit.price在订单创建时是否被误改以及total_amount是否在明细行创建前就计算了。第三条能查出库存已经扣了但订单明细里没有的脏数据通常是事务回滚不彻底导致的。三条 SQL 全部返回 0 行或预期行数后数据库这部分就可以放心写进文档了。5.2 编码与依赖这两个高频坑这一节的内容在答辩前务必逐条检查它们是我见过出现频率最高的三类问题第一坑MySQL 中文乱码。建库时必须显式声明字符集CREATE DATABASE fruit_shop DEFAULT CHARACTER SET utf8mb4;。连接串里也要带charsetutf8mb4。如果演示时出现中文乱码多半是 Windows 下命令行客户端默认用了 gbk用SET NAMES utf8mb4;可以临时解决但根因还是连接参数缺了字符集声明。第二坑依赖版本不兼容。Flask 3.x 与 SQLAlchemy 2.x 的 API 有变化源码里的代码如果是按旧版写的装最新依赖必然报错。解压 zip 后第一件事是执行pip install -r requirements.txtrequirements 里最好锁定版本号比如flask2.3.3然后pip freeze看看实际装了什么。演示前在干净的虚拟环境里跑一遍python app.py确认能起服务再录视频。第三坑端口被占用。Flask 默认 5000 端口在 macOS 上常被 AirPlay 接收器占用启动会报Address already in use。改端口加一行app.run(host0.0.0.0, port8000, debugTrue)。0.0.0.0可以让局域网内其他设备访问答辩时手机扫码访问系统比电脑屏幕演示更有区分度。5.3 给源码加分的一个改进点下单前的库存二次校验如果时间和代码量允许建议给订单模块加一个高并发下的库存防超卖处理这是整个项目里最能体现对并发理解的加分项。常规做法是在扣库存的 SQL 里加条件判断替代先查询再扣减的两步操作from sqlalchemy import update # 原子扣减库存充足才更新返回受影响行数 result db.session.execute( update(Fruit) .where(Fruit.id fruit_id, Fruit.stock quantity) .values(stockFruit.stock - quantity) ) affected result.rowcount if affected 0: db.session.rollback() return 库存不足请调整购买数量说明UPDATE ... WHERE stock quantity是数据库层面的原子操作InnoDB 在更新时对行加锁天然避免了两个请求同时读到库存 5各自扣 3最后变 -1的经典超卖问题。rowcount返回实际更新的行数为 0 说明条件不满足。这个改动只有几行代码但能在答辩时引出一个很有价值的讨论点如果销量极高、单行锁成为瓶颈怎么优化答案可以往减少锁持有时间、用 Redis 做预扣减的方向聊接得住就能给答辩加分不少。数据库脚本导入、源码调试、演示视频拍摄这三件事按上面顺序过一遍这个毕业设计基本就能达到演示流畅、数据闭环、问答有料的水准。本文还有配套的精品资源点击获取
返回列表