入手Gorm,小白能看懂的基础使用

正文之前也反应大家去我的博客看看喔笔者的blog

Gorm的基本使用

在文章之前,笔者想说,gorm相关内容在gorm的官方中文文档,写得很详细,也推荐大家学习观看喔~ 下面是读者自己觉得学习gorm过程中,觉得很重要的内容,反应大家指导观看。

下载相关包

go get -u gorm.io/gorm 
go get -u gorm.io/driver/mysql

我i这里是使用mysql来作为数据库,所以在在 go get -u gorm.io/driver/mysql后面写的是 mysql。下载你想使用的数据库包

简单示例

连接数据库:

dsn := "root:wst113929@tcp(127.0.0.1:3306)/database_learn?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

注意:一般操作均为 db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}),其中如果是mysql的情况均为 :

dsn := "username:password@tcp(hostname:port)/database_name?charset=utf8mb4&parseTime=True&loc=Local"

然后进行相关数据库操作,总体代码如下

package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type Person struct {
	gorm.Model
	Name string
	Age  int
}

func main() {
	dsn := "root:wst113929@tcp(127.0.0.1:3306)/database_learn?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	//迁移schema
	db.AutoMigrate(&Person{})

	//创建
	db.Create(&Person{Age: 10, Name: "hhh"})
	db.Create(&Person{Age: 19, Name: "wst"})

	//读取
	var p1 Person
	db.First(&p1, 1) //根据主键id
	fmt.Printf("%#v\n", p1)
	var p2 Person
	db.First(&p2, "name=?", "wst") //寻找匹配字段name为wst的第一条数据
	fmt.Printf("%#v\n", p2)

	//更新
	db.Model(&p2).Update("name", "aaa") //更新一个字段
	db.Model(&p2).Updates(Person{
		Age: 100,
	}) //更新多个字段

	//删除指定的记录
	db.Delete(&p1)
	//删除指定的记录,其中&Person{}是要删除的类型
	db.Where("name=?", "wst").Delete(&Person{})
	//删除所有记录
	db.Delete(Person{}) //谨慎操作,这是传递类型,删除所有
}

ps:此时的操作只是一次性的,并没有在数据库中设置相关操作来保证数据的唯一性,如果你多次运行该代码,或者写部分代码运行一次等不同操作,会造成数据库数据重复。就像下图中,数据的紊乱 。之后我们再来修改该问题。

image-20230919190620639

gorm介绍

gorm.Model 介绍

gorm.Model 是 GORM 提供的一个结构体,用于定义模型的基本字段和属性。

//结构定义如下:
type Model struct {
  ID        uint `gorm:"primary_key"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt *time.Time
}

gorm.Model 提供了一些基本的字段和属性,用于支持常见的数据库操作和模型生命周期的管理。通过嵌入 gorm.Model,你可以轻松地在你的模型中继承这些字段和属性。

例如,我上文定义的一个模型 Person 并嵌入 gorm.ModelPerson 将自动包含 IDCreatedAtUpdatedAtDeletedAt 字段。

//直接可以定义的结构
type Person struct {
  gorm.Model
  Name string
  Age  int
}

//就是等价于
type Person struct {
  ID        uint `gorm:"primary_key"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt *time.Time
  Name string
  Age  int
}

这样,Person 模型将具有 IDCreatedAtUpdatedAtDeletedAt 字段,并且可以通过 GORM 进行数据库操作,包括创建、读取、更新和软删除等操作。

声明模型

GORM 倾向于约定优于配置 默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAtUpdatedAt 字段追踪创建、更新时间。

如果您遵循 GORM 的约定,您就可以少写的配置、代码。 如果约定不符合您的实际要求,Gorm允许自己配置。

就像gorm.Model中字段id一样,你会发现在id有标签 gorm:"primary_key",这就是gorm给大家提供的标签。

对于正常的结构体字段,我们可以通过标签 embedded 将其嵌入,例如:

type Author struct {
    Name  string
    Email string
}

type Blog struct {
  ID      int
  Author  Author `gorm:"embedded"`
  Upvotes int32
}
// 等效于
type Blog struct {
  ID    int64
  Name  string
  Email string
  Upvotes  int32
}

并且,也可以使用标签 embeddedPrefix 来为 db 中的字段名添加前缀,例如:

type Blog struct {
  ID      int
  Author  Author `gorm:"embedded;embeddedPrefix:author_"`
  Upvotes int32
}
// 等效于
type Blog struct {
  ID          int64
  AuthorName string
  AuthorEmail string
  Upvotes     int32
}

下面列举一下常用的标签使用

标签说明
column指定数据库表中对应字段的名称
type指定数据库字段的数据类型
size指定数据库字段的长度或大小
primaryKey标记字段为主键
autoIncrement标记字段为自增主键
unique标记字段值必须在整个表中是唯一的
index在数据库中为字段创建索引
not null标记字段不能为空值
default指定字段的默认值
comment注释字段,用于记录字段的说明或备注
embedded嵌入其他模型或结构体,将其字段合并到当前模型中
ignore忽略该字段,GORM 将不会处理该字段
association_foreignkey指定关联关系中外键字段的名称

以上是一些常见的 GORM 标签,我们根据需要在模型的字段上使用这些标签来定义数据库表的结构和约束。根据需要来选择标签。其他标签可参见官方文档哦~Gorm官方文档/标签

主键、表名、列名的约定

  • 主键

    • 如果模型的字段名是 ID(不区分大小写),它将被视为默认的主键字段。
    • 如果你的模型没有名为 ID 的字段,你可以通过在字段上使用 gorm:"primaryKey" 标签来将其标记为主键。
  • 表名

    • 默认情况下,GORM 使用模型的结构体名称的复数形式作为数据库表的名称。例如,type User struct{} 的数据库表名将是 users
    • 你可以通过在模型的结构体上使用 gorm:"tableName:custom_table_name" 标签来自定义表名
  • 列名

    • 默认情况下,GORM 使用模型的字段名作为数据库表中对应列的名称。
    • 你可以通过在字段上使用 gorm:"column:custom_column_name" 标签来自定义列名。

如以下例子:

type Person struct {
	gorm.Model
	Name string `gorm:"column:name"` // 自定义列名
	Age  int    `gorm:"column:age"`  // 自定义列名
}

func (Person) TableName() string {
	return "my_custom_table_name" // 自定义表名
}

通过在结构体字段上使用 gorm:"column:name"gorm:"column:age" 来自定义列名,以及通过 TableName() 方法来自定义表名。

在上述示例中,Person 结构体的表名被自定义为 "my_custom_table_name"

注意:在自定义表名时,你需要确保表名在数据库中是唯一的,并且与其他表没有冲突。

CURD操作

首先结构是:

type User struct {
	ID       uint
	Name     string
	Age      uint8
	Birthday time.Time
}

增加 insert

创建记录

创建单条记录

user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
result := db.Create(&user) // 通过数据的指针来创建

也可以直接创建多项记录

db.Create([]User{
		User{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
		User{Name: "Jackson", Age: 19, Birthday: time.Now()}
	})

需要注意的是,db.Create() 方法是一个原子操作,它会在单个数据库事务中执行所有的插入操作,确保数据一致性。如果其中一个插入操作失败,所有的插入操作都将被回滚,数据不会被插入到数据库。

用指定的字段创建记录

创建记录并为指定的字段分配值:

db.Select("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("jinzhu", 18, "2020-07-04 11:05:21.775")

创建记录并忽略要省略的传递字段的值:

db.Omit("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2020-01-01 00:00:00.000", "2020-07-04 11:05:21.775")
批量插入

用slice和map的跟上方创建按多项记录相差不大。

查询 select

检索单个对象

GORM 提供了 FirstTakeLast 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1 条件,且没有找到记录时,它会返回 ErrRecordNotFound 错误

// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;

// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;

// 获取最后一条记录(主键降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;

result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error        // returns error or nil

// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)

如果你想避免ErrRecordNotFound错误,你可以使用Find,比如db.Limit(1).Find(&user)Find方法可以接受struct和slice的数据。

对单个对象使用Find而不带limit,db.Find(&user)将会查询整个表并且只返回第一个对象,这是性能不高并且不确定的。

此外,如果没有为相关模型定义主键,则模型将按第一个字段排序。

根据主键检索

如果主键是数字,则可以使用内联条件使用主键检索对象。

使用字符串时,需要格外小心以避免 SQL 注入;有关详细信息,请查看“安全性”部分。

db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;

db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;

db.Find(&users, []int{1,2,3})
// SELECT * FROM users WHERE id IN (1,2,3);

如果是主键是string的话,使用下列方法

db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a")
// SELECT * FROM users WHERE id = "1b74413f-f3b8-409f-ac47-e8c062e3472a";

当目标对象有一个主键值时,将使用主键构建查询条件,例如:

var user = User{ID: 10}
db.First(&user)
// SELECT * FROM users WHERE id = 10;

var result User
db.Model(User{ID: 10}).First(&result)
// SELECT * FROM users WHERE id = 10;
检索全部对象

使用 find()来查询所有

result:=db.Find(&user)
// SELECT * FROM users;

result.RowsAffected // 返回找到的记录数
result.Error        // returns error or nil
条件查询
  • string查询

    // Get first matched record
    db.Where("name = ?", "jinzhu").First(&user)
    // SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
    
    // AND
    db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
    // SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;
    
    // LIKE
    db.Where("name LIKE ?", "%jin%").Find(&users)
    // SELECT * FROM users WHERE name LIKE '%jin%';
    
    // IN
    db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
    // SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');
    

    在还有 <>或者 betwwen等操作,详情请参考gorm官方文档查询

    如果已设置对象的主键,则条件查询不会涵盖主键的值,而是将其用作“and”条件。

    var user = User{ID: 10}
    db.Where("id = ?", 20).First(&user)
    // SELECT * FROM users WHERE id = 10 and id = 20 ORDER BY id ASC LIMIT 1
    
  • Struct & Map 条件

    // Struct
    db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
    // SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;
    
    // Map
    db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
    // SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
    
    // Slice of primary keys
    db.Where([]int64{20, 21, 22}).Find(&users)
    // SELECT * FROM users WHERE id IN (20, 21, 22);
    

    但是注意:如果用struct查询,会忽略零值,如0, '', false,不会形成查询语句

    db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)// SELECT * FROM users WHERE name = "jinzhu";
    

    除非使用map,可以查询零值:

    db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
    // SELECT * FROM users WHERE name = "jinzhu" AND age = 0;
    

    使用结构进行搜索时,可以通过将相关字段名称或 dbname 传递给 Where()来指定要在查询条件中使用的结构中的哪些特定值,例如:

    db.Where(&User{Name: "jinzhu"}, "name", "Age").Find(&users)
    // SELECT * FROM users WHERE name = "jinzhu" AND age = 0;
    db.Where(&User{Name: "jinzhu"}, "Age").Find(&users)
    // SELECT * FROM users WHERE age = 0;
    
内联条件、Not条件

这两个用法跟where相似,直接使用,下面显示相关用法

//--------内联
db.Find(&user, "name = ?", "jinzhu")
// SELECT * FROM users WHERE name = "jinzhu";

// Plain SQL
db.Find(&user, "name = ?", "jinzhu")
// SELECT * FROM users WHERE name = "jinzhu";

//---------Not
db.Not("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE NOT name = "jinzhu" ORDER BY id LIMIT 1;

// Not In
db.Not(map[string]interface{}{"name": []string{"jinzhu", "jinzhu 2"}}).Find(&users)
// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");

// Not In slice of primary keys
db.Not([]int64{1,2,3}).First(&user)
// SELECT * FROM users WHERE id NOT IN (1,2,3) ORDER BY id LIMIT 1;
Or条件

就是在条件中添加or

db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';
其他查询语句

select、Limit & Offset、Group By & Having、order、Distinct等相关语句,其实差不多。

select是查询特定字段。

Limit & Offset是进行偏移和限制数量

Group By & Having查询分组后的结果

order进行排序

Distinct对相同数据的筛选

以上函数可以随便搜索找一下用法就可以使用,详细内容请看官方内容|查询特定字段

Scan函数、count函数

scan 用法相同于find函数,都是最后赋值给scanfind内部的变量

count函数获取数量,注意:该函数应该在最后,否则会覆盖前面的内容

//数量count,应该在最后
	var count int64
	db.Model(&User{}).Where("name=?", "Jinzhu").Or("name=?", "Jackson").Count(&count)
	println(count)

还有 pluck函数经常用到:

//pluck :查询model中的一个列
var ages []int64
db.Model(&User{}).Pluck("age", &ages)
for index, _ := range ages {
    println(ages[index])
}

更新 update

保存所有字段

Save 会保存所有的字段,即使字段是零值

db.First(&user)

user.Name = "jinzhu 2"
user.Age = 100
db.Save(&user)
// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;

保存是一个组合功能。如果保存值不包含主键,它将执行 Create,否则将执行 Update(包含所有字段)。

db.Save(&User{Name: "jinzhu", Age: 100})
// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu",100,"0000-00-00 00:00:00","0000-00-00 00:00:00")

db.Save(&User{ID: 1, Name: "jinzhu", Age: 100})
// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`update_at`="0000-00-00 00:00:00" WHERE `id` = 1

注意:不要再在save使用model,否则会触发未定义行为

更新单个列和多个列

使用 Update 更新单个列时,它需要具有任何条件,否则将引发错误ErrMissingWhereClause

// Update with conditions
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;

多个列的情况下使用 updates函数,使用struct或者map对象

// Update attributes with `struct`, will only update non-zero fields
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111; 这里id是因为在user这里面有id的设置

// Update attributes with `map`
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111; 

注意:使用 Updates 方法可以更新多个列,并且只会更新非零值字段。且对于未提供值的字段,它们将保持原有的值不变。

更新所选的字段

使用Select, Omit来更新

// Select with Map
// User's ID is `111`:
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;

db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

// Select with Struct (select zero value fields)
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;

// Select all fields (select all fields include zero value fields)
db.Model(&user).Select("*").Updates(User{Name: "jinzhu", Role: "admin", Age: 0})

// Select all fields but omit Role (select all fields include zero value fields)
db.Model(&user).Select("*").Omit("Role").Updates(User{Name: "jinzhu", Role: "admin", Age: 0})
其他更新内容

在官方中,还提供更新hook、子查询更新等高级操作,有想法可以看官方文档哦~

链接奉上:更新 | 高级操作.

删除delete

删除一条记录

删除一条记录时,删除对象需要指定主键,否则会触发 批量删除,例如:

// Email 的 ID 是 `10`
db.Delete(&email)
// DELETE from emails where id = 10;

// 带额外条件的删除
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE from emails where id = 10 AND name = "jinzhu";
根据主键删除

GORM 允许通过主键(可以是复合主键)和内联条件来删除对象,它可以使用数字。

db.Delete(&User{}, 10)
// DELETE FROM users WHERE id = 10;

db.Delete(&User{}, "10")
// DELETE FROM users WHERE id = 10;

db.Delete(&users, []int{1,2,3})
// DELETE FROM users WHERE id IN (1,2,3);
钩子函数

对于删除操作,GORM 支持 BeforeDeleteAfterDelete Hook,在删除记录时会调用这些方法,可以查看 Hook 获取详情

func (u *User) BeforeDelete(tx *gorm.DB) (err error) {
    if u.Role == "admin" {
        return errors.New("admin user not allowed to delete")
    }
    return
}
批量删除

如果指定的值不包括主属性,那么 GORM 会执行批量删除,它将删除所有匹配的记录

db.Where("email LIKE ?", "%jinzhu%").Delete(&Email{})
// DELETE from emails where email LIKE "%jinzhu%";

db.Delete(&Email{}, "email LIKE ?", "%jinzhu%")
// DELETE from emails where email LIKE "%jinzhu%";

可以将一个主键切片传递给Delete 方法,以便更高效的删除数据量大的记录

var users = []User{{ID: 1}, {ID: 2}, {ID: 3}}
db.Delete(&users)
// DELETE FROM users WHERE id IN (1,2,3);

db.Delete(&users, "name LIKE ?", "%jinzhu%")
// DELETE FROM users WHERE name LIKE "%jinzhu%" AND id IN (1,2,3); 
阻止全局删除

当你试图执行不带任何条件的批量删除时,GORM将不会运行并返回ErrMissingWhereClause 错误

如果一定要这么做,你必须添加一些条件,或者使用原生SQL,或者开启AllowGlobalUpdate 模式,如下例:

db.Delete(&User{}).Error // gorm.ErrMissingWhereClause

db.Delete(&[]User{{Name: "jinzhu1"}, {Name: "jinzhu2"}}).Error // gorm.ErrMissingWhereClause

db.Where("1 = 1").Delete(&User{})
// DELETE FROM `users` WHERE 1=1

db.Exec("DELETE FROM users")
// DELETE FROM users

db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{})
// DELETE FROM users
返回删除行的数据

返回被删除的数据,仅当数据库支持回写功能时才能正常运行,如下例:

// 回写所有的列
var users []User
DB.Clauses(clause.Returning{}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING *
// users => []User{{ID: 1, Name: "jinzhu", Role: "admin", Salary: 100}, {ID: 2, Name: "jinzhu.2", Role: "admin", Salary: 1000}}

// 回写指定的列
DB.Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "salary"}}}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING `name`, `salary`
// users => []User{{ID: 0, Name: "jinzhu", Role: "", Salary: 100}, {ID: 0, Name: "jinzhu.2", Role: "", Salary: 1000}}
软删除

如果你的模型包含了 gorm.DeletedAt字段(该字段也被包含在gorm.Model中),那么该模型将会自动获得软删除的能力!

当调用Delete时,GORM并不会从数据库中删除该记录,而是将该记录的DeleteAt设置为当前时间,而后的一般查询方法将无法查找到此条记录。

// user's ID is `111`
db.Delete(&user)
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;

// Batch Delete
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;

// Soft deleted records will be ignored when querying
db.Where("age = 20").Find(&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;

如果你并不想嵌套gorm.Model,你也可以像下方例子那样开启软删除特性:

type User struct {
  ID      int
  Deleted gorm.DeletedAt
  Name    string
}
查找被软删除的记录

你可以使用Unscoped来查询到被软删除的记录

db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20;
永久删除

你可以使用 Unscoped来永久删除匹配的记录

db.Unscoped().Delete(&order)
// DELETE FROM orders WHERE id=10;
users SET deleted_at="2013-10-29 10:23" WHERE id = 111;

// Batch Delete
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;

// Soft deleted records will be ignored when querying
db.Where("age = 20").Find(&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;

如果你并不想嵌套gorm.Model,你也可以像下方例子那样开启软删除特性:

type User struct {
  ID      int
  Deleted gorm.DeletedAt
  Name    string
}
查找被软删除的记录

你可以使用Unscoped来查询到被软删除的记录

db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20;
永久删除

你可以使用 Unscoped来永久删除匹配的记录

db.Unscoped().Delete(&order)
// DELETE FROM orders WHERE id=10;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值