52、使用Catalyst构建应用程序

使用Catalyst构建应用程序

1. 重启服务器与修改方法

1.1 重启服务器

首先,使用 CTRL - C 关闭当前运行的Web服务器。然后,使用以下命令重启它:

perl scripts/rights_server.pl -r

这里的 -r 开关会让Catalyst在你修改源代码时自动重启应用程序,这使得在编写应用程序时进行测试变得更加容易。

1.2 修改 index 方法

index 方法修改为如下代码:

sub index :Path  :Args(1) {
    my ( $self, $c, $name ) = @_;
    # Hello World
    $c->response->body( “Hello, $name” );
}

当你访问 http://localhost:3000/Ovid 时,网页会显示 Hello, Ovid 。不过,为了防止黑客注入任意HTML或JavaScript代码,建议使用 HTML::Entities 对输出进行转义处理。

1.3 默认方法与 end 方法

在Catalyst中, default() 方法可以作为不匹配其他路径的通用处理方法。例如,当你访问 http://localhost:3000/asdf 时,会收到一个 404 NOT FOUND 响应,因为 default 处理程序会匹配该路径:

sub default :Path {
    my ( $self, $c ) = @_;
    $c->response->body( ‘Page not found’ );
    $c->response->status(404);
}

end 方法也很有趣,它通常用于处理请求的结束阶段,例如渲染请求:

sub end : ActionClass(‘RenderView’) {}

或者,你也可以使用以下代码将操作转发到TT(Template Toolkit)视图进行渲染:

sub end : Private {
    my ( $self, $c ) = @_;
    $c->forward( $c->view(‘TT’) );
}

2. 使用Catalyst视图

2.1 创建视图

要在Catalyst中使用视图,可以使用 rights_create.pl 脚本。使用Template Toolkit创建视图的命令如下:

$ perl script/rights_create.pl view TT TT

运行上述命令后,会创建 ./Rights/script/../lib/Rights/View/TT.pm 模块和 ./Rights/script/../t/view_TT.t 测试文件。

2.2 视图模块代码

Rights::View::TT 模块的代码如下:

package Rights::View::TT;
use Moose;
use namespace::autoclean;
extends ‘Catalyst::View::TT’;
__PACKAGE__->config(
    TEMPLATE_EXTENSION => ‘.tt’,
    render_die => 1, # die if we encounter rendering errors
);
1;

以上代码确保你可以正确渲染Template Toolkit视图。

2.3 从控制器调用视图

Rights::Controller::Root 模块中添加以下方法:

sub hello : Path(‘hello’) : Args(1) {
    my ( $self, $c, $my_name ) = @_;
    $c->stash->{template} = ‘hello.tt’;
    $c->stash->{my_name}  = $my_name;
}

root/ 目录下创建 hello.tt 文件,内容如下:

<p>Hello, [% my_name | html %]!</p>

当你访问 http://localhost:3000/hello/World 时,网页会显示 <p>Hello, World!</p> | html 过滤器会对 my_name 变量进行HTML实体编码,这是一种推荐的做法,可以防止恶意代码注入。

2.4 路径与调试信息

hello() 方法中的 :Path 虽然通常与方法名相同,但并非必须如此。例如,你可以将 :Path 设置为 bonjour ,这样访问 http://localhost:3000/bonjour/Monde 时,浏览器会显示 <p>Hello, Monde!</p>

在开发大型应用程序时,可能很难记住所有的路径。当以 -Debug 模式运行Catalyst时,可以查看 Loaded Path Actions 输出,了解可用的路径:

[debug] Loaded Path actions:
.-----------------------------+------------------------------.
| Path                        | Private                      |
+-----------------------------+------------------------------+
| /                           | /index                       |
| /...                        | /default                     |
| /hello/*                    | /hello                       |
‘-----------------------------+------------------------------’

该输出表明你可以访问 / /hello/$something ,其他路径将由 default() 方法处理。

3. 使用Catalyst模型

3.1 创建模型

可以使用之前创建的 rights 数据库快速为Catalyst创建模型。在创建模型之前,需要安装 DBIx::Class::Schema::Loader MooseX::NonMoose MooseX::MarkAsMethods 。然后,运行以下命令创建模型:

perl script/rights_create.pl model Media DBIC::Schema Rights::Schema \
  create=static ‘dbi:SQLite:./rights.db’

该命令的参数解释如下:
- 第一个参数 model 表示要创建一个模型。
- 第二个参数 Media 为模型命名为 Rights::Model::Media
- DBIC::Schema 用于继承 Catalyst::Model::DBIC::Schema 类。
- 第四个参数指定顶级命名空间,通常为应用程序的名称,这里是 Rights
- create=static 告诉 Catalyst::Model::DBIC::Schema 从数据库读取信息来创建模型的模式类。
- 最后是数据源名称(DSN),类似于传递给 DBI->connect() 的第一个参数。

3.2 模型文件结构

运行 tree.pl lib/ 命令,你会看到以下文件结构:

lib/
|  Rights/
|  |  Controller/
|  |  |-- Root.pm
|  |  Model/
|  |  |-- Media.pm
|  |  Schema/
|  |  |  Result/
|  |  |  |-- License.pm
|  |  |  |-- Media.pm
|  |  |  |-- MediaType.pm
|  |-- Schema.pm
|  |  View/
|  |  |-- TT.pm
|-- Rights.pm

3.3 模型文件代码

Rights::Model::Media 文件的代码如下:

package Rights::Model::Media;
use strict;
use base ‘Catalyst::Model::DBIC::Schema’;
__PACKAGE__->config(
    schema_class => ‘Rights::Schema’,
    connect_info => {
        dsn => ‘dbi:SQLite:./rights.db’,
        user => ‘’,
        password => ‘’,
    }
);
1;

在实际应用中,建议将 dsn user password 数据存储在配置文件中,以便在开发和生产环境中连接不同的数据库。

3.4 模式类代码

以下是 Rights::Result::MediaType 模块的代码:

use utf8;
package Rights::Schema::Result::MediaType;
use strict;
use warnings;
use Moose;
use MooseX::NonMoose;
use MooseX::MarkAsMethods autoclean => 1;
extends ‘DBIx::Class::Core’;
__PACKAGE__->load_components(“InflateColumn::DateTime”);
__PACKAGE__->table(“media_types”);
__PACKAGE__->add_columns(
  “id”,
  { data_type => “integer”,is_auto_increment => 1,is_nullable => 0 },
  “media_type”,
  { data_type => “varchar”,is_nullable => 0,size => 10 },
);
__PACKAGE__->set_primary_key(“id”);
__PACKAGE__->has_many(
  “medias”,
  “Rights::Schema::Result::Media”,
  { “foreign.media_type_id” => “self.id” },
  { cascade_copy => 0, cascade_delete => 0 },
);
__PACKAGE__->meta->make_immutable;
1;

该代码包含了通过 DBIx::Class 访问数据所需的所有信息。

3.5 在控制器中使用模型

Rights::Controller::Root 类中添加以下方法:

sub media : Path(‘all_media’) : Args(0) {
    my ( $self, $c ) = @_;
    my $media_rs = $c->model(‘Media::Media’)->search(
        {},    # we want all of them
        { order_by => { -desc => ‘name’ } },
    );
    $c->stash->{template} = ‘all_media.tt’;
    $c->stash->{media_rs} = $media_rs;
}

root 目录下创建 all_media.tt 模板文件:

<table rules=”all”>
[% WHILE ( media = media_rs.next ) %]
  <tr>
    <td>[% media.name         |html %]</td>
    <td>[% media.license.name |html %]</td>
  </tr>
[% END %]
</table>

当你访问 http://localhost:3000/all_media 时,网页会显示一个包含媒体记录及其许可证名称的表格。这里展示了Template Toolkit的一些特性,例如 WHILE 循环的条件通常需要用括号括起来以确保正确解析,并且可以在模板对象上调用方法。

4. 使用Catalyst控制器

4.1 创建新控制器

为了更好地维护代码,我们可以创建一个新的控制器来处理媒体相关的操作。使用以下命令创建一个名为 Media 的控制器:

perl script/rights_create.pl controller Media

新的 Rights::Controller::Media 模块的基本代码如下:

package Rights::Controller::Media;
use Moose;
use namespace::autoclean;
BEGIN { extends ‘Catalyst::Controller’; }
sub index :Path :Args(0) {
    my ( $self, $c ) = @_;
    $c->response->body(‘Matched Rights::Controller::Media in Media.’);
}
__PACKAGE__->meta->make_immutable;
1;

4.2 调整方法与模板

Rights::Controller::Root 中的 media() 方法删除,并将其作为 Rights::Model::Media 中的 index() 方法:

sub index :Path :Args(0) {
    my ( $self, $c ) = @_;
    my $media_rs = $c->model(‘Video::Media’)->search(
        {},    # we want all of them
        { order_by => { -desc => ‘name’ } },
    );
    $c->stash->{template} = ‘media/all.tt’;
    $c->stash->{media_rs}  = $media_rs;
}

将模板文件 all_media.tt 重命名为 media/all.tt ,并将其移动到 root/media 目录下。现在,当你访问 http://localhost:3000/media 时,就可以看到所有媒体的表格。

4.3 显示详细信息

root/media/all.tt 修改为以下内容:

<table rules=”all”>
[% WHILE ( media = media_rs.next ) %]
  <tr>
    <td><a href=”/media/[%media.id%]”>[%media.name|html%]</a></td>
    <td>[% media.license.name |html %]</td>
  </tr>
[% END %]
</table>

此时,页面上会出现指向 /media/1 /media/2 等URL的超链接,但这些链接目前会返回 404 错误。因此,需要在 Rights::Controller::Media 中添加相应的处理方法:

sub media : Path : Args(1) {
    my ( $self, $c, $id ) = @_;
    my $media_rs = $c->model(‘Media::Media’)->search(
    $c->stash->{template} = ‘media/display.tt’;
    $c->stash->{media}    = $media;
}

root/media 目录下创建 display.tt 文件:

[% IF media %]
<table>
  <tr>
    <td>Name:</td>
    <td>[% media.name |html %]</a></td>
  </tr>
  <tr>
    <td>Location:</td>
    <td>[% media.location |html %]</a></td>
  </tr>
  <tr>
    <td>Name:</td>
    <td>[% media.source |html %]</a></td>
  </tr>
  <tr>
    <td>License:</td>
    <td>[% media.license.name |html %]</a></td>
  </tr>
</table>
[% ELSE %]
<strong>Media not found</strong>
[% END %]

当你访问 http://localhost:3000/media/1 时,如果存在匹配的ID,会显示详细信息;否则,会显示 Media not found

5. CRUD操作

5.1 使用 Catalyst::Plugin::AutoCRUD

随着时间的推移,直接使用SQL来创建、读取、更新和删除数据库记录可能会变得繁琐。这时,可以使用 Catalyst::Plugin::AutoCRUD 模块来创建一个CRUD(Create, Read, Update, and Delete)Web界面,该模块可以自动为你创建一个可配置的CRUD界面。

5.2 配置步骤

5.2.1 修改 Rights.pm 模块

Rights.pm 模块中,将 use Catalyst 导入列表修改为包含 AutoCRUD

use Catalyst qw/
   -Debug
    Confi gLoader
    Static::Simple
    AutoCRUD
/;
5.2.2 添加 display_name 方法

Rights::Schema::Result::Media Rights::Schema::Result::MediaType Rights::Schema::Result::License 类中添加 display_name() 方法。例如,在 Rights::Schema::Result::Media 中添加:

sub display_name {
    my $self = shift;
    return $self->name;
}

Rights::Schema::Result::License 中添加:

sub display_name {
    my $self = shift;
    return $self->name;
}

Rights::Schema::Result::MediaType 中添加:

sub display_name {
    my $self = shift;
    return $self->media_type;
}

这些 display_name() 方法将在 Catalyst::Plugin::AutoCRUD 创建下拉菜单时使用。

5.2.3 重启服务器并访问

重启Catalyst服务器(如果使用 -r 开关启动,则会自动重启),然后访问 http://localhost:3000/autocrud ,你会看到一个包含 Licenses Media Media Types 链接的页面。点击 License 链接,你将被带到 http://localhost:3000/autocrud/media/licenses ,在这里你可以添加、删除、修改数据或根据值进行过滤。

5.3 注意事项

Catalyst::Plugin::AutoCRUD 虽然功能强大,但容易因服务器配置错误而将数据库接口暴露给外部世界,导致数据被破坏。因此,建议在单独的Web服务器或具有更严格安全设置的应用程序中使用该模块。

6. 总结

本文介绍了如何使用Catalyst构建Web应用程序,包括重启服务器、修改方法、使用视图、模型和控制器,以及实现CRUD操作。同时,还涉及了一些相关的技术,如 DBIx::Class Template Toolkit 等。通过这些技术,你可以更轻松地构建和管理Web应用程序。

6.1 关键技术总结

技术名称 描述
ORM 对象 - 关系映射器,一种将数据库记录作为对象使用的简单方法
DBIx::Class Perl中最流行的ORM之一
DBIx::Class::Schema::Loader 用于轻松创建DBIx::Class模式的工具
Template Toolkit 一个强大、完整的Perl模板系统
Catalyst 一个用于Perl的Web框架,旨在简化网站的构建过程
MVC 用于组织网站的模型 - 视图 - 控制器模式
Model 管理数据和业务规则的地方
View 应用程序的外部接口,通常是HTML页面
Controller 连接模型和视图的代码
Catalyst::Plugin::AutoCRUD 一种创建数据库Web接口的简单方法

6.2 练习

  • 简要描述对象 - 关系映射器以及使用它的原因。
  • 使用Google Translate或你自己的外语知识,修改 listing_19_2_letter.pl 以包含一种新语言。你可以查看 DateTime::Locale 以确定它是否支持你要翻译的语言。
  • 这是一个综合性的练习,需要结合Perl、Catalyst、DBIx::Class、模板、HTML和SQL的知识。你需要向 rights 数据库中添加一个 videos 表和一个 video_to_media 查找表,并在Catalyst应用程序中添加相应的类和方法,同时创建新的控制器和模板来显示所有视频和单个视频的信息。在处理模板代码时,需要注意Template Toolkit的上下文问题,可通过在结果集方法名后添加 _rs 来解决。

通过完成这些练习,你可以进一步巩固所学的知识,提升自己的编程能力。

7. 详细练习步骤与解析

7.1 对象 - 关系映射器(ORM)概述

对象 - 关系映射器(ORM)是一种编程技术,它允许开发人员使用面向对象的方式来操作数据库。通过ORM,数据库记录可以被视为对象,开发人员可以直接对这些对象进行操作,而无需编写复杂的SQL语句。使用ORM的主要原因包括:
- 提高开发效率 :减少编写和维护SQL语句的工作量,开发人员可以更专注于业务逻辑。
- 增强代码可维护性 :将数据库操作封装在对象中,使代码结构更清晰,易于理解和修改。
- 跨数据库支持 :可以在不同的数据库系统之间切换,而无需对代码进行大量修改。

7.2 修改 listing_19_2_letter.pl 以支持新语言

7.2.1 确定目标语言

首先,使用 DateTime::Locale 检查它是否支持你要翻译的语言。例如,如果你想添加法语支持,可以查看 DateTime::Locale 的文档,确认是否有法语的Locale信息。

7.2.2 修改代码

listing_19_2_letter.pl 中,找到语言相关的部分,添加法语的翻译信息。以下是一个简单的示例:

# 假设这是 listing_19_2_letter.pl 中的语言翻译部分
my %translations = (
    'en' => {
        'greeting' => 'Hello',
        'closing'  => 'Best regards'
    },
    'fr' => {
        'greeting' => 'Bonjour',
        'closing'  => 'Cordialement'
    }
);

# 根据用户选择的语言获取翻译信息
my $language = 'fr'; # 假设用户选择法语
my $greeting = $translations{$language}{'greeting'};
my $closing  = $translations{$language}{'closing'};

# 输出包含翻译信息的内容
print "$greeting! This is a translated message.\n";
print "$closing\n";

7.3 综合练习:扩展权利数据库

7.3.1 添加新表到数据库

rights 数据库中添加 videos 表和 video_to_media 查找表:

-- 添加 videos 表
CREATE TABLE IF NOT EXISTS videos (
    id       INTEGER PRIMARY KEY,
    name     VARCHAR(255)  NOT NULL,
    url      VARCHAR(1000) NOT NULL,
    released DATETIME          NULL
);

-- 添加 video_to_media 查找表
CREATE TABLE IF NOT EXISTS video_to_media (
    id       INTEGER PRIMARY KEY,
    video_id INTEGER NOT NULL,
    media_id INTEGER NOT NULL,
    FOREIGN KEY (video_id) REFERENCES videos(id),
    FOREIGN KEY (media_id) REFERENCES media(id)
);
7.3.2 添加新的结果类

Rights Catalyst应用程序中添加 Rights::Schema::Result::Video 类和 Rights::Schema::Result::MediaToVideo 类。以下是 Rights::Schema::Result::Video 类的示例代码:

use utf8;
package Rights::Schema::Result::Video;
use strict;
use warnings;
use Moose;
use MooseX::NonMoose;
use MooseX::MarkAsMethods autoclean => 1;
extends 'DBIx::Class::Core';

__PACKAGE__->table("videos");
__PACKAGE__->add_columns(
    "id",
    { data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
    "name",
    { data_type => "varchar", is_nullable => 0, size => 255 },
    "url",
    { data_type => "varchar", is_nullable => 0, size => 1000 },
    "released",
    { data_type => "datetime", is_nullable => 1 }
);
__PACKAGE__->set_primary_key("id");

__PACKAGE__->has_many(
    "video_to_medias",
    "Rights::Schema::Result::MediaToVideo",
    { "foreign.video_id" => "self.id" },
    { cascade_copy => 0, cascade_delete => 0 }
);

__PACKAGE__->meta->make_immutable;
1;

Rights::Schema::Result::MediaToVideo 类的代码如下:

use utf8;
package Rights::Schema::Result::MediaToVideo;
use strict;
use warnings;
use Moose;
use MooseX::NonMoose;
use MooseX::MarkAsMethods autoclean => 1;
extends 'DBIx::Class::Core';

__PACKAGE__->table("video_to_media");
__PACKAGE__->add_columns(
    "id",
    { data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
    "video_id",
    { data_type => "integer", is_nullable => 0 },
    "media_id",
    { data_type => "integer", is_nullable => 0 }
);
__PACKAGE__->set_primary_key("id");

__PACKAGE__->belongs_to(
    "video",
    "Rights::Schema::Result::Video",
    { id => "video_id" },
    { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }
);

__PACKAGE__->belongs_to(
    "media",
    "Rights::Schema::Result::Media",
    { id => "media_id" },
    { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }
);

__PACKAGE__->meta->make_immutable;
1;
7.3.3 更新 Rights::Schema::Result::Media

Rights::Schema::Result::Media 类中添加与 video_to_media 表的关联:

# 在 Rights::Schema::Result::Media 类中添加以下代码
__PACKAGE__->has_many(
    "video_to_medias",
    "Rights::Schema::Result::MediaToVideo",
    { "foreign.media_id" => "self.id" },
    { cascade_copy => 0, cascade_delete => 0 }
);
7.3.4 创建新的控制器和模板

创建一个新的控制器来处理视频相关的操作。使用以下命令创建 Video 控制器:

perl script/rights_create.pl controller Video

Rights::Controller::Video 模块中添加以下方法:

sub all_videos : Path('all_videos') : Args(0) {
    my ( $self, $c ) = @_;
    my $video_rs = $c->model('Media::Video')->search(
        {},    # we want all of them
        { order_by => { -desc => 'name' } }
    );
    $c->stash->{template} = 'all_videos.tt';
    $c->stash->{video_rs} = $video_rs;
}

sub video : Path : Args(1) {
    my ( $self, $c, $id ) = @_;
    my $video = $c->model('Media::Video')->find($id);
    $c->stash->{template} = 'video/display.tt';
    $c->stash->{video}    = $video;
}

root 目录下创建 all_videos.tt 模板文件:

<table rules="all">
[% WHILE ( video = video_rs.next ) %]
  <tr>
    <td>[% video.name |html %]</td>
    <td>[% video.url  |html %]</td>
  </tr>
[% END %]
</table>

root/video 目录下创建 display.tt 模板文件:

[% IF video %]
<table>
  <tr>
    <td>Name:</td>
    <td>[% video.name |html %]</td>
  </tr>
  <tr>
    <td>URL:</td>
    <td>[% video.url  |html %]</td>
  </tr>
  <tr>
    <td>Released:</td>
    <td>[% video.released |html %]</td>
  </tr>
  <tr>
    <td>Media:</td>
    <td>
      [% video_to_medias_rs = video.video_to_medias_rs %]
      [% IF video_to_medias_rs.count %]
        <ul>
          [% WHILE ( v2m = video_to_medias_rs.next ) %]
            <li>
              <a href="/media/[%v2m.media.id%]">[%v2m.media.name%]</a>
            </li>
          [% END %]
        </ul>
      [% ELSE %]
        <strong>No media found</strong>
      [% END %]
    </td>
  </tr>
</table>
[% ELSE %]
<strong>Video not found</strong>
[% END %]

7.3.5 处理Template Toolkit上下文问题

在模板代码中,当使用 video.video_to_medias 时,由于Template Toolkit总是在列表上下文中调用方法,可能会导致结果不符合预期。解决方法是在结果集方法名后添加 _rs ,例如 video.video_to_medias_rs ,这样可以确保返回一个单一的结果集对象。

8. 总结与展望

8.1 知识回顾

通过本文的学习,我们掌握了使用Catalyst构建Web应用程序的详细步骤,包括服务器的重启与管理、视图、模型和控制器的使用,以及CRUD操作的实现。同时,我们还了解了一些重要的技术,如对象 - 关系映射器(ORM)、 DBIx::Class Template Toolkit 等。以下是一个简单的知识回顾表格:
| 技术 | 作用 |
| ---- | ---- |
| Catalyst | 构建Web应用程序的框架,遵循MVC模式 |
| ORM(如 DBIx::Class ) | 将数据库记录映射为对象,简化数据库操作 |
| Template Toolkit | 用于生成动态HTML页面的模板系统 |
| Catalyst::Plugin::AutoCRUD | 快速创建数据库的CRUD界面 |

8.2 未来发展建议

  • 深入学习Catalyst :阅读Catalyst的官方文档(http://search.cpan.org/dist/Catalyst-Manual/),了解更多高级特性和最佳实践。
  • 优化数据库操作 :学习如何优化 DBIx::Class 的查询性能,处理复杂的数据库关系。
  • 增强安全性 :在使用 Catalyst::Plugin::AutoCRUD 时,注意配置安全策略,防止数据库接口暴露给外部世界。
  • 持续实践 :通过不断完成类似的综合练习,巩固所学知识,提升自己的编程能力。

通过不断学习和实践,你可以成为一名熟练的Perl开发者,利用Catalyst和相关技术构建出高效、安全的Web应用程序。

STM32电机库无感代码注释无传感器版本龙贝格观测三电阻双AD采样前馈控制弱磁控制斜坡启动内容概要:本文档为一份关于STM32电机控制的无传感器版本代码注释资源,聚焦于龙贝格观测器在永磁同步电机(PMSM)无感控制中的应用。内容涵盖三电阻双通道AD采样技术、前馈控制、弱磁控制及斜坡启动等关键控制策略的实现方法,旨在通过详细的代码解析帮助开发者深入理解基于STM32平台的高性能电机控制算法设计与工程实现。文档适用于从事电机控制开发的技术人员,重点解析了无位置传感器控制下的转子初始定位、速度估算与系统稳定性优化等问题。; 适合人群:具备一定嵌入式开发基础,熟悉STM32平台及电机控制原理的工程师或研究人员,尤其适合从事无感FOC开发的中高级技术人员。; 使用场景及目标:①掌握龙贝格观测器在PMSM无感控制中的建模与实现;②理解三电阻采样与双AD同步采集的硬件匹配与软件处理机制;③实现前馈补偿提升动态响应、弱磁扩速控制策略以及平稳斜坡启动过程;④为实际项目中调试和优化无感FOC系统提供代码参考和技术支持; 阅读建议:建议结合STM32电机控制硬件平台进行代码对照阅读与实验验证,重点关注观测器设计、电流采样校准、PI参数整定及各控制模块之间的协同逻辑,建议配合示波器进行信号观测以加深对控制时序与性能表现的理解。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值