场景
系统里有个”清理测试店铺数据”的功能,用来一键清空测试订单、日志和商品记录。需求很直接:给测试人员用的,点一下就把脏数据清干净,店铺保留以便重新测试。
问题在于:这个按钮不能用在生产店铺上。
最直接的方案是加一个 if 判断:
if ($shop->shop_type !== 'test') {
die('Only test shops can be cleaned up');
}
但这就够了吗?
如果这段代码的校验逻辑因为某个疏忽被跳过,或者未来的开发者没注意到这个约束,生产数据一样会被清掉。这不是假设——类似的线上事故每天都在发生。
第一层:应用层校验
写在 Controller 里,语义最清晰:
if (!$shop->isTest()) {
return error('Only test shops can be cleaned up');
}
isTest() 是一个显式的命名,比 shop_type !== 'test' 更容易被代码审查发现。但这一层完全依赖程序员的正确执行——如果某人绕过这个 Controller 直接调 Repository,它就失效了。
这一层防的是”有意绕过”,不防”无意疏忽”。
第二层:持久层 SQL 保护
把校验下沉到 Repository 的 SQL 语句中:
$sql = "DELETE FROM orders WHERE shop_id = ? AND ? = 'test'";
$stmt->bind_param('is', $shopId, $shop->shop_type);
这里的关键是 AND ? = 'test'。? 是 shop_type 的值,它不是列名匹配,而是作为一个常量条件拼接进 SQL。如果 shop_type 是 'production',条件就变成了 AND 'production' = 'test'——永假,一行数据都不会删。
这一层不依赖上层的调用是否正确。即使 Controller 层忘记调用 isTest(),SQL 层面的”弹药库”是锁住的。
第三层:数据库约束兜底
最后的物理防线:
`shop_type` enum('production','test') NOT NULL DEFAULT 'production'
ENUM 限制了 shop_type 只能取这两个值,NOT NULL 防止空值绕过,DEFAULT ‘production’ 确保新增记录默认就是生产类型。
如果因为某种原因,上两层全部失效(比如 BUG 导致传入的 shop_id 和 shop_type 不匹配),物理上存在的 NOT NULL 和 ENUM 限制依然会阻止非法操作。
三层对比
应用层校验 → 语义检查(是否符合业务规则)
Repository 层 → SQL 级保护(AND ? = 'test')
数据库层 → 约束兜底(ENUM + NOT NULL + DEFAULT)
| 维度 | 应用层 | Repository 层 | 数据库层 |
|---|---|---|---|
| 依赖 | 代码逻辑正确 | 绑定参数正确 | 表结构正确 |
| 绕过方式 | 跳过 Controller | 修改 SQL | 修改表结构 |
| 绕过难度 | 低 | 中 | 高 |
| 故障发现 | 编译/测试期 | 运行时 SQL 报错 | 运行时 SQL 报错 |
每一层的保护强度和绕过难度依次递增。三层合在一起,不是简单的”三重保险”,而是从业务语义到物理约束的递进式防御。
这不是特例
同样的模式可以套用到很多场景:
删除用户
Controller: currentUser.isAdmin()
Repository: DELETE FROM users WHERE id = ? AND role != 'super_admin'
DB: FOREIGN KEY + ON DELETE RESTRICT
退款操作
Controller: order.isRefundable()
Repository: UPDATE orders SET refunded = 1 WHERE id = ? AND status = 'paid'
DB: CHECK (refunded IN (0, 1))
发布上线
Controller: deploy.hasApproval()
Repository: INSERT INTO releases WHERE branch = ? AND environment IN ('staging')
DB: INDEX (environment, branch) UNIQUE
总结
一个 if 判断不够,因为代码会变、人会犯错、架构会重构。但数据库的物理约束不会。
三层防御 = 应用语义校验 × SQL 级保护 × 数据库物理约束
每一层独立运作,互不依赖。任何一层失守,后面还有一层兜底。这不是过度设计——这是承认”人总会犯错”之后做出的合理安排。