☰
Spring Boot宠物商城网站设计与实现:从数据库设计到订单状态机实战
2026/10/2 13:59:13 网站建设 项目流程

最近在技术群里看到好几个朋友在问同一个问题:用Spring Boot做一个宠物商城网站,功能到底应该怎么设计,数据库怎么建,踩坑怎么避免。这个问题其实很有代表性——"基于Spring Boot的宠物商城网站设计与实现"这类题目,看起来是个标准的电商项目,但宠物商品和普通商品(比如衣服、数码产品)有本质区别。很多人拿着这个题目就开始写代码,写到购物车发现库存逻辑对不上,写到订单又发现状态流转混乱,最后只能推倒重来。

我前前后后参与过好几个电商类项目的设计与开发,也帮人改过不少这类毕设和实训项目的代码。今天不写那种教科书式的"项目介绍",直接把整套宠物商城网站的设计思路、核心代码结构、数据库设计细节和实际踩过的坑整理出来。内容围绕Spring Boot展开,覆盖从需求拆解到部署上线的完整链路,适合正在做毕业设计的学生,也适合想拿真实业务场景练手的初级开发者。你既可以把它当作完整项目的参考骨架,也可以只挑里面关系型数据库设计、订单状态机、权限认证这些模块来复用。

1. 把"宠物商城"四个字拆成一份可落地的功能清单

很多人在拿到题目之后的第一反应是"商城嘛,不就是商品列表加购物车加订单"。如果你这么想,后面大概率要返工。宠物商城这个命题里,真正决定项目质量的是"宠物"两个字——它带来的业务复杂度远超普通商品。

1.1 商城类系统绕不开的基础模块

先说说所有商城都有的部分,这部分是骨架,没有它项目不成立:

  • 用户模块:注册、登录(手机号+验证码或用户名+密码)、个人信息维护、收货地址管理。
  • 商品模块:分类浏览、商品列表(含分页和条件筛选)、商品详情(多图展示、价格、库存状态)。
  • 购物车模块:加入购物车、修改数量、选中结算、删除。
  • 订单模块:订单创建、订单列表(按状态筛选)、订单详情、取消订单、确认收货。
  • 管理后台:商品上下架、分类维护、订单处理(发货)、用户管理、基础数据统计。

这些模块在功能上互相依赖,但在代码层面建议完全解耦。我见过不少设计方案把商品和订单揉在一个Service里,看起来省事,实际维护起来非常痛苦。建议按照标准的单体分层结构来做:Controller只做参数校验和路由转发,Service只做业务逻辑和事务管理,Mapper/DAO层只做数据持久化。这个习惯越早养成越好。

1.2 宠物商品独有的业务场景

这才是这个项目的分水岭。宠物作为商品,有几个普通商品没有的特点:

第一,一物一码。你卖一只布偶猫,库存数量就是1,不能像卖手机一样设置库存为999。这就意味着商品表的库存字段不能单纯做加减法,还需要一个"锁定"状态——有人下单但未支付时,这只宠物不能同时被另一个人下单。

第二,信息密度极高。买一只狗或者一只猫,用户需要看的东西远不止"品牌、型号、价格"。它需要看出生日期、疫苗记录、驱虫记录、健康状态描述、父母信息甚至血统证书。这些信息不是两张表就能搞定的,需要专门设计健康档案表与基因/血统信息表。

第三,交易周期长。普通电商下单后基本就是发货、收货、完成;宠物交易可能涉及"定金 + 尾款"这种场景,或者用户在购买前会反复咨询,甚至还要视频连线看宠物状态。如果你在订单模块里拆了定金单和尾款单,会让数据对账变得很复杂,建议直接用一个订单主表加一个交易流水表来处理支付明细,而不是拆成多个订单。

第四,售后逻辑特殊。普通商品七天无理由退货,宠物不行,它有活体运输风险、健康周期等因素。所以订单状态里建议增加"售后审核"这类环节,而不是简单地在完成态下加上"退货"。

1.3 功能优先级:先走通主链路,再谈锦上添花

结合以上分析,我给这类项目做的功能优先级排序是这样的:

优先级功能模块说明
P0用户登录、商品列表/详情、购物车、订单创建、模拟支付、后台发货主交易链路,没它项目不成立
P1宠物健康档案展示、分类筛选、订单状态流转、后台商品上下架体现宠物业务特色,也是评分/答辩重点
P2地址管理、优惠券、搜索历史、数据统计图表、图片批量上传加分项,属于体验优化
P3WebSocket在线问诊、视频看宠、Spring Boot Admin监控、定时优惠活动扩展亮点,时间充裕再做

你如果真的想把这个项目做成能拿出来说的作品,P0和P1必须做得非常扎实,P2选两个做起来就行,P3属于"锦上添花"但答辩效果极好。后面我会详细展开P0和P1的关键实现。

2. 技术选型:为什么Spring Boot是这类项目的最优解

我知道肯定有人会在技术群里争论"为什么不用FastAPI""为什么不用Node.js"。技术选型这事,脱离场景谈优劣都是耍流氓。针对"宠物商城网站设计与实现"这个题目,Spring Boot就是最合理的选择,而且没有之一。原因有三点,我逐个说清楚。

2.1 Spring Boot解决了这类项目的核心痛点

商城类网站的本质是什么?是业务逻辑复杂、数据关系密集、需要稳定事务支撑的管理系统。用户在浏览商品、加购、下单、支付这一整套流程里,任何一步出现数据不一致都会引发严重问题。

Spring Boot最大的优势,是把Spring生态那套成熟的事务管理、依赖注入、面向切面编程能力,用极简的配置方式提供出来。你只需要加一个@Transactional注解,就能保证"扣库存+创建订单+生成支付流水"这三步操作要么全部成功、要么全部回滚。换成FastAPI或者Node.js,你得自己去实现分布式事务或者手工补偿逻辑,复杂度完全不是一个级别。对于做设计类项目的人来说,把精力省下来去打磨业务细节,比折腾底层事务框架要有价值得多。

另一个原因是生态成熟度。Spring Boot有极其完善的第三方集成方案:Spring Security做权限、MyBatis-Plus做数据持久化、Redis做缓存、Spring Boot Admin做监控、WebSocket做实时通信,全部都有成熟稳定的Starter,配置非常统一。这些能力对宠物商城来说几乎全是刚需。

2.2 版本选择:别盲目追求最新版

在帮人检查代码的过程中,我发现很多人一上来就选最新的Spring Boot 3.x,然后折腾JDK 17的兼容性,又踩一遍Jakarta命名空间迁移的坑。我的建议非常明确:

使用Spring Boot 2.6.x + JDK 8/11 + MyBatis-Plus 3.5.x。这个组合是经过大量生产环境验证的,文档齐全,网上遇到问题时能搜到的解决方案也最多。2.6.x相比2.3.x加入了路径匹配策略的调整,默认禁止了*匹配所有路径,如果从老项目迁移需要留意spring.mvc.pathmatch.matching-strategy配置。

如果你确实想用Spring Boot 3.x,那就要做好两个心理准备:第一,JDK必须17以上;第二,原来的javax.*包名全部要改成jakarta.*。这不是不能做,但对一个以"设计和实现"为核心目标的项目来说,完全没有必要把时间花在环境适配上面。

2.3 ORM选型与配套中间件

数据库访问层建议直接用MyBatis-Plus。理由很简单:单表CRUD它帮你写完了,复杂查询你还能手写XML控制SQL。宠物商城的查询场景非常杂——前端列表要根据品种、年龄、价格区间、是否已售等多个条件动态组合,用MyBatis-Plus的LambdaQueryWrapper可以非常优雅地动态拼接条件,而不需要写一堆if判断去拼接字符串SQL。

中间件方面,Redis在这个项目里是有实际价值的:商品详情页的访问热点集中,把商品基本信息缓存到Redis可以显著降低数据库压力;购物车也可以用Redis实现,以用户ID作为key,商品ID作为field,用Hash结构存储,性能远好于关系型数据库表。不过要注意,缓存一定要设置过期时间和主动更新策略,否则商品价格改了,用户看到的还是旧数据。

另外一个容易被忽略的配套工具是Spring Boot Admin,它能把项目的运行状态(内存、线程、健康检查、请求映射)可视化展示出来。在项目的答辩环节,你打开监控面板展示平稳运行的指标,比说一百句"系统稳定"都有说服力。

3. 数据库设计:宠物商品的信息密度比普通商品高一个量级

数据库设计是这类项目的灵魂,也是很多人卡壳最久的地方。我见过太多人把宠物商城设计得像一个简单的二手交易平台——一张商品表,一个价格字段,一个库存字段,然后就没有然后了。这种设计表面上看能跑,实际上完全体现不出"宠物商城"的业务内涵。

3.1 核心表结构设计

我实际推荐的表结构如下(去掉了一些非核心字段,保留主干):

用户表t_user:id、username、password(BCrypt加密)、phone、avatar、create_time。

宠物分类表t_pet_category:id、category_name(猫/狗/水族/小宠)、parent_id(支持二级分类,比如猫下面分英短/美短/布偶)、sort_order(排序权重)。

宠物商品表t_pet:这个表是核心,字段需要认真设计。除了常规的id、category_id、title、cover_image、price、original_price、banner_images、status(上架/下架/已售)、create_time、update_time之外,建议加上这几个关键字段:

  • pet_type:区分是活体宠物还是宠物用品,因为商城经常混卖宠物猫粮、玩具、笼子等周边商品。
  • gender:公/母。买猫买狗的人普遍在意性别。
  • birth_date:出生日期,用于计算年龄展示"2个月零10天"这种效果。
  • inventory_status:库存状态字段(0=未锁定/1=已锁定),用于一物一码的宠物商品。
  • view_count:浏览量,用于"人气宠物"推荐。

宠物档案表t_pet_profile:关联t_pet表的主键ID,字段包括vaccine_record(疫苗记录,用JSON数组存储,比如["第一针猫三联","第二针猫三联","狂犬疫苗"])、deworm_record(驱虫记录,文本描述)、health_status(健康状态描述,活泼好动/轻微软便等)、certificate_no(血统证编号,没有可留空)、parent_info(父母信息,JSON格式存储父母品种和照片链接)。

购物车表t_cart:id、user_id、pet_id、quantity、checked(是否选中结算)、create_time。

订单主表t_order:id、order_no(唯一订单号)、user_id、total_amount、pay_amount(实付金额)、pay_type(支付方式)、status(订单状态)、receiver_name、receiver_phone、receiver_address、remark(买家留言)、pay_time、ship_time、finish_time、cancel_time、create_time。

订单明细表t_order_item:id、order_id、pet_id、pet_title、pet_image、price(下单时快照价格)、quantity。明细表必须冗余商品名称和图片,因为商品可能后续被修改或者下架,但订单历史必须保持当时的信息快照。

支付流水表t_pay_transaction:id、order_no、transaction_no(模拟支付平台流水号)、amount、status、callback_time(模拟支付回调时间)。

3.2 宠物档案表为什么不用JSON字段一把梭

有人可能会说:疫苗记录、驱虫记录、父母信息直接用JSON字段存在t_pet表里不就行了?技术上确实可以,但我不建议这么做。

原因在于管理后台有独立的档案编辑页面,疫苗记录可能要支持逐条新增、删除、修改,还要记录每次操作的时间。如果把JSON存成一个整体字符串,每次修改都得把整个JSON读出来、反序列化、改完再序列化写回去。逻辑复杂不说,还容易出现并发覆盖的问题。拆成独立的关联表(或者分包成子表),后续无论是展示、统计(比如"所有打过狂犬疫苗的宠物")还是扩展,都会灵活很多。

如果你是抱着"简化设计"的心态,那退一步的方案是:t_pet_profile主表 +t_pet_vaccine子表。主表存健康状态和证书信息,子表专门存疫苗记录,每条记录包含名称和接种时间。这个设计既不复杂,又能支撑答辩时被追问"如何查询所有疫苗齐全的宠物"这类问题。

3.3 订单与库存的关联设计——宠物商城最容易翻车的地方

普通商品的库存扣减逻辑是:下单时UPDATE t_sku SET stock = stock - 1 WHERE id = ? AND stock > 0,用乐观锁保证不超卖。宠物商品因为一物一码的天然属性,处理方式完全不同。

我的方案是给宠物商品表加一个inventory_status字段,同时配合订单状态做"三段式锁定":

  1. 用户创建订单时,将inventory_status从0改为1,并在订单表记录锁定时间。如果用户超过30分钟未支付,定时任务自动将订单置为取消状态,同时把inventory_status改回0。
  2. 用户支付成功后,订单进入待发货状态,宠物继续保持锁定。
  3. 后台发货后,订单进入已发货状态,此时才允许把宠物商品标记为"已售"(status字段改为2),inventory_status可以保留锁定状态到订单完成。

这个设计避免了"扣库存和订单状态不一致"的问题。你在微服务架构里可以把它做成独立的库存服务,但对于单体应用来说,一张表加一个状态字段完全够用。

4. 核心交易链路:从商品浏览到订单支付的完整实现

功能清单和数据模型确定之后,接下来就是最核心的交易链路实现。这一段我按照前端用户实际操作的顺序来讲。

4.1 商品查询:动态条件筛选与缓存策略

宠物商城首页和列表页通常需要支持多条件组合筛选。用户在页面上可能同时选择"猫"、"2-4个月"、"预算2000-5000"、"已打第一针疫苗"这些条件。如果用传统的SQL拼接,代码会非常冗长且难以维护。

MyBatis-Plus的LambdaQueryWrapper非常适合这个场景。核心代码逻辑如下:

public Page<Pet> queryPetList(PetQueryDTO dto) { LambdaQueryWrapper<Pet> wrapper = new LambdaQueryWrapper<>(); // 按分类筛选 wrapper.eq(dto.getCategoryId() != null, Pet::getCategoryId, dto.getCategoryId()); // 按性别筛选 wrapper.eq(StringUtils.hasText(dto.getGender()), Pet::getGender, dto.getGender()); // 按价格区间筛选 wrapper.between(dto.getMinPrice() != null && dto.getMaxPrice() != null, Pet::getPrice, dto.getMinPrice(), dto.getMaxPrice()); // 按年龄筛选:根据出生日期动态计算 if (dto.getMinMonth() != null || dto.getMaxMonth() != null) { LocalDate now = LocalDate.now(); if (dto.getMaxMonth() != null) { wrapper.ge(Pet::getBirthDate, now.minusMonths(dto.getMaxMonth())); } if (dto.getMinMonth() != null) { wrapper.le(Pet::getBirthDate, now.minusMonths(dto.getMinMonth())); } } // 只查上架状态的商品 wrapper.eq(Pet::getStatus, PetStatus.ON_SALE.getCode()); wrapper.orderByDesc(Pet::getCreateTime); return petMapper.selectPage(new Page<>(dto.getPageNum(), dto.getPageSize()), wrapper); }

这里面有一个非常关键的细节:年龄筛选不要存"年龄"字段,而是存birth_date然后动态换算。因为宠物的年龄是实时变化的,存龄字段需要定时更新,早晚会出数据不一致的问题。用出生日期动态计算是唯一正确的做法。

商品详情页的数据建议做两级缓存。一级是Redis,key设计为pet:detail:{id},value存储JSON序列化后的商品详情(包含宠物档案);二级是本地应用缓存(Caffeine)。读取顺序:本地缓存 → Redis → 数据库,数据库查询回来后反填到缓存。缓存过期时间设为30分钟比较合适,既不会让数据过于陈旧,也不会频繁过期导致缓存穿透。后台编辑商品信息时,同步删除对应的Redis缓存。

4.2 购物车:数据库表方案与Redis方案的取舍

购物车的实现有两种主流方案,我来说说各自的适用场景。

数据库表实现:设计t_cart表,用户加购、勾选、删除都走CRUD。优点是对账清晰,重启数据不丢;缺点是每一次加购都要写数据库,压力较大。

Redis Hash实现:用cart:{userId}作为key,petId作为field,商品数量等信息作为value。优点是性能极高、代码简单;缺点是需要处理缓存过期和持久化问题。

宠物商城这个场景,我推荐数据库表方案。原因非常务实:宠物商城不是高并发秒杀系统,用户加购频率很低,数据库完全抗得住,而且订单创建时需要根据购物车记录联动查询商品信息和库存状态,走数据库天然具备事务一致性。Redis方案在毕业设计答辩现场还会面临"redis宕机怎么办"的灵魂拷问,数据库表方案没有这种问题。

购物车的核心操作里,有一个容易被忽略的校验:加入购物车时必须校验商品的status是否为上架状态、inventory_status是否未被锁定。否则用户把一只已经被人下单但未支付的宠物加进购物车,结算时才发现不能提交订单,体验非常差。我的做法是在加入购物车的Service方法里直接查一次商品表,不满足条件直接抛业务异常,由全局异常处理器转为友好提示返回给前端。

4.3 订单创建:事务、状态机与幂等性

订单创建是交易链路里最复杂的一步,涉及用户校验、购物车数据读取、商品状态校验、库存锁定、订单主表和明细表写入、购物车清理。这一整串操作必须放在同一个事务里,任何一步失败都整体回滚。

我建议的Service层核心代码结构如下:

@Transactional(rollbackFor = Exception.class) public OrderCreateResult createOrder(OrderCreateDTO dto) { // 1. 校验用户地址信息 UserAddress address = addressService.getValidAddress(dto.getAddressId(), dto.getUserId()); // 2. 读取购物车中勾选的商品 List<CartItem> checkedItems = cartService.getCheckedItems(dto.getUserId()); if (checkedItems.isEmpty()) { throw new BizException("请先选择要结算的商品"); } // 3. 校验商品状态并计算总价 List<OrderItem> orderItems = new ArrayList<>(); BigDecimal totalAmount = BigDecimal.ZERO; for (CartItem item : checkedItems) { Pet pet = petService.getById(item.getPetId()); if (pet == null || pet.getStatus() != PetStatus.ON_SALE.getCode()) { throw new BizException("商品已下架:" + pet.getTitle()); } if (pet.getInventoryStatus() == 1) { throw new BizException("该宠物已被其他用户锁定,请重新选择"); } // 4. 锁定宠物库存 petService.lockInventory(pet.getId()); // 组装订单明细快照 OrderItem orderItem = buildOrderItem(pet, item.getQuantity()); orderItems.add(orderItem); totalAmount = totalAmount.add(orderItem.getPrice().multiply( BigDecimal.valueOf(orderItem.getQuantity()))); } // 5. 生成订单主表 Order order = createOrderMaster(dto.getUserId(), dto.getAddressId(), totalAmount, orderItems); // 6. 生成订单编号,格式:时间戳 + 用户ID + 随机数 order.setOrderNo(generateOrderNo(dto.getUserId())); // 7. 清理购物车已结算项 cartService.removeCheckedItems(dto.getUserId()); return new OrderCreateResult(order.getOrderNo(), order.getTotalAmount()); }

这段代码里有几个值得展开讲的关键点:

幂等性:用户可能因为网络原因重复提交同一个创建订单请求,导致生成两笔相同内容的订单。处理方案有两种:一是前端在提交后立刻禁用按钮,这是体验层的兜底;二是后端在订单表增加一个source_token唯一字段,前端提交时生成一个UUID放入请求头,数据库层面保证唯一约束,重复请求直接报重复异常。

订单号生成:不要用数据库自增ID当订单号,那个会暴露平台的订单量,而且长度不够随机。推荐格式:年月日时分秒 + 用户ID后四位 + 随机四位数字。例如2025060710381500012345。如果遇到并发,加随机四位后基本不会冲突,即使冲突,数据库捕获唯一索引异常后重试即可。

金额计算:所有涉及金额的字段都用BigDecimal,严禁使用double或float。宠物商城可能会出现"定金 + 尾款"这类精确金额计算场景,用浮点数会出现0.1+0.2=0.30000000000000004这种经典问题,直接导致对账不平。

4.4 订单状态流转与模拟支付

订单状态设计我推荐用枚举类维护,代码里禁止散落魔法数字:

public enum OrderStatus { UNPAID(0, "待支付"), PAID(1, "待发货"), SHIPPED(2, "已发货"), COMPLETED(3, "已完成"), CANCELED(-1, "已取消"), REFUNDING(4, "售后中"), REFUNDED(5, "已退款"); private final int code; private final String desc; // getter... }

合法的状态流转路径:

  • UNPAID→PAID:用户支付成功,回调后更新。
  • UNPAID→CANCELED:用户主动取消,或定时任务30分钟未支付自动取消。
  • PAID→SHIPPED:后台管理员发货,需要填入物流单号。
  • SHIPPED→COMPLETED:用户确认收货,或发货后7天系统自动确认。
  • PAID/SHIPPED→REFUNDING→REFUNDED:用户发起售后(简化版)。

实现层面对非法的状态流转,直接用状态机校验或者在Service方法内检查if (order.getStatus() != OrderStatus.UNPAID.getCode())抛异常即可。不要过度设计状态机框架,这个项目的复杂度用简单的if判断完全够。

关于支付模块,我的建议是对接一个模拟支付页面,不要接入真实第三方支付。原因很简单:真实支付需要商户资质,而且涉及资金合规问题。模拟支付的实现路径是:用户点击支付→前端跳转到模拟支付页(显示应付金额)→用户点击"确认支付"→后端调用模拟支付Service生成一笔t_pay_transaction流水,将订单状态从UNPAID改为PAID,同时更新支付时间。

如果你想让答辩更有亮点,可以模拟真实支付的"回调"逻辑:支付成功后,通过Spring的事件发布机制(ApplicationEventPublisher)触发一个异步任务,模拟支付平台回调更新订单并推送用户通知。这样既体现了对真实支付流程的理解,又不需要接入任何外部系统。

5. 会员体系、后台管理与权限控制

一个完整的商城网站必须有前后台两套逻辑。前台面向用户,后台面向管理员。如果前后台混在一个入口,权限会很难控制。我的方案是拆成两个模块:pet-admin(管理后台)和pet-web(用户端商城),但共享同一个数据库和Service层。

5.1 基于JWT的登录认证

用户端的登录认证,我推荐用JWT而非传统的Session。原因很简单:Session需要服务端存储,JWT是无状态的,非常适合前后端分离的部署场景。JWT令牌的生成使用jjwt库,核心用法如下:

// 生成token,有效期设置为2小时 long expiration = 2 * 60 * 60 * 1000; String token = Jwts.builder() .setSubject(userId.toString()) .claim("username", user.getUsername()) .claim("role", "USER") .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expiration)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();

这里有一个非常容易踩的坑:jjwt 0.9.1和0.11.5的API差别非常大。0.9.1版本用Jwts.builder().signWith(SignatureAlgorithm.HS256, secretKey),而0.11.5版本要求Jwts.builder().signWith(secretKey)且密钥长度必须大于256位。很多人在整合Spring Security时看到报错WeakKeyException,就是因为密钥长度不满足要求。解决方法是生成一个Base64编码后的32字节以上密钥,或者用Keys.secretKeyFor(SignatureAlgorithm.HS256)自动生成。这里我实际推荐使用0.11.5版本,因为0.9.1已经停止维护,存在安全漏洞。

认证流程是:用户登录成功后返回token,前端存储在localStorage(或内存变量)中,每次请求在Authorization请求头携带Bearer token。后端写一个JwtAuthenticationFilter继承OncePerRequestFilter,拦截所有/api/**请求,解析token后把用户信息放入SecurityContextHolder。密码存储一律使用BCryptPasswordEncoder,绝不存明文。

5.2 管理后台:分类管理与商品上下架

后台管理端的核心是让运营人员能维护整个商品目录。这里我讲讲分类管理的最佳实践。分类表我设计了parent_id支持自关联,前台展示时需要把二级分类组合成树形结构。推荐的实现方式是查询所有分类后,在内存中组装成树,而不是在SQL里做递归查询。数据量不大时,内存组装性能极高且代码清晰。

public List<CategoryTreeNode> buildCategoryTree() { List<PetCategory> allCategories = categoryMapper.selectList(null); Map<Long, List<PetCategory>> categoryMap = allCategories.stream() .collect(Collectors.groupingBy(PetCategory::getParentId)); // 根节点 pid 为 0 return buildChildren(0L, categoryMap); }

商品上下架功能存在一个业务陷阱:商品下架时,用户购物车里已经添加了该宠物怎么办?如果直接允许下架,用户结算时会因为商品不存在而报错;如果禁止下架,运营又无法管理。"已售"状态的宠物商品必须自动下架,这是通过后台发货操作联动实现的。而下架操作时,建议同时清理该商品在Redis中的缓存,且在下架校验中提示"该商品已被N个用户加入购物车,是否确认下架",让运营做出判断。

5.3 订单提醒、定时任务与基础统计

后台订单管理的核心痛点是"待发货订单漏发货"。这里我引入了一个简单实用的定时任务方案:用Spring自带的@Scheduled注解,每5分钟扫描一次超过2小时仍未发货的已支付订单,通过站内信或者短信平台(可以对接阿里云短信,也可以直接对接一个模拟的日志通知)提醒管理员。

@Component public class OrderRemindTask { @Scheduled(cron = "0 */5 * * * ?") public void remindPendingShipOrders() { List<Order> pendingOrders = orderMapper.selectList( new LambdaQueryWrapper<Order>() .eq(Order::getStatus, OrderStatus.PAID.getCode()) .lt(Order::getPayTime, LocalDateTime.now().minusHours(2))); pendingOrders.forEach(order -> { // 发送提醒通知,异步处理,不要在定时任务里做耗时操作 asyncNotifier.notifyAdmin("有订单超过2小时未发货:" + order.getOrderNo()); }); } }

定时任务还有一个关键业务:处理超过30分钟未支付的订单并释放宠物库存。这个功能必须在前台抢购场景下才有意义。我建议使用@Scheduled每1分钟执行一次,扫描订单表里status=0且create_time小于当前时间30分钟的订单,将其置为取消状态并将对应宠物的inventory_status改为0。

基础统计方面,不需要引入复杂的报表工具。直接在后台首页用几个卡片展示:今日新增用户数、今日订单数、今日销售额、待发货订单数,再配合一个近7日销售额折线图(前端使用ECharts绘制,后端提供聚合查询接口)就足够了。这类聚合查询不需要实时计算,可以每天晚上跑一个定时任务把统计结果写入统计表,后台直接查统计表,响应速度会非常快。

6. 部署上线与踩坑记录

项目开发完成不等于项目结束,部署上线才是真正检验代码质量的时刻。这一节我来分享整个过程中最容易踩的坑,以及对应的解决方案。

6.1 环境准备与打包部署

先说基础环境。生产服务器建议使用2核4G的云服务器,安装JDK 8或11、MySQL 5.7或8.0、Redis 6.x、Nginx。打包命令很简单:

mvn clean package -DskipTests

打包后会在target/目录下生成pet-mall-0.0.1-SNAPSHOT.jar。启动命令建议使用nohup配合-Xms和-Xmx参数,避免JVM动态申请堆内存导致性能抖动:

nohup java -jar -Xms512m -Xmx512m -Dspring.profiles.active=prod \ pet-mall-0.0.1-SNAPSHOT.jar > /app/logs/pet-mall.log 2>&1 &

这里说一下application.yml的Profile设计,我习惯拆成三个文件:

  • application.yml:公共配置,比如应用名、端口。
  • application-dev.yml:开发环境配置,数据库地址本地,日志级别DEBUG,打印SQL。
  • application-prod.yml:生产环境配置,数据库地址云服务器,日志级别INFO,关闭SQL打印。

生产环境务必开启spring.jpa.show-sql=false或者mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl关闭SQL打印,否则日志文件会在一夜之间暴涨到几个GB。这个坑我真实踩过,血泪教训。

6.2 我实际遇到过的三个典型问题

问题一:MyBatis-Plus分页插件不生效

症状是selectPage返回的数据条数正确,但total始终为0,或者分页根本没生效。原因是MyBatis-Plus 3.5.x版本必须显式配置分页插件,在配置类里加入:

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

如果你用老版本的MyBatis-Plus(3.4.x之前),配置的是PaginationInterceptor,类名不一样,直接迁移会编译报错。另外注意DbType一定要和数据库对应,否则生成的方言SQL可能不兼容。

问题二:JSON序列化导致LocalDateTime格式变成一串数字

宠物健康档案里存了疫苗接种时间的LocalDateTime字段,未做序列化处理时,前端拿到的数据是类似于"birthTime": 1716800000000的时间戳数字。解决方案是在application.yml配置全局的JackSon序列化规则:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

同时给实体类的LocalDateTime字段加上@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解。注意time-zone必须设置为GMT+8,否则传回前端的时间会差8个小时。

问题三:文件上传大小限制

宠物商城需要上传多张商品图片,如果前端上传大图,会直接报MaxUploadSizeExceededException。Spring Boot默认上传文件大小上限只有1MB。需要手动调整:

spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB

图片存储方面,不建议把图片直接存到数据库BLOB字段,更推荐的方式是上传到服务器本地目录(或对象存储服务),数据库只存访问URL。本地存储需要配置一个静态资源映射,把/images/**映射到服务器磁盘目录:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/images/**") .addResourceHandler("file:/app/upload/"); } }

6.3 这个项目后续可以怎样扩展

如果你做完以上内容还有余力,我建议从三个方向做扩展,这三个方向在答辩或者面试时都能成为加分话题。

第一个是WebSocket实时通信。宠物商城非常适合"在线看宠"这个场景。买家在下单之前想视频看猫的状态,运营在后台发起一个视频会话,前端WebSocket就能接收到会话链接。Spring Boot集成WebSocket并不复杂,配置一个WebSocketConfigurer注册WebSocketHandler即可,核心要点是握手阶段通过token完成身份认证,以及心跳检测维持连接。

第二个是对接Spring Boot Admin做实时监控。在pom.xml引入spring-boot-admin-starter-server和spring-boot-admin-starter-client,就能以可视化的方式看到项目的堆内存、线程状态、HTTP接口耗时等信息。这些数据在演示系统健壮性时非常有说服力。

第三个是第三方接口集成。如果你希望项目更像一个真实可运营的系统,可以考虑对接真实的物流查询API(比如快递鸟、快递100)实现订单物流轨迹展示,或者对接阿里云短信服务发送发货通知。这个扩展的核心在于学会"第三方接口外置"的设计思路——将外部接口调用封装成独立的Service,通过接口定义隔离外部依赖,这样即使第三方服务不可用也不会阻塞主业务流程。

做这类项目最大的体会是:一个看起来简单的商城网站,真正深入进去会发现每一个环节都有设计决策要做。Spring Boot的价值在于它帮你把框架层的复杂度降到最低,让你能够把精力集中在真正的业务难点上——这正是我从这个项目里收获最大的地方。先把主交易链路做扎实,再逐层叠加亮点功能,你会发现这个题目能给到你的成长远比想象中多。

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

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

立即咨询