diff --git a/Server/vendor/topthink/think-helper/.github/workflows/ci.yml b/Server/vendor/topthink/think-helper/.github/workflows/ci.yml new file mode 100644 index 00000000..c63b5d42 --- /dev/null +++ b/Server/vendor/topthink/think-helper/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + + +jobs: + phpcs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup PHP environment + uses: shivammathur/setup-php@v2 + - name: Install dependencies + run: composer install + - name: PHPCSFixer check + run: composer check-style + phpunit: + strategy: + matrix: + php_version: [7.3, 7.4, 8.0] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup PHP environment + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php_version }} + coverage: xdebug + - name: Install dependencies + run: composer install + - name: PHPUnit check + run: ./vendor/bin/phpunit --coverage-text diff --git a/Server/vendor/topthink/think-helper/.github/workflows/php.yml b/Server/vendor/topthink/think-helper/.github/workflows/php.yml new file mode 100644 index 00000000..01d5b631 --- /dev/null +++ b/Server/vendor/topthink/think-helper/.github/workflows/php.yml @@ -0,0 +1,36 @@ +name: PHP Composer + +on: + push: + branches: [ 3.0 ] + pull_request: + branches: [ 3.0 ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" + # Docs: https://getcomposer.org/doc/articles/scripts.md + + - name: Run test suite + run: composer test diff --git a/Server/vendor/topthink/think-helper/.gitignore b/Server/vendor/topthink/think-helper/.gitignore new file mode 100644 index 00000000..5ea11e9f --- /dev/null +++ b/Server/vendor/topthink/think-helper/.gitignore @@ -0,0 +1,4 @@ +/vendor/ +/.idea/ +composer.lock +.phpunit.result.cache \ No newline at end of file diff --git a/Server/vendor/topthink/think-helper/LICENSE b/Server/vendor/topthink/think-helper/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/Server/vendor/topthink/think-helper/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Server/vendor/topthink/think-helper/README.md b/Server/vendor/topthink/think-helper/README.md new file mode 100644 index 00000000..f8f226de --- /dev/null +++ b/Server/vendor/topthink/think-helper/README.md @@ -0,0 +1,35 @@ +# thinkphp6 常用的一些扩展类库 + +基于PHP7.1+ + +[![PHP Composer](https://github.com/larvatecn/think-helper/actions/workflows/php.yml/badge.svg)](https://github.com/larvatecn/think-helper/actions/workflows/php.yml) + +> 以下类库都在`\\think\\helper`命名空间下 + +## Str + +> 字符串操作 + +``` +// 检查字符串中是否包含某些字符串 +Str::contains($haystack, $needles) + +// 检查字符串是否以某些字符串结尾 +Str::endsWith($haystack, $needles) + +// 获取指定长度的随机字母数字组合的字符串 +Str::random($length = 16) + +// 字符串转小写 +Str::lower($value) + +// 字符串转大写 +Str::upper($value) + +// 获取字符串的长度 +Str::length($value) + +// 截取字符串 +Str::substr($string, $start, $length = null) + +``` \ No newline at end of file diff --git a/Server/vendor/topthink/think-helper/composer.json b/Server/vendor/topthink/think-helper/composer.json new file mode 100644 index 00000000..0892fa63 --- /dev/null +++ b/Server/vendor/topthink/think-helper/composer.json @@ -0,0 +1,36 @@ +{ + "name": "topthink/think-helper", + "description": "The ThinkPHP6 Helper Package", + "license": "Apache-2.0", + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "autoload": { + "psr-4": { + "think\\": "src" + }, + "files": [ + "src/helper.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests" + } + }, + "scripts": { + "test": "./vendor/bin/phpunit --colors" + }, + "scripts-descriptions": { + "test": "Run all tests." + } +} diff --git a/Server/vendor/topthink/think-helper/phpunit.xml.dist b/Server/vendor/topthink/think-helper/phpunit.xml.dist new file mode 100644 index 00000000..c083f148 --- /dev/null +++ b/Server/vendor/topthink/think-helper/phpunit.xml.dist @@ -0,0 +1,17 @@ + + + + + ./tests/ + + + + + ./src + + + diff --git a/Server/vendor/topthink/think-helper/src/Collection.php b/Server/vendor/topthink/think-helper/src/Collection.php new file mode 100644 index 00000000..a6aaae1e --- /dev/null +++ b/Server/vendor/topthink/think-helper/src/Collection.php @@ -0,0 +1,678 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think; + +use ArrayAccess; +use ArrayIterator; +use Countable; +use IteratorAggregate; +use JsonSerializable; +use think\contract\Arrayable; +use think\contract\Jsonable; +use think\helper\Arr; +use Traversable; + +/** + * 数据集管理类 + * + * @template TKey of array-key + * @template-covariant TValue + * + * @implements ArrayAccess + * @implements IteratorAggregate + */ +class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable, Arrayable, Jsonable +{ + /** + * 数据集数据 + * @var array + */ + protected $items = []; + + /** + * 构造函数 + * @param iterable|Collection $items 数据 + */ + public function __construct($items = []) + { + $this->items = $this->convertToArray($items); + } + + /** + * @param iterable|Collection $items + * @return static + */ + public static function make($items = []) + { + return new static($items); + } + + /** + * 是否为空 + * @return bool + */ + public function isEmpty(): bool + { + return empty($this->items); + } + + public function toArray(): array + { + return array_map(function ($value) { + return $value instanceof Arrayable ? $value->toArray() : $value; + }, $this->items); + } + + /** + * @return array + */ + public function all(): array + { + return $this->items; + } + + /** + * 合并数组 + * + * @param mixed $items 数据 + * @return static + */ + public function merge($items) + { + return new static(array_merge($this->items, $this->convertToArray($items))); + } + + /** + * 按指定键整理数据 + * + * @param mixed $items 数据 + * @param string|null $indexKey 键名 + * @return array + */ + public function dictionary($items = null, ?string &$indexKey = null) + { + if ($items instanceof self) { + $items = $items->all(); + } + + $items = is_null($items) ? $this->items : $items; + + if ($items && empty($indexKey)) { + $indexKey = is_array($items[0]) ? 'id' : $items[0]->getPk(); + } + + if (isset($indexKey) && is_string($indexKey)) { + return array_column($items, null, $indexKey); + } + + return $items; + } + + /** + * 比较数组,返回差集 + * + * @param mixed $items 数据 + * @param string|null $indexKey 指定比较的键名 + * @return static + */ + public function diff($items, ?string $indexKey = null) + { + if ($this->isEmpty() || is_scalar($this->items[0])) { + return new static(array_diff($this->items, $this->convertToArray($items))); + } + + $diff = []; + $dictionary = $this->dictionary($items, $indexKey); + + if (is_string($indexKey)) { + foreach ($this->items as $item) { + if (!isset($dictionary[$item[$indexKey]])) { + $diff[] = $item; + } + } + } + + return new static($diff); + } + + /** + * 比较数组,返回交集 + * + * @param mixed $items 数据 + * @param string|null $indexKey 指定比较的键名 + * @return static + */ + public function intersect($items, ?string $indexKey = null) + { + if ($this->isEmpty() || is_scalar($this->items[0])) { + return new static(array_intersect($this->items, $this->convertToArray($items))); + } + + $intersect = []; + $dictionary = $this->dictionary($items, $indexKey); + + if (is_string($indexKey)) { + foreach ($this->items as $item) { + if (isset($dictionary[$item[$indexKey]])) { + $intersect[] = $item; + } + } + } + + return new static($intersect); + } + + /** + * 交换数组中的键和值 + * + * @return static + */ + public function flip() + { + return new static(array_flip($this->items)); + } + + /** + * 返回数组中所有的键名 + * + * @return static + */ + public function keys() + { + return new static(array_keys($this->items)); + } + + /** + * 返回数组中所有的值组成的新 Collection 实例 + * @return static + */ + public function values() + { + return new static(array_values($this->items)); + } + + /** + * 删除数组的最后一个元素(出栈) + * + * @return TValue + */ + public function pop() + { + return array_pop($this->items); + } + + /** + * 通过使用用户自定义函数,以字符串返回数组 + * + * @param callable $callback 调用方法 + * @param mixed $initial + * @return mixed + */ + public function reduce(callable $callback, $initial = null) + { + return array_reduce($this->items, $callback, $initial); + } + + /** + * 以相反的顺序返回数组。 + * + * @return static + */ + public function reverse() + { + return new static(array_reverse($this->items)); + } + + /** + * 删除数组中首个元素,并返回被删除元素的值 + * + * @return TValue + */ + public function shift() + { + return array_shift($this->items); + } + + /** + * 在数组结尾插入一个元素 + * + * @param mixed $value 元素 + * @param string|null $key KEY + * @return $this + */ + public function push($value, ?string $key = null) + { + if (is_null($key)) { + $this->items[] = $value; + } else { + $this->items[$key] = $value; + } + + return $this; + } + + /** + * 把一个数组分割为新的数组块. + * + * @param int $size 块大小 + * @param bool $preserveKeys + * @return static + */ + public function chunk(int $size, bool $preserveKeys = false) + { + $chunks = []; + + foreach (array_chunk($this->items, $size, $preserveKeys) as $chunk) { + $chunks[] = new static($chunk); + } + + return new static($chunks); + } + + /** + * 在数组开头插入一个元素 + * + * @param mixed $value 元素 + * @param string|null $key KEY + * @return $this + */ + public function unshift($value, ?string $key = null) + { + if (is_null($key)) { + array_unshift($this->items, $value); + } else { + $this->items = [$key => $value] + $this->items; + } + + return $this; + } + + /** + * 给每个元素执行个回调 + * + * + * @param callable $callback 回调 + * @return $this + */ + public function each(callable $callback) + { + foreach ($this->items as $key => $item) { + $result = $callback($item, $key); + + if (false === $result) { + break; + } elseif (!is_object($item)) { + $this->items[$key] = $result; + } + } + + return $this; + } + + /** + * 用回调函数处理数组中的元素 + * + * @param callable|null $callback 回调 + * @return static + */ + public function map(callable $callback) + { + return new static(array_map($callback, $this->items)); + } + + /** + * 用回调函数过滤数组中的元素 + * + * @param callable|null $callback 回调 + * @return static + */ + public function filter(?callable $callback = null) + { + if ($callback) { + return new static(array_filter($this->items, $callback)); + } + + return new static(array_filter($this->items)); + } + + /** + * 根据字段条件过滤数组中的元素 + * + * @param string $field 字段名 + * @param mixed $operator 操作符 + * @param mixed $value 数据 + * @return static + */ + public function where(string $field, $operator, $value = null) + { + if (is_null($value)) { + $value = $operator; + $operator = '='; + } + + return $this->filter(function ($data) use ($field, $operator, $value) { + if (strpos($field, '.')) { + [$field, $relation] = explode('.', $field); + + $result = $data[$field][$relation] ?? null; + } else { + $result = $data[$field] ?? null; + } + + switch (strtolower($operator)) { + case '===': + return $result === $value; + case '!==': + return $result !== $value; + case '!=': + case '<>': + return $result != $value; + case '>': + return $result > $value; + case '>=': + return $result >= $value; + case '<': + return $result < $value; + case '<=': + return $result <= $value; + case 'like': + return is_string($result) && false !== strpos($result, $value); + case 'not like': + return is_string($result) && false === strpos($result, $value); + case 'in': + return is_scalar($result) && in_array($result, $value, true); + case 'not in': + return is_scalar($result) && !in_array($result, $value, true); + case 'between': + [$min, $max] = is_string($value) ? explode(',', $value) : $value; + return is_scalar($result) && $result >= $min && $result <= $max; + case 'not between': + [$min, $max] = is_string($value) ? explode(',', $value) : $value; + return is_scalar($result) && $result > $max || $result < $min; + case '==': + case '=': + default: + return $result == $value; + } + }); + } + + /** + * LIKE过滤 + * + * @param string $field 字段名 + * @param string $value 数据 + * @return static + */ + public function whereLike(string $field, string $value) + { + return $this->where($field, 'like', $value); + } + + /** + * NOT LIKE过滤 + * + * @param string $field 字段名 + * @param string $value 数据 + * @return static + */ + public function whereNotLike(string $field, string $value) + { + return $this->where($field, 'not like', $value); + } + + /** + * IN过滤 + * + * @param string $field 字段名 + * @param array $value 数据 + * @return static + */ + public function whereIn(string $field, array $value) + { + return $this->where($field, 'in', $value); + } + + /** + * NOT IN过滤 + * + * @param string $field 字段名 + * @param array $value 数据 + * @return static + */ + public function whereNotIn(string $field, array $value) + { + return $this->where($field, 'not in', $value); + } + + /** + * BETWEEN 过滤 + * + * @param string $field 字段名 + * @param mixed $value 数据 + * @return static + */ + public function whereBetween(string $field, $value) + { + return $this->where($field, 'between', $value); + } + + /** + * NOT BETWEEN 过滤 + * + * @param string $field 字段名 + * @param mixed $value 数据 + * @return static + */ + public function whereNotBetween(string $field, $value) + { + return $this->where($field, 'not between', $value); + } + + /** + * 返回数据中指定的一列 + * + * @param string|null $columnKey 键名 + * @param string|null $indexKey 作为索引值的列 + * @return array + */ + public function column(?string $columnKey, ?string $indexKey = null) + { + return array_column($this->items, $columnKey, $indexKey); + } + + /** + * 对数组排序 + * + * @param callable|null $callback 回调 + * @return static + */ + public function sort(?callable $callback = null) + { + $items = $this->items; + + $callback = $callback ?: function ($a, $b) { + return $a == $b ? 0 : (($a < $b) ? -1 : 1); + }; + + uasort($items, $callback); + + return new static($items); + } + + /** + * 指定字段排序 + * + * @param string $field 排序字段 + * @param string $order 排序 + * @return $this + */ + public function order(string $field, string $order = 'asc') + { + return $this->sort(function ($a, $b) use ($field, $order) { + $fieldA = $a[$field] ?? null; + $fieldB = $b[$field] ?? null; + + return 'desc' == strtolower($order) ? intval($fieldB > $fieldA) : intval($fieldA > $fieldB); + }); + } + + /** + * 将数组打乱 + * + * @return static + */ + public function shuffle() + { + $items = $this->items; + + shuffle($items); + + return new static($items); + } + + /** + * 获取第一个单元数据 + * + * @param callable|null $callback + * @param null $default + * @return TValue + */ + public function first(?callable $callback = null, $default = null) + { + return Arr::first($this->items, $callback, $default); + } + + /** + * 获取最后一个单元数据 + * + * @param callable|null $callback + * @param null $default + * @return TValue + */ + public function last(?callable $callback = null, $default = null) + { + return Arr::last($this->items, $callback, $default); + } + + /** + * 截取数组 + * + * @param int $offset 起始位置 + * @param int|null $length 截取长度 + * @param bool $preserveKeys preserveKeys + * @return static + */ + public function slice(int $offset, ?int $length = null, bool $preserveKeys = false) + { + return new static(array_slice($this->items, $offset, $length, $preserveKeys)); + } + + /** + * @param TKey $key + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) : bool + { + return array_key_exists($offset, $this->items); + } + + /** + * @param TKey $offset + * @return TValue + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->items[$offset]; + } + + /** + * @param TKey|null $offset + * @param TValue $value + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->items[] = $value; + } else { + $this->items[$offset] = $value; + } + } + + /** + * @param TKey $offset + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + unset($this->items[$offset]); + } + + //Countable + public function count(): int + { + return count($this->items); + } + + /** + * @return ArrayIterator + */ + #[\ReturnTypeWillChange] + public function getIterator(): Traversable + { + return new ArrayIterator($this->items); + } + + //JsonSerializable + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->toArray(); + } + + /** + * 转换当前数据集为JSON字符串 + * + * @param integer $options json参数 + * @return string + */ + public function toJson(int $options = JSON_UNESCAPED_UNICODE): string + { + return json_encode($this->toArray(), $options); + } + + public function __toString() + { + return $this->toJson(); + } + + /** + * 转换成数组 + * + * @param mixed $items 数据 + * @return array + */ + protected function convertToArray($items): array + { + if ($items instanceof self) { + return $items->all(); + } + + return (array) $items; + } +} diff --git a/Server/vendor/topthink/think-helper/src/contract/Arrayable.php b/Server/vendor/topthink/think-helper/src/contract/Arrayable.php new file mode 100644 index 00000000..7c6b992b --- /dev/null +++ b/Server/vendor/topthink/think-helper/src/contract/Arrayable.php @@ -0,0 +1,8 @@ + +// +---------------------------------------------------------------------- + +use think\Collection; +use think\helper\Arr; + +if (!function_exists('throw_if')) { + /** + * 按条件抛异常 + * + * @template TValue + * @template TException of \Throwable + * + * @param TValue $condition + * @param TException|class-string|string $exception + * @param mixed ...$parameters + * @return TValue + * + * @throws TException + */ + function throw_if($condition, $exception, ...$parameters) + { + if ($condition) { + throw (is_string($exception) ? new $exception(...$parameters) : $exception); + } + + return $condition; + } +} + +if (!function_exists('throw_unless')) { + /** + * 按条件抛异常 + * + * @template TValue + * @template TException of \Throwable + * + * @param TValue $condition + * @param TException|class-string|string $exception + * @param mixed ...$parameters + * @return TValue + * + * @throws TException + */ + function throw_unless($condition, $exception, ...$parameters) + { + if (!$condition) { + throw (is_string($exception) ? new $exception(...$parameters) : $exception); + } + + return $condition; + } +} + +if (!function_exists('tap')) { + /** + * 对一个值调用给定的闭包,然后返回该值 + * + * @template TValue + * + * @param TValue $value + * @param (callable(TValue): mixed)|null $callback + * @return TValue + */ + function tap($value, $callback = null) + { + if (is_null($callback)) { + return $value; + } + + $callback($value); + + return $value; + } +} + +if (!function_exists('value')) { + /** + * Return the default value of the given value. + * + * @template TValue + * + * @param TValue|\Closure(): TValue $value + * @return TValue + */ + function value($value) + { + return $value instanceof Closure ? $value() : $value; + } +} + +if (!function_exists('collect')) { + /** + * Create a collection from the given value. + * + * @param mixed $value + * @return Collection + */ + function collect($value = null) + { + return new Collection($value); + } +} + +if (!function_exists('data_fill')) { + /** + * Fill in data where it's missing. + * + * @param mixed $target + * @param string|array $key + * @param mixed $value + * @return mixed + */ + function data_fill(&$target, $key, $value) + { + return data_set($target, $key, $value, false); + } +} + +if (!function_exists('data_get')) { + /** + * Get an item from an array or object using "dot" notation. + * + * @param mixed $target + * @param string|array|int $key + * @param mixed $default + * @return mixed + */ + function data_get($target, $key, $default = null) + { + if (is_null($key)) { + return $target; + } + + $key = is_array($key) ? $key : explode('.', $key); + + while (!is_null($segment = array_shift($key))) { + if ('*' === $segment) { + if ($target instanceof Collection) { + $target = $target->all(); + } elseif (!is_array($target)) { + return value($default); + } + + $result = []; + + foreach ($target as $item) { + $result[] = data_get($item, $key); + } + + return in_array('*', $key) ? Arr::collapse($result) : $result; + } + + if (Arr::accessible($target) && Arr::exists($target, $segment)) { + $target = $target[$segment]; + } elseif (is_object($target) && isset($target->{$segment})) { + $target = $target->{$segment}; + } else { + return value($default); + } + } + + return $target; + } +} + +if (!function_exists('data_set')) { + /** + * Set an item on an array or object using dot notation. + * + * @param mixed $target + * @param string|array $key + * @param mixed $value + * @param bool $overwrite + * @return mixed + */ + function data_set(&$target, $key, $value, $overwrite = true) + { + $segments = is_array($key) ? $key : explode('.', $key); + + if (($segment = array_shift($segments)) === '*') { + if (!Arr::accessible($target)) { + $target = []; + } + + if ($segments) { + foreach ($target as &$inner) { + data_set($inner, $segments, $value, $overwrite); + } + } elseif ($overwrite) { + foreach ($target as &$inner) { + $inner = $value; + } + } + } elseif (Arr::accessible($target)) { + if ($segments) { + if (!Arr::exists($target, $segment)) { + $target[$segment] = []; + } + + data_set($target[$segment], $segments, $value, $overwrite); + } elseif ($overwrite || !Arr::exists($target, $segment)) { + $target[$segment] = $value; + } + } elseif (is_object($target)) { + if ($segments) { + if (!isset($target->{$segment})) { + $target->{$segment} = []; + } + + data_set($target->{$segment}, $segments, $value, $overwrite); + } elseif ($overwrite || !isset($target->{$segment})) { + $target->{$segment} = $value; + } + } else { + $target = []; + + if ($segments) { + data_set($target[$segment], $segments, $value, $overwrite); + } elseif ($overwrite) { + $target[$segment] = $value; + } + } + + return $target; + } +} + +if (!function_exists('trait_uses_recursive')) { + /** + * 获取一个trait里所有引用到的trait + * + * @param string $trait Trait + * @return array + */ + function trait_uses_recursive(string $trait): array + { + $traits = class_uses($trait); + foreach ($traits as $trait) { + $traits += trait_uses_recursive($trait); + } + + return $traits; + } +} + +if (!function_exists('class_basename')) { + /** + * 获取类名(不包含命名空间) + * + * @param mixed $class 类名 + * @return string + */ + function class_basename($class): string + { + $class = is_object($class) ? get_class($class) : $class; + return basename(str_replace('\\', '/', $class)); + } +} + +if (!function_exists('class_uses_recursive')) { + /** + *获取一个类里所有用到的trait,包括父类的 + * + * @param mixed $class 类名 + * @return array + */ + function class_uses_recursive($class): array + { + if (is_object($class)) { + $class = get_class($class); + } + + $results = []; + $classes = array_merge([$class => $class], class_parents($class)); + foreach ($classes as $class) { + $results += trait_uses_recursive($class); + } + + return array_unique($results); + } +} + +if (!function_exists('array_is_list')) { + /** + * 判断数组是否为list + * + * @param array $array 数据 + * @return bool + */ + function array_is_list(array $array): bool + { + return array_values($array) === $array; + } +} + +if (!function_exists('json_validate')) { + /** + * 判断是否为有效json数据 + * + * @param string $string 数据 + * @return bool + */ + function json_validate(string $string): bool + { + json_decode($string); + return json_last_error() === JSON_ERROR_NONE; + } +} \ No newline at end of file diff --git a/Server/vendor/topthink/think-helper/src/helper/Arr.php b/Server/vendor/topthink/think-helper/src/helper/Arr.php new file mode 100644 index 00000000..11caffa4 --- /dev/null +++ b/Server/vendor/topthink/think-helper/src/helper/Arr.php @@ -0,0 +1,657 @@ + +// +---------------------------------------------------------------------- + +namespace think\helper; + +use ArrayAccess; +use InvalidArgumentException; +use think\Collection; + +class Arr +{ + + /** + * Determine whether the given value is array accessible. + * + * @param mixed $value + * @return bool + */ + public static function accessible($value) + { + return is_array($value) || $value instanceof ArrayAccess; + } + + /** + * Add an element to an array using "dot" notation if it doesn't exist. + * + * @param array $array + * @param string $key + * @param mixed $value + * @return array + */ + public static function add($array, $key, $value) + { + if (is_null(static::get($array, $key))) { + static::set($array, $key, $value); + } + + return $array; + } + + /** + * Collapse an array of arrays into a single array. + * + * @param array $array + * @return array + */ + public static function collapse($array) + { + $results = []; + + foreach ($array as $values) { + if ($values instanceof Collection) { + $values = $values->all(); + } elseif (!is_array($values)) { + continue; + } + + $results = array_merge($results, $values); + } + + return $results; + } + + /** + * Cross join the given arrays, returning all possible permutations. + * + * @param array ...$arrays + * @return array + */ + public static function crossJoin(...$arrays) + { + $results = [[]]; + + foreach ($arrays as $index => $array) { + $append = []; + + foreach ($results as $product) { + foreach ($array as $item) { + $product[$index] = $item; + + $append[] = $product; + } + } + + $results = $append; + } + + return $results; + } + + /** + * Divide an array into two arrays. One with keys and the other with values. + * + * @param array $array + * @return array + */ + public static function divide($array) + { + return [array_keys($array), array_values($array)]; + } + + /** + * Flatten a multi-dimensional associative array with dots. + * + * @param array $array + * @param string $prepend + * @return array + */ + public static function dot($array, $prepend = '') + { + $results = []; + + foreach ($array as $key => $value) { + if (is_array($value) && !empty($value)) { + $results = array_merge($results, static::dot($value, $prepend . $key . '.')); + } else { + $results[$prepend . $key] = $value; + } + } + + return $results; + } + + /** + * Get all of the given array except for a specified array of keys. + * + * @param array $array + * @param array|string $keys + * @return array + */ + public static function except($array, $keys) + { + static::forget($array, $keys); + + return $array; + } + + /** + * Determine if the given key exists in the provided array. + * + * @param \ArrayAccess|array $array + * @param string|int $key + * @return bool + */ + public static function exists($array, $key) + { + if ($array instanceof ArrayAccess) { + return $array->offsetExists($key); + } + + return array_key_exists($key, $array); + } + + /** + * Return the first element in an array passing a given truth test. + * + * @param array $array + * @param callable|null $callback + * @param mixed $default + * @return mixed + */ + public static function first($array, ?callable $callback = null, $default = null) + { + if (is_null($callback)) { + if (empty($array)) { + return value($default); + } + + foreach ($array as $item) { + return $item; + } + } + + foreach ($array as $key => $value) { + if (call_user_func($callback, $value, $key)) { + return $value; + } + } + + return value($default); + } + + /** + * Return the last element in an array passing a given truth test. + * + * @param array $array + * @param callable|null $callback + * @param mixed $default + * @return mixed + */ + public static function last($array, ?callable $callback = null, $default = null) + { + if (is_null($callback)) { + return empty($array) ? value($default) : end($array); + } + + return static::first(array_reverse($array, true), $callback, $default); + } + + /** + * Flatten a multi-dimensional array into a single level. + * + * @param array $array + * @param int $depth + * @return array + */ + public static function flatten($array, $depth = INF) + { + $result = []; + + foreach ($array as $item) { + $item = $item instanceof Collection ? $item->all() : $item; + + if (!is_array($item)) { + $result[] = $item; + } elseif ($depth === 1) { + $result = array_merge($result, array_values($item)); + } else { + $result = array_merge($result, static::flatten($item, $depth - 1)); + } + } + + return $result; + } + + /** + * Remove one or many array items from a given array using "dot" notation. + * + * @param array $array + * @param array|string $keys + * @return void + */ + public static function forget(&$array, $keys) + { + $original = &$array; + + $keys = (array) $keys; + + if (count($keys) === 0) { + return; + } + + foreach ($keys as $key) { + // if the exact key exists in the top-level, remove it + if (static::exists($array, $key)) { + unset($array[$key]); + + continue; + } + + $parts = explode('.', $key); + + // clean up before each pass + $array = &$original; + + while (count($parts) > 1) { + $part = array_shift($parts); + + if (isset($array[$part]) && is_array($array[$part])) { + $array = &$array[$part]; + } else { + continue 2; + } + } + + unset($array[array_shift($parts)]); + } + } + + /** + * Get an item from an array using "dot" notation. + * + * @param \ArrayAccess|array $array + * @param string $key + * @param mixed $default + * @return mixed + */ + public static function get($array, $key, $default = null) + { + if (!static::accessible($array)) { + return value($default); + } + + if (is_null($key)) { + return $array; + } + + if (static::exists($array, $key)) { + return $array[$key]; + } + + if (strpos($key, '.') === false) { + return $array[$key] ?? value($default); + } + + foreach (explode('.', $key) as $segment) { + if (static::accessible($array) && static::exists($array, $segment)) { + $array = $array[$segment]; + } else { + return value($default); + } + } + + return $array; + } + + /** + * Check if an item or items exist in an array using "dot" notation. + * + * @param \ArrayAccess|array $array + * @param string|array $keys + * @return bool + */ + public static function has($array, $keys) + { + $keys = (array) $keys; + + if (!$array || $keys === []) { + return false; + } + + foreach ($keys as $key) { + $subKeyArray = $array; + + if (static::exists($array, $key)) { + continue; + } + + foreach (explode('.', $key) as $segment) { + if (static::accessible($subKeyArray) && static::exists($subKeyArray, $segment)) { + $subKeyArray = $subKeyArray[$segment]; + } else { + return false; + } + } + } + + return true; + } + + /** + * Determines if an array is associative. + * + * An array is "associative" if it doesn't have sequential numerical keys beginning with zero. + * + * @param array $array + * @return bool + */ + public static function isAssoc(array $array) + { + $keys = array_keys($array); + + return array_keys($keys) !== $keys; + } + + /** + * Get a subset of the items from the given array. + * + * @param array $array + * @param array|string $keys + * @return array + */ + public static function only($array, $keys) + { + return array_intersect_key($array, array_flip((array) $keys)); + } + + /** + * Pluck an array of values from an array. + * + * @param array $array + * @param string|array $value + * @param string|array|null $key + * @return array + */ + public static function pluck($array, $value, $key = null) + { + $results = []; + + [$value, $key] = static::explodePluckParameters($value, $key); + + foreach ($array as $item) { + $itemValue = data_get($item, $value); + + // If the key is "null", we will just append the value to the array and keep + // looping. Otherwise we will key the array using the value of the key we + // received from the developer. Then we'll return the final array form. + if (is_null($key)) { + $results[] = $itemValue; + } else { + $itemKey = data_get($item, $key); + + if (is_object($itemKey) && method_exists($itemKey, '__toString')) { + $itemKey = (string) $itemKey; + } + + $results[$itemKey] = $itemValue; + } + } + + return $results; + } + + /** + * Explode the "value" and "key" arguments passed to "pluck". + * + * @param string|array $value + * @param string|array|null $key + * @return array + */ + protected static function explodePluckParameters($value, $key) + { + $value = is_string($value) ? explode('.', $value) : $value; + + $key = is_null($key) || is_array($key) ? $key : explode('.', $key); + + return [$value, $key]; + } + + /** + * Push an item onto the beginning of an array. + * + * @param array $array + * @param mixed $value + * @param mixed $key + * @return array + */ + public static function prepend($array, $value, $key = null) + { + if (is_null($key)) { + array_unshift($array, $value); + } else { + $array = [$key => $value] + $array; + } + + return $array; + } + + /** + * Get a value from the array, and remove it. + * + * @param array $array + * @param string $key + * @param mixed $default + * @return mixed + */ + public static function pull(&$array, $key, $default = null) + { + $value = static::get($array, $key, $default); + + static::forget($array, $key); + + return $value; + } + + /** + * Get one or a specified number of random values from an array. + * + * @param array $array + * @param int|null $number + * @return mixed + * + * @throws \InvalidArgumentException + */ + public static function random($array, $number = null) + { + $requested = is_null($number) ? 1 : $number; + + $count = count($array); + + if ($requested > $count) { + throw new InvalidArgumentException( + "You requested {$requested} items, but there are only {$count} items available." + ); + } + + if (is_null($number)) { + return $array[array_rand($array)]; + } + + if ((int) $number === 0) { + return []; + } + + $keys = array_rand($array, $number); + + $results = []; + + foreach ((array) $keys as $key) { + $results[] = $array[$key]; + } + + return $results; + } + + /** + * Set an array item to a given value using "dot" notation. + * + * If no key is given to the method, the entire array will be replaced. + * + * @param array $array + * @param string $key + * @param mixed $value + * @return array + */ + public static function set(&$array, $key, $value) + { + if (is_null($key)) { + return $array = $value; + } + + $keys = explode('.', $key); + + while (count($keys) > 1) { + $key = array_shift($keys); + + // If the key doesn't exist at this depth, we will just create an empty array + // to hold the next value, allowing us to create the arrays to hold final + // values at the correct depth. Then we'll keep digging into the array. + if (!isset($array[$key]) || !is_array($array[$key])) { + $array[$key] = []; + } + + $array = &$array[$key]; + } + + $array[array_shift($keys)] = $value; + + return $array; + } + + /** + * Shuffle the given array and return the result. + * + * @param array $array + * @param int|null $seed + * @return array + */ + public static function shuffle($array, $seed = null) + { + if (is_null($seed)) { + shuffle($array); + } else { + srand($seed); + + usort($array, function () { + return rand(-1, 1); + }); + } + + return $array; + } + + /** + * Sort the array using the given callback or "dot" notation. + * + * @param array $array + * @param callable|string|null $callback + * @return array + */ + public static function sort($array, $callback = null) + { + return Collection::make($array)->sort($callback)->all(); + } + + /** + * Recursively sort an array by keys and values. + * + * @param array $array + * @return array + */ + public static function sortRecursive($array) + { + foreach ($array as &$value) { + if (is_array($value)) { + $value = static::sortRecursive($value); + } + } + + if (static::isAssoc($array)) { + ksort($array); + } else { + sort($array); + } + + return $array; + } + + /** + * Convert the array into a query string. + * + * @param array $array + * @return string + */ + public static function query($array) + { + return http_build_query($array, null, '&', PHP_QUERY_RFC3986); + } + + /** + * Filter the array using the given callback. + * + * @param array $array + * @param callable $callback + * @return array + */ + public static function where($array, callable $callback) + { + return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH); + } + + /** + * If the given value is not an array and not null, wrap it in one. + * + * @param mixed $value + * @return array + */ + public static function wrap($value) + { + if (is_null($value)) { + return []; + } + + return is_array($value) ? $value : [$value]; + } + + public static function mergeDeep(array ...$arrays): array + { + $result = []; + foreach ($arrays as $array) { + foreach ($array as $key => $value) { + if (isset($result[$key]) && is_array($result[$key]) && is_array($value)) { + $result[$key] = self::mergeDeep( + $result[$key], + $value + ); + } else { + $result[$key] = $value; + } + } + } + return $result; + } + + public static function flatMap(callable $fn, array $array): array + { + return array_merge(...array_map($fn, $array)); + } +} diff --git a/Server/vendor/topthink/think-helper/src/helper/Macroable.php b/Server/vendor/topthink/think-helper/src/helper/Macroable.php new file mode 100644 index 00000000..ade5db78 --- /dev/null +++ b/Server/vendor/topthink/think-helper/src/helper/Macroable.php @@ -0,0 +1,66 @@ + +// +---------------------------------------------------------------------- +namespace think\helper; + +use Closure; +use think\exception\FuncNotFoundException; + +trait Macroable +{ + /** + * 方法注入. + * + * @var Closure[] + */ + protected static $macro = []; + + /** + * 设置方法注入. + * + * @param string $method + * @param Closure $closure + * + * @return void + */ + public static function macro(string $method, Closure $closure) + { + static::$macro[$method] = $closure; + } + + /** + * 检查方法是否已经有注入 + * + * @param string $name + * @return bool + */ + public static function hasMacro(string $method) + { + return isset(static::$macro[$method]); + } + + public function __call($method, $args) + { + if (!isset(static::$macro[$method])) { + throw new FuncNotFoundException('method not exists: ' . static::class . '::' . $method . '()', "{static::class}::{$method}"); + } + + return call_user_func_array(static::$macro[$method]->bindTo($this, static::class), $args); + } + + public static function __callStatic($method, $args) + { + if (!isset(static::$macro[$method])) { + throw new FuncNotFoundException('method not exists: ' . static::class . '::' . $method . '()', "{static::class}::{$method}"); + } + + return call_user_func_array(static::$macro[$method]->bindTo(null, static::class), $args); + } +} diff --git a/Server/vendor/topthink/think-helper/src/helper/Str.php b/Server/vendor/topthink/think-helper/src/helper/Str.php new file mode 100644 index 00000000..61b34d89 --- /dev/null +++ b/Server/vendor/topthink/think-helper/src/helper/Str.php @@ -0,0 +1,234 @@ + +// +---------------------------------------------------------------------- +namespace think\helper; + +class Str +{ + + protected static $snakeCache = []; + + protected static $camelCache = []; + + protected static $studlyCache = []; + + /** + * 检查字符串中是否包含某些字符串 + * @param string $haystack + * @param string|array $needles + * @return bool + */ + public static function contains(string $haystack, $needles): bool + { + foreach ((array) $needles as $needle) { + if ('' != $needle && mb_strpos($haystack, $needle) !== false) { + return true; + } + } + + return false; + } + + /** + * 检查字符串是否以某些字符串结尾 + * + * @param string $haystack + * @param string|array $needles + * @return bool + */ + public static function endsWith(string $haystack, $needles): bool + { + foreach ((array) $needles as $needle) { + if ((string) $needle === static::substr($haystack, -static::length($needle))) { + return true; + } + } + + return false; + } + + /** + * 检查字符串是否以某些字符串开头 + * + * @param string $haystack + * @param string|array $needles + * @return bool + */ + public static function startsWith(string $haystack, $needles): bool + { + foreach ((array) $needles as $needle) { + if ('' != $needle && mb_strpos($haystack, $needle) === 0) { + return true; + } + } + + return false; + } + + /** + * 获取指定长度的随机字母数字组合的字符串 + * + * @param int $length + * @param int $type + * @param string $addChars + * @return string + */ + public static function random(int $length = 6, ?int $type = null, string $addChars = ''): string + { + $str = ''; + switch ($type) { + case 0: + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' . $addChars; + break; + case 1: + $chars = str_repeat('0123456789', 3); + break; + case 2: + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' . $addChars; + break; + case 3: + $chars = 'abcdefghijklmnopqrstuvwxyz' . $addChars; + break; + case 4: + $chars = "们以我到他会作时要动国产的一是工就年阶义发成部民可出能方进在了不和有大这主中人上为来分生对于学下级地个用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所二起政三好十战无农使性前等反体合斗路图把结第里正新开论之物从当两些还天资事队批点育重其思与间内去因件日利相由压员气业代全组数果期导平各基或月毛然如应形想制心样干都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流入接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极土少已根共直团统式转别造切九你取西持总料连任志观调七么山程百报更见必真保热委手改管处己将修支识病象几先老光专什六型具示复安带每东增则完风回南广劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单色坚据速防史拉世设达尔场织历花受求传口断况采精金界品判参层止边清至万确究书" . $addChars; + break; + default: + $chars = 'ABCDEFGHIJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789' . $addChars; + break; + } + if ($length > 10) { + $chars = $type == 1 ? str_repeat($chars, $length) : str_repeat($chars, 5); + } + if ($type != 4) { + $chars = str_shuffle($chars); + $str = substr($chars, 0, $length); + } else { + for ($i = 0; $i < $length; $i++) { + $str .= mb_substr($chars, floor(mt_rand(0, mb_strlen($chars, 'utf-8') - 1)), 1); + } + } + return $str; + } + + /** + * 字符串转小写 + * + * @param string $value + * @return string + */ + public static function lower(string $value): string + { + return mb_strtolower($value, 'UTF-8'); + } + + /** + * 字符串转大写 + * + * @param string $value + * @return string + */ + public static function upper(string $value): string + { + return mb_strtoupper($value, 'UTF-8'); + } + + /** + * 获取字符串的长度 + * + * @param string $value + * @return int + */ + public static function length(string $value): int + { + return mb_strlen($value); + } + + /** + * 截取字符串 + * + * @param string $string + * @param int $start + * @param int|null $length + * @return string + */ + public static function substr(string $string, int $start, ?int $length = null): string + { + return mb_substr($string, $start, $length, 'UTF-8'); + } + + /** + * 驼峰转下划线 + * + * @param string $value + * @param string $delimiter + * @return string + */ + public static function snake(string $value, string $delimiter = '_'): string + { + $key = $value; + + if (isset(static::$snakeCache[$key][$delimiter])) { + return static::$snakeCache[$key][$delimiter]; + } + + if (!ctype_lower($value)) { + $value = preg_replace('/\s+/u', '', ucwords($value)); + + $value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, $value)); + } + + return static::$snakeCache[$key][$delimiter] = $value; + } + + /** + * 下划线转驼峰(首字母小写) + * + * @param string $value + * @return string + */ + public static function camel(string $value): string + { + if (isset(static::$camelCache[$value])) { + return static::$camelCache[$value]; + } + + return static::$camelCache[$value] = lcfirst(static::studly($value)); + } + + /** + * 下划线转驼峰(首字母大写) + * + * @param string $value + * @return string + */ + public static function studly(string $value): string + { + $key = $value; + + if (isset(static::$studlyCache[$key])) { + return static::$studlyCache[$key]; + } + + $value = ucwords(str_replace(['-', '_'], ' ', $value)); + + return static::$studlyCache[$key] = str_replace(' ', '', $value); + } + + /** + * 转为首字母大写的标题格式 + * + * @param string $value + * @return string + */ + public static function title(string $value): string + { + return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8'); + } +} diff --git a/Server/vendor/topthink/think-helper/tests/ArrTest.php b/Server/vendor/topthink/think-helper/tests/ArrTest.php new file mode 100644 index 00000000..eac9d7c7 --- /dev/null +++ b/Server/vendor/topthink/think-helper/tests/ArrTest.php @@ -0,0 +1,372 @@ + 'ThinkPHP'], 'price', 100); + $this->assertSame(['name' => 'ThinkPHP', 'price' => 100], $array); + } + + public function testCrossJoin() + { + // Single dimension + $this->assertSame( + [[1, 'a'], [1, 'b'], [1, 'c']], + Arr::crossJoin([1], ['a', 'b', 'c']) + ); + // Square matrix + $this->assertSame( + [[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']], + Arr::crossJoin([1, 2], ['a', 'b']) + ); + // Rectangular matrix + $this->assertSame( + [[1, 'a'], [1, 'b'], [1, 'c'], [2, 'a'], [2, 'b'], [2, 'c']], + Arr::crossJoin([1, 2], ['a', 'b', 'c']) + ); + // 3D matrix + $this->assertSame( + [ + [1, 'a', 'I'], [1, 'a', 'II'], [1, 'a', 'III'], + [1, 'b', 'I'], [1, 'b', 'II'], [1, 'b', 'III'], + [2, 'a', 'I'], [2, 'a', 'II'], [2, 'a', 'III'], + [2, 'b', 'I'], [2, 'b', 'II'], [2, 'b', 'III'], + ], + Arr::crossJoin([1, 2], ['a', 'b'], ['I', 'II', 'III']) + ); + // With 1 empty dimension + $this->assertSame([], Arr::crossJoin([], ['a', 'b'], ['I', 'II', 'III'])); + $this->assertSame([], Arr::crossJoin([1, 2], [], ['I', 'II', 'III'])); + $this->assertSame([], Arr::crossJoin([1, 2], ['a', 'b'], [])); + // With empty arrays + $this->assertSame([], Arr::crossJoin([], [], [])); + $this->assertSame([], Arr::crossJoin([], [])); + $this->assertSame([], Arr::crossJoin([])); + // Not really a proper usage, still, test for preserving BC + $this->assertSame([[]], Arr::crossJoin()); + } + + public function testDivide() + { + [$keys, $values] = Arr::divide(['name' => 'ThinkPHP']); + $this->assertSame(['name'], $keys); + $this->assertSame(['ThinkPHP'], $values); + } + + public function testDot() + { + $array = Arr::dot(['foo' => ['bar' => 'baz']]); + $this->assertSame(['foo.bar' => 'baz'], $array); + $array = Arr::dot([]); + $this->assertSame([], $array); + $array = Arr::dot(['foo' => []]); + $this->assertSame(['foo' => []], $array); + $array = Arr::dot(['foo' => ['bar' => []]]); + $this->assertSame(['foo.bar' => []], $array); + } + + public function testExcept() + { + $array = ['name' => 'ThinkPHP', 'price' => 100]; + $array = Arr::except($array, ['price']); + $this->assertSame(['name' => 'ThinkPHP'], $array); + } + + public function testExists() + { + $this->assertTrue(Arr::exists([1], 0)); + $this->assertTrue(Arr::exists([null], 0)); + $this->assertTrue(Arr::exists(['a' => 1], 'a')); + $this->assertTrue(Arr::exists(['a' => null], 'a')); + $this->assertFalse(Arr::exists([1], 1)); + $this->assertFalse(Arr::exists([null], 1)); + $this->assertFalse(Arr::exists(['a' => 1], 0)); + } + + public function testFirst() + { + $array = [100, 200, 300]; + $value = Arr::first($array, function ($value) { + return $value >= 150; + }); + $this->assertSame(200, $value); + $this->assertSame(100, Arr::first($array)); + + $this->assertSame('default', Arr::first([], null, 'default')); + + $this->assertSame('default', Arr::first([], function () { + return false; + }, 'default')); + } + + public function testLast() + { + $array = [100, 200, 300]; + $last = Arr::last($array, function ($value) { + return $value < 250; + }); + $this->assertSame(200, $last); + $last = Arr::last($array, function ($value, $key) { + return $key < 2; + }); + $this->assertSame(200, $last); + $this->assertSame(300, Arr::last($array)); + } + + public function testFlatten() + { + // Flat arrays are unaffected + $array = ['#foo', '#bar', '#baz']; + $this->assertSame(['#foo', '#bar', '#baz'], Arr::flatten(['#foo', '#bar', '#baz'])); + // Nested arrays are flattened with existing flat items + $array = [['#foo', '#bar'], '#baz']; + $this->assertSame(['#foo', '#bar', '#baz'], Arr::flatten($array)); + // Flattened array includes "null" items + $array = [['#foo', null], '#baz', null]; + $this->assertSame(['#foo', null, '#baz', null], Arr::flatten($array)); + // Sets of nested arrays are flattened + $array = [['#foo', '#bar'], ['#baz']]; + $this->assertSame(['#foo', '#bar', '#baz'], Arr::flatten($array)); + // Deeply nested arrays are flattened + $array = [['#foo', ['#bar']], ['#baz']]; + $this->assertSame(['#foo', '#bar', '#baz'], Arr::flatten($array)); + // Nested arrays are flattened alongside arrays + $array = [new Collection(['#foo', '#bar']), ['#baz']]; + $this->assertSame(['#foo', '#bar', '#baz'], Arr::flatten($array)); + // Nested arrays containing plain arrays are flattened + $array = [new Collection(['#foo', ['#bar']]), ['#baz']]; + $this->assertSame(['#foo', '#bar', '#baz'], Arr::flatten($array)); + // Nested arrays containing arrays are flattened + $array = [['#foo', new Collection(['#bar'])], ['#baz']]; + $this->assertSame(['#foo', '#bar', '#baz'], Arr::flatten($array)); + // Nested arrays containing arrays containing arrays are flattened + $array = [['#foo', new Collection(['#bar', ['#zap']])], ['#baz']]; + $this->assertSame(['#foo', '#bar', '#zap', '#baz'], Arr::flatten($array)); + } + + public function testFlattenWithDepth() + { + // No depth flattens recursively + $array = [['#foo', ['#bar', ['#baz']]], '#zap']; + $this->assertSame(['#foo', '#bar', '#baz', '#zap'], Arr::flatten($array)); + // Specifying a depth only flattens to that depth + $array = [['#foo', ['#bar', ['#baz']]], '#zap']; + $this->assertSame(['#foo', ['#bar', ['#baz']], '#zap'], Arr::flatten($array, 1)); + $array = [['#foo', ['#bar', ['#baz']]], '#zap']; + $this->assertSame(['#foo', '#bar', ['#baz'], '#zap'], Arr::flatten($array, 2)); + } + + public function testGet() + { + $array = ['products.item' => ['price' => 100]]; + $this->assertSame(['price' => 100], Arr::get($array, 'products.item')); + $array = ['products' => ['item' => ['price' => 100]]]; + $value = Arr::get($array, 'products.item'); + $this->assertSame(['price' => 100], $value); + // Test null array values + $array = ['foo' => null, 'bar' => ['baz' => null]]; + $this->assertNull(Arr::get($array, 'foo', 'default')); + $this->assertNull(Arr::get($array, 'bar.baz', 'default')); + // Test null key returns the whole array + $array = ['foo', 'bar']; + $this->assertSame($array, Arr::get($array, null)); + // Test $array is empty and key is null + $this->assertSame([], Arr::get([], null)); + $this->assertSame([], Arr::get([], null, 'default')); + } + + public function testHas() + { + $array = ['products.item' => ['price' => 100]]; + $this->assertTrue(Arr::has($array, 'products.item')); + $array = ['products' => ['item' => ['price' => 100]]]; + $this->assertTrue(Arr::has($array, 'products.item')); + $this->assertTrue(Arr::has($array, 'products.item.price')); + $this->assertFalse(Arr::has($array, 'products.foo')); + $this->assertFalse(Arr::has($array, 'products.item.foo')); + $array = ['foo' => null, 'bar' => ['baz' => null]]; + $this->assertTrue(Arr::has($array, 'foo')); + $this->assertTrue(Arr::has($array, 'bar.baz')); + $array = ['foo', 'bar']; + $this->assertFalse(Arr::has($array, null)); + $this->assertFalse(Arr::has([], null)); + $array = ['products' => ['item' => ['price' => 100]]]; + $this->assertTrue(Arr::has($array, ['products.item'])); + $this->assertTrue(Arr::has($array, ['products.item', 'products.item.price'])); + $this->assertTrue(Arr::has($array, ['products', 'products'])); + $this->assertFalse(Arr::has($array, ['foo'])); + $this->assertFalse(Arr::has($array, [])); + $this->assertFalse(Arr::has($array, ['products.item', 'products.price'])); + $this->assertFalse(Arr::has([], [null])); + } + + public function testIsAssoc() + { + $this->assertTrue(Arr::isAssoc(['a' => 'a', 0 => 'b'])); + $this->assertTrue(Arr::isAssoc([1 => 'a', 0 => 'b'])); + $this->assertTrue(Arr::isAssoc([1 => 'a', 2 => 'b'])); + $this->assertFalse(Arr::isAssoc([0 => 'a', 1 => 'b'])); + $this->assertFalse(Arr::isAssoc(['a', 'b'])); + } + + public function testOnly() + { + $array = ['name' => 'ThinkPHP', 'price' => 100, 'orders' => 10]; + $array = Arr::only($array, ['name', 'price']); + $this->assertSame(['name' => 'ThinkPHP', 'price' => 100], $array); + } + + public function testPrepend() + { + $array = Arr::prepend(['one', 'two', 'three', 'four'], 'zero'); + $this->assertSame(['zero', 'one', 'two', 'three', 'four'], $array); + $array = Arr::prepend(['one' => 1, 'two' => 2], 0, 'zero'); + $this->assertSame(['zero' => 0, 'one' => 1, 'two' => 2], $array); + } + + public function testPull() + { + $array = ['name' => 'ThinkPHP', 'price' => 100]; + $name = Arr::pull($array, 'name'); + $this->assertSame('ThinkPHP', $name); + $this->assertSame(['price' => 100], $array); + // Only works on first level keys + $array = ['i@example.com' => 'Joe', 'jack@localhost' => 'Jane']; + $name = Arr::pull($array, 'i@example.com'); + $this->assertSame('Joe', $name); + $this->assertSame(['jack@localhost' => 'Jane'], $array); + // Does not work for nested keys + $array = ['emails' => ['i@example.com' => 'Joe', 'jack@localhost' => 'Jane']]; + $name = Arr::pull($array, 'emails.i@example.com'); + $this->assertNull($name); + $this->assertSame(['emails' => ['i@example.com' => 'Joe', 'jack@localhost' => 'Jane']], $array); + } + + public function testRandom() + { + $randomValue = Arr::random(['foo', 'bar', 'baz']); + $this->assertContains($randomValue, ['foo', 'bar', 'baz']); + $randomValues = Arr::random(['foo', 'bar', 'baz'], 1); + $this->assertIsArray($randomValues); + $this->assertCount(1, $randomValues); + $this->assertContains($randomValues[0], ['foo', 'bar', 'baz']); + $randomValues = Arr::random(['foo', 'bar', 'baz'], 2); + $this->assertIsArray($randomValues); + $this->assertCount(2, $randomValues); + $this->assertContains($randomValues[0], ['foo', 'bar', 'baz']); + $this->assertContains($randomValues[1], ['foo', 'bar', 'baz']); + } + + public function testSet() + { + $array = ['products' => ['item' => ['price' => 100]]]; + Arr::set($array, 'products.item.price', 200); + Arr::set($array, 'goods.item.price', 200); + $this->assertSame(['products' => ['item' => ['price' => 200]], 'goods' => ['item' => ['price' => 200]]], $array); + } + + public function testWhere() + { + $array = [100, '200', 300, '400', 500]; + $array = Arr::where($array, function ($value, $key) { + return is_string($value); + }); + $this->assertSame([1 => '200', 3 => '400'], $array); + } + + public function testWhereKey() + { + $array = ['10' => 1, 'foo' => 3, 20 => 2]; + $array = Arr::where($array, function ($value, $key) { + return is_numeric($key); + }); + $this->assertSame(['10' => 1, 20 => 2], $array); + } + + public function testForget() + { + $array = ['products' => ['item' => ['price' => 100]]]; + Arr::forget($array, null); + $this->assertSame(['products' => ['item' => ['price' => 100]]], $array); + $array = ['products' => ['item' => ['price' => 100]]]; + Arr::forget($array, []); + $this->assertSame(['products' => ['item' => ['price' => 100]]], $array); + $array = ['products' => ['item' => ['price' => 100]]]; + Arr::forget($array, 'products.item'); + $this->assertSame(['products' => []], $array); + $array = ['products' => ['item' => ['price' => 100]]]; + Arr::forget($array, 'products.item.price'); + $this->assertSame(['products' => ['item' => []]], $array); + $array = ['products' => ['item' => ['price' => 100]]]; + Arr::forget($array, 'products.final.price'); + $this->assertSame(['products' => ['item' => ['price' => 100]]], $array); + $array = ['shop' => ['cart' => [150 => 0]]]; + Arr::forget($array, 'shop.final.cart'); + $this->assertSame(['shop' => ['cart' => [150 => 0]]], $array); + $array = ['products' => ['item' => ['price' => ['original' => 50, 'taxes' => 60]]]]; + Arr::forget($array, 'products.item.price.taxes'); + $this->assertSame(['products' => ['item' => ['price' => ['original' => 50]]]], $array); + $array = ['products' => ['item' => ['price' => ['original' => 50, 'taxes' => 60]]]]; + Arr::forget($array, 'products.item.final.taxes'); + $this->assertSame(['products' => ['item' => ['price' => ['original' => 50, 'taxes' => 60]]]], $array); + $array = ['products' => ['item' => ['price' => 50], null => 'something']]; + Arr::forget($array, ['products.amount.all', 'products.item.price']); + $this->assertSame(['products' => ['item' => [], null => 'something']], $array); + // Only works on first level keys + $array = ['i@example.com' => 'Joe', 'i@thinkphp.com' => 'Jane']; + Arr::forget($array, 'i@example.com'); + $this->assertSame(['i@thinkphp.com' => 'Jane'], $array); + // Does not work for nested keys + $array = ['emails' => ['i@example.com' => ['name' => 'Joe'], 'jack@localhost' => ['name' => 'Jane']]]; + Arr::forget($array, ['emails.i@example.com', 'emails.jack@localhost']); + $this->assertSame(['emails' => ['i@example.com' => ['name' => 'Joe']]], $array); + } + + public function testWrap() + { + $string = 'a'; + $array = ['a']; + $object = new stdClass(); + $object->value = 'a'; + $this->assertSame(['a'], Arr::wrap($string)); + $this->assertSame($array, Arr::wrap($array)); + $this->assertSame([$object], Arr::wrap($object)); + } + + public function testMergeDeep() + { + $this->assertSame( + [ + 'a' => [ + 'c' => [2], + 'e' => 5, + 'f' => 4, + ], + 'x' => 3, + ], + Arr::mergeDeep( + [ + 'a' => [ + 'c' => [1], + 'e' => 5, + ], + 'x' => 4, + ], + [ + 'a' => [ + 'c' => [2], + 'f' => 4, + ], + 'x' => 3, + ] + ) + ); + } +} diff --git a/Server/vendor/topthink/think-helper/tests/CollectionTest.php b/Server/vendor/topthink/think-helper/tests/CollectionTest.php new file mode 100644 index 00000000..b69c1755 --- /dev/null +++ b/Server/vendor/topthink/think-helper/tests/CollectionTest.php @@ -0,0 +1,70 @@ + 'Hello']); + $this->assertSame(['name' => 'Hello', 'id' => 1], $c->merge(['id' => 1])->all()); + } + + public function testFirst() + { + $c = new Collection(['name' => 'Hello', 'age' => 25]); + + $this->assertSame('Hello', $c->first()); + } + + public function testLast() + { + $c = new Collection(['name' => 'Hello', 'age' => 25]); + + $this->assertSame(25, $c->last()); + } + + public function testToArray() + { + $c = new Collection(['name' => 'Hello', 'age' => 25]); + + $this->assertSame(['name' => 'Hello', 'age' => 25], $c->toArray()); + } + + public function testToJson() + { + $c = new Collection(['name' => 'Hello', 'age' => 25]); + + $this->assertSame(json_encode(['name' => 'Hello', 'age' => 25]), $c->toJson()); + $this->assertSame(json_encode(['name' => 'Hello', 'age' => 25]), (string) $c); + $this->assertSame(json_encode(['name' => 'Hello', 'age' => 25]), json_encode($c)); + } + + public function testSerialize() + { + $c = new Collection(['name' => 'Hello', 'age' => 25]); + + $sc = serialize($c); + $c = unserialize($sc); + + $this->assertSame(['name' => 'Hello', 'age' => 25], $c->all()); + } + + public function testGetIterator() + { + $c = new Collection(['name' => 'Hello', 'age' => 25]); + + $this->assertInstanceOf(\ArrayIterator::class, $c->getIterator()); + + $this->assertSame(['name' => 'Hello', 'age' => 25], $c->getIterator()->getArrayCopy()); + } + + public function testCount() + { + $c = new Collection(['name' => 'Hello', 'age' => 25]); + + $this->assertCount(2, $c); + } +} diff --git a/Server/vendor/topthink/think-helper/tests/StrTest.php b/Server/vendor/topthink/think-helper/tests/StrTest.php new file mode 100644 index 00000000..813ad4ce --- /dev/null +++ b/Server/vendor/topthink/think-helper/tests/StrTest.php @@ -0,0 +1,59 @@ +assertSame('fooBar', Str::camel('FooBar')); + $this->assertSame('fooBar', Str::camel('FooBar')); + $this->assertSame('fooBar', Str::camel('foo_bar')); + $this->assertSame('fooBar', Str::camel('_foo_bar')); + $this->assertSame('fooBar', Str::camel('_foo_bar_')); + } + + public function testStudly() + { + $this->assertSame('FooBar', Str::studly('fooBar')); + $this->assertSame('FooBar', Str::studly('_foo_bar')); + $this->assertSame('FooBar', Str::studly('_foo_bar_')); + $this->assertSame('FooBar', Str::studly('_foo_bar_')); + } + + public function testSnake() + { + $this->assertSame('think_p_h_p_framework', Str::snake('ThinkPHPFramework')); + $this->assertSame('think_php_framework', Str::snake('ThinkPhpFramework')); + $this->assertSame('think php framework', Str::snake('ThinkPhpFramework', ' ')); + $this->assertSame('think_php_framework', Str::snake('Think Php Framework')); + $this->assertSame('think_php_framework', Str::snake('Think Php Framework ')); + // ensure cache keys don't overlap + $this->assertSame('think__php__framework', Str::snake('ThinkPhpFramework', '__')); + $this->assertSame('think_php_framework_', Str::snake('ThinkPhpFramework_', '_')); + $this->assertSame('think_php_framework', Str::snake('think php Framework')); + $this->assertSame('think_php_frame_work', Str::snake('think php FrameWork')); + // prevent breaking changes + $this->assertSame('foo-bar', Str::snake('foo-bar')); + $this->assertSame('foo-_bar', Str::snake('Foo-Bar')); + $this->assertSame('foo__bar', Str::snake('Foo_Bar')); + $this->assertSame('żółtałódka', Str::snake('ŻółtaŁódka')); + } + + public function testTitle() + { + $this->assertSame('Welcome Back', Str::title('welcome back')); + } + + public function testRandom() + { + $this->assertIsString(Str::random(10)); + } + + public function testUpper() + { + $this->assertSame('USERNAME', Str::upper('username')); + $this->assertSame('USERNAME', Str::upper('userNaMe')); + } +} diff --git a/Server/vendor/topthink/think-helper/tests/TestCase.php b/Server/vendor/topthink/think-helper/tests/TestCase.php new file mode 100644 index 00000000..87f6cb34 --- /dev/null +++ b/Server/vendor/topthink/think-helper/tests/TestCase.php @@ -0,0 +1,13 @@ + + */ +class TestCase extends BaseTestCase +{ +} diff --git a/Server/vendor/topthink/think-queue/.gitignore b/Server/vendor/topthink/think-queue/.gitignore new file mode 100644 index 00000000..4aec7821 --- /dev/null +++ b/Server/vendor/topthink/think-queue/.gitignore @@ -0,0 +1,4 @@ +/vendor/ +/.idea/ +/composer.lock +/thinkphp/ diff --git a/Server/vendor/topthink/think-queue/LICENSE b/Server/vendor/topthink/think-queue/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/Server/vendor/topthink/think-queue/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Server/vendor/topthink/think-queue/README.md b/Server/vendor/topthink/think-queue/README.md new file mode 100644 index 00000000..35dead16 --- /dev/null +++ b/Server/vendor/topthink/think-queue/README.md @@ -0,0 +1,135 @@ +# think-queue for ThinkPHP5.1 + +## 安装 + +> composer require topthink/think-queue + +## 配置 + +> 配置文件位于 `config/queue.php` + +### 公共配置 + +``` +[ + 'connector'=>'sync' //驱动类型,可选择 sync(默认):同步执行,database:数据库驱动,redis:Redis驱动,topthink:Topthink驱动 + //或其他自定义的完整的类名 +] +``` + +### 驱动配置 +> 各个驱动的具体可用配置项在`think\queue\connector`目录下各个驱动类里的`options`属性中,写在上面的`queue`配置里即可覆盖 + + +## 使用 Database +> 创建如下数据表 + +``` +CREATE TABLE `prefix_jobs` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `queue` varchar(255) NOT NULL, + `payload` longtext NOT NULL, + `attempts` tinyint(3) unsigned NOT NULL, + `reserved` tinyint(3) unsigned NOT NULL, + `reserved_at` int(10) unsigned DEFAULT NULL, + `available_at` int(10) unsigned NOT NULL, + `created_at` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` + +## 创建任务类 +> 单模块项目推荐使用 `app\job` 作为任务类的命名空间 +> 多模块项目可用使用 `app\module\job` 作为任务类的命名空间 +> 也可以放在任意可以自动加载到的地方 + +任务类不需继承任何类,如果这个类只有一个任务,那么就只需要提供一个`fire`方法就可以了,如果有多个小任务,就写多个方法,下面发布任务的时候会有区别 +每个方法会传入两个参数 `think\queue\Job $job`(当前的任务对象) 和 `$data`(发布任务时自定义的数据) + +还有个可选的任务失败执行的方法 `failed` 传入的参数为`$data`(发布任务时自定义的数据) + +### 下面写两个例子 + +``` +namespace app\job; + +use think\queue\Job; + +class Job1{ + + public function fire(Job $job, $data){ + + //....这里执行具体的任务 + + if ($job->attempts() > 3) { + //通过这个方法可以检查这个任务已经重试了几次了 + } + + + //如果任务执行成功后 记得删除任务,不然这个任务会重复执行,直到达到最大重试次数后失败后,执行failed方法 + $job->delete(); + + // 也可以重新发布这个任务 + $job->release($delay); //$delay为延迟时间 + + } + + public function failed($data){ + + // ...任务达到最大重试次数后,失败了 + } + +} + +``` + +``` + +namespace app\lib\job; + +use think\queue\Job; + +class Job2{ + + public function task1(Job $job, $data){ + + + } + + public function task2(Job $job, $data){ + + + } + + public function failed($data){ + + + } + +} + +``` + + +## 发布任务 +> `think\Queue::push($job, $data = '', $queue = null)` 和 `think\Queue::later($delay, $job, $data = '', $queue = null)` 两个方法,前者是立即执行,后者是在`$delay`秒后执行 + +`$job` 是任务名 +单模块的,且命名空间是`app\job`的,比如上面的例子一,写`Job1`类名即可 +多模块的,且命名空间是`app\module\job`的,写`model/Job1`即可 +其他的需要些完整的类名,比如上面的例子二,需要写完整的类名`app\lib\job\Job2` +如果一个任务类里有多个小任务的话,如上面的例子二,需要用@+方法名`app\lib\job\Job2@task1`、`app\lib\job\Job2@task2` + +`$data` 是你要传到任务里的参数 + +`$queue` 队列名,指定这个任务是在哪个队列上执行,同下面监控队列的时候指定的队列名,可不填 + +## 监听任务并执行 + +> php think queue:listen + +> php think queue:work --daemon(不加--daemon为执行单个任务) + +两种,具体的可选参数可以输入命令加 --help 查看 + +>可配合supervisor使用,保证进程常驻 diff --git a/Server/vendor/topthink/think-queue/composer.json b/Server/vendor/topthink/think-queue/composer.json new file mode 100644 index 00000000..0ed3b4ea --- /dev/null +++ b/Server/vendor/topthink/think-queue/composer.json @@ -0,0 +1,30 @@ +{ + "name": "topthink/think-queue", + "description": "The ThinkPHP5 Queue Package", + "type": "think-extend", + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "license": "Apache-2.0", + "autoload": { + "psr-4": { + "think\\": "src" + }, + "files": [ + "src/common.php" + ] + }, + "require": { + "topthink/think-helper": ">=1.0.4", + "topthink/think-installer": "^2.0", + "topthink/framework": "5.1.*" + }, + "extra": { + "think-config": { + "queue": "src/config.php" + } + } +} diff --git a/Server/vendor/topthink/think-queue/src/Queue.php b/Server/vendor/topthink/think-queue/src/Queue.php new file mode 100644 index 00000000..531169f2 --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/Queue.php @@ -0,0 +1,49 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\helper\Str; +use think\queue\Connector; + +/** + * Class Queue + * @package think\queue + * + * @method static push($job, $data = '', $queue = null) + * @method static later($delay, $job, $data = '', $queue = null) + * @method static pop($queue = null) + * @method static marshal() + */ +class Queue +{ + /** @var Connector */ + protected static $connector; + + private static function buildConnector() + { + $options = \think\facade\Config::pull('queue'); + $type = !empty($options['connector']) ? $options['connector'] : 'Sync'; + + if (!isset(self::$connector)) { + + $class = false !== strpos($type, '\\') ? $type : '\\think\\queue\\connector\\' . Str::studly($type); + + self::$connector = new $class($options); + } + return self::$connector; + } + + public static function __callStatic($name, $arguments) + { + return call_user_func_array([self::buildConnector(), $name], $arguments); + } +} diff --git a/Server/vendor/topthink/think-queue/src/common.php b/Server/vendor/topthink/think-queue/src/common.php new file mode 100644 index 00000000..81b35d1d --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/common.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +\think\Console::addDefaultCommands([ + "think\\queue\\command\\Work", + "think\\queue\\command\\Restart", + "think\\queue\\command\\Listen", + "think\\queue\\command\\Subscribe" +]); + +if (!function_exists('queue')) { + + /** + * 添加到队列 + * @param $job + * @param string $data + * @param int $delay + * @param null $queue + */ + function queue($job, $data = '', $delay = 0, $queue = null) + { + if ($delay > 0) { + \think\Queue::later($delay, $job, $data, $queue); + } else { + \think\Queue::push($job, $data, $queue); + } + } +} diff --git a/Server/vendor/topthink/think-queue/src/config.php b/Server/vendor/topthink/think-queue/src/config.php new file mode 100644 index 00000000..9223ef61 --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/config.php @@ -0,0 +1,14 @@ + +// +---------------------------------------------------------------------- + +return [ + 'connector' => 'Sync' +]; diff --git a/Server/vendor/topthink/think-queue/src/queue/CallQueuedHandler.php b/Server/vendor/topthink/think-queue/src/queue/CallQueuedHandler.php new file mode 100644 index 00000000..0f1a627d --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/CallQueuedHandler.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue; + +class CallQueuedHandler +{ + + public function call(Job $job, array $data) + { + $command = unserialize($data['command']); + + call_user_func([$command, 'handle']); + + if (!$job->isDeletedOrReleased()) { + $job->delete(); + } + } + + public function failed(array $data) + { + $command = unserialize($data['command']); + + if (method_exists($command, 'failed')) { + $command->failed(); + } + } +} diff --git a/Server/vendor/topthink/think-queue/src/queue/Connector.php b/Server/vendor/topthink/think-queue/src/queue/Connector.php new file mode 100644 index 00000000..b0694372 --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/Connector.php @@ -0,0 +1,69 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue; + +use InvalidArgumentException; + +abstract class Connector +{ + protected $options = []; + + abstract public function push($job, $data = '', $queue = null); + + abstract public function later($delay, $job, $data = '', $queue = null); + + abstract public function pop($queue = null); + + public function marshal() + { + throw new \RuntimeException('pop queues not support for this type'); + } + + protected function createPayload($job, $data = '', $queue = null) + { + if (is_object($job)) { + $payload = json_encode([ + 'job' => 'think\queue\CallQueuedHandler@call', + 'data' => [ + 'commandName' => get_class($job), + 'command' => serialize(clone $job), + ], + ]); + } else { + $payload = json_encode($this->createPlainPayload($job, $data)); + } + + if (JSON_ERROR_NONE !== json_last_error()) { + throw new InvalidArgumentException('Unable to create payload: ' . json_last_error_msg()); + } + + return $payload; + } + + protected function createPlainPayload($job, $data) + { + return ['job' => $job, 'data' => $data]; + } + + protected function setMeta($payload, $key, $value) + { + $payload = json_decode($payload, true); + $payload[$key] = $value; + $payload = json_encode($payload); + + if (JSON_ERROR_NONE !== json_last_error()) { + throw new InvalidArgumentException('Unable to create payload: ' . json_last_error_msg()); + } + + return $payload; + } +} diff --git a/Server/vendor/topthink/think-queue/src/queue/Job.php b/Server/vendor/topthink/think-queue/src/queue/Job.php new file mode 100644 index 00000000..618769ea --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/Job.php @@ -0,0 +1,213 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue; + +use DateTime; +use think\facade\Env; + +abstract class Job +{ + + /** + * The job handler instance. + * @var mixed + */ + protected $instance; + + /** + * The name of the queue the job belongs to. + * @var string + */ + protected $queue; + + /** + * Indicates if the job has been deleted. + * @var bool + */ + protected $deleted = false; + + /** + * Indicates if the job has been released. + * @var bool + */ + protected $released = false; + + /** + * Fire the job. + * @return void + */ + abstract public function fire(); + + /** + * Delete the job from the queue. + * @return void + */ + public function delete() + { + $this->deleted = true; + } + + /** + * Determine if the job has been deleted. + * @return bool + */ + public function isDeleted() + { + return $this->deleted; + } + + /** + * Release the job back into the queue. + * @param int $delay + * @return void + */ + public function release($delay = 0) + { + $this->released = true; + } + + /** + * Determine if the job was released back into the queue. + * @return bool + */ + public function isReleased() + { + return $this->released; + } + + /** + * Determine if the job has been deleted or released. + * @return bool + */ + public function isDeletedOrReleased() + { + return $this->isDeleted() || $this->isReleased(); + } + + /** + * Get the number of times the job has been attempted. + * @return int + */ + abstract public function attempts(); + + /** + * Get the raw body string for the job. + * @return string + */ + abstract public function getRawBody(); + + /** + * Resolve and fire the job handler method. + * @param array $payload + * @return void + */ + protected function resolveAndFire(array $payload) + { + list($class, $method) = $this->parseJob($payload['job']); + + $this->instance = $this->resolve($class); + if ($this->instance) { + $this->instance->{$method}($this, $payload['data']); + } + } + + /** + * Parse the job declaration into class and method. + * @param string $job + * @return array + */ + protected function parseJob($job) + { + $segments = explode('@', $job); + + return count($segments) > 1 ? $segments : [$segments[0], 'fire']; + } + + /** + * Resolve the given job handler. + * @param string $name + * @return mixed + */ + protected function resolve($name) + { + if (strpos($name, '\\') === false) { + + if (strpos($name, '/') === false) { + $module = ''; + } else { + list($module, $name) = explode('/', $name, 2); + } + + $name = Env::get('app_namespace') . ($module ? '\\' . strtolower($module) : '') . '\\job\\' . $name; + } + if (class_exists($name)) { + return new $name(); + } + } + + /** + * Call the failed method on the job instance. + * @return void + */ + public function failed() + { + $payload = json_decode($this->getRawBody(), true); + + list($class, $method) = $this->parseJob($payload['job']); + + $this->instance = $this->resolve($class); + if ($this->instance && method_exists($this->instance, 'failed')) { + $this->instance->failed($payload['data']); + } + } + + /** + * Calculate the number of seconds with the given delay. + * @param \DateTime|int $delay + * @return int + */ + protected function getSeconds($delay) + { + if ($delay instanceof DateTime) { + return max(0, $delay->getTimestamp() - $this->getTime()); + } + + return (int) $delay; + } + + /** + * Get the current system time. + * @return int + */ + protected function getTime() + { + return time(); + } + + /** + * Get the name of the queued job class. + * @return string + */ + public function getName() + { + return json_decode($this->getRawBody(), true)['job']; + } + + /** + * Get the name of the queue the job belongs to. + * @return string + */ + public function getQueue() + { + return $this->queue; + } +} diff --git a/Server/vendor/topthink/think-queue/src/queue/Listener.php b/Server/vendor/topthink/think-queue/src/queue/Listener.php new file mode 100644 index 00000000..e8fcaa40 --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/Listener.php @@ -0,0 +1,164 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue; + +use Closure; +use think\Process; + +class Listener +{ + + /** + * @var string + */ + protected $commandPath; + + /** + * @var int + */ + protected $sleep = 3; + + /** + * @var int + */ + protected $maxTries = 0; + + /** + * @var string + */ + protected $workerCommand; + + /** + * @var \Closure|null + */ + protected $outputHandler; + + /** + * @param string $commandPath + */ + public function __construct($commandPath) + { + $this->commandPath = $commandPath; + $this->workerCommand = + '"' . PHP_BINARY . '" think queue:work --queue="%s" --delay=%s --memory=%s --sleep=%s --tries=%s'; + } + + /** + * @param string $queue + * @param string $delay + * @param string $memory + * @param int $timeout + * @return void + */ + public function listen($queue, $delay, $memory, $timeout = 60) + { + $process = $this->makeProcess($queue, $delay, $memory, $timeout); + + while (true) { + $this->runProcess($process, $memory); + } + } + + /** + * @param \Think\Process $process + * @param int $memory + */ + public function runProcess(Process $process, $memory) + { + $process->run(function ($type, $line) { + $this->handleWorkerOutput($type, $line); + }); + + if ($this->memoryExceeded($memory)) { + $this->stop(); + } + } + + /** + * @param string $queue + * @param int $delay + * @param int $memory + * @param int $timeout + * @return \think\Process + */ + public function makeProcess($queue, $delay, $memory, $timeout) + { + $string = $this->workerCommand; + $command = sprintf($string, $queue, $delay, $memory, $this->sleep, $this->maxTries); + + return new Process($command, $this->commandPath, null, null, $timeout); + } + + /** + * @param int $type + * @param string $line + * @return void + */ + protected function handleWorkerOutput($type, $line) + { + if (isset($this->outputHandler)) { + call_user_func($this->outputHandler, $type, $line); + } + } + + /** + * @param int $memoryLimit + * @return bool + */ + public function memoryExceeded($memoryLimit) + { + return (memory_get_usage() / 1024 / 1024) >= $memoryLimit; + } + + /** + * @return void + */ + public function stop() + { + die; + } + + /** + * @param \Closure $outputHandler + * @return void + */ + public function setOutputHandler(Closure $outputHandler) + { + $this->outputHandler = $outputHandler; + } + + /** + * @return int + */ + public function getSleep() + { + return $this->sleep; + } + + /** + * @param int $sleep + * @return void + */ + public function setSleep($sleep) + { + $this->sleep = $sleep; + } + + /** + * @param int $tries + * @return void + */ + public function setMaxTries($tries) + { + $this->maxTries = $tries; + } +} diff --git a/Server/vendor/topthink/think-queue/src/queue/Queueable.php b/Server/vendor/topthink/think-queue/src/queue/Queueable.php new file mode 100644 index 00000000..2a6ec4e8 --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/Queueable.php @@ -0,0 +1,46 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue; + +trait Queueable +{ + + /** @var string 队列名称 */ + public $queue; + + /** @var integer 延迟时间 */ + public $delay; + + /** + * 设置队列名 + * @param $queue + * @return $this + */ + public function queue($queue) + { + $this->queue = $queue; + + return $this; + } + + /** + * 设置延迟时间 + * @param $delay + * @return $this + */ + public function delay($delay) + { + $this->delay = $delay; + + return $this; + } +} diff --git a/Server/vendor/topthink/think-queue/src/queue/ShouldQueue.php b/Server/vendor/topthink/think-queue/src/queue/ShouldQueue.php new file mode 100644 index 00000000..cb49c12d --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/ShouldQueue.php @@ -0,0 +1,17 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue; + +interface ShouldQueue +{ + +} diff --git a/Server/vendor/topthink/think-queue/src/queue/Worker.php b/Server/vendor/topthink/think-queue/src/queue/Worker.php new file mode 100644 index 00000000..a008d3c9 --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/Worker.php @@ -0,0 +1,119 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue; + +use Exception; +use think\facade\Hook; +use think\Queue; + +class Worker +{ + + /** + * 执行下个任务 + * @param string $queue + * @param int $delay + * @param int $sleep + * @param int $maxTries + * @return array + */ + public function pop($queue = null, $delay = 0, $sleep = 3, $maxTries = 0) + { + + $job = $this->getNextJob($queue); + + if (!is_null($job)) { + Hook::listen('worker_before_process', $queue); + return $this->process($job, $maxTries, $delay); + } + + Hook::listen('worker_before_sleep', $queue); + $this->sleep($sleep); + + return ['job' => null, 'failed' => false]; + } + + /** + * 获取下个任务 + * @param string $queue + * @return Job + */ + protected function getNextJob($queue) + { + if (is_null($queue)) { + return Queue::pop(); + } + + foreach (explode(',', $queue) as $queue) { + if (!is_null($job = Queue::pop($queue))) { + return $job; + } + } + } + + /** + * Process a given job from the queue. + * @param \think\queue\Job $job + * @param int $maxTries + * @param int $delay + * @return array + * @throws Exception + */ + public function process(Job $job, $maxTries = 0, $delay = 0) + { + if ($maxTries > 0 && $job->attempts() > $maxTries) { + return $this->logFailedJob($job); + } + + try { + $job->fire(); + + return ['job' => $job, 'failed' => false]; + } catch (Exception $e) { + if (!$job->isDeleted()) { + $job->release($delay); + } + + throw $e; + } + } + + /** + * Log a failed job into storage. + * @param \Think\Queue\Job $job + * @return array + */ + protected function logFailedJob(Job $job) + { + if (!$job->isDeleted()) { + try { + $job->delete(); + $job->failed(); + } finally { + Hook::listen('queue_failed', $job); + } + } + + return ['job' => $job, 'failed' => true]; + } + + /** + * Sleep the script for a given number of seconds. + * @param int $seconds + * @return void + */ + public function sleep($seconds) + { + sleep($seconds); + } + +} diff --git a/Server/vendor/topthink/think-queue/src/queue/command/Listen.php b/Server/vendor/topthink/think-queue/src/queue/command/Listen.php new file mode 100644 index 00000000..3f6cd639 --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/command/Listen.php @@ -0,0 +1,60 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; +use think\queue\Listener; + +class Listen extends Command +{ + /** @var Listener */ + protected $listener; + + public function configure() + { + $this->setName('queue:listen') + ->addOption('queue', null, Option::VALUE_OPTIONAL, 'The queue to listen on', null) + ->addOption('delay', null, Option::VALUE_OPTIONAL, 'Amount of time to delay failed jobs', 0) + ->addOption('memory', null, Option::VALUE_OPTIONAL, 'The memory limit in megabytes', 128) + ->addOption('timeout', null, Option::VALUE_OPTIONAL, 'Seconds a job may run before timing out', 60) + ->addOption('sleep', null, Option::VALUE_OPTIONAL, 'Seconds to wait before checking queue for jobs', 3) + ->addOption('tries', null, Option::VALUE_OPTIONAL, 'Number of times to attempt a job before logging it failed', 0) + ->setDescription('Listen to a given queue'); + } + + public function initialize(Input $input, Output $output) + { + $this->listener = new Listener(getcwd()); + $this->listener->setSleep($input->getOption('sleep')); + $this->listener->setMaxTries($input->getOption('tries')); + + $this->listener->setOutputHandler(function ($type, $line) use ($output) { + $output->write($line); + }); + } + + public function execute(Input $input, Output $output) + { + $delay = $input->getOption('delay'); + + $memory = $input->getOption('memory'); + + $timeout = $input->getOption('timeout'); + + $queue = $input->getOption('queue') ?: 'default'; + + $this->listener->listen($queue, $delay, $memory, $timeout); + } +} diff --git a/Server/vendor/topthink/think-queue/src/queue/command/Restart.php b/Server/vendor/topthink/think-queue/src/queue/command/Restart.php new file mode 100644 index 00000000..6e9336b8 --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/command/Restart.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue\command; + +use think\console\Command; +use think\console\Input; +use think\console\Output; +use think\facade\Cache; + +class Restart extends Command +{ + public function configure() + { + $this->setName('queue:restart')->setDescription('Restart queue worker daemons after their current job'); + } + + public function execute(Input $input, Output $output) + { + Cache::set('think:queue:restart', time()); + $output->writeln("Broadcasting queue restart signal."); + } +} diff --git a/Server/vendor/topthink/think-queue/src/queue/command/Subscribe.php b/Server/vendor/topthink/think-queue/src/queue/command/Subscribe.php new file mode 100644 index 00000000..cf266ea1 --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/command/Subscribe.php @@ -0,0 +1,46 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue\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\Url; +use think\Queue; + +class Subscribe extends Command +{ + public function configure() + { + $this->setName('queue:subscribe') + ->setDescription('Subscribe a URL to an push queue') + ->addArgument('name', Argument::REQUIRED, 'name') + ->addArgument('url', Argument::REQUIRED, 'The URL to be subscribed.') + ->addArgument('queue', Argument::OPTIONAL, 'The URL to be subscribed.') + ->addOption('option', null, Option::VALUE_IS_ARRAY | Option::VALUE_OPTIONAL, 'the options'); + } + + public function execute(Input $input, Output $output) + { + + $url = $input->getArgument('url'); + if (!preg_match('/^https?:\/\//', $url)) { + $url = Url::build($url); + } + + Queue::subscribe($input->getArgument('name'), $url, $input->getArgument('queue'), $input->getOption('option')); + + $output->write('Queue subscriber added: ' . $input->getArgument('url') . ''); + } +} diff --git a/Server/vendor/topthink/think-queue/src/queue/command/Work.php b/Server/vendor/topthink/think-queue/src/queue/command/Work.php new file mode 100644 index 00000000..06fcc16b --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/command/Work.php @@ -0,0 +1,210 @@ + +// +---------------------------------------------------------------------- +namespace think\queue\command; + +use Exception; +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; +use think\exception\Handle; +use think\exception\ThrowableError; +use think\facade\Cache; +use think\facade\Config; +use think\facade\Hook; +use think\queue\Job; +use think\queue\Worker; +use Throwable; + +class Work extends Command +{ + + /** + * The queue worker instance. + * @var \think\queue\Worker + */ + protected $worker; + + protected function initialize(Input $input, Output $output) + { + $this->worker = new Worker(); + } + + protected function configure() + { + $this->setName('queue:work') + ->addOption('queue', null, Option::VALUE_OPTIONAL, 'The queue to listen on') + ->addOption('daemon', null, Option::VALUE_NONE, 'Run the worker in daemon mode') + ->addOption('delay', null, Option::VALUE_OPTIONAL, 'Amount of time to delay failed jobs', 0) + ->addOption('force', null, Option::VALUE_NONE, 'Force the worker to run even in maintenance mode') + ->addOption('memory', null, Option::VALUE_OPTIONAL, 'The memory limit in megabytes', 128) + ->addOption('sleep', null, Option::VALUE_OPTIONAL, 'Number of seconds to sleep when no job is available', 3) + ->addOption('tries', null, Option::VALUE_OPTIONAL, 'Number of times to attempt a job before logging it failed', 0) + ->setDescription('Process the next job on a queue'); + } + + /** + * Execute the console command. + * @param Input $input + * @param Output $output + * @return int|null|void + */ + public function execute(Input $input, Output $output) + { + $queue = $input->getOption('queue'); + + $delay = $input->getOption('delay'); + + $memory = $input->getOption('memory'); + + if ($input->getOption('daemon')) { + Hook::listen('worker_daemon_start', $queue); + $this->daemon( + $queue, $delay, $memory, + $input->getOption('sleep'), $input->getOption('tries') + ); + } else { + $response = $this->worker->pop($queue, $delay, $input->getOption('sleep'), $input->getOption('tries')); + $this->output($response); + } + } + + protected function output($response) + { + if (!is_null($response['job'])) { + /** @var Job $job */ + $job = $response['job']; + if ($response['failed']) { + $this->output->writeln('Failed: ' . $job->getName()); + } else { + $this->output->writeln('Processed: ' . $job->getName()); + } + } + } + + /** + * 启动一个守护进程执行任务. + * + * @param string $queue + * @param int $delay + * @param int $memory + * @param int $sleep + * @param int $maxTries + * @return array + */ + protected function daemon($queue = null, $delay = 0, $memory = 128, $sleep = 3, $maxTries = 0) + { + $lastRestart = $this->getTimestampOfLastQueueRestart(); + + while (true) { + $this->runNextJobForDaemon( + $queue, $delay, $sleep, $maxTries + ); + + if ($this->memoryExceeded($memory)) { + Hook::listen('worker_memory_exceeded', $queue); + $this->stop(); + } + + if ($this->queueShouldRestart($lastRestart)) { + Hook::listen('worker_queue_restart', $queue); + $this->stop(); + } + } + } + + /** + * 以守护进程的方式执行下个任务. + * + * @param string $queue + * @param int $delay + * @param int $sleep + * @param int $maxTries + * @return void + */ + protected function runNextJobForDaemon($queue, $delay, $sleep, $maxTries) + { + try { + $response = $this->worker->pop($queue, $delay, $sleep, $maxTries); + + $this->output($response); + } catch (Exception $e) { + $this->getExceptionHandler()->report($e); + } catch (Throwable $e) { + $this->getExceptionHandler()->report(new ThrowableError($e)); + } + } + + /** + * 获取上次重启守护进程的时间 + * + * @return int|null + */ + protected function getTimestampOfLastQueueRestart() + { + return Cache::get('think:queue:restart'); + } + + /** + * 检查是否要重启守护进程 + * + * @param int|null $lastRestart + * @return bool + */ + protected function queueShouldRestart($lastRestart) + { + return $this->getTimestampOfLastQueueRestart() != $lastRestart; + } + + /** + * 检查内存是否超出 + * @param int $memoryLimit + * @return bool + */ + protected function memoryExceeded($memoryLimit) + { + return (memory_get_usage() / 1024 / 1024) >= $memoryLimit; + } + + /** + * 获取异常处理实例 + * + * @return \think\exception\Handle + */ + protected function getExceptionHandler() + { + static $handle; + + if (!$handle) { + + if ($class = Config::get('exception_handle')) { + if (class_exists($class) && is_subclass_of($class, "\\think\\exception\\Handle")) { + $handle = new $class; + } + } + if (!$handle) { + $handle = new Handle(); + } + } + + return $handle; + } + + /** + * 停止执行任务的守护进程. + * @return void + */ + public function stop() + { + die; + } + +} diff --git a/Server/vendor/topthink/think-queue/src/queue/connector/Database.php b/Server/vendor/topthink/think-queue/src/queue/connector/Database.php new file mode 100644 index 00000000..868ee27e --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/connector/Database.php @@ -0,0 +1,169 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue\connector; + +use think\Db; +use think\queue\Connector; +use think\queue\job\Database as DatabaseJob; + +class Database extends Connector +{ + + protected $options = [ + 'expire' => 60, + 'default' => 'default', + 'table' => 'jobs' + ]; + + public function __construct(array $options) + { + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + } + + public function push($job, $data = '', $queue = null) + { + return $this->pushToDatabase(0, $queue, $this->createPayload($job, $data)); + } + + public function later($delay, $job, $data = '', $queue = null) + { + return $this->pushToDatabase($delay, $queue, $this->createPayload($job, $data)); + } + + public function pop($queue = null) + { + $queue = $this->getQueue($queue); + + if (!is_null($this->options['expire'])) { + $this->releaseJobsThatHaveBeenReservedTooLong($queue); + } + + if ($job = $this->getNextAvailableJob($queue)) { + $this->markJobAsReserved($job->id); + + Db::commit(); + + return new DatabaseJob($this, $job, $queue); + } + + Db::commit(); + } + + /** + * 重新发布任务 + * + * @param string $queue + * @param \StdClass $job + * @param int $delay + * @return mixed + */ + public function release($queue, $job, $delay) + { + return $this->pushToDatabase($delay, $queue, $job->payload, $job->attempts); + } + + /** + * Push a raw payload to the database with a given delay. + * + * @param \DateTime|int $delay + * @param string|null $queue + * @param string $payload + * @param int $attempts + * @return mixed + */ + protected function pushToDatabase($delay, $queue, $payload, $attempts = 0) + { + return Db::name($this->options['table'])->insert([ + 'queue' => $this->getQueue($queue), + 'payload' => $payload, + 'attempts' => $attempts, + 'reserved' => 0, + 'reserved_at' => null, + 'available_at' => time() + $delay, + 'created_at' => time(), + ]); + } + + /** + * 获取下个有效任务 + * + * @param string|null $queue + * @return \StdClass|null + */ + protected function getNextAvailableJob($queue) + { + Db::startTrans(); + + $job = Db::name($this->options['table']) + ->lock(true) + ->where('queue', $this->getQueue($queue)) + ->where('reserved', 0) + ->where('available_at', '<=', time()) + ->order('id', 'asc') + ->find(); + + return $job ? (object) $job : null; + } + + /** + * 标记任务正在执行. + * + * @param string $id + * @return void + */ + protected function markJobAsReserved($id) + { + Db::name($this->options['table'])->where('id', $id)->update([ + 'reserved' => 1, + 'reserved_at' => time(), + ]); + } + + /** + * 重新发布超时的任务 + * + * @param string $queue + * @return void + */ + protected function releaseJobsThatHaveBeenReservedTooLong($queue) + { + $expired = time() - $this->options['expire']; + + Db::name($this->options['table']) + ->where('queue', $this->getQueue($queue)) + ->where('reserved', 1) + ->where('reserved_at', '<=', $expired) + ->update([ + 'reserved' => 0, + 'reserved_at' => null, + 'attempts' => ['inc', 1], + ]); + } + + /** + * 删除任务 + * + * @param string $id + * @return void + */ + public function deleteReserved($id) + { + Db::name($this->options['table'])->delete($id); + } + + protected function getQueue($queue) + { + return $queue ?: $this->options['default']; + } +} diff --git a/Server/vendor/topthink/think-queue/src/queue/connector/Redis.php b/Server/vendor/topthink/think-queue/src/queue/connector/Redis.php new file mode 100644 index 00000000..c3b2b563 --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/connector/Redis.php @@ -0,0 +1,236 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue\connector; + +use Exception; +use think\helper\Str; +use think\queue\Connector; +use think\queue\job\Redis as RedisJob; + +class Redis extends Connector +{ + /** @var \Redis */ + protected $redis; + + protected $options = [ + 'expire' => 60, + 'default' => 'default', + 'host' => '127.0.0.1', + 'port' => 6379, + 'password' => '', + 'select' => 0, + 'timeout' => 0, + 'persistent' => false, + ]; + + public function __construct(array $options) + { + if (!extension_loaded('redis')) { + throw new Exception('redis扩展未安装'); + } + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + $func = $this->options['persistent'] ? 'pconnect' : 'connect'; + $this->redis = new \Redis; + $this->redis->$func($this->options['host'], $this->options['port'], $this->options['timeout']); + + if ('' != $this->options['password']) { + $this->redis->auth($this->options['password']); + } + + if (0 != $this->options['select']) { + $this->redis->select($this->options['select']); + } + } + + public function push($job, $data = '', $queue = null) + { + return $this->pushRaw($this->createPayload($job, $data), $queue); + } + + public function later($delay, $job, $data = '', $queue = null) + { + $payload = $this->createPayload($job, $data); + + $this->redis->zAdd($this->getQueue($queue) . ':delayed', time() + $delay, $payload); + } + + public function pop($queue = null) + { + $original = $queue ?: $this->options['default']; + + $queue = $this->getQueue($queue); + + $this->migrateExpiredJobs($queue . ':delayed', $queue, false); + + if (!is_null($this->options['expire'])) { + $this->migrateExpiredJobs($queue . ':reserved', $queue); + } + + $job = $this->redis->lPop($queue); + + if (false !== $job) { + $this->redis->zAdd($queue . ':reserved', time() + $this->options['expire'], $job); + + return new RedisJob($this, $job, $original); + } + } + + /** + * 重新发布任务 + * + * @param string $queue + * @param string $payload + * @param int $delay + * @param int $attempts + * @return void + */ + public function release($queue, $payload, $delay, $attempts) + { + $payload = $this->setMeta($payload, 'attempts', $attempts); + + $this->redis->zAdd($this->getQueue($queue) . ':delayed', time() + $delay, $payload); + } + + public function pushRaw($payload, $queue = null) + { + $this->redis->rPush($this->getQueue($queue), $payload); + + return json_decode($payload, true)['id']; + } + + protected function createPayload($job, $data = '', $queue = null) + { + $payload = $this->setMeta( + parent::createPayload($job, $data), 'id', $this->getRandomId() + ); + + return $this->setMeta($payload, 'attempts', 1); + } + + /** + * 删除任务 + * + * @param string $queue + * @param string $job + * @return void + */ + public function deleteReserved($queue, $job) + { + $this->redis->zRem($this->getQueue($queue) . ':reserved', $job); + } + + /** + * 移动延迟任务 + * + * @param string $from + * @param string $to + * @param bool $attempt + */ + public function migrateExpiredJobs($from, $to, $attempt = true) + { + $this->redis->watch($from); + + $jobs = $this->getExpiredJobs( + $from, $time = time() + ); + if (count($jobs) > 0) { + $this->transaction(function () use ($from, $to, $time, $jobs, $attempt) { + $this->removeExpiredJobs($from, $time); + $this->pushExpiredJobsOntoNewQueue($to, $jobs, $attempt); + }); + } + $this->redis->unwatch(); + } + + /** + * redis事务 + * @param \Closure $closure + */ + protected function transaction(\Closure $closure) + { + $this->redis->multi(); + try { + call_user_func($closure); + if (!$this->redis->exec()) { + $this->redis->discard(); + } + } catch (Exception $e) { + $this->redis->discard(); + } + } + + /** + * 获取所有到期任务 + * + * @param string $from + * @param int $time + * @return array + */ + protected function getExpiredJobs($from, $time) + { + return $this->redis->zRangeByScore($from, '-inf', $time); + } + + /** + * 删除过期任务 + * + * @param string $from + * @param int $time + * @return void + */ + protected function removeExpiredJobs($from, $time) + { + $this->redis->zRemRangeByScore($from, '-inf', $time); + } + + /** + * 重新发布到期任务 + * + * @param string $to + * @param array $jobs + * @param boolean $attempt + */ + protected function pushExpiredJobsOntoNewQueue($to, $jobs, $attempt = true) + { + if ($attempt) { + foreach ($jobs as &$job) { + $attempts = json_decode($job, true)['attempts']; + $job = $this->setMeta($job, 'attempts', $attempts + 1); + } + } + call_user_func_array([$this->redis, 'rPush'], array_merge([$to], $jobs)); + } + + /** + * 随机id + * + * @return string + */ + protected function getRandomId() + { + return Str::random(32); + } + + /** + * 获取队列名 + * + * @param string|null $queue + * @return string + */ + protected function getQueue($queue) + { + return 'queues:' . ($queue ?: $this->options['default']); + } +} diff --git a/Server/vendor/topthink/think-queue/src/queue/connector/Sync.php b/Server/vendor/topthink/think-queue/src/queue/connector/Sync.php new file mode 100644 index 00000000..2fbdedaa --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/connector/Sync.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue\connector; + +use Exception; +use think\queue\Connector; +use think\queue\job\Sync as SyncJob; +use Throwable; + +class Sync extends Connector +{ + + public function push($job, $data = '', $queue = null) + { + $queueJob = $this->resolveJob($this->createPayload($job, $data, $queue)); + + try { + set_time_limit(0); + $queueJob->fire(); + } catch (Exception $e) { + $queueJob->failed(); + + throw $e; + } catch (Throwable $e) { + $queueJob->failed(); + + throw $e; + } + + return 0; + } + + public function later($delay, $job, $data = '', $queue = null) + { + return $this->push($job, $data, $queue); + } + + public function pop($queue = null) + { + + } + + protected function resolveJob($payload) + { + return new SyncJob($payload); + } + +} diff --git a/Server/vendor/topthink/think-queue/src/queue/connector/Topthink.php b/Server/vendor/topthink/think-queue/src/queue/connector/Topthink.php new file mode 100644 index 00000000..732da241 --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/connector/Topthink.php @@ -0,0 +1,223 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue\connector; + +use think\exception\HttpException; +use think\facade\Request; +use think\queue\Connector; +use think\queue\job\Topthink as TopthinkJob; +use think\Response; + +class Topthink extends Connector +{ + protected $options = [ + 'token' => '', + 'project_id' => '', + 'protocol' => 'https', + 'host' => 'qns.topthink.com', + 'port' => 443, + 'api_version' => 1, + 'max_retries' => 3, + 'default' => 'default', + ]; + + /** @var Request */ + protected $request; + + protected $url; + + protected $curl = null; + + protected $last_status; + + protected $headers = []; + + public function __construct(array $options) + { + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + $this->url = "{$this->options['protocol']}://{$this->options['host']}:{$this->options['port']}/v{$this->options['api_version']}/"; + + $this->headers['Authorization'] = "Bearer {$this->options['token']}"; + } + + public function push($job, $data = '', $queue = null) + { + return $this->pushRaw(0, $queue, $this->createPayload($job, $data)); + } + + public function later($delay, $job, $data = '', $queue = null) + { + return $this->pushRaw($delay, $queue, $this->createPayload($job, $data)); + } + + public function release($queue, $job, $delay) + { + return $this->pushRaw($delay, $queue, $job->payload, $job->attempts); + } + + public function marshal() + { + $job = new TopthinkJob($this, $this->marshalPushedJob(), Request::header('topthink-message-queue')); + if (Request::header('topthink-message-status') == 'success') { + $job->fire(); + } else { + $job->failed(); + } + return new Response('OK'); + } + + public function pushRaw($delay, $queue, $payload, $attempts = 0) + { + $queue_name = $this->getQueue($queue); + $queue = rawurlencode($queue_name); + $url = "project/{$this->options['project_id']}/queue/{$queue}/message"; + $message = [ + 'payload' => $payload, + 'attempts' => $attempts, + 'delay' => $delay, + ]; + + return $this->apiCall('POST', $url, $message)->id; + } + + public function deleteMessage($queue, $id) + { + $queue = rawurlencode($queue); + $url = "project/{$this->options['project_id']}/queue/{$queue}/message/{$id}"; + return $this->apiCall('DELETE', $url); + } + + protected function apiCall($type, $url, $params = []) + { + $url = "{$this->url}$url"; + + if (null == $this->curl) { + $this->curl = curl_init(); + } + + switch ($type = strtoupper($type)) { + case 'DELETE': + curl_setopt($this->curl, CURLOPT_URL, $url); + curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $type); + curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($params)); + break; + case 'PUT': + curl_setopt($this->curl, CURLOPT_URL, $url); + curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $type); + curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($params)); + break; + case 'POST': + curl_setopt($this->curl, CURLOPT_URL, $url); + curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $type); + curl_setopt($this->curl, CURLOPT_POST, true); + curl_setopt($this->curl, CURLOPT_POSTFIELDS, $params); + break; + case 'GET': + curl_setopt($this->curl, CURLOPT_POSTFIELDS, null); + curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $type); + curl_setopt($this->curl, CURLOPT_HTTPGET, true); + $url .= '?' . http_build_query($params); + curl_setopt($this->curl, CURLOPT_URL, $url); + break; + } + + curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true); + + $headers = []; + foreach ($this->headers as $k => $v) { + if ('Connection' == $k) { + $v = 'Close'; + } + $headers[] = "$k: $v"; + } + + curl_setopt($this->curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($this->curl, CURLOPT_CONNECTTIMEOUT, 10); + + return $this->callWithRetries(); + } + + protected function callWithRetries() + { + for ($retry = 0; $retry < $this->options['max_retries']; $retry++) { + $out = curl_exec($this->curl); + if (false === $out) { + $this->reportHttpError(0, curl_error($this->curl)); + } + $this->last_status = curl_getinfo($this->curl, CURLINFO_HTTP_CODE); + + if ($this->last_status >= 200 && $this->last_status < 300) { + return self::jsonDecode($out); + } elseif ($this->last_status >= 500) { + self::waitRandomInterval($retry); + } else { + $this->reportHttpError($this->last_status, $out); + } + } + $this->reportHttpError($this->last_status, "Service unavailable"); + return; + } + + protected static function jsonDecode($response) + { + $data = json_decode($response); + + $json_error = json_last_error(); + if (JSON_ERROR_NONE != $json_error) { + throw new \RuntimeException($json_error); + } + + return $data; + } + + protected static function waitRandomInterval($retry) + { + $max_delay = pow(4, $retry) * 100 * 1000; + usleep(rand(0, $max_delay)); + } + + protected function reportHttpError($status, $text) + { + throw new HttpException($status, "http error: {$status} | {$text}"); + } + + /** + * Marshal out the pushed job and payload. + * + * @return object + */ + protected function marshalPushedJob() + { + return (object) [ + 'id' => Request::header('topthink-message-id'), + 'payload' => Request::getContent(), + 'attempts' => Request::header('topthink-message-attempts'), + ]; + } + + public function __destruct() + { + if (null != $this->curl) { + curl_close($this->curl); + $this->curl = null; + } + } + + public function pop($queue = null) + { + throw new \RuntimeException('pop queues not support for this type'); + } +} diff --git a/Server/vendor/topthink/think-queue/src/queue/job/Database.php b/Server/vendor/topthink/think-queue/src/queue/job/Database.php new file mode 100644 index 00000000..06064b3e --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/job/Database.php @@ -0,0 +1,88 @@ + +// +---------------------------------------------------------------------- +namespace think\queue\job; + +use think\queue\Job; +use think\queue\connector\Database as DatabaseQueue; + +class Database extends Job +{ + /** + * The database queue instance. + * @var DatabaseQueue + */ + protected $database; + + /** + * The database job payload. + * @var Object + */ + protected $job; + + public function __construct(DatabaseQueue $database, $job, $queue) + { + $this->job = $job; + $this->queue = $queue; + $this->database = $database; + $this->job->attempts = $this->job->attempts + 1; + } + + /** + * 执行任务 + * @return void + */ + public function fire() + { + $this->resolveAndFire(json_decode($this->job->payload, true)); + } + + /** + * 删除任务 + * @return void + */ + public function delete() + { + parent::delete(); + $this->database->deleteReserved($this->job->id); + } + + /** + * 重新发布任务 + * @param int $delay + * @return void + */ + public function release($delay = 0) + { + parent::release($delay); + + $this->delete(); + + $this->database->release($this->queue, $this->job, $delay); + } + + /** + * 获取当前任务尝试次数 + * @return int + */ + public function attempts() + { + return (int) $this->job->attempts; + } + + /** + * Get the raw body string for the job. + * @return string + */ + public function getRawBody() + { + return $this->job->payload; + } +} diff --git a/Server/vendor/topthink/think-queue/src/queue/job/Redis.php b/Server/vendor/topthink/think-queue/src/queue/job/Redis.php new file mode 100644 index 00000000..10477ce6 --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/job/Redis.php @@ -0,0 +1,92 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue\job; + +use think\queue\Job; +use think\queue\connector\Redis as RedisQueue; + +class Redis extends Job +{ + + /** + * The redis queue instance. + * @var RedisQueue + */ + protected $redis; + + /** + * The database job payload. + * @var Object + */ + protected $job; + + public function __construct(RedisQueue $redis, $job, $queue) + { + $this->job = $job; + $this->queue = $queue; + $this->redis = $redis; + } + + /** + * Fire the job. + * @return void + */ + public function fire() + { + $this->resolveAndFire(json_decode($this->getRawBody(), true)); + } + + /** + * Get the number of times the job has been attempted. + * @return int + */ + public function attempts() + { + return json_decode($this->job, true)['attempts']; + } + + /** + * Get the raw body string for the job. + * @return string + */ + public function getRawBody() + { + return $this->job; + } + + /** + * 删除任务 + * + * @return void + */ + public function delete() + { + parent::delete(); + + $this->redis->deleteReserved($this->queue, $this->job); + } + + /** + * 重新发布任务 + * + * @param int $delay + * @return void + */ + public function release($delay = 0) + { + parent::release($delay); + + $this->delete(); + + $this->redis->release($this->queue, $this->job, $delay, $this->attempts() + 1); + } +} diff --git a/Server/vendor/topthink/think-queue/src/queue/job/Sync.php b/Server/vendor/topthink/think-queue/src/queue/job/Sync.php new file mode 100644 index 00000000..e81a758a --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/job/Sync.php @@ -0,0 +1,56 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue\job; + +use think\queue\Job; + +class Sync extends Job +{ + /** + * The queue message data. + * + * @var string + */ + protected $payload; + + public function __construct($payload) + { + $this->payload = $payload; + } + + /** + * Fire the job. + * @return void + */ + public function fire() + { + $this->resolveAndFire(json_decode($this->payload, true)); + } + + /** + * Get the number of times the job has been attempted. + * @return int + */ + public function attempts() + { + return 1; + } + + /** + * Get the raw body string for the job. + * @return string + */ + public function getRawBody() + { + return $this->payload; + } +} diff --git a/Server/vendor/topthink/think-queue/src/queue/job/Topthink.php b/Server/vendor/topthink/think-queue/src/queue/job/Topthink.php new file mode 100644 index 00000000..b98b59c1 --- /dev/null +++ b/Server/vendor/topthink/think-queue/src/queue/job/Topthink.php @@ -0,0 +1,85 @@ + +// +---------------------------------------------------------------------- + +namespace think\queue\job; + +use think\queue\Job; +use think\queue\connector\Topthink as TopthinkQueue; + +class Topthink extends Job +{ + + /** + * The Iron queue instance. + * + * @var TopthinkQueue + */ + protected $topthink; + + /** + * The IronMQ message instance. + * + * @var object + */ + protected $job; + + public function __construct(TopthinkQueue $topthink, $job, $queue) + { + $this->topthink = $topthink; + $this->job = $job; + $this->queue = $queue; + $this->job->attempts = $this->job->attempts + 1; + } + + /** + * Fire the job. + * @return void + */ + public function fire() + { + $this->resolveAndFire(json_decode($this->job->payload, true)); + } + + /** + * Get the number of times the job has been attempted. + * @return int + */ + public function attempts() + { + return (int) $this->job->attempts; + } + + public function delete() + { + parent::delete(); + + $this->topthink->deleteMessage($this->queue, $this->job->id); + } + + public function release($delay = 0) + { + parent::release($delay); + + $this->delete(); + + $this->topthink->release($this->queue, $this->job, $delay); + } + + /** + * Get the raw body string for the job. + * @return string + */ + public function getRawBody() + { + return $this->job->payload; + } + +}