基于Gin框架构建的web服务开发脚手架,统一规范开发,快速开发Api接口

This commit is contained in:
kwinwong
2024-06-18 16:51:37 +08:00
commit ec0af20b1d
53 changed files with 2651 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.vscode/
.env
go.sum
*.log

26
Dockerfile Normal file
View File

@@ -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"]

8
DockerfileBase Normal file
View File

@@ -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 .

20
LICENSE.md Normal file
View File

@@ -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.

81
Makefile Normal file
View File

@@ -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=<none> 的镜像
@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 .

157
README.md Normal file
View File

@@ -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
```

View File

@@ -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
}

View File

@@ -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(&param); 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(&param); 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
}

View File

@@ -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)
}

View File

@@ -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")))
}

13
app/http/request/user.go Normal file
View File

@@ -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:"确认密码"` //确认密码
}

View File

@@ -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)
}

55
app/http/response/user.go Normal file
View File

@@ -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,
},
}
}

View File

@@ -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)
}

57
app/model/user.go Normal file
View File

@@ -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
}

2
bin/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

37
cmd/main.go Normal file
View File

@@ -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 查看命令")
}

20
cmd/migrate.go Normal file
View File

@@ -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{})
}

19
cmd/server/cron.go Normal file
View File

@@ -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 {}
}

34
cmd/server/gin.go Normal file
View File

@@ -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"))
}

43
cmd/server/mq.go Normal file
View File

@@ -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)
}
}

21
cmd/version.go Normal file
View File

@@ -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")
}

0
common/services/.gitkeep Normal file
View File

36
config.yaml Normal file
View File

@@ -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

18
core/global/core.go Normal file
View File

@@ -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
)

47
core/gorm.go Normal file
View File

@@ -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
}

18
core/init.go Normal file
View File

@@ -0,0 +1,18 @@
package core
import (
"fastApi/core/logger"
)
func CortInit() {
ViperInit()
logger.InitLogger()
Database()
RedisInit()
NsqProducerInit()
}

View File

@@ -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
}

63
core/logger/zap.go Normal file
View File

@@ -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)
}

135
core/middleware/zap.go Normal file
View File

@@ -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()
}
}

28
core/nsq_producer.go Normal file
View File

@@ -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
}

29
core/redis.go Normal file
View File

@@ -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
}

63
core/trans.go Normal file
View File

@@ -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
}

21
core/viper.go Normal file
View File

@@ -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))
}
}

79
crontab/cron_init.go Normal file
View File

@@ -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)
}),
)
}

7
crontab/schedule.go Normal file
View File

@@ -0,0 +1,7 @@
package crontab
func Schedule() {
for _, cron := range CronList {
AddCron(cron)
}
}

27
crontab/testJob.go Normal file
View File

@@ -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")
}

View File

@@ -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

212
docs/docs.go Normal file
View File

@@ -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)
}

189
docs/swagger.json Normal file
View File

@@ -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"
}
}
}
}
}

133
docs/swagger.yaml Normal file
View File

@@ -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"

32
go.mod Normal file
View File

@@ -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
)

14
main.go Normal file
View File

@@ -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()
}

102
mq/mq_base.go Normal file
View File

@@ -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
}

View File

@@ -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",
}}
}

29
router/api.go Normal file
View File

@@ -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))
}
}
}

46
router/router.go Normal file
View File

@@ -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)
}

11
router/swagger.go Normal file
View File

@@ -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))
}

49
router/warp.go Normal file
View File

@@ -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
}

14
start.sh Executable file
View File

@@ -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

18
util/common.go Normal file
View File

@@ -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)
}

78
util/gin.go Normal file
View File

@@ -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, &params)
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
}

54
util/ip.go Normal file
View File

@@ -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
}