后端项目开发规范(通用)
本文档为后端项目提供了一套通用的开发规范,旨在提升代码质量、可维护性与团队协作效率。本规范独立于具体语言和框架,适用于各类后端技术栈。
当前后端项目优先选用 PostgreSQL 作为数据库,因此数据类型及 SQL 相关规范均以此为基准。
1. 架构与设计原则
Section titled “1. 架构与设计原则”1.1. 项目结构
Section titled “1.1. 项目结构”应根据所使用的技术栈,采用社区推荐的项目结构。确保代码模块化、职责单一,便于维护和扩展。
- 项目必须采用清晰的分层结构,至少拆分为对外暴露接口的
Controller层和承载业务逻辑的Service层,避免在Controller中直接编写复杂业务代码或数据访问逻辑。 - 关于
Controller和Service层具体如何命名和组织,可参考所使用语言和框架的最佳实践。 Service层必须遵循“接口先行”原则:先定义接口,再编写实现类/实现结构体。- 对于 Go 等无原生类与接口概念的语言,可通过“接口类型 + 结构体”的方式实现类似的分层。
- 代码文件和目录结构应按业务功能进行划分,而非简单按技术层次划分,确保模块边界清晰、易于维护和重构。
- 必须建立
common目录,用于存放全局异常处理、中间件、工具函数等公共代码。
1.1.1. 模块常量(路由前缀)
Section titled “1.1.1. 模块常量(路由前缀)”为提升路由管理的一致性并避免硬编码,建议为各路由模块定义路由前缀常量。
- 定义位置:在模块目录下的常量文件(如
constants)中定义。 - 层级组合:子模块路由应拼接在父模块前缀之后,形成层级化结构。
- 作用域限制:该常量作用域限定于模块内部,不应向外暴露。
- 版本集成:路由前缀应与 API 版本号结合,构成如
/v1/system/dict的完整路径。
1.2. 对象定义
Section titled “1.2. 对象定义”根据不同场景,后端项目的数据对象可分为以下几类:
- 数据模型 (Data Model):与数据库表结构一一对应的代码对象,亦称实体(Entity),本规范中统一称为
Model。Model用于数据持久化操作。例如,用户表对应的模型可命名为User。原则上,业务模型应继承基础模型BaseModel(详见后续章节)。- 若某些表(如配置表、关联表)无需公共字段,则可不继承
BaseModel。
- DTO (Data Transfer Object):用于层间(如客户端与服务端)数据传输的对象,通常作为 API 的输入参数。DTO 可命名为
CreateUserDTO。为复用公共字段,可定义BaseDTO供其他 DTO 继承,其结构可参考BaseModel。在Go等无类语言中,可以叫Request。 - VO (View Object):服务端返回给客户端的视图层对象。若其结构与 DTO 相同,可复用。命名方式可为
FindListVO。在 Go 等无类语言中,可以叫Response。
对于 Go 等无类概念的语言,上述对象均使用结构体(Struct)定义。对于继承,Go 可通过结构体嵌套实现;其他语言若不支持嵌套,可采用组合模式。
2. 编码规范
Section titled “2. 编码规范”2.1. 通用准则
Section titled “2.1. 通用准则”- 代码自解释:力求代码意图清晰,减少不必要的注释。推荐使用长的变量名和函数名,以提升可读性。
- 时间处理:所有时间数据在存储、处理和传输时,必须统一使用
UTC时间,以规避时区问题。 - 参数命名:回调函数或方法签名中的参数名应具象化,避免使用无意义的单字母变量(如
i,v,e)。
2.2. 注释规范
Section titled “2.2. 注释规范”仅在必要时添加注释。注释应聚焦于解释“为何如此”(Why),而非“这是什么”(What),重点阐述代码背后的设计思想、业务背景或特殊考量。注释必须使用中文。
3. API 设计规范
Section titled “3. API 设计规范”3.1. HTTP 方法与版本管理
Section titled “3.1. HTTP 方法与版本管理”- HTTP 方法:为简化客户端和网关实现,API 仅使用
GET和POST。不使用PUT,DELETE,PATCH等方法。GET:用于请求数据,参数通过 URL 查询字符串传递。POST:用于提交数据(创建/更新)或执行需要复杂参数的查询。
- API 版本管理:所有 API 必须包含版本标识。推荐采用 URL 路径进行版本控制(例如
/v1/system/user/findAll)。当发生不兼容变更时,应发布新版本(如v2),并为旧版本保留一段过渡期。
3.2. 基础接口命名
Section titled “3.2. 基础接口命名”为保持项目结构的一致性与可预测性,所有资源的 CRUD 操作应遵循以下标准命名。控制器(Controller)与服务(Service)层的方法名必须保持一致。
findOne:获取单个资源。findList:获取资源列表(分页)。findAll:获取所有资源列表(不分页,可选)。findTree:获取树形结构资源列表(分页),入参规范与findList相同。findList和findAll仅返回扁平结构。findTreeAll:获取所有资源的树形结构列表(不分页),入参规范与findAll相同。create:创建单个资源。createMany:批量创建资源(可选)。update:更新单个资源。delete:永久删除单个资源。deleteMany:批量永久删除资源(可选)。remove:逻辑删除(软删除)单个资源。removeMany:批量逻辑删除资源(可选)。restore:恢复被逻辑删除的资源。restoreMany:批量恢复被逻辑删除的资源(可选)。count:获取资源总数(可选)。findWithDeleted:返回全部数据(包含未删除与已软删除的数据,可选,分页)。findWithDeletedAll:返回全部数据(包含未删除与已软删除的数据,可选,不分页)。findOnlyDeleted:返回仅已被软删除的数据(分页)。findOnlyDeletedAll:仅返回已被软删除的数据(可选,不分页)。
3.3. 查询接口规范
Section titled “3.3. 查询接口规范”3.3.1. 复杂查询
Section titled “3.3.1. 复杂查询”findList、findAll、findTree、findTreeAll接口应使用POST方法,允许客户端通过请求体(Request Body)以 JSON 格式传递复杂的过滤和排序条件。- 对于已实现的列表查询接口(包括
findList、findAll以及基于列表语义的变体),它的 Request Body 中应当统一支持一个基础筛选字段ids,类型为数组;类似3.3.4中所述的“批量 ID 入参对象”,但和它不同的是,这里的ids字段是可选的,允许为空值,且仅用于筛选。若传递该字段,则表示仅查询这些 ID 对应的资源;若不传递,则表示不基于 ID 进行筛选。
3.3.2. 查询软删除数据 (With / Only Deleted)
Section titled “3.3.2. 查询软删除数据 (With / Only Deleted)”- 标准查询接口仅返回未被软删除的数据(
deleted_time IS NULL),为了方便起见,应当提供额外的接口支持软删除数据。 - 这些接口的入参、排序和分页规则与标准查询接口保持一致。可以复用标准查询的 DTO(Request)和 VO(Response)。
3.3.3. 分页与排序参数
Section titled “3.3.3. 分页与排序参数”- 分页参数通过 URL 查询参数传递:
currentPage:当前页码,默认值为1。pageSize:每页记录数,默认值为20。
- 对于
findList等接口,筛选条件应通过请求体(Request Body)以 JSON 格式传递。
由于默认按创建时间倒序排序,通常无需传递排序参数。若需自定义排序,可在 URL Query 中传递 sortBy 和 sortOrder 参数。
sortBy:指定排序字段。sortOrder:指定排序顺序,0为降序,1为升序,默认值为0。sortOrder可单独传递,此时表示对默认的created_time字段进行排序。
pageSize 上限为 150,超出则报错。
分页实现可直接使用 ORM(如 Mybatis-Plus、Gorm)的内置功能;若无,则需自行封装统一的分页逻辑。
3.3.3.1. 分页方式
Section titled “3.3.3.1. 分页方式”3.3.3.1.1. 折衷 OFFSET 与性能的方案
Section titled “3.3.3.1.1. 折衷 OFFSET 与性能的方案”项目对于常规需求还是采用 OFFSET 分页方式,但是针对 OFFSET 带来的性能问题,做出下面的调整:
- 默认限制客户端允许查询的最大页面数为
350,超过则报错,提示使用更精确的过滤条件缩小结果集。 - 当表数据量较大(如超过 100 万行)且无法避免深度分页时,禁止直接对主表进行
OFFSET查询。必须采用“延迟关联”策略:先通过覆盖索引查询出目标页的主键 ID 列表,再通过 ID 列表回表查询完整数据。
3.3.3.1.2. 游标分页
Section titled “3.3.3.1.2. 游标分页”如果上述方法均不适用,可针对特定接口实现游标分页,以提升深度分页性能。在涉及到具体业务时另行说明。 实行游标分页接口的入参和响应结构需单独定义,不得与标准分页接口混用。
3.3.4. 批量 ID 入参对象
Section titled “3.3.4. 批量 ID 入参对象”对于仅需一组资源 ID 的批量操作,建议统一使用“批量 ID 入参对象”,以提升接口的一致性与可读性。建议定义为公共 DTO,命名为 ByIdsDTO(ByIdsRequest)。
- 结构约定(推荐)
{"ids": ["a348f08c-8c9e-4bac-95bd-146750cfb064","6f7d8b9c-1234-4abc-9def-0123456789ab"]}
- 适用场景:
deleteMany、removeMany、restoreMany、updateMany等仅凭 ID 即可完成的批量接口。 - 校验与约束
ids必须为非空数组,元素类型与主键类型一致(默认UUID,推荐uuidv7)。- 元素应去重,且禁止
null或空字符串。可设置最大长度(如 100-500),视业务与性能而定。 - ID 格式必须合法。对于非 UUID 主键,应采用相应的校验规则。
- 行为约定
- 若任一 ID 不存在,默认应整体失败并返回明确的业务错误。也可按需提供“跳过不存在”模式,但需在接口文档中声明。
- 操作应与权限控制联动,仅当调用者具备相应权限时方可执行。
- 日志审计应记录请求 ID、操作人、影响的资源数量及样本 ID。
3.4. 响应结构
Section titled “3.4. 响应结构”3.4.1. 统一响应体
Section titled “3.4.1. 统一响应体”所有 API 响应都应遵循统一的 JSON 结构,以便客户端进行标准化处理。
{ "businessCode": 0, // 业务状态码:0 表示成功,非 0 表示具体错误类型。注意:这不是 HTTP 状态码。 // HTTP 状态码由 Axios 等库自动处理,后端无需在响应体中返回。 "isSuccess": true, // 操作是否成功,错误时为 false。 "message": "操作成功!", // 结果消息,错误时返回错误信息。 "data": { "id": "a348f08c-8c9e-4bac-95bd-146750cfb064", "name": "示例数据" }, // 返回的数据,错误时为 null。 "timestamp": "2025-11-05T07:44:01.608Z", // ISO 8601 格式的时间戳。 "errors": null // 可选,详细错误信息列表,用于复杂业务错误场景。}- 响应头应包含
X-Request-ID,用于请求追踪。
3.4.2. 分页响应结构
Section titled “3.4.2. 分页响应结构”对于分页接口,data 字段应采用以下结构,并封装为统一的 VO。
{ "total": 100, // 总记录数 "currentPage": 1, // 当前页码 "pageSize": 10, // 每页记录数 "records": [] // 当前页的记录数组}3.4.3. businessCode 状态码
Section titled “3.4.3. businessCode 状态码”0:操作成功1xxx:系统级错误(如数据库连接失败、超时)2xxx:认证/鉴权失败3xxx:参数错误(如缺少必填项、格式错误)4xxx:业务逻辑错误(包括权限不足导致的接口访问失败,例如findWithDeleted/findOnlyDeleted等)5xxx:数据库相关错误9xxx:未知错误
3.5. 文件处理
Section titled “3.5. 文件处理”- 文件下载应支持流式传输和断点续传。
4. 数据持久化规范
Section titled “4. 数据持久化规范”4.1. 数据库(PostgreSQL)
Section titled “4.1. 数据库(PostgreSQL)”4.1.1. 表与字段
Section titled “4.1.1. 表与字段”- 命名规范:表名和列名必须采用下划线命名法(
snake_case),例如user_profiles。 - 数据类型:
- 时间类型:统一使用带时区的
timestamptz类型,且时区必须为UTC。 - 精确数值:对于金额、汇率等需高精度计算的字段,必须使用
decimal类型。 - 大文本:对于长度不定的文本,应使用
text类型。
- 时间类型:统一使用带时区的
- 公共字段:所有业务相关的数据表都应包含一组标准公共字段,以记录元数据。
id(主键):UUID类型。created_time(创建时间):timestamptz类型,该字段默认和主键id建复合索引。updated_time(更新时间):timestamptz类型。deleted_time(软删除时间):可选,timestamptz类型,NULL表示未删除。created_by(创建者):可选,UUID类型,记录创建者 ID。NULL表示由系统创建。updated_by(更新者):可选,UUID类型,记录更新者 ID。NULL表示由系统更新。deleted_by(删除者):可选,UUID类型,记录删除者 ID。NULL表示由系统删除。status(状态):varchar(32)类型,0代表无效,1代表有效。remark(备注):text类型。is_system_reserved(系统保留):boolean类型,标识是否为系统保留数据,默认为false,如果是,则不可删除。
- 主键:主键 ID 应使用
uuidv7。若数据库支持,可使用uuid_generate_v7()函数生成;否则,在应用层生成。 - 模型定义:
BaseModel应包含上述公共字段,其他数据模型继承(组合)。模型中必须显式定义这些字段,不依赖 ORM 的隐式功能。此外,BaseModel一般不序列化为 JSON 返回给客户端,一般通过 VO(Response)来实现。 - 状态说明:
status字段表示资源自身的业务状态,与逻辑删除状态无关。逻辑删除通过deleted_time字段实现,两者职责分离。
4.1.2. 关系 (Relation)
Section titled “4.1.2. 关系 (Relation)”应根据业务需求选择合适的关系类型(一对一、一对多、多对多),避免滥用多对多关系。
4.1.2.1 外键策略
Section titled “4.1.2.1 外键策略”为避免跨库/跨服务场景下的约束复杂度与潜在性能问题,以及方便做分表分库和数据迁移,本规范中默认不使用数据库物理外键约束。数据一致性由应用层逻辑保障。
即便不使用物理外键,逻辑外键关系仍然必须严格维护,应用层需要承担全部责任:
- 所有涉及多表写入的关联操作(如主表 + 关联表插入/更新)必须放在同一数据库事务中执行,确保原子性。
- 删除或更新关联数据前,必须检查是否存在依赖关系(如子表记录、引用记录),防止产生“孤儿数据”或破坏业务约束。必要时应禁止删除或采用级联软删除策略。
- 对于重要的核心数据(如用户、权限、账务等),建议编写定期运行的数据完整性检查脚本,例如:
- 检查是否存在指向不存在主键的逻辑外键;
- 检查应唯一的业务键是否存在重复;
- 检查主从表在软删除状态上的不一致。
- 发现问题后应输出报表并提供自动/半自动修复脚本。
- 在设计表结构和接口 DTO 时,应显式标注出“逻辑外键字段”(如
user_id,role_id),并在代码层面集中封装校验逻辑,禁止在业务代码中到处散落手写校验。 - 对于跨服务的引用关系(如 A 服务保存 B 服务的资源 ID),应在调用链中引入存在性校验或通过统一的“资源目录服务”进行间接引用,避免长时间存在失效引用。
4.1.3. 树形结构
Section titled “4.1.3. 树形结构”- 实现方式:为保证性能,应尽可能通过 SQL 实现树结构的查询与操作,避免在应用层递归处理。若所用 ORM 提供成熟的树结构支持(包括节点筛选),亦可使用。
- 子节点键名:统一使用
children。可建立一个基础树形 VO 供其他树形结构继承。 - 基础树 VO:
{"children": [],"parentId": "父节点 ID" // 可选,根节点为 null}
- 邻接表:对于结构简单的树(如系统菜单),可使用邻接表实现。
- 闭包表:在以下场景中,应使用闭包表(Closure Table)实现树形结构:
- 树的层级较深,且需频繁查询某节点的所有子孙。
- 需频繁移动节点(改变父节点)。
- 需高效查询节点的祖先路径。
4.1.4. 查询与操作
Section titled “4.1.4. 查询与操作”- 参数化查询:所有数据库查询都必须使用参数化查询(Prepared Statements),以从根本上杜绝 SQL 注入。
- 避免
SELECT *:查询时必须明确指定所需字段,以减少不必要的数据传输和数据库开销。 - 避免 N+1 查询:严禁在循环中执行数据库查询。应通过
IN查询或JOIN将多次查询合并为单次操作。 - 复杂查询:对于涉及多表连接(JOIN)、聚合、子查询等复杂场景,推荐使用原生 SQL 或查询构建器,以保证代码的可读性与性能。
4.1.5. 事务 (Transaction)
Section titled “4.1.5. 事务 (Transaction)”- 原子性:任何涉及多个写操作的业务逻辑都必须封装在事务中,以保证操作的原子性。
- 缩短事务时长:避免在事务中执行外部 API 调用、文件 I/O 等耗时操作,以减少锁竞争和数据库压力。
- 隔离级别:默认为
READ COMMITTED。对一致性要求更高的读写流程,可提升至REPEATABLE READ(需评估死锁与性能影响)。
4.1.6. 索引
Section titled “4.1.6. 索引”- 应当根据查询需求合理创建索引,以提升查询性能。
4.1.7. 数据库结构变更 (Migration)
Section titled “4.1.7. 数据库结构变更 (Migration)”- 禁用自动同步:生产环境中,必须禁用任何自动同步数据库结构的功能。
- 迁移脚本:所有数据库结构变更(DDL)都必须通过迁移脚本管理。每个脚本应保证原子性与可回滚性。
- 工具:Go、Java 等项目统一使用
Flyway进行数据库迁移。对于 TypeORM 项目,简单场景可用其自带迁移工具,复杂场景仍推荐Flyway。 - 命名:Flyway 脚本命名格式为
{时间}__V{版本号}__{描述}.sql,例如2025_11_06_15_34__V1__create_user_table.sql。
4.2. 缓存 (Redis)
Section titled “4.2. 缓存 (Redis)”对于使用 Redis 的项目,应遵循以下规范:
- 实例隔离:每个项目应使用独立的 Redis 实例和数据库(DB),避免数据混淆。不同环境应连接到不同的 Redis 服务器,防止开发环境误操作生产数据。
- 连接管理:应使用连接池管理 Redis 连接,避免频繁创建和销毁。
- 键命名:键名应以项目名称为前缀,例如
myproject:user:captcha。 - 数据序列化:存储 JSON 等复杂数据时,应先序列化为字符串再存入,读取时反序列化,以保证兼容性。
- 过期时间:缓存数据必须设置合理的过期时间(TTL),并引入随机抖动,防止大量缓存同时失效导致缓存雪崩。
- 数据结构:根据业务场景选择合适的数据结构(String, Hash, List, Set, ZSet),避免滥用。
- 批量操作:对于大量数据读写,应使用管道(Pipeline)或
MGET等批量命令。 - 优雅降级:当 Redis 不可用时,应能优雅降级而不影响核心业务。可回退至数据库查询,或使用本地缓存作为二级缓存。
- 重试机制:应为网络抖动等临时性故障实现合理的重试机制(建议最多 3 次,采用指数退避策略)。
- 安全:
- 生产环境必须为 Redis 设置强密码,并通过环境变量配置。
- Redis 应部署在内网,不应直接暴露到公网。
- 性能与资源:
- 生产环境中严禁使用
KEYS命令,应改用SCAN进行增量迭代。 - 应设置
maxmemory限制,防止内存溢出。 - 避免存储过大的键值对(如 >100KB)。大对象应拆分或存入对象存储,Redis 中仅保存索引。
- 对于可预见的热点数据(如秒杀商品),应在架构上优化,采用多级缓存或本地缓存,避免请求集中于单个 Key。
- 生产环境中严禁使用
5. 安全与鉴权
Section titled “5. 安全与鉴权”5.1. 鉴权与权限
Section titled “5.1. 鉴权与权限”- 基础鉴权:使用
JWT。若项目需要,可引入OAuth2.0。 - 权限模型:后端项目应采用基于角色的访问控制(RBAC)模型。接口与权限点之间通过元数据(如“接口表 + 权限点表 + 关联表”)建立多对多关系:一个接口可以绑定多个权限点,一个权限点也可以绑定多个接口,由具体业务决定粒度和组合方式。
- 权限点命名:权限点标识符应形象、语义化,推荐采用“功能域 + 操作”的组合形式(例如
user:list、user:create、dashboard:system:dept:list)。不再强制要求与具体 Controller / Service 方法名一一对应,但整体风格应保持一致,避免难以理解的随意缩写。 - 查询类接口权限:
- 查看已删除数据的能力应被视为一项特殊权限,但通过独立接口实现(不再使用
mode参数切换行为)。 - 如提供
findWithDeleted/findWithDeletedAll/findOnlyDeleted/findOnlyDeletedAll等接口,应分别绑定独立权限点,命名规则与其他接口保持一致。 findAll/findWithDeletedAll/findOnlyDeletedAll等“不分页全量”接口,因可能返回大量数据,必须绑定独立的权限点,防止被滥用导致性能问题。
- 查看已删除数据的能力应被视为一项特殊权限,但通过独立接口实现(不再使用
- 软删除权限:
remove/removeMany(软删除)和restore/restoreMany(恢复)操作也必须有独立的权限点。
- 后端项目应当有以下两种 Token:
Access Token:用于访问受保护的资源,生命周期较短。Refresh Token:用于刷新访问令牌,生命周期较长。
- 设置
system_api_perm表来存储系统内置的 API 权限点,不在接口上硬编码权限点。
5.2. 安全规范
Section titled “5.2. 安全规范”- 密码存储:密码必须加盐,使用
argon2。 - 输入校验:虽然客户端会进行输入校验,但后端必须对重要输入进行再校验。
- 敏感数据:所有敏感数据(如密码、API 密钥等)在存储和传输过程中必须进行加密处理。严禁在日志或 API 响应中直接输出密码、密钥、身份证号等敏感信息。
- 速率限制:对于登录、注册等敏感接口,应实现速率限制,防止暴力破解攻击。
6. 横切关注点
Section titled “6. 横切关注点”6.1. 配置管理
Section titled “6.1. 配置管理”- 环境变量优先:所有配置项(如数据库凭证、API 密钥、服务端口等)应优先通过环境变量读取。
- 配置文件:可以提供一个默认的配置文件,但不应将包含敏感信息的配置文件提交到版本控制系统中。
- 环境分离:应为开发、测试、生产等不同环境提供独立的配置。
6.2. 日志规范
Section titled “6.2. 日志规范”- 格式:采用结构化 JSON 日志,必须包含
level,timestamp(UTC),logger,message,requestId,userId(若有)。 - 级别:生产环境记录
INFO及以上级别,本地开发可使用DEBUG。 - 安全:禁止在日志中输出密码、密钥、身份证号等敏感信息。
- 管理:应实现日志采样与滚动策略,并使用 ELK、Cloud Logging 等工具进行集中收集。
6.3. 异常处理
Section titled “6.3. 异常处理”- 封装数据库错误:严禁将原始的数据库错误信息直接暴露给客户端。
- 统一处理:应通过全局异常处理机制捕获数据库异常,并将其转换为标准化的错误响应。
- 记录日志:捕获异常时,必须记录详细的错误日志,以便于问题排查。
7. 开发与部署
Section titled “7. 开发与部署”7.1. 文档
Section titled “7.1. 文档”- API 文档:必须使用 OpenAPI (Swagger) 生成 API 文档。
- 文档内容:每个接口都应包含清晰的描述、参数说明和响应示例。
- 关键信息:必须标注接口的鉴权要求与所需权限点。为分页、树形结构、复杂过滤等提供示例。
7.2. 容器化
Section titled “7.2. 容器化”若项目采用容器化部署,应遵循以下规范:
- 镜像优化:使用多阶段构建(Multi-stage builds)来优化镜像大小。
- 配置:配置应通过环境变量传递,避免在镜像中硬编码。敏感配置应从密钥管理服务注入。
- 安全:容器应以非 root 用户运行;文件系统应尽量设为只读,仅在必要时挂载可写目录。
- 运行时:
- 优雅终止:应用必须能正确处理
SIGTERM信号以实现优雅停机。 - 时区同步:容器内部必须使用 UTC 时区。
- 优雅终止:应用必须能正确处理
8. 通用业务规范
Section titled “8. 通用业务规范”8.1 字典
Section titled “8.1 字典”- 字典的值允许传入字符串和数字,所以在数据库中使用
varchar(32)类型存储字典值。