简介:一份基于SSM+MySQL的在线收银系统毕业设计源码包,面向Java Web学习者、高校学生和小型零售开发人员,适用于超市、便利店等门店日常收银和进销存管理。覆盖系统用户管理、员工管理、用户管理、商品类别与商品管理、入库管理、销售管理和销售统计等模块,权限分配、商品建档、库存入库、销售开单、数据统计均有完整实现,可同时服务毕业设计、课程设计和实际轻量部署。压缩包约23.97MB,包含Java后端源码、SSM框架配置、MySQL数据库脚本、说明文档和LW论文材料,代码结构规范、注释清晰,便于阅读和二次开发。已有78人学习。通过研读源码,可掌握SSM分层架构、MyBatis持久化、订单与统计业务流的编写方式,并结合说明文档完成从环境搭建到系统部署的完整流程,适合需要实战参考的开发者和学生。
1. 这套在线收银系统源码,值得你花两天时间拆一遍
如果你正在做 Java 方向的毕业设计或课程设计,大概率会被一个问题卡住:选什么项目才能同时满足“代码量够、技术栈常见、答辩时讲得清楚”这三个条件。很多同学一上来就奔着秒杀系统、电商中台这种大厂业务去,结果写到一半发现事务、并发、权限全在硬凑,最后只能靠复制粘贴撑过答辩。相比之下,SSM + MySQL 的在线收银系统反而是被低估的好选择——业务链路完整,从前台选品到后台订单统计一条线走完,该有的注解、映射、状态流转一个不缺,又能控制在两周内复现完毕。
这套收银系统源码的核心价值在于它把「SSM 到底怎么串起来」讲明白了。很多教程只教你单独用 Spring、SpringMVC、MyBatis,但真正落地时,三个框架的整合细节才是坑最密集的地方。而收银系统的场景又天然覆盖了商品管理、购物车、订单、支付状态、库存扣减这些高频模块,你把这些代码吃透,等于把 Java 面试里常问的 Spring 容器、MyBatis 动态 SQL、事务传播机制全部过了一遍。适合谁?适合正在准备毕设/课设的在校生,也适合刚工作想快速补一套完整 SSM 工程结构的初级开发。
2. SSM 整合为什么是收银系统的最优解:选型理由与数据库设计
2.1 为什么是 SSM:Spring 管对象、SpringMVC 管请求、MyBatis 管 SQL
收银系统的核心诉求是「把一次按键操作变成一串稳定的数据库变更」。用户在收银台点一下「结算」,背后涉及商品价格读取、库存校验、订单记录、明细拆分、支付状态更新,这一串操作里任何一步失败,都不能让系统出现「订单生成了一半」这种脏数据。Spring 的声明式事务正好能把这串操作包在一个事务里,任何一个环节报错就整体回滚,这是你在答辩时最能讲出深度的点。
SpringMVC 管的是前端页面和后台接口之间的映射。收银台页面提交一个结算请求,DispatcherServlet 根据 URL 找到对应的 Controller 方法,把请求参数自动封装成对象,再返回逻辑视图名。MyBatis 则把 SQL 从业务逻辑里拆出来,写在 Mapper XML 里——这比 Hibernate 的全自动 ORM 更可控,因为复杂报表查询(比如按时间段统计营业额)你可以直接写原生 SQL 优化,而不是绕圈子拼 HQL。这套组合的另一个优点是「面试八股文里全是它」:IoC、AOP、动态代理、#{} 与 ${} 的区别,这些你答辩前准备的问题,代码里全都有对应实现。
2.2 数据库设计:六张表和一条完整的状态链
收银系统的表结构不像电商那样动辄几十张表,但麻雀虽小五脏俱全。原工程的 SQL 脚本里包含核心业务表,我建议你拿到源码先把表结构理清楚,不要急着跑起来。理表的过程就是你准备答辩「数据库设计」环节的过程。
| 表名 | 核心字段 | 作用 |
|---|---|---|
| t_user | id、username、password、role | 收银员登录与权限区分 |
| t_category | id、name | 商品分类,如饮品、主食 |
| t_product | id、name、price、stock、category_id | 商品基本信息与库存 |
| t_cart | id、product_id、quantity | 临时购物车记录 |
| t_order | id、order_no、total_price、status、pay_time | 订单主表 |
| t_order_item | id、order_id、product_id、quantity、price | 订单明细快照 |
这里的核心设计在于 t_order 和 t_order_item 是主从表关系,订单记录的是「一次交易的总价和状态」,明细记录的是「这次交易里每一件商品的单价和数量」。为什么要拆两张表?因为商品表里的价格会被修改,但订单生成后,用户以什么价格买了什么,必须永久留存——所以明细表里要冗余一份 price 快照,而不是下单时去关联查询商品表当前价格。这个设计你在答辩时主动提出来,评委老师会认为你理解「历史数据不可变」这一原则。
状态流转是另一个值得展开的细节。订单状态我用整数常量表示,避免魔法值散落在代码里。0 表示待支付,1 表示已支付、待取货,2 表示已完成(已取货),3 表示已取消(超时未支付或收银员主动取消)。四态流转限制为:只能从 0 走到 1 或 3,从 1 走到 2,不允许跳转。在 Service 层更新状态时,先查询当前状态再判断是否能转向目标状态,而不是直接执行 update——这个校验逻辑是你的「后悔药」,能挡住大量并发情况下的状态错乱。
2.3 工程结构:按包拆职责,别把所有类堆在一起
拿到源码后先看 src 目录下的包结构。我拆过的 SSM 毕设项目,最容易出现的问题就是 Controller 里塞了 SQL、Service 形同虚设。这套收银源码的包划分比较规范,建议你按这个顺序去读:
- controller 包:只做参数接收和视图转发,不写任何业务逻辑
- service 包 + impl 子包:业务核心,事务注解全部加在 impl 实现类上
- dao 包(Mapper 接口):只声明方法,SQL 写在 resources 下的 mapper XML 里
- entity 包:对应数据库表字段的实体类
- common 包 / util 包:统一返回结果、时间工具、常量定义
读代码时先盯 service/impl 下的 OrderServiceImpl.java 和 CartServiceImpl.java,这两个是业务最厚的部分。Controller 反而可以快速扫过,因为它的逻辑基本是「接收参数 → 调用 service → 把结果塞进 ModelAndView」。工程里 github 上常见的分包方式是把 mapper 接口和 XML 分开放在 main/java 和 main/resources 两个目录,路径要保持一致,否则启动时 MyBatis 扫不到 XML 直接报Invalid bound statement (not found)。
3. 把收银流程跑通:从选品到订单生成的核心代码拆解
3.1 收银主流程:先看 Controller,再看 Service,别颠倒
用户在收银台点「结算」时,前端页面会发起一个 POST 请求到结算接口,下面这段代码就是整套系统里「含金量最高」的部分,你答辩时大概率会被点名讲解。先从 Controller 层看起:
@Controller @RequestMapping("/cashier") public class CashierController { @Autowired private OrderService orderService; // 结算入口:接收购物车里的商品数组,生成订单 @RequestMapping(value = "/checkout", method = RequestMethod.POST) public String checkout(@RequestParam("productId") Integer[] productIds, @RequestParam("quantity") Integer[] quantities, Model model) { // 把两个数组组装成购物车条目集合,传给 service 层处理 List<CartItem> items = new ArrayList<>(); for (int i = 0; i < productIds.length; i++) { CartItem item = new CartItem(); item.setProductId(productIds[i]); item.setQuantity(quantities[i]); items.add(item); } Order order = orderService.createOrder(items, getCurrentUserId()); model.addAttribute("order", order); return "cashier/order_result"; } }这段代码的核心逻辑很简单:把页面传过来的两个等长数组(商品 ID 数组和对应数量数组)拼成一个购物车条目列表,交给 Service 层。注意这里用@RequestParam("productId") Integer[]来接批量参数,对应前端页面里 checkbox 或循环 input 的 name 属性,如果你前端 input 的 name 写成了productIds,这里就接收不到值,会报 400 参数缺失错误。这是一个非常容易翻车的细节。
真正干活的在 OrderService 的实现类里,事务注解打在方法上,保证生成订单、扣库存、清购物车三步要么全成功要么全回滚:
@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private OrderItemMapper orderItemMapper; @Autowired private ProductMapper productMapper; @Autowired private CartMapper cartMapper; @Override @Transactional(rollbackFor = Exception.class) public Order createOrder(List<CartItem> items, Integer userId) { // 1. 计算订单总价,这里必须用 BigDecimal,用 double 会丢精度 BigDecimal totalPrice = new BigDecimal("0.00"); for (CartItem item : items) { Product product = productMapper.selectByPrimaryKey(item.getProductId()); // 2. 校验库存,防止超卖 if (product.getStock() < item.getQuantity()) { throw new RuntimeException("商品「" + product.getName() + "」库存不足"); } // 3. 单价快照:以商品表当前价格为准存入订单明细 BigDecimal subtotal = product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())); totalPrice = totalPrice.add(subtotal); } // 4. 生成订单主表记录,状态初始为 0(待支付) Order order = new Order(); order.setOrderNo(generateOrderNo()); // 时间戳 + 随机数,保证唯一 order.setUserId(userId); order.setTotalPrice(totalPrice); order.setStatus(0); order.setCreateTime(new Date()); orderMapper.insert(order); // 5. 逐条插入订单明细 for (CartItem item : items) { Product product = productMapper.selectByPrimaryKey(item.getProductId()); OrderItem orderItem = new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(product.getId()); orderItem.setProductName(product.getName()); orderItem.setPrice(product.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); // 6. 扣减库存 productMapper.reduceStock(product.getId(), item.getQuantity()); } // 7. 清空该用户的购物车 cartMapper.deleteByUserId(userId); return order; } }这段代码里有几个地方值得你在答辩或面试时主动展开。第一,为什么总价和单价都用 BigDecimal 而不是 double?因为 double 在二进制里无法精确表示 0.1,比如 0.1 + 0.2 的结果是 0.30000000000000004,放在金额上就是致命的精度错误。Java 面试题里但凡是金融场景,问 8 大基本类型陷阱,必考这个点。第二,@Transactional(rollbackFor = Exception.class)为什么要显式指定 rollbackFor?因为 Spring 默认只在遇到 RuntimeException 时才回滚,遇到受检异常不会回滚,如果你抛出去的是一个自定义受检异常,事务照样提交,脏数据就落库了。第三,库存扣减用的 SQL 是UPDATE t_product SET stock = stock - #{count} WHERE id = #{id} AND stock >= #{count},这一步把「判断库存够不够」和「扣减」放在一条 SQL 里,是防超卖的常用做法。
3.2 模拟支付与状态推进:不用接支付 SDK,但状态机要严谨
毕业设计场景下不需要真实接入微信支付或支付宝,工程里通常是「模拟支付」——收银台点「确认收款」,直接把订单状态从待支付改成已支付。但越简单的地方越容易出问题,状态更新必须带前置条件校验,这个习惯能让你以后接真实支付 SDK 时不踩坑:
@Override @Transactional public boolean payOrder(Integer orderId, Integer userId) { // 查询当前订单 Order order = orderMapper.selectByPrimaryKey(orderId); // 校验订单归属,防止别的收银员操作别人的单子 if (order == null || !order.getUserId().equals(userId)) { throw new RuntimeException("订单不存在或无权操作"); } // 状态机校验:只有待支付(0)才能支付,已支付或已取消再点支付就是重复操作 if (order.getStatus() != 0) { throw new RuntimeException("当前订单状态不可支付"); } // 执行状态更新 Order update = new Order(); update.setId(orderId); update.setStatus(1); update.setPayTime(new Date()); return orderMapper.updateByPrimaryKeySelective(update) > 0; }核心就一句话:更新状态前必须 select 出来看一眼现状,再决定能不能 set 新状态。很多毕设代码里直接UPDATE t_order SET status = 1 WHERE id = #{id},用户连点两次「确认收款」不会报错,但会重复触发两次支付回调——你把if (order.getStatus() != 0)这个校验写在职业生涯里第一个正式项目的接口里,能少挨不少骂。实际的代码里还有一层校验是时间:创建超过 30 分钟未支付的订单视为超时,超时订单不能支付,只能走取消接口。这个逻辑放在定时任务里扫描,还是放在支付时懒校验,两种方案都能讲出道理,工程里默认用的是懒校验,答辩时可以提一下优化方向。
3.3 商品管理与报表查询:MyBatis 动态 SQL 的练兵场
收银系统的后台功能主要是商品 CRUD 和订单统计。商品列表通常有筛选条件:按分类、按价格区间、按关键字搜索。这种「条件可选、数量不定」的查询,正好用到 MyBatis 动态 SQL。工程 mapper XML 里有一段很典型的<where>+<if>写法:
<select id="selectByCondition" resultType="com.example.entity.Product"> SELECT id, name, category_id, price, stock, status FROM t_product <where> <if test="categoryId != null and categoryId != 0"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> <if test="minPrice != null"> AND price >= #{minPrice} </if> <if test="maxPrice != null"> AND price <= #{maxPrice} </if> </where> ORDER BY id DESC </select><where>标签会自动把第一个多余的 AND 去掉,<if>标签则实现「这个参数传了就拼条件,没传就跳过」。注意>=和<=是 XML 里的转义写法,直接用>=会让 XML 解析器报错。日报表统计则用到了 SQL 里的日期函数和分组聚合,比如「统计今天每小时的营业额」,核心 SQL 大致是:
<select id="selectHourlyReport" resultType="map"> SELECT HOUR(pay_time) AS hour, SUM(total_price) AS amount FROM t_order WHERE status = 1 AND pay_time >= #{startTime} AND pay_time <= #{endTime} GROUP BY HOUR(pay_time) ORDER BY hour </select>这种报表查询直接用 Mapper 返回List<Map<String, Object>>,省去为临时结果创建实体类的工作。你在答辩时可以说:「这里没有用实体类接收,因为报表结构不固定,用 Map 更灵活,后续如果要扩展成图表数据,直接加工 Map 就行。」——这句话会显得你有真实项目经验,而不是只会照着模板敲 CRUD。
4. 避坑指南:SSM + MySQL 整合过程中的五个高频翻车点
4.1 现象:启动 Tomcat 时报Invalid bound statement (not found),Mapper 接口找不到对应的 SQL 语句
原因分析:Mapper 接口和 XML 文件没有放到同一个包路径下,或者 MyBatis 的 mapper-locations 配置没有指向 XML 所在目录。这是所有 SSM 新手第一个遇到的报错,本质是「接口编译成 class 后,运行时去 resources 里找 XML,按全限定名找,路径对不上就找不到」。
解决方案:一种做法是把 XML 和接口放在同一个包路径下,比如接口在com.example.dao.ProductMapper,XML 也放在com/example/dao/目录下。但 maven 默认编译时不会把 src/main/java 下的 XML 一起编译输出,需要修改 pom.xml 加一段资源配置,或者干脆把 XML 放到 src/main/resources/mapper 目录,然后在 Spring 配置里指定 mapper-locations 为classpath:mapper/*.xml。我习惯用后者,因为 resources 本来就是放资源文件的地方,不用改 pom。检查你的 spring-mybatis.xml 里是不是写着<property name="mapperLocations" value="classpath:mapper/*.xml"/>,再确认 XML 里的 namespace 是不是等于接口的全限定名。
4.2 现象:数据库连接报Public Key Retrieval is not allowed,或者时区错误The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized
原因分析:你用的是 MySQL 8.0 及以上版本,但 JDBC 驱动连接串里没有加 allowPublicKeyRetrieval 参数,或者没有设置 serverTimezone。MySQL 8.0 默认的认证插件是 caching_sha2_password,换一种说法是它要求客户端在首次连接时安全地获取公钥,而你的驱动没允许它这么做。
解决方案:改 jdbc.properties 连接串,把参数补全。注意 MySQL 8.0 要配 com.mysql.cj.jdbc.Driver 而不是旧版的 com.mysql.jdbc.Driver,如果驱动版本是 5.x,直接换成 mysql-connector-java 8.0.x。连接串写法是:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/cashier?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=你的密码4.3 现象:数据库建表成功,但中文插入或显示乱码
原因分析:三层字符集没统一。数据库连接串里没指定 characterEncoding=utf8,表本身的字符集是 latin1,或者 JSP 页面没设置 pageEncoding。三个环节任何一层不是 utf8,中文就乱码。
解决方案:连接串加characterEncoding=utf8,建表语句里显式写DEFAULT CHARSET=utf8mb4(MySQL 8.0 推荐 utf8mb4,能存 emoji 表情),JSP 页面头部加<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>。如果已经建表了,用ALTER TABLE t_product CONVERT TO CHARACTER SET utf8mb4;还可以救回来。这个顺序不要做反,先改连接串再改表,最后刷新页面。
4.4 现象:Tomcat 正常启动,但点任何请求都返回 404,页面找不到资源
原因分析:SpringMVC 配置里把静态资源给拦截了,或者说 Controller 的扫描路径和 JSP 在 WEB-INF 下的实际路径不一致。常见的坑是配置<url-pattern>/</url-pattern>时没有配置<mvc:default-servlet-handler/>,导致 CSS、JS、图片全部被 DispatcherServlet 拦住。
解决方案:spring-mvc.xml 里加上<mvc:default-servlet-handler/>和<mvc:resources mapping="/static/**" location="/static/"/>这两行。它相当于把「处理不了的请求」还给容器默认 servlet,静态资源就能正常加载了。
4.5 现象:MySQL 启动报Can't connect to local server through socket '/tmp/mysql.sock'
原因分析:这个报错通常出现在 Linux 环境下,MySQL 服务没有启动,或者 socket 文件路径被移动过。很多同学是跟着教程装了 MySQL 但没设置系统服务开机自启,重启后就变成连不上。
解决方案:先检查服务状态,Ubuntu 用systemctl status mysql,CentOS 用systemctl status mysqld或service mysqld status。如果显示 inactive,执行启动命令后再用mysql -uroot -p登录测试。遇到这个报错最容易造成「明明配置都对但连不上」的错觉,先验系统服务,再验 JDBC 配置,顺序不要搞反。
5. 从源码到可演示项目:一份拿来即用的运行验证清单
5.1 启动顺序与最低可用配置
跑这套系统前,环境最低配置是 JDK 1.8、Maven 3.6+、Tomcat 8.5、MySQL 5.7 或 8.0。MySQL 如果是 5.7,jdbc 驱动改成 5.1.47 版本即可,不用强上 8.0。启动的顺序是 MySQL → Tomcat → 浏览器访问登录页。原工程是 Maven 结构,导入 IDEA 后先执行mvn clean package -DskipTests打成 war 包丢进 webapps 能跑,但调试期建议在 IDEA 里配置 Tomcat 直接 Debug 启动。
# 如果你用命令行打包,在工程根目录执行 mvn clean package -DskipTests # 打包完成后 war 文件在 target 目录下,拷到 Tomcat 的 webapps 目录 cp target/cashier-0.0.1-SNAPSHOT.war /path/to/tomcat/webapps/cashier.war # 启动 Tomcat /path/to/tomcat/bin/startup.sh5.2 验证清单:从登录到报表,每步都过一遍
启动成功后,按下面这份清单走一遍,每步都对应一个业务功能。这一步的目的不是「能跑就行」,而是帮你理解页面操作背后调用了哪些接口,答辩时被问到「你这个收银系统流程是什么样的」,你就能从页面一路讲到 SQL 层:
| 步骤 | 操作 | 预期结果 | 背后逻辑 |
|---|---|---|---|
| 1 | 用 admin 账号登录 | 跳转到收银台主页 | 登录拦截器放行,session 保存用户 |
| 2 | 新增一个商品分类「饮料」 | 分类列表出现新条目 | t_category 的 insert |
| 3 | 在「饮料」下新增商品「可乐」,单价 3.5 | 商品列表中可乐显示分类名称 | 商品表关联分类表查询 |
| 4 | 收银台搜索「可」,加入 2 个可乐 | 购物车数量为 2 | ProductMapper 关键字查询 |
| 5 | 点击结算 | 生成订单,状态为待支付 | OrderServiceImpl.createOrder |
| 6 | 点击「确认收款」 | 订单状态变已支付 | Status 从 0 变 1 |
| 7 | 查看订单列表 | 订单号码、总价 7.00 | 明细表快照查询 |
| 8 | 查看营业额报表 | 今日该笔交易计入报表 | 聚合查询 group by hour |
这 8 步全部跑通并且能一边点一边说出对应代码的位置,你已经具备独立掌握这套源码的能力。第 5 步可以故意操作一次:把库存改成 1,然后下单 2 件,看系统是否报「库存不足」——这个「失败路径」比「成功路径」更有演示价值,答辩时主动演示一个能拦截异常的操作,分数不会低。
5.3 进阶改造:从「答辩合格」到「面试有加分」
这套源码满足毕设没问题,但如果你想让它变成简历上的亮点,有三处低成本改造,工作量都不大,但对面试杀伤力很大。
第一处是把模拟支付改成「更完善的状态推进」,为订单增加「退款」路径:已支付订单可以发起退款,状态从 1 走到了 4(已退款),并记录退款时间和操作人。这一步改的只是状态常量和 Service 层的逻辑,加上一个 update 语句。但你在简历里写「订单支持退款流程」就比「订单能支付」多了一层业务理解。
第二处是给商品列表查询加分页。现在的查询是ORDER BY id DESC一把梭,你引入 PageHelper 插件,只需要改两行代码:
// 在 Service 方法里,Mapper 查询前加一行 PageHelper.startPage(pageNum, pageSize); List<Product> list = productMapper.selectByCondition(condition); PageInfo<Product> pageInfo = new PageInfo<>(list);PageHelper 会自动在 SQL 后面追加 LIMIT 子句,而且不会拦截非查询方法。这是 Spring Boot 项目里最常用的分页方案,提前用熟它,以后做互联网项目能省很多事。
第三处是把报表展示从纯表格改成 ECharts 图表。工程里后端已经返回了每个小时的营业额数据,你在前端引入 ECharts 的 CDN,把 Map 数据喂给折线图组件即可。这一步技术含量不高,但视觉效果极佳,答辩时评委看到图表直接认为你有真实开发经验。
我每次帮人改这类毕设代码,最后都会强制自己走一遍「查依赖 → 查连接串 → 查 XML 扫描路径 → 查状态流」这四步排查法。这套收银系统我拆过很多次,你只要把第 4 章的那五个坑提前标记在笔记本上,跑通它需要的调试时间不会超过一晚上。希望这份拆解能帮你把项目真正变成自己的东西——毕竟答辩时被问到「这段代码为什么这么写」,你得答得上来才行。
本文还有配套的精品资源,点击获取