diff --git a/src/base/base_do.go b/src/base/base_do.go new file mode 100644 index 0000000..f1f3f48 --- /dev/null +++ b/src/base/base_do.go @@ -0,0 +1,21 @@ +package base + +import ( + "time" +) + +type BaseDO struct { + // 自增ID + ID uint `gorm:"column:id;type:bigint(20);not null;primary_key;AUTO_INCREMENT" json:"id" form:"id"` + // 创建时间 + CreatedAt time.Time + // 更新时间 + UpdatedAt time.Time + // 删除时间 + DeletedAt *time.Time `sql:"index"` + // 扩展字段 + // 创建者 + CreatedBy string + // 删除者 + UpdatedBy string +} diff --git a/src/base/simple_page.go b/src/base/simple_page.go new file mode 100644 index 0000000..f57a56c --- /dev/null +++ b/src/base/simple_page.go @@ -0,0 +1,6 @@ +package base + +type SimplePage struct { + Page uint `json:"page"` + PageSize uint `json:"pageSize"` +} diff --git a/src/base/simple_page_data.go b/src/base/simple_page_data.go new file mode 100644 index 0000000..8a63c49 --- /dev/null +++ b/src/base/simple_page_data.go @@ -0,0 +1,6 @@ +package base + +type SimplePageData struct { + total uint `json:"total"` + data interface{} `json:"data"` +} diff --git a/src/config/application_config.go b/src/config/application_config.go new file mode 100644 index 0000000..1ecf61f --- /dev/null +++ b/src/config/application_config.go @@ -0,0 +1,129 @@ +package config + +import ( + "fmt" + "gopkg.in/yaml.v2" + "io/ioutil" + "os" + "strings" +) + +// 配置文件 +type AppConfig struct { + // 环境 + Env string `yaml:"env"` + // 运行模式 + Mode string `yaml:"mode"` + // 应用名称 + Name string `yaml:"name"` +} +type ServerConfig struct { + // 服务端口 + Port int `yaml:"port"` +} +type LogConfig struct { + // 日志文件配置 + File LogFileConfig `yaml:"file"` + // 日志级别(debug, info, warn, error) + Level string `yaml:"level"` +} +type LogFileConfig struct { + // 日志路径 + Path string `yaml:"path"` + // 日志名称 + Name string `yaml:"name"` +} + +type RedisConfig struct { + // 连接地址 + Addr string `yaml:"addr"` + // 密码 + Passwd string `yaml:"passwd"` + // 库索引 + Database int `yaml:"database"` +} + +type DataBaseConfig struct { + // 数据库方言 + Dialect string `yaml:"dialect"` + // 数据源 + DataSources DataSourceConfig `yaml:"datasources"` +} + +type DataSourceConfig struct { + MySQL DataSourceMySQLConfig `yaml:"mysql"` + SQLite3 DataSourceSQLite3Config `yaml:"sqlite3"` +} + +type DataSourceMySQLConfig struct { + // 连接地址 + Addr string `yaml:"addr"` + // 用户名 + Username string `yaml:"username"` + // 密码 + Password string `yaml:"password"` + // 数据库名称 + Database string `yaml:"database"` +} + +type DataSourceSQLite3Config struct { + // 数据库文件 + DBFile string `yaml:"dbfile"` +} + +type JWTConfig struct { + // 加密盐 + Secret string `yaml:"secret"` + // 过期时间 + Expiry int `yaml:"expiry"` +} + +type ApplicationProperties struct { + // 应用配置 + App AppConfig `yaml:"app"` + // Gin服务配置 + Server ServerConfig `yaml:"server"` + // 日志配置 + Log LogConfig `yaml:"log"` + // redis配置 + Redis RedisConfig `yaml:"redis"` + // 数据库配置 + DataBase DataBaseConfig `yaml:"database"` + // jwt配置 + Jwt JWTConfig `yaml:"jwt"` +} + +// 根据路径读取yaml文件 +func readYaml(path string) ApplicationProperties { + var result ApplicationProperties + data, err := ioutil.ReadFile(path) + if err != nil { + Logger.Error("File reading error, application.yml not exist!" + err.Error()) + panic(err) + } + err = yaml.Unmarshal(data, &result) + if err != nil { + Logger.Errorf("cannot unmarshal data: %v", err) + panic(err) + } + return result +} + +// 根据环境配置取配置明细 +func (v *ApplicationProperties) Init() { + result := readYaml(fmt.Sprintf("%s/application.yml", GetCurrentPath())) + // 判断环境 + if strings.Compare(result.App.Env, "dev") == 0 { + AppProps = readYaml(fmt.Sprintf("%s/application-%s.yml", GetCurrentPath(), "dev")) + } +} + +// 获取当前路径,比如:d:/abc +func GetCurrentPath() string { + dir, err := os.Getwd() + if err != nil { + Logger.Error(err) + panic(err) + } + return strings.Replace(dir, "\\", "/", -1) +} diff --git a/src/config/common.go b/src/config/common.go new file mode 100644 index 0000000..2a3b9b7 --- /dev/null +++ b/src/config/common.go @@ -0,0 +1,9 @@ +package config + +import "github.com/gin-gonic/gin" + +// 应用配置 +var AppProps ApplicationProperties + +// jwt拦截器 +var JwtHandle gin.HandlerFunc diff --git a/src/config/datasource_config.go b/src/config/datasource_config.go new file mode 100644 index 0000000..e60c10c --- /dev/null +++ b/src/config/datasource_config.go @@ -0,0 +1,59 @@ +package config + +import ( + "fmt" + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/mssql" + _ "github.com/jinzhu/gorm/dialects/mysql" + _ "github.com/jinzhu/gorm/dialects/postgres" + //_ "github.com/jinzhu/gorm/dialects/sqlite" + "lingye-gin/src/entity" + "strings" +) + +var SqlExcutor *gorm.DB + +type DataSourcePool struct{} + +func (v DataSourcePool) Connect() DataSourcePool { + if strings.Compare(AppProps.DataBase.Dialect, "") == 0 { + panic("yaml file dialect property is nil") + } + + var err error + if strings.Compare(AppProps.DataBase.Dialect, "mysql") == 0 { + connectStr := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", + AppProps.DataBase.DataSources.MySQL.Username, + AppProps.DataBase.DataSources.MySQL.Password, + AppProps.DataBase.DataSources.MySQL.Addr, + AppProps.DataBase.DataSources.MySQL.Database) + SqlExcutor, err = gorm.Open(AppProps.DataBase.Dialect, connectStr) + // 创建表时添加表后缀 + if SqlExcutor != nil { + SqlExcutor.Set("gorm:table_options", "ENGINE=InnoDB") + } + } else if strings.Compare(AppProps.DataBase.Dialect, "sqlite3") == 0 { + // windows上会报'cgo: exec gcc: exec'异常, 注掉吧 + // SqlExcutor, err = gorm.Open(AppProps.DataBase.Dialect, AppProps.DataBase.DataSources.SQLite3.DBFile) + } + if err != nil { + Logger.Error("db connect error: " + err.Error()) + panic(err) + } + if SqlExcutor != nil { + // 全局禁用表名复数 + // 如果设置为true,`User`的默认表名为`user`,使用`TableName`设置的表名不受影响 + SqlExcutor.SingularTable(true) + // 设置为true之后控制台会输出对应的SQL语句 + SqlExcutor.LogMode(true) + } + return v +} + +// 统一在这里注册数据表 +func (DataSourcePool) LoadEntity() { + // 自动迁移模式 + if SqlExcutor != nil { + SqlExcutor.AutoMigrate(&entity.User{}) + } +} diff --git a/src/config/log_config.go b/src/config/log_config.go new file mode 100644 index 0000000..5382a80 --- /dev/null +++ b/src/config/log_config.go @@ -0,0 +1,118 @@ +package config + +import ( + "fmt" + "github.com/gin-gonic/gin" + rotatelogs "github.com/lestrrat-go/file-rotatelogs" + "github.com/rifflock/lfshook" + "github.com/sirupsen/logrus" + "os" + "path" + "strings" + "time" +) + +// 实例化 +var Logger = logrus.New() + +// 日志记录到文件 +func LoggerToFile() gin.HandlerFunc { + logFilePath := AppProps.Log.File.Path + logFileName := AppProps.Log.File.Name + // 日志文件 + fileName := path.Join(logFilePath, logFileName) + + //禁止logrus的输出 + src, err := os.OpenFile(os.DevNull, os.O_APPEND|os.O_WRONLY, os.ModeAppend) + if err != nil { + panic(fmt.Sprintf("err: %v", err)) + } + + // 设置输出 + Logger.Out = src + // 设置日志级别 + if strings.Compare(AppProps.Log.Level, "debug") == 0 { + Logger.SetLevel(logrus.DebugLevel) + } else if strings.Compare(AppProps.Log.Level, "info") == 0 { + Logger.SetLevel(logrus.InfoLevel) + } else if strings.Compare(AppProps.Log.Level, "warn") == 0 { + Logger.SetLevel(logrus.WarnLevel) + } else if strings.Compare(AppProps.Log.Level, "error") == 0 { + Logger.SetLevel(logrus.ErrorLevel) + } + + // 设置 rotatelogs + logWriter, err := rotatelogs.New( + // 分割后的文件名称 + fileName+".%Y%m%d.log", + // 生成软链,指向最新日志文件 + rotatelogs.WithLinkName(fileName), + // 设置最大保存时间(7天) + rotatelogs.WithMaxAge(7*24*time.Hour), + // 设置日志切割时间间隔(1天) + rotatelogs.WithRotationTime(24*time.Hour), + ) + + writeMap := lfshook.WriterMap{ + logrus.InfoLevel: logWriter, + logrus.FatalLevel: logWriter, + logrus.DebugLevel: logWriter, + logrus.WarnLevel: logWriter, + logrus.ErrorLevel: logWriter, + logrus.PanicLevel: logWriter, + } + + // 新增钩子 + Logger.AddHook(lfshook.NewHook(writeMap, &logrus.JSONFormatter{ + // 这个日期是真的牛皮, yyyy-MM-dd hh:mm:ss它不香吗 + TimestampFormat: "2006-01-02 15:04:05", + })) + + return func(c *gin.Context) { + // 开始时间 + startTime := time.Now() + // 处理请求 + c.Next() + // 结束时间 + endTime := time.Now() + // 执行时间 + latencyTime := endTime.Sub(startTime) + // 请求方式 + reqMethod := c.Request.Method + // 请求路由 + reqUri := c.Request.RequestURI + // 状态码 + statusCode := c.Writer.Status() + // 请求IP + clientIP := c.ClientIP() + // 日志格式 + Logger.WithFields(logrus.Fields{ + "status_code": statusCode, + "latency_time": latencyTime, + "client_ip": clientIP, + "req_method": reqMethod, + "req_uri": reqUri, + }).Info() + } +} + +// 日志记录到 MongoDB +func LoggerToMongo() gin.HandlerFunc { + return func(c *gin.Context) { + + } +} + +// 日志记录到 ES +func LoggerToES() gin.HandlerFunc { + return func(c *gin.Context) { + + } +} + +// 日志记录到 MQ +func LoggerToMQ() gin.HandlerFunc { + return func(c *gin.Context) { + + } +} diff --git a/src/dao/user_dao.go b/src/dao/user_dao.go new file mode 100644 index 0000000..41514a5 --- /dev/null +++ b/src/dao/user_dao.go @@ -0,0 +1,64 @@ +package dao + +import ( + "lingye-gin/src/config" + "lingye-gin/src/entity" + "lingye-gin/src/service/dto" + "lingye-gin/src/util" +) + +type UserDAO struct{} + +// 新增 +func (UserDAO) Insert(resource *entity.User) { + config.SqlExcutor.Create(resource) +} + +// ID查询 +func (UserDAO) SelectOne(id uint) entity.User { + var user entity.User + config.SqlExcutor.First(&user, id) + return user +} + +// 查询所有记录 +func (UserDAO) SelectAll() []entity.User { + var users []entity.User + config.SqlExcutor.Find(&users) + return users +} + +// 条件查询 +func (UserDAO) SelectList(resource entity.User) []entity.User { + var users []entity.User + config.SqlExcutor.Where("username LIKE ?", "%"+resource.UserName+"%").Find(&users) + return users +} + +// 条件分页查询 +func (UserDAO) SelectPage(dto dto.UserDTO) []entity.User { + var users []entity.User + + page, pageSize := util.FixPage(dto.Page, dto.PageSize) + // 分页条件 + pageConfig := config.SqlExcutor.Where("username LIKE ?", "%"+dto.Username+"%") + // 分页参数 + pageConfig = pageConfig.Limit((page - 1) * pageSize).Offset(pageSize) + pageConfig.Find(&users) + return users +} + +func test() { + // + //user := entity.User{Username: "lingye"} + // + //userDAO := dao.UserDAO{} + //userDAO.Insert(&user) + //userDAO.SelectAll() + //userDAO.SelectOne(1) + //userDAO.SelectList(user) + // + //dto := dto2.UserDTO{} + //dto.Username = "测试" + //userDAO.SelectPage(dto) +} diff --git a/src/entity/user.go b/src/entity/user.go new file mode 100644 index 0000000..c1e5f44 --- /dev/null +++ b/src/entity/user.go @@ -0,0 +1,23 @@ +package entity + +import ( + "lingye-gin/src/base" +) + +type User struct { + base.BaseDO + UserName string `gorm:"column:user_name;type:varchar(255);not null;index:idx_username" json:"username" form:"username"` + NickName string `gorm:"column:nick_name;type:varchar(255)" json:"nickname" form:"nickname"` + Email string `gorm:"column:email;type:varchar(255)" json:"email" form:"email"` + PhoneNumber string `gorm:"column:phone_number;type:varchar(255)" json:"phoneNumber" form:"phoneNumber"` + Sex string `gorm:"column:sex;type:varchar(255)" json:"sex" form:"sex"` + Avatar string `gorm:"column:avatar;type:varchar(255)" json:"avatar" form:"avatar"` + Password string `gorm:"column:password;type:varchar(255);not null" json:"password" form:"password"` + Status string `gorm:"column:status;type:varchar(255);not null" json:"status" form:"status"` + Remark string `gorm:"column:remark;type:text(0)" json:"remark" form:"remark"` +} + +// 设置User的表名为`sys_user` +func (User) TableName() string { + return "sys_user" +} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..2c402cb --- /dev/null +++ b/src/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "lingye-gin/src/config" + "lingye-gin/src/middleware" +) + +func main() { + // 初始化yaml配置 + new(config.ApplicationProperties).Init() + // 初始化redis + new(middleware.RedisPool).Init() + // 初始化gorm + new(config.DataSourcePool).Connect().LoadEntity() + // 延时调用函数 + defer config.SqlExcutor.Close() + // 加载jwt + new(middleware.JwtEngine).Init() + // 初始化gin + new(middleware.GinEngine).Start() +} diff --git a/src/middleware/gin_engine.go b/src/middleware/gin_engine.go new file mode 100644 index 0000000..c14aca5 --- /dev/null +++ b/src/middleware/gin_engine.go @@ -0,0 +1,26 @@ +package middleware + +import ( + "fmt" + "github.com/gin-gonic/gin" + "lingye-gin/src/config" + "strings" +) + +type GinEngine struct { +} + +func (GinEngine) Start() { + config.Logger.Info("GinWebServer Init...") + if strings.Compare(config.AppProps.App.Mode, "debug") == 0 { + gin.SetMode(gin.DebugMode) + } else if strings.Compare(config.AppProps.App.Mode, "release") == 0 { + gin.SetMode(gin.ReleaseMode) + } + config.Logger.Info("GinWebServer Starting...") + engine := gin.Default() + engine.Use(config.LoggerToFile()) + // 设置路由 + new(GinRouter).Init(engine) + engine.Run(fmt.Sprintf("0.0.0.0:%d", config.AppProps.Server.Port)) +} diff --git a/src/middleware/gin_router.go b/src/middleware/gin_router.go new file mode 100644 index 0000000..a5a1e09 --- /dev/null +++ b/src/middleware/gin_router.go @@ -0,0 +1,115 @@ +package middleware + +import ( + "fmt" + "github.com/gin-gonic/gin" + "lingye-gin/src/config" + "lingye-gin/src/rest" + "strings" +) + +type GinRouter struct { +} + +func (v GinRouter) Init(r *gin.Engine) { + config.Logger.Info("GinRouter Init") + + // 一般路由映射关系 + groupMap := make(map[string]*gin.RouterGroup) + // api路由映射关系 + apiUrls := make([]rest.RequestApi, 0) + // api组名称 + apiGroupName := "api" + + for _, normalRa := range rest.Urls { + // 判断是否在某一组下 + if strings.Compare(normalRa.GroupName, "") == 0 { + // 批量处理 + if handle(normalRa, r) { + continue + } + } else { + // 分组名称 + groupName := fmt.Sprintf("/%s", normalRa.GroupName) + + // api组, 基于jwt验证 + if strings.Compare(normalRa.GroupName, apiGroupName) == 0 { + apiUrls = append(apiUrls, normalRa) + continue + } + + // 分组不存在 + if groupMap[normalRa.GroupName] == nil { + if normalRa.GroupHandleFunction == nil { + // 不存在分组过滤器 + groupMap[normalRa.GroupName] = r.Group(groupName) + } else { + groupMap[normalRa.GroupName] = r.Group(groupName, normalRa.GroupHandleFunction) + } + } + // 批量处理 + if handleGroup(normalRa, groupMap[normalRa.GroupName]) { + continue + } + } + } + + for _, apiRa := range apiUrls { + jwtR := r.Group(apiGroupName) + jwtR.Use(config.JwtHandle) + // 批量处理 + if handleGroup(apiRa, jwtR) { + continue + } + } + + config.Logger.Info("GinRouter Ok") +} + +func handle(request rest.RequestApi, engine *gin.Engine) bool { + // get + if strings.Compare(request.Mode, "get") == 0 { + engine.GET(request.RelativePath, request.HandleFunction) + return true + } + // post + if strings.Compare(request.Mode, "post") == 0 { + engine.POST(request.RelativePath, request.HandleFunction) + return true + } + // delete + if strings.Compare(request.Mode, "delete") == 0 { + engine.DELETE(request.RelativePath, request.HandleFunction) + return true + } + // put + if strings.Compare(request.Mode, "put") == 0 { + engine.PUT(request.RelativePath, request.HandleFunction) + return true + } + return false +} + +func handleGroup(request rest.RequestApi, group *gin.RouterGroup) bool { + // get + if strings.Compare(request.Mode, "get") == 0 { + group.GET(request.RelativePath, request.HandleFunction) + return true + } + // post + if strings.Compare(request.Mode, "post") == 0 { + group.POST(request.RelativePath, request.HandleFunction) + return true + } + // delete + if strings.Compare(request.Mode, "delete") == 0 { + group.DELETE(request.RelativePath, request.HandleFunction) + return true + } + // put + if strings.Compare(request.Mode, "put") == 0 { + group.PUT(request.RelativePath, request.HandleFunction) + return true + } + return false +} diff --git a/src/middleware/jwt_engine.go b/src/middleware/jwt_engine.go new file mode 100644 index 0000000..88d8999 --- /dev/null +++ b/src/middleware/jwt_engine.go @@ -0,0 +1,158 @@ +package middleware + +import ( + "github.com/dgrijalva/jwt-go" + "github.com/gin-gonic/gin" + "lingye-gin/src/config" + "lingye-gin/src/entity" + "net/http" + "strings" + "time" +) + +// Jwt引擎 +type JwtEngine struct { +} + +// 认证请求头 +const HeadAuthorization = "Authorization" +const HeadLingYe = "LingYe" + +// Payload 载荷 +// 继承jwt提供给载荷,扩展自己所需字段 +type JwtClaims struct { + jwt.StandardClaims + UserId uint `json:"id"` // 用户ID + UserName string `json:"username"` // 用户名 + NickName string `json:"nickname"` // 用户昵称 + Email string `json:"email"` // 邮箱账号 + UserRole []string `json:"userRoles"` // 用户角色编码合集 +} + +// 生成令牌 +func (JwtEngine) CreateJwtToken(user entity.User, timeout int) (string, bool) { + // Jwt Secret 私钥 + jwtSecret := config.AppProps.Jwt.Secret + // 过期时间 + expiredAt := time.Now().Add(time.Hour * time.Duration(timeout)).Unix() + // 设置载荷 + claims := JwtClaims{} + claims.UserId = user.ID + claims.UserName = user.UserName + claims.NickName = user.NickName + claims.Email = user.Email + claims.ExpiresAt = expiredAt + // claims.Issuer = "ginv" // 非必须,也可以填充用户名 + // 生成令牌 采用HMAC SHA256算法加密 + token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) + // 令牌签名 + tokenString, err := token.SignedString([]byte(jwtSecret)) + if err != nil { + return "", false + } + return tokenString, true +} + +// 验证令牌 +func (JwtEngine) validateJwt(tokenString string) (*JwtClaims, bool) { + // Jwt Secret 私钥 + jwtSecret := config.AppProps.Jwt.Secret + // 解析令牌字符串 + token, err := jwt.ParseWithClaims(tokenString, &JwtClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(jwtSecret), nil + }) + if err != nil { + config.Logger.Println(err) + return nil, false + } + // 获取载荷 + claims, ok := token.Claims.(*JwtClaims) + if ok && token.Valid { + return claims, true + } + return nil, false +} + +// 更新Token +func (engine JwtEngine) RefreshToken(tokenString string) (string, bool) { + // Jwt Secret 私钥 + jwtSecret := config.AppProps.Jwt.Secret + jwt.TimeFunc = func() time.Time { + return time.Unix(0, 0) + } + // 解析令牌字符串 + token, err := jwt.ParseWithClaims(tokenString, &JwtClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(jwtSecret), nil + }) + if err != nil { + config.Logger.Println(err) + return "", false + } + // 获取载荷 + var claims *JwtClaims + var ok bool + if claims, ok = token.Claims.(*JwtClaims); ok && token.Valid { + jwt.TimeFunc = time.Now + // 过期时间 + expiredAt := time.Now().Add(1 * time.Hour).Unix() + claims.StandardClaims.ExpiresAt = expiredAt + // 拷贝载体 + currentUserInfo := entity.User{} + currentUserInfo.ID = claims.UserId + currentUserInfo.UserName = claims.UserName + currentUserInfo.NickName = claims.NickName + currentUserInfo.Email = claims.Email + return engine.CreateJwtToken(currentUserInfo, config.AppProps.Jwt.Expiry) + } + return "", false +} + +// 初始化 +func (engine JwtEngine) Init() { + config.Logger.Info("JwtEngine Init...") + config.JwtHandle = func(c *gin.Context) { + // 获取请求头中的Authorization + authorization := c.Request.Header.Get(HeadAuthorization) + if strings.Compare(authorization, "") == 0 { + c.JSON(http.StatusOK, gin.H{ + "code": http.StatusUnauthorized, + "msg": "Authorization Header Is None, No Permission", + }) + c.Abort() + return + } + // 拆分Authorization字段获取token字符串 + strSlice := strings.SplitN(authorization, " ", 2) + if len(strSlice) != 2 || strings.Compare(HeadLingYe, strSlice[0]) != 0 { + c.JSON(http.StatusOK, gin.H{ + "code": http.StatusUnauthorized, + "msg": "Authorization Header Format Is Error, No Permission", + }) + c.Abort() + return + } + // 验证token字符串 + claim, ok := engine.validateJwt(strSlice[1]) + if !ok { + c.JSON(http.StatusOK, gin.H{ + "code": http.StatusBadRequest, + "msg": "Token Validate Error", + }) + c.Abort() + return + } + // 过期判断 + if time.Now().Unix() > claim.ExpiresAt { + c.JSON(http.StatusOK, gin.H{ + "code": http.StatusBadRequest, + "msg": "Token Is Expired", + }) + c.Abort() + return + } + // 设置用户名 + c.Set("username", claim.UserName) + c.Next() + } + config.Logger.Info("JwtEngine Ok...") +} diff --git a/src/middleware/redis_client.go b/src/middleware/redis_client.go new file mode 100644 index 0000000..c6948bb --- /dev/null +++ b/src/middleware/redis_client.go @@ -0,0 +1,355 @@ +package middleware + +import ( + "fmt" + "github.com/go-redis/redis" + "lingye-gin/src/config" + "math/rand" + "time" +) + +var RedisClient *redis.Client + +type RedisPool struct{} + +func (p RedisPool) Init() { + config.Logger.Info("RedisPool Init") + // 连接服务器 + RedisClient = redis.NewClient(&redis.Options{ + Addr: config.AppProps.Redis.Addr, + Password: config.AppProps.Redis.Passwd, + DB: config.AppProps.Redis.Database, + }) + + // 心跳 + pong, err := RedisClient.Ping().Result() + + // Output: PONG + config.Logger.Info("RedisPool Ping And ", pong, err) + config.Logger.Info("RedisPool Ok") +} + +func (p RedisPool) Test() { + ExampleClient_String() + ExampleClient_List() + ExampleClient_Hash() + ExampleClient_Set() + ExampleClient_SortSet() + ExampleClient_HyperLogLog() + ExampleClient_CMD() + ExampleClient_Scan() + ExampleClient_Tx() + ExampleClient_Script() + ExampleClient_PubSub() +} + +func ExampleClient_String() { + config.Logger.Println("ExampleClient_String") + defer config.Logger.Println("ExampleClient_String") + + //kv读写 + err := RedisClient.Set("key", "value", 1*time.Second).Err() + config.Logger.Println(err) + + //获取过期时间 + tm, err := RedisClient.TTL("key").Result() + config.Logger.Println(tm) + + val, err := RedisClient.Get("key").Result() + config.Logger.Println(val, err) + + val2, err := RedisClient.Get("missing_key").Result() + if err == redis.Nil { + config.Logger.Println("missing_key does not exist") + } else if err != nil { + config.Logger.Println("missing_key", val2, err) + } + + //不存在才设置 过期时间 nx ex + value, err := RedisClient.SetNX("counter", 0, 1*time.Second).Result() + config.Logger.Println("setnx", value, err) + + //Incr + result, err := RedisClient.Incr("counter").Result() + config.Logger.Println("Incr", result, err) +} + +func ExampleClient_List() { + config.Logger.Println("ExampleClient_List") + defer config.Logger.Println("ExampleClient_List") + + //添加 + config.Logger.Println(RedisClient.RPush("list_test", "message1").Err()) + config.Logger.Println(RedisClient.RPush("list_test", "message2").Err()) + + //设置 + config.Logger.Println(RedisClient.LSet("list_test", 2, "message set").Err()) + + //remove + ret, err := RedisClient.LRem("list_test", 3, "message1").Result() + config.Logger.Println(ret, err) + + rLen, err := RedisClient.LLen("list_test").Result() + config.Logger.Println(rLen, err) + + //遍历 + lists, err := RedisClient.LRange("list_test", 0, rLen-1).Result() + config.Logger.Println("LRange", lists, err) + + //pop没有时阻塞 + result, err := RedisClient.BLPop(1*time.Second, "list_test").Result() + config.Logger.Println("result:", result, err, len(result)) +} + +func ExampleClient_Hash() { + config.Logger.Println("ExampleClient_Hash") + defer config.Logger.Println("ExampleClient_Hash") + + datas := map[string]interface{}{ + "name": "LI LEI", + "sex": 1, + "age": 28, + "tel": 123445578, + } + + //添加 + if err := RedisClient.HMSet("hash_test", datas).Err(); err != nil { + config.Logger.Fatal(err) + } + + //获取 + rets, err := RedisClient.HMGet("hash_test", "name", "sex").Result() + config.Logger.Println("rets:", rets, err) + + //成员 + retAll, err := RedisClient.HGetAll("hash_test").Result() + config.Logger.Println("retAll", retAll, err) + + //存在 + bExist, err := RedisClient.HExists("hash_test", "tel").Result() + config.Logger.Println(bExist, err) + + bRet, err := RedisClient.HSetNX("hash_test", "id", 100).Result() + config.Logger.Println(bRet, err) + + //删除 + config.Logger.Println(RedisClient.HDel("hash_test", "age").Result()) +} + +func ExampleClient_Set() { + config.Logger.Println("ExampleClient_Set") + defer config.Logger.Println("ExampleClient_Set") + + //添加 + ret, err := RedisClient.SAdd("set_test", "11", "22", "33", "44").Result() + config.Logger.Println(ret, err) + + //数量 + count, err := RedisClient.SCard("set_test").Result() + config.Logger.Println(count, err) + + //删除 + ret, err = RedisClient.SRem("set_test", "11", "22").Result() + config.Logger.Println(ret, err) + + //成员 + members, err := RedisClient.SMembers("set_test").Result() + config.Logger.Println(members, err) + + bret, err := RedisClient.SIsMember("set_test", "33").Result() + config.Logger.Println(bret, err) + + RedisClient.SAdd("set_a", "11", "22", "33", "44") + RedisClient.SAdd("set_b", "11", "22", "33", "55", "66", "77") + //差集 + diff, err := RedisClient.SDiff("set_a", "set_b").Result() + config.Logger.Println(diff, err) + + //交集 + inter, err := RedisClient.SInter("set_a", "set_b").Result() + config.Logger.Println(inter, err) + + //并集 + union, err := RedisClient.SUnion("set_a", "set_b").Result() + config.Logger.Println(union, err) + + ret, err = RedisClient.SDiffStore("set_diff", "set_a", "set_b").Result() + config.Logger.Println(ret, err) + + rets, err := RedisClient.SMembers("set_diff").Result() + config.Logger.Println(rets, err) +} + +func ExampleClient_SortSet() { + config.Logger.Println("ExampleClient_SortSet") + defer config.Logger.Println("ExampleClient_SortSet") + + addArgs := make([]redis.Z, 100) + for i := 1; i < 100; i++ { + addArgs = append(addArgs, redis.Z{Score: float64(i), Member: fmt.Sprintf("a_%d", i)}) + } + //Logger.Println(addArgs) + + Shuffle := func(slice []redis.Z) { + r := rand.New(rand.NewSource(time.Now().Unix())) + for len(slice) > 0 { + n := len(slice) + randIndex := r.Intn(n) + slice[n-1], slice[randIndex] = slice[randIndex], slice[n-1] + slice = slice[:n-1] + } + } + + //随机打乱 + Shuffle(addArgs) + + //添加 + ret, err := RedisClient.ZAddNX("sortset_test", addArgs...).Result() + config.Logger.Println(ret, err) + + //获取指定成员score + score, err := RedisClient.ZScore("sortset_test", "a_10").Result() + config.Logger.Println(score, err) + + //获取制定成员的索引 + index, err := RedisClient.ZRank("sortset_test", "a_50").Result() + config.Logger.Println(index, err) + + count, err := RedisClient.SCard("sortset_test").Result() + config.Logger.Println(count, err) + + //返回有序集合指定区间内的成员 + rets, err := RedisClient.ZRange("sortset_test", 10, 20).Result() + config.Logger.Println(rets, err) + + //返回有序集合指定区间内的成员分数从高到低 + rets, err = RedisClient.ZRevRange("sortset_test", 10, 20).Result() + config.Logger.Println(rets, err) + + //指定分数区间的成员列表 + rets, err = RedisClient.ZRangeByScore("sortset_test", redis.ZRangeBy{Min: "(30", Max: "(50", Offset: 1, Count: 10}).Result() + config.Logger.Println(rets, err) +} + +//用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。 +//每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数 +func ExampleClient_HyperLogLog() { + config.Logger.Println("ExampleClient_HyperLogLog") + defer config.Logger.Println("ExampleClient_HyperLogLog") + + for i := 0; i < 10000; i++ { + RedisClient.PFAdd("pf_test_1", fmt.Sprintf("pfkey%d", i)) + } + ret, err := RedisClient.PFCount("pf_test_1").Result() + config.Logger.Println(ret, err) + + for i := 0; i < 10000; i++ { + RedisClient.PFAdd("pf_test_2", fmt.Sprintf("pfkey%d", i)) + } + ret, err = RedisClient.PFCount("pf_test_2").Result() + config.Logger.Println(ret, err) + + RedisClient.PFMerge("pf_test", "pf_test_2", "pf_test_1") + ret, err = RedisClient.PFCount("pf_test").Result() + config.Logger.Println(ret, err) +} + +func ExampleClient_PubSub() { + config.Logger.Println("ExampleClient_PubSub") + defer config.Logger.Println("ExampleClient_PubSub") + //发布订阅 + pubsub := RedisClient.Subscribe("subkey") + _, err := pubsub.Receive() + if err != nil { + config.Logger.Fatal("pubsub.Receive") + } + ch := pubsub.Channel() + time.AfterFunc(1*time.Second, func() { + config.Logger.Println("Publish") + + err = RedisClient.Publish("subkey", "test publish 1").Err() + if err != nil { + config.Logger.Fatal("RedisClient.Publish", err) + } + + RedisClient.Publish("subkey", "test publish 2") + }) + for msg := range ch { + config.Logger.Println("recv channel:", msg.Channel, msg.Pattern, msg.Payload) + } +} + +func ExampleClient_CMD() { + config.Logger.Println("ExampleClient_CMD") + defer config.Logger.Println("ExampleClient_CMD") + + //执行自定义redis命令 + Get := func(rdb *redis.Client, key string) *redis.StringCmd { + cmd := redis.NewStringCmd("get", key) + RedisClient.Process(cmd) + return cmd + } + + v, err := Get(RedisClient, "NewStringCmd").Result() + config.Logger.Println("NewStringCmd", v, err) + + v, err = RedisClient.Do("get", "RedisClient.do").String() + config.Logger.Println("RedisClient.Do", v, err) +} + +func ExampleClient_Scan() { + config.Logger.Println("ExampleClient_Scan") + defer config.Logger.Println("ExampleClient_Scan") + + //scan + for i := 1; i < 1000; i++ { + RedisClient.Set(fmt.Sprintf("skey_%d", i), i, 0) + } + + cusor := uint64(0) + for { + keys, retCusor, err := RedisClient.Scan(cusor, "skey_*", int64(100)).Result() + config.Logger.Println(keys, cusor, err) + cusor = retCusor + if cusor == 0 { + break + } + } +} + +func ExampleClient_Tx() { + pipe := RedisClient.TxPipeline() + incr := pipe.Incr("tx_pipeline_counter") + pipe.Expire("tx_pipeline_counter", time.Hour) + + // Execute + // + // MULTI + // INCR pipeline_counter + // EXPIRE pipeline_counts 3600 + // EXEC + // + // using one rdb-server roundtrip. + _, err := pipe.Exec() + fmt.Println(incr.Val(), err) +} + +func ExampleClient_Script() { + IncrByXX := redis.NewScript(` + if redis.call("GET", KEYS[1]) ~= false then + return redis.call("INCRBY", KEYS[1], ARGV[1]) + end + return false + `) + + n, err := IncrByXX.Run(RedisClient, []string{"xx_counter"}, 2).Result() + fmt.Println(n, err) + + err = RedisClient.Set("xx_counter", "40", 0).Err() + if err != nil { + panic(err) + } + + n, err = IncrByXX.Run(RedisClient, []string{"xx_counter"}, 2).Result() + fmt.Println(n, err) +} diff --git a/src/rest/api/student_rest.go b/src/rest/api/student_rest.go new file mode 100644 index 0000000..6917f6e --- /dev/null +++ b/src/rest/api/student_rest.go @@ -0,0 +1,20 @@ +package api + +import ( + "github.com/gin-gonic/gin" +) + +type Student struct { + ID uint `json:"id"` + Username string `json:"username"` + Age uint `json:"age"` +} + +func DescribeStudents(c *gin.Context) { + var students []Student + c.JSON(200, gin.H{ + "data": students, + "page": 1, + "pageSize": 15, + }) +} diff --git a/src/rest/sign_rest.go b/src/rest/sign_rest.go new file mode 100644 index 0000000..dfd7576 --- /dev/null +++ b/src/rest/sign_rest.go @@ -0,0 +1,21 @@ +package rest + +import ( + "github.com/gin-gonic/gin" + "lingye-gin/src/util" + "net/url" + "strconv" +) + +func DescribeSign(c *gin.Context) { + ts := strconv.FormatInt(util.GetTimeUnix(), 10) + res := map[string]interface{}{} + params := url.Values{ + "username": []string{"lingye"}, + "age": []string{"10"}, + "ts": []string{ts}, + } + res["sn"] = util.CreateSign(params) + res["ts"] = ts + util.RetJson("200", "", res, c) +} diff --git a/src/rest/urls.go b/src/rest/urls.go new file mode 100644 index 0000000..00f6f9d --- /dev/null +++ b/src/rest/urls.go @@ -0,0 +1,43 @@ +package rest + +import ( + "github.com/gin-gonic/gin" + v3 "lingye-gin/src/rest/api" + v1 "lingye-gin/src/rest/v1" + v2 "lingye-gin/src/rest/v2" + "lingye-gin/src/util" +) + +type RequestApi struct { + // get、post、delete、put + Mode string + // 分组名称 + GroupName string + // 分组过滤器 + GroupHandleFunction gin.HandlerFunc + // 请求路径 + RelativePath string + // 请求处理器 + HandleFunction gin.HandlerFunc +} + +// 定义变长数组变量 +var Urls = [...]RequestApi{ + // 定义请求方式和路径 + {Mode: "get", RelativePath: "/sn", HandleFunction: DescribeSign}, + // v1 + // 获取所有学生 + {GroupName: "v1", Mode: "get", RelativePath: "/students", HandleFunction: v1.DescribeStudents}, + // 根据ID获取学生 + {GroupName: "v1", Mode: "get", RelativePath: "/students/:id", HandleFunction: v1.DescribeStudentById}, + // 保存学生 + {GroupName: "v1", Mode: "post", RelativePath: "/students", HandleFunction: v1.CreateStudent}, + // 根据ID更新学生 + {GroupName: "v1", Mode: "put", RelativePath: "/students/:id", HandleFunction: v1.ModifyStudentById}, + // 根据ID删除学生 + {GroupName: "v1", Mode: "delete", RelativePath: "/students/:id", HandleFunction: v1.DeleteStudentById}, + // v2 + {GroupName: "v2", GroupHandleFunction: util.VerifySign, Mode: "get", RelativePath: "/students", HandleFunction: v2.DescribeStudents}, + // api jwt + {GroupName: "api", Mode: "get", RelativePath: "/students", HandleFunction: v3.DescribeStudents}, +} diff --git a/src/rest/v1/student_rest.go b/src/rest/v1/student_rest.go new file mode 100644 index 0000000..bcfd64d --- /dev/null +++ b/src/rest/v1/student_rest.go @@ -0,0 +1,65 @@ +package v1 + +import ( + "fmt" + "github.com/gin-gonic/gin" +) + +type Student struct { + ID uint `json:"id"` + Username string `json:"username"` + Age uint `json:"age"` +} + +func DescribeStudents(c *gin.Context) { + var students []Student + c.JSON(200, gin.H{ + "data": students, + "page": 1, + "pageSize": 15, + }) +} + +func DescribeStudentById(c *gin.Context) { + id := c.Params.ByName("id") + fmt.Println("id=", id) + var student Student + if student.ID == 0 { + c.JSON(404, gin.H{"message": "student not found"}) + return + } + c.JSON(200, student) +} + +func CreateStudent(c *gin.Context) { + var student Student + // 绑定一个请求主体到一个类型 + _ = c.BindJSON(&student) + c.JSON(200, "创建成功") +} + +func ModifyStudentById(c *gin.Context) { + id := c.Params.ByName("id") + fmt.Println("id=", id) + var student Student + if student.ID == 0 { + c.JSON(404, gin.H{"message": "student not found"}) + return + } else { + _ = c.BindJSON(&student) + c.JSON(200, student) + } +} + +func DeleteStudentById(c *gin.Context) { + id := c.Params.ByName("id") + fmt.Println("id=", id) + var student Student + if student.ID == 0 { + c.JSON(404, gin.H{"message": "student not found"}) + return + } else { + _ = c.BindJSON(&student) + c.JSON(200, gin.H{"message": "delete success"}) + } +} diff --git a/src/rest/v2/student_rest.go b/src/rest/v2/student_rest.go new file mode 100644 index 0000000..170437d --- /dev/null +++ b/src/rest/v2/student_rest.go @@ -0,0 +1,20 @@ +package v2 + +import ( + "github.com/gin-gonic/gin" +) + +type Student struct { + ID uint `json:"id"` + Username string `json:"username"` + Age uint `json:"age"` +} + +func DescribeStudents(c *gin.Context) { + var students []Student + c.JSON(200, gin.H{ + "data": students, + "page": 1, + "pageSize": 15, + }) +} diff --git a/src/service/dto/user_dto.go b/src/service/dto/user_dto.go new file mode 100644 index 0000000..c121cad --- /dev/null +++ b/src/service/dto/user_dto.go @@ -0,0 +1,8 @@ +package dto + +import "lingye-gin/src/base" + +type UserDTO struct { + base.SimplePage + Username string +} diff --git a/src/util/util.go b/src/util/util.go new file mode 100644 index 0000000..cf17b99 --- /dev/null +++ b/src/util/util.go @@ -0,0 +1,113 @@ +package util + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "github.com/gin-gonic/gin" + "lingye-gin/src/config" + "net/http" + "net/url" + "sort" + "strconv" + "time" +) + +// 打印 +func Print(i interface{}) { + fmt.Println("---") + fmt.Println(i) + fmt.Println("---") +} + +// 返回JSON +func RetJson(code, msg string, data interface{}, c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": code, + "msg": msg, + "data": data, + }) + c.Abort() +} + +// 获取当前时间戳 +func GetTimeUnix() int64 { + return time.Now().Unix() +} + +// MD5 方法 +func MD5(str string) string { + s := md5.New() + s.Write([]byte(str)) + return hex.EncodeToString(s.Sum(nil)) +} + +// 修正分页参数 +func FixPage(page uint, pageSize uint) (p uint, ps uint) { + if page == 0 { + p = 1 + } + if pageSize == 0 { + ps = 15 + } + return p, ps +} + +// 生成签名 +func CreateSign(params url.Values) string { + var key []string + var str = "" + for k := range params { + if k != "sn" { + key = append(key, k) + } + } + sort.Strings(key) + for i := 0; i < len(key); i++ { + if i == 0 { + str = fmt.Sprintf("%v=%v", key[i], params.Get(key[i])) + } else { + str = str + fmt.Sprintf("&%v=%v", key[i], params.Get(key[i])) + } + } + // 自定义签名算法 + sign := MD5(MD5(str) + MD5(config.AppProps.App.Name+config.AppProps.Jwt.Secret)) + return sign +} + +// 验证签名 +func VerifySign(c *gin.Context) { + var method = c.Request.Method + var ts int64 + var sn string + var req url.Values + + if method == "GET" { + req = c.Request.URL.Query() + sn = c.Query("sn") + ts, _ = strconv.ParseInt(c.Query("ts"), 10, 64) + + } else if method == "POST" { + _ = c.Request.ParseForm() + req = c.Request.PostForm + sn = c.PostForm("sn") + ts, _ = strconv.ParseInt(c.PostForm("ts"), 10, 64) + } else { + RetJson("500", "Illegal requests", "", c) + return + } + + exp, _ := strconv.ParseInt(string(rune(config.AppProps.Jwt.Expiry)), 10, 64) + + // 验证过期时间 + if ts > GetTimeUnix() || GetTimeUnix()-ts >= exp { + RetJson("500", "Ts Error", "", c) + return + } + + // 验证签名 + if sn == "" || sn != CreateSign(req) { + RetJson("500", "Sn Error", "", c) + return + } +}