简介:这份PDF是面向高校数据库课程大作业场景的超市管理系统项目文档,适合正在完成数据库课程设计、需要参考完整项目流程的计算机相关专业学生。内容围绕小型超市线下管理需求展开,覆盖顾客、员工、管理员三类角色的权限划分与功能设计,并完整呈现需求分析、系统设计理念、编程环境搭建等环节。资源包为单个PDF文件,大小约555KB,内容以图文形式组织,包含数据库基本表结构、E-R图、系统框架说明及关键代码段分析,便于对照理解表间关系与模块划分。文档还记录了Visual Studio 2013搭配MySQL与Navicat的开发环境配置,以及基于C语言和MySQL API实现数据库交互的具体写法,如连接、查询、结果集处理等。此外,作者整理了建表时主外键选取、MFC类库使用、代码整合、功能完善与并发访问等实际问题及解决思路,可作为排错与后续优化的参考。目前已有3641人学习,适合作为数据库应用开发的实践案例。
1. 从一份 C 语言 + MySQL 的课设说起:这套超市管理系统到底能跑通什么
如果你正在找一份能直接参考的数据库课程设计,又不想看那种只有 ER 图没有代码的 PPT,这份「数据库大作业--超市管理系统」值得翻一翻。它的技术栈很朴素:Visual Studio 2013 写 C 语言,MySQL 做数据存储,Navicat for MySQL 当可视化工具,通过mysql.h里的 API 完成增删改查。系统把角色拆成顾客、员工、管理员三端,覆盖商品查询、库存管理、员工信息维护、销售记录查看这些典型场景。适合谁?正在做数据库课设、想找一个「能连上真实数据库、有完整表结构、有踩坑记录」的参考项目的人。它不花哨,但把「C 语言怎么跟 MySQL 对话」这件事讲得比较实在,尤其是连接、查询、结果集处理这几步,新手照着能跑起来。
2. 环境搭不起来后面全是白费:VS2013 + MySQL + Navicat 的配置链路
2.1 为什么是 VS2013 配 MySQL 而不是换更省事的方案
这套系统选 VS2013 不是因为它新,而是因为课设场景下它足够稳。C 语言直接调 MySQL 的 C API,需要把 MySQL 安装目录下的include和lib挂到 VS 的项目属性里,这一步在 VS2013 上资料最多、报错最好查。换成 VS2019 或 VS2022 也能做,但字符集、运行库、平台工具集的默认值变了,反而容易在链接阶段翻车。MySQL 这边建议用 5.7 系列,因为mysql.h的 API 在 5.x 到 8.x 之间有变化,8.x 默认的caching_sha2_password认证方式会让老代码连不上,得额外改认证插件。Navicat 的作用是让你不用每次都用命令行看表,建表、改字段、导数据都直观,尤其适合课设阶段频繁调整表结构。
2.2 把 MySQL 的库挂进 VS 项目:四步配置
第一步,确认 MySQL 安装目录下有include和lib两个文件夹。第二步,在 VS 里右键项目 → 属性 → C/C++ → 常规 → 附加包含目录,把include路径加进去。第三步,链接器 → 常规 → 附加库目录,加上lib路径。第四步,链接器 → 输入 → 附加依赖项,填libmysql.lib。这四步做完,#include <mysql.h>才不会报「无法打开源文件」。
#include <mysql.h> #include <stdio.h> int main() { MYSQL *sock = mysql_init(NULL); // 初始化连接句柄 if (sock == NULL) { printf("mysql_init 失败\n"); return 1; } // 连接数据库:主机、用户、密码、库名、端口、socket、客户端标志 if (mysql_real_connect(sock, "localhost", "root", "123456", "supermarket", 0, NULL, 0) == NULL) { printf("连接失败: %s\n", mysql_error(sock)); mysql_close(sock); return 1; } printf("连接成功\n"); mysql_close(sock); return 0; }这段代码是整个系统的入口逻辑。mysql_init分配一个连接句柄,mysql_real_connect的七个参数依次是主机地址、用户名、密码、数据库名、端口(0 表示默认 3306)、socket 文件(NULL 表示默认)、客户端标志(0 表示无特殊标志)。常见翻车点是密码里带特殊字符没转义,或者 MySQL 服务根本没启动。连接失败时mysql_error(sock)会给出具体原因,别只看「连接失败」四个字就到处改代码。
2.3 建库建表:先跑通这条 SQL 再谈界面
在 Navicat 里新建一个名为supermarket的数据库,字符集选gb2312,因为原文代码里用了SET NAMES 'GB2312'来解决乱码。如果你用utf8mb4,那行设置也得跟着改,否则中文商品名会变问号。
CREATE DATABASE IF NOT EXISTS supermarket DEFAULT CHARSET gb2312; USE supermarket; -- 员工表:主键员工编号,姓名非空 CREATE TABLE employee ( emp_id VARCHAR(20) PRIMARY KEY, emp_name VARCHAR(50) NOT NULL, emp_password VARCHAR(50) DEFAULT NULL, emp_phone VARCHAR(20), emp_position VARCHAR(50) ); -- 商品表:主键商品编号,所有属性非空 CREATE TABLE product ( prod_id VARCHAR(20) PRIMARY KEY, prod_name VARCHAR(100) NOT NULL, prod_category VARCHAR(50) NOT NULL, prod_price DECIMAL(10,2) NOT NULL, prod_stock INT NOT NULL ); -- 货架表:编号 + 商品编号联合主键 CREATE TABLE shelf ( shelf_id VARCHAR(20), prod_id VARCHAR(20), shelf_position VARCHAR(50), PRIMARY KEY (shelf_id, prod_id) ); -- 进货表:主键商品编号 CREATE TABLE purchase ( prod_id VARCHAR(20) PRIMARY KEY, purchase_date DATE, purchase_quantity INT, purchase_price DECIMAL(10,2) ); -- 日销售量表 CREATE TABLE daily_sales ( sale_id INT AUTO_INCREMENT PRIMARY KEY, prod_id VARCHAR(20), sale_date DATE, sale_quantity INT, sale_amount DECIMAL(10,2) );建表顺序有讲究:shelf和purchase都引用了product的prod_id,虽然原文没显式写外键约束,但逻辑上应该先建product再建这两张表。daily_sales用自增主键是为了避免同一天同一商品多笔销售时主键冲突。字符集统一用gb2312是为了跟 C 代码里的SET NAMES对齐,如果你打算用utf8mb4,C 代码里那行也要改成SET NAMES 'utf8mb4',两边必须一致,否则就是乱码血泪经验。
3. 三端功能怎么落到代码:顾客查询、员工管理、管理员统计的实现拆解
3.1 顾客端:按名称或类别模糊查询商品
顾客端的核心就一个动作:输入关键词,返回匹配的商品信息。原文说「通过商品的名称或者是商品的类别去找到自己所需要的商品」,落到 SQL 就是LIKE模糊匹配。
void query_product(MYSQL *sock) { char keyword[50]; char sql[256]; printf("请输入商品名称或类别: "); scanf("%s", keyword); // 用 LIKE 做模糊匹配,% 表示任意字符 snprintf(sql, sizeof(sql), "SELECT prod_id, prod_name, prod_category, prod_price, prod_stock " "FROM product WHERE prod_name LIKE '%%%s%%' OR prod_category LIKE '%%%s%%'", keyword, keyword); if (mysql_query(sock, sql) != 0) { printf("查询失败: %s\n", mysql_error(sock)); return; } MYSQL_RES *res = mysql_store_result(sock); // 把结果集拉到本地 if (res == NULL) { printf("获取结果集失败: %s\n", mysql_error(sock)); return; } MYSQL_ROW row; MYSQL_FIELD *fd; int num_fields = mysql_num_fields(res); // 打印列名 while ((fd = mysql_fetch_field(res)) != NULL) { printf("%-15s", fd->name); } printf("\n"); // 逐行打印数据 while ((row = mysql_fetch_row(res)) != NULL) { for (int i = 0; i < num_fields; i++) { printf("%-15s", row[i] ? row[i] : "NULL"); } printf("\n"); } mysql_free_result(res); // 释放结果集内存 }mysql_store_result把整个结果集从服务器拉到客户端内存,适合数据量不大的课设场景。mysql_fetch_field遍历列名,mysql_fetch_row逐行取数据,返回的row[i]是字符串数组,NULL 值要单独判断。snprintf里的%%是为了在格式化字符串里输出一个%,因为%在snprintf里有特殊含义。常见坑是keyword缓冲区太小导致溢出,或者 SQL 语句拼错但没检查mysql_query的返回值,结果后面mysql_store_result返回 NULL 还继续用。
3.2 员工端:登录、改密码、商品增改
员工端的登录逻辑跟管理员类似,但原文提到「账号为员工的名字,密码一开始为空,后来进入界面后可以自己设计」。这意味着员工表里emp_password允许为 NULL,首次登录后强制改密码。
int employee_login(MYSQL *sock, char *name, char *password) { char sql[256]; snprintf(sql, sizeof(sql), "SELECT emp_id, emp_password FROM employee WHERE emp_name='%s'", name); if (mysql_query(sock, sql) != 0) { printf("查询失败: %s\n", mysql_error(sock)); return -1; } MYSQL_RES *res = mysql_store_result(sock); MYSQL_ROW row = mysql_fetch_row(res); if (row == NULL) { printf("员工不存在\n"); mysql_free_result(res); return 0; } // 密码为空表示首次登录,直接放行并提示设置新密码 if (row[1] == NULL) { printf("首次登录,请设置新密码: "); char new_pwd[50]; scanf("%s", new_pwd); snprintf(sql, sizeof(sql), "UPDATE employee SET emp_password='%s' WHERE emp_name='%s'", new_pwd, name); mysql_query(sock, sql); mysql_free_result(res); return 1; } // 密码非空则比对 if (strcmp(row[1], password) == 0) { mysql_free_result(res); return 1; } else { printf("密码错误\n"); mysql_free_result(res); return 0; } }这里有个细节:mysql_fetch_row返回的row[1]如果是 NULL,说明数据库里该字段为空,对应「密码一开始为空」。改密码用UPDATE语句,注意WHERE条件要精确到员工名,否则会误改所有人。商品管理的增改逻辑类似,INSERT INTO product ...和UPDATE product SET ... WHERE prod_id=...,参数从界面输入获取。
3.3 管理员端:员工信息查询修改与销售统计
管理员端比员工端多两个功能:按编号或姓名查员工、按日期或商品编号查销售。查询逻辑跟顾客端类似,区别在于管理员可以修改查到的员工信息。
void admin_query_employee(MYSQL *sock) { char condition[50]; char sql[256]; printf("输入员工编号或姓名: "); scanf("%s", condition); snprintf(sql, sizeof(sql), "SELECT emp_id, emp_name, emp_phone, emp_position " "FROM employee WHERE emp_id='%s' OR emp_name='%s'", condition, condition); if (mysql_query(sock, sql) != 0) { printf("查询失败: %s\n", mysql_error(sock)); return; } MYSQL_RES *res = mysql_store_result(sock); MYSQL_ROW row; while ((row = mysql_fetch_row(res)) != NULL) { printf("编号: %s, 姓名: %s, 电话: %s, 职位: %s\n", row[0], row[1], row[2] ? row[2] : "无", row[3] ? row[3] : "无"); } mysql_free_result(res); }销售统计按日期查的 SQL 是SELECT prod_id, SUM(sale_quantity), SUM(sale_amount) FROM daily_sales WHERE sale_date='%s' GROUP BY prod_id,按商品编号查则是把WHERE条件换成prod_id='%s'。GROUP BY配合聚合函数是这类统计的标配,注意SUM的字段类型要是数值型,DECIMAL和INT都行,但VARCHAR会返回 0。
4. 避坑与排查:乱码、连接失败、结果集为空、并发缺失的现场记录
4.1 中文商品名显示成问号
现象:Navicat 里看数据正常,C 程序打印出来是???。原因:数据库字符集、连接字符集、终端编码三者不一致。原文用mysql_query(sock, "SET NAMES 'GB2312'")解决,但如果你建库时用了utf8mb4,这行反而会引入乱码。解决:建库、建表、连接三处字符集统一。用gb2312就三处都用gb2312,用utf8mb4就三处都用utf8mb4,别混搭。
4.2mysql_real_connect返回 NULL 但错误信息是「Can't connect to MySQL server」
现象:代码没编译错误,运行就卡在连接失败。原因:MySQL 服务没启动,或者主机地址写错,或者端口被占用。解决:先在命令行跑mysql -u root -p确认服务活着,再检查代码里的主机名是localhost还是127.0.0.1,两者在 socket 连接和 TCP 连接上有区别。如果 MySQL 是 8.x,还要确认用户认证插件是不是mysql_native_password,不是的话老代码连不上。
4.3mysql_store_result返回 NULL 但mysql_query成功了
现象:查询语句执行没报错,取结果集时返回 NULL。原因:执行的 SQL 不是SELECT,比如INSERT、UPDATE、DELETE之后调mysql_store_result就会返回 NULL,因为这些语句没有结果集。解决:只在SELECT之后调mysql_store_result,增删改之后用mysql_affected_rows看影响行数。
4.4 多用户同时操作时数据错乱
现象:两个人同时改同一个商品库存,后改的覆盖先改的。原因:原文明确说「还没实现并发访问」,系统没有加锁机制。解决:课设阶段可以在UPDATE语句里用WHERE prod_stock=旧值做乐观锁,或者用SELECT ... FOR UPDATE做悲观锁。但更实际的做法是承认这个边界,在文档里写清楚「当前版本不支持并发,适合单用户演示」。
4.5 MFC 里 CString 和 MySQL API 的字符串类型不匹配
现象:用 MFC 做界面时,CString直接传给mysql_query报类型错误。原因:mysql_query要的是const char *,CString是宽字符或 MBCS 封装类,不能隐式转换。解决:用CStringA或者CT2A宏转成const char *,转换前确认编码跟数据库字符集一致,否则转完还是乱码。
5. 把课设推到能演示的程度:从单表查询到事务与索引的补强
5.1 给高频查询字段加索引
顾客端按prod_name和prod_category模糊查询,员工端按emp_name登录,管理员按sale_date统计。这些字段在数据量上来之后全表扫描会变慢。加索引的 SQL 很简单:
CREATE INDEX idx_prod_name ON product(prod_name); CREATE INDEX idx_prod_category ON product(prod_category); CREATE INDEX idx_emp_name ON employee(emp_name); CREATE INDEX idx_sale_date ON daily_sales(sale_date);注意LIKE '%关键词%'这种前置通配符的查询用不上索引,只有LIKE '关键词%'才能命中。课设数据量小感觉不出来,但养成加索引的习惯没坏处。
5.2 用事务保证进货和库存同步
原文的进货表purchase和商品表product是分开的,进货时应该同时更新purchase和product.prod_stock。如果只更新一个表,另一个失败了,数据就不一致。用事务包起来:
mysql_query(sock, "START TRANSACTION"); // 插入进货记录 snprintf(sql, sizeof(sql), "INSERT INTO purchase(prod_id, purchase_date, purchase_quantity, purchase_price) " "VALUES('%s', '%s', %d, %.2f)", prod_id, date, qty, price); if (mysql_query(sock, sql) != 0) { mysql_query(sock, "ROLLBACK"); printf("进货记录插入失败,已回滚\n"); return; } // 更新库存 snprintf(sql, sizeof(sql), "UPDATE product SET prod_stock = prod_stock + %d WHERE prod_id='%s'", qty, prod_id); if (mysql_query(sock, sql) != 0) { mysql_query(sock, "ROLLBACK"); printf("库存更新失败,已回滚\n"); return; } mysql_query(sock, "COMMIT"); printf("进货完成\n");START TRANSACTION开启事务,任何一步失败就ROLLBACK,全部成功才COMMIT。MySQL 默认的 InnoDB 引擎支持事务,MyISAM 不支持,建表时确认引擎是 InnoDB。
5.3 验证系统是否真的跑通:三条检查线
第一条,用 Navicat 直接查表,确认数据写进去了。第二条,在 C 程序里跑一遍完整流程:顾客查商品 → 员工登录改库存 → 管理员查销售,看每一步的输出跟数据库里的值是否一致。第三条,故意输错密码、查不存在的商品、插入重复主键,看错误提示是否清晰。这三条走完,基本能判断系统是「能演示」还是「只能编译」。
5.4 一个我踩过的坑:字符集设置的位置
我一开始把SET NAMES 'GB2312'放在mysql_real_connect之前,结果连接都建不起来,那行自然也没执行。正确顺序是先mysql_real_connect成功,再mysql_query(sock, "SET NAMES 'GB2312'")。从那以后我每次连完数据库第一件事就是设字符集,再干别的。希望这份拆解能帮到你,少走点弯路。
本文还有配套的精品资源,点击获取