简介:这是一套面向计算机相关专业学生与初级开发者的数据库课程设计完整工程,以小型超市管理系统为主题,覆盖商品、库存、订单、用户等典型业务模块,适合课程设计、期末大作业、毕业设计选题及工程实训等场景使用。资源包共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, stock | product_id 主键自增,price 用 DECIMAL(10,2) |
| member | 会员信息 | member_id, name, phone, points | phone 唯一索引,points 默认 0 |
| supplier | 供应商 | supplier_id, name, contact | supplier_id 主键 |
| sale_order | 销售订单 | order_id, member_id, total, create_time | member_id 外键关联 member |
| sale_item | 订单明细 | item_id, order_id, product_id, qty, price | order_id 和 product_id 都是外键 |
建表语句我习惯加上ENGINE=InnoDB和DEFAULT CHARSET=utf8mb4,前者保证事务支持,后者避免中文乱码。下面这段 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) ) ENGINE=InnoDB; CREATE TABLE member ( member_id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) UNIQUE, points INT DEFAULT 0 ) ENGINE=InnoDB; 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) ) ENGINE=InnoDB; 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) ) ENGINE=InnoDB;这里有几个参数值得说清楚。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( creator=pymysql, maxconnections=10, # 池中最大连接数 mincached=2, # 启动时预建的空闲连接 maxcached=5, # 池中最多空闲连接 blocking=True, # 连接耗尽时阻塞等待 host='127.0.0.1', port=3306, user='root', password='your_password', database='supermarket', charset='utf8mb4' ) def get_conn(): return POOL.connection()maxconnections=10对课设来说足够了,mincached=2保证启动后立刻有可用连接。blocking=True是关键,当连接池耗尽时,新的请求会等待而不是直接抛异常,这在并发场景下更稳。注意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(keyword=None, page=1, size=20): conn = get_conn() cursor = conn.cursor(pymysql.cursors.DictCursor) where = "" params = [] if keyword: where = "WHERE name LIKE %s" params.append(f"%{keyword}%") cursor.execute(f"SELECT COUNT(*) AS cnt FROM product {where}", params) total = cursor.fetchone()['cnt'] offset = (page - 1) * size cursor.execute( f"SELECT * 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 utf8mb4,Python 连接参数加charset='utf8mb4',两边必须一致。如果表已经建好了,用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()。解决:要么在连接参数里加autocommit=True,要么在每次写操作后手动提交。我一般用显式提交,因为收银逻辑需要事务控制。
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_infile=True。CSV 第一行是表头,用IGNORE 1 ROWS跳过。这个技巧在初始化测试数据和做数据迁移时特别实用。
最后说一个我自己的习惯:每次改完表结构或写完一个新查询,我都会把 SQL 单独拿到命令行里跑一遍,确认结果对了再写进 Python。直接在代码里调试 SQL 效率太低,命令行才是数据库工程师的主场。希望帮到你。
本文还有配套的精品资源,点击获取