数据库大作业实战:超市管理系统表结构设计与事务处理

发布时间:2026/9/26 1:46:19
数据库大作业实战:超市管理系统表结构设计与事务处理 简介这份PDF是面向高校数据库课程大作业场景的超市管理系统项目文档适合正在准备课程设计、需要完整案例参考的计算机相关专业学生。内容围绕小型超市线下管理展开覆盖顾客、员工、管理员三类角色的权限划分与功能设计并给出需求分析、Visual Studio 2013与MySQL开发环境配置、基本表结构与E-R图、数据库框架、关键代码段及实验问题解决等模块。资源包内含1个PDF文件大小约555KB结构紧凑便于按章节查阅。文档中详细记录了员工表、商品表、货架表、进货表与日销售量表的主键设计以及员工与商品、销售、货架之间的一对多关联还包含MFC界面优化、C-string类型调试、主外码选取等排错思路。目前已有3641人学习可作为数据库应用开发流程与项目文档撰写的实践参考。1. 从一份超市管理系统 PDF 说起数据库大作业到底在考什么每年期末总有一批人对着“数据库大作业”这几个字发愁。题目给的是超市管理系统交付物是一份 PDF但真正要交的其实是一套能跑起来的数据库建表、插数据、写查询、做事务最好再带个能演示的界面。很多人第一反应是去搜“数据库课程设计”的现成模板结果下载下来发现表结构对不上、字段名全是拼音、连主键都没设改起来比自己写还累。这份 PDF 标题背后考的不是你会不会背范式而是你能不能把一个真实业务——超市进货、销售、库存、会员——翻译成一组互相约束的表并且让增删改查在并发下不出错。适合两类人一类是刚学完 SQL 语法、需要把知识点串成项目的新手另一类是想借这个题目把索引、事务、锁这些工程概念真正用一遍的进阶者。下面我按自己带学生做课设的路径把这件事拆开讲清楚。2. 超市管理系统的表结构怎么设计才不会被老师打回2.1 先画业务流再落表别一上来就写 CREATE TABLE超市管理系统的核心业务其实就四条线商品从供应商进货入库、顾客购买出库、库存实时变动、会员积分累计。很多人翻车是因为直接打开 Navicat 就开始建表建到一半发现“销售明细”里不知道该存商品名还是商品 ID回头改表结构外键全乱。我一般会先在纸上画一张实体关系草图只写实体和动作不写字段。实体有供应商、商品、分类、库存、销售单、销售明细、会员、员工。动作有进货供应商→商品→库存增加、销售会员/散客→销售单→明细→库存减少、退货反向。画完这张图表自然就出来了而且每张表的职责边界很清楚。这里有个选型判断商品和分类要不要拆成两张表如果分类是固定几类生鲜、日化、零食可以拆方便按类统计如果分类经常变且层级深拆表后查询要递归课设阶段不划算。我一般建议拆因为“按分类查销量”是老师最爱考的查询之一。2.2 建表 SQL 与字段类型选择金额用 DECIMAL别用 FLOAT下面是我常用的建表脚本以 MySQL 8 为例字段名用英文注释写中文方便答辩时讲。注意金额字段一律用 DECIMAL(10,2)用 FLOAT 会在累加时出现 0.30000000000000004 这种玄学结果答辩现场被问到很难解释。-- 供应商表 CREATE TABLE supplier ( supplier_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 供应商ID, supplier_name VARCHAR(100) NOT NULL COMMENT 供应商名称, contact VARCHAR(50) COMMENT 联系人, phone VARCHAR(20) COMMENT 联系电话, address VARCHAR(200) COMMENT 地址 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT供应商表; -- 商品分类表 CREATE TABLE category ( category_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 分类ID, category_name VARCHAR(50) NOT NULL UNIQUE COMMENT 分类名称 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品分类表; -- 商品表 CREATE TABLE product ( product_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 商品ID, barcode VARCHAR(30) NOT NULL UNIQUE COMMENT 条形码, product_name VARCHAR(100) NOT NULL COMMENT 商品名称, category_id INT NOT NULL COMMENT 所属分类, supplier_id INT COMMENT 默认供应商, purchase_price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 进价, sale_price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 售价, stock_qty INT NOT NULL DEFAULT 0 COMMENT 库存数量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, CONSTRAINT fk_product_category FOREIGN KEY (category_id) REFERENCES category(category_id), CONSTRAINT fk_product_supplier FOREIGN KEY (supplier_id) REFERENCES supplier(supplier_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表; -- 会员表 CREATE TABLE member ( member_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 会员ID, card_no VARCHAR(20) NOT NULL UNIQUE COMMENT 会员卡号, member_name VARCHAR(50) NOT NULL COMMENT 姓名, phone VARCHAR(20) COMMENT 手机号, points INT NOT NULL DEFAULT 0 COMMENT 积分, join_date DATE COMMENT 入会日期 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会员表; -- 销售单主表 CREATE TABLE sale_order ( order_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 销售单ID, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 单号, member_id INT COMMENT 会员ID散客为空, employee_id INT COMMENT 收银员, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 总金额, pay_method TINYINT NOT NULL DEFAULT 1 COMMENT 1现金 2微信 3支付宝 4银行卡, order_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, CONSTRAINT fk_order_member FOREIGN KEY (member_id) REFERENCES member(member_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT销售单主表; -- 销售明细表 CREATE TABLE sale_detail ( detail_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 明细ID, order_id INT NOT NULL COMMENT 所属销售单, product_id INT NOT NULL COMMENT 商品ID, qty INT NOT NULL COMMENT 数量, unit_price DECIMAL(10,2) NOT NULL COMMENT 成交单价, subtotal DECIMAL(10,2) NOT NULL COMMENT 小计, CONSTRAINT fk_detail_order FOREIGN KEY (order_id) REFERENCES sale_order(order_id), CONSTRAINT fk_detail_product FOREIGN KEY (product_id) REFERENCES product(product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT销售明细表;这段脚本的关键点有三个。第一所有外键都显式命名fk_ 前缀后面如果要用ALTER TABLE删外键不用去查系统表。第二stock_qty直接放在 product 表里而不是单独建库存表因为课设阶段一个商品只在一个仓库拆表反而增加 join 成本如果题目要求多仓库再拆。第三sale_order和sale_detail拆成主从表这是订单类系统的标准做法主表存总额和支付方式明细存每个商品的数量和单价退货时只改明细状态即可。参数说明utf8mb4是为了支持 emoji 和生僻字虽然超市商品名一般用不到但会员昵称可能带ENGINEInnoDB必须写因为要事务和行锁MyISAM 不支持。DECIMAL(10,2)表示总共 10 位、小数 2 位最大 99999999.99对超市单品足够。2.3 插入测试数据用存储过程批量生成别手敲表建好后老师通常会要求“至少 20 条商品、100 条销售记录”。手敲 INSERT 既慢又容易漏我一般写一个存储过程循环插入。下面这段生成 50 个商品和 200 条销售明细数据随机但符合业务约束。-- 先插入基础分类和供应商 INSERT INTO category (category_name) VALUES (生鲜),(日化),(零食),(饮料),(粮油); INSERT INTO supplier (supplier_name, contact, phone) VALUES (华东食品, 张经理, 13800000001), (南方日化, 李经理, 13800000002), (本地果蔬, 王经理, 13800000003); -- 存储过程批量生成商品 DELIMITER $$ CREATE PROCEDURE gen_products(IN num INT) BEGIN DECLARE i INT DEFAULT 1; WHILE i num DO INSERT INTO product (barcode, product_name, category_id, supplier_id, purchase_price, sale_price, stock_qty) VALUES ( CONCAT(69, LPAD(i, 10, 0)), CONCAT(测试商品, i), FLOOR(1 RAND() * 5), FLOOR(1 RAND() * 3), ROUND(1 RAND() * 50, 2), ROUND(5 RAND() * 80, 2), FLOOR(10 RAND() * 200) ); SET i i 1; END WHILE; END$$ DELIMITER ; CALL gen_products(50);逻辑说明LPAD(i,10,0)把数字补成 10 位拼成类似真实条码的字符串FLOOR(1RAND()*5)生成 1 到 5 的随机分类 ID保证外键有效进价和售价用ROUND(...,2)保留两位避免插入时被截断报警告。执行完CALL gen_products(50)后用SELECT COUNT(*) FROM product验证应该是 50 条。如果报外键错误先检查 category 和 supplier 是否已插入这是最常见的顺序问题。3. 增删改查与事务把“销售一单”写成一条完整链路3.1 一条销售记录背后的四步操作与事务边界超市收银不是简单往 sale_order 插一行就完事。真实链路是① 插入销售单主表拿到 order_id② 循环插入销售明细③ 扣减 product 表的 stock_qty④ 如果会员累加积分。这四步必须在一个事务里否则出现“单子建了但库存没扣”或者“库存扣了但单子没建”的脏数据答辩时老师一问就露馅。下面是我常用的销售事务模板用 Python 的 pymysql 演示因为课设通常要求带界面Python 比 Java 轻量。import pymysql from decimal import Decimal def create_sale(conn, member_id, items, pay_method1): items: list of dict, 每项含 product_id, qty, unit_price cursor conn.cursor() try: conn.begin() # 开启事务 # 1. 生成单号时间戳 随机数 order_no S str(int(__import__(time).time())) str(__import__(random).randint(100,999)) total sum(Decimal(str(it[qty])) * Decimal(str(it[unit_price])) for it in items) # 2. 插入主表 cursor.execute( INSERT INTO sale_order (order_no, member_id, total_amount, pay_method) VALUES (%s,%s,%s,%s), (order_no, member_id, total, pay_method) ) order_id cursor.lastrowid # 3. 插入明细并扣库存 for it in items: subtotal Decimal(str(it[qty])) * Decimal(str(it[unit_price])) cursor.execute( INSERT INTO sale_detail (order_id, product_id, qty, unit_price, subtotal) VALUES (%s,%s,%s,%s,%s), (order_id, it[product_id], it[qty], it[unit_price], subtotal) ) # 扣库存同时用 stock_qty qty 防止超卖 affected cursor.execute( UPDATE product SET stock_qty stock_qty - %s WHERE product_id %s AND stock_qty %s, (it[qty], it[product_id], it[qty]) ) if affected 0: raise Exception(f商品 {it[product_id]} 库存不足) # 4. 会员积分每消费 1 元积 1 分 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.begin()显式开启事务pymysql 默认 autocommit 是 False但显式写更清楚。扣库存的 UPDATE 带了AND stock_qty %s条件这是防超卖的关键——如果库存不够affected 为 0直接抛异常回滚不会出现负库存。积分用int(total)取整因为积分一般是整数。参数说明items里 unit_price 用字符串转 Decimal避免浮点误差pay_method默认 1 现金。3.2 三个必练查询分类销量、会员消费排行、库存预警课设答辩时老师最爱让你现场写查询。我一般让学生提前练熟三个按分类统计销量、会员消费金额排行、库存低于阈值的商品列表。这三个覆盖了 GROUP BY、JOIN、子查询和 HAVING。-- 查询1每个分类的销售总数量和总金额 SELECT c.category_name, SUM(d.qty) AS total_qty, SUM(d.subtotal) AS total_amount FROM sale_detail d JOIN product p ON d.product_id p.product_id JOIN category c ON p.category_id c.category_id GROUP BY c.category_name ORDER BY total_amount DESC; -- 查询2会员消费排行只显示消费超过 100 元的 SELECT m.member_name, m.card_no, SUM(o.total_amount) AS consume_total FROM sale_order o JOIN member m ON o.member_id m.member_id GROUP BY m.member_id, m.member_name, m.card_no HAVING consume_total 100 ORDER BY consume_total DESC; -- 查询3库存预警低于 20 的商品 SELECT product_name, stock_qty, sale_price FROM product WHERE stock_qty 20 ORDER BY stock_qty ASC;查询 1 的 GROUP BY 必须包含所有非聚合列MySQL 8 默认开启 ONLY_FULL_GROUP_BY如果只写GROUP BY c.category_name而 SELECT 里有其他非聚合列会报错。查询 2 的 HAVING 用别名consume_totalMySQL 支持但标准 SQL 不支持答辩时如果老师较真可以改成HAVING SUM(o.total_amount) 100。查询 3 最简单但可以加一句“如果要同时显示分类名就再 JOIN 一次 category”展示你知道扩展方向。3.3 索引怎么加三个高频查询对应三个索引表数据量小的时候加不加索引看不出差别但老师常问“如果商品上万条查询慢怎么办”。这时候要能说出在 sale_detail 的 product_id 上加索引加速按商品统计在 sale_order 的 member_id 和 order_time 上加索引加速会员消费查询和按时间报表在 product 的 stock_qty 上加索引加速库存预警。CREATE INDEX idx_detail_product ON sale_detail(product_id); CREATE INDEX idx_order_member_time ON sale_order(member_id, order_time); CREATE INDEX idx_product_stock ON product(stock_qty);注意索引不是越多越好。sale_detail 插入频繁每多一个索引就多一次写开销课设阶段加这三个足够。如果老师问“为什么不在 product_name 上加索引”回答商品名模糊查询用 LIKE %xx% 用不上 B 树索引除非上全文索引课设不要求。4. 避坑与排查课设答辩前最容易翻车的五个点4.1 现象插入中文变问号原因字符集不是 utf8mb4解决建库时指定很多人建库时用默认字符集插入“可口可乐”变成“???”。原因是 MySQL 5.7 默认 latin18.0 默认 utf8mb4但如果你用工具建库没选就可能中招。解决建库语句写CREATE DATABASE supermarket DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;连接串里也加charsetutf8mb4。已经建错的用ALTER DATABASE和ALTER TABLE ... CONVERT TO补救但数据可能已经丢了只能重插。4.2 现象外键约束报错 1452原因插入顺序不对或引用了不存在的 ID解决先插父表再插子表这是最高频的错误。比如先插 sale_detail 再插 sale_order或者 product 的 category_id 填了 6 但 category 表只有 5 条。排查方法SELECT * FROM category看最大 ID再检查插入语句里的值。解决严格按 supplier→category→product→member→sale_order→sale_detail 的顺序插入。如果必须乱序先SET FOREIGN_KEY_CHECKS0;关掉检查插完再打开但课设不推荐因为掩盖了逻辑错误。4.3 现象事务没回滚库存扣了单子没建原因autocommit 为 True 或异常被吞解决显式 begin 并让异常抛出有人用 Python 的with conn以为自动事务但 pymysql 的 autocommit 默认是 Falsewith只负责关闭连接不负责回滚。更常见的是 try 里捕获异常后只 print 不 raise导致上层以为成功。解决按 3.1 的模板conn.begin()显式开except 里conn.rollback()后raise把异常抛出去让调用方知道失败。4.4 现象GROUP BY 报错 1055原因ONLY_FULL_GROUP_BY 模式解决补全非聚合列或改 SQLMySQL 5.7 以后默认开启 ONLY_FULL_GROUP_BYSELECT category_id, product_name, COUNT(*) FROM product GROUP BY category_id会报错因为 product_name 不在 GROUP BY 里也不是聚合。解决要么把 product_name 加进 GROUP BY要么用ANY_VALUE(product_name)要么改查询逻辑。答辩时如果老师问就说这是 SQL 标准要求保证分组后每列值确定。4.5 现象并发下库存变负原因先查后改中间被其他事务插入解决用 UPDATE 带条件原子扣减有人写SELECT stock_qty FROM product WHERE id1得到 10然后UPDATE product SET stock_qty9 WHERE id1。两个收银员同时查到 10都改成 9实际卖了 2 件但库存只扣 1。解决用 3.1 里的UPDATE ... SET stock_qty stock_qty - qty WHERE stock_qty qty一条语句完成判断和扣减InnoDB 行锁保证原子性。这是数据库并发锁最经典的考点答出来加分。5. 从能跑到能讲把课设变成面试素材的两个技巧5.1 用 EXPLAIN 验证索引把“我加了索引”变成“我验证了索引”很多人加了索引但不知道有没有用。答辩时如果老师问“你怎么证明索引生效”直接跑EXPLAIN SELECT ...看 type 列从 ALL 变成 ref 或 rangekey 列显示你建的索引名。下面是一个对比示例。-- 加索引前 EXPLAIN SELECT * FROM sale_detail WHERE product_id 10; -- 输出 typeALL全表扫描 -- 加索引后 CREATE INDEX idx_detail_product ON sale_detail(product_id); EXPLAIN SELECT * FROM sale_detail WHERE product_id 10; -- 输出 typerefkeyidx_detail_product这个技巧的价值在于它把“我做了”变成“我验证了”面试时讲出来比单纯说“我用了索引”有说服力。注意 EXPLAIN 的 rows 列是预估扫描行数不是实际但足够说明问题。5.2 把事务隔离级别讲成故事别背定义课设里如果涉及并发老师可能问隔离级别。别背“读未提交、读已提交、可重复读、串行化”的定义讲一个场景两个收银员同时卖同一件商品如果隔离级别是读未提交A 扣了库存还没提交B 就能看到扣后的值可能重复扣如果是可重复读MySQL 默认B 在事务里多次读到的库存一致但更新时用当前读配合行锁不会超卖。这样讲老师知道你理解而不是背的。我自己的习惯是每次做完课设把建表脚本、事务代码、三个查询和 EXPLAIN 结果整理成一个 markdown 文件答辩前过一遍。这个习惯后来帮我拿到了第一个实习 offer因为面试官问“你做过什么数据库项目”时我能直接打开文件讲细节而不是空谈“我做过超市管理系统”。希望帮到你。本文还有配套的精品资源点击获取