溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

怎么構(gòu)建一個(gè)自己的Laravel包

發(fā)布時(shí)間:2022-12-01 10:47:58 來(lái)源:億速云 閱讀:113 作者:iii 欄目:編程語(yǔ)言

這篇“怎么構(gòu)建一個(gè)自己的Laravel包”文章的知識(shí)點(diǎn)大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細(xì),步驟清晰,具有一定的借鑒價(jià)值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來(lái)看看這篇“怎么構(gòu)建一個(gè)自己的Laravel包”文章吧。

使用您的包名稱(chēng)創(chuàng)建一個(gè)新目錄,然后在您選擇的代碼編輯器中打開(kāi)它,以便我們開(kāi)始設(shè)置。 我對(duì)任何新包做的第一件事是將其初始化為 git 存儲(chǔ)庫(kù),因此運(yùn)行以下 git 命令:

git init

現(xiàn)在我們有了一個(gè)可以使用的存儲(chǔ)庫(kù),我們將能夠?qū)?nèi)容提交到歷史版本,并允許在適當(dāng)?shù)臅r(shí)候?qū)ΠM(jìn)行版本控制。

創(chuàng)建一個(gè) PHP 包需要馬上做一件事:一個(gè) composer.json 文件,它會(huì)告訴 Packagist 這個(gè)包是什么以及它需要運(yùn)行什么。你可以使用命令行 Composer 工具或手動(dòng)創(chuàng)建 Composer 文件。我通常使用命令行 composer init,因?yàn)樗且环N交互式的設(shè)置方式;但是,我將顯示我的 Composer 文件開(kāi)頭的輸出,以便你可以看到結(jié)果:

{
  "name": "juststeveking/laravel-data-object-tools",
  "description": "A set of tools to make working with Data Transfer Objects easier in Laravel",
  "type": "library",
  "license": "MIT",
  "authors": [
    {
      "role": "Developer",
      "name": "Steve McDougall",
      "email": "juststevemcd@gmail.com",
      "homepage": "https://www.juststeveking.uk/"
    }
  ],
  "autoload": {
    "psr-4": {
      "JustSteveKing\\DataObjects\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "JustSteveKing\\DataObjects\\Tests\\": "tests/"
    }
  },
  "require": {
    "php": "^8.1"
  },
  "require-dev": {},
  "minimum-stability": "dev",
  "prefer-stable": true,
  "config": {
    "sort-packages": true,
    "preferred-install": "dist",
    "optimize-autoloader": true
  }
}

這是我的大多數(shù)包的基礎(chǔ)結(jié)構(gòu),無(wú)論是 Laravel 還是普通的 PHP 包,它以一種我已知并保持風(fēng)格一致的方式進(jìn)行設(shè)置。我們需要在包中添加一些支持文件才能開(kāi)始。首先,我們需要添加 .gitignore 文件,這樣我們就可以告訴版本控制我們不想提交哪些文件和目錄:

/vendor/
/.idea
composer.lock

這是我們要忽略的文件的開(kāi)始。我正在使用 PHPStorm,它將添加一個(gè)名為 .idea 的元目錄,其中包含我的 IDE 理解我的項(xiàng)目所需的所有信息——我不想提交版本控制。接下來(lái),我們需要添加一些 git 的屬性配置,以便版本控制知道如何處理我們的存儲(chǔ)庫(kù)。這稱(chēng)為.gitattributes

* text=auto
*.md diff=markdown
*.php diff=php
/.github export-ignore
/tests export-ignore
.editorconfig export-ignore
.gitattributes export-ignore
.gitignore export-ignore
CHANGELOG.md export-ignore
phpunit.xml export-ignore

創(chuàng)建版本時(shí),我們會(huì)告訴源代碼控制提供者我們想要忽略哪些文件以及如何處理差異。最后,我們的最后一個(gè)支持文件將是 .editorconfig,該文件告訴我們的代碼編輯器如何處理我們正在編寫(xiě)的文件:

root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml,json}]
indent_size = 2

現(xiàn)在我們有了版本控制的支持文件和編輯器,我們可以開(kāi)始考慮我們的包在依賴(lài)關(guān)系方面需要什么。我們的包將依賴(lài)哪些依賴(lài)項(xiàng),以及我們使用哪些版本?讓我們開(kāi)始吧。

當(dāng)我們正在構(gòu)建一個(gè) Laravel 包時(shí),我們首先需要的是 Laravel 支持包,所以使用以下 composer 命令安裝它:

composer require illuminate/support

現(xiàn)在可以著手做一些事情,來(lái)看一下包需要的代碼的第一個(gè)重要部分:服務(wù)提供者。服務(wù)提供者是所有 Laravel 包的關(guān)鍵部分,因?yàn)樗嬖V Laravel 如何加載包以及可用的包。首先,我們想讓 Laravel 知道我們有一個(gè)安裝后可以使用的控制臺(tái)命令。我已經(jīng)調(diào)用了我的服務(wù)提供商 PackageServiceProvider,因?yàn)槲蚁胂罅τ邢蓿也粫?huì)起名。如果您愿意,請(qǐng)隨意更改您自己的命名。我在 src/Providers 下添加了我的服務(wù)提供商,因?yàn)樗煜?Laravel 應(yīng)用程序。

declare(strict_types=1);

namespace JustSteveKing\DataObjects\Providers;

use Illuminate\Support\ServiceProvider;
use JustSteveKing\DataObjects\Console\Commands\DataTransferObjectMakeCommand;

final class PackageServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        if ($this->app->runningInConsole()) {
            $this->commands(
                commands: [
                    DataTransferObjectMakeCommand::class,
                ],
            );
        }
    }
}

我通常將我知道不希望擴(kuò)展的類(lèi)作為最終類(lèi),因?yàn)檫@樣做會(huì)改變我希望包的操作方式。你不需要這樣做。這是你需要為自己做出的判斷。所以我們現(xiàn)在注冊(cè)了一個(gè)命令。我們應(yīng)該考慮創(chuàng)建它。從命名中可以看出,它是一個(gè)將為我們生成其他類(lèi)的命令——與典型的工匠命令略有不同。

我創(chuàng)建了一個(gè)名為 DataTransferObjectMakeCommand 的類(lèi),它非常冗長(zhǎng),但解釋了它在 src/Console/Commands 內(nèi)部的作用。如你所見(jiàn),在創(chuàng)建這些類(lèi)時(shí),我嘗試反映 Laravel 開(kāi)發(fā)人員熟悉的目錄結(jié)構(gòu)。這樣做會(huì)使使用包變得更加容易。讓我們看一下這個(gè)命令的代碼:

declare(strict_types=1);

namespace JustSteveKing\DataObjects\Console\Commands;

use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;

final class DataTransferObjectMakeCommand extends GeneratorCommand
{
    protected $signature = "make:dto {name : The DTO Name}";

    protected $description = "Create a new DTO";

    protected $type = 'Data Transfer Object';

    protected function getStub(): string
    {
        $readonly = Str::contains(
            haystack: PHP_VERSION,
            needles: '8.2',
        );

        $file = $readonly ? 'dto-82.stub' : 'dto.stub';

        return __DIR__ . "/../../../stubs/{$file}";
    }

    protected function getDefaultNamespace($rootNamespace): string
    {
        return "{$rootNamespace}\\DataObjects";
    }
}

讓我們通過(guò)這個(gè)命令來(lái)了解我們正在創(chuàng)建什么。我們的命令想要擴(kuò)展GeneratorCommand,因?yàn)槲覀兿胍梢粋€(gè)新文件。理解這一點(diǎn)很有用,因?yàn)閹缀鯖](méi)有關(guān)于如何做到這一點(diǎn)的文檔。對(duì)于這個(gè)命令,我們唯一需要的是一個(gè)名為 getStub 的方法--該命令需要知道如何加載存根文件的位置以幫助生成文件。我在包的根目錄中創(chuàng)建了一個(gè)名為 stubs 的目錄,這是 Laravel 應(yīng)用程序熟悉的地方。您將在這里看到我正在檢查已安裝的 PHP 版本,以查看我們是否使用 PHP 8.2,如果是 - 我們希望加載正確的存根版本以利用只讀類(lèi)。現(xiàn)在發(fā)生這種情況的可能性非常低 - 但是,我們離我們并不遙遠(yuǎn)。這種方法有助于為特定的 PHP 版本生成文件,因此您可以確保支持您希望支持的每個(gè)版本。

最后,我已經(jīng)為我的 DTO 設(shè)置了默認(rèn)命名空間,所以我知道我希望它們放在哪里。畢竟我不想過(guò)度填充根命名空間。

先來(lái)快速了解一下這些存根文件,默認(rèn)的命名空間為 stub:

<?php

declare(strict_types=1);

namespace {{ namespace }};

use JustSteveKing\DataObjects\Contracts\DataObjectContract;

final class {{ class }} implements DataObjectContract
{
    public function __construct(
        //
    ) {}

    public function toArray(): array
    {
        return [];
    }
}

我們的 DTO 將實(shí)施一個(gè)契約來(lái)保證一致性——我喜歡盡可能多地使用這些類(lèi)。此外,我們的 DTO 類(lèi)是 final 類(lèi)。我們可能不想擴(kuò)展這個(gè)類(lèi),所以默認(rèn)情況下將其設(shè)為 final 是一種明智的做法。現(xiàn)在讓我們看一下 PHP 8.2 版本:

<?php

declare(strict_types=1);

namespace {{ namespace }};

use JustSteveKing\DataObjects\Contracts\DataObjectContract;

readonly class {{ class }} implements DataObjectContract
{
    public function __construct(
        //
    ) {}

    public function toArray(): array
    {
        return [];
    }
}

這里唯一的區(qū)別是我們將 DTO 類(lèi)設(shè)為只讀以利用該語(yǔ)言的新特性。

我們?nèi)绾螠y(cè)試這個(gè)?首先,我們要安裝一個(gè)測(cè)試包,以確保我們可以編寫(xiě)運(yùn)行此命令的測(cè)試 - 我將為此使用 pestPHP,使用 PHPUnit 將可以以非常相似的方式工作。

composer require pestphp/pest --dev --with-all-dependencies

此命令將要求您允許 Pest 使用 Composer 插件,因此如果您需要 Pest 插件進(jìn)行測(cè)試(例如并行測(cè)試),請(qǐng)確保您對(duì)此表示同意。接下來(lái),我們需要一個(gè)允許我們?cè)跍y(cè)試中使用 Laravel 的包,以確保我們的包有效地工作。這個(gè)包叫做 Testbench,是我在構(gòu)建 Laravel 包時(shí)使用的。

composer require --dev orchestra/testbench

在我們的包中初始化測(cè)試套件的最簡(jiǎn)單方法是使用 pesPHP 為我們初始化它。運(yùn)行以下控制臺(tái)命令:

./vendor/bin/pest --init

這將生成 phpunit.xml 文件和一個(gè) tests/Pest.php 文件,用于控制和擴(kuò)展 pest。首先,我喜歡對(duì) Pest 要使用的 PHPUnit 配置文件進(jìn)行一些更改。我喜歡添加以下選項(xiàng)以使我的測(cè)試更容易:

stopOnFailure 我設(shè)置為 true
cacheResults 我設(shè)置為 false

我這樣做是因?yàn)槿绻麥y(cè)試失敗,我想立即知道。越早的返回和失敗有助于我們構(gòu)建更有信心的東西。緩存結(jié)果可以加速你的包的測(cè)試。但是,我喜歡確保每次都從頭開(kāi)始運(yùn)行我的測(cè)試套件,以確保它按我的預(yù)期工作。

現(xiàn)在讓我們將注意力集中在一個(gè)默認(rèn)測(cè)試用例上,我們需要我們的包測(cè)試來(lái)運(yùn)行它。在 tests/PackageTestCase.php 下創(chuàng)建一個(gè)新文件,這樣我們就可以更輕松地控制我們的測(cè)試。

declare(strict_types=1);

namespace JustSteveKing\DataObjects\Tests;

use JustSteveKing\DataObjects\Providers\PackageServiceProvider;
use Orchestra\Testbench\TestCase;

class PackageTestCase extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [
            PackageServiceProvider::class,
        ];
    }
}

PackageTestCase 擴(kuò)展了測(cè)試平臺(tái)TestCase,因此我們可以從包中借用行為來(lái)構(gòu)建我們的測(cè)試套件。然后我們注冊(cè)我們的包服務(wù)提供者,以確保我們的包被加載到測(cè)試應(yīng)用程序中。

現(xiàn)在讓我們看看如何測(cè)試它。在我們編寫(xiě)測(cè)試之前,我們要確保我們測(cè)試的內(nèi)容涵蓋了包的當(dāng)前行為。到目前為止,我們的測(cè)試所做的只是提供一個(gè)命令,可以運(yùn)行該命令來(lái)創(chuàng)建一個(gè)新文件。我們的測(cè)試目錄結(jié)構(gòu)將反映我們的包結(jié)構(gòu),所以在 tests/Console/Commands/DataTransferObjectMakeCommandTest.php 下創(chuàng)建我們的第一個(gè)測(cè)試文件,然后開(kāi)始我們的第一個(gè)測(cè)試。

在我們編寫(xiě)第一個(gè)測(cè)試之前,我們需要編輯 tests/Pest.php 文件以確保我們的測(cè)試套件正確使用我們的 PackageTestCase。

declare(strict_types=1);

use JustSteveKing\DataObjects\Tests\PackageTestCase;

uses(PackageTestCase::class)->in(__DIR__);

首先,要確保我們的命令可以運(yùn)行并且運(yùn)行成功。所以添加以下測(cè)試:

declare(strict_types=1);

use JustSteveKing\DataObjects\Console\Commands\DataTransferObjectMakeCommand;

use function PHPUnit\Framework\assertTrue;

it('can run the command successfully', function () {
    $this
        ->artisan(DataTransferObjectMakeCommand::class, ['name' => 'Test'])
        ->assertSuccessful();
});

我們正在測(cè)試當(dāng)我們調(diào)用這個(gè)命令時(shí),運(yùn)行沒(méi)有錯(cuò)誤。如果您問(wèn)我,這是最關(guān)鍵的測(cè)試之一,如果它出錯(cuò),則意味著出現(xiàn)問(wèn)題。

既然我們知道我們的測(cè)試可以運(yùn)行,我們還想確保創(chuàng)建了類(lèi)。所以讓我們接下來(lái)編寫(xiě)這個(gè)測(cè)試:

declare(strict_types=1);

use Illuminate\Support\Facades\File;
use JustSteveKing\DataObjects\Console\Commands\DataTransferObjectMakeCommand;

use function PHPUnit\Framework\assertTrue;

it('create the data transfer object when called', function (string $class) {
    $this->artisan(
        DataTransferObjectMakeCommand::class,
        ['name' => $class],
    )->assertSuccessful();

    assertTrue(
        File::exists(
            path: app_path("DataObjects/$class.php"),
        ),
    );
})->with('classes');

這里我們使用 Pest Dataset 來(lái)運(yùn)行一些選項(xiàng),有點(diǎn)像 PHPUnit Data Provider。我們遍歷每個(gè)選項(xiàng)并調(diào)用我們的命令,斷言文件存在。我們現(xiàn)在知道可以將名稱(chēng)傳遞給我們的 artisan 命令并創(chuàng)建一個(gè) DTO 供我們?cè)趹?yīng)用程序中使用。

最后,我們想為我們的包構(gòu)建一個(gè) facade,以允許我們的 DTO 輕松水合。擁有 DTO 通常只是成功的一半,是的,我們可以向 DTO 本身添加一個(gè)方法來(lái)靜態(tài)調(diào)用 - 但我們可以大大簡(jiǎn)化這個(gè)過(guò)程。我們將通過(guò) Frank de Jonge 在他的 Eventsauce 包 中使用一個(gè)非常有用的包來(lái)促進(jìn)這一點(diǎn),稱(chēng)為「對(duì)象保濕劑」。請(qǐng)運(yùn)行以下 composer 命令安裝它:

composer require eventsauce/object-hydrator

是時(shí)候圍繞這個(gè)包構(gòu)建一個(gè)包裝器,以便我們可以很好地使用它,所以讓我們?cè)?src/Hydrator/Hydrate.php 下創(chuàng)建一個(gè)新類(lèi),如果需要,我們還將創(chuàng)建一個(gè)契約在任何時(shí)候交換實(shí)現(xiàn)。這將是src/Contracts/HydratorContract.php。讓我們從契約開(kāi)始,了解我們想要它做什么。

declare(strict_types=1);

namespace JustSteveKing\DataObjects\Contracts;

interface HydratorContract
{
    /**
     * @param class-string<DataObjectContract> $class
     * @param array $properties
     * @return DataObjectContract
     */
    public function fill(string $class, array $properties): DataObjectContract;
}

我們所需要的只是一種水合對(duì)象的方法,因此我們使用對(duì)象的類(lèi)名和一組屬性來(lái)返回一個(gè)數(shù)據(jù)對(duì)象。現(xiàn)在讓我們看一下實(shí)現(xiàn):

declare(strict_types=1);

namespace JustSteveKing\DataObjects\Hydrator;

use EventSauce\ObjectHydrator\ObjectMapperUsingReflection;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
use JustSteveKing\DataObjects\Contracts\HydratorContract;

class Hydrate implements HydratorContract
{
    public function __construct(
        private readonly ObjectMapperUsingReflection $mapper = new ObjectMapperUsingReflection(),
    ) {}

    public function fill(string $class, array $properties): DataObjectContract
    {
        return $this->mapper->hydrateObject(
            className: $class,
            payload: $properties,
        );
    }
}

我們有一個(gè)對(duì)象映射器傳遞給構(gòu)造函數(shù)或在構(gòu)造函數(shù)中創(chuàng)建 - 然后我們?cè)谔畛浞椒ㄖ惺褂盟?。然后填充方法使用映射器?lái)水合對(duì)象。它使用簡(jiǎn)單干凈,如果我們將來(lái)選擇使用不同的保濕器,可以輕松復(fù)制。但是,使用這種方式,我們希望將水化器綁定到容器中,以允許我們使用依賴(lài)注入來(lái)解決它。將以下內(nèi)容添加到 PackageServiceProvider 的頂部:

public array $bindings = [
    HydratorContract::class => Hydrate::class,
];

現(xiàn)在我們有了 hydrator,我們需要?jiǎng)?chuàng)建一個(gè) facade,以便我們可以在我們的應(yīng)用程序中很好地調(diào)用它。現(xiàn)在讓我們?cè)?src/Facades/Hydrator.php 下創(chuàng)建它

declare(strict_types=1);

namespace JustSteveKing\DataObjects\Facades;

use Illuminate\Support\Facades\Facade;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
use JustSteveKing\DataObjects\Hydrator\Hydrate;

/**
 * @method static DataObjectContract fill(string $class, array $properties)
 *
 * @see \JustSteveKing\DataObjects\Hydrator\Hydrate;
 */
final class Hydrator extends Facade
{
    /**
     * @return class-string
     */
    protected static function getFacadeAccessor(): string
    {
        return Hydrate::class;
    }
}

所以我們的外觀(guān)當(dāng)前返回的是 Hydrator 的事件實(shí)現(xiàn)-這意味著我們無(wú)法從容器中解決這個(gè)問(wèn)題,所以如果我們切換實(shí)現(xiàn),我們將需要更改 facade。不過(guò),就目前而言,這還不是什么大事。接下來(lái),我們需要將此別名添加到我們的文件中,以便 Laravel 在我們安裝軟件包時(shí)知道它。

"extra": {
  "laravel": {
    "providers": [
      "JustSteveKing\\DataObjects\\Providers\\PackageServiceProvider"
    ],
    "aliases": [
      "JustSteveKing\\DataObjects\\Facades\\Hydrator"
    ]
  }
},

現(xiàn)在我們已經(jīng)注冊(cè)了 Facade,我們需要測(cè)試它是否按預(yù)期工作。讓我們來(lái)看看如何測(cè)試它。在 tests/Facades/HydratorTest.php 下創(chuàng)建一個(gè)新的測(cè)試文件,讓我們開(kāi)始吧:

declare(strict_types=1);

use JustSteveKing\DataObjects\Facades\Hydrator;
use JustSteveKing\DataObjects\Tests\Stubs\Test;

it('can create a data transfer object', function (string $string) {
    expect(
        Hydrator::fill(
            class: Test::class,
            properties: ['name' => $string],
        ),
    )->toBeInstanceOf(Test::class)->toArray()->toEqual(['name' => $string]);
})->with('strings');

我們創(chuàng)建了一個(gè)名為 strings 的新數(shù)據(jù)集,它返回一個(gè)隨機(jī)字符串?dāng)?shù)組供我們使用。我們將它傳遞給我們的測(cè)試并嘗試在我們的 facade 上調(diào)用填充方法。傳入一個(gè)測(cè)試類(lèi),我們可以創(chuàng)建一組屬性來(lái)進(jìn)行水合。然后,當(dāng)我們?cè)?DTO 上調(diào)用 toArray 方法時(shí),我們會(huì)測(cè)試該實(shí)例是否已創(chuàng)建以及它是否符合我們的預(yù)期。我們可以使用反射 API 來(lái)確保為最終測(cè)試按預(yù)期創(chuàng)建 DTO。

it('creates our data transfer object as we would expect', function (string $string) {
    $test = Hydrator::fill(
        class: Test::class,
        properties: ['name' => $string],
    );

    $reflection = new ReflectionClass(
        objectOrClass: $test,
    );

    expect(
        $reflection->getProperty(
            name: 'name',
        )->isReadOnly()
    )->toBeTrue()->and(
        $reflection->getProperty(
            name: 'name',
        )->isPrivate(),
    )->toBeTrue()->and(
        $reflection->getMethod(
            name: 'toArray',
        )->hasReturnType(),
    )->toBeTrue();
})->with('strings');

我們現(xiàn)在可以確定我們的包按預(yù)期工作。我們需要做的最后一件事是關(guān)注代碼的質(zhì)量。在我的大多數(shù)包中,我喜歡確保編碼風(fēng)格和靜態(tài)分析都在運(yùn)行,這樣我就有了一個(gè)值得信賴(lài)的可靠包。讓我們從代碼樣式開(kāi)始。為此,我們將安裝一個(gè)名為 Laravel Pint 的相對(duì)較新的軟件包:

composer require --dev laravel/pint

我喜歡使用 PSR-12 作為我的代碼風(fēng)格,所以讓我們?cè)诎母夸浿袆?chuàng)建一個(gè) pint.json 以確保我們配置 pint 以運(yùn)行我們想要運(yùn)行的標(biāo)準(zhǔn):

{
  "preset": "psr12"
}

現(xiàn)在運(yùn)行 pint 命令來(lái)修復(fù)任何不符合 PSR-12 的代碼樣式問(wèn)題:

./vendor/bin/pint

最后,我們可以安裝 PHPStan,這樣我們就可以靜態(tài)分析我們的代碼,以確保我們盡可能?chē)?yán)格并與我們的類(lèi)型保持一致:

composer require --dev phpstan/phpstan

要配置 PHPStan,我們需要在包的根目錄中創(chuàng)建一個(gè) phpstan.neon 以了解正在使用的配置。

parameters:
    level: 9
    paths:
        - src

最后,我們可以運(yùn)行 PHPStan 來(lái)分析我們的代碼

./vendor/bin/phpstan analyse

如果一切順利,我們現(xiàn)在應(yīng)該會(huì)看到 [OK] No errors

以上就是關(guān)于“怎么構(gòu)建一個(gè)自己的Laravel包”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對(duì)大家有幫助,若想了解更多相關(guān)的知識(shí)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI