From ec0af20b1dedb428e2892cfdfb257c07471ff26f Mon Sep 17 00:00:00 2001 From: kwinwong Date: Tue, 18 Jun 2024 16:51:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E4=BA=8EGin=E6=A1=86=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E7=9A=84web=E6=9C=8D=E5=8A=A1=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E8=84=9A=E6=89=8B=E6=9E=B6=EF=BC=8C=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E5=BC=80=E5=8F=91=EF=BC=8C=E5=BF=AB=E9=80=9F?= =?UTF-8?q?=E5=BC=80=E5=8F=91Api=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + Dockerfile | 26 ++++ DockerfileBase | 8 ++ LICENSE.md | 20 +++ Makefile | 81 ++++++++++++ README.md | 157 +++++++++++++++++++++++ app/http/controller/api.go | 21 +++ app/http/controller/user.go | 60 +++++++++ app/http/middleware/cors.go | 32 +++++ app/http/middleware/jwt.go | 71 +++++++++++ app/http/request/user.go | 13 ++ app/http/response/common.go | 56 ++++++++ app/http/response/user.go | 55 ++++++++ app/http/service/user_service.go | 100 +++++++++++++++ app/model/user.go | 57 +++++++++ bin/.gitignore | 2 + cmd/main.go | 37 ++++++ cmd/migrate.go | 20 +++ cmd/server/cron.go | 19 +++ cmd/server/gin.go | 34 +++++ cmd/server/mq.go | 43 +++++++ cmd/version.go | 21 +++ common/services/.gitkeep | 0 config.yaml | 36 ++++++ core/global/core.go | 18 +++ core/gorm.go | 47 +++++++ core/init.go | 18 +++ core/logger/gorm_logger.go | 95 ++++++++++++++ core/logger/zap.go | 63 +++++++++ core/middleware/zap.go | 135 ++++++++++++++++++++ core/nsq_producer.go | 28 ++++ core/redis.go | 29 +++++ core/trans.go | 63 +++++++++ core/viper.go | 21 +++ crontab/cron_init.go | 79 ++++++++++++ crontab/schedule.go | 7 + crontab/testJob.go | 27 ++++ docker/nsq/docker-compose.yaml | 37 ++++++ docs/docs.go | 212 +++++++++++++++++++++++++++++++ docs/swagger.json | 189 +++++++++++++++++++++++++++ docs/swagger.yaml | 133 +++++++++++++++++++ go.mod | 32 +++++ main.go | 14 ++ mq/mq_base.go | 102 +++++++++++++++ mq/send_registered_email.go | 30 +++++ router/api.go | 29 +++++ router/router.go | 46 +++++++ router/swagger.go | 11 ++ router/warp.go | 49 +++++++ start.sh | 14 ++ util/common.go | 18 +++ util/gin.go | 78 ++++++++++++ util/ip.go | 54 ++++++++ 53 files changed, 2651 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 DockerfileBase create mode 100644 LICENSE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 app/http/controller/api.go create mode 100644 app/http/controller/user.go create mode 100644 app/http/middleware/cors.go create mode 100644 app/http/middleware/jwt.go create mode 100644 app/http/request/user.go create mode 100644 app/http/response/common.go create mode 100644 app/http/response/user.go create mode 100644 app/http/service/user_service.go create mode 100644 app/model/user.go create mode 100644 bin/.gitignore create mode 100644 cmd/main.go create mode 100644 cmd/migrate.go create mode 100644 cmd/server/cron.go create mode 100644 cmd/server/gin.go create mode 100644 cmd/server/mq.go create mode 100644 cmd/version.go create mode 100644 common/services/.gitkeep create mode 100644 config.yaml create mode 100644 core/global/core.go create mode 100644 core/gorm.go create mode 100644 core/init.go create mode 100644 core/logger/gorm_logger.go create mode 100644 core/logger/zap.go create mode 100644 core/middleware/zap.go create mode 100644 core/nsq_producer.go create mode 100644 core/redis.go create mode 100644 core/trans.go create mode 100644 core/viper.go create mode 100644 crontab/cron_init.go create mode 100644 crontab/schedule.go create mode 100644 crontab/testJob.go create mode 100644 docker/nsq/docker-compose.yaml create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 go.mod create mode 100644 main.go create mode 100644 mq/mq_base.go create mode 100644 mq/send_registered_email.go create mode 100644 router/api.go create mode 100644 router/router.go create mode 100644 router/swagger.go create mode 100644 router/warp.go create mode 100755 start.sh create mode 100644 util/common.go create mode 100644 util/gin.go create mode 100644 util/ip.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1afe30 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.vscode/ +.env +go.sum +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..29cab1a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# stage 1: build src code to binary +FROM gobase as builder + +ENV GOPROXY https://goproxy.cn +ENV GO111MODULE on + +COPY ./ /app/ +RUN cd /app && go mod tidy && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o main . + +# stage 2: use alpine as base image +FROM alpine:3.10 +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \ + apk update && \ + apk --no-cache add tzdata ca-certificates && \ + cp -f /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + # apk del tzdata && \ + rm -rf /var/cache/apk/* + + +COPY --from=builder /app/main /go/main +COPY ./config/settings.yml /go/config/settings.yml + +WORKDIR /go +EXPOSE 8000 + +CMD ["/go/main","server","-c", "/go/config/settings.yml"] \ No newline at end of file diff --git a/DockerfileBase b/DockerfileBase new file mode 100644 index 0000000..96dd047 --- /dev/null +++ b/DockerfileBase @@ -0,0 +1,8 @@ +# stage 1: build src code to binary +FROM golang:1.22.1-alpine3.18 as builder + +ENV GOPROXY https://goproxy.cn +ENV GO111MODULE on + +COPY ./ /app/ +RUN cd /app && go mod tidy && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o main . \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..bf7d134 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (c) 2011-2017 GitHub Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..73fa17f --- /dev/null +++ b/Makefile @@ -0,0 +1,81 @@ +PROJECT:=fast-api + +linuxBash=GOPROXY=https://goproxy.cn,direct GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o ./bin/${PROJECT}_${@} ./ + +macBash=GOPROXY=https://goproxy.cn,direct GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o ./bin/${PROJECT}_${@} ./ + +winBash=GOPROXY=https://goproxy.cn,direct GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc CGO_ENABLED=0 go build -ldflags "-s -w" -o ./bin/${PROJECT}_${@} ./ + +ifeq ($(OS),Windows_NT) + PLATFORM="windows" + autoBash=$(winBash) +else + ifeq ($(shell uname),Darwin) + PLATFORM="mac" + autoBash=$(macBash) + else + PLATFORM="linux" + autoBash=$(linuxBash) + endif +endif + +$(PLATFORM): + @echo 当前系统是$(PLATFORM) + $(autoBash) + +linux: + $(linuxBash) + +mac: + $(macBash) + +win: + $(winBash) + +clean: + rm -rf ./bin/${PROJECT}_* + + + + +PWD := $(shell pwd) +VERSION := $(shell git log --pretty=format:"%h" | head -n 1) + +build-docker-gobase: + @if [ ! $(shell docker image ls --format "{{.Repository}}"|grep gobase) ]; then docker build -t gobase:latest -f ./DockerfileBase .; fi + +# make build-linux +build-docker: + @echo $(VERSION) + @docker build -t fast-api:${VERSION} -t fast-api:latest . + @echo "build successful" + + +# make run +run: + # delete fast-api-api container + @if [ $(shell docker ps -aq --filter name=fast-api) ]; then docker rm -f fast-api; fi + @if [ $(shell docker ps -aq --filter name=fast-mq) ]; then docker rm -f fast-asynq; fi + @if [ $(shell docker ps -aq --filter name=fast-cron) ]; then docker rm -f fast-cron; fi + + + @docker run -d --name fast-api --privileged --network web -v ${PWD}/config.yaml/:config.yaml -v ${PWD}/static/:/go/static/ -v /var/log/fast-api/:/go/temp/ fast-api:${VERSION} ./main server -c config.yaml + @docker run -d --name fast-asynq --privileged --network web -v ${PWD}/config.yaml/:config.yaml -v /var/log/fast-api-asynq/:/go/temp/ fast-api:${VERSION} ./main mq -c config.yaml + @docker run -d --name fast-cron --privileged --network web -v ${PWD}/config.yaml/:config.yaml -v /var/log/fast-api-cron/:/go/temp/ fast-api:${VERSION} ./main cron -c config.yaml + + @echo "fastApi service is running..." + + # delete Tag= 的镜像 + @docker image prune -f + @docker images fast-api --format "{{.Repository}}:{{.Tag}}" | grep -v 'fast-api:latest' | grep -v 'fast-api:${VERSION}' | xargs -r docker image rm + @docker ps -a | grep "fast" + + +# make deploy +deploy-dev: + make build-docker-gobase + make build-docker + make run + +swag-api: + @swag init --parseDependency --parseInternal --parseGoList=false --parseDepth=6 -o ./docs -d . \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..206d810 --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +# fastApi + +fastApi: 基于Gin框架构建的web服务开发手脚架,统一规范开发,快速开发Api接口 + +https://github.com/kwinH/fastApi + +## 目的 + +本项目采用了一系列Golang中比较流行的组件,可以以本项目为基础快速搭建Restful Web API + +## 特色 + +本项目已经整合了许多开发API所必要的组件: + +1. [cobra](github.com/spf13/cobra): cobra既是一个用于创建强大现代CLI应用程序的库,也是一个生成应用程序和命令文件的程序 +2. [Gin](https://github.com/gin-gonic/gin): 轻量级Web框架,Go世界里最流行的Web框架 +3. [Gin-Cors](https://github.com/gin-contrib/cors): Gin框架提供的跨域中间件 +4. [GORM](https://gorm.io/index.html): ORM工具。本项目需要配合Mysql使用 +5. [Go-Redis](https://github.com/go-redis/redis): Golang Redis客户端 +6. [viper](github.com/spf13/viper): Viper是适用于Go应用程序的完整配置解决方案 +7. [JWT](github.com/golang-jwt/jwt): 使用jwt-go这个库来实现我们生成JWT和解析JWT的功能 +8. [Zap](go.uber.org/zap): Zap日志库,配置traceId,可进行链路追踪 +9. [swag](github.com/swaggo/swag): 使用swag快速生成RESTFul API文档 +10. [cron](github.com/robfig/cron/v3): cron是golang中广泛使用的一个定时任务库 +11. [go-nsq](github.com/nsqio/go-nsq): nsq 是一款基于 go 语言开发实现的分布式消息队列组件 + +本项目已经预先实现了一些常用的代码方便参考和复用: + +1. 创建了用户模型 +2. 实现了```/api/v1/user/register```用户注册接口 +3. 实现了```/api/v1/user/login```用户登录接口 +4. 实现了```/api/v1/user/me```用户资料接口(需要登录后获取token) + +本项目已经预先创建了一系列文件夹划分出下列模块: +``` +. +├── Dockerfile +├── LICENSE.md +├── Makefile +├── README.md +├── app +│   ├── http +│   │   ├── controller MVC框架的controller,负责协调各部件完成任务 +│   │   ├── middleware 中间件 +│   │   ├── request 请求参数结构体 +│   │   ├── response 响应结构体 +│   │   └── service 业务逻辑处理 +│   └── model 数据库模型 +│   └── user.go +├── bin +├── cmd 命令行 +│   ├── main.go +│   ├── migrate.go +│   ├── server +│   │   ├── cron.go +│   │   ├── gin.go +│   │   └── mq.go +│   └── version.go +├── common 公共的 +│   └── services 公共的服务 +├── config.yaml 配置文件 +├── core 一些核心组件初始化 +│   ├── global +│   │   └── core.go +│   ├── gorm.go +│   ├── init.go +│   ├── logger +│   │   ├── gorm_logger.go +│   │   └── zap.go +│   ├── middleware +│   │   └── zap.go +│   ├── nsq_producer.go +│   ├── redis.go +│   ├── trans.go +│   └── viper.go +├── crontab 定时任务 +│   ├── cron_init.go +│   ├── schedule.go +│   └── testJob.go +├── docker docker相关 +│   └── nsq +│   └── docker-compose.yaml +├── docs swagger生成的文档 +│   ├── docs.go +│   ├── swagger.json +│   └── swagger.yaml +├── go.mod +├── go.sum +├── log.log +├── main.go +├── mq 消息队列 +│   ├── mq_base.go +│   └── send_registered_email.go +├── router +│   ├── api.go +│   ├── router.go +│   ├── swagger.go +│   └── warp.go +├── start.sh +└── util 工具类 + ├── common.go + ├── gin.go + └── ip.go + +``` + +## config.yaml + +项目在启动的时候依赖config.yaml配置文件 + +## Go Mod + +本项目使用[Go Mod](https://github.com/golang/go/wiki/Modules)管理依赖。 + +```shell +go mod init go-crud +export GOPROXY=http://mirrors.aliyun.com/goproxy/ +go run main.go // 自动安装 +``` + +## 运行HTTP + +> 项目运行后启动在3000端口(可以修改,参考gin文档) + +```shell +go run main.go server -c config.yaml +``` + +## 定时任务 + +```shell +go run main.go cron -c config.yaml +``` + +## 消息队列 + +### 先启动nsq服务 + +```shell +cd docker/nsq +docker-compose up -d +``` + +### 开启config.yaml配置中的`nsq`选项 + +```yaml +... +nsq: + producer: 127.0.0.1:4150 + consumer: 127.0.0.1:4161 +``` + +### 运行消费者 + +```shell +go run main.go nq -c config.yaml +``` diff --git a/app/http/controller/api.go b/app/http/controller/api.go new file mode 100644 index 0000000..48266b8 --- /dev/null +++ b/app/http/controller/api.go @@ -0,0 +1,21 @@ +package controller + +import ( + "fastApi/app/model" + "github.com/gin-gonic/gin" +) + +type Api struct { + +} + +// CurrentUser 获取当前用户 +func (a Api)currentUser(c *gin.Context) *model.User { + if userId, _ := c.Get("userId"); userId != nil { + user, err := model.GetUser(userId) + if err == nil { + return &user + } + } + return nil +} \ No newline at end of file diff --git a/app/http/controller/user.go b/app/http/controller/user.go new file mode 100644 index 0000000..6b58229 --- /dev/null +++ b/app/http/controller/user.go @@ -0,0 +1,60 @@ +package controller + +import ( + "fastApi/app/http/request" + "fastApi/app/http/response" + "fastApi/app/http/service" + "github.com/gin-gonic/gin" +) + +type UserController struct { + Api +} + +// UserRegister 用户注册接口 +// @Summary 用户注册接口 +// @Description 用户注册接口 +// @Tags 用户相关接口 +// @Accept application/json +// @Produce application/json +// @Param Authorization header string true "用户令牌" +// @Param object query request.RegisterRequest false "查询参数" +// @Success 200 {object} response.Response +// @Router /user/register [post] [get] +func (a UserController) UserRegister(c *gin.Context) (res interface{}, err error) { + var param request.RegisterRequest + var service service.UserService + if err = c.ShouldBind(¶m); err != nil { + return + } + + res = service.Register(param) + return +} + +// UserLogin 用户登录接口 +// @Summary 用户登录接口1 +// @Description 用户登录接口2 +// @Tags 用户相关接口 +// @Accept application/json +// @Produce application/json +// @Param object query request.LoginRequest false "查询参数" +// @Success 200 {object} response.UserResponse +// @Router /user/login [post] +func (a UserController) UserLogin(c *gin.Context) (res interface{}, err error) { + var param = request.LoginRequest{} + var service service.UserService + if err = c.ShouldBind(¶m); err != nil { + return + } + + res = service.Login(c, param) + return +} + +// UserMe 用户详情 +func (a UserController) UserMe(c *gin.Context) (res interface{}, err error) { + user := a.currentUser(c) + res = response.BuildUserResponse(*user) + return +} diff --git a/app/http/middleware/cors.go b/app/http/middleware/cors.go new file mode 100644 index 0000000..cb2f54f --- /dev/null +++ b/app/http/middleware/cors.go @@ -0,0 +1,32 @@ +package middleware + +import ( + "regexp" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +// Cors 跨域配置 +func Cors() gin.HandlerFunc { + config := cors.DefaultConfig() + config.AllowMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"} + config.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Cookie"} + if gin.Mode() == gin.ReleaseMode { + // 生产环境需要配置跨域域名,否则403 + config.AllowOrigins = []string{"http://www.example.com"} + } else { + // 测试环境下模糊匹配本地开头的请求 + config.AllowOriginFunc = func(origin string) bool { + if regexp.MustCompile(`^http://127\.0\.0\.1:\d+$`).MatchString(origin) { + return true + } + if regexp.MustCompile(`^http://localhost:\d+$`).MatchString(origin) { + return true + } + return false + } + } + config.AllowCredentials = true + return cors.New(config) +} diff --git a/app/http/middleware/jwt.go b/app/http/middleware/jwt.go new file mode 100644 index 0000000..9888efb --- /dev/null +++ b/app/http/middleware/jwt.go @@ -0,0 +1,71 @@ +package middleware + +import ( + "errors" + "fastApi/app/model" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v4" + "github.com/spf13/viper" + "net/http" + "time" +) + +func JwtAuth() gin.HandlerFunc { + return func(ctx *gin.Context) { + + tokenString := ctx.GetHeader("Authorization") + + if tokenString == "" { + ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限不足"}) + ctx.Abort() + return + } + + claims, err := parseToken(tokenString) + if err != nil { + ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限不足"}) + ctx.Abort() + return + } + + ctx.Set("userId", claims.UserId) + ctx.Next() + } +} + +func parseToken(tokenStr string) (*model.Claims, error) { + token, err := jwt.ParseWithClaims(tokenStr, &model.Claims{}, func(token *jwt.Token) (i interface{}, err error) { + return []byte(viper.GetString("jwt.key")), nil + }) + if err != nil { + if ve, ok := err.(*jwt.ValidationError); ok { + if ve.Errors&jwt.ValidationErrorMalformed != 0 { + return nil, errors.New("that's not even a token") + } else if ve.Errors&jwt.ValidationErrorExpired != 0 { + return nil, errors.New("token is expired") + } else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 { + return nil, errors.New("token not active yet") + } else { + return nil, errors.New("couldn't handle this token") + } + } + } + if claims, ok := token.Claims.(*model.Claims); ok && token.Valid { + return claims, nil + } + return nil, errors.New("couldn't handle this token") +} + +func GenerateToken(userId uint) (string, error) { + claims := &model.Claims{ + UserId: userId, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(viper.GetInt64("jwt.timeout")) * time.Hour * time.Duration(1))), // 过期时间 + IssuedAt: jwt.NewNumericDate(time.Now()), // 签发时间 + NotBefore: jwt.NewNumericDate(time.Now()), // 生效时间 + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + return token.SignedString([]byte(viper.GetString("jwt.key"))) +} diff --git a/app/http/request/user.go b/app/http/request/user.go new file mode 100644 index 0000000..b371456 --- /dev/null +++ b/app/http/request/user.go @@ -0,0 +1,13 @@ +package request + +type LoginRequest struct { + UserName string `form:"user_name" json:"user_name" binding:"required,min=5,max=30" trans:"用户名"` //用户名 + Password string `form:"password" json:"password" binding:"required,min=8,max=40" trans:"密码"` //密码 +} + +type RegisterRequest struct { + Nickname string `form:"nickname" json:"nickname" binding:"required,min=2,max=30" trans:"昵称"` //昵称 + UserName string `form:"user_name" json:"user_name" binding:"required,min=5,max=30" trans:"用户名"` //用户名 + Password string `form:"password" json:"password" binding:"required,min=8,max=40" trans:"密码"` //密码 + PasswordConfirm string `form:"password_confirm" json:"password_confirm" binding:"required,min=8,max=40" trans:"确认密码"` //确认密码 +} diff --git a/app/http/response/common.go b/app/http/response/common.go new file mode 100644 index 0000000..31b6bf8 --- /dev/null +++ b/app/http/response/common.go @@ -0,0 +1,56 @@ +package response + +import "github.com/gin-gonic/gin" + +// Response 基础序列化器 +type Response struct { + Code int `json:"code"` //Code 0正确 + Data interface{} `json:"data,omitempty"` + Msg string `json:"msg"` + Error string `json:"error,omitempty"` +} + +// TrackedErrorResponse 有追踪信息的错误响应 +type TrackedErrorResponse struct { + Response + TrackID string `json:"track_id"` +} + +// 三位数错误编码为复用http原本含义 +// 五位数错误编码为应用自定义错误 +// 五开头的五位数错误编码为服务器端错误,比如数据库操作失败 +// 四开头的五位数错误编码为客户端错误,有时候是客户端代码写错了,有时候是用户操作错误 +const ( + // CodeCheckLogin 未登录 + CodeCheckLogin = 401 + // CodeNoRightErr 未授权访问 + CodeNoRightErr = 403 + // CodeDBError 数据库操作失败 + CodeDBError = 50001 + // CodeEncryptError 加密失败 + CodeEncryptError = 50002 + //CodeParamErr 各种奇奇怪怪的参数错误 + CodeParamErr = 40001 +) + +// Err 通用错误处理 +func Err(errCode int, msg string, data interface{}, err error) Response { + res := Response{ + Code: errCode, + Msg: msg, + Data: data, + } + // 生产环境隐藏底层报错 + if err != nil && gin.Mode() != gin.ReleaseMode { + res.Error = err.Error() + } + return res +} + +// ParamErr 各种参数错误 +func ParamErr(msg string, data interface{}, err error) Response { + if msg == "" { + msg = "参数错误" + } + return Err(CodeParamErr, msg, data, err) +} diff --git a/app/http/response/user.go b/app/http/response/user.go new file mode 100644 index 0000000..73683bf --- /dev/null +++ b/app/http/response/user.go @@ -0,0 +1,55 @@ +package response + +import ( + "fastApi/app/model" +) + +// User 用户序列化器 +type User struct { + ID uint `json:"id"` //ID + UserName string `json:"user_name"` //用户名 + Nickname string `json:"nickname"` //昵称 + Status string `json:"status"` //状态 + Avatar string `json:"avatar"` //头像 + CreatedAt int64 `json:"created_at"` //注册时间 +} + +type Data struct { + User User `json:"userInfo"` + Token string `json:"token"` +} + +type UserResponse struct { + Response + Data User +} + +// BuildUserResponse 序列化用户响应 +func BuildUserResponse(user model.User) UserResponse { + return UserResponse{ + Data: User{ + ID: user.ID, + UserName: user.UserName, + Nickname: user.Nickname, + Status: user.Status, + Avatar: user.Avatar, + CreatedAt: user.CreatedAt.Unix(), + }, + } +} + +func BuildLoginResponse(user model.User, token string) Response { + return Response{ + Data: Data{ + User: User{ + ID: user.ID, + UserName: user.UserName, + Nickname: user.Nickname, + Status: user.Status, + Avatar: user.Avatar, + CreatedAt: user.CreatedAt.Unix(), + }, + Token: token, + }, + } +} diff --git a/app/http/service/user_service.go b/app/http/service/user_service.go new file mode 100644 index 0000000..bcd01e4 --- /dev/null +++ b/app/http/service/user_service.go @@ -0,0 +1,100 @@ +package service + +import ( + "fastApi/app/http/middleware" + "fastApi/app/http/request" + "fastApi/app/http/response" + "fastApi/app/model" + "fastApi/core/global" + "github.com/gin-gonic/gin" +) + +// UserService 管理用户登录的服务 +type UserService struct { +} + +// Login 用户登录函数 +func (service *UserService) Login(c *gin.Context, loginRequest request.LoginRequest) response.Response { + var userModel model.User + + if err := global.DB.Where("user_name = ?", loginRequest.UserName).First(&userModel).Error; err != nil { + return response.ParamErr("账号或密码错误", nil, nil) + } + + if userModel.CheckPassword(loginRequest.Password) == false { + return response.ParamErr("账号或密码错误", nil, nil) + } + + token, err := middleware.GenerateToken(userModel.ID) + if err != nil { + return response.ParamErr("token生成失败", nil, nil) + } + + return response.BuildLoginResponse(userModel, token) +} + +// valid 验证表单 +func (service *UserService) valid(registerRequest request.RegisterRequest) *response.Response { + if registerRequest.PasswordConfirm != registerRequest.Password { + return &response.Response{ + Code: 40001, + Msg: "两次输入的密码不相同", + } + } + + count := int64(0) + global.DB.Model(&model.User{}).Where("nickname = ?", registerRequest.Nickname).Count(&count) + if count > 0 { + return &response.Response{ + Code: 40001, + Msg: "昵称被占用", + } + } + + count = 0 + global.DB.Model(&model.User{}).Where("user_name = ?", registerRequest.UserName).Count(&count) + if count > 0 { + return &response.Response{ + Code: 40001, + Msg: "用户名已经注册", + } + } + + return nil +} + +// Register 用户注册 +func (service *UserService) Register(registerRequest request.RegisterRequest) response.Response { + user := model.User{ + Nickname: registerRequest.Nickname, + UserName: registerRequest.UserName, + Status: model.Active, + } + + // 表单验证 + if err := service.valid(registerRequest); err != nil { + return *err + } + + // 加密密码 + if err := user.SetPassword(registerRequest.Password); err != nil { + return response.Err( + response.CodeEncryptError, + "密码加密失败", + nil, + err, + ) + } + + // 创建用户 + if err := global.DB.Create(&user).Error; err != nil { + return response.ParamErr("注册失败", nil, err) + } + + token, err := middleware.GenerateToken(user.ID) + if err != nil { + return response.ParamErr("token生成失败", nil, nil) + } + + return response.BuildLoginResponse(user, token) +} diff --git a/app/model/user.go b/app/model/user.go new file mode 100644 index 0000000..b22683d --- /dev/null +++ b/app/model/user.go @@ -0,0 +1,57 @@ +package model + +import ( + "fastApi/core/global" + "github.com/golang-jwt/jwt/v4" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +// User 用户模型 +type User struct { + gorm.Model + UserName string + Password string + Nickname string + Status string + Avatar string `gorm:"size:1000"` +} + +type Claims struct { + UserId uint + jwt.RegisteredClaims +} + +const ( + // PassWordCost 密码加密难度 + PassWordCost = 12 + // Active 激活用户 + Active string = "active" + // Inactive 未激活用户 + Inactive string = "inactive" + // Suspend 被封禁用户 + Suspend string = "suspend" +) + +// GetUser 用ID获取用户 +func GetUser(ID interface{}) (User, error) { + var user User + result := global.DB.First(&user, ID) + return user, result.Error +} + +// SetPassword 设置密码 +func (user *User) SetPassword(password string) error { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), PassWordCost) + if err != nil { + return err + } + user.Password = string(bytes) + return nil +} + +// CheckPassword 校验密码 +func (user *User) CheckPassword(password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) + return err == nil +} diff --git a/bin/.gitignore b/bin/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/bin/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..83c67c2 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "fastApi/cmd/server" + "fastApi/core" + "fmt" + "github.com/spf13/cobra" +) + +var RootCmd = &cobra.Command{ + Use: "", + Short: "这是 cobra 测试程序主入口", + Long: `这是 cobra 测试程序主入口, 无参数启动时进入`, + PersistentPreRun: persistentPreRun, + Run: runRoot, +} + +func init() { + RootCmd.PersistentFlags().StringVarP(&core.ConfigFilePath, "config", "c", "", "Start server with provided configuration file") + RootCmd.AddCommand(server.StartApi) + RootCmd.AddCommand(server.StartCmd) + RootCmd.AddCommand(server.StartMQ) +} + +func persistentPreRun(cmd *cobra.Command, args []string) { + core.CortInit() +} + +func Execute() { + if err := RootCmd.Execute(); err != nil { + panic(err) + } +} + +func runRoot(cmd *cobra.Command, args []string) { + fmt.Print("欢迎使用gin脚手架fastApi 可以使用 -h 查看命令") +} diff --git a/cmd/migrate.go b/cmd/migrate.go new file mode 100644 index 0000000..622fd17 --- /dev/null +++ b/cmd/migrate.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "fastApi/app/model" + "fastApi/core/global" + "github.com/spf13/cobra" +) + +func init() { + serverCmd := &cobra.Command{ + Use: "migrate", + Short: "Initialize the database", + Run: migrate, + } + RootCmd.AddCommand(serverCmd) +} + +func migrate(cmd *cobra.Command, args []string) { + _ = global.DB.AutoMigrate(&model.User{}) +} diff --git a/cmd/server/cron.go b/cmd/server/cron.go new file mode 100644 index 0000000..54d5aab --- /dev/null +++ b/cmd/server/cron.go @@ -0,0 +1,19 @@ +package server + +import ( + "fastApi/crontab" + "github.com/spf13/cobra" +) + +var StartCmd = &cobra.Command{ + Use: "cron", + Short: "启动定时任务", + Long: "bin/fastApi cron -c config/settings.yml", + Run: crontabStart, +} + +func crontabStart(cmd *cobra.Command, args []string) { + crontab.CronInit() + + select {} +} diff --git a/cmd/server/gin.go b/cmd/server/gin.go new file mode 100644 index 0000000..be9a0f2 --- /dev/null +++ b/cmd/server/gin.go @@ -0,0 +1,34 @@ +package server + +import ( + "fastApi/core" + "fastApi/router" + "fmt" + "github.com/fvbock/endless" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "log" +) + +var StartApi = &cobra.Command{ + Use: "server", + Short: "start Api Server", + Long: "bin/fastApi server -c config.yaml", + Run: serverStart, +} + +func serverStart(cmd *cobra.Command, args []string) { + if err := core.InitTrans("zh"); err != nil { + panic(fmt.Sprintf("init trans failed, err:%v\n", err)) + } + + // 装载路由 + r := router.NewRouter() + + s := endless.NewServer(viper.GetString("api.port"), r) + err := s.ListenAndServe() + if err != nil { + log.Printf("server err: %v", err) + } + // r.Run(viper.GetString("api.port")) +} diff --git a/cmd/server/mq.go b/cmd/server/mq.go new file mode 100644 index 0000000..ab7aefa --- /dev/null +++ b/cmd/server/mq.go @@ -0,0 +1,43 @@ +package server + +import ( + "fastApi/mq" + _ "fastApi/mq" + "github.com/nsqio/go-nsq" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "time" +) + +var StartMQ = &cobra.Command{ + Use: "mq", + Short: "启动消息队列消费者", + Long: "bin/fastApi mq -c config/settings.yml", + Run: mqStart, +} + +func mqStart(cmd *cobra.Command, args []string) { + address := viper.GetString("nsq.consumer") + for _, mq := range mq.MQList { + initConsumer(mq.GetTopic(), mq.GetChannel(), address, mq) + } + + select {} +} + +// 初始化消费者 +func initConsumer(topic string, channel string, address string, handler nsq.Handler) { + cfg := nsq.NewConfig() + cfg.LookupdPollInterval = time.Second //设置重连时间 + c, err := nsq.NewConsumer(topic, channel, cfg) // 新建一个消费者 + if err != nil { + panic(err) + } + c.SetLogger(nil, 0) //屏蔽系统日志 + c.AddHandler(handler) // 添加消费者接口 + + //建立NSQLookupd连接 + if err := c.ConnectToNSQLookupd(address); err != nil { + panic(err) + } +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..00489b8 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" +) + +var serverCmd = &cobra.Command{ + Use: "version", + Short: "version 子命令.", + Long: "这是一个version 子命令", + Run: runVersion, +} + +func init() { + RootCmd.AddCommand(serverCmd) +} + +func runVersion(cmd *cobra.Command, args []string) { + fmt.Println("version is 1.0.0") +} diff --git a/common/services/.gitkeep b/common/services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..01f0e13 --- /dev/null +++ b/config.yaml @@ -0,0 +1,36 @@ +api: + port: ":3000" + mode: "debug" + +database: + host: "127.0.0.1" + port: "3306" + db: "oorm_demo" + username: "root" + password: "root" + charset: "utf8mb4" + max_idle_conn: 10 + max_open_conn: 20 + +redis: + host: "127.0.0.1" + port: "6379" + password: "" + db: 0 + + +jwt: + # token 密钥 + key: "dgE7B6SPrS%9yLE" + # token 过期时间 单位:小时 + timeout: 72 + +logger: + # 日志存放路径 + path: ./log.log + # 日志等级: debug, info, warn, error + level: info + +#nsq: +# producer: 127.0.0.1:4150 +# consumer: 127.0.0.1:4161 \ No newline at end of file diff --git a/core/global/core.go b/core/global/core.go new file mode 100644 index 0000000..ea8b051 --- /dev/null +++ b/core/global/core.go @@ -0,0 +1,18 @@ +package global + +import ( + ut "github.com/go-playground/universal-translator" + "github.com/go-redis/redis" + "github.com/nsqio/go-nsq" + "go.uber.org/zap" + "gorm.io/gorm" +) + +var ( + Trans ut.Translator // 定义一个全局翻译器T + Log *zap.Logger + SLog *zap.SugaredLogger + DB *gorm.DB // DB 数据库链接单例 + Redis *redis.Client + Producer *nsq.Producer +) diff --git a/core/gorm.go b/core/gorm.go new file mode 100644 index 0000000..fde628e --- /dev/null +++ b/core/gorm.go @@ -0,0 +1,47 @@ +package core + +import ( + "fastApi/core/global" + logger2 "fastApi/core/logger" + "fmt" + "github.com/spf13/viper" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +// Database 在中间件中初始化mysql链接 +func Database() { + host := viper.GetString("database.host") + if host == "" { + return + } + + newLogger := logger2.NewGormLog(global.Log) + db, err := gorm.Open(mysql.Open( + fmt.Sprintf( + "%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local", + viper.GetString("database.username"), + viper.GetString("database.password"), + host, + viper.GetString("database.port"), + viper.GetString("database.db"), + viper.GetString("database.charset"), + )), &gorm.Config{ + Logger: newLogger, + }) + // Error + if err != nil { + panic(err) + } + sqlDB, err := db.DB() + if err != nil { + panic(err) + } + + //设置连接池 + //空闲 + sqlDB.SetMaxIdleConns(viper.GetInt("database.max_idle_conn")) + //打开 + sqlDB.SetMaxOpenConns(viper.GetInt("database.max_open_conn")) + global.DB = db +} diff --git a/core/init.go b/core/init.go new file mode 100644 index 0000000..e0059f9 --- /dev/null +++ b/core/init.go @@ -0,0 +1,18 @@ +package core + +import ( + "fastApi/core/logger" +) + +func CortInit() { + + ViperInit() + + logger.InitLogger() + + Database() + + RedisInit() + + NsqProducerInit() +} diff --git a/core/logger/gorm_logger.go b/core/logger/gorm_logger.go new file mode 100644 index 0000000..74bc89b --- /dev/null +++ b/core/logger/gorm_logger.go @@ -0,0 +1,95 @@ +package logger + +import ( + "context" + "path/filepath" + "runtime" + "strings" + "time" + + "go.uber.org/zap" + gormlogger "gorm.io/gorm/logger" +) + +type Logger struct { + ZapLogger *zap.Logger + LogLevel gormlogger.LogLevel + SlowThreshold time.Duration + SkipCallerLookup bool + IgnoreRecordNotFoundError bool +} + +func NewGormLog(zapLogger *zap.Logger) Logger { + return Logger{ + ZapLogger: zapLogger, + LogLevel: gormlogger.Info, + SlowThreshold: 100 * time.Millisecond, + SkipCallerLookup: false, + IgnoreRecordNotFoundError: false, + } +} + +func (l Logger) SetAsDefault() { + gormlogger.Default = l +} + +func (l Logger) LogMode(level gormlogger.LogLevel) gormlogger.Interface { + return Logger{ + ZapLogger: l.ZapLogger, + SlowThreshold: l.SlowThreshold, + LogLevel: level, + SkipCallerLookup: l.SkipCallerLookup, + IgnoreRecordNotFoundError: l.IgnoreRecordNotFoundError, + } +} + +func (l Logger) Info(ctx context.Context, str string, args ...interface{}) { + if l.LogLevel < gormlogger.Info { + return + } + l.logger().Sugar().Infof("mysql:"+str, args...) +} + +func (l Logger) Warn(ctx context.Context, str string, args ...interface{}) { + if l.LogLevel < gormlogger.Warn { + return + } + l.logger().Sugar().Warnf("mysql:"+str, args...) +} + +func (l Logger) Error(ctx context.Context, str string, args ...interface{}) { + if l.LogLevel < gormlogger.Error { + return + } + l.logger().Sugar().Errorf("mysql:"+str, args...) +} + +func (l Logger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) { + if l.LogLevel <= 0 { + return + } + elapsed := time.Since(begin) + sql, rows := fc() + l.logger().Info("mysql:trace", zap.Duration("runtime", elapsed), zap.Int64("rows", rows), zap.String("sql", sql)) +} + +var ( + gormPackage = filepath.Join("gorm.io", "gorm") + zapgormPackage = filepath.Join("moul.io", "zapgorm2") +) + +func (l Logger) logger() *zap.Logger { + for i := 2; i < 15; i++ { + _, file, _, ok := runtime.Caller(i) + + switch { + case !ok: + case strings.HasSuffix(file, "_test.go"): + case strings.Contains(file, gormPackage): + case strings.Contains(file, zapgormPackage): + default: + return l.ZapLogger.WithOptions(zap.AddCallerSkip(i)) + } + } + return l.ZapLogger +} diff --git a/core/logger/zap.go b/core/logger/zap.go new file mode 100644 index 0000000..29e9a04 --- /dev/null +++ b/core/logger/zap.go @@ -0,0 +1,63 @@ +package logger + +import ( + "fastApi/core/global" + "github.com/google/uuid" + "github.com/spf13/viper" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "io" + "os" +) + +const TraceId = "traceId" + +var logger *zap.Logger + +func InitLogger() *zap.Logger { + + logLevel := zapcore.InfoLevel + switch viper.GetString("logger.level") { + case "debug": + logLevel = zapcore.DebugLevel + case "info": + logLevel = zapcore.InfoLevel + case "warn": + logLevel = zapcore.WarnLevel + case "error": + logLevel = zapcore.ErrorLevel + } + + zapCore := zapcore.NewCore( + getEncoder(), + getLogWriter(), + logLevel, + ) + + global.Log = zap.New(zapCore) + logger = global.Log + return logger +} + +func getEncoder() zapcore.Encoder { + encoderConfig := zap.NewProductionEncoderConfig() + encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder + return zapcore.NewJSONEncoder(encoderConfig) +} + +func getLogWriter() zapcore.WriteSyncer { + file, _ := os.Create(viper.GetString("logger.path")) + ws := io.MultiWriter(file, os.Stdout) // 打印到控制台和文件 + return zapcore.AddSync(ws) +} + +func CalcTraceId() (traceId string) { + return uuid.New().String() +} + +func With(fields ...zap.Field) { + global.Log = logger.With(fields...) + global.SLog = global.Log.Sugar() + global.DB.Logger = NewGormLog(global.Log) +} diff --git a/core/middleware/zap.go b/core/middleware/zap.go new file mode 100644 index 0000000..a5bee90 --- /dev/null +++ b/core/middleware/zap.go @@ -0,0 +1,135 @@ +package middleware + +import ( + "encoding/json" + "fastApi/core/global" + "fastApi/core/logger" + "fastApi/util" + "net" + "net/http" + "net/http/httputil" + "os" + "runtime/debug" + "strings" + "time" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// GinZap returns a gin.HandlerFunc using configs +func GinZap(confSkipPaths []string) gin.HandlerFunc { + + skipPaths := make(map[string]bool, len(confSkipPaths)) + for _, path := range confSkipPaths { + skipPaths[path] = true + } + + return func(c *gin.Context) { + logger := global.Log + start := time.Now() + // some evil middlewares modify this values + path := c.Request.URL.Path + // query := c.Request.URL.RawQuery + params := util.GetParams(c) + + c.Next() + + if _, ok := skipPaths[path]; !ok { + end := time.Now() + runtime := end.Sub(start) + + if len(c.Errors) > 0 { + // Append error field if this is an erroneous request. + for _, e := range c.Errors.Errors() { + logger.Error(e) + } + } else { + headers, _ := json.Marshal(c.Request.Header) + + paramsJson, _ := json.Marshal(params) + + fields := []zapcore.Field{ + zap.Int("userId", c.GetInt("userId")), + zap.Int("status", c.Writer.Status()), + zap.String("host", c.Request.Host), + zap.String("url", c.Request.URL.String()), + zap.String("method", c.Request.Method), + zap.String("ip", c.ClientIP()), + zap.String("headers", string(headers)), + zap.String("params", string(paramsJson)), + zap.Duration("runtime", runtime), + } + + logger.Info("", fields...) + } + } + } +} + +// RecoveryWithZap returns a gin.HandlerFunc (middleware) +// that recovers from any panics and logs requests using uber-go/zap. +// All errors are logged using zap.Error(). +// stack means whether output the stack info. +// The stack info is easy to find where the error occurs but the stack info is too large. +func RecoveryWithZap(stack bool) gin.HandlerFunc { + return func(c *gin.Context) { + logger := global.Log + defer func() { + if err := recover(); err != nil { + // Check for a broken connection, as it is not really a + // condition that warrants a panic stack trace. + var brokenPipe bool + if ne, ok := err.(*net.OpError); ok { + if se, ok := ne.Err.(*os.SyscallError); ok { + if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { + brokenPipe = true + } + } + } + + httpRequest, _ := httputil.DumpRequest(c.Request, false) + if brokenPipe { + logger.Error(c.Request.URL.Path, + zap.Any("error", err), + zap.String("request", string(httpRequest)), + ) + // If the connection is dead, we can't write a status to it. + c.Error(err.(error)) // nolint: errcheck + c.Abort() + return + } + + if stack { + logger.Error("[Recovery from panic]", + zap.Time("time", time.Now()), + zap.Any("error", err), + zap.String("request", string(httpRequest)), + zap.String("stack", string(debug.Stack())), + ) + } else { + logger.Error("[Recovery from panic]", + zap.Time("time", time.Now()), + zap.Any("error", err), + zap.String("request", string(httpRequest)), + ) + } + c.AbortWithStatus(http.StatusInternalServerError) + } + }() + c.Next() + } +} + +func AddTraceId() gin.HandlerFunc { + return func(ctx *gin.Context) { + // 每个请求生成的请求traceId具有全局唯一性 + traceId := logger.CalcTraceId() + ctx.Set(logger.TraceId, traceId) + logger.With( + zap.String("traceId", traceId), + ) + ctx.Next() + } +} diff --git a/core/nsq_producer.go b/core/nsq_producer.go new file mode 100644 index 0000000..7c03f2e --- /dev/null +++ b/core/nsq_producer.go @@ -0,0 +1,28 @@ +package core + +import ( + "fastApi/core/global" + "github.com/nsqio/go-nsq" + "github.com/spf13/viper" +) + +func NsqProducerInit() { + // 初始化生产者 + addr := viper.GetString("nsq.producer") + + if addr == "" { + return + } + + producer, err := nsq.NewProducer(addr, nsq.NewConfig()) + if err != nil { + panic(err) + } + + err = producer.Ping() + if err != nil { + panic(err) + } + + global.Producer = producer +} diff --git a/core/redis.go b/core/redis.go new file mode 100644 index 0000000..a9b6be2 --- /dev/null +++ b/core/redis.go @@ -0,0 +1,29 @@ +package core + +import ( + "fastApi/core/global" + "github.com/go-redis/redis" + "github.com/spf13/viper" +) + +func RedisInit() { + host := viper.GetString("redis.host") + if host == "" { + return + } + + client := redis.NewClient(&redis.Options{ + Addr: viper.GetString("redis.host") + ":" + viper.GetString("redis.port"), + Password: viper.GetString("redis.password"), + DB: viper.GetInt("redis.db"), + MaxRetries: 1, + }) + + _, err := client.Ping().Result() + + if err != nil { + panic("连接Redis不成功" + err.Error()) + } + + global.Redis = client +} diff --git a/core/trans.go b/core/trans.go new file mode 100644 index 0000000..db35ee9 --- /dev/null +++ b/core/trans.go @@ -0,0 +1,63 @@ +package core + +import ( + "fastApi/core/global" + "fmt" + "github.com/gin-gonic/gin/binding" + "github.com/go-playground/locales/en" + "github.com/go-playground/locales/zh" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + enTranslations "github.com/go-playground/validator/v10/translations/en" + zhTranslations "github.com/go-playground/validator/v10/translations/zh" + "reflect" +) + +// InitTrans 初始化翻译器 +func InitTrans(locale string) (err error) { + // 修改gin框架中的Validator引擎属性,实现自定制 + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + + zhT := zh.New() // 中文翻译器 + enT := en.New() // 英文翻译器 + + // 注册一个获取trans tag的自定义方法 + v.RegisterTagNameFunc(func(fld reflect.StructField) string { + name := fld.Tag.Get("trans") + + if name == "" { + name = fld.Tag.Get("json") + } + + if name == "-" { + return "" + } + return name + }) + + // 第一个参数是备用(fallback)的语言环境 + // 后面的参数是应该支持的语言环境(支持多个) + // uni := ut.New(zhT, zhT) 也是可以的 + uni := ut.New(enT, zhT, enT) + + // locale 通常取决于 http 请求头的 'Accept-Language' + var ok bool + // 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找 + global.Trans, ok = uni.GetTranslator(locale) + if !ok { + return fmt.Errorf("uni.GetTranslator(%s) failed", locale) + } + + // 注册翻译器 + switch locale { + case "en": + err = enTranslations.RegisterDefaultTranslations(v, global.Trans) + case "zh": + err = zhTranslations.RegisterDefaultTranslations(v, global.Trans) + default: + err = enTranslations.RegisterDefaultTranslations(v, global.Trans) + } + return + } + return +} diff --git a/core/viper.go b/core/viper.go new file mode 100644 index 0000000..7916533 --- /dev/null +++ b/core/viper.go @@ -0,0 +1,21 @@ +package core + +import ( + "fmt" + "github.com/spf13/viper" +) +var ConfigFilePath string +func ViperInit() { + if ConfigFilePath == "" { // 优先级: 命令行 > 环境变量 > 默认值 + ConfigFilePath = "./config.yaml" + fmt.Printf("您正在使用ConfigFilePath的默认值,ConfigFilePath的路径为%v\n", ConfigFilePath) + } else { + fmt.Printf("您正在使用命令行的-c参数传递的值,ConfigFilePath的路径为%v\n", ConfigFilePath) + } + + viper.SetConfigFile(ConfigFilePath) + err := viper.ReadInConfig() // 查找并读取配置文件 + if err != nil { // 处理读取配置文件的错误 + panic(fmt.Errorf("Fatal error ConfigFilePath file: %s \n", err)) + } +} diff --git a/crontab/cron_init.go b/crontab/cron_init.go new file mode 100644 index 0000000..e0d812d --- /dev/null +++ b/crontab/cron_init.go @@ -0,0 +1,79 @@ +package crontab + +import ( + "context" + "fastApi/core/global" + "fastApi/core/logger" + "github.com/google/uuid" + "github.com/robfig/cron/v3" + + "go.uber.org/zap" +) + +type InterfaceCron interface { + getSpec() string + getName() string + Run(context.Context) +} + +var Cron *cron.Cron +var CronList []InterfaceCron + +type Logger struct { + Log *zap.SugaredLogger +} + +func (l Logger) Info(msg string, keysAndValues ...interface{}) { + l.Log.Debug(msg, keysAndValues) +} + +func (l Logger) Error(err error, msg string, keysAndValues ...interface{}) { + l.Log.Error(err, msg, keysAndValues) +} + +func CronInit() { + newlog := Logger{ + Log: global.Log.Sugar(), + } + Cron = cron.New( + cron.WithChain( + cron.Recover(newlog), + cron.DelayIfStillRunning(newlog), + cron.SkipIfStillRunning(newlog), + ), + cron.WithSeconds(), + cron.WithLogger(newlog), + ) + + Schedule() + + Cron.Start() +} + +func WithRequestId(name, traceId string) { + logger.With( + zap.String("traceId", traceId), + zap.String("name", name), + ) +} + +func BaseCronFuc(name string, cmd func(context.Context)) func() { + return func() { + traceId := uuid.New().String() + ctx := context.WithValue(context.Background(), logger.TraceId, traceId) + + WithRequestId(name, traceId) + cmd(ctx) + } +} + +func AddCron(cmd InterfaceCron) { + Cron.AddFunc( + cmd.getSpec(), + BaseCronFuc( + cmd.getName(), + func(ctx context.Context) { + cmd.Run(ctx) + }), + ) +} diff --git a/crontab/schedule.go b/crontab/schedule.go new file mode 100644 index 0000000..853be72 --- /dev/null +++ b/crontab/schedule.go @@ -0,0 +1,7 @@ +package crontab + +func Schedule() { + for _, cron := range CronList { + AddCron(cron) + } +} diff --git a/crontab/testJob.go b/crontab/testJob.go new file mode 100644 index 0000000..fae6163 --- /dev/null +++ b/crontab/testJob.go @@ -0,0 +1,27 @@ +package crontab + +import ( + "context" + "fastApi/app/model" + "fastApi/core/global" +) + +func init() { + CronList = append(CronList, testJob{}) +} + +type testJob struct { +} + +func (j testJob) getSpec() string { + return "@every 3s" +} + +func (j testJob) getName() string { + return "test1" +} + +func (j testJob) Run(ctx context.Context) { + model.GetUser(1) + global.Log.Info("tick every 1 second run once") +} diff --git a/docker/nsq/docker-compose.yaml b/docker/nsq/docker-compose.yaml new file mode 100644 index 0000000..3905d96 --- /dev/null +++ b/docker/nsq/docker-compose.yaml @@ -0,0 +1,37 @@ + +version: '2' +services: + nsqlookupd: + image: nsqio/nsq + command: /nsqlookupd + networks: + - nsq-network + hostname: nsqlookupd + ports: + - "4161:4161" + - "4160:4160" + nsqd: + image: nsqio/nsq + command: /nsqd --lookupd-tcp-address=nsqlookupd:4160 -broadcast-address=nsqd + depends_on: + - nsqlookupd + hostname: nsqd + networks: + - nsq-network + ports: + - "4151:4151" + - "4150:4150" + nsqadmin: + image: nsqio/nsq + command: /nsqadmin --lookupd-http-address=nsqlookupd:4161 + depends_on: + - nsqlookupd + hostname: nsqadmin + ports: + - "4171:4171" + networks: + - nsq-network + +networks: + nsq-network: + driver: bridge \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..b942317 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,212 @@ +// Package docs GENERATED BY SWAG; DO NOT EDIT +// This file was generated by swaggo/swag +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/user/login": { + "post": { + "description": "用户登录接口2", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户相关接口" + ], + "summary": "用户登录接口1", + "parameters": [ + { + "maxLength": 40, + "minLength": 8, + "type": "string", + "description": "密码", + "name": "password", + "in": "query", + "required": true + }, + { + "maxLength": 30, + "minLength": 5, + "type": "string", + "description": "用户名", + "name": "user_name", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.UserResponse" + } + } + } + } + }, + "/user/register": { + "post": { + "description": "用户注册接口", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户相关接口" + ], + "summary": "用户注册接口", + "parameters": [ + { + "type": "string", + "description": "用户令牌", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "maxLength": 30, + "minLength": 2, + "type": "string", + "description": "昵称", + "name": "nickname", + "in": "query", + "required": true + }, + { + "maxLength": 40, + "minLength": 8, + "type": "string", + "description": "密码", + "name": "password", + "in": "query", + "required": true + }, + { + "maxLength": 40, + "minLength": 8, + "type": "string", + "description": "确认密码", + "name": "password_confirm", + "in": "query", + "required": true + }, + { + "maxLength": 30, + "minLength": 5, + "type": "string", + "description": "用户名", + "name": "user_name", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + } + }, + "definitions": { + "response.Response": { + "type": "object", + "properties": { + "code": { + "description": "Code 0正确", + "type": "integer" + }, + "data": {}, + "error": { + "type": "string" + }, + "msg": { + "type": "string" + } + } + }, + "response.User": { + "type": "object", + "properties": { + "avatar": { + "description": "头像", + "type": "string" + }, + "created_at": { + "description": "注册时间", + "type": "integer" + }, + "id": { + "description": "ID", + "type": "integer" + }, + "nickname": { + "description": "昵称", + "type": "string" + }, + "status": { + "description": "状态", + "type": "string" + }, + "user_name": { + "description": "用户名", + "type": "string" + } + } + }, + "response.UserResponse": { + "type": "object", + "properties": { + "code": { + "description": "Code 0正确", + "type": "integer" + }, + "data": { + "$ref": "#/definitions/response.User" + }, + "error": { + "type": "string" + }, + "msg": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "http://localhost:3000", + BasePath: "/api/v1", + Schemes: []string{}, + Title: "fastApi接口文档", + Description: "这里写描述信息", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..390c934 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,189 @@ +{ + "swagger": "2.0", + "info": { + "description": "这里写描述信息", + "title": "fastApi接口文档", + "contact": {}, + "version": "1.0" + }, + "host": "http://localhost:3000", + "basePath": "/api/v1", + "paths": { + "/user/login": { + "post": { + "description": "用户登录接口2", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户相关接口" + ], + "summary": "用户登录接口1", + "parameters": [ + { + "maxLength": 40, + "minLength": 8, + "type": "string", + "description": "密码", + "name": "password", + "in": "query", + "required": true + }, + { + "maxLength": 30, + "minLength": 5, + "type": "string", + "description": "用户名", + "name": "user_name", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.UserResponse" + } + } + } + } + }, + "/user/register": { + "post": { + "description": "用户注册接口", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户相关接口" + ], + "summary": "用户注册接口", + "parameters": [ + { + "type": "string", + "description": "用户令牌", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "maxLength": 30, + "minLength": 2, + "type": "string", + "description": "昵称", + "name": "nickname", + "in": "query", + "required": true + }, + { + "maxLength": 40, + "minLength": 8, + "type": "string", + "description": "密码", + "name": "password", + "in": "query", + "required": true + }, + { + "maxLength": 40, + "minLength": 8, + "type": "string", + "description": "确认密码", + "name": "password_confirm", + "in": "query", + "required": true + }, + { + "maxLength": 30, + "minLength": 5, + "type": "string", + "description": "用户名", + "name": "user_name", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + } + }, + "definitions": { + "response.Response": { + "type": "object", + "properties": { + "code": { + "description": "Code 0正确", + "type": "integer" + }, + "data": {}, + "error": { + "type": "string" + }, + "msg": { + "type": "string" + } + } + }, + "response.User": { + "type": "object", + "properties": { + "avatar": { + "description": "头像", + "type": "string" + }, + "created_at": { + "description": "注册时间", + "type": "integer" + }, + "id": { + "description": "ID", + "type": "integer" + }, + "nickname": { + "description": "昵称", + "type": "string" + }, + "status": { + "description": "状态", + "type": "string" + }, + "user_name": { + "description": "用户名", + "type": "string" + } + } + }, + "response.UserResponse": { + "type": "object", + "properties": { + "code": { + "description": "Code 0正确", + "type": "integer" + }, + "data": { + "$ref": "#/definitions/response.User" + }, + "error": { + "type": "string" + }, + "msg": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..809b0ad --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,133 @@ +basePath: /api/v1 +definitions: + response.Response: + properties: + code: + description: Code 0正确 + type: integer + data: {} + error: + type: string + msg: + type: string + type: object + response.User: + properties: + avatar: + description: 头像 + type: string + created_at: + description: 注册时间 + type: integer + id: + description: ID + type: integer + nickname: + description: 昵称 + type: string + status: + description: 状态 + type: string + user_name: + description: 用户名 + type: string + type: object + response.UserResponse: + properties: + code: + description: Code 0正确 + type: integer + data: + $ref: '#/definitions/response.User' + error: + type: string + msg: + type: string + type: object +host: http://localhost:3000 +info: + contact: {} + description: 这里写描述信息 + title: fastApi接口文档 + version: "1.0" +paths: + /user/login: + post: + consumes: + - application/json + description: 用户登录接口2 + parameters: + - description: 密码 + in: query + maxLength: 40 + minLength: 8 + name: password + required: true + type: string + - description: 用户名 + in: query + maxLength: 30 + minLength: 5 + name: user_name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.UserResponse' + summary: 用户登录接口1 + tags: + - 用户相关接口 + /user/register: + post: + consumes: + - application/json + description: 用户注册接口 + parameters: + - description: 用户令牌 + in: header + name: Authorization + required: true + type: string + - description: 昵称 + in: query + maxLength: 30 + minLength: 2 + name: nickname + required: true + type: string + - description: 密码 + in: query + maxLength: 40 + minLength: 8 + name: password + required: true + type: string + - description: 确认密码 + in: query + maxLength: 40 + minLength: 8 + name: password_confirm + required: true + type: string + - description: 用户名 + in: query + maxLength: 30 + minLength: 5 + name: user_name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + summary: 用户注册接口 + tags: + - 用户相关接口 +swagger: "2.0" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3d1c4b9 --- /dev/null +++ b/go.mod @@ -0,0 +1,32 @@ +module fastApi + +go 1.16 + +require ( + github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 + github.com/gin-contrib/cors v1.3.1 + github.com/gin-gonic/gin v1.8.1 + github.com/go-openapi/spec v0.20.7 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-playground/locales v0.14.0 + github.com/go-playground/universal-translator v0.18.0 + github.com/go-playground/validator/v10 v10.10.0 + github.com/go-redis/redis v6.15.9+incompatible + github.com/golang-jwt/jwt/v4 v4.4.2 + github.com/google/uuid v1.1.2 + github.com/nsqio/go-nsq v1.1.0 + github.com/onsi/gomega v1.12.0 // indirect + github.com/robfig/cron/v3 v3.0.1 + github.com/spf13/cobra v1.5.0 + github.com/spf13/viper v1.12.0 + github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a + github.com/swaggo/gin-swagger v1.5.3 + github.com/swaggo/swag v1.8.5 + go.uber.org/zap v1.19.1 + golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 + golang.org/x/net v0.0.0-20220907135653-1e95f45603a7 // indirect + golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect + golang.org/x/tools v0.1.12 // indirect + gorm.io/driver/mysql v1.1.0 + gorm.io/gorm v1.21.10 +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..c4703c0 --- /dev/null +++ b/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "fastApi/cmd" +) + +// @title fastApi接口文档 +// @version 1.0 +// @description 这里写描述信息 +// @host http://localhost:3000 +// @BasePath /api/v1 +func main() { + cmd.Execute() +} diff --git a/mq/mq_base.go b/mq/mq_base.go new file mode 100644 index 0000000..b63b81f --- /dev/null +++ b/mq/mq_base.go @@ -0,0 +1,102 @@ +package mq + +import ( + "context" + "encoding/json" + "errors" + "fastApi/core/global" + "fastApi/core/logger" + "fmt" + "github.com/nsqio/go-nsq" + "go.uber.org/zap" + "time" +) + +var MQList []InterfaceMQ + +type InterfaceMQ interface { + Producer(ctx context.Context, message []byte, delay ...time.Duration) error + HandleMessage(msg *nsq.Message) error + GetTopic() string + GetChannel() string +} + +type BaseMQ struct { + Topic string + Channel string +} + +func (b *BaseMQ) GetChannel() string { + if b.Channel == "" { + return "channel1" + } + return b.Channel +} + +func (b *BaseMQ) GetTopic() string { + return b.Topic +} + +func (b *BaseMQ) Producer(ctx context.Context, message []byte, delay ...time.Duration) (err error) { + producer := global.Producer + + if producer == nil { + return errors.New("producer is nil") + } + + traceId := ctx.Value(logger.TraceId).(string) + if traceId == "" { + traceId = logger.CalcTraceId() + } + + data := map[string]string{ + "traceId": traceId, + "message": string(message), + } + message, _ = json.Marshal(data) + + fmt.Printf("traceId: %s, message: %s\n", b.GetTopic(), string(message)) + if len(delay) == 0 { + err = producer.Publish(b.GetTopic(), message) // 发布消息 + } else { + err = producer.DeferredPublish(b.GetTopic(), delay[0], message) + } + + return err +} + +func (b *BaseMQ) Handle(msg *nsq.Message, h func(string) error) error { + startTime := time.Now() + + var data map[string]string + err := json.Unmarshal(msg.Body, &data) + + if err != nil { + global.Log.With( + zap.String("url", b.GetTopic()), + zap.String("params", string(msg.Body)), + zap.Uint16("attempts", msg.Attempts), + ).Error("数据解析失败: " + err.Error()) + } + + logger.With( + zap.String("traceId", data["traceId"]), + ) + err = h(data["message"]) + + endTime := time.Now() + latencyTime := endTime.Sub(startTime) + log := global.Log.With( + zap.String("url", b.GetTopic()), + zap.String("params", data["message"]), + zap.Uint16("attempts", msg.Attempts), + zap.Duration("runtime", latencyTime), + ) + if err != nil { + log.Error("任务执行失败: " + err.Error()) + } else { + log.Info("任务执行成功") + } + + return err +} diff --git a/mq/send_registered_email.go b/mq/send_registered_email.go new file mode 100644 index 0000000..ea18392 --- /dev/null +++ b/mq/send_registered_email.go @@ -0,0 +1,30 @@ +package mq + +import ( + "fastApi/app/model" + "fastApi/core/global" + "github.com/nsqio/go-nsq" +) + +type SendRegisteredEmail struct { + BaseMQ +} + +func (c *SendRegisteredEmail) HandleMessage(msg *nsq.Message) error { + return c.Handle(msg, func(data string) error { + model.GetUser(1) + global.Log.Info("ok") + return nil + }) +} + +func init() { + MQList = append(MQList, NewSendRegisteredEmail()) +} + +func NewSendRegisteredEmail() *SendRegisteredEmail { + return &SendRegisteredEmail{ + BaseMQ: BaseMQ{ + Topic: "sendRegisteredEmail", + }} +} diff --git a/router/api.go b/router/api.go new file mode 100644 index 0000000..a835d1f --- /dev/null +++ b/router/api.go @@ -0,0 +1,29 @@ +package router + +import ( + "fastApi/app/http/controller" + "fastApi/app/http/middleware" + "github.com/gin-gonic/gin" +) + +func apiRoute(r *gin.Engine) { + // 路由 + v1 := r.Group("/api/v1") + { + UserController := controller.UserController{} + + // 用户登录 + v1.POST("user/register", wrap(UserController.UserRegister)) + + // 用户登录 + v1.POST("user/login", wrap(UserController.UserLogin)) + + // 需要登录保护的 + auth := v1.Group("") + auth.Use(middleware.JwtAuth()) + { + // User Routing + auth.GET("user/me", wrap(UserController.UserMe)) + } + } +} diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..d8ad1de --- /dev/null +++ b/router/router.go @@ -0,0 +1,46 @@ +package router + +import ( + _ "fastApi/docs" // 千万不要忘了导入把你上一步生成的docs + "fastApi/mq" + "fmt" + + "fastApi/core/middleware" + "github.com/gin-gonic/gin" + "github.com/spf13/viper" +) + +// init router +func NewRouter() *gin.Engine { + r := initGin() + loadRoute(r) + return r +} + +// init Gin +func initGin() *gin.Engine { + //设置gin模式 + gin.SetMode(viper.GetString("api.mode")) + engine := gin.New() + + engine.Use(middleware.AddTraceId()) + engine.Use(middleware.GinZap([]string{}), middleware.RecoveryWithZap(true)) + + engine.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + engine.GET("/test", func(c *gin.Context) { + err := mq.NewSendRegisteredEmail().Producer(c, []byte("test")) + fmt.Printf("\n\n%#v\n\n", err) + c.String(200, "pong") + }) + + return engine +} + +// 加载路由 +func loadRoute(r *gin.Engine) { + apiRoute(r) + swaggerRoute(r) +} diff --git a/router/swagger.go b/router/swagger.go new file mode 100644 index 0000000..4d880db --- /dev/null +++ b/router/swagger.go @@ -0,0 +1,11 @@ +package router + +import ( + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + gs "github.com/swaggo/gin-swagger" +) + +func swaggerRoute(r *gin.Engine) { + r.GET("/swagger/*any", gs.WrapHandler(swaggerFiles.Handler)) +} diff --git a/router/warp.go b/router/warp.go new file mode 100644 index 0000000..a70a6ec --- /dev/null +++ b/router/warp.go @@ -0,0 +1,49 @@ +package router + +import ( + "encoding/json" + "fastApi/app/http/response" + "fastApi/core/global" + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "net/http" +) + +type HandlerFunc func(ctx *gin.Context) (res interface{}, err error) + +func wrap(handler HandlerFunc) func(c *gin.Context) { + return func(ctx *gin.Context) { + res, err := handler(ctx) + if err != nil { + ctx.JSON(http.StatusOK, errorResponse(err)) + return + } + ctx.JSON(http.StatusOK, res) + } +} + +// errorResponse 返回错误消息 +func errorResponse(err error) response.Response { + ve, ok := err.(validator.ValidationErrors) + + if ok { + return response.ParamErr("参数错误", validationErrorsFormat(ve.Translate(global.Trans)), err) + } + + if _, ok := err.(*json.UnmarshalTypeError); ok { + return response.ParamErr("JSON类型不匹配", nil, err) + } + + return response.ParamErr("参数错误", nil, err) +} + +func validationErrorsFormat(fields map[string]string) map[string][]string { + res := map[string][]string{} + var errs []string + for _, err := range fields { + errs = append(errs, err) + } + + res["errors"] = errs + return res +} diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..007f9bf --- /dev/null +++ b/start.sh @@ -0,0 +1,14 @@ +#/bin/bash +make + +pid=`lsof -i tcp:3000|grep "*:"|awk '{print $2}'|uniq` + +if [ $pid ]; then + echo $pid + kill -1 $pid + else + ./bin/fastApi_mac server & +fi + + +# lsof -i tcp:3000|grep "*:"|awk '{print $2}'|xargs kill -1 diff --git a/util/common.go b/util/common.go new file mode 100644 index 0000000..7d54f07 --- /dev/null +++ b/util/common.go @@ -0,0 +1,18 @@ +package util + +import ( + "math/rand" + "time" +) + +// RandStr 返回随机字符串 +func RandStr(n int) string { + var letterRunes = []rune("1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + + rand.Seed(time.Now().UnixNano()) + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +} diff --git a/util/gin.go b/util/gin.go new file mode 100644 index 0000000..600da0d --- /dev/null +++ b/util/gin.go @@ -0,0 +1,78 @@ +package util + +import ( + "bytes" + "encoding/json" + "errors" + "github.com/gin-gonic/gin" + "io/ioutil" + "net/http" +) + +func GetParams(c *gin.Context) (content map[string]interface{}) { + switch { + case c.Request.Method == "GET": + content = GetQueryParams(c) + case c.ContentType() == "application/json": + content = GetBodyParams(c) + default: + content = GetPostFormParams(c) + } + return +} + +func GetQueryParams(c *gin.Context) (params map[string]interface{}) { + if params, ok := c.Get("queryParams"); ok { + return params.(map[string]interface{}) + } + + params = make(map[string]interface{}) + for k, v := range c.Request.URL.Query() { + params[k] = v[0] + } + + c.Set("queryParams", params) + return +} + +func GetBodyParams(c *gin.Context) (params map[string]interface{}) { + if params, ok := c.Get("bodyParams"); ok { + return params.(map[string]interface{}) + } + + params = make(map[string]interface{}) + data, _ := c.GetRawData() + + //把读过的字节流重新放到body + c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(data)) + + json.Unmarshal(data, ¶ms) + + c.Set("bodyParams", params) + return +} + +func GetPostFormParams(c *gin.Context) (params map[string]interface{}) { + if params, ok := c.Get("postFormParams"); ok { + return params.(map[string]interface{}) + } + + params = make(map[string]interface{}) + + if err := c.Request.ParseMultipartForm(32 << 20); err != nil { + if !errors.Is(err, http.ErrNotMultipart) { + return + } + } + + for k, v := range c.Request.PostForm { + if len(v) > 1 { + params[k] = v + } else if len(v) == 1 { + params[k] = v[0] + } + } + + c.Set("postFormParams", params) + return +} diff --git a/util/ip.go b/util/ip.go new file mode 100644 index 0000000..ed82ee1 --- /dev/null +++ b/util/ip.go @@ -0,0 +1,54 @@ +package util + +import ( + "errors" + "net" +) + +//获取本机ip +func GetMyIP() (net.IP, error) { + ifaces, err := net.Interfaces() + if err != nil { + return nil, err + } + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 { + continue // interface down + } + if iface.Flags&net.FlagLoopback != 0 { + continue // loopback interface + } + addrs, err := iface.Addrs() + if err != nil { + return nil, err + } + for _, addr := range addrs { + ip := getIpFromAddr(addr) + if ip == nil { + continue + } + return ip, nil + } + } + return nil, errors.New("connected to the network?") +} + +//获取ip +func getIpFromAddr(addr net.Addr) net.IP { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if ip == nil || ip.IsLoopback() { + return nil + } + ip = ip.To4() + if ip == nil { + return nil // not an ipv4 address + } + + return ip +}