
毕业设计选了个宠物销售管理系统又是一套典型的 SSM 框架项目。说实话刚开始我也有点疑惑宠物销售非要搞成管理系统吗后来和宠物店老板聊了聊才明白宠物不比普通商品它牵扯到品种、血统、疫苗、健康状态、库存流转、客户预订单……如果用 Excel 管光是品种和订单状态就能让人崩溃。所以这个项目不是简单的“增删改查”而是一个真正覆盖宠物入库、展示、下单、出库、客户管理、积分营销、员工权限的全流程平台。这次我会把整个系统的设计思路、核心代码、数据库表结构、部署过程和答辩要点全程复盘一遍。无论是正在做毕设的同学还是想练手 SSM 项目的 Java 学习者这套思路都能直接拿来用。下面不绕弯子直接从需求拆解开始讲。1. 项目概述与需求拆解1.1 宠物销售管理系统解决的核心问题宠物店的日常经营场景比想象中复杂。一批泰迪幼犬到店后要先登记品种、月龄、性别、颜色、价格、疫苗状态客户来看猫狗时可能看中某一只但暂时不想带走需要记录意向成交时要生成订单扣减库存并关联疫苗记录和售后信息如果客户是会员还要累计积分、参与优惠。这些环节如果靠纸质记事本加 Excel数据分散、状态不统一经常出现“狗还在但已下单”的库存错误。宠物销售管理系统要解决的正是这些让人头疼的问题宠物档案统一维护、库存扣减与订单同步、客户跟随与会员积分、员工权限分级、数据统计查看。系统面向三类用户普通游客只能浏览宠物列表和详情注册客户可以在线预约或下单管理员负责宠物、订单、客户、员工、数据的全量管理。宠物店老板则是超级管理员的角色可以查看每日销售额、库存预警和客户增长。这套系统和一个标准电商后台很像但比电商多了几层特殊逻辑宠物不是标准 SKU每一只有唯一编号相当于“一物一码”宠物有活体状态生病、疫苗、怀孕都会影响是否可售订单完成后还有售后回访流程。所以在设计时不能简单套用“商品表 订单表”的模板。1.2 SSM 框架成为毕业设计主流选择的原因我在选型时也纠结过 Spring Boot 和 SSM。现在企业里几乎都是 Spring Boot但毕设平台普遍还要求“SSM 框架”。原因其实很简单Spring Boot 自动配置太多很多东西帮你挡掉了答辩时老师问“SpringMVC 的 DispatcherServlet 是干什么的”“MyBatis 的 SqlSessionFactory 是怎么创建的”如果只答“Spring Boot 自动配置的”就显得很虚。SSM 把 Spring、SpringMVC、MyBatis 的手动装配过程暴露出来需要自己写 service 接口、自己配事务、自己写 mapper.xml反而更容易讲清楚底层原理。另一个原因是 SSM 足够轻量。Tomcat 8 JDK 8 MySQL 5.7这套组合在低配置电脑上跑得非常顺。项目结构直观controller - service - mapper 三层边界清清楚楚适合一个人从零写完。等真正理解了 SSM 的配置和运行机制再去上手 Spring Boot会顺畅得多。2. 核心技术选型与架构设计方案2.1 三个框架的分工与协作SSM 是三套框架的组合没有谁替代谁而是各管一段Spring 管“对象”和“事务”。所有 service、dao、controller 实例都由 Spring 容器创建并装配这叫 IoC控制反转。同时 Spring 的 AOP 机制被用来做声明式事务只要在 service 方法上标注TransactionalSpring 就会自动在方法前后开启提交或回滚事务不用手写 JDBC 事务代码。SpringMVC 管“请求”。客户端的 HTTP 请求先由 web.xml 配置的 DispatcherServlet 接收然后通过 HandlerMapping 找到对应 controller 中的方法再将方法的返回值交给视图解析器渲染成 JSP 或 JSON。所以一次请求的完整路径是浏览器 → DispatcherServlet → Controller → Service → Mapper → MySQL → 逆向返回到 JSP把数据填充到视图后响应给浏览器。MyBatis 管“数据库操作”。它是一个半自动 ORM 框架SQL 写在 mapper.xml 里由 Java 接口方法对应执行。半自动的意思是开发人员自己写 SQLMyBatis 负责把结果集映射成 Java 实体对象。好处是 SQL 可控性能透明适合像宠物订单这种复杂关联查询。三者协作的典型场景Controller 接收PetQuery请求参数调用PetService.listPetByPage(query)PetServiceImpl里通过PetMapper.selectPetPage查询数据库。这个过程中Spring 负责了 Controller、Service、Mapper 对象的创建和注入SpringMVC 完成了请求参数到 PetQuery 对象的绑定MyBatis 在 mapper 中执行了真实 SQL 并封装返回结果。2.2 为什么不用 Spring Boot 而用传统 SSM前面我说了毕设答辩展示原理的需求。但从开发效率角度Spring Boot 确实比手写 SSM 快很多。为什么还要“自讨苦吃”手写配置有一点务实的考虑Spring Boot 会全局搜索依赖自动配置数据源、事务管理器、视图解析器。一旦配置出现冲突报错信息往往特别抽象新手很难定位。而 SSM 配置项少web.xml、spring-mvc.xml、spring-mybatis.xml每个文件都是几百行左右任何一处写错报错位置非常明确反而更容易通过排查异常来加深理解。另外虽然项目叫 SSM但我在代码里保留了一些“未来迁移 Spring Boot 的接口习惯”比如 service 层全部面向接口编程mapper 注解和 XML 混合使用配置文件中不使用绝对的包扫描路径。这样做的好处是如果以后想把系统改造为 Spring Boot 版只需要把分散的配置文件合并成 application.yml再把 web.xml 的监听器、Servlet 配置换成自动配置类即可完成迁移核心业务代码一行都不用改。2.3 项目整体分层结构标准 Maven 项目Java 包结构如下com.pet.sales ├── controller // 接收请求返回视图或JSON ├── service // 业务接口 │ └── impl // 业务实现事务控制 ├── mapper // MyBatis 的 mapper 接口 ├── entity // 实体类与数据库表字段一一对应 ├── dto // 数据传输对象用于组合查询条件 ├── common // 分页结果封装、统一返回对象、常量 ├── interceptor // 登录拦截器、权限拦截器 └── utils // 时间处理、ID生成、MD5加密等JSP 页面放在webapp/WEB-INF/views下访问路径由 spring-mvc.xml 的视图解析器控制。resources 目录存放 mapper XML、spring 配置文件、log4j 配置等。我特别想强调dto包的重要性。很多初学项目的同学习惯把查询条件和实体混在一起宠物实体有 20 个字段搜索条件可能只有 4 个字段如果直接用 Pet 对象接收请求一方面参数冗余另一方面无法封装跨表查询结果。所以我的做法是单独写PetQueryDTO包含keyword按名字或编号模糊查询、categoryId品种分类、minPrice、maxPrice、status可售/已售/下架、以及分页用的pageNum、pageSize。3. 数据库设计与核心表结构3.1 数据库设计原则宠物销售管理系统的数据库设计我总结了三句话核心表宁拆勿合外键逻辑关联索引按查询列建立。宁拆勿合的意思是不要把“宠物信息”和“宠物图片”塞在同一张表。因为每只宠物有多张图片侧面、正面、疫苗本照片如果像设置几个 image1、image2 列数据库表会非常臃肿。我做的是pet主表存基础信息pet_image子表存图片路径一个宠物对应多条图片记录。外键逻辑关联的意思是不在数据库物理层建约束而是通过业务代码维护关联字段。比如订单表的pet_id、customer_id如果加物理外键批量导入数据或删除时容易触发意外错误。所以在建表时我只建索引不建FOREIGN KEY靠 service 层代码保证逻辑合理性。索引怎么规划针对高频查询字段建索引宠物品类表的主键、订单表的订单编号唯一键、订单表的客户 ID、宠物表的品种分类 ID。索引不是越多越好每条索引都会降低增删改性能一般控制在 5-6 个以下。3.2 核心表的字段说明与关系系统数据库共设计 6 张核心表管理员表、客户表、宠物分类表、宠物表、订单表、订单明细表这里拆成主表和明细表。各表字段说明如下。表名核心字段说明pet_system_adminid, username, password, real_name, role, create_time管理员/员工表role 区分超级管理员与普通员工pet_customerid, nickname, phone, password, points, balance, create_time注册客户记录积分与预充值余额pet_categoryid, category_name, description宠物分类狗、猫、兔、鸟等pet_infoid, pet_no, category_id, name, gender, age_month, price, stock_status, vaccine_status, health_status, description, create_time宠物档案stock_status 控制上下架pet_imageid, pet_id, image_url, sort_order宠物多图pet_orderid, order_no, customer_id, total_amount, pay_status, order_status, pay_time, create_time订单主表pay_status 区分未付/已付order_status 区分待发货/已发货/已完成/已取消pet_order_itemid, order_id, pet_id, pet_name, price, quantity订单明细因为一只宠物唯一quantity 通常为 1但保留字段方便扩展关系上pet_info 与 pet_category 是多对一pet_order 与 pet_order_item 是一对多pet_order 与 pet_customer 是多对一。3.3 建表 SQL 实例与注意事项核心建表 SQL 如下可以直接复制到 MySQL 中运行。我特意去掉外键约束但加上了索引和统一字符集。CREATE DATABASE IF NOT EXISTS pet_sales DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE pet_sales; CREATE TABLE pet_category ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 品类ID, category_name VARCHAR(50) NOT NULL COMMENT 品类名称, description VARCHAR(200) DEFAULT NULL COMMENT 描述 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物分类表; CREATE TABLE pet_info ( id INT PRIMARY KEY AUTO_INCREMENT, pet_no VARCHAR(30) NOT NULL UNIQUE COMMENT 宠物编号如PET20240601, category_id INT NOT NULL COMMENT 所属品类ID, pet_name VARCHAR(50) NOT NULL, gender TINYINT NOT NULL DEFAULT 1 COMMENT 1公 2母, age_month INT DEFAULT 1 COMMENT 月龄, price DECIMAL(10,2) NOT NULL COMMENT 销售价, stock_status TINYINT NOT NULL DEFAULT 1 COMMENT 1在售 2已售 0下架, vaccine_status TINYINT NOT NULL DEFAULT 1 COMMENT 疫苗状态 1已接种 0未接种, health_status VARCHAR(100) DEFAULT 健康 COMMENT 健康状况, description TEXT, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_status (stock_status), INDEX idx_price (price) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物信息表; CREATE TABLE pet_customer ( id INT PRIMARY KEY AUTO_INCREMENT, nickname VARCHAR(30) NOT NULL, phone VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT MD5加密后的密码, points INT DEFAULT 0 COMMENT 积分, balance DECIMAL(10,2) DEFAULT 0.00 COMMENT 账户余额, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客户表; CREATE TABLE pet_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单号时间戳随机数, customer_id INT NOT NULL COMMENT 客户ID, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0, pay_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未支付 1已支付, order_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待发货 1已发货 2已完成 3已取消, pay_time DATETIME DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_customer (customer_id), INDEX idx_status (order_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; CREATE TABLE pet_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, pet_id INT NOT NULL COMMENT 宠物ID, pet_name VARCHAR(50) NOT NULL COMMENT 下单时宠物昵称快照, price DECIMAL(10,2) NOT NULL COMMENT 下单时单价快照, quantity INT NOT NULL DEFAULT 1, INDEX idx_order (order_id), INDEX idx_pet (pet_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;总结经验pet_no、order_no都要设置唯一键这是避免重复订单、重复入库的关键。另外所有时间字段统一用DATETIME前端展示格式化避免用字符串存时间导致比较大小出错。4. 核心功能模块的实现拆解4.1 宠物库存管理模块条件查询与分页宠物列表查询是系统的门面。游客进入首页要能看到按品类筛选、按价格排序、搜索宠物昵称的入口。我实现一个统一的分页查询接口public interface PetService { // 分页 条件查询宠物列表 PageResultPetInfoVO queryPetPage(PetQueryDTO dto); }MyBatis 的 mapper XML 里我用if标签动态拼 SQL。这是 SSM 中比较高频的写法select idselectPetPage resultTypecom.pet.sales.entity.PetInfo SELECT p.*, c.category_name FROM pet_info p LEFT JOIN pet_category c ON p.category_id c.id where if testdto.keyword ! null and dto.keyword ! AND (p.pet_name LIKE CONCAT(%, #{dto.keyword}, %) OR p.pet_no LIKE CONCAT(%, #{dto.keyword}, %)) /if if testdto.categoryId ! null AND p.category_id #{dto.categoryId} /if if testdto.minPrice ! null AND p.price gt; #{dto.minPrice} /if if testdto.maxPrice ! null AND p.price lt; #{dto.maxPrice} /if if testdto.status ! null AND p.stock_status #{dto.status} /if /where ORDER BY p.create_time DESC LIMIT #{dto.offset}, #{dto.pageSize} /select这里有几个坑值得单独说。第一个是gt;和lt;转义在 XML 里不能直接写大于小于号必须转义否则解析报错。第二个是LIKE CONCAT(%, #{dto.keyword}, %)而不是直接写LIKE %${dto.keyword}%后者会引发 SQL 注入。第三个是分页参数我手动在 Java 中计算出 offsetoffset (pageNum - 1) * pageSize因为我不想额外引入 PageHelper手动控制更直观也方便答辩时讲明白。实际项目中使用 PageHelper 确实更简洁但毕设需要解释原理所以我故意手写。4.2 销售订单模块事务与并发控制订单是系统中最容易出 bug 的地方。用户提交订单时系统需要做三件事检查宠物状态是否为在售扣减宠物库存将 stock_status 从 1 更新为 2生成订单主记录和订单明细记录。这三步必须在一个事务里完成任何一步失败都全部回滚。为了模拟真实并发场景我考虑两个用户同时买同一只宠物的情况。如果在代码里先查询宠物状态再后续更新两个事务可能同时读到“在售”然后都生成订单造成超卖。解决办法是在扣减时使用乐观锁或行锁。我用的是行级锁更新UPDATE pet_info SET stock_status 2 WHERE id #{petId} AND stock_status 1如果这条 SQL 影响行数为 0说明宠物状态已经不是 1订单创建失败。这种写法依靠数据库行锁比先查后改安全得多。配合Transactional注解整个下单流程是原子的Transactional(rollbackFor Exception.class) public OrderCreateResult createOrder(OrderCreateDTO dto) { // 1. 锁定并扣减宠物状态 int updated petInfoMapper.lockPetForSale(dto.getPetId()); if (updated 0) { throw new BizException(该宠物已售出或已下架); } // 2. 创建订单主记录 PetOrder order new PetOrder(); order.setOrderNo(generateOrderNo()); ... petOrderMapper.insert(order); // 3. 创建订单明细 PetOrderItem item new PetOrderItem(); ... petOrderItemMapper.insert(item); // 4. 增加客户积分 customerMapper.increasePoints(dto.getCustomerId(), points); return result; }rollbackFor Exception.class很重要。Spring 默认只对 RuntimeException 回滚如果抛的是自定义受检异常事务不会回滚。所以我明确指定所有异常都回滚。另外我在 Service 实现类中写try-catch但不吞异常而是转为自定义业务异常重新抛出。一旦在 controller 层才统一捕获页面可以友好提示“该宠物已经被其他客户预订了”而不是展示一堆堆栈信息。订单编号由“时间戳 3 位随机数”组成形如20260705143215086。上学时有人直接在代码里用UUID.randomUUID()虽然保证唯一但订单号太长且无规律客户体验不好。我尝试用DateTimeFormatter格式化时间再拼接ThreadLocalRandom确保同一毫秒内不同线程生成的随机数大概率不同。生产系统常使用 Redis 自增序列但毕设中随机数方案已经够用并且答辩时也能说清楚。4.3 客户管理与会员积分设计客户注册登录后每次下单成功自动增长积分。积分规则在系统里这样设计消费满 1 元积 1 分积分在订单状态变为“已完成”后发放。为了不把逻辑写乱我在CustomerService中单独定义addPoints(customerId, amount)public void addPoints(Integer customerId, BigDecimal amount) { int points amount.setScale(0, RoundingMode.DOWN).intValue(); if (points 0) { customerMapper.increasePoints(customerId, points); } }为什么要在订单完成时发放而不是支付时因为如果订单最终被取消积分必须回滚。我在订单取消事件中同时增加了扣减积分的方法保证数据一致。这个细节很容易被忽略但我在画数据库关系时特意把“积分变动”做了单独的points_log表用于记录每一条积分增减的来源单号。虽然系统复杂度上升了一点但查账非常方便老板也认可。客户表里还有一个balance字段支持余额预充值。充值时更新客户余额并生成一笔recharge_record。下单可以选择余额支付支付时走的是“校验余额是否充足 → 扣减余额 → 更新订单支付状态”三步。由于三步必须一致我也放在一个事务方法中。4.4 权限登录模块拦截器 密码加密系统有三类角色未登录游客、注册客户、管理员/员工。不同角色的权限差异较大游客只能看宠物列表和详情下单必须登录客户可以管理自己的订单和个人信息管理员可以进入后台看到所有客户和订单超级管理员还能管理员工账号。我用 SpringMVC 的 HandlerInterceptor 实现登录和角色拦截避免在每一个 controller 方法里重复写权限判断。拦截器类public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Admin admin (Admin) request.getSession().getAttribute(admin); if (admin null) { // 判断是否为异步请求异步返回 JSON 提示否则重定向 response.sendRedirect(request.getContextPath() /admin/login); return false; } return true; } }然后在 spring-mvc.xml 或 spring-mvc.xml 中注册拦截路径mvc:interceptors mvc:interceptor mvc:mapping path/admin/**/ mvc:exclude-mapping path/admin/login/ mvc:exclude-mapping path/admin/doLogin/ bean classcom.pet.sales.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors游客端的订单操作也做了一层拦截拦截/customer/**路径。这里最值得提醒的是拦截器只处理前端的访问并不能真正保证 API 安全。有些同学用 Form 表单提交后端接口路径却没有加拦截导致别人可以绕过页面直接访问/order/create实现恶意操作。我在每一个需要登录身份的 controller 手段中都从 session 中取出用户 ID如果取不到直接抛出未登录异常。也就是说拦截器是“第一道门”方法内判断是“第二道门”。密码加密方面我使用MD5 随机盐的方式。注册时生成 6 位随机盐密码存储为MD5(password salt)拼接盐值。虽然现在更推荐 BCrypt但在 SSM 毕设中手写 MD5 也能讲清楚加密思路。答辩时如果老师问“MD5 为什么不安全”你可以答可以加盐并多次迭代但真实生产系统建议用 BCrypt。这也是诚实且专业的回答方式。5. 实操过程从搭建环境到打包部署5.1 开发环境安装与配置我使用的开发环境是JDK 81.8.0_202、Maven 3.6.3、Tomcat 8.5.85、MySQL 5.7、IDEA 2023。这里特别提醒不要用 JDK 17 搭配 Tomcat 8否则很容易遇到模块访问限制还是纯老环境最稳。JDK 安装后环境变量配置是新手最易出错的地方。现在不用像老教程那样配置JAVA_HOME吗实际上 IDEA 能自动识别 JDK但 Maven 和 Tomcat 命令行运行时还需要它。我建议还是配好JAVA_HOME C:\Program Files\Java\jdk1.8.0_202 Path %JAVA_HOME%\bin配置完后在命令行输入java -version看到1.8版本即可。如果电脑上装了多个 JDK经常出现版本不对的问题。我的做法是直接检查JAVA_HOME的路径是否指向了正确版本。至于 MavenIDEA 自带了 Maven但为了防止终端打包异常我建议把 Maven 放在纯英文目录配置阿里云镜像到conf/settings.xml否则下载依赖会很慢。5.2 手动搭建 SSM 项目骨架直接在 IDEA 中新建 Maven 项目选择maven-archetype-webapp然后在pom.xml中添加依赖Spring 5.3.x、mybatis 3.5.x、mybatis-spring 2.0.x、mysql-connector-java 8.0.x、jstl 和 servlet-api。资源目录结构需要手动创建。我建议严格按下面来src/main/java src/main/resources src/main/webapp/WEB-INF/views src/main/webapp/staticspring 配置文件拆成三个spring-context.xml扫描 service 和 controller 之外的 Bean、spring-mvc.xml扫描 controller配置视图解析器、spring-mybatis.xml配置数据源、SqlSessionFactory、Mapper 扫描器。web.xml 中加载 Spring 监听器和 DispatcherServlet。初学时会漏掉很多细节。比如spring-mvc.xml中必须加mvc:annotation-driven/否则RequestMapping不生效spring-context.xml中如果用context:component-scan base-packagecom.pet.sales/会把 controller 也扫描进去导致对象重复创建。我的建议是Spring 根容器只扫描com.pet.sales.service、com.pet.sales.dao等SpringMVC 容器只扫描com.pet.sales.controller。数据库连接配置放在jdbc.properties中并注意 URL 参数。MySQL 8 必须带serverTimezoneAsia/Shanghai和useSSLfalse否则会因为时区问题报错。5.7 虽然不强制但写上也没坏处jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/pet_sales?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password1234565.3 常见部署问题与解决我这里整理了几个我在开发过程中真实踩过的坑以及排查思路。Tomcat 端口被占用。启动时报Port 8080 was already in use。解决办法是杀掉占用进程或者修改 Tomcat 的server.xml端口。我建议使用cmd输入netstat -ano | findstr 8080查 PID再taskkill /PID 号 /F。改端口虽然方便但会影响后面所有 URL 记忆。JSP 页面中文乱码。最稳妥的方案是在 web.xml 加编码过滤器filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping同时数据库连接 URL 中也要带characterEncodingutf8页面顶部用% page contentTypetext/html;charsetUTF-8 %。三道门都设置好才能根治乱码。启动时提示找不到petInfoMapper的 bean。这通常是 mapper 扫描器没有配置包路径或 mapper 接口没有加Mapper注解。我用的是扫描器方式bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.pet.sales.mapper/ /bean这样就不用每个接口都加注解。但有一个注意点mybatis-spring 和 spring 版本要兼容mybatis-spring 2.0.x 对应 Spring 5不要用 1.3.x 去配 Spring 5容易在事务上出兼容问题。ClassNotFoundException: javax.servlet.jsp.jstl.core.Config。这是因为没有引入 jstl 依赖或者 Tomcat 中 jar 冲突。我直接用 Maven 引入 jstl 1.2 版本顺便把 standard 标签库一起引入。另外如果 JSP 中用了${pageContext.request.contextPath}就不需要再手动拼接绝对路径方便省事。6. 毕设答辩和面试中常被问到的问题6.1 为什么选择 MyBatis 而不是 Hibernate这个问题几乎必问。我的回答思路是MyBatis 是半自动 ORM手写 SQL对联合查询、多表分页和复杂 SQL 有很强的控制力。宠物销售管理系统里有大量自定义查询比如“按价格区间 品类 状态”多条件搜索用 MyBatis 的if标签可以动态组装 SQL保持灵活。Hibernate 是全自动 ORM开发速度快但多表关联查询会生成很复杂的 SQL后期难以优化。做毕设选择 MyBatis 还有一个考量更容易调试。想查看 Java 的 PreparedStatement 最终生成的 SQL直接看日志即可Hibernate 则更抽象。6.2 项目中哪里用了设计模式我会这样归纳Controller 层是 SpringMVC 的前端控制器模式的体现所有请求由 DispatcherServlet 统一分发Service 层用的是门面模式对外暴露简单的接口内部处理订单、库存、积分等多个组件MyBatis 是典型的代理模式我们只需要写接口不用写实现类MapperProxy 通过 JDK 动态代理执行 SQLSpring AOP 本身就是动态代理模式事务注解通过代理在方法前后增强。这样回答不仅贴合项目还能让老师看到你对设计模式有实际理解不是背书。6.3 如果数据量大了系统怎么优化这个问题我会从三个角度回答第一SQL 层通过EXPLAIN分析慢查询确保查询走索引避免大表全表扫描多条件查询时尽量用覆盖索引。第二缓存层宠物详情页访问频繁且变化不大可以用 Redis 缓存pet_info的热点数据库存变更时删除对应缓存。第三读写分离如果将来订单量增加把 MySQL 拆成主从读操作走从库写操作走主库。但我在毕设工程量有限的前提下只做了分页查询保证一页数据不超过 10 条避免一次性取出过多记录。实际项目中还需要考虑缓存穿透、击穿、雪崩这些可以作为扩展方向。6.4 个人心得与项目扩展建议把整个项目做完后我最大的感触是SSM 框架的精髓不在于三个框架本身多牛而在于如何把它们拧成一股绳。事务、AOP 依赖注入、请求响应模型、SQL 映射任何一环出问题都会让系统跑不起来。做宠物销售系统时我当时花了整整一天排查“为什么事务没有回滚”最后发现自己是在 Controller 层直接调用了事务方法Spring AOP 生成代理对象失效了。原因在于 controller 持有的是 JdkDynamicAopProxy 还是 CglibProxy。实际解决方法是把事务方法放到独立的 service bean 中确保从容器中拿对象调用而不是在类内部横向调用。后面如果要扩展建议从两个方向走一是引入 Redis 做登录验证码和购物车模块二是引入 RabbitMQ 做下单后的短信通知异步化。这两块都能给答辩加分而且代码量完全可控。如果你打算把项目写成线上演示版还可以把 JSP 换成 easyui 或 bootstrap界面干净一点老师演示起来也会更舒服。最后再分享一个小技巧毕业设计最怕没有可视化界面。当时我花了一晚上给宠物列表做了一个基于 ECharts 的销售统计图表按月份查看销售额、订单量、热门宠物品种排行。有了这个图表整个系统的完整度立刻提升了一个档次答辩老师当场还问我要了项目源码。所以如果你时间允许一定不要只做冰冷的表格要多一点能“讲出来”的功能。