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

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>

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.cate/add')}"
data-auth-edit="{:auth('article.cate/edit')}"
data-auth-delete="{:auth('article.cate/delete')}"
lay-filter="currentTable">
</table>
</div>
</div>

View File

@ -0,0 +1,58 @@
<div class="layuimini-container">
<div class="layuimini-main">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label required">用户头像</label>
<div class="layui-input-block layuimini-upload">
<input name="head_img" class="layui-input layui-col-xs6" lay-reqtext="请上传用户头像" placeholder="请上传用户头像" value="{$row.head_img|default=''}">
<div class="layuimini-upload-btn">
<span><a class="layui-btn" data-upload="head_img" data-upload-number="one" data-upload-exts="png|jpg|ico|jpeg" data-upload-mimetype="image/*"><i class="fa fa-upload"></i> 上传</a></span>
<span><a class="layui-btn layui-btn-normal" id="select_head_img" data-upload-select="head_img" 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 required">登录账户</label>
<div class="layui-input-block">
<input type="text" name="username" class="layui-input" readonly value="{$row.username|default=''}">
<tip>填写登录账户。</tip>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">用户手机</label>
<div class="layui-input-block">
<input type="text" name="phone" class="layui-input" lay-reqtext="请输入用户手机" placeholder="请输入用户手机" value="{$row.phone|default=''}">
<tip>填写用户手机。</tip>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">登录方式</label>
<div class="layui-input-block">
{foreach notes.login_type as $key=>$val}
<input type="radio" name="login_type" lay-skin="primary" title="{$val}" value="{$key}" lay-filter="loginType-filter" {if $key==$row.login_type}checked=""{/if}>
{/foreach}
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">备注信息</label>
<div class="layui-input-block">
<textarea name="remark" class="layui-textarea" placeholder="请输入备注信息">{$row.remark|default=''}</textarea>
</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>
</div>

View File

@ -0,0 +1,38 @@
<div class="layuimini-container">
<div class="layuimini-main">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label required">登录账户</label>
<div class="layui-input-block">
<input type="text" name="username" class="layui-input" readonly value="{$row.username|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">登录密码</label>
<div class="layui-input-block">
<input type="password" name="password" class="layui-input" lay-verify="required" lay-reqtext="请输入登录密码" placeholder="请输入登录密码" value="">
<tip>填写登录密码。</tip>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">确认密码</label>
<div class="layui-input-block">
<input type="password" name="password_again" class="layui-input" lay-verify="required" lay-reqtext="请输入确认密码" placeholder="请输入确认密码" value="">
<tip>填写再次登录密码。</tip>
</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>
</div>

View File

@ -0,0 +1,120 @@
<link rel="stylesheet" href="/static/plugs/lay-module/layuimini/layuimini.css?v={$version}" media="all">
<link rel="stylesheet" href="/static/plugs/lay-module/layuimini/themes/default.css?v={$version}" media="all">
<style id="layuimini-bg-color"></style>
<div class="layui-layout-body layuimini-all">
<div class="layui-layout layui-layout-admin">
<div class="layui-header header">
<div class="layui-logo layuimini-logo"></div>
<div class="layuimini-header-content">
<a>
<div class="layuimini-tool"><i title="展开" class="fa fa-outdent" data-side-fold="1"></i></div>
</a>
<!--电脑端头部菜单-->
<ul class="layui-nav layui-layout-left layuimini-header-menu layuimini-menu-header-pc layuimini-pc-show">
</ul>
<!--手机端头部菜单-->
<ul class="layui-nav layui-layout-left layuimini-header-menu layuimini-mobile-show">
<li class="layui-nav-item">
<a href="javascript:;"><i class="fa fa-list-ul"></i> 选择模块</a>
<dl class="layui-nav-child layuimini-menu-header-mobile">
</dl>
</li>
</ul>
<ul class="layui-nav layui-layout-right">
<li class="layui-nav-item" lay-unselect>
<a href="http://easyadmin8.top" target="_blank"><i class="fa fa-home"></i></a>
</li>
<li class="layui-nav-item" lay-unselect>
<a href="javascript:;" data-refresh="刷新"><i class="fa fa-refresh"></i></a>
</li>
<li class="layui-nav-item" lay-unselect>
<a href="javascript:;" data-clear="清理" class="layuimini-clear"><i class="fa fa-trash"></i></a>
</li>
<li class="layui-nav-item mobile layui-hide-xs" lay-unselect>
<a href="javascript:;" data-check-screen="full"><i class="fa fa-arrows-alt"></i></a>
</li>
<li class="layui-nav-item mobile layui-hide-xs" lay-unselect>
<div class="layui-form ws-header-theme" lay-filter="header-theme">
<input type="checkbox" name="theme-mode" lay-filter="header-theme-mode" lay-skin="switch">
<div lay-checkbox>
<i class="layui-icon layui-icon-moon"></i> |
<i class="layui-icon layui-icon-light"></i>
</div>
</div>
</li>
<li class="layui-nav-item layuimini-setting">
<a href="javascript:;">
<img src="{:session('admin.head_img')}" class="layui-nav-img" width="50" height="50">
<cite class="adminName">{:session('admin.username')}</cite>
<span class="layui-nav-more"></span>
</a>
<dl class="layui-nav-child">
<dd>
<a href="javascript:;" layuimini-content-href="{:__url('index/editAdmin')}" data-title="基本资料" data-icon="fa fa-gears">基本资料<span class="layui-badge-dot"></span></a>
</dd>
<dd>
<a href="javascript:;" layuimini-content-href="{:__url('index/editPassword')}" data-title="修改密码" data-icon="fa fa-gears">修改密码</a>
</dd>
<dd>
<hr>
</dd>
<dd>
<a href="javascript:;" class="login-out">退出登录</a>
</dd>
</dl>
</li>
<li class="layui-nav-item layuimini-select-bgcolor" lay-unselect>
<a href="javascript:;" data-bgcolor="配色方案"><i class="fa fa-ellipsis-v"></i></a>
</li>
</ul>
</div>
</div>
<!--无限极左侧菜单-->
<div class="layui-side layui-bg-black layuimini-menu-left">
</div>
<!--初始化加载层-->
<div class="layuimini-loader">
<div class="layuimini-loader-inner"></div>
</div>
<!--手机端遮罩层-->
<div class="layuimini-make"></div>
<!-- 移动导航 -->
<div class="layuimini-site-mobile"><i class="layui-icon"></i></div>
<div class="layui-body">
<div class="layuimini-tab layui-tabs-rollTool layui-tabs" lay-filter="layuiminiTab" id="layuiminiTab">
<ul class="layui-tabs-header">
<li class="layui-this" id="layuiminiHomeTabId" lay-id=""></li>
</ul>
<div class="layui-tab-control">
<li class="layuimini-tab-roll-left layui-icon layui-icon-left"></li>
<li class="layuimini-tab-roll-right layui-icon layui-icon-right"></li>
<li class="layui-tab-tool layui-icon layui-icon-down">
<ul class="layui-nav close-box">
<li class="layui-nav-item">
<a href="javascript:;"><span class="layui-nav-more"></span></a>
<dl class="layui-nav-child">
<dd><a href="javascript:;" layuimini-tab-close="current"> </a></dd>
<dd><a href="javascript:;" layuimini-tab-close="other"> </a></dd>
<dd><a href="javascript:;" layuimini-tab-close="all"> </a></dd>
</dl>
</li>
</ul>
</li>
</div>
<div class="layui-tabs-body">
<div id="layuiminiHomeTabIframe" class="layui-tab-item layui-tabs-item layui-show"></div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,45 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form" autocomplete="off">
{if $old_secret}
<div class="layui-card">
<div class="layui-card-header">提示</div>
<div class="layui-card-body">
当前账号已经绑定过了 谷歌验证码 ,如果重新保存将替换
</div>
</div>
{/if}
<div class="layui-form-item">
<label class="layui-form-label required">验证秘钥</label>
<div class="layui-input-block">
<input type="text" name="ga_secret" class="layui-input" value="{$secret}" readonly disabled>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label required">二维码</label>
<div class="layui-input-block">
<img src="{$dataUri}" alt="二维码" style="width: 200px;height: 200px">
<div class="layui-text layui-font-cyan layui-font-12">
使用&nbsp;
<a href="https://2fas.com" target="_blank"><span class="layui-text layui-font-blue">2FAS</span></a>
&nbsp;或者&nbsp;
<a href="https://cn.bing.com/search?q=Google+Authenticator" target="_blank"><span class="layui-text layui-font-blue">Google Authenticator</span></a>
&nbsp;APP 扫描二维码 输入验证码 进行绑定
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label required">谷歌验证码</label>
<div class="layui-input-block">
<input type="text" name="ga_code" class="layui-input" maxlength="6" lay-verify="required" placeholder="扫描二维码,输入验证码" value="">
</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,40 @@
<link rel="stylesheet" href="/static/admin/css/welcome.css?v={$version}" media="all">
<div class="layui-layout layui-padding-2">
<div class="layui-layout-admin">
<div class="layui-row layui-col-space10">
<div class="layui-col-md8 ">
<div class="layui-row layui-col-space10">
<div class="layui-col-md12 ">
<div class="layui-card">
<div class="layui-card-header"><i class="fa fa-warning icon"></i>数据统计</div>
<div class="layui-card-body">
<div class="welcome-module">
<div class="layui-row layui-col-space20">
{volist name="data" id="vo"}
<div class="layui-col-xs3">
<div class="panel layui-bg-number">
<div class="panel-body">
<div class="panel-title">
<span class="label pull-right layui-bg-blue">实时</span>
<h5>{$vo.title}</h5>
</div>
<div class="panel-content">
<h1 class="no-margins">{$vo.data}</h1>
<small>当前分类总记录数</small>
</div>
</div>
</div>
</div>
{/volist}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{:sysConfig('site','site_name')}</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="icon" href="{:sysConfig('site', 'site_ico')}" type="image/x-icon">
<!--[if lt IE 9]>
<script src="https://cdn.staticfile.org/html5shiv/r29/html5.min.js"></script>
<script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
<link rel="stylesheet" href="/static/admin/css/public.css?v={$version}" media="all">
<link rel="stylesheet" href="" id="layuicss-theme-dark" media="all">
<script>
window.CONFIG = {
ADMIN: "{$adminModuleName|default='admin'}",
CONTROLLER_JS_PATH: "{$thisControllerJsPath|default=''}",
ACTION: "{$thisAction|default=''}",
AUTOLOAD_JS: "{$autoloadJs|default='false'}",
IS_SUPER_ADMIN: "{$isSuperAdmin|default='false'}",
VERSION: "{$version|default='1.0.0'}",
CSRF_TOKEN: "{:token()}",
ADMIN_UPLOAD_URL: "{$adminUploadUrl|default=''}",
IFRAME_OPEN_TOP: "{$iframeOpenTop|default=0}",
EDITOR_TYPE: "{$adminEditor|default='wangEditor'}",
};
</script>
<script src="/static/plugs/layui-v2.x/layui.js" charset="utf-8"></script>
<script src="/static/plugs/require-2.3.6/require.js" charset="utf-8"></script>
<script src="/static/config-admin.js?v={$version}" charset="utf-8"></script>
<script src="/static/common/js/admin.js?v={$version}" charset="utf-8"></script>
{include file="layout/editor" /}
</head>
<body>
{__CONTENT__}
</body>
</html>

View File

@ -0,0 +1,27 @@
{switch $adminEditor}
{case ckeditor}
<script src="/static/plugs/ckeditor4/ckeditor.js?v={$version}" charset="utf-8"></script>
{/case}
{case wangEditor}
<link rel="stylesheet" href="/static/plugs/wangEditor/dist/style.css?v={$version}">
<script src="/static/plugs/wangEditor/dist/index.js?v={$version}"></script>
{/case}
{case EasyMDE}
<link rel="stylesheet" href="/static/plugs/easymde/easymde.min.css?v={$version}">
<script src="/static/plugs/easymde/easymde.min.js?v={$version}"></script>
{/case}
{default /}
<script src="/static/plugs/ueditor/ueditor.config.js?v={$version}" charset="utf-8"></script>
<script src="/static/plugs/ueditor/ueditor.all.js?v={$version}" charset="utf-8"></script>
<script src="/static/plugs/ueditor/lang/zh-cn/zh-cn.js?v={$version}" charset="utf-8"></script>
<script src="/static/plugs/ueditor/third-party/codemirror/codemirror.js?v={$version}" charset="utf-8"></script>
<script src="/static/plugs/ueditor/third-party/zeroclipboard/zeroclipboard.js?v={$version}" charset="utf-8"></script>
{/switch}

View File

@ -0,0 +1,55 @@
<link rel="stylesheet" href="/static/admin/css/login.css?v={$version}" media="all">
<div class="container">
<div class="main-body">
<div class="login-main">
<div class="login-top">
<span>{:sysConfig('site','site_name')}</span>
<span class="bg1"></span>
<span class="bg2"></span>
</div>
<form class="layui-form login-bottom">
<div class="demo {if !$isDemo}layui-hide{/if}">用户名:admin 密码:123456</div>
<div class="center">
<div class="item">
<span class="icon icon-2"></span>
<input type="text" name="username" lay-verify="required" placeholder="请输入登录账号" maxlength="24"/>
</div>
<div class="item">
<span class="icon icon-3"></span>
<input type="password" name="password" lay-verify="required" placeholder="请输入密码" maxlength="20">
<span class="bind-password icon icon-4"></span>
</div>
<div class="item layui-hide" id="gaCode">
<span class="icon icon-3"></span>
<input type="text" name="ga_code" placeholder="谷歌验证码" maxlength="6">
</div>
{if $captcha == 1}
<div id="validatePanel" class="item" style="width: 137px;">
<input type="text" name="captcha" placeholder="请输入验证码" maxlength="4">
<img id="refreshCaptcha" class="validateImg" src="{:url('login/captcha')}" onclick="this.src='{:url(\'login/captcha\')}?seed='+Math.random()">
</div>
{/if}
</div>
<div class="tip">
<span class="icon-nocheck"></span>
<span class="login-tip">保持登录</span>
<a href="javascript:" class="forget-password">忘记密码?</a>
</div>
<div class="layui-form-item" style="text-align:center; width:100%;height:100%;margin:0px;">
<button type="button" class="login-btn" lay-submit>立即登录</button>
</div>
</form>
</div>
</div>
<div class="footer">
{:sysConfig('site','site_copyright')}<span class="padding-5">|</span><a target="_blank" href="http://www.miitbeian.gov.cn">{:sysConfig('site','site_beian')}</a>
</div>
</div>
<script>
let backgroundUrl = "{:sysConfig('site','admin_background')}"
</script>

View File

@ -0,0 +1,56 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form layui-form-pane">
<div class="layui-row">
<div class="layui-form-item">
<div class="layui-row">
<label class="layui-form-label required">金额</label>
<div class="layui-input-block layui-col-space5">
<div class="layui-col-xs10">
<div class="layui-input-wrap">
<input type="text" disabled name="money" class="layui-input" lay-verify="required" placeholder="请输入金额" value="{$row.money|default=''}">
</div>
</div>
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-row">
<label class="layui-form-label required">支付链接</label>
<div class="layui-input-block layui-col-space5">
<div class="layui-col-xs10">
<div class="layui-input-wrap">
<input type="text" name="url" class="layui-input" lay-verify="required" placeholder="请输入支付链接" value="{$row.url|default=''}">
</div>
</div>
</div>
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">备注信息</label>
<div class="layui-input-block">
<textarea name="remark" class="layui-textarea" placeholder="请输入备注信息">{$row.remark|default=''}</textarea>
</div>
</div>
</div>
<div class="layui-col-xl7 layui-col-lg7 layui-col-md12 layui-col-sm12 layui-col-xs12">
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">商品描述</label>
<div class="layui-input-block">
{:editor_textarea($row["describe"],'describe')}
</div>
</div>
</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,12 @@
<div class="layuimini-container">
<div class="layuimini-main">
<table id="currentTable" class="layui-table layui-hide"
data-auth-add="{:auth('mall.blackip/add')}"
data-auth-edit="{:auth('mall.blackip/edit')}"
data-auth-delete="{:auth('mall.blackip/delete')}"
data-auth-stock="{:auth('mall.blackip/stock')}"
data-auth-recycle="{:auth('mall.blackip/recycle')}"
lay-filter="currentTable">
</table>
</div>
</div>

View File

@ -0,0 +1,43 @@
<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 required">分类图片</label>
<div class="layui-input-block layuimini-upload">
<input name="image" class="layui-input layui-col-xs6" lay-verify="required" placeholder="请上传分类图片" value="">
<div class="layuimini-upload-btn">
<span><a class="layui-btn" data-upload="image" 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_image" data-upload-select="image" 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">
<input type="number" name="sort" class="layui-input" lay-affix="number" placeholder="请输入分类排序" value="0">
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">备注信息</label>
<div class="layui-input-block">
<textarea name="remark" class="layui-textarea" placeholder="请输入备注信息"></textarea>
</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