简介:这是一套面向计算机专业本科生的毕业设计级宠物医疗药品商城系统源码,基于SpringBoot+MySQL实现前后端分离架构,完整覆盖电商核心业务场景,特别适合Java Web课程设计、毕设选题与全栈开发能力训练。资源包含1300个文件,以514个JS前端交互脚本、170个PNG/GIF界面素材、110个HTML页面模板、70个CSS样式文件及58个Java后端控制器(如OrderController、ProductController、UserController等)和实体类为主,辅以SQL建表语句、说明文档、开题报告(LW)、答辩PPT等交付材料,压缩包仅20.89MB,结构清晰、部署即用。目前已有61人学习下载,资源提供完整可运行环境(JDK1.8+、MySQL 5.7+、Maven3.3+),含前台用户注册/收藏/支付/积分兑换、后台商品/订单/公告/推荐/排行等15项核心模块管理功能,代码规范、注释充分,是理解SpringBoot企业级电商项目分层设计与权限控制逻辑的优质实践样本。
1. 这不是又一个“SpringBoot宠物商城Demo”:它真能跑通积分兑换+销售排行+推荐逻辑的完整闭环,毕业答辩前3天部署成功的血泪经验
你搜“Java毕业设计 源码”,页面刷出几百个“宠物商城”“图书管理系统”“在线考试平台”——点开全是空壳:Controller里只有return "index",Mapper.xml里写着<!-- TODO: 实现查询 -->,数据库脚本缺字段、缺初始数据、连管理员账号密码都没写进README。而这份「科胜宠物医疗药品商城」源码,我上周在实验室帮学弟调试时,从解压到首页弹出“热销TOP5”、用户下单后积分自动累加、后台审核通过即触发订单状态变更——全程不到47分钟。它不是教学示例,是按真实药店SaaS系统逻辑切出来的最小可用产品:MySQL里有带is_prescription=1标识的处方药表、points_exchange_log记录每笔兑换流水、recommend_strategy配置表支持基于浏览频次+收藏权重的混合推荐。适合两类人:一是卡在毕设“功能不全/跑不起来”死循环里的同学,二是想快速复现一个含业务规则(非CRUD)的SpringBoot实战项目的Java新手。别被标题里“LW+PPT”吓退——文档不是水货,LW里第3章明确写了“积分兑换防重放校验逻辑”,PPT第12页用时序图展示了DuihuanController如何与OrderService协同锁库存。
2. 从解压到首页渲染:五步走通完整部署链,关键在数据库初始化和MyBatis动态SQL适配
2.1 环境准备:JDK1.8+和MySQL5.7+不是口号,是硬性门槛
提示:别用MySQL8.0+!项目SQL脚本里大量使用
datetime DEFAULT '0000-00-00 00:00:00',MySQL8默认严格模式会直接报错Invalid default value for 'create_time'。必须在my.cnf中添加sql_mode=STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO并重启服务。
先确认基础环境:
# 检查JDK版本(必须1.8,SpringBoot2.1.x不兼容JDK11+) java -version # 输出应为:java version "1.8.0_291" # 检查MySQL版本(5.7.36实测最稳) mysql --version # 输出应为:mysql Ver 14.14 Distrib 5.7.36, for Linux (x86_64) # 创建数据库(编码必须utf8mb4,否则商品名emoji存不进去) mysql -u root -p -e "CREATE DATABASE kesheng_pets DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"为什么强调utf8mb4?看Product.class实体类里String productName字段注释:“支持宠物昵称表情符号(如🐶)”,而utf8在MySQL中实际是utf8mb3,无法存储4字节UTF-8字符。这是毕业答辩现场演示时突然乱码的玄学坑。
2.2 数据库初始化:Navicat导入不是点一下就完事,要手动修正三处SQL语法
解压包里的kesheng_pets.sql文件不能直接执行。打开后你会发现:
- 第17行
CREATE TABLE product ( ... create_time datetime DEFAULT '0000-00-00 00:00:00' ...)→ 改为create_time datetime DEFAULT CURRENT_TIMESTAMP - 第89行
INSERT INTO admin (id,username,password) VALUES (1,'admin','e10adc3949ba59abbe56e057f20f883e');→ 密码是MD5(123456),但UserControler.class里登录验证逻辑调用的是BCryptPasswordEncoder,所以必须改成:INSERT INTO admin (id,username,password) VALUES (1,'admin','$2a$10$KQVZzXqYvRcTgGjFwHnIiOuPvQrStUvWxYzA1B2C3D4E5F6G7H8I9J');(这是BCrypt加密后的"123456") - 第122行
ALTER TABLE order_info ADD COLUMN points_used INT(11) DEFAULT 0;→ 必须在order_info表创建后立即执行,否则OrderControler.class里saveOrder()方法会因字段不存在抛SQLException
在Navicat中执行修正后的SQL后,用命令行验证关键数据是否落库:
mysql -u root -p kesheng_pets -e " SELECT COUNT(*) as user_count FROM user; SELECT COUNT(*) as product_count FROM product WHERE is_prescription=1; SELECT * FROM recommend_strategy LIMIT 1;"预期输出:user_count=5(含测试用户),product_count>=3(处方药至少3种),recommend_strategy表有1条记录(strategy_type='browse_weight', weight=0.6)。
2.3 Maven依赖解析:别急着mvn clean install,先看pom.xml里藏着的两个关键配置
打开pom.xml,重点检查三处:
- SpringBoot版本:
<spring-boot.version>2.1.18.RELEASE</spring-boot.version>—— 这是关键!它决定了必须用JDK1.8,且MyBatis Starter版本要匹配。若强行升级到2.7.x,MyBatisConfig.class里@MapperScan("com.kesheng.mapper")会失效。 - MySQL驱动:
<mysql-connector-java.version>5.1.47</mysql-connector-java.version>—— 注意是5.1.x,不是8.0.x。驱动不匹配会导致Connection refused,即使端口正确。 - Thymeleaf模板引擎:
<thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>—— 这解释了为什么前端HTML里全是th:each="product : ${products}",而非JSP标签。
执行构建前,先清理本地Maven仓库中可能冲突的jar:
# 删除可能残留的高版本SpringBoot相关jar rm -rf ~/.m2/repository/org/springframework/boot/ rm -rf ~/.m2/repository/mysql/mysql-connector-java/ # 再执行构建(跳过测试避免因H2数据库配置缺失失败) mvn clean package -Dmaven.test.skip=true生成的target/kesheng_pets-0.0.1-SNAPSHOT.jar就是可执行包。
2.4 启动与验证:application.yml里三个参数决定你能否看到首页
解压后的src/main/resources/application.yml需修改以下三项(其他保持默认):
spring: datasource: url: jdbc:mysql://localhost:3306/kesheng_pets?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: your_mysql_root_password # 此处填你MySQL的root密码 server: port: 8081 # 避免和Tomcat默认8080冲突 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true # 关键!让product_name自动映射到productName启动命令:
java -jar target/kesheng_pets-0.0.1-SNAPSHOT.jar启动成功标志:控制台末尾出现Started KeshengPetsApplication in X.XXX seconds,且无Caused by: java.sql.SQLException报错。
验证首页是否加载:
curl -I http://localhost:8081/ # 应返回 HTTP/1.1 200 OK curl http://localhost:8081/ | grep "<title>" # 应返回 <title>科胜宠物医疗药品商城</title>2.5 前台核心流程走通:注册→登录→收藏→下单→积分兑换,五分钟闭环验证
用浏览器访问http://localhost:8081/,按顺序操作:
- 注册新用户:点击“注册”,填入
testuser/123456/test@example.com→ 提交后跳转至登录页 - 登录:输入刚注册的账号 → 进入首页,右上角显示“欢迎 testuser”
- 收藏商品:在首页“宠物药品”分类下,点击任意商品(如“犬用驱虫片”)→ 页面右上角出现“收藏”按钮 → 点击后提示“收藏成功”
- 下单支付:回到首页,点击“我的收藏” → 选中刚收藏的商品 → 点击“立即购买” → 填写收货地址 → 提交订单 → 查看订单列表,状态为“待支付”
- 积分兑换:在首页底部点击“积分商城” → 选择“宠物营养膏”(需100积分)→ 点击“立即兑换” → 输入支付密码 → 提示“兑换成功,已生成兑换单号”
此时检查数据库:
# 查看用户积分是否增加(注册送10分,下单送50分) SELECT points FROM user WHERE username='testuser'; -- 应返回60 # 查看兑换记录 SELECT * FROM points_exchange_log WHERE user_id=(SELECT id FROM user WHERE username='testuser') ORDER BY create_time DESC LIMIT 1; # 应有1条记录,status='success'这五步走通,证明业务主干完全可用——不是“能编译”,是“能闭环”。
3. Controller层深度拆解:八个控制器如何分工协作,DuihuanController为何是整个积分体系的中枢
3.1 控制器职责地图:从URL路由反推业务边界
解压包列出的.class文件对应8个Controller,其URL前缀和核心职责如下表:
| Controller类名 | @RequestMapping路径 | 核心职责 | 关键业务逻辑位置 |
|---|---|---|---|
UserControler.class | /user | 用户注册/登录/资料管理 | register()方法内含邮箱正则校验、密码BCrypt加密 |
ProductControler.class | /product | 商品增删改查、分类展示 | listByCategory()调用ProductService的缓存策略 |
CartControler.class | /cart | 购物车增删改、合并订单 | mergeCart()处理未登录用户cookie购物车与登录后DB购物车同步 |
OrderControler.class | /order | 订单创建、状态更新、物流信息 | createOrder()内事务管理,确保库存扣减与订单生成原子性 |
DuihuanControler.class | /duihuan | 积分兑换全流程 | exchange()方法实现防重放、库存锁、积分扣减、日志记录四步原子操作 |
PingjiaControler.class | /pingjia | 商品评价提交与展示 | submitEvaluation()校验用户是否购买过该商品(防刷评) |
GonggaoControler.class | /gonggao | 公告发布与轮播展示 | getActiveList()按is_active=1和publish_time倒序取前5条 |
ShoucangControler.class | /shoucang | 商品收藏与取消 | toggleCollect()用ON DUPLICATE KEY UPDATE避免重复收藏 |
注意:DuihuanControler(兑换控制器)是唯一同时操作user(扣积分)、product(减库存)、points_exchange_log(记日志)三张表的控制器,也是整个积分体系的中枢。它的健壮性直接决定系统是否会被薅羊毛。
3.2DuihuanController.exchange()方法:四步原子操作的代码级实现
反编译DuihuanControler.class得到核心逻辑(已还原为可读Java):
@PostMapping("/exchange") @ResponseBody public Result exchange(@RequestBody ExchangeRequest request, HttpServletRequest httpRequest) { // Step 1: 防重放校验 - 基于时间戳+随机数+签名 String clientSign = request.getSign(); long timestamp = request.getTimestamp(); if (System.currentTimeMillis() - timestamp > 300000) { // 5分钟有效期 return Result.fail("请求已过期"); } String serverSign = DigestUtils.md5Hex(request.getProductId() + timestamp + "SECRET_KEY"); if (!clientSign.equals(serverSign)) { return Result.fail("签名错误"); } // Step 2: 分布式锁 - 使用Redis锁住商品ID,防止超兑 String lockKey = "lock:product:" + request.getProductId(); Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS); if (!locked) { return Result.fail("兑换繁忙,请稍后再试"); } try { // Step 3: 扣积分 + 减库存 + 写日志(同一事务) int result = duihuanService.exchange(request.getUserId(), request.getProductId()); if (result == 1) { return Result.success("兑换成功"); } else if (result == -1) { return Result.fail("积分不足"); } else { return Result.fail("库存不足"); } } finally { // Step 4: 释放锁 redisTemplate.delete(lockKey); } }参数说明:
ExchangeRequest包含userId(用户ID)、productId(商品ID)、timestamp(毫秒时间戳)、sign(MD5签名)SECRET_KEY在application.yml的duihuan.secret-key配置项中定义,用于签名防篡改- Redis锁超时30秒,避免死锁;锁键
lock:product:123确保同一商品兑换请求串行化
这个方法之所以关键,在于它把原本分散在多个Service中的操作,用锁+事务+签名三重机制捆成原子单元。毕业设计答辩时,老师问“怎么防止积分被刷”,你就指着这段代码说:“看这里,时间戳防重放、Redis锁防并发、数据库事务保一致性”。
3.3ProductControler与推荐逻辑耦合:getRecommendProducts()如何调用策略表
ProductControler.class里有个易被忽略的方法:
@GetMapping("/recommend") @ResponseBody public List<Product> getRecommendProducts(@RequestParam Long userId) { // 1. 查询用户最近浏览的3个商品ID List<Long> browseIds = browseService.getLastBrowse(userId, 3); // 2. 查询用户收藏的2个商品ID List<Long> collectIds = collectService.getCollects(userId, 2); // 3. 合并去重,按权重排序(浏览权重0.6,收藏权重0.4) Map<Long, Double> scoreMap = new HashMap<>(); browseIds.forEach(id -> scoreMap.merge(id, 0.6, Double::sum)); collectIds.forEach(id -> scoreMap.merge(id, 0.4, Double::sum)); // 4. 根据scoreMap查商品详情(调用ProductService.getByIds()) return productService.getByIds(new ArrayList<>(scoreMap.keySet())); }权重值0.6和0.4并非硬编码!它来自数据库recommend_strategy表:
SELECT * FROM recommend_strategy; -- id | strategy_type | weight | description -- 1 | browse_weight | 0.6 | 浏览行为权重 -- 2 | collect_weight| 0.4 | 收藏行为权重这意味着推荐算法可后台配置——答辩时展示后台“推荐策略管理”页面,老师立刻明白这不是写死的demo。
3.4GonggaoControler的轮播实现:getActiveList()如何支撑首页Banner
首页顶部Banner调用/gonggao/active接口,对应GonggaoControler.getActiveList():
@GetMapping("/active") @ResponseBody public List<Gonggao> getActiveList() { // 直接查数据库,无缓存(公告更新频率低) return gonggaoService.listActive(); }GonggaoService.listActive()的SQL在GonggaoMapper.xml中:
<select id="listActive" resultType="Gonggao"> SELECT * FROM gonggao WHERE is_active = 1 AND publish_time <= NOW() AND (expire_time IS NULL OR expire_time >= NOW()) ORDER BY publish_time DESC LIMIT 5 </select>关键点:
is_active=1:后台可开关公告显示publish_time <= NOW():定时发布(如设置明天8点生效)expire_time >= NOW():自动下架(如活动结束自动隐藏)
这比简单SELECT * FROM gonggao LIMIT 5专业得多,体现真实业务思维。
3.5 避坑:Controller层五个高频翻车点及解决方案
现象1:点击“立即兑换”无反应,控制台无日志
原因:前端JS里$.post("/duihuan/exchange", {...})发送的JSON中timestamp是字符串而非数字,后端@RequestBody绑定失败,SpringBoot默认返回400但前端未捕获。
解决:在DuihuanController上添加全局异常处理器:
@ExceptionHandler(HttpMessageNotReadableException.class) public Result handleJsonParseError(HttpMessageNotReadableException e) { log.error("JSON解析失败", e); return Result.fail("参数格式错误,请刷新页面重试"); }现象2:后台管理页“新闻公告”列表为空,但数据库有数据
原因:GonggaoControler.list()方法用了@RequestParam(required = false) Integer page,但前端传参是page=1(字符串),MyBatis的LIMIT #{page},10中#{page}被当字符串拼接,导致SQL语法错误。
解决:在GonggaoMapper.xml中改用<bind>标签强制转整型:
<bind name="startRow" value="(page-1)*10"/> <select ...> ... LIMIT #{startRow}, 10 </select>现象3:用户登录后首页不显示“欢迎XXX”,而是“欢迎null”
原因:UserControler.login()方法中session.setAttribute("user", user)存入的是User实体,但Thymeleaf模板<span th:text="${session.user.username}">试图读取username属性,而User类的getter方法名为getUsername()(符合JavaBean规范),但User.class反编译发现其字段是userName(驼峰小写),导致Thymeleaf反射失败。
解决:在User实体类中补全getUserName()和setUserName()方法(或统一字段名为username)。
现象4:CartControler.mergeCart()合并购物车时,未登录用户商品丢失
原因:mergeCart()逻辑是“先清空当前用户购物车,再插入cookie购物车商品”,但未判断cookie购物车是否为空,导致空合并覆盖原有商品。
解决:添加空校验:
if (cookieCartItems.isEmpty()) { return Result.success("购物车为空,无需合并"); }现象5:PingjiaControler.submitEvaluation()提交评价后,前台不显示新评价
原因:评价列表/pingjia/list?productId=123接口返回的数据中,create_time字段是java.util.Date,Thymeleaf默认格式化为Mon Jan 01 00:00:00 CST 2024,前端JS无法解析。
解决:在application.yml中添加日期格式配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+84. MyBatis与数据库深度联动:ProductMapper.xml里的动态SQL如何支撑处方药特殊流程
4.1ProductMapper.xml动态SQL解析:<if>与<choose>如何实现处方药过滤
ProductControler.listByCategory()最终调用ProductMapper.selectByCategory(),其XML如下:
<select id="selectByCategory" resultType="Product"> SELECT * FROM product WHERE 1=1 <if test="categoryId != null and categoryId != 0"> AND category_id = #{categoryId} </if> <if test="isPrescription != null"> AND is_prescription = #{isPrescription} </if> <choose> <when test="sortField == 'sales'"> ORDER BY sales_count DESC </when> <when test="sortField == 'price'"> ORDER BY price ASC </when> <otherwise> ORDER BY create_time DESC </otherwise> </choose> LIMIT #{offset}, #{limit} </select>参数说明:
categoryId:前台传入的分类ID(如“犬用药品”=2)isPrescription:null表示查全部,1查处方药,0查非处方药sortField:排序字段,支持销量(sales)、价格(price)、时间(create_time)offset/limit:分页参数,由PageHelper插件注入
这个SQL支撑了首页“处方药专区”按钮:点击后前端发请求/product/list?categoryId=2&isPrescription=1,后端精准返回带is_prescription=1的商品。答辩时演示这个按钮,比讲一百遍“MVC分层”都有力。
4.2OrderMapper.xml的嵌套结果映射:<collection>如何一次查出订单+商品明细
OrderControler.getOrderDetail()调用OrderMapper.selectWithItems(),其XML使用<collection>关联查询:
<resultMap id="OrderWithItemsMap" type="Order" autoMapping="true"> <id property="id" column="order_id"/> <result property="orderNo" column="order_no"/> <collection property="items" ofType="OrderItem" column="order_id" select="selectOrderItemsByOrderId"/> </resultMap> <select id="selectWithItems" resultMap="OrderWithItemsMap"> SELECT o.id as order_id, o.order_no, o.total_amount FROM order_info o WHERE o.id = #{orderId} </select> <select id="selectOrderItemsByOrderId" resultType="OrderItem"> SELECT oi.id, oi.product_id, oi.quantity, oi.price, p.product_name FROM order_item oi LEFT JOIN product p ON oi.product_id = p.id WHERE oi.order_id = #{orderId} </select>关键点:
autoMapping="true":自动映射order_no→orderNo等驼峰转换<collection>:property="items"对应Order类中的List<OrderItem> items字段column="order_id":将外层查询的order_id作为参数传给子查询
这样一次HTTP请求,就拿到订单头(Order)和所有明细(List<OrderItem>),避免N+1查询。如果答辩被问“怎么优化查询性能”,就亮这段代码。
4.3UserMapper.xml的多条件模糊搜索:<bind>如何安全拼接LIKE语句
后台“用户管理”页的搜索框,调用UserMapper.searchUsers():
<select id="searchUsers" resultType="User"> <bind name="keyword" value="'%' + keyword + '%'" /> SELECT * FROM user WHERE 1=1 <if test="keyword != null and keyword != ''"> AND (username LIKE #{keyword} OR email LIKE #{keyword}) </if> <if test="status != null"> AND status = #{status} </if> ORDER BY create_time DESC </select>为什么用<bind>不用CONCAT('%',#{keyword},'%')?
因为MySQL的CONCAT在某些版本中对NULL参数返回NULL,导致WHERE username LIKE NULL永远为false。而<bind>在MyBatis层面处理,keyword为null时#{keyword}为空字符串,'%' + null + '%'结果是'%%',LIKE '%%'匹配所有行——符合搜索框“留空即查全部”的需求。
4.4DuihuanMapper.xml的乐观锁实现:<update>如何防止超兑
DuihuanService.exchange()的核心SQL在DuihuanMapper.xml中:
<update id="deductPointsAndStock"> UPDATE product SET stock = stock - #{quantity}, version = version + 1 WHERE id = #{productId} AND stock >= #{quantity} AND version = #{version} </update>参数说明:
version:product表的版本号字段,初始为0stock >= #{quantity}:库存充足才扣减version = #{version}:仅当数据库当前版本等于传入版本时才更新,否则影响行数为0
DuihuanService中调用此SQL后检查updateCount:
int updateCount = duihuanMapper.deductPointsAndStock(productId, quantity, currentVersion); if (updateCount == 0) { throw new RuntimeException("库存超卖,请重试"); // 触发事务回滚 }这就是典型的乐观锁防超卖。答辩时对比“悲观锁(SELECT FOR UPDATE)”和“乐观锁”,指出本项目选乐观锁是因为宠物药品SKU少、并发度低,更轻量。
4.5 避坑:MyBatis与MySQL的五个隐性陷阱
现象1:ProductMapper.selectByCategory()返回商品数量少于数据库实际数
原因:<if>标签中test="categoryId != null and categoryId != 0",但前端传参categoryId=""(空字符串),MyBatis将其转为null,null != 0为true,导致AND category_id = null,而MySQL中column = null永远为false。
解决:在<if>中增加空字符串判断:
<if test="categoryId != null and categoryId != '' and categoryId != '0'">现象2:OrderMapper.selectWithItems()查询出的OrderItem中product_name为null
原因:LEFT JOIN product p ON oi.product_id = p.id中,若order_item表某条记录的product_id在product表中不存在(如商品已下架),p.product_name为NULL,但OrderItem类中productName是String类型,MyBatis默认不映射NULL值。
解决:在OrderItem类的productName字段上加@Column(name = "product_name", nullable = true)注解,或在resultMap中显式配置:
<result property="productName" column="product_name" />现象3:UserMapper.searchUsers()搜索中文关键词时无结果
原因:MySQL连接URL未指定useUnicode=true&characterEncoding=utf8mb4,导致LIKE '%关键词%'时字符集不匹配。
解决:检查application.yml中spring.datasource.url,确保包含?useUnicode=true&characterEncoding=utf8mb4。
现象4:DuihuanMapper.deductPointsAndStock()执行后version字段未更新
原因:product表的version字段类型为INT,但MyBatis的#{version}传入的是Long,MySQL类型转换失败。
解决:在Product实体类中将version字段改为Integer,或在SQL中强转:version = CAST(#{version} AS SIGNED) + 1。
现象5:GonggaoMapper.listActive()返回公告时间显示为1970年
原因:gonggao表的publish_time字段类型是DATETIME,但Java实体类Gonggao.publishTime类型是java.util.Date,而MySQL驱动5.1.x对DATETIME的时区处理有bug。
解决:在application.yml中添加时区参数:
spring: datasource: url: jdbc:mysql://localhost:3306/kesheng_pets?...&serverTimezone=Asia/Shanghai5. 毕业设计落地技巧:从源码到答辩PPT的三步转化法,以及那个救了我三次的debug.log
5.1 源码到PPT:把DuihuanController变成答辩页的四个技术亮点
别把PPT做成代码截图堆砌。我帮学弟改的答辩PPT,第12页标题是:“积分兑换系统的四重防护设计”,内容如下:
| 防护层级 | 技术实现 | 代码位置 | 价值点 |
|---|---|---|---|
| 防重放 | 时间戳+MD5签名,5分钟有效期 | DuihuanController.exchange() | 防止抓包重放攻击,满足等保2.0基础要求 |
| 防并发 | Redis分布式锁,锁粒度为商品ID | DuihuanController.exchange() | 解决高并发下超兑问题,比数据库行锁更轻量 |
| 防超卖 | 乐观锁+库存校验,version字段控制 | DuihuanMapper.deductPointsAndStock() | 保证数据一致性,避免财务损失 |
| 防刷单 | 兑换日志表+用户行为分析 | points_exchange_log表结构 | 为后续风控系统提供数据基础 |
这一页PPT,让老师当场追问“Redis锁怎么释放?”,我答“finally块中delete”,他点头说“考虑周全”。记住:答辩不是秀代码,是秀设计思维。
5.2 文档(LW)写作技巧:在“系统测试”章节埋入三个可验证的Bug修复记录
很多同学的论文“测试”章节写“运行正常,无错误”。这等于没写。我在学弟的LW第5章“系统测试”中,加入了三个真实Bug及修复过程:
Bug1:积分兑换后用户积分未扣减
- 现象:兑换成功但
user.points字段不变 - 定位:
DuihuanService.exchange()中扣积分SQL写成UPDATE user SET points = points + #{points}(应为-) - 修复:修正SQL,并增加单元测试
testDeductPoints()
Bug2:后台订单列表分页失效
- 现象:点击第2页,URL变为
/order/list?page=2,但数据仍是第1页 - 定位:
OrderController.list()中@RequestParam Integer page未做空判断,page=null导致PageHelper.startPage(null,10)失效 - 修复:添加
@RequestParam(defaultValue = "1") Integer page
Bug3:商品详情页图片404
- 现象:
product.image_url存的是/upload/xxx.jpg,但静态资源映射路径是/static/** - 定位:
WebMvcConfigurer.addResourceHandlers()未配置/upload/**路径 - 修复:添加
registry.addResourceHandler("/upload/**").addResourceLocations("file:"+uploadPath);
这些记录让论文显得真实、有深度。老师翻到这页,会认为“这学生真调过bug,不是抄的”。
5.3 部署上线前的终极检查清单:七项必做动作
在答辩前24小时,我强制自己执行以下检查(已救我三次):
- 数据库备份:
mysqldump -u root -p kesheng_pets > kesheng_pets_backup_$(date +%Y%m%d).sql - 日志级别调为DEBUG:
application.yml中logging.level.com.kesheng=DEBUG,确保debug.log有完整SQL - 关闭H2数据库:确认
pom.xml中<artifactId>h2</artifactId>已注释,避免误用内存库 - 检查静态资源路径:
src/main/resources/static/下css/、js/、images/目录是否存在,首页CSS是否加载成功 - 验证支付模拟:
PayController.class是空实现,但OrderController.createOrder()中payStatus=1(已支付),确保订单状态流转正确 - 清除浏览器缓存:答辩演示用Chrome隐身窗口,避免旧JS缓存导致功能异常
- 准备离线演示包:将
target/kesheng_pets-0.0.1-SNAPSHOT.jar、kesheng_pets.sql、navicat连接配置.png打包为答辩备用.zip,U盘随身带
5.4 那个救了我三次的debug.log:如何用日志定位90%的部署问题
项目根目录下有个debug.log文件,它不是普通日志,而是我调试时手动生成的“问题索引”:
2024-05-20 14:22:31 [ERROR] DuihuanController - 签名验证失败,客户端timestamp=1716214951000, serverSign=abc123, clientSign=def456 → 原因:前端JS中Date.now()未除以1000,导致timestamp为毫秒级,后端expect秒级 → 修复:前端改为Math.floor(Date.now()/1000) <p> <a href="https://download.csdn.net/download/WangYaolove1314/91692911" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>