Browse Source

first commit

root 1 day ago
commit
e9fd71b7a1

+ 11 - 0
Dockerfile

@@ -0,0 +1,11 @@
+FROM docker.1ms.run/alpine:3.16
+
+RUN mkdir /src
+WORKDIR /src
+RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
+RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
+    && echo "Asia/Shanghai" > /etc/timezone \
+    && apk del tzdata
+ADD . /src
+
+# ENTRYPOINT [ "/task_service" ]

+ 41 - 0
README.md

@@ -0,0 +1,41 @@
+# 服务说明
+
+服务使用 Gin + Gorm 开发
+
+核心功能包括:后台用户管理、后台权限管理、后台角色管理。
+
+## 部署方式
+
+> Shell 脚本部署
+
+```shell
+bash deploy.sh
+```
+
+## 重点文件说明
+
+> ./build.sh
+
+Golang 服务打包脚本
+
+> .env
+
+配置文件
+
+其中`AUTH_TYPE` 的配置是重要核心配置,其他服务如果需要通过该服务鉴权则必须保持一致。
+
+> /validators
+
+该目录存储的是所有接口暴露的 Response 响应结构体
+
+## 目录说明
+
+目录结构遵循 Gin 框架标准
+
+## 本地运行
+
+1. 安装 Golang
+
+2. 执行`go mod tidy`
+
+3. 执行`go run main.go`

+ 19 - 0
build.sh

@@ -0,0 +1,19 @@
+# Pre Set
+set -x
+
+# Set ENV
+export GO111MODULE=on                               # Go1.11.x 版本需要开启
+export GOPROXY=https://goproxy.cn
+export GOPRIVATE=gogs.uu.mdfitnesscao.com                 # 私有仓库地址
+
+# Set Var
+Src=./                                             # 程序源码
+Out="app"
+Options="-a -installsuffix cgo -o"
+PreSet='GOOS=linux GOARCH=amd64'                  # 构建变量设置
+
+# Build
+go version
+go env
+
+GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags=jsoniter -o ${Out} ${Src}             # build

+ 113 - 0
cache/redis.go

@@ -0,0 +1,113 @@
+package cache
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"os"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/go-redis/redis/v8"
+
+	jsoniter "github.com/json-iterator/go"
+)
+
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+
+type Cache struct {
+	client *redis.Client
+	prefix string
+}
+
+type ChannelUserCache struct {
+	ID               int64
+	Name             string
+	Permission       []string `json:"permission"`
+	IsTemp           bool     `json:"isTemp"`
+	ValidArchivesIds []string `json:"validArchivesIds"`
+}
+
+type ManagerCache struct {
+	ID int64
+}
+
+type MemberCache struct {
+	ID   int64
+	Name string
+}
+
+var cache = &Cache{}
+
+func Instance() *Cache {
+	return cache
+}
+
+func GetClient() *redis.Client {
+	return cache.client
+}
+
+func InitRedis() *redis.Client {
+	RedisDB, err := strconv.Atoi(os.Getenv("REDIS_DB"))
+	if err != nil {
+		log.Panic("Redis数据库错误", err)
+	}
+	// 初始化Cache
+	return InitRedisClient(fmt.Sprintf("%s:%s", os.Getenv("REDIS_HOST"), os.Getenv("REDIS_PORT")), os.Getenv("REDIS_PASSWORD"), "lumen_cache:", RedisDB)
+}
+
+func InitRedisClient(addr, password, p string, db int) *redis.Client {
+	client := redis.NewClient(&redis.Options{
+		Addr:     addr,
+		Password: password,
+		DB:       db,
+	})
+
+	if err := client.Ping(context.Background()).Err(); err != nil {
+		log.Panic("start cache", err)
+	}
+	cache.prefix = p
+	cache.client = client
+	return client
+}
+
+func (c *Cache) KeyWithPrefix(key string) string {
+	return c.prefix + key
+}
+
+func (c *Cache) Get(key string) (string, error) {
+	value, err := c.client.Get(context.Background(), c.KeyWithPrefix(key)).Result()
+	if err != nil {
+		return "", err
+	}
+	// PHP序列化之后的对象无法直接转为go的数据结构,直接都当数组处理
+	value = strings.ReplaceAll(value, `O:8:"stdClass"`, `a`)
+	value = strings.ReplaceAll(value, `O:17:"App\Models\Member"`, `a`)
+	value = strings.ReplaceAll(value, `O:15:"App\Models\User"`, `a`)
+	return value, nil
+}
+
+func (c *Cache) Put(key string, value interface{}, expiration time.Duration) error {
+	valueStr, err := json.MarshalToString(value)
+	if err != nil {
+		return err
+	}
+	ok, err := c.client.SetEX(context.Background(), c.KeyWithPrefix(key), valueStr, expiration).Result()
+	fmt.Println(ok, err)
+	return err
+}
+
+func (c *Cache) PutStr(key string, value string, expiration time.Duration) error {
+	ok, err := c.client.SetEX(context.Background(), c.KeyWithPrefix(key), value, expiration).Result()
+	fmt.Println(ok, err)
+	return err
+}
+
+func (c *Cache) Delete(key string) (int64, error) {
+	return c.client.Del(context.Background(), c.KeyWithPrefix(key)).Result()
+}
+
+func (c *Cache) Incr(key string) (int64, error) {
+	return c.client.Incr(context.Background(), c.KeyWithPrefix(key)).Result()
+}

BIN
controller/.DS_Store


+ 44 - 0
controller/auth/auth.go

@@ -0,0 +1,44 @@
+package auth
+
+import (
+	"authService/response"
+	"authService/service/user"
+	"authService/util"
+	"authService/util/constants"
+	"authService/validators"
+
+	"github.com/gin-gonic/gin"
+)
+
+// 登录
+func Login(c *gin.Context) {
+	type Validator struct {
+		Account  string `json:"account" form:"account" binding:"required,alphanum,min=4,max=20"`
+		Password string `json:"password" form:"password" binding:"required,min=6,max=20"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	token, expireIn, errCode := user.Login(validator.Account, validator.Password)
+
+	if errCode != nil {
+		response.Fail(c, errCode)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"token":    token,
+		"expireIn": expireIn,
+	})
+}
+
+// 退出登录
+func Logout(c *gin.Context) {
+	cacheUser, _ := util.GetFromGinContext[*validators.AuthUser](c, constants.UserCacheKey)
+	user.Logout(cacheUser.ID)
+	response.Success(c, map[string]any{})
+}

+ 116 - 0
controller/role/manage.go

@@ -0,0 +1,116 @@
+package role
+
+import (
+	"authService/response"
+	"authService/service/auth"
+	"authService/service/permission"
+	"authService/service/role"
+	"authService/service/user"
+	"authService/validators"
+
+	"github.com/gin-gonic/gin"
+)
+
+// 修改或创建授权信息
+func UpdateOrCreate(c *gin.Context) {
+	var validator validators.Role
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+	if validator.ID != 0 {
+		// 更新
+		errCode := role.Update(&validator)
+		if errCode != nil {
+			response.Fail(c, errCode)
+			return
+		}
+
+		// 更新用户的凭证
+		roleInfo, roleInfoErr := role.FindByRoleIdWithUser(validator.ID)
+		if roleInfoErr == nil {
+			for _, roleUser := range roleInfo.Users {
+				auth.Refresh(user.Format(&roleUser))
+			}
+		}
+
+	} else {
+		errCode := role.Create(&validator)
+		if errCode != nil {
+			response.Fail(c, errCode)
+			return
+		}
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 删除
+func Delete(c *gin.Context) {
+	type Validator struct {
+		ID int64 `json:"id" binding:"required"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	deleteErr := role.Delete(validator.ID)
+	if deleteErr != nil {
+		response.Fail(c, deleteErr)
+		return
+	}
+	response.Success(c, map[string]any{})
+}
+
+// 获取列表
+func List(c *gin.Context) {
+	roles := role.List()
+
+	var list = make([]*validators.Role, 0)
+	for _, item := range roles {
+		list = append(list, role.FormatRole(item))
+	}
+
+	response.Success(c, map[string]any{
+		"list": list,
+	})
+}
+
+// 分页获取列表
+func Paginate(c *gin.Context) {
+	type Validator struct {
+		Page     int    `json:"page" form:"page" binding:"omitempty,min=1"`
+		PageSize int    `json:"pageSize" form:"pageSize" binding:"omitempty,min=1,max=50"`
+		Key      string `json:"key" form:"key" binding:"omitempty"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBindQuery(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	roles, total := role.Paginate(validator.Page, validator.PageSize, validator.Key)
+
+	var list = make([]*validators.Role, 0)
+	for _, item := range roles {
+		list = append(list, role.FormatRole(item))
+	}
+
+	response.Success(c, map[string]any{
+		"list":  list,
+		"total": total,
+	})
+}
+
+// 获取用户权限组列表
+func ListPermissionGroup(c *gin.Context) {
+	permissionGroups := permission.ListPermissionGroup()
+	response.Success(c, map[string]any{
+		"list": permissionGroups,
+	})
+}

+ 131 - 0
controller/user/manage.go

@@ -0,0 +1,131 @@
+package user
+
+import (
+	"authService/response"
+	"authService/service/user"
+	"authService/validators"
+
+	"github.com/gin-gonic/gin"
+)
+
+// 分页获取用户列表
+func Paginate(c *gin.Context) {
+	type Validator struct {
+		Page     int    `json:"page" form:"page" binding:"omitempty,min=1"`
+		PageSize int    `json:"pageSize" form:"pageSize" binding:"omitempty,min=1,max=50"`
+		Key      string `json:"key" form:"key" binding:"omitempty"`
+	}
+
+	var validator Validator
+	validateErr := c.ShouldBindQuery(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	users, total := user.Paginate(validator.Page, validator.PageSize, validator.Key)
+
+	var list []*validators.User
+	for _, u := range users {
+		list = append(list, user.Format(u))
+	}
+
+	response.Success(c, map[string]any{
+		"list":  list,
+		"total": total,
+	})
+}
+
+// 分页获取用户列表
+func List(c *gin.Context) {
+	type Validator struct {
+		Key string `json:"key" form:"key" binding:"omitempty"`
+	}
+
+	var validator Validator
+	validateErr := c.ShouldBindQuery(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	users, _ := user.Paginate(1, 99999, validator.Key)
+
+	var list []*validators.User
+	for _, u := range users {
+		list = append(list, user.Format(u))
+	}
+
+	response.Success(c, map[string]any{
+		"list": list,
+	})
+}
+
+// 修改或创建用户信息
+func UpdateOrCreate(c *gin.Context) {
+	type Validator struct {
+		*validators.User
+		// User     *validators.User `json:"user" form:"user" binding:"required,dive"`
+		Password string `json:"password" form:"password" binding:"omitempty,min=6,max=20"`
+	}
+
+	var validator Validator
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	err := user.UpdateOrCreate(validator.User, validator.Password)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 删除用户
+func Delete(c *gin.Context) {
+	type Validator struct {
+		ID int64 `json:"id" form:"id" binding:"required,min=1"`
+	}
+
+	var validator Validator
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	err := user.Delete(validator.ID)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 修改备注
+func UpdateRemark(c *gin.Context) {
+	type Validator struct {
+		ID     int64  `json:"id" form:"id" binding:"required,min=1"`
+		Remark string `json:"remark" form:"remark" binding:"required,max=255"`
+	}
+
+	var validator Validator
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	err := user.UpdateRemark(validator.ID, validator.Remark)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}

+ 52 - 0
controller/user/user.go

@@ -0,0 +1,52 @@
+package user
+
+import (
+	"authService/response"
+	"authService/service/user"
+	"authService/util"
+	"authService/util/constants"
+	"authService/validators"
+
+	"github.com/gin-gonic/gin"
+)
+
+// 获取当前登录用户信息
+func Profile(c *gin.Context) {
+	cacheUser, _ := util.GetFromGinContext[*validators.AuthUser](c, constants.UserCacheKey)
+	// 获取用户信息
+	currentUser, err := user.Find(cacheUser.ID)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"profile": user.Format(currentUser),
+	})
+}
+
+// 修改当前登录用户信息
+func UpdateProfile(c *gin.Context) {
+	type Validator struct {
+		*validators.User
+		OldPassword string `json:"oldPassword" form:"oldPassword" binding:"omitempty,min=6,max=20"`
+		Password    string `json:"password" form:"password" binding:"omitempty,min=6,max=20"`
+	}
+
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	cacheUser, _ := util.GetFromGinContext[*validators.AuthUser](c, constants.UserCacheKey)
+
+	updateErr := user.UpdateMySelf(validator.User, validator.OldPassword, validator.Password, cacheUser.ID)
+	if updateErr != nil {
+		response.Fail(c, updateErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}

+ 71 - 0
deploy.sh

@@ -0,0 +1,71 @@
+APP_NAME="auth-service"
+
+SERVER_IP="150.158.176.67"
+SERVER_USER="ubuntu"
+SERVER_SSH_PORT="22"
+SERVER_DEPLOY_DIR="/docker/services/$APP_NAME"
+SERVER_DEPLOY_APP="$SERVER_DEPLOY_DIR/app"
+SERVER_DEPLOY_ENV="$SERVER_DEPLOY_DIR/.env"
+SERVER_DEPLOY_DOCKER_FILE="$SERVER_DEPLOY_DIR/Dockerfile"
+SERVER_DOCKER_COMPOSE_DIR="/docker"
+
+OUTPUT_APP="./app"
+OUTPUT_ENV="./.env"
+OUTPUT_DOCKERFILE="./Dockerfile"
+
+# 本地打包
+bash ./build.sh
+
+# 检查远程目录是否存在
+ssh -p $SERVER_SSH_PORT $SERVER_USER@$SERVER_IP \
+    "
+        if [ ! -d ${SERVER_DEPLOY_DIR} ]; then
+            sudo mkdir -p ${SERVER_DEPLOY_DIR};
+        fi
+    "
+
+# 删除app文件
+ssh -p $SERVER_SSH_PORT $SERVER_USER@$SERVER_IP \
+    "
+        sudo rm -rf $SERVER_DEPLOY_APP;
+    "
+
+# 将应用文件上传到服务器(使用rsync+sudo)
+rsync -avz -e "ssh -p $SERVER_SSH_PORT" \
+    $OUTPUT_APP \
+    $SERVER_USER@$SERVER_IP:$SERVER_DEPLOY_APP \
+    --rsync-path="sudo rsync"
+
+# 将环境文件上传到服务器
+rsync -avz -e "ssh -p $SERVER_SSH_PORT" \
+    $OUTPUT_ENV \
+    $SERVER_USER@$SERVER_IP:$SERVER_DEPLOY_ENV \
+    --rsync-path="sudo rsync"
+
+# 将Dockerfile上传到服务器
+rsync -avz -e "ssh -p $SERVER_SSH_PORT" \
+    $OUTPUT_DOCKERFILE \
+    $SERVER_USER@$SERVER_IP:$SERVER_DEPLOY_DOCKER_FILE \
+    --rsync-path="sudo rsync"
+
+# # 将文件上传到服务器
+# scp -r -P $SERVER_SSH_PORT \
+#     $OUTPUT_APP \
+#     $SERVER_USER@$SERVER_IP:$SERVER_DEPLOY_APP
+
+# scp -r -P $SERVER_SSH_PORT \
+#     $OUTPUT_ENV \
+#     $SERVER_USER@$SERVER_IP:$SERVER_DEPLOY_ENV
+
+# scp -r -P $SERVER_SSH_PORT \
+#     $OUTPUT_DOCKERFILE \
+#     $SERVER_USER@$SERVER_IP:$SERVER_DEPLOY_DOCKER_FILE
+# 构建Docker等部署
+ssh -p $SERVER_SSH_PORT $SERVER_USER@$SERVER_IP \
+    "
+        cd ${SERVER_DOCKER_COMPOSE_DIR};
+        sudo docker build -t $APP_NAME --no-cache ${SERVER_DEPLOY_DIR}
+        sudo docker compose stop $APP_NAME
+        sudo docker compose rm -f $APP_NAME
+        sudo docker compose up -d $APP_NAME
+    "

+ 262 - 0
go.sum

@@ -0,0 +1,262 @@
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 h1:iC9YFYKDGEy3n/FtqJnOkZsene9olVspKmkX5A2YBEo=
+github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
+github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.0 h1:mFWQsFJ8kV1xAT/3WCFvp/Hqu1HgWGJmJK6dWoBoTLk=
+github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.0/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ=
+github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 h1:NqugFkGxx1TXSh/pBcU00Y6bljgDPaFdh5MUSeJ7e50=
+github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
+github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.4 h1:fvf4vYZsVwiUYUoG+CQAPSphf2tjgZw7MJSegxJcB6w=
+github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.4/go.mod h1:JjNrV9FBk2nWiwHmq7hyA80rwCFoKt6MNB11Kiv2CAc=
+github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
+github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
+github.com/alibabacloud-go/openapi-util v0.0.11 h1:iYnqOPR5hyEEnNZmebGyRMkkEJRWUEjDiiaOHZ5aNhA=
+github.com/alibabacloud-go/openapi-util v0.0.11/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
+github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
+github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
+github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
+github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
+github.com/alibabacloud-go/tea v1.1.19 h1:Xroq0M+pr0mC834Djj3Fl4ZA8+GGoA0i7aWse1vmgf4=
+github.com/alibabacloud-go/tea v1.1.19/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
+github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
+github.com/alibabacloud-go/tea-utils v1.4.3 h1:8SzwmmRrOnQ09Hf5a9GyfJc0d7Sjv6fmsZoF4UDbFjo=
+github.com/alibabacloud-go/tea-utils v1.4.3/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
+github.com/alibabacloud-go/tea-utils/v2 v2.0.0 h1:s3XRBCDVHBQ42ck4xnLGcWgRMDf9v4KNN/Kr/mf2e8A=
+github.com/alibabacloud-go/tea-utils/v2 v2.0.0/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4=
+github.com/alibabacloud-go/tea-xml v1.1.2 h1:oLxa7JUXm2EDFzMg+7oRsYc+kutgCVwm+bZlhhmvW5M=
+github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
+github.com/aliyun/credentials-go v1.1.2 h1:qU1vwGIBb3UJ8BwunHDRFtAhS6jnQLnde/yk0+Ih2GY=
+github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
+github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
+github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
+github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
+github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
+github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU=
+github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
+github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
+github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
+github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
+github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
+github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
+github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
+github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
+github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang-module/carbon v1.7.3 h1:p5mUZj7Tg62MblrkF7XEoxVPvhVs20N/kimqsZOQ+/U=
+github.com/golang-module/carbon v1.7.3/go.mod h1:nUMnXq90Rv8a7h2+YOo2BGKS77Y0w/hMPm4/a8h19N8=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
+github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
+github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
+github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
+github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
+github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0=
+github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/speps/go-hashids/v2 v2.0.1 h1:ViWOEqWES/pdOSq+C1SLVa8/Tnsd52XC34RY7lt7m4g=
+github.com/speps/go-hashids/v2 v2.0.1/go.mod h1:47LKunwvDZki/uRVD6NImtyk712yFzIs3UF3KlHohGw=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo=
+github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM=
+github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
+github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
+github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+gogs.uu.mdfitnesscao.com/hys/sdk v0.0.0-20250325055517-6bf4941fda13 h1:OITftsAJoTeGRAKNPq4kQYjkLni04WvVyKOQZBjqaw8=
+gogs.uu.mdfitnesscao.com/hys/sdk v0.0.0-20250325055517-6bf4941fda13/go.mod h1:FGXNUVnTBv/69sF+s8I+RoghxoGUWu+oza0ET6+VRos=
+gogs.uu.mdfitnesscao.com/hys/sdk v0.0.0-20250325060619-91cdc8112fe9 h1:9bSOwVYQZKpB3i+rWxvOQGyh2eO1uTPE+JB2GjaMll8=
+gogs.uu.mdfitnesscao.com/hys/sdk v0.0.0-20250325060619-91cdc8112fe9/go.mod h1:FGXNUVnTBv/69sF+s8I+RoghxoGUWu+oza0ET6+VRos=
+gogs.uu.mdfitnesscao.com/hys/sdk v0.0.0-20250427024657-9edf2b9e788b h1:t9bKnaR1oF+AVKos6sdJjXOxwC0+k4+Zu/8sGrfLazk=
+gogs.uu.mdfitnesscao.com/hys/sdk v0.0.0-20250427024657-9edf2b9e788b/go.mod h1:FGXNUVnTBv/69sF+s8I+RoghxoGUWu+oza0ET6+VRos=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
+golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
+golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
+golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/ini.v1 v1.56.0 h1:DPMeDvGTM54DXbPkVIZsp19fp/I2K7zwA/itHYHKo8Y=
+gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
+gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
+gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
+gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
+gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=

+ 59 - 0
main.go

@@ -0,0 +1,59 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"time"
+
+	"authService/cache"
+	"authService/model"
+	"authService/router"
+	"authService/util/validator"
+
+	"github.com/joho/godotenv"
+)
+
+func main() {
+	// 加载dotEnv环境
+	loadEnvErr := godotenv.Load()
+	if loadEnvErr != nil {
+		fmt.Println("ENV环境加载Error")
+		return
+	}
+	// 开始初始化数据库
+	model.Construct(true)
+	// 开始初始化缓存
+	cache.InitRedis()
+	// 初始化校验器翻译
+	validator.Init()
+	router := router.Init()
+
+	srv := &http.Server{
+		Addr:    ":" + os.Getenv("HTTP_PORT"),
+		Handler: router,
+	}
+
+	go func() {
+		// 服务连接
+		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+			log.Fatalf("服务开启失败: %s\n", err)
+		}
+	}()
+
+	// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
+	quit := make(chan os.Signal)
+	signal.Notify(quit, os.Interrupt)
+	<-quit
+	log.Println("服务关闭中..")
+
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	if err := srv.Shutdown(ctx); err != nil {
+		log.Fatal("服务关闭异常:", err)
+	}
+	log.Println("服务已退出")
+}

+ 30 - 0
middleware/authorize.go

@@ -0,0 +1,30 @@
+package middleware
+
+import (
+	"authService/response"
+	"authService/service/auth"
+	"authService/util/constants"
+	"authService/validators"
+
+	"github.com/gin-gonic/gin"
+)
+
+func Authorize() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		token := c.Request.Header.Get("token")
+		if token == "" {
+			response.Fail(c, response.ErrAuthorizationExpired)
+			return
+		}
+		var currentUser *validators.AuthUser
+		var err *response.ErrCode
+		// 拿到用户系统的资料
+		currentUser, err = auth.Get(token)
+		if err != nil {
+			response.Fail(c, err)
+			return
+		}
+		c.Set(constants.UserCacheKey, currentUser)
+		c.Next()
+	}
+}

+ 23 - 0
middleware/cors.go

@@ -0,0 +1,23 @@
+package middleware
+
+import (
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+)
+
+func Cors() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		c.Header("Access-Control-Allow-Origin", "*")
+		c.Header("Access-Control-Allow-Headers", c.Request.Header.Get("Access-Control-Request-Headers"))
+		c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
+		c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
+		c.Header("Access-Control-Allow-Credentials", "false")
+
+		if c.Request.Method == "OPTIONS" {
+			c.AbortWithStatus(http.StatusOK)
+			return
+		}
+		c.Next()
+	}
+}

+ 23 - 0
middleware/permission_check.go

@@ -0,0 +1,23 @@
+package middleware
+
+import (
+	"authService/response"
+	"authService/util"
+	"authService/util/constants"
+	"authService/validators"
+
+	"github.com/gin-gonic/gin"
+)
+
+func PermissionCheck(routePermission string) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		currentUser, _ := util.GetFromGinContext[*validators.AuthUser](c, constants.UserCacheKey)
+		if !currentUser.FullPermission {
+			if !util.InArrayString(routePermission, currentUser.Permissions) {
+				response.Fail(c, response.ErrPermissionNotAllowed)
+				return
+			}
+		}
+		c.Next()
+	}
+}

BIN
model/.DS_Store


+ 95 - 0
model/migration.go

@@ -0,0 +1,95 @@
+package model
+
+import (
+	"fmt"
+	"os"
+
+	"gorm.io/driver/mysql"
+	"gorm.io/gorm"
+	"gorm.io/gorm/schema"
+)
+
+var DB *gorm.DB
+
+func Construct(isMigrate bool) {
+
+	dbPrefix := os.Getenv("DB_PREFIX")
+
+	var err error
+	DB, err = gorm.Open(mysql.Open(fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
+		os.Getenv("DB_USERNAME"),
+		os.Getenv("DB_PASSWORD"),
+		os.Getenv("DB_HOST"),
+		os.Getenv("DB_PORT"),
+		os.Getenv("DB_DATABASE"))),
+		&gorm.Config{
+			NamingStrategy: schema.NamingStrategy{
+				SingularTable: true,
+				TablePrefix:   dbPrefix,
+			},
+			DisableForeignKeyConstraintWhenMigrating: true,
+			SkipDefaultTransaction:                   true,
+			QueryFields:                              true,
+		})
+	if err != nil {
+		panic(err)
+	}
+
+	// debug模式输出所有sql语句
+	// if os.Getenv("APP_DEBUG") == "true" {
+	DB = DB.Debug()
+	// }
+
+	sqlDB, err := DB.DB()
+	if err != nil {
+		panic(err)
+	}
+	sqlDB.SetMaxIdleConns(10)
+	sqlDB.SetMaxOpenConns(100)
+
+	// 开始迁移
+	if isMigrate {
+		Migrate()
+	}
+}
+
+func Migrate() {
+	// 用户信息
+	DB.Set("gorm:table_options", "ENGINE=InnoDB default charset = utf8mb4").AutoMigrate(&User{})
+	// 用户角色关系
+	DB.Set("gorm:table_options", "ENGINE=InnoDB default charset = utf8mb4").AutoMigrate(&UserRole{})
+	// 角色
+	DB.Set("gorm:table_options", "ENGINE=InnoDB default charset = utf8mb4").AutoMigrate(&Role{})
+}
+
+// 分页查询的Scope
+func Paginate(page int, pageSize int) func(db *gorm.DB) *gorm.DB {
+	return func(db *gorm.DB) *gorm.DB {
+		if page == 0 {
+			page = 1
+		}
+		switch {
+		case pageSize > 100:
+			pageSize = 100
+		case pageSize <= 0:
+			pageSize = 10
+		}
+
+		offset := (page - 1) * pageSize
+		return db.Offset(offset).Limit(pageSize)
+	}
+}
+
+// 分页查询的Scope
+func Limit(limit int) func(db *gorm.DB) *gorm.DB {
+	return func(db *gorm.DB) *gorm.DB {
+		switch {
+		case limit > 100:
+			limit = 100
+		case limit <= 0:
+			limit = 10
+		}
+
+		return db.Limit(limit)
+	}
+}

+ 28 - 0
model/role.go

@@ -0,0 +1,28 @@
+package model
+
+import (
+	"os"
+	"time"
+
+	"gorm.io/gorm"
+)
+
+type Role struct {
+	ID          int64          `gorm:"type:int(20);autoIncrement;comment:ID;" json:"id"`
+	Name        string         `gorm:"type:varchar(255);comment:名称;" json:"account"`
+	Permissions string         `gorm:"type:text;comment:权限列表;nullable;" json:"permissions"`
+	Users       []User         `gorm:"many2many:user_role;" json:"users"`
+	DeletedAt   gorm.DeletedAt `gorm:"column:deleted_at" json:"-"`
+	CreatedAt   time.Time      `gorm:"column:created_at" json:"createdAt"`
+	UpdatedAt   time.Time      `gorm:"column:updated_at" json:"updatedAt"`
+}
+
+func (u *Role) AfterFind(tx *gorm.DB) (err error) {
+
+	return
+}
+
+func (u *Role) TableName() string {
+	dbPrefix := os.Getenv("DB_PREFIX")
+	return dbPrefix + "role"
+}

+ 35 - 0
model/types/types.go

@@ -0,0 +1,35 @@
+package types
+
+import (
+	"database/sql"
+	"encoding/json"
+	"fmt"
+	"time"
+)
+
+type NullTime struct {
+	sql.NullTime
+}
+
+func (v NullTime) MarshalJSON() ([]byte, error) {
+	if v.Valid {
+		return json.Marshal(v.Time)
+	} else {
+		return json.Marshal(nil)
+	}
+}
+
+func (v *NullTime) UnmarshalJSON(data []byte) error {
+	var s *time.Time
+	if err := json.Unmarshal(data, &s); err != nil {
+		return err
+	}
+	if s != nil {
+		v.Valid = true
+		v.Time = *s
+		fmt.Println(s)
+	} else {
+		v.Valid = false
+	}
+	return nil
+}

+ 39 - 0
model/user.go

@@ -0,0 +1,39 @@
+package model
+
+import (
+	"os"
+	"time"
+
+	"gorm.io/gorm"
+)
+
+const (
+	UserStatusEnable  = 1 // 用户状态正常
+	UserStatusDisable = 2 // 用户状态禁用
+	UserIsSuperTrue   = 1 // 用户是超级管理员
+	UserIsSuperFalse  = 2 // 用户不是超级管理员
+)
+
+type User struct {
+	ID        int64          `gorm:"type:int(20);autoIncrement;comment:ID;" json:"id"`
+	Account   string         `gorm:"type:varchar(255);comment:账号;" json:"account"`
+	Password  string         `gorm:"type:varchar(255);comment:密码;" json:"password"`
+	Nickname  string         `gorm:"type:varchar(255);comment:昵称;default:'';" json:"nickname"`
+	Status    int            `gorm:"type:int(1);comment:状态;default:2;" json:"status"`
+	IsSuper   int            `gorm:"type:int(1);comment:是否超级管理员;default:2;" json:"isSuper"`
+	Remark    string         `gorm:"type:varchar(255);comment:备注;default:'';" json:"remark"`
+	Roles     []Role         `gorm:"many2many:user_role;" json:"roles"`
+	DeletedAt gorm.DeletedAt `gorm:"column:deleted_at" json:"-"`
+	CreatedAt time.Time      `gorm:"column:created_at" json:"createdAt"`
+	UpdatedAt time.Time      `gorm:"column:updated_at" json:"updatedAt"`
+}
+
+func (u *User) AfterFind(tx *gorm.DB) (err error) {
+
+	return
+}
+
+func (u *User) TableName() string {
+	dbPrefix := os.Getenv("DB_PREFIX")
+	return dbPrefix + "user"
+}

+ 25 - 0
model/user_role.go

@@ -0,0 +1,25 @@
+package model
+
+import (
+	"os"
+
+	"gorm.io/gorm"
+)
+
+type UserRole struct {
+	ID     int64 `gorm:"type:int(20);autoIncrement;comment:ID;" json:"id"`
+	UserId int64 `gorm:"type:int(20);comment:用户ID;" json:"userId"`
+	RoleId int64 `gorm:"type:int(20);comment:角色ID;" json:"roleId"`
+	User   User  `gorm:"foreignKey:UserId;references:ID" json:"user"`
+	Role   Role  `gorm:"foreignKey:RoleId;references:ID" json:"role"`
+}
+
+func (u *UserRole) AfterFind(tx *gorm.DB) (err error) {
+
+	return
+}
+
+func (u *UserRole) TableName() string {
+	dbPrefix := os.Getenv("DB_PREFIX")
+	return dbPrefix + "user_role"
+}

+ 63 - 0
response/errcode.go

@@ -0,0 +1,63 @@
+package response
+
+type ErrCode struct {
+	Code int
+	Msg  string
+}
+
+const (
+	SUCCESS               = 200
+	AUTHORIZATION_EXPIRED = 401
+	INVALID_TOKEN         = 403
+	NOT_FOUND             = 404
+	ERROR                 = 500
+
+	PLATFORM_ERROR         = 1001 // 第三方错误
+	REDIS_ERROR            = 1002 // 缓存错误
+	VALIDATOR_ERROR        = 1003 // validator校验器错误
+	DEBUG_ERROR            = 1004 // DEBUG的错误信息
+	DB_ERROR               = 1005 // DB异常
+	PERMISSION_NOT_ALLOWED = 1006
+	DATA_NOT_FOUND         = 9000
+	NEED_REGIST            = 9001
+	SIGN_ERROR             = 9002
+	ACCOUNT_FORBID         = 9003
+
+	USER_MOBILE_NOT_BIND = 10002
+)
+
+var ErrorCodeMessage = map[int]string{
+	SUCCESS:                "操作成功",
+	ERROR:                  "服务器异常",
+	NOT_FOUND:              "无操作权限",
+	AUTHORIZATION_EXPIRED:  "凭证已失效,请重新登录",
+	INVALID_TOKEN:          "凭证无效,请重新登录",
+	PLATFORM_ERROR:         "第三方调用异常",
+	REDIS_ERROR:            "服务器异常[1002]",
+	VALIDATOR_ERROR:        "参数输入错误,请仔细检查",
+	DEBUG_ERROR:            "服务器异常",
+	DB_ERROR:               "服务器异常[1005]",
+	PERMISSION_NOT_ALLOWED: "您没有权限进行该操作",
+	DATA_NOT_FOUND:         "该项不存在",
+	NEED_REGIST:            "服务人员不存在,请先补充资料",
+	SIGN_ERROR:             "签名错误",
+	ACCOUNT_FORBID:         "您的账号已被禁用",
+}
+
+var (
+	ErrSuccess              = &ErrCode{Code: SUCCESS, Msg: ErrorCodeMessage[SUCCESS]}
+	Err                     = &ErrCode{Code: ERROR, Msg: ErrorCodeMessage[ERROR]}
+	ErrNotFound             = &ErrCode{Code: NOT_FOUND, Msg: ErrorCodeMessage[NOT_FOUND]}
+	ErrAuthorizationExpired = &ErrCode{Code: AUTHORIZATION_EXPIRED, Msg: ErrorCodeMessage[AUTHORIZATION_EXPIRED]}
+	ErrInvalidToken         = &ErrCode{Code: INVALID_TOKEN, Msg: ErrorCodeMessage[INVALID_TOKEN]}
+	ErrPlatform             = &ErrCode{Code: PLATFORM_ERROR, Msg: ErrorCodeMessage[PLATFORM_ERROR]}
+	ErrRedis                = &ErrCode{Code: REDIS_ERROR, Msg: ErrorCodeMessage[REDIS_ERROR]}
+	ErrValidator            = &ErrCode{Code: VALIDATOR_ERROR, Msg: ErrorCodeMessage[VALIDATOR_ERROR]}
+	ErrDebug                = &ErrCode{Code: DEBUG_ERROR, Msg: ErrorCodeMessage[DEBUG_ERROR]}
+	ErrDatabase             = &ErrCode{Code: DB_ERROR, Msg: ErrorCodeMessage[DB_ERROR]}
+	ErrPermissionNotAllowed = &ErrCode{Code: PERMISSION_NOT_ALLOWED, Msg: ErrorCodeMessage[PERMISSION_NOT_ALLOWED]}
+	ErrDataNotFound         = &ErrCode{Code: DATA_NOT_FOUND, Msg: ErrorCodeMessage[DATA_NOT_FOUND]}
+	ErrNeedRegist           = &ErrCode{Code: NEED_REGIST, Msg: ErrorCodeMessage[NEED_REGIST]}
+	ErrSign                 = &ErrCode{Code: SIGN_ERROR, Msg: ErrorCodeMessage[SIGN_ERROR]}
+	ErrAccountForbid        = &ErrCode{Code: ACCOUNT_FORBID, Msg: ErrorCodeMessage[ACCOUNT_FORBID]}
+)

+ 76 - 0
response/response.go

@@ -0,0 +1,76 @@
+package response
+
+import (
+	"net/http"
+	"strings"
+
+	"authService/util/validator"
+
+	"github.com/gin-gonic/gin"
+)
+
+type JSONResult[T any] struct {
+	Code int    `json:"code"`
+	Msg  string `json:"message"`
+	Data T      `json:"data"`
+}
+type ListResponse[T any] struct {
+	List []*T `json:"list"`
+}
+type PaginateResponse[T any] struct {
+	List  []*T `json:"list"`
+	Total int  `json:"total"`
+}
+type OffsetResponse[T any] struct {
+	List   []*T `json:"list"`
+	Offset any  `json:"offset"`
+}
+type OffsetTotalResponse[T any] struct {
+	List   []*T  `json:"list"`
+	Total  int64 `json:"total"`
+	Offset any   `json:"offset"`
+}
+
+// 成功响应
+func Success[T any](ctx *gin.Context, data T) {
+	ctx.JSON(http.StatusOK, &JSONResult[T]{
+		Code: ErrSuccess.Code,
+		Msg:  ErrSuccess.Msg,
+		Data: data,
+	})
+}
+
+// 其他响应
+func Response[T any](ctx *gin.Context, data T) {
+	ctx.JSON(http.StatusOK, &data)
+}
+
+// 错误响应
+func Fail(c *gin.Context, errcode *ErrCode, msg ...string) {
+	var errMsg string
+	if len(msg) > 0 {
+		errMsg = strings.Join(msg, "")
+	} else {
+		//错误提示优化, 增加错误展示方式
+		errMsg = errcode.Msg
+	}
+	var T any = struct{}{}
+	c.JSON(http.StatusOK, &JSONResult[any]{
+		Code: errcode.Code,
+		Msg:  errMsg,
+		Data: T,
+	})
+	c.Abort()
+}
+
+// 错误响应
+func FailValidator(c *gin.Context, err error) {
+	var T any = struct{}{}
+	validatorError := validator.TranslateError(err)
+	c.JSON(http.StatusOK, &JSONResult[any]{
+		Code: VALIDATOR_ERROR,
+		Msg:  validatorError.Error(),
+		Data: T,
+	})
+	c.Abort()
+}

+ 61 - 0
router/router.go

@@ -0,0 +1,61 @@
+package router
+
+import (
+	"net/http"
+
+	"authService/controller/auth"
+	"authService/controller/role"
+	"authService/controller/user"
+	"authService/middleware"
+
+	"github.com/gin-gonic/gin"
+	"gogs.uu.mdfitnesscao.com/cuiguohai/sdk/constants"
+)
+
+func Init() *gin.Engine {
+	router := gin.Default()
+	router.Use(middleware.Cors())
+
+	router.GET("/ping", func(c *gin.Context) {
+		c.String(http.StatusOK, "The Server is Running")
+	})
+	// 后台API
+	backendApiRouter := router.Group("/backend")
+	// 登录
+	backendApiRouter.POST("/login", auth.Login)
+	backendApiRouter.Use(middleware.Authorize())
+	{
+		// 退出登录
+		backendApiRouter.POST("/logout", auth.Logout)
+		// 获取当前登录用户信息
+		backendApiRouter.GET("/profile", user.Profile)
+		// 修改当前登录用户信息
+		backendApiRouter.POST("/profile/update", user.UpdateProfile)
+
+		// |------角色管理
+		// 获取角色列表(不要权限)
+		backendApiRouter.GET("/role/list", role.List)
+		// 获取角色列表(分页)
+		backendApiRouter.GET("/role/paginate", middleware.PermissionCheck(constants.PermissionRoleView), role.Paginate)
+		// 修改或创建角色
+		backendApiRouter.POST("/role/updateOrCreate", middleware.PermissionCheck(constants.PermissionRoleEdit), role.UpdateOrCreate)
+		// 删除角色
+		backendApiRouter.POST("/role/delete", middleware.PermissionCheck(constants.PermissionRoleDelete), role.Delete)
+		// 获取用户权限组列表
+		backendApiRouter.GET("/role/permission/list", middleware.PermissionCheck(constants.PermissionRoleEdit), role.ListPermissionGroup)
+
+		// |------用户管理
+		// 获取用户列表
+		backendApiRouter.GET("/user/paginate", middleware.PermissionCheck(constants.PermissionUserView), user.Paginate)
+		// 获取用户列表
+		backendApiRouter.GET("/user/list", user.List)
+		// 修改或创建用户
+		backendApiRouter.POST("/user/updateOrCreate", middleware.PermissionCheck(constants.PermissionUserEdit), user.UpdateOrCreate)
+		// 删除用户
+		backendApiRouter.POST("/user/delete", middleware.PermissionCheck(constants.PermissionUserDelete), user.Delete)
+		// 修改备注信息
+		backendApiRouter.POST("/user/updateRemark", middleware.PermissionCheck(constants.PermissionUserEdit), user.UpdateRemark)
+	}
+
+	return router
+}

BIN
service/.DS_Store


+ 132 - 0
service/auth/token.go

@@ -0,0 +1,132 @@
+package auth
+
+import (
+	"authService/cache"
+	"authService/model"
+	"authService/response"
+	"authService/util"
+	"authService/validators"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"os"
+	"time"
+
+	"github.com/go-redis/redis/v8"
+)
+
+var expireIn int = 86400 * 14
+
+// 获取授权Token的Redis键名
+func getAuthTokenStr(token string) string {
+	authTokenPrefix := os.Getenv("AUTH_TOKEN_PREFIX")
+	return fmt.Sprintf("%s:Authorize:Token:%s", authTokenPrefix, token)
+}
+
+// 获取用户Token的Redis键名
+func getUserAuthTokenStr(userId int64) string {
+	authTokenPrefix := os.Getenv("AUTH_TOKEN_PREFIX")
+	return fmt.Sprintf("%s:Authorize:User:%d:Token", authTokenPrefix, userId)
+}
+
+// 生成Token
+func Generate(formatedUser *validators.User) (string, int, *response.ErrCode) {
+	token := util.RandString(30)
+	expire := time.Duration(expireIn) * time.Second
+
+	cacheAuthUser := &validators.AuthUser{
+		ID:             formatedUser.ID,
+		Account:        formatedUser.Account,
+		Nickname:       formatedUser.Nickname,
+		FullPermission: formatedUser.IsSuper == model.UserIsSuperTrue,
+		RoleIds:        formatedUser.RoleIds,
+		Permissions:    formatedUser.Permissions,
+		LoginAt:        time.Now().Unix(),
+	}
+	cacheKey := getAuthTokenStr(token)
+	cacheErr := cache.Instance().Put(cacheKey, cacheAuthUser, expire)
+	if cacheErr != nil {
+		return token, expireIn, response.Err
+	}
+
+	userCacheKey := getUserAuthTokenStr(cacheAuthUser.ID)
+	// 这个key是缓存 服务人员ID 对应的token是什么,到时候用户如果被禁用了,要强制下线
+	cacheErr = cache.Instance().PutStr(userCacheKey, token, expire)
+	if cacheErr != nil {
+		return token, expireIn, response.Err
+	}
+	return token, expireIn, nil
+}
+
+// 刷新Token
+func Refresh(formatedUser *validators.User) (string, int, *response.ErrCode) {
+	token, err := GetTokenByUserId(formatedUser.ID)
+	if err != nil {
+		return "", 0, err
+	}
+
+	if token == "" {
+		return "", 0, nil
+	}
+
+	expire := time.Duration(expireIn) * time.Second
+	cacheKey := getAuthTokenStr(token)
+	cacheAuthUser := &validators.AuthUser{
+		ID:             formatedUser.ID,
+		Account:        formatedUser.Account,
+		Nickname:       formatedUser.Nickname,
+		FullPermission: formatedUser.IsSuper == model.UserIsSuperTrue,
+		RoleIds:        formatedUser.RoleIds,
+		Permissions:    formatedUser.Permissions,
+		LoginAt:        time.Now().Unix(),
+	}
+	cacheErr := cache.Instance().Put(cacheKey, cacheAuthUser, expire)
+	if cacheErr != nil {
+		return token, int(expireIn), response.Err
+	}
+	return token, int(expireIn), nil
+}
+
+// 退出登录
+func Exit(userId int64) {
+	token, _ := GetTokenByUserId(userId)
+	if token != "" {
+		userCacheKey := getUserAuthTokenStr(userId)
+		cacheKey := getAuthTokenStr(token)
+		cache.Instance().Delete(userCacheKey)
+		cache.Instance().Delete(cacheKey)
+	}
+}
+
+// 获取某个用户的Token
+func GetTokenByUserId(userId int64) (string, *response.ErrCode) {
+	userCacheKey := getUserAuthTokenStr(userId)
+	token, err := cache.Instance().Get(userCacheKey)
+	if err != nil {
+		if errors.Is(err, redis.Nil) {
+			return "", nil
+		}
+		return "", response.Err
+	}
+	return token, nil
+}
+
+// 根据Token获取用户信息
+func Get(token string) (*validators.AuthUser, *response.ErrCode) {
+	// 检查token是否存在
+	cacheKey := getAuthTokenStr(token)
+	userInfoJson, err := cache.Instance().Get(cacheKey)
+	if err != nil {
+		if errors.Is(err, redis.Nil) {
+			return nil, response.ErrAuthorizationExpired
+		}
+		return nil, response.Err
+	}
+
+	var currentUser validators.AuthUser
+	err = json.Unmarshal([]byte(userInfoJson), &currentUser)
+	if err != nil {
+		return nil, response.Err
+	}
+	return &currentUser, nil
+}

+ 225 - 0
service/permission/permission.go

@@ -0,0 +1,225 @@
+package permission
+
+import (
+	"authService/util"
+	"authService/validators"
+	"os"
+
+	"gogs.uu.mdfitnesscao.com/cuiguohai/sdk/constants"
+)
+
+const (
+	SystemFlagUser = "User" // 用户平台后台系统
+)
+
+// 获取所有权限组
+func ListPermissionGroup() []validators.UserPermissionGroup {
+	var commonPermissions = []validators.UserPermissionGroup{
+		{
+			Label: "用户管理",
+			Children: []validators.UserPermission{
+				{
+					Label:          constants.CommonPermissionNames[constants.PermissionUserView].Name,
+					Value:          constants.PermissionUserView,
+					RequiredValues: []string{},
+				},
+				{
+					Label:          constants.CommonPermissionNames[constants.PermissionUserEdit].Name,
+					Value:          constants.PermissionUserEdit,
+					RequiredValues: []string{constants.PermissionUserView},
+				},
+			},
+		},
+		{
+			Label: "角色管理",
+			Children: []validators.UserPermission{
+				{
+					Label:          constants.CommonPermissionNames[constants.PermissionRoleView].Name,
+					Value:          constants.PermissionRoleView,
+					RequiredValues: []string{},
+				},
+				{
+					Label:          constants.CommonPermissionNames[constants.PermissionRoleEdit].Name,
+					Value:          constants.PermissionRoleEdit,
+					RequiredValues: []string{constants.PermissionRoleView},
+				},
+				{
+					Label:          constants.CommonPermissionNames[constants.PermissionRoleDelete].Name,
+					Value:          constants.PermissionRoleDelete,
+					RequiredValues: []string{constants.PermissionRoleView},
+				},
+			},
+		},
+	}
+	var permissionGroups map[string][]validators.UserPermissionGroup = map[string][]validators.UserPermissionGroup{
+		SystemFlagUser: {
+			{
+				Label: "机构管理",
+				Children: []validators.UserPermission{
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionMechanismView].Name,
+						Value:          constants.UserPermissionMechanismView,
+						RequiredValues: []string{},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionMechanismEdit].Name,
+						Value:          constants.UserPermissionMechanismEdit,
+						RequiredValues: []string{constants.UserPermissionMechanismView},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionMechanismDelete].Name,
+						Value:          constants.UserPermissionMechanismDelete,
+						RequiredValues: []string{constants.UserPermissionMechanismView},
+					},
+				},
+			},
+			{
+				Label: "档案管理",
+				Children: []validators.UserPermission{
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionArchivesView].Name,
+						Value:          constants.UserPermissionArchivesView,
+						RequiredValues: []string{},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionArchivesEdit].Name,
+						Value:          constants.UserPermissionArchivesEdit,
+						RequiredValues: []string{constants.UserPermissionArchivesView},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionArchivesFormTemplateView].Name,
+						Value:          constants.UserPermissionArchivesFormTemplateView,
+						RequiredValues: []string{},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionArchivesFormTemplateEdit].Name,
+						Value:          constants.UserPermissionArchivesFormTemplateEdit,
+						RequiredValues: []string{constants.UserPermissionArchivesFormTemplateView},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionArchivesMechanismConfigView].Name,
+						Value:          constants.UserPermissionArchivesMechanismConfigView,
+						RequiredValues: []string{},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionArchivesMechanismConfigEdit].Name,
+						Value:          constants.UserPermissionArchivesMechanismConfigEdit,
+						RequiredValues: []string{constants.UserPermissionArchivesMechanismConfigView},
+					},
+				},
+			},
+			{
+				Label: "表单管理",
+				Children: []validators.UserPermission{
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionFormView].Name,
+						Value:          constants.UserPermissionFormView,
+						RequiredValues: []string{},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionFormEdit].Name,
+						Value:          constants.UserPermissionFormEdit,
+						RequiredValues: []string{constants.UserPermissionFormView},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionFormFieldEdit].Name,
+						Value:          constants.UserPermissionFormFieldEdit,
+						RequiredValues: []string{constants.UserPermissionFormView},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionFormFieldDelete].Name,
+						Value:          constants.UserPermissionFormFieldDelete,
+						RequiredValues: []string{constants.UserPermissionFormView},
+					},
+				},
+			},
+			{
+				Label: "数据中心",
+				Children: []validators.UserPermission{
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionMedicalDataView].Name,
+						Value:          constants.UserPermissionMedicalDataView,
+						RequiredValues: []string{},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionMedicalDataEdit].Name,
+						Value:          constants.UserPermissionMedicalDataEdit,
+						RequiredValues: []string{constants.UserPermissionMedicalDataView},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionMedicalDataRawDataView].Name,
+						Value:          constants.UserPermissionMedicalDataRawDataView,
+						RequiredValues: []string{constants.UserPermissionMedicalDataView},
+					},
+				},
+			},
+			{
+				Label: "问卷管理",
+				Children: []validators.UserPermission{
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionSurveyView].Name,
+						Value:          constants.UserPermissionSurveyView,
+						RequiredValues: []string{},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionSurveyEdit].Name,
+						Value:          constants.UserPermissionSurveyEdit,
+						RequiredValues: []string{constants.UserPermissionSurveyView},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionSurveyAuthorizeView].Name,
+						Value:          constants.UserPermissionSurveyAuthorizeView,
+						RequiredValues: []string{},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionSurveyAuthorizeEdit].Name,
+						Value:          constants.UserPermissionSurveyAuthorizeEdit,
+						RequiredValues: []string{constants.UserPermissionSurveyAuthorizeView},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionSurveyResultView].Name,
+						Value:          constants.UserPermissionSurveyResultView,
+						RequiredValues: []string{},
+					},
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionSurveyResultEdit].Name,
+						Value:          constants.UserPermissionSurveyResultEdit,
+						RequiredValues: []string{constants.UserPermissionSurveyResultView},
+					},
+				},
+			},
+			{
+				Label: "敏感数据",
+				Children: []validators.UserPermission{
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionDataPrivacyArchivesInfo].Name,
+						Value:          constants.UserPermissionDataPrivacyArchivesInfo,
+						RequiredValues: []string{constants.UserPermissionArchivesView},
+					},
+				},
+			},
+			{
+				Label: "数据同步",
+				Children: []validators.UserPermission{
+					{
+						Label:          constants.UserPermissionNames[constants.UserPermissionDataSync].Name,
+						Value:          constants.UserPermissionDataSync,
+						RequiredValues: []string{},
+					},
+				},
+			},
+		},
+	}
+
+	// 授权系统的类型
+	authType := os.Getenv("AUTH_TYPE")
+	currentPermissions := permissionGroups[authType]
+
+	// 将commonPermissions和currentPermissions合并
+	return append(commonPermissions, currentPermissions...)
+}
+
+// 检查权限
+func CheckPermission(permissions []string, permission string) bool {
+	return util.InArrayString(permission, permissions)
+}

+ 108 - 0
service/role/role.go

@@ -0,0 +1,108 @@
+package role
+
+import (
+	"authService/model"
+	"authService/response"
+	"authService/util"
+	"authService/validators"
+
+	"github.com/golang-module/carbon"
+	jsoniter "github.com/json-iterator/go"
+)
+
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+
+// 创建角色
+func Create(role *validators.Role) *response.ErrCode {
+	var permissions = util.JsonEncode(role.Permissions)
+	createErr := model.DB.Create(&model.Role{
+		Name:        role.Name,
+		Permissions: permissions,
+	}).Error
+	if createErr != nil {
+		return response.Err
+	}
+
+	return nil
+}
+
+// 修改角色
+func Update(role *validators.Role) *response.ErrCode {
+	if role.ID == 0 {
+		return &response.ErrCode{
+			Msg:  "ID不能为空",
+			Code: response.ERROR,
+		}
+	}
+
+	var permissions = util.JsonEncode(role.Permissions)
+	updateErr := model.DB.Where("id = ?", role.ID).Select("name", "permissions").Updates(model.Role{
+		Name:        role.Name,
+		Permissions: permissions,
+	}).Error
+	if updateErr != nil {
+		return response.Err
+	}
+	return nil
+}
+
+// 获取角色列表
+func FindByRoleIdWithUser(id int64) (*model.Role, *response.ErrCode) {
+	var role *model.Role
+	err := model.DB.Model(&model.Role{}).Where("id = ?", id).Preload("Users.Roles").First(&role).Error
+	if err != nil {
+		return nil, response.Err
+	}
+
+	return role, nil
+}
+
+// 获取角色列表
+func List() (roles []*model.Role) {
+	model.DB.Order("id desc").Find(&roles)
+	return
+}
+
+func Paginate(page, pageSize int, key string) ([]*model.Role, int64) {
+	var total int64
+	var roles []*model.Role
+	query := model.DB.Model(&model.Role{})
+	if key != "" {
+		query = query.Where("name like ?", "%"+key+"%")
+	}
+	query.Count(&total)
+	query.Scopes(model.Paginate(page, pageSize)).Preload("Users").Order("id desc").Find(&roles)
+	return roles, total
+}
+
+// 删除角色
+func Delete(id int64) *response.ErrCode {
+	// 检查角色是否关联了用户
+	var count int64
+	model.DB.Model(&model.UserRole{}).Where("role_id = ?", id).Count(&count)
+	if count > 0 {
+		return &response.ErrCode{
+			Msg:  "角色已关联用户,无法删除",
+			Code: response.ERROR,
+		}
+	}
+
+	model.DB.Where("id = ?", id).Delete(&model.Role{})
+
+	return nil
+}
+
+func FormatRole(role *model.Role) *validators.Role {
+	var permissions []string
+	json.UnmarshalFromString(role.Permissions, &permissions)
+	if permissions == nil {
+		permissions = make([]string, 0)
+	}
+	return &validators.Role{
+		ID:          role.ID,
+		Name:        role.Name,
+		Permissions: permissions,
+		UserTotal:   len(role.Users),
+		CreatedAt:   carbon.Time2Carbon(role.CreatedAt).Format("Y/m/d H:i:s"),
+	}
+}

+ 46 - 0
service/user/auth.go

@@ -0,0 +1,46 @@
+package user
+
+import (
+	"authService/model"
+	"authService/response"
+	"authService/service/auth"
+	"authService/util"
+)
+
+// 登录
+func Login(account, password string) (string, int, *response.ErrCode) {
+	var token = ""
+	var expireIn = 0
+	existUser := FindByAccount(account)
+	if existUser == nil {
+		return token, expireIn, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "账号或密码不正确",
+		}
+	}
+
+	// 如果被禁用,禁止登录
+	if existUser.Status == model.UserStatusDisable {
+		return token, expireIn, response.ErrAccountForbid
+	}
+
+	if existUser.Password != util.Md5(password) {
+		return token, expireIn, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "账号或密码不正确",
+		}
+	}
+
+	formatedUser := Format(existUser)
+	token, expireIn, err := auth.Generate(formatedUser)
+	if err != nil {
+		return token, expireIn, err
+	}
+
+	return token, expireIn, nil
+}
+
+// 退出登录
+func Logout(userId int64) {
+	auth.Exit(userId)
+}

+ 279 - 0
service/user/user.go

@@ -0,0 +1,279 @@
+package user
+
+import (
+	"authService/model"
+	"authService/response"
+	"authService/service/role"
+	"authService/util"
+	"authService/validators"
+	"errors"
+
+	jsoniter "github.com/json-iterator/go"
+	"github.com/samber/lo"
+	"gorm.io/gorm"
+)
+
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+
+// 获取用户信息
+func Find(id int64) (*model.User, *response.ErrCode) {
+	var user model.User
+	err := model.DB.Where("id = ?", id).Preload("Roles").First(&user).Error
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil, &response.ErrCode{
+				Code: response.DATA_NOT_FOUND,
+				Msg:  "用户不存在",
+			}
+		}
+		return nil, response.Err
+	}
+	return &user, nil
+}
+
+// 根据账号查找
+func FindByAccount(account string) *model.User {
+	var existUser *model.User
+	model.DB.Where("account = ?", account).Preload("Roles").First(&existUser)
+	return existUser
+}
+
+// 创建账号
+func Create(user *validators.User, password string) *response.ErrCode {
+	status := model.UserStatusDisable
+	if user.Status != 0 {
+		status = user.Status
+	}
+	if ExistUserAccount(user.Account, 0) {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "账户名已存在",
+		}
+	}
+
+	var createUser = &model.User{
+		Account:  user.Account,
+		Nickname: user.Nickname,
+		Status:   status,
+		Password: password,
+	}
+
+	// 创建
+	err := model.DB.Create(&createUser).Error
+	if err != nil {
+		return response.Err
+	}
+
+	// 创建用户角色
+	var roles = make([]*model.UserRole, 0)
+	for _, roleId := range user.RoleIds {
+		roles = append(roles, &model.UserRole{
+			UserId: createUser.ID,
+			RoleId: roleId,
+		})
+
+	}
+	model.DB.CreateInBatches(roles, 100)
+
+	return nil
+}
+
+// 修改账号
+func Update(user *validators.User, password string) *response.ErrCode {
+	existsUser, findErr := Find(user.ID)
+	if findErr != nil {
+		return findErr
+	}
+	if existsUser.IsSuper == model.UserIsSuperTrue {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "该用户禁止被修改",
+		}
+	}
+	// 更新
+	err := model.DB.Where("id = ?", user.ID).Updates(&model.User{
+		Nickname: user.Nickname,
+		Status:   user.Status,
+		Password: password,
+	}).Error
+	if err != nil {
+		return response.Err
+	}
+
+	// 删除用户角色
+	model.DB.Where("user_id = ?", user.ID).Delete(&model.UserRole{})
+	// 创建用户角色
+	var roles = make([]*model.UserRole, 0)
+	for _, roleId := range user.RoleIds {
+		roles = append(roles, &model.UserRole{
+			UserId: user.ID,
+			RoleId: roleId,
+		})
+
+	}
+
+	model.DB.CreateInBatches(roles, 100)
+
+	// 移除登录缓存
+	Logout(user.ID)
+
+	return nil
+}
+
+// 修改备注信息
+func UpdateRemark(id int64, remark string) *response.ErrCode {
+	existsUser, findErr := Find(id)
+	if findErr != nil {
+		return findErr
+	}
+	if existsUser.IsSuper == model.UserIsSuperTrue {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "该用户禁止被修改",
+		}
+	}
+
+	// 更新
+	err := model.DB.Where("id = ?", id).Update("remark", remark).Error
+	if err != nil {
+		return response.Err
+	}
+
+	return nil
+}
+
+// 修改或创建
+func UpdateOrCreate(user *validators.User, password string) *response.ErrCode {
+	if password != "" {
+		password = util.Md5(password)
+	}
+	var err *response.ErrCode
+	if user.ID != 0 {
+		err = Update(user, password)
+	} else {
+		err = Create(user, password)
+	}
+
+	return err
+}
+
+// 检查账户名是否重复
+func ExistUserAccount(account string, userId int64) bool {
+	var existUser *model.User
+	model.DB.Where("account = ?", account).First(&existUser)
+	if existUser != nil {
+		if existUser.ID != 0 && existUser.ID != userId {
+			return true
+		}
+	}
+	return false
+}
+
+// 修改自己的信息
+func UpdateMySelf(user *validators.User, oldPassword string, password string, userId int64) *response.ErrCode {
+	if password != "" {
+		password = util.Md5(password)
+		if oldPassword == "" {
+			return &response.ErrCode{
+				Code: response.ERROR,
+				Msg:  "请输入原密码",
+			}
+		}
+		oldPassword = util.Md5(oldPassword)
+	}
+	existsUser, findErr := Find(userId)
+	if findErr != nil {
+		return findErr
+	}
+	// 如果不是超级管理员,禁止被修改账户名
+	if existsUser.IsSuper == model.UserIsSuperFalse {
+		user.Account = ""
+	}
+
+	// 检查原密码是否正确
+	if oldPassword != existsUser.Password {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "原密码不正确",
+		}
+	}
+
+	// 检查账户名是否重复
+	if user.Account != "" && ExistUserAccount(user.Account, userId) {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "账户名已存在",
+		}
+	}
+
+	// 更新
+	err := model.DB.Where("id = ?", userId).Updates(&model.User{
+		Account:  user.Account,
+		Nickname: user.Nickname,
+		Password: password,
+	}).Error
+	if err != nil {
+		return response.Err
+	}
+
+	return nil
+}
+
+// 删除
+func Delete(id int64) *response.ErrCode {
+
+	existsUser, findErr := Find(id)
+	if findErr != nil {
+		return findErr
+	}
+
+	if existsUser.IsSuper == model.UserIsSuperTrue {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "该用户禁止被修改",
+		}
+	}
+
+	model.DB.Where("id = ?", id).Delete(&model.User{})
+
+	// 移除登录缓存
+	Logout(id)
+
+	return nil
+}
+
+// 获取分页列表
+func Paginate(page, pageSize int, key string) (list []*model.User, total int64) {
+	query := model.DB.Model(&model.User{}).Where("is_super = ?", model.UserIsSuperFalse)
+	if key != "" {
+		query = query.Where("account like ? or nickname like ?", "%"+key+"%", "%"+key+"%")
+	}
+	query.Count(&total)
+	query.Scopes(model.Paginate(page, pageSize)).Preload("Roles").Order("id desc").Find(&list)
+	return list, total
+}
+
+// 格式化用户
+func Format(user *model.User) *validators.User {
+	var roleIds = make([]int64, 0)
+	var roles = make([]*validators.Role, 0)
+	var permissions = make([]string, 0)
+	for _, currentRole := range user.Roles {
+		roleIds = append(roleIds, currentRole.ID)
+		formatedRole := role.FormatRole(&currentRole)
+		roles = append(roles, formatedRole)
+		permissions = append(permissions, formatedRole.Permissions...)
+	}
+	permissions = lo.Uniq(permissions)
+	return &validators.User{
+		ID:          user.ID,
+		Account:     user.Account,
+		Nickname:    user.Nickname,
+		Status:      user.Status,
+		IsSuper:     user.IsSuper,
+		Permissions: permissions,
+		RoleIds:     roleIds,
+		Roles:       roles,
+		Remark:      user.Remark,
+	}
+}

+ 68 - 0
service/user/user_test.go

@@ -0,0 +1,68 @@
+package user_test
+
+import (
+	"authService/model"
+	"authService/service/user"
+	"authService/util"
+	"authService/validators"
+	"testing"
+)
+
+func TestCreateUser(t *testing.T) {
+	util.InitTest()
+	var createUser = &validators.User{
+		Account:     "hailin",
+		Nickname:    "超级管理员",
+		Status:      model.UserStatusEnable,
+		IsSuper:     model.UserIsSuperTrue,
+		Permissions: []string{},
+	}
+	err := user.UpdateOrCreate(createUser, "qwer1234")
+	if err != nil {
+		t.Error(err)
+	}
+}
+
+func TestUpdateUser(t *testing.T) {
+	util.InitTest()
+	var updateUser = &validators.User{
+		ID:          1,
+		Account:     "hailinnn",
+		Nickname:    "",
+		Status:      model.UserStatusDisable,
+		IsSuper:     model.UserIsSuperTrue,
+		Permissions: []string{"User"},
+	}
+
+	err := user.UpdateOrCreate(updateUser, "123456")
+	if err != nil {
+		t.Error(err)
+	}
+}
+
+func TestUpdateMySelf(t *testing.T) {
+	util.InitTest()
+	var updateUser = &validators.User{
+		Account:  "hailin",
+		Nickname: "哈林",
+	}
+
+	err := user.UpdateMySelf(updateUser, "", "", 1)
+	if err != nil {
+		t.Error(err)
+	}
+}
+
+func TestPaginate(t *testing.T) {
+	util.InitTest()
+	users, total := user.Paginate(1, 10, "")
+	t.Log(users, total)
+
+	var list []*validators.User
+	for _, u := range users {
+		list = append(list, user.Format(u))
+	}
+
+	t.Log(util.JsonEncode(list))
+
+}

BIN
tests/.DS_Store


BIN
util/.DS_Store


+ 51 - 0
util/aliyun/dysms.go

@@ -0,0 +1,51 @@
+package aliyun
+
+import (
+	"fmt"
+	"os"
+
+	openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
+	dysmsapi20170525 "github.com/alibabacloud-go/dysmsapi-20170525/v3/client"
+	util "github.com/alibabacloud-go/tea-utils/v2/service"
+	"github.com/alibabacloud-go/tea/tea"
+)
+
+func Init() (*dysmsapi20170525.Client, error) {
+	AccessKeyId := os.Getenv("ALIYUN_ACCESS_KEY_ID")
+	AccessKeySecret := os.Getenv("ALIYUN_ACCESS_KEY_SECRET")
+	config := &openapi.Config{
+		// 您的 AccessKey ID
+		AccessKeyId: &AccessKeyId,
+		// 您的 AccessKey Secret
+		AccessKeySecret: &AccessKeySecret,
+	}
+	// 访问的域名
+	config.Endpoint = tea.String("dysmsapi.aliyuncs.com")
+	client := &dysmsapi20170525.Client{}
+	client, err := dysmsapi20170525.NewClient(config)
+	if err != nil {
+		return nil, err
+	}
+
+	return client, nil
+}
+
+// 发送短信验证码
+func SendCaptcha(mobile string, code string) error {
+	sendSmsRequest := &dysmsapi20170525.SendSmsRequest{
+		PhoneNumbers:  &mobile,
+		SignName:      tea.String("上医未来"),
+		TemplateCode:  tea.String("SMS_243630961"),
+		TemplateParam: tea.String(fmt.Sprintf("{\"code\":\"%s\"}", code)),
+	}
+	client, err := Init()
+	if err != nil {
+		return err
+	}
+	runtime := &util.RuntimeOptions{}
+	_, err = client.SendSmsWithOptions(sendSmsRequest, runtime)
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 5 - 0
util/constants/constants.go

@@ -0,0 +1,5 @@
+package constants
+
+const (
+	UserCacheKey = "user" // 用户登录的缓存键
+)

+ 1 - 0
util/log/log.go

@@ -0,0 +1 @@
+package log

+ 73 - 0
util/rabbitmq/index.go

@@ -0,0 +1,73 @@
+package rabbitmq
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"time"
+
+	jsoniter "github.com/json-iterator/go"
+	"github.com/streadway/amqp"
+)
+
+type WebhookMessage struct {
+	Body   any `json:"body"`
+	Config struct {
+		Url string `json:"url"`
+	} `json:"config"`
+}
+
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+
+func Webhook(webhookMessage WebhookMessage) error {
+	conn, err := Init()
+	if err != nil {
+		return err
+	}
+	p, err := conn.Producer(&Exchange{
+		Name: os.Getenv("RABBITMQ_WEBHOOK_EXCHANGE"),
+		Kind: amqp.ExchangeDirect,
+		Key:  os.Getenv("RABBITMQ_WEBHOOK_ROUTING"),
+	}, nil)
+	if err != nil {
+		return err
+	}
+
+	message, err := json.MarshalToString(webhookMessage)
+	if err != nil {
+		return err
+	}
+	fmt.Println("message", message)
+
+	err = p.PublishExchange(amqp.Publishing{
+		ContentType:  "text/plain",
+		DeliveryMode: amqp.Persistent,
+		Body:         []byte(message),
+	})
+	if err != nil {
+		return err
+	}
+	// 关闭连接
+	defer conn.conn.Close()
+	return nil
+}
+
+func Init() (*Conn, error) {
+	var broker2 = Broker{
+		Ssl:       false,                          // bool
+		Username:  os.Getenv("RABBITMQ_USER"),     // string
+		Password:  os.Getenv("RABBITMQ_PASSWORD"), // string
+		Server:    os.Getenv("RABBITMQ_HOST"),     // string
+		Port:      os.Getenv("RABBITMQ_PORT"),     // string
+		Vhost:     os.Getenv("RABBITMQ_VHOST"),    // string
+		TSL:       nil,                            // *tls.Config
+		ProxyAddr: "",                             // string
+		Beatime:   15 * time.Second,               // time.Duration
+	}
+
+	conn, err := New(context.Background(), broker2)
+	if err != nil {
+		return nil, err
+	}
+	return conn, nil
+}

+ 5 - 0
util/rabbitmq/logger.go

@@ -0,0 +1,5 @@
+package rabbitmq
+
+type Logger interface {
+	Errorf(format string, args ...interface{})
+}

+ 103 - 0
util/rabbitmq/producer.go

@@ -0,0 +1,103 @@
+package rabbitmq
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"time"
+
+	"github.com/streadway/amqp"
+)
+
+var p *Producer
+
+type Producer struct {
+	ctx         context.Context
+	msgIDPrefix string
+	exchange    *Exchange
+	ch          *amqp.Channel
+	conn        *Conn
+	logger      Logger
+	queue       *Queue
+}
+
+func Publish(data string) error {
+	conn, err := New(context.Background(), Broker{
+		Ssl:       false,                          // bool
+		Username:  os.Getenv("RABBITMQ_USER"),     // string
+		Password:  os.Getenv("RABBITMQ_PASSWORD"), // string
+		Server:    os.Getenv("RABBITMQ_HOST"),     // string
+		Port:      os.Getenv("RABBITMQ_PORT"),     // string
+		Vhost:     os.Getenv("RABBITMQ_VHOST"),    // string
+		TSL:       nil,                            // *tls.Config
+		ProxyAddr: "",                             // string
+		Beatime:   15 * time.Second,               // time.Duration
+	})
+	if err != nil {
+		panic(err)
+	}
+	var exchange *Exchange
+	var queue *Queue
+	if os.Getenv("RABBITMQ_PDF_BUILDER_QUEUE") != "" {
+		queue = &Queue{
+			Name:       os.Getenv("RABBITMQ_PDF_BUILDER_QUEUE"),
+			Durable:    true,
+			AutoDelete: true,
+		}
+	}
+	if exchange != nil || queue != nil {
+		_, err = conn.Producer(exchange, queue)
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	if p == nil {
+		return errors.New("nil producer")
+	}
+	// 关闭连接
+	defer conn.conn.Close()
+	return p.Publish(amqp.Publishing{
+		ContentType:  "text/plain",
+		DeliveryMode: amqp.Persistent,
+		Body:         []byte(data),
+	})
+}
+
+func PublishExchange(data string) error {
+	if p == nil {
+		return errors.New("nil producer")
+	}
+	return p.PublishExchange(amqp.Publishing{
+		ContentType:  "text/plain",
+		DeliveryMode: amqp.Persistent,
+		Body:         []byte(data),
+	})
+}
+
+func (p *Producer) Conn() *Conn {
+	return p.conn
+}
+
+func (p *Producer) Publish(msg amqp.Publishing) error {
+	msg.MessageId = fmt.Sprintf("%s:%d", p.msgIDPrefix, p.conn.MsgId())
+	return p.ch.Publish(
+		"",
+		p.queue.Name, // routing key
+		false,        // mandatory
+		false,        // immediate
+		msg,
+	)
+}
+
+func (p *Producer) PublishExchange(msg amqp.Publishing) error {
+	msg.MessageId = fmt.Sprintf("%s:%d", p.msgIDPrefix, p.conn.MsgId())
+	return p.ch.Publish(
+		p.exchange.Name, // exchange
+		p.exchange.Key,  // routing key
+		false,           // mandatory
+		false,           // immediate
+		msg,
+	)
+}

+ 177 - 0
util/rabbitmq/rabbitmq.go

@@ -0,0 +1,177 @@
+package rabbitmq
+
+import (
+	"context"
+	"crypto/tls"
+	"net"
+	"strconv"
+	"strings"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/streadway/amqp"
+)
+
+type Broker struct {
+	Ssl       bool
+	Username  string
+	Password  string
+	Server    string
+	Port      string
+	Vhost     string
+	TSL       *tls.Config
+	ProxyAddr string
+	Beatime   time.Duration
+}
+
+type Conn struct {
+	ctx    context.Context
+	msgID  uint64
+	broker Broker
+	logger Logger
+	l      sync.Mutex
+	conn   *amqp.Connection
+}
+
+type Exchange struct {
+	Key        string     // 生产者routingKey, 消费者bindingKey
+	Name       string     // 交换机名称
+	Kind       string     // fanout(广播)	direct(直接交换)比fanout多加了一层密码限制(routingKey)	topic(主题)	headers(首部)
+	Durable    bool       // 是否持久化 建议true, RabbitMQ关闭后,没有持久化的Exchange将被清除
+	AutoDelete bool       // 是否自动删除 建议false, 如果没有与之绑定的Queue,直接删除
+	Internal   bool       // 是否内置的 建议false,如果为true,只能通过Exchange到Exchange
+	Delay      bool       // 是否是延迟队列
+	NoWait     bool       // 是否非阻塞 建议false, true表示阻塞,创建交换器的请求发送后,阻塞等待RMQ Server返回信息。false非阻塞:不会阻塞等待RMQ Server的返回信息,而RMQ Server也不会返回信息。(不推荐使用)
+	Args       amqp.Table // 其他参数, 死信就是通过该参数设置
+}
+
+type Queue struct {
+	Name          string
+	Durable       bool       // 是否持久化 建议true, RabbitMQ关闭后,没有持久化的Exchange将被清除
+	AutoDelete    bool       // 是否自动删除 建议false, 如果没有与之绑定的Queue,直接删除
+	Exclusive     bool       // 是否排外的 建议false, 当连接关闭时connection.close()该队列是否会自动删除。	该队列是否是私有的private,如果不是排外的,可以使用两个消费者都访问同一个队列,没有任何问题,如果是排外的,会对当前队列加锁,其他通道channel是不能访问的,如果强制访问会报异常:com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method(reply-code=405, reply-text=RESOURCE_LOCKED - cannot obtain exclusive access to locked queue 'queue_name' in vhost '/', class-id=50, method-id=20)。	应用于一个队列只能有一个消费者来消费的场景。
+	NoWait        bool       // 是否非阻塞 建议false, true表示阻塞,创建交换器的请求发送后,阻塞等待RMQ Server返回信息。false非阻塞:不会阻塞等待RMQ Server的返回信息,而RMQ Server也不会返回信息。(不推荐使用)
+	Args          amqp.Table // 其他参数, 延迟就是通过该参数设置
+	PrefetchCount int        // 会告诉RabbitMQ不要同时给一个消费者推送多于N个消息,即一旦有N个消息还没有ack,则该Consumer将block掉,直到有消息ack
+	PrefetchSize  int        // prefetchSize:最多传输的内容的大小的限制,0 为不限制,但据说prefetchSize参数,rabbitmq没有实现
+	Global        bool       // 是否将上面设置应用于channel,简单点说,就是上面限制是channel级别的还是Consumer级别)
+	AutoAck       bool       // 自动确认
+}
+
+func (broker Broker) DSN() string {
+	protocol := "amqp"
+	if broker.Ssl {
+		protocol = "amqps"
+	}
+	builder := strings.Builder{}
+	builder.WriteString(protocol)
+	builder.WriteString("://")
+	if broker.Username != `` {
+		builder.WriteString(broker.Username)
+		builder.WriteString(":")
+		builder.WriteString(broker.Password)
+		builder.WriteString("@")
+	}
+	builder.WriteString(broker.Server)
+	if broker.Port != `` {
+		builder.WriteString(":")
+		builder.WriteString(broker.Port)
+	}
+	builder.WriteString("/")
+	if broker.Vhost != `` {
+		builder.WriteString(broker.Vhost)
+	}
+	return builder.String()
+}
+
+func New(ctx context.Context, broker Broker) (*Conn, error) {
+	c := &Conn{
+		ctx:    ctx,
+		broker: broker,
+	}
+	//尝试连接
+	if err := c.connect(); err != nil {
+		return nil, err
+	}
+
+	return c, nil
+}
+
+// Producer 生成一个生产者
+func (c *Conn) Producer(exchange *Exchange, queue *Queue) (*Producer, error) {
+	p = &Producer{
+		ctx:         c.ctx,
+		msgIDPrefix: strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10),
+		exchange:    exchange,
+		conn:        c,
+		logger:      c.logger,
+		queue:       queue,
+	}
+	ch, err := c.channel(exchange, queue)
+	if err != nil {
+		return nil, err
+	}
+	p.ch = ch
+	return p, nil
+}
+
+func (c *Conn) MsgId() uint64 {
+	return atomic.AddUint64(&c.msgID, 1)
+}
+
+// 初始化channel
+func (c *Conn) channel(exchange *Exchange, queue *Queue) (*amqp.Channel, error) {
+	ch, err := c.conn.Channel()
+	if err != nil {
+		return nil, err
+	}
+
+	if exchange != nil {
+		err := ch.ExchangeDeclarePassive(exchange.Name, exchange.Kind, exchange.Durable, exchange.AutoDelete, exchange.Internal, exchange.NoWait, nil)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if queue != nil {
+		_, err = ch.QueueDeclarePassive(
+			queue.Name,
+			queue.Durable,    // durable
+			queue.AutoDelete, // auto_delete
+			queue.Exclusive,  // exclusive
+			queue.NoWait,     // no_wait
+			nil,
+		)
+	}
+	if err != nil {
+		return nil, err
+	}
+	return ch, nil
+}
+
+func (c *Conn) connect() error {
+	c.l.Lock()
+	if c.conn != nil && !c.conn.IsClosed() {
+		c.l.Unlock()
+		return nil
+	}
+	var err error
+	if c.broker.TSL != nil {
+		c.conn, err = amqp.DialTLS(c.broker.DSN(), c.broker.TSL)
+	} else if c.broker.ProxyAddr != `` {
+		c.conn, err = amqp.DialConfig(c.broker.DSN(), amqp.Config{
+			Dial: func(network, addr string) (net.Conn, error) {
+				return net.Dial("tcp", c.broker.ProxyAddr)
+			},
+		})
+	} else {
+		c.conn, err = amqp.Dial(c.broker.DSN())
+	}
+	if err == nil {
+		c.conn.Config.Heartbeat = c.broker.Beatime
+		c.conn.Config.Locale = "en_US"
+	}
+	c.l.Unlock()
+	return err
+}

+ 82 - 0
util/rabbitmq/rabbitmq_test.go

@@ -0,0 +1,82 @@
+package rabbitmq_test
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"authService/util/rabbitmq"
+
+	"github.com/streadway/amqp"
+)
+
+var broker2 = rabbitmq.Broker{
+	Ssl:       false,            // bool
+	Username:  "md",             // string
+	Password:  "fNaxiyVthc",     // string
+	Server:    "47.109.41.4",    // string
+	Port:      "5672",           // string
+	Vhost:     "drmd",           // string
+	TSL:       nil,              // *tls.Config
+	ProxyAddr: "",               // string
+	Beatime:   15 * time.Second, // time.Duration
+}
+
+func TestRabbitmq(t *testing.T) {
+	conn, err := rabbitmq.New(context.Background(), broker2)
+	if err != nil {
+		t.Fatal(err)
+	}
+	p, err := conn.Producer(nil, &rabbitmq.Queue{
+		Name:       "queue:dev:pdf:builder",
+		Durable:    true,
+		AutoDelete: true,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = p.Publish(amqp.Publishing{
+		ContentType:  "text/plain",
+		DeliveryMode: amqp.Persistent,
+		Body:         []byte("hello"),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = p.Publish(amqp.Publishing{
+		ContentType:  "text/plain",
+		DeliveryMode: amqp.Persistent,
+		Body:         []byte("world"),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	time.Sleep(time.Second * 1)
+}
+
+func TestRabbitmqDelay(t *testing.T) {
+	conn, err := rabbitmq.New(context.Background(), broker2)
+	if err != nil {
+		t.Fatal(err)
+	}
+	p, err := conn.Producer(&rabbitmq.Exchange{
+		Name: "queue:webhook:exchange",
+		Kind: amqp.ExchangeDirect,
+		Key:  "queue:webhook:routing",
+	}, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = p.PublishExchange(amqp.Publishing{
+		Headers: amqp.Table{
+			"x-delay": int64(10000),
+		},
+		ContentType:  "text/plain",
+		DeliveryMode: amqp.Persistent,
+		Body:         []byte("hello world"),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	time.Sleep(time.Second * 1)
+}

+ 255 - 0
util/util.go

@@ -0,0 +1,255 @@
+package util
+
+import (
+	"bytes"
+	"crypto/md5"
+	"crypto/rand"
+	"database/sql"
+	"encoding/hex"
+	"fmt"
+	"image"
+	"io/ioutil"
+	"math/big"
+	"net/http"
+	"regexp"
+	"strings"
+	"time"
+
+	"authService/cache"
+	"authService/model"
+	"authService/model/types"
+
+	"github.com/gin-gonic/gin"
+	"github.com/joho/godotenv"
+	jsoniter "github.com/json-iterator/go"
+	"github.com/speps/go-hashids/v2"
+)
+
+// php序列化的对象转map之后,key值会有特定的字节前缀导致无法解析,需要去掉
+func PhpMap2Go(in map[string]interface{}) map[string]interface{} {
+	result := make(map[string]interface{})
+	for i, v := range in {
+		tmpi := []byte(i)
+		if tmpi[0] == 0 && tmpi[1] == 42 && tmpi[2] == 0 {
+			result[string(tmpi[3:])] = v
+		} else {
+			result[i] = v
+		}
+	}
+	return result
+}
+
+func GetRequestParam(c *gin.Context, key string) string {
+	if c.Request.Method == http.MethodGet {
+		return c.Query(key)
+	} else {
+		// 如果是Json格式
+		if c.Request.Header.Get("Content-Type") == "application/json" {
+			JsonData := GetRequestJsonData(c)
+			// 判断类型
+			if _, ok := JsonData[key].(string); !ok {
+				return ""
+			}
+			return JsonData[key].(string)
+		}
+		return c.PostForm(key)
+	}
+}
+
+func GetRequestJsonData(c *gin.Context) map[string]any {
+	data, _ := c.GetRawData()
+	var body map[string]any
+	_ = json.Unmarshal(data, &body)
+	c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(data))
+	return body
+}
+
+func GetPostFormParams(c *gin.Context) (map[string]any, error) {
+	var postMap = make(map[string]any, len(c.Request.PostForm))
+	for k, v := range c.Request.PostForm {
+		if len(v) > 1 {
+			postMap[k] = v
+		} else if len(v) == 1 {
+			postMap[k] = v[0]
+		}
+	}
+
+	return postMap, nil
+}
+
+// 将ID编译为HashId
+func GetHashId(id int64, salt string) string {
+	hd := hashids.NewData()
+	hd.Salt = salt
+	hd.MinLength = 8
+	h, _ := hashids.NewWithData(hd)
+	hashId, _ := h.Encode([]int{int(id)})
+	return hashId
+}
+
+// 根据HashId获取真实ID
+func GetIdByHashId(hashId string, salt string) (int64, error) {
+	hd := hashids.NewData()
+	hd.Salt = salt
+	hd.MinLength = 8
+	h, _ := hashids.NewWithData(hd)
+	d, err := h.DecodeWithError(hashId)
+	if err != nil {
+		return 0, err
+	}
+	return int64(d[0]), nil
+}
+
+func GetFromGinContext[T any](ctx *gin.Context, key string) (T, bool) {
+	var some T
+	value, exist := ctx.Get(key)
+	if !exist {
+		return some, false
+	}
+	some, ok := value.(T)
+	if !ok {
+		return some, false
+	}
+	return some, true
+}
+
+// MD5加密
+func Md5(src string) string {
+	m := md5.New()
+	m.Write([]byte(src + "3OTBF91KlFGMdrDn"))
+	res := hex.EncodeToString(m.Sum(nil))
+	return res
+}
+
+// 获取随机数
+func GetRandomNumber() string {
+	result, _ := rand.Int(rand.Reader, big.NewInt(9999))
+	return fmt.Sprintf("%04d", result)
+}
+
+// 转为sql.NullTime
+func ToNullTime(t time.Time) types.NullTime {
+	return types.NullTime{NullTime: sql.NullTime{Time: t, Valid: !t.IsZero()}}
+}
+
+// 数据脱敏
+func HideStar(str string) (result string) {
+	if str == "" {
+		return ""
+	}
+	if strings.Contains(str, "@") { // 邮箱
+		res := strings.Split(str, "@")
+		if len(res[0]) < 3 {
+			resString := "***"
+			result = resString + "@" + res[1]
+		} else {
+			res2 := Substr2(str, 0, 3)
+			resString := res2 + "***"
+			result = resString + "@" + res[1]
+		}
+		return result
+	} else {
+		reg := `^1[0-9]\d{9}$`
+		rgx := regexp.MustCompile(reg)
+		mobileMatch := rgx.MatchString(str)
+		if mobileMatch { // 手机号
+			result = Substr2(str, 0, 3) + "****" + Substr2(str, 7, 11)
+		} else {
+			nameRune := []rune(str)
+			lens := len(nameRune)
+			if lens <= 1 {
+				result = "***"
+			} else if lens == 2 {
+				result = string(nameRune[:1]) + "*"
+			} else if lens == 3 {
+				result = string(nameRune[:1]) + "*" + string(nameRune[2:3])
+			} else if lens == 4 {
+				result = string(nameRune[:1]) + "**" + string(nameRune[lens-1:lens])
+			} else if lens > 4 {
+				result = string(nameRune[:2]) + "***" + string(nameRune[lens-2:lens])
+			}
+		}
+		return
+	}
+}
+func Substr2(str string, start int, end int) string {
+	rs := []rune(str)
+	return string(rs[start:end])
+}
+
+// 读取远程文件
+func ReadImageData(url string) image.Image {
+	resp, err := http.Get(url)
+	if err != nil {
+		return nil
+	}
+	defer resp.Body.Close()
+	img, _, err := image.Decode(resp.Body)
+	if err != nil {
+		return nil
+	}
+	return img
+}
+
+// 获取随机字符串
+func RandString(n int) string {
+	var longLetters = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ=_")
+	if n <= 0 {
+		return ""
+	}
+	b := make([]byte, n)
+	arc := uint8(0)
+	if _, err := rand.Read(b[:]); err != nil {
+		return ""
+	}
+	for i, x := range b {
+		arc = x & 63
+		b[i] = longLetters[arc]
+	}
+	return string(b)
+}
+
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+
+func JsonEncode(v interface{}) string {
+	str, _ := json.MarshalToString(v)
+	return str
+}
+
+func Println(v interface{}) string {
+	var str string
+	// 判断是否是string类型
+	str = JsonEncode(str)
+	return str
+}
+
+func InArrayString(val string, arr []string) bool {
+	for _, v := range arr {
+		if v == val {
+			return true
+		}
+	}
+	return false
+}
+
+func InitTest() {
+	// 加载dotEnv环境
+	loadEnvErr := godotenv.Load("/Users/huang/Desktop/hys/authService/.env")
+	if loadEnvErr != nil {
+		fmt.Println("ENV环境加载Error")
+		return
+	}
+	// 开始初始化数据库
+	model.Construct(false)
+
+	// 开始初始化缓存
+	cache.InitRedis()
+}
+
+// 手机号
+func RegexMobile(mobile string) bool {
+	if matched, _ := regexp.MatchString(`^1[3456789]\d{9}$`, mobile); matched {
+		return true
+	}
+	return false
+}

+ 56 - 0
util/validator/validator.go

@@ -0,0 +1,56 @@
+package validator
+
+//将验证器错误翻译成中文
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/gin-gonic/gin/binding"
+	"github.com/go-playground/locales/zh"
+	ut "github.com/go-playground/universal-translator"
+	"github.com/go-playground/validator/v10"
+	zh_translations "github.com/go-playground/validator/v10/translations/zh"
+)
+
+var (
+	uni *ut.UniversalTranslator
+	// validate *validator.Validate
+	trans ut.Translator
+)
+
+func Init() {
+	//注册翻译器
+	zh := zh.New()
+	uni = ut.New(zh, zh)
+
+	trans, _ = uni.GetTranslator("zh")
+
+	//获取gin的校验器
+	validate := binding.Validator.Engine().(*validator.Validate)
+	//注册翻译器
+	zh_translations.RegisterDefaultTranslations(validate, trans)
+}
+
+// Translate 翻译错误信息
+func Translate(err error) map[string][]string {
+
+	var result = make(map[string][]string)
+
+	errors := err.(validator.ValidationErrors)
+
+	for _, err := range errors {
+		result[err.Field()] = append(result[err.Field()], err.Translate(trans))
+	}
+	return result
+}
+
+func TranslateError(err error) error {
+	var validationErr validator.ValidationErrors
+	if errors.As(err, &validationErr) {
+		errs := err.(validator.ValidationErrors)
+		return errors.New(errs[0].Translate(trans))
+	}
+	fmt.Println("数据格式校验错误: ", err)
+	return errors.New("数据格式校验不通过,请仔细检查必填项")
+}

+ 11 - 0
validators/auth.go

@@ -0,0 +1,11 @@
+package validators
+
+type AuthUser struct {
+	ID             int64    `json:"id"`             // 用户ID
+	Account        string   `json:"account"`        // 账号
+	Nickname       string   `json:"nickname"`       // 昵称
+	FullPermission bool     `json:"fullPermission"` // 是否全权限覆盖
+	RoleIds        []int64  `json:"roleIds"`        // 角色ID列表
+	Permissions    []string `json:"permissions"`    // 权限KEY列表
+	LoginAt        int64    `json:"loginAt"`        // 当前Token派发登录时间
+}

+ 34 - 0
validators/user.go

@@ -0,0 +1,34 @@
+package validators
+
+type User struct {
+	ID          int64    `json:"id" form:"id" binding:"omitempty,numeric"`
+	Account     string   `json:"account" form:"account" binding:"required,alphanum,min=4,max=20"`
+	Nickname    string   `json:"nickname" form:"nickname" binding:"required"`
+	Status      int      `json:"status" form:"status" binding:"omitempty,oneof=1 2"`
+	IsSuper     int      `json:"isSuper" form:"isSuper" binding:"omitempty,oneof=1 2"`
+	Permissions []string `json:"permissions" form:"-" binding:"-"`
+	Remark      string   `json:"remark" form:"remark" binding:"omitempty,max=255"`
+	RoleIds     []int64  `json:"roleIds" form:"roleIds" binding:"omitempty,dive,numeric"`
+	Roles       []*Role  `json:"roles" form:"-" binding:"-"`
+}
+
+// 用户权限组
+type UserPermissionGroup struct {
+	Label    string           `json:"label"`    // 权限组名称
+	Children []UserPermission `json:"children"` // 权限组下的权限
+}
+
+// 用户权限
+type UserPermission struct {
+	Label          string   `json:"label"`          // 权限名称
+	Value          string   `json:"value"`          // 权限值
+	RequiredValues []string `json:"requiredValues"` // 必须勾选的其他权限
+}
+
+type Role struct {
+	ID          int64    `json:"id" form:"id" binding:"omitempty,numeric"`
+	Name        string   `json:"name" form:"name" binding:"required"`
+	Permissions []string `json:"permissions" form:"permissions" binding:"omitempty,dive"`
+	UserTotal   int      `json:"userTotal" form:"-" binding:"-"`
+	CreatedAt   string   `json:"createdAt" form:"-" binding:"-"`
+}