hyperf 二十六 数据迁移 二

教程:Hyperf

参考文章hyperf 二十五 数据迁移 一-CSDN博客

根据之前写的数据迁移的文章,已经说明Hyperf\Database\Schema\Schema::create()实际运行Hyperf\Database\Schema\Grammars\MySqlGrammar::compileCreate()生成的sql字符串。

文档所谓"在迁移文件中主要通过 Hyperf\Database\Schema\Schema 类来定义数据表和管理迁移流程。",就是使用Schema来执行具体的迁移sql。生成迁移sql的内容由回调设置。

一 数据表

1.1 创建数据表

使用Hyperf\Database\Schema\Blueprint类对象设置表结构。

属性解释类型

engine

指定表存储引擎string

charset

指定表默认字符集string

collation

指定表默认排序规则string

temporary

指定是否为临时表boolen

after

在该列之后添加新列string

Blueprint::temporary()设置temporary为true,设置为临时表。

1.2 重命名数据表

可以使用 $table->rename($to),或者Schema::rename($from,$to)。

 1.2.1 $table->rename($to)

Blueprint::rename($to)重命名表名,$to为重新定义的表名。

执行的是Blueprint::rename(),其将执行内容设置为Hyperf\Utils\Fluent类的name属性,并将其放到数组commands中,之后获取sql时调用Blueprint::toSql()。

Blueprint::toSql()遍历数组commands,执行以compile开头的Hyperf\Database\Schema\Grammars\Grammar或其子类的对应方法。

1.2.2 Schema::rename($from, $to)

Hyperf\Database\Schema\Schema::__callStatic()获取对应驱动的Connection子类,再获取Builder或其子类执行对应方法。

以musql驱动为例,$table->rename($to)执行Hyperf\Database\Schema\Blueprint::rename($to),Schema::rename($from,$to)通过子类Hyperf\Database\Schema\MySqlBuilder执行Hyperf\Database\Schema\Builder::rename($from,$to)。

Builder::rename($from,$to)中调用Builder::createBlueprint(),返回Hyperf\Database\Schema\Blueprint类对象。即Schema::rename($from,$to)其实最后也是执行Blueprint类方法。

使用Blueprint需要在回调当中使用。根据源码Schema::table()中可以设置回调。

注:在重命名表之前,应判断该表是否为其他表外键。

1.3 删除数据表

在删除表之前应判断表是否存在。

1.3.1 删除表

使用Schema::drop($table),或者$table::drop()。

根据以上逻辑,$table::drop()执行Hyperf\Database\Schema\Blueprint::drop(),Schema::drop($table)执行Hyperf\Database\Schema\Builder::drop($table)。

Builder::drop($table)中也调用Builder::createBlueprint()。

1.3.2 判断表是否存在

使用Schema::dropIfExists($table),或者$table::dropIfExists()。

根据以上逻辑,$table::drop()执行Hyperf\Database\Schema\Blueprint::dropIfExists(),Schema::drop($table)执行Hyperf\Database\Schema\Builder::dropIfExists($table)。

Builder::dropIfExists($table)中也调用Builder::createBlueprint()。

1.3.3 检查表是否存在

使用Schema::hasTable($table),执行Hyperf\Database\Schema\Builder::hasTable($table)。

Schema\Builder::hasTable($table)直接执行Hyperf\Database\Schema\Grammars\Grammar或其子类compileTableExists()返回的sql字符串。

1.3.4 检查字段是否存在

使用Schema::hasColumn($table, $column),执行Hyperf\Database\Schema\Builder::hasColumn($table, $column)。也是直接执行查询出列名,再用in_array()做判断。

1.4 数据库链接选项

设置connection属性,其值为数据库配置文件中数组最外层key值。

为多个数据库连接设置对应配置……这样根据之前文章的修改内容就不用改了……

这里的内容,若迁移使用不同数据库配置,而且不修改源码的情况下,需要在前文件中设置连接。

这样确实安全点……但是执行迁移时,参数中--database会无效而且还不能回滚,除非设置成和命令一样的表。

/(ㄒoㄒ)/~~ 还是一次性看完比较好,当时就想到会有这样的设计,但是感觉不太合理。

二 字段

2.1 创建字段

可用字段定义方法

$table->bigIncrements('id');递增 ID(主键),相当于「UNSIGNED BIG INTEGER」
$table->bigInteger('votes');相当于 BIGINT
$table->binary('data');相当于 BLOB
$table->boolean('confirmed');相当于 BOOLEAN
$table->char('name', 100);相当于带有长度的 CHAR
$table->date('created_at');相当于 DATE
$table->dateTime('created_at');相当于 DATETIME
$table->dateTimeTz('created_at');相当于带时区 DATETIME
$table->decimal('amount', 8, 2);相当于带有精度与基数 DECIMAL
$table->double('amount', 8, 2);相当于带有精度与基数 DOUBLE
$table->enum('level', ['easy', 'hard']);相当于 ENUM
$table->float('amount', 8, 2);相当于带有精度与基数 FLOAT
$table->geometry('positions');相当于 GEOMETRY
$table->geometryCollection('positions');相当于 GEOMETRYCOLLECTION
$table->increments('id');递增的 ID (主键),相当于「UNSIGNED INTEGER」
$table->integer('votes');相当于 INTEGER
$table->ipAddress('visitor');相当于 IP 地址
$table->json('options');相当于 JSON
$table->jsonb('options');相当于 JSONB
$table->lineString('positions');相当于 LINESTRING
$table->longText('description');相当于 LONGTEXT
$table->macAddress('device');相当于 MAC 地址
$table->mediumIncrements('id');递增 ID (主键) ,相当于「UNSIGNED MEDIUM INTEGER」
$table->mediumInteger('votes');相当于 MEDIUMINT
$table->mediumText('description');相当于 MEDIUMTEXT
$table->morphs('taggable');相当于加入递增的 taggable_id 与字符串 taggable_type
$table->multiLineString('positions');相当于 MULTILINESTRING
$table->multiPoint('positions');相当于 MULTIPOINT
$table->multiPolygon('positions');相当于 MULTIPOLYGON
$table->nullableMorphs('taggable');相当于可空版本的 morphs() 字段
$table->nullableTimestamps();相当于可空版本的 timestamps() 字段
$table->point('position');相当于 POINT
$table->polygon('positions');相当于 POLYGON
$table->rememberToken();相当于可空版本的 VARCHAR (100) 的 remember_token 字段
$table->smallIncrements('id');递增 ID (主键) ,相当于「UNSIGNED SMALL INTEGER」
$table->smallInteger('votes');相当于 SMALLINT
$table->softDeletes();相当于为软删除添加一个可空的 deleted_at 字段
$table->softDeletesTz();相当于为软删除添加一个可空的 带时区的 deleted_at 字段
$table->string('name', 100);相当于带长度的 VARCHAR
$table->text('description');相当于 TEXT
$table->time('sunrise');相当于 TIME
$table->timeTz('sunrise');相当于带时区的 TIME
$table->timestamp('added_on');相当于 TIMESTAMP
$table->timestampTz('added_on');相当于带时区的 TIMESTAMP
$table->timestamps();相当于可空的 created_at 和 updated_at TIMESTAMP
$table->timestampsTz();相当于可空且带时区的 created_at 和 updated_at TIMESTAMP
$table->tinyIncrements('id');相当于自动递增 UNSIGNED TINYINT
$table->tinyInteger('votes');相当于 TINYINT
$table->unsignedBigInteger('votes');相当于 Unsigned BIGINT
$table->unsignedDecimal('amount', 8, 2);相当于带有精度和基数的 UNSIGNED DECIMAL
$table->unsignedInteger('votes');相当于 Unsigned INT
$table->unsignedMediumInteger('votes');相当于 Unsigned MEDIUMINT
$table->unsignedSmallInteger('votes');相当于 Unsigned SMALLINT
$table->unsignedTinyInteger('votes');相当于 Unsigned TINYINT
$table->uuid('id');相当于 UUID
$table->year('birth_year');相当于 YEAR
$table->comment('Table Comment');设置表注释,相当于 COMMENT

 大概运行都是Grammar或其子类,简称为Grammar类。运行Grammar类方法compileAdd(),Grammar::compileAdd()中运行Grammar::getType(),调用以type开头的Grammar类对应方法,用于返回sql字符串。

2.2 修改字段

composer require "doctrine/dbal:^3.0"

 根据文档

Schema::table('users', function (Blueprint $table) {
    // 将字段的长度修改为 50 并允许为空
    $table->string('name', 50)->nullable()->change();
});

$table->string()返回\Hyperf\Database\Schema\ColumnDefinition类对象。ColumnDefinition父类Hyperf\Utils\Fluent::__call()实际执行nullable()和change()。

Fluent::__call()返回$this,所以用“->”继续调用。其仅设置以方法名设置的属性值。

Hyperf\Database\Schema\Blueprint::toSql()时使用对应属性,具体执行Hyperf\Database\Schema\Grammars\Grammar::compileChange(),最终执行Doctrine\DBAL\Platforms\AbstractMySQLPlatform::getAlterTableSQL()。

所以必须安装doctrine/dbal。

只有下面的字段类型能被 "修改": bigInteger、 binary、 boolean、date、dateTime、dateTimeTz、decimal、integer、json、 longText、mediumText、smallInteger、string、text、time、 unsignedBigInteger、unsignedInteger and unsignedSmallInteger。

2.3 重命名字段

$table->renameColumn('from', 'to')->change();

执行Hyperf\Database\Schema\Grammars\Grammar::compileRenameColumn(),然后执行Hyperf\Database\Schema\Grammars\RenameColumn::compile(),最后也是执行Doctrine\DBAL\Platforms\AbstractMySQLPlatform::getAlterTableSQL()。

2.4 删除字段

$table->dropColumn('name');

$table->dropColumn(['name', 'age']);

根据以上逻辑执行Hyperf\Database\Schema\Grammars\Grammar:compileDropColumn(),直接生成sql字符串。

三 测试

3.1 表

测试流程

  1. 创建表
  2. 重命名表
  3. 删除表

3.1.1 创建表

use test1;
show tables;

 

php bin/hyperf gen:migration create_test_table
#migrations\2024_03_06_082623_create_userinfo_table.php
class CreateUserinfoTable extends Migration {
    protected $connection = 'default2';
    /**
     * Run the migrations.
     */
    public function up(): void {
        Schema::create('userinfo', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void {
        Schema::dropIfExists('userinfo');
    }
}
#migrations\2024_03_11_070103_create_test_table.php
class CreateTestTable extends Migration {

    protected $connection = 'default2';
    /**
     * Run the migrations.
     */
    public function up(): void {
        Schema::create('test', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->timestamps();
            $table->engine = 'MyISAM';
            // 指定数据表的默认字符集
            $table->charset = 'utf8mb4';
            // 指定数据表默认的排序规则
            $table->collation = 'utf8mb4_unicode_ci';
            // 创建临时表
            $table->temporary();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void {
        Schema::dropIfExists('test');
    }
}
php bin/hyperf.php   migrate --database=default2
Migrating: 2024_03_06_082623_create_userinfo_table
Migrated:  2024_03_06_082623_create_userinfo_table
Migrating: 2024_03_11_070103_create_test_table
Migrated:  2024_03_11_070103_create_test_table

 

 migration中有test表的记录,但是实际库中没有test表。可能和设置为临时表有关。

经查询mysql临时表仅在当前会话(session)中有效,会话结束时自动消失。

所以把临时表去掉再试下,但是记录未删除的话不会执行。

再次执行,从数据库表看,执行正常。

 3.1.2 重命名表

php bin/hyperf.php gen:migration update_test_table --table=test

[INFO] Created Migration: 2024_03_11_074311_update_test_table
#migrations\2024_03_11_074311_update_test_table.php
class UpdateTestTable extends Migration {
    protected $connection = 'default2';
    /**
     * Run the migrations.
     */
    public function up(): void {
        Schema::table('test', function (Blueprint $table) {
            $table->rename("test1");
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void {
        Schema::table('test', function (Blueprint $table) {
            //
        });
    }
}
php bin/hyperf.php   migrate --database=default2

Migrating: 2024_03_11_074311_update_test_table
Migrated:  2024_03_11_074311_update_test_table

  3.1.3 删除表

php bin/hyperf.php gen:migration update_test2_table --table=test

[INFO] Created Migration:2024_03_11_075322_update_test2_table.php

因为down函数仅在回滚时执行,所以在up()中设置表删除。

#migrations\2024_03_11_075322_update_test2_table.php
class UpdateTest2Table extends Migration {
    protected $connection = 'default2';
    /**
     * Run the migrations.
     */
    public function up(): void {
        Schema::table('test1', function (Blueprint $table) {
            $table->dropIfExists();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void {
        Schema::table('test1', function (Blueprint $table) {
            //
        });
    }
}
php bin/hyperf.php   migrate --database=default2

Migrating: 2024_03_11_075322_update_test2_table
Migrated:  2024_03_11_075322_update_test2_table

3.2 字段

测试流程

  1. 设置字段
  2. 修改字段
  3. 删除字段

3.2.1 设置字段

php bin/hyperf.php gen:migration update_userinfo_table --table=userinfo

[INFO] Created Migration: 2024_03_11_080602_update_userinfo_table
#migrations\2024_03_11_080602_update_userinfo_table.php
class UpdateUserinfoTable extends Migration {
    protected $connection = 'default2';
    /**
     * Run the migrations.
     */
    public function up(): void {
        Schema::table('userinfo', function (Blueprint $table) {
            $table->string('name', 255);
            $table->tinyInteger('age');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void {
        Schema::table('userinfo', function (Blueprint $table) {
            //
        });
    }
}
php bin/hyperf.php   migrate --database=default2

Migrating: 2024_03_11_080602_update_userinfo_table
Migrated:  2024_03_11_080602_update_userinfo_table
DESCRIBE userinfo;

3.2.2 修改字段

php bin/hyperf.php gen:migration update_userinfo2_table --table=userinfo

[INFO] Created Migration: 2024_03_11_081819_update_userinfo2_table
#migrations\2024_03_11_081819_update_userinfo2_table.php
class UpdateUserinfo2Table extends Migration {
    protected $connection = 'default2';
    /**
     * Run the migrations.
     */
    public function up(): void {
        Schema::table('userinfo', function (Blueprint $table) {
            $table->string("name")->nullable()->change();
            $table->renameColumn('age', 'age1')->nullable()->change();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void {
        Schema::table('userinfo', function (Blueprint $table) {
            //
        });
    }
}
php bin/hyperf.php   migrate --database=default2

Migrating: 2024_03_11_081819_update_userinfo2_table
Migrated:  2024_03_11_081819_update_userinfo2_table
DESCRIBE userinfo;

执行回滚,然会修改文件再次执行。

php bin/hyperf.php migrate:rollback --database=default2

Rolling back: 2024_03_11_081819_update_userinfo2_table
Rolled back:  2024_03_11_081819_update_userinfo2_table

但是数据库字段没变……因为down里没设置语句……

#migrations\2024_03_11_081819_update_userinfo2_table.php
class UpdateUserinfo2Table extends Migration {
    protected $connection = 'default2';
    /**
     * Run the migrations.
     */
    public function up(): void {
        Schema::table('userinfo', function (Blueprint $table) {
             $table->string("name")->nullable()->change();
            //$table->tinyInteger('age')->nullable()->change();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void {
        Schema::table('userinfo', function (Blueprint $table) {
            $table->string("name")->change();
            $table->tinyInteger('age')->change();
        });
    }
}

经过手动改动后,此时数据表结构:

 测试时用$table->tinyInteger('age')测试修改,确实报错PHP Fatal error:  Uncaught Doctrine\DBAL\Exception: Unknown column type "tinyinteger" requested. Any Doctrine type that you use has to be registered with \Doctrine\DBAL\Types\Type::addType().

因为这种字段类型不能修改。所以将代码改为上面图中内容,再次执行。

php bin/hyperf.php   migrate --database=default2

Migrating: 2024_03_11_081819_update_userinfo2_table
Migrated:  2024_03_11_081819_update_userinfo2_table

 测试发现写notnull无效,因为改前和改后表差异对比时返回false,但是Hyperf\Utils\Fluent属性notnull确实设置为true。

对应源码

#Hyperf\Database\Schema\Grammars\ChangeColumn
public static function compile($grammar, Blueprint $blueprint, Fluent $command, Connection $connection) {
        if (!$connection->isDoctrineAvailable()) {
            throw new RuntimeException(sprintf(
                'Changing columns for table "%s" requires Doctrine DBAL; install "doctrine/dbal".',
                $blueprint->getTable()
            ));
        }

        $tableDiff = static::getChangedDiff(
            $grammar,
            $blueprint,
            $schema = $connection->getDoctrineSchemaManager()
        );
        if ($tableDiff !== false) {
            return (array) $schema->getDatabasePlatform()->getAlterTableSQL($tableDiff);
        }

        return [];
    }

#$tableDiff 为false
#Hyperf\Database\Schema\Grammars\ChangeColumn
protected static function getTableWithColumnChanges(Blueprint $blueprint, Table $table) {
        $table = clone $table;
        foreach ($blueprint->getChangedColumns() as $fluent) {
            $column = static::getDoctrineColumn($table, $fluent);
            // Here we will spin through each fluent column definition and map it to the proper
            // Doctrine column definitions - which is necessary because Laravel and Doctrine
            // use some different terminology for various column attributes on the tables.
            foreach ($fluent->getAttributes() as $key => $value) {
                if (!is_null($option = static::mapFluentOptionToDoctrine($key))) {
                    if (method_exists($column, $method = 'set' . ucfirst($option))) {
                        $column->{$method}(static::mapFluentValueToDoctrine($option, $value));
                    }
                }
            }
        }

        return $table;
    }

protected static function getTableWithColumnChanges(Blueprint $blueprint, Table $table) {
        $table = clone $table;
        foreach ($blueprint->getChangedColumns() as $fluent) {
            $column = static::getDoctrineColumn($table, $fluent);
            // Here we will spin through each fluent column definition and map it to the proper
            // Doctrine column definitions - which is necessary because Laravel and Doctrine
            // use some different terminology for various column attributes on the tables.
            foreach ($fluent->getAttributes() as $key => $value) {
                if (!is_null($option = static::mapFluentOptionToDoctrine($key))) {
                    if (method_exists($column, $method = 'set' . ucfirst($option))) {
                        $column->{$method}(static::mapFluentValueToDoctrine($option, $value));
                    }
                }
            }
        }

        return $table;
    }

#column 
/***
object(Doctrine\DBAL\Schema\Column)#901 (16) {
  ["_type":protected]=>
  object(Doctrine\DBAL\Types\StringType)#884 (0) {
  }
  ["_length":protected]=>
  int(255)
  ["_precision":protected]=>
  int(10)
  ["_scale":protected]=>
  int(0)
  ["_unsigned":protected]=>
  bool(false)
  ["_fixed":protected]=>
  bool(false)
  ["_notnull":protected]=>
  bool(false)
  ["_default":protected]=>
  NULL
  ["_autoincrement":protected]=>
  bool(false)
  ["_platformOptions":protected]=>
  array(2) {
    ["charset"]=>
    string(7) "utf8mb4"
    ["collation"]=>
    string(18) "utf8mb4_unicode_ci"
  }
  ["_columnDefinition":protected]=>
  NULL
  ["_comment":protected]=>
  NULL
  ["_customSchemaOptions":protected]=>
  array(0) {
  }
  ["_name":protected]=>
  string(4) "name"
  ["_namespace":protected]=>
  NULL
  ["_quoted":protected]=>
  bool(false)
}
***/

#$fluent
/***
array(5) {
  ["type"]=>
  string(6) "string"
  ["name"]=>
  string(4) "name"
  ["length"]=>
  int(255)
  ["notnull"]=>
  bool(true)
  ["change"]=>
  bool(true)
}
***/
#Hyperf\Database\Schema\Grammars\ChangeColumn
protected static function mapFluentOptionToDoctrine($attribute) {
        switch ($attribute) {
        case 'type':
        case 'name':
            return;
        case 'nullable':
            return 'notnull';
        case 'total':
            return 'precision';
        case 'places':
            return 'scale';
        default:
            return $attribute;
        }
    }
#Hyperf\Utils\Fluent
public function __call($method, $parameters)
    {
        $this->attributes[$method] = count($parameters) > 0 ? $parameters[0] : true;

        return $this;
    }
#Doctrine\DBAL\Schema\Column
/** @return mixed[] */
    public function toArray()
    {
        return array_merge([
            'name'          => $this->_name,
            'type'          => $this->_type,
            'default'       => $this->_default,
            'notnull'       => $this->_notnull,
            'length'        => $this->_length,
            'precision'     => $this->_precision,
            'scale'         => $this->_scale,
            'fixed'         => $this->_fixed,
            'unsigned'      => $this->_unsigned,
            'autoincrement' => $this->_autoincrement,
            'columnDefinition' => $this->_columnDefinition,
            'comment' => $this->_comment,
        ], $this->_platformOptions, $this->_customSchemaOptions);
    }
#Doctrine\DBAL\Platforms\DB2Platform
private function getAlterColumnClausesSQL(ColumnDiff $columnDiff): array
    {
        $newColumn = $columnDiff->getNewColumn()->toArray();

        $alterClause = 'ALTER COLUMN ' . $columnDiff->getNewColumn()->getQuotedName($this);

        if ($newColumn['columnDefinition'] !== null) {
            return [$alterClause . ' ' . $newColumn['columnDefinition']];
        }
    ……
}

 

根据源码意思,应该是看Doctrine\DBAL\Schema\Column类中是否有对应的set方法。果然Column类中没有setNotnull(),仅有setNullable()。所以能用的就是Column类中以set开头的方法。

方法调用解释
Column::setType()type()字段类型
Column::setNamename()字段名
Column::setLength()length()字段长度

Column::setPrecision()

total()或者precision()

字段浮点精度

setScale()

places()或者scale()小数点后位数

setUnsigned()

unsigned()无符号

setFixed()

fixed()双精度浮点

setNotnull()

notnull()设置是否不为空

setDefault()

default()设置默认值

setPlatformOptions()

platformOptions()批量设置其他属性

setPlatformOption()

platformOption()单个设置其他属性

setColumnDefinition()

columnDefinition()大概是直接定义的语句

 

应该notnull这样的属性没被处理。 结合属性的定义,可以尝试传参数。比如nullable(false)。

执行代码改为

#migrations\2024_03_11_081819_update_userinfo2_table.php
class UpdateUserinfo2Table extends Migration {
    protected $connection = 'default2';
    /**
     * Run the migrations.
     */
    public function up(): void {
        Schema::table('userinfo', function (Blueprint $table) {
             $table->string("name")->nullable(false)->change();
            //$table->tinyInteger('age')->nullable()->change();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void {
        Schema::table('userinfo', function (Blueprint $table) {
            $table->string("name")->change();
            $table->tinyInteger('age')->change();
        });
    }
}
php bin/hyperf.php   migrate --database=default2

Migrating: 2024_03_11_081819_update_userinfo2_table
Migrated:  2024_03_11_081819_update_userinfo2_table

修改成功

3.2.3 删除字段

php bin/hyperf.php gen:migration update_userinfo3_table --table=userinfo

[INFO] Created Migration: 2024_03_11_094726_update_userinfo3_table
#migrations\2024_03_11_094726_update_userinfo3_table.php
class UpdateUserinfo3Table extends Migration {
    protected $connection = 'default2';
    /**
     * Run the migrations.
     */
    public function up(): void {
        Schema::table('userinfo', function (Blueprint $table) {
            $table->dropColumn(['name', 'age']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void {
        Schema::table('userinfo', function (Blueprint $table) {
            //
        });
    }
}
php bin/hyperf.php   migrate --database=default2

Migrating: 2024_03_11_094726_update_userinfo3_table
Migrated:  2024_03_11_094726_update_userinfo3_table
DESCRIBE userinfo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lsswear

感谢大佬打赏 q(≧▽≦q)

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值