This commit is contained in:
你的名字
2025-10-15 14:53:54 +08:00
commit ac0f12b21a
864 changed files with 200931 additions and 0 deletions

50
.example.env Normal file
View File

@ -0,0 +1,50 @@
APP_DEBUG=true
# 后台系统日志开关
APP_ADMIN_SYSTEM_LOG=true
DEFAULT_TIMEZONE=Asia/Shanghai
DB_TYPE=mysql
DB_HOST=127.0.0.1
DB_NAME=easyadmin8
DB_USER=root
DB_PASS=root
DB_PORT=3306
DB_CHARSET=utf8mb4
DB_PREFIX=ea8_
# 限流器开关 若启动需要配置 Redis 服务
RATE_LIMITING_STATUS=false
# Redis配置
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_PREFIX=
REDIS_DATABASE=0
# 后台配置项组
[EASYADMIN]
# 后台地址后缀名称
ADMIN=admin
# 后台登录验证码开关
CAPTCHA=false
# 是否为演示环境
IS_DEMO=false
# CDN配置项组
CDN=
EXAMPLE=true
# 是否开启CSRF过滤
IS_CSRF=false
# 静态文件路径前缀
STATIC_PATH=/static
# OSS静态文件路径前缀
OSS_STATIC_PREFIX=static_easyadmin

3
.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
*.js linguist-language=PHP
*.css linguist-language=PHP
*.html linguist-language=PHP

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
*.log
.env
composer.phar
composer.lock
.DS_Store
Thumbs.db
/.idea
/.vscode
/vendor
/.settings
/.buildpath
/.project

42
.travis.yml Normal file
View File

@ -0,0 +1,42 @@
sudo: false
language: php
branches:
only:
- stable
cache:
directories:
- $HOME/.composer/cache
before_install:
- composer self-update
install:
- composer install --no-dev --no-interaction --ignore-platform-reqs
- zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Core.zip .
- composer require --update-no-dev --no-interaction "topthink/think-image:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-migration:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-captcha:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-mongo:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-worker:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-helper:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-queue:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-angular:^1.0"
- composer require --dev --update-no-dev --no-interaction "topthink/think-testing:^1.0"
- zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Full.zip .
script:
- php think unit
deploy:
provider: releases
api_key:
secure: TSF6bnl2JYN72UQOORAJYL+CqIryP2gHVKt6grfveQ7d9rleAEoxlq6PWxbvTI4jZ5nrPpUcBUpWIJHNgVcs+bzLFtyh5THaLqm39uCgBbrW7M8rI26L8sBh/6nsdtGgdeQrO/cLu31QoTzbwuz1WfAVoCdCkOSZeXyT/CclH99qV6RYyQYqaD2wpRjrhA5O4fSsEkiPVuk0GaOogFlrQHx+C+lHnf6pa1KxEoN1A0UxxVfGX6K4y5g4WQDO5zT4bLeubkWOXK0G51XSvACDOZVIyLdjApaOFTwamPcD3S1tfvuxRWWvsCD5ljFvb2kSmx5BIBNwN80MzuBmrGIC27XLGOxyMerwKxB6DskNUO9PflKHDPI61DRq0FTy1fv70SFMSiAtUv9aJRT41NQh9iJJ0vC8dl+xcxrWIjU1GG6+l/ZcRqVx9V1VuGQsLKndGhja7SQ+X1slHl76fRq223sMOql7MFCd0vvvxVQ2V39CcFKao/LB1aPH3VhODDEyxwx6aXoTznvC/QPepgWsHOWQzKj9ftsgDbsNiyFlXL4cu8DWUty6rQy8zT2b4O8b1xjcwSUCsy+auEjBamzQkMJFNlZAIUrukL/NbUhQU37TAbwsFyz7X0E/u/VMle/nBCNAzgkMwAUjiHM6FqrKKBRWFbPrSIixjfjkCnrMEPw=
file:
- ThinkPHP_Core.zip
- ThinkPHP_Full.zip
skip_cleanup: true
on:
tags: true

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 EasyAdmin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

144
README.md Normal file
View File

@ -0,0 +1,144 @@
<div align="center" dir="auto">
<img alt="log" src="public/static/common/images/logo-8.png" />
<p>
<img src="https://img.shields.io/badge/php-%3E=8.1.0-brightgreen.svg?style=for-the-badge&logo=php&colorB=ff69b4" alt="php">
<img src="https://img.shields.io/badge/mysql-%3E=5.7-brightgreen.svg?style=for-the-badge&logo=mysql&colorB=blue" alt="MySQL">
<img src="https://img.shields.io/badge/thinkphp-%3E=8.0.0-brightgreen.svg?style=for-the-badge&logo=thinkphp" alt="ThinkPHP">
<img src="https://img.shields.io/badge/layui-%3E=2.9.0-brightgreen.svg?style=for-the-badge&logo=layui&colorB=orange" alt="layui">
<img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge&logo=license&colorB=purple" alt="License">
</p>
</div>
## `EasyAdmin8`所有版本 (当前项目为`ThinkPHP`版本)
| | Github | Gitee |
|----------|:----------------------------------------------------------------------:|:---------------------------------------------------------------------:|
| ThinkPHP | [EasyAdmin8](https://github.com/EasyAdmin8/EasyAdmin8) | [EasyAdmin8](https://gitee.com/EasyAdmin8/EasyAdmin8) |
| Laravel | [EasyAdmin8-Laravel](https://github.com/EasyAdmin8/EasyAdmin8-Laravel) | [EasyAdmin8-Laravel](https://gitee.com/EasyAdmin8/EasyAdmin8-Laravel) |
| webman | [EasyAdmin8-webman](https://github.com/EasyAdmin8/EasyAdmin8-webman) | [EasyAdmin8-webman](https://gitee.com/EasyAdmin8/EasyAdmin8-webman) |
## 项目介绍
> `EasyAdmin8` 在 [`EasyAdmin`](https://gitee.com/zhongshaofa/easyadmin) 的基础上更新 ThinkPHP 框架到 8.1+ PHP 最低版本要求不低于 8.1
>
> 2025年起 `PHP` 版本要求提升到 `8.1+`, 如果需要 `8.0` 到分支 `v8.0` 中下载
>
> ThinkPHP v8.1+ 和 Layui v2.9.x 的快速开发的后台管理系统。
>
> 项目地址:[http://easyadmin8.top](http://easyadmin8.top)
>
> 演示地址:[http://thinkphp.easyadmin8.top/admin](http://thinkphp.easyadmin8.top/admin)
>
> 如果您之前已经用过 `FastAdmin` 或者 `EasyAdmin` , 那么入手 `EasyAdmin8` 将会更加轻松
>
>【如果不能访问,可以自行本地搭建预览或参考下方界面预览图】
## 大版本更新记录:
[更新记录](log.md)
## 安装教程
> EasyAdmin8 使用 Composer 来管理项目依赖。因此,在使用 EasyAdmin8 之前,请确保你的机器已经安装了 Composer。
### 通过一键安装命令
```
if [ -f /usr/bin/curl ];then curl -sSO https://easyadmin8.top/auto-install-EasyAdmin8.sh;else wget -O auto-install-EasyAdmin8.sh https://easyadmin8.top/auto-install-EasyAdmin8.sh;fi;bash auto-install-EasyAdmin8.sh
```
### 通过`git`下载安装包,`composer`安装依赖包
```
1.下载安装包
git clone https://github.com/EasyAdmin8/EasyAdmin8
或者
git clone https://gitee.com/EasyAdmin8/EasyAdmin8
2.安装依赖包(确保 PHP 版本 >= 8.1
在根目录下 composer install ,如果有报错信息可以使用命令 composer install --ignore-platform-reqs
3. 拷贝 .example.env 文件重命名为 .env ,命令 cp .example.env .env ,修改数据库账号密码参数
4.配置伪静态(以 Nginx 为例)
location / {
if ( !-e $request_filename){
rewrite ^/(.*)$ /index.php?s=$1 last;
break;
}
}
```
## CURD命令大全
> 参考 [CURD命令大全](https://edocs.easyadmin8.top/curd/command.html)
## 常见问题
> 参考 [常见问题](https://easyadmin8.top/guide/question.html)
## 界面预览
![EasyAdmin8-01](public/static/common/images/easyadmin8-01.png)
![EasyAdmin8-02](public/static/common/images/easyadmin8-02.png)
![EasyAdmin8-03](public/static/common/images/easyadmin8-03.png)
## 交流群
<center>
![EasyAdmin8-ThinkPHP 交流群](public/static/common/images/EasyAdmin8-ThinkPHP.png)
</center>
## 相关文档
* [ThinkPHP 8.1](https://doc.thinkphp.cn)
* [EasyAdmin](http://easyadmin.99php.cn/docs)
* [Layui 2.9.x](https://layui.dev/docs)
* [Layuimini](https://github.com/zhongshaofa/layuimini)
* [Annotations](https://github.com/doctrine/annotations)
* [Jquery](https://github.com/jquery/jquery)
* [RequireJs](https://github.com/requirejs/requirejs)
* [CKEditor](https://github.com/ckeditor/ckeditor4)
* [Echarts](https://github.com/apache/incubator-echarts)
* [UEditorPlus](https://github.com/modstart-lib/ueditor-plus)
* [wangEditor](https://github.com/wangeditor-team/wangEditor)
## 免责声明
> 所有协议遵循 [`EasyAdmin`](https://gitee.com/zhongshaofa/easyadmin)
>
> 任何用户在使用 `EasyAdmin8` 后台框架前,请您仔细阅读并透彻理解本声明。您可以选择不使用`EasyAdmin8`后台框架,若您一旦使用`EasyAdmin8`后台框架,您的使用行为即被视为对本声明全部内容的认可和接受。
* `EasyAdmin8`后台框架是一款开源免费的后台快速开发框架 ,主要用于更便捷地开发后台管理;其尊重并保护所有用户的个人隐私权,不窃取任何用户计算机中的信息。更不具备用户数据存储等网络传输功能。
* 您承诺秉着合法、合理的原则使用`EasyAdmin8`后台框架,不利用`EasyAdmin8`后台框架进行任何违法、侵害他人合法利益等恶意的行为,亦不将`EasyAdmin8`后台框架运用于任何违反我国法律法规的 Web 平台。
* 任何单位或个人因下载使用`EasyAdmin8`后台框架而产生的任何意外、疏忽、合约毁坏、诽谤、版权或知识产权侵犯及其造成的损失 (包括但不限于直接、间接、附带或衍生的损失等),本开源项目不承担任何法律责任。
* 用户明确并同意本声明条款列举的全部内容,对使用`EasyAdmin8`后台框架可能存在的风险和相关后果将完全由用户自行承担,本开源项目不承担任何法律责任。
* 任何单位或个人在阅读本免责声明后应在《MIT 开源许可证》所允许的范围内进行合法的发布、传播和使用`EasyAdmin8`后台框架等行为,若违反本免责声明条款或违反法律法规所造成的法律责任(包括但不限于民事赔偿和刑事责任),由违约者自行承担。
* 如果本声明的任何部分被认为无效或不可执行,其余部分仍具有完全效力。不可执行的部分声明,并不构成我们放弃执行该声明的权利。
* 本开源项目有权随时对本声明条款及附件内容进行单方面的变更,并以消息推送、网页公告等方式予以公布,公布后立即自动生效,无需另行单独通知;若您在本声明内容公告变更后继续使用的,表示您已充分阅读、理解并接受修改后的声明内容。

1
app/.htaccess Normal file
View File

@ -0,0 +1 @@
deny from all

22
app/AppService.php Normal file
View File

@ -0,0 +1,22 @@
<?php
declare (strict_types = 1);
namespace app;
use think\Service;
/**
* 应用服务类
*/
class AppService extends Service
{
public function register()
{
// 服务注册
}
public function boot()
{
// 服务启动
}
}

94
app/BaseController.php Normal file
View File

@ -0,0 +1,94 @@
<?php
declare (strict_types = 1);
namespace app;
use think\App;
use think\exception\ValidateException;
use think\Validate;
/**
* 控制器基础类
*/
abstract class BaseController
{
/**
* Request实例
* @var \think\Request
*/
protected $request;
/**
* 应用实例
* @var \think\App
*/
protected $app;
/**
* 是否批量验证
* @var bool
*/
protected $batchValidate = false;
/**
* 控制器中间件
* @var array
*/
protected $middleware = [];
/**
* 构造方法
* @access public
* @param App $app 应用对象
*/
public function __construct(App $app)
{
$this->app = $app;
$this->request = $this->app->request;
// 控制器初始化
$this->initialize();
}
// 初始化
protected function initialize()
{}
/**
* 验证数据
* @access protected
* @param array $data 数据
* @param string|array $validate 验证器名或者验证规则数组
* @param array $message 提示信息
* @param bool $batch 是否批量验证
* @return array|string|true
* @throws ValidateException
*/
protected function validate(array $data, string|array $validate, array $message = [], bool $batch = false)
{
if (is_array($validate)) {
$v = new Validate();
$v->rule($validate);
} else {
if (strpos($validate, '.')) {
// 支持场景
[$validate, $scene] = explode('.', $validate);
}
$class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
$v = new $class();
if (!empty($scene)) {
$v->scene($scene);
}
}
$v->message($message);
// 是否批量验证
if ($batch || $this->batchValidate) {
$v->batch(true);
}
return $v->failException(true)->check($data);
}
}

58
app/ExceptionHandle.php Normal file
View File

@ -0,0 +1,58 @@
<?php
namespace app;
use think\db\exception\DataNotFoundException;
use think\db\exception\ModelNotFoundException;
use think\exception\Handle;
use think\exception\HttpException;
use think\exception\HttpResponseException;
use think\exception\ValidateException;
use think\Response;
use Throwable;
/**
* 应用异常处理类
*/
class ExceptionHandle extends Handle
{
/**
* 不需要记录信息(日志)的异常类列表
* @var array
*/
protected $ignoreReport = [
HttpException::class,
HttpResponseException::class,
ModelNotFoundException::class,
DataNotFoundException::class,
ValidateException::class,
];
/**
* 记录异常信息(包括日志或者其它方式记录)
*
* @access public
* @param Throwable $exception
* @return void
*/
public function report(Throwable $exception): void
{
// 使用内置的方式记录异常日志
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @access public
* @param \think\Request $request
* @param Throwable $e
* @return Response
*/
public function render($request, Throwable $e): Response
{
// 添加自定义异常处理机制
// 其他错误交给系统处理
return parent::render($request, $e);
}
}

85
app/LangService.php Normal file
View File

@ -0,0 +1,85 @@
<?php
namespace app;
use enums\LangEnum;
/**
*
*/
class LangService
{
/**
* @return string[]
*/
public static function getAllowLang(): array
{
return array_map(function ($item) {
return $item->value;
}, array_filter(LangEnum::cases(), function ($item) {
return $item->is_open();
}));
}
/**
* @return array
*/
public static function getLangCases(): array
{
return array_map(function ($item) {
return [
'value' => $item->value,
'lang' => $item->lang(),
'label' => $item->label()
];
}, array_filter(LangEnum::cases(), function ($item) {
return $item->is_open();
}));
}
/**
* @return array
*/
public static function getLabelCases(): array
{
return array_map(function ($item) {
return [
'value' => $item->value,
'label' => $item->label()
];
}, array_filter(LangEnum::cases(), function ($item) {
return $item->is_open();
}));
}
/**
* @return array
*/
public static function getLangExtend(): array
{
$extend = [];
foreach (array_filter(LangEnum::cases(), function ($item) {
return $item->is_open();
}) as $item) {
$extend[$item->value] = $item->extend();
}
return $extend;
}
/**
* @return array
*/
public static function getAcceptLang(): array
{
$extend = [];
foreach (array_filter(LangEnum::cases(), function ($item) {
return $item->is_open();
}) as $item) {
foreach ($item->getLangList() as $langListitem) {
$extend[$langListitem] = $item->value;
}
}
return $extend;
}
}

8
app/Request.php Normal file
View File

@ -0,0 +1,8 @@
<?php
namespace app;
// 应用请求对象类
class Request extends \think\Request
{
}

View File

@ -0,0 +1,36 @@
<?php
return [
// 后台路径地址 默认 admin
'alias_name' => env('EASYADMIN.ADMIN'),
// 不需要验证权限的控制器
'no_auth_controller' => [
'ajax',
'login',
'index',
],
// 不需要验证权限的节点
'no_auth_node' => [
'login/index',
'login/out',
],
//上传类型
'upload_types' => [
'local' => '本地存储',
'oss' => '阿里云oss',
'cos' => '腾讯云cos',
'qnoss' => '七牛云'
],
// 默认编辑器
'editor_types' => [
'ueditor' => '百度编辑器(不建议使用)',
'ckeditor' => 'CK编辑器',
'wangEditor' => 'wangEditor(推荐使用)',
'EasyMDE' => 'EasyMDE(markdown)',
],
];

View File

@ -0,0 +1,31 @@
<?php
use app\admin\middleware\CheckInstall;
use app\admin\middleware\CheckLogin;
use app\admin\middleware\CheckAuth;
use app\admin\middleware\SystemLog;
use app\admin\middleware\RateLimiting;
// 你可以在这里继续写你需要的路由
// +----------------------------------------------------------------------
// | 这里只是路由的中间件
// | 至于为什么要把中间件配置写在这里呢??? Why???
// | 因为 ThinkPHP官方最新版本 已经不支持在中间件获取 controller 和 action 了
// +----------------------------------------------------------------------
return [
'middleware' => [
// 限流中间件
RateLimiting::class,
// 判断是否已经安装后台系统
// CheckInstall::class,
// 检测是否登录
CheckLogin::class,
// 操作日志
SystemLog::class,
// 验证节点权限
CheckAuth::class,
],
];

View File

@ -0,0 +1,247 @@
<?php
namespace app\admin\controller;
use app\admin\model\SystemUploadfile;
use app\admin\service\UploadService;
use app\common\controller\AdminController;
use app\common\service\MenuService;
use app\Request;
use Kaadon\Helper\GdImageHelper;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use think\db\Query;
use think\facade\Cache;
use think\file\UploadedFile;
use think\response\Json;
class Ajax extends AdminController
{
/**
* 初始化后台接口地址
* @return Json
* @throws DataNotFoundException
* @throws DbException
* @throws ModelNotFoundException
*/
public function initAdmin(): Json
{
$cacheData = Cache::get('initAdmin_' . $this->adminUid);
if (!empty($cacheData)) {
return json($cacheData);
}
$menuService = new MenuService($this->adminUid);
$data = [
'logoInfo' => [
'title' => sysConfig('site', 'logo_title'),
'image' => sysConfig('site', 'logo_image'),
'href' => __url('index/index'),
],
'homeInfo' => $menuService->getHomeInfo(),
'menuInfo' => $menuService->getMenuTree(),
];
Cache::tag('initAdmin')->set('initAdmin_' . $this->adminUid, $data);
return json($data);
}
/**
* 清理缓存接口
*/
public function clearCache(): void
{
Cache::clear();
$this->success('清理缓存成功');
}
/**
* 上传文件
* @param Request $request
* @return Json|null
* @throws \Kaadon\Helper\HelperException
*/
public function upload(Request $request): Json|null
{
$this->isDemo && $this->error('演示环境下不允许修改');
$this->checkPostRequest();
$type = $request->param('type', '');
$file = $request->file($type == 'editor' ? 'upload' : 'file');
if (config('filesystem.image.to_webp', true) && GdImageHelper::isSupportSuffix($file->extension())) {
(new GdImageHelper($file->getRealPath(), $file->extension()))->convertTo($file->getRealPath(),'webp');
$file = new UploadedFile($file->getRealPath(), date("YmdHis") . "_" . md5($file->getOriginalName()) . '.webp');
$file->setExtension('webp');
}
$data = [
'upload_type' => $request->post('upload_type'),
'file' => $file,
];
$uploadConfig = sysConfig('upload');
empty($data['upload_type']) && $data['upload_type'] = $uploadConfig['upload_type'];
$rule = [
'upload_type|指定上传类型有误' => "in:{$uploadConfig['upload_allow_type']}",
'file|文件' => "require|file|fileExt:{$uploadConfig['upload_allow_ext']},webp|fileSize:{$uploadConfig['upload_allow_size']}",
];
$this->validate($data, $rule);
$upload_type = $uploadConfig['upload_type'];
try {
$upload = UploadService::instance()->setConfig($uploadConfig)->$upload_type($data['file'], $type);
}catch (\Exception $e) {
$this->error($e->getMessage());
}
$code = $upload['code'] ?? 0;
if ($code == 0) {
$this->error($upload['data'] ?? '');
}else {
if ($type == 'editor') {
return json(
[
'error' => ['message' => '上传成功', 'number' => 201,],
'fileName' => '',
'uploaded' => 1,
'url' => $upload['data']['url'] ?? '',
]
);
}else {
$this->success('上传成功', $upload['data'] ?? '');
}
}
}
/**
* 获取上传文件列表
* @param Request $request
* @return Json
* @throws DataNotFoundException
* @throws DbException
* @throws ModelNotFoundException
*/
public function getUploadFiles(Request $request): Json
{
$get = $request->get();
$page = !empty($get['page']) ? $get['page'] : 1;
$limit = !empty($get['limit']) ? $get['limit'] : 10;
$title = !empty($get['title']) ? $get['title'] : null;
$count = SystemUploadfile::where(function(Query $query) use ($title) {
!empty($title) && $query->where('original_name', 'like', "%{$title}%");
})
->count();
$list = SystemUploadfile::where(function(Query $query) use ($title) {
!empty($title) && $query->where('original_name', 'like', "%{$title}%");
})
->page($page, $limit)
->order($this->sort)
->select()->toArray();
$data = [
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $list,
];
return json($data);
}
/**
* 百度编辑器上传
* @param Request $request
* @return Json
* @throws DataNotFoundException
* @throws DbException
* @throws ModelNotFoundException
* @throws \Kaadon\Helper\HelperException
*/
public function uploadUEditor(Request $request): Json
{
$uploadConfig = sysConfig('upload');
$upload_allow_size = $uploadConfig['upload_allow_size'];
$_upload_allow_ext = explode(',', $uploadConfig['upload_allow_ext']);
$upload_allow_ext = [];
array_map(function($value) use (&$upload_allow_ext) {
$upload_allow_ext[] = '.' . $value;
}, $_upload_allow_ext);
$config = [
// 上传图片配置项
"imageActionName" => "image",
"imageFieldName" => "file",
"imageMaxSize" => $upload_allow_size,
"imageAllowFiles" => $upload_allow_ext,
"imageCompressEnable" => true,
"imageCompressBorder" => 5000,
"imageInsertAlign" => "none",
"imageUrlPrefix" => "",
// 列出图片
"imageManagerActionName" => "listImage",
"imageManagerListSize" => 20,
"imageManagerUrlPrefix" => "",
"imageManagerInsertAlign" => "none",
"imageManagerAllowFiles" => $upload_allow_ext,
// 上传 video
"videoActionName" => "video",
"videoFieldName" => "file",
"videoUrlPrefix" => "",
"videoMaxSize" => $upload_allow_size,
"videoAllowFiles" => $upload_allow_ext,
// 上传 附件
"fileActionName" => "attachment",
"fileFieldName" => "file",
"fileMaxSize" => $upload_allow_size,
"fileAllowFiles" => $upload_allow_ext,
];
$action = $request->param('action/s', '');
$file = $request->file('file');
$upload_type = $uploadConfig['upload_type'];
switch ($action) {
case 'image':
if (config('filesystem.image.to_webp', true) && GdImageHelper::isSupportSuffix($file->extension())) {
(new GdImageHelper($file->getRealPath(), $file->extension()))->convertTo($file->getRealPath(),'webp');
$file = new UploadedFile($file->getRealPath(), date("YmdHis") . "_" . md5($file->getOriginalName()) . '.webp');
$file->setExtension('webp');
}
case 'attachment':
case 'video':
if ($this->isDemo) return json(['state' => '演示环境下不允许修改']);
try {
$upload = UploadService::instance()->setConfig($uploadConfig)->$upload_type($file);
$code = $upload['code'] ?? 0;
if ($code == 0) {
return json(['state' => $upload['data'] ?? '上传错误信息']);
}else {
return json(['state' => 'SUCCESS', 'url' => $upload['data']['url'] ?? '']);
}
}catch (\Exception $e) {
$this->error($e->getMessage());
}
break;
case 'listImage':
$list = (new SystemUploadfile())->order($this->sort)->limit(100)->field('url')->select()->toArray();
$result = [
"state" => "SUCCESS",
"list" => $list,
"total" => 0,
"start" => 0,
];
return json($result);
default:
return json($config);
}
}
public function composerInfo(): Json
{
$lockFilePath = root_path() . '/composer.lock';
$list = [];
if (file_exists($lockFilePath)) {
$lockFileContent = file_get_contents($lockFilePath);
if ($lockFileContent !== false) {
$lockData = json_decode($lockFileContent, true);
if (!empty($lockData['packages'])) {
foreach ($lockData['packages'] as $package) {
$list[] = ['name' => $package['name'], 'version' => $package['version']];
}
}
}
}
$this->success('success', $list);
}
}

View File

@ -0,0 +1,198 @@
<?php
namespace app\admin\controller;
use app\admin\model\MallOrder;
use app\admin\model\SystemAdmin;
use app\admin\model\SystemQuick;
use app\common\controller\AdminController;
use app\Request;
use Exception;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use think\facade\Db;
class Index extends AdminController
{
/**
* 后台主页
* @param Request $request
* @return string
*/
public function index(Request $request): string
{
return $this->fetch('', ['admin' => $request->adminUserInfo,]);
}
/**
* 后台欢迎页
* @return string
* @throws Exception
*/
public function welcome(): string
{
$tpVersion = \think\facade\App::version();
$mysqlVersion = Db::query("select version() as version")[0]['version'] ?? '未知';
$phpVersion = phpversion();
$versions = compact('tpVersion', 'mysqlVersion', 'phpVersion');
$quick_list = SystemQuick::field('id,title,icon,href')
->where(['status' => 1])->order('sort', 'desc')->limit(50)->select()->toArray();
$quicks = array_chunk($quick_list, 8);
$this->assign(compact('quicks', 'versions'));
$data = [
'all_member'=>[
"title"=>"今日订单",
"data"=>(new MallOrder())->where([['create_time','>',strtotime(date('Y-m-d')." 06:00:00")]])->count(),
],
'login_member'=>[
"title"=>"今日金额",
"data"=> (new MallOrder())->where([['create_time','>',strtotime(date('Y-m-d')." 06:00:00")]])->sum('money'),
],
'recharge_member'=>[
"title"=>"今日已支付",
"data"=>(new MallOrder())->where([['create_time','>',strtotime(date('Y-m-d')." 06:00:00")],['status','=',2]])->count(),
],
'all_charge'=>[
"title"=>"今日已支付金额",
"data"=>(new MallOrder())->where([['create_time','>',strtotime(date('Y-m-d')." 06:00:00")],['status','=',2]])->sum('money'),
],
'day_charge'=>[
"title"=>"待处理订单",
"data"=>(new MallOrder())->where([['create_time','>',strtotime(date('Y-m-d')." 06:00:00")],['status','=',0]])->count(),
],
'recharge_number'=>[
"title"=>"总订单数",
"data"=>(new MallOrder())->count(),
],
];
$this->assign('data', $data);
return $this->fetch();
}
public function get_order()
{
$this->success('操作成功', [
'count'=>(new MallOrder())->where([['create_time','>',strtotime(date('Y-m-d')." 06:00:00")],['status','=',0]])->count(),
'rid'=>(new MallOrder())->where([['create_time','>',strtotime(date('Y-m-d')." 06:00:00")],['status','=',0]])
->order('id desc')->value('id')
]);
}
/**
* 修改管理员信息
* @param Request $request
* @return string
* @throws DataNotFoundException
* @throws DbException
* @throws ModelNotFoundException
*/
public function editAdmin(Request $request): string
{
$id = $this->adminUid;
$row = (new SystemAdmin())
->withoutField('password')
->find($id);
empty($row) && $this->error('用户信息不存在');
if ($request->isPost()) {
$post = $request->post();
$this->isDemo && $this->error('演示环境下不允许修改');
$rule = [];
$this->validate($post, $rule);
try {
$login_type = $post['login_type'] ?? 1;
if ($login_type == 2) {
$ga_secret = (new SystemAdmin())->where('id', $id)->value('ga_secret');
if (empty($ga_secret)) $this->error('请先绑定谷歌验证器');
}
$save = $row->allowField(['head_img', 'phone', 'remark', 'update_time', 'login_type'])->save($post);
}catch (\PDOException $e) {
$this->error('保存失败');
}
$save ? $this->success('保存成功') : $this->error('保存失败');
}
$this->assign('row', $row);
$notes = (new SystemAdmin())->notes;
$this->assign('notes', $notes);
return $this->fetch();
}
/**
* 修改密码
* @param Request $request
* @return string
*/
public function editPassword(Request $request): string
{
$id = $this->adminUid;
$row = (new SystemAdmin())
->withoutField('password')
->find($id);
if (!$row) {
$this->error('用户信息不存在');
}
if ($request->isPost()) {
$post = $request->post();
$this->isDemo && $this->error('演示环境下不允许修改');
$rule = [
'password|登录密码' => 'require',
'password_again|确认密码' => 'require',
];
$this->validate($post, $rule);
if ($post['password'] != $post['password_again']) {
$this->error('两次密码输入不一致');
}
try {
$save = $row->save([
'password' => password_hash($post['password'], PASSWORD_DEFAULT),
]);
}catch (Exception $e) {
$this->error('保存失败');
}
if ($save) {
$this->success('保存成功');
}else {
$this->error('保存失败');
}
}
$this->assign('row', $row);
return $this->fetch();
}
/**
* 设置谷歌验证码
* @param Request $request
* @return string
* @throws Exception
*/
public function set2fa(Request $request): string
{
$id = $this->adminUid;
$row = (new SystemAdmin())->withoutField('password')->find($id);
if (!$row) $this->error('用户信息不存在');
// You can see: https://gitee.com/wolf-code/authenticator
$ga = new \Wolfcode\Authenticator\google\PHPGangstaGoogleAuthenticator();
if (!$request->isAjax()) {
$old_secret = $row->ga_secret;
$secret = $ga->createSecret(32);
$ga_title = $this->isDemo ? 'EasyAdmin8演示环境' : '可自定义修改显示标题';
$dataUri = $ga->getQRCode($ga_title, $secret);
$this->assign(compact('row', 'dataUri', 'old_secret', 'secret'));
return $this->fetch();
}
$this->isDemo && $this->error('演示环境下不允许修改');
$post = $request->post();
$ga_secret = $post['ga_secret'] ?? '';
$ga_code = $post['ga_code'] ?? '';
if (empty($ga_code)) $this->error('请输入验证码');
if (!$ga->verifyCode($ga_secret, $ga_code)) $this->error('验证码错误');
$row->ga_secret = $ga_secret;
$row->login_type = 2;
$row->save();
$this->success('操作成功');
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace app\admin\controller;
use app\admin\model\SystemAdmin;
use app\common\controller\AdminController;
use app\common\utils\Helper;
use think\captcha\facade\Captcha;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use app\Request;
use think\Response;
use Wolfcode\RateLimiting\Attributes\RateLimitingMiddleware;
class Login extends AdminController
{
protected bool $ignoreLogin = true;
public function initialize(): void
{
parent::initialize();
$action = $this->request->action();
if (!empty($this->adminUid) && !in_array($action, ['out'])) {
$adminModuleName = config('admin.alias_name');
$this->success('已登录,无需再次登录', [], __url("@{$adminModuleName}"));
}
}
/**
* 用户登录
* @param Request $request
* @return string
* @throws DataNotFoundException
* @throws DbException
* @throws ModelNotFoundException
*/
#[RateLimitingMiddleware(key: [Helper::class, 'getIp'], seconds: 1, limit: 1, message: '请求过于频繁')]
public function index(Request $request): string
{
$captcha = env('EASYADMIN.CAPTCHA', 1);
if (!$request->isPost()) return $this->fetch('', compact('captcha'));
$post = $request->post();
$rule = [
'username|用户名' => 'require',
'password|密码' => 'require',
'keep_login|是否保持登录' => 'require',
];
$captcha == 1 && $rule['captcha|验证码'] = 'require|captcha';
$this->validate($post, $rule);
$admin = SystemAdmin::where(['username' => $post['username']])->find();
if (empty($admin)) {
$this->error('用户不存在');
}
if (!password_verify($post['password'], $admin->password)) {
$this->error('密码输入有误');
}
if ($admin->status == 0) {
$this->error('账号已被禁用');
}
if ($admin->login_type == 2) {
if (empty($post['ga_code'])) $this->error('请输入谷歌验证码', ['is_ga_code' => true]);
$ga = new \Wolfcode\Authenticator\google\PHPGangstaGoogleAuthenticator();
if (!$ga->verifyCode($admin->ga_secret, $post['ga_code'])) $this->error('谷歌验证码错误');;
}
$admin->login_num += 1;
$admin->save();
$admin = $admin->toArray();
unset($admin['password']);
$admin['expire_time'] = $post['keep_login'] == 1 ? 0 : time() + 7200;
session('admin', $admin);
$this->success('登录成功');
}
/**
* 用户退出
*/
public function out(): void
{
session('admin', null);
$this->success('退出登录成功');
}
/**
* 验证码
* @return Response
*/
public function captcha(): Response
{
return Captcha::instance()->create();
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace app\admin\controller\article;
use app\admin\service\annotation\NodeAnnotation;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\common\model\Articles;
use think\App;
#[ControllerAnnotation(title: '文章管理')]
class Article extends AdminController
{
public function __construct(App $app)
{
parent::__construct($app);
self::$model = Articles::class;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace app\admin\controller\article;
use app\admin\service\annotation\NodeAnnotation;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\common\model\ArticleCates;
use think\App;
#[ControllerAnnotation(title: '文章分类管理')]
class Cate extends AdminController
{
public function __construct(App $app)
{
parent::__construct($app);
self::$model = ArticleCates::class;
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace app\admin\controller\mall;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\MiddlewareAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use app\common\controller\AdminController;
use app\Request;
use think\App;
use think\response\Json;
#[ControllerAnnotation(title: '商城商品管理')]
class Blackip extends AdminController
{
#[NodeAnnotation(ignore: ['export'])] // 过滤不需要生成的权限节点 默认 CURD 中会自动生成部分节点 可以在此处过滤
protected array $ignoreNode;
public function __construct(App $app)
{
parent::__construct($app);
self::$model = new \app\admin\model\BlackIp();
}
#[NodeAnnotation(title: '列表', auth: true)]
public function index(Request $request): Json|string
{
if ($request->isAjax()) {
if (input('selectFields')) return $this->selectList();
list($page, $limit, $where) = $this->buildTableParams();
$count = self::$model::where($where)->count();
$list = self::$model::where($where)->page($page, $limit)->order($this->sort)->select()->toArray();
$data = [
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $list,
];
return json($data);
}
return $this->fetch();
}
#[MiddlewareAnnotation(ignore: MiddlewareAnnotation::IGNORE_LOGIN)]
public function no_check_login(Request $request): string
{
return '这里演示方法不需要经过登录验证';
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace app\admin\controller\mall;
use app\admin\model\MallCate;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use think\App;
#[ControllerAnnotation(title: '商品分类管理')]
class Cate extends AdminController
{
public function __construct(App $app)
{
parent::__construct($app);
self::$model = MallCate::class;
}
}

View File

@ -0,0 +1,135 @@
<?php
namespace app\admin\controller\mall;
use app\admin\model\MallCate;
use app\admin\model\MallGoods;
use app\admin\service\annotation\MiddlewareAnnotation;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use app\Request;
use think\App;
use think\response\Json;
use Wolfcode\Ai\Enum\AiType;
use Wolfcode\Ai\Service\AiChatService;
#[ControllerAnnotation(title: '商城商品管理')]
class Goods extends AdminController
{
#[NodeAnnotation(ignore: ['export'])] // 过滤不需要生成的权限节点 默认 CURD 中会自动生成部分节点 可以在此处过滤
protected array $ignoreNode;
public function __construct(App $app)
{
parent::__construct($app);
self::$model = new MallGoods();
$this->assign('cate', MallCate::column('title', 'id'));
}
#[NodeAnnotation(title: '列表', auth: true)]
public function index(Request $request): Json|string
{
if ($request->isAjax()) {
if (input('selectFields')) return $this->selectList();
list($page, $limit, $where) = $this->buildTableParams();
$count = self::$model::where($where)->count();
$list = self::$model::with(['cate'])->where($where)->page($page, $limit)->order($this->sort)->select()->toArray();
$data = [
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $list,
];
return json($data);
}
return $this->fetch();
}
#[NodeAnnotation(title: '入库', auth: true)]
public function stock(Request $request, $id): string
{
$row = self::$model::find($id);
empty($row) && $this->error('数据不存在');
if ($request->isPost()) {
$post = $request->post();
$rule = [];
$this->validate($post, $rule);
try {
$post['total_stock'] = $row->total_stock + $post['stock'];
$post['stock'] = $row->stock + $post['stock'];
$save = $row->save($post);
}catch (\Exception $e) {
$this->error('保存失败');
}
$save ? $this->success('保存成功') : $this->error('保存失败');
}
$this->assign('row', $row);
return $this->fetch();
}
#[MiddlewareAnnotation(ignore: MiddlewareAnnotation::IGNORE_LOGIN)]
public function no_check_login(Request $request): string
{
return '这里演示方法不需要经过登录验证';
}
#[NodeAnnotation(title: 'AI优化', auth: true)]
public function aiOptimization(Request $request): void
{
$message = $request->post('message');
if (empty($message)) $this->error('请输入内容');
// 演示环境下 默认返回的内容
if ($this->isDemo) {
$content = <<<EOF
演示环境中 默认返回的内容
我来帮你优化这个标题,让它更有吸引力且更符合电商平台的搜索逻辑:
"商务男士高端定制马克杯 | 办公室精英必备 | 优质陶瓷防烫手柄"
这个优化后的标题:
1. 突出了目标用户群体(商务男士)
2. 强调了产品定位(高端定制)
3. 点明了使用场景(办公室)
4. 添加了材质和功能特点(优质陶瓷、防烫手柄)
5. 使用了吸引人的关键词(精英必备)
这样的标题不仅更具体,也更容易被搜索引擎识别,同时能精准触达目标客户群。您觉得这个版本如何?
EOF;
$choices = [['message' => [
'role' => 'assistant',
'content' => $content,
]]];
$this->success('success', compact('choices'));
}
try {
$result = AiChatService::instance()
// 当使用推理模型时,可能存在超时的情况,所以需要设置超时时间为 0
// ->setTimeLimit(0)
// 请替换为您需要的模型类型
->setAiType(AiType::QWEN)
// 如果需要指定模型的 API 地址,可自行设置
// ->setAiUrl('https://xxx.com')
// 请替换为您的模型
->setAiModel('qwen-plus')
// 请替换为您的 API KEY
->setAiKey('sk-1234567890')
// 此内容会作为系统提示,会影响到回答的内容 当前仅作为测试使用
->setSystemContent('你现在是一位资深的海外电商产品经理')
->chat($message);
$choices = $result['choices'];
}catch (\Throwable $exception) {
$choices = [['message' => [
'role' => 'assistant',
'content' => $exception->getMessage(),
]]];
}
$this->success('success', compact('choices'));
}
}

View File

@ -0,0 +1,120 @@
<?php
namespace app\admin\controller\mall;
use app\admin\model\MallOrder;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\MiddlewareAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use app\common\controller\AdminController;
use app\Request;
use think\App;
use think\facade\Db;
use think\response\Json;
use Wolfcode\Ai\Enum\AiType;
use Wolfcode\Ai\Service\AiChatService;
#[ControllerAnnotation(title: '商城商品管理')]
class Order extends AdminController
{
#[NodeAnnotation(ignore: ['export'])] // 过滤不需要生成的权限节点 默认 CURD 中会自动生成部分节点 可以在此处过滤
protected array $ignoreNode;
public function __construct(App $app)
{
parent::__construct($app);
self::$model = new MallOrder();
}
protected array $allowModifyFields = [
'status',
'sort',
'remark',
'is_delete',
'is_auth',
'title',
'url',
];
#[NodeAnnotation(title: '列表', auth: true)]
public function index(Request $request): Json|string
{
if ($request->isAjax()) {
if (input('selectFields')) return $this->selectList();
list($page, $limit, $where) = $this->buildTableParams();
$count = self::$model::where($where)->count();
$list = self::$model::where($where)->page($page, $limit)->order($this->sort)->select()->toArray();
$data = [
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $list,
];
return json($data);
}
return $this->fetch();
}
#[NodeAnnotation(title: '确认支付成功', auth: true)]
public function recharge(Request $request, $id): string
{
$row = self::$model::find($id);
empty($row) && $this->error('数据不存在');
if ($request->isPost()) {
$post = $request->post();
try {
$post['status'] = 2;
$save = $row->save($post);
}catch (\Exception $e) {
$this->error('保存失败');
}
$save ? $this->success('确认支付成功') : $this->error('保存失败');
}
}
#[NodeAnnotation(title: '编辑', auth: true)]
public function edit(Request $request, $id = 0): string
{
$row = self::$model::find($id);
empty($row) && $this->error('数据不存在');
if ($request->isPost()) {
$post = $request->post();
$rule = [];
$this->validate($post, $rule);
try {
Db::transaction(function() use ($post, $row, &$save) {
$post['status'] = 1;
$save = $row->save($post);
});
}catch (\Exception $e) {
$this->error('保存失败');
}
$save ? $this->success('保存成功') : $this->error('保存失败');
}
$this->assign('row', $row);
return $this->fetch();
}
#[NodeAnnotation(title: 'IP拉黑', auth: true)]
public function blockip(Request $request, $id): string
{
$row = self::$model::find($id);
empty($row) && $this->error('数据不存在');
if ($request->isPost()) {
try {
$save = (new \app\admin\model\BlackIp())->save([
'ip' => $row->ip,
]);
}catch (\Exception $e) {
$this->error('保存失败');
}
$save ? $this->success('IP拉黑成功') : $this->error('保存失败');
}
}
#[MiddlewareAnnotation(ignore: MiddlewareAnnotation::IGNORE_LOGIN)]
public function no_check_login(Request $request): string
{
return '这里演示方法不需要经过登录验证';
}
}

View File

@ -0,0 +1,179 @@
<?php
namespace app\admin\controller\system;
use app\admin\model\SystemAdmin;
use app\admin\service\TriggerService;
use app\common\constants\AdminConstant;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use app\Request;
use think\App;
use think\response\Json;
#[ControllerAnnotation(title: '管理员管理')]
class Admin extends AdminController
{
protected array $sort = [
'sort' => 'desc',
'id' => 'desc',
];
public function __construct(App $app)
{
parent::__construct($app);
self::$model = SystemAdmin::class;
$this->assign('auth_list', self::$model::getAuthList());
}
#[NodeAnnotation(title: '列表', auth: true)]
public function index(Request $request): Json|string
{
if ($request->isAjax()) {
if (input('selectFields')) {
return $this->selectList();
}
list($page, $limit, $where) = $this->buildTableParams();
$count = self::$model::where($where)->count();
$list = self::$model::withoutField('password')
->where($where)
->page($page, $limit)
->order($this->sort)
->select()->toArray();
$data = [
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $list,
];
return json($data);
}
return $this->fetch();
}
#[NodeAnnotation(title: '添加', auth: true)]
public function add(Request $request): string
{
if ($request->isPost()) {
$post = $request->post();
$authIds = $request->post('auth_ids', []);
$post['auth_ids'] = implode(',', array_keys($authIds));
$rule = [];
$this->validate($post, $rule);
if (empty($post['password'])) $post['password'] = '123456';
$post['password'] = password_hash($post['password'],PASSWORD_DEFAULT);
try {
$save = self::$model::create($post);
}catch (\Exception $e) {
$this->error('保存失败' . $e->getMessage());
}
$save ? $this->success('保存成功') : $this->error('保存失败');
}
return $this->fetch();
}
#[NodeAnnotation(title: '编辑', auth: true)]
public function edit(Request $request, $id = 0): string
{
$row = self::$model::find($id);
empty($row) && $this->error('数据不存在');
if ($request->isPost()) {
$post = $request->post();
$authIds = $request->post('auth_ids', []);
$post['auth_ids'] = implode(',', array_keys($authIds));
$rule = [];
$this->validate($post, $rule);
try {
$save = $row->save($post);
TriggerService::updateMenu($id);
}catch (\Exception $e) {
$this->error('保存失败' . $e->getMessage());
}
$save ? $this->success('保存成功') : $this->error('保存失败');
}
$this->assign('row', $row);
return $this->fetch();
}
#[NodeAnnotation(title: '设置密码', auth: true)]
public function password(Request $request, $id): string
{
$row = self::$model::find($id);
empty($row) && $this->error('数据不存在');
if ($request->isAjax()) {
$post = $request->post();
$rule = [
'password|登录密码' => 'require',
'password_again|确认密码' => 'require',
];
$this->validate($post, $rule);
if ($post['password'] != $post['password_again']) {
$this->error('两次密码输入不一致');
}
try {
$save = $row->save([
'password' => password_hash($post['password'],PASSWORD_DEFAULT),
]);
}catch (\Exception $e) {
$this->error('保存失败');
}
$save ? $this->success('保存成功') : $this->error('保存失败');
}
$this->assign('row', $row);
return $this->fetch();
}
#[NodeAnnotation(title: '删除', auth: true)]
public function delete(Request $request): void
{
$this->checkPostRequest();
$id = $request->param('id');
$row = self::$model::whereIn('id', $id)->select();
$row->isEmpty() && $this->error('数据不存在');
$id == AdminConstant::SUPER_ADMIN_ID && $this->error('超级管理员不允许修改');
if (is_array($id)) {
if (in_array(AdminConstant::SUPER_ADMIN_ID, $id)) {
$this->error('超级管理员不允许修改');
}
}
try {
$save = $row->delete();
}catch (\Exception $e) {
$this->error('删除失败');
}
$save ? $this->success('删除成功') : $this->error('删除失败');
}
#[NodeAnnotation(title: '属性修改', auth: true)]
public function modify(Request $request): void
{
$this->checkPostRequest();
$post = $request->post();
$rule = [
'id|ID' => 'require',
'field|字段' => 'require',
'value|值' => 'require',
];
$this->validate($post, $rule);
if (!in_array($post['field'], $this->allowModifyFields)) {
$this->error('该字段不允许修改:' . $post['field']);
}
if ($post['id'] == AdminConstant::SUPER_ADMIN_ID && $post['field'] == 'status') {
$this->error('超级管理员状态不允许修改');
}
$row = self::$model::find($post['id']);
empty($row) && $this->error('数据不存在');
try {
$row->save([
$post['field'] => $post['value'],
]);
}catch (\Exception $e) {
$this->error($e->getMessage());
}
$this->success('保存成功');
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace app\admin\controller\system;
use app\admin\model\SystemAuth;
use app\admin\model\SystemAuthNode;
use app\admin\service\TriggerService;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use app\Request;
use think\App;
#[ControllerAnnotation(title: '角色权限管理', auth: true)]
class Auth extends AdminController
{
protected array $sort = [
'sort' => 'desc',
'id' => 'desc',
];
public function __construct(App $app)
{
parent::__construct($app);
self::$model = SystemAuth::class;
}
#[NodeAnnotation(title: '授权', auth: true)]
public function authorize(Request $request, $id): string
{
$row = self::$model::find($id);
empty($row) && $this->error('数据不存在');
if ($request->isAjax()) {
$list = self::$model::getAuthorizeNodeListByAdminId($id);
$this->success('获取成功', $list);
}
$this->assign('row', $row);
return $this->fetch();
}
#[NodeAnnotation(title: '授权保存', auth: true)]
public function saveAuthorize(Request $request): void
{
$this->checkPostRequest();
$id = $request->post('id');
$node = $request->post('node', "[]");
$node = json_decode($node, true);
$row = self::$model::find($id);
empty($row) && $this->error('数据不存在');
try {
$authNode = new SystemAuthNode();
$authNode->where('auth_id', $id)->delete();
if (!empty($node)) {
$saveAll = [];
foreach ($node as $vo) {
$saveAll[] = [
'auth_id' => $id,
'node_id' => $vo,
];
}
$authNode->saveAll($saveAll);
}
TriggerService::updateMenu();
}catch (\Exception $e) {
$this->error('保存失败');
}
$this->success('保存成功');
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace app\admin\controller\system;
use app\admin\model\SystemConfig;
use app\admin\service\TriggerService;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use app\Request;
use think\App;
use think\facade\Cache;
use think\response\Json;
#[ControllerAnnotation(title: '系统配置管理')]
class Config extends AdminController
{
public function __construct(App $app)
{
parent::__construct($app);
self::$model = SystemConfig::class;
$this->assign('upload_types', config('admin.upload_types'));
$this->assign('editor_types', config('admin.editor_types'));
}
#[NodeAnnotation(title: '列表', auth: true)]
public function index(Request $request): Json|string
{
return $this->fetch();
}
#[NodeAnnotation(title: '保存', auth: true)]
public function save(Request $request): void
{
$this->checkPostRequest();
$post = $request->post();
$notAddFields = ['_token', 'file', 'group'];
try {
$group = $post['group'] ?? '';
if (empty($group)) $this->error('保存失败');
if ($group == 'upload') {
$upload_types = config('admin.upload_types');
// 兼容旧版本
self::$model::where('name', 'upload_allow_type')->update(['value' => implode(',', array_keys($upload_types))]);
}
foreach ($post as $key => $val) {
if (in_array($key, $notAddFields)) continue;
$config_key_data = self::$model::where('name', $key)->find();
if (!is_null($config_key_data)) {
$config_key_data->save(['value' => $val,]);
}else {
self::$model::create(
[
'name' => $key,
'value' => $val,
'group' => $group,
]);
}
if (Cache::has($key)) Cache::set($key, $val);
}
TriggerService::updateMenu();
TriggerService::updateSysConfig();
}catch (\Exception $e) {
$this->error('保存失败' . $e->getMessage());
}
$this->success('保存成功');
}
}

View File

@ -0,0 +1,155 @@
<?php
namespace app\admin\controller\system;
use app\admin\service\curd\BuildCurd;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use app\Request;
use think\db\exception\PDOException;
use think\exception\FileException;
use think\facade\Console;
use think\facade\Db;
use think\helper\Str;
use think\response\Json;
#[ControllerAnnotation(title: 'CURD可视化管理')]
class CurdGenerate extends AdminController
{
#[NodeAnnotation(title: '列表', auth: true)]
public function index(Request $request): Json|string
{
return $this->fetch();
}
#[NodeAnnotation(title: '操作', auth: true)]
public function save(Request $request, string $type = ''): ?Json
{
if (!$request->isAjax()) $this->error();
switch ($type) {
case "search":
$tb_prefix = $request->param('tb_prefix/s', '');
$tb_name = $request->param('tb_name/s', '');
if (empty($tb_name)) $this->error('参数错误');
try {
$list = Db::query("SHOW FULL COLUMNS FROM {$tb_prefix}{$tb_name}");
$data = [];
foreach ($list as $value) {
$data[] = [
'name' => $value['Field'],
'type' => $value['Type'],
'key' => $value['Key'],
'extra' => $value['Extra'],
'null' => $value['Null'],
'desc' => $value['Comment'],
];
}
$this->success('查询成功', compact('data', 'list'));
}catch (PDOException $exception) {
$this->error($exception->getMessage());
}
break;
case "add":
$tb_prefix = $request->param('tb_prefix/s', '');
$tb_name = $request->param('tb_name/s', '');
if (empty($tb_name)) $this->error('参数错误');
$tb_fields = $request->param('tb_fields');
$force = $request->post('force/d', 0);
try {
$build = (new BuildCurd())->setTablePrefix($tb_prefix)->setTable($tb_name);
$build->setForce($force); // 强制覆盖
// 新增字段类型
if ($tb_fields) {
foreach ($tb_fields as $tk => $tf) {
if (empty($tf)) continue;
$tf = array_values($tf);
switch ($tk) {
case 'ignore':
$build->setIgnoreFields($tf, true);
break;
case 'select':
$build->setSelectFields($tf, true);
break;
case 'radio':
$build->setRadioFieldSuffix($tf, true);
break;
case 'checkbox':
$build->setCheckboxFieldSuffix($tf, true);
break;
case 'image':
$build->setImageFieldSuffix($tf, true);
break;
case 'images':
$build->setImagesFieldSuffix($tf, true);
break;
case 'date':
$build->setDateFieldSuffix($tf, true);
break;
case 'datetime':
$build->setDatetimeFieldSuffix($tf, true);
break;
case 'editor':
$build->setEditorFields($tf, true);
break;
default:
break;
}
}
}
$build = $build->render();
$fileList = $build->getFileList();
if (empty($fileList)) $this->error('这里什么都没有');
$result = $build->create();
$_file = $result[0] ?? '';
$link = '';
if (!empty($_file)) {
$_fileExp = explode(DIRECTORY_SEPARATOR, $_file);
$_fileExp_last = array_slice($_fileExp, -2);
$_fileExp_last_0 = $_fileExp_last[0] . '.';
if ($_fileExp_last[0] == 'controller') $_fileExp_last_0 = '';
$link = '/' . config('admin.alias_name') . '/' . $_fileExp_last_0 . Str::snake(explode('.php', end($_fileExp_last))[0] ?? '') . '/index';
}
$this->success('生成成功', compact('result', 'link'));
}catch (FileException $exception) {
return json(['code' => -1, 'msg' => $exception->getMessage()]);
}
break;
case "delete":
$tb_prefix = $request->param('tb_prefix/s', '');
$tb_name = $request->param('tb_name/s', '');
if (empty($tb_name)) $this->error('参数错误');
try {
$build = (new BuildCurd())->setTablePrefix($tb_prefix)->setTable($tb_name);
$build = $build->render();
$fileList = $build->getFileList();
if (empty($fileList)) $this->error('这里什么都没有');
$result = $build->delete();
$this->success('删除自动生成CURD文件成功', compact('result'));
}catch (FileException $exception) {
return json(['code' => -1, 'msg' => $exception->getMessage()]);
}
break;
case 'console':
$command = $request->post('command', '');
if (empty($command)) $this->error('请输入命令');
$commandExp = explode(' ', $command);
$commandExp = array_values(array_filter($commandExp));
try {
$output = Console::call('curd', [...$commandExp]);
}catch (\Throwable $exception) {
$this->error($exception->getMessage() . $exception->getLine());
}
if (empty($output)) $this->error('设置错误');
$this->success($output->fetch());
break;
default:
$this->error('参数错误');
break;
}
}
}

View File

@ -0,0 +1,138 @@
<?php
namespace app\admin\controller\system;
use app\admin\model\SystemLog;
use app\admin\service\annotation\MiddlewareAnnotation;
use app\admin\service\tool\CommonTool;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use app\Request;
use jianyan\excel\Excel;
use think\App;
use think\db\exception\DbException;
use think\db\exception\PDOException;
use think\facade\Db;
use think\response\Json;
#[ControllerAnnotation(title: '操作日志管理')]
class Log extends AdminController
{
public function __construct(App $app)
{
parent::__construct($app);
self::$model = SystemLog::class;
}
#[NodeAnnotation(title: '列表', auth: true)]
public function index(Request $request): Json|string
{
if ($request->isAjax()) {
if (input('selectFields')) {
return $this->selectList();
}
[$page, $limit, $where, $excludeFields] = $this->buildTableParams(['month']);
$month = !empty($excludeFields['month']) ? date('Ym', strtotime($excludeFields['month'])) : date('Ym');
$model = (new self::$model)->setSuffix("_$month")->with('admin')->where($where);
try {
$count = $model->count();
$list = $model->page($page, $limit)->order($this->sort)->select();
}catch (PDOException|DbException $exception) {
$count = 0;
$list = [];
}
$data = [
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $list,
];
return json($data);
}
return $this->fetch();
}
#[NodeAnnotation(title: '导出', auth: true)]
public function export()
{
if (env('EASYADMIN.IS_DEMO', false)) {
$this->error('演示环境下不允许操作');
}
[$page, $limit, $where, $excludeFields] = $this->buildTableParams(['month']);
$month = !empty($excludeFields['month']) ? date('Ym', strtotime($excludeFields['month'])) : date('Ym');
$tableName = (new self::$model)->setSuffix("_$month")->getName();
$tableName = CommonTool::humpToLine(lcfirst($tableName));
$prefix = config('database.connections.mysql.prefix');
$dbList = Db::query("show full columns from {$prefix}{$tableName}");
$header = [];
foreach ($dbList as $vo) {
$comment = !empty($vo['Comment']) ? $vo['Comment'] : $vo['Field'];
if (!in_array($vo['Field'], $this->noExportFields)) {
$header[] = [$comment, $vo['Field']];
}
}
$model = (new self::$model)->setSuffix("_$month")->with('admin')->where($where);
try {
$list = $model
->limit(10000)
->order('id', 'desc')
->select()
->toArray();
foreach ($list as &$vo) {
$vo['content'] = json_encode($vo['content'], JSON_UNESCAPED_UNICODE);
$vo['response'] = json_encode($vo['response'], JSON_UNESCAPED_UNICODE);
}
exportExcel($header, $list, '操作日志');
}catch (\Throwable $exception) {
$this->error($exception->getMessage());
}
}
#[NodeAnnotation(title: '删除指定日志', auth: true)]
public function deleteMonthLog(Request $request)
{
if (!$request->isAjax()) {
return $this->fetch();
}
if ($this->isDemo) $this->error('演示环境下不允许操作');
$monthsAgo = $request->param('month/d', 0);
if ($monthsAgo < 1) $this->error('月份错误');
$currentDate = new \DateTime();
$currentDate->modify("-$monthsAgo months");
$dbPrefix = env('DB_PREFIX');
$dbLike = "{$dbPrefix}system_log_";
$tables = Db::query("SHOW TABLES LIKE '$dbLike%'");
$threshold = date('Ym', strtotime("-$monthsAgo month"));
$tableNames = [];
try {
foreach ($tables as $table) {
$tableName = current($table);
if (!preg_match("/^$dbLike\d{6}$/", $tableName)) continue;
$datePart = substr($tableName, -6);
$issetTable = Db::query("SHOW TABLES LIKE '$tableName'");
if (!$issetTable) continue;
if ($datePart - $threshold <= 0) {
Db::execute("DROP TABLE `$tableName`");
$tableNames[] = $tableName;
}
}
}catch (PDOException) {
}
if (empty($tableNames)) $this->error('没有需要删除的表');
$this->success('操作成功 - 共删除 ' . count($tableNames) . ' 张表<br/>' . implode('<br>', $tableNames));
}
#[MiddlewareAnnotation(ignore: MiddlewareAnnotation::IGNORE_LOG)]
#[NodeAnnotation(title: '框架日志', auth: true, ignore: NodeAnnotation::IGNORE_NODE)]
public function record(): Json|string
{
return (new \Wolfcode\PhpLogviewer\thinkphp\LogViewer())->fetch();
}
}

View File

@ -0,0 +1,191 @@
<?php
namespace app\admin\controller\system;
use app\admin\model\SystemMenu;
use app\admin\model\SystemNode;
use app\admin\service\TriggerService;
use app\common\constants\MenuConstant;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use app\common\controller\AdminController;
use app\Request;
use think\App;
use think\response\Json;
#[ControllerAnnotation(title: '菜单管理')]
class Menu extends AdminController
{
protected array $sort = [
'sort' => 'desc',
'id' => 'asc',
];
public function __construct(App $app)
{
parent::__construct($app);
self::$model = SystemMenu::class;
}
#[NodeAnnotation(title: '列表', auth: true)]
public function index(Request $request): Json|string
{
if ($request->isAjax()) {
if (input('selectFields')) {
return $this->selectList();
}
$count = self::$model::count();
$list = self::$model::order($this->sort)->select()->toArray();
$data = [
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $list,
];
return json($data);
}
return $this->fetch();
}
#[NodeAnnotation(title: '添加', auth: true)]
public function add(Request $request): string
{
$id = $request->param('id');
$homeId = self::$model::where(['pid' => MenuConstant::HOME_PID,])->value('id');
if ($id == $homeId) {
$this->error('首页不能添加子菜单');
}
if ($request->isPost()) {
$post = $request->post();
$rule = [
'pid|上级菜单' => 'require',
'title|菜单名称' => 'require',
'icon|菜单图标' => 'require',
];
$this->validate($post, $rule);
try {
$save = self::$model::create($post);
}catch (\Exception $e) {
$this->error('保存失败');
}
if ($save) {
TriggerService::updateMenu();
$this->success('保存成功');
}else {
$this->error('保存失败');
}
}
$pidMenuList = self::$model::getPidMenuList();
$this->assign('id', $id);
$this->assign('pidMenuList', $pidMenuList);
return $this->fetch();
}
#[NodeAnnotation(title: '编辑', auth: true)]
public function edit(Request $request, $id = 0): string
{
$row = self::$model::find($id);
empty($row) && $this->error('数据不存在');
if ($request->isPost()) {
$post = $request->post();
$rule = [
'pid|上级菜单' => 'require',
'title|菜单名称' => 'require',
'icon|菜单图标' => 'require',
];
$this->validate($post, $rule);
if ($row->pid == MenuConstant::HOME_PID) $post['pid'] = MenuConstant::HOME_PID;
try {
$save = $row->save($post);
}catch (\Exception $e) {
$this->error('保存失败');
}
if (!empty($save)) {
TriggerService::updateMenu();
$this->success('保存成功');
}else {
$this->error('保存失败');
}
}
$pidMenuList = self::$model::getPidMenuList();
$this->assign([
'id' => $id,
'pidMenuList' => $pidMenuList,
'row' => $row,
]);
return $this->fetch();
}
#[NodeAnnotation(title: '删除', auth: true)]
public function delete(Request $request): void
{
$this->checkPostRequest();
$id = $request->param('id');
$row = self::$model::whereIn('id', $id)->select();
empty($row) && $this->error('数据不存在');
try {
$save = $row->delete();
}catch (\Exception $e) {
$this->error('删除失败');
}
if ($save) {
TriggerService::updateMenu();
$this->success('删除成功');
}else {
$this->error('删除失败');
}
}
#[NodeAnnotation(title: '属性修改', auth: true)]
public function modify(Request $request): void
{
$this->checkPostRequest();
$post = $request->post();
$rule = [
'id|ID' => 'require',
'field|字段' => 'require',
'value|值' => 'require',
];
$this->validate($post, $rule);
$row = self::$model::find($post['id']);
if (!$row) {
$this->error('数据不存在');
}
if (!in_array($post['field'], $this->allowModifyFields)) {
$this->error('该字段不允许修改:' . $post['field']);
}
$homeId = self::$model::where([
'pid' => MenuConstant::HOME_PID,
])
->value('id');
if ($post['id'] == $homeId && $post['field'] == 'status') {
$this->error('首页状态不允许关闭');
}
try {
$row->save([
$post['field'] => $post['value'],
]);
}catch (\Exception $e) {
$this->error($e->getMessage());
}
TriggerService::updateMenu();
$this->success('保存成功');
}
#[NodeAnnotation(title: '添加菜单提示', auth: true)]
public function getMenuTips(): Json
{
$node = input('get.keywords');
$list = SystemNode::whereLike('node', "%{$node}%")
->field('node,title')
->limit(10)
->select()->toArray();
return json([
'code' => 0,
'content' => $list,
'type' => 'success',
]);
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace app\admin\controller\system;
use app\admin\model\SystemMenu;
use app\admin\model\SystemNode;
use app\admin\service\TriggerService;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use app\admin\service\NodeService;
use app\Request;
use think\App;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use think\response\Json;
#[ControllerAnnotation(title: '系统节点管理')]
class Node extends AdminController
{
public function __construct(App $app)
{
parent::__construct($app);
self::$model = SystemNode::class;
}
#[NodeAnnotation(title: '列表', auth: true)]
public function index(Request $request): Json|string
{
if ($request->isAjax()) {
if (input('selectFields')) {
return $this->selectList();
}
$count = self::$model::count();
$list = self::$model::getNodeTreeList();
$data = [
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $list,
];
return json($data);
}
return $this->fetch();
}
#[NodeAnnotation(title: '系统节点更新', auth: true)]
public function refreshNode($force = 0): void
{
$this->checkPostRequest();
$nodeList = (new NodeService())->getNodeList();
empty($nodeList) && $this->error('暂无需要更新的系统节点');
try {
if ($force == 1) {
$updateNodeList = self::$model::whereIn('node', array_column($nodeList, 'node'))->select();
$formatNodeList = array_format_key($nodeList, 'node');
foreach ($updateNodeList as $vo) {
isset($formatNodeList[$vo['node']])
&& self::$model::where('id', $vo['id'])->update(
[
'title' => $formatNodeList[$vo['node']]['title'],
'is_auth' => $formatNodeList[$vo['node']]['is_auth'],
]
);
}
}
$existNodeList = self::$model::field('node,title,type,is_auth')->select();
foreach ($nodeList as $key => $vo) {
foreach ($existNodeList as $v) {
if ($vo['node'] == $v->node) {
unset($nodeList[$key]);
break;
}
}
}
if (!empty($nodeList)) {
(new self::$model)->saveAll($nodeList);
TriggerService::updateNode();
}
}catch (\Exception $e) {
$this->error('节点更新失败');
}
$this->success('节点更新成功');
}
#[NodeAnnotation(title: '清除失效节点', auth: true)]
public function clearNode(): void
{
$this->checkPostRequest();
$nodeList = (new NodeService())->getNodeList();
try {
$existNodeList = self::$model::field('id,node,title,type,is_auth')->select()->toArray();
$formatNodeList = array_format_key($nodeList, 'node');
foreach ($existNodeList as $vo) {
!isset($formatNodeList[$vo['node']]) && self::$model::where('id', $vo['id'])->delete();
}
TriggerService::updateNode();
}catch (\Exception $e) {
$this->error('节点更新失败');
}
$this->success('节点更新成功');
}
/**
* @throws \ReflectionException
* @throws \Doctrine\Common\Annotations\AnnotationException
*/
#[NodeAnnotation(title: '刷新菜单', auth: true)]
public function refreshMenu(): void
{
$this->checkPostRequest();
$nodeList = (new NodeService())->getNodeList();
empty($nodeList) && $this->error('暂无需要更新的系统节点');
try {
SystemMenu::refreshMenu($nodeList);
}catch (\Exception $e) {
$this->error($e->getMessage());
}
$this->success('菜单刷新成功');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace app\admin\controller\system;
use app\admin\model\SystemQuick;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use think\App;
#[ControllerAnnotation(title: '快捷入口管理')]
class Quick extends AdminController
{
protected array $sort = [
'sort' => 'desc',
'id' => 'desc',
];
public function __construct(App $app)
{
parent::__construct($app);
self::$model = SystemQuick::class;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace app\admin\controller\system;
use app\admin\model\SystemUploadfile;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use think\App;
#[ControllerAnnotation(title: '上传文件管理')]
class Uploadfile extends AdminController
{
public function __construct(App $app)
{
parent::__construct($app);
self::$model = SystemUploadfile::class;
$this->assign('upload_types', config('admin.upload_types'));
}
}

0
app/admin/entity/.keep Normal file
View File

12
app/admin/entity/Test.php Normal file
View File

@ -0,0 +1,12 @@
<?php
namespace app\admin\entity;
use app\common\entity\BaseEntity;
/**
* ThinkORM 4.0 实体模型案例
* 可与 Model 并存 或者 单独使用
* @package app\admin\entity
*/
class Test extends BaseEntity {}

5
app/admin/middleware.php Normal file
View File

@ -0,0 +1,5 @@
<?php
return [
// ...
];

View File

@ -0,0 +1,43 @@
<?php
namespace app\admin\middleware;
use app\common\service\AuthService;
use app\common\traits\JumpTrait;
use app\Request;
use Closure;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
class CheckAuth
{
use JumpTrait;
/**
* @throws ModelNotFoundException
* @throws DbException
* @throws DataNotFoundException
*/
public function handle(Request $request, Closure $next)
{
$adminUserInfo = $request->adminUserInfo;
if (empty($adminUserInfo)) return $next($request);
$adminConfig = config('admin');
$adminId = $adminUserInfo['id'];
$authService = app(AuthService::class, ['adminId' => $adminId]);
$currentNode = $authService->getCurrentNode();
$currentController = parse_name($request->controller());
if (!in_array($currentController, $adminConfig['no_auth_controller']) && !in_array($currentNode, $adminConfig['no_auth_node'])) {
$check = $authService->checkNode($currentNode);
!$check && $this->error('无权限访问');
// 判断是否为演示环境
if (env('EASYADMIN.IS_DEMO', false) && $request->isPost()) {
if (!in_array($currentNode, ['system.log/record', 'mall.goods/aiOptimization'])) $this->error('演示环境下不允许修改');
}
}
return $next($request);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace app\admin\middleware;
use app\common\traits\JumpTrait;
use app\Request;
use Closure;
class CheckInstall
{
use JumpTrait;
public function handle(Request $request, Closure $next)
{
$controller = $request->controller();
if (!is_file(root_path() . 'config' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'lock' . DIRECTORY_SEPARATOR . 'install.lock')) {
if ($controller != 'Install') return redirect('/install');
}
return $next($request);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace app\admin\middleware;
use app\common\traits\JumpTrait;
use app\Request;
use Closure;
use ReflectionClass;
use ReflectionException;
use app\admin\service\annotation\MiddlewareAnnotation;
class CheckLogin
{
use JumpTrait;
/**
* @throws ReflectionException
*/
public function handle(Request $request, Closure $next)
{
$controller = $request->controller();
if (empty($controller)) return $next($request);
if (str_contains($controller, '.')) $controller = str_replace('.', '\\', $controller);
$action = $request->action();
$controllerClass = 'app\\admin\\controller\\' . $controller;
$classObj = new ReflectionClass($controllerClass);
$properties = $classObj->getDefaultProperties();
// 整个控制器是否忽略登录
$ignoreLogin = $properties['ignoreLogin'] ?? false;
$adminUserInfo = session('admin');
if (!$ignoreLogin) {
$noNeedCheck = $properties['noNeedCheck'] ?? [];
if (in_array($action, $noNeedCheck)) {
return $next($request);
}
try {
$reflectionMethod = new \ReflectionMethod($controllerClass, $action);
$attributes = $reflectionMethod->getAttributes(MiddlewareAnnotation::class);
foreach ($attributes as $attribute) {
$annotation = $attribute->newInstance();
$_ignore = (array)$annotation->ignore;
// 控制器中的某个方法忽略登录
if (in_array('LOGIN', $_ignore)) return $next($request);
}
}catch (\Throwable) {
}
if (empty($adminUserInfo)) {
return redirect(__url('login/index'));
}
// 判断是否登录过期
$expireTime = $adminUserInfo['expire_time'];
if ($expireTime !== 0 && time() > $expireTime) {
session('admin', null);
$this->error('登录已过期,请重新登录', [], __url(env('EASYADMIN.ADMIN') . '/login/index'));
}
}
$request->adminUserInfo = $adminUserInfo ?: [];
return $next($request);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace app\admin\middleware;
use app\common\traits\JumpTrait;
use app\Request;
use Closure;
use Wolfcode\RateLimiting\Bootstrap;
class RateLimiting
{
use JumpTrait;
/**
* 启用限流器需要开启Redis
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next): mixed
{
// 是否启用限流器
if (!env('RATE_LIMITING_STATUS', false)) return $next($request);
if ($request->method() == 'GET') return $next($request);
$controller = $request->controller();
$module = app('http')->getName();
$appNamespace = config('app.app_namespace');
$controllerClass = "app\\{$module}\\controller\\{$controller}{$appNamespace}";
$controllerClass = str_replace('.', '\\', $controllerClass);
$action = $request->action();
try {
Bootstrap::init($controllerClass, $action, [
# Redis 相关配置
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => (int)env('REDIS_PORT', 6379),
'password' => env('REDIS_PASSWORD', ''),
'prefix' => env('REDIS_PREFIX', ''),
'database' => (int)env('REDIS_DATABASE', 0),
]);
}catch (\Throwable $exception) {
$this->error($exception->getMessage());
}
return $next($request);
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace app\admin\middleware;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\MiddlewareAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use app\admin\service\SystemLogService;
use app\common\traits\JumpTrait;
use app\Request;
use Closure;
use ReflectionException;
class SystemLog
{
use JumpTrait;
/**
* 敏感信息字段,日志记录时需要加密
* @var array
*/
protected array $sensitiveParams = [
'password',
'password_again',
'phone',
'mobile',
];
/**
* @throws ReflectionException
*/
public function handle(Request $request, Closure $next)
{
$response = $next($request);
if (!env('APP_ADMIN_SYSTEM_LOG', true)) return $response;
$params = $request->param();
if (isset($params['s'])) unset($params['s']);
foreach ($params as $key => $val) {
in_array($key, $this->sensitiveParams) && $params[$key] = "***********";
}
$method = strtolower($request->method());
$url = $request->url();
if (env('APP_DEBUG')) {
trace(['url' => $url, 'method' => $method, 'params' => $params,], 'requestDebugInfo');
}
if ($request->isAjax()) {
if (in_array($method, ['post', 'put', 'delete'])) {
$title = '';
try {
$pathInfo = $request->pathinfo();
$pathInfoExp = explode('/', $pathInfo);
$_action = end($pathInfoExp) ?? '';
$pathInfoExp = explode('.', $pathInfoExp[0] ?? '');
$_name = $pathInfoExp[0] ?? '';
$_controller = ucfirst($pathInfoExp[1] ?? '');
$className = $_controller ? "app\admin\controller\\{$_name}\\{$_controller}" : "app\admin\controller\\{$_name}";
if ($_name && $_action) {
$reflectionMethod = new \ReflectionMethod($className, $_action);
$attributes = $reflectionMethod->getAttributes(MiddlewareAnnotation::class);
foreach ($attributes as $attribute) {
$annotation = $attribute->newInstance();
$_ignore = (array)$annotation->ignore;
if (in_array('log', array_map('strtolower', $_ignore))) return $response;
}
$controllerTitle = $nodeTitle = '';
$controllerAttributes = (new \ReflectionClass($className))->getAttributes(ControllerAnnotation::class);
$actionAttributes = $reflectionMethod->getAttributes(NodeAnnotation::class);
foreach ($controllerAttributes as $controllerAttribute) {
$controllerAnnotation = $controllerAttribute->newInstance();
$controllerTitle = $controllerAnnotation->title ?? '';
}
foreach ($actionAttributes as $actionAttribute) {
$actionAnnotation = $actionAttribute->newInstance();
$nodeTitle = $actionAnnotation->title ?? '';
}
$title = $controllerTitle . ' - ' . $nodeTitle;
}
}catch (\Throwable $exception) {
}
$ip = $request->ip();
// 限制记录的响应内容,避免过大
$_response = json_encode($response->getData(), JSON_UNESCAPED_UNICODE);
$_response = mb_substr($_response, 0, 3000, 'utf-8');
$data = [
'admin_id' => session('admin.id'),
'title' => $title,
'url' => $url,
'method' => $method,
'ip' => $ip,
'content' => json_encode($params, JSON_UNESCAPED_UNICODE),
'response' => $_response,
'useragent' => $request->server('HTTP_USER_AGENT'),
'create_time' => time(),
];
SystemLogService::instance()->save($data);
}
}
return $response;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace app\admin\model;
use app\common\model\TimeModel;
class BlackIp extends TimeModel
{
protected function getOptions(): array
{
return [
'deleteTime' => 'delete_time',
];
}
// * +++++++++++++++++++++++++++
// | 以下两种写法适用于 with 关联
// * +++++++++++++++++++++++++
// public function cate(): BelongsTo
// {
// return $this->belongsTo('app\admin\model\MallCate', 'cate_id', 'id');
// }
}

View File

@ -0,0 +1,18 @@
<?php
namespace app\admin\model;
use app\common\model\TimeModel;
class MallCate extends TimeModel
{
protected function getOptions(): array
{
return [
'deleteTime' => 'delete_time',
];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace app\admin\model;
use app\common\model\TimeModel;
use think\model\relation\BelongsTo;
use think\model\relation\HasOne;
class MallGoods extends TimeModel
{
protected function getOptions(): array
{
return [
'deleteTime' => 'delete_time',
];
}
// * +++++++++++++++++++++++++++
// | 以下两种写法适用于 with 关联
// * +++++++++++++++++++++++++
// public function cate(): BelongsTo
// {
// return $this->belongsTo('app\admin\model\MallCate', 'cate_id', 'id');
// }
public function cate(): HasOne
{
return $this->hasOne(MallCate::class, 'id', 'cate_id');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace app\admin\model;
use app\common\model\TimeModel;
use think\model\relation\HasOne;
class MallOrder extends TimeModel
{
protected function getOptions(): array
{
return [
'deleteTime' => 'delete_time',
];
}
// * +++++++++++++++++++++++++++
// | 以下两种写法适用于 with 关联
// * +++++++++++++++++++++++++
// public function cate(): BelongsTo
// {
// return $this->belongsTo('app\admin\model\MallCate', 'cate_id', 'id');
// }
}

View File

@ -0,0 +1,36 @@
<?php
namespace app\admin\model;
use app\common\model\TimeModel;
class SystemAdmin extends TimeModel
{
protected function getOptions(): array
{
return [
'deleteTime' => 'delete_time',
];
}
public array $notes = [
'login_type' => [
1 => '密码登录',
2 => '密码 + 谷歌验证码登录'
],
];
public static function getAuthIdsAttr($value): array
{
if (!$value) return [];
return explode(',', $value);
}
public static function getAuthList(): array
{
return SystemAuth::where('status', 1)->column('title', 'id');
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace app\admin\model;
use app\common\model\TimeModel;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
class SystemAuth extends TimeModel
{
protected function getOptions(): array
{
return [
'deleteTime' => 'delete_time',
];
}
/**
* 根据角色ID获取授权节点
* @param $authId
* @return array
* @throws DataNotFoundException
* @throws DbException
* @throws ModelNotFoundException
*/
public static function getAuthorizeNodeListByAdminId($authId): array
{
$checkNodeList = (new SystemAuthNode())
->where('auth_id', $authId)
->column('node_id');
$systemNode = new SystemNode();
$nodeList = $systemNode
->where('is_auth', 1)
->field('id,node,title,type,is_auth')
->select()
->toArray();
$newNodeList = [];
foreach ($nodeList as $vo) {
if ($vo['type'] == 1) {
$vo = array_merge($vo, ['field' => 'node', 'spread' => true]);
$vo['checked'] = false;
$vo['title'] = "{$vo['title']}{$vo['node']}";
$children = [];
foreach ($nodeList as $v) {
if ($v['type'] == 2 && strpos($v['node'], $vo['node'] . '/') !== false) {
$v = array_merge($v, ['field' => 'node', 'spread' => true]);
$v['checked'] = in_array($v['id'], $checkNodeList) ? true : false;
$v['title'] = "{$v['title']}{$v['node']}";
$children[] = $v;
}
}
!empty($children) && $vo['children'] = $children;
$newNodeList[] = $vo;
}
}
return $newNodeList;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace app\admin\model;
use app\common\model\TimeModel;
class SystemAuthNode extends TimeModel
{
}

View File

@ -0,0 +1,21 @@
<?php
namespace app\admin\model;
use app\common\model\TimeModel;
use think\Model;
class SystemConfig extends TimeModel
{
public static function onBeforeUpdate(Model $model): void
{
if ($model->getData('name') === 'upload_allow_ext') {
//去除 php
$model->value = implode(',',array_map(function ($ext) {
return trim(strtolower($ext), ' ');
}, array_filter(explode(',', $model->getData('value')), function ($ext) {
return strtolower(trim($ext)) !== 'php';
})));
}
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace app\admin\model;
use app\admin\service\SystemLogService;
use app\common\model\TimeModel;
use think\model\relation\BelongsTo;
class SystemLog extends TimeModel
{
protected array $type = [
'content' => 'json',
'response' => 'json',
];
protected function init(): void
{
SystemLogService::instance()->detectTable();
}
public function admin(): BelongsTo
{
return $this->belongsTo('app\admin\model\SystemAdmin', 'admin_id', 'id');
}
}

View File

@ -0,0 +1,132 @@
<?php
namespace app\admin\model;
use app\common\constants\MenuConstant;
use app\common\model\TimeModel;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use think\Model;
class SystemMenu extends TimeModel
{
public static array $menuTypeList = [
'system' => '系统管理',
'mall' => '商城管理',
'article' => '文章管理',
];
protected function getOptions(): array
{
return [
'deleteTime' => 'delete_time',
];
}
public static function onBeforeUpdate(Model $model): void
{
$model->system = 0; // 系统添加
}
/**
* @throws ModelNotFoundException
* @throws DbException
* @throws DataNotFoundException
*/
public static function getPidMenuList(): array
{
$list = self::field('id,pid,title')->where([
['pid', '<>', MenuConstant::HOME_PID],
['status', '=', 1],
])->select()->toArray();
$pidMenuList = self::buildPidMenu(0, $list);
return array_merge([[
'id' => 0,
'pid' => 0,
'title' => '顶级菜单',
]], $pidMenuList);
}
protected static function buildPidMenu($pid, $list, $level = 0): array
{
$newList = [];
foreach ($list as $vo) {
if ($vo['pid'] == $pid) {
$level++;
foreach ($newList as $v) {
if ($vo['pid'] == $v['pid'] && isset($v['level'])) {
$level = $v['level'];
break;
}
}
$vo['level'] = $level;
if ($level > 1) {
$repeatString = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
$markString = str_repeat("{$repeatString}{$repeatString}", $level - 1);
$vo['title'] = $markString . $vo['title'];
}
$newList[] = $vo;
$childList = self::buildPidMenu($vo['id'], $list, $level);
!empty($childList) && $newList = array_merge($newList, $childList);
}
}
return $newList;
}
public static function refreshMenu($nodeList): void
{
$nodeList = array_filter($nodeList, function ($item) {
return $item['type'] == 1;
});
$menuList = array_map(function ($item) {
return "{$item['node']}/index";
}, $nodeList);
if (!empty($menuList)) {
$hasMenu = (new self())->whereIn('href', $menuList)->column('href');
$needInsertMenu = array_diff($menuList, $hasMenu);
$insertNode = array_filter($nodeList, function ($item) use ($needInsertMenu) {
return in_array("{$item['node']}/index", $needInsertMenu);
});
$data = [];
foreach ($insertNode as $vo) {
$pidText = explode('.', $vo['node']);
if (isset($pidText[0]) && self::$menuTypeList[$pidText[0]]) {
$pidMenuId = (new self())->where([
'title' => self::$menuTypeList[$pidText[0]],
'pid' => 0,
])->value('id');
if (empty($pidMenuId)) {
$pidMenuId = (new self())->insertGetId([
'title' => self::$menuTypeList[$pidText[0]],
'href' => '',
'icon' => 'fa fa-list',
'pid' => 0,
'status' => 1,
'system' => 0, // 系统添加
'create_time' => time(),
]);
}
$data[] = [
'title' => $vo['title'],
'href' => "{$vo['node']}/index",
'icon' => 'fa fa-list',
'target' => '_self',
'pid' => $pidMenuId,
'status' => 1,
'system' => 1, // 系统添加
'create_time' => time(),
];
}
};
if (count($data) > 0) (new self())->insertAll($data);
self::getPidMenuList(); // 刷新菜单缓存
}
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace app\admin\model;
use app\common\model\TimeModel;
class SystemNode extends TimeModel
{
public static function getNodeTreeList(): array
{
$list = self::select()->toArray();
return self::buildNodeTree($list);
}
protected static function buildNodeTree($list): array
{
$newList = [];
$repeatString = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
foreach ($list as $vo) {
if ($vo['type'] == 1) {
$newList[] = $vo;
foreach ($list as $v) {
if ($v['type'] == 2 && str_contains($v['node'], $vo['node'] . '/')) {
$v['node'] = "{$repeatString}{$repeatString}" . $v['node'];
$newList[] = $v;
}
}
}
}
return $newList;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace app\admin\model;
use app\common\model\TimeModel;
class SystemQuick extends TimeModel
{
protected function getOptions(): array
{
return [
'deleteTime' => 'delete_time',
];
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace app\admin\model;
use app\common\model\TimeModel;
class SystemUploadfile extends TimeModel
{
}

View File

@ -0,0 +1,20 @@
<?php
namespace app\admin\service;
use think\facade\Cache;
class ConfigService
{
public static function getVersion()
{
$version = cache('site_version');
if (empty($version)) {
$version = sysConfig('site', 'site_version');
cache('site_version', $version);
}
return $version;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace app\admin\service;
use app\admin\service\auth\Node;
class NodeService
{
/**
* 获取节点服务
* @return array
* @throws \Doctrine\Common\Annotations\AnnotationException
* @throws \ReflectionException
*/
public function getNodeList()
{
$basePath = base_path() . 'admin' . DIRECTORY_SEPARATOR . 'controller';
$baseNamespace = "app\admin\controller";
$nodeList = (new Node($basePath, $baseNamespace))->getNodeList();
return $nodeList;
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace app\admin\service;
use think\facade\Cache;
use think\facade\Db;
use think\facade\Config;
use think\facade\Env;
/**
* 系统日志表
* Class SystemLogService
* @package app\admin\service
*/
class SystemLogService
{
protected static ?SystemLogService $instance = null;
/**
* 表前缀
* @var string
*/
protected string $tablePrefix;
/**
* 表后缀
* @var string
*/
protected string $tableSuffix;
/**
* 表名
* @var string
*/
protected string $tableName;
/**
* 构造方法
* SystemLogService constructor.
*/
protected function __construct()
{
$this->tablePrefix = Config::get('database.connections.mysql.prefix');
$this->tableSuffix = date('Ym', time());
$this->tableName = "{$this->tablePrefix}system_log_{$this->tableSuffix}";
}
/**
* 获取实例对象
* @return SystemLogService
*/
public static function instance(): SystemLogService
{
if (is_null(self::$instance)) {
self::$instance = new static();
}
return self::$instance;
}
/**
* 保存数据
* @param $data
* @return bool|string
*/
public function save($data): bool|string
{
Db::startTrans();
try {
$this->detectTable();
Db::table($this->tableName)->strict(false)->insert($data);
Db::commit();
}catch (\Exception $e) {
Db::rollback();
return $e->getMessage();
}
return true;
}
/**
* 检测数据表
* @return bool
*/
public function detectTable(): bool
{
$_key = "system_log_{$this->tableName}_table";
// 手动删除日志表时候 记得清除缓存
$isset = Cache::get($_key);
if ($isset) return true;
$check = Db::query("show tables like '{$this->tableName}'");
if (empty($check)) {
$sql = $this->getCreateSql();
Db::execute($sql);
}
Cache::set($_key, !empty($check));
return true;
}
public function getAllTableList()
{
}
/**
* 根据后缀获取创建表的sql
* @return string
*/
protected function getCreateSql(): string
{
return <<<EOT
CREATE TABLE `{$this->tableName}` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`admin_id` int(10) unsigned DEFAULT '0' COMMENT '管理员ID',
`url` varchar(1500) NOT NULL DEFAULT '' COMMENT '操作页面',
`method` varchar(50) NOT NULL COMMENT '请求方法',
`title` varchar(100) DEFAULT '' COMMENT '日志标题',
`content` json NOT NULL COMMENT '请求数据',
`response` json DEFAULT NULL COMMENT '回调数据',
`ip` varchar(50) NOT NULL DEFAULT '' COMMENT 'IP',
`useragent` varchar(255) DEFAULT '' COMMENT 'User-Agent',
`create_time` int(10) DEFAULT NULL COMMENT '操作时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='后台操作日志表 - {$this->tableSuffix}';
EOT;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace app\admin\service;
use think\facade\Cache;
class TriggerService
{
/**
* 更新菜单缓存
* @param null $adminId
* @return bool
*/
public static function updateMenu($adminId = null)
{
if(empty($adminId)){
Cache::tag('initAdmin')->clear();
}else{
Cache::delete('initAdmin_' . $adminId);
}
return true;
}
/**
* 更新节点缓存
* @param null $adminId
* @return bool
*/
public static function updateNode($adminId = null)
{
if(empty($adminId)){
Cache::tag('authNode')->clear();
}else{
Cache::delete('allAuthNode_' . $adminId);
}
return true;
}
/**
* 更新系统设置缓存
* @return bool
*/
public static function updateSysConfig(): bool
{
Cache::tag('sysConfig')->clear();
return true;
}
}

View File

@ -0,0 +1,226 @@
<?php
namespace app\admin\service;
use app\admin\model\SystemUploadfile;
use OSS\Core\OssException;
use OSS\Credentials\EnvironmentVariableCredentialsProvider;
use OSS\OssClient;
use think\facade\Env;
use think\file\UploadedFile;
use think\helper\Str;
use Qcloud\Cos\Client;
use Exception;
use Qiniu\Storage\UploadManager;
use Qiniu\Auth;
class UploadService
{
public static ?UploadService $_instance = null;
protected array $options = [];
private array $saveData;
public static function instance(): ?UploadService
{
if (!static::$_instance) static::$_instance = new static();
return static::$_instance;
}
/**
* @param array $options
* @return $this
*/
public function setConfig(array $options = []): UploadService
{
$this->options = $options;
return $this;
}
/**
* @return array
*/
public function getConfig(): array
{
return $this->options;
}
/**
* @param UploadedFile $file
* @param string $base_path
* @return string
*/
protected function setFilePath(UploadedFile $file, string $base_path = ''): string
{
$path = date('Ymd') . '/' . Str::random(3) . time() . Str::random() . '.' . $file->extension();
return $base_path . $path;
}
/**
* @param UploadedFile $file
* @return UploadService
*/
protected function setSaveData(UploadedFile $file): static
{
$options = $this->options;
$data = [
'upload_type' => $options['upload_type'],
'original_name' => $file->getOriginalName(),
'mime_type' => $file->getMime(),
'file_size' => $file->getSize(),
'file_ext' => strtolower($file->extension()),
'create_time' => time(),
];
$this->saveData = $data;
return $this;
}
/**
* 本地存储
*
* @param UploadedFile $file
* @param string $type
* @return array
*/
public function local(UploadedFile $file, string $type = ''): array
{
if ($file->isValid()) {
$base_path = '/storage/' . date('Ymd') . '/';
// 上传文件的目标文件夹
$destinationPath = public_path() . $base_path;
$this->setSaveData($file);
// 将文件移动到目标文件夹中
$move = $file->move($destinationPath, Str::random(3) . time() . Str::random() . session('admin.id') . '.' . $file->extension());
$url = $base_path . $move->getFilename();
$data = ['url' => $url];
$this->save($url);
return ['code' => 1, 'data' => $data];
}
$data = '上传失败';
return ['code' => 0, 'data' => $data];
}
/**
* 阿里云OSS
*
* @param UploadedFile $file
* @param string $type
* @return array
*/
public function oss(UploadedFile $file, string $type = ''): array
{
$config = $this->getConfig();
$accessKeyId = $config['oss_access_key_id'];
$accessKeySecret = $config['oss_access_key_secret'];
$endpoint = $config['oss_endpoint'];
$bucket = $config['oss_bucket'];
// 升级 aliyuncs/oss-sdk-php 到 v2.7.2 以上, 使用签名 v4 版本
putenv('OSS_ACCESS_KEY_ID=' . $accessKeyId);
putenv('OSS_ACCESS_KEY_SECRET=' . $accessKeySecret);
$region = str_replace(['http://oss-', 'https://oss-', 'oss-'], '', explode('.aliyuncs.com', $endpoint)[0] ?? '');
$provider = new EnvironmentVariableCredentialsProvider();
$args = [
"provider" => $provider,
"endpoint" => $endpoint,
"signatureVersion" => OssClient::OSS_SIGNATURE_VERSION_V4,
"region" => $region
];
if ($file->isValid()) {
$object = $this->setFilePath($file, Env::get('EASYADMIN.OSS_STATIC_PREFIX', 'easyadmin8') . '/');
try {
$ossClient = new OssClient($args);
$_rs = $ossClient->putObject($bucket, $object, file_get_contents($file->getRealPath()));
$oss_request_url = $_rs['oss-request-url'] ?? '';
if (empty($oss_request_url)) return ['code' => 0, 'data' => '上传至OSS失败'];
$oss_request_url = str_replace('http://', 'https://', $oss_request_url);
$this->setSaveData($file);
} catch (OssException $e) {
return ['code' => 0, 'data' => $e->getMessage()];
}
$data = ['url' => $oss_request_url];
$this->save($oss_request_url);
return ['code' => 1, 'data' => $data];
}
$data = '上传失败';
return ['code' => 0, 'data' => $data];
}
/**
* 腾讯云cos
*
* @param UploadedFile $file
* @param string $type
* @return array
*/
public function cos(UploadedFile $file, string $type = ''): array
{
$config = $this->getConfig();
$secretId = $config['cos_secret_id']; //替换为用户的 secretId请登录访问管理控制台进行查看和管理https://console.cloud.tencent.com/cam/capi
$secretKey = $config['cos_secret_key']; //替换为用户的 secretKey请登录访问管理控制台进行查看和管理https://console.cloud.tencent.com/cam/capi
$region = $config['cos_region']; //替换为用户的 region已创建桶归属的region可以在控制台查看https://console.cloud.tencent.com/cos5/bucket
if ($file->isValid()) {
$cosClient = new Client(
[
'region' => $region,
'schema' => 'http',
'credentials' => ['secretId' => $secretId, 'secretKey' => $secretKey,
],
]);
try {
$object = $this->setFilePath($file, Env::get('EASYADMIN.OSS_STATIC_PREFIX', 'easyadmin8') . '/');
$result = $cosClient->upload(
$config['cos_bucket'], //存储桶名称由BucketName-Appid 组成可以在COS控制台查看 https://console.cloud.tencent.com/cos5/bucket
$object, //此处的 key 为对象键
file_get_contents($file->getRealPath())
);
$location = $result['Location'] ?? '';
if (empty($location)) return ['code' => 0, 'data' => '上传至COS失败'];
$location = 'https://' . $location;
$this->setSaveData($file);
}catch (Exception $e) {
return ['code' => 0, 'data' => $e->getMessage()];
}
$data = ['url' => $location];
$this->save($location);
return ['code' => 1, 'data' => $data];
}
$data = '上传失败';
return ['code' => 0, 'data' => $data];
}
/**
* 七牛云
*
* @param UploadedFile $file
* @param string $type
* @return array
* @throws Exception
*/
public function qnoss(UploadedFile $file, string $type = ''): array
{
if (!$file->isValid()) return ['code' => 1, 'data' => '上传验证失败'];
$uploadMgr = new UploadManager();
$config = $this->getConfig();
$accessKey = $config['qnoss_access_key'];
$secretKey = $config['qnoss_secret_key'];
$bucket = $config['qnoss_bucket'];
$domain = $config['qnoss_domain'];
$auth = new Auth($accessKey, $secretKey);
$token = $auth->uploadToken($bucket);
$object = $this->setFilePath($file, Env::get('EASYADMIN.OSS_STATIC_PREFIX', 'easyadmin8') . '/');
list($ret, $error) = $uploadMgr->putFile($token, $object, $file->getRealPath());
if (empty($ret)) return ['code' => 0, 'data' => $error->getResponse()->error ?? '上传失败,请检查七牛云相关参数配置'];
$url = $domain . "/" . $ret['key'];
$data = ['url' => $url];
$this->setSaveData($file);
$this->save($url);
return ['code' => 1, 'data' => $data];
}
protected function save(string $url = ''): bool
{
$data = $this->saveData;
$data['url'] = $url;
$data['upload_time'] = time();
return (new SystemUploadfile())->save($data);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace app\admin\service\annotation;
use Attribute;
/**
* controller 节点注解类
*/
#[Attribute]
final class ControllerAnnotation
{
/**
* @param string $title
* @param bool $auth 是否需要权限
* @param string|array $ignore
*/
public function __construct(public string $title = '', public bool $auth = true, public string|array $ignore = '')
{
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace app\admin\service\annotation;
use Attribute;
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
final class MiddlewareAnnotation
{
/** 过滤日志 */
const IGNORE_LOG = 'LOG';
/** 免登录 */
const IGNORE_LOGIN = 'LOGIN';
public function __construct(public string $type = '', public string|array $ignore = '')
{
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace app\admin\service\annotation;
use Attribute;
/**
* action 节点注解类
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD| Attribute::TARGET_PROPERTY)]
final class NodeAnnotation
{
/** 过滤节点 */
const IGNORE_NODE = 'NODE';
/**
* @param string $title
* @param bool $auth 是否需要权限
* @param string|array $ignore
*/
public function __construct(public string $title = '', public bool $auth = true, public string|array $ignore = '')
{
}
}

View File

@ -0,0 +1,155 @@
<?php
namespace app\admin\service\auth;
use Doctrine\Common\Annotations\AnnotationException;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Annotations\DocParser;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use app\admin\service\tool\CommonTool;
use ReflectionException;
/**
* 节点处理类
* Class Node
* @package EasyAdmin\auth
*/
class Node
{
/**
* @var string 当前文件夹
*/
protected string $basePath;
/**
* @var string 命名空间前缀
*/
protected string $baseNamespace;
/**
* 构造方法
* Node constructor.
* @param string $basePath 读取的文件夹
* @param string $baseNamespace 读取的命名空间前缀
*/
public function __construct(string $basePath, string $baseNamespace)
{
$this->basePath = $basePath;
$this->baseNamespace = $baseNamespace;
return $this;
}
/**
* 获取所有节点
* @return array
* @throws AnnotationException
* @throws ReflectionException
*/
public function getNodeList(): array
{
list($nodeList, $controllerList) = [[], $this->getControllerList()];
if (!empty($controllerList)) {
AnnotationRegistry::loadAnnotationClass('class_exists');
$parser = new DocParser();
$parser->setIgnoreNotImportedAnnotations(true);
$reader = new AnnotationReader($parser);
foreach ($controllerList as $controllerFormat => $controller) {
// 获取类和方法的注释信息
$reflectionClass = new \ReflectionClass($controller);
$methods = $reflectionClass->getMethods();
$actionList = [];
// 遍历读取所有方法的注释的参数信息
foreach ($methods as $method) {
// 忽略掉不需要的节点
$property = $reflectionClass->getProperty('ignoreNode');
$propertyAttributes = $property->getAttributes(NodeAnnotation::class);
if (!empty($propertyAttributes[0])) {
$propertyAttribute = $propertyAttributes[0]->newInstance();
if (in_array($method->name, $propertyAttribute->ignore)) continue;
}
$attributes = $reflectionClass->getMethod($method->name)->getAttributes(NodeAnnotation::class);
foreach ($attributes as $attribute) {
$annotation = $attribute->newInstance();
if (!empty($annotation->ignore)) if (strtolower($annotation->ignore) == 'node') continue;
$actionList[] = [
'node' => $controllerFormat . '/' . $method->name,
'title' => $annotation->title ?? null,
'is_auth' => $annotation->auth ?? false,
'type' => 2,
];
}
}
// 方法非空才读取控制器注解
if (!empty($actionList)) {
// 读取Controller的注解
$attributes = $reflectionClass->getAttributes(ControllerAnnotation::class);
foreach ($attributes as $attribute) {
$controllerAnnotation = $attribute->newInstance();
$nodeList[] = [
'node' => $controllerFormat,
'title' => $controllerAnnotation->title ?? null,
'is_auth' => $controllerAnnotation->auth ?? false,
'type' => 1,
];
}
$nodeList = array_merge($nodeList, $actionList);
}
}
}
return $nodeList;
}
/**
* 获取所有控制器
* @return array
*/
public function getControllerList(): array
{
return $this->readControllerFiles($this->basePath);
}
/**
* 遍历读取控制器文件
* @param $path
* @return array
*/
protected function readControllerFiles($path): array
{
list($list, $temp_list, $dirExplode) = [[], scandir($path), explode($this->basePath, $path)];
$middleDir = !empty($dirExplode[1]) ? str_replace('/', '\\', substr($dirExplode[1], 1)) . "\\" : '';
foreach ($temp_list as $file) {
// 排除根目录和没有开启注解的模块
if ($file == ".." || $file == ".") {
continue;
}
if (is_dir($path . DIRECTORY_SEPARATOR . $file)) {
// 子文件夹,进行递归
$childFiles = $this->readControllerFiles($path . DIRECTORY_SEPARATOR . $file);
$list = array_merge($childFiles, $list);
}else {
// 判断是不是控制器
$fileExplodeArray = explode('.', $file);
if (count($fileExplodeArray) != 2 || end($fileExplodeArray) != 'php') {
continue;
}
// 根目录下的文件
$className = str_replace('.php', '', $file);
$controllerFormat = str_replace('\\', '.', $middleDir) . CommonTool::humpToLine(lcfirst($className));
$list[$controllerFormat] = "{$this->baseNamespace}\\{$middleDir}" . $className;
}
}
return $list;
}
}

View File

@ -0,0 +1,166 @@
<?php
namespace app\admin\service\console;
class CliEcho
{
private array $foreground_colors = [];
private array $background_colors = [];
private static array $foregroundColors = [
'black' => '0;30',
'dark_gray' => '1;30',
'blue' => '0;34',
'light_blue' => '1;34',
'green' => '0;32',
'light_green' => '1;32',
'cyan' => '0;36',
'light_cyan' => '1;36',
'red' => '0;31',
'light_red' => '1;31',
'purple' => '0;35',
'light_purple' => '1;35',
'brown' => '0;33',
'yellow' => '1;33',
'light_gray' => '0;37',
'white' => '1;37',
];
private static $backgroundColors = [
'black' => '40',
'red' => '41',
'green' => '42',
'yellow' => '43',
'blue' => '44',
'magenta' => '45',
'cyan' => '46',
'light_gray' => '47',
];
public function __construct()
{
// Set up shell colors
$this->foreground_colors['black'] = '0;30';
$this->foreground_colors['dark_gray'] = '1;30';
$this->foreground_colors['blue'] = '0;34';
$this->foreground_colors['light_blue'] = '1;34';
$this->foreground_colors['green'] = '0;32';
$this->foreground_colors['light_green'] = '1;32';
$this->foreground_colors['cyan'] = '0;36';
$this->foreground_colors['light_cyan'] = '1;36';
$this->foreground_colors['red'] = '0;31';
$this->foreground_colors['light_red'] = '1;31';
$this->foreground_colors['purple'] = '0;35';
$this->foreground_colors['light_purple'] = '1;35';
$this->foreground_colors['brown'] = '0;33';
$this->foreground_colors['yellow'] = '1;33';
$this->foreground_colors['light_gray'] = '0;37';
$this->foreground_colors['white'] = '1;37';
$this->background_colors['black'] = '40';
$this->background_colors['red'] = '41';
$this->background_colors['green'] = '42';
$this->background_colors['yellow'] = '43';
$this->background_colors['blue'] = '44';
$this->background_colors['magenta'] = '45';
$this->background_colors['cyan'] = '46';
$this->background_colors['light_gray'] = '47';
}
// Returns colored string
public function getColoredString($string, $foreground_color = null, $background_color = null, $new_line = false): string
{
$colored_string = '';
// Check if given foreground color found
if (isset($this->foreground_colors[$foreground_color])) {
$colored_string .= "\033[" . $this->foreground_colors[$foreground_color] . 'm';
}
// Check if given background color found
if (isset($this->background_colors[$background_color])) {
$colored_string .= "\033[" . $this->background_colors[$background_color] . 'm';
}
// Add string and end coloring
$colored_string .= $string . "\033[0m";
return $new_line ? $colored_string . PHP_EOL : $colored_string;
}
// Returns all foreground color names
public function getForegroundColors(): array
{
return array_keys($this->foreground_colors);
}
// Returns all background color names
public function getBackgroundColors(): array
{
return array_keys($this->background_colors);
}
/**
* 获取带颜色的文字.
*
* @param string $string black|dark_gray|blue|light_blue|green|light_green|cyan|light_cyan|red|light_red|purple|brown|yellow|light_gray|white
* @param string|null $foregroundColor 前景颜色 black|red|green|yellow|blue|magenta|cyan|light_gray
* @param string|null $backgroundColor 背景颜色 同$foregroundColor
*
* @return string
*/
public static function initColoredString(
string $string,
?string $foregroundColor = null,
?string $backgroundColor = null
): string
{
$coloredString = '';
if (isset(static::$foregroundColors[$foregroundColor])) {
$coloredString .= "\033[" . static::$foregroundColors[$foregroundColor] . 'm';
}
if (isset(static::$backgroundColors[$backgroundColor])) {
$coloredString .= "\033[" . static::$backgroundColors[$backgroundColor] . 'm';
}
$coloredString .= $string . "\033[0m";
return $coloredString;
}
/**
* 输出提示信息.
*
* @param $msg
*/
public static function notice($msg): void
{
fwrite(STDOUT, self::initColoredString($msg, 'light_gray') . PHP_EOL);
}
/**
* 输出错误信息.
*
* @param $msg
*/
public static function error($msg): void
{
fwrite(STDERR, self::initColoredString($msg, 'white', 'red') . PHP_EOL);
}
/**
* 输出警告信息.
*
* @param $msg
*/
public static function warn($msg): void
{
fwrite(STDOUT, self::initColoredString($msg, 'red', 'yellow') . PHP_EOL);
}
/**
* 输出成功信息.
*
* @param $msg
*/
public static function success($msg): void
{
fwrite(STDOUT, self::initColoredString($msg, 'light_cyan') . PHP_EOL);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace app\admin\service\curd\exceptions;
class CurdException extends \Exception
{
}

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace app\admin\service\curd\exceptions;
class FileException extends \Exception
{
}

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace app\admin\service\curd\exceptions;
class TableException extends \Exception
{
}

View File

@ -0,0 +1,28 @@
<?php
namespace {{controllerNamespace}};
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnnotation;
use think\App;
#[ControllerAnnotation(title: '{{controllerAnnotation}}')]
class {{controllerName}} extends AdminController
{
private array $notes;
public function __construct(App $app)
{
parent::__construct($app);
self::$model = new {{modelFilename}}();
$notes = self::$model::$notes;
{{constructRelation}}
$this->notes =$notes;
$this->assign(compact('notes'));
}
{{indexMethod}}
}

View File

@ -0,0 +1,21 @@
#[NodeAnnotation(title: '列表', auth: true)]
public function index(\app\Request $request): \think\response\Json|string
{
if ($request->isAjax()) {
if (input('selectFields')) {
return $this->selectList();
}
list($page, $limit, $where) = $this->buildTableParams();
$count = self::$model::where($where)->{{relationIndexMethod}}->count();
$list = self::$model::where($where)->{{relationIndexMethod}}->page($page, $limit)->order($this->sort)->select()->toArray();
$data = [
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $list,
];
return json($data);
}
return $this->fetch();
}

View File

@ -0,0 +1,2 @@
$this->assign('{{name}}', $this->model->{{name}}());

View File

@ -0,0 +1,23 @@
<?php
namespace {{modelNamespace}};
use app\common\model\TimeModel;
class {{modelName}} extends TimeModel
{
protected function getOptions(): array
{
return [
'name' => "{{table}}",
'table' => "{{prefix_table}}",
'deleteTime' => {{deleteTime}},
];
}
public static array $notes = {{selectArrays}};
{{relationList}}
}

View File

@ -0,0 +1,5 @@
public function {{relationMethod}}()
{
return $this->belongsTo({{relationModel}}, '{{foreignKey}}', '{{primaryKey}}'){{relationFields}};
}

View File

@ -0,0 +1,5 @@
public function {{name}}()
{
return \app\admin\model\{{relation}}::column('{{values}}', 'id');
}

View File

@ -0,0 +1,5 @@
public function {{name}}()
{
return {{values}};
}

View File

@ -0,0 +1,91 @@
define(["jquery", "easy-admin"], function ($, ea) {
var init = {
table_elem: '#currentTable',
table_render_id: 'currentTableRenderId',
index_url: '{{controllerUrl}}/index',
add_url: '{{controllerUrl}}/add',
edit_url: '{{controllerUrl}}/edit',
delete_url: '{{controllerUrl}}/delete',
export_url: '{{controllerUrl}}/export',
modify_url: '{{controllerUrl}}/modify',
recycle_url: '{{controllerUrl}}/recycle',
};
return {
index: function () {
ea.table.render({
init: init,
cols: [[
{{indexCols}}
]],
});
ea.listen();
},
add: function () {
ea.listen();
},
edit: function () {
ea.listen();
},
recycle: function () {
init.index_url = init.recycle_url;
ea.table.render({
init: init,
toolbar: ['refresh',
[{
class: 'layui-btn layui-btn-sm',
method: 'get',
field: 'id',
icon: 'fa fa-refresh',
text: '全部恢复',
title: '确定恢复?',
auth: 'recycle',
url: init.recycle_url + '?type=restore',
checkbox: true
}, {
class: 'layui-btn layui-btn-danger layui-btn-sm',
method: 'get',
field: 'id',
icon: 'fa fa-delete',
text: '彻底删除',
title: '确定彻底删除?',
auth: 'recycle',
url: init.recycle_url + '?type=delete',
checkbox: true
}], 'export',
],
cols: [[
{{recycleCols}}
{
width: 250,
title: '操作',
templet: ea.table.tool,
operat: [
[{
title: '确认恢复?',
text: '恢复数据',
filed: 'id',
url: init.recycle_url + '?type=restore',
method: 'get',
auth: 'recycle',
class: 'layui-btn layui-btn-xs layui-btn-success',
}, {
title: '想好了吗?',
text: '彻底删除',
filed: 'id',
method: 'get',
url: init.recycle_url + '?type=delete',
auth: 'recycle',
class: 'layui-btn layui-btn-xs layui-btn-normal layui-bg-red',
}]]
}
]],
});
ea.listen();
},
};
});

View File

@ -0,0 +1,10 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
{{formList}}
<div class="hr-line"></div>
<div class="layui-form-item text-center">
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@ -0,0 +1,16 @@
<div class="layuimini-container">
<div class="layuimini-main">
<table id="currentTable" class="layui-table layui-hide"
data-auth-add="{:auth('{{controllerUrl}}/add')}"
data-auth-edit="{:auth('{{controllerUrl}}/edit')}"
data-auth-delete="{:auth('{{controllerUrl}}/delete')}"
data-auth-recycle="{:auth('{{controllerUrl}}/recycle')}"
lay-filter="currentTable">
<!-- searchTableShow="false" 隐藏搜索框 -->
</table>
</div>
</div>
<script>
{{notesScript}}
</script>

View File

@ -0,0 +1,7 @@
<div class="layui-form-item">
<label class="layui-form-label">{{comment}}</label>
<div class="layui-input-block">
{{define}}
</div>
</div>

View File

@ -0,0 +1,3 @@
{foreach ${{name}} as $k=>$v}
<input type="checkbox" name="{{field}}[]" value="{$k}" lay-skin="primary" title="{$v}" {{select}}>
{/foreach}

View File

@ -0,0 +1,7 @@
<div class="layui-form-item">
<label class="layui-form-label">{{comment}}</label>
<div class="layui-input-block">
<input type="text" name="{{field}}" data-date="" data-date-type="{{define}}" class="layui-input" {{required}} placeholder="请输入{{comment}}" value="{{value}}">
</div>
</div>

View File

@ -0,0 +1,8 @@
<div class="layui-form-item">
<label class="layui-form-label">{{comment}}</label>
<div class="layui-input-block">
{:editor_textarea({{value}},"{{field}}","{{comment}}")}
</div>
</div>

View File

@ -0,0 +1,11 @@
<div class="layui-form-item">
<label class="layui-form-label required">{{comment}}</label>
<div class="layui-input-block layuimini-upload">
<input name="{{field}}" class="layui-input layui-col-xs6" {{required}} placeholder="请上传{{comment}}" value="{{value}}">
<div class="layuimini-upload-btn">
<span><a class="layui-btn" data-upload="{{field}}" data-upload-number="one" data-upload-exts="*" data-upload-icon="file"><i class="fa fa-upload"></i> 上传</a></span>
<span><a class="layui-btn layui-btn-normal" id="select_{{field}}" data-upload-select="{{field}}" data-upload-number="one" data-upload-mimetype="*"><i class="fa fa-list"></i> 选择</a></span>
</div>
</div>
</div>

View File

@ -0,0 +1,11 @@
<div class="layui-form-item">
<label class="layui-form-label required">{{comment}}</label>
<div class="layui-input-block layuimini-upload">
<input name="{{field}}" class="layui-input layui-col-xs6" {{required}} placeholder="请上传{{comment}}" value="{{value}}">
<div class="layuimini-upload-btn">
<span><a class="layui-btn" data-upload="{{field}}" data-upload-number="more" data-upload-exts="*" data-upload-icon="file"><i class="fa fa-upload" data-upload-sign="{{define}}"></i> 上传</a></span>
<span><a class="layui-btn layui-btn-normal" id="select_{{field}}" data-upload-select="{{field}}" data-upload-number="more" data-upload-mimetype="*" data-upload-sign="{{define}}"><i class="fa fa-list"></i> 选择</a></span>
</div>
</div>
</div>

View File

@ -0,0 +1,11 @@
<div class="layui-form-item">
<label class="layui-form-label required">{{comment}}</label>
<div class="layui-input-block layuimini-upload">
<input name="{{field}}" class="layui-input layui-col-xs6" {{required}} placeholder="请上传{{comment}}" value="{{value}}">
<div class="layuimini-upload-btn">
<span><a class="layui-btn" data-upload="{{field}}" data-upload-number="one" data-upload-exts="png|jpg|ico|jpeg" data-upload-icon="image"><i class="fa fa-upload"></i> 上传</a></span>
<span><a class="layui-btn layui-btn-normal" id="select_{{field}}" data-upload-select="{{field}}" data-upload-number="one" data-upload-mimetype="image/*"><i class="fa fa-list"></i> 选择</a></span>
</div>
</div>
</div>

View File

@ -0,0 +1,11 @@
<div class="layui-form-item">
<label class="layui-form-label required">{{comment}}</label>
<div class="layui-input-block layuimini-upload">
<input name="{{field}}" class="layui-input layui-col-xs6" {{required}} placeholder="请上传{{comment}}" value="{{value}}">
<div class="layuimini-upload-btn">
<span><a class="layui-btn" data-upload="{{field}}" data-upload-number="more" data-upload-exts="png|jpg|ico|jpeg" data-upload-icon="image" data-upload-sign="{{define}}"><i class="fa fa-upload"></i> 上传</a></span>
<span><a class="layui-btn layui-btn-normal" id="select_{{field}}" data-upload-select="{{field}}" data-upload-number="more" data-upload-mimetype="image/*" data-upload-sign="{{define}}"><i class="fa fa-list"></i> 选择</a></span>
</div>
</div>
</div>

View File

@ -0,0 +1,7 @@
<div class="layui-form-item">
<label class="layui-form-label">{{comment}}</label>
<div class="layui-input-block">
<input type="text" name="{{field}}" class="layui-input" {{required}} placeholder="请输入{{comment}}" value="{{value}}">
</div>
</div>

View File

@ -0,0 +1,4 @@
<option value=''></option>
{foreach ${{name}} as $k=>$v}
<option value='{$k}' {{select}}>{$v}</option>
{/foreach}

View File

@ -0,0 +1,7 @@
<div class="layui-form-item">
<label class="layui-form-label">{{comment}}</label>
<div class="layui-input-block">
{{define}}
</div>
</div>

View File

@ -0,0 +1,3 @@
{foreach ${{name}} as $k=>$v}
<input type="radio" name="{{field}}" value="{$k}" title="{$v}" {{select}}>
{/foreach}

View File

@ -0,0 +1,9 @@
<div class="layui-form-item">
<label class="layui-form-label">{{comment}}</label>
<div class="layui-input-block">
<select name="{{field}}" {{required}}>
{{define}}
</select>
</div>
</div>

View File

@ -0,0 +1,7 @@
<div class="layui-form-item">
<label class="layui-form-label">{{comment}}</label>
<div class="layui-input-block">
<input type="number" name="{{field}}" class="layui-input" lay-affix="number" {{required}} placeholder="请输入{{comment}}" value="{{value}}">
</div>
</div>

View File

@ -0,0 +1,7 @@
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">{{comment}}</label>
<div class="layui-input-block">
<textarea name="{{field}}" class="layui-textarea" {{required}} placeholder="请输入{{comment}}">{{value}}</textarea>
</div>
</div>

View File

@ -0,0 +1,13 @@
<div class="layuimini-container">
<div class="layuimini-main">
<table id="currentTable" class="layui-table layui-hide"
data-auth-recycle="{:auth('{{controllerUrl}}/recycle')}"
lay-filter="currentTable">
<!-- searchTableShow="false" 隐藏搜索框 -->
</table>
</div>
</div>
<script>
{{notesScript}}
</script>

View File

@ -0,0 +1,108 @@
<?php
namespace app\admin\service\tool;
class CommonTool
{
/**
* 下划线转驼峰
* @param $str
* @return null|string|string[]
*/
public static function lineToHump($str)
{
$str = preg_replace_callback('/([-_]+([a-z]{1}))/i', function ($matches) {
return strtoupper($matches[2]);
}, $str);
return $str;
}
/**
* 驼峰转下划线
* @param $str
* @return null|string|string[]
*/
public static function humpToLine($str)
{
$str = preg_replace_callback('/([A-Z]{1})/', function ($matches) {
return '_' . strtolower($matches[0]);
}, $str);
return $str;
}
/**
* 获取真实IP
* @return mixed
*/
public static function getRealIp()
{
$ip = $_SERVER['REMOTE_ADDR'];
if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
foreach ($matches[0] as $xip) {
if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
$ip = $xip;
break;
}
}
}elseif (isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
}elseif (isset($_SERVER['HTTP_CF_CONNECTING_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CF_CONNECTING_IP'])) {
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
}elseif (isset($_SERVER['HTTP_X_REAL_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_X_REAL_IP'])) {
$ip = $_SERVER['HTTP_X_REAL_IP'];
}
return $ip;
}
/**
* 读取文件夹下的所有文件
* @param $path
* @param $basePath
* @return array|mixed
*/
public static function readDirAllFiles($path, $basePath = '')
{
list($list, $temp_list) = [[], scandir($path)];
empty($basePath) && $basePath = $path;
foreach ($temp_list as $file) {
if ($file != ".." && $file != ".") {
if (is_dir($path . DIRECTORY_SEPARATOR . $file)) {
$childFiles = self::readDirAllFiles($path . DIRECTORY_SEPARATOR . $file, $basePath);
$list = array_merge($childFiles, $list);
}else {
$filePath = $path . DIRECTORY_SEPARATOR . $file;
$fileName = str_replace($basePath . DIRECTORY_SEPARATOR, '', $filePath);
$list[$fileName] = $filePath;
}
}
}
return $list;
}
/**
* 模板值替换
* @param $string
* @param $array
* @return mixed
*/
public static function replaceTemplate($string, $array)
{
foreach ($array as $key => $val) {
if (is_null($val)) $val = '';
$string = str_replace("{{" . $key . "}}", $val, $string);
}
return $string;
}
public static function replaceArrayString(?string $arrayString): string
{
$arrayString = str_replace('array (', '[', $arrayString);
$arrayString = str_replace(')', ']', $arrayString);
$arrayString = str_replace('=>
[', '=> [', $arrayString);
return $arrayString;
}
}

203
app/admin/traits/Curd.php Normal file
View File

@ -0,0 +1,203 @@
<?php
namespace app\admin\traits;
use app\admin\service\annotation\NodeAnnotation;
use app\admin\service\tool\CommonTool;
use app\Request;
use think\db\exception\PDOException;
use think\facade\Db;
use think\response\Json;
/**
* 后台CURD复用
* Trait Curd
* @package app\admin\traits
*/
trait Curd
{
#[NodeAnnotation(title: '列表', auth: true)]
public function index(Request $request): Json|string
{
if ($request->isAjax()) {
if (input('selectFields')) {
return $this->selectList();
}
list($page, $limit, $where) = $this->buildTableParams();
$count = self::$model::where($where)->count();
$list = self::$model::where($where)->page($page, $limit)->order($this->sort)->select()->toArray();
$data = [
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $list,
];
return json($data);
}
return $this->fetch();
}
#[NodeAnnotation(title: '添加', auth: true)]
public function add(Request $request): string
{
if ($request->isPost()) {
$post = $request->post();
$rule = [];
$this->validate($post, $rule);
try {
Db::transaction(function() use ($post, &$save) {
$save = self::$model::create($post);
});
}catch (\Exception $e) {
$this->error('新增失败:' . $e->getMessage());
}
$save ? $this->success('新增成功') : $this->error('新增失败');
}
return $this->fetch();
}
#[NodeAnnotation(title: '编辑', auth: true)]
public function edit(Request $request, $id = 0): string
{
$row = self::$model::find($id);
empty($row) && $this->error('数据不存在');
if ($request->isPost()) {
$post = $request->post();
$rule = [];
$this->validate($post, $rule);
try {
Db::transaction(function() use ($post, $row, &$save) {
$save = $row->save($post);
});
}catch (\Exception $e) {
$this->error('保存失败');
}
$save ? $this->success('保存成功') : $this->error('保存失败');
}
$this->assign('row', $row);
return $this->fetch();
}
#[NodeAnnotation(title: '删除', auth: true)]
public function delete(Request $request): void
{
// 如果不是id作为主键 请在对应的控制器中覆盖重写
$id = $request->param('id', []);
$this->checkPostRequest();
$row = self::$model::whereIn('id', $id)->select();
$row->isEmpty() && $this->error('数据不存在');
try {
$save = $row->delete();
}catch (\Exception $e) {
$this->error('删除失败');
}
$save ? $this->success('删除成功') : $this->error('删除失败');
}
#[NodeAnnotation(title: '导出', auth: true)]
public function export()
{
if (env('EASYADMIN.IS_DEMO', false)) {
$this->error('演示环境下不允许操作');
}
list($page, $limit, $where) = $this->buildTableParams();
$tableName = (new self::$model)->getName();
$tableName = CommonTool::humpToLine(lcfirst($tableName));
$prefix = config('database.connections.mysql.prefix');
$dbList = Db::query("show full columns from {$prefix}{$tableName}");
$header = [];
foreach ($dbList as $vo) {
$comment = !empty($vo['Comment']) ? $vo['Comment'] : $vo['Field'];
if (!in_array($vo['Field'], $this->noExportFields)) {
$header[] = [$comment, $vo['Field']];
}
}
$list = self::$model::where($where)
->limit(100000)
->order($this->sort)
->select()
->toArray();
try {
exportExcel($header, $list);
}catch (\Throwable $exception) {
$this->error('导出失败: ' . $exception->getMessage() . PHP_EOL . $exception->getFile() . PHP_EOL . $exception->getLine());
}
}
#[NodeAnnotation(title: '属性修改', auth: true)]
public function modify(Request $request): void
{
$this->checkPostRequest();
$post = $request->post();
$rule = [
'id|ID' => 'require',
'field|字段' => 'require',
'value|值' => 'require',
];
$this->validate($post, $rule);
$row = self::$model::find($post['id']);
if (!$row) {
$this->error('数据不存在');
}
if (!in_array($post['field'], $this->allowModifyFields)) {
$this->error('该字段不允许修改:' . $post['field']);
}
try {
Db::transaction(function() use ($post, $row) {
$row->save([
$post['field'] => $post['value'],
]);
});
}catch (\Exception $e) {
$this->error($e->getMessage());
}
$this->success('保存成功');
}
#[NodeAnnotation(title: '回收站', auth: true)]
public function recycle(Request $request): Json|string
{
if (!$request->isAjax()) {
return $this->fetch();
}
$id = $request->param('id', []);
$type = $request->param('type', '');
$deleteTimeField = (new self::$model)->getOption('deleteTime'); // 获取软删除字段
$defaultErrorMsg = 'Model 中未设置软删除 deleteTime 对应字段 或 数据表中不存在该字段';
if (!$deleteTimeField) $this->success($defaultErrorMsg);
switch ($type) {
case 'restore':
self::$model::withTrashed()->whereIn('id', $id)->strict(false)->update([$deleteTimeField => null, 'update_time' => time()]);
$this->success('success');
break;
case 'delete':
self::$model::destroy($id, true);
$this->success('success');
break;
default:
list($page, $limit, $where) = $this->buildTableParams();
try {
$count = self::$model::withTrashed()->where($where)->whereNotNull($deleteTimeField)->count();
$list = self::$model::withTrashed()->where($where)->page($page, $limit)->order($this->sort)->whereNotNull($deleteTimeField)->select()->toArray();
$data = [
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $list,
];
} catch (\Throwable $e) {
$error = $e->getMessage();
if ($e instanceof PDOException) $error .= '<br>' . $defaultErrorMsg;
$data = [
'code' => -1,
'msg' => $error,
'count' => 0,
'data' => [],
];
}
return json($data);
}
}
}

View File

@ -0,0 +1,73 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">文章标题</label>
<div class="layui-input-block">
<input type="text" name="title" class="layui-input" lay-verify="required" placeholder="请输入文章标题" value="">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">商品分类</label>
<div class="layui-input-block">
<select name="cate_id" lay-verify="required" data-select="{:url('article.cate/index')}" data-fields="id,title" data-value="{$row.cate_id|default=''}">
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">封面图片</label>
<div class="layui-input-block layuimini-upload">
<input name="cover" class="layui-input layui-col-xs6" placeholder="请上传封面图片" value="">
<div class="layuimini-upload-btn">
<span><a class="layui-btn" data-upload="cover" data-upload-number="one" data-upload-exts="png|jpg|ico|jpeg" data-upload-icon="image" data-upload-mimetype="image/*"><i class="fa fa-upload"></i> 上传</a></span>
<span><a class="layui-btn layui-btn-normal" id="select_cover" data-upload-select="cover" data-upload-number="one"><i class="fa fa-list"></i> 选择</a></span>
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">文章简介</label>
<div class="layui-input-block">
<textarea name="summary" class="layui-textarea" placeholder="请输入文章简介"></textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">文章内容</label>
<div class="layui-input-block">
<textarea name="content" class="layui-textarea" placeholder="请输入文章内容" style="height: 200px;"></textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">作者</label>
<div class="layui-input-block">
<input type="text" name="author" class="layui-input" placeholder="请输入作者" value="">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">排序</label>
<div class="layui-input-block">
<input type="number" name="sort" class="layui-input" lay-affix="number" placeholder="请输入排序" value="0">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="status" value="1" title="启用" checked>
<input type="radio" name="status" value="0" title="禁用">
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@ -0,0 +1,71 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">文章标题</label>
<div class="layui-input-block">
<input type="text" name="title" class="layui-input" lay-verify="required" placeholder="请输入文章标题" value="{$row.title|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">商品分类</label>
<div class="layui-input-block">
<select name="cate_id" lay-verify="required" data-select="{:url('article.cate/index')}" data-fields="id,title" data-value="{$row.cate_id|default=''}">
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">封面图片</label>
<div class="layui-input-block layuimini-upload">
<input name="cover" class="layui-input layui-col-xs6" placeholder="请上传封面图片" value="{$row.cover|default=''}">
<div class="layuimini-upload-btn">
<span><a class="layui-btn" data-upload="cover" data-upload-number="one" data-upload-exts="png|jpg|ico|jpeg" data-upload-icon="image" data-upload-mimetype="image/*"><i class="fa fa-upload"></i> 上传</a></span>
<span><a class="layui-btn layui-btn-normal" id="select_cover" data-upload-select="cover" data-upload-number="one"><i class="fa fa-list"></i> 选择</a></span>
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">文章简介</label>
<div class="layui-input-block">
<textarea name="summary" class="layui-textarea" placeholder="请输入文章简介">{$row.summary|default=''}</textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">文章内容</label>
<div class="layui-input-block">
<textarea name="content" class="layui-textarea" placeholder="请输入文章内容" style="height: 200px;">{$row.content|default=''}</textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">作者</label>
<div class="layui-input-block">
<input type="text" name="author" class="layui-input" placeholder="请输入作者" value="{$row.author|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">排序</label>
<div class="layui-input-block">
<input type="number" name="sort" class="layui-input" lay-affix="number" placeholder="请输入排序" value="{$row.sort|default=0}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="status" value="1" title="启用" {if condition="$row.status == 1"}checked{/if}>
<input type="radio" name="status" value="0" title="禁用" {if condition="$row.status == 0"}checked{/if}>
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@ -0,0 +1,10 @@
<div class="layuimini-container">
<div class="layuimini-main">
<table id="currentTable" class="layui-table layui-hide"
data-auth-add="{:auth('article.article/add')}"
data-auth-edit="{:auth('article.article/edit')}"
data-auth-delete="{:auth('article.article/delete')}"
lay-filter="currentTable">
</table>
</div>
</div>

View File

@ -0,0 +1,33 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">分类名称</label>
<div class="layui-input-block">
<input type="text" name="title" class="layui-input" lay-verify="required" placeholder="请输入分类名称" value="">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">分类排序</label>
<div class="layui-input-block">
<input type="number" name="sort" class="layui-input" lay-affix="number" placeholder="请输入分类排序" value="0">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="status" value="1" title="启用" checked>
<input type="radio" name="status" value="0" title="禁用">
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@ -0,0 +1,33 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">分类名称</label>
<div class="layui-input-block">
<input type="text" name="title" class="layui-input" lay-verify="required" placeholder="请输入分类名称" value="{$row.title|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">分类排序</label>
<div class="layui-input-block">
<input type="number" name="sort" class="layui-input" lay-affix="number" placeholder="请输入分类排序" value="{$row.sort|default=0}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="status" value="1" title="启用" {if condition="$row.status == 1"}checked{/if}>
<input type="radio" name="status" value="0" title="禁用" {if condition="$row.status == 0"}checked{/if}>
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

Some files were not shown because too many files have changed in this diff Show More