外观
后端开发规范
2026-04-01
基于 Nebula 框架实际项目整理,适用于所有业务后端开发。
读完本文,你将掌握从实体类到 Controller 的完整分层规范,以及常见陷阱的正确写法。
目录
- 一、实体类规范(DO)
- 二、VO 规范
- 三、MapStruct 对象转换
- 四、MapperExt 动态查询规范
- 五、Mapper 接口规范
- 六、Service 层规范
- 七、Controller 层规范
- 八、通用 Java 规范
- 九、文件流与特殊响应规范
- 十、常量管理规范
- 十一、新业务模块开发检查清单
一、实体类规范(DO)
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_demo") // 必须:映射数据库表名(t_ 前缀)
@Schema(description = "示例实体") // 必须:Swagger 文档描述
public class DemoDO extends BaseEntity { // 必须继承 BaseEntity
@Schema(description = "示例名称")
private String demoName; // 驼峰,自动映射 demo_name 列
@Schema(description = "状态(0:正常 1:禁用)")
private Integer status;
}注意事项:
BaseEntity已有的字段(id、tenantId、creator、createTime、remark等 11 个)不要在子类重复声明- 字段类型映射:
VARCHAR → String、INT → Integer、BIGINT → Long、DATETIME → LocalDateTime - 枚举值在注释中说明,如:
状态(0:正常 1:禁用) - DO 中禁止添加计算属性或前端展示逻辑
二、VO 规范
2.1 分页入参(XxxPageParamVO)
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "示例分页查询参数")
public class DemoPageParamVO extends PageParam { // 必须继承 PageParam
@Schema(description = "示例名称(支持模糊匹配)")
private String demoName; // 所有过滤字段均为可选,不传则不过滤
@Schema(description = "状态(0:正常 1:禁用)")
private Integer status;
}PageParam 内置:pageNo(默认 1)、pageSize(默认 10)、sorts(List<SortItem>)
2.2 新增/修改入参(XxxSaveParamVO)
新增和修改共用一个 VO,通过校验分组区分:
@Data
@Schema(description = "示例新增/修改参数")
public class DemoSaveParamVO implements Serializable {
/** 修改专用校验分组 */
public interface UpdateGroup {}
@NotNull(groups = UpdateGroup.class, message = "修改时主键 id 不能为空")
@Schema(description = "主键(新增时不传,修改时必填)")
private Long id;
@NotBlank(message = "示例名称不能为空")
@Size(max = 100, message = "名称不能超过 100 个字符")
@Schema(description = "示例名称", requiredMode = Schema.RequiredMode.REQUIRED)
private String demoName;
/** 判断是否为新增操作 */
public boolean isNew() { return id == null; }
}Controller 使用方式:
- 新增:
@Valid @RequestBody(不校验 id) - 修改:
@Validated(DemoSaveParamVO.UpdateGroup.class) @RequestBody(id 变为必填)
2.3 列表响应 VO(XxxRespVO)
只返回列表展示所需字段:
@Data
@Schema(description = "示例列表项")
public class DemoRespVO implements Serializable {
@Schema(description = "主键ID") private Long id;
@Schema(description = "示例名称") private String demoName;
@Schema(description = "状态") private Integer status;
@Schema(description = "创建人") private String creator;
@Schema(description = "创建时间") private LocalDateTime createTime;
@Schema(description = "最后修改时间") private LocalDateTime modifyTime;
}2.4 详情响应 VO(XxxDetailVO)
包含所有相关字段,含备注、审计信息:
@Data
@Schema(description = "示例详情")
public class DemoDetailVO implements Serializable {
private Long id;
private String demoName;
private Integer status;
@Schema(description = "备注") private String remark;
@Schema(description = "自定义扩展字段") private String customFields;
@Schema(description = "创建人") private String creator;
@Schema(description = "创建时间") private LocalDateTime createTime;
@Schema(description = "创建人ID") private Long createUserId;
@Schema(description = "最后修改人用户名") private String updater;
@Schema(description = "最后修改人ID") private Long modifyUserId;
@Schema(description = "最后修改时间") private LocalDateTime modifyTime;
}对外 VO 设计原则:VO 和 RPC DTO 中禁止包含存储内部字段(如
storagePath、bucketName、storageType),调用方需要访问文件时应通过getAccessUrl接口获取 URL。
三、MapStruct 对象转换
@Mapper(config = BaseMapperConfig.class) // 必须指定,忽略未映射字段(防编译报错)
public interface DemoConvert {
DemoConvert INSTANCE = Mappers.getMapper(DemoConvert.class); // 全局单例
DemoRespVO doToRespVO(DemoDO demoDO);
DemoDetailVO doToDetailVO(DemoDO demoDO);
DemoDO saveParamToDo(DemoSaveParamVO saveParam);
// 修改:只覆盖 saveParam 中有值的字段,createTime 等不受影响
void saveParamMergeToDo(DemoSaveParamVO saveParam, @MappingTarget DemoDO demoDO);
}四种使用场景:
// 分页查询 → 批量转换
return pageResult.convert(DemoConvert.INSTANCE::doToRespVO);
// 详情查询
return DemoConvert.INSTANCE.doToDetailVO(demoDO);
// 新增
DemoDO demoDO = DemoConvert.INSTANCE.saveParamToDo(saveParam);
save(demoDO); // id、tenantId、createTime 等由框架自动填充
// 修改(增量更新)
DemoDO demoDO = getByIdOrThrow(id);
DemoConvert.INSTANCE.saveParamMergeToDo(saveParam, demoDO);
updateById(demoDO);四、MapperExt 动态查询规范
4.1 类结构
@Repository
@RequiredArgsConstructor
public class DemoMapperExt {
private final DemoMapper demoMapper;
}4.2 分页两段式查询(必须遵守)
public PageResult<DemoDO> pageQuery(DemoPageParamVO query) {
LambdaQueryWrapper<DemoDO> wrapper = buildPageWrapper(query);
// 第一段:先 count,count=0 时短路,避免无效的 LIMIT 查询
Long count = demoMapper.selectCount(wrapper);
if (count == null || count == 0) {
return PageResult.empty(query);
}
// 第二段:fetch,false 表示不让 MP 自动再 count 一次
Page<DemoDO> mpPage = new Page<>(query.getPageNo(), query.getPageSize(), false);
applySort(mpPage, query);
Page<DemoDO> result = demoMapper.selectPage(mpPage, wrapper);
return PageResult.of(result.getRecords(), count, query);
}4.3 动态条件构建规则
private LambdaQueryWrapper<DemoDO> buildPageWrapper(DemoPageParamVO query) {
return new LambdaQueryWrapper<DemoDO>()
// String 类型:用 StringUtils.hasText() 判空(防止空字符串参与过滤)
.like(StringUtils.hasText(query.getDemoName()), DemoDO::getDemoName, query.getDemoName())
// 对象类型:用 != null 判空
.eq(query.getStatus() != null, DemoDO::getStatus, query.getStatus())
.orderByDesc(DemoDO::getCreateTime);
}判空规则:
| 字段类型 | 判空方式 |
|---|---|
String | StringUtils.hasText(value) |
Integer / Long / 其他对象 | value != null |
4.4 唯一性检查
// excludeId:修改时传入自身 id(排除自身),新增时传 null
public boolean existsByCode(String demoCode, Long excludeId) {
return demoMapper.selectCount(
new LambdaQueryWrapper<DemoDO>()
.eq(DemoDO::getDemoCode, demoCode)
.ne(excludeId != null, DemoDO::getId, excludeId)
) > 0;
}五、Mapper 接口规范
@Mapper
public interface DemoMapper extends BaseMapper<DemoDO> {
// BaseMapper 已提供:save / removeById / updateById / selectById / selectPage 等
// 极少数需要固定 SQL 时,才在此用 @Select,必须手动加租户和删除过滤
@Select("SELECT * FROM t_demo WHERE demo_code = #{code} AND delete_flag = 0 LIMIT 1")
Optional<DemoDO> selectByCode(@Param("code") String code);
}多租户与逻辑删除过滤规则:
| SQL 来源 | 自动过滤 delete_flag | 自动追加 tenant_id |
|---|---|---|
MP BaseMapper / LambdaQueryWrapper | ✅ 自动 | ✅ 自动(TenantLineInnerInterceptor) |
@Select 注解 / XML Mapper | ❌ 需手动加 | ❌ 需手动加 |
结论:凡是需要多租户隔离的查询,必须走
LambdaQueryWrapper(放在MapperExt),不能用@Select注解 SQL。
六、Service 层规范
6.1 接口定义
public interface IDemoService extends BaseService<DemoDO> {
PageResult<DemoRespVO> pageDemo(DemoPageParamVO query);
DemoDetailVO getDemoById(Long id);
Long createDemo(DemoSaveParamVO saveParam);
void updateDemo(DemoSaveParamVO saveParam);
void deleteDemo(List<Long> ids);
void enableDemo(Long id); // 语义化命名,而非 updateStatus
void disableDemo(Long id);
}6.2 实现类结构
@Slf4j
@Service
@RequiredArgsConstructor
public class DemoServiceImpl extends BaseServiceImpl<DemoMapper, DemoDO>
implements IDemoService {
private final DemoMapperExt demoMapperExt;
}6.3 关键规范
写操作加事务:
@Transactional(rollbackFor = Exception.class)
public Long createDemo(DemoSaveParamVO saveParam) { ... }查询不存在时抛异常(不要手动判 null):
DemoDO demoDO = getByIdOrThrow(id); // 不存在自动抛 BusinessException(DATA_NOT_FOUND)业务校验抛异常:
if (demoMapperExt.existsByCode(saveParam.getDemoCode(), null)) {
throw new BusinessException(GlobalErrorCode.PARAM_INVALID.getCode(),
"编码 [" + saveParam.getDemoCode() + "] 已存在");
}状态变更(最小更新,不污染其他字段):
public void enableDemo(Long id) {
getByIdOrThrow(id);
DemoDO update = new DemoDO();
update.setId(id);
update.setStatus(0);
updateById(update);
log.info("启用示例成功,id={}", id);
}关键写操作必须打日志:
log.info("新增示例成功,id={},code={}", demoDO.getId(), demoDO.getDemoCode());
log.info("修改示例成功,id={}", id);
log.info("批量逻辑删除成功,ids={}", ids);七、Controller 层规范
7.1 类结构
@Tag(name = "示例管理", description = "t_demo 增删改查接口")
@Validated
@RestController
@RequestMapping("/ltc/demo") // 路径格式:/{业务域}/{资源名}
@RequiredArgsConstructor
public class DemoController {
private final IDemoService demoService; // 只依赖接口,不依赖 Impl
}7.2 Swagger 注解规范
| 注解 | 位置 | 说明 |
|---|---|---|
@Tag(name, description) | Controller 类 | 接口分组 |
@Operation(summary) | 接口方法 | 接口描述 |
@Parameter(name, description, required) | Path/Query 参数 | 参数说明 |
@Schema(description) | VO 字段 | 字段说明 |
@Schema(requiredMode = REQUIRED) | VO 必填字段 | 标记必填 |
7.3 统一响应规范
- 返回类型必须为
R<T>,不允许直接返回裸对象 - 有数据:
R.ok(data) - 无数据(写操作):
R.ok() - Controller 中不处理异常,由全局异常处理器统一响应
八、通用 Java 规范
8.1 依赖注入
统一使用构造器注入,配合 @RequiredArgsConstructor(字段必须为 final):
private final DemoMapperExt demoMapperExt; // 不使用 @Autowired8.2 日志规范
@Slf4j // Lombok 自动注入 log 字段
// 使用 log.info/warn/error,不使用 System.out.println
// 异常捕获:log.error("操作失败", e)
// 不在日志中打印密码、Token 等敏感信息8.3 空值处理
// String 判空:使用 StringUtils.hasText(同时排除 null 和空白字符串)
if (!StringUtils.hasText(demoCode)) { ... }
// 集合判空:先判 null 再判 empty
if (ids == null || ids.isEmpty()) { return; }
// 对象判空:使用框架提供的 getByIdOrThrow,不要手动 if(xx == null) throw ...8.4 异常处理
| 场景 | 正确用法 |
|---|---|
| 业务校验失败(参数非法、数据不存在、状态不符) | BusinessException(GlobalErrorCode.PARAM_INVALID, "...") |
| 系统级失败(IO 异常、外部调用失败、序列化失败) | SystemException(GlobalErrorCode.INTERNAL_SERVER_ERROR, "...", e) |
| 查询不存在直接抛异常 | getByIdOrThrow(id) |
// IO 异常正确写法
try {
StreamUtils.copy(in, out);
} catch (IOException e) {
throw new SystemException(GlobalErrorCode.INTERNAL_SERVER_ERROR, "文件下载失败: " + e.getMessage(), e);
}- 不允许吞掉异常(
catch(Exception e) {}) @Transactional必须指定rollbackFor = Exception.class
九、文件流与特殊响应规范
Controller 禁止直接处理 InputStream/OutputStream,所有文件下载、预览、流式响应逻辑必须下沉到 Service 层:
// ✅ Controller:只做透传
@GetMapping("/{id}/download")
public void download(@PathVariable Long id, HttpServletResponse response) {
fileService.download(id, response);
}
// ❌ Controller 手动设置响应头、读写流 —— 严格禁止Service 中文件流处理规范:
// 使用 StreamUtils.copy 替代手动 byte[] 循环
try (InputStream in = storageService.download(path, bucket);
OutputStream out = response.getOutputStream()) {
StreamUtils.copy(in, out); // org.springframework.util.StreamUtils
out.flush();
} catch (IOException e) {
throw new SystemException(GlobalErrorCode.INTERNAL_SERVER_ERROR, "文件下载失败", e);
}十、常量管理规范
业务域内使用的字符串常量必须集中定义到 {域}Constants 类,禁止在 Service/Controller 中出现硬编码字符串常量:
// 位置:{业务模块}-api/src/main/java/com/.../api/constant/{域名}Constants.java
public final class LtcConstants {
private LtcConstants() {}
public static final String SERVICE_NAME = "nebula-ltc";
public static final String URI_PREFIX = CommonConstants.RPC_URI_PREFIX + "/nebula/ltc";
public static final String STATUS_NORMAL = "NORMAL";
public static final String STATUS_DISABLE = "DISABLE";
}常量类放置规则:
| 常量类型 | 放置位置 |
|---|---|
| 微服务 RPC 路径、服务名、业务枚举值 | {业务模块}-api 的 constant/ 包下 |
| 框架内部常量(不对外暴露) | {业务模块}-core 的 constant/ 包下 |
| 全局公共常量 | nebula-framework-core 的 CommonConstants |
