13 KiB
后端基础结构与分页规范
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 的转换类使用
Adapter、Converter等后缀,不使用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。 - 不为少量相似代码提前抽象大而全的
CommonService、CommonUtil或通用模型。 - 当修改一个模块会牵动多个无关模块时,应优先检查依赖方向和接口边界。
- 测试优先覆盖模块公开行为、跨模块契约和关键业务边界。
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 安全整数上限是 9007199254740991,Snowflake 这类 18 到 19 位 ID 可能丢精度。
规则:
- API JSON 中 ID 建议返回字符串。
- TypeScript 中 ID 类型使用
string。 - 前端不使用
number保存后端 Long ID。 - 前端不对 ID 做数学运算。
示例:
{
"id": "1890123456789012345"
}
4.3 公开 ID
如果需要不可枚举、适合公开传播的 ID,不要替代内部主键,另加字段:
publicId
externalId
businessKey
可选方案:
- ULID:
VARCHAR(26) - UUID:
VARCHAR(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由后端当前用户上下文自动填充。- 前端不得传入审计字段。
- 业务代码不得散落手动填写审计字段。
- 不把查询参数、分页参数、
paramsMap 放进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 规范
Domain 或 Entity 对应数据库表结构,只表达持久化字段和基础业务含义。
规则:
- 可以继承统一
BaseEntity。 - 不直接作为 Controller 入参。
- 不直接作为 API 响应返回给前端。
- 不混入外部系统 DTO 字段。
- 金额使用
BigDecimal。 - 时间使用
LocalDateTime或明确时区语义的类型。 - 状态字段使用稳定英文代码或枚举,不使用中文展示文案。
- 字段注释说明业务含义,不只复述字段名。
8. Request / QueryRequest 规范
请求对象用于接收前端入参。
建议拆分:
XxxCreateRequest
XxxUpdateRequest
XxxQueryRequest
规则:
- 不使用一个大而全的
Bo承载所有场景。 - Request、Command、Query 条件建议放在模块内
common/request。 - 创建、更新、查询参数分开。
- 使用 Bean Validation 做基础校验。
- 查询对象可以包含分页、排序、时间范围等条件。
- Request 不继承
BaseEntity。 - Request 不包含
createdBy、updatedBy、createdAt、updatedAt。 - 不把前端展示文案作为业务参数。
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 自带的
selectById、insert、updateById等继承方法不需要额外 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只允许asc或desc。
统一分页响应:
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 风格规范。