后台业务系统通用开发规范
适用于 Spring Boot、Quarkus、Solon 等 Java 后台业务系统。
本文重点规范接口对象设计、数据访问方式、更新策略、命名约定和长期维护原则。
1. 背景
在后台业务系统中,很多数据并不是一次性完整写入并完整更新的。
更常见的业务过程是:
- 创建时只插入部分基础字段。
- 审核流程更新审核字段。
- 状态流转更新状态字段。
- 支付流程更新支付字段。
- 回调流程更新回调字段。
- 人工处理或补偿任务更新各自负责的字段。
也就是说,一条业务数据往往会被多个业务动作逐步补全。
因此,后台业务系统不适合默认采用“对象整体保存”的开发方式,而应优先采用“部分字段插入 + 指定字段更新”的方式。
本文用于统一后台业务系统中的接口对象、数据更新、命名和维护规范,降低长期维护成本,避免误更新、字段覆盖和对象复用混乱。
2. 核心原则
2.1 一个业务动作对应一个 Req
每个接口入参对象应当对应一个明确的业务动作。
例如:
CreateOrderReq
UpdateOrderNameReq
AuditOrderReq
UpdateOrderStatusReq
CallbackOrderReq不推荐使用一个大对象承载多个业务动作的所有字段。
错误示例:
OrderReq
OrderDTO
OrderVO如果一个对象中同时包含创建、编辑、审核、支付、回调等字段,后续维护者很难判断:
- 当前接口到底使用哪些字段。
- 哪些字段是前端允许传入的。
- 哪些字段是后端内部维护的。
- 空值表示“不修改”还是“清空”。
- 当前接口是否会误更新其他业务字段。
2.2 Req 只包含当前动作允许提交的字段
Req 不是 Entity 的子集复制品,而是某个业务动作的输入契约。
例如,修改名称接口只允许提交名称相关字段:
public class UpdateOrderNameReq {
private Long id;
private String name;
}审核接口只允许提交审核相关字段:
public class AuditOrderReq {
private Long id;
private Integer auditResult;
private String auditRemark;
}不应该为了复用而把所有字段都塞进一个大对象中。
2.3 Resp 只包含当前接口需要返回的字段
响应对象也应按接口场景拆分。
例如:
OrderListResp
OrderDetailResp
OrderAuditDetailResp列表接口和详情接口不要无脑复用同一个大对象。
列表接口通常只需要返回摘要字段:
public class OrderListResp {
private Long id;
private String orderNo;
private Integer status;
private BigDecimal amount;
private LocalDateTime createTime;
}详情接口可以返回更完整的信息:
public class OrderDetailResp {
private Long id;
private String orderNo;
private Integer status;
private BigDecimal amount;
private String auditRemark;
private LocalDateTime auditTime;
private LocalDateTime createTime;
}这样可以减少接口字段污染,也能降低前端和后端对无关字段的依赖。
2.4 创建时部分字段 insert,后续指定字段 update
后台业务系统中,创建数据时通常只插入基础字段。
示例:
insert into order_table (
merchant_id,
order_no,
amount,
status,
create_time
) values (
?, ?, ?, ?, now()
);后续不同业务动作只更新自己负责的字段。
修改名称:
update order_table
set name = ?
where id = ?;审核:
update order_table
set status = ?,
audit_remark = ?,
audit_time = now()
where id = ?;支付:
update order_table
set pay_status = ?,
pay_time = now(),
pay_no = ?
where id = ?;回调:
update order_table
set callback_status = ?,
callback_result = ?,
callback_time = now()
where id = ?;2.5 默认禁止对象整体保存
不推荐默认使用如下模式:
Entity entity = repository.findById(id);
BeanUtils.copyProperties(req, entity);
repository.save(entity);也不推荐:
repository.persist(entity);或者类似的对象整体保存方式作为复杂业务的默认更新方式。
原因是对象整体保存容易覆盖其他业务流程已经修改过的字段。
典型问题:
- A 用户编辑名称。
- B 用户处理状态。
- A 和 B 都基于旧对象保存。
- 后保存的一方可能覆盖前一方已经修改的字段。
业务真实期望通常是:
update order_table set name = ? where id = ?;和:
update order_table set status = ? where id = ?;两个动作互不影响。
因此,更新操作默认应采用指定字段更新。
2.6 状态流转使用条件更新
状态流转类操作不应只按 id 更新,建议带上当前状态条件。
推荐写法:
update order_table
set status = ?,
audit_time = now()
where id = ?
and status = ?;然后判断影响行数。
影响行数 = 1:更新成功
影响行数 = 0:数据不存在、状态已变化、重复操作或并发冲突这样可以避免重复审核、重复处理、状态回退和并发覆盖问题。
3. 接口对象命名规范
3.1 请求对象统一使用 Req 后缀
请求对象表示前端或外部系统传入后端的数据。
推荐命名:
CreateOrderReq
UpdateOrderNameReq
AuditOrderReq
OrderPageQueryReq
CallbackOrderReq不推荐命名:
OrderDTO
OrderBO
OrderVO
OrderParam
OrderModel原因是 DTO、BO、VO、Model 都不能明确表达数据方向。
看到 OrderDTO 时,很难判断它是:
- 前端请求对象。
- 后端响应对象。
- Service 内部传递对象。
- 数据库查询结果对象。
- 第三方接口对象。
3.2 响应对象统一使用 Resp 后缀
响应对象表示系统返回给前端或外部调用方的数据。
推荐命名:
OrderListResp
OrderDetailResp
OrderCreateResp
OrderAuditRespResp 的方向非常明确:只用于返回。
3.3 查询请求使用 QueryReq 或 PageQueryReq
查询类请求建议单独命名。
普通查询:
OrderQueryReq分页查询:
OrderPageQueryReq不建议把查询字段放入创建或编辑请求中。
3.4 禁止 Entity 直接作为接口入参或出参
Entity 是数据库持久化对象,不应直接暴露到接口层。
禁止:
public R<OrderEntity> detail(Long id)禁止:
public R<Void> create(OrderEntity entity)原因:
- Entity 字段通常多于接口需要。
- Entity 可能包含内部字段、敏感字段、状态字段。
- Entity 变更会影响接口契约。
- 前端可能传入不允许修改的字段。
- 数据库结构不应直接暴露给接口调用方。
推荐:
public R<OrderDetailResp> detail(Long id)public R<Void> create(CreateOrderReq req)4. Req 拆分规范
4.1 创建请求
创建请求只包含创建时允许提交的字段。
示例:
public class CreateOrderReq {
private Long merchantId;
private BigDecimal amount;
private String remark;
}不应包含审核字段、支付字段、回调字段、系统内部状态字段。
4.2 修改请求
修改请求应按具体动作拆分,而不是使用一个通用 UpdateReq。
推荐:
UpdateOrderNameReq
UpdateOrderRemarkReq
UpdateOrderAmountReq而不是:
UpdateOrderReq如果确实是一个“编辑基础信息”接口,可以使用:
UpdateOrderBaseInfoReq但其中也只应包含该接口允许编辑的字段。
4.3 审核请求
审核请求只包含审核动作需要的字段。
public class AuditOrderReq {
private Long id;
private Integer auditResult;
private String auditRemark;
}对应 SQL 只更新审核字段和状态字段。
update order_table
set status = ?,
audit_result = ?,
audit_remark = ?,
audit_time = now()
where id = ?
and status = ?;4.4 状态流转请求
状态流转请求应包含当前状态和目标状态,或者包含能判断状态流转合法性的字段。
public class UpdateOrderStatusReq {
private Long id;
private Integer fromStatus;
private Integer toStatus;
}对应 SQL:
update order_table
set status = ?
where id = ?
and status = ?;不建议直接:
update order_table
set status = ?
where id = ?;4.5 回调请求
外部回调请求应单独定义,不能复用内部业务 Req。
public class CallbackOrderReq {
private String orderNo;
private String callbackStatus;
private String callbackResult;
private String sign;
}回调接口要特别注意:
- 签名校验。
- 幂等处理。
- 状态条件更新。
- 原始报文记录。
- 异常返回格式。
5. Resp 拆分规范
5.1 列表响应
列表响应只返回列表展示需要的字段。
public class OrderListResp {
private Long id;
private String orderNo;
private BigDecimal amount;
private Integer status;
private LocalDateTime createTime;
}不要为了复用详情对象而返回大量无关字段。
5.2 详情响应
详情响应可以返回更多字段,但也不应无脑返回 Entity 全字段。
public class OrderDetailResp {
private Long id;
private String orderNo;
private BigDecimal amount;
private Integer status;
private String auditRemark;
private LocalDateTime auditTime;
private LocalDateTime createTime;
}如果不同角色看到的详情不同,可以继续拆分:
AdminOrderDetailResp
MerchantOrderDetailResp5.3 创建响应
如果创建后只需要返回 id,可以单独定义:
public class CreateOrderResp {
private Long id;
}不要为了返回一个 id,就返回完整 Entity。
6. 数据访问规范
6.1 主方案:Repository / Mapper 风格
后台业务系统建议优先采用 Repository / Mapper 风格。
优点:
- SQL 可控。
- 更新字段明确。
- 条件更新方便。
- 批量更新方便。
- 复杂查询容易落地。
- 更适合后台业务流程式数据修改。
长期方向可以采用 MyBatis、MyBatis-Plus 或类似能力。
6.2 JPA/Panache 使用边界
JPA/Panache 不作为复杂后台业务的主数据访问方案。
它可以用于:
- 简单表。
- 只读查询。
- 配置类小表。
- Demo 或快速原型。
- 无并发修改风险的数据。
不建议用于:
- 多人并发编辑。
- 后台审核状态流转。
- 只更新部分字段。
- 复杂 SQL。
- 批量更新。
- 需要精确控制 SQL 的核心业务表。
原因是 JPA/Panache 更偏向对象状态管理和整体持久化,而后台业务系统更常见的是字段级更新。
6.3 指定字段更新
更新操作默认只更新当前业务动作负责的字段。
推荐:
update order_table
set name = ?
where id = ?;不推荐:
OrderEntity entity = findById(id);
entity.setName(req.getName());
save(entity);如果使用框架自动更新,也必须确认最终 SQL 只更新允许修改的字段。
6.4 动态更新要限制字段范围
动态更新不是“前端传什么就更新什么”。
正确原则是:
当前业务动作允许哪些字段,后端只在这些字段范围内做动态更新。例如 UpdateOrderNameReq 只有 name 字段,那么这个接口最多只能更新 name。
不能因为前端额外传了 status、payStatus、auditResult,后端就跟着更新。
6.5 状态流转必须检查影响行数
状态流转类 SQL 必须检查影响行数。
示例:
update order_table
set status = 2
where id = ?
and status = 1;处理逻辑:
影响行数为 1:状态流转成功。
影响行数为 0:状态已变化或数据不存在,应返回业务失败。这可以避免重复处理、并发处理和状态错乱。
6.6 批量更新
批量更新应明确更新范围和更新字段。
推荐:
update order_table
set sync_status = ?
where id in (?, ?, ?);或者:
update order_table
set expire_status = ?
where status = ?
and expire_time < now();批量更新尤其要避免无条件更新。
禁止:
update order_table
set status = ?;除非有非常明确的维护脚本场景,并且经过确认。
7. 参数校验规范
7.1 Req 中添加必要校验
请求对象中应添加必要的参数校验。
示例:
public class AuditOrderReq {
@NotNull(message = "ID不能为空")
private Long id;
@NotNull(message = "审核结果不能为空")
private Integer auditResult;
private String auditRemark;
}7.2 不同动作使用不同校验规则
不同业务动作不要共用一个大对象后再通过复杂分组校验解决。
如果创建、编辑、审核需要的字段不同,优先拆成不同 Req。
推荐:
CreateOrderReq
AuditOrderReq不推荐:
OrderReq + ValidationGroup分组校验可以使用,但不应成为大对象复用的借口。
8. 统一返回规范
接口返回建议统一使用统一结构。
示例:
public class R<T> {
private Integer code;
private String msg;
private T data;
}常见返回:
R<OrderDetailResp>
R<List<OrderListResp>>
R<PageResp<OrderListResp>>
R<Void>分页响应建议统一:
public class PageResp<T> {
private Long total;
private List<T> records;
}也可以根据前端习惯使用:
total
rows但整个系统应保持一致。
9. LocalDateTime 规范
9.1 接口返回格式
接口返回时间建议统一为:
yyyy-MM-dd HH:mm:ss避免同一系统中出现多种时间格式。
9.2 数据库字段
常见字段建议统一命名:
create_time
update_time
audit_time
pay_time
callback_time
finish_timeJava 类型优先使用:
LocalDateTime9.3 时区
如果系统涉及跨时区业务,需要单独制定时区规范。
如果只服务单一时区业务,也应明确数据库、应用、日志的时区设置,避免排查问题时混乱。
10. 异常处理规范
系统应统一异常返回格式。
需要统一处理:
- 业务异常。
- 参数校验异常。
- 数据不存在异常。
- 状态不允许异常。
- 数据库异常。
- 认证授权异常。
- 未知系统异常。
业务异常示例:
throw new BizException("当前状态不允许审核");返回示例:
{
"code": -1,
"msg": "当前状态不允许审核",
"data": null
}内部日志应记录完整异常堆栈,但返回前端的信息不应暴露数据库、SQL、堆栈等敏感细节。
11. 命名规范总结
11.1 推荐后缀
| 后缀 | 含义 |
|---|---|
Req |
请求入参 |
Resp |
响应结果 |
Entity |
数据库实体 |
Repository |
数据访问封装 |
Mapper |
SQL 映射 |
Service |
业务服务 |
Controller / Resource |
接口入口 |
Client |
外部服务客户端 |
Config |
配置类 |
Properties |
配置属性类 |
Exception |
异常类 |
11.2 谨慎使用的后缀
| 后缀 | 问题 |
|---|---|
DTO |
数据方向不明确 |
BO |
边界不清,容易变成杂物对象 |
VO |
不同团队理解不一致 |
Model |
过于泛化 |
Param |
可以用,但不如 Req 统一 |
Form |
偏页面表单,不适合所有接口 |
11.3 基本要求
接口层对象优先使用 Req / Resp,避免使用 DTO / BO / VO 作为通用命名。
12. 推荐调用链路
推荐结构:
前端
↓
Controller / Resource
↓
Req
↓
Service
↓
Repository / Mapper
↓
Entity / Table返回结构:
Entity / QueryResult
↓
Service 组装
↓
Resp
↓
Controller / Resource
↓
前端原则:
Req只向内流动。Resp只向外返回。Entity不直接暴露给前端。Repository / Mapper不接收前端大对象。Service负责业务编排。SQL只更新当前动作负责的字段。
13. 最终检查清单
| 分类 | 检查项 | 要求 |
|---|---|---|
| 接口对象 | 请求对象是否以 Req 结尾 | 必须 |
| 接口对象 | 响应对象是否以 Resp 结尾 | 必须 |
| 接口对象 | 是否一个业务动作一个 Req | 必须 |
| 接口对象 | 是否存在大 Req 多接口复用 | 禁止 |
| 接口对象 | Entity 是否直接作为入参 | 禁止 |
| 接口对象 | Entity 是否直接作为出参 | 禁止 |
| 数据访问 | 是否采用 Repository / Mapper 风格 | 推荐 |
| 数据访问 | 是否默认指定字段更新 | 必须 |
| 数据访问 | 是否存在对象整体保存覆盖风险 | 必须检查 |
| 数据访问 | 状态流转是否带当前状态条件 | 必须 |
| 数据访问 | 状态流转是否检查影响行数 | 必须 |
| 数据访问 | 批量更新是否有明确条件 | 必须 |
| 数据访问 | 动态更新是否限制字段范围 | 必须 |
| 校验 | Req 是否添加必要参数校验 | 必须 |
| 返回 | 是否统一返回 R | 推荐 |
| 返回 | 分页结构是否统一 | 必须 |
| 时间 | LocalDateTime 返回格式是否统一 | 必须 |
| 异常 | 业务异常是否统一处理 | 必须 |
| 异常 | 参数校验异常是否统一处理 | 必须 |
| 命名 | 是否避免 DTO/BO/VO 混用 | 推荐 |
| 维护 | 代码是否能从方法名和 Req 名看出业务意图 | 必须 |
14. 结论
后台业务系统的长期维护重点不是少写几个类,而是降低理解成本和误修改风险。
因此,本规范强调:
- 一个业务动作一个 Req。
- Req / Resp 明确数据方向。
- Entity 不直接暴露。
- 创建时部分字段 insert。
- 后续默认指定字段 update。
- 状态流转使用条件更新。
- Repository / Mapper 风格优先。
- 禁止一个大对象在多个业务动作中复用。
- 禁止对象整体保存导致无关字段被覆盖。
宁可多建几个小对象,也不要复用一个职责不清的大对象。
代码应当让维护者一眼看出:
这个接口做什么;
允许前端传什么;
后端会更新哪些字段;
不会影响哪些字段。这比短期少写几个类更重要。
最后编辑:张三 更新时间:2026-06-10 11:30