Files
fdesk/app/common/utils/CrudUtil.php
T

406 lines
13 KiB
PHP
Raw Normal View History

2025-07-18 17:32:42 +08:00
<?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();
// 1. eager load
if (!empty($config['with'])) {
$q->with($config['with']);
}
// 2. 关联表字段选择
if (!empty($config['withFields'])) {
foreach ($config['withFields'] as $rel => $fields) {
$q->with([$rel => function ($qr) use ($fields) {
$fk = $qr->getModel()->getForeignKey();
$qr->select(array_unique(array_merge($fields, [$fk])));
}]);
}
}
// 3. 本表字段选择
if (!empty($config['fields'])) {
$q->select($config['fields']);
}
// 4. where 条件(支持 dot-notation 关联过滤)
if (!empty($config['where'])) {
foreach ($config['where'] as $col => $val) {
if (strpos($col, '.') !== false) {
[$rel, $relCol] = explode('.', $col, 2);
$q->whereHas($rel, function ($qr) use ($relCol, $val) {
self::applyWhere($qr, $relCol, $val);
});
} else {
self::applyWhere($q, $col, $val);
}
}
}
// 5. 排序
if (!empty($config['order'])) {
foreach ($config['order'] as $col => $dir) {
$q->orderBy($col, strtolower($dir) === 'asc' ? 'asc' : 'desc');
}
}
// 6. 数据权限过滤
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 string $modelClass
* @param array $where
* @param array $with
* @param array $dataLimit
* @return Model
* @throws BusinessException
*/
// public static function findOneBy(
// string $modelClass,
// array $where,
// array $with = [],
// array $dataLimit = []
// ): Model {
// $config = compact('modelClass');
// $config['model'] = $modelClass;
// $config['where'] = $where;
// $config['with'] = $with;
// $config['dataLimit'] = $dataLimit;
// $m = self::buildQuery($config)->first();
// if (!$m) {
// throw new BusinessException("未找到符合条件的记录", 404);
// }
// return $m;
// }
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 string $modelClass
* @param array $where
* @param array $data
* @param array $dataLimit
* @return int 受影响行数
*/
// public static function updateBy(
// string $modelClass,
// array $where,
// array $data,
// array $dataLimit = []
// ): int {
// $config = ['model'=>$modelClass, 'where'=>$where, 'dataLimit'=>$dataLimit];
// return self::buildQuery($config)->update($data);
// }
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 string $modelClass
* @param array $where
* @param array $dataLimit
* @return int 删除条数
*/
// public static function deleteBy(
// string $modelClass,
// array $where,
// array $dataLimit = []
// ): int {
// $config = ['model'=>$modelClass, 'where'=>$where, 'dataLimit'=>$dataLimit];
// return self::buildQuery($config)->delete();
// }
public static function deleteBy(Builder $q): int
{
return $q->delete();
}
//=================== Internal Methods ===================//
/**
* 应用单字段 where 过滤
*
* @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) && count($val) === 2) {
[$op, $v] = $val;
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);
}
} else {
$q->where($col, $val);
}
}
/**
* 应用数据权限过滤到查询(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);
}
}
}
}