找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1009

积分

0

好友

131

主题
发表于 3 天前 | 查看: 6| 回复: 0

在业务开发时最怕的一件事,就是校验逻辑写得哪儿都是:

  • 有些在 Controller
  • 有些在 Service
  • 有些靠数据库
  • 有些写在前端
  • 有些写在 if 判断里
  • 有些写成注解

久而久之,业务校验变成一锅粥,想重构都无从下手。其实,大部分系统的问题不是“不会校验”,而是不知道校验应该放哪一层

这篇文章旨在厘清业务校验最核心的职责划分:Controller、Service、数据库分别负责什么校验?边界在哪里? 理解并实践这一分层原则,将显著提升整个系统的可维护性与代码清晰度。

1. 先明确一点:只有三类校验

业务逻辑里的所有校验,拆开本质只有三类:
① 格式校验
比如字符串不能为空、手机号格式正确、金额必须大于 0。
② 业务校验
比如库存够不够、用户是否具有权限、订单状态是否允许支付。
③ 数据一致性校验
比如唯一性、外键、约束、状态合法性。

这三类校验,分别应该由 Controller、Service 和数据库协同完成。

2. Controller 的职责:只做“格式校验”

Controller 层的职责非常单纯:验证请求参数是否合法,禁止格式错误的脏数据进入业务层。

属于 Controller 的校验:

  • 必填字段校验(不能为空)
  • 数字范围校验(>0、>=0)
  • 字符串格式校验(手机号、邮箱)
  • 列表不能为空
  • DTO 基础合法性

Java 的 Spring 框架中,原生支持通过 @Valid 注解进行声明式校验:

@PostMapping("/create")
public Result create(@Valid @RequestBody OrderDTO dto) {
    orderService.create(dto);
    return Result.ok();
}

对应的 DTO 类:

@Data
public class OrderDTO {
    @NotNull(message = "商品ID不能为空")
    private Long productId;
    @Min(value = 1, message = "购买数量必须 >= 1")
    private Integer amount;
}

Controller 的核心目标:

  • 快速拒绝格式错误的输入。
  • 防止 Service 层处理无意义的数据,减轻业务层负担。
  • 提供清晰的接口契约。

注意:Controller 不应进行任何涉及业务规则的判断。

3. Service 的职责:业务校验核心区

最重要的校验层就是 Service,它负责处理所有“业务规则”:只要涉及业务含义和状态的判断,都属于 Service。

典型业务校验场景:

  • 商品是否存在
  • 库存是否足够
  • 用户是否有权限执行操作
  • 是否重复下单
  • 订单状态是否允许进行下一步(如支付)
  • 金额是否超过用户上限
  • 业务条件是否存在冲突

示例代码:

public void createOrder(OrderDTO dto) {
    // 业务校验:商品是否存在、状态是否可售、库存是否充足
    Product product = productMapper.findById(dto.getProductId());
    if (product == null) {
        throw new BizException("商品不存在");
    }
    if (product.getStock() < dto.getAmount()) {
        throw new BizException("库存不足");
    }
    if (!product.getStatus().equals(Status.ON_SALE)) {
        throw new BizException("商品已下架");
    }
    // 校验通过,执行业务逻辑
    saveOrder(dto);
}

为何业务校验必须放在 Service 层?

  • 更贴近业务规则:Service 是业务逻辑的聚合点。
  • 易于测试与复用:独立的业务校验方法便于单元测试,也方便被定时任务、消息队列消费者等其他组件调用。
  • 保持控制器纯净:避免业务规则分散在各个 Controller 中,导致维护困难。

4. 数据库的职责:数据一致性的最后防线

有些校验是程序无法完全保证的,必须由数据库作为兜底,确保“不可被程序错误或并发操作绕过的核心规则”。

属于数据库的校验(通过约束实现):

  • 唯一索引(防止重复)
  • 非空约束(NOT NULL)
  • 外键约束(保证关联完整性)
  • 字段类型与长度限制
  • 枚举值范围(通过 CHECK 约束或字段类型,如 TINYINT)

示例 SQL:

CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    phone VARCHAR(20) NOT NULL UNIQUE,
    status TINYINT NOT NULL
);

为什么这些必须交给数据库?

  • 防御程序 Bug:即使程序逻辑疏于校验,数据库也能从底层拦截。
  • 保证强一致性:在并发场景下,数据库约束是保证数据最终一致性的基石。
  • 终极安全保障:建立无法从应用层绕过的数据规则底线。

一句话总结三层协作关系:

  • Controller 拦住无效的格式输入。
  • Service 拦住不符合业务规则的非法操作。
  • 数据库 拦住底线级别的数据一致性错误。

三者职责清晰,缺一不可。

5. 三层职责边界总结

直接给出清晰结论:

✔ Controller 负责(格式校验):
  • 参数格式是否正确
  • 字段是否为空
  • 类型是否合法
  • 数值是否在约定范围内
  • 不涉及任何具体业务规则的判断
✔ Service 负责(业务校验):
  • 相关实体是否存在(如商品、用户)
  • 操作是否被业务规则允许(如是否有权限)
  • 状态是否正确,流程是否可继续
  • 业务条件是否满足(如库存、余额)
  • 是否构成重复操作
✔ 数据库负责(一致性校验):
  • 字段不可为 NULL(非空约束)
  • 数据唯一性(唯一索引)
  • 关联关系有效性(外键约束)
  • 数据类型与范围(字段类型、CHECK约束)
  • 强一致性规则兜底

将校验放在正确的层级,是构建可维护系统的关键。

6. 一个反例:业务校验放在 Controller 会怎样?

观察下面这段问题代码:

@PostMapping("/create")
public Result create(@RequestBody OrderDTO dto) {
    // 错误示范:在Controller中进行业务判断
    if (!productService.isOnSale(dto.getProductId())) {
        return Result.fail("商品下架");
    }
    if (!productService.stockEnough(dto.getProductId(), dto.getAmount())) {
        return Result.fail("库存不足");
    }
    // ... 调用service
}

带来的问题:

  • 逻辑无法复用:其他入口(如消息处理、定时任务)无法直接复用这套校验。
  • 业务规则分散:相同的商品状态判断可能散落在多个Controller中。
  • 违背单一职责:Controller 混杂了参数校验、业务判断、流程协调等多重职责。
    久而久之,系统将变得难以理解和维护。

7. 更专业的落地方式:颗粒化的校验方法

在 Service 层中,推荐将校验逻辑拆分为独立的、颗粒化的方法:

// 商品校验
private void validateProduct(Long productId) { ... }
// 库存校验
private void validateStock(Long productId, int amount) { ... }
// 用户权限校验
private void validateUserPermission(Long userId) { ... }

public void createOrder(OrderDTO dto) {
    validateProduct(dto.getProductId());
    validateStock(dto.getProductId(), dto.getAmount());
    validateUserPermission(getCurrentUserId());
    // 所有校验通过后,执行业务逻辑
    doCreateOrder(dto);
}

优点:

  • 高可复用性:校验方法可以被同一Service内的多个主方法调用。
  • 单一职责:每个校验方法只关心一个特定的规则,清晰明确。
  • 便于测试:可以针对每个校验方法编写独立的单元测试。
  • 易于维护:规则变更时,只需修改对应的校验方法。

此时,控制器将变得非常干净,只负责参数格式校验和请求转发:

@PostMapping("/create")
public Result create(@Valid @RequestBody OrderDTO dto) {
    orderService.createOrder(dto); // 一行调用,清晰明了
    return Result.ok();
}

总结

业务校验该放在哪一层?核心原则如下:

校验类型 负责层级 核心目标
格式校验 Controller 输入“正确不正确”
业务校验 Service 操作“允许不允许”
一致性校验 数据库 数据“绝不允许发生”

遵循 Controller 校验格式、Service 校验业务、数据库保证底线 的分层规范,是在企业级项目中构建清晰、健壮、易维护架构的标准实践。通过明确各层边界,并利用像数据库约束这样的底层机制,可以显著提升代码质量和系统可靠性。




上一篇:树莓派CM0 NANO本地部署EMQX实战:基于Docker的物联网MQTT接入测试
下一篇:AI编程的认知差异:为何工具更擅长后端,舆论却更担忧前端?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 17:36 , Processed in 0.123371 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表