crud
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\admin\service\CorpService;
|
||||
|
||||
class CorpController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new CorpService();
|
||||
}
|
||||
|
||||
protected function indexFormat($data)
|
||||
{
|
||||
if (isset($data['created_at']))
|
||||
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
|
||||
if (isset($data['updated_at']))
|
||||
$data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果想对某些接口做额外校验或改写,
|
||||
* 也可以在这里覆盖 trait 提供的方法:
|
||||
*
|
||||
* public function store(Request $request)
|
||||
* {
|
||||
* // 自定义验证
|
||||
* $this->validate(...);
|
||||
* // 然后再调用标准逻辑
|
||||
* return parent::store($request);
|
||||
* }
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\Corp;
|
||||
|
||||
class CorpLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = Corp::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\service;
|
||||
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\Corp;
|
||||
use support\Request;
|
||||
|
||||
class CorpService
|
||||
{
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = Corp::class;
|
||||
|
||||
|
||||
/**
|
||||
* 如果需要自定义,比如强制加一个 where['status']=1,
|
||||
* 只要在这里重写:
|
||||
*/
|
||||
// protected function buildQueryConfig(Request $request): array
|
||||
// {
|
||||
// $config = [];
|
||||
// $config['where']['id'] = 2;
|
||||
// return $config;
|
||||
// }
|
||||
|
||||
/**
|
||||
* 如果需要自定义“哪些字段可筛选”或额外联表,
|
||||
* 只需重写 buildQueryConfig:
|
||||
*/
|
||||
// protected function buildQueryConfig(Request $request): array
|
||||
// {
|
||||
// $config = parent::buildQueryConfig($request);
|
||||
// // 强制只看 status=1 的记录
|
||||
// $config['where']['status'] = 1;
|
||||
// // 自动 eager load company relation
|
||||
// $config['with'][] = 'company';
|
||||
// return $config;
|
||||
// }
|
||||
|
||||
/**
|
||||
* 复杂查询,覆盖 complexQuery 方法
|
||||
*/
|
||||
// protected function complexQuery(array $params): array
|
||||
// {
|
||||
// return $this->logicComplex(function($q) use ($params) {
|
||||
// // 自定义 join、where、select
|
||||
// return $q->where('status', 1)
|
||||
// ->orderBy('created_at','desc')
|
||||
// ->paginate($params['limit'] ?? 15);
|
||||
// })->toArray();
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 复杂查询示例:
|
||||
// * 只要在 URL 里添加 ?complex_query=1,
|
||||
// * 并传入你需要的任何参数,
|
||||
// * `serviceList()` 就会走到这里来:
|
||||
// */
|
||||
// protected function complexQuery(Request $request): array
|
||||
// {
|
||||
// // 假设前端传了 date_from、date_to
|
||||
// $from = $request->get('date_from');
|
||||
// $to = $request->get('date_to');
|
||||
|
||||
// // 交给 CrudUtil 执行一个自定义 query
|
||||
// $pager = $this->logicComplex(function($q) use ($from, $to) {
|
||||
// $q->where('status', 1);
|
||||
|
||||
// if ($from && $to) {
|
||||
// $q->whereBetween('created_at', [$from, $to]);
|
||||
// }
|
||||
|
||||
// // 再加个排序
|
||||
// $q->orderBy('created_at','desc');
|
||||
|
||||
// // 用 paginate 而不是 listAll
|
||||
// return $q->paginate(
|
||||
// /* perPage */ (int)request()->get('limit',15),
|
||||
// ['*'],
|
||||
// 'page',
|
||||
// (int)request()->get('page',1)
|
||||
// );
|
||||
// });
|
||||
|
||||
// // 把 Paginator 转成数组格式
|
||||
// return [
|
||||
// 'total' => $pager->total(),
|
||||
// 'data' => $pager->items(),
|
||||
// ];
|
||||
// }
|
||||
|
||||
|
||||
// protected function afterServiceList(array $res): array
|
||||
// {
|
||||
// // 对列表的 data 做处理
|
||||
// $res['data'] = array_map(fn($i)=>[...], $res['data']);
|
||||
// return $res;
|
||||
// }
|
||||
|
||||
// protected function afterServiceShow($item)
|
||||
// {
|
||||
// // 对详情对象处理
|
||||
// $item['extra']=123;
|
||||
// return $item;
|
||||
// }
|
||||
|
||||
// afterServiceStore/Update/Destroy 同理
|
||||
|
||||
|
||||
// protected function afterServiceList(mixed $result): array
|
||||
// {
|
||||
// if ($result['records']) {
|
||||
// $result['records'] = array_map(fn($i) => [...$i], $result['records']);
|
||||
// }
|
||||
|
||||
// return $result;
|
||||
// }
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class AuthController extends BaseController
|
||||
/**
|
||||
* 不需要头信息的方法
|
||||
*/
|
||||
protected $noNeedHeader = ['feishuAuth'];
|
||||
protected $noNeedHeaderData = ['feishuAuth'];
|
||||
|
||||
protected $noNeedLogin = ['feishuScope', 'login', 'register', 'feishuAuth'];
|
||||
|
||||
|
||||
@@ -10,10 +10,54 @@ use support\Request;
|
||||
|
||||
class BaseController
|
||||
{
|
||||
/**
|
||||
* 响应处理、数据格式化处理
|
||||
*/
|
||||
use ResponseTrait, DataTransformer;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
* @var Validate
|
||||
*/
|
||||
protected mixed $validator = null;
|
||||
|
||||
/**
|
||||
* @var Model
|
||||
*/
|
||||
protected $model = null;
|
||||
|
||||
/**
|
||||
* 不登录不鉴权
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* 登录不鉴权
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedAuth = [];
|
||||
|
||||
/**
|
||||
* 不用头部数据
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedHeaderData = [];
|
||||
|
||||
/**
|
||||
* 数据限制
|
||||
* null 不做限制,任何管理员都可以查看该表的所有数据
|
||||
* auth 管理员能看到自己以及自己的子管理员插入的数据
|
||||
* personal 管理员只能看到自己插入的数据
|
||||
* @var string
|
||||
*/
|
||||
protected $dataLimit = null;
|
||||
|
||||
/**
|
||||
* 数据限制字段
|
||||
*/
|
||||
protected $dataLimitField = 'admin_id';
|
||||
|
||||
/**
|
||||
* 验证数据
|
||||
*
|
||||
|
||||
@@ -1,593 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\model\Corp;
|
||||
use app\common\model\CorpFeishuApp;
|
||||
use app\common\model\UserTokenLog;
|
||||
use app\common\model\Systems;
|
||||
use app\common\model\User;
|
||||
use app\common\model\UserFeishuApp;
|
||||
use app\common\model\UserSn;
|
||||
use app\common\utils\Feishu;
|
||||
use app\common\utils\Jwt;
|
||||
use support\Redis;
|
||||
use support\exception\BusinessException;
|
||||
use support\Log;
|
||||
|
||||
class AuthLogic_bak
|
||||
{
|
||||
/**
|
||||
* 账号密码登录
|
||||
*/
|
||||
public function uPwdLogin(array $data): array
|
||||
{
|
||||
$user = $this->validateUser($data['account'], $data['password']);
|
||||
|
||||
$system = $this->validateSystem($data['system_code']);
|
||||
$this->validateSystemPermission($user, $system);
|
||||
$payload = $this->buildJwtPayload($user, $system);
|
||||
$token = $this->generateAccessToken($payload);
|
||||
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
|
||||
|
||||
// 记录 token 到 Redis 和 MySQL
|
||||
$this->recordUserToken($user->id, $token, $data['system_code']);
|
||||
|
||||
return [
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_in' => config('jwt.jwt_expiration'),
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞书登录
|
||||
* 如果用户不存在则新增用户信息
|
||||
*/
|
||||
public function feishuLogin(array $data, bool $isLogin = true): array
|
||||
{
|
||||
$feishuUserInfo = $this->getFeishuUserInfo($data);
|
||||
$user = $this->getOrCreateUser($feishuUserInfo);
|
||||
|
||||
$system = $this->validateSystem($data['system_code']);
|
||||
if ($user->isLogin === true) {
|
||||
$this->validateSystemPermission($user, $system);
|
||||
}
|
||||
$payload = $this->buildJwtPayload($user, $system);
|
||||
$token = $this->generateAccessToken($payload);
|
||||
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
|
||||
|
||||
// 记录 token 到 Redis 和 MySQL
|
||||
$this->recordUserToken($user->id, $token, $data['system_code']);
|
||||
|
||||
return [
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_in' => config('jwt.jwt_expiration'),
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码登录
|
||||
*/
|
||||
public function qrCodeLogin(array $data): array
|
||||
{
|
||||
$userId = $this->validateQrCodeToken($data['qr_code_token']);
|
||||
$user = User::find($userId);
|
||||
if (!$user || $user->status != 1) {
|
||||
throw new BusinessException('用户无效', 403);
|
||||
}
|
||||
|
||||
$system = $this->validateSystem($data['system_code']);
|
||||
$this->validateSystemPermission($user, $system);
|
||||
$payload = $this->buildJwtPayload($user, $system);
|
||||
$token = $this->generateAccessToken($payload);
|
||||
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
|
||||
|
||||
// 记录 token 到 Redis 和 MySQL
|
||||
$this->recordUserToken($user->id, $token, $data['system_code']);
|
||||
|
||||
return [
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_in' => config('jwt.jwt_expiration'),
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*/
|
||||
public function register(array $data)
|
||||
{
|
||||
// 获取可用用户编号
|
||||
$sn = $this->getAvailableUserSn();
|
||||
$data['sn'] = $sn->user_sn;
|
||||
|
||||
// 对密码进行哈希处理
|
||||
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
|
||||
// 获取企业ID
|
||||
$corpInfo = $this->validateCorp($data['corp_abbr']);
|
||||
$data['corp_id'] = $corpInfo->id;
|
||||
|
||||
// 创建用户记录
|
||||
$user = $this->createUser($data);
|
||||
|
||||
// 更新用户编号的用户 ID
|
||||
$this->updateUserId($user->id, $sn->user_sn);
|
||||
|
||||
// 注册后如果直接登录(返回token)
|
||||
$system = $this->validateSystem($data['system_code']);
|
||||
$payload = $this->buildJwtPayload($user, $system);
|
||||
$token = $this->generateAccessToken($payload);
|
||||
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
|
||||
$this->recordUserToken($user->id, $token, $data['system_code']);
|
||||
|
||||
return [
|
||||
'user' => $user,
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_in' => config('jwt.jwt_expiration'),
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
public function updateUserId($userId, $userSn)
|
||||
{
|
||||
$updated = UserSn::where('user_sn', $userSn)
|
||||
->update(['user_id' => $userId]);
|
||||
|
||||
if ($updated === 0) {
|
||||
throw new BusinessException('用户编号不存在或更新失败');
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的用户编号
|
||||
*
|
||||
* @return \app\common\model\UserSn 用户编号模型实例
|
||||
* @throws BusinessException 当未找到适用编号时抛出
|
||||
*/
|
||||
private function getAvailableUserSn(): \app\common\model\UserSn
|
||||
{
|
||||
$userSnModel = new UserSn();
|
||||
$sn = $userSnModel->where('user_id', '=', 0)->inRandomOrder()->first();
|
||||
if (!$sn) {
|
||||
Log::error('用户注册时未找到适用编号');
|
||||
throw new BusinessException('未找到适用编号');
|
||||
}
|
||||
return $sn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户记录
|
||||
*
|
||||
* @param array $data 包含用户信息的数组
|
||||
* @return \app\common\model\User 用户模型实例
|
||||
*/
|
||||
private function createUser(array $data): \app\common\model\User
|
||||
{
|
||||
try {
|
||||
return User::create($data);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('用户注册时创建用户记录失败', [
|
||||
'data' => $data,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
throw new BusinessException('用户注册失败,请稍后重试', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
* @param string $refreshToken
|
||||
* @param string|null $systemCode 可选,指定系统
|
||||
* @return array
|
||||
*/
|
||||
public function refreshToken(string $refreshToken, ?string $systemCode = null): array
|
||||
{
|
||||
if (!$systemCode) throw new BusinessException('缺少system_code', 422);
|
||||
|
||||
// 校验refresh_token
|
||||
$userId = Redis::get("refresh_token_map:{$refreshToken}:{$systemCode}");
|
||||
if (!$userId) {
|
||||
throw new BusinessException('refresh_token无效或已过期', 401);
|
||||
}
|
||||
$validToken = Redis::get("refresh_token:{$userId}:{$systemCode}");
|
||||
if ($validToken !== $refreshToken) {
|
||||
throw new BusinessException('refresh_token无效或已过期', 401);
|
||||
}
|
||||
|
||||
$user = User::find($userId);
|
||||
if (!$user || $user->status != 1) {
|
||||
throw new BusinessException('用户无效', 403);
|
||||
}
|
||||
|
||||
$system = $this->validateSystem($systemCode);
|
||||
$this->validateSystemPermission($user, $system);
|
||||
|
||||
$payload = $this->buildJwtPayload($user, $system);
|
||||
$token = $this->generateAccessToken($payload);
|
||||
$this->recordUserToken($user->id, $token, $systemCode);
|
||||
|
||||
return [
|
||||
'access_token' => $token,
|
||||
'expires_in' => config('jwt.jwt_expiration'),
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录用户token到 Redis 和 MySQL
|
||||
*/
|
||||
protected function recordUserToken(int $userId, string $token, string $systemCode)
|
||||
{
|
||||
$request = request();
|
||||
$ip = $request ? $request->getRealIp() : '';
|
||||
$ua = $request ? $request->header('user-agent', '') : '';
|
||||
$now = time();
|
||||
$expireAt = $now + config('jwt.jwt_expiration');
|
||||
|
||||
// Redis Set 用户token集合
|
||||
Redis::sAdd("user_tokens:{$userId}:{$systemCode}", $token);
|
||||
Redis::expire("user_tokens:{$userId}:{$systemCode}", config('jwt.jwt_expiration'));
|
||||
|
||||
// Redis Hash token元信息
|
||||
$meta = [
|
||||
'system_code' => $systemCode,
|
||||
'login_ip' => $ip,
|
||||
'user_agent' => $ua,
|
||||
'login_time' => $now,
|
||||
'expired_at' => $expireAt,
|
||||
];
|
||||
Redis::hMSet("user_token_meta:{$token}", $meta);
|
||||
Redis::expire("user_token_meta:{$token}", config('jwt.jwt_expiration'));
|
||||
|
||||
// 可选:MySQL 持久化
|
||||
try {
|
||||
UserTokenLog::create([
|
||||
'user_id' => $userId,
|
||||
'token' => $token,
|
||||
'system_code'=> $systemCode,
|
||||
'login_ip' => $ip,
|
||||
'user_agent' => $ua,
|
||||
'login_time' => $now,
|
||||
'expired_at' => $expireAt,
|
||||
'is_valid' => 1,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Token log write failed: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 校验token有效性
|
||||
*/
|
||||
public function verifyToken(string $token, string $systemCode): array
|
||||
{
|
||||
if (!$systemCode) throw new BusinessException('缺少system_code', 422);
|
||||
|
||||
$payload = (new Jwt())->verifyToken($token);
|
||||
|
||||
// 校验黑名单
|
||||
if (Redis::has("token_blacklist:{$token}")) {
|
||||
throw new BusinessException('token已失效', 403);
|
||||
}
|
||||
|
||||
// 强制校验aud(系统隔离)
|
||||
if ($systemCode && $payload['aud'] != $systemCode && $payload['aud'] != ($payload['system_code'] ?? '')) {
|
||||
throw new BusinessException('token不属于当前系统', 403);
|
||||
}
|
||||
|
||||
return (array)$payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出/失效token
|
||||
*/
|
||||
public function logout(?string $token, ?string $systemCode = null)
|
||||
{
|
||||
if ($token) {
|
||||
Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration'));
|
||||
$payload = null;
|
||||
try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {}
|
||||
$userId = $payload['uid'] ?? null;
|
||||
$aud = $payload['aud'] ?? null;
|
||||
$sc = $systemCode ?? $aud;
|
||||
if ($userId && $sc) {
|
||||
Redis::sRem("user_tokens:{$userId}:{$sc}", $token);
|
||||
}
|
||||
Redis::del("user_token_meta:{$token}");
|
||||
UserTokenLog::where('token', $token)->update(['is_valid'=>0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制拉黑token
|
||||
*/
|
||||
public function blockToken(string $token, ?string $systemCode = null)
|
||||
{
|
||||
if (!$token) {
|
||||
throw new BusinessException('token不能为空', 422);
|
||||
}
|
||||
Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration'));
|
||||
$payload = null;
|
||||
try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {}
|
||||
$userId = $payload['uid'] ?? null;
|
||||
$aud = $payload['aud'] ?? null;
|
||||
$sc = $systemCode ?? $aud;
|
||||
if ($userId && $sc) {
|
||||
Redis::sRem("user_tokens:{$userId}:{$sc}", $token);
|
||||
}
|
||||
Redis::del("user_token_meta:{$token}");
|
||||
UserTokenLog::where('token', $token)->update(['is_valid'=>0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所有活跃token及元信息(按system_code)
|
||||
*/
|
||||
public function getUserTokens(int $userId, string $systemCode): array
|
||||
{
|
||||
$tokens = Redis::sMembers("user_tokens:{$userId}:{$systemCode}") ?: [];
|
||||
$result = [];
|
||||
foreach ($tokens as $token) {
|
||||
$meta = Redis::hGetAll("user_token_meta:{$token}");
|
||||
$result[] = [
|
||||
'token' => $token,
|
||||
'meta' => $meta,
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制某用户所有token全部下线
|
||||
*/
|
||||
public function blockAllTokensOfUser(int $userId, string $systemCode)
|
||||
{
|
||||
$tokens = Redis::sMembers("user_tokens:{$userId}:{$systemCode}") ?: [];
|
||||
foreach ($tokens as $token) {
|
||||
$this->blockToken($token, $systemCode);
|
||||
}
|
||||
}
|
||||
|
||||
/** ----------- 内部方法 ------------- **/
|
||||
|
||||
/**
|
||||
* 用户验证
|
||||
*/
|
||||
private function validateUser(string $account, string $password)
|
||||
{
|
||||
$user = User::byAccount($account)->first();
|
||||
if (!$user || !password_verify($password, $user->password)) {
|
||||
throw new BusinessException('账号或密码错误', 401);
|
||||
}
|
||||
if ($user->status != 1) {
|
||||
throw new BusinessException('账号已禁用', 403);
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统验证
|
||||
*/
|
||||
private function validateSystem(string $systemCode)
|
||||
{
|
||||
$system = Systems::where('code', $systemCode)->first();
|
||||
if (!$system) {
|
||||
throw new BusinessException('系统不存在', 403);
|
||||
}
|
||||
return $system;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公司验证
|
||||
*/
|
||||
private function validateCorp(string $corpAbbr)
|
||||
{
|
||||
$corp = Corp::where('abbr', $corpAbbr)->first();
|
||||
if (!$corp) {
|
||||
throw new BusinessException('公司不存在', 403);
|
||||
}
|
||||
return $corp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞书应用信息
|
||||
*/
|
||||
private function validateFeishuApp(int $corpId, string $systemCode)
|
||||
{
|
||||
$feishuAppInfo = CorpFeishuApp::where('corp_id', $corpId)->where('system_code', $systemCode)->first();
|
||||
if (!$feishuAppInfo) {
|
||||
throw new BusinessException('飞书应用不存在', 403);
|
||||
}
|
||||
return $feishuAppInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户系统权限校验
|
||||
*/
|
||||
private function validateSystemPermission(User $user, Systems $system)
|
||||
{
|
||||
if ($system->is_role) {
|
||||
$systemIds = $user->systems->pluck('id')->toArray();
|
||||
if (!in_array($system->id, $systemIds)) {
|
||||
throw new BusinessException('无该系统权限', 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT payload 构建
|
||||
* 可扩展 payload 字段
|
||||
*/
|
||||
private function buildJwtPayload(User $user, ?Systems $system = null): array
|
||||
{
|
||||
$now = time();
|
||||
$exp = $now + config('jwt.jwt_expiration');
|
||||
$system_ids = $user->systems ? $user->systems->pluck('id')->toArray() : [];
|
||||
$role_ids = $user->roles ? $user->roles->pluck('id')->toArray() : [];
|
||||
|
||||
return [
|
||||
'sub' => 'user_' . $user->id,
|
||||
'aud' => $system->code,
|
||||
'iat' => $now,
|
||||
'nbf' => $now,
|
||||
'exp' => $exp,
|
||||
'typ' => 'access',
|
||||
'uid' => $user->id,
|
||||
'role_ids' => $role_ids,
|
||||
'system_ids' => $system_ids
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT签发
|
||||
*/
|
||||
private function generateAccessToken(array $payload)
|
||||
{
|
||||
return (new Jwt())->generateToken($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌生成
|
||||
*/
|
||||
private function generateRefreshToken(int $userId, string $systemCode)
|
||||
{
|
||||
$refreshToken = bin2hex(random_bytes(32));
|
||||
// user_id+system_code只允许一个refresh_token生效
|
||||
Redis::set("refresh_token:{$userId}:{$systemCode}", $refreshToken, config('jwt.jwt_refresh_token_expiration'));
|
||||
// 建立refresh_token到user_id+system_code映射,便于校验
|
||||
Redis::set("refresh_token_map:{$refreshToken}:{$systemCode}", $userId, config('jwt.jwt_refresh_token_expiration'));
|
||||
return $refreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证扫码令牌
|
||||
*/
|
||||
private function validateQrCodeToken(string $qrCodeToken)
|
||||
{
|
||||
$userId = Redis::get("qr_code_token:{$qrCodeToken}");
|
||||
if (!$userId) {
|
||||
throw new BusinessException('扫码令牌无效或已过期', 401);
|
||||
}
|
||||
return $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取飞书用户信息
|
||||
*/
|
||||
private function getFeishuUserInfo(array $data)
|
||||
{
|
||||
// 获取凭证
|
||||
$certificate = $this->feishuAppInfo($data['corp_abbr'], $data['system_code']);
|
||||
// 获取token
|
||||
$feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']);
|
||||
$userAccessToken = $feishu->userAccessToken($data['feishu_code'], $certificate['r_url']);
|
||||
// 获取用户信息
|
||||
$userInfo = $feishu->userInfo($userAccessToken['access_token']);
|
||||
if ($userInfo['code'] != 0) {
|
||||
throw new BusinessException('授权失败,请重试');
|
||||
}
|
||||
return $userInfo['data'] + $data + $certificate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找或创建飞书用户
|
||||
*/
|
||||
private function getOrCreateUser(array $feishuUserInfo)
|
||||
{
|
||||
$userFeishuApp = UserFeishuApp::firstOrCreate([
|
||||
'name' => $feishuUserInfo['en_name'],
|
||||
'mobile' => $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''),
|
||||
'union_id' => $feishuUserInfo['union_id'],
|
||||
'user_id' => $feishuUserInfo['user_id'],
|
||||
'open_id' => $feishuUserInfo['open_id'],
|
||||
'tenant_key' => $feishuUserInfo['tenant_key']
|
||||
]);
|
||||
|
||||
if ($userFeishuApp->uid) {
|
||||
$user = User::find($userFeishuApp->uid);
|
||||
$user->isLogin = true;
|
||||
return $user;
|
||||
} else {
|
||||
$user = User::where('phone', $userFeishuApp->mobile)->first();
|
||||
$user->isLogin = true;
|
||||
if (!$user) {
|
||||
$user = $this->createUserByFeishu($feishuUserInfo, $userFeishuApp);
|
||||
$user->isLogin = false;
|
||||
}
|
||||
$userFeishuApp->uid = $user->id;
|
||||
$userFeishuApp->corp_id = $feishuUserInfo['corp_id'];
|
||||
if (!$userFeishuApp->save()) {
|
||||
throw new BusinessException('绑定用户飞书记录失败');
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤和格式化手机号
|
||||
*/
|
||||
private function sanitizePhone($mobile)
|
||||
{
|
||||
return isset($mobile) ? str_replace('+86', '', $mobile) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建飞书用户
|
||||
*/
|
||||
private function createUserByFeishu(array $feishuUserInfo, UserFeishuApp $userFeishuApp)
|
||||
{
|
||||
// 1. 获取适用的用户编号
|
||||
$userSn = UserSn::where('user_id', '=', 0)->inRandomOrder()->first();
|
||||
if (!$userSn) {
|
||||
throw new BusinessException('未找到适用编号');
|
||||
}
|
||||
|
||||
// 2. 创建用户
|
||||
$user = new User();
|
||||
$user->sn = $userSn->user_sn;
|
||||
$user->name = $feishuUserInfo['en_name'];
|
||||
$user->avatar = $feishuUserInfo['avatar_middle'] ?? '';
|
||||
$user->password = password_hash($userSn->user_sn, PASSWORD_DEFAULT);
|
||||
$user->phone = $userFeishuApp->mobile;
|
||||
$user->email = '';
|
||||
$user->system_code = $feishuUserInfo['system_code'];
|
||||
$user->corp_id = $feishuUserInfo['corp_id'];
|
||||
|
||||
if (!$user->save()) {
|
||||
throw new BusinessException('用户创建失败');
|
||||
}
|
||||
|
||||
// 更新使用状态
|
||||
$this->updateUserId($user->id, $userSn->user_sn);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 飞书应用信息
|
||||
*/
|
||||
public function feishuAppInfo(string $corpAbbr, string $systemCode)
|
||||
{
|
||||
$corpInfo = $this->validateCorp($corpAbbr);
|
||||
$fsAppInfo = $this->validateFeishuApp($corpInfo->id, $systemCode);
|
||||
return [
|
||||
'app_id' => $fsAppInfo->app_id,
|
||||
'app_secret' => $fsAppInfo->app_secret,
|
||||
'r_url' => $fsAppInfo->r_url,
|
||||
'scope' => $fsAppInfo->scopes,
|
||||
'state' => $fsAppInfo->state,
|
||||
'abbr' => $corpAbbr,
|
||||
'corp_id' => $corpInfo->id
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class BaseLogic
|
||||
{
|
||||
/**
|
||||
* 获取列表
|
||||
*/
|
||||
public function getList(array $params, string $modelClass)
|
||||
{
|
||||
$query = $modelClass::query();
|
||||
|
||||
// 你可以在此处添加分页、排序等功能
|
||||
if (isset($params['page']) && isset($params['per_page'])) {
|
||||
$query->paginate($params['per_page'], ['*'], 'page', $params['page']);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个记录
|
||||
*/
|
||||
public function getDetail(int $id, string $modelClass)
|
||||
{
|
||||
$record = $modelClass::find($id);
|
||||
if (!$record) {
|
||||
throw new BusinessException("记录不存在", 404);
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建记录
|
||||
*/
|
||||
public function create(array $data, string $modelClass)
|
||||
{
|
||||
$model = new $modelClass;
|
||||
$model->fill($data); // 根据字段填充数据
|
||||
try {
|
||||
$model->save();
|
||||
} catch (\Throwable $e) {
|
||||
throw new BusinessException("创建记录失败: " . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新记录
|
||||
*/
|
||||
public function update(array $data, string $modelClass)
|
||||
{
|
||||
$record = $modelClass::find($data['id']);
|
||||
if (!$record) {
|
||||
throw new BusinessException("记录不存在", 404);
|
||||
}
|
||||
|
||||
$record->update($data);
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除记录
|
||||
*/
|
||||
public function delete(int $id, string $modelClass)
|
||||
{
|
||||
$record = $modelClass::find($id);
|
||||
if (!$record) {
|
||||
throw new BusinessException("记录不存在", 404);
|
||||
}
|
||||
|
||||
try {
|
||||
$record->delete();
|
||||
} catch (\Throwable $e) {
|
||||
throw new BusinessException("删除记录失败: " . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\utils\CrudUtil;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CrudLogic_bak
|
||||
{
|
||||
protected string $modelClass;
|
||||
|
||||
/**
|
||||
* @param string $modelClass 完整模型类名,例如 \app\common\model\User::class
|
||||
*/
|
||||
public function __construct(string $modelClass)
|
||||
{
|
||||
$this->modelClass = $modelClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表(带分页)
|
||||
* @param array $config CrudUtil::buildQuery 接受的配置
|
||||
* @param int $limit
|
||||
* @param int $page
|
||||
*/
|
||||
public function list(array $config, int $limit, int $page)
|
||||
{
|
||||
$config['model'] = $this->modelClass;
|
||||
$query = CrudUtil::buildQuery($config);
|
||||
return CrudUtil::paginate($query, $limit, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表(不分页)
|
||||
*/
|
||||
public function listAll(array $config)
|
||||
{
|
||||
$config['model'] = $this->modelClass;
|
||||
$query = CrudUtil::buildQuery($config);
|
||||
return CrudUtil::getAll($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据主键查询单条
|
||||
*/
|
||||
public function show($id, array $with = [])
|
||||
{
|
||||
return CrudUtil::findByPk($this->modelClass, $id, $with);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据任意条件查询单条(可带 dataLimit)
|
||||
*/
|
||||
public function showBy(array $where, array $with = [], array $dataLimit = [])
|
||||
{
|
||||
return CrudUtil::findOneBy($this->modelClass, $where, $with, $dataLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建(可带 dataLimit)
|
||||
*/
|
||||
public function create(array $data, array $dataLimit = [])
|
||||
{
|
||||
return CrudUtil::create($this->modelClass, $data, $dataLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主键更新(可带 dataLimit)
|
||||
*/
|
||||
public function updateByPk($id, array $data, array $dataLimit = [])
|
||||
{
|
||||
return CrudUtil::updateByPk($this->modelClass, $id, $data, $dataLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 任意条件更新(可带 dataLimit)
|
||||
*/
|
||||
public function updateBy(array $where, array $data, array $dataLimit = [])
|
||||
{
|
||||
return CrudUtil::updateBy($this->modelClass, $where, $data, $dataLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主键删除
|
||||
*/
|
||||
public function deleteByPk(array $ids)
|
||||
{
|
||||
return CrudUtil::deleteByPk($this->modelClass, $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 任意条件删除(可带 dataLimit)
|
||||
*/
|
||||
public function deleteBy(array $where, array $dataLimit = [])
|
||||
{
|
||||
return CrudUtil::deleteBy($this->modelClass, $where, $dataLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复杂查询回调
|
||||
* @param callable $cb function(Builder $q){ ... }
|
||||
*/
|
||||
public function complex(callable $cb)
|
||||
{
|
||||
return CrudUtil::executeComplex($this->modelClass, $cb);
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
/**
|
||||
* user_feishu_app 用户-飞书应用
|
||||
* @property integer $id ID(主键)
|
||||
* @property integer $uid 用户ID
|
||||
* @property integer $corp_id 公司ID
|
||||
* @property string $name 名称
|
||||
* @property string $mobile 手机
|
||||
* @property string $union_id ISV的唯一标识
|
||||
* @property string $user_id 用户 user_id
|
||||
* @property string $open_id 应用内的唯一标识
|
||||
* @property string $tenant_key 当前企业标识
|
||||
* @property integer $sort 排序
|
||||
* @property integer $status 状态
|
||||
* @property integer $created_at 创建时间
|
||||
* @property integer $updated_at 更新时间
|
||||
*/
|
||||
class UserFeishuApp_bak extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'user_feishu_app';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
|
||||
protected $fillable = [
|
||||
'uid',
|
||||
'corp_id',
|
||||
'name',
|
||||
'mobile',
|
||||
'union_id',
|
||||
'user_id',
|
||||
'open_id',
|
||||
'tenant_key',
|
||||
'sort',
|
||||
'status'
|
||||
];
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\logic\AuthLogic;
|
||||
use app\common\model\User;
|
||||
use support\Cache;
|
||||
|
||||
class AuthService_bak
|
||||
{
|
||||
// 缓存 Key
|
||||
protected static function cacheKey(int $userId): string
|
||||
{
|
||||
return "user_resources_{$userId}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所有资源(目录/菜单/权限),合并角色+直接分配,并缓存
|
||||
*/
|
||||
public static function getUserResources(int $userId)
|
||||
{
|
||||
$cacheKey = self::cacheKey($userId);
|
||||
$cached = Cache::get($cacheKey);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$user = User::with(['roles.resources', 'resources'])->find($userId);
|
||||
if (!$user) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
// 合并:角色资源 + 用户直接资源
|
||||
$all = collect();
|
||||
foreach ($user->roles as $role) {
|
||||
$all = $all->merge($role->resources);
|
||||
}
|
||||
$all = $all->merge($user->resources);
|
||||
$unique = $all->unique('id')->values();
|
||||
|
||||
Cache::set($cacheKey, $unique, 3600);
|
||||
return $unique;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建目录/菜单树,并给每个菜单节点挂载权限列表和打开方式
|
||||
*/
|
||||
public static function getMenuTree(int $userId): array
|
||||
{
|
||||
$resources = self::getUserResources($userId);
|
||||
$menus = $resources
|
||||
->filter(fn($r) => in_array($r->type, ['directory','menu']))
|
||||
->values();
|
||||
return self::buildTree($menus, $resources);
|
||||
}
|
||||
|
||||
protected static function buildTree($nodes, $allResources, int $parentId = 0): array
|
||||
{
|
||||
$tree = [];
|
||||
foreach ($nodes as $node) {
|
||||
if ($node->parent_id !== $parentId) continue;
|
||||
|
||||
// 子节点
|
||||
$children = self::buildTree($nodes, $allResources, $node->id);
|
||||
|
||||
// 本级权限列表
|
||||
$authList = $allResources
|
||||
->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id)
|
||||
->map(fn($r) => [
|
||||
'id' => $r->id,
|
||||
'title' => $r->name,
|
||||
'auth_mark' => $r->code,
|
||||
])->values()->all();
|
||||
|
||||
$tree[] = [
|
||||
'id' => $node->id,
|
||||
'name' => $node->name,
|
||||
'path' => $node->url,
|
||||
'meta' => [
|
||||
'title' => $node->name,
|
||||
'icon' => $node->icon,
|
||||
'keepAlive' => true,
|
||||
'authList' => $authList,
|
||||
// 新增打开方式字段
|
||||
'openType' => $node->open_type, // 'internal' | 'external' | 'iframe'
|
||||
],
|
||||
'children' => $children,
|
||||
];
|
||||
}
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所有权限 code(仅 type=permission)
|
||||
*/
|
||||
public static function getPermissionCodes(int $userId): array
|
||||
{
|
||||
return self::getUserResources($userId)
|
||||
->filter(fn($res) => $res->type === 'permission')
|
||||
->pluck('code')
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/** 清除缓存(角色/资源变更时调用) */
|
||||
public static function clearCache(int $userId): void
|
||||
{
|
||||
Cache::delete(self::cacheKey($userId));
|
||||
}
|
||||
|
||||
public function login(array $data): array
|
||||
{
|
||||
$loginType = $data['login_type'] ?? 'upwd';
|
||||
$logic = new AuthLogic();
|
||||
|
||||
return match ($loginType) {
|
||||
'feishu' => $logic->feishuLogin($data),
|
||||
'qrcode' => $logic->qrCodeLogin($data),
|
||||
default => $logic->uPwdLogin($data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param array $data
|
||||
* @return User
|
||||
*/
|
||||
public function register(array $data): User
|
||||
{
|
||||
return (new AuthLogic())->register($data);
|
||||
}
|
||||
|
||||
|
||||
public function verifyToken(string $token): array
|
||||
{
|
||||
return (new AuthLogic())->verifyToken($token);
|
||||
}
|
||||
|
||||
public function refreshToken(string $refreshToken): array
|
||||
{
|
||||
return (new AuthLogic())->refreshToken($refreshToken);
|
||||
}
|
||||
|
||||
public function logout(?string $token)
|
||||
{
|
||||
(new AuthLogic())->logout($token);
|
||||
}
|
||||
|
||||
public function blockToken(string $token)
|
||||
{
|
||||
(new AuthLogic())->blockToken($token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\logic\BaseCrudLogic;
|
||||
use support\Request;
|
||||
|
||||
class CrudService
|
||||
{
|
||||
protected BaseCrudLogic $logic;
|
||||
|
||||
public function __construct(BaseCrudLogic $logic)
|
||||
{
|
||||
$this->logic = $logic;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表接口
|
||||
* 支持前端直接传递:
|
||||
* - filter_* → where
|
||||
* - sort_by, order
|
||||
* - fields (逗号分隔)
|
||||
* - with (逗号分隔)
|
||||
* - withFields (relation:field,field;...)
|
||||
* - dataLimitType, dataLimitField
|
||||
* - limit, page
|
||||
* - complex_query=1 则调用 complexQuery()
|
||||
*/
|
||||
public function list(Request $request): array
|
||||
{
|
||||
$p = $request->get();
|
||||
|
||||
// 复杂查询开关
|
||||
if (!empty($p['complex_query']) && method_exists($this, 'complexQuery')) {
|
||||
return $this->complexQuery($p);
|
||||
}
|
||||
|
||||
// 1. 构造 where
|
||||
$where = [];
|
||||
foreach ($p as $k => $v) {
|
||||
if (strpos($k, 'filter_') === 0 && $v !== '') {
|
||||
$col = substr($k, 7);
|
||||
// 简单等值或 like
|
||||
$where[$col] = strpos($v, '%') !== false ? ['like', $v] : $v;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 构造其他配置
|
||||
$config = [
|
||||
'where' => $where,
|
||||
'order' => [($p['sort_by'] ?? 'id') => ($p['order'] ?? 'desc')],
|
||||
'fields' => !empty($p['fields']) ? explode(',', $p['fields']) : [],
|
||||
'with' => !empty($p['with']) ? explode(',', $p['with']) : [],
|
||||
'withFields' => $this->parseWithFields($p['withFields'] ?? ''),
|
||||
'dataLimit' => !empty($p['dataLimitType']) ? [
|
||||
'type' => $p['dataLimitType'],
|
||||
'field' => $p['dataLimitField'] ?? 'admin_id'
|
||||
] : [],
|
||||
];
|
||||
|
||||
$limit = (int)($p['limit'] ?? 15);
|
||||
$page = (int)($p['page'] ?? 1);
|
||||
|
||||
$pager = $this->logic->list($config, $limit, $page);
|
||||
return ['total' => $pager->total(), 'data' => $pager->items()];
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
return $this->logic->show($id, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->post();
|
||||
$dataLimit = $this->getDataLimitFromRequest($request);
|
||||
return $this->logic->create($data, $dataLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
$key = $this->logic->modelClass::make()->getKeyName();
|
||||
$id = (int)$request->post($key);
|
||||
$data= $request->post();
|
||||
$dataLimit = $this->getDataLimitFromRequest($request);
|
||||
return $this->logic->updateByPk($id, $data, $dataLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$key = $this->logic->modelClass::make()->getKeyName();
|
||||
$ids = (array)$request->post($key, []);
|
||||
return $this->logic->deleteByPk($ids);
|
||||
}
|
||||
|
||||
/** ---------- helpers ---------- */
|
||||
|
||||
/**
|
||||
* 解析 withFields 参数 "rel:field,field;rel2:..."
|
||||
*/
|
||||
protected function parseWithFields(string $str): array
|
||||
{
|
||||
if (!$str) return [];
|
||||
$out = [];
|
||||
foreach (explode(';', $str) as $pair) {
|
||||
[$rel, $flds] = explode(':', $pair);
|
||||
$out[$rel] = explode(',', $flds);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求里读取 dataLimitType/Field
|
||||
*/
|
||||
protected function getDataLimitFromRequest(Request $req): array
|
||||
{
|
||||
$type = $req->get('dataLimitType');
|
||||
if (!$type) return [];
|
||||
return [
|
||||
'type' => $type,
|
||||
'field' => $req->get('dataLimitField', 'admin_id'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 子类可重写,做复杂联表查询等
|
||||
*/
|
||||
protected function complexQuery(array $params): array
|
||||
{
|
||||
throw new \LogicException('请在子类实现 complexQuery 方法');
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\traits;
|
||||
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* 数据转换器 Trait
|
||||
* 用于将原始数据(如模型、集合、分页等)统一转换为数组格式,
|
||||
* 并支持字段级与结果级的灵活后置加工。
|
||||
*/
|
||||
trait DataTransformer
|
||||
{
|
||||
/**
|
||||
* 核心方法:对任意数据进行转换并返回数组
|
||||
* 核心转换方法
|
||||
*
|
||||
* @param mixed $rawData 原始数据,可为 Paginator / Model / Collection / array 等
|
||||
* @param callable|null $postProcessor 可选的后置处理回调,接受处理完的数组再返回新的数组
|
||||
* @param callable|null $fieldFormatter 可选的字段命名转换回调,如 snake_case
|
||||
* @param mixed $rawData 原始数据:Model、Collection、Paginator、数组等
|
||||
* @param array $opts 可选配置:
|
||||
* - 'fieldFormatter' => callable 字段名格式化回调,如 fn($f)=>snake_case($f)
|
||||
* - 'postFormatter' => callable 结果级加工回调,接收整个数组并返回
|
||||
*
|
||||
* @return array
|
||||
* @return array 转换并加工后的纯 PHP 数组
|
||||
*/
|
||||
protected function transformData(
|
||||
mixed $rawData,
|
||||
?callable $postProcessor = null,
|
||||
?callable $postFormatter = null,
|
||||
?callable $fieldFormatter = null
|
||||
): array {
|
||||
try {
|
||||
$fieldFormatter = $fieldFormatter ?: fn($field) => $field;
|
||||
|
||||
if ($rawData instanceof LengthAwarePaginator) {
|
||||
$formatted = $this->formatPaginated($rawData, $postProcessor, $fieldFormatter);
|
||||
$arr = $this->formatPaginated($rawData, $postFormatter, $fieldFormatter);
|
||||
} elseif ($rawData instanceof Model) {
|
||||
$formatted = $this->formatModel($rawData, $fieldFormatter);
|
||||
$arr = $this->formatModel($rawData, $fieldFormatter);
|
||||
} elseif ($rawData instanceof Collection) {
|
||||
$formatted = $this->formatCollection($rawData, $fieldFormatter);
|
||||
$arr = $this->formatCollection($rawData, $fieldFormatter);
|
||||
} elseif (is_array($rawData)) {
|
||||
$formatted = $this->formatArray($rawData, $fieldFormatter);
|
||||
$arr = $this->formatArray($rawData, $fieldFormatter);
|
||||
} else {
|
||||
$formatted = (array) $rawData;
|
||||
$arr = (array)$rawData;
|
||||
}
|
||||
|
||||
if (is_callable($postProcessor)) {
|
||||
$formatted = $postProcessor($formatted);
|
||||
if (is_callable($postFormatter)) {
|
||||
$arr = $postFormatter($arr);
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
return $arr;
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'error' => true,
|
||||
@@ -52,84 +56,96 @@ trait DataTransformer
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化分页数据:将每条记录也进行 transformData 递归处理
|
||||
* 格式化分页数据:转换每条记录
|
||||
*
|
||||
* @param LengthAwarePaginator $paginator
|
||||
* @param callable|null $postProcessor
|
||||
* @param callable $fieldFormatter
|
||||
* @return array
|
||||
* @param LengthAwarePaginator $p
|
||||
* @param callable $fmt
|
||||
* @return array{records: array, total: int, current: int, size: int}
|
||||
*/
|
||||
protected function formatPaginated(
|
||||
LengthAwarePaginator $paginator,
|
||||
?callable $postProcessor,
|
||||
callable $fieldFormatter
|
||||
LengthAwarePaginator $p,
|
||||
?callable $postFormatter,
|
||||
callable $fmt
|
||||
): array {
|
||||
$list = $paginator->getCollection()->map(function ($item) use ($postProcessor, $fieldFormatter) {
|
||||
return $this->transformData($item, $postProcessor, $fieldFormatter);
|
||||
})->toArray();
|
||||
$records = $p->getCollection()
|
||||
->map(fn($item) => $this->transformData($item, $postFormatter, $fmt))
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'records' => $list,
|
||||
'total' => $paginator->total(),
|
||||
'current' => $paginator->currentPage(),
|
||||
'size' => $paginator->perPage()
|
||||
'records' => $records,
|
||||
'total' => $p->total(),
|
||||
'current' => $p->currentPage(),
|
||||
'size' => $p->perPage(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 Eloquent 模型:递归嵌套关联
|
||||
* 格式化 Eloquent 模型:主表属性 + 关联递归
|
||||
*
|
||||
* @param Model $model
|
||||
* @param callable $fieldFormatter
|
||||
* @param callable $fmt
|
||||
* @return array
|
||||
*/
|
||||
protected function formatModel(Model $model, callable $fieldFormatter): array
|
||||
{
|
||||
$formatted = [];
|
||||
|
||||
// 主表字段
|
||||
protected function formatModel(
|
||||
Model $model,
|
||||
callable $fmt
|
||||
): array {
|
||||
$out = [];
|
||||
// 1) 主表字段
|
||||
foreach ($model->getAttributes() as $key => $value) {
|
||||
$formatted[$fieldFormatter($key)] = $value;
|
||||
$out[$fmt($key)] = $value;
|
||||
}
|
||||
|
||||
// 关联数据,递归嵌套
|
||||
foreach ($model->getRelations() as $relation => $relationData) {
|
||||
if ($relationData instanceof Model) {
|
||||
// 单个关联模型
|
||||
$formatted[$relation] = $this->formatModel($relationData, $fieldFormatter);
|
||||
} elseif ($relationData instanceof Collection) {
|
||||
// 关联集合
|
||||
$formatted[$relation] = $relationData
|
||||
->map(fn($item) => $this->transformData($item, null, $fieldFormatter))
|
||||
// 2) 关联递归
|
||||
foreach ($model->getRelations() as $rel => $relData) {
|
||||
if ($relData instanceof Model) {
|
||||
$out[$rel] = $this->formatModel($relData, $fmt);
|
||||
} elseif ($relData instanceof Collection) {
|
||||
$out[$rel] = $relData
|
||||
->map(fn($item) => $this->transformData($item, null, $fmt))
|
||||
->toArray();
|
||||
} else {
|
||||
$out[$rel] = $relData;
|
||||
}
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化集合
|
||||
* 格式化集合数据:对每个元素递归调用 transformData
|
||||
*
|
||||
* @param Collection $collection
|
||||
* @param callable $fieldFormatter
|
||||
* @param Collection $c
|
||||
* @param callable $fmt
|
||||
* @return array
|
||||
*/
|
||||
protected function formatCollection(Collection $collection, callable $fieldFormatter): array
|
||||
{
|
||||
return $collection->map(fn($item) => $this->transformData($item, null, $fieldFormatter))
|
||||
protected function formatCollection(
|
||||
Collection $c,
|
||||
callable $fmt
|
||||
): array {
|
||||
return $c
|
||||
->map(fn($item) => $this->transformData($item, null, $fmt))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化普通数组
|
||||
* 格式化普通数组:递归处理可能出现的模型或集合
|
||||
*
|
||||
* @param array $array
|
||||
* @param callable $fieldFormatter
|
||||
* @param array $arr
|
||||
* @param callable $fmt
|
||||
* @return array
|
||||
*/
|
||||
protected function formatArray(array $array, callable $fieldFormatter): array
|
||||
{
|
||||
return array_map(fn($item) => $this->transformData($item, null, $fieldFormatter), $array);
|
||||
protected function formatArray(
|
||||
array $arr,
|
||||
callable $fmt
|
||||
): array {
|
||||
return array_map(function($item) use ($fmt) {
|
||||
if (is_array($item)
|
||||
|| $item instanceof Model
|
||||
|| $item instanceof Collection
|
||||
|| $item instanceof LengthAwarePaginator
|
||||
) {
|
||||
return $this->transformData($item, null, $fmt);
|
||||
}
|
||||
return $item;
|
||||
}, $arr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
namespace app\common\traits;
|
||||
|
||||
use support\Request;
|
||||
|
||||
trait FormatKvTrait
|
||||
{
|
||||
/**
|
||||
* 主入口:一行把前端扁平化参数变成完整的查询配置
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $modelClass Eloquent 模型类名
|
||||
* @return array{
|
||||
* model: string,
|
||||
* where: array,
|
||||
* order: array<string,string>,
|
||||
* fields: string[],
|
||||
* include: string[]
|
||||
* }
|
||||
*/
|
||||
protected function buildQueryConfig(Request $request, string $modelClass): array
|
||||
{
|
||||
$all = $request->all();
|
||||
|
||||
return [
|
||||
'model' => $modelClass,
|
||||
'where' => $this->parseFilters($all),
|
||||
'order' => $this->parseSort($all['sort'] ?? ''),
|
||||
'fields' => $this->parseCsv($all['fields'] ?? ''),
|
||||
'include' => $this->parseCsv($all['include'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 把除 sort/fields/include/page/limit 以外的所有参数
|
||||
* 智能转换为 where 条件
|
||||
*
|
||||
* 支持:
|
||||
* - 等值:foo=bar
|
||||
* - NOT:foo=!bar 或 foo=!bar1,bar2
|
||||
* - 范围:foo=2025-07-01,2025-07-15 → between
|
||||
* - 列表:foo=a,b,c → in
|
||||
* - NULL:foo=null
|
||||
* - NOT NULL:foo=!null
|
||||
* - 通配符:foo=xia* → like 'xia%'
|
||||
*
|
||||
* 关联字段直接写 relation.field=xxx
|
||||
*/
|
||||
protected function parseFilters(array $params): array
|
||||
{
|
||||
$where = [];
|
||||
$reserved = ['sort','fields','include','page','limit'];
|
||||
|
||||
foreach ($params as $key => $val) {
|
||||
if (in_array($key, $reserved, true)) {
|
||||
continue;
|
||||
}
|
||||
// NULL / NOT NULL
|
||||
if ($val === 'null') {
|
||||
$where[$key] = ['is null'];
|
||||
continue;
|
||||
}
|
||||
if ($val === '!null') {
|
||||
$where[$key] = ['is not null'];
|
||||
continue;
|
||||
}
|
||||
// NOT equal or NOT IN
|
||||
if (str_starts_with($val, '!')) {
|
||||
$v = substr($val, 1);
|
||||
if (str_contains($v, ',')) {
|
||||
$list = array_map('trim', explode(',', $v));
|
||||
$where[$key] = ['not in', $list];
|
||||
} else {
|
||||
$where[$key] = ['<>', $this->cast($v)];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// BETWEEN or IN
|
||||
if (str_contains($val, ',')) {
|
||||
$parts = array_map('trim', explode(',', $val));
|
||||
if (count($parts) === 2 && $this->isRangeValue($parts[0], $parts[1])) {
|
||||
$where[$key] = ['between', $parts];
|
||||
} else {
|
||||
$where[$key] = ['in', $this->castArray($parts)];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// LIKE
|
||||
if (str_contains($val, '*') || str_contains($val, '%')) {
|
||||
$pattern = str_replace('*', '%', $val);
|
||||
$where[$key] = ['like', $pattern];
|
||||
continue;
|
||||
}
|
||||
// 默认 EQ
|
||||
$where[$key] = $this->cast($val);
|
||||
}
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 sort=id,-created_at → ['id'=>'asc','created_at'=>'desc']
|
||||
*/
|
||||
protected function parseSort(string $sort): array
|
||||
{
|
||||
$order = [];
|
||||
foreach (explode(',', $sort) as $piece) {
|
||||
$f = trim($piece);
|
||||
if ($f === '') {
|
||||
continue;
|
||||
}
|
||||
if ($f[0] === '-') {
|
||||
$order[substr($f, 1)] = 'desc';
|
||||
} else {
|
||||
$order[$f] = 'asc';
|
||||
}
|
||||
}
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 逗号分割成数组
|
||||
*/
|
||||
protected function parseCsv(string $csv): array
|
||||
{
|
||||
if ($csv === '') {
|
||||
return [];
|
||||
}
|
||||
return array_filter(array_map('trim', explode(',', $csv)));
|
||||
}
|
||||
|
||||
/** 简单类型转换:全数字→int/float,其余保留原字符串 */
|
||||
protected function cast(string $v): mixed
|
||||
{
|
||||
if (is_numeric($v)) {
|
||||
return strpos($v, '.') !== false ? (float)$v : (int)$v;
|
||||
}
|
||||
return $v;
|
||||
}
|
||||
|
||||
/** 批量类型转换 */
|
||||
protected function castArray(array $arr): array
|
||||
{
|
||||
return array_map(fn($v) => $this->cast($v), $arr);
|
||||
}
|
||||
|
||||
/** 判断两端是否可做 BETWEEN(数字或 YYYY-MM-DD 日期) */
|
||||
protected function isRangeValue(string $a, string $b): bool
|
||||
{
|
||||
return (is_numeric($a) && is_numeric($b))
|
||||
|| (preg_match('/^\d{4}-\d{2}-\d{2}$/', $a)
|
||||
&& preg_match('/^\d{4}-\d{2}-\d{2}$/', $b));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\traits\crud;
|
||||
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* CRUD 操作控制器 Trait
|
||||
* 提供通用的增删改查控制器方法,通过调用 service 层实现业务逻辑解耦。
|
||||
*/
|
||||
trait CrudControllerTrait
|
||||
{
|
||||
/**
|
||||
* 获取资源列表(GET /resource)
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @return mixed 返回分页数据及总数
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$raw = $this->service->serviceList($request);
|
||||
$data = $this->transformData($raw, method_exists($this, 'indexFormat')
|
||||
? [$this, 'indexFormat']
|
||||
: null);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定资源详情(GET /resource/{id})
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @param int|string $id 资源 ID
|
||||
* @return mixed 返回资源详情数据
|
||||
*/
|
||||
public function show(Request $request)
|
||||
{
|
||||
$raw = $this->service->serviceShow($request);
|
||||
$data = $this->transformData($raw, method_exists($this, 'showFormat')
|
||||
? [$this, 'showFormat']
|
||||
: null);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新资源(POST /resource)
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @return mixed 返回创建成功的数据或信息
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$res = $this->service->serviceStore($request);
|
||||
return $this->success($res, '创建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定资源(PUT/PATCH /resource/{id})
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @return mixed 返回更新成功的数据或信息
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
$res = $this->service->serviceUpdate($request);
|
||||
return $this->success($res, '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定资源(DELETE /resource/{id})
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @return mixed 返回删除操作结果
|
||||
*/
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$count = $this->service->serviceDestroy($request);
|
||||
return $this->success([], "删除了 {$count} 条");
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 以下三个钩子方法,**在使用此 Trait 的 Controller** 中实现
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 字段名格式化:例如全部下划线
|
||||
*
|
||||
* @param string $field
|
||||
* @return string
|
||||
*/
|
||||
// protected function fieldFormatter(string $field): string
|
||||
// {
|
||||
// return snake_case($field);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 字段级后置加工规则
|
||||
*
|
||||
* @return array<string,callable>
|
||||
* 键为 “点号嵌套”或 “通配符” 字段路径,
|
||||
* 值为 fn($value, $row)=>mixed
|
||||
*/
|
||||
// protected function getFieldTransforms(): array
|
||||
// {
|
||||
// return [
|
||||
// 'name' => fn($v,$row)=>strtoupper($v),
|
||||
// 'orders.*.total' => fn($v,$row)=>number_format($v,2),
|
||||
// ];
|
||||
// }
|
||||
|
||||
/**
|
||||
* 结果级后置加工:接收整个数组,可任意增删改
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
// protected function postFormat(array $data): array
|
||||
// {
|
||||
// // 例如重命名 records 为 items
|
||||
// if (isset($data['records'])) {
|
||||
// $data['items'] = $data['records'];
|
||||
// unset($data['records']);
|
||||
// }
|
||||
// return $data;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
namespace app\common\traits\crud;
|
||||
|
||||
use app\common\utils\CrudUtil;
|
||||
|
||||
/**
|
||||
* Trait CrudLogicTrait
|
||||
*
|
||||
* 提供最基础的 CRUD 和复杂查询方法,基于 CrudUtil。
|
||||
* 使用此 Trait 的类,须定义:
|
||||
* protected string $modelClass = \App\common\model\YourModel::class;
|
||||
*/
|
||||
trait CrudLogicTrait
|
||||
{
|
||||
/**
|
||||
* 分页列表
|
||||
*/
|
||||
public function logicList(array $config, int $limit, int $page)
|
||||
{
|
||||
$config['model'] = $this->modelClass;
|
||||
return CrudUtil::paginate(CrudUtil::buildQuery($config), $limit, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量列表
|
||||
*/
|
||||
public function logicListAll(array $config)
|
||||
{
|
||||
$config['model'] = $this->modelClass;
|
||||
return CrudUtil::getAll(CrudUtil::buildQuery($config));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询单条
|
||||
*/
|
||||
public function logicShow(array $config)
|
||||
{
|
||||
return CrudUtil::findOneBy(CrudUtil::buildQuery($config));
|
||||
}
|
||||
|
||||
/**
|
||||
* 任意条件查询单条(可带 dataLimit)
|
||||
*/
|
||||
// public function logicShowBy(array $where, array $with = [], array $dataLimit = [])
|
||||
// {
|
||||
// return CrudUtil::findOneBy($this->modelClass, $where, $with, $dataLimit);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 创建(可带 dataLimit)
|
||||
*/
|
||||
public function logicCreate(array $data, array $dataLimit = [])
|
||||
{
|
||||
return CrudUtil::create($this->modelClass, $data, $dataLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主键更新(可带 dataLimit)
|
||||
*/
|
||||
// public function logicUpdateByPk($id, array $data, array $dataLimit = [])
|
||||
// {
|
||||
// return CrudUtil::updateByPk($this->modelClass, $id, $data, $dataLimit);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 任意条件更新(可带 dataLimit)
|
||||
*/
|
||||
public function logicUpdateBy(array $config, array $data)
|
||||
{
|
||||
return CrudUtil::updateBy(CrudUtil::buildQuery($config), $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主键删除(批量)
|
||||
*/
|
||||
// public function logicDeleteByPk(array $ids)
|
||||
// {
|
||||
// return CrudUtil::deleteByPk($this->modelClass, $ids);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 任意条件删除(可带 dataLimit)
|
||||
*/
|
||||
public function logicDeleteBy(array $config)
|
||||
{
|
||||
return CrudUtil::deleteBy(CrudUtil::buildQuery($config));
|
||||
}
|
||||
|
||||
/**
|
||||
* 复杂查询回调
|
||||
*/
|
||||
public function logicComplex(callable $callback)
|
||||
{
|
||||
return CrudUtil::executeComplex($this->modelClass, $callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
namespace app\common\traits\crud;
|
||||
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* Trait CrudServiceTrait
|
||||
*
|
||||
* 提供标准的增删改查 Service 接口,依赖 CrudLogicTrait。
|
||||
* 使用此 Trait 的 Service 类,须:
|
||||
* use CrudLogicTrait, CrudServiceTrait;
|
||||
* protected string $modelClass = \App\common\model\YourModel::class;
|
||||
*/
|
||||
trait CrudServiceTrait
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/** 具体 Service 里定义:protected string $modelClass = YourModel::class; */
|
||||
|
||||
/** 分页列表 */
|
||||
public function serviceList(Request $request)
|
||||
{
|
||||
if (method_exists($this, 'complexQuery')) {
|
||||
$result = $this->complexQuery($request->get());
|
||||
} else {
|
||||
$config = $this->buildQueryConfig($request, 'index');
|
||||
$limit = (int)$request->get('limit', 15);
|
||||
$page = (int)$request->get('page', 1);
|
||||
$result = $this->logicList($config, $limit, $page);
|
||||
}
|
||||
return $this->postProcess('List', $result);
|
||||
}
|
||||
|
||||
/** 全量列表 */
|
||||
public function serviceListAll(Request $request)
|
||||
{
|
||||
if (method_exists($this, 'complexQuery')) {
|
||||
$result = $this->complexQuery($request->get());
|
||||
} else {
|
||||
$config = $this->buildQueryConfig($request, 'default');
|
||||
$result = $this->logicListAll($config);
|
||||
}
|
||||
return $this->postProcess('List', $result);
|
||||
}
|
||||
|
||||
/** 查看单条 */
|
||||
public function serviceShow(Request $request)
|
||||
{
|
||||
$config = $this->buildQueryConfig($request, 'show');
|
||||
$item = $this->logicShow($config);
|
||||
return $this->postProcess('Show', $item);
|
||||
}
|
||||
|
||||
/** 创建 */
|
||||
public function serviceStore(Request $request)
|
||||
{
|
||||
$data = $request->post();
|
||||
$dl = $this->getDataLimitFromRequest($request);
|
||||
$created = $this->logicCreate($data, $dl);
|
||||
return $this->postProcess('Store', $created);
|
||||
}
|
||||
|
||||
/** 更新 */
|
||||
public function serviceUpdate(Request $request)
|
||||
{
|
||||
$data= $request->post();
|
||||
$config = $this->buildQueryConfig($request, 'update');
|
||||
$updated = $this->logicUpdateBy($config, $data);
|
||||
return $this->postProcess('Update', $updated);
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
public function serviceDestroy(Request $request)
|
||||
{
|
||||
$config = $this->buildQueryConfig($request, 'destroy');
|
||||
$deleted = $this->logicDeleteBy($config);
|
||||
return $this->postProcess('Destroy', $deleted);
|
||||
}
|
||||
|
||||
/** ----------------- 公共辅助方法 ----------------- */
|
||||
|
||||
/**
|
||||
* 构造 CrudUtil 的查询配置1
|
||||
*
|
||||
* 前端可以通过以下参数控制:
|
||||
* - filter_<column>=<value> → where
|
||||
* - sort_by=<column>&order=<asc|desc>
|
||||
* - fields=id,name,email → select
|
||||
* - with=roles,posts → 关联
|
||||
* - withFields=roles:id,name;... → 关联字段
|
||||
* - dataLimitType=personal|auth
|
||||
* - dataLimitField=admin_id
|
||||
*/
|
||||
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
|
||||
{
|
||||
$p = $request->all();
|
||||
|
||||
$config = [];
|
||||
$config['model'] = $this->modelClass;
|
||||
switch ($scene) {
|
||||
case 'default':
|
||||
$config['where']['status'] = 1;
|
||||
$config['order']['sort'] = ['desc'];
|
||||
$config['order']['id'] = ['desc'];
|
||||
break;
|
||||
case 'index':
|
||||
$config['where']['status'] = 1;
|
||||
$config['order']['sort'] = ['desc'];
|
||||
$config['order']['id'] = ['desc'];
|
||||
break;
|
||||
case 'show':
|
||||
$config['where']['id'] = $p['id'];
|
||||
$config['where']['status'] = 1;
|
||||
break;
|
||||
case 'store':
|
||||
break;
|
||||
case 'update':
|
||||
$config['where']['id'] = $p['id'];
|
||||
break;
|
||||
case 'destroy':
|
||||
$config['where']['id'] = $p['id'];
|
||||
break;
|
||||
}
|
||||
|
||||
return $config;
|
||||
|
||||
// // 1. where 从 filter_ 前缀自动映射
|
||||
// $where = [];
|
||||
// foreach ($p as $key => $val) {
|
||||
// if (strpos($key, 'filter_') === 0 && $val !== '') {
|
||||
// $col = substr($key, 7);
|
||||
// // 支持 %like% 查询
|
||||
// $where[$col] = strpos($val, '%') !== false
|
||||
// ? ['like', $val]
|
||||
// : $val;
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 2. 其余配置
|
||||
// return [
|
||||
// 'model' => $this->modelClass,
|
||||
// 'where' => $where,
|
||||
// 'order' => [
|
||||
// $p['sort_by'] ?? 'id' => $p['order'] ?? 'desc'
|
||||
// ],
|
||||
// 'fields' => !empty($p['fields'])
|
||||
// ? explode(',', $p['fields'])
|
||||
// : [],
|
||||
// 'with' => !empty($p['with'])
|
||||
// ? explode(',', $p['with'])
|
||||
// : [],
|
||||
// 'withFields' => $this->parseWithFields(
|
||||
// $p['withFields'] ?? ''
|
||||
// ),
|
||||
// 'dataLimit' => $this->getDataLimitFromRequest($request),
|
||||
// ];
|
||||
}
|
||||
|
||||
/** 解析 withFields 字符串 "rel:field,field;..." */
|
||||
protected function parseWithFields(string $str): array
|
||||
{
|
||||
if (!$str) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
foreach (explode(';', $str) as $pair) {
|
||||
[$rel, $flds] = explode(':', $pair);
|
||||
$out[$rel] = explode(',', $flds);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** 从请求读取 dataLimitType, dataLimitField */
|
||||
protected function getDataLimitFromRequest(Request $req): array
|
||||
{
|
||||
$type = $req->get('dataLimitType');
|
||||
if (!$type) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
'type' => $type,
|
||||
'field' => $req->get('dataLimitField', 'admin_id'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理复杂查询
|
||||
* 覆盖此方法,实现复杂查询逻辑
|
||||
*/
|
||||
// protected function complexQuery(array $params): array
|
||||
// {
|
||||
// throw new \LogicException('请在子类实现 complexQuery');
|
||||
// }
|
||||
|
||||
/**
|
||||
* 根据操作类型,调用对应的后置钩子
|
||||
*
|
||||
* @param string $op 操作:List, Show, Store, Update, Destroy
|
||||
* @param mixed $data 原始返回值
|
||||
* @return mixed 处理后的返回值
|
||||
*/
|
||||
protected function postProcess(string $op, mixed $data): mixed
|
||||
{
|
||||
$hook = "afterService{$op}";
|
||||
return method_exists($this, $hook)
|
||||
? $this->{$hook}($data)
|
||||
: $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
namespace app\common\traits;
|
||||
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* CrudServiceTrait
|
||||
*
|
||||
* 合并了:
|
||||
* - 扁平化 Request → 查询配置(where/with/order/fields/withFields/dataLimit)
|
||||
* - 标准增删改查 Service 接口(serviceList/Show/Store/Update/Destroy)
|
||||
* - 复杂查询入口(complexQuery)
|
||||
* - 统一后置钩子(postProcess)
|
||||
*
|
||||
* 使用方式:
|
||||
* class XxxService {
|
||||
* use CrudLogicTrait, CrudServiceTrait;
|
||||
* protected string $modelClass = \app\xxx\model\Xxx::class;
|
||||
* }
|
||||
*/
|
||||
trait CrudServiceTrait_bak2
|
||||
{
|
||||
// use CrudLogicTrait;
|
||||
|
||||
// ---------------- Service 接口 ---------------- //
|
||||
|
||||
/**
|
||||
* 列表接口
|
||||
* - 支持 complex_query=1 → 调用 complexQuery()
|
||||
* - 否则自动扁平化构建查询配置并分页调用 logicList()
|
||||
*/
|
||||
public function serviceList(Request $request)
|
||||
{
|
||||
if ($request->get('complex_query') && method_exists($this, 'complexQuery')) {
|
||||
$data = $this->complexQuery($request->get());
|
||||
} else {
|
||||
$cfg = $this->buildQueryConfig($request, 'list');
|
||||
$limit = (int)$request->get('limit', 15);
|
||||
$page = (int)$request->get('page', 1);
|
||||
$data = $this->logicList($cfg, $limit, $page);
|
||||
}
|
||||
return $this->postProcess('List', $data);
|
||||
}
|
||||
|
||||
/** 查看单条 */
|
||||
public function serviceShow(Request $request, $id)
|
||||
{
|
||||
$item = $this->logicShow($id, []);
|
||||
return $this->postProcess('Show', $item);
|
||||
}
|
||||
|
||||
/** 创建 */
|
||||
public function serviceStore(Request $request)
|
||||
{
|
||||
$data = $request->post();
|
||||
$dl = $this->getDataLimitFromRequest($request);
|
||||
$item = $this->logicCreate($data, $dl);
|
||||
return $this->postProcess('Store', $item);
|
||||
}
|
||||
|
||||
/** 更新 */
|
||||
public function serviceUpdate(Request $request)
|
||||
{
|
||||
$key = (new $this->modelClass)->getKeyName();
|
||||
$id = (int)$request->post($key);
|
||||
$data = $request->post();
|
||||
$dl = $this->getDataLimitFromRequest($request);
|
||||
$item = $this->logicUpdateByPk($id, $data, $dl);
|
||||
return $this->postProcess('Update', $item);
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
public function serviceDestroy(Request $request)
|
||||
{
|
||||
$key = (new $this->modelClass)->getKeyName();
|
||||
$ids = (array)$request->post($key, []);
|
||||
$result = $this->logicDeleteByPk($ids);
|
||||
return $this->postProcess('Destroy', $result);
|
||||
}
|
||||
|
||||
// ---------------- 统一后置钩子 ---------------- //
|
||||
|
||||
/**
|
||||
* 各操作后触发 afterService{Op}()
|
||||
* @param string $op List|Show|Store|Update|Destroy
|
||||
*/
|
||||
protected function postProcess(string $op, mixed $data): mixed
|
||||
{
|
||||
$hook = "afterService{$op}";
|
||||
return method_exists($this, $hook)
|
||||
? $this->{$hook}($data)
|
||||
: $data;
|
||||
}
|
||||
|
||||
// ---------------- 复杂查询入口 ---------------- //
|
||||
|
||||
/**
|
||||
* 子类 override 实现完全定制的复杂查询
|
||||
* @param array $params 全部请求参数
|
||||
*/
|
||||
protected function complexQuery(array $params)
|
||||
{
|
||||
throw new \LogicException('请在子类实现 complexQuery()');
|
||||
}
|
||||
|
||||
// ------------- 辅助:数据权限 & withFields 解析 ------------- //
|
||||
|
||||
/** 从请求读取 dataLimitType & dataLimitField → ['type'=>..,'field'=>..] */
|
||||
protected function getDataLimitFromRequest(Request $req): array
|
||||
{
|
||||
$type = $req->get('dataLimitType');
|
||||
if (!$type) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
'type' => $type,
|
||||
'field' => $req->get('dataLimitField', 'admin_id'),
|
||||
];
|
||||
}
|
||||
|
||||
/** 解析 withFields 字符串 "rel:field,field;rel2:..." */
|
||||
protected function parseWithFields(string $str): array
|
||||
{
|
||||
if (!$str) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
foreach (explode(';', $str) as $pair) {
|
||||
[$rel, $flds] = explode(':', $pair);
|
||||
$out[$rel] = explode(',', $flds);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
// ------------- 扁平化 Request → 查询配置 ------------- //
|
||||
|
||||
/**
|
||||
* 构建完整配置
|
||||
* 返回 [
|
||||
* where=>[], with=>[], order=>[], withFields=>?, fields=>?, dataLimit=>[]
|
||||
* ]
|
||||
*/
|
||||
protected function buildQueryConfig(Request $request, string $scene = 'list'): array
|
||||
{
|
||||
return [
|
||||
'where' => $this->buildFilters($request, $scene),
|
||||
'with' => $this->buildRelations($request, $scene),
|
||||
'order' => $this->buildOrder($request, $scene),
|
||||
'withFields' => $this->buildWithFields($request,$scene),
|
||||
'fields' => $this->buildFields($request, $scene),
|
||||
'dataLimit' => $this->buildDataLimit($request, $scene),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用合并方法:取公共配置 + 场景专属配置,若均为 array 则 array_merge
|
||||
*
|
||||
* @param mixed $common 公共配置
|
||||
* @param Request $request
|
||||
* @param string $scene
|
||||
* @param string $type Filters|Relations|Order|WithFields|Fields|DataLimit
|
||||
*/
|
||||
private function applySceneConfig(
|
||||
mixed $common,
|
||||
Request $request,
|
||||
string $scene,
|
||||
string $type
|
||||
): mixed {
|
||||
$method = 'apply' . ucfirst($scene) . $type;
|
||||
if (! method_exists($this, $method)) {
|
||||
return $common;
|
||||
}
|
||||
$sceneCfg = $this->{$method}($common, $request);
|
||||
if (is_array($common) && is_array($sceneCfg)) {
|
||||
return array_merge($common, $sceneCfg);
|
||||
}
|
||||
return $sceneCfg;
|
||||
}
|
||||
|
||||
/** 构建 WHERE:commonFilters + apply{Scene}Filters */
|
||||
protected function buildFilters(Request $r, string $scene): array
|
||||
{
|
||||
$common = $this->commonFilters($r);
|
||||
return (array)$this->applySceneConfig($common, $r, $scene, 'Filters');
|
||||
}
|
||||
|
||||
/** 构建 with 关联 */
|
||||
protected function buildRelations(Request $r, string $scene): array
|
||||
{
|
||||
$common = $this->commonRelations();
|
||||
return (array)$this->applySceneConfig($common, $r, $scene, 'Relations');
|
||||
}
|
||||
|
||||
/** 构建 order 排序 */
|
||||
protected function buildOrder(Request $r, string $scene): array
|
||||
{
|
||||
$common = $this->commonOrder();
|
||||
return (array)$this->applySceneConfig($common, $r, $scene, 'Order');
|
||||
}
|
||||
|
||||
/** 构建 withFields(关联模型字段白名单) */
|
||||
protected function buildWithFields(Request $r, string $scene): ?array
|
||||
{
|
||||
$common = [];
|
||||
return $this->applySceneConfig($common, $r, $scene, 'WithFields');
|
||||
}
|
||||
|
||||
/** 构建 fields(主表字段白名单) */
|
||||
protected function buildFields(Request $r, string $scene): ?array
|
||||
{
|
||||
$common = [];
|
||||
return $this->applySceneConfig($common, $r, $scene, 'Fields');
|
||||
}
|
||||
|
||||
/** 构建 dataLimit(数据权限) */
|
||||
protected function buildDataLimit(Request $r, string $scene): array
|
||||
{
|
||||
$common = [];
|
||||
return (array)$this->applySceneConfig($common, $r, $scene, 'DataLimit');
|
||||
}
|
||||
|
||||
|
||||
// ------------- 可重写的“公共 + 场景” 方法 ------------- //
|
||||
|
||||
// --- common* 系列(全局默认) ---
|
||||
protected function commonFilters(Request $r): array { return []; }
|
||||
protected function commonRelations(): array { return []; }
|
||||
protected function commonOrder(): array { return []; }
|
||||
|
||||
// // --- list* 系列 ---
|
||||
// protected function applyListFilters(array $c, Request $r): array { return $this->listFilters($r); }
|
||||
// protected function listFilters(Request $r): array { return []; }
|
||||
|
||||
// protected function applyListRelations(array $c, Request $r): array { return $this->listRelations(); }
|
||||
// protected function listRelations(): array { return []; }
|
||||
|
||||
// protected function applyListOrder(array $c, Request $r): array { return $this->listOrder($r); }
|
||||
// protected function listOrder(Request $r): array { return ['created_at'=>'desc']; }
|
||||
|
||||
// protected function applyListWithFields(array $c, Request $r): array{ return $this->listWithFields($r); }
|
||||
// protected function listWithFields(Request $r): array { return []; }
|
||||
|
||||
// protected function applyListFields(array $c, Request $r): array { return $this->listFields($r); }
|
||||
// protected function listFields(Request $r): array { return []; }
|
||||
|
||||
// protected function applyListDataLimit(array $c, Request $r): array { return $this->listDataLimit($r); }
|
||||
// protected function listDataLimit(Request $r): array { return []; }
|
||||
|
||||
// // --- detail* 系列 ---
|
||||
// protected function applyDetailFilters(array $c, Request $r): array { return $this->detailFilters($r); }
|
||||
// protected function detailFilters(Request $r): array { return ['id'=>$r->get('id')]; }
|
||||
|
||||
// protected function applyDetailRelations(array $c, Request $r): array { return $this->detailRelations(); }
|
||||
// protected function detailRelations(): array { return []; }
|
||||
|
||||
// protected function applyDetailOrder(array $c, Request $r): array { return $this->detailOrder($r); }
|
||||
// protected function detailOrder(Request $r): array { return []; }
|
||||
|
||||
// protected function applyDetailWithFields(array $c, Request $r): array{ return $this->detailWithFields($r); }
|
||||
// protected function detailWithFields(Request $r): array { return []; }
|
||||
|
||||
// protected function applyDetailFields(array $c, Request $r): array { return $this->detailFields($r); }
|
||||
// protected function detailFields(Request $r): array { return []; }
|
||||
|
||||
// protected function applyDetailDataLimit(array $c, Request $r): array { return $this->listDataLimit($r); }
|
||||
|
||||
// // --- export* 系列(如有需要可 override) ---
|
||||
// protected function applyExportFilters(array $c, Request $r): array { return $this->exportFilters($r); }
|
||||
// protected function exportFilters(Request $r): array { return []; }
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ class IndexController
|
||||
|
||||
protected $noNeedLogin = ['index', 'view', 'perm'];
|
||||
|
||||
protected $noNeedHeaderData = ['index', 'view', 'perm'];
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
return htmlpage();
|
||||
@@ -58,6 +60,7 @@ class IndexController
|
||||
Cache::delete("user_resources_{$data['user_id']}");
|
||||
return json(['code' => 0, 'msg' => 'success']);
|
||||
} catch (\Throwable $e) {
|
||||
var_dump($e);
|
||||
return json(['code' => 1, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,50 @@ use ReflectionClass;
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Response;
|
||||
use Webman\Http\Request;
|
||||
use app\common\service\BasicDataService;
|
||||
|
||||
class AuthCheck implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler) : Response
|
||||
{
|
||||
$controller = new ReflectionClass($request->controller);
|
||||
$noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];
|
||||
$properties = $controller->getDefaultProperties();
|
||||
$noNeedLogin = $properties['noNeedLogin'] ?? [];
|
||||
$noNeedAuth = $properties['noNeedAuth'] ?? [];
|
||||
$noNeedHeaderData = $properties['noNeedHeaderData'] ?? [];
|
||||
|
||||
if (in_array($request->action, $noNeedLogin)) {
|
||||
// 白名单
|
||||
// header数据
|
||||
if (!in_array($request->action, $noNeedHeaderData)) { //如果需要header数据
|
||||
try {
|
||||
$corpAbbr = $request->header('Fdesk-Corp-Abbr');
|
||||
$systemCode = $request->header('Fdesk-System-Code');
|
||||
$loginType = $request->header('Fdesk-Login-Type');
|
||||
$basicDataService = new BasicDataService();
|
||||
if ($basicDataService->checkAndCacheBaseParams($loginType, $systemCode, $corpAbbr)) {
|
||||
$request->login_type = $loginType;
|
||||
$request->system_code = $systemCode;
|
||||
$request->corp_abbr = $corpAbbr;
|
||||
$request->basic_data = [
|
||||
'login_type' => $loginType,
|
||||
'system_code' => $systemCode,
|
||||
'corp_abbr' => $corpAbbr
|
||||
];
|
||||
}
|
||||
return $handler($request);
|
||||
} else {
|
||||
} catch (\Throwable $th) {
|
||||
throw $th;
|
||||
$body = [
|
||||
'status' => 'fail',
|
||||
'code' => 401,
|
||||
'message' => $th->getMessage(),
|
||||
'data' => [],
|
||||
];
|
||||
return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']);
|
||||
}
|
||||
}
|
||||
|
||||
// 不登陆&不鉴权
|
||||
if (!in_array($request->action, $noNeedLogin)) { //如果需要登陆&鉴权
|
||||
try {
|
||||
$authHeader = $request->header('Fdesk-Auth-Token');
|
||||
if (!$authHeader || !preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
|
||||
@@ -31,6 +63,7 @@ class AuthCheck implements MiddlewareInterface
|
||||
}
|
||||
$token = $matches[1];
|
||||
$request->user = (new Jwt())->verifyToken($token);
|
||||
return $handler($request);
|
||||
} catch (\Throwable $e) {
|
||||
$body = [
|
||||
'status' => 'fail',
|
||||
@@ -40,7 +73,24 @@ class AuthCheck implements MiddlewareInterface
|
||||
];
|
||||
return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']);
|
||||
}
|
||||
}
|
||||
|
||||
// 登陆&不鉴权
|
||||
// if (!in_array($request->action, $noNeedAuth)) { //如果需要登陆&不鉴权
|
||||
// try {
|
||||
|
||||
// return $handler($request);
|
||||
// } catch (\Throwable $e) {
|
||||
// $body = [
|
||||
// 'status' => 'fail',
|
||||
// 'code' => 401,
|
||||
// 'message' => $e->getMessage(),
|
||||
// 'data' => [],
|
||||
// ];
|
||||
// return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']);
|
||||
// }
|
||||
// }
|
||||
|
||||
return $handler($request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
namespace app\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Response;
|
||||
use Webman\Http\Request;
|
||||
use app\service\AuthService;
|
||||
|
||||
class PermissionMiddleware implements MiddlewareInterface
|
||||
{
|
||||
protected $permission_code;
|
||||
|
||||
// 支持传参(如 "user:view"),可以在路由闭包里传入
|
||||
public function __construct($permission_code = null)
|
||||
{
|
||||
$this->permission_code = $permission_code;
|
||||
}
|
||||
|
||||
public function process(Request $request, callable $next): Response
|
||||
{
|
||||
// 假设有 auth() helper 方法能获取当前登录用户ID
|
||||
$user_id = $request->session()->get('user_id');
|
||||
if (!$user_id) {
|
||||
return json(['code' => 401, 'msg' => '未登录']);
|
||||
}
|
||||
|
||||
if ($this->permission_code && !AuthService::check($user_id, $this->permission_code)) {
|
||||
return json(['code' => 403, 'msg' => '没有权限']);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
return [
|
||||
'' => [
|
||||
\app\middleware\AccessControl::class,
|
||||
\app\middleware\BasicData::class,
|
||||
\app\middleware\AuthCheck::class
|
||||
]
|
||||
];
|
||||
@@ -58,6 +58,13 @@ Route::group('/admin', function () {
|
||||
Route::any('/update', [app\admin\controller\RoleController::class, 'update']);
|
||||
Route::any('/delete', [app\admin\controller\RoleController::class, 'delete']);
|
||||
});
|
||||
Route::group('/corp', function () {
|
||||
Route::any('/list', [app\admin\controller\CorpController::class, 'index']);
|
||||
Route::any('/show', [app\admin\controller\CorpController::class, 'show']);
|
||||
// Route::any('/create', [app\admin\controller\CorpController::class, 'store']);
|
||||
Route::any('/update', [app\admin\controller\CorpController::class, 'update']);
|
||||
// Route::any('/delete', [app\admin\controller\CorpController::class, 'destroy']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user