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 command
go-doudou -v
If 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 supported
go
. 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.