GORM学习入门

文章介绍

在篇内容介绍基于golang的gorm,这里我将简单介绍如何安装,连接数据库(以MySQL为例),以及基本的curd操作

安装

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

模型定义

模型是标准的 struct,由 Go 的基本数据类型、实现了 ScannerValuer 接口的自定义类型及其指针或别名组成,我们最后的表名就是结构体的和结构体名一致,当然字段也一致的

例如:

type User struct {
  ID           uint
  Name         string
  Email        *string
  Age          uint8
  Birthday     *time.Time
  MemberNumber sql.NullString
  ActivatedAt  sql.NullTime
  CreatedAt    time.Time
  UpdatedAt    time.Time
}

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

当然,遵循 GORM 已有的约定,可以减少您的配置和代码量。如果约定不符合您的需求,GORM 允许您自定义配置它们

自定义配置

使用 ID 作为主键

默认情况下,GORM 会使用 ID 作为表的主键。

type User struct {
  ID   string // 默认情况下,名为 `ID` 的字段会作为表的主键
  Name string
}

你可以通过标签 primaryKey 将其它字段设为主键

// 将 `UUID` 设为主键
type Animal struct {
  ID     int64
  UUID   string `gorm:"primaryKey"`
  Name   string
  Age    int64
}

此外,您还可以看看 复合主键

gorm.Model

GORM 定义一个 gorm.Model 结构体,其包括字段 IDCreatedAtUpdatedAtDeletedAt

// gorm.Model 的定义
type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}

您可以将它嵌入到您的结构体中,以包含这几个字段

连接数据库

这里我们以mysql为例:

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

func main() {
  // 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
  dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  //注意:pass为MySQL数据库的管理员密码,dbname为要连接的数据库
  db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

MySQl 驱动程序提供了 一些高级配置 可以在初始化过程中使用

建表

这里我们先连接数据库,为了方便查看SQL语句,我们将记入日志

package main

import (
    "log"
    "os"
    "time"

    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

//定义表结构
type Producttest struct {
    gorm.Model
    Name  string
    Code  string
    Price uint
}

func main() {
    // 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
    dsn := "root:Qq/2013XiaoKUang@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"

    //用于输出使用的sql语句
    newLogger := logger.New(
        log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
        logger.Config{
            SlowThreshold:             time.Second, // 慢 SQL 阈值
            LogLevel:                  logger.Info, // 日志级别
            IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(记录未找到)错误
            Colorful:                  true,        // 禁用彩色打印
        },
    )

    //打开mysql服务中对应的数据库
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        Logger: newLogger,
    })
    if err != nil {
        panic(err)
    }

    //AutoMigrate 为给定模式运行自动迁移,建立Product类型的数据表
    err = db.AutoMigrate(&Producttest{})
    if err != nil {
        log.Fatal("建表失败", err)
    }

我们看到建表成功:

mysql> show tables;
+———————+
| Tables_in_gorm_test |
+———————+
| dities |
| products |
| producttests |
| subways |
| test |
| test1 |
| user2 |
| user_infos |
| users |
+———————+
9 rows in set (0.00 sec)

快速入门

新增数据

这里以结构体的方式插入数据

    // Create 新增数据
    db.Create(&Producttest{Code: "01", Name: "golang程序设计", Price: 100})
    db.Create(&Producttest{Code: "02", Name: "python入门", Price: 200})

然后我们可以看到输出结果:


2022/06/23 09:31:03 /Users/feng/go/src/GormStart/ch01/main.go:51
[16.193ms] [rows:1] INSERT INTO `producttests` (`created_at`,`updated_at`,`deleted_at`,`name`,`code`,`price`) VALUES ('2022-06-23 09:31:03.35','2022-06-23 09:31:03.35',NULL,'golang程序设计','01',100)

2022/06/23 09:31:03 /Users/feng/go/src/GormStart/ch01/main.go:52
[2.331ms] [rows:1] INSERT INTO `producttests` (`created_at`,`updated_at`,`deleted_at`,`name`,`code`,`price`) VALUES ('2022-06-23 09:31:03.364','2022-06-23 09:31:03.364',NULL,'python入门','02',200)
Process 96312 has exited with status 0
Detaching
dlv dap (96294) exited with code: 0

这样我们就将两条数据插入了数据库中

查找数据
var product Producttest  //需要实例化一个表结构
    db.First(&product, 2) // 根据整型主键查找
    fmt.Println(product.Name)
    db.First(&product, "code = ?", "02") // 查找 code 字段值为 D42 的记录
    fmt.Println(product)

查询结果:

[3.323ms] [rows:1] SELECT * FROM `producttests` WHERE `producttests`.`id` = 2 AND `producttests`.`deleted_at` IS NULL ORDER BY `producttests`.`id` LIMIT 1
python入门

[1.002ms] [rows:1] SELECT * FROM `producttests` WHERE code = '02' AND `producttests`.`deleted_at` IS NULL AND `producttests`.`id` = 2 ORDER BY `producttests`.`id` LIMIT 1
{{2 2022-06-23 09:31:03.364 +0800 CST 2022-06-23 09:31:03.364 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} python入门 02 200}
更新数据
db.Model(&Producttest{}).Where("Code = ?", "01").Update("Price", "50")
// UPDATE users SET Price='50', updated_at='2022-06-23 10:00:23.654' WHERE Code='01';

输出结果:

[9.204ms] [rows:0] UPDATE `producttests` SET `price`='50',`updated_at`='2022-06-23 10:05:49.719' WHERE Code = '01' AND `producttests`.`deleted_at` IS NULL
删除数据
var product Producttest
db.Delete(&product, 1) //删除主码为1的数据

新增数据(C)

我们先定义表结构

type User struct {
    Name     string
    Age      uint
    Birthday time.Time
  Addr     string
  Work     string
}

连接数据库:

func main() {
    // 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
    dsn := "root:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"

    //用于输出使用的sql语句
    newLogger := logger.New(
        log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
        logger.Config{
            SlowThreshold:             time.Second, // 慢 SQL 阈值
            LogLevel:                  logger.Info, // 日志级别
            IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(记录未找到)错误
            Colorful:                  true,        // 禁用彩色打印
        },
    )

    //打开mysql服务中对应的数据库
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        Logger: newLogger,
    })
    if err != nil {
        panic(err)
    }

      //建立表
    _ = db.AutoMigrate(&User{})

建表成功:

[18.454ms] [rows:0] CREATE TABLE `users` (`name` longtext,`age` bigint unsigned,`birthday` datetime(3) NULL,`addr` longtext,`work` longtext)

新增数据项:

//新增数据项
    var Age uint = 18
    for i := 0; i < 5; i++ {
        time.Sleep(time.Second * 2)
        _ = db.Create(&User{Name: fmt.Sprintf("小杨%d", i), Age: Age, Birthday: time.Now()})
        Age++
    }

我们可以看到:


2022/06/23 10:35:28 /Users/feng/go/src/GormStart/ch03/main.go:73
[6.255ms] [rows:1] INSERT INTO `users` (`name`,`age`,`birthday`,`addr`,`work`) VALUES ('小杨0',18,'2022-06-23 10:35:28.267','','')

2022/06/23 10:35:30 /Users/feng/go/src/GormStart/ch03/main.go:73
[4.036ms] [rows:1] INSERT INTO `users` (`name`,`age`,`birthday`,`addr`,`work`) VALUES ('小杨1',19,'2022-06-23 10:35:30.275','','')

2022/06/23 10:35:32 /Users/feng/go/src/GormStart/ch03/main.go:73
[5.979ms] [rows:1] INSERT INTO `users` (`name`,`age`,`birthday`,`addr`,`work`) VALUES ('小杨2',20,'2022-06-23 10:35:32.284','','')

2022/06/23 10:35:34 /Users/feng/go/src/GormStart/ch03/main.go:73
[4.307ms] [rows:1] INSERT INTO `users` (`name`,`age`,`birthday`,`addr`,`work`) VALUES ('小杨3',21,'2022-06-23 10:35:34.292','','')

2022/06/23 10:35:36 /Users/feng/go/src/GormStart/ch03/main.go:73
[3.353ms] [rows:1] INSERT INTO `users` (`name`,`age`,`birthday`,`addr`,`work`) VALUES ('小杨4',22,'2022-06-23 10:35:36.296','','')
Process 97602 has exited with status 0
单值插入:

当然我们可以使用:db.create()

db.Create(&User{Name: "小李", Age: 20, Birthday: time.Now(), Addr: "北京", Work: "程序员"})

即:

[2.769ms] [rows:1] INSERT INTO `users` (`name`,`age`,`birthday`,`addr`,`work`) VALUES ('小李',20,'2022-06-23 10:42:39.84','北京','程序员')
批量插入

现在我们修改一下表结构

type User struct {
    ID           uint
    Name         string
    Email        *string
    Age          uint8
    Addr         string
    Work         string
    Birthday     *time.Time
    MemberNumber sql.NullString
    ActivatedAt  sql.NullTime
    CreatedAt    time.Time
    UpdatedAt    time.Time
}

将原来的users表删除

然后建立表:

//建立表
    _ = db.AutoMigrate(&User{})
//批量插入数据
    var users = []User{{Name: "小杨"}, {Name: "小张"}, {Name: "小李"}, {Name: "小冯"}}
    db.Create(&users)
    for _, u := range users {
        fmt.Println(u.Name)
    }

输出结果:

[2.378ms] [rows:4] INSERT INTO `users` (`name`,`email`,`age`,`addr`,`work`,`birthday`,`member_number`,`activated_at`,`created_at`,`updated_at`) VALUES ('小杨',NULL,0,'','',NULL,NULL,NULL,'2022-06-23 10:57:56.088','2022-06-23 10:57:56.088'),('小张',NULL,0,'','',NULL,NULL,NULL,'2022-06-23 10:57:56.088','2022-06-23 10:57:56.088'),('小李',NULL,0,'','',NULL,NULL,NULL,'2022-06-23 10:57:56.088','2022-06-23 10:57:56.088'),('小冯',NULL,0,'','',NULL,NULL,NULL,'2022-06-23 10:57:56.088','2022-06-23 10:57:56.088')
小杨
小张
小李
小冯

我们还可以这样做:

db.Model(&User{}).Create(map[string]interface{}{
        "Name": "小刚", "Age": 25, "Addr": "广州",
    })

查找数据(R)

按排序找找
  1. 升序
//检索单个数据,升序
    var user User
    _ = db.First(&user)
    fmt.Println(user)

输出:

[1.218ms] [rows:1] SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1
{1 小杨 <nil> 0   <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 10:57:56.088 +0800 CST 2022-06-23 10:57:56.088 +0800 CST}
  1. 降序
//降序
    var user User
    _ = db.Last(&user)
    fmt.Println(user)
按照位置查找
//按数据表中的位置
    var user User
    _ = db.Take(&user, 2)
    fmt.Println(user)

输出:

[1.653ms] [rows:1] SELECT * FROM `users` WHERE `users`.`id` = 2 LIMIT 1
{2 小张 <nil> 0   <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 10:57:56.088 +0800 CST 2022-06-23 10:57:56.088 +0800 CST}
按照主键查找
    //通过主键查询
    var user User
    result := db.First(&user, 3)
    if errors.Is(result.Error, gorm.ErrRecordNotFound) {
        fmt.Println("数据未找到")
    }
    fmt.Println(user)
查找表中所有数据
//检索全部对象
    var users []User
    result := db.Find(&users)
    for _, user := range users {
        fmt.Println(user)
    }
    fmt.Println(result.RowsAffected)

输出:

[1.553ms] [rows:5] SELECT * FROM `users`
{1 小杨 <nil> 0   <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 10:57:56.088 +0800 CST 2022-06-23 10:57:56.088 +0800 CST}
{2 小张 <nil> 0   <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 10:57:56.088 +0800 CST 2022-06-23 10:57:56.088 +0800 CST}
{3 小李 <nil> 0   <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 10:57:56.088 +0800 CST 2022-06-23 10:57:56.088 +0800 CST}
{4 小冯 <nil> 0   <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 10:57:56.088 +0800 CST 2022-06-23 10:57:56.088 +0800 CST}
{5 小刚 <nil> 25 广州  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC}
5
根据条件查找
  1. 匹配一条数据:First()

这里db.Where().First()只会匹配一条数据

//根据条件检索
    var users []User
    //匹配一条数据
    db.Where("name= ?", "小杨").First(&users)
    for _, user := range users {
        fmt.Println(user)
    }

输出:

[2.400ms] [rows:1] SELECT * FROM `users` WHERE name= '小杨' ORDER BY `users`.`id` LIMIT 1
{1 小杨 <nil> 0   <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 10:57:56.088 +0800 CST 2022-06-23 10:57:56.088 +0800 CST}
  1. 匹配多条数据:Find()
    db.Where("Addr = ?", "北京").Find((&users))
    for _, user := range users {
        fmt.Println(user)
    }

输出:

[2.133ms] [rows:2] SELECT * FROM `users` WHERE Addr = '北京'
{6 小熊 <nil> 0 北京  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:30:17.018 +0800 CST 2022-06-23 11:30:17.018 +0800 CST}
{8 小张 <nil> 0 北京  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:33:21.414 +0800 CST 2022-06-23 11:33:21.414 +0800 CST}

注意:当我们不知道sql表中字段名的时候可以直接使用结构体,这样可以直接屏蔽数据表底层逻辑,这样我们就可以不用关心数据表的结构了

例如:

//此方法可屏蔽底层SQL数据表字段
    var user User
    db.Where(&User{Name: "小杨"}).First(&user)
    fmt.Println(user)
  1. 根据条件IN检索
//根据条件检索 IN 
//查找age等于18、19、20的数据
    var users []User
    db.Where("age IN ?", []uint{18, 19, 20}).Find(&users)
    for _, user := range users {
        fmt.Println(user)
    }

输出:

[0.917ms] [rows:3] SELECT * FROM `users` WHERE age IN (18,19,20)
{9 小周 <nil> 18 上海  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:49:05.759 +0800 CST 2022-06-23 11:49:05.759 +0800 CST}
{10 小周 <nil> 19 上海  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:49:05.763 +0800 CST 2022-06-23 11:49:05.763 +0800 CST}
{11 小周 <nil> 20 上海  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:49:05.77 +0800 CST 2022-06-23 11:49:05.77 +0800 CST}
  1. 根据条件AND检索
//根据条件检索 AND
//查找地址在北京并且大于等于18岁的人
    var users []User
    db.Where("addr=? AND age>=?", "北京", 18).Find(&users)
    for _, user := range users {
        fmt.Println(user)
    }

输出:

[2.181ms] [rows:3] SELECT * FROM `users` WHERE addr='北京' AND age>=18
{12 小董 <nil> 23 北京  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:49:05.773 +0800 CST 2022-06-23 11:49:05.773 +0800 CST}
{13 小周 <nil> 18 北京  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:54:55.964 +0800 CST 2022-06-23 11:54:55.964 +0800 CST}
{16 小画 <nil> 23 北京  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:54:55.973 +0800 CST 2022-06-23 11:54:55.973 +0800 CST}

根据条件OR 检索

var users []User
db.Where("addr=? OR age>=?", "北京", 20).Find(&users)
for _, user := range users {
    fmt.Println(user)
}

输出:

[1.749ms] [rows:8] SELECT * FROM `users` WHERE addr='北京' OR age>=20
{5 小刚 <nil> 25 广州  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC}
{6 小熊 <nil> 0 北京  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:30:17.018 +0800 CST 2022-06-23 11:30:17.018 +0800 CST}
{8 小张 <nil> 0 北京  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:33:21.414 +0800 CST 2022-06-23 11:33:21.414 +0800 CST}
{11 小周 <nil> 20 上海  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:49:05.77 +0800 CST 2022-06-23 11:49:05.77 +0800 CST}
{12 小董 <nil> 23 北京  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:49:05.773 +0800 CST 2022-06-23 11:49:05.773 +0800 CST}
{13 小周 <nil> 18 北京  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:54:55.964 +0800 CST 2022-06-23 11:54:55.964 +0800 CST}
{15 小杨 <nil> 20 上海  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:54:55.971 +0800 CST 2022-06-23 11:54:55.971 +0800 CST}
{16 小画 <nil> 23 北京  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:54:55.973 +0800 CST 2022-06-23 11:54:55.973 +0800 CST}

使用struct & map

//使用struct
    var users []User
    db.Where(&User{Name: "小周", Age: 18, Addr: "北京"}).Find(&users)
    for _, user := range users {
        fmt.Println(user)
    }

输出:

[2.005ms] [rows:1] SELECT * FROM `users` WHERE `users`.`name` = '小周' AND `users`.`age` = 18 AND `users`.`addr` = '北京'
{13 小周 <nil> 18 北京  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:54:55.964 +0800 CST 2022-06-23 11:54:55.964 +0800 CST}
    //使用map
    var users []User
    db.Where(map[string]interface{}{"name": "小周", "age": 18}).Find(&users)
    // SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
    for _, user := range users {
        fmt.Println(user)
    }

输出:

[4.219ms] [rows:2] SELECT * FROM `users` WHERE `age` = 18 AND `name` = '小周'
{9 小周 <nil> 18 上海  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:49:05.759 +0800 CST 2022-06-23 11:49:05.759 +0800 CST}
{13 小周 <nil> 18 北京  <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 11:54:55.964 +0800 CST 2022-06-23 11:54:55.964 +0800 CST}

更新数据(U)

保存所有字段

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

//通过save方法更新
    var user User
    _ = db.First(&user)
    fmt.Println(user)

    user.Name = "小旷"
    user.Age = 22
    user.ID = 17
    user.Addr = "深圳"
    user.Work = "go开发工程师&gis开发工程师"
    _ = db.Save(&user)
    fmt.Println(user)

输出结果:

[rows:1] INSERT INTO `users` (`name`,`email`,`age`,`addr`,`work`,`birthday`,`member_number`,`activated_at`,`created_at`,`updated_at`,`id`) VALUES ('小旷',NULL,22,'深圳','go开发工程师&gis开发工程师',NULL,NULL,NULL,'2022-06-23 10:57:56.088','2022-06-23 13:28:10.252',17)
{17 小旷 <nil> 22 深圳 go开发工程师&gis开发工程师 <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 2022-06-23 10:57:56.088 +0800 CST 2022-06-23 13:28:10.252 +0800 CST}
更新指定字段
//通过指定字段更新
//将age等于0的更新为20
    var user User
    db.Model(&user).Where("age", 0).Update("age", 20)

输出结果:

[rows:7] UPDATE `users` SET `age`=20,`updated_at`='2022-06-23 13:33:50.548' WHERE `age` = 0
db.Model(&user).Where("addr", "北京").Update("work", "go开发工程师")

输出结果:

[rows:5] UPDATE `users` SET `work`='go开发工程师',`updated_at`='2022-06-23 13:38:32.69' WHERE `addr` = '北京'

删除数据(D)

删除一条记录

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

根据主键删除
//根据主键删除
    var user User
    db.Delete(&user, 17)

输出:

[rows:1] DELETE FROM `users` WHERE `users`.`id` = 17
//使用slice
    var user User
    db.Delete(&user, []int{18, 19, 20, 21})

输出:

[rows:0] DELETE FROM `users` WHERE `users`.`id` IN (18,19,20,21)
软删除

如果您的模型包含了一个 gorm.deletedat 字段(gorm.Model 已经包含了该字段),它将自动获得软删除的能力!

拥有软删除能力的模型调用 Delete 时,记录不会从数据库中被真正删除。但 GORM 会将 DeletedAt 置为当前时间, 并且你不能再通过普通的查询方法找到该记录。

    // 批量删除
    db.Where("age = ?", 20).Delete(&User{})

输出:

rows:8] DELETE FROM `users` WHERE age = 20
// 在查询时会忽略被软删除的记录
db.Where("age = 20").Find(&user)
fmt.Println(user)

输出:查询无果,已经被软删除了

{0  <nil> 0   <nil> { false} {0001-01-01 00:00:00 +0000 UTC false} 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC}
本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!