save(['ip' => 'x', 'user_agent' => 'y']) 不生效,因 ThinkPHP 自动完成仅在 data() 后的字段过滤阶段触发,而直接 save($data) 绕过该阶段;应改用 data()->save() 或 create()(TP5.1+ 推荐前者)。

自动填充 IP 和 User-Agent 不能靠 save() 直接传数组,必须走 data() + save() 或 create() 流程,否则字段直接被跳过。
为什么 save(['ip' => 'x', 'user_agent' => 'y']) 不生效
ThinkPHP 的自动完成($_auto 或 $auto/$insert)只在模型执行字段过滤阶段触发,而 save($data) 直接绕过该阶段,把数组原样交给数据库写入。哪怕字段名写对、规则配全,也完全不执行。
- 常见错误现象:
ip和user_agent字段始终为空或为默认值 - TP5.1+ 已废弃
create(),推荐统一用$model->data($data)->save() - 若用了严格模式(
'strict' => true),未在$schema中声明的字段还会被静默丢弃 - 别在控制器里手动拼接再传给
save()—— 这等于把本该由模型承担的职责外移,后续加代理 IP 提取、UA 截断等逻辑时极易遗漏
get_client_ip() 和 request()->header('user-agent') 怎么安全取值
原生函数和框架方法都得做清洗,否则容易入库失败或哈希错乱。
-
get_client_ip()在多层代理下可能返回数组,应先判断类型:is_string($ip) ? $ip : ($ip[0] ?? '0.0.0.0') - 别用
$this->request->ip(true),它在某些 Nginx + Cloudflare 组合下会返回异常格式 -
request()->header('user-agent')必须trim(),且建议substr($ua, 0, 200)截断,防止超长写入失败(MySQL TEXT 虽不限长,但索引和指纹生成都依赖可控长度) - 空 UA 或仅含空格/控制字符的字符串应归为
'unknown',避免 MD5 出现大量重复值
用修改器(mutator)还是自动完成规则?
优先选修改器,尤其当字段需参与设备指纹、终端识别等后续逻辑时。
立即学习“PHP免费学习笔记(深入)”;
- 修改器如
setIpAttr()和setUserAgentAttr()在赋值时即触发,$model->ip = 'x'或$model->data(['ip' => 'x'])都会进方法,比自动完成更早、更稳 - 自动完成规则(如
$insert = ['ip', 'user_agent'])只在插入时生效,更新登录 IP 等场景就得额外配$update,维护成本高 - 修改器可复用逻辑:比如
setFingerprintAttr()内部调用$this->ip和$this->user_agent,天然拿到清洗后值 - 注意命名严格对应字段名:
ip字段 →setIpAttr()方法;user_agent→setUserAgentAttr()(驼峰转下划线自动处理,但建议显式命名)
数据库字段要不要设 NOT NULL?
要,但必须配默认值或允许空字符串,否则 strict mode 下新增空模型会报错:Field 'ip' doesn't have a default value。
- 迁移文件中建议:
$table->string('ip', 45)->nullable(false)->default('')(IPv6 最长 45 字符) -
user_agent同理:$table->string('user_agent', 200)->default('') - 别设
DEFAULT NULL,因为NOT NULL DEFAULT NULL在 MySQL 8.0+ 会直接报语法错 - 如果业务强依赖非空,可在模型
beforeInsert()事件里补校验:if (!$this->ip) $this->ip = '0.0.0.0';
真正麻烦的不是怎么填,而是填进去的值是否稳定可追溯——IP 要区分真实客户端还是代理头,UA 要防截断失真,字段变更时还要同步影响指纹和终端识别逻辑。这些细节一旦散落在控制器或中间件里,下次改需求时基本得重翻一遍代码。



















