各类知识收集,PHP技术分享与解决方案各类知识收集,PHP技术分享与解决方案各类知识收集,PHP技术分享与解决方案

Str Tom,为分享PHP技术和解决方案,贡献一份自己的力量!
收藏本站(不迷路),每天更新好文章!
当前位置:首页 > CMS教程 > PHP

规则引擎RulerZ用法及实现原理(代码示例)

管理员 2023-09-05
PHP
122

规则引擎RulerZ用法及实现原理(代码示例)

内容导读

收集整理的这篇技术教程文章主要介绍了规则引擎RulerZ用法及实现原理(代码示例),小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含19717字,纯文字阅读大概需要29分钟

内容图文

本篇文章给大家带来的内容是关于规则引擎RulerZ用法及实现原理(代码示例),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。

废话不多说,rulerz的官方地址是:https://github.com/K-Phoen/ru...

注意,本例中只拿普通数组做例子进行分析

1. 简介

RulerZ是一个用php实现的composer依赖包,目的是实现一个数据过滤规则引擎。RulerZ不仅支持数组过滤,也支持一些市面上常见的ORM,如Eloquent、Doctrine等,也支持Solr搜索引擎。
这是一个缺少中文官方文档的开源包,当然由于star数比较少,可能作者也觉得没必要。

2.安装

在你的项目composer.json所在目录下运行:

composer require 'kphoen/rulerz'

3.使用 - 过滤

现有数组如下:

$players = [    ['pseudo' => 'Joe',   'fullname' => 'Joe la frite',             'gender' => 'M', 'points' => 2500],    ['pseudo' => 'Moe',   'fullname' => 'Moe, from the bar!',       'gender' => 'M', 'points' => 1230],    ['pseudo' => 'Alice', 'fullname' => 'Alice, from... you know.', 'gender' => 'F', 'points' => 9001],];

初始化引擎:

use RulerZCompilerCompiler;use RulerZTarget;use RulerZRulerZ;// compiler$compiler = Compiler::create();// RulerZ engine$rulerz = new RulerZ(    $compiler, [        new TargetNativeNative([ // 请注意,这里是添加目标编译器,处理数组类型的数据源时对应的是Native            'length' => 'strlen'        ]),    ]);

创建一条规则:

$rule = "gender = :gender and points > :min_points'

将参数和规则交给引擎分析。

$parameters = [    'min_points' => 30,    'gender'     => 'F',];$result = iterator_to_array(        $rulerz->filter($players, $rule, $parameters) // the parameters can be omitted if empty    );// result 是一个过滤后的数组array:1 [▼  0 => array:4 [▼    "pseudo" => "Alice"    "fullname" => "Alice, from... you know."    "gender" => "F"    "points" => 9001  ]]

4.使用 - 判断是否满足规则

$rulerz->satisfies($player, $rule, $parameters);// 返回布尔值,true表示满足

5.底层代码解读

下面,让我们看看从创建编译器开始,到最后出结果的过程中发生了什么。
1.Compiler::create();
这一步是实例化一个FileEvaluator类,这个类默认会将本地的系统临时目录当做下一步临时类文件读写所在目录,文件类里包含一个has()方法和一个write()方法。文件类如下:

<?phpdeclare(strict_types=1);namespace RulerZCompiler;class NativeFilesystem implements Filesystem{    public function has(string $filePath): bool    {        return file_exists($filePath);    }    public function write(string $filePath, string $content): void    {        file_put_contents($filePath, $content, LOCK_EX);    }}

2.初始化RulerZ引擎,new RulerZ()
先看一下RulerZ的构建方法:

    public function __construct(Compiler $compiler, array $compilationTargets = [])    {        $this->compiler = $compiler;        foreach ($compilationTargets as $targetCompiler) {            $this->registerCompilationTarget($targetCompiler);        }    }

这里的第一个参数,就是刚刚的编译器类,第二个是目标编译器类(实际处理数据源的),因为我们选择的是数组,所以这里的目标编译器是Native,引擎会将这个目标编译类放到自己的属性$compilationTargets

    public function registerCompilationTarget(CompilationTarget $compilationTarget): void    {        $this->compilationTargets[] = $compilationTarget;    }

3.运用filter或satisfies方法

这一点便是核心了。
以filter为例:

    public function filter($target, string $rule, array $parameters = [], array $executionContext = [])    {        $targetCompiler = $this->findTargetCompiler($target, CompilationTarget::MODE_FILTER);        $compilationContext = $targetCompiler->createCompilationContext($target);        $executor = $this->compiler->compile($rule, $targetCompiler, $compilationContext);        return $executor->filter($target, $parameters, $targetCompiler->getOperators()->getOperators(), new ExecutionContext($executionContext));    }

第一步会检查目标编译器是否支持筛选模式。
第二步创建编译上下文,这个一般统一是Context类实例

    public function createCompilationContext($target): Context    {        return new Context();    }

第三步,执行compiler的compile()方法

        public function compile(string $rule, CompilationTarget $target, Context $context): Executor    {        $context['rule_identifier'] = $this->getRuleIdentifier($target, $context, $rule);        $context['executor_classname'] = 'Executor_'.$context['rule_identifier'];        $context['executor_fqcn'] = 'RulerZCompiledExecutor\Executor_'.$context['rule_identifier'];        if (!class_exists($context['executor_fqcn'], false)) {            $compiler = function () use ($rule, $target, $context) {                return $this->compileToSource($rule, $target, $context);            };            $this->evaluator->evaluate($context['rule_identifier'], $compiler);        }        return new $context['executor_fqcn']();    }        protected function getRuleIdentifier(CompilationTarget $compilationTarget, Context $context, string $rule): string    {        return hash('crc32b', get_class($compilationTarget).$rule.$compilationTarget->getRuleIdentifierHint($rule, $context));    }            protected function compileToSource(string $rule, CompilationTarget $compilationTarget, Context $context): string    {        $ast = $this->parser->parse($rule);        $executorModel = $compilationTarget->compile($ast, $context);        $flattenedTraits = implode(PHP_EOL, array_map(function ($trait) {            return "t".'use \'.ltrim($trait, '\').';';        }, $executorModel->getTraits()));        $extraCode = '';        foreach ($executorModel->getCompiledData() as $key => $value) {            $extraCode .= sprintf('private $%s = %s;'.PHP_EOL, $key, var_export($value, true));        }        $commentedRule = str_replace(PHP_EOL, PHP_EOL.'    // ', $rule);        return <<<EXECUTORnamespace RulerZCompiledExecutor;use RulerZExecutorExecutor;class {$context['executor_classname']} implements Executor{    $flattenedTraits    $extraCode    // $commentedRule    protected function execute($target, array $operators, array $parameters)    {        return {$executorModel->getCompiledRule()};    }}EXECUTOR;    }

这段代码会依照crc13算法生成一个哈希串和Executor拼接作为执行器临时类的名称,并将执行器相关代码写进上文提到的临时目录中去。生成的代码如下:

// /private/var/folders/w_/sh4r42wn4_b650l3pc__fh7h0000gp/T/rulerz_executor_ff2800e8<?phpnamespace RulerZCompiledExecutor;use RulerZExecutorExecutor;class Executor_ff2800e8 implements Executor{        use RulerZExecutorArrayTargetFilterTrait;    use RulerZExecutorArrayTargetSatisfiesTrait;    use RulerZExecutorArrayTargetArgumentUnwrappingTrait;    // gender = :gender and points > :min_points and points > :min_points    protected function execute($target, array $operators, array $parameters)    {        return ($this->unwrapArgument($target["gender"]) == $parameters["gender"] && ($this->unwrapArgument($target["points"]) > $parameters["min_points"] && $this->unwrapArgument($target["points"]) > $parameters["min_points"]));    }}

这个临时类文件就是最后要执行过滤动作的类。
FilterTrait中的filter方法是首先被执行的,里面会根据execute返回的布尔值来判断,是否通过迭代器返回符合条件的行。
execute方法就是根据具体的参数和操作符挨个判断每行中对应的cell是否符合判断来返回true/false。

    public function filter($target, array $parameters, array $operators, ExecutionContext $context)    {        return IteratorTools::fromGenerator(function () use ($target, $parameters, $operators) {            foreach ($target as $row) {                $targetRow = is_array($row) ? $row : new ObjectContext($row);                if ($this->execute($targetRow, $operators, $parameters)) {                    yield $row;                }            }        });    }

satisfies和filter基本逻辑类似,只是最后satisfies是执行单条判断。

有一个问题,我们的编译器是如何知道我们设立的操作规则$rule的具体含义的,如何parse的?
这就涉及另一个问题了,抽象语法树(AST)。

Go further - 抽象语法树

我们都知道php zend引擎在解读代码的过程中有一个过程是语法和词法分析,这个过程叫做parser,中间会将代码转化为抽象语法树,这是引擎能够读懂代码的关键步骤。

同样,我们在写一条规则字符串的时候,代码如何能够明白我们写的是什么呢?那就是抽象语法树。

以上面的规则为例:

gender = :gender and points > :min_points

这里, =、and、>都是操作符,但是机器并不知道他们是操作符,也不知道其他字段是什么含义。

于是rulerz使用自己的语法模板。

首先是默认定义了几个操作符。

<?phpdeclare(strict_types=1);namespace RulerZTargetNative;use RulerZTargetOperatorsDefinitions;class NativeOperators{    public static function create(Definitions $customOperators): Definitions    {        $defaultInlineOperators = [            'and' => function ($a, $b) {                return sprintf('(%s && %s)', $a, $b);            },            'or' => function ($a, $b) {                return sprintf('(%s || %s)', $a, $b);            },            'not' => function ($a) {                return sprintf('!(%s)', $a);            },            '=' => function ($a, $b) {                return sprintf('%s == %s', $a, $b);            },            'is' => function ($a, $b) {                return sprintf('%s === %s', $a, $b);            },            '!=' => function ($a, $b) {                return sprintf('%s != %s', $a, $b);            },            '>' => function ($a, $b) {                return sprintf('%s > %s', $a, $b);            },            '>=' => function ($a, $b) {                return sprintf('%s >= %s', $a, $b);            },            '<' => function ($a, $b) {                return sprintf('%s < %s', $a, $b);            },            '<=' => function ($a, $b) {                return sprintf('%s <= %s', $a, $b);            },            'in' => function ($a, $b) {                return sprintf('in_array(%s, %s)', $a, $b);            },        ];        $defaultOperators = [            'sum' => function () {                return array_sum(func_get_args());            },        ];        $definitions = new Definitions($defaultOperators, $defaultInlineOperators);        return $definitions->mergeWith($customOperators);    }}

在RulerZParserParser中,有如下方法:

public function parse($rule){    if ($this->parser === null) {        $this->parser = CompilerLlk::load(            new FileRead(__DIR__.'/../Grammar.pp')        );    }    $this->nextParameterIndex = 0;    return $this->visit($this->parser->parse($rule));}

这里要解读一个核心语法文件:

//// Hoa////// @license//// New BSD License//// Copyright © 2007-2015, Ivan Enderlin. All rights reserved.//// Redistribution and use in source and binary forms, with or without// modification, are permitted provided that the following conditions are met://     * Redistributions of source code must retain the above copyright//       notice, this list of conditions and the following disclaimer.//     * Redistributions in binary form must reproduce the above copyright//       notice, this list of conditions and the following disclaimer in the//       documentation and/or other materials provided with the distribution.//     * Neither the name of the Hoa nor the names of its contributors may be//       used to endorse or promote products derived from this software without//       specific prior written permission.//// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE// POSSIBILITY OF SUCH DAMAGE.//// Inspired from HoaRulerGrammar.//// @author     Stéphane Py <stephane.py@hoa-project.net>// @author     Ivan Enderlin <ivan.enderlin@hoa-project.net>// @author     Kévin Gomez <contact@kevingomez.fr>// @copyright  Copyright © 2007-2015 Stéphane Py, Ivan Enderlin, Kévin Gomez.// @license    New BSD License%skip   space         s// Scalars.%token  true          (?i)true%token  false         (?i)false%token  null          (?i)null// Logical operators%token  not           (?i)notb%token  and           (?i)andb%token  or            (?i)orb%token  xor           (?i)xorb// Value%token  string        ("|')(.*?)(?<!\)1%token  float         -?d+.d+%token  integer       -?d+%token  parenthesis_  (%token _parenthesis   )%token  bracket_      [%token _bracket       ]%token  comma          ,%token  dot           .%token  positional_parameter ?%token  named_parameter      :[a-z-A-Z0-9_]+%token  identifier    [^s()[],.]+#expression:    logical_operation()logical_operation:    operation()    ( ( ::and:: #and | ::or:: #or | ::xor:: #xor ) logical_operation() )?operation:    operand() ( <identifier> logical_operation() #operation )?operand:    ::parenthesis_:: logical_operation() ::_parenthesis::  | value()parameter:    <positional_parameter>  | <named_parameter>value:    ::not:: logical_operation() #not  | <true> | <false> | <null> | <float> | <integer> | <string>  | parameter()  | variable()  | array_declaration()  | function_call()variable:    <identifier> ( object_access() #variable_access )*object_access:    ::dot:: <identifier> #attribute_access#array_declaration:    ::bracket_:: value() ( ::comma:: value() )* ::_bracket::#function_call:    <identifier> ::parenthesis_::    ( logical_operation() ( ::comma:: logical_operation() )* )?    ::_parenthesis::

上面Llk::load方法会加载这个基础语法内容并解析出片段tokens,tokens解析的逻辑就是正则匹配出我们需要的一些操作符和基础标识符,并将对应的正则表达式提取出来:

array:1 [▼  "default" => array:20 [▼    "skip" => "s"    "true" => "(?i)true"    "false" => "(?i)false"    "null" => "(?i)null"    "not" => "(?i)notb"    "and" => "(?i)andb"    "or" => "(?i)orb"    "xor" => "(?i)xorb"    "string" => "("|')(.*?)(?<!\)1"    "float" => "-?d+.d+"    "integer" => "-?d+"    "parenthesis_" => "("    "_parenthesis" => ")"    "bracket_" => "["    "_bracket" => "]"    "comma" => ","    "dot" => "."    "positional_parameter" => "?"    "named_parameter" => ":[a-z-A-Z0-9_]+"    "identifier" => "[^s()[],.]+"  ]]

这一步也会生成一个rawRules

array:10 [▼  "#expression" => " logical_operation()"  "logical_operation" => " operation() ( ( ::and:: #and | ::or:: #or | ::xor:: #xor ) logical_operation() )?"  "operation" => " operand() ( <identifier> logical_operation() #operation )?"  "operand" => " ::parenthesis_:: logical_operation() ::_parenthesis:: | value()"  "parameter" => " <positional_parameter> | <named_parameter>"  "value" => " ::not:: logical_operation() #not | <true> | <false> | <null> | <float> | <integer> | <string> | parameter() | variable() | array_declaration() | function_call( ?"  "variable" => " <identifier> ( object_access() #variable_access )*"  "object_access" => " ::dot:: <identifier> #attribute_access"  "#array_declaration" => " ::bracket_:: value() ( ::comma:: value() )* ::_bracket::"  "#function_call" => " <identifier> ::parenthesis_:: ( logical_operation() ( ::comma:: logical_operation() )* )? ::_parenthesis::"]

这个rawRules会通过analyzer类的analyzeRules方法解析替换里面的::表示的空位,根据$_ppLexemes属性的值,CompilerLlkLexer()词法解析器会将rawRules数组每一个元素解析放入双向链表栈(SplStack)中,然后再通过对该栈插入和删除操作,形成一个包含所有操作符和token实例的数组$rules。

array:54 [▼  0 => Concatenation {#64 ?}  "expression" => Concatenation {#65 ▼    #_name: "expression"    #_children: array:1 [▼      0 => 0    ]    #_nodeId: "#expression"    #_nodeOptions: []    #_defaultId: "#expression"    #_defaultOptions: []    #_pp: " logical_operation()"    #_transitional: false  }  2 => Token {#62 ?}  3 => Concatenation {#63 ▼    #_name: 3    #_children: array:1 [▼      0 => 2    ]    #_nodeId: "#and"    #_nodeOptions: []    #_defaultId: null    #_defaultOptions: []    #_pp: null    #_transitional: true  }  4 => Token {#68 ?}  5 => Concatenation {#69 ?}  6 => Token {#70 ?}  7 => Concatenation {#71 ?}  8 => Choice {#72 ?}  9 => Concatenation {#73 ?}  10 => Repetition {#74 ?}  "logical_operation" => Concatenation {#75 ?}  12 => Token {#66 ?}  13 => Concatenation {#67 ?}  14 => Repetition {#78 ?}  "operation" => Concatenation {#79 ?}  16 => Token {#76 ?}  17 => Token {#77 ?}  18 => Concatenation {#82 ?}  "operand" => Choice {#83 ?}  20 => Token {#80 ?}  21 => Token {#81 ▼    #_tokenName: "named_parameter"    #_namespace: null    #_regex: null    #_ast: null    #_value: null    #_kept: true    #_unification: -1    #_name: 21    #_children: null    #_nodeId: null    #_nodeOptions: []    #_defaultId: null    #_defaultOptions: []    #_pp: null    #_transitional: true  }  "parameter" => Choice {#86 ?}  23 => Token {#84 ?}  24 => Concatenation {#85 ?}  25 => Token {#89 ?}  26 => Token {#90 ?}  27 => Token {#91 ?}  28 => Token {#92 ?}  29 => Token {#93 ?}  30 => Token {#94 ?}  "value" => Choice {#95 ?}  32 => Token {#87 ?}  33 => Concatenation {#88 ?}  34 => Repetition {#98 ?}  "variable" => Concatenation {#99 ?}  36 => Token {#96 ?}  37 => Token {#97 ?}  "object_access" => Concatenation {#102 ?}  39 => Token {#100 ?}  40 => Token {#101 ?}  41 => Concatenation {#105 ?}  42 => Repetition {#106 ?}  43 => Token {#107 ?}  "array_declaration" => Concatenation {#108 ?}  45 => Token {#103 ?}  46 => Token {#104 ?}  47 => Token {#111 ?}  48 => Concatenation {#112 ?}  49 => Repetition {#113 ?}  50 => Concatenation {#114 ?}  51 => Repetition {#115 ?}  52 => Token {#116 ?}  "function_call" => Concatenation {#117 ?}]

然后返回HoaCompilerLlkParser实例,这个实例有一个parse方法,正是此方法构成了一个语法树。

public function parse($text, $rule = null, $tree = true)    {        $k = 1024;        if (isset($this->_pragmas['parser.lookahead'])) {            $k = max(0, intval($this->_pragmas['parser.lookahead']));        }        $lexer                = new Lexer($this->_pragmas);        $this->_tokenSequence = new IteratorBuffer(            $lexer->lexMe($text, $this->_tokens),            $k        );        $this->_tokenSequence->rewind();        $this->_errorToken = null;        $this->_trace      = [];        $this->_todo       = [];        if (false === array_key_exists($rule, $this->_rules)) {            $rule = $this->getRootRule();        }        $closeRule   = new RuleEkzit($rule, 0);        $openRule    = new RuleEntry($rule, 0, [$closeRule]);        $this->_todo = [$closeRule, $openRule];        do {            $out = $this->unfold();            if (null  !== $out &&                'EOF' === $this->_tokenSequence->current()['token']) {                break;            }            if (false === $this->backtrack()) {                $token  = $this->_errorToken;                if (null === $this->_errorToken) {                    $token = $this->_tokenSequence->current();                }                $offset = $token['offset'];                $line   = 1;                $column = 1;                if (!empty($text)) {                    if (0 === $offset) {                        $leftnl = 0;                    } else {                        $leftnl = strrpos($text, "n", -(strlen($text) - $offset) - 1) ?: 0;                    }                    $rightnl = strpos($text, "n", $offset);                    $line    = substr_count($text, "n", 0, $leftnl + 1) + 1;                    $column  = $offset - $leftnl + (0 === $leftnl);                    if (false !== $rightnl) {                        $text = trim(substr($text, $leftnl, $rightnl - $leftnl), "n");                    }                }                throw new CompilerExceptionUnexpectedToken(                    'Unexpected token "%s" (%s) at line %d and column %d:' .                    "n" . '%s' . "n" . str_repeat(' ', $column - 1) . '↑',                    0,                    [                        $token['value'],                        $token['token'],                        $line,                        $column,                        $text                    ],                    $line,                    $column                );            }        } while (true);        if (false === $tree) {            return true;        }        $tree = $this->_buildTree();        if (!($tree instanceof TreeNode)) {            throw new CompilerException(                'Parsing error: cannot build AST, the trace is corrupted.',                1            );        }        return $this->_tree = $tree;    }

我们得到的一个完整的语法树是这样的:

Rule {#120 ▼  #_root: Operator {#414 ▼    #_name: "and"    #_arguments: array:2 [▼      0 => Operator {#398 ▼        #_name: "="        #_arguments: array:2 [▼          0 => Context {#396 ▼            #_id: "gender"            #_dimensions: []          }          1 => Parameter {#397 ▼            -name: "gender"          }        ]        #_function: false        #_laziness: false        #_id: null        #_dimensions: []      }      1 => Operator {#413 ▼        #_name: "and"        #_arguments: array:2 [▼          0 => Operator {#401 ▼            #_name: ">"            #_arguments: array:2 [▼              0 => Context {#399 ?}              1 => Parameter {#400 ?}            ]            #_function: false            #_laziness: false            #_id: null            #_dimensions: []          }          1 => Operator {#412 ?}        ]        #_function: false        #_laziness: true        #_id: null        #_dimensions: []      }    ]    #_function: false    #_laziness: true    #_id: null    #_dimensions: []  }}

这里有根节点、子节点、操作符参数以及HoaRulerModelOperator实例。

这时$executorModel = $compilationTarget->compile($ast, $context);就可以通过NativeVisitor的visit方法对这个语法树进行访问和分析了。

这一步走的是visitOperator()

    /**     * {@inheritdoc}     */    public function visitOperator(ASTOperator $element, &$handle = null, $eldnah = null)    {        $operatorName = $element->getName();        // the operator does not exist at all, throw an error before doing anything else.        if (!$this->operators->hasInlineOperator($operatorName) && !$this->operators->hasOperator($operatorName)) {            throw new OperatorNotFoundException($operatorName, sprintf('Operator "%s" does not exist.', $operatorName));        }        // expand the arguments        $arguments = array_map(function ($argument) use (&$handle, $eldnah) {            return $argument->accept($this, $handle, $eldnah);        }, $element->getArguments());        // and either inline the operator call        if ($this->operators->hasInlineOperator($operatorName)) {            $callable = $this->operators->getInlineOperator($operatorName);            return call_user_func_array($callable, $arguments);        }        $inlinedArguments = empty($arguments) ? '' : ', '.implode(', ', $arguments);        // or defer it.        return sprintf('call_user_func($operators["%s"]%s)', $operatorName, $inlinedArguments);    }

返回的逻辑代码可以通过得到:

$executorModel->getCompiledRule()

以上就是规则引擎RulerZ用法及实现原理(代码示例)的详细内容,更多请关注Gxl网其它相关文章!

内容总结

以上是为您收集整理的规则引擎RulerZ用法及实现原理(代码示例)全部内容,希望文章能够帮你解决规则引擎RulerZ用法及实现原理(代码示例)所遇到的程序开发问题。 如果觉得技术教程内容还不错,欢迎将网站推荐给程序员好友。

内容备注

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

扫码关注

qrcode

QQ交谈

回顶部