宠物店营业系统毕设:从零实现一个高内聚低耦合的零售管理后端
最近在辅导学弟学妹做毕业设计发现很多“宠物店营业系统”项目虽然功能都做出来了但代码质量堪忧像是“能跑就行”的缝合怪。最常见的几个问题下单不扣库存、扣了库存但订单没生成、接口改一处崩一片、完全没有错误处理。今天我就以这个经典毕设为例聊聊如何从零搭建一个高内聚、低耦合、真正有点“工程样子”的零售管理后端。1. 背景痛点为什么你的毕设代码像“一锅粥”很多同学上手就是Controller-Service-Mapper三层硬套结果往往是“裸奔”的数据操作在Service里直接写一堆SQL更新语句没有事务Transactional包裹。用户下单库存表扣减成功了但订单表插入失败结果库存没了订单也没生成数据直接不一致。“上帝类”服务层一个OrderService里既有订单逻辑又有库存计算还有用户积分变更成百上千行代码牵一发而动全身。脆弱的接口API设计随意/createOrder、/addOrder、/newOrder混用参数用Map接收前后端联调全靠猜。“薛定谔”的异常代码里到处是try-catch(Exception e){}抓了又不处理或者直接e.printStackTrace()线上出问题两眼一抹黑。这些问题在答辩时一旦被老师问到“如何保证数据一致性”或“你这个系统能承受多人同时下单吗”很容易露怯。2. 技术选型为什么是Spring Boot MyBatis-Plus做Java后端Spring Boot几乎是默认选择。对比Python的Django或Flask我们的选择基于以下几点生态与就业Spring Boot是Java企业级开发的事实标准社区庞大资料丰富。完成这个毕设的过程本身就是在学习未来工作中最可能用到的技术栈。“约定大于配置”Spring Boot能快速搭建项目内嵌Tomcat一键运行让我们更专注于业务逻辑而不是XML配置。MyBatis-Plus的加持相比原生的MyBatisMyBatis-Plus提供了强大的CRUD封装和条件构造器能极大减少简单SQL的编写。它的Lambda查询方式让代码更优雅、更安全避免SQL注入。结构化与工程化Spring Boot提倡的依赖注入、AOP、分层架构天然引导我们写出更结构化的代码。而Django虽然更“全栈”但其MTV模式对初学者理解严格的分层有时不如Spring清晰。简单说这个组合能让你快速做出一个“像样”的项目并且学到的知识迁移性很强。3. 核心实现订单创建与库存扣减的原子之旅这是系统的核心交易链路必须保证原子性Atomicity。我们采用领域驱动设计DDD的简化分层思想Controller - Service - Repository。清晰的职责分离Controller层只负责HTTP请求的接收、参数校验可使用Valid、格式转换并调用Service。它不应该有任何业务逻辑。Service层核心业务逻辑所在地。一个方法应该代表一个完整的业务操作如“创建订单”。在这里我们通过事务注解来保证边界。Repository层或叫Mapper层只负责与数据库的交互执行最纯粹的增删改查。MyBatis-Plus的BaseMapper和自定义的OrderMapper、ProductMapper就在这里。让我们聚焦最关键的OrderService.createOrder方法。4. 代码示例一个清晰且健壮的订单创建服务以下是一个力求符合Clean Code原则的实现关键点都有注释。package com.petshop.service.impl; import com.petshop.dto.OrderCreateDTO; import com.petshop.entity.Order; import com.petshop.entity.OrderItem; import com.petshop.entity.Product; import com.petshop.repository.OrderItemRepository; import com.petshop.repository.OrderRepository; import com.petshop.repository.ProductRepository; import com.petshop.service.OrderService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; /** * 订单服务实现类 */ Service Slf4j RequiredArgsConstructor // Lombok注解自动生成构造器注入final字段 public class OrderServiceImpl implements OrderService { private final OrderRepository orderRepository; private final OrderItemRepository orderItemRepository; private final ProductRepository productRepository; /** * 创建订单核心方法 * Transactional 注解确保方法内所有数据库操作在一个事务中要么全成功要么全回滚 * rollbackFor 指定发生任何异常都回滚事务 */ Override Transactional(rollbackFor Exception.class) public Order createOrder(OrderCreateDTO orderCreateDTO) { // 1. 参数校验 (这里可以结合Valid在Controller层做初步校验) Long customerId orderCreateDTO.getCustomerId(); ListOrderCreateDTO.OrderItemDTO itemDTOs orderCreateDTO.getItems(); if (itemDTOs null || itemDTOs.isEmpty()) { throw new IllegalArgumentException(订单商品列表不能为空); } // 2. 计算总金额 预扣库存在事务内查询保证数据一致性视图 BigDecimal totalAmount BigDecimal.ZERO; for (OrderCreateDTO.OrderItemDTO itemDTO : itemDTOs) { Product product productRepository.selectById(itemDTO.getProductId()); if (product null) { throw new RuntimeException(商品ID[ itemDTO.getProductId() ]不存在); } if (product.getStock() itemDTO.getQuantity()) { throw new RuntimeException(商品[ product.getName() ]库存不足); } // 累加商品单价*数量 totalAmount totalAmount.add( product.getPrice().multiply(BigDecimal.valueOf(itemDTO.getQuantity())) ); } // 3. 扣减库存使用乐观锁见下文第5点 for (OrderCreateDTO.OrderItemDTO itemDTO : itemDTOs) { int updateCount productRepository.deductStock(itemDTO.getProductId(), itemDTO.getQuantity()); if (updateCount 0) { // 乐观锁更新失败可能库存已被其他事务修改抛出异常触发事务回滚 throw new RuntimeException(商品库存并发更新失败请重试); } } // 4. 创建订单主记录 Order order new Order(); order.setOrderNo(generateOrderNo()); // 生成唯一订单号 order.setCustomerId(customerId); order.setTotalAmount(totalAmount); order.setStatus(1); // 1-待支付 order.setCreateTime(LocalDateTime.now()); orderRepository.insert(order); // 插入后order对象会自动获取主键ID // 5. 创建订单明细记录 ListOrderItem orderItems itemDTOs.stream().map(itemDTO - { OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); // 关联主订单ID orderItem.setProductId(itemDTO.getProductId()); orderItem.setQuantity(itemDTO.getQuantity()); // 这里可以再次查询商品快照信息如当时价格存入明细与主商品表解耦 return orderItem; }).collect(Collectors.toList()); orderItemRepository.batchInsert(orderItems); // 批量插入 log.info(订单创建成功订单号{} 用户ID{}, order.getOrderNo(), customerId); return order; } /** * 生成订单号示例方法实际项目需要更复杂的分布式ID方案 */ private String generateOrderNo() { return PS System.currentTimeMillis() (int)(Math.random() * 1000); } }代码设计亮点单一职责createOrder方法只负责“创建订单”这一件事虽然内部有多个步骤。事务边界明确整个方法被Transactional包裹库存检查、扣减、订单创建、明细插入要么全部成功要么全部失败。清晰的异常处理业务校验失败如库存不足时抛出非受检异常RuntimeException触发事务回滚并将错误信息反馈给前端。日志记录在关键业务节点如订单创建成功使用log.info记录便于排查问题。5. 性能与安全如何应对“库存超卖”多人同时下单同一件商品如果只是先查询后扣减就会发生超卖。解决方案是乐观锁。我们在商品表product中增加一个版本号字段version或使用更新时间戳。// ProductRepository 或 ProductMapper.xml 中定义的方法 // 使用MyBatis-Plus的UpdateWrapper结合版本号实现乐观锁 int deductStock(Param(productId) Long productId, Param(quantity) Integer quantity, Param(version) Integer version); // 对应的XML片段 !-- update iddeductStock UPDATE product SET stock stock - #{quantity}, version version 1 WHERE id #{productId} AND stock #{quantity} AND version #{version} /update --在createOrder方法中我们先查询出商品的version然后在扣减库存时将其作为条件。如果更新返回的行数为0说明在这期间库存已经被其他请求修改我们抛出异常事务回滚前端提示用户“操作冲突请重试”。这是一种轻量级的并发控制方案非常适合读多写少的电商场景。6. 生产环境避坑指南从毕设到“像样”的项目配置分离千万不要把数据库密码写在application.yml里然后提交到Git使用application-dev.yml本地开发和application-prod.yml生产环境通过spring.profiles.active激活。敏感信息使用环境变量或配置中心。全局异常处理别在Controller里到处try-catch了。使用ControllerAdvice和ExceptionHandler编写全局异常处理器将不同的异常如IllegalArgumentException,SQLException转化为结构化的错误JSON返回给前端。日志规范化告别System.out.println()。使用SLF4J Logback合理设置INFO,WARN,ERROR级别。在application.yml中配置日志文件路径和滚动策略确保出问题有迹可循。接口文档一定要写API文档用Swagger现为SpringDoc OpenAPI自动生成前后端开发效率倍增答辩时也能直观展示你的工作。基础校验在Controller的DTO参数上加上Valid注解配合NotBlank,Min等注解进行基础校验避免无效请求直接进入业务层。思考部署你的项目最终如何跑在云服务器上学会用Dockerfile将Spring Boot应用打包成镜像用docker-compose管理MySQL和你的应用这会让你的项目演示环节非常出彩。总结与延伸通过以上步骤我们实现了一个具备事务安全、并发控制、清晰分层的订单创建核心流程。这不仅仅是完成一个功能更是构建一个可维护、可测试系统的工程实践。这个宠物店系统还有很多可以扩展的地方例如会员积分系统下单成功后如何异步地、可靠地给用户增加积分可以考虑使用Spring的Async异步任务或者引入消息队列如RabbitMQ来解耦。服务预约模块宠物洗澡、美容等服务的预约涉及到时间段的排他性校验这比商品库存扣减更复杂你可以思考如何设计数据表结构和校验逻辑。毕业设计是检验学习成果的舞台也是一个绝佳的“练手”项目。希望这篇笔记能帮你跳出“功能堆砌”的陷阱写出让答辩老师眼前一亮的、具有工程美感的代码。从理清每一层的职责开始从写好一个事务方法开始你的代码质量会得到质的提升。加油