2026/9/9 6:24:35

SpringBoot实战:从零搭建桌游信息管理系统

SpringBoot实战:从零搭建桌游信息管理系统 去年帮一个学弟复盘他的毕业设计题目正好就是“基于SpringBoot的桌游信息管理系统”。乍一看这东西平平无奇无非是登录、增删改查、统计报表这几板斧。但真正上手之后才发现桌游店的业务里藏着不少值得细抠的点桌游库存怎么扣减、订单状态怎么流转、会员余额和押金怎么处理、租借时长怎么计费。把这些逻辑理清楚项目做出来才有含金量而不是一个空壳CRUD。这篇文章打算把这个系统的设计思路、数据库建模、后端实现、前端页面、部署调试全过程拆开讲一遍。适合三类人看正在做Java毕业设计、选题是管理系统的同学刚学完SpringBoot想找一个完整项目练手的初级开发者确实有桌游吧信息化需求、想自己搞个小工具的经营人员。我会尽量写得能直接抄作业从环境搭建到核心代码再到排查Bug的经验全部摊开来说。1. 项目概览与技术选型为什么大家都选SpringBoot1.1 桌游店管的是什么需求梳理做系统之前先别急着写代码得先弄明白“桌游信息管理系统”到底要管什么。我接触过的桌游吧日常运营至少有这几块固定动作第一桌游的商品化管理。店里一般有成百上千套桌游名字、类型、适合人数、单局时长、每小时租金、库存数量、损坏下架状态这些信息靠Excel记录特别容易乱需要一套能增删改查和模糊搜索的商品模块。第二会员和储值管理。桌游吧大多靠办卡锁定回头客会员的手机号、余额、积分、等级需要维护。这里有个常见需求是“桌游租借费用从会员余额里扣”所以会员余额字段必须和订单模块联动。第三租借与订单管理。这是整个系统的核心业务。顾客到店选一套桌游系统要生成一个租借订单记录桌游、时长、金额、押金、开始时间、结束时间并实时更新桌游库存。订单不能只是新增、删除还得有“租借中、已完成、已取消、逾期未还”这类状态流转。第四简单的统计报表。老板最关心的其实是“今天进账多少、哪几款桌游最热门、最近会员增长情况”。所以系统最好带一个数据看板用图表把订单金额趋势、桌游热度排行展示出来。这就是需求清单。管理系统的功能基本都由业务倒推出来千万不要想到哪写到哪否则后面改起来非常痛苦。1.2 技术栈选型SpringBoot MyBatis-Plus MySQL Thymeleaf这套系统的技术栈我很推荐一个成熟的组合SpringBoot 2.7.x MyBatis-Plus MySQL 5.7/8.0 Thymeleaf Bootstrap ECharts。原因很简单每一层都有明确替代方案但这个组合是综合上手成本、资料丰富度、调试便利性之后最稳妥的。SpringBoot的优势不用多吹内嵌Tomcat、自动装配、约定大于配置能省掉传统SSM里一长串的XML配置。MyBatis-Plus相比原生MyBatis最大的价值是内置了通用Mapper和通用Service单表CRUD几乎不用手写SQL分页插件也用得顺手。配合LambdaQueryWrapper条件查询写起来非常直观对赶工期的项目来说极其友好。前端为什么选Thymeleaf而不是前后端分离的Vue核心原因是这套系统是单体应用不需要单独起后端接口服务。用Thymeleaf做服务端渲染页面直接通过模板引擎拿到数据部署时一个jar包搞定不用处理跨域、不用配置Nginx转发多个服务对毕业设计和中小型内部管理系统来说交付和维护成本最低。如果你已经熟练Vue也可以改成SpringBoot写接口 Vue页面但复杂度会明显上升不是这个项目的最优解。1.3 功能模块全景图把需求落到功能模块上这套系统我建议这样划分管理员模块登录、退出、修改密码、管理员信息维护。桌游分类模块分类列表、新增、编辑、删除用于给桌游打标签。桌游信息模块桌游列表查询、条件搜索、添加桌游、编辑、上下架、删除封面图上传。会员管理模块会员列表、新增会员、余额充值、积分调整、删除/禁用。订单管理模块创建租借订单、归还结算、订单状态流转、订单条件查询。统计看板模块今日营收、订单数量、桌游热度排行、近7日订单金额趋势。模块之间不是孤立的。创建订单要扣桌游库存、算金额要读桌游单价和时长、支付要扣会员余额、归还计算要更新订单状态和桌游库存。这些都是跨表的业务操作必须放在Service层用事务串起来不能拆开来各写各的。2. 数据库设计先把地基砸实2.1 六张核心表怎么建数据库设计是这套系统的地基。我见过太多人一上来先写代码结果表结构建得七零八落后面业务逻辑越写越拧巴。这里我直接给出一个可落地的建表思路。核心表分六张管理员表、桌游分类表、桌游信息表、会员表、租借订单表、订单明细表。考虑到毕设复杂度适中订单明细表可以先不拆一张订单对应一套桌游即可如果后续要支持一个订单租多套桌游再拆订单明细表。桌游信息表是最核心的表字段设置如下CREATE TABLE board_game ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, game_name VARCHAR(100) NOT NULL COMMENT 桌游名称, type_id BIGINT NOT NULL COMMENT 分类ID, min_players INT NOT NULL DEFAULT 2 COMMENT 最少游玩人数, max_players INT NOT NULL DEFAULT 4 COMMENT 最多游玩人数, play_duration DECIMAL(4,1) DEFAULT 1.0 COMMENT 单局预计时长(小时), price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 每小时租金, stock INT NOT NULL DEFAULT 0 COMMENT 当前库存数量, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态:1上架 0下架, cover_url VARCHAR(255) DEFAULT NULL COMMENT 封面图地址, description TEXT COMMENT 桌游玩法简介, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT桌游信息表;租借订单表设计的时候要特别注意不要直接用order做表名因为order是SQL关键字容易埋坑。建议用rent_orderCREATE TABLE rent_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单号, member_id BIGINT NOT NULL COMMENT 会员ID, board_game_id BIGINT NOT NULL COMMENT 桌游ID, quantity INT NOT NULL DEFAULT 1 COMMENT 租借数量, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, deposit DECIMAL(10,2) DEFAULT 0.00 COMMENT 押金, start_time DATETIME NOT NULL COMMENT 租借开始时间, end_time DATETIME NOT NULL COMMENT 预计归还时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态:0待支付 1租借中 2已完成 3已取消, remark VARCHAR(255) COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT租借订单表;会员表和管理员表相对简单会员表需要包含member_no、name、phone、balance、points、level等字段。这里我特别加了一个member_no会员编号虽然手机号也能唯一标识会员但单独编号在后期对接小程序、会员卡时更方便。2.2 字段类型和状态值的取舍建表时最容易被忽略的是字段类型的选择。我举两个最常见的例子金额字段一定要用DECIMAL不能用FLOAT或DOUBLE。因为二进制浮点数在计算0.10.2这类场景会出现精度误差而金额误差是绝对不能接受的。DECIMAL(10,2)可以存最大10亿的金额对于桌游吧来说完全足够。状态字段建议用TINYINT不要用VARCHAR存中文。比如订单状态用0、1、2、3表示在Java后端定义一个枚举类代码里只操作枚举展示到页面前再转换成对应的中文文案。这样做的好处是数据库体积小、查询效率高、后端判断逻辑不会因为中英文字符差异出问题。虽然看起来多写了几行转换代码但项目的健壮性会明显提升。2.3 外键到底建不建我的建议是不要建物理外键用逻辑外键。也就是在业务表里存关联表的ID但不在数据库层面强制约束。原因有几点物理外键会让删除操作变得非常麻烦你想删一个分类结果数据库提示有桌游还在引用它必须先处理子表数据高并发场景下外键约束还会引入额外的锁开销而且大多数管理系统根本不依赖数据库来保证完整性而是靠Service层代码控制。既然不建物理外键那就要在代码里兜底。比如删除分类前先查询该分类下是否还有桌游有就禁止删除创建订单前校验会员和桌游是否存在校验库存是否充足。这些逻辑放在Service层比依赖数据库约束更灵活报错信息也能写得更友好。3. 后端实现登录、CRUD与库存扣减的细节3.1 项目骨架与核心依赖配置后端代码的包结构我建议按功能模块划分而不是按技术类型划分。按照controller、service、mapper这种分法到后期类一多就乱成一锅粥。我习惯这样组织com.example.boardgame ├── controller # 控制器只做参数接收和结果封装 ├── service # 业务逻辑接口 ├── service.impl # 业务实现事务在这里 ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体类 ├── dto # 前端传输对象 ├── config # 配置类 ├── interceptor # 拦截器 └── common # 通用返回结果、异常处理、枚举工程创建时pom.xml里最核心的依赖就这么几个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency这里有个细节如果你用的是SpringBoot 3.x依赖名称和兼容性都有变化MySQL驱动类名也从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver。我建议初学者锁死SpringBoot 2.7.x版本相关资料最多、踩坑最容易被搜索引擎找到。3.2 登录鉴权拦截器 Session登录功能每个管理系统都有但实现方式有不少讲究。最简单的方案是用Session保存登录状态配合Spring MVC拦截器统一校验。不用引入Spring Security或者JWT因为这套系统是服务端渲染的单体应用用Session完全够而且逻辑更直观。拦截器代码长这样Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); if (session.getAttribute(admin) ! null) { return true; } // 判断是否为AJAX请求 String xrw request.getHeader(X-Requested-With); if (XMLHttpRequest.equals(xrw)) { response.setContentType(application/json;charsetutf-8); response.getWriter().write({\code\:401,\msg\:\登录已过期请重新登录\}); } else { response.sendRedirect(/login); } return false; } }然后注册拦截器排除登录接口和静态资源路径Configuration public class WebConfig implements WebMvcConfigurer { Resource private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /css/**, /js/**, /images/**, /fonts/**); } }为什么要把AJAX请求和普通页面请求分开处理因为页面跳转和异步请求对未登录状态的处理方式不一样。普通页面直接重定向到登录页用户能看到但AJAX请求如果也返回302前端拿到的是一个登录页的HTML解析数据就会报错。所以拦截器里要做一次区分。3.3 分页查询与通用增删改查MyBatis-Plus最大的好处就是单表CRUD基本不用手写SQL。实体类继承BaseMapperService继承IService就拥有了最基础的增删改查方法。但实际项目中肯定要带条件查询和分页这就要用到它的分页插件。分页插件配置很简单Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }配置好之后Service层的分页查询可以这样写public PageResultBoardGame pageGames(int current, int size, String keyword, Long typeId) { PageBoardGame page new Page(current, size); LambdaQueryWrapperBoardGame wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), BoardGame::getGameName, keyword) .eq(typeId ! null, BoardGame::getTypeId, typeId) .orderByDesc(BoardGame::getCreateTime); PageBoardGame result boardGameMapper.selectPage(page, wrapper); return PageResult.of(result); }这里有两个地方值得展开。第一个是LambdaQueryWrapper的条件写法。.like(boolean condition, column, value)这种重载作用是当condition为false时这个条件自动跳过。这样省掉了大量if判断代码看起来非常干净。第二个是PageResult返回对象。Controller层不要直接把MyBatis-Plus的Page对象丢给前端因为里面包含了一些前端不需要的字段。更合理的做法是封装一个统一的PageResult包含total、current、size、records等字段这样前端分页组件取数才规范。这个习惯对以后写接口很有帮助。3.4 桌游租借一次跨表的事务操作租借桌游是这个系统里最核心的业务链路也是最能体现事务能力的地方。完整逻辑是这样的创建订单时先从数据库查出桌游信息校验状态是否为上架校验库存是否大于等于租借数量。然后根据桌游单价、租借小时数、数量计算出总金额检查会员余额是否充足。都通过后生成订单号插入订单记录扣减会员余额扣减桌游库存这一切必须在一个事务里完成。库存扣减建议不要在代码里先查出来减完再写回去而是用一条带条件的UPDATE语句原子执行Update(UPDATE board_game SET stock stock - #{num} WHERE id #{id} AND stock #{num}) int deductStock(Param(id) Long id, Param(num) Integer num);这条SQL的含义是“只有当库存大于等于租借数量时才扣减”并且是数据库层面的原子操作。如果返回值是0说明库存不足或者桌游不存在直接在Service层抛出业务异常并回滚事务。这种做法能避免两个用户同时下单时超卖的问题比先查再改安全得多。Service层的方法上必须加Transactional注解Transactional(rollbackFor Exception.class) public RentOrder createOrder(RentOrderCreateDTO dto) { // 1. 校验桌游状态与库存 // 2. 校验会员余额 // 3. 计算金额 // 4. 创建订单 // 5. 扣减库存 // 6. 扣减余额 }rollbackFor Exception.class一定要写。Spring默认只在抛出RuntimeException时回滚如果你抛的是普通Exception事务不会回滚到时候库存扣了但订单没生成数据就乱了。这是我之前踩过的坑特别提醒一句。订单状态流转也可以用枚举管理。建议定义一个OrderStatusEnumGetter AllArgsConstructor public enum OrderStatusEnum { PENDING_PAY(0, 待支付), RENTING(1, 租借中), FINISHED(2, 已完成), CANCELLED(3, 已取消); private final Integer code; private final String desc; }所有订单状态修改都通过枚举的code来操作前端拿到code后根据枚举转成中文。这样代码里不会出现魔法数字满天飞的情况。4. 前端页面从登录页到数据看板4.1 后台布局与侧边栏导航前端我用的是Thymeleaf Bootstrap这套组合。后台布局基本固定左侧是菜单栏右侧是内容区顶部是当前管理员信息和退出按钮。Thymeleaf提供了强大的模板复用能力公共的侧边栏、顶栏、脚本引用都可以抽成fragment片段。公共片段文件一般是layout.html里面定义几个th:fragment。各业务页面只需要引入页面内容的片段并在对应位置放自己的业务模块。这样做的好处是改菜单、改公共脚本只动一个文件就够了。侧边栏菜单的高亮状态需要传入当前菜单标识。我的做法是在Controller里往Model放一个navActive字段页面根据这个字段判断要激活哪个菜单。比如访问桌游列表navActive等于game侧边栏里game对应的a标签就自动追加active样式。别小看这个细节少了它用户点来点去根本不知道自己在哪个模块。4.2 表单校验和异步请求表单新增和编辑是管理系统最常见的交互。我只讲两个核心点。第一表单校验要两层都做。前端校验用HTML自带的required、maxlength、pattern属性或者用jQuery Validation插件好处是响应快输入错误立刻提示。但前端校验可以被绕过所以后端还需要再校验一次。后端校验最简单的方式是Controller里手动判断关键字段必要时用JSR-303注解比如NotNull、DecimalMin。第二数据提交方式。新增、编辑、删除这些操作我建议用AJAX JSON格式交互而不是传统的表单同步提交。原因很现实同步提交刷新的是整个页面校验出错时用户填了一半的数据全没了体验很差。异步提交配合弹窗提示和layui或Bootstrap的模态框操作流顺很多。而且统一返回Result对象后Controller的代码也可以收敛得很干净$.ajax({ url: /game/save, type: POST, contentType: application/json, data: JSON.stringify(formData), success: function(res) { if (res.code 200) { layer.msg(保存成功); // 刷新表格 } else { layer.msg(res.msg, {icon: 2}); } } });4.3 ECharts统计看板统计看板是一个很能提升系统档次的模块而且实现起来并不复杂。桌游管理系统看板我建议放三个核心图近7日订单金额趋势用折线图、桌游热度排行用横向柱状图、会员等级分布用饼图。数据来源是后端统计分析接口。比如统计近7日营收SQL可以按天分组SELECT DATE(create_time) AS day, SUM(total_amount) AS amount FROM rent_order WHERE status 2 AND create_time DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(create_time) ORDER BY day;Controller把查询结果转成ECharts需要的格式返回前端通过Thymeleaf内联脚本接收并渲染。这里有个容易踩的坑时间格式化问题。传给图表的数据如果是标准的LocalDateTime对象默认序列化出来可能是一长串数字或带T的格式建议在后端统一格式化成yyyy-MM-dd或yyyy-MM-dd HH:mm:ss字符串再返回省去前端处理麻烦。ECharts初始化的时机也要注意。页面数据没加载完或者容器宽度为0时就初始化图表经常变成一堆拥挤的图形。建议在window.onload或$(document).ready之后再初始化同时给图表容器设置固定高度。5. 从零搭建开发环境到服务器部署5.1 本机开发环境怎么配开发这套系统本机需要准备这些软件JDK 1.8、Maven 3.6、MySQL 5.7/8.0、IDEA。这几个东西版本匹配很重要JDK推荐1.8对应SpringBoot 2.7.x完全没有问题。如果你装了JDK 17甚至更高有些老版本的依赖可能会不兼容所以Java环境不是越高越好要看项目基于什么版本。MySQL安装后要注意字符集设置。建库时显式指定utf8mb4CREATE DATABASE boardgame_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;utf8mb4兼容完整的UTF-8编码能存emoji避免一些生僻字或特殊符号插入数据库时报错。MySQL 8.0默认字符集已经是utf8mb4但保险起见建库时还是明确写出来。IDEA建议直接安装Ultimate版因为社区版不直接支持Spring Initializr创建项目但我一般建议直接到Spring官网的start.spring.io生成项目再导入这样社区版也能用。导入项目后先让Maven把依赖下载完再配置运行环境。5.2 数据库初始化和项目导入数据库脚本文件可以直接用建表SQL。为了演示方便可以在脚本里预置几套桌游数据和管理员账号这样项目一启动页面就有内容不用费劲一条条新增。配置数据源时我把最常用的一套application.yml贴出来server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/boardgame_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 thymeleaf: cache: false mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这段配置里有几个关键点我需要多说一句。连接串里的serverTimezoneAsia/Shanghai是必须写的MySQL 8.0默认时区设置经常导致数据库连接报错。allowPublicKeyRetrievaltrue也是用MySQL 8.0时经常需要的参数否则会报Public Key Retrieval is not allowed。thymeleaf的cache: false很重要开发时改了模板不用重启就能看到效果。mybatis-plus的log-impl输出SQL日志排查问题时能直观看到每条语句执行情况。5.3 打包部署jar包与Nginx本地开发没问题后部署到服务器是另一个常见环节。打包命令非常简单mvn clean package如果用的是IDEA也可以直接在Maven面板里双击package。打包完成后target目录下会生成一个spring-boot-bootstrapped的jar包。这里有个常见的坑有些同学打包后运行报“找不到主类”大概率是没有配置spring-boot-maven-plugin的mainClass或者是项目里存在多个SpringBootApplication启动类。这个问题排查时去Maven插件配置里确认一下即可。服务器上运行jar包常规姿势是用nohup后台启动nohup java -jar boardgame-system.jar --server.port8080 app.log 21 这样ssh断开后程序也不会停。如果你想做得更专业可以用systemd写一个服务文件支持开机自启和自动重启。但对于毕业设计和中小项目nohup方案已经够用。端口和外部访问这块也要提前规划。如果你的服务器上只有这一个应用8080端口直接开放就行。但如果还有别的项目或者80端口被占用了建议用Nginx做反向代理把80端口转发到8080顺便还能代理静态资源。生产环境我习惯把前端静态资源和后端接口都通过一个域名入口进来省得浏览器里带端口号也方便以后上HTTPS。6. 常见问题排查与避坑手册6.1 数据库连接报错这一类报错在开发期出现频率最高。我随便列几个最常见的场景启动时报Access denied for user rootlocalhost说明用户名或密码不对去application.yml里核对。报Unknown database boardgame_db说明数据库没建或者名字写错。报Public Key Retrieval is not allowed这是MySQL 8.0的加密连接特性导致的在连接串加allowPublicKeyRetrievaltrue就能解决。报The server time zone value说明时区没配置加上serverTimezoneAsia/Shanghai。这些报错信息其实已经把问题原因说得很清楚但很多同学一看到一大段英文就慌。我建议遇到报错先看最下面几行也就是Caused by开头的部分那才是真正的根因。6.2 启动失败的几类原因启动失败除了数据库问题还有一类是端口被占用。SpringBoot默认端口是8080如果你本机已经跑了别的服务占用了8080启动日志会报Web server failed to start这时要么改server.port要么用命令查端口占用并杀掉对应进程。还有一类是依赖冲突或版本不对。比如SpringBoot 3.x的项目强行使用了为2.x写的MyBatis-Plus配置类启动时会出现找不到类或者方法签名不一致的问题。我的建议是一开始就锁定SpringBoot 2.7.x和MyBatis-Plus 3.5.x的组合不要混用。版本不是越新越好稳定的组合才能让你把精力放在业务逻辑上。6.3 JSON序列化和时间格式的坑后端返回LocalDateTime类型数据时默认序列化结果往往是“2024-03-18T10:24:30”这种带T的格式前端展示起来很不好看。解决办法有两个。一是在实体类的日期字段上加注解JsonFormat(timezone GMT8, pattern yyyy-MM-dd HH:mm:ss) private LocalDateTime createTime;timezone GMT8必须写否则序列化出来可能和数据库时间相差8小时。第二种是全局配置Jackson的ObjectMapper统一处理所有日期字段。两种方案对比局部注解适合少量字段全局配置适合整个项目日期字段多的情况。我建议用全局配置省事且统一。6.4 给毕设和项目的三点建议做了这么多项目最后分享几个掏心窝的建议。第一先花时间把数据库设计好再动代码。我见过太多项目做着做着要加字段、改表名连带着Service层逻辑全部重写。设计阶段多花一天开发阶段能省一周。表关系、状态枚举、金额精度这些东西一定要在一开始就想清楚。第二Controller要保持薄。Controller只做参数接收、调用Service、返回结果业务逻辑不要写在Controller里。很多人觉得这样多写一层很麻烦但项目一旦变大你就会发现这是唯一能保持代码清爽的方式。我见过把库存扣减写在Controller里的项目后来光排查并发问题就花了两周。第三项目要能在全新的环境里跑起来。交付项目时数据库初始化脚本、配置说明、启动说明、部署文档这几样缺一不可。我每次开发完都会在自己的电脑上用干净的环境走一遍部署流程模拟第一次运行的人会遇到什么问题。这个习惯帮我避免过很多次“在我电脑上明明好的”的尴尬。桌游信息管理系统这类项目技术点不算高深但它在业务链路上包含了登录鉴权、分页查询、文件上传、跨表事务、状态机、数据统计等真实项目中几乎必用的能力。把这套流程完整走一遍你对SpringBoot和MyBatis-Plus的理解会比看十遍教程都深刻。做的时候不用追求炫技先把核心链路跑通再一步步完善细节你会发现这个项目带给你的收获远比想象中多。