ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot连锁门店销售与库存管理系统:从数据库设计到并发控制实战

SpringBoot连锁门店销售与库存管理系统:从数据库设计到并发控制实战 1. 项目概述与核心需求拆解坦白说第一次看到这个项目标题时我第一反应是又一个常规的CRUD管理系统。直到我真正把它拆开来看才发现里面能聊的东西比想象中要多得多。这个题目看似是咖啡连锁销售与库存管理换成奶茶店线上点单与库存管理本质上是一套标准化的连锁门店进销存线上交易中台的简化实现它覆盖了商品管理、订单流转、库存联动、门店协同、报表统计这几条核心链路。这个项目最适合三类人一类是正在做课程设计或毕业设计、需要一套完整可复现系统的同学一类是想入门 SpringBoot 全栈开发、但不想只做一个增删改查 Demo的初学者还有一类是准备面试时拿一个业务完整度高的项目来撑场面的求职者。它解决了什么问题一句话说透单店点单系统谁都会写但一旦牵扯到多门店中央库存线上点单销售数据归集业务逻辑就会立刻复杂起来。这个题目恰好卡在简单到能做完和复杂到能体现设计能力之间的平衡点上这也是我建议你认真做它的核心原因。从技术角度看它要求你掌握的不只是 SpringBoot 的 Controller-Service-Mapper 三板斧还包括数据库表设计时的范式与反范式取舍、事务控制下的库存扣减方案、订单状态机设计、以及多门店数据权限隔离等真切的项目难点。这些能力光靠看视频教程是学不来的必须落到代码里踩一遍坑才长记性。2. 整体架构设计与技术选型思路2.1 单体应用还是微服务先回答这个问题很多同学一看到连锁两个字就本能地往微服务上想订单服务、库存服务、用户服务各拆一个模块再用 Nacos 做注册中心用 OpenFeign 做远程调用。我必须泼一盆冷水课程设计和毕业设计阶段微服务几乎一定是负资产。微服务引入的分布式事务、服务间调用链追踪、部署运维成本对三五个人的开发周期来说完全是降维打击。哪怕你硬着头皮做出来了大概率也只能把订单模块调用了库存模块的接口这种伪分布式逻辑写在论文里答辩老师一问数据一致性怎么保证你就会非常被动。这个项目最合理的架构就是单体内部分层SpringBoot 作为统一应用入口内部按业务域拆包比如order、product、inventory、store通过 Service 层完成业务编排通过 Mapper 层访问数据库。单体架构并不意味着代码混乱相反强制你在包结构清晰度上花心思反而更贴近真实中小型连锁企业的软件形态。2.2 技术栈补全SpringBoot 只是起点我能理解标题里只写了 SpringBoot但你需要自己在项目里补齐一套完整的技术拼图持久层框架选 MyBatis-Plus它和 SpringBoot 的贴合度最高内置的LambdaQueryWrapper能大幅度减少 SQL 拼接的样板代码。不推荐用 JPA在复杂的多表统计场景里它的表达能力会让你怀疑人生。数据库MySQL 8.x无条件选择。需要注意连接串里带上serverTimezoneAsia/Shanghai否则日期字段会因为时区偏差出现少8小时的诡异问题这是经典到不能再经典的坑。前端方案如果底子一般直接用 Thymeleaf 模板引擎 Bootstrap服务端渲染模式不用考虑跨域和鉴权拦截的复杂配置。如果前端有些基础可以上 Vue3 Element Plus 前后端分离但意味着你要额外处理 JWT 拦截器和 CORS 配置工作量会涨一截。权限控制Spring Security 在课程设计里通常偏重用拦截器HandlerInterceptor 自定义注解就能解决 90% 的权限问题实现简单不说答辩时讲起来也容易自圆其说。这里重点提醒一个容易被忽略的版本问题SpringBoot 3.x 要求 JDK 17而且不再兼容 MyBatis-Plus 老版本。如果你在网上下到的教程是 SpringBoot 2.x 配 JDK 8 的写法直接照搬到 3.x 就会遇到javax包名不存在的报错。我见过太多人处理这个版本兼容问题花掉一整天这是完全可以提前避开的坑。2.3 为什么销售库存必须放在一个系统里从标题上看有人会觉得销售和库存本来就是两个模块硬揉在一起是不是增加复杂度实际上这两个模块天然是强耦合的。每一笔线上订单成交必然引起库存数字变化每一次库存盘点调整又会直接影响能否继续接单。若把销售和库存拆成两套独立系统数据同步会成为永远的心病。从业务上讲这个耦合点尤其体现在预扣库存与事务边界的设计上。用户下单时系统扣减库存用户支付失败或超时取消系统回补库存。如果扣减和回补不在同一个数据库事务里高并发场景下库存负数将不可避免。这一点会贯穿整个项目的数据设计。3. 数据库设计一张表都不能少也不能多3.1 核心表结构与字段设计的思路数据库是这个项目的地基地基打歪了上层业务逻辑再精巧也是空中楼阁。建议以 8 张核心表起步表名作用关键字段store门店信息store_id,name,address,statusproduct商品信息咖啡/奶茶product_id,name,category,price,imagesku规格库存单位sku_id,product_id,spec_name,stockinventory_record库存变动流水record_id,sku_id,change_type,change_count,create_timecustomer会员客户customer_id,phone,nickname,levelorders订单主表order_id,store_id,customer_id,total_amount,statusorder_item订单明细item_id,order_id,sku_id,quantity,priceuser系统后台用户user_id,username,password,role,store_id设计这套表时有一个核心原则面向业务过程建表而不是面向界面建表。比如不要因为页面上需要一个显示门店剩余库存的功能就去建一个store_stock表而应该通过sku.stock字段判断归属门店。这样可以避免大量冗余字段在后续迭代中变成沉重的包袱。3.2 为什么强烈建议加一张库存变动流水表这是我有意加入的一张看起来非必要的表但实际价值非常大。没有它时如果 A 门店今天的库存从 100 变成 60你只能看到结果根本说不清这 40 杯咖啡是卖出去了、过期报废了还是调拨给 B 门店了。有了流水表每一次变动都能追溯来源。更重要的是流水表能有效解决并发场景下的数据打架问题。两张表配合时库存更新走显式行级锁SELECT ... FOR UPDATE同时写入一条流水记录两条 SQL 在同一个事务里提交能够保证账实相符。不要小看这一张表的威力答辩时老师问你的库存设计如何保证一致性你直接拿出流水表和锁机制回答就比干巴巴说我用 UPDATE 语句改一下库存字段高出好几个层次。3.3 软删除与逻辑外键两种业务避坑做法很多新手会直接在表上建物理外键约束觉得这样能保证数据完整性。但实际生产中物理外键在插入、删除时都要做额外一致性校验复杂业务里极其容易死锁。线上系统几乎清一色使用逻辑外键——字段里存着对方的id但不建立真实约束靠 Service 层保证关联正确性。比如订单删除时不应该直接把orders表记录 DELETE 掉而是将status置为-1已取消。同理商品下架也不应该物理删除product行而应该维护一个enable_status字段。这在面试里叫软删除是一个高频考点许多缺乏实战经验的人并不知道这个操作背后的价值在于保留业务审计历史而非简单规避外键报错。4. 核心功能模块拆解与实操要点4.1 线上点单流程状态机比你想的重要整个系统里最有意思的部分是订单状态的流转。如果只用一两个字段存状态后面会陷入if-else满天飞的泥潭。我的做法是定义一个清晰的订单状态枚举Getter public enum OrderStatus { PENDING_PAYMENT(0, 待支付), PAID(1, 已支付), MAKING(2, 制作中), COMPLETED(3, 已完成), CANCELED(4, 已取消), REFUNDING(5, 退款中); private final Integer code; private final String desc; OrderStatus(Integer code, String desc) { this.code code; this.desc desc; } }你可能觉得这就是一个简单的状态枚举没什么技术含量。但真正的难点在于哪些状态间可以迁移。比如待支付状态可以迁移到已支付也可以迁移到已取消但已完成状态绝对不允许直接迁移到已支付。为了写好这个约束我建议在 Service 层做一个状态机校验方法public void changeOrderStatus(Long orderId, OrderStatus targetStatus) { Orders order getById(orderId); OrderStatus currentStatus OrderStatus.of(order.getStatus()); if (!currentStatus.canTransferTo(targetStatus)) { throw new BusinessException(非法的订单状态变更: currentStatus.getDesc() - targetStatus.getDesc()); } order.setStatus(targetStatus.getCode()); updateById(order); }没有状态机约束时前端调一次接口取消订单后端就默默把订单置为取消状态再调一次支付接口又会把同一个订单置为已支付。这种混乱在演示环节会把答辩老师直接看愣住。做状态机其实并不难难的是你先有这个意识。4.2 库存扣减的并发方案从乐观锁到原子更新线上点单系统最大的并发压力集中在同一时间大量用户对同一杯咖啡下单。如果直接写UPDATE sku SET stock stock - #{quantity}确实是最简单的写法但 MySQL 层面如果没控制好事务隔离级别在高并发下可能出现超卖。我首推原子更新语句UPDATE sku SET stock stock - #{quantity} WHERE sku_id #{skuId} AND stock #{quantity}关键在于后半句stock #{quantity}这一步把扣减库存和库存充足性校验合并成一条 SQL由数据库自己保证原子性。如果影响行数为 0说明库存不足此时直接抛异常回滚订单即可。这种方式比先查询再更新少了整整一个来回的网络交互也在很大程度上避免了并发窗口。再补充一个进阶思路如果后续项目复杂度上升可以在库存表中加一个version字段使用乐观锁机制重试三次。但初版项目用到原子更新事务回滚已经足够不要为了炫技引入 Redis 分布式锁之类的东西它会让你的架构复杂度和部署难度同步上升。4.3 门店与商品的关系从单表到多对多映射如果你做过实际门店调研很容易发现不同门店能卖的商品并不完全相同比如 A 门店有生椰拿铁但 B 门店因为设备条件限制就没有。如果设计为每张product表只有一个store_id字段那商品和门店的关系就退化成了一对多这是不符合实际业务形态的。正确的做法是增加一张store_product关联表字段名说明id主键store_id门店 IDproduct_id商品 IDsale_status门店内上架状态1 上架/0 下架sort_order门店内自定义排序权重这种设计允许不同门店以不同价格、不同状态售卖同一款商品也为后续做门店差异化菜单留下了扩展余地。同理库存表sku也可以挂store_id字段区分门店维度。多对多映射是关系型数据库最核心的设计能力把这个想透了你就已经超过大量只会照着视频敲代码的同行了。4.4 报表统计GROUP BY 是常规武器标题没有明说但销售与库存管理系统一定逃不开报表模块。日报、周报、门店排行、商品销量排行这些功能如果能做成 MySQL 的GROUP BY聚合查询通常不需要引入额外的大数据组件。比如统计某一时间段内各门店销售额SELECT s.store_name, DATE_FORMAT(o.create_time, %Y-%m-%d) AS biz_date, SUM(o.total_amount) AS total_sales FROM orders o LEFT JOIN store s ON o.store_id s.store_id WHERE o.create_time BETWEEN #{startTime} AND #{endTime} AND o.status IN (2, 3, 5) GROUP BY s.store_id, biz_date ORDER BY biz_date DESC, total_sales DESC这里有一个很多新手会疏忽的点统计订单金额时一定要过滤订单状态。未支付订单、已取消订单不应该计入销售额否则报表就只能自己骗自己。在实践里我吃过这个亏表单一度显示的门店日销售额虚高到吓人排查了半天才发现是状态过滤条件漏了。业务口径不明确之前千万别急着写统计 SQL先对照需求把有效订单的定义梳理清楚。5. 实操过程与核心环节实现5.1 秒建项目骨架Spring Initializr 的正确打开方式说到创建项目我强烈建议直接用 Spring Initializr 网页版而不是在 IDE 里点向导创建。网页版的好处是依赖选择一目了然而且会自动生成规范到无可挑剔的pom.xml。以 SpringBoot 2.7.x JDK 8 为例依赖勾选这几个就够Spring WebMyBatis Framework如果后续用 MyBatis-Plus可以忽略这个手动引入MySQL DriverLombokValidation这里分享一个我个人的版本选择建议如果不是对新技术有强烈追求还是优先选 2.7.x 而不是 3.x。2.7.x 是 SpringBoot 2.x 的最后一个版本社区资料极其丰富几乎你踩过的每一个报错都能在网上找到现成答案。而 3.x 虽然性能更强但踩坑成本对课程设计来说是有点高的。很多现成的整合教程都是基于 2.x 写的盲上 3.x 大概率会卡在环境兼容问题上。创建完项目后第一步先别着急写代码而是搭一个最基础的 Health Check 接口。用 5 分钟验证从项目能启动到数据库能连通这条链路是通的再着手业务代码。如果你一上来就写一大堆 Controller第一次启动时报错都不知道是哪个环节出了问题。5.2 目录结构设计让每个包都承担明确职责项目包结构是我每次带人做项目时都会反复强调的部分。它直接决定了一个项目的可读性与可维护性。下方是我推荐的一版结构com.example.coffee ├── common // 公共类统一返回结果、异常处理、常量定义 │ ├── result │ ├── exception │ └── constant ├── config // 配置类MyBatis-Plus、拦截器注册、CORS ├── controller // 控制器层接收请求参数校验 ├── service // 业务层核心业务逻辑事务控制 │ └── impl ├── mapper // 数据访问层MyBatis-Plus Mapper接口 ├── entity // 实体类对应数据库表 ├── dto // 数据传输对象接收前端参数 └── vo // 视图对象返回前端数据我发现很多初学者喜欢把前端传来的参数直接让 Entity 实体去接收比如在Orders实体里加一个createTimeBegin字段用来接收时间范围。这确实省事但时间一长实体类就会被各种页面参数污染得面目全非数据库字段和请求参数混杂在一起谁看谁头疼。养成用 DTO 接收请求参数、用 VO 返回响应结果的习惯项目结构会瞬间清爽一个维度答辩讲述分层设计时也有了实践支撑。5.3 从登录鉴权讲起Interceptor 的完整落地由于系统分店长角色和总部管理员角色登录鉴权就是不可绕过的一环。我的做法是自定义一个AuthInterceptor注册到 Spring MVC 拦截器链中public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/user/login)) { return true; } HttpSession session request.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); return false; } // 把用户信息放到 ThreadLocal 或 Request 属性中供后续业务使用 request.setAttribute(loginUser, loginUser); return true; } }配置类里注册这个拦截器时记得用excludePathPatterns把登录接口、静态资源路径放行否则 CSS 样式都会被拦截页面样式乱七八糟又是个巨坑。比如/css/**,/js/**,/images/**,/user/login全部加进白名单。关于角色权限区分我建议在User表里维护一个role字段取值约定为ADMIN总部管理员和STORE_MANAGER店长。总部管理员可以查看所有门店的销售报表店长只能查看本门店数据。这个store_id字段在查询时作为默认过滤条件可以在很大程度上规避越权风险。5.4 订单提交的核心事务代码写不对就库存负数下面是订单提交的简化 Service 代码注意我在里面对库存扣减和订单创建做了事务控制Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateDTO dto) { // 1. 校验门店和商品信息 Store store storeMapper.selectById(dto.getStoreId()); if (store null || store.getStatus() ! 1) { throw new BusinessException(门店不存在或已停业); } // 2. 计算订单总价 BigDecimal totalAmount new BigDecimal(0); ListOrderItem orderItems new ArrayList(); for (OrderItemDTO itemDTO : dto.getItems()) { Sku sku skuMapper.selectById(itemDTO.getSkuId()); // 3. 原子扣减库存影响行数为0表示库存不足 int updated skuMapper.deductStock(sku.getId(), itemDTO.getQuantity()); if (updated 0) { throw new BusinessException(商品[ sku.getProductName() ]库存不足); } // 4. 记录库存流水 inventoryRecordMapper.insert(InventoryRecord.builder() .skuId(sku.getId()) .changeType(InventoryChangeType.SALE.getCode()) .changeCount(-itemDTO.getQuantity()) .createTime(LocalDateTime.now()) .build()); // 5. 组装订单明细 OrderItem orderItem new OrderItem(); orderItem.setSkuId(sku.getId()); orderItem.setQuantity(itemDTO.getQuantity()); orderItem.setPrice(sku.getPrice()); orderItems.add(orderItem); totalAmount totalAmount.add(sku.getPrice().multiply(new BigDecimal(itemDTO.getQuantity()))); } // 6. 创建订单主记录 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setStoreId(dto.getStoreId()); order.setCustomerId(dto.getCustomerId()); order.setTotalAmount(totalAmount); order.setStatus(OrderStatus.PENDING_PAYMENT.getCode()); orderMapper.insert(order); // 7. 批量插入订单明细 orderItemMapper.insertBatch(order.getId(), orderItems); return OrderVO.from(order); }你留意到第 3 步了吗这就是前面提到的原子更新。deductStock方法用一条 SQL 完成校验库存扣减库存也不用手动加悲观锁。Transactional(rollbackFor Exception.class)保证了如果后面任何一步出问题前面扣减的库存会自动回滚避免了订单没生成但库存没了的尴尬场景。5.5 MyBatis-Plus 的查询构造器缩短时间和提升可读性如果你不想写大量 XML 映射文件使用 MyBatis-Plus 的LambdaQueryWrapper能大幅提升效率。比如门店商品列表public ListProductVO listStoreProducts(Long storeId, String category) { LambdaQueryWrapperStoreProduct wrapper new LambdaQueryWrapper(); wrapper.eq(StoreProduct::getStoreId, storeId) .eq(StoreProduct::getSaleStatus, 1); if (StringUtils.hasText(category)) { wrapper.eq(Product::getCategory, category); } wrapper.orderByAsc(StoreProduct::getSortOrder); return storeProductMapper.selectList(wrapper); }MyBatis-Plus 通过方法引用StoreProduct::getStoreId在编译期就确定了字段名这样即使后续数据库表结构改动IDEA 也会帮你全局检索出所有引用位置而不像字符串硬编码那样默默运行报错到深夜。还有一点经验之谈尽量不要在循环里调用 selectOne 查询数据库能用selectBatchIds批量查询就批量查询否则数据量一大数据库连接池很容易被拖垮。6. 前端页面与交互设计别让后端成果毁在丑字上6.1 后台管理页面表格为主表单为辅后台管理端我推荐用 Bootstrap 或者现成的 AdminLTE 模板网上大把免费开源模板可参考。休息一下眼睛不要自己从零写 CSS费时费力效果还不如人意。页面功能清单不必贪多但该有的必须有登录页工作台仪表盘显示今日订单数、今日销售额、低库存预警数量商品管理商品列表、添加商品、编辑商品、上下架库存管理库存列表、入库操作、盘点操作、流水查看订单管理订单列表、订单详情、订单状态流转操作报表统计按门店/商品/时间维度查看销售数据用户管理创建店长账号分配门店一个真实的坑表格分页。如果你用了 PageHelper 或者 MyBatis-Plus 的分页插件千万记得把每页的查询参数当前页数、页面大小传给前端。前端翻页时后端Page对象里的total字段要正确返回否则你永远只能看到第一页内容这会让演示环节彻底翻车。6.2 移动端点单页把重点放在点单流程而不是视觉特效C 端点单页面就是顾客操作的手机端建议做成一个简洁的 H5 页面。选好门店以后左侧显示商品分类右侧显示商品列表点击商品弹出规格选择比如大杯/中杯/去冰/少糖选好加购购物车浮在底部提交订单后跳转支付模拟页。这里核心交互点是规格选择。一个生椰拿铁可以有冷/热大杯/中杯两个规格维度规格不同价格不同对应的 SKU 也不同。我建议前端点击商品时向后端请求该商品的全部 SKU 列表由前端根据规格维度动态展示选项。后端不需要承担规格组合拼装的逻辑数据模型更清爽。6.3 数据可视化用 ECharts 写两个图表就够报表页不要求制作一个完整的 BI 系统两三个图表就能说明问题折线图最近 7 天销售额走势柱状图各门店销售额对比饼图商品分类销量占比要注意的是ECharts 拿到数据后渲染的时机必须放在 DOM 挂载之后。如果你用 jQuery 的$(document).ready()去初始化图表一般没问题但如果用的是 Vue 的mounted钩子得用this.$nextTick()确保容器已经渲染完成。这个细节错了图表区域就只会显示一片空白。7. 常见问题与排查技巧实录7.1 数据库连接失败时区、驱动、字符集三巨头这是整个项目生命周期中最常见的报错没有之一。The server time zone value...是典型的 MySQL 8.x 时区问题解决办法在 JDBC 连接串中加上serverTimezoneAsia/Shanghai。还有一个容易踩的是驱动类名问题。如果你用com.mysql.jdbc.Driver在 MySQL 8.x 下会直接提示 ClassNotFound。应该使用新驱动com.mysql.cj.jdbc.Driver。字符集方面建议连接串加上characterEncodingutf8否则中文写入数据库后会变成问号那画面相当恐怖。7.2 PageHelper 分页失效SqlSessionFactory 注入顺序如果引入了 PageHelper 但分页不生效第一检查 Maven 依赖版本和 MyBatis-Plus 版本是否兼容。如果用的是 MyBatis-Plus 的分页插件需要先注册MybatisPlusInterceptor然后添加PaginationInnerInterceptor。漏配置这个类分页查询会默认查出全部数据页面数据一多就会非常卡。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个配置类很容易被忽略。很多人依赖加对了SQL 也写了但后端分页全量返回前端翻页无效查来查去最后发现是拦截器忘了注册。这样的小问题极其消耗耐心写在这里帮你避坑。7.3 库存超卖复现模拟并发下单的压测思路答辩前建议自己先做一轮简单并发测试别等到现场出情况。我用过最简单的方式用 IDEA 的 HTTP Client 写一个请求脚本然后用线程池模拟 200 个并发请求同时提交订单观察库存是否会变成负数。# IDEA HTTP Client 脚本示例 POST http://localhost:8080/api/order/create Content-Type: application/json { storeId: 1, customerId: 1001, items: [ { skuId: 1, quantity: 1 } ] }如果你加上了原子更新和事务回滚测试后库存数量永远不会小于 0。如果发现超卖优先检查 SQL 中是否漏掉了stock #{quantity}这个条件。这个问题被我放进最常见清单是因为它几乎是库存系统是否合格的唯一硬指标。7.4 静态资源 404SpringBoot 拦截器白名单和路径匹配如果你写了拦截器却发现 CSS、JS、图片通通加载不出来大概率是拦截器没有放行静态资源路径。解决方式有两种在excludePathPatterns中显式加入/static/**如果使用了自定义路径映射同时注册WebMvcConfigurer的addResourceHandlers方法一个更稳妥的组合做法是registry.addInterceptor(authInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/user/login, /static/**, /error);排查这一类问题时打开浏览器开发者工具看到样式请求的状态码是 401 或 302第一反应就应该想到拦截器而不是去检查 HTML 里的路径写错了没有。7.5 常见问题速查表问题现象可能原因解决方案启动报端口被占上一个服务未关netstat -ano查 PIDtaskkill杀掉中文乱码连接串缺字符集加characterEncodingutf8时间少 8 小时JDBC 未设置时区加serverTimezoneAsia/Shanghai库存超卖SQL 未限制库存量改用UPDATE ... WHERE stock ?分页不生效分页拦截器未注册添加PaginationInnerInterceptor访问需登录的接口直接 401登录凭证失效检查 Session/Token 传递和有效期接口路径总是 404包扫描未覆盖确保启动类在根包而非子包8. 扩展方向与答辩亮点提炼8.1 从能跑到能讲给项目加一点点反光纸如果把项目比作学生作品单纯能跑只能代表功能基本完整想要在答辩或面试中拿到更高的评价还需要一些额外的亮点设计。不需要多一两个就足以形成记忆点。我建议优先加库存预警机制。当某个 SKU 库存低于设定阈值时后台工作台在页面右上角显示醒目的提示信息比如生椰拿铁大杯库存剩余 10低于预警阈值 20。这个功能再配上简单的邮件提醒用 Spring 的JavaMailSender或者企业微信机器人 Webhook就能非常直观地体现库存管理系统的实用价值而不仅仅是点单后扣库存这种基础链路。另一个可行方向是订单超时自动取消。借助 Spring 的Scheduled定时任务每分钟扫描一次创建超过 15 分钟仍未支付的订单自动将其置为已取消状态同时回补库存。定时任务在课程设计中并不复杂却能让线上点单的业务闭环变得完整答辩效果极佳。8.2 用 Docker 打包让演示环境不再翻车答辩现场最怕什么不是代码出错而是在我电脑上明明是好的这种地狱开局。为了避免现场环境不一致我强烈建议把项目打包成 Docker 镜像提供一份简单的docker-compose.yml同时拉起 MySQL 和 SpringBoot 应用version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: coffee_db ports: - 3306:3306 volumes: - ./data:/var/lib/mysql app: build: . depends_on: - mysql ports: - 8080:8080 environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/coffee_db?serverTimezoneAsia/Shanghai SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root123456这份配置文件的价值在于任何时候重新部署只要执行一条docker-compose up -d就能把整套系统带起来。它本身就是一份展示工程化能力的有力证明即使你不一定在答辩中用上把它写进文档中提及本系统支持容器化一键部署都是明显的加分项。8.3 部署避坑数据库初始化脚本要写进文档课程设计评审老师很看重的一项是代码能否开箱即用。你的项目里必须包含一份完整的init.sql脚本按顺序创建数据库、建表、插入初始数据和测试账号。千万要保证脚本在纯净 MySQL 上执行不会报错。我在做项目评审时见过太多人把init.sql里的字段类型和实体类属性对不上或者漏加外键/索引。建议亲自在全新的数据库环境里跑一遍脚本启动项目走一遍核心流程再提交代码和文档。这个细节看似普通却决定了你项目演示满分体验的下限。8.4 万字文档的写作策略不要写成代码说明书项目自带的万字文档千万不要写成把每个方法列表贴一遍的代码说明书。文档要有逻辑主线系统背景、需求分析角色用例、数据库设计ER 图表结构说明、核心流程订单时序图、库存扣减逻辑、系统展示截图功能说明、遇到的问题与解决方案。这里分享一个小技巧每个核心模块的写作用业务背景 → 遇到的问题 → 我的解决思路 → 最终效果四段式来展开这种结构比平铺直叙的功能截图要有说服力得多。比如库存模块可以写背景是连锁店经常出现超卖问题解决思路是原子更新事务回滚最终效果是压测 200 并发无超卖并附上测试截图。这段直接展示了你的逻辑思维和解决真实问题的能力远比贴一堆代码有价值。9. 写在最后的几点实在话这个项目做到最后我最大的感受是它不是一个写代码的任务而是一个设计业务系统的任务。很多人一上来就急着敲 Controller忽略了对业务诉求的理解做到一半发现库存与订单之间关系理不顺推倒重来成本奇高。我个人建议的落地顺序是先画 ER 图再定接口文档其次搭工程骨架接着实现核心订单流程最后补充库存管理、报表和权限模块。这个顺序能保证你最先把系统的主干打通后续功能再一个一个往上挂整体节奏非常从容。另外想给所有准备拿这个项目去答辩的同学一个忠告不要过度追求功能数量。一个稳定运行、逻辑严谨、文档清晰的系统远比一个功能堆砌、到处报错、代码混乱的系统得分更高。我在评审时最看重的是核心链路是否可靠、异常分支是否考虑、文档是否愿意让人读懂这些恰恰是这个项目最值得打磨的部分。如果你在实现过程中卡在白屏无报错、页面 404、库存数据异常这类问题上可以参考上文第七部分的排查思路。大部分坑都是版本问题、配置遗漏和事务边界不清导致的花点时间逐项排查不会有什么真正过不去的坎。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进