go Web进阶

Go Web项目

项目结构

  • 在Go语言中web项目的标准结构如下
    在这里插入图片描述

  • Go语言标准库中html/template包提供了html模板支持,把HTML当作模板可以在访问控制器时显示HTML模板信息,这也符合MVC思想

HTML模板显示

  • 使用template.ParseFiles()可以解析多个模板文件
// ParseFiles creates a new Template and parses the template definitions from
// the named files. The returned template's name will have the base name and
// parsed contents of the first file. There must be at least one file.
// If an error occurs, parsing stops and the returned *Template is nil.
//
// When parsing multiple files with the same name in different directories,
// the last one mentioned will be the one that results.
// For instance, ParseFiles("a/foo", "b/foo") stores "b/foo" as the template
// named "foo", while "a/foo" is unavailable.
func ParseFiles(filenames ...string) (*Template, error) {
	return parseFiles(nil, readFileOS, filenames...)
}
  • 把模板信息响应写入到输出流中
// Execute applies a parsed template to the specified data object,
// writing the output to wr.
// If an error occurs executing the template or writing its output,
// execution stops, but partial results may already have been written to
// the output writer.
// A template may be executed safely in parallel, although if parallel
// executions share a Writer the output may be interleaved.
func (t *Template) Execute(wr io.Writer, data interface{}) error {
	if err := t.escape(); err != nil {
		return err
	}
	return t.text.Execute(wr, data)
}
  • 代码实现
  • main代码
package main

import (
	"net/http"
	"text/template"
)

func welcome(res http.ResponseWriter, req *http.Request) {
	files, _ := template.ParseFiles("HTML-Template/view/index.gtpl")
	files.Execute(res, nil)
}

func main() {
	server := http.Server{Addr: "localhost:8090"}
	http.HandleFunc("/", welcome)
	server.ListenAndServe()
}

  • HTML代码
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Title</title>
</head>
<body>
Hello,HTML.Long time no see.
</body>
</html>

引用静态文件

  • 把静态文件放入到特定的文件夹中,使用Go语言的文件服务就可以进行加载

  • 项目结构
    在这里插入图片描述

  • 代码实现

  • main代码

package main

import (
	"net/http"
	"text/template"
)

func welcome(res http.ResponseWriter, req *http.Request) {
	files, _ := template.ParseFiles("importStatic/view/index.gtpl")
	files.Execute(res, nil)
}

func main() {
	server := http.Server{Addr: "localhost:8090"}
	// 当发现url以/static开头时,把请求转发给指定的路径
	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("importStatic/static"))))
	http.HandleFunc("/", welcome)
	server.ListenAndServe()
}

  • html代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script type="text/javascript" src="/static/js/index.js"></script>
</head>
<body>
Hello,HTML.Long time no see.<br/>
<button onclick="myClick()">按钮</button>
</body>
</html>
  • js代码
function myClick() {
    alert("您点击了按钮")
}

向模板传递数据

  • 可以在HTML中使用{{}}获取template.Execute()第二个参数传递的值
  • 最常用的{{.}}中的“.”是指针,指向当前变量,称为“dot”
  • 在{{}}可以有的Argument,官方给定如下
// - go语法的布尔值、字符串、字符、整数、浮点数、虚数、复数,视为无类型字面常数,字符串不能跨行
// - 关键字nil,代表一个go的无类型的nil值
// - 字符'.'(句点,用时加单引号),代表dot的值
// - 变量名,以美元符号起始加上(可为空的)字母和数字构成的字符串,如:piOver2和$;
// 执行结果为变量的值,变量请见下面的介绍
// - 结构体数据的字段名,以句点起始,如:.Field;
// 执行结果为字段的值,支持链式调用:.Field1.Field2;
// 字段也可以在变量上使用(包括链式调用):x.Field1.Field2;
// - 字典类型数据的键名:以句点起始,如:.key;
// 执行结果是该键在字典中对应的成员元素的值;
// 键也可以和字段配合做链式调用,深度不限:.Field1.Key1.Field2.Key2;
// 虽然键也必须是字母和数字构成的标识字符串,但不需要以大写字母起始;
// 键也可以用于变量(包括链式调用):x.key1.key2;
// - 数据的无参数方法名,以句点为起始,如:.Method;
// 执行结果为dot调用该方法的返回值,如:dot.Method();
// 该方法必须有1到2个返回值,如果有2个则后一个必须是error接口类型;
// 如果有2个返回值返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;
// 方法可和字段、键配合做链式调用,深度不限:.Field1.Key1.Method1.Field2.Key2.Method2;
// 方法也可以在变量上使用(包括链式调用):$x.Method1.Field;
// - 无参数的函数名,如:fun;
// 执行结果是调用该函数的返回值fun();对返回值的要求和方法一样;函数和函数名细节参见后面。
// - 上面某一条的实例加上括弧(用于分组)
//执行结果可以访问其字段或者键对应的值:
print (.F1 arg1) (.F2 arg2)
(.StructValuedMethod "arg").Field
  • 向HTML传递字符串数据,在HTML中使用{{.}}获取传递数据即可,所有基本类型都是使用此方式进行传递
  • 代码实现
  • main代码
package main

import (
	"net/http"
	"text/template"
)

func welcome(res http.ResponseWriter, req *http.Request) {
	files, _ := template.ParseFiles("dataToTemplate/view/index.gtpl")
	files.Execute(res, "stronger")
}

func main() {
	server := http.Server{Addr: ":8090"}
	http.HandleFunc("/", welcome)
	server.ListenAndServe()
}

  • html代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<pre>
    尊敬的{{.}}先生/女士:
        巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉
        再次恭喜您:{{.}}
</pre>
</body>
</html>

传递结构体类型数据

  • 结构体的属性首字母必须大写才能被模板访问
  • 在模板中直接使用{{.属性名}}获取结构体的属性
  • 代码实现
  • main代码
package main

import (
	"net/http"
	"text/template"
)

type User struct {
	Name string
	Age  int
}

func welcome(res http.ResponseWriter, req *http.Request) {
	files, _ := template.ParseFiles("structToTemplate/view/index.gtpl")
	files.Execute(res, User{"张三", 18})
}

func main() {
	server := http.Server{Addr: ":8090"}
	http.HandleFunc("/", welcome)
	server.ListenAndServe()
}

  • html代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
获取到的信息:
    姓名:{{.Name}}<br/>
    年龄:{{.Age}}
</body>
</html>

传递map类型数据

  • 直接使用{{.key}}获取map中的数据
  • 模板中支持连缀写法(不仅仅是map)
  • 代码实现
  • main代码
package main

import (
	"net/http"
	"text/template"
)

type User struct {
	Name string
	Age  int
}

func welcome(res http.ResponseWriter, req *http.Request) {
	files, _ := template.ParseFiles("mapToTemplate/view/index.gtpl")
	m := make(map[string]interface{})
	m["user"] = User{"张三", 18}
	m["money"] = 9999999
	files.Execute(res, m)
}

func main() {
	server := http.Server{Addr: ":8090"}
	http.HandleFunc("/", welcome)
	server.ListenAndServe()
}

  • html代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
获取到的信息:
姓名:{{.user.Name}}<br/>
年龄:{{.user.Age}}<br/>
薪资:{{.money}}
</body>
</html>

在模板中调用函数

  • 在模板中调用函数时,如果是无参函数直接调用函数名即可,没有函数的括号
  • 例如在go源码中时间变量.Year(),在模板中{{时间.Year}}
  • 在模板中调用有参函数时参数和函数名称之间有空格,参数和参数之间也是空格
  • 代码实现
  • main代码
package main

import (
	"net/http"
	"text/template"
	"time"
)

func welcome(res http.ResponseWriter, req *http.Request) {
	files, _ := template.ParseFiles("functionInTemplate/view/index.gtpl")
	date := time.Date(2025, 3, 13, 3, 4, 5, 0, time.Local)
	files.Execute(res, date)
}

func main() {
	server := http.Server{Addr: ":8090"}
	http.HandleFunc("/", welcome)
	server.ListenAndServe()
}

  • html代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
完整时间:{{.}}<br/>
年:{{.Year}}<br/>
月:{{.Month}}<br/>
转换后的格式为:{{.Format "2006-01-02 03-04-05"}}
</body>
</html>

在模板中调用自定义函数/方法

  • 如果希望调用自定义函数,需要借助html/template包下的FuncMap进行映射
  • FuncMap本质就是map的别名type FuncMap map[string]interface{}
  • 函数被添加映射后,只能通过函数在FuncMap中的key调用函数
  • 代码实现
  • main代码
package main

import (
	"net/http"
	"text/template"
	"time"
)

func custom(t time.Time) string {
	return t.Format("2006-01-02 03:04:05")
}

func welcome(res http.ResponseWriter, req *http.Request) {
	funcMap := template.FuncMap{"cu": custom}
	funcs := template.New("index.gtpl").Funcs(funcMap)
	files, _ := funcs.ParseFiles("customInTemplate/view/index.gtpl")
	date := time.Date(2025, 3, 13, 3, 4, 5, 0, time.Local)
	files.Execute(res, date)
}

func main() {
	server := http.Server{Addr: ":8090"}
	http.HandleFunc("/", welcome)
	server.ListenAndServe()
}

  • html代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
完整时间:{{.}}<br/>
年:{{.Year}}<br/>
月:{{.Month}}<br/>
转换后的格式为:{{.Format "2006-01-02 03-04-05"}}<br/>

调用自定义函数:{{cu .}}
</body>
</html>

Action

  • Go语言官方文档给出action(动作)的列表,"Arguments"和"pipelines"代表数据的执行结果
{{/* a comment */}}
    注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止,就像这里表示的一样。
{{pipeline}}
    pipeline的值的默认文本表示会被拷贝到输出里。
{{if pipeline}} T1 {{end}}
    如果pipeline的值为empty,不产生输出,否则输出T1执行结果。不改变dot的值。
    Empty值包括false0、任意nil指针或者nil接口,任意长度为0的数组、切片、字典。
{{if pipeline}} T1 {{else}} T0 {{end}}
    如果pipeline的值为empty,输出T0执行结果,否则输出T1执行结果。不改变dot的值。
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
    用于简化if-else链条,else action可以直接包含另一个if;等价于:
        {{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
{{range pipeline}} T1 {{end}}
    pipeline的值必须是数组、切片、字典或者通道。
    如果pipeline的值其长度为0,不会有任何输出;
    否则dot依次设为数组、切片、字典或者通道的每一个成员元素并执行T1;
    如果pipeline的值为字典,且键可排序的基本类型,元素也会按键的顺序排序。
{{range pipeline}} T1 {{else}} T0 {{end}}
    pipeline的值必须是数组、切片、字典或者通道。
    如果pipeline的值其长度为0,不改变dot的值并执行T0;否则会修改dot并执行T1。
{{template "name"}}
    执行名为name的模板,提供给模板的参数为nil,如模板不存在输出为""
{{template "name" pipeline}}
    执行名为name的模板,提供给模板的参数为pipeline的值。
{{with pipeline}} T1 {{end}}
    如果pipeline为empty不产生输出,否则将dot设为pipeline的值并执行T1。不修改外面的dot。
{{with pipeline}} T1 {{else}} T0 {{end}}
    如果pipeline为empty,不改变dot并执行T0,否则dot设为pipeline的值并执行T1。
  • action主要完成流程控制、循环、模块等操作。通过使用action可以在模板中完成简单逻辑处理(复杂逻辑处理应该在go中实现,传递给模板的数据应该时已经加工完的数据)

if使用

  • if写在模板中和写在go文件中功能是相同的,区别是语法
  • 布尔函数会将任何类型的零值视为假,其余视为真
  • if后面的表达式中如果包含逻辑控制符在模板中实际上是全局函数
and
    函数返回它的第一个empty参数或者最后一个参数;
    就是说"and x y"等价于"if x then y else x";所有参数都会执行;
or
    返回第一个非empty参数或者最后一个参数;
    亦即"or x y"等价于"if x then x else y";所有参数都会执行;
not
    返回它的单个参数的布尔值的否定
len
    返回它的参数的整数类型长度
index
    执行结果为第一个参数以剩下的参数为索引/键指向的值;
    如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。
print
    即fmt.Sprint
printf
    即fmt.Sprintf
println
    即fmt.Sprintln
html
    返回其参数文本表示的HTML逸码等价表示。
urlquery
    返回其参数文本表示的可嵌入URL查询的逸码等价表示。
js
    返回其参数文本表示的JavaScript逸码等价表示。
call
    执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;
    如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2);
    其中Y是函数类型的字段或者字典的值,或者其他类似情况;
    call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);
    该函数类型值必须有12个返回值,如果有2个则后一个必须是error接口类型;
    如果有2个返回值的方法返回的errornil,模板执行会中断并返回给调用模板执行者该错误;
  • 二元比较运算的集合:(也是函数,函数具有两个参数,满足参数语法)
eq      如果arg1 == arg2则返回真
ne      如果arg1 != arg2则返回真
lt      如果arg1 < arg2则返回真
le      如果arg1 <= arg2则返回真
gt      如果arg1 > arg2则返回真
ge      如果arg1 >= arg2则返回真
  • 代码实现
  • main代码
package main

import (
	"net/http"
	"text/template"
)

func welcome(res http.ResponseWriter, req *http.Request) {
	files, _ := template.ParseFiles("action_if/view/index.gtpl")
	files.Execute(res, "123")
}

func main() {
	server := http.Server{Addr: ":8090"}
	http.HandleFunc("/", welcome)
	server.ListenAndServe()
}

  • html代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--  取出数据{{.}}<br/>-->
<!--  {{if .}}-->
<!--    执行了if语句-->
<!--  {{else}}-->
<!--    执行了else语句-->
<!--  {{end}}-->
<!--  666666-->

  {{$n:=123}}
  {{if lt $n 456}}
    执行if语句
  {{else}}
    执行else语句
  {{end}}
</body>
</html>

range使用

  • range遍历数组或切片或map或channel时,在range内容中{{.}}标识获取迭代变量
  • 代码实现
  • main代码
package main

import (
	"net/http"
	"text/template"
)

func welcome(res http.ResponseWriter, req *http.Request) {
	files, _ := template.ParseFiles("action_range/view/index.gtpl")
	//strings := []string{"a", "b", "c", "d"}
	m := map[string]string{"key1": "value1", "key2": "value2", "key3": "value3", "key4": "value4"}
	files.Execute(res, m)
}

func main() {
	server := http.Server{Addr: ":8090"}
	http.HandleFunc("/", welcome)
	server.ListenAndServe()
}

  • html代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    {{range .}}
        {{.}}<br/>
    {{end}}
</body>
</html> 

模板嵌套

  • 在实际项目中经常出现页面复用的情况,例如:整个网站的头部信息和底部信息复用
  • 可以使用动作{{template “模板名称”}}引用模板
  • 引用的模板必须在HTML中定义这个模板
  • 定义方式
{{define "名称"}}
html
{{end}}
  • 执行主模版时也要给主模版一个名称,执行时调用的是ExecuteTemplate()方法
  • 代码实现

调用模板时同时传递参数

  • 如果直接引用html可以直接使用html标签的iframe,但是要动态效果时,可以在调用模板给模板传递参数
  • 代码实现

Web开发核心功能

文件上传

  • 文件上传:客户端把上传文件转换为二进制流后发送给服务器,服务器对二进制流进行解析
  • HTML表单(form)enctype(Encode Type)属性控制表单在提交数据到服务器时数据的编码类型
    • enctype="application/x-www-form-urlencoded"默认值,表单数据会被编码为名称/值形式
    • enctype="multipart/form-data"编码成消息,每个控件对应消息的一部分,请求方式必须是POST
    • enctype="text/plain"纯文本形式进行编码的
  • 服务端可以使用FormFile(“name”)获取上传到的文件,官方定义如下
// FormFile returns the first file for the provided form key.
// FormFile calls [Request.ParseMultipartForm] and [Request.ParseForm] if necessary.
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) {
	if r.MultipartForm == multipartByReader {
		return nil, nil, errors.New("http: multipart handled by MultipartReader")
	}
	if r.MultipartForm == nil {
		err := r.ParseMultipartForm(defaultMaxMemory)
		if err != nil {
			return nil, nil, err
		}
	}
	if r.MultipartForm != nil && r.MultipartForm.File != nil {
		if fhs := r.MultipartForm.File[key]; len(fhs) > 0 {
			f, err := fhs[0].Open()
			return f, fhs[0], err
		}
	}
	return nil, nil, ErrMissingFile
}
  • multipart.File是文件对象
// File is an interface to access the file part of a multipart message.
// Its contents may be either stored in memory or on disk.
// If stored on disk, the File's underlying concrete type will be an *os.File.
type File interface {
	io.Reader
	io.ReaderAt
	io.Seeker
	io.Closer
}
  • 封装了文件的基本信息
// A FileHeader describes a file part of a multipart request.
type FileHeader struct {
	Filename string
	Header   textproto.MIMEHeader
	Size     int64

	content   []byte
	tmpfile   string
	tmpoff    int64
	tmpshared bool
}

文件下载

  • 文件下载总体步骤
    • 客户端向服务端发起请求,请求参数包含要下载的文件的名称
    • 服务器接收到客户端请求后把文件设置到响应对象中,相应给客户端浏览器
  • 下载时需要设置的响应头信息
    • Content-Type:内容MIME类型
      • application/octet-stream 任意类型
    • Content-Disposition:客户端对内容的操作方式
      • inline默认值,表示浏览器能解析就解析,不能解析就下载
      • attachment;filename=下载时显示的文件名,客户端浏览器恒下载

json简介

  • 轻量级数据传输格式

  • 总体上分为两种:

    • 一种是JSONObject(json对象

      {"key":value,"key":value}
      
    • 一种时JSONArrayP(json数组),包含多个JSONObject

      [{"key":value},{"key":value}]
      
  • key是string类型,value可以是string类型(值被双引导号包含),也可以是数值或布尔类型等,也可以是JSONObject类型或JSONArray类型

  • 可以使用Go语言标准库中encoding/json包下的Marshal()或Unmarshal()把结构体对象转换成[]byte或把[]byte中信息写入到结构体对象中

    • 在转换过程中结构体属性tag中定义了json中的key,属性的值就是json中的value
    • 如果属性没有配置tag,属性就是json中的key
  • 属性的tag可以进行下面配置

    // 字段被本包忽略
    Field int `json:"-"`
    // 字段在json里的键为"myName"
    Field int `json:"myName"`
    // 字段在json里的键位"myName"且如果字段为空值将在对象中省略掉
    Field int `json:"myName,omitempty"`
    // 字段在json里的键为"Field"(默认值),但如果字段为空值会跳过;注意前导的逗号
    Field int `json:",omitempty"`
    

正则表达式

  • 正则表达式:(Regular Experssion)
  • 正则表达式就正则字符和普通字符组成字符串的规则
  • 正则内容如下
.任意字符(标志 s == true 时还包括换行符)
[xyz]字符族
[^xyz]反向字符族
\dPerl 预定义字符族
\D反向 Perl 预定义字符族
[:alpha:]ASCII 字符族
[:^alpha:]反向 ASCII 字符族
\p{H}Unicode 字符族(单字符名),参见 unicode 包
\P{H}反向 Unicode 字符族(单字符名)
\p{Greek}Unicode 字符族(完整字符名)
\P{Greek}反向 Unicode 字符族(完整字符名)

组合:

表达式含义
xy匹配 x 后接着匹配 y
xy匹配 x 或 y(优先匹配 x)

重复:

表达式含义
x*重复 >=0 次匹配 x,越多越好(优先重复匹配 x)
x+重复 >=1 次匹配 x,越多越好(优先重复匹配 x)
x?0 或 1 次匹配 x,优先 1 次
x{n,m}n 到 m 次匹配 x,越多越好(优先重复匹配 x)
x{n,}重复 >=n 次匹配 x,越多越好(优先重复匹配 x)
x{n}重复 n 次匹配 x
x*?重复 >=0 次匹配 x,越少越好(优先跳出重复)
x+?重复 >=1 次匹配 x,越少越好(优先跳出重复)
x??0 或 1 次匹配 x,优先 0 次

cookie

Cookie
  • Cookie就是客户端存储技术,以键值对的形式存在
  • 在B/S架构中,服务器端产生Cookie响应给客户端,浏览器接收后把Cookie存在在特定的文件夹中,以后每次请求浏览器会把Cookie内容放入到请求中
Go语言对Cookie的支持
  • 在net/http包下提供了Cookie结构体
    • Name设置Cookie的名称
    • Value表示Cookie的值
    • Path有效范围
    • Domain可访问Cookie的域
    • Expires过期时间
    • MaxAge最大存活时间,单位秒
    • HttpOnly是否可以通过脚本访问
type Cookie struct {
	Name  string
	Value string

	Path       string    // optional
	Domain     string    // optional
	Expires    time.Time // optional
	RawExpires string    // for reading cookies only

	// MaxAge=0 means no 'Max-Age' attribute specified.
	// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
	// MaxAge>0 means Max-Age attribute present and given in seconds
	MaxAge   int
	Secure   bool
	HttpOnly bool
	SameSite SameSite
	Raw      string
	Unparsed []string // Raw text of unparsed attribute-value pairs
}

Cookie常用设置

HttpOnly
  • HttpOnly:控制Cookie的内容是否可以被JavaScript访问到。通过设置HttpOnly为true时防止XSS攻击防御手段之一
  • 默认HttpOnly为false,表示客户端可以通过js获取
  • 在项目中导入jquery.cookie.js库,使用jquery获取客户端Cookie内容
Path
  • path属性设置Cookie的访问范围
  • 默认为"/"表示当前项目下所有都可以访问
  • Path设置路径及子路径内容都可以访问
  • 首先先访问index.html,点击超链接产生cookie,在浏览器地址栏输入localhost:8090/abc/mypath后发现可以访问cookie
Expires
  • Cookie默认存活时间时浏览器不关闭,当浏览器关闭后,Cookie失效
  • 可以通过Expires设置具体什么时候过期,Cookie失效,也可以通过MaxAge设置Cookie多长时间后实现
  • IE6,7,8和很多浏览器不支持MaxAge,建议使用Expires
  • Expires是time.Time类型,所有设置时需要明确设置过期时间
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值