《Go Web编程实战派——从入门到精通》学习笔记之第2章 Go Web开发基础

130 篇文章 4 订阅
31 篇文章 31 订阅

第2章 Go Web开发基础

2.1 helloWorldWeb

//helloWorldWeb.go
//go run helloWorldWeb.go
//127.0.0.1
package main
import (
	"fmt"
	"net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World")
}

func main() {
	server := &http.Server {
		Addr: "0.0.0.0:80",
	}
	http.HandleFunc("/", hello)
	server.ListenAndServe()
}

2.2 Web程序运行原理简介

2.2.1 Web基本原理

  1. 运行原理
    (1)用户打开客户端浏览器,输入URL地址。
    (2)客户端浏览器通过HTTP协议向服务器端发送浏览请求。
    (3)服务器端通过CGI程序接收请求,调用解释引擎处理“动态内容”,访问数据库并处理数据,通过HTTP协议将得到的处理结果返回给客户端浏览器。
    (4)客户端浏览器解释并显示HTML页面。
  2. DNS(Domain Name System,域名系统)
    将主机名和域名转换为IP地址。
    DNS解析过程:
    (1)用户打开浏览器,输入URL地址。浏览器从URL中抽取域名(主机名),传给DNS应用程序的客户端。
    (2)DNS客户端向DNS服务器端发送查询报文,其中包含主机名。
    (3)DNS服务器端向DNS客户端发送回答报文,其中包含该主机名对应IP地址。
    (4)浏览器收到DNS的IP地址后,向该IP地址定位的HTTP服务器端发起TCP连接。

2.2.2 HTTP简介

HTTP(Hyper Text Transfer Protocal,超文本传输协议),简单请求-响应协议,运行在TCP协议上,无状态。它指定客户端发送给服务器端的消息和得到的响应。请求和响应消息头是ASCII码;消息内容则类似MIME格式。

2.2.3 HTTP请求

客户端发送到服务器端的请求消息。

  1. 请求行(Request Line)

请求方法、URI、HTTP协议/协议版本组成。

请求方法方法描述
GET请求页面,并返回页面内容,请求参数包含在URL中,提交数据最多1024byte
HEAD类似GET,只获取报头
POST提交表单或上传文件,数据(含请求参数)包含在请求体中
PUT取代指定内容的文档
DELETE删除指定资源
OPTIONS查看服务器的性能
CONNECT服务器当作跳板,访问其他网页
TRACE回显服务器收到的请求,用于测试或诊断
  1. 请求头(Request Header)
请求头示例说明
AcceptAccept: text/plain, text/html客户端能够接收的内容类型
Accept-charsetAccept-charset: iso-8859-5字符编码集
Accept-EncodingAccept-Encoding: compress, gzip压缩编码类型
Accept-LanguageAccept-Language: en, zh语言
Accept-RangesAccept-Ranges: bytes子范围字段
AuthorizationAuthorization: Basic dbXleoOEpePOetpoe2Ftyd==授权证书
Cache-ControlCache-Control: no-cache缓存机制
ConnectionConnection: close是否需要持久连接(HTTP1.1默认持久连接)
CookieCookie: $version=1; Skin=new;请求域名下的所有cookie值
Content-LengthContent-Length: 348内容长度
  1. 请求体(Request Body)

HTTP请求中传输数据的实体。

2.2.4 HTTP响应

服务器端返回给客户端。

  1. 响应状态码(Response Status Code)

表示服务器的响应状态。

状态码说明详情
100继续服务器收到部分请求,等待客户端继续提出请求
101切换协议请求者已要求服务器切换协议,服务器已确认并准备切换协议
200成功成功处理请求
201已创建服务器创建了新的资源
202已接受已接收请求,但尚未处理
203非授权信息成功处理请求,但返回信息来自另一个源
204无内容成功处理请求,无返回内容
205重置内容成功处理请求,内容重置
206部分内容成功处理部分内容
300多种选择可执行多种操作
301永久移动永久重定向
302临时移动暂时重定向
303查看其他位置重定向目标文档应通过GET获取
304未修改使用上次网页资源
305使用代理应使用代理访问
307临时重定向临时从其他位置响应
400错误请求无法解析
401未授权无身份验证或验证未通过
403禁止访问拒绝
404未找到找不到
405方法禁用禁用指定方法
406不接受无法使用内容响应
407需要代理授权需要使用代理授权
408请求超时请求超时
409冲突完成请求时发生冲突
410已删除资源永久删除
411需要有效长度不接受标头字段不含有效内容长度
412未满足前提条件服务器未满足某个前提条件
413请求实体过大超出能力
414请求URI过长网址过长,无法处理
415不支持类型格式不支持
416请求范围不符页面无法提供请求范围
417未满足期望值未满足期望请求标头字段
500服务器内部发生错误服务器错误
501未实现不具备功能
502错误网关收到无效响应
503服务不可用无法使用
504网关超时没及时收到请求
505HTTP版本不支持不支持HTTP协议版本
  1. 响应头(Response Headers)

包含服务器对请求的应答信息。

响应头说明
Allow服务器支持的请求方法
Content-Encondig文档编码方法。
Content-Length内容长度,浏览器持久HTTP连接时需要
Content-Type文档的MIME类型
DateGMT时间
Expires过期时间后,不再缓存
Last-Modified文档最后改动时间。通过比较客户端头if-Modified-Since,可能返回304(Not Modified)。
Location客户端应去哪里提取文档。
Refresh浏览器应刷新时间,秒
Server服务器名字
Set-Cookie设置页面关联Cookie
WWW-Authenticate客户应在Authorization中提供授权信息,通常返回401。
  1. 响应体(Response Body)

HTTP请求返回的内容。
HTML,二进制数据,JSON文档,XML文档等。

2.2.5 URI与URL

  1. URI(Uniform Resource Identifier,统一资源标识符)
    用来标识Web上每一种可用资源,概念。由资源的命名机制、存放资源的主机名、资源自身的名称等组成。

  2. URL(Uniform Resource Locator,统一资源定位符)
    用于描述网络上的资源(描述信息资源的字符串),实现。使用统一格式,包括文件、服务器地址和目录等。

scheme://host[:port#]/path/.../[?query-string][#anchor]
//协议(服务方式)
//主机域名或IP地址(可含端口号)
//具体地址,目录和文件名等
  1. URN(Uniform Resource Name,统一资源名)
    带有名字的因特网资源,是URL的更新形式,不依赖位置,可减少失效链接个数。

2.2.6 HTTPS简介

HTTPS(Hyper Text Transfer Protocol over SecureSocket Layer),在HTTP基础上,通过传输加密和身份认证保证传输过程的安全型。HTTP + SSL/TLS。

TLS(Transport Layer Security,传输层安全性协议),及其前身SSL(Secure Socket Layer,安全套接字层),保障通信安全和数据完整性。

2.2.7 HTTP2简介

  1. HTTP协议历史
  • HTTP 0.9
    只支持GET方法,不支持MIME类型和HTTP各种头信息等。
  • HTTP 1.0
    增加很多方法、各种HTTP头信息,以及对多媒体对象的处理。
  • HTTP 1.1
    主流HTTP协议,改善结构性缺陷,明确语义,增删特性,支持更复杂的Web应用程序。
  • HTTP 2
    优化性能,兼容HTTP 1.1语义,是二进制协议,头部采用HPACK压缩,支持多路复用、服务器推送等。
  1. HTTP 1.1与HTTP 2的对比
  • 头信息压缩
    HTTP 1.1中,每一次发送和响应,都有HTTP头信息。HTTP 2压缩头信息,减少带宽。
  • 推送功能
    HTTP 2之前,只能客户端发送数据,服务器端返回数据。HTTP2中,服务器可以主动向客户端发起一些数据传输(如css和png等),服务器可以并行发送html,css,js等数据。

2.2.8 Web应用程序的组成

  1. 处理器(hendler)
    接收HTTP请求并处理。调用模板引擎生成html文档返给客户端。

MVC软件架构模型

  • 模型(Model)
    处理与业务逻辑相关的数据,以及封装对数据的处理方法。有对数据直接访问的权力,例如访问数据库。
  • 视图(View)
    实现有目的的显示数据,一般没有程序的逻辑。
  • 控制器(Controller)
    组织不同层面,控制流程,处理用户请求,模型交互等事件,并做出响应。
title
模型Model
控制器Controller
视图View
浏览器
模板引擎
数据库
  1. 模板引擎(template engine)
    分离界面与数据(内容),组合模板(template)与数据(data),生成html文档。
    分为置换型(模板内容中特定标记替换)、解释型和编译型等。
模板template
数据data
模板引擎
HTML文档

2.3 net/http包

2.3.1 创建简单服务器端

  1. 创建和解析HTTP服务器端
package main

import (
	"net/http"
)

func sayHello(w http.ResponseWriter, req *http.Request) {
	w.Write([]byte("Hello World"))
}

func main() {
	//注册路由
	http.HandleFunc("/hello", sayHello)
	//开启对客户端的监听
	http.ListenAndServe(":8080", nil)
}
http.HandleFunc()函数

//输入参数:监听端口号和事件处理器handler
http.ListenAndServe()函数

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}
package main

import (
	"net/http"
)

type Refer struct {
	handler http.Handler
	refer string
}

func (this *Refer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Referer() == this.refer {
		this.handler.ServeHTTP(w, r)
	} else {
		w.WriteHeader(403)
	}
}

func myHandler(w http.ResponseWriter, req *http.Request) {
	w.Write([]byte("this is handler"))
}

func hello(w http.ResponseWriter, req *http.Request) {
	w.Write([]byte("hello"))
}

func main() {
	referer := &Refer{
		handler: http.HandlerFunc(myHandler),
		refer: "www.shirdon.com",
	}
	http.HandleFunc("/hello", hello)
	http.ListenAndServe(":8080", referer)
}

//curl --header "Referer:: www.shirdon.com" -v http://127.0.0.1:8080/hello
  1. 创建和解析HTTPS服务器端
//证书文件路径,私钥文件路径
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error
package main

import (
	"log"
	"net/http"
)

func handle(w http.ResponseWriter, r *http.Request) {
	log.Printf("Got connection: %s", r.Proto)
	w.Write([]byte("Hello this is a HTTP 2 message!"))
}

func main() {
	srv := &http.Server{Addr: ":8088", Handler: http.HandlerFunc(handle)}
	log.Printf("Serving on https://0.0.0.0:8088")
	log.Fatal(srv.ListenAndServeTLS("server.crt", "server.key"))
}

//创建证书
//openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt

//发送请求
//curl --http2 --insecure -I https://127.0.0.1:8088
//https://127.0.0.1:8088/

2.3.2 创建简单的客户端

//src/net/http/client.go
var DefaultClient = &Client{}

func Get(url string) (resp *Response, err error) {
	return DefaultClient.Get(url)
}

func (c *Client) Get(url string) (resp *Response, err error) {
	req, err := NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	return c.Do(req)
}

func Post(url, contentType string, body io.Reader) (resp *Response, err error) {
	return DefaultClient.Post(url, contentType, body)
}

func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {
	req, err := NewRequest("POST", url, body)
	if err != nil {
		return nil, err
	}
	req.Header.set("Content-Type", contentType)
	return c.Do(req)
}
func NewRequest(method, url string, body io.Reader) (*Request, error)
//请求类型
//请求地址
//若body实现io.Closer接口,则Request返回值的Body字段会被设置为body值,并被Client的Do()、Post()和PostForm()方法关闭。
  1. 创建GET请求
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)

func main() {
	resp, err := http.Get("https://www.baidu.com")
	if err != nil {
		fmt.Println("err:", err)
	}
	closer := resp.Body
	bytes, err := ioutil.ReadAll(closer)
	fmt.Println(string(bytes))
}
  1. 创建POST请求
package main

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"
)

func main() {
	url := "https://www.shirdon.com/comment/add"
	body := `{"userId": 1, "articleId": 1, "comment": 这是一条评论}`
	resp, err := http.Post(url, "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(body)))
	if err != nil {
		fmt.Println("err:", err)
	}
	bytes, err := ioutil.ReadAll(resp.Body)
	fmt.Println(string(bytes))
}
  1. 创建PUT请求
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
)

func main() {
	url := "https://www.shirdon.com/comment/update"
	payload := strings.NewReader(`{"userId": 1, "articleId": 1, "comment": 这是一条评论}`)
	req, _ := http.NewRequest("PUT", url, payload)
	req.Header.Add("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("err:", err)
	}
	defer res.Body.Close()
	bytes, err := ioutil.ReadAll(res.Body)
	fmt.Println(string(res))
	fmt.Println(string(bytes))
}
  1. 创建DELETE请求
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
)

func main() {
	url := "https://www.shirdon.com/comment/delete"
	payload := strings.NewReader(`{"userId": 1, "articleId": 1, "comment": 这是一条评论}`)
	req, _ := http.NewRequest("DELETE", url, payload)
	req.Header.Add("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("err:", err)
	}
	defer res.Body.Close()
	bytes, err := ioutil.ReadAll(res.Body)
	fmt.Println(string(res))
	fmt.Println(string(bytes))
}
  1. 请求头设置
type Header map[string][]string
headers := http.Header{"token": {"feeowiwpor23dlspweh"}}
headers.Add("Accept-Charset", "UTF-8")
headers.Set("Host", "www.shirdon.com")
headers.Set("Location", "www.baidu.com")

2.4 html/template包

text/template处理任意格式的文本,html/template生成可对抗代码注入的安全html文档。

2.4.1 模板原理

  1. 模板和模板引擎
    模板是事先定义好的不变的html文档,模板渲染使用可变数据替换html文档中的标记。模板用于显示和数据分离(前后端分离)。模板技术,本质是模板引擎利用模板文件和数据生成html文档。

  2. Go语言模板引擎

  • 模板文件后缀名通常为.tmpl和.tpl,UTF-8编码
  • 模板文件中{{和}}包裹和标识传入数据
  • 点号(.)访问数据,{{.FieldName}}访问字段
  • 除{{和}}包裹内容外,其他内容原样输出

使用:
(1)定义模板文件
按照相应语法规则去定义。
(2)解析模板文件
创建指定模板名称的模板对象

func New(name string) *Template

解析模板内容

func (t *Template) Parse(src string) (*Template, error)

解析模板文件

func ParseFiles(filenames...string) (*Template, error)

正则匹配解析文件,template.ParaeGlob(“a*”)

func ParseGlob(pattern string) (*Template, error)

(3)渲染模板文件

func (t *Template) Execute(wr io.Writer, data interface{}) error

//配合ParseFiles()函数使用,需指定模板名称
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error

2.4.2 使用html/template包

  1. 第1个模板

template_example.tmpl

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>模板使用示例</title>
</head>
<body>
   <p>加油,小伙伴, {{ . }} </p>
</body>
</html>
package main

import (
	"fmt"
	"html/template"
	"net/http"
)

func helloHandleFunc(w http.ResponseWriter, r *http.Request) {
	// 1. 解析模板
	t, err := template.ParseFiles("./template_example.tmpl")
	if err != nil {
		fmt.Println("template parsefile failed, err:", err)
		return
	}
	// 2.渲染模板
	name := "我爱Go语言"
	t.Execute(w, name)
}

func main() {
	http.HandleFunc("/", helloHandleFunc)
	http.ListenAndServe(":8086", nil)
}
  1. 模板语法
    模板语法都包含在{{和}}中间。
type UserInfo struct {
	Name string
	Gender string
	Age int
}

func sayHello(w http.ResponseWriter, r *http.Request) {
	tmpl, err := template.ParseFiles("./hello.html")
	if err != nil {
		fmp.Println("create template failed, err:", err)
		return
	}

	user := UserInfo {
		Name: "张三",
		Gender: "男",
		Age: 28,
	}
	tmpl.Execute(w, user)
}

hello.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello</title>
</head>
<body>
   <p>Hello {{.Name}}</p>
   <p>性别:{{.Gender}}</p>
   <p>年龄:{{.Age}}</p>
</body>
</html>

常用语法:

  • 注释
{{/* 这是一个注释,不会解析 */}}
  • 管道(pipeline)
    产生数据的操作,{{.Name}}等。支持|链接多个命令,类似UNIX下管道。
  • 变量
    变量捕获管道的执行结果。
$variable := pipeline
  • 条件判断
{{if pipeline}} T1 {{end}}
{{if pipeline}} T1 {{else}} T0 {{end}}
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
  • range关键字
{{range pipeline}} T1 {{end}}
{{range pipeline}} T1 {{else}} T0 {{end}}
package main

import (
	"log"
	"os"
	"text/template"
)

func main() {
	//创建一个模版
	rangeTemplate := `
{{if .Kind}}
{{range $i, $v := .MapContent}}
{{$i}} => {{$v}} , {{$.OutsideContent}}
{{end}}
{{else}}
{{range .MapContent}}
{{.}} , {{$.OutsideContent}}
{{end}}    
{{end}}`

	str1 := []string{"第一次 range", "用 index 和 value"}
	str2 := []string{"第二次 range", "没有用 index 和 value"}

	type Content struct {
		MapContent     []string
		OutsideContent string
		Kind           bool
	}
	var contents = []Content{
		{str1, "第一次外面的内容", true},
		{str2, "第二次外面的内容", false},
	}

	// 创建模板并将字符解析进去
	t := template.Must(template.New("range").Parse(rangeTemplate))

	// 接收并执行模板
	for _, c := range contents {
		err := t.Execute(os.Stdout, c)
		if err != nil {
			log.Println("executing template:", err)
		}
	}
}
/*
//输出
0 => 第一次 range, 第一次外面的内容
1 => 用 index 和 value, 第一次外面的内容

第二次 range, 第二次外面的内容
没有用 index 和 value, 第二次外面的内容
*/
  • with关键字
{{with pipeline}} T1 {{end}}
{{with pipeline}} T1 {{else}} T0 {{end}}
  • 比较函数
    比较函数只适用于基本函数(或重定义的基本类型,如type Banance float32),整数和浮点数不能相互比较。
    布尔函数将任何类型的零值视为假。
    只有eq可以接受2个以上参数。
{{eq arg1 arg2 arg3}}
eq
ne
lt
le
gt
ge
  • 预定义函数
函数名功能
and返回第1个空参数或最后一个参数,所有参数都执行。and x y等价于if x then y else x
or返回第1个非空参数或最后一个参数,所有参数都执行。and x y等价于if x then y else x
not
len长度
indexindex y 1 2 3, index[1][2][3]
printfmt.Sprint
printffmt.Sprintf
printlnfmt.Sprintln
htmlhtml逸码等价表示
urlquery可嵌入URL查询的逸码等价表示
jsJavaScript逸码等价表示
callcall func a b, func(a, b);1或2个返回值,第2个为error,非nil会中断并返回给调用者。
  • 自定义函数
    模板对象t的函数字典加入funcMap内的键值对。funcMap某个值不是函数类型,或该函数类型不符合要求,会panic。返回*Template便于链式调用。
func (t *Template) Funcs(funcMap FuncMap) *Template

FuncMap映射函数要求1或2个返回值,第2个为error,非nil会中断并返回给调用者。

type FuncMap map[string]interface{}
package main

import (
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
)

func Welcome() string { //没参数
	return "Welcome"
}

func Doing(name string) string { //有参数
	return name + ", Learning Go Web template "
}

func sayHello(w http.ResponseWriter, r *http.Request) {
	htmlByte, err := ioutil.ReadFile("./funcs.html")
	if err != nil {
		fmt.Println("read html failed, err:", err)
		return
	}
	// 自定义一个匿名模板函数
	loveGo := func() (string) {
		return "欢迎一起学习《Go Web编程实战派从入门到精通》"
	}
	// 采用链式操作在Parse()方法之前调用Funcs添加自定义的loveGo函数
	tmpl1, err := template.New("funcs").Funcs(template.FuncMap{"loveGo": loveGo}).Parse(string(htmlByte))
	if err != nil {
		fmt.Println("create template failed, err:", err)
		return
	}
	funcMap := template.FuncMap{
		//在FuncMap中声明相应要使用的函数,然后就能够在template字符串中使用该函数
		"Welcome": Welcome,
		"Doing":   Doing,
	}
	name := "Shirdon"
	tmpl2, err := template.New("test").Funcs(funcMap).Parse("{{Welcome}}<br/>{{Doing .}}")
	if err != nil {
		panic(err)
	}

	// 使用user渲染模板,并将结果写入w
	tmpl1.Execute(w, name)
	tmpl2.Execute(w, name)
}

func main() {
	http.HandleFunc("/", sayHello)
	http.ListenAndServe(":8087", nil)
}

funcs.html

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<meta http-equiv="X-UA-Compatible" content="ie=edge">
		<title>tmpl test</title>
	</head>
	<body>
		<h1>{{loveGo}}</h1>
	</body>
</html>
  • 模板嵌套
    可以通过文件嵌套和define定义
{{define "name"}} T {{end}}
{{template "name"}}
{{template "name" pipeline}}
{{block "name" pipeline}} T {{end}}
//等价于
{{define "name"}} T {{end}}
{{template "name" pipeline}}

t.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>tmpl test</title>
</head>
<body>
<h1>测试嵌套template语法</h1>
<hr>
{{template "ul.html"}}
<hr>
{{template "ol.html"}}
</body>
</html>
{{define "ol.html"}}
<h1>这是ol.html</h1>
<ol>
    <li>I love Go</li>
    <li>I love java</li>
    <li>I love c</li>
</ol>
{{end}}

ul.html

<ul>
    <li>注释</li>
    <li>日志</li>
    <li>测试</li>
</ul>
package main

import (
	"fmt"
	"html/template"
	"net/http"
)

//定义一个UserInfo结构体
type UserInfo struct {
	Name string
	Gender string
	Age int
}

func tmplSample(w http.ResponseWriter, r *http.Request) {
	tmpl, err := template.ParseFiles("./t.html", "./ul.html")
	if err != nil {
		fmt.Println("create template failed, err:", err)
		return
	}
	user := UserInfo{
		Name:   "张三",
		Gender: "男",
		Age:    28,
	}
	tmpl.Execute(w, user)
	fmt.Println(tmpl)
}

func main() {
	http.HandleFunc("/", tmplSample)
	http.ListenAndServe(":8087", nil)
}
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值