每年课程设计的高峰期,SSM 药店管理系统都是 Java Web 方向出现频率最高的题目之一。它表面看只是一个普通的药品增删改查,背后却把 Spring、SpringMVC、MyBatis、MySQL、Tomcat 这一整套经典技术栈串了起来,还配套源码、数据库脚本、调试部署说明和万字论文文档。对准备做课程设计、毕业设计的同学来说,这种系统最大的价值不是“能跑”,而是能通过它把 Java Web 项目从前端页面到数据库落库,完整走一遍。这篇文章就围绕这套系统,从业务需求拆解、数据库设计、核心代码实现,到本地调试部署和论文文档组织,把整个链路用实际经验讲透,希望能帮你少踩几个坑。
1. 系统设计与技术选型:药店管理系统到底要解决什么问题
1.1 药店真实业务场景里的核心需求
很多人第一次接触药店管理系统,以为核心就是“药品列表增删改查”,这就把问题想简单了。药店的实际业务链条是:采购员从供应商进货,仓库验收后录入批次和效期,药品上架后由店员销售给顾客,同时库存要实时扣减,快到效期的药品要提醒,库存低于安全值要补货,月底还要统计这个月进了多少、卖了多少、赚了多少。任何一个环节数据对不上,月底盘点的账就对不上,这就是药店管理系统最核心的存在意义。
从业务场景反推,系统的功能模块其实非常清晰:药品档案管理、供应商管理、客户管理、采购入库、销售出库、库存查询与预警、统计报表、用户登录与权限控制。听起来不少,但每个模块的实现难度并不高。真正需要花心思的是模块之间的数据联动,尤其是“一次销售同时扣减库存”“一次入库同时增加库存写流水”这种跨表操作,一旦漏了事务,系统就会出现数据不一致。
1.2 为什么这套系统用 SSM 而不是 Spring Boot
现在新项目确实都用 Spring Boot,课程设计里出现 SSM 并不是因为它落后,而是因为 SSM 能更好地展示 Java Web 的分层思想。用 SSM 的时候,你得自己配置 Spring 容器、自己写 SpringMVC 的控制器映射、自己维护 MyBatis 的 Mapper 映射文件,这个过程能把“IoC 容器是什么”“AOP 事务切到哪一层”“前端请求怎么被分发到 Controller”这些概念真正搞明白。Spring Boot 把这些都自动化了,开发快,但是答辩的时候反而讲不出太多东西。
而且从技术栈成熟度看,SSM 项目的资料太全了,几乎任何一个报错都能在社区里找到同类问题。对新手来说,遇到问题“能搜到答案”比“用新版技术栈”重要得多。这套系统的具体技术选型是:Spring 负责对象管理和事务,SpringMVC 负责请求路由,MyBatis 负责 SQL 映射,MySQL 存数据,Tomcat 作为运行容器,前端用 JSP + JSTL + Bootstrap 做页面,构建工具用 Maven,数据库连接池用 Druid。整套组合足够经典,也足够稳定。
1.3 从标题里的“程序+源码+数据库”倒推项目结构
标题里提到的“程序+源码+数据库+调试部署+开发环境”,其实就是一套课程设计项目的标准交付内容。程序指的是编译后可运行的项目包,源码是完整的工程文件,数据库是建表脚本和初始化数据,调试部署则是整个环境搭建的过程文档。我见过不少同学拿到这种项目后,第一件事就是双击打开某个文件夹,看能不能直接运行,结果很容易失败。
正确的做法是先把项目解压,确认里面的目录结构。典型的 SSM 项目会包含:pom.xml、src 目录、resources 配置文件目录、sql 脚本文件夹、以及说明文档。先搞清楚每个目录是干什么的,再按数据库初始化、配置文件修改、启动部署的顺序操作,这样就比盲目点运行稳得多。
2. 数据库设计:系统好不好用,建表的时候就决定了
2.1 核心业务表的梳理与关系设计
药店管理系统的数据库不需要做得很复杂,但表之间的关系一定要清楚。我按业务习惯把表分成几组:系统用户表存登录账号;药品信息表存药品档案;供应商和客户表存往来单位;入库单主表、入库单明细表、销售单主表、销售单明细表存业务流水;库存数量可以直接冗余在药品表里,也可以单独建库存表。课程设计里大部分项目直接把库存字段放在药品表,这样查询简单,页面展示也方便。
表之间的关系方面,药品和供应商是多对一,一张供应商表对应多条药品记录;药品和销售单据之间是多对多,因为一个销售单里可以包含多个药品,一个药品也会出现在多个销售单里,这种关系必须通过销售单明细表来拆开。如果直接把药品名称和数量塞进销售单表的一个字段,后面统计“某个药品卖了多少”就会非常痛苦。这一点在数据库设计答辩的时候经常被问到,建议提前准备好话术。
2.2 药品信息表字段设计的关键细节
药品表是整个系统的核心,字段设计直接决定后面的代码好不好写。我建表时一般这样定义:
CREATE TABLE `drug_info` ( `drug_id` int NOT NULL AUTO_INCREMENT COMMENT '药品ID', `drug_code` varchar(50) NOT NULL COMMENT '药品编码', `drug_name` varchar(100) NOT NULL COMMENT '药品名称', `specification` varchar(100) DEFAULT NULL COMMENT '规格', `unit` varchar(20) DEFAULT NULL COMMENT '单位', `manufacturer` varchar(100) DEFAULT NULL COMMENT '生产厂家', `produce_date` date DEFAULT NULL COMMENT '生产日期', `expire_date` date DEFAULT NULL COMMENT '有效期至', `purchase_price` decimal(10,2) DEFAULT NULL COMMENT '进货价', `sale_price` decimal(10,2) DEFAULT NULL COMMENT '销售价', `stock_quantity` int DEFAULT 0 COMMENT '当前库存', `warning_value` int DEFAULT 10 COMMENT '库存预警值', PRIMARY KEY (`drug_id`), UNIQUE KEY `uk_drug_code` (`drug_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='药品信息表';这里有几个细节值得注意。药品编码必须加唯一索引,否则系统里可能出现两条名称相同但编码不同的药品记录,后面统计时容易重复。价格字段必须用 decimal,不能直接使用 float 或 double,浮点数的精度在实际开发中会带来“账目差一分钱”的尴尬问题。库存预警值一定要单独建字段,很多新手只在页面上写死一个“库存小于10就提醒”,但不同药品的合理库存水平完全不同,急救药和慢病药要分开设置。
2.3 为什么业务流水表和库存冗余不能互相替代
药品表里的库存字段属于冗余设计,但它不能替代流水表。任何一个系统,库存数字都应该是“结果”,而不是“事实的源头”。比如销售单明细表记录了今天卖出了哪几盒阿莫西林,那么库存扣减就是根据销售单计算出来的结果。如果只留一个库存字段,月底想统计“本月感冒药卖了多少钱”,完全没有数据支撑;想过期损耗想追溯到哪一批药出了问题,也查不到。
所以入库和销售的流水表一定要保留。入库单主表记录入库单号、供应商、入库日期、操作人、总金额,入库单明细表记录每一种药品的入库数量和单价。销售单同理,主表记录单号、客户、销售日期、操作人、总金额,明细表记录每种药品的销售数量和售价。后面做报表统计的时候,所有的数据都从这几张流水表聚合,药品表里的库存字段只是为了方便页面实时展示才存在的。
2.4 数据库连接池与配置文件的常见写法
数据库连接这块,课程设计里常用的连接池有 Druid 和 C3P0,推荐用 Druid,因为监控页面方便,配置也不复杂。数据库配置文件一般放在 resources 目录下,比如 jdbc.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/drug_store?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456如果你的 MySQL 是 5.7 以下版本,驱动类名可以写com.mysql.jdbc.Driver,MySQL 8.0 以上就必须用com.mysql.cj.jdbc.Driver,同时加上 serverTimezone 参数,否则连接时会报时区错误。这个配置是调试部署环节最常出问题的地方,后面我会专门展开讲。
3. 核心代码实现:从登录到库存预警的完整链路
3.1 三层架构的包结构怎么组织
一套标准的 SSM 项目,包结构最好按照 controller、service、mapper、entity 来划分。实体类放 entity,对应数据库表;Mapper 接口和 Mapper XML 放 mapper;Service 接口和实现类放 service;Controller 放 controller。另外可以建一个 common 或 util 包,放拦截器、工具类、分页插件配置等。这样划分的好处不只是看着整齐,更重要的是答辩时画架构图、讲调用链非常方便。
我在实际调试中见过不少反面教材,业务逻辑全部写在 Controller 里,或者 JSP 页面里直接查数据库。这种写法在小项目里看起来能跑,但一旦要加一个“库存不足不能销售”的规则,就得在多个 Controller 里重复修改,代码很快就会失控。所以哪怕只是做课程设计,也建议保持 Controller-Service-Mapper 的调用关系:Controller 只接收参数和返回视图,Service 写业务规则,Mapper 只做 SQL 交互。
3.2 登录认证、验证码与权限拦截
药店管理系统虽然没有特别复杂的权限要求,但登录功能几乎是必备的。使用 SSM 实现登录,最常见的方案是:用户提交用户名密码,Service 层校验后把用户信息放进 Session;然后定义一个拦截器,拦截除登录页、静态资源之外的所有请求,每次请求前判断 Session 里有没有用户对象,没有就重定向到登录页。
拦截器的写法大致是这样:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user = request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }然后在 SpringMVC 配置里注册拦截路径。这种方案比在每个 Controller 方法里手动判断登录状态要省事得多,代码也不会重复。关于验证码,课程设计里用简单的随机数字图片验证码就够了,注意验证码生成时要存在 Session 里,登录校验时先取 Session 里的值比较,比较完无论对错都要删除,避免同一个验证码被重复使用。
3.3 药品管理模块的增删改查与模糊查询
药品管理是整个系统最基础的功能,涉及列表展示、新增、编辑、删除、模糊查询。Controller 里的方法设计一般是一套固定套路:列表方法返回 list 页面,新增方法接收表单参数后调用 Service 保存,编辑方法根据 ID 查出记录回显到页面,删除方法根据 ID 删除并重定向回列表。这个模块虽然简单,但它能锻炼对 REST 风格 URL 和数据绑定的理解。
MyBatis 的 Mapper XML 里,模糊查询是必考内容。查询条件有药品名称、编码、厂家等,因为条件可能为空,所以要使用动态 SQL:
<select id="listDrug" resultType="com.example.entity.DrugInfo"> SELECT * FROM drug_info <where> <if test="drugName != null and drugName != ''"> AND drug_name LIKE CONCAT('%', #{drugName}, '%') </if> <if test="drugCode != null and drugCode != ''"> AND drug_code LIKE CONCAT('%', #{drugCode}, '%') </if> </where> ORDER BY drug_id DESC </select>注意这里的参数绑定一定要用#{},不能直接拼字符串。#{}会生成预编译 SQL,能有效防止 SQL 注入,这也是面试时必问的一个点。如果使用了 PageHelper 分页插件,只需要在查询之前调用PageHelper.startPage(pageNum, pageSize),后面的查询会自动带上 limit,非常方便。
3.4 库存预警与销售单创建的事务处理
库存预警的实现从代码角度来说很直接:写一个查询方法,条件是库存数量小于等于预警值。WHERE stock_quantity <= warning_value,查出来后在页面上用红色或橙色标出。前端展示部分建议加筛选条件,让页面可以分别显示“正常药品”“库存不足”“全部”,不然药品一多,整个列表全是红色,预警就失效了。
销售模块是事务处理的重点。一次销售要同时做三件事:写入销售单主表、写入销售单明细表、扣减药品库存。这三件事必须在一个事务里完成,否则可能出现“主表写入成功但库存没扣”的严重 bug。Spring 的声明式事务很简单,在 Service 方法上加@Transactional注解即可:
@Transactional public boolean createSale(SaleOrder order, List<SaleOrderItem> items) { saleOrderMapper.insert(order); for (SaleOrderItem item : items) { saleOrderItemMapper.insert(item); drugInfoMapper.decreaseStock(item.getDrugId(), item.getQuantity()); } return true; }这里还有一个容易忽略的细节:扣减库存的 SQL 一定要带上库存充足条件,例如:
UPDATE drug_info SET stock_quantity = stock_quantity - #{quantity} WHERE drug_id = #{drugId} AND stock_quantity >= #{quantity}如果影响行数为 0,说明库存不足,此时应该抛出业务异常让事务回滚。只做前端判断“库存是否够”是不够的,并发场景下必须靠 SQL 条件来兜底。这个点是系统在并发场景下能不能真正可用的关键,也是面试和答辩时最能体现水平的细节之一。
4. 调试部署全流程:让系统在一小时内跑起来
4.1 开发环境版本搭配
SSM 项目对环境版本非常敏感,版本不匹配是跑不起来的首要原因。我调试这类项目时推荐的组合是:JDK 1.8、Tomcat 8.5、Maven 3.6.3、MySQL 5.7 或 8.0、IDEA 2021 及以上版本、Navicat 作为数据库可视化工具。
这里重点强调几个兼容性坑。Tomcat 10 之后把所有 javax 命名空间改成了 jakarta,老的项目如果用的是原版 SSM 依赖,部署到 Tomcat 10 会直接报 ClassNotFound 之类的错误,老老实实用 Tomcat 8 或 9 最省事。MySQL 版本如果高于 8.0,连接驱动必须换成mysql-connector-java 8.0.x,同时 JDBC URL 里加serverTimezone=Asia/Shanghai。IDEA 启动项目时如果出现 “java: 错误: 不支持发行版本 5” 这类提示,多半是项目编译级别和当前 JDK 版本不一致,在 Project Structure 里把 SDK 和语言级别都改成 1.8 即可。
4.2 导入项目并初始化数据库的具体步骤
假设你已经拿到了一套完整的项目文件夹,里面包含源码和 SQL 脚本,我建议按下面的顺序操作,每一步都确认没问题再走下一步。
第一步,把项目压缩包解压到一个没有中文和空格的路径下。Windows 下如果路径里有中文,某些工具读取配置文件时会出编码问题,非常难排查。第二步,打开 IDEA,选择 File -> New -> Project from Existing Sources,定位到项目根目录的 pom.xml,以 Maven 项目方式导入,然后等待依赖下载完成。如果下载很慢,去 maven 的 settings.xml 里配置阿里云镜像,能省一大半时间。第三步,打开资源目录下的 jdbc.properties,把数据库地址、用户名、密码改成你自己的。第四步,在 Navicat 里新建一个数据库,字符集选 utf8mb4,然后执行项目里的 SQL 脚本,导入表结构和初始数据。第五步,配置 Tomcat,在 Deployment 标签页把项目部署上去,Application context 建议填/,这样可以直接通过http://localhost:8080/访问系统首页。
配置文件改完之后,启动前再看一遍控制台有没有红色报错。没有报错但访问 404,先看看 Tomcat 的访问路径和项目上下文路径是否一致。按照这套顺序来,绝大多数项目都能在一小时内跑起来。
4.3 启动失败的快速定位思路
启动失败是这门课里最磨人的环节,但经验多了之后会发现,90% 的问题集中在四个地方。第一,端口被占用。Tomcat 默认端口 8080,如果被某个进程占用,启动日志里会直接写Port 8080 required by Tomcat ... is already in use,解决方式是在 IDEA 的 Run Configuration 里修改 Tomcat 端口,或者在命令行查找占用端口的进程并杀掉。第二,数据库连不上。启动报Communications link failure或者Access denied for user,这种问题优先检查 jdbc.properties 里的主机、端口、用户名、密码有没有填错,然后再看 MySQL 服务是否已经启动。第三,Maven 依赖缺失。如果启动时频繁报ClassNotFoundException,基本可以断定某个依赖没有下载完整或没有被打包进项目,执行一次 Maven 的 clean 和 package,再看 target 目录下是否有对应 jar。第四,日志里有 Spring 或 MyBatis 的配置错误,一般是 XML 配置文件中的路径写错或者 Bean 扫描包路径不匹配,注意检查 Mapper 接口包名和 Mapper XML 文件路径是否对应。
5. 使用过程中的常见坑与排查心得
5.1 数据库连接、乱码、MyBatis 映射问题速查
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 启动报 Communications link failure | MySQL 服务未启动,或连接地址端口写错 | 检查 MySQL 服务状态,核对 URL 和端口 |
| 登录时 Access denied for user | 数据库用户名或密码错误 | 修改 jdbc.properties,确认 hosts 权限允许远程登录 |
| 页面操作时 Unknown column 报错 | 实体类字段和数据库列名不一致 | 在 MyBatis 配置中开启驼峰映射,或修改字段名对应关系 |
| 插入数据后拿不到自增主键 | 没配置 useGeneratedKeys | 在 insert 语句中配置 useGeneratedKeys="true" keyProperty="id" |
| 页面中文显示问号 | JSP 页面编码、Tomcat 编码或数据库字符集不对 | 统一使用 UTF-8,Tomcat 配置 URIEncoding="UTF-8" |
| 删除报外键约束失败 | 该药品已被采购或销售记录引用 | 改成逻辑删除(状态字段),或先处理关联明细 |
这里面最值得展开的是中文乱码问题。SSM 项目的中文乱码通常是三个位置同时出问题:JSP 页面文件本身的编码没有设置为 UTF-8,数据库表和连接 URL 没有指定 utf8mb4,以及 Tomcat 处理请求参数时的编码设置。检查顺序建议是:先看数据库表字符集,再查 jdbc 连接 URL 里的characterEncoding=utf8,最后在 JSP 页面顶部确认pageEncoding="UTF-8"。如果这几个都改了还是乱码,可以给 SpringMVC 配置一个编码过滤器 CharacterEncodingFilter,强制请求和响应都走 UTF-8。
5.2 配置了事务但没生效是怎么回事
@Transactional注解加上去了,但回滚却失效,这个问题我有一次排查了很久。最后发现原因是 Spring 的配置文件里没有开启注解事务管理,也就是缺少<tx:annotation-driven transaction-manager="transactionManager" />或者对应的 Java 配置。没有这一行,@Transactional就是个普通注解,不会触发任何行为。另外还要注意,事务默认只对 RuntimeException 回滚,如果代码里抛出的是 checked Exception,事务不会自动回滚,需要在注解上设置 rollbackFor。课程设计里写业务校验时,建议直接抛出new RuntimeException("库存不足")这类运行时异常,简单直接,也能利用到默认回滚机制。
5.3 MyBatis 写 SQL 的几个隐性习惯
用 MyBatis 写 SQL 时,代码规范比想象中更重要。查询结果映射到实体类时,如果数据库列名是drug_name,实体属性是drugName,必须在 mybatis-config.xml 里开启mapUnderscoreToCamelCase,否则查询结果里这个字段就是 null。复杂的多表查询建议使用 ResultMap 而不是实体类返回,因为列名冲突时实体类根本承载不了。插入数据时如果用了自增主键,记得配置useGeneratedKeys="true"和keyProperty="id",否则插入之后拿不到主键,后续写明细表时会很被动。最后一个习惯是,尽量不要在 Mapper XML 里写SELECT *,特别是联表查询,列名重复会导致字段值覆盖,显式列出需要的列名在排查问题时能明显少折腾。
6. 配套论文文档怎么组织:1 万字并不难凑,难点在结构
6.1 课程设计论文的章节结构与字数分配
标题里说“带论文文档 1 万字以上”,这个字数对药店管理系统来说,只要结构搭起来,完全能写够,而且不难。我的建议是分七章:第一章绪论写选题背景和意义,大概写 800 字;第二章相关技术介绍写 Spring、SpringMVC、MyBatis、MySQL 的基本概念和作用,大概写 1500 字;第三章系统需求分析写功能需求和非功能需求,配上用例图,大概写 1500 字;第四章系统设计写总体架构、功能模块划分、流程图,大概写 1500 字;第五章数据库设计写 E-R 图、表结构说明,大概写 1500 字;第六章系统实现是重头戏,每个模块写页面截图、核心代码、实现思路和主要代码说明,大概写 2500 字以上;第七章系统测试写测试用例、测试过程和结果,大概写 1000 字;最后加摘要、目录、参考文献和致谢。
这样算下来总量超过一万字一点问题没有。关键点在每个章节不要写空话。比如“相关技术介绍”里不要只写“Spring 是一个轻量级框架”,要写你在这个项目里具体用它做了什么——用了 IoC 管理哪些对象、用了 AOP 处理什么事务。每章内容都跟项目实际结合起来,论文才有说服力。
6.2 系统实现章节最容易出现的问题
论文评审老师最反感的就是“代码堆砌式”写法。如果一整页全是代码,没有任何解释,这种论文核心质量很差。正确的做法是:先放一张功能模块运行的界面截图,用一至两段话说明这个界面的功能,再截取该模块中一段有代表性的核心代码,然后用文字解释这段代码的流程和关键点。以药品管理为例,可以讲清楚新增药品时如何处理前端表单数据、Service 层如何校验必填字段、Mapper 层如何执行 insert,以及为什么价格用 decimal 而不是浮点型。一段代码配两段解释,一万字很快就自然写出来了。
另外,截图要统一边框和大小,论文里的图片要有编号和图题,表要有表头。这些格式细节虽然更偏向排版,但在评审时印象分影响很大。先搭好主干,再逐段细化,比自己闷头从第一章开始写高效得多。
6.3 测试章节怎么写才有说服力
测试部分不用写太复杂,重点是展示测试用例的覆盖度。把核心功能分成登录模块、药品管理模块、入库模块、销售模块、库存预警模块,每个模块列出三个左右的测试用例,包括正常操作和异常操作。比如销售模块的测试用例可以设计为:正常销售数量小于等于库存,验证扣减成功;销售数量大于库存,验证系统拒绝并提示库存不足;未登录状态直接访问销售页面,验证请求被拦截。每个用例写清楚测试步骤、预期结果、实际结果,页面上再补一两个操作截图,这个章节就非常完整了。
个人经验里有几个东西值得提醒。第一,千万别改完数据库表结构再去运行原来 SQL,最简单的数据恢复方式是保留一份初始脚本,数据库搞坏了直接重导。第二,系统里的时间字段建议所有表统一格式,避免 Oracle 风格和 MySQL 风格的日期写法混在一起,统计报表时非常难受。第三,做演示前一定要把药品库存预警值设置成当前库存附近的数字,这样点击首页就能看到“药品库存不足”的红色告警,演示效果比翻半天列表强太多了。