简介这是一套面向计算机相关专业学生与初级开发者的数据库课程设计完整工程以小型超市管理系统为主题覆盖商品、库存、订单、用户等典型业务模块适合课程设计、期末大作业、毕业设计选题及工程实训等场景使用。资源包共369个文件约68.62MB包含Java后端源码、Vue前端页面、JavaScript脚本、SQL建表脚本以及大量SVG图标与样式文件另附说明文档、PDF资料与Docker部署配置前后端分离结构清晰便于按模块阅读与二次开发。目前已有50人学习下载。项目代码经过测试运行功能完整可直接复现出相同系统设计报告亦可借鉴参考读者既能据此快速完成课设答辩也能在现有基础上扩展新功能用于练手或初期项目立项遇到使用问题还可与作者沟通获取帮助。1. 从一份课设压缩包说起小型超市管理系统到底要解决什么很多同学拿到「数据库课设--小型超市管理系统.zip」这类题目时第一反应是去搜现成源码结果下载下来一堆跑不起来的半成品数据库连不上、表结构缺字段、增删改查全是硬编码。其实这个题目的本质不是让你写一个超市而是让你用一套完整的业务闭环把数据库从设计到落地走一遍商品、库存、收银、会员、供应商这五块业务彼此有外键约束又各自有独立的增删改查需求正好覆盖数据库课程设计要求的实体关系建模、范式设计、事务处理和查询优化。适合谁看如果你正在做数据库课程设计或者刚学完 SQL 想找一个能写进简历的练手项目这套方案可以直接复现。我下面讲的不是某个压缩包里的具体代码而是这类小型超市管理系统最常见的可靠做法——用 MySQL 做存储、Python 做后端逻辑、命令行或简单 Web 界面做交互整套东西在一台普通笔记本上就能跑通。你会看到表怎么设计、连接池怎么配、事务怎么加、索引怎么建以及那些只有真正跑过一遍才会遇到的坑。2. 先把表结构定下来超市管理系统的实体关系与字段设计2.1 五张核心表撑起整个业务小型超市管理系统的数据模型不复杂但字段设计直接决定后面写 SQL 顺不顺手。我一般会先画一张实体关系草图然后落成下面这五张表。注意每张表的字段类型和约束这些是后面所有增删改查的基础。表名作用关键字段约束说明product商品信息product_id, name, category, price, stockproduct_id 主键自增price 用 DECIMAL(10,2)member会员信息member_id, name, phone, pointsphone 唯一索引points 默认 0supplier供应商supplier_id, name, contactsupplier_id 主键sale_order销售订单order_id, member_id, total, create_timemember_id 外键关联 membersale_item订单明细item_id, order_id, product_id, qty, priceorder_id 和 product_id 都是外键建表语句我习惯加上ENGINEInnoDB和DEFAULT CHARSETutf8mb4前者保证事务支持后者避免中文乱码。下面这段 SQL 可以直接在 MySQL 8.0 里执行CREATE DATABASE supermarket DEFAULT CHARSET utf8mb4; USE supermarket; CREATE TABLE product ( product_id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category VARCHAR(50), price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, INDEX idx_category (category) ) ENGINEInnoDB; CREATE TABLE member ( member_id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) UNIQUE, points INT DEFAULT 0 ) ENGINEInnoDB; CREATE TABLE sale_order ( order_id INT PRIMARY KEY AUTO_INCREMENT, member_id INT, total DECIMAL(10,2) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (member_id) REFERENCES member(member_id) ) ENGINEInnoDB; CREATE TABLE sale_item ( item_id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, qty INT NOT NULL, price DECIMAL(10,2) NOT NULL, FOREIGN KEY (order_id) REFERENCES sale_order(order_id), FOREIGN KEY (product_id) REFERENCES product(product_id) ) ENGINEInnoDB;这里有几个参数值得说清楚。price用DECIMAL(10,2)而不是FLOAT因为金额计算不能有浮点误差这是财务类系统的铁律。phone加UNIQUE约束防止同一个手机号注册两次会员。sale_item里冗余了一个price字段记录下单时的成交价因为商品价格后续可能调整订单历史必须保留当时的价格。idx_category索引是为了后面按分类查商品时走索引而不是全表扫描。2.2 外键约束和索引怎么选外键在课设里经常被忽略很多人建表时只写主键结果删商品的时候把订单明细也删了数据直接乱掉。我的做法是sale_order和sale_item之间的外键必须加sale_item到product的外键也加但product到supplier的外键可以不加因为供应商信息变动不影响商品销售记录。这是业务边界决定的不是所有关联都要用外键锁死。索引方面除了主键自带的聚簇索引我一般会在product.name上建一个前缀索引因为按商品名搜索是最频繁的操作。sale_order.create_time也建议加索引后面按日期统计销售额时能明显提速。但索引不是越多越好每加一个索引插入和更新就多一份开销课设数据量小感觉不出来真到几十万行的时候就是性能分水岭。提示建表时先把所有字段和约束想清楚再执行后期改字段类型在 MySQL 里虽然支持ALTER TABLE但如果有外键依赖改起来非常麻烦血泪经验。3. 用 Python 打通增删改查连接池、事务和参数化查询3.1 为什么不用裸连接而用连接池很多课设代码里是这么写的每次操作都mysql.connector.connect()新建一个连接用完close()。这在单次测试时没问题但如果你写了一个循环去批量插入一千条商品数据每次新建连接的开销会让整个脚本慢到怀疑人生。更严重的是频繁创建连接可能导致 MySQL 的max_connections被打满报出「Too many connections」的错误。我一般用DBUtils的PooledDB来做连接池配合pymysql驱动。下面是一个最小可用的连接池配置from dbutils.pooled_db import PooledDB import pymysql POOL PooledDB( creatorpymysql, maxconnections10, # 池中最大连接数 mincached2, # 启动时预建的空闲连接 maxcached5, # 池中最多空闲连接 blockingTrue, # 连接耗尽时阻塞等待 host127.0.0.1, port3306, userroot, passwordyour_password, databasesupermarket, charsetutf8mb4 ) def get_conn(): return POOL.connection()maxconnections10对课设来说足够了mincached2保证启动后立刻有可用连接。blockingTrue是关键当连接池耗尽时新的请求会等待而不是直接抛异常这在并发场景下更稳。注意charset必须和建库时一致否则中文商品名会变成问号。3.2 收银逻辑里的事务怎么写超市管理系统最核心的操作是收银插入一条订单、插入多条订单明细、扣减商品库存、给会员加积分。这四步必须在一个事务里完成任何一步失败都要回滚否则会出现「订单生成了但库存没扣」这种脏数据。def checkout(member_id, items): items: list of dict, e.g. [{product_id:1, qty:2, price:5.0}] conn get_conn() cursor conn.cursor() try: conn.begin() total sum(i[qty] * i[price] for i in items) # 1. 插入订单 cursor.execute( INSERT INTO sale_order (member_id, total) VALUES (%s, %s), (member_id, total) ) order_id cursor.lastrowid # 2. 插入明细并扣库存 for it in items: cursor.execute( INSERT INTO sale_item (order_id, product_id, qty, price) VALUES (%s,%s,%s,%s), (order_id, it[product_id], it[qty], it[price]) ) cursor.execute( UPDATE product SET stock stock - %s WHERE product_id %s AND stock %s, (it[qty], it[product_id], it[qty]) ) if cursor.rowcount 0: raise Exception(f库存不足: product_id{it[product_id]}) # 3. 会员积分 if member_id: cursor.execute( UPDATE member SET points points %s WHERE member_id %s, (int(total), member_id) ) conn.commit() return order_id except Exception as e: conn.rollback() raise e finally: cursor.close() conn.close()这段代码有几个细节值得注意。UPDATE product SET stock stock - %s WHERE ... AND stock %s这个写法把库存检查放进了 SQL 的 WHERE 条件里利用数据库的行锁保证并发下不会超卖。如果rowcount为 0说明库存不够直接抛异常触发回滚。cursor.lastrowid拿到刚插入的订单 ID用于关联明细。所有参数都用%s占位符这是参数化查询能防止 SQL 注入也是课设答辩时老师常问的点。3.3 查询接口的写法与分页查询商品列表时如果数据量大一定要分页。我一般用LIMIT和OFFSET同时返回总条数供前端计算页数def list_products(keywordNone, page1, size20): conn get_conn() cursor conn.cursor(pymysql.cursors.DictCursor) where params [] if keyword: where WHERE name LIKE %s params.append(f%{keyword}%) cursor.execute(fSELECT COUNT(*) AS cnt FROM product {where}, params) total cursor.fetchone()[cnt] offset (page - 1) * size cursor.execute( fSELECT * FROM product {where} ORDER BY product_id DESC LIMIT %s OFFSET %s, params [size, offset] ) rows cursor.fetchall() cursor.close() conn.close() return {total: total, page: page, items: rows}DictCursor让查询结果直接是字典省去手动映射字段的麻烦。LIKE查询的%通配符放在参数里而不是拼进 SQL 字符串同样是为了防注入。分页参数size不要设太大课设场景 20 到 50 足够设成 1000 的话一次拉太多数据内存和网络都吃不消。4. 避坑与排查课设里最容易翻车的五个地方4.1 中文乱码现象是商品名显示问号原因是字符集不统一现象插入「可口可乐」后查询出来是「??????」。原因通常是建库时用了latin1或者连接时没指定utf8mb4。解决建库语句加DEFAULT CHARSET utf8mb4Python 连接参数加charsetutf8mb4两边必须一致。如果表已经建好了用ALTER TABLE product CONVERT TO CHARACTER SET utf8mb4;转换。4.2 外键报错现象是插入订单明细时提示 Cannot add or update a child row现象插入sale_item时提示外键约束失败。原因通常是order_id或product_id在父表里不存在或者插入顺序反了——先插了明细再插订单。解决严格按member → sale_order → sale_item的顺序插入并且确保引用的 ID 真实存在。调试时可以先SELECT一下父表确认记录在。4.3 连接池耗尽现象是程序跑一会儿就卡住不动现象批量操作时程序突然卡死日志里出现Too many connections。原因通常是get_conn()之后忘记close()连接被占满。解决所有数据库操作都用try...finally包裹在finally里关闭游标和连接。用连接池的话conn.close()实际是归还连接而不是真正断开所以必须调用。4.4 事务未提交现象是插入成功但换个客户端查不到数据现象Python 里执行了INSERT没报错但用 Navicat 或命令行查不到新数据。原因是pymysql默认开启了事务但没自动提交必须显式conn.commit()。解决要么在连接参数里加autocommitTrue要么在每次写操作后手动提交。我一般用显式提交因为收银逻辑需要事务控制。4.5 库存超卖现象是并发测试时库存变成负数现象两个线程同时收银同一个商品各卖 5 件库存只有 8 件结果两个都成功库存变成 -2。原因是先查库存再更新中间有时间窗口。解决把库存检查写进UPDATE的WHERE条件里利用 InnoDB 的行锁保证原子性就像 3.2 节里那样写。这是数据库并发锁最典型的应用场景。5. 让课设拿高分从能跑到好用的三个进阶技巧5.1 用视图和存储过程简化统计查询课设答辩时老师很喜欢问「你这个系统能统计什么」。与其在 Python 里写一堆聚合逻辑不如直接在数据库层建视图。比如每日销售额视图CREATE VIEW v_daily_sales AS SELECT DATE(create_time) AS sale_date, COUNT(*) AS order_count, SUM(total) AS revenue FROM sale_order GROUP BY DATE(create_time);之后查询只需要SELECT * FROM v_daily_sales ORDER BY sale_date DESC;干净利落。存储过程也可以写一个「按月统计会员消费」的答辩时演示一下调用过程比纯 CRUD 有说服力得多。5.2 用 EXPLAIN 验证索引有没有生效建了索引不代表查询就会走索引。我习惯在关键查询前加EXPLAIN看一眼执行计划EXPLAIN SELECT * FROM product WHERE category 饮料 ORDER BY price DESC;重点看type列如果是ALL说明全表扫描需要检查索引是否建对如果是ref或range就正常。rows列估算扫描行数越小越好。课设数据量小的时候全表扫描也很快但养成看执行计划的习惯面试时能加分。5.3 数据导入导出用 LOAD DATA 批量初始化演示系统时手动插几十条商品数据太慢我一般用LOAD DATA LOCAL INFILE从 CSV 批量导入LOAD DATA LOCAL INFILE /tmp/products.csv INTO TABLE product FIELDS TERMINATED BY , LINES TERMINATED BY \n IGNORE 1 ROWS (name, category, price, stock);注意 MySQL 8.0 默认关闭了local_infile需要在服务器端SET GLOBAL local_infile 1;并在连接参数里加local_infileTrue。CSV 第一行是表头用IGNORE 1 ROWS跳过。这个技巧在初始化测试数据和做数据迁移时特别实用。最后说一个我自己的习惯每次改完表结构或写完一个新查询我都会把 SQL 单独拿到命令行里跑一遍确认结果对了再写进 Python。直接在代码里调试 SQL 效率太低命令行才是数据库工程师的主场。希望帮到你。本文还有配套的精品资源点击获取
