Spring Boot美发商城系统实战:从数据库设计到部署上线

发布时间:2026/9/8 12:53:20
Spring Boot美发商城系统实战:从数据库设计到部署上线 看到这个题目我就知道这八成是又一位要做毕设、或者想系统巩固 Spring Boot 技术栈的同学找上门了。“基于 Spring Boot 的美发商城系统”听上去常见但真正动手做起来踩坑的地方一点都不比普通商城少。美发这门生意跟卖衣服、卖数码完全不一样它卖的是“服务商品卡项”的组合背后还牵扯预约排班、发型师档案、会员储值这些特有逻辑所以这个题目与其说是在做一个电商系统不如说是在做一个“服务预约 线上商城 会员管理”的复合型系统。这篇内容我会从技术选型、数据库设计、核心流程、常用组件整合、部署上线到问题排查完整拆一遍争取让你照着思路能把项目从零搭起来也能在答辩或面试时把每一步为什么这么做讲清楚。1. 项目定位这不是普通商城是“服务商品”双引擎系统1.1 美发行业的商城到底在卖什么先想清楚一件事美发商城的核心交易对象不只是洗发水、发膜这类实体商品更大头在“剪发服务”“烫染项目”“头皮护理”和“会员卡”。这就决定了系统不能照搬淘宝模式你至少需要处理两类完全不同的业务对象服务类有项目名称、时长、适用发质、负责的发型师、预约时段、价格商品类有 SPU、SKU、库存、规格比如颜色、容量、快递或到店自提卡项类储值卡、次卡、年卡有面额、有效期、使用规则、余额流水。所以项目名叫“美发商城”但更准确的说法是一个垂直行业的小型中台。我见过不少同学上来就建了商品表和订单表结果做到一半发现没法排班、没法锁发型师时段等于推倒重来。正确的思路是先梳理业务域再设计数据模型最后才落到 Spring Boot 的代码结构上。1.2 为什么 Spring Boot 是这个项目最稳的底座选 Spring Boot 几乎没什么悬念原因也很实在自动配置省掉大量样板配置。你不需要像 SSM 时代那样手写一堆 XML 去声明数据源、事务管理器、SqlSessionFactorySpring Boot 会基于 classpath 依赖帮你把常见组件自动装配好。这对从零开始的学生项目特别友好能把时间花在业务上而不是配置上。生态成熟社区资料多。MyBatis-Plus、Spring Security、Redis、RabbitMQ、Docker 部署随便搜都能找到和 Spring Boot 搭配的完整案例。遇到问题也容易排查因为踩过坑的人多。内置 Tomcat独立运行。部署时一个java -jar就完事配合 Docker 做镜像非常顺简历上写“使用 Docker 部署 Spring Boot 项目”是很加分的一笔。前后端分离形态天然契合。Spring Boot 只负责提供 RESTful API前端用 Vue 或 React 来渲染正好也符合现在企业里主流的开发模式。我的建议是Spring Boot 版本选 2.7.x比如 2.7.18这一代因为它既有足够的新特性各种第三方库兼容性也成熟。不要一上来就追 Spring Boot 3.x涉及 Jakarta EE 命名空间迁移很多网上教程已经过时新手容易踩版本坑。1.3 整体架构与核心模块规划这个项目整体架构我建议按前后端分离来做后端内部再按业务域分包。一个能说服面试官的分层逻辑是接入层Spring MVCController 层负责参数接收、校验、统一响应业务层Service 层处理事务、业务规则、编排各类领域逻辑数据层MyBatis-Plus MySQL只负责持久化中间件Redis 做缓存和分布式锁RabbitMQ 或 RocketMQ 做异步通知与订单超时处理基础设施Docker Compose 管理 MySQL、Redis、后端服务。模块划分上我建议拆成下面这几个模块核心功能用户与会员中心注册登录、JWT 鉴权、会员等级、储值余额、消费记录商品中心商品 SPU/SKU、库存管理、商品分类、上下架预约中心发型师管理、服务项目、时段排班、预约单状态流转订单中心购物车、下单、订单状态机、支付回调、订单超时关闭营销中心优惠券、限时折扣、新人礼包、推荐关系绑定后台管理运营端统计、订单处理、商品录入、预约核销我用一个实际场景串起来给你看用户在小程序看到一张“烫染套餐 5 折券”领券后选择发型师阿杰的周六下午 2 点时段下预约单并支付支付成功后系统锁定时段、生成消费流水、给阿杰发待服务通知。这一连串动作里涉及营销、商品套餐、预约、订单、会员、通知至少 6 个模块所以千万不要把系统做窄了否则没法完整表达行业能力。2. 从配置文件到自动装配Spring Boot 核心机制怎么落地2.1 application.yml 多环境配置与自定义参数Spring Boot 的配置能力是项目的第一个“隐藏考点”。这个项目至少要拆三套环境本地开发dev、测试test、生产prod。做法是用application.yml做公共配置再用application-dev.yml、application-prod.yml做环境差异覆盖启动时加参数--spring.profiles.activedev切换。我的习惯是直接把数据库、Redis、文件上传路径这类容易变化的东西全拆到环境配置里公共配置只保留应用名、Jackson 时间格式、MyBatis 驼峰映射这类“到哪都一样”的项。比如application-dev.yml核心内容大概长这样server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hair_salon?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 data: redis: repositories: enabled: false mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 # 自定义配置本地上传路径与访问前缀 upload: path: ./upload/ url-prefix: /upload/**这里有两个值得你在答辩时讲两句的细节自定义配置项可以单独用一个配置类绑定读取干净用ConfigurationProperties(prefix upload)比到处写Value好用得多也方便做参数校验和默认值。MySQL 地址后面的serverTimezoneAsia/Shanghai千万别省否则日期字段容易差 8 个小时。这个问题十个人里能有八个人遇到。2.2 自动装配原理不是玄学是约定与条件判断很多人背了“Spring Boot 自动装配原理”面试题但到了自己项目里反而不理解为什么依赖一加就能用。这里我用这个美发商城的实际依赖给你顺一遍。以 MyBatis-Plus 为例你引入mybatis-plus-boot-starter后并没有手动声明SqlSessionFactory和MapperScannerConfigurer但 Mapper 接口却能直接注入。这是因为 starter 内部的MybatisPlusAutoConfiguration上有一堆条件注解ConditionalOnClass(SqlSessionFactory.class)只有 classpath 里存在 MyBatis 相关类才生效ConditionalOnSingleCandidate(DataSource.class)容器里只有一个数据源时生效如果有多个就要你手动指定AutoConfigureAfter(DataSourceAutoConfiguration.class)保证数据源先装配好再装配 MyBatis。这就是 Spring Boot 的核心设计约定大于配置 条件装配。说白了框架帮你在背后根据“环境里有什么”做决策。你如果自己写一个SqlSessionFactoryBean反而可能覆盖默认配置之后发现 Mapper 无法扫到。SpringBootApplication这个组合注解也值得说清楚它由三部分组成SpringBootConfiguration表明这是一个配置类EnableAutoConfiguration开启自动装配它通过AutoConfigurationImportSelector读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里声明的一堆自动配置类再按条件逐一判断是否生效ComponentScan扫描启动类所在包及其子包下的Component、Service、Controller等。所以启动类的位置很关键。我见过有人把启动类放在com.example.demo结果 Mapper 或 Controller 放在com.example.business运行起来 404原因就是ComponentScan默认扫不到外部的包。2.3 常用注解在项目里的实际用法Spring Boot 项目中注解的使用频率很高但很多同学的用法其实“能用但不够严谨”。我按使用场景整理一份注解使用位置核心作用RestControllerController 类组合了Controller和ResponseBody方法返回值直接序列化为 JSONRequestMapping类/方法映射 HTTP 路径类上写公共前缀比如/api/orderPostMapping/GetMapping方法限制请求方法语义更清晰RequestParam方法参数绑定查询参数可设置required、defaultValuePathVariable方法参数绑定路径参数比如/order/{id}RequestBody方法参数绑定 JSON 请求体到对象并触发 Jackson 反序列化Validated类上开启方法级参数校验Valid参数前触发 JSR-303 实体校验ServiceService 类声明业务层组件交给容器管理Transactional(rollbackFor Exception.class)Service 方法/类声明事务边界异常时回滚ConfigurationProperties配置类绑定前缀配置到属性对象可配合Validated校验Slf4j类Lombok 提供日志对象开发排查问题必备以一个查询订单分页列表为例把这些注解串起来Slf4j RestController RequestMapping(/api/order) Validated public class OrderController { Resource private OrderService orderService; GetMapping(/page) public ResultPageResultOrderVO page( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Integer status, RequestParam(required false) String keyword) { PageResultOrderVO result orderService.queryPage(pageNum, pageSize, status, keyword); return Result.ok(result); } PostMapping(/create) public ResultLong create(RequestBody Valid OrderCreateDTO dto) { Long orderId orderService.create(dto); return Result.ok(orderId); } }这段代码里有两个细节可以跟面试官聊一是 DTO 和 VO 要严格分离接收参数用 DTO返回给前端用 VO别直接把数据库实体丢出去否则容易把密码、余额等敏感字段暴露二是ResultT统一响应体配合全局异常处理器RestControllerAdvice前端只需要处理code、message、data三种结构联调体验会好很多。2.4 自定义启动 Banner一个提升细节感的小功能这个功能对系统没有实质影响但能让你在答辩演示时给人“这项目是真自己折腾过”的印象。Spring Boot 启动时会读取banner.txt你只需把它放到resources目录下即可。推荐用一些在线生成工具把项目名“Hair Salon System”转成 ASCII Art再把版本号、作者、端口放上去。控制台启动效果会明显不一样这个小细节也经常被做成博客文章讲。3. 核心业务模块设计会员、预约、商品、订单怎么串起来3.1 数据库设计思路与核心表清单数据库设计是这类项目最容易暴露短板的地方所以我单独拉一个章节来说。美发商城的核心表建议不少于 16 张我先列一个最小可用清单业务域表名关键字段用户t_userid, phone, password, nickname, avatar, role会员t_memberid, user_id, level, balance, points余额流水t_balance_logid, member_id, change_amount, balance_after, biz_type发型师t_stylistid, name, avatar, skill_list, status服务项目t_service_itemid, name, duration, price, description, cover商品 SPUt_productid, title, category_id, main_image, status商品 SKUt_product_skuid, product_id, spec_name, stock, price, image购物车t_cart_itemid, user_id, sku_id, quantity, checked预约时段t_appointment_slotid, stylist_id, service_item_id, slot_time, booked_status预约单t_appointmentid, order_no, user_id, stylist_id, slot_id, status订单t_orderid, order_no, user_id, total_amount, pay_status, status订单明细t_order_itemid, order_id, sku_id/service_item_id, price, quantity, item_type卡项t_card_templateid, name, type, face_value, price, valid_days用户卡项t_user_cardid, user_id, card_id, remain_count, expire_time优惠券模板t_coupon_templateid, name, threshold, discount, total_count用户优惠券t_user_couponid, user_id, template_id, status, use_time设计时有几个核心原则我踩过坑才真正明白订单号不要用数据库自增 ID要单独生成唯一业务号。可以采取“时间戳 随机数 业务前缀”的方式或者用 Redis 自增序列拼出更短的订单号。原因是自增 ID 容易暴露销量也容易被遍历爬数据。金额字段不要用double要用DECIMAL(10,2)。浮点数计算精度有损储值卡扣款一旦出现一分钱误差对账时会非常痛苦。所有表建议带create_time、update_time、deleted三个字段。MyBatis-Plus 的自动填充和逻辑删除都要靠它们。预约时段表要有唯一索引约束比如uk_stylist_slot(stylist_id, slot_time)防止并发下同一发型师同一时段被约两次。3.2 会员储值与余额流水别让用户的钱算错会员储值是美发行业的高频场景用户买了一张 2000 元的卡再送 300 元余额这种优惠规则很常见。系统设计上我的建议是储值行为本身创建一笔余额变动记录写入t_balance_log同时更新t_member.balance消费时反过来扣减余额并再记一条流水流水的biz_type区分“充值/消费/退款/赠送/调整”等业务动作方便财务对账。扣减余额时要注意并发。用户同时发起两笔消费如果只是先查余额再计算扣减结果然后 update就会发生超扣。解决办法有三个一是 SQL 层扣减UPDATE t_member SET balance balance - #{amount} WHERE id #{id} AND balance #{amount}靠UPDATE返回影响行数判断是否扣成二是用乐观锁版本号三是 Redis 分布式锁。最稳妥的还是第一种因为它没有中间态。3.3 预约与时段管理这是最有行业特色的模块预约模块是这个项目最值得展开讲的部分因为它和普通商城差异最大。我的设计思路是这样的t_stylist表维护发型师列表每个发型师有擅长项目字段排班时由后台管理功能基于“营业时间 项目时长”生成对应的时段例如营业时间是 10:00-20:00剪发项目时长 30 分钟则生成 10:00、10:30、11:00……直到 19:30用户预约时锁定一个时段booked_status从 0 改为 1并在预约单里记录用户与发型师支付成功之后才真正锁定未支付订单超过 15 分钟则释放时段。这里有个设计难点生成时段时如果同一个发型师同时可以被不同类型项目约时长不一样时段表格粒度就要很细。最简单的方式是按最小单位 15 分钟生成槽位一天 10 小时就是 40 个槽位一个发型师一天 40 条记录10 个发型师 400 条数据量完全可控。锁定时段时用UPDATE t_appointment_slot SET booked_status 1 WHERE id ? AND booked_status 0返回 1 才表示抢到了不要先查再改。3.4 商品订单流程从购物车到订单状态机商品订单这块跟普通商城大同小异核心是把状态机理清楚。我在项目里定义的订单状态流是待支付(0)-已支付待发货(1)-已发货(2)-已完成(3)另外有已取消(4)、退款中(5)、已退款(6)。每个状态变更都记录到t_order_log方便后期排查“订单为什么走到了这个状态”。下单流程要防止超卖我的做法是用户提交订单 DTO 后先加 Redis 分布式锁以 SKU ID 作为 key防止同一商品同时扣库存查询并锁定 SKU 行数据判断库存是否足够扣减库存并创建订单主表与明细表释放锁返回订单号。库存扣减用UPDATE t_product_sku SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}来保证原子性。至于下单时如果购物车里有多个 SKU涉及多把锁的获取顺序需要统一先按 SKU ID 排序再加锁不然两个订单互相等待就容易死锁。这个点写成简历上的“解决高并发库存扣减问题”会比单纯说用了 Redis 有说服力得多。4. 关键技术实战登录鉴权、文件上传、接口文档和异步通知4.1 JWT Spring Security 实现登录鉴权与授权美发商城的用户类型至少有三种普通用户、发型师、管理员。所以认证授权这块不能只做一个简单的登录。我的落地方案是用户登录时校验手机号和密码成功以后生成 JWT Token把userId、role放进 Token Claims前端把 Token 放在请求头Authorization: Bearer token后端用 Spring Security 过滤器链统一校验自定义一个JwtAuthenticationFilter从请求头解析 Token构造UsernamePasswordAuthenticationToken放进SecurityContextHolder通过PreAuthorize(hasRole(ADMIN))控制接口权限比如后台管理的商品接口只有管理员能调。这里最容易被卡住的是 Swagger 放行问题。默认情况下 Spring Security 会把/swagger-ui/**、/v3/api-docs/**全部拦截导致接口文档没法访问。解决方式有两种在 SecurityConfig 中直接放行相关路径使用 SpringDoc 文档中提供的安全配置让 Swagger 页面直接携带 Token 调试。如果你的版本是 Spring Boot 2.7 Spring Security 5.x建议用SecurityFilterChain这种 Bean 方式配置别再用已经废弃的WebSecurityConfigurerAdapter否则一启动就会提示方法已过时。4.2 文件上传与静态资源映射用户头像与商品图怎么存商品图片、用户头像上传是这个项目的刚需功能。小体量项目最省事的方案是“本地磁盘存储 静态资源映射”不需要引入 OSS。后端接收文件的接口大概是PostMapping(/api/file/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { throw new BizException(上传文件不能为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String filename UUID.randomUUID().toString().replace(-, ) ext; File dest new File(uploadProperties.getPath() filename); try { file.transferTo(dest); return Result.ok(/upload/ filename); } catch (IOException e) { throw new BizException(文件上传失败); } }上传后还需要让前端能访问图片两种方式在 WebMvc 配置里加资源映射Configuration public class WebMvcConfig implements WebMvcConfigurer { Resource private UploadProperties uploadProperties; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(uploadProperties.getUrlPrefix()) .addResourceLocations(file: uploadProperties.getPath()); } }也可以放在 Nginx 层做映射适合部署到云服务器之后用。这里要特别提醒上传目录别放在项目运行目录内否则每次重新部署尤其是 Docker 重建容器文件就丢了。生产环境里把上传路径挂载到宿主机数据卷是一个必须考虑的细节。如果需求里说可能要上传视频类大文件那就要考虑分片上传、断点续传、秒传校验这类方案了核心是前端把文件切片后端合并并用 Redis 记录已上传分片这块做起来又是一个完整专题这里先不展开。4.3 聚合接口文档SpringDoc 替代旧版 Swagger这两年 Spring Boot 3 的普及加上 Springfox 停更新项目已经不再推荐springfox-swagger2我更愿意用springdoc-openapi。它在 Spring Boot 2.x 项目中依赖springdoc-openapi-uiSpring Boot 3 项目中依赖springdoc-openapi-starter-webmvc-ui版本要选对否则起不来。基础配置springdoc: api-docs: path: /v3/api-docs swagger-ui: path: /swagger-ui.html tags-sorter: alpha operations-sorter: method然后在接口上用注解补充信息Tag说明模块Operation说明方法作用Parameter描述参数。做完了以后前后端联调效率会提升很多前端同学自己就能在 Swagger 页面上试所有接口不需要你反复截图。4.4 用消息队列解耦异步通知与订单超时这个项目我建议至少引入一个消息队列不是因为“炫技”而是确实有适合异步的场景用户下单成功后异步通知仓储/店长发消息预约确认后异步发送短信或站内信通知用户未支付订单超时关闭用一个延迟消息来处理。技术选型上如果项目部署环境单纯消息量不大用 RabbitMQ 就够了如果你后续想往高并发方向吹一吹项目就可以考虑 RocketMQ因为它自带延迟消息等级消费性能和事务消息机制更强简历上也更亮眼。配置方法大同小异都是引入 starter、配置连接信息、定义队列和交换机、写生产者消费者。下面是一个 RabbitMQ 延迟队列的简化思路用户创建预约单后发送一条延迟消息到order.delay.queueTTL 设置为 15 分钟消息到期后自动路由到order.cancel.queue消费者查询对应订单状态如果是待支付就执行取消并释放预约时段。这样避免写定时任务轮询全表性能更好业务也解耦。如果不想上消息队列复杂配置先用 Spring 的Scheduled每分钟扫一次超时订单也能顶住开发压力但实现优雅程度就差了一截。5. 从本地到 Docker环境搭建与部署上线全流程5.1 本地开发环境怎么搭才不费劲要说初学者最常卡住的点反而不是业务代码而是环境。我这个项目的本地环境清单是组件版本建议作用JDK1.8 或 11Spring Boot 2.7 建议 8/11编译与运行Maven3.6依赖管理MySQL5.7 或 8.0主数据库Redis6.x缓存、分布式锁、Token 黑名单IDEA2023.x开发工具创建的步骤我就不一步步截图了重点说几个最容易出错的地方Maven 镜像建议配置阿里云仓库否则依赖下载能让人等到怀疑人生创建项目时选择依赖最好一次性加齐Spring Web、Validation、MySQL Driver、MyBatis-Plus这个 IDEA 初始列表里没有需要手动在pom.xml中加、Redis、Lombok后面再补容易因为版本兼容问题折腾半天如果你用hanlp这类 NLP 依赖做智能搜索或者集成agentscope这类 AI 能力依赖版本冲突的可能性会很高建议用mvn dependency:tree排查冲突避免出现 NoClassDefFoundError。5.2 用 Docker Compose 一键启动全套服务部署本地和部署服务器我推荐用 Docker Compose 把 MySQL、Redis、后端服务编排起来。这样做的好处是环境一致性好到答辩现场一台新机器也能快速复现项目。我贴一个简化版的docker-compose.ymlversion: 3 services: mysql: image: mysql:8.0 container_name: hair-mysql restart: always environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: hair_salon ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql - ./sql:/docker-entrypoint-initdb.d command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci redis: image: redis:6.0 container_name: hair-redis restart: always ports: - 6379:6379 app: build: . container_name: hair-app restart: always ports: - 8080:8080 depends_on: - mysql - redis environment: SPRING_PROFILES_ACTIVE: prod volumes: - ./upload:/upload后端服务的 Dockerfile 写法也很常规FROM maven:3.8-jdk-8 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM openjdk:8-jre-slim WORKDIR /app COPY --frombuild /app/target/hair-salon.jar ./app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]这里把构建阶段和运行阶段拆开也就是多阶段构建目的是让最终镜像只保留运行环境和 Jar 包镜像体积会小很多。上传目录挂载到宿主机的./upload这样容器重建不会丢图片。5.3 部署之后必须做的检查项上服务器以后有些问题本地永远测不出来。我列一下我每次部署完都会检查的清单数据库时区是否与服务器一致查询时间是否差 8 小时Redis 密码是否配置如果不配置会被扫描爆破Swagger 是否只在内网开放生产环境建议关闭或加权限JVM 内存参数是否合理小服务器上用-Xmx256m控制占用日志是否持续输出到文件方便tail -f排查问题。6. 常见问题与排查:那些我踩过、也看别人踩过的坑这节我来集中分享一些高频问题都是这类项目几乎必踩的坑最好先收藏等到报错了再来对照。问题现象常见原因解决方式启动报Field userMapper in ... required a bean of typeMapper 接口没有被扫描到在启动类加MapperScan或检查ComponentScan的包路径访问接口返回 404Controller 包不在启动类子包下统一把业务包放在启动类所在包内实体里的字段查询出来全是 null驼峰映射未开启或数据库字段不一致确认配置map-underscore-to-camel-case: true登录接口报 401但 Swagger 能访问Security 拦截了放行之外的请求检查securityMatcher路径和 JWT 过滤器顺序JSON 序列化报could not initialize proxy懒加载导致会话关闭用 DTO/VO 手动填充字段避免直接返回实体两个订单同时购买同一商品库存变成负数超卖用UPDATE ... WHERE stock #{quantity}原子扣减本地图片能访问服务器上图片挂掉上传路径未挂载或权限不对配置 Docker 数据卷检查目录可写权限启动很慢或连接redis超时Redis 未启动或配置错误检查环境配置确认spring.redis.host是否正确事务没生效方法被非 Spring 代理调用或异常被吞了事务方法必须通过代理对象调用rollbackFor要指定我重点展开两个最典型的因为几乎每次带同学调试项目都会遇到。第一个是required a bean of type XxxMapper。这个报错初看是“缺 Bean”实际上是扫描问题。你引入 MyBatis-Plus 后Mapper 接口的扫描不是靠 Spring 默认组件扫描完成的而要靠Mapper注解或者启动类上的MapperScan(com.xxx.mapper)显式声明。如果你的启动类包路径写得太上层或者忘了加注解就会出现这个错。排查思路是先确认 Mapper 接口上有Mapper注解再看MapperScan的包路径最后由debugtrue查看自动装配报告。第二个是事务失效。项目里常见写法是OrderServiceImpl.create()方法加了Transactional然后内部调用了同类里的另一个方法比如this.updateStock(...)。因为 Spring 默认通过 AOP 代理实现事务this调用走的是普通方法调用不会触发代理逻辑所以第二个方法里的事务不起作用。解决方案是拆到不同 Service 中调用或者把第二个方法也放到代理入口才能实现事务嵌套最实用的方案是把库存扣减和订单创建都放在同一个Transactional方法里让它们在同一个事务中执行而不是拆成两个方法互相调用。7. 关于项目扩展和打磨的一些个人建议主体功能做完以后不要立刻停下这个项目还能往几个方向打磨每一处都能成为简历或答辩的亮点引入 Redis 缓存商品详情和首页数据缓存穿透、击穿、雪崩的应对策略都写进项目笔记面试官问起来有实例支撑用 Redis 实现接口幂等性设计下单接口防止用户重复提交金额一致时直接返回旧订单用 Spring Boot 的定时任务或消息队列实现订单超时关闭这种“延迟任务”是面试高频题如果想把 AI 能力加进去可以考虑对接大模型 API 做智能发型推荐用户上传照片后分析脸型推荐发型和服务项目这个功能非常加分但要注意依赖注入和异步调用的稳定性配置后台统计报表用 ECharts 展示营业额趋势、办卡转化率、发型师工作量数据从订单表和预约表聚合汇总。我在实际做这类项目时的体会是真正让你成长的不是跑通一个 CRUD而是你开始思考“为什么会这样设计”“如果并发高了怎么办”“换一种中间件行不行”。这些思考过程才是 Spring Boot 项目真正的加分项。如果你现在还在起步阶段先把最基本的注册、登录、商品展示、下单流程跑通再逐步加中间件和优化不要想着一口气吃成胖子。每次加一个组件就写一节笔记等答辩时你会发现这个项目已经远不止“一个商城系统”那么简单了。