DEV Community

Cover image for go-doudou series 01: How to develop a monolithic RESTful service with go-doudou
go-doudou
go-doudou

Posted on

go-doudou series 01: How to develop a monolithic RESTful service with go-doudou

TOC

In this tutorial, I will introduce go-doudou microservice framework to you. Go-doudou has built-in service register, discover and fault tolerance features based on gossip protocol, and it uses gorilla/mux as http router, and uses golang interface as IDL(Interface Definition Language). Go-doudou is an IDL compiler and server/client code generator tool at first, then it becomes a microservice framework. Its RESTful version is stable and production ready, while grpc version is in early development.

After brief introduction, now I will use a user management service as an example to demonstrate how to develop a monolithic service with go-doudou. This will be the first post about go-doudou, we will begin from monolithic and then go to microservice step by step.

 

TODO

  • User SignUp
  • User LogIn
  • User Detail
  • User Pagination
  • Upload Avatar
  • Download Avatar

 

Learning Goals

  • Protect user detail, user pagination and upload avatar endpoints by jwt token
  • Online OpenAPI 3.0 api documentation
  • Golang http client code
  • Fake response for frontend
  • Use built-in ddl tool for syncing structs to tables and generating structs from tables
  • Use generated dao layer code for single table CRUD

 

Dev Requirements

 

Install go-doudou

  • If go version is below 1.16:
GO111MODULE=on  go get -v github.com/unionj-cloud/go-doudou@v0.9.6
Enter fullscreen mode Exit fullscreen mode

if go version is 1.16 or above:

go get -v github.com/unionj-cloud/go-doudou@v0.9.6
Enter fullscreen mode Exit fullscreen mode
  • Try to execute command go-doudou -v, if it printed below content, installation is successful:
➜  ~ go-doudou -v
go-doudou version v0.9.6
Enter fullscreen mode Exit fullscreen mode

if it printed command not found, you should try to check GOPATH, and the go-doudou binary should be in $GOPATH/bin directory.

➜  ~ go env | grep GOPATH
GOPATH="/Users/wubin1989/go"
Enter fullscreen mode Exit fullscreen mode

I can find go-doudou binary in /Users/wubin1989/go/bin directory for example. Then you should add one line export PATH=/Users/wubin1989/go/bin:$PATH to .zshrc or .bashrc file. And then execute source ~/.zshrc or source ~/.bashrc or open a new command line tab to make it work.

 

Init Project

Execute command in arbitrary directory. Here is in tutorials directory:

go-doudou svc init usersvc 
Enter fullscreen mode Exit fullscreen mode

cd usersvc, you will see below structure:

➜  tutorials ll
total 0
drwxr-xr-x  9 wubin1989  staff   288B 10 24 20:05 usersvc
➜  tutorials cd usersvc  
➜  usersvc git:(master) ✗ ll
total 24
-rw-r--r--  1 wubin1989  staff   707B 10 24 20:05 Dockerfile
-rw-r--r--  1 wubin1989  staff   439B 10 24 20:05 go.mod
-rw-r--r--  1 wubin1989  staff   247B 10 24 20:05 svc.go
drwxr-xr-x  3 wubin1989  staff    96B 10 24 20:05 vo
Enter fullscreen mode Exit fullscreen mode
  • svc.go file:IDL file for defining api endpoints by interface methods
  • vo folder:define structs as api input and output parameters. Vo is named from view object.
  • Dockerfile:for building docker image

 

Define Apis

Have a look at svc.go file:

package service

import (
    "context"
    v3 "github.com/unionj-cloud/go-doudou/openapi/v3"
    "os"
    "usersvc/vo"
)

// Usersvc is user management service
// You should set Bearer Token header when you request protected endpoints such as user detail, user pagination and upload avatar.
// You can add doc for whole service here
type Usersvc interface {
    // PageUsers is user pagination api
    // demo how to define post request api which accepts application/json content-type
    PageUsers(ctx context.Context,
        // pagination parameter
        query vo.PageQuery) (
        // pagination result
        data vo.PageRet,
        // error
        err error)

    // GetUser is user detail api
    // demo how to define get http request with query string parameters
    GetUser(ctx context.Context,
        // user id
        userId int) (
        // user detail
        data vo.UserVo,
        // error
        err error)

    // PublicSignUp is user signup api
    // demo how to define post request api which accepts application/x-www-form-urlencoded content-type
    PublicSignUp(ctx context.Context,
        // username
        username string,
        // password
        password string,
        // image code
        code string,
    ) (
        // return OK if success
        data string, err error)

    // PublicLogIn is user login api
    // demo how to do authentication and issue token
    PublicLogIn(ctx context.Context,
        // username
        username string,
        // password
        password string) (
        // token
        data string, err error)

    // UploadAvatar is avatar upload api
    // demo how to define file upload api
    // NOTE: there must be at least one []*v3.FileModel or *v3.FileModel input parameter
    UploadAvatar(ctx context.Context,
        // user avatar
        avatar *v3.FileModel) (
        // return OK if success
        data string, err error)

    // GetPublicDownloadAvatar is avatar download api
    // demo how to define file download api
    // NOTE: there must be one and at most one *os.File output parameter
    GetPublicDownloadAvatar(ctx context.Context,
        // user id
        userId int) (
        // avatar file
        data *os.File, err error)
}
Enter fullscreen mode Exit fullscreen mode

All comments in above code will be displayed in online OpenAPI 3.0 documentation, so recommend to add comments to let your clients use your apis more easily. For simplicity, no support for fancy symbol like @, only support golang // syntax.

 

Generate Code

Execute below command to generate server and client code:

go-doudou svc http --handler -c go --doc
Enter fullscreen mode Exit fullscreen mode

Explain:

  • --handler: to generate http handler implementation
  • -c:to generate client http request code. Currently only support go.
  • --doc:to generate OpenAPI 3.0 doc file in json format This is the most used command for me. Recommend to all go-doudou users. When you made some changes in svc.go file, execute this command, and you will get incrementally generated new code. The rule is:
  • handler.go file, *_openapi3.json file and client.go file will always be fully overwritten. So you should not edit these files manually.
  • handlerimpl.go file, svcimpl.go file and clientproxy.go file will always be incrementally appended new code. Existing code will not be changed by go-doudou. So you can edit these files to fit your needs and still get all benefits from go-doudou.
  • Other files will be skipped if already exist.

You'd better execute below command to make sure all dependencies have been downloaded:

go mod tidy
Enter fullscreen mode Exit fullscreen mode

Let's see project structure now:

➜  usersvc git:(master) ✗ ll
total 296
-rw-r--r--  1 wubin1989  staff   707B 10 24 20:05 Dockerfile
drwxr-xr-x  3 wubin1989  staff    96B 10 24 23:10 client
drwxr-xr-x  3 wubin1989  staff    96B 10 24 23:10 cmd
drwxr-xr-x  3 wubin1989  staff    96B 10 24 23:10 config
drwxr-xr-x  3 wubin1989  staff    96B 10 24 23:10 db
-rw-r--r--  1 wubin1989  staff   514B 10 24 23:10 go.mod
-rw-r--r--  1 wubin1989  staff   115K 10 24 23:10 go.sum
-rw-r--r--  1 wubin1989  staff   1.7K 10 24 23:21 svc.go
-rw-r--r--  1 wubin1989  staff   1.6K 10 25 09:18 svcimpl.go
drwxr-xr-x  3 wubin1989  staff    96B 10 24 23:10 transport
-rwxr-xr-x  1 wubin1989  staff   5.9K 10 25 09:18 usersvc_openapi3.go
-rwxr-xr-x  1 wubin1989  staff   5.7K 10 25 09:18 usersvc_openapi3.json
drwxr-xr-x  3 wubin1989  staff    96B 10 24 23:07 vo
Enter fullscreen mode Exit fullscreen mode
  • client folder: generated go client code
  • cmd folder: main.go file in it for starting the service
  • config folder: used for populating configs
  • db folder: used for connecting database
  • svcimpl.go file: there are mock implementations initially to return fake response data for clients. You should replace them with your own implementation later.
  • transport folder:there are http handlers in it.
  • usersvc_openapi3.go file:used for online doc
  • usersvc_openapi3.json file:used for online doc

 

Start Service

go-doudou svc run
Enter fullscreen mode Exit fullscreen mode

You will see:

➜  usersvc git:(master) ✗ go-doudou svc run
INFO[2022-01-23 15:55:07] Initializing logging reporter                
INFO[2022-01-23 15:55:07] ================ Registered Routes ================ 
INFO[2022-01-23 15:55:07] +----------------------+--------+-------------------------+ 
INFO[2022-01-23 15:55:07] |         NAME         | METHOD |         PATTERN         | 
INFO[2022-01-23 15:55:07] +----------------------+--------+-------------------------+ 
INFO[2022-01-23 15:55:07] | PageUsers            | POST   | /page/users             | 
INFO[2022-01-23 15:55:07] | User                 | GET    | /user                   | 
INFO[2022-01-23 15:55:07] | PublicSignUp         | POST   | /public/sign/up         | 
INFO[2022-01-23 15:55:07] | PublicLogIn          | POST   | /public/log/in          | 
INFO[2022-01-23 15:55:07] | UploadAvatar         | POST   | /upload/avatar          | 
INFO[2022-01-23 15:55:07] | PublicDownloadAvatar | GET    | /public/download/avatar | 
INFO[2022-01-23 15:55:07] | GetDoc               | GET    | /go-doudou/doc          | 
INFO[2022-01-23 15:55:07] | GetOpenAPI           | GET    | /go-doudou/openapi.json | 
INFO[2022-01-23 15:55:07] | Prometheus           | GET    | /go-doudou/prometheus   | 
INFO[2022-01-23 15:55:07] | GetRegistry          | GET    | /go-doudou/registry     | 
INFO[2022-01-23 15:55:07] +----------------------+--------+-------------------------+ 
INFO[2022-01-23 15:55:07] =================================================== 
INFO[2022-01-23 15:55:07] Started in 233.424µs                        
INFO[2022-01-23 15:55:07] Http server is listening on :6060      
Enter fullscreen mode Exit fullscreen mode

When you see Http server is listening on :6060, it means service has been started and we also have a mock server. For example, we can send a request to /user api, to see what will be sent back(I use httpie):

➜  usersvc git:(master) ✗ http http://localhost:6060/user
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 109
Content-Type: application/json; charset=UTF-8
Date: Sun, 23 Jan 2022 07:59:28 GMT
Vary: Accept-Encoding

{
    "data": {
        "Dept": "ZkkCmcLU",
        "Id": -1941954111002502016,
        "Name": "aiMtQ",
        "Phone": "XMAqXf"
    }
}
Enter fullscreen mode Exit fullscreen mode

You may notice that all attribute names are capitalised, and that is not what you want. There is one line go generate command in vo.go file:

//go:generate go-doudou name --file $GOFILE
Enter fullscreen mode Exit fullscreen mode

The command uses a built-in small tool called name. It can add or modify json tag to every exported field of each struct in a file. Default mode is camelcase, and it also supports snakecase.

go generate ./...
Enter fullscreen mode Exit fullscreen mode

Then restart the service, resend a request to /user api, you can see it works.

➜  usersvc git:(master) ✗ http http://localhost:6060/user
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 114
Content-Type: application/json; charset=UTF-8
Date: Sun, 23 Jan 2022 08:00:40 GMT
Vary: Accept-Encoding

{
    "data": {
        "dept": "wGAEEeveHp",
        "id": -816946940349962228,
        "name": "hquwOKl",
        "phone": "AriWmKYB"
    }
}
Enter fullscreen mode Exit fullscreen mode

If you want to know more about name command, please refer to README.md.
At the moment, as json tag of each exported field has been changed, you should regenerate OpenAPI 3.0 doc json file:

go-doudou svc http --doc
Enter fullscreen mode Exit fullscreen mode

Then restart the service, open browser and navigate to http://localhost:6060/go-doudou/doc, input http basic username admin, password admin to see online doc.

Image description

Image description

 

Init Database

For supporting utf8/utf8mb4, you should create my/custom.cnf file in project root path and paste below content:

[client]
default-character-set=utf8mb4

[mysql]
default-character-set=utf8mb4

[mysqld]
character_set_server=utf8mb4
collation-server=utf8mb4_general_ci
default-authentication-plugin=mysql_native_password
init_connect='SET NAMES utf8mb4'
Enter fullscreen mode Exit fullscreen mode

Then create sqlscripts/init.sql file for initialising database, paste below sqls:

CREATE SCHEMA `tutorial` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;

CREATE TABLE `tutorial`.`t_user`
(
    `id`        INT         NOT NULL AUTO_INCREMENT,
    `username`  VARCHAR(45) NOT NULL COMMENT 'username',
    `password`  VARCHAR(60) NOT NULL COMMENT 'password',
    `name`      VARCHAR(45) NOT NULL COMMENT 'real name',
    `phone`     VARCHAR(45) NOT NULL COMMENT 'phone number',
    `dept`      VARCHAR(45) NOT NULL COMMENT 'department',
    `create_at` DATETIME    NULL DEFAULT current_timestamp,
    `update_at` DATETIME    NULL DEFAULT current_timestamp on update current_timestamp,
    `delete_at` DATETIME    NULL,
    PRIMARY KEY (`id`)
);
Enter fullscreen mode Exit fullscreen mode

Create docker-compose.yml file, paste below content:

version: '3.9'

services:
  db:
    container_name: db
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: 1234
    ports:
      - 3306:3306
    volumes:
      - $PWD/my:/etc/mysql/conf.d
      - $PWD/sqlscripts:/docker-entrypoint-initdb.d
    networks:
      - tutorial

networks:
  tutorial:
    driver: bridge
Enter fullscreen mode Exit fullscreen mode

Execute docker compose command to start mysql docker container:

docker-compose -f docker-compose.yml up -d
Enter fullscreen mode Exit fullscreen mode

Execute docker ps to make sure it started.

➜  usersvc git:(master) ✗ docker ps        
CONTAINER ID   IMAGE       COMMAND                  CREATED          STATUS          PORTS                                                  NAMES
df6af6362c41   mysql:5.7   "docker-entrypoint.s…"   13 minutes ago   Up 13 minutes   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   db
Enter fullscreen mode Exit fullscreen mode

 

Generate Domain and CRUD Code

As our mysql schema name is tutorial, so we should change the value of environment variable DB_SCHEMA to tutorial in .env file.

DB_SCHEMA=tutorial
Enter fullscreen mode Exit fullscreen mode

Execute below command to generate domain and dao layer(CRUD) code:

go-doudou ddl -r --dao --pre=t_
Enter fullscreen mode Exit fullscreen mode

Flag explain:

  • -r:short for reverse, generate structs from tables
  • --dao:generate dao layer code, only support single table CRUD operations based on sqlx
  • --pre:table name has prefix t_ At the moment, you can see two more folders:

Image description

More about the built-in ddl tool, please refer to README.md
Here we should see what CRUD operation methods in dao/base.go file, we will use them to implement our business logic.

package dao

import (
    "context"
    "github.com/unionj-cloud/go-doudou/ddl/query"
)

type Base interface {
    Insert(ctx context.Context, data interface{}) (int64, error)
    Upsert(ctx context.Context, data interface{}) (int64, error)
    UpsertNoneZero(ctx context.Context, data interface{}) (int64, error)
    DeleteMany(ctx context.Context, where query.Q) (int64, error)
    Update(ctx context.Context, data interface{}) (int64, error)
    UpdateNoneZero(ctx context.Context, data interface{}) (int64, error)
    UpdateMany(ctx context.Context, data interface{}, where query.Q) (int64, error)
    UpdateManyNoneZero(ctx context.Context, data interface{}, where query.Q) (int64, error)
    Get(ctx context.Context, id interface{}) (interface{}, error)
    SelectMany(ctx context.Context, where ...query.Q) (interface{}, error)
    CountMany(ctx context.Context, where ...query.Q) (int, error)
    PageMany(ctx context.Context, page query.Page, where ...query.Q) (query.PageRet, error)
}

Enter fullscreen mode Exit fullscreen mode

We will add *sqlx.DB instance as field to UsersvcImpl struct in svcimpl.go file.

type UsersvcImpl struct {
    conf *config.Config
    db   *sqlx.DB
}
Enter fullscreen mode Exit fullscreen mode

Then make a small fix in factory function NewUsersvc.

func NewUsersvc(conf *config.Config, db *sqlx.DB) Usersvc {
    return &UsersvcImpl{
        conf,
        db,
    }
}
Enter fullscreen mode Exit fullscreen mode

In our main function, it has already injected *sqlx.DB instance conn into NewUsersvc.

svc := service.NewUsersvc(conf, conn)
Enter fullscreen mode Exit fullscreen mode

Afterwards, we can use db field of UsersvcImpl for CRUD operations.

 

User SignUp

Fix Domain

Normally username should be unique, so we should fix domain/user.go to add unique constraint to User struct

Username string     `dd:"type:varchar(45);extra:comment 'username';unique"`
Enter fullscreen mode Exit fullscreen mode

Here we can see we add ;unique to dd tag.
Then execute go-doudou ddl command:

go-doudou ddl --pre=t_
Enter fullscreen mode Exit fullscreen mode

There is no -r flag because we need to sync User struct to t_user table.

Image description

PublicSignUp Method

We should add CheckUsernameExists method to UserDao interface for checking if the username has already been used.

package dao

import "context"

type UserDao interface {
    Base
    CheckUsernameExists(ctx context.Context, username string) (bool, error)
}
Enter fullscreen mode Exit fullscreen mode

Then we create a new file dao/userdaoimplext.go to add extra method implementations.

package dao

import (
    "context"
    "github.com/unionj-cloud/go-doudou/ddl/query"
    "usersvc/domain"
)

func (receiver UserDaoImpl) CheckUsernameExists(ctx context.Context, username string) (bool, error) {
    many, err := receiver.SelectMany(ctx, query.C().Col("username").Eq(username))
    if err != nil {
        return false, err
    }
    users := many.([]domain.User)
    if len(users) > 0 {
        return true, nil
    }
    return false, nil
}
Enter fullscreen mode Exit fullscreen mode

In this way, we can fulfill more complex needs by adding custom CRUD operation methods to generated dao layer interface. If you add or delete fields of a domain struct, you just need to remove *daosql.go file, and generate new one by executing command go-doudou ddl --dao --pre=t_, existing dao layer files won't be changed anything.

Then we implement PublicSignUp method.

func (receiver *UsersvcImpl) PublicSignUp(ctx context.Context, username string, password string, code string) (data string, err error) {
    hashPassword, _ := lib.HashPassword(password)
    userDao := dao.NewUserDao(receiver.db)
    var exists bool
    exists, err = userDao.CheckUsernameExists(ctx, username)
    if err != nil {
        panic(err)
    }
    if exists {
        panic(lib.ErrUsernameExists)
    }
    _, err = userDao.Insert(ctx, domain.User{
        Username: username,
        Password: hashPassword,
    })
    if err != nil {
        panic(err)
    }
    return "OK", nil
}
Enter fullscreen mode Exit fullscreen mode

If error ocurred, you can simply let program panic, or return "", lib.ErrUsernameExists because we add a recover middleware ddhttp.Recover. It can let our service recover and send error message to clients. If any non-nil error returned, http status code is 500 by default. If you don't like it, you can change generated http handler code manually. Remember existing code in handlerimpl.go file won't be changed when execute go-doudou command, only be incrementally appended new code.

Test by Postman

Image description

Image description

 

User LogIn

PublicLogIn Method

func (receiver *UsersvcImpl) PublicLogIn(ctx context.Context, username string, password string) (data string, err error) {
    userDao := dao.NewUserDao(receiver.db)
    many, err := userDao.SelectMany(ctx, query.C().Col("username").Eq(username).And(query.C().Col("delete_at").IsNull()))
    if err != nil {
        return "", err
    }
    users := many.([]domain.User)
    if len(users) == 0 || !lib.CheckPasswordHash(password, users[0].Password) {
        panic(lib.ErrUsernameOrPasswordIncorrect)
    }
    now := time.Now()
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "userId": users[0].Id,
        "exp":    now.Add(10 * time.Minute).Unix(),
        //"iat":    now.Unix(),
        //"nbf":    now.Unix(),
    })
    return token.SignedString(receiver.conf.JWTConf.Secret)
}
Enter fullscreen mode Exit fullscreen mode

The code logic is query user record from database by input parameter username, if not found, return Incorrect username or password error, if password was correct, issue token. The jwt library used here is golang-jwt/jwt

Test by Postman

Image description

 

Upload Avatar

Fix Domain

We add avatar field to user struct now:

Avatar   string     `dd:"type:varchar(255);extra:comment 'user avatar'"`
Enter fullscreen mode Exit fullscreen mode

Before continue, we should remove dao/userdaosql.go file first, then execute ddl command:

go-doudou ddl --dao --pre=t_
Enter fullscreen mode Exit fullscreen mode

If multiple domain structs were changed, we can use wildcard to delete all *sql.go files in order to generate new ones.

rm -rf dao/*sql.go
Enter fullscreen mode Exit fullscreen mode

Fix .env

Add three lines. JWT_ prefixed ones are jwt related configs, while Biz_ prefixed ones are business related configs.

JWT_SECRET=secret
JWT_IGNORE_URL=/public/sign/up,/public/log/in,/public/get/download/avatar,/public/**
BIZ_OUTPUT=out
Enter fullscreen mode Exit fullscreen mode

You can simply set JWT_IGNORE_URL to /public/** only. I add so many values here to show what kind of values can be used.

At the same time, config/config.go should be changed, too. Of course, you can directly use os.Getenv.

package config

import (
    "github.com/kelseyhightower/envconfig"
    "github.com/sirupsen/logrus"
)

type Config struct {
    DbConf  DbConfig
    JWTConf JWTConf
    BizConf BizConf
}

type BizConf struct {
    Output string
}

type JWTConf struct {
    Secret    []byte
    IgnoreUrl []string `split_words:"true"`
}

type DbConfig struct {
    Driver  string `default:"mysql"`
    Host    string `default:"localhost"`
    Port    string `default:"3306"`
    User    string
    Passwd  string
    Schema  string
    Charset string `default:"utf8mb4"`
}

func LoadFromEnv() *Config {
    var dbconf DbConfig
    err := envconfig.Process("db", &dbconf)
    if err != nil {
        logrus.Panicln("Error processing env", err)
    }
    var jwtConf JWTConf
    err = envconfig.Process("jwt", &jwtConf)
    if err != nil {
        logrus.Panicln("Error processing env", err)
    }
    var bizConf BizConf
    err = envconfig.Process("biz", &bizConf)
    if err != nil {
        logrus.Panicln("Error processing env", err)
    }
    return &Config{
        dbconf,
        jwtConf,
        bizConf,
    }
}
Enter fullscreen mode Exit fullscreen mode

JWT Middleware

As go-doudou is relying on gorilla/mux, so if you can write middlewares for gorilla/mux, you can also write ones for go-doudou.

package middleware

import (
    "context"
    "fmt"
    "github.com/dgrijalva/jwt-go"
    "github.com/gobwas/glob"
    "net/http"
    "os"
    "strings"
)

type ctxKey int

const userIdKey ctxKey = ctxKey(0)

func NewContext(ctx context.Context, id int) context.Context {
    return context.WithValue(ctx, userIdKey, id)
}

func FromContext(ctx context.Context) (int, bool) {
    userId, ok := ctx.Value(userIdKey).(int)
    return userId, ok
}

func Jwt(g glob.Glob) func(inner http.Handler) http.Handler {
    return func(inner http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if g.Match(r.RequestURI) {
                inner.ServeHTTP(w, r)
                return
            }
            authHeader := r.Header.Get("Authorization")
            tokenString := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))

            token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
                if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                    return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
                }
                return []byte(os.Getenv("BIZ_JWT_SECRET")), nil
            })
            if err != nil || !token.Valid {
                w.WriteHeader(401)
                w.Write([]byte("Unauthorised.\n"))
                return
            }

            claims := token.Claims.(jwt.MapClaims)
            if userId, exists := claims["userId"]; !exists {
                w.WriteHeader(401)
                w.Write([]byte("Unauthorised.\n"))
                return
            } else {
                inner.ServeHTTP(w, r.WithContext(NewContext(r.Context(), int(userId.(float64)))))
            }
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

Then you can use it like this srv.AddMiddleware(middleware.Jwt(glob.MustCompile(fmt.Sprintf("{%s}", conf.BizConf.JwtIgnoreUrl)))).

UploadAvatar Method

func (receiver *UsersvcImpl) UploadAvatar(ctx context.Context, avatar *v3.FileModel) (data string, err error) {
    defer avatar.Close()
    _ = os.MkdirAll(receiver.conf.BizConf.Output, os.ModePerm)
    out := filepath.Join(receiver.conf.BizConf.Output, avatar.Filename)
    var f *os.File
    f, err = os.OpenFile(out, os.O_WRONLY|os.O_CREATE, os.ModePerm)
    if err != nil {
        panic(err)
    }
    defer f.Close()
    _, err = io.Copy(f, avatar.Reader)
    if err != nil {
        panic(err)
    }
    userId, _ := middleware.FromContext(ctx)
    userDao := dao.NewUserDao(receiver.db)
    _, err = userDao.UpdateNoneZero(ctx, domain.User{
        Id:     userId,
        Avatar: out,
    })
    if err != nil {
        panic(err)
    }
    return "OK", nil
}
Enter fullscreen mode Exit fullscreen mode

NOTE: don't forget to add defer avatar.Close() to release resource.

 

Download Avatar

GetPublicDownloadAvatar Method

func (receiver *UsersvcImpl) GetPublicDownloadAvatar(ctx context.Context, userId int) (data *os.File, err error) {
    userDao := dao.NewUserDao(receiver.db)
    var get interface{}
    get, err = userDao.Get(ctx, userId)
    if err != nil {
        panic(err)
    }
    return os.Open(get.(domain.User).Avatar)
}
Enter fullscreen mode Exit fullscreen mode

 

User Pagination

Import Test Data

INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (2, 'crazyboy', '$2a$14$VaQLa/GbLAhRZvvTlgE8OOQgsBY4RDAJC5jkz13kjP9RlntdKBZVW', 'John Snow', '13552053960', 'IT dept.', '2021-12-28 06:41:00', '2021-12-28 14:59:20', null, 'out/wolf-wolves-snow-wolf-landscape-985ca149f06cd03b9f0ed8dfe326afdb.jpg');
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (4, 'david', '$2a$14$AKCs.u9vFUOCe5VwcmdfwOAkeiDtQYEgIB/nSU8/eemYwd91.qU.i', 'David Li', '13552053961', 'Admin dept.', '2021-12-28 12:12:32', '2021-12-28 14:59:20', null, '');
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (5, 'lucy', '$2a$14$n0.l54axUqnKGagylQLu7ee.yDrtLubxzM1qmOaHK9Ft2P09YtQUS', 'Lucy Zhang', '13552053962', 'Sales dept.', '2021-12-28 12:13:17', '2021-12-28 14:59:20', null, '');
INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (6, 'jack', '$2a$14$jFCwiZHcD7.DL/teao.Dl.HAFwk8wM2f1riH1fG2f52WYKqSiGZlC', 'Jack Chen', '', 'CEO Office', '2021-12-28 12:14:19', '2021-12-28 14:59:20', null, '');
Enter fullscreen mode Exit fullscreen mode

PageUsers Method

func (receiver *UsersvcImpl) PageUsers(ctx context.Context, pageQuery vo.PageQuery) (data vo.PageRet, err error) {
    userDao := dao.NewUserDao(receiver.db)
    var q query.Q
    q = query.C().Col("delete_at").IsNull()
    if stringutils.IsNotEmpty(pageQuery.Filter.Name) {
        q = q.And(query.C().Col("name").Like(fmt.Sprintf(`%s%%`, pageQuery.Filter.Name)))
    }
    if stringutils.IsNotEmpty(pageQuery.Filter.Dept) {
        q = q.And(query.C().Col("dept").Eq(pageQuery.Filter.Dept))
    }
    var page query.Page
    if len(pageQuery.Page.Orders) > 0 {
        for _, item := range pageQuery.Page.Orders {
            page = page.Order(query.Order{
                Col:  item.Col,
                Sort: sortenum.Sort(item.Sort),
            })
        }
    }
    if pageQuery.Page.PageNo == 0 {
        pageQuery.Page.PageNo = 1
    }
    page = page.Limit((pageQuery.Page.PageNo-1)*pageQuery.Page.Size, pageQuery.Page.Size)
    var ret query.PageRet
    ret, err = userDao.PageMany(ctx, page, q)
    if err != nil {
        panic(err)
    }
    var items []vo.UserVo
    for _, item := range ret.Items.([]domain.User) {
        var userVo vo.UserVo
        _ = copier.DeepCopy(item, &userVo)
        items = append(items, userVo)
    }
    data = vo.PageRet{
        Items:    items,
        PageNo:   ret.PageNo,
        PageSize: ret.PageSize,
        Total:    ret.Total,
        HasNext:  ret.HasNext,
    }
    return data, nil
}
Enter fullscreen mode Exit fullscreen mode

Test by Postman

Image description

 

User Detail

GetUser Method

func (receiver *UsersvcImpl) GetUser(ctx context.Context, userId int) (data vo.UserVo, err error) {
    userDao := dao.NewUserDao(receiver.db)
    var get interface{}
    get, err = userDao.Get(ctx, userId)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return vo.UserVo{}, lib.ErrUserNotFound
        } else {
            panic(err)
        }
    }
    user := get.(domain.User)
    return vo.UserVo{
        Id:       user.Id,
        Username: user.Username,
        Name:     user.Name,
        Phone:    user.Phone,
        Dept:     user.Dept,
    }, nil
}
Enter fullscreen mode Exit fullscreen mode

Test by Postman

Before test, you should add token to postman.

Image description

Then send a request with fake user id.

Image description

Then send a request with real user id.

Image description

 

Deploy Service

I want to show you how to deploy our service by docker-compose.
Dockerfile

FROM golang:1.16.6-alpine AS builder

ENV GO111MODULE=on
ARG user
ENV HOST_USER=$user
ENV GOPROXY=https://goproxy.cn,direct

WORKDIR /repo

ADD go.mod .
ADD go.sum .

ADD . ./

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache bash tzdata

ENV TZ="Asia/Shanghai"

RUN go mod vendor

RUN export GDD_VER=$(go list -mod=vendor -m -f '{{ .Version }}' github.com/unionj-cloud/go-doudou) && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -ldflags="-X 'github.com/unionj-cloud/go-doudou/svc/config.BuildUser=$HOST_USER' -X 'github.com/unionj-cloud/go-doudou/svc/config.BuildTime=$(date)' -X 'github.com/unionj-cloud/go-doudou/svc/config.GddVer=$GDD_VER'" -mod vendor -o api cmd/main.go

ENTRYPOINT ["/repo/api"]
Enter fullscreen mode Exit fullscreen mode

docker-compose.yml file

version: '3.9'

services:
  db:
    container_name: db
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: 1234
    ports:
      - 3306:3306
    volumes:
      - $PWD/my:/etc/mysql/conf.d
      - $PWD/sqlscripts:/docker-entrypoint-initdb.d
    networks:
      - tutorial

  usersvc:
    container_name: usersvc
    build:
      context: .
    environment:
      - GDD_BANNER=off
      - GDD_PORT=6060
      - DB_HOST=db
    expose:
      - "6060"
    ports:
      - "6060:6060"
    networks:
      - tutorial
    depends_on:
      - db

networks:
  tutorial:
    driver: bridge
Enter fullscreen mode Exit fullscreen mode

Then execute below command:

docker-compose -f docker-compose.yml up -d
Enter fullscreen mode Exit fullscreen mode

 

Source Code

Here is full source code with postman collection file Click Me.

Top comments (0)