$readonly对财务金额字段完全无效,仅在save()时丢弃字段,防篡改必须依赖数据库约束+应用层专用方法+状态流转校验三重控制。

$readonly 对财务金额字段(如 amount、balance)完全无效,它不校验值、不拦截非法写入、不阻止原生 SQL 或强制赋值,仅在模型 save() 时“礼貌性丢弃”字段 —— 真正防篡改必须靠数据库约束 + 应用层专用方法 + 状态流转校验三重控制。
为什么 $readonly = ['amount'] 在财务场景下是危险错觉
财务字段不是“不该被覆盖”,而是“只能按特定业务规则变更”。$readonly 的设计目标是过滤字段,不是守护资金安全。
- 用户 POST
{"id":1001,"amount":"9999999"},哪怕$readonly包含amount,只要控制器里写了$model->data($data, true)->save(),金额照改不误 - 定时任务调用
Db::name('account')->where('id', 1001)->update(['amount' => $new]),完全绕过模型,$readonly形同虚设 - 前端传错字段名(比如
AMOUNT或payAmount),框架匹配失败,amount字段直接进 SQL - 更严重的是:
$readonly不做任何合法性检查 —— 即使你拦住了字段,也没法阻止用户提交"-9999999"或字符串"abc",而这些值可能已在验证器之前就进入日志或触发扣款逻辑
财务字段必须用专用方法封装变更逻辑
金额变动不能是裸 update,必须绑定明确的业务动因(如充值、退款、扣费)和状态前提(如订单已支付、退款未审核)。
- 拒绝在控制器里直接调用
$model->amount = $x; $model->save(),哪怕加了$readonly - 在模型中定义
recharge($money)、refund($money)等方法,内部先校验当前status和可用余额,再执行原子更新:Db::raw('amount + ' . (int)$money) - 所有变更必须带操作类型、来源 IP、操作人 ID、原始单号(如
order_no),这些信息不进$readonly,但必须落库审计 - 关键操作(如大额退款)需额外走风控中间件,在模型层之前拦截,而不是等进到
recharge()才判断
数据库层必须设硬约束,不让非法值写入成为可能
PHP 层再严密也挡不住 DBA 直连、SQL 注入残留、或迁移脚本误操作。财务字段的底线是数据库拒绝非法值。
- MySQL 中对
amount字段加CHECK (amount >= 0)(8.0.16+)或用触发器抛异常;PostgreSQL 可直接用CHECK - 把
amount设为DECIMAL(12,2) UNSIGNED,避免负数和精度丢失,$readonly做不到这点 - 主键外的关键字段(如
order_id、trade_no)必须加唯一索引,防止重复记账;$readonly对索引无影响 - 禁止在财务表上用
allowField(true),所有写入必须显式声明字段列表,从源头堵住字段注入
最容易被忽略的点:金额字段根本不能依赖 $readonly 来“防篡改”
它既不校验数值范围,也不记录谁改的、为什么改、改前值是多少。一个被设为 $readonly 的 amount 字段,只是让 save() 忽略它 —— 而真正的风险,从来不在 save() 这一步。
真正该冻结的是变更入口:不是字段本身,而是“谁能在什么条件下改它”。这需要数据库约束兜底、应用层方法封装、中间件前置拦截三层咬合。把希望押在 $readonly 上,等于把保险柜钥匙挂在门把手上。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











