ThinkPHP 的动态排序,说起来不算复杂,但真正动手做的时候,不少人会踩进同一个坑里——模型静态配置搞不定,实时请求的排序参数又容易搞丢,安全上更是漏洞百出。下面这几个要点,基本覆盖了从“能跑”到“稳跑”的必经之路。
动态排序必须在查询链式调用中设置
模型类里写 $defaultOrder 或者在 initialize() 里调 order()?没用。框架根本不认这套。真正生效的时机只有一个:查询发起的那一刻。
- 正确的姿势是
Model::where(...)->order(...)->paginate(),order()必须在paginate()之前调用,顺序错了就白费。 - 建议封装成一个可复用的方法,比如
applyUserOrder($query, $request),在控制器或服务层调用,接收查询对象、返回修改后的实例,逻辑清晰也好维护。 - 如果偏好 Query Scope,可以在模型中写
scopeUserOrder,接收查询对象和偏好数组,直接操作查询对象本身,不必返回数据。
字段和方向必须白名单硬校验
用户传来的 sort_field 和 sort_dir 属于不可信输入,直接丢进 order() 等于把 SQL 注入的钥匙交给对方。
- 白名单要硬编码,比如
['id', 'create_time', 'username', 'status'],用in_array()判断是否合法。不在白名单里的字段,直接拒绝或 fallback 到默认值。 - 方向只接受
'asc'或'desc',其他一律退回默认方向(比如'id desc')。 - 千万、千万、千万别用字符串拼接:
order($field . ' ' . $dir)。即使字段校验过了,组合后也可能触发解析歧义或注入。必须用数组语法:order([$field => $dir])。TP 6+ 会自动为字段加反引号,避免保留字报错,安全又省心。
分页与参数透传要同步处理
启用分页后,用户选好的排序参数很容易在翻页时丢失——翻到第二页,排序就恢复到默认了。这个问题非常常见,处理起来也简单:
paginate()之后调appends(),显式把当前排序参数带进去,比如->appends(['sort_field' => $field, 'sort_dir' => $dir])。- 如果用了缓存,缓存 key 里必须包含排序字段和方向,否则不同排序结果可能命中同一缓存,数据就乱了。
- 前端表头点击排序时,建议用 GET 参数传递,这样浏览器的前进/后退和书签功能都能正常保留排序状态。
多字段排序统一用关联数组
有时候需要按多个字段分级排序,比如先按状态升序,再按创建时间降序。这时候不能拆成多次 order() 调用,后一次会覆盖前一次。
- 正确写法:
order(['status' => 'asc', 'create_time' => 'desc'])。 - 错误写法:
order('status asc')->order('create_time desc')—— 后面那个会把前面的覆盖掉。 - 数组写法天然支持字段名转义,也避免了手动处理空格、逗号、大小写这些容易出错的细节。
