作者 谢百川

七牛云配置

<?php
namespace addons\qiniu;
use addons\qiniu\library\Auth;
use fast\Http;
use think\Addons;
/**
* 七牛上传插件
*/
class Qiniu extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
return true;
}
/**
* 上传初始化时
*/
public function uploadConfigInit(&$upload)
{
$config = $this->getConfig();
$policy = array(
'saveKey' => ltrim($config['savekey'], '/'),
);
//如果启用服务端回调
if ($config['notifyenabled']) {
$policy = array_merge($policy, [
'callbackUrl' => $config['notifyurl'],
'callbackBody' => 'filename=$(fname)&key=$(key)&imageInfo=$(imageInfo)&filesize=$(fsize)&admin=$(x:admin)&user=$(x:user)'
]);
}
if ($config['uploadmode'] == 'client') {
$auth = new Auth($config['app_key'], $config['secret_key']);
$multipart['token'] = $auth->uploadToken($config['bucket'], null, $config['expire'], $policy);
$multipart['x:admin'] = (int)session('admin.id');
$multipart['x:user'] = (int)cookie('uid');
$upload = [
'cdnurl' => $config['cdnurl'],
'uploadurl' => $config['uploadurl'],
'bucket' => $config['bucket'],
'maxsize' => $config['maxsize'],
'mimetype' => $config['mimetype'],
'multipart' => $multipart,
'multiple' => $config['multiple'] ? true : false,
];
} else {
$upload = array_merge($upload, [
'cdnurl' => $config['cdnurl'],
'uploadurl' => addon_url('qiniu/index/upload'),
'maxsize' => $config['maxsize'],
'mimetype' => $config['mimetype'],
'multiple' => $config['multiple'] ? true : false,
]);
}
}
/**
* 附件删除后
*/
public function uploadDelete($attachment)
{
$config = $this->getConfig();
if ($attachment['storage'] == 'qiniu' && isset($config['syncdelete']) && $config['syncdelete']) {
$auth = new Auth($config['app_key'], $config['secret_key']);
$entry = $config['bucket'] . ':' . ltrim($attachment->url, '/');
$encodedEntryURI = $auth->base64_urlSafeEncode($entry);
$url = 'http://rs.qiniu.com/delete/' . $encodedEntryURI;
$headers = $auth->authorization($url);
//删除云储存文件
$ret = Http::sendRequest($url, [], 'POST', [CURLOPT_HTTPHEADER => ['Authorization: ' . $headers['Authorization']]]);
//如果是服务端中转,还需要删除本地文件
if ($config['uploadmode'] == 'server') {
$filePath = ROOT_PATH . 'public' . str_replace('/', DS, $attachment->url);
if ($filePath) {
@unlink($filePath);
}
}
}
return true;
}
}
... ...
//修改上传的接口调用
require(['upload'], function (Upload) {
var _onUploadResponse = Upload.events.onUploadResponse;
Upload.events.onUploadResponse = function (response) {
try {
var ret = typeof response === 'object' ? response : JSON.parse(response);
if (ret.hasOwnProperty("code") && ret.hasOwnProperty("data")) {
return _onUploadResponse.call(this, response);
} else if (ret.hasOwnProperty("key") && !ret.hasOwnProperty("err_code")) {
ret.code = 1;
ret.data = {
url: '/' + ret.key
};
return _onUploadResponse.call(this, JSON.stringify(ret));
}
} catch (e) {
}
return _onUploadResponse.call(this, response);
};
});
\ No newline at end of file
... ...
<?php
return array (
0 =>
array (
'name' => 'app_key',
'title' => 'app_key',
'type' => 'string',
'content' =>
array (
),
'value' => 'Amzxi3GqgGXNFcFx206ZjERD8Io4wBX9pheZlu29',
'rule' => 'required',
'msg' => '',
'tip' => '请在个人中心 > 密钥管理中获取 > AK',
'ok' => '',
'extend' => '',
),
1 =>
array (
'name' => 'secret_key',
'title' => 'secret_key',
'type' => 'string',
'content' =>
array (
),
'value' => 'td90DlFRiYWgJbckeclgwfNiYCDTj62-nm-rIXBb',
'rule' => 'required',
'msg' => '',
'tip' => '请在个人中心 > 密钥管理中获取 > SK',
'ok' => '',
'extend' => '',
),
2 =>
array (
'name' => 'bucket',
'title' => 'bucket',
'type' => 'string',
'content' =>
array (
),
'value' => 'seniors-say',
'rule' => 'required',
'msg' => '',
'tip' => '存储空间名称',
'ok' => '',
'extend' => '',
),
3 =>
array (
'name' => 'uploadurl',
'title' => '上传接口地址',
'type' => 'select',
'content' =>
array (
'https://upload-z0.qiniup.com' => '华东 https://upload-z0.qiniup.com',
'https://upload-z1.qiniup.com' => '华北 https://upload-z1.qiniup.com',
'https://upload-z2.qiniup.com' => '华南 https://upload-z2.qiniup.com',
'https://upload-na0.qiniup.com' => '北美 https://upload-na0.qiniup.com',
'https://upload-as0.qiniup.com' => '东南亚 https://upload-as0.qiniup.com',
),
'value' => 'https://upload-z0.qiniup.com',
'rule' => 'required',
'msg' => '',
'tip' => '推荐选择最近的地址',
'ok' => '',
'extend' => '',
),
4 =>
array (
'name' => 'cdnurl',
'title' => 'CDN地址',
'type' => 'string',
'content' =>
array (
),
'value' => 'http://q8teucqsl.bkt.clouddn.com',
'rule' => 'required',
'msg' => '',
'tip' => '未绑定CDN的话可使用七牛分配的测试域名',
'ok' => '',
'extend' => '',
),
5 =>
array (
'name' => 'notifyenabled',
'title' => '启用服务端回调',
'type' => 'bool',
'content' =>
array (
),
'value' => '0',
'rule' => '',
'msg' => '',
'tip' => '本地开发请禁用服务端回调',
'ok' => '',
'extend' => '',
),
6 =>
array (
'name' => 'notifyurl',
'title' => '回调通知地址',
'type' => 'string',
'content' =>
array (
),
'value' => 'http://www.yoursite.com/addons/qiniu/index/notify',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
7 =>
array (
'name' => 'uploadmode',
'title' => '上传模式',
'type' => 'select',
'content' =>
array (
'client' => '客户端直传(速度快,无备份)',
'server' => '服务器中转(占用服务器带宽,有备份)',
),
'value' => 'server',
'rule' => '',
'msg' => '',
'tip' => '启用服务器中转时务必配置操作员和密码',
'ok' => '',
'extend' => '',
),
8 =>
array (
'name' => 'savekey',
'title' => '保存文件名',
'type' => 'string',
'content' =>
array (
),
'value' => '/uploads/$(year)$(mon)$(day)/$(etag)$(ext)',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
9 =>
array (
'name' => 'expire',
'title' => '上传有效时长',
'type' => 'string',
'content' =>
array (
),
'value' => '600',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
10 =>
array (
'name' => 'maxsize',
'title' => '最大可上传',
'type' => 'string',
'content' =>
array (
),
'value' => '10M',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
11 =>
array (
'name' => 'mimetype',
'title' => '可上传后缀格式',
'type' => 'string',
'content' =>
array (
),
'value' => 'jpg,png,bmp,jpeg,gif,zip,rar,xls,xlsx',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
12 =>
array (
'name' => 'multiple',
'title' => '多文件上传',
'type' => 'bool',
'content' =>
array (
),
'value' => '0',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
13 =>
array (
'name' => 'syncdelete',
'title' => '附件删除时是否同步删除文件',
'type' => 'bool',
'content' =>
array (
),
'value' => '0',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
14 =>
array (
'name' => '__tips__',
'title' => '温馨提示',
'type' => '',
'content' =>
array (
),
'value' => '在使用之前请注册七牛账号并进行认证,注册链接:<a href="https://portal.qiniu.com/signup?code=3l79xtos9w9qq" target="_blank">https://portal.qiniu.com/signup?code=3l79xtos9w9qq</a>',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
);
... ...
<?php
namespace addons\qiniu\controller;
use addons\qiniu\library\Auth;
use app\common\model\Attachment;
use think\addons\Controller;
use think\Config;
/**
* 七牛管理
*
*/
class Index extends Controller
{
public function index()
{
$this->error("当前插件暂无前台页面");
}
/**
* 上传接口
*/
public function upload()
{
Config::set('default_return_type', 'json');
if (!session('admin') && !$this->auth->id) {
$this->error("请登录后再进行操作");
}
$config = get_addon_config('qiniu');
$file = $this->request->file('file');
if (!$file || !$file->isValid()) {
$this->error("请上传有效的文件");
}
$fileInfo = $file->getInfo();
$filePath = $file->getRealPath() ?: $file->getPathname();
preg_match('/(\d+)(\w+)/', $config['maxsize'], $matches);
$type = strtolower($matches[2]);
$typeDict = ['b' => 0, 'k' => 1, 'kb' => 1, 'm' => 2, 'mb' => 2, 'gb' => 3, 'g' => 3];
$size = (int)$config['maxsize'] * pow(1024, isset($typeDict[$type]) ? $typeDict[$type] : 0);
$suffix = strtolower(pathinfo($fileInfo['name'], PATHINFO_EXTENSION));
$suffix = $suffix ? $suffix : 'file';
$md5 = md5_file($filePath);
$search = ['$(year)', '$(mon)', '$(day)', '$(etag)', '$(ext)'];
$replace = [date("Y"), date("m"), date("d"), $md5, '.' . $suffix];
$object = ltrim(str_replace($search, $replace, $config['savekey']), '/');
$mimetypeArr = explode(',', strtolower($config['mimetype']));
$typeArr = explode('/', $fileInfo['type']);
//检查文件大小
if (!$file->checkSize($size)) {
$this->error("起过最大可上传文件限制");
}
//验证文件后缀
if ($config['mimetype'] !== '*' &&
(
!in_array($suffix, $mimetypeArr)
|| (stripos($typeArr[0] . '/', $config['mimetype']) !== false && (!in_array($fileInfo['type'], $mimetypeArr) && !in_array($typeArr[0] . '/*', $mimetypeArr)))
)
) {
$this->error(__('上传格式限制'));
}
$savekey = '/' . $object;
$uploadDir = substr($savekey, 0, strripos($savekey, '/') + 1);
$fileName = substr($savekey, strripos($savekey, '/') + 1);
//先上传到本地
$splInfo = $file->move(ROOT_PATH . '/public' . $uploadDir, $fileName);
if ($splInfo) {
$extparam = $this->request->post();
$filePath = $splInfo->getRealPath() ?: $splInfo->getPathname();
$sha1 = sha1_file($filePath);
$imagewidth = $imageheight = 0;
if (in_array($suffix, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf'])) {
$imgInfo = getimagesize($splInfo->getPathname());
$imagewidth = isset($imgInfo[0]) ? $imgInfo[0] : $imagewidth;
$imageheight = isset($imgInfo[1]) ? $imgInfo[1] : $imageheight;
}
$params = array(
'admin_id' => session('admin.id'),
'user_id' => $this->auth->id,
'filesize' => $fileInfo['size'],
'imagewidth' => $imagewidth,
'imageheight' => $imageheight,
'imagetype' => $suffix,
'imageframes' => 0,
'mimetype' => $fileInfo['type'],
'url' => $uploadDir . $splInfo->getSaveName(),
'uploadtime' => time(),
'storage' => 'local',
'sha1' => $sha1,
'extparam' => json_encode($extparam),
);
$attachment = Attachment::create(array_filter($params), true);
$policy = array(
'saveKey' => ltrim($savekey, '/'),
);
$auth = new Auth($config['app_key'], $config['secret_key']);
$token = $auth->uploadToken($config['bucket'], null, $config['expire'], $policy);
$multipart = [
['name' => 'token', 'contents' => $token],
[
'name' => 'file',
'contents' => fopen($filePath, 'r'),
'filename' => $fileName,
]
];
try {
$client = new \GuzzleHttp\Client();
$res = $client->request('POST', $config['uploadurl'], [
'multipart' => $multipart
]);
$code = $res->getStatusCode();
//成功不做任何操作
} catch (\GuzzleHttp\Exception\ClientException $e) {
$attachment->delete();
unlink($filePath);
$this->error("上传失败");
}
$url = '/' . $object;
//上传成功后将存储变更为qiniu
$attachment->storage = 'qiniu';
$attachment->save();
$this->success("上传成功", null, ['url' => $url]);
} else {
$this->error('上传失败');
}
return;
}
/**
* 通知回调
*/
public function notify()
{
$config = get_addon_config('qiniu');
$auth = new Auth($config['app_key'], $config['secret_key']);
$contentType = 'application/x-www-form-urlencoded';
$authorization = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] : '';
if (!$authorization && function_exists('apache_request_headers')) {
$headers = apache_request_headers();
$authorization = isset($headers['Authorization']) ? $headers['Authorization'] : '';
}
$url = $config['notifyurl'];
$body = file_get_contents('php://input');
$ret = $auth->verifyCallback($contentType, $authorization, $url, $body);
if ($ret) {
parse_str($body, $arr);
$admin_id = isset($arr['admin']) ? $arr['admin'] : 0;
$user_id = isset($arr['user']) ? $arr['user'] : 0;
$imageInfo = json_decode($arr['imageInfo'], true);
$params = array(
'admin_id' => (int)$admin_id,
'user_id' => (int)$user_id,
'filesize' => $arr['filesize'],
'imagewidth' => isset($imageInfo['width']) ? $imageInfo['width'] : 0,
'imageheight' => isset($imageInfo['height']) ? $imageInfo['height'] : 0,
'imagetype' => isset($imageInfo['format']) ? $imageInfo['format'] : '',
'imageframes' => 1,
'mimetype' => "image/" . (isset($imageInfo['format']) ? $imageInfo['format'] : ''),
'extparam' => '',
'url' => '/' . $arr['key'],
'uploadtime' => time(),
'storage' => 'qiniu'
);
Attachment::create($params);
return json(['ret' => 'success', 'code' => 1, 'data' => ['url' => $params['url']]]);
}
return json(['ret' => 'failed']);
}
}
... ...
name = qiniu
title = 七牛上传
intro = 使用七牛云存储,上传时直传七牛
author = Karson
website = https://www.fastadmin.net
version = 1.0.5
state = 1
url = /addons/qiniu.html
... ...
<?php
namespace addons\qiniu\library;
final class Auth
{
private $accessKey;
private $secretKey;
public function __construct($accessKey, $secretKey)
{
$this->accessKey = $accessKey;
$this->secretKey = $secretKey;
}
public function getAccessKey()
{
return $this->accessKey;
}
public function sign($data)
{
$hmac = hash_hmac('sha1', $data, $this->secretKey, true);
return $this->accessKey . ':' . $this->base64_urlSafeEncode($hmac);
}
public function signWithData($data)
{
$encodedData = $this->base64_urlSafeEncode($data);
return $this->sign($encodedData) . ':' . $encodedData;
}
public function signRequest($urlString, $body, $contentType = null)
{
$url = parse_url($urlString);
$data = '';
if (array_key_exists('path', $url)) {
$data = $url['path'];
}
if (array_key_exists('query', $url)) {
$data .= '?' . $url['query'];
}
$data .= "\n";
if ($body !== null && $contentType === 'application/x-www-form-urlencoded') {
$data .= $body;
}
return $this->sign($data);
}
public function verifyCallback($contentType, $originAuthorization, $url, $body)
{
$authorization = 'QBox ' . $this->signRequest($url, $body, $contentType);
return $originAuthorization === $authorization;
}
public function privateDownloadUrl($baseUrl, $expires = 3600)
{
$deadline = time() + $expires;
$pos = strpos($baseUrl, '?');
if ($pos !== false) {
$baseUrl .= '&e=';
} else {
$baseUrl .= '?e=';
}
$baseUrl .= $deadline;
$token = $this->sign($baseUrl);
return "$baseUrl&token=$token";
}
public function uploadToken($bucket, $key = null, $expires = 3600, $policy = null, $strictPolicy = true)
{
$deadline = time() + $expires;
$scope = $bucket;
if ($key !== null) {
$scope .= ':' . $key;
}
$args = self::copyPolicy($args, $policy, $strictPolicy);
$args['scope'] = $scope;
$args['deadline'] = $deadline;
$b = json_encode($args);
return $this->signWithData($b);
}
/**
* 上传策略,参数规格详见
* http://developer.qiniu.com/docs/v6/api/reference/security/put-policy.html
*/
private static $policyFields = array(
'callbackUrl',
'callbackBody',
'callbackHost',
'callbackBodyType',
'callbackFetchKey',
'returnUrl',
'returnBody',
'endUser',
'saveKey',
'insertOnly',
'detectMime',
'mimeLimit',
'fsizeMin',
'fsizeLimit',
'persistentOps',
'persistentNotifyUrl',
'persistentPipeline',
'deleteAfterDays',
'fileType',
'upHosts',
);
private static function copyPolicy(&$policy, $originPolicy, $strictPolicy)
{
if ($originPolicy === null) {
return array();
}
foreach ($originPolicy as $key => $value) {
if (!$strictPolicy || in_array((string)$key, self::$policyFields, true)) {
$policy[$key] = $value;
}
}
return $policy;
}
public function authorization($url, $body = null, $contentType = null)
{
$authorization = 'QBox ' . $this->signRequest($url, $body, $contentType);
return array('Authorization' => $authorization);
}
/**
* 对提供的数据进行urlsafe的base64编码。
*
* @param string $data 待编码的数据,一般为字符串
*
* @return string 编码后的字符串
* @link http://developer.qiniu.com/docs/v6/api/overview/appendix.html#urlsafe-base64
*/
function base64_urlSafeEncode($data)
{
$find = array('+', '/');
$replace = array('-', '_');
return str_replace($find, $replace, base64_encode($data));
}
/**
* 对提供的urlsafe的base64编码的数据进行解码
*
* @param string $str 待解码的数据,一般为字符串
*
* @return string 解码后的字符串
*/
function base64_urlSafeDecode($str)
{
$find = array('-', '_');
$replace = array('+', '/');
return base64_decode(str_replace($find, $replace, $str));
}
}
... ...
... ... @@ -572,4 +572,54 @@ class Student extends Api
$this->success('获取我喜欢的学长成功', $likeStudentList);
}
/**
* 修改头像或姓名
* @ApiTitle (修改头像或姓名)
* @ApiSummary (修改头像或姓名)
* @ApiMethod (POST)
* @ApiHeaders (name="token", type="string", required=true, description="请求的Token")
* @ApiParams (name="nickname", type="string", required=true, description="姓名")
* @ApiParams (name="head_image", type="string", required=true, description="头像")
* @ApiReturnParams (name="code", type="integer", required=true, sample="0")
* @ApiReturnParams (name="msg", type="string", required=true, sample="返回成功")
* @ApiReturnParams (name="data", type="object", sample="{'user_id':'int','user_name':'string','profile':{'email':'string','age':'integer'}}", description="扩展数据返回")
* @ApiReturn ({
"code": 0,
"msg": "需要注册",
"time": "1586307199",
"data": null
})
*/
public function updateStudentInfo()
{
$userId = $this->auth->id;
$student = new StudentModel();
$studentInfo = $student->infoByUserId($userId);
if(!$studentInfo) {
$this->error('还没有注册');
}
$nickname = $this->request->param('nickname', '', 'string');
if(!$nickname || mb_strlen($nickname) > 255) {
$this->error('请填写您的姓名');
}
$headImage = $this->request->param('head_image', '', 'string');
if(!$headImage) {
$this->error('请您上传一张头像');
}
$pathInfo = pathinfo($headImage);
$imageExtArr = ['jpg', 'jpeg', 'png'];
if(!isset($pathInfo['extension']) || !in_array($pathInfo['extension'], $imageExtArr)) {
$this->error('请选择正确的图片格式');
}
$res = $student->updateOne($userId, ['nickname' => $nickname, 'head_image' => $headImage]);
if(!$res) {
$this->error('对不起,您的信息修改失败');
}
$this->success('修改成功', []);
}
}
\ No newline at end of file
... ...
... ... @@ -39,23 +39,12 @@ class Student extends Model
return $this->where(['university_id' => ['=', $universityId], 'show_switch' => ['=', 1], 'school_id' => ['in', $schoolIds]])->useSoftDelete($this->deleteTime)->count();
}
// 根据用户ID获取一个可以展示的学生的数据
public function infoByUserIdCanShow($userId)
{
return $this->where(['user_id' => ['=', $userId], 'show_switch' => ['=', 1]])->useSoftDelete($this->deleteTime)->find();
}
// 获取高中同校的本校学长
public function listByUniversityIdAndSchoolId($universityId, $schoolId, $page, $size) {
return $this
->where(['university_id' => ['=', $universityId], 'show_switch' => ['=', 1], 'school_id' => ['=', $schoolId]])
->useSoftDelete($this->deleteTime)
->page($page, $size)
->order($this->createTime, 'desc')
->select();
}
// 获取高中同城/省的本校学长
public function listByUniversityIdAndSchoolIds($where, $page, $size)
{
... ... @@ -72,4 +61,17 @@ class Student extends Model
{
return $this->where(['id' => ['=', $id]])->update($data);
}
// 修改一群学生的数据
public function updateByIds($userIds, $data)
{
return $this->where(['user_id' => ['in', $userIds]])->update($data);
}
// 根据条件查询
public function listByWhere($where)
{
return $this->where($where)->select();
}
}
\ No newline at end of file
... ...
<?php
/**
* Created by PhpStorm.
* User: DELL
* Date: 2019/5/31
* Time: 9:25
*/
namespace app\command;
use app\api\model\Student;
use think\console\Input;
use think\console\Output;
use think\console\Command;
class CloseVip extends Command{
protected function configure()
{
$this->setName('CloseVip')->setDescription('关闭vip已经TimeOut的vip用户, 每1分钟执行一次');
}
protected function execute(Input $input, Output $output)
{
$student = new Student();
$where['vip_endtime'] = ['<', time()];
$studentList = $student->listByWhere($where);
$userIds = [];
foreach ($studentList as $v) {
$userIds[] = $v['user_id'];
}
if($userIds) {
$student->updateByIds($userIds, ['vip_level' => 0, 'vip_endtime' => null, 'updatetime' => time()]);
}
}
}
\ No newline at end of file
... ...
... ... @@ -8,6 +8,14 @@ return array (
array (
0 => 'epay',
),
'upload_config_init' =>
array (
0 => 'qiniu',
),
'upload_delete' =>
array (
0 => 'qiniu',
),
),
'route' =>
array (
... ...
... ... @@ -12,4 +12,25 @@ define([], function () {
}
});
//修改上传的接口调用
require(['upload'], function (Upload) {
var _onUploadResponse = Upload.events.onUploadResponse;
Upload.events.onUploadResponse = function (response) {
try {
var ret = typeof response === 'object' ? response : JSON.parse(response);
if (ret.hasOwnProperty("code") && ret.hasOwnProperty("data")) {
return _onUploadResponse.call(this, response);
} else if (ret.hasOwnProperty("key") && !ret.hasOwnProperty("err_code")) {
ret.code = 1;
ret.data = {
url: '/' + ret.key
};
return _onUploadResponse.call(this, JSON.stringify(ret));
}
} catch (e) {
}
return _onUploadResponse.call(this, response);
};
});
});
\ No newline at end of file
... ...