
每年到了毕设季总会有学生来问同一个问题老师我想做一个SSM框架的宠物销售管理系统但不知道从哪里下手。说实话这个题目在我眼里是Java Web毕设里性价比很高的一类。它的业务链路完整从商品展示、购物车、下单到库存管理一应俱全技术考察点刚好覆盖Spring、SpringMVC、MyBatis三大框架的核心用法而且与现实商业场景贴合度高答辩时很容易讲出东西来。这篇文章我以一个实际带过学生做完这套系统的角度把整个项目从需求拆解、数据库设计、核心功能实现到部署踩坑完整过一遍。内容按照可复现的思路来组织做这个题目的同学可以直接对着搭框架也可以把里面提到的几个技术细节拿去应付答辩追问。文中涉及的表结构、接口流程、事务处理和权限方案都是毕设级别的标准做法不会故意堆复杂设计来增加负担。1. 把宠物销售全流程拆成可落地的功能模块1.1 系统里到底有哪几类人、要管哪些事宠物销售管理系统本质上是电商系统的垂直简化版核心参与者只有两类前台用户和后台管理员。前台用户关心的是买买买的体验注册登录、浏览首页宠物、按分类筛选、搜索关键字、查看宠物详情、加入购物车、结算下单、模拟支付、查看订单状态、确认收货以及维护个人中心的基本信息。后台管理员关心的是卖卖卖的运营管理宠物商品的上架与下架、编辑库存和价格、维护宠物分类、处理订单查看、发货、退款标记、管理注册用户禁用不活跃账号、发布公告通知。很多学生拿到题目之后第一反应是把功能堆得又大又全加了一堆评论回复、宠物寄养、预约洗护之类的模块。我建议核心模块控制在五个以内用户管理、分类管理、宠物商品管理、购物车与订单、公告管理。每个模块的深度做到能完整演示、能回答出实现细节的程度比十个浅尝辄止的模块更有说服力。1.2 一条完整交易链路要经过几个节点拆清楚业务闭环之后代码逻辑才不会乱。以一次正常的购买流程为例用户在前台注册账号并登录登录态由服务端Session或Token维护。用户浏览首页或通过分类、关键字筛选找到心仪的宠物。进入宠物详情页查看图片、年龄、品种、价格、库存。点击加入购物车系统检查该用户是否已添加过同一宠物是则数量加一否则新增一条购物车记录。用户进入购物车页勾选商品点击结算提交收货人姓名、电话、地址。后端创建订单主表记录和订单明细表记录同时清空已购买商品的购物车数据。用户模拟支付订单状态从待支付变为已支付同时扣减库存、增加销量。管理员在后台看到新订单点击发货。用户确认收货订单闭环收尾。这个过程对应到数据库操作就是user、cart、orders、order_item、pet这几张表的增删改查再配合事务保护。把链路在纸上画出来让每张表服务于链路中的某一个环节模块边界自然就清楚了。1.3 功能清单怎么增怎么减毕设选题最忌讳什么都想要。如果时间只有一个月我建议按优先级来排基础必备用户注册登录、宠物列表与详情、购物车增删改、订单创建、模拟支付、后台商品管理、后台订单管理。加分亮点多条件检索分类加关键字加价格区间、库存防超卖、登录拦截器权限控制、订单状态机流转、销售数据统计。可以舍弃在线真实支付接入支付宝微信需要企业资质、多级会员体系、复杂秒杀活动、消息通知推送。舍弃这些不是做不了而是会占用大量调试时间对毕业设计的核心考察点贡献很低。在需求文档里把功能模块列成表格标明每个模块的前后端实现方式答辩时老师一眼就能看出你做过系统的整体规划而不是拿到需求就盲目写代码。2. SSM分层架构与数据库建模七张表撑起一个销售闭环2.1 SSM各司其职为什么这个组合在毕设里依然能打SSM是Spring、SpringMVC、MyBatis三个框架的组合。虽然现在企业项目里更多用Spring Boot但很多学校毕设题目仍然明确要求SSM或者希望学生通过手动整合来理解框架底层运作。三个框架的分工可以这样记Spring是大管家负责管理所有Bean对象的创建、依赖注入以及声明式事务的控制SpringMVC是前台接待负责接收HTTP请求、解析参数、调用Service、把数据渲染到页面MyBatis是数据库操作的执行者负责把Java对象映射成SQL语句、执行查询并将结果集封装回Java对象。对比维度SSM手动整合Spring Boot配置量需要手写多个XML或配置类大量自动配置少很多模板代码理解深度必须清楚DispatcherServlet、ContextLoaderListener的关系方便但容易停留在会用答辩友好度可以深入讲原理回答为什么的问题容易卡在你解释下自动配置原理这类追问搭建成本前期整合需要一到两天几分钟启动适用场景学校指定要求或想扎实学原理课程设计或入职项目做SSM项目时要摆正心态不要抱怨配置繁琐。手动整合一遍你对IoC容器、依赖注入、AOP事务、Mapper代理机制的理解会上一个台阶这些恰好是Java面试高频考点。2.2 核心表结构与字段设计数据库设计是整个项目的地基表结构定了后面的代码只是围绕数据在转。我给出一套适合这个题目的七张表设计用户表 user字段包括 id、username、password、nickname、phone、email、avatar、role、status、create_time。其中 role 字段标记身份0 为普通用户1 为管理员status 控制账号是否被禁用。分类表 category字段包括 id、name、sort_order、create_time。如果宠物分类只有一级不需要加 parent_id 自关联保持简单。宠物商品表 pet这是核心业务表。字段包括 id、category_id、name、breed、age、sex、price、original_price、stock、sales、image、description、status、create_time、update_time。status 为 0 下架、1 上架前台查询必须过滤 status1。image 保存图片的访问路径如果有多个图片可以另建表或使用逗号分隔。购物车表 cart字段包括 id、user_id、pet_id、quantity、add_time。购物车是典型的中间关系表不需要存商品名称和价格快照展示时联查 pet 表即可。订单主表 orders字段包括 id、order_no、user_id、total_amount、status、consignee_name、consignee_phone、consignee_address、pay_time、ship_time、finish_time、create_time。order_no 是业务订单号要保证唯一性后续用户查看和后台搜索都靠它。订单明细表 order_item字段包括 id、order_id、pet_id、pet_name、pet_image、price、quantity、subtotal。这里必须冗余商品名称、图片、价格快照因为商品信息后续可能变动而历史订单要保留交易发生时的原始数据。这一点在答辩时如果被问到订单表为什么不像购物车那么简单是非常好的回答素材。公告表 notice字段包括 id、title、content、create_time。用于后台发布系统公告前台用户能看到。表与表之间的关系很简单用户与宠物是多对多通过购物车表关联订单与订单明细是一对多订单与用户是多对一。所有外键逻辑交给应用层控制不必在数据库里强加物理外键避免增删数据时出现外键约束干扰。2.3 Maven项目结构与分包规范不建议毕设项目强行使用多模块Maven工程单模块足够。分包建议这样划分controller存放SpringMVC的控制器负责接收请求和返回视图。service 与 service.impl接口加实现类业务逻辑放在实现类中接口便于扩展和测试。mapperMyBatis的Mapper接口方法名对应mapper XML里的语句ID。pojo或者叫 entity/domain数据库表对应的实体类。common存放通用返回结果类、常量、异常处理类。interceptor存放登录拦截器、权限拦截器。utils工具类如时间处理、订单号生成、MD5加密。resources 下放 Spring 配置文件、MyBatis映射文件、数据库连接配置。包名的规范程度直接影响代码审阅的印象分。我见过不少学生把Controller和Servlet混着写或者一个类放到三千行这类代码即使功能跑通了答辩展示时也很容易被追问到项目组织问题。3. 核心交易链路怎么打通商品检索、购物车、订单与库存3.1 多条件商品检索MyBatis动态SQL写法前台商品列表最常见的查询是分类、关键字、价格区间这三个条件任意组合。直接写死SQL完全行不通必须用MyBatis的动态SQL。核心是where标签配合if标签自动拼接条件。select idselectPetPage resultTypecom.example.pojo.Pet SELECT * FROM pet where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR breed LIKE CONCAT(%, #{keyword}, %)) /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if AND status 1 /where ORDER BY sales DESC /select这段XML里有两个细节值得注意。第一where标签会自动去掉第一个多余的AND这是MyBatis非常实用的特性。第二LIKE查询拼接时要使用 CONCAT 函数不能直接写%${keyword}%因为${}是字符串拼接存在SQL注入风险。#{}是预编译占位符安全性能得到保证。分页推荐使用PageHelper插件只需要在查询前调用 PageHelper.startPage(pageNum, pageSize)MyBatis会在执行查询时自动拼接limit返回的数据会包装成 PageInfo连总数和总页数都计算好了。3.2 购物车的加购、合并与结算逻辑购物车的设计并不复杂但要注意重复加购的处理。用户连续两次点击加入购物车时不能直接无脑插入新记录而是先根据 user_id 和 pet_id 查购物车表如果已经存在则执行数量加一否则才插入新记录。Override public void addCart(Long userId, Long petId) { CartExample example new CartExample(); example.createCriteria().andUserIdEqualTo(userId).andPetIdEqualTo(petId); ListCart list cartMapper.selectByExample(example); if (list.isEmpty()) { Cart cart new Cart(); cart.setUserId(userId); cart.setPetId(petId); cart.setQuantity(1); cartMapper.insert(cart); } else { Cart exist list.get(0); exist.setQuantity(exist.getQuantity() 1); cartMapper.updateByPrimaryKey(exist); } }购物车列表展示时要注意联查商品表用VO类接收关联结果。字段里需要包含 pet_name、pet_image、price、quantity、subtotal。这种用联查返回组合数据的方式是Service层的常规操作也是MyBatis中resultMapassociation或使用JOIN加自动映射的典型场景。结算时勾选购物车中若干商品前端把这些 cart id 传到后端后端查询对应的购物车明细并生成订单。3.3 下单事务一主多明细不能丢任何一条订单创建是整个项目里最需要事务保护的地方它涉及三个数据操作插入订单主表、批量插入订单明细表、删除购物车记录。任何一步失败都不允许订单主表残留半成品数据。Transactional(rollbackFor Exception.class) Override public Order createOrder(Long userId, ListLong cartIds, OrderAddress address) { ListCart cartList cartMapper.selectByCartIds(cartIds); if (cartList.isEmpty()) { throw new BusinessException(购物车数据为空); } // 计算总金额并校验库存 BigDecimal totalAmount new BigDecimal(0.00); for (Cart c : cartList) { Pet pet petMapper.selectByPrimaryKey(c.getPetId()); if (pet.getStock() c.getQuantity()) { throw new BusinessException(商品库存不足 pet.getName()); } totalAmount totalAmount.add(pet.getPrice().multiply(new BigDecimal(c.getQuantity()))); } // 插入订单主表 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setStatus(0); // 0待支付 order.setConsignee(address.getName()); order.setConsigneePhone(address.getPhone()); order.setConsigneeAddress(address.getDetail()); orderMapper.insert(order); // 批量插入订单明细 for (Cart c : cartList) { Pet pet petMapper.selectByPrimaryKey(c.getPetId()); OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setPetId(pet.getId()); item.setPetName(pet.getName()); item.setPetImage(pet.getImage()); item.setPrice(pet.getPrice()); item.setQuantity(c.getQuantity()); item.setSubtotal(pet.getPrice().multiply(new BigDecimal(c.getQuantity()))); orderItemMapper.insert(item); } // 清空已结算的购物车 cartMapper.deleteByIds(cartIds); return order; }写这段逻辑时有三个坑要提醒。第一Transactional注解默认只对 RuntimeException 回滚如果抛的是受检异常需要显式设置 rollbackFor。第二库存校验和扣库存如果不是原子的并发时会出现问题这个问题在下一节展开。第三下单时前端传过来的金额绝不能信任后端必须根据数据库里的价格重新计算否则用户可以篡改请求参数实现0.01元买宠物。为了验证事务确实生效我建议你故意在插入明细之后抛出 RuntimeException再检查数据库会发现订单主表和明细表都不会出现残存记录。这个演示在答辩现场非常有冲击力。3.4 支付、发货与库存防超卖模拟支付的后端操作是更新订单状态为已支付、扣减库存、增加销量。很多学生会写出这样的代码Pet pet petMapper.selectByPrimaryKey(petId); if (pet.getStock() quantity) { pet.setStock(pet.getStock() - quantity); petMapper.updateByPrimaryKey(pet); }这个写法在单用户测试时没问题但并发场景下会超卖。两个用户同时查到库存还剩1条都判断库存足够都执行减库存最终库存变成 -1。毕设级别的解决方案有两种。第一种最简单在扣库存的SQL里加数量条件让数据库决定是否成功UPDATE pet SET stock stock - #{quantity} WHERE id #{petId} AND stock #{quantity}受影响行数为1表示扣减成功为0表示库存不足。这种条件更新利用的是数据库行锁的原子性代码简单且能应对大多数并发场景。第二种是乐观锁给 pet 表增加 version 字段更新时同时校验版本号UPDATE pet SET stock stock - #{quantity}, version version 1 WHERE id #{petId} AND stock #{quantity} AND version #{version}如果 update 后的受影响行数为0说明期间有其他事务修改过这条数据需要重试或提示用户手速太快重新下单。两种方案对比下来条件更新对毕设来说更方便因为它不需要重试机制一条SQL就能同时完成校验和更新。答辩时把两种方案的优劣讲清楚属于非常加分的细节。订单状态机上建议使用状态值加时间字段的方式管理0 待支付、1 已支付、2 已发货、3 已收货完成、4 已取消。每个状态变化都对应一个后端方法比如用户支付调用 payOrder管理员发货调用 shipOrder用户确认收货调用 confirmOrder。方法内部只允许从合法前置状态流转到目标状态避免用户直接跳过支付就把订单改成已完成。4. 登录拦截与权限隔离答辩追问率最高的区域4.1 用拦截器实现登录态校验如果不做任何登录校验用户可以直接在地址栏输入 /admin/order/list 访问后台接口这种隐患在答辩演示中一旦被发现项目档次立刻下降。SpringMVC的拦截器 HandlerInterceptor 是解决这个问题的标准方案。创建一个 LoginInterceptor在 preHandle 方法里检查 Session 中是否存有 user 对象public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object user session.getAttribute(loginUser); if (user null) { // AJAX请求返回JSON提示普通页面请求重定向到登录页 if (XMLHttpRequest.equals(request.getHeader(X-Requested-With))) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\请先登录\}); } else { response.sendRedirect(request.getContextPath() /login); } return false; } return true; } }拦截器不能所有路径都拦登录页、注册接口、静态资源css、js、img必须放行。在SpringMVC配置中注册拦截器并指定排除路径即可。这里要特别说明为什么用拦截器而不是过滤器。过滤器 Filter 是Servlet规范的一部分在 SpringMVC 的 DispatcherServlet 之前执行拦截器 HandlerInterceptor 是SpringMVC框架的组件它不仅能访问请求上下文还能拿到目标 Handler并且拦截器本身是Spring容器中的Bean可以注入Service依赖。用拦截器做登录校验最大的优势是可以精细排除静态资源和放行指定路径代码结构也更符合SpringMVC的开发习惯。4.2 管理员与普通用户的权限隔离用户表的 role 字段负责区分身份。后台管理接口统一放在 /admin/** 路径前缀下再配置一个 AdminInterceptor在登录校验通过之后继续校验当前登录用户的 role 是否为管理员。public class AdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(loginUser); if (user ! null 1.equals(((User) user).getRole())) { return true; } response.setContentType(text/html;charsetUTF-8); response.getWriter().write(403 无权限访问); return false; } }安全起见登录成功后存到 Session 的 User 对象里不应该包含明文密码可以把密码字段置空再放进去。密码存储必须加密不能直接明文入库。较稳妥的做法是 MD5 加固定盐或者直接使用 Spring Security 中的 BCryptPasswordEncoder 工具类它生成的结果自带随机盐安全性相比原始 MD5 高一整个档次。这个模块在答辩时几乎是必考区。老师通常问Session和Cookie有什么区别、拦截器和过滤器用哪个好、密码怎么存、为什么商品浏览接口不拦截但购物车接口要拦截。这些问题回答清楚了Java Web基础这一关就稳了。4.3 高频追问知识点Session、Cookie、拦截器与过滤器Session 数据存放在服务端内存中通过 SessionID 标识会话Cookie 数据存放在浏览器端每次请求自动携带。SessionID 通常就是以 Cookie 形式存储在客户端的。Session 在服务器重启或超过有效期后会失效所以多台服务器部署时要用共享Session容器单机毕设没有这个压力。过滤器属于Servlet容器层面拦截器属于SpringMVC框架层面。过滤器更偏向通用的请求预处理比如字符编码、跨域处理拦截器可以拿到处理该请求的 Controller 方法信息适合做精细化权限控制。实际项目中两者经常结合使用过滤器设置编码拦截器做登录和权限校验。平时自己写项目时可以故意打印执行顺序来验证请求到达后先由过滤器处理再进入 DispatcherServlet到达 Controller 方法前被拦截器拦截方法执行后按相反顺序处理响应。这个验证过程理解透了以后面试被问到一次请求的完整调用链也有话可答。5. 从零搭环境时最容易卡住的几个点位亲身踩坑记录5.1 Maven与JDK版本组合的稳定性问题很多学生第一步不是写代码而是在环境搭建阶段卡一整天。最典型的坑是IDEA默认内置的Maven版本与本地JDK版本不兼容导致编译报错或依赖包下载异常。我踩过之后得出的结论是毕设项目直接使用 Maven 3.6.3 配 JDK 1.8 是最稳的组合改好配置文件里的镜像地址后依赖下载速度也能接受。检查JDK版本可以通过命令行执行 java -version检查Maven版本执行 mvn -version。如果IDEA里出现 java: 程序包 org.apache.poi.ss.usermodel 不存在 这类错误多数情况不是代码问题而是依赖包没导入或Maven仓库缓存损坏执行 clean 后重新 import 项目即可。5.2 SSM整合时事务不生效的经典场景SSM手动整合时最经典的报错是事务不生效。表现是代码里明明写了 Transactional但操作报错后数据还是会残留在数据库里。根因通常是 SpringMVC 配置文件中扫描了包含 Service 的包同时 applicationContext.xml 也扫描了导致 Service 对象被创建了两份。DispatcherServlet 中初始化的容器优先使用 SpringMVC 扫描出来的那份而事务增强是在 Spring 容器中通过 AOP 代理实现的两份对象不一致导致注解形同虚设。解决方案很简单springmvc.xml 里context:component-scan base-packagecom.example.controller/只扫描 ControllerapplicationContext.xml 里扫描 service、mapper、配置事务管理器。这个坑几乎每年都会有人踩写配置时多花一分钟检查扫描范围能省下半天调试时间。5.3 MySQL8连接配置的特殊坑如果本机安装的是 MySQL 8.x数据库连接配置和传统写法完全不同。第一JDBC驱动类要写成 com.mysql.cj.jdbc.Driver如果继续用 com.mysql.jdbc.Driver 会直接报 ClassNotFoundException。第二JDBC URL 需要带上时区参数和关闭SSL校验jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/pet_shop?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456如果使用 MySQL 5.7 及以下版本驱动类用 com.mysql.jdbc.DriverURL 中也不需要 serverTimezone 参数。数据库版本和驱动不匹配时控制台报的错误是 Could not create connection to database server排查路径就两步确认驱动包版本、确认连接参数。5.4 答辩演示脚本与数据准备工作技术之外答辩演示也是需要提前准备的环节。我建议做三件事。第一准备足量的演示数据。宠物商品至少放十几种覆盖猫、狗、小宠等分类每类商品的图片、价格、库存、销量字段要完整订单数据至少三条不同状态的记录方便演示订单列表和状态流转。第二准备一份30秒的走查脚本。从首页打开开始用最短的路径完成注册登录→搜索宠物→加入购物车→下单→模拟支付→后台发货→用户确认收货的完整演示每一步在界面上能快速定位到按钮位置。第三想好项目难点的答案。不要笼统地说项目不难从下面三个方向中挑一个重点讲异步多线程处理订单号生成、条件更新SQL解决并发扣库存、拦截器统一权限校验。把难点讲清楚配合一段代码演示比任何自夸都更有说服力。如果项目的某些模块借鉴了开源项目或网上资料答辩时大方承认即可甚至可以主动说明参考了哪些设计思路这比被老师当场戳穿强得多。毕设评估的是你有没有真正理解和消化这些内容而不是要求你从零发明一套系统。最后还想多说两句每次带学生做完类似项目我都会反复强调一个观念毕设与其说是在给学校交差不如说是在给自己补一堂集成实战课。你把这个宠物销售系统的表结构、事务边界和权限拦截吃透了后面去面Java岗位时至少能找到三四个有实操经验的面试案例可讲。如果你正准备动手做这个题目我的建议是先别急着复制网上整套源码。按照我上面给出的表结构和模块划分自己动手建数据库、写Mapper、搭拦截器大概两到三周时间就能把购物车和订单流转跑通。这个过程本身才是毕设真正值钱的地方。最后再分享一个小技巧做完系统之后给每个核心模块写一段我当时为什么这样做的注释或者笔记比如订单明细为什么冗余商品快照、库存扣减为什么要用条件SQL。答辩时这些话就是你的底气。祝各位都能顺利写完带着真正理解过的项目走出答辩教室。