ThinkPHP 6.x 搭建简单的通用项目示例

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();
    }
}
posted on 2021-02-06 22:07  sochishun  阅读(2004)  评论(0编辑  收藏  举报