
药店收银管理系统这种题目在JavaWeb的毕业设计和课程设计里出现频率一直很高。它的核心难点不在于技术有多深而在于你能否把SSM框架、Maven、MySQL、Layui这一整套技术栈顺畅地串起来——从数据库表设计到后端接口开发再到前端页面渲染任何一个环节掉链子整个项目就跑不起来。今天这篇文章就把这套药店收银系统的设计思路、环境搭建、核心代码实现和踩坑经验一次性讲透给正在做类似JavaWeb项目的朋友一份可以直接参考的实操指南。1. 项目全局设计为什么SSMMySQLLayui是这类系统的首选组合1.1 系统需求拆解与功能模块划分在动手写代码之前得先把药店的收银业务流程想清楚。我见过太多人一上来就建表、写Mapper结果做到一半发现字段不够用改表结构改到怀疑人生。药店的日常运营其实是一条清晰的链路采购入库、库存管理、前台收银、销售记录、利润统计。围绕这条链路系统的功能模块可以拆成这样药品管理药品的增删改查、分类维护、厂商信息管理这是整个系统的基础数据源。库存管理入库登记、库存盘点、库存预警药品过期和断货是药店经营的大忌所以库存模块必须精细到批次和有效期。收银台模块搜索或扫码添加药品到购物车、修改数量、计算总价、确认收款、打印小票这是收银员每天面对最多的界面。订单管理销售订单的查询与明细查看支持按日期、按药品、按收银员多维度检索。统计报表日销售额、月度销售趋势、药品销量排行这些数据直接决定药店后续的进货策略。员工管理收银员和管理员的账号管理、登录日志权限要区分开。这套需求不算复杂但麻雀虽小五脏俱全。用户管理、药品管理、订单管理这种组合几乎是JavaWeb项目的标准模板做完这一套你的增删改查、事务控制、多表关联查询、前端渲染这些基本功都能练到。1.2 技术栈选型背后的逻辑很多人会问都2025年了为什么还要用SSM而不是SpringBoot我先说结论作为学习项目或者课程设计SSM反而更能体现你对Web开发底层原理的理解。先看Spring。它是整个系统的粘合剂IOC容器负责管理所有Bean的生命周期AOP负责切面事务和日志记录。在SSM架构里Spring的配置文件你能看得见摸得着applicationContext.xml里每一行bean配置都是真实的依赖注入过程这比SpringBoot的自动配置要直观得多。再看SpringMVC。它的请求流转过程非常清晰DispatcherServlet接收请求通过HandlerMapping找到对应的Controller方法方法执行完返回ModelAndView或者JSON数据。做这个项目时你能清楚地感觉到请求是怎么从前端走到后端再返回来的而不是像SpringBoot那样一堆注解把过程都封装黑了。MyBatis则是把SQL控制权完全交给你。药店系统的查询场景很多——药品按名称模糊查询、按分类筛选、按厂商过滤、库存低于预警值的列表这些动态SQL在MyBatis里写起来非常顺手。它不像JPA那样帮你自动生成SQL所有查询逻辑都在你的掌控之中出了问题也好排查。前端选Layui核心原因是它对后端开发者极其友好。你不需要懂npm、不需要配Vue工程、不需要写webpack配置把layui.js和layui.css引入页面就能用而且它提供的表格、表单、弹窗、日期选择器这些组件覆盖了管理端系统90%的界面需求。对于以Java后端为主的项目来说Layui是最务实的选型。Maven在这套系统里的作用常被低估。它最重要的事不是打包而是依赖管理。SSM整合涉及Spring、SpringMVC、MyBatis、MyBatis-Spring、MySQL驱动、Jackson、JSTL等十几个依赖每个依赖还有版本兼容性问题。用Maven统一声明依赖版本配合阿里云镜像仓库能够彻底告别手动下载jar包并把它们塞进WEB-INF/lib目录的原始操作。2. 环境搭建与配置Maven、MySQL、IDEA的实操记录2.1 Maven安装配置与阿里云镜像仓库Maven的安装本身没太多技术含量但配置环节有两个细节必须处理好一是本地仓库路径二是镜像源。下载Maven二进制包注意是apache-maven-x.x.x-bin.zip不是src.zip解压到纯英文路径比如D:\maven。然后修改conf/settings.xml文件第一步是把本地仓库定位到非C盘避免系统盘空间膨胀localRepositoryD:/maven/repository/localRepository第二步是配置阿里云镜像。国内不配镜像的话拉取依赖的速度会让你等到怀疑人生配了之后基本几十秒搞定mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror然后在IDEA的Settings → Build Tools → Maven中把User settings file指向你改好的settings.xmlLocal repository会自动识别为D:/maven/repository。注意IDEA自带的Maven和你的Maven最好保持同一个配置文件否则可能出现在命令行能打包但IDEA里依赖红线的问题。2.2 MySQL安装与连接参数避坑MySQL的安装过程网上教程一抓一大把我重点说两个项目运行阶段最容易踩的坑。第一个坑是SSL连接报错。我用的是MySQL 8.0JDBC连接URL如果写成这样启动Tomcat时大概率报SSL connection errorjdbc.mysql.urljdbc:mysql://localhost:3306/drugstore?serverTimezoneAsia/Shanghai原因是MySQL 8.0默认开启SSL而JDBC驱动默认也会尝试SSL连接两边一握手就出问题。解决办法是在URL后面显式声明关闭SSL同时设置时区和编码jdbc.mysql.urljdbc:mysql://localhost:3306/drugstore?useSSLfalseserverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8allowPublicKeyRetrievaltrueallowPublicKeyRetrievaltrue这个参数是给MySQL 8.0用的不加的话用caching_sha2_password认证方式可能会报Public Key Retrieval is not allowed错误。第二个坑是驱动类名。MySQL 5.x用com.mysql.jdbc.DriverMySQL 8.x用com.mysql.cj.jdbc.Driver。如果你的pom里引入的是8.x驱动却在db.properties里写了老驱动类名启动直接报ClassNotFoundException。这类问题排查起来不难但新手往往会忽略驱动和数据库版本是配套的。2.3 Maven项目创建与SSM整合的关键配置创建一个标准的Maven Web项目后项目的目录结构必须是这样的main/java存放Java源码main/resources存放配置文件和Mapper映射main/webapp存放前端页面和静态资源。不要自作聪明地把配置文件放到其他位置Maven约定优于配置乱了约定后面处处是坑。SSM整合需要三份Spring配置文件。第一份是spring-mvc.xml负责SpringMVC的组件扫描、注解驱动和视图解析器配置。第二份是spring-mybatis.xml负责注入数据源、创建SqlSessionFactoryBean、扫描Mapper接口。第三份是spring-service.xml有些项目合并成一份applicationContext.xml负责Service层的事务管理和组件扫描。三份配置在web.xml里通过ContextLoaderListener加载Spring容器通过DispatcherServlet加载SpringMVC容器。有一个细节新手几乎必踩SpringMVC的子容器和Spring的父容器如果都扫到了Service或Controller会出现Bean重复定义或者事务失效的问题。正确的做法是spring-mvc.xml只扫Controllerspring-service.xml只扫Service和Repository。提示使用IDEA创建项目时如果ecplise风格的项目结构完全不认识Maven是因为缺少src/main/java等目录需要手动右键New → Directory创建并标记为Sources Root。3. 数据库设计与核心业务代码实现3.1 药店系统核心数据表结构设计数据库是整个系统的地基。药店系统的表结构围绕药品—库存—销售这条主线展开。这是我经过实战调整后的核心表结构CREATE TABLE drug ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 药品ID, drug_code VARCHAR(50) UNIQUE COMMENT 药品编码/条码, name VARCHAR(100) NOT NULL COMMENT 药品名称, category VARCHAR(50) COMMENT 药品分类, manufacturer VARCHAR(100) COMMENT 生产厂家, specification VARCHAR(100) COMMENT 规格, unit VARCHAR(20) COMMENT 单位, purchase_price DECIMAL(10,2) COMMENT 进价, selling_price DECIMAL(10,2) COMMENT 售价, stock INT DEFAULT 0 COMMENT 库存数量, warning_line INT DEFAULT 10 COMMENT 库存预警线, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sale_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) UNIQUE COMMENT 订单编号, cashier_id INT COMMENT 收银员ID, total_amount DECIMAL(10,2) COMMENT 总金额, payment_method VARCHAR(20) COMMENT 支付方式, sale_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sale_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT COMMENT 订单ID, drug_id INT COMMENT 药品ID, drug_name VARCHAR(100), price DECIMAL(10,2), qty INT, subtotal DECIMAL(10,2) );药品编码要设置为唯一索引因为收银台最常见的操作就是扫码或者输入编码快速查药唯一索引能保证查询效率也能防止相同药品重复录入。warning_line字段是库存预警的基石查哪些药快卖完了就是一条简单的WHERE stock warning_line。3.2 收银台业务的后端事务实现收银结账是整个系统里最需要原子性的操作。一次收银动作牵扯到三张表插入sale_order主表、批量插入sale_order_item明细表、更新drug表的库存。这三步任何一个失败都不能让其他两步落库否则就会出现订单没生成但库存扣了这类的数据孤儿。Spring声明式事务是干这个活最优雅的方式。在Service层的收款方法上加一个Transactional注解事务边界就划定完毕Service public class SaleOrderServiceImpl implements SaleOrderService { Autowired private SaleOrderMapper saleOrderMapper; Autowired private SaleOrderItemMapper saleOrderItemMapper; Autowired private DrugMapper drugMapper; Override Transactional(rollbackFor Exception.class) public void createOrder(SaleOrder order, ListSaleOrderItem items) { // 1.生成订单号并插入主表 order.setOrderNo(generateOrderNo()); saleOrderMapper.insert(order); // 2.批量插入明细 for (SaleOrderItem item : items) { item.setOrderId(order.getId()); saleOrderItemMapper.insert(item); // 3.扣减库存必须加上库存充足校验 int affected drugMapper.deductStock(item.getDrugId(), item.getQty()); if (affected 0) { throw new RuntimeException(药品库存不足或药品不存在: item.getDrugName()); } } } }这里有个非常关键的细节扣减库存不用UPDATE drug SET stock stock - #{qty}盲目操作而是通过affected返回值来判断是否扣成功。因为你在更新之前先查库存、再判断、再更新这三步存在并发间隙访问问题。我的做法是直接在SQL里用条件更新UPDATE drug SET stock stock - #{qty} WHERE id #{drugId} AND stock #{qty}这样只有库存真的够时受影响行数才是1否则返回0直接抛出异常触发事务回滚。这一招比很多人常用的先查后判更可靠推荐直接抄作业。3.3 MyBatis动态SQL与常用注解实践药店药品列表的筛选条件非常多——按名称模糊查、按分类精确查、按厂商查询、库存预警查询这些条件组合在一起就是MyBatis动态SQL的经典场景。Mapper接口里我用Param注解绑定参数然后在XML中用where标签智能拼接条件会产生前导AND的SQL会被自动去掉非常实用例如select idfindDrugList resultTypecom.drugstore.entity.Drug SELECT * FROM drug where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testcategory ! null and category ! AND category #{category} /if if teststockWarning ! null and stockWarning true AND stock lt; warning_line /if /where ORDER BY create_time DESC /select使用Param注解的原因很直接多参数传递时MyBatis无法自动识别参数名必须显式声明否则编译后参数名被替换成arg0、param1这种SQL里的#{name}根本对应不上。4. 前端页面与Layui组件实战4.1 药品管理页面Layui表格快速落地Layui的table模块是管理端列表页面的主力军。不需要手动拼接HTML表格只需一个容器元素加上一段渲染函数table.render({ elem: #drugTable, url: /drug/list, page: true, cols: [[ { field: id, title: ID, width: 80 }, { field: drugCode, title: 药品编码, width: 120 }, { field: name, title: 药品名称, minWidth: 160 }, { field: category, title: 分类, width: 100 }, { field: sellingPrice, title: 售价, width: 100 }, { field: stock, title: 库存, width: 90, templet: function(d) { if (d.stock d.warningLine) { return span stylecolor:red; d.stock /span; } return d.stock; }}, { title: 操作, toolbar: #barTemplate, width: 150 } ]], skin: line });templet在这里派上了大用场。当库存低于预警线时数字直接标红收银员和管理员扫一眼就能看出哪些药需要进货不用再进详情页查看。Layui表格向后端发的参数是约定好的page页码、limit每页条数前端搜索时通过table.reload把搜索条件带过去。后端配合PageHelper分页插件返回标准格式即可。这里有一个前期开发时就要对齐的JSON结构{ code: 0, msg: , count: 1000, data: [...] }code必须为0才能被Layui识别为成功这是Layui表格最容易被新手忽略的规范。4.2 新增编辑弹窗与表单提交药品管理的新增和编辑功能我用的Layui的layer.open弹窗配合form模块。点击添加药品按钮弹出一个表单页面提交时通过form.on(submit(...))监听然后使用$.ajax把表单数据发到后端接口。这样做的体验比跳转独立页面好收银员不用频繁切换页面操作效率明显提升。表单验证也是前端减少无用请求的重要手段。药名必填、价格必须是非负数、库存数必须是整数这些规则在Layui的lay-verify属性里声明好不符合要求的请求连后端都不用到达既省流量又减轻服务器压力。4.3 收银台界面交互的细节收银台页面是整个系统中交互最频繁的界面。我的布局是左侧一个搜索框加药品列表右侧一个购物车面板。扫码或输入关键词后药品列表实时筛选点击加入购物车后药品进入右侧列表同时累加总金额。购物车中的数量输入框失焦时需要重新计算该药品的小计和订单总金额。这些交互全部用jQuery处理即可Layui内置了jQuery不需要额外引入。核心思路是维护一个JavaScript数组作为购物车数据源每次增删或修改数量后遍历数组重新渲染购物车列表并计算总额。逻辑简单直接适合演示和学习。提示收银台的搜索建议使用keyup事件加防抖处理。用户每敲一个字符就发一次Ajax请求前端交互会很卡数据库也吃不消。用setTimeout做一个300毫秒的延时最后一次输入结束后才发送请求体感顺畅很多。5. 常见问题排查与避坑手册5.1 数据库连接与事务相关问题问题现象一启动Tomcat时报错The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。这是数据库连接URL没指定serverTimezone导致的。MySQL 8.0的驱动对时区校验很严格按前面说的URL里加上serverTimezoneAsia/Shanghai即可解决。问题现象二Invalid bound statement (not found)。这个报错说明MyBatis的Mapper接口和XML文件没绑定上。逐一检查XML文件的包路径是否和Mapper接口的包路径一致、XML文件名是否和接口名一致、spring-mybatis.xml里是否配置了mapperLocations指向classpath:mapper/*.xml。这三处都对上问题基本消失。问题现象三加了Transactional但是数据没回滚。最常见原因是没有引入tx:annotation-driven transaction-managertransactionManager/配置或者Service实现类里有内部方法调用this.otherMethod()导致AOP代理失效。把事务方法改成外部调用用注入的Service对象去调用事务就生效了。5.2 Maven依赖冲突与构建问题问题现象启动时Jackson相关类报NoClassDefFoundError。这种报错十有八九是版本冲突引入的。SpringMVC 4.x和Spring 4.x自带的Jackson版本是2.x但有可能你的pom里又引入了jackson-databind的1.x版本产生两套Jackson并存。处理方式是打开IDEA的Maven Helper插件在Dependency Analyzer里搜索jackson看依赖树里是否存在多个版本把不用的exclusion掉。问题现象Failed to execute goal clean install之类的Maven构建失败。多半是单元测试里连接不上数据库。像SSM项目里如果有MyBatis的Mapper测试又没配置测试环境数据源mvn package就会被测试卡住。可以在pom.xml里跳过测试打包plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId configuration skipTeststrue/skipTests /configuration /plugin5.3 中文乱码问题的全面排查中文乱码有四个来源需要逐一排查数据库编码建库时就必须用utf8mb4CREATE DATABASE drugstore DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;。JDBC连接编码URL里要有characterEncodingutf8。IDEA项目编码Settings → Editor → File EncodingsGlobal Encoding、Project Encoding、Properties Files都改成UTF-8。Tomcat连接请求编码在web.xml里配置字符编码过滤器强制把请求编码转成UTF-8。filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param force-encodingtrue/force-encoding /filter这四处全对齐中文基本不会乱码。5.4 运维部署时Tomcat端口占用一个很日常的问题IDEA里启动Tomcat时端口被占用Port 8080 was already in use。这是因为上一个Tomcat进程没被完整杀掉尤其在Windows上特别常见。打开任务管理器找到java.exe的进程结束掉或者在命令行执行netstat -ano | findstr 8080查看占用端口的PID然后taskkill /F /PID pid。如果项目经常跑多种服务端口冲突是常态建议在server.xml里把Tomcat的HTTP端口改成8081或自定义端口避免和其他开发组抢资源。6. 项目总结与经验复盘这个药店收银管理系统做下来最有含金量的收获不是某个框架的API背得多熟而是你把一条完整的数据链路彻底打通了页面上的一个点击动作生成一个Layui表格的请求经过SpringMVC的Controller接收参数、Service层处理业务逻辑、MyBatis执行SQL、MySQL返回数据再一层层解析渲染回页面。整个过程你亲手搭建、亲手调试、亲手填坑过一遍对JavaWeb的理解会远超想象。就我个人的实操体会来看手术刀式的精细学习比大而全更有用。你不用急着去追SpringBoot或者微服务那一套先把SSM的每个环节吃透明白他们各自负责什么、配置文件里每一行代表什么后面的路子会顺很多。尤其是事务边界、动态SQL、Maven依赖管理这些基础能力在任何技术栈里都能复用。希望这篇接近实战的记录能帮你把项目跑起来也能帮你把其中的原理看明白。