
简介一份基于SSMMySQL的在线收银系统毕业设计源码包面向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 为什么是 SSMSpring 管对象、SpringMVC 管请求、MyBatis 管 SQL收银系统的核心诉求是「把一次按键操作变成一串稳定的数据库变更」。用户在收银台点一下「结算」背后涉及商品价格读取、库存校验、订单记录、明细拆分、支付状态更新这一串操作里任何一步失败都不能让系统出现「订单生成了一半」这种脏数据。Spring 的声明式事务正好能把这串操作包在一个事务里任何一个环节报错就整体回滚这是你在答辩时最能讲出深度的点。SpringMVC 管的是前端页面和后台接口之间的映射。收银台页面提交一个结算请求DispatcherServlet 根据 URL 找到对应的 Controller 方法把请求参数自动封装成对象再返回逻辑视图名。MyBatis 则把 SQL 从业务逻辑里拆出来写在 Mapper XML 里——这比 Hibernate 的全自动 ORM 更可控因为复杂报表查询比如按时间段统计营业额你可以直接写原生 SQL 优化而不是绕圈子拼 HQL。这套组合的另一个优点是「面试八股文里全是它」IoC、AOP、动态代理、#{} 与 ${} 的区别这些你答辩前准备的问题代码里全都有对应实现。2.2 数据库设计六张表和一条完整的状态链收银系统的表结构不像电商那样动辄几十张表但麻雀虽小五脏俱全。原工程的 SQL 脚本里包含核心业务表我建议你拿到源码先把表结构理清楚不要急着跑起来。理表的过程就是你准备答辩「数据库设计」环节的过程。表名核心字段作用t_userid、username、password、role收银员登录与权限区分t_categoryid、name商品分类如饮品、主食t_productid、name、price、stock、category_id商品基本信息与库存t_cartid、product_id、quantity临时购物车记录t_orderid、order_no、total_price、status、pay_time订单主表t_order_itemid、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 层处理 ListCartItem 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(ListCartItem 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 里有一段很典型的whereif写法select idselectByCondition resultTypecom.example.entity.Product SELECT id, name, category_id, price, stock, status FROM t_product where if testcategoryId ! null and categoryId ! 0 AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if /where ORDER BY id DESC /selectwhere标签会自动把第一个多余的 AND 去掉if标签则实现「这个参数传了就拼条件没传就跳过」。注意gt;和lt;是 XML 里的转义写法直接用会让 XML 解析器报错。日报表统计则用到了 SQL 里的日期函数和分组聚合比如「统计今天每小时的营业额」核心 SQL 大致是select idselectHourlyReport resultTypemap SELECT HOUR(pay_time) AS hour, SUM(total_price) AS amount FROM t_order WHERE status 1 AND pay_time gt; #{startTime} AND pay_time lt; #{endTime} GROUP BY HOUR(pay_time) ORDER BY hour /select这种报表查询直接用 Mapper 返回ListMapString, 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.ProductMapperXML 也放在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 namemapperLocations valueclasspath: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.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/cashier?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password你的密码4.3 现象数据库建表成功但中文插入或显示乱码原因分析三层字符集没统一。数据库连接串里没指定 characterEncodingutf8表本身的字符集是 latin1或者 JSP 页面没设置 pageEncoding。三个环节任何一层不是 utf8中文就乱码。解决方案连接串加characterEncodingutf8建表语句里显式写DEFAULT CHARSETutf8mb4MySQL 8.0 推荐 utf8mb4能存 emoji 表情JSP 页面头部加% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-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 启动报Cant connect to local server through socket /tmp/mysql.sock原因分析这个报错通常出现在 Linux 环境下MySQL 服务没有启动或者 socket 文件路径被移动过。很多同学是跟着教程装了 MySQL 但没设置系统服务开机自启重启后就变成连不上。解决方案先检查服务状态Ubuntu 用systemctl status mysqlCentOS 用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.7jdbc 驱动改成 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 的 insert3在「饮料」下新增商品「可乐」单价 3.5商品列表中可乐显示分类名称商品表关联分类表查询4收银台搜索「可」加入 2 个可乐购物车数量为 2ProductMapper 关键字查询5点击结算生成订单状态为待支付OrderServiceImpl.createOrder6点击「确认收款」订单状态变已支付Status 从 0 变 17查看订单列表订单号码、总价 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); ListProduct list productMapper.selectByCondition(condition); PageInfoProduct pageInfo new PageInfo(list);PageHelper 会自动在 SQL 后面追加 LIMIT 子句而且不会拦截非查询方法。这是 Spring Boot 项目里最常用的分页方案提前用熟它以后做互联网项目能省很多事。第三处是把报表展示从纯表格改成 ECharts 图表。工程里后端已经返回了每个小时的营业额数据你在前端引入 ECharts 的 CDN把 Map 数据喂给折线图组件即可。这一步技术含量不高但视觉效果极佳答辩时评委看到图表直接认为你有真实开发经验。我每次帮人改这类毕设代码最后都会强制自己走一遍「查依赖 → 查连接串 → 查 XML 扫描路径 → 查状态流」这四步排查法。这套收银系统我拆过很多次你只要把第 4 章的那五个坑提前标记在笔记本上跑通它需要的调试时间不会超过一晚上。希望这份拆解能帮你把项目真正变成自己的东西——毕竟答辩时被问到「这段代码为什么这么写」你得答得上来才行。本文还有配套的精品资源点击获取