482 lines
12 KiB
Markdown
482 lines
12 KiB
Markdown
# 后端基础结构与分页规范
|
||
|
||
## 1. 文档定位
|
||
|
||
本文记录后端基础分层、通用实体、ID 策略、分页、Mapper、Service 和 Controller 的开发习惯。
|
||
|
||
本文适用于 Java + Spring Boot + MyBatis-Plus 项目。具体业务项目可以在此基础上补充自己的包名、模块边界、认证方式和接口响应格式。
|
||
|
||
## 2. 分层结构
|
||
|
||
后端业务代码按以下结构组织:
|
||
|
||
```text
|
||
server/src/main/java/<base-package>/
|
||
├── platform/
|
||
│ ├── common/
|
||
│ │ ├── domain/
|
||
│ │ ├── mapper/
|
||
│ │ ├── page/
|
||
│ │ ├── response/
|
||
│ │ └── security/
|
||
│ ├── message/
|
||
│ ├── ai/
|
||
│ └── system/
|
||
├── workflows/
|
||
│ └── <business-domain>/
|
||
└── integrations/
|
||
```
|
||
|
||
每个业务对象建议按以下文件拆分:
|
||
|
||
```text
|
||
<domain>/
|
||
├── domain/
|
||
│ └── Xxx.java
|
||
├── mapper/
|
||
│ └── XxxMapper.java
|
||
├── service/
|
||
│ ├── XxxService.java
|
||
│ └── impl/XxxServiceImpl.java
|
||
├── api/
|
||
│ └── XxxController.java
|
||
├── dto/
|
||
│ ├── XxxCreateRequest.java
|
||
│ ├── XxxUpdateRequest.java
|
||
│ └── XxxQueryRequest.java
|
||
└── vo/
|
||
└── XxxResponse.java
|
||
```
|
||
|
||
规则:
|
||
|
||
- `platform` 放通用能力。
|
||
- `workflows` 放业务流程。
|
||
- `integrations` 放外部系统适配器。
|
||
- 平台层不得依赖具体业务流程。
|
||
- 业务层不得直接依赖外部系统 DTO。
|
||
- Controller 不直接访问 Mapper。
|
||
- 前端不直接调用外部系统。
|
||
|
||
## 3. 低耦合与可维护性
|
||
|
||
后端模块必须保持低耦合、高内聚和高可维护性。设计新功能时,应先确认模块归属、依赖方向和公开契约,再创建类和接口。
|
||
|
||
落地要求:
|
||
|
||
- 一个业务模块只表达一个清晰业务能力,避免在同一个 Service 中混入多个业务流程。
|
||
- `platform` 只能提供部门中立能力,不依赖 `workflows`。
|
||
- `workflows` 可以依赖 `platform`,但不同业务流程之间不得随意互相调用内部实现。
|
||
- `integrations` 只保存外部系统适配器和外部 DTO,不让外部 DTO 进入领域模型。
|
||
- 跨模块调用通过 Service、Port、API 契约或事件完成,不直接访问对方 Mapper、Entity 或私有工具类。
|
||
- Controller 只调用本模块或明确授权的应用服务,不跨层访问 Mapper 或 Adapter。
|
||
- Mapper 只负责本聚合或本表的数据访问,不承载跨业务编排。
|
||
- 公共能力先放在业务模块内,出现真实重复和稳定语义后再提升到 `platform`。
|
||
- 不为少量相似代码提前抽象大而全的 `CommonService`、`CommonUtil` 或通用模型。
|
||
- 当修改一个模块会牵动多个无关模块时,应优先检查依赖方向和接口边界。
|
||
- 测试优先覆盖模块公开行为、跨模块契约和关键业务边界。
|
||
|
||
## 4. ID 策略
|
||
|
||
### 4.1 内部主键
|
||
|
||
默认使用数据库内部主键:
|
||
|
||
```text
|
||
数据库类型:BIGINT
|
||
Java 类型:Long
|
||
生成方式:MyBatis-Plus IdType.ASSIGN_ID
|
||
算法类型:Snowflake 风格 64 位 ID
|
||
常见长度:18 到 19 位数字
|
||
```
|
||
|
||
推荐实体写法:
|
||
|
||
```java
|
||
@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 做数学运算。
|
||
|
||
示例:
|
||
|
||
```json
|
||
{
|
||
"id": "1890123456789012345"
|
||
}
|
||
```
|
||
|
||
### 4.3 公开 ID
|
||
|
||
如果需要不可枚举、适合公开传播的 ID,不要替代内部主键,另加字段:
|
||
|
||
```text
|
||
publicId
|
||
externalId
|
||
businessKey
|
||
```
|
||
|
||
可选方案:
|
||
|
||
- ULID:`VARCHAR(26)`
|
||
- UUID:`VARCHAR(36)`
|
||
- 业务编号:按业务规则生成
|
||
|
||
规则:
|
||
|
||
- 内部主键负责关联。
|
||
- 公开 ID 负责对外展示、分享、回调或跨系统引用。
|
||
- 是否需要公开 ID 由具体业务决定,不默认添加。
|
||
|
||
## 5. BaseEntity 规范
|
||
|
||
推荐统一基础实体:
|
||
|
||
```java
|
||
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` 来自后端当前用户上下文,不来自前端请求体。
|
||
|
||
建议定义统一接口:
|
||
|
||
```java
|
||
public interface CurrentUserProvider {
|
||
|
||
String getCurrentUserId();
|
||
}
|
||
```
|
||
|
||
初期没有登录系统时,可以返回:
|
||
|
||
```text
|
||
system
|
||
local-dev
|
||
```
|
||
|
||
未来接入登录后,可以从以下来源获取:
|
||
|
||
- Spring Security `SecurityContext`
|
||
- JWT subject
|
||
- Session 用户
|
||
- 网关注入的用户 Header
|
||
- 内部系统任务身份
|
||
|
||
推荐通过 MyBatis-Plus `MetaObjectHandler` 自动填充:
|
||
|
||
```text
|
||
insert 时填充:
|
||
- createdAt
|
||
- updatedAt
|
||
- createdBy
|
||
- updatedBy
|
||
|
||
update 时填充:
|
||
- updatedAt
|
||
- updatedBy
|
||
```
|
||
|
||
字段类型建议:
|
||
|
||
```text
|
||
createdBy: VARCHAR
|
||
updatedBy: VARCHAR
|
||
```
|
||
|
||
原因:操作者可能是内部用户、系统任务、外部集成、本地开发身份或未来 IAM subject,不一定永远是数据库用户表的 Long ID。
|
||
|
||
如果后续有稳定用户表,可以额外添加:
|
||
|
||
```text
|
||
createdByUserId BIGINT
|
||
updatedByUserId BIGINT
|
||
```
|
||
|
||
但不建议第一阶段默认添加。
|
||
|
||
## 7. Domain / Entity 规范
|
||
|
||
`Domain` 或 `Entity` 对应数据库表结构,只表达持久化字段和基础业务含义。
|
||
|
||
规则:
|
||
|
||
- 可以继承统一 `BaseEntity`。
|
||
- 不直接作为 Controller 入参。
|
||
- 不直接作为 API 响应返回给前端。
|
||
- 不混入外部系统 DTO 字段。
|
||
- 金额使用 `BigDecimal`。
|
||
- 时间使用 `LocalDateTime` 或明确时区语义的类型。
|
||
- 状态字段使用稳定英文代码或枚举,不使用中文展示文案。
|
||
- 字段注释说明业务含义,不只复述字段名。
|
||
|
||
## 8. Request / QueryRequest 规范
|
||
|
||
请求对象用于接收前端入参。
|
||
|
||
建议拆分:
|
||
|
||
```text
|
||
XxxCreateRequest
|
||
XxxUpdateRequest
|
||
XxxQueryRequest
|
||
```
|
||
|
||
规则:
|
||
|
||
- 不使用一个大而全的 `Bo` 承载所有场景。
|
||
- 创建、更新、查询参数分开。
|
||
- 使用 Bean Validation 做基础校验。
|
||
- 查询对象可以包含分页、排序、时间范围等条件。
|
||
- Request 不继承 `BaseEntity`。
|
||
- Request 不包含 `createdBy`、`updatedBy`、`createdAt`、`updatedAt`。
|
||
- 不把前端展示文案作为业务参数。
|
||
|
||
## 9. Response / VO 规范
|
||
|
||
响应对象用于返回给前端。
|
||
|
||
规则:
|
||
|
||
- 不直接返回 Entity。
|
||
- 不暴露内部字段、Secret、外部系统原始响应或敏感数据。
|
||
- 字段名稳定。
|
||
- 前端不能依赖中文或英文文案判断业务。
|
||
- Long ID 返回给前端时按字符串处理。
|
||
- 时间统一按 API 规范格式化。
|
||
- 列表接口使用统一分页响应。
|
||
|
||
## 10. Mapper 规范
|
||
|
||
Mapper 基于 MyBatis-Plus。
|
||
|
||
建议提供统一基础接口:
|
||
|
||
```java
|
||
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。
|
||
- 手写 SQL 必须使用 `#{}` 参数绑定。
|
||
- 禁止使用 `${}` 拼接业务参数。
|
||
- 手写 SQL 要避免返回过宽字段。
|
||
- Mapper 不直接依赖 Controller Request。
|
||
- Mapper 不调用外部系统。
|
||
|
||
## 11. Service 规范
|
||
|
||
Service 分接口和实现:
|
||
|
||
```text
|
||
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. 分页规范
|
||
|
||
统一分页请求对象:
|
||
|
||
```java
|
||
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`。
|
||
|
||
统一分页响应:
|
||
|
||
```java
|
||
public class PageResult<T> {
|
||
|
||
private List<T> items;
|
||
|
||
private long total;
|
||
|
||
private int pageNum;
|
||
|
||
private int pageSize;
|
||
}
|
||
```
|
||
|
||
推荐 JSON:
|
||
|
||
```json
|
||
{
|
||
"items": [],
|
||
"total": 0,
|
||
"pageNum": 1,
|
||
"pageSize": 20
|
||
}
|
||
```
|
||
|
||
不建议使用 `rows/total` 这种偏表格组件的命名,除非前端组件强依赖。
|
||
|
||
## 14. 统一响应规范
|
||
|
||
可以使用统一响应包装,例如:
|
||
|
||
```java
|
||
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 风格规范。
|