每到毕业季,总有不少同学在私聊里问同一个问题:Java方向的毕业设计,选什么题既不太难又能顺利通过答辩?我提到频率最高的答案之一,就是基于SSM的饰品商城网站。这个题目看着普通,但背后的技术链路很完整——Spring负责对象管理、SpringMVC处理请求路由、MyBatis搞定数据库读写,三个框架各司其职,是一个能把课堂知识串起来的小型项目。更重要是饰品这个品类和服装、数码比,SKU少、分类简单、业务流程清晰,适合在有限时间里把核心功能做扎实。
这篇文章我想把项目的完整思路、数据库设计、功能实现过程以及踩过的坑,从“为什么这么做”讲起,而不是直接甩代码。如果你手里已经有源码,想快速跑起来看效果,可以直接跳到第6章,那里有完整部署步骤。
1. 选这个题目之前,先把这几件事想明白
1.1 为什么是SSM,而不是Spring Boot
很多同学现在一上来就写Spring Boot,理由是“配置更少、开发更快”。但作为毕业设计,SSM反而有它的优势:Spring、SpringMVC、MyBatis三者分开,每一层的边界非常清楚。答辩时你说“Controller调用Service,Service调用Mapper”,老师一听就知道你对分层有概念。如果全程用Spring Boot的自动配置,很多细节被隐藏了,被问到“过滤器在哪个配置里”“SQL映射文件怎么加载”时反而容易卡壳。
SSM还有一个现实好处:网上的参考代码、学习资料存量极大。遇到问题搜索一下,几乎都能找到类似案例。相比Spring Boot版本差异导致的坑,SSM的版本组合相对固定,更容易复现。
1.2 为什么做饰品商城,而不是做通用商城
通用商城题目太大,商品、订单、库存、营销、支付全都要做,很容易失控。饰品商城的好处是业务边界清晰:一类商品、一种典型购物流程、一个后台管理。包围的范围小了,你反而能把注册登录、商品列表、详情、购物车、下单、后台发货这些主链路做深。
饰品还有一个隐性优点:图片好看。商城类项目演示时,页面视觉效果非常影响第一印象。饰品商品图通常精致,用一套整齐的图片做数据填充,整个页面档次感就上来了,这对答辩时的主观评分有帮助。
1.3 这个项目做到什么程度算达标
以我见过的大多数合格作品来看,最低标准是“两条闭环”:前台用户能注册登录、浏览商品、加购物车、提交订单、查看订单状态;后台管理员能登录、管理商品分类、上下架商品、处理订单发货。能跑通这两条主链路,再有数据库表设计和项目文档配合,就已经是能过审的完整毕设。
想往上加分,可以再攻克“商品搜索分页”“图片上传”“订单状态流转”“登录拦截器”这几个点。它们不算难,但写进论文和答辩PPT里,会显得工作量和技术深度都够。
2. 开发环境与项目骨架搭建
2.1 环境版本清单与选型理由
我个人建议的固定版本组合如下:
| 工具 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | SSM项目最稳的版本,过高版本可能出现兼容问题 |
| Tomcat | 8.5 或 9.0 | 支持Servlet 3.1,JSP运行正常 |
| Maven | 3.6.x | 管理依赖,避免手动导jar包的混乱 |
| IDE | Eclipse 或 IDEA | 任选,IDEA对Maven支持更好 |
| MySQL | 5.7 或 8.0 | 5.7最稳,8.0需要调整驱动 |
| 数据库工具 | Navicat 或 DataGrip | 方便导入SQL、查看表结构 |
这套组合很好复现。同学之间传项目时,版本不一致是最容易出问题的点,统一版本能节省大量排查时间。
2.2 项目初始化与目录结构设计
创建Maven工程时,选择maven-archetype-webapp骨架,然后补全Java目录结构。标准结构长这样:
src/main/java ├── com/xxx/controller # Controller层,接收请求、返回视图或JSON ├── com/xxx/service # Service层接口 ├── com/xxx/service/impl # Service层实现 ├── com/xxx/dao # MyBatis的Mapper接口 ├── com/xxx/entity # 实体类 ├── com/xxx/interceptor # 登录拦截器 └── com/xxx/util # 工具类 src/main/resources ├── jdbc.properties # 数据库连接配置 ├── spring-mvc.xml # SpringMVC配置 ├── spring-mybatis.xml # Spring与MyBatis整合配置 └── mapper # MyBatis的XML映射文件 src/main/webapp ├── static # CSS、JS、图片 ├── WEB-INF/jsp # JSP页面 └── WEB-INF/web.xml # Web部署描述文件分层对应关系很清楚:JSP发请求到Controller,Controller调Service,Service调Mapper接口,Mapper XML负责SQL。这样分的好处是后续定位问题很快。比如商品列表出不来,先看Controller有没有接收到参数,再看Service层有没有返回数据,最后检查Mapper SQL是否写错,逐层排查。
2.3 核心公共配置解析
jdbc.properties是最基础的文件,很多人复制粘贴后忘记改密码,项目跑不起来还不知道为什么:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ssm_accessory jdbc.username=root jdbc.password=你的密码注意MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver,URL里还要加useSSL=false&serverTimezone=Asia/Shanghai。如果你用的5.7,驱动类名保持旧的com.mysql.jdbc.Driver即可。
Spring与MyBatis整合时,需要把数据源、SqlSessionFactory、Mapper扫描三件事配好。核心配置如下:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.xxx.dao"/> </bean>SpringMVC配置里要开启注解驱动、视图解析器指向JSP目录、静态资源放行:
<mvc:annotation-driven/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:resources mapping="/static/**" location="/static/"/>建议:JSP文件一律放在
WEB-INF/jsp下,不要放在webapp根目录。这样用户无法通过URL直接访问未经过Controller的页面,安全性更好,答辩时老师问起也有话可说。
3. 数据库设计——先把表结构抠清楚
3.1 核心业务表清单
饰品商城看起来功能多,但落到数据库里,核心表就这几张:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| tb_user | 前台用户 | 用户名、密码、昵称、电话 |
| tb_admin | 后台管理员 | 管理员账号、密码 |
| tb_type | 商品分类 | 分类名称 |
| tb_product | 商品 | 商品名、分类ID、价格、库存、图片、描述 |
| tb_cart | 购物车 | 用户ID、商品ID、数量 |
| tb_favorite | 收藏 | 用户ID、商品ID |
| tb_order | 订单主表 | 订单号、用户ID、总金额、状态、下单时间 |
| tb_order_detail | 订单明细表 | 订单ID、商品ID、商品名、单价、数量 |
| tb_notice | 公告 | 公告标题、内容、发布时间 |
基本覆盖了“用户逛店买东西,管理员后台发货”的全部数据需求。建表时我建议统一使用utf8mb4字符集,兼容表情符号,也避免中文乱码。
3.2 用户表和商品表字段设计
用户表的核心字段:
CREATE TABLE tb_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );密码不要存明文,用MD5加密后再入库。注册时做一次加密,登录时把输入的密码加密后和数据库比对。这个方法不难,但能体现安全意识,论文里可以单写一段。
商品表是前台页面数据的主要来源:
CREATE TABLE tb_product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, type_id INT, price DECIMAL(10,2), stock INT, image VARCHAR(255), description TEXT, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这里的status是上下架状态,1为上架,0为下架。后台编辑商品时可以切换状态,前台查询时只展示status=1的商品。这个字段避免了删除商品造成的历史订单数据断裂。
3.3 订单表为什么要拆主表和明细表
这是数据库设计的一个重要考点。我见过一些同学图省事,把订单里所有商品信息都塞进一张表,结果一个订单包含三件商品时,总金额、地址等信息重复存了三遍,既冗余又容易出错。
正确做法是拆两张表:tb_order存订单的整体信息,tb_order_detail存订单下的每件商品。举例,一个订单买了项链和耳环,主表里只有一条记录,明细表里有两条记录。明细表里冗余存商品名称、单价等信息——这些数据不查外键也能直接展示,避免删除商品后订单明细变得不可读。
下单时还要注意事务。扣减库存和生成订单必须同时成功或同时失败,否则会出现“订单创建了但库存没减”的问题。在Service方法上加@Transactional注解就能解决。
3.4 购物车与收藏的处理思路
购物车表的字段要明确:哪个用户、哪件商品、数量。设计时加上user_id和product_id的联合唯一索引,防止同一件商品被重复加入购物车。如果用户重复点击加入,应变成更新数量而不是新增一条记录。
收藏表的结构和购物车类似,区别是没有数量字段。用户点“收藏”时先查是否已存在,存在就取消收藏,不存在则插入记录,实现前端按钮的切换状态。
注意:很多新手喜欢把购物车存到Session里,省事不用建表。但这样用户换浏览器、换电脑购物车就丢了。数据库存购物车才能让“加购”和“结算”跨页面、跨设备保持一致,也是项目更接近真实商城的体现。
4. 前台功能实现与页面联动
4.1 用户注册与登录
注册页提交表单后,Controller接收用户名、密码、确认密码。先在后端查一次用户名是否已存在,存在就返回提示信息,不存在则密码加密切入数据库,然后跳转到登录页。
这部分有一个很常见的错误:只用前端JS做非空校验。如果用户绕过页面直接传参,后端没有校验,空数据就会进库。正确做法是Controller里再做一次非空判断,双保险。
登录成功后用Session保存用户信息:
@RequestMapping("/login") public String login(String username, String password, HttpSession session) { User user = userService.login(username, MD5Util.encode(password)); if (user != null) { session.setAttribute("user", user); return "redirect:/index"; } return "login"; }用户点击“退出”时,调用session.invalidate()销毁会话,再跳回首页。
4.2 商品展示与搜索分页
首页商品列表是流量入口,一个能用的商城必须有分页功能。用MyBatis写分页有两种方式:自己写LIMIT或者引PageHelper插件。
PageHelper更省事,引入依赖后,在查询前调用PageHelper.startPage(pageNum, pageSize)即可:
PageHelper.startPage(pageNum, 8); List<Product> products = productDao.selectByType(typeId); PageInfo<Product> pageInfo = new PageInfo<>(products);PageInfo里自带总条数、总页数、当前页等数据,前端用JSTL循环渲染即可。搜索功能在商品名上做模糊查询:
SELECT * FROM tb_product WHERE name LIKE CONCAT('%', #{keyword}, '%') AND status = 1这里要注意一句话:不要自己手动拼SQL字符串。用${}直接拼接用户输入会有SQL注入风险,MyBatis里一律用#{}传参。
4.3 商品详情与关联展示
商品详情页展示商品主图、名称、价格、库存、描述,同时显示同类饰品推荐。做法是详情页接收商品id,查询商品信息,再按该商品分类查一批同类商品:
Product product = productService.getById(id); List<Product> related = productService.listByType(product.getTypeId()); request.setAttribute("related", related);图片路径要约定好:上传的商品图统一放在项目某个目录下,数据库存相对路径,页面使用<img src="${product.image}">渲染。不要直接拼接操作系统的绝对路径,否则部署到别的电脑就找不到图片了。
4.4 购物车功能的实现
每次加入购物车时,前端传商品id和数量。后端逻辑分两种情况:购物车里已有该商品,数量累加;没有该商品,新插入一条记录。
public void addCart(int userId, int productId, int count) { Cart cart = cartDao.findByUserAndProduct(userId, productId); if (cart != null) { cart.setCount(cart.getCount() + count); cartDao.updateCount(cart); } else { Cart newCart = new Cart(userId, productId, count); cartDao.insert(newCart); } }购物车页面要显示“小计”和“总价”。总价有两种算法:前端JS逐个累加,或者后端把购物车数据查出来后用Java循环计算。建议用后端计算,把总价放进request作用域,JSP页面直接显示,这样刷新页面数据不会错。
修改数量时,前端监听输入框变化,点击“更新购物车”后提交整个表格,后端循环更新每一条数量。这里要加一个库存校验:如果用户把数量改到超过商品库存,直接给出提示,不能任由用户下单。
4.5 订单结算与模拟支付的完整流程
订单流程是最能展示项目完整度的部分,我会重点展开。用户从购物车页点击“去结算”,进入订单确认页,页面显示收货人信息(可以用用户手机号预填)、商品清单和总金额。确认后提交,后端执行如下逻辑:
- 遍历购物车中的商品,逐项检查库存是否充足;
- 生成订单号,规则可以设为“时间戳 + 用户id + 随机数”;
- 插入
tb_order主表记录; - 循环插入
tb_order_detail明细记录; - 扣减商品库存;
- 清空该用户的购物车记录。
这些步骤必须在一个事务里完成:
@Transactional public void submitOrder(int userId, String address) { List<Cart> carts = cartDao.findByUserId(userId); // 检查库存、生成订单、插入主表、插入明细、扣库存、清购物车 }支付这里是模拟支付:订单初始状态为“待付款”,用户点击“确认支付”后,把订单状态改为“已付款”。在这个页面还可以加一个简单的倒计时,增加“真实感”。其实不需要真的接支付接口,但“状态机”的概念要体现出来:
待付款 -> 已付款 -> 待发货 -> 已发货 -> 已完成后台管理员点击“发货”,订单状态从“已付款”变为“待发货”再变为“已发货”时,前台用户看到订单状态同步更新。这套状态流转能画成一张图放进论文,非常加分。
5. 后台管理端的实现要点
5.1 权限控制与拦截器
后台管理页面的所有请求必须以管理员身份访问。用SpringMVC的拦截器实现最合适:
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("admin"); if (admin == null) { response.sendRedirect(request.getContextPath() + "/admin/login"); return false; } return true; } }在spring-mvc.xml里配置拦截路径:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/admin/**"/> <mvc:exclude-mapping path="/admin/login"/> <mvc:exclude-mapping path="/admin/doLogin"/> <bean class="com.xxx.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>这样用户直接访问/admin/product/list时会先被拦截跳转到登录页,后台页面就被保护起来了。
5.2 商品管理与图片上传
后台商品管理页有增删改查四个功能。列表页用表格显示所有商品,支持编辑状态、删除、修改、新增。删除商品这里建议用逻辑删除(改status=0),不要物理删除,避免历史订单里的关联数据出问题。
图片上传是后台管理里一个容易出问题的地方。前端表单设置enctype="multipart/form-data",后端用MultipartFile接收文件:
@RequestMapping("/admin/product/save") public String save(Product product, @RequestParam("file") MultipartFile file) { if (!file.isEmpty()) { String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = System.currentTimeMillis() + "_" + new Random().nextInt(1000) + ext; String path = "D:/upload/" + fileName; file.transferTo(new File(path)); product.setImage("/upload/" + fileName); } productService.save(product); return "redirect:/admin/product/list"; }这里文件名用时间戳加随机数重命名,有两个原因:第一避免用户上传同名文件互相覆盖;第二避免中文文件名在URL中乱码。图片不能直接传到项目部署目录,因为项目重新部署后文件会被清掉。建议用一个独立的路径存放,同时配置Tomcat的虚拟目录映射到/upload/**。
5.3 订单管理与发货状态流转
后台订单列表展示所有订单,支持按状态筛选。管理员看到订单后点击“发货”,更新订单状态:
@RequestMapping("/admin/order/ship") public String ship(int orderId) { orderService.updateStatus(orderId, 3); return "redirect:/admin/order/list?status=2"; }我建议订单列表页把订单号和收货人信息放在一起展示,方便快速核对。明细表通过订单id查询后展示在一个弹窗或子页面里。后台整体风格不用花哨,用普通后台框架模板即可,重点是数据清晰、按钮逻辑正确。
6. 部署运行与常见问题排坑实录
6.1 把项目部署到Tomcat并跑起来
拿到源码后的标准启动步骤:
- 创建数据库,导入项目里的
sql文件; - 修改
jdbc.properties里的数据库账号密码; - 用IDEA打开项目,等待Maven下载依赖;
- 配置Tomcat,选择
war exploded部署方式; - 启动Tomcat,浏览器访问
http://localhost:8080/项目名/。
war exploded这种模式改动Java代码后会自动编译,刷新页面就能看到效果,比打成war包再部署方便得多。如果你的项目没有Maven支持,也可以手动把项目转成Web项目,然后把lib目录配好。但这容易漏依赖,建议优先用Maven。
6.2 导入项目后最常见的几个报错
这里整理一下我在实际中遇到的频率最高的报错:
| 报错现象 | 原因 | 解决方案 |
|---|---|---|
ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet | 项目没有引入SpringMVC依赖 | 检查Maven依赖是否下载完整,重新Import或Reimport |
Access denied for user 'root'@'localhost' | 数据库密码错误或账号无权限 | 检查jdbc.properties,在Navicat里实测同账号密码能否连接 |
Cause: java.sql.SQLSyntaxErrorException | SQL语句与数据库版本或字段不匹配 | 对照表结构和Mapper XML逐项检查,重点看字段名是否大小写一致 |
Whitelabel Error Page或 404 | 访问路径和Controller映射不匹配 | 检查@RequestMapping里的路径是否和页面链接一致 |
| JSP页面EL表达式不生效 | web.xml版本太低 | 用Servlet 3.1以上的web.xml配置metadata-complete="false" |
Invalid bound statement (not found) | Mapper接口和Mapper XML的namespace或方法id不对应 | 检查XML里的namespace是否为接口全限定名,方法id是否和接口方法名一致 |
第七个高频问题:java.lang.ClassCastException。这通常是因为Tomcat的lib目录下和项目里同时存在同一个jar包,导致类加载器重复加载。解决办法是删除WEB-INF/lib里和Tomcat自带的冲突jar包,或者更换Tomcat版本。
6.3 中文乱码的全面排查
乱码问题要从三个环节分别解决:
数据库层面,连接URL上加characterEncoding=utf8,建表时使用utf8mb4字符集。页面层面,JSP文件第一行声明contentType="text/html; charset=UTF-8",并且文件本身要用UTF-8编码保存。请求和响应层面,在web.xml里配置字符编码过滤器:
<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter>如果还乱码,检查IDEA底部“File Encoding”面板,把全局编码、项目编码、属性文件编码全部设置为UTF-8。绝大多数乱码问题的根源,是这三个环节有一个不一致。
7. 毕业设计答辩,准备好这几件事
7.1 功能演示顺序怎么排
答辩现场演示比背PPT更让人放心。我的建议是:先走前台用户流程,注册一个账号,登录,浏览首页商品,查看商品详情,加入购物车,提交订单,模拟支付;再切换到后台,管理员登录,看到刚才的订单,执行发货。整个流程一气呵成,等于完整走了一遍“顾客下单,商家发货”的商业闭环。
演示前把数据库中测试数据准备得干净整齐,商品图统一风格,分类做好排序。不要出现一个分类下面只有一件商品的情况,页面效果单薄会影响印象分。
7.2 答辩老师常问的问题
老师问的问题集中在几个方向:
- 为什么选SSM框架?三个框架分别负责什么?
- 数据库表为什么这么设计?订单主表和明细表的关系是什么?
- 购物车存在Session还是数据库?为什么?
- 密码是怎么存储的?有没有做加密处理?
- 用户直接访问后台页面,会不会被拦截?
- 下单时库存不足怎么处理?事务在哪里加的?
这些问题一旦你亲自把项目写了一遍,基本都能答上来。哪怕源码是参考的,也建议逐行读懂核心模块,至少要把“订单提交”“登录拦截”“数据库表关系”这三个点吃透。
7.3 想拿高分,可以自己加的加分项
如果你的时间来得及,我推荐几个性价比高的扩展:
把分页插件去掉,改成手写LIMIT分页,并在论文里写清楚分页参数的推导过程;给用户表加一个头像上传字段,个人中心可以修改头像;商品增加“上架时间排序”,让首页商品更动态;后台增加留言管理或公告管理模块;订单增加“取消订单”功能,状态机多一个分支。这些功能不大,但是答辩展示时能明显丰富项目内容。
关于代码里加不加注释,我的建议是:核心Service层和SQL的XML里加好注释,比全部代码堆砌注释更重要。老师翻代码时首先看Service逻辑和SQL语句,注释能帮助他们快速理解你的思路。
最后再说一点个人经验。走完整个SSM饰品商城的开发,最大的收获不是会用这三个框架,而是理解了什么叫“把大问题拆成小问题”。一个商城网站拆开看,无非是登录、展示、交易、管理几件事,每件事拆成一个模块,每个模块拆成几张表、几个接口、几个页面,开发压力瞬间就小了。你复现代码的时候,也建议按这个顺序来:先配环境,再导数据库,然后跑通前台,最后研究后台。不要一上来就追着某个报错卡死。
多试几次,跑通整条链路,你会发现自己对这个项目已经熟悉到可以给人讲清楚了。答辩这事,就真的只是一个环节而已。