初始化本地仓库
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\service\admin\system;
|
||||
|
||||
use app\dao\system\AdminRoleDao;
|
||||
use core\base\BaseService;
|
||||
use core\exception\handler\AdminException;
|
||||
use support\Container;
|
||||
|
||||
/**
|
||||
* @author Mr.April
|
||||
* @since 1.0
|
||||
* @method query()
|
||||
*/
|
||||
class AdminRoleService extends BaseService
|
||||
{
|
||||
|
||||
public function __construct(AdminRoleDao $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户角色关联
|
||||
*
|
||||
* @param array $data 包含user_id和role_id_list的数组
|
||||
*
|
||||
* @throws AdminException
|
||||
*/
|
||||
public function save(array $data): void
|
||||
{
|
||||
try {
|
||||
$this->transaction(function () use ($data) {
|
||||
$userId = $data['id'] ?? 0;
|
||||
$newRoleIds = $data['role_id_list'] ?? [];
|
||||
|
||||
if ($userId <= 0) {
|
||||
throw new AdminException("用户ID不能为空");
|
||||
}
|
||||
|
||||
// 1. 获取用户现有角色ID
|
||||
$oldRoleIds = array_column(
|
||||
$this->query()->where('admin_id', $userId)->get()->toArray(),
|
||||
'role_id'
|
||||
);
|
||||
|
||||
// 2. 计算需要删除和新增的角色ID(差集计算)
|
||||
$rolesToDelete = array_diff($oldRoleIds, $newRoleIds);
|
||||
$rolesToAdd = array_diff($newRoleIds, $oldRoleIds);
|
||||
|
||||
// 3. 批量清理旧角色关联
|
||||
if (!empty($rolesToDelete)) {
|
||||
// 中间表批量删除
|
||||
$this->dao->getModel()
|
||||
->where('admin_id', $userId)
|
||||
->whereIn('role_id', $rolesToDelete)
|
||||
->delete();
|
||||
}
|
||||
|
||||
// 4. 批量添加新角色关联
|
||||
if (!empty($rolesToAdd)) {
|
||||
// 中间表批量插入
|
||||
$insertData = array_map(function ($roleId) use ($userId) {
|
||||
return ['admin_id' => $userId, 'role_id' => $roleId];
|
||||
}, $rolesToAdd);
|
||||
$this->dao->getModel()->insert($insertData);
|
||||
}
|
||||
|
||||
// 5. 清理用户权限缓存
|
||||
$currentUser = Container::make(\app\adminapi\CurrentUser::class);
|
||||
$currentUser->clearCache($userId);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException("角色授权失败:{$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除用户-关联角色
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function removeUserRole(array $data)
|
||||
{
|
||||
try {
|
||||
$this->transaction(function () use ($data) {
|
||||
$userIds = [];
|
||||
foreach ($data as $item) {
|
||||
$this->dao->getModel()->where($item)->delete();
|
||||
$userIds[] = $item['admin_id'];
|
||||
}
|
||||
|
||||
// 清理相关用户的权限缓存
|
||||
$userIds = array_unique($userIds);
|
||||
if (!empty($userIds)) {
|
||||
$currentUser = Container::make(\app\adminapi\CurrentUser::class);
|
||||
foreach ($userIds as $userId) {
|
||||
$currentUser->clearCache($userId);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户-关联角色
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return void
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function saveUserRoles(array $data): void
|
||||
{
|
||||
try {
|
||||
$this->transaction(function () use ($data) {
|
||||
$this->dao->saveAll($data);
|
||||
|
||||
// 清理相关用户的权限缓存
|
||||
$userIds = array_unique(array_column($data, 'admin_id'));
|
||||
if (!empty($userIds)) {
|
||||
$currentUser = Container::make(\app\adminapi\CurrentUser::class);
|
||||
foreach ($userIds as $userId) {
|
||||
$currentUser->clearCache($userId);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过角色ID获取用户ID列表
|
||||
*
|
||||
* @param int|string $roleId
|
||||
* @return array
|
||||
*/
|
||||
public function getUserIdsByRoleId(int|string $roleId): array
|
||||
{
|
||||
return $this->dao->getModel()
|
||||
->where('role_id', $roleId)
|
||||
->pluck('admin_id')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,723 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\service\admin\system;
|
||||
|
||||
use app\adminapi\event\LoginLogEvent;
|
||||
use app\dao\system\AdminDao;
|
||||
use app\enum\system\PolicyPrefix;
|
||||
use app\model\system\Admin;
|
||||
use app\scope\global\AccessPermissionScope;
|
||||
use app\service\admin\logs\LoginLogService;
|
||||
use core\base\BaseService;
|
||||
use core\cache\CacheService;
|
||||
use core\exception\handler\AdminException;
|
||||
use core\jwt\JwtToken;
|
||||
use core\tool\RSAService;
|
||||
use support\Container;
|
||||
use Webman\Event\Event;
|
||||
|
||||
/**
|
||||
* @method getAdminInfo(string $username)
|
||||
* @method getAdminById($uid, $withoutScopes = null)
|
||||
* @method getList(mixed $where, mixed $field, mixed $page, mixed $limit, mixed $order, array $array, false $false)
|
||||
* @method getUsersListByRoleId(mixed $where, mixed $field, mixed $page, mixed $limit)
|
||||
* @method getAdminByName(string $username, array|null $withoutScopes)
|
||||
*/
|
||||
class AdminService extends BaseService
|
||||
{
|
||||
|
||||
public function __construct(AdminDao $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* save
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return Admin|null
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function save(array $data): Admin|null
|
||||
{
|
||||
try {
|
||||
return $this->transaction(function () use ($data) {
|
||||
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
$roles = $this->normalizeRelationIds($data['role_id_list'] ?? []);
|
||||
$posts = $this->normalizeRelationIds($data['post_id_list'] ?? []);
|
||||
$depts = $this->normalizeRelationIds($data['dept_id_list'] ?? []);
|
||||
$mainDeptId = $data['main_dept_id'] ?? null;
|
||||
$mainPosId = $data['main_post_id'] ?? null;
|
||||
unset($data['role_id_list'], $data['post_id_list'], $data['dept_id_list'], $data['main_dept_id'], $data['main_post_id']);
|
||||
$model = $this->dao->save($data);
|
||||
|
||||
$this->updateModel($model, $data, $depts, $posts, $roles);
|
||||
$this->syncRoles($model, $roles);
|
||||
$this->syncMainInfo($model, $mainDeptId, $mainPosId);
|
||||
return $model;
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param int|string $id
|
||||
* @param array $data
|
||||
*
|
||||
* @return \app\model\system\Admin|null
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function update(int|string $id, array $data): ?Admin
|
||||
{
|
||||
try {
|
||||
return $this->transaction(function () use ($id, $data) {
|
||||
$this->updatePasswordIfNeeded($data);
|
||||
$roles = $this->normalizeRelationIds($data['role_id_list'] ?? []);
|
||||
$posts = $this->normalizeRelationIds($data['post_id_list'] ?? []);
|
||||
$depts = $this->normalizeRelationIds($data['dept_id_list'] ?? []);
|
||||
$mainDeptId = $data['main_dept_id'] ?? null;
|
||||
$mainPosId = $data['main_post_id'] ?? null;
|
||||
unset($data['role_id_list'], $data['post_id_list'], $data['dept_id_list'], $data['main_dept_id'], $data['main_post_id']);
|
||||
$model = $this->dao->getModel()
|
||||
->findOrFail($id);
|
||||
$this->updateModel($model, $data, $depts, $posts, $roles);
|
||||
$this->syncRoles($model, $roles);
|
||||
$this->syncMainInfo($model, $mainDeptId, $mainPosId);
|
||||
return $model;
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeRelationIds(mixed $value): array
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return array_values(array_filter($value, static fn ($item) => $item !== null && $item !== ''));
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
return array_values(array_filter(array_map('trim', explode(',', $value)), static fn ($item) => $item !== ''));
|
||||
}
|
||||
|
||||
if ($value === null || $value === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$value];
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码处理
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
private function updatePasswordIfNeeded(array &$data): void
|
||||
{
|
||||
if (isset($data['password'])) {
|
||||
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新-部门职位关系
|
||||
*
|
||||
* @param \app\model\system\Admin $model
|
||||
* @param array $data
|
||||
* @param array $depts
|
||||
* @param array $posts
|
||||
* @param array $roles
|
||||
*/
|
||||
private function updateModel(Admin $model, array $data, array $depts, array $posts, array $roles): void
|
||||
{
|
||||
$model->fill($data);
|
||||
$model->save();
|
||||
$model->depts()->sync($depts);
|
||||
$model->posts()->sync($posts);
|
||||
$model->roles()->sync($roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步-角色
|
||||
*
|
||||
* @param \app\model\system\Admin $model
|
||||
* @param array $roles
|
||||
*/
|
||||
private function syncRoles(Admin $model, array $roles): void
|
||||
{
|
||||
$model->roles()->sync($roles);
|
||||
}
|
||||
|
||||
private function syncMainInfo(Admin $model, ?string $mainDeptId, ?string $mainPosId): void
|
||||
{
|
||||
$mainInfo = \app\model\system\AdminMain::updateOrCreate(
|
||||
['admin_id' => (string)$model->id],
|
||||
[
|
||||
'main_dept_id' => $mainDeptId,
|
||||
'main_post_id' => $mainPosId,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* destroy
|
||||
*
|
||||
* @param $id
|
||||
* @param $force
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function destroy($id, $force): mixed
|
||||
{
|
||||
$ret = $this->dao->count([['id', 'in', $id], ['is_super', '=', 1]]);
|
||||
if ($ret > 0) {
|
||||
throw new AdminException('系统内置用户,不允许删除');
|
||||
}
|
||||
return $this->dao->destroy($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户-冻结
|
||||
*
|
||||
* @param array|string $id
|
||||
*/
|
||||
public function locked(array|string $id): void
|
||||
{
|
||||
try {
|
||||
if (is_string($id)) {
|
||||
$id = array_map('trim', explode(',', $id));
|
||||
}
|
||||
$ret = $this->dao->count([['id', 'in', $id], ['is_super', '=', 1]]);
|
||||
if ($ret > 0) {
|
||||
throw new AdminException('系统内置用户,不允许冻结');
|
||||
}
|
||||
$this->dao->batchUpdate($id, ['is_locked' => 1]);
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户-解除冻结
|
||||
*
|
||||
* @param array|string $id
|
||||
*/
|
||||
public function unLocked(array|string $id): void
|
||||
{
|
||||
try {
|
||||
if (is_string($id)) {
|
||||
$id = array_map('trim', explode(',', $id));
|
||||
}
|
||||
$this->dao->batchUpdate($id, ['is_locked' => 0]);
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param string $type
|
||||
* @param string $grantType
|
||||
* @param array $params
|
||||
*
|
||||
* @return array
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function login(string $username, string $password = '', string $type = 'admin', string $grantType = 'default', array $params = []): array
|
||||
{
|
||||
$adminInfo = $this->getAdminByName($username, [AccessPermissionScope::class]);
|
||||
$this->validateAdminStatus($adminInfo);
|
||||
$decryptedPassword = $this->validateRsaKeys($params['key_id'], $password);
|
||||
// $decryptedPassword= $password;//临时注释关闭RSA验证要不无法调试
|
||||
$this->validatePassword($adminInfo, $decryptedPassword, $grantType);
|
||||
[$userInfo, $token] = $this->generateTokenData($adminInfo, $type);
|
||||
$this->emitLoginSuccessEvent(array_merge($userInfo, $token), $tenant?->id ?? null);
|
||||
return $token ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 第三方应用登录
|
||||
*
|
||||
* @param mixed $acctId
|
||||
* @param mixed $appId
|
||||
* @param mixed $appSecret
|
||||
* @param mixed $userName
|
||||
* @param string $type
|
||||
*
|
||||
* @return array
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function thirdPartyLogin(mixed $acctId, mixed $appId, mixed $appSecret, mixed $userName, string $type = 'admin'): array
|
||||
{
|
||||
$adminInfo = $this->getAdminByName($userName);
|
||||
$this->validateAdminStatus($adminInfo);
|
||||
$this->validateThirdPartyApp($acctId, $appId, $appSecret);
|
||||
[$userInfo, $token] = $this->generateTokenData($adminInfo, $type);
|
||||
$this->emitLoginSuccessEvent(array_merge($userInfo, $token), $tenant?->id ?? null);
|
||||
return $token ?? [];
|
||||
}
|
||||
|
||||
private function validateThirdPartyApp(mixed $acctId, mixed $appId, mixed $appSecret)
|
||||
{
|
||||
|
||||
if ($acctId !== config('app.acct_id')) {
|
||||
throw new AdminException('第三方应用不存在或已删除');
|
||||
}
|
||||
if ($appId !== config('app.app_id')) {
|
||||
throw new AdminException('第三方应用ID错误');
|
||||
}
|
||||
if ($appSecret !== config('app.app_secret')) {
|
||||
throw new AdminException('第三方应用ID或密钥错误');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证管理员状态
|
||||
*
|
||||
* @param \app\model\system\Admin|null $adminInfo
|
||||
*
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
private function validateAdminStatus(?Admin $adminInfo): void
|
||||
{
|
||||
if (!$adminInfo) {
|
||||
throw new AdminException('账号或密码错误,请重新输入!');
|
||||
}
|
||||
if ($adminInfo->enabled === 0) {
|
||||
throw new AdminException('您已被禁止登录!');
|
||||
}
|
||||
if ($adminInfo->is_locked === 1) {
|
||||
throw new AdminException('您的账号已被锁定,禁止登录!');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
*
|
||||
* @param \app\model\system\Admin $adminInfo
|
||||
* @param string $password
|
||||
* @param string $grantType
|
||||
*
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
private function validatePassword(Admin $adminInfo, string $password, string $grantType): void
|
||||
{
|
||||
if (!in_array($grantType, ['sms', 'refresh_token']) && !password_verify($password, $adminInfo->password)) {
|
||||
$msg = '账号或密码错误,请重新输入!';
|
||||
$this->emitLoginFailedEvent($adminInfo->toArray(), $msg);
|
||||
throw new AdminException($msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* token生成
|
||||
*
|
||||
* @param \app\model\system\Admin $adminInfo
|
||||
* @param string $type
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function generateTokenData(Admin $adminInfo, string $type): array
|
||||
{
|
||||
$loginIp = request()->getRealIp();
|
||||
$userAgent = request()->header('user-agent', '');
|
||||
$browser = $this->getBrowser($userAgent);
|
||||
$os = $this->getOs($userAgent);
|
||||
$ipLocation = $this->getIpLocation($loginIp);
|
||||
|
||||
$adminInfo->login_time = time();
|
||||
$adminInfo->login_ip = $loginIp;
|
||||
$adminInfo->save();
|
||||
$userInfo = $adminInfo->makeHidden([
|
||||
'password',
|
||||
'backend_setting',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'created_at',
|
||||
'deleted_at',
|
||||
'remark',
|
||||
'created_date',
|
||||
'updated_date',
|
||||
])->toArray();
|
||||
|
||||
// 追加扩展数据到 userInfo(用于 JWT extra)
|
||||
$userInfo['ip'] = $loginIp;
|
||||
$userInfo['ip_location'] = $ipLocation;
|
||||
$userInfo['browser'] = $browser;
|
||||
$userInfo['os'] = $os;
|
||||
|
||||
// 使用新的 JwtToken 生成 token
|
||||
$jwt = new JwtToken();
|
||||
$tokenObj = $jwt->generate((string)$adminInfo->id, $type, $userInfo);
|
||||
|
||||
$token = [
|
||||
'access_token' => $tokenObj->accessToken,
|
||||
'refresh_token' => $tokenObj->refreshToken,
|
||||
'expires_in' => $tokenObj->expiresIn,
|
||||
'client_id' => $this->generateUniqueId($loginIp),
|
||||
'expires_time' => time() + $tokenObj->expiresIn
|
||||
];
|
||||
|
||||
return [$userInfo, $token];
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功-事件发起
|
||||
*
|
||||
* @param array $tokenData
|
||||
* @param int|null $tenantId
|
||||
*/
|
||||
private function emitLoginSuccessEvent(array $tokenData, ?int $tenantId = null): void
|
||||
{
|
||||
$loginIp = request()->getRealIp();
|
||||
$event = new LoginLogEvent(
|
||||
'登录成功',
|
||||
request()->app,
|
||||
$loginIp,
|
||||
$this->getIpLocation($loginIp),
|
||||
$this->getBrowser(request()->header('user-agent', '')),
|
||||
$this->getOs(request()->header('user-agent', '')),
|
||||
0,
|
||||
'登录成功',
|
||||
$tokenData['user_name'],
|
||||
$tokenData['id'],
|
||||
time(),
|
||||
$tokenData['access_token'],
|
||||
$tokenData['expires_time']
|
||||
);
|
||||
$event->dispatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录失败-事件发起
|
||||
*
|
||||
* @param array $adminInfo
|
||||
* @param string $message
|
||||
*/
|
||||
private function emitLoginFailedEvent(array $adminInfo, string $message): void
|
||||
{
|
||||
$loginIp = request()->getRealIp();
|
||||
$event = new LoginLogEvent(
|
||||
'登录失败',
|
||||
request()->app,
|
||||
$loginIp,
|
||||
$this->getIpLocation($loginIp),
|
||||
$this->getBrowser(request()->header('user-agent', '')),
|
||||
$this->getOs(request()->header('user-agent', '')),
|
||||
-1,
|
||||
$message,
|
||||
$adminInfo['user_name'],
|
||||
$adminInfo['id'],
|
||||
time(),
|
||||
'',
|
||||
time()
|
||||
);
|
||||
$event->dispatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器信息
|
||||
*
|
||||
* @param string $userAgent
|
||||
* @return string
|
||||
*/
|
||||
private function getBrowser(string $userAgent): string
|
||||
{
|
||||
$br = 'Unknown';
|
||||
if (preg_match('/MSIE/i', $userAgent)) {
|
||||
$br = 'MSIE';
|
||||
} elseif (preg_match('/Firefox/i', $userAgent)) {
|
||||
$br = 'Firefox';
|
||||
} elseif (preg_match('/Edg/i', $userAgent)) {
|
||||
$br = 'Edge';
|
||||
} elseif (preg_match('/OPR|Opera/i', $userAgent)) {
|
||||
$br = 'Opera';
|
||||
} elseif (preg_match('/Chrome/i', $userAgent)) {
|
||||
$br = 'Chrome';
|
||||
} elseif (preg_match('/Safari/i', $userAgent)) {
|
||||
$br = 'Safari';
|
||||
} else {
|
||||
$br = 'Other';
|
||||
}
|
||||
return $br;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作系统信息
|
||||
*
|
||||
* @param string $userAgent
|
||||
* @return string
|
||||
*/
|
||||
private function getOs(string $userAgent): string
|
||||
{
|
||||
$os = 'Unknown';
|
||||
if (preg_match('/win/i', $userAgent)) {
|
||||
$os = 'Windows';
|
||||
} elseif (preg_match('/mac/i', $userAgent)) {
|
||||
$os = 'Mac';
|
||||
} elseif (preg_match('/linux/i', $userAgent)) {
|
||||
$os = 'Linux';
|
||||
} else {
|
||||
$os = 'Other';
|
||||
}
|
||||
return $os;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取IP归属地
|
||||
*
|
||||
* @param string $ip
|
||||
* @return string
|
||||
*/
|
||||
private function getIpLocation(string $ip): string
|
||||
{
|
||||
// 本地IP或无效IP直接返回
|
||||
if (empty($ip) || in_array($ip, ['127.0.0.1', '::1', 'localhost', '0.0.0.0'])) {
|
||||
return '本地';
|
||||
}
|
||||
|
||||
// 使用新浪API获取IP归属地
|
||||
try {
|
||||
// 创建Guzzle HTTP客户端
|
||||
$client = new \GuzzleHttp\Client([
|
||||
'timeout' => 2,
|
||||
'connect_timeout' => 1,
|
||||
'verify' => false
|
||||
]);
|
||||
|
||||
$url = "http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=json&ip=" . $ip;
|
||||
$response = $client->get($url);
|
||||
|
||||
$content = $response->getBody()->getContents();
|
||||
|
||||
$data = json_decode($content, true);
|
||||
if (isset($data['ret']) && $data['ret'] === 1 && !empty($data['city'])) {
|
||||
$location = $data['city'];
|
||||
if (!empty($data['province']) && strpos($data['province'], $data['city']) === false) {
|
||||
$location = $data['province'] . ' ' . $location;
|
||||
}
|
||||
return $location;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 忽略异常
|
||||
}
|
||||
|
||||
return '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*
|
||||
* @param string|int $id
|
||||
* @param array $data
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function updateUserInfo(string|int $id, array $data): mixed
|
||||
{
|
||||
return $this->dao->update(['id' => $id], $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户头像
|
||||
*
|
||||
* @param string|int $id
|
||||
* @param string $avatar
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function updateAvatarUser(string|int $id, string $avatar): mixed
|
||||
{
|
||||
return $this->dao->update(['id' => $id], ['avatar' => $this->getPathFromUrl($avatar)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新个人用户密码
|
||||
*
|
||||
* @param string|int $id
|
||||
* @param array $data
|
||||
*
|
||||
* @return void
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function updateUserPwd(string|int $id, array $data): void
|
||||
{
|
||||
$this->transaction(function () use ($id, $data) {
|
||||
$info = $this->dao->getModel()->where('id', $id)->first();
|
||||
if (empty($info)) {
|
||||
throw new AdminException('用户不存在或被删除');
|
||||
}
|
||||
if (!password_verify($data['old_password'], $info->password)) {
|
||||
throw new AdminException('旧密码错误,请重新输入!');
|
||||
}
|
||||
return $this->dao->update(['id' => $id], ['password' => $this->passwordHash($data['new_password'])]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新个人偏好设置
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function updateUserPreferences(string|int $id, array $data = []): void
|
||||
{
|
||||
$this->transaction(function () use ($id, $data) {
|
||||
unset($data['id']);
|
||||
return $this->dao->update(['id' => $id], ['backend_setting' => $data]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制下线
|
||||
*
|
||||
* @param $token
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function kickoutByTokenValueUser($token): void
|
||||
{
|
||||
$this->transaction(function () use ($token) {
|
||||
/** @var LoginLogService $systemLoginLogService */
|
||||
$systemLoginLogService = Container::make(LoginLogService::class);
|
||||
// 使用 firstOrFail 避免空值检查
|
||||
$loginLog = $systemLoginLogService->getModel()
|
||||
->where('key', $token)
|
||||
->firstOrFail();
|
||||
|
||||
// 批量更新字段
|
||||
$loginLog->update([
|
||||
'expires_at' => time(), // 使用 Carbon 时间
|
||||
'remark' => '强制下线',
|
||||
'updated_at' => time(), // 确保更新时间戳
|
||||
]);
|
||||
|
||||
// 添加Token 致黑名单
|
||||
$result = JwtToken::addToBlacklist($token, true);
|
||||
if (!$result) {
|
||||
throw new AdminException('操作失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除关联源-并清理残留关联数据
|
||||
*
|
||||
* @param array $ids
|
||||
*
|
||||
* @return array
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function batchDelete(array $ids): array
|
||||
{
|
||||
try {
|
||||
return $this->transaction(function () use ($ids) {
|
||||
// 1. 验证:禁止删除超级管理员
|
||||
$superUserCount = $this->dao->getModel()->whereIn('id', $ids)
|
||||
->where('is_super', 1)
|
||||
->count();
|
||||
if ($superUserCount > 0) {
|
||||
throw new AdminException('系统内置用户不允许删除');
|
||||
}
|
||||
|
||||
// 2. 关联同步:通过模型关联清理角色中间表数据
|
||||
|
||||
$admins = $this->dao->getModel()->whereIn('id', $ids)->get();
|
||||
foreach ($admins as $admin) {
|
||||
// 解除角色关联
|
||||
$admin->roles()->detach();
|
||||
$admin->depts()->detach();
|
||||
}
|
||||
|
||||
// 4. 执行管理员数据删除
|
||||
$deleteCount = $this->dao->destroy($ids);
|
||||
if ($deleteCount <= 0) {
|
||||
throw new AdminException('删除失败,未找到有效用户');
|
||||
}
|
||||
return ['id' => $ids];
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一UUID
|
||||
*
|
||||
* @param string|null $currentIp
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function generateUniqueId(string|null $currentIp = null): string
|
||||
{
|
||||
if (empty($currentIp)) {
|
||||
$currentIp = request()->getRemoteIp();
|
||||
}
|
||||
return uniqid($currentIp . '-', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 入参移除url
|
||||
*
|
||||
* @param string $url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getPathFromUrl(string $url): string
|
||||
{
|
||||
// 检测协议头存在性
|
||||
if (!preg_match('#^https?://#i', $url)) {
|
||||
return $url; // 非http(s)协议直接原样返回
|
||||
}
|
||||
// 支持子域名和端口号
|
||||
$pattern = '#^https?://
|
||||
(?:[^/@]+@)? // 忽略用户认证信息
|
||||
(?:[^/:]+|\[[^\]]+\]) // 匹配域名或IPv6地址
|
||||
(?::\d+)? // 忽略端口号
|
||||
(/[^\?#]*) // 精确捕获路径部分
|
||||
#x';
|
||||
preg_match($pattern, $url, $matches);
|
||||
return $matches[1] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验密钥
|
||||
*
|
||||
* @param $keyId
|
||||
* @param $encryptedPassword
|
||||
*
|
||||
* @return string
|
||||
* @throws AdminException
|
||||
*/
|
||||
private function validateRsaKeys($keyId, $encryptedPassword): string
|
||||
{
|
||||
$cache = Container::make(CacheService::class, []);
|
||||
$privateKey = $cache->get("rsa_private_key_$keyId");
|
||||
if (!$privateKey) {
|
||||
throw new AdminException('私钥不存在或已过期,请刷新页面重试');
|
||||
}
|
||||
return RSAService::decrypt($encryptedPassword, $privateKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\service\admin\system;
|
||||
|
||||
use app\adminapi\CurrentUser;
|
||||
use app\dao\system\AdminDao;
|
||||
use core\base\BaseService;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use support\Container;
|
||||
|
||||
class AuthService extends BaseService
|
||||
{
|
||||
|
||||
public function __construct(AdminDao $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单
|
||||
*
|
||||
* @param \app\adminapi\CurrentUser $currentUser
|
||||
* @param bool $includeButtons
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getMenusByUserRoles(CurrentUser $currentUser, bool $includeButtons = false): ?Collection
|
||||
{
|
||||
$adminModel = $currentUser->admin();
|
||||
if (!$adminModel) {
|
||||
return new Collection();
|
||||
}
|
||||
|
||||
$isSuperAdmin = boolval($adminModel->getAttribute('is_super'));
|
||||
/** @var MenuService $menuService */
|
||||
$menuService = Container::make(MenuService::class);
|
||||
$map1 = $includeButtons ? ['type' => [1, 2, 3, 4]] : ['type' => [1, 2]];
|
||||
|
||||
if ($isSuperAdmin) {
|
||||
// 顶级管理员返回全部菜单
|
||||
return $menuService->selectList($map1, '*', 0, 0, 'sort', [], true);
|
||||
}
|
||||
|
||||
// 普通成员 - 通过角色获取菜单
|
||||
$menuIds = $this->getUserMenuIds($adminModel);
|
||||
return $this->getMenusByIds($menuService, array_unique($menuIds), $includeButtons);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户拥有的菜单ID列表
|
||||
*
|
||||
* @param \app\model\system\Admin $adminModel
|
||||
* @return array 菜单ID数组
|
||||
*/
|
||||
private function getUserMenuIds($adminModel): array
|
||||
{
|
||||
$menuIds = [];
|
||||
|
||||
// 通过角色获取菜单
|
||||
$roles = $adminModel->roles()->with('menus')->get();
|
||||
|
||||
foreach ($roles as $role) {
|
||||
foreach ($role->menus as $menu) {
|
||||
$menuIds[] = $menu->id;
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($menuIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据IDS输出菜单
|
||||
*
|
||||
* @param \app\service\admin\system\MenuService $menuService
|
||||
* @param array $ids
|
||||
* @param bool $includeButtons
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function getMenusByIds(MenuService $menuService, array $ids, bool $includeButtons = false): Collection
|
||||
{
|
||||
if (empty($ids)) {
|
||||
return new Collection();
|
||||
}
|
||||
$menuModel = $menuService->dao->getModel();
|
||||
$chunkSize = 200;
|
||||
$chunks = array_chunk($ids, $chunkSize);
|
||||
$typeFilter = function ($query) use ($includeButtons) {
|
||||
$types = $includeButtons ? [1, 2, 3, 4] : [1, 2];
|
||||
$query->whereIn('type', $types);
|
||||
};
|
||||
|
||||
// 分块查询 + 批量合并(减少内存占用)
|
||||
$allResults = new Collection();
|
||||
foreach ($chunks as $chunk) {
|
||||
$results = $menuModel
|
||||
->whereIn('id', $chunk)
|
||||
->where('enabled', 1)
|
||||
->where($typeFilter)
|
||||
->orderBy('sort')
|
||||
->get();
|
||||
$allResults = $allResults->merge($results);
|
||||
}
|
||||
return $allResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户角色-权限码
|
||||
*
|
||||
* @param \app\adminapi\CurrentUser $currentUser
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCodesByUserRoles(CurrentUser $currentUser): array
|
||||
{
|
||||
$adminModel = $currentUser->admin();
|
||||
if (!$adminModel) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$isSuperAdmin = boolval($adminModel->getAttribute('is_super'));
|
||||
if ($isSuperAdmin) {
|
||||
// 顶级管理员
|
||||
return ['admin'];
|
||||
}
|
||||
|
||||
// 普通成员 - 通过角色获取权限码
|
||||
return $currentUser->getPermissions();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: https://madong.tech
|
||||
*/
|
||||
|
||||
namespace app\service\admin\system;
|
||||
|
||||
use app\enum\system\PolicyPrefix;
|
||||
use core\base\BaseService;
|
||||
use core\casbin\Permission;
|
||||
use core\exception\handler\AdminException;
|
||||
|
||||
class CasbinService extends BaseService
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->dao = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新casbin 策略
|
||||
*
|
||||
* @param array $currentPermissions
|
||||
* @param array $incomingPolicies
|
||||
*
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function updateCasbinPolicies(array $currentPermissions, array $incomingPolicies): void
|
||||
{
|
||||
// 计算需要删除的权限:当前有但传入没有的
|
||||
$permissionsToDelete = array_filter($currentPermissions, function ($currentPolicy) use ($incomingPolicies) {
|
||||
foreach ($incomingPolicies as $incomingPolicy) {
|
||||
if (
|
||||
(string)$currentPolicy[0] === (string)$incomingPolicy[0] &&
|
||||
(string)$currentPolicy[1] === (string)$incomingPolicy[1] &&
|
||||
(string)$currentPolicy[2] === (string)$incomingPolicy[2] &&
|
||||
(string)$currentPolicy[5] === (string)$incomingPolicy[5]
|
||||
) {
|
||||
return false; // 找到了匹配的,当前权限不需要删除
|
||||
}
|
||||
}
|
||||
return true; // 没有找到匹配的,当前权限需要删除
|
||||
});
|
||||
|
||||
// 计算需要添加的权限:传入有但当前没有的
|
||||
$permissionsToAdd = array_filter($incomingPolicies, function ($incomingPolicy) use ($currentPermissions) {
|
||||
foreach ($currentPermissions as $currentPolicy) {
|
||||
if (
|
||||
(string)$incomingPolicy[0] === (string)$currentPolicy[0] &&
|
||||
(string)$incomingPolicy[1] === (string)$currentPolicy[1] &&
|
||||
(string)$incomingPolicy[2] === (string)$currentPolicy[2] &&
|
||||
(string)$incomingPolicy[5] === (string)$currentPolicy[5]
|
||||
) {
|
||||
return false; // 找到了匹配的,传入权限不需要添加
|
||||
}
|
||||
}
|
||||
return true; // 没有找到匹配的,传入权限需要添加
|
||||
});
|
||||
|
||||
// 删除不需要的权限
|
||||
foreach ($permissionsToDelete as $policy) {
|
||||
Permission::removeFilteredPolicy("0", $policy[0], $policy[1], '', '', '', $policy[5]);
|
||||
}
|
||||
|
||||
// 添加新的权限
|
||||
foreach ($permissionsToAdd as $policy) {
|
||||
$res = Permission::addPolicy(...$policy);
|
||||
if (!$res) {
|
||||
throw new AdminException('添加失败:' . $policy[5]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据菜单ID删除关联Casbin策略
|
||||
*
|
||||
* @param int|string $menuId 菜单ID
|
||||
*
|
||||
* @throws AdminException
|
||||
*/
|
||||
public function deletePoliciesByMenuId(int|string $menuId): void
|
||||
{
|
||||
try {
|
||||
$policyIdentifier = PolicyPrefix::MENU->value . (string)$menuId;
|
||||
Permission::removeFilteredPolicy("0", '', '', '', '', '', $policyIdentifier);
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException("删除菜单关联策略失败:{$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,721 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\service\admin\system;
|
||||
|
||||
use app\dao\system\ConfigDao;
|
||||
use app\scope\global\AccessPermissionScope;
|
||||
use core\base\BaseService;
|
||||
|
||||
class ConfigService extends BaseService
|
||||
{
|
||||
|
||||
public function __construct(ConfigDao $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置项
|
||||
*
|
||||
* @param string $code 配置code
|
||||
* @param mixed $default 默认值
|
||||
* @param array $options 选项,如 ['group_code' => '...']
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function config(string $code, mixed $default = [], array $options = []): mixed
|
||||
{
|
||||
$map = ['code' => $code];
|
||||
// 如果选项中有分组,则添加到查询条件
|
||||
if (!empty($options['group_code'])) {
|
||||
$map['group_code'] = $options['group_code'];
|
||||
}
|
||||
|
||||
// 查询配置
|
||||
$configModel = $this->dao->get($map, ['*'], [], '', [AccessPermissionScope::class]);
|
||||
|
||||
// 如果配置不存在,自动创建
|
||||
if (!$configModel) {
|
||||
$configData = [
|
||||
'code' => $code,
|
||||
'name' => $options['name'] ?? $code,
|
||||
'group_code' => $options['group_code'] ?? 'default',
|
||||
'content' => is_array($default) ? json_encode($default, JSON_UNESCAPED_UNICODE) : $default,
|
||||
'type' => $options['type'] ?? (is_array($default) ? 'json' : 'string'),
|
||||
'enabled' => 1,
|
||||
'sort' => 0,
|
||||
'remark' => $options['remark'] ?? '',
|
||||
];
|
||||
|
||||
try {
|
||||
$this->dao->save($configData);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
$content = $configModel->getOriginal('content', null);
|
||||
|
||||
// 如果content是数组/JSON格式,返回整个数组;否则返回原值
|
||||
if (is_string($content) && !empty($content)) {
|
||||
$decoded = json_decode($content, true);
|
||||
return json_last_error() === JSON_ERROR_NONE ? $decoded : $content;
|
||||
}
|
||||
|
||||
return $content ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置分组详情
|
||||
*
|
||||
* @param string $groupCode 配置分组
|
||||
* @param array $defaults 默认配置项
|
||||
* @param array $options 选项参数
|
||||
* - enabled_only: bool 是否只获取启用的配置项,默认true
|
||||
* - with_metadata: bool 是否包含配置元数据,默认false
|
||||
*
|
||||
* @return array
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function getByGroup(string $groupCode, array $defaults = [], array $options = []): array
|
||||
{
|
||||
// 验证groupCode参数,防止SQL注入
|
||||
if (empty($groupCode)) {
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
$enabledOnly = $options['enabled_only'] ?? true;
|
||||
$withMetadata = $options['with_metadata'] ?? false;
|
||||
|
||||
$map = ['group_code' => $groupCode];
|
||||
if ($enabledOnly) {
|
||||
$map['enabled'] = 1;
|
||||
}
|
||||
|
||||
if ($withMetadata) {
|
||||
// 获取完整配置信息
|
||||
$configs = $this->dao->getList($map);
|
||||
$processedData = [];
|
||||
foreach ($configs as $config) {
|
||||
$content = $config['content'] ?? null;
|
||||
if (is_string($content) && !empty($content)) {
|
||||
$decoded = json_decode($content, true);
|
||||
$config['content'] = json_last_error() === JSON_ERROR_NONE ? $decoded : $content;
|
||||
}
|
||||
|
||||
if (isset($options['key_by']) && $options['key_by'] === 'code') {
|
||||
$processedData[$config['code']] = $config;
|
||||
} else {
|
||||
$processedData[] = $config;
|
||||
}
|
||||
}
|
||||
|
||||
return $processedData;
|
||||
} else {
|
||||
// 只获取配置内容
|
||||
$data = $this->dao->getColumn($map, 'content', 'code');
|
||||
// 处理每个配置项的内容,如果是JSON格式则解码
|
||||
$processedData = [];
|
||||
foreach ($data as $code => $content) {
|
||||
if (is_string($content) && !empty($content)) {
|
||||
$decoded = json_decode($content, true);
|
||||
$processedData[$code] = json_last_error() === JSON_ERROR_NONE ? $decoded : $content;
|
||||
} else {
|
||||
$processedData[$code] = $content;
|
||||
}
|
||||
}
|
||||
|
||||
// 合并默认配置
|
||||
return array_merge($defaults, $processedData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有配置并按分组整理
|
||||
*
|
||||
* @param array $options 选项参数
|
||||
* - enabled_only: bool 是否只获取启用的配置项,默认true
|
||||
* - group_filter: array 分组过滤,只返回指定分组的配置
|
||||
* - with_metadata: bool 是否包含配置项的完整元数据,默认false
|
||||
* - key_by: string 按指定字段作为键名('code' 或 'id'),默认null
|
||||
* - sort_groups: bool 是否对分组进行排序,默认false
|
||||
* - sort_configs: bool 是否对配置项进行排序,默认false
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getAllGrouped(array $options = []): array
|
||||
{
|
||||
$enabledOnly = $options['enabled_only'] ?? true;
|
||||
$groupFilter = $options['group_filter'] ?? [];
|
||||
$withMetadata = $options['with_metadata'] ?? false;
|
||||
$keyBy = $options['key_by'] ?? null;
|
||||
$sortGroups = $options['sort_groups'] ?? false;
|
||||
$sortConfigs = $options['sort_configs'] ?? false;
|
||||
|
||||
$map = [];
|
||||
if ($enabledOnly) {
|
||||
$map['enabled'] = 1;
|
||||
}
|
||||
if (!empty($groupFilter)) {
|
||||
$map['group_code'] = $groupFilter;
|
||||
}
|
||||
|
||||
$configs = $this->dao->getList($map);
|
||||
$grouped = [];
|
||||
|
||||
foreach ($configs as $config) {
|
||||
$groupCode = $config['group_code'] ?? 'default';
|
||||
|
||||
// 处理内容
|
||||
$content = $config['content'];
|
||||
if (is_string($content) && !empty($content)) {
|
||||
$decoded = json_decode($content, true);
|
||||
$config['content'] = json_last_error() === JSON_ERROR_NONE ? $decoded : $content;
|
||||
}
|
||||
|
||||
// 初始化分组
|
||||
if (!isset($grouped[$groupCode])) {
|
||||
$grouped[$groupCode] = [];
|
||||
}
|
||||
|
||||
// 根据选项决定返回的数据结构
|
||||
if ($withMetadata) {
|
||||
// 返回完整配置信息
|
||||
if ($keyBy && isset($config[$keyBy])) {
|
||||
$grouped[$groupCode][$config[$keyBy]] = $config;
|
||||
} else {
|
||||
$grouped[$groupCode][] = $config;
|
||||
}
|
||||
} else {
|
||||
// 只返回配置内容
|
||||
if ($keyBy && isset($config[$keyBy])) {
|
||||
$grouped[$groupCode][$config[$keyBy]] = $config['content'];
|
||||
} else {
|
||||
$grouped[$groupCode][$config['code']] = $config['content'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 排序处理
|
||||
if ($sortGroups) {
|
||||
ksort($grouped);
|
||||
}
|
||||
|
||||
if ($sortConfigs) {
|
||||
foreach ($grouped as &$configs) {
|
||||
if ($withMetadata) {
|
||||
// 对元数据模式按code排序
|
||||
uasort($configs, function ($a, $b) {
|
||||
return strcmp($a['code'] ?? '', $b['code'] ?? '');
|
||||
});
|
||||
} else {
|
||||
// 对内容模式按键名排序
|
||||
ksort($configs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置项中的某个键值
|
||||
*
|
||||
* @param string $code 配置项
|
||||
* @param string $key 要获取的键,支持多级键,如 key1.key2.key3
|
||||
* @param mixed $default 默认值,默认null
|
||||
* @param array $options 选项参数
|
||||
* - group_code: string 配置分组,默认null(在所有分组中查找)
|
||||
* - require_group: bool 是否必须指定分组,默认false
|
||||
* - fallback_to_all: bool 当未找到分组配置时,是否在所有分组中查找,默认true
|
||||
* - search_groups: array 搜索的分组列表,按顺序查找
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getValue(string $code, string $key, mixed $default = null, array $options = []): mixed
|
||||
{
|
||||
$groupCode = $options['group_code'] ?? null;
|
||||
$requireGroup = $options['require_group'] ?? false;
|
||||
$fallbackToAll = $options['fallback_to_all'] ?? true;
|
||||
$searchGroups = $options['search_groups'] ?? [];
|
||||
|
||||
// 如果指定了搜索分组列表,按顺序查找
|
||||
if (!empty($searchGroups) && is_array($searchGroups)) {
|
||||
foreach ($searchGroups as $searchGroupCode) {
|
||||
$config = $this->config($code, null, ['group_code' => $searchGroupCode]);
|
||||
if (is_array($config)) {
|
||||
$value = $this->getNestedValue($config, $key);
|
||||
if ($value !== null) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
// 如果指定了分组,优先在该分组中查找
|
||||
if ($groupCode !== null) {
|
||||
$config = $this->config($code, null, ['group_code' => $groupCode]);
|
||||
|
||||
if (is_array($config)) {
|
||||
$value = $this->getNestedValue($config, $key);
|
||||
if ($value !== null) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果未找到且允许回退到所有分组,则尝试在所有分组中查找
|
||||
if ($fallbackToAll && !$requireGroup) {
|
||||
$config = $this->config($code, null);
|
||||
if (is_array($config)) {
|
||||
$value = $this->getNestedValue($config, $key);
|
||||
if ($value !== null) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
// 默认在所有分组中查找
|
||||
$config = $this->config($code, null);
|
||||
if (is_array($config)) {
|
||||
$value = $this->getNestedValue($config, $key);
|
||||
if ($value !== null) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置特定配置项中的某个键值
|
||||
* 支持两种调用方式:
|
||||
* 1. 新版本(推荐):setConfigValue(string $code, string $key, mixed $value, array $options = [])
|
||||
* 2. 旧版本(兼容):setConfigValue(string $groupCode, string $code, string $key, mixed $value)
|
||||
*
|
||||
* @param string $code 配置项或配置分组(旧版本)
|
||||
* @param string $key 要设置的键,支持多级键,如 key1.key2.key3 或配置项(旧版本)
|
||||
* @param mixed $value 要设置的值或要设置的键(旧版本)
|
||||
* @param array|mixed $options 选项参数或要设置的值(旧版本)
|
||||
* - group_code: string 配置分组,默认null
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function setValue(string $code, string $key, mixed $value, mixed $options = []): bool
|
||||
{
|
||||
// 检查是否为旧版本调用方式:setConfigValue($groupCode, $code, $key, $value)
|
||||
if (func_num_args() === 4 && is_string($options)) {
|
||||
// 旧版本调用,参数顺序为:$groupCode, $code, $key, $value
|
||||
$groupCode = $code;
|
||||
$code = $key;
|
||||
$key = $value;
|
||||
$value = $options;
|
||||
$options = ['group_code' => $groupCode];
|
||||
}
|
||||
|
||||
$groupCode = $options['group_code'] ?? null;
|
||||
|
||||
// 验证groupCode参数,防止SQL注入
|
||||
if ($groupCode !== null && !preg_match('/^[a-zA-Z0-9_-]+$/', $groupCode)) {
|
||||
throw new \Exception('Invalid group_code format');
|
||||
}
|
||||
|
||||
$config = $this->config($code, [], ['group_code' => $groupCode]);
|
||||
|
||||
if (is_array($config)) {
|
||||
// 支持多级键设置
|
||||
$updatedConfig = $this->setNestedValue($config, $key, $value);
|
||||
} else {
|
||||
// 如果当前配置不是数组,将其转换为数组
|
||||
$updatedConfig = $this->setNestedValue([], $key, $value);
|
||||
}
|
||||
|
||||
// 更新配置,传递groupCode参数
|
||||
return $this->update($code, $updatedConfig, ['group_code' => $groupCode]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据多级键获取嵌套数组中的值
|
||||
*
|
||||
* @param array $array 要查找的数组
|
||||
* @param string $key 多级键,如 key1.key2.key3
|
||||
*
|
||||
* @return mixed|null 找到的值或null
|
||||
*/
|
||||
private function getNestedValue(array $array, string $key): mixed
|
||||
{
|
||||
// 如果key不包含点号,直接返回对应值
|
||||
if (!str_contains($key, '.')) {
|
||||
return $array[$key] ?? null;
|
||||
}
|
||||
|
||||
// 拆分多级键
|
||||
$keys = explode('.', $key);
|
||||
$value = $array;
|
||||
|
||||
// 逐级查找
|
||||
foreach ($keys as $k) {
|
||||
if (!is_array($value) || !isset($value[$k])) {
|
||||
return null;
|
||||
}
|
||||
$value = $value[$k];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据多级键设置嵌套数组中的值
|
||||
*
|
||||
* @param array $array 要设置值的数组
|
||||
* @param string $key 多级键,如 key1.key2.key3
|
||||
* @param mixed $value 要设置的值
|
||||
*
|
||||
* @return array 更新后的数组
|
||||
*/
|
||||
private function setNestedValue(array $array, string $key, mixed $value): array
|
||||
{
|
||||
// 如果key不包含点号,直接设置值
|
||||
if (!str_contains($key, '.')) {
|
||||
$array[$key] = $value;
|
||||
return $array;
|
||||
}
|
||||
|
||||
// 拆分多级键
|
||||
$keys = explode('.', $key);
|
||||
$lastKey = array_pop($keys);
|
||||
$current = &$array;
|
||||
|
||||
// 逐级创建或获取子数组
|
||||
foreach ($keys as $k) {
|
||||
if (!isset($current[$k]) || !is_array($current[$k])) {
|
||||
$current[$k] = [];
|
||||
}
|
||||
$current = &$current[$k];
|
||||
}
|
||||
|
||||
// 设置最终值
|
||||
$current[$lastKey] = $value;
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*
|
||||
* @param string $code 配置code
|
||||
* @param mixed $content 配置内容
|
||||
* @param array $options 扩展选项参数
|
||||
* - group_code: string 配置分组
|
||||
* - name: string 配置名称
|
||||
* - enabled: int 是否启用 (1=启用, 0=禁用)
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function update(string $code, mixed $content, array $options = []): void
|
||||
{
|
||||
try {
|
||||
// 处理内容:如果是数组则转换为JSON,字符串直接存储
|
||||
$processedContent = $content;
|
||||
if (is_array($content)) {
|
||||
$processedContent = json_encode($content, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
$map = ['code' => $code];
|
||||
$configModel = $this->dao->get($map);
|
||||
|
||||
if (!empty($configModel)) {
|
||||
// 更新内容
|
||||
$configModel->content = $processedContent;
|
||||
// 更新其他可选字段
|
||||
if (isset($options['group_code'])) {
|
||||
// 验证group_code格式
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $options['group_code'])) {
|
||||
throw new \Exception('Invalid group_code format');
|
||||
}
|
||||
$configModel->group_code = $options['group_code'];
|
||||
}
|
||||
|
||||
if (isset($options['name'])) {
|
||||
$configModel->name = $options['name'];
|
||||
}
|
||||
|
||||
if (isset($options['enabled'])) {
|
||||
$configModel->enabled = (int)$options['enabled'];
|
||||
}
|
||||
|
||||
$configModel->save();
|
||||
} else {
|
||||
// 创建新的配置项
|
||||
$data = [
|
||||
'code' => $code,
|
||||
'content' => $processedContent,
|
||||
'name' => $options['name'] ?? $code,
|
||||
'enabled' => isset($options['enabled']) ? (int)$options['enabled'] : 1,
|
||||
];
|
||||
|
||||
// 设置可选的分组
|
||||
if (isset($options['group_code'])) {
|
||||
// 验证group_code格式
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $options['group_code'])) {
|
||||
throw new \Exception('Invalid group_code format');
|
||||
}
|
||||
$data['group_code'] = $options['group_code'];
|
||||
}
|
||||
|
||||
$this->dao->save($data);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查配置项是否存在
|
||||
*
|
||||
* @param string $code 配置code
|
||||
* @param array $options 扩展选项参数
|
||||
* - group_code: string 配置分组
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function exists(string $code, array $options = []): bool
|
||||
{
|
||||
$map = ['code' => $code];
|
||||
if (isset($options['group_code'])) {
|
||||
// 验证group_code格式
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $options['group_code'])) {
|
||||
throw new \Exception('Invalid group_code format');
|
||||
}
|
||||
$map['group_code'] = $options['group_code'];
|
||||
}
|
||||
|
||||
$config = $this->dao->get($map);
|
||||
return !empty($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除配置项
|
||||
*
|
||||
* @param string $code 配置code
|
||||
* @param array $options 扩展参数
|
||||
* - group_code: string 配置分组
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function delete(string $code, array $options = []): bool|int
|
||||
{
|
||||
$map = [['code' ,'EQ', $code]];
|
||||
if (isset($options['group_code'])) {
|
||||
// 验证group_code格式
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $options['group_code'])) {
|
||||
throw new \Exception('Invalid group_code format');
|
||||
}
|
||||
$map[] = ['group_code','EQ',$options['group_code']];
|
||||
}
|
||||
$config = $this->dao->get($map);
|
||||
if (!$config) {
|
||||
return false;
|
||||
}
|
||||
// 验证是否为系统配置,系统配置不允许删除
|
||||
if (isset($config->is_sys) && $config->is_sys == 1) {
|
||||
throw new \Exception('系统配置不允许删除');
|
||||
}
|
||||
|
||||
return $this->dao->delete($config['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用配置项
|
||||
*
|
||||
* @param string $code 配置code
|
||||
* @param int $enabled
|
||||
* @param array $options 扩展参数
|
||||
* - group_code: string 配置分组
|
||||
* - enabled: bool 是否启用,默认true
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function enabled(string $code, int $enabled, array $options = []): bool
|
||||
{
|
||||
$map = ['code' => $code];
|
||||
if (isset($options['group_code'])) {
|
||||
// 验证group_code格式
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $options['group_code'])) {
|
||||
throw new \Exception('Invalid group_code format');
|
||||
}
|
||||
$map['group_code'] = $options['group_code'];
|
||||
}
|
||||
|
||||
$config = $this->dao->get($map);
|
||||
if (!$config) {
|
||||
return false;
|
||||
}
|
||||
$config->enabled = $enabled;
|
||||
return $config->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置项列表(分页)
|
||||
*
|
||||
* @param int $page 页码
|
||||
* @param int $pageSize 每页数量
|
||||
* @param string $groupCode 分组代码
|
||||
* @param string $keyword 搜索关键字
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getItems(int $page, int $pageSize, string $groupCode, string $keyword = ''): array
|
||||
{
|
||||
|
||||
$map1 = [['is_sys', 'ne', 1]];
|
||||
if (!empty($keyword)) {
|
||||
$map1[] = ['code', 'eq', $keyword];
|
||||
}
|
||||
|
||||
$list = $this->dao->getList($map1, ['*'], $page, $pageSize, 'code', [], false);
|
||||
$total = $this->dao->count($map1);
|
||||
// 处理每个配置项的值
|
||||
foreach ($list as &$item) {
|
||||
$item['value'] = $this->formatValue($item['content'], $item['type'] ?? 'string');
|
||||
}
|
||||
return [
|
||||
'items' => $list,
|
||||
'total' => $total,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单个配置项
|
||||
*
|
||||
* @param array $data 配置数据
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createItem(array $data): void
|
||||
{
|
||||
// 检查配置代码是否已存在
|
||||
if ($this->exists($data['code'], ['group_code' => $data['group_code']])) {
|
||||
throw new \Exception('配置代码已存在');
|
||||
}
|
||||
|
||||
$this->dao->save($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单个配置项
|
||||
*
|
||||
* @param int $id 配置ID
|
||||
* @param array $data 配置数据
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function updateItem(int $id, array $data): void
|
||||
{
|
||||
$config = $this->dao->get(['id' => $id]);
|
||||
if (!$config) {
|
||||
throw new \Exception('配置不存在');
|
||||
}
|
||||
|
||||
// 验证是否为系统配置
|
||||
if (isset($config->is_sys) && $config->is_sys == 1) {
|
||||
throw new \Exception('系统配置不允许修改');
|
||||
}
|
||||
|
||||
$config->name = $data['name'];
|
||||
$config->content = $data['content'];
|
||||
$config->type = $data['type'] ?? 'string';
|
||||
$config->description = $data['description'] ?? '';
|
||||
$config->enabled = $data['enabled'] ?? 1;
|
||||
|
||||
$config->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除配置项
|
||||
*
|
||||
* @param int $id 配置ID
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function deleteItem(int $id): void
|
||||
{
|
||||
$config = $this->dao->get(['id' => $id]);
|
||||
if (!$config) {
|
||||
throw new \Exception('配置不存在');
|
||||
}
|
||||
|
||||
// 验证是否为系统配置
|
||||
if (isset($config->is_sys) && $config->is_sys == 1) {
|
||||
throw new \Exception('系统配置不允许删除');
|
||||
}
|
||||
|
||||
$this->dao->delete($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换配置项启用状态
|
||||
*
|
||||
* @param int $id 配置ID
|
||||
* @param bool $enabled 是否启用
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function toggleItem(int $id, bool $enabled): void
|
||||
{
|
||||
$config = $this->dao->get(['id' => $id]);
|
||||
if (!$config) {
|
||||
throw new \Exception('配置不存在');
|
||||
}
|
||||
|
||||
$config->enabled = $enabled ? 1 : 0;
|
||||
$config->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化配置值
|
||||
*
|
||||
* @param mixed $content 配置内容
|
||||
* @param string $type 配置类型
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function formatValue(mixed $content, string $type): mixed
|
||||
{
|
||||
return match ($type) {
|
||||
'string' => (string)$content,
|
||||
'number' => (float)$content,
|
||||
'boolean' => (bool)$content,
|
||||
'json' => is_string($content) ? json_decode($content, true) : $content,
|
||||
default => $content
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\service\admin\system;
|
||||
|
||||
use app\dao\system\FeedbackDao;
|
||||
use core\base\BaseService;
|
||||
|
||||
class FeedbackService extends BaseService
|
||||
{
|
||||
public function __construct(FeedbackDao $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\service\admin\system;
|
||||
|
||||
use app\dao\system\MenuDao;
|
||||
use app\model\system\Menu;
|
||||
use core\base\BaseService;
|
||||
use core\exception\handler\AdminException;
|
||||
use madong\helper\Arr;
|
||||
use madong\helper\Tree;
|
||||
use support\Container;
|
||||
|
||||
/**
|
||||
* @method save(array $data)
|
||||
* @method selectModel(array $where, array|string $field = '*', int $page = 0, int $limit = 0, string $order = '', array $with = [], bool $search = false, ?array $withoutScopes = null)
|
||||
*/
|
||||
class MenuService extends BaseService
|
||||
{
|
||||
|
||||
/**
|
||||
* 缓存key
|
||||
*/
|
||||
const CACHE_KEY = 'menu_list';
|
||||
|
||||
/**
|
||||
* 所有权限
|
||||
*/
|
||||
const CACHE_ALL_AUTHS_DATA = 'all_auths_data';
|
||||
|
||||
/**
|
||||
* 所有菜单
|
||||
*/
|
||||
const CACHE_ALL_MENUS_DATA = 'all_menus_data';
|
||||
|
||||
public function __construct(MenuDao $dao)
|
||||
{
|
||||
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出所有权限
|
||||
*
|
||||
* @param array $menuIds
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getAllAuth(array $menuIds = []): array
|
||||
{
|
||||
// 1. 优先从缓存获取所有权限数据(自动处理缓存未命中时的数据库查询)
|
||||
$allAuthItems = $this->cacheDriver()->remember(
|
||||
self::CACHE_ALL_AUTHS_DATA,
|
||||
function () {
|
||||
// 缓存未命中时,从数据库获取数据(仅查询需要的字段)
|
||||
return $this->dao->getModel()
|
||||
->where('path', '<>', '')
|
||||
->where('type', '=', 4)
|
||||
->get(['id', 'path', 'methods'])
|
||||
->toArray();
|
||||
}
|
||||
);
|
||||
|
||||
// 2. 如果传入了 $menuIds,则筛选出对应的权限项
|
||||
if (!empty($menuIds)) {
|
||||
$allAuthItems = array_filter($allAuthItems, function ($item) use ($menuIds) {
|
||||
return in_array($item['id'], $menuIds);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 格式化输出(复用现有方法)
|
||||
return $this->formatAuthDataFromItems($allAuthItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据授权项数组格式化输出为 ['METHOD' => ['/path1', '/path2'], ...] 的形式。
|
||||
*
|
||||
* @param array $allAuthItems 授权项数组,每个元素包含 'id', 'path', 'methods'
|
||||
*
|
||||
* @return array 格式化后的授权数据
|
||||
*/
|
||||
private function formatAuthDataFromItems(array $allAuthItems): array
|
||||
{
|
||||
$allAuth = [];
|
||||
foreach ($allAuthItems as $item) {
|
||||
// 假设 'methods' 是逗号分隔的字符串,如 "GET,POST"
|
||||
$methodArray = explode(',', $item['methods']); // 转换为数组
|
||||
|
||||
$pathKey = strtolower(trim(str_replace(' ', '', $item['path'])));
|
||||
|
||||
foreach ($methodArray as $method) {
|
||||
$methodKey = strtolower(trim($method));
|
||||
$allAuth[$methodKey][] = $pathKey;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果需要,可以对每个方法的路径数组进行去重
|
||||
foreach ($allAuth as &$paths) {
|
||||
$paths = array_unique($paths);
|
||||
}
|
||||
unset($paths); // 解除引用
|
||||
return $allAuth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有菜单,优先从缓存读取,并根据 $where 条件筛选。
|
||||
*
|
||||
* @param array $where 筛选条件数组,键为字段名,值为条件值或数组(用于 IN 条件)或操作符数组(如 ['>', 5])
|
||||
*
|
||||
* @return array 处理后的菜单树形结构
|
||||
*/
|
||||
// public function getAllMenus(array $where = [], $mode = 'menu', $isTree = true): array
|
||||
// {
|
||||
// // 1. 优先从缓存获取所有菜单数据
|
||||
// $allMenus = $this->cacheDriver()->remember(
|
||||
// self::CACHE_ALL_MENUS_DATA, // 缓存键名
|
||||
// function () {
|
||||
// $list = $this->dao->getModel()
|
||||
// ->where('enabled', 1)
|
||||
//// ->whereIn('type', [1, 2])// 目录 & 菜单类型 全量保存缓存条件放到缓存输出
|
||||
// ->orderBy('sort', 'asc')
|
||||
// ->get();
|
||||
// foreach ($list as $item) {
|
||||
// $item->set('name', $item->code);
|
||||
// $item->set('meta', SysMenu::getMetaAttribute($item));
|
||||
// }
|
||||
// $list->makeVisible(['id', 'pid', 'type', 'sort', 'redirect', 'path', 'name', 'meta', 'component']);
|
||||
// return $list->toArray();
|
||||
// }
|
||||
// );
|
||||
//
|
||||
// // 2. 根据 $where 条件筛选菜单数据
|
||||
// $menusToProcess = Arr::filterByWhere($allMenus, $where);
|
||||
//
|
||||
// if ($mode == 'code') {
|
||||
// return array_column($menusToProcess, 'code');
|
||||
// }
|
||||
// if (!$isTree) {
|
||||
// return $menusToProcess;
|
||||
// }
|
||||
//
|
||||
// // 构建树形结构
|
||||
// return $this->buildMenuTree($menusToProcess);
|
||||
//
|
||||
// }
|
||||
|
||||
public function getAllMenus(array $where = [], $mode = 'menu', $isTree = true): array
|
||||
{
|
||||
// 1. 优先从缓存获取所有菜单数据
|
||||
$allMenus = $this->cacheDriver()->remember(
|
||||
self::CACHE_ALL_MENUS_DATA, // 缓存键名
|
||||
function () {
|
||||
return $this->dao->getModel()
|
||||
->where('enabled', 1)
|
||||
// ->whereIn('type', [1, 2])// 目录 & 菜单类型 全量保存缓存条件放到缓存输出
|
||||
->orderBy('sort', 'asc')
|
||||
->get();
|
||||
// foreach ($list as $item) {
|
||||
// $item->set('name', $item->code);
|
||||
// $item->set('meta', SysMenu::getMetaAttribute($item));
|
||||
// }
|
||||
// $list->makeVisible(['id', 'pid', 'type', 'sort', 'redirect', 'path', 'name', 'meta', 'component']);
|
||||
// return $list->toArray();
|
||||
}
|
||||
);
|
||||
|
||||
// 2. 根据 $where 条件筛选菜单数据
|
||||
$menusToProcess = Arr::filterByWhere($allMenus, $where);
|
||||
|
||||
if ($mode == 'code') {
|
||||
return array_column($menusToProcess, 'code');
|
||||
}
|
||||
if (!$isTree) {
|
||||
return $menusToProcess;
|
||||
}
|
||||
|
||||
// 构建树形结构
|
||||
return $this->buildMenuTree($menusToProcess);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建菜单的树形结构。
|
||||
*
|
||||
* @param array $formattedMenus 已处理的菜单数据
|
||||
*
|
||||
* @return array 树形结构的菜单数据
|
||||
*/
|
||||
protected function buildMenuTree(array $formattedMenus): array
|
||||
{
|
||||
$tree = new Tree($formattedMenus);
|
||||
return $tree->getTree();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*** 重写方法***/
|
||||
|
||||
/**
|
||||
* 重写更新方法:统一事务管理菜单更新、缓存清理、Casbin同步
|
||||
*
|
||||
* @param int|string $id 菜单ID
|
||||
* @param array $data 更新数据
|
||||
*
|
||||
* @return \app\model\system\Menu 更新后的模型
|
||||
* @throws \Throwable
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function update(int|string $id, array $data): \app\model\system\Menu
|
||||
{
|
||||
return $this->transaction(function () use ($id, $data) {
|
||||
// 1. 获取菜单模型
|
||||
$model = $this->get($id);
|
||||
if (!$model) {
|
||||
throw new AdminException("菜单ID:{$id}不存在");
|
||||
}
|
||||
|
||||
// 2. 更新菜单数据(使用属性复制或直接赋值)
|
||||
$model->fill($data);
|
||||
$model->save();
|
||||
|
||||
// 3. 清理缓存(原控制器中的缓存删除逻辑迁移至此)
|
||||
$this->cacheDriver()->delete(self::CACHE_ALL_AUTHS_DATA);
|
||||
$this->cacheDriver()->delete(self::CACHE_ALL_MENUS_DATA);
|
||||
|
||||
// 4. 同步Casbin策略(复用已实现的同步方法)
|
||||
$this->syncPermissionCacheAfterUpdate($id);
|
||||
return $model;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除菜单(含子菜单)并同步删除Casbin策略
|
||||
*
|
||||
* @param array $data 菜单ID数组或逗号分隔字符串
|
||||
*
|
||||
* @return array 被删除的所有菜单ID
|
||||
* @throws \Throwable
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function batchDelete(array $data = []): array
|
||||
{
|
||||
|
||||
return $this->transaction(function () use ($data) {
|
||||
$deletedIds = [];
|
||||
|
||||
foreach ($data as $id) {
|
||||
/** @var Menu $item */
|
||||
$item = $this->get($id);
|
||||
if (!$item) {
|
||||
continue;
|
||||
}
|
||||
// 删除菜单及子菜单,获取所有被删除的ID
|
||||
$ids = $item->deleteWithAllChildren();
|
||||
$deletedIds = array_merge($deletedIds, $ids);
|
||||
}
|
||||
|
||||
// 清理角色菜单中间表关联数据
|
||||
if (!empty($deletedIds)) {
|
||||
/** @var RoleMenuService $roleMenuService */
|
||||
$roleMenuService = Container::make(RoleMenuService::class);
|
||||
/** @var RoleMenuModel $roleMenuModel */
|
||||
$roleMenuModel = $roleMenuService->getModel();
|
||||
$roleMenuModel->whereIn('menu_id', $deletedIds)->delete();
|
||||
|
||||
// 清理相关用户的权限缓存
|
||||
$this->clearUserPermissionCacheByMenus($deletedIds);
|
||||
}
|
||||
|
||||
return array_unique($deletedIds);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单更新后清理相关用户权限缓存
|
||||
*
|
||||
* @param int|string $menuId 被更新的菜单ID
|
||||
*
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
private function syncPermissionCacheAfterUpdate(int|string $menuId): void
|
||||
{
|
||||
try {
|
||||
// 1. 查询所有关联此菜单的角色ID(通过角色菜单服务)
|
||||
/** @var RoleMenuService $roleMenuService */
|
||||
$roleMenuService = Container::make(RoleMenuService::class);
|
||||
$roleIds = $roleMenuService->getColumn(
|
||||
['menu_id' => $menuId],
|
||||
'role_id'
|
||||
);
|
||||
if (empty($roleIds)) {
|
||||
return; // 无关联角色,无需同步
|
||||
}
|
||||
|
||||
// 2. 清理拥有这些角色的用户的权限缓存
|
||||
foreach ($roleIds as $roleId) {
|
||||
/** @var \app\service\admin\system\AdminRoleService $adminRoleService */
|
||||
$adminRoleService = Container::make(\app\service\admin\system\AdminRoleService::class);
|
||||
|
||||
// 获取拥有此角色的用户ID列表
|
||||
$userIds = \app\model\system\AdminRole::where('role_id', $roleId)
|
||||
->pluck('admin_id')
|
||||
->toArray();
|
||||
|
||||
if (!empty($userIds)) {
|
||||
// 清理这些用户的权限缓存
|
||||
$currentUser = Container::make(\app\adminapi\CurrentUser::class);
|
||||
foreach ($userIds as $userId) {
|
||||
$currentUser->clearCache($userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理与指定菜单相关的用户权限缓存
|
||||
*
|
||||
* @param array $menuIds 菜单ID数组
|
||||
*/
|
||||
private function clearUserPermissionCacheByMenus(array $menuIds): void
|
||||
{
|
||||
try {
|
||||
if (empty($menuIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取拥有这些菜单的角色ID
|
||||
/** @var RoleMenuService $roleMenuService */
|
||||
$roleMenuService = Container::make(RoleMenuService::class);
|
||||
/** @var RoleMenuModel $roleMenuModel */
|
||||
$roleMenuModel = $roleMenuService->getModel();
|
||||
|
||||
$roleIds = $roleMenuModel->whereIn('menu_id', $menuIds)
|
||||
->pluck('role_id')
|
||||
->unique()
|
||||
->toArray();
|
||||
|
||||
if (!empty($roleIds)) {
|
||||
// 获取拥有这些角色的用户ID
|
||||
$userIds = \app\model\system\AdminRole::whereIn('role_id', $roleIds)
|
||||
->pluck('admin_id')
|
||||
->unique()
|
||||
->toArray();
|
||||
|
||||
if (!empty($userIds)) {
|
||||
// 清理这些用户的权限缓存
|
||||
$currentUser = Container::make(\app\adminapi\CurrentUser::class);
|
||||
foreach ($userIds as $userId) {
|
||||
$currentUser->clearCache($userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 缓存清理失败不影响主要业务,记录日志即可
|
||||
\core\logger\Logger::error("清理菜单相关用户权限缓存失败: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\service\admin\system;
|
||||
|
||||
use app\dao\system\RecycleBinDao;
|
||||
use app\model\system\RecycleBin;
|
||||
use core\base\BaseService;
|
||||
use core\exception\handler\AdminException;
|
||||
use support\Db;
|
||||
|
||||
/**
|
||||
* 回收站服务
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0
|
||||
*/
|
||||
class RecycleBinService extends BaseService
|
||||
{
|
||||
public function __construct(RecycleBinDao $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据恢复
|
||||
*
|
||||
* @param int $recycleId
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function restoreRecycleBin(int $recycleId): void
|
||||
{
|
||||
$this->transaction(function () use ($recycleId) {
|
||||
$record = $this->dao->get($recycleId, null, [], '');
|
||||
if (empty($record)) {
|
||||
throw new AdminException("回收站记录不存在");
|
||||
}
|
||||
// 3. 动态恢复数据
|
||||
$this->restoreOriginalData($record);
|
||||
|
||||
// 4. 删除回收站记录
|
||||
$record->delete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复原始数据
|
||||
*/
|
||||
protected function restoreOriginalData(RecycleBin $record): void
|
||||
{
|
||||
$tableName = $record->table_name;
|
||||
$config = self::getTableConfig($tableName);
|
||||
$storage_mode = $config['storage_mode'];
|
||||
$connection = config('database.default');
|
||||
$tableData = json_decode($record->getData('data'), true);
|
||||
$columns = $this->getTableColumns($tableName, $connection);
|
||||
$tableData = array_intersect_key($tableData, array_flip($columns));
|
||||
Db::connection($connection)->table($tableName)->insert($tableData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取还原表的数据列
|
||||
*
|
||||
* @param string $tableName
|
||||
* @param string $connection
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getTableColumns(string $tableName, string $connection = ''): array
|
||||
{
|
||||
return Db::connection($connection)->getSchemaBuilder()->getColumnListing($tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表配置(合并全局+表级配置)
|
||||
*
|
||||
* @param string $table
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
// 获取合并配置
|
||||
public static function getTableConfig(string $table): array
|
||||
{
|
||||
return array_merge(
|
||||
config('recycle_bin.default'),
|
||||
config("recycle_bin.tables.{$table}", []),
|
||||
['tenant' => config('recycle_bin.tenant')]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\service\admin\system;
|
||||
|
||||
use app\dao\system\RoleMenuDao;
|
||||
use core\base\BaseService;
|
||||
use core\exception\handler\AdminException;
|
||||
use support\Container;
|
||||
|
||||
/**
|
||||
* @method getColumn(int[]|string[] $array, string $string)
|
||||
*/
|
||||
class RoleMenuService extends BaseService
|
||||
{
|
||||
|
||||
public function __construct(RoleMenuDao $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设置角色权限
|
||||
*
|
||||
* @param $data
|
||||
*
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
// public function save($data): void
|
||||
// {
|
||||
// try {
|
||||
// $this->transaction(function () use ($data) {
|
||||
// $roleId = $data['role_id'] ?? '';
|
||||
// $newPermissions = $data['menu_id'] ?? [];
|
||||
// if (empty($roleId)) {
|
||||
// throw new AdminException('参数错误缺少role_id', ['errorCode' => -1]);
|
||||
// }
|
||||
//
|
||||
// // 1. 获取角色模型(确保角色存在)
|
||||
// /** @var SysRoleService $roleService */
|
||||
// $roleService = Container::make(SysRoleService::class);
|
||||
// $roleModel = $roleService->get($roleId);
|
||||
// if (!$roleModel) {
|
||||
// throw new AdminException('角色不存在', ['errorCode' => -1]);
|
||||
// }
|
||||
//
|
||||
// // 2. 同步 role_menu 关联关系(自动处理新增/删除,无需手动计算差异)
|
||||
// // 假设 SysRole 模型定义了 roleMenus 关联(hasMany: SysRoleMenu)
|
||||
// $roleModel->menus()->sync($newPermissions);
|
||||
//
|
||||
// // 3. 获取同步后的菜单ID(从关联关系中直接获取,确保数据一致性)
|
||||
// $syncedMenuIds = $roleModel->menus()->pluck('menu_id')->toArray();
|
||||
//
|
||||
// // 4. 构建 Casbin 策略(基于同步后的最新菜单权限)
|
||||
// $casbinRoleId = PolicyPrefix::ROLE->value . $roleId;
|
||||
// $domain = '*';
|
||||
//
|
||||
// // 4.1 获取菜单详情(用于构建策略)
|
||||
// /** @var SysMenuService $menuService */
|
||||
// $menuService = Container::make(SysMenuService::class);
|
||||
// $menuDetails = $menuService->selectList(['id' => $syncedMenuIds], '*', 0, 0, 'sort', [], true)->toArray();
|
||||
//
|
||||
// $incomingPolicies = [];
|
||||
// foreach ($menuDetails as $menu) {
|
||||
// if (empty($menu['code'])) continue;
|
||||
// $incomingPolicies[] = [
|
||||
// $casbinRoleId, // sub (角色标识)
|
||||
// $domain, // dom (租户域)
|
||||
// $menu['code'], // obj (权限码)
|
||||
// '*', // act (操作类型)
|
||||
// $menu['methods'], // method (HTTP方法)
|
||||
// PolicyPrefix::MENU->value . $menu['id'], // trace_id
|
||||
// ];
|
||||
// }
|
||||
//
|
||||
// // 4.2 更新 Casbin 策略
|
||||
// $currentCasbinPolicies = Permission::getImplicitResourcesForUser($casbinRoleId, $domain);
|
||||
// /** @var SysCasbinService $casbinService */
|
||||
// $casbinService = Container::make(SysCasbinService::class);
|
||||
// $casbinService->updateCasbinPolicies($currentCasbinPolicies, $incomingPolicies);
|
||||
//
|
||||
// });
|
||||
// } catch (\Throwable $e) {
|
||||
// throw new AdminException($e->getMessage());
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 更新设置角色权限(含事务封装)
|
||||
*
|
||||
* @param $data
|
||||
*
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function save($data): void
|
||||
{
|
||||
try {
|
||||
$this->transaction(function () use ($data) {
|
||||
$this->saveWithoutTransaction($data);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 无事务版本:核心业务逻辑(供内部/外部事务复用)
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function saveWithoutTransaction(array $data): void
|
||||
{
|
||||
try {
|
||||
$roleId = $data['role_id'] ?? '';
|
||||
$newPermissions = $data['menu_id'] ?? [];
|
||||
if (empty($roleId)) {
|
||||
throw new AdminException('参数错误缺少role_id');
|
||||
}
|
||||
// 1. 获取角色模型(确保角色存在)
|
||||
/** @var RoleService $roleService */
|
||||
$roleService = Container::make(RoleService::class);
|
||||
$roleModel = $roleService->get($roleId);
|
||||
if (!$roleModel) {
|
||||
throw new AdminException('角色不存在');
|
||||
}
|
||||
|
||||
// 2. 同步 role_menu 关联关系(自动处理新增/删除,无需手动计算差异)
|
||||
$roleModel->menus()->sync($newPermissions);
|
||||
|
||||
// 3. 清理相关用户的权限缓存
|
||||
$this->clearUserPermissionCacheByRole($roleId);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理拥有此角色的用户的权限缓存
|
||||
*
|
||||
* @param string|int $roleId 角色ID
|
||||
*/
|
||||
private function clearUserPermissionCacheByRole(string|int $roleId): void
|
||||
{
|
||||
try {
|
||||
// 获取拥有此角色的所有用户
|
||||
/** @var \app\service\admin\system\AdminRoleService $adminRoleService */
|
||||
$adminRoleService = Container::make(\app\service\admin\system\AdminRoleService::class);
|
||||
|
||||
// 获取拥有此角色的用户ID列表
|
||||
$userIds = \app\model\system\AdminRole::where('role_id', $roleId)
|
||||
->pluck('admin_id')
|
||||
->toArray();
|
||||
|
||||
if (!empty($userIds)) {
|
||||
// 清理这些用户的权限缓存
|
||||
$currentUser = Container::make(\app\adminapi\CurrentUser::class);
|
||||
foreach ($userIds as $userId) {
|
||||
$currentUser->clearCache($userId);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 缓存清理失败不影响主要业务,记录日志即可
|
||||
\core\logger\Logger::error("清理角色{$roleId}的用户权限缓存失败: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\service\admin\system;
|
||||
|
||||
use app\dao\system\RoleScopeDeptDao;
|
||||
use core\base\BaseService;
|
||||
|
||||
/**
|
||||
* @method getColumn(int[]|string[] $array, string $string)
|
||||
*/
|
||||
class RoleScopeDeptService extends BaseService
|
||||
{
|
||||
|
||||
public function __construct(RoleScopeDeptDao $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\service\admin\system;
|
||||
|
||||
use app\dao\system\RoleDao;
|
||||
use app\model\system\Role;
|
||||
use core\base\BaseService;
|
||||
use core\exception\handler\AdminException;
|
||||
use madong\helper\Arr;
|
||||
use madong\helper\PropertyCopier;
|
||||
use support\Container;
|
||||
|
||||
class RoleService extends BaseService
|
||||
{
|
||||
|
||||
/**
|
||||
* 缓存key
|
||||
*/
|
||||
const CACHE_ALL_ROLES_DATA = 'roles_all_data';
|
||||
|
||||
public function __construct(RoleDao $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有的角色
|
||||
*
|
||||
* @param array $where
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getAllRoles(array $where = []): array
|
||||
{
|
||||
// 1. 优先从缓存获取所有角色数据
|
||||
$allRoles = $this->cacheDriver()->remember(
|
||||
self::CACHE_ALL_ROLES_DATA, // 缓存键名
|
||||
function () {
|
||||
$results = $this->dao->getModel()
|
||||
->where('enabled', 1)
|
||||
->orderBy('sort', 'asc')
|
||||
->get(["id",
|
||||
"pid",
|
||||
"name",
|
||||
"code",
|
||||
"is_super_admin",
|
||||
"role_type",
|
||||
"data_scope",
|
||||
"enabled",
|
||||
"sort"]);
|
||||
|
||||
return $results->toArray();
|
||||
}
|
||||
);
|
||||
// 2.$where 条件筛数据
|
||||
return Arr::filterByWhere($allRoles, $where);
|
||||
}
|
||||
|
||||
/**
|
||||
* 传入角色id获取权限
|
||||
*
|
||||
* @param array $where
|
||||
* @param string $mode merge|unique
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getPermissions(array $where = [], string $mode = 'merge'): array
|
||||
{
|
||||
// 1. 优先从缓存获取所有全量角色数据
|
||||
$data = $this->cacheDriver()->remember(
|
||||
self::CACHE_ALL_ROLES_DATA, // 缓存键名
|
||||
function () {
|
||||
return $this->dao->getRolePermissions();
|
||||
}
|
||||
);
|
||||
//2. 条件过滤
|
||||
$result = Arr::filterByWhere($data, $where);
|
||||
if ($mode == 'unique') {
|
||||
return $result['menu'] ?? [];
|
||||
}
|
||||
return [
|
||||
'menu' => array_merge([], ...array_column($result, 'menu')),
|
||||
'api' => array_merge([], ...array_column($result, 'api')),
|
||||
'button' => array_merge([], ...array_column($result, 'button')),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过角色id 获取菜单权限IDS
|
||||
*
|
||||
* @param int|string $id
|
||||
* @param string|null $tenantId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPermissionColumns(int|string $id, string|null $tenantId = null): array
|
||||
{
|
||||
$role = $this->dao->get($id);
|
||||
if (!$role) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 通过关联关系获取菜单ID
|
||||
return $role->menus()->pluck('id')->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* save
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return Role|null
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function save(array $data): Role|null
|
||||
{
|
||||
try {
|
||||
return $this->transaction(function () use ($data) {
|
||||
$model = $this->dao->save($data);
|
||||
$handlePermissions = isset($data['permissions']) && $data['permissions'] !== null;
|
||||
|
||||
// 有传权限key,才处理权限
|
||||
if ($handlePermissions) {
|
||||
$menus = $data['permissions'];
|
||||
// 同步角色菜单关联
|
||||
$model->menus()->sync($menus);
|
||||
|
||||
// 清理相关用户的权限缓存
|
||||
$this->clearUserPermissionCacheByRole($model->id);
|
||||
}
|
||||
return $model;
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param $id
|
||||
* @param $data
|
||||
*
|
||||
* @return void
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
// public function update($id, $data): void
|
||||
// {
|
||||
// try {
|
||||
// $this->transaction(function () use ($id, $data) {
|
||||
// // 获取传入的权限列表(如果存在)
|
||||
// $menus = $data['permissions'] ?? [];
|
||||
// // 从数据访问对象中获取模型实例
|
||||
// $model = $this->dao->get($id);
|
||||
// if (!$model) {
|
||||
// return;
|
||||
// }
|
||||
// // 使用属性复制器将传入的数据复制到模型中
|
||||
// PropertyCopier::copyProperties((object)$data, $model);
|
||||
// $model->save();
|
||||
//
|
||||
// // 格式化用户标识符,用于 Casbin
|
||||
// $roleId = PolicyPrefix::ROLE->value . strval($model->id);
|
||||
// $domain = '*';
|
||||
//
|
||||
// // 获取当前用户在 Casbin 中的隐式资源权限
|
||||
// $currentPolicies = Permission::getImplicitResourcesForUser($roleId, $domain);
|
||||
//
|
||||
// // 提取完整的当前权限(0-5)
|
||||
// $currentPermissions = array_map(
|
||||
// function ($policy) {
|
||||
// return $policy; // 保留完整的策略数组
|
||||
// },
|
||||
// $currentPolicies
|
||||
// );
|
||||
//
|
||||
// // 将传入的菜单数据转换为 Casbin 策略格式
|
||||
// $menuService = new SysMenuService();
|
||||
// $policyService = new SysCasbinService();
|
||||
//
|
||||
// $menusData = $menuService->getAllMenus(['id' => $menus], 'menu', false);
|
||||
// // 构建传入的权限列表,格式与当前权限相同
|
||||
// $incomingPolicies = [];
|
||||
// foreach ($menusData as $item) {
|
||||
// $path = $policyService->resolvePermissionIdentifier($item);
|
||||
// // 构建策略数组
|
||||
// $policy = [
|
||||
// $roleId, // sub
|
||||
// $domain, // dom
|
||||
// $path, // obj
|
||||
// '*', // act(根据你的业务需求调整)
|
||||
// $item['methods'], // method
|
||||
// PolicyPrefix::MENU->value . $item['id'], // trace_id 资源归属用于追踪
|
||||
// ];
|
||||
// // 保留完整的策略数组
|
||||
// $incomingPolicies[] = $policy;
|
||||
// }
|
||||
// // 更新 Casbin 权限
|
||||
// $policyService->updateCasbinPolicies($currentPermissions, $incomingPolicies);
|
||||
// // 同步 Casbin 权限,确保只有授权角色保留
|
||||
// $model->casbin()->sync([$roleId]);
|
||||
// });
|
||||
// } catch (\Throwable $e) {
|
||||
// // 捕获异常并抛出自定义异常
|
||||
// throw new AdminException($e->getMessage());
|
||||
// }
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param $id
|
||||
* @param $data
|
||||
*
|
||||
* @return void
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function update($id, $data): void
|
||||
{
|
||||
try {
|
||||
$this->transaction(function () use ($id, $data) {
|
||||
$model = $this->dao->get($id);
|
||||
if (!$model) {
|
||||
return;
|
||||
}
|
||||
|
||||
PropertyCopier::copyProperties((object)$data, $model);
|
||||
$model->save();
|
||||
|
||||
// 检查 permissions 是否存在且不为 null(未定义或显式 null 时不处理权限)
|
||||
$handlePermissions = isset($data['permissions']) && $data['permissions'] !== null;
|
||||
if ($handlePermissions) {
|
||||
$menus = $data['permissions']; // 获取权限列表
|
||||
|
||||
// 同步角色菜单关联
|
||||
$model->menus()->sync($menus);
|
||||
|
||||
// 清理相关用户的权限缓存
|
||||
$this->clearUserPermissionCacheByRole($model->id);
|
||||
}
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
// 捕获异常并抛出自定义异常
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*
|
||||
* @param array $id
|
||||
* @param bool $force
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function destroy(array $id, bool $force = false): mixed
|
||||
{
|
||||
try {
|
||||
return $this->transaction(function () use ($id, $force) {
|
||||
$data = Arr::normalize($id);
|
||||
$deletedIds = [];
|
||||
|
||||
foreach ($data as $id) {
|
||||
$item = $this->get($id);
|
||||
if ($item) {
|
||||
$primaryKey = $item->getPk();
|
||||
|
||||
// 清理相关用户的权限缓存
|
||||
$this->clearUserPermissionCacheByRole($id);
|
||||
|
||||
$item->delete();
|
||||
$deletedIds[] = $item->{$primaryKey};
|
||||
}
|
||||
}
|
||||
return $deletedIds;
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配数据权限
|
||||
*
|
||||
* @param $id
|
||||
* @param $data
|
||||
*
|
||||
* @throws \core\exception\handler\AdminException
|
||||
*/
|
||||
public function updateScope($id, $data): void
|
||||
{
|
||||
try {
|
||||
$this->transaction(function () use ($id, $data) {
|
||||
$scope = $data['scopes'] ?? [];
|
||||
$model = $this->dao->get($id);
|
||||
PropertyCopier::copyProperties((object)$data, $model);
|
||||
if (!empty($model)) {
|
||||
$model->save();
|
||||
if ($model->data_scope !== 2) {
|
||||
$scope = [];
|
||||
}
|
||||
$model->scopes()->sync($scope);
|
||||
}
|
||||
$this->cacheDriver()->delete(self::CACHE_ALL_ROLES_DATA);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理角色的用户权限缓存
|
||||
*
|
||||
* @param int|string $roleId
|
||||
*/
|
||||
private function clearUserPermissionCacheByRole(int|string $roleId): void
|
||||
{
|
||||
// 获取拥有此角色的所有用户
|
||||
/** @var AdminRoleService $adminRoleService */
|
||||
$adminRoleService = Container::make(AdminRoleService::class);
|
||||
$userIds = $adminRoleService->getUserIdsByRoleId($roleId);
|
||||
|
||||
if (!empty($userIds)) {
|
||||
// 清理每个用户的权限缓存
|
||||
foreach ($userIds as $userId) {
|
||||
$cacheKey = 'user_permissions_' . $userId;
|
||||
$this->cacheDriver()->delete($cacheKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\service\admin\system;
|
||||
|
||||
use app\service\core\metadata\MetadataCollectorService;
|
||||
use core\base\BaseService;
|
||||
use support\Container;
|
||||
|
||||
/**
|
||||
* 规则扫描服务
|
||||
* 使用 MetadataCollectorService 扫描控制器,提供缓存机制
|
||||
*/
|
||||
class RuleService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 缓存键
|
||||
*/
|
||||
const CACHE_KEY = 'rule_data';
|
||||
const CACHE_TTL = 300; // 缓存5分钟
|
||||
|
||||
/**
|
||||
* 获取扫描的权限列表(带缓存)
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getPermissions(): array
|
||||
{
|
||||
return $this->cacheDriver()->remember(
|
||||
self::CACHE_KEY,
|
||||
function () {
|
||||
$collector = Container::get(MetadataCollectorService::class);
|
||||
return $collector->collect();
|
||||
},
|
||||
self::CACHE_TTL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类树(基于 tags)
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getCategories(): array
|
||||
{
|
||||
$permissions = $this->getPermissions();
|
||||
$categories = [];
|
||||
$categoryMap = [];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
// 确保 tags 是字符串数组
|
||||
$tags = $permission['tags'] ?? ['未分类'];
|
||||
if (!is_array($tags)) {
|
||||
$tags = [$tags];
|
||||
}
|
||||
// 确保数组中的每个元素都是字符串
|
||||
$tags = array_map(function($tag) {
|
||||
if (is_string($tag)) {
|
||||
return $tag;
|
||||
}
|
||||
if (is_object($tag) && isset($tag->name)) {
|
||||
return $tag->name;
|
||||
}
|
||||
return (string) $tag;
|
||||
}, $tags);
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (!isset($categoryMap[$tag])) {
|
||||
$categoryId = md5($tag);
|
||||
$categoryMap[$tag] = $categoryId;
|
||||
$categories[] = [
|
||||
'id' => $categoryId,
|
||||
'pid' => 0,
|
||||
'name' => $tag,
|
||||
'value' => $categoryId,
|
||||
'label' => $tag,
|
||||
'children' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分类获取接口列表
|
||||
*
|
||||
* @param string|null $categoryId
|
||||
* @param string|null $keyword
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getRoutesByCategory(?string $categoryId = null, ?string $keyword = null): array
|
||||
{
|
||||
$permissions = $this->getPermissions();
|
||||
$routes = [];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
// 确保 tags 是字符串数组
|
||||
$tags = $permission['tags'] ?? ['未分类'];
|
||||
if (!is_array($tags)) {
|
||||
$tags = [$tags];
|
||||
}
|
||||
// 确保数组中的每个元素都是字符串
|
||||
$tags = array_map(function($tag) {
|
||||
if (is_string($tag)) {
|
||||
return $tag;
|
||||
}
|
||||
if (is_object($tag) && isset($tag->name)) {
|
||||
return $tag->name;
|
||||
}
|
||||
return (string) $tag;
|
||||
}, $tags);
|
||||
|
||||
// 过滤分类
|
||||
if ($categoryId !== null) {
|
||||
$categoryIds = array_map(fn($tag) => md5($tag), $tags);
|
||||
if (!in_array($categoryId, $categoryIds)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤关键词
|
||||
if ($keyword !== null && !empty($keyword)) {
|
||||
// 构建搜索文本,只使用字符串类型的字段
|
||||
$searchParts = [];
|
||||
|
||||
// 添加 tags
|
||||
$searchParts[] = implode(' ', $tags);
|
||||
|
||||
// 添加其他字段(确保是字符串)
|
||||
$fields = ['summary', 'description', 'controller', 'method', 'code', 'route'];
|
||||
foreach ($fields as $field) {
|
||||
if (isset($permission[$field]) && is_string($permission[$field])) {
|
||||
$searchParts[] = $permission[$field];
|
||||
}
|
||||
}
|
||||
|
||||
$searchText = implode(' ', $searchParts);
|
||||
|
||||
if (stripos($searchText, $keyword) === false) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$routes[] = [
|
||||
'id' => md5($permission['route'] . '_' . $permission['method']),
|
||||
'name' => $permission['summary'] ?? $permission['description'] ?? $permission['code'],
|
||||
'method' => $this->extractHttpMethod($permission['method']),
|
||||
'path' => $permission['route'],
|
||||
'code' => $permission['code'],
|
||||
'controller' => $permission['controller'],
|
||||
'action' => $permission['method'],
|
||||
'description' => $permission['description'] ?? '',
|
||||
'tags' => $tags,
|
||||
];
|
||||
}
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从方法名中提取 HTTP 方法
|
||||
*
|
||||
* @param string $methodName
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function extractHttpMethod(string $methodName): string
|
||||
{
|
||||
$prefixes = ['get', 'post', 'put', 'delete', 'patch'];
|
||||
$lowerMethod = strtolower($methodName);
|
||||
|
||||
foreach ($prefixes as $prefix) {
|
||||
if (str_starts_with($lowerMethod, $prefix)) {
|
||||
return strtoupper($prefix);
|
||||
}
|
||||
}
|
||||
|
||||
return 'GET';
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->cacheDriver()->delete(self::CACHE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发扫描并刷新缓存
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function refresh(): array
|
||||
{
|
||||
$this->clearCache();
|
||||
return $this->getPermissions();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\service\admin\system;
|
||||
|
||||
use app\dao\system\UploadDao;
|
||||
use core\base\BaseService;
|
||||
use core\exception\handler\AdminException;
|
||||
use core\upload\UploadFile;
|
||||
use madong\helper\Arr;
|
||||
use support\Container;
|
||||
|
||||
class UploadService extends BaseService
|
||||
{
|
||||
|
||||
|
||||
public function __construct(UploadDao $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程下载图片到本地
|
||||
*
|
||||
* @param string $url
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function saveNetworkImage(string $url): array
|
||||
{
|
||||
$config = UploadFile::config('local');
|
||||
$data = file_get_contents($url);
|
||||
if ($data === false) {
|
||||
throw new AdminException('获取文件资源失败');
|
||||
}
|
||||
$image_resource = imagecreatefromstring($data);
|
||||
if (!$image_resource) {
|
||||
throw new AdminException('创建图片资源失败');
|
||||
}
|
||||
$filename = basename($url);
|
||||
$file_extension = pathinfo($filename, PATHINFO_EXTENSION);
|
||||
$full_dir = runtime_path() . '/resource/';
|
||||
if (!is_dir($full_dir)) {
|
||||
mkdir($full_dir, 0777, true);
|
||||
}
|
||||
$save_path = $full_dir . $filename;
|
||||
$content_type = 'image/';
|
||||
switch ($file_extension) {
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
$content_type = 'image/jpeg';
|
||||
$result = imagejpeg($image_resource, $save_path);
|
||||
break;
|
||||
case 'png':
|
||||
$content_type = 'image/png';
|
||||
$result = imagepng($image_resource, $save_path);
|
||||
break;
|
||||
case 'gif':
|
||||
$content_type = 'image/gif';
|
||||
$result = imagegif($image_resource, $save_path);
|
||||
break;
|
||||
default:
|
||||
imagedestroy($image_resource);
|
||||
throw new AdminException('文件格式错误');
|
||||
}
|
||||
imagedestroy($image_resource);
|
||||
if (!$result) {
|
||||
throw new AdminException('文件保存失败');
|
||||
}
|
||||
$hash = md5_file($save_path);
|
||||
$size = filesize($save_path);
|
||||
|
||||
$result = $this->dao->get(['hash' => $hash]);
|
||||
if (!empty($result)) {
|
||||
unlink($save_path);
|
||||
return $result->toArray();
|
||||
}
|
||||
|
||||
/** @var ConfigService $systemConfigService */
|
||||
$systemConfigService = Container::get(ConfigService::class);
|
||||
$local = $systemConfigService->dao->get(['code' => 'local']);
|
||||
if (empty($local)) {
|
||||
throw new AdminException('缺少本地上传配置信息');
|
||||
}
|
||||
$root = Arr::fetchConfigValue($config, 'root');
|
||||
$folder = date('Ymd');
|
||||
$full_dir = base_path() . DIRECTORY_SEPARATOR . $root . $folder . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($full_dir)) {
|
||||
mkdir($full_dir, 0777, true);
|
||||
}
|
||||
$object_name = bin2hex(pack('Nn', time(), random_int(1, 65535))) . ".$file_extension";
|
||||
$newPath = $full_dir . $object_name;
|
||||
|
||||
copy($save_path, $newPath);
|
||||
unlink($save_path);
|
||||
$domain = Arr::fetchConfigValue($config, 'domain');
|
||||
$dirname = Arr::fetchConfigValue($config, 'dirname');
|
||||
$baseUrl = $dirname . $folder . '/';
|
||||
|
||||
$info['platform'] = 'local';
|
||||
$info['original_filename'] = $filename;
|
||||
$info['filename'] = $object_name;
|
||||
$info['hash'] = $hash;
|
||||
$info['content_type'] = $content_type;
|
||||
$info['base_path'] = $root . $folder . '/' . $object_name;
|
||||
$info['ext'] = $file_extension;
|
||||
$info['size'] = $size;
|
||||
$info['size_info'] = formatBytes($size);
|
||||
$info['url'] = $baseUrl . $object_name;//访问地
|
||||
$result = $this->dao->save($info);
|
||||
return $result->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
*
|
||||
* @param string $upload
|
||||
* @param bool $isLocal
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function upload(string $upload = '', bool $isLocal = false): mixed
|
||||
{
|
||||
try {
|
||||
return $this->transaction(function () use ($upload, $isLocal) {
|
||||
/** @var ConfigService $systemConfigService */
|
||||
$baseConfig = UploadFile::config('upload');//获取上次配置
|
||||
if (empty($baseConfig)) {
|
||||
throw new AdminException('缺少上传配置信息');
|
||||
}
|
||||
$type = Arr::fetchConfigValue($baseConfig, 'mode') ?: 'local';//上次模式默认本地
|
||||
if ($isLocal) {
|
||||
$type = 'local';
|
||||
}
|
||||
$options = [];
|
||||
if (!empty($upload)) {
|
||||
$options['sub_dir'] = $upload;
|
||||
}
|
||||
$result = UploadFile::uploadFile($options);
|
||||
$data = $result[0];
|
||||
$hash = $data['unique_id'];
|
||||
|
||||
$url = str_replace('\\', '/', $data['url']);
|
||||
$path = str_replace('\\', '/', $data['save_path']);
|
||||
|
||||
// 检查文件是否已存在
|
||||
if ($filesInfo = $this->dao->get(['hash' => $hash])) {
|
||||
return $filesInfo;
|
||||
}
|
||||
|
||||
$inData = [
|
||||
'platform' => $type,
|
||||
'original_filename' => $data['origin_name'] ?? '',
|
||||
'filename' => $data['save_name'],
|
||||
'hash' => $hash,
|
||||
'content_type' => $data['mime_type'],
|
||||
'base_path' => $data['base_path'],
|
||||
'ext' => $data['extension'],
|
||||
'size' => $data['size'],
|
||||
'size_info' => formatBytes($data['size']),
|
||||
'url' => full_url($url),
|
||||
'path' => $path,
|
||||
];
|
||||
return $this->dao->save($inData);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
throw new AdminException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user