简介一套基于SSM框架实现的电脑配件销售系统面向Java毕业设计、数据库课程设计等学习者后台包含用户管理、商品分类、上下架、库存、订单等模块前台支持注册登录、商品推荐、购物车、收藏、评论、充值支付等功能适合作为B/S架构项目练手或毕设参考。资源为zip压缩包共1344个文件主要包含359个JS脚本、166个CSS样式、156个JSP页面、132个Java源文件及SQL数据库脚本等整体大小22.21MB目录结构清晰便于检索使用。目前已有133人学习下载。系统基于JDK1.8、MySQL5.7采用SpringSpringMVCMyBatisMaven整合开发前端使用JSP、CSS、JS并预置了Bootstrap、LayUI等样式资源导入Eclipse或IDEA即可运行调试。对需要掌握SSM框架完整电商流程的读者从源码到数据库脚本、从前台交互到后台管理均有较高参考价值。1. 接到“SSM电脑配件销售商城”需求后先想清楚这三件事再做先说你拿到这个标题时最该搞懂的一点这是一套基于SSM框架、采用B/S架构的Java Web课程设计或毕业设计项目。它的本质是一个“带商品展示、购物车、订单管理、后台维护”的电商系统业务范围圈定在电脑配件这个品类里。换成大白话从浏览器打开页面游客能浏览CPU、显卡、主板、内存这些配件并加入购物车下单管理员能登录后台管理商品分类、库存和订单状态。SSM作为Java后端技术栈的经典组合Spring管对象、SpringMVC管Web层、MyBatis管数据库加上MySQL存储业务数据正好覆盖企业级Java开发的完整链条。做这套东西的人的典型画像就是Java初学者想拿一个完整项目练手或者是准备课程设计、Java面试时能讲清楚一个真实系统的数据流转。比起Spring BootSSM的好处是每一层都暴露在明面上XML配置和注解各占一半对理解框架原理更有帮助。很多人在纠结“现在谁还用SSM直接上Spring Boot不行吗”——这里要泼盆冷水毕业设计和课程设计的评分点恰恰是SSM这套“看得出层次”的架构B/S模式下的请求如何从浏览器走到数据库再返回这个链路讲得越清楚分数越好看。所以别急着嫌弃先把这套骨架吃透。2. B/S架构下SSM商城的分层拆解一次“浏览显卡列表”的请求要经过哪几个类2.1 B/S架构和C/S架构的差别以及SSM商城为什么必须选B/SB/S架构Browser/Server浏览器/服务器架构。用户在客户端不装任何专用软件用Chrome、Edge这类浏览器输入网址就能访问系统Java代码、数据库、配置文件全部部署在服务器上。C/S架构则要在每台电脑上装客户端程序比如老式的桌面进销存软件、Delphi写的POS系统。这个商城题目点名要求B/S架构是因为它天然适配“多个学生宿舍、多个管理员在不同机器上同时访问”的场景——只要有网络和浏览器就行。从课程设计的评分角度看B/S架构意味着你要交代清楚这几件事浏览器发HTTP请求给Tomcat服务器Tomcat根据URL找到对应的SpringMVC控制器控制器调用Service层Service层通过MyBatis操作MySQL数据库查询结果再一层层返回并渲染成HTML页面。这整个闭环能画出来、能讲出来项目的基本盘就稳了。很多人只会在浏览器里点来点去面试官问你“整个请求经历了什么”就卡壳就是这个链路没在脑子里建立起来。2.2 SSM三个框架的职责边界与配置归属在动手写代码前先把SSM三个框架的职责楔进脑子里后面调试才不糊涂。Spring是容器负责管理Service层和DAO层的对象创建、依赖注入、事务控制SpringMVC是Web层的MVC框架负责接收请求、分发到Controller、返回视图MyBatis是持久层框架负责把Java对象和数据库记录做映射执行SQL并返回结果。数据库连接池、事务管理器由Spring统一配置MyBatis的Mapper接口和XML文件负责具体的SQL。这套组合里Spring是粘合剂SpringMVC是入口MyBatis是出口。关键的配置打磨点我一般会这么落# jdbc.properties - 数据库连接配置 jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/computer_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456这里的URL参数要单独说。useUnicode和characterEncodingutf8解决中文乱码问题serverTimezoneAsia/Shanghai解决MySQL 8.x的时区报错。很多人在本地连数据库报The server time zone value的异常就是少了这个参数。driver用的com.mysql.cj.jdbc.Driver是针对MySQL 8的老项目写com.mysql.jdbc.Driver在MySQL 8下会直接报类找不到。接着是Spring整合MyBatis的核心配置这块是SSM项目的“黑匣子”配错一个引用就启动报错!-- spring-mybatis.xml 部分关键配置 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value5/ property namemaxActive value20/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nametypeAliasesPackage valuecom.mall.entity/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue/ /bean /property /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.mall.dao/ /bean数据源这里选了Druid阿里巴巴的连接池监控和防SQL注入比Spring自带的DriverManagerDataSource靠谱得多。initialSize和maxActive是连接池的初始连接数和最大连接数课程设计给5和20就够用不用按生产环境的官方推荐值调。typeAliasesPackage是实体类的包名mapperLocations把XML文件指向resources下的mapper目录mapUnderscoreToCamelCase开启后数据库的create_time能自动映射到Java的createTime这句是无数人踩坑的高发地不打开的话SQL查询结果全是null。MapperScannerConfigurer指定DAO接口所在包Spring会为每个接口生成代理对象直接在Controller或Service里用Autowired注入即可。这三个配置是一个整体漏了任何一个启动时会报org.springframework.beans.factory.BeanCreationException看到这个异常先回这里检查。2.3 一次完整的“查看配件列表”请求链路现在从代码层面看看一个请求的空洞是怎么被填上的。假设前台页面上有一个“显卡列表”的入口点击后URL是/product/list?categoryId12。请求先被SpringMVC的DispatcherServlet拦截根据RequestMapping(/product/list)找到ProductController的list方法。Controller不直接写SQL它调用ProductService接口Service实现类里写业务逻辑——比如先判断categoryId是否合法再调用ProductDAO接口的selectByCategoryId方法。ProductDAO是MyBatis的Mapper接口真正执行的是resources/mapper/ProductMapper.xml里id为selectByCategoryId的SQL语句。查询结果从ResultSet映射成List 原路返回到ControllerController把数据塞进ModelAndView指向product_list.jsp页面最后由JSP引擎渲染成HTML返回浏览器。// ProductController.java - 商品列表查询入口 Controller RequestMapping(/product) public class ProductController { Autowired private ProductService productService; RequestMapping(/list) public String list(RequestParam(value categoryId, defaultValue 1) Integer categoryId, Model model) { // 调用Service层Service内部再调用DAO层 ListProduct productList productService.findByCategoryId(categoryId); model.addAttribute(products, productList); // 返回逻辑视图名由视图解析器拼装成 /WEB-INF/views/product_list.jsp return product_list; } }// ProductServiceImpl.java - Service层实现类事务边界在这里 Service Transactional public class ProductServiceImpl implements ProductService { Autowired private ProductDao productDao; Override public ListProduct findByCategoryId(Integer categoryId) { // 参数校验分类ID不存在时返回空列表不直接抛异常 if (categoryId null || categoryId 0) { return Collections.emptyList(); } return productDao.selectByCategoryId(categoryId); } }Controller用Autowired注入ServiceService用Autowired注入DAO这个依赖链是SSM的经典写法。Transactional加在Service实现类上表示这个类里的方法都走事务订单创建这类多步操作才能保证要么全成功要么全回滚。RequestParam里的defaultValue给了一个默认兜底用户没传categoryId时不会直接报400错误而是按分类1查询。MyBatis的XML文件长这样注意这个SQL的写法能防止一个常见数据库问题!-- ProductMapper.xml 中的查询语句 -- select idselectByCategoryId resultTypecom.mall.entity.Product SELECT id, name, brand, price, stock, image_url, category_id, description FROM product WHERE category_id #{categoryId} ORDER BY id DESC /select#{categoryId}是预编译占位符MyBatis会把它转成JDBC的PreparedStatement参数防止SQL注入。${}是字符串拼接用户输入能直接改你的SQL结构这个商城项目里全部用#{}别图省事写${}。resultType指向实体类的全限定名字段名和数据库列名严格对应所以写SQL时显式列出所有列不用SELECT *免得后面给表加字段时前端页面跟着翻车。3. 设计电脑配件商城数据库建表是第一步订单状态和库存扣减是第二个坑3.1 配件品类杂、规格差异大分类表和参数表怎么设计电脑配件这个品类有个特点不同分类的属性差异极大。CPU要标插槽类型、核心线程数显卡要标显存容量、位宽、功耗内存要标频率、代数电源要标注额定功率和认证标准。如果把所有属性都做成product表里的列这张表会膨胀到三四十个字段而且大量字段是空的。更合理的做法是拆一张商品参数表用key-value形式存不同分类的差异化属性。我一般会设计成五张核心表用户表user、分类表category、商品表product、订单表orders、订单明细表order_item外加购物车。分类表只有id、name、parent_id三列支持二级分类一级是“CPU/主板/显卡/内存/硬盘/电源/机箱/散热器”二级可以挂具体系列。商品表只存所有分类共有的字段差异化参数放product_param表每行存一条“商品ID 参数名 参数值”。比如显卡的“显存容量8GB”是一条记录“功耗200W”是另一条记录。这样做的好处是新增一个“水冷散热器”分类时不需要改数据库表结构前端商品详情页变成动态渲染参数列表。商品表的字段是这套系统的地基列出来看一下-- 商品表核心字段与说明 CREATE TABLE product ( id INT NOT NULL AUTO_INCREMENT COMMENT 商品ID, category_id INT NOT NULL COMMENT 所属分类ID, name VARCHAR(200) NOT NULL COMMENT 商品名称, brand VARCHAR(100) DEFAULT NULL COMMENT 品牌, price DECIMAL(10,2) NOT NULL COMMENT 售价单位元, original_price DECIMAL(10,2) DEFAULT NULL COMMENT 原价用于显示折扣, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量, image_url VARCHAR(500) DEFAULT NULL COMMENT 商品主图路径, description TEXT COMMENT 商品描述, sales_count INT NOT NULL DEFAULT 0 COMMENT 销量用于前台排序, status TINYINT NOT NULL DEFAULT 1 COMMENT 商品状态1上架0下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_status (status) ) ENGINEInnoDB AUTO_INCREMENT1000 DEFAULT CHARSETutf8mb4 COMMENT电脑配件商品表;price用DECIMAL(10,2)不用FLOAT或者DOUBLE否则金额计算会出现莫名的浮点数误差这是银行系统带出来的血泪经验。category_id建索引因为前台商品列表和后台管理都要按分类查。status区分上架和下架下架商品在前台直接不展示但后台管理还能看到。ON UPDATE CURRENT_TIMESTAMP让update_time自动刷新省得每次更新数据还要手动维护时间字段。AUTO_INCREMENT从1000开始加商品时id不会从0开始看起来更像真实的商用系统。3.2 订单表的状态机设计从待付款到已取消状态流转要能用代码约束订单表是整个商城业务逻辑最密集的地方课程设计答辩时考官最爱追问的就是“订单状态怎么设计的”。状态不能是普通字符串字段随便填要有一套状态机约束。常见设计是status字段用TINYINT存数字状态码0待付款、1待发货、2待收货、3已完成、4已取消。整个流转方向是0→1→2→3或者0→4。这个流转规则要同时写在数据库字段注释里和Service代码里。-- 订单主表一个订单包含多个明细 CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT COMMENT 订单ID, order_no VARCHAR(32) NOT NULL COMMENT 订单编号唯一, user_id INT NOT NULL COMMENT 下单用户ID, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, pay_amount DECIMAL(10,2) NOT NULL COMMENT 实付金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 订单状态0待付款1待发货2待收货3已完成4已取消, receiver_name VARCHAR(50) NOT NULL COMMENT 收货人姓名, receiver_phone VARCHAR(20) NOT NULL COMMENT 收货人电话, receiver_address VARCHAR(200) NOT NULL COMMENT 收货地址, pay_time DATETIME DEFAULT NULL COMMENT 支付时间, delivery_time DATETIME DEFAULT NULL COMMENT 发货时间, finish_time DATETIME DEFAULT NULL COMMENT 完成时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;order_no要有唯一索引因为直接用自增id当订单号会暴露系统每天的订单量而且多表联查时字符串订单号比数字id更安全。pay_amount单独存一个字段和total_amount分开因为商城做活动时可能有打折订单页展示原价和实付价数据库要把两个值都留下来方便对账。pay_time、delivery_time、finish_time分别记录三个关键节点的时间这三个字段是后面统计“平均发货时长”“订单完成率”的数据来源课程设计里画折线图、柱状图这些能出彩的展示就靠它们。订单明细表要冗余一份商品名称和商品快照价格。为什么叫快照因为商品价格会调、商品可能下架但用户已经下的订单不能跟着变。比如周一用户下单时显卡卖2999元周二调价到3299元这份订单的明细里必须还记着2999。所以order_item表里除了product_id还要有goods_name、goods_price这两个冗余字段从商品表复制进来。查询历史订单时直接用明细表里的数据不join商品表。这也是为什么一张订单里即使商品后来删除了历史订单依然能正常显示的原因。3.3 库存扣减的并发问题下单先减库存还是先校验库存这里藏着大坑库存在商城系统里是典型的并发数据。用户A和用户B同时看到一款显卡只剩最后1件两个人都下单如果不做控制就可能两个订单都成功库存变成-1。数据库层面最简单的方案是UPDATE product SET stock stock - 1 WHERE id ? AND stock 0这句SQL能在数据库层面保证扣减操作是原子的超出库存的更新影响行数为0代码里判断影响行数就能知道是否扣减成功。// OrderServiceImpl.java - 创建订单时的库存扣减逻辑 Override Transactional(rollbackFor Exception.class) public boolean createOrder(OrderCreateDTO dto) { // 1. 校验订单参数和购物车条目 // 2. 加锁扣减库存SQL中使用 stock 0 条件防止超卖 int updateRows productDao.deductStock(dto.getProductId(), dto.getQuantity()); if (updateRows 0) { throw new BusinessException(商品库存不足扣减失败); } // 3. 生成订单主记录和订单明细插入数据库 // 4. 清空购物车中已下单的商品 // 操作任何一步异常上面所有步骤自动回滚 return true; }!-- ProductMapper.xml - 安全的库存扣减SQL -- update iddeductStock UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity} /update注意这里不能先SELECT stock查出来等于多少再在Java里比较再执行UPDATE。因为Java代码的两条SQL之间有时间差另一个请求可能已经扣了库存你查到的stock是旧值这就是经典的竞态条件。把判断条件写进UPDATE的WHERE子句中让数据库自己判断才是正确的做法。Transactional表示只要后面任一环节抛异常前面扣减的库存会回滚所以不会出现“订单没创建成功但库存少了”的状态。事务这块还有个高级配置要留个心眼Transactional默认只对RuntimeException和Error回滚如果Service方法里自己catch了异常再抛一个普通的new Exception()事务是不会回滚的。自己代码里要统一抛BusinessException这类运行时异常或者在Transactional注解上显式加rollbackFor Exception.class。上面代码里两个都写了这是比较稳妥的保险做法。3.4 购物车放数据库还是放Session这是个值得在答辩时讲清楚的选择购物车方案有两个主流选择存Session或存数据库。存Session实现简单用户没登录也能加购物车请求速度快但换浏览器、关掉页面购物车就丢了而且Session默认存在服务器内存里用户多了占内存。存数据库的购物车表体验好用户登录后在任何电脑上都能看到相同的购物车但需要多一张表、多一套增删改查。课程设计我建议表结构里把购物车表建出来因为答辩时“Redis缓存购物车”这个话题你能接得上而且数据库课程设计里购物车本身就是一张标准的业务表。-- 购物车表 CREATE TABLE cart_item ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL COMMENT 用户ID, product_id INT NOT NULL COMMENT 商品ID, quantity INT NOT NULL DEFAULT 1 COMMENT 数量, checked TINYINT NOT NULL DEFAULT 1 COMMENT 是否勾选1勾选0未勾选, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_product (user_id, product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT购物车表;user_id加product_id做联合唯一索引用户重复添加同一款商品时代码里先查这条记录存在就UPDATE数量加1不存在才INSERT新记录这样购物车里同一件商品不会出现两行。checked字段是用复选框批量结算时用的前端勾选哪几项、下单时就只结算那几项不要全部塞进订单。4. 把SSM电脑配件商城跑起来从Maven工程结构到前后台联调的完整落地路径4.1 Maven工程结构和依赖清单先别急着写业务代码把骨架立住SSM项目最常见的工程结构是Maven的war包单体应用。先建一个空的Maven项目然后在pom.xml里引入依赖。依赖版本是个大学问SSM框架之间版本不兼容导致的报错能折腾一整天。我的个人经验是Spring用5.x、SpringMVC跟着Spring版本走、MyBatis用3.5.x、MyBatis-Spring用2.0.x这个组合经过大量项目验证。JDK建议用1.8JDK 11以上有些老版本配置类要额外加依赖。Tomcat用8.5或9和Servlet 3.1/4.0规范对应上。pom.xml里的关键依赖就这些!-- pom.xml 核心依赖 -- properties spring.version5.3.20/spring.version mybatis.version3.5.10/mybatis.version /properties dependencies !-- Spring核心context、webmvc、jdbc、tx -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis与Spring整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency !-- MySQL驱动8.x版本对应ci驱动类 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency !-- Druid连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.15/version /dependency /dependencies依赖配齐后目录结构按Maven标准来src/main/java放Java类按包分层controller、service、dao、entity、commonsrc/main/resources放配置文件applicationContext.xml、spring-mvc.xml、jdbc.properties、mapper目录src/main/webapp放JSP页面、静态资源、WEB-INF下的web.xml。这个结构的顺序就是SSM的加载顺序web.xml先启动监听器加载Spring的根容器DispatcherServlet加载SpringMVC的子容器子容器能访问父容器的Bean反过来不行。Controller在子容器Service和DAO在父容器这个分工搞反了会出现“注入失败”的典型报错。4.2 前端页面组织方式JSP加JSTL还是前后端分离对于课程设计场景前端用JSP是最自然的方案因为SSM本来就是为“服务端渲染”设计的。JSP里可以直接用JSTL的c:forEach循环输出商品列表不需要额外搭Vue或React的前端工程。全套技术栈限定在“Java基础SSMJSPMySQL”范围内答辩时考官也不会揪着前端框架不放。实际开发中我会用JSP拼接一个公共布局头部是导航栏包含首页、商品分类、购物车入口、用户登录状态主体是每个页面自己的内容底部是版权信息。用% include fileheader.jsp %静态包含公共部分减少重复代码。访问路径交给SpringMVC的视图解析器统一管理配置文件里的InternalResourceViewResolver把逻辑视图名解析成/WEB-INF/views/目录下的JSP文件。放在WEB-INF目录下面的JSP页面用户不能通过浏览器直接输入URL访问只能经过Controller跳转这样防止有人绕过登录直接访问后台管理页面。商品列表页是前台流量的入口列表页里每个商品卡片展示主图、名称、品牌、价格、销量点击跳转到商品详情页。详情页用JSP里el表达式取数据。后台管理页面不要和前台放在同一个Controller里单独建一个AdminController在类上加RequestMapping(/admin)方法上加RequiresPermissions之类的东西做权限控制——这个商城项目里最朴素的权限控制就是加一个AdminInterceptor拦截器检查Session里有没有管理员标记没有就重定向到登录页。4.3 后台管理功能的最小集商品管理、订单管理、用户管理后台管理是“毕业论文/课程设计说明书”里工作量最集中的部分。最基本的三个模块是商品管理、订单管理、用户管理。商品管理包含商品列表、新增商品、编辑商品、上架下架切换、库存调整。新增商品时要处理图片上传图片存放在服务器本地的upload目录数据库中存访问的相对路径页面显示时拼上项目上下文路径就完成访问。图片上传用commons-fileupload组件配置一个MultipartResolver限制单个文件大小为2MB、总大小10MB这些参数都写在SpringMVC配置文件的bean属性里。订单管理功能的重点是订单列表筛选和状态操作。列表页要支持按订单号、用户名、订单状态三个条件组合查询对应MyBatis里写动态SQL。订单状态操作只有两个核心动作管理员发货状态从1推到2、查看订单详情含收货地址和订单明细。用户确认收货状态从2推到3和取消订单状态从0推到4是用户端的操作管理员不要替用户执行。状态变更的操作每次都要校验“当前状态是否等于前置状态”比如已发货的订单不能直接变成待付款这个校验逻辑写在一个updateOrderStatus方法里通过SQL的WHERE id ? AND status #{expectStatus}来保证。用户管理在课程设计里做三板斧就够用户列表分页展示、启用/禁用、重置密码。禁用用户用status字段1正常、0禁用用户登录时校验这个字段被禁用的用户提示“账号已被禁用请联系管理员”。因为这是商城系统用户不能注册成管理员管理员账号直接通过SQL插入数据库密码用MD5加盐存储数据库里看到的是密文答辩时这个细节能加分。4.4 权限拦截器和登录状态保持过滤器还是拦截器前台用户登录和后台管理员登录在同一个系统里但权限边界要清楚。我建议用SpringMVC的HandlerInterceptor做登录拦截比Servlet的Filter更好定位具体Controller方法也方便在preHandle里拿到HandlerMethod判断权限级别。// LoginInterceptor.java - 前台用户登录拦截器 public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行静态资源和登录相关请求 if (handler instanceof HandlerMethod) { HandlerMethod hm (HandlerMethod) handler; if (hm.hasMethodAnnotation(PassLogin.class)) { return true; } } // 检查Session中是否有用户信息 Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /user/login); return false; } return true; } }这里加了一个自定义注解PassLogin在登录页、注册页、商品列表页、商品详情页这些不需要登录就能访问的接口上标注拦截器检测到注解就放行。购物车、订单结算、个人中心这些需要登录才能访问的路径不标注解拦截器统一拦截没登录就跳去登录页登录成功后用redirect回之前想访问的页面。登录状态的保持是用Session课程设计里不用上Redis和JWT但拦截器这个设计本身就是Java面试里常问的“SpringMVC拦截器和Filter的区别”的实操版本面试官问到你能拿出这个项目举例效果比背概念好很多。5. 电脑配件商城高频率问题排查与避坑从环境变量到数据库死锁的五个真实记录5.1 Maven工程导入后IDE大量报红依赖下载失败现象是导入项目后pom.xml第一行就报错spring-webmvc等依赖下面有红色波浪线Maven Dependencies里缺包。原因大概率是Maven配置的中央仓库连不通或者本地的JDK和Maven版本不匹配依赖下载不完整。排查时先看IDE的Maven控制台输出报错里有Could not transfer artifact字样说明网络问题有Invalid JDK version说明Maven运行的JDK不对。解决办法是检查IDE的Maven配置把settings.xml指向一个可用的私服镜像阿里云镜像最常见配置mirrorOf为central同时把Maven的Runner的JRE改成已安装的JDK 1.8。还有一个容易忽略的点Maven工程本身要配置Java compiler的level为1.8不然代码里用了Lambda表达式会报语法错误。5.2 Tomcat启动时Spring容器初始化报错提示找不到sqlSessionFactory现象是在IDE里部署到Tomcat启动日志打出BeanCreationException根本原因是SqlSessionFactoryBean创建失败再往下看Caused By多半是数据库连接失败。原因有三个高频项第一MySQL服务没启动——很多人只装了MySQL但没在服务里启动连接被拒第二jdbc.properties里的用户名密码和本地数据库不一致第三MySQL 8的驱动类写成了com.mysql.jdbc.Driver导致ClassNotFound。解决路径是按“先看数据库连不连得上→再看驱动类对不对→最后看URL参数全不全”的顺序排查。最快验证方法是先用命令行工具连一次数据库确认服务和账号没问题再回头看配置。项目启动依赖数据库这个顺序别搞反。5.3 前端页面显示中文乱码商品名称变成问号现象是JSP页面上从数据库查出来的商品名称显示正常但浏览器直接打开JSP页面时中文标签、静态文字全是乱码或者更隐蔽的是“新增商品”后在列表页看到商品名称乱码。第一种情况是JSP文件本身的编码问题检查% page contentTypetext/html;charsetUTF-8 languagejava %和文件的实际编码一致建议统一用UTF-8。第二种情况是请求POST提交时Tomcat默认用ISO-8859-1解码表单里提交的中文在Controller里收到乱码。解决方式是配置SpringMVC的CharacterEncodingFilter过滤器强制请求和响应都走UTF-8。但要注意一个坑forceEncoding这个参数要设为true否则只能设置响应的编码请求编码设不上。提示CharacterEncodingFilter是Servlet的Filter在web.xml里配置必须放在所有其他Filter的最前面。它只处理POST请求的编码GET请求的中文乱码要从Tomcat的server.xml里给Connector加URIEncodingUTF-8。5.4 查询出的商品列表里价格和库存为null其他字段正常现象是前端页面商品名称、品牌显示出来了但price和stock字段是空的。这个问题的原因十有八九是MyBatis的字段映射错位。实体类里定义的属性是price、stock数据库列名也是price、stock理论上能映射上但如果实体类里写的是productPrice这种驼峰命名的属性数据库列是priceMyBatis默认就匹配不上了。解决路径顺势而来开启mapUnderscoreToCamelCase能解决create_time映射到createTime这类下划线问题但解决不了productPrice和price这种名称完全对不上的问题。最不折腾的写法是SQL里给列起别名比如SELECT price AS product_price让别名和实体属性对得上。MyBatis的映射规则精细得有点玄学少依赖它自动匹配多在SQL里把别名写好才是稳妥的路子。5.5 下单时超卖或库存扣成负数现象是班里两个同学同时用这个系统买同一款限量商品数据库里库存居然出现负数。原因是代码写了“先查库存再减库存”的两步操作两条SQL之间没锁并发请求同时查到了库存还有1都认为可以下单各自执行了扣减。解决思路在3.3节里已经说了把判断库存的SQL动作合并成一条原子UPDATE。还有一种情况是下单时校验库存0通过了但扣减SQL没写stock #{quantity}这个条件直接SET stock stock - 1库存就穿底了。最低成本的止损方案是给product表加一个stock 0的CHECK约束但MySQL 8之前的版本不生效所以最靠谱的还是FROM条件判断。这个问题的排查信号是“订单创建成功但库存对不上账”直接用命令行查product表的stock字段再看代码里的扣减SQL写法马上就能定位。6. 让SSM商城项目“拿得出手”从日志记录到分期分页的答辩亮点怎么打磨项目跑通之后离“能交差”还有两步一是把代码和配置打磨得有章法二是把整个系统的设计讲得出彩。代码层的第一个亮点是给Controller加AOP日志用一个自定义注解标记需要记录操作的接口比如后台管理员上架下架商品、修改价格这类敏感操作操作人、操作内容、操作时间、操作前数据快照都记录到一张sys_log表里。这个功能工作量不大但呈现出来的效果是“这人有工程意识”答辩时能直接打开日志表给考官看比嘴说一百句“我做了日志”都有说服力。// SysLogAspect.java - 基于AOP的操作日志切面记录管理员关键操作 Aspect Component public class SysLogAspect { Autowired private SysLogDao sysLogDao; Around(annotation(com.mall.common.annotation.SysLog)) public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable { // 记录操作前时间 long startTime System.currentTimeMillis(); Object result joinPoint.proceed(); long endTime System.currentTimeMillis(); // 通过反射获取方法上的注解描述 MethodSignature signature (MethodSignature) joinPoint.getSignature(); Method method signature.getMethod(); SysLog sysLogAnnotation method.getAnnotation(SysLog.class); // 构建日志实体并插入数据库 SysLogEntity logEntity new SysLogEntity(); logEntity.setOperation(sysLogAnnotation.value()); logEntity.setMethod(signature.getName()); logEntity.setTimeCost(endTime - startTime); logEntity.setCreateTime(new Date()); sysLogDao.insert(logEntity); return result; } }第二个亮点是分页查询用上PageHelper插件别自己写LIMIT #{offset}, #{pageSize}。PageHelper用ThreadLocal实现使用时有一个必须记住的坑PageHelper.startPage(pageNum, pageSize)之后必须紧跟第一条执行的查询语句中间不能插入其他SQL操作否则分页会作用到错误的查询上。调试分页时如果发现“返回的总数是全表的总数不是筛选后的总数”多半就是查询中间夹了其他数据库操作把顺序换一下就正常。第三个必做的是统一的返回结果对象ResultT包含code、msg、data三个字段。Controller的返回类型统一用ResultT正常返回Result.success(data)抛出异常时由ControllerAdvice全局异常处理器捕获并返回Result.error(商品库存不足)。这样做的直接好处是前端不用在每个页面判断返回结构是否一致而且全局异常处理器能在日志里把堆栈信息记录下来用户看到的却是友好提示而不是一屏500报错。这个结构在Java面试里叫“统一封装返回结果”属于高频考点做项目的同时顺手把面试题也准备了。最后一个细节是代码里注释和命名习惯Service接口、实体类、DAO方法名这一段要形成自己的固定习惯。实体类字段和数据库列名一一对应、Service方法名用find、create、update、delete开头、Controller方法名用action开头或者直接语义化命名。答辩时考官翻代码就像看一个人写字的笔迹整齐的命名和局部简洁的注释完全能撑起“有工作经验”的印象分。时间管理上前面代码写好、能跑通的时间要压在总进度的70%剩下30%全部花在写说明书和准备演示数据上。演示数据不要只有一两条找个靠谱的电脑配件数据表导入三四十个商品、四五个分类、测试用户和测试订单页面展示出来的效果才不空。说到底技术点拆开都不难难的是把它们串起来形成一条顺畅的链路。这套SSM商城做完收获的不只是一份能交差的课程设计更是Java后端从“会写单点代码”到“理解系统如何运转”的跨越。少走捷径、多跑几遍调试才有顶着黑眼圈调通一个Bug时的那点成就感。希望这份落地笔记能帮你在做这个项目的路上少踩几个坑把精力花在真正有价值的设计和功能上。本文还有配套的精品资源点击获取
