资讯动态

SSM在线收银系统实战解析:从架构到事务管理

发布时间:2026/10/17 14:54:31 来源:尧图企业网站定制
简介在线收银系统是一套基于Java SSM框架的毕业设计完整源码包面向小型零售企业的收银与销售管理场景适用于计算机专业学生进行毕业设计、课程设计或Java Web开发进阶学习。压缩包约23.97MB内部以Java源码、SSM配置文件、MyBatis映射、MySQL数据库脚本以及说明文档和LW论文为主代码结构清晰、注释完整可直接导入Eclipse或IDEA配合Tomcat7运行。目前已有78人学习/下载可作为毕业设计或课程设计的直接参考。系统功能覆盖系统用户管理、员工管理、用户管理、商品类别与商品管理、入库管理、销售管理和销售统计等完整业务闭环并配有详细说明文档通过学习这套源码能够深入理解SSM框架整合、MyBatis数据持久层、订单与库存业务逻辑处理等关键知识点掌握从数据库设计到Web层调用的完整开发思路也可为毕业设计答辩提供完整方案适合需要快速搭建管理类项目、参考商业级代码结构的开发者使用。1. 在线收银系统一个把 SSM 底层操作全部串起来的实战型毕设在超市或者奶茶店的收银台前收银员点几下鼠标就能完成扫码、算价、收款、出小票这一串动作。把这串动作原样搬到浏览器里就是我要拆的这套在线收银系统。它用 SSMSpring SpringMVC MyBatis和 MySQL 组合实现覆盖商品管理、购物车收银、订单入库、会员积分、登录权限这类门店收银全流程适合正在找 java 毕业设计、课程设计源码的同学也适合想通过一个完整项目彻底弄懂 SSM 整合过程的人。先说一个反直觉的结论这套系统在功能上并不炫它的价值在完整两个字。它没有堆中间件而是把一个真实收银场景做闭环了——前台能下单后台能做商品上下架和订单管理压缩包里还带着说明文档和论文LW需求分析、数据库设计、流程图全都有落点。比起那种只贴五个增删改查页面的凑数项目它真正把 SSM 的考点串了进去三层架构、声明式事务、MyBatis 动态 SQL、拦截器权限控制。拆完代码我的感受是这类项目最怕的不是功能少而是代码糊成一团。这套系统分层很规矩Controller 只做参数接收和页面跳转Service 层控制事务Mapper 层写 SQL。顺着代码往下读一次收银从点击按钮到扣库存的完整链路很快就能理清这也正好是毕业答辩时最好讲的地方。2. 技术选型与整体架构SSM MySQL 的经典分层是怎么搭起来的2.1 技术栈拆解为什么选 SSM 而不是 Spring Boot打开压缩包里的 pom.xml第一印象就是熟——spring-webmvc、mybatis、mybatis-spring、mysql-connector-java再加一个 druid 连接池这就是一个标准的 SSM 工程。Spring 负责管理对象和事务SpringMVC 负责接收请求和路由MyBatis 负责和 MySQL 打交道。前端页面用 JSP 加 jQuery表格渲染常见的是 layui这个组合在近几年的毕业设计里出现频率非常高。很多人会问现在新项目不都用 Spring Boot 吗为什么毕设还选 SSM我的看法是SSM 需要你亲手写 spring-mvc.xml、spring-mybatis.xml、web.xml 这些配置文件恰恰能展示你对框架底层的理解。Spring Boot 把一切自动配置了答辩时老师一句你讲讲自动配置原理很多同学就卡住了。而 SSM 的配置是透明的每个 bean、每条扫描规则都在 xml 里摆着讲起来有据可依。版本搭配上这类工程最稳的组合是 JDK 1.8 Maven 3.6 Tomcat 8.5 MySQL 5.7Spring 4.3 或 5.xMyBatis 3.4 左右。这套组合的兼容性最好网上资料也全出问题好搜。如果非要用 JDK 17 跑大概率会在启动阶段遇到反射相关的报错后面避坑部分会具体说。我一般建议直接按这个组合搭环境省下的时间足够把代码读一遍。2.2 项目目录结构从 Controller 到 Mapper 的分层地图拿到源码后先别急着跑花十分钟把目录结构过一遍。这是一个标准的 Maven 工程代码按 controller、service、mapper、entity、interceptor、utils 分层逻辑非常清晰online-pos ├── pom.xml ├── sql │ └── pos.sql └── src/main ├── java │ └── com/xxx/pos │ ├── controller │ │ ├── LoginController.java │ │ ├── GoodsController.java │ │ ├── OrderController.java │ │ └── MemberController.java │ ├── service │ │ ├── GoodsService.java │ │ ├── OrderService.java │ │ └── impl │ │ ├── GoodsServiceImpl.java │ │ └── OrderServiceImpl.java │ ├── mapper │ │ ├── GoodsMapper.java │ │ ├── OrderMapper.java │ │ └── MemberMapper.java │ ├── entity │ │ ├── Goods.java │ │ ├── Order.java │ │ └── Member.java │ ├── interceptor │ │ └── LoginInterceptor.java │ └── utils │ └── OrderNoGenerator.java ├── resources │ ├── jdbc.properties │ ├── spring-mybatis.xml │ ├── spring-mvc.xml │ └── mapper │ ├── GoodsMapper.xml │ └── OrderMapper.xml └── webapp ├── index.jsp ├── static │ ├── layui │ └── js └── WEB-INF └── views ├── login.jsp ├── goods_list.jsp └── order_list.jsp这个分层其实就是 SSM 的教科书结构Controller 层接收前端参数调用 Service 接口ServiceImpl 里写业务逻辑和事务注解Mapper 接口对应 resources/mapper 下的 XML 文件SQL 不会散落在 Java 代码里。改业务的时候从上往下找定位效率很高。有两个细节值得注意。第一entity 里的字段和数据库列是驼峰映射的MyBatis 的 mapUnderscoreToCamelCase 一般在 spring-mybatis.xml 里开启这样 order_no 能自动映射到 orderNo不用每个字段写 resultMap省不少事。第二JSP 放在 WEB-INF/views 下面浏览器不能直接访问只能通过 Controller 的 return goods_list 这种方式转发进去这是一个经典的 MVC 隔离做法也避免别人绕过后端直接打开页面。2.3 数据库设计订单、商品、会员这几张核心表怎么建模sql 目录下的初始化脚本会把库和表都建好。收银系统的核心表我拆到的主要是这几张用户表 user登录和角色控制、商品分类表 goods_type、商品表 goods、订单主表脚本里一般命名为 t_order避免 order 这个保留字、订单明细表 t_order_detail、会员表 member余额和积分。表不多但关系很清晰一张订单对应多条明细一个会员拥有多张订单商品挂在分类下面。先看商品表CREATE TABLE goods ( id int(11) NOT NULL AUTO_INCREMENT, barcode varchar(32) DEFAULT NULL COMMENT 条码, name varchar(100) NOT NULL COMMENT 商品名称, type_id int(11) DEFAULT NULL COMMENT 分类ID, purchase_price decimal(10,2) DEFAULT NULL COMMENT 进价, sale_price decimal(10,2) NOT NULL COMMENT 售价, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8;商品表有几个设计讲究。金额字段全部用 decimal不用 float 和 double这是血泪教训——浮点数在累加和比较时会出现 0.1 0.2 ! 0.3 的问题收银系统天天和钱打交道金额精度错了会直接在答辩时被老师问住。库存用 int状态用 tinyint 做上下架标记下架的商品在收银台前端搜不到但后台还能看到记录这比直接删数据合理。订单主表和明细表是收银系统的核心。一次收银产生一条主表记录存订单号、总额、实收、优惠、积分明细表存每一件商品的数量和单价CREATE TABLE t_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, cashier_id int(11) DEFAULT NULL COMMENT 收银员ID, member_id int(11) DEFAULT NULL COMMENT 会员ID, total_amount decimal(10,2) NOT NULL COMMENT 商品总额, discount_amount decimal(10,2) DEFAULT 0.00 COMMENT 优惠金额, pay_amount decimal(10,2) NOT NULL COMMENT 实收金额, points int(11) DEFAULT 0 COMMENT 本次获得积分, create_time datetime DEFAULT NULL COMMENT 下单时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8; CREATE TABLE t_order_detail ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id bigint(20) NOT NULL COMMENT 订单ID, goods_id int(11) NOT NULL COMMENT 商品ID, quantity int(11) NOT NULL COMMENT 数量, price decimal(10,2) NOT NULL COMMENT 成交单价, subtotal decimal(10,2) NOT NULL COMMENT 小计, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8;这里有个细节表名避开了 order 这个 MySQL 保留字用了 t_order 前缀。很多初学同学建表直接用 order查询、排序、group by 都有可能报 SQL 语法错误最后只能用反引号硬扛。订单号加唯一索引是因为它是用户查单和退单的关键凭据不能重复。至于订单明细为什么不直接塞进主表——一张订单可能买十几件商品主表和明细分开主表保持轻量统计销售额时 group by create_time 也更快。3. 核心模块拆解从登录拦截到订单入库的完整链路这一章按真实业务链路走一遍登录鉴权、商品浏览、收银下单、会员积分每个环节都是系统里一个不可跳过的关卡。3.1 登录认证与权限拦截收银员和管理员的访问边界系统里有两类用户管理员管商品和订单收银员只负责在前台点单收银。如果收银员直接访问后台管理 URL就需要拦截器把这层权限挡下来。项目里用 SpringMVC 的 HandlerInterceptorAdapter 实现登录和角色校验public class LoginInterceptor extends HandlerInterceptorAdapter { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { // 未登录的请求统一踢回登录页 response.sendRedirect(request.getContextPath() /login); return false; } String uri request.getRequestURI(); // 收银员角色不能访问 /admin/** 的后台管理接口 if (cashier.equals(user.getRole()) uri.contains(/admin/)) { request.setAttribute(msg, 权限不足请联系管理员); request.getRequestDispatcher(/WEB-INF/views/error.jsp).forward(request, response); return false; } return true; } }这段拦截器的逻辑很直白先看 session 里有没有登录用户没有就重定向到登录页有用户再看角色收银员碰到管理员专属路径就转发到错误页。注意重定向和转发的区别——sendRedirect 会让浏览器重新发起一次请求request 里的提示信息会丢forward 是服务端内部跳转能把 msg 带到目标页面所以权限不足的提示必须用 forward。拦截器要在 spring-mvc.xml 里注册并且必须给静态资源放行否则 layui 的 css、js 全被拦下来mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/static/**/ mvc:interceptor classcom.xxx.pos.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptorsmvc:mapping 配置拦截范围mvc:exclude-mapping 放行登录接口和静态资源。这个配置漏了 static 路径页面样式会全部崩掉只剩光秃秃的 HTML。登录接口也要放行不然用户连登录页都打不开。3.2 商品管理分页查询与上下架的 MyBatis 实现商品列表是后台最常用的功能查询条件多名称模糊、分类、状态还要分页。MyBatis 的动态 SQL 在这里体现得非常典型select idqueryGoodsList resultTypecom.xxx.pos.entity.Goods SELECT g.id, g.name, g.barcode, g.sale_price, g.stock, g.status, gt.name AS type_name FROM goods g LEFT JOIN goods_type gt ON g.type_id gt.id where if testname ! null and name ! AND g.name LIKE CONCAT(%, #{name}, %) /if if testtypeId ! null and typeId ! 0 AND g.type_id #{typeId} /if if teststatus ! null AND g.status #{status} /if /where ORDER BY g.id DESC LIMIT #{offset}, #{pageSize} /selectwhere 标签会自动处理开头的 AND条件不满足时整个 where 不生成查询退化成全表分页。LIMIT 后面的 offset 是起始行pageSize 是每页条数前端传页码 pageNum 时Controller 里要算 offset (pageNum - 1) * pageSize这个计算经常有人漏。配合分页还要写一个 count 查询拿总条数否则前端渲染不出总页码。上下架功能更简单一个 update 把 status 在 0 和 1 之间切换前台收银界面只查 status 1 的商品。这个设计比直接删除商品聪明既保留了下架商品的历史订单数据又不影响在售商品的搜索体验。就算要清空商品数据也应该用逻辑删除而不是物理删除。3.3 收银下单订单主表与明细表如何在同一个事务里写入收银是这个项目的核心场景。前端把购物车里的商品 ID 和数量拼成 JSON 提交到后端OrderServiceImpl 用一个方法完成生成订单号、写主表、写明细、扣库存、累计积分。这五个动作任何一步失败前面写的订单数据都不能留下来所以方法上挂了 TransactionalOverride Transactional(rollbackFor Exception.class) public void createOrder(OrderVO vo) { BigDecimal total BigDecimal.ZERO; // 1. 遍历购物车校验商品并计算总额 ListOrderItemVO items vo.getItems(); for (OrderItemVO item : items) { Goods goods goodsMapper.selectById(item.getGoodsId()); if (goods null || goods.getStatus() ! 1) { throw new RuntimeException(商品不存在或已下架 item.getGoodsId()); } item.setPrice(goods.getSalePrice()); BigDecimal subtotal goods.getSalePrice().multiply(BigDecimal.valueOf(item.getQuantity())); item.setSubtotal(subtotal); total total.add(subtotal); } // 2. 生成订单号插入主表拿到自增ID Order order new Order(); order.setOrderNo(OrderNoGenerator.next()); order.setTotalAmount(total); order.setPayAmount(vo.getPayAmount()); order.setMemberId(vo.getMemberId()); orderMapper.insert(order); // 3. 写明细并扣减库存 for (OrderItemVO item : items) { OrderDetail detail new OrderDetail(); detail.setOrderId(order.getId()); detail.setGoodsId(item.getGoodsId()); detail.setQuantity(item.getQuantity()); detail.setPrice(item.getPrice()); detail.setSubtotal(item.getSubtotal()); orderDetailMapper.insert(detail); goodsMapper.reduceStock(item.getGoodsId(), item.getQuantity()); } // 4. 给会员累计积分 if (vo.getMemberId() ! null) { memberMapper.addPoints(vo.getMemberId(), total.intValue()); } }这段代码的关键在第 3 步之前插入主表后必须立刻拿到自增主键明细表才能通过 order_id 关联到正确的订单。MyBatis 里常见做法是在 mapper.xml 配置 useGeneratedKeysinsert idinsert useGeneratedKeystrue keyPropertyid INSERT INTO t_order (order_no, total_amount, pay_amount, member_id, create_time) VALUES (#{orderNo}, #{totalAmount}, #{payAmount}, #{memberId}, NOW()) /insertuseGeneratedKeys 表示使用数据库自增主键插入后自动回填到 Order 对象的 id 属性后续业务直接用 order.getId() 就行。比另一种 getLastInsertId 的写法更严谨——后者在并发高的场景下可能拿到别的线程刚插入的 ID。收银台订单密集这个细节很容易翻车。3.4 会员积分一次消费怎么联动会员余额和积分会员在收银台被识别后下单成功要自动加积分。积分更新的 SQL 是典型的直接量操作update idaddPoints UPDATE member SET points points #{points} WHERE id #{memberId} /update注意这里写的是 points points #{points}不是先 SELECT 出来加完再 UPDATE 回去后者在并发下会丢更新。SQL 层面直接做加减天然避免了竞态问题。同理会员储值卡扣款也是同一套路不过扣款多了一个校验条件update iddeductBalance UPDATE member SET balance balance - #{amount} WHERE id #{memberId} AND balance gt; #{amount} /update执行这条 SQL 后如果受影响行数是 0说明余额不足这笔操作要抛异常回滚前面的订单、明细、积分全部撤销。很多同学习惯在 Java 里先 SELECT 余额再比较单用户测试时没毛病但并发扣款时两个请求都读到了够用的余额最终余额就变成负数。把余额检验下推到数据库的 WHERE 条件里才是这类资金场景的标准姿势。4. 本地跑通全流程从导入 SQL 到 IDEA 启动的踩坑记录拆项目最重要的是先让它跑起来。这一章我用完整流程讲清楚从拿到压缩包到浏览器里出现登录页的每一步每一步都标注我踩过的坑。4.1 环境准备与版本搭配先把 JDK 和数据库版本对齐第一次跑 SSM 项目环境版本不对是最大的翻车点。推荐直接用这个组合JDK 1.8、Maven 3.6.x、Tomcat 8.5、MySQL 5.7、IDEA社区版或旗舰版都行。这个组合是这类工程最常见的部署环境兼容性最好。特别提醒如果你本机装的是 JDK 17 或更高版本启动阶段很容易报错比如 ClassNotFoundException 或者各种反射异常因为老版本的 CGLIB 代理和 Tomcat 对高版本 Java 支持不完整。我一般会在机器上同时装一个 JDK 8IDEA 里为这个项目单独把 Project SDK 和编译器都切成 8这样最省事。MySQL 建议用 5.7。如果你只有 8.x 也可以用但连接驱动和连接串要改对这个细节在第 4.4 节单独说。4.2 导入项目与修改配置三处必改的配置项把压缩包解压后里面是一个完整的 Maven 工程目录。IDEA 里 File - New - Project from Existing Sources选中 pom.xmlImport Maven等依赖下载完。首次下载依赖可能很慢这是 Maven 中央仓库的访问速度问题跟项目本身没关系。等待期间先把三处配置改好。第一处是资源目录下的 jdbc.properties这是系统能不能连上数据库的关键jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/pos?useUnicodetruecharacterEncodingutf8useSSLfalse jdbc.usernameroot jdbc.password123456jdbc.url 里pos 是数据库名要用初始化脚本建好的库名保持一致characterEncodingutf8 是中文不乱码的前提useSSLfalse 是避免 MySQL 8 连接时告警或报错。第二处是 Maven 的远程仓库配置。IDEA 用的 Maven 如果没配国内镜像源依赖可能下到一半就断。常见做法是在 Maven 的 settings.xml 里配置一个国内仓库镜像源把 mirrorOf 指向 central。第三处是 Tomcat 配置。IDEA 里 Run - Edit Configurations新建 Tomcat Server - LocalDeployment 里把项目 artifact 添加进去Application context 填 /pos这里填什么决定了访问入口。建议统一填项目名 /pos避免和别人的访问路径混淆。三处改完先别急着启动把数据库脚本先跑了。用命令行或数据库客户端执行 sql 目录下的 pos.sql确认建库建表和初始化数据都正常。注意连接 URL 里的库名和脚本里 CREATE DATABASE 的库名必须一致这也是常见的连接失败原因。4.3 启动与验证从登录页到第一笔订单配置完成后启动 Tomcat控制台出现启动成功的日志后浏览器访问 http://localhost:8080/pos/login。看到收银系统登录界面第一步就成了。登录页一般有一个初始化管理员账号通常写在说明文档里比如 admin / admin。登录后先进商品管理检查列表能否正常分页新建一个测试商品把价格填好后去收银台搜索这个商品加购物车点结账。这套流程完整走通说明数据库读写、前端静态资源、Controller 跳转、事务回滚全链路都是通的。如果商品列表能打开但收银台页面样式乱了八成是静态资源被拦截器拦了回 3.1 节检查 mvc:exclude-mapping。如果下单成功但订单列表找不到记录优先查订单主表和明细表的 ID 关联有没有配 useGeneratedKeys。验证这一步值得多花几分钟走完整流程而不是登录成功就收工。登录页能打开只说明入口通真正决定项目质量的是下单这个核心事务链路能不能完整走通。4.4 避坑SSM 老项目启动最常见的 5 个问题这里整理的都是跑 SSM 老项目时反复出现的问题按现象、原因、解决三段式记录碰到类似报错直接对号入座。第一个坑Tomcat 启动成功但访问页面 404。现象是启动日志正常浏览器访问却 404。原因基本是 URL 没带 context path或者 IDEA 部署时没把 artifact 挂到 Tomcat 上。解决检查访问地址是不是 http://localhost:8080/pos/xxx再看 Deployment 里有没有添加项目 artifact没有就补上并确认 Application context。第二个坑ClassNotFoundException: com.mysql.jdbc.Driver。现象很直接数据库驱动类找不到。原因是本机 MySQL 是 8.x连接串里却用老版驱动类名。解决8.x 改成 com.mysql.cj.jdbc.Driver并在 URL 里加 useSSLfalse 和 serverTimezoneAsia/Shanghai时区不指定一样会报连接错误。第三个坑Maven 依赖下载卡死。现象是 IDEA 一直停留在 Downloading进度条不动。原因是默认中央仓库在境外网络不稳。解决在 settings.xml 配国内镜像源然后让 IDEA 重新导入 Maven。第四个坑启动报 BeanCreationException。现象是 Spring 容器创建某个 bean 失败错误堆栈里能看到 CGLIB 或 Enhancer 字样。原因多半是 JDK 版本太高老 Spring 的代理库在高版本 Java 下不兼容。解决把项目 SDK 切到 JDK 8同时确认 Maven 编译 target 也是 8。第五个坑页面中文乱码。现象是商品名称、会员姓名在页面上显示成问号。原因有两层MySQL 连接串少了 characterEncodingutf8或者 JSP 页面头部没声明 UTF-8。解决连接串补上参数同时确认 spring-mvc.xml 里配置了 CharacterEncodingFilter两个位置都改对乱码才会彻底消失。提示报错日志如果出现多行堆栈先看最上面的 Caused by 那一行真正根因基本都在最底层别被中间大段堆栈带偏。5. 核心代码精读Service 事务与 MyBatis 动态 SQL 的边界把握第 3 章偏重业务链路这一章单独把三个最容易考、也最容易出问题的技术点拿出来精读事务边界、动态 SQL 拼接、前端 JSON 交互。5.1 OrderService 的下单事务边界往往就是方法边界Transactional 放在 createOrder 方法上作用范围就是方法体内所有数据库操作。Spring 的声明式事务默认行为方法进入时开启事务正常返回时提交方法内部抛出没有被捕获的异常时回滚。但有个关键细节——默认只回滚 RuntimeException如果业务抛出的是受检异常比如直接 throws Exception事务不会回滚。所以在下单方法上写 rollbackFor Exception.class是给所有异常兜底。另一个更容易翻车的点同类内自调用导致事务失效。比如你在追加功能时写出这样一段Service public class OrderServiceImpl implements OrderService { Override public void pay(OrderVO vo) { // 内部调用同类方法this 不是代理对象 this.createOrder(vo); } Transactional(rollbackFor Exception.class) public void createOrder(OrderVO vo) { // 事务操作 } }这个场景在给老项目加功能时非常容易遇到。pay 方法调用了 this.createOrder()但 this 是对象本身不是 Spring 生成的代理对象createOrder 上的 Transactional 完全没生效。表现就是下单过程中库存扣减失败订单数据却留在了库里产生脏数据。正确做法是把需要事务的方法放到另一个被 Spring 管理的 bean 里或者把 pay 的逻辑合并进 createOrder。判断依据很简单这个事务方法是不是通过注入的代理对象来调用的。如果写的是 this.xxx()事务大概率就没生效。另外一个边界很容易被忽略事务方法不能是 private。Spring AOP 基于代理private 方法根本不会被代理织入class 也不能是 final。这些限制在 SSM 项目里哪怕不报错也会在数据不一致的时候坑你一次。5.2 MyBatis 动态 SQLwhere、set、foreach 的边界动态 SQL 是 MyBatis 的灵魂也是面试高频点。除了 3.2 节的分页查询下单里批量插入订单明细是最需要 foreach 的地方。假设购物车里有五件商品每次循环单条 insert 就是五次网络往返性能差代码也难看。用 foreach 一次拼成一条批量插入insert idbatchInsertDetail INSERT INTO t_order_detail (order_id, goods_id, quantity, price, subtotal) VALUES foreach collectiondetailList itemitem separator, (#{item.orderId}, #{item.goodsId}, #{item.quantity}, #{item.price}, #{item.subtotal}) /foreach /insertforeach 的 collection 对应 Mapper 接口方法里 List 参数标注的 Param 名称item 是循环变量名separator 是每条记录之间的分隔符这里必须用逗号。注意collection 值要跟 Param(detailList) 一致不能凭感觉写 list。批量插入 SQL 有长度上限受数据库 max_allowed_packet 参数影响常规收银几十条明细完全够用不用刻意改数据库配置。商品编辑场景则用到 set 标签。更新商品时前端可能只改了价格没改名称直接把整个实体覆盖更新会产生问题。set 标签能动态拼接要更新的字段并自动去掉末尾逗号update idupdateGoods UPDATE goods set if testname ! nullname #{name},/if if testsalePrice ! nullsale_price #{salePrice},/if if teststock ! nullstock #{stock},/if /set WHERE id #{id} /update这里每个 if 分支末尾都带逗号set 标签会在最后一个有效条件那里把多余逗号去掉避免生成 SET name ? 这种语法错误。跟 where 标签的 AND 处理是同一个思路——动态 SQL 的核心就是让 MyBatis 帮你处理拼接边界的脏活。再看库存扣减。这段 SQL 本身不复杂但条件值得留意update idreduceStock UPDATE goods SET stock stock - #{quantity} WHERE id #{goodsId} AND stock #{quantity} /updateUPDATE 语句里用 stock #{quantity} 作为条件把库存校验和扣减合并成一步。Mapper 接口方法的返回值是 intService 层拿到返回值为 0 就说明库存不足抛异常回滚整个订单。这是并发减库存的常见写法不用写 SELECT FOR UPDATE也没有先查后改的竞态窗口。很多项目在 Java 里先查库存再更新并发一旦上来库存就变负数属于典型的看起来对、实际会崩。5.3 Controller 与前端交互AJAX 提交 JSON 数据的正确姿势收银台页面的购物车操作是异步的下单接口通过 AJAX 把购物车数据以 JSON 格式发给后端。Controller 这边用 RequestBody 接收RequestMapping(/order/create) ResponseBody public Result createOrder(RequestBody OrderVO vo) { orderService.createOrder(vo); return Result.success(下单成功); }RequestBody 把请求体里的 JSON 字符串反序列化成 OrderVO 对象OrderVO 里的 List items 字段会自动映射 JSON 数组。这里最容易出的问题前端用了表单序列化contentType 是 application/x-www-form-urlencoded数据是 keyvaluekeyvalue 格式RequestBody 根本收不到方法直接抛异常。前端正确写法是$.ajax({ url: /pos/order/create, type: POST, contentType: application/json, data: JSON.stringify({ memberId: memberId, items: [ { goodsId: 1, quantity: 2 }, { goodsId: 3, quantity: 1 } ] }), dataType: json, success: function (res) { if (res.code 0) { layer.msg(下单成功); // 清空购物车、刷新订单列表 } else { layer.msg(res.message); } } });两个参数是绑定的contentType 必须写 application/jsondata 必须是 JSON.stringify 序列化后的字符串少一个都不行。JSON 字段名要和 OrderVO 属性名一致Java 里是 items前端就不能写成 itemList否则映射出来的 List 是空的下单后明细表一条记录都没有。收银端的成功回调还要顺手清空购物车并刷新订单列表否则用户会重复提交订单。三个点其实是层层递进的事务控制保证数据可靠性动态 SQL 保证数据库操作高效正确JSON 交互保证前后端数据对得上。把这三处吃透整个项目的核心代码基本就掌握了大半。6. 二次开发指南把收银系统改成你想要的毕设题目拿到一套能跑的源码只是第一步怎么把它改成自己的东西才是毕业设计加分的开始。基于这套系统的结构我给出三个改造方向按投入产出比排序。6.1 加一个日结报表模块用 ECharts 展示销售额趋势在 OrderController 里加一个统计接口按天聚合订单实收金额select idcountDailySale resultTypemap SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS sale_date, SUM(pay_amount) AS amount FROM t_order WHERE create_time #{startDate} AND create_time lt; #{endDate} GROUP BY DATE_FORMAT(create_time, %Y-%m-%d) ORDER BY sale_date /selectDATE_FORMAT 把订单时间归整到天SUM 聚合当天实收GROUP BY 产出每一天的销售额。前端接一个 ECharts 折线图几行代码就能把报表页做出来。这个模块工作量小但能显著提升项目完整度销售数据可视化在答辩时是很亮的加分项。6.2 换一个业务场景从超市收银改成奶茶店点单如果不想只是做一个通用收银系统可以给商品加规格字段比如中杯、大杯、加珍珠订单表加一个桌号或取餐号字段再把商品分类从饮料、零食改成奶茶、果茶、小食。改动集中在数据库加字段和前端页面文案核心的订单事务、库存扣减完全不用重写。这也是这套系统最值得利用的地方——骨架是通用的业务语义换一换就是新题目。6.3 答辩导向的改造顺序建议先跑通原版再做两个小改造然后准备一个你最有把握讲清楚的功能点。在这个项目里我最推荐重点准备下单事务链路从 AJAX 提交 JSON、到 Transactional 控制事务、再到 MyBatis 动态 SQL 扣库存最后会员积分累积这是一条完整且有深度的链路拆成两分钟讲稿比讲十个页面效果好十倍。我拆这类 SSM 老项目时养成一个习惯动手加功能前先把关联表的唯一约束和事务边界列一遍再动代码。这样一来像扫码枪重复提交、并发扣库存这类问题基本能在设计阶段避开不用上线后再返工。希望这个习惯对你跑通和改造这套系统也有帮助。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑