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

495 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 后端基础结构与分页规范
## 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
├── 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 内部主键
默认使用数据库内部主键:
```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` 承载所有场景。
- 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。
建议提供统一基础接口:
```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。
- MyBatis-Plus 自带的 `selectById``insert``updateById` 等继承方法不需要额外 default 包装。
- 自定义 default 方法只有在表达明确业务语义且能减少真实重复时才添加,不能只是改名转调。
- 手写 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 风格规范。