简介:一套基于SSM框架实现的电脑配件销售系统,面向Java毕业设计、数据库课程设计等学习者,后台包含用户管理、商品分类、上下架、库存、订单等模块,前台支持注册登录、商品推荐、购物车、收藏、评论、充值支付等功能,适合作为B/S架构项目练手或毕设参考。资源为zip压缩包,共1344个文件,主要包含359个JS脚本、166个CSS样式、156个JSP页面、132个Java源文件及SQL数据库脚本等,整体大小22.21MB,目录结构清晰便于检索使用。目前已有133人学习下载。系统基于JDK1.8、MySQL5.7,采用Spring+SpringMVC+MyBatis+Maven整合开发,前端使用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 Boot,SSM的好处是每一层都暴露在明面上,XML配置和注解各占一半,对理解框架原理更有帮助。很多人在纠结“现在谁还用SSM,直接上Spring Boot不行吗”——这里要泼盆冷水:毕业设计和课程设计的评分点恰恰是SSM这套“看得出层次”的架构,B/S模式下的请求如何从浏览器走到数据库再返回,这个链路讲得越清楚,分数越好看。所以别急着嫌弃,先把这套骨架吃透。
2. B/S架构下SSM商城的分层拆解:一次“浏览显卡列表”的请求要经过哪几个类
2.1 B/S架构和C/S架构的差别,以及SSM商城为什么必须选B/S
B/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.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/computer_mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456这里的URL参数要单独说。useUnicode和characterEncoding=utf8解决中文乱码问题,serverTimezone=Asia/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 id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="initialSize" value="5"/> <property name="maxActive" value="20"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="typeAliasesPackage" value="com.mall.entity"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="configuration"> <bean class="org.apache.ibatis.session.Configuration"> <property name="mapUnderscoreToCamelCase" value="true"/> </bean> </property> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.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?categoryId=12。请求先被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 ,原路返回到Controller,Controller把数据塞进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层 List<Product> 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 List<Product> findByCategoryId(Integer categoryId) { // 参数校验:分类ID不存在时返回空列表,不直接抛异常 if (categoryId == null || categoryId <= 0) { return Collections.emptyList(); } return productDao.selectByCategoryId(categoryId); } }Controller用@Autowired注入Service,Service用@Autowired注入DAO,这个依赖链是SSM的经典写法。@Transactional加在Service实现类上,表示这个类里的方法都走事务,订单创建这类多步操作才能保证要么全成功要么全回滚。@RequestParam里的defaultValue给了一个默认兜底,用户没传categoryId时不会直接报400错误,而是按分类1查询。
MyBatis的XML文件长这样,注意这个SQL的写法能防止一个常见数据库问题:
<!-- ProductMapper.xml 中的查询语句 --> <select id="selectByCategoryId" resultType="com.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`) ) ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 id="deductStock"> 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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.8,JDK 11以上有些老版本配置类要额外加依赖。Tomcat用8.5或9,和Servlet 3.1/4.0规范对应上。
pom.xml里的关键依赖就这些:
<!-- pom.xml 核心依赖 --> <properties> <spring.version>5.3.20</spring.version> <mybatis.version>3.5.10</mybatis.version> </properties> <dependencies> <!-- Spring核心:context、webmvc、jdbc、tx --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis与Spring整合 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <!-- MySQL驱动,8.x版本对应ci驱动类 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <!-- Druid连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.15</version> </dependency> </dependencies>依赖配齐后,目录结构按Maven标准来:src/main/java放Java类,按包分层controller、service、dao、entity、common;src/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基础+SSM+JSP+MySQL”范围内,答辩时考官也不会揪着前端框架不放。
实际开发中我会用JSP拼接一个公共布局,头部是导航栏,包含首页、商品分类、购物车入口、用户登录状态;主体是每个页面自己的内容;底部是版权信息。用<%@ include file="header.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 contentType="text/html;charset=UTF-8" language="java" %>和文件的实际编码一致,建议统一用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加URIEncoding="UTF-8"。
5.4 查询出的商品列表里价格和库存为null,其他字段正常
现象是前端页面商品名称、品牌显示出来了,但price和stock字段是空的。这个问题的原因十有八九是MyBatis的字段映射错位。实体类里定义的属性是price、stock,数据库列名也是price、stock,理论上能映射上,但如果实体类里写的是productPrice这种驼峰命名的属性,数据库列是price,MyBatis默认就匹配不上了。解决路径顺势而来:开启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操作,否则分页会作用到错误的查询上。调试分页时如果发现“返回的总数是全表的总数,不是筛选后的总数”,多半就是查询中间夹了其他数据库操作,把顺序换一下就正常。
第三个必做的是统一的返回结果对象Result<T>,包含code、msg、data三个字段。Controller的返回类型统一用Result<T>,正常返回Result.success(data),抛出异常时由@ControllerAdvice全局异常处理器捕获并返回Result.error("商品库存不足")。这样做的直接好处是前端不用在每个页面判断返回结构是否一致,而且全局异常处理器能在日志里把堆栈信息记录下来,用户看到的却是友好提示而不是一屏500报错。这个结构在Java面试里叫“统一封装返回结果”,属于高频考点,做项目的同时顺手把面试题也准备了。
最后一个细节是代码里注释和命名习惯,Service接口、实体类、DAO方法名这一段要形成自己的固定习惯。实体类字段和数据库列名一一对应、Service方法名用find、create、update、delete开头、Controller方法名用action开头或者直接语义化命名。答辩时考官翻代码就像看一个人写字的笔迹,整齐的命名和局部简洁的注释完全能撑起“有工作经验”的印象分。时间管理上,前面代码写好、能跑通的时间要压在总进度的70%,剩下30%全部花在写说明书和准备演示数据上。演示数据不要只有一两条,找个靠谱的电脑配件数据表,导入三四十个商品、四五个分类、测试用户和测试订单,页面展示出来的效果才不空。
说到底,技术点拆开都不难,难的是把它们串起来形成一条顺畅的链路。这套SSM商城做完,收获的不只是一份能交差的课程设计,更是Java后端从“会写单点代码”到“理解系统如何运转”的跨越。少走捷径、多跑几遍调试,才有顶着黑眼圈调通一个Bug时的那点成就感。希望这份落地笔记能帮你在做这个项目的路上少踩几个坑,把精力花在真正有价值的设计和功能上。
本文还有配套的精品资源,点击获取