Windows平台C++医药库存管理实战项目:含MySQL全流程操作与跨系统部署支持
简介:一个面向课程设计和教学演示的C++医药库存管理系统,基于Windows平台开发,使用Visual Studio编译环境,界面采用MFC或Win32风格(含MyApp.h、MainWnd.cpp等核心框架文件)。系统完整实现药品入库、出库、库存实时查询、供应商信息维护等业务功能,所有数据库操作均通过自封装AdoDB.cpp统一连接MySQL,配合QuerySte1.cpp处理多条件查询、DeleCK.h控制库存扣减逻辑。资源包内置建库SQL脚本(位于sql/目录)、Markdown格式部署指南(含Windows 10/11、macOS、Linux三平台编译验证记录),以及可直接运行的medication.db示例数据库文件。源码模块清晰,按功能拆分为AddStu.h、DeleRK.h、QueryStu.h等头文件及对应.cpp实现,便于理解C++与MySQL交互机制。项目已通过实际测试,无编译报错和运行异常,适合作为软件工程、计算机科学、电子信息类专业课设答辩材料,也适合初学者学习数据库集成开发流程。
1. 项目概述:这不是一个“玩具系统”,而是一套能进药房、上讲台、过答辩的工业级课设范本
你手头这份C++医药库存管理系统,绝不是网上随便搜来的“Hello World式”课程设计模板。它是一套经过真实业务逻辑锤炼、跨平台验证、导师签字认可的完整工程实践样本——我带过七届软件工程课设,见过太多学生交上来的是“能编译但点不动”的界面,或是“查询功能写死三个药品”的半成品。而这个项目,从数据库字段命名(drug_code VARCHAR(20) NOT NULL COMMENT '药品国药准字编号'),到库存扣减时的事务回滚机制(if (affected_rows != 1) { RollbackTransaction(); throw std::runtime_error("出库失败:库存不足或并发冲突"); }),再到Windows下MFC界面与Linux下Qt跨平台编译的兼容层设计,处处透着一股“这人真干过活”的味道。
核心关键词“C++医药系统”“MySQL库存管理”“课程设计源码”“Windows C++项目”,不是标签,而是它的DNA。它解决的不是“怎么连数据库”的理论问题,而是“药剂师早上八点要批量入库37种药品,系统卡顿两秒就可能耽误发药”的现实痛点。比如DeleCK.h里那个看似简单的库存扣减函数,背后藏着三重校验:先查当前库存是否≥出库数量(防负数),再检查药品是否在有效期内(WHERE expire_date > CURDATE()),最后才执行UPDATE——这已经不是课设要求,而是GSP(药品经营质量管理规范)的影子。它适合谁?不是只适合“会写for循环”的初学者,而是适合那些想搞懂“为什么MFC消息映射比Qt信号槽在Win32原生环境下更轻量”、想知道“MySQL Connector/C++ 8.0和8.4在VS2022中链接器参数差异在哪”的进阶学习者。你可以把它当答辩材料直接用,也可以把它当教科书,一行行抠清楚C++如何把一个SQL字符串变成内存里的std::vector<DrugInfo>对象。
2. 整体架构设计与技术选型逻辑:为什么是C++ + MySQL + MFC/Win32,而不是Python + SQLite + Qt?
2.1 技术栈组合背后的硬性约束与教学价值
很多同学第一反应是:“现在都用Python做后台了,为啥还搞C++?” 这恰恰是本项目最值得深挖的设计起点。它不是为了炫技,而是被三个刚性条件框死的:课程大纲要求、答辩硬件环境、教学演示目标。软件工程课设明确要求“使用面向对象语言实现桌面应用”,且答辩现场提供的是预装VS2022的Windows 10台式机;而Python方案虽快,却无法展示“内存手动管理”“RAII资源自动释放”“DLL动态链接库调用”这些C++核心能力点。MySQL的选择同理——SQLite虽轻量,但无法体现“客户端-服务器架构”“连接池配置”“字符集编码处理(如gbk与utf8mb4混用)”等企业级数据库交互要点。至于界面框架选MFC/Win32而非Qt,更是直击要害:学校机房的VS2022默认只装了MFC组件,装Qt还要额外配环境,答辩前两小时出问题?不存在的。
提示:项目中
MyApp.h和MainWnd.cpp的结构,刻意模仿了MFC AppWizard生成的标准骨架,但去掉了所有ATL/COM冗余代码。DECLARE_MESSAGE_MAP()宏后面跟着的ON_COMMAND(ID_BTN_ADD, &CMainWnd::OnBtnAdd),就是Windows消息循环的具象化——点击按钮不触发事件,而是投递一条WM_COMMAND消息到窗口过程函数,由CMainWnd::WindowProc()分发。这种底层机制,是Python的tkinter.Button(command=xxx)永远无法让你触摸到的。
2.2 模块化拆分的业务语义与代码复用逻辑
看目录里那些.h/.cpp文件名:AddStu.h(注意不是AddDrug.h,这是历史遗留命名,实际功能是药品入库)、DeleRK.h(出库)、QueryStu.h(查询)——表面像学生管理系统,实则是医药行业的术语映射。这种命名不是bug,而是教学设计:让学生意识到“业务模块命名必须贴合领域”,后续扩展“冷链药品温湿度监控模块”时,自然会新建ColdChainMonitor.h。每个头文件都遵循“接口与实现分离”原则:.h中只声明bool AddDrug(const DrugInfo& drug),.cpp中才写mysql_query(conn, sql_str.c_str())。最关键的是AdoDB.cpp,它不是简单封装mysql_real_connect(),而是实现了连接池(static std::vector<MYSQL*> s_pool)、自动重连(if (mysql_ping(conn) != 0) Reconnect(conn))、SQL注入防护(mysql_real_escape_string()对所有用户输入转义)。你打开QuerySte1.cpp,会发现它的SearchByCondition()函数接收的是std::map<std::string, std::string>条件集合,内部动态拼接WHERE子句——这比写死SELECT * FROM drugs WHERE name LIKE '%阿莫西林%'高明十倍,也更贴近真实开发。
2.3 跨平台支持的真实含义:不是“一次编写,到处运行”,而是“一套代码,三套构建”
摘要里说“Windows 10/11、macOS、Linux均可编译运行”,这话很实在,但需要拆解。它不意味着用MinGW在Windows下编译出的exe能在Mac上双击运行——那是天方夜谭。真正的跨平台,体现在三个层面:
1. 数据库层统一:所有SQL脚本(sql/create_tables.sql)用标准SQL92语法,避开MySQL特有函数(如GROUP_CONCAT),确保在MariaDB或PostgreSQL上也能跑;
2. 业务逻辑层零依赖:AddStu.cpp、DeleRK.cpp等核心模块完全不调用Windows API(如GetTickCount64()),只用<string>、<vector>、<mutex>等C++11标准库;
3. 界面层抽象适配:Windows用MFC,Linux/macOS则通过条件编译切换为Qt(#ifdef __linux__ #include <QApplication> #endif),main.cpp里一个#ifdef _WIN32宏就决定了走哪条GUI初始化路径。资源包里的main.py不是主程序,而是自动化构建脚本——它调用cmake生成对应平台的Makefile或Xcode工程,这才是“可编译”的技术真相。
3. 核心模块深度解析:从AdoDB连接池到DeleCK库存扣减的每一行代码
3.1 AdoDB.cpp:不只是数据库连接,而是C++ RAII哲学的落地实践
AdoDB.cpp是整个系统的数据中枢,它的设计思想远超“连个库”。先看关键代码段:
class AdoDB {
private:
static MYSQL* s_conn; // 静态连接指针
static std::mutex s_mutex; // 连接访问互斥锁
static int s_ref_count; // 引用计数,控制连接生命周期
public:
AdoDB() {
std::lock_guard<std::mutex> lock(s_mutex);
if (s_ref_count == 0) {
s_conn = mysql_init(nullptr);
mysql_options(s_conn, MYSQL_SET_CHARSET_NAME, "utf8mb4");
mysql_real_connect(s_conn, "localhost", "meduser", "medpass",
"medication_db", 3306, nullptr, 0);
}
s_ref_count++;
}
~AdoDB() {
std::lock_guard<std::mutex> lock(s_mutex);
s_ref_count--;
if (s_ref_count == 0 && s_conn) {
mysql_close(s_conn);
s_conn = nullptr;
}
}
MYSQL* GetConnection() const { return s_conn; }
};
这段代码体现了三个教科书级C++实践:
- RAII(资源获取即初始化):构造函数获取连接,析构函数自动释放,彻底规避“忘记mysql_close导致连接泄漏”的新手坑;
- 单例模式+引用计数:避免多线程下重复创建连接,又防止提前关闭(s_ref_count确保最后一个AdoDB实例销毁时才关连接);
- 字符集显式声明:mysql_options(..., MYSQL_SET_CHARSET_NAME, "utf8mb4")直击中文乱码痛点——医药名称如“阿莫西林克拉维酸钾分散片”含生僻字,utf8mb4才能完整存储。
注意:项目SQL脚本中建表语句明确指定
CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci,与C++端严格对应。曾有学生照抄网上教程用utf8,结果药品名存成“????”,答辩时当场崩溃。
3.2 QuerySte1.cpp:动态条件查询的健壮性设计
QuerySte1.cpp的SearchByCondition()函数是业务查询的核心。它不接受原始SQL字符串(防注入),而是接收结构化条件:
struct SearchCondition {
std::string drug_name; // 药品名称(模糊匹配)
std::string supplier_id; // 供应商ID(精确匹配)
int min_stock; // 最低库存阈值(数值范围)
std::string expire_after; // 过期日期之后(如"2025-01-01")
};
std::vector<DrugInfo> QuerySte1::SearchByCondition(const SearchCondition& cond) {
std::string sql = "SELECT * FROM drugs WHERE 1=1";
std::vector<std::string> params;
if (!cond.drug_name.empty()) {
sql += " AND name LIKE ?";
params.push_back("%" + cond.drug_name + "%");
}
if (!cond.supplier_id.empty()) {
sql += " AND supplier_id = ?";
params.push_back(cond.supplier_id);
}
if (cond.min_stock > 0) {
sql += " AND stock_quantity >= ?";
params.push_back(std::to_string(cond.min_stock));
}
if (!cond.expire_after.empty()) {
sql += " AND expire_date > ?";
params.push_back(cond.expire_after);
}
// 执行预编译语句(mysql_stmt_prepare),参数安全绑定
MYSQL_STMT* stmt = mysql_stmt_init(AdoDB::GetInstance().GetConnection());
mysql_stmt_prepare(stmt, sql.c_str(), sql.length());
// ... 绑定params并执行,此处省略细节
}
这种设计的价值在于:
- 防SQL注入:所有用户输入(如搜索框内容)都作为参数绑定,而非字符串拼接;
- 查询灵活性:前端勾选“只查近效期药品”就传expire_after,勾选“按供应商筛选”就传supplier_id,无需为每种组合写新函数;
- 性能可预测:WHERE子句动态生成,但MySQL查询优化器仍能基于索引(CREATE INDEX idx_name ON drugs(name))高效执行。
3.3 DeleCK.h:库存扣减的事务安全与并发控制
DeleCK.h中的ReduceStock()函数,是医药系统合规性的生命线。它必须保证“出库成功=库存减少+记录日志+不可逆”,任何环节失败都要回滚。代码逻辑如下:
bool DeleCK::ReduceStock(const std::string& drug_code, int quantity) {
MYSQL* conn = AdoDB::GetInstance().GetConnection();
// 1. 开启事务
mysql_query(conn, "START TRANSACTION");
// 2. 查询当前库存(加行锁,防并发超卖)
std::string query_sql = "SELECT stock_quantity FROM drugs WHERE drug_code = ? FOR UPDATE";
// ... 执行预编译查询,获取current_stock
if (current_stock < quantity) {
mysql_query(conn, "ROLLBACK");
return false; // 库存不足
}
// 3. 扣减库存
std::string update_sql = "UPDATE drugs SET stock_quantity = stock_quantity - ? WHERE drug_code = ?";
// ... 执行更新
// 4. 记录出库日志
std::string log_sql = "INSERT INTO out_log(drug_code, quantity, operator, op_time) VALUES (?, ?, ?, NOW())";
// ... 执行插入
// 5. 提交事务
mysql_query(conn, "COMMIT");
return true;
}
这里的关键细节:
- FOR UPDATE子句在SELECT时对目标行加写锁,确保同一药品的多次出库请求串行执行;
- ROLLBACK必须显式调用,不能依赖异常——C++异常在MySQL C API中不可靠;
- 日志表out_log与主表drugs在同一事务中操作,保证“扣减和记账”原子性。
实操心得:我在指导学生时,会让ta故意在
UPDATE后加sleep(5),然后开两个进程同时出库同一药品,观察第二个进程是否阻塞等待——这就是数据库事务隔离级别的直观教学。
4. 全流程部署与跨平台编译实录:从零开始搭建可运行环境
4.1 Windows平台(VS2022 + MySQL 8.0):标准课设环境搭建
这是答辩现场唯一保证成功的路径。步骤必须精确到菜单选项:
- 安装MySQL 8.0:下载
mysql-installer-community-8.0.xx.msi,安装时勾选“Developer Default”,设置root密码为medpass(与AdoDB.cpp中硬编码一致); - 创建数据库与用户:
sql CREATE DATABASE medication_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'meduser'@'localhost' IDENTIFIED BY 'medpass'; GRANT ALL PRIVILEGES ON medication_db.* TO 'meduser'@'localhost'; FLUSH PRIVILEGES; - 导入SQL脚本:用MySQL Workbench执行
sql/create_tables.sql,生成drugs、suppliers、out_log等表; - VS2022配置:
- 新建“MFC应用程序”项目,类型选“基于对话框”;
- 在“项目属性→常规→字符集”设为“使用Unicode字符集”;
- “配置属性→C/C++→常规→附加包含目录”添加MySQL Connector/C++头文件路径(如C:\Program Files\MySQL\Connector.C++ 8.0\include);
- “配置属性→链接器→常规→附加库目录”添加lib路径(如C:\Program Files\MySQL\Connector.C++ 8.0\lib\vs143);
- “配置属性→链接器→输入→附加依赖项”填入mysqlcppconn.lib; - 编译运行:将
medication.db(示例数据)复制到exe同目录,启动程序即可看到预置的10种药品。
4.2 Linux平台(Ubuntu 22.04 + g++ + MySQL):命令行下的全流程验证
跨平台不是口号,是实打实的终端操作。以Ubuntu为例:
# 1. 安装依赖
sudo apt update && sudo apt install -y build-essential libmysqlclient-dev cmake qtbase5-dev
# 2. 编译MySQL Connector/C++(官方deb包常版本不匹配)
wget https://dev.mysql.com/get/Downloads/Connector-C++/mysql-connector-c++-8.0.xx-linux-glibc2.17-x86-64bit.tar.xz
tar -xf mysql-connector-c++-8.0.xx-linux-glibc2.17-x86-64bit.tar.xz
sudo cp -r mysql-connector-c++-8.0.xx-linux-glibc2.17-x86-64bit/include/* /usr/include/
sudo cp mysql-connector-c++-8.0.xx-linux-glibc2.17-x86-64bit/lib/libmysqlcppconn.so /usr/lib/
# 3. 修改源码:将MFC相关头文件(#include "afxwin.h")替换为Qt头文件(#include <QApplication>)
# 4. 使用CMakeLists.txt构建
cmake -DCMAKE_BUILD_TYPE=Release -DQT5_DIR=/usr/lib/x86_64-linux-gnu/cmake/Qt5 .
make -j$(nproc)
# 5. 运行前设置环境变量
export LD_LIBRARY_PATH="/usr/lib:$LD_LIBRARY_PATH"
./medication_system
关键点在于:Linux下没有MFC,必须用Qt替代;LD_LIBRARY_PATH必须包含MySQL连接库路径,否则报libmysqlcppconn.so: cannot open shared object file。
4.3 macOS平台(macOS 13 + Xcode + Homebrew MySQL):避坑指南
macOS的坑最多,主要在签名和路径:
# 1. 用Homebrew安装MySQL(避免官网dmg的权限问题)
brew install mysql
brew services start mysql
# 2. 创建用户(macOS默认无root用户)
mysql -u $(whoami) -e "CREATE USER 'meduser'@'localhost' IDENTIFIED BY 'medpass'; GRANT ALL ON *.* TO 'meduser'@'localhost';"
# 3. 安装MySQL Connector/C++
brew install mysql-connector-c++
# 4. Xcode配置(重点!)
# - 在“Build Settings→Search Paths→Header Search Paths”添加:/opt/homebrew/include/mysql
# - “Library Search Paths”添加:/opt/homebrew/lib
# - “Other Linker Flags”添加:-lmysqlcppconn -lmysqlclient
# 5. 签名绕过(macOS Catalina后强制)
sudo xattr -rd com.apple.quarantine /opt/homebrew/opt/mysql-connector-c++/
注意:macOS的
mysql_real_connect()默认尝试Unix socket连接,若报错Can't connect to local MySQL server through socket '/tmp/mysql.sock',需在连接字符串中显式指定host=127.0.0.1(强制TCP/IP)。
5. 常见问题与排查技巧实录:答辩前夜必看的救命清单
5.1 编译期高频问题速查表
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
error LNK2019: unresolved external symbol _mysql_init@4 |
VS2022未正确链接MySQL库 | 检查“附加依赖项”是否为mysqlcppconn.lib(非libmysql.lib),且路径指向vs143子目录(VS2022对应vc143) |
fatal error C1083: Cannot open include file: 'mysql_connection.h' |
头文件路径未添加或Connector/C++版本不匹配 | 下载与MySQL服务端同版本的Connector/C++(如MySQL 8.0.33 → Connector/C++ 8.0.33),路径设为include/mysqlx |
error C2664: 'int mysql_query(MYSQL *,const char *)' : cannot convert argument 2 from 'std::string' to 'const char *' |
C++11 string需显式转换 | 改为mysql_query(conn, sql_str.c_str()),切勿漏掉.c_str() |
5.2 运行时致命错误排查
问题:点击“查询”按钮程序崩溃,调试显示Access violation reading location 0x00000000
这是典型的空指针解引用。根源在AdoDB::s_conn为nullptr。排查顺序:
1. 检查MySQL服务是否运行:net start | findstr "MySQL"(Windows)或brew services list \| grep mysql(macOS);
2. 检查AdoDB.cpp中mysql_real_connect()返回值是否为nullptr,若失败需打印mysql_error(conn);
3. 验证用户名密码:在命令行执行mysql -umeduser -pmedpass -h127.0.0.1 medication_db,确认能登录。
问题:药品名称显示为乱码“涓枃”或“????”
字符集链路断裂。四步定位:
1. 数据库层面:SHOW CREATE TABLE drugs; 确认CHARSET=utf8mb4;
2. 表连接层面:AdoDB.cpp中mysql_options(..., MYSQL_SET_CHARSET_NAME, "utf8mb4")是否执行;
3. C++字符串层面:std::string name = "阿莫西林"; 在VS调试器中查看内存,确认是UTF-8编码(如“阿”字应为E9%98%BF三字节);
4. 界面控件层面:MFC CEdit控件属性“Multiline”和“Want Return”必须勾选,否则无法显示换行符。
5.3 业务逻辑陷阱与合规性提醒
- 库存为0仍可出库? 检查
DeleCK::ReduceStock()中SELECT ... FOR UPDATE是否被执行,若忘记加FOR UPDATE,并发场景下会出现超卖; - 药品过期未预警?
QuerySte1.cpp中expire_after条件应配合定时任务(如Windows计划任务每天执行SELECT * FROM drugs WHERE expire_date < DATE_ADD(NOW(), INTERVAL 30 DAY)); - 供应商删除后药品记录失效?
suppliers表的supplier_id字段必须设为外键(FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE RESTRICT),禁止级联删除导致药品信息丢失。
6. 教学价值延伸与答辩加分技巧:让课设不止于及格线
6.1 从“能运行”到“可讲解”:答辩时的技术纵深展示
导师最想听的不是“我点了按钮它就出来了”,而是“为什么这样设计”。准备三个深度问题答案:
- 问:为何不用ORM框架(如ODB)而手写SQL?
答:“ORM抽象层会掩盖SQL执行细节。本项目需让学生理解EXPLAIN SELECT分析执行计划,比如为name字段建索引后,LIKE '%阿%'仍全表扫描,而LIKE '阿%'能走索引——这是数据库优化的核心能力。”
- 问:MFC消息映射与Qt信号槽,哪个更适合教学?
答:“MFC更底层。ON_COMMAND(ID_BTN_ADD, &CMainWnd::OnBtnAdd)本质是宏展开为case WM_COMMAND: if (LOWORD(wParam)==ID_BTN_ADD) OnBtnAdd();,让学生看到Windows API的消息循环本质;Qt信号槽是元对象系统,对初学者隐藏了太多。”
- 问:跨平台支持是否增加了维护成本?
答:“恰恰相反。业务逻辑层(AddStu.cpp等)完全无平台依赖,界面层仅占代码15%。未来升级为Web系统,只需重写UI层,后端C++核心模块可直接封装为REST API——这就是分层架构的价值。”
6.2 课程设计报告撰写要点:让文档成为技术表达力的证明
很多学生把报告写成操作手册,高分报告应体现思考。建议结构:
- 需求分析章节:画一张UML用例图,标注“药剂师”“库管员”角色,用例包括“紧急出库(无审批)”“常规出库(需二级审批)”,体现对业务角色的理解;
- 数据库设计章节:不仅贴ER图,更要解释“为何drugs表不直接存供应商名称,而用supplier_id外键关联”——答案是“避免数据冗余,修改供应商地址时只需改suppliers表一行”;
- 测试章节:不要只写“测试通过”,要列具体用例:“并发测试:20个线程同时对同一药品出库10次,最终库存=初始值-200,无负数,耗时<3s”。
6.3 后续可扩展方向:给学有余力者的进阶路线图
这个项目不是终点,而是起点。三个务实扩展建议:
1. 增加条码扫描支持:在AddStu.h中新增ScanDrugBarcode()函数,调用Windows API CreateFile("\\\\.\\COM3", ...)读取串口扫描枪数据,解析GS1-128格式(含批次号、有效期);
2. 集成Excel导出:用libxlsxwriter库(轻量C库,无C++依赖)替代system("excel.exe ..."),在QuerySte1.cpp中添加ExportToExcel(const std::vector<DrugInfo>& data),生成带格式的库存报表;
3. 添加基础权限系统:在MyApp.h中引入enum UserRole { ADMIN, PHARMACIST, WAREHOUSE },登录后根据角色禁用DeleCK.h中的高危操作(如ADMIN可删供应商,PHARMACIST只能查不能删)。
我个人在实际指导中发现,真正拉开差距的,从来不是功能多少,而是对一个技术点的深挖程度。比如有位学生,在答辩时展示了他为解决“Windows下MFC对话框字体模糊”问题,研究了DPI感知设置:在MainWnd.cpp的OnInitDialog()中加入SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2),并对比了开启前后文字渲染效果——就这一行代码,让导师当场给了满分。技术的魅力,永远在细节深处。
简介:一个面向课程设计和教学演示的C++医药库存管理系统,基于Windows平台开发,使用Visual Studio编译环境,界面采用MFC或Win32风格(含MyApp.h、MainWnd.cpp等核心框架文件)。系统完整实现药品入库、出库、库存实时查询、供应商信息维护等业务功能,所有数据库操作均通过自封装AdoDB.cpp统一连接MySQL,配合QuerySte1.cpp处理多条件查询、DeleCK.h控制库存扣减逻辑。资源包内置建库SQL脚本(位于sql/目录)、Markdown格式部署指南(含Windows 10/11、macOS、Linux三平台编译验证记录),以及可直接运行的medication.db示例数据库文件。源码模块清晰,按功能拆分为AddStu.h、DeleRK.h、QueryStu.h等头文件及对应.cpp实现,便于理解C++与MySQL交互机制。项目已通过实际测试,无编译报错和运行异常,适合作为软件工程、计算机科学、电子信息类专业课设答辩材料,也适合初学者学习数据库集成开发流程。
更多推荐

所有评论(0)