网上讲宠物寄养系统的教程不算少,但大多数代码例程到“生成订单”就停了,后面的退租、结算、评价基本靠线下想象。真正把“寄养”和“退租”做成一个完整闭环、同时适合当计算机毕设或简历项目参考的,反而少见。这篇来拆一个基于SpringBoot的宠物家庭式寄养系统——从需求分析到数据表设计,从寄养下单到退租结算,把整条业务线讲透。无论你是正在准备毕设的Java后端学习者,还是想做一个C2C平台类项目练手,这篇都能直接照着搭。
1. 这个系统到底解决什么问题:宠物寄养的业务场景与需求拆解
1.1 宠物寄养的痛点:为什么是“家庭式”而不是宠物店
传统宠物店寄养的主要问题是环境陌生、猫狗集中、可能会产生应激反应。对不少宠物主人来说,让宠物在类似家庭的环境里呆几天,比关在笼子里踏实得多。家庭式寄养本质上是一种C2C模式:有空闲时间和空间的家庭腾出一块区域接收宠物,平台在中间做撮合和信任背书。
这个场景放到系统里,就产生了几类核心角色:
- 宠物主人:找寄养家庭、下单、退租、评价。
- 寄养家庭:申请入驻、维护可接单状态、接单、确认退租。
- 平台管理员:审核寄养家庭入驻、监管订单、处理异常。
不少学生一上来就把它理解成“宠物店的订单管理系统”,只做简单的增删改查。这是个方向性错误。家庭式寄养的关键是“人”和“信任”,不是“货”和“库存”。这一点会在后面的权限、审核、评价模块反复体现。
1.2 标题里“寄养与退租一体化”到底指什么
很多毕设项目把重心放在“下单”上,订单状态做到“寄养中”就结束了,退租变成线下行为,系统里没有记录。所谓“一体化”,是指从用户提交寄养订单开始,一直到宠物归还、押金退回、订单关闭,整个过程都能在系统内闭环完成。
拆开来看,两个动作缺一不可:
- 寄养:选家庭、选宠物、选时间、算价格、支付、接单、进入寄养状态。
- 退租:用户发起退租、系统到期自动提醒、家庭确认归还、逾期费用计算、押金结算、评价。
其中退租是很多初学项目最容易做含糊的部分。因为退租不是简单把订单状态改成“已完成”,它牵扯到时间计算、扣费规则、结算明细,这些恰恰是答辩时能跟老师讲清楚“业务逻辑完整”的加分点。
1.3 一个贴近毕设的MVP需求清单
如果从零开始做,我建议只保留下面这些功能,先保证闭环,不急着堆功能。
| 端侧 | 功能模块 | 说明 |
|---|---|---|
| 用户端 | 注册登录、宠物档案 | 每种宠物需要单独的档案,退租和评价都围绕宠物展开 |
| 用户端 | 寄养家庭列表、下单支付 | 列表按评分、距离、可接单数量排序 |
| 用户端 | 订单查询、申请退租、评价 | 订单状态实时可见 |
| 寄养端 | 入驻申请、家庭信息维护 | 需要管理员审核 |
| 寄养端 | 接单、确认退租、查看结算 | 接单后订单才进入真正的“寄养中” |
| 管理端 | 寄养家庭审核、订单监控、用户管理 | 审核通过后才能出现在用户端列表 |
这个MVP清单已经足够支撑一篇合格的毕设论文和演示:业务线完整、角色清晰、状态流转有复杂度但又不过度。后面所有技术设计都围绕这几项展开。
2. 技术选型逻辑:宁可“标准”也不要“炫技”
2.1 为什么SpringBoot几乎是这类题目的标准答案
看到一个“Java毕设宠物寄养系统”的标题,第一反应大概率就是SpringBoot。原因很直接:SpringBoot的自动配置和起步依赖把SSH时代一堆繁琐的XML配置全部抹平了,新手拿到代码就能跑;内嵌Tomcat让部署变成一条java -jar命令,演示的时候不用经历服务器配置翻车的尴尬。
更重要的是,SpringBoot生态足够标准,答辩时每个环节都有现成的成熟说法。比如自动装配、依赖注入、事务管理,这些基础知识老师一听就知道你会不会。用冷门框架虽然显得有个性,但风险是老师不熟悉,很多设计决策解释起来费劲。毕设不是创新大赛,稳定、可解释、业务完整,比技术冷门重要得多。
2.2 配套选型的“最小但完整”清单
我给这套系统的推荐选型如下:
| 层次 | 选型 | 理由 |
|---|---|---|
| 后端框架 | SpringBoot 2.7.x | 稳定,资料多,避免3.x某些变化 |
| ORM | MyBatis-Plus | 单表CRUD基本不用写SQL,自带分页和乐观锁插件 |
| 数据库 | MySQL 8.0 | 状态字段、时间字段处理方便 |
| 权限框架 | Spring Security 或 Sa-Token | 角色型权限够用,Sa-Token对新手更友好 |
| 前端 | Vue 3 + Element Plus | 配合管理端页面,组件齐全,视觉效果加分 |
| 缓存/分布式锁 | Redis(可选) | 用在下单防并发重复,属于亮点点缀 |
如果前端不熟,也可以用Thymeleaf模板渲染。但B/S类毕设通常管理端页面较多,用Vue写起来更省力,视觉效果也更好。这里我给一个个人偏好:不要为了“技术完整”强行引入消息队列、微服务、分布式事务。一个宠物寄养系统根本没有这种并发量,堆一堆中间件只会让系统变重,答辩时反而被追问到答不上来。
2.3 为什么“克制”本身也是一种设计
我见过有同学把寄养系统做成订单服务、家庭服务、用户服务三个微服务,还上了Nacos和OpenFeign。看起来技术栈很漂亮,但演示时服务一停全崩,排查问题要开三个终端。毕设项目的评审重点永远是“你能不能讲清楚自己的业务”,而不是“你用了多少中间件”。把SpringBoot单应用做好,把事务边界、状态机、定时任务做明白,这套系统已经能超出大多数同题目的平均水平。
3. 数据库设计:三张核心表和一张结算表怎么撑起整个平台
3.1 主表结构:用户、宠物、寄养家庭
数据库设计是这个项目的地基,我建议表名都用t_前缀,清晰且统一。核心表就三张:
CREATE TABLE `t_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `nickname` varchar(50) NOT NULL, `phone` varchar(11) NOT NULL, `user_type` tinyint(4) DEFAULT '1' COMMENT '1-普通用户 2-寄养家庭 3-管理员', `audit_status` tinyint(4) DEFAULT '0' COMMENT '0-未申请 1-审核中 2-通过 3-拒绝', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) COMMENT='用户表';user_type我建议直接放在用户表里,原因是这个项目里“寄养家庭”并不是一个独立的注册实体,而是普通用户申请入驻后变身为寄养角色。audit_status专门用来标识寄养家庭入驻审核状态,普通用户那边用不到。
宠物档案表:
CREATE TABLE `t_pet` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `owner_id` bigint(20) NOT NULL COMMENT '主人,关联t_user', `pet_name` varchar(30) NOT NULL, `pet_type` tinyint(4) DEFAULT '1' COMMENT '1-狗 2-猫 3-其他', `breed` varchar(50) DEFAULT '' COMMENT '品种', `age` int(11) DEFAULT NULL COMMENT '月龄', `health_status` varchar(255) DEFAULT '' COMMENT '疫苗/驱虫情况', PRIMARY KEY (`id`) ) COMMENT='宠物档案表';寄养家庭表:
CREATE TABLE `t_home` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '属于哪个用户,关联t_user', `home_name` varchar(50) NOT NULL COMMENT '家庭名称', `address` varchar(255) NOT NULL, `latitude` decimal(10,6) DEFAULT NULL, `longitude` decimal(10,6) DEFAULT NULL, `max_pets` int(11) DEFAULT '1' COMMENT '最多可同时接收宠物数', `current_pets` int(11) DEFAULT '0', `service_desc` text COMMENT '环境照片/服务描述', `avg_score` decimal(2,1) DEFAULT '5.0', `status` tinyint(4) DEFAULT '0' COMMENT '0-停用 1-启用', PRIMARY KEY (`id`) ) COMMENT='寄养家庭表';经纬度字段是为了支持“按距离排序”的展示逻辑。毕设不需要接地图SDK,只需要能算两个坐标之间的直线距离,在SQL里用Haversine公式或直接用Math.sqrt近似排序都行。
3.2 订单表的状态设计:一个状态机解决所有边界
订单表是核心中的核心,它的状态字段决定了整个系统的流转逻辑:
CREATE TABLE `t_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` bigint(20) NOT NULL, `pet_id` bigint(20) NOT NULL, `home_id` bigint(20) NOT NULL, `start_date` date NOT NULL, `end_date` date NOT NULL, `unit_price` decimal(10,2) NOT NULL COMMENT '寄养日单价', `total_amount` decimal(10,2) NOT NULL COMMENT '寄养费+增值服务费', `deposit_amount` decimal(10,2) NOT NULL DEFAULT '300.00' COMMENT '押金', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0-待支付 1-待接单 2-寄养中 3-待退租 4-已完成 5-已取消 6-已退款', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) COMMENT='寄养订单表';状态流转严格按照下面的方向走:
- 正向:0待支付 -> 1待接单 -> 2寄养中 -> 3待退租 -> 4已完成。
- 取消:0待支付或1待接单时可以取消到5已取消。
- 退款:异常情况下从2或3可以走到6已退款。
为什么要单独设“待退租”这个中间态?因为退租不是瞬间完成的。用户发起退租后,寄养家庭还要确认宠物已经安全归还,才算真正结束。如果直接把订单从“寄养中”改成“已完成”,那么退租时间、逾期天数、结算明细这些数据就无处安放,整个退租逻辑就塌了。
3.3 退租结算表:为什么不能把扣费直接写死在订单里
我在不少项目里看到有人把退租扣费金额直接加到订单表的某个字段上。这样做不是不行,但很不利于演示和追溯。我更推荐单独建一张结算表:
CREATE TABLE `t_settlement` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_id` bigint(20) NOT NULL, `actual_return_time` date NOT NULL COMMENT '实际上归还时间', `overdue_days` int(11) DEFAULT '0' COMMENT '逾期天数', `overdue_fee` decimal(10,2) DEFAULT '0.00' COMMENT '逾期扣费', `damage_fee` decimal(10,2) DEFAULT '0.00' COMMENT '损坏赔偿', `refund_amount` decimal(10,2) NOT NULL COMMENT '实际退款金额', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) COMMENT='退租结算表';这个设计的好处是:每个订单都能对应一条独立结算记录,原始金额和扣减明细都在。演示的时候点开一个已退租订单,页面能同时展示订单信息和结算明细,逻辑闭环一眼就能看出来。而且后续如果要加“押金是否原路退回”“管理员是否修改扣费金额”这些功能,也不会动到订单主表。
4. 寄养下单的完整链路:从选择家庭到订单状态机的代码实现
4.1 下单流程与费用计算规则
下单流程从用户视角看很简单:打开家庭列表 -> 选日期 -> 选宠物 -> 选增值服务 -> 支付。但到代码层面,需要在同一个事务里完成校验、算价、生成订单三个动作。
费用计算规则建议这样约定:
- 寄养费用 = 寄养天数 × 家庭日单价。
- 寄养天数 =
ChronoUnit.DAYS.between(startDate, endDate) + 1,也就是说开始当天也算一天。这个约定要在前端和文档里写清楚,否则用户会疑惑为什么1月1日到1月2日收了两天钱。 - 增值服务费 = 每一项服务单独计费后累加,比如加价洗澡、每日遛狗。
- 押金固定300元,退租时按规则扣除。
核心代码如下:
@Service public class OrderServiceImpl implements OrderService { @Override @Transactional(rollbackFor = Exception.class) public Order createOrder(CreateOrderRequest request) { // 1. 校验家庭可接单状态 Home home = homeMapper.selectById(request.getHomeId()); if (home == null || home.getStatus() != 1) { throw new BizException("寄养家庭不存在或已停运"); } if (home.getCurrentPets() >= home.getMaxPets()) { throw new BizException("该家庭当前可接收宠物数已满"); } // 2. 校验日期,开始时间不能晚于结束时间 long days = ChronoUnit.DAYS.between(request.getStartDate(), request.getEndDate()) + 1; if (days <= 0) { throw new BizException("寄养日期不合法"); } // 3. 算价 BigDecimal careFee = request.getUnitPrice().multiply(BigDecimal.valueOf(days)); BigDecimal extraFee = request.getExtraFee() == null ? BigDecimal.ZERO : request.getExtraFee(); BigDecimal deposit = new BigDecimal("300.00"); // 4. 生成订单 Order order = new Order(); order.setOrderNo(OrderNoGenerator.generate()); order.setUserId(request.getUserId()); order.setPetId(request.getPetId()); order.setHomeId(request.getHomeId()); order.setStartDate(request.getStartDate()); order.setEndDate(request.getEndDate()); order.setTotalAmount(careFee.add(extraFee)); order.setDepositAmount(deposit); order.setStatus(OrderStatus.WAIT_PAY.getCode()); orderMapper.insert(order); // 5. 预占家庭名额,防止反复下单 homeMapper.increaseCurrentPets(request.getHomeId()); return order; } }注意最后一步increaseCurrentPets,这是非常容易被忽略的点。如果不做这个预占,就会出现一个家庭在相同时间段被反复下单,到了现场才发现宠物已经满了。简单起见,在t_home里维护一个current_pets字段,下单时加1,取消或退租完成时减1,基本够用。
4.2 状态流转的落地写法:枚举和状态机校验缺一不可
订单状态如果没有任何约束,代码写多了就容易出现状态乱跳,比如用户还在“待支付”订单直接被改成“已完成”。我习惯在状态变更前做一个统一校验。先定义枚举:
@Getter public enum OrderStatus { WAIT_PAY(0, "待支付"), WAIT_ACCEPT(1, "待接单"), CARING(2, "寄养中"), WAIT_RETURN(3, "待退租"), FINISHED(4, "已完成"), CANCELED(5, "已取消"), REFUNDED(6, "已退款"); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code = code; this.desc = desc; } }再写一个状态机校验类:
public class OrderStatusMachine { private static final Map<Integer, Set<Integer>> TRANSITIONS = new HashMap<>(); static { TRANSITIONS.put(0, Set.of(1, 5)); // 待支付 -> 待接单/已取消 TRANSITIONS.put(1, Set.of(2, 5)); // 待接单 -> 寄养中/已取消 TRANSITIONS.put(2, Set.of(3)); // 寄养中 -> 待退租 TRANSITIONS.put(3, Set.of(4, 6)); // 待退租 -> 已完成/已退款 TRANSITIONS.put(4, Set.of()); TRANSITIONS.put(5, Set.of()); TRANSITIONS.put(6, Set.of()); } public static boolean canTransition(int from, int to) { return TRANSITIONS.getOrDefault(from, Collections.emptySet()).contains(to); } }所有订单状态更新操作在改库前都必须先调canTransition,不满足就抛业务异常。这套逻辑虽然只有十几行代码,但在答辩时非常加分,因为它直接展示了“业务边界意识”——知道哪些操作是不被允许的,比只会写CRUD显得专业得多。
4.3 家庭接单机制:人工接单比自动分配更适合毕设
寄养家庭端有一个“待接单”列表,里面是所有状态为待接单的订单。家庭可以点接单,也可以点拒单。接单后订单状态变成寄养中,此时才意味着宠物确实已经被家庭接收。
自动分配这个功能对毕设来说性价比不高:一是匹配规则很难定复杂,二是容易出边界问题。做一个人工接单流程,把列表排序做好——比如按距离、评分、历史接单数排序——已经足够体现“平台撮合”的逻辑了。
4.4 日期的边界处理:跨天计算的坑
时间计算是这个项目里最容易出错的地方。我强烈建议LocalDate只用来表示日期,不要掺入时分秒。订单的开始和结束日期是date类型,从MySQL查出后直接用LocalDate接收。计算天数时用:
long days = ChronoUnit.DAYS.between(startDate, endDate) + 1;这里有一个坑:很多新手会写成LocalDate.now()和结束日期比较,结果因为时区或者字符串转换问题,导致“明明到了退租日,系统还显示寄养中”。最稳妥的做法是:所有日期计算统一走LocalDate,绝不用java.util.Date去减,也不要直接拿字符串日期做加减。
5. 退租结算一体化:押金、逾期扣费与自动结算的细节
5.1 两条退租触发路径:用户主动发起和系统定时自动触发
退租不能只靠用户手动操作,否则超过结束日期没人处理,系统就卡在“寄养中”了。所以要设计两条路径:
第一条是用户主动发起。用户在“寄养中”的订单上点“申请退租”,订单状态从2变成3,等待家庭确认。
第二条是系统自动触发。用定时任务每天凌晨扫描所有处于“寄养中”但结束日期已经小于今天日期的订单,把它们批量改成“待退租”。
@Component public class OrderAutoReturnTask { @Resource private OrderMapper orderMapper; @Resource private OrderLogMapper orderLogMapper; @Scheduled(cron = "0 30 2 * * ?") public void autoTriggerReturn() { List<Order> expiredOrders = orderMapper.selectList( new LambdaQueryWrapper<Order>() .eq(Order::getStatus, OrderStatus.CARING.getCode()) .lt(Order::getEndDate, LocalDate.now())); for (Order order : expiredOrders) { // 记录状态变更日志 OrderLog log = new OrderLog(); log.setOrderId(order.getId()); log.setFromStatus(order.getStatus()); log.setToStatus(OrderStatus.WAIT_RETURN.getCode()); orderLogMapper.insert(log); order.setStatus(OrderStatus.WAIT_RETURN.getCode()); orderMapper.updateById(order); } } }定时任务执行完并不等于退租完成,家庭还需要确认宠物确实已经归还。如果宠物主人迟了几天才来接,家庭端可以在“待退租”列表里看到订单,确认退租时系统会顺带把逾期费用算出来。
5.2 押金扣除规则:用公式把话说清楚
退租结算时对押金的处理,是整个项目里最需要跟老师讲明白的业务规则。我建议用一套明确的公式:
- 正常退租:返还全部押金。
- 逾期退租:逾期费用 = 超期天数 × 家庭日单价 × 1.5倍。
- 宠物造成损坏:由家庭在确认退租时录入损坏赔偿金额,管理员可复核。
- 最终退还金额 = 押金 - 逾期费用 - 损坏赔偿,最低为0。
@Transactional(rollbackFor = Exception.class) public Settlement settleOrder(Long orderId, BigDecimal damageFee) { Order order = orderMapper.selectById(orderId); if (order.getStatus() != OrderStatus.WAIT_RETURN.getCode()) { throw new BizException("当前状态不允许结算"); } LocalDate actualReturn = LocalDate.now(); long overdueDays = 0; BigDecimal overdueFee = BigDecimal.ZERO; if (actualReturn.isAfter(order.getEndDate())) { overdueDays = ChronoUnit.DAYS.between(order.getEndDate(), actualReturn); overdueFee = order.getUnitPrice() .multiply(BigDecimal.valueOf(overdueDays)) .multiply(new BigDecimal("1.5")); } BigDecimal refund = order.getDepositAmount() .subtract(overdueFee) .subtract(damageFee == null ? BigDecimal.ZERO : damageFee) .max(BigDecimal.ZERO); Settlement settlement = new Settlement(); settlement.setOrderId(orderId); settlement.setActualReturnTime(actualReturn); settlement.setOverdueDays(overdueDays); settlement.setOverdueFee(overdueFee); settlement.setDamageFee(damageFee); settlement.setRefundAmount(refund); settlementMapper.insert(settlement); order.setStatus(OrderStatus.FINISHED.getCode()); orderMapper.updateById(order); return settlement; }这里用BigDecimal.max(BigDecimal.ZERO)可以保证最终退款不会变成负数。虽然正常情况下押金足够覆盖扣费,但万一出现极端情况——比如日单价很高、逾期很多天——退款金额被扣成负数就很奇怪。
5.3 结算与评价的顺序:先结算后评价
很多人会把评价和结算同时处理,但我建议先完成结算,再开放评价入口。原因很简单:如果用户还没退租就先评价,评价内容可能建立在不满意的寄养体验上,而后台的逾期扣费又和安全无关。把评价放在结算完成之后,等于把“体验反馈”和“费用结算”两个动作解耦,业务上更合理。
具体做法是在结算接口返回成功后,前端把评价按钮点亮。后端评价接口额外校验订单状态必须是FINISHED,且没有评价记录。
6. 家庭式寄养特有的审核与权限:为什么这套逻辑比普通商城复杂
6.1 “个人既是用户又是寄养家庭”的身份模型
这个系统最容易被低估的地方在于:寄养家庭不是独立注册的账号,而是一个普通用户“变身”成的角色。普通用户可以下单寄养,也可以申请入驻成为寄养家庭去接单。也就是说,t_user表中的角色不是互斥关系,而是可以叠加的。
在设计权限时,建议用Sa-Token的角色配置,或者直接在接口上用注解判断。比如:
- 普通用户只能访问下单、退租、评价接口。
- 寄养家庭额外拥有接单、确认退租接口。
- 管理员额外拥有审核、用户管理接口。
如果一个用户既申请入驻成功,又去别人家下单,那么他同时拥有两端的权限,这是合理且必须支持的。在代码层面,只需要在注册或登录时把用户角色放到登录态里,接口鉴权时判断“是否包含某个角色”即可,不要让角色互斥。
6.2 入驻审核接口:一个带来大量演示画面的模块
寄养家庭入驻审核是整个平台信任机制的第一道关。流程建议这样走:
- 用户提交申请,填写家庭地址、可接收宠物数量、服务描述,并把
audit_status改成1(审核中)。 - 管理员后台看到审核列表,点击通过或拒绝。
- 审核通过后,用户的
user_type增加寄养家庭角色,同时t_home表插入对应家庭记录,状态置为启用。 - 审核前,用户端家庭列表里看不到该家庭。
@PostMapping("/admin/audit/sitter") @SaCheckRole("admin") public Result auditSitter(@RequestBody AuditRequest request) { User user = userMapper.selectById(request.getUserId()); if (user == null) { return Result.fail("用户不存在"); } if (request.getPass()) { // 升级为寄养家庭角色 user.setUserType(UserType.SITTER.getCode()); user.setAuditStatus(AuditStatus.AUDIT_PASS.getCode()); userMapper.updateById(user); // 同步创建寄养家庭信息 Home home = new Home(); home.setUserId(user.getId()); home.setHomeName(request.getHomeName()); home.setAddress(request.getAddress()); home.setMaxPets(request.getMaxPets()); home.setCurrentPets(0); home.setStatus(1); homeMapper.insert(home); } else { user.setAuditStatus(AuditStatus.AUDIT_REJECT.getCode()); userMapper.updateById(user); } return Result.success(); }审核这个模块虽然代码量不大,但它是“家庭式寄养”区别于普通商城的核心。答辩时如果老师问“你怎么保证寄养家庭靠谱”,你指着这个审核流程讲一遍,比背任何概念都更有说服力。
6.3 家庭列表的匹配逻辑
用户端进入寄养家庭列表后,系统应该按什么顺序展示家庭?最基本的是三个维度:
- 评分:从高到低。
- 距离:按经纬度计算后由近到远。
- 可接数量:
currentPets < maxPets的家庭优先展示。
如果不想接地图服务,可以用近似公式算直线距离:
public double distance(Double lat1, Double lng1, Double lat2, Double lng2) { double radLat1 = lat1 * Math.PI / 180.0; double radLat2 = lat2 * Math.PI / 180.0; double a = radLat1 - radLat2; double b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0; double s = 2 * Math.asin(Math.sqrt( Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); return s * 6371000; // 单位:米 }这个公式放到工具类里,家庭列表查询出来后逐一计算再排序。毕设级别的数据量完全够用,不需要引入Elasticsearch或者空间索引。
6.4 双向评价与纠纷记录:家庭式寄养的特殊需求
在家庭式寄养里,不光是主人评价寄养家庭,寄养家庭也应该能评价主人。原因很现实:寄养家庭最怕遇到宠物有攻击性、主人隐瞒健康问题的情况。双向评价机制能把双方的行为都纳入考量,提高平台的整体可信度。
评价表和订单表关联即可,建议加一个evaluate_type字段区分是“主人评家庭”还是“家庭评主人”。同时可以加一个纠纷留言表,记录双方对某个订单的意见。这个功能不复杂,但能把平台规则讲得更有血有肉,演示时展示一张完整的评价页面,比空谈安全机制直观得多。
7. 实际开发中踩过的四个坑和对应解法
7.1 退租状态被直接改成“已完成”,结算数据全丢了
有一次帮人调代码,发现订单状态从“寄养中”直接跳到“已完成”,结算表里完全没有记录,押金也查不到去向。原因是对接前端时为了省事,把“确认退租”按钮直接调了updateStatus(orderId, FINISHED),把中间的结算逻辑跳过了。
解法就是按本文第5章的思路来:状态只能按状态机允许的路径流转,待退租到已完成必须经过结算接口,结算成功后生成t_settlement记录,同时才更新状态。
7.2 MyBatis-Plus乐观锁配置后不生效
用MyBatis-Plus的乐观锁,最常见的问题就是配置了插件但更新时版本号不自动加。原因是@Version注解加到了实体类的version字段上,但数据库的version没有默认值,插入时是null,MyBatis-Plus就不会执行版本自增。
正确做法是数据库字段设置DEFAULT 0,实体类version字段加@Version,并且在配置类里注册乐观锁插件:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }7.3 @Scheduled方法里调用同类Service方法,事务失效
定时任务里如果直接调用本类的另一个@Transactional方法,事务会失效。这是Spring AOP的经典坑:类内部自调用走的是this对象,而不是代理对象。
解法有三种:把被调用的方法拆到独立的Bean里;或者通过ApplicationContext.getBean拿到代理对象再调用;或者直接在当前方法上整体加@Transactional。我的建议是拆Bean,结构更清晰,也方便复用。
7.4 同一宠物的同时间段被重复下单
“当前家庭其实还有空位,但下单已经超了”这个并发问题,在演示时很难触发,但如果答辩时被问到并发场景,答不上来就会扣分。我推荐用Redis分布式锁,锁的key格式为home:order:{homeId},下单时先加锁,处理完释放,至少保证同一家庭的同一时刻只存在一个创建订单的事务。
如果不想引Redis,也可以用数据库的唯一索引或者乐观锁,但逻辑上会绕一些。我个人建议这个项目加一个简版Redis锁,代码量不大,但能展示并发意识。
写到最后一点个人体会
这套宠物寄养系统我前前后后帮几届同学梳理过,最有价值的其实不是SpringBoot本身,而是把“订单状态机”“日期边界”“退租结算”这三件事想清楚。很多人一开始觉得增删改查很简单,真到自己写的时候才发现,状态跳着跳着就乱了,日期算着算着就差了,退租改成“已完成”就再也回不去了。如果你也在做这个题目,我建议先把订单状态流转图画在纸上,然后把结算公式写清楚,再动手写代码。这三件事通了,剩下的大部分功能其实都是沿着业务线把接口补完的事。最后再分享一个小技巧:演示的时候故意展示一个“逾期退租”的订单,让老师看结算明细里逾期扣费怎么自动出现,这种细节比任何技术点都更能证明你把业务流程吃透了。