ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring Boot养老项目实战:MyBatis自动填充与分层架构设计

2026/9/12 21:52:14 拓冰建站 浏览量
Spring Boot养老项目实战:MyBatis自动填充与分层架构设计 简介中州养老项目设计源码是一套基于Java技术构建的养老服务平台完整工程面向Java开发者和养老信息化项目学习者。源码采用模块化分层设计包含common公共模块、web交互模块和service业务模块覆盖用户管理、数据访问、服务接口等关键功能适合用于智慧养老系统的学习参考与二次开发。压缩包共116个文件其中Java源文件99个承载核心业务逻辑XML配置文件13个用于环境与数据库连接等参数设置另有yml、json、配置文件及说明文档辅助项目构建与版本管理整体压缩后仅356KB结构紧凑清晰。目前该资源已有331人学习下载可直接解压导入IDE进行阅读和调试。通过学习本项目可以掌握Java Web项目从分层设计到配置管理的完整思路理解养老业务场景中常见模块的实现方式同时借助Maven配置文件快速了解依赖管理与构建流程为实际开发或毕业设计提供可落地的参考。1. 从文件列表看中州养老项目一个Spring Boot管理的业务基座中州养老项目从文件命名上就能看出它是典型的分层业务系统LoginController.java处理登录入口RoomTypeServiceImpl.java承载房型管理逻辑VisitController.java负责探访相关接口而AutoFillInterceptor.java用于在MyBatis写入数据时自动填充创建时间、更新时间这类公共字段。115个文件里99个是Java源文件13个XML配置文件加上Maven的pom.xml和Git忽略文件整体围绕Spring Boot MyBatis这套主流组合搭建。对于正在做养老、家政、物业这类实体服务行业管理系统的开发者来说这份源码的价值在于它把业务代码怎么写和工程结构怎么组织两件事同时演示了。本文将按模块拆解这套项目重点看分层方式、统一响应封装、MyBatis拦截器实现和Swagger文档化这几个可复用的设计点。2. 分层骨架与统一响应common/web/service如何协作2.1 三类模块的职责边界从文件列表里能明确看到三个分包前缀common、web、service。这是Spring Boot项目最常见的分包习惯但很多项目写着写着就把它退化成放工具类的目录和放Controller的目录失去了分层的约束力。common模块是底层支撑只允许被上层依赖不允许反向依赖。它一般放两类东西与业务无关的通用能力比如UUID.java、StringUtils.java以及跨模块共享的契约比如BasicEnum.java枚举基类、PageResponse.java分页响应封装。这些类不依赖Spring容器不依赖Servlet API可以独立编译和单测。web模块是HTTP层和协议层负责参数接收、参数校验、会话管理和响应装配。LoginController.java和VisitController.java属于这一层。这里只做翻译和调度不写业务规则。service模块是业务逻辑的落地位置RoomTypeServiceImpl.java体现的是典型的接口 实现模式RoomTypeService接口定义方法签名RoomTypeServiceImpl实现具体逻辑。这种拆法带来的直接好处是将来在接口上标注事务注解、缓存注解时不会污染Controller业务方法也能被非HTTP场景复用比如定时任务、MQ消费者直接注入Service。2.2 统一响应体的泛型设计看PageResponse.java这个类名它的职责不只是包装单条数据还要承载分页信息。常见做法是设计成一个泛型类字段包含total总数、records当前页数据集合、pageNum、pageSize再配一个code和message用于表达业务状态。下面是一个精简后可以直接用的实现public class PageResponseT { private int code; private String message; private long total; private ListT records; private int pageNum; private int pageSize; public static T PageResponseT ok(ListT records, long total, int pageNum, int pageSize) { PageResponseT response new PageResponse(); response.setCode(200); response.setMessage(success); response.setRecords(records); response.setTotal(total); response.setPageNum(pageNum); response.setPageSize(pageSize); return response; } public static T PageResponseT fail(int code, String message) { PageResponseT response new PageResponse(); response.setCode(code); response.setMessage(message); return response; } // getter / setter 省略 }ok方法把分页查询的四个要素一次打包Controller里只要拿到total和records就能直接返回不用每写一个接口就重复new一个HashMap。fail方法用于业务异常时返回code建议按业务域分段定义比如1000开头是参数错误2000开头是权限问题3000开头是业务冲突。把code各段的含义写进BasicEnum.java就能避免整个团队各写各的状态码。2.3 UUID与StringUtils在业务中的应用场景UUID.java在实际项目中很少直接拿来当主键因为UUID不带时间语义做范围查询和索引排序时性能不友好。常见的做法是生成去掉横线的32位字符串用于业务单号、流水号或临时凭证。StringUtils.java则是防null处理的集中地比如判断字符串是否为空、去空格、截断、隐藏手机号中间四位。这类方法写在common模块里是因为多个模块都会用到但要注意不要往StringUtils里堆砌业务方法比如把订单号格式化这种业务逻辑塞进去会让通用工具类逐渐腐化成大杂烩。2.3.1 为什么把枚举基类放在commonBasicEnum.java承担的是业务状态码 描述的常量收敛职责。常见实现方式是定义一个接口枚举实现这个接口这样在Service层处理业务分支时可以用统一的getValue()方法获取code而不是散落一地的魔法数字。房型状态、探访审批状态、预约状态这些都可以定义为枚举放在service模块里引用。3. AutoFillInterceptor自动填充MyBatis插件机制在房型管理里的实战3.1 MyBatis拦截器的触发原理AutoFillInterceptor.java是这份源码里技术含量最高的一个类。它的本质是MyBatis的Interceptor接口实现拦截的是Executor的update方法。当Mapper执行insert或update语句时拦截器会先于SQL真正执行之前介入通过反射修改参数对象中的字段值完成创建时间、更新时间、创建人、更新人的自动填充。简单梳理一下拦截器的配置顺序通过Intercepts注解声明拦截类型和方法签名然后在MyBatis配置中注册拦截器。Spring Boot场景下可以通过Configuration类返回一个Interceptor类型的Bean完成注册。下面是一个填充创建时间和更新时间的实现Intercepts({ Signature(type Executor.class, method update, args {MappedStatement.class, Object.class}) }) public class AutoFillInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement mappedStatement (MappedStatement) invocation.getArgs()[0]; String sqlCommandType mappedStatement.getSqlCommandType().name(); Object parameter invocation.getArgs()[1]; fillField(parameter, createTime, new Date()); if (UPDATE.equals(sqlCommandType)) { fillField(parameter, updateTime, new Date()); } return invocation.proceed(); } private void fillField(Object parameter, String fieldName, Object value) throws Exception { if (parameter null) { return; } MetaObject metaObject SystemMetaObject.forObject(parameter); if (metaObject.hasSetter(fieldName)) { metaObject.setValue(fieldName, value); } } Override public Object plugin(Object target) { return Plugin.wrap(target, this); } }invocation.getArgs()[0]拿到的是MappedStatement从它里面可以读出当前执行的SQL类型是INSERT还是UPDATE。参数对象通常是一个实体对象也有可能是ParamMap后者需要额外处理param1这类包裹参数。fillField方法里使用MetaObject而不是直接反射是因为MetaObject能处理MyBatis对参数的包装结构也支持级联属性的读写。3.2 插入与更新场景的参数差异处理INSERT场景需要填充创建时间和更新时间UPDATE场景只需要填充更新时间。如果项目里有逻辑删除字段UPDATE时还需要把删除标记位初始化为0。这个判断不能写在拦截器里硬编码更合理的做法是根据sqlCommandType区分再配合TableField注解的fill属性标记字段让拦截器只处理标记过的字段。3.2.1 和MyBatis-Plus的对比用过MyBatis-Plus的开发者都知道MP自带MetaObjectHandler来实现字段自动填充业务代码只需要在实体字段上标注TableField(fill FieldFill.INSERT)即可。而这份源码选择手写拦截器说明项目在起步阶段就决定不引入MP的重量级依赖或者团队希望保持对SQL的完全掌控。手写拦截器的收益在于不用被MP的动态SQL生成逻辑限制执行计划更可控代价是每个实体都必须有对应的字段否则反射设置值时会静默跳过。后者这份源码的做法用hasSetter做了保护字段缺失不会抛异常。3.3 RoomTypeServiceImpl中如何利用自动填充房型管理是养老机构的核心基础数据新增一个房型时创建时间和更新时间都由拦截器统一写入。看RoomTypeServiceImpl的职责划分应该包含房型列表查询、房型详情、新增房型、修改房型价格和状态启用禁用这几个方法。新增与修改操作走Mapper的insert和update自动填充字段在这两个方法里不用重复编写。3.3.1 房型状态流转的典型设计房型状态适合定义为枚举常见状态有空置、已预约、已入住、维护中。状态流转不能仅依靠Controller传参修改需要在Service里做状态校验。比如已入住的房型不能被设置为空置必须走退房流程。这个约束放在Service层通过BasicEnum定义的code做状态机判断避免前端绕过按钮直接调接口。以下是房型房态管理常见的表结构字段字段名类型说明idbigint主键room_novarchar房号room_typevarchar房型单人间/双人间/套间pricedecimal月费statustinyint1-空置 2-已预约 3-已入住 4-维护中create_timedatetime创建时间自动填充update_timedatetime更新时间自动填充价格字段建议用decimal而不是double避免浮点精度问题。房号字段注意校验唯一性这个唯一性约束既要在数据库层面建唯一索引也要在Service层提前查库做提示。自动填充保证的是审计字段不缺失业务数据的完整性仍然要靠数据库约束和Service层双重控制。4. 登录接口与Swagger配置web层请求从鉴权到文档化4.1 基于UUID的会话凭证生成LoginController.java和UUID.java一起看能推断出这套系统的登录方案是用户提交账号密码校验通过后生成一个UUID作为token返回给前端后续请求在请求头携带这个token。这种方案适合内部管理系统实现成本低不引入额外的认证框架。生成token的常见逻辑如下PostMapping(/login) public ResultVOString login(RequestBody LoginRequest request) { User user userService.authenticate(request.getUsername(), request.getPassword()); if (user null) { return ResultVO.fail(账号或密码错误); } String token UUID.randomUUID().toString().replace(-, ); redisTemplate.opsForValue().set(login:token: token, user.getId().toString(), 2, TimeUnit.HOURS); return ResultVO.ok(token); }登录成功之后把用户ID和token的映射写入Redis并设置两小时过期replace(-, )去掉横线是为了让token更紧凑。后续请求拿到token后去Redis反查用户ID查不到就返回401。这套方案比JWT简单服务端可以随时吊销token缺点是每次请求都有一次Redis查询开销。4.1.1 密码安全的关键细节密码校验不能直接比对数据库明文项目中应该在用户表存的是加盐后的哈希值。推荐使用BCrypt算法BCryptPasswordEncoder是Spring Security里现成的实现即使不引入完整Security依赖也可以单独引入spring-security-crypto。每次登录时从数据库取出哈希值用matches方法校验明文这样数据库泄露也不会直接暴露明文密码。4.2 SwaggerConfig解决接口文档同步问题SwaggerConfig.java的存在说明项目已经接了Swagger做接口文档。统一响应体PageResponse和Swagger配合时需要在配置里设置全局响应消息或使用ApiResponse注解描述错误码否则前端看到的文档只有数据结构不知道什么时候返回什么业务码。常见的Swagger配置类写法Configuration EnableSwagger2WebMvc public class SwaggerConfig { Bean public Docket apiDocket() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage(com.zhongzhou.web)) .paths(PathSelectors.any()) .build() .apiInfo(apiInfo()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(中州养老平台接口文档) .description(房型管理、探访管理、用户模块) .version(1.0) .build(); } }basePackage指定扫描web包下的Controller这样service包或common包里不该暴露的类不会出现在文档里。配置完成后访问/swagger-ui/index.html可以看到全部接口在线调试时不需要再依赖前端联调环境自己就能发请求验证。4.3 VisitController的探访流程封装VisitController.java处理探访登记这类业务的特点是操作链条短、状态变化频繁。探访登记一般经历四个步骤家属提交预约申请、机构审核确认、到访登记、离访签出。这套流程映射到Controller里就是/visit/book、/visit/audit、/visit/checkIn、/visit/checkOut四个端点。每个端点只做参数接收和结果返回真正的状态流转和业务判断全部在Service层完成。访客预约登记时通常需要填写老人ID、访客姓名、手机号、预约时间段和关系。手机号在前端做格式校验Service层再查一遍当天是否已有重复预约。审核通过后生成探访凭证到访时核验凭证并记录实际到访时间。这套流程和房型状态流转一样都是Service层状态机设计的具体应用。5. 配置校验技巧pom依赖、XML映射与接口验证的落地检查拿到源码包后不要急着跑起来先按下面三步做配置侧体检能避开大多数环境问题。先看pom.xml的依赖范围。养老项目常规依赖涉及Spring Boot Web、MyBatis Starter、MySQL驱动、Redis客户端、Swagger和Lombok。重点检查MyBatis和MySQL驱动的版本是否匹配当前数据库版本比如MySQL 8.x需要com.mysql:mysql-connector-j的坐标。再确认spring-boot-maven-plugin是否配置了mainClass指向Application启动类否则打包出来的jar可能无法用java -jar直接启动。再检查13个XML配置文件的归属。application配置文件中spring.datasource.url要核对serverTimezoneAsia/Shanghai时区不设置会出现数据库时间比本地时间早8小时的情况。Mapper XML文件要注意namespace必须指向对应的Mapper接口全限定名resultType要写实体类的完整包名少写或写错namespace时启动阶段不报错运行期才抛BindingException排查起来很费时间。最后用命令行实际验证接口链路。启动项目后用curl模拟一次带token的请求验证自动填充是否生效curl -X POST http://localhost:8080/api/room/type/add \ -H Content-Type: application/json \ -H token: 9f8e7d6c5b4a \ -d {roomNo:A101,roomType:单人间,price:3200.00} mysql -uroot -p -e select room_no, create_time, update_time from room_type where room_noA101;第一段命令提交新增房型请求token换成登录接口返回的真实值。第二段命令直接查库验证create_time和update_time是否被AutoFillInterceptor自动写入。如果查询结果时间为null优先检查拦截器是否在配置中注册成功以及实体字段名是否和MetaObjectsetter名称一致。较新版本的Spring Boot默认开启HikariCP连接池观察控制台日志里有没有出现连接初始化超时如有则需要调大spring.datasource.hikari.connection-timeout。本文还有配套的精品资源点击获取