431 lines
13 KiB
PHP
431 lines
13 KiB
PHP
<?php
|
||
|
||
namespace app\common\utils;
|
||
|
||
use Illuminate\Database\Eloquent\Builder;
|
||
use Illuminate\Database\Eloquent\Model;
|
||
use plugin\admin\app\common\Auth;
|
||
use support\exception\BusinessException;
|
||
|
||
/**
|
||
* 构建查询工具类
|
||
* | 方法 | 说明 |
|
||
* | ------------------------------- | --------------------- |
|
||
* | `buildQuery($config)` | 构建动态查询(关联加载/字段/过滤/排序) |
|
||
* | `paginate($q,$per,$page)` | 分页返回 Paginator |
|
||
* | `getAll($q)` | 不分页直接 `get()` |
|
||
* | `findByPk($model,$id)` | 根据主键查询单条,找不到抛 404 |
|
||
* | `findOneBy($model,$where)` | 根据任意条件查询单条,找不到抛 404 |
|
||
* | `deleteByPk($model,$ids)` | 根据主键删除(可批量),返回删除条数 |
|
||
* | `deleteBy($model,$where)` | 根据任意条件删除,返回删除条数 |
|
||
* | `updateByPk($model,$id,$data)` | 根据主键更新,找不到抛 404 |
|
||
* | `updateBy($model,$where,$data)` | 根据任意条件更新,返回影响行数 |
|
||
*/
|
||
|
||
class CrudUtil
|
||
{
|
||
|
||
/**
|
||
* 构建查询,支持:
|
||
* - 本表字段过滤 where
|
||
* - 关联加载 with
|
||
* - 关联表字段选择 withFields
|
||
* - 关联表字段过滤 where with dot-notation
|
||
* - 排序 order
|
||
* - 本表字段选择 fields
|
||
*
|
||
* $config = [
|
||
* 'model' => User::class,
|
||
* 'where' => [
|
||
* 'status' => 1,
|
||
* 'name' => ['like','%张%'],
|
||
* 'roles.name' => '管理员', // 关联表字段过滤
|
||
* 'posts.created_at'=> ['>','2025-01-01'],
|
||
* ],
|
||
* 'order' => ['id'=>'desc'],
|
||
* 'fields' => ['id','name','email'],
|
||
* 'with' => ['roles','posts'],
|
||
* 'withFields' => [
|
||
* 'roles' => ['id','name'], // 关联表字段选择
|
||
* 'posts' => ['id','title','user_id'],
|
||
* ],
|
||
* ];
|
||
*/
|
||
public static function buildQuery(array $config): Builder
|
||
{
|
||
/** @var Model $model */
|
||
$model = new $config['model'];
|
||
$q = $model->newQuery();
|
||
|
||
// eager-load:支持 ['rel','rel2'] 和 ['rel'=>fields[], ...]
|
||
if (! empty($config['with'])) {
|
||
foreach ($config['with'] as $rel => $fields) {
|
||
if (is_int($rel)) {
|
||
// 普通关联
|
||
$q->with($fields);
|
||
} else {
|
||
// 带字段筛选的关联
|
||
$q->with([$rel => function ($qr) use ($fields) {
|
||
$key = $qr->getModel()->getKeyName();
|
||
if (! in_array($key, $fields, true)) {
|
||
$fields[] = $key;
|
||
}
|
||
$qr->select($fields);
|
||
}]);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 表字段
|
||
if (!empty($config['fields'])) {
|
||
$q->select($config['fields']);
|
||
}
|
||
|
||
// where
|
||
if (!empty($config['where'])) {
|
||
foreach ($config['where'] as $col => $val) {
|
||
if (strpos($col, '.') !== false) {
|
||
// 多层支持
|
||
$segments = explode('.', $col);
|
||
$leaf = array_pop($segments);
|
||
self::applyNestedWhere($q, $segments, $leaf, $val);
|
||
} else {
|
||
self::applyWhere($q, $col, $val);
|
||
}
|
||
}
|
||
}
|
||
|
||
// or where
|
||
if (!empty($config['orWhere'])) {
|
||
$q->where(function($q) use($config) {
|
||
foreach ($config['orWhere'] as $cond) {
|
||
[$col,$op,$val] = $cond;
|
||
$q->orWhere($col,$op,$val);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 排序
|
||
if (!empty($config['order'])) {
|
||
foreach ($config['order'] as $col => $dir) {
|
||
$q->orderBy($col, strtolower($dir) === 'asc' ? 'asc' : 'desc');
|
||
}
|
||
}
|
||
|
||
// 数据权限过滤
|
||
if (!empty($config['dataLimit'])) {
|
||
self::applyDataLimit($q, $config['dataLimit']);
|
||
}
|
||
|
||
return $q;
|
||
}
|
||
|
||
/**
|
||
* 分页查询
|
||
*
|
||
* @param Builder $query
|
||
* @param int $perPage
|
||
* @param int $page
|
||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||
*/
|
||
public static function paginate(Builder $q, int $perPage, int $page)
|
||
{
|
||
return $q->paginate($perPage, ['*'], 'page', $page);
|
||
}
|
||
|
||
/**
|
||
* 非分页查询
|
||
*
|
||
* @param Builder $query
|
||
* @return \Illuminate\Database\Eloquent\Collection
|
||
*/
|
||
public static function getAll(Builder $q)
|
||
{
|
||
return $q->get();
|
||
}
|
||
|
||
/**
|
||
* 执行复杂查询——仅在显式调用时生效
|
||
*
|
||
* @param string $modelClass 模型类
|
||
* @param callable $callback function(Builder $q) { ... }
|
||
* @return mixed 由 callback 返回的结果
|
||
*/
|
||
public static function executeComplex(string $modelClass, callable $cb)
|
||
{
|
||
/** @var Model $model */
|
||
$model = new $modelClass;
|
||
return $cb($model->newQuery());
|
||
}
|
||
|
||
/**
|
||
* 主键单条查询
|
||
*
|
||
* @param string $modelClass
|
||
* @param mixed $id
|
||
* @param array $with
|
||
* @return Model
|
||
* @throws BusinessException
|
||
*/
|
||
public static function findByPk(string $modelClass, $id, array $with = []): Model
|
||
{
|
||
$m = $modelClass::with($with)->find($id);
|
||
if (!$m) {
|
||
throw new BusinessException("记录 ID={$id} 不存在", 404);
|
||
}
|
||
return $m;
|
||
}
|
||
|
||
/**
|
||
* 任意条件单条查询,支持 dataLimit
|
||
*
|
||
* @param Builder $query
|
||
* @return Model
|
||
* @throws BusinessException
|
||
*/
|
||
public static function findOneBy(Builder $q): Model
|
||
{
|
||
return $q->first();
|
||
}
|
||
|
||
/**
|
||
* 创建,支持 dataLimit
|
||
*
|
||
* @param string $modelClass
|
||
* @param array $data
|
||
* @param array $dataLimit
|
||
* @return Model
|
||
* @throws BusinessException
|
||
*/
|
||
public static function create(
|
||
string $modelClass,
|
||
array $data,
|
||
array $dataLimit = []
|
||
): Model {
|
||
// dataLimit 检查/注入
|
||
if (!empty($dataLimit)) {
|
||
self::applyDataLimitOnMutate($data, $dataLimit);
|
||
}
|
||
return $modelClass::create($data);
|
||
}
|
||
|
||
/**
|
||
* 主键更新,支持 dataLimit
|
||
*
|
||
* @param string $modelClass
|
||
* @param mixed $id
|
||
* @param array $data
|
||
* @param array $dataLimit
|
||
* @return Model
|
||
* @throws BusinessException
|
||
*/
|
||
public static function updateByPk(
|
||
string $modelClass,
|
||
$id,
|
||
array $data,
|
||
array $dataLimit = []
|
||
): Model {
|
||
/** @var Model $m */
|
||
$m = $modelClass::find($id);
|
||
if (!$m) {
|
||
throw new BusinessException("记录 ID={$id} 不存在", 404);
|
||
}
|
||
// dataLimit 校验
|
||
if (!empty($dataLimit)) {
|
||
self::applyDataLimitOnMutate($m, $dataLimit, $data);
|
||
}
|
||
$m->fill($data)->save();
|
||
return $m;
|
||
}
|
||
|
||
/**
|
||
* 非主键条件更新(批量),支持 dataLimit
|
||
*
|
||
* @param Builder $query
|
||
* @param array $data
|
||
* @return int 受影响行数
|
||
*/
|
||
public static function updateBy(Builder $q, array $data): int
|
||
{
|
||
return $q->update($data);
|
||
}
|
||
|
||
/**
|
||
* 主键删除(支持批量)
|
||
*
|
||
* @param string $modelClass
|
||
* @param array $ids
|
||
* @return int 删除条数
|
||
*/
|
||
public static function deleteByPk(string $modelClass, array $ids): int
|
||
{
|
||
return $modelClass::destroy($ids);
|
||
}
|
||
|
||
/**
|
||
* 非主键条件删除,支持 dataLimit
|
||
*
|
||
* @param Builder $query
|
||
* @return int 删除条数
|
||
*/
|
||
public static function deleteBy(Builder $q): int
|
||
{
|
||
return $q->delete();
|
||
}
|
||
|
||
|
||
//=================== Internal Methods ===================//
|
||
|
||
|
||
/**
|
||
* 支持多层的 whereHas:$relations=['roles','permissions'],$leaf='name'
|
||
*/
|
||
protected static function applyNestedWhere(Builder $q, array $relations, string $leaf, $val): void
|
||
{
|
||
$rel = array_shift($relations);
|
||
$q->whereHas($rel, function (Builder $qr) use ($relations, $leaf, $val) {
|
||
if ($relations) {
|
||
// 还有更深层,继续递归
|
||
self::applyNestedWhere($qr, $relations, $leaf, $val);
|
||
} else {
|
||
// 最后一层,使用普通 applyWhere
|
||
self::applyWhere($qr, $leaf, $val);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 单字段 where 支持:
|
||
* - 多条件数组 [ [op1,val1], [op2,val2], … ]
|
||
* - 单条件 ['op', val]
|
||
* - 直接值
|
||
*
|
||
* @param Builder $q
|
||
* @param string $col
|
||
* @param mixed $val
|
||
* @return void
|
||
*/
|
||
protected static function applyWhere(Builder $q, string $col, $val): void
|
||
{
|
||
// 多条件数组
|
||
if (is_array($val) && isset($val[0]) && is_array($val[0])) {
|
||
foreach ($val as $pair) {
|
||
if (is_array($pair) && count($pair) === 2) {
|
||
self::applySingleWhere($q, $col, $pair[0], $pair[1]);
|
||
}
|
||
}
|
||
}
|
||
// 单条件
|
||
elseif (is_array($val) && count($val) === 2) {
|
||
self::applySingleWhere($q, $col, $val[0], $val[1]);
|
||
}
|
||
// 直接等值
|
||
else {
|
||
$q->where($col, $val);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行条件 where
|
||
*/
|
||
protected static function applySingleWhere(Builder $q, string $col, string $op, $v): void
|
||
{
|
||
switch (strtolower($op)) {
|
||
case 'like':
|
||
$q->where($col, 'like', $v);
|
||
break;
|
||
case 'in':
|
||
$q->whereIn($col, (array)$v);
|
||
break;
|
||
case 'not in':
|
||
$q->whereNotIn($col, (array)$v);
|
||
break;
|
||
case 'null':
|
||
$q->whereNull($col);
|
||
break;
|
||
case 'not null':
|
||
$q->whereNotNull($col);
|
||
break;
|
||
case 'between':
|
||
$q->whereBetween($col, (array)$v);
|
||
break;
|
||
default:
|
||
$q->where($col, $op, $v);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 应用数据权限过滤到查询(buildQuery 场景)
|
||
*/
|
||
protected static function applyDataLimit(Builder $q, array $dataLimit): void
|
||
{
|
||
if (Auth::isSuperAdmin()) {
|
||
return;
|
||
}
|
||
$type = $dataLimit['type'] ?? null;
|
||
$field = $dataLimit['field'] ?? 'admin_id';
|
||
|
||
if ($type === 'personal') {
|
||
$q->where($field, admin_id());
|
||
} elseif ($type === 'auth') {
|
||
$q->whereIn($field, Auth::getScopeAdminIds(true));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 应用数据权限到“创建/更新”操作
|
||
*
|
||
* - $target 可能是 array|Model 对象
|
||
* - $newData 仅在 updateByPk 场景时传入,用以校验变更归属
|
||
*/
|
||
protected static function applyDataLimitOnMutate(&$target, array $dataLimit, array $newData = []): void
|
||
{
|
||
if (Auth::isSuperAdmin()) {
|
||
return;
|
||
}
|
||
$type = $dataLimit['type'] ?? null;
|
||
$field = $dataLimit['field'] ?? 'admin_id';
|
||
|
||
// 读取当前归属:create 时 $target 是 array,update 时是 Model
|
||
$current = is_array($target) ? null : $target->{$field};
|
||
|
||
// 1. personal → 只能自己
|
||
if ($type === 'personal') {
|
||
// create: 强制设置
|
||
if (is_array($target)) {
|
||
$target[$field] = admin_id();
|
||
}
|
||
// update: 校验当前归属
|
||
elseif ($current !== admin_id()) {
|
||
throw new BusinessException("无权操作该记录", 403);
|
||
}
|
||
// 若要修改归属,也必须和自己一致
|
||
if (isset($newData[$field]) && $newData[$field] !== admin_id()) {
|
||
throw new BusinessException("无权变更归属字段", 403);
|
||
}
|
||
}
|
||
|
||
// 2. auth → 自己及下属
|
||
if ($type === 'auth') {
|
||
$scope = Auth::getScopeAdminIds(true);
|
||
// create: 若前端指定归属,则校验范围;否则默认自己
|
||
if (is_array($target)) {
|
||
if (isset($target[$field])) {
|
||
if (!in_array($target[$field], $scope)) {
|
||
throw new BusinessException("无权为指定用户操作", 403);
|
||
}
|
||
} else {
|
||
$target[$field] = admin_id();
|
||
}
|
||
}
|
||
// update: 校验当前归属
|
||
elseif (!in_array($current, $scope)) {
|
||
throw new BusinessException("无权操作该记录", 403);
|
||
}
|
||
// 校验更新中指定的新归属
|
||
if (isset($newData[$field]) && !in_array($newData[$field], $scope)) {
|
||
throw new BusinessException("无权变更归属字段", 403);
|
||
}
|
||
}
|
||
}
|
||
} |