# 后端基础结构与分页规范 ## 1. 文档定位 本文记录后端基础分层、通用实体、ID 策略、分页、Mapper、Service 和 Controller 的开发习惯。 本文适用于 Java + Spring Boot + MyBatis-Plus 项目。具体业务项目可以在此基础上补充自己的包名、模块边界、认证方式和接口响应格式。 ## 2. 分层结构 后端业务代码按以下结构组织: ```text server/src/main/java// ├── platform/ │ ├── common/ │ │ ├── domain/ │ │ ├── mapper/ │ │ ├── page/ │ │ ├── response/ │ │ └── security/ │ ├── message/ │ ├── ai/ │ └── system/ ├── workflows/ │ └── / └── integrations/ ``` 每个业务对象建议按以下文件拆分: ```text / ├── 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 extends BaseMapper { V selectVoById(Serializable id); List selectVoList(Wrapper wrapper); PageResult selectVoPage(PageQuery pageQuery, Wrapper 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 { private List 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 { 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 风格规范。