增加 github actions 管理

This commit is contained in:
邹景立 2024-08-02 08:59:54 +08:00
parent ce7375ce87
commit 9969a655a2
91 changed files with 14944 additions and 0 deletions

View File

@ -0,0 +1,26 @@
on:
push:
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
name: Release
permissions: write-all
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false

8
plugin/think-library/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.git
.svn
.idea
*.cache
/vendor
/composer.lock
!.gitignore
!composer.json

View File

@ -0,0 +1,60 @@
{
"name": "zoujingli/think-library",
"license": "MIT",
"homepage": "https://thinkadmin.top",
"description": "Basic Library for ThinkAdmin",
"authors": [
{
"name": "Anyon",
"email": "zoujingli@qq.com"
}
],
"support": {
"email": "zoujingli@qq.com",
"wiki": "https://thinkadmin.top",
"forum": "https://thinkadmin.top",
"source": "https://gitee.com/zoujingli/ThinkLibrary",
"issues": "https://gitee.com/zoujingli/ThinkLibrary/issues"
},
"require": {
"php": ">=7.1",
"ext-gd": "*",
"ext-curl": "*",
"ext-json": "*",
"ext-zlib": "*",
"ext-iconv": "*",
"ext-openssl": "*",
"ext-mbstring": "*",
"symfony/process": "^5.4|^6.0|*",
"topthink/framework": "^6.0|^8.0|*",
"topthink/think-migration": "^3.0|*"
},
"autoload": {
"files": [
"src/common.php"
],
"psr-4": {
"think\\admin\\": "src"
}
},
"require-dev": {
"phpunit/phpunit": "*"
},
"autoload-dev": {
"psr-4": {
"think\\admin\\tests\\": "tests"
}
},
"extra": {
"think": {
"services": [
"think\\admin\\Library"
]
}
},
"prefer-stable": true,
"minimum-stability": "dev",
"config": {
"sort-packages": true
}
}

View File

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

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
colors="true"
bootstrap="tests/bootstrap.php"
stopOnError="false"
backupGlobals="false"
stopOnFailure="false"
processIsolation="false"
backupStaticProperties="false"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd"
cacheDirectory=".phpunit.cache"
beStrictAboutTestsThatDoNotTestAnything="false"
>
<testsuites>
<testsuite name="ThinkAdmin Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,397 @@
# ThinkLibrary for ThinkPHP6
[![Latest Stable Version](https://poser.pugx.org/zoujingli/think-library/v/stable)](https://packagist.org/packages/zoujingli/think-library)
[![Latest Unstable Version](https://poser.pugx.org/zoujingli/think-library/v/unstable)](https://packagist.org/packages/zoujingli/think-library)
[![Total Downloads](https://poser.pugx.org/zoujingli/think-library/downloads)](https://packagist.org/packages/zoujingli/think-library)
[![Monthly Downloads](https://poser.pugx.org/zoujingli/think-library/d/monthly)](https://packagist.org/packages/zoujingli/think-library)
[![Daily Downloads](https://poser.pugx.org/zoujingli/think-library/d/daily)](https://packagist.org/packages/zoujingli/think-library)
[![PHP Version](https://thinkadmin.top/static/icon/php-7.1.svg)](https://thinkadmin.top)
[![License](https://thinkadmin.top/static/icon/license-mit.svg)](https://mit-license.org)
**ThinkLibrary** 是一个针对 **ThinkPHP 6 & 8** 的封装库,它提供了完整的 **CRUD**(创建、读取、更新、删除)操作和一系列常用工具。
该库特别注重多应用支持,为开发者提供便利。前端代码的主仓库位于 **Gitee**,而 **GitHub** 则作为镜像仓库用于发布 **Composer** 包,以方便开发者下载和使用。
## 功能说明
1. 数据列表展示组件
* 功能:展示数据列表,支持分页、排序和高级搜索。
* 优化点:提供友好的用户界面和交互体验,确保数据展示的准确性和实时性。
* 高级特性:支持多种排序方式、自定义搜索字段和条件。
2. 表单处理模块
* 功能:用于创建、展示和提交表单数据。
* 优化点:表单验证和错误处理机制,确保数据的有效性和完整性。
* 高级特性:支持多种表单元素、表单验证规则和动态表单生成。
3. 数据状态快速处理模块
* 功能:根据业务需求快速更新数据状态。
* 优化点:提供简洁的接口和操作方式,支持多字段同时更新。
* 高级特性:支持条件判断和事务处理,确保数据一致性和完整性。
4. 数据安全删除模块
* 功能:根据业务需求安全地删除数据。
* 优化点:提供软删除和硬删除两种方式,确保数据彻底消失或标记为已删除。
* 高级特性:支持根据条件自动软删除、可配置的软删除标记字段。
5. 文件存储通用组件
* 功能:支持多种文件存储方式,包括本地服务存储、云存储等。
* 优化点:提供统一的接口和配置方式,方便开发者快速集成和使用。
* 高级特性:支持文件上传、下载、删除和版本控制等功能。
6. 通用数据保存更新模块
* 功能:根据 key 值及 where 条件判断数据是否存在,进行更新或新增操作。
* 优化点:提供简洁的接口和操作方式,减少冗余代码和重复工作量。
* 高级特性:支持乐观锁和悲观锁机制,确保并发控制和数据一致性。
7. 通用网络请求模块
* 功能:支持 GET、POST 和 PUT 请求,可配置请求参数、证书等。
* 优化点:提供统一的接口和配置方式,方便开发者快速发起网络请求。
* 高级特性:支持请求重试、超时设置、自动捕获异常等功能。
8. 系统参数通用 g-k-v 配置模块
* 功能:快速配置系统参数,支持长久化保存。
* 优化点:提供简洁的接口和操作方式,方便开发者管理和维护系统参数。
* 高级特性:支持参数加密存储、权限控制和日志记录等功能。
9. UTF8 加密算法支持模块
* 功能:提供 UTF8 字符串的加密和解密功能。
* 优化点:确保加密过程的安全性和数据的机密性。
* 高级特性:支持多种加密算法、密钥管理等功能。
10. 接口 CORS 跨域默认支持模块
* 功能:默认支持跨域请求,输出标准化 JSON 数据。
* 优化点:减少开发者的工作量,自动处理跨域问题。
* 高级特性:支持定制化响应头、跨域请求限制等功能。
11. 表单 CSRF 安全验证模块
* 功能:自动为表单添加 CSRF 安全验证字段,防止恶意提交。
* 优化点:简化开发者的工作流程,提高表单提交的安全性。
* 高级特性:支持自定义验证规则、多种验证方式等功能。
## 参考项目
#### ThinkAdmin - V6
* Gitee 仓库 https://gitee.com/zoujingli/ThinkAdmin
* Github 仓库 https://github.com/zoujingli/ThinkAdmin
* 体验地址账号密码都是adminhttps://v6.thinkadmin.top
## 代码仓库
**ThinkLibrary** 遵循 **MIT** 开源协议发布,并免费提供使用。
部分代码来自互联网,若有异议可以联系作者进行删除。
* 在线体验地址https://v6.thinkadmin.top (账号和密码都是 `admin`
* **Gitee** 仓库地址https://gitee.com/zoujingli/ThinkLibrary
* **Github** 仓库地址https://github.com/zoujingli/ThinkLibrary
## 使用说明
1. **依赖管理**ThinkLibrary 需要 Composer 支持进行安装和依赖管理。
2. **安装指南**:您可以使用以下命令通过 Composer 安装 ThinkLibrary`composer require zoujingli/think-library`
3. **使用示例**:在使用 ThinkLibrary 时,确保您的控制器类继承自 `think\admin\Controller`。一旦继承完成,您就可以通过 `$this` 对象访问并使用全部功能。
```php
// 定义 MyController 控制器
class MyController extend \think\admin\Controller {
// 指定当前数据表名
protected $dbQuery = '数据表名';
// 显示数据列表
public function index(){
$this->_page($this->dbQuery);
}
// 当前列表数据处理
protected function _index_page_filter(&$data){
foreach($data as &$vo){
// @todo 修改原列表
}
}
}
```
* 必要数据库表SQLsysdata 函数需要用这个表)
```sql
CREATE TABLE `system_data`
(
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL COMMENT '配置名',
`value` longtext COMMENT '配置值',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_system_data_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统-数据';
```
* 必要数据库表SQlsysoplog 函数需要用的这个表)
```sql
CREATE TABLE `system_oplog`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`node` varchar(200) NOT NULL DEFAULT '' COMMENT '当前操作节点',
`geoip` varchar(15) NOT NULL DEFAULT '' COMMENT '操作者IP地址',
`action` varchar(200) NOT NULL DEFAULT '' COMMENT '操作行为名称',
`content` varchar(1024) NOT NULL DEFAULT '' COMMENT '操作内容描述',
`username` varchar(50) NOT NULL DEFAULT '' COMMENT '操作人用户名',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统-日志';
```
* 必要数据库表SQL`sysconf`函数需要用到这个表)
```sql
CREATE TABLE `system_config`
(
`type` varchar(20) DEFAULT '' COMMENT '分类',
`name` varchar(100) DEFAULT '' COMMENT '配置名',
`value` varchar(500) DEFAULT '' COMMENT '配置值',
KEY `idx_system_config_type` (`type`),
KEY `idx_system_config_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统-配置';
```
* 系统任务列队支持需要的数据表
```sql
CREATE TABLE `system_queue` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`code` varchar(20) DEFAULT '' COMMENT '任务编号',
`title` varchar(50) NOT NULL DEFAULT '' COMMENT '任务名称',
`command` varchar(500) DEFAULT '' COMMENT '执行指令',
`exec_data` longtext COMMENT '执行参数',
`exec_time` bigint(20) unsigned DEFAULT '0' COMMENT '执行时间',
`exec_desc` varchar(500) DEFAULT '' COMMENT '状态描述',
`enter_time` bigint(20) DEFAULT '0' COMMENT '开始时间',
`outer_time` bigint(20) DEFAULT '0' COMMENT '结束时间',
`attempts` bigint(20) DEFAULT '0' COMMENT '执行次数',
`rscript` tinyint(1) DEFAULT '1' COMMENT '单例模式',
`status` tinyint(1) DEFAULT '1' COMMENT '任务状态(1新任务,2处理中,3成功,4失败)',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_system_queue_code` (`code`),
KEY `idx_system_queue_title` (`title`) USING BTREE,
KEY `idx_system_queue_status` (`status`) USING BTREE,
KEY `idx_system_queue_rscript` (`rscript`) USING BTREE,
KEY `idx_system_queue_create_at` (`create_at`) USING BTREE,
KEY `idx_system_queue_exec_time` (`exec_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统-任务';
```
#### 列表处理
```php
// 列表展示
$this->_page($dbQuery, $isPage, $isDisplay, $total);
// 列表展示搜索器(按 name、title 模糊搜索;按 status 精确搜索)
$this->_query($dbQuery)->like('name,title')->equal('status')->page();
// 对列表查询器进行二次处理
$query = $this->_query($dbQuery)->like('name, title')->equal('status');
$db = $query->db(); // @todo 这里可以对db进行操作
$this->_page($db); // 显示列表分页
```
#### 表单处理
```php
// 表单显示及数据更新
$this->_form($dbQuery, $tplFile, $pkField , $where, $data);
```
#### 删除处理
```php
// 数据删除处理
$this->_deleted($dbQuery);
```
#### 禁用启用处理
```php
// 数据禁用处理
$this->_save($dbQuery, ['status'=>'0']);
// 数据启用处理
$this->_save($dbQuery, ['status'=>'1']);
```
#### 文件存储组件( oss 及 qiniu 需要配置参数)
```php
// 配置默认存储方式
sysconf('storage.type','文件存储类型');
// 七牛云存储配置
sysconf('storage.qiniu_region', '文件存储节点');
sysconf('storage.qiniu_domain', '文件访问域名');
sysconf('storage.qiniu_bucket', '文件存储空间名称');
sysconf('storage.qiniu_is_https', '文件HTTP访问协议');
sysconf('storage.qiniu_access_key', '接口授权AccessKey');
sysconf('storage.qiniu_secret_key', '接口授权SecretKey');
// 生成文件名称(链接url或文件md5)
$filename = \think\admin\Storage::name($url, $ext, $prv, $fun);
// 获取文件内容(自动存储方式)
$result = \think\admin\Storage::get($filename);
// 保存内容到文件(自动存储方式)
$result = \think\admin\Storage::save($filename, $content);
// 判断文件是否存在
boolean \think\admin\Storage::has($filename);
// 获取文件信息
$result = \think\admin\Storage::info($filename);
//指定存储类型(调用方法)
$result = \think\admin\Storage::instance('local')->save($filename, $content);
$result = \think\admin\Storage::instance('qiniu')->save($filename, $content);
$result = \think\admin\Storage::instance('txcos')->save($filename, $content);
$result = \think\admin\Storage::instance('upyun')->save($filename, $content);
$result = \think\admin\Storage::instance('alioss')->save($filename, $content);
// 读取文件内容
$result = \think\admin\Storage::instance('local')->get($filename);
$result = \think\admin\Storage::instance('qiniu')->get($filename);
$result = \think\admin\Storage::instance('txcos')->get($filename);
$result = \think\admin\Storage::instance('upyun')->get($filename);
$result = \think\admin\Storage::instance('alioss')->get($filename);
// 生成 URL 访问地址
$result = \think\admin\Storage::instance('local')->url($filename);
$result = \think\admin\Storage::instance('qiniu')->url($filename);
$result = \think\admin\Storage::instance('txcos')->url($filename);
$result = \think\admin\Storage::instance('upyun')->url($filename);
$result = \think\admin\Storage::instance('alioss')->url($filename);
// 检查文件是否存在
boolean \think\admin\Storage::instance('local')->has($filename);
boolean \think\admin\Storage::instance('qiniu')->has($filename);
boolean \think\admin\Storage::instance('txcos')->has($filename);
boolean \think\admin\Storage::instance('upyun')->has($filename);
boolean \think\admin\Storage::instance('alioss')->has($filename);
// 生成文件信息
$resutl = \think\admin\Storage::instance('local')->info($filename);
$resutl = \think\admin\Storage::instance('qiniu')->info($filename);
$resutl = \think\admin\Storage::instance('txcos')->info($filename);
$resutl = \think\admin\Storage::instance('upyun')->info($filename);
$resutl = \think\admin\Storage::instance('alioss')->info($filename);
```
#### 通用数据保存
```php
// 指定关键列更新($where 为扩展条件)
boolean data_save($dbQuery, $data, 'pkname', $where);
```
#### 通用网络请求
```php
// 发起get请求
$result = http_get($url, $query, $options);
// 发起post请求
$result = http_post($url, $data, $options);
```
#### 系统参数配置(基于 system_config 数据表)
```php
// 设置参数
sysconf($keyname, $keyvalue);
// 获取参数
$keyvalue = sysconf($kename);
```
### 数据加密
**自研 UTF8 加密**
```php
// 自研 UTF8 字符串加密操作
$string = encode($content);
// 自研 UTF8 加密字符串解密
$content = decode($string);
```
**数据解密**
```php
use think\admin\extend\CodeExtend;
// 数据 AES-256-CBC 对称加密
$encrypt = CodeExtend::encrypt($content, $enckey);
// 数据 AES-256-CBC 对称解密
$content = CodeExtend::decrypt($encrypt, $enckey);
```
**文本转 UTF8 编码**
```php
use think\admin\extend\CodeExtend;
// 文本转 UTF8 编码
$content = CodeExtend::text2utf8($content)
```
**文本 Base64 URL 编码**
```php
use think\admin\extend\CodeExtend;
// 文本 Base64 URL 编码
$safe64 = CodeExtend::enSafe64($content)
// 文本 Base64 URL 解码
$content = CodeExtend::deSafe64($safe64)
```
### 数据压缩处理
```php
use think\admin\extend\CodeExtend;
// 数据压缩 ( 内容越大效果越好 )
$enzip = CodeExtend::enzip($content)
// 数据解压 ( 内容越大效果越好 )
$content = CodeExtend::dezip($enzip)
```
### 数组结构处理
```php
use think\admin\extend\CodeExtend;
// 二维数组 转为 立体数据结构,需要存在 id 及 pid 关系
$tree = CodeExtend::arr2tree($list);
// 二维数组 转为 扁平数据结构,需要存在 id 及 pid 关系
$tree = CodeExtend::arr2table($list);
```

View File

@ -0,0 +1,411 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin;
use think\exception\HttpResponseException;
/**
* 表单模板构建器
* 后面会在兼容的基础上慢慢完善
* @class Builder
* @deprecated 试验中建议不使用
* @package think\admin
*/
class Builder
{
/**
* 生成类型
* @var string
*/
private $type;
/**
* 显示方式
* @var string
*/
private $mode;
/**
* 当前控制器
* @var \think\admin\Controller
*/
private $class;
/**
* 提交地址
* @var string
*/
private $action;
/**
* 表单变量
* @var string
*/
private $variable = '$vo';
/**
* 表单项目
* @var array
*/
private $fields = [];
private $buttons = [];
/**
* Constructer
* @param string $type 页面类型
* @param string $mode 页面模式
* @param \think\admin\Controller $class
*/
public function __construct(string $type, string $mode, Controller $class)
{
$this->type = $type;
$this->mode = $mode;
$this->class = $class;
}
/**
* 创建表单生成器
* @param string $type 页面类型
* @param string $mode 页面模式
* @return \think\admin\Builder
*/
public static function mk(string $type = 'form', string $mode = 'modal'): Builder
{
return Library::$sapp->invokeClass(static::class, ['type' => $type, 'mode' => $mode]);
}
/**
* 设置表单地址
* @param string $url
* @return $this
*/
public function setAction(string $url): Builder
{
$this->action = $url;
return $this;
}
/**
* 设置变量名称
* @param string $name
* @return $this
*/
public function setVariable(string $name): Builder
{
$this->variable = $name;
return $this;
}
/**
* 增加输入表单元素
* @param string $name 字段名称
* @param string $title 字段标题
* @param string $subtitle 字段子标题
* @param string $remark 字段备注
* @param array $attrs 附加属性
* @return $this
*/
protected function addInput(string $name, string $title, string $subtitle = '', string $remark = '', array $attrs = []): Builder
{
$html = "\n\t\t" . '<label class="layui-form-item block relative">';
$html .= "\n\t\t\t" . sprintf('<span class="help-label %s"><b>%s</b>%s</span>', empty($attrs['required']) ? '' : 'label-required-prev', $title, $subtitle);
$html .= "\n\t\t\t" . sprintf('<input name="%s" %s placeholder="请输入%s" value="{%s.%s|default=\'\'}" class="layui-input">', $name, $this->_attrs($attrs), $title, $this->variable, $name);
if ($remark) $html .= "\n\t\t\t" . sprintf('<span class="help-block">%s</span>', $remark);
$this->fields[] = "{$html}\n\t\t</label>";
return $this;
}
/**
* 创建文本输入框架
* @param string $name 字段名称
* @param string $title 字段标题
* @param string $substr 字段子标题
* @param array $attrs 附加属性
* @return $this
*/
public function addTextArea(string $name, string $title, string $substr = '', bool $required = false, $remark = '', array $attrs = []): Builder
{
if ($required) $attrs['required'] = 'required';
$html = "\n\t\t" . '<label class="layui-form-item block relative">';
$html .= "\n\t\t\t" . sprintf('<span class="help-label %s"><b>%s</b>%s</span>', empty($attrs['required']) ? '' : 'label-required-prev', $title, $substr);
$html .= "\n\t\t\t" . sprintf('<textarea name="%s" %s placeholder="请输入%s" class="layui-textarea">{%s.%s|default=\'\'}</textarea>', $name, $this->_attrs($attrs), $title, $this->variable, $name);
if ($remark) $html .= "\n\t\t\t" . sprintf('<span class="help-block">%s</span>', $remark);
$this->fields[] = "{$html}\n\t\t</lable>";
return $this;
}
/**
* 字段属性转换
* @param array $attrs
* @param string $html
* @return string
*/
protected function _attrs(array $attrs, string $html = ''): string
{
foreach ($attrs as $k => $v) $html .= is_null($v) ? sprintf(' %s', $k) : sprintf(' %s="%s"', $k, $v);
return $html;
}
/**
* 创建 Text 输入
* @param string $name 字段名称
* @param string $title 字段标题
* @param string $substr 字段子标题
* @param string $remark 字段备注
* @param boolean $required 是否必填
* @param ?string $pattern 验证规则
* @param array $attrs 附加属性
* @return $this
*/
public function addTextInput(string $name, string $title, string $substr = '', bool $required = false, string $remark = '', ?string $pattern = null, array $attrs = []): Builder
{
$attrs['vali-name'] = $title;
if ($required) $attrs['required'] = 'required';
if (is_string($pattern)) $attrs['pattern'] = $pattern;
return $this->addInput($name, $title, $substr, $remark, $attrs);
}
/**
* 创建密钥输入框
* @param string $name 字段名称
* @param string $title 字段标题
* @param string $substr 字段子标题
* @param string $remark 字段备注
* @param boolean $required 是否必填
* @param ?string $pattern 验证规则
* @param array $attrs 附加属性
* @return $this
*/
public function addPassInput(string $name, string $title, string $substr = '', bool $required = false, string $remark = '', ?string $pattern = null, array $attrs = []): Builder
{
$attrs['type'] = 'password';
return $this->addTextInput($name, $title, $substr, $required, $remark, $pattern, $attrs);
}
/**
* 添加表单按钮
* @param string $name 按钮名称
* @param string $confirm 确认提示
* @param string $type 按钮类型
* @param string $class 按钮样式
* @param array $attrs 附加属性
* @return $this
*/
protected function addButton(string $name, string $confirm, string $type, string $class = '', array $attrs = []): Builder
{
$attrs['type'] = $type;
if ($confirm) $attrs['data-confirm'] = $confirm;
$this->buttons[] = sprintf('<button class="layui-btn %s" %s>%s</button>', $class, $this->_attrs($attrs), $name);
return $this;
}
/**
* 添加取消按钮
* @param string $name 按钮名称
* @param string $confirm 确认提示
* @return $this
*/
public function addCancelButton(string $name = '取消编辑', string $confirm = '确定要取消编辑吗?'): Builder
{
return $this->addButton($name, $confirm, 'button', 'layui-btn-danger', ['data-close' => null]);
}
/**
* 添加提交按钮
* @param string $name 按钮名称
* @param string $confirm 确认提示
* @return $this
*/
public function addSubmitButton(string $name = '保存数据', string $confirm = ''): Builder
{
return $this->addButton($name, $confirm, 'submit');
}
/**
* 添加上传单个文件
* @param string $name 字段名称
* @param string $title 字段标题
* @param string $substr 字段子标题
* @param array $attrs 附加属性
* @param string $type 上传类型
* @return $this
*/
private function _addUploadOneView(string $name, string $title, string $substr = '', array $attrs = [], string $type = 'image'): Builder
{
$attrs = array_merge($attrs, ['type' => 'text', 'placeholder' => "请上传{$title}", 'vali-name' => $title]);
$html = "\n\t\t" . '<div class="layui-form-item">';
$html .= "\n\t\t\t" . sprintf('<span class="help-label %s"><b>%s</b>%s</span>', empty($attrs['required']) ? '' : 'label-required-prev', $title, $substr);
$html .= "\n\t\t\t" . '<div class="relative block label-required-null">';
$html .= "\n\t\t\t\t" . sprintf('<input class="layui-input layui-bg-gray" name="%s" %s value="{%s.%s|default=\'\'}">', $name, $this->_attrs($attrs), $this->variable, $name);
if ($type === 'image') {
$html .= "\n\t\t\t\t" . sprintf('<a class="layui-icon layui-icon-upload input-right-icon" data-file="image" data-field="%s" data-type="gif,png,jpg,jpeg"></a>', $name);
} else {
$html .= "\n\t\t\t\t" . sprintf('<a class="layui-icon layui-icon-upload input-right-icon" data-file data-field="%s" data-type="mp4"></a>', $name);
}
$html .= "\n\t\t\t</div>\n\t\t</div>";
if ($type === 'image') {
$html .= "\n\t\t" . sprintf('<script>$("input[name=%s]").uploadOneImage()</script>', $name);
} else {
$html .= "\n\t\t" . sprintf('<script>$("input[name=%s]").uploadOneVideo()</script>', $name);
}
$this->fields[] = $html;
return $this;
}
/**
* 添加上传单图字段
* @param string $name 字段名称
* @param string $title 字段标题
* @param string $substr 字段子标题
* @param bool $required 必填字段
* @param array $attrs 附加属性
* @return $this
*/
public function addUploadOneImage(string $name, string $title, string $substr = '', bool $required = false, array $attrs = []): Builder
{
if ($required) $attrs['required'] = 'required';
return $this->_addUploadOneView($name, $title, $substr, $attrs);
}
/**
* 添加上传视频字段
* @param string $name 字段名称
* @param string $title 字段标题
* @param string $substr 字段子标题
* @param bool $required 必填字段
* @param array $attrs 附加属性
* @return $this
*/
public function addUploadOneVideo(string $name, string $title, string $substr = '', bool $required = false, array $attrs = []): Builder
{
if ($required) $attrs['required'] = 'required';
return $this->_addUploadOneView($name, $title, $substr, $attrs, 'video');
}
/**
* 创建上传多图字段
* @param string $name 字段名称
* @param string $title 字段标题
* @param string $substr 字段子标题
* @param bool $required 必填字段
* @param array $attrs 附加属性
* @return $this
*/
public function addUploadMulImage(string $name, string $title, string $substr = '', bool $required = false, array $attrs = []): Builder
{
if ($required) $attrs['required'] = 'required';
$attrs = array_merge($attrs, ['type' => 'hidden', 'placeholder' => "请上传{$title} ( 多图 )"]);
$html = "\n\t\t" . '<div class="layui-form-item">';
$html .= "\n\t\t\t" . sprintf('<span class="help-label %s"><b>%s</b>%s</span>', empty($attrs['required']) ? '' : 'label-required-prev ', $title, $substr);
$html .= "\n\t\t\t" . '<div class="layui-textarea help-images layui-bg-gray">';
$html .= "\n\t\t\t\t" . sprintf('<input name="%s" %s value="{%s.%s|default=\'\'}">', $name, $this->_attrs($attrs), $this->variable, $name);
$html .= "\n\t\t\t" . '</div>' . "\n\t\t" . '</div>';
$html .= "\n\t\t" . sprintf('<script>$("input[name=%s]").uploadMultipleImage()</script>', $name);
$this->fields[] = $html;
return $this;
}
/**
* 创建复选框字段
* @param string $name 字段名称
* @param string $title 字段标题
* @param string $substr 字段子标题
* @param string $vname 变量名称
* @param bool $required 是否必选
* @param array $attrs 附加属性
* @return $this
*/
public function addCheckInput(string $name, string $title, string $substr, string $vname, bool $required = false, array $attrs = [], string $type = 'checkbox'): Builder
{
if ($required) $attrs['required'] = 'required';
$attrs = array_merge($attrs, ['type' => $type, 'lay-ignore' => null, 'name' => $name . ($type === 'checkbox' ? '[]' : '')]);
$html = "\n\t\t" . '<div class="layui-form-item">';
$html .= "\n\t\t\t" . sprintf('<span class="help-label %s"><b>%s</b>%s</span>', empty($attrs['required']) ? '' : ' label-required-prev', $title, $substr);
$html .= "\n\t\t\t" . '<div class="layui-textarea help-checks layui-bg-gray">';
$html .= "\n\t\t\t\t" . sprintf('<!--{foreach $%s as $k=>$v}item-->', $vname);
$html .= "\n\t\t\t\t" . sprintf('<label class="think-%s label-required-null">', $type);
$html .= "\n\t\t\t\t\t" . sprintf('<!--if{if isset(%s.types) and is_array(%s.types) and in_array($k,%s.types)}-->', $this->variable, $this->variable, $this->variable);
$html .= "\n\t\t\t\t\t" . sprintf('<input value="{$k|default=\'\'}" %s checked> {$v|default=\'\'}', $this->_attrs($attrs));
$html .= "\n\t\t\t\t\t" . '<!--{else}else-->';
$html .= "\n\t\t\t\t\t" . sprintf('<input value="{$k|default=\'\'}" %s> {$v|default=\'\'}', $this->_attrs($attrs)) . "\n";
$html .= "\n\t\t\t\t\t" . '<!--{/if}if-->';
$html .= "\n\t\t\t\t" . '</label>';
$html .= "\n\t\t\t\t" . '<!--{/foreach}end-->';
$this->fields[] = $html . "\n\t\t\t</div>\n\t\t</div>";
return $this;
}
/**
* 添加单选框架字段
* @param string $name 字段名称
* @param string $title 字段标题
* @param string $substr 字段子标题
* @param string $vname 变量名称
* @param bool $required 是否必选
* @param array $attrs 附加属性
* @return $this
*/
public function addRadioInput(string $name, string $title, string $substr, string $vname, bool $required = false, array $attrs = []): Builder
{
return $this->addCheckInput($name, $title, $substr, $vname, $required, $attrs, 'radio');
}
/**
* 显示模板内容
* @return mixed
*/
public function fetch(array $vars = [])
{
$html = '';
$type = "{$this->type}.{$this->mode}";
if ($type === 'form.page') {
$html = $this->_buildFormPage();
} elseif ($type === 'form.modal') {
$html = $this->_buildFormModal();
}
foreach ($this->class as $k => $v) $vars[$k] = $v;
throw new HttpResponseException(display($html, $vars));
}
/**
* 生成弹层表单模板
* @return string
*/
private function _buildFormModal(): string
{
$html = sprintf('<form action="%s" method="post" data-auto="true" class="layui-form layui-card">', $this->action ?? url()->build());
$html .= "\n\t" . '<div class="layui-card-body padding-left-40">' . join("\n", $this->fields);
if (count($this->buttons)) {
$html .= "\n\n\t\t" . '<div class="hr-line-dashed"></div>';
$html .= "\n\t\t" . sprintf('{notempty name="vo.id"}<input type="hidden" value="{%s.id}" name="id">{/notempty}', $this->variable);
$html .= "\n\t\t" . sprintf('<div class="layui-form-item text-center">%s</div>', "\n\t\t\t" . join("\n\t\t\t", $this->buttons) . "\n\t\t");
$html .= "\n\t" . '</div>';
}
return $html . "\n</form>";
}
/**
* 生成页面表单模板
* @return string
*/
private function _buildFormPage(): string
{
return '';
}
}

View File

@ -0,0 +1,124 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin;
use think\admin\service\ProcessService;
use think\admin\service\QueueService;
use think\console\Input;
use think\console\Output;
/**
* 自定义指令基类
* @class Command
* @package think\admin
*/
abstract class Command extends \think\console\Command
{
/**
* 任务控制服务
* @var QueueService
*/
protected $queue;
/**
* 进程控制服务
* @var ProcessService
*/
protected $process;
/**
* 初始化指令变量
* @param \think\console\Input $input
* @param \think\console\Output $output
* @return $this
* @throws \think\admin\Exception
*/
protected function initialize(Input $input, Output $output): Command
{
$this->queue = QueueService::instance();
$this->process = ProcessService::instance();
if (defined('WorkQueueCode') && $this->queue->code !== WorkQueueCode) {
$this->queue->initialize(WorkQueueCode);
}
return $this;
}
/**
* 设置失败消息并结束进程
* @param string $message 消息内容
* @throws \think\admin\Exception
*/
protected function setQueueError(string $message)
{
if (defined('WorkQueueCode')) {
$this->queue->error($message);
} else {
$this->process->message($message);
exit(0);
}
}
/**
* 设置成功消息并结束进程
* @param string $message 消息内容
* @throws \think\admin\Exception
*/
protected function setQueueSuccess(string $message)
{
if (defined('WorkQueueCode')) {
$this->queue->success($message);
} else {
$this->process->message($message);
exit(0);
}
}
/**
* 设置进度消息并继续执行
* @param null|string $message 进度消息
* @param null|string $progress 进度数值
* @param integer $backline 回退行数
* @return static
* @throws \think\admin\Exception
*/
protected function setQueueProgress(?string $message = null, ?string $progress = null, int $backline = 0): Command
{
if (defined('WorkQueueCode')) {
$this->queue->progress(2, $message, $progress, $backline);
} elseif (is_string($message)) {
$this->process->message($message, $backline);
}
return $this;
}
/**
* 更新任务进度
* @param integer $total 记录总和
* @param integer $count 当前记录
* @param string $message 文字描述
* @param integer $backline 回退行数
* @return static
* @throws \think\admin\Exception
*/
public function setQueueMessage(int $total, int $count, string $message = '', int $backline = 0): Command
{
$this->queue->message($total, $count, $message, $backline);
return $this;
}
}

View File

@ -0,0 +1,324 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin;
use stdClass;
use think\admin\extend\JwtExtend;
use think\admin\helper\DeleteHelper;
use think\admin\helper\FormHelper;
use think\admin\helper\PageHelper;
use think\admin\helper\QueryHelper;
use think\admin\helper\SaveHelper;
use think\admin\helper\TokenHelper;
use think\admin\helper\ValidateHelper;
use think\admin\service\NodeService;
use think\admin\service\QueueService;
use think\App;
use think\db\BaseQuery;
use think\exception\HttpResponseException;
use think\Model;
use think\Request;
/**
* 标准控制器基类
* @class Controller
* @package think\admin
*/
class Controller extends stdClass
{
/**
* 应用容器
* @var App
*/
public $app;
/**
* 请求GET参数
* @var array
*/
public $get = [];
/**
* 当前功能节点
* @var string
*/
public $node;
/**
* 请求参数对象
* @var Request
*/
public $request;
/**
* 表单CSRF验证状态
* @var boolean
*/
public $csrf_state = false;
/**
* 表单CSRF验证消息
* @var string
*/
public $csrf_message;
/**
* Constructor.
* @param App $app
*/
public function __construct(App $app)
{
if (in_array($app->request->action(), get_class_methods(__CLASS__))) {
$this->error('禁止访问内置方法!');
}
$this->get = $app->request->get();
$this->app = $app->bind('think\admin\Controller', $this);
$this->node = NodeService::getCurrent();
$this->request = $this->app->request;
$this->initialize();
}
/**
* 控制器初始化
*/
protected function initialize()
{
}
/**
* 返回失败的内容
* @param mixed $info 消息内容
* @param mixed $data 返回数据
* @param mixed $code 返回代码
*/
public function error($info, $data = '{-null-}', $code = 0): void
{
$this->success($info, $data, $code);
}
/**
* 返回成功的内容
* @param mixed $info 消息内容
* @param mixed $data 返回数据
* @param mixed $code 返回代码
*/
public function success($info, $data = '{-null-}', $code = 1): void
{
if ($data === '{-null-}') $data = new stdClass();
$result = ['code' => $code, 'info' => is_string($info) ? lang($info) : $info, 'data' => $data];
if (JwtExtend::isRejwt()) $result['token'] = JwtExtend::token();
throw new HttpResponseException(json($result));
}
/**
* URL重定向
* @param string $url 跳转链接
* @param integer $code 跳转代码
*/
public function redirect(string $url, int $code = 302): void
{
throw new HttpResponseException(redirect($url, $code));
}
/**
* 返回视图内容
* @param string $tpl 模板名称
* @param array $vars 模板变量
* @param null|string $node 授权节点
*/
public function fetch(string $tpl = '', array $vars = [], ?string $node = null): void
{
if (JwtExtend::$sessionId) {
JwtExtend::fetch($this, $vars);
} else {
foreach ($this as $name => $value) {
$vars[$name] = $value;
}
if ($this->csrf_state) {
TokenHelper::fetch($tpl, $vars, $node);
} else {
throw new HttpResponseException(view($tpl, $vars));
}
}
}
/**
* 模板变量赋值
* @param mixed $name 要显示的模板变量
* @param mixed $value 变量的值
* @return $this
*/
public function assign($name, $value = ''): Controller
{
if (is_string($name)) {
$this->$name = $value;
} elseif (is_array($name)) {
foreach ($name as $k => $v) {
if (is_string($k)) $this->$k = $v;
}
}
return $this;
}
/**
* 数据回调处理机制
* @param string $name 回调方法名称
* @param mixed $one 回调引用参数1
* @param mixed $two 回调引用参数2
* @param mixed $thr 回调引用参数3
* @return boolean
*/
public function callback(string $name, &$one = [], &$two = [], &$thr = []): bool
{
if (is_callable($name)) return call_user_func($name, $this, $one, $two, $thr);
foreach (["_{$this->app->request->action()}{$name}", $name] as $method) {
if (method_exists($this, $method) && false === $this->$method($one, $two, $thr)) {
return false;
}
}
return true;
}
/**
* 快捷查询逻辑器
* @param BaseQuery|Model|string $dbQuery
* @param array|string|null $input
* @return QueryHelper
* @throws \think\db\exception\DbException
*/
protected function _query($dbQuery, $input = null): QueryHelper
{
return QueryHelper::instance()->init($dbQuery, $input);
}
/**
* 快捷分页逻辑器
* @param BaseQuery|Model|string $dbQuery
* @param boolean|integer $page 是否分页或指定分页
* @param boolean $display 是否渲染模板
* @param boolean|integer $total 集合分页记录数
* @param integer $limit 集合每页记录数
* @param string $template 模板文件名称
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
protected function _page($dbQuery, $page = true, bool $display = true, $total = false, int $limit = 0, string $template = ''): array
{
return PageHelper::instance()->init($dbQuery, $page, $display, $total, $limit, $template);
}
/**
* 快捷表单逻辑器
* @param BaseQuery|Model|string $dbQuery
* @param string $template 模板名称
* @param string $field 指定数据主键
* @param mixed $where 额外更新条件
* @param array $data 表单扩展数据
* @return array|boolean
* @throws \think\admin\Exception
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
protected function _form($dbQuery, string $template = '', string $field = '', $where = [], array $data = [])
{
return FormHelper::instance()->init($dbQuery, $template, $field, $where, $data);
}
/**
* 快捷输入并验证( 支持 规则 # 别名
* @param array $rules 验证规则( 验证信息数组
* @param string|array $type 输入方式 ( post. get. )
* @param callable|null $callable 异常处理操作
* @return array
*/
protected function _vali(array $rules, $type = '', ?callable $callable = null): array
{
return ValidateHelper::instance()->init($rules, $type, $callable);
}
/**
* 快捷更新逻辑器
* @param BaseQuery|Model|string $dbQuery
* @param array $data 表单扩展数据
* @param string $field 数据对象主键
* @param mixed $where 额外更新条件
* @return boolean
* @throws \think\db\exception\DbException
*/
protected function _save($dbQuery, array $data = [], string $field = '', $where = []): bool
{
return SaveHelper::instance()->init($dbQuery, $data, $field, $where);
}
/**
* 快捷删除逻辑器
* @param BaseQuery|Model|string $dbQuery
* @param string $field 数据对象主键
* @param mixed $where 额外更新条件
* @return boolean
* @throws \think\db\exception\DbException
*/
protected function _delete($dbQuery, string $field = '', $where = []): bool
{
return DeleteHelper::instance()->init($dbQuery, $field, $where);
}
/**
* 检查表单令牌验证
* @param boolean $return 是否返回结果
* @return boolean
*/
protected function _applyFormToken(bool $return = false): bool
{
return TokenHelper::instance()->init($return);
}
/**
* 创建异步任务并返回任务编号
* @param string $title 任务名称
* @param string $command 执行内容
* @param integer $later 延时执行时间
* @param array $data 任务附加数据
* @param integer $rscript 任务类型(0单例,1多例)
* @param integer $loops 循环等待时间
*/
protected function _queue(string $title, string $command, int $later = 0, array $data = [], int $rscript = 0, int $loops = 0)
{
try {
$queue = QueueService::register($title, $command, $later, $data, $rscript, $loops);
$this->success('创建任务成功!', $queue->code);
} catch (Exception $exception) {
$code = $exception->getData();
if (is_string($code) && stripos($code, 'Q') === 0) {
$this->success('任务已经存在,无需再次创建!', $code);
} else {
$this->error($exception->getMessage());
}
} catch (HttpResponseException $exception) {
throw $exception;
} catch (\Exception $exception) {
trace_file($exception);
$this->error(lang('创建任务失败,%s', [$exception->getMessage()]));
}
}
}

View File

@ -0,0 +1,65 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin;
/**
* 自定义数据异常
* @class Exception
* @package think\admin
*/
class Exception extends \Exception
{
/**
* 异常数据对象
* @var mixed
*/
protected $data = [];
/**
* Exception constructor.
* @param string $message
* @param integer $code
* @param mixed $data
*/
public function __construct($message = "", $code = 0, $data = [])
{
parent::__construct($message);
$this->code = $code;
$this->data = $data;
$this->message = $message;
}
/**
* 获取异常停止数据
* @return mixed
*/
public function getData()
{
return $this->data;
}
/**
* 设置异常停止数据
* @param mixed $data
*/
public function setData($data)
{
$this->data = $data;
}
}

View File

@ -0,0 +1,124 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin;
use think\admin\extend\VirtualModel;
use think\App;
use think\Container;
use think\db\BaseQuery;
use think\db\Mongo;
use think\db\Query;
use think\Model;
/**
* 控制器助手
* @class Helper
* @package think\admin
*/
abstract class Helper
{
/**
* 应用容器
* @var App
*/
public $app;
/**
* 控制器实例
* @var Controller
*/
public $class;
/**
* 当前请求方式
* @var string
*/
public $method;
/**
* 自定输出格式
* @var string
*/
public $output;
/**
* Helper constructor.
* @param App $app
* @param Controller $class
*/
public function __construct(App $app, Controller $class)
{
$this->app = $app;
$this->class = $class;
// 计算指定输出格式
$output = $app->request->request('output', 'default');
$method = $app->request->method() ?: ($app->runningInConsole() ? 'cli' : 'nil');
$this->output = strtolower("{$method}.{$output}");
}
/**
* 实例对象反射
* @param array $args
* @return static
*/
public static function instance(...$args): Helper
{
return Container::getInstance()->invokeClass(static::class, $args);
}
/**
* 获取数据库查询对象
* @param BaseQuery|Model|string $query
* @return Query|Mongo|BaseQuery
*/
public static function buildQuery($query)
{
if (is_string($query)) {
return static::buildModel($query)->db();
}
if ($query instanceof Model) return $query->db();
if ($query instanceof BaseQuery && !$query->getModel()) {
$name = $query->getConfig('name') ?: '';
if (is_string($name) && strlen($name) > 0) {
$name = config("database.connections.{$name}") ? $name : '';
}
$query->model(static::buildModel($query->getName(), [], $name));
}
return $query;
}
/**
* 动态创建模型对象
* @param mixed $name 模型名称
* @param array $data 初始数据
* @param mixed $conn 指定连接
* @return Model
*/
public static function buildModel(string $name, array $data = [], string $conn = ''): Model
{
if (strpos($name, '\\') !== false) {
if (class_exists($name)) {
$model = new $name($data);
if ($model instanceof Model) return $model;
}
$name = basename(str_replace('\\', '/', $name));
}
return VirtualModel::mk($name, $data, $conn);
}
}

View File

@ -0,0 +1,155 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin;
use think\admin\service\RuntimeService;
use think\admin\support\command\Database;
use think\admin\support\command\Package;
use think\admin\support\command\Publish;
use think\admin\support\command\Queue;
use think\admin\support\command\Replace;
use think\admin\support\command\Sysmenu;
use think\admin\support\middleware\JwtSession;
use think\admin\support\middleware\MultAccess;
use think\admin\support\middleware\RbacAccess;
use think\exception\HttpResponseException;
use think\middleware\LoadLangPack;
use think\Request;
use think\Response;
use think\Service;
/**
* 模块注册服务
* @class Library
* @package think\admin
*/
class Library extends Service
{
/** @var \think\App */
public static $sapp;
/**
* 启动服务
* @return void
*/
public function boot()
{
// 静态应用赋值
static::$sapp = $this->app;
// 注册 ThinkAdmin 指令
$this->commands([
Queue::class,
Package::class,
Sysmenu::class,
Publish::class,
Replace::class,
Database::class,
]);
// 动态应用运行参数
RuntimeService::apply();
// 请求初始化处理
$this->app->event->listen('HttpRun', function (Request $request) {
// 运行环境配置同步
RuntimeService::sync();
// 配置默认输入过滤
$request->filter([static function ($value) {
return is_string($value) ? xss_safe($value) : $value;
}]);
// 判断访问模式兼容处理
if ($this->app->runningInConsole()) {
// 兼容 CLI 访问控制器
if (empty($_SERVER['REQUEST_URI']) && isset($_SERVER['argv'][1])) {
$request->setPathinfo($_SERVER['argv'][1]);
}
} else {
// 兼容 HTTP 调用 Console 后 URL 问题
$request->setHost($request->host());
}
// 注册多应用中间键
$this->app->middleware->add(MultAccess::class);
});
// 请求结束后处理
$this->app->event->listen('HttpEnd', static function () {
function_exists('sysvar') && sysvar('', '');
});
}
/**
* 初始化服务
* @return void
*/
public function register()
{
// 动态加载全局配置
[$dir, $ext] = [$this->app->getBasePath(), $this->app->getConfigExt()];
foreach (glob("{$dir}*/sys{$ext}") as $file) include_once $file;
if (is_file($file = "{$dir}common{$ext}")) include_once $file;
if (is_file($file = "{$dir}provider{$ext}")) $this->app->bind(include $file);
if (is_file($file = "{$dir}event{$ext}")) $this->app->loadEvent(include $file);
if (is_file($file = "{$dir}middleware{$ext}")) $this->app->middleware->import(include $file, 'app');
// 终端 HTTP 访问时特殊处理
if (!$this->app->runningInConsole()) {
// 动态注释 CORS 跨域处理
$this->app->middleware->add(function (Request $request, \Closure $next): Response {
$header = ['X-Frame-Options' => $this->app->config->get('app.cors_frame') ?: 'sameorigin'];
// HTTP.CORS 跨域规则配置
if ($this->app->config->get('app.cors_on', true) && ($origin = $request->header('origin', '-')) !== '-') {
if (is_string($hosts = $this->app->config->get('app.cors_host', []))) $hosts = str2arr($hosts);
if (empty($hosts) || in_array(parse_url(strtolower($origin), PHP_URL_HOST), $hosts)) {
$headers = $this->app->config->get('app.cors_headers', 'Api-Name,Api-Type,Api-Token,Jwt-Token,User-Form-Token,User-Token,Token');
$header['Access-Control-Allow-Origin'] = $origin;
$header['Access-Control-Allow-Methods'] = $this->app->config->get('app.cors_methods', 'GET,PUT,POST,PATCH,DELETE');
$header['Access-Control-Allow-Headers'] = "Authorization,Content-Type,If-Match,If-Modified-Since,If-None-Match,If-Unmodified-Since,X-Requested-With,{$headers}";
$header['Access-Control-Allow-Credentials'] = 'true';
$header['Access-Control-Expose-Headers'] = $headers;
}
}
// 跨域预请求状态处理
if ($request->isOptions()) {
throw new HttpResponseException(response()->code(204)->header($header));
} else {
return $next($request)->header($header);
}
});
// 初始化会话和语言包
$isapi = $this->app->request->header('api-token') !== null;
$agent = preg_replace('|\s+|', '', $this->app->request->header('user-agent', ''));
$isrpc = is_numeric(stripos($agent, 'think-admin-jsonrpc')) || is_numeric(stripos($agent, 'PHPYarRPC'));
if (empty($isapi) && empty($isrpc) && empty($this->app->request->get('not_init_session'))) {
// 非接口模式,注册会话中间键
$this->app->middleware->add(JwtSession::class);
// 启用会话后,注册语言包中间键
$this->app->middleware->add(LoadLangPack::class);
}
// 注册权限验证中间键
$this->app->middleware->add(RbacAccess::class, 'route');
}
}
}

View File

@ -0,0 +1,117 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin;
use think\admin\helper\QueryHelper;
/**
* 基础模型类
* @class Model
* @mixin \think\db\Query
* @package think\admin
*
* --- 静态助手调用
* @method static bool mSave(array $data = [], string $field = '', mixed $where = []) 快捷更新
* @method static bool mDelete(string $field = '', mixed $where = []) 快捷删除
* @method static bool|array mForm(string $template = '', string $field = '', mixed $where = [], array $data = []) 快捷表单
* @method static bool|integer mUpdate(array $data = [], string $field = '', mixed $where = []) 快捷保存
* @method static QueryHelper mQuery($input = null, callable $callable = null) 快捷查询
*/
abstract class Model extends \think\Model
{
/**
* 日志类型
* @var string
*/
protected $oplogType;
/**
* 日志名称
* @var string
*/
protected $oplogName;
/**
* 日志过滤
* @var callable
*/
public static $oplogCall;
/**
* 创建模型实例
* @template t of static
* @param mixed $data
* @return t|static
*/
public static function mk($data = [])
{
return new static($data);
}
/**
* 创建查询实例
* @param array $data
* @return \think\db\Query|\think\db\Mongo
*/
public static function mq(array $data = [])
{
return static::mk($data)->newQuery();
}
/**
* 调用魔术方法
* @param string $method 方法名称
* @param array $args 调用参数
* @return $this|false|mixed
*/
public function __call($method, $args)
{
$oplogs = [
'onAdminSave' => "修改%s[%s]状态",
'onAdminUpdate' => "更新%s[%s]记录",
'onAdminInsert' => "增加%s[%s]成功",
"onAdminDelete" => "删除%s[%s]成功",
];
if (isset($oplogs[$method])) {
if ($this->oplogType && $this->oplogName) {
$changeIds = $args[0] ?? '';
if (is_callable(static::$oplogCall)) {
$changeIds = call_user_func(static::$oplogCall, $method, $changeIds, $this);
}
sysoplog($this->oplogType, lang($oplogs[$method], [lang($this->oplogName), $changeIds]));
}
return $this;
} else {
return parent::__call($method, $args);
}
}
/**
* 静态魔术方法
* @param string $method 方法名称
* @param array $args 调用参数
* @return mixed|false|integer|QueryHelper
*/
public static function __callStatic($method, $args)
{
return QueryHelper::make(static::class, $method, $args, function ($method, $args) {
return parent::__callStatic($method, $args);
});
}
}

View File

@ -0,0 +1,235 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin;
use think\admin\service\ModuleService;
use think\admin\service\NodeService;
use think\App;
use think\Service;
/**
* 插件注册服务
*
* @class Plugin
* @package think\admin\service
*
* @method string getAppCode() static 获取插件编号
* @method string getAppName() static 获取插件名称
* @method string getAppPath() static 获取插件路径
* @method string getAppSpace() static 获取插件空间名
* @method string getAppPackage() static 获取插件安装包
*/
abstract class Plugin extends Service
{
/**
* 必填,插件包名
* @var string
*/
protected $package = '';
/**
* 必填,插件编码
* @var string
*/
protected $appCode = '';
/**
* 必填,插件名称
* @var string
*/
protected $appName = '';
/**
* 可选,插件目录
* @var string
*/
protected $appPath = '';
/**
* 可选,插件别名
* @var string
*/
protected $appAlias = '';
/**
* 可选,命名空间
* @var string
*/
protected $appSpace = '';
/**
* 可选,注册服务
* @var string
*/
protected $appService = '';
/**
* 插件配置
* @var array
*/
private static $addons = [];
/**
* 自动注册插件
* @param \think\App $app
*/
public function __construct(App $app)
{
parent::__construct($app);
// 获取基础服务类
$ref = new \ReflectionClass(static::class);
// 应用服务注册类
if (empty($this->appService)) {
$this->appService = static::class;
}
// 应用命名空间名
if (empty($this->appSpace)) {
$this->appSpace = $ref->getNamespaceName();
}
// 应用插件路径计算
if (empty($this->appPath) || !is_dir($this->appPath)) {
$this->appPath = dirname($ref->getFileName());
}
// 应用插件包名计算
if (empty($this->package) && ($path = $ref->getFileName())) {
for ($level = 1; $level <= 3; $level++) {
if (is_file($file = dirname($path, $level) . '/composer.json')) {
$this->package = json_decode(file_get_contents($file), true)['name'] ?? '';
break;
}
}
}
// 应用插件计算名称及别名
$attr = explode('\\', $ref->getNamespaceName());
if ($attr[0] === NodeService::space()) array_shift($attr);
$this->appCode = $this->appCode ?: join('-', $attr);
if ($this->appCode === $this->appAlias) $this->appAlias = '';
if (is_dir($this->appPath)) {
// 写入插件参数信息
self::$addons[$this->appCode] = [
'name' => $this->appName,
'path' => realpath($this->appPath) . DIRECTORY_SEPARATOR,
'alias' => $this->appAlias,
'space' => $this->appSpace ?: NodeService::space($this->appCode),
'package' => $this->package,
'service' => $this->appService
];
// 插件别名动态设置
if (!empty($this->appAlias) && $this->appCode !== $this->appAlias) {
Library::$sapp->config->set([
'app_map' => array_merge(Library::$sapp->config->get('app.app_map', []), [
$this->appAlias => $this->appCode
]),
], 'app');
}
}
}
/**
* 注册应用启动
* @return void
*/
public function boot(): void
{
}
/**
* 获取插件及安装信息
* @param ?string $code 指定插件编号
* @param boolean $append 关联安装数据
* @return ?array
*/
public static function get(?string $code = null, bool $append = false): ?array
{
// 读取插件原始信息
$data = empty($code) ? self::$addons : (self::$addons[$code] ?? null);
if (empty($data) || empty($append)) return $data;
// 关联插件安装信息
$versions = ModuleService::getLibrarys();
return empty($code) ? array_map(static function ($item) use ($versions) {
$item['install'] = $versions[$item['package']] ?? [];
if (empty($item['name'])) $item['name'] = $item['install']['name'] ?? '';
return $item;
}, $data) : $data + ['install' => $versions[$data['package']] ?? []];
}
/**
* 获取对象内部属性
* @param string $name
* @return null
*/
public function __get(string $name)
{
return $this->$name ?? '';
}
/**
* 静态调用方法兼容
* @param string $method
* @param array $arguments
* @return array|string|null
* @throws \think\admin\Exception
*/
public static function __callStatic(string $method, array $arguments)
{
switch (strtolower($method)) {
case 'all':
return self::get(...$arguments);
case 'getappcode':
return app(static::class)->appCode;
case 'getappname':
return app(static::class)->appName;
case 'getapppath':
return app(static::class)->appPath;
case 'getappspace':
return app(static::class)->appSpace;
case 'getapppackage';
return app(static::class)->package;
default:
$class = basename(str_replace('\\', '/', static::class));
throw new Exception("method not exists: {$class}::{$method}()");
}
}
/**
* 魔术方法调用兼容处理
* @param string $method
* @param array $arguments
* @return array|null
* @throws \think\admin\Exception
*/
public function __call(string $method, array $arguments)
{
return self::__callStatic($method, $arguments);
}
/**
* 定义插件菜单
* @return array 一级或二级菜单
*/
abstract public static function menu(): array;
}

View File

@ -0,0 +1,127 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin;
use think\admin\service\ProcessService;
use think\admin\service\QueueService;
use think\App;
/**
* 任务基础类
* @class Queue
* @package think\admin
*/
abstract class Queue
{
/**
* 应用实例
* @var App
*/
protected $app;
/**
* 任务控制服务
* @var QueueService
*/
protected $queue;
/**
* 进程控制服务
* @var ProcessService
*/
protected $process;
/**
* Constructor.
* @param App $app
* @param ProcessService $process
*/
public function __construct(App $app, ProcessService $process)
{
$this->app = $app;
$this->process = $process;
}
/**
* 初始化任务数据
* @param QueueService $queue
* @return $this
*/
public function initialize(QueueService $queue): Queue
{
$this->queue = $queue;
return $this;
}
/**
* 执行任务处理内容
* @param array $data
* @return void|string
*/
abstract public function execute(array $data = []);
/**
* 设置失败的消息
* @param string $message 消息内容
* @throws Exception
*/
protected function setQueueError(string $message)
{
$this->queue->error($message);
}
/**
* 设置成功的消息
* @param string $message 消息内容
* @throws Exception
*/
protected function setQueueSuccess(string $message)
{
$this->queue->success($message);
}
/**
* 更新任务进度
* @param integer $total 记录总和
* @param integer $count 当前记录
* @param string $message 文字描述
* @param integer $backline 回退行数
* @return static
* @throws \think\admin\Exception
*/
protected function setQueueMessage(int $total, int $count, string $message = '', int $backline = 0): Queue
{
$this->queue->message($total, $count, $message, $backline);
return $this;
}
/**
* 设置任务的进度
* @param ?string $message 进度消息
* @param ?string $progress 进度数值
* @param integer $backline 回退行数
* @return Queue
* @throws \think\admin\Exception
*/
protected function setQueueProgress(?string $message = null, ?string $progress = null, int $backline = 0): Queue
{
$this->queue->progress(2, $message, $progress, $backline);
return $this;
}
}

View File

@ -0,0 +1,64 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin;
use think\App;
use think\Container;
/**
* 自定义服务基类
* @class Service
* @package think\admin
*/
abstract class Service
{
/**
* 应用实例
* @var App
*/
protected $app;
/**
* Constructor.
* @param App $app
*/
public function __construct(App $app)
{
$this->app = $app;
$this->initialize();
}
/**
* 初始化服务
*/
protected function initialize()
{
}
/**
* 静态实例对象
* @param array $var 实例参数
* @param boolean $new 创建新实例
* @return static|mixed
*/
public static function instance(array $var = [], bool $new = false)
{
return Container::getInstance()->make(static::class, $var, $new);
}
}

View File

@ -0,0 +1,203 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin;
use think\admin\contract\StorageInterface;
use think\admin\storage\LocalStorage;
use think\Container;
/**
* 文件存储引擎管理
* @class Storage
* @package think\admin
* @method static array info($name, $safe = false, $attname = null) 文件存储信息
* @method static array set($name, $file, $safe = false, $attname = null) 储存文件
* @method static string url($name, $safe = false, $attname = null) 获取文件链接
* @method static string get($name, $safe = false) 读取文件内容
* @method static string path($name, $safe = false) 文件存储路径
* @method static boolean del($name, $safe = false) 删除存储文件
* @method static boolean has($name, $safe = false) 检查是否存在
* @method static string upload() 获取上传地址
*/
abstract class Storage
{
/**
* 实例化存储操作对象
* @param ?string $name 驱动名称
* @param ?string $class 驱动类名
* @return \think\admin\contract\StorageInterface
* @throws \think\admin\Exception
*/
public static function instance(?string $name = null, ?string $class = null): StorageInterface
{
try {
if (is_null($class)) {
$type = ucfirst(strtolower($name ?: sysconf('storage.type|raw')));
$class = "think\\admin\\storage\\{$type}Storage";
}
if (class_exists($class)) return Container::getInstance()->make($class);
throw new Exception("Storage driver [{$class}] does not exist.");
} catch (Exception $exception) {
throw $exception;
} catch (\Exception $exception) {
throw new Exception($exception->getMessage());
}
}
/**
* 获取文件相对名称
* @param string $url 文件访问链接
* @param string $ext 文件后缀名称
* @param string $pre 文件存储前缀
* @param string $fun 名称规则方法
* @return string
*/
public static function name(string $url, string $ext = '', string $pre = '', string $fun = 'md5'): string
{
[$hah, $ext] = [$fun($url), trim($ext ?: pathinfo($url, 4), '.\\/')];
$attr = [trim($pre, '.\\/'), substr($hah, 0, 2), substr($hah, 2, 30)];
return trim(join('/', $attr), '/') . '.' . strtolower($ext ?: 'tmp');
}
/**
* 下载文件到本地
* @param string $url 文件URL地址
* @param boolean $force 是否强制下载
* @param integer $expire 文件保留时间
* @return array
*/
public static function down(string $url, bool $force = false, int $expire = 0): array
{
try {
$local = LocalStorage::instance();
$filename = static::name($url, '', 'down/');
if (empty($force) && $local->has($filename)) {
if ($expire < 1 || filemtime($local->path($filename)) + $expire > time()) {
return $local->info($filename);
}
}
return $local->set($filename, static::curlGet($url));
} catch (\Exception $exception) {
return ['url' => $url, 'hash' => md5($url), 'key' => $url, 'file' => $url];
}
}
/**
* 获取后缀类型
* @param array|string $exts 文件后缀
* @param array $mime 文件信息
* @return string
*/
public static function mime($exts, array $mime = []): string
{
$mimes = static::mimes();
foreach (is_string($exts) ? explode(',', $exts) : $exts as $ext) {
$mime[] = $mimes[strtolower($ext)] ?? 'application/octet-stream';
}
return join(',', array_unique($mime));
}
/**
* 获取所有类型
* @return array
*/
public static function mimes(): array
{
static $mimes = [];
if (count($mimes) > 0) return $mimes;
return $mimes = include __DIR__ . '/storage/bin/mimes.php';
}
/**
* 获取存储类型
* @return array
*/
public static function types(): array
{
return [
'local' => lang('本地服务器存储'),
'alist' => lang('自建Alist存储'),
'qiniu' => lang('七牛云对象存储'),
'upyun' => lang('又拍云USS存储'),
'txcos' => lang('腾讯云COS存储'),
'alioss' => lang('阿里云OSS存储'),
];
}
/**
* 使用CURL读取网络资源
* @param string $url 资源地址
* @return string
*/
public static function curlGet(string $url): string
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$body = curl_exec($ch) ?: '';
curl_close($ch);
return $body;
}
/**
* 静态访问启用
* @param string $method 方法名称
* @param array $arguments 调用参数
* @return mixed
* @throws \think\admin\Exception
*/
public static function __callStatic(string $method, array $arguments)
{
if (method_exists($storage = static::instance(), $method)) {
return call_user_func_array([$storage, $method], $arguments);
} else {
throw new Exception("method not exists: " . get_class($storage) . "->{$method}()");
}
}
/**
* 图片数据存储
* @param string $base64 图片内容
* @param string $prefix 保存前缀
* @param boolean $safemode 安全模式
* @return array [ url => URL ]
* @throws \think\admin\Exception
*/
public static function saveImage(string $base64, string $prefix = 'image', bool $safemode = false): array
{
if (preg_match('|^data:image/(.*?);base64,|i', $base64)) {
[$ext, $img] = explode('|||', preg_replace('|^data:image/(.*?);base64,|i', '$1|||', $base64));
$name = static::name($img, $ext, $prefix);
if (empty($ext) || !in_array(strtolower($ext), ['gif', 'png', 'jpg', 'jpeg'])) {
throw new Exception('内容格式异常!');
} elseif ($safemode) {
return LocalStorage::instance()->set($name, base64_decode($img), true);
} else {
return static::instance()->set($name, base64_decode($img));
}
} else {
return ['url' => $base64];
}
}
}

View File

@ -0,0 +1,462 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
use think\admin\extend\CodeExtend;
use think\admin\extend\HttpExtend;
use think\admin\Helper;
use think\admin\helper\TokenHelper;
use think\admin\Library;
use think\admin\service\AdminService;
use think\admin\service\QueueService;
use think\admin\service\RuntimeService;
use think\admin\service\SystemService;
use think\admin\Storage;
use think\db\Query;
use think\helper\Str;
use think\Model;
if (!function_exists('p')) {
/**
* 打印输出数据到文件
* @param mixed $data 输出的数据
* @param boolean $new 强制替换文件
* @param ?string $file 保存文件名称
* @return false|int
*/
function p($data, bool $new = false, ?string $file = null)
{
return SystemService::putDebug($data, $new, $file);
}
}
if (!function_exists('m')) {
/**
* 动态创建模型对象
* @param string $name 模型名称
* @param array $data 初始数据
* @param string $conn 指定连接
* @return Model
*/
function m(string $name, array $data = [], string $conn = ''): Model
{
return Helper::buildModel($name, $data, $conn);
}
}
if (!function_exists('auth')) {
/**
* 访问权限检查
* @param ?string $node
* @return boolean
*/
function auth(?string $node): bool
{
return AdminService::check($node);
}
}
if (!function_exists('admuri')) {
/**
* 生成后台 URL 地址
* @param string $url 路由地址
* @param array $vars PATH 变量
* @param boolean|string $suffix 后缀
* @param boolean|string $domain 域名
* @return string
*/
function admuri(string $url = '', array $vars = [], $suffix = true, $domain = false): string
{
return sysuri('admin/index/index') . '#' . url($url, $vars, $suffix, $domain)->build();
}
}
if (!function_exists('sysvar')) {
/**
* 读写单次请求的内存缓存
* @param null|string $name 数据名称
* @param null|mixed $value 数据内容
* @return null|array|mixed 返回内容
*/
function sysvar(?string $name = null, $value = null)
{
static $swap = [];
if ($name === '' && $value === '') {
return $swap = [];
} elseif (is_null($value)) {
return is_null($name) ? $swap : ($swap[$name] ?? null);
} else {
return $swap[$name] = $value;
}
}
}
if (!function_exists('sysuri')) {
/**
* 生成最短 URL 地址
* @param string $url 路由地址
* @param array $vars PATH 变量
* @param boolean|string $suffix 后缀
* @param boolean|string $domain 域名
* @return string
*/
function sysuri(string $url = '', array $vars = [], $suffix = true, $domain = false): string
{
if (preg_match('#^(https?://|\\|/|@)#', $url)) {
return Library::$sapp->route->buildUrl($url, $vars)->suffix($suffix)->domain($domain)->build();
}
if (count($attr = $url === '' ? [] : explode('/', rtrim($url, '/'))) < 3) {
$map = [Library::$sapp->http->getName(), Library::$sapp->request->controller(), Library::$sapp->request->action(true)];
while (count($attr) < 3) array_unshift($attr, $map[2 - count($attr)] ?? 'index');
}
$attr[1] = Str::snake($attr[1]);
[$rcf, $tmp] = [Library::$sapp->config->get('route', []), uniqid('think_admin_replace_temp_vars_')];
$map = [Str::lower($rcf['default_app'] ?? ''), Str::snake($rcf['default_controller'] ?? ''), Str::lower($rcf['default_action'] ?? '')];
for ($idx = count($attr) - 1; $idx >= 0; $idx--) if ($attr[$idx] == ($map[$idx] ?: 'index')) $attr[$idx] = $tmp; else break;
$url = Library::$sapp->route->buildUrl(join('/', $attr), $vars)->suffix($suffix)->domain($domain)->build();
$ext = is_string($suffix) ? $suffix : ($rcf['url_html_suffix'] ?? 'html');
$new = preg_replace("#/{$tmp}(\.{$ext})?#", '', $old = parse_url($url, PHP_URL_PATH) ?: '', -1, $count);
$count > 0 && $suffix && $new && $ext !== '' && $new !== Library::$sapp->request->baseUrl() && $new .= ".{$ext}";
return str_replace($old, $new ?: '/', $url);
}
}
if (!function_exists('encode')) {
/**
* 加密 UTF8 字符串
* @param string $content
* @return string
*/
function encode(string $content): string
{
[$chars, $length] = ['', strlen($string = CodeExtend::text2utf8($content))];
for ($i = 0; $i < $length; $i++) $chars .= str_pad(base_convert(strval(ord($string[$i])), 10, 36), 2, '0', 0);
return $chars;
}
}
if (!function_exists('decode')) {
/**
* 解密 UTF8 字符串
* @param string $content
* @return string
*/
function decode(string $content): string
{
$chars = '';
foreach (str_split($content, 2) as $char) {
$chars .= chr(intval(base_convert($char, 36, 10)));
}
return CodeExtend::text2utf8($chars);
}
}
if (!function_exists('str2arr')) {
/**
* 字符串转数组
* @param string $text 待转内容
* @param string $separ 分隔字符
* @param ?array $allow 限定规则
* @return array
*/
function str2arr(string $text, string $separ = ',', ?array $allow = null): array
{
$items = [];
foreach (explode($separ, trim($text, $separ)) as $item) {
if ($item !== '' && (!is_array($allow) || in_array($item, $allow))) {
$items[] = trim($item);
}
}
return $items;
}
}
if (!function_exists('arr2str')) {
/**
* 数组转字符串
* @param array $data 待转数组
* @param string $separ 分隔字符
* @param ?array $allow 限定规则
* @return string
*/
function arr2str(array $data, string $separ = ',', ?array $allow = null): string
{
foreach ($data as $key => $item) {
if ($item === '' || (is_array($allow) && !in_array($item, $allow))) {
unset($data[$key]);
}
}
return $separ . join($separ, $data) . $separ;
}
}
if (!function_exists('isDebug')) {
/**
* 调试模式运行
* @return boolean
*/
function isDebug(): bool
{
return RuntimeService::isDebug();
}
}
if (!function_exists('isOnline')) {
/**
* 产品模式运行
* @return boolean
*/
function isOnline(): bool
{
return RuntimeService::isOnline();
}
}
if (!function_exists('sysconf')) {
/**
* 获取或配置系统参数
* @param string $name 参数名称
* @param mixed $value 参数内容
* @return mixed
* @throws \think\admin\Exception
*/
function sysconf(string $name = '', $value = null)
{
if (is_null($value) && is_string($name)) {
return SystemService::get($name);
} else {
return SystemService::set($name, $value);
}
}
}
if (!function_exists('sysdata')) {
/**
* JSON 数据读取与存储
* @param string $name 数据名称
* @param mixed $value 数据内容
* @return mixed
* @throws \think\admin\Exception
*/
function sysdata(string $name, $value = null)
{
if (is_null($value)) {
return SystemService::getData($name);
} else {
return SystemService::setData($name, $value);
}
}
}
if (!function_exists('syspath')) {
/**
* 获取文件绝对路径
* @param string $name 文件路径
* @param ?string $root 程序根路径
* @return string
*/
function syspath(string $name = '', ?string $root = null): string
{
if (is_null($root)) $root = Library::$sapp->getRootPath();
$attr = ['/' => DIRECTORY_SEPARATOR, '\\' => DIRECTORY_SEPARATOR];
return rtrim($root, '\\/') . DIRECTORY_SEPARATOR . ltrim(strtr($name, $attr), '\\/');
}
}
if (!function_exists('sysoplog')) {
/**
* 写入系统日志
* @param string $action 日志行为
* @param string $content 日志内容
* @return boolean
*/
function sysoplog(string $action, string $content): bool
{
return SystemService::setOplog($action, $content);
}
}
if (!function_exists('systoken')) {
/**
* 生成 CSRF-TOKEN 参数
* @return string
*/
function systoken(): string
{
return TokenHelper::token();
}
}
if (!function_exists('sysqueue')) {
/**
* 注册异步处理任务
* @param string $title 任务名称
* @param string $command 执行内容
* @param integer $later 延时执行时间
* @param array $data 任务附加数据
* @param integer $rscript 任务类型(0单例,1多例)
* @param integer $loops 循环等待时间
* @return string
* @throws \think\admin\Exception
*/
function sysqueue(string $title, string $command, int $later = 0, array $data = [], int $rscript = 1, int $loops = 0): string
{
return QueueService::register($title, $command, $later, $data, $rscript, $loops)->code;
}
}
if (!function_exists('enbase64url')) {
/**
* Base64安全URL编码
* @param string $string
* @return string
*/
function enbase64url(string $string): string
{
return CodeExtend::enSafe64($string);
}
}
if (!function_exists('debase64url')) {
/**
* Base64安全URL解码
* @param string $string
* @return string
*/
function debase64url(string $string): string
{
return CodeExtend::deSafe64($string);
}
}
if (!function_exists('xss_safe')) {
/**
* 文本内容XSS过滤
* @param string $text
* @return string
*/
function xss_safe(string $text): string
{
// 将所有 onxxx= 中的字母 o 替换为符号 ο,注意它不是字母
$rules = ['#<script.*?<\/script>#is' => '', '#(\s)on(\w+=\S)#i' => '$1οn$2'];
return preg_replace(array_keys($rules), array_values($rules), trim($text));
}
}
if (!function_exists('http_get')) {
/**
* get 模拟网络请求
* @param string $url HTTP请求URL地址
* @param array|string $query GET请求参数
* @param array $options CURL参数
* @return boolean|string
*/
function http_get(string $url, $query = [], array $options = [])
{
return HttpExtend::get($url, $query, $options);
}
}
if (!function_exists('http_post')) {
/**
* post 模拟网络请求
* @param string $url HTTP请求URL地址
* @param array|string $data POST请求数据
* @param array $options CURL参数
* @return boolean|string
*/
function http_post(string $url, $data, array $options = [])
{
return HttpExtend::post($url, $data, $options);
}
}
if (!function_exists('data_save')) {
/**
* 数据增量保存
* @param Model|Query|string $dbQuery
* @param array $data 需要保存或更新的数据
* @param string $key 条件主键限制
* @param mixed $where 其它的where条件
* @return boolean|integer
* @throws \think\admin\Exception
*/
function data_save($dbQuery, array $data, string $key = 'id', $where = [])
{
return SystemService::save($dbQuery, $data, $key, $where);
}
}
if (!function_exists('down_file')) {
/**
* 下载远程文件到本地
* @param string $source 远程文件地址
* @param boolean $force 是否强制重新下载
* @param integer $expire 强制本地存储时间
* @return string
*/
function down_file(string $source, bool $force = false, int $expire = 0): string
{
return Storage::down($source, $force, $expire)['url'] ?? $source;
}
}
if (!function_exists('trace_file')) {
/**
* 输出异常数据到文件
* @param \Exception $exception
* @return boolean
*/
function trace_file(Exception $exception): bool
{
$path = Library::$sapp->getRuntimePath() . 'trace';
if (!is_dir($path)) mkdir($path, 0777, true);
$name = substr($exception->getFile(), strlen(syspath()));
$file = $path . DIRECTORY_SEPARATOR . date('Ymd_His_') . strtr($name, ['/' => '.', '\\' => '.']);
$json = json_encode($exception instanceof \think\admin\Exception ? $exception->getData() : [], 64 | 128 | 256);
$class = get_class($exception);
return false !== file_put_contents($file,
"[CODE] {$exception->getCode()}" . PHP_EOL .
"[INFO] {$exception->getMessage()}" . PHP_EOL .
($exception instanceof \think\admin\Exception ? "[DATA] {$json}" . PHP_EOL : '') .
"[FILE] {$class} in {$name} line {$exception->getLine()}" . PHP_EOL .
"[TIME] " . date('Y-m-d H:i:s') . PHP_EOL . PHP_EOL .
'[TRACE]' . PHP_EOL . $exception->getTraceAsString()
);
}
}
if (!function_exists('format_bytes')) {
/**
* 文件字节单位转换
* @param string|integer $size
* @return string
*/
function format_bytes($size): string
{
if (is_numeric($size)) {
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2) . ' ' . $units[$i];
} else {
return $size;
}
}
}
if (!function_exists('format_datetime')) {
/**
* 日期格式标准输出
* @param int|string $datetime 输入日期
* @param string $format 输出格式
* @return string
*/
function format_datetime($datetime, string $format = 'Y年m月d日 H:i:s'): string
{
if (empty($datetime)) {
return '-';
} elseif (is_numeric($datetime)) {
return date(lang($format), intval($datetime));
} elseif ($timestamp = strtotime($datetime)) {
return date(lang($format), $timestamp);
} else {
return $datetime;
}
}
}

View File

@ -0,0 +1,99 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\contract;
/**
* 文件存储标准接口
* @class StorageInterface
* @package think\admin\contract
*/
interface StorageInterface
{
/**
* 上传文件内容
* @param string $name 文件名称
* @param string $file 文件内容
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return array
*/
public function set(string $name, string $file, bool $safe = false, ?string $attname = null): array;
/**
* 读取文件内容
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return string
*/
public function get(string $name, bool $safe = false): string;
/**
* 删除存储文件
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return boolean
*/
public function del(string $name, bool $safe = false): bool;
/**
* 判断是否存在
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return boolean
*/
public function has(string $name, bool $safe = false): bool;
/**
* 获取访问地址
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return string
*/
public function url(string $name, bool $safe = false, ?string $attname = null): string;
/**
* 获取存储路径
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return string
*/
public function path(string $name, bool $safe = false): string;
/**
* 获取文件信息
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return array
*/
public function info(string $name, bool $safe = false, ?string $attname = null): array;
/**
* 获取上传地址
* @return string
*/
public function upload(): string;
/**
* 获取存储区域
* @return array
*/
public static function region(): array;
}

View File

@ -0,0 +1,143 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\contract;
use think\admin\Exception;
use think\App;
use think\Container;
/**
* 文件存储公共属性
* @class StorageUsageTrait
* @package think\admin\contract
*/
trait StorageUsageTrait
{
/**
* @var \think\App $app
*/
protected $app;
/**
* 链接类型
* @var string
*/
protected $link;
/**
* 链接前缀
* @var string
*/
protected $domain;
/**
* 存储器构造方法
* @param \think\App $app
* @throws \think\admin\Exception
*/
public function __construct(App $app)
{
$this->app = $app;
$this->link = sysconf('storage.link_type|raw');
$this->init();
}
/**
* 自定义初始化方法
* @return void
*/
protected function init()
{
}
/**
* 获取对象实例
* @return static
*/
public static function instance()
{
/** @var \think\admin\contract\StorageInterface */
return Container::getInstance()->make(static::class);
}
/**
* 获取下载链接后缀
* @param null|string $attname 下载名称
* @param null|string $filename 文件名称
* @return string
*/
protected function getSuffix(?string $attname = null, ?string $filename = null): string
{
[$class, $suffix] = [class_basename(get_class($this)), ''];
if (is_string($filename) && stripos($this->link, 'compress') !== false) {
$compress = [
'LocalStorage' => '',
'QiniuStorage' => '?imageslim',
'UpyunStorage' => '!/format/webp',
'TxcosStorage' => '?imageMogr2/format/webp',
'AliossStorage' => '?x-oss-process=image/format,webp',
];
$extens = strtolower(pathinfo($this->delSuffix($filename), PATHINFO_EXTENSION));
$suffix = in_array($extens, ['png', 'jpg', 'jpeg']) ? ($compress[$class] ?? '') : '';
}
if (is_string($attname) && strlen($attname) > 0 && stripos($this->link, 'full') !== false) {
if ($class === 'UpyunStorage') {
$suffix .= ($suffix ? '&' : '?') . '_upd=' . urlencode($attname);
} else {
$suffix .= ($suffix ? '&' : '?') . 'attname=' . urlencode($attname);
}
}
return $suffix;
}
/**
* 获取文件基础名称
* @param string $name 文件名称
* @return string
*/
protected function delSuffix(string $name): string
{
if (strpos($name, '?') !== false) {
return strstr($name, '?', true);
}
if (stripos($name, '!') !== false) {
return strstr($name, '!', true);
}
return $name;
}
/**
* 重构后兼容处理
* @param string $method
* @param array $arguments
* @return array|string
* @throws \think\admin\Exception
*/
public function __call(string $method, array $arguments)
{
if (strtolower($method) === 'builduploadtoken') {
if (method_exists($this, 'token')) {
return $this->token(...$arguments);
}
}
// 调用方法异常处理
$class = class_basename(static::class);
throw new Exception("method not exists: {$class}->{$method}()");
}
}

View File

@ -0,0 +1,85 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\contract;
/**
* 流协议接口
* @class StreamInterface
* @package think\admin\contract
*/
interface StreamInterface
{
public function dir_closedir(): bool;
public function dir_opendir(string $path, int $options): bool;
public function dir_readdir(): string;
public function dir_rewinddir(): bool;
public function mkdir(string $path, int $mode, int $options): bool;
public function rename(string $path_from, string $path_to): bool;
public function rmdir(string $path, int $options): bool;
public function stream_cast(int $cast_as);
public function stream_close(): void;
public function stream_eof(): bool;
public function stream_flush(): bool;
public function stream_lock(int $operation): bool;
public function stream_metadata(string $path, int $option, $value): bool;
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool;
/**
* @param int $count
* @return string|false
*/
public function stream_read(int $count);
public function stream_seek(int $offset, int $whence = SEEK_SET): bool;
public function stream_set_option(int $option, int $arg1, int $arg2): bool;
/**
* @return array|false
*/
public function stream_stat();
public function stream_tell(): int;
public function stream_truncate(int $new_size): bool;
public function stream_write(string $data): int;
public function unlink(string $path): bool;
/**
* @param string $path
* @param integer $flags
* @return array|false
*/
public function url_stat(string $path, int $flags);
}

View File

@ -0,0 +1,168 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\extend;
/**
* 随机数码管理扩展
* @class CodeExtend
* @package think\admin\extend
*/
class CodeExtend
{
/**
* 生成 UUID 编码
* @return string
*/
public static function uuid(): string
{
$chars = md5(uniqid(strval(mt_rand(0, 9999)), true));
$value = substr($chars, 0, 8) . '-' . substr($chars, 8, 4) . '-';
$value .= substr($chars, 12, 4) . '-' . substr($chars, 16, 4) . '-';
return strtoupper($value . substr($chars, 20, 12));
}
/**
* 生成随机编码
* @param integer $size 编码长度
* @param integer $type 编码类型(1纯数字,2纯字母,3数字字母)
* @param string $prefix 编码前缀
* @return string
*/
public static function random(int $size = 10, int $type = 1, string $prefix = ''): string
{
$numbs = '0123456789';
$chars = 'abcdefghijklmnopqrstuvwxyz';
if ($type === 1) $chars = $numbs;
if ($type === 3) $chars = "{$numbs}{$chars}";
$code = $prefix . $chars[rand(1, strlen($chars) - 1)];
while (strlen($code) < $size) $code .= $chars[rand(0, strlen($chars) - 1)];
return $code;
}
/**
* 生成日期编码
* @param integer $size 编码长度
* @param string $prefix 编码前缀
* @return string
*/
public static function uniqidDate(int $size = 16, string $prefix = ''): string
{
if ($size < 14) $size = 14;
$code = $prefix . date('Ymd') . (date('H') + date('i')) . date('s');
while (strlen($code) < $size) $code .= rand(0, 9);
return $code;
}
/**
* 生成数字编码
* @param integer $size 编码长度
* @param string $prefix 编码前缀
* @return string
*/
public static function uniqidNumber(int $size = 12, string $prefix = ''): string
{
$time = strval(time());
if ($size < 10) $size = 10;
$code = $prefix . (intval($time[0]) + intval($time[1])) . substr($time, 2) . rand(0, 9);
while (strlen($code) < $size) $code .= rand(0, 9);
return $code;
}
/**
* 文本转码
* @param string $text 文本内容
* @param string $target 目标编码
* @return string
*/
public static function text2utf8(string $text, string $target = 'UTF-8'): string
{
[$first2, $first4] = [substr($text, 0, 2), substr($text, 0, 4)];
if ($first4 === chr(0x00) . chr(0x00) . chr(0xFE) . chr(0xFF)) $ft = 'UTF-32BE';
elseif ($first4 === chr(0xFF) . chr(0xFE) . chr(0x00) . chr(0x00)) $ft = 'UTF-32LE';
elseif ($first2 === chr(0xFE) . chr(0xFF)) $ft = 'UTF-16BE';
elseif ($first2 === chr(0xFF) . chr(0xFE)) $ft = 'UTF-16LE';
return mb_convert_encoding($text, $target, $ft ?? mb_detect_encoding($text));
}
/**
* 数据加密处理
* @param mixed $data 加密数据
* @param string $skey 安全密钥
* @return string
*/
public static function encrypt($data, string $skey): string
{
$iv = static::random(16, 3);
$value = openssl_encrypt(serialize($data), 'AES-256-CBC', $skey, 0, $iv);
return static::enSafe64(json_encode(['iv' => $iv, 'value' => $value]));
}
/**
* 数据解密处理
* @param string $data 解密数据
* @param string $skey 安全密钥
* @return mixed
*/
public static function decrypt(string $data, string $skey)
{
$attr = json_decode(static::deSafe64($data), true);
return unserialize(openssl_decrypt($attr['value'], 'AES-256-CBC', $skey, 0, $attr['iv']));
}
/**
* Base64Url 安全编码
* @param string $text 待加密文本
* @return string
*/
public static function enSafe64(string $text): string
{
return rtrim(strtr(base64_encode($text), '+/', '-_'), '=');
}
/**
* Base64Url 安全解码
* @param string $text 待解密文本
* @return string
*/
public static function deSafe64(string $text): string
{
return base64_decode(str_pad(strtr($text, '-_', '+/'), strlen($text) % 4, '='));
}
/**
* 压缩数据对象
* @param mixed $data
* @return string
*/
public static function enzip($data): string
{
return static::enSafe64(gzcompress(serialize($data)));
}
/**
* 解压数据对象
* @param string $string
* @return mixed
*/
public static function dezip(string $string)
{
return unserialize(gzuncompress(static::deSafe64($string)));
}
}

View File

@ -0,0 +1,91 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\extend;
/**
* 数据处理扩展
* @class DataExtend
* @package think\admin\extend
*/
class DataExtend
{
/**
* 二维数组转多维数据树
* @param array $list 待处理数据
* @param string $ckey 自己的主键
* @param string $pkey 上级的主键
* @param string $chil 子数组名称
* @return array
*/
public static function arr2tree(array $list, string $ckey = 'id', string $pkey = 'pid', string $chil = 'sub'): array
{
[$tree, $list] = [[], array_column($list, null, $ckey)];
foreach ($list as $it) isset($list[$it[$pkey]]) ? $list[$it[$pkey]][$chil][] = &$list[$it[$ckey]] : $tree[] = &$list[$it[$ckey]];
return $tree;
}
/**
* 二维数组转数据树表
* @param array $list 待处理数据
* @param string $ckey 自己的主键
* @param string $pkey 上级的主键
* @param string $path 当前 PATH
* @return array
*/
public static function arr2table(array $list, string $ckey = 'id', string $pkey = 'pid', string $path = 'path'): array
{
$build = static function (array $nodes, callable $build, array &$data = [], string $parent = '') use ($ckey, $pkey, $path) {
foreach ($nodes as $node) {
$subs = $node['sub'] ?? [];
unset($node['sub']);
$node[$path] = "{$parent}-{$node[$ckey]}";
$node['spc'] = count($subs);
$node['spt'] = substr_count($parent, '-');
$node['spl'] = str_repeat('ㅤ├ㅤ', $node['spt']);
$node['sps'] = ",{$node[$ckey]},";
array_walk_recursive($subs, static function ($val, $key) use ($ckey, &$node) {
if ($key === $ckey) $node['sps'] .= "{$val},";
});
$node['spp'] = arr2str(str2arr(strtr($parent . $node['sps'], '-', ',')));
$data[] = $node;
if (empty($subs)) continue;
$build($subs, $build, $data, $node[$path]);
}
return $data;
};
return $build(static::arr2tree($list, $ckey, $pkey), $build);
}
/**
* 获取数据树子ID集合
* @param array $list 数据列表
* @param mixed $value 起始有效ID值
* @param string $ckey 当前主键ID名称
* @param string $pkey 上级主键ID名称
* @return array
*/
public static function getArrSubIds(array $list, $value = 0, string $ckey = 'id', string $pkey = 'pid'): array
{
$ids = [intval($value)];
foreach ($list as $vo) if (intval($vo[$pkey]) > 0 && intval($vo[$pkey]) === intval($value)) {
$ids = array_merge($ids, static::getArrSubIds($list, intval($vo[$ckey]), $ckey, $pkey));
}
return $ids;
}
}

View File

@ -0,0 +1,81 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\extend;
/**
* 导出 CSV 文件扩展
* @class ExcelExtend
* @deprecated 改用 JavaScript
* @package think\admin\extend
*/
class ExcelExtend
{
/**
* 设置写入 CSV 文件头部
* @param string $name 导出文件名称
* @param array $headers 表格头部(一维数组)
*/
public static function header(string $name, array $headers): void
{
header('Content-Type: application/octet-stream');
header("Content-Disposition: attachment; filename=" . iconv('utf-8', 'gbk//TRANSLIT', $name));
$handle = fopen('php://output', 'w');
foreach ($headers as $key => $value) {
$headers[$key] = iconv("utf-8", "gbk//TRANSLIT", $value);
}
fputcsv($handle, $headers);
if (is_resource($handle)) {
fclose($handle);
}
}
/**
* 设置写入CSV文件内容
* @param array $list 数据列表(二维数组)
* @param array $rules 数据规则(一维数组)
*/
public static function body(array $list, array $rules): void
{
$handle = fopen('php://output', 'w');
foreach ($list as $data) {
$rows = [];
foreach ($rules as $rule) {
$rows[] = static::parseKeyDotValue($data, $rule);
}
fputcsv($handle, $rows);
}
if (is_resource($handle)) {
fclose($handle);
}
}
/**
* 根据数组key查询(可带点规则)
* @param array $data 数据
* @param string $rule 规则,如: order.order_no
* @return string
*/
public static function parseKeyDotValue(array $data, string $rule): string
{
[$temp, $attr] = [$data, explode('.', trim($rule, '.'))];
while ($key = array_shift($attr)) $temp = $temp[$key] ?? $temp;
return (is_string($temp) || is_numeric($temp)) ? @iconv('utf-8', 'gbk//TRANSLIT', "{$temp}") : '';
}
}

View File

@ -0,0 +1,204 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\extend;
use think\admin\Exception;
/**
* 网站 ICO 文件生成工具
* @class FaviconExtend
* @package think\admin\extend
*/
class FaviconExtend
{
/**
* 转换后的 BMP 图像
* @var array
*/
private $images = [];
/**
* Constructor - 创建一个新的 ICO 生成器
* @param ?string $file 源图像文件的路径
* @param array $size 图片文件尺寸 [W1,H1]
* @throws \think\admin\Exception
*/
function __construct(?string $file = null, array $size = [])
{
$functions = [
'imagesx',
'imagesy',
'getimagesize',
'imagesavealpha',
'imagecreatefromstring',
'imagecreatetruecolor',
'imagecolortransparent',
'imagecolorallocatealpha',
'imagecopyresampled',
'imagealphablending',
];
foreach ($functions as $function) if (!function_exists($function)) {
throw new Exception(lang('Required %s function not found.', [$function]));
}
if (is_string($file)) {
$this->addImage($file, $size);
}
}
/**
* 添加图像到生成器中
*
* @param string $file 图像文件路径
* @param array $size 图像文件尺寸
* @throws \think\admin\Exception
*/
public function addImage(string $file, array $size = []): FaviconExtend
{
if (false === ($im = $this->loadImageFile($file))) {
throw new Exception(lang('Read picture file Failed.'));
}
if (empty($size)) {
$size = [imagesx($im), imagesy($im)];
}
[$width, $height] = $size;
$image = imagecreatetruecolor($width, $height);
imagecolortransparent($image, imagecolorallocatealpha($image, 0, 0, 0, 127));
imagealphablending($image, false);
imagesavealpha($image, true);
[$sourceWidth, $sourceHeight] = [imagesx($im), imagesy($im)];
if (false === imagecopyresampled($image, $im, 0, 0, 0, 0, $width, $height, $sourceWidth, $sourceHeight)) {
throw new Exception(lang('Parse and process picture Failed.'));
}
$this->addImageData($image);
return $this;
}
/**
* ICO 内容写入到文件
*
* @param string $file 写入文件路径
* @return boolean
*/
public function saveIco(string $file): bool
{
if (false === ($data = $this->getIcoData())) {
return false;
}
if (false === ($fh = fopen($file, 'w'))) {
return false;
}
if (false === (fwrite($fh, $data))) {
fclose($fh);
return false;
}
fclose($fh);
return true;
}
/**
* 生成并获取 ICO 图像数据
*/
private function getIcoData()
{
if (!is_array($this->images) || empty($this->images)) {
return false;
}
[$pixelData, $entrySize] = ['', 16];
$data = pack('vvv', 0, 1, count($this->images));
$offset = 6 + ($entrySize * count($this->images));
foreach ($this->images as $image) {
$data .= pack('CCCCvvVV', $image['width'], $image['height'], $image['colors'], 0, 1, $image['pixel'], $image['size'], $offset);
$pixelData .= $image['data'];
$offset += $image['size'];
}
$data .= $pixelData;
unset($pixelData);
return $data;
}
/**
* GD 图像转为 BMP 格式
*/
private function addImageData($im)
{
[$width, $height] = [imagesx($im), imagesy($im)];
[$pixelData, $opacityData, $opacityValue] = [[], [], 0];
for ($y = $height - 1; $y >= 0; $y--) {
for ($x = 0; $x < $width; $x++) {
$color = imagecolorat($im, $x, $y);
$alpha = ($color & 0x7F000000) >> 24;
$alpha = (1 - ($alpha / 127)) * 255;
$color &= 0xFFFFFF;
$color |= 0xFF000000 & (intval($alpha) << 24);
$pixelData[] = $color;
$opacity = ($alpha <= 127) ? 1 : 0;
$opacityValue = ($opacityValue << 1) | $opacity;
if ((($x + 1) % 32) == 0) {
$opacityData[] = $opacityValue;
$opacityValue = 0;
}
}
if (($x % 32) > 0) {
while (($x++ % 32) > 0) {
$opacityValue = $opacityValue << 1;
}
$opacityData[] = $opacityValue;
$opacityValue = 0;
}
}
$imageHeaderSize = 40;
$colorMaskSize = $width * $height * 4;
$opacityMaskSize = (ceil($width / 32) * 4) * $height;
$data = pack('VVVvvVVVVVV', 40, $width, ($height * 2), 1, 32, 0, 0, 0, 0, 0, 0);
foreach ($pixelData as $color) $data .= pack('V', $color);
foreach ($opacityData as $opacity) $data .= pack('N', $opacity);
$this->images[] = [
'data' => $data,
'size' => $imageHeaderSize + $colorMaskSize + $opacityMaskSize,
'width' => $width, 'height' => $height,
'pixel' => 32, 'colors' => 0,
];
}
/**
* 读取图片资源
* @param string $file 文件路径
* @return false|resource|\GdImage
*/
private function loadImageFile(string $file)
{
if (false === getimagesize($file)) {
return false;
}
if (false === ($data = file_get_contents($file))) {
return false;
}
if (false === ($image = @imagecreatefromstring($data))) {
return false;
}
unset($data);
return $image;
}
}

View File

@ -0,0 +1,176 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\extend;
/**
* CURL模拟请求扩展
* @class HttpExtend
* @package think\admin\extend
*/
class HttpExtend
{
/**
* GET 模拟网络请求
* @param string $location HTTP请求地址
* @param array|string $data GET请求参数
* @param array $options CURL请求参数
* @return boolean|string
*/
public static function get(string $location, $data = [], array $options = [])
{
$options['query'] = $data;
return static::request('get', $location, $options);
}
/**
* POST 模拟网络请求
* @param string $location HTTP请求地址
* @param array|string $data POST请求数据
* @param array $options CURL请求参数
* @return boolean|string
*/
public static function post(string $location, $data = [], array $options = [])
{
$options['data'] = $data;
return static::request('post', $location, $options);
}
/**
* FormData 模拟网络请求
* @param string $url 模拟请求地址
* @param array $data 模拟请求参数数据
* @param array $file 提交文件 [field,name,type,content]
* @param array $header 请求头部信息,默认带 Content-type
* @param string $method 模拟请求的方式 [GET,POST,PUT]
* @param boolean $returnHeader 是否返回头部信息
* @return boolean|string
*/
public static function submit(string $url, array $data = [], array $file = [], array $header = [], string $method = 'POST', bool $returnHeader = true)
{
[$line, $boundary] = [[], CodeExtend::random(18)];
foreach ($data as $key => $value) {
$line[] = "--{$boundary}";
$line[] = "Content-Disposition: form-data; name=\"{$key}\"";
$line[] = "";
$line[] = $value;
}
if (is_array($file) && isset($file['field']) && isset($file['name'])) {
$line[] = "--{$boundary}";
$line[] = "Content-Disposition: form-data; name=\"{$file['field']}\"; filename=\"{$file['name']}\"";
if (isset($file['type'])) $line[] = "Content-Type: \"{$file['type']}\"";
$line[] = "";
$line[] = $file['content'];
}
$line[] = "--{$boundary}--";
$header[] = "Content-type:multipart/form-data;boundary={$boundary}";
return static::request($method, $url, ['data' => join("\r\n", $line), 'returnHeader' => $returnHeader, 'headers' => $header]);
}
/**
* CURL 模拟网络请求
* @param string $method 模拟请求方式
* @param string $location 模拟请求地址
* @param array $options 请求参数[headers,query,data,cookie,cookie_file,timeout,returnHeader]
* @return boolean|string
*/
public static function request(string $method, string $location, array $options = [])
{
// GET 参数设置
if (!empty($options['query'])) {
$location .= strpos($location, '?') !== false ? '&' : '?';
if (is_array($options['query'])) {
$location .= http_build_query($options['query']);
} elseif (is_string($options['query'])) {
$location .= $options['query'];
}
}
$curl = curl_init();
// Agent 代理设置
curl_setopt($curl, CURLOPT_USERAGENT, $options['agent'] ?? static::getUserAgent());
// Cookie 信息设置
if (!empty($options['cookie'])) {
curl_setopt($curl, CURLOPT_COOKIE, $options['cookie']);
}
// Header 头信息设置
if (!empty($options['headers'])) {
curl_setopt($curl, CURLOPT_HTTPHEADER, $options['headers']);
}
if (!empty($options['cookie_file'])) {
curl_setopt($curl, CURLOPT_COOKIEJAR, $options['cookie_file']);
curl_setopt($curl, CURLOPT_COOKIEFILE, $options['cookie_file']);
}
// 设置请求方式
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, strtoupper($method));
if (strtolower($method) === 'head') {
curl_setopt($curl, CURLOPT_NOBODY, 1);
} elseif (isset($options['data'])) {
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $options['data']);
}
// 请求超时设置
if (isset($options['timeout']) && is_numeric($options['timeout'])) {
curl_setopt($curl, CURLOPT_TIMEOUT, $options['timeout']);
} else {
curl_setopt($curl, CURLOPT_TIMEOUT, 60);
}
// 是否返回前部内容
if (empty($options['returnHeader'])) {
curl_setopt($curl, CURLOPT_HEADER, false);
} else {
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
}
// 自定义扩展参数配置,二维数组内每个单元为一个设置语句,格式如下:
// $setopt = [ [CURLOPT_URL, $location], [CURLOPT_AUTOREFERER, true], ...];
if (isset($options['setopt']) && is_array($options['setopt'])) {
foreach ($options['setopt'] as $value) if (is_array($value)) {
curl_setopt($curl, ...$value);
}
}
curl_setopt($curl, CURLOPT_URL, $location);
curl_setopt($curl, CURLOPT_AUTOREFERER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
$content = curl_exec($curl);
curl_close($curl);
return $content;
}
/**
* 获取浏览器代理信息
* @return string
*/
private static function getUserAgent(): string
{
$agents = [
"Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 10.0; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0",
"Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; InfoPath.3; rv:11.0) like Gecko",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
];
return $agents[array_rand($agents)];
}
}

View File

@ -0,0 +1,261 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\extend;
use think\admin\Library;
use think\admin\Storage;
use think\admin\storage\LocalStorage;
/**
* 拼图拖拽验证器
* @class ImageVerify
* @package think\admin\extend
*/
class ImageVerify
{
// 浮层圆半径
private $r = 10;
// 图片路径
private $srcImage;
// 浮层图宽高
private $picWidth = 100;
private $picHeight = 100;
// 目标图宽高
private $dstWidth = 600;
private $dstHeight = 300;
/**
* 验证器构造方法
* @param string $image 原始图片
* @param array $options 配置参数
*/
public function __construct(string $image, array $options = [])
{
if (!empty($options)) {
foreach ($options as $k => $v) {
if (isset($this->$k)) $this->$k = $v;
}
}
$this->srcImage = $image;
}
/**
* 生成图片拼图
* @param string $image 原始图片
* @param integer $time 缓存时间
* @param integer $diff 容错数值
* @param integer $retry 容错次数
* @return array [code, bgimg, water]
*/
public static function render(string $image, int $time = 1800, int $diff = 10, int $retry = 3): array
{
$data = (new static($image))->create();
$range = [$data['point'] - $diff, $data['point'] + $diff];
$result = ['retry' => $retry, 'error' => 0, 'expire' => time() + $time, 'range' => $range];
Library::$sapp->cache->set($code = CodeExtend::uniqidNumber(16, 'V'), $result, $time);
return ['code' => $code, 'bgimg' => $data['bgimg'], 'water' => $data['water']];
}
/**
* 在线验证是否通过
* @param string $code 验证码编码
* @param string $value 待验证数值
* @param boolean $clear 验证成功清理
* @return integer [ -1:需要刷新, 0:验证失败, 1:验证成功 ]
*/
public static function verify(string $code, string $value, bool $clear = false): int
{
$cache = Library::$sapp->cache->get($code);
if (empty($cache['range']) || empty($cache['retry'])) return -1;
if ($cache['range'][0] <= $value && $value <= $cache['range'][1]) {
$clear && Library::$sapp->cache->delete($code);
return 1;
}
// 验证失败记录次数
if (++$cache['error'] < $cache['retry']) {
if (($tll = $cache['expire'] - time()) > 0) {
Library::$sapp->cache->set($code, $cache, $tll);
return 0;
}
}
// 其他异常直接清空
Library::$sapp->cache->delete($code);
return -1;
}
/**
* 剧中裁剪图片
* @param string $image 图片资源
* @param integer $width 目标宽度
* @param integer $height 目标高度
* @return \GdImage|resource
*/
public static function cover(string $image, int $width, int $height)
{
// 读取缓存返回图片资源
$local = LocalStorage::instance();
$name = Storage::name(join('#', func_get_args()), 'png', 'cache');
if ($local->has($name, true)) return imageCreateFromString($local->get($name, true));
// 计算图片尺寸裁剪坐标
[$w, $h] = getimagesize($image);
if ($w > $h) {
[$_sw, $_sh, $_sx, $_sy] = [$h, $h, intval(($w - $h) / 2), 0];
} elseif ($w < $h) {
[$_sw, $_sh, $_sx, $_sy] = [$w, $w, 0, intval(($h - $w) / 2)];
} else {
[$_sw, $_sh, $_sx, $_sy] = [$w, $h, 0, 0];
}
$newim = imageCreateTrueColor($width, $height);
$srcim = imageCreateFromString(file_get_contents($image));
imagecopyresampled($newim, $srcim, 0, 0, $_sx, $_sy, $width, $height, $_sw, $_sh);
imagedestroy($srcim);
// 缓存图片内容
$file = $local->path($name, true);
is_dir($path = dirname($file)) || mkdir($path, 0755, true);
imagepng($newim, $file);
// 返回新图片资源
return $newim;
}
/**
* 创建背景图和浮层图、浮层图X坐标
* @return array [point, bgimg, water]
*/
public function create(): array
{
// 创建目标背景图画布
$dstim = $this->cover($this->srcImage, $this->dstWidth, $this->dstHeight);
// 生成透明底浮层图画布
$watim = imageCreateTrueColor($this->picWidth, $this->dstHeight);
imageSaveAlpha($watim, true) && imageAlphaBlending($watim, false);
imageFill($watim, 0, 0, imageColorAllocateAlpha($watim, 255, 255, 255, 127));
// 随机位置
$srcX1 = mt_rand(150, $this->dstWidth - $this->picWidth); // 水印位于大图X坐标
$srcY1 = mt_rand(0, $this->dstHeight - $this->picHeight); // 水印位于大图Y坐标
// 去除第二个干扰水印
// do { // 干扰位置
// $srcX2 = mt_rand(100, $this->dstWidth - $this->picWidth);
// $srcY2 = mt_rand(0, $this->dstHeight - $this->picHeight);
// } while (abs($srcX1 - $srcX2) < $this->picWidth);
// 水印边框颜色
$broders = [
imageColorAllocateAlpha($dstim, 250, 100, 0, 50),
imageColorAllocateAlpha($dstim, 250, 0, 100, 50),
imageColorAllocateAlpha($dstim, 100, 0, 250, 50),
imageColorAllocateAlpha($dstim, 100, 250, 0, 50),
imageColorAllocateAlpha($dstim, 0, 250, 100, 50),
];
shuffle($broders);
$c1 = array_pop($broders);
$c2 = array_pop($broders);
$gray = imageColorAllocateAlpha($dstim, 0, 0, 0, 80);
$blue = imageColorAllocateAlpha($watim, 0, 100, 250, 50);
// 取原图像素颜色,生成浮层图
$waters = $this->withWaterPoint();
for ($i = 0; $i < $this->picHeight; $i++) {
for ($j = 0; $j < $this->picWidth; $j++) {
if ($waters[$i][$j] === 1) {
if (
empty($waters[$i - 1][$j - 1]) || empty($waters[$i - 2][$j - 2]) ||
empty($waters[$i + 1][$j + 1]) || empty($waters[$i + 2][$j + 2])
) {
imagesetpixel($watim, $j, $srcY1 + $i, $blue);
} else {
imagesetpixel($watim, $j, $srcY1 + $i, ImageColorAt($dstim, $srcX1 + $j, $srcY1 + $i));
}
}
}
}
// 在原图挖坑,打上灰色水印
for ($i = 0; $i < $this->picHeight; $i++) {
for ($j = 0; $j < $this->picWidth; $j++) {
if ($waters[$i][$j] === 1) {
if (
empty($waters[$i - 1][$j - 1]) ||
empty($waters[$i - 2][$j - 2]) ||
empty($waters[$i + 1][$j + 1]) ||
empty($waters[$i + 2][$j + 2])
) {
imagesetpixel($dstim, $srcX1 + $j, $srcY1 + $i, $c1);
// 去除第二个干扰水印
// imagesetpixel($dstim, $srcX2 + $j, $srcY2 + $i, $c2);
} else {
imagesetpixel($dstim, $srcX1 + $j, $srcY1 + $i, $gray);
// 去除第二个干扰水印
// imagesetpixel($dstim, $srcX2 + $j, $srcY2 + $i, $gray);
}
}
}
}
// 获取背景图及浮层图内容
[, , $bgimg] = [ob_start(), imagepng($dstim), ob_get_contents(), ob_end_clean(), imagedestroy($dstim)];
[, , $water] = [ob_start(), imagepng($watim), ob_get_contents(), ob_end_clean(), imagedestroy($watim)];
return [
'point' => $srcX1,
'bgimg' => 'data:image/png;base64,' . base64_encode($bgimg),
'water' => 'data:image/png;base64,' . base64_encode($water)
];
}
/**
* 计算水印矩阵坐标
* @return void
*/
private function withWaterPoint(): array
{
$waters = [];
// 半径平方
$dr = $this->r * $this->r;
$lw = $this->r * 2 - 5;
// 第一个圆中心点
$c_1_x = $lw + ($this->picWidth - $lw * 2) / 2;
$c_1_y = $this->r;
// 第二个圆中心点
$c_2_x = $this->picHeight - $this->r;
$c_2_y = $lw + ($this->picHeight - ($lw) * 2) / 2;
for ($i = 0; $i < $this->picHeight; $i++) {
for ($j = 0; $j < $this->picWidth; $j++) {
// 根据公式x-a)² + (y-b)² = r² 算出像素是否在圆内
$d1 = pow($j - $c_1_x, 2) + pow($i - $c_1_y, 2);
$d2 = pow($j - $c_2_x, 2) + pow($i - $c_2_y, 2);
if (($i >= $lw && $j >= $lw && $i <= $this->picHeight - $lw && $j <= $this->picWidth - $lw) || $d1 <= $dr || $d2 <= $dr) {
$waters[$i][$j] = 1;
} else {
$waters[$i][$j] = 0;
}
}
}
return $waters;
}
}

View File

@ -0,0 +1,105 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\extend;
use think\admin\Exception;
/**
* JsonRpc 客户端
* @class JsonRpcClient
* @package think\admin\extend
*/
class JsonRpcClient
{
/**
* 请求ID
* @var integer
*/
private $id;
/**
* 服务端地址
* @var string
*/
private $proxy;
/**
* 请求头部参数
* @var string
*/
private $header;
/**
* JsonRpcClient constructor.
* @param string $proxy
* @param array $header
*/
public function __construct(string $proxy, array $header = [])
{
$this->id = time();
$this->proxy = $proxy;
$this->header = $header;
}
/**
* 执行 JsonRpc 请求
* @param string $method
* @param array $params
* @return mixed
* @throws \think\admin\Exception
*/
public function __call(string $method, array $params = [])
{
$options = [
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
'http' => [
'method' => 'POST', "timeout" => 60,
'header' => join("\r\n", array_merge(['Content-Type:application/json'], $this->header, ['User-Agent:think-admin-jsonrpc', ''])),
'content' => json_encode(['jsonrpc' => '2.0', 'method' => $method, 'params' => $params, 'id' => $this->id], JSON_UNESCAPED_UNICODE),
],
];
try {
// Performs the HTTP POST
if ($fp = fopen($this->proxy, 'r', false, stream_context_create($options))) {
$response = '';
while ($line = fgets($fp)) $response .= trim($line) . "\n";
[, $response] = [fclose($fp), json_decode($response, true)];
} else {
throw new Exception(lang("Unable connect: %s", [$this->proxy]));
}
} catch (Exception $exception) {
throw $exception;
} catch (\Exception $exception) {
throw new Exception($exception->getMessage());
}
// Compatible with normal
if (isset($response['code']) && isset($response['info'])) {
throw new Exception($response['info'], intval($response['code']), $response['data'] ?? []);
}
// Final checks and return
if (empty($response['id']) || $response['id'] != $this->id) {
throw new Exception(lang("Error flag ( Request tag: %s, Response tag: %s )", [$this->id, $response['id'] ?? '-']), 0, $response);
}
if (is_null($response['error'])) return $response['result'];
throw new Exception($response['error']['message'], intval($response['error']['code']), $response['result'] ?? []);
}
}

View File

@ -0,0 +1,125 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\extend;
use think\App;
use think\Container;
use think\exception\HttpResponseException;
/**
* JsonRpc 服务端
* @class JsonRpcServer
* @package think\admin\extend
*/
class JsonRpcServer
{
/**
* 当前App对象
* @var App
*/
protected $app;
/**
* JsonRpcServer constructor.
* @param App $app
*/
public function __construct(App $app)
{
$this->app = $app;
}
/**
* 静态实例对象
* @param array $args
* @return static
*/
public static function instance(...$args): JsonRpcServer
{
return Container::getInstance()->make(static::class, $args);
}
/**
* 设置监听对象
* @param mixed $object
*/
public function handle($object)
{
// Checks if a JSON-RCP request has been received
if ($this->app->request->method() !== 'POST' || $this->app->request->contentType() !== 'application/json') {
$this->printMethod($object);
} else {
// Reads the input data
$request = json_decode(file_get_contents('php://input'), true) ?: [];
if (empty($request)) {
$error = ['code' => '-32700', 'message' => lang('Syntax parsing error.'), 'meaning' => lang('Invalid JSON parameter.')];
$response = ['jsonrpc' => '2.0', 'id' => '0', 'result' => null, 'error' => $error];
} elseif (!isset($request['id']) || !isset($request['method']) || !isset($request['params'])) {
$error = ['code' => '-32600', 'message' => lang('Invalid request.'), 'meaning' => lang('Invalid JSON parameter.')];
$response = ['jsonrpc' => '2.0', 'id' => $request['id'] ?? '0', 'result' => null, 'error' => $error];
} else try {
if ($object instanceof \Exception) {
throw $object;
} elseif (strtolower($request['method']) === '_get_class_name_') {
$response = ['jsonrpc' => '2.0', 'id' => $request['id'], 'result' => get_class($object), 'error' => null];
} elseif (method_exists($object, $request['method'])) {
$result = call_user_func_array([$object, $request['method']], $request['params']);
$response = ['jsonrpc' => '2.0', 'id' => $request['id'], 'result' => $result, 'error' => null];
} else {
$info = lang('method not exists: %s::%s', [class_basename($object), $request['method']]);
$error = ['code' => '-32601', 'message' => $info, 'meaning' => lang('The method does not exist or is invalid.')];
$response = ['jsonrpc' => '2.0', 'id' => $request['id'], 'result' => null, 'error' => $error];
}
} catch (\think\admin\Exception $exception) {
$error = ['code' => $exception->getCode(), 'message' => lang($exception->getMessage()), 'meaning' => lang('Business Exception.')];
$response = ['jsonrpc' => '2.0', 'id' => $request['id'], 'result' => $exception->getData(), 'error' => $error];
} catch (\Exception $exception) {
$error = ['code' => $exception->getCode(), 'message' => lang($exception->getMessage()), 'meaning' => lang('System Exception.')];
$response = ['jsonrpc' => '2.0', 'id' => $request['id'], 'result' => null, 'error' => $error];
}
// Output the response
throw new HttpResponseException(json($response));
}
}
/**
* 打印输出对象方法
* @param mixed $object
*/
protected function printMethod($object)
{
try {
$object = new \ReflectionClass($object);
echo "<h2>{$object->getName()}</h2><hr>";
foreach ($object->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
if (stripos($method->getName(), '_') === 0) continue;
$params = [];
foreach ($method->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type instanceof \ReflectionType) $type = $type->getName();
$params[] = ($type ? "{$type} $" : '$') . $parameter->getName();
}
$params = count($params) > 0 ? join(', ', $params) : '';
echo '<div style="color:#666">' . nl2br($method->getDocComment() ?: '') . '</div>';
echo "<div style='color:#00E'>{$object->getShortName()}::{$method->getName()}({$params})</div><br>";
}
} catch (\Exception $exception) {
echo "<h3>[{$exception->getCode()}] {$exception->getMessage()}</h3>";
}
}
}

View File

@ -0,0 +1,244 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\extend;
use think\admin\Controller;
use think\admin\Exception;
use think\admin\Library;
/**
* 接口 JWT 接口扩展
* @class JwtExtend
* @package think\admin\extend
* @method static bool isRejwt() 是否输出令牌
* @method static array getInData() 获取输入数据
*/
class JwtExtend
{
// 头部参数
private const header = ['typ' => 'JWT', 'alg' => 'HS256'];
// 签名配置
private const signTypes = [
'HS256' => 'sha256',
'HS384' => 'sha384',
'HS512' => 'sha512'
];
/**
* 是否返回令牌
* @var boolean
*/
private static $rejwt = false;
/**
* 当前请求数据
* @var array
*/
private static $input = [];
/**
* 获取原会话标签
* @var string
*/
public static $sessionId = '';
/**
* 生成 jwt token
* @param array $data jwt 载荷 格式如下非必须
* {
* "iss": "http://example.org", // 签发者IssuerJWT的签发者
* "sub": "1234567890", // 主题SubjectJWT所面向的用户
* "aud": "http://example.com", // 受众Audience接收JWT的一方
* "exp": 1625174400, // 过期时间Expiration timeJWT的过期时间戳
* "iat": 1625138400, // 签发时间Issued atJWT的签发时间戳
* "nbf": 1625138400, // 生效时间Not BeforeJWT的生效时间戳
* "...": ... // 其他扩展内容
* }
* @param ?string $jwtkey 签名密钥
* @param ?boolean $rejwt 输出令牌
* @return string
*/
public static function token(array $data = [], ?string $jwtkey = null, ?bool $rejwt = null): string
{
$jwtkey = self::jwtkey($jwtkey);
if (is_bool($rejwt)) self::$rejwt = $rejwt;
// JWT 载荷数据组装
[$fields, $payload] = [['iss', 'sub', 'aud', 'exp', 'iat', 'nbf'], ['iat' => time()]];
foreach ($data as $k => $v) if (in_array($k, $fields)) {
$payload[$k] = $v;
unset($data[$k]);
}
// 自定义需要的数据
$data['.ssid'] = self::withSess();
if (empty($data['.ssid'])) unset($data['.ssid']);
$payload['enc'] = CodeExtend::encrypt(json_encode($data, JSON_UNESCAPED_UNICODE), $jwtkey);
// 组装 JWT 内容格式
$two = CodeExtend::enSafe64(json_encode($payload, JSON_UNESCAPED_UNICODE));
$one = CodeExtend::enSafe64(json_encode(self::header, JSON_UNESCAPED_UNICODE));
return "{$one}.{$two}." . self::withSign("{$one}.{$two}", self::header['alg'], $jwtkey);
}
/**
* 验证 token 是否有效, 默认验证 exp,nbf,iat 时间
* @param string $token 加密数据
* @param ?string $jwtkey 签名密钥
* @return array
* @throws \think\admin\Exception
*/
public static function verify(string $token, ?string $jwtkey = null): array
{
$tokens = explode('.', $token);
if (count($tokens) != 3) throw new Exception('数据解密失败!', 0, []);
[$base64header, $base64payload, $signature] = $tokens;
// 加密算法
$header = json_decode(CodeExtend::deSafe64($base64header), true);
if (empty($header['alg'])) throw new Exception('数据解密失败!', 0, []);
// 签名验证
$jwtkey = self::jwtkey($jwtkey);
if (self::withSign("{$base64header}.{$base64payload}", $header['alg'], $jwtkey) !== $signature) {
throw new Exception('验证签名失败!', 0, []);
}
// 获取 Playload 数据
$payload = json_decode(CodeExtend::deSafe64($base64payload), true);
// 签发时间大于当前服务器时间验证失败
if (isset($payload['iat']) && $payload['iat'] > time()) {
throw new Exception('服务器时间验证失败!', 0, $payload);
}
// 过期时间小于当前服务器时间验证失败
if (isset($payload['exp']) && $payload['exp'] < time()) {
throw new Exception('服务器时间验证失败!', 0, $payload);
}
// 该 nbf 时间之前不接收处理该 TOKEN
if (isset($payload['nbf']) && $payload['nbf'] > time()) {
throw new Exception('不接收处理该TOKEN', 0, $payload);
}
// 返回自定义数据字段
if (isset($payload['enc'])) {
$extra = json_decode(CodeExtend::decrypt($payload['enc'], $jwtkey), true);
if (!empty($extra['.ssid'])) self::$sessionId = $extra['.ssid'];
unset($payload['enc'], $extra['.ssid']);
}
return self::$input = array_merge($payload, $extra ?? []);
}
/**
* 获取 JWT 密钥
* @param ?string $jwtkey
* @return string
*/
public static function jwtkey(?string $jwtkey = null): string
{
try {
if (!empty($jwtkey)) return $jwtkey;
// 优先读取配置文件
$jwtkey = config('app.jwtkey');
if (!empty($jwtkey)) return $jwtkey;
// 再次读取数据配置
$jwtkey = sysconf('data.jwtkey|raw');
if (!empty($jwtkey)) return $jwtkey;
// 自动生成新的密钥
$jwtkey = md5(uniqid(strval(rand(1000, 9999)), true));
sysconf('data.jwtkey', $jwtkey);
return $jwtkey;
} catch (\Exception $exception) {
trace_file($exception);
return 'thinkadmin';
}
}
/**
* 输出模板变量
* @param \think\admin\Controller $class
* @param array $vars
* @return void
*/
public static function fetch(Controller $class, array $vars = [])
{
$ignore = array_keys(get_class_vars(Controller::class));
foreach ($class as $name => $value) if (!in_array($name, $ignore)) {
if (is_array($value) || is_numeric($value) || is_string($value) || is_bool($value) || is_null($value)) {
$vars[$name] = $value;
}
}
$class->success('获取变量成功!', $vars);
}
/**
* 获取原会话标识
* @return string
*/
private static function withSess(): string
{
if (!isset(Library::$sapp->session)) return self::$sessionId = '';
return self::$sessionId = Library::$sapp->session->getId();
}
/**
* 生成数据签名
* @param string $input base64UrlEncode(header).".".base64UrlEncode(payload)
* @param string $alg 算法方式
* @param ?string $key 签名密钥
* @return string
*/
private static function withSign(string $input, string $alg = 'HS256', ?string $key = null): string
{
return CodeExtend::enSafe64(hash_hmac(self::signTypes[$alg], $input, self::jwtkey($key), true));
}
/**
* 兼容历史方法
* @param string $method
* @param array $arguments
* @return array|string
* @throws \think\admin\Exception
*/
public static function __callStatic(string $method, array $arguments)
{
switch ($method) {
case 'isRejwt': // 是否返回令牌
return self::$rejwt;
case 'getInData': // 获取请求数据
return self::$input;
case 'getToken': // 生成接口令牌
return self::token(...$arguments);
case 'verifyToken': // 验证接口令牌
return self::verify(...$arguments);
default:
throw new Exception("method not exists: JwtExtend::{$method}()");
}
}
}

View File

@ -0,0 +1,309 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\extend;
use Exception;
use Phinx\Db\Adapter\AdapterInterface;
use think\admin\Library;
use think\admin\model\SystemMenu;
use think\admin\service\ProcessService;
use think\helper\Str;
/**
* 数据库迁移扩展
* @class PhinxExtend
* @package think\admin\extend
*/
class PhinxExtend
{
/**
* 批量写入菜单
* @param array $zdata 菜单数据
* @param mixed $exists 检测条件
* @return boolean
*/
public static function write2menu(array $zdata, $exists = []): bool
{
// 检查是否需要写入菜单
try {
if (!empty($exists) && SystemMenu::mk()->where($exists)->findOrEmpty()->isExists()) {
return false;
}
} catch (Exception $exception) {
return false;
}
// 循环写入系统菜单数据
foreach ($zdata as $one) {
$pid1 = static::write1menu($one);
if (!empty($one['subs'])) foreach ($one['subs'] as $two) {
$pid2 = static::write1menu($two, $pid1);
if (!empty($two['subs'])) foreach ($two['subs'] as $thr) {
static::write1menu($thr, $pid2);
}
}
}
return true;
}
/**
* 单个写入菜单
* @param array $menu 菜单数据
* @param integer $ppid 上级菜单
* @return integer
*/
private static function write1menu(array $menu, int $ppid = 0): int
{
return (int)SystemMenu::mk()->insertGetId([
'pid' => $ppid,
'url' => empty($menu['url']) ? (empty($menu['node']) ? '#' : $menu['node']) : $menu['url'],
'sort' => $menu['sort'] ?? 0,
'icon' => $menu['icon'] ?? '',
'node' => empty($menu['node']) ? (empty($menu['url']) ? '' : $menu['url']) : $menu['node'],
'title' => $menu['name'] ?? ($menu['title'] ?? ''),
'params' => $menu['params'] ?? '',
'target' => $menu['target'] ?? '_self',
]);
}
/**
* 创建数据库安装脚本
* @param array $tables
* @param string $class
* @return string[]
* @throws \Exception
*/
public static function create2table(array $tables = [], string $class = 'InstallTable'): array
{
$br = "\r\n";
$content = static::_build2table($tables, true);
$content = substr($content, strpos($content, "\n") + 1);
$content = '<?php' . "{$br}{$br}use think\migration\Migrator;{$br}{$br}@set_time_limit(0);{$br}@ini_set('memory_limit', -1);{$br}{$br}class {$class} extends Migrator {{$br}{$content}}{$br}";
return ['file' => static::nextFile($class), 'text' => $content];
}
/**
* 创建数据库备份脚本
* @param array $tables
* @param string $class
* @param boolean $progress
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public static function create2backup(array $tables = [], string $class = 'InstallPackage', bool $progress = true): array
{
// 处理菜单数据
[$menuData, $menuList] = [[], SystemMenu::mk()->where(['status' => 1])->order('sort desc,id asc')->select()->toArray()];
foreach (DataExtend::arr2tree($menuList) as $sub1) {
$one = ['name' => $sub1['title'], 'icon' => $sub1['icon'], 'url' => $sub1['url'], 'node' => $sub1['node'], 'params' => $sub1['params'], 'subs' => []];
if (!empty($sub1['sub'])) foreach ($sub1['sub'] as $sub2) {
$two = ['name' => $sub2['title'], 'icon' => $sub2['icon'], 'url' => $sub2['url'], 'node' => $sub2['node'], 'params' => $sub2['params'], 'subs' => []];
if (!empty($sub2['sub'])) foreach ($sub2['sub'] as $sub3) {
$two['subs'][] = ['name' => $sub3['title'], 'url' => $sub3['url'], 'node' => $sub3['node'], 'icon' => $sub3['icon'], 'params' => $sub3['params']];
}
if (empty($two['subs'])) unset($two['subs']);
$one['subs'][] = $two;
}
if (empty($one['subs'])) unset($one['subs']);
$menuData[] = $one;
}
// 备份数据表
[$extra, $version] = [[], strstr($filename = static::nextFile($class), '_', true)];
if (count($tables) > 0) foreach ($tables as $table) {
if (($count = ($db = Library::$sapp->db->table($table))->count()) > 0) {
$dataFileName = "{$version}/{$table}.data";
$dataFilePath = syspath("database/migrations/{$dataFileName}");
is_dir($dataDirectory = dirname($dataFilePath)) || mkdir($dataDirectory, 0777, true);
$progress && ProcessService::message(" -- Starting write {$table}.data ..." . PHP_EOL);
[$used, $fp] = [0, fopen($dataFilePath, 'w+')];
foreach ($db->cursor() as $item) {
fwrite($fp, json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\r\n");
if ($progress && ($number = sprintf("%.4f", (++$used / $count) * 100) . '%')) {
ProcessService::message(" -- -- write {$table}.data: {$used}/{$count} {$number}", 1);
}
}
fclose($fp);
$extra[$table] = $dataFileName;
$progress && ProcessService::message(" -- Finished write {$table}.data, Total {$used} rows.", 2);
}
}
// 生成迁移脚本
$template = file_get_contents(dirname(__DIR__) . '/service/bin/package.stub');
$dataJson = json_encode($extra, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$menuJson = json_encode($menuData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$replaces = ['__CLASS__' => $class, '__MENU_JSON__' => $menuJson, '__DATA_JSON__' => $dataJson];
return ['file' => $filename, 'text' => str_replace(array_keys($replaces), array_values($replaces), $template)];
}
/**
* 数组转代码
* @param array $data
* @return string
*/
private static function _arr2str(array $data): string
{
return preg_replace(['#\s+#', '#, \)$#', '#^array \( #'], [' ', ']', '[',], var_export($data, true));
}
/**
* 生成数据库表格创建模板
* @param array $tables 指定数据表
* @param boolean $rehtml 是否返回内容
* @return string
* @throws \Exception
*/
private static function _build2table(array $tables = [], bool $rehtml = false): string
{
$br = "\r\n";
$connect = Library::$sapp->db->connect();
if ($connect->getConfig('type') !== 'mysql') {
throw new Exception(' ** Notify: 只支持 MySql 数据库生成数据库脚本');
}
$schema = $connect->getConfig('database');
$content = '<?php' . "{$br}{$br}\t/**{$br}\t * 创建数据库{$br}\t */{$br}\t public function change() {";
foreach ($tables as $table) $content .= "{$br}\t\t\$this->_create_{$table}();";
$content .= "{$br}{$br}\t}{$br}{$br}";
// 字段默认长度
$sizes = ['tinyint' => 4, 'smallint' => 6, 'mediumint' => 9, 'int' => 11, 'bigint' => 20];
// 字段类型转换 ( 仅需定义与MySQL不同的配置 )
$types = [
// 整形数字
'tinyint' => AdapterInterface::PHINX_TYPE_TINY_INTEGER,
'smallint' => AdapterInterface::PHINX_TYPE_SMALL_INTEGER,
'int' => AdapterInterface::PHINX_TYPE_INTEGER,
'bigint' => AdapterInterface::PHINX_TYPE_BIG_INTEGER,
// 字符类型
'varchar' => AdapterInterface::PHINX_TYPE_STRING,
'tinytext' => AdapterInterface::PHINX_TYPE_TEXT,
'mediumtext' => AdapterInterface::PHINX_TYPE_TEXT,
'longtext' => AdapterInterface::PHINX_TYPE_TEXT,
// 仅 mysql 有的字段需要单独处理
'set' => AdapterInterface::PHINX_TYPE_STRING,
'enum' => AdapterInterface::PHINX_TYPE_STRING,
'year' => AdapterInterface::PHINX_TYPE_INTEGER,
'mediumint' => AdapterInterface::PHINX_TYPE_INTEGER,
'tinyblob' => AdapterInterface::PHINX_TYPE_BLOB,
'longblob' => AdapterInterface::PHINX_TYPE_BLOB,
'mediumblob' => AdapterInterface::PHINX_TYPE_BLOB,
];
foreach ($tables as $table) {
// 读取数据表 - 备注参数
$comment = Library::$sapp->db->table('information_schema.TABLES')->where([
'TABLE_SCHEMA' => $schema, 'TABLE_NAME' => $table,
])->value('TABLE_COMMENT', '');
// 读取数据表 - 自动生成结构
$class = Str::studly($table);
$content .= <<<CODE
/**
* 创建数据对象
* @class {$class}
* @table {$table}
* @return void
*/
private function _create_{$table}() {
// 当前数据表
\$table = '{$table}';
// 存在则跳过
if (\$this->hasTable(\$table)) return;
// 创建数据表
\$this->table(\$table, [
'engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => '{$comment}',
])
CODE;
foreach (Library::$sapp->db->getFields($table) as $field) {
if ($field['name'] === 'id') continue;
$type = $types[$field['type']] ?? $field['type'];
$data = ['default' => $field['default'], 'null' => empty($field['notnull']), 'comment' => $field['comment'] ?? ''];
if ($field['type'] === 'enum') {
$type = $types[$field['type']] ?? 'string';
$data = array_merge(['limit' => 10], $data);
} elseif (preg_match('/(tinyblob|blob|mediumblob|longblob|varbinary|bit|binary|varchar|char)\((\d+)\)/', $field['type'], $attr)) {
$type = $types[$attr[1]] ?? 'string';
$data = array_merge(['limit' => intval($attr[2])], $data);
} elseif (preg_match('/(tinyint|smallint|mediumint|int|bigint)\((\d+)\)/', $field['type'], $attr)) {
$type = $types[$attr[1]] ?? 'integer';
$data = array_merge(['limit' => intval($attr[2])], $data, ['default' => intval($data['default'])]);
} elseif (preg_match('/(tinyint|smallint|mediumint|int|bigint)\s+unsigned/i', $field['type'], $attr)) {
$type = $types[$attr[1]] ?? 'integer';
if (isset($sizes[$attr[1]])) {
$data = array_merge(['limit' => $sizes[$attr[1]]], $data);
}
$data['default'] = intval($data['default']);
} elseif (preg_match('/(float|decimal)\((\d+),(\d+)\)/', $field['type'], $attr)) {
$type = $types[$attr[1]] ?? 'decimal';
$data = array_merge(['precision' => intval($attr[2]), 'scale' => intval($attr[3])], $data);
}
$params = static::_arr2str($data);
$content .= "{$br}\t\t->addColumn('{$field["name"]}','{$type}',{$params})";
}
// 读取数据表 - 自动生成索引
$idxs = [];
$indexs = Library::$sapp->db->connect()->query("show index from {$table}");
foreach ($indexs as $index) {
if ($index['Key_name'] === 'PRIMARY') continue;
$short = substr(md5($index['Table']), 0, 9);
$params = static::_arr2str(['name' => "i{$short}_{$index['Column_name']}"]);
$idxs[] = "{$br}\t\t->addIndex('{$index['Column_name']}', {$params})";
}
usort($idxs, function ($a, $b) {
return strlen($a) <=> strlen($b);
});
$content .= join('', $idxs);
$content .= "{$br}\t\t->create();{$br}{$br}\t\t// 修改主键长度";
$content .= "{$br}\t\t\$this->table(\$table)->changeColumn('id', 'integer', ['limit' => 11, 'identity' => true]);";
$content .= "{$br}\t}{$br}{$br}";
}
return $rehtml ? $content : highlight_string($content, true);
}
/**
* 生成下一个脚本名称
* @param string $class 脚本类名
* @return string
*/
private static function nextFile(string $class): string
{
[$filename, $versions, $start] = [Str::snake($class), [], 20009999999999];
if (count($files = glob(syspath('database/migrations/*.php'))) > 0) {
foreach ($files as $file) {
$versions[] = $version = intval(substr($bname = pathinfo($file, 8), 0, 14));
if ($filename === substr($bname, 15) && unlink($file)) {
echo " ** Notify: Class {$class} already exists and has been replaced." . PHP_EOL;
if (is_dir($dataPath = dirname($file) . DIRECTORY_SEPARATOR . $version)) {
ToolsExtend::removeEmptyDirectory($dataPath);
}
}
}
$version = min($versions) - 1;
}
if (!isset($version) || $version > $start) $version = $start;
return "{$version}_{$filename}.php";
}
}

View File

@ -0,0 +1,141 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\extend;
use Closure;
use FilesystemIterator;
use Generator;
use SplFileInfo;
/**
* 通用工具扩展
* @class ToolsExtend
* @package think\admin\extend
*/
class ToolsExtend
{
/**
* 深度拷贝到指定目录
* @param string $frdir 来源目录
* @param string $todir 目标目录
* @param array $files 指定文件
* @param boolean $force 强制替换
* @param boolean $remove 删除文件
* @return boolean
*/
public static function copyfile(string $frdir, string $todir, array $files = [], bool $force = true, bool $remove = true): bool
{
$frdir = rtrim($frdir, '\\/') . DIRECTORY_SEPARATOR;
$todir = rtrim($todir, '\\/') . DIRECTORY_SEPARATOR;
// 扫描目录文件
if (empty($files) && is_dir($frdir)) {
$files = static::findFilesArray($frdir, static function (SplFileInfo $info) {
return substr($info->getBasename(), 0, 1) !== '.';
}, static function (SplFileInfo $info) {
return substr($info->getBasename(), 0, 1) !== '.';
});
}
// 复制文件列表
foreach ($files as $target) {
if ($force || !is_file($todir . $target)) {
$dir = dirname($todir . $target);
is_dir($dir) or mkdir($dir, 0777, true);
copy($frdir . $target, $todir . $target);
}
// 删除来源文件
$remove && unlink($frdir . $target);
}
// 删除来源目录
$remove && static::removeEmptyDirectory($frdir);
return true;
}
/**
* 扫描目录列表
* @param string $path 扫描目录
* @param string $filterExt 筛选后缀
* @param boolean $shortPath 相对路径
* @return array
*/
public static function scanDirectory(string $path, string $filterExt = '', bool $shortPath = true): array
{
return static::findFilesArray($path, static function (SplFileInfo $info) use ($filterExt) {
return empty($filterExt) || $info->getExtension() === $filterExt;
}, static function (SplFileInfo $info) {
return substr($info->getBasename(), 0, 1) !== '.';
}, $shortPath);
}
/**
* 扫描指定目录
* @param string $path
* @param ?Closure $filterFile
* @param ?Closure $filterPath
* @param boolean $shortPath
* @return array
*/
public static function findFilesArray(string $path, ?Closure $filterFile = null, ?Closure $filterPath = null, bool $shortPath = true): array
{
$items = [];
if (file_exists($path)) {
$files = static::findFilesYield($path, $filterFile, $filterPath);
foreach ($files as $file) $items[] = $file->getRealPath();
if ($shortPath && ($offset = strlen(realpath($path)) + 1)) {
foreach ($items as &$item) $item = substr($item, $offset);
}
}
return $items;
}
/**
* 扫描指定目录
* @param string $path
* @param \Closure|null $filterFile
* @param \Closure|null $filterPath
* @param boolean $fullDirectory
* @return \Generator|\SplFileInfo[]
*/
public static function findFilesYield(string $path, ?Closure $filterFile = null, ?Closure $filterPath = null, bool $fullDirectory = false): Generator
{
if (file_exists($path)) {
$items = is_file($path) ? [new SplFileInfo($path)] : new FilesystemIterator($path);
foreach ($items as $item) if ($item->isDir() && !$item->isLink()) {
if (is_null($filterPath) || $filterPath($item)) {
yield from static::findFilesYield($item->getPathname(), $filterFile, $filterPath, $fullDirectory);
}
$fullDirectory && yield $item;
} elseif (is_null($filterFile) || $filterFile($item)) {
yield $item;
}
}
}
/**
* 移除清空目录
* @param string $path
* @return boolean
*/
public static function removeEmptyDirectory(string $path): bool
{
foreach (self::findFilesYield($path, null, null, true) as $item) {
($item->isFile() || $item->isLink()) ? unlink($item->getRealPath()) : rmdir($item->getRealPath());
}
return is_file($path) ? unlink($path) : (!is_dir($path) || rmdir($path));
}
}

View File

@ -0,0 +1,190 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\extend;
use think\admin\contract\StreamInterface;
use think\Model;
/**
* 虚拟模型构建协议
* @class VirtualModel
* @package think\admin\extend
*/
class VirtualModel extends \stdClass implements StreamInterface
{
/**
* 虚拟模型模板
* @var string
*/
private $template;
/**
* 读取进度标量
* @var integer
*/
private $position;
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
{
// 解析链接参数
$attr = parse_url($path);
if (empty($attr['fragment'])) $attr['fragment'] = '';
$type = strtolower($attr['fragment'] ?: 'default');
// 生成模型代码
$this->position = 0;
$this->template = '<?php ';
$this->template .= "namespace virtual\\model\\_{$type}; ";
$this->template .= "class {$attr['host']} extends \\think\\admin\\Model { ";
if (!empty($attr['fragment'])) {
$this->template .= "protected \$connection='{$attr['fragment']}'; ";
}
$this->template .= '}';
return true;
}
public function stream_read(int $count)
{
$content = substr($this->template, $this->position, $count);
$this->position += strlen($content);
return $content;
}
public function stream_eof(): bool
{
return $this->position >= strlen($this->template);
}
public function stream_cast(int $cast_as)
{
}
public function stream_close(): void
{
}
public function stream_flush(): bool
{
return true;
}
public function stream_lock(int $operation): bool
{
return true;
}
public function stream_set_option(int $option, int $arg1, int $arg2): bool
{
return true;
}
public function stream_metadata(string $path, int $option, $value): bool
{
return true;
}
public function stream_stat()
{
return stat(__FILE__);
}
public function stream_tell(): int
{
return $this->position;
}
public function stream_truncate(int $new_size): bool
{
return true;
}
public function stream_seek(int $offset, int $whence = SEEK_SET): bool
{
return true;
}
public function stream_write(string $data): int
{
return strlen($data);
}
public function dir_opendir(string $path, int $options): bool
{
return true;
}
public function dir_readdir(): string
{
return __DIR__;
}
public function dir_closedir(): bool
{
return true;
}
public function dir_rewinddir(): bool
{
return true;
}
public function mkdir(string $path, int $mode, int $options): bool
{
return true;
}
public function rmdir(string $path, int $options): bool
{
return true;
}
public function rename(string $path_from, string $path_to): bool
{
return true;
}
public function unlink(string $path): bool
{
return true;
}
public function url_stat(string $path, int $flags)
{
return stat(__FILE__);
}
/**
* 创建虚拟模型
* @param mixed $name 模型名称
* @param array $data 模型数据
* @param mixed $conn 默认链接
* @return \think\Model
*/
public static function mk(string $name, array $data = [], string $conn = ''): Model
{
$type = strtolower($conn ?: 'default');
if (!class_exists($class = "\\virtual\\model\\_{$type}\\{$name}")) {
if (!in_array('model', stream_get_wrappers())) {
stream_wrapper_register('model', static::class);
}
include "model://{$name}#{$conn}";
}
return new $class($data);
}
}

View File

@ -0,0 +1,95 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\helper;
use think\admin\Helper;
use think\db\BaseQuery;
use think\Model;
/**
* 通用删除管理器
* @class DeleteHelper
* @package think\admin\helper
*/
class DeleteHelper extends Helper
{
/**
* 逻辑器初始化
* @param BaseQuery|Model|string $dbQuery
* @param string $field 操作数据主键
* @param mixed $where 额外更新条件
* @return boolean|void
* @throws \think\db\exception\DbException
*/
public function init($dbQuery, string $field = '', $where = [])
{
$query = static::buildQuery($dbQuery);
$field = $field ?: ($query->getPk() ?: 'id');
$value = $this->app->request->post($field);
// 查询限制处理
if (!empty($where)) $query->where($where);
if (!isset($where[$field]) && is_string($value)) {
$query->whereIn($field, str2arr($value));
}
// 前置回调处理
if (false === $this->class->callback('_delete_filter', $query, $where)) {
return false;
}
// 阻止危险操作
if (!$query->getOptions('where')) {
$this->class->error('数据删除失败!');
}
// 组装执行数据
$data = [];
if (method_exists($query, 'getTableFields')) {
$fields = $query->getTableFields();
if (in_array('deleted', $fields)) $data['deleted'] = 1;
if (in_array('is_deleted', $fields)) $data['is_deleted'] = 1;
if (isset($data['deleted']) || isset($data['is_deleted'])) {
if (in_array('deleted_at', $fields)) $data['deleted_at'] = date('Y-m-d H:i:s');
if (in_array('deleted_time', $fields)) $data['deleted_time'] = time();
}
}
// 执行删除操作
if ($result = (empty($data) ? $query->delete() : $query->update($data)) !== false) {
// 模型自定义事件回调
$model = $query->getModel();
if ($model instanceof \think\admin\Model) {
$model->onAdminDelete(strval($value));
}
}
// 结果回调处理
if (false === $this->class->callback('_delete_result', $result)) {
return $result;
}
// 回复返回结果
if ($result !== false) {
$this->class->success('数据删除成功!', '');
} else {
$this->class->error('数据删除失败!');
}
}
}

View File

@ -0,0 +1,79 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\helper;
use think\admin\Helper;
use think\admin\service\SystemService;
use think\db\BaseQuery;
use think\Model;
/**
* 表单视图管理器
* @class FormHelper
* @package think\admin\helper
*/
class FormHelper extends Helper
{
/**
* 逻辑器初始化
* @param BaseQuery|Model|string $dbQuery
* @param string $template 视图模板名称
* @param string $field 指定数据主键
* @param mixed $where 限定更新条件
* @param array $edata 表单扩展数据
* @return void|array|boolean
* @throws \think\admin\Exception
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function init($dbQuery, string $template = '', string $field = '', $where = [], array $edata = [])
{
$query = static::buildQuery($dbQuery);
$field = $field ?: ($query->getPk() ?: 'id');
$value = $edata[$field] ?? input($field);
if ($this->app->request->isGet()) {
if ($value !== null) {
$exist = $query->where([$field => $value])->where($where)->find();
if ($exist instanceof Model) $exist = $exist->toArray();
$edata = array_merge($edata, $exist ?: []);
}
if (false !== $this->class->callback('_form_filter', $edata)) {
$this->class->fetch($template, ['vo' => $edata]);
} else {
return $edata;
}
}
if ($this->app->request->isPost()) {
$edata = array_merge($this->app->request->post(), $edata);
if (false !== $this->class->callback('_form_filter', $edata, $where)) {
$result = SystemService::save($query, $edata, $field, $where) !== false;
if (false !== $this->class->callback('_form_result', $result, $edata)) {
if ($result !== false) {
$this->class->success('数据保存成功!');
} else {
$this->class->error('数据保存失败!');
}
}
return $result;
}
}
}
}

View File

@ -0,0 +1,194 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\helper;
use think\admin\Helper;
use think\admin\service\AdminService;
use think\db\BaseQuery;
use think\db\Query;
use think\exception\HttpResponseException;
use think\Model;
/**
* 列表处理管理器
* @class PageHelper
* @package think\admin\helper
*/
class PageHelper extends Helper
{
/**
* 逻辑器初始化
* @param BaseQuery|Model|string $dbQuery
* @param boolean|integer $page 是否分页或指定分页
* @param boolean $display 是否渲染模板
* @param boolean|integer $total 集合分页记录数
* @param integer $limit 集合每页记录数
* @param string $template 模板文件名称
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function init($dbQuery, $page = true, bool $display = true, $total = false, int $limit = 0, string $template = ''): array
{
$query = $this->autoSortQuery($dbQuery);
if ($page !== false) {
$get = $this->app->request->get();
$limits = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200];
if ($limit <= 1) {
$limit = $get['limit'] ?? $this->app->cookie->get('limit', 20);
if (in_array($limit, $limits) && ($get['not_cache_limit'] ?? 0) < 1) {
$this->app->cookie->set('limit', ($limit = intval($limit >= 5 ? $limit : 20)) . '');
}
}
$inner = strpos($get['spm'] ?? '', 'm-') === 0;
$prefix = $inner ? (sysuri('admin/index/index') . '#') : '';
// 生成分页数据
$config = ['list_rows' => $limit, 'query' => $get];
if (is_numeric($page)) $config['page'] = $page;
$data = ($paginate = $query->paginate($config, $this->getCount($query, $total)))->toArray();
$result = ['page' => ['limit' => $data['per_page'], 'total' => $data['total'], 'pages' => $data['last_page'], 'current' => $data['current_page']], 'list' => $data['data']];
// 分页跳转参数
$select = "<select onchange='location.href=this.options[this.selectedIndex].value'>";
if (in_array($limit, $limits)) foreach ($limits as $num) {
$get = array_merge($get, ['limit' => $num, 'page' => 1]);
$url = $this->app->request->baseUrl() . '?' . http_build_query($get, '', '&', PHP_QUERY_RFC3986);
$select .= sprintf('<option data-num="%d" value="%s" %s>%d</option>', $num, $prefix . $url, $limit === $num ? 'selected' : '', $num);
} else {
$select .= "<option selected>{$limit}</option>";
}
$html = lang('共 %s 条记录,每页显示 %s 条,共 %s 页当前显示第 %s 页。', [$data['total'], "{$select}</select>", $data['last_page'], $data['current_page']]);
$link = $inner ? str_replace('<a href="', '<a data-open="' . $prefix, $paginate->render() ?: '') : ($paginate->render() ?: '');
$this->class->assign('pagehtml', "<div class='pagination-container nowrap'><span>{$html}</span>{$link}</div>");
} else {
$result = ['list' => $query->select()->toArray()];
}
if (false !== $this->class->callback('_page_filter', $result['list'], $result) && $display) {
if ($this->output === 'get.json') {
$this->class->success('JSON-DATA', $result);
} else {
$this->class->fetch($template, $result);
}
}
return $result;
}
/**
* 组件 Layui.Table 处理
* @param BaseQuery|Model|string $dbQuery
* @param string $template
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function layTable($dbQuery, string $template = ''): array
{
if ($this->output === 'get.json') {
$get = $this->app->request->get();
$query = static::buildQuery($dbQuery);
// 根据参数排序
if (isset($get['_field_']) && isset($get['_order_'])) {
$dbQuery->order("{$get['_field_']} {$get['_order_']}");
}
return PageHelper::instance()->init($query);
}
if ($this->output === 'get.layui.table') {
$get = $this->app->request->get();
$query = $this->autoSortQuery($dbQuery);
// 根据参数排序
if (isset($get['_field_']) && isset($get['_order_'])) {
$query->order("{$get['_field_']} {$get['_order_']}");
}
// 数据分页处理
if (empty($get['page']) || empty($get['limit'])) {
$data = $query->select()->toArray();
$result = ['msg' => '', 'code' => 0, 'count' => count($data), 'data' => $data];
} else {
$cfg = ['list_rows' => $get['limit'], 'query' => $get];
$data = $query->paginate($cfg, static::getCount($query))->toArray();
$result = ['msg' => '', 'code' => 0, 'count' => $data['total'], 'data' => $data['data']];
}
if (false !== $this->class->callback('_page_filter', $result['data'], $result)) {
static::xssFilter($result['data']);
throw new HttpResponseException(json($result));
} else {
return $result;
}
} else {
$this->class->fetch($template);
return [];
}
}
/**
* 输出 XSS 过滤处理
* @param array $items
*/
private static function xssFilter(array &$items)
{
foreach ($items as &$item) if (is_array($item)) {
static::xssFilter($item);
} elseif (is_string($item)) {
$item = htmlspecialchars($item, ENT_QUOTES);
}
}
/**
* 查询对象数量统计
* @param BaseQuery|Query $query
* @param boolean|integer $total
* @return integer|boolean|string
* @throws \think\db\exception\DbException
*/
private static function getCount($query, $total = false)
{
if ($total === true || is_numeric($total)) return $total;
[$query, $options] = [clone $query, $query->getOptions()];
if (isset($options['order'])) $query->removeOption('order');
if (empty($options['union'])) return $query->count();
$table = [$query->buildSql() => '_union_count_'];
return $query->newQuery()->table($table)->count();
}
/**
* 绑定排序并返回操作对象
* @param BaseQuery|Model|string $dbQuery
* @param string $field 指定排序字段
* @return \think\db\Query
* @throws \think\db\exception\DbException
*/
public function autoSortQuery($dbQuery, string $field = 'sort'): Query
{
$query = static::buildQuery($dbQuery);
if ($this->app->request->isPost() && $this->app->request->post('action') === 'sort') {
AdminService::isLogin() or $this->class->error('请重新登录!');
if (method_exists($query, 'getTableFields') && in_array($field, $query->getTableFields())) {
if ($this->app->request->has($pk = $query->getPk() ?: 'id', 'post')) {
$map = [$pk => $this->app->request->post($pk, 0)];
$data = [$field => intval($this->app->request->post($field, 0))];
$query->newQuery()->where($map)->update($data);
$this->class->success('列表排序成功!', '');
}
}
$this->class->error('列表排序失败!');
}
return $query;
}
}

View File

@ -0,0 +1,398 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\helper;
use think\admin\Helper;
use think\admin\service\SystemService;
use think\Container;
use think\db\BaseQuery;
use think\db\Query;
use think\Model;
/**
* 搜索条件处理器
* @see \think\db\Query
* @mixin \think\db\Query
* @class QueryHelper
* @package think\admin\helper
*
* --- 动态方法调用声明
* @method bool mSave(array $data = [], string $field = '', mixed $where = []) 快捷更新
* @method bool mDelete(string $field = '', mixed $where = []) 快捷删除
* @method bool|array mForm(string $tpl = '', string $field = '', mixed $where = [], array $data = []) 快捷表单
* @method bool|integer mUpdate(array $data = [], string $field = '', mixed $where = []) 快捷保存
*/
class QueryHelper extends Helper
{
/**
* 分页助手工具
* @var PageHelper
*/
protected $page;
/**
* 当前数据操作
* @var Query
*/
protected $query;
/**
* 初始化默认数据
* @var array
*/
protected $input;
/**
* 获取当前Db操作对象
* @return Query
*/
public function db(): Query
{
return $this->query;
}
/**
* 逻辑器初始化
* @param BaseQuery|Model|string $dbQuery
* @param string|array|null $input 输入数据
* @param callable|null $callable 初始回调
* @return $this
* @throws \think\db\exception\DbException
*/
public function init($dbQuery, $input = null, ?callable $callable = null): QueryHelper
{
$this->page = PageHelper::instance();
$this->input = $this->getInputData($input);
$this->query = $this->page->autoSortQuery($dbQuery);
is_callable($callable) && call_user_func($callable, $this, $this->query);
return $this;
}
/**
* 设置 Like 查询条件
* @param string|array $fields 查询字段
* @param string $split 前后分割符
* @param string|array|null $input 输入数据
* @param string $alias 别名分割符
* @return $this
*/
public function like($fields, string $split = '', $input = null, string $alias = '#'): QueryHelper
{
$data = $this->getInputData($input ?: $this->input);
foreach (is_array($fields) ? $fields : explode(',', $fields) as $field) {
[$dk, $qk] = [$field, $field];
if (stripos($field, $alias) !== false) {
[$dk, $qk] = explode($alias, $field);
}
if (isset($data[$qk]) && $data[$qk] !== '') {
$this->query->whereLike($dk, "%{$split}{$data[$qk]}{$split}%");
}
}
return $this;
}
/**
* 设置 Equal 查询条件
* @param string|array $fields 查询字段
* @param string|array|null $input 输入类型
* @param string $alias 别名分割符
* @return $this
*/
public function equal($fields, $input = null, string $alias = '#'): QueryHelper
{
$data = $this->getInputData($input ?: $this->input);
foreach (is_array($fields) ? $fields : explode(',', $fields) as $field) {
[$dk, $qk] = [$field, $field];
if (stripos($field, $alias) !== false) {
[$dk, $qk] = explode($alias, $field);
}
if (isset($data[$qk]) && $data[$qk] !== '') {
$this->query->where($dk, strval($data[$qk]));
}
}
return $this;
}
/**
* 设置 IN 区间查询
* @param string|array $fields 查询字段
* @param string $split 输入分隔符
* @param string|array|null $input 输入数据
* @param string $alias 别名分割符
* @return $this
*/
public function in($fields, string $split = ',', $input = null, string $alias = '#'): QueryHelper
{
$data = $this->getInputData($input ?: $this->input);
foreach (is_array($fields) ? $fields : explode(',', $fields) as $field) {
[$dk, $qk] = [$field, $field];
if (stripos($field, $alias) !== false) {
[$dk, $qk] = explode($alias, $field);
}
if (isset($data[$qk]) && $data[$qk] !== '') {
$this->query->whereIn($dk, explode($split, strval($data[$qk])));
}
}
return $this;
}
/**
* 两字段范围查询
* @example field1:field2#field,field11:field22#field00
* @param string|array $fields 查询字段
* @param string|array|null $input 输入数据
* @param string $alias 别名分割符
* @return $this
*/
public function valueRange($fields, $input = null, string $alias = '#'): QueryHelper
{
$data = $this->getInputData($input ?: $this->input);
foreach (is_array($fields) ? $fields : explode(',', $fields) as $field) {
if (strpos($field, ':') !== false) {
if (stripos($field, $alias) !== false) {
[$dk0, $qk0] = explode($alias, $field);
[$dk1, $dk2] = explode(':', $dk0);
} else {
[$qk0] = [$dk1, $dk2] = explode(':', $field, 2);
}
if (isset($data[$qk0]) && $data[$qk0] !== '') {
$this->query->where([[$dk1, '<=', $data[$qk0]], [$dk2, '>=', $data[$qk0]]]);
}
}
}
return $this;
}
/**
* 设置内容区间查询
* @param string|array $fields 查询字段
* @param string $split 输入分隔符
* @param string|array|null $input 输入数据
* @param string $alias 别名分割符
* @return $this
*/
public function valueBetween($fields, string $split = ' ', $input = null, string $alias = '#'): QueryHelper
{
return $this->setBetweenWhere($fields, $split, $input, $alias);
}
/**
* 设置日期时间区间查询
* @param string|array $fields 查询字段
* @param string $split 输入分隔符
* @param string|array|null $input 输入数据
* @param string $alias 别名分割符
* @return $this
*/
public function dateBetween($fields, string $split = ' - ', $input = null, string $alias = '#'): QueryHelper
{
return $this->setBetweenWhere($fields, $split, $input, $alias, static function ($value, $type) {
if (preg_match('#^\d{4}(-\d\d){2}\s+\d\d(:\d\d){2}$#', $value)) return $value;
return $type === 'after' ? "{$value} 23:59:59" : "{$value} 00:00:00";
});
}
/**
* 设置时间戳区间查询
* @param string|array $fields 查询字段
* @param string $split 输入分隔符
* @param string|array|null $input 输入数据
* @param string $alias 别名分割符
* @return $this
*/
public function timeBetween($fields, string $split = ' - ', $input = null, string $alias = '#'): QueryHelper
{
return $this->setBetweenWhere($fields, $split, $input, $alias, static function ($value, $type) {
if (preg_match('#^\d{4}(-\d\d){2}\s+\d\d(:\d\d){2}$#', $value)) return strtotime($value);
return $type === 'after' ? strtotime("{$value} 23:59:59") : strtotime("{$value} 00:00:00");
});
}
/**
* 实例化分页管理器
* @param boolean|integer $page 是否启用分页
* @param boolean $display 是否渲染模板
* @param boolean|integer $total 集合分页记录数
* @param integer $limit 集合每页记录数
* @param string $template 模板文件名称
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function page($page = true, bool $display = true, $total = false, int $limit = 0, string $template = ''): array
{
return $this->page->init($this->query, $page, $display, $total, $limit, $template);
}
/**
* 清空数据并保留表结构
* @return $this
* @throws \think\db\exception\DbException
*/
public function empty(): QueryHelper
{
$table = $this->query->getTable();
$ctype = strtolower($this->query->getConfig('type'));
if ($ctype === 'mysql') {
$this->query->getConnection()->execute("truncate table `{$table}`");
} elseif (in_array($ctype, ['sqlsrv', 'oracle', 'pgsql'])) {
$this->query->getConnection()->execute("truncate table {$table}");
} else {
$this->query->newQuery()->whereRaw('1=1')->delete();
}
return $this;
}
/**
* 中间回调处理
* @param callable $after
* @return $this
*/
public function filter(callable $after): QueryHelper
{
call_user_func($after, $this, $this->query);
return $this;
}
/**
* Layui.Table 组件数据
* @param ?callable $befor 表单前置操作
* @param ?callable $after 表单后置操作
* @param string $template 视图模板文件
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function layTable(?callable $befor = null, ?callable $after = null, string $template = '')
{
if (in_array($this->output, ['get.json', 'get.layui.table'])) {
if (is_callable($after)) {
call_user_func($after, $this, $this->query);
}
$this->page->layTable($this->query, $template);
} else {
if (is_callable($befor)) {
call_user_func($befor, $this, $this->query);
}
$this->class->fetch($template);
}
}
/**
* 设置区域查询条件
* @param string|array $fields 查询字段
* @param string $split 输入分隔符
* @param string|array|null $input 输入数据
* @param string $alias 别名分割符
* @param callable|null $callback 回调函数
* @return $this
*/
private function setBetweenWhere($fields, string $split = ' ', $input = null, string $alias = '#', ?callable $callback = null): QueryHelper
{
$data = $this->getInputData($input ?: $this->input);
foreach (is_array($fields) ? $fields : explode(',', $fields) as $field) {
[$dk, $qk] = [$field, $field];
if (stripos($field, $alias) !== false) {
[$dk, $qk] = explode($alias, $field);
}
if (isset($data[$qk]) && $data[$qk] !== '') {
[$begin, $after] = explode($split, strval($data[$qk]));
if (is_callable($callback)) {
$after = call_user_func($callback, $after, 'after');
$begin = call_user_func($callback, $begin, 'begin');
}
$this->query->whereBetween($dk, [$begin, $after]);
}
}
return $this;
}
/**
* 获取输入数据
* @param string|array|null $input
* @return array
*/
private function getInputData($input): array
{
if (is_array($input)) {
return $input;
} else {
$input = $input ?: 'request';
return $this->app->request->$input();
}
}
/**
* 克隆属性复制
* @return void
*/
public function __clone()
{
$this->page = clone $this->page;
$this->query = clone $this->query;
}
/**
* QueryHelper call.
* @param string $name 调用方法名称
* @param array $args 调用参数内容
* @return $this|mixed
*/
public function __call(string $name, array $args)
{
return static::make($this->query, $name, $args, function ($name, $args) {
if (is_callable($callable = [$this->query, $name])) {
$value = call_user_func_array($callable, $args);
if ($name[0] === '_' || $value instanceof $this->query) {
return $this;
} else {
return $value;
}
} else {
return $this;
}
});
}
/**
* 快捷助手调用勾子
* @param string|Model|Query $model
* @param string $method
* @param array $args
* @param callable|null $nohook
* @return mixed|false|integer|QueryHelper
*/
public static function make($model, string $method = 'init', array $args = [], ?callable $nohook = null)
{
$hooks = [
'mForm' => [FormHelper::class, 'init'],
'mSave' => [SaveHelper::class, 'init'],
'mQuery' => [QueryHelper::class, 'init'],
'mDelete' => [DeleteHelper::class, 'init'],
'mUpdate' => [SystemService::class, 'save'],
];
if (isset($hooks[$method])) {
[$class, $method] = $hooks[$method];
return Container::getInstance()->invokeClass($class)->$method($model, ...$args);
} else {
return is_callable($nohook) ? $nohook($method, $args) : false;
}
}
}

View File

@ -0,0 +1,78 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\helper;
use think\admin\Helper;
use think\db\BaseQuery;
use think\Model;
/**
* 数据更新管理器
* @class SaveHelper
* @package think\admin\helper
*/
class SaveHelper extends Helper
{
/**
* 逻辑器初始化
* @param BaseQuery|Model|string $dbQuery
* @param array $edata 表单扩展数据
* @param string $field 数据对象主键
* @param mixed $where 额外更新条件
* @return boolean|void
* @throws \think\db\exception\DbException
*/
public function init($dbQuery, array $edata = [], string $field = '', $where = [])
{
$query = static::buildQuery($dbQuery);
$field = $field ?: ($query->getPk() ?: 'id');
$edata = $edata ?: $this->app->request->post();
$value = $this->app->request->post($field);
// 主键限制处理
if (!isset($where[$field]) && !is_null($value)) {
$query->whereIn($field, str2arr(strval($value)));
if (isset($edata)) unset($edata[$field]);
}
// 前置回调处理
if (false === $this->class->callback('_save_filter', $query, $edata)) {
return false;
}
// 检查原始数据
$query->master()->where($where)->update($edata);
// 模型自定义事件回调
$model = $query->getModel();
if ($model instanceof \think\admin\Model) {
$model->onAdminSave(strval($value));
}
// 结果回调处理
$result = true;
if (false === $this->class->callback('_save_result', $result, $model)) {
return $result;
}
// 回复前端结果
$this->class->success('数据保存成功!', '');
}
}

View File

@ -0,0 +1,71 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\helper;
use think\admin\Helper;
use think\admin\Library;
use think\exception\HttpResponseException;
/**
* 表单令牌验证器
* @class TokenHelper
* @package think\admin\helper
*/
class TokenHelper extends Helper
{
/**
* 初始化验证码器
* @param boolean $return
* @return boolean|void
*/
public function init(bool $return = false)
{
$this->class->csrf_state = true;
if (!$this->app->request->isPost()) return true;
$token = $this->app->request->post('_token_');
$extra = ['_token_' => $token ?: $this->app->request->header('User-Form-Token')];
if ($this->app->request->checkToken('_token_', $extra)) return true; elseif ($return) return false;
$this->class->error($this->class->csrf_message ?: '表单令牌验证失败!');
}
/**
* 返回视图内容
* @param string $tpl 模板名称
* @param array $vars 模板变量
* @param string|null $node 授权节点
*/
public static function fetch(string $tpl = '', array $vars = [], ?string $node = null)
{
throw new HttpResponseException(view($tpl, $vars, 200, static function ($html) use ($node) {
return preg_replace_callback('/<\/form>/i', static function () use ($node) {
return sprintf("<input type='hidden' name='_token_' value='%s'></form>", static::token());
}, $html);
}));
}
/**
* 返回表单令牌数据
* 为了兼容JWT模式使用表单令牌
* @return string
*/
public static function token(): string
{
return Library::$sapp->request->buildToken('_token_');
}
}

View File

@ -0,0 +1,78 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\helper;
use think\admin\Helper;
use think\Validate;
/**
* 快捷输入验证器
* @class ValidateHelper
* @package think\admin\helper
*/
class ValidateHelper extends Helper
{
/**
* 快捷输入并验证( 支持 规则 # 别名
* @param array $rules 验证规则( 验证信息数组
* @param string|array $input 输入内容 ( post. get. )
* @param callable|null $callable 异常处理操作
* @return array|void
*
* age.require => message // 最大值限定
* age.between:1,120 => message // 范围限定
* name.require => message // 必填内容
* name.default => 100 // 获取并设置默认值
* region.value => value // 固定字段数值内容
*
* 更多规则参照 ThinkPHP 官方的验证类
*/
public function init(array $rules, $input = '', ?callable $callable = null)
{
if (is_string($input)) {
$type = trim($input, '.') ?: 'param';
$input = $this->app->request->$type();
}
[$data, $rule, $info] = [[], [], []];
foreach ($rules as $name => $message) if (is_numeric($name)) {
[$name, $alias] = explode('#', $message . '#');
$data[$name] = $input[($alias ?: $name)] ?? null;
} elseif (strpos($name, '.') === false) {
$data[$name] = $message;
} elseif (preg_match('|^(.*?)\.(.*?)#(.*?)#?$|', $name . '#', $matches)) {
[, $_key, $_rule, $alias] = $matches;
if (in_array($_rule, ['value', 'default'])) {
if ($_rule === 'value') $data[$_key] = $message;
elseif ($_rule === 'default') $data[$_key] = $input[($alias ?: $_key)] ?? $message;
} else {
$info[explode(':', $name)[0]] = $message;
$data[$_key] = $data[$_key] ?? ($input[($alias ?: $_key)] ?? null);
$rule[$_key] = isset($rule[$_key]) ? ($rule[$_key] . '|' . $_rule) : $_rule;
}
}
$validate = new Validate();
if ($validate->rule($rule)->message($info)->check($data)) {
return $data;
} elseif (is_callable($callable)) {
return call_user_func($callable, lang($validate->getError()), $data);
} else {
$this->class->error(lang($validate->getError()));
}
}
}

View File

@ -0,0 +1,143 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
use think\admin\Library;
use think\admin\model\SystemBase;
// 动态读取英文数据字典
if (count($langs = Library::$sapp->cache->get('lang-en-us', [])) < 1) {
$langs = array_column(SystemBase::items('英文字典'), 'name', 'code');
$menus = array_column(SystemBase::items('英文菜单'), 'name', 'code');
foreach ($menus as $key => $name) $langs["menus_{$key}"] = $name;
Library::$sapp->cache->set('lang-en-us', $langs, 360);
}
// 定义菜单专用语言包,使用固定前缀 `menus_` 开头
// 数据字典菜单语言包类型为:英文菜单,配置与 英文字典 相同
// PS. 使用前缀是方便后缀追加配置,另外历史版本未开启语言分组
// PS. 该文件仅在英文模式下才会加载,系统默认使用 `中文` 模式
$menus = [
// // 系统管理菜单
// 'menus_系统管理' => 'System',
// 'menus_系统配置' => 'Configuration',
// 'menus_系统参数配置' => 'Parameter',
// 'menus_系统任务管理' => 'Tasks',
// 'menus_系统日志管理' => 'Oplog',
// 'menus_数据字典管理' => 'Dictionary',
// 'menus_系统文件管理' => 'File',
// 'menus_系统菜单管理' => 'Menu',
// 'menus_权限管理' => 'Permission',
// 'menus_访问权限管理' => 'Role',
// 'menus_系统用户管理' => 'User',
// // 微信管理菜单
// 'menus_微信管理' => 'WeChat',
// 'menus_微信接口配置' => 'Configuration',
// 'menus_微信支付配置' => 'Pay parameters',
// 'menus_微信粉丝管理' => 'Fan User',
// 'menus_微信定制' => 'Custom ',
// 'menus_微信图文管理' => 'News',
// 'menus_微信菜单配置' => 'Menu',
// 'menus_回复规则管理' => 'Reply Rule',
// 'menus_关注自动回复' => 'Auto Reply',
// 'menus_微信支付' => 'Payment',
// 'menus_支付行为管理' => 'Action Record',
// 'menus_支付退款管理' => 'Refund Record',
// // 插件中心菜单
// 'menus_插件中心' => 'Plugins'
];
$extra = [];
$extra['Y年m月d日 H:i:s'] = 'Y/m/d H:i:s';
$extra['请重新登录!'] = 'Invalid login authorization, Please login again.';
$extra['共 %s 条记录,每页显示 %s 条,共 %s 页当前显示第 %s 页。'] = 'Total %s records, display %s per page, total %s page current display %s page.';
return array_merge([
// 常规操作翻译
// '全部' => 'All',
// '添 加' => 'Add',
// '编 辑' => 'Edit',
// '删 除' => 'Delete',
// '搜 索' => 'Search',
// '导 出' => 'Export',
// '已禁用' => 'Disabled',
// '已激活' => 'Activated',
// '排序权重' => 'Sort',
// '回 收 站' => 'Recycle',
// '保存数据' => 'Submit',
// '取消编辑' => 'Cancel',
// '操作面板' => 'Panel',
// '使用状态' => 'Status',
// '条件搜索' => 'Search',
// '清空数据' => 'Clears Data',
// '创建时间' => 'Create Time',
// '批量删除' => 'Remove Selected',
// '批量禁用' => 'Forbid Selected',
// '批量恢复' => 'Resume Selected',
// '已禁用记录' => 'Disabled Records',
// '已激活记录' => 'Activated Records',
// 接口提示内容
'数据删除成功!' => 'Successfully deleted.',
'数据删除失败!' => 'Sorry, Delete failed.',
'数据保存成功!' => 'Successfully saved.',
'数据保存失败!' => 'Sorry, Save failed.',
'数据排序成功!' => 'Successfully Sorted.',
'列表排序失败!' => 'Sorry, Sorting failed.',
'请求响应异常!' => 'Sorry, Request response exception.',
'请求响应成功!' => 'Sorry, Request response successful.',
'未授权禁止访问!' => 'Sorry, Unauthorized access prohibited.',
'会话无效或已失效!' => 'The session is invalid or has expired.',
'表单令牌验证失败!' => 'The Form token is validation failed.',
'接口账号验证失败!' => 'Interface account verification failed.',
'接口请求时差过大!' => 'Interface request time difference too large.',
'接口签名验证失败!' => 'Interface signature verification failed.',
'非JWT访问' => 'Please use JWT to access.',
'请求参数 %s 不能为空!' => 'Request parameter %s cannot be empty.',
'接口请求响应格式异常!' => 'Abnormal format of interface request response.',
'耗时 %.4f 秒' => 'Time taken %.4f seconds',
'创建任务失败,%s' => 'Failed to create task, %s',
'已创建请等待处理完成!' => 'Task has been created, please wait for processing to complete.',
'删除%s[%s]及授权配置' => 'Delete %s[%s] and authorization configuration',
'暂无轨迹信息~' => 'No trajectory information currently available',
// 存储引擎翻译
'本地服务器存储' => 'Local server storage',
'自建Alist存储' => 'Self built Alist storage',
'又拍云USS存储' => 'Upyun Cloud USS storage',
'阿里云OSS存储' => 'Aliyun Cloud OSS storage',
'腾讯云COS存储' => 'Tencent Cloud COS Storage',
'七牛云对象存储' => 'Qiniu Cloud Object storage',
'未配置又拍云域名' => 'Unconfigured Upyun Cloud domain',
'未配置阿里云域名' => 'Unconfigured Aliyun Cloud domain',
'未配置七牛云域名' => 'Unconfigured Qiniu Cloud domain',
'未配置腾讯云域名' => 'Unconfigured Tencent Cloud domain',
'未配置Alist域名' => 'Unconfigured Alist Server domain',
// 默认日志翻译
'增加%s[%s]成功' => 'Added: %s [ %s ]',
'修改%s[%s]状态' => 'Modify: %s [ %s ]',
'更新%s[%s]记录' => 'Update: %s [ %s ]',
'删除%s[%s]成功' => 'Remove: %s [ %s ]',
// 模块管理翻译
// '系统任务管理' => 'System Task Management',
// '系统菜单管理' => 'System Menu Management',
// '系统文件管理' => 'System File Management',
// '系统用户管理' => 'System User Management',
// '系统日志管理' => 'System Oplog Management',
// '系统参数配置' => 'System Parameter Management',
// '系统权限管理' => 'System Permission Management',
// '数据字典管理' => 'System Dictionary Management',
// '系统运维管理' => 'System Maintenance Management',
], $extra, $menus, $langs);

View File

@ -0,0 +1,76 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
use think\admin\Library;
use think\admin\model\SystemBase;
// 动态读取繁体数据字典
if (count($langs = Library::$sapp->cache->get('lang-zh-tw', [])) < 1) {
$langs = array_column(SystemBase::items('繁体中文'), 'name', 'code');
$menus = array_column(SystemBase::items('繁体菜单'), 'name', 'code');
foreach ($menus as $key => $name) $langs["menus_{$key}"] = $name;
Library::$sapp->cache->set('lang-zh-tw', $langs, 360);
}
$extra = [];
$extra['Y年m月d日 H:i:s'] = 'Y年m月d日 H:i:s';
$extra['请重新登录!'] = '登錄授權無效,請重新登錄!';
$extra['共 %s 条记录,每页显示 %s 条,共 %s 页当前显示第 %s 页。'] = '共 %s 條記錄,每頁顯示 %s 條,共 %s 頁當前顯示第 %s 頁。';
return array_merge([
// 接口提示内容
'数据删除成功!' => '數據刪除成功!',
'数据删除失败!' => '數據刪除失敗!',
'数据保存成功!' => '數據保存成功!',
'数据保存失败!' => '數據保存失敗!',
'数据排序成功!' => '數據排序成功!',
'列表排序失败!' => '列表排序失敗!',
'请求响应异常!' => '請求響應異常!',
'请求响应成功!' => '請求響應成功!',
'未授权禁止访问!' => '未授權禁止訪問!',
'会话无效或已失效!' => '會話無效或已失效!',
'表单令牌验证失败!' => '表單令牌驗證失敗!',
'接口账号验证失败!' => '接口账号验证失败!',
'接口请求时差过大!' => '接口請求時差過大!',
'接口签名验证失败!' => '接口簽名驗證失敗!',
'非JWT访问' => '請使用 JWT 方式訪問!',
'请求参数 %s 不能为空!' => '請求參數 %s 不能爲空!',
'接口请求响应格式异常!' => '接口請求響應格式異常!',
'耗时 %.4f 秒' => '耗時 %.4f 秒',
'创建任务失败,%s' => '創建任務失敗,%s',
'已创建请等待处理完成!' => '已創建請等待處理完成!',
'删除%s[%s]及授权配置' => '刪除%s[%s]及授權配置',
'暂无轨迹信息~' => '暫無軌迹信息~',
// 存储引擎翻译
'本地服务器存储' => '本地服務器存儲',
'自建Alist存储' => '自建Alist存儲',
'七牛云对象存储' => '七牛雲對象存儲',
'又拍云USS存储' => '又拍雲USS存儲',
'阿里云OSS存储' => '阿裏雲OSS存儲',
'腾讯云COS存储' => '騰訊雲COS存儲',
'未配置又拍云域名' => '未配置又拍雲域名',
'未配置阿里云域名' => '未配置阿裏雲域名',
'未配置七牛云域名' => '未配置七牛雲域名',
'未配置腾讯云域名' => '未配置腾讯云域名',
'未配置Alist域名' => '未配置Alist域名',
// 默认日志翻译
'增加%s[%s]成功' => '增加%s[%s]成功',
'修改%s[%s]状态' => '修改%s[%s]狀態',
'更新%s[%s]记录' => '更新%s[%s]記錄',
'删除%s[%s]成功' => '刪除%s[%s]成功',
], $extra, $langs);

View File

@ -0,0 +1,76 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\model;
use think\admin\Model;
/**
* 用户权限模型
* @class SystemAuth
* @package think\admin\model
*/
class SystemAuth extends Model
{
protected $createTime = 'create_at';
protected $updateTime = false;
/**
* 日志名称
* @var string
*/
protected $oplogName = '系统权限';
/**
* 日志类型
* @var string
*/
protected $oplogType = '系统权限管理';
/**
* 获取权限数据
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public static function items(): array
{
return static::mk()->where(['status' => 1])->order('sort desc,id desc')->select()->toArray();
}
/**
* 删除权限事件
* @param string $ids
*/
public function onAdminDelete(string $ids)
{
if (count($aids = str2arr($ids)) > 0) SystemNode::mk()->whereIn('auth', $aids)->delete();
sysoplog($this->oplogType, lang("删除%s[%s]及授权配置", [lang($this->oplogName), $ids]));
}
/**
* 格式化创建时间
* @param mixed $value
* @return string
*/
public function getCreateAtAttr($value): string
{
return format_datetime($value);
}
}

View File

@ -0,0 +1,82 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\model;
use think\admin\Model;
/**
* 数据字典模型
* @class SystemBase
* @package think\admin\model
*/
class SystemBase extends Model
{
protected $createTime = 'create_at';
protected $updateTime = false;
/**
* 日志名称
* @var string
*/
protected $oplogName = '数据字典';
/**
* 日志类型
* @var string
*/
protected $oplogType = '数据字典管理';
/**
* 获取指定数据列表
* @param string $type 数据类型
* @param array $data 外围数据
* @param string $field 外链字段
* @param string $bind 绑定字段
* @return array
*/
public static function items(string $type, array &$data = [], string $field = 'base_code', string $bind = 'base_info'): array
{
$map = ['type' => $type, 'status' => 1, 'deleted' => 0];
$bases = static::mk()->where($map)->order('sort desc,id asc')->column('code,name,content', 'code');
if (count($data) > 0) foreach ($data as &$vo) $vo[$bind] = $bases[$vo[$field]] ?? [];
return $bases;
}
/**
* 获取所有数据类型
* @param boolean $simple 加载默认值
* @return array
*/
public static function types(bool $simple = false): array
{
$types = static::mk()->where(['deleted' => 0])->distinct()->column('type');
if (empty($types) && empty($simple)) $types = ['身份权限'];
return $types;
}
/**
* 格式化创建时间
* @param mixed $value
* @return string
*/
public function getCreateAtAttr($value): string
{
return format_datetime($value);
}
}

View File

@ -0,0 +1,30 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\model;
use think\admin\Model;
/**
* 系统配置模型
* @class SystemConfig
* @package think\admin\model
*/
class SystemConfig extends Model
{
}

View File

@ -0,0 +1,30 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\model;
use think\admin\Model;
/**
* 系统数据模型
* @class SystemData
* @package think\admin\model
*/
class SystemData extends Model
{
}

View File

@ -0,0 +1,71 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\model;
use think\admin\Model;
use think\model\relation\HasOne;
/**
* 文件管理系统
* @class SystemFile
* @package think\admin\model
*/
class SystemFile extends Model
{
/**
* 创建字段
* @var string
*/
protected $createTime = 'create_at';
/**
* 更新字段
* @var string
*/
protected $updateTime = 'update_at';
/**
* 关联用户数据
* @return \think\model\relation\HasOne
*/
public function user(): HasOne
{
return $this->hasOne(SystemUser::class, 'id', 'uuid')->field('id,username,nickname');
}
/**
* 格式化创建时间
* @param mixed $value
* @return string
*/
public function getCreateAtAttr($value): string
{
return format_datetime($value);
}
/**
* 格式化更新时间
* @param mixed $value
* @return string
*/
public function getUpdateAtAttr($value): string
{
return format_datetime($value);
}
}

View File

@ -0,0 +1,54 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\model;
use think\admin\Model;
/**
* 系统菜单模型
* @class SystemMenu
* @package think\admin\model
*/
class SystemMenu extends Model
{
protected $createTime = 'create_at';
protected $updateTime = false;
/**
* 日志名称
* @var string
*/
protected $oplogName = '系统菜单';
/**
* 日志类型
* @var string
*/
protected $oplogType = '系统菜单管理';
/**
* 格式化创建时间
* @param mixed $value
* @return string
*/
public function getCreateAtAttr($value): string
{
return format_datetime($value);
}
}

View File

@ -0,0 +1,36 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\model;
use think\admin\Model;
/**
* 授权节点模型
* @class SystemNode
* @mixin \think\db\Query
* @package think\admin\model
*/
class SystemNode extends Model
{
/**
* 绑定模型名称
* @var string
*/
protected $name = 'SystemAuthNode';
}

View File

@ -0,0 +1,42 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\model;
use think\admin\Model;
/**
* 系统日志模型
* @class SystemOplog
* @package think\admin\model
*/
class SystemOplog extends Model
{
protected $createTime = 'create_at';
protected $updateTime = false;
/**
* 格式化创建时间
* @param mixed $value
* @return string
*/
public function getCreateAtAttr($value): string
{
return format_datetime($value);
}
}

View File

@ -0,0 +1,77 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\model;
use think\admin\Model;
/**
* 系统任务模型
* @class SystemQueue
* @package think\admin\model
*/
class SystemQueue extends Model
{
protected $createTime = 'create_at';
protected $updateTime = false;
/**
* 格式化计划时间
* @param mixed $value
* @return string
*/
public function getExecTimeAttr($value): string
{
return format_datetime($value);
}
/**
* 执行开始时间处理
* @param mixed $value
* @return string
*/
public function getEnterTimeAttr($value): string
{
return floatval($value) > 0 ? format_datetime(intval($value)) : '';
}
/**
* 执行结束时间处理
* @param mixed $value
* @param array $data
* @return string
*/
public function getOuterTimeAttr($value, array $data): string
{
if ($value > 0 && $value > $data['enter_time']) {
return lang("耗时 %.4f 秒", [$data['outer_time'] - $data['enter_time']]);
} else {
return ' - ';
}
}
/**
* 格式化创建时间
* @param mixed $value
* @return string
*/
public function getCreateAtAttr($value): string
{
return format_datetime($value);
}
}

View File

@ -0,0 +1,114 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\model;
use think\admin\Model;
use think\model\relation\HasOne;
/**
* 系统用户模型
* @class SystemUser
* @package think\admin\model
*/
class SystemUser extends Model
{
protected $createTime = 'create_at';
protected $updateTime = false;
/**
* 日志名称
* @var string
*/
protected $oplogName = '系统用户';
/**
* 日志类型
* @var string
*/
protected $oplogType = '系统用户管理';
/**
* 获取用户数据
* @param mixed $map 数据查询规则
* @param array $data 用户数据集合
* @param string $field 原外连字段
* @param string $target 关联目标字段
* @param string $fields 关联数据字段
* @return array
*/
public static function items($map, array &$data = [], string $field = 'uuid', string $target = 'user_info', string $fields = 'username,nickname,headimg,status,is_deleted'): array
{
$query = static::mk()->where($map)->order('sort desc,id desc');
if (count($data) > 0) {
$users = $query->whereIn('id', array_unique(array_column($data, $field)))->column($fields, 'id');
foreach ($data as &$vo) $vo[$target] = $users[$vo[$field]] ?? [];
return $users;
} else {
return $query->column($fields, 'id');
}
}
/**
* 关联身份权限
* @return HasOne
*/
public function userinfo(): HasOne
{
return $this->hasOne(SystemBase::class, 'code', 'usertype')->where([
'type' => '身份权限', 'status' => 1, 'deleted' => 0,
]);
}
/**
* 默认头像处理
* @param mixed $value
* @return string
*/
public function getHeadimgAttr($value): string
{
if (empty($value)) try {
$host = sysconf('base.site_host|raw') ?: 'https://v6.thinkadmin.top';
return "{$host}/static/theme/img/headimg.png";
} catch (\Exception $exception) {
return "https://v6.thinkadmin.top/static/theme/img/headimg.png";
} else {
return $value;
}
}
/**
* 格式化登录时间
* @param string $value
* @return string
*/
public function getLoginAtAttr(string $value): string
{
return format_datetime($value);
}
/**
* 格式化创建时间
* @param mixed $value
* @return string
*/
public function getCreateAtAttr($value): string
{
return format_datetime($value);
}
}

View File

@ -0,0 +1,334 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\service;
use think\admin\Exception;
use think\admin\extend\CodeExtend;
use think\admin\extend\DataExtend;
use think\admin\Library;
use think\admin\model\SystemAuth;
use think\admin\model\SystemNode;
use think\admin\model\SystemUser;
use think\admin\Service;
use think\helper\Str;
use think\Session;
/**
* 系统权限管理服务
* @class AdminService
* @package think\admin\service
*/
class AdminService extends Service
{
/**
* 自定义回调处理
* @var array
*/
private static $checkCallables = [];
/**
* 是否已经登录
* @return boolean
*/
public static function isLogin(): bool
{
return static::getUserId() > 0;
}
/**
* 是否为超级用户
* @return boolean
*/
public static function isSuper(): bool
{
return static::getUserName() === static::getSuperName();
}
/**
* 获取超级用户账号
* @return string
*/
public static function getSuperName(): string
{
return Library::$sapp->config->get('app.super_user', 'admin');
}
/**
* 获取后台用户ID
* @return integer
*/
public static function getUserId(): int
{
return intval(Library::$sapp->session->get('user.id', 0));
}
/**
* 获取后台用户名称
* @return string
*/
public static function getUserName(): string
{
return Library::$sapp->session->get('user.username', '');
}
/**
* 获取用户扩展数据
* @param null|string $field
* @param null|mixed $default
* @return array|mixed
*/
public static function getUserData(?string $field = null, $default = null)
{
$data = SystemService::getData('UserData_' . static::getUserId());
return is_null($field) ? $data : ($data[$field] ?? $default);
}
/**
* 设置用户扩展数据
* @param array $data
* @param boolean $replace
* @return boolean
* @throws \think\admin\Exception
*/
public static function setUserData(array $data, bool $replace = false): bool
{
$data = $replace ? $data : array_merge(static::getUserData(), $data);
return SystemService::setData('UserData_' . static::getUserId(), $data);
}
/**
* 获取用户主题名称
* @return string
* @throws \think\admin\Exception
*/
public static function getUserTheme(): string
{
$default = sysconf('base.site_theme|raw') ?: 'default';
return static::getUserData('site_theme', $default);
}
/**
* 设置用户主题名称
* @param string $theme 主题名称
* @return boolean
* @throws \think\admin\Exception
*/
public static function setUserTheme(string $theme): bool
{
return static::setUserData(['site_theme' => $theme]);
}
/**
* 注册权限检查函数
* @param callable $callable
* @return integer
*/
public static function registerCheckCallable(callable $callable): int
{
self::$checkCallables[] = $callable;
return count(self::$checkCallables) - 1;
}
/**
* 移除权限检查函数
* @param ?integer $index
* @return boolean
*/
public static function removeCheckCallable(?int $index): bool
{
if (is_null($index)) {
self::$checkCallables = [];
return true;
} elseif (isset(self::$checkCallables[$index])) {
unset(self::$checkCallables[$index]);
return true;
} else {
return false;
}
}
/**
* 检查指定节点授权
* --- 需要读取缓存或扫描所有节点
* @param null|string $node
* @return boolean
*/
public static function check(?string $node = ''): bool
{
$skey1 = 'think.admin.methods';
$current = NodeService::fullNode($node);
$methods = sysvar($skey1) ?: sysvar($skey1, NodeService::getMethods());
$userNodes = Library::$sapp->session->get('user.nodes', []);
// 自定义权限检查回调
if (count(self::$checkCallables) > 0) {
foreach (self::$checkCallables as $callable) {
if ($callable($current, $methods, $userNodes) === false) {
return false;
}
}
return true;
}
// 自定义权限检查方法
if (function_exists('admin_check_filter')) {
return call_user_func('admin_check_filter', $current, $methods, $userNodes);
}
// 超级用户不需要检查权限
if (static::isSuper()) return true;
// 节点权限检查,需要兼容 windows 控制器不区分大小写,统一去除节点下划线再检查权限
if (empty($simples = sysvar($skey2 = 'think.admin.fulls') ?: [])) {
foreach ($methods as $k => $v) $simples[strtr($k, ['_' => ''])] = $v;
sysvar($skey2, $simples);
}
if (empty($simples[$simple = strtr($current, ['_' => ''])]['isauth'])) {
return !(!empty($simples[$simple]['islogin']) && !static::isLogin());
} else {
return in_array($current, $userNodes);
}
}
/**
* 获取授权节点列表
* @param array $checkeds
* @return array
*/
public static function getTree(array $checkeds = []): array
{
[$nodes, $pnodes, $methods] = [[], [], array_reverse(NodeService::getMethods())];
foreach ($methods as $node => $method) {
[$count, $pnode] = [substr_count($node, '/'), substr($node, 0, strripos($node, '/'))];
if ($count === 2 && !empty($method['isauth'])) {
in_array($pnode, $pnodes) or array_push($pnodes, $pnode);
$nodes[$node] = ['node' => $node, 'title' => lang($method['title']), 'pnode' => $pnode, 'checked' => in_array($node, $checkeds)];
} elseif ($count === 1 && in_array($pnode, $pnodes)) {
$nodes[$node] = ['node' => $node, 'title' => lang($method['title']), 'pnode' => $pnode, 'checked' => in_array($node, $checkeds)];
}
}
foreach (array_keys($nodes) as $key) foreach ($methods as $node => $method) if (stripos($key, $node . '/') !== false) {
$pnode = substr($node, 0, strripos($node, '/'));
$nodes[$node] = ['node' => $node, 'title' => lang($method['title']), 'pnode' => $pnode, 'checked' => in_array($node, $checkeds)];
$nodes[$pnode] = ['node' => $pnode, 'title' => Str::studly($pnode), 'pnode' => '', 'checked' => in_array($pnode, $checkeds)];
}
return DataExtend::arr2tree(array_reverse($nodes), 'node', 'pnode', '_sub_');
}
/**
* 初始化用户权限
* @param boolean $force 强刷权限
* @return array
*/
public static function apply(bool $force = false): array
{
if ($force) static::clear();
if (($uuid = static::getUserId()) <= 0) return [];
$user = SystemUser::mk()->where(['id' => $uuid])->findOrEmpty()->toArray();
if (!static::isSuper() && count($aids = str2arr($user['authorize'])) > 0) {
$aids = SystemAuth::mk()->where(['status' => 1])->whereIn('id', $aids)->column('id');
if (!empty($aids)) $nodes = SystemNode::mk()->distinct()->whereIn('auth', $aids)->column('node');
}
$user['nodes'] = $nodes ?? [];
Library::$sapp->session->set('user', $user);
return $user;
}
/**
* 清理节点缓存
* @return bool
*/
public static function clear(): bool
{
Library::$sapp->cache->delete('SystemAuthNode');
return true;
}
/**
* 获取会员上传配置
* @param ?string $uptoken
* @return array [unid,exts]
*/
public static function withUploadUnid(?string $uptoken = null): array
{
try {
if ($uptoken === '') return [0, []];
$session = Library::$sapp->session;
if (is_null($uptoken)) {
$sesskey = $session->get('UploadSessionKey');
if (empty($sesskey)) return [0, []];
if ($session->getId() !== $sesskey) {
$session = Library::$sapp->invokeClass(Session::class);
$session->setId($sesskey);
$session->init();
}
$unid = intval($session->get('AdminUploadUnid', 0));
} else {
$sesskey = CodeExtend::decrypt($uptoken, sysconf('data.jwtkey'));
if (empty($sesskey)) return [0, []];
if ($session->getId() !== $sesskey) {
$session = Library::$sapp->invokeClass(Session::class);
$session->setId($sesskey);
$session->init();
}
if ($unid = intval($session->get('AdminUploadUnid', 0))) {
$session->set('UploadSessionKey', $session->getId());
}
}
return [$unid, $session->get('AdminUploadExts', [])];
} catch (\Error|\Exception $exception) {
return [0, []];
}
}
/**
* 生成上传入口令牌
* @param integer $unid 会员编号
* @param string $exts 允许后缀(多个以英文逗号隔开)
* @return string
* @throws \think\admin\Exception
*/
public static function withUploadToken(int $unid, string $exts = ''): string
{
Library::$sapp->session->set('AdminUploadUnid', $unid);
Library::$sapp->session->set('AdminUploadExts', str2arr(strtolower($exts)));
return CodeExtend::encrypt(Library::$sapp->session->getId(), sysconf('data.jwtkey'));
}
/**
* 静态方法兼容(临时)
* @param string $method
* @param array $arguments
* @return bool
* @throws \think\admin\Exception
*/
public static function __callStatic(string $method, array $arguments)
{
if (strtolower($method) === 'clearcache') return static::clear();
throw new Exception("method not exists: AdminService::{$method}()");
}
/**
* 对象方法兼容(临时)
* @param string $method
* @param array $arguments
* @return bool
* @throws \think\admin\Exception
*/
public function __call(string $method, array $arguments)
{
return static::__callStatic($method, $arguments);
}
}

View File

@ -0,0 +1,185 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\service;
use think\admin\Library;
use think\admin\Service;
/**
* 图形验证码服务
* @class CaptchaService
* @package think\admin\service
*/
class CaptchaService extends Service
{
private $code; // 验证码
private $uniqid; // 唯一序号
private $charset = 'ABCDEFGHKMNPRSTUVWXYZ23456789'; // 随机因子
private $width = 130; // 图片宽度
private $height = 50; // 图片高度
private $length = 4; // 验证码长度
private $fontsize = 20; // 指定字体大小
/**
* 验证码服务初始化
* @param array $config
* @return static
*/
public function initialize(array $config = []): CaptchaService
{
// 动态配置属性
foreach ($config as $k => $v) if (isset($this->$k)) $this->$k = $v;
// 生成验证码序号
$this->uniqid = uniqid('captcha') . mt_rand(1000, 9999);
// 生成验证码字符串
[$this->code, $length] = ['', strlen($this->charset) - 1];
for ($i = 0; $i < $this->length; $i++) {
$this->code .= $this->charset[mt_rand(0, $length)];
}
// 缓存验证码字符串
$this->app->cache->set($this->uniqid, $this->code, 360);
// 返回当前对象
return $this;
}
/**
* 动态切换配置
* @param array $config
* @return $this
*/
public function config(array $config = []): CaptchaService
{
return $this->initialize($config);
}
/**
* 获取验证码值
* @return string
*/
public function getCode(): string
{
return $this->code;
}
/**
* 获取图片的内容
* @return string
*/
public function getData(): string
{
return "data:image/png;base64,{$this->_image()}";
}
/**
* 获取验证码编号
* @return string
*/
public function getUniqid(): string
{
return $this->uniqid;
}
/**
* 获取验证码数据
* @return array
*/
public function getAttrs(): array
{
return [
'code' => $this->getCode(),
'data' => $this->getData(),
'uniqid' => $this->getUniqid(),
];
}
/**
* 输出图形验证码
* @return string
*/
public function __toString()
{
return $this->getData();
}
/**
* 创建验证码图片
* @return string
*/
private function _image(): string
{
// 生成背景
$img = imagecreatetruecolor($this->width, $this->height);
$color = imagecolorallocate($img, mt_rand(220, 255), mt_rand(220, 255), mt_rand(220, 255));
imagefilledrectangle($img, 0, $this->height, $this->width, 0, $color);
// 生成线条
for ($i = 0; $i < 6; $i++) {
$color = imagecolorallocate($img, mt_rand(0, 50), mt_rand(0, 50), mt_rand(0, 50));
imageline($img, mt_rand(0, $this->width), mt_rand(0, $this->height), mt_rand(0, $this->width), mt_rand(0, $this->height), $color);
}
// 生成雪花
for ($i = 0; $i < 100; $i++) {
$color = imagecolorallocate($img, mt_rand(200, 255), mt_rand(200, 255), mt_rand(200, 255));
imagestring($img, mt_rand(1, 5), mt_rand(0, $this->width), mt_rand(0, $this->height), '*', $color);
}
// 生成文字
$_x = $this->width / $this->length;
$fontfile = self::font();
for ($i = 0; $i < $this->length; $i++) {
$fontcolor = imagecolorallocate($img, mt_rand(0, 156), mt_rand(0, 156), mt_rand(0, 156));
if (function_exists('imagettftext')) {
imagettftext($img, $this->fontsize, mt_rand(-30, 30), intval($_x * $i + mt_rand(1, 5)), intval($this->height / 1.4), $fontcolor, $fontfile, $this->code[$i]);
} else {
imagestring($img, 15, intval($_x * $i + mt_rand(10, 15)), mt_rand(10, 30), $this->code[$i], $fontcolor);
}
}
ob_start();
imagepng($img);
$data = ob_get_contents();
ob_end_clean();
imagedestroy($img);
return base64_encode($data);
}
/**
* 获取字体文件
* @return string
*/
public static function font(): string
{
return __DIR__ . '/bin/captcha.ttf';
}
/**
* 检查验证码是否正确
* @param string $code 需要验证的值
* @param null|string $uniqid 验证码编号
* @return boolean
*/
public static function check(string $code, ?string $uniqid = null): bool
{
$_uni = is_string($uniqid) ? $uniqid : input('uniqid', '-');
$_val = Library::$sapp->cache->get($_uni, '');
if (is_string($_val) && strtolower($_val) === strtolower($code)) {
Library::$sapp->cache->delete($_uni);
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,162 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\service;
use think\admin\extend\CodeExtend;
use think\admin\extend\HttpExtend;
use think\admin\Service;
/**
* 百度快递100物流查询
* @class ExpressService
* @deprecated 独立封装为插件
* @package think\admin\service
*/
class ExpressService extends Service
{
/**
* 网络请求参数
* @var array
*/
protected $options = [];
/**
* 公司编码别名
* @var array
*/
protected $codes = [
'YD' => 'yunda',
'SF' => 'shunfeng',
'UC' => 'youshuwuliu',
'YTO' => 'yuantong',
'STO' => 'shentong',
'ZTO' => 'zhongtong',
'ZJS' => 'zhaijisong',
'DBL' => 'debangwuliu',
'HHTT' => 'tiantian',
'HTKY' => 'huitongkuaidi',
'YZPY' => 'youzhengguonei',
];
/**
* 快递服务初始化
* @return $this
*/
protected function initialize(): ExpressService
{
// 获取当前请求 IP 地址
$clentip = $this->app->request->ip();
if (empty($clentip) || $clentip === '0.0.0.0') {
$clentip = join('.', [rand(1, 254), rand(1, 254), rand(1, 254), rand(1, 254)]);
}
// 创建 CURL 请求模拟参数
$this->options['cookie_file'] = syspath('runtime/.cok');
$this->options['headers'] = ['Host:express.baidu.com', "CLIENT-IP:{$clentip}", "X-FORWARDED-FOR:{$clentip}"];
return $this;
}
/**
* 通过百度快递100应用查询物流信息
* @param string $code 快递公司编辑
* @param string $number 快递物流编号
* @param array $list 快递路径列表
* @return array
*/
public function express(string $code, string $number, array $list = []): array
{
// 新状态1-新订单,2-在途中,3-签收,4-问题件
// 原状态0-在途1-揽收2-疑难3-签收4-退签5-派件6-退回7-转投8-清关14-拒签
$ckey = md5("{$code}{$number}");
$cache = $this->app->cache->get($ckey, []);
$message = [1 => '新订单', 2 => '在途中', 3 => '签收', 4 => '问题件'];
if (!empty($cache)) return $cache;
for ($i = 0; $i < 6; $i++) if (is_array($result = $this->doExpress($code, $number))) {
if (isset($result['data']['info']['context']) && isset($result['data']['info']['state'])) {
$state = intval($result['data']['info']['state']);
$status = in_array($state, [0, 1, 5, 7, 8]) ? 2 : ($state === 3 ? 3 : 4);
foreach ($result['data']['info']['context'] as $vo) $list[] = ['time' => date('Y-m-d H:i:s', intval($vo['time'])), 'context' => $vo['desc']];
$result = ['message' => lang($message[$status] ?? $result['msg']), 'status' => $status, 'express' => $code, 'number' => $number, 'data' => $list];
$this->app->cache->set($ckey, $result, 30);
return $result;
}
}
return ['message' => lang('暂无轨迹信息~'), 'status' => 1, 'express' => $code, 'number' => $number, 'data' => $list];
}
/**
* 获取快递公司列表
* @return array
*/
public function getExpressList(): array
{
return $this->getQueryData(2);
}
/**
* 执行百度快递100应用查询请求
* @param string $code 快递公司编号
* @param string $number 快递单单号
* @return mixed
*/
private function doExpress(string $code, string $number)
{
[$code, $qqid] = [$this->codes[$code] ?? $code, CodeExtend::uniqidNumber(19, '7740')];
$url = "{$this->getQueryData(1)}&appid=4001&nu={$number}&com={$code}&qid={$qqid}&new_need_di=1&source_xcx=0&vcode=&token=&sourceId=4155";
$result = json_decode(trim(HttpExtend::get($url, [], $this->options)), true);
if (!empty($result['status']) || !empty($result['error_code'])) {
@unlink($this->options['cookie_file']);
$this->app->cache->delete('express_kuaidi_uri');
$this->app->cache->delete('express_kuaidi_com');
}
return $result;
}
/**
* 获取快递查询接口
* @param integer $type 类型数据
* @return string|array
*/
private function getQueryData(int $type)
{
$times = 0;
$expressUri = $this->app->cache->get('express_kuaidi_uri', '');
if ($type == 1 && !empty($expressUri)) return $expressUri;
$expressCom = $this->app->cache->get('express_kuaidi_com', []);
if ($type === 2 && !empty($expressCom)) return $expressCom;
while (true) {
if ($times++ >= 10) {
$times = 0;
@unlink($this->options['cookie_file']);
}
[$ts, $input] = [mt_rand(2000000, 2900000), CodeExtend::random(5)];
$content = HttpExtend::get("https://m.baidu.com/s?word=快递查询&ts={$ts}&t_kt=0&ie=utf-8&rsv_iqid=&rsv_t=&sa=&rsv_pq=&rsv_sug4=&tj=1&inputT={$input}&sugid=&ss=", [], $this->options);
if (preg_match('#"(expSearchApi|checkExpUrl)":"(.*?)"#i', $content, $matches)) {
$this->app->cache->set('express_kuaidi_uri', $expressUri = $matches[2], 3600);
if (preg_match('#"text":"快递查询","option":.*?(\[.*?]).*?#i', $content, $items)) {
$attr = json_decode($items[1], true);
$expressCom = array_combine(array_column($attr, 'value'), array_column($attr, 'text'));
$this->app->cache->set('express_kuaidi_com', $expressCom, 3600);
if ($type === 2) return $expressCom;
}
if ($type === 1) return $expressUri;
} else usleep(100000);
}
}
}

View File

@ -0,0 +1,275 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\service;
use stdClass;
use think\admin\Exception;
use think\admin\extend\HttpExtend;
use think\admin\helper\ValidateHelper;
use think\admin\Service;
use think\App;
use think\exception\HttpResponseException;
/**
* 通用接口基础服务
* @class InterfaceService
* @package think\admin\service
*/
class InterfaceService extends Service
{
/**
* 输出格式
* @var string
*/
private $type = 'json';
/**
* 接口认证账号
* @var string
*/
private $appid;
/**
* 接口认证密钥
* @var string
*/
private $appkey;
/**
* 接口请求地址
* @var string
*/
private $getway;
/**
* 接口服务初始化
* InterfaceService constructor.
* @param App $app
* @throws \think\admin\Exception
*/
public function __construct(App $app)
{
parent::__construct($app);
$this->appid = sysconf('data.interface_appid|raw') ?: '';
$this->appkey = sysconf('data.interface_appkey|raw') ?: '';
$this->getway = sysconf('data.interface_getway|raw') ?: '';
}
/**
* 设置接口网关
* @param string $getway 接口网关
* @return $this
*/
public function getway(string $getway): InterfaceService
{
$this->getway = $getway;
return $this;
}
/**
* 设置授权账号
* @param string $appid 接口账号
* @param string $appkey 接口密钥
* @return $this
*/
public function setAuth(string $appid, string $appkey): InterfaceService
{
$this->appid = $appid;
$this->appkey = $appkey;
return $this;
}
/**
* 设置输出类型为 JSON
* @return $this
*/
public function setOutTypeJson(): InterfaceService
{
$this->type = 'json';
return $this;
}
/**
* 设置输出类型为 Array
* @return $this
*/
public function setOutTypeArray(): InterfaceService
{
$this->type = 'array';
return $this;
}
/**
* 获取当前APPID
* @return string
*/
public function getAppid(): string
{
return $this->appid ?: '';
}
/**
* 获取请求参数
* @return array
*/
public function getData(): array
{
// 基础参数获取
$input = ValidateHelper::instance()->init([
'time.require' => lang('请求参数 %s 不能为空!', ['time']),
'sign.require' => lang('请求参数 %s 不能为空!', ['sign']),
'data.require' => lang('请求参数 %s 不能为空!', ['data']),
'appid.require' => lang('请求参数 %s 不能为空!', ['appid']),
'nostr.require' => lang('请求参数 %s 不能为空!', ['nostr']),
], 'post', [$this, 'baseError']);
// 检查请求签名,使用通用签名方式
$build = $this->signString($input['data'], $input['time'], $input['nostr']);
if ($build['sign'] !== $input['sign']) {
$this->baseError(lang('接口签名验证失败!'));
}
// 检查请求时间,与服务差不能超过 30 秒
if (abs(intval($input['time']) - time()) > 30) {
$this->baseError(lang('接口请求时差过大!'));
}
// 返回并解析数据内容,如果解析失败返回空数组
return json_decode($input['data'], true) ?: [];
}
/**
* 回复业务处理失败的消息
* @param mixed $info 消息内容
* @param mixed $data 返回数据
* @param mixed $code 返回状态码
*/
public function error($info, $data = '{-null-}', $code = 0): void
{
if ($data === '{-null-}') $data = new stdClass();
$this->baseResponse(lang('请求响应异常!'), [
'code' => $code, 'info' => $info, 'data' => $data,
]);
}
/**
* 回复业务处理成功的消息
* @param mixed $info 消息内容
* @param mixed $data 返回数据
* @param mixed $code 返回状态码
*/
public function success($info, $data = '{-null-}', $code = 1): void
{
if ($data === '{-null-}') $data = new stdClass();
$this->baseResponse(lang('请求响应成功!'), [
'code' => $code, 'info' => is_string($info) ? lang($info) : $info, 'data' => $data,
]);
}
/**
* 回复根失败消息
* @param mixed $info 消息内容
* @param mixed $data 返回数据
* @param mixed $code 根状态码
*/
public function baseError($info, $data = [], $code = 0): void
{
$this->baseResponse($info, $data, $code);
}
/**
* 回复根成功消息
* @param mixed $info 消息内容
* @param mixed $data 返回数据
* @param mixed $code 根状态码
*/
public function baseSuccess($info, $data = [], $code = 1): void
{
$this->baseResponse($info, $data, $code);
}
/**
* 回复根签名消息
* @param mixed $info 消息内容
* @param mixed $data 返回数据
* @param mixed $code 根状态码
*/
public function baseResponse($info, $data = [], $code = 1): void
{
$array = $this->signData($data);
throw new HttpResponseException(json([
'code' => $code, 'info' => $info, 'time' => $array['time'],
'sign' => $array['sign'], 'appid' => $array['appid'], 'nostr' => $array['nostr'],
'data' => $this->type !== 'json' ? json_decode($array['data'], true) : $array['data'],
]));
}
/**
* 接口数据模拟请求
* @param string $uri 接口地址
* @param array $data 请求数据
* @param boolean $check 验证结果
* @return array
* @throws Exception
*/
public function doRequest(string $uri, array $data = [], bool $check = true): array
{
$url = rtrim($this->getway, '/') . '/' . ltrim($uri, '/');
$content = HttpExtend::post($url, $this->signData($data)) ?: '';
// 解析返回的结果
if (!($result = json_decode($content, true)) || empty($result)) {
throw new Exception(lang('接口请求响应格式异常!'));
}
// 返回业务异常结果
if (empty($result['code'])) throw new Exception($result['info']);
$array = is_array($result['data']) ? $result['data'] : json_decode($result['data'], true);
// 无需验证直接返回
if (empty($check)) return $array;
// 返回结果签名验证
$json = is_string($result['data']) ? $result['data'] : json_encode($result['data'], JSON_UNESCAPED_UNICODE);
$build = $this->signString($json, $result['time'], $result['nostr']);
if ($build['sign'] === $result['sign']) return $array ?: [];
throw new Exception(lang('返回结果签名验证失败!'));
}
/**
* 接口响应数据签名
* @param array $data ['appid','nostr','time','sign','data']
* @return array
*/
private function signData(array $data): array
{
return $this->signString(json_encode($data, JSON_UNESCAPED_UNICODE));
}
/**
* 数据字符串数据签名
* @param string $json 待签名的数据
* @param mixed $time 签名的时间戳
* @param mixed $rand 签名随机字符
* @return array
*/
private function signString(string $json, $time = null, $rand = null): array
{
$time = strval($time ?: time());
$rand = strval($rand ?: md5(uniqid('', true)));
$sign = md5("{$this->appid}#{$json}#{$time}#{$this->appkey}#{$rand}");
return ['appid' => $this->appid, 'nostr' => $rand, 'time' => $time, 'sign' => $sign, 'data' => $json];
}
}

View File

@ -0,0 +1,103 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\service;
use think\admin\extend\DataExtend;
use think\admin\model\SystemMenu;
use think\admin\Service;
/**
* 系统菜单管理服务
* @class MenuService
* @package app\admin\service
*/
class MenuService extends Service
{
/**
* 菜单分组语言包
* @param string $name
* @return string
*/
private static function lang(string $name): string
{
$lang = lang("menus_{$name}");
if (stripos($lang, 'menus_') === 0) {
return lang(substr($lang, 6));
} else {
return $lang;
}
}
/**
* 获取可选菜单节点
* @param boolean $force 强制刷新
* @return array
*/
public static function getList(bool $force = false): array
{
$nodes = sysvar($keys = 'think.admin.menus') ?: [];
if (empty($force) && count($nodes) > 0) return $nodes; else $nodes = [];
foreach (NodeService::getMethods($force) as $node => $method) {
if ($method['ismenu']) $nodes[] = ['node' => $node, 'title' => self::lang($method['title'])];
}
return sysvar($keys, $nodes);
}
/**
* 获取系统菜单树数据
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public static function getTree(): array
{
$menus = SystemMenu::mk()->where(['status' => 1])->order('sort desc,id asc')->select()->toArray();
if (function_exists('admin_menu_filter')) $menus = call_user_func('admin_menu_filter', $menus);
foreach ($menus as &$menu) $menu['title'] = self::lang($menu['title']);
return static::filter(DataExtend::arr2tree($menus));
}
/**
* 后台主菜单权限过滤
* @param array $menus 当前菜单列表
* @return array
*/
private static function filter(array $menus): array
{
foreach ($menus as $key => &$menu) {
if (!empty($menu['sub'])) {
$menu['sub'] = static::filter($menu['sub']);
}
if (!empty($menu['sub'])) {
$menu['url'] = '#';
} elseif (empty($menu['url']) || $menu['url'] === '#' || !(empty($menu['node']) || AdminService::check($menu['node']))) {
unset($menus[$key]);
} elseif (preg_match('#^(https?:)?//\w+#i', $menu['url'])) {
if ($menu['params']) $menu['url'] .= (strpos($menu['url'], '?') === false ? '?' : '&') . $menu['params'];
} else {
$node = join('/', array_slice(str2arr($menu['url'], '/'), 0, 3));
$menu['url'] = admuri($menu['url']) . ($menu['params'] ? '?' . $menu['params'] : '');
if (!AdminService::check($node)) unset($menus[$key]);
}
}
return $menus;
}
}

View File

@ -0,0 +1,505 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\service;
use think\admin\extend\HttpExtend;
use think\admin\Service;
/**
* 旧助通短信接口服务
* @class MessageService
* @package app\store\service
* @deprecated 建议使用云平台服务
* =================================
*
* CREATE TABLE `system_message_history` (
* `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
* `phone` varchar(100) DEFAULT '' COMMENT '目标手机',
* `region` varchar(100) DEFAULT '' COMMENT '国家编号',
* `result` varchar(100) DEFAULT '' COMMENT '返回结果',
* `content` varchar(512) DEFAULT '' COMMENT '短信内容',
* `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
* PRIMARY KEY (`id`) USING BTREE,
* KEY `idx_system_message_history_phone` (`phone`) USING BTREE
* ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='系统-短信';
*
* =================================
* 发送国内短信需要给产品码 [productid]
* --- 验证短信的产品码为676767
* --- 营销短信的产品码为333333
* ---------------------------------
* ---------------------------------
* 发送国际短信需要给国家代码 [code]
* --- 国家代码见 getGlobeRegionMap
* ---------------------------------
* ---------------------------------
* 需要开通短信账号请联系客服
* --- 客服电话18122377655
* =================================
*/
class MessageService extends Service
{
private $table;
private $chinaUsername;
private $chinaPassword;
private $globeUsername;
private $globePassword;
/**
* @return $this
* @throws \think\admin\Exception
*/
protected function initialize(): MessageService
{
$this->table = 'SystemMessageHistory';
$this->chinaUsername = sysconf('sms_zt.china_username|raw');
$this->chinaPassword = sysconf('sms_zt.china_password|raw');
$this->globeUsername = sysconf('sms_zt.globe_username|raw');
$this->globePassword = sysconf('sms_zt.globe_password|raw');
return $this;
}
/**
* 配置内陆短信认证
* @param string $username 账号名称
* @param string $password 账号密码
* @return $this
*/
public function configChina(string $username, string $password): MessageService
{
$this->chinaUsername = $username;
$this->chinaPassword = $password;
return $this;
}
/**
* 配置国际短信认证
* @param string $username 账号名称
* @param string $password 账号密码
* @return $this
*/
public function configGlobe(string $username, string $password): MessageService
{
$this->globeUsername = $username;
$this->globePassword = $password;
return $this;
}
/**
* 设置存储数据表
* @param string $table
* @return $this
*/
public function setSaveTable(string $table): MessageService
{
$this->table = $table;
return $this;
}
/**
* 生成短信内容
* @param string $content
* @param array $params
* @return string
*/
public function buildContent(string $content, array $params = []): string
{
foreach ($params as $key => $value) {
$content = str_replace("{{$key}}", $value, $content);
}
return $content;
}
/**
* 发送国内短信验证码
* @param integer|string $phone 手机号
* @param integer|string $content 短信内容
* @param integer|string $productid 短信通道
* @return boolean
*/
public function sendChinaSms($phone, $content, $productid = '676767'): bool
{
$tkey = date("YmdHis");
$result = HttpExtend::get('http' . '://www.ztsms.cn/sendNSms.do', [
'tkey' => $tkey,
'mobile' => $phone,
'content' => $content,
'username' => $this->chinaUsername,
'productid' => $productid,
'password' => md5(md5($this->chinaPassword) . $tkey),
]);
[$code] = explode(',', $result . ',');
$this->app->db->name($this->table)->insert([
'phone' => $phone, 'region' => '860',
'content' => $content, 'result' => $result,
]);
return intval($code) === 1;
}
/**
* 发送国内短信验证码
* @param integer|string $phone 目标手机
* @param integer $wait 等待时间
* @param string $type 短信模板
* @return array
* @throws \think\admin\Exception
*/
public function sendChinaSmsByCode($phone, int $wait = 120, string $type = 'sms_reg_template'): array
{
$cache = $this->app->cache->get($ckey = "{$type}_{$phone}", []);
if (is_array($cache) && isset($cache['time']) && $cache['time'] > time() - $wait) {
$dtime = ($cache['time'] + $wait < time()) ? 0 : ($wait - time() + $cache['time']);
return [1, lang('短信验证码已经发送!'), ['time' => $dtime]];
}
[$code, $content] = [rand(1000, 9999) . '', sysconf("{$type}|raw")];
if (empty($content) || stripos($content, '{code}') === false) {
$content = lang('您的验证码为{code},请在十分钟内完成操作!');
}
$this->app->cache->set($ckey, $cache = ['code' => $code, 'time' => time()], 600);
if ($this->sendChinaSms($phone, str_replace('{code}', $code, $content))) {
$dtime = ($cache['time'] + $wait < time()) ? 0 : ($wait - time() + $cache['time']);
return [1, lang('短信验证码发送成功!'), ['time' => $dtime]];
} else {
return [0, lang('短信发送失败,请稍候再试!'), []];
}
}
/**
* 验证手机短信验证码
* @param integer|string $phone 目标手机
* @param integer|string $code 短信验证码
* @param string $type 短信模板
* @return boolean
*/
public function check($phone, $code, string $type = 'sms_reg_template'): bool
{
$cache = $this->app->cache->get("{$type}_{$phone}", []);
return is_array($cache) && isset($cache['code']) && $cache['code'] == $code;
}
/**
* 查询国内短信余额
* @return array
*/
public function queryChinaSmsBalance(): array
{
$tkey = date("YmdHis");
$result = HttpExtend::get('http' . '://www.ztsms.cn/balanceN.do', [
'username' => $this->chinaUsername, 'tkey' => $tkey,
'password' => md5(md5($this->chinaPassword) . $tkey),
]);
if ($result > -1) {
return ['code' => 1, 'num' => $result, 'msg' => lang('获取短信剩余条数成功!')];
} elseif ($result > -2) {
return ['code' => 0, 'num' => '0', 'msg' => lang('用户名或者密码不正确!')];
} elseif ($result > -3) {
return ['code' => 0, 'num' => '0', 'msg' => lang('tkey不正确')];
} elseif ($result > -4) {
return ['code' => 0, 'num' => '0', 'msg' => lang('用户不存在或用户停用!')];
} else {
return ['code' => 0, 'num' => '0', 'msg' => lang('未知错误原因!')];
}
}
/**
* 错误消息处理
* @var array
*/
private $globeMessageMap = [
2 => '用户账号为空',
3 => '用户账号错误',
4 => '授权密码为空',
5 => '授权密码错误',
6 => '当前时间为空',
7 => '当前时间错误',
8 => '用户类型错误',
9 => '用户鉴权错误',
10 => '请求IP已被列入黑名单',
];
/**
* 发送国际短信内容
* @param integer|string $code 国家代码
* @param integer|string $mobile 手机号码
* @param string $content 发送内容
* @return boolean
* @throws \think\admin\Exception
*/
public function sendGlobeSms($code, $mobile, string $content): bool
{
$tkey = date("YmdHis");
$result = HttpExtend::get('http' . '://intl.zthysms.com/intSendSms.do', [
'tkey' => $tkey, 'code' => $code, 'mobile' => $mobile,
'content' => $content, 'username' => sysconf('sms_zt_username2|raw'),
'password' => md5(md5(sysconf('sms_zt_password2|raw')) . $tkey),
]);
$this->app->db->name($this->table)->insert([
'region' => $code, 'phone' => $mobile, 'content' => $content, 'result' => $result,
]);
return intval($result) === 1;
}
/**
* 查询国际短信余额
* @return array
*/
public function queryGlobeSmsBalance(): array
{
$tkey = date("YmdHis");
$result = HttpExtend::get('http' . '://intl.zthysms.com/intBalance.do', [
'username' => $this->globeUsername, 'tkey' => $tkey, 'password' => md5(md5($this->globePassword) . $tkey),
]);
if (!is_numeric($result) && ($state = intval($result)) && isset($this->globeMessageMap[$state])) {
return ['code' => 0, 'num' => 0, 'msg' => lang($this->globeMessageMap[$state])];
} else {
return ['code' => 1, 'num' => $result, 'msg' => lang('查询成功')];
}
}
/**
* 获取国际地域编号
* @return array
*/
public function getGlobeRegionMap(): array
{
return [
['title' => '中国 台湾', 'english' => 'Taiwan', 'code' => 886],
['title' => '东帝汶民主共和国', 'english' => 'DEMOCRATIC REPUBLIC OF TIMORLESTE', 'code' => 670],
['title' => '中非共和国', 'english' => 'Central African Republic', 'code' => 236],
['title' => '丹麦', 'english' => 'Denmark', 'code' => 45],
['title' => '乌克兰', 'english' => 'Ukraine', 'code' => 380],
['title' => '乌兹别克斯坦', 'english' => 'Uzbekistan', 'code' => 998],
['title' => '乌干达', 'english' => 'Uganda', 'code' => 256],
['title' => '乌拉圭', 'english' => 'Uruguay', 'code' => 598],
['title' => '乍得', 'english' => 'Chad', 'code' => 235],
['title' => '也门', 'english' => 'Yemen', 'code' => 967],
['title' => '亚美尼亚', 'english' => 'Armenia', 'code' => 374],
['title' => '以色列', 'english' => 'Israel', 'code' => 972],
['title' => '伊拉克', 'english' => 'Iraq', 'code' => 964],
['title' => '伊朗', 'english' => 'Iran', 'code' => 98],
['title' => '伯利兹', 'english' => 'Belize', 'code' => 501],
['title' => '佛得角', 'english' => 'Cape Verde', 'code' => 238],
['title' => '俄罗斯', 'english' => 'Russia', 'code' => 7],
['title' => '保加利亚', 'english' => 'Bulgaria', 'code' => 359],
['title' => '克罗地亚', 'english' => 'Croatia', 'code' => 385],
['title' => '关岛', 'english' => 'Guam', 'code' => 1671],
['title' => '冈比亚', 'english' => 'The Gambia', 'code' => 220],
['title' => '冰岛', 'english' => 'Iceland', 'code' => 354],
['title' => '几内亚', 'english' => 'Guinea', 'code' => 224],
['title' => '几内亚比绍', 'english' => 'Guinea - Bissau', 'code' => 245],
['title' => '列支敦士登', 'english' => 'Liechtenstein', 'code' => 423],
['title' => '刚果共和国', 'english' => 'The Republic of Congo', 'code' => 242],
['title' => '刚果民主共和国', 'english' => 'Democratic Republic of the Congo', 'code' => 243],
['title' => '利比亚', 'english' => 'Libya', 'code' => 218],
['title' => '利比里亚', 'english' => 'Liberia', 'code' => 231],
['title' => '加拿大', 'english' => 'Canada', 'code' => 1],
['title' => '加纳', 'english' => 'Ghana', 'code' => 233],
['title' => '加蓬', 'english' => 'Gabon', 'code' => 241],
['title' => '匈牙利', 'english' => 'Hungary', 'code' => 36],
['title' => '南非', 'english' => 'South Africa', 'code' => 27],
['title' => '博茨瓦纳', 'english' => 'Botswana', 'code' => 267],
['title' => '卡塔尔', 'english' => 'Qatar', 'code' => 974],
['title' => '卢旺达', 'english' => 'Rwanda', 'code' => 250],
['title' => '卢森堡', 'english' => 'Luxembourg', 'code' => 352],
['title' => '印尼', 'english' => 'Indonesia', 'code' => 62],
['title' => '印度', 'english' => 'India', 'code' => 91918919],
['title' => '危地马拉', 'english' => 'Guatemala', 'code' => 502],
['title' => '厄瓜多尔', 'english' => 'Ecuador', 'code' => 593],
['title' => '厄立特里亚', 'english' => 'Eritrea', 'code' => 291],
['title' => '叙利亚', 'english' => 'Syria', 'code' => 963],
['title' => '古巴', 'english' => 'Cuba', 'code' => 53],
['title' => '吉尔吉斯斯坦', 'english' => 'Kyrgyzstan', 'code' => 996],
['title' => '吉布提', 'english' => 'Djibouti', 'code' => 253],
['title' => '哥伦比亚', 'english' => 'Colombia', 'code' => 57],
['title' => '哥斯达黎加', 'english' => 'Costa Rica', 'code' => 506],
['title' => '喀麦隆', 'english' => 'Cameroon', 'code' => 237],
['title' => '图瓦卢', 'english' => 'Tuvalu', 'code' => 688],
['title' => '土库曼斯坦', 'english' => 'Turkmenistan', 'code' => 993],
['title' => '土耳其', 'english' => 'Turkey', 'code' => 90],
['title' => '圣卢西亚', 'english' => 'Saint Lucia', 'code' => 1758],
['title' => '圣基茨和尼维斯', 'english' => 'Saint Kitts and Nevis', 'code' => 1869],
['title' => '圣多美和普林西比', 'english' => 'Sao Tome and Principe', 'code' => 239],
['title' => '圣文森特和格林纳丁斯', 'english' => 'Saint Vincent and the Grenadines', 'code' => 1784],
['title' => '圣皮埃尔和密克隆群岛', 'english' => 'Saint Pierre and Miquelon', 'code' => 508],
['title' => '圣赫勒拿岛', 'english' => 'Saint Helena', 'code' => 290],
['title' => '圣马力诺', 'english' => 'San Marino', 'code' => 378],
['title' => '圭亚那', 'english' => 'Guyana', 'code' => 592],
['title' => '坦桑尼亚', 'english' => 'Tanzania', 'code' => 255],
['title' => '埃及', 'english' => 'Egypt', 'code' => 20],
['title' => '埃塞俄比亚', 'english' => 'Ethiopia', 'code' => 251],
['title' => '基里巴斯', 'english' => 'Kiribati', 'code' => 686],
['title' => '塔吉克斯坦', 'english' => 'Tajikistan', 'code' => 992],
['title' => '塞内加尔', 'english' => 'Senegal', 'code' => 221],
['title' => '塞尔维亚', 'english' => 'Serbia and Montenegro', 'code' => 381],
['title' => '塞拉利昂', 'english' => 'Sierra Leone', 'code' => 232],
['title' => '塞浦路斯', 'english' => 'Cyprus', 'code' => 357],
['title' => '塞舌尔', 'english' => 'Seychelles', 'code' => 248],
['title' => '墨西哥', 'english' => 'Mexico', 'code' => 52],
['title' => '多哥', 'english' => 'Togo', 'code' => 228],
['title' => '多米尼克', 'english' => 'Dominica', 'code' => 1767],
['title' => '奥地利', 'english' => 'Austria', 'code' => 43],
['title' => '委内瑞拉', 'english' => 'Venezuela', 'code' => 58],
['title' => '孟加拉', 'english' => 'Bangladesh', 'code' => 880],
['title' => '安哥拉', 'english' => 'Angola', 'code' => 244],
['title' => '安圭拉岛', 'english' => 'Anguilla', 'code' => 1264],
['title' => '安道尔', 'english' => 'Andorra', 'code' => 376],
['title' => '密克罗尼西亚', 'english' => 'Federated States of Micronesia', 'code' => 691],
['title' => '尼加拉瓜', 'english' => 'Nicaragua', 'code' => 505],
['title' => '尼日利亚', 'english' => 'Nigeria', 'code' => 234],
['title' => '尼日尔', 'english' => 'Niger', 'code' => 227],
['title' => '尼泊尔', 'english' => 'Nepal', 'code' => 977],
['title' => '巴勒斯坦', 'english' => 'Palestine', 'code' => 970],
['title' => '巴哈马', 'english' => 'The Bahamas', 'code' => 1242],
['title' => '巴基斯坦', 'english' => 'Pakistan', 'code' => 92],
['title' => '巴巴多斯', 'english' => 'Barbados', 'code' => 1246],
['title' => '巴布亚新几内亚', 'english' => 'Papua New Guinea', 'code' => 675],
['title' => '巴拉圭', 'english' => 'Paraguay', 'code' => 595],
['title' => '巴拿马', 'english' => 'Panama', 'code' => 507],
['title' => '巴林', 'english' => 'Bahrain', 'code' => 973],
['title' => '巴西', 'english' => 'Brazil', 'code' => 55],
['title' => '布基纳法索', 'english' => ' Burkina Faso', 'code' => 226],
['title' => '布隆迪', 'english' => 'Burundi', 'code' => 257],
['title' => '希腊', 'english' => ' Greece', 'code' => 30],
['title' => '帕劳', 'english' => 'Palau', 'code' => 680],
['title' => '库克群岛', 'english' => ' Cook Islands', 'code' => 682],
['title' => '开曼群岛', 'english' => 'Cayman Islands', 'code' => 1345],
['title' => '德国', 'english' => ' Germany', 'code' => 49],
['title' => '意大利', 'english' => 'Italy', 'code' => 39],
['title' => '所罗门群岛', 'english' => ' Solomon Islands', 'code' => 677],
['title' => '托克劳', 'english' => 'Tokelau', 'code' => 690],
['title' => '拉脱维亚', 'english' => 'Latvia', 'code' => 371],
['title' => '挪威', 'english' => 'Norway', 'code' => 47],
['title' => '捷克共和国', 'english' => 'Czech Republic', 'code' => 420],
['title' => '摩尔多瓦', 'english' => 'Moldova', 'code' => 373],
['title' => '摩洛哥', 'english' => 'Morocco', 'code' => 212],
['title' => '摩纳哥', 'english' => 'Monaco', 'code' => 377],
['title' => '文莱', 'english' => 'Brunei Darussalam', 'code' => 673],
['title' => '斐济', 'english' => 'Fiji', 'code' => 679],
['title' => '斯威士兰王国', 'english' => 'The Kingdom of Swaziland', 'code' => 268],
['title' => '斯洛伐克', 'english' => 'Slovakia', 'code' => 421],
['title' => '斯洛文尼亚', 'english' => 'Slovenia', 'code' => 386],
['title' => '斯里兰卡', 'english' => 'Sri Lanka', 'code' => 94],
['title' => '新加坡', 'english' => 'Singapore ', 'code' => 65],
['title' => '新喀里多尼亚', 'english' => 'New Caledonia', 'code' => 687],
['title' => '新西兰', 'english' => 'New Zealand', 'code' => 64],
['title' => '日本', 'english' => 'Japan', 'code' => 81],
['title' => '智利', 'english' => 'Chile', 'code' => 56],
['title' => '朝鲜', 'english' => 'Korea, North', 'code' => 850],
['title' => '柬埔寨 ', 'english' => 'Cambodia', 'code' => 855],
['title' => '格林纳达', 'english' => 'Grenada', 'code' => 1473],
['title' => '格陵兰', 'english' => 'Greenland', 'code' => 299],
['title' => '格鲁吉亚', 'english' => 'Georgia', 'code' => 995],
['title' => '比利时', 'english' => 'Belgium', 'code' => 32],
['title' => '毛里塔尼亚', 'english' => 'Mauritania', 'code' => 222],
['title' => '毛里求斯', 'english' => 'Mauritius', 'code' => 230],
['title' => '汤加', 'english' => 'Tonga', 'code' => 676],
['title' => '沙特阿拉伯', 'english' => 'Saudi Arabia', 'code' => 966],
['title' => '法国', 'english' => 'France', 'code' => 33],
['title' => '法属圭亚那', 'english' => 'French Guiana', 'code' => 594],
['title' => '法属波利尼西亚', 'english' => 'French Polynesia', 'code' => 689],
['title' => '法属西印度群岛', 'english' => 'french west indies', 'code' => 596],
['title' => '法罗群岛', 'english' => 'Faroe Islands', 'code' => 298],
['title' => '波兰', 'english' => 'Poland', 'code' => 48],
['title' => '波多黎各', 'english' => 'The Commonwealth of Puerto Rico', 'code' => 17871939],
['title' => '波黑', 'english' => 'Bosnia and Herzegovina ', 'code' => 387],
['title' => '泰国', 'english' => 'Thailand', 'code' => 66],
['title' => '津巴布韦', 'english' => 'Zimbabwe', 'code' => 263],
['title' => '洪都拉斯', 'english' => 'Honduras', 'code' => 504],
['title' => '海地', 'english' => 'Haiti', 'code' => 509],
['title' => '澳大利亚', 'english' => 'Australia', 'code' => 61],
['title' => '澳门', 'english' => 'Macao', 'code' => 853],
['title' => '爱尔兰', 'english' => 'Ireland', 'code' => 353],
['title' => '爱沙尼亚', 'english' => 'Estonia', 'code' => 372],
['title' => '牙买加 ', 'english' => 'Jamaica', 'code' => 1876],
['title' => '特克斯和凯科斯群岛', 'english' => 'Turks and Caicos Islands', 'code' => 1649],
['title' => '特立尼达和多巴哥', 'english' => 'Trinidad and Tobago', 'code' => 1868],
['title' => '玻利维亚', 'english' => 'Bolivia', 'code' => 591],
['title' => '瑙鲁', 'english' => 'Nauru', 'code' => 674],
['title' => '瑞典', 'english' => 'Sweden', 'code' => 46],
['title' => '瑞士', 'english' => 'Switzerland', 'code' => 41],
['title' => '瓜德罗普', 'english' => 'Guadeloupe', 'code' => 590],
['title' => '瓦利斯和富图纳群岛', 'english' => 'Wallis et Futuna', 'code' => 681],
['title' => '瓦努阿图', 'english' => 'Vanuatu', 'code' => 678],
['title' => '留尼汪 ', 'english' => 'Reunion', 'code' => 262],
['title' => '白俄罗斯', 'english' => 'Belarus', 'code' => 375],
['title' => '百慕大', 'english' => 'Bermuda', 'code' => 1441],
['title' => '直布罗陀', 'english' => 'Gibraltar', 'code' => 350],
['title' => '福克兰群岛', 'english' => 'Falkland', 'code' => 500],
['title' => '科威特', 'english' => 'Kuwait', 'code' => 965],
['title' => '科摩罗和马约特', 'english' => 'Comoros', 'code' => 269],
['title' => '科特迪瓦', 'english' => 'Cote dIvoire', 'code' => 225],
['title' => '秘鲁', 'english' => 'Peru', 'code' => 51],
['title' => '突尼斯', 'english' => 'Tunisia', 'code' => 216],
['title' => '立陶宛', 'english' => 'Lithuania', 'code' => 370],
['title' => '索马里', 'english' => 'Somalia', 'code' => 252],
['title' => '约旦', 'english' => 'Jordan', 'code' => 962],
['title' => '纳米比亚', 'english' => 'Namibia', 'code' => 264],
['title' => '纽埃岛', 'english' => 'Island of Niue', 'code' => 683],
['title' => '缅甸  ', 'english' => 'Burma', 'code' => 95],
['title' => '罗马尼亚', 'english' => 'Romania', 'code' => 40],
['title' => '美国', 'english' => 'United States of America', 'code' => 1],
['title' => '美属维京群岛', 'english' => 'Virgin Islands', 'code' => 1340],
['title' => '美属萨摩亚', 'english' => 'American Samoa', 'code' => 1684],
['title' => '老挝', 'english' => 'Laos', 'code' => 856],
['title' => '肯尼亚', 'english' => 'Kenya', 'code' => 254],
['title' => '芬兰', 'english' => 'Finland', 'code' => 358],
['title' => '苏丹', 'english' => 'Sudan', 'code' => 249],
['title' => '苏里南', 'english' => 'Suriname', 'code' => 597],
['title' => '英国', 'english' => 'United Kingdom', 'code' => 44],
['title' => '英属维京群岛', 'english' => 'British Virgin Islands', 'code' => 1284],
['title' => '荷兰', 'english' => 'Netherlands', 'code' => 31],
['title' => '荷属安的列斯', 'english' => 'Netherlands Antilles', 'code' => 599],
['title' => '莫桑比克', 'english' => 'Mozambique', 'code' => 258],
['title' => '莱索托', 'english' => 'Lesotho', 'code' => 266],
['title' => '菲律宾', 'english' => 'Philippines', 'code' => 63],
['title' => '萨尔瓦多', 'english' => 'El Salvador', 'code' => 503],
['title' => '萨摩亚', 'english' => 'Samoa', 'code' => 685],
['title' => '葡萄牙', 'english' => 'Portugal', 'code' => 351],
['title' => '蒙古', 'english' => 'Mongolia', 'code' => 976],
['title' => '西班牙', 'english' => 'Spain', 'code' => 34],
['title' => '贝宁', 'english' => 'Benin', 'code' => 229],
['title' => '赞比亚', 'english' => 'Zambia', 'code' => 260],
['title' => '赤道几内亚', 'english' => 'Equatorial Guinea', 'code' => 240],
['title' => '越南', 'english' => 'Vietnam', 'code' => 84],
['title' => '阿塞拜疆', 'english' => 'Azerbaijan', 'code' => 994],
['title' => '阿富汗', 'english' => 'Afghanistan', 'code' => 93],
['title' => '阿尔及利亚', 'english' => 'Algeria', 'code' => 213],
['title' => '阿尔巴尼亚', 'english' => 'Albania', 'code' => 355],
['title' => '阿拉伯联合酋长国', 'english' => 'United Arab Emirates', 'code' => 971],
['title' => '阿曼', 'english' => 'Oman', 'code' => 968],
['title' => '阿根廷', 'english' => 'Argentina', 'code' => 54],
['title' => '阿鲁巴', 'english' => 'Aruba', 'code' => 297],
['title' => '韩国', 'english' => 'Korea, South)', 'code' => 82],
['title' => '香港', 'english' => 'Hong Kong(SAR)', 'code' => 852],
['title' => '马其顿', 'english' => 'Macedonia', 'code' => 389],
['title' => '马尔代夫', 'english' => 'Maldives  ', 'code' => 960],
['title' => '马拉维', 'english' => ' Malawi', 'code' => 265],
['title' => '马来西亚', 'english' => 'Malaysia', 'code' => 60],
['title' => '马绍尔群岛', 'english' => 'Marshall Islands', 'code' => 692],
['title' => '马耳他', 'english' => 'Malta', 'code' => 356],
['title' => '马达加斯加', 'english' => 'Madagascar', 'code' => 261],
['title' => '马里', 'english' => 'Mali', 'code' => 223],
['title' => '黎巴嫩', 'english' => 'Lebanon', 'code' => 961],
['title' => '黑山共和国', 'english' => 'The Republic of Montenegro', 'code' => 382],
];
}
}

View File

@ -0,0 +1,100 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\service;
use think\admin\Library;
use think\admin\Service;
/**
* 系统模块管理
* @class ModuleService
* @package think\admin\service
*/
class ModuleService extends Service
{
/**
* 获取版本号信息
* @return string
*/
public static function getVersion(): string
{
$libray = self::getLibrarys('zoujingli/think-library');
return trim($libray['version'] ?? 'v6.0.0', 'v');
}
/**
* 获取运行参数变量
* @param string $field 指定字段
* @return string
*/
public static function getRunVar(string $field): string
{
$file = syspath('vendor/binarys.php');
if (is_file($file) && is_array($binarys = include $file)) {
return $binarys[$field] ?? '';
} else {
return '';
}
}
/**
* 获取 PHP 执行路径
* @return string
*/
public static function getPhpExec(): string
{
if ($phpExec = sysvar($keys = 'phpBinary')) return $phpExec;
if (ProcessService::isFile($phpExec = self::getRunVar('php'))) {
return sysvar($keys, $phpExec);
} else {
$phpExec = str_replace('/sbin/php-fpm', '/bin/php', PHP_BINARY);
$phpExec = preg_replace('#-(cgi|fpm)(\.exe)?$#', '$2', $phpExec);
return sysvar($keys, ProcessService::isFile($phpExec) ? $phpExec : 'php');
}
}
/**
* 获取应用模块
* @param array $data
* @return array
*/
public static function getModules(array $data = []): array
{
$path = Library::$sapp->getBasePath();
foreach (scandir($path) as $item) if ($item[0] !== '.') {
if (is_dir(realpath($path . $item))) $data[] = $item;
}
return $data;
}
/**
* 获取本地组件
* @param ?string $package 指定包名
* @param boolean $force 强制刷新
* @return array|string|null
*/
public static function getLibrarys(?string $package = null, bool $force = false)
{
$plugs = sysvar($keys = 'think.admin.version');
if ((empty($plugs) || $force) && is_file($file = syspath('vendor/versions.php'))) {
$plugs = sysvar($keys, include $file);
}
return empty($package) ? $plugs : ($plugs[$package] ?? null);
}
}

View File

@ -0,0 +1,218 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\service;
use ReflectionClass;
use ReflectionMethod;
use think\admin\Exception;
use think\admin\extend\ToolsExtend;
use think\admin\Library;
use think\admin\Plugin;
use think\admin\Service;
/**
* 应用节点服务管理
* @class NodeService
* @method static array getModules() 获取应用列表
* @method static array scanDirectory() 扫描目录列表
* @package think\admin\service
*/
class NodeService extends Service
{
/**
* 获取默认应用空间名
* @param string $suffix 后缀路径
* @return string
*/
public static function space(string $suffix = ''): string
{
$default = Library::$sapp->config->get('app.app_namespace') ?: 'app';
return empty($suffix) ? $default : trim($default . '\\' . trim($suffix, '\\/'), '\\');
}
/**
* 驼峰转下划线规则
* @param string $name
* @return string
*/
public static function nameTolower(string $name): string
{
$dots = [];
foreach (explode('.', strtr($name, '/', '.')) as $dot) {
$dots[] = trim(preg_replace("/[A-Z]/", "_\\0", $dot), '_');
}
return strtolower(join('.', $dots));
}
/**
* 获取当前节点内容
* @param string $type app|module|controller|action
* @return string
*/
public static function getCurrent(string $type = ''): string
{
// 获取应用节点
$appname = strtolower(Library::$sapp->http->getName());
if (in_array($type, ['app', 'module'])) return $appname;
// 获取控制器节点
$controller = static::nameTolower(Library::$sapp->request->controller());
if ($type === 'controller') return "{$appname}/{$controller}";
// 获取方法权限节点
$method = strtolower(Library::$sapp->request->action());
return "{$appname}/{$controller}/{$method}";
}
/**
* 检查并完整节点内容
* @param ?string $node
* @return string
*/
public static function fullNode(?string $node = ''): string
{
if (empty($node)) return static::getCurrent();
switch (count($attrs = explode('/', $node))) {
case 1: # 方法名
return static::getCurrent('controller') . '/' . strtolower($node);
case 2: # 控制器/方法名
$suffix = static::nameTolower($attrs[0]) . '/' . $attrs[1];
return static::getCurrent('module') . '/' . strtolower($suffix);
default: # 应用名/控制器/方法名?[其他参数]
$attrs[1] = static::nameTolower($attrs[1]);
return strtolower(join('/', $attrs));
}
}
/**
* 获取所有控制器入口
* @param boolean $force 强制更新
* @return array
*/
public static function getMethods(bool $force = false): array
{
$skey = 'think.admin.methods';
if (empty($force)) {
$data = sysvar($skey) ?: Library::$sapp->cache->get('SystemAuthNode', []);
if (count($data) > 0) return sysvar($skey, $data);
} else {
$data = [];
}
// 排除内置方法,禁止访问内置方法及忽略的应用模块配置
$ignoreMethods = get_class_methods('\think\admin\Controller');
$ignoreAppNames = Library::$sapp->config->get('app.rbac_ignore', []);
// 扫描所有代码控制器节点,更新节点缓存
foreach (ToolsExtend::scanDirectory(Library::$sapp->getBasePath(), 'php') as $name) {
if (preg_match("|^(\w+)/controller/(.+)\.php$|i", strtr($name, '\\', '/'), $matches)) {
[, $appName, $className] = $matches;
if (in_array($appName, $ignoreAppNames)) continue;
static::_parseClass($appName, self::space($appName), $className, $ignoreMethods, $data);
}
}
// 扫描所有插件代码
foreach (Plugin::get() as $appName => $plugin) {
if (in_array($appName, $ignoreAppNames)) continue;
[$appPath, $appSpace] = [$plugin['path'], $plugin['space']];
foreach (ToolsExtend::scanDirectory($appPath, 'php') as $name) {
if (preg_match("|^.*?controller/(.+)\.php$|i", strtr($name, '\\', '/'), $matches)) {
static::_parseClass($appName, $appSpace, $matches[1], $ignoreMethods, $data);
}
}
}
// 节点数据回调处理
if (function_exists('admin_node_filter')) {
$data = call_user_func('admin_node_filter', $data);
}
// 缓存系统节点数据
Library::$sapp->cache->set('SystemAuthNode', $data);
return sysvar($skey, $data);
}
/**
* 解析节点数据
* @param string $appName 应用名称
* @param string $appSpace 应用空间
* @param string $className 应用类型
* @param array $ignoreNode 忽略节点
* @param array $data 绑定节点的数据
* @return void
*/
private static function _parseClass(string $appName, string $appSpace, string $className, array $ignoreNode, array &$data)
{
$classfull = strtr("{$appSpace}/controller/{$className}", '/', '\\');
if (class_exists($classfull) && ($class = new ReflectionClass($classfull))) {
$prefix = strtolower(strtr("{$appName}/" . static::nameTolower($className), '\\', '/'));
$data[$prefix] = static::_parseComment($class->getDocComment() ?: '', $className);
foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
if (in_array($metname = $method->getName(), $ignoreNode)) continue;
$data[strtolower("{$prefix}/{$metname}")] = static::_parseComment($method->getDocComment() ?: '', $metname);
}
}
}
/**
* 解析硬节点属性
* @param string $comment 备注内容
* @param string $default 默认标题
* @return array
*/
private static function _parseComment(string $comment, string $default = ''): array
{
$text = strtr($comment, "\n", ' ');
$title = preg_replace('/^\/\*\s*\*\s*\*\s*(.*?)\s*\*.*?$/', '$1', $text);
if (in_array(substr($title, 0, 5), ['@auth', '@menu', '@logi'])) $title = $default;
return [
'title' => $title ?: $default,
'isauth' => intval(preg_match('/@auth\s*true/i', $text)),
'ismenu' => intval(preg_match('/@menu\s*true/i', $text)),
'islogin' => intval(preg_match('/@login\s*true/i', $text)),
];
}
/**
* 重构兼容处理
* @param string $name
* @param array $arguments
* @return array
* @throws \think\admin\Exception
*/
public function __call(string $name, array $arguments)
{
return static::__callStatic($name, $arguments);
}
/**
* 重构兼容处理
* @param string $name
* @param array $arguments
* @return array
* @throws \think\admin\Exception
*/
public static function __callStatic(string $name, array $arguments)
{
if ($name === 'scanDirectory') {
return ToolsExtend::scanDirectory(...$arguments);
} elseif ($name === 'getModules') {
return ModuleService::getModules(...$arguments);
} else {
throw new Exception("method not exists: NodeService::{$name}()");
}
}
}

View File

@ -0,0 +1,236 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\service;
use Symfony\Component\Process\Process;
use think\admin\Exception;
use think\admin\extend\CodeExtend;
use think\admin\Library;
use think\admin\Service;
/**
* 系统进程管理服务
* @class ProcessService
* @package think\admin\service
*/
class ProcessService extends Service
{
/**
* 生成 PHP 指令
* @param string $args
* @return string
*/
public static function php(string $args = ''): string
{
return ModuleService::getPhpExec() . ' ' . $args;
}
/**
* 生成 Think 指令
* @param string $args 指令参数
* @param boolean $simple 仅返回内容
* @return string
*/
public static function think(string $args = '', bool $simple = false): string
{
$command = syspath('think') . ' ' . $args;
return $simple ? $command : self::php($command);
}
/**
* 生成 Composer 指令
* @param string $args 参数
* @return string
*/
public static function composer(string $args = ''): string
{
static $comExec;
if (empty($comExec)) {
$comExec = ModuleService::getRunVar('com');
$comExec = self::isFile($comExec) ? self::php($comExec) : 'composer';
}
$root = Library::$sapp->getRootPath();
return "{$comExec} -d {$root} {$args}";
}
/**
* 创建 Think 进程
* @param string $args 执行参数
* @param integer $usleep 延时等待
* @param boolean $doQuery 查询进程
* @return array
*/
public static function thinkExec(string $args, int $usleep = 0, bool $doQuery = false): array
{
static::create(static::think($args), $usleep);
return $doQuery ? static::query(static::think($args, true)) : [];
}
/**
* 检查 Think 进程
* @param string $args 执行参数
* @return array
*/
public static function thinkQuery(string $args): array
{
return static::query(static::think($args, true));
}
/**
* 创建异步进程
* @param string $command 任务指令
* @param integer $usleep 延时毫米
*/
public static function create(string $command, int $usleep = 0)
{
if (static::isWin()) {
static::exec(__DIR__ . "/bin/console.exe {$command}");
} else {
static::exec("{$command} > /dev/null 2>&1 &");
}
$usleep > 0 && usleep($usleep);
}
/**
* 查询进程列表
* @param string $cmd 任务指令
* @param string $name 进程名称
* @return array
*/
public static function query(string $cmd, string $name = 'php.exe'): array
{
$list = [];
if (static::isWin()) {
$lines = static::exec("wmic process where name=\"{$name}\" get processid,CommandLine", true);
foreach ($lines as $line) if (is_numeric(stripos($line, $cmd))) {
$attr = explode(' ', trim(preg_replace('#\s+#', ' ', $line)));
$list[] = ['pid' => array_pop($attr), 'cmd' => join(' ', $attr)];
}
} else {
$lines = static::exec("ps ax|grep -v grep|grep \"{$cmd}\"", true);
foreach ($lines as $line) if (is_numeric(stripos($line, $cmd))) {
$attr = explode(' ', trim(preg_replace('#\s+#', ' ', $line)));
[$pid] = [array_shift($attr), array_shift($attr), array_shift($attr), array_shift($attr)];
$list[] = ['pid' => $pid, 'cmd' => join(' ', $attr)];
}
}
return $list;
}
/**
* 关闭指定进程
* @param integer $pid 进程号
* @return boolean
*/
public static function close(int $pid): bool
{
if (static::isWin()) {
static::exec("wmic process {$pid} call terminate");
} else {
static::exec("kill -9 {$pid}");
}
return true;
}
/**
* 立即执行指令
* @param string $command 执行指令
* @param boolean $outarr 返回数组
* @param ?callable $callable 逐行处理
* @return string|array
*/
public static function exec(string $command, bool $outarr = false, ?callable $callable = null)
{
$process = Process::fromShellCommandline($command)->setWorkingDirectory(Library::$sapp->getRootPath());
$process->run(is_callable($callable) ? static function ($type, $text) use ($callable, $process) {
call_user_func($callable, $process, $type, trim(CodeExtend::text2utf8($text))) === true && $process->stop();
} : null);
$output = str_replace("\r\n", "\n", CodeExtend::text2utf8($process->getOutput()));
return $outarr ? explode("\n", $output) : trim($output);
}
/**
* 输出命令行消息
* @param string $message 输出内容
* @param integer $backline 回退行数
* @return void
*/
public static function message(string $message, int $backline = 0)
{
while ($backline-- > 0) $message = "\033[1A\r\033[K{$message}";
print_r($message . PHP_EOL);
}
/**
* 判断系统类型 WINDOWS
* @return boolean
*/
public static function isWin(): bool
{
return PATH_SEPARATOR === ';';
}
/**
* 判断系统类型 UNIX
* @return bool
*/
public static function isUnix(): bool
{
return PATH_SEPARATOR !== ';';
}
/**
* 检查文件是否存在
* @param string $file 文件路径
* @return boolean
*/
public static function isFile(string $file): bool
{
try {
return $file !== '' && is_file($file);
} catch (\Error|\Exception $exception) {
try {
if (self::isWin()) {
return self::exec("if exist \"{$file}\" echo 1") === '1';
} else {
return self::exec("if [ -f \"{$file}\" ];then echo 1;fi") === '1';
}
} catch (\Error|\Exception $exception) {
return false;
}
}
}
/**
* 静态兼容处理
* @param string $method
* @param array $arguments
* @return array
* @throws \think\admin\Exception
*/
public static function __callStatic(string $method, array $arguments)
{
if ($method === 'thinkCreate') {
return self::thinkExec(...$arguments);
} else {
throw new Exception("method not exists: ProcessService::{$method}()");
}
}
}

View File

@ -0,0 +1,299 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\service;
use think\admin\Exception;
use think\admin\extend\CodeExtend;
use think\admin\model\SystemQueue;
use think\admin\Service;
/**
* 任务基础服务
* @class QueueService
* @package think\admin\service
*/
class QueueService extends Service
{
/**
* 当前任务编号
* @var string
*/
public $code = '';
/**
* 当前任务标题
* @var string
*/
public $title = '';
/**
* 当前任务参数
* @var array
*/
public $data = [];
/**
* 当前任务数据
* @var SystemQueue
*/
public $record;
/**
* 运行消息记录
* @var array
*/
private $msgs = [];
/**
* 运行消息写库
* @var boolean
*/
private $msgsWriteDb = false;
/**
* 异常尝试次数
* @var integer
*/
private $tryTimes = 0;
/**
* 数据初始化
* @param string $code
* @return static
* @throws \think\admin\Exception
*/
public function initialize(string $code = ''): QueueService
{
// 重置消息内容
if (!empty($this->code) && $this->code !== $code) {
$this->_lazyWrite(true);
$this->msgs = [];
}
// 初始化新任务数据
if (!empty($code)) {
$this->record = SystemQueue::mk()->master()->where(['code' => $code])->findOrEmpty();
if ($this->record->isEmpty()) {
$message = sprintf("Qeueu initialize failed, Queue %s not found.", $code);
$this->app->log->error($message);
throw new Exception($message);
}
$this->code = $code;
$this->data = json_decode($this->record['exec_data'], true) ?: [];
$this->title = $this->record['title'];
}
// 消息写入数据库
$this->msgsWriteDb = in_array('message', SystemQueue::mk()->getTableFields());
return $this;
}
/**
* 重发异步任务
* @param integer $wait 等待时间
* @return $this
* @throws \think\admin\Exception
*/
public function reset(int $wait = 0): QueueService
{
if ($this->record->isEmpty()) {
$message = "Qeueu reset failed, Queue {$this->code} data cannot be empty!";
$this->app->log->error($message);
throw new Exception($message);
}
$this->record->save(['exec_pid' => 0, 'exec_time' => time() + $wait, 'status' => 1]);
return $this;
}
/**
* 添加定时清理任务
* @param integer $loops 循环时间
* @return $this
* @throws \think\admin\Exception
*/
public static function addCleanQueue(int $loops = 3600): QueueService
{
return static::register('定时清理系统任务数据', "xadmin:service clean", 0, [], 0, $loops);
}
/**
* 注册异步处理任务
* @param string $title 任务名称
* @param string $command 执行脚本
* @param integer $later 延时时间
* @param array $data 任务附加数据
* @param integer $rscript 任务类型(0单例,1多例)
* @param integer $loops 循环等待时间
* @return $this
* @throws \think\admin\Exception
*/
public static function register(string $title, string $command, int $later = 0, array $data = [], int $rscript = 0, int $loops = 0): QueueService
{
try {
$map = [['title', '=', $title], ['status', 'in', [1, 2]]];
if (empty($rscript) && ($queue = SystemQueue::mk()->master()->where($map)->findOrEmpty())->isExists()) {
throw new Exception(lang('已创建请等待处理完成!'), 0, $queue['code']);
}
// 生成唯一编号
do $map = ['code' => $code = CodeExtend::uniqidDate(16, 'Q')];
while (($queue = SystemQueue::mk()->master()->where($map)->findOrEmpty())->isExists());
// 写入任务数据
$queue->save([
'code' => $code,
'title' => $title,
'command' => $command,
'attempts' => 0,
'rscript' => intval(boolval($rscript)),
'exec_data' => json_encode($data, JSON_UNESCAPED_UNICODE),
'exec_time' => $later > 0 ? time() + $later : time(),
'enter_time' => 0,
'outer_time' => 0,
'loops_time' => $loops,
'create_at' => date('Y-m-d H:i:s'),
]);
$that = static::instance([], true)->initialize($code);
$that->progress(1, '>>> 任务创建成功 <<<', '0.00');
return $that;
} catch (Exception $exception) {
throw $exception;
} catch (\Exception $exception) {
throw new Exception($exception->getMessage(), $exception->getCode());
}
}
/**
* 设置任务进度信息
* @param ?integer $status 任务状态
* @param ?string $message 进度消息
* @param ?string $progress 进度数值
* @param integer $backline 回退信息行
* @return array
* @throws \think\admin\Exception
*/
public function progress(?int $status = null, ?string $message = null, ?string $progress = null, int $backline = 0): array
{
if (is_numeric($status) && intval($status) === 3) {
if (!is_numeric($progress)) $progress = '100.00';
if (is_null($message)) $message = '>>> 任务已经完成 <<<';
}
if (is_numeric($status) && intval($status) === 4) {
if (!is_numeric($progress)) $progress = '0.00';
if (is_null($message)) $message = '>>> 任务执行失败 <<<';
}
try {
if (empty($this->msgs)) $this->msgs = $this->app->cache->get("queue_{$this->code}_progress", [
'code' => $this->code, 'status' => $status, 'sctime' => 0, 'message' => $message, 'progress' => $progress, 'history' => []
]);
$this->tryTimes = 0;
} catch (\Exception|\Error $exception) {
if ($this->tryTimes++ > 10) throw new Exception('读取进程缓存异常!');
return $this->progress($status, $message, $progress, $backline);
}
while (--$backline > -1 && count($this->msgs['history']) > 0) array_pop($this->msgs['history']);
if (is_numeric($status)) $this->msgs['status'] = intval($status);
if (is_numeric($progress)) $progress = str_pad(sprintf('%.2f', $progress), 6, '0', STR_PAD_LEFT);
if (is_string($message) && is_null($progress)) {
$this->msgs['swrite'] = 0;
$this->msgs['message'] = $message;
$this->msgs['history'][] = ['message' => $message, 'progress' => $this->msgs['progress'], 'datetime' => date('Y-m-d H:i:s')];
} elseif (is_null($message) && is_numeric($progress)) {
$this->msgs['swrite'] = 0;
$this->msgs['progress'] = $progress;
$this->msgs['history'][] = ['message' => $this->msgs['message'], 'progress' => $progress, 'datetime' => date('Y-m-d H:i:s')];
} elseif (is_string($message) && is_numeric($progress)) {
$this->msgs['swrite'] = 0;
$this->msgs['message'] = $message;
$this->msgs['progress'] = $progress;
$this->msgs['history'][] = ['message' => $message, 'progress' => $progress, 'datetime' => date('Y-m-d H:i:s')];
}
if (is_string($message) || is_numeric($progress)) if (count($this->msgs['history']) > 10) {
$this->msgs['history'] = array_slice($this->msgs['history'], -10);
}
// 延时写入并返回内容
return $this->_lazyWrite();
}
/**
* 延时写入记录
* @param boolean $force 强制更新
* @return array
*/
private function _lazyWrite(bool $force = false): array
{
// 无消息状态
if (!isset($this->msgs['status'])) return $this->msgs;
// 消息延时写数据库
if ($force || empty($this->msgs['sctime']) || in_array($this->msgs['status'], [3, 4]) || microtime(true) - $this->msgs['sctime'] > 1) {
if (empty($this->msgs['swrite']) && $this->record->isExists()) {
[$this->msgs['swrite'], $this->msgs['sctime']] = [1, microtime(true)];
$this->app->cache->set("queue_{$this->code}_progress", $this->msgs, 864000);
if ($this->msgsWriteDb) {
$this->record->save(['message' => json_encode($this->msgs, JSON_UNESCAPED_UNICODE)]);
}
}
}
return $this->msgs;
}
/**
* 更新任务进度
* @param integer $total 记录总和
* @param integer $count 当前记录
* @param string $message 文字描述
* @param integer $backline 回退行数
* @throws \think\admin\Exception
*/
public function message(int $total, int $count, string $message = '', int $backline = 0): void
{
$prefix = str_pad("{$count}", strlen(strval($total)), '0', STR_PAD_LEFT);
if (defined('WorkQueueCode')) {
$this->progress(2, "[{$prefix}/{$total}] {$message}", sprintf("%.2f", $count / max($total, 1) * 100), $backline);
} else {
ProcessService::message("[{$prefix}/{$total}] {$message}", $backline);
}
}
/**
* 任务执行成功
* @param string $message 消息内容
* @throws Exception
*/
public function success(string $message): void
{
throw new Exception($message, 3, $this->code);
}
/**
* 任务执行失败
* @param string $message 消息内容
* @throws Exception
*/
public function error(string $message): void
{
throw new Exception($message, 4, $this->code);
}
/**
* 执行任务处理
* @param array $data 任务参数
* @return void
*/
public function execute(array $data = [])
{
}
}

View File

@ -0,0 +1,263 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\service;
use think\admin\Library;
use think\admin\support\Route;
use think\admin\support\Url;
use think\App;
use think\Container;
use think\Request;
use think\Response;
/**
* 系统运行服务
* @class RuntimeService
* @package think\admin\service
*/
class RuntimeService
{
/**
* 开发运行模式
* @var string
*/
public const MODE_DEVE = 'dev';
/**
* 演示运行模式
* @var string
*/
public const MODE_DEMO = 'demo';
/**
* 本地运行模式
* @var string
*/
public const MODE_LOCAL = 'local';
/**
* 环境配置文件位置
* @var string
*/
private static $envFile = './runtime/.env';
/**
* 初始化文件哈希值
* @var string
*/
private static $evnHash = '';
/**
* 系统服务初始化
* @param ?\think\App $app
* @return App
*/
public static function init(?App $app = null): App
{
// 初始化运行环境
Library::$sapp = $app ?: Container::getInstance()->make(App::class);
Library::$sapp->bind('think\Route', Route::class);
Library::$sapp->bind('think\route\Url', Url::class);
// 初始化运行配置位置
self::$envFile = syspath('runtime/.env');
return Library::$sapp->debug(static::isDebug());
}
/**
* 获取动态配置
* @param null|string $name 配置名称
* @param array $default 配置内容
* @return array|string
*/
public static function get(?string $name = null, array $default = [])
{
$keys = 'think.admin.runtime';
if (empty($envs = sysvar($keys) ?: [])) {
// 读取默认配置
clearstatcache(true, self::$envFile);
is_file(self::$envFile) && Library::$sapp->env->load(self::$envFile);
// 动态判断赋值
$envs['mode'] = Library::$sapp->env->get('RUNTIME_MODE') ?: 'debug';
$envs['appmap'] = Library::$sapp->env->get('RUNTIME_APPMAP') ?: [];
$envs['domain'] = Library::$sapp->env->get('RUNTIME_DOMAIN') ?: [];
sysvar($keys, $envs);
}
return is_null($name) ? $envs : ($envs[$name] ?? $default);
}
/**
* 设置动态配置
* @param null|mixed $mode 支持模式
* @param null|array $appmap 应用映射
* @param null|array $domain 域名映射
* @return boolean 是否调试模式
*/
public static function set(?string $mode = null, ?array $appmap = [], ?array $domain = []): bool
{
$envs = self::get();
$envs['mode'] = is_null($mode) ? $envs['mode'] : $mode;
$envs['appmap'] = static::uniqueMergeArray($envs['appmap'], $appmap);
$envs['domain'] = static::uniqueMergeArray($envs['domain'], $domain);
// 组装配置文件格式
$rows[] = "mode = {$envs['mode']}";
foreach ($envs['appmap'] as $key => $item) $rows[] = "appmap[{$key}] = {$item}";
foreach ($envs['domain'] as $key => $item) $rows[] = "domain[{$key}] = {$item}";
// 写入并刷新文件希值
@file_put_contents(self::$envFile, "[RUNTIME]\n" . join("\n", $rows));
// 同步更新当前环境
sysvar('think.admin.runtime', $envs);
// 应用当前的配置文件
return static::apply($envs);
}
/**
* 同步运行配置
* @return void
*/
public static function sync()
{
clearstatcache(true, self::$envFile);
is_file(self::$envFile) && md5_file(self::$envFile) !== self::$evnHash && self::apply();
}
/**
* 绑定动态配置
* @param array $data 配置数据
* @return boolean 是否调试模式
*/
public static function apply(array $data = []): bool
{
// 设置模块绑定
$data = array_merge(static::get(), $data);
$appmap = static::uniqueMergeArray(Library::$sapp->config->get('app.app_map', []), $data['appmap']);
$domain = static::uniqueMergeArray(Library::$sapp->config->get('app.domain_bind', []), $data['domain']);
Library::$sapp->config->set(['app_map' => $appmap, 'domain_bind' => $domain], 'app');
// 记录配置文件
is_file(self::$envFile) && self::$evnHash = md5_file(self::$envFile);
// 初始化调试配置
return Library::$sapp->debug($data['mode'] !== 'product')->isDebug();
}
/**
* 压缩发布项目
* @return string
*/
public static function push(): string
{
self::set('product');
$connection = Library::$sapp->db->getConfig('default');
Library::$sapp->console->call('optimize:schema', ["--connection={$connection}"]);
return $connection;
}
/**
* 判断运行环境
* @param string $type 运行模式dev|demo|local
* @return boolean
*/
public static function check(string $type = 'dev'): bool
{
$domain = Library::$sapp->request->host(true);
$isDemo = boolval(preg_match('|v\d+\.thinkadmin\.top|', $domain));
$isLocal = $domain === '127.0.0.1' || is_numeric(stripos($domain, 'local'));
if ($type === static::MODE_DEVE) return $isLocal || $isDemo;
if ($type === static::MODE_DEMO) return $isDemo;
if ($type === static::MODE_LOCAL) return $isLocal;
return true;
}
/**
* 清理运行缓存
* @param boolean $force 清理目录
* @return boolean
*/
public static function clear(bool $force = true): bool
{
$data = static::get();
AdminService::clear() && Library::$sapp->cache->clear();
$force && Library::$sapp->console->call('clear', ['--dir']);
return static::set($data['mode'], $data['appmap'], $data['domain']);
}
/**
* 开发模式运行
* @return boolean
*/
public static function isDebug(): bool
{
return static::get('mode') !== 'product';
}
/**
* 生产模式运行
* @return boolean
*/
public static function isOnline(): bool
{
return static::get('mode') === 'product';
}
/**
* 初始化主程序
* @param ?\think\App $app
* @param ?\think\Request $request
* @return \think\Response
*/
public static function doWebsiteInit(?App $app = null, ?Request $request = null): Response
{
$http = static::init($app)->http;
$request = $request ?: Library::$sapp->make(Request::class);
Library::$sapp->instance('request', $request);
($response = $http->run($request))->send();
$http->end($response);
return $response;
}
/**
* 初始化命令行
* @param ?\think\App $app
* @return integer
*/
public static function doConsoleInit(?App $app = null): int
{
try {
return static::init($app)->console->run();
} catch (\Exception $exception) {
ProcessService::message($exception->getMessage());
return 0;
}
}
/**
* 生成唯一数组
* @param array ...$args
* @return array
*/
private static function uniqueMergeArray(...$args): array
{
return array_unique(array_reverse(array_merge(...$args)));
}
}

View File

@ -0,0 +1,411 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\service;
use think\admin\Exception;
use think\admin\extend\FaviconExtend;
use think\admin\Helper;
use think\admin\Library;
use think\admin\model\SystemConfig;
use think\admin\model\SystemData;
use think\admin\model\SystemOplog;
use think\admin\Service;
use think\admin\Storage;
use think\admin\storage\LocalStorage;
use think\App;
use think\db\Query;
use think\Model;
/**
* 系统参数管理服务
* @class SystemService
* @package think\admin\service
*
* @method static bool isDebug() 调式模式运行
* @method static bool isOnline() 产品模式运行
*
* 运行环境配置
* @method static array getRuntime(?string $name = null, array $default = []) 获取动态配置
* @method static bool setRuntime(?string $mode = null, ?array $appmap = [], ?array $domain = []) 设置动态配置
* @method static bool bindRuntime(array $data = []) 绑定动态配置
*
* 运行缓存管理
* @method static bool pushRuntime() 压缩发布项目
* @method static bool clearRuntime() 清理运行缓存
* @method static bool checkRunMode(string $type = 'dev') 判断运行环境
*
* 初始化启动系统
* @method static mixed doInit(?App $app = null) 初始化主程序
* @method static mixed doConsoleInit(?App $app = null) 初始化命令行
*/
class SystemService extends Service
{
/**
* 生成静态路径链接
* @param string $path 后缀路径
* @param ?string $type 路径类型
* @param mixed $default 默认数据
* @return string|array
*/
public static function uri(string $path = '', ?string $type = '__ROOT__', $default = '')
{
$plugin = Library::$sapp->http->getName();
if (strlen($path)) $path = '/' . ltrim($path, '/');
$prefix = rtrim(dirname(Library::$sapp->request->basefile()), '\\/');
$data = [
'__APP__' => rtrim(url('@')->build(), '\\/') . $path,
'__ROOT__' => $prefix . $path,
'__PLUG__' => "{$prefix}/static/extra/{$plugin}{$path}",
'__FULL__' => Library::$sapp->request->domain() . $prefix . $path
];
return is_null($type) ? $data : ($data[$type] ?? $default);
}
/**
* 生成全部静态路径
* @param string $path
* @return string[]
*/
public static function uris(string $path = ''): array
{
return static::uri($path, null);
}
/**
* 设置配置数据
* @param string $name 配置名称
* @param mixed $value 配置内容
* @return integer|string
* @throws \think\admin\Exception
*/
public static function set(string $name, $value = '')
{
[$type, $field] = static::_parse($name);
if (is_array($value)) {
$count = 0;
foreach ($value as $kk => $vv) {
$count += static::set("{$field}.{$kk}", $vv);
}
return $count;
} else try {
$map = ['type' => $type, 'name' => $field];
SystemConfig::mk()->master()->where($map)->findOrEmpty()->save(array_merge($map, ['value' => $value]));
sysvar('think.admin.config', []);
Library::$sapp->cache->delete('SystemConfig');
return 1;
} catch (\Exception $exception) {
throw new Exception($exception->getMessage(), $exception->getCode());
}
}
/**
* 读取配置数据
* @param string $name
* @param string $default
* @return array|mixed|string
* @throws \think\admin\Exception
*/
public static function get(string $name = '', string $default = '')
{
try {
if (empty($config = sysvar($keys = 'think.admin.config') ?: [])) {
SystemConfig::mk()->cache('SystemConfig')->select()->map(function ($item) use (&$config) {
$config[$item['type']][$item['name']] = $item['value'];
});
sysvar($keys, $config);
}
[$type, $field, $outer] = static::_parse($name);
if (empty($name)) {
return $config;
} elseif (isset($config[$type])) {
$group = $config[$type];
if ($outer !== 'raw') foreach ($group as $kk => $vo) {
$group[$kk] = htmlspecialchars(strval($vo));
}
return $field ? ($group[$field] ?? $default) : $group;
} else {
return $default;
}
} catch (\Exception $exception) {
throw new Exception($exception->getMessage(), $exception->getCode());
}
}
/**
* 数据增量保存
* @param Model|Query|string $query 数据查询对象
* @param array $data 需要保存的数据,成功返回对应模型
* @param string $key 更新条件查询主键
* @param mixed $map 额外更新查询条件
* @return boolean|integer 失败返回 false, 成功返回主键值或 true
* @throws \think\admin\Exception
*/
public static function save($query, array &$data, string $key = 'id', $map = [])
{
try {
$query = Helper::buildQuery($query)->master()->strict(false);
if (empty($map[$key])) $query->where([$key => $data[$key] ?? null]);
$model = $query->where($map)->findOrEmpty();
// 当前操作方法描述
$action = $model->isExists() ? 'onAdminUpdate' : 'onAdminInsert';
// 写入或更新模型数据
if ($model->save($data) === false) return false;
// 模型自定义事件回调
if ($model instanceof \think\admin\Model) {
$model->$action(strval($model->getAttr($key)));
}
$data = $model->toArray();
return $model[$key] ?? true;
} catch (\Exception $exception) {
throw new Exception($exception->getMessage(), $exception->getCode());
}
}
/**
* 解析缓存名称
* @param string $rule 配置名称
* @return array
*/
private static function _parse(string $rule): array
{
$type = 'base';
if (stripos($rule, '.') !== false) {
[$type, $rule] = explode('.', $rule, 2);
}
[$field, $outer] = explode('|', "{$rule}|");
return [$type, $field, strtolower($outer)];
}
/**
* 获取数据库所有数据表
* @return array [table, total, count]
*/
public static function getTables(): array
{
$tables = Library::$sapp->db->getTables();
return [$tables, count($tables), 0];
}
/**
* 复制并创建表结构
* @param string $from 来源表名
* @param string $create 创建表名
* @param array $tables 现有表集合
* @param boolean $copy 是否复制
* @param mixed $where 复制条件
* @throws \think\admin\Exception
*/
public static function copyTableStruct(string $from, string $create, array $tables = [], bool $copy = false, $where = [])
{
try {
if (empty($tables)) [$tables] = static::getTables();
if (!in_array($from, $tables)) {
throw new Exception("待复制的数据表 {$from} 不存在!");
}
if (!in_array($create, $tables)) {
Library::$sapp->db->connect()->query("CREATE TABLE IF NOT EXISTS {$create} (LIKE {$from})");
if ($copy) {
$sql1 = Library::$sapp->db->name($from)->where($where)->buildSql(false);
Library::$sapp->db->connect()->query("INSERT INTO {$create} {$sql1}");
}
}
} catch (\Exception $exception) {
throw new Exception($exception->getMessage(), $exception->getCode());
}
}
/**
* 保存数据内容
* @param string $name 数据名称
* @param mixed $value 数据内容
* @return boolean
* @throws \think\admin\Exception
*/
public static function setData(string $name, $value): bool
{
try {
$data = ['name' => $name, 'value' => json_encode([$value], 64 | 256)];
return SystemData::mk()->where(['name' => $name])->findOrEmpty()->save($data);
} catch (\Exception $exception) {
throw new Exception($exception->getMessage(), $exception->getCode());
}
}
/**
* 读取数据内容
* @param string $name 数据名称
* @param mixed $default 默认内容
* @return mixed
*/
public static function getData(string $name, $default = [])
{
try {
// 读取原始序列化或JSON数据
$value = SystemData::mk()->where(['name' => $name])->value('value');
if (is_null($value)) return $default;
if (is_string($value) && strpos($value, '[') === 0) {
return json_decode($value, true)[0];
}
} catch (\Exception $exception) {
trace_file($exception);
return $default;
}
try {
// 尝试正常反序列解析
return unserialize($value);
} catch (\Exception $exception) {
trace_file($exception);
}
try {
// 尝试修复反序列解析
$unit = 'i:\d+;|b:[01];|s:\d+:".*?";|O:\d+:".*?":\d+:\{';
$preg = '/(?=^|' . $unit . ')s:(\d+):"(.*?)";(?=' . $unit . '|}+$)/';
return unserialize(preg_replace_callback($preg, static function ($attr) {
return sprintf('s:%d:"%s";', strlen($attr[2]), $attr[2]);
}, $value));
} catch (\Exception $exception) {
trace_file($exception);
return $default;
}
}
/**
* 写入系统日志内容
* @param string $action
* @param string $content
* @return boolean
*/
public static function setOplog(string $action, string $content): bool
{
return SystemOplog::mk()->save(static::getOplog($action, $content)) !== false;
}
/**
* 获取系统日志内容
* @param string $action
* @param string $content
* @return array
*/
public static function getOplog(string $action, string $content): array
{
return [
'node' => NodeService::getCurrent(),
'action' => lang($action), 'content' => lang($content),
'geoip' => Library::$sapp->request->ip() ?: '127.0.0.1',
'username' => AdminService::getUserName() ?: '-',
'create_at' => date('Y-m-d H:i:s'),
];
}
/**
* 打印输出数据到文件
* @param mixed $data 输出的数据
* @param boolean $new 强制替换文件
* @param string|null $file 文件名称
* @return false|int
*/
public static function putDebug($data, bool $new = false, ?string $file = null)
{
ob_start();
var_dump($data);
$output = preg_replace('/]=>\n(\s+)/m', '] => ', ob_get_clean());
if (is_null($file)) $file = syspath('runtime/' . date('Ymd') . '.log');
else if (!preg_match('#[/\\\\]+#', $file)) $file = syspath("runtime/{$file}.log");
is_dir($dir = dirname($file)) or mkdir($dir, 0777, true);
return $new ? file_put_contents($file, $output) : file_put_contents($file, $output, FILE_APPEND);
}
/**
* 设置网页标签图标
* @param ?string $icon 网页标签图标
* @return boolean
* @throws \think\admin\Exception
*/
public static function setFavicon(?string $icon = null): bool
{
try {
$icon = $icon ?: sysconf('base.site_icon|raw');
if (!preg_match('#^https?://#i', $icon)) {
throw new Exception(lang('无效的原文件地址!'));
}
if (preg_match('#/upload/(\w{2}/\w{30}.\w+)$#i', $icon, $vars)) {
$info = LocalStorage::instance()->info($vars[1]);
}
if (empty($info) || empty($info['file'])) {
$name = Storage::name($icon, 'tmp', 'icon');
$info = LocalStorage::instance()->set($name, Storage::curlGet($icon), true);
}
if (empty($info) || empty($info['file'])) return false;
$favicon = new FaviconExtend($info['file'], [48, 48]);
return $favicon->saveIco(syspath('public/favicon.ico'));
} catch (Exception $exception) {
throw $exception;
} catch (\Exception $exception) {
trace_file($exception);
throw new Exception($exception->getMessage(), $exception->getCode());
}
}
/**
* 魔术方法调用(临时)
* @param string $method 方法名称
* @param array $arguments 调用参数
* @return mixed
* @throws \think\admin\Exception
*/
public function __call(string $method, array $arguments)
{
return static::__callStatic($method, $arguments);
}
/**
* 静态方法兼容(临时)
* @param string $method 方法名称
* @param array $arguments 调用参数
* @return mixed
* @throws \think\admin\Exception
*/
public static function __callStatic(string $method, array $arguments)
{
$map = [
'setRuntime' => 'set',
'getRuntime' => 'get',
'bindRuntime' => 'apply',
'isDebug' => 'isDebug',
'isOnline' => 'isOnline',
'doInit' => 'doWebsiteInit',
'doConsoleInit' => 'doConsoleInit',
'pushRuntime' => 'push',
'clearRuntime' => 'clear',
'checkRunMode' => 'check',
];
switch (strtolower($method)) {
case 'setconfig':
return self::setData(...$arguments);
case 'getconfig':
return self::getData(...$arguments);
}
if (isset($map[$method])) {
return RuntimeService::{$map[$method]}(...$arguments);
} else {
throw new Exception("method not exists: RuntimeService::{$method}()");
}
}
}

View File

@ -0,0 +1,296 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\service;
use think\admin\extend\HttpExtend;
use think\admin\Service;
/**
* 新助通短信接口服务
* @class ZtSmsService
* @deprecated 独立封装为插件
* @package think\admin\service
*/
class ZtSmsService extends Service
{
/**
* 接口地址
* @var string
*/
private $api = 'https://api.mix2.zthysms.com';
/**
* 子账号名称
* @var string
*/
protected $username;
/**
* 子账号密码
* @var string
*/
protected $password;
/**
* 短信服务初始化
* @throws \think\admin\Exception
*/
protected function initialize()
{
$this->username = sysconf('ztsms.username|raw') ?: '';
$this->password = sysconf('ztsms.password|raw') ?: '';
}
/**
* 短信服务初始化
* @param string $username 账号名称
* @param string $password 账号密码
* @return static
*/
public function setAuth(string $username, string $password): ZtSmsService
{
$this->username = $username;
$this->password = $password;
return $this;
}
/**
* 验证手机短信验证码
* @param string $code 验证码
* @param string $phone 手机号验证
* @param string $template 模板编码
* @return boolean
*/
public function checkVerifyCode(string $code, string $phone, string $template = 'ztsms.register_verify'): bool
{
$cache = $this->app->cache->get($ckey = md5("code-{$template}-{$phone}"), []);
if (is_array($cache) && isset($cache['code']) && $cache['code'] == $code) {
$this->app->cache->delete($ckey);
return true;
} else {
return false;
}
}
/**
* 验证手机短信验证码
* @param string $phone 手机号码
* @param integer $wait 等待时间
* @param string $template 模板编码
* @return array
* @throws \think\admin\Exception
*/
public function sendVerifyCode(string $phone, int $wait = 120, string $template = 'ztsms.register_verify'): array
{
$time = time();
// 检查是否已经发送
$cache = $this->app->cache->get($ckey = md5("code-{$template}-{$phone}"), []);
if (is_array($cache) && isset($cache['time']) && $cache['time'] + $wait > $time) {
$dtime = $cache['time'] + $wait < $time ? 0 : $cache['time'] + $wait - $time;
return [1, lang('短信验证码已经发送!'), ['time' => $dtime]];
}
// 生成新的验证码
$code = (string)rand(100000, 999999);
$this->app->cache->set($ckey, ['code' => $code, 'time' => $time], 600);
// 尝试发送短信内容
$content = sysconf("{$template}|raw") ?: lang('您的验证码为{code},请在十分钟内完成操作!');
[$state] = $this->timeSend($phone, str_replace('{code}', $code, $content));
if ($state) {
return [1, lang('短信验证码发送成功!'), ['time' => $wait]];
} else {
$this->app->cache->delete($ckey);
return [0, lang('短信发送失败,请稍候再试!'), ['time' => 0]];
}
}
/**
* 创建短信签名
* @param array $signs 签名列表
* @param string $remark 签名备注
* @return array
*/
public function signAdd(array $signs = [], string $remark = ''): array
{
foreach ($signs as $key => $name) $signs[$key] = $this->_singName($name);
return $this->doRequest('/sms/v1/sign', ['sign' => $signs, 'remark' => $remark]);
}
/**
* 查询短信签名
* @param string $name 短信签名
* @return array
*/
public function signGet(string $name): array
{
return $this->doRequest('/sms/v1/sign/query', [
'sign' => $this->_singName($name),
]);
}
/**
* 报备短信模板
* @param string $name 模板名称
* @param integer $type 模板类型1验证码, 2行业通知, 3营销推广)
* @param string $content 模板内容
* @param array $params 模板变量
* @param string $remark 模板备注
* @return array
*/
public function tplAdd(string $name, int $type, string $content, array $params = [], string $remark = ''): array
{
return $this->doRequest('/sms/v2/template', [
'temName' => $name, 'temType' => $type, 'temContent' => $content, 'paramJson' => $params, 'remark' => $remark,
]);
}
/**
* 查询模板状态
* @param string $temId 短信模板
* @return array
*/
public function tplGet(string $temId): array
{
return $this->doRequest('/sms/v2/template/query', ['temId' => $temId]);
}
/**
* 发送模板短信
* @param string $tpId 短信模板
* @param string $sign 短信签名
* @param array $records 发送记录
* @return array
*/
public function tplSend(string $tpId, string $sign, array $records): array
{
return $this->doRequest('/v2/sendSmsTp', [
'tpId' => $tpId, 'records' => $records, 'signature' => $this->_singName($sign),
]);
}
/**
* 发送定时短信
* @param string $mobile 发送手机号码
* @param string $content 发送短信内容
* @param integer $time 定时发送时间(为 0 立即发送)
* @return array
*/
public function timeSend(string $mobile, string $content, int $time = 0): array
{
$data = ['mobile' => $mobile, 'content' => $content];
if ($time > 0) $data['time'] = $time;
return $this->doRequest('/v2/sendSms', $data);
}
/**
* 批量发送短信
* @param array $records
* @return array
*/
public function batchSend(array $records): array
{
return $this->doRequest('/v2/sendSmsPa', ['records' => $records]);
}
/**
* 短信条数查询
*/
public function balance(): array
{
[$state, $result, $message] = $this->doRequest('/v2/balance', []);
return [$state, $state ? $result['sumSms'] : 0, $message];
}
/**
* 短信签名内容处理
* @param string $name
* @return string
*/
private function _singName(string $name): string
{
if (strpos($name, '】') === false) $name = $name . '】';
if (strpos($name, '【') === false) $name = '【' . $name;
return $name;
}
/**
* 执行网络请求
* @param string $uri 接口请求地址
* @param array $data 接口请求参数
* @return array
*/
private function doRequest(string $uri, array $data): array
{
$encode = md5(md5($this->password) . ($tkey = time()));
$options = ['headers' => ['Content-Type:application/json;charset="UTF-8"']];
$extends = ['username' => $this->username, 'password' => $encode, 'tKey' => $tkey];
$result = json_decode(HttpExtend::post($this->api . $uri, json_encode(array_merge($data, $extends)), $options), true);
if (empty($result['code'])) {
return [0, [], lang('接口请求网络异常')];
} elseif (intval($result['code']) === 200) {
return [1, $result, lang($this->error($result['code']))];
} else {
return [0, $result, lang($this->error($result['code']))];
}
}
/**
* 获取状态描述
* @param integer $code 异常编号
* @return string
*/
private function error(int $code): string
{
$arrs = [
200 => '提交成功',
4001 => '用户名错误',
4002 => '密码不能为空',
4003 => '短信内容不能为空',
4004 => '手机号码错误',
4006 => 'IP鉴权错误',
4007 => '用户禁用',
4008 => 'tKey错误',
4009 => '密码错误',
4011 => '请求错误',
4013 => '定时时间错误',
4014 => '模板错误',
4015 => '扩展号错误',
4019 => '用户类型错误',
4023 => '签名错误',
4025 => '模板变量内容为空',
4026 => '手机号码数最大2000个',
4027 => '模板变量内容最大200组',
4029 => '请使用 POST 请求',
4030 => 'Content-Type 请使用 application/json',
4031 => '模板名称不能为空',
4032 => '模板类型不正确',
4034 => '模板内容不能为空',
4035 => '模板名称已经存在',
4036 => '添加模板信息失败',
4037 => '模板名称最大20字符',
4038 => '模板内容超过最大字符数',
4040 => '模板内容缺少变量值或规则错误',
4041 => '模板内容中变量规范错误',
4042 => '模板变量个数超限',
4044 => '接口24小时限制提交次数超限',
9998 => 'JSON解析错误',
9999 => '非法请求',
];
return $arrs[$code] ?? "{$code}";
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,110 @@
<?php
use think\admin\extend\PhinxExtend;
use think\admin\model\SystemConfig;
use think\admin\model\SystemMenu;
use think\admin\model\SystemUser;
use think\admin\service\ProcessService;
use think\helper\Str;
use think\migration\Migrator;
@set_time_limit(0);
@ini_set('memory_limit', -1);
/**
* 数据安装包
* @class __CLASS__
*/
class __CLASS__ extends Migrator
{
/**
* 数据库初始化
* @return void
* @throws \think\db\exception\DbException
*/
public function change()
{
$this->inserData();
$this->insertConf();
$this->insertUser();
$this->insertMenu();
}
/**
* 安装扩展数据
* @return void
* @throws \think\db\exception\DbException
*/
private function inserData()
{
// 待解析处理数据
$json = '__DATA_JSON__';
// 解析并写入扩展数据
if (is_array($tables = json_decode($json, true)) && count($tables) > 0) {
foreach ($tables as $table => $path) if (($model = m($table))->count() < 1) {
$name = Str::studly($table);
ProcessService::message(" -- Starting write {$table} table ..." . PHP_EOL);
[$ls, $rs, $fp] = [0, [], fopen(__DIR__ . DIRECTORY_SEPARATOR . $path, 'r+')];
while (!feof($fp)) {
if (empty($text = trim(fgets($fp)))) continue; else $ls++;
if (is_array($rw = json_decode($text, true))) $rs[] = $rw;
if (count($rs) > 100) [, $rs] = [$model->strict(false)->insertAll($rs), []];
ProcessService::message(" -- -- {$name}:{$ls}", 1);
}
count($rs) > 0 && $model->strict(false)->insertAll($rs);
ProcessService::message(" -- Finished write {$table} table, Total {$ls} rows.", 2);
}
}
}
/**
* 初始化配置参数
* @return void
*/
private function insertConf()
{
$modal = SystemConfig::mk()->whereRaw('1=1')->findOrEmpty();
$modal->isEmpty() && $modal->insertAll([
['type' => 'base', 'name' => 'app_name', 'value' => 'ThinkAdmin'],
['type' => 'base', 'name' => 'app_version', 'value' => 'v6'],
['type' => 'base', 'name' => 'editor', 'value' => 'ckeditor5'],
['type' => 'base', 'name' => 'login_name', 'value' => '系统管理'],
['type' => 'base', 'name' => 'site_copy', 'value' => '©版权所有 2014-' . date('Y') . ' ThinkAdmin'],
['type' => 'base', 'name' => 'site_icon', 'value' => 'https://thinkadmin.top/static/img/logo.png'],
['type' => 'base', 'name' => 'site_name', 'value' => 'ThinkAdmin'],
['type' => 'base', 'name' => 'site_theme', 'value' => 'default'],
['type' => 'wechat', 'name' => 'type', 'value' => 'api'],
['type' => 'storage', 'name' => 'type', 'value' => 'local'],
['type' => 'storage', 'name' => 'allow_exts', 'value' => 'doc,gif,ico,jpg,mp3,mp4,p12,pem,png,zip,rar,xls,xlsx'],
]);
}
/**
* 初始化用户数据
* @return void
*/
private function insertUser()
{
$modal = SystemUser::mk()->whereRaw('1=1')->findOrEmpty();
$modal->isEmpty() && $modal->insert([
'id' => '10000',
'username' => 'admin',
'nickname' => '超级管理员',
'password' => '21232f297a57a5a743894a0e4a801fc3',
'headimg' => 'https://thinkadmin.top/static/img/head.png',
]);
}
/**
* 初始化系统菜单
* @return void
*/
private function insertMenu()
{
if (SystemMenu::mk()->whereRaw('1=1')->findOrEmpty()->isEmpty()) {
// 解析并初始化菜单数据
$json = '__MENU_JSON__';
PhinxExtend::write2menu(json_decode($json, true) ?: []);
}
}
}

View File

@ -0,0 +1,286 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\storage;
use think\admin\contract\StorageInterface;
use think\admin\contract\StorageUsageTrait;
use think\admin\Exception;
use think\admin\extend\HttpExtend;
use think\admin\Storage;
/**
* 阿里云OSS存储支持
* @class AliossStorage
* @package think\admin\storage
*/
class AliossStorage implements StorageInterface
{
use StorageUsageTrait;
/**
* 数据中心
* @var string
*/
private $point;
/**
* 存储空间名称
* @var string
*/
private $bucket;
/**
* AccessId
* @var string
*/
private $accessKey;
/**
* AccessSecret
* @var string
*/
private $secretKey;
/**
* 初始化入口
* @throws \think\admin\Exception
*/
protected function init()
{
// 读取配置文件
$this->point = sysconf('storage.alioss_point|raw');
$this->bucket = sysconf('storage.alioss_bucket|raw');
$this->accessKey = sysconf('storage.alioss_access_key|raw');
$this->secretKey = sysconf('storage.alioss_secret_key|raw');
// 计算链接前缀
$host = strtolower(sysconf('storage.alioss_http_domain|raw'));
$type = strtolower(sysconf('storage.alioss_http_protocol|raw'));
if ($type === 'auto') {
$this->domain = "//{$host}";
} elseif (in_array($type, ['http', 'https'])) {
$this->domain = "{$type}://{$host}";
} else {
throw new Exception(lang('未配置阿里云域名'));
}
}
/**
* 上传文件内容
* @param string $name 文件名称
* @param string $file 文件内容
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return array
*/
public function set(string $name, string $file, bool $safe = false, ?string $attname = null): array
{
$token = $this->token($name);
$data = ['key' => $name];
$data['policy'] = $token['policy'];
$data['Signature'] = $token['signature'];
$data['OSSAccessKeyId'] = $this->accessKey;
$data['success_action_status'] = '200';
if (is_string($attname) && strlen($attname) > 0) {
$data['Content-Disposition'] = 'inline;filename=' . urlencode($attname);
}
$file = ['field' => 'file', 'name' => $name, 'content' => $file];
if (is_numeric(stripos(HttpExtend::submit($this->upload(), $data, $file), '200 OK'))) {
return ['file' => $this->path($name, $safe), 'url' => $this->url($name, $safe, $attname), 'key' => $name];
} else {
return [];
}
}
/**
* 读取文件内容
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return string
*/
public function get(string $name, bool $safe = false): string
{
return Storage::curlGet($this->url($name, $safe));
}
/**
* 删除存储文件
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return boolean
*/
public function del(string $name, bool $safe = false): bool
{
[$file] = explode('?', $name);
$result = HttpExtend::request('DELETE', "https://{$this->bucket}.{$this->point}/{$file}", [
'returnHeader' => true, 'headers' => $this->_sign('DELETE', $file),
]);
return is_numeric(stripos($result, '204 No Content'));
}
/**
* 判断是否存在
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return boolean
*/
public function has(string $name, bool $safe = false): bool
{
$file = $this->delSuffix($name);
$result = HttpExtend::request('HEAD', "https://{$this->bucket}.{$this->point}/{$file}", [
'returnHeader' => true, 'headers' => $this->_sign('HEAD', $file),
]);
return is_numeric(stripos($result, 'HTTP/1.1 200 OK'));
}
/**
* 获取访问地址
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return string
*/
public function url(string $name, bool $safe = false, ?string $attname = null): string
{
return "{$this->domain}/{$this->delSuffix($name)}{$this->getSuffix($attname,$name)}";
}
/**
* 获取存储路径
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return string
*/
public function path(string $name, bool $safe = false): string
{
return $this->url($name, $safe);
}
/**
* 获取文件信息
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return array
*/
public function info(string $name, bool $safe = false, ?string $attname = null): array
{
return $this->has($name, $safe) ? [
'url' => $this->url($name, $safe, $attname),
'key' => $name, 'file' => $this->path($name, $safe),
] : [];
}
/**
* 获取上传地址
* @return string
*/
public function upload(): string
{
$proc = $this->app->request->isSsl() ? 'https' : 'http';
return "{$proc}://{$this->bucket}.{$this->point}";
}
/**
* 获取上传令牌
* @param string $name 文件名称
* @param integer $expires 有效时间
* @param ?string $attname 下载名称
* @return array
*/
public function token(string $name, int $expires = 3600, ?string $attname = null): array
{
$data = [
'policy' => base64_encode(json_encode([
'conditions' => [['content-length-range', 0, 1048576000]],
'expiration' => date('Y-m-d\TH:i:s.000\Z', time() + $expires),
])),
'keyid' => $this->accessKey,
'siteurl' => $this->url($name, false, $attname),
];
$data['signature'] = base64_encode(hash_hmac('sha1', $data['policy'], $this->secretKey, true));
return $data;
}
/**
* 请求数据签名
* @param string $method 请求方式
* @param string $soruce 资源名称
* @return array
*/
private function _sign(string $method, string $soruce): array
{
$header = [];
$header['Date'] = gmdate('D, d M Y H:i:s \G\M\T');
$header['Content-Type'] = 'application/xml';
uksort($header, 'strnatcasecmp');
$content = "{$method}\n\n";
foreach ($header as $key => $value) {
$value = str_replace(["\r", "\n"], '', $value);
if (in_array(strtolower($key), ['content-md5', 'content-type', 'date'])) {
$content .= "{$value}\n";
} elseif (stripos($key, 'x-oss-') === 0) {
$content .= strtolower($key) . ":{$value}\n";
}
}
$content = rawurldecode($content) . "/{$this->bucket}/{$soruce}";
$signature = base64_encode(hash_hmac('sha1', $content, $this->secretKey, true));
$header['Authorization'] = "OSS {$this->accessKey}:{$signature}";
foreach ($header as $key => $value) $header[$key] = "{$key}: {$value}";
return array_values($header);
}
/**
* 获取存储区域
* @return array
*/
public static function region(): array
{
return [
'oss-cn-hangzhou.aliyuncs.com' => lang('华东 1杭州'),
'oss-cn-shanghai.aliyuncs.com' => lang('华东 2上海'),
'oss-cn-nanjing.aliyuncs.com' => lang('华东 5南京本地地域'),
'oss-cn-fuzhou.aliyuncs.com' => lang('华东 6福州本地地域'),
'oss-cn-qingdao.aliyuncs.com' => lang('华北 1青岛'),
'oss-cn-beijing.aliyuncs.com' => lang('华北 2北京'),
'oss-cn-zhangjiakou.aliyuncs.com' => lang('华北 3张家口'),
'oss-cn-huhehaote.aliyuncs.com' => lang('华北 5呼和浩特'),
'oss-cn-wulanchabu.aliyuncs.com' => lang('华北 6乌兰察布'),
'oss-cn-shenzhen.aliyuncs.com' => lang('华南 1深圳'),
'oss-cn-heyuan.aliyuncs.com' => lang('华南 2河源'),
'oss-cn-guangzhou.aliyuncs.com' => lang('华南 3广州'),
'oss-cn-chengdu.aliyuncs.com' => lang('西南 1成都'),
'oss-cn-hongkong.aliyuncs.com' => lang('中国(香港)'),
'oss-us-west-1.aliyuncs.com' => lang('美国(硅谷)'),
'oss-us-east-1.aliyuncs.com' => lang('美国(弗吉尼亚)'),
'oss-ap-northeast-1.aliyuncs.com' => lang('日本(东京)'),
'oss-ap-northeast-2.aliyuncs.com' => lang('韩国(首尔)'),
'oss-ap-southeast-1.aliyuncs.com' => lang('新加坡'),
'oss-ap-southeast-2.aliyuncs.com' => lang('澳大利亚(悉尼)'),
'oss-ap-southeast-3.aliyuncs.com' => lang('马来西亚(吉隆坡)'),
'oss-ap-southeast-5.aliyuncs.com' => lang('印度尼西亚(雅加达)'),
'oss-ap-southeast-6.aliyuncs.com' => lang('菲律宾(马尼拉)'),
'oss-ap-southeast-7.aliyuncs.com' => lang('泰国(曼谷)'),
'oss-ap-south-1.aliyuncs.com' => lang('印度(孟买)'),
'oss-eu-central-1.aliyuncs.com' => lang('德国(法兰克福)'),
'oss-eu-west-1.aliyuncs.com' => lang('英国(伦敦)'),
'oss-me-east-1.aliyuncs.com' => lang('阿联酋(迪拜)'),
'oss-rg-china-mainland.aliyuncs.com' => lang('无地域属性(中国内地)')
];
}
}

View File

@ -0,0 +1,322 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\storage;
use think\admin\contract\StorageInterface;
use think\admin\contract\StorageUsageTrait;
use think\admin\Exception;
use think\admin\extend\HttpExtend;
use think\admin\Storage;
/**
* Alist 自建存储支持
* @class AlistStorage
* @package think\admin\storage
*/
class AlistStorage implements StorageInterface
{
use StorageUsageTrait;
/**
* 用户账号
* @var string
*/
protected $username;
/**
* 用户密码
* @var string
*/
protected $password;
/**
* 保存路径
* @var string
*/
protected $savepath;
/**
* 缓存前缀
* @var string
*/
protected $cachekey;
/**
* 存储引擎初始化
* @return void
* @throws \think\admin\Exception
*/
protected function init()
{
$host = strtolower(sysconf('storage.alist_http_domain|raw'));
$type = strtolower(sysconf('storage.alist_http_protocol|raw'));
if (!empty($host) && $type === 'auto') {
$this->domain = "//{$host}";
} elseif (!empty($host) && in_array($type, ['http', 'https'])) {
$this->domain = "{$type}://{$host}";
} else {
throw new Exception(lang('未配置Alist域名'));
}
$this->username = sysconf('storage.alist_username|raw') ?: '';
$this->password = sysconf('storage.alist_password|raw') ?: '';
$this->savepath = trim(sysconf('storage.alist_savepath|raw') ?: '', '\\/');
$this->savepath = $this->savepath ? "{$this->savepath}/" : '';
$this->cachekey = md5($this->domain . $this->username . $this->password);
}
/**
* 上传文件内容
* @param string $name 文件名称
* @param string $file 文件内容
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return array
* @throws \think\admin\Exception
*/
public function set(string $name, string $file, bool $safe = false, ?string $attname = null): array
{
$file = ['field' => 'file', 'name' => $name, 'content' => $file];
$header = ["Authorization: {$this->token()}", "file-path:" . urlencode($this->real($name))];
$result = HttpExtend::submit("{$this->domain}/api/fs/form", [], $file, $header, 'PUT', false);
if (is_array($data = json_decode($result, true))) {
if ($data['code'] === 200 && $data['message'] === 'success') {
return $this->info($name, $safe);
} else {
throw new Exception($data['message'] ?? '接口请求失败!', intval($data['code'] ?? 0));
}
}
return [];
}
/**
* 读取文件内容
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return string
*/
public function get(string $name, bool $safe = false): string
{
return Storage::curlGet($this->url($name, $safe));
}
/**
* 删除存储文件
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return boolean
*/
public function del(string $name, bool $safe = false): bool
{
try {
$path = $this->real($this->delSuffix($name));
$data = ['dir' => dirname($path) ?: '/', 'names' => [basename($path)]];
$this->httpPost('/api/fs/remove', $data);
return true;
} catch (\Exception $exception) {
return false;
}
}
/**
* 判断是否存在
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return boolean
*/
public function has(string $name, bool $safe = false): bool
{
try {
$this->httpPost('/api/fs/get', [
'path' => $this->real($name)
]);
return true;
} catch (\Exception $exception) {
return false;
}
}
/**
* 获取文件下载链接
* @param string $name
* @param bool $safe
* @param string|null $attname
* @return string
*/
public function url(string $name, bool $safe = false, ?string $attname = null): string
{
$path = rtrim($this->userPath(), '\\/') . $this->real($name);
return "{$this->domain}/d{$this->delSuffix($path)}{$this->getSuffix($attname,$path)}";
}
/**
* 获取存储路径
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return string
*/
public function path(string $name, bool $safe = false): string
{
return $this->url($name, $safe);
}
/**
* 获取文件信息
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return array
*/
public function info(string $name, bool $safe = false, ?string $attname = null): array
{
return $this->has($name, $safe) ? [
'key' => $name,
'url' => $this->url($name, $safe, $attname),
'file' => $this->path($name, $safe),
] : [];
}
/**
* 获取上传地址
* @return string
*/
public function upload(): string
{
return "{$this->domain}/api/fs/form";
}
/**
* 获取存储区域
* @return array
*/
public static function region(): array
{
return [];
}
/**
* 创建目录
* @param string $path
* @return boolean
*/
protected function mkdir(string $path): bool
{
try {
$this->httpPost('/api/fs/mkdir', [
'path' => $this->real($path)
]);
return true;
} catch (\Exception $exception) {
return false;
}
}
/**
* 转换为绝对路径
* @param string $path
* @return string
*/
public function real(string $path): string
{
return "/{$this->savepath}" . trim($path, '\\/');
}
/**
* 获取用户 Token 信息
* @param boolean $force
* @return string
* @throws \think\admin\Exception
*/
public function token(bool $force = false): string
{
try {
$skey = "{$this->cachekey}.token";
if (empty($force) && ($token = $this->app->cache->get($skey))) return $token;
$data = ['Password' => $this->password, 'Username' => $this->username];
$body = $this->httpPost("/api/auth/login", $data, false);
if (!empty($body['data']['token'])) {
$this->app->cache->set($skey, $body['data']['token'], 60);
return $body['data']['token'];
} else {
throw new Exception('获取用户 Token 失败!');
}
} catch (\Exception $exception) {
throw new Exception($exception->getMessage());
}
}
/**
* 获取基础路径
* @return string
*/
private function userPath(): string
{
try {
$skey = "{$this->cachekey}.path";
if ($path = $this->app->cache->get($skey)) return $path;
$data = $this->httGet('/api/me');
if (empty($data['data']['base_path'])) return '/';
$path = trim($data['data']['base_path'], '\\/');
$this->app->cache->set($skey, $path = $path ? "/{$path}/" : '/', 60);
return $path;
} catch (\Exception $exception) {
return "/{$this->savepath}";
}
}
/**
* Get 提交数据
* @param string $uri
* @return array
* @throws \think\admin\Exception
*/
private function httGet(string $uri): array
{
$header = ["Authorization: {$this->token()}"];
$header[] = "Content-Type: application/json;charset=UTF-8";
$result = HttpExtend::get($this->domain . $uri, [], ['headers' => $header]);
if (is_array($data = json_decode($result, true))) {
if ($data['code'] === 200 && $data['message'] === 'success') return $data;
throw new Exception($data['message'] ?? '接口请求失败!', intval($data['code'] ?? 0));
} else {
throw new Exception('接口请求失败!');
}
}
/**
* POST 提交数据
* @param string $uri
* @param array $body
* @param boolean $auth
* @return array
* @throws \think\admin\Exception
*/
private function httpPost(string $uri, array $body = [], bool $auth = true): array
{
$body = json_encode($body, JSON_UNESCAPED_UNICODE);
$header = $auth ? ["Authorization: {$this->token()}"] : [];
$header[] = "Content-Type: application/json;charset=UTF-8";
$result = HttpExtend::post($this->domain . $uri, $body, ['headers' => $header]);
if (is_array($data = json_decode($result, true))) {
if ($data['code'] === 200 && $data['message'] === 'success') return $data;
throw new Exception($data['message'] ?? '接口请求失败!', intval($data['code'] ?? 0));
} else {
throw new Exception('接口请求失败!');
}
}
}

View File

@ -0,0 +1,167 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\storage;
use think\admin\contract\StorageInterface;
use think\admin\contract\StorageUsageTrait;
/**
* 本地存储支持
* @class LocalStorage
* @package think\admin\storage
*/
class LocalStorage implements StorageInterface
{
use StorageUsageTrait;
/**
* 初始化入口
* @throws \think\admin\Exception
*/
protected function init()
{
$type = sysconf('storage.local_http_protocol|raw') ?: 'follow';
if ($type === 'follow') $type = $this->app->request->scheme();
$this->domain = trim(dirname($this->app->request->baseFile()), '\\/');
if ($type !== 'path') {
$domain = sysconf('storage.local_http_domain|raw') ?: $this->app->request->host();
if ($type === 'auto') {
$this->domain = "//{$domain}";
} elseif (in_array($type, ['http', 'https'])) {
$this->domain = "{$type}://{$domain}";
}
}
}
/**
* 上传文件内容
* @param string $name 文件名称
* @param string $file 文件内容
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return array
*/
public function set(string $name, string $file, bool $safe = false, ?string $attname = null): array
{
try {
$path = $this->path($name, $safe);
is_dir($dir = dirname($path)) || mkdir($dir, 0777, true);
if (file_put_contents($path, $file)) {
return $this->info($name, $safe, $attname);
}
} catch (\Exception $exception) {
}
return [];
}
/**
* 读取文件内容
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return string
*/
public function get(string $name, bool $safe = false): string
{
if (!$this->has($name, $safe)) return '';
return file_get_contents($this->path($name, $safe));
}
/**
* 删除存储文件
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return boolean
*/
public function del(string $name, bool $safe = false): bool
{
if ($this->has($name, $safe)) {
return @unlink($this->path($name, $safe));
} else {
return false;
}
}
/**
* 判断是否存在
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return boolean
*/
public function has(string $name, bool $safe = false): bool
{
return is_file($this->path($name, $safe));
}
/**
* 获取访问地址
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return string
*/
public function url(string $name, bool $safe = false, ?string $attname = null): string
{
return $safe ? $name : "{$this->domain}/upload/{$this->delSuffix($name)}{$this->getSuffix($attname,$name)}";
}
/**
* 获取存储路径
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return string
*/
public function path(string $name, bool $safe = false): string
{
$path = $safe ? 'safefile' : 'public/upload';
return strtr(syspath("{$path}/{$this->delSuffix($name)}"), '\\', '/');
}
/**
* 获取文件信息
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return array
*/
public function info(string $name, bool $safe = false, ?string $attname = null): array
{
return $this->has($name, $safe) ? [
'url' => $this->url($name, $safe, $attname),
'key' => "upload/{$name}", 'file' => $this->path($name, $safe),
] : [];
}
/**
* 获取上传地址
* @return string
*/
public function upload(): string
{
return url('admin/api.upload/file', [], false, true)->build();
}
/**
* 获取存储区域
* @return array
*/
public static function region(): array
{
return [];
}
}

View File

@ -0,0 +1,252 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\storage;
use think\admin\contract\StorageInterface;
use think\admin\contract\StorageUsageTrait;
use think\admin\Exception;
use think\admin\extend\HttpExtend;
use think\admin\Storage;
/**
* 七牛云存储支持
* @class QiniuStorage
* @package think\admin\storage
*/
class QiniuStorage implements StorageInterface
{
use StorageUsageTrait;
private $bucket;
private $accessKey;
private $secretKey;
/**
* 初始化入口
* @throws \think\admin\Exception
*/
protected function init()
{
// 读取配置文件
$this->bucket = sysconf('storage.qiniu_bucket|raw');
$this->accessKey = sysconf('storage.qiniu_access_key|raw');
$this->secretKey = sysconf('storage.qiniu_secret_key|raw');
// 计算链接前缀
$host = strtolower(sysconf('storage.qiniu_http_domain|raw'));
$type = strtolower(sysconf('storage.qiniu_http_protocol|raw'));
if ($type === 'auto') {
$this->domain = "//{$host}";
} elseif (in_array($type, ['http', 'https'])) {
$this->domain = "{$type}://{$host}";
} else {
throw new Exception(lang('未配置七牛云域名'));
}
}
/**
* 上传文件内容
* @param string $name 文件名称
* @param string $file 文件内容
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return array
* @throws \think\admin\Exception
*/
public function set(string $name, string $file, bool $safe = false, ?string $attname = null): array
{
$token = $this->token($name, 3600, $attname);
$data = ['key' => $name, 'token' => $token, 'fileName' => $name];
$file = ['field' => "file", 'name' => $name, 'content' => $file];
$result = HttpExtend::submit($this->upload(), $data, $file, [], 'POST', false);
return json_decode($result, true);
}
/**
* 读取文件内容
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return string
*/
public function get(string $name, bool $safe = false): string
{
$url = $this->url($name, $safe) . "?e=" . time();
$token = "{$this->accessKey}:{$this->safeBase64(hash_hmac('sha1', $url, $this->secretKey, true))}";
return Storage::curlGet("{$url}&token={$token}");
}
/**
* 删除存储文件
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return boolean
*/
public function del(string $name, bool $safe = false): bool
{
[$EncodedEntryURI, $AccessToken] = $this->getAccessToken($name, 'delete');
$data = json_decode(HttpExtend::post("https://rs.qiniu.com/delete/{$EncodedEntryURI}", [], [
'headers' => ["Authorization:QBox {$AccessToken}"],
]), true);
return empty($data['error']);
}
/**
* 判断是否存在
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return boolean
*/
public function has(string $name, bool $safe = false): bool
{
return !empty($this->info($name, $safe));
}
/**
* 获取访问地址
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return string
*/
public function url(string $name, bool $safe = false, ?string $attname = null): string
{
return "{$this->domain}/{$this->delSuffix($name)}{$this->getSuffix($attname,$name)}";
}
/**
* 获取存储路径
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return string
*/
public function path(string $name, bool $safe = false): string
{
return $this->url($name, $safe);
}
/**
* 获取文件信息
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return array
*/
public function info(string $name, bool $safe = false, ?string $attname = null): array
{
[$entry, $token] = $this->getAccessToken($name);
$data = json_decode(HttpExtend::get("https://rs.qiniu.com/stat/{$entry}", [], ['headers' => ["Authorization: QBox {$token}"]]), true);
return isset($data['md5']) ? ['file' => $name, 'url' => $this->url($name, $safe, $attname), 'key' => $name] : [];
}
/**
* 获取上传地址
* @return string
* @throws \think\admin\Exception
*/
public function upload(): string
{
try {
$proc = $this->app->request->isSsl() ? 'https' : 'http';
$region = sysconf('storage.qiniu_region|raw');
} catch (\Exception $exception) {
throw new Exception($exception->getMessage());
}
// 注:汉字为兼容旧版本区域配置
switch ($region) {
case '华东':
case 'up.qiniup.com':
return "{$proc}://up.qiniup.com";
case 'up-cn-east-2.qiniup.com':
return "{$proc}://up-cn-east-2.qiniup.com";
case '华北':
case 'up-z1.qiniup.com':
return "{$proc}://up-z1.qiniup.com";
case '华南':
case 'up-z2.qiniup.com':
return "{$proc}://up-z2.qiniup.com";
case '北美':
case 'up-na0.qiniup.com':
return "{$proc}://up-na0.qiniup.com";
case '东南亚':
case 'up-as0.qiniup.com':
return "{$proc}://up-as0.qiniup.com";
case 'up-ap-northeast-1.qiniup.com':
return "{$proc}://up-ap-northeast-1.qiniup.com";
default:
throw new Exception(lang('未配置七牛云空间区域哦'));
}
}
/**
* 生成上传令牌
* @param ?string $name 文件名称
* @param integer $expires 有效时间
* @param ?string $attname 下载名称
* @return string
*/
public function token(?string $name = null, int $expires = 3600, ?string $attname = null): string
{
$key = is_null($name) ? '$(key)' : $name;
$url = "{$this->domain}/$(key){$this->getSuffix($attname,$name)}";
$policy = $this->safeBase64(json_encode([
"deadline" => time() + $expires, "scope" => is_null($name) ? $this->bucket : "{$this->bucket}:{$name}",
'returnBody' => json_encode(['uploaded' => true, 'filename' => '$(key)', 'url' => $url, 'key' => $key, 'file' => $key], JSON_UNESCAPED_UNICODE),
]));
return "{$this->accessKey}:{$this->safeBase64(hash_hmac('sha1', $policy, $this->secretKey, true))}:{$policy}";
}
/**
* 安全BASE64编码
* @param string $content
* @return string
*/
private function safeBase64(string $content): string
{
return str_replace(['+', '/'], ['-', '_'], base64_encode($content));
}
/**
* 生成管理凭证
* @param string $name 文件名称
* @param string $type 操作类型
* @return array
*/
private function getAccessToken(string $name, string $type = 'stat'): array
{
$entry = $this->safeBase64("{$this->bucket}:{$name}");
$sign = hash_hmac('sha1', "/{$type}/{$entry}\n", $this->secretKey, true);
return [$entry, "{$this->accessKey}:{$this->safeBase64($sign)}"];
}
/**
* 获取存储区域
* @return array
*/
public static function region(): array
{
return [
'up.qiniup.com' => lang('华东-浙江'),
'up-cn-east-2.qiniup.com' => lang('华东-浙江2'),
'up-z1.qiniup.com' => lang('华北-河北'),
'up-z2.qiniup.com' => lang('华南-广东'),
'up-na0.qiniup.com' => lang('北美-洛杉矶'),
'up-as0.qiniup.com' => lang('亚太-新加坡'),
'up-ap-northeast-1.qiniup.com' => lang('亚太-首尔'),
];
}
}

View File

@ -0,0 +1,304 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\storage;
use think\admin\contract\StorageInterface;
use think\admin\contract\StorageUsageTrait;
use think\admin\Exception;
use think\admin\extend\HttpExtend;
use think\admin\Storage;
/**
* 腾讯云COS存储支持
* @class TxcosStorage
* @package think\admin\storage
*/
class TxcosStorage implements StorageInterface
{
use StorageUsageTrait;
/**
* 数据中心
* @var string
*/
private $point;
/**
* 存储空间名称
* @var string
*/
private $bucket;
/**
* $secretId
* @var string
*/
private $secretId;
/**
* secretKey
* @var string
*/
private $secretKey;
/**
* 初始化入口
* @throws \think\admin\Exception
*/
protected function init()
{
// 读取配置文件
$this->point = sysconf('storage.txcos_point|raw');
$this->bucket = sysconf('storage.txcos_bucket|raw');
$this->secretId = sysconf('storage.txcos_access_key|raw');
$this->secretKey = sysconf('storage.txcos_secret_key|raw');
// 计算链接前缀
$host = strtolower(sysconf('storage.txcos_http_domain|raw'));
$type = strtolower(sysconf('storage.txcos_http_protocol|raw'));
if ($type === 'auto') {
$this->domain = "//{$host}";
} elseif (in_array($type, ['http', 'https'])) {
$this->domain = "{$type}://{$host}";
} else {
throw new Exception(lang('未配置腾讯云域名'));
}
}
/**
* 上传文件内容
* @param string $name 文件名称
* @param string $file 文件内容
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return array
*/
public function set(string $name, string $file, bool $safe = false, ?string $attname = null): array
{
$data = $this->token($name) + ['key' => $name];
if (is_string($attname) && strlen($attname) > 0) {
$data['Content-Disposition'] = urlencode($attname);
}
$data['success_action_status'] = '200';
$file = ['field' => 'file', 'name' => $name, 'content' => $file];
if (is_numeric(stripos(HttpExtend::submit($this->upload(), $data, $file), '200 OK'))) {
return ['file' => $this->path($name, $safe), 'url' => $this->url($name, $safe, $attname), 'key' => $name];
} else {
return [];
}
}
/**
* 读取文件内容
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return string
*/
public function get(string $name, bool $safe = false): string
{
return Storage::curlGet($this->url($name, $safe));
}
/**
* 删除存储文件
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return boolean
*/
public function del(string $name, bool $safe = false): bool
{
[$file] = explode('?', $name);
$result = HttpExtend::request('DELETE', "https://{$this->bucket}.{$this->point}/{$file}", [
'returnHeader' => true, 'headers' => $this->_sign('DELETE', $file),
]);
return is_numeric(stripos($result, '204 No Content'));
}
/**
* 判断是否存在
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return boolean
*/
public function has(string $name, bool $safe = false): bool
{
$file = $this->delSuffix($name);
$result = HttpExtend::request('HEAD', "https://{$this->bucket}.{$this->point}/{$file}", [
'returnHeader' => true, 'headers' => $this->_sign('HEAD', $name),
]);
return is_numeric(stripos($result, 'HTTP/1.1 200 OK'));
}
/**
* 获取访问地址
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return string
*/
public function url(string $name, bool $safe = false, ?string $attname = null): string
{
return "{$this->domain}/{$this->delSuffix($name)}{$this->getSuffix($attname,$name)}";
}
/**
* 获取存储路径
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return string
*/
public function path(string $name, bool $safe = false): string
{
return $this->url($name, $safe);
}
/**
* 获取文件信息
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return array
*/
public function info(string $name, bool $safe = false, ?string $attname = null): array
{
return $this->has($name, $safe) ? [
'url' => $this->url($name, $safe, $attname),
'key' => $name, 'file' => $this->path($name, $safe),
] : [];
}
/**
* 获取上传地址
* @return string
*/
public function upload(): string
{
$proc = $this->app->request->isSsl() ? 'https' : 'http';
return "{$proc}://{$this->bucket}.{$this->point}";
}
/**
* 生成上传令牌
* @param string $name 文件名称
* @param integer $expires 有效时间
* @param ?string $attname 下载名称
* @return array
*/
public function token(string $name, int $expires = 3600, ?string $attname = null): array
{
$startTimestamp = time();
$endTimestamp = $startTimestamp + $expires;
$keyTime = "{$startTimestamp};{$endTimestamp}";
$siteurl = $this->url($name, false, $attname);
$policy = json_encode([
'expiration' => date('Y-m-d\TH:i:s.000\Z', $endTimestamp),
'conditions' => [['q-ak' => $this->secretId], ['q-sign-time' => $keyTime], ['q-sign-algorithm' => 'sha1']],
]);
return [
'policy' => base64_encode($policy), 'q-ak' => $this->secretId,
'siteurl' => $siteurl, 'q-key-time' => $keyTime, 'q-sign-algorithm' => 'sha1',
'q-signature' => hash_hmac('sha1', sha1($policy), hash_hmac('sha1', $keyTime, $this->secretKey)),
];
}
/**
* 生成请求签名
* @param string $method 请求方式
* @param string $soruce 资源名称
* @return array
*/
private function _sign(string $method, string $soruce): array
{
$header = [];
// 1.生成 KeyTime
$startTimestamp = time();
$endTimestamp = $startTimestamp + 3600;
$keyTime = "{$startTimestamp};{$endTimestamp}";
// 2.生成 SignKey
$signKey = hash_hmac('sha1', $keyTime, $this->secretKey);
// 3.生成 UrlParamList, HttpParameters
[$parse_url, $urlParamList, $httpParameters] = [parse_url($soruce), '', ''];
if (!empty($parse_url['query'])) {
parse_str($parse_url['query'], $params);
uksort($params, 'strnatcasecmp');
$urlParamList = join(';', array_keys($params));
$httpParameters = http_build_query($params);
}
// 4.生成 HeaderList, HttpHeaders
[$headerList, $httpHeaders] = ['', ''];
if (!empty($header)) {
uksort($header, 'strnatcasecmp');
$headerList = join(';', array_keys($header));
$httpHeaders = http_build_query($header);
}
// 5.生成 HttpString
$httpString = strtolower($method) . "\n/{$parse_url['path']}\n{$httpParameters}\n{$httpHeaders}\n";
// 6.生成 StringToSign
$httpStringSha1 = sha1($httpString);
$stringToSign = "sha1\n{$keyTime}\n{$httpStringSha1}\n";
// 7.生成 Signature
$signature = hash_hmac('sha1', $stringToSign, $signKey);
// 8.生成签名
$signArray = [
'q-sign-algorithm' => 'sha1',
'q-ak' => $this->secretId,
'q-sign-time' => $keyTime,
'q-key-time' => $keyTime,
'q-header-list' => $headerList,
'q-url-param-list' => $urlParamList,
'q-signature' => $signature,
];
$header['Authorization'] = urldecode(http_build_query($signArray));
foreach ($header as $key => $value) $header[$key] = ucfirst($key) . ": {$value}";
return array_values($header);
}
/**
* 获取存储区域
* @return array
*/
public static function region(): array
{
return [
'cos.ap-beijing-1.myqcloud.com' => lang('中国大陆 公有云地域 北京一区'),
'cos.ap-beijing.myqcloud.com' => lang('中国大陆 公有云地域 北京'),
'cos.ap-nanjing.myqcloud.com' => lang('中国大陆 公有云地域 南京'),
'cos.ap-shanghai.myqcloud.com' => lang('中国大陆 公有云地域 上海'),
'cos.ap-guangzhou.myqcloud.com' => lang('中国大陆 公有云地域 广州'),
'cos.ap-chengdu.myqcloud.com' => lang('中国大陆 公有云地域 成都'),
'cos.ap-chongqing.myqcloud.com' => lang('中国大陆 公有云地域 重庆'),
'cos.ap-shenzhen-fsi.myqcloud.com' => lang('中国大陆 金融云地域 深圳金融'),
'cos.ap-shanghai-fsi.myqcloud.com' => lang('中国大陆 金融云地域 上海金融'),
'cos.ap-beijing-fsi.myqcloud.com' => lang('中国大陆 金融云地域 北京金融'),
'cos.ap-hongkong.myqcloud.com' => lang('亚太地区 公有云地域 中国香港'),
'cos.ap-singapore.myqcloud.com' => lang('亚太地区 公有云地域 新加坡'),
'cos.ap-mumbai.myqcloud.com' => lang('亚太地区 公有云地域 孟买'),
'cos.ap-jakarta.myqcloud.com' => lang('亚太地区 公有云地域 雅加达'),
'cos.ap-seoul.myqcloud.com' => lang('亚太地区 公有云地域 首尔'),
'cos.ap-bangkok.myqcloud.com' => lang('亚太地区 公有云地域 曼谷'),
'cos.ap-tokyo.myqcloud.com' => lang('亚太地区 公有云地域 东京'),
'cos.na-siliconvalley.myqcloud.com' => lang('北美地区 公有云地域 硅谷'),
'cos.na-ashburn.myqcloud.com' => lang('北美地区 公有云地域 弗吉尼亚'),
'cos.na-toronto.myqcloud.com' => lang('北美地区 公有云地域 多伦多'),
'cos.sa-saopaulo.myqcloud.com' => lang('北美地区 公有云地域 圣保罗'),
'cos.eu-frankfurt.myqcloud.com' => lang('欧洲地区 公有云地域 法兰克福'),
'cos.eu-moscow.myqcloud.com' => lang('欧洲地区 公有云地域 莫斯科'),
];
}
}

View File

@ -0,0 +1,232 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\storage;
use think\admin\contract\StorageInterface;
use think\admin\contract\StorageUsageTrait;
use think\admin\Exception;
use think\admin\extend\HttpExtend;
use think\admin\Storage;
/**
* 又拍云存储支持
* @class UpyunStorage
* @package think\admin\storage
*/
class UpyunStorage implements StorageInterface
{
use StorageUsageTrait;
/**
* 存储空间名称
* @var string
*/
private $bucket;
/**
* AccessId
* @var string
*/
private $accessKey;
/**
* AccessSecret
* @var string
*/
private $secretKey;
/**
* 初始化入口
* @throws \think\admin\Exception
*/
protected function init()
{
// 读取配置文件
$this->bucket = sysconf('storage.upyun_bucket|raw');
$this->accessKey = sysconf('storage.upyun_access_key|raw');
$this->secretKey = sysconf('storage.upyun_secret_key|raw');
// 计算链接前缀
$host = strtolower(sysconf('storage.upyun_http_domain|raw'));
$type = strtolower(sysconf('storage.upyun_http_protocol|raw'));
if ($type === 'auto') {
$this->domain = "//{$host}";
} elseif (in_array($type, ['http', 'https'])) {
$this->domain = "{$type}://{$host}";
} else {
throw new Exception(lang('未配置又拍云域名'));
}
}
/**
* 上传文件内容
* @param string $name 文件名称
* @param string $file 文件内容
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return array
*/
public function set(string $name, string $file, bool $safe = false, ?string $attname = null): array
{
$data = [];
$token = $this->token($name, 3600, $attname, md5($file));
$data['policy'] = $token['policy'];
$data['authorization'] = $token['authorization'];
$file = ['field' => 'file', 'name' => $name, 'content' => $file];
if (is_numeric(stripos(HttpExtend::submit($this->upload(), $data, $file), '200 OK'))) {
return ['file' => $this->path($name, $safe), 'url' => $this->url($name, $safe, $attname), 'key' => $name];
} else {
return [];
}
}
/**
* 读取文件内容
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return string
*/
public function get(string $name, bool $safe = false): string
{
return Storage::curlGet($this->url($name, $safe));
}
/**
* 删除存储文件
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return boolean
*/
public function del(string $name, bool $safe = false): bool
{
[$file] = explode('?', $name);
$result = HttpExtend::request('DELETE', "{$this->upload()}/{$file}", [
'returnHeader' => true, 'headers' => $this->_sign('DELETE', $file),
]);
return is_numeric(stripos($result, 'HTTP/1.1 200 OK'));
}
/**
* 判断是否存在
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return boolean
*/
public function has(string $name, bool $safe = false): bool
{
$file = $this->delSuffix($name);
$result = HttpExtend::request('HEAD', "{$this->upload()}/{$file}", [
'returnHeader' => true, 'headers' => $this->_sign('HEAD', $file),
]);
return is_numeric(stripos($result, 'HTTP/1.1 200 OK'));
}
/**
* 获取访问地址
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return string
*/
public function url(string $name, bool $safe = false, ?string $attname = null): string
{
return "{$this->domain}/{$this->delSuffix($name)}{$this->getSuffix($attname,$name)}";
}
/**
* 获取存储路径
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @return string
*/
public function path(string $name, bool $safe = false): string
{
return $this->url($name, $safe);
}
/**
* 获取文件信息
* @param string $name 文件名称
* @param boolean $safe 安全模式
* @param ?string $attname 下载名称
* @return array
*/
public function info(string $name, bool $safe = false, ?string $attname = null): array
{
return $this->has($name, $safe) ? [
'url' => $this->url($name, $safe, $attname),
'key' => $name, 'file' => $this->path($name, $safe),
] : [];
}
/**
* 获取上传地址
* @return string
*/
public function upload(): string
{
$protocol = $this->app->request->isSsl() ? 'https' : 'http';
return "{$protocol}://v0.api.upyun.com/{$this->bucket}";
}
/**
* 生成上传令牌
* @param string $name 文件名称
* @param integer $expires 有效时间
* @param ?string $attname 下载名称
* @param ?string $fileHash 文件哈希
* @return array
*/
public function token(string $name, int $expires = 3600, ?string $attname = null, ?string $fileHash = ''): array
{
$policy = ['save-key' => $name];
$policy['date'] = gmdate('D, d M Y H:i:s \G\M\T');
$policy['bucket'] = $this->bucket;
$policy['expiration'] = time() + $expires;
if ($fileHash) $policy['content-md5'] = $fileHash;
$data = ['keyid' => $this->accessKey, 'siteurl' => $this->url($name, false, $attname)];
$data['policy'] = base64_encode(json_encode($policy, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$content = "POST&/{$this->bucket}&{$policy['date']}&{$data['policy']}";
if ($fileHash) $content .= "&{$fileHash}";
$data['signature'] = base64_encode(hash_hmac('sha1', $content, md5($this->secretKey), true));
$data['authorization'] = "UPYUN {$this->accessKey}:{$data['signature']}";
return $data;
}
/**
* 生成请求签名
* @param string $method 请求方式
* @param string $name 资源名称
* @return array
*/
private function _sign(string $method, string $name): array
{
$data = [$method, "/{$this->bucket}/{$name}", $date = gmdate('D, d M Y H:i:s \G\M\T')];
$signature = base64_encode(hash_hmac('sha1', join('&', $data), md5($this->secretKey), true));
return ["Authorization:UPYUN {$this->accessKey}:{$signature}", "Date:{$date}"];
}
/**
* 获取存储区域
* @return array
*/
public static function region(): array
{
return [];
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\support;
use think\Route as ThinkRoute;
/**
* 自定义路由对象
* @class Route
* @package think\admin\support
*/
class Route extends ThinkRoute
{
/**
* 重载路由配置
* @return $this
*/
public function reload(): Route
{
$this->config = array_merge($this->config, $this->app->config->get('route'));
return $this;
}
}

View File

@ -0,0 +1,203 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
// 以下代码来自 topthink/think-multi-app有部分修改以兼容 ThinkAdmin 的需求
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\support;
use InvalidArgumentException;
use think\admin\service\NodeService;
use think\route\Url as ThinkUrl;
/**
* 多应用 URL 生成与解析
* @class Url
* @package think\admin\support
*/
class Url extends ThinkUrl
{
/**
* 直接解析 URL 地址
* @param string $url URL
* @param string|boolean $domain Domain
* @return string
*/
protected function parseUrl(string $url, &$domain): string
{
$request = $this->app->request;
if (0 === strpos($url, '/')) {
$url = substr($url, 1);
} elseif (false !== strpos($url, '\\')) {
$url = ltrim(str_replace('\\', '/', $url), '/');
} elseif (0 === strpos($url, '@')) {
$url = substr($url, 1);
} else {
$attrs = str2arr($url, '/');
$action = empty($attrs) ? $request->action() : array_pop($attrs);
$contrl = empty($attrs) ? $request->controller() : array_pop($attrs);
$module = empty($attrs) ? $this->app->http->getName() : array_pop($attrs);
// 拼装新的链接地址
$url = NodeService::nameTolower($contrl) . '/' . $action;
$bind = $this->app->config->get('app.domain_bind', []);
if ($key = array_search($module, $bind)) {
if (isset($bind[$_SERVER['SERVER_NAME']])) $domain = $_SERVER['SERVER_NAME'];
$domain = is_bool($domain) ? $key : $domain;
} elseif ($key = array_search($module, $this->app->config->get('app.app_map', []))) {
$url = $key . '/' . $url;
} else {
$url = $module . '/' . $url;
}
}
return $url;
}
/**
* Build URL
* @return string
*/
public function build(): string
{
$url = $this->url;
$vars = $this->vars;
$domain = $this->domain;
$suffix = $this->suffix;
$request = $this->app->request;
if (0 === strpos($url, '[') && $pos = strpos($url, ']')) {
// [name] 表示使用路由命名标识生成URL
$name = substr($url, 1, $pos - 1);
$url = 'name' . substr($url, $pos + 1);
}
if (false === strpos($url, '://') && 0 !== strpos($url, '/')) {
$info = parse_url($url);
$url = !empty($info['path']) ? $info['path'] : '';
if (isset($info['fragment'])) {
// 解析锚点
$anchor = $info['fragment'];
if (false !== strpos($anchor, '?')) {
// 解析参数
[$anchor, $info['query']] = explode('?', $anchor, 2);
}
if (false !== strpos($anchor, '@')) {
// 解析域名
[$anchor, $domain] = explode('@', $anchor, 2);
}
} elseif (strpos($url, '@') && false === strpos($url, '\\')) {
// 解析域名
[$url, $domain] = explode('@', $url, 2);
}
}
if ($url) {
$checkDomain = $domain && is_string($domain) ? $domain : null;
$checkName = $name ?? $url . (isset($info['query']) ? '?' . $info['query'] : '');
$rule = $this->route->getName($checkName, $checkDomain);
if (empty($rule) && isset($info['query'])) {
$rule = $this->route->getName($url, $checkDomain);
parse_str($info['query'], $params);
$vars = array_merge($params, $vars);
unset($info['query']);
}
}
if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain)) {
$url = $match[0];
if ($domain && !empty($match[1])) $domain = $match[1];
if (!is_null($match[2])) $suffix = $match[2];
if (!$this->app->http->isBind()) {
$url = $this->app->http->getName() . '/' . $url;
}
} elseif (!empty($rule) && isset($name)) {
throw new InvalidArgumentException('route name not exists:' . $name);
} else {
// 检测URL绑定
$bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null);
if ($bind && 0 === strpos($url, $bind)) {
$url = substr($url, strlen($bind) + 1);
} else {
$binds = $this->route->getBind();
foreach ($binds as $key => $val) {
if (is_string($val) && 0 === strpos($url, $val) && substr_count($val, '/') > 1) {
$url = substr($url, strlen($val) + 1);
$domain = $key;
break;
}
}
}
// 路由标识不存在 直接解析
$url = $this->parseUrl($url, $domain);
if (isset($info['query'])) {
// 解析地址里面参数 合并到vars
parse_str($info['query'], $params);
$vars = array_merge($params, $vars);
}
}
// 还原 URL 分隔符
$file = $request->baseFile();
$depr = $this->route->config('pathinfo_depr');
[$uri, $url] = [$request->url(), str_replace('/', $depr, $url)];
if ($file && 0 !== strpos($uri, $file)) {
$file = str_replace('\\', '/', dirname($file));
}
/*=====- 多应用绑定 URL 生成处理 -=====*/
$app = $this->app->http->getName();
if ($this->app->http->isBind()) {
if (preg_match("#^{$app}({$depr}|\.|$)#i", $url)) {
$url = trim(substr($url, strlen($app)), $depr);
} elseif (substr_count($url, $depr) >= 2) {
$file = 'index.php';
}
}
$url = rtrim($file, '/') . '/' . ltrim($url, '/');
// URL后缀
if ('/' == substr($url, -1) || '' == $url) {
$suffix = '';
} else {
$suffix = $this->parseSuffix($suffix);
}
// 锚点
$anchor = !empty($anchor) ? '#' . $anchor : '';
// 参数组装
if (!empty($vars)) {
// 添加参数
if ($this->route->config('url_common_param')) {
$vars = http_build_query($vars);
$url .= $suffix . '?' . $vars . $anchor;
} else {
foreach ($vars as $var => $val) {
$val = (string)$val;
if ('' !== $val) {
$url .= $depr . $var . $depr . urlencode($val);
}
}
$url .= $suffix . $anchor;
}
} else {
$url .= $suffix . $anchor;
}
// 检测域名
$domain = $this->parseDomain($url, $domain);
// URL 组装
return $domain . rtrim($this->root, '/') . '/' . ltrim($url, '/');
}
}

View File

@ -0,0 +1,96 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\support\command;
use think\admin\Command;
use think\admin\service\SystemService;
use think\console\Input;
use think\console\input\Argument;
use think\console\Output;
/**
* 数据库修复优化指令
* @class Database
* @package think\admin\support\command
*/
class Database extends Command
{
/**
* 指令任务配置
*/
public function configure()
{
$this->setName('xadmin:database');
$this->addArgument('action', Argument::OPTIONAL, 'repair|optimize', 'optimize');
$this->setDescription('Database Optimize and Repair for ThinkAdmin');
}
/**
* 任务执行入口
* @param \think\console\Input $input
* @param \think\console\Output $output
* @return void
* @throws \think\admin\Exception
*/
protected function execute(Input $input, Output $output): void
{
if ($this->app->db->connect()->getConfig('type') === 'sqlite') {
$this->setQueueError("Sqlite 数据库不支持 REPAIR 和 OPTIMIZE 操作!");
}
$action = $input->getArgument('action');
if (method_exists($this, $method = "_{$action}")) $this->$method();
else $this->output->error('Wrong operation, currently allow repair|optimize');
}
/**
* 修复所有数据表
* @return void
* @throws \think\admin\Exception
*/
protected function _repair(): void
{
$this->setQueueProgress("正在获取需要修复的数据表", '0');
[$tables, $total, $count] = SystemService::getTables();
$this->setQueueProgress("总共需要修复 {$total} 张数据表", '0');
foreach ($tables as $table) {
$this->setQueueMessage($total, ++$count, "正在修复数据表 {$table}");
$this->app->db->connect()->query("REPAIR TABLE `{$table}`");
$this->setQueueMessage($total, $count, "完成修复数据表 {$table}", 1);
}
$this->setQueueSuccess("已完成对 {$total} 张数据表修复操作");
}
/**
* 优化所有数据表
* @return void
* @throws \think\admin\Exception
*/
protected function _optimize(): void
{
$this->setQueueProgress("正在获取需要优化的数据表", '0');
[$tables, $total, $count] = SystemService::getTables();
$this->setQueueProgress("总共需要优化 {$total} 张数据表", '0');
foreach ($tables as $table) {
$this->setQueueMessage($total, ++$count, "正在优化数据表 {$table}");
$this->app->db->connect()->query("OPTIMIZE TABLE `{$table}`");
$this->setQueueMessage($total, $count, "完成优化数据表 {$table}", 1);
}
$this->setQueueSuccess("已完成对 {$total} 张数据表优化操作");
}
}

View File

@ -0,0 +1,158 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\support\command;
use think\admin\Command;
use think\admin\Exception;
use think\admin\extend\PhinxExtend;
use think\admin\Library;
use think\admin\service\SystemService;
use think\console\input\Option;
/**
* 生成数据安装包
* @class Package
* @package think\admin\support\command
*/
class Package extends Command
{
/**
* 系统指定配置
* @return void
*/
public function configure()
{
$this->setName('xadmin:package');
$this->addOption('all', 'a', Option::VALUE_NONE, 'Backup All Tables');
$this->addOption('table', 't', Option::VALUE_OPTIONAL, 'Package Tables Scheme', '');
$this->addOption('backup', 'b', Option::VALUE_OPTIONAL, 'Package Tables Backup', '');
$this->setDescription('Generate System Install Package for ThinkAdmin');
}
/**
* 生成系统安装数据包
* @return void
* @throws \think\admin\Exception
*/
public function handle()
{
try {
// 创建数据库迁移脚本目录
$dirname = syspath('database/migrations');
is_dir($dirname) or mkdir($dirname, 0777, true);
// 开始创建数据库迁移脚本
$this->output->writeln('--- 开始创建数据库迁移脚本 ---');
if ($this->createBackup() && $this->createScheme()) {
$this->setQueueSuccess('--- 数据库迁移脚本创建成功 ---');
} else {
$this->setQueueError('--- 数据库迁移脚本创建失败 ---');
}
} catch (Exception $exception) {
throw $exception;
} catch (\Exception $exception) {
trace_file($exception);
$this->setQueueError($exception->getMessage());
}
}
/**
* 创建数据表
* @return boolean
* @throws \Exception
*/
private function createScheme(): bool
{
// 接收指定打包数据表
if ($this->input->hasOption('table')) {
$tables = str2arr(strtr($this->input->getOption('table'), '|', ','));
} elseif ($this->input->hasOption('all')) {
[$tables] = SystemService::getTables();
} else {
$tables = Library::$sapp->config->get('phinx.tables', []);
if (empty($tables)) [$tables] = SystemService::getTables();
}
// 去除忽略的数据表
$ignore = Library::$sapp->config->get('phinx.ignore', []);
$tables = array_unique(array_diff($tables, $ignore, ['migrations']));
// 创建数据库结构安装脚本
[$prefix, $groups] = ['', []];
foreach ($tables as $table) {
$attr = explode('_', $table);
if ($attr[0] === 'plugin') array_shift($attr);
if (empty($prefix) || $prefix !== $attr[0]) {
$prefix = $attr[0];
}
$groups[$prefix][] = $table;
}
[$total, $count] = [count($groups), 0];
$this->setQueueMessage($total, 0, '开始创建数据表创建脚本!');
foreach ($groups as $key => $tbs) {
$name = 'Install' . ucfirst($key) . 'Table';
$phinx = PhinxExtend::create2table($tbs, $name);
$target = syspath("database/migrations/{$phinx['file']}");
if (file_put_contents($target, $phinx['text']) !== false) {
$this->setQueueMessage($total, ++$count, "创建数据库 {$name} 安装脚本成功!");
} else {
$this->setQueueMessage($total, ++$count, "创建数据库 {$name} 安装脚本失败!");
return false;
}
}
return true;
}
/**
* 创建数据包
* @return boolean
* @throws \think\admin\Exception
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
private function createBackup(): bool
{
// 接收指定打包数据表
if ($this->input->hasOption('backup')) {
$tables = str2arr(strtr($this->input->getOption('backup'), '|', ','));
} elseif ($this->input->hasOption('all')) {
[$tables] = SystemService::getTables();
} else {
[$tables] = SystemService::getTables();
$tables = array_intersect($tables, Library::$sapp->config->get('phinx.backup', []));
}
// 去除忽略的数据表
$ignore = Library::$sapp->config->get('phinx.ignore', []);
if (empty($ignore)) $ignore = ['system_queue', 'system_oplog'];
$tables = array_unique(array_diff($tables, $ignore, ['migrations']));
// 创建数据库记录安装脚本
$this->setQueueMessage(4, 1, '开始创建数据包安装脚本!');
$phinx = PhinxExtend::create2backup($tables);
$target = syspath("database/migrations/{$phinx['file']}");
if (file_put_contents($target, $phinx['text']) !== false) {
$this->setQueueMessage(4, 2, '成功创建数据包安装脚本!');
return true;
} else {
$this->setQueueMessage(4, 2, '创建数据包安装脚本失败!');
return false;
}
}
}

View File

@ -0,0 +1,160 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\support\command;
use think\admin\extend\ToolsExtend;
use think\admin\service\ModuleService;
use think\admin\service\RuntimeService;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
/**
* 组件安装指令
* @class Publish
* @package think\admin\support\command
*/
class Publish extends Command
{
/**
* 任务参数配置
* @return void
*/
public function configure()
{
$this->setName('xadmin:publish');
$this->addOption('force', 'f', Option::VALUE_NONE, 'Overwrite any existing files');
$this->addOption('migrate', 'm', Option::VALUE_NONE, 'Execute phinx database script');
$this->setDescription('Publish Plugs and Config Assets for ThinkAdmin');
}
/**
* 任务合并执行
* @param \think\console\Input $input
* @param \think\console\Output $output
* @return null|void
*/
public function execute(Input $input, Output $output)
{
RuntimeService::clear(false);
$this->parse()->plugin()->output->writeln('<info>Succeed!</info>');
}
/**
* 安装数据库
* @return $this
*/
private function plugin(): Publish
{
// 执行子应用安装
$force = boolval($this->input->getOption('force'));
foreach (ModuleService::getModules() as $appName) {
$appPath = $this->app->getBasePath() . $appName;
is_dir($appPath) && $this->copy($appPath, $force);
}
// 执行数据库脚本
if ($this->input->getOption('migrate')) {
$this->app->console->call('migrate:run', [], 'console');
}
return $this;
}
/**
* 初始化组件文件
* @param string $copy 应用资源目录
* @param boolean $force 是否强制替换
* @return void
*/
private function copy(string $copy, bool $force = false)
{
// 复制系统配置文件
$frdir = rtrim($copy, '\\/') . DIRECTORY_SEPARATOR . 'config';
ToolsExtend::copyfile($frdir, syspath('config'), [], $force, false);
// 复制静态资料文件
$frdir = rtrim($copy, '\\/') . DIRECTORY_SEPARATOR . 'public';
ToolsExtend::copyfile($frdir, syspath('public'), [], true, false);
// 复制数据库脚本
$frdir = rtrim($copy, '\\/') . DIRECTORY_SEPARATOR . 'database';
ToolsExtend::copyfile($frdir, syspath('database/migrations'), [], $force, false);
}
/**
* 解析 json
* @return $this
*/
private function parse(): Publish
{
[$services, $versions] = [[], []];
if (is_file($file = syspath('vendor/composer/installed.json'))) {
$packages = json_decode(@file_get_contents($file), true);
foreach ($packages['packages'] ?? $packages as $package) {
// 生成组件版本
$type = $package['type'] ?? '';
$config = $package['extra']['config'] ?? [];
$versions[$package['name']] = [
'type' => $config['type'] ?? ($type === 'think-admin-plugin' ? 'plugin' : 'library'),
'name' => $config['name'] ?? ($package['name'] ?? ''),
'icon' => $config['icon'] ?? '',
'cover' => $config['cover'] ?? '',
'license' => (array)($config['license'] ?? ($package['license'] ?? [])),
'version' => $config['version'] ?? ($package['version'] ?? ''),
'homepage' => $config['homepage'] ?? ($package['homepage'] ?? ''),
'document' => $config['document'] ?? ($package['document'] ?? ''),
'platforms' => $config['platforms'] ?? [],
'description' => $config['description'] ?? ($package['description'] ?? ''),
];
// 生成服务配置
if (!empty($package['extra']['think']['services'])) {
$services = array_merge($services, (array)$package['extra']['think']['services']);
}
// 复制配置文件
if (!empty($package['extra']['think']['config'])) {
$configPath = $this->app->getConfigPath();
$installPath = syspath("vendor/{$package['name']}/");
foreach ((array)$package['extra']['think']['config'] as $name => $file) {
if (is_file($target = $configPath . $name . '.php')) {
$this->output->info("File {$target} exist!");
continue;
}
if (!is_file($source = $installPath . $file)) {
$this->output->info("File {$source} not exist!");
continue;
}
copy($source, $target);
}
}
}
}
// 写入服务配置
$header = "// Automatically Generated At: " . date('Y-m-d H:i:s') . PHP_EOL . 'declare(strict_types=1);';
$content = '<?php' . PHP_EOL . $header . PHP_EOL . 'return ' . var_export($services, true) . ';';
@file_put_contents(syspath('vendor/services.php'), $content);
// 写入组件版本
$content = '<?php' . PHP_EOL . $header . PHP_EOL . 'return ' . var_export($versions, true) . ';';
@file_put_contents(syspath('vendor/versions.php'), preg_replace('#\s+=>\s+array\s+\(#m', ' => array (', $content));
return $this;
}
}

View File

@ -0,0 +1,378 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\support\command;
use Error;
use Exception;
use Psr\Log\NullLogger;
use think\admin\Command;
use think\admin\model\SystemQueue;
use think\admin\service\QueueService;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use Throwable;
/**
* 异步任务管理指令
* @class Queue
* @package think\admin\support\command
*/
class Queue extends Command
{
/**
* 任务等待处理
* @var integer
*/
const STATE_WAIT = 1;
/**
* 任务正在处理
* @var integer
*/
const STATE_LOCK = 2;
/**
* 任务处理完成
* @var integer
*/
const STATE_DONE = 3;
/**
* 任务处理失败
* @var integer
*/
const STATE_ERROR = 4;
/**
* 监听进程指令
* @var string
*/
const QUEUE_LISTEN = 'xadmin:queue listen';
/**
* 当前任务编号
* @var string
*/
protected $code;
/**
* 指令任务配置
*/
public function configure()
{
$this->setName('xadmin:queue');
$this->addOption('host', '-H', Option::VALUE_OPTIONAL, 'The host of WebServer.');
$this->addOption('port', '-p', Option::VALUE_OPTIONAL, 'The port of WebServer.');
$this->addOption('daemon', 'd', Option::VALUE_NONE, 'The queue listen in daemon mode');
$this->addArgument('action', Argument::OPTIONAL, 'stop|start|status|query|listen|clean|dorun|webstop|webstart|webstatus', 'listen');
$this->addArgument('code', Argument::OPTIONAL, 'Taskcode');
$this->addArgument('spts', Argument::OPTIONAL, 'Separator');
$this->setDescription('Asynchronous Command Queue Task for ThinkAdmin');
}
/**
* 任务执行入口
* @param Input $input
* @param Output $output
* @return void
*/
protected function execute(Input $input, Output $output)
{
$action = $input->hasOption('daemon') ? 'start' : $input->getArgument('action');
if (method_exists($this, $method = "{$action}Action")) return $this->$method();
$this->output->error("># Wrong operation, Allow stop|start|status|query|listen|clean|dorun|webstop|webstart|webstatus");
}
/**
* 停止 WebServer 调试进程
*/
protected function webStopAction()
{
$root = syspath('public/');
if (count($result = $this->process->query("{$root} {$root}router.php")) < 1) {
$this->output->writeln("># There are no WebServer processes to stop");
} else foreach ($result as $item) {
$this->process->close(intval($item['pid']));
$this->output->writeln("># Successfully sent end signal to process {$item['pid']}");
}
}
/**
* 启动 WebServer 调试进程
*/
protected function webStartAction()
{
$prot = 'http';
$port = $this->input->getOption('port') ?: '80';
$host = $this->input->getOption('host') ?: '127.0.0.1';
$root = syspath('public' . DIRECTORY_SEPARATOR);
$command = "php -S {$host}:{$port} -t {$root} {$root}router.php";
$this->output->comment(">$ {$command}");
if (count($result = $this->process->query($command)) > 0) {
if ($this->process->isWin()) $this->process->exec("start {$prot}://{$host}:{$port}");
$this->output->writeln("># WebServer process already exist for pid {$result[0]['pid']}");
} else {
$this->process->create($command, 2000);
if (count($result = $this->process->query($command)) > 0) {
$this->output->writeln("># WebServer process started successfully for pid {$result[0]['pid']}");
if ($this->process->isWin()) $this->process->exec("start {$prot}://{$host}:{$port}");
} else {
$this->output->writeln('># WebServer process failed to start');
}
}
}
/**
* 查看 WebServer 调试进程
*/
protected function webStatusAction()
{
$root = syspath('public' . DIRECTORY_SEPARATOR);
if (count($result = $this->process->query("{$root} {$root}router.php")) > 0) {
$this->output->comment(">$ {$result[0]['cmd']}");
$this->output->writeln("># WebServer process {$result[0]['pid']} running");
} else {
$this->output->writeln("># The WebServer process is not running");
}
}
/**
* 停止所有任务
*/
protected function stopAction()
{
if (count($result = $this->process->thinkQuery('xadmin:queue')) < 1) {
$this->output->writeln("># There are no task processes to stop");
} else foreach ($result as $item) {
$this->process->close(intval($item['pid']));
$this->output->writeln("># Successfully sent end signal to process {$item['pid']}");
}
}
/**
* 启动后台任务
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
protected function startAction()
{
SystemQueue::mk()->count();
$this->output->comment(">$ {$this->process->think(static::QUEUE_LISTEN)}");
if (count($result = $this->process->thinkQuery(static::QUEUE_LISTEN)) > 0) {
if (is_file($lock = syspath('runtime/cache/time.queue')) && intval(file_get_contents($lock)) + 60 < time()) {
$this->output->writeln("># The task monitoring delay has exceeded 60 seconds, and the monitoring will be restarted.");
$this->process->close(intval($result[0]['pid'])) && $this->process->thinkExec(static::QUEUE_LISTEN, 1000);
} else {
$this->output->writeln("># Queue daemons already exist for pid {$result[0]['pid']}");
}
} else {
$this->process->thinkExec(static::QUEUE_LISTEN, 1000);
if (count($result = $this->process->thinkQuery(static::QUEUE_LISTEN)) > 0) {
$this->output->writeln("># Queue daemons started successfully for pid {$result[0]['pid']}");
} else {
$this->output->writeln("># Queue daemons failed to start");
}
}
}
/**
* 查询所有任务
*/
protected function queryAction()
{
$items = $this->process->thinkQuery('xadmin:queue');
if (count($items) > 0) foreach ($items as $item) {
$this->output->writeln("># {$item['pid']}\t{$item['cmd']}");
} else {
$this->output->writeln('># No related task process found');
}
}
/**
* 清理所有任务
* @throws \think\admin\Exception
* @throws \think\db\exception\DbException
*/
protected function cleanAction()
{
// 清理任务历史记录
$days = intval(sysconf('base.queue_clean_days|raw') ?: 7);
$clean = SystemQueue::mk()->where('exec_time', '<', time() - $days * 24 * 3600)->delete();
// 标记超过 1 小时未完成的任务为失败状态,循环任务失败重置
$map1 = [['loops_time', '>', 0], ['status', '=', static::STATE_ERROR]]; // 执行失败的循环任务
$map2 = [['exec_time', '<', time() - 3600], ['status', '=', static::STATE_LOCK]]; // 执行超时的任务
[$timeout, $loops, $total] = [0, 0, SystemQueue::mk()->whereOr([$map1, $map2])->count()];
foreach (SystemQueue::mk()->whereOr([$map1, $map2])->cursor() as $queue) {
$queue['loops_time'] > 0 ? $loops++ : $timeout++;
if ($queue['loops_time'] > 0) {
$this->queue->message($total, $timeout + $loops, "正在重置任务 {$queue['code']} 为运行");
[$status, $message] = [static::STATE_WAIT, $queue['status'] === static::STATE_ERROR ? '任务执行失败,已自动重置任务!' : '任务执行超时,已自动重置任务!'];
} else {
$this->queue->message($total, $timeout + $loops, "正在标记任务 {$queue['code']} 为超时");
[$status, $message] = [static::STATE_ERROR, '任务执行超时,已自动标识为失败!'];
}
$queue->save(['status' => $status, 'exec_desc' => $message]);
}
$this->setQueueSuccess("清理 {$clean} 条历史任务,关闭 {$timeout} 条超时任务,重置 {$loops} 条循环任务");
}
/**
* 查询兼听状态
*/
protected function statusAction()
{
if (count($result = $this->process->thinkQuery(static::QUEUE_LISTEN)) > 0) {
$this->output->writeln("Listening for main process {$result[0]['pid']} running");
} else {
$this->output->writeln("The Listening main process is not running");
}
}
/**
* 启动任务监听
* @return void
*/
protected function listenAction()
{
try {
set_time_limit(0) && PHP_SAPI !== 'cli' && ignore_user_abort(true);
$this->app->db->setLog(new NullLogger());
$this->createListenProcess();
} catch (Exception $exception) {
trace_file($exception) && usleep(3000000);
$this->output->write('=============== EXCEPTION ===============');
$this->output->write($exception->getMessage());
$this->output->writeln('=============== TRY-REBOOT ===============');
$this->createListenProcess();
}
}
/**
* 执行任务监听
* @return void
*/
private function createListenProcess()
{
$this->output->writeln("\n\tYou can exit with <info>`CTRL-C`</info>");
$this->output->writeln('=============== LISTENING ===============');
while (true) {
@file_put_contents(syspath('runtime/cache/time.queue'), strval(time()));
[$map, $start] = [[['status', '=', static::STATE_WAIT], ['exec_time', '<=', time()]], microtime(true)];
foreach (SystemQueue::mk()->where($map)->order('exec_time asc')->cursor() as $queue) try {
$args = "xadmin:queue dorun {$queue['code']} -";
$this->output->comment(">$ {$this->process->think($args)}");
if (count($this->process->thinkQuery($args)) > 0) {
$this->output->writeln("># Already in progress -> [{$queue['code']}] {$queue['title']}");
} else {
$this->process->thinkExec($args);
$this->output->writeln("># Created new process -> [{$queue['code']}] {$queue['title']}");
}
} catch (Exception $exception) {
$queue->save(['status' => static::STATE_ERROR, 'outer_time' => time(), 'exec_desc' => $exception->getMessage()]);
$this->output->error("># Execution failed -> [{$queue['code']}] {$queue['title']}{$exception->getMessage()}");
}
if (microtime(true) < $start + 1) usleep(1000000);
}
}
/**
* 执行指定任务
* @return void
* @throws \think\admin\Exception
*/
protected function doRunAction()
{
$this->code = trim($this->input->getArgument('code'));
if (empty($this->code)) {
$this->output->error('Task number needs to be specified for task execution');
} else try {
set_time_limit(0) && PHP_SAPI !== 'cli' && ignore_user_abort(true);
$this->queue->initialize($this->code);
if (empty($this->queue->record) || intval($this->queue->record->getAttr('status')) !== static::STATE_WAIT) {
// 这里不做任何处理(该任务可能在其它地方已经在执行)
$this->output->warning("The or status of task {$this->code} is abnormal");
} else {
// 锁定任务状态,防止任务再次被执行
SystemQueue::mk()->strict(false)->where(['code' => $this->code])->inc('attempts')->update([
'enter_time' => microtime(true), 'outer_time' => 0, 'exec_pid' => getmypid(), 'exec_desc' => '', 'status' => static::STATE_LOCK,
]);
$this->queue->progress(2, '>>> 任务处理开始 <<<', '0');
// 执行任务内容
defined('WorkQueueCall') or define('WorkQueueCall', true);
defined('WorkQueueCode') or define('WorkQueueCode', $this->code);
if (class_exists($command = $this->queue->record->getAttr('command'))) {
// 自定义任务,支持返回消息(支持异常结束,异常码可选择 3|4 设置任务状态)
/**@var \think\admin\Queue|QueueService $class */
$class = $this->app->make($command, [], true);
if ($class instanceof \think\admin\Queue) {
$this->updateQueue(static::STATE_DONE, $class->initialize($this->queue)->execute($this->queue->data) ?: '');
} elseif ($class instanceof QueueService) {
$this->updateQueue(static::STATE_DONE, $class->initialize($this->queue->code)->execute($this->queue->data) ?: '');
} else {
throw new \think\admin\Exception("自定义 {$command} 未继承 think\admin\Queue 或 think\admin\service\QueueService");
}
} else {
// 自定义指令,不支持返回消息(支持异常结束,异常码可选择 3|4 设置任务状态)
$attr = explode(' ', trim(preg_replace('|\s+|', ' ', $command)));
$this->updateQueue(static::STATE_DONE, $this->app->console->call(array_shift($attr), $attr)->fetch(), false);
}
}
} catch (Exception|Throwable|Error $exception) {
$isDone = intval($exception->getCode()) === static::STATE_DONE;
$this->updateQueue($isDone ? static::STATE_DONE : static::STATE_ERROR, $exception->getMessage());
}
}
/**
* 修改当前任务状态
* @param integer $status 任务状态
* @param string $message 消息内容
* @param boolean $isSplit 是否分隔
* @throws \think\admin\Exception
*/
private function updateQueue(int $status, string $message, bool $isSplit = true)
{
// 更新当前任务
$desc = $isSplit ? explode("\n", trim($message)) : [$message];
SystemQueue::mk()->strict(false)->where(['code' => $this->code])->update([
'status' => $status, 'outer_time' => microtime(true), 'exec_pid' => getmypid(), 'exec_desc' => $desc[0],
]);
$this->process->message($message);
// 任务进度标记
if (!empty($desc[0])) {
$this->queue->progress($status, ">>> {$desc[0]} <<<");
}
// 任务状态标记
if ($status === static::STATE_DONE) {
$this->queue->progress($status, '>>> 任务处理完成 <<<', '100.00');
} elseif ($status === static::STATE_ERROR) {
$this->queue->progress($status, '>>> 任务处理失败 <<<');
}
// 注册循环任务
if (($time = intval($this->queue->record->getAttr('loops_time'))) > 0) try {
$this->queue->initialize($this->code)->reset($time);
} catch (Exception|Throwable|Error $exception) {
$this->app->log->error("Queue {$this->queue->record->getAttr('code')} Loops Failed. {$exception->getMessage()}");
}
}
}

View File

@ -0,0 +1,78 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\support\command;
use think\admin\Command;
use think\admin\service\SystemService;
use think\console\Input;
use think\console\input\Argument;
use think\console\Output;
use think\helper\Str;
/**
* 数据库字符替换
* @class Replace
* @package think\admin\support\command
*/
class Replace extends Command
{
/**
* 指令任务配置
*/
protected function configure()
{
$this->setName('xadmin:replace');
$this->addArgument('search', Argument::OPTIONAL, '查找替换的字符内容', '');
$this->addArgument('replace', Argument::OPTIONAL, '目标替换的字符内容', '');
$this->setDescription('Database Character Field Replace for ThinkAdmin');
}
/**
* 任务执行入口
* @param \think\console\Input $input
* @param \think\console\Output $output
* @return void
* @throws \think\admin\Exception
* @throws \think\db\exception\DbException
*/
protected function execute(Input $input, Output $output)
{
$search = $input->getArgument('search');
$repalce = $input->getArgument('replace');
if ($search === '') $this->setQueueError('查找替换字符内容不能为空!');
if ($repalce === '') $this->setQueueError('目标替换字符内容不能为空!');
[$tables, $total, $count] = SystemService::getTables();
foreach ($tables as $table) {
$data = [];
$this->setQueueMessage($total, ++$count, sprintf("准备替换数据表 %s", Str::studly($table)));
foreach ($this->app->db->table($table)->getFields() as $field => $attrs) {
if (preg_match('/char|text/', $attrs['type'])) {
$data[$field] = $this->app->db->raw(sprintf('REPLACE(`%s`,"%s","%s")', $field, $search, $repalce));
}
}
if (count($data) > 0) {
$this->app->db->table($table)->master()->where('1=1')->update($data);
$this->setQueueMessage($total, $count, sprintf("成功替换数据表 %s", Str::studly($table)), 1);
} else {
$this->setQueueMessage($total, $count, sprintf("无需替换数据表 %s", Str::studly($table)), 1);
}
}
$this->setQueueSuccess("批量替换 {$total} 张数据表成功");
}
}

View File

@ -0,0 +1,88 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\support\command;
use think\admin\Command;
use think\admin\extend\DataExtend;
use think\admin\model\SystemMenu;
/**
* 重置并清理系统菜单
* @class Sysmenu
* @package think\admin\support\command
*/
class Sysmenu extends Command
{
/**
* 指令任务配置
*/
public function configure()
{
$this->setName('xadmin:sysmenu');
$this->setDescription('Clean and Reset System Menu Data for ThinkAdmin');
}
/**
* 任务执行入口
* @return void
* @throws \think\admin\Exception
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function handle()
{
$query = SystemMenu::mQuery()->where(['status' => 1]);
$menus = $query->db()->order('sort desc,id asc')->select()->toArray();
[$total, $count] = [count($menus), 0, $query->empty()];
$this->setQueueMessage($total, 0, '开始重置系统菜单编号...');
foreach (DataExtend::arr2tree($menus) as $sub1) {
$pid1 = $this->write($sub1);
$this->setQueueMessage($total, ++$count, "重写1级菜单{$sub1['title']}");
if (!empty($sub1['sub'])) foreach ($sub1['sub'] as $sub2) {
$pid2 = $this->write($sub2, $pid1);
$this->setQueueMessage($total, ++$count, "重写2级菜单-> {$sub2['title']}");
if (!empty($sub2['sub'])) foreach ($sub2['sub'] as $sub3) {
$this->write($sub3, $pid2);
$this->setQueueMessage($total, ++$count, "重写3级菜单-> -> {$sub3['title']}");
}
}
}
$this->setQueueMessage($total, $count, "完成重置系统菜单编号!");
}
/**
* 写入单项菜单数据
* @param array $arr 单项菜单数据
* @param mixed $pid 上级菜单编号
* @return int|string
*/
private function write(array $arr, $pid = 0)
{
return SystemMenu::mk()->insertGetId([
'pid' => $pid,
'url' => $arr['url'],
'icon' => $arr['icon'],
'node' => $arr['node'],
'title' => $arr['title'],
'params' => $arr['params'],
'target' => $arr['target'],
]);
}
}

View File

@ -0,0 +1,113 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\support\middleware;
use Closure;
use think\admin\Exception;
use think\admin\extend\JwtExtend;
use think\App;
use think\exception\HttpResponseException;
use think\Request;
use think\Response;
/**
* 兼容会话中间键
* @class JwtSession
* @package think\admin\support\middleware
*/
class JwtSession
{
/**
* 当前 App 对象
* @var \think\App
*/
protected $app;
/**
* 当前 Session 对象
* @var \think\Session
*/
protected $session;
/**
* Construct
* @param \think\App $app
*/
public function __construct(App $app)
{
$this->app = $app;
$this->session = $app->session;
}
/**
* 中间键处理
* @param \think\Request $request
* @param \Closure $next
* @return \think\Response
*/
public function handle(Request $request, Closure $next): Response
{
// 处理 Jwt 请求,请求头存在 jwt-token 字段
if (($token = $request->header('jwt-token', ''))) try {
if (preg_match('#^\s*([\w\-]+\.[\w\-]+\.[\w\-]+)\s*$#', $token, $match)) {
JwtExtend::verify($match[1]);
$sessionId = JwtExtend::$sessionId;
} else {
throw new Exception('令牌格式错误!', 401);
}
} catch (\Exception $exception) {
throw new HttpResponseException(json(['code' => $exception->getCode(), 'info' => lang($exception->getMessage())]));
}
if (empty($sessionId)) {
$varSessionId = $this->app->config->get('session.var_session_id');
if ($varSessionId && $request->request($varSessionId)) {
$sessionId = $request->request($varSessionId);
} else {
$sessionId = $request->cookie($this->session->getName());
}
}
if ($sessionId) {
$this->session->setId($sessionId);
}
// 基础 Session 初始化
$this->session->init();
$request->withSession($this->session);
if (empty(JwtExtend::$sessionId)) {
// 非 Jwt 会话需写入 Cookie 记录 SessionID
$this->app->cookie->set($this->session->getName(), $this->session->getId());
}
// 执行下一步控制器操作
return $next($request)->setSession($this->session);
}
/**
* 保存会话数据
* @return void
*/
public function end()
{
$this->session->save();
JwtExtend::$sessionId = '';
}
}

View File

@ -0,0 +1,189 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\support\middleware;
use Closure;
use think\admin\Plugin;
use think\admin\service\NodeService;
use think\admin\service\SystemService;
use think\App;
use think\exception\HttpException;
use think\Request;
use think\Response;
/**
* 多应用调度中间键
* @class MultAccess
* @package think\admin\support\middleware
*/
class MultAccess
{
/**
* 应用实例
* @var App
*/
private $app;
/**
* 应用路径
* @var string
*/
private $appPath;
/**
* 应用空间
* @var string
*/
private $appSpace;
/**
* App constructor.
* @param App $app
*/
public function __construct(App $app)
{
$this->app = $app;
}
/**
* 多应用解析
* @param Request $request
* @param Closure $next
* @return Response
*/
public function handle(Request $request, Closure $next): Response
{
[$this->appPath, $this->appSpace] = ['', ''];
if (!$this->parseMultiApp()) return $next($request);
return $this->app->middleware->pipeline('app')->send($request)->then(function ($request) use ($next) {
return $next($request);
});
}
/**
* 解析多应用
* @return bool
*/
private function parseMultiApp(): bool
{
$defaultApp = $this->app->config->get('route.default_app') ?: 'index';
[$script, $pathinfo] = [$this->scriptName(), $this->app->request->pathinfo()];
if ($script && !in_array($script, ['index', 'router', 'think'])) {
$this->app->request->setPathinfo(preg_replace("#^{$script}\.php(/|\.|$)#i", '', $pathinfo) ?: '/');
return $this->setMultiApp($script, true);
} else {
// 域名绑定处理
$domains = $this->app->config->get('app.domain_bind', []);
if (!empty($domains)) foreach ([$this->app->request->host(true), $this->app->request->subDomain(), '*'] as $key) {
if (isset($domains[$key])) return $this->setMultiApp($domains[$key], true);
}
$name = current(explode('/', $pathinfo));
if (strpos($name, '.')) $name = strstr($name, '.', true);
// 应用绑定与插件处理
$addons = Plugin::get();
$appmap = $this->app->config->get('app.app_map', []);
if (isset($appmap[$name])) {
$appName = $appmap[$name] instanceof Closure ? (call_user_func_array($appmap[$name], [$this->app]) ?: $name) : $appmap[$name];
} elseif ($name && (in_array($name, $appmap) || in_array($name, $this->app->config->get('app.deny_app_list', [])))) {
throw new HttpException(404, "app not exists: {$name}");
} elseif ($name && isset($appmap['*'])) {
$appName = $appmap['*'];
} else {
$appName = $name ?: $defaultApp;
if (!isset($addons[$appName]) && !is_dir($this->app->getBasePath() . $appName)) {
return $this->app->config->get('app.app_express', false) && $this->setMultiApp($defaultApp, false);
}
}
// 插件绑定处理
if (isset($addons[$appName])) {
[$this->appPath, $this->appSpace] = [$addons[$appName]['path'], $addons[$appName]['space']];
}
if ($name) {
$this->app->request->setRoot('/' . $name);
$this->app->request->setPathinfo(strpos($pathinfo, '/') ? ltrim(strstr($pathinfo, '/'), '/') : '');
}
}
return $this->setMultiApp($appName ?? $defaultApp, $this->app->http->isBind());
}
/**
* 获取当前运行入口名称
* @codeCoverageIgnore
* @return string
*/
private function scriptName(): string
{
$file = $_SERVER['SCRIPT_FILENAME'] ?? ($_SERVER['argv'][0] ?? '');
return empty($file) ? '' : pathinfo($file, PATHINFO_FILENAME);
}
/**
* 设置应用参数
* @param string $appName 应用名称
* @param boolean $appBind 应用绑定
* @return boolean
*/
private function setMultiApp(string $appName, bool $appBind): bool
{
if (is_dir($this->appPath = $this->appPath ?: syspath("app/{$appName}/"))) {
// 设置多应用模式
$this->app->setNamespace($this->appSpace ?: NodeService::space($appName))->setAppPath($this->appPath);
$this->app->http->setBind($appBind)->name($appName)->path($this->appPath)->setRoutePath($this->appPath . 'route' . DIRECTORY_SEPARATOR);
// 修改模板参数配置
$uris = array_merge($this->app->config->get('view.tpl_replace_string', []), SystemService::uris());
$this->app->config->set(['view_path' => $this->appPath . 'view' . DIRECTORY_SEPARATOR, 'tpl_replace_string' => $uris], 'view');
// 初始化多应用文件
return $this->loadMultiApp($this->appPath);
} else {
return false;
}
}
/**
* 加载应用文件
* @param string $appPath 应用路径
* @codeCoverageIgnore
* @return boolean
*/
private function loadMultiApp(string $appPath): bool
{
[$ext, $fmaps] = [$this->app->getConfigExt(), []];
if (is_file($file = "{$appPath}common{$ext}")) include_once $file;
foreach (glob($appPath . 'config' . DIRECTORY_SEPARATOR . '*' . $ext) as $file) {
$this->app->config->load($file, $fmaps[] = pathinfo($file, PATHINFO_FILENAME));
}
if (in_array('route', $fmaps) && method_exists($this->app->route, 'reload')) {
$this->app->route->reload();
}
if (is_file($file = "{$appPath}provider{$ext}")) {
$this->app->bind(include $file);
}
if (is_file($file = "{$appPath}event{$ext}")) {
$this->app->loadEvent(include $file);
}
if (is_file($file = "{$appPath}middleware{$ext}")) {
$this->app->middleware->import(include $file, 'app');
}
// 重新加载应用语言包
if (method_exists($this->app->lang, 'switchLangSet')) {
$this->app->lang->switchLangSet($this->app->lang->getLangSet());
}
return true;
}
}

View File

@ -0,0 +1,84 @@
<?php
// +----------------------------------------------------------------------
// | Library for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免费声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/zoujingli/ThinkLibrary
// | github 仓库地址 https://github.com/zoujingli/ThinkLibrary
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace think\admin\support\middleware;
use think\admin\service\AdminService;
use think\App;
use think\exception\HttpResponseException;
use think\Request;
use think\Response;
/**
* 后台权限中间键
* @class RbacAccess
* @package think\admin\support\middleware
*/
class RbacAccess
{
/**
* 当前 App 对象
* @var \think\App
*/
protected $app;
/**
* Construct
* @param \think\App $app
*/
public function __construct(App $app)
{
$this->app = $app;
}
/**
* 中间键处理
* @param \think\Request $request
* @param \Closure $next
* @return \think\Response
*/
public function handle(Request $request, \Closure $next): Response
{
// HTTP.LANG 语言包处理
$langSet = $this->app->lang->getLangSet();
if (is_file($file = dirname(__DIR__, 2) . "/lang/{$langSet}.php")) {
$this->app->lang->load($file, $langSet);
}
// 动态加载全局语言包
if (is_file($file = syspath("lang/{$langSet}.php"))) {
$this->app->lang->load($file, $langSet);
}
// 跳过忽略配置应用 或 有权限访问
$ignore = $this->app->config->get('app.rbac_ignore', []);
if (in_array($this->app->http->getName(), $ignore) || AdminService::check()) {
return $next($request);
}
// 无权限已登录,提示异常
if (AdminService::isLogin()) {
throw new HttpResponseException(json(['code' => 0, 'info' => lang('禁用访问!')]));
}
// 无权限未登录,跳转登录
$loginUrl = $this->app->config->get('app.rbac_login') ?: 'admin/login/index';
$loginPage = preg_match('#^(/|https?://)#', $loginUrl) ? $loginUrl : sysuri($loginUrl);
throw new HttpResponseException(json(['code' => 0, 'info' => lang('请重新登录!'), 'url' => $loginPage]));
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace think\admin\tests;
use PHPUnit\Framework\TestCase;
use think\admin\extend\CodeExtend;
class CodeTest extends TestCase
{
public function testUuidCreate()
{
$uuid = CodeExtend::uuid();
$this->assertNotEmpty(preg_match('|^[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}$|i', $uuid));
}
public function testEncode()
{
$value = '235215321351235123dasfdasfasdfas';
$encode = CodeExtend::encrypt($value, 'thinkadmin');
$this->assertEquals($value, CodeExtend::decrypt($encode, 'thinkadmin'), '验证加密解密');
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace think\admin\tests;
use PHPUnit\Framework\TestCase;
use think\admin\extend\JwtExtend;
class JwtTest extends TestCase
{
public function testJwtCreateAndVerify()
{
$jwtkey = 'thinkadmin';
$testdata = ['user' => 'admin' . mt_rand(0, 1000), 'iss' => 'thinkadmin.top', 'exp' => time() + 30];
$token = JwtExtend::token($testdata, $jwtkey);
$result = JwtExtend::verify($token, $jwtkey);
$this->assertEquals($testdata['user'], $result['user']);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace think\admin\tests;
use PHPUnit\Framework\TestCase;
use think\admin\model\SystemUser;
class ModelTest extends TestCase
{
public function testVirtualModel()
{
$this->assertEquals(m('SystemUser')->getTable(), SystemUser::mk()->getTable(), '动态模型测试');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace think\admin\tests;
use PHPUnit\Framework\TestCase;
class StorageTest extends TestCase
{
public function testInit()
{
$this->assertEquals(1, 1);
}
// public function testAlist()
// {
// $alist = AlistStorage::instance();
// $alist->set('test.tt', $content = uniqid());
// $this->assertEquals($alist->get('test.tt'), $content);
// }
}

View File

@ -0,0 +1,22 @@
<?php
use think\facade\Db;
include_once dirname(__DIR__) . '/vendor/autoload.php';
include_once dirname(__DIR__) . '/vendor/topthink/framework/src/helper.php';
Db::setConfig([
'default' => 'mysql',
'connections' => [
'mysql' => [
'type' => 'mysql',
'hostname' => '127.0.0.1',
'database' => 'admin_v6',
'username' => 'admin_v6',
'password' => 'FbYBHcWKr2',
'hostport' => '3306',
'charset' => 'utf8mb4',
'debug' => true,
],
],
]);