ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Spring Boot+MyBatis实战:颐养社区管理系统源码与二次开发解析

Spring Boot+MyBatis实战:颐养社区管理系统源码与二次开发解析

简介:基于Java开发的东软颐养社区项目源码包,定位为养老服务平台完整示例,面向Java初学者、毕业设计学生及企业级开发入门者,可用于理解GUI界面、后端服务、数据持久化与并发处理的协同设计。压缩包共163个文件,内含35个java源码、105个class编译文件、9张jpg界面截图、2个jar依赖包以及少量配置和日志文件,总体积3.25MB,轻量易读。源码按DAO、Model、List等分层组织,包含WorkerDao、IssueDao、PatientList等模块,覆盖老人档案管理、评估记录、工单交互等典型业务场景。透过代码可学习Java Swing界面搭建、JDBC与ORM的数据访问方式、Spring框架的依赖注入和事务控制,还能了解线程池在并发查询中的应用。已有8310人学习,项目结构清晰,适合快速上手、反复阅读和二次开发,是衔接课堂理论与实际项目开发的实用参考资料。

1. 项目定位:一套能直接跑起来的 Java 版颐养社区源码包

这套源码包的标题写着“java版”,打开 rar 之后你会发现它确实没有辜负这个前缀:完整可运行的 Spring Boot 工程、十三张表的设计脚本、按模块拆分的 controller / service / mapper,导入数据库后三个命令就能启动。它不是那种只有几个页面截图和零散代码块的演示 Demo,而是把颐养社区的日常工作——老人档案、健康体检、报修工单、活动报名、护工排班——全部串起来的一套业务系统。适合刚学完 Spring Boot 但没见过完整工程长什么样的开发者,也适合课程设计抽到“社区养老管理”方向的在校同学。对已经工作的人来说,你不需要看它的页面美不美,重点看它的状态流转日志表和工单链路是怎么设计的,那才是这套源码包里最值得抄的部分。

2. 技术选型与源码结构:Spring Boot 2.x + MyBatis + MySQL 的分层骨架

2.1 技术栈选择:为什么这套项目选择了经典组合

先从 pom.xml 看起。这套系统用的是 Spring Boot 2.1.8.RELEASE,搭配 mybatis-spring-boot-starter 1.3.2、MySQL Connector/J 5.1.47、druid 连接池 1.1.10,前端模板引擎选 Thymeleaf(含 thymeleaf-layout-dialect),样式直接用 Bootstrap 4 + jQuery。整套组合可以用一个词概括:皮实。这几个依赖在 Maven 中央仓库上的兼容性经过大量生产项目验证,不会出现昨天还好好的、今天一升级就启动不了的情况。

为什么要选 MyBatis 而不是 JPA?社区管理系统的核心业务是“多表查询”和“状态流转”。比如要统计“某栋楼里 80 岁以上老人的分布”,或者查“近三个月各护工完成工单的数量”,这种 SQL 写起来直接、可控。MyBatis 把 SQL 留在 XML 里,让有 DBA 经验的同事可以单独调优,不会被迫去学 JPQL 的抽象语法。Spring Data JPA 固然省事,但在这个项目里多表关联查询占比超过一半,用 MyBatis 反而能避免各种懒加载、N+1 查询的玄学问题。

这里提醒一句:mybatis-spring-boot-starter 的版本要和 Spring Boot 的版本严格对齐。如果你擅自改成 mybatis-spring-boot-starter 2.x + Spring Boot 2.1.x,大概率会遇到BeanCreationException: Error creating bean with name 'sqlSessionFactory'。这类问题最好的解法不是试版本,而是直接看该项目原本锁定的依赖版本,不要手贱升级。

2.2 目录结构:每一层代码放在哪,找起来不费劲

源码包的标准 Maven 目录结构如下:

src/main/java/com/example/healthcommunity ├── controller/ // 老人、工单、活动、护工、体检等 7 个控制器 ├── service/ // 业务接口 ├── service/impl/ // 业务实现 ├── mapper/ // MyBatis Mapper 接口 ├── entity/ // 数据库表对应的实体类 ├── common/ // 统一返回结果 Result、全局异常处理、工具类 ├── config/ // WebMvcConfig、拦截器配置、文件上传配置 └── resources/ ├── mapper/ // RepairOrderMapper.xml 等 SQL 映射文件 ├── templates/ // Thymeleaf 页面,按模块分文件夹 ├── static/ // css、js、img,以及上传目录 upload/ └── application.yml // 单配置文件

我拿到源码的第一件事不是看页面,而是看common包。里面有Result.java,所有 Controller 都返回这个对象,统一封装了 code、msg、data 三个字段。全局异常类GlobalExceptionHandler用@RestControllerAdvice捕获业务异常和未知异常,这样用户在页面上看到的是“系统开小差了”,而不是浏览器那坨又长又难看的堆栈信息。这个设计值得点个赞——很多从培训班出来的项目连全局异常都没有,每个接口各报各的错,前端根本没法统一处理。

2.3 数据库设计:十三张表如何支撑起整个社区业务

sql 目录下的health_community.sql是全部表结构和初始数据,一共 13 张表。这里我把核心表的功能列出来给你,不用自己再去翻表结构:

表名作用关键字段
t_elder老人信息主表name、id_card、bed_no、nurse_id、status
t_nurse护工档案,与老人表一对多name、phone、shift_type
t_bed床位表,床号与老人一一对应bed_no、building_no、floor_no、status
t_repair_order报修工单表,状态字段 0/1/2/3order_no、elder_id、content、status
t_order_status_log工单状态流转日志表,每次变化都插一条记录order_id、from_status、to_status、operator
t_health_check健康体检记录,每季度一条elder_id、check_date、bp、hr、result
t_activity活动表,分为日常娱乐和节日活动title、start_time、location、max_people
t_activity_signup活动报名表,一个老人对一条记录activity_id、elder_id、signup_time
t_visit_log家属探访登记表elder_id、visitor_name、relation
t_charge_record费用缴费记录表,包含床位费、护理费elder_id、item_name、amount、pay_time
t_user系统用户表,区分管理员与护工角色username、password、role_type
t_dict数据字典表,装常量用type、label、value
t_building楼栋信息表,用于楼层与房间级联building_no、building_name、floors

这 13 张表里我建议你重点看t_order_status_log。表面上它只是多了个日志表,实际上它把工单的每一次状态变更都变成了可查记录。比如有一天家属投诉“我家老人的报修一周都没处理”,管理员可以通过这张表立刻定位:是谁在哪个环节把单子挂起了。很多同类项目压根没有这个设计,出了问题只能翻聊天记录,有这张表就能直接甩数据。

3. 核心模块拆解:一个报修工单从创建到办结的完整链路

这一章不给你讲空架构,跟着一条真实链路走:护工登录系统 → 提交报修单 → 管理员派单 → 维修工处理 → 状态关闭。每一层的代码怎么写的,异常怎么处理,前端怎么渲染,都拆出来看。

3.1 数据层:Mapper 接口与 XML 的 resultMap

先看实体类,RepairOrder.java的核心字段:

public class RepairOrder { private Long id; // 主键 private String orderNo; // 工单号,如 WX20240512-001 private Long elderId; // 老人 id private String content; // 报修内容 private Integer status; // 0待派单 1处理中 2已完成 3已关闭 private Long createBy; // 提交人用户 id private Date createTime; // 创建时间 private Long handleBy; // 维修工用户 id private Date handleTime; // 处理时间 private String remark; // 备注 // 省略 getter/setter }

status 字段没有用枚举类,而是直接用 Integer,配合常量类里定义的ORDER_STATUS_*静态变量,避免硬编码。实体字段命名和表字段完全一致,所以没有额外写@TableField那一堆注解,MyBatis 下划线与驼峰映射打开后自动处理。

Mapper 接口只需要一个方法:

@Mapper public interface RepairOrderMapper { void insert(@Param("order") RepairOrder order); int updateStatus(@Param("id") Long id, @Param("status") Integer status, @Param("operatorId") Long operatorId); List<RepairOrder> selectByCondition(@Param("status") Integer status, @Param("elderId") Long elderId); }

对应的 XML 长这样,resultMap把数据库字段映射到实体属性,selectByCondition是动态拼接查询条件的典型写法:

<mapper namespace="com.example.healthcommunity.mapper.RepairOrderMapper"> <resultMap id="orderMap" type="com.example.healthcommunity.entity.RepairOrder"> <id column="id" property="id"/> <result column="order_no" property="orderNo"/> <result column="elder_id" property="elderId"/> <result column="content" property="content"/> <result column="status" property="status"/> <result column="create_by" property="createBy"/> <result column="create_time" property="createTime"/> <result column="handle_by" property="handleBy"/> <result column="handle_time" property="handleTime"/> </resultMap> <select id="selectByCondition" resultMap="orderMap"> select * from t_repair_order <where> <if test="status != null"> and status = #{status} </if> <if test="elderId != null"> and elder_id = #{elderId} </if> </where> order by create_time desc </select> </mapper>

逻辑说明:resultMap里的column是数据库列名,property是实体属性名,两者不一致时需要手动指定。<where>标签会自动处理多余的第一个and,比加一个where 1=1干净得多。参数用#{status}走的是 PreparedStatement 预编译,能防 SQL 注入,千万别用字符串拼接的方式传参。order by create_time desc是最常用的清单排序,如果你想改成按紧急程度排,可以再琢磨加个priority字段。

3.2 业务层:Service 事务与状态机判断

业务层不能只是透传调用 Mapper,它要担起事务和状态流转正确性的责任。看RepairOrderServiceImpl.createOrder()的简化版:

@Override @Transactional(rollbackFor = Exception.class) public boolean createOrder(RepairOrder order) { // 1. 生成唯一工单号 String orderNo = "WX" + DateUtil.format(new Date(), "yyyyMMdd") + "-" + RandomUtil.randomNumbers(4); order.setOrderNo(orderNo); order.setStatus(ORDER_STATUS_WAITING); // 初始状态:待派单 // 2. 校验老人是否存在且状态为入住中 Elder elder = elderMapper.selectById(order.getElderId()); if (elder == null || elder.getStatus() != ELDER_STATUS_LIVING) { throw new ServiceException("老人信息不存在或未入住"); } // 3. 插入工单主记录 repairOrderMapper.insert(order); // 4. 写状态流转日志 OrderStatusLog log = new OrderStatusLog(); log.setOrderId(order.getId()); log.setFromStatus(null); // 新建无前状态 log.setToStatus(ORDER_STATUS_WAITING); log.setOperatorId(order.getCreateBy()); orderStatusLogMapper.insert(log); return true; }

逻辑说明:@Transactional声明该方法处于事务中,步骤 3 和步骤 4 要么同时成功,要么同时回滚,避免出现“工单建了但日志没记”的脏数据。我在改这类项目时习惯把事务加在实现类的 public 方法上,因为 Spring 默认用 JDK 动态代理,只有外部调用该方法才会触发事务拦截,如果是同类内部调用,this点方法不会走代理,事务会失效——这是新手踩得最多的一个坑。

参数说明:orderNo生成规则是固定前缀 + 当前日期 + 随机四位数字,防重复。如果你要部署到多实例环境,这种生成方式会有并发重复风险,生产上建议换成 Redis incr 或者雪花算法。这里还有个细节:步骤 2 查询老人状态,前置校验放在事务最前面,能减少无意义的锁开销。

3.3 控制层:参数校验与统一返回

Controller 层接收前端传参,调用 Service,但不写任何业务判断。核心代码如下:

@RestController @RequestMapping("/api/repair") public class RepairOrderController { private final RepairOrderService repairOrderService; public RepairOrderController(RepairOrderService repairOrderService) { this.repairOrderService = repairOrderService; } @PostMapping("/create") public Result create(@RequestBody @Valid RepairOrderCreateReq req) { RepairOrder order = new RepairOrder(); order.setElderId(req.getElderId()); order.setContent(req.getContent().trim()); order.setCreateBy(getCurrentUserId()); // 从 SecurityContext 或 Session 取 boolean success = repairOrderService.createOrder(order); return success ? Result.ok() : Result.fail("提交失败"); } @GetMapping("/list") public Result list(@RequestParam(required = false) Integer status, @RequestParam(required = false) Long elderId) { return Result.ok(repairOrderService.listByCondition(status, elderId)); } }

逻辑说明:@Valid配合RepairOrderCreateReqDTO 里的@NotBlank、@NotNull注解做参数校验,校验不通过时框架会抛出异常,被全局异常处理器转成带具体提示的Result。为什么不直接把RepairOrder实体暴露给前端?因为实体字段里有createBy、createTime这些不应该由客户端提交的字段,使用独立 DTO 能隔离接口契约和表结构,这个习惯越早养成越好。

Result.ok()和Result.fail()是common包里封装的静态方法,所有接口返回值格式保持一致,前端拿去直接用res.code === 200判断即可。注意控制器的构造函数式注入用的是改造后的写法,已经放弃了 Spring 旧文档里常用的@Autowired字段注入,这样的好处是单元测试时能直接 new 一个 Controller 传 mock service,不依赖 Spring 容器。

3.4 前端页面:Thymeleaf 模板如何绑定数据

因为项目没有做前后端分离,页面是 Thymeleaf 渲染的。看一下repair_order_list.html的关键片段:

<table class="table table-hover"> <thead> <tr> <th>工单号</th> <th>老人</th> <th>状态</th> <th>提交时间</th> <th class="text-center">操作</th> </tr> </thead> <tbody> <tr th:each="order : ${orderList}"> <td th:text="${order.orderNo}"></td> <td th:text="${order.elderName}"></td> <td> <span th:switch="${order.status}"> <span th:case="0" class="badge badge-warning">待派单</span> <span th:case="1" class="badge badge-info">处理中</span> <span th:case="2" class="badge badge-success">已完成</span> <span th:case="3" class="badge badge-secondary">已关闭</span> </span> </td> <td th:text="${#dates.format(order.createTime, 'yyyy-MM-dd HH:mm')}"></td> <td class="text-center"> <a th:href="@{'/repair/detail/' + ${order.id}}" class="btn btn-sm btn-outline-primary">详情</a> </td> </tr> </tbody> </table>

逻辑说明:th:each是 Thymeleaf 的循环语法,等价于 Java 里的增强 for。order.elderName这个属性并不存在实体类里,它是 Service 层做多表查询时用alias追加进去的冗余字段,可以用,但心里要清楚它跟主表字段的来源不同。th:switch是状态显示的技术方案,比用一堆th:if可读性更好,新增状态时只需要在 switch 里加一行。

这里有个血泪经验:Thymeleaf 的#dates.format依赖页面Content-Type中的 charset 设置,如果spring.thymeleaf.encoding没配成 UTF-8,页面上的时间和中文字符串都会变成乱码,你会在浏览器里看到一屏问号,而且控制台还不报错。下面避坑章节会专门讲这个。

4. 避坑指南:跑通这套 Java 项目最常见的六个拦路虎

4.1 数据库连接失败,不是配置写错了

  • 现象:启动时报Communications link failure,或Access denied for user。
  • 原因:本地 MySQL 是 8.x,而项目驱动是com.mysql.jdbc.Driver,且 URL 没带时区参数。MySQL 8 的驱动类名变成了com.mysql.cj.jdbc.Driver,并且强制要求serverTimezone。
  • 解决:老实的做法是装一个 MySQL 5.7,或者手动改 application.yml:驱动改com.mysql.cj.jdbc.Driver,URL 末尾拼接?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false。连接池驱动如果还是 druid 老版本,建议同步升级到 1.2.6 以上,注意 druid 的 wall filter 会拦截某些函数,遇到启动报 filter 初始化异常时可以先把 filter 配置注释掉。

4.2 中文乱码,一屏全是问号

  • 现象:页面和日志里中文变成???或者乱码字,但英文正常。
  • 原因:三层编码不一致。一是 HTML 文件没有声明 meta charset,二是spring.thymeleaf.encoding没有配置,三是 IDEA 的默认文件编码不是 UTF-8。
  • 解决:application.yml 里加spring.thymeleaf.encoding: UTF-8,IDEA Settings 里把 File Encodings 三项全改成 UTF-8,同时检查 HTML head 里有没有<meta charset="UTF-8">。改完重启,清浏览器缓存。如果还乱,看 p_om.xml 里project.build.sourceEncoding是不是 UTF-8。

4.3 页面样式全丢了,静态资源 404

  • 现象:页面能打开,但 css/js 全加载不出来,浏览器 F12 一堆 404。
  • 原因:Spring Boot 的静态资源默认放在classpath:/static/,如果模板里引用路径写的是/static/css/bootstrap.min.css,实际请求会变成/static/static/...。
  • 解决:模板中直接写/css/bootstrap.min.css,或者/css/${ctx}里别带 static。同时确认 config 包里的 WebMvcConfig 没有把/static/**拦截掉,如果项目用了拦截器做登录校验,记得在addPathPatterns里排除/static/**和/css/**、/js/**。

4.4 MyBatis 报 “Invalid bound statement (not found)”

  • 现象:启动正常,一调用 Mapper 方法就抛BindingException: Invalid bound statement (not found)。
  • 原因:Mapper 接口找到了,但对应 XML 文件没被扫描进容器。常见两种原因:XML 文件没有放到resources/mapper目录下,而是误放进到 java 包里;或者 application.yml 里忘了写mybatis.mapper-locations: classpath:mapper/*.xml。
  • 解决:确认 XML 路径和 yml 配置一致,如果 XML 和接口的 namespace 不对应也会报同样的错。检查无误后执行 mvn clean,把 target 里旧的 class 清掉再启动。

4.5 上传的图片过一会儿就 404

  • 现象:上传工单照片或老人头像时显示成功,刷新页面再访问图片变成 404。
  • 原因:配置文件里写的是相对路径,比如upload.path=./upload/,而 IDEA 启动时的工作目录和打包后 jar 的运行目录不一致,重启后相对路径变化,文件就找不到了。
  • 解决:把上传路径配置改成绝对路径,比如D:/data/healthcommunity/upload/(Linux 写/usr/local/healthcommunity/upload/),然后在 WebMvcConfig 里加一个资源映射,把访问前缀/upload/**指向磁盘绝对路径。注意 Linux 下目录权限要放开读写。

4.6 启动端口被占用,Spring Boot 启动失败

  • 现象:控制台报Web server failed to start. Port 8080 was already in use.
  • 原因:本地已有程序(常见为其他 Spring Boot 应用或被谁启动了同一工程)占用了 8080。
  • 解决:先换端口,application.yml 里server.port: 8081临时验证;要杀掉占用进程就在命令行执行netstat -ano | findstr 8080(Linux 用lsof -i:8080)找到 PID 后 kill。顺便检查一下是不是有多个 IDEA 工程残留进程没退出,这是开发机最常见的翻车现场。

5. 本地跑通:从解压 rar 到浏览器看到登录页的 30 分钟

5.1 环境准备:JDK、Maven、MySQL

  • JDK 必须 1.8,不建议用 11 或 17 直接跑,部分老依赖在更高版本 JDK 下会反射报错。装完后命令行验证java -version。
  • Maven 用 3.6.x,设好 settings.xml 的本地仓库和阿里云镜像,不然首次下载依赖会慢到怀疑人生。
  • MySQL 建议 5.7,字符集选 utf8mb4。图形界面用 Navicat 或命令行都行,先建一个库,比如healthcommunity。

5.2 初始化数据库

用 Navicat 新建连接,右键数据库选“运行 SQL 文件”,定位到解压目录的sql/health_community.sql。跑完之后确认一下 t_user 表里有没有初始账号。一般这套系统的初始账号是admin / 123456,如果你拿到的源码包里没有初始数据或者密码不是这个,就去data.sql或者 README 文件里找,实在找不到就自己往 t_user 表插一条记录,密码字段那是 MD5 还是 BCrypt 看字段长度就能判断出来——admin的 MD5 是21232f297a57a5a743894a0e4a801fc3,如果长度是 60 那多半是 BCrypt,别直接裸填明文进去。

5.3 修改配置文件

打开src/main/resources/application.yml,需要动的就三处:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/healthcommunity?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: encoding: UTF-8 cache: false mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.healthcommunity.entity configuration: map-underscore-to-camel-case: true upload: path: D:/healthcommunity/upload/

注意最后那行upload.path,Windows 用户照抄这个目录,提前把文件夹建出来;Linux/macOS 改成/home/xxx/upload。很多跑不通的原因就是没建这个目录,上传功能一调用就抛FileNotFoundException。driver-class-name这里用 MySQL 8 的驱动类名,如果你按第 4.1 节保留了 MySQL 5.7,也可以继续用com.mysql.jdbc.Driver,两者搭配都没问题。

5.4 启动项目并看日志

在项目根目录执行:

mvn clean spring-boot:run

第一次启动会下载不少依赖,耐心等。看到类似这行日志说明启动成功:

Tomcat started on port(s): 8080 (http) with context path '' Started HealthCommunityApplication in 9.781 seconds

浏览器打开http://localhost:8080/login,正常会跳转到登录页。输入账号密码后如果卡在登录没反应,优先检查控制台有没有Bound statement或数据库连接异常,这两类占了百分之七十的启动问题。

5.5 验证功能链路

登录进去后按下面这张表走一遍,能过就是真跑通了:

测试点操作步骤预期结果
登录admin / 123456 登录跳转首页,右上角显示用户名
老人档案新增一位老人,填姓名、身份证、床位老人列表出现该记录,床位状态变为占用
报修工单在工单模块创建一个报修,关联该老人工单号生成,状态为“待派单”
状态流转把工单派给某维修工,再标记为已完成工单状态更新,状态日志表多两条记录
活动报名创建一个活动,老人档案里点击报名活动报名人数 +1,重复报名被拦截
体检记录给老人录入一条体检数据老人详情页显示最近体检结果

如果某个环节失败,不要急着改代码,先看GlobalExceptionHandler有没有把异常吞掉。这套项目在 DEBUG 模式下日志很全,把logging.level.com.example.healthcommunity: DEBUG加到 yml 再重启,能看到每条 SQL 的参数和返回值,比对着代码盲猜效率高多了。

6. 二次开发:给社区系统新增“体检报告批量导入”功能

源码包自带的体检录入是一条一条新增的,但社区每季度组织一次大规模体检,护士需要把 Excel 里的几百条结果快速导入系统。改造思路很简单:前端放一个文件上传控件,后端用 POI 读 Excel,逐行解析后校验身份证号、检查日期、血压等字段,批量写入t_health_check表,最后把成功数和失败行号返回给页面。

核心逻辑写在 Service 实现里,关键代码粘贴给你:

@Override @Transactional(rollbackFor = Exception.class) public ImportResult importHealthChecks(MultipartFile file) { ImportResult result = new ImportResult(); Workbook workbook = WorkbookFactory.create(file.getInputStream()); Sheet sheet = workbook.getSheetAt(0); for (Row row : sheet) { if (row.getRowNum() == 0) continue; // 跳过表头 String idCard = row.getCell(0).getStringCellValue(); String checkDate = row.getCell(1).getStringCellValue(); String bp = row.getCell(2).getStringCellValue(); Elder elder = elderMapper.selectByIdCard(idCard.trim()); if (elder == null) { result.getFailedRows().add(row.getRowNum()); continue; } // 检查该老人本期是否已存在记录,存在则跳过 int exists = healthCheckMapper.countByElderIdAndDate(elder.getId(), checkDate); if (exists > 0) continue; HealthCheck check = new HealthCheck(); check.setElderId(elder.getId()); check.setCheckDate(DateUtil.parse(checkDate, "yyyy-MM-dd")); check.setBp(bp); check.setResult(bp.contains("正常") ? "正常" : "异常"); healthCheckMapper.insert(check); result.setSuccessCount(result.getSuccessCount() + 1); } return result; }

逻辑说明:事务加在整个批量方法上,任何一个批次失败都会整体回滚,避免导入一半的脏数据。校验身份证在前,查重在后,能保证不会因为重读同一个文件产生重复记录。ImportResult是自定义的返回对象,包含successCount和failedRows,前端拿到后把失败行号渲染成红色列表,护士照着Excel行号修改后重新上传即可。

那以后我每接手一个老项目,都会先看三样东西:事务注解写在哪一层、状态日志表存了什么、全局异常吞掉了什么。把这三样摸透了再动手改业务,能少走非常多的弯路。这套颐养社区源码包虽然技术栈不算新,但分层干净、链路完整,拿来当模仿样板跑一遍,你对 Spring Boot + MyBatis 的掌握绝对比只看短视频学来的扎实得多。希望帮到你。

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

RELATED READING

延伸阅读

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