毕业设计季总有人拿着“宠物用品系统”这个题目来找我聊,说白了这个题在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_time |
| category | 分类表 | id, name, parent_id, sort, icon |
| product | 商品表 | id, category_id, name, subtitle, main_image, detail, price, stock, sales, status |
| product_spec | 规格表 | id, product_id, spec_name, stock, price |
| cart_item | 购物车项 | id, user_id, product_id, quantity, checked |
| orders | 订单主表 | id, order_no, user_id, total_amount, status, receiver_name, receiver_phone, address, create_time |
| order_item | 订单明细表 | id, order_id, product_id, product_name, product_image, price, quantity |
| pet_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, List<CartItem> 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: GMT+8顺手把时区设置好,否则你存进去的时间比实际少了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的人完全不同。拿着这套思路和代码去应对验收,应该绰绰有余了。