- 仿照 medoo 的方法和 条件查询,内部使用 $wpdb
- codex-5.6-sol
- 能用,已经在一个表上面试验了
- 缺少对象缓存
这里备份一下当时用AI手写的代码
目前项目已经替换为基于BerlinDB库封装WU_DB类,直观的好处可能是自动化注册表升级表?更全面一点?随之而来的是空间占用增加四五百KB ….
<?php
/**
* 自定义表基础操作方法
* 使用时继承该类,按需重写方法属性
*
* 【基本用法】
* class Demo_Table extends WU_DB_Table {
* protected function config() {
* return [
* 'table_name' => 'demo',
* ];
* }
* }
*
* 【表名说明】
* table_name 默认填写不带 $wpdb->prefix 的表名,如 demo 会自动转成 wp_demo。
* 如果 table_name 已经包含 $wpdb->prefix,则直接使用原表名。
*
* 【字段说明】
* $columns 支持字符串、数组和 "*"。
* 示例:'*'、'id,title'、['id','title']。
* 为了避免 SQL 注入,字段名和表名只支持普通 SQL 标识符,不支持表达式和函数。
*
* 【$where 查询条件】
* 简单等于:['user'=>'admin','age'=>'18']
* NULL 判断:['deleted_at'=>null] 等同于 deleted_at IS NULL
* IN 查询:['id'=>[1,2,3]] 或 ['id[in]'=>[1,2,3]]
* NOT IN 查询:['id[!]'=>[1,2,3]] 或 ['id[not in]'=>[1,2,3]]
*
* 【比较运算符】
* [>] 大于
* [>=] 大于等于
* [<] 小于
* [<=] 小于等于
* [!] 不等于、IS NOT NULL、NOT IN
* [~] LIKE 模糊匹配,普通字符串会自动转成 %关键词%
* [!~] NOT LIKE
* [<>] BETWEEN,在范围内
* [><] NOT BETWEEN,不在范围内
* [REGEXP] 正则匹配
* [in] IN 查询
* [not in] NOT IN 查询
*
* 【比较运算符示例】
* ['user_id[>]' => 100,'age[>=]' => 18]
* ['title[~]' => 'WordPress']
* ['title[~]' => ['AND' => ['WordPress','Plugin']]]
* ['created_at[<>]' => ['2026-01-01','2026-12-31']]
* ['created_at[><]' => ['2026-01-01','2026-12-31']]
* ['name[REGEXP]' => '^wu_']
*
* 【字段和字段比较】
* 使用数字索引写法,右侧会被当作字段名而不是字段值。
* ['created_at[>]updated_at']
* ['user_id[=]author_id']
*
* 【AND / OR 条件】
* 默认为 AND 条件:['user_id[>]' => 100,'age[>]' => 18]
* 指定 OR 条件:['OR'=>['status' => 'active','status[!]' => 'pending']]
* 同级需要多个 AND/OR 分组时,可以加注释后缀:['OR #status'=>[...],'OR #role'=>[...]]
*
* 【ORDER / GROUP / HAVING / LIMIT】
* ORDER:['ORDER' => ['id' => 'DESC','name' => 'ASC']]
* ORDER 指定排序值:['ORDER' => ['status' => ['publish','draft']]]
* GROUP:['GROUP' => 'user_id'] 或 ['GROUP' => ['user_id','status']]
* HAVING:['HAVING' => ['total[>]' => 10]]
* LIMIT:['LIMIT' => 10] 或 ['LIMIT' => [20,10]],数组表示 OFFSET 20 LIMIT 10
*
* 【MATCH 全文检索】
* ['MATCH' => ['columns' => ['title','content'],'keyword' => 'wordpress','mode' => 'boolean']]
* mode 可选:natural、natural+query、boolean、query。
*
* 【注意事项】
* 该类只处理单表查询,不支持 join 联表。
* 所有字段名会被反引号包裹,所有字段值会通过 $wpdb->prepare() 处理。
* 不支持任意 raw SQL;复杂表达式建议在子类中单独写专用方法。
*
* 【可用方法】
* get($columns, $where = []) 获取一行数据,单字段查询时直接返回字段值。
* select($columns = '*', $where = []) 获取多行数据。
* insert($datas) 插入单条或多条数据,返回插入成功的行数。
* update($data, $where) 更新数据,必须传入有效 WHERE 条件。
* delete($where) 删除数据,必须传入有效 WHERE 条件。
* has($where = []) 检查是否存在匹配数据。
* rand($columns = '*', $where = []) 随机获取数据。
* count($where = [], $column = '*') 统计行数。
* max($column, $where = []) 获取字段最大值。
* min($column, $where = []) 获取字段最小值。
* avg($column, $where = []) 获取字段平均值。
* sum($column, $where = []) 获取字段求和值。
* id() 获取最后插入的自增 ID。
* last() 获取最后执行的 SQL。
* log() 返回当前实例执行过的 SQL 列表。
* */
abstract class WU_DB_Table {
// WordPress 数据库对象
protected $wpdb;
// 当前模型对应的完整数据表名
protected $table_name = '';
// 最后一次执行的 SQL
protected $last_sql = '';
// 当前实例执行过的 SQL 列表
protected $logs = [];
public function __construct() {
global $wpdb;
$this->wpdb = $wpdb;
$config = array_merge([
# 数据表名,不带 $wpdb->prefix
'table_name' => '',
], $this->config());
if (str_starts_with($config['table_name'], $wpdb->prefix)) $this->table_name = $config['table_name'];
else $this->table_name = $wpdb->prefix . $config['table_name'];
}
// 子类重写这里配置表
protected function config() {
return [];
}
/***
* 从表中返回一行数据。注意:这个方法只能获取一条数据。
* @param $columns string|array 返回的字段列('*'为全部字段)
* @param $where array WHERE 条件
* @return string|array
* */
function get($columns, $where = []) {
// get 只取一条数据,强制覆盖 LIMIT
$where = is_array($where) ? $where : [];
$where['LIMIT'] = 1;
// 构建查询字段和条件
$fields = $this->build_columns($columns);
[
$where_sql,
$where_values,
] = $this->build_where($where);
// 执行查询并记录 SQL
$sql = "SELECT {$fields} FROM {$this->build_identifier($this->table_name)} {$where_sql}";
$row = $this->wpdb->get_row($this->query_sql($sql, $where_values), ARRAY_A);
if (empty($row)) return [];
// 单字段查询时直接返回字段值,方便快速取值
$column = is_string($columns) ? trim($columns) : '';
if ($column !== '*' && strpos($column, ',') === false && array_key_exists($column, $row)) {
return $row[$column];
}
return $row;
}
/***
* 数据库查询
* @param $columns string|array 返回的字段列
* @param $where array WHERE 条件
* @return array
* */
function select($columns = '*', $where = []) {
// 构建查询字段和条件
$fields = $this->build_columns($columns);
[
$where_sql,
$where_values,
] = $this->build_where($where);
// 执行查询并返回二维数组
$sql = "SELECT {$fields} FROM {$this->build_identifier($this->table_name)} {$where_sql}";
return $this->wpdb->get_results($this->query_sql($sql, $where_values), ARRAY_A);
}
/**
* 插入数据
* @param $datas array 要插入的数据
* 单条数据:['name'=>'xx']
* 多条数据:[['name'=>'x1'],['name'=>'x2']]
* @return number 插入的行数
* */
function insert($datas) {
if (empty($datas) || !is_array($datas)) return 0;
// 兼容单条数据和多条数据,两种写法统一转成二维数组处理
$rows = isset($datas[0]) && is_array($datas[0]) ? $datas : [$datas];
$count = 0;
foreach ($rows as $row) {
if (empty($row) || !is_array($row)) continue;
// 只保留合法字段,避免字段名参与 SQL 注入
$data = [];
foreach ($row as $field => $value) {
if (!$this->is_identifier($field)) continue;
$data[$field] = $value;
}
if (empty($data)) continue;
// 交给 $wpdb->insert 处理插入和字段值转义
$result = $this->wpdb->insert($this->table_name, $data);
$this->record_sql($this->wpdb->last_query ?? '');
if ($result !== false) $count++;
}
return $count;
}
/**
* 更新数据
* @param $data array 要更新的数据
* @param $where array 更新条件
* @return number 受影响的行数
* */
function update($data, $where) {
if (empty($data) || !is_array($data) || empty($where)) return 0;
if (!$this->has_filter_condition($where)) return 0;
// 构建 SET 字段和值
$sets = [];
$values = [];
foreach ($data as $field => $value) {
if (!$this->is_identifier($field)) continue;
if ($value === null) {
$sets[] = $this->build_identifier($field) . ' = NULL';
continue;
}
$sets[] = $this->build_identifier($field) . ' = %s';
$values[] = $value;
}
// 构建 WHERE 条件,更新必须带有效条件
[
$where_sql,
$where_values,
] = $this->build_where($this->filter_write_where($where));
if (empty($sets) || empty($where_sql)) return 0;
// 执行更新并返回受影响行数
$values = array_merge($values, $where_values);
$sql = "UPDATE {$this->build_identifier($this->table_name)} SET " . implode(', ', $sets) . " {$where_sql}";
$result = $this->wpdb->query($this->query_sql($sql, $values));
return $result === false ? 0 : (int)$result;
}
/**
* 删除数据:出于安全考虑,$where 参数是必需的。
* @param $where array
* @return number 受影响的行数
* */
function delete($where) {
if (empty($where)) return 0;
if (!$this->has_filter_condition($where)) return 0;
// 构建 WHERE 条件,删除必须带有效条件
[
$where_sql,
$where_values,
] = $this->build_where($this->filter_write_where($where));
if (empty($where_sql)) return 0;
// 执行删除并返回受影响行数
$sql = "DELETE FROM {$this->build_identifier($this->table_name)} {$where_sql}";
$result = $this->wpdb->query($this->query_sql($sql, $where_values));
return $result === false ? 0 : (int)$result;
}
/**
* 检查数据是否存在。
*
* @param $where array WHERE 条件
* @return bool 是否存在匹配数据
*/
function has($where = []) {
// 构建 EXISTS 子查询条件
[
$where_sql,
$where_values,
] = $this->build_where($where);
// EXISTS 只需要判断是否有数据,不取具体字段
$sql = "SELECT EXISTS(SELECT 1 FROM {$this->build_identifier($this->table_name)} {$where_sql})";
$result = $this->wpdb->get_var($this->query_sql($sql, $where_values));
return $result === '1' || $result === 1 || $result === true;
}
/**
* 随机获取数据。
*
* @param $columns string|array 返回的字段列
* @param $where array WHERE 条件
* @return array 随机排序后的数据列表
*/
function rand($columns = '*', $where = []) {
// 构建查询字段和随机排序条件
$fields = $this->build_columns($columns);
[
$where_sql,
$where_values,
] = $this->build_where($where, true);
// 使用 MySQL RAND() 随机排序
$sql = "SELECT {$fields} FROM {$this->build_identifier($this->table_name)} {$where_sql}";
return $this->wpdb->get_results($this->query_sql($sql, $where_values), ARRAY_A);
}
/**
* 获取数据表中的行数。
*
* @param $where array WHERE 条件
* @param $column string 统计字段,默认 *
* @return number 行的数量
* */
function count($where = [], $column = '*') {
// COUNT 返回整数,空结果按 0 处理
return (int)$this->aggregate('COUNT', $column, $where);
}
/**
* 获取指定字段的最大值。
*
* @param $column string 统计字段
* @param $where array WHERE 条件
* @return string|null 最大值
*/
function max($column, $where = []) {
// 调用通用聚合方法执行 MAX
return $this->aggregate('MAX', $column, $where);
}
/**
* 获取指定字段的最小值。
*
* @param $column string 统计字段
* @param $where array WHERE 条件
* @return string|null 最小值
*/
function min($column, $where = []) {
// 调用通用聚合方法执行 MIN
return $this->aggregate('MIN', $column, $where);
}
/**
* 获取指定字段的平均值。
*
* @param $column string 统计字段
* @param $where array WHERE 条件
* @return string|null 平均值
*/
function avg($column, $where = []) {
// 调用通用聚合方法执行 AVG
return $this->aggregate('AVG', $column, $where);
}
/**
* 获取指定字段的求和值。
*
* @param $column string 统计字段
* @param $where array WHERE 条件
* @return string|null 求和值
*/
function sum($column, $where = []) {
// 调用通用聚合方法执行 SUM
return $this->aggregate('SUM', $column, $where);
}
/**
* 获取最后插入的 ID。
*
* @return number 最后插入的自增 ID
*/
function id() {
// $wpdb 会在 insert 后记录 insert_id
return (int)($this->wpdb->insert_id ?? 0);
}
/**
* 获取最后执行的 SQL。
*
* @return string 最后一次执行的 SQL
*/
function last() {
// 返回当前实例记录的最后一条 SQL
return $this->last_sql;
}
/**
* 返回所有执行的查询。
*
* @return array 当前实例执行过的 SQL 列表
*/
function log() {
// 返回 SQL 日志数组,方便调试
return $this->logs;
}
/**
* 执行聚合查询。
*
* @param $type string 聚合函数名称
* @param $column string 聚合字段
* @param $where array WHERE 条件
* @return string|null 聚合结果
*/
protected function aggregate($type, $column = '*', $where = []) {
// 聚合函数只允许固定白名单
$type = strtoupper($type);
if (!in_array($type, [
'COUNT',
'MAX',
'MIN',
'AVG',
'SUM',
], true)) return null;
// 构建聚合字段和 WHERE 条件
$field = $this->build_aggregate_column($column);
if ($field === '') return null;
[
$where_sql,
$where_values,
] = $this->build_where($where);
// 执行聚合查询
$sql = "SELECT {$type}({$field}) FROM {$this->build_identifier($this->table_name)} {$where_sql}";
$result = $this->wpdb->get_var($this->query_sql($sql, $where_values));
return $result === null ? null : (string)$result;
}
/**
* 构建查询字段列表
* 只允许普通字段名,避免字段名参与 SQL 注入;需要复杂表达式时建议子类单独实现。
*
* @param string|array $columns 返回字段
* @return string
*/
protected function build_columns($columns) {
if ($columns === '*' || $columns === ['*']) return '*';
// 字符串字段支持逗号分隔
if (is_string($columns)) $columns = array_map('trim', explode(',', $columns));
if (!is_array($columns)) return '*';
// 过滤并转义字段名
$fields = [];
foreach ($columns as $column) {
if (!$this->is_identifier($column)) continue;
$fields[] = $this->build_identifier($column);
}
return empty($fields) ? '*' : implode(', ', $fields);
}
/**
* 构建 WHERE 条件
* 支持 Medoo 风格的字段运算符、AND/OR 嵌套和 ORDER/GROUP/HAVING/LIMIT。
*
* @param array $where WHERE 条件
* @param bool $random_order 是否强制随机排序
* @return array [SQL片段, prepare参数]
*/
protected function build_where($where, $random_order = false) {
if (empty($where) || !is_array($where)) return [
$random_order ? 'ORDER BY RAND()' : '',
[],
];
$values = [];
$where_parts = [];
$conditions = [];
// 分离普通条件和特殊子句
foreach ($where as $field => $value) {
if (in_array($field, [
'GROUP',
'HAVING',
'ORDER',
'LIMIT',
'MATCH',
], true)) continue;
$conditions[$field] = $value;
}
// 构建普通 WHERE 条件
if (!empty($conditions)) {
$condition_sql = $this->build_conditions($conditions, $values, 'AND');
$where_parts[] = $condition_sql === '' ? '0=1' : $condition_sql;
}
// 构建 MATCH 全文检索条件
if (isset($where['MATCH'])) {
$match_sql = $this->build_match($where['MATCH'], $values);
if ($match_sql !== '') $where_parts[] = $match_sql;
}
$clause = empty($where_parts) ? '' : 'WHERE ' . implode(' AND ', $where_parts);
// 构建 GROUP BY
if (isset($where['GROUP'])) {
$group_sql = $this->build_group($where['GROUP']);
if ($group_sql !== '') $clause .= ($clause === '' ? '' : ' ') . $group_sql;
}
// 构建 HAVING
if (isset($where['HAVING'])) {
$having_sql = $this->build_having($where['HAVING'], $values);
if ($having_sql !== '') $clause .= ($clause === '' ? '' : ' ') . $having_sql;
}
// 构建 ORDER BY,随机查询会强制覆盖原 ORDER
if ($random_order) {
$clause .= ($clause === '' ? '' : ' ') . 'ORDER BY RAND()';
} elseif (isset($where['ORDER'])) {
$order_sql = $this->build_order($where['ORDER'], $values);
if ($order_sql !== '') $clause .= ($clause === '' ? '' : ' ') . $order_sql;
}
// 构建 LIMIT
if (isset($where['LIMIT'])) {
$limit_sql = $this->build_limit($where['LIMIT']);
if ($limit_sql !== '') $clause .= ($clause === '' ? '' : ' ') . $limit_sql;
}
return [
$clause,
$values,
];
}
/**
* 判断写操作是否包含真实过滤条件。
*
* @param array $where WHERE 条件
* @return bool
*/
protected function has_filter_condition($where) {
if (empty($where) || !is_array($where)) return false;
// MATCH 本身会生成 WHERE 条件
if (isset($where['MATCH'])) return true;
// 除特殊子句外,只要有普通条件就允许继续构建
foreach ($where as $field => $value) {
if (!in_array($field, [
'GROUP',
'HAVING',
'ORDER',
'LIMIT',
'MATCH',
], true)) return true;
}
return false;
}
/**
* 过滤写操作不支持的查询子句。
*
* @param array $where WHERE 条件
* @return array
*/
protected function filter_write_where($where) {
if (!is_array($where)) return [];
// UPDATE / DELETE 不处理 GROUP 和 HAVING
unset($where['GROUP'], $where['HAVING']);
return $where;
}
/**
* 构建普通条件片段
* 支持 AND/OR 嵌套、字段运算符和字段对字段比较。
*
* @param array $conditions 条件数组
* @param array $values prepare 参数
* @param string $connector 条件连接符
* @return string
*/
protected function build_conditions($conditions, &$values, $connector = 'AND') {
$stack = [];
foreach ($conditions as $key => $value) {
// 处理 AND / OR 嵌套条件
if (is_array($value) && preg_match('/^(AND|OR)(\s+#.*)?$/i', (string)$key, $relation_match)) {
$relation = strtoupper($relation_match[1]);
$nested = $this->build_conditions($value, $values, $relation);
$stack[] = $nested === '' ? '(0=1)' : "({$nested})";
continue;
}
// 处理字段和字段比较,如 ['created_at[>]updated_at']
if (is_int($key)) {
$field_compare = $this->build_field_compare($value);
if ($field_compare !== '') $stack[] = $field_compare;
continue;
}
// 处理普通字段条件
[
$field,
$operator,
] = $this->parse_condition_key($key);
if ($field === '') continue;
$stack[] = $this->build_condition($field, $operator, $value, $values);
}
return implode(" {$connector} ", array_filter($stack));
}
/**
* 构建单个字段条件
* 根据字段运算符生成对应 SQL 片段。
*
* @param string $field 字段名
* @param string $operator 运算符
* @param mixed $value 字段值
* @param array $values prepare 参数
* @return string
*/
protected function build_condition($field, $operator, $value, &$values) {
$field_sql = $this->build_identifier($field);
$operator = strtolower(trim($operator ?: '='));
// 处理等于、IS NULL 和 IN
if ($operator === '=' || $operator === 'in') {
if ($value === null) return "{$field_sql} IS NULL";
if (is_array($value)) return $this->build_in($field_sql, $value, $values);
return "{$field_sql} = " . $this->add_value($value, $values);
}
// 处理不等于、IS NOT NULL 和 NOT IN
if ($operator === '!' || $operator === '!=' || $operator === 'not in') {
if ($value === null) return "{$field_sql} IS NOT NULL";
if (is_array($value)) return $this->build_in($field_sql, $value, $values, true);
return "{$field_sql} != " . $this->add_value($value, $values);
}
// 处理大小比较
if (in_array($operator, [
'>',
'>=',
'<',
'<=',
], true)) {
return "{$field_sql} {$operator} " . $this->add_value($value, $values);
}
// 处理 LIKE 和 NOT LIKE
if ($operator === '~' || $operator === '!~') {
return $this->build_like($field_sql, $value, $values, $operator === '!~');
}
// 处理 BETWEEN 和 NOT BETWEEN
if ($operator === '<>' || $operator === '><') {
return $this->build_between($field_sql, $value, $values, $operator === '><');
}
// 处理 REGEXP
if ($operator === 'regexp') {
return "{$field_sql} REGEXP " . $this->add_value($value, $values);
}
return '0=1';
}
/**
* 构建 IN / NOT IN 条件。
*
* @param string $field_sql 已转义字段名
* @param array $items 条件值
* @param array $values prepare 参数
* @param bool $not 是否 NOT IN
* @return string
*/
protected function build_in($field_sql, $items, &$values, $not = false) {
// 空 IN 必定无结果,空 NOT IN 不限制结果
if (empty($items)) return $not ? '1=1' : '0=1';
$placeholders = [];
foreach ($items as $item) $placeholders[] = $this->add_value($item, $values);
return $field_sql . ($not ? ' NOT' : '') . ' IN (' . implode(', ', $placeholders) . ')';
}
/**
* 构建 LIKE / NOT LIKE 条件。
*
* @param string $field_sql 已转义字段名
* @param string|array $value LIKE 条件
* @param array $values prepare 参数
* @param bool $not 是否 NOT LIKE
* @return string
*/
protected function build_like($field_sql, $value, &$values, $not = false) {
// LIKE 支持字符串或数组,数组默认使用 OR 连接
if (!is_array($value)) $value = [$value];
$connector = 'OR';
if (isset($value['AND']) && is_array($value['AND'])) {
$connector = 'AND';
$value = $value['AND'];
} elseif (isset($value['OR']) && is_array($value['OR'])) {
$connector = 'OR';
$value = $value['OR'];
}
$likes = [];
foreach ($value as $item) {
$likes[] = $field_sql . ($not ? ' NOT' : '') . ' LIKE ' . $this->add_value($this->build_like_value($item), $values);
}
return empty($likes) ? '0=1' : '(' . implode(" {$connector} ", $likes) . ')';
}
/**
* 构建 BETWEEN / NOT BETWEEN 条件。
*
* @param string $field_sql 已转义字段名
* @param array $value 范围值
* @param array $values prepare 参数
* @param bool $not 是否 NOT BETWEEN
* @return string
*/
protected function build_between($field_sql, $value, &$values, $not = false) {
// BETWEEN 必须传入两个范围值
if (!is_array($value) || count($value) < 2) return '0=1';
$left = $this->add_value($value[0], $values);
$right = $this->add_value($value[1], $values);
return "({$field_sql}" . ($not ? ' NOT' : '') . " BETWEEN {$left} AND {$right})";
}
/**
* 构建字段和字段比较条件。
*
* @param string $condition 条件字符串
* @return string
*/
protected function build_field_compare($condition) {
if (!is_string($condition)) return '';
// 匹配 left[operator]right 格式
if (!preg_match('/^([^\[\]]+)\[([^\]]+)\]([^\[\]]+)$/u', $condition, $match)) return '';
$left = trim($match[1]);
$operator = trim($match[2]);
$right = trim($match[3]);
if (!$this->is_identifier($left) || !$this->is_identifier($right)) return '';
if ($operator === '!') $operator = '!=';
if (!in_array($operator, [
'>',
'>=',
'<',
'<=',
'=',
'!=',
], true)) return '';
return $this->build_identifier($left) . " {$operator} " . $this->build_identifier($right);
}
/**
* 构建 MATCH 全文检索条件。
*
* @param array $match MATCH 条件
* @param array $values prepare 参数
* @return string
*/
protected function build_match($match, &$values) {
if (!is_array($match) || empty($match['columns']) || !isset($match['keyword'])) return '';
// 过滤全文检索字段
$columns = [];
foreach ((array)$match['columns'] as $column) {
if ($this->is_identifier($column)) $columns[] = $this->build_identifier($column);
}
if (empty($columns)) return '';
// 处理全文检索模式
$mode = '';
$options = [
'natural' => ' IN NATURAL LANGUAGE MODE',
'natural+query' => ' IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION',
'boolean' => ' IN BOOLEAN MODE',
'query' => ' WITH QUERY EXPANSION',
];
if (!empty($match['mode']) && isset($options[$match['mode']])) $mode = $options[$match['mode']];
return 'MATCH (' . implode(', ', $columns) . ') AGAINST (' . $this->add_value($match['keyword'], $values) . $mode . ')';
}
/**
* 构建 GROUP BY 子句。
*
* @param string|array $group 分组字段
* @return string
*/
protected function build_group($group) {
$groups = is_array($group) ? $group : [$group];
$stack = [];
// 过滤并转义分组字段
foreach ($groups as $column) {
if ($this->is_identifier($column)) $stack[] = $this->build_identifier($column);
}
return empty($stack) ? '' : 'GROUP BY ' . implode(', ', $stack);
}
/**
* 构建 HAVING 子句。
*
* @param array $having HAVING 条件
* @param array $values prepare 参数
* @return string
*/
protected function build_having($having, &$values) {
if (empty($having) || !is_array($having)) return '';
// HAVING 复用普通条件语法
$condition_sql = $this->build_conditions($having, $values, 'AND');
return $condition_sql === '' ? '' : 'HAVING ' . $condition_sql;
}
/**
* 构建 ORDER BY 子句。
*
* @param string|array $order 排序配置
* @param array $values prepare 参数
* @return string
*/
protected function build_order($order, &$values) {
// 字符串排序只支持单个字段名
if (is_string($order)) {
return $this->is_identifier($order) ? 'ORDER BY ' . $this->build_identifier($order) : '';
}
if (!is_array($order)) return '';
$stack = [];
foreach ($order as $column => $value) {
// 数字索引写法:['id','name']
if (is_int($column)) {
if ($this->is_identifier($value)) $stack[] = $this->build_identifier($value);
continue;
}
if (!$this->is_identifier($column)) continue;
// 数组写法会转成 FIELD(column, ...)
if (is_array($value)) {
$field_values = [];
foreach ($value as $item) $field_values[] = $this->add_value($item, $values);
if (!empty($field_values)) $stack[] = 'FIELD(' . $this->build_identifier($column) . ', ' . implode(', ', $field_values) . ')';
continue;
}
// ASC / DESC 排序
$direction = strtoupper((string)$value);
if (in_array($direction, [
'ASC',
'DESC',
], true)) {
$stack[] = $this->build_identifier($column) . ' ' . $direction;
}
}
return empty($stack) ? '' : 'ORDER BY ' . implode(', ', $stack);
}
/**
* 构建 LIMIT 子句。
*
* @param number|array $limit 数量或 [offset, limit]
* @return string
*/
protected function build_limit($limit) {
// LIMIT 数字写法
if (is_numeric($limit)) {
return 'LIMIT ' . max(0, (int)$limit);
}
// LIMIT 数组写法:[offset, limit]
if (is_array($limit) && isset($limit[0], $limit[1]) && is_numeric($limit[0]) && is_numeric($limit[1])) {
$offset = max(0, (int)$limit[0]);
$count = max(0, (int)$limit[1]);
return "LIMIT {$count} OFFSET {$offset}";
}
return '';
}
/**
* 解析字段条件 key。
*
* @param string $key 字段条件 key
* @return array [字段名, 运算符]
*/
protected function parse_condition_key($key) {
if (!is_string($key)) return [
'',
'',
];
// 匹配 field[operator] 格式
if (!preg_match('/^([^\[\]]+)(?:\[([^\]]*)\])?$/u', $key, $match)) return [
'',
'',
];
$field = trim($match[1]);
$operator = isset($match[2]) && $match[2] !== '' ? trim($match[2]) : '=';
if (!$this->is_identifier($field)) return [
'',
'',
];
return [
$field,
$operator,
];
}
/**
* 构建聚合字段。
*
* @param string $column 聚合字段
* @return string
*/
protected function build_aggregate_column($column) {
if ($column === '*' || $column === null || $column === '') return '*';
return $this->is_identifier($column) ? $this->build_identifier($column) : '';
}
/**
* 构建 LIKE 参数值。
*
* @param mixed $value 原始值
* @return string
*/
protected function build_like_value($value) {
$value = (string)$value;
// 已经手动传入 % 或 _ 时,认为调用方正在控制 LIKE 模式
if (strpos($value, '%') !== false || strpos($value, '_') !== false) return $value;
$value = method_exists($this->wpdb, 'esc_like') ? $this->wpdb->esc_like($value) : addcslashes($value, '_%\\');
return '%' . $value . '%';
}
// 添加一个 prepare 参数并返回占位符
protected function add_value($value, &$values) {
$values[] = $value;
return '%s';
}
// 判断字段名或表名是否为普通 SQL 标识符
protected function is_identifier($name) {
if (!is_string($name) || $name === '') return false;
$parts = explode('.', $name);
foreach ($parts as $part) {
if (!preg_match('/^[\p{L}_][\p{L}\p{N}@$#\-_]*$/u', $part)) return false;
}
return true;
}
// 给字段名或表名加反引号,避免和 SQL 保留字冲突
protected function build_identifier($name) {
$parts = explode('.', $name);
$parts = array_map(function ($part) {
return '`' . str_replace('`', '``', $part) . '`';
}, $parts);
return implode('.', $parts);
}
// 准备并记录 SQL,所有直接执行的 SQL 都走这里
protected function query_sql($sql, $values = []) {
$sql = $this->prepare_sql($sql, $values);
$this->record_sql($sql);
return $sql;
}
// 统一处理 prepare 参数,兼容没有参数的 SQL
protected function prepare_sql($sql, $values = []) {
if (empty($values)) return $sql;
return call_user_func_array([
$this->wpdb,
'prepare',
], array_merge([$sql], $values));
}
// 记录最后一次 SQL 和全部 SQL 日志
protected function record_sql($sql) {
if (empty($sql)) return;
$this->last_sql = $sql;
$this->logs[] = $sql;
}
}