用 PHP 实现 Excel 的 stdevp 函数
在 Excel 中,stdevp
是计算样本总体标准偏差的函数,它反映了相对于平均值的离散程度。但在 PHP 里是没有该函数的,要计算标准偏差时,只能自己进行写算法,十分不便。于是查询相关资料和公式,总结出了以下代码。
在 Excel 中,stdevp
是计算样本总体标准偏差的函数,它反映了相对于平均值的离散程度。但在 PHP 里是没有该函数的,要计算标准偏差时,只能自己进行写算法,十分不便。于是查询相关资料和公式,总结出了以下代码。
在自己电脑(Windows)开发测试代码都没问题,但一上生产环境就报错了。经过对比,本机和服务器的PHP版本和OpenSSL版本不一样,猜测可能是这个原因导致的。经过一番查找,找到了从代码上解决问题的办法,规避了调整生产服务器的风险。
报错的代码
/**
* 字符串加密(加密方法:DES-ECB)
* @param string $data 待加密字符串
* @param string $key 对称加密密钥
* @return string
*/
function encryptData(string $data, string $key)
{
// 获取密码iv长度
$length = openssl_cipher_iv_length('DES-ECB');
// 生成一个伪随机字节串
$iv = openssl_random_pseudo_bytes($length);
// 加密数据
$ciphertext = openssl_encrypt($data, 'DES-ECB', $key, OPENSSL_RAW_DATA, $iv);
// 把包含数据的二进制字符串转换为十六进制值,然后返回结果
return bin2hex($ciphertext);
}
报错信息
Fatal error: Uncaught Error: Length must be greater than 0 in frame.php:87
Stack trace:
#0 frame.php(87): openssl_random_pseudo_bytes()
#1 frame.php(60): encryptData()
#2 {main} thrown in frame.php on line 87
根据报错信息,主要问题是openssl_cipher_iv_length()
返回的长度为0
,而openssl_random_pseudo_bytes()
的参数的长度必须大于0
,所以就产生了报错。
但是openssl_cipher_iv_length()
为什么返回0
呢?难道是不支持DES-ECB
加密方法?
使用openssl_get_cipher_methods()
方法获取可用的加密算法的列表,发现DES-ECB
在列表内,那应该是支持的!
这时候,我就猜想是不是openssl低版本的BUG,因为以前也见过一些因为openssl版本过低导致的问题,于是继续Google一番。
经过查找,发现了两条有用的结果:
在第一条中得知ECB 加密模式是不安全的,因为它没有初始化矢量
,openssl_cipher_iv_length()
返回的长度为0
的原因就得知了。
但结论没有经过仔细论证。也不知道为什么java为什么要ECB。
在第二条中找到了调整代码的思路。
最终得到了以下没有报错的代码~
/**
* 字符串加密(加密方法:DES-ECB)
* @param string $data 待加密字符串
* @param string $key 对称加密密钥
* @return string
*/
function encryptData(string $data, string $key)
{
// 获取密码iv长度
$length = openssl_cipher_iv_length('DES-ECB');
// 生成一个伪随机字节串
if ($length > 0) {
$iv = openssl_random_pseudo_bytes($length);
} else {
$iv = '';
}
// 加密数据
$ciphertext = openssl_encrypt($data, 'DES-ECB', $key, OPENSSL_RAW_DATA, $iv);
// 把包含数据的二进制字符串转换为十六进制值,然后返回结果
return bin2hex($ciphertext);
}
在制作项目中,难免会遇到有跨域问题,需要增加指定响应头来满足跨域的需求。但ThinkPHP5.1版本的手册中,对跨域怎么设置提供的方法比较局限,所以这里经过研究,总结出了几种办法,推荐使用第三种。
这方法是手册当中介绍的,这里简单复制下,深入了解可以查看手册。
如果某个路由或者分组需要支持跨域请求,可以使用
Route::get('new/:id', 'News/read')->ext('html')->allowCrossDomain();
Route::group('index', function() {
Route::get('new/:id', 'News/read');
})->prefix('index/')->allowCrossDomain();
这方法,仅适合定义了路由的情况下使用,如果是默认路由,这方法不适用。
可以在入口文件index.php
、公共函数文件common.php
等文件里使用header()
函数定义跨域响应头。代码如下:
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE');
header('Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With');
header('Access-Control-Allow-Origin: *');
全局允许跨域的话,在入口文件或全局公共函数文件里增加跨域代码
单模块允许跨域的话,在模块公共函数文件里增加跨域代码
单控制器允许跨域的话,在控制器文件里增加跨域代码(命名空间与类声明之间区域)
单方法允许跨域的话,在方法代码开头增加跨域代码。或者使用路由设置跨域。
这个方法可能会存在响应头被覆盖的问题(框架输出响应内容时,设置了相同的响应头,后设置覆盖前设置)
可以使用中间件,更改响应输出内容。这种方法适合全局或部分模块使用。
创建application/http/middleware/AllowCrossDomain.php
文件,文件内容如下:
注意修改允许跨域的域名
<?php
namespace app\http\middleware;
/**
* 跨域中间件
* @package app\http\middleware
*/
class AllowCrossDomain
{
/**
* @param \think\Request $request
* @param \Closure $next
* @return mixed|\think\Response
*/
public function handle($request, \Closure $next)
{
// 允许跨域的域名
$allowOriginDomain = ['www.kancloud.cn', 'll00.cn'];
// HTTP请求头中的Origin
$origin = $request->header('origin', '');
// 附加响应头
$header = [];
if (!empty($origin)) {
$domain = explode('://', $origin)[1] ?? '';
if (in_array($domain, $allowOriginDomain)) {
$header['Access-Control-Allow-Credentials'] = 'true';
$header['Access-Control-Allow-Methods'] = 'GET, POST, PATCH, PUT, DELETE';
$header['Access-Control-Allow-Headers'] = 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With';
$header['Access-Control-Allow-Origin'] = '*';
}
}
return $next($request)->header($header);
}
}
在application/middleware.php
或application/index/middleware.php
文件加入
\app\http\middleware\AllowCrossDomain::class
示例:
<?php
return [
// 允许跨域
\app\http\middleware\AllowCrossDomain::class,
// 登录认证
//\app\wxamp\middleware\CheckLogin::class,
// 权限验证
//\app\wxamp\middleware\CheckPermission::class,
];
可以在json()
、jsonp()
和xml()
等函数里设置跨域响应头,如:
return json(
[
'code' => 0,
'msg' => '操作成功',
],
200,
[
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Allow-Methods' => 'GET, POST, PATCH, PUT, DELETE',
'Access-Control-Allow-Headers' => 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With',
'Access-Control-Allow-Origin' => '*',
]
);
记录一下自己写的PHP大文件分段上传代码,方面以后要用的时候直接复制粘贴。使用了Layui、JQuery和ThinkPHP,还有一些优化空间,等下次用到的时候再完善~
<style>
.input-toolbar {
display: inline-block;
position: absolute;
top: 1px;
right: 1px;
height: 36px;
line-height: 36px;
border-radius: 0 2px 2px 0;
border-left: 1px solid #e6e6e6;
background-color: #eee;
font-size: 0;
}
.input-toolbar .layui-icon {
width: 36px;
height: 36px;
display: inline-block;
text-align: center;
border: 0;
border-right: 1px solid #e6e6e6;
cursor: pointer;
}
.input-toolbar .layui-icon:hover {
background-color: #e0e0e0;
}
.input-toolbar .layui-icon:last-child {
border-right: 0;
}
.video-upload {
position: absolute;
opacity: 0;
top: 0;
display: block;
width: 36px;
height: 36px;
}
</style>
<div class="layui-form-item" id="video-container">
<label class="layui-form-label">视频文件</label>
<div class="layui-input-block">
<input type="text" name="video" class="layui-input" placeholder="请在右侧上传视频">
<div class="input-toolbar">
<button type="button" class="layui-icon layui-icon-upload" title="点击上传">
<input type="file" class="video-upload" data-bind="[name='video']" data-progress="videoProgress">
</button>
<button type="button" class="layui-icon video-preview" data-bind="[name='video']" title="点击查看预览"></button>
</div>
<br/>
<div class="layui-progress layui-progress-big" lay-filter="videoProgress" lay-showPercent="true">
<div class="layui-progress-bar" lay-percent="0%"></div>
</div>
</div>
</div>
layui.use(['jquery', 'layer', 'element'], function(){
let $ = layui.jquery
,layer = layui.layer
,element = layui.element;
// 图片预览
$('.preview').click(function () {
var bind = $(this).data('bind');
if (!bind) {
layer.msg('缺少属性: data-bind');
return ;
}
var src = $(bind).val();
if (!src) {
layer.msg('图片地址为空');
return ;
}
layer.photos({
photos: {
"title": "预览",
"data": [{"src": src}]
}
});
});
$('.video-upload').change(async function () {
let that = this;
$(this).prop('disabled', true);
element.progress(that.dataset.progress, '0%');
console.log(that)
try {
if (!that.files.length) {
throw new Error('请选择文件');
}
let chunkSize = 1 * 1024 * 1024
,blob = that.files[0]
,chunkTotal = Math.ceil(blob.size / chunkSize)
,start = 0
,progress = 0
,end, formData;
if (!/.mp4$/i.test(blob.name)) {
throw new Error('仅允许上传mp4格式的视频');
}
for (var i = 1; i <= chunkTotal; i++) {
if (i == chunkTotal) {
end = blob.size;
} else {
end = start + chunkSize;
}
formData = new FormData();
formData.append("file", blob.slice(start, end), blob.name);
formData.append("batch", i);
await fetch("{:url('videoUpload')}", {
body: formData
,method: 'POST'
,credentials: 'same-origin'
})
.then(response => response.json())
.then(function (result) {
progress = i / (chunkTotal + 1);
element.progress(that.dataset.progress, progress * 100 + '%');
})
.catch(function (error) {
throw new Error(error);
});
start = end;
}
formData = new FormData();
formData.append("filename", blob.name);
formData.append("success", 1);
await fetch("{:url('videoUpload')}", {
body: formData
,method: 'POST'
,credentials: 'same-origin'
})
.then(response => response.json())
.then(function (result) {
$(that.dataset.bind).val(result.data.src);
})
.catch(function (error) {
throw new Error(error);
});
progress = 1;
element.progress(that.dataset.progress, progress * 100 + '%');
} catch (e) {
layer.alert(e.message);
}
$(this).prop('disabled', false);
});
// 视频预览
$('.video-preview').click(function () {
var bind = $(this).data('bind');
if (!bind) {
layer.msg('缺少属性: data-bind');
return ;
}
var src = $(bind).val();
if (!src) {
layer.msg('视频地址为空');
return ;
}
layer.open({
type: 1,
title: '视频预览',
area: ['450px', '400px'],
content: '<video src="' + src + '" controls="controls" style="width: 100%;"></video>'
});
});
});
public function videoUpload()
{
if (Request::has('success', 'param', true)) {
$tmpDir = 'storage/article_tmp/';
$videoDir = 'storage/article_video/';
$filenameMd5 = md5(Request::param('filename'));
$tmpFilePath = $tmpDir . $filenameMd5 . '.' . pathinfo(Request::param('filename'), PATHINFO_EXTENSION);
$fileResource = fopen($tmpFilePath, 'w+');
foreach(scandir($tmpDir) as $item) {
if (0 !== strpos($item, $filenameMd5 . '.mp4_')) continue;
fwrite($fileResource, file_get_contents($tmpDir . $item));
unlink($tmpDir . $item);
}
fclose($fileResource);
$newFileName = md5_file($tmpFilePath) . '.mp4';
$newFilePath = $videoDir . $newFileName;
if (!file_exists($newFilePath)) {
rename($tmpFilePath, $newFilePath);
} else {
unlink($tmpFilePath);
}
return json([
'code' => 0,
'msg' => '上传成功',
'data' => [
'src' => '/storage/article_video/' . $newFileName,
'size' => filesize($newFilePath),
]
]);
} else {
try {
$file = Request::file('file');
if (null === $file) {
throw new \Exception('请上传文件', UPLOAD_ERR_NO_FILE);
}
validate(['file' => [
'fileSize' => Config::get('filesystem.maxFileSize'),
'fileExt' => 'mp4',
]])->check(['file' => $file]);
$name = md5($file->getOriginalName()) . '.' . $file->extension() . '_' . Request::param('batch', 1);
Filesystem::disk('public')->putFileAs('article_tmp', $file, $name);
} catch (\Exception $e) {
return json([
'code' => 1,
'msg' => $e->getMessage(),
]);
}
return json([
'code' => 0,
'msg' => '上传成功',
]);
}
}
从ThinkPHP6.0.2升级到ThinkPHP6.0.3后,测试整体网站,发现用了多对多关联关联统计的地方均报错SQLSTATE[42000]: Syntax error or access violation: 1066 Not unique table/alias: 'pivot'
。经过排查,系think-orm扩展最近一次的升级,调整了多对多关联导致的错误。
下面是关键代码部分
\app\model\TaskLabel::withCount(['task'])->select()
<?php
namespace app\model;
use think\Model;
/**
* 任务标签模型
* @package app\model
*/
class TaskLabel extends Model
{
// 自动写入时间
protected $autoWriteTimestamp = 'timestamp';
// 关闭更新时间
protected $updateTime = false;
/**
* 任务关联
* @return \think\model\relation\BelongsToMany
*/
public function task()
{
return $this->belongsToMany(Task::class, TaskLabelPivot::class, 'task_id', 'label_id')
->where('status', '>', 0);
}
}
<?php
namespace app\model;
use think\model\Pivot;
/**
* 任务标签中间表模型
* @package app\model
*/
class TaskLabelPivot extends Pivot
{
// 自动写入时间
protected $autoWriteTimestamp = 'timestamp';
// 关闭更新时间
protected $updateTime = false;
}
<?php
namespace app\model;
use think\Model;
/**
* 任务
* @package app\model
*/
class Task extends Model
{
// 自动写入时间
protected $autoWriteTimestamp = 'timestamp';
}
\think\db\BaseQuery::withCount()
\think\db\BaseQuery::withAggregate()
\think\Model::relationCount()
\app\model\TaskLabel::task()
因为where是Query
里面的方法,在关联类里面不存在,所以会触发魔术方法
\think\model\Relation::__call()
\think\model\relation\BelongsToMany::baseQuery()
这方法里面会调用Query
类的field
、join
、where
等方法
\think\model\relation\BelongsToMany::belongsToManyQuery()
\think\model\relation\BelongsToMany::getRelationCountQuery()
这方法里面会调用Query
类的field
、join
、where
等方法
\think\model\relation\BelongsToMany::belongsToManyQuery()
从执行流程可以看出,\think\model\relation\BelongsToMany::belongsToManyQuery()
执行了两次,导致的结果就是join
也执行了两次,出现了开头的报错SQLSTATE[42000]: Syntax error or access violation: 1066 Not unique table/alias: 'pivot'
belongsToManyQuery
为什么会执行两次呢?
主要原因就是我在关联方法里面使用了where
方法,这个方法是Query类的,在关联类里面不存在,所以会触发魔术方法。
而魔术方法里会执行baseQuery
,baseQuery
又会执行belongsToManyQuery
。getRelationCountQuery
也会执行belongsToManyQuery
。就这样,join
重复了。
问题已找到,关联方法后面不能跟着Query
类的方法,否则就会出错。目前尚不清楚是框架的问题还是自己的用法问题,但框架问题的可能性大些,毕竟用法按照手册也报错。
虽然找问题的时间很长,但最终的解决办法也很简单,只需要将think-orm
扩展降级即可。
composer require topthink/think-orm:v2.0.32
好了,散了散了,该干嘛干嘛去。等官方出结果
2020-07-15:目前最新开发版已修复该问题,除了降级,我们还可以使用下列命令升级到最新开发版。
composer require topthink/think-orm:2.0.x-dev
下面是完整的执行流程记录,备份记录下吧
array(9) {
[0]=>
array(5) {
["file"]=>
string(63) "/vendor/topthink/think-orm/src/model/relation/BelongsToMany.php"
["line"]=>
int(687)
["function"]=>
string(18) "belongsToManyQuery"
["class"]=>
string(34) "think\model\relation\BelongsToMany"
["type"]=>
string(2) "->"
}
[1]=>
array(5) {
["file"]=>
string(49) "/vendor/topthink/think-orm/src/model/Relation.php"
["line"]=>
int(249)
["function"]=>
string(9) "baseQuery"
["class"]=>
string(34) "think\model\relation\BelongsToMany"
["type"]=>
string(2) "->"
}
[2]=>
array(5) {
["file"]=>
string(24) "/app/model/TaskLabel.php"
["line"]=>
int(25)
["function"]=>
string(6) "__call"
["class"]=>
string(20) "think\model\Relation"
["type"]=>
string(2) "->"
}
[3]=>
array(5) {
["file"]=>
string(61) "/vendor/topthink/think-orm/src/model/concern/RelationShip.php"
["line"]=>
int(392)
["function"]=>
string(4) "task"
["class"]=>
string(19) "app\model\TaskLabel"
["type"]=>
string(2) "->"
}
[4]=>
array(5) {
["file"]=>
string(64) "/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php"
["line"]=>
int(273)
["function"]=>
string(13) "relationCount"
["class"]=>
string(11) "think\Model"
["type"]=>
string(2) "->"
}
[5]=>
array(5) {
["file"]=>
string(64) "/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php"
["line"]=>
int(325)
["function"]=>
string(13) "withAggregate"
["class"]=>
string(18) "think\db\BaseQuery"
["type"]=>
string(2) "->"
}
[6]=>
array(3) {
["function"]=>
string(9) "withCount"
["class"]=>
string(18) "think\db\BaseQuery"
["type"]=>
string(2) "->"
}
[7]=>
array(3) {
["file"]=>
string(40) "/vendor/topthink/think-orm/src/Model.php"
["line"]=>
int(1047)
["function"]=>
string(20) "call_user_func_array"
}
[8]=>
array(5) {
["file"]=>
string(24) "/app/controller/Task.php"
["line"]=>
int(573)
["function"]=>
string(12) "__callStatic"
["class"]=>
string(11) "think\Model"
["type"]=>
string(2) "::"
}
}
array(7) {
[0]=>
array(5) {
["file"]=>
string(63) "/vendor/topthink/think-orm/src/model/relation/BelongsToMany.php"
["line"]=>
int(379)
["function"]=>
string(18) "belongsToManyQuery"
["class"]=>
string(34) "think\model\relation\BelongsToMany"
["type"]=>
string(2) "->"
}
[1]=>
array(5) {
["file"]=>
string(61) "/vendor/topthink/think-orm/src/model/concern/RelationShip.php"
["line"]=>
int(392)
["function"]=>
string(21) "getRelationCountQuery"
["class"]=>
string(34) "think\model\relation\BelongsToMany"
["type"]=>
string(2) "->"
}
[2]=>
array(5) {
["file"]=>
string(64) "/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php"
["line"]=>
int(273)
["function"]=>
string(13) "relationCount"
["class"]=>
string(11) "think\Model"
["type"]=>
string(2) "->"
}
[3]=>
array(5) {
["file"]=>
string(64) "/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php"
["line"]=>
int(325)
["function"]=>
string(13) "withAggregate"
["class"]=>
string(18) "think\db\BaseQuery"
["type"]=>
string(2) "->"
}
[4]=>
array(3) {
["function"]=>
string(9) "withCount"
["class"]=>
string(18) "think\db\BaseQuery"
["type"]=>
string(2) "->"
}
[5]=>
array(3) {
["file"]=>
string(40) "/vendor/topthink/think-orm/src/Model.php"
["line"]=>
int(1047)
["function"]=>
string(20) "call_user_func_array"
}
[6]=>
array(5) {
["file"]=>
string(24) "/app/controller/Task.php"
["line"]=>
int(573)
["function"]=>
string(12) "__callStatic"
["class"]=>
string(11) "think\Model"
["type"]=>
string(2) "::"
}
}