Initial commit

This commit is contained in:
18338781771
2025-05-14 13:19:23 +08:00
commit 1ab607f65a
313 changed files with 27595 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace app\mzl\common\utils;
class SearchHelper
{
public static function parseFiltersAndRelations(array $params, array $mapping): array
{
$filters = [];
$relations = [];
$hasRelations = [];
// 解析过滤条件和关联字段
foreach ($mapping as $paramKey => $definition) {
$field = $definition['field'];
$operator = $definition['operator'];
$value = $params[$paramKey] ?? null;
if (self::isValidValue($value, $operator)) {
$filters[] = compact('field', 'operator', 'value');
if (str_contains($field, '.')) {
$relations[] = self::extractRelationPath($field);
}
}
}
// 解析 whereHas 关联字段(如 profile.age)
foreach ($params as $key => $value) {
if (str_contains($key, '.')) {
$relation = explode('.', $key)[0]; // 获取关联名
$hasRelations[$relation] = function ($query) use ($key, $value) {
$query->where($key, '=', $value);
};
}
}
return [
'filters' => $filters,
'relations' => array_unique($relations),
'hasRelations' => $hasRelations,
];
}
private static function isValidValue(mixed $value, string $operator): bool
{
$operator = strtolower($operator);
if (in_array($operator, ['in', 'not_in', 'between'])) {
return is_array($value) && !empty($value);
}
if (in_array($operator, ['null', 'not_null'])) {
return true;
}
return $value !== '' && $value !== null;
}
private static function extractRelationPath(string $field): string
{
$parts = explode('.', $field);
array_pop($parts);
return implode('.', $parts);
}
}
+584
View File
@@ -0,0 +1,584 @@
<?php
namespace app\mzl\common\utils;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
/**
* 通用查询构建器 SimpleQueryBuilder
*/
class SimpleQueryBuilder
{
protected ?Model $model = null;
protected ?Builder $query = null;
protected array $filters = [];
protected array $sorts = [];
protected array $fields = [];
protected array $relations = [];
protected array $hasRelations = [];
protected array $pagination = [
'page' => 1,
'per_page' => 10,
];
/** @var string[] 要执行 withCount 的关联 */
protected array $counts = [];
public array $allowedFields = [];
protected ?\Closure $customQueryCallback = null;
public function __construct(?Model $model = null)
{
if ($model) {
$this->setModel($model);
}
}
/**
* 设置模型
*/
public function setModel(Model $model): self
{
$this->model = $model;
$this->query = $model->newQuery();
return $this;
}
/**
* 批量应用查询配置
*
* @param array $config
* @return $this
*/
public function applyQueryConfig(array $config): self
{
foreach ($config['filters'] ?? [] as $filter) {
$this->addFilter($filter['field'], $filter['operator'], $filter['value']);
}
foreach ($config['sorts'] ?? [] as $sort) {
$this->addSort($sort['field'], $sort['direction'] ?? 'asc');
}
if (!empty($config['fields'])) {
$this->selectFields($config['fields']);
}
foreach ($config['relations'] ?? [] as $relation => $fields) {
$this->withRelation($relation, $fields);
}
foreach ($config['hasRelations'] ?? [] as $relation => $callback) {
if ($callback instanceof \Closure) {
$this->addHasRelation($relation, $callback);
}
}
// 新增:处理 counts
foreach ($config['counts'] ?? [] as $relation) {
$this->addCount($relation);
}
if (!empty($config['pagination'])) {
$this->setPagination(
$config['pagination']['page'] ?? 1,
$config['pagination']['per_page'] ?? 15
);
}
if (!empty($config['custom']) && $config['custom'] instanceof \Closure) {
$this->withCustomQuery($config['custom']);
}
return $this;
}
/**
* 添加单个过滤条件
*/
public function addFilter(string $field, string $operator, mixed $value): self
{
$this->filters[] = compact('field', 'operator', 'value');
return $this;
}
/**
* 添加排序字段
*/
public function addSort(string $field, string $direction = 'asc'): self
{
$this->sorts[$field] = strtolower($direction) === 'desc' ? 'desc' : 'asc';
return $this;
}
/**
* 设置查询字段(select),可配合 allowedFields 校验
*/
public function selectFields(array $fields): self
{
$this->fields = array_unique($fields);
return $this;
}
/**
* 加载关联(支持指定字段)
*/
public function withRelation2(string $relation, array $fields = []): self
{
$this->relations[$relation] = !empty($fields) ? $fields : '*';
return $this;
}
/**
* 加载关联(支持指定字段 或 自定义闭包)
*
* @param string $relation 关联名,支持点式多级
* @param array|\Closure $fields 字段数组 或 查询闭包
*/
public function withRelation(string $relation, $fields = []): self
{
if ($fields instanceof \Closure) {
// 自定义关联查询闭包
$this->relations[$relation] = $fields;
} else {
// 字段选择,空数组表示全部
$this->relations[$relation] = !empty($fields) ? $fields : '*';
}
return $this;
}
/**
* 添加 whereHas 条件
*/
public function addHasRelation(string $relation, \Closure $callback): self
{
$this->hasRelations[] = compact('relation', 'callback');
return $this;
}
/**
* 设置分页
*/
public function setPagination(int $page, int $perPage): self
{
$this->pagination = [
'page' => max(1, $page),
'per_page' => min(max(1, $perPage), 100),
];
return $this;
}
/**
* 注入复杂查询闭包(适合业务层扩展)
*/
public function withCustomQuery(\Closure $callback): self
{
$this->customQueryCallback = $callback;
return $this;
}
/**
* 添加关联数量统计 (withCount)
*
* @param string $relation 关联名,支持点式多层,如 "orders.items"
* @return $this
*/
public function addCount(string $relation): self
{
$this->counts[] = $relation;
return $this;
}
/**
* 构建完整查询(不执行)
*/
public function build(): Builder
{
if (!$this->query && $this->model) {
$this->query = $this->model->newQuery();
}
$this->applyFilters()
->applySorts()
->applyFields()
->applyRelations()
->applyWhereHas();
// ========== 新增:关联计数 ==========
if (!empty($this->counts)) {
// Laravel 支持 withCount(['rel1','rel2'])
$this->query->withCount($this->counts);
}
if ($this->customQueryCallback) {
call_user_func($this->customQueryCallback, $this->query);
}
return $this->query;
}
/**
* 获取集合(不分页)
*/
public function get(): Collection
{
return $this->build()->get();
}
/**
* 获取分页结果
*/
public function paginate(): LengthAwarePaginator
{
return $this->build()->paginate(
$this->pagination['per_page'],
['*'],
'page',
$this->pagination['page']
);
}
/**
* 应用过滤
*/
protected function applyFilters(): self
{
foreach ($this->filters as $filter) {
$field = $filter['field'];
$operator = $filter['operator'];
$value = $filter['value'];
if (str_contains($field, '.')) {
$segments = explode('.', $field);
$relationPath = implode('.', array_slice($segments, 0, -1));
$lastField = end($segments);
$this->query->whereHas($relationPath, function (Builder $q) use ($lastField, $operator, $value) {
$this->applyCondition($q, $lastField, $operator, $value);
});
} else {
$this->applyCondition($this->query, $field, $operator, $value);
}
}
return $this;
}
/**
* 应用排序
*/
protected function applySorts(): self
{
foreach ($this->sorts as $field => $direction) {
$this->query->orderBy($field, $direction);
}
return $this;
}
/**
* 应用字段选择
*/
// protected function applyFields2(): self
// {
// if (!empty($this->fields)) {
// // 排除关联字段(如 class.teacher.realname)
// $fields = array_filter($this->fields, fn($field) => !str_contains($field, '.'));
// // 白名单过滤
// $fields = !empty($this->allowedFields)
// ? array_intersect($fields, $this->allowedFields)
// : $fields;
// if (empty($fields)) {
// throw new \InvalidArgumentException('字段选择无效');
// }
// $this->query->select(array_unique($fields));
// }
// return $this;
// }
/**
* 应用字段选择,支持普通列和带 alias 的 Raw 表达式
*/
protected function applyFields(): self
{
if (empty($this->fields)) {
return $this;
}
// 只保留主表字段(排除带点的关联字段)
$cols = array_filter($this->fields, fn($f) => ! str_contains($f, '.'));
// 如果设置了 allowedFields,做一次白名单交集
if (! empty($this->allowedFields)) {
$cols = array_intersect($cols, $this->allowedFields);
}
if (empty($cols)) {
throw new \InvalidArgumentException('字段选择无效');
}
$plain = [];
$raws = [];
foreach ($cols as $col) {
// 简单判断带 “ as ” 的就归到 Raw
if (preg_match('/\s+as\s+/i', $col)) {
$raws[] = $col;
} else {
$plain[] = $col;
}
}
// 先 select 普通列
if ($plain) {
$this->query->select($plain);
}
// 再 selectRaw alias
if ($raws) {
// 注意:selectRaw 接受一整个字符串
$this->query->selectRaw(implode(', ', $raws));
}
return $this;
}
/**
* 应用关联加载,支持 dot-notation 多级关联
*/
// protected function applyRelations2(): self
// {
// if (empty($this->relations)) {
// return $this;
// }
// $with = [];
// foreach ($this->relations as $path => $fields) {
// $with[$path] = function ($q) use ($fields) {
// if ($fields === '*' || empty($fields)) {
// return;
// }
// // 保证 id 在内
// $model = $q->getModel();
// $key = $model->getKeyName();
// $q->select(array_merge([$key], $fields));
// };
// }
// $this->query->with($with);
// return $this;
// }
/**
* 应用多级关联加载,支持给关联字段加 alias
*/
protected function applyRelations2(): self
{
if (empty($this->relations)) {
return $this;
}
$with = [];
foreach ($this->relations as $path => $fields) {
$with[$path] = function ($q) use ($fields) {
if ($fields === '*' || empty($fields)) {
return;
}
// 模型主键一定要带上
$model = $q->getModel();
$key = $model->getKeyName();
$plain = [];
$raws = [];
foreach ($fields as $col) {
if (preg_match('/\s+as\s+/i', $col)) {
$raws[] = $col;
} else {
$plain[] = $col;
}
}
// 确保主键至少出现一次
if (! in_array($key, $plain, true)) {
array_unshift($plain, $key);
}
// select 普通列
if ($plain) {
$q->select($plain);
}
// selectRaw 带 alias 的列
if ($raws) {
$q->selectRaw(implode(', ', $raws));
}
};
}
$this->query->with($with);
return $this;
}
/**
* 应用多级关联加载,支持字段数组或直接传入闭包
*/
protected function applyRelations(): self
{
if (empty($this->relations)) {
return $this;
}
$with = [];
foreach ($this->relations as $path => $handler) {
if ($handler instanceof \Closure) {
// 直接使用用户传入的闭包
$with[$path] = $handler;
} else {
// 按字段数组构造查询
$fields = $handler;
$with[$path] = function ($q) use ($fields) {
if ($fields === '*' || empty($fields)) {
return;
}
// 确保主键被选中
$model = $q->getModel();
$key = $model->getKeyName();
$plain = [];
$raws = [];
foreach ($fields as $col) {
if (preg_match('/\s+as\s+/i', $col)) {
$raws[] = $col;
} else {
$plain[] = $col;
}
}
if (! in_array($key, $plain, true)) {
array_unshift($plain, $key);
}
if ($plain) {
$q->select($plain);
}
if ($raws) {
$q->selectRaw(implode(', ', $raws));
}
};
}
}
$this->query->with($with);
return $this;
}
/**
* 应用 whereHas 关联过滤
*/
protected function applyWhereHas(): self
{
foreach ($this->hasRelations as $relationData) {
$this->query->whereHas($relationData['relation'], $relationData['callback']);
}
return $this;
}
/**
* 应用单个过滤条件
*/
protected function applyCondition(Builder $query, string $field, string $operator, mixed $value): void
{
switch (strtolower($operator)) {
case 'between':
$query->whereBetween($field, (array)$value);
break;
case 'like':
$query->where($field, 'LIKE', "%{$value}%");
break;
case 'in':
$query->whereIn($field, (array)$value);
break;
case 'not_in':
$query->whereNotIn($field, (array)$value);
break;
case 'null':
$query->whereNull($field);
break;
case 'not_null':
$query->whereNotNull($field);
break;
default:
$query->where($field, $operator, $value);
}
}
/**
* 根据 queryConfig 构造 fieldsMap
* - 兼容 "col as alias"
* - 自动把 counts 生成的 xxx_count 加入根字段
*
* @param array $config 完整的 applyQueryConfig 输入
* @return array [''=>[主表字段…], 'relation'=>[字段…], …]
*/
public static function buildFieldsMap(array $config): array
{
$relations = $config['relations'] ?? [];
$counts = $config['counts'] ?? [];
$rawFields = $config['fields'] ?? [];
// 1) 根层字段:拆 alias + 自动加 count + 加顶级关联名
$root = [];
foreach ($rawFields as $f) {
if (preg_match('/\s+as\s+/i', $f)) {
[, $alias] = preg_split('/\s+as\s+/i', $f);
$root[] = $alias;
} else {
$root[] = $f;
}
}
// 把 withCount 生成的字段也加进来
foreach ($counts as $rel) {
$root[] = "{$rel}_count";
}
// 顶级关联名
foreach (array_keys($relations) as $path) {
$root[] = explode('.', $path, 2)[0];
}
$fieldsMap = ['' => array_values(array_unique($root))];
// 2) 每个关联:拆 alias + 加直接子关联
foreach ($relations as $path => $rawCols) {
$arr = [];
$segments = explode('.', $path);
$depth = count($segments);
// 拆 alias
foreach ($rawCols as $col) {
if (preg_match('/\s+as\s+/i', $col)) {
[, $alias] = preg_split('/\s+as\s+/i', $col);
$arr[] = $alias;
} else {
$arr[] = $col;
}
}
// 子关联名
if ($depth > 1) {
for ($i = 1; $i < $depth; $i++) {
$parent = implode('.', array_slice($segments, 0, $i));
$child = $segments[$i];
$fieldsMap[$parent][] = $child;
}
}
$fieldsMap[$path] = array_values(array_unique($arr));
}
return $fieldsMap;
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace app\mzl\common\utils;
class SnGenerator
{
/**
* 生成唯一的用户编号
*
* @param int $length 编号长度(6 或 8)
* @return string
* @throws Exception
*/
public function generateUniqueUserId($length = 6)
{
if (!in_array($length, [6, 8])) {
throw new \Exception("长度必须是 6 或 8 位");
}
$maxAttempts = 100; // 最大尝试次数,避免无限循环
$attempts = 0;
do {
$userId = $this->generateRandomNumber($length);
$attempts++;
if ($attempts > $maxAttempts) {
throw new \Exception("无法生成唯一的用户编号,请重试");
}
} while ($this->isUserIdExists($userId));
return $userId;
}
/**
* 生成随机数字
*
* @param int $length 数字长度
* @return string
*/
private function generateRandomNumber($length)
{
$min = pow(10, $length - 1); // 最小值,如 6 位是 100000
$max = pow(10, $length) - 1; // 最大值,如 6 位是 999999
return (string) mt_rand($min, $max);
}
/**
* 检查用户编号是否已存在
*
* @param string $userId
* @return bool
*/
private function isUserIdExists($userId)
{
$userIds = \support\Db::table('hs_users')->pluck('user_sn')->toArray();
return in_array($userId, $userIds);
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace app\mzl\common\utils;
use support\Db;
class TableInspector
{
/**
* 获取某个表的所有字段信息
*/
public static function getColumns(string $table, ?string $database = null): array
{
$database = $database ?: Db::connection()->getDatabaseName();
$columns = Db::select("
SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE,
COLUMN_DEFAULT, COLUMN_COMMENT, EXTRA
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
ORDER BY ORDINAL_POSITION
", [$database, $table]);
return array_map(function ($col) {
return [
'name' => $col->COLUMN_NAME,
'type' => $col->DATA_TYPE,
'column_type' => $col->COLUMN_TYPE,
'nullable' => $col->IS_NULLABLE === 'YES',
'default' => $col->COLUMN_DEFAULT,
'comment' => $col->COLUMN_COMMENT,
'extra' => $col->EXTRA,
];
}, $columns);
}
/**
* 获取字段名列表
*/
public static function getColumnNames(string $table, ?string $database = null): array
{
return array_column(self::getColumns($table, $database), 'name');
}
/**
* 获取字段注释 map:字段名 => 注释
*/
public static function getColumnComments(string $table, ?string $database = null): array
{
$columns = self::getColumns($table, $database);
$map = [];
foreach ($columns as $col) {
$map[$col['name']] = $col['comment'];
}
return $map;
}
}