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();
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM