好久没写这类业务系统了,上次帮人做蛋糕店的点单管理,被店里那本手写登记册折腾得够呛:客户电话、蛋糕口味、取货时间全挤在一个本子上,节假日高峰期更是找不到北。后来我用 Spring Boot 从零搭了一套蛋糕售卖管理系统,从用户下单到后台接单、库存预警、销量统计一条龙跑通,还顺手整理了完整的设计文档和答辩 PPT。这次就借着这个项目,把从需求到落地的完整思路、核心代码片段和踩坑经验全部聊透。
这个系统适合谁?两类人最对口:一是刚学完 Java Web 基础、准备做项目练手的同学,二是正在准备相关项目展示或答辩、需要一套能讲清楚业务逻辑的案例的人。不夸张地说,把这一套吃透,你对 Spring Boot 单体应用的理解能比刷一百遍教程都深刻。
1. 项目设计与技术选型思路
1.1 为什么选 Spring Boot 做单体应用
刚开始拿到“蛋糕售卖管理系统”这个需求时,你可能觉得这就是个普通的 CRUD 项目。但往深了想,它牵扯到用户登录、商品展示、购物车、订单状态流转、库存扣减、图片上传、统计报表,业务完整度比单纯的“学生管理系统”高不少。技术选型上,Spring Boot 几乎是这个场景最稳的答案:它把繁琐的配置全部自动搞定,内嵌容器直接跑,开发效率高;生态齐全,MyBatis、Spring Data JPA、安全框架随时能接进来;同时它还是当前企业里使用率最高的 Java 框架之一,做完这个项目,简历上写“熟悉 Spring Boot 开发”是有真实支撑的。
1.2 技术栈清单与取舍逻辑
这套系统我采用的是经典的“Spring Boot + MyBatis-Plus + MySQL + Thymeleaf + Bootstrap + ECharts”组合。很多人会问,为什么不用前后端分离的 Vue + Spring Boot?确实,现在企业项目里前后端分离是主流,但作为一个售卖管理系统,我想让它在答辩演示时不折腾:用 Thymeleaf 做服务端渲染,前端页面和后端接口在同一个 web 工程里,部署起来就是一个 jar 包,打开浏览器输入域名就能直接看到页面效果,不用再单独启一个 Nginx 或者 Node 服务。这在实际业务里也常见——内部管理系统、中小型电商运营后台,服务端模板渲染依然有大量场景在用。
具体来说:
- Spring Boot 3.2 / 2.7 都可以,我用的是 2.7 系列,避免高版本带来的一些兼容坑。
- MyBatis-Plus做持久层,单表操作不用写 SQL,分页插件和代码生成器能省掉一堆重复劳动。
- MySQL 8.0做数据存储,主流版本,性能和稳定性都有保证。
- Thymeleaf渲染页面,配合 Bootstrap 把页面搭得工整好看。
- ECharts做管理员端的销量趋势图、分类占比图,视觉效果直接拉满。
这里想特别说一下为什么选 MyBatis-Plus 而不是原生 MyBatis。我的体会是:开发生库、字段多的时候,原生 MyBatis 光是写 resultMap 和 CRUD 语句就要占掉一半时间。MyBatis-Plus 提供了BaseMapper,常用的增删改查直接继承就有,复杂的多表查询还能用它自带的Wrapper构造条件,配合 Lambda 表达式写起来非常直观。团队里有成员刚入门的话,学习成本也低,只需要懂基础 SQL,照样能看出来代码在干嘛。
1.3 功能模块划分与用户角色视角
需求分析阶段,我习惯把自己代入三种角色:普通顾客、店铺运营员、系统管理员。从他们的视角把需求一条条列出来,再合并成模块。
普通顾客关心的是:注册登录、浏览分类蛋糕、查看详情、加购物车、提交订单、查看自己的订单状态、取消订单、个人资料维护。
店铺运营员关心的是:管理蛋糕分类与商品上下架、维护价格库存、处理订单(接单、发货、完成)、发布店铺公告、回复顾客留言。
系统管理员在运营员的基础上多一个“用户管理”,可以禁用账号、重置密码、查看所有数据统计。
把需求汇总后,系统前台核心模块就是首页导航、用户中心、商品检索、购物车、订单结算;后台核心模块是工作台、商品管理、订单管理、用户管理、公告管理、数据报表。一个常规的 Spring Boot 单体项目,模块之间通过分层架构——Controller、Service、Mapper——自然解耦,不会出现应付不过来甚至“代码里塞一堆 if else 散弹式修改”的情况。
2. 数据库设计与核心业务模型
2.1 实体关系梳理与建表思路
数据库设计是这类系统的灵魂。我一开始就画出了实体间的关系:用户与订单是 1 对 N;订单与订单详情是 1 对 N;蛋糕与分类是 N 对 1;用户与评论是 1 对 N;公告独立成表。为了不把购物车做得过度复杂,购物车我用的是基于用户 ID 的独立表,简单清晰,用户关掉浏览器后下次登录购物车还在,体验比用 Session 购物车好很多。
实际建表时,我分了这几张核心表:
user:用户表category:蛋糕分类表cake:蛋糕商品表cart:购物车表orders:订单主表order_item:订单明细表notice:公告表comment:评论表
这里要说明的是,为什么订单要拆成主表和明细表两张?这实际是个很常见的数据库建模问题。一次购买可能包含多个蛋糕,如果所有数据都塞在一条订单记录里,比如用一个字段存多个商品 ID,那么以后统计某个蛋糕卖了多少的时候,SQL 会写得异常痛苦,而且违反第一范式。拆成两张表后,订单主表只存订单级别信息,比如订单号、总价格、收货人、状态、下单时间;明细表每行代表一个蛋糕的购买数量和单价,通过order_id关联主表。查询“某蛋糕的销量”时,对明细表按cake_id分组求和即可,统计效率非常理想。
2.2 关键表字段设计细节
我把最核心的几张表的结构列在下面,你可以直接作为设计参考。
user表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(50) | 用户名,唯一 |
| password | varchar(100) | 加密后的密码 |
| nickname | varchar(50) | 昵称 |
| phone | varchar(20) | 手机号 |
| varchar(100) | 邮箱 | |
| role | tinyint | 1-顾客 2-运营 3-管理员 |
| status | tinyint | 1-正常 0-禁用 |
| create_time | datetime | 注册时间 |
密码一定要加密存储,我使用的是BCryptPasswordEncoder,这是 Spring Security 里自带且业界广泛认可的单向加密算法。明文密码一旦数据库泄露就全完了,而 BCrypt 对相同密码每次生成的哈希值还会不同,能有效对抗彩虹表攻击。
cake表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| category_id | bigint | 分类 ID |
| name | varchar(100) | 蛋糕名称 |
| description | varchar(500) | 详细描述 |
| price | decimal(10,2) | 销售单价 |
| stock | int | 库存 |
| sales | int | 销量 |
| image | varchar(255) | 图片路径 |
| status | tinyint | 1-上架 0-下架 |
| create_time | datetime | 创建时间 |
这里要解释sales字段的作用。统计销量有两种思路:一是实时计算order_item表里每个蛋糕的累计个数,适合数据量小的时候;二是商品表维护一个“销量”字段,每次下单成功后做累加。我选择后者,因为前台商品列表要按“销量排序”,如果每次都去聚合订单明细表,商品一多查询就卡慢了。这就是典型的“空间换时间”优化,代价是写入订单时需要多维护一个字段,但对售卖系统来说完全值得。
orders表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 下单用户 ID |
| order_no | varchar(32) | 订单编号,全局唯一 |
| total_amount | decimal(10,2) | 订单总金额 |
| receiver_name | varchar(50) | 收货人姓名 |
| receiver_phone | varchar(20) | 收货电话 |
| address | varchar(255) | 详细收货地址 |
| remark | varchar(255) | 订单备注 |
| status | tinyint | 状态:0-待确认 1-制作中 2-配送中 3-已完成 4-已取消 |
| create_time | datetime | 下单时间 |
| pay_time | datetime | 支付时间 |
订单状态流转是这个系统的业务核心。我设置了五个状态:顾客提交订单后是“待确认”,管理员后台确认接单后变成“制作中”,制作完成配送时改成“配送中”,顾客确认收货或管理员点击完成变成“已完成”;顾客在下单后一段时间内可以“取消订单”,但是一旦管理员接了单,取消请求就需要管理员同意,这种闭环流程虽然简单,却让整个系统显得真实可信。
订单编号的生成我踩过坑,最开始用了UUID.randomUUID().toString().replace("-", ""),生成结果是32位字符串,纯随机,但客户电话来查询订单时,客服念那串乱码简直灾难。后来改成“时间戳 + 三位随机数”的组合,比如20250520143015078,人工念也好、记也好,都容易得多。要注意并发场景下必须保证唯一性,用 synchronized 加短时间窗口检查,或者直接依赖数据库的唯一索引兜底。
order_item表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_id | bigint | 订单主表 ID |
| cake_id | bigint | 蛋糕 ID |
| cake_name | varchar(100) | 蛋糕名称快照 |
| cake_price | decimal(10,2) | 单价快照 |
| quantity | int | 购买数量 |
| subtotal | decimal(10,2) | 小计金额 |
这里有个容易被忽略的设计:既然关联了cake_id,为什么还要冗余存cake_name和cake_price?因为商品名称和价格会随时修改,比如“草莓奶油蛋糕”改成“特级草莓奶油蛋糕”、价格从 128 涨到 138。如果订单明细里不保存快照,下单三个月后调出历史订单,看到的可能是修改后的当前价,那客户绝对会投诉。数据冗余在业务系统中是常态,所有操作都要往“保留历史真实数据”的方向设计。
2.3 购物车的实现方案选择
购物车我选择了“持久化到数据库”,而不是纯内存或 Cookie。原因很直接:商品数量多、状态复杂,数据库能保证跨会话不丢失;另外在后台统计中,数据中心能看到“加入购物车但未下单”的行为,这对运营做促销也有参考价值。cart表字段是主键 id、user_id、cake_id、quantity、create_time、update_time,并用user_id + cake_id建立唯一索引,这样同一个用户重复添加同一款蛋糕时,直接更新数量即可,不会产生脏重复行。
3. 密码安全、登录校验与核心业务代码剖析
3.1 用户认证流程的实现细节
登录模块很多初学者会跳过安全处理,直接写一个select * from user where username=? and password=?,这种代码放到生产环境就是灾难。我用了 Spring Security 的加密库,但没有引入整套 Spring Security 认证框架,因为后台角色的权限控制相对简单,用 Spring Boot 拦截器就能解决。推荐大家也这样控制工程复杂度,避免过度设计。
注册时密码加密代码:
@PostMapping("/register") public String doRegister(User user) { // 检查用户名是否存在 User exist = userService.getOne(new LambdaQueryWrapper<User>() .eq(User::getUsername, user.getUsername())); if (exist != null) { // 返回提示 return "redirect:/register?error=exist"; } // BCrypt加密 user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword())); user.setRole(1); // 顾客 user.setStatus(1); // 正常 user.setCreateTime(LocalDateTime.now()); userService.save(user); return "redirect:/login?success=reg"; }登录校验在 Controller 里做,查库后调用encoder.matches(rawPassword, encodePassword)来比对,匹配成功后把用户对象放进 Session。注意千万不能用equals比对密文,因为 BCrypt 每次加密结果不一样,必须用matches方法。
登录拦截器是防止未登录用户直接访问后台接口的关键手段。开发时最容易犯的错是拦截器把静态资源也拦截了,页面加载出来一团乱,CSS、JS、图片全部失效。因此在配置拦截器时,要单独把/css/**、/js/**、/images/**、/api/login这些路径放行。还需要注意 Thymeleaf 里登录页表单提交的地址要与 Controller 路由严格匹配,否则会出现“404 或者 405”的尴尬情况。
3.2 下单核心链路:事务与库存扣减
下单是整个系统技术含量最高的部分。简单来说,是以下几个步骤:
- 从购物车读取当前用户选中的商品列表;
- 检查每件商品的库存是否充足;
- 扣减库存,生成订单主表记录;
- 生成订单明细快照;
- 累加商品销量;
- 清空购物车。
这几步必须绑定在同一个数据库事务里,任何一个环节失败,比如库存不足、用户中途异常等,都不能留下半截数据。很多新手会忘掉事务,结果订单生成了,库存没减,这种情况补起来相当痛苦。正确做法是在 Service 方法上加@Transactional(rollbackFor = Exception.class),保证遇到任何运行时异常都回滚。
核心订单提交代码我给出一个简洁版本:
@Transactional(rollbackFor = Exception.class) public boolean submitOrder(Long userId, List<CartItem> items, String receiverName, String receiverPhone, String address) { // 1. 计算总价并检查库存 BigDecimal total = BigDecimal.ZERO; for (CartItem item : items) { Cake cake = cakeService.getById(item.getCakeId()); if (cake == null || cake.getStock() < item.getQuantity()) { throw new BizException("库存不足:" + item.getCakeName()); } total = total.add(cake.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity()))); } // 2. 生成订单主表 Orders order = new Orders(); order.setUserId(userId); order.setOrderNo(generateOrderNo()); order.setTotalAmount(total); order.setReceiverName(receiverName); order.setReceiverPhone(receiverPhone); order.setAddress(address); order.setStatus(0); order.setCreateTime(LocalDateTime.now()); orderService.save(order); // 3. 生成明细并扣库存 for (CartItem item : items) { Cake cake = cakeService.getById(item.getCakeId()); OrderItem orderItem = new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setCakeId(cake.getId()); orderItem.setCakeName(cake.getName()); orderItem.setCakePrice(cake.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItem.setSubtotal(cake.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity()))); orderItemService.save(orderItem); // 扣库存 cake.setStock(cake.getStock() - item.getQuantity()); cake.setSales(cake.getSales() + item.getQuantity()); cakeService.updateById(cake); } // 4. 清空购物车 cartService.remove(new LambdaQueryWrapper<Cart>() .eq(Cart::getUserId, userId)); return true; }这里想特别强调BigDecimal的使用。店铺里蛋糕价格往往带小数,比如 88.5、168.9,后端计算金额不能使用double。因为二进制浮点数在计算时会出现 0.1 + 0.2 != 0.3 的问题,尤其是多次累加商品金额后差异会被放大。一旦金额算错,用户和管理员都会炸毛。使用BigDecimal配合字符串构造方法,才能保证金额分毫不差。
3.3 文件上传:蛋糕图片的处理
蛋糕图片上传是后台商品管理的重头戏。我在application.yml里配置了上传大小限制,默认的 1MB 显然不够用,蛋糕的美图一上就是几 MB。配置为:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB同时,我用 UUID 重命名上传文件,避免了中文名、重复名导致的资源覆盖。图片保存路径这里要强调一下:上传到本地磁盘绝对地址,而数据库只存一个相对路径,比如/upload/cake/20250520/uuid.jpg。这样在部署时把整个upload目录打包,后续做静态资源映射也很简单——在 WebMvcConfigurer 里把/upload/**映射到磁盘真实路径。
一个常见的坑是前后端联调时,图片上传成功但刷新页面不出来。原因通常是:后端把图片写到本地磁盘了,但 Thymeleaf 页面引用的路径是/upload/...,结果请求打到 Controller 上没找到对应处理器,Spring Boot 默认不会把磁盘文件暴露成静态资源。必须做一个addResourceHandlers的配置:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadDir + "/"); }uploadDir对应的就是实际的物理路径,这个配置解决了我当时一个下午的烦恼。
4. 管理后台与数据可视化实现
4.1 前台检索与后台列表的分页查询
前台商品列表要支持按分类筛选、按价格/销量排序、分页浏览。这里得益于 MyBatis-Plus 的分页插件,只需要三步:注入分页拦截器MybatisPlusInterceptor,调用page(new Page<>(pageNum, pageSize), wrapper),返回IPage对象给前端。分页对象里自带总条数、总页数、当前页数据,前端 Thymeleaf 可以通过page.getPages()渲染页码。我不建议自己手写 SQL 的 LIMIT 语句,因为分页还要配套查总条数,业务一复杂就容易漏掉 or 算错,伤不起。
后台列表页还有一个需求:关键字搜索。比如运营要快速找到“巧克力”相关的蛋糕,只需要在查询条件上加like:
LambdaQueryWrapper<Cake> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), Cake::getName, keyword); wrapper.eq(cake.getCategoryId() != null, Cake::getCategoryId, cake.getCategoryId()); wrapper.orderByDesc(Cake::getCreateTime);这里StringUtils.hasText(keyword)作为条件的好处是,keyword 为空时根本不会把 like 拼到 SQL 里,页面无需再做额外判断。很多新手写的代码是if(keyword != null && !keyword.isEmpty())再套一层,虽说也能实现,但代码肉眼可见地繁琐,用了条件构造器后,几行代码就搞定组合查询。
4.2 订单状态流转的后端实现
订单状态的每一次变更我都是用接口驱动,前台和后台的按钮分别对应不同的状态操作接口:
- 后台“确认接单”接口:
POST /admin/order/confirm/{id},状态从 0 变为 1。 - 后台“开始配送”接口:
POST /admin/order/deliver/{id},状态从 1 变为 2。 - 后台“完成订单”接口:
POST /admin/order/finish/{id},状态从 2 变为 3。 - 前台“取消订单”接口:
POST /user/order/cancel/{id},状态从 0 变为 4,且只能在待确认状态操作。
状态变更时需要校验当前状态是否合法,否则用户并发点击按钮可能造成状态错乱。我用一个统一的transition(order, targetStatus)方法,内部定义一个合法的状态转移映射,比如允许0->1、1->2、2->3、0->4,其他一律抛出异常。这样把状态机逻辑收拢在一个地方,比散落各处的 if else 好维护得多。
4.3 用 ECharts 做出销售看板
管理员一进后台最想看的就是“这周卖了多少、哪个蛋糕最火”。我单独做了个DashboardController,提供一组 JSON 数据接口,然后在页面中用 JQuery 的$.ajax获取数据,交给 ECharts 渲染。
销量趋势图的数据来源是order_item关联商品类型,按日期聚合。SQL 大致是:
SELECT DATE_FORMAT(o.create_time, '%Y-%m-%d') AS day, SUM(oi.quantity) AS total FROM orders o JOIN order_item oi ON o.id = oi.order_id WHERE o.status != 4 AND o.create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY day ORDER BY day把结果转成两个数组,X 轴是日期列表,Y 轴是销量列表,前端一填充就能展示漂亮的折线图。分类占比图则用饼图,按category_id分组统计销售金额。
这部分最需要注意的是,SQL 中用的字段名一定要和实体类映射对好,group by 出来的字段冗余关系要在 DTO 中定义清楚,否则接口返回的数据结构不稳定,前端图表时好时坏。
5. 上线部署前的典型问题与排查方案
5.1 分页查询永远只有第一页
这是一个出现频率极高的“错觉”。很多同学写完分页后,点击第二页发现页面数据还是原来的第一页。排查思路其实就两条:第一,检查浏览器地址栏的page参数是否传递到位;第二,检查 Controller 接收参数时是否写入了@RequestParam(defaultValue = "1") Integer pageNum。我见过一个项目里,分页参数前端叫pageNumber,后端叫pageNum,名字对不上导致每次翻页都从默认值开始。解决方法是把参数名统一,或者用一个PageQuery对象接收参数。
5.2 事务不生效的三种可能
你兴冲冲地在 Service 上写了@Transactional,最后发现数据该入库的入库了,该扣的库存没扣,回滚了个寂寞。我从实践看,主要有三种原因:
- 方法被同类内部调用,例如 OrderService 的
submitOrder调用同类中的updateStock,而@Transactional只有在外部代理调用时才生效。也就是说,自调用会绕过代理,事务失效。 - 异常被 catch 住了。事务回滚的前提是异常传出去,如果你在方法内部 try-catch 掉了,Spring 根本不知道出错了,自然不回滚。
- 方法不是 public 的。Spring AOP 默认只代理 public 方法,你用 private 的话,事务也是静默失效。
所以最稳妥的写法是:把事务方法设为 public,并且不要内部 catch 掉关键异常,如果非要 catch,最后记得手动抛出TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。不过说实话,手动回滚标记这种代码写出来逻辑很绕,不如仔细设计异常往外抛。
5.3 请求参数中文乱码的零成本解决
开发时最容易引起烦躁的问题就是中文乱码。Spring Boot 高版本其实默认就启用了 UTF-8 字符集过滤器,如果你用的是 2.7,基本不用额外设置。但如果你自己写了过滤器,或者修改了spring.http.encoding相关配置导致覆盖,就可能出现乱码。
排查步骤:先看数据库连接字符串上有没有characterEncoding=utf8;再看页面<meta charset="UTF-8">;最后看你要展示的字段本身入库时是否已经是乱码。我遇到过一次,问题出在前端表单提交时的enctype设置错误——上传图片的 multipart 表单把每个字段都编码成了二进制,后端拿到中文就乱。解决方式就是不要给纯文本表单加 multipart。
5.4 图片上传报空指针
上传图片时如果直接拿MultipartFile去调transferTo(File)保存文件,容易在文件路径不存在时报空指针或 IO 异常。原因是transferTo底层要求目标文件的父目录存在。我当时的解决方法是,先判断并创建目录:
File dir = new File(uploadDir + "/" + dateStr); if (!dir.exists()) { dir.mkdirs(); }不然每次新建一个日期的子目录都会抛异常。并且要注意 Windows 和 Linux 路径分隔符的差异,最好用File.separator拼接,别硬编码/或\。
6. 配套技术文档与答辩 PPT 的梳理经验
做这个项目时,除了写代码,我还整理了完整的设计文档和 PPT,这里顺便分享下内容组织的心得。
技术文档我分成了六章:需求分析、可行性分析、系统设计、数据库设计、系统实现、系统测试。其中需求分析和数据库设计占了全程大约 40% 的篇幅。写文档最大的价值是倒逼你把功能模块、表结构、业务流程想清楚,而不要急着敲代码。实际上我在文档里先把需求画成用例图、流程图画成时序图,后续开发基本就是照着图填实现代码,效率反而高。
答辩 PPT 我的建议是不要堆文字。控制在一页一个核心观点,多用截图和原型图。重点放三块:第一块讲系统整体架构图,用分层图证明你对项目有全局观;第二块贴一个核心业务代码片段,比如下单事务或者身份鉴权,并说明为什么这么写;第三块现场演示重点页面,比如下单流程和数据统计图表。如果被问到“这个项目有什么亮点”,我准备的是“订单状态机封装 + 数据库快照设计 + 事务保证库存一致性”,把自己的实战思考讲出来,比背概念强太多。
很多同学项目做完了,代码没问题,却不知道文档从何写起。其实有一个捷径:对照系统的功能清单,把每个操作步骤截图,配上 2-3 句说明文字,再补上 E-R 图和核心表结构,那份文档就已经能撑起七成的分数了。
7. 写在最后的个人实操心得
整套系统从需求到上线,最耗时的不是代码,而是数据建模和状态流转设计。我之前也吃过图省事、把订单和购物车揉在一张表的亏,结果后面对接报表时悔得肠子都青了。这次从一开始就坚持“主表 + 明细表 + 快照字段”的思路,后面写统计接口时顺手得简直想给自己鼓掌。
还有一点要提醒刚接触 Spring Boot 的朋友,别一上来就追求“微服务”“分布式”。蛋糕店的售卖场景,一台 2 核 4G 的服务器完全能扛住常规流量。把单体应用做扎实,把事务、权限、文件存储这些基础能力弄明白,再去接触分布式的各种中间件,路线会稳得多。这个系统里你学到的分层思想、状态机思维、事务处理,换到任何后端项目里都是通用的底层能力。
如果你也准备做类似的项目,建议你先从数据库设计入手,把表建出来,再考虑 Controller 怎么写。文档、PPT、源码都只是项目成果的载体,真正核心的是你对业务逻辑的理解和表达能力。欢迎在评论区交流你在这个项目中遇到的坑。