使用DTO在Laravel中简化API响应

创建自定义数据传输对象(DTO)的全面指南,以增强Laravel API集成的可读性、效率和可测试性

Laravel DTO

介绍

有效处理API响应对于集成第三方API非常重要。在之前的文章中,我讨论了如何使用Http facade设置简单的客户端和请求类。如果你还没有阅读过这篇文章,我建议你去看一下。

在此基础上,本文将为您详细介绍如何创建自定义数据传输对象(DTO),以将数据映射到API响应中。我将使用正在进行的Google Books API集成场景作为实际示例,使事情更容易理解。

将响应数据映射到DTO

首先,让我们来看一下从Google Books API获取搜索结果时的示例响应。为此,我调用了之前创建的QueryBooksByTitle动作,并搜索书籍"The Ferryman":

$response = app(QueryBooksByTitle::class)("The Ferryman");
dump($response->json());

这将输出以下JSON数据,我只选择了我想要追踪的字段:

{
    "kind": "books#volumes",
    "totalItems": 367,
    "items": [
        {
            ...
        },
        {
            ...
        },
        {
            "kind": "books#volume",
            "id": "dO5-EAAAQBAJ",
            "volumeInfo": {
                "title": "The Ferryman",
                "subtitle": "A Novel",
                "authors": [
                    "Justin Cronin"
                ],
                "publisher": "Doubleday Canada",
                "publishedDate": "2023-05-02",
                "description": "..."
            },
            ...
        },
        ...
    ]
}

现在我们知道了响应的格式,让我们创建必要的DTO来映射数据。让我们从BookListData开始,它可以是一个简单的PHP类。

<?php
namespace App\DataTransferObjects;
use Illuminate\Contracts\Support\Arrayable;
/**
 * 存储来自Google Books volumes API的顶层数据。
 */
readonly class BooksListData implements Arrayable
{
    public function __construct(
        public string $kind,
        public string $id,
        public int $totalItems,
    ) {
    }
    /**
     * 从数据数组创建类的新实例。
     */
    public static function fromArray(array $data): BooksListData
    {
        return new self(
            data_get($data, 'kind'),
            data_get($data, 'id'),
            data_get($data, 'totalItems'),
        );
    }
    /**
     * 实现Laravel的Arrayable接口,允许将对象序列化为数组。
     */
    public function toArray(): array
    {
        return [
            'kind' => $this->kind,
            'items' => $this->items,
            'totalItems' => $this->totalItems,
        ];
    }
}

创建完DTO后,我们可以更新之前文章中创建的QueryBooksByTitle动作。

<?php
namespace App\Actions;
use App\DataTransferObjects\BooksListData;
use App\Support\ApiRequest;
use App\Support\GoogleBooksApiClient;
use Illuminate\Http\Client\Response;
/**
 * QueryBooksByTitle类是一个用于从Google Books API查询书籍的动作。
 * 它提供了一个__invoke方法,接受一个标题并返回API的响应。
 */
class QueryBooksByTitle
{
    /**
     * 根据标题从Google Books API查询书籍,并返回BookListData。
     * 该方法创建一个GoogleBooksApiClient和一个ApiRequest,
     * 使用给定的标题作为'q'查询参数和'books'作为'printType'查询参数,
     * 并使用客户端发送请求,然后返回书籍列表数据。
     */
    public function __invoke(string $title): BooksListData
    {
        $client = app(GoogleBooksApiClient::class);
        $request = ApiRequest::get('volumes')
            ->setQuery('q', 'intitle:'.$title)
            ->setQuery('printType', 'books');
        $response = $client->send($request);
        return BooksListData::fromArray($response->json());
    }
}

Test the Response Data

我们可以创建一个测试来确保在调用该动作时返回BooksListData对象:

<?php
use App\Actions\QueryBooksByTitle;
use App\DataTransferObjects\BooksListData;
it('fetches books by title', function () {
    $title = 'The Lord of the Rings';
    $response = resolve(QueryBooksByTitle::class)($title);
    expect($response)->toBeInstanceOf(BooksListData::class);
});

你可能没有注意到,但上面的测试存在一个问题。我们正在访问Google Books API。这对于不经常运行的集成测试可能没问题,但在我们的Laravel测试中,应该修复这个问题。我们可以利用Http facade的功能来解决这个问题,因为我们的Client类是使用该facade构建的。

防止测试中的HTTP请求

我喜欢做的第一步是确保我的测试没有进行我没有预期的外部HTTP请求。我们可以将`Http::preventStrayRequests();`添加到Pest.php文件中。然后,在使用Http facade发出请求的任何测试中,除非我们模拟请求,否则会引发异常。

<?php
use Illuminate\Foundation\Testing\TestCase;
use Illuminate\Support\Facades\Http;
use Tests\CreatesApplication;
uses(
    TestCase::class,
    CreatesApplication::class,
)
    ->beforeEach(function () {
        Http::preventStrayRequests();
    })
    ->in('Feature');

如果再次运行QueryBooksByTitle测试,现在会出现一个失败的测试,显示以下错误信息:

RuntimeException: Attempted request to [https://www.googleapis.com/books/v1/volumes?key=XXXXXXXXXXXXX&q=intitle%3AThe%20Lord%20of%20the%20Rings&printType=books] without a matching fake.

现在,让我们使用Http facade来伪造响应。

<?php
use App\Actions\QueryBooksByTitle;
use App\DataTransferObjects\BooksListData;
use Illuminate\Support\Facades\Http;
it('fetches books by title', function () {
    $title = fake()->sentence();
    // 从Google Books API生成一个假响应。
    $responseData = [
        'kind' => 'books#volumes',
        'totalItems' => 1,
        'items' => [
            [
                'id' => fake()->uuid,
                'volumeInfo' => [
                    'title' => $title,
                    'subtitle' => fake()->sentence(),
                    'authors' => [fake()->name],
                    'publisher' => fake()->company(),
                    'publishedDate' => fake()->date(),
                    'description' => fake()->paragraphs(asText: true),
                    'pageCount' => fake()->numberBetween(100, 500),
                    'categories' => [fake()->word],
                    'imageLinks' => [
                        'thumbnail' => fake()->url(),
                    ],
                ],
            ],
        ],
    ];
    // 当客户端向Google Books API发送请求时,返回假响应。
    Http::fake(['https://www.googleapis.com/books/v1/*' => Http::response(
        body: $responseData,
        status: 200
    )]);
    $response = resolve(QueryBooksByTitle::class)($title);
    expect($response)->toBeInstanceOf(BooksListData::class);
    expect($response->items[0]['volumeInfo']['title'])->toBe($title);
});

现在运行测试,不再出现RuntimeException,因为我们使用Http::fake()方法伪造了请求。Http::fake()方法非常灵活,可以接受一个包含不同URL的项目数组。根据您的应用程序,您可以只使用'*'而不是完整的URL,甚至可以更具体地包括查询参数或其他动态URL数据。如果需要,甚至可以伪造请求序列。有关更多信息,请参阅Laravel文档。

这个测试效果很好,但仍然有一些改进的空间。

扩展数据传输对象(DTO)

首先,让我们再次看一下响应数据。将顶层的响应映射到BooksListData对象中是不错的,但使用items[0]['volumeInfo']['title']并不方便开发人员,并且IDE无法提供任何类型的自动完成。为了解决这个问题,我们需要创建更多的DTOs。通常最容易从需要映射的最低级别的项开始。在这种情况下,需要映射响应中的imageLinks数据。查看来自Google Books的响应,似乎该数据可能包含缩略图和小缩略图属性。我们将创建一个ImageLinksData对象来映射这部分数据。

namespace App\DataTransferObjects;
use Illuminate\Contracts\Support\Arrayable;
readonly class ImageLinksData implements Arrayable
{
    public function __construct(
        public ?string $thumbnail = null,
        public ?string $smallThumbnail = null,
    ) {
    }
    public static function fromArray(array $data): self
    {
        return new self(
            thumbnail: data_get($data, 'thumbnail'),
            smallThumbnail: data_get($data, 'smallThumbnail'),
        );
    }
    public function toArray(): array
    {
        return [
            'thumbnail' => $this->thumbnail,
            'smallThumbnail' => $this->smallThumbnail,
        ];
    }
}
#从那里,往上走一级,我们有VolumeInfoData对象。
namespace App\DataTransferObjects;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection;
readonly class VolumeInfoData implements Arrayable
{
    public function __construct(
        public string $title,
        public string $subtitle,
        // 使用集合而不是数组是个人偏好。
        // 这使得处理数据稍微更容易一些。
        /** @var Collection<int, string> */
        public Collection $authors,
        public string $publisher,
        public string $publishedDate,
        public string $description,
        public int $pageCount,
        /** @var Collection<int, string> */
        public Collection $categories,
        // 图片链接由ImageLinksData对象映射。
        public ImageLinksData $imageLinks,
    ) {
    }
    public static function fromArray(array $data): self
    {
        return new self(
            title: data_get($data, 'title'),
            subtitle: data_get($data, 'subtitle'),
            // 从数据数组创建集合。
            authors: collect(data_get($data, 'authors')),
            publisher: data_get($data, 'publisher'),
            publishedDate: data_get($data, 'publishedDate'),
            description: data_get($data, 'description'),
            pageCount: data_get($data, 'pageCount'),
            // 从数据数组创建集合。
            categories: collect(data_get($data, 'categories')),
            // 将图片链接映射到ImageLinksData对象。
            imageLinks: ImageLinksData::fromArray(data_get($data, 'imageLinks')),
        );
    }
    public function toArray(): array
    {
        return [
            'title' => $this->title,
            'subtitle' => $this->subtitle,
            // 将集合转换为数组,因为它们实现了可数组化接口。
            'authors' => $this->authors->toArray(),
            'publisher' => $this->publisher,
            'publishedDate' => $this->publishedDate,
            'description' => $this->description,
            'pageCount' => $this->pageCount,
            'categories' => $this->categories->toArray(),
            // 由于我们使用了可数组化接口,我们可以直接调用imageLinks对象的toArray方法。
            'imageLinks' => $this->imageLinks->toArray(),
        ];
    }
}

请注意,我使用了Laravel的集合而不是数组。我更喜欢使用集合,因此每当响应中有数组时,我都会映射到集合。另外,由于VolumeInfoData包含imageLinks属性,我们可以使用ImageLinksData对象进行映射。

再往上走一级,我们有一个项的列表,所以我们可以创建ItemData对象。

namespace App\DataTransferObjects;
use Illuminate\Contracts\Support\Arrayable;
readonly class ItemData implements Arrayable
{
    public function __construct(
        public string $id,
        public VolumeInfoData $volumeInfo,
    ) {
    }

    public static function fromArray(array $data): self
    {
        return new self(
            id: data_get($data, 'id'),
            volumeInfo: VolumeInfoData::fromArray(data_get($data, 'volumeInfo')),
        );
    }

    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'volumeInfo' => $this->volumeInfo->toArray(),
        ];
    }
}

最后,我们需要回到原始的BooksListData对象,而不是映射数据数组,我们想要映射一个ItemData对象的集合。

namespace App\DataTransferObjects;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection;
/**
 * 存储来自Google Books volumes API的顶级数据。
 */
readonly class BooksListData implements Arrayable
{
    public function __construct(
        public string $kind,
        /** @var Collection<int, ItemData> */
        public Collection $items,
        public int $totalItems,
    ) {
    }
    /**
     * 从数据数组创建类的新实例。
     */
    public static function fromArray(array $data): BooksListData
    {
        return new self(
            data_get($data, 'kind'),
            // 将项映射到ItemData对象的集合。
            collect(data_get($data, 'items', []))->map(fn (array $item) => ItemData::fromArray($item)),
            data_get($data, 'totalItems'),
        );
    }
    /**
     * 实现Laravel的Arrayable接口,允许将对象序列化为数组。
     */
    public function toArray(): array
    {
        return [
            'kind' => $this->kind,
            'items' => $this->items->toArray(),
            'totalItems' => $this->totalItems,
        ];
    }
}

有了所有新创建的DTO,让我们回到测试并进行更新。

测试完整的数据传输对象(DTO)


<?php
use App\Actions\QueryBooksByTitle;
use App\DataTransferObjects\BooksListData;
use App\DataTransferObjects\ImageLinksData;
use App\DataTransferObjects\ItemData;
use App\DataTransferObjects\VolumeInfoData;
use Illuminate\Support\Facades\Http;
it('按标题获取图书', function () {
    $title = fake()->sentence();
    // 从Google Books API生成一个假响应。
    $responseData = [
        'kind' => 'books#volumes',
        'totalItems' => 1,
        'items' => [
            [
                'id' => fake()->uuid,
                'volumeInfo' => [
                    'title' => $title,
                    'subtitle' => fake()->sentence(),
                    'authors' => [fake()->name],
                    'publisher' => fake()->company(),
                    'publishedDate' => fake()->date(),
                    'description' => fake()->paragraphs(asText: true),
                    'pageCount' => fake()->numberBetween(100, 500),
                    'categories' => [fake()->word],
                    'imageLinks' => [
                        'thumbnail' => fake()->url(),
                    ],
                ],
            ],
        ],
    ];
    // 当客户端向Google Books API发送请求时,返回假响应。
    Http::fake(['https://www.googleapis.com/books/v1/*' => Http::response(
        body: $responseData,
        status: 200
    )]);
    $response = resolve(QueryBooksByTitle::class)($title);
    expect($response)->toBeInstanceOf(BooksListData::class)
        ->and($response->items->first())->toBeInstanceOf(ItemData::class)
        ->and($response->items->first()->volumeInfo)->toBeInstanceOf(VolumeInfoData::class)
        ->imageLinks->toBeInstanceOf(ImageLinksData::class)
        ->title->toBe($title);
});

现在我们的期望中可以看到,响应正在映射所有不同的DTO,并正确设置标题。

通过使操作返回DTO而不是默认的Illuminate/Http/Client/Response,我们现在对API响应具有类型安全性,并在编辑器中获得更好的自动完成,这极大地提高了开发人员的体验。

创建测试响应辅助函数

另一个我喜欢做的测试技巧是创建类似于响应工厂的东西。在每个可能需要查询图书的单个测试中模拟响应是耗时的,因此我更喜欢创建一个简单的trait来帮助我更快地模拟响应。

<?php
namespace Tests\Helpers;
use Illuminate\Support\Facades\Http;
trait GoogleBooksApiResponseHelpers
{
    /**
     * 为按标题查询图书生成一个假响应。
     */
    private function fakeQueryBooksByTitleResponse(array $items = [], int $status = 200, bool $raw = false): void
    {
        // 如果raw为true,则直接返回items数组。否则,从Google Books API返回一个假响应。
        $data = $raw ? $items : [
            'kind' => 'books#volumes',
            'totalItems' => count($items),
            'items' => array_map(fn (array $item) => $this->createItem($item), $items),
        ];
        Http::fake(['https://www.googleapis.com/books/v1/*' => Http::response(
            body: $data,
            status: $status
        )]);
    }
    // 创建一个假的项目数组。
    private function createItem(array $data = []): array
    {
        return [
            'id' => data_get($data, 'id', '123'),
            'volumeInfo' => $this->createVolumeInfo(data_get($data, 'volumeInfo', [])),
        ];
    }
    // 创建一个假的卷信息数组。
    private function createVolumeInfo(array $data = []): array
    {
        return [
            'title' => data_get($data, 'title', fake()->sentence),
            'subtitle' => data_get($data, 'subtitle', '图书副标题'),
            'authors' => data_get($data, 'authors', ['作者1', '作者2']),
            'publisher' => data_get($data, 'publisher', '出版商'),
            'publishedDate' => data_get($data, 'publishedDate', '2021-01-01'),
            'description' => data_get($data, 'description', '图书描述'),
            'pageCount' => data_get($data, 'pageCount', 123),
            'categories' => data_get($data, 'categories', ['类别1', '类别2']),
            'imageLinks' => data_get($data, 'imageLinks', ['thumbnail' => 'https://example.com/image.jpg']),
        ];
    }
}

在Pest测试中使用该trait,我们只需要使用uses方法。

uses(GoogleBooksApiResponseHelpers::class);

有了这个trait,我们现在可以轻松地添加其他测试,而无需在每个测试中编写所有的模拟数据。

<?php
use App\Actions\QueryBooksByTitle;
use App\DataTransferObjects\BooksListData;
use App\DataTransferObjects\ImageLinksData;
use App\DataTransferObjects\ItemData;
use App\DataTransferObjects\VolumeInfoData;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Tests\Helpers\GoogleBooksApiResponseHelpers;
uses(GoogleBooksApiResponseHelpers::class);
it('按标题获取图书', function () {
    $title = fake()->sentence();
    // 从Google Books API生成一个假响应。
    $this->fakeQueryBooksByTitleResponse([['volumeInfo' => ['title' => $title]]]);
    $response = resolve(QueryBooksByTitle::class)($title);
    expect($response)->toBeInstanceOf(BooksListData::class)
        ->and($response->items->first())->toBeInstanceOf(ItemData::class)
        ->and($response->items->first()->volumeInfo)->toBeInstanceOf(VolumeInfoData::class)
        ->imageLinks->toBeInstanceOf(ImageLinksData::class)
        ->title->toBe($title);
});
it('将标题作为查询参数传递', function () {
    $title = fake()->sentence();
    // 从Google Books API生成一个假响应。
    $this->fakeQueryBooksByTitleResponse([['volumeInfo' => ['title' => $title]]]);
    resolve(QueryBooksByTitle::class)($title);
    Http::assertSent(function (Illuminate\Http\Client\Request $request) use ($title) {
        expect($request)
            ->method()->toBe('GET')
            ->data()->toHaveKey('q', 'intitle:'.$title);
    return true;
});
});
it('获取多本图书的列表', function () {
    // 从Google Books API生成一个假响应。
    $this->fakeQueryBooksByTitleResponse([
    $this->createItem(),
    $this->createItem(),
    $this->createItem(),
    ]);
    $response = resolve(QueryBooksByTitle::class)('Fake Title');
    expect($response->items)->toHaveCount(3);
});
it('抛出异常', function () {
    // 从Google Books API生成一个假响应。
    $this->fakeQueryBooksByTitleResponse([
        $this->createItem(),
    ], 400);
    resolve(QueryBooksByTitle::class)('Fake Title');
})->throws(RequestException::class);

通过这样做,我们现在有了更干净的测试,并且我们的API响应被映射到DTO中。对于更多的优化,您可以考虑使用Spatie提供的Laravel Data包来创建DTO,它可以帮助减少一些创建fromArray和toArray方法的模板代码。

总结

在这篇文章中,你学习了如何通过使用数据传输对象(DTO)来简化 Laravel 中开发和测试 API 集成的过程。

我们探讨了使用 DTO 的好处,以及如何创建 DTO、将 API 响应映射到 DTO,并开发测试响应辅助函数。这不仅提高了代码的可读性,还促进了更加类型安全、高效和可测试的开发流程。

这些技术不仅适用于 Laravel 的 API 集成,也适用于任何类型的 API 集成。然而,如果你希望了解更高级的解决方案,我推荐看一下 Saloon PHP 库。文章来源地址https://www.toymoban.com/diary/laravel/694.html

到此这篇关于使用DTO在Laravel中简化API响应的文章就介绍到这了,更多相关内容可以在右上角搜索或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

原文地址:https://www.toymoban.com/diary/laravel/694.html

如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请联系站长进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用
使用 Laravel Mock 提高您的工作效率
上一篇 2024年01月18日 10:51
解决 SVN 错误 "Could not open the requested SVN filesystem"
下一篇 2024年01月20日 01:06

相关文章

  • HTTP简化版 API使用

    1.1、获取当前IP(限制 1200次 /小时) 用浏览器访问 http://ip.hahado.cn/simple/current-ip?username=usernamepassword=password URL上面加上用户名和密码 \\\"ip\\\": 字段是当前的外网IP (\\\"ip\\\":\\\"null\\\" 正在切换中,暂时没有IP) \\\"ttl\\\": 字段是ip可以使用的剩余时间(秒) 1.2、手动切换IP(限制 180次 /小时,间隔

    2023年04月09日
    浏览(52)
  • 如何使用Laravel的HTTP客户端与外部API交互

    Laravel使API交互对新的和有经验的Web开发人员来说都是轻而易举的。Larvel的HTTP客户端是建立在PHP的Guzzle HTTP客户端之上,让开发者在进行HTTP请求时有更顺畅的体验。它的主要功能包括认证, 路由, 和有效的对象关系映射(ORM). 本文将探讨如何使用Laravel的HTTP客户端来进行请求, 调

    2024年01月21日
    浏览(90)
  • larvel 中的api.php_Laravel 开发 API

    Laravel10中提示了Target *classController does not exist,为什么呢? 原因是:laravel8开始写法变了。换成了新的写法了 解决方法一: 在路由数组加入 AppHttpControllers 即可。 再次访问URL,搞定。 解决方法二: 打开 appProvidersRouteServiceProvider.php 修改,添加一个namespace变量

    2024年02月06日
    浏览(80)
  • 使用Azure Data Factory REST API和HDInsight Spark进行简化数据处理

    在这篇文章中,我们将探讨如何利用Azure Data Factory和HDInsight Spark创建一个强大的数据处理管道。 在当今数据驱动的世界中,组织经常面临着高效可靠地处理和分析大量数据的挑战。Azure Data Factory是一种基于云的数据集成服务,结合HDInsight Spark,一种快速可扩展的大数据处理框

    2024年02月10日
    浏览(106)
  • 【API接口工具】postman-请求响应使用详解

    Postman 可以轻松创建和发送 API 请求。向端点发送请求、从数据源检索数据或测试 API 的功能。您无需在终端中输入命令或编写任何代码。创建一个新请求并选择Send,API 响应出现在 Postman 中。 定义的 API 请求 API 为一个应用程序访问另一个应用程序的功能提供了一种结构化的方

    2024年02月03日
    浏览(76)
  • 使用 Python 流式传输来自 OpenAI API 的响应:分步指南

    OpenAI API 提供了大量可用于执行各种 NLP 任务的尖端 AI 模型。但是,在某些情况下,仅向 OpenAI 发出 API 请求可能还不够,例如需要实时更新时。这就是服务器发送事件 (SSE) 发挥作用的地方。 SSE 是一种简单有效的技术,用于将数据从服务器实时流式传输到客户端。 如何在 W

    2023年04月19日
    浏览(74)
  • 使用 Python 集成 ChatGPT API

    目录 一、安装 ChatGPT API 二、创建 Python 程序 三、调用 ChatGPT API 四、使用上下文进行对话 五、自定义模型 六、总结 随着人工智能技术的不断发展,自然语言处理技术也越来越成熟。ChatGPT 是一种基于深度学习的自然语言生成技术,可以用于构建智能对话系统。ChatGPT API 是

    2024年02月04日
    浏览(62)
  • SpringBoot 如何使用 TestRestTemplate 进行 RESTful API 集成测试

    在使用 SpringBoot 开发 RESTful API 的过程中,我们需要进行集成测试,以确保 API 的正确性和可用性。而 TestRestTemplate 是 Spring Framework 提供的一个工具类,可以用来进行 RESTful API 的集成测试。在本文中,我们将介绍如何使用 TestRestTemplate 进行 RESTful API 集成测试。 TestRestTemplate 是

    2024年02月13日
    浏览(80)
  • “利用Python使用API进行数据集成和自动化开发的指南“

    标题:利用Python使用API进行数据集成和自动化开发的指南 摘要:本文将为读者提供一个详细而全面的指南,教您如何使用Python编程语言来利用API进行数据集成和自动化开发。我们将介绍API的基本概念,探讨Python中常用的API库和工具,以及演示如何通过编写Python代码来调用和处

    2024年02月13日
    浏览(68)
  • Minio入门系列【5】JAVA集成Minio之存储桶操作API使用详解

    官方文档:https://min.io/docs/minio/kubernetes/upstream/index.html?ref=docs-redirect SDK:https://github.com/minio/minio-java Minio 提供了多种语言的SDK,比如java、go、python等。JAVA开发平台可以选择JS和java SDK,也就是前端和后端都可以直接集成minio。 每个OSS的用户都会用到上传服务。Web端常见的上传

    2024年02月05日
    浏览(56)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包