
做校园超市收银系统这个项目最初就是冲着一个目标去的把 JavaWeb 里那套经典的 SSM 全家桶真正打通而不是照着教程把几个 Demo 拼在一起交差。JavaWeb 课设每年都有人选超市管理但真正把 SSM、Maven、Bootstrap 这套组合玩明白再把 MySQL 表结构设计得能扛住真实收银场景的其实不多。这个项目做完我最大的感触是它不是一个“增删改查”堆出来的演示系统而是一个能直接拿去校内超市做日常收银的完整闭环从扫码加购、结算找零、库存扣减到订单查询、销售统计全是真实业务里绕不开的环节。整篇文章我就围绕这套系统的技术选型、数据库设计、收银核心流程、环境部署和调试排错这几个维度展开把实现过程、关键配置、参数计算方式以及我实际踩过的坑全部摊开讲。适合正在做 JavaWeb 课设、毕业设计的同学也适合想快速搞懂 SSM 项目完整落地流程的人。所有配置和代码都是我当时实际跑通的版本你可以直接照着抄有什么细节对不上的大概率是版本差异文末也总结了排查思路。1. 项目定位与技术栈拆解1.1 校园超市收银到底需要什么先说业务。校园超市和普通便利店最大的区别在于客单集中、时段性强。课间十分钟、午晚饭前后是高峰收银员必须“快”学生买的东西往往单价低、种类杂但每天卖出去的品类又非常多付款方式也不是单一现金校园卡、微信、支付宝都得支持到了晚上还要对账哪个班次收了多少钱、哪些商品卖了多少都得能查。所以这个系统在设计功能时就不能只是把商品表做个增删改查应付了事。我最终划定的核心模块是这六个登录认证操作员登录、退出、会话管理区分管理员和收银员权限。商品管理商品分类、商品信息维护、库存查询、上下架状态管理。收银结算搜索商品、加入购物车、修改数量、结算、找零这是整个系统的心脏。订单管理订单列表、订单明细查看、按单号或时间段检索必要的退单操作。销售统计按天/按月汇总销售额、订单数商品销量排行用于超市盘点和对账。基础资料供应商信息、操作员账号管理。把功能边界划清楚之后再动手写代码后面基建才不会乱。很多同学一上来就建表、写 Controller写到一半才发现缺这个缺那个返工成本很高。1.2 为什么是 SSM Maven Bootstrap MySQL这套组合看起来“老”但放在校园超市这个场景下每一样都站得住脚。SSM 指的是 Spring、SpringMVC、MyBatis 三个框架。Spring 负责对象管理和事务SpringMVC 负责请求路由和参数绑定MyBatis 负责 SQL 和数据映射。三者的边界非常清晰比直接上 Spring Boot 更能看清 Web 应用的每一层在干什么。Spring Boot 固然省事但如果你正在做课设或者想打好基础SSM 的“手动感”反而能逼你理解 IoC、AOP、DispatcherServlet 这些底层机制是怎么串起来的。Maven 解决的是依赖管理问题。一个 SSM 项目光 jar 包就二十多个版本稍微不对就会冒出各种ClassNotFoundException或NoSuchMethodError。用了 Maven 之后所有依赖都写在pom.xml里版本统一管理打包也方便。而且 Maven 的依赖传递机制能自动把 Spring、MyBatis 各自依赖的第三方库拉下来省了大量手工找包的痛苦。Bootstrap 的价值在于不用做复杂的前端工程化。直接下载本地静态资源配合 JSP 服务端渲染几个小时就能把后台管理的界面框架搭出来。栅格系统、表格样式、弹窗组件都有现成的收银页面用它的 grid 布局排商品卡片非常顺手。MySQL 更不用多说单机部署、免费、资料多校园超市这种每天几千单的量级InnoDB 引擎配合合理的索引设计完全扛得住。1.3 技术栈版本选型与目录结构版本选择是这类项目里最容易被忽视、又最容易踩坑的地方。我的实际搭配是这样JDK 1.8不要用 17 或更高版本SSM 老组合在 JDK8 上最稳。Maven 3.6.33.8 以上对镜像仓库配置有调整习惯 3.6.x 更省心。Tomcat 8.5 或 9.0支持 Servlet 3.1配 IDEA 部署很方便。Spring 5.3.x、SpringMVC 5.3.x、MyBatis 3.5.x。MySQL 5.7 或 8.0驱动用 8.0.x。Bootstrap 3.4.1。Druid 连接池 PageHelper 分页插件 Jackson 做 JSON 序列化。项目目录我采用 Maven 标准结构src/main/java下按包分层controller、service、dao、pojo、interceptor、commonsrc/main/resources下放 Spring 配置、MyBatis 映射文件和mapper/*.xmlsrc/main/webapp下放 JSP 页面、静态资源和 WEB-INF。这种结构的好处是谁负责哪一层一看就懂后续扩展也不容易迷路。2. 数据库设计与核心表结构2.1 表结构总览与设计原则数据库是整个系统的地基我建议先建库、再建表、最后写代码。建库时直接指定字符集避免后面乱码折腾CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;用 utf8mb4 而不是 utf8是因为它支持完整的 Unicode 编码商品名里如果出现生僻字或者特殊符号不会变问号。整个系统我设计了六张核心表这里先把整体关系列出来表名作用核心字段admin_user操作员账号id, username, password, real_name, role, status, create_timegoods_category商品分类id, name, sort, statusgoods商品信息id, category_id, goods_no, goods_name, price, stock, unit, status, create_timesupplier供应商id, name, contact, phone, address, create_timesales_order销售订单头id, order_no, cashier_id, cashier_name, total_amount, receive_amount, change_amount, pay_type, sale_time, remarkorder_item订单明细id, order_id, goods_id, goods_name, price, quantity, subtotalstock_log库存流水id, goods_id, change_type, change_num, after_stock, operator, create_time设计上的几个关键原则我在这个项目里感受特别深第一金额字段一律用DECIMAL(10,2)绝对不要用 double 或 float。这么说吧double 在二进制里存 0.1 本身就是近似值收银算总额和找零时0.1 0.2 可能得到 0.30000000000000004这在真金白银的业务里是不能忍的。BigDecimal 是 Java 侧的配合方案数据库侧就是 DECIMAL。第二外键只用逻辑关联不建物理外键。说白了就是表与表之间通过category_id、order_id这类字段在应用层关联但不在数据库里写FOREIGN KEY约束。原因很简单物理外键在高并发写入时容易引发锁竞争而且给后续订单归档、分表拆库制造麻烦。校园超市的并发虽然不算极端但收银高峰期的写入频率也足够让外键约束变成瓶颈了。第三订单表必须做商品快照。订单明细里不仅存goods_id还要冗余一份goods_name和当时的单价price。这样做之后即便商品后来改了价格、改了名字历史订单查询出来依然是下单那一刻的真实信息。这个细节是真实收银系统的基本要求但很多课设项目都没做。2.2 核心表建表语句商品表的建表语句我直接贴出来字段注释都写在里面了CREATE TABLE goods ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, category_id BIGINT NOT NULL COMMENT 分类id, goods_no VARCHAR(32) NOT NULL COMMENT 商品编码/条码, goods_name VARCHAR(128) NOT NULL COMMENT 商品名称, price DECIMAL(10,2) NOT NULL COMMENT 售价, stock INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 库存数量, unit VARCHAR(16) DEFAULT 个 COMMENT 单位, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_goods_no (goods_no), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;goods_no加唯一索引保证扫码枪扫出来的编码不会重复。status字段用来做上下架下架的商品在收银台搜索时直接过滤掉避免卖出已停售的商品。订单头和订单明细是分开设计的这是为了后续查询的灵活性CREATE TABLE sales_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号, cashier_id BIGINT NOT NULL COMMENT 收银员id, cashier_name VARCHAR(50) NOT NULL COMMENT 收银员姓名快照, total_amount DECIMAL(10,2) NOT NULL COMMENT 应收总额, receive_amount DECIMAL(10,2) NOT NULL COMMENT 实收金额, change_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 找零金额, pay_type TINYINT NOT NULL DEFAULT 1 COMMENT 1现金 2微信 3支付宝 4校园卡, sale_time DATETIME NOT NULL COMMENT 销售时间, remark VARCHAR(255) DEFAULT COMMENT 备注, UNIQUE KEY uk_order_no (order_no), KEY idx_sale_time (sale_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT销售订单表; CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT 订单id, goods_id BIGINT NOT NULL COMMENT 商品id, goods_name VARCHAR(128) NOT NULL COMMENT 商品名称快照, price DECIMAL(10,2) NOT NULL COMMENT 成交单价快照, quantity INT NOT NULL COMMENT 购买数量, subtotal DECIMAL(10,2) NOT NULL COMMENT 小计金额, KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;sale_time加索引非常重要因为销售统计报表几乎都按时间段查询没有索引的话数据量一大GROUP BY DATE_FORMAT(...)就会全表扫描页面响应会肉眼可见地变慢。2.3 库存流水表与数据一致性库存模块我单独设计了一张stock_log流水表。它的作用是把每一次库存变动记录下来CREATE TABLE stock_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, goods_id BIGINT NOT NULL, change_type TINYINT NOT NULL COMMENT 1销售出库 2采购入库 3盘点调整, change_num INT NOT NULL COMMENT 变动数量正负表示增减方向, after_stock INT NOT NULL COMMENT 变动后库存, operator VARCHAR(50) NOT NULL COMMENT 操作人, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_goods_time (goods_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存流水表;有了它任何一笔库存变动都能追溯。比如某天对账发现库存对不上就可以把时间段的流水拉出来逐条核对是销售漏单了还是入库没登记一目了然。这个表现在看起来是“额外设计”但等系统真上线用了一周之后你会感谢当初加了它。3. 环境搭建与 SSM 整合配置3.1 开发环境准备开始写代码之前我建议先把环境统一好。我这边实际用的是 IDEA 2023.2JDK 1.8Maven 3.6.3Tomcat 9.0MySQL 8.0。Maven 装好后第一件事就是配镜像仓库。这一步不做的话默认中央仓库在国内下载慢到怀疑人生。在 Maven 安装目录的conf/settings.xml里找到mirrors节点加上阿里云镜像mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/central/url /mirror另外强烈建议把本地仓库路径单独改到一个固定的地方比如D:/maven-repo并在 IDEA 的 Maven 设置里指过去。这样以后换项目、换工作区jar 包不用重新下一遍也方便你哪天发现某个 jar 坏了直接到本地仓库目录里删掉对应文件夹重新拉取就行。MySQL 安装完建议用 Navicat 或 MySQL Workbench 手动建一个supermarket库执行上节的建表语句然后单独建一个专用的数据库账号不要用 root 直连。3.2 pom.xml 关键依赖清单SSM 项目的pom.xml是重头戏依赖少了报错版本不对也报错。我这份是调试通过的完整核心依赖properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies !-- Spring 核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.30/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.30/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.3.30/version /dependency !-- MyBatis 与整合包 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.13/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.1/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.30/version /dependency !-- Druid 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.16/version /dependency !-- 分页插件 -- dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper/artifactId version5.3.2/version /dependency !-- JSON 序列化 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency !-- JSTL 标签库 -- dependency groupIdjstl/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- Servlet API打包时不需要包含 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency /dependencies这里说一个我踩过的坑javax.servlet-api的 scope 必须是provided意思是 Tomcat 容器里已经有这份 jar 了打包时不要重复打进去。如果不加这个 scope项目里同时存在两套 Servlet API运行时经常出现奇怪的ClassCastException。3.3 Spring、SpringMVC、MyBatis 三个配置文件怎么分工SSM 整合的核心是三个配置文件职责必须分清楚applicationContext.xml管 Spring 容器主要做三件事扫描service包下的注解、配置 Druid 数据源、把 MyBatis 的 SqlSessionFactory 和 Mapper 接口扫描注册进来bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/supermarket?useUnicodetrueamp;characterEncodingutf8amp;useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;allowPublicKeyRetrievaltrue/ property nameusername valueyour_user/ property namepassword valueyour_password/ property nameinitialSize value5/ property namemaxActive value50/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nametypeAliasesPackage valuecom.supermarket.pojo/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.supermarket.dao/ /beanspring-mvc.xml只管 Controller 层。核心配置是开启注解驱动、配置视图解析器、放行静态资源、注册拦截器。视图解析器决定了 Controller 返回的字符串对应哪个 JSP 页面context:component-scan base-packagecom.supermarket.controller/ mvc:annotation-driven/ mvc:resources location/static/ mapping/static/**/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean重点说一下静态资源放行是新手最容易漏的。Bootstrap 的 css、js 如果不放行页面打开就是一团乱码般的样式丢失。上面用mvc:resources把/static/**映射到项目里的 static 目录这个位置一定要和webapp/static的实际路径对上。web.xml是 Web 容器的入口。需要配置三样东西Spring 的监听器、SpringMVC 的前端控制器、全局字符编码过滤器。字符编码过滤器必须放在所有过滤器的最前面而且要强制编码为 UTF-8。3.4 Bootstrap JSP 的前端页面组织前端部分我用的是 Bootstrap 3.4.1 jQuery整体结构做成后台管理系统的三段式顶部导航栏、左侧菜单、右侧内容区。每个 JSP 页面都 include 公共的头部和菜单内容区根据功能切换。JSP 的优势是可以直接用 JSTL 标签遍历后端传过来的 List配合 EL 表达式输出数据不用额外写前端渲染逻辑。收银台页面是花心思最多的一个页面。布局我用了 Bootstrap 的栅格系统左边大区域放商品列表按分类标签筛选每个商品做成一个卡片点击卡片就加入到右侧的购物车列表购物车列表下面显示总金额、实收金额、找零金额底部放结算按钮。交互全部用 jQuery 的 ajax 完成用户体验上接近真实的收银操作。Bootstrap 的 modal 组件用来做确认弹窗比如结算前展示订单明细、退单时确认操作都比自己手写弹窗舒服得多。表格直接用.table table-striped table-hover就能获得不错的观感和隔行变色效果。4. 收银核心流程的实现与代码解析4.1 登录认证与操作权限控制登录这块没有引入 Shiro 或 Spring Security因为场景简单用 Session 拦截器就够了。密码不能明文存我用的方案是 MD5 加盐。加盐的意思是存的不是md5(password)而是md5(password 固定盐值)。这样即使数据库泄露预计算的 MD5 彩虹表也直接失效。登录成功后把用户对象放进 Session比如session.setAttribute(loginUser, user)所有需要登录的页面都经过拦截器校验。拦截器是 SpringMVC 里非常实用的组件我定义了一个LoginInterceptorpublic class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object loginUser session.getAttribute(loginUser); String uri request.getRequestURI(); // 登录接口本身要放行否则会死循环 if (uri.contains(/login) || uri.contains(/static/)) { return true; } if (loginUser 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/static/**/ bean classcom.supermarket.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors我在这里栽过一个跟头一开始没有排除/login路径结果登录请求本身也被拦了页面无限重定向。这个细节看起来小但排查起来很费时间。4.2 商品搜索与购物车会话管理收银台的第一个操作是找商品。我做了两种方式一种是按分类浏览另一种是搜索框输入商品编码或名称关键字实时查询。搜索的 Controller 方法是这样的思路GetMapping(/goods/search) ResponseBody public Result searchGoods(String keyword) { ListGoods list goodsService.searchOnSale(keyword); return Result.success(list); }前端 input 绑定keyup事件用 jQuery 发起 ajax 请求返回的商品列表渲染成卡片。这里要注意接口返回必须是 JSON所以用ResponseBody配合项目里配置的 Jackson对象自动序列化成 JSON 字符串。购物车我选择放在 Session 里维护不用数据库表。因为购物车是临时的收银员加购、删改、取消都不应该立刻产生数据库记录。用一个ListCartItem存在 Session 中CartItem 包含 goodsId、goodsName、price、quantity。每次加减数量、删除商品都在内存里操作只有点击结算时才落库。这种设计的数据库压力最小也符合收银的场景直觉。前端购物车同步更新总金额用 jQuery 遍历购物车数据实时计算避免每次变动都要向服务器请求。这样页面响应快用户体验好。4.3 结算下单事务、库存扣减与订单生成结算是整个系统的核心中的核心。表面上它做的事是“算钱、存单、扣库存”但并发场景下每一环都可能出问题。我最终实现的逻辑是这样的Transactional(rollbackFor Exception.class) public OrderResult createOrder(Integer cashierId, String cashierName, ListCartItem items, BigDecimal receiveAmount, Integer payType) { BigDecimal totalAmount BigDecimal.ZERO; for (CartItem item : items) { totalAmount totalAmount.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 实收金额不能小于应收金额现金支付场景 if (receiveAmount.compareTo(totalAmount) 0) { throw new BusinessException(实收金额小于应收金额); } BigDecimal changeAmount receiveAmount.subtract(totalAmount); // 生成订单号时间戳 4位随机数保证唯一 String orderNo new SimpleDateFormat(yyyyMMddHHmmss).format(new Date()) String.format(%04d, new Random().nextInt(10000)); SalesOrder order new SalesOrder(); order.setOrderNo(orderNo); order.setCashierId(cashierId); order.setCashierName(cashierName); order.setTotalAmount(totalAmount); order.setReceiveAmount(receiveAmount); order.setChangeAmount(changeAmount); order.setPayType(payType); order.setSaleTime(new Date()); orderDao.insert(order); for (CartItem item : items) { // 关键SQL库存扣减条件带 stock 数量防止超卖 int rows goodsDao.deductStock(item.getGoodsId(), item.getQuantity()); if (rows 0) { throw new BusinessException(商品[ item.getGoodsName() ]库存不足); } OrderItem oi new OrderItem(); oi.setOrderId(order.getId()); oi.setGoodsId(item.getGoodsId()); oi.setGoodsName(item.getGoodsName()); oi.setPrice(item.getPrice()); oi.setQuantity(item.getQuantity()); oi.setSubtotal(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); orderItemDao.insert(oi); // 写库存流水 stockLogDao.insert(new StockLog(item.getGoodsId(), 1, -item.getQuantity(), goodsDao.getStockById(item.getGoodsId()), cashierName)); } return new OrderResult(orderNo, totalAmount, changeAmount); }这里最值得强调的是库存扣减那行 SQL。我用的是一条带条件的更新语句UPDATE goods SET stock stock - #{quantity} WHERE id #{goodsId} AND stock #{quantity}UPDATE操作本身会加行锁stock #{quantity}这个条件保证只有库存足够时才更新成功返回值rows为 0 就说明库存不足。这比“先 SELECT 再 UPDATE”的经典两步做法靠谱得多因为两步之间如果有两个收银员同时买同一个商品就可能都查到了足够的库存但扣减时库存已经不够了。这个场景就是教科书里的并发超卖问题用一条带条件的 UPDATE 直接在数据库层面解决了。Transactional(rollbackFor Exception.class)这个注解是事务的开关。它的含义是方法内任何异常都会触发整个事务回滚也就是说订单头、订单明细、库存扣减、库存流水这四步操作要么全部成功要么全部不生效。如果忘了加这个注解就可能出现订单生成了但库存没扣的情况对账的时候会非常痛苦。订单号生成用“时间戳 4位随机数”配合数据库的唯一索引基本不会重复。如果对并发要求更高可以改成“时间戳 用户id后四位 随机数”。4.4 销售统计与报表实现统计报表是超市管理者的刚需每天打烊后要对着账目核对。我实现了两个最核心的报表按天统计销售额和订单数SELECT DATE_FORMAT(sale_time, %Y-%m-%d) AS saleDate, COUNT(*) AS orderCount, SUM(total_amount) AS totalAmount FROM sales_order WHERE sale_time #{startTime} AND sale_time #{endTime} GROUP BY DATE_FORMAT(sale_time, %Y-%m-%d) ORDER BY saleDate DESC商品销量排行SELECT oi.goods_id, oi.goods_name, SUM(oi.quantity) AS totalQty, SUM(oi.subtotal) AS totalAmount FROM order_item oi JOIN sales_order so ON oi.order_id so.id WHERE so.sale_time #{startTime} AND so.sale_time #{endTime} GROUP BY oi.goods_id, oi.goods_name ORDER BY totalQty DESC LIMIT 10注意这里的startTime和endTime从前端接收时是字符串比如2025-01-01和2025-01-31。用而不是比较结束时间可以避开23:59:59这种边界精度问题。查询结果用 PageHelper 分页插件直接封装成 PageInfo前端表格展示再用 Bootstrap 的进度条组件做一个条形图式的销量对比效果朴素但实用。5. 部署调试中的常见问题与排查技巧5.1 Maven 依赖下载与冲突问题这类项目的环境问题几乎都出在 Maven 上。我把最典型的几种情况整理成一张表现象原因解决办法依赖下载超时或失败中央仓库不稳定配置阿里云镜像重试IDEA 中引入的 jar 报红本地仓库 jar 包损坏删除本地仓库对应目录后 Reimport“本地有包但是引不进来”IDEA 的 Maven 仓库路径和命令行不一致检查settings.xml的 localRepository 并统一NoSuchMethodError同一个库存在两个版本在 IDEA 的 Maven 面板里Show Dependencies排除冲突版本打包后运行报ClassNotFoundException依赖 scope 错误或者 WEB-INF/lib 缺少 jar检查providedscope 是否错用重新打包我遇到过最诡异的是“本地仓库明明有 jarIDEA 却始终报红”。最后发现原因是我在本机装了多个 MavenIDEA 用的是内置 Maven 和默认仓库路径而命令行用的则是另一个仓库。统一settings.xml里的localRepository后立刻解决。遇到这种情况先看 IDEA 设置里 Maven 的User settings file到底指向哪再去确认仓库路径基本就破案了。5.2 数据库连接与数据源配置问题数据库连接报错是第二大户我把高频报错按文案列一下Access denied for user ...账号密码错误或者账号没有远程访问权限。解决方法是确认密码以及确认 MySQL 用户表里host字段允许当前来源 IP 登录。Communications link failure或Connection refusedMySQL 服务没启动或端口不是默认的 3306。先确认服务状态再看连接 URL 里的端口。The server time zone value ... is unrecognizedMySQL 8 的时区问题。URL 上必须加serverTimezoneAsia/Shanghai。Public Key Retrieval is not allowedMySQL 8 使用 caching_sha2_password 认证时会报这个错。URL 上加allowPublicKeyRetrievaltrue。还有个细节useSSLfalse一定要加上。MySQL 8 默认尝试 SSL 连接本地开发环境没配证书不加这参数就会出现 SSL 握手失败。5.3 中文乱码问题乱码是 Web 项目的经典问题这个项目里我总结的排查顺序是第一层请求乱码。表单提交中文到后端变问号多半是web.xml的CharacterEncodingFilter没配置或者没放在过滤器链最前面。第二层响应乱码。Controller 返回 JSON 或页面中文乱码检查 SpringMVC 的注解驱动是否配置了消息转换器以及 JSP 页面头部的pageEncoding是否设置为 UTF-8。第三层数据库乱码。数据写入 MySQL 后中文变问号需要同时确认数据库默认字符集是 utf8mb4、连接 URL 带了characterEncodingutf8、表结构的 charset 也是 utf8mb4。三者缺一个数据就可能出问题。我之前犯过一个低级错误库是 utf8mb4但建表时没指定 charset结果表继承了 MySQL 服务端默认的 latin1写入中文直接变问号。后来把表ALTER TABLE ... DEFAULT CHARSETutf8mb4才救回来。5.4 部署到 Tomcat 后的 404 / 500 排查本地 IDEA 直接跑 Tomcat 和打包部署到独立 Tomcat遇到的情况还不一样。我用 IDEA 部署时遇到最多的是 404排查思路大部分集中在部署配置上。404 的第一反应看控制台启动日志有没有报错。如果日志干净但还是 404重点检查 IDEA 里项目的 Artifacts 配置。SSM 项目打成 war 包时必须把lib目录加进去否则所有第三方 jar 都不会进WEB-INF/lib启动不报错但访问任意页面都会 404 或 500。这个坑的典型特征是IDEA 的 “Tomcat 可以启动、日志无异常、访问全 404”。500 错误则要看异常栈。最常见的两种一种是 SQL 映射文件没有打进 classpath。Maven 默认只把src/main/resources下的文件复制到 classes如果你把mapper/*.xml放在了src/main/java目录下打包时会被跳过运行时 MyBatis 就会报Invalid bound statement (not found)。解决办法是在pom.xml的build节点里显式声明资源目录。另一种是ClassNotFoundException几乎都是依赖 scope 配错或者 jar 没打全。把WEB-INF/lib目录列出来跟pom.xml的依赖清单对一遍就能发现少了谁。6. 写在最后的实操心得项目做完回看技术上最值钱的经验其实不是某个框架的 API而是几个贯穿始终的原则。第一个原则收银流程的每一步都得能追溯。订单有唯一单号明细有商品快照库存有流水记录登录有会话控制。系统运行中你一定会遇到对不上的情况这时候靠的就是这些痕迹。宁可多设计一张表也不要等出问题时靠猜。第二个原则金额计算永远用 BigDecimal库存扣减永远走事务加条件更新这两条是数据正确性的底线。课设答辩或实际使用中最容易被挑刺的就是这两个点。你说不清楚为什么不用 double或者为什么扣库存要加stock #{quantity}条件那基本等于告诉别人你没做过真正落地的系统。第三个原则先跑通主链路再补细节。做这个系统时我先用最原始的方式把“登录 → 加购 → 结算 → 扣库存 → 订单查询”整条链路跑通然后才开始补分页、补权限、补报表、补前端样式。主链路通了系统就是一个能用的东西剩下的都是让体验更好。反过来如果一开始就在 Bootstrap 美化上花时间主流程没通随时都会推翻重来。最后再分享一个小技巧本地开发时把 MySQL 的general_log打开就能在日志里看到 MyBatis 执行的每一条 SQL。排查库存扣减异常、定位慢查询的时候这个开关能省下大量盲猜的时间。我就是靠它发现了一条统计 SQL 因为少了索引在全表扫描加上索引之后页面从卡顿变成秒开。这套系统现在看来技术栈偏传统但它的业务完整度是很多花架子项目比不上的。如果你正在做类似的 JavaWeb 课设照着这个思路把表设计、事务边界、库存扣减、统计报表这四块啃下来写出来的东西一定比单纯的增删改查高一个档次。