毕业设计季总有人拿着“宠物用品系统”这个题目来找我聊说白了这个题在Java Web方向的选题清单里几乎年年出现。还有一类人不是学生是宠物店老板想把自己那套手工记账和微信群接单的流程搬到线上。这两类人虽然出发点不一样但最终要的东西是同一件事一套基于Spring Boot的宠物用品商城系统能让用户逛、选、买能让管理员管商品、管订单、分析卖得好的货。这个题听起来简单但它其实覆盖了一条完整的web开发链路——注册登录、类目导航、商品搜索、购物车、订单流转、后台管理、数据统计。把这些模块一个个打通你对Java后端这套体系的掌握程度基本就到“能干活”的水平了。这篇文章我不讲空泛概念直接按我做这类项目的习惯把系统拆成五块来讲需求边界怎么定、技术栈为什么这么选、数据库怎么设计、核心模块的代码思路、以及那些没有出现在文档里但一定会踩到的坑。1. 需求定位宠物用品系统要做什么不做什么1.1 使用角色与核心流程任何系统第一步不是写代码是搞清楚给谁用。宠物用品系统的典型使用者有两种消费者和管理员。消费者在手机或电脑上逛商城管理员在后台维护商品、处理订单。消费者侧的核心路径基本固定注册/登录 → 按类目逛猫粮、狗粮、猫砂、玩具、驱虫药 → 搜索商品 → 进详情页看价格和库存 → 加购物车 → 下单填写收货地址 → 支付/货到付款 → 查看订单状态 → 确认收货。宠物用品有个特点复购率极高——猫粮狗粮是消耗品用户一旦认准某个品牌就会反复购买。所以系统里最好有“常购清单”或者“按购买历史推荐”这类小功能哪怕做简单一点也能让系统比单纯的商品展示更有业务深度。管理员侧的核心流程是管理员登录 → 维护商品分类和商品信息 → 处理用户订单发货/取消 → 管理会员 → 查看销售统计。对宠物店来说后台能不能看清哪个品类卖得好非常关键。很多初级开发只做CRUD忘了把“统计报表”放进后台这样系统就缺少了最直接的决策价值。1.2 功能清单与“本期不做”清单整理需求时我习惯做两个清单一个是“这期必须做”一个是“这期绝对不能碰”。很多项目做砸不是因为做得少而是因为想做太多。必须有用户端注册登录、商品分类浏览、关键词搜索、商品详情、购物车、下单、订单列表、个人中心地址管理、宠物档案管理端管理员登录、商品增删改查、分类增删改查、图片上传、订单状态变更、用户列表、销售数据统计本期不做不做真实支付对接微信/支付宝。毕设和大部分内部系统用“模拟支付”就够了对接真实支付需要商户资质流程很长对项目验收没有本质帮助。不做即时聊天。客服系统看着加分但会牵扯大量在线会话逻辑不值得。不做店铺分销、拼团、秒杀。这些营销玩法规则复杂做了还没说清楚不如把基础订单管好。把边界划清楚之后你写代码时思路会非常顺。表格整理如下模块本期范围说明用户端注册、登录、浏览、购物车、订单核心闭环管理端商品、分类、订单、用户、统计核心闭环支付模拟支付/货到付款不做真实对接营销优惠券可选不做拼团秒杀即时通讯无不做1.3 宠物行业场景带来的隐性需求宠物用品系统和普通日用品商城有个明显差异商品属性差异巨大。狗粮要按重量卖1.5kg、3kg、10kg猫砂要按膨润土还是豆腐砂区分驱虫药要按宠物年龄筛选。所以商品表上最好留一个规格字段或者做一个简单的规格表前端展示时能显示“规格3kg装”而不是让用户去猜。这个细节很实际做出来之后答辩的时候老师会认为你真的研究过业务而不是单纯在写增删改查。2. 技术选型和数据库设计为什么Spring Boot刚刚好2.1 技术栈的取舍这个项目叫“Spring Boot基于Java的宠物用品系统”所以主语言是Java框架是Spring Boot这一点没有悬念。选型理由也很朴素Spring Boot的自动配置让项目能快速从一个空目录跑起来不用像SSM那样准备一堆XML文件学起来省心用起来稳定社区资料多到你想踩的坑别人早就踩完了。前端技术栈按团队熟悉程度来。如果你是偏后端的直接用模板引擎也能完成整站就是交互体验粗糙一点。我习惯前后端分离前端用Vue这类框架通过接口和Java后端交互这样分工清晰后期扩展也更方便。不过如果是一个人全干前后端分离意味着维护两套代码成本翻倍。毕设场景我更推荐一个折中方案后端模板引擎渲染核心页面前端用少量原生JavaScript或Vue的CDN模式做交互。这样劳动量可控对方看代码也不费劲。数据库选MySQL没什么好纠结的主流稳定教程多。用Redis做缓存属于“加分项”——缓存商品分类、首页推荐位这种热点数据能体现你对性能有所思考但别指望一台开发环境的MySQL有多弱不要为了用Redis而用。2.2 分层架构与包结构拿到了技术选型下一步是定工程结构。我第一次带人做项目时候发现新手最常犯的错误是把所有代码堆在Controller里面一个方法写一百行查完表还要在方法里做各种判断。这不是不能跑是没法维护。我常用的分层结构是这样com.example.petmall ├── controller // 接收请求返回结果 ├── service // 业务逻辑事务边界 ├── dao // 数据访问层MyBatis-Plus的Mapper ├── entity // 数据库实体类 ├── dto // 请求/响应对象避免直接暴露实体 ├── config // 配置类比如拦截器、WebMvc配置 ├── common // 通用返回结果、异常处理、常量 └── utils // 工具类这里有个关键点业务逻辑必须放在service层。比如“下单”涉及库存扣减、订单生成、购物车清理这个流程必须在service的一个事务方法里完成而不能在controller里调三个dao方法。controller只做参数接收和结果包装这样代码可测试性高出了问题也方便定位。2.3 数据库核心表设计数据库设计是我最看重的一步。很多项目后期改起来痛苦根源就是表结构没想清楚。宠物用品系统的核心表可以控制在7到9张下面这个列表直接可抄表名作用关键字段user用户表id, username, password, phone, avatar, create_timecategory分类表id, name, parent_id, sort, iconproduct商品表id, category_id, name, subtitle, main_image, detail, price, stock, sales, statusproduct_spec规格表id, product_id, spec_name, stock, pricecart_item购物车项id, user_id, product_id, quantity, checkedorders订单主表id, order_no, user_id, total_amount, status, receiver_name, receiver_phone, address, create_timeorder_item订单明细表id, order_id, product_id, product_name, product_image, price, quantitypet_profile宠物档案表id, user_id, pet_name, pet_type, pet_birthday, breed价格字段必须用decimal绝不能用double或float这是金额精度问题的底线。后面我会专门讲为什么。订单状态我建议用整数存储配合常量或枚举类。比如0待支付1已支付待发货2已发货3已完成4已取消。这里要特别注意状态不要直接在业务代码里写魔法数字应该封装成常量或枚举否则后期加一个状态要到处改非常痛苦。分类表的parent_id是为了支持两级分类比如“猫粮”下面挂“猫主粮”和“猫零食”。实现上就是自关联查询的时候先查一级分类再根据parent_id查子分类前端展示二级联动。这个结构简单且实用比单独建两张表要省事得多。3. 核心模块的实现思路从登录到订单流转3.1 登录会话与权限控制登录这块我推荐直接使用Java生态里最普及的方案Spring Security JWT或者轻量一点自己拦截Token。很多新手觉得Spring Security配置太复杂确实Security的过滤器链对初学者不太友好。但你要是写一个毕设完全可以用更轻的方式自定义一个JwtInterceptor注册到Spring MVC的拦截器列表里遇到需要登录的请求就检查请求头里的Token校验通过就把用户信息放到ThreadLocal里供后续使用。JWT本身结构不复杂分成三部分header.payload.signature。服务器签发Token时用密钥对payload签名客户端每次请求带上这个Token服务器验签通过就认为是合法登录用户。好处是服务端不用存Token天然支持水平扩展。坏处是踢用户下线比较麻烦但项目阶段不涉及这些场景忽略即可。管理员和普通用户可以用字段role区分。拦截器里做一个简单的权限判断如果请求的URL以/admin/开头且当前用户角色不是管理员直接返回无权限。这套轻量级权限方案足够应付整个系统了。3.2 商品浏览、分类筛选与购物车商品列表页是流量入口也是缓存收益最明显的地方。首页的分类导航、热卖推荐这些数据变化不频繁完全可以在第一次查询后放进Redis设置几分钟过期时间。我做过一个简单实现查询分类时先查缓存缓存没有再到数据库查然后写回缓存。代码量不多但能在“性能优化”这个问题上给你加分。购物车模块核心是cart_item表。加购接口的逻辑先查购物车表里有没有该用户同一个商品有则加数量没有则新增一条记录。结算时把选中的项读取出来计算出总金额返回前端前端确认后调下单接口。购物车里有个小细节容易漏商品价格是从商品表里实时带出来的而不是直接用购物车表里存的价格这样才能避免商品调价之后用户按旧价格下单。购物车在Redis里也可以用hash结构存但从实现简单和数据一致性角度我建议直接用数据库表这个场景的访问量离数据库承载上限还远得很别过度设计。3.3 订单状态机与库存扣减订单是系统中状态最多的模块也是最值得仔细设计的模块。以下是订单状态流转的核心链路待支付 - 已支付待发货 - 已发货 - 已完成 | | | | - 已取消 - 已取消仅售后场景 - 已取消为了清晰的表达代码里应该用枚举或者常量类定义订单状态。下单时核心逻辑是三步校验商品库存是否充足扣减库存生成订单主表和订单明细表清空购物车对应项这三步必须在同一个事务里完成。代码大概长这样Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, ListCartItem cartItems) { // 1. 遍历购物车选中项校验库存并计算总金额 // 2. 生成订单号创建order记录状态为待支付 // 3. 保存order_item明细 // 4. 扣减product表库存 // 5. 移除对应的cart_item }扣库存这里不引入分布式锁——单机项目用数据库的行锁就够了。简单做法是更新商品时带一个stock 购买数量的条件UPDATE product SET stock stock - #{count} WHERE id #{productId} AND stock #{count}受影响行数为0说明库存不足抛异常回滚。这个写法在单库场景下非常可靠而且没有锁等待的问题比先查后减的方式安全得多。很多教程里讲“乐观锁版本号”在库存扣减场景本质也是同一个思想。支付环节做“模拟支付”用户点击支付后直接把这个订单的状态从待支付改成已支付。演示的时候就说业务流程已经预留真实支付接口的位置实际接入只需替换对接逻辑即可。这样既完成了业务闭环又不会把自己拖进繁琐的支付SDK对接里。3.4 用户画像宠物档案的妙用宠物档案表这个模块是宠物用品系统区别于普通商城的标志。宠物主人创建宠物档案之后可以根据宠物的年龄、体重、品种推荐合适的粮食和驱虫药。这个功能从表设计上来讲不复杂就是user到pet_profile的一对多难的是怎么把档案和商品关联起来。最简单实用的做法是在商品表加一个pet_type字段比如“猫”或“狗”宠物档案里存了宠物类型用户浏览时可以直接按自己养宠的类型筛选。这个功能实现成本很低但整个系统的专业程度立刻提升一截。4. 实操中踩过的坑与排查链路4.1 金额精度、时间格式和JSON序列化金额精度这个问题遇到太多次了。Java里如果用double计算金额10.0减0.1得到的是9.9没错但某些数字组合下会出现9.899999999这样的浮点误差。原因很简单二进制无法精确表示所有十进制小数。所以数据库字段、实体类BigDecimal、前端展示三处必须保持格式一致。实体类里private BigDecimal price;运算时用total total.add(item.getPrice().multiply(new BigDecimal(item.getQuantity())));时间格式是另一个高发问题。LocalDateTime序列化到前端默认是一个很长的数组或者ISO字符串前端不好渲染。解决办法有两种在字段上添加JsonFormat注解或者在application.yml里全局配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8顺手把时区设置好否则你存进去的时间比实际少了8小时排查起来特别容易怀疑人生。4.2 重复提交与“多点一下”的订单翻倍用户点击下单按钮之后因为网络慢又点了一次结果生成了两个一模一样的订单。这是开发初期一定会遇到的场景。解决的思路是幂等性——同一个请求发多次结果只有一个订单生效。最简单可靠的做法是前端生成一个requestId下单时传给后端后端检查这个requestId是否已经存在存在则不重复处理通过给订单表加request_id唯一索引来兜底。另一个思路是前端在下单时把按钮禁用几秒但这只能缓解不能根治。用唯一索引做服务端校验才是正解。4.3 商品图片不显示一次完整的排查记录某次开发中商品图片上传后无法访问我在浏览器打开图片地址一直在404。这里复现一遍完整排查链路非常典型第一步确认上传的文件确实存在。查看配置的file.upload-dir发现文件已经写到指定目录了。那问题出在访问阶段。第二步确认静态资源映射。Spring Boot默认只映射classpath:/static/目录外部磁盘路径不在默认映射范围内。所以你需要手动配置Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceHandler(file: uploadDir); } }第三步检查URL拼接。如果数据库里存的是D:/upload/a.jpg这种绝对路径直接拼进前端URL里的src是会有问题的。正确做法是数据库存相对路径/images/a.jpg这个路径恰好对应上面的映射规则。第四步确认部署环境差异。本地Windows和服务器Linux路径分隔符和磁盘路径不一样如果配置写死在代码里换环境就崩。正确做法是把上传根目录配置在application.yml中部署时按环境改。这一套排查下来你对Spring Boot静态资源处理的理解会非常扎实。这些在官方文档里都能找到但过程远比文档曲折。4.4 分类删除后商品“失联”分类表删了但商品表的category_id还指着那个已经不存在的分类前端按分类查商品时出现空白。根因是外键关联没有被正确处理。这不是数据库级外键的锅——我建议不要用物理外键来约束而是在业务代码里做控制删除分类前先检查该分类下有没有商品有则提示不能删除或者将商品移到未分类中。习惯上用逻辑删除在分类表加deleted字段删除操作只是置为1展示时过滤掉。好处是历史订单引用的分类信息不会诡异消失数据可追溯安全性更好。5. 答辩前准备与低成本的优化方向5.1 演示环境要提前想好的事在现场演示和提交演示视频时代码能跑只是底线演示流畅才是加分项。我的经验有这几条准备一批好看的商品图片和完整的种子数据别让评委看到空空的首页演示时千万别现场删数据一旦误删就手忙脚乱视频演示提前录好一份完整的现场即使网络故障也不慌测试账号密码写在演示文档里方便评委想动手试试的时候马上能用。5.2 老师最爱追问的几个问题答辩环节老师盯着项目问的问题其实非常集中提前准备好答案就能稳住。为什么选Spring Boot而不是SSH/SSM因为自动配置减少配置成本、生态成熟、社区活跃、开发效率高。数据库为什么这样设计从核心实体出发讲清楚订单主表与明细表的拆分是为了让订单的商品快照独立存在——商品改了名字价格也不影响历史订单。库存并发如何处理在更新语句中用条件扣减用数据库行锁保证原子性比先查后写安全。缓存一致性怎么做分类和首页推荐这类低频变更数据设置短期过期时间即可缓存删除策略用更新时主动清缓存。每一个回答都要落到“你实际做了什么”上哪怕方案不复杂真实感就是说服力。5.3 低成本且有效果的扩展方向答辩中系统只能基础CRUD也能过但如果你有精力我建议按下面顺序做低成本扩展性价比从高到低排列参数校验使用Validated注解做入参校验手机号格式、库存非负、金额大于零。这个小动作代表你对代码质量的在意。全局异常处理用RestControllerAdvice统一捕获异常并返回规范结构再也不会出现一大段Java异常堆栈抛给前端。操作日志简单记录管理员的关键操作比如改价格、删除商品存入一张日志表。销售统计用GROUP BY按商品统计销量排行用ECharts画几个柱状图和折线图后台立刻像模像样。我个人的建议是与其追新追复杂不如把上面四个中的一个做到完整、可用、有细节。比如销售统计这个功能做到能按周按月筛选能展示销量Top10的图表在答辩里就是一个非常亮眼的落地案例。实际上历年看到的好项目往往不是功能多而是每个功能都能讲清楚“为什么这么做”。做这个宠物用品系统项目我最大的体会是别看它只是一个普通的Java Web题目把它从需求到表结构再到接口一路理清楚你对“系统”这两个字的理解会和只会写CRUD的人完全不同。拿着这套思路和代码去应对验收应该绰绰有余了。