jwj 发布的文章

记录一下自己写的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>

HTML

<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="点击查看预览">&#xe64a;</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>

Javascript

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

PHP ThinkPHP控制器方法代码

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'  => '上传成功',
        ]);
    }
}

1.安装Visual Studio 2019
https://visualstudio.microsoft.com/zh-hans/thank-you-downloading-visual-studio/?sku=Community&rel=16

2.安装时,勾选上“使用 C++ 的桌面开发”
请输入图片描述

3.安装好之后,在开始菜单打开Visual Studio 2019-x64 Native Tools Command Prompt for VS 2019
QQ截图20200827112523.png

4.下载PHP SDK
打开https://github.com/microsoft/php-sdk-binary-tools/releases,下载php-sdk-2.2.0

5.解压PHP SDK
创建php-sdk目录,然后将下载的php-sdk-2.2.0压缩包里php-sdk-binary-tools-php-sdk-2.2.0目录下的所有文件解压到php-sdk目录。

6.切换到刚刚创建的php-sdk目录

cd E:\php-sdk

7.执行“调用初学者脚本”

phpsdk-vs16-x64.bat

输出:

[vcvarsall.bat] Environment initialized for: 'x64'

PHP SDK 2.2.0

OS architecture:    64-bit
Build architecture: 64-bit
Visual C++:         14.27.29111.0
PHP-SDK path:       E:\php-sdk

8.运行批处理脚本
脚本会自动创建所需的目录结构

phpsdk_buildtree phpdev

9.下载php8源代码
打开https://github.com/php/php-src/releases,下载PHP8的源代码压缩包

10.解压PHP源代码
将源代码压缩包内php-src-php-8.0.0beta2目录下的所有文件解压到E:\php-sdk\phpdev\vs16\x64\php-8.0.0-src

11.获取PHP所依赖的库
切换到php源代码目录

cd E:\php-sdk\phpdev\vs16\x64\php-8.0.0-src

使用 PHP SDK 工具通过phpsdk_deps -u命令自动获取适当的依赖关系。这里的下载速度很感人。。。

phpsdk_deps -u

输出

Configuration: 8.0-vs16-x64-stable

Processing package apache-2.4.43-vs16-x64.zip
Processing package c-client-2007f-vs16-x64.zip
Processing package fbclient-3.0.6-nocrt-x64.zip
Processing package freetype-2.9.1-1-vs16-x64.zip
Processing package glib-2.53.3-vs16-x64.zip
Processing package ICU-67.1-vs16-x64.zip
Processing package libargon2-20190702-vs16-x64.zip
Processing package libbzip2-1.0.8-vs16-x64.zip
Processing package libcurl-7.71.1-vs16-x64.zip
Processing package libenchant2-2.2.8-vs16-x64.zip
Processing package libffi-3.3-1-vs16-x64.zip
Processing package libiconv-1.16-2-vs16-x64.zip
Processing package libintl-0.18.3-5-vs16-x64.zip
Processing package libjpeg-9c-vs16-x64.zip
Processing package liblmdb-0.9.22-3-vs16-x64.zip
Processing package liblzma-5.2.5-vs16-x64.zip
Processing package libonig-6.9.5-1-vs16-x64.zip
Processing package libpng-1.6.34-4-vs16-x64.zip
Processing package libpq-11.4-vs16-x64.zip
Processing package libqdbm-1.8.78-vs16-x64.zip
Processing package libsasl-2.1.27-2-vs16-x64.zip
Processing package libsodium-1.0.18-vs16-x64.zip
Processing package libssh2-1.9.0-vs16-x64.zip
Processing package libtidy-5.6.0-2-vs16-x64.zip
Processing package libwebp-1.1.0-vs16-x64.zip
Processing package libxml2-2.9.10-vs16-x64.zip
Processing package libxpm-3.5.12-5-vs16-x64.zip
Processing package libxslt-1.1.34-vs16-x64.zip
Processing package libzip-1.7.1-vs16-x64.zip
Processing package mpir-3.0.0-vs16-x64.zip
Processing package net-snmp-5.7.3-1-vs16-x64.zip
Processing package nghttp2-1.40.0-vs16-x64.zip
Processing package openldap-2.4.47-vs16-x64.zip
Processing package openssl-1.1.1g-vs16-x64.zip
Processing package sqlite3-3.33.0-vs16-x64.zip
Processing package wineditline-2.205-1-vs16-x64.zip
Processing package zlib-1.2.11-vs16-x64.zip
Updates performed successfully.
Old dependencies backed up into 'E:\php-sdk\phpdev\vs16\x64\deps.202008270422'.
buildconf
Rebuilding configure.js
Now run 'configure --help'
configure --help
PHP Version: 8.0.0beta2

Options that enable extensions and SAPI will accept 'yes' or 'no' as a
parameter. They also accept 'shared' as a synonym for 'yes' and request a
shared build of that module. Not all modules can be built as shared modules;
configure will display [shared] after the module name if can be built that
way.

  --enable-snapshot-build           Build a snapshot; turns on everything it
                                    can and ignores build errors
  --with-verbosity                  Output verbosity, 0-2.
  --with-toolset                    Toolset to use for the compilation, give:
                                    vs, clang, icc. The only recommended and
                                    supported toolset for production use is
                                    Visual Studio. Use others at your own
                                    risk.
  --with-cygwin                     Path to cygwin utilities on your system
  --enable-object-out-dir           Alternate location for binary objects
                                    during build
  --enable-debug                    Compile with debugging symbols
  --enable-debug-pack               Release binaries with external debug
                                    symbols (--enable-debug must not be
                                    specified)
  --enable-pgi                      Generate PGO instrumented binaries
  --with-pgo                        Compile optimized binaries using training
                                    data from folder
  --disable-zts                     Thread safety
  --with-prefix                     where PHP will be installed
  --with-mp                         Tell Visual Studio use up to
                                    [n,auto,disable] processes for compilation
  --with-php-build                  Path to where you extracted the
                                    development libraries
                                    (http://wiki.php.net/internals/windows/libs).
                                    Assumes that it is a sibling of this
                                    source dir (..\deps) if not specified
  --with-extra-includes             Extra include path to use when building
                                    everything
  --with-extra-libs                 Extra library path to use when linking
                                    everything
  --with-analyzer                   Enable static analyzer. Pass vs for Visual
                                    Studio, clang for clang, cppcheck for
                                    Cppcheck, pvs for PVS-Studio
  --disable-ipv6                    Disable IPv6 support (default is turn it
                                    on if available)
  --enable-fd-setsize               Set maximum number of sockets for
                                    select(2)
  --with-snapshot-template          Path to snapshot builder template dir
  --disable-security-flags          Disable the compiler security flags
  --without-uncritical-warn-choke   Disable some uncritical warnings
  --enable-sanitizer                Enable ASan and UBSan extensions
  --with-codegen-arch               Architecture for code generation: ia32.
                                    Use --enable-native-intrinsics to enable
                                    SIMD optimizations.
  --with-all-shared                 Force all the non obligatory extensions to
                                    be shared
  --with-config-profile             Name of the configuration profile to save
                                    this to in php-src/config.name.bat
  --disable-test-ini                Enable automatic php.ini generation. The
                                    test.ini will be put into the build dir
                                    and used to automatically load the shared
                                    extensions.
  --with-test-ini-ext-exclude       Comma separated list of shared extensions
                                    to be excluded from the test.ini
  --enable-native-intrinsics        Comma separated list of intrinsic
                                    optimizations to enable. Available
                                    instruction set names are sse, sse2, sse3,
                                    ssse3, sse4.1, sse4.2, avx, avx2. SSE and
                                    SSE2 are enabled by default. The best
                                    instruction set specified will
                                    automatically enable all the older
                                    instruction sets. Note, that the produced
                                    binary might not work properly, if the
                                    chosen instruction sets are not available
                                    on the target processor.
  --enable-apache2handler           Build Apache 2.x handler
  --enable-apache2-2handler         Build Apache 2.2.x handler
  --enable-apache2-4handler         Build Apache 2.4.x handler
  --disable-cgi                     Build CGI version of PHP
  --disable-cli                     Build CLI version of PHP
  --enable-cli-win32                Build console-less CLI version of PHP
  --enable-embed                    Embedded SAPI library
  --enable-phpdbg                   Build phpdbg
  --enable-phpdbgs                  Build phpdbg shared
  --disable-phpdbg-webhelper        Build phpdbg webhelper
  --disable-bcmath                  bc style precision math functions
  --with-bz2                        BZip2
  --disable-calendar                calendar conversion support
  --disable-com-dotnet              COM and .Net support
  --disable-ctype                   ctype
  --with-curl                       cURL support
  --with-dba                        DBA support
  --with-qdbm                       DBA: QDBM support
  --with-db                         DBA: Berkeley DB support
  --with-lmdb                       DBA: Lightning memory-mapped database
                                    support
  --with-enchant                    Enchant Support
  --with-ffi                        ffi support
  --enable-fileinfo                 fileinfo support
  --disable-filter                  Filter Support
  --enable-ftp                      ftp support
  --without-gd                      Bundled GD support
  --without-libwebp                 webp support
  --with-gettext                    gettext support
  --with-gmp                        Include GNU MP support.
  --with-mhash                      mhash support (BC via hash)
  --without-iconv                   iconv support
  --with-imap                       IMAP Support
  --enable-intl                     Enable internationalization support
  --with-ldap                       LDAP support
  --enable-mbstring                 multibyte string functions
  --enable-mbregex                  multibyte regex support
  --without-mysqlnd                 Mysql Native Client Driver
  --with-oci8                       OCI8 support
  --with-oci8-11g                   OCI8 support using Oracle 11g Instant
                                    Client
  --with-oci8-12c                   OCI8 support using Oracle Database 12c
                                    Instant Client
  --enable-odbc                     ODBC support
  --with-odbcver                    Force support for the passed ODBC version.
                                    A hex number is expected, default 0x0350.
                                    Use the special value of 0 to prevent an
                                    explicit ODBCVER to be defined.
  --disable-opcache                 whether to enable Zend OPcache support
  --disable-opcache-jit             whether to enable JIT
  --with-openssl                    OpenSSL support
  --without-pcre-jit                Enable PCRE JIT support
  --with-pgsql                      PostgreSQL support
  --with-pspell                     pspell/aspell (whatever it's called this
                                    month) support
  --without-readline                Readline support
  --disable-session                 session support
  --enable-shmop                    shmop support
  --with-snmp                       SNMP support
  --enable-sockets                  SOCKETS support
  --with-sodium                     for libsodium support
  --with-sqlite3                    SQLite 3 support
  --with-password-argon2            Argon2 support
  --with-config-file-scan-dir       Dir to check for additional php ini files
  --enable-sysvshm                  SysV Shared Memory support
  --with-tidy                       TIDY support
  --disable-tokenizer               tokenizer support
  --enable-zend-test                enable zend-test extension
  --disable-zip                     ZIP support
  --disable-zlib                    ZLIB support
  --without-libxml                  LibXML support
  --without-dom                     DOM support
  --enable-exif                     Exchangeable image information (EXIF)
                                    Support
  --with-mysqli                     MySQLi support
  --enable-pdo                      Enable PHP Data Objects support
  --with-pdo-dblib                  freetds dblib (Sybase, MS-SQL) support for
                                    PDO
  --with-pdo-mssql                  Native MS-SQL support for PDO
  --with-pdo-firebird               Firebird support for PDO
  --with-pdo-mysql                  MySQL support for PDO
  --with-pdo-oci                    Oracle OCI support for PDO
  --with-pdo-odbc                   ODBC support for PDO
  --with-pdo-pgsql                  PostgreSQL support for PDO
  --with-pdo-sqlite                 for pdo_sqlite support
  --disable-phar                    disable phar support
  --enable-phar-native-ssl          enable phar with native OpenSSL support
  --without-simplexml               Simple XML support
  --enable-soap                     SOAP support
  --without-xml                     XML support
  --disable-xmlreader               XMLReader support
  --disable-xmlwriter               XMLWriter support
  --with-xsl                        xsl support

Some influential environment variables:
  CFLAGS      C compiler flags
  LDFLAGS     linker flags
configure --disable-all --enable-cli --enable-$remains
PHP Version: 8.0.0beta2

Saving configure options to config.nice.bat
Checking for cl.exe ...  <in default path>
  Detected compiler Visual C++ 2019
  Detected 64-bit compiler
Checking for link.exe ...  <in default path>
Checking for nmake.exe ...  <in default path>
Checking for lib.exe ...  <in default path>
Checking for bison.exe ...  <in default path>
  Detected bison version 3.3.2
Checking for sed.exe ...  <in default path>
Checking for re2c.exe ...  <in default path>
  Detected re2c version 1.1.1
Checking for zip.exe ...  <in default path>
Checking for lemon.exe ...  <in default path>
Checking for 7za.exe ...  <in default path>
Checking for mc.exe ...  E:\Windows Kits\10\bin\10.0.18362.0\x64
Checking for mt.exe ...  E:\Windows Kits\10\bin\10.0.18362.0\x64
Enabling multi process build

Build dir: E:\php-sdk\phpdev\vs16\x64\php-8.0.0-src\x64\Release_TS
PHP Core:  php8ts.dll and php8ts.lib

Checking for wspiapi.h ...  <in default path>
Enabling IPv6 support
Enabling SAPI sapi\cli
Checking for library edit_a.lib;edit.lib ... <in deps path> \lib\edit_a.lib
Checking for editline/readline.h ...  <in deps path> \include
Enabling extension ext\date
Enabling extension ext\hash
Checking for KeccakHash.h ...  ext/hash/sha3/generic64lc
Enabling extension ext\json
Enabling extension ext\pcre
Enabling extension ext\reflection
Enabling extension ext\spl
Checking for timelib_config.h ...  ext/date/lib
Enabling extension ext\standard

Creating build dirs...
Generating files...
Generating Makefile
Generating main/internal_functions.c
Generating main/config.w32.h
Generating phpize
Done.



Enabled extensions:
-----------------------
| Extension  | Mode   |
-----------------------
| date       | static |
| hash       | static |
| json       | static |
| pcre       | static |
| reflection | static |
| spl        | static |
| standard   | static |
-----------------------


Enabled SAPI:
-------------
| Sapi Name |
-------------
| cli       |
-------------


---------------------------------------
|                   |                 |
---------------------------------------
| Build type        | Release         |
| Thread Safety     | Yes             |
| Compiler          | Visual C++ 2019 |
| Architecture      | x64             |
| Optimization      | PGO disabled    |
| Native intrinsics | SSE2            |
| Static analyzer   | disabled        |
---------------------------------------


WARNING
The following arguments is invalid, and therefore ignored:
 --enable-$remains


Type 'nmake' to build PHP
nmake

‎aria2 是用于下载文件的实用程序。它支持 HTTP(S)/FTP/SFTP/BitTorrent 和 Metalink 协议。aria2可以从多个来源/协议下载文件,并尝试利用您的最大下载带宽。它支持同时从HTTP(S)/FTP/SFTP和BitTorrent下载文件,而从HTTP(S)/FTP/SFTP下载的数据上传到BitTorrent群。使用 Metalink 块校验和,aria2 在下载文件时自动验证数据块。‎

安装Aria2

sudo apt install aria2

image.png

配置Aria2配置文件

Arai2默认会从$HOME/.aria2/aria2.conf$XDG_CONFIG_HOME/aria2/aria2.conf这两个位置读取配置,为了方便管理,我们可以将配置文件放到/etc/aria2/aria2.conf,以方便管理。

创建/etc/aria2目录,新建并编辑/etc/aria2/aria2.conf配置文件

sudo mkdir /etc/aria2/
sudo vim /etc/aria2/aria2.conf

创建下载文件保存目录

mkdir /home/ubuntu/downloads

把以下配置内容写入到/etc/aria2/aria2.conf

# 设置加密的密钥
rpc-secret=12580
# RPC 开关
enable-rpc=true
# RPC 跨域(WEB 界面需要跨域)
rpc-allow-origin-all=true
# RPC 外部访问(false 的话只监听本地端口)
rpc-listen-all=true
# RPC 端口, 仅当默认端口被占用时修改
rpc-listen-port=6800
# 最大并行下载数(默认值:5)
#max-concurrent-downloads=5
# 单文件并行下载数
split=5
# 断点续传
continue=true
# 同服务器最大连接数
max-connection-per-server=5
# 最小文件分片大小, 下载线程数上限取决于能分出多少片, 对于小文件重要
min-split-size=10M
# 下载速度限制(0代表不限制)(示例值:500K、10M)
max-overall-download-limit=0
# 单文件速度限制
max-download-limit=0
# 上传速度限制
max-overall-upload-limit=0
# 单文件速度限制
max-upload-limit=0
# 断开速度过慢的连接
#lowest-speed-limit=0
# 设置请求头 referer
#referer=*
# 文件保存路径, 默认为当前启动位置
dir=/home/ubuntu/downloads
# 启用磁盘缓存
#disk-cache=0
# 文件分配方法
#file-allocation=prealloc

配置开机启动

sudo nano /etc/systemd/system/aria2.service
[Unit]
Description=aria2 Service
After=network.target

[Service]
Type=simple
User=www
Group=www
Restart=on-failure
RestartSec=5s
ExecStart=aria2c --conf-path=/etc/aria2/aria2.conf

[Install]
WantedBy=multi-user.target

此时,我们尝试下启动服务

sudo systemctl start aria2

然后查看服务状态

sudo systemctl status aria2

如果Active的状态是 active (running) ,则代表服务运行正常。

那我们就可以直接启用服务了,那么服务就会开机时自动启动。

sudo systemctl enable aria2

开放端口

防火墙放通 6800 端口

sudo ufw allow 6800
sudo ufw enable

imageb714790991761b6e.png

参考资料

从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()

TaskLabel模型

<?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';
}

流程解刨

1.执行withCount方法

\think\db\BaseQuery::withCount()

2.执行withAggregate方法

\think\db\BaseQuery::withAggregate()

3.执行relationCount方法

\think\Model::relationCount()

4.执行task方法

\app\model\TaskLabel::task()

5.触发__call方法

因为where是Query里面的方法,在关联类里面不存在,所以会触发魔术方法

\think\model\Relation::__call()

6.执行baseQuery方法

\think\model\relation\BelongsToMany::baseQuery()

7.执行belongsToManyQuery方法

这方法里面会调用Query类的fieldjoinwhere等方法

\think\model\relation\BelongsToMany::belongsToManyQuery()

8.执行getRelationCountQuery方法

\think\model\relation\BelongsToMany::getRelationCountQuery()

9.执行belongsToManyQuery方法

这方法里面会调用Query类的fieldjoinwhere等方法

\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类的,在关联类里面不存在,所以会触发魔术方法。
而魔术方法里会执行baseQuerybaseQuery又会执行belongsToManyQuerygetRelationCountQuery也会执行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) "::"
  }
}

开启WSL

以管理员身份启动 Windows PowerShell,输入以下命令

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

命令执行可能需要几分钟,系统设置完成后会提示是否重启,我们输入“Y”重启系统以完成设置

下载子系统

WIndows获取发行版子系统下载链接。
然后再命令行执行下载,文件大概四五百兆

Invoke-WebRequest -Uri https://aka.ms/wslubuntu2004 -OutFile Ubuntu.appx -UseBasicParsing

安装子系统

Add-AppxPackage .\Ubuntu.appx

安装好之后,打开开始菜单会看到一个新的应用程序,打开它即可打开子系统。
第一次运行需要配置用户和密码,按照提示设置用户和密码即可。

资料参考:https://blog.irain.in/archives/Windows_Server_2019_Subsystem.html