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