This commit is contained in:
2025-04-30 10:53:02 +08:00
commit 701b9a909f
1007 changed files with 353274 additions and 0 deletions
@@ -0,0 +1,312 @@
<?php
namespace plugin\admin\app\controller;
use plugin\admin\app\common\Auth;
use plugin\admin\app\common\Util;
use plugin\admin\app\model\Admin;
use plugin\admin\app\model\AdminRole;
use plugin\admin\app\model\User;
use support\exception\BusinessException;
use support\Request;
use support\Response;
use Throwable;
use Webman\Captcha\CaptchaBuilder;
use Webman\Captcha\PhraseBuilder;
use Webman\Config;
/**
* 管理员账户
*/
class AccountController extends Crud
{
/**
* 不需要登录的方法
* @var string[]
*/
protected $noNeedLogin = ['login', 'logout', 'captcha', 'register', 'registerPage'];
/**
* 不需要鉴权的方法
* @var string[]
*/
protected $noNeedAuth = ['info'];
/**
* @var Admin
*/
protected $model = null;
/**
* 构造函数
*/
public function __construct()
{
$this->model = new Admin;
}
/**
* 账户设置
* @return Response
* @throws Throwable
*/
public function index()
{
return raw_view('account/index');
}
/**
* 登录
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function login(Request $request): Response
{
$this->checkDatabaseAvailable();
$captcha = $request->post('captcha', '');
// if (strtolower($captcha) !== session('captcha-login')) {
// return $this->json(1, '验证码错误');
// }
$request->session()->forget('captcha-login');
$username = $request->post('username', '');
$password = $request->post('password', '');
if (!$username) {
return $this->json(1, '用户名不能为空');
}
$this->checkLoginLimit($username);
$admin = Admin::where('username', $username)->first();
if (!$admin || !Util::passwordVerify($password, $admin->password)) {
return $this->json(1, '账户不存在或密码错误');
}
if ($admin->status != 0) {
return $this->json(1, '当前账户暂时无法登录');
}
$admin->login_at = date('Y-m-d H:i:s');
$admin->save();
$this->removeLoginLimit($username);
$admin = $admin->toArray();
$session = $request->session();
$admin['password'] = md5($admin['password']);
$session->set('admin', $admin);
return $this->json(0, '登录成功', [
'nickname' => $admin['nickname'],
'token' => $request->sessionId(),
]);
}
/**
* 退出
* @param Request $request
* @return Response
*/
public function logout(Request $request): Response
{
$request->session()->delete('admin');
return $this->json(0);
}
/**
* 获取登录信息
* @param Request $request
* @return Response
*/
public function info(Request $request): Response
{
$admin = admin();
if (!$admin) {
return $this->json(1);
}
$info = [
'id' => $admin['id'],
'username' => $admin['username'],
'nickname' => $admin['nickname'],
'avatar' => $admin['avatar'],
'email' => $admin['email'],
'mobile' => $admin['mobile'],
'isSuperAdmin' => Auth::isSuperAdmin(),
'token' => $request->sessionId(),
];
return $this->json(0, 'ok', $info);
}
/**
* 更新
* @param Request $request
* @return Response
*/
public function update(Request $request): Response
{
$allow_column = [
'nickname' => 'nickname',
'avatar' => 'avatar',
'email' => 'email',
'mobile' => 'mobile',
];
$data = $request->post();
$update_data = [];
foreach ($allow_column as $key => $column) {
if (isset($data[$key])) {
$update_data[$column] = $data[$key];
}
}
if (isset($update_data['password'])) {
$update_data['password'] = Util::passwordHash($update_data['password']);
}
Admin::where('id', admin_id())->update($update_data);
$admin = admin();
unset($update_data['password']);
foreach ($update_data as $key => $value) {
$admin[$key] = $value;
}
$request->session()->set('admin', $admin);
return $this->json(0);
}
/**
* 修改密码
* @param Request $request
* @return Response
*/
public function password(Request $request): Response
{
$hash = Admin::find(admin_id())['password'];
$password = $request->post('password');
if (!$password) {
return $this->json(2, '密码不能为空');
}
if ($request->post('password_confirm') !== $password) {
return $this->json(3, '两次密码输入不一致');
}
if (!Util::passwordVerify($request->post('old_password'), $hash)) {
return $this->json(1, '原始密码不正确');
}
$update_data = [
'password' => Util::passwordHash($password)
];
Admin::where('id', admin_id())->update($update_data);
return $this->json(0);
}
/**
* 验证码
* @param Request $request
* @param string $type
* @return Response
*/
public function captcha(Request $request, string $type = 'login'): Response
{
$builder = new PhraseBuilder(4, 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ');
$captcha = new CaptchaBuilder(null, $builder);
$captcha->build(120);
$request->session()->set("captcha-$type", strtolower($captcha->getPhrase()));
$img_content = $captcha->get();
return response($img_content, 200, ['Content-Type' => 'image/jpeg']);
}
/**
* 检查登录频率限制
* @param $username
* @return void
* @throws BusinessException
*/
protected function checkLoginLimit($username)
{
$limit_log_path = runtime_path() . '/login';
if (!is_dir($limit_log_path)) {
mkdir($limit_log_path, 0777, true);
}
$limit_file = $limit_log_path . '/' . md5($username) . '.limit';
$time = date('YmdH') . ceil(date('i')/5);
$limit_info = [];
if (is_file($limit_file)) {
$json_str = file_get_contents($limit_file);
$limit_info = json_decode($json_str, true);
}
if (!$limit_info || $limit_info['time'] != $time) {
$limit_info = [
'username' => $username,
'count' => 0,
'time' => $time
];
}
$limit_info['count']++;
file_put_contents($limit_file, json_encode($limit_info));
if ($limit_info['count'] >= 5) {
throw new BusinessException('登录失败次数过多,请5分钟后再试');
}
}
/**
* 解除登录频率限制
* @param $username
* @return void
*/
protected function removeLoginLimit($username)
{
$limit_log_path = runtime_path() . '/login';
$limit_file = $limit_log_path . '/' . md5($username) . '.limit';
if (is_file($limit_file)) {
unlink($limit_file);
}
}
protected function checkDatabaseAvailable()
{
if (!config('plugin.admin.database')) {
throw new BusinessException('请重启webman');
}
}
/**
* 注册页面
* @return Response
*/
public function registerPage(Request $request): Response
{
$config = Config::get('plugin.admin.app', []);
$data = [
'logo' => $config['logo'] ?? '/app/admin/admin/images/logo.png',
'title' => $config['name'] ?? '管理系统',
];
return view('account/register', $data);
}
/**
* 注册
* @param Request $request
* @return Response
*/
public function register(Request $request): Response
{
$username = $request->post('username');
$nickname = $request->post('nickname');
$password = $request->post('password');
if (empty($username) || empty($password) || empty($nickname)) {
return json(['code' => 1, 'msg' => '用户名、昵称和密码不能为空']);
}
// 检查用户名是否已存在
if (Admin::where('username', $username)->first()) {
return json(['code' => 1, 'msg' => '用户名已存在']);
}
// 创建新用户
$user = new Admin();
$user->username = $username;
$user->nickname = $nickname;
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->created_at = date('Y-m-d H:i:s');
$user->save();
$adminRole = new AdminRole();
$adminRole->admin_id = $user->id;
$adminRole->role_id = 3;
$adminRole->save();
return json(['code' => 0, 'msg' => '注册成功']);
}
}
@@ -0,0 +1,261 @@
<?php
namespace plugin\admin\app\controller;
use plugin\admin\app\common\Auth;
use plugin\admin\app\common\Util;
use plugin\admin\app\model\Admin;
use plugin\admin\app\model\AdminRole;
use support\exception\BusinessException;
use support\Request;
use support\Response;
use Throwable;
use Webman\Captcha\CaptchaBuilder;
use Webman\Captcha\PhraseBuilder;
/**
* 管理员账户
*/
class AccountController extends Crud
{
/**
* 不需要登录的方法
* @var string[]
*/
protected $noNeedLogin = ['login', 'logout', 'captcha', 'register', 'registerPage'];
/**
* 不需要鉴权的方法
* @var string[]
*/
protected $noNeedAuth = ['info'];
/**
* @var Admin
*/
protected $model = null;
/**
* 构造函数
*/
public function __construct()
{
$this->model = new Admin;
}
/**
* 账户设置
* @return Response
* @throws Throwable
*/
public function index()
{
return raw_view('account/index');
}
/**
* 登录
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function login(Request $request): Response
{
$this->checkDatabaseAvailable();
$captcha = $request->post('captcha', '');
if (strtolower($captcha) !== session('captcha-login')) {
return $this->json(1, '验证码错误');
}
$request->session()->forget('captcha-login');
$username = $request->post('username', '');
$password = $request->post('password', '');
if (!$username) {
return $this->json(1, '用户名不能为空');
}
$this->checkLoginLimit($username);
$admin = Admin::where('username', $username)->first();
if (!$admin || !Util::passwordVerify($password, $admin->password)) {
return $this->json(1, '账户不存在或密码错误');
}
if ($admin->status != 0) {
return $this->json(1, '当前账户暂时无法登录');
}
$admin->login_at = date('Y-m-d H:i:s');
$admin->save();
$this->removeLoginLimit($username);
$admin = $admin->toArray();
$session = $request->session();
$admin['password'] = md5($admin['password']);
$session->set('admin', $admin);
return $this->json(0, '登录成功', [
'nickname' => $admin['nickname'],
'token' => $request->sessionId(),
]);
}
/**
* 退出
* @param Request $request
* @return Response
*/
public function logout(Request $request): Response
{
$request->session()->delete('admin');
return $this->json(0);
}
/**
* 获取登录信息
* @param Request $request
* @return Response
*/
public function info(Request $request): Response
{
$admin = admin();
if (!$admin) {
return $this->json(1);
}
$info = [
'id' => $admin['id'],
'username' => $admin['username'],
'nickname' => $admin['nickname'],
'avatar' => $admin['avatar'],
'email' => $admin['email'],
'mobile' => $admin['mobile'],
'isSuperAdmin' => Auth::isSuperAdmin(),
'token' => $request->sessionId(),
];
return $this->json(0, 'ok', $info);
}
/**
* 更新
* @param Request $request
* @return Response
*/
public function update(Request $request): Response
{
$allow_column = [
'nickname' => 'nickname',
'avatar' => 'avatar',
'email' => 'email',
'mobile' => 'mobile',
];
$data = $request->post();
$update_data = [];
foreach ($allow_column as $key => $column) {
if (isset($data[$key])) {
$update_data[$column] = $data[$key];
}
}
if (isset($update_data['password'])) {
$update_data['password'] = Util::passwordHash($update_data['password']);
}
Admin::where('id', admin_id())->update($update_data);
$admin = admin();
unset($update_data['password']);
foreach ($update_data as $key => $value) {
$admin[$key] = $value;
}
$request->session()->set('admin', $admin);
return $this->json(0);
}
/**
* 修改密码
* @param Request $request
* @return Response
*/
public function password(Request $request): Response
{
$hash = Admin::find(admin_id())['password'];
$password = $request->post('password');
if (!$password) {
return $this->json(2, '密码不能为空');
}
if ($request->post('password_confirm') !== $password) {
return $this->json(3, '两次密码输入不一致');
}
if (!Util::passwordVerify($request->post('old_password'), $hash)) {
return $this->json(1, '原始密码不正确');
}
$update_data = [
'password' => Util::passwordHash($password)
];
Admin::where('id', admin_id())->update($update_data);
return $this->json(0);
}
/**
* 验证码
* @param Request $request
* @param string $type
* @return Response
*/
public function captcha(Request $request, string $type = 'login'): Response
{
$builder = new PhraseBuilder(4, 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ');
$captcha = new CaptchaBuilder(null, $builder);
$captcha->build(120);
$request->session()->set("captcha-$type", strtolower($captcha->getPhrase()));
$img_content = $captcha->get();
return response($img_content, 200, ['Content-Type' => 'image/jpeg']);
}
/**
* 检查登录频率限制
* @param $username
* @return void
* @throws BusinessException
*/
protected function checkLoginLimit($username)
{
$limit_log_path = runtime_path() . '/login';
if (!is_dir($limit_log_path)) {
mkdir($limit_log_path, 0777, true);
}
$limit_file = $limit_log_path . '/' . md5($username) . '.limit';
$time = date('YmdH') . ceil(date('i')/5);
$limit_info = [];
if (is_file($limit_file)) {
$json_str = file_get_contents($limit_file);
$limit_info = json_decode($json_str, true);
}
if (!$limit_info || $limit_info['time'] != $time) {
$limit_info = [
'username' => $username,
'count' => 0,
'time' => $time
];
}
$limit_info['count']++;
file_put_contents($limit_file, json_encode($limit_info));
if ($limit_info['count'] >= 5) {
throw new BusinessException('登录失败次数过多,请5分钟后再试');
}
}
/**
* 解除登录频率限制
* @param $username
* @return void
*/
protected function removeLoginLimit($username)
{
$limit_log_path = runtime_path() . '/login';
$limit_file = $limit_log_path . '/' . md5($username) . '.limit';
if (is_file($limit_file)) {
unlink($limit_file);
}
}
protected function checkDatabaseAvailable()
{
if (!config('plugin.admin.database')) {
throw new BusinessException('请重启webman');
}
}
}
@@ -0,0 +1 @@
@@ -0,0 +1,210 @@
<?php
namespace plugin\admin\app\controller;
use plugin\admin\app\common\Auth;
use plugin\admin\app\model\Admin;
use plugin\admin\app\model\AdminRole;
use support\exception\BusinessException;
use support\Request;
use support\Response;
use Throwable;
/**
* 管理员列表
*/
class AdminController extends Crud
{
/**
* 不需要鉴权的方法
* @var array
*/
protected $noNeedAuth = ['select'];
/**
* @var Admin
*/
protected $model = null;
/**
* 开启auth数据限制
* @var string
*/
protected $dataLimit = 'auth';
/**
* 以id为数据限制字段
* @var string
*/
protected $dataLimitField = 'id';
/**
* 构造函数
* @return void
*/
public function __construct()
{
$this->model = new Admin;
}
/**
* 浏览
* @return Response
* @throws Throwable
*/
public function index(): Response
{
return raw_view('admin/index');
}
/**
* 查询
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function select(Request $request): Response
{
[$where, $format, $limit, $field, $order] = $this->selectInput($request);
$query = $this->doSelect($where, $field, $order);
if ($format === 'select') {
return $this->formatSelect($query->get());
}
$paginator = $query->paginate($limit);
$items = $paginator->items();
$admin_ids = array_column($items, 'id');
$roles = AdminRole::whereIn('admin_id', $admin_ids)->get();
$roles_map = [];
foreach ($roles as $role) {
$roles_map[$role['admin_id']][] = $role['role_id'];
}
$login_admin_id = admin_id();
foreach ($items as $index => $item) {
$admin_id = $item['id'];
$items[$index]['roles'] = isset($roles_map[$admin_id]) ? implode(',', $roles_map[$admin_id]) : '';
$items[$index]['show_toolbar'] = $admin_id != $login_admin_id;
}
return json(['code' => 0, 'msg' => 'ok', 'count' => $paginator->total(), 'data' => $items]);
}
/**
* 插入
* @param Request $request
* @return Response
* @throws BusinessException|Throwable
*/
public function insert(Request $request): Response
{
if ($request->method() === 'POST') {
$data = $this->insertInput($request);
unset($data['id']);
$admin_id = $this->doInsert($data);
$role_ids = $request->post('roles');
$role_ids = $role_ids ? explode(',', $role_ids) : [];
if (!$role_ids) {
return $this->json(1, '至少选择一个角色组');
}
if (!Auth::isSuperAdmin() && array_diff($role_ids, Auth::getScopeRoleIds())) {
return $this->json(1, '角色超出权限范围');
}
AdminRole::where('admin_id', $admin_id)->delete();
foreach ($role_ids as $id) {
$admin_role = new AdminRole;
$admin_role->admin_id = $admin_id;
$admin_role->role_id = $id;
$admin_role->save();
}
return $this->json(0, 'ok', ['id' => $admin_id]);
}
return raw_view('admin/insert');
}
/**
* 更新
* @param Request $request
* @return Response
* @throws BusinessException|Throwable
*/
public function update(Request $request): Response
{
if ($request->method() === 'POST') {
[$id, $data] = $this->updateInput($request);
$admin_id = $request->post('id');
if (!$admin_id) {
return $this->json(1, '缺少参数');
}
// 不能禁用自己
if (isset($data['status']) && $data['status'] == 1 && $id == admin_id()) {
return $this->json(1, '不能禁用自己');
}
// 需要更新角色
$role_ids = $request->post('roles');
if ($role_ids !== null) {
if (!$role_ids) {
return $this->json(1, '至少选择一个角色组');
}
$role_ids = explode(',', $role_ids);
$is_supper_admin = Auth::isSuperAdmin();
$exist_role_ids = AdminRole::where('admin_id', $admin_id)->pluck('role_id')->toArray();
$scope_role_ids = Auth::getScopeRoleIds();
if (!$is_supper_admin && !array_intersect($exist_role_ids, $scope_role_ids)) {
return $this->json(1, '无权限更改该记录');
}
if (!$is_supper_admin && array_diff($role_ids, $scope_role_ids)) {
return $this->json(1, '角色超出权限范围');
}
// 删除账户角色
$delete_ids = array_diff($exist_role_ids, $role_ids);
AdminRole::whereIn('role_id', $delete_ids)->where('admin_id', $admin_id)->delete();
// 添加账户角色
$add_ids = array_diff($role_ids, $exist_role_ids);
foreach ($add_ids as $role_id) {
$admin_role = new AdminRole;
$admin_role->admin_id = $admin_id;
$admin_role->role_id = $role_id;
$admin_role->save();
}
}
$this->doUpdate($id, $data);
return $this->json(0);
}
return raw_view('admin/update');
}
/**
* 删除
* @param Request $request
* @return Response
*/
public function delete(Request $request): Response
{
$primary_key = $this->model->getKeyName();
$ids = $request->post($primary_key);
if (!$ids) {
return $this->json(0);
}
$ids = (array)$ids;
if (in_array(admin_id(), $ids)) {
return $this->json(1, '不能删除自己');
}
if (!Auth::isSuperAdmin() && array_diff($ids, Auth::getScopeAdminIds())) {
return $this->json(1, '无数据权限');
}
$this->model->whereIn($primary_key, $ids)->each(function (Admin $admin) {
$admin->delete();
});
AdminRole::whereIn('admin_id', $ids)->each(function (AdminRole $admin_role) {
$admin_role->delete();
});
return $this->json(0);
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace plugin\admin\app\controller;
use support\Model;
use support\Response;
/**
* 基础控制器
*/
class Base
{
/**
* @var Model
*/
protected $model = null;
/**
* 无需登录及鉴权的方法
* @var array
*/
protected $noNeedLogin = [];
/**
* 需要登录无需鉴权的方法
* @var array
*/
protected $noNeedAuth = [];
/**
* 数据限制
* null 不做限制,任何管理员都可以查看该表的所有数据
* auth 管理员能看到自己以及自己的子管理员插入的数据
* personal 管理员只能看到自己插入的数据
* @var string
*/
protected $dataLimit = null;
/**
* 数据限制字段
*/
protected $dataLimitField = 'admin_id';
/**
* 返回格式化json数据
*
* @param int $code
* @param string $msg
* @param array $data
* @return Response
*/
protected function json(int $code, string $msg = 'ok', array $data = []): Response
{
return json(['code' => $code, 'data' => $data, 'msg' => $msg]);
}
protected function success(string $msg = '成功', array $data = []): Response
{
return $this->json(0, $msg, $data);
}
protected function fail(string $msg = '失败', array $data = []): Response
{
return $this->json(1, $msg, $data);
}
}
@@ -0,0 +1,148 @@
<?php
namespace plugin\admin\app\controller;
use plugin\admin\app\common\Util;
use plugin\admin\app\model\Option;
use support\exception\BusinessException;
use support\Request;
use support\Response;
use Throwable;
/**
* 系统设置
*/
class ConfigController extends Base
{
/**
* 不需要验证权限的方法
* @var string[]
*/
protected $noNeedAuth = ['get'];
/**
* 账户设置
* @return Response
* @throws Throwable
*/
public function index(): Response
{
return raw_view('config/index');
}
/**
* 获取配置
* @return Response
*/
public function get(): Response
{
return json($this->getByDefault());
}
/**
* 基于配置文件获取默认权限
* @return mixed
*/
protected function getByDefault()
{
$name = 'system_config';
$config = Option::where('name', $name)->value('value');
if (empty($config)) {
$config = file_get_contents(base_path('plugin/admin/public/config/pear.config.json'));
if ($config) {
$option = new Option();
$option->name = $name;
$option->value = $config;
$option->save();
}
}
return json_decode($config, true);
}
/**
* 更改
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function update(Request $request): Response
{
$post = $request->post();
$config = $this->getByDefault();
$data = [];
foreach ($post as $section => $items) {
if (!isset($config[$section])) {
continue;
}
switch ($section) {
case 'logo':
$data[$section]['title'] = htmlspecialchars($items['title'] ?? '');
$data[$section]['image'] = Util::filterUrlPath($items['image'] ?? '');
$data[$section]['icp'] = htmlspecialchars($items['icp'] ?? '');
$data[$section]['beian'] = htmlspecialchars($items['beian'] ?? '');
$data[$section]['footer_txt'] = htmlspecialchars($items['footer_txt'] ?? '');
break;
case 'menu':
$data[$section]['data'] = Util::filterUrlPath($items['data'] ?? '');
$data[$section]['accordion'] = !empty($items['accordion']);
$data[$section]['collapse'] = !empty($items['collapse']);
$data[$section]['control'] = !empty($items['control']);
$data[$section]['controlWidth'] = (int)($items['controlWidth'] ?? 2000);
$data[$section]['select'] = (int)$items['select'] ?? 0;
$data[$section]['async'] = true;
break;
case 'tab':
$data[$section]['enable'] = true;
$data[$section]['keepState'] = !empty($items['keepState']);
$data[$section]['preload'] = !empty($items['preload']);
$data[$section]['session'] = !empty($items['session']);
$data[$section]['max'] = Util::filterNum($items['max'] ?? '30');
$data[$section]['index']['id'] = Util::filterNum($items['index']['id'] ?? '0');
$data[$section]['index']['href'] = Util::filterUrlPath($items['index']['href'] ?? '');
$data[$section]['index']['title'] = htmlspecialchars($items['index']['title'] ?? '首页');
break;
case 'theme':
$data[$section]['defaultColor'] = Util::filterNum($items['defaultColor'] ?? '2');
$data[$section]['defaultMenu'] = $items['defaultMenu'] ?? '' == 'dark-theme' ? 'dark-theme' : 'light-theme';
$data[$section]['defaultHeader'] = $items['defaultHeader'] ?? '' == 'dark-theme' ? 'dark-theme' : 'light-theme';
$data[$section]['allowCustom'] = !empty($items['allowCustom']);
$data[$section]['banner'] = !empty($items['banner']);
break;
case 'colors':
foreach ($config['colors'] as $index => $item) {
if (!isset($items[$index])) {
$config['colors'][$index] = $item;
continue;
}
$data_item = $items[$index];
$data[$section][$index]['id'] = $index + 1;
$data[$section][$index]['color'] = $this->filterColor($data_item['color'] ?? '');
$data[$section][$index]['second'] = $this->filterColor($data_item['second'] ?? '');
}
break;
}
}
$config = array_merge($config, $data);
$name = 'system_config';
Option::where('name', $name)->update([
'value' => json_encode($config)
]);
return $this->json(0);
}
/**
* 颜色检查
* @param string $color
* @return string
* @throws BusinessException
*/
protected function filterColor(string $color): string
{
if (!preg_match('/\#[a-zA-Z]6/', $color)) {
throw new BusinessException('参数错误');
}
return $color;
}
}
+469
View File
@@ -0,0 +1,469 @@
<?php
namespace plugin\admin\app\controller;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use plugin\admin\app\common\Auth;
use plugin\admin\app\common\Tree;
use plugin\admin\app\common\Util;
use support\exception\BusinessException;
use support\Model;
use support\Request;
use support\Response;
class Crud extends Base
{
/**
* @var Model
*/
protected $model = null;
/**
* 查询
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function select(Request $request): Response
{
[$where, $format, $limit, $field, $order] = $this->selectInput($request);
$query = $this->doSelect($where, $field, $order);
return $this->doFormat($query, $format, $limit);
}
/**
* 添加
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function insert(Request $request): Response
{
$data = $this->insertInput($request);
$id = $this->doInsert($data);
return $this->json(0, 'ok', ['id' => $id]);
}
/**
* 更新
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function update(Request $request): Response
{
[$id, $data] = $this->updateInput($request);
$this->doUpdate($id, $data);
return $this->json(0);
}
/**
* 删除
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function delete(Request $request): Response
{
$ids = $this->deleteInput($request);
$this->doDelete($ids);
return $this->json(0);
}
/**
* 查询前置
* @param Request $request
* @return array
* @throws BusinessException
*/
protected function selectInput(Request $request): array
{
$field = $request->get('field');
$order = $request->get('order', 'asc');
$format = $request->get('format', 'normal');
$limit = (int)$request->get('limit', $format === 'tree' ? 1000 : 10);
$limit = $limit <= 0 ? 10 : $limit;
$order = $order === 'asc' ? 'asc' : 'desc';
$where = $request->get();
$page = (int)$request->get('page');
$page = $page > 0 ? $page : 1;
$table = config('plugin.admin.database.connections.mysql.prefix') . $this->model->getTable();
$allow_column = Util::db()->select("desc `$table`");
if (!$allow_column) {
throw new BusinessException('表不存在');
}
$allow_column = array_column($allow_column, 'Field', 'Field');
if (!in_array($field, $allow_column)) {
$field = null;
}
foreach ($where as $column => $value) {
if (
$value === '' || !isset($allow_column[$column]) ||
is_array($value) && (empty($value) || !in_array($value[0], ['null', 'not null']) && !isset($value[1]))
) {
unset($where[$column]);
}
}
// 按照数据限制字段返回数据
if (!Auth::isSuperAdmin()) {
if ($this->dataLimit === 'personal') {
$where[$this->dataLimitField] = admin_id();
} elseif ($this->dataLimit === 'auth') {
$primary_key = $this->model->getKeyName();
if (!Auth::isSuperAdmin() && (!isset($where[$primary_key]) || $this->dataLimitField != $primary_key)) {
$where[$this->dataLimitField] = ['in', Auth::getScopeAdminIds(true)];
}
}
}
return [$where, $format, $limit, $field, $order, $page];
}
/**
* 指定查询where条件,并没有真正的查询数据库操作
* @param array $where
* @param string|null $field
* @param string $order
* @return EloquentBuilder|QueryBuilder|Model
*/
protected function doSelect(array $where, ?string $field = null, string $order= 'desc')
{
$model = $this->model;
foreach ($where as $column => $value) {
if (is_array($value)) {
if ($value[0] === 'like' || $value[0] === 'not like') {
$model = $model->where($column, $value[0], "%$value[1]%");
} elseif (in_array($value[0], ['>', '=', '<', '<>'])) {
$model = $model->where($column, $value[0], $value[1]);
} elseif ($value[0] == 'in' && !empty($value[1])) {
$valArr = $value[1];
if (is_string($value[1])) {
$valArr = explode(",", trim($value[1]));
}
$model = $model->whereIn($column, $valArr);
} elseif ($value[0] == 'not in' && !empty($value[1])) {
$valArr = $value[1];
if (is_string($value[1])) {
$valArr = explode(",", trim($value[1]));
}
$model = $model->whereNotIn($column, $valArr);
}elseif ($value[0] == 'null') {
$model = $model->whereNull($column);
} elseif ($value[0] == 'not null') {
$model = $model->whereNotNull($column);
} elseif ($value[0] !== '' || $value[1] !== '') {
$model = $model->whereBetween($column, $value);
}
} else {
$model = $model->where($column, $value);
}
}
if ($field) {
$model = $model->orderBy($field, $order);
}
return $model;
}
/**
* 执行真正查询,并返回格式化数据
* @param $query
* @param $format
* @param $limit
* @return Response
*/
protected function doFormat($query, $format, $limit): Response
{
$methods = [
'select' => 'formatSelect',
'tree' => 'formatTree',
'table_tree' => 'formatTableTree',
'normal' => 'formatNormal',
];
$paginator = $query->paginate($limit);
$total = $paginator->total();
$items = $paginator->items();
if (method_exists($this, "afterQuery")) {
$items = call_user_func([$this, "afterQuery"], $items);
}
$format_function = $methods[$format] ?? 'formatNormal';
return call_user_func([$this, $format_function], $items, $total);
}
/**
* 插入前置方法
* @param Request $request
* @return array
* @throws BusinessException
*/
protected function insertInput(Request $request): array
{
$data = $this->inputFilter($request->post());
$password_filed = 'password';
if (isset($data[$password_filed])) {
$data[$password_filed] = Util::passwordHash($data[$password_filed]);
}
if (!Auth::isSuperAdmin()) {
if ($this->dataLimit === 'personal') {
$data[$this->dataLimitField] = admin_id();
} elseif ($this->dataLimit === 'auth') {
if (!empty($data[$this->dataLimitField])) {
$admin_id = $data[$this->dataLimitField];
if (!in_array($admin_id, Auth::getScopeAdminIds(true))) {
throw new BusinessException('无数据权限');
}
} else {
$data[$this->dataLimitField] = admin_id();
}
}
} elseif ($this->dataLimit && empty($data[$this->dataLimitField])) {
$data[$this->dataLimitField] = admin_id();
}
return $data;
}
/**
* 执行插入
* @param array $data
* @return mixed|null
*/
protected function doInsert(array $data)
{
$primary_key = $this->model->getKeyName();
$model_class = get_class($this->model);
$model = new $model_class;
foreach ($data as $key => $val) {
$model->{$key} = $val;
}
$model->save();
return $primary_key ? $model->$primary_key : null;
}
/**
* 更新前置方法
* @param Request $request
* @return array
* @throws BusinessException
*/
protected function updateInput(Request $request): array
{
$primary_key = $this->model->getKeyName();
$id = $request->post($primary_key);
$data = $this->inputFilter($request->post());
$model = $this->model->find($id);
if (!$model) {
throw new BusinessException('记录不存在', 2);
}
if (!Auth::isSuperAdmin()) {
if ($this->dataLimit == 'personal') {
if ($model->{$this->dataLimitField} != admin_id()) {
throw new BusinessException('无数据权限');
}
} elseif ($this->dataLimit == 'auth') {
$scopeAdminIds = Auth::getScopeAdminIds(true);
$admin_ids = [
$data[$this->dataLimitField] ?? false, // 检查要更新的数据admin_id是否是有权限的值
$model->{$this->dataLimitField} ?? false // 检查要更新的记录的admin_id是否有权限
];
foreach ($admin_ids as $admin_id) {
if ($admin_id && !in_array($admin_id, $scopeAdminIds)) {
throw new BusinessException('无数据权限');
}
}
}
}
$password_filed = 'password';
if (isset($data[$password_filed])) {
// 密码为空,则不更新密码
if ($data[$password_filed] === '') {
unset($data[$password_filed]);
} else {
$data[$password_filed] = Util::passwordHash($data[$password_filed]);
}
}
unset($data[$primary_key]);
return [$id, $data];
}
/**
* 执行更新
* @param $id
* @param $data
* @return void
*/
protected function doUpdate($id, $data)
{
$model = $this->model->find($id);
foreach ($data as $key => $val) {
$model->{$key} = $val;
}
$model->save();
}
/**
* 对用户输入表单过滤
* @param array $data
* @return array
* @throws BusinessException
*/
protected function inputFilter(array $data): array
{
$table = config('plugin.admin.database.connections.mysql.prefix') . $this->model->getTable();
$allow_column = $this->model->getConnection()->select("desc `$table`");
if (!$allow_column) {
throw new BusinessException('表不存在', 2);
}
$columns = array_column($allow_column, 'Type', 'Field');
foreach ($data as $col => $item) {
if (!isset($columns[$col])) {
unset($data[$col]);
continue;
}
// 非字符串类型传空则为null
if ($item === '' && strpos(strtolower($columns[$col]), 'varchar') === false && strpos(strtolower($columns[$col]), 'text') === false) {
$data[$col] = null;
}
if (is_array($item)) {
$data[$col] = implode(',', $item);
}
}
if (empty($data['created_at'])) {
unset($data['created_at']);
}
if (empty($data['updated_at'])) {
unset($data['updated_at']);
}
return $data;
}
/**
* 删除前置方法
* @param Request $request
* @return array
* @throws BusinessException
*/
protected function deleteInput(Request $request): array
{
$primary_key = $this->model->getKeyName();
if (!$primary_key) {
throw new BusinessException('该表无主键,不支持删除');
}
$ids = (array)$request->post($primary_key, []);
if (!Auth::isSuperAdmin()){
$admin_ids = [];
if ($this->dataLimit) {
$admin_ids = $this->model->where($primary_key, $ids)->pluck($this->dataLimitField)->toArray();
}
if ($this->dataLimit == 'personal') {
if (!in_array(admin_id(), $admin_ids)) {
throw new BusinessException('无数据权限');
}
} elseif ($this->dataLimit == 'auth') {
if (array_diff($admin_ids, Auth::getScopeAdminIds(true))) {
throw new BusinessException('无数据权限');
}
}
}
return $ids;
}
/**
* 执行删除
* @param array $ids
* @return void
*/
protected function doDelete(array $ids)
{
if (!$ids) {
return;
}
$primary_key = $this->model->getKeyName();
$this->model->whereIn($primary_key, $ids)->each(function ($model) {
$model->delete();
});
}
/**
* 格式化树
* @param $items
* @return Response
*/
protected function formatTree($items): Response
{
$format_items = [];
$primary_key = $this->model->getKeyName();
foreach ($items as $item) {
$format_items[] = [
'name' => $this->guessName($item) ?: $item->$primary_key,
'value' => (string)$item->$primary_key,
'id' => $item->$primary_key,
'pid' => $item->pid,
];
}
$tree = new Tree($format_items);
return $this->json(0, 'ok', $tree->getTree());
}
/**
* 格式化表格树
* @param $items
* @return Response
*/
protected function formatTableTree($items): Response
{
$tree = new Tree($items);
return $this->json(0, 'ok', $tree->getTree());
}
/**
* 格式化下拉列表
* @param $items
* @return Response
*/
protected function formatSelect($items): Response
{
$formatted_items = [];
$primary_key = $this->model->getKeyName();
foreach ($items as $item) {
$formatted_items[] = [
'name' => $this->guessName($item) ?: $item->$primary_key,
'value' => $item->$primary_key
];
}
return $this->json(0, 'ok', $formatted_items);
}
/**
* 通用格式化
* @param $items
* @param $total
* @return Response
*/
protected function formatNormal($items, $total): Response
{
return json(['code' => 0, 'msg' => 'ok', 'count' => $total, 'data' => $items]);
}
/**
* 查询数据库后置方法,可用于修改数据
* @param mixed $items 原数据
* @return mixed 修改后数据
*/
protected function afterQuery($items)
{
return $items;
}
/**
* 猜测记录名称
* @param $item
* @return mixed
*/
protected function guessName($item)
{
return $item->title ?? $item->name ?? $item->nickname ?? $item->username ?? $item->id;
}
}
@@ -0,0 +1,23 @@
<?php
namespace plugin\admin\app\controller;
use support\Response;
use Throwable;
/**
* 开发辅助相关
*/
class DevController
{
/**
* 表单构建
* @return Response
* @throws Throwable
*/
public function formBuild()
{
return raw_view('dev/form-build');
}
}
@@ -0,0 +1,122 @@
<?php
namespace plugin\admin\app\controller;
use plugin\admin\app\model\Dict;
use plugin\admin\app\model\Option;
use support\exception\BusinessException;
use support\Request;
use support\Response;
use Throwable;
/**
* 字典管理
*/
class DictController extends Base
{
/**
* 不需要授权的方法
*/
protected $noNeedAuth = ['get'];
/**
* 浏览
* @return Response
* @throws Throwable
*/
public function index(): Response
{
return raw_view('dict/index');
}
/**
* 查询
* @param Request $request
* @return Response
*/
public function select(Request $request): Response
{
$name = $request->get('name', '');
$page = $request->get('page', 1);
$limit = $request->get('limit', 10);
$offset = ($page-1)*$limit;
if ($name && is_string($name)) {
$items = Option::where('name', 'like', "dict_$name%")->limit($limit)->offset($offset)->get()->toArray();
} else {
$items = Option::where('name', 'like', 'dict_%')->limit($limit)->offset($offset)->get()->toArray();
}
$get_items = Option::where('name', 'like', "dict_$name%")->get()->toArray();
$count = count($get_items);
foreach ($items as &$item) {
$item['name'] = Dict::optionNameTodictName($item['name']);
}
return json(['code' => 0, 'msg' => 'ok', 'count' => $count, 'data' => $items]);
}
/**
* 插入
* @param Request $request
* @return Response
* @throws BusinessException|Throwable
*/
public function insert(Request $request): Response
{
if ($request->method() === 'POST') {
$name = $request->post('name');
if (Dict::get($name)) {
return $this->json(1, '字典已经存在');
}
if (!preg_match('/^[a-zA-Z0-9]+$/', $name)) {
return $this->json(2, '字典名称只能是字母数字的组合');
}
$values = (array)$request->post('value', []);
Dict::save($name, $values);
}
return raw_view('dict/insert');
}
/**
* 更新
* @param Request $request
* @return Response
* @throws BusinessException|Throwable
*/
public function update(Request $request): Response
{
if ($request->method() === 'POST') {
$name = $request->post('name');
if (!Dict::get($name)) {
return $this->json(1, '字典不存在');
}
if (!preg_match('/^[a-zA-Z0-9]+$/', $name)) {
return $this->json(2, '字典名称只能是字母数字的组合');
}
Dict::save($name, $request->post('value'));
}
return raw_view('dict/update');
}
/**
* 删除
* @param Request $request
* @return Response
*/
public function delete(Request $request): Response
{
$names = (array)$request->post('name');
Dict::delete($names);
return $this->json(0);
}
/**
* 获取
* @param Request $request
* @param $name
* @return Response
*/
public function get(Request $request, $name): Response
{
return $this->json(0, 'ok', (array)Dict::get($name));
}
}
@@ -0,0 +1,97 @@
<?php
namespace plugin\admin\app\controller;
use plugin\admin\app\common\Util;
use plugin\admin\app\model\Option;
use plugin\admin\app\model\User;
use support\exception\BusinessException;
use support\Request;
use support\Response;
use think\db\Where;
use Throwable;
use Workerman\Worker;
class IndexController
{
/**
* 无需登录的方法
* @var string[]
*/
protected $noNeedLogin = ['index'];
/**
* 不需要鉴权的方法
* @var string[]
*/
protected $noNeedAuth = ['dashboard'];
/**
* 后台主页
* @param Request $request
* @return Response
* @throws BusinessException|Throwable
*/
public function index(Request $request): Response
{
clearstatcache();
if (!is_file(base_path('plugin/admin/config/database.php'))) {
return raw_view('index/install');
}
$admin = admin();
if (!$admin) {
$name = 'system_config';
$config = Option::where('name', $name)->value('value');
$config = json_decode($config, true);
$title = $config['logo']['title'] ?? 'webman admin';
$logo = $config['logo']['image'] ?? '/app/admin/admin/images/logo.png';
return raw_view('account/login',['logo'=>$logo,'title'=>$title]);
}
return raw_view('index/index');
}
/**
* 仪表板
* @param Request $request
* @return Response
* @throws Throwable
*/
public function dashboard(Request $request): Response
{
// 今日新增用户数
$today_user_count = User::where('created_at', '>', date('Y-m-d') . ' 00:00:00')->count();
// 7天内新增用户数
$day7_user_count = User::where('created_at', '>', date('Y-m-d H:i:s', time() - 7 * 24 * 60 * 60))->count();
// 30天内新增用户数
$day30_user_count = User::where('created_at', '>', date('Y-m-d H:i:s', time() - 30 * 24 * 60 * 60))->count();
// 总用户数
$user_count = User::count();
// mysql版本
$version = Util::db()->select('select VERSION() as version');
$mysql_version = $version[0]->version ?? 'unknown';
$day7_detail = [];
$now = time();
for ($i = 0; $i < 7; $i++) {
$date = date('Y-m-d', $now - 24 * 60 * 60 * $i);
$day7_detail[substr($date, 5)] = User::where('created_at', '>', "$date 00:00:00")
->where('created_at', '<', "$date 23:59:59")->count();
}
return raw_view('index/dashboard', [
'today_user_count' => $today_user_count,
'day7_user_count' => $day7_user_count,
'day30_user_count' => $day30_user_count,
'user_count' => $user_count,
'php_version' => PHP_VERSION,
'workerman_version' => Worker::VERSION,
'webman_version' => Util::getPackageVersion('workerman/webman-framework'),
'admin_version' => Util::getPackageVersion('webman/admin'),
'mysql_version' => $mysql_version,
'os' => PHP_OS,
'day7_detail' => array_reverse($day7_detail),
]);
}
}
@@ -0,0 +1,391 @@
<?php
namespace plugin\admin\app\controller;
use Illuminate\Database\Capsule\Manager;
use plugin\admin\app\common\Util;
use support\exception\BusinessException;
use support\Request;
use support\Response;
use Webman\Captcha\CaptchaBuilder;
/**
* 安装
*/
class InstallController extends Base
{
/**
* 不需要登录的方法
* @var string[]
*/
protected $noNeedLogin = ['step1', 'step2'];
/**
* 设置数据库
* @param Request $request
* @return Response
* @throws BusinessException|\Throwable
*/
public function step1(Request $request): Response
{
$database_config_file = base_path() . '/plugin/admin/config/database.php';
clearstatcache();
if (is_file($database_config_file)) {
return $this->json(1, '管理后台已经安装!如需重新安装,请删除该插件数据库配置文件并重启');
}
if (!class_exists(CaptchaBuilder::class) || !class_exists(Manager::class)) {
return $this->json(1, '请运行 composer require -W illuminate/database 安装illuminate/database组件并重启');
}
$user = $request->post('user');
$password = $request->post('password');
$database = $request->post('database');
$host = $request->post('host');
$port = (int)$request->post('port') ?: 3306;
$overwrite = $request->post('overwrite');
try {
$db = $this->getPdo($host, $user, $password, $port);
$smt = $db->query("show databases like '$database'");
if (empty($smt->fetchAll())) {
$db->exec("create database $database");
}
$db->exec("use $database");
$smt = $db->query("show tables");
$tables = $smt->fetchAll();
} catch (\Throwable $e) {
if (stripos($e, 'Access denied for user')) {
return $this->json(1, '数据库用户名或密码错误');
}
if (stripos($e, 'Connection refused')) {
return $this->json(1, 'Connection refused. 请确认数据库IP端口是否正确,数据库已经启动');
}
if (stripos($e, 'timed out')) {
return $this->json(1, '数据库连接超时,请确认数据库IP端口是否正确,安全组及防火墙已经放行端口');
}
throw $e;
}
$tables_to_install = [
'wa_admins',
'wa_admin_roles',
'wa_roles',
'wa_rules',
'wa_options',
'wa_users',
'wa_uploads',
];
$tables_exist = [];
foreach ($tables as $table) {
$tables_exist[] = current($table);
}
$tables_conflict = array_intersect($tables_to_install, $tables_exist);
if (!$overwrite) {
if ($tables_conflict) {
return $this->json(1, '以下表' . implode(',', $tables_conflict) . '已经存在,如需覆盖请选择强制覆盖');
}
} else {
foreach ($tables_conflict as $table) {
$db->exec("DROP TABLE `$table`");
}
}
$sql_file = base_path() . '/plugin/admin/install.sql';
if (!is_file($sql_file)) {
return $this->json(1, '数据库SQL文件不存在');
}
$sql_query = file_get_contents($sql_file);
$sql_query = $this->removeComments($sql_query);
$sql_query = $this->splitSqlFile($sql_query, ';');
foreach ($sql_query as $sql) {
$db->exec($sql);
}
// 导入菜单
$menus = include base_path() . '/plugin/admin/config/menu.php';
// 安装过程中没有数据库配置,无法使用api\Menu::import()方法
$this->importMenu($menus, $db);
$config_content = <<<EOF
<?php
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => '$host',
'port' => '$port',
'database' => '$database',
'username' => '$user',
'password' => '$password',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_general_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],
],
];
EOF;
file_put_contents($database_config_file, $config_content);
$think_orm_config = <<<EOF
<?php
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
// 数据库类型
'type' => 'mysql',
// 服务器地址
'hostname' => '$host',
// 数据库名
'database' => '$database',
// 数据库用户名
'username' => '$user',
// 数据库密码
'password' => '$password',
// 数据库连接端口
'hostport' => $port,
// 数据库连接参数
'params' => [
// 连接超时3秒
\PDO::ATTR_TIMEOUT => 3,
],
// 数据库编码默认采用utf8
'charset' => 'utf8mb4',
// 数据库表前缀
'prefix' => '',
// 断线重连
'break_reconnect' => true,
// 关闭SQL监听日志
'trigger_sql' => true,
// 自定义分页类
'bootstrap' => ''
],
],
];
EOF;
file_put_contents(base_path() . '/plugin/admin/config/thinkorm.php', $think_orm_config);
// 尝试reload
if (function_exists('posix_kill')) {
set_error_handler(function () {});
posix_kill(posix_getppid(), SIGUSR1);
restore_error_handler();
}
return $this->json(0);
}
/**
* 设置管理员
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function step2(Request $request): Response
{
$username = $request->post('username');
$password = $request->post('password');
$password_confirm = $request->post('password_confirm');
if ($password != $password_confirm) {
return $this->json(1, '两次密码不一致');
}
if (!is_file($config_file = base_path() . '/plugin/admin/config/database.php')) {
return $this->json(1, '请先完成第一步数据库配置');
}
$config = include $config_file;
$connection = $config['connections']['mysql'];
$pdo = $this->getPdo($connection['host'], $connection['username'], $connection['password'], $connection['port'], $connection['database']);
if ($pdo->query('select * from `wa_admins`')->fetchAll()) {
return $this->json(1, '后台已经安装完毕,无法通过此页面创建管理员');
}
$smt = $pdo->prepare("insert into `wa_admins` (`username`, `password`, `nickname`, `created_at`, `updated_at`) values (:username, :password, :nickname, :created_at, :updated_at)");
$time = date('Y-m-d H:i:s');
$data = [
'username' => $username,
'password' => Util::passwordHash($password),
'nickname' => '超级管理员',
'created_at' => $time,
'updated_at' => $time
];
foreach ($data as $key => $value) {
$smt->bindValue($key, $value);
}
$smt->execute();
$admin_id = $pdo->lastInsertId();
$smt = $pdo->prepare("insert into `wa_admin_roles` (`role_id`, `admin_id`) values (:role_id, :admin_id)");
$smt->bindValue('role_id', 1);
$smt->bindValue('admin_id', $admin_id);
$smt->execute();
$request->session()->flush();
return $this->json(0);
}
/**
* 添加菜单
* @param array $menu
* @param \PDO $pdo
* @return int
*/
protected function addMenu(array $menu, \PDO $pdo): int
{
$allow_columns = ['title', 'key', 'icon', 'href', 'pid', 'weight', 'type'];
$data = [];
foreach ($allow_columns as $column) {
if (isset($menu[$column])) {
$data[$column] = $menu[$column];
}
}
$time = date('Y-m-d H:i:s');
$data['created_at'] = $data['updated_at'] = $time;
$values = [];
foreach ($data as $k => $v) {
$values[] = ":$k";
}
$columns = array_keys($data);
foreach ($columns as $k => $column) {
$columns[$k] = "`$column`";
}
$sql = "insert into wa_rules (" .implode(',', $columns). ") values (" . implode(',', $values) . ")";
$smt = $pdo->prepare($sql);
foreach ($data as $key => $value) {
$smt->bindValue($key, $value);
}
$smt->execute();
return $pdo->lastInsertId();
}
/**
* 导入菜单
* @param array $menu_tree
* @param \PDO $pdo
* @return void
*/
protected function importMenu(array $menu_tree, \PDO $pdo)
{
if (is_numeric(key($menu_tree)) && !isset($menu_tree['key'])) {
foreach ($menu_tree as $item) {
$this->importMenu($item, $pdo);
}
return;
}
$children = $menu_tree['children'] ?? [];
unset($menu_tree['children']);
$smt = $pdo->prepare("select * from wa_rules where `key`=:key limit 1");
$smt->execute(['key' => $menu_tree['key']]);
$old_menu = $smt->fetch();
if ($old_menu) {
$pid = $old_menu['id'];
$params = [
'title' => $menu_tree['title'],
'icon' => $menu_tree['icon'] ?? '',
'key' => $menu_tree['key'],
];
$sql = "update wa_rules set title=:title, icon=:icon where `key`=:key";
$smt = $pdo->prepare($sql);
$smt->execute($params);
} else {
$pid = $this->addMenu($menu_tree, $pdo);
}
foreach ($children as $menu) {
$menu['pid'] = $pid;
$this->importMenu($menu, $pdo);
}
}
/**
* 去除sql文件中的注释
* @param $sql
* @return string
*/
protected function removeComments($sql): string
{
return preg_replace("/(\n--[^\n]*)/","", $sql);
}
/**
* 分割sql文件
* @param $sql
* @param $delimiter
* @return array
*/
function splitSqlFile($sql, $delimiter): array
{
$tokens = explode($delimiter, $sql);
$output = array();
$matches = array();
$token_count = count($tokens);
for ($i = 0; $i < $token_count; $i++) {
if (($i != ($token_count - 1)) || (strlen($tokens[$i] > 0))) {
$total_quotes = preg_match_all("/'/", $tokens[$i], $matches);
$escaped_quotes = preg_match_all("/(?<!\\\\)(\\\\\\\\)*\\\\'/", $tokens[$i], $matches);
$unescaped_quotes = $total_quotes - $escaped_quotes;
if (($unescaped_quotes % 2) == 0) {
$output[] = $tokens[$i];
$tokens[$i] = "";
} else {
$temp = $tokens[$i] . $delimiter;
$tokens[$i] = "";
$complete_stmt = false;
for ($j = $i + 1; (!$complete_stmt && ($j < $token_count)); $j++) {
$total_quotes = preg_match_all("/'/", $tokens[$j], $matches);
$escaped_quotes = preg_match_all("/(?<!\\\\)(\\\\\\\\)*\\\\'/", $tokens[$j], $matches);
$unescaped_quotes = $total_quotes - $escaped_quotes;
if (($unescaped_quotes % 2) == 1) {
$output[] = $temp . $tokens[$j];
$tokens[$j] = "";
$temp = "";
$complete_stmt = true;
$i = $j;
} else {
$temp .= $tokens[$j] . $delimiter;
$tokens[$j] = "";
}
}
}
}
}
return $output;
}
/**
* 获取pdo连接
* @param $host
* @param $username
* @param $password
* @param $port
* @param $database
* @return \PDO
*/
protected function getPdo($host, $username, $password, $port, $database = null): \PDO
{
$dsn = "mysql:host=$host;port=$port;";
if ($database) {
$dsn .= "dbname=$database";
}
$params = [
\PDO::MYSQL_ATTR_INIT_COMMAND => "set names utf8mb4",
\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
\PDO::ATTR_EMULATE_PREPARES => false,
\PDO::ATTR_TIMEOUT => 5,
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
];
return new \PDO($dsn, $username, $password, $params);
}
}
@@ -0,0 +1,79 @@
<?php
namespace plugin\admin\app\controller;
use support\Request;
use support\Response;
use plugin\admin\app\model\Location;
use plugin\admin\app\controller\Crud;
use support\exception\BusinessException;
use util\Util;
/**
* 仓库/港口
*/
class LocationController extends Crud
{
/**
* @var Location
*/
protected $model = null;
/**
* 构造函数
* @return void
*/
public function __construct()
{
$this->model = new Location;
}
/**
* 浏览
* @return Response
*/
public function index(): Response
{
return view('location/index');
}
/**
* 插入
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function insert(Request $request): Response
{
if ($request->method() === 'POST') {
return parent::insert($request);
}
return view('location/insert');
}
/**
* 更新
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function update(Request $request): Response
{
if ($request->method() === 'POST') {
return parent::update($request);
}
return view('location/update');
}
/**
* 获取下拉选项接口
*/
public function getAll()
{
$locations = Location::get();
return Util::success($locations);
}
}
@@ -0,0 +1,548 @@
<?php
namespace plugin\admin\app\controller;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use plugin\admin\app\common\Util;
use process\Monitor;
use support\exception\BusinessException;
use support\Log;
use support\Request;
use support\Response;
use ZIPARCHIVE;
use function array_diff;
use function ini_get;
use function scandir;
use const DIRECTORY_SEPARATOR;
use const PATH_SEPARATOR;
class PluginController extends Base
{
/**
* 不需要鉴权的方法
* @var string[]
*/
protected $noNeedAuth = ['schema', 'captcha'];
/**
* @param Request $request
* @return string
* @throws GuzzleException
*/
public function index(Request $request)
{
$client = $this->httpClient();
$response = $client->get("/webman-admin/apps");
return (string)$response->getBody();
}
/**
* 列表
* @param Request $request
* @return Response
* @throws GuzzleException
*/
public function list(Request $request): Response
{
$installed = $this->getLocalPlugins();
$client = $this->httpClient();
$query = $request->get();
$query['version'] = $this->getAdminVersion();
$response = $client->get('/api/app/list', ['query' => $query]);
$content = $response->getBody()->getContents();
$data = json_decode($content, true);
if (!$data) {
$msg = "/api/app/list return $content";
echo "msg\r\n";
Log::error($msg);
return $this->json(1, '获取数据出错');
}
$disabled = is_phar();
foreach ($data['data']['items'] as $key => $item) {
$name = $item['name'];
$data['data']['items'][$key]['installed'] = $installed[$name] ?? 0;
$data['data']['items'][$key]['disabled'] = $disabled;
}
$items = $data['data']['items'];
$count = $data['data']['total'];
return json(['code' => 0, 'msg' => 'ok', 'data' => $items, 'count' => $count]);
}
/**
* 安装
* @param Request $request
* @return Response
* @throws GuzzleException|BusinessException
*/
public function install(Request $request): Response
{
$name = $request->post('name');
$version = $request->post('version');
$installed_version = $this->getPluginVersion($name);
if (!$name || !$version) {
return $this->json(1, '缺少参数');
}
$user = session('app-plugin-user');
if (!$user) {
return $this->json(-1, '请登录');
}
// 获取下载zip文件url
$data = $this->getDownloadUrl($name, $version);
if ($data['code'] != 0) {
return $this->json($data['code'], $data['msg'], $data['data'] ?? []);
}
// 下载zip文件
$base_path = base_path() . "/plugin/$name";
$zip_file = "$base_path.zip";
$extract_to = base_path() . '/plugin/';
$this->downloadZipFile($data['data']['url'], $zip_file);
$has_zip_archive = class_exists(ZipArchive::class, false);
if (!$has_zip_archive) {
$cmd = $this->getUnzipCmd($zip_file, $extract_to);
if (!$cmd) {
throw new BusinessException('请给php安装zip模块或者给系统安装unzip命令');
}
if (!function_exists('proc_open')) {
throw new BusinessException('请解除proc_open函数的禁用或者给php安装zip模块');
}
}
Util::pauseFileMonitor();
try {
// 解压zip到plugin目录
if ($has_zip_archive) {
$zip = new ZipArchive;
$zip->open($zip_file);
}
$context = null;
$install_class = "\\plugin\\$name\\api\\Install";
if ($installed_version) {
// 执行beforeUpdate
if (class_exists($install_class) && method_exists($install_class, 'beforeUpdate')) {
$context = call_user_func([$install_class, 'beforeUpdate'], $installed_version, $version);
}
}
if (!empty($zip)) {
$zip->extractTo(base_path() . '/plugin/');
unset($zip);
} else {
$this->unzipWithCmd($cmd);
}
unlink($zip_file);
if ($installed_version) {
// 执行update更新
if (class_exists($install_class) && method_exists($install_class, 'update')) {
call_user_func([$install_class, 'update'], $installed_version, $version, $context);
}
} else {
// 执行install安装
if (class_exists($install_class) && method_exists($install_class, 'install')) {
call_user_func([$install_class, 'install'], $version);
}
}
} finally {
Util::resumeFileMonitor();
}
Util::reloadWebman();
return $this->json(0);
}
/**
* 卸载
* @param Request $request
* @return Response
*/
public function uninstall(Request $request): Response
{
$name = $request->post('name');
$version = $request->post('version');
if (!$name || !preg_match('/^[a-zA-Z0-9_]+$/', $name)) {
return $this->json(1, '参数错误');
}
// 获得插件路径
clearstatcache();
$path = get_realpath(base_path() . "/plugin/$name");
if (!$path || !is_dir($path)) {
return $this->json(1, '已经删除');
}
// 执行uninstall卸载
$install_class = "\\plugin\\$name\\api\\Install";
if (class_exists($install_class) && method_exists($install_class, 'uninstall')) {
call_user_func([$install_class, 'uninstall'], $version);
}
// 删除目录
clearstatcache();
if (is_dir($path)) {
$monitor_support_pause = method_exists(Monitor::class, 'pause');
if ($monitor_support_pause) {
Monitor::pause();
}
try {
$this->rmDir($path);
} finally {
if ($monitor_support_pause) {
Monitor::resume();
}
}
}
clearstatcache();
Util::reloadWebman();
return $this->json(0);
}
/**
* 支付
* @param Request $request
* @return string|Response
* @throws GuzzleException
*/
public function pay(Request $request)
{
$app = $request->get('app');
if (!$app) {
return response('app not found');
}
$token = session('app-plugin-token');
if (!$token) {
return 'Please login workerman.net';
}
$client = $this->httpClient();
$response = $client->get("/payment/app/$app/$token");
return (string)$response->getBody();
}
/**
* 登录验证码
* @param Request $request
* @return Response
* @throws GuzzleException
*/
public function captcha(Request $request): Response
{
$client = $this->httpClient();
$response = $client->get('/user/captcha?type=login');
$sid_str = $response->getHeaderLine('Set-Cookie');
if (preg_match('/PHPSID=([a-zA-Z_0-9]+?);/', $sid_str, $match)) {
$sid = $match[1];
session()->set('app-plugin-token', $sid);
}
return response($response->getBody()->getContents())->withHeader('Content-Type', 'image/jpeg');
}
/**
* 登录官网
* @param Request $request
* @return Response|string
* @throws GuzzleException
*/
public function login(Request $request)
{
$client = $this->httpClient();
if ($request->method() === 'GET') {
$response = $client->get("/webman-admin/login");
return (string)$response->getBody();
}
$response = $client->post('/api/user/login', [
'form_params' => [
'email' => $request->post('username'),
'password' => $request->post('password'),
'captcha' => $request->post('captcha')
]
]);
$content = $response->getBody()->getContents();
$data = json_decode($content, true);
if (!$data) {
$msg = "/api/user/login return $content";
echo "msg\r\n";
Log::error($msg);
return $this->json(1, '发生错误');
}
if ($data['code'] != 0) {
return $this->json($data['code'], $data['msg']);
}
session()->set('app-plugin-user', [
'uid' => $data['data']['uid']
]);
return $this->json(0);
}
/**
* 获取zip下载url
* @param $name
* @param $version
* @return mixed
* @throws BusinessException
* @throws GuzzleException
*/
protected function getDownloadUrl($name, $version)
{
$client = $this->httpClient();
$response = $client->get("/app/download/$name?version=$version");
$content = $response->getBody()->getContents();
$data = json_decode($content, true);
if (!$data) {
$msg = "/api/app/download return $content";
Log::error($msg);
throw new BusinessException('访问官方接口失败 ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase());
}
if ($data['code'] && $data['code'] != -1 && $data['code'] != -2) {
throw new BusinessException($data['msg']);
}
if ($data['code'] == 0 && !isset($data['data']['url'])) {
throw new BusinessException('官方接口返回数据错误');
}
return $data;
}
/**
* 下载zip
* @param $url
* @param $file
* @return void
* @throws BusinessException
* @throws GuzzleException
*/
protected function downloadZipFile($url, $file)
{
$client = $this->downloadClient();
$response = $client->get($url);
$body = $response->getBody();
$status = $response->getStatusCode();
if ($status == 404) {
throw new BusinessException('安装包不存在');
}
$zip_content = $body->getContents();
if (empty($zip_content)) {
throw new BusinessException('安装包不存在');
}
file_put_contents($file, $zip_content);
}
/**
* 获取系统支持的解压命令
* @param $zip_file
* @param $extract_to
* @return mixed|string|null
*/
protected function getUnzipCmd($zip_file, $extract_to)
{
if ($cmd = $this->findCmd('unzip')) {
$cmd = "$cmd -o -qq $zip_file -d $extract_to";
} else if ($cmd = $this->findCmd('7z')) {
$cmd = "$cmd x -bb0 -y $zip_file -o$extract_to";
} else if ($cmd = $this->findCmd('7zz')) {
$cmd = "$cmd x -bb0 -y $zip_file -o$extract_to";
}
return $cmd;
}
/**
* 使用解压命令解压
* @param $cmd
* @return void
* @throws BusinessException
*/
protected function unzipWithCmd($cmd)
{
$desc = [
0 => ["pipe", "r"],
1 => ["pipe", "w"],
2 => ["pipe", "w"],
];
$handler = proc_open($cmd, $desc, $pipes);
if (!is_resource($handler)) {
throw new BusinessException("解压zip时出错:proc_open调用失败");
}
$err = fread($pipes[2], 1024);
fclose($pipes[2]);
proc_close($handler);
if ($err) {
throw new BusinessException("解压zip时出错:$err");
}
}
/**
* 获取已安装的插件列表
* @return array
*/
protected function getLocalPlugins(): array
{
clearstatcache();
$installed = [];
$plugin_names = array_diff(scandir(base_path() . '/plugin/'), array('.', '..')) ?: [];
foreach ($plugin_names as $plugin_name) {
if (is_dir(base_path() . "/plugin/$plugin_name") && $version = $this->getPluginVersion($plugin_name)) {
$installed[$plugin_name] = $version;
}
}
return $installed;
}
/**
* 获取已安装的插件列表
* @param Request $request
* @return Response
*/
public function getInstalledPlugins(Request $request): Response
{
return $this->json(0, 'ok', $this->getLocalPlugins());
}
/**
* 获取本地插件版本
* @param $name
* @return array|mixed|null
*/
protected function getPluginVersion($name)
{
if (!is_file($file = base_path() . "/plugin/$name/config/app.php")) {
return null;
}
$config = include $file;
return $config['version'] ?? null;
}
/**
* 获取webman/admin版本
* @return string
*/
protected function getAdminVersion(): string
{
return config('plugin.admin.app.version', '');
}
/**
* 删除目录
* @param $src
* @return void
*/
protected function rmDir($src)
{
$dir = opendir($src);
while (false !== ($file = readdir($dir))) {
if (($file != '.') && ($file != '..')) {
$full = $src . '/' . $file;
if (is_dir($full)) {
$this->rmDir($full);
} else {
unlink($full);
}
}
}
closedir($dir);
rmdir($src);
}
/**
* 获取httpclient
* @return Client
*/
protected function httpClient(): Client
{
// 下载zip
$options = [
'base_uri' => config('plugin.admin.app.plugin_market_host'),
'timeout' => 60,
'connect_timeout' => 5,
'verify' => false,
'http_errors' => false,
'headers' => [
'Referer' => \request()->fullUrl(),
'User-Agent' => 'webman-app-plugin',
'Accept' => 'application/json;charset=UTF-8',
]
];
if ($token = session('app-plugin-token')) {
$options['headers']['Cookie'] = "PHPSID=$token;";
}
return new Client($options);
}
/**
* 获取下载httpclient
* @return Client
*/
protected function downloadClient(): Client
{
// 下载zip
$options = [
'timeout' => 59,
'connect_timeout' => 5,
'verify' => false,
'http_errors' => false,
'headers' => [
'Referer' => \request()->fullUrl(),
'User-Agent' => 'webman-app-plugin',
]
];
if ($token = session('app-plugin-token')) {
$options['headers']['Cookie'] = "PHPSID=$token;";
}
return new Client($options);
}
/**
* 查找系统命令
* @param string $name
* @param string|null $default
* @param array $extraDirs
* @return mixed|string|null
*/
protected function findCmd(string $name, ?string $default = null, array $extraDirs = [])
{
if (ini_get('open_basedir')) {
$searchPath = array_merge(explode(PATH_SEPARATOR, ini_get('open_basedir')), $extraDirs);
$dirs = [];
foreach ($searchPath as $path) {
if (@is_dir($path)) {
$dirs[] = $path;
} else {
if (basename($path) == $name && @is_executable($path)) {
return $path;
}
}
}
} else {
$dirs = array_merge(
explode(PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')),
$extraDirs
);
}
$suffixes = [''];
if ('\\' === DIRECTORY_SEPARATOR) {
$pathExt = getenv('PATHEXT');
$suffixes = array_merge($pathExt ? explode(PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com'], $suffixes);
}
foreach ($suffixes as $suffix) {
foreach ($dirs as $dir) {
if (@is_file($file = $dir . DIRECTORY_SEPARATOR . $name . $suffix) && ('\\' === DIRECTORY_SEPARATOR || @is_executable($file))) {
return $file;
}
}
}
return $default;
}
}
@@ -0,0 +1,257 @@
<?php
namespace plugin\admin\app\controller;
use plugin\admin\app\common\Auth;
use plugin\admin\app\common\Tree;
use plugin\admin\app\model\Role;
use plugin\admin\app\model\Rule;
use support\exception\BusinessException;
use support\Request;
use support\Response;
use Throwable;
/**
* 角色管理
*/
class RoleController extends Crud
{
/**
* 不需要鉴权的方法
* @var array
*/
protected $noNeedAuth = ['select'];
/**
* @var Role
*/
protected $model = null;
/**
* 构造函数
*/
public function __construct()
{
$this->model = new Role;
}
/**
* 浏览
* @return Response
* @throws Throwable
*/
public function index(): Response
{
return raw_view('role/index');
}
/**
* 查询
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function select(Request $request): Response
{
$id = $request->get('id');
[$where, $format, $limit, $field, $order] = $this->selectInput($request);
$limit = 100000;
$role_ids = Auth::getScopeRoleIds(true);
if (!$id) {
$where['id'] = ['in', $role_ids];
} elseif (!in_array($id, $role_ids)) {
throw new BusinessException('无权限');
}
$query = $this->doSelect($where, $field, $order);
return $this->doFormat($query, $format, $limit);
}
/**
* 插入
* @param Request $request
* @return Response
* @throws BusinessException
* @throws Throwable
*/
public function insert(Request $request): Response
{
if ($request->method() === 'POST') {
$data = $this->insertInput($request);
$pid = $data['pid'] ?? null;
if (!$pid) {
return $this->json(1, '请选择父级角色组');
}
if (!Auth::isSuperAdmin() && !in_array($pid, Auth::getScopeRoleIds(true))) {
return $this->json(1, '父级角色组超出权限范围');
}
$this->checkRules($pid, $data['rules'] ?? '');
$id = $this->doInsert($data);
return $this->json(0, 'ok', ['id' => $id]);
}
return raw_view('role/insert');
}
/**
* 更新
* @param Request $request
* @return Response
* @throws BusinessException|Throwable
*/
public function update(Request $request): Response
{
if ($request->method() === 'GET') {
return raw_view('role/update');
}
[$id, $data] = $this->updateInput($request);
$is_supper_admin = Auth::isSuperAdmin();
$descendant_role_ids = Auth::getScopeRoleIds();
if (!$is_supper_admin && !in_array($id, $descendant_role_ids)) {
return $this->json(1, '无数据权限');
}
$role = Role::find($id);
if (!$role) {
return $this->json(1, '数据不存在');
}
$is_supper_role = $role->rules === '*';
// 超级角色组不允许更改rules pid 字段
if ($is_supper_role) {
unset($data['rules'], $data['pid']);
}
if (key_exists('pid', $data)) {
$pid = $data['pid'];
if (!$pid) {
return $this->json(1, '请选择父级角色组');
}
if ($pid == $id) {
return $this->json(1, '父级不能是自己');
}
if (!$is_supper_admin && !in_array($pid, Auth::getScopeRoleIds(true))) {
return $this->json(1, '父级超出权限范围');
}
} else {
$pid = $role->pid;
}
if (!$is_supper_role) {
$this->checkRules($pid, $data['rules'] ?? '');
}
$this->doUpdate($id, $data);
// 删除所有子角色组中已经不存在的权限
if (!$is_supper_role) {
$tree = new Tree(Role::select(['id', 'pid'])->get());
$descendant_roles = $tree->getDescendant([$id]);
$descendant_role_ids = array_column($descendant_roles, 'id');
$rule_ids = $data['rules'] ? explode(',', $data['rules']) : [];
foreach ($descendant_role_ids as $role_id) {
$tmp_role = Role::find($role_id);
$tmp_rule_ids = $role->getRuleIds();
$tmp_rule_ids = array_intersect(explode(',',$tmp_role->rules), $tmp_rule_ids);
$tmp_role->rules = implode(',', $tmp_rule_ids);
$tmp_role->save();
}
}
return $this->json(0);
}
/**
* 删除
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function delete(Request $request): Response
{
$ids = $this->deleteInput($request);
if (in_array(1, $ids)) {
return $this->json(1, '无法删除超级管理员角色');
}
if (!Auth::isSuperAdmin() && array_diff($ids, Auth::getScopeRoleIds())) {
return $this->json(1, '无删除权限');
}
$tree = new Tree(Role::get());
$descendants = $tree->getDescendant($ids);
if ($descendants) {
$ids = array_merge($ids, array_column($descendants, 'id'));
}
$this->doDelete($ids);
return $this->json(0);
}
/**
* 获取角色权限
* @param Request $request
* @return Response
* @throws \Exception
*/
public function rules(Request $request): Response
{
$role_id = $request->get('id');
if (empty($role_id)) {
return $this->json(0, 'ok', []);
}
if (!Auth::isSuperAdmin() && !in_array($role_id, Auth::getScopeRoleIds(true))) {
return $this->json(1, '角色组超出权限范围');
}
$rule_id_string = Role::where('id', $role_id)->value('rules');
if ($rule_id_string === '') {
return $this->json(0, 'ok', []);
}
$rules = Rule::get();
$include = [];
if ($rule_id_string !== '*') {
$include = explode(',', $rule_id_string);
}
$items = [];
foreach ($rules as $item) {
$items[] = [
'name' => $item->title ?? $item->name ?? $item->id,
'value' => (string)$item->id,
'id' => $item->id,
'pid' => $item->pid,
];
}
$tree = new Tree($items);
return $this->json(0, 'ok', $tree->getTree($include));
}
/**
* 检查权限字典是否合法
* @param int $role_id
* @param $rule_ids
* @return void
* @throws BusinessException
*/
protected function checkRules(int $role_id, $rule_ids)
{
if ($rule_ids) {
$rule_ids = explode(',', $rule_ids);
if (in_array('*', $rule_ids)) {
throw new BusinessException('非法数据');
}
$rule_exists = Rule::whereIn('id', $rule_ids)->pluck('id');
if (count($rule_exists) != count($rule_ids)) {
throw new BusinessException('权限不存在');
}
$rule_id_string = Role::where('id', $role_id)->value('rules');
if ($rule_id_string === '') {
throw new BusinessException('数据超出权限范围');
}
if ($rule_id_string === '*') {
return;
}
$legal_rule_ids = explode(',', $rule_id_string);
if (array_diff($rule_ids, $legal_rule_ids)) {
throw new BusinessException('数据超出权限范围');
}
}
}
}
@@ -0,0 +1,354 @@
<?php
namespace plugin\admin\app\controller;
use Exception;
use plugin\admin\app\common\Tree;
use plugin\admin\app\common\Util;
use plugin\admin\app\model\Role;
use plugin\admin\app\model\Rule;
use support\exception\BusinessException;
use support\Request;
use support\Response;
use Throwable;
/**
* 权限菜单
*/
class RuleController extends Crud
{
/**
* 不需要权限的方法
*
* @var string[]
*/
protected $noNeedAuth = ['get', 'permission'];
/**
* @var Rule
*/
protected $model = null;
/**
* 构造函数
*/
public function __construct()
{
$this->model = new Rule;
}
/**
* 浏览
* @return Response
* @throws Throwable
*/
public function index(): Response
{
return raw_view('rule/index');
}
/**
* 查询
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function select(Request $request): Response
{
$this->syncRules();
return parent::select($request);
}
/**
* 获取菜单
* @param Request $request
* @return Response
* @throws Exception
*/
function get(Request $request): Response
{
$rules = $this->getRules(admin('roles'));
$types = $request->get('type', '0,1');
$types = is_string($types) ? explode(',', $types) : [0, 1];
$items = Rule::orderBy('weight', 'desc')->get()->toArray();
$formatted_items = [];
foreach ($items as $item) {
$item['pid'] = (int)$item['pid'];
$item['name'] = $item['title'];
$item['value'] = $item['id'];
$item['icon'] = $item['icon'] ? "layui-icon {$item['icon']}" : '';
$formatted_items[] = $item;
}
$tree = new Tree($formatted_items);
$tree_items = $tree->getTree();
// 超级管理员权限为 *
if (!in_array('*', $rules)) {
$this->removeNotContain($tree_items, 'id', $rules);
}
$this->removeNotContain($tree_items, 'type', $types);
$menus = $this->empty_filter(Tree::arrayValues($tree_items));
return $this->json(0, 'ok', $menus);
}
private function empty_filter($menus)
{
return array_map(
function ($menu) {
if (isset($menu['children'])) {
$menu['children'] = $this->empty_filter($menu['children']);
}
return $menu;
},
array_values(array_filter(
$menus,
function ($menu) {
return $menu['type'] != 0 || isset($menu['children']) && count($this->empty_filter($menu['children'])) > 0;
}
))
);
}
/**
* 获取权限
* @param Request $request
* @return Response
* @throws Exception
*/
public function permission(Request $request): Response
{
$rules = $this->getRules(admin('roles'));
// 超级管理员
if (in_array('*', $rules)) {
return $this->json(0, 'ok', ['*']);
}
$keys = Rule::whereIn('id', $rules)->pluck('key');
$permissions = [];
foreach ($keys as $key) {
if (!$key = Util::controllerToUrlPath($key)) {
continue;
}
$code = str_replace('/', '.', trim($key, '/'));
$permissions[] = $code;
}
$permissions = ["app.admin.vehicle.select","app.admin.vehicle.detail"];
return $this->json(0, 'ok', $permissions);
}
/**
* 根据类同步规则到数据库
* @return void
*/
protected function syncRules()
{
$items = $this->model->where('key', 'like', '%\\\\%')->get()->keyBy('key');
$methods_in_db = [];
$methods_in_files = [];
foreach ($items as $item) {
$class = $item->key;
if (strpos($class, '@')) {
$methods_in_db[$class] = $class;
continue;
}
if (class_exists($class)) {
$reflection = new \ReflectionClass($class);
$properties = $reflection->getDefaultProperties();
$no_need_auth = array_merge($properties['noNeedLogin'] ?? [], $properties['noNeedAuth'] ?? []);
$class = $reflection->getName();
$pid = $item->id;
$methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method) {
$method_name = $method->getName();
if (strtolower($method_name) === 'index' || strpos($method_name, '__') === 0 || in_array($method_name, $no_need_auth)) {
continue;
}
$name = "$class@$method_name";
$methods_in_files[$name] = $name;
$title = Util::getCommentFirstLine($method->getDocComment()) ?: $method_name;
$menu = $items[$name] ?? [];
if ($menu) {
if ($menu->title != $title) {
Rule::where('key', $name)->update(['title' => $title]);
}
continue;
}
$menu = new Rule;
$menu->pid = $pid;
$menu->key = $name;
$menu->title = $title;
$menu->type = 2;
$menu->save();
}
}
}
// 从数据库中删除已经不存在的方法
$menu_names_to_del = array_diff($methods_in_db, $methods_in_files);
if ($menu_names_to_del) {
//Rule::whereIn('key', $menu_names_to_del)->delete();
}
}
/**
* 查询前置方法
* @param Request $request
* @return array
* @throws BusinessException
*/
protected function selectInput(Request $request): array
{
[$where, $format, $limit, $field, $order] = parent::selectInput($request);
// 允许通过type=0,1格式传递菜单类型
$types = $request->get('type');
if ($types && is_string($types)) {
$where['type'] = ['in', explode(',', $types)];
}
// 默认weight排序
if (!$field) {
$field = 'weight';
$order = 'desc';
}
return [$where, $format, $limit, $field, $order];
}
/**
* 添加
* @param Request $request
* @return Response
* @throws BusinessException|Throwable
*/
public function insert(Request $request): Response
{
if ($request->method() === 'GET') {
return raw_view('rule/insert');
}
$data = $this->insertInput($request);
if (empty($data['type'])) {
$data['type'] = strpos($data['key'], '\\') ? 1 : 0;
}
$data['key'] = str_replace('\\\\', '\\', $data['key']);
$key = $data['key'] ?? '';
if ($this->model->where('key', $key)->first()) {
return $this->json(1, "菜单标识 $key 已经存在");
}
$data['pid'] = empty($data['pid']) ? 0 : $data['pid'];
$this->doInsert($data);
return $this->json(0);
}
/**
* 更新
* @param Request $request
* @return Response
* @throws BusinessException|Throwable
*/
public function update(Request $request): Response
{
if ($request->method() === 'GET') {
return raw_view('rule/update');
}
[$id, $data] = $this->updateInput($request);
if (!$row = $this->model->find($id)) {
return $this->json(2, '记录不存在');
}
if (isset($data['pid'])) {
$data['pid'] = $data['pid'] ?: 0;
if ($data['pid'] == $row['id']) {
return $this->json(2, '不能将自己设置为上级菜单');
}
}
if (isset($data['key'])) {
$data['key'] = str_replace('\\\\', '\\', $data['key']);
}
$this->doUpdate($id, $data);
return $this->json(0);
}
/**
* 删除
* @param Request $request
* @return Response
*/
public function delete(Request $request): Response
{
$ids = $this->deleteInput($request);
// 子规则一起删除
$delete_ids = $children_ids = $ids;
while($children_ids) {
$children_ids = $this->model->whereIn('pid', $children_ids)->pluck('id')->toArray();
$delete_ids = array_merge($delete_ids, $children_ids);
}
$this->doDelete($delete_ids);
return $this->json(0);
}
/**
* 移除不包含某些数据的数组
* @param $array
* @param $key
* @param $values
* @return void
*/
protected function removeNotContain(&$array, $key, $values)
{
foreach ($array as $k => &$item) {
if (!is_array($item)) {
continue;
}
if (!$this->arrayContain($item, $key, $values)) {
unset($array[$k]);
} else {
if (!isset($item['children'])) {
continue;
}
$this->removeNotContain($item['children'], $key, $values);
}
}
}
/**
* 判断数组是否包含某些数据
* @param $array
* @param $key
* @param $values
* @return bool
*/
protected function arrayContain(&$array, $key, $values): bool
{
if (!is_array($array)) {
return false;
}
if (isset($array[$key]) && in_array($array[$key], $values)) {
return true;
}
if (!isset($array['children'])) {
return false;
}
foreach ($array['children'] as $item) {
if ($this->arrayContain($item, $key, $values)) {
return true;
}
}
return false;
}
/**
* 获取权限规则
* @param $roles
* @return array
*/
protected function getRules($roles): array
{
$rules_strings = $roles ? Role::whereIn('id', $roles)->pluck('rules') : [];
$rules = [];
foreach ($rules_strings as $rule_string) {
if (!$rule_string) {
continue;
}
$rules = array_merge($rules, explode(',', $rule_string));
}
return $rules;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,378 @@
<?php
namespace plugin\admin\app\controller;
use app\service\OssService;
use Exception;
use Intervention\Image\ImageManagerStatic as Image;
use plugin\admin\app\model\Upload;
use Shopwwi\WebmanFilesystem\Facade\Storage;
use support\exception\BusinessException;
use support\Log;
use support\Request;
use support\Response;
use Throwable;
use util\Util;
/**
* 附件管理
*/
class UploadController extends Crud
{
/**
* @var Upload
*/
protected $model = null;
/**
* 只返回当前管理员数据
* @var string
*/
protected $dataLimit = 'personal';
/**
* 构造函数
* @return void
*/
public function __construct()
{
$this->model = new Upload;
}
/**
* 浏览
* @return Response
* @throws Throwable
*/
public function index(): Response
{
return raw_view('upload/index');
}
/**
* 浏览附件
* @return Response
* @throws Throwable
*/
public function attachment(): Response
{
return raw_view('upload/attachment');
}
/**
* 查询附件
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function select(Request $request): Response
{
[$where, $format, $limit, $field, $order] = $this->selectInput($request);
if (!empty($where['ext']) && is_string($where['ext'])) {
$where['ext'] = ['in', explode(',', $where['ext'])];
}
if (!empty($where['name']) && is_string($where['name'])) {
$where['name'] = ['like', "%{$where['name']}%"];
}
$query = $this->doSelect($where, $field, $order);
return $this->doFormat($query, $format, $limit);
}
/**
* 更新附件
* @param Request $request
* @return Response
* @throws BusinessException|Throwable
*/
public function update(Request $request): Response
{
if ($request->method() === 'GET') {
return raw_view('upload/update');
}
return parent::update($request);
}
/**
* 添加附件
* @param Request $request
* @return Response
* @throws Exception|Throwable
*/
public function insert(Request $request): Response
{
if ($request->method() === 'GET') {
return raw_view('upload/insert');
}
$file = current($request->file());
if (!$file || !$file->isValid()) {
return $this->json(1, '未找到文件');
}
$data = $this->base($request, '/upload/files/' . date('Ymd'));
$upload = new Upload;
$upload->admin_id = admin_id();
$upload->name = $data['name'];
[
$upload->url,
$upload->name,
$_,
$upload->file_size,
$upload->mime_type,
$upload->image_width,
$upload->image_height,
$upload->ext
] = array_values($data);
$upload->category = $request->post('category');
$upload->save();
return $this->json(0, '上传成功', [
'url' => $data['url'],
'name' => $data['name'],
'size' => $data['size'],
]);
}
/**
* 上传文件
* @param Request $request
* @return Response
* @throws Exception
*/
public function file(Request $request): Response
{
$file = current($request->file());
if (!$file || !$file->isValid()) {
return $this->json(1, '未找到文件');
}
$img_exts = [
'jpg',
'jpeg',
'png',
'gif'
];
if (in_array($file->getUploadExtension(), $img_exts)) {
return $this->image($request);
}
$data = $this->base($request, '/upload/files/' . date('Ymd'));
return $this->json(0, '上传成功', [
'url' => $data['url'],
'name' => $data['name'],
'size' => $data['size'],
]);
}
/**
* 上传图片
* @param Request $request
* @return Response
* @throws Exception
*/
public function image(Request $request): Response
{
$data = $this->base($request, '/upload/img/' . date('Ymd'));
$realpath = $data['realpath'];
try {
$img = Image::make($realpath);
$max_height = 1170;
$max_width = 1170;
$width = $img->width();
$height = $img->height();
$ratio = 1;
if ($height > $max_height || $width > $max_width) {
$ratio = $width > $height ? $max_width / $width : $max_height / $height;
}
$img->resize($width * $ratio, $height * $ratio)->save($realpath);
} catch (Exception $e) {
unlink($realpath);
return json([
'code' => 500,
'msg' => '处理图片发生错误'
]);
}
return json([
'code' => 0,
'msg' => '上传成功',
'data' => [
'url' => $data['url'],
'name' => $data['name'],
'size' => $data['size'],
]
]);
}
/**
* 上传头像
* @param Request $request
* @return Response
* @throws Exception
*/
public function avatar(Request $request): Response
{
$file = current($request->file());
if ($file && $file->isValid()) {
$ext = strtolower($file->getUploadExtension());
if (!in_array($ext, ['jpg', 'jpeg', 'gif', 'png'])) {
return json(['code' => 2, 'msg' => '仅支持 jpg jpeg gif png格式']);
}
$image = Image::make($file);
$width = $image->width();
$height = $image->height();
$size = min($width, $height);
$relative_path = 'upload/avatar/' . date('Ym');
$real_path = base_path() . "/plugin/admin/public/$relative_path";
if (!is_dir($real_path)) {
mkdir($real_path, 0777, true);
}
$name = bin2hex(pack('Nn', time(), random_int(1, 65535)));
$ext = $file->getUploadExtension();
$image->crop($size, $size)->resize(300, 300);
$path = base_path() . "/plugin/admin/public/$relative_path/$name.lg.$ext";
$image->save($path);
$image->resize(120, 120);
$path = base_path() . "/plugin/admin/public/$relative_path/$name.md.$ext";
$image->save($path);
$image->resize(60, 60);
$path = base_path() . "/plugin/admin/public/$relative_path/$name.$ext";
$image->save($path);
$image->resize(30, 30);
$path = base_path() . "/plugin/admin/public/$relative_path/$name.sm.$ext";
$image->save($path);
return json([
'code' => 0,
'msg' => '上传成功',
'data' => [
'url' => "/app/admin/$relative_path/$name.md.$ext"
]
]);
}
return json(['code' => 1, 'msg' => 'file not found']);
}
/**
* 删除附件
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function delete(Request $request): Response
{
$ids = $this->deleteInput($request);
$primary_key = $this->model->getKeyName();
$files = $this->model->whereIn($primary_key, $ids)->get()->toArray();
$file_list = array_map(function ($item) {
$path = $item['url'];
if (preg_match("#^/app/admin#", $path)) {
$admin_public_path = config('plugin.admin.app.public_path') ?: base_path() . "/plugin/admin/public";
return $admin_public_path . str_replace("/app/admin", "", $item['url']);
}
return null;
}, $files);
$file_list = array_filter($file_list, function ($item) {
return !empty($item);
});
$result = parent::delete($request);
if (($res = json_decode($result->rawBody())) && $res->code === 0) {
foreach ($file_list as $file) {
@unlink($file);
}
}
return $result;
}
/**
* 获取上传数据
* @param Request $request
* @param $relative_dir
* @return array
* @throws BusinessException|\Random\RandomException
*/
protected function base(Request $request, $relative_dir): array
{
$relative_dir = ltrim($relative_dir, '\\/');
$file = current($request->file());
if (!$file || !$file->isValid()) {
throw new BusinessException('未找到上传文件', 400);
}
$admin_public_path = rtrim(config('plugin.admin.app.public_path', ''), '\\/');
$base_dir = $admin_public_path ? $admin_public_path . DIRECTORY_SEPARATOR : base_path() . '/plugin/admin/public/';
$full_dir = $base_dir . $relative_dir;
if (!is_dir($full_dir)) {
mkdir($full_dir, 0777, true);
}
$ext = $file->getUploadExtension() ?: null;
$mime_type = $file->getUploadMimeType();
$file_name = $file->getUploadName();
$file_size = $file->getSize();
if (!$ext && $file_name === 'blob') {
[$___image, $ext] = explode('/', $mime_type);
unset($___image);
}
$ext = strtolower($ext);
$ext_forbidden_map = ['php', 'php3', 'php5', 'css', 'js', 'html', 'htm', 'asp', 'jsp'];
if (in_array($ext, $ext_forbidden_map)) {
throw new BusinessException('不支持该格式的文件上传', 400);
}
$relative_path = $relative_dir . '/' . bin2hex(pack('Nn', time(), random_int(1, 65535))) . ".$ext";
$full_path = $base_dir . $relative_path;
$file->move($full_path);
$image_with = $image_height = 0;
if ($img_info = getimagesize($full_path)) {
[$image_with, $image_height] = $img_info;
$mime_type = $img_info['mime'];
}
return [
'url' => "/app/admin/$relative_path",
'name' => $file_name,
'realpath' => $full_path,
'size' => $file_size,
'mime_type' => $mime_type,
'image_with' => $image_with,
'image_height' => $image_height,
'ext' => $ext,
];
}
// 上传单文件图片
public function uploadImage(Request $request)
{
try {
// $file = $request->file('file');
$file = current($request->file());
if (!$file || !$file->isValid()) {
return Util::fail($file,'上传失败2');
}
$result = Storage::adapter('oss')
->path('lingke_crm/'.date('Ymd'))
->size(1024*1024*5)
->extYes(['image/jpeg','image/png'])
->extNo(['image/gif'])
->upload($file);
$ossservice = new OssService();
$data = [
'path' => $result->file_name,
'original_name' => $result->origin_name,
'image_url' => $ossservice->generatePrivateUrl($result->file_name)
];
return Util::success($data,'上传成功');
}catch (\Throwable $e){
Log::error('文件上传失败: ' . $e->getMessage());
return Util::fail([],'file:'.$e->getFile().'line:'.$e->getLine().'message:'.$e->getMessage());
}
}
}
@@ -0,0 +1,69 @@
<?php
namespace plugin\admin\app\controller;
use plugin\admin\app\model\User;
use support\exception\BusinessException;
use support\Request;
use support\Response;
use Throwable;
/**
* 用户管理
*/
class UserController extends Crud
{
/**
* @var User
*/
protected $model = null;
/**
* 构造函数
* @return void
*/
public function __construct()
{
$this->model = new User;
}
/**
* 浏览
* @return Response
* @throws Throwable
*/
public function index(): Response
{
return raw_view('user/index');
}
/**
* 插入
* @param Request $request
* @return Response
* @throws BusinessException|Throwable
*/
public function insert(Request $request): Response
{
if ($request->method() === 'POST') {
return parent::insert($request);
}
return raw_view('user/insert');
}
/**
* 更新
* @param Request $request
* @return Response
* @throws BusinessException|Throwable
*/
public function update(Request $request): Response
{
if ($request->method() === 'POST') {
return parent::update($request);
}
return raw_view('user/update');
}
}
@@ -0,0 +1,132 @@
<?php
namespace plugin\admin\app\controller;
use plugin\admin\app\model\Location;
use support\Request;
use support\Response;
use plugin\admin\app\model\Vehicle;
use plugin\admin\app\controller\Crud;
use support\exception\BusinessException;
/**
* 车辆信息
*/
class VehicleController extends Crud
{
/**
* 不需要登录的方法
* @var string[]
*/
protected $noNeedLogin = ['detail'];
/**
* 不需要鉴权的方法
* @var string[]
*/
protected $noNeedAuth = ['detail'];
protected $status = ['未知','待发车','在途(发往fbo港口)','抵达发货港口','在途(发往目的地仓库)','抵达仓库','售出'];
/**
* @var Vehicle
*/
protected $model = null;
/**
* 构造函数
* @return void
*/
public function __construct()
{
$this->model = new Vehicle;
}
/**
* 浏览
* @return Response
*/
public function index(): Response
{
return view('vehicle/index');
}
/**
* 插入
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function insert(Request $request): Response
{
if ($request->method() === 'POST') {
return parent::insert($request);
}
return view('vehicle/insert');
}
/**
* 更新
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function update(Request $request): Response
{
if ($request->method() === 'POST') {
return parent::update($request);
}
return view('vehicle/update');
}
/**
* 详情
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function detail(Request $request): Response
{
return view('vehicle/detail');
}
/**
* 查询
* @param Request $request
* @return Response
* @throws BusinessException
*/
/* public function list(Request $request)
{
[$where, $format, $limit, $field, $order] = $this->selectInput($request);
return json($where);
$query = $this->doSelect($where, $field, $order);
$data = $this->doFormat($query, $format, $limit);
// foreach ($data['data'] as &$value) {
// $value['locations'] = Location::find($value['locations_id'])->name;
// }
// $data = json_decode(json_encode($data), true);
return $data;
}*/
/**
* 查询后置处理,添加位置信息
* @param mixed $items
* @return mixed
*/
protected function afterQuery($items)
{
foreach ($items as &$item) {
$location = Location::find($item->locations_id);
$item->status = $this->status[$item->status]??$this->status[0];
$item->location_name = $location ? $location->name : '';
$final_location = Location::find($item->final_locations_id);
$item->final_location_name = $final_location ? $final_location->name : '';
$fbo_location = Location::find($item->fbo_locations_id);
$item->fbo_location_name = $fbo_location ? $fbo_location->name : '';
}
return $items;
}
}