Files
th-hotel-simple/docs/import/reusable/backend-base-structure-pagination-guidelines.md

13 KiB
Raw Permalink Blame History

后端基础结构与分页规范

1. 文档定位

本文记录后端基础分层、通用实体、ID 策略、分页、Mapper、Service 和 Controller 的开发习惯。

本文适用于 Java + Spring Boot + MyBatis-Plus 项目。具体业务项目可以在此基础上补充自己的包名、模块边界、认证方式和接口响应格式。

2. 分层结构

后端业务代码按以下结构组织:

server/src/main/java/<base-package>/
├── platform/
│   ├── common/
│   │   ├── domain/
│   │   ├── mapper/
│   │   ├── page/
│   │   ├── response/
│   │   └── security/
│   ├── message/
│   ├── ai/
│   └── system/
├── workflows/
│   └── <business-domain>/
└── integrations/

每个业务对象建议按以下文件拆分:

<domain>/
├── domain/
│   └── Xxx.java
├── mapper/
│   └── XxxMapper.java
├── service/
│   ├── XxxService.java
│   └── impl/XxxServiceImpl.java
├── control/
│   └── XxxController.java
├── repository/
│   └── XxxRepository.java
└── common/
    ├── request/
    │   ├── XxxCreateRequest.java
    │   ├── XxxUpdateRequest.java
    │   └── XxxQueryRequest.java
    ├── result/
    │   └── XxxResult.java
    ├── dto/
    │   └── XxxSnapshot.java
    └── enums/
        └── XxxStatus.java

规则:

  • platform 放通用能力。
  • workflows 放业务流程。
  • integrations 放外部系统适配器;外部协议适配类建议放在 integrations/<capability>/<provider>/adapter
  • 平台层不得依赖具体业务流程。
  • 业务层不得直接依赖外部系统 DTO。
  • Controller 不直接访问 Mapper。
  • 前端不直接调用外部系统。
  • 外部协议到内部命令或 DTO 的转换类使用 AdapterConverter 等后缀,不使用 Mapper 命名,避免和 MyBatis Mapper 混淆。

3. 低耦合与可维护性

后端模块必须保持低耦合、高内聚和高可维护性。设计新功能时,应先确认模块归属、依赖方向和公开契约,再创建类和接口。

落地要求:

  • 一个业务模块只表达一个清晰业务能力,避免在同一个 Service 中混入多个业务流程。
  • platform 只能提供部门中立能力,不依赖 workflows
  • workflows 可以依赖 platform,但不同业务流程之间不得随意互相调用内部实现。
  • integrations 只保存外部系统适配器和外部 DTO不让外部 DTO 进入领域模型。
  • 跨模块调用通过 Service、Port、API 契约或事件完成,不直接访问对方 Mapper、Entity 或私有工具类。
  • Controller 只调用本模块或明确授权的应用服务,不跨层访问 Mapper 或 Adapter。
  • Mapper 只负责本聚合或本表的数据访问,不承载跨业务编排。
  • 不为了重命名 MyBatis-Plus 自带方法而在 Mapper 中写薄 default 包装;语义化查询优先放在 Repository 或确有必要的自定义 Mapper 方法中。
  • 公共能力先放在业务模块内,出现真实重复和稳定语义后再提升到 platform
  • 不为少量相似代码提前抽象大而全的 CommonServiceCommonUtil 或通用模型。
  • 当修改一个模块会牵动多个无关模块时,应优先检查依赖方向和接口边界。
  • 测试优先覆盖模块公开行为、跨模块契约和关键业务边界。

4. ID 策略

4.1 内部主键

默认使用数据库内部主键:

数据库类型BIGINT
Java 类型Long
生成方式MyBatis-Plus IdType.ASSIGN_ID
算法类型Snowflake 风格 64 位 ID
常见长度18 到 19 位数字

推荐实体写法:

@TableId(type = IdType.ASSIGN_ID)
private Long id;

规则:

  • 内部主键用于数据库主键、关联关系和内部查询。
  • 不依赖数据库自增。
  • 不使用 UUID 作为默认数据库主键。
  • 不把业务含义塞进主键。
  • 不根据主键大小判断业务时间顺序,除非明确验证过生成器语义。

4.2 API 与前端 ID

后端返回给前端的 ID 必须按字符串处理。

原因JavaScript number 安全整数上限是 9007199254740991Snowflake 这类 18 到 19 位 ID 可能丢精度。

规则:

  • API JSON 中 ID 建议返回字符串。
  • TypeScript 中 ID 类型使用 string
  • 前端不使用 number 保存后端 Long ID。
  • 前端不对 ID 做数学运算。

示例:

{
  "id": "1890123456789012345"
}

4.3 公开 ID

如果需要不可枚举、适合公开传播的 ID不要替代内部主键另加字段

publicId
externalId
businessKey

可选方案:

  • ULIDVARCHAR(26)
  • UUIDVARCHAR(36)
  • 业务编号:按业务规则生成

规则:

  • 内部主键负责关联。
  • 公开 ID 负责对外展示、分享、回调或跨系统引用。
  • 是否需要公开 ID 由具体业务决定,不默认添加。

5. BaseEntity 规范

推荐统一基础实体:

public abstract class BaseEntity {

    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;

    private String createdBy;

    private String updatedBy;
}

规则:

  • id 使用 Long + ASSIGN_ID
  • 数据库字段使用 BIGINT
  • createdAt / updatedAt 由后端自动填充。
  • createdBy / updatedBy 由后端当前用户上下文自动填充。
  • 前端不得传入审计字段。
  • 业务代码不得散落手动填写审计字段。
  • 不把查询参数、分页参数、params Map 放进 BaseEntity
  • Request 对象不继承 BaseEntity

6. createdBy / updatedBy 来源

createdBy / updatedBy 来自后端当前用户上下文,不来自前端请求体。

建议定义统一接口:

public interface CurrentUserProvider {

    String getCurrentUserId();
}

初期没有登录系统时,可以返回:

system
local-dev

未来接入登录后,可以从以下来源获取:

  • Spring Security SecurityContext
  • JWT subject
  • Session 用户
  • 网关注入的用户 Header
  • 内部系统任务身份

推荐通过 MyBatis-Plus MetaObjectHandler 自动填充:

insert 时填充:
- createdAt
- updatedAt
- createdBy
- updatedBy

update 时填充:
- updatedAt
- updatedBy

字段类型建议:

createdBy: VARCHAR
updatedBy: VARCHAR

原因:操作者可能是内部用户、系统任务、外部集成、本地开发身份或未来 IAM subject不一定永远是数据库用户表的 Long ID。

如果后续有稳定用户表,可以额外添加:

createdByUserId BIGINT
updatedByUserId BIGINT

但不建议第一阶段默认添加。

7. Domain / Entity 规范

DomainEntity 对应数据库表结构,只表达持久化字段和基础业务含义。

规则:

  • 可以继承统一 BaseEntity
  • 不直接作为 Controller 入参。
  • 不直接作为 API 响应返回给前端。
  • 不混入外部系统 DTO 字段。
  • 金额使用 BigDecimal
  • 时间使用 LocalDateTime 或明确时区语义的类型。
  • 状态字段使用稳定英文代码或枚举,不使用中文展示文案。
  • 字段注释说明业务含义,不只复述字段名。

8. Request / QueryRequest 规范

请求对象用于接收前端入参。

建议拆分:

XxxCreateRequest
XxxUpdateRequest
XxxQueryRequest

规则:

  • 不使用一个大而全的 Bo 承载所有场景。
  • Request、Command、Query 条件建议放在模块内 common/request
  • 创建、更新、查询参数分开。
  • 使用 Bean Validation 做基础校验。
  • 查询对象可以包含分页、排序、时间范围等条件。
  • Request 不继承 BaseEntity
  • Request 不包含 createdByupdatedBycreatedAtupdatedAt
  • 不把前端展示文案作为业务参数。

9. Response / VO 规范

响应对象用于返回给前端。

规则:

  • 不直接返回 Entity。
  • 不暴露内部字段、Secret、外部系统原始响应或敏感数据。
  • 字段名稳定。
  • 前端不能依赖中文或英文文案判断业务。
  • Long ID 返回给前端时按字符串处理。
  • 时间统一按 API 规范格式化。
  • 列表接口使用统一分页响应。
  • Result、分页结果、操作结果建议放在模块内 common/result;跨层 DTO、Snapshot、Draft 建议放在模块内 common/dto

10. Mapper 规范

Mapper 基于 MyBatis-Plus。

建议提供统一基础接口:

public interface BaseMapperPlus<M, T, V> extends BaseMapper<T> {

    V selectVoById(Serializable id);

    List<V> selectVoList(Wrapper<T> wrapper);

    PageResult<V> selectVoPage(PageQuery pageQuery, Wrapper<T> wrapper);
}

规则:

  • Mapper 只负责数据库访问。
  • 复杂业务判断不放在 Mapper。
  • 优先使用 MyBatis-Plus Wrapper。
  • MyBatis-Plus 自带的 selectByIdinsertupdateById 等继承方法不需要额外 default 包装。
  • 自定义 default 方法只有在表达明确业务语义且能减少真实重复时才添加,不能只是改名转调。
  • 手写 SQL 必须使用 #{} 参数绑定。
  • 禁止使用 ${} 拼接业务参数。
  • 手写 SQL 要避免返回过宽字段。
  • Mapper 不直接依赖 Controller Request。
  • Mapper 不调用外部系统。

11. Service 规范

Service 分接口和实现:

XxxService
XxxServiceImpl

职责:

  • 编排业务流程。
  • 管理事务边界。
  • 调用 Mapper、Repository 或外部端口。
  • 做业务校验、幂等、并发版本、审计和失败恢复。
  • 完成 Entity 与 Response 的转换。

规则:

  • Controller 不直接调用 Mapper。
  • Service 不返回数据库 Entity 给 Controller。
  • Service 不吞异常。
  • 失败要转换为受控业务异常。
  • 日志只记录必要上下文,不输出敏感信息。
  • 不在 Service 中硬编码中文展示文案作为业务判断。
  • 外部系统调用通过端口或 Adapter 隔离。

12. Controller 规范

Controller 只做 HTTP 入口。

职责:

  • 路由定义。
  • 参数校验。
  • 权限入口。
  • 调用 Service。
  • 返回统一响应结构。

规则:

  • Controller 不直接访问 Mapper。
  • Controller 不直接调用外部系统 Adapter。
  • 方法名必须和业务语义一致。
  • 不在 Controller 写复杂业务逻辑。
  • 不返回 Entity。
  • 不暴露内部异常栈。
  • 不接收或信任前端传入的审计字段。

13. 分页规范

统一分页请求对象:

public class PageQuery {

    private Integer pageNum = 1;

    private Integer pageSize = 20;

    private String orderBy;

    private String orderDirection;
}

规则:

  • pageNum 从 1 开始。
  • pageSize 默认 20。
  • pageSize 必须有最大值限制,例如 100。
  • 不允许默认 Integer.MAX_VALUE 查询全部。
  • 排序字段必须做白名单映射。
  • 不能直接拼接前端字段进 SQL。
  • orderDirection 只允许 ascdesc

统一分页响应:

public class PageResult<T> {

    private List<T> items;

    private long total;

    private int pageNum;

    private int pageSize;
}

推荐 JSON

{
  "items": [],
  "total": 0,
  "pageNum": 1,
  "pageSize": 20
}

不建议使用 rows/total 这种偏表格组件的命名,除非前端组件强依赖。

14. 统一响应规范

可以使用统一响应包装,例如:

public class ApiResponse<T> {

    private String code;

    private String message;

    private T data;
}

规则:

  • code 使用稳定英文代码。
  • message 只用于展示,不用于前端业务判断。
  • 错误响应不返回 Secret、Token、SQL、内部堆栈或个人敏感信息。
  • 业务错误、参数错误、系统错误要有清晰边界。
  • 前端业务判断依赖 code 和结构化字段,不依赖 message

15. 从旧项目借鉴但不照搬

可以借鉴:

  • Domain / Mapper / Service / Controller 分层。
  • BO / VO 分离思想。
  • BaseMapperPlus 减少 Entity 到 VO 转换重复代码的思想。
  • PageQuery + 分页结果 的统一封装。
  • BaseEntity 统一审计字段的思想。

不照搬:

  • Mapper 中使用 ${} 拼 SQL。
  • catch 后空处理。
  • 默认 pageSize = Integer.MAX_VALUE
  • 在日志里打印完整业务对象。
  • Controller 方法名和业务语义不一致。
  • 创建、更新、查询共用一个大 Bo
  • Request 继承包含持久化审计字段的 BaseEntity
  • 业务代码手动散落填写 createdBy / updatedBy
  • 前端按 number 处理后端 Long ID。

16. 与 Alibaba Java Coding Guidelines 的关系

本规范默认遵守 Alibaba Java Coding Guidelines尤其是

  • 命名清晰。
  • 不写魔法值。
  • 异常不吞掉。
  • 日志不泄露敏感信息。
  • 集合、空值、字符串、时间、金额和 BigDecimal 使用安全写法。
  • 复杂流程、幂等、事务、外部字段映射和脱敏逻辑写必要中文注释。

项目自身业务边界优先级高于通用 Java 风格规范。