
我平时给学生改课程设计或者看毕业设计代码时遇到最多的项目类型就是校内超市收银管理系统这类JavaWeb管理系统但说句实在话能真正跑通、逻辑清楚、代码整洁的没有几个。今天我把这个基于SSMMavenBootstrapMySQLJSP的校内超市收银管理系统掰开揉碎讲清楚它不只是应付答辩的一个作业而是一个能完整覆盖商品管理、收银结算、库存扣减、销售统计的真实业务闭环。如果你正在找javaweb项目练手、或者正被mysql建表、maven配置、idea运行javaweb项目折腾到头大那这篇内容应该能替你省下不少时间。1. 为什么是这个技术组合项目设计与选型盘点1.1 系统到底解决了什么问题先搞清楚应用场景。校内超市和校外大商超不一样它的商品种类一般几百到上千客单价不高但客流量集中在下课时间段人工收银容易算错账月底盘点经常发现账实不符。更麻烦的是商品调价了要一张张改价签优惠活动想去掉也得手动算折扣。这套系统把这些痛点全部接住商品信息统一录入数据库、价格修改即时生效、收银台快速结算并自动累计销售额、库存低于阈值还能给出预警。一句话概括它把原先写在本子上的流水账变成了一台有记忆的收银机器。对学习者来说它又是一个麻雀虽小五脏俱全的JavaWeb完整案例前端有页面中间有Controller处理请求底层有MyBatis操作MySQL所有数据流是通的。1.2 SSM三件套各干各的活SSM由Spring、SpringMVC、MyBatis三个框架组成很多人学的时候弄不清它们的分工我用大白话说一下Spring是大管家负责创建和管理对象Bean比如Service层的实例、数据库连接池它还管着事务——下单时如果扣库存的SQL挂了它能自动把前面执行的SQL全部回滚。SpringMVC是前台客服所有HTTP请求先到它这里它根据URL路由到对应的Controller方法把前端传的参数绑定成Java对象处理完再返回一个JSP页面给浏览器。MyBatis是仓库管理员它不关心业务逻辑只负责把SQL语句执行了、把数据库记录换成Java对象。你可以把SQL写在Mapper.xml里也可以在注解里写非常灵活。这三个框架配合起来就是一个标准的三层架构Controller层接收请求Service层写业务规则Mapper层访问数据库。1.3 Maven、Bootstrap、JSP在项目里扮演的角色Maven解决的是依赖管理项目构建两件事。没有Maven的时候你得手动下载一堆jar包拷到WEB-INF/lib目录还得担心版本冲突有了Mavenpom.xml里写一行依赖坐标它自动把jar包拉下来打包成war包部署到Tomcat也是一条命令的事。项目里用到的Spring版本、MyBatis版本、MySQL驱动版本全部在pom.xml里统一管理。Bootstrap则负责页面外观。收银管理系统的后台界面结构其实很常规左侧菜单栏、顶部导航、中间内容区Bootstrap的栅格系统能很好地完成这种布局表格自带样式、按钮不需要自己调CSS、弹出框Modal做新增编辑非常顺手。你甚至不需要自己写多少CSS代码。JSP的价值在于服务端渲染。商品列表页可以直接在JSP里写循环把后端传来的List用JSTL标签一行行输出成表格行不需要前端写Ajax再去拼HTML。这种模式在SSM项目中是最顺手的。2. 数据库怎么设计表结构从头拆解2.1 功能模块地图模块规划直接决定数据表结构。系统按角色划分功能管理员和收银员看到的界面不一样角色可用功能管理员商品管理增删改查、上下架、商品分类管理、订单查询、会员管理、销售统计、库存预警收银员收银台结算、商品搜索、订单查询、会员积分登记功能确定后数据表最少要有这几张管理员表admin、商品分类表goods_category、商品表goods、订单表orders、订单明细表order_item、会员表member。如果还要做积分和折扣member表里加积分字段即可。2.2 每张表的设计思路商品表是核心表字段包括id、分类id、商品名、条码、售价、库存、销量、状态上架/下架、创建时间。我特别强调一下条码字段校内超市很多商品自带条码收银时如果有扫码枪直接扫条码就能定位商品比输入关键词快得多。订单表要记录的是整笔交易的信息订单号、总金额、支付方式现金/微信/支付宝、收银员id、会员id、下单时间。为什么还要一个订单明细表因为一笔订单可以包含多个商品如果只存一张表要么一行存不下所有商品要么就得重复记录订单信息。明细表一行对应一个商品包含订单id、商品id、商品名、单价、数量、小计金额。这里有个我强烈建议的做法订单明细表里冗余一个商品名字段。如果不冗余商品改过名或者被删除后历史订单查到的就是空的或者显示无。冗余之后即使商品被删了订单照样能正确显示当时的商品名和单价。建表SQL的关键部分如下我只截取核心完整脚本放到项目源码即可CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, barcode VARCHAR(50) DEFAULT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, sales INT NOT NULL DEFAULT 0, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, total_price DECIMAL(10,2) NOT NULL, pay_type TINYINT NOT NULL COMMENT 1现金 2微信 3支付宝, member_id INT DEFAULT NULL, cashier_id INT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, goods_id INT NOT NULL, goods_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, subtotal DECIMAL(10,2) NOT NULL );2.3 字段设计的几个经验点金额字段不要用float或double必须用DECIMAL。浮点算钱会出现0.10.2不等于0.3的问题收银系统涉及金额精度这一点踩过坑的人都懂。库存和销量我放在了goods表里这对小项目来说是最简单的方案。但要注意扣减库存时不能先查出来再减如果两个收银员同时下单同一件商品后写的覆盖先写的库存就错了。正确的做法是UPDATE goods SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}受影响行数为0说明库存不足这一步后面我会展开讲。数据库中顺手给order_no加了唯一索引。订单号用时间戳加随机数生成比如20250112103000012同一个人同一秒钟不可能下两单但不排除极端情况唯一索引是兜底保障。3. 收银核心流程怎么实现关键代码逐个说3.1 登录与权限拦截登录模块看起来简单但权限拦截才是重点。用户登录成功后在session里存当前管理员对象没登录就想访问收银台、商品管理等页面是不允许的。SSM里实现这个用的是SpringMVC的拦截器HandlerInterceptor。拦截器的preHandle方法里判断session有没有登录用户没有就重定向到登录页public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Admin admin (Admin) session.getAttribute(loginAdmin); if (admin null) { response.sendRedirect(request.getContextPath() /login); return false; } return true; } }在spring-mvc.xml里配置拦截路径mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/loginCheck/ mvc:exclude-mapping path/static/**/ mvc:bean classcom.demo.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors有点要注意Bootstrap的css、js、图片这些静态资源也要排除掉否则登录页都加载不出样式浏览器控制台会报一堆403。我第一次做的时候没排除static路径卡了半天才发现是拦截器把静态资源拦了。3.2 收银台实现前端购物车与后端事务收银台页面是使用频率最高的页面用户体验直接决定系统好不好用。前端页面上有一个商品搜索框输入条码或商品名关键词页面用Ajax请求后台接口把匹配的商品列在一个表格区点加入购物车按钮商品进入下方购物车区域并累加数量和金额。结算时把购物车商品列表按JSON格式提交到后端。前端这块用JSP加jQuery即可不必引入太复杂的前端框架。购物车的数据我用JavaScript的数组维护重点代码如下var cart []; // 每个元素{goodsId, name, price, quantity} function addToCart(goodsId, name, price) { var found cart.find(item item.goodsId goodsId); if (found) { found.quantity; } else { cart.push({goodsId: goodsId, name: name, price: price, quantity: 1}); } renderCart(); } function checkout() { if (cart.length 0) { alert(购物车为空); return; } $.ajax({ url: /order/create, type: POST, contentType: application/json, data: JSON.stringify({items: cart, payType: selectedPayType, memberId: selectedMemberId}), success: function(result) { if (result.code 200) { // 清空购物车显示订单金额 cart []; renderCart(); } } }); }后端Controller接收到JSON后调用Service层方法。Service层是整个收银流程的心脏它要做的事按顺序排列生成订单号、计算订单总金额、插入订单主表、遍历明细列表插入订单明细表、扣减每个商品的库存。任何一个环节抛异常整笔交易都应该回滚。核心Service代码的核心结构如下Transactional(rollbackFor Exception.class) public Order createOrder(CreateOrderForm form) { // 1. 生成订单号 String orderNo NO System.currentTimeMillis() RandomUtil.randomNumbers(4); Order order new Order(); order.setOrderNo(orderNo); BigDecimal total BigDecimal.ZERO; // 2. 计算总价以数据库商品价格为准不能相信前端传来的价格 ListOrderItem itemList new ArrayList(); for (OrderItemForm itemForm : form.getItems()) { Goods goods goodsMapper.selectById(itemForm.getGoodsId()); if (goods null || goods.getStatus() ! 1) { throw new RuntimeException(商品不存在或已下架); } BigDecimal subtotal goods.getPrice().multiply(new BigDecimal(itemForm.getQuantity())); total total.add(subtotal); // 组装OrderItem... } order.setTotalPrice(total); // 3. 插入订单 orderMapper.insert(order); // 4. 插入明细 for (OrderItem item : itemList) { orderItemMapper.insert(item); } // 5. 扣减库存 增加销量 for (OrderItemForm itemForm : form.getItems()) { int rows goodsMapper.deductStock(itemForm.getGoodsId(), itemForm.getQuantity()); if (rows 0) { throw new RuntimeException(商品库存不足); } } return order; }代码里有几个关键点我必须强调第一计算金额时不能用前端传来的价格要拿数据库里的商品价格重新算。否则有人抓包把价格改成1分钱整单就废了。服务端永远不信任客户端输入这是开发的基本素养。第二Transactional是Spring的声明式事务方法内的多个数据库操作会在同一个事务中执行。inventory扣减失败抛出RuntimeException前面插入的订单和明细会被自动回滚不会出现订单有了但库存没扣的脏数据。第三deductStock方法对应前面说的条件更新SQL而不是先select再update。这段mapper代码是update iddeductStock UPDATE goods SET stock stock - #{quantity}, sales sales #{quantity} WHERE id #{id} AND stock #{quantity} /update这种写法保证了并发情况下也不会超卖因为数据库层面的行锁会挡住两个同时执行的更新操作后执行的那一个会因为stock不满足条件而更新0行从而触发那个库存不足异常。3.3 销售统计的SQL怎么凑收银数据积累起来之后管理员最关心的几个统计今日销售额、今日订单数、商品销量排行、库存预警。这些用SQL聚合函数比在Java里循环计算效率高得多而且写法很直观。今日销售额SELECT IFNULL(SUM(total_price), 0) FROM orders WHERE DATE(create_time) CURDATE();商品销量排行榜前10SELECT goods_name, SUM(quantity) AS total_qty FROM order_item GROUP BY goods_id, goods_name ORDER BY total_qty DESC LIMIT 10;库存预警比如小于10就提示补货SELECT id, name, stock FROM goods WHERE stock 10 AND status 1;Controller里把统计结果封装成VO返回给JSP页面再通过JSTL或少量JS渲染成卡片和表格。整个实现下来你会发现MyBatis写这类SQL比JPA爽太多这也是SSM这类教学项目仍然有生命力的原因。4. 环境搭建与部署实战从JDK到Tomcat走一遍4.1 开发环境版本怎么选这个项目对环境的要求不高但版本匹配问题可能把你卡在第一步。我推荐的组合是JDK 1.8、Maven 3.6.3、MySQL 5.7或8.0、Tomcat 8.5或9.0、IDEA 2020以上。JDK 1.8是目前兼容性最稳的版本Spring 5.x、Tomcat 9都能正常跑后面新学Spring Boot也不会觉得跳变。Maven 3.6.3是不会和IDEA内置的Maven冲突的稳妥选择太高版本在一些老IDEA里可能初始化报错。MySQL建议5.7它对表结构校验较松练习项目完全够用但你电脑里装的是8.0也没问题只要把驱动换成mysql-connector-java 8.0.x即可。4.2 Maven配置阿里云镜像Maven装好之后第一步不是建项目而是改settings.xml。默认的中央仓库在国内下载依赖非常慢pom里几十个依赖可能拉半小时还报错。打开Maven安装目录下conf/settings.xml在mirrors节点里加上阿里云镜像mirror idaliyunmaven/id mirrorOfcentral/mirrorOf namealiyun public repository/name urlhttps://maven.aliyun.com/repository/public/url /mirror改完后到IDEA里File - Settings - Build Tools - Maven把User settings file指向这个settings.xml顺便把Maven home path指到你本地安装的Maven目录而不是用IDEA捆绑的Maven。这样依赖下载、仓库管理等行为才是可控的。4.3 IDEA导入与Tomcat部署用IDEA直接打开pom.xml选择Open as Project后面步骤是Project Structure确认Project SDK是1.8Settings里找到Build Tools - Maven把Runner的JRE设为1.8然后配置TomcatRun - Edit Configurations - 左上角加号 - Tomcat Server - LocalServer选项卡里Application Server选择Tomcat安装目录Deployment选项卡加Artifact选择xxx:war exploded。地址填http://localhost:8080/然后Run。第一次启动时Idea会调用Maven下载项目依赖并编译war包如果前面镜像配置好了整个过程两三分钟内能完成。4.4 数据库连接配置项目的jdbc.properties配置文件是连接MySQL的关键不同的MySQL版本、不同的连接方式这个配置差异巨大很多人的项目栽在这里。以MySQL 8.0为例驱动类名是com.mysql.cj.jdbc.Driver连接串必须带时区参数否则会报Server returns invalid timezone错误jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password你的密码如果你是MySQL 5.7驱动类名用com.mysql.jdbc.Driver配置里不建议带useSSLfalse以外的多余参数。补充一点直接用Navicat或MySQL Workbench建库建表很方便但学习阶段我强烈建议你在命令行窗口手敲一遍建库导入SQL这样对数据库的底层行为理解更深。5. 开发与运行中的经典问题排查5.1 Maven依赖下载慢或找不到包这个问题我在给同学调代码时几乎每次都遇到。典型症状是IDEA报Could not resolve dependencies或者红色波浪线出现在import语句上。排查顺序我建议这样第一步看settings.xml有没有配置阿里云镜像没配置或配置不生效大概率就是这个原因。第二步看IDEA使用的Maven是不是本地安装的版本如果你改了系统配置但IDEA里还是捆绑Maven设置不会读到你的settings.xml。第三步看本地仓库目录位置默认在用户目录/.m2/repository如果之前有下载到一半的损坏文件删掉对应目录重新加载即可。还有种情况特别坑pom.xml里依赖版本和本地仓库里已有的版本不一致IDEA显示红色的Artifact无法解析。解决办法是在IDEA右侧Maven面板点刷新图标Reload All Maven Projects让它重新解析一次。5.2 MySQL连接失败的各种姿势连接失败是JavaWeb项目最高发的故障错误信息五花八门。整理一个速查表报错关键信息原因解决Access denied for user rootlocalhost密码错误或账号无权限检查jdbc.properties里的用户名密码确认mysql -u root -p能登录Unknown database supermarket数据库还没建执行CREATE DATABASE supermarket再导入表脚本Communications link failure连接串不对或服务没启动确认MySQL服务运行中URL无拼写错误Server returns invalid timezoneMySQL 8时区未设置URL加serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowedMySQL 8.0.12及以上版本安全限制URL加allowPublicKeyRetrievaltruePort 3306连接超时MySQL端口被占用或防火墙拦截查看netstat -ano5.3 Tomcat部署后端口占用与404端口占用是启动阶段的常见问题。IDEA启动Tomcat时左下角报Port 8080 was already in use说明有别的进程占了8080。可以换端口也可以找到占用进程关掉。在Windows上命令是netstat -ano | findstr 8080找到PID后在任务管理器里结束它。Linux/macOS用lsof -i:8080然后kill进程。部署成功后访问页面404优先检查部署Artifact配置和项目访问路径。使用war exploded方式部署IDEA右上角的Application context默认是/xxx_war_exploded如果你的Controller里写的是redirect:/login可能因为上下文路径不对导致跳转失败建议把Application context改成/。另外也要确认项目的web.xml存在并且DispatcherServlet配置的url-pattern没有错映射掉JSP请求。5.4 中文乱码问题JSP页面上中文商品名变成问号或者MySQL里存的数据出现乱码基本可以确认是编码环节没统一。要端到端检查从浏览器到Java再到MySQL一条链路全部设置UTF-8JSP页面顶部加% page contentTypetext/html;charsetUTF-8 %页面form表单提交时methodpost并让过滤器处理编码web.xml里配置Spring的CharacterEncodingFilter它专门把request的编码设置为UTF-8filter 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 init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingMySQL连接URL里带characterEncodingutf8建库时指定utf8mb4字符集CREATE DATABASE supermarket DEFAULT CHARSET utf8mb4;这几个全部一致之后乱码问题基本绝迹。5.5 常见问题速查表经验总结场景现象我常用的排查命令/工具依赖问题红色import、reload无效检查settings.xml和localRepository路径数据库问题连接超时、时区报错先命令行登录sql验证账号再看URL权限问题某些页面直接跳登录页看拦截器exclude-mapping是否把静态资源漏掉事务问题订单出现在但库存未扣检查Service方法上是否有Transactional页面问题表单提交后后台报空指针检查Controller方法参数名是否与input的name一致必要时加RequestParam数据精度金额计算多出几分钱检查是否用了float/double统一BigDecimal和DECIMAL6. 做完这个项目的几点个人体会做这个校内超市收银管理系统最大的收获不是写完了多少行代码而是把数据从页面到数据库再回到页面的完整链路彻底打通了。你会真正理解Maven管理依赖的价值理解为什么MySQL要设计成事务型数据库理解自研系统里金额计算绝不能信前端这种红线原则。如果你是用它当课程设计或者毕业设计强烈建议在答辩前自己亲手把Recovery流程跑一遍编译、部署、初始化数据库、连续下几单、查看库存变化和日报表。这个过程能帮你把项目真正装进脑子里不会被老师的随机提问打懵。踩过坑之后我现在做一切JavaWeb项目都养成了三个习惯先画数据表关系再写业务逻辑、写任何金额计算先声明BigDecimal、改配置前先备份一份原文件。这套系统如果你愿意后续还可以往微信小程序点单、刷脸支付的方向扩展但先把这套SSM的地基打扎实比什么都值。