ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python购物商城毕设核心:数据库表设计、事务与库存扣减全解析

Python购物商城毕设核心:数据库表设计、事务与库存扣减全解析 简介基于Python的购物商城管理系统源码与数据库打包而成的高分毕业设计项目评审分98分。面向计算机相关专业正在做毕设的学生也适合需要项目实战练习的学习者可作为课程设计或期末大作业。项目难度适中源码均本地编译并严格调试确保可运行。压缩包共384个文件总大小15.8MB其中94个py为业务核心源码59个html配合10个js与4个css构成前端页面141个jpg和33个png用于界面图片与素材10个xml与4个pem处理配置和证书3个sql提供数据库脚本另有11个md、5个txt及2个docx文档便于了解项目架构与开发过程。已有239人学习下载内容含蔬果优选商城的前后端实现与架构概述目录结构清晰适合快速搭建完整购物流程也可作为课程设计、期末大作业及毕业设计的直接参考。1. 一个毕业设计源码包值钱的是数据流和事务拿到一份基于Python的购物商城管理系统源码数据库毕业设计.zip第一件事不是解压跑通而是拆清楚里面哪些是能答辩的、哪些是凑数的。很多人的毕设代码跑起来是一回事讲不清楚数据怎么流转、库存为什么不会超卖是另一回事。这篇笔记就顺着这个标题把商城系统的模块划分、表结构、接口实现和最容易翻车的几个坑一次讲透最后给你一份能拿去自查的验收清单。适合正在做课程设计、或者想拿 Python 数据库练手写商城系统的从业者和学生。2. 先把商城拆成能落地的模块从数据流到表结构2.1 商城最小闭环不是“增删改查”是状态流转购物商城听起来功能很多但如果以毕业设计和课程设计的尺度去拆真正核心的闭环只有五条链路用户注册登录、浏览商品列表与详情、加入购物车、提交订单、模拟支付后订单状态推进。其余什么优惠券、积分、秒杀、评论统统是加分项不是必需品。这五条链路背后对应的是两张最重要的状态机订单的状态机和库存的数量变化。订单状态必须从“待支付”走到“已支付”再到“已完成”每一步都有时间戳和操作记录库存则是在下单那一刻被扣减而不是在支付时扣。很多初写商城的人习惯在支付回调里扣库存结果用户下单不支付库存被锁死这是第一个认知误区。从数据流的角度看一次购买行为产生的数据依次是用户表的一条记录、商品表的一条只读查询、购物车表或 session里的临时数据、订单表的一条主记录、订单明细表的若干条子记录以及商品表里某个字段的库存减一。这个顺序决定了你需要哪些表以及每张表的主外键关系。2.2 五张核心表逐字段设计用户、分类、商品、订单、订单明细我不建议一上来就用 Django 的 models 自动建表因为答辩老师一定会问“你的数据库是怎么设计的”。你至少得能拿出一份手工写的建表 SQL讲清楚每个字段为什么存在。一个最小可用的 MySQL 库我一般保留五张表user用户表字段包括id、username、password至少要哈希、phone、create_time。category商品分类表字段包括id、name、parent_id支持两级分类。product商品表字段包括id、name、category_id、price用 DECIMAL别用 FLOAT、stock、image_url、description。orders订单主表字段包括id、order_no业务单号别用自增 id 当单号、user_id、total_price、status、create_time、pay_time。order_item订单明细表字段包括id、order_id、product_id、product_name快照、price快照、quantity。强调两个容易忽略的设计点。第一order_item里必须冗余product_name和price因为商品表的价格以后会改订单明细不能跟着变这叫快照第二订单主表的status不要用魔法数字 0/1/2 散落在代码里建议用常量类或 Python 枚举去定义答辩时讲“我用常量定义状态”是个加分表达。至于为什么用 DECIMAL 而不是 FLOAT 存价格直接告诉老师FLOAT 是浮点数0.1 在二进制里是无限循环小数累加会出精度问题。这一句话就能把“你懂数据库基础”立起来。2.3 选 MySQL 而不是 SQLite别让答辩死在“本地能跑”数据库选型是毕设答辩的高频问点。SQLite 确实零配置、文件即库但老师会追问“如果并发 100 个人同时下单SQLite 怎么处理写锁”答不上来就尴尬了。所以我建议直接上 MySQL理由有三个一是 MySQL 的SELECT ... FOR UPDATE行锁能支撑你讲清楚并发扣库存二是 MySQL 的字符集和事务机制比 SQLite 更接近生产环境三是你简历上写“熟悉 MySQL”比写“用过 SQLite”更有说服力。本地开发我一般用 XAMPP 或 Docker 起一个 MySQL 8.0字符集统一utf8mb4排序规则utf8mb4_unicode_ci。连接池这块毕设层面不必上 HikariCP 或者 MyBatis 那种重型池Python 生态里用PyMySQL配DBUtils.PooledDB就够了能答出“我用了连接池避免频繁创建连接”就行。热词“mysql的数据库连接池”指的就是这一类方案代码里留一个pooled.py是加分项。3. 用 Flask 立起骨架建库脚本与第一个商品接口3.1 环境准备Python3 Flask PyMySQL 的组合Python 写商城系统框架选择上 Flask 和 Django 都行。Django 自带 Admin 后台做毕设确实省事但它的 ORM 和自动 admin 容易让新手讲不清原理Flask 更轻路由、请求、JSON 返回都能亲手写出来答辩复盘更容易。我下面的示例统一用 Flask。依赖安装一句话讲完pip install flask pymysql dbutilsflask提供路由和 Web 服务。pymysql是纯 Python 的 MySQL 驱动负责执行 SQL。dbutils提供连接池避免每次请求都新建连接。安装完成后先写一个最小的config.py保存数据库连接参数再写db.py封装连接池。注意字符集参数一定要传charsetutf8mb4否则后面中文乱码的坑躲不掉。3.2 建库建表 SQL把用户、商品、订单一次落地以下是可直接执行的 MySQL 脚本建议存成init.sql随源码一起提交。CREATE DATABASE IF NOT EXISTS mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE mall; CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL COMMENT 存哈希不存明文, phone VARCHAR(20) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE category ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, parent_id INT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE product ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, category_id INT NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, image_url VARCHAR(255) DEFAULT NULL, description TEXT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, product_id INT NOT NULL, product_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明所有表都用 InnoDB因为需要行锁和事务user表username加唯一索引防止重复注册order_no用 UUID 或时间戳加随机数生成不能用自增 id 当单号暴露订单量order_item里的product_name和price是前面说的快照字段商品改名不影响历史订单。参数说明DECIMAL(10,2) 表示总长 10 位、小数 2 位最大能存 99999999.99对毕设商城完全够status用 TINYINT 省空间配合代码里的枚举类使用。3.3 商品列表接口验证连接池与 JSON 返回建好表之后第一个接口我建议写“商品列表”因为它能一次性打通数据库连接、查询、JSON 序列化三条链路。from flask import Flask, jsonify from db import get_conn app Flask(__name__) app.route(/api/products) def product_list(): conn get_conn() cursor conn.cursor() try: cursor.execute(SELECT id, name, price, stock, image_url FROM product WHERE stock 0) rows cursor.fetchall() products [ {id: r[0], name: r[1], price: float(r[2]), stock: r[3], image: r[4]} for r in rows ] return jsonify({code: 0, data: products}) finally: cursor.close() conn.close() if __name__ __main__: app.run(debugTrue, port5000)这段代码的关键点在于get_conn()从连接池拿连接finally里确保关闭游标和连接把连接还回池子而不是直接舍弃。price转float是因为 DECIMAL 从 MySQL 取出来是Decimal对象jsonify不能直接序列化必须转类型。如果省略这一步你会看到TypeError: Object of type Decimal is not JSON serializable这是新手最常见的翻车点。参数说明stock 0的过滤条件保证前端不会展示已售罄商品这是商品列表和后台管理的根本区别debugTrue仅限本地开发部署时必须关掉。4. 购物车、下单与库存扣减最容易翻车的事务逻辑4.1 购物车用 session 还是数据库购物车有两种实现路线存 session 或建一张cart表。毕设尺度上我强烈建议 session 方案理由有三第一购物车是临时数据没必要落库用户关掉浏览器就清空逻辑简单第二省去一张表的增删改查答辩工作量变少第三session 方案能让你讲清楚“无状态 HTTP 协议如何通过 session 维持会话”。from flask import session app.route(/api/cart/add, methods[POST]) def add_cart(): user_id session.get(user_id) if not user_id: return jsonify({code: 401, msg: 未登录}) data request.get_json() product_id data.get(product_id) quantity int(data.get(quantity, 1)) cart session.get(cart, {}) cart[product_id] cart.get(product_id, 0) quantity session[cart] cart return jsonify({code: 0, msg: 已加入购物车, cart: cart})购物车存储结构是{商品id: 数量}的字典序列化进 session。这里有个细节要交代session.get(cart, {})每次拿到的都是新字典副本赋值回去必须用session[cart] cart否则 session 不会更新。这是 Flask session 机制里一个非常隐蔽的坑很多人改了购物车内容刷新页面又变回去就卡在这里。参数说明quantity要转 int 防止前端传字符串session默认存在客户端 cookie 里只适合存小数据购物车这种量级恰好合适。如果你做的是“用户未登录也能加购物车”的版本就把user_id校验去掉但订单提交时仍要校验登录。4.2 下单事务用 SELECT ... FOR UPDATE 扣库存下单是整个系统技术含量最高的地方也是答辩老师最爱深挖的地方。核心问题是两个用户同时买同一件库存只剩 1 的商品怎么保证只有一个人成功答案是用事务加行锁。MySQL 的 InnoDB 支持SELECT ... FOR UPDATE选定某一行商品并加排他锁在事务结束前其他事务的更新会被阻塞。下单接口的伪代码如下from db import get_conn app.route(/api/order/create, methods[POST]) def create_order(): user_id session.get(user_id) data request.get_json() items data.get(items) # [{product_id: 1, quantity: 2}] conn get_conn() conn.begin() try: for item in items: cursor conn.cursor() # 加锁查询防止超卖 cursor.execute( SELECT id, name, price, stock FROM product WHERE id%s FOR UPDATE, (item[product_id],) ) row cursor.fetchone() if not row: conn.rollback() return jsonify({code: 1, msg: 商品不存在}) if row[3] item[quantity]: conn.rollback() return jsonify({code: 1, msg: f商品 {row[1]} 库存不足}) # 扣减库存 cursor.execute( UPDATE product SET stock stock - %s WHERE id%s, (item[quantity], item[product_id]) ) cursor.close() order_no generate_order_no() total calculate_total(items) cursor conn.cursor() cursor.execute( INSERT INTO orders (order_no, user_id, total_price, status) VALUES (%s,%s,%s,0), (order_no, user_id, total) ) order_id cursor.lastrowid for item in items: cursor.execute( INSERT INTO order_item (order_id, product_id, product_name, price, quantity) SELECT %s, id, name, price, %s FROM product WHERE id%s, (order_id, item[quantity], item[product_id]) ) conn.commit() return jsonify({code: 0, order_no: order_no}) except Exception as e: conn.rollback() return jsonify({code: 1, msg: str(e)})这段代码的动作顺序值得反复讲先FOR UPDATE锁行再检查库存再UPDATE扣库存最后插入订单主表和明细表。FOR UPDATE必须在事务里才有意义所以conn.begin()提前开启事务。任何一个环节失败rollback()会把库存回滚不会出现“扣了库存但订单没生成”的脏数据。参数说明item[quantity]要做 int 转换和上限校验比如单次不能超过 99防止恶意大单锁库存generate_order_no()建议用datetime.now().strftime(%Y%m%d%H%M%S) 随机数避免并发下单单号冲突conn.lastrowid拿到的自增 id 用作order_item的外键。4.3 订单状态机待支付、已支付、已发货、已完成订单表里的status字段只在代码里直接赋数字是远远不够的你需要一个状态机。最小实现是写一个枚举类class OrderStatus: WAIT_PAY 0 PAID 1 SHIPPED 2 DONE 3 CANCELED 4状态流转规则只有四条待支付可以取消、待支付可以支付成已支付、已支付可以发货成已发货、已发货可以完成成已完成。禁止从待支付直接跳到已完成禁止从已取消回到待支付。这段规则写进order_service.py的一个方法里每次更新状态前先校验当前状态是否允许跳转否则直接抛异常。答辩时把这个状态机画在黑板上比念代码有效十倍。这里想体现数据库增删改查之外的东西就是把状态流转和库存扣减绑定在同一个事务里这才是商城系统区别于普通 CRUD 系统的关键。5. 避坑指南毕设答辩前的四个高频故障排查5.1 中文全部变成 “???”根源在字符集没对齐现象页面商品名称、用户昵称入库后变成一堆问号或者读到 Python 里报UnicodeDecodeError。原因数据库连接串没有指定charsetutf8mb4或者建库时用了默认的latin1。MySQL 的字符集是分层的库、表、列、连接任何一层是 latin1中文就进不去。解决建库 SQL 里明确DEFAULT CHARACTER SET utf8mb4连接池参数加charsetutf8mb4并且建表时每张表都跟一句DEFAULT CHARSETutf8mb4。改完之后已经变成???的数据只能删掉重来没有后悔药。所以这个坑必须在初始化时就堵住。5.2 Cant connect to MySQL server服务起来了没现象代码一跑就报pymysql.err.OperationalError: (2003, Cant connect to MySQL server on localhost)。原因大概率是 MySQL 服务根本没启动或者端口不是默认的 3306。还有可能是你连的是localhost但 MySQL 只绑定了127.0.0.1之外的地址。解决先执行netstat -an | grep 3306看端口是否监听再确认你的连接串里host、port、user、password四项和 MySQL 实际配置一致。我见过最离谱的是密码里带了符号被 URL 解析吞掉一半。用PyMySQL的connect()传参方式不要拼 URL能避开这类解析问题。5.3 库存扣成负数事务没生效现象商品库存只剩 5但连续下了 10 单最终库存变成 -5。原因下单逻辑里没有开启事务或者用了MyISAM引擎或者用了SELECT stock FROM product之后在 Python 里做减法再UPDATE中间查出来的是旧值。解决引擎必须 InnoDB扣减库存用单条UPDATE product SET stock stock - %s WHERE id %s AND stock %s把库存判断也塞进 SQL 条件里。这样即使事务没开数据库也会因为affected_rows为 0 而拒绝扣减。加上FOR UPDATE行锁之后并发场景也不会超卖。5.4 密码明文入库答辩老师一定会追问现象user表的password字段直接存了明文或者在 Python 里str(password)一把梭。原因偷懒或者不知道有哈希方案。解决用werkzeug.security的generate_password_hash和check_password_hash这是 Flask 自带的密码哈希工具不需要额外装包。注册时存哈希登录时check_password_hash比对。答辩时这句话值得主动说“密码不以明文存储我用的是加盐哈希”然后展示两行代码这个加分项就拿到了。6. 从“能跑”到“能交”一份验收视角的自测清单无论你的代码是下载的源码包还是自己写的交之前我建议按这张清单过一遍顺序就是从用户入口走到订单完成再反向走异常分支。验收项操作预期结果注册新用户名 密码提交提示成功数据库能看到用户名且密码是哈希串登录错误密码提交提示密码错误不跳转商品列表打开商城首页只显示库存大于 0 的商品加入购物车同一商品加两次购物车数量累加刷新页面不丢失下单购买库存刚好剩余数量的商品订单生成库存立刻减少超卖测试开两个浏览器同时买最后一件只有一个成功另一个提示库存不足订单状态模拟支付后查订单状态从 0 变 1支付时间被写入中文显示所有列表页刷新无乱码、无 ? 号这张清单每一行都是一个可演示的脚本答辩时按顺序走一遍比翻代码讲更直观。我自己的习惯是先把第八行“中文显示”测了因为这一个坑卡住后面所有演示都会灰头土脸。另一个经验是超卖测试一定要现场做哪怕只是开两个浏览器抢同一件商品这个动作一出来数据库事务的知识点就当场立住了。如果你打算在这个方向上再往前走一步可以给订单表加一个payment表模拟支付流水或者用 Redis 做热点商品缓存但切记不要为了炫技打乱主流程。毕设系统最重要的是闭环完整而不是功能花哨。我见过太多人死在“半点付款逻辑没做还硬加了个推荐算法”上。先把增删改查、事务、状态机这三件事讲透你的商城系统就已经超过一半的课程设计作品了。希望帮到你。本文还有配套的精品资源点击获取
返回列表