【Bluebell】项目总结:基于 golang 的前后端分离 web 项目实战

06-01 1531阅读

文章目录

  • Bluebell 项目总结:基于 golang 的前后端分离 web 项目实战
  • 项目入口:先从 main 函数入手
  • 准备工作:加载配置、初始化日志、初始化数据库等
    • 加载配置
    • 初始化日志
    • 初始化 MySQL 连接
    • 初始化 Redis 连接
    • 初始化雪花算法
    • 初始化 GIN 框架内置的校验器所使用的翻译器
    • 注册路由并启动服务
    • 路由注册:将具体的业务与路由相绑定
      • Middleware
        • RateLimitMiddleware:限流中间件,基于令牌桶算法
        • JWTAuthMiddleware:鉴权中间件,用于处理与登录相关的业务
        • 中间件总结
        • 将业务逻辑与路由相绑定:以基本的 MySQL CRUD 为例
          • Response 的封装
          • 参数的封装
          • signup:用户注册业务
            • Controller
            • Logic
            • Dao:MySQL
            • login:用户登录业务
              • Controller
              • Logic
              • Dao:MySQL
              • community:拉取社区信息
                • Controller
                • Logic
                • Dao
                • posts:获取帖子信息
                  • Controller
                  • Logic
                  • Dao
                  • post:发表帖子
                    • Controller
                    • Logic
                    • Dao:MySQL
                    • Redis Keys
                    • Dao:Redis
                    • 业务逻辑进阶:通过 Redis 当中帖子的分数降序地获取帖子列表
                      • Controller
                      • Logic
                        • Logic:GetPostList2,从所有社区查询帖子并按分数排序
                        • Logic:GetCommunityPostList2,从特定的社区查询帖子并按分数排序
                        • Dao:Redis
                          • Redis:GetpostIDsInOrder
                          • Redis:GetCommunityPostIDsInOrder
                          • Redis:GetPostVoteData
                          • Dao:MySQL
                            • MySQL:GetPostListByIDs
                            • MySQL:GetUserByID
                            • MySQL:GetCommunityDetailByID
                            • 业务逻辑进阶:基于 Redis 实现帖子点赞并记录
                              • Controller
                              • Logic
                              • Dao
                              • 工程化:以 Swagger 生成 RESTful 风格接口文档 / pprof 进行性能调优 / Docker 部署为例
                                • Swagger:生成 RESTful 风格的接口文档
                                  • 第一步:添加注释
                                  • 第二部:生成接口文档数据
                                  • 第三步:引入 gin-swagger 渲染文档数据
                                  • 使用 pprof 进行性能调优
                                    • go tool pprof
                                    • pprof + go-wrk
                                    • 使用 Docker 对 bluebell 项目进行部署

                                      Bluebell 项目总结:基于 golang 的前后端分离 web 项目实战

                                      Bluebell 项目是 q1mi 老师 Golang web 开发进阶课程的实战项目,概括地来说这是一个仿 Reddit 论坛的前后端分离 Web 项目,由于 q1mi 老师的课程是付费的,因此我在学习的过程中并没有通过文章的形式对学习的过程进行记录,感兴趣的同学可以前往平台订阅 q1mi 老师的课程。本篇文章仅用于对 Bluebell 项目进行总结,以梳理 Golang web 开发思路,仅做学习与复习之用。

                                      Bluebell 项目涉及到的技术栈比较完整,基本上可以应对大部分基于 Golang 进行的 Web 应用开发所需要的技术。所涉及的技术包括:

                                      • Golang 的 GIN Web 框架实战;
                                      • 将 zap 日志库集成到 GIN 当中用于日志记录;
                                      • 通过 viper 进行参数配置管理;
                                      • 基于 Controller + Logic + Dao(CLD 分层,有别于 MVC 开发模式)对应用进行分层,搭建 Web 开发的脚手架;
                                      • Golang + MySQL(使用 sqlx 库)进行 CRUD;
                                      • Golang + Redis(使用 go-redis 库)实现帖子的点赞计数;
                                      • 在 GIN 框架当中集成 JWT 鉴权;
                                      • 使用 snowflake 算法生成唯一的用户标识 ID;
                                      • 使用令牌桶算法进行访问限流;
                                      • 使用 Swagger 生成 RESTful 风格的接口文档;
                                      • 通过 Docker 对 bluebell 项目进行部署。
                                      • 使用 pprof 对 golang 程序进行性能分析;
                                      • 使用 go-wrk 对 web 项目进行压力测试;

                                        可以说,bluebell 项目基本上涵盖了所有必要的后端开发技术栈,其不足在于缺少时兴的诸如消息队列中间件、NoSQL 数据库等的实战使用。此外,bluebell 是一个单体 web 应用,不涉及到更高阶的微服务开发部分。通过 bluebell 项目对基于 golang 的 MySQL CRUD 以及 Redis 的使用进行熟悉可以说非常的合适。

                                        下面让我们从 main 函数开始,对 bluebell 这个完整的 golang 后端项目进行回顾。

                                        项目入口:先从 main 函数入手

                                        bluebell 项目的 main 函数如下:

                                        package main
                                        import (
                                        	"bluebell/controller"
                                        	"bluebell/dao/mysql"
                                        	"bluebell/dao/redis"
                                        	"bluebell/logger"
                                        	"bluebell/pkg/snowflake"
                                        	"bluebell/router"
                                        	"bluebell/settings"
                                        	"context"
                                        	"fmt"
                                        	"log"
                                        	"net/http"
                                        	"os"
                                        	"os/signal"
                                        	"syscall"
                                        	"time"
                                        	"go.uber.org/zap"
                                        )
                                        // @title bluebell项目接口文档
                                        // @version 1.0
                                        // @description ... ... ...
                                        // @contact.name yggp
                                        // @contact.url ... ... ...
                                        // @host 127.0.0.1:8081
                                        // @BasePath /api/v1
                                        func main() {
                                        	// 1. 加载配置
                                        	if err := settings.Init(); err != nil {
                                        		fmt.Printf("init settings failed, err:%v\n", err)
                                        		return
                                        	}
                                        	// 2. 初始化日志
                                        	if err := logger.Init(settings.Conf.LogConfig, settings.Conf.Mode); err != nil {
                                        		fmt.Printf("init logger failed, err:%v\n", err)
                                        		return
                                        	}
                                        	defer zap.L().Sync()
                                        	zap.L().Debug("logger init success...")
                                        	// 3. 初始化MySQL连接
                                        	if err := mysql.Init(settings.Conf.MySQLConfig); err != nil {
                                        		fmt.Printf("init mysql failed, err:%v\n", err)
                                        		return
                                        	}
                                        	defer mysql.Close()
                                        	// 4. 初始化Redis连接
                                        	if err := redis.Init(settings.Conf.RedisConfig); err != nil {
                                        		fmt.Printf("init redis failed, err:%v\n", err)
                                        		return
                                        	}
                                        	defer redis.Close()
                                        	// 初始化雪花算法
                                        	if err := snowflake.Init(settings.Conf.StartTime, settings.Conf.MachineID); err != nil {
                                        		fmt.Printf("init snowflake failed, err:%v\n", err)
                                        		return
                                        	}
                                        	// 初始化 gin 框架内置的校验器使用的翻译器
                                        	if err := controller.InitTrans("zh"); err != nil {
                                        		fmt.Printf("init validator trans failed, err:%v\n", err)
                                        		return
                                        	}
                                        	// 5. 注册路由
                                        	r := router.SetupRouter(settings.Conf.Mode)
                                        	// 6. 启动服务 (优雅关机)
                                        	fmt.Println(settings.Conf.Port)
                                        	srv := &http.Server{
                                        		Addr:    fmt.Sprintf(":%d", settings.Conf.Port),
                                        		Handler: r,
                                        	}
                                        	go func() {
                                        		// 开启一个goroutine启动服务
                                        		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
                                        			log.Fatalf("listen: %s\n", err)
                                        		}
                                        	}()
                                        	// 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时
                                        	quit := make(chan os.Signal, 1) // 创建一个接收信号的通道
                                        	// kill 默认会发送 syscall.SIGTERM 信号
                                        	// kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号
                                        	// kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它
                                        	// signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit
                                        	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞
                                        	
                                        		zap.L().Fatal("Server Shutdown", zap.Error(err))
                                        	}
                                        	zap.L().Info("Server exiting")
                                        }
                                        
                                        	fmt.Printf("init settings failed, err:%v\n", err)
                                        	return
                                        }
                                        
                                        	Name         string `mapstructure:"name"`
                                        	Mode         string `mapstructure:"mode"`
                                        	Version      string `mapstructure:"version"`
                                        	StartTime    string `mapstructure:"start_time"`
                                        	MachineID    int64  `mapstructure:"machine_id"`
                                        	Port         int    `mapstructure:"port"`
                                        	*LogConfig   `mapstructure:"log"`
                                        	*MySQLConfig `mapstructure:"mysql"`
                                        	*RedisConfig `mapstructure:"redis"`
                                        }
                                        type LogConfig struct {
                                        	Level      string `mapstructure:"level"`
                                        	Filename   string `mapstructure:"filename"`
                                        	MaxSize    int    `mapstructure:"max_size"`
                                        	MaxAge     int    `mapstructure:"max_age"`
                                        	MaxBackups int    `mapstructure:"max_backups"`
                                        }
                                        type MySQLConfig struct {
                                        	Host         string `mapstructure:"host"`
                                        	User         string `mapstructure:"user"`
                                        	Password     string `mapstructure:"password"`
                                        	DbName       string `mapstructure:"dbname"`
                                        	Port         int    `mapstructure:"port"`
                                        	MaxOpenConns int    `mapstructure:"max_open_conns"`
                                        	MaxIdleConns int    `mapstructure:"max_idle_conns"`
                                        }
                                        type RedisConfig struct {
                                        	Host     string `mapstructure:"host"`
                                        	Password string `mapstructure:"password"`
                                        	Port     int    `mapstructure:"port"`
                                        	DB       int    `mapstructure:"db"`
                                        	PoolSize int    `mapstructure:"pool_size"`
                                        }
                                        func Init() (err error) {
                                        	viper.SetConfigFile("./conf/config.yaml")
                                        	//viper.SetConfigName("config") // 指定配置文件名称(不需要带后缀)
                                        	//viper.SetConfigType("yaml")   // 指定配置文件类型(专用于从远程获取配置信息时指定配置文件类型的)
                                        	viper.AddConfigPath(".")   // 指定查找配置文件的路径(这里使用相对路径)
                                        	err = viper.ReadInConfig() // 读取配置信息
                                        	if err != nil {
                                        		// 读取配置信息失败
                                        		fmt.Printf("viper.ReadInConfig() failed, err:%v\n", err)
                                        		return
                                        	}
                                        	// 把读取到的配置信息反序列化到 Conf 变量中
                                        	if err := viper.Unmarshal(Conf); err != nil {
                                        		fmt.Printf("viper.Unmarshal failed, err:%v\n", err)
                                        	}
                                        	viper.WatchConfig()
                                        	viper.OnConfigChange(func(in fsnotify.Event) {
                                        		fmt.Println("配置文件修改了...")
                                        		if err := viper.Unmarshal(Conf); err != nil {
                                        			fmt.Printf("viper.Unmarshal failed, err:%v\n", err)
                                        		}
                                        	})
                                        	return
                                        }
                                        
                                        	// 读取配置信息失败
                                        	fmt.Printf("viper.ReadInConfig() failed, err:%v\n", err)
                                        	return
                                        }
                                        
                                        	fmt.Printf("viper.Unmarshal failed, err:%v\n", err)
                                        }
                                        
                                        	fmt.Printf("init logger failed, err:%v\n", err)
                                        	return
                                        }
                                        defer zap.L().Sync()
                                        zap.L().Debug("logger init success...")
                                        
                                        	writeSyncer := getLogWriter(	// cfg 是 Conf 当中保存的有关 logger 的配置
                                        		cfg.Filename,
                                        		cfg.MaxSize,
                                        		cfg.MaxBackups,
                                        		cfg.MaxAge,
                                        	)
                                        	encoder := getEncoder()
                                        	var l = new(zapcore.Level)
                                        	err = l.UnmarshalText([]byte(cfg.Level))
                                        	if err != nil {
                                        		return
                                        	}
                                        	var core zapcore.Core
                                        	if mode == "dev" {
                                        		// 开发模式, 日志输出到 Terminal
                                        		consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) // 在日志库中内置的开发模式
                                        		// 日志可以有多种输出
                                        		core = zapcore.NewTee(
                                        			zapcore.NewCore(encoder, writeSyncer, l),
                                        			zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), zap.DebugLevel),
                                        		)
                                        	} else {
                                        		core = zapcore.NewCore(encoder, writeSyncer, l)
                                        	}
                                        	lg := zap.New(core, zap.AddCaller())
                                        	// 替换zap库中全局的logger
                                        	zap.ReplaceGlobals(lg)
                                        	return
                                        }
                                        func getEncoder() zapcore.Encoder {
                                        	encoderConfig := zap.NewProductionEncoderConfig()
                                        	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
                                        	encoderConfig.TimeKey = "time"
                                        	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
                                        	encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
                                        	encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
                                        	return zapcore.NewJSONEncoder(encoderConfig)
                                        }
                                        func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
                                        	lumberJackLogger := &lumberjack.Logger{
                                        		Filename:   filename,
                                        		MaxSize:    maxSize,
                                        		MaxBackups: maxBackup,
                                        		MaxAge:     maxAge,
                                        	}
                                        	return zapcore.AddSync(lumberJackLogger)
                                        }
                                        // GinLogger 接收 gin 框架默认的日志
                                        func GinLogger() gin.HandlerFunc {
                                        	return func(c *gin.Context) {
                                        		start := time.Now()
                                        		path := c.Request.URL.Path
                                        		query := c.Request.URL.RawQuery
                                        		c.Next()
                                        		cost := time.Since(start)
                                        		zap.L().Info(path,
                                        			zap.Int("status", c.Writer.Status()),
                                        			zap.String("method", c.Request.Method),
                                        			zap.String("path", path),
                                        			zap.String("query", query),
                                        			zap.String("ip", c.ClientIP()),
                                        			zap.String("user-agent", c.Request.UserAgent()),
                                        			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
                                        			zap.Duration("cost", cost),
                                        		)
                                        	}
                                        }
                                        // GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志
                                        func GinRecovery(stack bool) gin.HandlerFunc {
                                        	return func(c *gin.Context) {
                                        		defer func() {
                                        			if err := recover(); err != nil {
                                        				// Check for a broken connection, as it is not really a
                                        				// condition that warrants a panic stack trace.
                                        				var brokenPipe bool
                                        				if ne, ok := err.(*net.OpError); ok {
                                        					if se, ok := ne.Err.(*os.SyscallError); ok {
                                        						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
                                        							brokenPipe = true
                                        						}
                                        					}
                                        				}
                                        				httpRequest, _ := httputil.DumpRequest(c.Request, false)
                                        				if brokenPipe {
                                        					zap.L().Error(c.Request.URL.Path,
                                        						zap.Any("error", err),
                                        						zap.String("request", string(httpRequest)),
                                        					)
                                        					// If the connection is dead, we can't write a status to it.
                                        					c.Error(err.(error)) // nolint: errcheck
                                        					c.Abort()
                                        					return
                                        				}
                                        				if stack {
                                        					zap.L().Error("[Recovery from panic]",
                                        						zap.Any("error", err),
                                        						zap.String("request", string(httpRequest)),
                                        						zap.String("stack", string(debug.Stack())),
                                        					)
                                        				} else {
                                        					zap.L().Error("[Recovery from panic]",
                                        						zap.Any("error", err),
                                        						zap.String("request", string(httpRequest)),
                                        					)
                                        				}
                                        				c.AbortWithStatus(http.StatusInternalServerError)
                                        			}
                                        		}()
                                        		c.Next()
                                        	}
                                        }
                                        
                                        	fmt.Printf("init mysql failed, err:%v\n", err)
                                        	return
                                        }
                                        defer mysql.Close()
                                        
                                        	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True",
                                        		cfg.User,
                                        		cfg.Password,
                                        		cfg.Host,
                                        		cfg.Port,
                                        		cfg.DbName,
                                        	)
                                        	// 也可以使用MustConnect连接不成功就panic
                                        	db, err = sqlx.Connect("mysql", dsn)
                                        	if err != nil {
                                        		zap.L().Error("connect DB failed", zap.Error(err))
                                        		return
                                        	}
                                        	db.SetMaxOpenConns(cfg.MaxOpenConns)
                                        	db.SetMaxIdleConns(cfg.MaxIdleConns)
                                        	return
                                        }
                                        
                                        	fmt.Printf("init redis failed, err:%v\n", err)
                                        	return
                                        }
                                        defer redis.Close()
                                        
                                        	client = redis.NewClient(&redis.Options{
                                        		Addr: fmt.Sprintf("%s:%d",
                                        			cfg.Host,
                                        			cfg.Port,
                                        		),
                                        		Password: cfg.Password, // no password set
                                        		DB:       cfg.DB,       // use default DB
                                        		PoolSize: cfg.PoolSize,
                                        	})
                                        	_, err = client.Ping().Result()
                                        	return
                                        }
                                        func Close() {
                                        	_ = client.Close()
                                        }
                                        
                                        	fmt.Printf("init snowflake failed, err:%v\n", err)
                                        	return
                                        }
                                        
                                        	var st time.Time
                                        	st, err = time.Parse("2006-01-02", startTime)
                                        	if err != nil {
                                        		return
                                        	}
                                        	sf.Epoch = st.UnixNano() / 1000000
                                        	node, err = sf.NewNode(machineID)
                                        	return
                                        }
                                        func GenID() int64 {
                                        	return node.Generate().Int64()
                                        }
                                        
                                        	fmt.Printf("init validator trans failed, err:%v\n", err)
                                        	return
                                        }
                                        
                                        	// 修改gin框架中的Validator引擎属性,实现自定制
                                        	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
                                        		// 注册一个获取 json tag 的自定义方法, 从结构体字段中取 json tag 作为返回提示信息的字段
                                        		v.RegisterTagNameFunc(func(fld reflect.StructField) string {
                                        			name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
                                        			if name == "-" {
                                        				return ""
                                        			}
                                        			return name
                                        		})
                                        		v.RegisterStructValidation(SignUpParamsStructLevelValidation, models.ParamSignUp{})
                                        		zhT := zh.New() // 中文翻译器
                                        		enT := en.New() // 英文翻译器
                                        		// 第一个参数是备用(fallback)的语言环境
                                        		// 后面的参数是应该支持的语言环境(支持多个)
                                        		// uni := ut.New(zhT, zhT) 也是可以的
                                        		uni := ut.New(enT, zhT, enT)
                                        		// locale 通常取决于 http 请求头的 'Accept-Language'
                                        		var ok bool
                                        		// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
                                        		trans, ok = uni.GetTranslator(locale)
                                        		if !ok {
                                        			return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
                                        		}
                                        		// 注册翻译器
                                        		switch locale {
                                        		case "en":
                                        			err = enTranslations.RegisterDefaultTranslations(v, trans)
                                        		case "zh":
                                        			err = zhTranslations.RegisterDefaultTranslations(v, trans)
                                        		default:
                                        			err = enTranslations.RegisterDefaultTranslations(v, trans)
                                        		}
                                        		return
                                        	}
                                        	return
                                        }
                                        type SignUpParam struct {
                                        	Age        uint8  `json:"age" binding:"gte=1,lte=130"`
                                        	Name       string `json:"name" binding:"required"`
                                        	Email      string `json:"email" binding:"required,email"`
                                        	Password   string `json:"password" binding:"required"`
                                        	RePassword string `json:"re_password" binding:"required,eqfield=Password"`
                                        }
                                        // removeTopStruct 去除提示信息中的结构体名称
                                        func removeTopStruct(fields map[string]string) map[string]string {
                                        	res := map[string]string{}
                                        	for field, err := range fields {
                                        		res[field[strings.Index(field, ".")+1:]] = err
                                        	}
                                        	return res
                                        }
                                        func SignUpParamsStructLevelValidation(sl validator.StructLevel) {
                                        	su := sl.Current().Interface().(models.ParamSignUp)
                                        	if su.Password != su.RePassword {
                                        		sl.ReportError(su.RePassword, "re_password", "RePassword", "eqfield", "password")
                                        	}
                                        }
                                        
                                        	Addr:    fmt.Sprintf(":%d", settings.Conf.Port),
                                        	Handler: r,
                                        }
                                        go func() {
                                        	// 开启一个goroutine启动服务
                                        	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
                                        		log.Fatalf("listen: %s\n", err)
                                        	}
                                        }()
                                        
                                        	if mode == gin.ReleaseMode {
                                        		gin.SetMode(gin.ReleaseMode) // gin 设置成发布模式
                                        	}
                                        	r := gin.New()
                                        	r.Use(logger.GinLogger(), logger.GinRecovery(true), middleware.RateLimitMiddleware(2*time.Second, 1))
                                        	r.LoadHTMLFiles("./templates/index.html")
                                        	r.Static("/static", "./static")
                                        	r.GET("/", func(c *gin.Context) {
                                        		c.HTML(http.StatusOK, "index.html", nil)
                                        	})
                                        	r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
                                        	v1 := r.Group("/api/v1")
                                        	// 注册业务路由
                                        	v1.POST("/signup", controller.SignUpHandler)
                                        	// 登录
                                        	v1.POST("/login", controller.LoginHandler)
                                        	v1.GET("/community", controller.CommunityHandler)
                                        	v1.GET("/community/:id", controller.CommunityDetailHandler)
                                        	v1.GET("/post/:id", controller.GetPostDetailHandler)
                                        	v1.GET("/posts/", controller.GetPostListDetailHandler)
                                        	v1.GET("/posts2/", controller.GetPostListHandler2)
                                        	v1.Use(middleware.JWTAuthMiddleware()) // 应用 JWT 鉴权中间件
                                        	{
                                        		v1.POST("/post", controller.CreatePostHandler)
                                        		// 根据时间或分数获取帖子列表
                                        		v1.POST("/vote", controller.PostVoteController)
                                        	}
                                        	
                                        	pprof.Register(r) // 注册 pprof 相关路由
                                        	r.NoRoute(func(c *gin.Context) {
                                        		c.JSON(http.StatusOK, gin.H{
                                        			"msg": "404",
                                        		})
                                        	})
                                        	return r
                                        }
                                        
                                        	bucket := ratelimit.NewBucket(fillInterval, cap)
                                        	return func(c *gin.Context) {
                                        		// 如果取不到令牌就返回响应
                                        		if bucket.TakeAvailable(1) == 0 {
                                        			c.String(http.StatusOK, "rate limit...")
                                        			c.Abort()
                                        			return
                                        		}
                                        		c.Next()
                                        	}
                                        }
                                        
                                        	return func(c *gin.Context) {
                                        		// 客户端携带 Token 有三种方式: 1. 放在请求头; 2. 放在请求体; 3. 放在 URL
                                        		// 此处假设 Token 放在 Header 的 Authorization 当中, 并使用 Bearer 开头
                                        		// 这里的具体实现需要根据具体的业务情况来定
                                        		authHeader := c.Request.Header.Get("Authorization")
                                        		if authHeader == "" {
                                        			controller.ResponseError(c, controller.CodeNeedLogin)
                                        			c.Abort()
                                        			return
                                        		}
                                        		// 按空格分割
                                        		parts := strings.SplitN(authHeader, " ", 2)
                                        		if !(len(parts) == 2 && parts[0] == "Bearer") {
                                        			controller.ResponseError(c, controller.CodeInvalidToken)
                                        			c.Abort()
                                        			return
                                        		}
                                        		mc, err := jwt.ParseToken(parts[1])
                                        		if err != nil {
                                        			controller.ResponseError(c, controller.CodeInvalidToken)
                                        			c.Abort()
                                        			return
                                        		}
                                        		// 将当前请求的 userID 保存在请求的上下文 c 中
                                        		c.Set(controller.CtxUserIDKey, mc.UserID)
                                        		c.Next() // 后续的处理请求的函数中可以用 c.Get(CtxUserIDKey) 获取用户信息
                                        	}
                                        }
                                        
                                        	UserID   int64  `json:"user_id"`
                                        	Username string `json:"username"`
                                        	jwt.StandardClaims
                                        }
                                        // ParseToken 用于解析 JWT
                                        func ParseToken(tokenString string) (*MyClaims, error) {
                                        	// 解析 token
                                        	var mc = new(MyClaims)
                                        	token, err := jwt.ParseWithClaims(tokenString, mc, func(token *jwt.Token) (i interface{}, err error) {
                                        		return mySecret, nil
                                        	})
                                        	if err != nil {
                                        		return nil, err
                                        	}
                                        	if token.Valid {
                                        		return mc, nil
                                        	}
                                        	return nil, errors.New("invalid token")
                                        }
                                        
                                        	// 首先创建一个我们自己的声明
                                        	c := MyClaims{
                                        		userID,
                                        		username,
                                        		jwt.StandardClaims{
                                        			ExpiresAt: time.Now().Add(time.Duration(viper.GetInt("auth.jwt_expire")) * time.Hour).Unix(), // 过期时间
                                        			Issuer:    "bluebell",                         // 签发人, 可以写 app 的名称
                                        		},
                                        	}
                                        	token := jwt.NewWithClaims(jwt.SigningMethodHS256, c) // jwt.SigningMethodHS256 是加密的算法
                                        	return token.SignedString(mySecret)
                                        }
                                        
                                        	Code ResCode     `json:"code"`
                                        	Msg  interface{} `json:"msg"`            // 定义为空接口的原因是接收到的内容可能比较复杂
                                        	Data interface{} `json:"data,omitempty"` // 原因同上
                                        }
                                        func ResponseError(c *gin.Context, code ResCode) {
                                        	c.JSON(http.StatusOK, &ResponseData{
                                        		Code: code,
                                        		Msg:  code.Msg(),
                                        		Data: nil,
                                        	})
                                        }
                                        func ResponseSuccess(c *gin.Context, data interface{}) {
                                        	c.JSON(http.StatusOK, &ResponseData{
                                        		Code: CodeSuccess,
                                        		Msg:  CodeSuccess.Msg(),
                                        		Data: data,
                                        	})
                                        }
                                        func ResponseErrorWithMsg(c *gin.Context, code ResCode, msg interface{}) {
                                        	c.JSON(http.StatusOK, &ResponseData{
                                        		Code: code,
                                        		Msg:  msg,
                                        		Data: nil,
                                        	})
                                        }
                                        
                                        	CodeSuccess:         "success",
                                        	CodeInvalidParam:    "请求参数错误",
                                        	CodeUserExist:       "用户名已存在",
                                        	CodeUserNotExist:    "用户名不存在",
                                        	CodeInvalidPassword: "用户名或密码错误",
                                        	CodeServerBusy:      "服务繁忙",
                                        	CodeNeedLogin:       "需要登录",
                                        	CodeInvalidToken:    "无效的token",
                                        }
                                        func (c ResCode) Msg() string {
                                        	msg, ok := codeMsgMap[c]
                                        	if !ok {
                                        		msg = codeMsgMap[CodeServerBusy]
                                        	}
                                        	return msg
                                        }
                                        
                                        	Username   string `json:"username" binding:"required"`
                                        	Password   string `json:"password" binding:"required"`
                                        	RePassword string `json:"re_password" binding:"required,eqfield=Password"`
                                        }
                                        type ParamLogin struct {
                                        	Username string `json:"username" binding:"required"`
                                        	Password string `json:"password" binding:"required"`
                                        }
                                        type ParamVoteData struct {
                                        	// UserID 从请求中获取当前的用户
                                        	PostID    string `json:"post_id" binding:"required"`              // 帖子 ID
                                        	Direction int8   `json:"direction,string" binding:"oneof=1 0 -1"` // 赞成票(1) or 反对票(-1) or 取消投票(0)
                                        }
                                        // ParamPostList 是获取帖子列表的 query string 参数
                                        type ParamPostList struct {
                                        	CommunityID int64  `json:"community_id" form:"community_id"` // 可以为空, 如果不传这个参数, 那么就按照所有社区去查询
                                        	Page        int64  `json:"page" form:"page"`
                                        	Size        int64  `json:"size" form:"size"`
                                        	Order       string `json:"order" form:"order"`
                                        }
                                        type ParamCommunityPostList struct {
                                        	*ParamPostList
                                        }
                                        
                                        	// 1. 获取参数和参数校验
                                        	p := new(models.ParamSignUp)
                                        	if err := c.ShouldBindJSON(p); err != nil {
                                        		// 请求参数有误, 直接返回响应, 注意此处只能检查参数类型是否合规, 比如要求一个 string, 传入的是 int, 就会报错
                                        		zap.L().Error("SignUp with invalid param", zap.Error(err))
                                        		// 判断 err 是不是 validator.ValidationErrors 类型
                                        		errs, ok := err.(validator.ValidationErrors)
                                        		if !ok {
                                        			// 如果 err 不是 validator.ValidationErrors 类型, 说明在参数校验之前已经出错, 此时没必要翻译
                                        			ResponseError(c, CodeInvalidParam) // 使用 ResponseError 对错误响应进一步地封装
                                        			return
                                        		}
                                        		ResponseErrorWithMsg(c, CodeInvalidParam, removeTopStruct(errs.Translate(trans)))
                                        		return
                                        	}
                                        	// 可以使用 Golang 的 validator 第三方库对参数进行校验, 它原生支持 gin 框架
                                        	// 2. 业务处理
                                        	if err := logic.SignUp(p); err != nil {
                                        		zap.L().Error("logic.SignUp failed", zap.Error(err))
                                        		if errors.Is(err, mysql.ErrorUserNotExist) {
                                        			ResponseError(c, CodeUserExist)
                                        			return
                                        		}
                                        		ResponseError(c, CodeServerBusy)
                                        		return
                                        	}
                                        	// 3. 返回响应
                                        	ResponseSuccess(c, nil)
                                        }
                                        
                                        	// 1. 首先判断用户是否存在, 如果存在则直接返回
                                        	if err = mysql.CheckUserExist(p.Username); err != nil {
                                        		return
                                        	}
                                        	// 2. 生成 UID
                                        	userID := snowflake.GenID()
                                        	// 构造一个 user 实例
                                        	user := &models.User{
                                        		UserID:   userID,
                                        		Username: p.Username,
                                        		Password: p.Password,
                                        	}
                                        	// 3. 用户密码加密, 已经在 mysql.user 完成
                                        	// 4. 保存数据进入数据库, 设计到 dao 层
                                        	return mysql.InsertUser(user)
                                        }
                                        
                                        	sqlStr := `select count(user_id) from user where username = ?`
                                        	var count int
                                        	if err = db.Get(&count, sqlStr, username); err != nil {
                                        		return
                                        	}
                                        	if count  0 {
                                        		return ErrorUserExist
                                        	}
                                        	return nil
                                        }
                                        
                                        	// 对密码进行加密, 因为数据库中不应该存储明文的密码
                                        	user.Password = encryptPassword(user.Password)
                                        	// 执行 SQL 语句入库
                                        	sqlStr := `insert into user(user_id, username, password) values(?, ?, ?)`
                                        	_, err = db.Exec(sqlStr, user.UserID, user.Username, user.Password)
                                        	return
                                        }
                                        // encryptPassword 对明文的密码进行加密
                                        func encryptPassword(oPassword string) string {
                                        	h := md5.New()
                                        	h.Write([]byte(secret))
                                        	return hex.EncodeToString(h.Sum([]byte(oPassword)))
                                        }
                                        
                                        	// 1. 获取请求参数, 并进行参数校验
                                        	p := new(models.ParamLogin)
                                        	if err := c.ShouldBindJSON(p); err != nil {
                                        		zap.L().Error("Login with invalid param", zap.Error(err))
                                        		errs, ok := err.(validator.ValidationErrors)
                                        		if !ok {
                                        			ResponseError(c, CodeInvalidParam)
                                        			return
                                        		}
                                        		ResponseErrorWithMsg(c, CodeInvalidParam, removeTopStruct(errs.Translate(trans)))
                                        		return
                                        	}
                                        	// 2. 业务逻辑处理
                                        	/* 登录需要做什么?
                                        	   1. 检查用户名是否存在, 如果不存在转到注册页面.
                                        	   2. 如果用户存在, 检查用户名与密码是否对应, 如果对应的话, 让用户登录, 否则登录失败
                                        	*/
                                        	user, err := logic.Login(p)
                                        	if err != nil {
                                        		zap.L().Error("logic.Login failed", zap.String("username", p.Username), zap.Error(err))
                                        		if errors.Is(err, mysql.ErrorUserNotExist) {
                                        			ResponseError(c, CodeUserNotExist)
                                        			return
                                        		}
                                        		ResponseError(c, CodeInvalidPassword)
                                        		return
                                        	}
                                        	// 3. 返回响应
                                        	//ResponseSuccess(c, token)
                                        	ResponseSuccess(c, gin.H{
                                        		"user_id":   fmt.Sprintf("%d", user.UserID), // 如果 ID 的值大于 1 
                                        	user = &models.User{
                                        		Username: p.Username,
                                        		Password: p.Password,
                                        	}
                                        	// mysql.Login 传递的是指针, 可以拿到 UserID
                                        	if err = mysql.Login(user); err != nil {
                                        		return nil, err
                                        	}
                                        	// 拿到 UserID 之后就可以生成 JWT
                                        	token, err := jwt.GenToken(user.UserID, user.Username)
                                        	if err != nil {
                                        		return
                                        	}
                                        	user.Token = token
                                        	return
                                        }
                                        
                                        	oPassword := user.Password // 用户登录的密码
                                        	sqlStr := `select user_id, username, password from user where username=?`
                                        	err = db.Get(user, sqlStr, user.Username)
                                        	if err == sql.ErrNoRows {
                                        		// 用户不存在·失败
                                        		return ErrorUserNotExist
                                        	}
                                        	if err != nil {
                                        		// 查询数据库失败
                                        		return err
                                        	}
                                        	// 判断密码是否正确
                                        	password := encryptPassword(oPassword)
                                        	if password != user.Password {
                                        		// 密码错误·失败
                                        		return ErrorInvalidPassword
                                        	}
                                        	return
                                        }
                                        
                                        	// 查询到所有的社区 (community_id, community_name), 以切片的形式返回
                                        	// 没有参数校验和检查, 直接从 Logic 层获取社区列表
                                        	data, err := logic.GetCommunityList()
                                        	if err != nil {
                                        		// 后端的错误通常不会详细地对外暴露
                                        		zap.L().Error("logic.GetCommunityList() failed", zap.Error(err))
                                        		ResponseError(c, CodeServerBusy) // 不轻易将服务端的报错暴露给外面
                                        		return
                                        	}
                                        	ResponseSuccess(c, data)
                                        }
                                        
                                        	// 简单来说这个函数的作用就是查找数据库当中所有的 community, 并返回
                                        	return mysql.GetCommunityList()
                                        }
                                        
                                        	sqlStr := `select community_id, community_name from community`
                                        	if err = db.Select(&communityList, sqlStr); err != nil {
                                        		if err == sql.ErrNoRows {
                                        			// select 语句没有查询到记录, 返回空的分类列表
                                        			zap.L().Warn("There is no community in db")
                                        			err = nil
                                        		}
                                        	}
                                        	return
                                        }
                                        // GetCommunityDetailByID 根据 id 查询社区详情
                                        func GetCommunityDetailByID(id int64) (community *models.CommunityDetail, err error) {
                                        	sqlStr := `select community_id, community_name, introduction, create_time from community where community_id = ?`
                                        	community = new(models.CommunityDetail)
                                        	if err = db.Get(community, sqlStr, id); err != nil {
                                        		if err == sql.ErrNoRows {
                                        			err = ErrorInvalidID
                                        		}
                                        	}
                                        	return community, err
                                        }
                                        
                                        	ID   int64  `json:"id" db:"community_id"`
                                        	Name string `json:"name" db:"community_name"`
                                        }
                                        type CommunityDetail struct {
                                        	ID           int64     `json:"id" db:"community_id"`
                                        	Name         string    `json:"name" db:"community_name"`
                                        	Introduction string    `json:"introduction,omitempty" db:"introduction"`
                                        	CreateTime   time.Time `json:"create_time" db:"create_time"`
                                        }
                                        
                                        	page, size := getPageInfo(c)
                                        	// 1. 获取数据
                                        	data, err := logic.GetPostList(page, size)
                                        	if err != nil {
                                        		zap.L().Error("logic.GetPostList() failed", zap.Error(err))
                                        		ResponseError(c, CodeServerBusy)
                                        		return
                                        	}
                                        	ResponseSuccess(c, data)
                                        	// 2. 返回响应
                                        }
                                        
                                        	// 0. 获取分页参数
                                        	pageStr := c.Query("page")
                                        	sizeStr := c.Query("size")
                                        	var (
                                        		page int64
                                        		size int64
                                        		err  error
                                        	)
                                        	page, err = strconv.ParseInt(pageStr, 10, 64)
                                        	if err != nil {
                                        		page = 1
                                        	}
                                        	size, err = strconv.ParseInt(sizeStr, 10, 64)
                                        	if err != nil {
                                        		size = 10
                                        	}
                                        	return page, size
                                        }
                                        
                                        	// 查询
                                        	posts, err := mysql.GetPostList(page, size)
                                        	if err != nil {
                                        		return
                                        	}
                                        	data = make([]*models.ApiPostDetail, 0, len(posts))
                                        	for _, post := range posts {
                                        		// 根据作者 id 查询作者信息
                                        		user, err := mysql.GetUserByID(post.AuthorID)
                                        		if err != nil {
                                        			zap.L().Error("mysql.GetUserByID(post.AuthorID) failed",
                                        				zap.Int64("author_id", post.AuthorID),
                                        				zap.Error(err))
                                        			continue
                                        		}
                                        		// 根据社区 id 查询社区的详细信息
                                        		community, err := mysql.GetCommunityDetailByID(post.CommunityID)
                                        		if err != nil {
                                        			zap.L().Error("mysql.GetCommunityDetailByID(post.CommunityID)",
                                        				zap.Int64("community_id", post.CommunityID),
                                        				zap.Error(err))
                                        			continue
                                        		}
                                        		postDetail := &models.ApiPostDetail{
                                        			AuthorName:      user.Username,
                                        			Post:            post,
                                        			CommunityDetail: community,
                                        		}
                                        		data = append(data, postDetail)
                                        	}
                                        	return
                                        }
                                        
                                        	ID          int64     `json:"id,string" db:"post_id"`
                                        	AuthorID    int64     `json:"author_id" db:"author_id"`
                                        	CommunityID int64     `json:"community_id" db:"community_id" binding:"required"`
                                        	Status      int32     `json:"status" db:"status"`
                                        	Title       string    `json:"title" db:"title" binding:"required"`
                                        	Content     string    `json:"content" db:"content" binding:"required"`
                                        	CreateTime  time.Time `json:"create_time" db:"create_time"`
                                        }
                                        // ApiPostDetail 帖子详情接口的结构体
                                        type ApiPostDetail struct {
                                        	AuthorName       string             `json:"author_name"`
                                        	VoteNum          int64              `json:"vote_num"`
                                        	*Post                               // 嵌入帖子的结构体
                                        	*CommunityDetail `json:"community"` // 嵌入社区信息的结构体
                                        }
                                        
                                        	sqlStr := `select 
                                        	post_id, title, content, author_id, community_id, create_time 
                                        	from post 
                                        	ORDER BY create_time 
                                        	DESC 
                                        	limit ?,?
                                        	`
                                        	posts = make([]*models.Post, 0, 2)
                                        	err = db.Select(&posts, sqlStr, (page-1)*size, size)
                                        	return
                                        }
                                        
                                        	user = new(models.User)
                                        	sqlStr := `select user_id, username from user where user_id = ?`
                                        	err = db.Get(user, sqlStr, uid)
                                        	return
                                        }
                                        
                                        	sqlStr := `select community_id, community_name, introduction, create_time from community where community_id = ?`
                                        	community = new(models.CommunityDetail)
                                        	if err = db.Get(community, sqlStr, id); err != nil {
                                        		if err == sql.ErrNoRows {
                                        			err = ErrorInvalidID
                                        		}
                                        	}
                                        	return community, err
                                        }
                                        
                                        	// 1. 获取参数及参数校验
                                        	p := new(models.Post)	// 涉及到帖子的结构, 保存在 models/post.go 下, 上文已经提到过
                                        	if err := c.ShouldBindJSON(p); err != nil {
                                        		zap.L().Debug("c.ShouldBindJSON(p) error", zap.Any("err", err))
                                        		zap.L().Error("Create post with invalid param")
                                        		ResponseError(c, CodeInvalidParam)
                                        		return
                                        	}
                                        	// 从 c.Context 当中取得当前发起请求的用户的 id
                                        	userID, err := getCurrentUser(c)
                                        	if err != nil {
                                        		ResponseError(c, CodeNeedLogin)
                                        		return
                                        	}
                                        	p.AuthorID = userID
                                        	// 2. 创建帖子, 交给 logic 层
                                        	if err = logic.CreatePost(p); err != nil {
                                        		zap.L().Error("logic.CreatePost(p) failed", zap.Error(err))
                                        		ResponseError(c, CodeServerBusy)
                                        		return
                                        	}
                                        	// 3. 返回响应
                                        	ResponseSuccess(c, nil)
                                        }
                                        
                                        	uid, ok := c.Get(CtxUserIDKey)
                                        	if !ok {
                                        		err = ErrorUserNotLogin
                                        		return
                                        	}
                                        	userID, ok = uid.(int64)
                                        	if !ok {
                                        		err = ErrorUserNotLogin
                                        		return
                                        	}
                                        	return
                                        }
                                        
                                        	// 1. 生成 post_id, 通过雪花算法
                                        	p.ID = snowflake.GenID()
                                        	// 2. 保存到数据库 && 3. 返回
                                        	err = mysql.CreatePost(p)
                                        	if err != nil {
                                        		return err
                                        	}
                                        	err = redis.CreatePost(p.ID, p.CommunityID)
                                        	return
                                        }
                                        
                                        	sqlStr := `insert into post( 
                                        	post_id, title, content, author_id, community_id)
                                        	values (?, ?, ?, ?, ?)`
                                        	_, err = db.Exec(sqlStr, p.ID, p.Title, p.Content, p.AuthorID, p.CommunityID)
                                        	return
                                        }
                                        
                                        	return Prefix + key
                                        }
                                        
                                        	pipeline := client.TxPipeline()
                                        	// 帖子时间
                                        	pipeline.ZAdd(getRedisKey(KeyPostTimeZSet), redis.Z{
                                        	// getRedisKey(KeyPostTimeZSet) 得到 "bluebell:post:time"
                                        		Score:  float64(time.Now().Unix()),
                                        		Member: postID,
                                        	})
                                        	// 帖子分数
                                        	pipeline.ZAdd(getRedisKey(KeyPostScoreZSet), redis.Z{
                                        	// getRedisKey(KeyPostScoreZSet) 得到 "bluebell:post:score"
                                        		Score:  float64(time.Now().Unix()),
                                        		Member: postID,
                                        	})
                                        	// 把帖子 id 加到社区的 set 当中
                                        	cKey := getRedisKey(KeyCommunitySetPF + strconv.Itoa(int(communityID)))
                                        	pipeline.SAdd(cKey, postID)
                                        	_, err := pipeline.Exec()
                                        	return err
                                        }
                                        
                                        	// GET 请求参数: /api/v1/posts2?page=1&size=10&order=time (query string 参数)
                                        	// 获取分页参数
                                        	// c.ShouldBind() 根据请求的数据类型动态地获取数据
                                        	// 👇 初始化结构体时指定初始函数
                                        	p := &models.ParamPostList{
                                        		Page:  1,
                                        		Size:  10,
                                        		Order: models.OrderTime,
                                        	}
                                        	if err := c.ShouldBindQuery(p); err != nil {
                                        		zap.L().Error("GetPostListHandler2 with invalid params", zap.Error(err))
                                        		ResponseError(c, CodeInvalidParam)
                                        		return
                                        	}
                                        	// 获取数据
                                        	data, err := logic.GetPostListNew(p) // 更新: 合二为一
                                        	if err != nil {
                                        		zap.L().Error("logic.GetPostList() failed", zap.Error(err))
                                        		ResponseError(c, CodeServerBusy)
                                        		return
                                        	}
                                        	ResponseSuccess(c, data)
                                        }
                                        
                                        	CommunityID int64  `json:"community_id" form:"community_id"` // 可以为空, 如果不传这个参数, 那么就按照所有社区去查询
                                        	Page        int64  `json:"page" form:"page"`
                                        	Size        int64  `json:"size" form:"size"`
                                        	Order       string `json:"order" form:"order"`
                                        }
                                        
                                        	if p.CommunityID == 0 {
                                        		data, err = GetPostList2(p)
                                        	} else {
                                        		data, err = GetCommunityPostList2(p)
                                        	}
                                        	if err != nil {
                                        		zap.L().Error("GetPostListNew failed", zap.Error(err))
                                        		return nil, err
                                        	}
                                        	return
                                        }
                                        
                                        	// 2. 去 redis 查询 id 列表
                                        	ids, err := redis.GetPostIDsInOrder(p)
                                        	if err != nil {
                                        		//fmt.Println("IM HERE 1")
                                        		return
                                        	}
                                        	if len(ids) == 0 {
                                        		// 没有必要继续查询下去
                                        		zap.L().Warn("redis.GetPostIDsInOrder(p) returns 0 data")
                                        		return
                                        	}
                                        	// 3. 根据 id 去 mysql 数据库查询帖子的详细信息
                                        	// 返回的数据还要按照给定的 id 的顺序返回
                                        	posts, err := mysql.GetPostListByIDs(ids)
                                        	if err != nil {
                                        		//fmt.Println("IM HERE 2")
                                        		return
                                        	}
                                        	// 提前查询好每篇帖子的投票数
                                        	voteData, err := redis.GetPostVoteData(ids)
                                        	if err != nil {
                                        		return
                                        	}
                                        	// 将帖子的作者及分区信息查询出来填充到帖子中
                                        	for idx, post := range posts {
                                        		// 根据作者 id 查询作者信息
                                        		user, err := mysql.GetUserByID(post.AuthorID)
                                        		if err != nil {
                                        			zap.L().Error("mysql.GetUserByID(post.AuthorID) failed",
                                        				zap.Int64("author_id", post.AuthorID),
                                        				zap.Error(err))
                                        			continue
                                        		}
                                        		// 根据社区 id 查询社区的详细信息
                                        		community, err := mysql.GetCommunityDetailByID(post.CommunityID)
                                        		if err != nil {
                                        			zap.L().Error("mysql.GetCommunityDetailByID(post.CommunityID)",
                                        				zap.Int64("community_id", post.CommunityID),
                                        				zap.Error(err))
                                        			continue
                                        		}
                                        		postDetail := &models.ApiPostDetail{
                                        			AuthorName:      user.Username,
                                        			VoteNum:         voteData[idx],
                                        			Post:            post,
                                        			CommunityDetail: community,
                                        		}
                                        		data = append(data, postDetail)
                                        	}
                                        	return
                                        }
                                        
                                        	// 2. 去 redis 查询 id 列表
                                        	ids, err := redis.GetCommunityPostIDsInOrder(p)
                                        	if err != nil {
                                        		return
                                        	}
                                        	if len(ids) == 0 {
                                        		// 没有必要继续查询下去
                                        		zap.L().Warn("redis.GetPostIDsInOrder(p) returns 0 data")
                                        		return
                                        	}
                                        	// 3. 根据 id 去 mysql 数据库查询帖子的详细信息
                                        	// 返回的数据还要按照给定的 id 的顺序返回
                                        	posts, err := mysql.GetPostListByIDs(ids)
                                        	if err != nil {
                                        		return
                                        	}
                                        	// 提前查询好每篇帖子的投票数
                                        	voteData, err := redis.GetPostVoteData(ids)
                                        	if err != nil {
                                        		return
                                        	}
                                        	// 将帖子的作者及分区信息查询出来填充到帖子中
                                        	for idx, post := range posts {
                                        		// 根据作者 id 查询作者信息
                                        		user, err := mysql.GetUserByID(post.AuthorID)
                                        		if err != nil {
                                        			zap.L().Error("mysql.GetUserByID(post.AuthorID) failed",
                                        				zap.Int64("author_id", post.AuthorID),
                                        				zap.Error(err))
                                        			continue
                                        		}
                                        		// 根据社区 id 查询社区的详细信息
                                        		community, err := mysql.GetCommunityDetailByID(post.CommunityID)
                                        		if err != nil {
                                        			zap.L().Error("mysql.GetCommunityDetailByID(post.CommunityID)",
                                        				zap.Int64("community_id", post.CommunityID),
                                        				zap.Error(err))
                                        			continue
                                        		}
                                        		postDetail := &models.ApiPostDetail{
                                        			AuthorName:      user.Username,
                                        			VoteNum:         voteData[idx],
                                        			Post:            post,
                                        			CommunityDetail: community,
                                        		}
                                        		data = append(data, postDetail)
                                        	}
                                        	return
                                        }
                                        
                                        	// 从 redis 获取 id
                                        	// 1. 首先根据用户请求中携带的 order 参数确定要查询的 redis 的 key
                                        	key := getRedisKey(KeyPostTimeZSet)
                                        	if p.Order == models.OrderScore {
                                        		key = getRedisKey(KeyPostScoreZSet)
                                        	}
                                        	return getIDsFromKey(key, p.Page, p.Size)
                                        }
                                        func getIDsFromKey(key string, page, size int64) ([]string, error) {
                                        	// 2. 确定查询的索引起始点
                                        	start := (page - 1) * size
                                        	end := start + size - 1
                                        	// 3. ZREVRANGE 按分数从大到小的顺序查询指定数量的元素
                                        	return client.ZRevRange(key, start, end).Result()
                                        }
                                        
                                        	orderKey := getRedisKey(KeyPostTimeZSet)
                                        	if p.Order == models.OrderScore {
                                        		orderKey = getRedisKey(KeyPostScoreZSet)
                                        	}
                                        	// 使用 zinterstore 把分区的帖子 set 与帖子分数的 zset 生成一个新的 zset
                                        	// 针对新的 zset, 按之前的逻辑取数据
                                        	// 从 redis 获取 id
                                        	// 社区的 key
                                        	cKey := getRedisKey(KeyCommunitySetPF + strconv.Itoa(int(p.CommunityID)))
                                        	// 利用缓存 key 减少 zinterstore 的执行次数
                                        	key := orderKey + strconv.Itoa(int(p.CommunityID))
                                        	if client.Exists(key).Val()  更新分数和投票记录
                                        		direction = -1时, 有两种情况:
                                        	1. 之前没有投过票, 现在投反对票 -> 更新分数和投票记录
                                        	2. 之前投过赞成票, 现在改投反对票 -> 更新分数和投票记录
                                        	========== 投票的限制 ==========
                                        		每个帖子自发表之日起, 一个星期之内, 允许用户投票. 超过一个星期就不允许再投票了.
                                        	1. 到期之后将 redis 中保存的赞成票数以及反对票数存储到 mysql 表中 (因为这些数据是已经变冷的数据);
                                        	2. 到期之后删除那个 KeyPostVotedZSetPrefix
                                        */
                                        // VoteForPost 是为帖子投票的函数
                                        func VoteForPost(userID int64, p *models.ParamVoteData) error {
                                        	zap.L().Debug("VoteForPost",
                                        		zap.Int64("userID", userID),
                                        		zap.String("postID", p.PostID),
                                        		zap.Int8("direction", p.Direction))
                                        	return redis.VoteForPost(strconv.Itoa(int(userID)), p.PostID, float64(p.Direction))
                                        }
                                        

                                        在 Logic 层没有要具体处理的业务逻辑,直接去 Dao 层对 Redis 当中保存的投票数进行修改即可。

                                        Dao

                                        const (
                                        	oneWeekInSeconds = 7 * 24 * 3600
                                        	scorePerVote     = 432 // 每一票值多少分
                                        )
                                        var (
                                        	ErrVoteTimeExpired = errors.New("投票时间已过")
                                        	ErrVoteRepeated    = errors.New("不允许重复投票")
                                        )
                                        func VoteForPost(userID, postID string, value float64) error {
                                        	// 1. 判断投票的限制
                                        	// 去 redis 取帖子的发布时间
                                        	postTime := client.ZScore(getRedisKey(KeyPostTimeZSet), postID).Val()
                                        	if float64(time.Now().Unix())-postTime > oneWeekInSeconds {
                                        		fmt.Println(time.Now().Unix(), postTime)
                                        		return ErrVoteTimeExpired
                                        	}
                                        	// 2. 更新分数
                                        	// 先查当前用户给当前帖子的投票记录
                                        	ov := client.ZScore(getRedisKey(KeyPostVotedZSetPF+postID), userID).Val()
                                        	// 如果这次投票的值和之前保存的值一致, 则提示不允许投票
                                        	if value == ov {
                                        		return ErrVoteRepeated
                                        	}
                                        	var op float64
                                        	if value > ov {
                                        		op = 1
                                        	} else {
                                        		op = -1
                                        	}
                                        	diff := math.Abs(ov - value) // 计算两次投票的差值
                                        	// 2 和 3 需要放到同一个事物当中
                                        	pipeline := client.TxPipeline()
                                        	pipeline.ZIncrBy(getRedisKey(KeyPostScoreZSet), op*diff*scorePerVote, postID)
                                        	// 3. 记录用户为该帖子投过票的数据
                                        	if value == 0 {
                                        		pipeline.ZRem(getRedisKey(KeyPostVotedZSetPF+postID), postID)
                                        	} else {
                                        		pipeline.ZAdd(getRedisKey(KeyPostVotedZSetPF+postID), redis.Z{
                                        			Score:  value, // 当前用户投的是赞成票还是反对票
                                        			Member: userID,
                                        		})
                                        	}
                                        	_, err := pipeline.Exec()
                                        	return err
                                        }
                                        

                                        在 Dao 层,首先判断当前投票行为是否在允许的时间范围内,如果已经超过了时间范围,那么返回 ErrVoteTimeExpired。值得注意的是,此处使用了 Redis 的 ZScore 方法,它的作用是从某个 Sorted Set(由 Redis Key 指定)获取 Member(即 postID) 的 Score。

                                        否则,我们先去 Redis 查找当前用户之前是否为帖子投过票,如果之前投过票且投票行为与本次相同,那么返回 ErrVoteRepeated,禁止用户重复投票。同样通过 Redis 的 ZScore 方法来完成,Redis Key 是 getRedisKey(KeyPostVotedZSetPF+postID),即 postID 指定的帖子的投票情况集合,member 是 userID,即查询当前用户之前对该帖子的投票行为。

                                        如果用户没有投过票,或者之前的投票行为与本次不同,那么首先判断出本次的投票行为,保存到 op 当中。

                                        之后计算两次投票的差值。

                                        接下来我们开启一个 Redis 事务记录投票并修改帖子分数。首先修改帖子的分数,通过 ZIncrBy 来完成,它的作用是修改 Sorted Set 中某个 Member 的 Score。然后记录当前用户对帖子投票的情况,如果 value 为 0 表示用户取消投票,使用 ZRem 将用户从 Sorted Set 中移除。否则通过 ZAdd 将用户当前的投票行为加入到 Sorted Set 当中。

                                        值得注意的是在使用 ZAdd 时,如果用户之前已经使用了 ZAdd 添加相同的 Member 到 Sorted Set,那么本次 ZAdd 将会把之前的 Member 和 Score 覆盖掉。

                                        工程化:以 Swagger 生成 RESTful 风格接口文档 / pprof 进行性能调优 / Docker 部署为例

                                        在后端开发的基础上,一个完整的项目应该有撰写良好的文档,供前端及相关开发人员使用,这可以通过 swagger 来帮助我们完成。此外,如何对我们程序的性能进行调优也是重要的,通过 golang 内置的 pprof 可以帮助我们完成。最后,我们希望自己的程序可以被打包并快速部署,这可以通过 Docker 来完成。

                                        接下来我将回顾如何将 Swagger / pprof / Docker 与 bluebell 项目相结合。

                                        Swagger:生成 RESTful 风格的接口文档

                                        这部分引用自 q1mi 老师的技术博客:

                                        Swagger本质上是一种用于描述使用JSON表示的RESTful API的接口描述语言。Swagger与一组开源软件工具一起使用,以设计、构建、记录和使用RESTful Web服务。Swagger包括自动文档,代码生成和测试用例生成。

                                        使用 gin-swagger 可以为我们的代码自动生成接口文档,一般需要三步:

                                        1. 按照 swagger 的要求给接口代码添加声明式注释;
                                        2. 使用 swag 工具扫描代码,以自动生成 API 接口文档的数据;
                                        3. 使用 gin-swagger 渲染在线接口文档页面,并与 GIN 的 router 绑定。

                                        第一步:添加注释

                                        首先在程序入口 main 函数添加项目介绍的相关信息:

                                        // @title bluebell 项目接口文档
                                        // @version 1.0
                                        // @description bluebell 项目接口文档
                                        // @termsOfService http://swagger.io/terms
                                        // @contact.name yggp
                                        // @license.name Apache 2.0
                                        // @host 127.0.0.1:8081
                                        // @BasePath /api/v1
                                        func main() {
                                        	// ... ... ...
                                        }
                                        

                                        接下来在代码中处理请求的接口函数(通常位于 Controller 层)按照如下方式添加注释:

                                        // CreatePostHandler 创建帖子接口
                                        // @Summary 创建帖子接口
                                        // @Description 用户登录后可以创建帖子
                                        // @Tags 帖子相关接口
                                        // @Accept application/json
                                        // @Produce application/json
                                        // @Param Authorization header string true "Bearer 用户令牌"
                                        // @Param object query models.Post false "查询参数"
                                        // @Security ApiKeyAuth
                                        // @Success 200
                                        // @Router /post [post]
                                        func CreatePostHandler(c *gin.Context) {
                                        	// 1. 获取参数及参数校验
                                        	p := new(models.Post)
                                        	if err := c.ShouldBindJSON(p); err != nil {
                                        		zap.L().Debug("c.ShouldBindJSON(p) error", zap.Any("err", err))
                                        		zap.L().Error("Create post with invalid param")
                                        		ResponseError(c, CodeInvalidParam)
                                        		return
                                        	}
                                        	// 从 c.Context 当中取得当前发起请求的用户的 id
                                        	userID, err := getCurrentUser(c)
                                        	if err != nil {
                                        		ResponseError(c, CodeNeedLogin)
                                        		return
                                        	}
                                        	p.AuthorID = userID
                                        	// 2. 创建帖子, 交给 logic 层
                                        	if err = logic.CreatePost(p); err != nil {
                                        		zap.L().Error("logic.CreatePost(p) failed", zap.Error(err))
                                        		ResponseError(c, CodeServerBusy)
                                        		return
                                        	}
                                        	// 3. 返回响应
                                        	ResponseSuccess(c, nil)
                                        }
                                        // GetPostListHandler2 升级版帖子列表接口
                                        // @Summary 升级版帖子列表接口
                                        // @Description 可按社区按时间或分数排序查询帖子列表接口
                                        // @Tags 帖子相关接口
                                        // @Accept application/json
                                        // @Produce application/json
                                        // @Param Authorization header string false "Bearer 用户令牌"
                                        // @Param object query models.ParamPostList false "查询参数"
                                        // @Security ApiKeyAuth
                                        // @Success 200 {object} _ResponsePostList
                                        // @Router /posts2 [get]
                                        func GetPostListHandler2(c *gin.Context) {
                                        	// GET 请求参数: /api/v1/posts2?page=1&size=10&order=time (query string 参数)
                                        	// 获取分页参数
                                        	//c.ShouldBind() 根据请求的数据类型动态地获取数据
                                        	// 👇 初始化结构体时指定初始函数
                                        	p := &models.ParamPostList{
                                        		Page:  1,
                                        		Size:  10,
                                        		Order: models.OrderTime,
                                        	}
                                        	if err := c.ShouldBindQuery(p); err != nil {
                                        		zap.L().Error("GetPostListHandler2 with invalid params", zap.Error(err))
                                        		ResponseError(c, CodeInvalidParam)
                                        		return
                                        	}
                                        	// 获取数据
                                        	data, err := logic.GetPostListNew(p) // 更新: 合二为一
                                        	if err != nil {
                                        		zap.L().Error("logic.GetPostList() failed", zap.Error(err))
                                        		ResponseError(c, CodeServerBusy)
                                        		return
                                        	}
                                        	ResponseSuccess(c, data)
                                        }
                                        

                                        第二部:生成接口文档数据

                                        通过 swag 工具生成接口文档,gin-swagger 的安装可参考 q1mi 老师的博客(使用 Windows 的同学需要将 swag 导入到环境变量才能使用 swag init)。

                                        使用 swag init 生成接口文档数据,如果注释格式没问题,项目根目录下会多出一个 doc 文件夹。

                                        第三步:引入 gin-swagger 渲染文档数据

                                        首先需要按照 q1mi 老师的教程引入 gin-swagger,之后在 GIN 中注册路由。

                                        启动项目,访问 localhost:8081/swagger/index.html 我们就可以得到 swagger 文档:

                                        【Bluebell】项目总结:基于 golang 的前后端分离 web 项目实战

                                        使用 pprof 进行性能调优

                                        在 GIN 框架下可以直接使用:

                                        pprof.Register(r) // 注册 pprof 相关路由
                                        

                                        来绑定 pprof 相关路由。

                                        此时,当我们启动项目之后,可以在 /debug/pprof 获得下面的内容:

                                        【Bluebell】项目总结:基于 golang 的前后端分离 web 项目实战

                                        在这里我们看不出什么门道,想要进行性能调优需要进一步使用 pprof 可视化工具。

                                        go tool pprof

                                        go tool pprof 的用法如下:

                                        go tool pprof [binary] [source]
                                        
                                        • binary 是应用的二进制文件,用来解析各种符号;
                                        • source 表示 profile 数据的来源,可以是本地文件,也可以是 http 地址;

                                          获取的 Profiling 数据是动态的,想要获得有效的数据,请保证应用处于较大的负载(比如使用go-wrk对服务接口进行压测的过程中获取 profiling)。否则,如果应用处于空闲状态,得到的结果意义不大。

                                          pprof + go-wrk

                                          现在我们使用 go-wrk 开启压测,然后使用 pprof 进行性能分析。

                                          首先使用 go-wrk 进行压测:

                                          go-wrk -t=8 -c=100 -n=10000 "http://127.0.0.1:8081/api/v1/posts"
                                          

                                          然后进行性能分析:

                                          go tool pprof http://127.0.0.1:8081/debug/pprof/profile
                                          

                                          在 pprof 的 Terminal 下输入 web 可以得到:

                                          【Bluebell】项目总结:基于 golang 的前后端分离 web 项目实战

                                          使用 Docker 对 bluebell 项目进行部署

                                          最后一步就是使用 Docker 对 bluebell 项目进行部署。在进行这一步之前,我们需要确保所使用的 mysql 和 redis 都是来自于 docker 的,这样才能够一步成功,因为我们需要在打包 docker 之后与外部 docker 进行交互,如果使用本机的 mysql 或 redis,则 docker 会找不到服务器。

                                          首先编写 Dockerfile:

                                          FROM golang:alpine AS builder
                                          # 为我们的镜像设置必要的环境变量
                                          ENV GO111MODULE=on \
                                              GOPROXY=https://goproxy.cn,direct \
                                              CGO_ENABLED=0 \
                                              GOOS=linux \
                                              GOARCH=amd64
                                          # 移动到工作目录:/build
                                          WORKDIR /build
                                          # 将代码复制到容器中
                                          COPY go.mod .
                                          COPY go.sum .
                                          RUN go mod download
                                          COPY . .
                                          # 将我们的代码编译成二进制可执行文件 app
                                          RUN go build -o bluebell_app .
                                          FROM debian:stretch-slim
                                          COPY ./wait-for.sh /
                                          COPY ./templates /templates
                                          COPY ./static /static
                                          COPY ./conf /conf
                                          # 从builder镜像中把/dist/app 拷贝到当前目录
                                          COPY --from=builder /build/bluebell_app /
                                          RUN set -eux; \
                                              apt get updates; \
                                              apt get install -y \
                                                  --no-install-recommends \
                                                  netcat; \
                                                  chmod 755 wait-for.sh
                                          EXPOSE 8081
                                          

                                          之后我们构建镜像并指定镜像名为 bluebell:

                                          docker build . -t bluebell_app
                                          

                                          我们在启动 docker 的时候需要关联其它容器:

                                          docker run --link=mysql:mysql --link=redis507:redis507 -p 8081:8081 bluebell_app
                                          

                                          当然在启动前我们需要修改我们项目的 yaml 配置文件:

                                          name: "web_app"
                                          mode: "dev"
                                          port: 8081
                                          version: "v0.0.1"
                                          start_time: "2020-02-14"
                                          machine_id: 1
                                          auth:
                                            jwt_expire: 8760
                                          log:
                                            level: "debug"
                                            filename: "web_app.log"
                                            max_size: 200
                                            max_age: 30
                                            max_backups: 7
                                          mysql:
                                            host: mysql
                                            port: 13306
                                            user: "root"
                                            password: "root"
                                            dbname: "bluebell"
                                            max_open_conns: 200
                                            max_idle_conns: 50
                                          redis:
                                            host: redis507
                                            port: 6379
                                            password: ""
                                            db: 0
                                            pool_size: 100
                                          

                                          将 mysql 和 redis 的 host 修改为 docker 的名称。

                                          在 Terminal 直接运行,即可启动程序。

                                          【Bluebell】项目总结:基于 golang 的前后端分离 web 项目实战

                                          至此,我们完整地回顾了整个 bluebell 项目。

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

相关阅读

目录[+]

取消
微信二维码
微信二维码
支付宝二维码