资讯动态

Spring Boot水产品交易管理系统设计与实现:订单库存闭环全解析

发布时间:2026/9/9 4:41:11 来源:尧图企业网站定制
做毕业设计最烦的事情是什么大概率是“题目拿了一个月代码还停在Hello World”。如果是MySQL、SSM那一套的老系统还好照着crud糊弄一下也能过可一旦涉及像水产品交易这种带业务场景、带订单流转、带库存和价格联动的管理类系统很多同学的Spring Boot水平就到了瓶颈。恰好我近期折腾完一套“Spring Boot水产品交易管理系统”的毕业设计源码编号35842趁着思路还热乎把核心设计、实现细节、常见坑位和排查套路完整写出来给正在做同类系统的同学一个能直接“抄作业”的参考。这套系统其实就是典型的交易管理双核心面向普通用户买家和运营方管理员围绕水产品的商品信息、分类、库存、订单、支付对接、售后状态做全流程闭环。技术上用的是Spring Boot 2.7 MyBatis Plus JWT Vue 3 Element Plus 这套组合数据库用MySQL 8.0开发工具IDEA。源码里自带基础数据脚本和文档能直接跑起来演示二次开发也能省掉大量从零搭框架的时间。无论你是正在选毕设题目还是已经定了但没思路这篇都能给你一块完整的拼图。1. 从题目到需求水产品交易系统的定位与核心链路拆解1.1 这类毕设题目的考核点到底在哪水产品交易管理系统名字听着不算起眼但把题目拆开看它实际上兜住了商业系统里绝大多数关键环节商品管理多规格、多维度的分类、订单生命周期下单、支付、发货、收货、取消、库存联动预占、扣减、回滚、用户体系买家、管理员、商户多种角色。一个Spring Boot项目能把这几条链路走通答辩时能讲的东西就非常多了。这个题目的考核重心不在于算法或高并发而在于业务建模的完整度以及Spring Boot工程结构的规范程度。老师大概率会关注这几个点数据库表设计是否合理订单与库存的关系有没有闭环JWT认证有没有做角色权限控制代码分层是否清晰前端是否能有基本的管理台交互。所以我在搭建这个系统的时候刻意没有跟风上“微服务全家桶”而是老老实实把单体架构做好。单体是毕业设计里最稳妥、也最好答辩的选择。你完全能理直气壮地说“单体优先是架构权衡的结果”而不是被面试官问得哑口无言。1.2 用户是谁核心场景是什么做项目之前先搞清楚角色的诉求。水产品交易系统里最核心的角色是两种C端买家和管理员。买家浏览水产品列表、按分类筛选、查看详情、加入购物车、下单、在线支付、查看订单状态、处理收货或申请售后、维护个人信息。管理员维护用户账号状态、管理商品分类、商品上下架、设置库存和价格、处理订单发货、关闭异常单、查看销售统计与数据面板。有一点要注意很多同学会把“买家下单”和“管理员上架”混在一个Controller里写这在答辩时是会被追问的。系统里我拆得非常明确admin接口统一走后台路由用户端走openApiJWT里通过角色字段区分访问范围数据权限也做了隔离这一点后期写在论文里非常加分。1.3 功能模块清单与优先级设计按毕设的交付习惯我把功能拆成两个大端再用一个公共的认证模块撑着。功能拆解如下用户端小程序/Web商城端注册、登录、token续期商品分类多级浏览、关键字检索商品详情页库存、价格、图片、简介购物车添加、修改数量、批量结算订单确认页、地址管理在线支付模拟支付通道、订单状态机流转收货地址CRUD、个人订单查询、订单取消/售后申请管理端后台管理台控制台数据概览今日订单数、销售额Top5、库存预警用户管理列表、冻结/解冻分类管理增删改查、排序商品管理新增规格、上下架、库存调整订单管理查询、发货、关闭、导出系统日志登录日志、操作日志另外加了一个比较轻量的数据初始化和SQL脚本方便不同环境直接起库。整体功能控制在“够用但不至于赶工”的粒度毕业设计阶段没必要上物流配送和会员积分那一套把核心闭环做扎实就赢了。2. 技术选型与工程结构为什么这么配有没有坑2.1 技术栈清单与版本选择这个项目我采用了非常主流、网上资料最多、踩坑成本最低的一套组合。技术版本过于激进反而容易在环境上花大量时间大家不要盲目追新。技术组件选用版本选型理由JDK1.8大多数学校机房和服务器环境仍以1.8为主兼容性好Spring Boot2.7.18稳定第三方集成资料丰富避免3.x和javax/jakarta切换的坑MyBatis Plus3.5.3单表CRUD零SQL复杂查询用注解即可MySQL8.0.x成熟稳定支持JSON字段用于商品规格快照JWTjjwt 0.11.5无状态认证适合前后端分离项目Hutool5.8.x工具库减少重复代码树形结构、日期处理、加密Vue 3 Element Plus3.x管理后台开发效率高组件齐全Maven3.6依赖管理毕业设计标配选Spring Boot 2.7而不是3.x是有具体原因的。网上大量现成案例、老师的PPT、答辩组老师的认知基本都是2.x体系换到3.x后很多配置类名和starter名称变化会造成不必要的信息差。而且到了部署阶段2.7对JDK8的兼容会让你省心很多。2.2 工程目录与三层架构划分很多同学的工程坏就坏在一股脑往controller里塞代码。这个题目虽然数据量不大但代码组织规范一点后面查问题和扩展都会舒服很多。我采用的包结构如下com.example.aquatic ├── common // 全局返回结果、异常处理、常量、枚举 ├── config // 跨域、MyBatis Plus分页插件、JWT拦截器配置 ├── controller // 前端接口入口按模块拆分文件 ├── service // 业务核心接口实现分离 ├── mapper // MyBatis Plus接口层 ├── entity // 数据库实体映射 ├── dto // 前端参数模型 ├── vo // 视图返回模型 └── utils // JWT工具、日期工具等按照这样的划分代码的可读性比什么都能往controller里塞的版本高一大截。写论文的时候画软件的架构图也会非常清晰不会出现“一张图里面全是箭头和调用”的尴尬场景。2.3 前后端分离还是服务端渲染如果你是一个人在做毕设不追求“前后端分离”这个亮点标签那用Thymeleaf这种服务端渲染确实会更省事。但考虑到现在答辩组的老师普遍会关注“工程化”概念而且后台管理界面的交互如果只用JQuery来实现会很费功夫我最终选择了Vue 3 Element Plus做管理台前端用户端做一套简单但功能齐全的Web商城页面。前后端分离之后CORS跨域问题需要处理好。全局CORS配置一下也不复杂关键是得注意自定义拦截器里面token验证失败时的响应不能被CORS拦截掉否则前端会同时面临跨域和401双重迷惑这里我后面会在常见问题里详细展开。2.4 为什么还需要一套可运行的SQL初始化脚本源码压缩包里我放了一个database.sql里面包含建库建表语句和基础数据。这么做的原因很实在大多数同学拿到源码后的第一动作不是读文档而是期望一键跑起来。如果没有预置的分类数据和几个演示商品你第一次登录进去看到空空如也的界面很容易误以为系统挂掉了。初始化脚本里放默认管理员账号admin / admin123、测试买家账号以及若干商品数据演示效率直接翻倍。3. 数据库设计水产品交易系统的表结构与关系梳理3.1 核心表清单这张表是整套系统的地基。按业务域划分我建了8张核心表user_account用户账号角色、状态、手机号、昵称、头像product_category水产品分类支持两级树形product_info商品基本信息名称、图片、描述、上下架状态product_sku商品规格库存重量区间、价格、库存、预警值shopping_cart购物车记录order_info订单主表订单号、状态、金额、实付、用户ID、地址快照order_item订单明细表商品快照、数量、单价快照address_info收货地址表在此基础上还有access_log登录日志和oper_log操作日志两张辅助表用于支撑后端管理台的数据展示和审计诉求。这个设计里加了一个“快照”思路说一下为什么。对于订单类的系统商品名称、价格、规格都是会变的如果你下单之后在订单明细里去关联商品表的最新价格会出大问题比如后台改了价格导致历史订单金额错乱。所以下单时把商品名称、单价、规格、图片等信息冗余一份到order_item表这就是典型的“用空间换正确性”的建模思路。蛋糕店下单会打印小票小票上的价格和现在蛋糕店的挂牌价无关就是这个道理。3.2 关键表字段设计与索引思路用户表的核心字段CREATE TABLE user_account ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录用户名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, phone varchar(20) DEFAULT NULL, nickname varchar(50) DEFAULT NULL, user_role tinyint(4) NOT NULL DEFAULT 1 COMMENT 1买家 2管理员, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1正常 0冻结, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户账号表;注意username一定要加唯一索引这是用户体系的第一道保障。密码这里我没有用MD5而是采用了BCrypt加密Spring Security里的BCryptPasswordEncoder可以直接做校验安全性好很多也方便在论文里写一句“采用BCrypt哈希对用户口令进行保护”。商品SKU表的字段设计要重点说因为水产品的“规格”和衣服鞋子不一样更多是重量区间比如0.5-1kg、1-1.5kg同一品种、不同规格价格可能完全不同CREATE TABLE product_sku ( id bigint(20) NOT NULL AUTO_INCREMENT, product_id bigint(20) NOT NULL COMMENT 关联商品ID, sku_name varchar(100) DEFAULT NULL COMMENT 规格名称如0.5-1kg, price decimal(10,2) NOT NULL COMMENT 售价, original_price decimal(10,2) DEFAULT NULL COMMENT 划线原价, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, safety_stock int(11) DEFAULT 10 COMMENT 库存预警值, status tinyint(4) DEFAULT 1 COMMENT 1启用 0停用, PRIMARY KEY (id), KEY idx_product_id (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品规格库存表;订单状态我用的是tINYINT类型0已取消、10待支付、20已支付/待发货、30已发货/待收货、40已完成、50售后中。这里不建议直接用字符串状态因为排序筛选和统计的性能差别很大而且用整数枚举在代码里写状态机时也更干净。CREATE TABLE order_info ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务订单号, user_id bigint(20) NOT NULL, total_amount decimal(10,2) NOT NULL COMMENT 商品原价总额, pay_amount decimal(10,2) NOT NULL COMMENT 实付金额, order_status tinyint(4) NOT NULL DEFAULT 10 COMMENT 订单状态, receiver_name varchar(50) NOT NULL, receiver_phone varchar(20) NOT NULL, receiver_address varchar(200) NOT NULL, pay_time datetime DEFAULT NULL, deliver_time datetime DEFAULT NULL, finish_time datetime DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_status (order_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;订单号我采用“yyyyMMddHHmmss 随机数”的方式生成不用数据库自增ID当订单号是为了避免订单号被猜测——你在小店下单后如果小票单号是“1、2、3”隔壁桌猜一下就能拿你的号去冒领餐品。生成逻辑用Hutool的IdUtil.getSnowflakeNextIdStr()简单可靠。3.3 商品分类的表结构怎么设计才不low很多糙快猛的毕设会把分类做成一张单表用parent_id自关联这次我也没有免俗。不过加了一个排序字段sort和状态字段status。需要注意的是删除分类之前一定要判断该分类下是否存在商品。写一个count查询如果商品数量大于0直接抛业务异常提示管理员先去转移商品。否则删完分类之后商品列表会出现空分类的“孤儿数据”看起来非常拉胯。4. 核心功能实战认证、商品、购物车与订单状态机4.1 JWT认证设计与拦截器配置JWT在前后端分离项目里几乎是标配核心思路是用户登录成功后服务端签发一个包含用户ID、角色、过期时间的token。后续请求在Header里带上Authorization: Bearer 拦截器解析并校验合法性然后直接把用户信息放入ThreadLocal供Service层直接使用。工具类如下浓缩版public class JwtUtil { private static final String SECRET your-secret-key-your-secret-key; private static final long EXPIRE 1000 * 60 * 60 * 24L; // 24小时 public static String generateToken(Long userId, Integer role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }注意一个非常容易被忽视的细节SECRET的长度必须32字节否则HS256会报错。许多同学卡在一个极其隐蔽的WeakKeyException上折腾一晚上其实就是密钥长度不够。这里建议直接用至少32个字符以上的随机串。拦截器里做了一个区分未登录可以访问商品列表、商品详情但下单、查看订单、购物车相关接口必须登录。把这些白名单路径加上registry.addInterceptor(jwtInterceptor()) .addPathPatterns(/**) .excludePathPatterns( /api/auth/login, /api/auth/register, /api/open/**, /error );管理端接口统一以/api/admin为前缀拦截器里再判断一下角色字段不是2的话就抛403。这是最粗粒度也最有效的管理权限防线。4.2 商品列表的分页与多条件检索商品检索这一块用了MyBatis Plus的分页插件加上LambdaQueryWrapper做多条件拼接。核心代码是这样public IPageProductInfoVO queryProductPage(ProductQueryDTO dto) { PageProductInfo page new Page(dto.getPageNum(), dto.getPageSize()); LambdaQueryWrapperProductInfo wrapper new LambdaQueryWrapper(); wrapper.eq(ProductInfo::getStatus, 1); if (StrUtil.isNotBlank(dto.getKeyword())) { wrapper.and(w - w.like(ProductInfo::getProductName, dto.getKeyword()) .or().like(ProductInfo::getProductDesc, dto.getKeyword())); } if (dto.getCategoryId() ! null) { wrapper.eq(ProductInfo::getCategoryId, dto.getCategoryId()); } wrapper.orderByDesc(ProductInfo::getCreateTime); PageProductInfo result productInfoMapper.selectPage(page, wrapper); // 转换VO并填充SKU列表与库存状态 return convertToVO(result); }关键词检索这里如果直接用“name like or desc like”可能会把完全无关的“野生捕捞三文鱼”也搜出来因为desc字段里可能包含某个关键词的片段。可以用一个优化的做法把搜索目标缩小到name和categoryName借助categoryName反查分类ID再做等值过滤。效果更好性能也OK。4.3 购物车与结算从勾选到生成订单的完整链路用户加购时前端只传SKU ID和数量。后端要做的校验包括商品是否上架、SKU是否启用、库存是否足够。购物车表和业务场景挂钩不需要搞太复杂一个userId skuId 的唯一键即可数量做累加或覆盖都可以。核心下单的逻辑是这次的重头戏完整的链路是这样参数接收一个“订单提交DTO”里面包含skuId列表、对应数量、收货地址ID。加锁预占库存这里我用了MySQL悲观锁后续会讲为什么不用乐观锁。查询SKU及关联商品校验状态和库存。按最新价格计算出总金额、实付金额可以把运费减免写死在常量里。生成订单主表记录、循环生成订单明细记录同时写入“商品快照”。扣减库存清空已购买的购物车项。中间任何一步失败要么整体事务回滚要么补偿恢复库存。用代码来表达这一步Service层的方法大致长这样Transactional(rollbackFor Exception.class) public OrderInfo createOrder(OrderCreateDTO dto, Long userId) { // 1. 地址校验 AddressInfo address addressInfoMapper.selectById(dto.getAddressId()); if (address null || !address.getUserId().equals(userId)) { throw new BizException(收货地址不存在); } // 2. 预占库存for update锁住SKU行 ListOrderItem itemList new ArrayList(); BigDecimal totalAmount BigDecimal.ZERO; for (OrderItemDTO item : dto.getItems()) { ProductSku sku productSkuMapper.selectByIdForUpdate(item.getSkuId()); if (sku null || sku.getStatus() ! 1) { throw new BizException(商品规格不存在或已下架); } if (sku.getStock() item.getQuantity()) { throw new BizException(商品库存不足 sku.getSkuName()); } ProductInfo product productInfoMapper.selectById(sku.getProductId()); // 组装明细快照 OrderItem orderItem new OrderItem(); orderItem.setProductId(product.getId()); orderItem.setProductName(product.getProductName()); orderItem.setSkuName(sku.getSkuName()); orderItem.setPrice(sku.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItem.setPicUrl(product.getMainPic()); itemList.add(orderItem); totalAmount totalAmount.add(sku.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 3. 生成订单主表 OrderInfo orderInfo new OrderInfo(); orderInfo.setOrderNo(generateOrderNo()); orderInfo.setUserId(userId); orderInfo.setTotalAmount(totalAmount); orderInfo.setPayAmount(totalAmount); orderInfo.setOrderStatus(10); // ... 地址字段写入 orderInfoMapper.insert(orderInfo); // 4. 写入明细并扣库存、清购物车 for (OrderItem item : itemList) { item.setOrderId(orderInfo.getId()); orderItemMapper.insert(item); productSkuMapper.deductStock(item.getSkuId(), item.getQuantity()); } shoppingCartMapper.deleteByUserAndSkuIds(userId, dto.getSkuIds()); return orderInfo; }这里有个很值得在答辩里展开的点为什么使用selectByIdForUpdate这种悲观锁而不是乐观锁的版本号方案。原因在于订单创建和库存扣减是一个强一致性的高频操作水产品交易系统里的并发峰值不高悲观锁简单可靠能有效避免超卖。如果未来真的要做高并发扩展再替换成Redis分布式锁或乐观库存扣减也不迟。毕业设计阶段用悲观锁把正确性放在第一位完全站得住脚。4.4 订单状态机的流转实现订单状态机是整个系统的灵魂也是最容易写成一坨乱麻的地方。我选择了“状态枚举 状态修改方法”的集中式管理模式public enum OrderStatusEnum { CANCELED(0, 已取消), UNPAID(10, 待支付), PAID(20, 待发货), DELIVERED(30, 待收货), FINISHED(40, 已完成), AFTER_SALE(50, 售后中); private final Integer code; private final String desc; // 构造器、getter略 }每次状态变更都在Service层封装一个独立方法不允许直接对order_status字段做裸更新。例如cancelOrder仅允许从待支付状态流转到已取消同时解锁库存。payOrder仅允许从待支付流转到已支付记录支付时间。deliverOrder管理员操作仅允许从已支付流转到已发货。confirmReceive仅允许从已发货流转到已完成记录完成时间。applyAfterSale仅允许从已完成签收时间内流转到售后中并冻结相应商品库存。在取消订单这里有一个高概率踩坑的地方取消时必须恢复库存而且只能恢复该订单明细对应的SKU库存。如果用“一次性给SKU加回一个固定数字”去处理最后库存就乱了。正确做法是遍历order_item按skuId和quantity进行加回public void cancelOrder(Long orderId, Long userId) { OrderInfo order orderInfoMapper.selectById(orderId); if (order null || !order.getUserId().equals(userId)) { throw new BizException(订单不存在); } if (order.getOrderStatus() ! OrderStatusEnum.UNPAID.getCode()) { throw new BizException(当前状态不可取消); } // 回滚库存 ListOrderItem items orderItemMapper.selectList( new LambdaQueryWrapperOrderItem().eq(OrderItem::getOrderId, orderId)); for (OrderItem item : items) { productSkuMapper.restoreStock(item.getSkuId(), item.getQuantity()); } order.setOrderStatus(OrderStatusEnum.CANCELED.getCode()); orderInfoMapper.updateById(order); }4.5 模拟支付的设计思路支付单独做一个微服务或者对接沙箱太重了毕设场景下不需要。要设计的是“模拟支付”流程下单后订单状态是待支付前端页面提供一个“去支付”按钮后端提供一个payOrder接口用一个固定的模拟支付账号直接置为已支付即可。为了增加答辩的表演效果可以在支付接口里写死一个1秒的延时前端弹窗展示“支付处理中”让整个过程显得更真实。public void payOrder(Long orderId, Long userId) { // 模拟第三方支付回调延迟 // 实际项目中这里应校验支付平台签名此处简化 OrderInfo order orderInfoMapper.selectById(orderId); if (order null || !order.getUserId().equals(userId)) { throw new BizException(订单不存在); } if (order.getOrderStatus() ! OrderStatusEnum.UNPAID.getCode()) { throw new BizException(订单状态异常无法支付); } order.setOrderStatus(OrderStatusEnum.PAID.getCode()); order.setPayTime(LocalDateTime.now()); orderInfoMapper.updateById(order); }这里其实可以加一个“支付流水表”来记录模拟交易但这个功能在毕业设计里属于加分项时间不够完全可以不写。5. 管理后台与数据面板让项目从“能用”到“好看”5.1 管理端前端页面与后端接口设计后台管理页面用的Vue 3 Element Plus组件风格统一开发效率高。核心页面包括仪表盘、商品管理、订单管理、用户管理、分类管理。每个页面都对应一组RESTful接口GET /api/admin/dashboard/summary获取今日订单数、今日销售额、用户总数、商品总数GET /api/admin/product/page分页查询商品含关键字、分类筛选POST /api/admin/product新增商品含SKU列表PUT /api/admin/product/{id}修改商品与SKUDELETE /api/admin/product/{id}删除商品逻辑删除GET /api/admin/order/page分页查询订单按状态、订单号、用户手机号筛选POST /api/admin/order/{orderId}/deliver订单发货GET /api/admin/user/page分页查询用户PUT /api/admin/user/{id}/status冻结/解冻用户管理端的接口数据在返回时Service层统一转换为VO杜绝直接把Entity吐给前端避免密码等敏感字段泄露。举例说明订单管理列表的分页查询public IPageOrderAdminVO queryAdminOrderPage(OrderAdminQueryDTO dto) { PageOrderInfo page new Page(dto.getPageNum(), dto.getPageSize()); LambdaQueryWrapperOrderInfo wrapper new LambdaQueryWrapper(); if (dto.getOrderStatus() ! null) { wrapper.eq(OrderInfo::getOrderStatus, dto.getOrderStatus()); } if (StrUtil.isNotBlank(dto.getOrderNo())) { wrapper.like(OrderInfo::getOrderNo, dto.getOrderNo()); } if (StrUtil.isNotBlank(dto.getUserPhone())) { // 先按手机号查出用户ID再过滤订单 ListLong userIds userAccountMapper.selectUserIdsByPhone(dto.getUserPhone()); if (CollUtil.isEmpty(userIds)) { return new Page(dto.getPageNum(), dto.getPageSize()); } wrapper.in(OrderInfo::getUserId, userIds); } wrapper.orderByDesc(OrderInfo::getCreateTime); // 关联查询用户昵称 PageOrderInfo result orderInfoMapper.selectPage(page, wrapper); return convertToAdminVO(result); }5.2 仪表盘统计给答辩PPT喂数据仪表盘是管理后台的门面也是答辩时老师第一眼看到的东西。我做了四个统计卡片今日订单数、今日销售额、商品总数、注册用户数再加上“最近一周订单趋势”柱状图。后端用一个聚合查询来完成public DashboardSummaryVO getSummary() { DashboardSummaryVO vo new DashboardSummaryVO(); vo.setTodayOrderCount(orderInfoMapper.selectCount( new LambdaQueryWrapperOrderInfo() .ge(OrderInfo::getCreateTime, LocalDate.now().atStartOfDay()))); vo.setTodaySales(orderInfoMapper.sumTodaySales(LocalDate.now().atStartOfDay())); vo.setProductCount(productInfoMapper.selectCount(null)); vo.setUserCount(userAccountMapper.selectCount( new LambdaQueryWrapperUserAccount().eq(UserAccount::getUserRole, 1))); return vo; }这个sumTodaySales可以是一条自定义SQL在Mapper里写Select(SELECT COALESCE(SUM(pay_amount), 0) FROM order_info WHERE order_status IN (20,30,40) AND pay_time #{startTime}) BigDecimal sumTodaySales(LocalDateTime startTime);“最近一周订单趋势”则用了一个GROUP BY日期的查询按天聚合订单数和销售额前端拿到后直接渲成ECharts柱状图。这一块是答辩现场的视觉记忆点值得多花半小时做。5.3 文件上传如何处理商品图片的上传我没有引入OSS或者MinIO而是存到本地磁盘再通过一个静态资源映射暴露URL。Spring Boot里配置spring: web: resources: static-locations: file:${upload.path}/或者在配置类里加一个资源映射Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: uploadPath /); }上传接口接收MultipartFile保存时用UUID重命名文件防止中文文件名乱码和路径穿越问题public String uploadFile(MultipartFile file) { if (file.isEmpty()) { throw new BizException(文件不能为空); } String originalFilename file.getOriginalFilename(); String ext StrUtil.subAfter(originalFilename, ., true); String fileName IdUtil.fastSimpleUUID() . ext; File dest new File(uploadPath, fileName); try { file.transferTo(dest); } catch (IOException e) { throw new BizException(文件上传失败); } return /upload/ fileName; }需要注意一点在Windows本地跑没问题但部署到Linux服务器后路径分隔符和权限都要检查。为了省心可以在配置里把上传路径放到项目外部比如/opt/aquatic/upload这样打jar包后也能灵活指向。6. 排查实录Spring Boot水产品系统里最常见的6个大坑6.1 坑位一MyBatis Plus分页失效查询返回全表这是新手最高频的坑分页插件没生效。检查点是否在Config类里注册了PaginationInnerInterceptor。是否把拦截器加到了MybatisPlusInterceptor而不是直接加了个空bean。正确的配置长这样Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }每一条都需要缺一不可。如果注册了还不行检查mybatis-plus版本是否和Spring Boot 2.7兼容3.5.x基本都没问题。6.2 坑位二日期时间字段返回给前端变成了时间戳LocalDateTime默认序列化为数组或者时间戳的问题困扰很多人。解决方式有两种一是在字段上加JsonFormat(patternyyyy-MM-dd HH:mm:ss)但字段一多就很烦更推荐二在application.yml里全局配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8如果实体里用了LocalDateTime还需要引入jackson-datatype-jsr310依赖Spring Boot starter-web里通常已经带上了这样返回前端的日期就是规范的字符串了。6.3 坑位三前端请求接口跨域报错偶尔还带401跨域配置和JWT拦截器同时存在时容易出现优先级问题。我的做法是用过滤器实现CORS而不是在Controller上加CrossOrigin注解。一个容易踩的细节如果使用自定义拦截器拦截所有请求且拦截器在token校验失败时直接返回401的JSON此时如果前端发的是预检请求OPTIONS后端没有对OPTIONS放行就会出现“跨域测试通过但真实请求被拦截”的诡异现象。对策很简单拦截器里对HttpMethod.OPTIONS直接放行if (request.getMethod().equals(HttpMethod.OPTIONS.name())) { return true; }6.4 坑位四库存扣减出现负数超卖这个问题在并发量稍高一点时就会出现。由于我选择了悲观锁本质上已经解决了。但有一种情况会导致问题依旧——多次点击下单按钮。前端要做按钮防重复提交后端也要在创建订单接口入口处做幂等处理可以用Redis存一个用户级别的短时间锁或用“订单号唯一索引”兜底双保险最稳。6.5 坑位五修改商品时SKU更新逻辑错误很多同学在做“编辑商品”功能时会把前端传来的SKU列表全删了再重新插入。这样做有两个问题一是历史订单明细里的sku_id会被置为失效如果后续要退款退货会有很大隐患二是自增ID不断增长而且如果删除操作失败会导致数据混乱。我的做法是前端传的SKU里带id的做更新不带id的做新增同时找出数据库中不存在于前端列表的SKU做逻辑删除。虽然逻辑多了几行但数据的完整性和可追溯性完全不一样。6.6 坑位六数据库时间与本地时间相差8小时这个问题的根源是serverTimezone配置和MySQL的时区不一致。推荐在JDBC连接串里明确指定jdbc:mysql://localhost:3306/aquatic?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue同时检查MySQL全局时区SET GLOBAL time_zone 8:00; SET SESSION time_zone 8:00;如果Java层面已经用的LocalDateTime通常不会有太大问题但如果项目里用了new Date()传给MySQL的datetime字段就要注意两者之间的时区差异。7. 我的一线实操心得与这个小项目的扩展价值整套系统从建表到前后端联调通大概花了三周左右的业余时间。给我最大的感受是做毕设项目最难的不是某个技术点不会而是把散落的需求串成一条能自圆其说的业务闭环。比如订单和库存一定要联动商品SKU和商品信息一定要拆开登录认证和角色权限一定要分清这些都是这个题目能带给你的核心收获。如果要在这个基础上做扩展我建议按这几个方向选一个深挖即可一是加一个基于ECharts的销售趋势分析面板把订单数据按周按月聚合二是接一个真实的微信支付沙箱或支付宝沙箱把模拟支付替换成真实流程三是给买家端做一个微信小程序端复用现有API工作量可控但答辩亮点非常足四是引入Redis缓存首页商品列表和分类信息减少数据库压力顺带讲清楚了“缓存穿透、击穿、雪崩”的解决方案。这些中的任何一个都能让你的项目在答辩时比同组同学明显高出一截。最后再说一个细节。源码里我是自带README文档和数据库初始化脚本的但千万别把“能跑”当成“做完”。写论文时数据库设计的ER图、订单状态机的状态图、JWT认证流程图这“三张图”基本决定了老师对你系统的第一印象。把细节打磨好让老师看到你对业务和代码是有整体把控的比展示多少个页面都管用。希望这篇拆解能帮到正在和Spring Boot系统死磕的你。如果在搭建过程中遇到报错rebuild一下项目看下控制台有没有SQL异常再对着上面六个坑逐一排查大概率能顺下来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价