mirror of
https://github.com/kwinH/fastApi.git
synced 2025-12-24 13:07:58 +08:00
基于Gin框架构建的web服务开发脚手架,统一规范开发,快速开发Api接口
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.vscode/
|
||||
.env
|
||||
go.sum
|
||||
*.log
|
||||
26
Dockerfile
Normal file
26
Dockerfile
Normal 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
8
DockerfileBase
Normal 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
20
LICENSE.md
Normal 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
81
Makefile
Normal 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
157
README.md
Normal 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
|
||||
```
|
||||
21
app/http/controller/api.go
Normal file
21
app/http/controller/api.go
Normal 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
|
||||
}
|
||||
60
app/http/controller/user.go
Normal file
60
app/http/controller/user.go
Normal 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(¶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
|
||||
}
|
||||
32
app/http/middleware/cors.go
Normal file
32
app/http/middleware/cors.go
Normal 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)
|
||||
}
|
||||
71
app/http/middleware/jwt.go
Normal file
71
app/http/middleware/jwt.go
Normal 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
13
app/http/request/user.go
Normal 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:"确认密码"` //确认密码
|
||||
}
|
||||
56
app/http/response/common.go
Normal file
56
app/http/response/common.go
Normal 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
55
app/http/response/user.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
100
app/http/service/user_service.go
Normal file
100
app/http/service/user_service.go
Normal 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
57
app/model/user.go
Normal 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
2
bin/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
37
cmd/main.go
Normal file
37
cmd/main.go
Normal 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
20
cmd/migrate.go
Normal 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
19
cmd/server/cron.go
Normal 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
34
cmd/server/gin.go
Normal 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
43
cmd/server/mq.go
Normal 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
21
cmd/version.go
Normal 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
0
common/services/.gitkeep
Normal file
36
config.yaml
Normal file
36
config.yaml
Normal 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
18
core/global/core.go
Normal 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
47
core/gorm.go
Normal 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
18
core/init.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fastApi/core/logger"
|
||||
)
|
||||
|
||||
func CortInit() {
|
||||
|
||||
ViperInit()
|
||||
|
||||
logger.InitLogger()
|
||||
|
||||
Database()
|
||||
|
||||
RedisInit()
|
||||
|
||||
NsqProducerInit()
|
||||
}
|
||||
95
core/logger/gorm_logger.go
Normal file
95
core/logger/gorm_logger.go
Normal 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
63
core/logger/zap.go
Normal 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
135
core/middleware/zap.go
Normal 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
28
core/nsq_producer.go
Normal 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
29
core/redis.go
Normal 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
63
core/trans.go
Normal 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
21
core/viper.go
Normal 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
79
crontab/cron_init.go
Normal 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
7
crontab/schedule.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package crontab
|
||||
|
||||
func Schedule() {
|
||||
for _, cron := range CronList {
|
||||
AddCron(cron)
|
||||
}
|
||||
}
|
||||
27
crontab/testJob.go
Normal file
27
crontab/testJob.go
Normal 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")
|
||||
}
|
||||
37
docker/nsq/docker-compose.yaml
Normal file
37
docker/nsq/docker-compose.yaml
Normal 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
212
docs/docs.go
Normal 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
189
docs/swagger.json
Normal 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
133
docs/swagger.yaml
Normal 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
32
go.mod
Normal 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
14
main.go
Normal 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
102
mq/mq_base.go
Normal 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
|
||||
}
|
||||
30
mq/send_registered_email.go
Normal file
30
mq/send_registered_email.go
Normal 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
29
router/api.go
Normal 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
46
router/router.go
Normal 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
11
router/swagger.go
Normal 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
49
router/warp.go
Normal 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
14
start.sh
Executable 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
18
util/common.go
Normal 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
78
util/gin.go
Normal 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, ¶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
|
||||
}
|
||||
54
util/ip.go
Normal file
54
util/ip.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user