Files
fdesk/app/common/utils/CrudUtil.php
T
2025-09-22 15:50:36 +08:00

808 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common\utils;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use support\exception\BusinessException;
use support\Db;
/**
* 构建查询工具类
* | 方法 | 说明 |
* | ------------------------------- | --------------------- |
* | `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'],
* ],
* 'dataLimit' => [
* 'type' => 'personal|same_role|role_tree|relation', // personal:个人数据,same_role:同角色,role_tree:角色树,relation:从属关联数据
* 'userId' => 1,
* 'roleId' => 2,
* 'userField' => 'user_id',
* 'roleField' => 'role_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
{
$model = $q->first();
if (!$model) {
throw new BusinessException("记录不存在", 404);
}
return $model;
}
/**
* 创建,支持 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);
}
}
// ================== Data Limit ==================
/**
* 应用数据权限过滤到查询(buildQuery 场景)
*/
protected static function applyDataLimit(Builder $q, array $dataLimit): void
{
$type = $dataLimit['type'] ?? 'personal';
$userId = $dataLimit['userId'] ?? 0;
$roleId = $dataLimit['roleId'] ?? 0;
$userField = $dataLimit['userField'] ?? 'user_id';
$roleField = $dataLimit['roleField'] ?? 'role_id';
self::applyQueryPermission($q, $type, $userId, $roleId, $userField, $roleField);
}
/**
* 应用数据权限到"创建/更新"操作
*
* - $target 可能是 array|Model 对象
* - $newData 仅在 updateByPk 场景时传入,用以校验变更归属
*/
protected static function applyDataLimitOnMutate(&$target, array $dataLimit, array $newData = []): void
{
$type = $dataLimit['type'] ?? 'personal';
$userId = $dataLimit['userId'] ?? 0;
$roleId = $dataLimit['roleId'] ?? 0;
$userField = $dataLimit['userField'] ?? 'user_id';
$roleField = $dataLimit['roleField'] ?? 'role_id';
self::applyMutationPermission($target, $type, $userId, $roleId, $newData, $userField, $roleField);
}
/**
* 应用数据权限过滤到查询
*
* @param Builder $query 查询构造器
* @param string $type 权限类型:superadmin, personal, same_role, role_tree, relation
* @param int $currentUserId 当前用户ID
* @param int $currentRoleId 当前角色ID
* @param string $userField 用户ID字段名
* @param string $roleField 角色ID字段名
*/
public static function applyQueryPermission(
Builder $query,
string $type,
int $currentUserId = 0,
int $currentRoleId = 0,
string $userField = 'user_id',
string $roleField = 'role_id'
): void {
// 超管不需要过滤
if (self::isSuperAdmin($currentUserId)) {
return;
}
switch ($type) {
case 'personal':
// 个人权限:只能访问自己的数据
$query->where($userField, $currentUserId);
break;
case 'same_role':
// 同角色权限:访问同角色的数据
$userIds = self::getSameRoleUserIds($currentRoleId);
$query->whereIn($userField, $userIds);
break;
case 'role_tree':
// 角色树权限:访问自己角色及子角色的数据
$userIds = self::getRoleTreeUserIds($currentRoleId);
$query->whereIn($userField, $userIds);
break;
case 'relation':
// 从属关系权限:访问自己及下属的数据
$userIds = self::getRelationUserIds($currentUserId);
$query->whereIn($userField, $userIds);
break;
default:
// 默认个人权限
$query->where($userField, $currentUserId);
}
}
/**
* 应用数据权限到创建/更新操作
*
* @param mixed $target 创建时为数组,更新时为Model对象
* @param string $type 权限类型
* @param int $currentUserId 当前用户ID
* @param int $currentRoleId 当前角色ID
* @param array $newData 更新时的数据
* @param string $userField 用户ID字段名
* @param string $roleField 角色ID字段名
*/
public static function applyMutationPermission(
&$target,
string $type,
int $currentUserId,
int $currentRoleId,
array $newData = [],
string $userField = 'user_id',
string $roleField = 'role_id'
): void {
// 超管不需要检查
if (self::isSuperAdmin($currentUserId)) {
return;
}
switch ($type) {
case 'personal':
self::handlePersonalPermission($target, $newData, $userField, $currentUserId);
break;
case 'same_role':
self::handleSameRolePermission($target, $newData, $userField, $roleField, $currentRoleId);
break;
case 'role_tree':
self::handleRoleTreePermission($target, $newData, $userField, $roleField, $currentRoleId);
break;
case 'relation':
self::handleRelationPermission($target, $newData, $userField, $currentUserId);
break;
default:
throw new BusinessException("未定义的数据权限类型: $type", 403);
}
}
/**
* 获取同角色的用户ID列表
*
* @param int $roleId 角色ID
* @return array 用户ID列表
*/
public static function getSameRoleUserIds(int $roleId): array
{
return Db::table('user_roles')
->where('role_id', $roleId)
->pluck('user_id')
->toArray();
}
/**
* 获取角色树下的所有用户ID列表
*
* @param int $roleId 当前角色ID
* @return array 用户ID列表
*/
public static function getRoleTreeUserIds(int $roleId): array
{
// 获取角色树中的所有角色ID
$roleIds = self::getRoleTreeIds($roleId);
// 获取这些角色下的所有用户ID
return Db::table('user_roles')
->whereIn('role_id', $roleIds)
->pluck('user_id')
->toArray();
}
/**
* 获取从属关系下的用户ID列表(包括自己和下属)
*
* @param int $userId 当前用户ID
* @return array 用户ID列表
*/
public static function getRelationUserIds(int $userId): array
{
// 获取所有下属用户ID(递归)
$subordinateIds = self::getSubordinateIds($userId);
// 返回自己和所有下属的ID
return array_merge([$userId], $subordinateIds);
}
/**
* 获取直接下属的用户ID列表
*
* @param int $userId 用户ID
* @return array 直接下属用户ID列表
*/
public static function getDirectSubordinateIds(int $userId): array
{
return Db::table('user')
->where('report_to', $userId)
->pluck('id')
->toArray();
}
/**
* 递归获取所有下属用户ID列表
*
* @param int $userId 用户ID
* @return array 所有下属用户ID列表
*/
private static function getSubordinateIds(int $userId): array
{
$subordinates = self::getDirectSubordinateIds($userId);
$allSubordinates = $subordinates;
foreach ($subordinates as $subordinateId) {
$allSubordinates = array_merge($allSubordinates, self::getSubordinateIds($subordinateId));
}
return $allSubordinates;
}
/**
* 处理个人权限
*/
private static function handlePersonalPermission(
&$target,
array $newData,
string $userField,
int $currentUserId
): void {
if (is_array($target)) {
// 创建时强制设置用户ID
$target[$userField] = $currentUserId;
} else {
// 更新时校验原始记录
if ($target->{$userField} !== $currentUserId) {
throw new BusinessException("无权操作该记录", 403);
}
// 禁止变更归属字段
if (isset($newData[$userField]) && $newData[$userField] !== $currentUserId) {
throw new BusinessException("无权变更归属字段", 403);
}
}
}
/**
* 处理同角色权限
*/
private static function handleSameRolePermission(
&$target,
array $newData,
string $userField,
string $roleField,
int $currentRoleId
): void {
// 获取同角色用户ID列表
$sameRoleUserIds = self::getSameRoleUserIds($currentRoleId);
if (is_array($target)) {
// 创建时设置归属用户并校验
if (isset($target[$userField])) {
if (!in_array($target[$userField], $sameRoleUserIds)) {
throw new BusinessException("无权为指定用户创建数据", 403);
}
} else {
$target[$userField] = $currentRoleId;
}
} else {
// 更新时校验原始记录
if (!in_array($target->{$userField}, $sameRoleUserIds)) {
throw new BusinessException("无权操作该记录", 403);
}
// 禁止变更归属字段到无权限的用户
if (isset($newData[$userField]) && !in_array($newData[$userField], $sameRoleUserIds)) {
throw new BusinessException("无权变更归属字段", 403);
}
}
}
/**
* 处理角色树权限
*/
private static function handleRoleTreePermission(
&$target,
array $newData,
string $userField,
string $roleField,
int $currentRoleId
): void {
// 获取角色树用户ID列表
$roleTreeUserIds = self::getRoleTreeUserIds($currentRoleId);
if (is_array($target)) {
// 创建时校验用户是否在允许范围内
if (isset($target[$userField])) {
if (!in_array($target[$userField], $roleTreeUserIds)) {
throw new BusinessException("无权为指定用户创建数据", 403);
}
} else {
$target[$userField] = $currentRoleId;
}
} else {
// 更新时校验原始记录用户
if (!in_array($target->{$userField}, $roleTreeUserIds)) {
throw new BusinessException("无权操作该记录", 403);
}
// 校验新用户是否在允许范围内
if (isset($newData[$userField])) {
if (!in_array($newData[$userField], $roleTreeUserIds)) {
throw new BusinessException("无权变更用户字段", 403);
}
}
}
}
/**
* 处理从属关系权限
*/
private static function handleRelationPermission(
&$target,
array $newData,
string $userField,
int $currentUserId
): void {
// 获取从属关系用户ID列表
$relationUserIds = self::getRelationUserIds($currentUserId);
if (is_array($target)) {
// 创建时校验用户是否在允许范围内
if (isset($target[$userField])) {
if (!in_array($target[$userField], $relationUserIds)) {
throw new BusinessException("无权为指定用户创建数据", 403);
}
} else {
$target[$userField] = $currentUserId;
}
} else {
// 更新时校验原始记录用户
if (!in_array($target->{$userField}, $relationUserIds)) {
throw new BusinessException("无权操作该记录", 403);
}
// 校验新用户是否在允许范围内
if (isset($newData[$userField])) {
if (!in_array($newData[$userField], $relationUserIds)) {
throw new BusinessException("无权变更用户字段", 403);
}
}
}
}
/**
* 获取角色树ID(当前角色及其所有子角色)
* 从数据库中动态获取角色树关系
*/
private static function getRoleTreeIds(int $roleId): array
{
static $roleTreeCache = [];
if (isset($roleTreeCache[$roleId])) {
return $roleTreeCache[$roleId];
}
// 从数据库中递归获取角色树
$ids = [$roleId];
$queue = [$roleId];
while (!empty($queue)) {
$current = array_shift($queue);
// 查找当前角色的所有子角色
$childRoles = Db::table('roles')
->where('parent_id', $current)
->where('status', 1) // 只获取启用的角色
->pluck('id')
->toArray();
foreach ($childRoles as $childId) {
if (!in_array($childId, $ids)) {
$ids[] = $childId;
$queue[] = $childId;
}
}
}
$roleTreeCache[$roleId] = $ids;
return $ids;
}
/**
* 检查是否为超管
*
* @param int $userId 用户ID
* @return bool 是否为超管
*/
public static function isSuperAdmin(int $userId): bool
{
// 检查用户是否关联了超级管理员角色
// 设定角色ID为1的是超级管理员角色
$superAdminRoleId = 1;
return Db::table('user_roles')
->where('user_id', $userId)
->where('role_id', $superAdminRoleId)
->exists();
}
/**
* 获取用户的角色ID
*
* @param int $userId 用户ID
* @return int 角色ID
*/
public static function getUserIdRoleId(int $userId): int
{
// 从 user_roles 表中获取当前用户的角色ID
$roleId = Db::table('user_roles')
->where('user_id', $userId)
->value('role_id');
return $roleId ?: 0;
}
/**
* 获取用户的所有角色ID(包括继承的角色)
*
* @param int $userId 用户ID
* @return array 角色ID列表
*/
public static function getUserAllRoleIds(int $userId): array
{
// 获取用户直接关联的角色
$directRoleIds = Db::table('user_roles')
->where('user_id', $userId)
->pluck('role_id')
->toArray();
// 获取所有继承的角色
$allRoleIds = [];
foreach ($directRoleIds as $roleId) {
$allRoleIds = array_merge($allRoleIds, self::getRoleTreeIds($roleId));
}
// 去重并返回
return array_unique($allRoleIds);
}
}