TOC
- TODO
- Learning Goals
- Dev Requirements
- Install go-doudou
- Init Project
- Define Apis
- Generate Code
- Start Service
- Init Database
- Generate Domain and CRUD Code
- User SignUp
- User LogIn
- Upload Avatar
- Download Avatar
- User Pagination
- User Detail
- Deploy Service
- Source Code
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
- docker: recommend to install Desktop from official website
- IDE:goland
Install go-doudou
- If go version is below 1.16:
GO111MODULE=on go get -v github.com/unionj-cloud/go-doudou@v0.9.6
if go version is 1.16 or above:
go get -v github.com/unionj-cloud/go-doudou@v0.9.6
- Try to execute command
go-doudou -v
, if it printed below content, installation is successful:
➜ ~ go-doudou -v
go-doudou version v0.9.6
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"
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
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
- 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)
}
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
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
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
- 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
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
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"
}
}
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
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 ./...
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"
}
}
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
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.
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'
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`)
);
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
Execute docker compose
command to start mysql docker container:
docker-compose -f docker-compose.yml up -d
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
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
Execute below command to generate domain and dao layer(CRUD) code:
go-doudou ddl -r --dao --pre=t_
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:
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)
}
We will add *sqlx.DB
instance as field to UsersvcImpl
struct in svcimpl.go
file.
type UsersvcImpl struct {
conf *config.Config
db *sqlx.DB
}
Then make a small fix in factory function NewUsersvc
.
func NewUsersvc(conf *config.Config, db *sqlx.DB) Usersvc {
return &UsersvcImpl{
conf,
db,
}
}
In our main function, it has already injected *sqlx.DB
instance conn
into NewUsersvc
.
svc := service.NewUsersvc(conf, conn)
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"`
Here we can see we add ;unique
to dd
tag.
Then execute go-doudou ddl
command:
go-doudou ddl --pre=t_
There is no -r
flag because we need to sync User
struct to t_user
table.
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)
}
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
}
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
}
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
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)
}
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
Upload Avatar
Fix Domain
We add avatar
field to user
struct now:
Avatar string `dd:"type:varchar(255);extra:comment 'user avatar'"`
Before continue, we should remove dao/userdaosql.go
file first, then execute ddl command:
go-doudou ddl --dao --pre=t_
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
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
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,
}
}
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)))))
}
})
}
}
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
}
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)
}
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, '');
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
}
Test by Postman
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
}
Test by Postman
Before test, you should add token to postman.
Then send a request with fake user id.
Then send a request with real user id.
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"]
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
Then execute below command:
docker-compose -f docker-compose.yml up -d
Source Code
Here is full source code with postman collection file Click Me.
Top comments (0)