构建可持续交付的SaaS平台(5)——统一规范之数据模型和接口

Jagger|阅读 2
2026/01/20 18:46
SaaS平台平台架构
构建可持续交付的SaaS平台(5)——统一规范之数据模型和接口

001_image.jpg

构建可持续交付的SaaS平台(5)——谷雨开源SaaS接口规范之数据模型和接口

在SaaS平台研发体系中,数据模型是业务落地的核心载体,接口是服务间交互的关键桥梁。规范、统一的数据模型与接口设计,是保障平台可扩展性、可维护性及可持续交付的基础。G2rain作为面向企业级场景的SaaS开源平台,围绕认证与授权、微前端框架等核心能力构建解决方案,其Java服务层设计严格遵循统一规范。本文 将聚焦G2rain微服务Java体系中的数据模型设计与接口规范,拆解核心设计思路与实现细节。

首先明确G2rain Java服务的基础技术选型:所有Java服务均基于JDK21构建,核心原因是JDK21的虚拟线程特性可轻松支撑十万甚至百万级并发请求,大幅提升高并发处理能力。基于JDK21,Spring生态组件统一采用Spring Boot 4.0.x版本(对应Spring核心版本7.0.x),该版 本对JDK21的虚拟线程及其他新特性提供完善适配与升级支持,为规范落地奠定稳定技术基座。

一、基础模型类概述

G2rain倡导“表为系统设计核心”理念,开发初期需完成所有核心表设计,且表名、字段名必须遵循统一规范。Java层面功能与表一一对应:下划线分隔的表名转换为驼峰命名的Java类并添加用途后缀;表字段的下划线命名同样转换为Java类的驼峰属性。

为实现数据模型标准化,G2rain在 com.g2rain.common.model 包中提供统一方案,覆盖持久化层、业务层及接口响应层,核心基础类包括:

  • 持久化层 :BasePo(定义ID、创建时间、更新时间、版本号等通用约束);
  • 业务层 :BaseVo(出参基础类)、BaseDto(新增/编辑入参基础类)、BaseSelectListDto(列表查询入参基础类)、BasePageSelectListDto(分页查询入参基础类),适配不同业务场景的参数传递;
  • 统一响应 :Result(通用接口出参包装)、PageData(分页数据包装),实现接口响应标准化。

二、核心基础类详细说明

2.1 数据模型分层设计逻辑

G2rain采用分层数据模型设计,明确区分持久化层、展示层、业务层职责,避免跨层数据混乱,提升代码复用性与可维护性。各层级核心类设计如下:

2.1.1 BasePo - 持久化层核心类(Persistence Object)

所有数据库实体类必须继承BasePo,该类定义数据库表通用核心字段,实现持久化层标准化约束。核心代码与设计说明:

@Datapublic class BasePo {    private Long id;    private LocalDateTime updateTime;    private LocalDateTime createTime;    /**     * 版本号,支持乐观锁机制,数据更新时自增1     */    private Integer version;}

Java

  • 核心用途 :作为数据库表与Java实体的直接映射载体,精准对应表结构。
  • 命名规范 :下划线表名转驼峰并加Po后缀,如 sys_user 对应 SysUserPo 。
  • 关键设计 :内置version字段实现乐观锁,解决并发更新冲突,每次更新自增1。

2.1.2 BaseVo - 展示层核心类(View Object)

所有Controller层返回前端的查询数据需基于BaseVo扩展,是接口响应数据的基础载体,聚焦前端展示需求。核心代码:

@Datapublic class BaseVo {    private Long id;     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")    private String updateTime;     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")    private String createTime;}

Java

  • 核心用途 :封装接口响应数据,为前端提供标准化展示格式。
  • 核心特点 :① 时间适配:将BasePo的LocalDateTime转为String,避免前端解析异常;② 统一格式化:通过 @JsonFormat 指定格式为“yyyy-MM-dd HH:mm:ss”、时区GMT+8,确保跨时区一致性;③ 精简体积:移除前端无需关注的version字段。

2.1.3 BaseDto - 编辑入参核心类(Data Transfer Object)

用于封装创建、更新接口的请求参数,所有新增/编辑接口入参类需继承BaseDto,设计与BaseVo高度一致,便于数据复用与转换。核心代码:

@Datapublic class BaseDto {    private Long id;    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")    private String updateTime;    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")    private String createTime;}

Java

  • 核心用途 :作为创建/更新接口的请求参数载体,接收前端业务数据。
  • 核心特点 :① 时间适配:用String接收前端时间字符串,通过 @JsonFormat 标准化解析;② 结构复用:与BaseVo结构一致,减少跨层转换复杂度;③ 扩展性强:业务层可扩展字段适配不同场景。

2.1.4 BaseSelectListDto - 列表查询入参基类

针对列表查询场景设计,封装通用查询条件,所有列表查询接口入参类需继承该类。核心代码:

@Datapublic class BaseSelectListDto {    private Long id;            // 单个ID查询    private Set<Long> ids;        // 批量ID查询    private List<String> updateTime;  // 更新时间段:[开始时间, 结束时间]    private List<String> createTime;  // 创建时间段:[开始时间, 结束时间]     // 向ids集合添加元素    public void addId(Long id) {        if (ids == null) {            ids = new HashSet<>();        }        ids.add(id);    }}

Java

  • 核心用途 :封装列表查询通用条件,支持单个ID、批量ID及时间段查询。
  • 核心特点 :① 多维度查询:支持单个ID精准查询(详情关联)、批量ID查询(批量加载);② 灵活时间段:通过 createTime 和 updateTime 的List字段实现“大于/小于/介于某时间”的查询;③ 便捷方法:提供 addId 简化批量ID参数组装。

2.1.5 BasePageSelectListDto - 分页查询入参基类

继承BaseSelectListDto,在通用查询条件基础上新增分页参数及计算逻辑,适配分页列表查询。核心代码:

@EqualsAndHashCode(callSuper = true)@Setterpublic class BasePageSelectListDto extends BaseSelectListDto {    public static final int DEFAULT_PAGE_SIZE = 10;     private int pageNum;    // 当前页码,最小为1    private int pageSize;   // 每页条数,默认10     // 自动校正pageNum≥1    public int getPageNum() {        return Math.max(pageNum, 1);    }     // 自动校正pageSize默认10    public int getPageSize() {        return pageSize <= 0 ? DEFAULT_PAGE_SIZE : pageSize;    }     // 计算数据库查询偏移量(适配MySQL LIMIT语法)    public int getOffset() {        return (getPageNum() - 1) * getLimit();    }     // 获取每页查询条数    public int getLimit() {        return getPageSize();    }}

Java

  • 核心用途 :封装分页列表查询的所有参数(通用条件+分页参数)。
  • 核心特点 :① 继承性:复用BaseSelectListDto的通用查询能力;② 参数校验:自动校正pageNum≥1、pageSize默认10,避免非法参数;③ 分页计算:内置 getOffset (偏移量)和 getLimit (每页条数),直接适配数据库分页语法。

2.2 MapStruct 转换支持

G2rain采用分层数据模型设计,不同层级(如Po→Vo、Dto→Po)存在大量数据转换需求。为避免手动编写繁琐代码,统一采用MapStruct实现转换,并结合自定义通用转换器解决时间类型(LocalDateTime与String)转换问题。

结合前文的代码生成器,每个数据库表的分层模型类(Po、Vo、Dto等)生成时,会自动生成对应的MapStruct转换器,核心聚焦时间转换——G2rain在common包中提供 CommonConverter 通用转换器,供所有自动生成的转换器复用。

2.2.1 CommonConverter - 通用时间转换器

提供LocalDateTime与String的双向转换方法,通过MapStruct的 @Named 注解标记,便于具体转换器引用。核心代码:

public class CommonConverter {    /**     * LocalDateTime → String,格式:yyyy-MM-dd HH:mm:ss     */    @Named("localDateTimeToString")    public String localDateTimeToString(LocalDateTime time) {        return Moments.format(time);    }     /**     * String → LocalDateTime,解析格式:yyyy-MM-dd HH:mm:ss     */    @Named("stringToLocalDateTime")    public LocalDateTime stringToLocalDateTime(String time) {        return Moments.parse(time);    }}

Java

注: Moments 是G2rain封装的时间工具类,内部实现指定格式的时间格式化与解析,确保转换一致性。

2.2.2 MapStruct 转换器示例

以Test模块为例,自动生成的 TestConverter 实现 TestPo 、 TestVo 、 TestDto 间的转换,核心代码:

@Mapper(uses = CommonConverter.class)public interface TestConverter {     /**     * 单例实例,通过 {@link Mappers#getMapper(Class)} 获取 MapStruct 自动生成的实现。     */    TestConverter INSTANCE = Mappers.getMapper(TestConverter.class);     /**     * Po -> Vo     * 自动将 createTime 和 updateTime 从 {@link LocalDateTime} 转换为 {@link String}     */    @Mapping(target = "createTime", source = "createTime", qualifiedByName = "localDateTimeToString")    @Mapping(target = "updateTime", source = "updateTime", qualifiedByName = "localDateTimeToString")    TestVo po2vo(TestPo po);     /**     * Dto -> Po     * 自动将 createTime 和 updateTime 从 {@link String} 转换为 {@link LocalDateTime}     * 忽略 version 字段     * 忽略 deleteFlag 字段     */    @Mapping(target = "version", ignore = true)    @Mapping(target = "deleteFlag", ignore = true)    @Mapping(target = "createTime", source = "createTime", qualifiedByName = "stringToLocalDateTime")    @Mapping(target = "updateTime", source = "updateTime", qualifiedByName = "stringToLocalDateTime")    TestPo dto2po(TestDto dto);}

Java

  • 组件化配置 :通过 @Mapper(componentModel = "spring") 指定为Spring组件,可直接依赖注入使用;
  • 通用转换器复用 :通过 uses = CommonConverter.class 引入通用时间转换器,实现时间字段自动转换;
  • 场景化映射 :针对创建、更新场景,通过 @Mapping(ignore = true) 忽略无需前端传递的字段(如主键、版本号、系统填充时间),确保数据安全与一致性;
  • 批量转换支持 :提供 toVoList 等方法,适配批量数据查询与响应。

2.3 统一响应格式

为实现接口响应标准化,便于前端统一处理数据、后端统一处理异常日志,G2rain定义 Result (通用响应)和 PageData (分页响应)两套格式,所有Controller接口返回值需基于此封装。

2.3.1 Result - 通用响应结果封装

适用于所有接口,封装响应状态、错误信息、业务数据等核心内容。核心代码:

@Datapublic class Result<T> {    private int status;                    // 200成功,500失败(可扩展其他状态码)    private String errorCode;              // 错误码,status≠200时有效    private String errorMessage;           // 错误信息,status≠200时展示    private Map<String, Object> keyArgs;   // 键值参数,填充errorMessage占位符    private Object[] indexArgs;            // 索引参数,填充errorMessage占位符    private List<FieldError> fieldErrors;  // 字段错误列表,适配参数校验失败    private T data;                        // 业务数据:单个为Vo对象,多个为Vo列表    private String requestId;              // 请求ID,用于链路追踪与问题排查    private String requestTime;            // 请求时间,格式:yyyy-MM-dd HH:mm:ss}

Java

  • 状态码标准化 :采用HTTP风格状态码(200成功、500失败),便于前端快速判断结果;
  • 错误信息精细化 :通过 errorCode 和 errorMessage 区分错误类型,结合 keyArgs 、 indexArgs 支持动态占位符填充(如“参数{paramName}不能为空”);
  • 参数校验支持 : fieldErrors 封装参数校验失败详情(如字段名、错误原因),适配JSR-380规范;
  • 链路追踪 : requestId 记录请求唯一标识,结合日志系统实现全链路排查;
  • 通用性强 :泛型设计(

)适配不同类型业务数据。

2.3.2 PageData - 分页数据封装

适用于分页列表查询接口,封装分页核心参数与数据列表,通常作为 Result 的 data 字段值。核心代码:

@Datapublic class PageData<T> {    private int pageNum;        // 当前页码    private int pageSize;       // 每页条数    private long total;          // 总记录数    private int totalPages;      // 总页数(自动计算:total%pageSize==0? total/pageSize : total/pageSize+1)    private List<T> records;     // 当前页数据列表(Vo对象集合)}

Java

  • 分页参数完整 :包含 pageNum (当前页)、 pageSize (每页条数)、 total (总记录数)、 totalPages (总页数)四大核心参数,满足前端分页控件需求;
  • 总页数自动计算 : totalPages 通过总记录数与每页条数自动计算,无需前端额外处理;
  • 数据通用性 :泛型设计适配不同业务模块的分页数据(如用户列表、角色列表)。

分页接口响应示例 :

{    "status": 200,    "errorCode": null,    "errorMessage": null,    "data": {        "pageNum": 1,        "pageSize": 10,        "total": 100,        "totalPages": 10,        "records": [            {"id": 1, "userName": "admin", "createTime": "2024-01-01 10:00:00"},            // 更多用户数据...        ]    },    "requestId": "req-20240520123456-1234",    "requestTime": "2024-05-20 12:34:56"}

JSON

三、规范落地价值与总结

G2rain的数据模型与接口规范,基于企业级SaaS平台研发实践总结,核心价值是“标准化”与“高效率”,具体体现在:

  1. 提升研发效率 :通过统一基础类(BasePo、BaseVo等)避免重复定义通用字段;结合代码生成器,可 快速生成分层模型类、MapStruct转换器及基础CRUD接口,开发者聚焦核心业务逻辑,大幅缩短周期。
  2. 降低维护成本 :所有模块遵循统一命名、数据转换及响应格式规范,新开发者可快速上手;通过 requestId 等标准化字段实现全链路追踪,降低跨团队协作与长期维护成本。
  3. 保障平台稳定性 :通过乐观锁(version字段)、参数自动校验、非法参数校正等设计,从源头规避并发冲突 、数据异常;统一响应格式便于前端统一处理异常,提升用户体验。
  4. 支撑可持续交付 :标准化设计使平台具备良好扩展性,新增业务模块可复用现有规范与工具,无需重构基础架构;规 范代码结构便于自动化测试、持续集成与部署,支撑可持续迭代。

综上,数据模型与接口规范是G2rain微服务体系的核心基石。实际研发中,需严格遵循“表为核心”理念,基于统一基础类扩展业务模型,借助MapStruct实现高效转换,通过标准化响应保障接口一致性。后续,我们将围绕G2rain的微服务治理、安全防护等核心能力,持续分享企业级SaaS平台的构建规范与实践经验。