☰
SpringBoot+MySQL+HTML宠物商城毕业设计实战指南
2026/9/26 17:59:40 网站建设 项目流程

简介:一份基于Spring Boot、MySQL和HTML实现的宠物商城管理系统,属于高分毕业设计项目,适合Java方向学生用于毕业设计、课程设计或期末大作业等场景。系统功能完善,涵盖宠物商品展示、订单管理、用户管理及库存管理等核心模块,界面简洁美观,操作流程清晰,完整体现了Spring Boot快速开发、MySQL数据存储与HTML页面渲染的技术栈整合。压缩包内共2个文件,分别为ZIP项目源码包和SQL数据库脚本,总大小约14.06MB;源码包集成前端页面、后端业务逻辑及部署配置,SQL脚本则用于创建数据表并初始化示例数据,可直接还原运行环境。该项目当前已有59人学习或下载,经严格调试确保开箱即用,拿到后即可部署演示。对于需要快速获得一套完整商城系统作为参考的开发者而言,它既可直接用于毕设答辩,又能作为功能拓展和技术学习的扎实蓝本。

1. 宠物商城管理系统:SpringBoot+MySQL+HTML这套组合到底能做出什么

SpringBoot+MySQL+HTML的宠物商城管理系统,是这两年毕业设计里出现频率极高的一类单体Web项目:后端一套SpringBoot接口提供业务能力,MySQL负责存宠物、用户、订单这些核心数据,前端用HTML页面配合Thymeleaf模板引擎做展示和交互。把它做成“高分毕业设计”的关键,不是功能堆得多,而是能不能把一条主链路讲清楚、跑通、扛得住追问。

很多同学拿到题目后的第一反应是去搜现成源码,结果下回来的项目要么基于老SSH框架,依赖缺一堆;要么拆成微服务,本地根本起不来。这个题目的价值恰恰在于它足够收敛:单体架构、三层结构、单一数据库,从建表到页面渲染,每一步都能在一个月内亲手做完。你不需要在生产级电商的并发、缓存、分布式事务上较劲,但要把用户注册登录、宠物展示、购物车、下单、订单管理这条闭环做到能演示、能解释、能扩展。

这篇文章按一条可复现的路径来讲:先讲为什么选这套技术栈、数据库怎么设计,再落到后端接口和前端页面怎么写,然后是五个高频踩坑,最后是一套答辩前可执行的验证方法。适合两类人:一类是正在做毕设、需要一个可靠骨架的学生,另一类是想快速搭一个小型商城原型验证业务的开发者。

2. SpringBoot+MySQL的选型逻辑与数据库初始化:先搞清楚为什么是这三样

2.1 为什么SpringBoot+MySQL+HTML是毕业设计的安全牌

先回答一个很多人在开题时就会被问住的问题:为什么不用SSM?为什么不用Vue前后端分离?这个系统的定位是“毕业设计级商城”,不是“生产级电商”。SpringBoot相比SSM最大的优势是省掉了大量XML配置,内置Tomcat,一个java -jar就能跑起来。老师看你的部署过程时,不需要看你调半天web.xml,而是看你能否讲清楚自动配置和starter机制,这是SpringBoot面试题里最常考的点,放在答辩里也是加分项。

MySQL的选择理由更直白:它是目前简历和课程里最通用的关系型数据库,可视化工具多,出问题能搜到的解决方案也最多。这里建议直接用MySQL 8.x而不是5.7,因为新版默认认证插件是caching_sha2_password,虽然配置上多个步骤,但更能体现你跟进过环境变化。数据库连接池用HikariCP,SpringBoot 2.x默认内置,不用额外引入。

至于HTML为什么不用前后端分离,核心原因是单体演示成本低。Vue工程要启动node服务、要处理跨域、要把打包产物和SpringBoot合并,这一套流程对毕设来说时间成本偏高。而用Thymeleaf服务端渲染,一个Controller方法返回一个页面名字,数据通过Model传进HTML模板,整个过程都在同一个应用里,演示时只需要确保SpringBoot这一个进程活着。这套组合的边界也要心里有数:并发能力有限、页面状态靠session或参数传递,但这些在论文的“不足与展望”里如实写,反而比硬吹更有说服力。

维度SSM + JSPSpringBoot + ThymeleafSpringBoot + Vue前后端分离
配置成本高,多个XML低,自动配置中等,需要处理跨域和打包
演示过程需要容器部署单进程直接跑前端后端两个进程
答辩提问点框架整合细节自动配置与starter鉴权与跨域方案
适合作业周期偏紧最合适时间充裕可选

2.2 数据库设计与建表SQL:用户、宠物、订单、购物车七张表

数据库设计是答辩时老师盯得最紧的部分。宠物商城不需要花哨的表,但表之间的关系和字段约束要经得起追问。我一般会拆成七张表:category(品类)、pet(宠物)、user(用户)、cart(购物车)、orders(订单主表)、order_item(订单明细)、favorite(收藏)。收藏表不是核心链路,但加上它能让论文里的功能列表多一个亮点。

-- 品类表 CREATE TABLE `category` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '品类ID', `name` varchar(50) NOT NULL COMMENT '品类名称', `sort` int DEFAULT 0 COMMENT '排序值,越小越靠前', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物品类'; -- 宠物表 CREATE TABLE `pet` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '宠物ID', `name` varchar(100) NOT NULL COMMENT '宠物名称', `category_id` int NOT NULL COMMENT '所属品类ID', `price` decimal(10,2) NOT NULL COMMENT '售价', `stock` int NOT NULL DEFAULT 0 COMMENT '库存', `image` varchar(255) DEFAULT NULL COMMENT '图片地址', `description` text COMMENT '描述', `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:1上架 0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物信息'; -- 用户表 CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码,密文存储', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `address` varchar(255) DEFAULT NULL COMMENT '收货地址', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 购物车表 CREATE TABLE `cart` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL COMMENT '用户ID', `pet_id` int NOT NULL COMMENT '宠物ID', `quantity` int NOT NULL DEFAULT 1 COMMENT '数量', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车'; -- 订单主表 CREATE TABLE `orders` ( `id` int NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` int NOT NULL COMMENT '用户ID', `total_price` decimal(10,2) NOT NULL COMMENT '总金额', `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0待支付 1已支付 2已发货 3已完成', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表'; -- 订单明细表 CREATE TABLE `order_item` ( `id` int NOT NULL AUTO_INCREMENT, `order_id` int NOT NULL COMMENT '订单ID', `pet_id` int NOT NULL COMMENT '宠物ID', `pet_name` varchar(100) NOT NULL COMMENT '下单时的宠物名称快照', `price` decimal(10,2) NOT NULL COMMENT '下单时的单价快照', `quantity` int NOT NULL COMMENT '数量', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细';

这里有两个设计点要说明。第一,order_item里冗余了pet_name和price两个快照字段,这是故意为之——如果宠物改价或改名,历史订单里的金额和名称不能跟着变,这在订单类系统里是基础要求,答辩时可以主动讲出来。第二,所有表都用utf8mb4而不是utf8,因为utf8在MySQL里最多存3字节,遇到emoji或生僻字会变成问号。

然后是application.yml。这个文件是SpringBoot项目的命门,我见过太多项目栽在数据库连接参数上。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pet_shop? useUnicode=true&characterEncoding=utf8 &useSSL=false&allowPublicKeyRetrieval=true &serverTimezone=Asia/Shanghai username: root password: 你的密码 thymeleaf: cache: false mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

几个参数值得单独说。useSSL=false是因为本地开发不需要SSL加密连接,不关掉会看到大量SSL警告日志。allowPublicKeyRetrieval=true是配合MySQL 8的caching_sha2_password用的,不加这个启动时会直接报Public Key Retrieval is not allowed。serverTimezone=Asia/Shanghai则是解决数据库时间比本地晚8小时的问题。这些参数如果一步到位配好,后面的时间能省出一大截。

3. 用SpringBoot把商城后端跑起来:核心接口与MyBatis-Plus落地

3.1 用MyBatis-Plus写宠物表增删改查:实体类、Mapper与分页插件

后端持久层我首选MyBatis-Plus而不是原生MyBatis,因为单表CRUD和分页这两件事,MyBatis-Plus可以直接省掉大量重复SQL编写。这是血泪经验换来的选择:拿到的旧项目里,光一个PetMapper的XML就写了十几个标签,而MyBatis-Plus里这些操作基本都是现成的。

实体类直接映射宠物表,注意字段名和表字段之间的对应关系:

@Data @TableName("pet") public class Pet { @TableId(type = IdType.AUTO) private Integer id; private String name; @TableField("category_id") private Integer categoryId; private BigDecimal price; private Integer stock; private String image; private String description; @TableField("status") private Integer status; private LocalDateTime createTime; }

说明几点:@TableName指定表名,当实体类名和表名不一致时必须写。@TableId标注主键,type = IdType.AUTO表示数据库自增。@TableField("category_id")用来处理Java驼峰字段和数据库下划线字段的映射——虽然MyBatis-Plus默认开了驼峰映射,但显式标注能让代码更清晰,也避免字段改名时出问题。

Mapper层就更简单了,继承BaseMapper就拥有了单表的所有基础方法:

@Mapper public interface PetMapper extends BaseMapper<Pet> { }

这里不要觉得太简单就没价值。面答时候老师问“你怎么实现增删改查的”,你可以回答“单表操作由MyBatis-Plus的BaseMapper提供,复杂查询用LambdaQueryWrapper构造条件,必要时写XML”。这个回答比“全靠复制粘贴”有力得多。

分页是商城类系统的标配能力,但MyBatis-Plus的分页有个前置条件:必须注册分页拦截器,否则Page对象返回的records为空,total永远是0。这个坑几乎人人会踩,细节放在第5章,这里先把配置写好:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

PaginationInnerInterceptor是MyBatis-Plus分页插件,DbType.MYSQL告诉它按MySQL的方言拼接LIMIT语句。不引入这个Bean,selectPage方法不会生效。

3.2 Controller层与Service层怎么写:统一返回体和三个核心接口

后端接口设计决定了前端对接时改不改代码。我习惯从第一行代码就引入统一返回体,而不是每个接口各自返回Map或裸对象。统一返回体的好处是前端只需要判断一个code字段,不用针对每个接口做特殊处理。

@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> fail(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

然后在Controller里按三层结构往下走。这里的核心链路是宠物分页查询,对应商城首页的商品列表:

@RestController @RequestMapping("/api/pet") public class PetController { @Autowired private PetService petService; @GetMapping("/page") public Result<IPage<Pet>> page( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword) { return Result.ok(petService.pageQuery(pageNum, pageSize, keyword)); } }

Controller只做参数接收和结果包装,真正的业务逻辑在Service里。pageNum和pageSize都有默认值,keyword可选,这种设计让接口在没有参数时也能直接访问,方便调试。

Service实现类里的核心是条件构造器:

public IPage<Pet> pageQuery(Integer pageNum, Integer pageSize, String keyword) { Page<Pet> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Pet> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), Pet::getName, keyword) .eq(Pet::getStatus, 1) .orderByDesc(Pet::getCreateTime); return petMapper.selectPage(page, wrapper); }

LambdaQueryWrapper的两个关键点:like方法的第一个参数是boolean,当keyword为空时不拼接这个条件,这是避免动态SQL出错的标准写法;eq(Pet::getStatus, 1)是只查上架商品,下架的商品不能出现在前台列表里。

除了分页,登录和购物车是两个避不开的接口。登录我建议用POST + JSON体提交,密码用BCrypt加密存储,不要用MD5——老师现在对MD5的批评已经成条件反射了。购物车加购的逻辑要注意幂等性:同一个用户往购物车加同一个宠物,应该累加数量而不是新增一行记录。

public void addCartItem(Integer userId, Integer petId, Integer quantity) { Cart cart = cartMapper.selectOne(new LambdaQueryWrapper<Cart>() .eq(Cart::getUserId, userId) .eq(Cart::getPetId, petId)); if (cart != null) { cart.setQuantity(cart.getQuantity() + quantity); cartMapper.updateById(cart); } else { Cart newCart = new Cart(); newCart.setUserId(userId); newCart.setPetId(petId); newCart.setQuantity(quantity); cartMapper.insert(newCart); } }

selectOne这个方法的潛在风险是:如果查出来多行记录它会直接报错。所以这里依赖数据库里(user_id, pet_id)的联合唯一索引,没有唯一索引就不要用selectOne查业务数据。这里的索引在2.2节的建表语句里没有体现,你要在cart表上补一个:

ALTER TABLE `cart` ADD UNIQUE KEY `uk_user_pet` (`user_id`, `pet_id`);

还有一个答辩加分项值得提:如果项目里涉及文件上传(比如宠物图片),可以加一个全局过滤器统一处理请求参数里的XSS风险。这不是核心链路,但能体现安全意识。实现方式是实现OncePerRequestFilter,包装HttpServletRequest的getParameter方法做转义,然后通过FilterRegistrationBean注册过滤路径。这块放到论文“系统安全性”一节里写,答辩时一笔带过即可,别展开太深,否则容易被追问细节。

4. 用HTML+Thymeleaf做商城前端:页面渲染与接口对接

4.1 宠物列表页的HTML模板:从静态页面到Thymeleaf动态渲染

前端部分最容易让人迷惑的是目录结构。SpringBoot里静态资源放在src/main/resources/static下,比如css、js、图片,访问时不需要加static前缀。Thymeleaf模板放在src/main/resources/templates下,Controller返回的字符串就是templates目录下的文件名。这个约定搞清楚之后,页面渲染就顺了。

先写一个完整的HTML页面骨架,标准HTML5结构,注意lang="zh-cn"和charset="utf-8"是必须的,否则页面可能出现中文乱码:

<!DOCTYPE html> <html lang="zh-cn" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>宠物商城 - 首页</title> <link th:href="@{/css/style.css}" rel="stylesheet"> </head> <body> <header class="header"> <h1>宠物商城</h1> <nav> <a th:href="@{/index}">首页</a> <a th:href="@{/cart}">购物车</a> <a th:href="@{/order}">我的订单</a> </nav> </header> <main class="container"> <div class="pet-grid" th:each="pet : ${page.records}"> <div class="pet-card"> <img th:src="${pet.image}" th:alt="${pet.name}" class="pet-image"> <h3 th:text="${pet.name}">宠物名称</h3> <p class="price" th:text="'¥' + ${pet.price}">¥0.00</p> <button th:onclick="'addToCart(' + ${pet.id} + ')'">加入购物车</button> <a th:href="@{/detail/{id}(id=${pet.id})}" class="btn">查看详情</a> </div> </div> </main> </body> </html>

这段模板里有一堆Thymeleaf语法,逐一说明。xmlns:th是命名空间声明,让编辑器能识别th:开头的属性。th:href="@{/css/style.css}"是URL表达式,最终会生成带上下文路径的完整地址,不会因为部署路径变化而404。th:each="pet : ${page.records}"是循环渲染,等价于Java里的for (Pet pet : page.getRecords())。th:text="${pet.name}"是输出变量值,注意th:text会转义HTML特殊字符,比直接拼接字符串安全。

页面数据从哪来?Controller里返回视图名并塞入Model:

@GetMapping("/index") public String index( Model model, @RequestParam(defaultValue = "1") Integer pageNum) { IPage<Pet> page = petService.pageQuery(pageNum, 8, null); model.addAttribute("page", page); return "index"; }

这里返回的字符串"index"对应templates/index.html。model.addAttribute("page", page)相当于request.setAttribute,Thymeleaf里用${page.records}取自page对象里的records字段。每页8条数据,这是商城首页常见的栅格数量。

需要注意的一点:这个Controller返回的是页面而不是JSON,所以类上不能用@RestController,而是用@Controller。如果两个注解混用,前端会收到一个JSON字符串而不是HTML页面。这是SpringBoot初学者最容易犯的错。

4.2 登录注册与购物车:用Ajax把HTML页面和后端接口打通

列表页是服务端渲染,但登录、加购这类操作需要局部刷新,常规做法是jQuery + Ajax。先写登录页面里的核心脚本:

function login() { var username = $('#username').val(); var password = $('#password').val(); if (!username || !password) { alert('用户名和密码不能为空'); return; } $.ajax({ url: '/api/user/login', type: 'POST', contentType: 'application/json', data: JSON.stringify({ username: username, password: password }), success: function (res) { if (res.code === 200) { sessionStorage.setItem('userId', res.data.id); sessionStorage.setItem('username', res.data.username); location.href = '/index'; } else { alert(res.message); } }, error: function () { alert('网络异常,请稍后重试'); } }); }

登录成功后把userId和username放进sessionStorage,后续加购、下单接口就从sessionStorage取用户信息。这里不用localStorage的原因很简单:关闭浏览器标签页后sessionStorage自动清空,更符合登录态的预期。

加购按钮的事件绑定和Ajax请求要注意传参格式。后端接口接收JSON体,前端就必须设置contentType为application/json,然后用JSON.stringify把对象序列化成字符串。很多对不上接口的情况都是因为前端用了表单格式提交。

购物车页面的渲染要比列表页稍复杂一些,因为要展示“哪只宠物、单价、数量、小计”,最后算出总价。这里我给一个可以抄的模板片段:

<tr th:each="item : ${cartItems}"> <td th:text="${item.petName}">宠物名</td> <td th:text="'¥' + ${item.price}">¥0.00</td> <td> <input type="number" th:value="${item.quantity}" min="1" th:onchange="'updateQuantity(' + ${item.cartId} + ', this.value)'"> </td> <td th:text="'¥' + ${item.price * item.quantity}">¥0.00</td> <td> <button th:onclick="'removeFromCart(' + ${item.cartId} + ')'">删除</button> </td> </tr>

这里th:text="${item.price * item.quantity}"直接在模板里做运算,是Thymeleaf支持的标准表达式能力。注意item里封装了cart表的id和pet表的name、price的联表查询结果,所以前端拿到的不是一个单纯的Cart对象,而是一个CartVO。这就是代码里常见的VO模式——把页面上需要展示的多个表字段拼成一个视图对象,避免前端写多表关联逻辑。

订单创建是前端交互链路里最敏感的一步,它涉及库存扣减和事务。后端Service方法要加@Transactional注解:

@Transactional(rollbackFor = Exception.class) public void createOrder(Integer userId) { List<Cart> cartList = cartMapper.selectList( new LambdaQueryWrapper<Cart>().eq(Cart::getUserId, userId)); if (cartList.isEmpty()) { throw new RuntimeException("购物车为空"); } BigDecimal total = BigDecimal.ZERO; for (Cart cart : cartList) { Pet pet = petMapper.selectById(cart.getPetId()); if (pet.getStock() < cart.getQuantity()) { throw new RuntimeException("库存不足:" + pet.getName()); } } // 扣库存、生成订单主表和明细、清空购物车 // 这些操作都在同一个事务里,任何一步失败都会回滚 }

@Transactional(rollbackFor = Exception.class) 是必写的,默认情况下Spring事务只在RuntimeException时回滚,加上rollbackFor = Exception.class后,checked exception也能触发回滚。库存扣减要先检查后扣减,虽然这不是严格的并发安全方案,但毕业设计到这一步已经足够。如果老师追问并发场景,可以回答“生产环境会加悲观锁或乐观锁”,这个台阶得自己留好。

5. 宠物商城系统5个高频踩坑:从启动翻车到页面404的排查记录

5.1 启动报8080端口被占用

现象:SpringBoot启动到一半直接报错,日志里出现Port 8080 was already in use。这是本地开发最常见的第一道坎。

原因:8080被其他进程占用。常见元凶是之前启动的Java进程没有完全退出,或者Windows下Hyper-V动态保留端口把8080占了。

解决:先找出占用端口的进程,然后按需处理。Windows下执行:

netstat -ano | findstr :8080 taskkill /PID 进程号 /F

如果端口被系统保留,可以换一个端口。不想改配置就在启动命令后面加参数:

java -jar pet-shop.jar --server.port=8081

我一般直接改application.yml里的server.port为8080,本地固定端口方便前端联调。这个坎跨过去之后,后面就顺畅了。

5.2 MySQL连接报Public Key Retrieval is not allowed

现象:SpringBoot启动时数据源初始化失败,控制台报Public Key Retrieval is not allowed,或者报Unable to load authentication plugin 'caching_sha2_password'。

原因:MySQL 8默认的认证插件是caching_sha2_password,JDBC驱动在首次连接时需要向服务器获取公钥来加密密码,而连接串里没有放行这个行为。

解决:在jdbc url后面加两个参数,这个在前面2.2节已经给了完整配置:

useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai

这三个参数是MySQL 8开发环境的三件套,少了哪个都会出幺蛾子。useSSL=false关掉SSL握手,allowPublicKeyRetrieval=true允许客户端获取公钥,serverTimezone把时区校正到东八区。如果你用的是MySQL 5.7,allowPublicKeyRetrieval不加也不影响,但MySQL 8建议直接配齐。这个坑也对应很多人说的“mysql连接错误”和“mysql ssl连接错误”,本质上都是同一批参数没配全。

5.3 页面HTML出来了但css和图片全是404

现象:浏览器访问localhost:8080/index,HTML骨架和文字内容都出来了,但页面完全没有样式,控制台一堆Failed to load resource: the server responded with a status of 404,报错路径指向/css/style.css。

原因:静态资源放错位置,或者路径写法用了相对路径。很多人把style.css放进了templates目录,然后页面里写href="/css/style.css"。实际上SpringBoot对templates目录下的文件只做模板解析,不做静态资源映射;而static目录才是静态资源的根目录。

解决:确保目录结构正确,resources/static/css/style.css,页面里用th:href而不是href:

<link th:href="@{/css/style.css}" rel="stylesheet">

th:href="@{/css/style.css}"会生成带应用上下文根的绝对路径,不管你部署在根路径还是子路径下都不会404。这是个细节,但能让你的页面在所有机器上表现一致。顺带说一句,{/detail/{id}(id=${pet.id})}这种URL模板写法也是@{}语法的一部分,构造带路径参数的链接时比字符串拼接清爽得多。

5.4 数据库和页面都显示中文问号

现象:后台管理添加宠物时填了“布偶猫”,保存后在页面和Navicat里看到的是“???”。

原因:编码链路断了一环。MySQL连接串里没带characterEncoding=utf8,建表语句里没有DEFAULT CHARSET=utf8mb4,HTML页面meta标签漏了charset=utf-8。这三处只要有一处不匹配,中文就会变成问号或乱码。

解决:从上到下检查三层。第一,建表时统一指定utf8mb4,可以查看现有表:

SHOW TABLE STATUS LIKE 'pet';

第二,jdbc url带上characterEncoding=utf8。第三,页面head里保留<meta charset="utf-8">。如果数据已经变成问号,需要把表的数据导出再用正确编码重新导入,这个说起来费劲,最省事的办法是把脏数据删掉重新录一遍。数据库乱码这个坑,属于典型的“配一次落终身”的问题,建库时多花一分钟配置,后面省一天。

5.5 分页查不到数据但SQL单独执行能查到

现象:宠物列表页第一页空荡荡,但直接在Navicat里执行同样的SQL能查到数据。更诡异的是,有时候total是对的,但records是空的。

原因:MyBatis-Plus的分页拦截器没有注册。没有PaginationInnerInterceptor这个Bean时,selectPage方法不会真正执行分页SQL,Page对象里的records可能为空,total也可能为0,但控制台日志里看不到LIMIT语句。

解决:检查MybatisPlusConfig是否加了@Configuration注解、分页拦截器是否注册到MybatisPlusInterceptor里。对照3.1节的代码检查一遍,重点看两行:

@Configuration public class MybatisPlusConfig {

这个Class有没有被Spring扫描到,以及PaginationInnerInterceptor有没有真正add进去。这个坑特别适合写进论文的“问题与解决”章节,因为它是典型的“代码看起来没问题、运行起来全是问题”的黑匣子故障,排查过程能体现你对框架机制的理解。

6. 答辩前怎么验证整套系统:接口自测脚本与演示方法论

6.1 用curl把核心链路过一遍:注册、登录、加购、下单

答辩现场最怕的是页面演示到一半接口报错。我的习惯是在答辩前用一组curl命令把核心链路完整跑一遍,确认后端接口都是通的。这比手动点页面更快、更可重复,而且出问题能直接看到返回的message。下面是一个可以直接保存成.sh脚本的验证流程:

#!/bin/bash BASE=http://localhost:8080 # 1. 注册新用户 curl -X POST $BASE/api/user/register \ -H "Content-Type: application/json" \ -d '{"username":"demo2","password":"123456","phone":"13800138000"}' echo # 2. 登录,返回的JSON里包含userId curl -X POST $BASE/api/user/login \ -H "Content-Type: application/json" \ -d '{"username":"demo2","password":"123456"}' echo # 3. 分页查询宠物列表,确认商品能查到 curl "$BASE/api/pet/page?pageNum=1&pageSize=10" echo # 4. 加购一只宠物,假设宠物ID为1、数量为1 curl -X POST $BASE/api/cart/add \ -H "Content-Type: application/json" \ -d '{"userId":1,"petId":1,"quantity":1}' echo # 5. 创建订单,验证库存扣减和订单生成 curl -X POST $BASE/api/order/create \ -H "Content-Type: application/json" \ -d '{"userId":1}' echo

脚本里的userId和petId要按你数据库里的真实值改,别照抄。每次返回的JSON里看code字段,如果code不是200,直接看message就能定位问题。这个脚本的额外好处是:老师如果问“你怎么保证接口没问题”,你可以说“我写了一套接口自测脚本,部署后先跑脚本再演示”,这比口头保证有力得多。

6.2 答辩演示的节奏:从注册到订单的完整闭环

功能演示不要从后台管理开始讲。我见过太多人上去先展示添加宠物,讲半天表单项,等讲到核心的下单流程时时间已经不够了。正确的顺序是:用户注册 → 用户登录 → 首页宠物列表 → 查看宠物详情 → 加入购物车 → 查看购物车 → 下单 → 查看订单列表。这条链路是商城的核心业务闭环,走完它,老师就知道系统是真正能用的,而不仅仅是页面堆砌。

演示过程中被问到最多的问题集中在三个地方:密码为什么用BCrypt、库存扣减怎么保证不超卖、订单和购物车之间的事务怎么控制。对应的回答口径分别是:BCrypt是自适应哈希算法,自带随机盐,能抵抗彩虹表攻击;库存超卖在单体架构下可以用乐观锁解决,也就是更新时加上stock > 0的条件;事务用@Transactional控制,任何一步异常都会回滚,保证订单和库存数据一致。这三个问答能讲清楚,“高分”基本就有底了。

还有一个实用技巧:提前准备好一个“演示数据”脚本,把品类、宠物图片、用户、购物车数据都预置好,确保演示时页面不是空的。别等到打开首页才发现一张宠物图片都没有,那比任何技术问题都尴尬。图片可以放到static/images下,用本地图片路径而不是外链,这样离线也能演示。

最后说一个我自己的教训。当年我做类似项目时,分页拦截器没有注册,页面一直查不出数据,我以为是SQL写错了,排查了一个通宵,最后发现是配置类没生效。从那以后,凡是用到框架提供的扩展能力时,我都会先确认“对应Bean有没有被Spring管理”。这个习惯帮我避免了很多玄学问题。希望这篇笔记能让你在这个项目上少走几步弯路,一次跑通,答辩顺利。

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

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

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

立即咨询