$readonly仅在模型save()和update()中过滤字段,无法防御原生SQL、强制赋值、大小写不匹配或关联模型修改;它不是安全锁,而是开发提醒。

$readonly 对已支付订单的核心字段(如 pay_status、amount、user_id)仅在模型 save() 和 update() 中起过滤作用,无法防住原生 SQL、强制赋值、大小写不匹配或关联模型修改——它不是锁,只是提醒你“别这么写”。
哪些字段该设为只读?看业务语义,不是看名字
已支付订单的 pay_status 必须设只读:一旦变成 2(已支付),就不该被任何 POST 请求覆盖回 1(待支付)或 0(取消)。但 user_id 要谨慎——如果后台支持“订单转交”,设只读反而卡死流程。amount 通常不该进 $readonly,因为退款、补差价等场景需合法变更,应走专用方法(如 refunding())校验状态和权限,而不是靠只读硬拦。
-
create_time、delete_time、source属于“写入即定型”,必须只读 -
update_time别加进$readonly,否则它真不会更新——框架不会自动补,也不会报错,时间就冻住了 -
id不用设只读,TP 新增时自动忽略主键赋值;加了反而干扰 UUID 或自增逻辑
为什么 pay_status 还是被改了?检查这三处绕过点
日志里看到已支付订单的 pay_status 被改成 1,大概率不是 $readonly 失效,而是代码绕过了它:
- 用了
Db::name('order')->where('id', 123)->update(['pay_status' => 1]):原生 SQL 完全不走模型层,$readonly形同虚设 - 调了
$order->data($data, true)->save():第二个参数true表示强制赋值,直接跳过$readonly过滤 - 数据库列名是
pay_status,但$readonly写成payStatus或PAY_STATUS:框架按字符串精确匹配,大小写/下划线不一致就认不出来
真要防住已支付订单字段,得组合用验证器 + 专用方法 + 数据库约束
$readonly 是模型层的“礼貌性过滤”,不是安全边界。对已支付订单这类高风险场景,必须分层加固:
立即学习“PHP免费学习笔记(深入)”;
- 验证器里用
only(['remark', 'logistics_no'])显式放行可编辑字段,比$readonly更早拦截非法字段 - 封装
changePayStatus($newStatus)方法,在内部校验当前状态是否允许流转(比如只允许从1→2,禁止反向)、检查操作人权限、记录审计日志 - 数据库加 CHECK 约束(MySQL 8.0.16+ 支持):
CHECK (pay_status = 2 OR pay_status IN (0,1)),让非法 UPDATE 直接报错 - 关键字段(如
amount)禁用直接赋值,改用setAmountAttr()拦截:若当前pay_status === 2,则抛异常或静默拒绝
关联模型的只读字段最容易被忽略
预载入 with('user') 后,$order->user->name = 'xxx'; $order->user->save() 仍能改用户表——$readonly 在 Order 模型里对 User 模型完全无效。
稳妥做法是:在关联模型(如 User)的 setNameAttr() 里加判断,或避免修改预载入对象,改用显式更新:User::where('id', $order->user_id)->update(['name' => 'xxx'])。最常被忽略的是:只读字段一旦设错,比如把本该动态更新的 update_time 加进 $readonly,那它就真不会自动更新了——数据悄悄“冻住”,连警告都没有。



















