65.Slice处理工具【Sort、Contains、Find】

本文详细介绍了Go语言中sort包的使用,包括对基本数据类型切片的排序、自定义结构体的排序方法、sort包的相关函数如逆序、搜索,以及如何在切片中实现查找和包含功能。

Go语言中,Slice是非常重要且使用频率极高的数据结构,但标准库对于Slice结构提供的方法很少。因此对于常见的排序、包含、查找等,我们可以自己提前写好工具包,方便要用的时候直接取用。

一、排序

1、sort包介绍

该包内部实现了插入排序,归并排序,堆排序和快速排序,但并不对外公开,即并不需要我们通过参数传入具体使用何种排序算法,而是在使用的时候会自动选择高效的排序算法。

外部Slice结构在使用的时候需要实现sort.Interface定义的三个方法

  • Len():获取集合长度
  • Less():比较两个元素大小的
  • Swap():交换两个元素位置。

sort包对切片类型提供了完整的支持,主要包括

  • 对基本数据类型切片的排序支持
  • 基本数据元素查找
  • 判断基本数据类型切片是否已经排好序
  • 对排好序的数据集合逆序

2、对自定义结构体集合进行排序

package main
import (
	"fmt"
	"sort"
)
// 学生成绩结构体
type StuScore struct {
	name  string    // 姓名
	score int   // 成绩
}
type StuScores []StuScore
//Len()获取集合长度
func (s StuScores) Len() int {
	return len(s)
}
//Less(): 两个集合元素进行比较,return true不执行Swap(),false执行。
// 可简单记为小于号则是从小到大排序,因为i位置的元素在前面,不和后面j位置更大的元素交换
func (s StuScores) Less(i, j int) bool {
	return s[i].score < s[j].score
}

//Swap()对的两个元素制定移动规则
func (s StuScores) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}
func main() {
	stus := StuScores{
		{"alan", 95},
		{"hikerell", 91},
		{"acmfly", 96},
		{"leao", 90},
	}
	// 打印未排序的 stus 数据
	fmt.Println("Default:\n\t",stus)
	//StuScores 已经实现了 sort.Interface 接口 , 所以可以调用 Sort 函数进行排序
	sort.Sort(stus)
	// 判断是否已经排好顺序,将会打印 true
	fmt.Println("IS Sorted?\n\t", sort.IsSorted(stus))
	// 打印排序后的 stus 数据
	fmt.Println("Sorted:\n\t",stus)
}

输出结果:

Default:
	 [{alan 95} {hikerell 91} {acmfly 96} {leao 90}]
IS Sorted?
	 true
Sorted:
	 [{leao 90} {hikerell 91} {alan 95} {acmfly 96}]

3、sort包的相关函数

  1. sort.Reverse():可以允许将数据按 Less() 定义的排序方式逆序排序,而不必修改 Less() 代码。

例如sort.Sort(sort.Reverse(stus))实现逆序。

//内部实现
// 定义了一个 reverse 结构类型,嵌入 Interface 接口
type reverse struct {
    Interface
}

//内部的Less()与自定义的Less()有着相反的逻辑
func (r reverse) Less(i, j int) bool {
    return r.Interface.Less(j, i)
}

// 返回新的实现 Interface 接口的数据类型
func Reverse(data Interface) Interface {
    return &reverse{data}
}
// 把stus传入到Recerse()方法中,返回在reverse结构体中的Interface,
// 然会外部调用Less的时候实际上是调用reverse结构体实现的Less方法,这个Less()方法与外部的有着相反的逻辑
sort.Sort(sort.Reverse(stus))//
fmt.Println(stus)
  1. sort.Search():
//方法定义
func Search(n int, f func(int) bool) int
// 该方法会使用“二分查找”算法来找出能使 f(x)(0<=x<n) 返回 ture 的最小值 i,即返回切片[0:n]索引对应元素第一个符合f函数的元素。 
// 前提条件 ,切片已经是有序的: 即f(x)(0<=x<i) 均返回 false, f(x)(i<=x<n) 均返回 ture。 如果不存在 i ,则返回 n。
x := 11
s := []int{3, 6, 8, 11, 45} // 注意已经升序排序
pos := sort.Search(len(s), func(i int) bool { return s[i] >= x })

if pos < len(s) && s[pos] == x {
    fmt.Println(x, " 在 s 中的位置为:", pos)
} else {
    fmt.Println("s 不包含元素 ", x)
}

4、sort包已经支持的内部数据类型排序

1、IntSlice 类型及[]int 排序

内部实现
由于[]int 切片排序内部实现及使用方法与[]float64 []string 类似,所以只详细描述该部分。

//IntSlice 类型及[]int 排序
//sort包定义了一个 IntSlice 类型,并且实现了 sort.Interface 接口:
type IntSlice []int
func (p IntSlice) Len() int           { return len(p) }
func (p IntSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p IntSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

//IntSlice 类型定义了 Sort() 方法,包装了 sort.Sort() 函数
func (p IntSlice) Sort() { Sort(p) }
//IntSlice 类型定义了 SearchInts() 方法,包装了 SearchInts() 函数
func (p IntSlice) Search(x int) int { return SearchInts(p, x) }

//并且提供的 sort.Ints() 方法使用了该 IntSlice 类型:所以直接使用该方法即可进行排序
func Ints(a []int) { Sort(IntSlice(a)) }

func Ints(a []int) { Sort(IntSlice(a)) }:默认升序排序

s := []int{5, 2, 6, 3, 1, 4} // 未排序的切片数据
sort.Ints(s)
fmt.Println(s) // 将会输出[1 2 3 4 5 6]
//使用Reverse方法进行降序
s := []int{5, 2, 6, 3, 1, 4} // 未排序的切片数据
sort.Sort(sort.Reverse(sort.IntSlice(s)))
fmt.Println(s) // 将会输出[6 5 4 3 2 1]

func SearchInts(a []int, x int) int :查找切片中元素,需要提前进行升序排序

2、Float64Slice 类型及[]float64 排序与IntSlice类似

go内部实现

type Float64Slice []float64

func (p Float64Slice) Len() int           { return len(p) }
func (p Float64Slice) Less(i, j int) bool { return p[i] < p[j] || isNaN(p[i]) && !isNaN(p[j]) }
func (p Float64Slice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
func (p Float64Slice) Sort() { Sort(p) }
func (p Float64Slice) Search(x float64) int { return SearchFloat64s(p, x) }

与 Sort()、IsSorted()、Search() 相对应的三个方法:

func Float64s(a []float64)  
func Float64sAreSorted(a []float64) bool
func SearchFloat64s(a []float64, x float64) int

3、StringSlice 类型及[]string 排序

内部实现

type StringSlice []string

func (p StringSlice) Len() int           { return len(p) }
func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p StringSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
func (p StringSlice) Sort() { Sort(p) }
func (p StringSlice) Search(x string) int { return SearchStrings(p, x) }

与 Sort()、IsSorted()、Search() 相对应的三个方法:

func Strings(a []string)
func StringsAreSorted(a []string) bool
func SearchStrings(a []string, x string) int

5、自定义结构体的简单化排序

sort.Slice():不稳定排序

    people := []struct {
    Name string
    Age  int
}{
    {"Gopher", 7},
    {"Alice", 55},
    {"Vera", 24},
    {"Bob", 75},
}

sort.Slice(people, func(i, j int) bool { return people[i].Age < people[j].Age }) // 按年龄升序排序
fmt.Println("Sort by age:", people)

sort.SliceStable():稳定排序,原集合中相同数据排序后仍然保持原集合的顺序

   people := []struct {
    Name string
    Age  int
}{
    {"Gopher", 7},
    {"Alice", 55},
    {"Vera", 24},
    {"Bob", 75},
}

sort.SliceStable(people, func(i, j int) bool { return people[i].Age > people[j].Age }) // 按年龄降序排序
fmt.Println("Sort by age:", people)

sort.SliceIsSorted():该函数根据自定义的规则判断集合是否为有序.

   people := []struct {
    Name string
    Age  int
}{
    {"Gopher", 7},
    {"Alice", 55},
    {"Vera", 24},
    {"Bob", 75},
}

sort.Slice(people, func(i, j int) bool { return people[i].Age > people[j].Age }) // 按年龄降序排序
fmt.Println("Sort by age:", people)
fmt.Println("Sorted:",sort.SliceIsSorted(people,func(i, j int) bool { return people[i].Age < people[j].Age }))

sort.Search():该函数判断集合是否存在指定元素,举个栗子:

a := []int{2, 3, 4, 200, 100, 21, 234, 56}
x := 21

sort.Slice(a, func(i, j int) bool { return a[i] < a[j] })   // 升序排序
index := sort.Search(len(a), func(i int) bool { return a[i] >= x }) // 查找元素

if index < len(a) && a[index] == x {
    fmt.Printf("found %d at index %d in %v\n", x, index, a)
} else {
    fmt.Printf("%d not found in %v,index:%d\n", x, a, index)
}

二、包含Contains

我们这里就写几个常见类型的,其他的类似

func ContainsString(s []string, v string) bool {
	for _, vv := range s {
		if vv == v {
			return true
		}
	}
	return false
}


func ContainsInt(s []int, v int) bool {
	for _, vv := range s {
		if vv == v {
			return true
		}
	}
	return false
}

func ContainsInt64(s []int64, v int64) bool {
	for _, vv := range s {
		if vv == v {
			return true
		}
	}
	return false
}

func ContainsFloat64(s []float64, v float64) bool {
	for _, vv := range s {
		if vv == v {
			return true
		}
	}
	return false
}

需要注意的是ContainsFloat64函数,对于浮点数的比较很多情况下我们不会用==号,而是对比两个浮点数相减是都小于某个很小的数

三、查找Find

对于查找,我们不是说列表中有某个指定的元素,那是上面介绍的包含,这里的查找指的是包含符合某条件的元素。既然要看列表中的元素是否符合某个元素,所以需要传递一个函数作为参数。

同样,也只给出常用类型的。

返回第一个符合条件的元素和索引。用-1表示列表中没有符合给定条件的元素。

func FindString(s []string, f func(v string) bool) (string, int) {
	for index, value := range s {
		result := f(value)
		if result {
			return value, index
		}
	}
	return "", -1
}

func FindInt(s []int, f func(v int) bool) (int, int) {
	for index, value := range s {
		result := f(value)
		if result {
			return value, index
		}
	}
	return 0, -1
}

func FindInt64(s []int64, f func(v int64) bool) (int64, int) {
	for index, value := range s {
		result := f(value)
		if result {
			return value, index
		}
	}
	return 0, -1
}

func FindFloat64(s []float64, f func(v float64) bool) (float64, int) {
	for index, value := range s {
		result := f(value)
		if result {
			return value, index
		}
	}
	return 0.0, -1
}
<template> <div class="cascader-select"> <!-- 输入框 --> <div class="input-wrapper" @click="toggleDropdown" > <input v-model="displayValue" :placeholder="placeholder" class="cascader-input" readonly /> <div class="input-icons"> <span v-if="selectedPath.length > 0" class="clear-icon" @click.stop="handleClear" > <el-icon><Close /></el-icon> </span> <span class="arrow-icon"> <el-icon v-if="showDropdown"><ArrowDown /></el-icon> <el-icon v-else><ArrowUp /></el-icon> </span> </div> </div> <!-- 级联下拉框 --> <div v-show="showDropdown" class="cascader-dropdown" > <div class="cascader-content"> <!-- 搜索框 --> <div class="search-input-wrapper"> <el-input ref="searchInput" v-model="searchKeyword" :placeholder="t('请输入')" maxlength="30" @input="handleSearchInput" @focus="handleSearchFocus" @blur="handleSearchBlur" @keydown.enter="handleSearchEnter" @keydown.esc="handleSearchEsc" /> <div class="input-icons"> <span class="search-input-icon"> <el-icon><Close @click.stop="clearSearchKeyword"/></el-icon> <span class="split-line"></span> <el-icon><Search @click.stop="handleSearchEnter"/></el-icon> </span> </div> </div> <!-- 搜索结果面板 --> <div v-if="showSearchResults" class="search-results-panel"> <div v-if="searchResults.length === 0" class="no-search-results"> <div class="no-results-text">{{ t('未找到匹配结果') }}</div> <div class="no-results-hint">{{ t('请尝试其他关键词') }}</div> </div> <div v-else class="search-results-content"> <!-- 大区分类 --> <div v-if="areaResults.length > 0" class="search-category"> <div class="category-header"> <span class="category-title">{{ t('大区') }}</span> </div> <div class="category-items"> <div v-for="item in areaResults" :key="item.fullPath.join('-')" class="search-result-item" @click="handleSearchResultClick(item)" > <div class="result-content"> <div class="result-label">{{ item.label }}</div> </div> </div> </div> </div> <!-- Region 分类 --> <div v-if="regionResults.length > 0" class="search-category"> <div class="category-header"> <span class="category-title">Region</span> </div> <div class="category-items"> <div v-for="item in regionResults" :key="item.fullPath.join('-')" class="search-result-item" @click="handleSearchResultClick(item)" > <div class="result-content"> <div class="result-label">{{ item.label }}</div> </div> </div> </div> </div> <!-- az分类 --> <div v-if="azResults.length > 0" class="search-category"> <div class="category-header"> <span class="category-title">AZ</span> </div> <div class="category-items"> <div v-for="item in azResults" :key="item.fullPath.join('-')" class="search-result-item" @click="handleSearchResultClick(item)" > <div class="result-content"> <div class="result-label">{{ item.label }}</div> </div> </div> </div> </div> </div> </div> <!-- 级联头部标注 --> <div class="cascader-header"> <div v-for="(panel, level) in cascaderPanels" :key="`header-${level}`" class="cascader-header-item" > {{ getHeaderLabel(level) }} </div> </div> <!-- 级联面板 --> <div class="cascader-panels"> <div v-for="(panel, level) in cascaderPanels" :key="level" class="cascader-panel" > <div v-for="option in panel" :key="option.name" class="cascader-option" :class="{ 'is-active': isOptionActive(option, level), 'is-selected': isOptionSelected(option, level), 'has-children': option.children && option.children.length > 0 }" @click="handleOptionClick(option, level)" > <span class="option-label">{{ option.nameCn }}</span> <span v-if="option.children?.length" class="option-arrow" > > </span> </div> </div> </div> </div> <!-- 操作按钮 --> <div class="cascader-footer"> <button class="btn btn-cancel" @click="handleCancel" > 取消 </button> <button class="btn btn-confirm" @click="handleConfirm" :disabled="!tempSelectedPath.length" > 确定 </button> </div> </div> </div> </template> <script setup lang="ts"> import { ref, computed, watch, onUnmounted } from 'vue'; import { Search, Close, ArrowDown, ArrowUp } from '@element-plus/icons-vue'; import { useI18n } from 'vue-i18n'; const { t } = useI18n(); // 类型定义 export interface CascaderOption { name: string; nameCn: string; nameEn: string; children?: CascaderOption[]; } export interface CascaderProps { options: CascaderOption[]; modelValue?: string[]; placeholder?: string; headerLabels?: string[]; } export interface SearchResultItem { name: string; label: string; path: string[]; fullPath: string[]; level: number; type: 'country' | 'area' | 'region' | 'az'; } export interface CascaderEmits { (event: 'update:modelValue', value: string[]): void; (event: 'change', value: string[], selectedOptions: CascaderOption[]): void; } // 定义props,带默认值 const props = withDefaults(defineProps<CascaderProps>(), { modelValue: () => [], placeholder: '请选择', headerLabels: () => ['区域', '大区', 'Region', 'AZ'], }); // 定义emits const emit = defineEmits<CascaderEmits>(); // 响应式数据 const searchInput = ref<HTMLInputElement>(); const showDropdown = ref<boolean>(false); const tempSelectedPath = ref<string[]>([...props.modelValue]); const selectedPath = ref<string[]>([...props.modelValue]); const searchKeyword = ref<string>(''); const searchResults = ref<SearchResultItem[]>([]); const showSearchResults = ref<boolean>(false); // 计算属性 - 级联面板数据 const cascaderPanels = computed<CascaderOption[][]>(() => { const panels: CascaderOption[][] = [props.options]; let currentOptions: CascaderOption[] = props.options; for (let i = 0; i < tempSelectedPath.value.length; i++) { const selectedValue = tempSelectedPath.value[i]; const selectedOption = currentOptions.find((option: CascaderOption) => option.name === selectedValue); if (selectedOption && selectedOption.children && selectedOption.children.length > 0) { panels.push(selectedOption.children); currentOptions = selectedOption.children; } else { break; } } return panels; }); // 计算属性 - 显示值(只显示最后一级的label) const displayValue = computed<string>(() => { if (!selectedPath.value.length) { return ''; } let currentOptions: CascaderOption[] = props.options; let finalLabel = ''; for (const value of selectedPath.value) { const option = currentOptions.find((opt: CascaderOption) => opt.name === value); if (option) { finalLabel = option.nameCn; currentOptions = option.children || []; } } return finalLabel; }); // 搜索结果按类别分组 const LEVEL_AREA = 1; const LEVEL_REGION = 2; const LEVEL_SITE = 3; const areaResults = computed<SearchResultItem[]>(() => { return searchResults.value.filter(item => item.level === LEVEL_AREA); }); const regionResults = computed<SearchResultItem[]>(() => { return searchResults.value.filter(item => item.level === LEVEL_REGION); }); const azResults = computed<SearchResultItem[]>(() => { return searchResults.value.filter(item => item.level === LEVEL_SITE); }); // 方法 - 切换下拉框显示状态 const toggleDropdown = (): void => { showDropdown.value = !showDropdown.value; if (showDropdown.value) { // 打开时重置临时选中路径 tempSelectedPath.value = [...selectedPath.value]; } else { // 关闭时清空搜索 searchKeyword.value = ''; searchResults.value = []; showSearchResults.value = false; } }; // 方法 - 检查选项是否处于激活状态 const isOptionActive = (option: CascaderOption, level: number): boolean => { return tempSelectedPath.value[level] === option.name; }; // 方法 - 检查选项是否已选中 const isOptionSelected = (option: CascaderOption, level: number): boolean => { return selectedPath.value[level] === option.name; }; // 方法 - 处理选项点击 const handleOptionClick = (option: CascaderOption, level: number): void => { const newPath = tempSelectedPath.value.slice(0, level); newPath.push(option.name); tempSelectedPath.value = newPath; }; // 方法 - 确认选择 const handleConfirm = (): void => { selectedPath.value = [...tempSelectedPath.value]; // 获取选中的完整选项对象 const selectedOptions: CascaderOption[] = []; let currentOptions: CascaderOption[] = props.options; for (const value of selectedPath.value) { const option = currentOptions.find((opt: CascaderOption) => opt.name === value); if (option) { selectedOptions.push(option); currentOptions = option.children || []; } } // 触发事件 emit('update:modelValue', selectedPath.value); emit('change', selectedPath.value, selectedOptions); // 关闭下拉框 showDropdown.value = false; }; // 方法 - 取消选择 const handleCancel = (): void => { tempSelectedPath.value = [...selectedPath.value]; showDropdown.value = false; }; // 方法 - 清空选择 const handleClear = (): void => { selectedPath.value = []; tempSelectedPath.value = []; emit('update:modelValue', []); emit('change', [], []); showDropdown.value = false; }; // 方法 - 清空搜索关键词 const clearSearchKeyword = (): void => { searchKeyword.value = []; }; // 方法 - 获取头部标签 const getHeaderLabel = (level: number): string => { return props.headerLabels[level] || `级别 ${level + 1}`; }; // 搜索相关方法 const flattenCascaderOptions = (options: CascaderOption[], parentPath: string[] = [], parentFullPath: string[] = [], level: number = 0): SearchResultItem[] => { const results: SearchResultItem[] = []; options.forEach(option => { const currentPath = [...parentPath, option.nameCn]; const currentFullPath = [...parentFullPath, option.name]; // 确定类型(四层级联:国家、区域、Zone、AZ) let type: 'country' | 'area' | 'region' | 'az' = 'country'; if (level === 1) { type = 'area'; } else if (level === 2) { type = 'region'; } else if (level === 3) { type = 'az'; } else { // 不做任何处理 } results.push({ name: option.name, label: option.nameCn, path: currentPath, fullPath: currentFullPath, level, type, }); // 递归处理子选项 if (option.children && option.children.length > 0) { results.push(...flattenCascaderOptions(option.children, currentPath, currentFullPath, level + 1)); } }); return results; }; const performSearch = (keyword: string): void => { if (!keyword.trim()) { searchResults.value = []; showSearchResults.value = false; return; } const allOptions = flattenCascaderOptions(props.options); const filteredResults = allOptions.filter(item => { const keywordLower = keyword.toLowerCase(); return ( item.label.toLowerCase() .includes(keywordLower) || item.name.toLowerCase() .includes(keywordLower) || item.path.some(pathItem => pathItem.toLowerCase() .includes(keywordLower)) ); }); // 按相关性排序 filteredResults.sort((a, b) => { const keywordLower = keyword.toLowerCase(); // 完全匹配优先 const aExactMatch = a.label.toLowerCase() === keywordLower; const bExactMatch = b.label.toLowerCase() === keywordLower; if (aExactMatch && !bExactMatch) { return -1; } if (!aExactMatch && bExactMatch) { return 1; } // 开头匹配次之 const aStartsWithMatch = a.label.toLowerCase() .startsWith(keywordLower); const bStartsWithMatch = b.label.toLowerCase() .startsWith(keywordLower); if (aStartsWithMatch && !bStartsWithMatch) { return -1; } if (!aStartsWithMatch && bStartsWithMatch) { return 1; } // 按级别排序(先显示上级) if (a.level !== b.level) { return a.level - b.level; } // 最后按字母顺序 return a.label.localeCompare(b.label); }); searchResults.value = filteredResults; showSearchResults.value = true; }; const handleSearchInput = (): void => { performSearch(searchKeyword.value); }; const handleSearchFocus = (): void => { if (searchKeyword.value) { showSearchResults.value = true; } }; const handleSearchBlur = (): void => { // 延迟隐藏,允许点击搜索结果 setTimeout(() => { showSearchResults.value = false; }, 200); }; const handleSearchEnter = (): void => { if (searchKeyword.value.trim()) { performSearch(searchKeyword.value.trim()); } }; const handleSearchEsc = (): void => { searchKeyword.value = ''; searchResults.value = []; showSearchResults.value = false; if (searchInput.value) { searchInput.value.blur(); } }; const handleSearchResultClick = (item: SearchResultItem): void => { // 设置选中路径 selectedPath.value = [...item.fullPath]; tempSelectedPath.value = [...item.fullPath]; // 获取选中的选项对象 const selectedOptions: CascaderOption[] = []; let currentOptions: CascaderOption[] = props.options; for (const value of item.fullPath) { const option = currentOptions.find((opt: CascaderOption) => opt.name === value); if (option) { selectedOptions.push(option); currentOptions = option.children || []; } } // 触发事件 emit('update:modelValue', [...item.fullPath]); emit('change', [...item.fullPath], selectedOptions); // 清空搜索并关闭面板 searchKeyword.value = ''; searchResults.value = []; showSearchResults.value = false; showDropdown.value = false; }; // 监听外部值变化 watch(() => props.modelValue, (newValue: string[]) => { selectedPath.value = [...newValue]; tempSelectedPath.value = [...newValue]; }, { deep: true }); // 点击外部关闭下拉框 const handleClickOutside = (event: Event): void => { const target = event.target as HTMLElement; const cascaderElement = document.querySelector('.cascader-select') as HTMLElement; if (cascaderElement && !cascaderElement.contains(target)) { showDropdown.value = false; } }; // 添加/移除全局点击监听 watch(showDropdown, (isShow: boolean) => { if (isShow) { document.addEventListener('click', handleClickOutside); } else { document.removeEventListener('click', handleClickOutside); } }); onUnmounted(() => { document.removeEventListener('click', handleClickOutside); }); </script> 我想要搜索结果面板,只显示搜索词在内的结果,regionResults、azResults我只取搜索词在内的搜索结果
09-24
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值