├── 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 |
--------------------------------------------------------------------------------