☰
SpringBoot宠物商城毕业设计实战:MySQL多表+原生HTML前端
2026/10/7 2:55:22 网站建设 项目流程

简介:本资源是一套基于SpringBoot+MySQL+HTML开发的宠物商城管理系统,面向计算机专业本科生及Java初学者,适用于毕业设计、课程设计与期末大作业等实践场景。系统功能完整,涵盖商品展示、订单管理、用户中心、库存控制与支付对接等核心模块,界面简洁美观,代码结构清晰,经严格调试可直接运行,显著降低二次开发门槛。压缩包共2个文件(1个ZIP源码包+1个SQL数据库脚本),总大小14.06MB;ZIP内含前后端完整代码、IDEA项目配置及微信小程序适配支持,SQL脚本提供建库建表语句与初始数据,便于快速初始化环境。目前已有59人学习下载,资源由实战经验丰富的开发者整理,附带Navicat数据库操作指引与Maven依赖说明,特别适合希望掌握主流Java Web全栈开发流程的学习者,从环境搭建到功能部署形成闭环实践路径。

1. 这不是又一个“增删改查”Demo:基于 SpringBoot + MySQL + HTML 的宠物商城系统,真能当毕业设计交上去?

你翻过 GitHub 上那些标着「毕设」「Java 毕业设计」的仓库——十有八九是用户管理+商品管理+订单管理三件套,连前端都用 Thymeleaf 拼个th:each就完事,后台 Controller 里塞满@Autowired private XxxService,连事务边界在哪都懒得画。但这个「宠物商城管理系统」不一样:它没用任何前端框架(Vue/React),纯 HTML + CSS + JS 写了完整的商品详情页轮播、购物车实时计数、订单状态机流转(待付款→已发货→已完成)、甚至带宠物品种筛选的二级联动下拉(猫:英短/布偶/缅因;狗:金毛/泰迪/柯基)。MySQL 里建了 9 张表,含pet_category(分类树形结构)、pet_sku(SKU 级别库存与价格)、order_item(支持同一订单混搭活体宠物+食品+玩具),还配了application-prod.yml和application-dev.yml双环境配置。我拿它帮三个学生过盲审,导师问得最狠的是「活体宠物怎么退换货?」——系统里真写了「7 天无理由不适用,但提供 24 小时健康保障期」的业务规则校验逻辑。适合 Java 初学刚撸完《Spring Boot 实战》第 5 章、想交一份让答辩老师愿意多问两句的毕业设计的同学。别再抄「图书管理系统」了,宠物商城的业务细节足够撑起 30 页论文的「需求分析」和「数据库设计」章节。

2. 从零跑通:项目结构、依赖配置与三步启动法

2.1 项目骨架与核心模块划分

这不是单模块 Maven 工程。解压后你会看到标准的 SpringBoot 多模块结构:

  • pet-shop-parent(pom.xml 声明 parent,统一管理 SpringBoot 2.7.18 版本、Lombok、MyBatis-Plus)
  • pet-shop-common(工具类:IdWorker雪花 ID 生成器、Result<T>统一响应封装、PetEnum枚举定义)
  • pet-shop-api(Controller 层,含/api/pet、/api/order、/api/user三大 REST 接口组)
  • pet-shop-service(Service 层,关键逻辑在OrderServiceImpl.java:创建订单时校验库存、扣减、生成物流单号)
  • pet-shop-dao(Mapper 接口 + XML,PetMapper.xml里有<resultMap>显式映射pet_category.name到PetDO.categoryName)

提示:pet-shop-dao模块的pom.xml中<scope>provided</scope>仅对pet-shop-service生效,避免 MyBatis-Plus 与 SpringBoot Starter 冲突。这是很多同学本地跑不通的根源——他们直接把 Mapper 放进api模块,导致@MapperScan扫描失效。

2.2 MySQL 数据库初始化:建库、建表、插初始数据

项目根目录下有sql/pet_shop_init.sql,但不能直接全量执行。必须分三步:

  1. 手动创建数据库并指定编码(Windows 下 MySQL 8.0+ 必须显式声明):
CREATE DATABASE pet_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

注意:utf8mb4是硬性要求。宠物名称含 emoji(如🐶)或生僻字(如「黇」鹿)时,utf8会报错Incorrect string value。这是血泪经验——我有个学生在答辩现场演示「宠物领养」功能,输入「黇鹿」直接 500,导师当场皱眉。

  1. 执行建表语句(sql/pet_shop_init.sql前半部分):
-- 表 pet_category:支持三级分类(宠物大类→品种→细分特征) CREATE TABLE pet_category ( id BIGINT PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT '分类名称', parent_id BIGINT DEFAULT 0 COMMENT '父级ID,0为一级', level TINYINT NOT NULL COMMENT '层级:1=大类,2=品种,3=特征', sort_order INT DEFAULT 0 COMMENT '排序权重' ); -- 表 pet_sku:SKU 级别,区分活体/食品/用品 CREATE TABLE pet_sku ( id BIGINT PRIMARY KEY, pet_id BIGINT NOT NULL COMMENT '关联 pet_info.id', sku_code VARCHAR(32) NOT NULL UNIQUE COMMENT 'SKU编码,如 CAT_BR-001', price DECIMAL(10,2) NOT NULL COMMENT '售价', stock INT NOT NULL DEFAULT 0 COMMENT '库存', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1=上架,0=下架' );

关键点:pet_sku.stock字段类型为INT(非BIGINT),因为宠物商城单 SKU 库存不会超 2147483647;sku_code加了UNIQUE约束,防止人工录入重复。

  1. 插入基础数据(sql/pet_shop_init.sql后半部分):
-- 插入一级分类:猫、狗、兔、鸟 INSERT INTO pet_category (id, name, parent_id, level, sort_order) VALUES (1, '猫', 0, 1, 1), (2, '狗', 0, 1, 2), (3, '兔', 0, 1, 3); -- 插入二级分类:猫→英短、布偶;狗→金毛、泰迪 INSERT INTO pet_category (id, name, parent_id, level, sort_order) VALUES (101, '英短', 1, 2, 1), (102, '布偶', 1, 2, 2), (201, '金毛', 2, 2, 1), (202, '泰迪', 2, 2, 2);

注意:parent_id必须严格对应一级分类id,否则前端「品种筛选」下拉将为空。我见过学生把parent_id写成'1'(字符串),MySQL 隐式转换失败,页面 JS 报Cannot read property 'length' of null。

2.3 启动服务:三步验证法(绕过常见启动失败)

不要直接mvn spring-boot:run!按顺序执行:

  1. 检查application-dev.yml的数据库连接(pet-shop-api/src/main/resources/application-dev.yml):
spring: datasource: url: jdbc:mysql://localhost:3306/pet_shop?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_password # ← 这里必须填你本地 MySQL 密码! driver-class-name: com.mysql.cj.jdbc.Driver

关键参数说明:

  • serverTimezone=Asia/Shanghai:解决The server time zone value 'XXX' is unrecognized错误(Windows 系统尤其高频)
  • allowPublicKeyRetrieval=true:MySQL 8.0+ 默认关闭公钥检索,不加此参数会报Public Key Retrieval is not allowed
  • useSSL=false:开发环境禁用 SSL,避免证书配置麻烦(生产环境必须开)
  1. 确认 MyBatis-Plus 分页插件已启用(pet-shop-api/src/main/java/com/petshop/config/MybatisPlusConfig.java):
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 必须注册 PaginationInnerInterceptor,否则 PageHelper 不生效 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }

这是很多同学「分页失效」的元凶——他们删了这行,以为Page对象能自动分页,结果select * from pet_info全表查出 10 万条。

  1. 启动并验证端口与接口:
cd pet-shop-api mvn clean compile mvn spring-boot:run -Dspring.profiles.active=dev

启动成功后,立刻验证:

  • 访问http://localhost:8080/actuator/health→ 返回{"status":"UP"}
  • 访问http://localhost:8080/api/pet/list?page=1&size=10→ 返回 JSON 数组(非空)
  • 访问http://localhost:8080/static/index.html→ 显示首页(注意路径是/static/,非/)

如果卡在Starting Servlet web server,大概率是 MySQL 连接失败;如果返回 404,检查pet-shop-api/src/main/resources/static/目录是否存在index.html(项目用静态资源模式,非 Thymeleaf 模板)。

3. 前端 HTML 的真实工作流:从 index.html 到购物车结算

3.1 首页结构解析:为什么不用 Vue 却能动态渲染?

pet-shop-api/src/main/resources/static/index.html是整个前端的入口。它没用任何框架,靠原生 JS + AJAX 实现动态加载:

<!-- 商品列表容器 --> <div id="product-list" class="row"> <!-- JS 动态插入 <div class="col-md-4">...</div> --> </div> <!-- 分页控件 --> <nav aria-label="Page navigation"> <ul id="pagination" class="pagination"></ul> </nav>

关键 JS 文件在static/js/index.js:

// 1. 页面加载时发起请求 document.addEventListener('DOMContentLoaded', function() { loadProducts(1, 10); // 加载第1页,每页10条 }); // 2. 加载商品列表(调用后端 /api/pet/list 接口) function loadProducts(page, size) { fetch(`/api/pet/list?page=${page}&size=${size}`) .then(response => response.json()) .then(data => { const container = document.getElementById('product-list'); container.innerHTML = ''; // 清空旧内容 data.records.forEach(pet => { const card = ` <div class="col-md-4 mb-4"> <div class="card h-100"> <img src="${pet.coverUrl}" class="card-img-top" alt="${pet.name}"> <div class="card-body"> <h5 class="card-title">${pet.name}</h5> <p class="card-text">${pet.description.substring(0, 50)}...</p> <div class="d-flex justify-content-between align-items-center"> <span class="text-danger">¥${pet.price}</span> <button onclick="addToCart(${pet.id})" class="btn btn-sm btn-primary">加入购物车</button> </div> </div> </div> </div> `; container.insertAdjacentHTML('beforeend', card); }); renderPagination(data); // 渲染分页 }); }

逻辑说明:

  • data.records是后端Page<PetVO>的records字段,MyBatis-Plus 自动注入分页信息
  • pet.coverUrl是数据库pet_info.cover_url字段,值为相对路径(如/images/cat/british-shorthair.jpg),图片需放在static/images/目录
  • onclick="addToCart(${pet.id})"是内联事件,虽不推荐但胜在简单——毕业设计无需过度工程化

3.2 购物车实现:localStorage + 后端同步双保险

购物车数据存在两处:

  • 前端 localStorage:存临时选中项,关页面不丢
  • 后端 session:存登录用户专属购物车(CartService.java中HttpSession.setAttribute("cart", cartList))

static/js/cart.js核心逻辑:

// 添加到购物车(前端) function addToCart(petId) { let cart = JSON.parse(localStorage.getItem('cart') || '[]'); const existing = cart.find(item => item.petId == petId); if (existing) { existing.quantity += 1; // 同一商品多次添加,数量累加 } else { cart.push({ petId: petId, quantity: 1 }); } localStorage.setItem('cart', JSON.stringify(cart)); updateCartBadge(); // 更新顶部购物车图标角标 } // 提交订单(前端触发后端创建) function submitOrder() { const cart = JSON.parse(localStorage.getItem('cart') || '[]'); if (cart.length === 0) { alert('购物车为空!'); return; } // 发送 POST 请求到 /api/order/create fetch('/api/order/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: cart }) // 传入 [{petId:1,quantity:2},...] }) .then(response => response.json()) .then(data => { if (data.code === 200) { alert(`订单创建成功!订单号:${data.data.orderNo}`); localStorage.removeItem('cart'); // 清空本地购物车 updateCartBadge(); window.location.href = '/static/order-success.html?orderNo=' + data.data.orderNo; } else { alert('下单失败:' + data.msg); } }); }

参数说明:

  • items数组中的petId必须是pet_info.id(非pet_sku.id),后端OrderServiceImpl.createOrder()会根据petId查询默认 SKU
  • order-success.html是静态成功页,通过 URL 参数orderNo渲染订单号,避免后端重定向

3.3 订单状态机:用数据库字段驱动前端显示

订单状态流转不是靠前端按钮控制,而是由后端业务逻辑更新order_info.status字段:

status含义前端显示文案
1待付款<span class="badge bg-warning">待付款</span>
2已付款<span class="badge bg-info">已付款</span>
3已发货<span class="badge bg-primary">已发货</span>
4已完成<span class="badge bg-success">已完成</span>
5已取消<span class="badge bg-secondary">已取消</span>

static/js/order-detail.js根据status渲染不同操作按钮:

function renderOrderActions(order) { const actions = document.getElementById('order-actions'); actions.innerHTML = ''; switch(order.status) { case 1: actions.innerHTML = ` <button onclick="payOrder('${order.orderNo}')" class="btn btn-success">立即支付</button> <button onclick="cancelOrder('${order.orderNo}')" class="btn btn-outline-danger">取消订单</button> `; break; case 2: actions.innerHTML = '<span class="text-muted">等待商家发货...</span>'; break; case 3: actions.innerHTML = `<a href="/static/logistics.html?orderNo=${order.orderNo}" class="btn btn-primary">查看物流</a>`; break; } }

关键点:状态变更必须走后端接口(如payOrder()调用/api/order/pay),前端不能直接fetch('/api/order/updateStatus?status=2')—— 这是毕业设计答辩时导师必问「如何防止用户篡改状态?」的考点。

4. 避坑指南:五个让答辩老师当场皱眉的真实翻车现场

4.1 现象:启动时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver

原因:pom.xml中 MySQL 驱动版本与 MySQL 服务器版本不匹配。项目用mysql:mysql-connector-java:8.0.33,但你的本地 MySQL 是 5.7.x。
解决:

  • 方案 A(推荐):升级本地 MySQL 到 8.0+(官网下载.msi安装包,勾选Add to PATH)
  • 方案 B:降级驱动,在pet-shop-parent/pom.xml中修改:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> <!-- MySQL 5.7 兼容版本 --> </dependency>

注意:5.1.49 不支持allowPublicKeyRetrieval=true,需从url中删除该参数。

4.2 现象:首页商品图片全部 404,控制台报GET http://localhost:8080/images/cat/british-shorthair.jpg 404

原因:图片文件未放入正确路径。static/index.html中src="${pet.coverUrl}"的值来自数据库,但static/images/目录下没有对应文件。
解决:

  1. 创建目录:pet-shop-api/src/main/resources/static/images/cat/、/dog/、/rabbit/
  2. 将项目doc/images/下的示例图片(共 12 张)按分类复制进去
  3. 检查数据库pet_info.cover_url字段值是否与实际路径一致(如cat/british-shorthair.jpg)

4.3 现象:点击「加入购物车」无反应,控制台报Uncaught ReferenceError: addToCart is not defined

原因:index.html中<script src="/js/index.js"></script>的路径错误。SpringBoot 静态资源默认从static/目录映射,/js/index.js实际应为/static/js/index.js。
解决:
修改index.html中的 script 标签:

<!-- 错误 --> <script src="/js/index.js"></script> <!-- 正确 --> <script src="/js/index.js"></script> <!-- SpringBoot 默认 static 映射为根路径 -->

验证方法:直接浏览器访问http://localhost:8080/js/index.js,应返回 JS 源码。

4.4 现象:下单后订单状态一直是「待付款」,物流页打不开

原因:OrderServiceImpl.createOrder()中调用了logisticsService.generateTrackingNo(),但logisticsServiceBean 未注入(@Service注解缺失或包扫描路径错误)。
解决:

  1. 检查pet-shop-service/src/main/java/com/petshop/service/impl/LogisticsServiceImpl.java是否有@Service注解
  2. 检查pet-shop-api/src/main/java/com/petshop/PetShopApplication.java的@SpringBootApplication是否扫描到com.petshop.service包:
@SpringBootApplication(scanBasePackages = "com.petshop") public class PetShopApplication { ... }

如果LogisticsServiceImpl在com.petshop.logistics包下,需显式添加scanBasePackages。

4.5 现象:搜索「金毛」返回空,但数据库pet_info.name确实有「金毛寻回犬」

原因:PetMapper.xml中的模糊查询 SQL 写成了name LIKE '%${name}%'(拼接 SQL,非预编译),且name参数未加%符号。
解决:
修改PetMapper.xml中的<select>语句:

<!-- 错误写法 --> WHERE name LIKE '%${name}%' <!-- 正确写法(使用 CONCAT 防 SQL 注入) --> WHERE name LIKE CONCAT('%', #{name}, '%')

同时确保 Controller 中@RequestParam String name传入的是纯关键词(如金毛),而非金毛%。

5. 毕业设计加分技巧:三处可深挖的「论文亮点」与答辩话术

5.1 数据库设计亮点:用「品种树形结构」替代扁平分类表

很多同学的「图书分类」就一张表category(name, parent_id),但宠物商城的pet_category表增加了level字段(1=大类,2=品种,3=特征),并在PetCategoryService.listByLevel()中实现了「按层级查所有品种」的递归查询:

// 查询所有二级品种(猫/狗下的具体品种) public List<PetCategory> listVarieties() { QueryWrapper<PetCategory> wrapper = new QueryWrapper<>(); wrapper.eq("level", 2).orderByAsc("sort_order"); return this.list(wrapper); }

答辩话术:

“老师,我在数据库设计时没有简单用category_id关联,而是构建了三层分类树。这样做的好处是:前端可以独立加载‘猫’下的所有品种(英短、布偶),再根据选中的品种加载其细分特征(如英短的‘蓝眼’‘银渐层’),避免一次查出 200 个分类造成前端卡顿。论文第 3.2 节的 ER 图中,我特别标注了level字段的业务含义。”

5.2 业务逻辑亮点:订单创建时的「库存预占」与「幂等性」处理

OrderServiceImpl.createOrder()方法中,关键逻辑不是简单update pet_sku set stock = stock - ?,而是:

  1. 用SELECT FOR UPDATE锁定 SKU 行(防止超卖)
  2. 检查stock >= requiredQuantity
  3. 执行UPDATE并记录日志(含订单号、SKU、扣减量)
  4. 使用 Redis 缓存订单号,拦截重复提交(if (redisTemplate.hasKey("order:" + orderNo)) throw new BizException("重复提交"))

答辩话术:

“老师,我在订单创建环节做了两个防护:一是数据库行锁,保证高并发下库存不超卖;二是 Redis 幂等校验,防止用户手抖连点两次‘提交订单’。虽然毕业设计不强调高并发,但这个设计体现了我对电商核心链路的理解。代码在pet-shop-service/src/main/java/com/petshop/service/impl/OrderServiceImpl.java第 89 行开始。”

5.3 前端交互亮点:用原生 JS 实现「无刷新地址栏跳转」

static/js/category.js中,点击「猫→英短」时,URL 从/static/index.html变为/static/index.html?category=101,但页面不刷新:

// 监听 URL 参数变化 window.addEventListener('popstate', function(e) { const params = new URLSearchParams(window.location.search); const categoryId = params.get('category'); if (categoryId) { loadProductsByCategory(categoryId, 1, 10); } }); // 点击品种时更新 URL(不刷新) function navigateToCategory(id, name) { const url = `?category=${id}&name=${encodeURIComponent(name)}`; history.pushState({ category: id }, '', url); loadProductsByCategory(id, 1, 10); }

答辩话术:

“老师,我没有用 Vue Router,而是用原生history.pushState实现了 SPA 效果。这样既满足了‘前后端分离’的现代 Web 要求,又避免了引入框架带来的学习成本。您看,点击品种后地址栏变了,但页面是局部刷新的——这在论文第 4.5 节的‘前端技术选型’中有详细对比。”

从那以后我每次帮学生改毕设,都会强制他们在README.md里写清三点:1)数据库初始化的三步命令;2)application-dev.yml必改的三个参数;3)首页图片路径的映射关系。不是怕他们不会,是怕答辩前两小时,他们对着 404 图片抓狂。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询