☰
SSM框架宠物销售管理系统毕设实战:从数据库设计到部署避坑
2026/10/9 6:41:48 网站建设 项目流程

每年到了毕设季,总会有学生来问同一个问题:"老师,我想做一个SSM框架的宠物销售管理系统,但不知道从哪里下手。"说实话,这个题目在我眼里是Java Web毕设里性价比很高的一类。它的业务链路完整,从商品展示、购物车、下单到库存管理一应俱全,技术考察点刚好覆盖Spring、SpringMVC、MyBatis三大框架的核心用法,而且与现实商业场景贴合度高,答辩时很容易讲出东西来。

这篇文章我以一个实际带过学生做完这套系统的角度,把整个项目从需求拆解、数据库设计、核心功能实现到部署踩坑完整过一遍。内容按照可复现的思路来组织,做这个题目的同学可以直接对着搭框架,也可以把里面提到的几个技术细节拿去应付答辩追问。文中涉及的表结构、接口流程、事务处理和权限方案,都是毕设级别的标准做法,不会故意堆复杂设计来增加负担。

1. 把宠物销售全流程拆成可落地的功能模块

1.1 系统里到底有哪几类人、要管哪些事

宠物销售管理系统本质上是电商系统的垂直简化版,核心参与者只有两类:前台用户和后台管理员。

前台用户关心的是"买买买"的体验:注册登录、浏览首页宠物、按分类筛选、搜索关键字、查看宠物详情、加入购物车、结算下单、模拟支付、查看订单状态、确认收货,以及维护个人中心的基本信息。后台管理员关心的是"卖卖卖"的运营:管理宠物商品的上架与下架、编辑库存和价格、维护宠物分类、处理订单(查看、发货、退款标记)、管理注册用户(禁用不活跃账号)、发布公告通知。

很多学生拿到题目之后第一反应是把功能堆得又大又全,加了一堆评论回复、宠物寄养、预约洗护之类的模块。我建议核心模块控制在五个以内:用户管理、分类管理、宠物商品管理、购物车与订单、公告管理。每个模块的深度做到"能完整演示、能回答出实现细节"的程度,比十个浅尝辄止的模块更有说服力。

1.2 一条完整交易链路要经过几个节点

拆清楚业务闭环之后,代码逻辑才不会乱。以一次正常的购买流程为例:

  1. 用户在前台注册账号并登录,登录态由服务端Session或Token维护。
  2. 用户浏览首页或通过分类、关键字筛选找到心仪的宠物。
  3. 进入宠物详情页查看图片、年龄、品种、价格、库存。
  4. 点击加入购物车,系统检查该用户是否已添加过同一宠物,是则数量加一,否则新增一条购物车记录。
  5. 用户进入购物车页,勾选商品点击结算,提交收货人姓名、电话、地址。
  6. 后端创建订单主表记录和订单明细表记录,同时清空已购买商品的购物车数据。
  7. 用户模拟支付,订单状态从"待支付"变为"已支付",同时扣减库存、增加销量。
  8. 管理员在后台看到新订单,点击发货。
  9. 用户确认收货,订单闭环收尾。

这个过程对应到数据库操作,就是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 上架,前台查询必须过滤 status=1。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:接口加实现类,业务逻辑放在实现类中,接口便于扩展和测试。
  • mapper:MyBatis的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 id="selectPetPage" resultType="com.example.pojo.Pet"> SELECT * FROM pet <where> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR breed LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="minPrice != null"> AND price &gt;= #{minPrice} </if> <if test="maxPrice != 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); List<Cart> 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中resultMap+association或使用JOIN加自动映射的典型场景。

结算时,勾选购物车中若干商品,前端把这些 cart id 传到后端,后端查询对应的购物车明细并生成订单。

3.3 下单事务:一主多明细不能丢任何一条

订单创建是整个项目里最需要事务保护的地方,它涉及三个数据操作:插入订单主表、批量插入订单明细表、删除购物车记录。任何一步失败,都不允许订单主表残留"半成品"数据。

@Transactional(rollbackFor = Exception.class) @Override public Order createOrder(Long userId, List<Long> cartIds, OrderAddress address) { List<Cart> 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;charset=UTF-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;charset=UTF-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-package="com.example.controller"/>只扫描 Controller;applicationContext.xml 里扫描 service、mapper、配置事务管理器。这个坑几乎每年都会有人踩,写配置时多花一分钟检查扫描范围,能省下半天调试时间。

5.3 MySQL8连接配置的特殊坑

如果本机安装的是 MySQL 8.x,数据库连接配置和传统写法完全不同。第一,JDBC驱动类要写成 com.mysql.cj.jdbc.Driver,如果继续用 com.mysql.jdbc.Driver 会直接报 ClassNotFoundException。第二,JDBC URL 需要带上时区参数和关闭SSL校验:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/pet_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456

如果使用 MySQL 5.7 及以下版本,驱动类用 com.mysql.jdbc.Driver,URL 中也不需要 serverTimezone 参数。数据库版本和驱动不匹配时,控制台报的错误是 "Could not create connection to database server",排查路径就两步:确认驱动包版本、确认连接参数。

5.4 答辩演示脚本与数据准备工作

技术之外,答辩演示也是需要提前准备的环节。我建议做三件事。

第一,准备足量的演示数据。宠物商品至少放十几种,覆盖猫、狗、小宠等分类,每类商品的图片、价格、库存、销量字段要完整,订单数据至少三条不同状态的记录,方便演示订单列表和状态流转。

第二,准备一份30秒的走查脚本。从首页打开开始,用最短的路径完成"注册登录→搜索宠物→加入购物车→下单→模拟支付→后台发货→用户确认收货"的完整演示,每一步在界面上能快速定位到按钮位置。

第三,想好"项目难点"的答案。不要笼统地说"项目不难",从下面三个方向中挑一个重点讲:异步多线程处理订单号生成、条件更新SQL解决并发扣库存、拦截器统一权限校验。把难点讲清楚,配合一段代码演示,比任何自夸都更有说服力。

如果项目的某些模块借鉴了开源项目或网上资料,答辩时大方承认即可,甚至可以主动说明参考了哪些设计思路,这比被老师当场戳穿强得多。毕设评估的是你有没有真正理解和消化这些内容,而不是要求你从零发明一套系统。

最后还想多说两句

每次带学生做完类似项目,我都会反复强调一个观念:毕设与其说是在给学校交差,不如说是在给自己补一堂集成实战课。你把这个宠物销售系统的表结构、事务边界和权限拦截吃透了,后面去面Java岗位时,至少能找到三四个有实操经验的面试案例可讲。

如果你正准备动手做这个题目,我的建议是先别急着复制网上整套源码。按照我上面给出的表结构和模块划分,自己动手建数据库、写Mapper、搭拦截器,大概两到三周时间就能把购物车和订单流转跑通。这个过程本身才是毕设真正值钱的地方。

最后再分享一个小技巧:做完系统之后,给每个核心模块写一段"我当时为什么这样做"的注释或者笔记,比如订单明细为什么冗余商品快照、库存扣减为什么要用条件SQL。答辩时这些话就是你的底气。祝各位都能顺利写完,带着真正理解过的项目走出答辩教室。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询