ThinkPHP 6.0 的环境要求如下:
- PHP >= 7.1.0
安装 Composer
如果还没有安装 Composer,在 Linux 和 Mac OS X 中可以运行如下命令:
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
在 Windows 中,你需要下载并运行 Composer-Setup.exe。Composer 文档(英文文档,中文文档)。
由于众所周知的原因,国外的网站连接速度很慢。建议使用国内镜像(阿里云)。
打开命令行窗口(windows用户)或控制台(Linux、Mac 用户)并执行如下命令:
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
创建 ThinkPHP 项目
如果你是第一次安装的话,在命令行窗口执行命令:
composer create-project topthink/think my-thinkphp-app
已经安装过的项目可以执行下面的命令进行更新:
composer update
开启调试模式
应用默认是部署模式,在开发阶段,可以修改环境变量APP_DEBUG开启调试模式,上线部署后切换到部署模式。
重命名项目默认创建的
.example.env文件为.env
.env 文件内容:
APP_DEBUG = true
[APP]
DEFAULT_TIMEZONE = Asia/Shanghai
[DATABASE]
TYPE = mysql
HOSTNAME = 127.0.0.1
DATABASE = db_test
USERNAME = root
PASSWORD = 123456
HOSTPORT = 3306
CHARSET = utf8mb4
DEBUG = true
[LANG]
default_lang = zh-cn
运行项目
在命令行界面执行下面到指令
php think run
在浏览器中输入地址:
http://localhost:8000/
会看到欢迎页面。恭喜你,现在已经完成 ThinkPHP6.0 的安装!
如果你本地80端口没有被占用的话,也可以直接使用
php think run -p 80
然后就可以直接访问:
http://localhost/
安装常用插件
# 安装 ThinkPHP 官方视图插件
composer require topthink/think-view
# 安装 ThinkPHP 官方验证码插件
composer require topthink/think-captcha
# 安装 JWT Token 插件
composer require firebase/php-jwt
# 安装 邮件 插件
composer require swiftmailer/swiftmailer
# 安装 linux 定时任务 插件
composer require dragonmantank/cron-expression
设置上传目录
修改文件 config/filesystem.php
<?php
return [
// 默认磁盘
\'default\' => env(\'filesystem.driver\', \'local\'),
// 磁盘列表
\'disks\' => [
\'local\' => [
\'type\' => \'local\',
\'root\' => app()->getRuntimePath() . \'storage\',
],
\'public\' => [
// 磁盘类型
\'type\' => \'local\',
// 磁盘路径
\'root\' => app()->getRootPath() . \'public/storage\',
// 磁盘路径对应的外部URL路径
\'url\' => \'/storage\',
// 可见性
\'visibility\' => \'public\',
],
// 更多的磁盘配置信息
// 网站上传目录,位置:public/uploads
\'uploads\' => [
// 磁盘类型
\'type\' => \'local\',
// 磁盘路径
\'root\' => app()->getRootPath() . \'public/uploads\',
// 磁盘路径对应的外部URL路径
\'url\' => \'/uploads\',
// 可见性
\'visibility\' => \'public\',
],
],
];
设置 Redis 缓存服务
修改文件 config/cache.php
<?php
// +----------------------------------------------------------------------
// | 缓存设置
// +----------------------------------------------------------------------
return [
// 默认缓存驱动
\'default\' => env(\'cache.driver\', \'file\'),
// 缓存连接方式配置
\'stores\' => [
\'file\' => [
// 驱动方式
\'type\' => \'File\',
// 缓存保存目录
\'path\' => \'\',
// 缓存前缀
\'prefix\' => \'\',
// 缓存有效期 0表示永久缓存
\'expire\' => 0,
// 缓存标签前缀
\'tag_prefix\' => \'tag:\',
// 序列化机制 例如 [\'serialize\', \'unserialize\']
\'serialize\' => [],
],
// 更多的缓存连接
// 应用数据(保存到 runtime/app_data 目录)
\'app_data\' => [
// 驱动方式
\'type\' => \'File\',
// 缓存保存目录
\'path\' => app()->getRuntimePath() . \'app_data\',
// 缓存前缀
\'prefix\' => \'\',
// 缓存有效期 0表示永久缓存
\'expire\' => 0,
// 缓存标签前缀
\'tag_prefix\' => \'tag:\',
// 序列化机制 例如 [\'serialize\', \'unserialize\']
\'serialize\' => [],
],
// redis 缓存(若配置此选项,需开启 redis 服务,否则启动会报错)
\'redis\' => [
// 驱动方式
\'type\' => \'redis\',
// 服务器地址
\'host\' => \'127.0.0.1\',
],
// session 缓存(使用 redis 保存 session 数据,需开启 redis 服务,否则启动会报错)
\'session\' => [
// 驱动方式
\'type\' => \'redis\',
// 服务器地址
\'host\' => \'127.0.0.1\',
// 缓存前缀
\'prefix\' => \'sess_\',
],
],
];
修改 Session 存储方式
修改文件:/config/session.php
<?php
// +----------------------------------------------------------------------
// | 会话设置
// +----------------------------------------------------------------------
return [
// session name
\'name\' => \'PHPSESSID\',
// SESSION_ID的提交变量,解决flash上传跨域
\'var_session_id\' => \'\',
// 驱动方式 支持file cache
// \'type\' => \'file\',
\'type\' => \'cache\',
// 存储连接标识 当type使用cache的时候有效
// \'store\' => null,
\'store\' => \'session\',
// 过期时间
// \'expire\' => 1440,
\'expire\' => 86400, // 24 小时
// 前缀
\'prefix\' => \'\',
];
修改路由配置文件,开启控制器后缀功能
修改文件:config/route.php
<?php
// +----------------------------------------------------------------------
// | 路由设置
// +----------------------------------------------------------------------
return [
// pathinfo分隔符
\'pathinfo_depr\' => \'/\',
// URL伪静态后缀
\'url_html_suffix\' => \'html\',
// URL普通方式参数 用于自动生成
\'url_common_param\' => true,
// 是否开启路由延迟解析
\'url_lazy_route\' => false,
// 是否强制使用路由
// \'url_route_must\' => false,
\'url_route_must\' => true,
// 合并路由规则
\'route_rule_merge\' => false,
// 路由是否完全匹配
\'route_complete_match\' => false,
// 访问控制器层名称
\'controller_layer\' => \'controller\',
// 空控制器名
\'empty_controller\' => \'Error\',
// 是否使用控制器后缀(若设为 true 值,则控制器需加后缀 Controller,例如:UserController.php)
\'controller_suffix\' => true,
// 默认的路由变量规则
\'default_route_pattern\' => \'[\w\.]+\',
// 是否开启请求缓存 true自动缓存 支持设置请求缓存规则
\'request_cache_key\' => false,
// 请求缓存有效期
\'request_cache_expire\' => null,
// 全局请求缓存排除规则
\'request_cache_except\' => [],
// 默认控制器名
\'default_controller\' => \'Index\',
// 默认操作名
\'default_action\' => \'index\',
// 操作方法后缀
\'action_suffix\' => \'\',
// 默认JSONP格式返回的处理方法
\'default_jsonp_handler\' => \'jsonpReturn\',
// 默认JSONP处理方法
\'var_jsonp_handler\' => \'callback\',
];
设置验证码
修改配置文件:config/captcha.php
<?php
// +----------------------------------------------------------------------
// | Captcha配置文件
// +----------------------------------------------------------------------
return [
//验证码位数
\'length\' => 4,
// 验证码字符集合
\'codeSet\' => \'2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY\',
// 验证码过期时间
\'expire\' => 1800,
// 是否使用中文验证码
\'useZh\' => false,
// 是否使用算术验证码
\'math\' => false,
// 是否使用背景图
\'useImgBg\' => false,
//验证码字符大小
// \'fontSize\' => 25,
\'fontSize\' => 30,
// 是否使用混淆曲线
\'useCurve\' => false,
//是否添加杂点
\'useNoise\' => false,
// 验证码字体 不设置则随机
\'fontttf\' => \'\',
//背景颜色
\'bg\' => [243, 251, 254],
// 验证码图片高度
\'imageH\' => 0,
// 验证码图片宽度
\'imageW\' => 0,
// 添加额外的验证码设置
// verify => [
// \'length\'=>4,
// ...
//],
];
开启 Session 中间件
文件:app/middleware.php
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
// \think\middleware\LoadLangPack::class,
// Session初始化
\think\middleware\SessionInit::class
];
通用助手类
<?php
/**
* 通用助手类
*/
declare(strict_types=1);
namespace app\helper;
use think\facade\Cache;
/**
* 通用 助手类
*/
class CommonHelper
{
/**
* 获取标准消息格式
* @param integer|boolean $status
* @param string $msg
* @param mixed $data
* @param integer $code
* @return array [\'status\',\'msg\',\'data\',\'code\']
*/
public static function stdmessage($status, $msg, $data = \'\', $code = 0)
{
return [
\'status\' => intval($status),
\'msg\' => $msg,
\'data\' => $data,
\'code\' => $code,
];
}
/**
* 生成参数签名
* @param array &$params 请求参数数组
* @param integer $appid 应用ID
* @param string $appkey 应用KEY
* @return string 返回sign参数签名字符串
*/
public static function makeParamSignature(&$params, $appid, $appkey)
{
// 过滤空数组
$params = array_filter($params);
// 加入时间戳参数
$params[\'timestamp\'] = time();
// 加入应用ID参数
$params[\'appid\'] = $appid;
// 加入应用Key参数
$params[\'appkey\'] = $appkey;
// 加入随机值参数
$params[\'nonce\'] = substr(uniqid(), 7);
// 数组按键值正序重新排序
ksort($params);
// 用md5加密重新串联成请求字符串的参数数组
$sign = md5(http_build_query($params));
// 截取中间16位作为签名
$sign = substr($sign, 8, 16);
// 删除appkey参数
unset($params[\'appkey\']);
// 加入签名参数
$params[\'sign\'] = $sign;
return $sign;
}
/**
* 验证参数签名
* @param array $params 请求参数数组,一般为 appid,nonce,sign,timestamp 四个就可以了
* @return array
*/
public static function validateParamSignature($params)
{
if (!is_array($params)) {
return [\'status\' => false, \'message\' => \'签名校验失败:请求参数错误\'];
}
$needKeys = [\'timestamp\', \'appid\', \'sign\', \'nonce\'];
foreach ($needKeys as $key) {
if (empty($params[$key])) {
return [\'status\' => false, \'message\' => \'签名校验失败:请求参数无效\'];
}
}
array_filter($params);
extract($params);
// 链接请求1分钟内使用有效
$invideTimeStamp = time() - 360;
if ($timestamp < $invideTimeStamp) {
return [\'status\' => false, \'message\' => \'签名校验失败:请求过期失效\'];
}
if ($appid == \'-1\') {
$appkey = \'a99AE2d2a736X65f5Ye63Ae299b0e339\';
} else {
$appkey = Cache::get(\'appid:\' . $appid); // 获取appkey
}
if (!$appkey) {
return [\'status\' => false, \'message\' => \'签名校验失败:应用未注册\'];
}
unset($params[\'sign\']);
$params[\'appkey\'] = $appkey;
ksort($params);
$servSign = substr(md5(http_build_query($params)), 8, 16);
if ($sign != $servSign) {
return [\'status\' => false, \'message\' => \'签名校验失败:签名无效 \' . $servSign];
}
return [\'status\' => true, \'message\' => \'签名校验成功:签名有效\'];
}
/**
* 密码加密
*/
public static function hashPassword($password, $salt = \'\')
{
// 截取中间16位作为签名
return substr(md5($password . $salt), 8, 16);
}
/**
* 获取不重复序列号
* 大约是原来长度的一半,比如12位生成6位,21位生成13位
*/
public static function hashSerial($prefix = \'\')
{
$time = date(\'y-m-d-h-i-s\');
if (is_numeric($prefix)) {
$time = chunk_split(strval($prefix), 2, \'-\') . $time;
$prefix = \'\';
}
$atime = explode(\'-\', $time);
foreach ($atime as $stime) {
$itime = $stime * 1;
if ($itime < 26) {
$prefix .= chr(65 + $itime);
continue;
}
if ($itime >= 48 && $itime <= 57) {
$prefix .= chr($stime);
continue;
}
$prefix .= $stime;
}
return $prefix;
}
/**
* 语义化时间
*
* @param integer|string $time 时间
* @param string $break 断点,超过断点以后的时间会直接以指定的日期格式显示
* @param string $format 日期格式, 与$break参数结合使用
* @param boolean $aliasable 是否允许以 昨天、前天 来代替 1 天前、2 天前
* @return string 返回语义化时间,例如:几秒,几分,几小时,几天前,几小时前,几月前 等
* @example
* humantime(strtotime(\'-5 month\'), \'month\') 返回 2019-10-27 17:50:17
* humantime(strtotime(\'-5 month\'), \'year\') 返回 5 个月前
* humantime(strtotime(\'yesterday\')) 返回 昨天
* humantime(strtotime(\'-2 day\')); 返回 前天
*/
public static function humantime($time, $break = \'\', $format = \'Y-m-d H:i:s\', $aliasable = true)
{
if (!$time) {
return \'\';
}
if (!is_numeric($time)) {
$time = strtotime($time);
}
$text = \'\';
$seconds = time() - $time;
if ($seconds > 0) {
$formater = array(
\'second\' => [\'time\' => \'1\', \'text\' => \'秒\'],
\'minute\' => [\'time\' => \'60\', \'text\' => \'分钟\'],
\'hour\' => [\'time\' => \'3600\', \'text\' => \'小时\'],
\'day\' => [\'time\' => \'86400\', \'text\' => \'天\', \'alias\' => [\'1\' => \'昨天\', \'2\' => \'前天\']],
\'week\' => [\'time\' => \'604800\', \'text\' => \'星期\'],
\'month\' => [\'time\' => \'2592000\', \'text\' => \'个月\'],
\'year\' => [\'time\' => \'31536000\', \'text\' => \'年\'],
);
$prevName = \'\';
foreach ($formater as $name => $data) {
if ($seconds < intval($data[\'time\'])) {
$prevData = $formater[$prevName];
$count = floor($seconds / intval($prevData[\'time\']));
if ($aliasable && isset($prevData[\'alias\']) && isset($prevData[\'alias\'][strval($count)])) {
$text = $prevData[\'alias\'][strval($count)];
break;
}
$text = $count . \' \' . $prevData[\'text\'] . \'前\';
break;
}
$prevName = $name;
if ($break && ($name == $break)) {
$text = date($format, $time);
break;
}
}
} else {
$text = date($format, $time);
}
return $text;
}
/**
* 解析字符串类型的 ID 值
* @param integer|string $id 以逗号隔开的编号值,例如:1,3,5
* @param string $separator 分割符号,默认是逗号
* @return integer|array 返回安全的数值
*/
public static function parseTextIds($id, $separator = \',\')
{
if (is_numeric($id)) {
return $id;
}
$ids = [];
$data = explode($separator, $id);
foreach ($data as $v) {
if (is_numeric($v)) {
$ids[] = intval($v);
}
}
return array_filter($ids);
}
/**
* 返回当前的毫秒时间戳
*/
public static function microtime()
{
return round(microtime(true) * 1000);
}
/**
* 字符串转二维数组
* 说明:如果是url请求字符串,可以通过原生方法 parse_str 和 http_build_query 来互相转换
* @param string $text 文本内容
* @param string $groupSeparator 组分隔符
* @param string $valueSeparator 值分隔符
* @return array 键值数组
* 示例:text2array(\'a=1;b=2\',\';\',\'=\')
*/
public static function text2array($text, $groupSeparator = "\n", $valueSeparator = \'=\')
{
$text = trim($text);
$data = [];
if (!$text) {
return $data;
}
$arr = array_filter(explode($groupSeparator, $text));
foreach ($arr as $row) {
$pair = explode($valueSeparator, $row, 2);
$data[trim($pair[0])] = trim($pair[1]);
}
return $data;
}
/**
* ver_export() 方法的现代风格版
*/
function varExport($var, $indent = "")
{
switch (gettype($var)) {
case "string":
return \'\\'\' . addcslashes($var, "\\\$\"\r\n\t\v\f") . \'\\'\';
case "array":
$indexed = array_keys($var) === range(0, count($var) - 1);
$r = [];
foreach ($var as $key => $value) {
$r[] = "$indent " . ($indexed ? "" : $this->varExport($key) . " => ") . $this->varExport($value, "$indent ");
}
return "[\n" . implode(",\n", $r) . "\n" . $indent . "]";
case "boolean":
return $var ? "TRUE" : "FALSE";
default:
return var_export($var, true);
}
}
}
JWT Token 插件的使用示例
<?php
/**
* JWT Token 助手类
*/
declare(strict_types=1);
namespace app\helper;
use \Firebase\JWT\JWT;
/**
* JSON Web Tokens 助手类
* https://jwt.io/
* https://github.com/firebase/php-jwt
* composer require firebase/php-jwt
*/
class JWTHelper
{
/**
* 加密
* @param string $iss jwt 签发者(网址或IP,例如:http://example.com)
* @param string $aud 接收 jwt 的一方(网址或IP,例如:http://example.com)
* @param integer $nbf 定义在什么时间之前,该 jwt 都是不可用的.(例如:strtotime(\'10 hours\'))
* @param array $extConf 扩展参数
* @param string $key 密钥
* @return string jwt-token 内容
* 使用方法:JWTHelper::encode(\'\', \'\', 0, [\'user_id\' => 1]);
*/
public static function encode($iss, $aud, $nbf, $extConf = [], $key = \'my-jwt-key\')
{
if (!$nbf) {
$nbf = strtotime(\'10 hours\');
}
// 载荷(存放有效信息的地方)
$payload = array(
// iss: jwt签发者(网址或IP,例如:http://example.com)
"iss" => $iss ?: $iss, request()->domain(),
// aud: 接收jwt的一方(网址或IP,例如:http://example.com)
"aud" => $aud,
// iat: jwt的签发时间
"iat" => time(),
// nbf: 定义在什么时间之前,该jwt都是不可用的.(例如:strtotime(\'10 hours\'))
"nbf" => $nbf,
);
if (!empty($extConf)) {
$payload = array_merge($payload, $extConf);
}
/**
* IMPORTANT:
* You must specify supported algorithms for your application. See
* https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
* for a list of spec-compliant algorithms.
*/
return JWT::encode($payload, $key);
}
/**
* 解密
* @param string $jwt jwt-token 字符串
* @param string $key 秘钥
* @return array 返回 payload 数组内容
* 使用方法:
* try {
* $tokenInfo = (array) JWTHelper::decode($token);
* $userId = intval($tokenInfo[\'user_id\']);
* } catch (\Exception $ex) {
* return $ex;
* }
*/
public static function decode($jwt, $key = \'my-jwt-key\')
{
JWT::$leeway = 36000; // 延迟10小时
try {
return JWT::decode($jwt, $key, array(\'HS256\'));
} catch (\Exception $ex) {
throw $ex;
}
}
}
Excel 插件的使用示例
<?php
/**
* Excel 文档数据处理助手类
*/
declare(strict_types=1);
namespace app\helper;
use InvalidArgumentException;
use PhpOffice\PhpSpreadsheet\IOFactory;
/**
* Excel 助手类
*/
class ExcelHelper
{
/**
* 导入excel文件
* @param string $filename excel文件路径
* @return array excel文件内容数组
*/
public static function importExcel($filename)
{
if ($filename) {
$filename = \'.\' . $filename;
}
if (!$filename || !file_exists($filename)) {
return CommonHelper::stdmessage(0, \'文件不存在! \' . $filename);
}
// 判断文件是什么格式
$fileExt = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($fileExt, [\'csv\', \'xls\', \'xlsx\'])) {
return CommonHelper::stdmessage(0, \'文件格式错误, 只支持csv,xls,xlsx格式的文件!\');
}
ini_set(\'max_execution_time\', \'0\');
try {
$spreadsheet = IOFactory::load($filename);
return CommonHelper::stdmessage(1, \'\', $spreadsheet->getActiveSheet()->toArray(null, true, true, true));
} catch (InvalidArgumentException $e) {
return CommonHelper::stdmessage(0, $e->getMessage());
}
}
}
Http 模拟请求助手类
<?php
/**
* HTTP 模拟请求助手类
*/
declare(strict_types=1);
namespace app\helper;
/**
* Http 模拟请求助手类
*/
class HttpHelper
{
/**
* Ping IP 是否可用
* 依赖:需要开启扩展 extension=sockets
*/
public static function ping($ip, $port = 80)
{
if (strpos($ip, \':\')) {
list($ip, $port) = explode(\':\', $ip);
$port = intval($port);
}
$socket = null;
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$socket = socket_create(AF_INET6, SOCK_STREAM, SOL_TCP);
} else if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
} else {
return false;
}
return socket_connect($socket, $ip, $port);
}
/**
* 发送一个POST请求
* @param string $url 请求URL(带Http的完整地址)
* @param array $params 请求参数
* @param array $options 扩展参数
* @return mixed|string
*/
public static function post($url, $params = [], $options = [])
{
$req = self::sendRequest($url, $params, \'POST\', $options);
return $req;
}
/**
* 发送一个GET请求
* @param string $url 请求URL
* @param array $params 请求参数
* @param array $options 扩展参数
* @return mixed|string
*/
public static function get($url, $params = [], $options = [])
{
$req = self::sendRequest($url, $params, \'GET\', $options);
return $req;
}
/**
* CURL发送Request请求,含POST和REQUEST
* @param string $url 请求的链接(带Http的完整地址)
* @param mixed $params 传递的参数
* @param string $method 请求的方法
* @param mixed $options CURL的参数
* @return array
*/
public static function sendRequest($url, $params = [], $method = \'POST\', $options = [])
{
$msgInfo = [
\'status\' => 0,
\'msg\' => \'\',
\'code\' => 0,
\'data\' => [],
];
if (!$url || 0 !== strpos($url, \'http\')) {
$msgInfo[\'msg\'] = \'URL地址无效:\' . $url;
return $msgInfo;
}
$method = strtoupper($method);
$protocol = substr($url, 0, 5);
$query_string = is_array($params) ? http_build_query($params) : $params;
$ch = curl_init();
$defaults = [];
if (\'GET\' == $method) {
$geturl = $query_string ? $url . (stripos($url, "?") !== false ? "&" : "?") . $query_string : $url;
$defaults[CURLOPT_URL] = $geturl;
} else {
$defaults[CURLOPT_URL] = $url;
if ($method == \'POST\') {
$defaults[CURLOPT_POST] = 1;
} else {
$defaults[CURLOPT_CUSTOMREQUEST] = $method;
}
$defaults[CURLOPT_POSTFIELDS] = $params;
}
$defaults[CURLOPT_HEADER] = false;
$defaults[CURLOPT_USERAGENT] = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.98 Safari/537.36";
$defaults[CURLOPT_FOLLOWLOCATION] = true;
$defaults[CURLOPT_RETURNTRANSFER] = true;
$defaults[CURLOPT_CONNECTTIMEOUT] = 3;
$defaults[CURLOPT_TIMEOUT] = 3;
// disable 100-continue
curl_setopt($ch, CURLOPT_HTTPHEADER, array(\'Expect:\'));
if (\'https\' == $protocol) {
$defaults[CURLOPT_SSL_VERIFYPEER] = false;
$defaults[CURLOPT_SSL_VERIFYHOST] = false;
}
curl_setopt_array($ch, (array)$options + $defaults);
$ret = curl_exec($ch);
$err = curl_error($ch);
if (false === $ret || !empty($err)) {
$errno = curl_errno($ch);
$info = curl_getinfo($ch);
curl_close($ch);
$msgInfo[\'msg\'] = $err;
$msgInfo[\'code\'] = $errno;
$msgInfo[\'data\'] = $info;
return $msgInfo;
}
curl_close($ch);
$msgInfo[\'status\'] = 1;
$msgInfo[\'data\'] = $ret;
return $msgInfo;
}
/**
* 异步发送一个请求
* @param string $url 请求的链接
* @param mixed $params 请求的参数
* @param string $method 请求的方法
* @return boolean TRUE
*/
public static function sendAsyncRequest($url, $params = [], $method = \'POST\')
{
$method = strtoupper($method);
$method = $method == \'POST\' ? \'POST\' : \'GET\';
//构造传递的参数
if (is_array($params)) {
$post_params = [];
foreach ($params as $k => &$v) {
if (is_array($v)) {
$v = implode(\',\', $v);
}
$post_params[] = $k . \'=\' . urlencode($v);
}
$post_string = implode(\'&\', $post_params);
} else {
$post_string = $params;
}
$parts = parse_url($url);
//构造查询的参数
if ($method == \'GET\' && $post_string) {
$parts[\'query\'] = isset($parts[\'query\']) ? $parts[\'query\'] . \'&\' . $post_string : $post_string;
$post_string = \'\';
}
$parts[\'query\'] = isset($parts[\'query\']) && $parts[\'query\'] ? \'?\' . $parts[\'query\'] : \'\';
//发送socket请求,获得连接句柄
$fp = fsockopen($parts[\'host\'], isset($parts[\'port\']) ? $parts[\'port\'] : 80, $errno, $errstr, 3);
if (!$fp) {
return false;
}
//设置超时时间
stream_set_timeout($fp, 3);
$out = "{$method} {$parts[\'path\']}{$parts[\'query\']} HTTP/1.1\r\n";
$out .= "Host: {$parts[\'host\']}\r\n";
$out .= "Content-Type: application/x-www-form-urlencoded\r\n";
$out .= "Content-Length: " . strlen($post_string) . "\r\n";
$out .= "Connection: Close\r\n\r\n";
if ($post_string !== \'\') {
$out .= $post_string;
}
fwrite($fp, $out);
//不用关心服务器返回结果
//echo fread($fp, 1024);
fclose($fp);
return true;
}
/**
* 发送文件到客户端
* @param string $file
* @param bool $delaftersend
* @param bool $exitaftersend
*/
public static function sendToBrowser($file, $delaftersend = true, $exitaftersend = true)
{
if (file_exists($file) && is_readable($file)) {
header(\'Content-Description: File Transfer\');
header(\'Content-Type: application/octet-stream\');
header(\'Content-Disposition: attachment;filename = \' . basename($file));
header(\'Content-Transfer-Encoding: binary\');
header(\'Expires: 0\');
header(\'Cache-Control: must-revalidate, post-check = 0, pre-check = 0\');
header(\'Pragma: public\');
header(\'Content-Length: \' . filesize($file));
ob_clean();
flush();
readfile($file);
if ($delaftersend) {
unlink($file);
}
if ($exitaftersend) {
exit;
}
}
}
}
邮件 插件的使用示例
<?php
/**
* 邮件 助手类
*/
declare(strict_types=1);
namespace app\helper;
use Swift_Mailer;
use Swift_Message;
use Swift_SmtpTransport;
use think\facade\Config;
/**
* 邮件 助手类
*/
class MailHelper
{
protected $user = \'\';
protected $password = \'\';
protected $host = \'\';
protected $port = 25;
protected $fromEmail = [];
public function initByConfig()
{
$smtpInfo=Config::get(\'site.email.smtp\');
if(!isset($smtpInfo[\'password\'])){
return;
}
$this->user = $smtpInfo[\'username\'];
$this->password = $smtpInfo[\'password\'];
$this->host = $smtpInfo[\'host\'];
$this->port = $smtpInfo[\'port\'];
}
public function setSmtp($user, $password, $host, $port = 25)
{
$this->user = $user;
$this->password = $password;
$this->host = $host;
$this->port = $port;
}
public function send($subject, $body, $toEmail, $fromEmail=\'\')
{
if ($fromEmail) {
$this->fromEmail = $fromEmail;
} else {
$fromEmail = $this->fromEmail;
}
if (!$fromEmail || !$toEmail || !$subject) {
return CommonHelper::stdmessage(0, \'参数无效\');
}
// Create the Transport
$transport = (new Swift_SmtpTransport($this->host, $this->port))
->setUsername($this->user)
->setPassword($this->password);
// Create the Mailer using your created Transport
$mailer = new Swift_Mailer($transport);
// Create a message
$message = (new Swift_Message($subject))
->setFrom($fromEmail)
->setTo($toEmail)
->setBody($body);
// Send the message
$result = $mailer->send($message);
return CommonHelper::stdmessage($result, $result ? \'\' : \'发送失败\');
}
}
图片处理助手类
<?php
/**
* 图片处理 助手类
*/
declare(strict_types=1);
namespace app\helper;
/**
* 图片处理助手类
*/
class ImageHelper
{
// 常用文件大小字节常量
const SIZE_50KB = 51200;
const SIZE_200KB = 204800;
const SIZE_500KB = 512000;
const SIZE_1MB = 1048576;
const SIZE_2MB = 2097152;
const IMAGE_MIME = [\'image/gif\', \'image/jpg\', \'image/jpeg\', \'image/bmp\', \'image/png\', \'image/webp\'];
const IMAGE_EXT = [\'gif\', \'jpg\', \'jpeg\', \'bmp\', \'png\', \'webp\'];
/**
* 获取图像类型
* 返回图像常量值(1=gif,2=jpeg,3=png,6=bmp),否则返回 false。
*/
public static function getImageType($fileName)
{
if (function_exists(\'exif_imagetype\')) {
// 返回对应的常量,否则返回 FALSE。
// 详见:https://www.php.net/manual/zh/function.exif-imagetype.php
return exif_imagetype($fileName);
}
try {
// 获取图像大小及相关信息,成功返回一个数组(宽度、高度、类型常量、宽高属性、颜色位数、通道值、 MIME信息),失败则返回 FALSE
// 详见:https://www.php.net/manual/zh/function.getimagesize.php
// 警告:getimagesize存在上传漏洞。需要额外的条件检查,比如文件大小、扩展名、文件类型,并设置上传目录不允许执行PHP文件。
$info = getimagesize($fileName);
return $info ? $info[2] : false;
} catch (\Exception $e) {
return false;
}
}
public static function checkImageMime($mimeType)
{
return in_array($mimeType, self::IMAGE_MIME);
}
public static function checkImageExt($ext)
{
return in_array($ext, self::IMAGE_EXT);
}
public static function checkWidthAndHeight($fileName, $imgWidth, $imgHeight)
{
try {
// 获取图像大小及相关信息,成功返回一个数组(宽度、高度、类型常量、宽高属性、颜色位数、通道值、 MIME信息),失败则返回 FALSE
// 详见:https://www.php.net/manual/zh/function.getimagesize.php
// 警告:getimagesize存在上传漏洞。需要额外的条件检查,比如文件大小、扩展名、文件类型,并设置上传目录不允许执行PHP文件。
$imgInfo = getimagesize($fileName);
if (!$imgInfo || !isset($imgInfo[2])) {
return \'文件不是有效的图像\';
}
if (($imgWidth && $imgWidth != $imgInfo[0]) || ($imgHeight && $imgHeight != $imgInfo[1])) {
return "图片尺寸 [宽度{$imgInfo[0]}, 高度{$imgInfo[1]}] 不符合规范 [宽度{$imgWidth}, 高度{$imgHeight}]";
}
return true;
} catch (\Exception $e) {
return $e->getMessage();
}
}
}
文件上传控制器类
<?php
/**
* 文件上传 控制器类
*/
declare(strict_types=1);
namespace app\controller\admin;
use think\Request;
use app\helper\CommonHelper;
use app\helper\ImageHelper;
/**
* 文件上传控制器
*/
class UploaderController
{
/**
* 保存上传的文件(根据is_multiple参数自动识别多文件和单文件上传)
*/
public function save(Request $request)
{
// 禁止上传 PHP 和 HTML 文件
$forbiddenMimes = [\'text/x-php\', \'text/html\', \'text/css\', \'text/javascript\', \'text/x-shellscript\', \'application/x-javascript\'];
$forbiddenExts = [\'php\', \'html\', \'htm\', \'js\', \'css\'];
// 获取表单参数
$imgWidth = $request->param(\'imgwidth/d\', 0);
$imgHeight = $request->param(\'imgheight/d\', 0);
$isMultipleFile = $request->param(\'is_multiple/d\', 0);
$allfiles = $request->file();
// 单文件:[\'file\'=>[\'originalName\'=>\'xxx.png\', \'mimeType\'=>\'image/png\', \'error\'=>0, ...]]
// 多文件:[\'file\'=>[0=>[\'originalName\'=>\'xxx.png\', \'mimeType\'=>\'image/png\', \'error\'=>0, ...]]]
// 如果是单文件,先封装成多文件数据格式
$firstFileKey = key($allfiles);
if (gettype(current($allfiles)) == \'object\') {
$allfiles[$firstFileKey] = [$allfiles[$firstFileKey]];
}
// 自动识别是否多文件上传
if (!$isMultipleFile && count($allfiles) > 1) {
$isMultipleFile = 1;
}
$returnInfo = [];
foreach ($allfiles as $files) {
foreach ($files as $file) {
$fileInfo = [
\'mime\' => $file->getMime(),
\'ext\' => $file->getExtension(),
\'file_name\' => $file->getOriginalName(),
\'size\' => $file->getSize(), //文件大小,单位字节
\'tmp_name\' => $file->getPathname(), // 全路径
];
// 文件大小校验(2MB)
if ($fileInfo[\'size\'] > 2097152) {
$returnInfo[] = CommonHelper::stdmessage(0, \'文件大小超过最大上传限制\', $fileInfo[\'file_name\']);
continue;
}
if (in_array($fileInfo[\'mime\'], $forbiddenMimes) || in_array($fileInfo[\'ext\'], $forbiddenExts)) {
$returnInfo[] = CommonHelper::stdmessage(0, \'文件类型不允许上传\', $fileInfo[\'file_name\']);
continue;
}
//验证是否为图片文件
if (ImageHelper::checkImageMime($fileInfo[\'mime\'])) {
$message = ImageHelper::checkWidthAndHeight($fileInfo[\'tmp_name\'], $imgWidth, $imgHeight);
if (true !== $message) {
$returnInfo[] = CommonHelper::stdmessage(0, $message, $fileInfo[\'file_name\']);
continue;
}
}
// 上传到本地服务器(\'https://picsum.photos/200/300\')
$saveName = \think\facade\Filesystem::disk(\'uploads\')->putFile(date(\'Y\'), $file);
$fileName = \'/uploads/\' . str_replace(\'\\\', \'/\', $saveName);
$returnInfo[] = CommonHelper::stdmessage(1, $fileInfo[\'file_name\'], $fileName);
}
}
if ($isMultipleFile) {
return json(CommonHelper::stdmessage(1, \'\', $returnInfo));
} else {
return json(current($returnInfo));
}
}
/**
* 删除指定资源
*
* @param int $id
* @return \think\Response
*/
public function delete(Request $request)
{
$filePath = $request->param(\'filePath\');
return json(CommonHelper::stdmessage(1, \'\', [\'file\' => $filePath]));
}
/**
* PHP 原生上传处理
*/
public function orgupload()
{
$ofile = $_FILES[\'file\'];
if (!$ofile) {
output_json(stdmessage(0, \'未选择任何文件\'));
}
if ($ofile["error"] > 0) {
output_json(stdmessage(0, $ofile["error"]));
}
$fileInfo = [
// 上传文件名
\'file_name\' => $ofile["name"],
// 文件类型
\'file_type\' => $ofile["type"],
// 文件大小
\'file_size\' => $ofile["size"],
// 文件临时存储的位置
\'tmp_name\' => $ofile["tmp_name"],
// 扩展名
\'file_ext\' => pathinfo($ofile[\'name\'], PATHINFO_EXTENSION),
];
// 验证文件后缀
if (!in_array($fileInfo[\'file_ext\'], ["gif", "jpeg", "jpg", "png"])) {
output_json(stdmessage(0, \'不是有效的图形文件\', $fileInfo[\'file_ext\']));
}
// 验证文件类型
if (0 !== strpos($fileInfo[\'file_type\'], \'image/\')) {
output_json(stdmessage(0, \'不是有效的图形文件\', $fileInfo[\'file_type\']));
}
// 验证文件大小
$maxFileSize = 1024 * 1024 * 2;
if ($fileInfo[\'file_size\'] > $maxFileSize) {
output_json(stdmessage(0, \'文件尺寸超过 2 MB\'));
}
$dir = \'./files/\';
$saveFileName = $dir . md5($fileInfo[\'name\']) . \'.\' . $fileInfo[\'file_ext\'];
// 尝试自动创建目录
if (!is_dir($dir) && !@mkdir($dir, 0777)) {
output_json(stdmessage(0, \'创建目录失败\'));
}
// 如果 upload 目录不存在该文件则将文件上传到 upload 目录下
move_uploaded_file($ofile["tmp_name"], $saveFileName);
output_json(stdmessage(1, \'\', $saveFileName));
// ===============通用函数库=============
function stdmessage($code, $msg, $data = \'\')
{
return [\'code\' => $code, \'msg\' => $msg, \'data\' => $data];
}
function output_json($msgInfo)
{
var_export($msgInfo);
exit;
header(\'Content-Type:application/json; charset=utf-8\');
echo json_encode($msgInfo);
exit;
}
}
}
管理员控制器代码示例
<?php
declare(strict_types=1);
namespace app\controller\admin;
use think\Request;
use think\facade\View;
use app\model\pedm_auth\AdminModel;
use app\helper\CommonHelper;
use app\helper\StringHelper;
/**
* 管理员 控制器
*
*/
class AdminController extends BaseController
{
/**
* 显示资源列表页
*/
public function index()
{
return View::fetch();
}
/**
* 显示创建资源表单页.
*/
public function create()
{
return View::fetch(\'\');
}
/**
* 显示指定的资源
*/
public function profile()
{
return View::fetch(\'\');
}
/**
* 显示编辑资源表单页.
*/
public function edit()
{
return View::fetch(\'\');
}
/**
* 获取资源列表
*/
public function list(Request $request)
{
$searchText = $request->param(\'search_text\');
$pageSize = $this->getPageSize();
if ($searchText) {
$list = AdminModel::where(\'id\', intval($searchText))->paginate($pageSize);
} else {
$list = AdminModel::paginate($pageSize);
}
return json(CommonHelper::stdmessage(1, \'\', $list->append([\'status_text\', \'sex_text\'])));
}
/**
* 获取一条资源
*/
public function read(Request $request)
{
$id = $request->param(\'id/d\', 0);
if ($id > 0) {
$model = AdminModel::find($id);
} else {
$model = new AdminModel();
$model->sex = 0;
$model->status = 1;
}
$viewData = [
\'data\' => $model,
\'sex_data\' => $model->getSexData(),
\'status_data\' => $model->getStatusData(),
];
return json($viewData);
}
/**
* 保存新建的资源
*/
public function save(Request $request)
{
if (!$request->isPost()) {
return json(CommonHelper::stdmessage(0, \'非法请求\'));
}
// 表单校验(alphaDash: 字母和数字,下划线_及破折号-)
$validate = \think\facade\Validate::rule([
\'user_name|用户名\' => \'require|max:60\',
\'password|密码\' => \'require|max:32\',
\'avatar|头像\' => \'max:200\',
\'email|邮件\' => \'email|max:100\',
\'mobile|手机号\' => \'max:11\',
]);
$postData = $request->param();
if (!$validate->check($postData)) {
return json(CommonHelper::stdmessage(0, $validate->getError()));
}
// 表单数据补全
$postData[\'register_ip\'] = $request->ip();
if (isset($postData[\'password\'])) {
$postData[\'password\']=trim($postData[\'password\']);
if ($postData[\'password\'] == \'\') {
unset($postData[\'password\']);
} else {
$postData[\'salt\'] = StringHelper::newAlpha(6);
$postData[\'password\'] = CommonHelper::hashPassword($postData[\'password\'], $postData[\'salt\']);
}
}
// 保存到数据库
$resultInfo = [];
try {
AdminModel::create($postData);
$resultInfo = CommonHelper::stdmessage(1, \'\');
} catch (\Exception $e) {
// 数据库操作失败 输出错误信息
$resultInfo = CommonHelper::stdmessage(0, $e->getMessage());
}
return json($resultInfo);
}
/**
* 保存更新的资源
*/
public function update(Request $request)
{
if (!$request->isPost()) {
return json(CommonHelper::stdmessage(0, \'非法请求\'));
}
// 表单校验(alphaDash: 字母和数字,下划线_及破折号-)
$validate = \think\facade\Validate::rule([
\'id\' => \'require|number\',
\'nick_name|昵称\' => \'max:60\',
\'avatar|头像\' => \'max:200\',
\'email|邮件\' => \'email|max:100\',
\'mobile|手机号\' => \'max:11\',
]);
$postData = $request->param();
if (!$validate->check($postData)) {
return json(CommonHelper::stdmessage(0, $validate->getError()));
}
// 表单数据处理
if (isset($postData[\'password\'])) {
$postData[\'password\']=trim($postData[\'password\']);
if ($postData[\'password\'] == \'\') {
unset($postData[\'password\']);
} else {
$postData[\'salt\'] = StringHelper::newAlpha(6);
$postData[\'password\'] = CommonHelper::hashPassword($postData[\'password\'], $postData[\'salt\']);
}
}
$id = $postData[\'id\'];
unset($postData[\'id\']);
// 保存到数据库
$resultInfo = [];
try {
AdminModel::where(\'id\', $id)->update($postData);
$resultInfo = CommonHelper::stdmessage(1, \'\');
} catch (\Exception $e) {
// 数据库操作失败 输出错误信息
$resultInfo = CommonHelper::stdmessage(0, $e->getMessage());
}
return json($resultInfo);
}
/**
* 删除指定资源
*/
public function delete($id)
{
$id = CommonHelper::parseTextIds($id);
if ($id) {
$result = AdminModel::destroy($id);
} else {
$result = false;
}
return json(CommonHelper::stdmessage($result ? 1 : 0, \'\'));
}
/**
* 检测资源是否存在
*/
public function checkExists(Request $request)
{
$name = $request->param(\'name\');
$id = 0;
if ($name) {
$id = AdminModel::where(\'user_name\', $name)->value(\'id\');
}
if ($id) {
return json(CommonHelper::stdmessage(1, \'\', $id));
} else {
return json(CommonHelper::stdmessage(0, \'查无记录\'));
}
}
}
管理员 模型类代码示例
<?php
declare(strict_types=1);
namespace app\model;
use think\Model;
use app\helper\MailHelper;
use app\helper\StringHelper;
use app\helper\CommonHelper;
/**
* 权限管理员模型类
*/
class AdminModel extends Model
{
// 设置当前模型对应的完整数据表名称
protected $table = \'tbl_admin\';
// 自动时间戳
protected $autoWriteTimestamp = \'int\';
// 定义时间戳字段名
protected $createTime = \'created_at\';
protected $updateTime = \'updated_at\';
/**
* status 字段内容
*/
protected $status_data = [\'无效\', \'有效\'];
/**
* 返回 status 字段内容
*/
public function getStatusData()
{
return $this->status_data;
}
/**
* 返回 status 字段获取器的值
*/
public function getStatusTextAttr($value)
{
$value = intval($this->data[\'status\']);
return isset($this->status_data[$value]) ? $this->status_data[$value] : $value;
}
/**
* 返回 login_time 字段获取器的值
*/
public function getLoginTimeTextAttr()
{
return date(\'Y-m-d H:i:s\', $this->login_time);
}
/**
* 登录操作
*/
public static function login($account, $password, $ip, $field = \'*\')
{
if (!$account || !$password) {
return CommonHelper::stdmessage(0, \'账号和密码是必填项\');
}
// 登录查询字段
$model = self::where(\'user_name|mobile|email\', $account)->field($field)->find();
if (!$model) {
return CommonHelper::stdmessage(0, \'用户不存在\');
}
if (!$model->status) {
return CommonHelper::stdmessage(0, \'用户已被锁定\');
}
$hashPassowrd = CommonHelper::hashPassword($password, $model->salt);
if ($hashPassowrd != $model->password) {
return CommonHelper::stdmessage(0, \'密码错误\');
}
// 记录登录时间和IP
self::where(\'id\', $model->id)->inc(\'login_count\', 1)->update([\'login_time\' => time(), \'login_ip\' => $ip]);
return CommonHelper::stdmessage(1, \'\', $model->toArray());
}
/**
* 重置密码
*/
public static function resetPassword($email)
{
$model = self::where(\'email\', $email)->field(\'user_name, salt\');
if (!$model) {
return CommonHelper::stdmessage(0, \'用户不存在\');
}
$password = StringHelper::newAlphaNum(8);
$model->salt = StringHelper::newAlpha(6);
$model->password = CommonHelper::hashPassword($password, $model->salt);
$model->save();
$mailModel = new MailHelper();
$mailModel->initByConfig();
$msgInfo = $mailModel->send(\'密码重置\', "尊敬的{$model->user_name},<p>您的密码已被重置为 {$password},请尽快登录网站修改您的新密码。</p>", $email);
return $msgInfo;
}
/**
* 添加一条记录
*/
public static function createRecord($userName, $password, $ip, $roleName = \'\')
{
$id = self::where(\'user_name\', $userName)->value(\'id\');
if ($id) {
return CommonHelper::stdmessage(0, \'账号已存在\');
}
$salt = StringHelper::newAlpha(6);
$data = [
\'user_name\' => $userName,
\'password\' => CommonHelper::hashPassword($password, $salt),
\'salt\' => $salt,
\'role_name\' => $roleName,
\'register_ip\' => $ip,
\'status\' => 1,
];
$model = self::create($data);
if ($model) {
return CommonHelper::stdmessage(1, \'创建成功\', $model->id);
} else {
return CommonHelper::stdmessage(0, \'创建失败\');
}
}
}
定时任务功能示例
<?php
/**
* 定时任务 命令类
*/
declare(strict_types=1);
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use think\facade\Cache;
use app\model\CrontabModel;
use app\model\AutoTaskModel;
/**
* 定时任务 命令类
* 主要功能:定时执行SQL;定时请求项目URL或外部URL;定时清空缓存
* 说明:此功能不支持 Windows 系统,需要结合 Linux 的 Crontab 才可以正常使用,可以定时执行一系列的操作。
* 准备工作:Linux 下使用 crontab -e -u [用户名] 添加一条记录(这里的用户名是指 Apache 或 Nginx 的执行用户,一般为 www 或 nginx)
* 命令示例:
* 命令:crontab -e -u www (以 www 用户编辑 crontab 文件)
* 粘帖:* * * * * /usr/bin/php /www/yoursite/think autotask > /dev/null 2>&1 &
* 命令:systemctl restart crond.service
* 命令:crontab -l -u www
*/
class AutoTask extends Command
{
protected function configure()
{
// 指令配置
$this->setName(\'autotask\')
->setDescription(\'the autotask command\');
}
protected function execute(Input $input, Output $output)
{
// 指令输出
$output->writeln(\'autotask\');
file_put_contents(runtime_path() . \'auto_task.log\', date(\'Y-m-d H:i:s\') . PHP_EOL, FILE_APPEND);
AutoTaskModel::run();
}
}