diff --git a/app/conf/app.toml b/app/conf/app.toml index 886986bee2883915db8d738df29095f1ad8202e7..91fab0451482b211607484609409fd6f384b1be0 100644 --- a/app/conf/app.toml +++ b/app/conf/app.toml @@ -6,3 +6,4 @@ salt_api = "https://95.179.239.175:8088" redis_url = "127.0.0.1:6379" nginx_log_path = "/usr/local/var/log/nginx/access.log" mysql_url = "root:123456@tcp(localhost:3306)/lan_db?charset=utf8&parseTime=True" + diff --git a/app/conf/config.go b/app/conf/config.go index f9ebd9a9bbeee735cda8b4b5e0c601860c0173b1..da935dfc39a9aa56694d62fe4ec658f6402812bc 100644 --- a/app/conf/config.go +++ b/app/conf/config.go @@ -23,6 +23,8 @@ type Config struct { RedisURL string `toml:"redis_url"` NginxLogPath string `toml:"nginx_log_path"` MysqlUrl string `toml:"mysql_url"` + AppID string `toml:"app_id"` + Secret string `toml:"secret"` } // GetDB ... diff --git a/app/handler/activity.go b/app/handler/activity.go new file mode 100644 index 0000000000000000000000000000000000000000..6bbcc37b2b83a4afa93bb0fe6e589177c5e2723b --- /dev/null +++ b/app/handler/activity.go @@ -0,0 +1,139 @@ +package handler + +import ( + "fmt" + "gitee.com/lanshiren/miniprogram/app/log" + "gitee.com/lanshiren/miniprogram/app/model" + "gitee.com/lanshiren/miniprogram/app/service" + "github.com/gin-gonic/gin" + "strings" +) + +func HandleCreateActivity(c *gin.Context) { + var activity model.ActivityVO + if err := c.BindJSON(&activity); err != nil { + log.MainLogger.Error(fmt.Sprintf("bind client json to activity err: %v", err)) + } + if err := service.SaveActivity(activity); err != nil { + log.MainLogger.Error(fmt.Sprintf("save activity err: %v", err)) + c.JSON(500, gin.H{ + "errMsg": fmt.Sprintf("database exception: %v", err.Error()), + }) + return + } + c.JSON(200, gin.H{ + "err_msg": "", + }) +} + +func HandleSearchActivities(c *gin.Context) { + var param model.SearchActivitiesParam + if err := c.BindJSON(¶m); err != nil { + log.MainLogger.Error(fmt.Sprintf("bind client json to SearchActivitiesParam err: %v", err)) + } + total, acs, err := service.SearchActivities(param) + if err != nil { + log.MainLogger.Error(fmt.Sprintf("SearchActivities err: %v", err)) + c.JSON(500, gin.H{ + "errMsg": fmt.Sprintf("database exception: %v", err.Error()), + }) + return + } + c.JSON(200, gin.H{ + "total": total, + "results": acs, + }) +} + +func HandleUpdateActivity(c *gin.Context) { + var param model.UpdateActivityParam + if err := c.BindJSON(¶m); err != nil { + log.MainLogger.Error(fmt.Sprintf("bind client json to UpdateActivityParam err: %v", err)) + } + if err := service.UpdateActivity(param); err != nil { + log.MainLogger.Error(fmt.Sprintf("UpdateActivityParam err: %v", err)) + c.JSON(500, gin.H{ + "errMsg": fmt.Sprintf("database exception: %v", err.Error()), + }) + return + } + c.JSON(200, gin.H{ + "err_msg": "", + }) +} + +func HandleVerifyActivity(c *gin.Context) { + var param model.VerifyActivityParam + if err := c.BindJSON(¶m); err != nil { + log.MainLogger.Error(fmt.Sprintf("bind client json to VerifyActivityParam err: %v", err)) + } + // TODO: check request token has the permission to verify + if param.IsApprove { + if err := service.VerifyActivity(param.ID, true); err != nil { + log.MainLogger.Error(fmt.Sprintf("VerifyActivity err: %v", err)) + c.JSON(500, gin.H{ + "errMsg": fmt.Sprintf("database exception: %v", err.Error()), + }) + return + } + } + c.JSON(200, gin.H{ + "err_msg": "", + }) +} + +func HandleRegistrationActivity(c *gin.Context) { + token := c.Request.Header.Get("token") + userData, err := service.TryGetUserSession("session-" + token) + if err != nil { + return + } + openId := strings.Split(userData, "$")[0] + user, err := service.GetUserByOpenID(openId) + if err != nil { + return + } + // TODO: check user has permission + var param model.RegistrationActivityParam + if err := c.BindJSON(¶m); err != nil { + log.MainLogger.Error(fmt.Sprintf("bind client json to RegistrationActivityParam err: %v", err)) + } + if err := service.RegistrationActivity(user.ID, param.ActivityID); err != nil { + log.MainLogger.Error(fmt.Sprintf("RegistrationActivity err: %v", err)) + c.JSON(500, gin.H{ + "errMsg": fmt.Sprintf("database exception: %v", err.Error()), + }) + return + } + c.JSON(200, gin.H{ + "err_msg": "", + }) +} + +func HandleCancelRegistrationActivity(c *gin.Context) { + token := c.Request.Header.Get("token") + userData, err := service.TryGetUserSession("session-" + token) + if err != nil { + return + } + openId := strings.Split(userData, "$")[0] + user, err := service.GetUserByOpenID(openId) + if err != nil { + return + } + // TODO: check user has permission + var param model.RegistrationActivityParam + if err := c.BindJSON(¶m); err != nil { + log.MainLogger.Error(fmt.Sprintf("bind client json to RegistrationActivityParam err: %v", err)) + } + if err := service.CancelRegistrationActivity(user.ID, param.ActivityID); err != nil { + log.MainLogger.Error(fmt.Sprintf("CancelRegistrationActivity err: %v", err)) + c.JSON(500, gin.H{ + "errMsg": fmt.Sprintf("database exception: %v", err.Error()), + }) + return + } + c.JSON(200, gin.H{ + "err_msg": "", + }) +} diff --git a/app/handler/auth.go b/app/handler/auth.go new file mode 100644 index 0000000000000000000000000000000000000000..81ea08b83144d690dd0323ce76599ad20da75539 --- /dev/null +++ b/app/handler/auth.go @@ -0,0 +1,123 @@ +package handler + +import ( + "encoding/json" + "fmt" + config "gitee.com/lanshiren/miniprogram/app/conf" + "gitee.com/lanshiren/miniprogram/app/log" + "gitee.com/lanshiren/miniprogram/app/model" + "gitee.com/lanshiren/miniprogram/app/service" + "github.com/gin-gonic/gin" + "github.com/xlstudio/wxbizdatacrypt" + "net/http" + "strings" +) + +func HandleAuthCode(c *gin.Context) { + var authData model.AuthData + if err := c.BindJSON(&authData); err != nil { + log.MainLogger.Error(fmt.Sprintf("bind client json to code err: %v", err)) + } + auRes, err := service.WxJsCode2Session(authData.Code) + if err != nil { + c.JSON(500, gin.H{ + "errMsg": err.Error(), + }) + return + } + fmt.Printf("%v", auRes) + // token := SHA(Openid + SessionKey + authData.NickName + authData.AvatarUrl + randString) + token := "" + switch auRes.Errcode { + case 0: // success + token = service.GenToken(fmt.Sprintf("%s.%s.%s", auRes.Openid, auRes.SessionKey, service.GenRandString(32))) + // handle register success + if err := service.SaveSession(3600, "session-" + token, auRes.Openid+"$"+auRes.SessionKey); err != nil { + c.JSON(500, gin.H{ + "errMsg": "服务器错误,请稍后再试", + }) + return + } + // Save Open ID to db + err := service.SaveUserOpenID(auRes.Openid) + if err != nil { + c.JSON(500, gin.H{ + "errMsg": fmt.Sprintf("database exception: %v", err.Error()), + }) + return + } + break + case -1: + c.JSON(500, gin.H{ + "errMsg": "系统繁忙, 请稍候再试", + }) + return + case 40029: + c.JSON(500, gin.H{ + "errMsg": "code 无效", + }) + return + case 45011: + c.JSON(500, gin.H{ + "errMsg": "频率限制,每个用户每分钟100次", + }) + return + } + c.JSON(200, gin.H{ + "token": token, + }) +} + +func AuthorizedLogin(c *gin.Context) { + token := c.Request.Header.Get("token") + userData, err := service.TryGetUserSession("session-" + token) + if err != nil { + return + } + var loginData model.LoginData + if err := c.BindJSON(&loginData); err != nil { + log.MainLogger.Error(fmt.Sprintf("bind client json to code err: %v", err)) + } + pc := wxbizdatacrypt.WxBizDataCrypt{AppId: config.Conf.AppID, SessionKey: strings.Split(userData, "$")[1]} + result, err := pc.Decrypt(loginData.EncryptedData, loginData.Iv, true) + if err != nil { + fmt.Println(err) + } else { + log.MainLogger.Info(result.(string)) + } + var userInfo model.UserInfo + err = json.Unmarshal([]byte(result.(string)), &userInfo) + if err != nil { + return + } + // commit user info to db + if err = service.CommitLoginUserInfo( + userInfo.OpenId, + model.UsersDO{ + Nickname: userInfo.NickName, + Gender: userInfo.Gender, + City: userInfo.City, + AvatarUrl: userInfo.AvatarUrl}); err != nil { + c.JSON(500, gin.H{ + "errMsg": fmt.Sprintf("database exception: %v", err.Error()), + }) + return + } + c.JSON(200, gin.H{ + "userInfo": userInfo, + }) +} + +func Authorize() gin.HandlerFunc { + return func(c *gin.Context) { + token := c.Request.Header.Get("token") + _, err := service.TryGetUserSession("session-" + token) + if err != nil { + c.Abort() + c.JSON(http.StatusUnauthorized, gin.H{"err_msg": "permission denied! please login first"}) + return + } else { + c.Next() + } + } +} diff --git a/app/handler/monitor.go b/app/handler/monitor.go index f175cdcd23bbba125c49e5879e9c6a04ebeb8db0..d29f678eba3263ee3207510d686bc312ce93c161 100644 --- a/app/handler/monitor.go +++ b/app/handler/monitor.go @@ -2,15 +2,24 @@ package handler import ( "gitee.com/lanshiren/miniprogram/app/model" + "gitee.com/lanshiren/miniprogram/app/service" "github.com/gin-gonic/gin" ) func HandleMonitorMysqlQPS(c *gin.Context) { indicators := &model.Indicators{} + token := c.Request.Header.Get("token") + userData, err := service.TryGetUserSession("session-" + token) + if err != nil { + c.JSON(401, gin.H{ + "errMsg": "你的登录已过期, 请重新登录", + }) + return + } indicators.Points = append(indicators.Points, &model.Point{X: "18:00", Y: 234.5}) indicators.Points = append(indicators.Points, &model.Point{X: "18:00", Y: 214.5}) c.JSON(200, gin.H{ - "result": indicators, + "result": userData, }) } diff --git a/app/handler/users.go b/app/handler/users.go new file mode 100644 index 0000000000000000000000000000000000000000..c0a97ccf59cf641969a54e389244c5289d92656f --- /dev/null +++ b/app/handler/users.go @@ -0,0 +1,28 @@ +package handler + +import ( + "fmt" + "gitee.com/lanshiren/miniprogram/app/log" + "gitee.com/lanshiren/miniprogram/app/model" + "gitee.com/lanshiren/miniprogram/app/service" + "github.com/gin-gonic/gin" +) + +func HandleSearchContacts(c *gin.Context) { + var param model.SearchUsersParam + if err := c.BindJSON(¶m); err != nil { + log.MainLogger.Error(fmt.Sprintf("bind client json to SearchUsersParam err: %v", err)) + } + total, contacts, err := service.SearchContacts(param) + if err != nil { + log.MainLogger.Error(fmt.Sprintf("SearchContacts err: %v", err)) + c.JSON(500, gin.H{ + "errMsg": fmt.Sprintf("database exception: %v", err.Error()), + }) + return + } + c.JSON(200, gin.H{ + "total": total, + "results": contacts, + }) +} diff --git a/app/main.go b/app/main.go index 51564c41438664bdafad0147f5d74070c26dc06c..ffcc6cb055e41c3ee7bcec6f7ba5dea84c5a65f4 100644 --- a/app/main.go +++ b/app/main.go @@ -5,14 +5,12 @@ import ( config "gitee.com/lanshiren/miniprogram/app/conf" "gitee.com/lanshiren/miniprogram/app/handler" "gitee.com/lanshiren/miniprogram/app/log" - "gitee.com/lanshiren/miniprogram/app/service" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" "os" "os/signal" "syscall" - "time" ) var banner = ` @@ -45,29 +43,42 @@ func main() { db.DB().SetMaxOpenConns(100) db.LogMode(true) config.SetDB(db) - go func() { - for { - total := 0 - db.Raw("select 1").Row().Scan(&total) - time.Sleep(200 * time.Millisecond) - } - }() + //go func() { + //for { + //total := 0 + //db.Raw("select 1").Row().Scan(&total) + //time.Sleep(200 * time.Millisecond) + //} + //}() // scheduler - sch := service.NewScheduler() - sch.AddTask(1000, service.FetchStatusCycle) - sch.RunAllTasks() + //sch := service.NewScheduler() + //sch.AddTask(1000, service.FetchStatusCycle) + //sch.RunAllTasks() fmt.Print(banner) log.MainLogger.Info(fmt.Sprintf("app start, listen on %s", conf.Listen)) r := gin.Default() + + r.POST("/api/v1/wx_login", handler.HandleAuthCode) + + r.Use(handler.Authorize()) + r.POST("/api/v1/auth_login", handler.AuthorizedLogin) r.GET("/api/v1/mysql_qps_monitor", handler.HandleMonitorMysqlQPS) r.GET("/api/v1/mysql_global_status", handler.HandleQueryMySQLGlobalStatus) + r.POST("/api/v1/create_activity", handler.HandleCreateActivity) + r.GET("/api/v1/get_activities", handler.HandleSearchActivities) + r.PUT("/api/v1/update_activity", handler.HandleUpdateActivity) + r.PUT("/api/v1/verify_activity", handler.HandleVerifyActivity) + r.POST("/api/v1/registration_activity", handler.HandleRegistrationActivity) + r.POST("/api/v1/cancel_registration_activity", handler.HandleCancelRegistrationActivity) + r.GET("/api/v1/get_contacts", handler.HandleSearchContacts) + fmt.Printf("%v", conf) go func () { <-c - sch.StopAllTasks() + //sch.StopAllTasks() os.Exit(1) }() diff --git a/app/model/activity_do.go b/app/model/activity_do.go new file mode 100644 index 0000000000000000000000000000000000000000..a10df2260af1ad99b03be635c7727152581af2f2 --- /dev/null +++ b/app/model/activity_do.go @@ -0,0 +1,156 @@ +package model + +import ( + "fmt" + config "gitee.com/lanshiren/miniprogram/app/conf" + "strings" + "time" +) + +const ( + UnderReview = iota + Registering + RegistrationEdit + RegistrationOver + EndActivity + Unknown +) + +type ActivityDO struct { + ID uint `gorm:"primary_key"` + UpdatedAt time.Time + CreatedAt time.Time + Title string + StartPlace string + EndPlace string + StartTime time.Time + RegistrationDeadline time.Time + CollectionPlace string + LeaderID int + LeaderSay string + RouteDesc string + RoutePics string + Attention string + CurCount int + CountLimit int + RouteId int + QrCode string + Status int +} + +type VerifyActivityParam struct { + ID uint `json:"id"` + IsApprove bool `json:"is_approve"` +} + +type UpdateActivityParam struct { + ID uint `json:"id"` + IsCancel bool `json:"is_cancel"` + Activity ActivityVO `json:"activity"` +} + +type ActivityVO struct { + ID uint `json:"id"` + Title string `json:"title"` + StartPlace string `json:"start_place"` + EndPlace string `json:"end_place"` + StartTime string `json:"start_time"` + RegistrationDeadline string `json:"registration_deadline"` + CollectionPlace string `json:"collection_place"` + LeaderID int `json:"leader_id"` + LeaderSay string `json:"leader_say"` + RouteDesc string `json:"route_desc"` + RoutePics string `json:"route_pics"` + Attention string `json:"attention"` + CurCount int `json:"cur_count"` + CountLimit int `json:"count_limit"` + QrCode string `json:"qr_code"` + Status int `json:"status"` +} + +type SearchActivitiesParam struct { + Page int `json:"page"` + PageSize int `json:"pagesize"` + ID int `json:"id"` + Title string `json:"title"` + StartTime string `json:"start_time"` + RegistrationDeadline string `json:"registration_deadline"` + CollectionPlace string `json:"collection_place"` + LeaderName string `json:"leader_name"` +} + +func (ActivityDO) TableName() string { + return "activities" +} + +func CreateActivity(ac *ActivityDO) error { + db := config.GetDB() + return db.Save(ac).Error +} + +func SearchActivityList(param SearchActivitiesParam) (total int, activities []*ActivityDO, err error) { + db := config.GetDB() + offset := param.Page * param.PageSize + limit := param.PageSize + + clauseKeys := []string{} + clauseValues := []interface{}{} + + if param.ID != 0 { + clauseKeys = append(clauseKeys, "activities.id LIKE ?") + clauseValues = append(clauseValues, fmt.Sprintf(`%%%d%%`, param.ID)) + } + if len(param.Title) != 0 { + clauseKeys = append(clauseKeys, "activities.title LIKE ?") + clauseValues = append(clauseValues, fmt.Sprintf(`%%%s%%`, param.Title)) + } + if len(param.CollectionPlace) != 0 { + clauseKeys = append(clauseKeys, "activities.collection_place LIKE ?") + clauseValues = append(clauseValues, fmt.Sprintf(`%%%s%%`, param.CollectionPlace)) + } + if len(clauseKeys) > 0 { + err := db.Table("activities").Select("activities.*"). + Where(strings.Join(clauseKeys, " AND "), clauseValues...).Count(&total).Error + if err != nil { + return total, nil, err + } + err = db.Table("activities").Select("activities.*"). + Where(strings.Join(clauseKeys, " AND "), clauseValues...).Order("activities.id DESC"). + Limit(limit).Offset(offset).Scan(&activities).Error + } else { + err := db.Table("activities").Select("activities.*").Count(&total).Error + if err != nil { + return total, nil, err + } + err = db.Table("activities").Select("activities.*"). + Order("activities.id DESC"). + Limit(limit).Offset(offset).Scan(&activities).Error + } + return total, activities, nil +} + +func UpdateActivityDO(ac *ActivityDO) error { + db := config.GetDB() + activity := ActivityDO{} + err := db.Where("id = ?", ac.ID).First(&activity).Error + if err != nil { + return err + } + err = db.Model(&activity).Updates( + ActivityDO{ + Title: ac.Title, + LeaderSay: ac.LeaderSay, + RouteDesc: ac.RouteDesc, + RoutePics: ac.RoutePics, + StartTime: ac.StartTime, + CollectionPlace: ac.CollectionPlace, + QrCode: ac.QrCode, + RegistrationDeadline: ac.RegistrationDeadline, + CountLimit: ac.CountLimit, + Status: ac.Status, + }).Error + if err != nil { + return err + } + return nil +} diff --git a/app/model/auth.go b/app/model/auth.go new file mode 100644 index 0000000000000000000000000000000000000000..51079aa1eb6331245cf8987a1d86bdc22d8030bf --- /dev/null +++ b/app/model/auth.go @@ -0,0 +1,28 @@ +package model + + +type AuthData struct { + Code string `json:"code"` +} + +type LoginData struct { + EncryptedData string `json:"encryptedData"` + Iv string `json:"iv"` +} + +type Watermark struct { + Timestamp uint64 `json:"timestamp"` + Appid string `json:"appid"` +} + +type UserInfo struct { + OpenId string `json:"openId"` + NickName string `json:"nickName"` + Gender uint `json:"gender"` + Language string `json:"language"` + City string `json:"city"` + Province string `json:"province"` + Country string `json:"country"` + AvatarUrl string `json:"avatarUrl"` + Watermark Watermark `json:"watermark"` +} diff --git a/app/model/registration_do.go b/app/model/registration_do.go new file mode 100644 index 0000000000000000000000000000000000000000..3363fc645e9fe33a5b4367c870a052ed198aba1c --- /dev/null +++ b/app/model/registration_do.go @@ -0,0 +1,56 @@ +package model + +import ( + config "gitee.com/lanshiren/miniprogram/app/conf" + "time" +) + +const ( + RegistrationDone = iota + RegistrationCancel +) + +type RegistrationActivityParam struct { + ActivityID uint `json:"activity_id"` +} + +type RegistrationDO struct { + ID uint `gorm:"primary_key"` + UpdatedAt time.Time + CreatedAt time.Time + UserID uint + ActivityID uint + CancelTime time.Time + Status int +} + +func (RegistrationDO) TableName() string { + return "registration" +} + +func CreateRegistration(rs *RegistrationDO) error { + db := config.GetDB() + return db.Save(rs).Error +} + +func UpdateRegistrationDO(rs *RegistrationDO) error { + db := config.GetDB() + registrations := []*RegistrationDO{} + err := db.Table("registration").Where("user_id = ? AND activity_id = ?", rs.UserID, rs.ActivityID). + Scan(®istrations).Error + if err != nil { + return err + } + for _, registration := range registrations { + err = db.Table("registration").Model(®istration).Updates( + RegistrationDO{ + ID: registration.ID, + Status: rs.Status, + CancelTime: time.Now(), + }).Error + if err != nil { + return err + } + } + return nil +} diff --git a/app/model/users_do.go b/app/model/users_do.go new file mode 100644 index 0000000000000000000000000000000000000000..4b0de989109477a8b003b9bc7f6fbf103981496f --- /dev/null +++ b/app/model/users_do.go @@ -0,0 +1,138 @@ +package model + +import ( + "fmt" + config "gitee.com/lanshiren/miniprogram/app/conf" + "strings" + "time" +) + +type UsersDO struct { + ID uint `gorm:"primary_key"` + UpdatedAt time.Time + CreatedAt time.Time + Name string + Nickname string + Gender uint + City string + AvatarUrl string + Campus string + Phone string + Major string + WeId string + OpenId string + RoleId int + BanStatus int + LastBanPoint time.Time + BanTime int +} + +type UserVO struct { + ID uint `json:"id"` + Name string `json:"name"` + Nickname string `json:"nickname"` + Gender uint `json:"gender"` + City string `json:"city"` + AvatarUrl string `json:"avatar_url"` + Campus string `json:"campus"` + Phone string `json:"phone"` + RoleId int `json:"role_id"` +} + +type SearchUsersParam struct { + Page int `json:"page"` + PageSize int `json:"page_size"` + Name string `json:"name"` + Phone string `json:"phone"` +} + +func (UsersDO) TableName() string { + return "users" +} + +func CreateUser(user *UsersDO) error { + db := config.GetDB() + u := UsersDO{} + db.Where("open_id = ?", user.OpenId).First(&u) + if u.OpenId != "" { + return nil + } + if err := db.Save(user).Error; err != nil { + return err + } + return nil +} + +func QueryAndUpdateUser(openId string, user *UsersDO) error { + db := config.GetDB() + u := UsersDO{} + err := db.Where("open_id = ?", openId).First(&u).Error + if err != nil { + return err + } else { + if u.Name != "" && user.Name == "" { + user.Name = u.Name + } + if u.ID != 0 { + err := db.Model(&u).Updates( + map[string]interface{}{ + "nickname": user.Nickname, + "name": user.Name, + "gender": user.Gender, + "avatar_url": user.AvatarUrl, + "city": user.City}).Error + if err != nil { + return err + } + } + } + return err +} + +func QueryUser(openId string) (UsersDO, error) { + db := config.GetDB() + u := UsersDO{} + err := db.Where("open_id = ?", openId).First(&u).Error + if err != nil { + return UsersDO{}, err + } + return u, nil +} + +func SearchContactsList(param SearchUsersParam) (total int, contacts []*UsersDO, err error) { + db := config.GetDB() + offset := param.Page * param.PageSize + limit := param.PageSize + + clauseKeys := []string{} + clauseValues := []interface{}{} + + if len(param.Name) != 0 { + clauseKeys = append(clauseKeys, "users.name LIKE ?") + clauseValues = append(clauseValues, fmt.Sprintf(`%%%s%%`, param.Name)) + } + if len(param.Phone) != 0 { + clauseKeys = append(clauseKeys, "users.phone LIKE ?") + clauseValues = append(clauseValues, fmt.Sprintf(`%%%s%%`, param.Phone)) + } + + if len(clauseKeys) > 0 { + err := db.Table("users").Select("users.*").Where( + strings.Join(clauseKeys, " AND "), clauseValues...).Count(&total).Error + if err != nil { + return total, nil, err + } + err = db.Table("users").Select("users.*"). + Where(strings.Join(clauseKeys, " AND "), clauseValues...).Order("users.id DESC"). + Limit(limit).Offset(offset).Scan(&contacts).Error + } else { + err := db.Table("users").Select("users.*").Count(&total).Error + if err != nil { + return total, nil, err + } + err = db.Table("users").Select("users.*"). + Order("users.id DESC"). + Limit(limit).Offset(offset).Scan(&contacts).Error + } + return total, contacts, nil +} diff --git a/app/service/activity.go b/app/service/activity.go new file mode 100644 index 0000000000000000000000000000000000000000..295ce40408c02cfb54bc397d27f9057f709ebe84 --- /dev/null +++ b/app/service/activity.go @@ -0,0 +1,117 @@ +package service + +import ( + "gitee.com/lanshiren/miniprogram/app/model" + "time" +) + +func SaveActivity(vo model.ActivityVO) error { + startTime, err := RFC3339StringToTime(vo.StartTime) + if err != nil { + return err + } + deadline, err := RFC3339StringToTime(vo.RegistrationDeadline) + if err != nil { + return err + } + if err := model.CreateActivity(&model.ActivityDO{ + Title: vo.Title, + LeaderSay: vo.LeaderSay, + RouteDesc: vo.RouteDesc, + RoutePics: vo.RoutePics, + CollectionPlace: vo.CollectionPlace, + QrCode: vo.QrCode, + StartTime: *startTime, + RegistrationDeadline: *deadline, + CountLimit: vo.CountLimit, + }); err != nil { + return err + } + return nil +} + +func SearchActivities(param model.SearchActivitiesParam) (int, []*model.ActivityVO, error) { + var activities []*model.ActivityVO + total, acDos, err := model.SearchActivityList(param) + if err != nil { + return 0, nil, err + } + for _, ac := range acDos { + activities = append(activities, &model.ActivityVO{ + ID: ac.ID, + Title: ac.Title, + LeaderSay: ac.LeaderSay, + RouteDesc: ac.RouteDesc, + RoutePics: ac.RoutePics, + StartTime: ac.StartTime.String(), + CollectionPlace: ac.CollectionPlace, + QrCode: ac.QrCode, + RegistrationDeadline: ac.RegistrationDeadline.String(), + CountLimit: ac.CountLimit, + Status: ac.Status, + }) + } + return total, activities, nil +} + +func UpdateActivity(param model.UpdateActivityParam) error { + status := model.Unknown + if param.IsCancel { + status = model.EndActivity + } + startTime, err := RFC3339StringToTime(param.Activity.StartTime) + if err != nil { + return err + } + deadline, err := RFC3339StringToTime(param.Activity.RegistrationDeadline) + if err != nil { + return err + } + if status != model.EndActivity { + return model.UpdateActivityDO(&model.ActivityDO{ + ID: param.ID, + Status: param.Activity.Status, + Title: param.Activity.Title, + LeaderSay: param.Activity.LeaderSay, + RouteDesc: param.Activity.RouteDesc, + RoutePics: param.Activity.RoutePics, + StartTime: *startTime, + CollectionPlace: param.Activity.CollectionPlace, + QrCode: param.Activity.QrCode, + RegistrationDeadline: *deadline, + CountLimit: param.Activity.CountLimit, + }) + } else { + return model.UpdateActivityDO(&model.ActivityDO{ + ID: param.ID, + Status: status, + }) + } +} + +func VerifyActivity(id uint, isApprove bool) error { + if isApprove { + return model.UpdateActivityDO(&model.ActivityDO{ + ID: id, + Status: model.Registering, + }) + } + return nil +} + +func RegistrationActivity(userId uint, acId uint) error { + return model.CreateRegistration(&model.RegistrationDO{ + UserID: userId, + ActivityID: acId, + CancelTime: time.Now(), + }) +} + +func CancelRegistrationActivity(userId uint, acId uint) error { + return model.UpdateRegistrationDO( + &model.RegistrationDO{ + UserID: userId, + ActivityID: acId, + Status: model.RegistrationCancel, + }) +} diff --git a/app/service/auth.go b/app/service/auth.go new file mode 100644 index 0000000000000000000000000000000000000000..9cd55e62e9cca4f5de346762f96d3442dbb6070a --- /dev/null +++ b/app/service/auth.go @@ -0,0 +1,78 @@ +package service + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + config "gitee.com/lanshiren/miniprogram/app/conf" + "github.com/gomodule/redigo/redis" + "io/ioutil" + "net/http" + "net/url" +) + +type AuthResult struct { + Openid string `json:"openid"` + SessionKey string `json:"session_key"` + Unionid string `json:"unionid"` + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` +} + +// 小程序登录凭证校验 https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html +// code: 小程序端通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器 +// AuthResult: 认证的结果 +func WxJsCode2Session(code string) (AuthResult, error) { + // openid + params := url.Values{} + Url, err := url.Parse("https://api.weixin.qq.com/sns/jscode2session") + if err != nil { + return AuthResult{}, err + } + params.Set("appid", config.Conf.AppID) + params.Set("secret", config.Conf.Secret) + params.Set("js_code", code) + params.Set("grant_type", "authorization_code") + Url.RawQuery = params.Encode() + urlPath := Url.String() + resp, err := http.Get(urlPath) + if resp == nil { + return AuthResult{}, err + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return AuthResult{}, err + } + defer resp.Body.Close() + var res AuthResult + if err := json.Unmarshal(body, &res); err != nil { + return AuthResult{}, err + } + return res, err +} + +func GenToken(authStr string) (token string) { + h := sha256.New() + h.Write([]byte(authStr)) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +func SaveSession(ttl uint64, token, userData string) error { + conn := Pool.Get() + defer conn.Close() + _, err := conn.Do("SETEX", token, ttl, userData) + if err != nil { + return err + } + return nil +} + +func TryGetUserSession(token string) (string, error) { + conn := Pool.Get() + defer conn.Close() + userData, err := redis.String(conn.Do("GET", token)) + if err != nil { + return "", err + } + return userData, nil +} diff --git a/app/service/auth_test.go b/app/service/auth_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bf47ff408fdc1d67f8d5cfc56aeb1699b54d49fe --- /dev/null +++ b/app/service/auth_test.go @@ -0,0 +1,26 @@ +package service + +import ( + "fmt" + "github.com/xlstudio/wxbizdatacrypt" + "testing" +) + +func TestGenToken(t *testing.T) { + t.Logf("%s", GenToken("lanshiren")) + t.Logf("%s", GenRandString(32)) +} + +func TestDecodeUserRawData(t *testing.T) { + appId := "wx10d1df89cb0303cb" + sessionKey := "hkGJbsVU70xIQHeUIA+NZg==" + encryptedData := "z6mIBHGpz2bFdEXhH05L4aE7lqWF4GziLKyjT9DIl9uEskrWreFugZ+jceC0k0Ro7y2PcEkZjxVTHD+YTfM5MB3NxpUPlLYsV4eW0ggMQedEM4sJkH+eLl9LOgmMCZ+dhyV16G6gE3xn/XYIA435TZAsDV+AHDaquh8IP8crGEXZko333PCQeYxGkxkY7Kn6X4JO0wSThrQinr6y8nSUw4d0lD2PKKn7Hd6o3tXSA+tmEt8OqHG9Gjz+15pgAkJvd3wxkiRg9shns9bzo3pgP5SAzXYsRy4mbfzSI5CjN7pnqId3EQISw1hP0rFp8H+UxYbN+E3WGDcdHLkA1g42emqkgE3bQI/oaNGAExHX3/Qs8iAOlomQvG70OniznGxt02fQTcRgb6Szc+kJFtvme0EYU4KCRhkq/5Pc79HXOmtjDgx64kfRSXqcx88ha7gjR/BpfMlAH3oXigfmONKncmhVJQvO/6najNZ4N6Y/a7A=" + iv := "7qqfShnOX9XzCTFbIz8zfw==" + pc := wxbizdatacrypt.WxBizDataCrypt{AppId: appId, SessionKey: sessionKey} + result, err := pc.Decrypt(encryptedData, iv, true) //第三个参数解释: 需要返回 JSON 数据类型时 使用 true, 需要返回 map 数据类型时 使用 false + if err != nil { + fmt.Println(err) + } else { + fmt.Println(result) + } +} diff --git a/app/service/rand.go b/app/service/rand.go new file mode 100644 index 0000000000000000000000000000000000000000..aa904100e67a9ee99f86046e5c03463918cfb710 --- /dev/null +++ b/app/service/rand.go @@ -0,0 +1,24 @@ +package service + +import ( + "math/rand" + "time" +) + +const charset = "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +var seededRand *rand.Rand = rand.New( + rand.NewSource(time.Now().UnixNano())) + +func StringWithCharset(length int, charset string) string { + b := make([]byte, length) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + return string(b) +} + +func GenRandString(length int) string { + return StringWithCharset(length, charset) +} diff --git a/app/service/redis_util.go b/app/service/redis_util.go new file mode 100644 index 0000000000000000000000000000000000000000..ccc2346534afbff9e6bcf7cbb706a04de016fc6a --- /dev/null +++ b/app/service/redis_util.go @@ -0,0 +1,65 @@ +package service + +import ( + config "gitee.com/lanshiren/miniprogram/app/conf" + "github.com/gomodule/redigo/redis" + "os" + "os/signal" + "syscall" + "time" +) + + +var ( + Pool *redis.Pool +) + +func init() { + redisHost := "" + if config.Conf == nil { // for test + redisHost = ":6379" + } else { + redisHost := config.Conf.RedisURL + if redisHost == "" { + redisHost = ":6379" + } + } + Pool = newPool(redisHost) + cleanupHook() +} + +func newPool(server string) *redis.Pool { + + return &redis.Pool{ + + MaxIdle: 3, + IdleTimeout: 240 * time.Second, + + Dial: func() (redis.Conn, error) { + c, err := redis.Dial("tcp", server) + if err != nil { + return nil, err + } + return c, err + }, + + TestOnBorrow: func(c redis.Conn, t time.Time) error { + _, err := c.Do("PING") + return err + }, + } +} + +func cleanupHook() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + signal.Notify(c, syscall.SIGTERM) + signal.Notify(c, syscall.SIGKILL) + go func() { + <-c + Pool.Close() + os.Exit(0) + }() +} + + diff --git a/app/service/redis_util_test.go b/app/service/redis_util_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e8540dd1822a69f6c6df547f33f5d7d48d560138 --- /dev/null +++ b/app/service/redis_util_test.go @@ -0,0 +1,14 @@ +package service + +import ( + "testing" +) + +func TestRedisUtil(t *testing.T) { + conn := Pool.Get() + defer conn.Close() + _, err := conn.Do("SETEX", "token-dew", 3600, "abcd") + if err != nil { + t.Logf("%v", err.Error()) + } +} diff --git a/app/service/users.go b/app/service/users.go new file mode 100644 index 0000000000000000000000000000000000000000..6f2489e7a3edb34ca2b3e58a8be0508ef2ad5fd5 --- /dev/null +++ b/app/service/users.go @@ -0,0 +1,40 @@ +package service + +import ( + "gitee.com/lanshiren/miniprogram/app/model" + "time" +) + +func SaveUserOpenID(openId string) error { + return model.CreateUser(&model.UsersDO{OpenId: openId, LastBanPoint: time.Now()}) +} + +func CommitLoginUserInfo(openId string, user model.UsersDO) error { + return model.QueryAndUpdateUser(openId, &user) +} + +func GetUserByOpenID(id string) (model.UsersDO, error) { + return model.QueryUser(id) +} + +func SearchContacts(param model.SearchUsersParam) (int, []*model.UserVO, error) { + var contacts []*model.UserVO + total, userDos, err := model.SearchContactsList(param) + if err != nil { + return 0, nil, err + } + for _, user := range userDos { + contacts = append(contacts, &model.UserVO{ + ID: user.ID, + Name: user.Name, + Nickname: user.Nickname, + Gender: user.Gender, + City: user.City, + AvatarUrl: user.AvatarUrl, + Campus: user.Campus, + Phone: user.Phone, + RoleId: user.RoleId, + }) + } + return total, contacts, nil +} \ No newline at end of file diff --git a/app/service/utils.go b/app/service/utils.go new file mode 100644 index 0000000000000000000000000000000000000000..1aba176ff632800082f29716419f1d88fb129978 --- /dev/null +++ b/app/service/utils.go @@ -0,0 +1,11 @@ +package service + +import "time" + +func RFC3339StringToTime(str string) (t *time.Time, err error) { + uTime, err := time.Parse(time.RFC3339, str) + if err != nil { + return nil, err + } + return &uTime, nil +} diff --git a/data/design.txt b/data/design.txt new file mode 100644 index 0000000000000000000000000000000000000000..50767852330ddea0346ef5dbfd892c4c903951c7 --- /dev/null +++ b/data/design.txt @@ -0,0 +1,5 @@ +1.根据需求分析,可以抽象出系统中有用户、活动、权限、轨迹对象。 + +2.用户与活动对象进行交互,并依赖于权限对象做校验。 + +3.活动与报名用户之间的关系需要一张表维护。 diff --git a/data/lan_mini.sql b/data/lan_mini.sql new file mode 100644 index 0000000000000000000000000000000000000000..a718b79bd1f9c86722760501a7f1d26ed9cd9fa1 --- /dev/null +++ b/data/lan_mini.sql @@ -0,0 +1,136 @@ +-- --------------------------------------- +-- Database structure for lan_mini_db; +-- @author LiuJ +-- @date Sun Dec 13 19:59:56 CST 2020 +-- @version v0.0.1 +-- @备注: 不设置外键约束是为了以后数据库横向扩展 +-- --------------------------------------- + +-- ------------------------------- +-- Table structure for users +-- ------------------------------- +DROP TABLE IF EXISTS `users`; +CREATE TABLE `users` ( + `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT 'auto incr id', + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '用户姓名', + `nickname` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '昵称', + `gender` INT(8) NOT NULL DEFAULT 0 COMMENT '性别 0-女, 1-男', + `city` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '城市', + `avatar_url` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '用户头像地址', + `campus` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '所在校区', + `phone` VARCHAR(24) UNIQUE NOT NULL DEFAULT '' COMMENT '用户手机号', + `major` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '专业 (从事行业)', + `we_id` VARCHAR(256) UNIQUE NOT NULL DEFAULT '' COMMENT '微信ID (微信号唯一标识)', + `open_id` VARCHAR(256) UNIQUE NOT NULL DEFAULT '' COMMENT '微信唯一 ID', + `role_id` INT(11) NOT NULL DEFAULT 0 COMMENT '角色 ID', + `ban_status` INT(11) NOT NULL DEFAULT 0 COMMENT '小黑屋状态 (0-正常, 1-被拉黑)', + `last_ban_point` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上次被拉黑的时间点', + `ban_time` INT(8) NOT NULL DEFAULT 0 COMMENT '被拉黑的时间长(单位:天)', + PRIMARY KEY (`id`), + KEY `idx_name` (`name`), + KEY `idx_phone` (`phone`), + KEY `idx_ban_status` (`ban_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED; + +-- ------------------------------- +-- Table structure for activities +-- ------------------------------- +DROP TABLE IF EXISTS `activities`; +CREATE TABLE `activities` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `title` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '路线标题', + `start_place` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '路线起点', + `end_place` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '路线终点', + `start_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '集合时间', + `registration_deadline` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '报名截止时间', + `collection_place` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '集合地点', + `leader_id` INT(11) NOT NULL DEFAULT 0 COMMENT '活动领队 id', + `leader_say` VARCHAR(2048) NOT NULL DEFAULT '' COMMENT '领队的话', + `route_desc` VARCHAR(2048) NOT NULL DEFAULT '' COMMENT '路线描述', + `route_pics` VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '路线图片路径,如有多个,用逗号分割线', + `attention` VARCHAR(2048) NOT NULL DEFAULT '' COMMENT '注意事项', + `cur_count` INT(11) NOT NULL DEFAULT 0 COMMENT '记录当前报名人数', + `count_limit` INT(11) NOT NULL DEFAULT 0 COMMENT '人数限制', + `route_id` INT(11) NOT NULL DEFAULT 0 COMMENT '路线id', + `qr_code` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '群二维码', + `status` INT(11) NOT NULL DEFAULT 0 COMMENT '活动状态 (0-审核中, 1-报名中, 2-报名编辑, 3-报名结束, 4-活动结束)', + PRIMARY KEY (`id`), + KEY `idx_leader_id` (`leader_id`), + KEY `idx_start_place` (`start_place`), + KEY `idx_title` (`title`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED; + +-- ------------------------------- +-- Table structure for role +-- ------------------------------- +DROP TABLE IF EXISTS `role`; +CREATE TABLE `role` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `name` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '角色名称', + `p_join_activities` INT(8) NOT NULL DEFAULT 0 COMMENT '0-无权限, 1-有权限', + `p_create_activities` INT(8) NOT NULL DEFAULT 0 COMMENT '0-无权限, 1-有权限', + `p_manage_user` INT(8) NOT NULL DEFAULT 0 COMMENT '0-无权限, 1-有权限', + `p_manage_leader` INT(8) NOT NULL DEFAULT 0 COMMENT '0-无权限, 1-有权限', + `p_manage_editor` INT(8) NOT NULL DEFAULT 0 COMMENT '0-无权限, 1-有权限', + `p_ban_user` INT(8) NOT NULL DEFAULT 0 COMMENT '0-无权限, 1-有权限', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED; + +-- ------------------------------- +-- Table structure for registration +-- ------------------------------- +DROP TABLE IF EXISTS `registration`; +CREATE TABLE `registration` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `user_id` INT(11) NOT NULL DEFAULT 0 COMMENT '报名人id', + `activity_id` INT(11) NOT NULL DEFAULT 0 COMMENT '活动id', + `cancel_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '取消报名时间点', + `status` INT(8) NOT NULL DEFAULT 0 COMMENT '0-已报名, 1-取消报名', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_activity_id` (`activity_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED; + +-- ------------------------------- +-- Table structure for routes +-- ------------------------------- +DROP TABLE IF EXISTS `routes`; +CREATE TABLE `routes` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `pic` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '轨迹封面图片url', + `start_point` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '轨迹起点', + `end_point` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '轨迹终点', + `cumulative_climb` INT(11) NOT NULL DEFAULT 0 COMMENT '累计爬升高度,单位m', + `total_mileage` FLOAT(4, 2) NOT NULL DEFAULT 0.00 COMMENT '里程数', + `landscape_index` INT(8) NOT NULL DEFAULT 0 COMMENT '风景指数', + `leisure_index` INT(8) NOT NULL DEFAULT 0 COMMENT '休闲指数', + `gpx_url` VARCHAR(256) NOT NULL DEFAULT '' COMMENT 'gpx格式文件', + PRIMARY KEY (`id`), + KEY `idx_start_point` (`start_point`), + KEY `idx_end_point` (`end_point`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED; + +-- ------------------------------- +-- Table structure for blacklist +-- ------------------------------- +DROP TABLE IF EXISTS `blacklist`; +DROP TABLE IF EXISTS `blacklist`; +CREATE TABLE `blacklist` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `user_id` INT(11) NOT NULL DEFAULT 0 COMMENT '报名人id', + `activity_id` INT(11) NOT NULL DEFAULT 0 COMMENT '活动id', + `reason` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '拉黑原因', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED; diff --git a/go.mod b/go.mod index 509a0b07b8ac40d79f0cb600f7a2099a32fb1bc1..0612aece12260ba8bdf1ef0d213dcb2e899492fc 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ go 1.13 require ( github.com/BurntSushi/toml v0.3.1 github.com/gin-gonic/gin v1.6.3 + github.com/gomodule/redigo v1.8.3 github.com/jinzhu/gorm v1.9.16 + github.com/xlstudio/wxbizdatacrypt v1.0.2 go.uber.org/atomic v1.6.0 go.uber.org/zap v1.16.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 diff --git a/go.sum b/go.sum index 45596daa9828c25918df24920b7b9fe536e9d6ab..78fd03d7b6ffec1835794d2b0fe689efe978867f 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,9 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/gomodule/redigo v1.8.3 h1:HR0kYDX2RJZvAup8CsiJwxB4dTCSC0AaUq6S4SiLwUc= +github.com/gomodule/redigo v1.8.3/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/gomodule/redigo/redis v0.0.0-do-not-use h1:J7XIp6Kau0WoyT4JtXHT3Ei0gA1KkSc6bc87j9v9WIo= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= @@ -51,10 +54,13 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/xlstudio/wxbizdatacrypt v1.0.2 h1:EEh38tPrntWhd8bArCqQWEMiysuPhssPTO1vqEosY78= +github.com/xlstudio/wxbizdatacrypt v1.0.2/go.mod h1:5P4Yi0JRnlrs3CSmMZXzctar3VsxedGfZrf8fZbVPe8= go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= diff --git a/static/Base-X/pages/index/index.js b/static/Base-X/pages/index/index.js index 608a282e726b485731e9b38b19b2ea4c00b9dc93..521e74241782e1464f2386b15856c7b26d0ea397 100644 --- a/static/Base-X/pages/index/index.js +++ b/static/Base-X/pages/index/index.js @@ -6,6 +6,10 @@ Page({ data: { motto: 'Hello World', userInfo: {}, + tiptext: '尚未登录', + apiRes: 'api 返回结果', + apiToken: '', + avatarUrl: '', hasUserInfo: false, canIUse: wx.canIUse('button.open-type.getUserInfo') }, @@ -16,32 +20,65 @@ Page({ }) }, onLoad: function () { - if (app.globalData.userInfo) { - this.setData({ - userInfo: app.globalData.userInfo, - hasUserInfo: true - }) - } else if (this.data.canIUse){ - // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 - // 所以此处加入 callback 以防止这种情况 - app.userInfoReadyCallback = res => { - this.setData({ - userInfo: res.userInfo, - hasUserInfo: true + // /api/v1/wxLogin + let this_ = this + wx.login({ + success:function(res){ + //获取登录的临时凭证 + var code_ = res.code; + //调用后端,获取微信的session_key,secret + wx.request({ + url: "http://192.168.0.104:9999/api/v1/wxLogin", + method: "POST", + data: { + code: code_, + }, + success: function(result){ + console.log(result.data) + if(result.statusCode === 200) { + this_.setData({ + apiToken: result.data.token, + }) + } + } + }) + } + }) + }, + getPhoneNumber: function(e) { + console.log(e.detail.errMsg) + console.log(e.detail.iv) + console.log(e.detail.encryptedData) + }, + doLogin: function(e) { + let this_ = this + // console.log(e) + wx.login({ + success:function(res){ + //获取登录的临时凭证 + var code_ = res.code; + //调用后端,获取微信的session_key,secret + wx.request({ + url: "http://192.168.0.104:9999/api/v1/authLogin", + method: "POST", + header: { + token: this_.data.apiToken, + }, + data: { + encryptedData: e.detail.encryptedData, + iv: e.detail.iv,code_, + }, + success: function(result){ + if(result.statusCode === 200) { + console.log(result) + this_.setData({ + userInfo: result.data.userInfo, + }) + } + } }) } - } else { - // 在没有 open-type=getUserInfo 版本的兼容处理 - wx.getUserInfo({ - success: res => { - app.globalData.userInfo = res.userInfo - this.setData({ - userInfo: res.userInfo, - hasUserInfo: true - }) - } - }) - } + }) }, getUserInfo: function(e) { console.log(e) @@ -50,5 +87,43 @@ Page({ userInfo: e.detail.userInfo, hasUserInfo: true }) + }, + getData: function(e) { + console.log("request api") + let this_ = this + wx.request({ + url: "http://192.168.0.104:9999/api/v1/mysql_qps_monitor", + method: "GET", + header: { + "token": this.data.apiToken + }, + data: { + }, + success: function(result){ + console.log(result.statusCode) + if(result.statusCode === 401) { + this_.setData({ + apiRes: result.data.errMsg, + }) + } else if(result.statusCode === 200) { + this_.setData({ + apiRes: "您的 OpenID: " + result.data.result, + }) + console.log(result.data.result) + } + }, + }) + }, + loginOut: function(e) { + this.setData({ + userInfo: null, + hasUserInfo: false, + avatarUrl: "", + tiptext: "", + apiToken: "", + }) + wx.clearStorage({ + success: (res) => {}, + }) } }) diff --git a/static/Base-X/pages/index/index.wxml b/static/Base-X/pages/index/index.wxml index 33d3e8d9f4563a3ba5c9f8406cdf23d89f0cbaf2..8a1a93b42ba9f4aa61f41257dd6af508f38cf328 100644 --- a/static/Base-X/pages/index/index.wxml +++ b/static/Base-X/pages/index/index.wxml @@ -1,11 +1,27 @@ - - {{userInfo.nickName}} - MySQL QPS +
+ + + + + + + + + + + + + + + {{apiRes}} + +
+
{{motto}} diff --git a/static/Base-X/pages/index/index.wxss b/static/Base-X/pages/index/index.wxss index ce30de019b216310ab4ef9ae5c3e6c954ee0a1b2..b97be7b13f8616c93e942089fc807130e55503fa 100644 --- a/static/Base-X/pages/index/index.wxss +++ b/static/Base-X/pages/index/index.wxss @@ -12,6 +12,11 @@ border-radius: 50%; } +.mytext { + width: 100px; + margin-right: 10px; +} + .userinfo-nickname { color: #aaa; } diff --git a/static/Base-X/project.config.json b/static/Base-X/project.config.json index 4a3e3fc43477fb22411b7a9e5de71bdceb2c98af..78746fbdd94cdc0102d3aa9f39c772fffd9b9425 100644 --- a/static/Base-X/project.config.json +++ b/static/Base-X/project.config.json @@ -4,7 +4,7 @@ "ignore": [] }, "setting": { - "urlCheck": true, + "urlCheck": false, "es6": true, "enhance": false, "postcss": true, diff --git a/vendor/github.com/gomodule/redigo/LICENSE b/vendor/github.com/gomodule/redigo/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f433b1a53f5b830a205fd2df78e2b34974656c7b --- /dev/null +++ b/vendor/github.com/gomodule/redigo/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/vendor/github.com/gomodule/redigo/redis/commandinfo.go b/vendor/github.com/gomodule/redigo/redis/commandinfo.go new file mode 100644 index 0000000000000000000000000000000000000000..b6df6a25aa3c802606065f146d8aaf85347d7349 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/commandinfo.go @@ -0,0 +1,55 @@ +// Copyright 2014 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "strings" +) + +const ( + connectionWatchState = 1 << iota + connectionMultiState + connectionSubscribeState + connectionMonitorState +) + +type commandInfo struct { + // Set or Clear these states on connection. + Set, Clear int +} + +var commandInfos = map[string]commandInfo{ + "WATCH": {Set: connectionWatchState}, + "UNWATCH": {Clear: connectionWatchState}, + "MULTI": {Set: connectionMultiState}, + "EXEC": {Clear: connectionWatchState | connectionMultiState}, + "DISCARD": {Clear: connectionWatchState | connectionMultiState}, + "PSUBSCRIBE": {Set: connectionSubscribeState}, + "SUBSCRIBE": {Set: connectionSubscribeState}, + "MONITOR": {Set: connectionMonitorState}, +} + +func init() { + for n, ci := range commandInfos { + commandInfos[strings.ToLower(n)] = ci + } +} + +func lookupCommandInfo(commandName string) commandInfo { + if ci, ok := commandInfos[commandName]; ok { + return ci + } + return commandInfos[strings.ToUpper(commandName)] +} diff --git a/vendor/github.com/gomodule/redigo/redis/conn.go b/vendor/github.com/gomodule/redigo/redis/conn.go new file mode 100644 index 0000000000000000000000000000000000000000..5d7841c67031257a6f3f5b8e462565c6bb6fd132 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/conn.go @@ -0,0 +1,767 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "bufio" + "bytes" + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "net" + "net/url" + "regexp" + "strconv" + "sync" + "time" +) + +var ( + _ ConnWithTimeout = (*conn)(nil) +) + +// conn is the low-level implementation of Conn +type conn struct { + // Shared + mu sync.Mutex + pending int + err error + conn net.Conn + + // Read + readTimeout time.Duration + br *bufio.Reader + + // Write + writeTimeout time.Duration + bw *bufio.Writer + + // Scratch space for formatting argument length. + // '*' or '$', length, "\r\n" + lenScratch [32]byte + + // Scratch space for formatting integers and floats. + numScratch [40]byte +} + +// DialTimeout acts like Dial but takes timeouts for establishing the +// connection to the server, writing a command and reading a reply. +// +// Deprecated: Use Dial with options instead. +func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) { + return Dial(network, address, + DialConnectTimeout(connectTimeout), + DialReadTimeout(readTimeout), + DialWriteTimeout(writeTimeout)) +} + +// DialOption specifies an option for dialing a Redis server. +type DialOption struct { + f func(*dialOptions) +} + +type dialOptions struct { + readTimeout time.Duration + writeTimeout time.Duration + tlsHandshakeTimeout time.Duration + dialer *net.Dialer + dialContext func(ctx context.Context, network, addr string) (net.Conn, error) + db int + username string + password string + clientName string + useTLS bool + skipVerify bool + tlsConfig *tls.Config +} + +// DialTLSHandshakeTimeout specifies the maximum amount of time waiting to +// wait for a TLS handshake. Zero means no timeout. +// If no DialTLSHandshakeTimeout option is specified then the default is 30 seconds. +func DialTLSHandshakeTimeout(d time.Duration) DialOption { + return DialOption{func(do *dialOptions) { + do.tlsHandshakeTimeout = d + }} +} + +// DialReadTimeout specifies the timeout for reading a single command reply. +func DialReadTimeout(d time.Duration) DialOption { + return DialOption{func(do *dialOptions) { + do.readTimeout = d + }} +} + +// DialWriteTimeout specifies the timeout for writing a single command. +func DialWriteTimeout(d time.Duration) DialOption { + return DialOption{func(do *dialOptions) { + do.writeTimeout = d + }} +} + +// DialConnectTimeout specifies the timeout for connecting to the Redis server when +// no DialNetDial option is specified. +// If no DialConnectTimeout option is specified then the default is 30 seconds. +func DialConnectTimeout(d time.Duration) DialOption { + return DialOption{func(do *dialOptions) { + do.dialer.Timeout = d + }} +} + +// DialKeepAlive specifies the keep-alive period for TCP connections to the Redis server +// when no DialNetDial option is specified. +// If zero, keep-alives are not enabled. If no DialKeepAlive option is specified then +// the default of 5 minutes is used to ensure that half-closed TCP sessions are detected. +func DialKeepAlive(d time.Duration) DialOption { + return DialOption{func(do *dialOptions) { + do.dialer.KeepAlive = d + }} +} + +// DialNetDial specifies a custom dial function for creating TCP +// connections, otherwise a net.Dialer customized via the other options is used. +// DialNetDial overrides DialConnectTimeout and DialKeepAlive. +func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption { + return DialOption{func(do *dialOptions) { + do.dialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + return dial(network, addr) + } + }} +} + +// DialContextFunc specifies a custom dial function with context for creating TCP +// connections, otherwise a net.Dialer customized via the other options is used. +// DialContextFunc overrides DialConnectTimeout and DialKeepAlive. +func DialContextFunc(f func(ctx context.Context, network, addr string) (net.Conn, error)) DialOption { + return DialOption{func(do *dialOptions) { + do.dialContext = f + }} +} + +// DialDatabase specifies the database to select when dialing a connection. +func DialDatabase(db int) DialOption { + return DialOption{func(do *dialOptions) { + do.db = db + }} +} + +// DialPassword specifies the password to use when connecting to +// the Redis server. +func DialPassword(password string) DialOption { + return DialOption{func(do *dialOptions) { + do.password = password + }} +} + +// DialUsername specifies the username to use when connecting to +// the Redis server when Redis ACLs are used. +func DialUsername(username string) DialOption { + return DialOption{func(do *dialOptions) { + do.username = username + }} +} + +// DialClientName specifies a client name to be used +// by the Redis server connection. +func DialClientName(name string) DialOption { + return DialOption{func(do *dialOptions) { + do.clientName = name + }} +} + +// DialTLSConfig specifies the config to use when a TLS connection is dialed. +// Has no effect when not dialing a TLS connection. +func DialTLSConfig(c *tls.Config) DialOption { + return DialOption{func(do *dialOptions) { + do.tlsConfig = c + }} +} + +// DialTLSSkipVerify disables server name verification when connecting over +// TLS. Has no effect when not dialing a TLS connection. +func DialTLSSkipVerify(skip bool) DialOption { + return DialOption{func(do *dialOptions) { + do.skipVerify = skip + }} +} + +// DialUseTLS specifies whether TLS should be used when connecting to the +// server. This option is ignore by DialURL. +func DialUseTLS(useTLS bool) DialOption { + return DialOption{func(do *dialOptions) { + do.useTLS = useTLS + }} +} + +// Dial connects to the Redis server at the given network and +// address using the specified options. +func Dial(network, address string, options ...DialOption) (Conn, error) { + return DialContext(context.Background(), network, address, options...) +} + +type tlsHandshakeTimeoutError struct{} + +func (tlsHandshakeTimeoutError) Timeout() bool { return true } +func (tlsHandshakeTimeoutError) Temporary() bool { return true } +func (tlsHandshakeTimeoutError) Error() string { return "TLS handshake timeout" } + +// DialContext connects to the Redis server at the given network and +// address using the specified options and context. +func DialContext(ctx context.Context, network, address string, options ...DialOption) (Conn, error) { + do := dialOptions{ + dialer: &net.Dialer{ + Timeout: time.Second * 30, + KeepAlive: time.Minute * 5, + }, + tlsHandshakeTimeout: time.Second * 10, + } + for _, option := range options { + option.f(&do) + } + if do.dialContext == nil { + do.dialContext = do.dialer.DialContext + } + + netConn, err := do.dialContext(ctx, network, address) + if err != nil { + return nil, err + } + + if do.useTLS { + var tlsConfig *tls.Config + if do.tlsConfig == nil { + tlsConfig = &tls.Config{InsecureSkipVerify: do.skipVerify} + } else { + tlsConfig = cloneTLSConfig(do.tlsConfig) + } + if tlsConfig.ServerName == "" { + host, _, err := net.SplitHostPort(address) + if err != nil { + netConn.Close() + return nil, err + } + tlsConfig.ServerName = host + } + + tlsConn := tls.Client(netConn, tlsConfig) + errc := make(chan error, 2) // buffered so we don't block timeout or Handshake + if d := do.tlsHandshakeTimeout; d != 0 { + timer := time.AfterFunc(d, func() { + errc <- tlsHandshakeTimeoutError{} + }) + defer timer.Stop() + } + go func() { + errc <- tlsConn.Handshake() + }() + if err := <-errc; err != nil { + // Timeout or Handshake error. + netConn.Close() // nolint: errcheck + return nil, err + } + + netConn = tlsConn + } + + c := &conn{ + conn: netConn, + bw: bufio.NewWriter(netConn), + br: bufio.NewReader(netConn), + readTimeout: do.readTimeout, + writeTimeout: do.writeTimeout, + } + + if do.password != "" { + authArgs := make([]interface{}, 0, 2) + if do.username != "" { + authArgs = append(authArgs, do.username) + } + authArgs = append(authArgs, do.password) + if _, err := c.Do("AUTH", authArgs...); err != nil { + netConn.Close() + return nil, err + } + } + + if do.clientName != "" { + if _, err := c.Do("CLIENT", "SETNAME", do.clientName); err != nil { + netConn.Close() + return nil, err + } + } + + if do.db != 0 { + if _, err := c.Do("SELECT", do.db); err != nil { + netConn.Close() + return nil, err + } + } + + return c, nil +} + +var pathDBRegexp = regexp.MustCompile(`/(\d*)\z`) + +// DialURL connects to a Redis server at the given URL using the Redis +// URI scheme. URLs should follow the draft IANA specification for the +// scheme (https://www.iana.org/assignments/uri-schemes/prov/redis). +func DialURL(rawurl string, options ...DialOption) (Conn, error) { + u, err := url.Parse(rawurl) + if err != nil { + return nil, err + } + + if u.Scheme != "redis" && u.Scheme != "rediss" { + return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme) + } + + if u.Opaque != "" { + return nil, fmt.Errorf("invalid redis URL, url is opaque: %s", rawurl) + } + + // As per the IANA draft spec, the host defaults to localhost and + // the port defaults to 6379. + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + // assume port is missing + host = u.Host + port = "6379" + } + if host == "" { + host = "localhost" + } + address := net.JoinHostPort(host, port) + + if u.User != nil { + password, isSet := u.User.Password() + if isSet { + options = append(options, DialUsername(u.User.Username()), DialPassword(password)) + } + } + + match := pathDBRegexp.FindStringSubmatch(u.Path) + if len(match) == 2 { + db := 0 + if len(match[1]) > 0 { + db, err = strconv.Atoi(match[1]) + if err != nil { + return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) + } + } + if db != 0 { + options = append(options, DialDatabase(db)) + } + } else if u.Path != "" { + return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) + } + + options = append(options, DialUseTLS(u.Scheme == "rediss")) + + return Dial("tcp", address, options...) +} + +// NewConn returns a new Redigo connection for the given net connection. +func NewConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) Conn { + return &conn{ + conn: netConn, + bw: bufio.NewWriter(netConn), + br: bufio.NewReader(netConn), + readTimeout: readTimeout, + writeTimeout: writeTimeout, + } +} + +func (c *conn) Close() error { + c.mu.Lock() + err := c.err + if c.err == nil { + c.err = errors.New("redigo: closed") + err = c.conn.Close() + } + c.mu.Unlock() + return err +} + +func (c *conn) fatal(err error) error { + c.mu.Lock() + if c.err == nil { + c.err = err + // Close connection to force errors on subsequent calls and to unblock + // other reader or writer. + c.conn.Close() + } + c.mu.Unlock() + return err +} + +func (c *conn) Err() error { + c.mu.Lock() + err := c.err + c.mu.Unlock() + return err +} + +func (c *conn) writeLen(prefix byte, n int) error { + c.lenScratch[len(c.lenScratch)-1] = '\n' + c.lenScratch[len(c.lenScratch)-2] = '\r' + i := len(c.lenScratch) - 3 + for { + c.lenScratch[i] = byte('0' + n%10) + i -= 1 + n = n / 10 + if n == 0 { + break + } + } + c.lenScratch[i] = prefix + _, err := c.bw.Write(c.lenScratch[i:]) + return err +} + +func (c *conn) writeString(s string) error { + c.writeLen('$', len(s)) + c.bw.WriteString(s) + _, err := c.bw.WriteString("\r\n") + return err +} + +func (c *conn) writeBytes(p []byte) error { + c.writeLen('$', len(p)) + c.bw.Write(p) + _, err := c.bw.WriteString("\r\n") + return err +} + +func (c *conn) writeInt64(n int64) error { + return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10)) +} + +func (c *conn) writeFloat64(n float64) error { + return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64)) +} + +func (c *conn) writeCommand(cmd string, args []interface{}) error { + c.writeLen('*', 1+len(args)) + if err := c.writeString(cmd); err != nil { + return err + } + for _, arg := range args { + if err := c.writeArg(arg, true); err != nil { + return err + } + } + return nil +} + +func (c *conn) writeArg(arg interface{}, argumentTypeOK bool) (err error) { + switch arg := arg.(type) { + case string: + return c.writeString(arg) + case []byte: + return c.writeBytes(arg) + case int: + return c.writeInt64(int64(arg)) + case int64: + return c.writeInt64(arg) + case float64: + return c.writeFloat64(arg) + case bool: + if arg { + return c.writeString("1") + } else { + return c.writeString("0") + } + case nil: + return c.writeString("") + case Argument: + if argumentTypeOK { + return c.writeArg(arg.RedisArg(), false) + } + // See comment in default clause below. + var buf bytes.Buffer + fmt.Fprint(&buf, arg) + return c.writeBytes(buf.Bytes()) + default: + // This default clause is intended to handle builtin numeric types. + // The function should return an error for other types, but this is not + // done for compatibility with previous versions of the package. + var buf bytes.Buffer + fmt.Fprint(&buf, arg) + return c.writeBytes(buf.Bytes()) + } +} + +type protocolError string + +func (pe protocolError) Error() string { + return fmt.Sprintf("redigo: %s (possible server error or unsupported concurrent read by application)", string(pe)) +} + +// readLine reads a line of input from the RESP stream. +func (c *conn) readLine() ([]byte, error) { + // To avoid allocations, attempt to read the line using ReadSlice. This + // call typically succeeds. The known case where the call fails is when + // reading the output from the MONITOR command. + p, err := c.br.ReadSlice('\n') + if err == bufio.ErrBufferFull { + // The line does not fit in the bufio.Reader's buffer. Fall back to + // allocating a buffer for the line. + buf := append([]byte{}, p...) + for err == bufio.ErrBufferFull { + p, err = c.br.ReadSlice('\n') + buf = append(buf, p...) + } + p = buf + } + if err != nil { + return nil, err + } + i := len(p) - 2 + if i < 0 || p[i] != '\r' { + return nil, protocolError("bad response line terminator") + } + return p[:i], nil +} + +// parseLen parses bulk string and array lengths. +func parseLen(p []byte) (int, error) { + if len(p) == 0 { + return -1, protocolError("malformed length") + } + + if p[0] == '-' && len(p) == 2 && p[1] == '1' { + // handle $-1 and $-1 null replies. + return -1, nil + } + + var n int + for _, b := range p { + n *= 10 + if b < '0' || b > '9' { + return -1, protocolError("illegal bytes in length") + } + n += int(b - '0') + } + + return n, nil +} + +// parseInt parses an integer reply. +func parseInt(p []byte) (interface{}, error) { + if len(p) == 0 { + return 0, protocolError("malformed integer") + } + + var negate bool + if p[0] == '-' { + negate = true + p = p[1:] + if len(p) == 0 { + return 0, protocolError("malformed integer") + } + } + + var n int64 + for _, b := range p { + n *= 10 + if b < '0' || b > '9' { + return 0, protocolError("illegal bytes in length") + } + n += int64(b - '0') + } + + if negate { + n = -n + } + return n, nil +} + +var ( + okReply interface{} = "OK" + pongReply interface{} = "PONG" +) + +func (c *conn) readReply() (interface{}, error) { + line, err := c.readLine() + if err != nil { + return nil, err + } + if len(line) == 0 { + return nil, protocolError("short response line") + } + switch line[0] { + case '+': + switch string(line[1:]) { + case "OK": + // Avoid allocation for frequent "+OK" response. + return okReply, nil + case "PONG": + // Avoid allocation in PING command benchmarks :) + return pongReply, nil + default: + return string(line[1:]), nil + } + case '-': + return Error(string(line[1:])), nil + case ':': + return parseInt(line[1:]) + case '$': + n, err := parseLen(line[1:]) + if n < 0 || err != nil { + return nil, err + } + p := make([]byte, n) + _, err = io.ReadFull(c.br, p) + if err != nil { + return nil, err + } + if line, err := c.readLine(); err != nil { + return nil, err + } else if len(line) != 0 { + return nil, protocolError("bad bulk string format") + } + return p, nil + case '*': + n, err := parseLen(line[1:]) + if n < 0 || err != nil { + return nil, err + } + r := make([]interface{}, n) + for i := range r { + r[i], err = c.readReply() + if err != nil { + return nil, err + } + } + return r, nil + } + return nil, protocolError("unexpected response line") +} + +func (c *conn) Send(cmd string, args ...interface{}) error { + c.mu.Lock() + c.pending += 1 + c.mu.Unlock() + if c.writeTimeout != 0 { + c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) + } + if err := c.writeCommand(cmd, args); err != nil { + return c.fatal(err) + } + return nil +} + +func (c *conn) Flush() error { + if c.writeTimeout != 0 { + c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) + } + if err := c.bw.Flush(); err != nil { + return c.fatal(err) + } + return nil +} + +func (c *conn) Receive() (interface{}, error) { + return c.ReceiveWithTimeout(c.readTimeout) +} + +func (c *conn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) { + var deadline time.Time + if timeout != 0 { + deadline = time.Now().Add(timeout) + } + c.conn.SetReadDeadline(deadline) + + if reply, err = c.readReply(); err != nil { + return nil, c.fatal(err) + } + // When using pub/sub, the number of receives can be greater than the + // number of sends. To enable normal use of the connection after + // unsubscribing from all channels, we do not decrement pending to a + // negative value. + // + // The pending field is decremented after the reply is read to handle the + // case where Receive is called before Send. + c.mu.Lock() + if c.pending > 0 { + c.pending -= 1 + } + c.mu.Unlock() + if err, ok := reply.(Error); ok { + return nil, err + } + return +} + +func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) { + return c.DoWithTimeout(c.readTimeout, cmd, args...) +} + +func (c *conn) DoWithTimeout(readTimeout time.Duration, cmd string, args ...interface{}) (interface{}, error) { + c.mu.Lock() + pending := c.pending + c.pending = 0 + c.mu.Unlock() + + if cmd == "" && pending == 0 { + return nil, nil + } + + if c.writeTimeout != 0 { + c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) + } + + if cmd != "" { + if err := c.writeCommand(cmd, args); err != nil { + return nil, c.fatal(err) + } + } + + if err := c.bw.Flush(); err != nil { + return nil, c.fatal(err) + } + + var deadline time.Time + if readTimeout != 0 { + deadline = time.Now().Add(readTimeout) + } + c.conn.SetReadDeadline(deadline) + + if cmd == "" { + reply := make([]interface{}, pending) + for i := range reply { + r, e := c.readReply() + if e != nil { + return nil, c.fatal(e) + } + reply[i] = r + } + return reply, nil + } + + var err error + var reply interface{} + for i := 0; i <= pending; i++ { + var e error + if reply, e = c.readReply(); e != nil { + return nil, c.fatal(e) + } + if e, ok := reply.(Error); ok && err == nil { + err = e + } + } + return reply, err +} diff --git a/vendor/github.com/gomodule/redigo/redis/doc.go b/vendor/github.com/gomodule/redigo/redis/doc.go new file mode 100644 index 0000000000000000000000000000000000000000..69ad506cd3a04658f5d262268849e149a65e9e42 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/doc.go @@ -0,0 +1,177 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +// Package redis is a client for the Redis database. +// +// The Redigo FAQ (https://github.com/gomodule/redigo/wiki/FAQ) contains more +// documentation about this package. +// +// Connections +// +// The Conn interface is the primary interface for working with Redis. +// Applications create connections by calling the Dial, DialWithTimeout or +// NewConn functions. In the future, functions will be added for creating +// sharded and other types of connections. +// +// The application must call the connection Close method when the application +// is done with the connection. +// +// Executing Commands +// +// The Conn interface has a generic method for executing Redis commands: +// +// Do(commandName string, args ...interface{}) (reply interface{}, err error) +// +// The Redis command reference (http://redis.io/commands) lists the available +// commands. An example of using the Redis APPEND command is: +// +// n, err := conn.Do("APPEND", "key", "value") +// +// The Do method converts command arguments to bulk strings for transmission +// to the server as follows: +// +// Go Type Conversion +// []byte Sent as is +// string Sent as is +// int, int64 strconv.FormatInt(v) +// float64 strconv.FormatFloat(v, 'g', -1, 64) +// bool true -> "1", false -> "0" +// nil "" +// all other types fmt.Fprint(w, v) +// +// Redis command reply types are represented using the following Go types: +// +// Redis type Go type +// error redis.Error +// integer int64 +// simple string string +// bulk string []byte or nil if value not present. +// array []interface{} or nil if value not present. +// +// Use type assertions or the reply helper functions to convert from +// interface{} to the specific Go type for the command result. +// +// Pipelining +// +// Connections support pipelining using the Send, Flush and Receive methods. +// +// Send(commandName string, args ...interface{}) error +// Flush() error +// Receive() (reply interface{}, err error) +// +// Send writes the command to the connection's output buffer. Flush flushes the +// connection's output buffer to the server. Receive reads a single reply from +// the server. The following example shows a simple pipeline. +// +// c.Send("SET", "foo", "bar") +// c.Send("GET", "foo") +// c.Flush() +// c.Receive() // reply from SET +// v, err = c.Receive() // reply from GET +// +// The Do method combines the functionality of the Send, Flush and Receive +// methods. The Do method starts by writing the command and flushing the output +// buffer. Next, the Do method receives all pending replies including the reply +// for the command just sent by Do. If any of the received replies is an error, +// then Do returns the error. If there are no errors, then Do returns the last +// reply. If the command argument to the Do method is "", then the Do method +// will flush the output buffer and receive pending replies without sending a +// command. +// +// Use the Send and Do methods to implement pipelined transactions. +// +// c.Send("MULTI") +// c.Send("INCR", "foo") +// c.Send("INCR", "bar") +// r, err := c.Do("EXEC") +// fmt.Println(r) // prints [1, 1] +// +// Concurrency +// +// Connections support one concurrent caller to the Receive method and one +// concurrent caller to the Send and Flush methods. No other concurrency is +// supported including concurrent calls to the Do and Close methods. +// +// For full concurrent access to Redis, use the thread-safe Pool to get, use +// and release a connection from within a goroutine. Connections returned from +// a Pool have the concurrency restrictions described in the previous +// paragraph. +// +// Publish and Subscribe +// +// Use the Send, Flush and Receive methods to implement Pub/Sub subscribers. +// +// c.Send("SUBSCRIBE", "example") +// c.Flush() +// for { +// reply, err := c.Receive() +// if err != nil { +// return err +// } +// // process pushed message +// } +// +// The PubSubConn type wraps a Conn with convenience methods for implementing +// subscribers. The Subscribe, PSubscribe, Unsubscribe and PUnsubscribe methods +// send and flush a subscription management command. The receive method +// converts a pushed message to convenient types for use in a type switch. +// +// psc := redis.PubSubConn{Conn: c} +// psc.Subscribe("example") +// for { +// switch v := psc.Receive().(type) { +// case redis.Message: +// fmt.Printf("%s: message: %s\n", v.Channel, v.Data) +// case redis.Subscription: +// fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count) +// case error: +// return v +// } +// } +// +// Reply Helpers +// +// The Bool, Int, Bytes, String, Strings and Values functions convert a reply +// to a value of a specific type. To allow convenient wrapping of calls to the +// connection Do and Receive methods, the functions take a second argument of +// type error. If the error is non-nil, then the helper function returns the +// error. If the error is nil, the function converts the reply to the specified +// type: +// +// exists, err := redis.Bool(c.Do("EXISTS", "foo")) +// if err != nil { +// // handle error return from c.Do or type conversion error. +// } +// +// The Scan function converts elements of a array reply to Go types: +// +// var value1 int +// var value2 string +// reply, err := redis.Values(c.Do("MGET", "key1", "key2")) +// if err != nil { +// // handle error +// } +// if _, err := redis.Scan(reply, &value1, &value2); err != nil { +// // handle error +// } +// +// Errors +// +// Connection methods return error replies from the server as type redis.Error. +// +// Call the connection Err() method to determine if the connection encountered +// non-recoverable error such as a network error or protocol parsing error. If +// Err() returns a non-nil value, then the connection is not usable and should +// be closed. +package redis diff --git a/vendor/github.com/gomodule/redigo/redis/go17.go b/vendor/github.com/gomodule/redigo/redis/go17.go new file mode 100644 index 0000000000000000000000000000000000000000..5f36379113c773e1ec5929abe9fec82fef96ca64 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/go17.go @@ -0,0 +1,29 @@ +// +build go1.7,!go1.8 + +package redis + +import "crypto/tls" + +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + return &tls.Config{ + Rand: cfg.Rand, + Time: cfg.Time, + Certificates: cfg.Certificates, + NameToCertificate: cfg.NameToCertificate, + GetCertificate: cfg.GetCertificate, + RootCAs: cfg.RootCAs, + NextProtos: cfg.NextProtos, + ServerName: cfg.ServerName, + ClientAuth: cfg.ClientAuth, + ClientCAs: cfg.ClientCAs, + InsecureSkipVerify: cfg.InsecureSkipVerify, + CipherSuites: cfg.CipherSuites, + PreferServerCipherSuites: cfg.PreferServerCipherSuites, + ClientSessionCache: cfg.ClientSessionCache, + MinVersion: cfg.MinVersion, + MaxVersion: cfg.MaxVersion, + CurvePreferences: cfg.CurvePreferences, + DynamicRecordSizingDisabled: cfg.DynamicRecordSizingDisabled, + Renegotiation: cfg.Renegotiation, + } +} diff --git a/vendor/github.com/gomodule/redigo/redis/go18.go b/vendor/github.com/gomodule/redigo/redis/go18.go new file mode 100644 index 0000000000000000000000000000000000000000..558363be39aed79ff5530c02bf742d7933721a91 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/go18.go @@ -0,0 +1,9 @@ +// +build go1.8 + +package redis + +import "crypto/tls" + +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + return cfg.Clone() +} diff --git a/vendor/github.com/gomodule/redigo/redis/log.go b/vendor/github.com/gomodule/redigo/redis/log.go new file mode 100644 index 0000000000000000000000000000000000000000..a06db9d6c3e1dddd5eeef9b81e7fd7f4a102a88b --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/log.go @@ -0,0 +1,146 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "bytes" + "fmt" + "log" + "time" +) + +var ( + _ ConnWithTimeout = (*loggingConn)(nil) +) + +// NewLoggingConn returns a logging wrapper around a connection. +func NewLoggingConn(conn Conn, logger *log.Logger, prefix string) Conn { + if prefix != "" { + prefix = prefix + "." + } + return &loggingConn{conn, logger, prefix, nil} +} + +//NewLoggingConnFilter returns a logging wrapper around a connection and a filter function. +func NewLoggingConnFilter(conn Conn, logger *log.Logger, prefix string, skip func(cmdName string) bool) Conn { + if prefix != "" { + prefix = prefix + "." + } + return &loggingConn{conn, logger, prefix, skip} +} + +type loggingConn struct { + Conn + logger *log.Logger + prefix string + skip func(cmdName string) bool +} + +func (c *loggingConn) Close() error { + err := c.Conn.Close() + var buf bytes.Buffer + fmt.Fprintf(&buf, "%sClose() -> (%v)", c.prefix, err) + c.logger.Output(2, buf.String()) + return err +} + +func (c *loggingConn) printValue(buf *bytes.Buffer, v interface{}) { + const chop = 32 + switch v := v.(type) { + case []byte: + if len(v) > chop { + fmt.Fprintf(buf, "%q...", v[:chop]) + } else { + fmt.Fprintf(buf, "%q", v) + } + case string: + if len(v) > chop { + fmt.Fprintf(buf, "%q...", v[:chop]) + } else { + fmt.Fprintf(buf, "%q", v) + } + case []interface{}: + if len(v) == 0 { + buf.WriteString("[]") + } else { + sep := "[" + fin := "]" + if len(v) > chop { + v = v[:chop] + fin = "...]" + } + for _, vv := range v { + buf.WriteString(sep) + c.printValue(buf, vv) + sep = ", " + } + buf.WriteString(fin) + } + default: + fmt.Fprint(buf, v) + } +} + +func (c *loggingConn) print(method, commandName string, args []interface{}, reply interface{}, err error) { + if c.skip != nil && c.skip(commandName) { + return + } + var buf bytes.Buffer + fmt.Fprintf(&buf, "%s%s(", c.prefix, method) + if method != "Receive" { + buf.WriteString(commandName) + for _, arg := range args { + buf.WriteString(", ") + c.printValue(&buf, arg) + } + } + buf.WriteString(") -> (") + if method != "Send" { + c.printValue(&buf, reply) + buf.WriteString(", ") + } + fmt.Fprintf(&buf, "%v)", err) + c.logger.Output(3, buf.String()) +} + +func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{}, error) { + reply, err := c.Conn.Do(commandName, args...) + c.print("Do", commandName, args, reply, err) + return reply, err +} + +func (c *loggingConn) DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (interface{}, error) { + reply, err := DoWithTimeout(c.Conn, timeout, commandName, args...) + c.print("DoWithTimeout", commandName, args, reply, err) + return reply, err +} + +func (c *loggingConn) Send(commandName string, args ...interface{}) error { + err := c.Conn.Send(commandName, args...) + c.print("Send", commandName, args, nil, err) + return err +} + +func (c *loggingConn) Receive() (interface{}, error) { + reply, err := c.Conn.Receive() + c.print("Receive", "", nil, reply, err) + return reply, err +} + +func (c *loggingConn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error) { + reply, err := ReceiveWithTimeout(c.Conn, timeout) + c.print("ReceiveWithTimeout", "", nil, reply, err) + return reply, err +} diff --git a/vendor/github.com/gomodule/redigo/redis/pool.go b/vendor/github.com/gomodule/redigo/redis/pool.go new file mode 100644 index 0000000000000000000000000000000000000000..b74e469d74073290100a6399b2b367398007536c --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/pool.go @@ -0,0 +1,622 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/sha1" + "errors" + "io" + "strconv" + "sync" + "time" +) + +var ( + _ ConnWithTimeout = (*activeConn)(nil) + _ ConnWithTimeout = (*errorConn)(nil) +) + +var nowFunc = time.Now // for testing + +// ErrPoolExhausted is returned from a pool connection method (Do, Send, +// Receive, Flush, Err) when the maximum number of database connections in the +// pool has been reached. +var ErrPoolExhausted = errors.New("redigo: connection pool exhausted") + +var ( + errPoolClosed = errors.New("redigo: connection pool closed") + errConnClosed = errors.New("redigo: connection closed") +) + +// Pool maintains a pool of connections. The application calls the Get method +// to get a connection from the pool and the connection's Close method to +// return the connection's resources to the pool. +// +// The following example shows how to use a pool in a web application. The +// application creates a pool at application startup and makes it available to +// request handlers using a package level variable. The pool configuration used +// here is an example, not a recommendation. +// +// func newPool(addr string) *redis.Pool { +// return &redis.Pool{ +// MaxIdle: 3, +// IdleTimeout: 240 * time.Second, +// // Dial or DialContext must be set. When both are set, DialContext takes precedence over Dial. +// Dial: func () (redis.Conn, error) { return redis.Dial("tcp", addr) }, +// } +// } +// +// var ( +// pool *redis.Pool +// redisServer = flag.String("redisServer", ":6379", "") +// ) +// +// func main() { +// flag.Parse() +// pool = newPool(*redisServer) +// ... +// } +// +// A request handler gets a connection from the pool and closes the connection +// when the handler is done: +// +// func serveHome(w http.ResponseWriter, r *http.Request) { +// conn := pool.Get() +// defer conn.Close() +// ... +// } +// +// Use the Dial function to authenticate connections with the AUTH command or +// select a database with the SELECT command: +// +// pool := &redis.Pool{ +// // Other pool configuration not shown in this example. +// Dial: func () (redis.Conn, error) { +// c, err := redis.Dial("tcp", server) +// if err != nil { +// return nil, err +// } +// if _, err := c.Do("AUTH", password); err != nil { +// c.Close() +// return nil, err +// } +// if _, err := c.Do("SELECT", db); err != nil { +// c.Close() +// return nil, err +// } +// return c, nil +// }, +// } +// +// Use the TestOnBorrow function to check the health of an idle connection +// before the connection is returned to the application. This example PINGs +// connections that have been idle more than a minute: +// +// pool := &redis.Pool{ +// // Other pool configuration not shown in this example. +// TestOnBorrow: func(c redis.Conn, t time.Time) error { +// if time.Since(t) < time.Minute { +// return nil +// } +// _, err := c.Do("PING") +// return err +// }, +// } +// +type Pool struct { + // Dial is an application supplied function for creating and configuring a + // connection. + // + // The connection returned from Dial must not be in a special state + // (subscribed to pubsub channel, transaction started, ...). + Dial func() (Conn, error) + + // DialContext is an application supplied function for creating and configuring a + // connection with the given context. + // + // The connection returned from Dial must not be in a special state + // (subscribed to pubsub channel, transaction started, ...). + DialContext func(ctx context.Context) (Conn, error) + + // TestOnBorrow is an optional application supplied function for checking + // the health of an idle connection before the connection is used again by + // the application. Argument t is the time that the connection was returned + // to the pool. If the function returns an error, then the connection is + // closed. + TestOnBorrow func(c Conn, t time.Time) error + + // Maximum number of idle connections in the pool. + MaxIdle int + + // Maximum number of connections allocated by the pool at a given time. + // When zero, there is no limit on the number of connections in the pool. + MaxActive int + + // Close connections after remaining idle for this duration. If the value + // is zero, then idle connections are not closed. Applications should set + // the timeout to a value less than the server's timeout. + IdleTimeout time.Duration + + // If Wait is true and the pool is at the MaxActive limit, then Get() waits + // for a connection to be returned to the pool before returning. + Wait bool + + // Close connections older than this duration. If the value is zero, then + // the pool does not close connections based on age. + MaxConnLifetime time.Duration + + mu sync.Mutex // mu protects the following fields + closed bool // set to true when the pool is closed. + active int // the number of open connections in the pool + initOnce sync.Once // the init ch once func + ch chan struct{} // limits open connections when p.Wait is true + idle idleList // idle connections + waitCount int64 // total number of connections waited for. + waitDuration time.Duration // total time waited for new connections. +} + +// NewPool creates a new pool. +// +// Deprecated: Initialize the Pool directly as shown in the example. +func NewPool(newFn func() (Conn, error), maxIdle int) *Pool { + return &Pool{Dial: newFn, MaxIdle: maxIdle} +} + +// Get gets a connection. The application must close the returned connection. +// This method always returns a valid connection so that applications can defer +// error handling to the first use of the connection. If there is an error +// getting an underlying connection, then the connection Err, Do, Send, Flush +// and Receive methods return that error. +func (p *Pool) Get() Conn { + // GetContext returns errorConn in the first argument when an error occurs. + c, _ := p.GetContext(context.Background()) + return c +} + +// GetContext gets a connection using the provided context. +// +// The provided Context must be non-nil. If the context expires before the +// connection is complete, an error is returned. Any expiration on the context +// will not affect the returned connection. +// +// If the function completes without error, then the application must close the +// returned connection. +func (p *Pool) GetContext(ctx context.Context) (Conn, error) { + // Wait until there is a vacant connection in the pool. + waited, err := p.waitVacantConn(ctx) + if err != nil { + return errorConn{err}, err + } + + p.mu.Lock() + + if waited > 0 { + p.waitCount++ + p.waitDuration += waited + } + + // Prune stale connections at the back of the idle list. + if p.IdleTimeout > 0 { + n := p.idle.count + for i := 0; i < n && p.idle.back != nil && p.idle.back.t.Add(p.IdleTimeout).Before(nowFunc()); i++ { + pc := p.idle.back + p.idle.popBack() + p.mu.Unlock() + pc.c.Close() + p.mu.Lock() + p.active-- + } + } + + // Get idle connection from the front of idle list. + for p.idle.front != nil { + pc := p.idle.front + p.idle.popFront() + p.mu.Unlock() + if (p.TestOnBorrow == nil || p.TestOnBorrow(pc.c, pc.t) == nil) && + (p.MaxConnLifetime == 0 || nowFunc().Sub(pc.created) < p.MaxConnLifetime) { + return &activeConn{p: p, pc: pc}, nil + } + pc.c.Close() + p.mu.Lock() + p.active-- + } + + // Check for pool closed before dialing a new connection. + if p.closed { + p.mu.Unlock() + err := errors.New("redigo: get on closed pool") + return errorConn{err}, err + } + + // Handle limit for p.Wait == false. + if !p.Wait && p.MaxActive > 0 && p.active >= p.MaxActive { + p.mu.Unlock() + return errorConn{ErrPoolExhausted}, ErrPoolExhausted + } + + p.active++ + p.mu.Unlock() + c, err := p.dial(ctx) + if err != nil { + c = nil + p.mu.Lock() + p.active-- + if p.ch != nil && !p.closed { + p.ch <- struct{}{} + } + p.mu.Unlock() + return errorConn{err}, err + } + return &activeConn{p: p, pc: &poolConn{c: c, created: nowFunc()}}, nil +} + +// PoolStats contains pool statistics. +type PoolStats struct { + // ActiveCount is the number of connections in the pool. The count includes + // idle connections and connections in use. + ActiveCount int + // IdleCount is the number of idle connections in the pool. + IdleCount int + + // WaitCount is the total number of connections waited for. + // This value is currently not guaranteed to be 100% accurate. + WaitCount int64 + + // WaitDuration is the total time blocked waiting for a new connection. + // This value is currently not guaranteed to be 100% accurate. + WaitDuration time.Duration +} + +// Stats returns pool's statistics. +func (p *Pool) Stats() PoolStats { + p.mu.Lock() + stats := PoolStats{ + ActiveCount: p.active, + IdleCount: p.idle.count, + WaitCount: p.waitCount, + WaitDuration: p.waitDuration, + } + p.mu.Unlock() + + return stats +} + +// ActiveCount returns the number of connections in the pool. The count +// includes idle connections and connections in use. +func (p *Pool) ActiveCount() int { + p.mu.Lock() + active := p.active + p.mu.Unlock() + return active +} + +// IdleCount returns the number of idle connections in the pool. +func (p *Pool) IdleCount() int { + p.mu.Lock() + idle := p.idle.count + p.mu.Unlock() + return idle +} + +// Close releases the resources used by the pool. +func (p *Pool) Close() error { + p.mu.Lock() + if p.closed { + p.mu.Unlock() + return nil + } + p.closed = true + p.active -= p.idle.count + pc := p.idle.front + p.idle.count = 0 + p.idle.front, p.idle.back = nil, nil + if p.ch != nil { + close(p.ch) + } + p.mu.Unlock() + for ; pc != nil; pc = pc.next { + pc.c.Close() + } + return nil +} + +func (p *Pool) lazyInit() { + p.initOnce.Do(func() { + p.ch = make(chan struct{}, p.MaxActive) + if p.closed { + close(p.ch) + } else { + for i := 0; i < p.MaxActive; i++ { + p.ch <- struct{}{} + } + } + }) +} + +// waitVacantConn waits for a vacant connection in pool if waiting +// is enabled and pool size is limited, otherwise returns instantly. +// If ctx expires before that, an error is returned. +// +// If there were no vacant connection in the pool right away it returns the time spent waiting +// for that connection to appear in the pool. +func (p *Pool) waitVacantConn(ctx context.Context) (waited time.Duration, err error) { + if !p.Wait || p.MaxActive <= 0 { + // No wait or no connection limit. + return 0, nil + } + + p.lazyInit() + + // wait indicates if we believe it will block so its not 100% accurate + // however for stats it should be good enough. + wait := len(p.ch) == 0 + var start time.Time + if wait { + start = time.Now() + } + + select { + case <-p.ch: + // Additionally check that context hasn't expired while we were waiting, + // because `select` picks a random `case` if several of them are "ready". + select { + case <-ctx.Done(): + p.ch <- struct{}{} + return 0, ctx.Err() + default: + } + case <-ctx.Done(): + return 0, ctx.Err() + } + + if wait { + return time.Since(start), nil + } + return 0, nil +} + +func (p *Pool) dial(ctx context.Context) (Conn, error) { + if p.DialContext != nil { + return p.DialContext(ctx) + } + if p.Dial != nil { + return p.Dial() + } + return nil, errors.New("redigo: must pass Dial or DialContext to pool") +} + +func (p *Pool) put(pc *poolConn, forceClose bool) error { + p.mu.Lock() + if !p.closed && !forceClose { + pc.t = nowFunc() + p.idle.pushFront(pc) + if p.idle.count > p.MaxIdle { + pc = p.idle.back + p.idle.popBack() + } else { + pc = nil + } + } + + if pc != nil { + p.mu.Unlock() + pc.c.Close() + p.mu.Lock() + p.active-- + } + + if p.ch != nil && !p.closed { + p.ch <- struct{}{} + } + p.mu.Unlock() + return nil +} + +type activeConn struct { + p *Pool + pc *poolConn + state int +} + +var ( + sentinel []byte + sentinelOnce sync.Once +) + +func initSentinel() { + p := make([]byte, 64) + if _, err := rand.Read(p); err == nil { + sentinel = p + } else { + h := sha1.New() + io.WriteString(h, "Oops, rand failed. Use time instead.") + io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10)) + sentinel = h.Sum(nil) + } +} + +func (ac *activeConn) Close() error { + pc := ac.pc + if pc == nil { + return nil + } + ac.pc = nil + + if ac.state&connectionMultiState != 0 { + pc.c.Send("DISCARD") + ac.state &^= (connectionMultiState | connectionWatchState) + } else if ac.state&connectionWatchState != 0 { + pc.c.Send("UNWATCH") + ac.state &^= connectionWatchState + } + if ac.state&connectionSubscribeState != 0 { + pc.c.Send("UNSUBSCRIBE") + pc.c.Send("PUNSUBSCRIBE") + // To detect the end of the message stream, ask the server to echo + // a sentinel value and read until we see that value. + sentinelOnce.Do(initSentinel) + pc.c.Send("ECHO", sentinel) + pc.c.Flush() + for { + p, err := pc.c.Receive() + if err != nil { + break + } + if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) { + ac.state &^= connectionSubscribeState + break + } + } + } + pc.c.Do("") + ac.p.put(pc, ac.state != 0 || pc.c.Err() != nil) + return nil +} + +func (ac *activeConn) Err() error { + pc := ac.pc + if pc == nil { + return errConnClosed + } + return pc.c.Err() +} + +func (ac *activeConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) { + pc := ac.pc + if pc == nil { + return nil, errConnClosed + } + ci := lookupCommandInfo(commandName) + ac.state = (ac.state | ci.Set) &^ ci.Clear + return pc.c.Do(commandName, args...) +} + +func (ac *activeConn) DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error) { + pc := ac.pc + if pc == nil { + return nil, errConnClosed + } + cwt, ok := pc.c.(ConnWithTimeout) + if !ok { + return nil, errTimeoutNotSupported + } + ci := lookupCommandInfo(commandName) + ac.state = (ac.state | ci.Set) &^ ci.Clear + return cwt.DoWithTimeout(timeout, commandName, args...) +} + +func (ac *activeConn) Send(commandName string, args ...interface{}) error { + pc := ac.pc + if pc == nil { + return errConnClosed + } + ci := lookupCommandInfo(commandName) + ac.state = (ac.state | ci.Set) &^ ci.Clear + return pc.c.Send(commandName, args...) +} + +func (ac *activeConn) Flush() error { + pc := ac.pc + if pc == nil { + return errConnClosed + } + return pc.c.Flush() +} + +func (ac *activeConn) Receive() (reply interface{}, err error) { + pc := ac.pc + if pc == nil { + return nil, errConnClosed + } + return pc.c.Receive() +} + +func (ac *activeConn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) { + pc := ac.pc + if pc == nil { + return nil, errConnClosed + } + cwt, ok := pc.c.(ConnWithTimeout) + if !ok { + return nil, errTimeoutNotSupported + } + return cwt.ReceiveWithTimeout(timeout) +} + +type errorConn struct{ err error } + +func (ec errorConn) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err } +func (ec errorConn) DoWithTimeout(time.Duration, string, ...interface{}) (interface{}, error) { + return nil, ec.err +} +func (ec errorConn) Send(string, ...interface{}) error { return ec.err } +func (ec errorConn) Err() error { return ec.err } +func (ec errorConn) Close() error { return nil } +func (ec errorConn) Flush() error { return ec.err } +func (ec errorConn) Receive() (interface{}, error) { return nil, ec.err } +func (ec errorConn) ReceiveWithTimeout(time.Duration) (interface{}, error) { return nil, ec.err } + +type idleList struct { + count int + front, back *poolConn +} + +type poolConn struct { + c Conn + t time.Time + created time.Time + next, prev *poolConn +} + +func (l *idleList) pushFront(pc *poolConn) { + pc.next = l.front + pc.prev = nil + if l.count == 0 { + l.back = pc + } else { + l.front.prev = pc + } + l.front = pc + l.count++ + return +} + +func (l *idleList) popFront() { + pc := l.front + l.count-- + if l.count == 0 { + l.front, l.back = nil, nil + } else { + pc.next.prev = nil + l.front = pc.next + } + pc.next, pc.prev = nil, nil +} + +func (l *idleList) popBack() { + pc := l.back + l.count-- + if l.count == 0 { + l.front, l.back = nil, nil + } else { + pc.prev.next = nil + l.back = pc.prev + } + pc.next, pc.prev = nil, nil +} diff --git a/vendor/github.com/gomodule/redigo/redis/pubsub.go b/vendor/github.com/gomodule/redigo/redis/pubsub.go new file mode 100644 index 0000000000000000000000000000000000000000..2da60211d9f8b4667cb32a63de5a41901d0e10cd --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/pubsub.go @@ -0,0 +1,148 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "errors" + "time" +) + +// Subscription represents a subscribe or unsubscribe notification. +type Subscription struct { + // Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe" + Kind string + + // The channel that was changed. + Channel string + + // The current number of subscriptions for connection. + Count int +} + +// Message represents a message notification. +type Message struct { + // The originating channel. + Channel string + + // The matched pattern, if any + Pattern string + + // The message data. + Data []byte +} + +// Pong represents a pubsub pong notification. +type Pong struct { + Data string +} + +// PubSubConn wraps a Conn with convenience methods for subscribers. +type PubSubConn struct { + Conn Conn +} + +// Close closes the connection. +func (c PubSubConn) Close() error { + return c.Conn.Close() +} + +// Subscribe subscribes the connection to the specified channels. +func (c PubSubConn) Subscribe(channel ...interface{}) error { + c.Conn.Send("SUBSCRIBE", channel...) + return c.Conn.Flush() +} + +// PSubscribe subscribes the connection to the given patterns. +func (c PubSubConn) PSubscribe(channel ...interface{}) error { + c.Conn.Send("PSUBSCRIBE", channel...) + return c.Conn.Flush() +} + +// Unsubscribe unsubscribes the connection from the given channels, or from all +// of them if none is given. +func (c PubSubConn) Unsubscribe(channel ...interface{}) error { + c.Conn.Send("UNSUBSCRIBE", channel...) + return c.Conn.Flush() +} + +// PUnsubscribe unsubscribes the connection from the given patterns, or from all +// of them if none is given. +func (c PubSubConn) PUnsubscribe(channel ...interface{}) error { + c.Conn.Send("PUNSUBSCRIBE", channel...) + return c.Conn.Flush() +} + +// Ping sends a PING to the server with the specified data. +// +// The connection must be subscribed to at least one channel or pattern when +// calling this method. +func (c PubSubConn) Ping(data string) error { + c.Conn.Send("PING", data) + return c.Conn.Flush() +} + +// Receive returns a pushed message as a Subscription, Message, Pong or error. +// The return value is intended to be used directly in a type switch as +// illustrated in the PubSubConn example. +func (c PubSubConn) Receive() interface{} { + return c.receiveInternal(c.Conn.Receive()) +} + +// ReceiveWithTimeout is like Receive, but it allows the application to +// override the connection's default timeout. +func (c PubSubConn) ReceiveWithTimeout(timeout time.Duration) interface{} { + return c.receiveInternal(ReceiveWithTimeout(c.Conn, timeout)) +} + +func (c PubSubConn) receiveInternal(replyArg interface{}, errArg error) interface{} { + reply, err := Values(replyArg, errArg) + if err != nil { + return err + } + + var kind string + reply, err = Scan(reply, &kind) + if err != nil { + return err + } + + switch kind { + case "message": + var m Message + if _, err := Scan(reply, &m.Channel, &m.Data); err != nil { + return err + } + return m + case "pmessage": + var m Message + if _, err := Scan(reply, &m.Pattern, &m.Channel, &m.Data); err != nil { + return err + } + return m + case "subscribe", "psubscribe", "unsubscribe", "punsubscribe": + s := Subscription{Kind: kind} + if _, err := Scan(reply, &s.Channel, &s.Count); err != nil { + return err + } + return s + case "pong": + var p Pong + if _, err := Scan(reply, &p.Data); err != nil { + return err + } + return p + } + return errors.New("redigo: unknown pubsub notification") +} diff --git a/vendor/github.com/gomodule/redigo/redis/redis.go b/vendor/github.com/gomodule/redigo/redis/redis.go new file mode 100644 index 0000000000000000000000000000000000000000..e4464874471a0a40f3c484f0be62367bd73a0b48 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/redis.go @@ -0,0 +1,138 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "errors" + "time" +) + +// Error represents an error returned in a command reply. +type Error string + +func (err Error) Error() string { return string(err) } + +// Conn represents a connection to a Redis server. +type Conn interface { + // Close closes the connection. + Close() error + + // Err returns a non-nil value when the connection is not usable. + Err() error + + // Do sends a command to the server and returns the received reply. + Do(commandName string, args ...interface{}) (reply interface{}, err error) + + // Send writes the command to the client's output buffer. + Send(commandName string, args ...interface{}) error + + // Flush flushes the output buffer to the Redis server. + Flush() error + + // Receive receives a single reply from the Redis server + Receive() (reply interface{}, err error) +} + +// Argument is the interface implemented by an object which wants to control how +// the object is converted to Redis bulk strings. +type Argument interface { + // RedisArg returns a value to be encoded as a bulk string per the + // conversions listed in the section 'Executing Commands'. + // Implementations should typically return a []byte or string. + RedisArg() interface{} +} + +// Scanner is implemented by an object which wants to control its value is +// interpreted when read from Redis. +type Scanner interface { + // RedisScan assigns a value from a Redis value. The argument src is one of + // the reply types listed in the section `Executing Commands`. + // + // An error should be returned if the value cannot be stored without + // loss of information. + RedisScan(src interface{}) error +} + +// ConnWithTimeout is an optional interface that allows the caller to override +// a connection's default read timeout. This interface is useful for executing +// the BLPOP, BRPOP, BRPOPLPUSH, XREAD and other commands that block at the +// server. +// +// A connection's default read timeout is set with the DialReadTimeout dial +// option. Applications should rely on the default timeout for commands that do +// not block at the server. +// +// All of the Conn implementations in this package satisfy the ConnWithTimeout +// interface. +// +// Use the DoWithTimeout and ReceiveWithTimeout helper functions to simplify +// use of this interface. +type ConnWithTimeout interface { + Conn + + // Do sends a command to the server and returns the received reply. + // The timeout overrides the read timeout set when dialing the + // connection. + DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error) + + // Receive receives a single reply from the Redis server. The timeout + // overrides the read timeout set when dialing the connection. + ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) +} + +var errTimeoutNotSupported = errors.New("redis: connection does not support ConnWithTimeout") + +// DoWithTimeout executes a Redis command with the specified read timeout. If +// the connection does not satisfy the ConnWithTimeout interface, then an error +// is returned. +func DoWithTimeout(c Conn, timeout time.Duration, cmd string, args ...interface{}) (interface{}, error) { + cwt, ok := c.(ConnWithTimeout) + if !ok { + return nil, errTimeoutNotSupported + } + return cwt.DoWithTimeout(timeout, cmd, args...) +} + +// ReceiveWithTimeout receives a reply with the specified read timeout. If the +// connection does not satisfy the ConnWithTimeout interface, then an error is +// returned. +func ReceiveWithTimeout(c Conn, timeout time.Duration) (interface{}, error) { + cwt, ok := c.(ConnWithTimeout) + if !ok { + return nil, errTimeoutNotSupported + } + return cwt.ReceiveWithTimeout(timeout) +} + +// SlowLog represents a redis SlowLog +type SlowLog struct { + // ID is a unique progressive identifier for every slow log entry. + ID int64 + + // Time is the unix timestamp at which the logged command was processed. + Time time.Time + + // ExecutationTime is the amount of time needed for the command execution. + ExecutionTime time.Duration + + // Args is the command name and arguments + Args []string + + // ClientAddr is the client IP address (4.0 only). + ClientAddr string + + // ClientName is the name set via the CLIENT SETNAME command (4.0 only). + ClientName string +} diff --git a/vendor/github.com/gomodule/redigo/redis/reply.go b/vendor/github.com/gomodule/redigo/redis/reply.go new file mode 100644 index 0000000000000000000000000000000000000000..dfe6aff7938c836b903f536334bd3f374bd0d444 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/reply.go @@ -0,0 +1,583 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "errors" + "fmt" + "strconv" + "time" +) + +// ErrNil indicates that a reply value is nil. +var ErrNil = errors.New("redigo: nil returned") + +// Int is a helper that converts a command reply to an integer. If err is not +// equal to nil, then Int returns 0, err. Otherwise, Int converts the +// reply to an int as follows: +// +// Reply type Result +// integer int(reply), nil +// bulk string parsed reply, nil +// nil 0, ErrNil +// other 0, error +func Int(reply interface{}, err error) (int, error) { + if err != nil { + return 0, err + } + switch reply := reply.(type) { + case int64: + x := int(reply) + if int64(x) != reply { + return 0, strconv.ErrRange + } + return x, nil + case []byte: + n, err := strconv.ParseInt(string(reply), 10, 0) + return int(n), err + case nil: + return 0, ErrNil + case Error: + return 0, reply + } + return 0, fmt.Errorf("redigo: unexpected type for Int, got type %T", reply) +} + +// Int64 is a helper that converts a command reply to 64 bit integer. If err is +// not equal to nil, then Int64 returns 0, err. Otherwise, Int64 converts the +// reply to an int64 as follows: +// +// Reply type Result +// integer reply, nil +// bulk string parsed reply, nil +// nil 0, ErrNil +// other 0, error +func Int64(reply interface{}, err error) (int64, error) { + if err != nil { + return 0, err + } + switch reply := reply.(type) { + case int64: + return reply, nil + case []byte: + n, err := strconv.ParseInt(string(reply), 10, 64) + return n, err + case nil: + return 0, ErrNil + case Error: + return 0, reply + } + return 0, fmt.Errorf("redigo: unexpected type for Int64, got type %T", reply) +} + +func errNegativeInt(v int64) error { + return fmt.Errorf("redigo: unexpected negative value %v for Uint64", v) +} + +// Uint64 is a helper that converts a command reply to 64 bit unsigned integer. +// If err is not equal to nil, then Uint64 returns 0, err. Otherwise, Uint64 converts the +// reply to an uint64 as follows: +// +// Reply type Result +// +integer reply, nil +// bulk string parsed reply, nil +// nil 0, ErrNil +// other 0, error +func Uint64(reply interface{}, err error) (uint64, error) { + if err != nil { + return 0, err + } + switch reply := reply.(type) { + case int64: + if reply < 0 { + return 0, errNegativeInt(reply) + } + return uint64(reply), nil + case []byte: + n, err := strconv.ParseUint(string(reply), 10, 64) + return n, err + case nil: + return 0, ErrNil + case Error: + return 0, reply + } + return 0, fmt.Errorf("redigo: unexpected type for Uint64, got type %T", reply) +} + +// Float64 is a helper that converts a command reply to 64 bit float. If err is +// not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts +// the reply to a float64 as follows: +// +// Reply type Result +// bulk string parsed reply, nil +// nil 0, ErrNil +// other 0, error +func Float64(reply interface{}, err error) (float64, error) { + if err != nil { + return 0, err + } + switch reply := reply.(type) { + case []byte: + n, err := strconv.ParseFloat(string(reply), 64) + return n, err + case nil: + return 0, ErrNil + case Error: + return 0, reply + } + return 0, fmt.Errorf("redigo: unexpected type for Float64, got type %T", reply) +} + +// String is a helper that converts a command reply to a string. If err is not +// equal to nil, then String returns "", err. Otherwise String converts the +// reply to a string as follows: +// +// Reply type Result +// bulk string string(reply), nil +// simple string reply, nil +// nil "", ErrNil +// other "", error +func String(reply interface{}, err error) (string, error) { + if err != nil { + return "", err + } + switch reply := reply.(type) { + case []byte: + return string(reply), nil + case string: + return reply, nil + case nil: + return "", ErrNil + case Error: + return "", reply + } + return "", fmt.Errorf("redigo: unexpected type for String, got type %T", reply) +} + +// Bytes is a helper that converts a command reply to a slice of bytes. If err +// is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts +// the reply to a slice of bytes as follows: +// +// Reply type Result +// bulk string reply, nil +// simple string []byte(reply), nil +// nil nil, ErrNil +// other nil, error +func Bytes(reply interface{}, err error) ([]byte, error) { + if err != nil { + return nil, err + } + switch reply := reply.(type) { + case []byte: + return reply, nil + case string: + return []byte(reply), nil + case nil: + return nil, ErrNil + case Error: + return nil, reply + } + return nil, fmt.Errorf("redigo: unexpected type for Bytes, got type %T", reply) +} + +// Bool is a helper that converts a command reply to a boolean. If err is not +// equal to nil, then Bool returns false, err. Otherwise Bool converts the +// reply to boolean as follows: +// +// Reply type Result +// integer value != 0, nil +// bulk string strconv.ParseBool(reply) +// nil false, ErrNil +// other false, error +func Bool(reply interface{}, err error) (bool, error) { + if err != nil { + return false, err + } + switch reply := reply.(type) { + case int64: + return reply != 0, nil + case []byte: + return strconv.ParseBool(string(reply)) + case nil: + return false, ErrNil + case Error: + return false, reply + } + return false, fmt.Errorf("redigo: unexpected type for Bool, got type %T", reply) +} + +// MultiBulk is a helper that converts an array command reply to a []interface{}. +// +// Deprecated: Use Values instead. +func MultiBulk(reply interface{}, err error) ([]interface{}, error) { return Values(reply, err) } + +// Values is a helper that converts an array command reply to a []interface{}. +// If err is not equal to nil, then Values returns nil, err. Otherwise, Values +// converts the reply as follows: +// +// Reply type Result +// array reply, nil +// nil nil, ErrNil +// other nil, error +func Values(reply interface{}, err error) ([]interface{}, error) { + if err != nil { + return nil, err + } + switch reply := reply.(type) { + case []interface{}: + return reply, nil + case nil: + return nil, ErrNil + case Error: + return nil, reply + } + return nil, fmt.Errorf("redigo: unexpected type for Values, got type %T", reply) +} + +func sliceHelper(reply interface{}, err error, name string, makeSlice func(int), assign func(int, interface{}) error) error { + if err != nil { + return err + } + switch reply := reply.(type) { + case []interface{}: + makeSlice(len(reply)) + for i := range reply { + if reply[i] == nil { + continue + } + if err := assign(i, reply[i]); err != nil { + return err + } + } + return nil + case nil: + return ErrNil + case Error: + return reply + } + return fmt.Errorf("redigo: unexpected type for %s, got type %T", name, reply) +} + +// Float64s is a helper that converts an array command reply to a []float64. If +// err is not equal to nil, then Float64s returns nil, err. Nil array items are +// converted to 0 in the output slice. Floats64 returns an error if an array +// item is not a bulk string or nil. +func Float64s(reply interface{}, err error) ([]float64, error) { + var result []float64 + err = sliceHelper(reply, err, "Float64s", func(n int) { result = make([]float64, n) }, func(i int, v interface{}) error { + p, ok := v.([]byte) + if !ok { + return fmt.Errorf("redigo: unexpected element type for Floats64, got type %T", v) + } + f, err := strconv.ParseFloat(string(p), 64) + result[i] = f + return err + }) + return result, err +} + +// Strings is a helper that converts an array command reply to a []string. If +// err is not equal to nil, then Strings returns nil, err. Nil array items are +// converted to "" in the output slice. Strings returns an error if an array +// item is not a bulk string or nil. +func Strings(reply interface{}, err error) ([]string, error) { + var result []string + err = sliceHelper(reply, err, "Strings", func(n int) { result = make([]string, n) }, func(i int, v interface{}) error { + switch v := v.(type) { + case string: + result[i] = v + return nil + case []byte: + result[i] = string(v) + return nil + default: + return fmt.Errorf("redigo: unexpected element type for Strings, got type %T", v) + } + }) + return result, err +} + +// ByteSlices is a helper that converts an array command reply to a [][]byte. +// If err is not equal to nil, then ByteSlices returns nil, err. Nil array +// items are stay nil. ByteSlices returns an error if an array item is not a +// bulk string or nil. +func ByteSlices(reply interface{}, err error) ([][]byte, error) { + var result [][]byte + err = sliceHelper(reply, err, "ByteSlices", func(n int) { result = make([][]byte, n) }, func(i int, v interface{}) error { + p, ok := v.([]byte) + if !ok { + return fmt.Errorf("redigo: unexpected element type for ByteSlices, got type %T", v) + } + result[i] = p + return nil + }) + return result, err +} + +// Int64s is a helper that converts an array command reply to a []int64. +// If err is not equal to nil, then Int64s returns nil, err. Nil array +// items are stay nil. Int64s returns an error if an array item is not a +// bulk string or nil. +func Int64s(reply interface{}, err error) ([]int64, error) { + var result []int64 + err = sliceHelper(reply, err, "Int64s", func(n int) { result = make([]int64, n) }, func(i int, v interface{}) error { + switch v := v.(type) { + case int64: + result[i] = v + return nil + case []byte: + n, err := strconv.ParseInt(string(v), 10, 64) + result[i] = n + return err + default: + return fmt.Errorf("redigo: unexpected element type for Int64s, got type %T", v) + } + }) + return result, err +} + +// Ints is a helper that converts an array command reply to a []int. +// If err is not equal to nil, then Ints returns nil, err. Nil array +// items are stay nil. Ints returns an error if an array item is not a +// bulk string or nil. +func Ints(reply interface{}, err error) ([]int, error) { + var result []int + err = sliceHelper(reply, err, "Ints", func(n int) { result = make([]int, n) }, func(i int, v interface{}) error { + switch v := v.(type) { + case int64: + n := int(v) + if int64(n) != v { + return strconv.ErrRange + } + result[i] = n + return nil + case []byte: + n, err := strconv.Atoi(string(v)) + result[i] = n + return err + default: + return fmt.Errorf("redigo: unexpected element type for Ints, got type %T", v) + } + }) + return result, err +} + +// StringMap is a helper that converts an array of strings (alternating key, value) +// into a map[string]string. The HGETALL and CONFIG GET commands return replies in this format. +// Requires an even number of values in result. +func StringMap(result interface{}, err error) (map[string]string, error) { + values, err := Values(result, err) + if err != nil { + return nil, err + } + if len(values)%2 != 0 { + return nil, errors.New("redigo: StringMap expects even number of values result") + } + m := make(map[string]string, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, okKey := values[i].([]byte) + value, okValue := values[i+1].([]byte) + if !okKey || !okValue { + return nil, errors.New("redigo: StringMap key not a bulk string value") + } + m[string(key)] = string(value) + } + return m, nil +} + +// IntMap is a helper that converts an array of strings (alternating key, value) +// into a map[string]int. The HGETALL commands return replies in this format. +// Requires an even number of values in result. +func IntMap(result interface{}, err error) (map[string]int, error) { + values, err := Values(result, err) + if err != nil { + return nil, err + } + if len(values)%2 != 0 { + return nil, errors.New("redigo: IntMap expects even number of values result") + } + m := make(map[string]int, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, ok := values[i].([]byte) + if !ok { + return nil, errors.New("redigo: IntMap key not a bulk string value") + } + value, err := Int(values[i+1], nil) + if err != nil { + return nil, err + } + m[string(key)] = value + } + return m, nil +} + +// Int64Map is a helper that converts an array of strings (alternating key, value) +// into a map[string]int64. The HGETALL commands return replies in this format. +// Requires an even number of values in result. +func Int64Map(result interface{}, err error) (map[string]int64, error) { + values, err := Values(result, err) + if err != nil { + return nil, err + } + if len(values)%2 != 0 { + return nil, errors.New("redigo: Int64Map expects even number of values result") + } + m := make(map[string]int64, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, ok := values[i].([]byte) + if !ok { + return nil, errors.New("redigo: Int64Map key not a bulk string value") + } + value, err := Int64(values[i+1], nil) + if err != nil { + return nil, err + } + m[string(key)] = value + } + return m, nil +} + +// Positions is a helper that converts an array of positions (lat, long) +// into a [][2]float64. The GEOPOS command returns replies in this format. +func Positions(result interface{}, err error) ([]*[2]float64, error) { + values, err := Values(result, err) + if err != nil { + return nil, err + } + positions := make([]*[2]float64, len(values)) + for i := range values { + if values[i] == nil { + continue + } + p, ok := values[i].([]interface{}) + if !ok { + return nil, fmt.Errorf("redigo: unexpected element type for interface slice, got type %T", values[i]) + } + if len(p) != 2 { + return nil, fmt.Errorf("redigo: unexpected number of values for a member position, got %d", len(p)) + } + lat, err := Float64(p[0], nil) + if err != nil { + return nil, err + } + long, err := Float64(p[1], nil) + if err != nil { + return nil, err + } + positions[i] = &[2]float64{lat, long} + } + return positions, nil +} + +// Uint64s is a helper that converts an array command reply to a []uint64. +// If err is not equal to nil, then Uint64s returns nil, err. Nil array +// items are stay nil. Uint64s returns an error if an array item is not a +// bulk string or nil. +func Uint64s(reply interface{}, err error) ([]uint64, error) { + var result []uint64 + err = sliceHelper(reply, err, "Uint64s", func(n int) { result = make([]uint64, n) }, func(i int, v interface{}) error { + switch v := v.(type) { + case uint64: + result[i] = v + return nil + case []byte: + n, err := strconv.ParseUint(string(v), 10, 64) + result[i] = n + return err + default: + return fmt.Errorf("redigo: unexpected element type for Uint64s, got type %T", v) + } + }) + return result, err +} + +// Uint64Map is a helper that converts an array of strings (alternating key, value) +// into a map[string]uint64. The HGETALL commands return replies in this format. +// Requires an even number of values in result. +func Uint64Map(result interface{}, err error) (map[string]uint64, error) { + values, err := Values(result, err) + if err != nil { + return nil, err + } + if len(values)%2 != 0 { + return nil, errors.New("redigo: Uint64Map expects even number of values result") + } + m := make(map[string]uint64, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, ok := values[i].([]byte) + if !ok { + return nil, errors.New("redigo: Uint64Map key not a bulk string value") + } + value, err := Uint64(values[i+1], nil) + if err != nil { + return nil, err + } + m[string(key)] = value + } + return m, nil +} + +// SlowLogs is a helper that parse the SLOWLOG GET command output and +// return the array of SlowLog +func SlowLogs(result interface{}, err error) ([]SlowLog, error) { + rawLogs, err := Values(result, err) + if err != nil { + return nil, err + } + logs := make([]SlowLog, len(rawLogs)) + for i, rawLog := range rawLogs { + rawLog, ok := rawLog.([]interface{}) + if !ok { + return nil, errors.New("redigo: slowlog element is not an array") + } + + var log SlowLog + + if len(rawLog) < 4 { + return nil, errors.New("redigo: slowlog element has less than four elements") + } + log.ID, ok = rawLog[0].(int64) + if !ok { + return nil, errors.New("redigo: slowlog element[0] not an int64") + } + timestamp, ok := rawLog[1].(int64) + if !ok { + return nil, errors.New("redigo: slowlog element[1] not an int64") + } + log.Time = time.Unix(timestamp, 0) + duration, ok := rawLog[2].(int64) + if !ok { + return nil, errors.New("redigo: slowlog element[2] not an int64") + } + log.ExecutionTime = time.Duration(duration) * time.Microsecond + + log.Args, err = Strings(rawLog[3], nil) + if err != nil { + return nil, fmt.Errorf("redigo: slowlog element[3] is not array of string. actual error is : %s", err.Error()) + } + if len(rawLog) >= 6 { + log.ClientAddr, err = String(rawLog[4], nil) + if err != nil { + return nil, fmt.Errorf("redigo: slowlog element[4] is not a string. actual error is : %s", err.Error()) + } + log.ClientName, err = String(rawLog[5], nil) + if err != nil { + return nil, fmt.Errorf("redigo: slowlog element[5] is not a string. actual error is : %s", err.Error()) + } + } + logs[i] = log + } + return logs, nil +} diff --git a/vendor/github.com/gomodule/redigo/redis/scan.go b/vendor/github.com/gomodule/redigo/redis/scan.go new file mode 100644 index 0000000000000000000000000000000000000000..a4cf20156c1f0364f00c0e1ace61be66614ba42a --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/scan.go @@ -0,0 +1,684 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "sync" +) + +var ( + scannerType = reflect.TypeOf((*Scanner)(nil)).Elem() +) + +func ensureLen(d reflect.Value, n int) { + if n > d.Cap() { + d.Set(reflect.MakeSlice(d.Type(), n, n)) + } else { + d.SetLen(n) + } +} + +func cannotConvert(d reflect.Value, s interface{}) error { + var sname string + switch s.(type) { + case string: + sname = "Redis simple string" + case Error: + sname = "Redis error" + case int64: + sname = "Redis integer" + case []byte: + sname = "Redis bulk string" + case []interface{}: + sname = "Redis array" + case nil: + sname = "Redis nil" + default: + sname = reflect.TypeOf(s).String() + } + return fmt.Errorf("cannot convert from %s to %s", sname, d.Type()) +} + +func convertAssignNil(d reflect.Value) (err error) { + switch d.Type().Kind() { + case reflect.Slice, reflect.Interface: + d.Set(reflect.Zero(d.Type())) + default: + err = cannotConvert(d, nil) + } + return err +} + +func convertAssignError(d reflect.Value, s Error) (err error) { + if d.Kind() == reflect.String { + d.SetString(string(s)) + } else if d.Kind() == reflect.Slice && d.Type().Elem().Kind() == reflect.Uint8 { + d.SetBytes([]byte(s)) + } else { + err = cannotConvert(d, s) + } + return +} + +func convertAssignString(d reflect.Value, s string) (err error) { + switch d.Type().Kind() { + case reflect.Float32, reflect.Float64: + var x float64 + x, err = strconv.ParseFloat(s, d.Type().Bits()) + d.SetFloat(x) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + var x int64 + x, err = strconv.ParseInt(s, 10, d.Type().Bits()) + d.SetInt(x) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + var x uint64 + x, err = strconv.ParseUint(s, 10, d.Type().Bits()) + d.SetUint(x) + case reflect.Bool: + var x bool + x, err = strconv.ParseBool(s) + d.SetBool(x) + case reflect.String: + d.SetString(s) + case reflect.Slice: + if d.Type().Elem().Kind() == reflect.Uint8 { + d.SetBytes([]byte(s)) + } else { + err = cannotConvert(d, s) + } + case reflect.Ptr: + err = convertAssignString(d.Elem(), s) + default: + err = cannotConvert(d, s) + } + return +} + +func convertAssignBulkString(d reflect.Value, s []byte) (err error) { + switch d.Type().Kind() { + case reflect.Slice: + // Handle []byte destination here to avoid unnecessary + // []byte -> string -> []byte converion. + if d.Type().Elem().Kind() == reflect.Uint8 { + d.SetBytes(s) + } else { + err = cannotConvert(d, s) + } + case reflect.Ptr: + if d.CanInterface() && d.CanSet() { + if s == nil { + if d.IsNil() { + return nil + } + + d.Set(reflect.Zero(d.Type())) + return nil + } + + if d.IsNil() { + d.Set(reflect.New(d.Type().Elem())) + } + + if sc, ok := d.Interface().(Scanner); ok { + return sc.RedisScan(s) + } + } + err = convertAssignString(d, string(s)) + default: + err = convertAssignString(d, string(s)) + } + return err +} + +func convertAssignInt(d reflect.Value, s int64) (err error) { + switch d.Type().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + d.SetInt(s) + if d.Int() != s { + err = strconv.ErrRange + d.SetInt(0) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if s < 0 { + err = strconv.ErrRange + } else { + x := uint64(s) + d.SetUint(x) + if d.Uint() != x { + err = strconv.ErrRange + d.SetUint(0) + } + } + case reflect.Bool: + d.SetBool(s != 0) + default: + err = cannotConvert(d, s) + } + return +} + +func convertAssignValue(d reflect.Value, s interface{}) (err error) { + if d.Kind() != reflect.Ptr { + if d.CanAddr() { + d2 := d.Addr() + if d2.CanInterface() { + if scanner, ok := d2.Interface().(Scanner); ok { + return scanner.RedisScan(s) + } + } + } + } else if d.CanInterface() { + // Already a reflect.Ptr + if d.IsNil() { + d.Set(reflect.New(d.Type().Elem())) + } + if scanner, ok := d.Interface().(Scanner); ok { + return scanner.RedisScan(s) + } + } + + switch s := s.(type) { + case nil: + err = convertAssignNil(d) + case []byte: + err = convertAssignBulkString(d, s) + case int64: + err = convertAssignInt(d, s) + case string: + err = convertAssignString(d, s) + case Error: + err = convertAssignError(d, s) + default: + err = cannotConvert(d, s) + } + return err +} + +func convertAssignArray(d reflect.Value, s []interface{}) error { + if d.Type().Kind() != reflect.Slice { + return cannotConvert(d, s) + } + ensureLen(d, len(s)) + for i := 0; i < len(s); i++ { + if err := convertAssignValue(d.Index(i), s[i]); err != nil { + return err + } + } + return nil +} + +func convertAssign(d interface{}, s interface{}) (err error) { + if scanner, ok := d.(Scanner); ok { + return scanner.RedisScan(s) + } + + // Handle the most common destination types using type switches and + // fall back to reflection for all other types. + switch s := s.(type) { + case nil: + // ignore + case []byte: + switch d := d.(type) { + case *string: + *d = string(s) + case *int: + *d, err = strconv.Atoi(string(s)) + case *bool: + *d, err = strconv.ParseBool(string(s)) + case *[]byte: + *d = s + case *interface{}: + *d = s + case nil: + // skip value + default: + if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { + err = cannotConvert(d, s) + } else { + err = convertAssignBulkString(d.Elem(), s) + } + } + case int64: + switch d := d.(type) { + case *int: + x := int(s) + if int64(x) != s { + err = strconv.ErrRange + x = 0 + } + *d = x + case *bool: + *d = s != 0 + case *interface{}: + *d = s + case nil: + // skip value + default: + if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { + err = cannotConvert(d, s) + } else { + err = convertAssignInt(d.Elem(), s) + } + } + case string: + switch d := d.(type) { + case *string: + *d = s + case *interface{}: + *d = s + case nil: + // skip value + default: + err = cannotConvert(reflect.ValueOf(d), s) + } + case []interface{}: + switch d := d.(type) { + case *[]interface{}: + *d = s + case *interface{}: + *d = s + case nil: + // skip value + default: + if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { + err = cannotConvert(d, s) + } else { + err = convertAssignArray(d.Elem(), s) + } + } + case Error: + err = s + default: + err = cannotConvert(reflect.ValueOf(d), s) + } + return +} + +// Scan copies from src to the values pointed at by dest. +// +// Scan uses RedisScan if available otherwise: +// +// The values pointed at by dest must be an integer, float, boolean, string, +// []byte, interface{} or slices of these types. Scan uses the standard strconv +// package to convert bulk strings to numeric and boolean types. +// +// If a dest value is nil, then the corresponding src value is skipped. +// +// If a src element is nil, then the corresponding dest value is not modified. +// +// To enable easy use of Scan in a loop, Scan returns the slice of src +// following the copied values. +func Scan(src []interface{}, dest ...interface{}) ([]interface{}, error) { + if len(src) < len(dest) { + return nil, errors.New("redigo.Scan: array short") + } + var err error + for i, d := range dest { + err = convertAssign(d, src[i]) + if err != nil { + err = fmt.Errorf("redigo.Scan: cannot assign to dest %d: %v", i, err) + break + } + } + return src[len(dest):], err +} + +type fieldSpec struct { + name string + index []int + omitEmpty bool +} + +type structSpec struct { + m map[string]*fieldSpec + l []*fieldSpec +} + +func (ss *structSpec) fieldSpec(name []byte) *fieldSpec { + return ss.m[string(name)] +} + +func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) { +LOOP: + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + switch { + case f.PkgPath != "" && !f.Anonymous: + // Ignore unexported fields. + case f.Anonymous: + switch f.Type.Kind() { + case reflect.Struct: + compileStructSpec(f.Type, depth, append(index, i), ss) + case reflect.Ptr: + // TODO(steve): Protect against infinite recursion. + if f.Type.Elem().Kind() == reflect.Struct { + compileStructSpec(f.Type.Elem(), depth, append(index, i), ss) + } + } + default: + fs := &fieldSpec{name: f.Name} + tag := f.Tag.Get("redis") + + var ( + p string + ) + first := true + for len(tag) > 0 { + i := strings.IndexByte(tag, ',') + if i < 0 { + p, tag = tag, "" + } else { + p, tag = tag[:i], tag[i+1:] + } + if p == "-" { + continue LOOP + } + if first && len(p) > 0 { + fs.name = p + first = false + } else { + switch p { + case "omitempty": + fs.omitEmpty = true + default: + panic(fmt.Errorf("redigo: unknown field tag %s for type %s", p, t.Name())) + } + } + } + d, found := depth[fs.name] + if !found { + d = 1 << 30 + } + switch { + case len(index) == d: + // At same depth, remove from result. + delete(ss.m, fs.name) + j := 0 + for i := 0; i < len(ss.l); i++ { + if fs.name != ss.l[i].name { + ss.l[j] = ss.l[i] + j += 1 + } + } + ss.l = ss.l[:j] + case len(index) < d: + fs.index = make([]int, len(index)+1) + copy(fs.index, index) + fs.index[len(index)] = i + depth[fs.name] = len(index) + ss.m[fs.name] = fs + ss.l = append(ss.l, fs) + } + } + } +} + +var ( + structSpecMutex sync.RWMutex + structSpecCache = make(map[reflect.Type]*structSpec) + defaultFieldSpec = &fieldSpec{} +) + +func structSpecForType(t reflect.Type) *structSpec { + + structSpecMutex.RLock() + ss, found := structSpecCache[t] + structSpecMutex.RUnlock() + if found { + return ss + } + + structSpecMutex.Lock() + defer structSpecMutex.Unlock() + ss, found = structSpecCache[t] + if found { + return ss + } + + ss = &structSpec{m: make(map[string]*fieldSpec)} + compileStructSpec(t, make(map[string]int), nil, ss) + structSpecCache[t] = ss + return ss +} + +var errScanStructValue = errors.New("redigo.ScanStruct: value must be non-nil pointer to a struct") + +// ScanStruct scans alternating names and values from src to a struct. The +// HGETALL and CONFIG GET commands return replies in this format. +// +// ScanStruct uses exported field names to match values in the response. Use +// 'redis' field tag to override the name: +// +// Field int `redis:"myName"` +// +// Fields with the tag redis:"-" are ignored. +// +// Each field uses RedisScan if available otherwise: +// Integer, float, boolean, string and []byte fields are supported. Scan uses the +// standard strconv package to convert bulk string values to numeric and +// boolean types. +// +// If a src element is nil, then the corresponding field is not modified. +func ScanStruct(src []interface{}, dest interface{}) error { + d := reflect.ValueOf(dest) + if d.Kind() != reflect.Ptr || d.IsNil() { + return errScanStructValue + } + d = d.Elem() + if d.Kind() != reflect.Struct { + return errScanStructValue + } + ss := structSpecForType(d.Type()) + + if len(src)%2 != 0 { + return errors.New("redigo.ScanStruct: number of values not a multiple of 2") + } + + for i := 0; i < len(src); i += 2 { + s := src[i+1] + if s == nil { + continue + } + name, ok := src[i].([]byte) + if !ok { + return fmt.Errorf("redigo.ScanStruct: key %d not a bulk string value", i) + } + fs := ss.fieldSpec(name) + if fs == nil { + continue + } + if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil { + return fmt.Errorf("redigo.ScanStruct: cannot assign field %s: %v", fs.name, err) + } + } + return nil +} + +var ( + errScanSliceValue = errors.New("redigo.ScanSlice: dest must be non-nil pointer to a struct") +) + +// ScanSlice scans src to the slice pointed to by dest. +// +// If the target is a slice of types which implement Scanner then the custom +// RedisScan method is used otherwise the following rules apply: +// +// The elements in the dest slice must be integer, float, boolean, string, struct +// or pointer to struct values. +// +// Struct fields must be integer, float, boolean or string values. All struct +// fields are used unless a subset is specified using fieldNames. +func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error { + d := reflect.ValueOf(dest) + if d.Kind() != reflect.Ptr || d.IsNil() { + return errScanSliceValue + } + d = d.Elem() + if d.Kind() != reflect.Slice { + return errScanSliceValue + } + + isPtr := false + t := d.Type().Elem() + st := t + if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct { + isPtr = true + t = t.Elem() + } + + if t.Kind() != reflect.Struct || st.Implements(scannerType) { + ensureLen(d, len(src)) + for i, s := range src { + if s == nil { + continue + } + if err := convertAssignValue(d.Index(i), s); err != nil { + return fmt.Errorf("redigo.ScanSlice: cannot assign element %d: %v", i, err) + } + } + return nil + } + + ss := structSpecForType(t) + fss := ss.l + if len(fieldNames) > 0 { + fss = make([]*fieldSpec, len(fieldNames)) + for i, name := range fieldNames { + fss[i] = ss.m[name] + if fss[i] == nil { + return fmt.Errorf("redigo.ScanSlice: ScanSlice bad field name %s", name) + } + } + } + + if len(fss) == 0 { + return errors.New("redigo.ScanSlice: no struct fields") + } + + n := len(src) / len(fss) + if n*len(fss) != len(src) { + return errors.New("redigo.ScanSlice: length not a multiple of struct field count") + } + + ensureLen(d, n) + for i := 0; i < n; i++ { + d := d.Index(i) + if isPtr { + if d.IsNil() { + d.Set(reflect.New(t)) + } + d = d.Elem() + } + for j, fs := range fss { + s := src[i*len(fss)+j] + if s == nil { + continue + } + if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil { + return fmt.Errorf("redigo.ScanSlice: cannot assign element %d to field %s: %v", i*len(fss)+j, fs.name, err) + } + } + } + return nil +} + +// Args is a helper for constructing command arguments from structured values. +type Args []interface{} + +// Add returns the result of appending value to args. +func (args Args) Add(value ...interface{}) Args { + return append(args, value...) +} + +// AddFlat returns the result of appending the flattened value of v to args. +// +// Maps are flattened by appending the alternating keys and map values to args. +// +// Slices are flattened by appending the slice elements to args. +// +// Structs are flattened by appending the alternating names and values of +// exported fields to args. If v is a nil struct pointer, then nothing is +// appended. The 'redis' field tag overrides struct field names. See ScanStruct +// for more information on the use of the 'redis' field tag. +// +// Other types are appended to args as is. +func (args Args) AddFlat(v interface{}) Args { + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.Struct: + args = flattenStruct(args, rv) + case reflect.Slice: + for i := 0; i < rv.Len(); i++ { + args = append(args, rv.Index(i).Interface()) + } + case reflect.Map: + for _, k := range rv.MapKeys() { + args = append(args, k.Interface(), rv.MapIndex(k).Interface()) + } + case reflect.Ptr: + if rv.Type().Elem().Kind() == reflect.Struct { + if !rv.IsNil() { + args = flattenStruct(args, rv.Elem()) + } + } else { + args = append(args, v) + } + default: + args = append(args, v) + } + return args +} + +func flattenStruct(args Args, v reflect.Value) Args { + ss := structSpecForType(v.Type()) + for _, fs := range ss.l { + fv := v.FieldByIndex(fs.index) + if fs.omitEmpty { + var empty = false + switch fv.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + empty = fv.Len() == 0 + case reflect.Bool: + empty = !fv.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + empty = fv.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + empty = fv.Uint() == 0 + case reflect.Float32, reflect.Float64: + empty = fv.Float() == 0 + case reflect.Interface, reflect.Ptr: + empty = fv.IsNil() + } + if empty { + continue + } + } + if arg, ok := fv.Interface().(Argument); ok { + args = append(args, fs.name, arg.RedisArg()) + } else if fv.Kind() == reflect.Ptr { + if !fv.IsNil() { + args = append(args, fs.name, fv.Elem().Interface()) + } + } else { + args = append(args, fs.name, fv.Interface()) + } + } + return args +} diff --git a/vendor/github.com/gomodule/redigo/redis/script.go b/vendor/github.com/gomodule/redigo/redis/script.go new file mode 100644 index 0000000000000000000000000000000000000000..0ef1c821f7144bd86c4dd24e7655b7141c60f5b6 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/script.go @@ -0,0 +1,91 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "crypto/sha1" + "encoding/hex" + "io" + "strings" +) + +// Script encapsulates the source, hash and key count for a Lua script. See +// http://redis.io/commands/eval for information on scripts in Redis. +type Script struct { + keyCount int + src string + hash string +} + +// NewScript returns a new script object. If keyCount is greater than or equal +// to zero, then the count is automatically inserted in the EVAL command +// argument list. If keyCount is less than zero, then the application supplies +// the count as the first value in the keysAndArgs argument to the Do, Send and +// SendHash methods. +func NewScript(keyCount int, src string) *Script { + h := sha1.New() + io.WriteString(h, src) + return &Script{keyCount, src, hex.EncodeToString(h.Sum(nil))} +} + +func (s *Script) args(spec string, keysAndArgs []interface{}) []interface{} { + var args []interface{} + if s.keyCount < 0 { + args = make([]interface{}, 1+len(keysAndArgs)) + args[0] = spec + copy(args[1:], keysAndArgs) + } else { + args = make([]interface{}, 2+len(keysAndArgs)) + args[0] = spec + args[1] = s.keyCount + copy(args[2:], keysAndArgs) + } + return args +} + +// Hash returns the script hash. +func (s *Script) Hash() string { + return s.hash +} + +// Do evaluates the script. Under the covers, Do optimistically evaluates the +// script using the EVALSHA command. If the command fails because the script is +// not loaded, then Do evaluates the script using the EVAL command (thus +// causing the script to load). +func (s *Script) Do(c Conn, keysAndArgs ...interface{}) (interface{}, error) { + v, err := c.Do("EVALSHA", s.args(s.hash, keysAndArgs)...) + if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") { + v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...) + } + return v, err +} + +// SendHash evaluates the script without waiting for the reply. The script is +// evaluated with the EVALSHA command. The application must ensure that the +// script is loaded by a previous call to Send, Do or Load methods. +func (s *Script) SendHash(c Conn, keysAndArgs ...interface{}) error { + return c.Send("EVALSHA", s.args(s.hash, keysAndArgs)...) +} + +// Send evaluates the script without waiting for the reply. +func (s *Script) Send(c Conn, keysAndArgs ...interface{}) error { + return c.Send("EVAL", s.args(s.src, keysAndArgs)...) +} + +// Load loads the script without evaluating it. +func (s *Script) Load(c Conn) error { + _, err := c.Do("SCRIPT", "LOAD", s.src) + return err +} diff --git a/vendor/github.com/xlstudio/wxbizdatacrypt/.gitignore b/vendor/github.com/xlstudio/wxbizdatacrypt/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..daf913b1b347aae6de6f48d599bc89ef8c8693d6 --- /dev/null +++ b/vendor/github.com/xlstudio/wxbizdatacrypt/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/xlstudio/wxbizdatacrypt/LICENSE b/vendor/github.com/xlstudio/wxbizdatacrypt/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..8dada3edaf50dbc082c9a125058f25def75e625a --- /dev/null +++ b/vendor/github.com/xlstudio/wxbizdatacrypt/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/xlstudio/wxbizdatacrypt/README.md b/vendor/github.com/xlstudio/wxbizdatacrypt/README.md new file mode 100644 index 0000000000000000000000000000000000000000..2dc8094db897d2f4e73026ac3b1f0ace57ee366a --- /dev/null +++ b/vendor/github.com/xlstudio/wxbizdatacrypt/README.md @@ -0,0 +1,48 @@ +### 微信小程序加密数据解密算法Go语言版 + +**微信小程序开发官方说明文档 - 签名加密篇文档链接** + +***1. 最早的文档链接(已跳转,可能随时会失效)*** +> http://mp.weixin.qq.com/debug/wxadoc/dev/api/signature.html?t=20161107 + +***2. 旧文档链接(已暂停更新,可能随时会失效)*** +> https://developers.weixin.qq.com/miniprogram/dev/api/signature.html + +***3. 新文档链接(当前官方最新文档)*** +> https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html + +**使用方法** + +> go get github.com/xlstudio/wxbizdatacrypt + +**引入方法** +```Go +import ( + "github.com/xlstudio/wxbizdatacrypt" +) +``` +**使用示例** + +```Go +package main + +import ( + "fmt" + "github.com/xlstudio/wxbizdatacrypt" +) + +func main() { + appId := "wx4f4bc4dec97d474b" + sessionKey := "tiihtNczf5v6AKRyjwEUhQ==" + encryptedData := "CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZMQmRzooG2xrDcvSnxIMXFufNstNGTyaGS9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+3hVbJSRgv+4lGOETKUQz6OYStslQ142dNCuabNPGBzlooOmB231qMM85d2/fV6ChevvXvQP8Hkue1poOFtnEtpyxVLW1zAo6/1Xx1COxFvrc2d7UL/lmHInNlxuacJXwu0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn/Hz7saL8xz+W//FRAUid1OksQaQx4CMs8LOddcQhULW4ucetDf96JcR3g0gfRK4PC7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns/8wR2SiRS7MNACwTyrGvt9ts8p12PKFdlqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYVoKlaRv85IfVunYzO0IKXsyl7JCUjCpoG20f0a04COwfneQAGGwd5oa+T8yO5hzuyDb/XcxxmK01EpqOyuxINew==" + iv := "r7BXXKkLb8qrSNn05n0qiA==" + + pc := wxbizdatacrypt.WxBizDataCrypt{AppId: appId, SessionKey: sessionKey} + result, err := pc.Decrypt(encryptedData, iv, true) //第三个参数解释: 需要返回 JSON 数据类型时 使用 true, 需要返回 map 数据类型时 使用 false + if err != nil { + fmt.Println(err) + } else { + fmt.Println(result) + } +} +``` \ No newline at end of file diff --git a/vendor/github.com/xlstudio/wxbizdatacrypt/go.mod b/vendor/github.com/xlstudio/wxbizdatacrypt/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..d54114785af9dced355de1ced8932544d9f2d914 --- /dev/null +++ b/vendor/github.com/xlstudio/wxbizdatacrypt/go.mod @@ -0,0 +1,3 @@ +module github.com/xlstudio/wxbizdatacrypt + +go 1.14 diff --git a/vendor/github.com/xlstudio/wxbizdatacrypt/wxbizdatacrypt.go b/vendor/github.com/xlstudio/wxbizdatacrypt/wxbizdatacrypt.go new file mode 100644 index 0000000000000000000000000000000000000000..06c5061c4ff73c95eef0c9f819bb74b0cddc3af9 --- /dev/null +++ b/vendor/github.com/xlstudio/wxbizdatacrypt/wxbizdatacrypt.go @@ -0,0 +1,102 @@ +package wxbizdatacrypt + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "regexp" + "strings" +) + +var errorCode = map[string]int{ + "illegalAppId": -41000, + "illegalAesKey": -41001, + "illegalIv": -41002, + "illegalBuffer": -41003, + "decodeBase64Error": -41004, + "decodeJsonError": -41005, +} + +// WxBizDataCrypt represents an active WxBizDataCrypt object +type WxBizDataCrypt struct { + AppId string + SessionKey string +} + +type showError struct { + errorCode int + errorMsg error +} + +func (e showError) Error() string { + return fmt.Sprintf("{code: %v, error: \"%v\"}", e.errorCode, e.errorMsg) +} + +// Decrypt Weixin APP's AES Data +// If isJSON is true, Decrypt return JSON type. +// If isJSON is false, Decrypt return map type. +func (wxCrypt *WxBizDataCrypt) Decrypt(encryptedData string, iv string, isJSON bool) (interface{}, error) { + sessionKey := strings.Replace(strings.TrimSpace(wxCrypt.SessionKey), " ", "+", -1) + if len(sessionKey) != 24 { + return nil, showError{errorCode["illegalAesKey"], errors.New("sessionKey length is error")} + } + aesKey, err := base64.StdEncoding.DecodeString(sessionKey) + if err != nil { + return nil, showError{errorCode["decodeBase64Error"], err} + } + iv = strings.Replace(strings.TrimSpace(iv), " ", "+", -1) + if len(iv) != 24 { + return nil, showError{errorCode["illegalIv"], errors.New("iv length is error")} + } + aesIv, err := base64.StdEncoding.DecodeString(iv) + if err != nil { + return nil, showError{errorCode["decodeBase64Error"], err} + } + encryptedData = strings.Replace(strings.TrimSpace(encryptedData), " ", "+", -1) + aesCipherText, err := base64.StdEncoding.DecodeString(encryptedData) + if err != nil { + return nil, showError{errorCode["decodeBase64Error"], err} + } + aesPlantText := make([]byte, len(aesCipherText)) + + aesBlock, err := aes.NewCipher(aesKey) + if err != nil { + return nil, showError{errorCode["illegalBuffer"], err} + } + + mode := cipher.NewCBCDecrypter(aesBlock, aesIv) + mode.CryptBlocks(aesPlantText, aesCipherText) + aesPlantText = PKCS7UnPadding(aesPlantText) + + var decrypted map[string]interface{} + + re := regexp.MustCompile(`[^\{]*(\{.*\})[^\}]*`) + aesPlantText = []byte(re.ReplaceAllString(string(aesPlantText), "$1")) + err = json.Unmarshal(aesPlantText, &decrypted) + if err != nil { + return nil, showError{errorCode["decodeJsonError"], err} + } + + if decrypted["watermark"].(map[string]interface{})["appid"] != wxCrypt.AppId { + return nil, showError{errorCode["illegalAppId"], errors.New("appId is not match")} + } + + if isJSON { + return string(aesPlantText), nil + } + + return decrypted, nil +} + +// PKCS7UnPadding return unpadding []Byte plantText +func PKCS7UnPadding(plantText []byte) []byte { + length := len(plantText) + if length > 0 { + unPadding := int(plantText[length-1]) + return plantText[:(length - unPadding)] + } + return plantText +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 819deab1c7b390af658b44d9a52e215c2218bbea..3acbccd28f18a9ab0d31e2e248ecf25d3c84b55a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -19,6 +19,8 @@ github.com/go-playground/validator/v10 github.com/go-sql-driver/mysql # github.com/golang/protobuf v1.3.3 github.com/golang/protobuf/proto +# github.com/gomodule/redigo v1.8.3 +github.com/gomodule/redigo/redis # github.com/jinzhu/gorm v1.9.16 github.com/jinzhu/gorm github.com/jinzhu/gorm/dialects/mysql @@ -36,6 +38,8 @@ github.com/modern-go/concurrent github.com/modern-go/reflect2 # github.com/ugorji/go/codec v1.1.7 github.com/ugorji/go/codec +# github.com/xlstudio/wxbizdatacrypt v1.0.2 +github.com/xlstudio/wxbizdatacrypt # go.uber.org/atomic v1.6.0 go.uber.org/atomic # go.uber.org/multierr v1.5.0