后端开发规范
-
接口文档管理
新开发的后端接口必须在 Apifox 中进行详细记录。具体要求如下:- 每个接口需明确描述其功能、请求方式(如
GET
、POST
等)、请求参数及参数类型。 - 在接口的“成功示例”中,提供完整的返回结果集,并对每个字段进行清晰注释,说明字段含义、数据类型及可能的取值范围。
示例:{ "code": 200, // 状态码,200 表示成功 "message": "操作成功", // 提示信息 "data": { // 返回数据 "userId": 123, // 用户ID,整数类型 "username": "testUser", // 用户名,字符串类型 "createdAt": "2025-04-08" // 创建时间,格式为 YYYY-MM-DD } }
- 确保接口文档与实际代码保持一致,避免因文档不准确导致前后端联调问题。
- 每个接口需明确描述其功能、请求方式(如
-
前后端职责划分
- 前端职责:前端仅负责数据展示和用户交互,所有涉及业务逻辑的计算均交由后端处理。
- 前端应避免直接对数据进行复杂计算或逻辑判断,例如金额计算、日期格式化等,这些操作统一由后端完成并返回结果。
- 后端职责:后端需承担所有业务逻辑处理及数据校验工作,包括但不限于:
- 表单提交的数据校验(如字段是否为空、数据格式是否正确、值范围是否合法等)。
- 数据计算、逻辑判断及异常处理。
- 返回标准化的错误信息,便于前端进行错误提示。
- 前端职责:前端仅负责数据展示和用户交互,所有涉及业务逻辑的计算均交由后端处理。
-
表单校验
- 原则上,表单提交的校验工作由后端全权负责,前端无需重复校验。
- 若前端需要实现简单的校验(如必填项检查)(正常情况下无需做任何校验),仅为提升用户体验,不能替代后端校验。
- 后端在校验失败时,需返回清晰的错误信息,告知具体的校验失败原因,例如:
{ "code": 400, "message": "参数错误", "errors": [ { "field": "username", "message": "用户名不能为空" }, { "field": "email", "message": "邮箱格式不正确" } ] }
-
接口文档目录结构规范
-
接口文档的目录结构需按照实际应用(App)的功能模块或入口目录进行组织,确保文档结构清晰、易于查找。
-
具体要求如下:
- 按照项目的功能模块划分目录,例如:
用户管理
、订单管理
、支付管理
等。 - 如果项目有明确的入口层级(如
/api/v1
、/api/v2
),则在 Apifox 中创建对应的目录层级,与实际接口路径保持一致。
示例目录结构:├── 用户管理 │ ├── 登录接口 (POST /api/v1/user/login) │ ├── 注册接口 (POST /api/v1/user/register) │ └── 用户信息查询 (GET /api/v1/user/info) ├── 订单管理 │ ├── 创建订单 (POST /api/v1/order/create) │ ├── 查询订单详情 (GET /api/v1/order/detail) │ └── 取消订单 (POST /api/v1/order/cancel) ├── 支付管理 │ ├── 发起支付 (POST /api/v1/pay/initiate) │ └── 支付结果查询 (GET /api/v1/pay/status)
- 如果某个模块下有子模块,则进一步细分目录。例如,在
订单管理
下可以增加退款管理
子目录:├── 订单管理 │ ├── 退款管理 │ │ ├── 申请退款 (POST /api/v1/order/refund/apply) │ │ └── 查询退款状态 (GET /api/v1/order/refund/status)
- 按照项目的功能模块划分目录,例如:
-
命名规范:
- 目录名称和接口名称应简洁明了,避免使用模糊词汇。
- 使用小写字母和中划线(
-
)分隔单词,例如:用户管理
->user-management
。 - 接口路径需与后端实际路由保持一致,便于前后端对接。
-
维护要求:
- 新增接口时,需严格按照目录结构添加到对应模块下,避免随意放置导致文档混乱。
- 定期检查接口文档目录,清理废弃接口或调整目录结构以适应项目需求变化。
-
接口返回字段优化规范
- 按需返回字段:
- 在列表接口中,应根据实际需求明确指定返回的字段,避免使用
SELECT *
查询所有字段。 - 通过 MyBatis-Plus 的
queryWrapperX.select(...)
方法,显式列出需要返回的字段,减少不必要的数据传输,提升查询效率。 - 示例代码如下:
return selectPage(reqVO, queryWrapperX.select( StarUserDO::getId, StarUserDO::getUname, StarUserDO::getSex, StarUserDO::getJobCode, StarUserDO::getStatus, StarUserDO::getEmergencyContactName, StarUserDO::getJobTitle, StarUserDO::getFace, StarUserDO::getBirthday ));
- 注意事项:
- 确保只返回前端或业务逻辑所需的字段。
- 如果新增字段需求,需同步更新
select(...)
中的字段列表。
- 在列表接口中,应根据实际需求明确指定返回的字段,避免使用
逻辑代码优化规范
- 避免硬编码数字,使用静态常量或枚举:
- 在业务逻辑中,禁止直接使用魔法数字(如
0
、1
、2
等)表示状态值或其他含义。应使用静态常量或枚举类来代替,增强代码的可读性和可维护性。 - 示例代码如下:
public enum AuditStatusEnum { UNSENT(0, "草稿(未提交)"), WAIT(1, "待审核(已提交)"), SUCCESS(2, "已通过"); private final int code; private final String description; AuditStatusEnum(int code, String description) { this.code = code; this.description = description; } public int getCode() { return code; } public String getDescription() { return description; } }
- 使用示例:
if (status == AuditStatusEnum.WAIT.getCode()) { // 执行待审核逻辑 }
- 优点:
- 提高代码可读性:通过枚举的名称(如
WAIT
)直观表达含义。 - 减少错误:避免因硬编码导致的状态值不一致问题。
- 易于扩展:新增状态时只需在枚举类中添加新值,无需修改多处代码。
- 提高代码可读性:通过枚举的名称(如
- 在业务逻辑中,禁止直接使用魔法数字(如
2025年4月2日新增后台开发规范
1. 字段非空设置
为了确保与历史数据的兼容性并减少因字段为空导致的错误,数据库字段应尽量避免设置为 NOT NULL
。若确实需要将某个字段设置为非空,请务必充分评估其必要性和影响范围,并在设计文档中详细说明理由及处理方案。
2. 字符长度校验
当数据库字段设置了字符长度限制时,前端应在提交表单数据前对输入参数进行字符长度校验,以确保传入的数据不超过字段的最大长度限制。这有助于避免因数据过长而引发的异常或报错,保障系统的稳定性和数据完整性。
3. 数据格式与编码
在前后端交互过程中,建议对所有输入数据进行必要的格式校验和编码处理,以防止乱码或格式不匹配问题的发生。
4. 错误处理与提示
接口应当对异常输入提供明确且详细的错误信息,帮助前端快速定位问题并作出相应调整。具体要求如下:
- 清晰的错误消息:错误信息应包含具体的错误原因、发生位置及相关上下文,以便于调试和修复。
- 用户友好的提示:对于终端用户可见的错误信息,应尽量使用通俗易懂的语言描述问题,并给出可能的解决方案或操作指引。
示例注释模板
以下是一个简单的示例,展示如何在代码中实现上述规范:
/**
* 短信验证码发送请求参数
*
* 用于接收短信验证码发送请求的入参。
* 包含手机号、发送场景及客户端 IP 地址等必要信息。
*/
@Data
@Schema(description = "短信验证码发送请求参数")
public class SmsCodeSendReqDTO {
/**
* 手机号,必须为合法中国大陆手机号格式。
* 示例:15601691300
*/
@Schema(description = "手机号", example = "15601691300", required = true)
@NotEmpty(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String mobile;
/**
* 发送场景,参考 SmsSceneEnum 枚举值。
* 示例:1(登录),2(注册),3(找回密码)
*/
@Schema(description = "发送场景", example = "1", required = true)
@NotNull(message = "发送场景不能为空")
private Integer scene;
/**
* 客户端 IP 地址,用于风控和日志记录。
* 示例:10.20.30.40
*/
@Schema(description = "发送来源 IP 地址", example = "10.20.30.40", required = true)
@NotEmpty(message = "发送 IP 地址不能为空")
private String createIp;
}
Controller 层使用示例
/**
* 发送短信验证码接口
*
* @param request 请求参数,包含手机号、发送场景和客户端 IP
*/
@PostMapping("/send-code")
public String sendCode(@Valid @RequestBody SmsCodeSendReqDTO request) {
// 正常进入业务逻辑处理
return "验证码发送成功,手机号:" + request.getMobile() +
",场景:" + request.getScene() +
",IP:" + request.getCreateIp();
}
2025年5月4日新增后台开发规范
1. 表命名规则
前缀:所有表名都应以 yd_ 开始。
实体描述:紧跟前缀之后是描述表中存储的数据类型的名称,采用单数形式。例如,用户表应命名为 yd_user 而不是 yd_users。
下划线分隔:当需要组合多个单词来描述表时,使用下划线 _ 分隔单词。例如,订单详情表可以命名为 yd_order_detail。
大小写敏感性:除非特定数据库系统要求区分大小写,否则一律使用小写字母。
示例:
用户信息表:yd_user
订单表:yd_order
商品分类表:yd_product_category
为了方便团队成员理解与协作,我们统一使用 有道词典 提供的标准英文翻译作为命名参考。例如,“艺人”对应的英文为 “Artist”。
因此,艺人相关表建议命名为:yd_artist_user
。其中:
yd_
表示所有新表的统一前缀;artist
是“艺人”的英文翻译,用于表明业务实体;user
表示该表与用户信息相关联(非强制,根据实际业务灵活添加)。命名应尽量做到见名知意,保持一致性与可读性。
当然可以,以下是对你提供的“定时任务开发规范”内容的优化版本,使其表达更清晰、专业,便于团队理解与执行:
2. 定时任务开发规范
-
业务逻辑不得写在 Job 类中
所有业务逻辑(包括简单的处理逻辑)都应封装在对应业务表的Service
或Helper
类中,禁止将任何业务逻辑直接写在 Job 页面中。
编写公共方法时,需考虑其可能被实时业务调用,以确保数据一致性。底层通用逻辑建议放置在Helper
类中,供 Job 和其他业务模块共同调用。 -
Job 类中的方法长度控制
Job 类中每个方法原则上不超过 20 行代码,建议控制在 10 行以内。目的是避免业务逻辑堆积在定时任务类中,提高可读性和可维护性。 -
注释规范
每个定时任务必须添加清晰的注释,说明该任务的作用,帮助他人快速理解任务目的。 -
参考示例
- 定时任务编写参考:可在 IDEA 中搜索
UpdateUserLevelJob
查看标准实现; - 公共静态方法参考:可在 IDEA 中搜索
determineUserGrade
查看通用逻辑抽取方式。
- 定时任务编写参考:可在 IDEA 中搜索
新增前后端开发规范(2025年5月12日生效)
订单相关表的支付状态统一管理
为了保证系统中所有与订单相关的表在处理支付状态时的一致性和可维护性,特此规定:
-
后端:必须使用统一的
OrderPayStatusEnum
枚举来表示支付状态。这包括但不限于数据库操作、业务逻辑处理以及API接口的设计。示例代码注释要求:
/** * 查询订单信息 * @param payStatus 使用 OrderPayStatusEnum 枚举作为参数, * 确保支付状态的有效性和一致性 */ public List<Order> getOrderByPayStatus(OrderPayStatusEnum payStatus) { // 方法实现... }
-
前端:在展示支付状态时,需使用统一的支付字典
ORDER_RECHARGE_ORDER_TYPE='order_recharge_order_type'
进行翻译和显示,以确保用户界面的一致性和准确性。
规范实施细节
-
后端入参查询:当涉及到支付状态的查询条件时,务必使用相应的枚举类型作为方法参数,并提供清晰的注释说明其用途。
-
前端数据展示:在需要向用户展示支付状态的地方,应通过调用统一的支付字典服务获取对应的描述信息,并正确地渲染到页面上。
示例代码
后端代码示例
/**
* 订单支付状态枚举
*/
@Getter
@AllArgsConstructor
public enum OrderPayStatusEnum {
PAYING(1, "支付中"),
PAID(2, "已支付"),
FAILED(-1, "已失败"),
REFUNDED(10, "已退款");
private final Integer code;
private final String description;
/**
* 根据 code 获取对应的枚举
*/
public static OrderPayStatusEnum fromCode(Integer code) {
for (OrderPayStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
throw new IllegalArgumentException("Invalid pay status code: " + code);
}
}
/**
* 根据订单ID、订单类型、统计状态和支付状态,检查指定订单是否已更新统计数据。
*
* <p>该方法用于查询指定订单在对应的业务表中是否存在未更新统计数据的记录。</p>
*
* @param orderId 订单ID,用于匹配数据库中的记录
* @param orderTypeEnum 订单类型枚举,用于动态选择数据表(例如 DSJ_ORDER_XXX)
* @param orderUpdateStatistic 统计更新状态枚举(如 UPDATED, NOT_UPDATED)
* @param orderPayStatusEnum 支付状态枚举(如 PAID, UNPAID)
* @return 符合条件的记录数量(0 或 1)
*/
@Select({
"<script>",
"SELECT COUNT(*) FROM ${orderTypeEnum.tableName}",
"WHERE id = #{orderId}",
" AND is_update_statistic = #{orderUpdateStatistic.code}",
" AND status = #{orderPayStatusEnum.code}",
" AND deleted = 0",
"</script>"
})
Integer selectByNoStatisticId(
@Param("orderId") Long orderId,
@Param("orderTypeEnum") NotifyRelatedOrderType orderTypeEnum,
@Param("orderUpdateStatistic") OrderUpdateStatistic orderUpdateStatistic,
@Param("orderPayStatusEnum") OrderPayStatusEnum orderPayStatusEnum);
前端代码示例
//下拉案列
<el-select v-model="queryParams.orderType" placeholder="请选择订单类型" clearable class="!w-240px">
<el-option v-for="dict in getIntDictOptions(DICT_TYPE.ORDER_RECHARGE_ORDER_TYPE)" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
//展示案列
<el-table-column label="订单信息" align="left" prop="vipTitle" width="205px">
<template #default="scope">
<div class="flex items-start flex-col">
<p>类型:<dict-tag :type="DICT_TYPE.ORDER_RECHARGE_ORDER_TYPE" :value="scope.row.orderType" /></p>
</div>
</template>
</el-table-column>
请各团队严格按照上述规范执行,确保系统的稳定性和用户体验的一致性。
新增后端开发规范(2025年6月12日生效)
🎯 目标
为提升系统性能与代码可维护性,避免在循环中频繁调用数据库查询或远程接口,特制定如下开发规范。
✅ 规范说明
在进行分页数据处理时,如需对每条记录补充额外信息(如关联商品、订单等),应遵循以下原则:
完整示例:
public PageResult<PlayMovieRespVO> getPlayMoviePage(PlayMoviePageReqVO pageReqVO) {
IPage<PlayMovieRespVO> pageResult = MyBatisUtils.buildPage(pageReqVO);
playMovieMapper.findAll(pageResult, pageReqVO);
pageResult.getRecords().forEach(this::formatter);
List<PlayMovieRespVO> records = pageResult.getRecords();
// 收集所有 movieIds
List<Long> movieIds = records.stream()
.map(PlayMovieRespVO::getId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 一次性批量查询关联商品信息
Map<Long, List<MovieInfoVO>> spuInfoMap;
if (!movieIds.isEmpty()) {
List<MovieInfoVO> allSpuInfoList = playMovieMapper.batchGetSpuInfoListByMovieIds(movieIds);
// 构建 movieId -> spuInfo 列表的映射
spuInfoMap = allSpuInfoList.stream()
.collect(Collectors.groupingBy(MovieInfoVO::getMovieId));
} else {
spuInfoMap = new HashMap<>();
}
pageResult.getRecords().forEach(playMovieRespVO -> {
List<AppOrderPlayMovieRespDTO> orderPlayMovieResps = orderPlayMovieApi.getOrderPlayMovieListByMovieId(playMovieRespVO.getId().intValue()).getCheckedData();
Long movieId = playMovieRespVO.getId();
// 设置 spuInfoList
playMovieRespVO.setSpuInfoList(spuInfoMap.getOrDefault(movieId, Collections.emptyList()));
playMovieRespVO.setPayCount(orderPlayMovieResps.size());
playMovieRespVO.setPayAmount(orderPlayMovieResps.stream()
.map(AppOrderPlayMovieRespDTO::getPayMoney) // 获取每个对象的 playMoney
.filter(Objects::nonNull) // 过滤掉可能的 null 值
.reduce(BigDecimal.ZERO, BigDecimal::add));
});
return new PageResult<>(pageResult.getRecords(), pageResult.getTotal());
}
- 建议先批量查询,再映射使用
对于需要根据当前数据字段(如 movieId)获取关联信息的场景,应优先将所有目标 ID 收集起来,通过一次批量查询获取全部数据,并将其构造成以主键为 Key 的 Map 结构,在后续遍历中直接通过 Key 获取对应信息。
// 收集所有 movieIds
List<Long> movieIds = records.stream()
.map(PlayMovieRespVO::getId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 一次性批量查询关联商品信息
Map<Long, List<MovieInfoVO>> spuInfoMap;
if (!movieIds.isEmpty()) {
List<MovieInfoVO> allSpuInfoList = playMovieMapper.batchGetSpuInfoListByMovieIds(movieIds);
// 构建 movieId -> spuInfo 列表的映射
spuInfoMap = allSpuInfoList.stream()
.collect(Collectors.groupingBy(MovieInfoVO::getMovieId));
} else {
spuInfoMap = new HashMap<>();
}
- 禁止在循环中单次调用数据库或远程服务
以下做法是严格禁止的:
pageResult.getRecords().forEach(playMovieRespVO -> {
// ❌ 每次循环都调用远程接口或数据库,严重影响性能
List<AppOrderPlayMovieRespDTO> orderPlayMovieResps =
orderPlayMovieApi.getOrderPlayMovieListByMovieId(playMovieRespVO.getId().intValue()).getCheckedData();
});
该写法会导致大量重复请求,增加数据库/接口压力,降低系统响应速度。
- 推荐做法:统一收集 ID,批量查询后再映射
应在循环之前统一收集所有需要查询的 ID,执行一次批量查询,之后在循环中从本地 Map 中取值,实现高效处理。
📌 总结建议
场景 | 推荐做法 | 禁止做法 |
---|---|---|
查询关联信息 | 统一收集 ID,批量查询,构造 Map 后映射使用 | 在循环中逐条调用数据库或接口 |
数据格式化 | 使用 Java Stream API 高效处理集合数据 | 多重嵌套逻辑,影响可读性 |
分页处理 | 使用分页工具类统一封装逻辑 | 手动拼接 SQL 或分页参数 |