【Go教程】全网最经典、易懂、适合入门学习的学生成绩管理系统设计与实现

一、引言

如何检验自己对一门语言是否入门?基于这门语言设计一款学生成绩管理系统就是一个很好的评判标准。今天就一起考察下咱们对go的掌握程度吧

二、题目

背景:学生成绩管理系统是用于录入学生成绩信息并支持查询的系统,大体流程如下
在这里插入图片描述
可能有些人会觉得,咦,怎么这么简单?简单就对了,因为只有这样咱们才能将注意力集中在go的基本知识上面。接下来就开始我们的实现吧

三、实现

在开始看之前,请尝试自己实现一个版本,这样再来品尝下面的文章效果更佳~

1.版本一

第一个版本咱们仅实现最基本也是最核心的成绩录入功能,其他的先暂时忽略。

这个版本的实现需要考虑以下几点

  1. 如何将学生的属性封装到一块
  2. 如何获取控制台输入数据并赋值给学生

接下来看看下面的代码实现

package main

import "fmt"

//支持用户录入成绩 1. 通过struct记录每个学生的各项数据
type Student struct {
	id                     int64
	name                   string
	age                    int
	chinese, math, english float32 //类型想同的话可以这样简写
}

func main() {

	var id int64
	fmt.Println("请录入学生学号:")
	fmt.Scanln(&id)

	var name string
	fmt.Println("请录入学生名称:")
	fmt.Scanln(&name)

	var age int
	fmt.Println("请录入学生年龄:")
	fmt.Scanln(&age)

	var chinese float32
	fmt.Println("请录入学生语文成绩:")
	fmt.Scanln(&chinese)

	var math float32
	fmt.Println("请录入学生数学成绩:")
	fmt.Scanln(&math)

	var english float32
	fmt.Println("请录入学生英语成绩:")
	fmt.Scanln(&english)

	student1 := Student{id, name, age, chinese, math, chinese}
	fmt.Println("学生成绩录入完毕,该学生信息为:", student1)
}

可以看到代码实现比较简陋,这里我们采用struct封装学生的各个属性,然后通过fmt的提供的函数读取用户的输入信息,在输入完毕后就进行打印出来,绘成图如下
在这里插入图片描述

接下来让咱们看看执行的效果
在这里插入图片描述

通过上面可以看到此时已经完成了最基本了信息录入功能。简单小结如下

  • 完成最基本的成绩录入功能,涉及知识点:变量、struct、指针
  • 缺点:仅支持单个学生的录入

2.版本二

上个版本咱们的录入功能仅支持单个学生的录入,那么这个版本咱们要进阶一下,支持录入多个学生的信息。因此这个版本咱们需要考虑以下几个问题

  1. 代码如何支持录入多个学生的信息
  2. 如何存多个学生的信息
  3. 如何简化录入信息的消息

接下来看看下面的代码实现

package main

import "fmt"

type Student struct {
   id                     int64
   name                   string
   age                    int
   chinese, math, english float32 //类型想同的话可以这样简写
}

func main() {

   var studentarray [10]Student

   var num int
   fmt.Println("请录入要录入的学生数量:")
   fmt.Scanln(&num)

   for i := 0; i < num; i++ {
      var id int64
      var name string
      var age int
      var chinese, math, english float32
      fmt.Println("请输入学生的学号,姓名,年龄,语文成绩,数学成绩,英语成绩,使用空格进行分隔:")
      fmt.Scanf("%d %s %d %f %f %f", &id, &name, &age, &chinese, &math, &english)
      student := Student{id, name, age, chinese, math, english}
      fmt.Println("当前学生成绩录入完毕,该学生信息为:", student)
      studentarray[i] = student
   }
   result := studentarray[0:num]
   fmt.Println("所有学生成绩录入完毕,信息为:", result)
}

在这里咱们用到了for这个流程控制的关键字,通过它可以实现多次录入的效果,同时这里采用了go自带的数组容器来进行存放多个学生成绩信息,最后是通过Scanf的方式来简化了录入成绩的工作,绘制成图如下
在这里插入图片描述

让我们继续来看看最终的执行效果
在这里插入图片描述
通过上面可以看到此时已经完成了多个学生录入功能。简单小结如下

  • 完成支持多个学生成绩录入功能,拓展知识点:array、for
  • 缺点:仅支持成绩录入,不支持查询

3.版本三

上个版本咱们的录入功能支持了多个学生的录入,那么这个版本咱们继续进阶一下,支持查询的功能。这个版本咱们要考虑以下几个问题

  1. 如何存学生信息用于索引查询
  2. 如何更好的对代码进行封装
  3. 如何根据不同的操作(增加、查询)进行对应的处理

接下来看看下面的代码实现

package main

import "fmt"

type Student struct {
   id                    int64
   name                  string
   age                   int
   chinese, math, english float32 //类型想同的话可以这样简写
}

var studentinfo map[int64]Student

func search() {
   var id int64
   fmt.Println("请录入要查询的学生学号:")
   fmt.Scanln(&id)
   student, ok := studentinfo[id]
   if ok == true {
      fmt.Println("查到该学生,此学生的详细信息为:", student)
      return
   }
   fmt.Println("未找到该学生信息")
}

func add() {
   var studentarray [10]Student

   var num int
   fmt.Println("请录入要录入的学生数量:")
   fmt.Scanln(&num)

   for i := 0; i < num; i++ {
      var id int64
      var name string
      var age int
      var chinese, math, english float32
      fmt.Println("请输入学生的学号,姓名,年龄,语文成绩,数学成绩,英语成绩,使用空格进行分隔:")
      fmt.Scanf("%d %s %d %f %f %f", &id, &name, &age, &chinese, &math, &english)
      student := Student{id, name, age, chinese, math, english}
      fmt.Println("当前学生成绩录入完毕,该学生信息为:", student)
      studentarray[i] = student
      studentinfo[id] = student
   }
   result := studentarray[0:num]
   fmt.Println("所有学生成绩录入完毕,信息为:", result)
}

func list() {
   fmt.Println("所有学生信息如下\n", studentinfo)
}

func main() {
   studentinfo = make(map[int64]Student)
   for {
      var operator int
      fmt.Println("请问要做什么:\n输入1 进行学生成绩录入,输入2进行学生成绩查询,输入3显示所有学生信息")
      fmt.Scanln(&operator)
      switch operator {
      case 1:
         add()
      case 2:
         search()
      case 3:
         list()
      default:
         fmt.Println("输入不符合预期,不进行任何操作")
      }
   }
}

在这里我们可以看到可以通过全局map的方式来存储学生的成绩信息,key为学生学号,value是存储学生信息的struct。同时针对不同的操作抽成对应的func,这样大幅提升了代码的可读性,最后咱们通过switch关键字对不同的操作选择了对应的处理func。那么到这里咱们的第三个版本就算是开发好了,接下来让咱们来演示下看看吧
在这里插入图片描述
通过上面的流程,可以看到已经完成了用户信息录入、根据学号查询成绩、列出所有学生信息的功能了。此时虽然好像也能用,但是是不是觉得有什么问题?发现没有,咱们的数据现在是存在内存中的,当服务出现故障或者机器重启就会丢失,那么咱们的用户又要辛苦的重新录入数据。这里简单小结下

  • 完成成绩录入查询功能,拓展知识点:map、switch、func函数
  • 缺点:服务器重启数据会丢失

4.版本四

上个版本咱们的录入功能支持了录入和查询,这个版本需要考虑以下几个问题

  1. 如何实现数据持久化
  2. 如何避免用户使用系统时阻塞数据持久化,或者持久化阻塞用户使用

接下来看看下面的代码实现

package main

import (
   "bufio"
   "encoding/json"
   "flag"
   "fmt"
   "io"
   "log"
   "os"
   "time"
)

type Student struct {
   Id                    int64
   Name                  string
   Age                   int
   Chinese, Math, English float32 //类型想同的话可以这样简写
}

var studentinfo map[int64]Student

func search() {
   var id int64
   fmt.Println("请录入要查询的学生学号:")
   fmt.Scanln(&id)
   student, ok := studentinfo[id]
   if ok == true {
      fmt.Println("查到该学生,此学生的详细信息为:", student)
      return
   }
   fmt.Println("未找到该学生信息")
}

func add() {
   var studentarray [10]Student

   var num int
   fmt.Println("请录入要录入的学生数量:")
   fmt.Scanln(&num)

   fmt.Println("请输入学生的学号,姓名,年龄,语文成绩,数学成绩,英语成绩,使用空格进行分隔:")
   for i := 0; i < num; i++ {
      var id int64
      var name string
      var age int
      var Chinese, Math, English float32
      fmt.Scanf("%d %s %d %f %f %f", &id, &name, &age, &Chinese, &Math, &English)
      student := Student{id, name, age, Chinese, Math, English}
      studentarray[i] = student
      studentinfo[id] = student
   }
   result := studentarray[0:num]
   fmt.Println("所有学生成绩录入完毕,信息为:", result)
}

func list() {
   fmt.Println("所有学生信息如下\n", studentinfo)
}

func persistent() {
   for {
      //定期将map的数据持久化到本地文件 student_info.txt上
      time.Sleep(10 * time.Second)
      fmt.Println("========当前是持久化协程,准备进行数据持久化=========")
      f, err := os.Create("student_info.txt")
      if err != nil {
         fmt.Println(err)
         return
      }

      fmt.Println("持久化时查询到的学生信息studentinfo为:", studentinfo)
      var count int
      for key, student := range studentinfo {
         count++
         fmt.Println("当前key是:", key)
         fmt.Println("当前student是:", student)
         res, err := json.Marshal(student)
         fmt.Println("序列化后的结果是:", string(res))
         if err != nil {
            fmt.Println(err)
         }
         fmt.Println(string(res))
         _, writeErr := f.WriteString(string(res) + "\n")
         if writeErr != nil {
            fmt.Println(err)
            f.Close()
            return
         }
      }
      fmt.Println("本次共持久化学生数量为:", count)
      fmt.Println(" written successfully")
      err = f.Close()
      if err != nil {
         fmt.Println(err)
         return
      }
      fmt.Println("========当前是持久化协程,成功完成数据持久化=========")
   }
}

func recoverInfo() {
   //从持久化中进行数据恢复
   fptr := flag.String("fpath", "student_info.txt", "file path to read from")
   flag.Parse()

   f, err := os.Open(*fptr)
   if err != nil {
      log.Fatal(err)
   }

   defer func() {
      if err = f.Close(); err != nil {
         log.Fatal(err)
      }
   }()

   r := bufio.NewReader(f)
   for {
      n, _, err := r.ReadLine()
      if err == io.EOF {
         fmt.Println("finished reading file")
         break
      }
      if err != nil {
         fmt.Printf("Error %s reading file", err)
      }
      fmt.Println(string(n))
      var student Student
      unmarshalErr := json.Unmarshal(n, &student)
      if unmarshalErr != nil {
         fmt.Println("反序列化失败:", unmarshalErr)
      }
      fmt.Println("当前的信息为:", studentinfo)
      studentinfo[student.Id] = student
   }
}

func main() {
   studentinfo = make(map[int64]Student)
   recoverInfo()
   go persistent()
   for {
      var operator int
      fmt.Println("请问要做什么:\n输入1 进行学生成绩录入,输入2进行学生成绩查询,输入3显示所有学生信息")
      fmt.Scanln(&operator)
      switch operator {
      case 1:
         add()
      case 2:
         search()
      case 3:
         list()
      default:
         fmt.Println("输入不符合预期,不进行任何操作")
      }
   }
}

这里采用了go的io周期性的将内存中的学生数据持久化到文件中,并在服务启动时再去文件中读取数据到内存,从而避免了重新录入数据。同时在这里我们通过go原生并发的语法 go func()的方式新开一个协程去处理出久化的事情,这样相当于用户使用系统跟数据持久化是两个人在处理,彼此之间互相不影响。接下来一起演示一遍看看
在这里插入图片描述

从上面可以看到咱们录入的数据已经被写到磁盘文件了,那么再来看看启动的时候是否有正确加载到内存中呢
在这里插入图片描述

可以看到,当咱们重新启动程序时,之前录入的数据也是存在的并支持正常的搜索。这基本上满足咱们对学生成绩管理系统的设想了对吧?那从技术角度想想是否还存在什么问题或者待优化的空间呢,大家发现没有,现在的数据会周期的写到磁盘文件进行持久化,那如果刚录入完数据到文件持久化这个阶段服务出故障了呢?此时数据就会丢失;另一方面当录入的学生数量过大时例如上万个学生,此时每次全量写磁盘性能相对来说就会比较差并且也是没有必要的。小结如下

  • 完成数据的持久化功能,拓展知识点:文件读写、goroutinue、defer、序列化/反序列化、异常处理
  • 缺点:每次都要全量将数据进行持久化,性能不太好并且有丢数据的可能,同时代码有待优化空间

5.版本五

上个版本咱们已经基本完成了开发,这个版本需要考虑以下几个问题

  1. 如何避免每次全量写以及可能会导致丢数据的问题

接下来看看下面的代码实现

package main

import (
	"bufio"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"time"
)

type Student struct {
	Id                    int64
	Name                  string
	Age                   int
	Chinese, Math, English float32 //类型想同的话可以这样简写
}

var studentinfo map[int64]Student

func search() {
	var id int64
	fmt.Println("请录入要查询的学生学号:")
	fmt.Scanln(&id)
	student, ok := studentinfo[id]
	if ok == true {
		fmt.Println("查到该学生,此学生的详细信息为:", student)
		return
	}
	fmt.Println("未找到该学生信息")
}

func add(studentChan chan Student) {
	var studentarray [10]Student

	var num int
	fmt.Println("请录入要录入的学生数量:")
	fmt.Scanln(&num)

	fmt.Println("请输入学生的学号,姓名,年龄,语文成绩,数学成绩,英语成绩,使用空格进行分隔:")
	for i := 0; i < num; i++ {
		var id int64
		var name string
		var age int
		var Chinese, Math, English float32
		fmt.Scanf("%d %s %d %f %f %f", &id, &name, &age, &Chinese, &Math, &English)
		student := Student{id, name, age, Chinese, Math, English}
		studentarray[i] = student
		studentinfo[id] = student
		studentChan <- student
	}
	result := studentarray[0:num]
	fmt.Println("所有学生成绩录入完毕,信息为:", result)
}

func list() {
	fmt.Println("所有学生信息如下\n", studentinfo)
}

func persistent() {
	for {
		//定期将map的数据持久化到本地文件 student_info.txt上
		time.Sleep(1 * time.Minute)
		fmt.Println("========当前是持久化协程,准备进行数据持久化=========")
		f, err := os.Create("student_info.txt")
		if err != nil {
			fmt.Println(err)
			return
		}

		fmt.Println("持久化时查询到的学生信息studentinfo为:", studentinfo)
		var count int
		for key, student := range studentinfo {
			count++
			fmt.Println("当前key是:", key)
			fmt.Println("当前student是:", student)
			res, err := json.Marshal(student)
			fmt.Println("序列化后的结果是:", string(res))
			if err != nil {
				fmt.Println(err)
			}
			fmt.Println(string(res))
			_, writeErr := f.WriteString(string(res) + "\n")
			if writeErr != nil {
				fmt.Println(err)
				f.Close()
				return
			}
		}
		fmt.Println("本次共持久化学生数量为:", count)
		fmt.Println(" written successfully")
		err = f.Close()
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Println("========当前是持久化协程,成功完成数据持久化=========")
	}
}

func innerAppendPersistent(student Student) {
	file, err := os.OpenFile("student_info.txt", os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		fmt.Println("文件打开失败", err)
		return
	}

	defer file.Close()

	res, err := json.Marshal(student)
	fmt.Println("序列化后的结果是:", string(res))
	if err != nil {
		fmt.Println(err)
	}

	//写入数据时使用带有缓冲的方式
	write := bufio.NewWriter(file)
	write.WriteString(string(res) + "\n")
	write.Flush()
	fmt.Println("当前学生信息追加成功", student)
}

func appendPersistent(studentChan chan Student) {
	//增量持久化
	for {
		time.Sleep(250 * time.Millisecond)
		select {
		case student := <-studentChan:
			fmt.Println("接收到信息:", student)
			innerAppendPersistent(student)
			break
		default:
			//fmt.Println("没有接收到任何值")
		}
	}
}

func recoverInfo() {
	//从持久化中进行数据恢复
	fptr := flag.String("fpath", "student_info.txt", "file path to read from")
	flag.Parse()

	f, err := os.Open(*fptr)
	if err != nil {
		log.Fatal(err)
	}

	//defer的作用是?会在函数返回前进行调用,类似钩子函数
	defer func() {
		if err = f.Close(); err != nil {
			log.Fatal(err)
		}
	}()

	r := bufio.NewReader(f)
	for {
		n, _, err := r.ReadLine()
		if err == io.EOF {
			fmt.Println("finished reading file")
			break
		}
		if err != nil {
			fmt.Printf("Error %s reading file", err)
		}
		fmt.Println(string(n))
		var student Student
		unmarshalErr := json.Unmarshal(n, &student)
		if unmarshalErr != nil {
			fmt.Println("反序列化失败:", unmarshalErr)
		}
		studentinfo[student.Id] = student
	}
	fmt.Println("当前studentinfo的信息为:", studentinfo)
}

func main() {
	studentinfo = make(map[int64]Student)
	var studentChan chan Student = make(chan Student)
	recoverInfo()
	go persistent()
	go appendPersistent(studentChan)
	for {
		var operator int
		fmt.Println("请问要做什么:\n输入1 进行学生成绩录入,输入2进行学生成绩查询,输入3显示所有学生信息")
		fmt.Scanln(&operator)
		switch operator {
		case 1:
			add(studentChan)
		case 2:
			search()
		case 3:
			list()
		default:
			fmt.Println("输入不符合预期,不进行任何操作")
		}
	}
}

为了解决全量写的问题,咱们将实现方案改成了全量写+增量写的逻辑。如每隔10分钟再全量将数据写到磁盘,然后每次有数据新增都立马追加到磁盘,这样既避免了丢数据,也能保证内存中的数据跟磁盘中是一致的。接下来咱们照惯例继续演示一下
在这里插入图片描述

通过演示可以看到,每次新加的数据都会立马追加到文件中,同时咱们的定时任务也会周期性的将内存中的数据全量写一次到磁盘文件中。本次实现主要用了go的通道功能,在新增数据func跟追加写的func建立通道,每次有新加数据就通过通道传递给追加写的func,func收到后就将数据追加到磁盘文件中,避免了数据丢失的可能。小结如下

  • 完成数据增量/全量持久化功能,拓展知识点:chan、文件追加写

6.版本六

上个版本咱们已经完成了功能的开发,但作为一名极客,咱们也要尽可能的对代码设计进行优化,因此这个版本最后对程序进行一版优化,优化后的代码如下

package main

import (
   "bufio"
   "encoding/json"
   "flag"
   "fmt"
   "io"
   "log"
   "os"
   "time"
)

type Student struct {
   Id                    int64
   Name                  string
   Age                   int
   Chinese, Math, English float32 //类型想同的话可以这样简写
}

var studentinfo map[int64]Student

func search() {
   var id int64
   fmt.Println("请录入要查询的学生学号:")
   fmt.Scanln(&id)
   student, ok := studentinfo[id]
   if ok == true {
      fmt.Println("查到该学生,此学生的详细信息为:", student)
      return
   }
   fmt.Println("未找到该学生信息")
}

func add(studentChan chan<- Student) {
   var num int
   fmt.Println("请录入要录入的学生数量:")
   fmt.Scanln(&num)

   fmt.Println("请输入学生的学号,姓名,年龄,语文成绩,数学成绩,英语成绩,使用空格进行分隔:")
   for i := 0; i < num; i++ {
      student := new(Student)
      fmt.Scanf("%d %s %d %f %f %f", &student.Id, &student.Name, &student.Age, &student.Chinese, &student.Math, &student.English)

      studentinfo[student.Id] = *student
      studentChan <- *student
   }
   fmt.Println("所有学生成绩录入完毕")
}

func list() {
   fmt.Println("所有学生信息如下\n", studentinfo)
}

func persistent() {
   for {
      //定期将map的数据持久化到本地文件 student_info.txt上
      time.Sleep(1 * time.Minute)
      fmt.Println("========当前是持久化协程,准备进行数据持久化=========")
      f, err := os.Create("student_info.txt")
      if err != nil {
         fmt.Println(err)
         return
      }

      fmt.Println("持久化时查询到的学生信息studentinfo为:", studentinfo)
      var count int
      for _, student := range studentinfo {
         count++
         res, err := json.Marshal(student)
         fmt.Println("序列化后的结果是:", string(res))
         if err != nil {
            fmt.Println(err)
         }
         fmt.Println(string(res))
         _, writeErr := f.WriteString(string(res) + "\n")
         if writeErr != nil {
            fmt.Println(err)
            f.Close()
            return
         }
      }
      fmt.Println("本次共持久化学生数量为:", count)
      fmt.Println(" written successfully")
      err = f.Close()
      if err != nil {
         fmt.Println(err)
         return
      }
      fmt.Println("========当前是持久化协程,成功完成数据持久化=========")
   }
}

func innerAppendPersistent(student Student) {
   file, err := os.OpenFile("student_info.txt", os.O_WRONLY|os.O_APPEND, 0666)
   if err != nil {
      fmt.Println("文件打开失败", err)
      return
   }

   defer file.Close()

   res, err := json.Marshal(student)
   fmt.Println("序列化后的结果是:", string(res))
   if err != nil {
      fmt.Println(err)
   }

   //写入数据时使用带有缓冲的方式
   write := bufio.NewWriter(file)
   write.WriteString(string(res) + "\n")
   write.Flush()
   fmt.Println("当前学生信息追加成功", student)
}

func appendPersistent(studentChan <-chan Student) {
   //增量持久化
   for {
      time.Sleep(250 * time.Millisecond)
      select {
      case student := <-studentChan:
         fmt.Println("接收到信息:", student)
         innerAppendPersistent(student)
         break
      default:
         //fmt.Println("没有接收到任何值")
      }
   }
}

func recoverInfo() {
   //从持久化中进行数据恢复
   fptr := flag.String("fpath", "student_info.txt", "file path to read from")
   flag.Parse()

   f, err := os.Open(*fptr)
   if err != nil {
      log.Fatal(err)
   }

   defer func() {
      if err = f.Close(); err != nil {
         log.Fatal(err)
      }
   }()

   r := bufio.NewReader(f)
   for {
      n, _, err := r.ReadLine()
      if err == io.EOF {
         fmt.Println("finished reading file")
         break
      }
      if err != nil {
         fmt.Printf("Error %s reading file", err)
      }
      fmt.Println(string(n))
      var student Student
      unmarshalErr := json.Unmarshal(n, &student)
      if unmarshalErr != nil {
         fmt.Println("反序列化失败:", unmarshalErr)
      }
      studentinfo[student.Id] = student
   }
   fmt.Println("当前studentinfo的信息为:", studentinfo)
}

func main() {
   studentinfo = make(map[int64]Student)
   var studentChan chan Student = make(chan Student)
   recoverInfo()
   go persistent()
   go appendPersistent(studentChan)
   for {
      var operator int
      fmt.Println("请问要做什么:\n输入1 进行学生成绩录入,输入2进行学生成绩查询,输入3显示所有学生信息")
      fmt.Scanln(&operator)
      switch operator {
      case 1:
         add(studentChan)
      case 2:
         search()
      case 3:
         list()
      default:
         fmt.Println("输入不符合预期,不进行任何操作")
      }
   }
}

这个版本对代码做了进一步的优化,其中如chan改成单向的,结构体的初始化以及赋值等,到这里基本上就结束了,小结如下

  • 完成代码优化,拓展知识点:单向chan、结构体初始化以及指针

四、总结与思考

通过上面一套组合拳打下来,相信你对go的基础知识以及设计系统都有一定的了解,如果可以希望可以基于自己的理解实现一个,因为实践才是检验真理的唯一标准。基于Go开发出学生成绩管理系统(或者其他系统)并不是终点,只是一个起点,准备好用这门优雅的语言去打开属于你的新世界吧~ 😋

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

夏洛克·林

有钱的捧个钱💰场或人场~

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

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

打赏作者

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

抵扣说明:

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

余额充值