广告
返回顶部
首页 > 资讯 > 后端开发 > GO >go语言PflagViperCobra核心功能使用介绍
  • 902
分享到

go语言PflagViperCobra核心功能使用介绍

2024-04-02 19:04:59 902人浏览 安东尼
摘要

目录1.如何构建应用框架2.命令行参数解析工具:Pflag2.1 Pflag 包 Flag 定义2.2 Pflag 包 FlagSet 定义2.3 Pflag 使用方法3.配置解析神

1.如何构建应用框架

一般来说构建应用框架包含3个部分:

  • 命令行参数解析
  • 配置文件解析
  • 应用的命令行框架:需要具备 Help 功能、需要能够解析命令行参数和配置文件、命令需要能够初始化业务代码,并最终启动业务进程 上3个需求便涉及Pflag、Viper、Cobra的使用,并且这三个包也是相互联系滴

2.命令行参数解析工具:Pflag

虽然 Go 源码中提供了一个标准库 Flag 包,用来对命令行参数进行解析,但在大型项目中应用更广泛的是另外一个包:Pflag

2.1 Pflag 包 Flag 定义

Pflag 可以对命令行参数进行处理,一个命令行参数在 Pflag 包中会解析为一个 Flag 类型的变量

type Flag struct {
    Name                string // flag长选项的名称
    Shorthand           string // flag短选项的名称,一个缩写的字符
    Usage               string // flag的使用文本
    Value               Value  // flag的值
    DefValue            string // flag的默认值
    Changed             bool // 记录flag的值是否有被设置过
    NoOptDefVal         string // 当flag出现在命令行,但是没有指定选项值时的默认值
    Deprecated          string // 记录该flag是否被放弃
    Hidden              bool // 如果值为true,则从help/usage输出信息中隐藏该flag
    ShorthandDeprecated string // 如果flag的短选项被废弃,当使用flag的短选项时打印该信息
    Annotations         map[string][]string // 给flag设置注解
}

Flag 的值是一个 Value 类型的接口,Value 定义如下

type Value interface {
    String() string // 将flag类型的值转换为string类型的值,并返回string的内容
    Set(string) error // 将string类型的值转换为flag类型的值,转换失败报错
    Type() string // 返回flag的类型,例如:string、int、ip等
}

2.2 Pflag 包 FlagSet 定义

Pflag 除了支持单个的 Flag 之外,还支持 FlagSet。FlagSet 是一些预先定义好的 Flag 的集合

  • 调用 NewFlagSet 创建一个 FlagSet
  • 使用 Pflag 包定义的全局 FlagSet:CommandLine
var version bool
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
flagSet.BoolVar(&version, "version", true, "Print version infORMation and quit.")
=================================
import (
    "GitHub.com/spf13/pflag"
)
pflag.BoolVarP(&version, "version", "v", true, "Print version information and quit.")
func BoolVarP(p *bool, name, shorthand string, value bool, usage string) {
    flag := CommandLine.VarPF(newBoolValue(value, p), name, shorthand, usage)
    flag.NoOptDefVal = "true"
}

2.3 Pflag 使用方法

  • 支持多种命令行参数定义方式
// 支持长选项、默认值和使用文本,并将标志的值存储在指针中。
var name = pflag.String("name", "colin", "Input Your Name")
// 支持长选项、短选项、默认值和使用文本,并将标志的值存储在指针中
var name = pflag.StringP("name", "n", "colin", "Input Your Name")
// 支持长选项、默认值和使用文本,并将标志的值绑定到变量。
var name string
pflag.StringVar(&name, "name", "colin", "Input Your Name")
// 支持长选项、短选项、默认值和使用文本,并将标志的值绑定到变量。
var name string
pflag.StringVarP(&name, "name", "n","colin", "Input Your Name")
// 函数名带Var说明是将标志的值绑定到变量,否则是将标志的值存储在指针中
// 函数名带P说明支持短选项,否则不支持短选项。
  • 使用Get获取参数的值。
i, err := flagset.GetInt("flagname")
  • 获取非选项参数。
package main
import (
    "fmt"
    "github.com/spf13/pflag"
)
var (
    flagvar = pflag.Int("flagname", 1234, "help message for flagname")
)
// 在定义完标志之后,可以调用pflag.Parse()来解析定义的标志。解析后,可通过pflag.Args()返回所有的非选项参数,通过pflag.Arg(i)返回第 i 个非选项参数。参数下标 0 到 pflag.NArg() - 1。
func main() {
    pflag.Parse()
    fmt.Printf("argument number is: %v\n", pflag.NArg())
    fmt.Printf("argument list is: %v\n", pflag.Args())
    fmt.Printf("the first argument is: %v\n", pflag.Arg(0))
}
  • 指定了选项但是没有指定选项值时的默认值。
var ip = pflag.IntP("flagname", "f", 1234, "help message")
pflag.Lookup("flagname").NoOptDefVal = "4321"
--flagname=1357 ==> 1357
--flagname ==> 4321
[nothing] ==> 1234
  • 弃用标志或者标志的简写
// deprecate a flag by specifying its name and a usage message
pflag.CommandLine.MarkDeprecated("logmode", "please use --log-mode instead")
  • 保留名为 port 的标志,但是弃用它的简写形式
pflag.IntVarP(&port, "port", "P", 3306, "Mysql service host port.")
// deprecate a flag shorthand by specifying its flag name and a usage message
pflag.CommandLine.MarkShorthandDeprecated("port", "please use --port only")
  • 隐藏标志。
// 可以将 Flag 标记为隐藏的,这意味着它仍将正常运行,但不会显示在 usage/help 文本中。
// hide a flag by specifying its name
pflag.CommandLine.MarkHidden("secretFlag")

3.配置解析神器:Viper

几乎所有的后端服务,都需要一些配置项来配置我们的服务;Viper 是 Go 应用程序现代化的、完整的解决方案,能够处理不同格式的配置文件 Viper 可以从不同的位置读取配置,不同位置的配置具有不同的优先级:

  • 通过 viper.Set 函数显示设置的配置
  • 命令行参数
  • 环境变量
  • 配置文件
  • Key/Value 存储
  • 默认值

3.1读入配置

读入配置,就是将配置读入到 Viper 中,有如下读入方式:

  • 设置默认值。
// 当没有通过配置文件、环境变量、远程配置或命令行标志设置 key 时,设置默认值通常是很有用的
viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})
  • 读取配置文件
package main
import (
  "fmt"
  "github.com/spf13/pflag"
  "github.com/spf13/viper"
)
var (
  cfg  = pflag.StringP("config", "c", "", "Configuration file.")
  help = pflag.BoolP("help", "h", false, "Show this help message.")
)
func main() {
  pflag.Parse()
  if *help {
    pflag.Usage()
    return
  }
  // 从配置文件中读取配置
  if *cfg != "" {
    viper.SetConfigFile(*cfg)   // 指定配置文件名
    viper.SetConfigType("yaml") // 如果配置文件名中没有文件扩展名,则需要指定配置文件的格式,告诉viper以何种格式解析文件
  } else {
    viper.AddConfigPath(".")          // 把当前目录加入到配置文件的搜索路径中
    viper.AddConfigPath("$HOME/.iam") // 配置文件搜索路径,可以设置多个配置文件搜索路径
    viper.SetConfigName("config")     // 配置文件名称(没有文件扩展名)
  }
  if err := viper.ReadInConfig(); err != nil { // 读取配置文件。如果指定了配置文件名,则使用指定的配置文件,否则在注册的搜索路径中搜索
    panic(fmt.Errorf("Fatal error config file: %s \n", err))
  }
  fmt.Printf("Used configuration file is: %s\n", viper.ConfigFileUsed())
}
  • 监听和重新读取配置文件。 Viper 支持在运行时让应用程序实时读取配置文件,也就是热加载配置
viper.WatchConfig()
viper.OnConfiGChange(func(e fsnotify.Event) {
   // 配置文件发生变更之后会调用的回调函数
  fmt.Println("Config file changed:", e.Name)
})
//不建议在实际开发中使用热加载功能,因为即使配置热加载了,程序中的代码也不一定会热加载。例如:修改了服务监听端口,但是服务没有重启,这时候服务还是监听在老的端口上,会造成不一致
  • 设置配置值
// 可以通过 viper.Set() 函数来显式设置配置:
viper.Set("user.username", "colin")
  • 使用环境变量
// 使用环境变量
os.Setenv("VIPER_USER_SECRET_ID", "QLdywI2MrmDVjsSv6e95weNRvmteRjfKAuNV")
os.Setenv("VIPER_USER_SECRET_KEY", "bVix2WBv0VPfrDrvlLWrhEdzjLpPCNYb")
viper.AutomaticEnv()                                             // 读取环境变量
viper.SetEnvPrefix("VIPER")                                      // 设置环境变量前缀:VIPER_,如果是viper,将自动转变为大写。
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) // 将viper.Get(key) key字符串中'.'和'-'替换为'_'
viper.BindEnv("user.secret-key")
viper.BindEnv("user.secret-id", "USER_SECRET_ID") // 绑定环境变量名到key
  • 使用标志 Viper 支持 Pflag 包,能够绑定 key 到 Flag。与 BindEnv 类似,在调用绑定方法时,不会设置该值,但在访问它时会设置。
viper.BindPFlag("token", pflag.Lookup("token")) // 绑定单个标志
viper.BindPFlags(pflag.CommandLine)             //绑定标志集

3.2 读取配置

  • 访问嵌套的键。
{
    "host": {
        "address": "localhost",
        "port": 5799
    },
    "datastore": {
        "metric": {
            "host": "127.0.0.1",
            "port": 3099
        },
        "warehouse": {
            "host": "198.0.0.1",
            "port": 2112
        }
    }
}
viper.GetString("datastore.metric.host") // (返回 "127.0.0.1")
  • 反序列化 Viper 可以支持将所有或特定的值解析到结构体、map 等。可以通过两个函数来实现:
type config struct {
  Port int
  Name string
  PathMap string `mapstructure:"path_map"`
}
var C config
err := viper.Unmarshal(&C)
if err != nil {
  t.Fatalf("unable to decode into struct, %v", err)
}

Viper 在后台使用github.com/mitchellh/mapstructure来解析值,其默认情况下使用mapstructure tags。当我们需要将 Viper 读取的配置反序列到我们定义的结构体变量中时,一定要使用 mapstructure tags

  • 序列化成字符串
import (
    yaml "gopkg.in/yaml.v2"
    // ...
)
func yamlStringSettings() string {
    c := viper.AllSettings()
    bs, err := yaml.Marshal(c)
    if err != nil {
        log.Fatalf("unable to marshal config to YAML: %v", err)
    }
    return string(bs)
}

4.现代化的命令行框架:Cobra

Cobra 既是一个可以创建强大的现代 CLI 应用程序的库,也是一个可以生成应用和命令文件的程序 应用程序通常遵循如下模式:APPNAME VERB NOUN --ADJECTIVE或者APPNAME COMMAND ARG --FLAG VERB 代表动词,NOUN 代表名词,ADJECTIVE 代表形容词

4.1 使用 Cobra 库创建命令

  • 创建 rootCmd
$ mkdir -p newApp2 && cd newApp2
// 通常情况下,我们会将 rootCmd 放在文件 cmd/root.go 中。
var rootCmd = &cobra.Command{
  Use:   "hugo",
  Short: "Hugo is a very fast static site generator",
  Long: `A Fast and Flexible Static Site Generator built with
                love by spf13 and friends in Go.
                Complete documentation is available at Http://hugo.spf13.com`,
  Run: func(cmd *cobra.Command, args []string) {
    // Do Stuff Here
  },
}
func Execute() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
}
// 还可以在 init() 函数中定义标志和处理配置
import (
  "fmt"
  "os"
  homedir "github.com/mitchellh/go-homedir"
  "github.com/spf13/cobra"
  "github.com/spf13/viper"
)
var (
    cfgFile     string
    projectBase string
    userLicense string
)
func init() {
  cobra.OnInitialize(initConfig)
  rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
  rootCmd.PersistentFlags().StringVarP(&projectBase, "projectbase", "b", "", "base project directory eg. github.com/spf13/")
  rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "Author name for copyright attribution")
  rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "Name of license for the project (can provide `licensetext` in config)")
  rootCmd.PersistentFlags().Bool("viper", true, "Use Viper for configuration")
  viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
  viper.BindPFlag("projectbase", rootCmd.PersistentFlags().Lookup("projectbase"))
  viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
  viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
  viper.SetDefault("license", "apache")
}
func initConfig() {
  // Don't forget to read config either from cfgFile or from home directory!
  if cfgFile != "" {
    // Use config file from the flag.
    viper.SetConfigFile(cfgFile)
  } else {
    // Find home directory.
    home, err := homedir.Dir()
    if err != nil {
      fmt.Println(err)
      os.Exit(1)
    }
    // Search config in home directory with name ".cobra" (without extension).
    viper.AddConfigPath(home)
    viper.SetConfigName(".cobra")
  }
  if err := viper.ReadInConfig(); err != nil {
    fmt.Println("Can't read config:", err)
    os.Exit(1)
  }
}
  • 创建 main.go 我们还需要一个 main 函数来调用 rootCmd,通常我们会创建一个 main.go 文件,在 main.go 中调用 rootCmd.Execute() 来执行命令
package main
import (
  "{pathToYourApp}/cmd"
)
func main() {
  cmd.Execute()
}
  • 添加命令 通常情况下,我们会把其他命令的源码文件放在 cmd/ 目录下,例如,我们添加一个 version 命令,可以创建 cmd/version.go 文件
package cmd
import (
  "fmt"
  "github.com/spf13/cobra"
)
func init() {
  rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
  Use:   "version",
  Short: "Print the version number of Hugo",
  Long:  `All software has versions. This is Hugo's`,
  Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("Hugo Static Site Generator v0.9 -- HEAD")
  },
}
  • 编译并运行
$ go mod init github.com/marmotedu/gopractise-demo/cobra/newApp2
$ go build -v .
$ ./newApp2 -h
A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.
Complete documentation is available at http://hugo.spf13.com
Usage:
hugo [flags]
hugo [command]
Available Commands:
help Help about any command
version Print the version number of Hugo
Flags:
-a, --author string Author name for copyright attribution (default "YOUR NAME")
--config string config file (default is $HOME/.cobra.yaml)
-h, --help help for hugo
-l, --license licensetext Name of license for the project (can provide licensetext in config)
-b, --projectbase string base project directory eg. github.com/spf13/
--viper Use Viper for configuration (default true)
 Use "hugo [command] --help" for more information about a command.

4.2使用标志

Cobra 可以跟 Pflag 结合使用,实现强大的标志功能

// 使用持久化的标志
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
// 分配一个本地标志,本地标志只能在它所绑定的命令上使用
rootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
// 将标志绑定到 Viper
var author string
func init() {
 rootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution")
 viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
}
// 设置标志为必选
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")

5.总结

在开发 Go 项目时,我们可以通过 Pflag 来解析命令行参数,通过 Viper 来解析配置文件,用 Cobra 来实现命令行框架。

你可以通过 pflag.String()、 pflag.StringP()、pflag.StringVar()、pflag.StringVarP() 方法来设置命令行参数,并使用 Get 来获取参数的值

以上就是go语言Pflag Viper Cobra 核心功能使用介绍的详细内容,更多关于Go语言Pflag Viper Cobra功能的资料请关注编程网其它相关文章!

您可能感兴趣的文档:

--结束END--

本文标题: go语言PflagViperCobra核心功能使用介绍

本文链接: https://www.lsjlt.com/news/121195.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • go语言PflagViperCobra核心功能使用介绍
    目录1.如何构建应用框架2.命令行参数解析工具:Pflag2.1 Pflag 包 Flag 定义2.2 Pflag 包 FlagSet 定义2.3 Pflag 使用方法3.配置解析神...
    99+
    2022-11-11
  • Go语言使用select{}阻塞main函数介绍
    很多时候我们需要让main函数不退出,让它在后台一直执行,例如: func main() { for i := 0; i < 20; i++ { //启动20个...
    99+
    2022-06-07
    main GO main函数 go语言 select
  • java guava主要功能介绍及使用心得总结
    目录1. 前言2. Guava主要功能介绍2.1 集合操作2.2 缓存2.3 字符串处理2.4 函数式编程2.5 其他实用工具3. 结论1. 前言 Guava是一个由Google开发...
    99+
    2023-05-16
    java guava使用 java guava
  • Go语言中定时任务库Cron使用方法介绍
    目录快速入门Cron表达式格式预定义时间表设置时区常用的方法介绍快速入门 安装cron,注意这里安装的是v3版本。新版本和旧版时间使用有所区别 go get github.com/r...
    99+
    2022-11-13
  • go语言代码生成器code generator使用示例介绍
    目录代码生成器介绍code-generator示例代码生成tag全局tag局部tag补充代码生成器介绍 client-go为每种k8s内置资源提供了对应的clientset和info...
    99+
    2022-11-13
  • Go语言中的Base64编码原理介绍以及使用
    目录前言Go Base64编码什么是Base64编码为什么需要Base64编码Base64编码原理编码步骤位数不足情况Base64解码原理Base64标准编码变种总结前言 在网络中传...
    99+
    2022-11-13
  • 使用Go语言进行大数据处理的基础知识介绍
    使用Go语言进行大数据处理的基础知识介绍随着互联网的快速发展,数据量的爆炸式增长已经成为一种常态。对于大数据的处理,选择合适的编程语言非常重要。Go语言,作为一种简洁、高效、并发的编程语言,逐渐成为大数据处理的首选语言。本文将介绍在Go语言...
    99+
    2023-12-23
    go语言:Go 大数据处理:大数据 基础知识:基础
  • go语言context包功能及操作使用详解
    目录Context包到底是干嘛用的?context原理什么时候应该使用 Context?如何创建 Context?主协程通知有子协程,子协程又有多个子协程context核心接口emp...
    99+
    2022-11-13
  • 怎么使用Go语言实现数据转发功能
    这篇文章主要介绍“怎么使用Go语言实现数据转发功能”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“怎么使用Go语言实现数据转发功能”文章能帮助大家解决问题。首先,我们需要考虑数据实体的格式。在许多情况...
    99+
    2023-07-06
  • go语言context包功能及操作使用的方法
    本篇内容介绍了“go语言context包功能及操作使用的方法”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!Context包到底是干嘛用的?我...
    99+
    2023-06-30
  • 如何使用Go语言和Redis实现购物车功能
    如何使用Go语言和Redis实现购物车功能购物车是电商网站必备的功能之一,它允许用户将他们感兴趣的商品添加到购物车中,然后可以随时查看、编辑和结算购物车中的商品。在本文中,我们将以Go语言为例,结合Redis数据库,实现购物车功能。环境准备...
    99+
    2023-10-27
    Go语言 redis 购物车功能
  • 如何使用MySQL和Go语言实现用户注册功能
    如何使用MySQL和Go语言实现用户注册功能开发一个具有用户注册功能的网站或应用程序,是很常见的需求。本文将介绍如何使用MySQL数据库和Go语言来实现用户注册功能,包括数据库的设计和操作、Go语言的路由和处理函数、表单验证、密码加密等内容...
    99+
    2023-10-22
    MySQL Go语言 用户注册
  • 如何使用Go语言和Redis实现社交网络功能
    如何使用Go语言和Redis实现社交网络功能引言:社交网络在现代人的日常生活中扮演着重要的角色,为人们提供了沟通交流、分享生活和建立关系的平台。在构建社交网络应用程序时,选择合适的技术栈至关重要。本文将向读者介绍如何使用Go语言和Redis...
    99+
    2023-10-27
    Go语言 redis 社交网络功能
  • 如何使用Go语言和Redis开发实时聊天功能
    如何使用Go语言和Redis开发实时聊天功能在如今这个互联网时代,聊天功能已经成为了大部分应用的基本需求。为了实现实时聊天功能,我们可以使用Go语言和Redis作为后台技术支持。Go语言是一种高效且简洁的编程语言,而Redis则是一个开源的...
    99+
    2023-10-28
    Go语言 redis 实时聊天
  • go语言定时器Timer及Ticker的功能使用示例详解
    目录定时器1-"*/5 * * * * *"设置说明定时器2-Timer-TickerTimer-只执行一次Ticker-循环执行Timer延时功能停止和重置定时...
    99+
    2022-11-13
  • 如何使用Go语言和Redis实现文件上传下载功能
    如何使用Go语言和Redis实现文件上传下载功能简介在现代Web应用开发中,文件上传和下载是常见的功能需求。本文将介绍如何使用Go语言和Redis来实现文件上传和下载功能,并提供具体的代码示例。一、文件上传功能实现文件上传功能是指将客户端的...
    99+
    2023-10-26
    Go语言 redis 文件传输
  • 如何在go语言中使用接口实现日志存储功能?
    在Go语言中,接口是一个非常强大的概念。它可以让我们以一种更加抽象的方式来描述代码中的行为,并且可以让我们更好地复用代码。在本文中,我们将探讨如何使用接口实现日志存储功能。 首先,让我们来看一下日志存储功能需要实现哪些功能。通常来说,日志存...
    99+
    2023-08-22
    日志 接口 存储
  • Go 语言中如何使用接口实现二维码扫描功能?
    随着移动互联网的快速发展,二维码已经成为了一种不可或缺的技术。而在开发过程中,如何快速、高效地实现二维码扫描功能是一个关键问题。本文将介绍如何使用 Go 语言中的接口实现二维码扫描功能,帮助开发者快速实现这一功能。 一、了解二维码扫描的原...
    99+
    2023-08-27
    二维码 索引 接口
  • 如何使用Go语言开发点餐系统的外卖配送功能
    如何使用Go语言开发点餐系统的外卖配送功能随着外卖行业的快速发展,越来越多的餐馆和用户开始使用点餐系统以及外卖配送服务。本文将介绍如何使用Go语言开发一个基于点餐系统的外卖配送功能,包括订单管理、骑手派送、订单状态跟踪等。系统概述点餐系统的...
    99+
    2023-11-01
    Go语言 点餐系统 外卖配送
  • 如何使用Go语言开发点餐系统的评价管理功能
    如何使用Go语言开发点餐系统的评价管理功能引言:现代社会的高速发展,使得人们对于美食的需求越来越多样化。随着外卖行业的兴起,点餐系统成为了餐饮业务的重要组成部分。而评价管理功能在点餐系统中发挥着重要的作用,帮助商家实时获取用户反馈,进一步提...
    99+
    2023-11-01
    Go语言开发 评价管理 点餐系统
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作