
简介这是一份面向数据库课程设计的超市管理系统完整项目文档适合计算机相关专业学生、需要完成数据库大作业的开发者以及想了解 C 语言结合 MySQL 做管理系统的初学者。内容涵盖小型超市管理系统的设计理念、需求分析、开发环境、数据库表与 E-R 图、界面框架、核心代码和问题排查能帮助读者从零理解一个数据库应用项目的完整落地过程。资源为单个 PDF 文档压缩包仅 1 个文件大小 555KB。文档围绕顾客、员工、管理员三类权限展开顾客可查询商品信息员工负责库存与销售记录管理员可查看销售数据并管理员工信息同时梳理了员工表、商品表、货架表、进货表、日销售量表等基本表设计也给出 E-R 图中员工整理商品、员工记录销售、货架摆放商品的一对多关系。代码部分列出 mysql_real_connect、mysql_query、mysql_store_result、mysql_fetch_row 等关键 API 的用途并针对主键外键选择、MFC 使用、代码整合及并发访问等问题给出排错思路已有 3641 人浏览学习对正在做数据库课设或想参考超市管理系统设计方案的学习者具有实用价值。1. 为什么超市管理系统是数据库课程设计的样板题这个项目是从学校小卖部场景长出来的。写这套系统的人用了 Visual Studio 2013、MySQL 和 Navicat后台是 C 语言界面是 MFC最后交上去的是一份带完整 E-R 图和建表脚本的课程设计报告。超市管理系统之所以常被选作数据库课程设计和数据库原理的演练对象在于它实体够少、关系够典型员工、商品、货架、进货、销售五张表就能把主外键、一对多、查询统计全部覆盖同时又不像订单系统那样有复杂的多级关联。任何想在一周内完成能跑通的数据库增删改查 三个角色界面的人都值得把它拆一遍。下面从数据建模开始逐步把整个系统的骨架、代码和坑位过一遍。2. 从业务到表结构超市管理系统的数据建模与 E-R 图2.1 三端用户的需求如何映射到数据模型原报告把系统分成顾客、员工、管理员三个入口这是很标准的基于角色的访问控制思路。顾客只关心商品名称、类别和价格员工要修改库存和货架位置还要记录每笔销售管理员则要员工信息和销售汇总。这三种需求落到数据库上就是谁对哪些表有读权限、写权限的问题。数据模型首先要回答的是实体有哪些。原项目最终确认了员工、商品、货架、进货、日销售量五张基本表。其中员工与商品是整理关系一个员工整理多种商品所以是一对多员工与销售是记录关系一个员工记录多条销售记录货架与商品是摆放关系一个货架摆放多种商品也是一对多。这三个一对多关系分别对应商品表里的员工外键、日销售量表里的员工外键以及货架表与商品表之间通过编号关联的组合键。这里有一个容易犯的设计错误一开始很容易把货架设计成商品表的一个普通字段里面直接写3号货架。但货架和商品是独立业务实体因为哪个商品放在哪个货架的哪个位置属于多值信息。如果合并成字段后续查某货架上有什么商品就只能写 LIKE 模糊查询没法走索引。这一点在最初画 E-R 图时就该定下来否则中途改表结构会牵扯到所有 C 语言代码。2.2 五张核心表的设计与主外键取舍按原报告里的实体描述我整理出一套可以直接在 MySQL 5.x 上运行的建表脚本。员工表以员工编号为主键姓名非空商品表以商品编号为主键其余属性均非空货架表以编号和商品编号共同作为主键进货表以商品编号为主键日销售量表单独给一个自增主键便于记录流水。员工表employee字段类型约束说明emp_idINTPRIMARY KEY员工编号登录账号emp_nameVARCHAR(20)NOT NULL员工姓名也是初始登录名emp_pwdVARCHAR(20)DEFAULT NULL初始为空员工登录后可自行修改positionVARCHAR(20)DEFAULT NULL岗位管理员/收银员/理货员phoneVARCHAR(20)DEFAULT NULL联系方式商品表product字段类型约束说明prod_idINTPRIMARY KEY商品编号prod_nameVARCHAR(50)NOT NULL商品名称顾客端搜索入口categoryVARCHAR(30)NOT NULL商品类别priceDECIMAL(10,2)NOT NULL售价stockINTNOT NULL库存数量emp_idINTFOREIGN KEY负责整理该商品的员工日销售量表daily_sales用于记录每次销售原文叫日销售量表但为了支持按日期和商品编号查询我把它设计为带自增主键的流水表而不是每天只存一条汇总。这里特别要说的主键选择货架表用复合主键(shelf_id, prod_id)能保证一个货架上的商品记录唯一但真实业务里一个商品只放在一个货架更常见真正应该唯一的是prod_id。如果沿用原设计同一个商品可以被放进多个货架后续盘点会重复统计。我的建议是如果只是交课程设计复合主键可以如果想让模型更贴近现实就改成shelf_id主键 prod_id外键再对prod_id加唯一索引并在文档里说明为什么这样改。2.3 建表顺序与外键约束的坑原报告明确提到建表时对主码和外码的选取问题由于初期没有完全考虑清楚表之间的关系而出现错误。这个坑几乎每个做数据库课程设计的人都会踩。最常见的是先建了带外键的商品表再建员工表MySQL 直接报Cannot add foreign key constraint。正确顺序是先建被引用的表再建引用表。初始化时如果外键报错优先检查三件事外键指向的列在主表里是不是主键或唯一索引两张表的字段类型是否完全一致比如一个是 INT 一个是 VARCHAR存储引擎是不是 InnoDB因为 MyISAM 不强制外键。下面的脚本就是按正确顺序写的CREATE DATABASE IF NOT EXISTS supermarket DEFAULT CHARSET utf8mb4; USE supermarket; CREATE TABLE employee ( emp_id INT PRIMARY KEY, emp_name VARCHAR(20) NOT NULL, emp_pwd VARCHAR(20) DEFAULT NULL, position VARCHAR(20) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL ) ENGINEInnoDB; CREATE TABLE product ( prod_id INT PRIMARY KEY, prod_name VARCHAR(50) NOT NULL, category VARCHAR(30) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL, emp_id INT, CONSTRAINT fk_product_emp FOREIGN KEY (emp_id) REFERENCES employee(emp_id) ) ENGINEInnoDB; CREATE TABLE shelf ( shelf_id INT NOT NULL, prod_id INT NOT NULL, location VARCHAR(50), PRIMARY KEY (shelf_id, prod_id), CONSTRAINT fk_shelf_prod FOREIGN KEY (prod_id) REFERENCES product(prod_id) ) ENGINEInnoDB; CREATE TABLE purchase ( prod_id INT PRIMARY KEY, supplier VARCHAR(50), quantity INT, purchase_date DATE, CONSTRAINT fk_purchase_prod FOREIGN KEY (prod_id) REFERENCES product(prod_id) ) ENGINEInnoDB; CREATE TABLE daily_sales ( sale_id INT AUTO_INCREMENT PRIMARY KEY, emp_id INT NOT NULL, prod_id INT NOT NULL, quantity INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, sale_date DATE NOT NULL, CONSTRAINT fk_sales_emp FOREIGN KEY (emp_id) REFERENCES employee(emp_id), CONSTRAINT fk_sales_prod FOREIGN KEY (prod_id) REFERENCES product(prod_id) ) ENGINEInnoDB;脚本里刻意写了ENGINEInnoDB。原项目在 VS2013 里用 C 语言连接 MySQL如果建表时漏掉引擎参数默认可能落到 MyISAM外键约束不生效删除员工时也体验不到级联或限制效果。daily_sales使用AUTO_INCREMENT是为了让每次销售有独立流水号方便管理员按日期区间统计。也就是表结构设计不只是为了存数据还要为后续的增删改查和统计查询提供顺手的索引。3. VS2013 环境配置与 C 语言操作 MySQL 的核心流程3.1 让 VS2013 找到 mysql.hC 语言操作 MySQL 的前提是让编译器能找到 MySQL 提供的头文件和库文件。典型做法是在 VS2013 项目属性里做两处配置在配置属性 - C/C - 常规 - 附加包含目录里加入 MySQL 安装目录下的 include 文件夹在链接器 - 常规 - 附加库目录里加入 lib 文件夹然后在链接器 - 输入 - 附加依赖项里写入libmysql.lib。版本不同路径会不一样不要照抄应该在 MySQL 安装目录下确认 include 和 lib 确实存在。很多初学者卡在这一步是因为 VS2013 默认按 32 位编译而 MySQL 装的是 64 位链接时提示找不到 libmysql.lib。解决办法是把项目平台切成 x64或者把 MySQL 安装目录下的libmysql.dll复制到 exe 同目录。libmysql.dll是动态链接库调试时忘记拷贝程序启动就会提示缺失 dll。3.2 连接数据库与执行增删改查的标准代码原报告列出了最核心的几个 APImysql_real_connect负责连接mysql_query负责执行 SQLmysql_store_result把查询结果拉到客户端mysql_fetch_row逐行读取。把它们串起来就是完整的连接 - 查询 - 取数 - 释放流程。下面这段代码是顾客端查询商品信息的雏形#include mysql.h #include stdio.h #include string.h int query_product(const char *name) { MYSQL *sock; MYSQL_RES *res; MYSQL_ROW row; char sql[512]; sock mysql_init(NULL); if (sock NULL) { return -1; } if (mysql_real_connect(sock, localhost, root, 123456, supermarket, 3306, NULL, 0) NULL) { fprintf(stderr, connect error: %s\n, mysql_error(sock)); return -1; } // 统一客户端字符集原项目用 GB2312 mysql_query(sock, SET NAMES GB2312); sprintf(sql, SELECT prod_id, prod_name, price, stock FROM product WHERE prod_name %s, name); if (mysql_query(sock, sql) ! 0) { fprintf(stderr, query error: %s\n, mysql_error(sock)); mysql_close(sock); return -1; } res mysql_store_result(sock); if (res NULL) { fprintf(stderr, store result error: %s\n, mysql_error(sock)); mysql_close(sock); return -1; } while ((row mysql_fetch_row(res)) ! NULL) { printf(%s | %s | %s | %s\n, row[0], row[1], row[2], row[3]); } mysql_free_result(res); mysql_close(sock); return 0; }这段代码的关键参数mysql_real_connect的第一个参数是mysql_init初始化好的句柄第二到第五个参数分别是主机名、用户名、密码、数据库名第六个参数3306是端口最后两个参数分别对应 unix socket 和客户端标志都填 0 和 NULL 即可。mysql_query执行的是原生 SQL 字符串返回 0 表示成功。mysql_store_result是一次性把所有结果读入内存适合小数据量如果换成海量数据应该用mysql_use_result配合mysql_fetch_row流式读取。mysql_free_result必须调用否则长连接服务端会内存泄漏。3.3 中文乱码的根源客户端字符集与 CString 的转换原项目专门加了mysql_query(sock, SET NAMES GB2312)解决乱码这是那个年代的常见写法。乱码的本质是数据库表字符集、客户端连接字符集、程序内部字符串编码三者不一致。在 VS2013 的 MFC 工程里默认字符集可能是 Unicode而 C 语言char*是 ANSI 窄字节。从mysql_fetch_row拿到的字节串直接赋给CStringMFC 会按 Unicode 理解显示自然乱码。常见做法是拿到row[i]后用MultiByteToWideChar显式转换或者把 VS2013 项目字符集改成使用多字节字符集让CString直接以 ANSI 方式处理。但改了工程字符集后所有依赖TCHAR宏的代码行为都会变化。另一个隐患是 SQL 字符串里的中文值sprintf拼出来的 SQL 本身是 GB2312 编码而 MySQL 连接设置是 utf8查询条件里的中文就匹配不上。所以原项目保持 GB2312 设置和 MFC 默认代码页是自洽的。3.4 把增删改查封装成通用函数mysql_query不仅能执行 SELECT也能执行 INSERT、UPDATE、DELETE。员工端的商品管理、管理员端的员工信息修改底层都是 UPDATE。常见封装是写一个execute_update(const char *sql)函数int execute_update(MYSQL *sock, const char *sql) { if (mysql_query(sock, sql) ! 0) { fprintf(stderr, update error: %s\nsql: %s\n, mysql_error(sock), sql); return -1; } /* 对于 INSERT/UPDATE/DELETE查看受影响行数 */ if (mysql_affected_rows(sock) 0) { printf(no row affected\n); } return 0; }mysql_affected_rows返回上一条语句影响的行数。用这个函数可以处理管理员更新员工信息但编号不存在的情况页面上给出未找到员工的提示而不是什么都不做。核心思路是增删改查共用同一个执行入口只是传入的 SQL 不同SELECT 结果多交给mysql_store_result非 SELECT 语句直接看影响行数即可。4. 三端功能实现顾客搜索、员工管理、管理员登录的业务逻辑4.1 顾客端按商品名称或类别检索顾客端界面在原项目里是最简单的一个文本框加一个结果列表。顾客输入商品名称或类别程序拼接 SQL 查询。为了让搜索接近真实超市可以支持模糊匹配SELECT prod_id, prod_name, price, stock FROM product WHERE prod_name LIKE CONCAT(%, 可乐, %) OR category LIKE CONCAT(%, 饮料, %);这里用CONCAT而不是在 C 代码里直接拼%是为了避免用户输入的单引号破坏 SQL 结构。顾客端只读不需要事务。查询结果需要按价格排序时加ORDER BY price ASC排序在数据库端做不要在 C 代码里自己写冒泡排序。4.2 员工端个人信息、商品管理与销售记录员工端比顾客端多了写权限。员工登录后有三个子模块个人信息修改是 UPDATE employee商品信息管理复用 3.4 的封装函数对 product 做增删改销售记录是 INSERT 进 daily_sales。这里有个容易被忽略的逻辑销售发生时必须同步扣减 product 表的 stock。原报告没有明确提但如果不做库存联动商品信息管理的库存就永远对不上账。库存扣减通常写成两条语句先查当前库存再 UPDATE 成新库存。这两条语句之间如果没有事务两个收银员同时卖同一件商品就会超卖。第 5 章会专门说并发这里先按课程设计的简单实现来UPDATE product SET stock stock - %d WHERE prod_id %d把扣减下推给数据库避免读出来减完再写回去的中间态。4.3 管理员端登录校验、员工信息增改查与销售统计管理员界面进入前必须登录。原项目输入用户名和密码程序去 employee 表里比对并检查 position。朴素实现就是sprintf拼 SQL然后看有没有返回行char sql[256]; sprintf(sql, SELECT emp_id, emp_name FROM employee WHERE emp_name%s AND emp_pwd%s AND position管理员, name, pwd); mysql_query(sock, sql); res mysql_store_result(sock); if (mysql_num_rows(res) 0) { /* 登录成功进入管理员界面 */ } else { printf(用户名或密码错误\n); } mysql_free_result(res);这里有两个问题。第一密码明文存储课程设计可以接受但想显得专业一点至少用 MySQL 的SHA2做哈希。第二name和pwd直接拼接进 SQL输入 OR 11就能绕过登录。这种注入漏洞原项目没有提及但被评审问到的概率很高第 5.2 节会给出修复办法。管理员端的销售统计是报表功能。按日期查询销售情况时SQL 这样写SELECT DATE_FORMAT(sale_date, %Y-%m-%d) AS d, SUM(total_amount) AS total FROM daily_sales WHERE sale_date BETWEEN 2024-01-01 AND 2024-01-31 GROUP BY d;DATE_FORMAT把日期格式化成天GROUP BY d按天汇总。在 C 语言中显示结果时mysql_store_result拿到的行可以直接打印。这种聚合查询是数据库优化里的典型场景索引应该建在sale_date上否则数据量大了全表扫描会让界面卡顿。4.4 MFC 界面集成中的 CString 与 char* 转换原报告提到 CString 类型调试出错。MFC 的 CString 在 VS2013 里默认是宽字符而 MySQL API 返回的char*是窄字符。直接赋值CString 会把每个字节当作宽字符的高 8 位显示成乱码。解决方法是显式转换CString strName; const char *name row[1]; #ifdef _UNICODE int len MultiByteToWideChar(CP_ACP, 0, name, -1, NULL, 0); MultiByteToWideChar(CP_ACP, 0, name, -1, strName.GetBuffer(len), len); strName.ReleaseBuffer(); #else strName name; #endifCP_ACP表示当前 ANSI 代码页。如果 MySQL 连接设置是SET NAMES GB2312那么row[1]是 GBK 编码CP_ACP正好对应中文 Windows 的 GBK。另一种省事的方式是把项目字符集切到多字节但那时 MFC 控件方法签名从LPCTSTR变成char*同样需要适应。无论哪种方案原则都是保持程序编码 连接字符集 表字符集三者在一条线上。4.5 功能拆分带来的权限控制问题三端界面在同一台机器上运行数据库连接使用同一个 root 账号。这意味着任何一端都能执行删除员工、清空商品表的操作。如果想让系统更像生产环境可以分别建三个 MySQL 账号顾客端只授 SELECT员工端授 product 和 daily_sales 的 SELECT/UPDATE/INSERT管理员端授全部。数据库权限本身就是一层防线比在程序里判断角色更可靠。5. 从课程设计到可演示SQL 注入防护、并发控制与验证技巧5.1 并发访问未实现的隐患与补救方案原报告最后一条写的是还没实现并发访问。对于单机课程设计这不算扣分项但要清楚并发指的是什么多个顾客或收银员同时操作时数据库可能产生脏读、超卖、死锁。以扣库存为例两个会话同时执行UPDATE product SET stock stock - 1MySQL 的行锁会让后一个会话阻塞不会超卖。问题出在先 SELECT 再计算再 UPDATE的模式两个会话读到同一个库存值都写回减 1 后的结果库存少减了一次。补救方案分两档。简单做法是把查库存扣减写销售包成一个事务START TRANSACTION; SELECT stock FROM product WHERE prod_id 1 FOR UPDATE; UPDATE product SET stock stock - 1 WHERE prod_id 1; INSERT INTO daily_sales (emp_id, prod_id, quantity, total_amount, sale_date) VALUES (1, 1, 1, 2.50, CURDATE()); COMMIT;SELECT ... FOR UPDATE会对命中行加排他锁直到 COMMIT 才释放另一个会话再执行同样语句会阻塞从而避免超卖。更进阶的做法是引入 MySQL 连接池让多线程窗口各自持有独立连接而不是共用一个MYSQL *sock。这一步在原项目里改动较大答辩时可以只讲设计思路。5.2 SQL 注入防护从拼接 SQL 改为转义输入4.3 里的登录校验是典型注入点。修复方式有两种最推荐用预处理语句MySQL C API 提供mysql_stmt_prepare系列轻量级方案是转义用户输入char escaped_name[128]; mysql_real_escape_string(sock, escaped_name, name, strlen(name)); sprintf(sql, SELECT emp_id FROM employee WHERE emp_name%s AND emp_pwd%s, escaped_name, escaped_pwd);mysql_real_escape_string会把单引号、反斜杠等字符转义使输入失去闭合 SQL 的能力。注意这个函数依赖当前连接句柄因为它要参考 MySQL 的字符集设置。转义后拼 SQL 能堵住绝大多数注入但对 LIKE 通配符%和_还有残留风险。如果顾客端支持模糊搜索应当再配合ESCAPE关键字处理用户输入的通配符。5.3 答辩前快速验证数据库状态的三个命令交作业前用 Navicat 或命令行跑三个检查。第一确认外键约束真的建上了执行SHOW CREATE TABLE product;看输出里有没有FOREIGN KEY。第二验证销售统计 SQL 能返回预期行数SELECT COUNT(*) FROM daily_sales;。第三检查有没有残留连接导致程序第二次启动报Too many connections执行SHOW PROCESSLIST;。如果有大量 Sleep 连接说明程序退出时漏掉了mysql_close。最后补充一个连老师都认的细节每次mysql_query之后检查返回值每次mysql_store_result之后检查是否为 NULL。原项目所有查询共用一个连接顺序执行如果某条 SQL 出错后没有释放结果集后续查询会直接失败。养成这个习惯演示时就不会出现点按钮就崩溃。用mysqladmin status观察Threads_connected也能直观证明系统在多窗口并发打开时不再只是单线程玩具。本文还有配套的精品资源点击获取