├── Dockerfile ├── README.md ├── cmd ├── db.go ├── db │ ├── createIndexes.go │ ├── empty.go │ └── listIndexes.go ├── root.go └── server.go ├── common.sh ├── config.json ├── controllers ├── account.go ├── response.go └── user.go ├── deploy-prod.sh ├── deploy-test.sh ├── deploy.sh ├── docker-build.sh ├── docker-compose.override.yml ├── docker-compose.prod.yml ├── docker-compose.test.yml ├── docker-compose.yml ├── fswatch.sh ├── main.go ├── middlewares ├── auth.go ├── context.go └── session.go ├── models ├── mongo.go ├── mongodbIndex.go ├── redis.go └── user.go ├── restart.sh ├── services ├── error.go ├── param.go └── user.go ├── supervisord.conf ├── test.sh ├── test ├── common.go ├── main_test.go └── user_test.go └── util ├── collection.go ├── http.go ├── string.go └── validator.go /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM daocloud.io/jaggerwang/go 2 | 3 | ENV APP_PATH=/go/src/zqc 4 | ENV DATA_PATH=/data 5 | 6 | ADD . $APP_PATH 7 | WORKDIR $APP_PATH 8 | 9 | RUN go get -d -v ./... 10 | RUN go install -v 11 | 12 | RUN go get github.com/smartystreets/goconvey 13 | 14 | VOLUME $DATA_PATH 15 | 16 | EXPOSE 1323 17 | 18 | CMD supervisord 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zaiqiuchang server demo 2 | 3 | [Zaiqiuchang(在球场)](https://www.zaiqiuchang.com) is a mobile app developed using React Native, both iOS and Android are supported. This project is the lite version of the api server, which developed in Go, and deploy using Docker. The lite version only include account related apis, but it should be a good start to develop your own. 4 | 5 | ### Packages 6 | 7 | |Package|Description| 8 | |-------|-----------| 9 | |[github.com/labstack/echo](https://echo.labstack.com/)|Http server and framework.| 10 | |[github.com/spf13/cobra](https://github.com/spf13/cobra)|CLI app framework.| 11 | |[github.com/spf13/viper](https://github.com/spf13/viper)|Manage app configuration.| 12 | |[github.com/asaskevich/govalidator](https://github.com/asaskevich/govalidator)|Validate input.| 13 | |[github.com/Sirupsen/logrus](https://github.com/Sirupsen/logrus)|Logging.| 14 | |[gopkg.in/mgo.v2](https://labix.org/mgo)|Mongodb driver.| 15 | |[github.com/garyburd/redigo/redis](https://github.com/garyburd/redigo/)|Redis driver.| 16 | |[github.com/gorilla/sessions](https://github.com/gorilla/sessions)|Manage user session.| 17 | |[github.com/smartystreets/goconvey/convey](https://github.com/smartystreets/goconvey)|Test framework.| 18 | 19 | ### How to deploy 20 | 21 | You need install [docker engine](https://docs.docker.com/engine/installation/) first. 22 | 23 | **run in dev mode with auto detecting code change** 24 | 25 | ``` 26 | > git clone git@github.com:jaggerwang/zqc-server-demo.git && cd zqc-server-demo 27 | > mkdir -p ~/data/projects/zqc-server-demo # create directory for data volumes 28 | > ./deploy.sh # pull images and run containers 29 | > ./fswatch.sh # watching code change, fswatch needed 30 | ``` 31 | 32 | The data and log of server, mongodb and redis will be saved at host's path "~/data/projects/zqc-server-demo", which mounted at container's path "/data". 33 | 34 | **run in prod mode** 35 | 36 | ``` 37 | > git clone git@github.com:jaggerwang/zqc-server-demo.git && cd zqc-server-demo 38 | > mkdir -p /data/zqc-server-demo # create directory for data volumes 39 | > ./deploy-prod.sh 40 | ``` 41 | 42 | The data and log of server, mongodb and redis will be saved at host's path "/data/zqc-server-demo", which mounted at container's path "/data". 43 | 44 | **run test** 45 | 46 | ``` 47 | > cd zqc-server-demo 48 | > ./test.sh 49 | ... 50 | 2017/02/11 16:01:03 parser.go:24: [passed]: zqc/test 51 | 2017/02/11 16:01:03 executor.go:69: Executor status: 'idle' 52 | ``` 53 | 54 | Script `tesh.sh` will run a new group of docker containers to run unittests, including server, mongodb and redis. The new group has separated volumes and ports, to avoid empty the existing data. We use `goconvey` to write and run unittests. It supplied a web console to show the test result, and will run test automatically when detected code change. 55 | 56 | Open url "http://localhost:10402/" to view test result. 57 | 58 | 59 | **build image of your own** 60 | 61 | ``` 62 | > cd zqc-server-demo 63 | > ./docker-build.sh 64 | ``` 65 | 66 | ### Command 67 | 68 | **help** 69 | 70 | ``` 71 | > cd zqc-server-demo 72 | > docker-compose -p zqc-server-demo exec server zqc 73 | Zai qiu chang app. 74 | 75 | Usage: 76 | zqc [command] 77 | 78 | Available Commands: 79 | db Database admin 80 | server Run server 81 | 82 | Flags: 83 | -c, --config string config file (default "./config.json") 84 | --dir.data string directory for saving runtime data 85 | --env string deployment environment 86 | --log.level string log filter level 87 | --mongodb.zqc.addrs string address of zqc db 88 | 89 | Use "zqc [command] --help" for more information about a command. 90 | ``` 91 | 92 | **create mongodb index** 93 | 94 | ``` 95 | > cd zqc-server-demo 96 | > docker-compose -p zqc-server-demo exec server zqc db createIndexes 97 | ``` 98 | When deploy, it will auto run this command to create mongodb index. So normally you do not need to do this by your own. 99 | 100 | ### API 101 | 102 | The server container exposed port 1323, and it mapped to port 10400 of the host. So you can use domain "http://localhost:10400" to access the following api. 103 | 104 | Path|Method|Description 105 | ----|------|----------- 106 | /register|POST|Register account. 107 | /login|GET|Login. 108 | /isLogined|GET|Check whether logined. 109 | /logout|GET|Logout. 110 | /account/edit|POST|Edit account profile. 111 | /account/info|GET|Get current account info. 112 | /user/info|GET|Get user info by id. 113 | /user/infos|GET|Get user info by ids. 114 | 115 | ### FAQ 116 | 117 | **How to change image repository?** 118 | 119 | > Search and replace all "daocloud.io/jaggerwang/zqc-server-demo" to your own. 120 | 121 | **How can I build the base images of this project, including go, mongodb and redis?** 122 | 123 | > The dockerfiles of the base images can be found at "https://github.com/jaggerwang/jw-dockerfiles". 124 | 125 | ### Other resources 126 | 127 | * [技术文章 - Go + Docker API服务开发和部署](https://jaggerwang.net/develop-and-deploy-api-service-with-go-and-docker-intro/) 128 | * [在球场官网](https://www.zaiqiuchang.com) 129 | -------------------------------------------------------------------------------- /cmd/db.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Jagger Wang 2 | 3 | package cmd 4 | 5 | import ( 6 | "github.com/spf13/cobra" 7 | 8 | "zqc/cmd/db" 9 | ) 10 | 11 | var dbCmd = &cobra.Command{ 12 | Use: "db", 13 | Short: "Database admin", 14 | Long: `Database admin.`, 15 | } 16 | 17 | func init() { 18 | dbCmd.AddCommand(db.CreateIndexesCmd) 19 | dbCmd.AddCommand(db.ListIndexesCmd) 20 | dbCmd.AddCommand(db.EmptyCmd) 21 | } 22 | -------------------------------------------------------------------------------- /cmd/db/createIndexes.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Jagger Wang 2 | 3 | package db 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "zqc/models" 11 | ) 12 | 13 | var createIndexesFlags struct { 14 | cluster string 15 | db string 16 | coll string 17 | pos int 18 | } 19 | 20 | func init() { 21 | CreateIndexesCmd.Flags().StringVar(&createIndexesFlags.cluster, "cluster", "zqc", "which cluster") 22 | CreateIndexesCmd.Flags().StringVar(&createIndexesFlags.db, "db", "zqc", "which db") 23 | CreateIndexesCmd.Flags().StringVar(&createIndexesFlags.coll, "coll", "", "which collection, empty means all collections in db") 24 | CreateIndexesCmd.Flags().IntVar(&createIndexesFlags.pos, "pos", -1, "which index, postion in index array, -1 means all") 25 | } 26 | 27 | var CreateIndexesCmd = &cobra.Command{ 28 | Use: "createIndexes", 29 | Short: "Create indexes", 30 | Long: `Create indexes.`, 31 | RunE: func(cmd *cobra.Command, args []string) error { 32 | err := models.CreateDBIndexes(createIndexesFlags.cluster, createIndexesFlags.db, createIndexesFlags.coll, createIndexesFlags.pos) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | fmt.Println("create indexes ok") 38 | return nil 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /cmd/db/empty.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Jagger Wang 2 | 3 | package db 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "zqc/models" 11 | ) 12 | 13 | var emptyFlags struct { 14 | cluster string 15 | db string 16 | coll string 17 | } 18 | 19 | func init() { 20 | EmptyCmd.Flags().StringVar(&emptyFlags.cluster, "cluster", "zqc", "which cluster") 21 | EmptyCmd.Flags().StringVar(&emptyFlags.db, "db", "zqc", "which db") 22 | EmptyCmd.Flags().StringVar(&emptyFlags.coll, "coll", "", "which collection, empty means all in db") 23 | } 24 | 25 | var EmptyCmd = &cobra.Command{ 26 | Use: "empty", 27 | Short: "Empty all collections in db", 28 | Long: `Empty all collections in db.`, 29 | RunE: func(cmd *cobra.Command, args []string) error { 30 | err := models.EmptyDB(emptyFlags.cluster, emptyFlags.db, emptyFlags.coll) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | fmt.Println("empty db ok") 36 | return nil 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /cmd/db/listIndexes.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Jagger Wang 2 | 3 | package db 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/kr/pretty" 10 | "github.com/spf13/cobra" 11 | 12 | "zqc/models" 13 | ) 14 | 15 | var listIndexesFlags struct { 16 | cluster string 17 | db string 18 | coll string 19 | required bool 20 | } 21 | 22 | func init() { 23 | ListIndexesCmd.Flags().StringVar(&listIndexesFlags.cluster, "cluster", "zqc", "which cluster") 24 | ListIndexesCmd.Flags().StringVar(&listIndexesFlags.db, "db", "zqc", "which db") 25 | ListIndexesCmd.Flags().StringVar(&listIndexesFlags.coll, "coll", "", "which collection, empty means all in db") 26 | ListIndexesCmd.Flags().BoolVar(&listIndexesFlags.required, "required", false, "if true list required indexes, else list exist indexes") 27 | } 28 | 29 | var ListIndexesCmd = &cobra.Command{ 30 | Use: "listIndexes", 31 | Short: "List indexes", 32 | Long: `List indexes.`, 33 | RunE: func(cmd *cobra.Command, args []string) error { 34 | if listIndexesFlags.required { 35 | if listIndexesFlags.db == "zqc" { 36 | fmt.Printf("%# v\n", pretty.Formatter(models.ZqcDBIndexes)) 37 | } else { 38 | return errors.New("unknown db") 39 | } 40 | } else { 41 | collNames, err := models.DBCollNames(listIndexesFlags.cluster, listIndexesFlags.db) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | for _, collName := range collNames { 47 | if listIndexesFlags.coll == "" || listIndexesFlags.coll == collName { 48 | coll, err := models.NewMongoColl(listIndexesFlags.cluster, listIndexesFlags.db, collName) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | indexes, err := coll.Indexes() 54 | if err != nil { 55 | return err 56 | } 57 | fmt.Printf("%# v\n", pretty.Formatter(indexes)) 58 | } 59 | } 60 | } 61 | 62 | return nil 63 | }, 64 | } 65 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Jagger Wang 2 | 3 | package cmd 4 | 5 | import ( 6 | "encoding/gob" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | log "github.com/Sirupsen/logrus" 12 | "github.com/fsnotify/fsnotify" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/viper" 15 | "gopkg.in/mgo.v2/bson" 16 | 17 | "zqc/services" 18 | ) 19 | 20 | var rootFlags struct { 21 | cfgFile string 22 | } 23 | 24 | var rootCmd = &cobra.Command{ 25 | Use: "zqc", 26 | Short: "Zai qiu chang app", 27 | Long: `Zai qiu chang app.`, 28 | } 29 | 30 | func Execute() { 31 | if err := rootCmd.Execute(); err != nil { 32 | fmt.Println(err) 33 | os.Exit(-1) 34 | } 35 | } 36 | 37 | func init() { 38 | cobra.OnInitialize(initConfig, initLog) 39 | 40 | rootCmd.PersistentFlags().StringVarP(&rootFlags.cfgFile, "config", "c", "./config.json", "config file") 41 | rootCmd.PersistentFlags().String("env", "", "deployment environment") 42 | rootCmd.PersistentFlags().String("dir.data", "", "directory for saving runtime data") 43 | rootCmd.PersistentFlags().String("log.level", "", "log filter level") 44 | rootCmd.PersistentFlags().String("mongodb.zqc.addrs", "", "address of zqc db") 45 | 46 | viper.BindPFlags(rootCmd.PersistentFlags()) 47 | 48 | rootCmd.AddCommand(serverCmd) 49 | rootCmd.AddCommand(dbCmd) 50 | 51 | registerGobTypes() 52 | } 53 | 54 | func initConfig() { 55 | if e := os.Getenv("ZQC_CONFIG_FILE"); e != "" { 56 | rootFlags.cfgFile = e 57 | } 58 | viper.SetConfigFile(rootFlags.cfgFile) 59 | err := viper.ReadInConfig() 60 | if err != nil { 61 | panic(err) 62 | } 63 | fmt.Println("using config file", viper.ConfigFileUsed()) 64 | 65 | viper.WatchConfig() 66 | viper.OnConfigChange(func(e fsnotify.Event) { 67 | fmt.Println("config changed") 68 | }) 69 | } 70 | 71 | func initLog() { 72 | log.SetFormatter(&log.JSONFormatter{}) 73 | 74 | level, err := log.ParseLevel(viper.GetString("log.level")) 75 | if err != nil { 76 | panic(err) 77 | } 78 | log.SetLevel(level) 79 | 80 | w, err := os.OpenFile(filepath.Join(viper.GetString("dir.data"), viper.GetString("log.app.file")), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0640) 81 | if err != nil { 82 | panic(err) 83 | } 84 | log.SetOutput(w) 85 | } 86 | 87 | func registerGobTypes() { 88 | gob.Register(bson.NewObjectId()) 89 | gob.Register(services.User{}) 90 | } 91 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Jagger Wang 2 | 3 | package cmd 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | valid "github.com/asaskevich/govalidator" 11 | "github.com/labstack/echo" 12 | "github.com/labstack/echo/middleware" 13 | "github.com/labstack/gommon/log" 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/viper" 16 | 17 | "zqc/controllers" 18 | "zqc/middlewares" 19 | ) 20 | 21 | var serverCmd = &cobra.Command{ 22 | Use: "server", 23 | Short: "Run server", 24 | Long: `Run server.`, 25 | Run: func(cmd *cobra.Command, args []string) { 26 | addr := viper.GetString("server.listenAddr") 27 | uploadDir := viper.GetString("storage.local.dir") 28 | 29 | if _, err := os.Stat(uploadDir); os.IsNotExist(err) { 30 | os.Mkdir(uploadDir, 0755) 31 | } 32 | 33 | e := echo.New() 34 | e.Debug = viper.GetBool("server.debug") 35 | e.HTTPErrorHandler = controllers.ErrorHandler 36 | 37 | initEchoLog(e) 38 | 39 | addMiddlewares(e) 40 | 41 | addRoutes(e) 42 | 43 | e.Logger.Info("server pid ", os.Getpid()) 44 | e.Logger.Info("server listening on ", addr) 45 | e.Logger.Fatal(e.Start(addr)) 46 | }, 47 | } 48 | 49 | func init() { 50 | serverCmd.Flags().StringP("server.listenAddr", "l", "", "server listen address") 51 | serverCmd.Flags().Bool("server.debug", false, "enable/disable server debug mode") 52 | 53 | viper.BindPFlags(serverCmd.Flags()) 54 | 55 | valid.SetFieldsRequiredByDefault(true) 56 | } 57 | 58 | func initEchoLog(e *echo.Echo) { 59 | w, err := os.OpenFile(filepath.Join(viper.GetString("dir.data"), viper.GetString("log.echo.file")), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0640) 60 | if err != nil { 61 | panic(err) 62 | } 63 | e.Logger.SetOutput(w) 64 | 65 | var lvl log.Lvl 66 | switch strings.ToUpper(viper.GetString("log.level")) { 67 | case "DEBUG": 68 | lvl = log.DEBUG 69 | case "INFO": 70 | lvl = log.INFO 71 | case "WARN": 72 | lvl = log.WARN 73 | case "ERROR": 74 | lvl = log.ERROR 75 | case "OFF": 76 | lvl = log.OFF 77 | default: 78 | lvl = log.INFO 79 | } 80 | e.Logger.SetLevel(lvl) 81 | } 82 | 83 | func addMiddlewares(e *echo.Echo) { 84 | e.Pre(middleware.RemoveTrailingSlash()) 85 | 86 | e.Use(middleware.Recover()) 87 | 88 | lcfg := middleware.DefaultLoggerConfig 89 | w, err := os.OpenFile(filepath.Join(viper.GetString("dir.data"), viper.GetString("log.request.file")), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0640) 90 | if err != nil { 91 | panic(err) 92 | } 93 | lcfg.Output = w 94 | e.Use(middleware.LoggerWithConfig(lcfg)) 95 | 96 | e.Use(middlewares.Session()) 97 | 98 | e.Use(middlewares.MiddlewareContext()) 99 | } 100 | 101 | func addRoutes(e *echo.Echo) { 102 | auth := middlewares.Auth() 103 | 104 | e.POST("/register", controllers.RegisterAccount) 105 | e.GET("/login", controllers.Login) 106 | e.GET("/isLogined", controllers.IsLogined) 107 | e.GET("/logout", controllers.Logout) 108 | 109 | var g *echo.Group 110 | 111 | g = e.Group("/account", auth) 112 | g.POST("/edit", controllers.EditAccount) 113 | g.GET("/info", controllers.AccountInfo) 114 | 115 | g = e.Group("/user", auth) 116 | g.GET("/info", controllers.UserInfo) 117 | g.GET("/infos", controllers.UserInfos) 118 | } 119 | -------------------------------------------------------------------------------- /common.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | log() { 4 | level=$1 5 | message=$2 6 | echo "`hostname` `date +'%Y-%m-%d %H:%M:%S'` $1 $2" 7 | } 8 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "production", 3 | "dir": { 4 | "data": "/data" 5 | }, 6 | "server": { 7 | "listenAddr": ":1323", 8 | "debug": false 9 | }, 10 | "session": { 11 | "name": "zqc", 12 | "maxAge": 2592000, 13 | "maxLength": 1048576, 14 | "keyPrefix": "session_", 15 | "secretKey": "!cL5H#!vihAxdDqdinfC39&Hen8K8#n$" 16 | }, 17 | "log": { 18 | "level": "info", 19 | "echo": { 20 | "file": "echo.log" 21 | }, 22 | "request": { 23 | "file": "request.log" 24 | }, 25 | "app": { 26 | "file": "app.log" 27 | } 28 | }, 29 | "mongodb": { 30 | "zqc": { 31 | "addrs": "mongodb", 32 | "timeout": 5 33 | } 34 | }, 35 | "redis": { 36 | "zqc": { 37 | "address": "redis:6379", 38 | "timeout": { 39 | "connect": 3, 40 | "read": 5, 41 | "write": 5 42 | }, 43 | "password": "", 44 | "maxIdle": 100, 45 | "idleTimeout": 300 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /controllers/account.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | valid "github.com/asaskevich/govalidator" 7 | "github.com/labstack/echo" 8 | "gopkg.in/mgo.v2/bson" 9 | 10 | "zqc/middlewares" 11 | "zqc/services" 12 | ) 13 | 14 | func RegisterAccount(c echo.Context) (err error) { 15 | cc := c.(*middlewares.Context) 16 | params := struct { 17 | Mobile string `valid:"matches(^[0-9]{11}$)"` 18 | Password string `valid:"stringlength(6|20)"` 19 | Nickname string `valid:"stringlength(3|20)"` 20 | Gender string `valid:"stringlength(1|1)"` 21 | }{ 22 | Mobile: cc.FormValue("mobile"), 23 | Password: cc.FormValue("password"), 24 | Nickname: cc.FormValue("nickname"), 25 | Gender: cc.FormValue("gender"), 26 | } 27 | if ok, err := valid.ValidateStruct(params); !ok { 28 | return services.NewError(services.ErrCodeInvalidParams, err.Error()) 29 | } 30 | 31 | user, err := services.CreateUser(params.Mobile, params.Password, params.Nickname, params.Gender) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | return ResponseJSON(http.StatusOK, Response{ 37 | Data: map[string]interface{}{ 38 | "user": user, 39 | }, 40 | }, cc) 41 | } 42 | 43 | func Login(c echo.Context) (err error) { 44 | cc := c.(*middlewares.Context) 45 | params := struct { 46 | Mobile string `valid:"matches(^[0-9]{11}$)"` 47 | Password string `valid:"stringlength(6|20)"` 48 | }{ 49 | Mobile: cc.FormValue("mobile"), 50 | Password: cc.FormValue("password"), 51 | } 52 | if ok, err := valid.ValidateStruct(params); !ok { 53 | return services.NewError(services.ErrCodeInvalidParams, err.Error()) 54 | } 55 | 56 | user, err := services.GetUserByMobile(params.Mobile) 57 | if err != nil { 58 | return services.NewError(services.ErrCodeNotFound, "mobile not exist") 59 | } 60 | 61 | user, err = services.VerifyUserPassword(user.Id, params.Password) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | cc.SetSessionItem("userId", user.Id) 67 | 68 | return ResponseJSON(http.StatusOK, Response{ 69 | Data: map[string]interface{}{ 70 | "user": user, 71 | }, 72 | }, cc) 73 | } 74 | 75 | func IsLogined(c echo.Context) (err error) { 76 | cc := c.(*middlewares.Context) 77 | 78 | id := cc.SessionUserId() 79 | if id != "" { 80 | user, err := services.GetUser(id) 81 | if err != nil { 82 | return err 83 | } 84 | return ResponseJSON(http.StatusOK, Response{ 85 | Data: map[string]interface{}{ 86 | "user": user, 87 | }, 88 | }, cc) 89 | } else { 90 | return ResponseJSON(http.StatusOK, Response{}, cc) 91 | } 92 | 93 | } 94 | 95 | func Logout(c echo.Context) (err error) { 96 | cc := c.(*middlewares.Context) 97 | cc.DeleteSession() 98 | 99 | return ResponseJSON(http.StatusOK, Response{}, cc) 100 | } 101 | 102 | func EditAccount(c echo.Context) (err error) { 103 | cc := c.(*middlewares.Context) 104 | params := struct { 105 | Mobile string `valid:"matches(^[0-9]{11}$),optional"` 106 | Nickname string `valid:"stringlength(3|20),optional"` 107 | Gender string `valid:"matches(^m|f$),optional"` 108 | }{ 109 | Mobile: cc.FormValue("mobile"), 110 | Nickname: cc.FormValue("nickname"), 111 | Gender: cc.FormValue("gender"), 112 | } 113 | if ok, err := valid.ValidateStruct(¶ms); !ok { 114 | return services.NewError(services.ErrCodeInvalidParams, err.Error()) 115 | } 116 | 117 | formParams, err := cc.FormParams() 118 | if err != nil { 119 | return services.NewError(services.ErrCodeInvalidParams, err.Error()) 120 | } 121 | updateParams := bson.M{} 122 | if _, ok := formParams["mobile"]; ok { 123 | updateParams["mobile"] = params.Mobile 124 | } 125 | if _, ok := formParams["nickname"]; ok { 126 | updateParams["nickname"] = params.Nickname 127 | } 128 | if _, ok := formParams["gender"]; ok { 129 | updateParams["gender"] = params.Gender 130 | } 131 | 132 | id := cc.SessionUserId() 133 | user, err := services.UpdateUser(id, updateParams) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | return ResponseJSON(http.StatusOK, Response{ 139 | Data: map[string]interface{}{ 140 | "user": user, 141 | }, 142 | }, cc) 143 | } 144 | 145 | func AccountInfo(c echo.Context) (err error) { 146 | cc := c.(*middlewares.Context) 147 | 148 | id := cc.SessionUserId() 149 | user, err := services.GetUser(id) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | return ResponseJSON(http.StatusOK, Response{ 155 | Data: map[string]interface{}{ 156 | "user": user, 157 | }, 158 | }, cc) 159 | } 160 | -------------------------------------------------------------------------------- /controllers/response.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | log "github.com/Sirupsen/logrus" 7 | "github.com/gorilla/sessions" 8 | "github.com/labstack/echo" 9 | 10 | "zqc/services" 11 | ) 12 | 13 | type Response struct { 14 | Code int `json:"code"` 15 | Message string `json:"message"` 16 | Context interface{} `json:"context"` 17 | Data interface{} `json:"data"` 18 | } 19 | 20 | func ResponseJSON(status int, resp Response, c echo.Context) (err error) { 21 | if session, ok := c.Get("session").(*sessions.Session); ok { 22 | if session.IsNew || c.Get("sessionModified") != nil { 23 | if err := session.Save(c.Request(), c.Response()); err != nil { 24 | log.Error(err) 25 | } 26 | } 27 | } 28 | 29 | return c.JSON(status, resp) 30 | } 31 | 32 | func ErrorHandler(err error, c echo.Context) { 33 | status := http.StatusOK 34 | code := services.ErrCodeFail 35 | var message string 36 | var context interface{} 37 | 38 | if c.Echo().Debug { 39 | message = err.Error() 40 | } 41 | 42 | if he, ok := err.(*echo.HTTPError); ok { 43 | status = he.Code 44 | code = services.ErrCodeHttp 45 | message = he.Error() 46 | } else if se, ok := err.(*services.Error); ok { 47 | code = se.Code 48 | if c.Echo().Debug { 49 | message = se.Message 50 | context = se.Context 51 | } else { 52 | message = services.ErrMessages[code] 53 | } 54 | } 55 | 56 | if !c.Response().Committed { 57 | ResponseJSON(status, Response{ 58 | Code: code, 59 | Message: message, 60 | Context: context, 61 | }, c) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /controllers/user.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | valid "github.com/asaskevich/govalidator" 7 | "github.com/labstack/echo" 8 | 9 | "zqc/middlewares" 10 | "zqc/services" 11 | ) 12 | 13 | func UserInfo(c echo.Context) (err error) { 14 | cc := c.(*middlewares.Context) 15 | params := struct { 16 | Id string `valid:"objectidhex"` 17 | }{ 18 | Id: cc.FormValue("id"), 19 | } 20 | if ok, err := valid.ValidateStruct(params); !ok { 21 | return services.NewError(services.ErrCodeInvalidParams, err.Error()) 22 | } 23 | id, err := services.ParseObjectId(params.Id) 24 | if err != nil { 25 | return services.NewError(services.ErrCodeInvalidParams, err.Error()) 26 | } 27 | 28 | user, err := services.GetUser(id) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | return ResponseJSON(http.StatusOK, Response{ 34 | Data: map[string]interface{}{ 35 | "user": user, 36 | }, 37 | }, cc) 38 | } 39 | 40 | func UserInfos(c echo.Context) (err error) { 41 | cc := c.(*middlewares.Context) 42 | params := struct { 43 | Ids string `valid:"stringlength(24|2400)"` 44 | }{ 45 | Ids: cc.FormValue("ids"), 46 | } 47 | if ok, err := valid.ValidateStruct(params); !ok { 48 | return services.NewError(services.ErrCodeInvalidParams, err.Error()) 49 | } 50 | ids, err := services.ParseObjectIds(params.Ids) 51 | if err != nil { 52 | return services.NewError(services.ErrCodeInvalidParams, err.Error()) 53 | } 54 | 55 | users, err := services.GetUsers(ids) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | return ResponseJSON(http.StatusOK, Response{ 61 | Data: map[string]interface{}{ 62 | "users": users, 63 | }, 64 | }, cc) 65 | } 66 | -------------------------------------------------------------------------------- /deploy-prod.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source ./common.sh 4 | 5 | log INFO "deploy production begin ..." 6 | docker-compose pull 7 | docker-compose -p zqc-server-demo -f docker-compose.yml -f docker-compose.prod.yml up -d 8 | log INFO "deploy production end" 9 | 10 | log INFO "create db indexes begin ..." 11 | docker-compose -p zqc-server-demo exec server zqc db createIndexes 12 | log INFO "create db indexes end" 13 | 14 | exit 0 15 | -------------------------------------------------------------------------------- /deploy-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source ./common.sh 4 | 5 | log INFO "deploy test begin ..." 6 | docker-compose pull 7 | docker-compose -p zqc-server-demo-test -f docker-compose.yml -f docker-compose.test.yml up -d 8 | log INFO "deploy test end" 9 | 10 | log INFO "empty db begin ..." 11 | docker-compose -p zqc-server-demo-test exec server zqc db empty 12 | log INFO "empty db end" 13 | 14 | log INFO "create db indexes begin ..." 15 | docker-compose -p zqc-server-demo-test exec server zqc db createIndexes 16 | log INFO "create db indexes end" 17 | 18 | exit 0 19 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source ./common.sh 4 | 5 | log INFO "deploy development begin ..." 6 | docker-compose pull 7 | docker-compose -p zqc-server-demo up -d 8 | log INFO "deploy development end" 9 | 10 | log INFO "create db indexes begin ..." 11 | docker-compose -p zqc-server-demo exec server zqc db createIndexes 12 | log INFO "create db indexes end" 13 | 14 | exit 0 15 | -------------------------------------------------------------------------------- /docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source ./common.sh 4 | 5 | log INFO "docker build begin ..." 6 | docker build -t daocloud.io/jaggerwang/zqc-server-demo . 7 | if [[ $? != 0 ]]; then 8 | log ERROR "docker build failed" 9 | exit 1 10 | fi 11 | log INFO "docker build ok" 12 | 13 | exit 0 14 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | server: 4 | environment: 5 | ZQC_ENV: development 6 | ZQC_SERVER_DEBUG: "true" 7 | ZQC_LOG_LEVEL: debug 8 | ports: 9 | - 10400:1323 10 | volumes: 11 | - ./:/go/src/zqc 12 | - ~/data/projects/zqc-server-demo/server:/data 13 | mongodb: 14 | ports: 15 | - 10410:27017 16 | volumes: 17 | - ~/data/projects/zqc-server-demo/mongodb:/data 18 | redis: 19 | ports: 20 | - 10420:6379 21 | volumes: 22 | - ~/data/projects/zqc-server-demo/redis:/data 23 | -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | server: 4 | ports: 5 | - 10400:1323 6 | volumes: 7 | - /data/zqc-server-demo/server:/data 8 | mongodb: 9 | ports: 10 | - 10410:27017 11 | volumes: 12 | - /data/zqc-server-demo/mongodb:/data 13 | redis: 14 | ports: 15 | - 10420:6379 16 | volumes: 17 | - /data/zqc-server-demo/redis:/data 18 | -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | server: 4 | ports: 5 | - 10401:1323 6 | - 10402:8080 7 | volumes: 8 | - ./:/go/src/zqc 9 | mongodb: 10 | ports: 11 | - 10411:27017 12 | redis: 13 | ports: 14 | - 10421:6379 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | server: 4 | image: daocloud.io/jaggerwang/zqc-server-demo 5 | environment: 6 | ZQC_ENV: production 7 | ZQC_SERVER_DEBUG: "false" 8 | ZQC_LOG_LEVEL: info 9 | depends_on: 10 | - mongodb 11 | - redis 12 | mongodb: 13 | image: daocloud.io/jaggerwang/mongodb 14 | redis: 15 | image: daocloud.io/jaggerwang/redis 16 | -------------------------------------------------------------------------------- /fswatch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source ./common.sh 4 | 5 | docker-compose -p zqc-server-demo exec server ./restart.sh 6 | 7 | fswatch -e ".*" -i "\.go$" -r . >>.fswatch_modified 2>&1 & 8 | 9 | while [[ true ]] 10 | do 11 | if [[ `wc .fswatch_modified | awk {'print $1'}` -gt 0 ]]; then 12 | cat /dev/null >.fswatch_modified 13 | docker-compose -p zqc-server-demo exec server ./restart.sh 14 | fi 15 | 16 | sleep 1 17 | done 18 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Jagger Wang 2 | 3 | package main 4 | 5 | import ( 6 | "zqc/cmd" 7 | ) 8 | 9 | func main() { 10 | cmd.Execute() 11 | } 12 | -------------------------------------------------------------------------------- /middlewares/auth.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo" 7 | ) 8 | 9 | func Auth() echo.MiddlewareFunc { 10 | return func(next echo.HandlerFunc) echo.HandlerFunc { 11 | return func(c echo.Context) (err error) { 12 | cc := c.(*Context) 13 | if cc.SessionUserId() == "" { 14 | return echo.NewHTTPError(http.StatusUnauthorized) 15 | } 16 | return next(c) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /middlewares/context.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/gorilla/sessions" 5 | "github.com/labstack/echo" 6 | "gopkg.in/mgo.v2/bson" 7 | ) 8 | 9 | type Context struct { 10 | echo.Context 11 | } 12 | 13 | func (c *Context) Session() *sessions.Session { 14 | return c.Get("session").(*sessions.Session) 15 | } 16 | 17 | func (c *Context) DeleteSession() { 18 | c.Session().Options.MaxAge = -1 19 | c.Set("sessionModified", true) 20 | } 21 | 22 | func (c *Context) SetSessionItem(key string, value interface{}) { 23 | c.Session().Values[key] = value 24 | c.Set("sessionModified", true) 25 | } 26 | 27 | func (c *Context) DeleteSessionItem(key string) { 28 | delete(c.Session().Values, key) 29 | c.Set("sessionModified", true) 30 | } 31 | 32 | func (c *Context) SessionUserId() (userId bson.ObjectId) { 33 | if v, ok := c.Session().Values["userId"]; ok { 34 | return v.(bson.ObjectId) 35 | } else { 36 | return userId 37 | } 38 | } 39 | 40 | func MiddlewareContext() echo.MiddlewareFunc { 41 | return func(next echo.HandlerFunc) echo.HandlerFunc { 42 | return func(c echo.Context) (err error) { 43 | cc := &Context{c} 44 | return next(cc) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /middlewares/session.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/labstack/echo" 5 | "github.com/spf13/viper" 6 | "gopkg.in/boj/redistore.v1" 7 | 8 | "zqc/models" 9 | ) 10 | 11 | func Session() echo.MiddlewareFunc { 12 | return func(next echo.HandlerFunc) echo.HandlerFunc { 13 | return func(c echo.Context) (err error) { 14 | store, err := redistore.NewRediStoreWithPool( 15 | models.RedisPool("zqc"), []byte(viper.GetString("secretkey"))) 16 | if err != nil { 17 | panic(err) 18 | } 19 | store.SetMaxAge(viper.GetInt("session.maxAge")) 20 | store.SetMaxLength(viper.GetInt("session.maxLength")) 21 | store.SetKeyPrefix(viper.GetString("session.keyPrefix")) 22 | 23 | session, err := store.Get(c.Request(), viper.GetString("session.name")) 24 | if err != nil { 25 | panic(err) 26 | } 27 | c.Set("session", session) 28 | 29 | err = next(c) 30 | 31 | return err 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /models/mongo.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "math" 5 | "strings" 6 | "time" 7 | 8 | "github.com/spf13/viper" 9 | "gopkg.in/mgo.v2" 10 | ) 11 | 12 | var MongoSessions = map[string]*mgo.Session{} 13 | 14 | func NewMongoSession(clusterName string) (session *mgo.Session, err error) { 15 | if session, ok := MongoSessions[clusterName]; !ok { 16 | config := viper.GetStringMap("mongodb." + clusterName) 17 | info := mgo.DialInfo{ 18 | Addrs: strings.Split(config["addrs"].(string), ","), 19 | } 20 | timeout, ok := config["timeout"].(int64) 21 | if ok { 22 | info.Timeout = time.Duration(timeout * int64(math.Pow10(9))) 23 | } 24 | session, err = mgo.DialWithInfo(&info) 25 | if err != nil { 26 | return nil, err 27 | } 28 | session.SetMode(mgo.Monotonic, false) 29 | session.SetSafe(&mgo.Safe{ 30 | WMode: "majority", 31 | }) 32 | MongoSessions[clusterName] = session 33 | } 34 | return MongoSessions[clusterName].Copy(), nil 35 | } 36 | 37 | type MongoDB struct { 38 | *mgo.Database 39 | } 40 | 41 | func NewMongoDB(clusterName string, dbName string) (db *MongoDB, err error) { 42 | session, err := NewMongoSession(clusterName) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return &MongoDB{session.DB(dbName)}, nil 47 | } 48 | 49 | func (m *MongoDB) Close() { 50 | m.Session.Close() 51 | } 52 | 53 | type MongoColl struct { 54 | *mgo.Collection 55 | } 56 | 57 | func NewMongoColl(clusterName string, dbName string, collName string) (coll *MongoColl, err error) { 58 | db, err := NewMongoDB(clusterName, dbName) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return &MongoColl{db.C(collName)}, nil 63 | } 64 | 65 | func (m *MongoColl) Close() { 66 | m.Database.Session.Close() 67 | } 68 | 69 | func EmptyDB(clusterName string, dbName string, collName string) (err error) { 70 | var collNames []string 71 | if collName == "" { 72 | collNames, err = DBCollNames(clusterName, dbName) 73 | if err != nil { 74 | return err 75 | } 76 | } else { 77 | collNames = []string{collName} 78 | } 79 | 80 | for _, collName := range collNames { 81 | coll, err := NewMongoColl(clusterName, dbName, collName) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | _, err = coll.RemoveAll(nil) 87 | if err != nil { 88 | return err 89 | } 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func DBCollNames(clusterName string, dbName string) (collNames []string, err error) { 96 | db, err := NewMongoDB(clusterName, dbName) 97 | if err != nil { 98 | return nil, err 99 | } 100 | collNames, err = db.CollectionNames() 101 | if err != nil { 102 | return nil, err 103 | } 104 | return collNames, nil 105 | } 106 | -------------------------------------------------------------------------------- /models/mongodbIndex.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | ) 6 | 7 | var ZqcDBIndexes = map[string][]mgo.Index{ 8 | "user": []mgo.Index{ 9 | mgo.Index{ 10 | Key: []string{"mobile"}, 11 | Unique: true, 12 | Sparse: true, 13 | Background: true, 14 | }, 15 | mgo.Index{ 16 | Key: []string{"nickname"}, 17 | Unique: true, 18 | Sparse: true, 19 | Background: true, 20 | }, 21 | mgo.Index{ 22 | Key: []string{"createtime"}, 23 | Unique: false, 24 | Sparse: false, 25 | Background: true, 26 | }, 27 | }, 28 | } 29 | 30 | func CreateDBIndexes(clusterName string, dbName string, collName string, pos int) (err error) { 31 | var collNames []string 32 | if collName == "" { 33 | collNames, err = DBCollNames(clusterName, dbName) 34 | if err != nil { 35 | return err 36 | } 37 | } else { 38 | collNames = []string{collName} 39 | } 40 | 41 | for _, collName := range collNames { 42 | coll, err := NewMongoColl(clusterName, dbName, collName) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | for i, index := range ZqcDBIndexes[collName] { 48 | if pos == -1 || i == pos { 49 | err := coll.EnsureIndex(index) 50 | if err != nil { 51 | return err 52 | } 53 | } 54 | } 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /models/redis.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/garyburd/redigo/redis" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | var RedisPools = map[string]*redis.Pool{} 12 | 13 | func RedisPool(name string) *redis.Pool { 14 | if _, ok := RedisPools[name]; !ok { 15 | prefix := fmt.Sprintf("redis.%s", name) 16 | RedisPools[name] = &redis.Pool{ 17 | MaxIdle: viper.GetInt(fmt.Sprintf("%s.%s", prefix, "maxIdle")), 18 | IdleTimeout: viper.GetDuration(fmt.Sprintf("%s.%s", prefix, "idleTimeout")) * time.Second, 19 | Dial: func() (redis.Conn, error) { 20 | c, err := redis.DialTimeout( 21 | "tcp", 22 | viper.GetString(fmt.Sprintf("%s.%s", prefix, "address")), 23 | viper.GetDuration(fmt.Sprintf("%s.%s", prefix, "timeout.connect"))*time.Second, 24 | viper.GetDuration(fmt.Sprintf("%s.%s", prefix, "timeout.read"))*time.Second, 25 | viper.GetDuration(fmt.Sprintf("%s.%s", prefix, "timeout.write"))*time.Second, 26 | ) 27 | if err != nil { 28 | return nil, err 29 | } 30 | password := viper.GetString(fmt.Sprintf("%s.%s", prefix, "password")) 31 | if password != "" { 32 | if _, err := c.Do("AUTH", password); err != nil { 33 | c.Close() 34 | return nil, err 35 | } 36 | } 37 | return c, err 38 | }, 39 | TestOnBorrow: func(c redis.Conn, t time.Time) error { 40 | _, err := c.Do("PING") 41 | return err 42 | }, 43 | } 44 | } 45 | return RedisPools[name] 46 | } 47 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "gopkg.in/mgo.v2/bson" 7 | ) 8 | 9 | type User struct { 10 | Id bson.ObjectId `bson:"_id,omitempty"` 11 | Mobile string `bson:"mobile"` 12 | Password string `bson:"password"` 13 | Salt string `bson:"salt"` 14 | Nickname string `bson:"nickname,omitempty"` 15 | Gender string `bson:"gender,omitempty"` 16 | CreateTime *time.Time `bson:"createTime"` 17 | UpdateTime *time.Time `bson:"updateTime,omitempty"` 18 | } 19 | 20 | type UserColl struct { 21 | *MongoColl 22 | } 23 | 24 | func NewUserColl() (uc *UserColl, err error) { 25 | coll, err := NewMongoColl("zqc", "zqc", "user") 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return &UserColl{coll}, nil 31 | } 32 | -------------------------------------------------------------------------------- /restart.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source ./common.sh 4 | 5 | log INFO "build begin ..." 6 | go get -d -v ./... && go install -v 7 | if [[ $? != 0 ]]; then 8 | log ERROR "build failed" 9 | exit 1 10 | fi 11 | log INFO "build ok" 12 | 13 | supervisorctl restart all 14 | 15 | exit 0 16 | -------------------------------------------------------------------------------- /services/error.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | // Common 4 | const ( 5 | ErrCodeOk = iota 6 | ErrCodeFail 7 | ErrCodeHttp 8 | ErrCodeSystem 9 | ErrCodeNotFound 10 | ErrCodeDuplicated 11 | ErrCodeNoPermission 12 | ErrCodeInvalidParams 13 | ErrCodeInvalidVerifyCode 14 | ) 15 | 16 | // Account 17 | const ( 18 | ErrCodeWrongPassword = 1000 + iota 19 | ) 20 | 21 | var ErrMessages = map[int]string{ 22 | ErrCodeOk: "成功", 23 | ErrCodeFail: "失败", 24 | ErrCodeHttp: "请求错误", 25 | ErrCodeSystem: "系统错误", 26 | ErrCodeNotFound: "资源未找到", 27 | ErrCodeDuplicated: "资源重复", 28 | ErrCodeNoPermission: "没有权限", 29 | ErrCodeInvalidParams: "参数错误", 30 | ErrCodeInvalidVerifyCode: "验证码错误", 31 | 32 | ErrCodeWrongPassword: "密码错误", 33 | } 34 | 35 | type Error struct { 36 | Code int 37 | Message string 38 | Context interface{} 39 | } 40 | 41 | func NewError(code int, message string, ctx ...interface{}) (err *Error) { 42 | if message == "" { 43 | message = ErrMessages[code] 44 | } 45 | return &Error{ 46 | Code: code, 47 | Message: message, 48 | Context: ctx, 49 | } 50 | } 51 | 52 | func (s *Error) Error() (err string) { 53 | return s.Message 54 | } 55 | -------------------------------------------------------------------------------- /services/param.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "gopkg.in/mgo.v2/bson" 11 | ) 12 | 13 | func ParseInt(s string, min interface{}, max interface{}) (int, error) { 14 | i64, err := strconv.ParseInt(s, 10, 64) 15 | if err != nil { 16 | return 0, errors.New(fmt.Sprintf("invalid int %v", s)) 17 | } 18 | i := int(i64) 19 | if min != nil && i < min.(int) { 20 | return 0, errors.New(fmt.Sprintf("must >= %v", min)) 21 | } 22 | if max != nil && i > max.(int) { 23 | return 0, errors.New(fmt.Sprintf("must <= %v", max)) 24 | } 25 | return i, nil 26 | } 27 | 28 | func ParseObjectId(s string) (id bson.ObjectId, err error) { 29 | if !bson.IsObjectIdHex(s) { 30 | return id, errors.New(fmt.Sprintf("invalid ObjectId %v", s)) 31 | } 32 | return bson.ObjectIdHex(s), nil 33 | } 34 | 35 | func ParseObjectIds(s string) (ids []bson.ObjectId, err error) { 36 | ss := strings.Split(s, ",") 37 | for _, v := range ss { 38 | if !bson.IsObjectIdHex(v) { 39 | return nil, errors.New(fmt.Sprintf("invalid ObjectId %v", v)) 40 | } 41 | } 42 | 43 | ids = make([]bson.ObjectId, 0, len(ss)) 44 | for _, v := range ss { 45 | ids = append(ids, bson.ObjectIdHex(v)) 46 | } 47 | return ids, nil 48 | } 49 | 50 | func ParseTime(s string) (*time.Time, error) { 51 | t, err := time.Parse(time.RFC3339, s) 52 | return &t, err 53 | } 54 | -------------------------------------------------------------------------------- /services/user.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "gopkg.in/mgo.v2/bson" 8 | 9 | "zqc/models" 10 | "zqc/util" 11 | ) 12 | 13 | const ( 14 | UserGenderMale = "m" 15 | UserGenderFemale = "f" 16 | ) 17 | 18 | type User struct { 19 | Id bson.ObjectId `json:"id"` 20 | Mobile string `json:"mobile"` 21 | Password string `json:"-"` 22 | Nickname string `json:"nickname"` 23 | Gender string `json:"gender"` 24 | CreateTime *time.Time `json:"createTime"` 25 | UpdateTime *time.Time `json:"updateTime"` 26 | } 27 | 28 | func NewUserFromModel(m models.User) (user User) { 29 | user = User{ 30 | Id: m.Id, 31 | Mobile: m.Mobile, 32 | Password: m.Password, 33 | Nickname: m.Nickname, 34 | Gender: m.Gender, 35 | CreateTime: m.CreateTime, 36 | UpdateTime: m.UpdateTime, 37 | } 38 | 39 | return user 40 | } 41 | 42 | func CreateUser(mobile string, password string, nickname string, gender string) (user User, err error) { 43 | c, err := models.NewUserColl() 44 | if err != nil { 45 | return user, NewError(ErrCodeSystem, err.Error()) 46 | } 47 | defer c.Close() 48 | 49 | salt := util.RandString(16, nil) 50 | password = util.Md5WithSalt(password, salt) 51 | t := time.Now() 52 | m := models.User{ 53 | Id: bson.NewObjectId(), 54 | Mobile: mobile, 55 | Password: password, 56 | Salt: salt, 57 | Nickname: nickname, 58 | Gender: gender, 59 | CreateTime: &t, 60 | } 61 | err = c.Insert(m) 62 | if err != nil { 63 | return user, NewError(ErrCodeDuplicated, err.Error()) 64 | } 65 | 66 | err = c.FindId(m.Id).One(&m) 67 | if err != nil { 68 | return user, NewError(ErrCodeNotFound, err.Error()) 69 | } 70 | 71 | return NewUserFromModel(m), nil 72 | } 73 | 74 | func UpdateUser(id bson.ObjectId, update bson.M) (user User, err error) { 75 | c, err := models.NewUserColl() 76 | if err != nil { 77 | return user, NewError(ErrCodeSystem, err.Error()) 78 | } 79 | defer c.Close() 80 | 81 | var m models.User 82 | err = c.FindId(id).One(&m) 83 | if err != nil { 84 | return user, NewError(ErrCodeNotFound, err.Error()) 85 | } 86 | 87 | if password, ok := update["password"]; ok { 88 | update["password"] = util.Md5WithSalt(password.(string), m.Salt) 89 | } 90 | 91 | update["updateTime"] = time.Now() 92 | err = c.UpdateId(id, bson.M{ 93 | "$set": update, 94 | }) 95 | if err != nil { 96 | code := ErrCodeSystem 97 | if strings.HasPrefix(err.Error(), "E11000 ") { 98 | code = ErrCodeDuplicated 99 | } 100 | return user, NewError(code, err.Error()) 101 | } 102 | 103 | err = c.FindId(id).One(&m) 104 | if err != nil { 105 | return user, NewError(ErrCodeNotFound, err.Error()) 106 | } 107 | 108 | return NewUserFromModel(m), nil 109 | } 110 | 111 | func GetUser(id bson.ObjectId) (user User, err error) { 112 | c, err := models.NewUserColl() 113 | if err != nil { 114 | return user, NewError(ErrCodeSystem, err.Error()) 115 | } 116 | defer c.Close() 117 | 118 | var m models.User 119 | err = c.FindId(id).One(&m) 120 | if err != nil { 121 | return user, NewError(ErrCodeNotFound, err.Error()) 122 | } 123 | 124 | return NewUserFromModel(m), nil 125 | } 126 | 127 | func GetUsers(ids []bson.ObjectId) (users []User, err error) { 128 | c, err := models.NewUserColl() 129 | if err != nil { 130 | return users, NewError(ErrCodeSystem, err.Error()) 131 | } 132 | defer c.Close() 133 | 134 | ms := make([]models.User, 0, len(ids)) 135 | err = c.Find(bson.M{"_id": bson.M{"$in": ids}}).All(&ms) 136 | if err != nil { 137 | return users, NewError(ErrCodeNotFound, err.Error()) 138 | } 139 | 140 | users = make([]User, 0, len(ids)) 141 | for _, m := range ms { 142 | users = append(users, NewUserFromModel(m)) 143 | } 144 | 145 | return users, nil 146 | } 147 | 148 | func GetUserByMobile(mobile string) (user User, err error) { 149 | c, err := models.NewUserColl() 150 | if err != nil { 151 | return user, NewError(ErrCodeSystem, err.Error()) 152 | } 153 | defer c.Close() 154 | 155 | var m models.User 156 | err = c.Find(bson.M{"mobile": mobile}).One(&m) 157 | if err != nil { 158 | return user, NewError(ErrCodeNotFound, err.Error()) 159 | } 160 | 161 | return NewUserFromModel(m), nil 162 | } 163 | 164 | func VerifyUserPassword(id bson.ObjectId, password string) (user User, err error) { 165 | c, err := models.NewUserColl() 166 | if err != nil { 167 | return user, NewError(ErrCodeSystem, err.Error()) 168 | } 169 | defer c.Close() 170 | 171 | var m models.User 172 | err = c.Find(bson.M{"_id": id}).One(&m) 173 | if err != nil { 174 | return user, NewError(ErrCodeNotFound, err.Error()) 175 | } 176 | 177 | if util.Md5WithSalt(password, m.Salt) != m.Password { 178 | return user, NewError(ErrCodeWrongPassword, "") 179 | } 180 | 181 | return NewUserFromModel(m), nil 182 | } 183 | -------------------------------------------------------------------------------- /supervisord.conf: -------------------------------------------------------------------------------- 1 | [inet_http_server] 2 | port = 127.0.0.1:9001 3 | 4 | [supervisord] 5 | nodaemon = true 6 | logfile = /data/supervisord.log 7 | pidfile = /data/supervisord.pid 8 | childlogdir = /data 9 | 10 | [rpcinterface:supervisor] 11 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 12 | 13 | [supervisorctl] 14 | serverurl = http://127.0.0.1:9001 15 | 16 | [program:server] 17 | command = zqc server --env=%(ENV_ZQC_ENV)s --log.level=%(ENV_ZQC_LOG_LEVEL)s --server.debug=%(ENV_ZQC_SERVER_DEBUG)s 18 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source ./common.sh 4 | 5 | ./deploy-test.sh 6 | 7 | log INFO "run unittest begin ..." 8 | docker-compose -p zqc-server-demo-test exec server goconvey -host 0.0.0.0 -launchBrowser=false 9 | log INFO "run unittest end" 10 | -------------------------------------------------------------------------------- /test/common.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "zqc/models" 5 | "zqc/services" 6 | ) 7 | 8 | func emptyDB() { 9 | models.EmptyDB("zqc", "zqc", "") 10 | } 11 | 12 | func createUser() services.User { 13 | user, err := services.CreateUser("18600000000", "123456", "jag", "m") 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | return user 19 | } 20 | -------------------------------------------------------------------------------- /test/main_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func TestMain(m *testing.M) { 11 | initConfig() 12 | 13 | result := m.Run() 14 | 15 | os.Exit(result) 16 | } 17 | 18 | func initConfig() { 19 | viper.SetConfigFile("../config.json") 20 | err := viper.ReadInConfig() 21 | if err != nil { 22 | panic(err) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/user_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | 8 | "zqc/services" 9 | ) 10 | 11 | func TestCreateUser(t *testing.T) { 12 | Convey("Given an empty db", t, func() { 13 | emptyDB() 14 | 15 | Convey("Create a user", func() { 16 | _, err := services.CreateUser("18600000000", "123456", "jag", "m") 17 | So(err, ShouldBeNil) 18 | }) 19 | }) 20 | } 21 | 22 | func TestEditUser(t *testing.T) { 23 | Convey("Given an exist user", t, func() { 24 | emptyDB() 25 | user := createUser() 26 | 27 | Convey("Update user", func() { 28 | nickname := "jag1" 29 | gender := "f" 30 | user, err := services.UpdateUser(user.Id, map[string]interface{}{ 31 | "nickname": nickname, 32 | "gender": gender, 33 | }) 34 | So(err, ShouldBeNil) 35 | So(user.Nickname, ShouldEqual, nickname) 36 | So(user.Gender, ShouldEqual, gender) 37 | }) 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /util/collection.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | func Index(vs []string, t string) int { 8 | for i, v := range vs { 9 | if v == t { 10 | return i 11 | } 12 | } 13 | return -1 14 | } 15 | 16 | func Include(vs []string, t string) bool { 17 | return Index(vs, t) >= 0 18 | } 19 | 20 | func Any(vs []string, f func(string) bool) bool { 21 | for _, v := range vs { 22 | if f(v) { 23 | return true 24 | } 25 | } 26 | return false 27 | } 28 | 29 | func All(vs []string, f func(string) bool) bool { 30 | for _, v := range vs { 31 | if !f(v) { 32 | return false 33 | } 34 | } 35 | return true 36 | } 37 | 38 | func Filter(vs []string, f func(string) bool) []string { 39 | vsf := make([]string, 0) 40 | for _, v := range vs { 41 | if f(v) { 42 | vsf = append(vsf, v) 43 | } 44 | } 45 | return vsf 46 | } 47 | 48 | func Map(vs []string, f func(string) string) []string { 49 | vsm := make([]string, len(vs)) 50 | for i, v := range vs { 51 | vsm[i] = f(v) 52 | } 53 | return vsm 54 | } 55 | 56 | func Keys(m map[string]string) []string { 57 | ks := make([]string, 0, len(m)) 58 | for k, _ := range m { 59 | ks = append(ks, k) 60 | } 61 | return ks 62 | } 63 | 64 | func Values(m map[string]string) []string { 65 | vs := make([]string, 0, len(m)) 66 | for _, v := range m { 67 | vs = append(vs, v) 68 | } 69 | return vs 70 | } 71 | 72 | func Items(m map[string]string) [][2]string { 73 | is := make([][2]string, 0, len(m)) 74 | for k, v := range m { 75 | is = append(is, [...]string{k, v}) 76 | } 77 | return is 78 | } 79 | 80 | func Sort(vs []string) []string { 81 | vsm := make([]string, 0, len(vs)) 82 | for _, v := range vs { 83 | vsm = append(vsm, v) 84 | } 85 | sort.Strings(vsm) 86 | return vsm 87 | } 88 | -------------------------------------------------------------------------------- /util/http.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "time" 8 | 9 | log "github.com/Sirupsen/logrus" 10 | ) 11 | 12 | func Request(req *http.Request, timeout time.Duration) (body []byte, err error) { 13 | client := &http.Client{ 14 | Timeout: timeout, 15 | } 16 | resp, err := client.Do(req) 17 | if err != nil { 18 | return nil, err 19 | } 20 | defer resp.Body.Close() 21 | 22 | body, err = ioutil.ReadAll(resp.Body) 23 | if err != nil { 24 | return nil, err 25 | } 26 | log.WithFields(log.Fields{ 27 | "method": req.Method, 28 | "url": req.URL, 29 | "reqHeader": req.Header, 30 | "respHeader": resp.Header, 31 | "respBody": string(body), 32 | }).Debug("http request") 33 | 34 | return body, nil 35 | } 36 | 37 | func RequestJSON(req *http.Request, timeout time.Duration) (result map[string]interface{}, err error) { 38 | body, err := Request(req, timeout) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | err = json.Unmarshal(body, &result) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return result, nil 49 | } 50 | -------------------------------------------------------------------------------- /util/string.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/md5" 6 | "crypto/sha1" 7 | "encoding/base64" 8 | "encoding/hex" 9 | "math/rand" 10 | "net/url" 11 | "strings" 12 | ) 13 | 14 | func RandString(n int, runes []rune) string { 15 | if runes == nil { 16 | runes = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 17 | } 18 | b := make([]rune, n) 19 | for i := range b { 20 | b[i] = runes[rand.Intn(len(runes))] 21 | } 22 | return string(b) 23 | } 24 | 25 | func Md5WithSalt(s string, salt string) string { 26 | h := md5.Sum([]byte(s + salt)) 27 | return hex.EncodeToString(h[:]) 28 | } 29 | 30 | func Md5(s string) string { 31 | return Md5WithSalt(s, "") 32 | } 33 | 34 | func CanonicalizedQueryString(query url.Values) string { 35 | q := make(map[string]string, len(query)) 36 | for k, v := range query { 37 | if len(v) > 0 { 38 | q[k] = v[0] 39 | } else { 40 | q[k] = "" 41 | } 42 | } 43 | s := strings.Join( 44 | Map( 45 | Sort(Keys(q)), 46 | func(k string) string { 47 | return url.QueryEscape(k) + "=" + url.QueryEscape(q[k]) 48 | }, 49 | ), 50 | "&", 51 | ) 52 | return s 53 | } 54 | 55 | func HmacSha1(input string, key string) string { 56 | h := hmac.New(sha1.New, []byte(key)) 57 | h.Write([]byte(input)) 58 | s := base64.StdEncoding.EncodeToString(h.Sum(nil)) 59 | return s 60 | } 61 | -------------------------------------------------------------------------------- /util/validator.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | valid "github.com/asaskevich/govalidator" 5 | "gopkg.in/mgo.v2/bson" 6 | ) 7 | 8 | func init() { 9 | valid.TagMap["objectidhex"] = bson.IsObjectIdHex 10 | } 11 | --------------------------------------------------------------------------------