
简介面向数据库课程设计的一份药店管理系统报告适合计算机、信息管理等专业学生借鉴。文档完整展示数据库设计全流程需求分析阶段明确信息要求、角色权限与功能模块并绘制数据流图、编制数据字典概念结构设计阶段给出局部E-R图与综合E-R图后续再逐步落实到逻辑结构设计与物理结构设计覆盖关系模式优化、索引与触发器等实施要点并考虑系统维护与数据安全。系统功能涉及医生与药剂师信息管理、药品进货上柜、营业数据统计、日周月季销售查询及用户权限控制场景具体、结构规范。资源为单份Word文档约353KB可直接下载后按模板修改适合用作课程设计报告蓝图。已有673人学习对正在完成数据库课程设计、需要撰写完整报告的读者有较高参考价值。1. 药店管理系统的数据库报告最该先定下的是库存模型我审过不少数据库课程设计报告药店管理系统是出现频率最高的一类。十有八九的文档里drugs表上直接挂了一个stock字段写“入库 update 一下出库再 update 一下”看着很简洁但一旦涉及批次、有效期、进价变动这个库存模型就撑不住。这份.doc报告要拿高分先要回答一个核心问题一盒药入库时进价 8 元卖出时零售价 12 元批次到期退回供应商数据应该怎么记本文不评价某一篇现成报告只按最常见的 MySQL 8.0 方案把 ER 设计、DDL、进销存 SQL、报告写作和答辩要点一条线讲透。适合正在做数据库课程设计、想从“能跑通”升级为“讲得清”的同学也适合带毕设的工程师快速审一份报告的核心逻辑。2. 关系建模与建表 DDL从 ER 图到六张核心表2.1 药店业务如何拆实体药品、批次、供应商、员工、销售单、销售明细药店业务本身并不复杂拆实体最怕“看见什么就建什么表”。我一般会把系统分成主数据、批次数据和流水数据三层这样递进下来的 ER 图层次清楚后期写查询也不会东拼西凑关联十几张表。主数据是suppliers、drugs、employees。供应商和员工各自独立成表药品主表只存“这个商品叫什么、什么规格、是否在售”不直接写库存数字。很多课程设计把供应商字段直接挂在药品上供应商换了一家就要 update 药品表历史批次也就丢了这是第一个要避开的坑。批次数据是drug_batches。同一种药来自不同供应商、不同进货时间进价可能不同有效期也不同。把批次抽出来后续才能做先进先出、效期预警和供应商追溯。这一张表的存在是整套数据库设计能不能立住的根本。流水数据是sale_orders和sale_items。销售单主表记一次交易销售明细记这次交易里每个批次卖了多少盒。主表和明细表是一对多明细行带上batch_id每一盒药卖出去之后都能追到是哪个批次的货。对药店而言“卖出去哪一批”比“卖出去多少盒”更重要批号直接关联召回和售后。2.1.1 为什么必须拆出批次表药品有有效期这个刚性业务约束这是它和普通超市商品最大的区别。如果不把批次单独拆出来效期预警很难写drugs表上挂一个expiry_date只能表示最新批次历史批次一多就没有位置放。拆出drug_batches之后库存数字也顺理成章地落到批次行上而不是药品主表上。这种先拆业务对象、再画 ER 图的做法也是课程设计报告里“需求分析”和“概念结构设计”两章的重点。引用一个我见过的反面例子有人把expiry_date放在drugs表两批药同时存在时只能留最近的一批另一批的效期信息完全丢失预警报表自然全是错的。2.2 范式与反范式取舍库存字段放在哪里三范式是所有数据库课程设计都会提的概念但药店系统里它并不是“所有表都必须到第三范式”。我的取舍是drugs、drug_batches、sale_items之间不存冗余的药品名称只存drug_id满足第三范式drug_batches上冗余retail_price允许一个批次的零售价和药品主表不同这是有意的反范式sale_items冗余unit_price即使日后药品改价历史销售单的金额也不变。最容易放错库存字段的地方是drugs.total_stock。我建议把它保留但只做冗余展示真实可用的库存以SUM(drug_batches.stock_qty)为准。两张表都维护库存好处是列表页不需要每次 join 批次表就能展示“在库总数”坏处是有两份数据要同步。如果不想引入这种一致性成本也可以完全去掉total_stock查询时用SUM聚合课程设计报告里给出“反范式原因”反而比只贴一张表更有信息量。物理设计章节需要有一小段专门讲这个冗余说明什么时候可以容忍数据不一致什么时候不能。2.3 建库建表 DDL 与约束参数MySQL 8.0下面这份 DDL 可以直接跑字段注释即数据字典。这里用 MySQL 8.0注意CHECK约束在 8.0.16 之后才真正生效如果课程设计环境是 5.7这些约束要由应用层兜底。CREATE DATABASE IF NOT EXISTS pharmacy DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci; USE pharmacy; CREATE TABLE suppliers ( id INT UNSIGNED AUTO_INCREMENT COMMENT 供应商ID, name VARCHAR(80) NOT NULL COMMENT 供应商名称, phone VARCHAR(20) NOT NULL, contact_person VARCHAR(40) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_supplier_name (name) ) ENGINEInnoDB COMMENT供应商表; CREATE TABLE drugs ( id INT UNSIGNED AUTO_INCREMENT COMMENT 药品ID, drug_code VARCHAR(30) NOT NULL COMMENT 药品编码/批准文号, name VARCHAR(100) NOT NULL COMMENT 药品名称, spec VARCHAR(60) NOT NULL COMMENT 规格如 0.25g*24粒, dosage_form VARCHAR(20) NOT NULL COMMENT 剂型, unit VARCHAR(10) NOT NULL COMMENT 最小销售单位, manufacturer VARCHAR(100) DEFAULT COMMENT 生产厂家, retail_price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 默认零售价, status ENUM(ON_SALE,STOP_SALE) NOT NULL DEFAULT ON_SALE, PRIMARY KEY (id), UNIQUE KEY uk_drug_code (drug_code), KEY idx_drug_name (name) ) ENGINEInnoDB COMMENT药品主表; CREATE TABLE employees ( id INT UNSIGNED AUTO_INCREMENT COMMENT 员工ID, emp_no VARCHAR(20) NOT NULL COMMENT 工号, name VARCHAR(40) NOT NULL COMMENT 姓名, position VARCHAR(30) NOT NULL COMMENT 岗位, phone VARCHAR(20) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_emp_no (emp_no) ) ENGINEInnoDB COMMENT员工表;逻辑说明id用INT UNSIGNED AUTO_INCREMENT字段注释直接写在 COMMENT后续从系统表导数据字典时不用另备文档。utf8mb4_0900_ai_ci是 MySQL 8 默认排序规则对中文排序比 5.7 的utf8mb4_general_ci更规范。ENUM放业务状态读数据的人不会把“停售”写成OFF_SALE或0这种不一致的格式。CREATE TABLE drug_batches ( id INT UNSIGNED AUTO_INCREMENT COMMENT 批次ID, drug_id INT UNSIGNED NOT NULL COMMENT 药品ID, batch_no VARCHAR(40) NOT NULL COMMENT 批号, production_date DATE NOT NULL COMMENT 生产日期, expiry_date DATE NOT NULL COMMENT 有效期至, purchase_price DECIMAL(10,2) NOT NULL COMMENT 进货价, retail_price DECIMAL(10,2) NOT NULL COMMENT 本批次零售价, supplier_id INT UNSIGNED NOT NULL COMMENT 供应商ID, stock_qty INT NOT NULL DEFAULT 0 COMMENT 当前库存, PRIMARY KEY (id), UNIQUE KEY uk_drug_batch (drug_id, batch_no), KEY idx_expiry (expiry_date), CONSTRAINT fk_batch_drug FOREIGN KEY (drug_id) REFERENCES drugs (id) ON UPDATE CASCADE ON DELETE RESTRICT, CONSTRAINT fk_batch_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers (id) ON UPDATE CASCADE ON DELETE RESTRICT, CONSTRAINT chk_batch_expiry CHECK (expiry_date production_date), CONSTRAINT chk_batch_stock CHECK (stock_qty 0) ) ENGINEInnoDB COMMENT药品批次与库存表; CREATE TABLE sale_orders ( id INT UNSIGNED AUTO_INCREMENT COMMENT 销售单ID, order_no VARCHAR(32) NOT NULL COMMENT 销售单号, order_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 销售时间, employee_id INT UNSIGNED NOT NULL COMMENT 收银员工ID, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 应收金额, remark VARCHAR(200) DEFAULT NULL COMMENT 备注, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_order_date (order_date), CONSTRAINT fk_order_emp FOREIGN KEY (employee_id) REFERENCES employees (id) ON UPDATE CASCADE ON DELETE RESTRICT ) ENGINEInnoDB COMMENT销售单主表; CREATE TABLE sale_items ( id INT UNSIGNED AUTO_INCREMENT COMMENT 明细ID, order_id INT UNSIGNED NOT NULL COMMENT 销售单ID, drug_id INT UNSIGNED NOT NULL COMMENT 药品ID, batch_id INT UNSIGNED NOT NULL COMMENT 批次ID, quantity INT NOT NULL COMMENT 销售数量, unit_price DECIMAL(10,2) NOT NULL COMMENT 成交单价, amount DECIMAL(10,2) NOT NULL COMMENT 金额小计, PRIMARY KEY (id), KEY idx_item_order (order_id), CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES sale_orders (id) ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT fk_item_drug FOREIGN KEY (drug_id) REFERENCES drugs (id), CONSTRAINT fk_item_batch FOREIGN KEY (batch_id) REFERENCES drug_batches (id), CONSTRAINT chk_item_qty CHECK (quantity 0), CONSTRAINT chk_item_price CHECK (unit_price 0) ) ENGINEInnoDB COMMENT销售明细表;参数说明ON UPDATE CASCADE ON DELETE RESTRICT表示主数据编号变动时自动同步到子表有销售记录时禁止物理删除主表记录防止历史对不上。sale_items的order_id外键用ON DELETE CASCADE作用是清除测试无用单的时候连带清明细但fk_item_drug和fk_item_batch不放级联删除药品或批次记录不能因为销售明细还存在就被删掉。一个表里混用两种策略写报告时值得专门讲一句这比整篇复制通用模板更能体现理解。表主键唯一键外键CHECK 约束suppliersidname无无drugsiddrug_code无retail_price 0, status 枚举employeesidemp_no无无drug_batchesid(drug_id, batch_no)drugs.id, suppliers.idexpiry_date production_date, stock_qty 0sale_ordersidorder_noemployees.idtotal_amount 0sale_itemsid无sale_orders.id, drugs.id, drug_batches.idquantity 0, unit_price 0上面这张约束清单在报告里对应“逻辑结构设计”的数据字典和“物理结构设计”的约束说明。把它整理成 Word 表格外键名称保持简短答辩时才能记得住每个键的作用。3. 进销存 SQL 落地入库、出库、效期预警与销售报表3.1 入库连招INSERT 批次 UPDATE 总库存别用裸 UPDATE入库这个动作很多人的第一反应是“给库存字段加数量”。但按上面的表结构新进一批药应该先新增drug_batches一行记录再同步drugs.total_stock。为什么先插入批次因为后续每一次盘点、效期查询、退货要的都不是“库存总量”而是“哪一批还剩多少”。如果把全部库存压在一行上批次信息只能存在某个 JSON 字段或临时表里查询时拆起来非常痛苦。START TRANSACTION; INSERT INTO drug_batches (drug_id, batch_no, production_date, expiry_date, purchase_price, retail_price, supplier_id, stock_qty) VALUES (1001, B20250601, 2025-06-01, 2027-05-31, 8.50, 12.00, 201, 100); UPDATE drugs SET total_stock total_stock 100 WHERE id 1001; COMMIT;逻辑说明两步必须在同一个事务里否则批次插入成功而总量更新失败库存汇总就对不上。实际生产系统里入库还有采购单主表和采购明细表本例为了课程设计精简省略但事务思路一致先写批次再刷新汇总。START TRANSACTION和COMMIT中间不要夹带网络请求或文件操作否则事务会长时间挂着占住连接资源。如果这里不用START TRANSACTION而用“先查再改”的裸 UPDATE两个会话并发入库时可能把total_stock算错。具体表现是两个事务都读到 100各自加 50最后写 150实际应该是 200。这个现象是典型的“丢失更新”也是下一节为什么要加锁的引子。下面这段会和“数据库死锁”一样成为并发章节的素材。3.2 销售出库的存储过程与行锁防止超卖销售是最能展示数据库课程设计报告含金量的地方。我一般会用一个带行锁的存储过程把“扣库存 写销售单 写销售明细”包在一起而不是让应用层分三步执行。这样一份 MySQL 脚本就能说明并发安全性答辩时比贴一大段 JDBC 代码更容易讲清楚。DELIMITER $$ CREATE PROCEDURE sp_sell_batch( IN p_batch_id INT, IN p_quantity INT, IN p_employee_id INT, IN p_order_no VARCHAR(32) ) BEGIN DECLARE v_stock INT DEFAULT 0; DECLARE v_drug_id INT DEFAULT 0; DECLARE v_price DECIMAL(10,2) DEFAULT 0; START TRANSACTION; SELECT stock_qty, drug_id, retail_price INTO v_stock, v_drug_id, v_price FROM drug_batches WHERE id p_batch_id FOR UPDATE; IF v_stock p_quantity THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 库存不足无法销售; END IF; UPDATE drug_batches SET stock_qty stock_qty - p_quantity WHERE id p_batch_id; UPDATE drugs SET total_stock total_stock - p_quantity WHERE id v_drug_id; INSERT INTO sale_orders (order_no, order_date, employee_id, total_amount) VALUES (p_order_no, NOW(), p_employee_id, p_quantity * v_price); SET order_id LAST_INSERT_ID(); INSERT INTO sale_items (order_id, drug_id, batch_id, quantity, unit_price, amount) VALUES (order_id, v_drug_id, p_batch_id, p_quantity, v_price, p_quantity * v_price); COMMIT; END$$ DELIMITER ;参数说明SELECT ... FOR UPDATE对被销售的批次行加排他锁第二个会话再执行同一批次销售时会阻塞直到第一个事务提交。这是解决超卖的正路不是靠应用层预扣。LAST_INSERT_ID()拿当前会话刚生成的sale_orders.id注意它只适用于这个连接不能跨连接使用。SIGNAL SQLSTATE 45000是主动抛错语法能让应用层捕获到“库存不足”的业务异常而不是撞唯一键。还有一个细节值得写进报告行锁只锁一条批次。如果同一笔销售要买多个批次应用层会多次调用存储过程注意保持加锁顺序比如先按p_batch_id升序锁多个批次再更新drugs.total_stock否则两个事务互相等待对方已锁的行就会触发数据库死锁。把这一句写进“并发控制”小节比单纯列代码更有深度。3.3 近效期预警视图CURDATE 和 DATE_ADD 的参数边界药店系统里最有业务特色的查询是近效期预警——哪些药 90 天内过期、还剩多少。这个查询在报告里可以直接做成视图日常程序查询v_expiring_drugs即可后续增加“按仓库过滤”“按供应商过滤”时不用改应用代码。CREATE OR REPLACE VIEW v_expiring_drugs AS SELECT d.drug_code, d.name, d.spec, b.batch_no, b.expiry_date, b.stock_qty FROM drug_batches b JOIN drugs d ON d.id b.drug_id WHERE b.expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 90 DAY) AND b.stock_qty 0 ORDER BY b.expiry_date;参数说明CURDATE()只取日期部分不受当前时间影响INTERVAL 90 DAY是 MySQL 固定写法不能省略INTERVAL也不能写成DATE_ADD(CURDATE(), 90)。如果想改成“本月到期的药”把INTERVAL 90 DAY换成LAST_DAY(CURDATE())。注意BETWEEN是闭区间应用层传参时不要把带时分秒的Datetime直接塞进来否则边界记录会漏。视图用CREATE OR REPLACE课程设计反复改报表时重新执行脚本不会报错。3.4 月度销售统计与 TOP10 排名GROUP BY 里的日期处理报表部分最能拉开报告差距。月度统计的难点是日期分组以及“单数”和“金额”的聚合口径。另一个常见雷区是对索引字段套函数导致索引失效下面第一个查询用的是区间条件而不是MONTH(order_date) 6这种写法。SELECT DATE_FORMAT(order_date, %Y-%m) AS sale_month, COUNT(DISTINCT id) AS order_cnt, SUM(total_amount) AS sales_amount FROM sale_orders WHERE order_date 2025-06-01 AND order_date DATE_ADD(2025-06-01, INTERVAL 1 MONTH) GROUP BY DATE_FORMAT(order_date, %Y-%m);逻辑说明用 月初 AND 下月初代替BETWEEN可以避开“月末 23:59:59.999”这种边界坑。COUNT(DISTINCT id)统计单数SUM(total_amount)统计金额。如果订单表有退款记录建议加status NORMAL条件否则金额对不上。把这种 WHERE 条件写进课程设计报告是“数据库优化”意识最好的证明不要在idx_order_date字段上套函数否则索引会失效。销量排行 TOP10 查询如下SELECT d.name, SUM(si.quantity) AS sale_qty, SUM(si.amount) AS sale_amount FROM sale_items si JOIN drugs d ON d.id si.drug_id GROUP BY d.id, d.name ORDER BY sale_qty DESC LIMIT 10;参数说明按d.id分组而不是按d.name避免两个不同药品重名时被合并。LIMIT 10配合ORDER BY才有意义。这个查询是典型的分组聚合能覆盖增删改查里最容易被追问的GROUP BY语义select 后的非聚合列必须出现在 group by 中MySQL 的ONLY_FULL_GROUP_BY模式默认开启5.7 之后写少了会直接报错。4. 课程设计报告写作与答辩数据字典、ER 图、InnoDB 与外键追问4.1 从 DDL 生成数据字典报告表格的字段映射“药店管理系统.doc”最终是给人看的报告不是 SQL 脚本集合。数据字典放在“逻辑结构设计”章节通常一张表一个小节字段列表包括字段名、类型、长度/精度、是否为空、默认值、键、说明。用前面 DDL 的字段注释就能填满“说明”列不用手工再编一遍。报告章节需要写的内容对应素材需求分析角色、业务流程、数据流员工/供应商/药品主数据概念结构设计ER 图及实体关系说明六张表的实体与联系逻辑结构设计关系模式、数据字典、约束DDL、外键、CHECK物理结构设计存储引擎、字符集、索引InnoDB、utf8mb4、idx_expiry详细设计与实现关键事务 SQL、存储过程sp_sell_batch、v_expiring_drugs这一节有个小技巧先跑 5.1 节的information_schema查询再把结果用 DataGrip 或 Navicat 导出成 CSV最后在 Word 里用“插入表格 → 文本转表格”粘贴。不要手抄 60 个字段手抄必错而且和实际库对不上时答辩老师只要随机问一个字段就能看出来。4.2 关系模式和 ER 图的转换要点很多同学会画 ER 图但不会把 ER 图转成规范的关系模式。常见错误是“实体直接照搬成表联系全部忽略”。药店系统里销售这个联系必须单独拆成sale_orders和sale_items因为它是多对多联系“员工—药品—批次”并且带有数量和金额属性。关系模式写为suppliers( id, name, phone, contact_person ) drugs( id, drug_code, name, spec, dosage_form, unit, manufacturer, retail_price, status ) drug_batches( id, drug_id, batch_no, production_date, expiry_date, purchase_price, retail_price, supplier_id, stock_qty ) sale_orders( id, order_no, order_date, employee_id, total_amount, remark ) sale_items( id, order_id, drug_id, batch_id, quantity, unit_price, amount )下划线表示主键外键在关系模式中加波浪线或单独标注不同教材习惯不同但要在报告里先说明图例。drug_batches同时带drug_id、supplier_id两个外键说明批次是“药品与供应商”多对一关联的载体这一步写得好答辩时“为什么多一张表”的问题就能少一半。画 ER 图时菱形框里写“销售”连接员工、药品批次和销售单属性中把quantity和unit_price挂在联系边上不要挂在实体上。4.3 答辩追问里最常见的外键与索引问题数据库课程设计答辩的老师往往不看完整代码专门问几个“为什么”。以下三个问题是药店管理系统出现频率最高的为什么用 InnoDB 而不是 MyISAMInnoDB 支持外键约束和事务MyISAM 只有表锁。出库存储过程里FOR UPDATE的行锁、START TRANSACTION的回滚在 MyISAM 上都不成立。为什么 MySQL 8.0 之前 CHECK 约束“失效”MySQL 5.7 能解析CHECK语法但不执行到了 8.0.16 才开始真正生效。课程设计如果部署在 5.7stock_qty 0还是要靠应用层校验。把这一点写进报告能看出版本意识。为什么idx_expiry索引对效期预警是必要的查询里 WHERE 条件带expiry_date BETWEEN在该字段建 B-TREE 索引后数据库能走范围扫描不用遍历所有批次。索引的副作用是写变慢药品销售频率远高于入库频率读多写少场景建索引是划算的。如果老师继续追问“为什么不用全文索引搜索药品名”回答是药名匹配本质是前缀匹配普通 B 树索引已经能命中LIKE 阿莫%没必要为课程设计引入全文索引。这种数量级的业务索引设计主线是“主键 唯一键 外键索引 高频 WHERE 字段”不要每个字段都加索引。5. 用 information_schema 一行命令导出整本数据字典写报告改字段时最烦的是数据字典表与已修改的 DDL 不同步。这里有一个不用额外工具的做法直接查information_schema字段结构永远和真实库一致。这一章的 SQL 可以直接复制到 DataGrip 或 Navicat 查询窗口运行。SELECT TABLE_NAME AS 表名, COLUMN_NAME AS 字段名, COLUMN_TYPE AS 数据类型, IS_NULLABLE AS 是否为空, COLUMN_DEFAULT AS 默认值, COLUMN_KEY AS 键, COLUMN_COMMENT AS 说明 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA pharmacy ORDER BY TABLE_NAME, ORDINAL_POSITION;参数说明ORDINAL_POSITION是字段在表里的原始顺序按它排序导出的 CSV 页码顺序正好和 DDL 一致。COLUMN_KEY返回值有三种PRI表示主键MUL表示非唯一索引或外键的第一列UNI表示唯一键。MUL可能同时是外键和普通索引导出后需要在 Excel 里再核对一遍不能直接当作外键清单。外键依赖查询用另一个系统表SELECT table_name, column_name, referenced_table_name, referenced_column_name FROM information_schema.KEY_COLUMN_USAGE WHERE table_schema pharmacy AND referenced_table_name IS NOT NULL ORDER BY table_name;这段 SQL 有两个价值一是给报告画 ER 图时核对“每一条外键是否真的对应一个关系”二是生成“测试数据清空脚本”的删除顺序先删sale_items再删sale_orders和drug_batches最后删drugs与suppliers。删除顺序反了外键会立刻报错这是课程设计验收前最常见的翻车点。实操时在 macOS 或 Linux 命令行下用mysql -u root -p -N -e SELECT ... pharmacy columns.tsv输出制表符文件再交给 Excel 粘贴成表格在 Windows 上直接用 Navicat 的“导出查询结果 → CSV”记得选 UTF-8否则 Word 打开中文会乱码。导出后把“表名”列按六张表的业务顺序手动排一下Word 里的数据字典章节就成型了。这样无论后面改了多少表报告附录里的数据字典都能用同一段 SQL 重新生成不用手工维护第二份文档。本文还有配套的精品资源点击获取