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

187
app/common/command/Curd.php Normal file
View File

@ -0,0 +1,187 @@
<?php
namespace app\common\command;
use app\admin\service\console\CliEcho;
use app\admin\service\curd\BuildCurd;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\Exception;
class Curd extends Command
{
protected function configure()
{
$this->setName('curd')
->addOption('table', 't', Option::VALUE_REQUIRED, '主表名', null)
->addOption('controllerFilename', 'c', Option::VALUE_REQUIRED, '控制器文件名', null)
->addOption('modelFilename', 'm', Option::VALUE_REQUIRED, '主表模型文件名', null)
#
->addOption('checkboxFieldSuffix', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '复选框字段后缀', null)
->addOption('radioFieldSuffix', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '单选框字段后缀', null)
->addOption('imageFieldSuffix', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '单图片字段后缀', null)
->addOption('imagesFieldSuffix', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '多图片字段后缀', null)
->addOption('fileFieldSuffix', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '单文件字段后缀', null)
->addOption('filesFieldSuffix', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '多文件字段后缀', null)
->addOption('dateFieldSuffix', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '时间字段后缀', null)
->addOption('switchFields', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '开关的字段', null)
->addOption('selectFields', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '下拉的字段', null)
->addOption('editorFields', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '富文本的字段', null)
->addOption('sortFields', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '排序的字段', null)
->addOption('ignoreFields', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '忽略的字段', null)
#
->addOption('relationTable', 'r', Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '关联表名', null)
->addOption('foreignKey', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '关联外键', null)
->addOption('primaryKey', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '关联主键', null)
->addOption('relationModelFilename', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '关联模型文件名', null)
->addOption('relationOnlyFields', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '关联模型中只显示的字段', null)
->addOption('relationBindSelect', null, Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, '关联模型中的字段用于主表外键的表单下拉选择', null)
#
->addOption('force', 'f', Option::VALUE_REQUIRED, '强制覆盖模式', 0)
->addOption('delete', 'd', Option::VALUE_REQUIRED, '删除模式', 0)
->setDescription('一键curd命令服务');
}
protected function execute(Input $input, Output $output)
{
$table = $input->getOption('table');
$controllerFilename = $input->getOption('controllerFilename');
$modelFilename = $input->getOption('modelFilename');
$checkboxFieldSuffix = $input->getOption('checkboxFieldSuffix');
$radioFieldSuffix = $input->getOption('radioFieldSuffix');
$imageFieldSuffix = $input->getOption('imageFieldSuffix');
$imagesFieldSuffix = $input->getOption('imagesFieldSuffix');
$fileFieldSuffix = $input->getOption('fileFieldSuffix');
$filesFieldSuffix = $input->getOption('filesFieldSuffix');
$dateFieldSuffix = $input->getOption('dateFieldSuffix');
$switchFields = $input->getOption('switchFields');
$selectFields = $input->getOption('selectFields');
$sortFields = $input->getOption('sortFields');
$ignoreFields = $input->getOption('ignoreFields');
$relationTable = $input->getOption('relationTable');
$foreignKey = $input->getOption('foreignKey');
$primaryKey = $input->getOption('primaryKey');
$relationModelFilename = $input->getOption('relationModelFilename');
$relationOnlyFields = $input->getOption('relationOnlyFields');
$relationBindSelect = $input->getOption('relationBindSelect');
$force = $input->getOption('force');
$delete = $input->getOption('delete');
$relations = [];
foreach ($relationTable as $key => $val) {
$relations[] = [
'table' => $val,
'foreignKey' => $foreignKey[$key] ?? null,
'primaryKey' => $primaryKey[$key] ?? null,
'modelFilename' => $relationModelFilename[$key] ?? null,
'onlyField' => isset($relationOnlyFields[$key]) ? explode(",", $relationOnlyFields[$key]) : [],
'relationBindSelect' => $relationBindSelect[$key] ?? null,
];
}
if (empty($table)) {
if (PHP_SAPI == 'cli')
CliEcho::error('请设置主表');
else
$output->writeln('请设置主表');
return false;
}
try {
$build = (new BuildCurd())
->setTable($table)
->setForce($force);
!empty($controllerFilename) && $build = $build->setControllerFilename($controllerFilename);
!empty($modelFilename) && $build = $build->setModelFilename($modelFilename);
!empty($checkboxFieldSuffix) && $build = $build->setCheckboxFieldSuffix($checkboxFieldSuffix);
!empty($radioFieldSuffix) && $build = $build->setRadioFieldSuffix($radioFieldSuffix);
!empty($imageFieldSuffix) && $build = $build->setImageFieldSuffix($imageFieldSuffix);
!empty($imagesFieldSuffix) && $build = $build->setImagesFieldSuffix($imagesFieldSuffix);
!empty($fileFieldSuffix) && $build = $build->setFileFieldSuffix($fileFieldSuffix);
!empty($filesFieldSuffix) && $build = $build->setFilesFieldSuffix($filesFieldSuffix);
!empty($dateFieldSuffix) && $build = $build->setDateFieldSuffix($dateFieldSuffix);
!empty($switchFields) && $build = $build->setSwitchFields($switchFields);
!empty($selectFields) && $build = $build->setselectFields($selectFields);
!empty($sortFields) && $build = $build->setSortFields($sortFields);
!empty($ignoreFields) && $build = $build->setIgnoreFields($ignoreFields);
foreach ($relations as $relation) {
$build = $build->setRelation($relation['table'], $relation['foreignKey'], $relation['primaryKey'], $relation['modelFilename'], $relation['onlyField'], $relation['relationBindSelect']);
}
$build = $build->render();
$fileList = $build->getFileList();
if (!$delete) {
$result = $build->create();
if ($force) {
if (PHP_SAPI == 'cli') {
$output->info(">>>>>>>>>>>>>>>");
foreach ($fileList as $key => $val) {
$output->info($key);
}
$output->info(">>>>>>>>>>>>>>>");
$output->info("确定强制生成上方所有文件? 如果文件存在会直接覆盖。 请输入 'yes' 按回车键继续操作: ");
$line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
if (trim($line) != 'yes') {
throw new Exception("取消文件CURD生成操作");
}
CliEcho::success('自动生成CURD成功');
}else {
$output->writeln('自动生成CURD成功');
}
}
}else {
if (PHP_SAPI == 'cli') {
$output->info(">>>>>>>>>>>>>>>");
foreach ($fileList as $key => $val) {
$output->info($key);
}
$output->info(">>>>>>>>>>>>>>>");
$output->info("确定删除上方所有文件? 请输入 'yes' 按回车键继续操作: ");
$line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
if (trim($line) != 'yes') {
throw new Exception("取消删除文件操作");
}
$result = $build->delete();
CliEcho::success('>>>>>>>>>>>>>>>');
CliEcho::success('删除自动生成CURD文件成功');
CliEcho::success('>>>>>>>>>>>>>>>');
foreach ($result as $vo) {
CliEcho::success($vo);
}
}else {
$result = $build->delete();
$output->writeln('>>>>>>>>>>>>>>>');
$output->writeln('删除自动生成CURD文件成功');
$output->writeln('>>>>>>>>>>>>>>>');
foreach ($result as $vo) {
$output->writeln($vo);
}
}
}
if (PHP_SAPI == 'cli') {
$output->info(">>>>>>>>>>>>>>>");
$output->info('执行成功');
}else {
$output->writeln('执行成功');
}
}catch (\Exception $e) {
if (PHP_SAPI == 'cli')
CliEcho::error($e->getMessage());
else
$output->writeln($e->getMessage());
}
return false;
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace app\common\command;
use app\admin\model\SystemNode;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use app\admin\service\NodeService;
class Node extends Command
{
protected function configure()
{
$this->setName('node')
->addOption('force', null, Option::VALUE_REQUIRED, '是否强制刷新', 0)
->setDescription('系统节点刷新服务');
}
protected function execute(Input $input, Output $output)
{
$force = $input->getOption('force');
$output->writeln("========正在刷新节点服务:=====" . date('Y-m-d H:i:s'));
$check = $this->refresh($force);
$check !== true && $output->writeln("节点刷新失败:" . $check);
$output->writeln("刷新完成:" . date('Y-m-d H:i:s'));
}
protected function refresh($force)
{
$nodeList = (new NodeService())->getNodeList();
if (empty($nodeList)) {
return true;
}
$model = new SystemNode();
try {
if ($force == 1) {
$updateNodeList = $model->whereIn('node', array_column($nodeList, 'node'))->select();
$formatNodeList = array_format_key($nodeList, 'node');
foreach ($updateNodeList as $vo) {
isset($formatNodeList[$vo['node']]) && $model->where('id', $vo['id'])->update([
'title' => $formatNodeList[$vo['node']]['title'],
'is_auth' => $formatNodeList[$vo['node']]['is_auth'],
]);
}
}
$existNodeList = $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;
}
}
}
$model->insertAll($nodeList);
} catch (\Exception $e) {
return $e->getMessage();
}
return true;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace app\common\constants;
/**
* 管理员常量
* Class AdminConstant
* @package app\common\constants
*/
class AdminConstant
{
/**
* 超级管理员,不受权限控制
*/
const SUPER_ADMIN_ID = 1;
}

View File

@ -0,0 +1,23 @@
<?php
namespace app\common\constants;
/**
* 菜单常量
* Class MenuConstant
* @package app\common\constants
*/
class MenuConstant
{
/**
* 首页的PID
*/
const HOME_PID = 99999999;
/**
* 模块名前缀
*/
const MODULE_PREFIX = 'easyadmin_';
}

View File

@ -0,0 +1,277 @@
<?php
namespace app\common\controller;
use app\admin\service\ConfigService;
use app\admin\traits\Curd;
use app\BaseController;
use app\common\constants\AdminConstant;
use app\common\traits\JumpTrait;
use think\facade\Db;
use think\facade\View;
use think\helper\Str;
use think\response\Json;
class AdminController extends BaseController
{
use Curd;
use JumpTrait;
/**
* 当前模型
* @Model
* @var mixed
*/
protected static mixed $model;
/**
* 字段排序
* @var array
*/
protected array $sort = [
'id' => 'desc',
];
/**
* 允许修改的字段
* @var array
*/
protected array $allowModifyFields = [
'status',
'sort',
'remark',
'is_delete',
'is_auth',
'title',
];
/**
* 过滤节点更新
* @var array
*/
protected array $ignoreNode = [];
/**
* 不导出的字段信息
* @var array
*/
protected array $noExportFields = ['delete_time', 'update_time'];
/**
* 下拉选择条件
* @var array
*/
protected array $selectWhere = [];
/**
* 是否关联查询
* @var bool
*/
protected bool $relationSearch = false;
/**
* 模板布局, false取消
* @var string|bool
*/
protected string|bool $layout = 'layout/default';
/**
* 是否为演示环境
* @var bool
*/
protected bool $isDemo = false;
/**
* @var int|string
*/
protected int|string $adminUid;
/**
* 初始化方法
*/
protected function initialize(): void
{
parent::initialize();
$this->adminUid = request()->adminUserInfo['id'] ?? 0;
$this->isDemo = env('EASYADMIN.IS_DEMO', false);
$this->setOrder();
$this->viewInit();
}
/**
* 初始化排序
* @return $this
*/
public function setOrder(): static
{
$tableOrder = $this->request->param('tableOrder/s', '');
if (!empty($tableOrder)) {
[$orderField, $orderType] = explode(' ', $tableOrder);
$this->sort = [$orderField => $orderType];
}
return $this;
}
/**
* 模板变量赋值
* @param array|string $name 模板变量
* @param mixed|null $value 变量值
*/
public function assign(array|string $name, mixed $value = null): void
{
View::assign($name, $value);
}
/**
* 解析和获取模板内容 用于输出
* @param string $template
* @param array $vars
* @param bool $layout 是否需要自动布局
* @return string
*/
public function fetch(string $template = '', array $vars = [], bool $layout = true): string
{
if ($layout) View::instance()->engine()->layout('/layout/default');
View::assign($vars);
return View::fetch($template);
}
/**
* 重写验证规则
* @param array $data
* @param array|string $validate
* @param array $message
* @param bool $batch
* @return bool
*/
public function validate(array $data, $validate, array $message = [], bool $batch = false): bool
{
try {
parent::validate($data, $validate, $message, $batch);
}catch (\Exception $e) {
$this->error($e->getMessage());
}
return true;
}
/**
* 构建请求参数
* @param array $excludeFields 忽略构建搜索的字段
* @return array
*/
protected function buildTableParams(array $excludeFields = []): array
{
$get = $this->request->get();
$page = !empty($get['page']) ? $get['page'] : 1;
$limit = !empty($get['limit']) ? $get['limit'] : 15;
$filters = !empty($get['filter']) ? htmlspecialchars_decode($get['filter']) : '{}';
$ops = !empty($get['op']) ? htmlspecialchars_decode($get['op']) : '{}';
// json转数组
$filters = json_decode($filters, true);
$ops = json_decode($ops, true);
$where = [];
$excludes = [];
// 判断是否关联查询
$tableName = Str::snake(lcfirst((new self::$model)->getName()));
foreach ($filters as $key => $val) {
if (in_array($key, $excludeFields)) {
$excludes[$key] = $val;
continue;
}
$op = !empty($ops[$key]) ? $ops[$key] : '%*%';
if ($this->relationSearch && count(explode('.', $key)) == 1) {
$key = "{$tableName}.{$key}";
}
switch (strtolower($op)) {
case '=':
$where[] = [$key, '=', $val];
break;
case '%*%':
$where[] = [$key, 'LIKE', "%{$val}%"];
break;
case '*%':
$where[] = [$key, 'LIKE', "{$val}%"];
break;
case '%*':
$where[] = [$key, 'LIKE', "%{$val}"];
break;
case 'in':
$where[] = [$key, 'IN', $val];
break;
case 'find_in_set':
$where[] = ['', 'exp', Db::raw("FIND_IN_SET(:param,$key)", ['param' => $val])];
break;
case 'range':
[$beginTime, $endTime] = explode(' - ', $val);
$where[] = [$key, '>=', strtotime($beginTime)];
$where[] = [$key, '<=', strtotime($endTime)];
break;
case 'datetime':
[$beginTime, $endTime] = explode(' - ', $val);
$where[] = [$key, '>=', $beginTime];
$where[] = [$key, '<=', $endTime];
break;
default:
$where[] = [$key, $op, "%{$val}"];
}
}
return [(int)$page, (int)$limit, $where, $excludes];
}
/**
* 下拉选择列表
* @return Json
*/
public function selectList(): Json
{
$fields = input('selectFields');
$data = self::$model::where($this->selectWhere)->field($fields)->select()->toArray();
$this->success(null, $data);
}
/**
* 初始化视图参数
*/
private function viewInit(): void
{
$request = app()->request;
list($thisModule, $thisController, $thisAction) = [app('http')->getName(), app()->request->controller(), $request->action()];
list($thisControllerArr, $jsPath) = [explode('.', $thisController), null];
foreach ($thisControllerArr as $vo) {
empty($jsPath) ? $jsPath = parse_name($vo) : $jsPath .= '/' . parse_name($vo);
}
$autoloadJs = file_exists(root_path('public') . "static/{$thisModule}/js/{$jsPath}.js");
$thisControllerJsPath = "{$thisModule}/js/{$jsPath}.js";
$adminModuleName = config('admin.alias_name');
$isSuperAdmin = $this->adminUid == AdminConstant::SUPER_ADMIN_ID;
$data = [
'isDemo' => $this->isDemo,
'adminModuleName' => $adminModuleName,
'thisController' => parse_name($thisController),
'thisAction' => $thisAction,
'thisRequest' => parse_name("{$thisModule}/{$thisController}/{$thisAction}"),
'thisControllerJsPath' => "{$thisControllerJsPath}",
'autoloadJs' => $autoloadJs,
'isSuperAdmin' => $isSuperAdmin,
'version' => env('APP_DEBUG') ? time() : ConfigService::getVersion(),
'adminUploadUrl' => url('ajax/upload', [], false),
'adminEditor' => sysConfig('site', 'editor_type') ?: 'wangEditor',
'iframeOpenTop' => sysConfig('site', 'iframe_open_top') ?: 0,
];
View::assign($data);
}
/**
* 严格校验接口是否为POST请求
*/
protected function checkPostRequest(): void
{
if (!$this->request->isPost()) {
$this->error("当前请求不合法!");
}
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace app\common\entity;
use think\Entity;
use think\model\type\DateTime;
class BaseEntity extends Entity
{
protected function getOptions(): array
{
return [
'type' => [
'create_time' => DateTime::class,
'update_time' => DateTime::class,
'delete_time' => DateTime::class,
],
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace app\common\model;
use think\model\relation\HasMany;
class ArticleCates extends TimeModel
{
protected array $rule = [
'title' => 'require|max:50|unique:article_cates',
'sort' => 'integer|egt:0',
'status' => 'in:0,1'
];
protected array $message = [
'title.require' => '分类名称不能为空',
'title.max' => '分类名称最多50个字符',
'title.unique' => '分类名称已存在',
'sort.integer' => '排序必须为整数',
'sort.egt' => '排序不能小于0',
'status.in' => '状态值错误'
];
public function articles(): HasMany
{
return $this->hasMany(Articles::class, 'cate_id');
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace app\common\model;
class Articles extends TimeModel
{
protected array $rule = [
'title' => 'require|max:50',
'cate_id' => 'require|integer|gt:0',
'cover' => 'max:255',
'summary' => 'max:255',
'author' => 'max:50',
'sort' => 'integer|egt:0',
'status' => 'in:0,1'
];
protected array $message = [
'title.require' => '文章标题不能为空',
'title.max' => '文章标题最多50个字符',
'cate_id.require' => '分类ID不能为空',
'cate_id.integer' => '分类ID必须为整数',
'cate_id.gt' => '分类ID必须大于0',
'cover.max' => '封面路径最多255个字符',
'summary.max' => '简介最多255个字符',
'author.max' => '作者最多50个字符',
'sort.integer' => '排序必须为整数',
'sort.egt' => '排序不能小于0',
'status.in' => '状态值错误'
];
public function cate()
{
return $this->belongsTo(ArticleCates::class, 'cate_id');
}
public function getCateNameAttr($value, $data)
{
return $this->cate ? $this->cate->title : '';
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace app\common\model;
use think\Model;
use think\model\concern\SoftDelete;
/**
* 有关时间的模型
* Class TimeModel
* @package app\common\model
*/
class TimeModel extends Model
{
public array $statusText = [
1 => '启用',
-1 => '禁用',
0 => '未启用',
];
/**
* 软删除
*/
use SoftDelete;
protected function getOptions(): array
{
return [
'autoWriteTimestamp' => true,
'createTime' => 'create_time',
'updateTime' => 'update_time',
'deleteTime' => false,
];
}
public function getStatusTextAttr($value): string
{
return $this->statusText[$value] ?? '未知状态';
}
}

View File

@ -0,0 +1,232 @@
<?php
namespace app\common\service;
use app\admin\service\annotation\NodeAnnotation;
use app\common\constants\AdminConstant;
use think\facade\Db;
/**
* 权限验证服务
* Class AuthService
* @package app\common\service
*/
class AuthService
{
/**
* 用户ID
* @var null
*/
protected $adminId = null;
/**
* 默认配置
* @var array
*/
protected $config = [
'auth_on' => true, // 权限开关
'system_admin' => 'system_admin', // 用户表
'system_auth' => 'system_auth', // 权限表
'system_node' => 'system_node', // 节点表
'system_auth_node' => 'system_auth_node',// 权限-节点表
];
/**
* 管理员信息
* @var array|\think\Model|null
*/
protected $adminInfo;
/**
* 所有节点信息
* @var array
*/
protected $nodeList;
/**
* 管理员所有授权节点
* @var array
*/
protected $adminNode;
/***
* 构造方法
* AuthService constructor.
* @param null $adminId
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function __construct($adminId = null)
{
$this->adminId = $adminId;
$this->adminInfo = $this->getAdminInfo();
$this->nodeList = $this->getNodeList();
$this->adminNode = $this->getAdminNode();
return $this;
}
/**
* 检测检测权限
* @param null $node
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function checkNode($node = null)
{
// 判断是否为超级管理员
if ($this->adminId == AdminConstant::SUPER_ADMIN_ID) {
return true;
}
// 判断权限验证开关
if ($this->config['auth_on'] == false) {
return true;
}
// 验证是否为URL
if (filter_var($node, FILTER_VALIDATE_URL)) {
return true;
}
// 判断是否需要获取当前节点
if (empty($node)) {
$node = $this->getCurrentNode();
}else {
$node = $this->parseNodeStr($node);
}
// 判断是否加入节点控制,优先获取缓存信息
if (!isset($this->nodeList[$node])) {
return false;
}
$nodeInfo = $this->nodeList[$node];
if ($nodeInfo['is_auth'] == 0) {
return true;
}
// 用户验证,优先获取缓存信息
if (empty($this->adminInfo) || $this->adminInfo['status'] != 1 || empty($this->adminInfo['auth_ids'])) {
return false;
}
// 判断该节点是否允许访问
if (in_array($node, $this->adminNode)) {
return true;
}
if ($this->checkNodeAnnotationAttrAuth($node)) return true;
return false;
}
protected function checkNodeAnnotationAttrAuth(string $node): bool
{
$bool = false;
$controller = request()->controller();
try {
$controllerExplode = explode('.', $controller);
[$_name, $_controller] = $controllerExplode;
$nodeExplode = explode('/', $node);
$action = end($nodeExplode);
$reflectionClass = new \ReflectionClass("app\admin\controller\\{$_name}\\{$_controller}");
$attributes = $reflectionClass->getMethod($action)->getAttributes(NodeAnnotation::class);
foreach ($attributes as $attribute) {
$annotation = $attribute->newInstance();
$bool = $annotation->auth === false;
}
}catch (\Throwable) {
}
return $bool;
}
/**
* 获取当前节点
* @return string
*/
public function getCurrentNode()
{
$node = $this->parseNodeStr(request()->controller() . '/' . request()->action());
return $node;
}
/**
* 获取当前管理员所有节点
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getAdminNode()
{
$nodeList = [];
$adminInfo = Db::name($this->config['system_admin'])
->where([
'id' => $this->adminId,
'status' => 1,
])->find();
if (!empty($adminInfo) && !empty($adminInfo['auth_ids'])) {
$buildAuthSql = Db::name($this->config['system_auth'])
->distinct(true)
->whereIn('id', $adminInfo['auth_ids'])
->field('id')
->buildSql(true);
$buildAuthNodeSql = Db::name($this->config['system_auth_node'])
->distinct(true)
->where("auth_id IN {$buildAuthSql}")
->field('node_id')
->buildSql(true);
$nodeList = Db::name($this->config['system_node'])
->distinct(true)
->where("id IN {$buildAuthNodeSql}")
->column('node');
}
return $nodeList;
}
/**
* 获取所有节点信息
* @time 2021-01-07
* @return array
* @author zhongshaofa <shaofa.zhong@happy-seed.com>
*/
public function getNodeList()
{
return Db::name($this->config['system_node'])
->column('id,node,title,type,is_auth', 'node');
}
/**
* 获取管理员信息
* @time 2021-01-07
* @return array|\think\Model|null
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author zhongshaofa <shaofa.zhong@happy-seed.com>
*/
public function getAdminInfo()
{
return Db::name($this->config['system_admin'])
->where('id', $this->adminId)
->find();
}
/**
* 驼峰转下划线规则
* @param string $node
* @return string
*/
public function parseNodeStr($node)
{
$array = explode('/', $node);
foreach ($array as $key => $val) {
if ($key == 0) {
$val = explode('.', $val);
foreach ($val as &$vo) {
$vo = \think\helper\Str::snake(lcfirst($vo));
}
$val = implode('.', $val);
$array[$key] = $val;
}
}
$node = implode('/', $array);
return $node;
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace app\common\service;
use app\common\constants\MenuConstant;
use think\facade\Db;
class MenuService
{
/**
* 管理员ID
* @var integer
*/
protected $adminId;
public function __construct($adminId)
{
$this->adminId = $adminId;
return $this;
}
/**
* 获取首页信息
* @return array|\think\Model|null
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getHomeInfo()
{
$data = Db::name('system_menu')
->field('title,icon,href')
->where("delete_time is null")
->where('pid', MenuConstant::HOME_PID)
->find();
!empty($data) && $data['href'] = __url($data['href']);
return $data;
}
/**
* 获取后台菜单树信息
* @return mixed
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getMenuTree()
{
/** @var AuthService $authService */
$authServer = app(AuthService::class, ['adminId' => $this->adminId]);
return $this->buildMenuChild(0, $this->getMenuData(), $authServer);
}
private function buildMenuChild($pid, $menuList, AuthService $authServer)
{
$treeList = [];
foreach ($menuList as &$v) {
$check = empty($v['href']) || $authServer->checkNode($v['href']);
!empty($v['href']) && $v['href'] = __url($v['href']);
if ($pid == $v['pid'] && $check) {
$node = $v;
$child = $this->buildMenuChild($v['id'], $menuList, $authServer);
if (!empty($child)) {
$node['child'] = $child;
}
if (!empty($v['href']) || !empty($child)) {
$treeList[] = $node;
}
}
}
return $treeList;
}
/**
* 获取所有菜单数据
* @return \think\Collection
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
protected function getMenuData()
{
$menuData = Db::name('system_menu')
->field('id,pid,title,icon,href,target')
->where("delete_time is null")
->where([
['status', '=', '1'],
['pid', '<>', MenuConstant::HOME_PID],
])
->order([
'sort' => 'desc',
'id' => 'asc',
])
->select();
return $menuData;
}
}

View File

@ -0,0 +1,161 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>跳转提示</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: Lantinghei SC, Open Sans, Arial, Hiragino Sans GB, Microsoft YaHei, "微软雅黑", STHeiti, WenQuanYi Micro Hei, SimSun, sans-serif;
-webkit-font-smoothing: antialiased
}
body {
padding: 70px 0;
background: #edf1f4;
font-weight: 400;
font-size: 1pc;
-webkit-text-size-adjust: none;
color: #333
}
a {
outline: 0;
color: #3498db;
text-decoration: none;
cursor: pointer
}
.system-message {
margin: 20px 5%;
padding: 40px 20px;
background: #fff;
box-shadow: 1px 1px 1px hsla(0, 0%, 39%, .1);
text-align: center
}
.system-message h1 {
margin: 0;
margin-bottom: 9pt;
color: #444;
font-weight: 400;
font-size: 40px
}
.system-message .jump, .system-message .image {
margin: 20px 0;
padding: 0;
padding: 10px 0;
font-weight: 400
}
.system-message .jump {
font-size: 14px
}
.system-message .jump a {
color: #333
}
.system-message p {
font-size: 9pt;
line-height: 20px
}
.system-message .btn {
display: inline-block;
margin-right: 10px;
width: 138px;
height: 2pc;
border: 1px solid #44a0e8;
border-radius: 30px;
color: #44a0e8;
text-align: center;
font-size: 1pc;
line-height: 2pc;
margin-bottom: 5px;
}
.success .btn {
border-color: #69bf4e;
color: #69bf4e
}
.error .btn {
border-color: #ff8992;
color: #ff8992
}
.info .btn {
border-color: #3498db;
color: #3498db
}
.copyright p {
width: 100%;
color: #919191;
text-align: center;
font-size: 10px
}
.system-message .btn-grey {
border-color: #bbb;
color: #bbb
}
.clearfix:after {
clear: both;
display: block;
visibility: hidden;
height: 0;
content: "."
}
@media (max-width: 768px) {
body {
padding: 20px 0;
}
}
@media (max-width: 480px) {
.system-message h1 {
font-size: 30px;
}
}
</style>
</head>
<body>
<?php
$codeText = $code == 1 ? 'success' : ($code == 0 ? 'error' : 'info');
?>
<div class="system-message {$codeText}">
<div class="image">
<img src="/static/common/images/{$codeText}.svg" alt="" width="150"/>
</div>
<h1><?php echo(strip_tags($msg));?></h1>
<p class="jump">
页面将在 <span id="wait"><?php echo($wait);?></span> 秒后自动跳转
</p>
<p class="clearfix">
<a href="#" onClick="history.back(-1);" class="btn btn-grey">返回上一页</a>
<a id="href" href="{$url}" class="btn btn-primary">立即跳转</a>
</p>
</div>
<script type="text/javascript">
(function () {
var wait = document.getElementById('wait'),
href = document.getElementById('href').href;
var interval = setInterval(function () {
var time = --wait.innerHTML;
if (time <= 0) {
location.href = href;
clearInterval(interval);
}
}, 1000);
})();
</script>
</body>
</html>

View File

@ -0,0 +1,101 @@
<?php
$cdnurl = function_exists('config') ? config('view_replace_str.__CDN__') : '';
$publicurl = function_exists('config') ? config('view_replace_str.__PUBLIC__') : '/';
$debug = function_exists('config') ? config('app_debug') : false;
$lang = [
'An error occurred' => 'IP禁止充值',
'Home' => '返回主页',
'Feedback' => '禁止充值',
'The page you are looking for is temporarily unavailable' => '您的IP已被禁止充值如有疑问请联系管理员',
'You can return to the previous page and try again' => '你可以返回上一页重试,或直接向我们反馈错误报告'
];
$langSet = '';
if (isset($_GET['lang'])) {
$langSet = strtolower($_GET['lang']);
} elseif (isset($_COOKIE['think_var'])) {
$langSet = strtolower($_COOKIE['think_var']);
} elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
preg_match('/^([a-z\d\-]+)/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches);
$langSet = strtolower($matches[1]);
}
$langSet = $langSet && in_array($langSet, ['zh-cn', 'en']) ? $langSet : 'zh-cn';
$langSet == 'en' && $lang = array_combine(array_keys($lang), array_keys($lang));
?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title><?=$lang['An error occurred']?></title>
<meta name="robots" content="noindex,nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<link rel="shortcut icon" href="<?php echo $cdnurl;?>/assets/img/favicon.ico" />
<style>
* {-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;}
html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,abbr,address,cite,code,del,dfn,em,img,ins,kbd,q,samp,small,strong,sub,sup,var,b,i,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,caption,article,aside,canvas,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary,time,mark,audio,video {margin:0;padding:0;border:0;outline:0;vertical-align:baseline;background:transparent;}
article,aside,details,figcaption,figure,footer,header,hgroup,nav,section {display:block;}
html {font-size:16px;line-height:24px;width:100%;height:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;overflow-y:scroll;overflow-x:hidden;}
img {vertical-align:middle;max-width:100%;height:auto;border:0;-ms-interpolation-mode:bicubic;}
body {min-height:100%;background:#edf1f4;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:"Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",,Arial,sans-serif;}
.clearfix {clear:both;zoom:1;}
.clearfix:before,.clearfix:after {content:"\0020";display:block;height:0;visibility:hidden;}
.clearfix:after {clear:both;}
body.error-page-wrapper,.error-page-wrapper.preview {background-position:center center;background-repeat:no-repeat;background-size:cover;position:relative;}
.error-page-wrapper .content-container {border-radius:2px;text-align:center;box-shadow:1px 1px 1px rgba(99,99,99,0.1);padding:50px;background-color:#fff;width:100%;max-width:560px;position:absolute;left:50%;top:50%;margin-top:-220px;margin-left:-280px;}
.error-page-wrapper .content-container.in {left:0px;opacity:1;}
.error-page-wrapper .head-line {transition:color .2s linear;font-size:40px;line-height:60px;letter-spacing:-1px;margin-bottom:20px;color:#777;}
.error-page-wrapper .subheader {transition:color .2s linear;font-size:32px;line-height:46px;color:#494949;}
.error-page-wrapper .hr {height:1px;background-color:#eee;width:80%;max-width:350px;margin:25px auto;}
.error-page-wrapper .context {transition:color .2s linear;font-size:16px;line-height:27px;color:#aaa;}
.error-page-wrapper .context p {margin:0;}
.error-page-wrapper .context p:nth-child(n+2) {margin-top:16px;}
.error-page-wrapper .buttons-container {margin-top:35px;overflow:hidden;}
.error-page-wrapper .buttons-container a {transition:text-indent .2s ease-out,color .2s linear,background-color .2s linear;text-indent:0px;font-size:14px;text-transform:uppercase;text-decoration:none;color:#fff;background-color:#2ecc71;border-radius:99px;padding:8px 0 8px;text-align:center;display:inline-block;overflow:hidden;position:relative;width:45%;}
.error-page-wrapper .buttons-container a:hover {text-indent:15px;}
.error-page-wrapper .buttons-container a:nth-child(1) {float:left;}
.error-page-wrapper .buttons-container a:nth-child(2) {float:right;}
@media screen and (max-width:580px) {
.error-page-wrapper {padding:30px 5%;}
.error-page-wrapper .content-container {padding:37px;position:static;left:0;margin-top:0;margin-left:0;}
.error-page-wrapper .head-line {font-size:36px;}
.error-page-wrapper .subheader {font-size:27px;line-height:37px;}
.error-page-wrapper .hr {margin:30px auto;width:215px;}
}
@media screen and (max-width:450px) {
.error-page-wrapper {padding:30px;}
.error-page-wrapper .head-line {font-size:32px;}
.error-page-wrapper .hr {margin:25px auto;width:180px;}
.error-page-wrapper .context {font-size:15px;line-height:22px;}
.error-page-wrapper .context p:nth-child(n+2) {margin-top:10px;}
.error-page-wrapper .buttons-container {margin-top:29px;}
.error-page-wrapper .buttons-container a {float:none !important;width:65%;margin:0 auto;font-size:13px;padding:9px 0;}
.error-page-wrapper .buttons-container a:nth-child(2) {margin-top:12px;}
}
</style>
</head>
<body class="error-page-wrapper">
<div class="content-container">
<div class="head-line">
<img src="/static/common/images/error.svg" alt="" width="150" />
</div>
<div class="subheader">
<?=$debug?$message:$lang['The page you are looking for is temporarily unavailable']?>
</div>
<div class="hr"></div>
<div class="context">
<p>
<?=$lang['You can return to the previous page and try again']?>
</p>
</div>
<div class="buttons-container">
<a href="/"><?=$lang['Home']?></a>
<a href="/"><?=$lang['Feedback']?></a>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,131 @@
<?php
namespace app\common\traits;
use think\exception\HttpResponseException;
use think\Response;
/**
* Trait JumpTrait
* @package app\common\traits
*/
trait JumpTrait
{
/**
* 操作成功跳转的快捷方法
* @access protected
* @param string|null $msg 提示信息
* @param mixed|string $data 返回的数据
* @param string|null $url 跳转的 URL 地址
* @param int $wait 跳转等待时间
* @param array $header 发送的 Header 信息
* @return void
*/
protected function success(?string $msg = null, mixed $data = '', ?string $url = null, int $wait = 3, array $header = []): void
{
if (is_null($url)) {
$url = app()->request->server('HTTP_REFERER');
}elseif ($url) {
$url = (strpos($url, '://') || str_starts_with($url, '/')) ? $url : app('route')->buildUrl($url)->__toString();
}
$result = [
'code' => 1,
'msg' => $msg,
'data' => $data,
'url' => $url,
'wait' => $wait,
'__token__' => request()->buildToken('__token__'),
];
$type = $this->getResponseType();
if ($type == 'html') {
$response = view(config('app.dispatch_success_tmpl'), $result);
}else {
$response = json($result);
}
throw new HttpResponseException($response);
}
/**
* 操作错误跳转的快捷方法
* @access protected
* @param string|null $msg 提示信息
* @param mixed $data 返回的数据
* @param string|null $url 跳转的 URL 地址
* @param int $wait 跳转等待时间
* @param array $header 发送的 Header 信息
* @return void
*/
protected function error(?string $msg = null, mixed $data = '', ?string $url = null, int $wait = 3, array $header = []): void
{
if (is_null($url)) {
$url = request()->isAjax() ? '' : 'javascript:history.back(-1);';
}elseif ($url) {
$url = (strpos($url, '://') || str_starts_with($url, '/')) ? $url : app('route')->buildUrl($url)->__toString();
}
$type = $this->getResponseType();
$result = [
'code' => 0,
'msg' => $msg,
'data' => $data,
'url' => $url,
'wait' => $wait,
'__token__' => request()->buildToken('__token__'),
];
if ($type == 'html') {
$response = view(config('app.dispatch_error_tmpl'), $result);
}else {
$response = json($result);
}
throw new HttpResponseException($response);
}
/**
* 返回封装后的 API 数据到客户端
* @access protected
* @param mixed $data 要返回的数据
* @param int $code 返回的 code
* @param string|null $msg 提示信息
* @param string $type 返回数据格式
* @param array $header 发送的 Header 信息
* @return void
*/
protected function result(mixed $data, int $code = 0, ?string $msg = '', string $type = '', array $header = []): void
{
$result = [
'code' => $code,
'msg' => $msg,
'time' => time(),
'data' => $data,
];
$type = $type ?: $this->getResponseType();
$response = Response::create($result, $type)->header($header);
throw new HttpResponseException($response);
}
/**
* URL 重定向
* @access protected
* @param string $url 跳转的 URL 表达式
* @param int $code http code
* @return void
* @throws HttpResponseException
*/
protected function redirect(string $url = '', int $code = 302): void
{
$response = Response::create($url, 'redirect', $code);
throw new HttpResponseException($response);
}
/**
* 获取当前的 response 输出类型
* @access protected
* @return string
*/
protected function getResponseType(): string
{
return (request()->isJson() || request()->isAjax() || request()->isPost()) ? 'json' : 'html';
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types = 1);
namespace app\common\utils;
class Helper
{
/**
* 获取当前IP地址
* @return string
*/
public static function getIp(): string
{
return request()->ip();
}
/**
* 获取当前登录用户ID
* @return int|string
*/
public static function getAdminUid(): int|string
{
return session('admin.id') ?: 0;
}
}