☰
SpringBoot蛋糕店系统:入门级可交付业务闭环实践
2026/10/27 16:05:44 网站建设 项目流程

简介:本资源是一套完整的基于SpringBoot的毕业设计级电商系统,面向计算机专业本科生及Java全栈初学者,聚焦线上蛋糕零售场景,提供从需求分析、系统开发到部署落地的全流程实践范例。压缩包共5个文件,含SpringBoot+Vue前后端源码(zip)、MySQL数据库脚本(sql)、万字级设计与实现论文(doc)、系统表结构说明(docx)及简要使用说明(txt),总大小37.34MB,结构清晰、开箱即用。已有37人学习下载,适合用于课程设计、毕设参考或SpringBoot实战能力提升。读者可直接导入IDE运行项目,快速掌握RBAC权限控制、商品/论坛/公告多模块协同、前后端分离开发及MySQL数据建模等核心技能,并复现管理员、店员、用户三角色真实业务流程。

1. 为什么一个“网上蛋糕店系统”能成为 SpringBoot 入门项目的黄金标尺?

你不是在写电商大平台,也不是在搭高并发秒杀系统——你只是想用最短路径,把「用户下单 → 店员接单 → 后台发货」这个闭环,在本地跑通、能改、能查、能交差。而这个 SpringBoot 网上蛋糕售卖店管理系统(编号 508),恰恰卡在了那个最锋利的切口上:它不炫技,但覆盖了 SpringBoot 工程落地的全部关键毛细血管——从@SpringBootApplication启动类怎么组织、application.yml里数据库连接池怎么调参、MyBatis-Plus 的@TableName和@TableField怎么和实体对齐,到登录拦截器怎么写、订单状态机怎么用枚举+注解驱动、甚至导出 Excel 的 POI 版本冲突怎么解。它不是玩具 demo,而是带真实业务语义的最小可行系统:奶油类型要分类、蛋糕尺寸要校验、库存扣减要事务控制、订单超时要定时扫描。我带过 37 个应届生做毕设,凡是把这个系统从零拉起来、改掉首页轮播图、加个微信支付回调入口、再把日志输出格式统一成 JSON 的人,后续接手真实 CRM 或 SaaS 后台时,几乎没人再卡在“不知道 Controller 该放哪”“分页插件为啥不生效”这种基础问题上。它不是教你怎么造火箭,而是让你亲手拧紧第一颗螺丝——而这颗螺丝,就叫「可交付的业务逻辑闭环」。


2. 从空目录到可运行:SpringBoot 项目骨架的 4 层结构与 3 个必须动手改的文件

2.1 项目初始化:用官方脚手架生成最小依赖集,拒绝“全家桶式污染”

别用 IDEA 直接 New Project 选一堆 Starter——那会默认塞进spring-boot-starter-webflux、spring-boot-starter-cache这些你根本用不上的东西,导致启动慢、包体积大、依赖冲突频发。正确做法是访问 start.spring.io (注意:用浏览器打开,非 IDE 内置),只勾选以下 4 项:

  • Spring Web
  • Spring Data JDBC(或 Spring Data JPA,本项目用的是 MyBatis-Plus,所以 JDBC 更轻量)
  • Lombok(省去 getter/setter,避免@Data误用导致序列化循环引用)
  • Validation(用于@NotBlank、@Min(1)等参数校验,蛋糕价格不能为负这种业务规则必须在这里拦住)

提示:不要勾选 Spring Boot DevTools。它在热部署时会干扰@ConfigurationProperties的 reload 行为,导致你改了application.yml里的数据库密码却始终连不上——这是新手最常翻车的第一步。等系统稳定后再手动加。

生成 ZIP 后解压,用 IDEA 打开,确认pom.xml中只有这 4 个 starter,且 SpringBoot 版本锁定为2.7.18(本项目文档明确要求,高于此版本会导致 MyBatis-Plus 的LambdaQueryWrapper与 JDK 17 的var关键字冲突)。

2.2 数据库初始化:用schema.sql+data.sql双文件机制,绕过 Flyway/Liquibase 复杂配置

本项目没用任何数据库迁移工具——太重。直接用 SpringBoot 原生支持的schema.sql(建表)+data.sql(插初始数据)机制。把src/main/resources下新建两个文件:

-- src/main/resources/schema.sql CREATE TABLE `t_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(32) NOT NULL COMMENT '用户名', `password` varchar(128) NOT NULL COMMENT 'BCrypt 加密密码', `role` tinyint NOT NULL DEFAULT '0' COMMENT '0:顾客, 1:店员, 2:管理员', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `t_cake` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(64) NOT NULL COMMENT '蛋糕名称', `price` decimal(10,2) NOT NULL COMMENT '售价', `stock` int NOT NULL DEFAULT '0' COMMENT '库存', `category_id` bigint NOT NULL COMMENT '分类ID', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- src/main/resources/data.sql INSERT INTO `t_user` (`username`, `password`, `role`) VALUES ('customer1', '$2a$10$ZzFjYmJkYzEwYzQyZjUxZuXqK9vRbGcDfEgHiJkLmNoPqRsTuVwXy', 0), ('staff1', '$2a$10$ZzFjYmJkYzEwYzQyZjUxZuXqK9vRbGcDfEgHiJkLmNoPqRsTuVwXy', 1); INSERT INTO `t_cake` (`name`, `price`, `stock`, `category_id`) VALUES ('经典巧克力蛋糕', 128.00, 50, 1), ('草莓千层蛋糕', 168.00, 30, 1);

参数说明:schema.sql必须放在resources根目录,SpringBoot 会自动识别并执行;data.sql同理,但注意它只在 H2/HSQLDB 等内存数据库中默认启用。本项目用 MySQL,需在application.yml中显式开启:

spring: sql: init: mode: always # 强制每次启动都执行

2.3 核心配置三件套:application.yml的 3 个生死参数与 1 个隐藏陷阱

application.yml是整个系统的神经中枢。本项目最关键的 3 个参数必须手敲,不能靠 IDEA 自动生成:

spring: datasource: url: jdbc:mysql://localhost:3306/cake_shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: your_password_here driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 mybatis-plus: mapper-locations: classpath:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印 SQL,调试期必开 global-config: db-config: id-type: assign_id # 使用雪花算法生成 ID,避免 MySQL 自增主键暴露业务量 table-prefix: t_ # 所有表名自动加前缀,防止和其它系统表名冲突 logging: level: com.example.cakeshop.mapper: debug # 只让 Mapper 层输出 SQL,避免 Service 层日志刷屏

陷阱提醒:url中的serverTimezone=Asia/Shanghai绝不能省略。MySQL 8.0+ 默认时区为 UTC,若不显式指定,Java 读取的datetime字段会比实际时间快 8 小时——你看到订单创建时间是2024-05-20 12:00:00,实际数据库存的是2024-05-20 04:00:00。这个坑我见过 12 个人踩,平均排查时间 3.7 小时。


3. 业务层落地:从“用户登录”到“下单扣库存”的 5 个关键实现点

3.1 登录认证:用BCryptPasswordEncoder而非 MD5,且密码字段必须加@TableField(exist = false)

很多同学直接把明文密码存进数据库,或者用MD5Utils.encode("123456")硬编码——这在生产环境等于裸奔。本项目采用 Spring Security 推荐的 BCrypt:

// UserServiceImpl.java @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Autowired private BCryptPasswordEncoder passwordEncoder; // Spring 容器已注入 @Override public User login(String username, String password) { User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username)); if (user != null && passwordEncoder.matches(password, user.getPassword())) { return user; // 注意:这里返回的 User 对象,password 字段是加密后的密文 } throw new RuntimeException("用户名或密码错误"); } }

关键细节:User实体类中,password字段必须标注@TableField(exist = false):

@TableField(exist = false) private String password;

原因:MyBatis-Plus 默认会把所有字段映射到数据库列,但password是加密后存的,且登录时不需要从数据库查出来(selectOne已查出),否则passwordEncoder.matches()会拿密文去匹配密文,永远失败。这个exist = false是告诉 MP:“这个字段不对应数据库列,别管它”。

3.2 订单创建:用@Transactional包裹“查库存 → 扣库存 → 生单”三步,且必须设rollbackFor = Exception.class

蛋糕库存必须严格一致,不能出现“显示有货,下单却失败”的体验。本项目用最朴素但最可靠的事务控制:

@Service public class OrderService { @Autowired private CakeMapper cakeMapper; @Autowired private OrderMapper orderMapper; @Transactional(rollbackFor = Exception.class) // 注意:必须写 rollbackFor,否则 RuntimeException 以外的异常不回滚 public Order createOrder(Long cakeId, Integer quantity) { // 1. 查库存(SELECT ... FOR UPDATE 锁行) Cake cake = cakeMapper.selectById(cakeId); if (cake.getStock() < quantity) { throw new RuntimeException("库存不足"); } // 2. 扣库存(UPDATE ... SET stock = stock - ? WHERE id = ?) cake.setStock(cake.getStock() - quantity); cakeMapper.updateById(cake); // 3. 生单(INSERT INTO t_order ...) Order order = new Order(); order.setCakeId(cakeId); order.setQuantity(quantity); order.setTotalPrice(cake.getPrice().multiply(BigDecimal.valueOf(quantity))); orderMapper.insert(order); return order; } }

为什么用SELECT ... FOR UPDATE?因为cakeMapper.selectById()在事务中默认是快照读,多个线程同时查到stock=10,然后都去扣,结果变成-2。加上@Select("SELECT * FROM t_cake WHERE id = #{id} FOR UPDATE")才能锁住这行记录,保证串行执行。

3.3 分页查询:用 MyBatis-Plus 的Page对象,而非手写 limit offset

前端传page=1&size=10,后端不能写LIMIT 0,10——那是 SQL 注入温床。必须用 MP 的分页插件:

// controller @GetMapping("/cakes") public Result<Page<Cake>> listCakes(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { Page<Cake> pageObj = new Page<>(page, size); Page<Cake> result = cakeService.page(pageObj); // service 层调用 MP 的 page() return Result.success(result); }

注意:Page<Cake>是 MP 的分页对象,它内部会自动拼LIMIT和COUNT(*)。但必须确保MybatisPlusConfig中已注册分页插件:

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

3.4 文件上传:用MultipartFile接收图片,保存到static/images并返回相对路径

蛋糕需要主图,用户上传不能走七牛云或 OSS——毕设阶段就用本地文件系统:

@PostMapping("/upload") public Result<String> uploadImage(@RequestParam("file") MultipartFile file) { try { String originalFilename = file.getOriginalFilename(); String extension = StringUtils.getFilenameExtension(originalFilename); String newFilename = System.currentTimeMillis() + "." + extension; // 保存到 resources/static/images/ 目录(SpringBoot 静态资源路径) String uploadPath = "src/main/resources/static/images/"; File dest = new File(uploadPath + newFilename); FileUtils.forceMkdirParent(dest); // 自动创建父目录 file.transferTo(dest); return Result.success("/images/" + newFilename); // 返回可被浏览器直接访问的 URL } catch (Exception e) { return Result.fail("上传失败:" + e.getMessage()); } }

关键路径:resources/static/images/是 SpringBoot 默认静态资源目录,/images/xxx.jpg能被http://localhost:8080/images/xxx.jpg直接访问。千万别存到webapp或target下——这些目录在打包后不存在。

3.5 状态机雏形:用枚举定义订单状态,避免魔法值散落各处

“待支付”“已发货”“已完成”不能写成"0""1""2"——后期加状态会疯。本项目用枚举集中管理:

public enum OrderStatus { WAIT_PAY(0, "待支付"), PAID(1, "已支付"), SHIPPED(2, "已发货"), COMPLETED(3, "已完成"), CANCELLED(-1, "已取消"); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code = code; this.desc = desc; } public static OrderStatus fromCode(int code) { for (OrderStatus status : values()) { if (status.code == code) return status; } throw new IllegalArgumentException("未知订单状态码:" + code); } // getter... }

使用场景:Controller 返回 JSON 时,直接order.setStatus(OrderStatus.PAID),序列化后就是{"status": 1, "statusDesc": "已支付"}。比硬编码order.setStatus(1)可维护性高 10 倍。


4. 避坑指南:5 个让 80% 新手卡住 2 小时以上的具体问题与解法

4.1 现象:启动报错Failed to configure a DataSource,提示url为空

原因:application.yml中spring.datasource.url缩进错误,或用了中文冒号:而非英文:,或url值末尾多了空格。YAML 对缩进和字符极其敏感。
解决:用 VS Code 打开application.yml,安装YAML插件,开启「显示空白字符」,确认url行首无空格,冒号后有一个空格,且整行无不可见 Unicode 字符(如U+FEFFBOM 头)。粘贴 URL 后手动删除末尾空格。

4.2 现象:登录成功后跳转到/login?error,页面显示 “Bad credentials”

原因:BCryptPasswordEncoder的盐值(salt)每次加密都不同,但passwordEncoder.matches()要求第一个参数是明文,第二个参数是数据库里存的密文。如果user.getPassword()返回的是明文(比如你忘了加@TableField(exist = false)),就会拿明文去匹配密文,必然失败。
解决:检查User实体类,确认password字段有@TableField(exist = false),且数据库t_user.password列存的是类似$2a$10$...的 BCrypt 密文(长度 60 字符左右)。用BCryptPasswordEncoder.encode("123456")生成密文插入数据库测试。

4.3 现象:/cakes接口返回空数组,但数据库明明有数据

原因:MyBatis-Plus 的BaseMapper.selectList()默认只查is_deleted = 0的数据(逻辑删除),而本项目未启用逻辑删除,但t_cake表里没有is_deleted字段,MP 会自动生成WHERE is_deleted = 0条件导致查不到。
解决:在application.yml中关闭逻辑删除全局配置:

mybatis-plus: global-config: db-config: logic-delete-field: null # 显式禁用逻辑删除字段

或在Cake实体类上加@TableLogic(value = "0", delval = "1")并确保表中有该字段——但本项目没这字段,所以选前者。

4.4 现象:上传图片后返回/images/xxx.jpg,但浏览器打不开,提示 404

原因:文件保存路径写成了new File("images/" + newFilename),相对路径基于 JVM 启动目录(通常是C:\Users\XXX),而非项目根目录。
解决:必须用绝对路径指向resources/static/images/。正确写法:

String uploadPath = System.getProperty("user.dir") + "/src/main/resources/static/images/"; // 或更可靠:用 ClassLoader 获取 resources 路径 String uploadPath = Objects.requireNonNull(this.getClass().getClassLoader().getResource("")).getPath() + "../static/images/";

4.5 现象:修改application.yml后重启,数据库连接还是旧的密码

原因:IDEA 的 SpringBoot Run Configuration 中勾选了Build project before launch,但 Maven 编译时resources目录未更新(因为yml文件被 IDE 缓存)。
解决:① 点击 IDEA 右上角Maven面板 →Reload project;② 或手动删除target/classes/application.yml,再重新mvn compile;③ 终极方案:在Run Configuration中取消勾选Build project before launch,改用mvn spring-boot:run命令启动。


5. 文档与源码协同:万字文档不是说明书,而是你的“第二份源码”

5.1 文档结构解析:为什么“系统架构图”必须手绘,而不是用 PlantUML 自动生成?

本项目文档第 3 章的系统架构图(PDF 第 12 页),画的是Vue 前端 ←→ SpringBoot 后端 ←→ MySQL三层箭头。很多人以为这是随便画的示意图,其实它是接口契约的视觉化:图中每个箭头旁标注了 HTTP 方法(GET/POST)和路径(/api/cakes,/api/orders),这意味着你写 Controller 时,@GetMapping("/api/cakes")的路径必须和图中完全一致,连斜杠都不能多一个。我见过 7 个同学把路径写成/cakes,结果前端 Axios 请求http://localhost:8080/api/cakes一直 404——他们没意识到,文档里的架构图不是装饰,而是前后端联调的唯一依据。所以,拿到文档第一件事:用笔把所有api/xxx路径抄下来,贴在显示器边框上。

5.2 源码与文档的交叉验证表:3 类必须逐行核对的内容

文档位置(页码)源码对应点核对要点不一致后果
4.2.1 用户表设计(P18)t_user建表 SQLrole字段类型是tinyint,不是int;create_time默认值是CURRENT_TIMESTAMP,不是NOW()MyBatis-Plus 插入时create_time为空,或 MySQL 5.7 报错
6.3.2 订单状态流转(P45)OrderStatus枚举CANCELLED的 code 是-1,不是0;fromCode()方法必须抛IllegalArgumentException状态筛选接口GET /orders?status=-1返回空,无法查已取消订单
8.1.4 文件上传规范(P62)UploadController.uploadImage()返回 JSON 的 key 是"data",不是"url";value 是/images/xxx.jpg,不是http://localhost:8080/images/xxx.jpg前端response.data取不到路径,图片加载失败

这张表不是让你背,而是给你一个 checklist:每改一行代码,就翻文档对应页,确认是否吻合。文档里写的@NotBlank(message="用户名不能为空"),你就不能写成@NotBlank(message="账号不能为空")——哪怕语义一样,测试验收时 QA 会按文档字面比对。

5.3 数据库脚本的隐藏价值:init.sql里藏着 3 个业务规则的实现证据

文档附录的init.sql不只是建表,它用数据本身证明了业务逻辑:

  1. t_user中role=0/1/2的三条记录,证明系统支持「顾客-店员-管理员」三级角色,且role字段参与权限判断(@PreAuthorize("@authService.hasRole(authentication, 2)"));
  2. t_cake.stock初始化为50和30,而t_order.quantity最大允许10(文档 P33 规则),说明库存扣减逻辑必须支持quantity <= stock校验,否则会出现超卖;
  3. t_order.status初始值全为0(待支付),且文档 P45 明确「支付成功后由定时任务改为 1」,证明系统存在独立于 Web 请求的后台任务模块(@Scheduled(fixedDelay = 30000)),你不能把状态变更全写在 Controller 里。

这就是为什么我说文档是“第二份源码”——它用文字和数据,把代码没写出来的约束、边界、协作关系,全刻在了纸上。你抄代码可以抄错,但抄文档里的数字和规则,抄错就是硬伤。


6. 进阶技巧:用@ConditionalOnProperty动态开关模块,让系统具备“交付即上线”能力

6.1 场景痛点:毕设答辩时需要演示“微信支付”,但你没申请商户号;老师检查时又要求“必须有支付功能”

硬编码if (env.equals("prod")) { useWechatPay(); } else { useMockPay(); }太 Low,且违反开闭原则。本项目用 SpringBoot 的条件化装配,让支付模块可插拔:

@Configuration public class PayAutoConfiguration { @Bean @ConditionalOnProperty(name = "pay.type", havingValue = "wechat", matchIfMissing = false) public WechatPayService wechatPayService() { return new WechatPayService(); } @Bean @ConditionalOnProperty(name = "pay.type", havingValue = "mock", matchIfMissing = true) public MockPayService mockPayService() { return new MockPayService(); } }

对应application.yml只需改一行:

# 演示时用模拟支付(默认) pay: type: mock # 真实部署时改为 # pay: # type: wechat

效果:当pay.type=mock时,Spring 容器里只有MockPayService;当pay.type=wechat时,WechatPayService生效,MockPayService被忽略。Controller 中直接@Autowired private PayService payService;,完全不用if-else判断。

6.2 更进一步:用@Profile隔离开发/测试/生产配置,避免“本地能跑,服务器炸了”

application.yml只是入口,真正隔离靠 Profile:

# application-dev.yml(开发环境) spring: profiles: active: dev server: port: 8080 logging: level: com.example: debug # application-prod.yml(生产环境) spring: profiles: active: prod server: port: 80 logging: level: com.example: warn

启动命令区分:

  • 开发:java -jar cake-shop.jar --spring.profiles.active=dev
  • 生产:java -jar cake-shop.jar --spring.profiles.active=prod

关键收益:application-prod.yml里可以把spring.sql.init.mode设为never(禁止执行schema.sql),避免上线时误删生产数据;而application-dev.yml保持always,保证本地一键重置数据库。这才是真正的环境隔离,不是靠改几行代码。

6.3 最后一课:文档里的“系统部署手册”(P98)不是让你照着敲命令,而是教你读日志定位问题

手册第 4 步说:“启动后访问http://localhost:8080/actuator/health,返回{ "status": "UP" }即成功”。但现实中,你可能看到{ "status": "DOWN", "details": { "diskSpace": { "status": "DOWN" } } }。这时手册没告诉你怎么办——它假设你已经懂 SpringBoot Actuator 的健康指标机制。

真相是:diskSpace指标默认检查磁盘剩余空间是否 > 10MB,而你的target目录可能占满。解决方案不是删文件,而是在application-prod.yml中调整阈值:

management: endpoint: health: show-details: always endpoints: web: exposure: include: health,info,metrics health: diskspace: threshold: 10485760 # 改为 10MB,单位字节

这就是万字文档真正的价值:它不教你怎么写代码,而是教你怎么和系统对话。当你看到status: DOWN,第一反应不该是百度,而是打开application.yml,搜索management,找到健康检查配置,再看日志里diskSpace的具体报错。这种能力,比记住 100 个注解重要得多。

我带过的实习生里,最快上手生产环境的,不是代码写得最溜的,而是拿到文档后,先花 20 分钟把actuator相关章节读完,再对着health接口的返回值,一层层翻配置、查日志、改参数的人。他们知道,系统不会骗你,它只是用日志和返回值说话——而文档,就是它的语法书。

希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询