The author started to contact go language in 2015 and has been engaged in Web project development with GO language since then. He has used BeeGO, GIN, GRPC and other frameworks successively. These frameworks are excellent, and I’ve learned a lot by studying their source code. I have been working alone in the company, a person on the front and back of the work package, with ready-made frame is actually pretty good. Just then with the team, and then a lot of projects, begin to contact and learning agile development, project management, and other aspects of the theory and practice, found before and after the communication between different members and alignment is also need a lot of cost, especially if the front-end colleagues don’t understand the back-end, back-end colleagues don’t understand completely under the condition of front end will encounter a lot of headaches. Therefore, a micro-service framework with low code, easy to develop quickly, and convenient for communication and joint adjustment between front and back end colleagues was developed with go language, which is called Go-Doudou micro-service framework. The Go Doudou framework is mainly based on Gorilla MUX routing library for fast RESTful interface generation, and based on Hashicorp open source MemberList library for service registration, discovery and fault detection. It also supports the development of single application and micro-service application. This tutorial will be divided into several articles that show you how to develop a single RESTful interface using Go-Doudou, using a case study of user management services.

List of requirements

  • User registration
  • The user login
  • The user details
  • The user page
  • Upload the picture
  • Download the picture

Learning goals

  • JWT is used for permission verification for user details, user pages, and profile picture uploads
  • User registration, user login, and download profile picture interfaces are publicly accessible without authentication
  • Provides online interface documentation
  • Provide go language client SDK
  • Provide mock interface implementations
  • Implement real business logic
  • Go – Doudou built-in DDL table structure synchronization tool
  • Go – Doudou built-in DAO layer code generation and use

Preparing for development Environment

  • Docker environment: It is recommended to download and install docker official Desktop software, official installation document address
  • IDE: goland is recommended, but vscode is also available

Install the go – doudou

  • Configure the goproxy.cn proxy to speed up dependency downloads
export GOPROXY=https://goproxy.cn,direct
Copy the code
  • If you are using go version 1.16 below:
GO111MODULE=on  go get -v github.com/unionj-cloud/[email protected]
Copy the code

If you are using Go 1.16 or later:

Go get the -v github.com/unionj-cloud/[email protected]Copy the code
  • The synchronization of goproxy.cn will be delayed. If the preceding command fails, you can shut down the proxy and log on to the Internet scientifically
export GOPROXY=https://proxy.golang.org,direct
Copy the code
  • The above methods are not available, can be directly cloned to gitee source code, local installation
git clone [email protected]:unionj-cloud/go-doudou.git
Copy the code

Switch to the root path and run the following command:

go install
Copy the code
  • Execute the commandgo-doudou -vIf the following information is displayed, the installation is successful:
➜  ~ go-doudou -v
go-doudou version v0.8.6
Copy the code

Initialization engineering

Execute command:

go-doudou svc init usersvc 
Copy the code

If you cut to the usersvc path, you can see the following file structure generated:

➜ tutorials ll total 0 drwxr-xr-x 9 wubin1989 staff 288B 10 24 20:05 usersvc ➜ tutorials CD usersvc ➜ usersvc Git :(master) qualify 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 voCopy the code
  • Svc. go file: do interface design and definition
  • Vo folder: structure that defines interface input and output parameters
  • Dockerfile: Used for docker image packaging

Defines the interface

Let’s open the svc.go file and take a look:

package service

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

// Usersvc user management service
// Calls to user details, user paging and upload profile image interfaces require Bearer Token request headers
// User registration, user login, and download profile picture interfaces are publicly accessible without authentication
type Usersvc interface {
	// PageUsers User paging interface
	// How to define an interface for POST requests with content-type application/json
	PageUsers(ctx context.Context,
		// Paging request parameters
		query vo.PageQuery) (
		// Paging result
		data vo.PageRet,
		// Error message
		err error)

	// GetUser user details interface
	// Show how to define GET request interface with query string parameter
	GetUser(ctx context.Context,
		/ / user ID
		userId int) (
		// User details
		data vo.UserVo,
		// Error message
		err error)

	// PublicSignUp Interface for registering users
	// Show how to define POST requests and content-type is application/x-www-form-urlencoded interface
	PublicSignUp(ctx context.Context,
		/ / user name
		username string./ / password
		password string.// Graphic verification code
		code string(,)// Returns OK on success
		data string, err error)

	// PublicLogIn User login interface
	// Show how to authenticate and return the token
	PublicLogIn(ctx context.Context,
		/ / user name
		username string./ / password
		password string) (
		// token
		data string, err error)

	// UploadAvatar UploadAvatar
	// Show how to define the file upload interface
	// The signature of the function must have at least one []* v3.filemodel or * v3.filemodel parameter
	UploadAvatar(ctx context.Context,
		// User profile picture
		avatar *v3.FileModel) (
		// Returns OK on success
		data string, err error)

	// GetPublicDownloadAvatar Indicates the interface for downloading the avatar
	// Show how to define the file download interface
	// There must be only one * os.file parameter in the output parameter of the function signature
	GetPublicDownloadAvatar(ctx context.Context,
		/ / user ID
		userId int) (
		// File binary stream
		data *os.File, err error)
}
Copy the code

Each method in the above code has a comment. Please read it carefully. The interface definition supports document comments, only the // comments common to the GO language. These comments are exported as the value of the description parameter in the OpenAPI3.0 specification to the generated json document and to the go-doudou built-in online document, as demonstrated below.

The generated code

Run the following command to generate all the code needed to start a service

go-doudou svc http --handler -c go --doc
Copy the code

Explain the flag argument in the command:

  • Handler: indicates that the HTTP Handler interface implementation needs to be generated, that is, the code that parses the HTTP request parameters and encodes the return value
  • -c: indicates the client SDK for generating service interfaces. Currently, only the SDK is supportedgo. If you don’t need to generate the client SDK, you can leave this flag out because the generation process is time-consuming compared to other code
  • –doc: json document generated for the OpenAPI3.0 specification

This line of command is a common command used by the author and is recommended for use by others. And this command can be executed after each modification of the interface definition in the svc.go file, incrementally generating code. The rule is:

  • Handler. go files and json documents from the OpenAPI3.0 specification are always regenerated
  • The handlerImp. go and svcimp. go files are only generated incrementally and do not modify existing code
  • All other files check whether the file with the same name exists first and skip it if it does

To ensure that all dependencies are downloaded, it is best to execute this command again:

go mod tidy
Copy the code

Let’s look at the project structure at this point:

➜ usersvc git:(master) qualify 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.7k 10 25 09:18 usersvc_openapi3.json wubin1989 staff 5.7k 10 25 09:18 usersvc_openapi3.json  drwxr-xr-x 3 wubin1989 staff 96B 10 24 23:07 voCopy the code
  • Dockerfile file: used to package docker images
  • Client package: Generated GO client code
  • CMD package: contains the main.go file used to start the service
  • Config package: Used to load the configuration
  • Db package: Used to connect to the database
  • Svc.go file: Design interface
  • Svcimp.go file: this contains the mock interface implementation, which is then used to write real business logic based on business requirements
  • Transport package: Contains the HTTP handler interface and implementation, responsible for the specific interface input parameter parsing and output parameter serialization
  • Usersvc_openapi3. go file: used for online interface document function
  • Usersvc_openapi3. json file: an interface document that complies with OpenAPI 3.0 specifications
  • Vo package: Inside is the input and output parameter structure type of the interface

Start the service

go-doudou svc run
Copy the code

We can see the following output:

➜ usersvc git:(master) qualify go-doudou SVC run INFO[2021-12-28 22:39:35] Initializing logging reporter INFO[2021-12-28 22:39:35] ================ Registered Routes ================ INFO[2021-12-28 22:39:35] +----------------------+--------+-------------------------+ INFO[2021-12-28 22:39:35] | NAME | METHOD | PATTERN | INFO[2021-12-28 22:39:35] +----------------------+--------+-------------------------+ INFO[2021-12-28 22:39:35] | PageUsers | POST | /page/users | INFO[2021-12-28 22:39:35] | User | GET | /user | INFO[2021-12-28 22:39:35] | PublicSignUp | POST | /public/sign/up | INFO[2021-12-28 22:39:35] | PublicLogIn | POST | /public/log/in | INFO[2021-12-28 22:39:35] | UploadAvatar | POST | /upload/avatar | INFO[2021-12-28 22:39:35] | PublicDownloadAvatar | GET | /public/download/avatar | INFO[2021-12-28 22:39:35] | GetDoc | GET | /go-doudou/doc | INFO[2021-12-28 22:39:35] | GetOpenAPI | GET | /go-doudou/openapi.json | INFO[2021-12-28 22:39:35] | Prometheus | GET | /go-doudou/prometheus | INFO[2021-12-28 22:39:35] | GetRegistry | GET | /go-doudou/registry | INFO[2021-12-28 22:39:35] +----------------------+--------+-------------------------+ INFO[2021-12-28 22:39:35] = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = INFO 22:39:35 [2021-12-28] Started in 233.424 (including s INFO [in the 2021-12-28 s 22:39:35] Http server is listening on :6060Copy the code

When “Http Server is listening on :6060” appears, the service has been started and we have the mock service interface implementation. For example, we could request the /user interface with the following command to see what data is returned:

➜ usersvc git http://localhost:6060/user HTTP / 1.1: (master) ✗ HTTP 200 OK Content - Encoding: gzip Content - Length: 109 Content-Type: application/json; charset=UTF-8 Date: Mon, 01 Nov 2021 15:21:10 GMT Vary: Accept-Encoding { "data": { "Dept": "ZkkCmcLU", "Id": -1941954111002502016, "Name": "aiMtQ", "Phone": "XMAqXf" } }Copy the code

At this point you may notice that the field names of the returned data are capitalized, which is probably not what you want. In the vo package, the vo. Go file contains the go generate command:

//go:generate go-doudou name --file $GOFILE
Copy the code

This command uses the name of a utility built into the Go-Doudou framework. It can generate THE JSON tag following the structure field according to the specified naming convention. The default generation policy is a hump naming policy with a lowercase initial and supports snake naming. The unexported fields are skipped and only the JSON labels of exported fields are modified. Command line Execution command:

go generate ./...
Copy the code

Then restart the service, request /user interface, and you can see that the field name has changed to a hump with a lowercase first letter.

➜ usersvc git http://localhost:6060/user HTTP / 1.1: (master) ✗ HTTP 200 OK Content - Encoding: gzip Content - Length: 114 Content-Type: application/json; charset=UTF-8 Date: Tue, 02 Nov 2021 08:25:39 GMT Vary: Accept-Encoding { "data": { "dept": "wGAEEeveHp", "id": -816946940349962228, "name": "hquwOKl", "phone": "AriWmKYB" } }Copy the code

Refer to the documentation for more usage of the Name tool. At this point, because the structure in the VO package has changed the JSON tag, the OpenAPI document needs to be regenerated, otherwise the field names in the online document will be the same as before. Run the following command:

go-doudou svc http --doc
Copy the code

Let’s restart the service, type http://localhost:6060/go-doudou/doc in the address bar, and then enter the HTTP Basic user name admin and password admin to see what the online document looks like:

The interface and parameter descriptions in the online documentation are taken from the interface method and parameter annotations in svC.go.

Database and table structure preparation

To support Chinese characters, create the mysql configuration file my/custom.cnf in the root directory and paste the following contents:

[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'
Copy the code

Create the database initialization script sqlscripts/init. SQL in the root directory and paste the following contents:

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 'Mobile 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`)
);

INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (2.'peter'.'$2a$14$VaQLa/GbLAhRZvvTlgE8OOQgsBY4RDAJC5jkz13kjP9RlntdKBZVW'.'Zhang SAN Feng'.'13552053960'.'Technology'.'the 2021-12-28 06:41:00'.'the 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.'john'.'$2a$14$AKCs.u9vFUOCe5VwcmdfwOAkeiDtQYEgIB/nSU8/eemYwd91.qU.i'.'Li Shimin'.'13552053961'.Administration Department.'the 2021-12-28 12:12:32'.'the 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'.'Zhu Yuanzhang'.'13552053962'.'Sales Department'.'the 2021-12-28 12:13:17'.'the 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'.'Zhang Wuji'.' '.'President's office'.'the 2021-12-28 12:14:19'.'the 2021-12-28 14:59:20'.null.' ');
Copy the code

Create docker-comemess. yml file in the root directory and paste it into the following 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: bridgeCopy the code

Docker compose: docker compose: docker compose: docker compose

docker-compose -f docker-compose.yml up -d
Copy the code

You can view running containers using the docker ps command

➜ usersvc git:(Master) Qualify 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, ::3306->3306/ TCP, 33060/ TCP dbCopy the code

Generate domain and DAO layer code

Since the name of our initial schema is tutorial, we will first change the value of the environment variable DB_SCHEMA in the. Env file to tutorial

DB_SCHEMA=tutorial
Copy the code

Generate domain and DAO layer code by executing the following command:

go-doudou ddl -r --dao --pre=t_
Copy the code

Explain:

  • -r: Generates the GO structure from the database table structure
  • — DAO: generates DAO layer code
  • –pre: indicates that the table name has the prefix t_

At this point, you can see two more directories in the project:

Please refer to the specific usageDDL documentLet’s take a look at what CRUD methods are provided in the DAO /base.go file, which will be used later when implementing the specific 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) }Copy the code

Modify the UsersvcImpl structure of the svCimp. go file again

type UsersvcImpl struct {
	conf *config.Config
	db   *sqlx.DB
}
Copy the code

And the NewUsersvc method

func NewUsersvc(conf *config.Config, db *sqlx.DB) Usersvc {
	return &UsersvcImpl{
		conf,
		db,
	}
}
Copy the code

The generated main method already injected the mysql connection instance for us, so don’t change it

svc := service.NewUsersvc(conf, conn)
Copy the code

We will call the DB property of the UsersvcImpl structure directly from the interface implementation

User registration interface

Modify the domain

Since usernames must generally be unique, we need to change the domain/user.go file:

Username string     `dd:"type:varchar(45); Extra :comment 'username '; unique"`
Copy the code

Then run the DDL command

go-doudou ddl --pre=t_
Copy the code

This command has no -r argument, indicating updates from the GO structure to the table structure.

PublicSignUp method implementation

To implement the registration logic, we need to add a method CheckUsernameExists to the DAO layer code to determine whether the user name passed in has been registered. Change the DAO /userdao.go file first

package dao

import "context"

type UserDao interface {
	Base
	CheckUsernameExists(ctx context.Context, username string) (bool, error)
}
Copy the code

Create a new dao/ UserDaoImplext. go file and add the following code

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))
	iferr ! =nil {
		return false, err
	}
	users := many.([]domain.User)
	if len(users) > 0 {
		return true.nil
	}
	return false.nil
}
Copy the code

This enables a custom extension to the generated DAO layer code. If the fields of the user entity are added or decreased in the future, delete the userdaosql.go file and run the go doudou DDL –dao –pre=t_ command again to generate the userdaosql.go file. The existing DAO layer file will not be modified. Then the SignUp method is implemented

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)
	iferr ! =nil {
		panic(err)
	}
	if exists {
		panic(lib.ErrUsernameExists)
	}
	_, err = userDao.Insert(ctx, domain.User{
		Username: username,
		Password: hashPassword,
	})
	iferr ! =nil {
		panic(err)
	}
	return "OK".nil
}
Copy the code

If an error occurs, you can panic or return “”, lib.ErrUsernameExists. Since DDHTTP.Recover middleware has been added, it can automatically Recover from Panic and return error messages to the front end. Note that the HTTP status code is 500, not 200. Whenever an error parameter is returned from an interface method, the generated HTTP handler code defaults to 500. If you want to customize the code in the default generated HTTP handler, you can. When an interface definition is added or modified, run the go-doudou SVC HTTP — handler-c go –doc command again. The existing code is not overwritten, but is incrementally generated.

Postman test

Test the interface. This is the first request

This is the second request

User login interface

PublicLogIn method is implemented

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()))
	iferr ! =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)
}
Copy the code

This code is based on the input parameter username to find the database user, if not found or the password is incorrect, return “username or password error” error, if the password is correct, sign the token return. The JWT library used is Golang-jwt/JWT.

Postman test

Uploading profile Picture Interface

Modify the domain

There is an avatar field missing from the table, now we add:

Avatar   string     `dd:"type:varchar(255); Extra :comment 'user profile '"
Copy the code

Because new fields are added, delete the DAO/userdaOSQl. go file before running the DDL command

go-doudou ddl --dao --pre=t_
Copy the code

If multiple fields are added or deleted and multiple entities are involved, you can run the following command to delete all *sql.go files and generate them again

rm -rf dao/*sql.go
Copy the code

Modify. Env configuration

Add a three-line configuration. The JWT_ prefix indicates the configurations related to JWT token verification. The prefix Biz_ indicates the configurations related to actual services.

JWT_SECRET=secret
JWT_IGNORE_URL=/public/sign/up,/public/log/in,/public/get/download/avatar,/public/**
BIZ_OUTPUT=out
Copy the code

JWT_IGNORE_URL should be set to /public/** to indicate that both wildcard and full matching are supported. The config/config.go file also needs to be modified accordingly. You can also call the os.getenv method directly.

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(a) *Config {
	var dbconf DbConfig
	err := envconfig.Process("db", &dbconf)
	iferr ! =nil {
		logrus.Panicln("Error processing env", err)
	}
	var jwtConf JWTConf
	err = envconfig.Process("jwt", &jwtConf)
	iferr ! =nil {
		logrus.Panicln("Error processing env", err)
	}
	var bizConf BizConf
	err = envconfig.Process("biz", &bizConf)
	iferr ! =nil {
		logrus.Panicln("Error processing env", err)
	}
	return &Config{
		dbconf,
		jwtConf,
		bizConf,
	}
}
Copy the code

JWT validation middleware

Because go-Doudou’s HTTP router uses Gorilla/MUx, it is fully compatible with Gorilla/MUx middleware, and custom middleware is written the same way.

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(inner http.Handler) http.Handler {
	g := glob.MustCompile(fmt.Sprintf("{%s}", os.Getenv("JWT_IGNORE_URL")))
	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("JWT_SECRET")), nil
		})
		iferr ! =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)))))}})}Copy the code

UploadAvatar method implementation

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)
	iferr ! =nil {
		panic(err)
	}
	defer f.Close()
	_, err = io.Copy(f, avatar.Reader)
	iferr ! =nil {
		panic(err)
	}
	userId, _ := middleware.FromContext(ctx)
	userDao := dao.NewUserDao(receiver.db)
	_, err = userDao.UpdateNoneZero(ctx, domain.User{
		Id:     userId,
		Avatar: out,
	})
	iferr ! =nil {
		panic(err)
	}
	return "OK".nil
}
Copy the code

It is important to note here that the line defer avatar.close () must be written as soon as possible, which is the code to release the file descriptor resource.

Download profile picture Interface

The GetPublicDownloadAvatar method is implemented

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)
	iferr ! =nil {
		panic(err)
	}
	return os.Open(get.(domain.User).Avatar)
}
Copy the code

User Details Interface

GetUser method implementation

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)
	iferr ! =nil {
		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
}
Copy the code

Postman test

User paging interface

Import test data

INSERT INTO tutorial.t_user (id, username, password, name, phone, dept, create_at, update_at, delete_at, avatar) VALUES (2.'peter'.'$2a$14$VaQLa/GbLAhRZvvTlgE8OOQgsBY4RDAJC5jkz13kjP9RlntdKBZVW'.'Zhang SAN Feng'.'13552053960'.'Technology'.'the 2021-12-28 06:41:00'.'the 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.'john'.'$2a$14$AKCs.u9vFUOCe5VwcmdfwOAkeiDtQYEgIB/nSU8/eemYwd91.qU.i'.'Li Shimin'.'13552053961'.Administration Department.'the 2021-12-28 12:12:32'.'the 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'.'Zhu Yuanzhang'.'13552053962'.'Sales Department'.'the 2021-12-28 12:13:17'.'the 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'.'Zhang Wuji'.' '.'President's office'.'the 2021-12-28 12:14:19'.'the 2021-12-28 14:59:20'.null.' ');
Copy the code

PageUsers method implementation

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)
	iferr ! =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
}
Copy the code

Postman test

Service deployment

The docker-compose deployment service first modifies the Dockerfile

FROM golang:1.16.6-alpine AS Builder ENV GO111MODULE=on ARG 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"]Copy the code

Then modify docker-comemage.yml

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: bridgeCopy the code

Execute command at last

docker-compose -f docker-compose.yml up -d
Copy the code

If the usersvc container does not start successfully, it may be because the DB container has not been started completely. You can execute the above command several times.

conclusion

At this point, we have achieved all of our learning goals and implemented all of the interfaces in the requirements list. The full source code for the tutorial is here.