├── main_windows_386.syso
├── web
└── web.go
├── main_windows_amd64.syso
├── wiki
├── img
│ ├── Feature-V1.png
│ ├── Git-Commit-Template-Use.jpg
│ └── Git-Commit-Template-Open.jpg
└── sql
│ ├── mysql_5.7_20221111.sql
│ └── mysql_20221122.sql
├── global
├── constant.go
├── global.go
└── model.go
├── config
├── upload.go
├── jwt.go
├── config.go
├── system.go
├── zap.go
└── gorm.go
├── api
├── service
│ ├── system
│ │ ├── enter.go
│ │ ├── file.go
│ │ ├── menu.go
│ │ ├── user.go
│ │ └── role.go
│ ├── message
│ │ ├── enter.go
│ │ ├── channelTemplate.go
│ │ ├── mailLog.go
│ │ ├── channel.go
│ │ ├── application.go
│ │ └── mailTemplate.go
│ └── enter.go
├── router
│ ├── system
│ │ ├── enter.go
│ │ ├── base.go
│ │ ├── file.go
│ │ ├── menu.go
│ │ ├── user.go
│ │ └── role.go
│ ├── message
│ │ ├── enter.go
│ │ ├── mailLog.go
│ │ ├── channelTemplate.go
│ │ ├── mailTemplate.go
│ │ ├── application.go
│ │ └── channel.go
│ └── enter.go
├── model
│ ├── system
│ │ ├── response
│ │ │ ├── role.go
│ │ │ ├── user.go
│ │ │ └── menu.go
│ │ ├── request
│ │ │ ├── jwt.go
│ │ │ ├── clamis.go
│ │ │ ├── role.go
│ │ │ ├── menu.go
│ │ │ └── user.go
│ │ ├── file.go
│ │ ├── role.go
│ │ ├── user.go
│ │ └── menu.go
│ ├── message
│ │ ├── response
│ │ │ ├── application.go
│ │ │ ├── channel.go
│ │ │ └── mailTemplate.go
│ │ ├── application.go
│ │ ├── channelTemplate.go
│ │ ├── common.go
│ │ ├── mailTemplate.go
│ │ ├── channel.go
│ │ └── mailLog.go
│ ├── common
│ │ ├── response
│ │ │ ├── common.go
│ │ │ └── response.go
│ │ ├── attachment.go
│ │ ├── request
│ │ │ └── common.go
│ │ └── localTime.go
│ └── validation
│ │ ├── verify.go
│ │ └── validator.go
└── v1
│ ├── enter.go
│ ├── system
│ ├── enter.go
│ ├── file.go
│ ├── menu.go
│ ├── role.go
│ └── user.go
│ └── message
│ ├── enter.go
│ ├── channelTemplate.go
│ ├── mailLog.go
│ ├── application.go
│ ├── channel.go
│ └── mailTemplate.go
├── .gitignore
├── utils
├── ip_test.go
├── open_darwin.go
├── open_unix.go
├── md5_test.go
├── open_windows.go
├── open.go
├── oss
│ ├── oss.go
│ └── local.go
├── id_test.go
├── id.go
├── rotatelogs_windows.go
├── directory.go
├── rotatelogs_unix.go
├── md5.go
├── helper
│ ├── aliyun_test.go
│ └── aliyun.go
├── ip.go
└── snowflake
│ └── snowflake.go
├── .editorconfig
├── middleware
├── recover.go
├── logger.go
├── cors.go
├── rbac.go
└── jwt.go
├── core
├── sms
│ ├── aliyun_test.go
│ └── aliyun.go
├── cron
│ └── cron_test.go
├── mail
│ ├── smtp_test.go
│ └── smtp.go
├── server.go
├── viper.go
├── zap.go
└── websocket
│ └── ws.go
├── .goreleaser.yaml
├── config-sample.yaml
├── main.go
├── CONTRIBUTION.md
├── README.md
├── initialize
├── router.go
├── gorm.go
└── logger
│ └── logger.go
├── go.mod
└── LICENSE
/main_windows_386.syso:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devote-team/aixinge/HEAD/main_windows_386.syso
--------------------------------------------------------------------------------
/web/web.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import "embed"
4 |
5 | //go:embed dist
6 | var Dist embed.FS
7 |
--------------------------------------------------------------------------------
/main_windows_amd64.syso:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devote-team/aixinge/HEAD/main_windows_amd64.syso
--------------------------------------------------------------------------------
/wiki/img/Feature-V1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devote-team/aixinge/HEAD/wiki/img/Feature-V1.png
--------------------------------------------------------------------------------
/global/constant.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | const (
4 | ConfigEnv = "CONFIG"
5 | ConfigFile = "config.yaml"
6 | )
7 |
--------------------------------------------------------------------------------
/wiki/img/Git-Commit-Template-Use.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devote-team/aixinge/HEAD/wiki/img/Git-Commit-Template-Use.jpg
--------------------------------------------------------------------------------
/wiki/img/Git-Commit-Template-Open.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devote-team/aixinge/HEAD/wiki/img/Git-Commit-Template-Open.jpg
--------------------------------------------------------------------------------
/config/upload.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | type Upload struct {
4 | Path string `mapstructure:"path" json:"path" yaml:"path"` // 上传文件路径
5 |
6 | }
7 |
--------------------------------------------------------------------------------
/api/service/system/enter.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | type ServiceGroup struct {
4 | UserService
5 | RoleService
6 | MenuService
7 | FileService
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vscode/
3 | .fleet/
4 | log/
5 | bin/
6 | dist/
7 | upload/
8 | *.db
9 | *.exe
10 | latest_log
11 | .bin
12 | .DS_Store
13 | config.yaml
14 |
--------------------------------------------------------------------------------
/api/router/system/enter.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | type RouterGroup struct {
4 | BaseRouter
5 | MenuRouter
6 | UserRouter
7 | RoleRouter
8 | FilesRouter
9 | }
10 |
--------------------------------------------------------------------------------
/api/model/system/response/role.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "aixinge/api/model/system"
5 | )
6 |
7 | type RoleResponse struct {
8 | Role system.Role `json:"role"`
9 | }
10 |
--------------------------------------------------------------------------------
/api/model/message/response/application.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import "aixinge/api/model/message"
4 |
5 | type AppResponse struct {
6 | Application message.Application `json:"application"`
7 | }
8 |
--------------------------------------------------------------------------------
/api/router/message/enter.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | type RouterGroup struct {
4 | ApplicationRouter
5 | ChannelRouter
6 | ChannelTemplateRouter
7 | MailLogRouter
8 | MailTemplateRouter
9 | }
10 |
--------------------------------------------------------------------------------
/utils/ip_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "log"
5 | "testing"
6 | )
7 |
8 | func TestExternalIP(t *testing.T) {
9 | ip, err := ExternalIP()
10 | log.Println(ip.String(), err)
11 | }
12 |
--------------------------------------------------------------------------------
/api/model/message/response/channel.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "aixinge/api/model/message"
5 | )
6 |
7 | type ChannelResponse struct {
8 | Channel message.Channel `json:"channel"`
9 | }
10 |
--------------------------------------------------------------------------------
/api/service/message/enter.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | type ServiceGroup struct {
4 | ApplicationService
5 | ChannelService
6 | ChannelTemplateService
7 | MailLogService
8 | MailTemplateService
9 | }
10 |
--------------------------------------------------------------------------------
/utils/open_darwin.go:
--------------------------------------------------------------------------------
1 | //go:build darwin
2 | // +build darwin
3 |
4 | package utils
5 |
6 | import (
7 | "os/exec"
8 | )
9 |
10 | func OpenUri(uri string) {
11 | exec.Command(`open`, uri).Start()
12 | }
13 |
--------------------------------------------------------------------------------
/api/model/message/response/mailTemplate.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "aixinge/api/model/message"
5 | )
6 |
7 | type MailTemplateResponse struct {
8 | MailTemplate message.MailTemplate `json:"mailTemplate"`
9 | }
10 |
--------------------------------------------------------------------------------
/utils/open_unix.go:
--------------------------------------------------------------------------------
1 | //go:build aix || dragonfly || freebsd || linux || netbsd || openbsd || solaris
2 | // +build aix dragonfly freebsd linux netbsd openbsd solaris
3 |
4 | package utils
5 |
6 | func OpenUri(uri string) {
7 | // to do nothing
8 | }
9 |
--------------------------------------------------------------------------------
/utils/md5_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/stretchr/testify/assert"
5 | "testing"
6 | )
7 |
8 | func TestGetFileMd5(t *testing.T) {
9 | assert.Equal(t, "ed7fb0c7ce9e95343016a3e7f6be70dd", GetFileMd5("./md5.go"))
10 | }
11 |
--------------------------------------------------------------------------------
/api/v1/enter.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "aixinge/api/v1/message"
5 | "aixinge/api/v1/system"
6 | )
7 |
8 | type ApiGroup struct {
9 | SystemApi system.ApiGroup
10 | MessageApi message.ApiGroup
11 | }
12 |
13 | var AppApi = new(ApiGroup)
14 |
--------------------------------------------------------------------------------
/config/jwt.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | type JWT struct {
4 | SigningKey string `mapstructure:"signing-key" json:"signingKey" yaml:"signing-key"` // jwt签名
5 | ExpiresTime uint `mapstructure:"expires-time" json:"expiresTime" yaml:"expires-time"` // 过期时间
6 | }
7 |
--------------------------------------------------------------------------------
/api/router/enter.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "aixinge/api/router/message"
5 | "aixinge/api/router/system"
6 | )
7 |
8 | type Router struct {
9 | System system.RouterGroup
10 | Message message.RouterGroup
11 | }
12 |
13 | var AppRouter = new(Router)
14 |
--------------------------------------------------------------------------------
/global/global.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | import (
4 | "aixinge/config"
5 | "github.com/spf13/viper"
6 | "go.uber.org/zap"
7 | "gorm.io/gorm"
8 | )
9 |
10 | var (
11 | DB *gorm.DB
12 | CONFIG config.Server
13 | VP *viper.Viper
14 | LOG *zap.Logger
15 | )
16 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | trim_trailing_whitespace = true
6 | insert_final_newline = true
7 | end_of_line = lf
8 |
9 | [*.go]
10 | indent_style = space
11 | indent_size = 4
12 |
13 | [*.yaml]
14 | indent_style = space
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/middleware/recover.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "github.com/gofiber/fiber/v2"
5 | "github.com/gofiber/fiber/v2/middleware/recover"
6 | )
7 |
8 | func Recover() fiber.Handler {
9 | return recover.New(recover.Config{
10 | EnableStackTrace: true,
11 | })
12 | }
13 |
--------------------------------------------------------------------------------
/api/service/enter.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "aixinge/api/service/message"
5 | "aixinge/api/service/system"
6 | )
7 |
8 | type Service struct {
9 | SystemService system.ServiceGroup
10 | MessageService message.ServiceGroup
11 | }
12 |
13 | var AppService = new(Service)
14 |
--------------------------------------------------------------------------------
/utils/open_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 | // +build windows
3 |
4 | package utils
5 |
6 | import (
7 | "os/exec"
8 | "syscall"
9 | )
10 |
11 | func OpenUri(uri string) {
12 | cmd := exec.Command(`cmd`, `/c`, `start`, uri)
13 | cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
14 | cmd.Start()
15 | }
16 |
--------------------------------------------------------------------------------
/utils/open.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "aixinge/global"
5 | "fmt"
6 | )
7 |
8 | func Open() {
9 | uri := `http://`
10 | ip, err := ExternalIP()
11 | if err == nil {
12 | uri += ip.String()
13 | } else {
14 | uri += `localhost`
15 | }
16 | uri += fmt.Sprintf("%s%d", `:`, global.CONFIG.System.Port)
17 | OpenUri(uri)
18 | }
19 |
--------------------------------------------------------------------------------
/api/model/system/response/user.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "aixinge/api/model/system"
5 | )
6 |
7 | type UserResponse struct {
8 | User system.User `json:"user"`
9 | }
10 |
11 | type LoginResponse struct {
12 | User system.User `json:"user"`
13 | Token string `json:"token"`
14 | RefreshToken string `json:"refreshToken"`
15 | }
16 |
--------------------------------------------------------------------------------
/utils/oss/oss.go:
--------------------------------------------------------------------------------
1 | package oss
2 |
3 | import (
4 | "mime/multipart"
5 | )
6 |
7 | type OSS interface {
8 | UploadFile(file *multipart.FileHeader) (string, string, string, error)
9 |
10 | FGetObject(key, infile string) error
11 |
12 | GetObject(key string) ([]byte, error)
13 |
14 | DeleteFile(key string) error
15 | }
16 |
17 | func NewLocal() OSS {
18 | return &Local{}
19 | }
20 |
--------------------------------------------------------------------------------
/api/model/message/application.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import "aixinge/global"
4 |
5 | type Application struct {
6 | global.MODEL
7 | Name string `json:"name"` // 应用名称
8 | AppKey string `json:"appKey"` // 应用 ID
9 | AppSecret string `json:"appSecret"` // 应用密钥
10 | Remark string `json:"remark"` // 备注
11 | Status int `json:"status"` // 状态,1、正常 2、禁用
12 | }
13 |
--------------------------------------------------------------------------------
/middleware/logger.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "github.com/gofiber/fiber/v2"
5 | "github.com/gofiber/fiber/v2/middleware/logger"
6 | )
7 |
8 | func Logger() fiber.Handler {
9 | config := logger.ConfigDefault
10 | config.Format = "${time} ${status} - ${latency} ${method} ${path} \n"
11 | config.TimeFormat = "2006/01/02 - 15:04:05"
12 | return logger.New(config)
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/api/router/system/base.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/api/v1"
5 | "github.com/gofiber/fiber/v2"
6 | )
7 |
8 | type BaseRouter struct {
9 | }
10 |
11 | func (s *BaseRouter) InitBaseRouter(router fiber.Router) fiber.Router {
12 | var userApi = v1.AppApi.SystemApi.User
13 | {
14 | router.Post("login", userApi.Login)
15 | router.Post("refresh-token", userApi.RefreshToken)
16 | }
17 | return router
18 | }
19 |
--------------------------------------------------------------------------------
/api/v1/system/enter.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import "aixinge/api/service"
4 |
5 | type ApiGroup struct {
6 | User
7 | Role
8 | Menu
9 | File
10 | }
11 |
12 | var menuService = service.AppService.SystemService.MenuService
13 | var userService = service.AppService.SystemService.UserService
14 | var roleService = service.AppService.SystemService.RoleService
15 | var fileService = service.AppService.SystemService.FileService
16 |
--------------------------------------------------------------------------------
/api/model/system/request/jwt.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "aixinge/utils/snowflake"
5 | "github.com/golang-jwt/jwt/v4"
6 | uuid "github.com/satori/go.uuid"
7 | )
8 |
9 | type TokenClaims struct {
10 | UUID uuid.UUID
11 | ID snowflake.ID
12 | Username string
13 | Nickname string
14 | jwt.RegisteredClaims
15 | }
16 |
17 | type RefreshTokenClaims struct {
18 | ID snowflake.ID
19 | jwt.RegisteredClaims
20 | }
21 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | type Server struct {
4 | JWT JWT `mapstructure:"jwt" json:"jwt" yaml:"jwt"`
5 | Zap Zap `mapstructure:"zap" json:"zap" yaml:"zap"`
6 | System System `mapstructure:"system" json:"system" yaml:"system"`
7 | // 数据库
8 | Database Database `mapstructure:"database" json:"database" yaml:"database"`
9 | // 上传文件
10 | Upload Upload `mapstructure:"upload" json:"upload" yaml:"upload"`
11 | }
12 |
--------------------------------------------------------------------------------
/api/model/common/response/common.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | type PageResult struct {
4 | List interface{} `json:"list"` // 数据列表
5 | Total int64 `json:"total"` // 总数
6 | Page int `json:"page"` // 页码
7 | PageSize int `json:"pageSize"` // 每页大小
8 | }
9 |
10 | // SelectResult 选择列表响应对应对象
11 | type SelectResult struct {
12 | ID uint `json:"id"` // 主键ID
13 | Name string `json:"name"` // 名称
14 | }
15 |
--------------------------------------------------------------------------------
/api/router/message/mailLog.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | v1 "aixinge/api/v1"
5 | "github.com/gofiber/fiber/v2"
6 | )
7 |
8 | type MailLogRouter struct {
9 | }
10 |
11 | func (m *MailLogRouter) InitMailLogRouter(router fiber.Router) {
12 | mlRouter := router.Group("mail-log")
13 | var mlApi = v1.AppApi.MessageApi.MailLog
14 | {
15 | mlRouter.Post("delete", mlApi.Delete) // 删除
16 | mlRouter.Post("page", mlApi.Page) // 分页
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/api/router/system/file.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/api/v1"
5 | "github.com/gofiber/fiber/v2"
6 | )
7 |
8 | type FilesRouter struct {
9 | }
10 |
11 | func (s *FilesRouter) InitFileRouter(router fiber.Router) {
12 | fileRouter := router.Group("file")
13 | var fileApi = v1.AppApi.SystemApi.File
14 | {
15 | fileRouter.Post("upload", fileApi.Upload) // 上传,记录操作日志
16 | fileRouter.Get("download", fileApi.Download) // 下载
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/api/model/system/request/clamis.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "aixinge/global"
5 | "github.com/gofiber/fiber/v2"
6 | )
7 |
8 | // GetUserInfo 从Gin的Context中获取从jwt解析出来的用户角色id
9 | func GetUserInfo(c *fiber.Ctx) *TokenClaims {
10 | if claims := c.Locals("claims"); claims == nil {
11 | global.LOG.Error("从Gin的Context中获取从jwt解析出来的用户UUID失败, 请检查路由是否使用jwt中间件!")
12 | return nil
13 | } else {
14 | waitUse := claims.(*TokenClaims)
15 | return waitUse
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/utils/id_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestUuid(t *testing.T) {
8 | if uuid := Uuid(); uuid == "" {
9 | t.Errorf("uuid generate result is null")
10 | } else {
11 | t.Logf("uuid generate result: %s", uuid)
12 | }
13 | }
14 |
15 | func TestId(t *testing.T) {
16 | if id := Id(); id == 0 {
17 | t.Errorf("snowflake generate result is null")
18 | } else {
19 | t.Logf("snowflake id generate result: %d", id)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/global/model.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | import (
4 | "aixinge/utils/snowflake"
5 | "gorm.io/gorm"
6 | "time"
7 | )
8 |
9 | type MODEL struct {
10 | ID snowflake.ID `json:"id,omitempty" swaggertype:"string"` // 主键ID
11 | CreatedAt time.Time `json:"createdAt,omitempty"` // 创建时间
12 | UpdatedAt time.Time `json:"updatedAt,omitempty"` // 更新时间
13 | DeletedAt gorm.DeletedAt `json:"-"` // 删除时间
14 | }
15 |
--------------------------------------------------------------------------------
/api/router/message/channelTemplate.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | v1 "aixinge/api/v1"
5 | "github.com/gofiber/fiber/v2"
6 | )
7 |
8 | type ChannelTemplateRouter struct {
9 | }
10 |
11 | func (c *ChannelRouter) InitChannelTemplateRouter(router fiber.Router) {
12 | ctRouter := router.Group("channel-template")
13 | var ctApi = v1.AppApi.MessageApi.ChannelTemplate
14 | {
15 | ctRouter.Post("create", ctApi.Create) // 创建
16 | ctRouter.Post("delete", ctApi.Delete) // 删除
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/config/system.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | type System struct {
4 | Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口
5 | Node int64 `mapstructure:"node" json:"node" yaml:"node"` // 节点
6 | DbType string `mapstructure:"db-type" json:"dbType" yaml:"db-type"` // 数据库类型:mysql(默认)|sqlite|sqlserver|postgresql
7 | ContextPath string `mapstructure:"context-path" json:"contextPath" yaml:"context-path"` // 请求上下文路径
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/api/model/message/channelTemplate.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "aixinge/utils/snowflake"
5 | )
6 |
7 | type ChannelTemplate struct {
8 | ChannelId snowflake.ID `json:"channelId,omitempty" swaggertype:"string"` // 渠道ID
9 | TemplateId snowflake.ID `json:"templateId,omitempty" swaggertype:"string"` // 模板ID
10 | Type MsgType `json:"type"` // 消息类型(枚举)
11 | Default int `json:"default"` // 默认渠道 1、是 2、否
12 | }
13 |
--------------------------------------------------------------------------------
/api/service/message/channelTemplate.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/message"
6 | "aixinge/global"
7 | )
8 |
9 | type ChannelTemplateService struct {
10 | }
11 |
12 | func (c *ChannelTemplateService) Create(ct message.ChannelTemplate) error {
13 | return global.DB.Create(&ct).Error
14 | }
15 |
16 | func (c *ChannelTemplateService) Delete(idsReq request.IdsReq) error {
17 | return global.DB.Delete(&[]message.Channel{}, "id in ?", idsReq.Ids).Error
18 | }
19 |
--------------------------------------------------------------------------------
/utils/id.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "aixinge/global"
5 | "aixinge/utils/snowflake"
6 | uuid "github.com/satori/go.uuid"
7 | "sync"
8 | )
9 |
10 | func Uuid() string {
11 | u := uuid.NewV4()
12 | return u.String()
13 | }
14 |
15 | var sfn *snowflake.Node
16 | var once sync.Once
17 |
18 | func Id() snowflake.ID {
19 | once.Do(func() {
20 | var err error
21 | sfn, err = snowflake.NewNode(global.CONFIG.System.Node)
22 | if err != nil {
23 | panic(err)
24 | }
25 | })
26 | return sfn.Generate()
27 | }
28 |
--------------------------------------------------------------------------------
/core/sms/aliyun_test.go:
--------------------------------------------------------------------------------
1 | package sms
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestSendSms(t *testing.T) {
9 | accessKeyId := "accessKeyId"
10 | accessKeySecret := "accessKeySecret"
11 | phoneNumbers := []string{
12 | "电话号码",
13 | }
14 | signName := "短信签名"
15 | templateCode := "短信模板"
16 | templateParam := "{\"code\":\"6683\"}" // 短信参数
17 |
18 | client := CreateClient(accessKeyId, accessKeySecret)
19 | response := client.SendSms(phoneNumbers, signName, templateCode, templateParam)
20 | fmt.Printf("%+v\n", response)
21 | }
22 |
--------------------------------------------------------------------------------
/api/model/system/file.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/utils/snowflake"
5 | "time"
6 | )
7 |
8 | type File struct {
9 | ID snowflake.ID `json:"id,omitempty" gorm:"primarykey" swaggertype:"string"`
10 | CreatedAt time.Time `json:"createdAt,omitempty"`
11 | Md5 string `json:"md5"`
12 | Path string `json:"path"`
13 | Ext string `json:"ext"`
14 | ContentType string `json:"contentType"`
15 | Size int64 `json:"size"`
16 | Filename string `json:"filename"`
17 | }
18 |
--------------------------------------------------------------------------------
/api/model/system/request/role.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import "aixinge/utils/snowflake"
4 |
5 | // RoleMenuParams 角色分配菜单参数对象
6 | type RoleMenuParams struct {
7 | ID snowflake.ID `json:"id,omitempty" swaggertype:"string"` // 角色ID
8 | MenuIds []snowflake.ID `json:"menuIds" swaggertype:"array,string"` // 菜单ID集合
9 | }
10 |
11 | // RoleUserParams 角色分配用户参数对象
12 | type RoleUserParams struct {
13 | ID snowflake.ID `json:"id,omitempty" swaggertype:"string"` // 角色ID
14 | UserIds []snowflake.ID `json:"userIds" swaggertype:"array,string"` // 用户ID集合
15 | }
16 |
--------------------------------------------------------------------------------
/api/model/system/role.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/global"
5 | "aixinge/utils/snowflake"
6 | )
7 |
8 | type Role struct {
9 | global.MODEL
10 | Name string `json:"name"` // 名称
11 | Alias string `json:"alias"` // 别名
12 | Remark string `json:"remark"` // 备注
13 | Status int `json:"status"` // 状态,1、正常 2、禁用
14 | Sort int `json:"sort"` // 排序
15 | }
16 |
17 | type RoleMenu struct {
18 | RoleId snowflake.ID `json:"roleId" swaggertype:"string"` // 角色ID
19 | MenuId snowflake.ID `json:"menuId" swaggertype:"string"` // 菜单ID
20 | }
21 |
--------------------------------------------------------------------------------
/core/cron/cron_test.go:
--------------------------------------------------------------------------------
1 | package cron
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 | )
8 | import "github.com/go-co-op/gocron"
9 |
10 | func TestCron(t *testing.T) {
11 | // https://github.com/go-co-op/gocron
12 | s := gocron.NewScheduler(time.UTC)
13 | s.Every(5).Seconds().Do(func() {
14 | fmt.Println("----cron-----")
15 | })
16 | // you can start running the scheduler in two different ways:
17 | // starts the scheduler asynchronously
18 | s.StartAsync()
19 | // starts the scheduler and blocks current execution path
20 | s.StartBlocking()
21 | }
22 |
--------------------------------------------------------------------------------
/api/model/message/common.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | // MsgType 消息类型
4 | type MsgType int
5 |
6 | const (
7 | Sms MsgType = 1 // 短信
8 | Mail MsgType = 2 // 邮件
9 | Inbox MsgType = 3 // 站内信
10 | WebSocket MsgType = 4 // WebSocket
11 | MiniPrograms MsgType = 5 // 小程序
12 | )
13 |
14 | // ChannelProvider 消息服务商
15 | type ChannelProvider int
16 |
17 | const (
18 | AliCloud ChannelProvider = 1 // 阿里云
19 | TencentCloud ChannelProvider = 2 // 腾讯云
20 | BaiduCloud ChannelProvider = 3 // 百度云
21 | HuaweiCloud ChannelProvider = 4 // 华为云
22 | )
23 |
--------------------------------------------------------------------------------
/api/model/system/response/menu.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import "aixinge/api/model/system"
4 |
5 | type MenuResponse struct {
6 | Menu system.Menu `json:"menu"`
7 | }
8 |
9 | type MenuTreeResponse struct {
10 | system.Menu
11 | Children []*MenuTreeResponse `json:"children,omitempty"` // 子类
12 | }
13 |
14 | type SysMenusResponse struct {
15 | Menus []system.Menu `json:"menus"`
16 | }
17 |
18 | type SysBaseMenusResponse struct {
19 | Menus []system.BaseMenu `json:"menus"`
20 | }
21 |
22 | type SysBaseMenuResponse struct {
23 | Menu system.BaseMenu `json:"menu"`
24 | }
25 |
--------------------------------------------------------------------------------
/middleware/cors.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "github.com/gofiber/fiber/v2"
5 | "github.com/gofiber/fiber/v2/middleware/cors"
6 | )
7 |
8 | // Cors 处理跨域请求,支持options访问
9 | func Cors() fiber.Handler {
10 | return cors.New(cors.Config{
11 | AllowMethods: "POST,GET,OPTIONS,DELETE,PUT",
12 | AllowHeaders: "Content-Type,AccessToken,X-CSRF-Token,Authorization,Token,X-Token,X-User-Id",
13 | AllowCredentials: true,
14 | ExposeHeaders: "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type",
15 | MaxAge: 0,
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/api/v1/message/enter.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import "aixinge/api/service"
4 |
5 | type ApiGroup struct {
6 | Application
7 | Channel
8 | ChannelTemplate
9 | MailLog
10 | MailTemplate
11 | }
12 |
13 | var applicationService = service.AppService.MessageService.ApplicationService
14 | var channelService = service.AppService.MessageService.ChannelService
15 | var channelTemplateService = service.AppService.MessageService.ChannelTemplateService
16 | var mailLogService = service.AppService.MessageService.MailLogService
17 | var mailTemplateService = service.AppService.MessageService.MailTemplateService
18 |
--------------------------------------------------------------------------------
/api/model/common/attachment.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "aixinge/utils/snowflake"
5 | "database/sql/driver"
6 | "encoding/json"
7 | )
8 |
9 | type Attachments []Attachment
10 |
11 | func (a Attachments) Value() (driver.Value, error) {
12 | b, err := json.Marshal(a)
13 | return string(b), err
14 | }
15 |
16 | func (a *Attachments) Scan(src any) error {
17 | return json.Unmarshal(src.([]byte), a)
18 | }
19 |
20 | type Attachment struct {
21 | FileId snowflake.ID `json:"fileId" swaggertype:"string"` // 文件 ID
22 | FileName string `json:"fileName"` // 文件名称
23 | }
24 |
--------------------------------------------------------------------------------
/api/router/message/mailTemplate.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | v1 "aixinge/api/v1"
5 | "github.com/gofiber/fiber/v2"
6 | )
7 |
8 | type MailTemplateRouter struct {
9 | }
10 |
11 | func (m *MailTemplateRouter) InitMailTemplateRouter(router fiber.Router) {
12 | mtRouter := router.Group("mail-template")
13 | var mtApi = v1.AppApi.MessageApi.MailTemplate
14 | {
15 | mtRouter.Post("create", mtApi.Create) // 创建
16 | mtRouter.Post("delete", mtApi.Delete) // 删除
17 | mtRouter.Post("update", mtApi.Update) // 更新
18 | mtRouter.Post("get", mtApi.Get) // 根据id获取邮件模板
19 | mtRouter.Post("page", mtApi.Page) // 分页
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/api/router/message/application.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | v1 "aixinge/api/v1"
5 | "github.com/gofiber/fiber/v2"
6 | )
7 |
8 | type ApplicationRouter struct {
9 | }
10 |
11 | func (a *ApplicationRouter) InitApplicationRouter(router fiber.Router) {
12 | appRouter := router.Group("app")
13 | var appApi = v1.AppApi.MessageApi.Application
14 | {
15 | appRouter.Post("create", appApi.Create) // 创建
16 | appRouter.Post("delete", appApi.Delete) // 删除应用
17 | appRouter.Post("update", appApi.Update) // 更新应用信息
18 | appRouter.Post("get", appApi.Update) // 根据id获取应用
19 | appRouter.Post("page", appApi.Page) // 分页
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | # documentation https://goreleaser.com
2 | before:
3 | hooks:
4 | # - go mod tidy
5 | # - go generate ./...
6 | project_name: aixinge
7 | builds:
8 | - env:
9 | - CGO_ENABLED=0
10 | goos:
11 | - linux
12 | - windows
13 | - darwin
14 | archives:
15 | - replacements:
16 | darwin: Darwin
17 | linux: Linux
18 | windows: Windows
19 | 386: i386
20 | amd64: x86_64
21 | checksum:
22 | name_template: 'checksums.txt'
23 | snapshot:
24 | name_template: "{{ incpatch .Version }}-next"
25 | changelog:
26 | sort: asc
27 | filters:
28 | exclude:
29 | - '^docs:'
30 | - '^test:'
31 |
--------------------------------------------------------------------------------
/api/router/message/channel.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | v1 "aixinge/api/v1"
5 | "github.com/gofiber/fiber/v2"
6 | )
7 |
8 | type ChannelRouter struct {
9 | }
10 |
11 | func (c *ChannelRouter) InitChannelRouter(router fiber.Router) {
12 | channelRouter := router.Group("channel")
13 | var channelApi = v1.AppApi.MessageApi.Channel
14 | {
15 | channelRouter.Post("create", channelApi.Create) // 创建
16 | channelRouter.Post("delete", channelApi.Delete) // 删除
17 | channelRouter.Post("update", channelApi.Update) // 更新
18 | channelRouter.Post("get", channelApi.Get) // 根据id获取邮件模板
19 | channelRouter.Post("page", channelApi.Page) // 分页
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/utils/rotatelogs_windows.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "aixinge/global"
5 | zapRotateLogs "github.com/lestrrat-go/file-rotatelogs"
6 | "go.uber.org/zap/zapcore"
7 | "os"
8 | "path"
9 | "time"
10 | )
11 |
12 | func GetWriteSyncer() (zapcore.WriteSyncer, error) {
13 | fileWriter, err := zapRotateLogs.New(
14 | path.Join(global.CONFIG.Zap.Director, "%Y-%m-%d.log"),
15 | zapRotateLogs.WithMaxAge(7*24*time.Hour),
16 | zapRotateLogs.WithRotationTime(24*time.Hour),
17 | )
18 | if global.CONFIG.Zap.LogInConsole {
19 | return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(fileWriter)), err
20 | }
21 | return zapcore.AddSync(fileWriter), err
22 | }
23 |
--------------------------------------------------------------------------------
/api/model/system/user.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/global"
5 | "aixinge/utils/snowflake"
6 |
7 | uuid "github.com/satori/go.uuid"
8 | )
9 |
10 | type User struct {
11 | global.MODEL
12 | UUID uuid.UUID `json:"uuid"` // 用户UUID
13 | Username string `json:"username"` // 用户登录名
14 | Password string `json:"-"` // 用户登录密码
15 | Nickname string `json:"nickname"` // 用户昵称
16 | Avatar string `json:"avatar"` // 用户头像˚
17 | Status int `json:"status"` // 状态,1、正常 2、禁用
18 | }
19 |
20 | type UserRole struct {
21 | UserId snowflake.ID `json:"userId" swaggertype:"string"` // 用户ID
22 | RoleId snowflake.ID `json:"roleId" swaggertype:"string"` // 角色ID
23 | }
24 |
--------------------------------------------------------------------------------
/config-sample.yaml:
--------------------------------------------------------------------------------
1 | system:
2 | port: 8888
3 | node: 0
4 | db-type: mysql
5 | context-path: /
6 | database:
7 | path: 192.168.0.1:3306
8 | config: charset=utf8mb4&parseTime=True&loc=Local
9 | db-name: axg
10 | username: root
11 | password: "axg123456"
12 | max-idle-conns: 10
13 | max-open-conns: 100
14 | log-mode: "error"
15 | log-zap: false
16 | jwt:
17 | signing-key: jCyJhbGciOi7ruWCOt29eJIUzI
18 | expires-time: 30
19 | upload:
20 | path: ./upload
21 | zap:
22 | level: info
23 | format: console
24 | prefix: '[axg]'
25 | director: log
26 | link-name: latest_log
27 | showLine: false
28 | encode-level: LowercaseColorLevelEncoder
29 | stacktrace-key: stacktrace
30 | log-in-console: true
31 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "aixinge/core"
5 | "aixinge/global"
6 | "aixinge/initialize"
7 | "aixinge/utils"
8 | )
9 |
10 | //go:generate go env -w GO111MODULE=on
11 | //go:generate go env -w GOPROXY=https://goproxy.cn,direct
12 | //go:generate go mod tidy
13 | //go:generate go mod download
14 |
15 | // @title AiXinGe API
16 | // @version 1.0.0
17 | // @description artificial intelligence message push service
18 | // @securityDefinitions.apikey ApiKeyAuth
19 | // @in header
20 | // @name x-token
21 | // @BasePath /
22 | func main() {
23 | global.VP = core.Viper() // 初始化Viper
24 | global.LOG = core.Zap() // 初始化zap日志库
25 | global.DB = initialize.Gorm() // gorm连接数据库
26 | utils.Open() // 打开首页
27 | core.RunServer()
28 | }
29 |
--------------------------------------------------------------------------------
/utils/directory.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "aixinge/global"
5 | "go.uber.org/zap"
6 | "os"
7 | )
8 |
9 | func PathExists(path string) (bool, error) {
10 | _, err := os.Stat(path)
11 | if err == nil {
12 | return true, nil
13 | }
14 | if os.IsNotExist(err) {
15 | return false, nil
16 | }
17 | return false, err
18 | }
19 |
20 | func CreateDir(dirs ...string) (err error) {
21 | for _, v := range dirs {
22 | exist, err := PathExists(v)
23 | if err != nil {
24 | return err
25 | }
26 | if !exist {
27 | global.LOG.Debug("create directory" + v)
28 | if err := os.MkdirAll(v, os.ModePerm); err != nil {
29 | global.LOG.Error("create directory"+v, zap.Any(" error:", err))
30 | return err
31 | }
32 | }
33 | }
34 | return err
35 | }
36 |
--------------------------------------------------------------------------------
/utils/rotatelogs_unix.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package utils
5 |
6 | import (
7 | "aixinge/global"
8 | zapRotateLogs "github.com/lestrrat-go/file-rotatelogs"
9 | "go.uber.org/zap/zapcore"
10 | "os"
11 | "path"
12 | "time"
13 | )
14 |
15 | func GetWriteSyncer() (zapcore.WriteSyncer, error) {
16 | fileWriter, err := zapRotateLogs.New(
17 | path.Join(global.CONFIG.Zap.Director, "%Y-%m-%d.log"),
18 | zapRotateLogs.WithLinkName(global.CONFIG.Zap.LinkName),
19 | zapRotateLogs.WithMaxAge(7*24*time.Hour),
20 | zapRotateLogs.WithRotationTime(24*time.Hour),
21 | )
22 | if global.CONFIG.Zap.LogInConsole {
23 | return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(fileWriter)), err
24 | }
25 | return zapcore.AddSync(fileWriter), err
26 | }
27 |
--------------------------------------------------------------------------------
/api/router/system/menu.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/api/v1"
5 | "github.com/gofiber/fiber/v2"
6 | )
7 |
8 | type MenuRouter struct {
9 | }
10 |
11 | func (s *MenuRouter) InitMenuRouter(router fiber.Router) {
12 | menuRouter := router.Group("menu")
13 | var menuApi = v1.AppApi.SystemApi.Menu
14 | {
15 | menuRouter.Post("create", menuApi.Create) // 创建
16 | menuRouter.Post("delete", menuApi.Delete) // 删除
17 | menuRouter.Post("update", menuApi.Update) // 更新
18 | menuRouter.Post("get", menuApi.Get) // 根据id获取
19 | menuRouter.Post("page", menuApi.Page) // 分页获取列表
20 | menuRouter.Post("auth", menuApi.Auth) // 授权菜单
21 | menuRouter.Post("list", menuApi.List) // 列表
22 | menuRouter.Post("list-tree", menuApi.ListTree) // 列表树
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/middleware/rbac.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "aixinge/api/model/common/response"
5 | "aixinge/api/model/system/request"
6 | "github.com/gofiber/fiber/v2"
7 | )
8 |
9 | func RbacHandler() fiber.Handler {
10 | // rbac 权限处理
11 | return func(c *fiber.Ctx) error {
12 | var success = false
13 | claims := c.Locals("claims")
14 | waitUse := claims.(*request.TokenClaims)
15 | uid := waitUse.ID
16 | if uid == 1 {
17 | // 管理员
18 | success = true
19 | } else {
20 | // 获取请求的URI
21 | url := c.OriginalURL()
22 | // 获取请求方法
23 | method := c.Method()
24 | // 未来要做 RBAC 权限认证
25 | print("获取请求的URI = " + url + ", method= " + method)
26 | }
27 | if success {
28 | return c.Next()
29 | } else {
30 | return response.FailWithDetailed(fiber.Map{}, "权限不足", c)
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/utils/md5.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/hex"
6 | "io"
7 | "log"
8 | "os"
9 | "path/filepath"
10 | )
11 |
12 | func GetFileMd5(filename string) string {
13 | path, err := filepath.Abs(filename)
14 | if err != nil {
15 | panic("Convert file absolute path error: " + path)
16 | }
17 |
18 | f, err := os.Open(filename)
19 | if err != nil {
20 | log.Fatal(err)
21 | }
22 | defer f.Close()
23 |
24 | h := md5.New()
25 | if _, err := io.Copy(h, f); err != nil {
26 | log.Fatal(err)
27 | }
28 |
29 | return hex.EncodeToString(h.Sum(nil))
30 | }
31 |
32 | func GetStringMd5(s string) string {
33 | return GetByteMd5([]byte(s))
34 | }
35 |
36 | func GetByteMd5(s []byte) string {
37 | md5 := md5.New()
38 | md5.Write(s)
39 | return hex.EncodeToString(md5.Sum(nil))
40 | }
41 |
--------------------------------------------------------------------------------
/core/mail/smtp_test.go:
--------------------------------------------------------------------------------
1 | package mail
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestUuid(t *testing.T) {
9 | user := "发信地址"
10 | password := "SMTP密码"
11 | host := "mail.qq.com:587"
12 | to := []string{"收件人地址", "收件人地址1"}
13 | cc := []string{"抄送地址", "抄送地址1"}
14 | bcc := []string{"密送地址", "密送地址1"}
15 |
16 | subject := "AiXinGe Smtp Send Test"
17 | replyToAddress := "回信地址"
18 |
19 | body := `
20 |
21 |
22 |
23 | "AiXinGe SMTP Send Test Successful!"
24 |
25 |
26 |
27 | `
28 | fmt.Println("send email")
29 | s := NewSmtp("", user, password, host)
30 | err := s.SendMail(true, subject, body, replyToAddress, to, cc, bcc)
31 | if err != nil {
32 | fmt.Println("Send mail error!", err)
33 | } else {
34 | fmt.Println("Send mail success!")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/utils/helper/aliyun_test.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "testing"
7 | )
8 |
9 | func TestAliyunApiUtil(t *testing.T) {
10 | parameters := InitCommonRequestParameters("123", "SendSms", "2017-05-25")
11 | out, _ := json.Marshal(parameters)
12 | fmt.Println("Parameters:")
13 | fmt.Println(string(out))
14 | fmt.Println()
15 |
16 | urlParams := BuildUrlParams(parameters)
17 | fmt.Println("StandardRequestStr:")
18 | fmt.Println(urlParams)
19 | fmt.Println()
20 |
21 | encodeUrlParams := urlParams.Encode()
22 | percent := PercentEncode(encodeUrlParams)
23 | signStr := BuildSignStr("GET", percent)
24 | fmt.Println("SignStr:")
25 | fmt.Println(signStr)
26 | fmt.Println()
27 |
28 | signature := BuildSignature("test", signStr)
29 | fmt.Println("Signature:")
30 | fmt.Println(signature)
31 | fmt.Println()
32 | }
33 |
--------------------------------------------------------------------------------
/api/model/message/mailTemplate.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "aixinge/api/model/common"
5 | "aixinge/global"
6 | "aixinge/utils/snowflake"
7 | )
8 |
9 | type MailTemplate struct {
10 | global.MODEL
11 | AppId snowflake.ID `json:"appId,omitempty" swaggertype:"string"` // 应用 ID
12 | Name string `json:"name"` // 模板名称
13 | Content string `json:"content"` // 模板内容
14 | Type int `json:"type"` // 模板类型(1-文本、2-HTML)
15 | Attachments common.Attachments `json:"attachments" gorm:"type:json"` // 附件JSON
16 | Remark string `json:"remark"` // 备注
17 | Status int `json:"status"` // 状态 1、正常 2、禁用
18 | }
19 |
--------------------------------------------------------------------------------
/api/service/message/mailLog.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/message"
6 | "aixinge/global"
7 | "aixinge/utils"
8 | )
9 |
10 | type MailLogService struct {
11 | }
12 |
13 | func (m *MailLogService) Create(ml message.MailLog) error {
14 | ml.ID = utils.Id()
15 | return global.DB.Create(&ml).Error
16 | }
17 |
18 | func (m *MailLogService) Delete(idsReq request.IdsReq) error {
19 | return global.DB.Delete(&[]message.MailLog{}, "id in ?", idsReq.Ids).Error
20 | }
21 |
22 | func (m *MailLogService) Page(page request.PageInfo) (err error, list interface{}, total int64) {
23 | db := global.DB.Model(&message.MailLog{})
24 | var mlList []message.MailLog
25 | err = db.Count(&total).Error
26 | if total > 0 {
27 | err = db.Limit(page.PageSize).Offset(page.Offset()).Find(&mlList).Error
28 | }
29 | return err, mlList, total
30 | }
31 |
--------------------------------------------------------------------------------
/api/model/system/request/menu.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/system"
6 | "aixinge/global"
7 | )
8 |
9 | // AddMenuAuthorityInfo Add menu authority info structure
10 | type AddMenuAuthorityInfo struct {
11 | Menus []system.BaseMenu
12 | AuthorityId string // 角色ID
13 | }
14 |
15 | func DefaultMenu() []system.BaseMenu {
16 | return []system.BaseMenu{{
17 | MODEL: global.MODEL{ID: 1},
18 | ParentId: 1,
19 | Path: "dashboard",
20 | Name: "dashboard",
21 | Component: "view/dashboard/index.vue",
22 | Sort: 1,
23 | Meta: system.Meta{
24 | Title: "仪表盘",
25 | Icon: "setting",
26 | },
27 | }}
28 | }
29 |
30 | type MenuParams struct {
31 | Name string `json:"name"` // 菜单名称
32 | }
33 |
34 | type MenuPageParams struct {
35 | request.PageInfo
36 | Title string `json:"title"`
37 | Status int `json:"status,string,int"`
38 | }
39 |
--------------------------------------------------------------------------------
/core/server.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "aixinge/global"
5 | "aixinge/initialize"
6 | "fmt"
7 | "github.com/gofiber/fiber/v2"
8 | "log"
9 | "os"
10 | "os/signal"
11 | "syscall"
12 | "time"
13 | )
14 |
15 | type Server interface {
16 | ServeAsync(string, *fiber.App) error
17 | }
18 |
19 | func RunServer() {
20 | // init routers
21 | app := initialize.Routers()
22 | address := fmt.Sprintf(":%d", global.CONFIG.System.Port)
23 |
24 | // kill daemon exit
25 | quit := make(chan os.Signal, 1)
26 | signal.Notify(quit, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
27 | go func() {
28 | <-quit
29 | fmt.Println("Shutdown Server ...")
30 | if err := app.Shutdown(); err != nil {
31 | fmt.Println(err)
32 | log.Fatalf("Server Shutdown: %s", err)
33 | }
34 | fmt.Println("Server exit")
35 | }()
36 |
37 | // start app
38 | time.Sleep(10 * time.Microsecond)
39 | global.LOG.Error(app.Listen(address).Error())
40 | }
41 |
--------------------------------------------------------------------------------
/api/service/system/file.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/api/model/system"
5 | "aixinge/global"
6 | "aixinge/utils"
7 | "time"
8 | )
9 |
10 | type FileService struct {
11 | }
12 |
13 | func (f *FileService) GetById(id int64) (err error, file system.File) {
14 | err = global.DB.First(&file, "id = ?", id).Error
15 | return err, file
16 | }
17 |
18 | func (f *FileService) GetByMd5(md5 string) (err error, file system.File) {
19 | err = global.DB.First(&file, "md5 = ?", md5).Error
20 | return err, file
21 | }
22 |
23 | func (f *FileService) Save(md5, path, ext, contentType, filename string, size int64) (error, system.File) {
24 | if len(filename) > 255 {
25 | filename = filename[0:253]
26 | }
27 | file := system.File{ID: utils.Id(), CreatedAt: time.Now(), Md5: md5, Path: path, Ext: ext, Filename: filename,
28 | ContentType: contentType, Size: size}
29 | err := global.DB.Create(&file).Error
30 | // 不显示存储路径
31 | file.Path = ""
32 | return err, file
33 | }
34 |
--------------------------------------------------------------------------------
/api/model/message/channel.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "aixinge/global"
5 | "database/sql/driver"
6 | "encoding/json"
7 | )
8 |
9 | type Channel struct {
10 | global.MODEL
11 | Name string `json:"name"` // 消息渠道名称
12 | Type MsgType `json:"type"` // 消息类型(枚举)
13 | Provider ChannelProvider `json:"provider"` // 消息服务提供商
14 | Weight int `json:"weight"` // 权重
15 | Config ChannelConfig `json:"config" gorm:"type:json"` // 消息渠道配置(JSON)
16 | Remark string `json:"remark"` // 备注
17 | Status int `json:"status"` // 状态 1、正常 2、禁用
18 | }
19 |
20 | type ChannelConfig map[string]interface{}
21 |
22 | func (c ChannelConfig) Value() (driver.Value, error) {
23 | b, err := json.Marshal(c)
24 | return string(b), err
25 | }
26 |
27 | func (c *ChannelConfig) Scan(src any) error {
28 | return json.Unmarshal(src.([]byte), c)
29 | }
30 |
--------------------------------------------------------------------------------
/api/router/system/user.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/api/v1"
5 | "github.com/gofiber/fiber/v2"
6 | )
7 |
8 | type UserRouter struct {
9 | }
10 |
11 | func (s *UserRouter) InitUserRouter(router fiber.Router) {
12 | userRouter := router.Group("user")
13 | var userApi = v1.AppApi.SystemApi.User
14 | {
15 | userRouter.Post("create", userApi.Create) // 创建
16 | userRouter.Post("delete", userApi.Delete) // 删除
17 | userRouter.Post("update", userApi.Update) // 更新
18 | userRouter.Post("change-password", userApi.ChangePassword) // 修改密码
19 | userRouter.Post("assign-role", userApi.AssignRole) // 用户分配角色
20 | userRouter.Post("selected-roles", userApi.SelectedRoles) // 用户已分配角色ID列表
21 | userRouter.Post("get", userApi.Get) // 根据id获取用户
22 | userRouter.Post("page", userApi.Page) // 分页获取用户列表
23 | userRouter.Post("list", userApi.List) // 获取用户列表
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/config/zap.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | type Zap struct {
4 | Level string `mapstructure:"level" json:"level" yaml:"level"` // 级别
5 | Format string `mapstructure:"format" json:"format" yaml:"format"` // 输出
6 | Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 日志前缀
7 | Director string `mapstructure:"director" json:"director" yaml:"director"` // 日志文件夹
8 | LinkName string `mapstructure:"link-name" json:"linkName" yaml:"link-name"` // 软链接名称
9 | ShowLine bool `mapstructure:"show-line" json:"showLine" yaml:"showLine"` // 显示行
10 | EncodeLevel string `mapstructure:"encode-level" json:"encodeLevel" yaml:"encode-level"` // 编码级
11 | StacktraceKey string `mapstructure:"stacktrace-key" json:"stacktraceKey" yaml:"stacktrace-key"` // 栈名
12 | LogInConsole bool `mapstructure:"log-in-console" json:"logInConsole" yaml:"log-in-console"` // 输出控制台
13 | }
14 |
--------------------------------------------------------------------------------
/config/gorm.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | type Database struct {
4 | Path string `mapstructure:"path" json:"path" yaml:"path"` // 服务器地址:端口
5 | Config string `mapstructure:"config" json:"config" yaml:"config"` // 高级配置
6 | Dbname string `mapstructure:"db-name" json:"dbname" yaml:"db-name"` // 数据库名
7 | Username string `mapstructure:"username" json:"username" yaml:"username"` // 数据库用户名
8 | Password string `mapstructure:"password" json:"password" yaml:"password"` // 数据库密码
9 | MaxIdleConns int `mapstructure:"max-idle-conns" json:"maxIdleConns" yaml:"max-idle-conns"` // 空闲中的最大连接数
10 | MaxOpenConns int `mapstructure:"max-open-conns" json:"maxOpenConns" yaml:"max-open-conns"` // 打开到数据库的最大连接数
11 | LogMode string `mapstructure:"log-mode" json:"logMode" yaml:"log-mode"` // 是否开启Gorm全局日志
12 | LogZap bool `mapstructure:"log-zap" json:"logZap" yaml:"log-zap"` // 是否通过zap写入日志文件
13 | }
14 |
--------------------------------------------------------------------------------
/utils/ip.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "errors"
5 | "net"
6 | )
7 |
8 | func ExternalIP() (net.IP, error) {
9 | faces, err := net.Interfaces()
10 | if err != nil {
11 | return nil, err
12 | }
13 | for _, face := range faces {
14 | if face.Flags&net.FlagUp == 0 {
15 | continue
16 | }
17 | if face.Flags&net.FlagLoopback != 0 {
18 | continue
19 | }
20 | address, err := face.Addrs()
21 | if err != nil {
22 | return nil, err
23 | }
24 | for _, addr := range address {
25 | ip := getIpFromAddr(addr)
26 | if ip == nil {
27 | continue
28 | }
29 | return ip, nil
30 | }
31 | }
32 | return nil, errors.New("connected to the network?")
33 | }
34 |
35 | func getIpFromAddr(addr net.Addr) net.IP {
36 | var ip net.IP
37 | switch v := addr.(type) {
38 | case *net.IPNet:
39 | ip = v.IP
40 | case *net.IPAddr:
41 | ip = v.IP
42 | }
43 | if ip == nil || ip.IsLoopback() {
44 | return nil
45 | }
46 | ip = ip.To4()
47 | if ip == nil {
48 | // not an ipv4 address
49 | return nil
50 | }
51 | return ip
52 | }
53 |
--------------------------------------------------------------------------------
/api/model/common/request/common.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import "aixinge/utils/snowflake"
4 |
5 | // PageInfo Paging common input parameter structure
6 | type PageInfo struct {
7 | Page int `json:"page" form:"page"` // 页码
8 | PageSize int `json:"pageSize" form:"pageSize"` // 每页大小
9 | }
10 |
11 | func (p PageInfo) Offset() int {
12 | return p.PageSize * (p.Page - 1)
13 | }
14 |
15 | // GetById Find by id structure
16 | type GetById struct {
17 | ID snowflake.ID `json:"id" form:"id" swaggertype:"string"` // 主键ID
18 | }
19 |
20 | type IdsReq struct {
21 | Ids []snowflake.ID `json:"ids" form:"ids" swaggertype:"array,string"` //ID数组
22 | }
23 |
24 | type IdsRemarkReq struct {
25 | Ids []snowflake.ID `json:"ids" form:"ids" swaggertype:"array,string"` //ID数组
26 | Remark string `json:"remark" form:"remark"` //备注
27 | }
28 |
29 | // IdRelIdsReq 一对多关联
30 | type IdRelIdsReq struct {
31 | RelIds []snowflake.ID `json:"relIds" form:"relIds" swaggertype:"array,string"` //关联ID数组
32 | ID snowflake.ID `json:"id" form:"id" swaggertype:"string"` // 主键ID
33 | }
34 |
35 | type Empty struct{}
36 |
--------------------------------------------------------------------------------
/api/model/system/request/user.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/utils/snowflake"
6 | )
7 |
8 | type UserCreate struct {
9 | Username string `json:"username"` // 用户登录名
10 | Password string `json:"password"` // 用户登录密码
11 | Nickname string `json:"nickname"` // 用户昵称
12 | }
13 |
14 | type Login struct {
15 | Username string `json:"username"` // 用户名
16 | Password string `json:"password"` // 密码
17 | }
18 |
19 | type RefreshToken struct {
20 | RefreshToken string `json:"refreshToken"` // 刷新票据
21 | }
22 |
23 | type ChangePasswordStruct struct {
24 | Username string `json:"username"` // 用户名
25 | Password string `json:"password"` // 密码
26 | NewPassword string `json:"newPassword"` // 新密码
27 | }
28 |
29 | // UserRoleParams 用户分配角色参数对象
30 | type UserRoleParams struct {
31 | ID snowflake.ID `json:"id,omitempty" swaggertype:"string"` // 用户ID
32 | RoleIds []snowflake.ID `json:"roleIds" swaggertype:"array,string"` // 角色ID集合
33 | }
34 |
35 | type UserPageParams struct {
36 | request.PageInfo
37 | Username string `json:"username"`
38 | Status int `json:"status,string,int"`
39 | }
40 |
--------------------------------------------------------------------------------
/api/model/common/localTime.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "database/sql/driver"
5 | "time"
6 | )
7 |
8 | const TimeFormat = "2006-01-02 15:04:05"
9 |
10 | type LocalTime time.Time
11 |
12 | func (t *LocalTime) UnmarshalJSON(data []byte) (err error) {
13 | if len(data) == 2 {
14 | *t = LocalTime(time.Time{})
15 | return
16 | }
17 |
18 | now, err := time.Parse(`"`+TimeFormat+`"`, string(data))
19 | *t = LocalTime(now)
20 | return
21 | }
22 |
23 | func (t LocalTime) MarshalJSON() ([]byte, error) {
24 | b := make([]byte, 0, len(TimeFormat)+2)
25 | b = append(b, '"')
26 | b = time.Time(t).AppendFormat(b, TimeFormat)
27 | b = append(b, '"')
28 | return b, nil
29 | }
30 |
31 | func (t LocalTime) Value() (driver.Value, error) {
32 | if t.String() == "0001-01-01 00:00:00" {
33 | return nil, nil
34 | }
35 | return []byte(time.Time(t).Format(TimeFormat)), nil
36 | }
37 |
38 | func (t *LocalTime) Scan(v interface{}) error {
39 | tTime, _ := time.Parse("2006-01-02 15:04:05 +0800 CST", v.(time.Time).String())
40 | *t = LocalTime(tTime)
41 | return nil
42 | }
43 |
44 | func (t LocalTime) String() string {
45 | return time.Time(t).Format(TimeFormat)
46 | }
47 |
--------------------------------------------------------------------------------
/api/service/message/channel.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/message"
6 | "aixinge/global"
7 | "aixinge/utils"
8 | "aixinge/utils/snowflake"
9 | )
10 |
11 | type ChannelService struct {
12 | }
13 |
14 | func (c *ChannelService) Create(channel message.Channel) error {
15 | channel.ID = utils.Id()
16 | // 状态,1、正常 2、禁用
17 | channel.Status = 1
18 | return global.DB.Create(&channel).Error
19 | }
20 |
21 | func (c *ChannelService) Delete(idsReq request.IdsReq) error {
22 | return global.DB.Delete(&[]message.Channel{}, "id in ?", idsReq.Ids).Error
23 | }
24 |
25 | func (c *ChannelService) Update(channel message.Channel) (error, message.Channel) {
26 | return global.DB.Updates(&channel).Error, channel
27 | }
28 |
29 | func (c *ChannelService) GetById(id snowflake.ID) (err error, mt message.Channel) {
30 | err = global.DB.Where("id = ?", id).First(&mt).Error
31 | return err, mt
32 | }
33 |
34 | func (c *ChannelService) Page(page request.PageInfo) (err error, list interface{}, total int64) {
35 | db := global.DB.Model(&message.Channel{})
36 | var channelList []message.Channel
37 | err = db.Count(&total).Error
38 | if total > 0 {
39 | err = db.Limit(page.PageSize).Offset(page.Offset()).Find(&channelList).Error
40 | }
41 | return err, channelList, total
42 | }
43 |
--------------------------------------------------------------------------------
/api/service/message/application.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/message"
6 | "aixinge/global"
7 | "aixinge/utils"
8 | "aixinge/utils/snowflake"
9 | )
10 |
11 | type ApplicationService struct {
12 | }
13 |
14 | func (a *ApplicationService) Create(app message.Application) error {
15 | app.ID = utils.Id()
16 | // 状态,1、正常 2、禁用
17 | app.Status = 1
18 | return global.DB.Create(&app).Error
19 | }
20 |
21 | func (a *ApplicationService) Delete(idsReq request.IdsReq) error {
22 | return global.DB.Delete(&[]message.Channel{}, "id in ?", idsReq.Ids).Error
23 | }
24 |
25 | func (a *ApplicationService) Update(app message.Application) (error, message.Application) {
26 | return global.DB.Updates(&app).Error, app
27 | }
28 |
29 | func (a *ApplicationService) GetById(id snowflake.ID) (err error, mt message.Application) {
30 | err = global.DB.Where("id = ?", id).First(&mt).Error
31 | return err, mt
32 | }
33 |
34 | func (a *ApplicationService) Page(page request.PageInfo) (err error, list interface{}, total int64) {
35 | db := global.DB.Model(&message.Application{})
36 | var appList []message.Application
37 | err = db.Count(&total).Error
38 | if total > 0 {
39 | err = db.Limit(page.PageSize).Offset(page.Offset()).Find(&appList).Error
40 | }
41 | return err, appList, total
42 | }
43 |
--------------------------------------------------------------------------------
/api/service/message/mailTemplate.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/message"
6 | "aixinge/global"
7 | "aixinge/utils"
8 | "aixinge/utils/snowflake"
9 | )
10 |
11 | type MailTemplateService struct {
12 | }
13 |
14 | func (e *MailTemplateService) Create(app message.MailTemplate) error {
15 | app.ID = utils.Id()
16 | // 状态,1、正常 2、禁用
17 | app.Status = 1
18 | return global.DB.Create(&app).Error
19 | }
20 |
21 | func (e *MailTemplateService) Delete(idsReq request.IdsReq) error {
22 | return global.DB.Delete(&[]message.MailTemplate{}, "id in ?", idsReq.Ids).Error
23 | }
24 |
25 | func (e *MailTemplateService) Update(mt message.MailTemplate) (error, message.MailTemplate) {
26 | return global.DB.Updates(&mt).Error, mt
27 | }
28 |
29 | func (e *MailTemplateService) GetById(id snowflake.ID) (err error, mt message.MailTemplate) {
30 | err = global.DB.Where("id = ?", id).First(&mt).Error
31 | return err, mt
32 | }
33 |
34 | func (e *MailTemplateService) Page(page request.PageInfo) (err error, list interface{}, total int64) {
35 | db := global.DB.Model(&message.MailTemplate{})
36 | var mtList []message.MailTemplate
37 | err = db.Count(&total).Error
38 | if total > 0 {
39 | err = db.Limit(page.PageSize).Offset(page.Offset()).Find(&mtList).Error
40 | }
41 | return err, mtList, total
42 | }
43 |
--------------------------------------------------------------------------------
/api/router/system/role.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/api/v1"
5 | "github.com/gofiber/fiber/v2"
6 | )
7 |
8 | type RoleRouter struct {
9 | }
10 |
11 | func (s *RoleRouter) InitRoleRouter(router fiber.Router) {
12 | roleRouter := router.Group("role")
13 | var roleApi = v1.AppApi.SystemApi.Role
14 | {
15 | roleRouter.Post("create", roleApi.Create) // 创建
16 | roleRouter.Post("delete", roleApi.Delete) // 删除
17 | roleRouter.Post("update", roleApi.Update) // 更新
18 | roleRouter.Post("assign-user", roleApi.AssignUser) // 角色分配用户
19 | roleRouter.Post("selected-users", roleApi.SelectedUsers) // 角色已分配用户ID列表
20 | roleRouter.Post("assign-menu", roleApi.AssignMenu) // 角色分配菜单
21 | roleRouter.Post("selected-menus", roleApi.SelectedMenus) // 角色已分配菜单ID列表
22 | roleRouter.Post("selected-menus-detail", roleApi.SelectedMenusDetail) // 角色已分配菜单详细信息列表
23 | roleRouter.Post("get", roleApi.Get) // 根据id获取角色
24 | roleRouter.Post("batch-get", roleApi.BatchGet) // 批量根据id获取角色
25 | roleRouter.Post("page", roleApi.Page) // 分页获取角色列表
26 | roleRouter.Post("list", roleApi.List) // 获取角色列表
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/api/model/common/response/response.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "github.com/gofiber/fiber/v2"
5 | )
6 |
7 | type Response struct {
8 | Code int `json:"code"`
9 | Data interface{} `json:"data"`
10 | Msg string `json:"msg"`
11 | }
12 |
13 | const (
14 | Error = -1 // 异常
15 | Success = 0 // 正常
16 | ExpireToken = 1 // 登录 Token 过期
17 | ExpireRefreshToken = 2 // 刷新 RefreshToken 过期
18 | )
19 |
20 | func Result(code int, data interface{}, msg string, c *fiber.Ctx) error {
21 | // 开始时间
22 | return c.JSON(Response{
23 | code,
24 | data,
25 | msg,
26 | })
27 | }
28 |
29 | func Ok(c *fiber.Ctx) error {
30 | return Result(Success, map[string]interface{}{}, "操作成功", c)
31 | }
32 |
33 | func OkWithMessage(message string, c *fiber.Ctx) error {
34 | return Result(Success, map[string]interface{}{}, message, c)
35 | }
36 |
37 | func OkWithData(data interface{}, c *fiber.Ctx) error {
38 | return Result(Success, data, "操作成功", c)
39 | }
40 |
41 | func OkWithDetailed(data interface{}, message string, c *fiber.Ctx) error {
42 | return Result(Success, data, message, c)
43 | }
44 |
45 | func Fail(c *fiber.Ctx) error {
46 | return Result(Error, map[string]interface{}{}, "操作失败", c)
47 | }
48 |
49 | func FailWithMessage(message string, c *fiber.Ctx) error {
50 | return Result(Error, map[string]interface{}{}, message, c)
51 | }
52 |
53 | func FailWithDetailed(data interface{}, message string, c *fiber.Ctx) error {
54 | return Result(Error, data, message, c)
55 | }
56 |
--------------------------------------------------------------------------------
/api/model/message/mailLog.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "aixinge/api/model/common"
5 | "aixinge/global"
6 | "aixinge/utils/snowflake"
7 | "database/sql/driver"
8 | "encoding/json"
9 | )
10 |
11 | type MailLog struct {
12 | global.MODEL
13 | AppId snowflake.ID `json:"appId,omitempty" swaggertype:"string"` // 应用 ID
14 | TemplateId snowflake.ID `json:"templateId,omitempty" swaggertype:"string"` // 邮件模板 ID
15 | RequestId snowflake.ID `json:"requestId,omitempty" swaggertype:"string"` // 唯一请求 ID
16 | To MailAddress `json:"to" gorm:"type:json"` // 发件地址集合
17 | Cc MailAddress `json:"cc" gorm:"type:json"` // 抄送地址集合
18 | Bcc MailAddress `json:"bcc" gorm:"type:json"` // 密送地址集合
19 | Parameters string `json:"parameters"` // 邮件参数
20 | Content string `json:"content"` // 邮件具体内容
21 | Attachments common.Attachments `json:"attachments" gorm:"type:json"` // 附件JSON
22 | Status int `json:"status"` // 状态 1、正常 2、异常
23 | ErrMsg string `json:"errMsg"` // 错误日志
24 |
25 | }
26 |
27 | type MailAddress []string
28 |
29 | func (m MailAddress) Value() (driver.Value, error) {
30 | b, err := json.Marshal(m)
31 | return string(b), err
32 | }
33 |
34 | func (m *MailAddress) Scan(src any) error {
35 | return json.Unmarshal(src.([]byte), m)
36 | }
37 |
--------------------------------------------------------------------------------
/core/viper.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "aixinge/global"
5 | "flag"
6 | "fmt"
7 | "github.com/fsnotify/fsnotify"
8 | "github.com/spf13/viper"
9 | "os"
10 | )
11 |
12 | func Viper(path ...string) *viper.Viper {
13 | var config string
14 | if len(path) == 0 {
15 | flag.StringVar(&config, "c", "", "choose config file.")
16 | flag.Parse()
17 | if config == "" {
18 | // 优先级: 命令行 > 环境变量 > 默认值
19 | if configEnv := os.Getenv(global.ConfigEnv); configEnv == "" {
20 | config = global.ConfigFile
21 | fmt.Printf("您正在使用config的默认值,config的路径为%v\n", global.ConfigFile)
22 | } else {
23 | config = configEnv
24 | fmt.Printf("您正在使用CONFIG环境变量,config的路径为%v\n", config)
25 | }
26 | } else {
27 | fmt.Printf("您正在使用命令行的-c参数传递的值,config的路径为%v\n", config)
28 | }
29 | } else {
30 | config = path[0]
31 | fmt.Printf("您正在使用func Viper()传递的值,config的路径为%v\n", config)
32 | }
33 |
34 | v := viper.New()
35 | v.SetConfigFile(config)
36 | v.SetConfigType("yaml")
37 | if err := v.ReadInConfig(); err != nil {
38 | if _, ok := err.(viper.ConfigFileNotFoundError); ok {
39 | //配置文件没有找到
40 | panic(fmt.Errorf("the config file does not exist: %s \n", err))
41 | } else {
42 | // 配置文件找到了,但是在这个过程有又出现别的什么error
43 | panic(fmt.Errorf("Fatal error config file: %s \n", err))
44 | }
45 | }
46 | v.WatchConfig()
47 |
48 | v.OnConfigChange(func(e fsnotify.Event) {
49 | fmt.Println("config file changed:", e.Name)
50 | if err := v.Unmarshal(&global.CONFIG); err != nil {
51 | fmt.Println(err)
52 | }
53 | })
54 | if err := v.Unmarshal(&global.CONFIG); err != nil {
55 | fmt.Println(err)
56 | }
57 | return v
58 | }
59 |
--------------------------------------------------------------------------------
/CONTRIBUTION.md:
--------------------------------------------------------------------------------
1 | # 贡献手册
2 |
3 | 本文为想要参与 AiXinGe 开源项目的指导手册,欢迎大家积极参与贡献代码。
4 |
5 | ## 贡献方式
6 |
7 | - 提交 PullRequest:包括但不限于针对项目提供新特性、修复缺陷、完善注释、修正拼写问题等方式。
8 | - 创建 Issue:为项目进行测验,发现问题并提出问题
9 | - 参与讨论:欢迎加群或者联系作者进行讨论
10 |
11 | ## 代码规范
12 |
13 | - 本项目集成了 editorconfig,相关的缩进配置都在里面,有不清楚的可以查询。
14 | - 代码编写的时候尽可能完善注释,如果可以,最好是英文注释。
15 | - 命名要清晰,尽可能做到见名知意。
16 |
17 | ## Git 提交规范
18 |
19 | 为了方便管理,我们的 Git
20 | 提交遵循 [AngularJS Git 提交规范](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit)
21 | ,这是一个相对标准并受到大部分人认可的 Commit 模板,主要说明如下:
22 |
23 | 一次 Git 提交信息格式类似于:
24 |
25 | ```
26 | type(scope): short description
27 |
28 | long description
29 | ```
30 |
31 | 其中 type 主要有以下几种类型:
32 |
33 | - feat:新功能相关改动
34 | - fix:缺陷相关的修复(如果有 Issue 编号请带上)
35 | - docs:文档相关变化
36 | - style:代码格式调整(不影响代码运行的变动)
37 | - refactor:代码的重构或优化(既不增加新功能,也不是修复bug)
38 | - perf:性能优化相关变动
39 | - test:测试文件相关变动
40 | - ci:持续集成相关文件的变动
41 | - chore:其他文件的变动(不涉及源码和测试源码)
42 | - revert:回退至某一次提交
43 |
44 | 而 scope 则表示了当前改动的范围,short description 为本次提交的短描述,long description 为本次提交的长描述(长描述非必选,在有必要的情况下进行填写)
45 |
46 | 故此,一个正确的提交规范类似于:
47 |
48 | ```
49 | docs(md): add contribution
50 |
51 | add project contribution guide
52 | ```
53 |
54 | 如果有不清楚的地方,也可以搜索相关文件或教程,查看更多的说明。
55 |
56 | ### 插件
57 |
58 | Jetbrains 系列 IDE 中,有插件可以帮助我们快捷添加满足规范的 Commit Message。
59 |
60 | **插件地址:**[git-commit-template](https://plugins.jetbrains.com/plugin/9861-git-commit-template)
61 |
62 | **使用方法:**
63 | 
64 | 
65 |
66 | 除了插件以外,大家还可以搜索到更多的工具,在此就不赘述了,大家感兴趣可以自行了解。
67 |
--------------------------------------------------------------------------------
/api/model/validation/verify.go:
--------------------------------------------------------------------------------
1 | package validation
2 |
3 | var (
4 | Id = Rules{"ID": {NotEmpty()}}
5 | Api = Rules{"Path": {NotEmpty()}, "Description": {NotEmpty()}, "ApiGroup": {NotEmpty()}, "Method": {NotEmpty()}}
6 | Menu = Rules{"Path": {NotEmpty()}, "ParentId": {NotEmpty()}, "Name": {NotEmpty()}, "Component": {NotEmpty()}, "Sort": {Ge("0")}}
7 | MenuMeta = Rules{"Title": {NotEmpty()}}
8 | Login = Rules{"Username": {NotEmpty()}, "Password": {NotEmpty()}}
9 | UserCreate = Rules{"Username": {NotEmpty()}, "NickName": {NotEmpty()}, "Password": {NotEmpty()}}
10 | PageInfo = Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}}
11 | PurchasePageInfo = Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}, "PurchaseOrderId": {NotEmpty()}}
12 | SalesPageInfo = Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}, "SalesOrderId": {NotEmpty()}}
13 | PurchaseReturnPageInfo = Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}, "PurchaseReturnId": {NotEmpty()}}
14 | ReturnOrderPageInfo = Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}, "ReturnOrderId": {NotEmpty()}}
15 | Customer = Rules{"CustomerName": {NotEmpty()}, "CustomerPhoneData": {NotEmpty()}}
16 | Authority = Rules{"AuthorityId": {NotEmpty()}, "AuthorityName": {NotEmpty()}, "ParentId": {NotEmpty()}}
17 | AuthorityId = Rules{"AuthorityId": {NotEmpty()}}
18 | OldAuthority = Rules{"OldAuthorityId": {NotEmpty()}}
19 | ChangePassword = Rules{"Username": {NotEmpty()}, "Password": {NotEmpty()}, "NewPassword": {NotEmpty()}}
20 | SetUserAuthority = Rules{"AuthorityId": {NotEmpty()}}
21 | )
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AiXinGe
2 |
3 | Ai(爱)Xin(信)Ge(鸽) - 智能消息推送平台致力于解决大家在集成消息推送时的各种难题,力求将消息通知简单化、统一化,实现推送"All
4 | in One"的效果。
5 |
6 | > 目前项目刚刚启动,欢迎感兴趣的小伙伴 Star 插眼,我们会在后续的更新中逐步去实现我们的目标特性。
7 |
8 | ## 目标特性
9 |
10 | - 易使用:一个 SDK/API 即可实现不同类型的消息推送,再也不用对接各种消息推送 SDK 了
11 | - 易管理:集成市面上绝大部分推送渠道,实现统一管理,如需更改渠道,只需要进行相应配置并绑定到对应的消息模板即可。
12 | - 易部署:可通过二进制文件或者 Docker 镜像实现一键启动,同时有可视化的引导式配置简化大家的配置难度。
13 | - 高性能:依托于 Go 语言的特性,全程通过 Pipeline + Async 为推送平台提供强劲性能。
14 |
15 | ## 功能规划
16 |
17 | V1 版本功能规划:
18 | 
19 |
20 | 后续功能计划中(小程序消息、OA 消息、订阅号等)
21 |
22 | 欢迎大家踊跃参与贡献,相关贡献手册见:[贡献指南](CONTRIBUTION.md)
23 |
24 | ## 快速开始
25 |
26 | - 配置,拷贝 `config-sample.yaml` 文件为 `config.yaml` 修改数据配置信息
27 | - 数据库默认 `MySQL` 导入 `wiki/sql` 脚本
28 | - 编译打包前端 [aixinge-ui](https://gitee.com/aixinge/aixinge-ui) 编译文件 `dist` 放到 `web` 目录下(不想打包前端可以创建 `web/dist/index.html` 目录及文件)
29 |
30 | [GoLang 1.19+ 点击下载](https://studygolang.com/dl)
31 |
32 | [开发工具GoLand下载](https://www.jetbrains.com.cn/go/)
33 |
34 | ## 技术栈
35 |
36 | - [fiber](https://gofiber.io)
37 | - [gorm](https://gorm.io/zh_CN/)
38 | - [viper](https://github.com/spf13/viper)
39 | - [casbin](https://github.com/casbin/casbin)
40 | - [swag](https://github.com/swaggo/swag/blob/master/README_zh-CN.md)
41 |
42 | ## 依赖升级
43 |
44 | ```shell
45 | go get -u github.com/gofiber/fiber/v2@latest
46 |
47 | go get -u all
48 |
49 | go mod tidy
50 | ```
51 |
52 | ## 文档更新
53 |
54 | ```shell
55 | go install github.com/swaggo/swag/cmd/swag@latest
56 |
57 | swag init
58 | ```
59 |
60 | > 用户名 `admin` 密码 `123456`
61 | http://127.0.0.1:8888/swagger/index.html
62 |
63 | ## 打包
64 |
65 | > 交叉编译打包命令
66 |
67 | ```shell
68 | goreleaser --snapshot --skip-publish --rm-dist
69 | ```
70 |
71 |
72 |
--------------------------------------------------------------------------------
/core/mail/smtp.go:
--------------------------------------------------------------------------------
1 | package mail
2 |
3 | import (
4 | "bytes"
5 | "net/smtp"
6 | "strings"
7 | )
8 |
9 | type Smtp struct {
10 | address string
11 | username string
12 | auth smtp.Auth
13 | }
14 |
15 | func NewSmtp(identity, address, username, password string) Smtp {
16 | s := Smtp{}
17 | hp := strings.Split(address, ":")
18 | s.username = username
19 | s.auth = smtp.PlainAuth(identity, username, password, hp[0])
20 | return s
21 | }
22 |
23 | // SendMail 发送邮件
24 | // html 发送内容是否为 Html
25 | // subject 主题
26 | // content 内容
27 | // replyToAddress 回信地址
28 | // to 收件人
29 | // cc 抄送人
30 | // bcc 密送人
31 | func (s Smtp) SendMail(html bool, subject, content, replyToAddress string, to, cc, bcc []string) error {
32 | var sendTo = to
33 | var bt bytes.Buffer
34 | // 收件人
35 | bt.WriteString("To:")
36 | bt.WriteString(strings.Join(to, ","))
37 | bt.WriteString("\r\n")
38 | // 主题
39 | bt.WriteString("Subject: ")
40 | bt.WriteString(subject)
41 | bt.WriteString("\r\n")
42 | // 回信地址
43 | bt.WriteString("Reply-To: ")
44 | bt.WriteString(replyToAddress)
45 | bt.WriteString("\r\n")
46 | // 抄送人
47 | if len(cc) > 0 {
48 | sendTo = append(sendTo, cc...)
49 | bt.WriteString("Cc: ")
50 | bt.WriteString(strings.Join(cc, ","))
51 | bt.WriteString("\r\n")
52 | }
53 | // 密送人
54 | if len(bcc) > 0 {
55 | sendTo = append(sendTo, bcc...)
56 | bt.WriteString("Bcc: ")
57 | bt.WriteString(strings.Join(bcc, ","))
58 | bt.WriteString("\r\n")
59 | }
60 | // 内容类型
61 | bt.WriteString("Content-Type: text/")
62 | if html {
63 | bt.WriteString("html")
64 | } else {
65 | bt.WriteString("plain")
66 | }
67 | bt.WriteString("; charset=UTF-8")
68 | bt.WriteString("\r\n\r\n")
69 | bt.WriteString(content)
70 | return smtp.SendMail(s.address, s.auth, s.username, sendTo, bt.Bytes())
71 | }
72 |
--------------------------------------------------------------------------------
/core/sms/aliyun.go:
--------------------------------------------------------------------------------
1 | package sms
2 |
3 | import (
4 | "aixinge/global"
5 | "aixinge/utils/helper"
6 | "encoding/json"
7 | "go.uber.org/zap"
8 | "io"
9 | "net/http"
10 | "strings"
11 | )
12 |
13 | const (
14 | Endpoint = "dysmsapi.aliyuncs.com"
15 | GET = "GET"
16 | POST = "POST"
17 | Version = "2017-05-25"
18 | )
19 |
20 | type AliyunSmsClient struct {
21 | accessKeyId string
22 | accessKeySecret string
23 | }
24 |
25 | type AliyunSmsResponse struct {
26 | Message string `json:"message"`
27 | RequestId string `json:"requestId"`
28 | Code string `json:"code"`
29 | BizId string `json:"bizId"`
30 | }
31 |
32 | func CreateClient(accessKeyId, accessKeySecret string) AliyunSmsClient {
33 | c := AliyunSmsClient{}
34 | c.accessKeyId = accessKeyId
35 | c.accessKeySecret = accessKeySecret
36 | return c
37 | }
38 |
39 | func (c AliyunSmsClient) SendSms(phoneNumbers []string, signName, templateCode, templateParam string) AliyunSmsResponse {
40 | parameters := map[string]string{
41 | "PhoneNumbers": strings.Join(phoneNumbers, ","),
42 | "SignName": signName,
43 | "TemplateCode": templateCode,
44 | "TemplateParam": templateParam,
45 | }
46 | url := helper.BuildOpenApiRequestUrl("SendSms", Version, GET, Endpoint, c.accessKeyId, c.accessKeySecret, parameters)
47 | resp, requestErr := http.Get(url)
48 | if requestErr != nil {
49 | global.LOG.Error("请求发送阿里云短信失败!", zap.Any("err", requestErr))
50 | }
51 | defer resp.Body.Close()
52 | body, readErr := io.ReadAll(resp.Body)
53 | if readErr != nil {
54 | global.LOG.Error("解析阿里云短信响应失败!", zap.Any("err", readErr))
55 | }
56 |
57 | smsResponse := AliyunSmsResponse{}
58 | jsonErr := json.Unmarshal(body, &smsResponse)
59 | if jsonErr != nil {
60 | global.LOG.Error("解析阿里云短信响应失败!", zap.Any("err", jsonErr))
61 | }
62 | return smsResponse
63 | }
64 |
--------------------------------------------------------------------------------
/api/model/system/menu.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/global"
5 | "aixinge/utils/snowflake"
6 | )
7 |
8 | type BaseMenu struct {
9 | global.MODEL
10 | ParentId snowflake.ID `json:"parentId" swaggertype:"string"` // 父菜单ID
11 | Path string `json:"path" ` // 路由path
12 | Redirect string `json:"redirect"` // 重定向的路由path
13 | Name string `json:"name"` // 路由name
14 | Hidden int `json:"hidden"` // 是否在列表隐藏 1,是 2,否
15 | Component string `json:"component"` // 对应前端文件路径
16 | Sort int `json:"sort"` // 排序标记
17 | IsFrame int `json:"isFrame"` // Frame 1,是 2,否
18 | Meta `json:"meta"` // 附加属性
19 | }
20 |
21 | type Menu struct {
22 | global.MODEL
23 | ParentId snowflake.ID `json:"parentId" swaggertype:"string"` // 父菜单ID
24 | Path string `json:"path" ` // 路由path
25 | Redirect string `json:"redirect"` // 重定向的路由path
26 | Name string `json:"name"` // 路由name
27 | Hidden int `json:"hidden"` // 是否在列表隐藏 1,是 2,否
28 | Component string `json:"component"` // 对应前端文件路径
29 | Sort int `json:"sort"` // 排序标记
30 | IsFrame int `json:"isFrame"` // Frame 1,是 2,否
31 | Status int `json:"status"` // 状态,1、正常 2、禁用
32 | Meta `json:"meta"` // 附加属性
33 | }
34 |
35 | type Meta struct {
36 | NoCache int `json:"noCache"` // 不缓存 1,是 2,否
37 | Title string `json:"title"` // 菜单名
38 | Icon string `json:"icon"` // 菜单图标
39 | Remark string `json:"remark"` // 备注
40 | }
41 |
--------------------------------------------------------------------------------
/api/v1/message/channelTemplate.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/common/response"
6 | "aixinge/api/model/message"
7 | "aixinge/api/model/validation"
8 | "aixinge/global"
9 | "github.com/gofiber/fiber/v2"
10 | "go.uber.org/zap"
11 | )
12 |
13 | type ChannelTemplate struct {
14 | }
15 |
16 | // Create
17 | // @Tags ChannelTemplate
18 | // @Summary 创建渠道模板
19 | // @Security ApiKeyAuth
20 | // @accept application/json
21 | // @Produce application/json
22 | // @Param data body message.ChannelTemplate true "创建渠道模板"
23 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"渠道模板创建成功"}"
24 | // @Router /v1/channel-template/create [post]
25 | func (b *ChannelTemplate) Create(c *fiber.Ctx) error {
26 | var ct message.ChannelTemplate
27 | _ = c.BodyParser(&ct)
28 | err := channelTemplateService.Create(ct)
29 | if err != nil {
30 | global.LOG.Error("创建渠道模板失败!", zap.Any("err", err))
31 | return response.FailWithMessage("渠道模板创建失败:"+err.Error(), c)
32 | }
33 | return response.OkWithMessage("渠道模板创建成功!", c)
34 | }
35 |
36 | // Delete
37 | // @Tags ChannelTemplate
38 | // @Summary 删除渠道模板
39 | // @Security ApiKeyAuth
40 | // @accept application/json
41 | // @Produce application/json
42 | // @Param data body request.IdsReq true "ID集合"
43 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
44 | // @Router /v1/channel-template/delete [post]
45 | func (b *ChannelTemplate) Delete(c *fiber.Ctx) error {
46 | var idsReq request.IdsReq
47 | _ = c.BodyParser(&idsReq)
48 | if err := validation.Verify(idsReq, validation.Id); err != nil {
49 | return response.FailWithMessage(err.Error(), c)
50 | }
51 | if err := channelTemplateService.Delete(idsReq); err != nil {
52 | global.LOG.Error("删除失败!", zap.Any("err", err))
53 | return response.FailWithMessage("删除失败", c)
54 | } else {
55 | return response.OkWithMessage("删除成功", c)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/api/v1/message/mailLog.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/common/response"
6 | "aixinge/api/model/validation"
7 | "aixinge/global"
8 | "github.com/gofiber/fiber/v2"
9 | "go.uber.org/zap"
10 | )
11 |
12 | type MailLog struct {
13 | }
14 |
15 | // Delete
16 | // @Tags MailLog
17 | // @Summary 删除邮件日志
18 | // @Security ApiKeyAuth
19 | // @accept application/json
20 | // @Produce application/json
21 | // @Param data body request.IdsReq true "ID集合"
22 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
23 | // @Router /v1/mail-log/delete [post]
24 | func (m *MailLog) Delete(c *fiber.Ctx) error {
25 | var idsReq request.IdsReq
26 | _ = c.BodyParser(&idsReq)
27 | if err := validation.Verify(idsReq, validation.Id); err != nil {
28 | return response.FailWithMessage(err.Error(), c)
29 | }
30 | if err := mailLogService.Delete(idsReq); err != nil {
31 | global.LOG.Error("删除失败!", zap.Any("err", err))
32 | return response.FailWithMessage("删除失败", c)
33 | } else {
34 | return response.OkWithMessage("删除成功", c)
35 | }
36 | }
37 |
38 | // Page
39 | // @Tags MailLog
40 | // @Summary 分页获取邮件日志
41 | // @Security ApiKeyAuth
42 | // @accept application/json
43 | // @Produce application/json
44 | // @Param data body request.PageInfo true "页码, 每页大小"
45 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
46 | // @Router /v1/mail-log/page [post]
47 | func (m *MailLog) Page(c *fiber.Ctx) error {
48 | var pageInfo request.PageInfo
49 | _ = c.BodyParser(&pageInfo)
50 | if err := validation.Verify(pageInfo, validation.PageInfo); err != nil {
51 | return response.FailWithMessage(err.Error(), c)
52 | }
53 | if err, list, total := mailLogService.Page(pageInfo); err != nil {
54 | global.LOG.Error("获取失败!", zap.Any("err", err))
55 | return response.FailWithMessage("获取失败", c)
56 | } else {
57 | return response.OkWithDetailed(response.PageResult{
58 | List: list,
59 | Total: total,
60 | Page: pageInfo.Page,
61 | PageSize: pageInfo.PageSize,
62 | }, "获取成功", c)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/utils/oss/local.go:
--------------------------------------------------------------------------------
1 | package oss
2 |
3 | import (
4 | "aixinge/global"
5 | "aixinge/utils"
6 | "errors"
7 | "io"
8 | "mime/multipart"
9 | "os"
10 | "path"
11 | "path/filepath"
12 | "strings"
13 | "time"
14 | )
15 |
16 | type Local struct{}
17 |
18 | // UploadFile 上传文件
19 | func (*Local) UploadFile(file *multipart.FileHeader) (string, string, string, error) {
20 | // 读取文件后缀
21 | ext := path.Ext(file.Filename)
22 | // 读取文件名并加密
23 | name := strings.TrimSuffix(file.Filename, ext)
24 | name = utils.GetByteMd5([]byte(name))
25 | yearMonth := time.Now().Format("200601")
26 | // 尝试创建此路径
27 | var uploadPath = global.CONFIG.Upload.Path
28 | mkdirErr := os.MkdirAll(filepath.Join(uploadPath, yearMonth), os.ModePerm)
29 | if mkdirErr != nil {
30 | return "", "", ext, errors.New("function os.MkdirAll() Filed, err:" + mkdirErr.Error())
31 | }
32 | // 拼接新文件名
33 | filename := name + time.Now().Format("02150405") + ext
34 | // 路径/年月/文件名
35 | p := filepath.Join(uploadPath, yearMonth, filename)
36 |
37 | f, openError := file.Open() // 读取文件
38 | if openError != nil {
39 | return "", "", ext, errors.New("function file.Open() Filed, err:" + openError.Error())
40 | }
41 | defer func(f multipart.File) {
42 | _ = f.Close()
43 | }(f) // 创建文件 defer 关闭
44 |
45 | out, createErr := os.Create(p)
46 | if createErr != nil {
47 | return "", "", ext, errors.New("function os.Create() Filed, err:" + createErr.Error())
48 | }
49 | defer func(out *os.File) {
50 | _ = out.Close()
51 | }(out) // 创建文件 defer 关闭
52 |
53 | _, copyErr := io.Copy(out, f) // 传输(拷贝)文件
54 | if copyErr != nil {
55 | return "", "", ext, errors.New("function io.Copy() Filed, err:" + copyErr.Error())
56 | }
57 | return p, utils.GetFileMd5(p), ext, nil
58 | }
59 |
60 | func (*Local) FGetObject(key, infile string) error {
61 | return nil
62 | }
63 |
64 | func (*Local) GetObject(key string) ([]byte, error) {
65 | return os.ReadFile(key)
66 | }
67 |
68 | // DeleteFile 删除文件
69 | func (*Local) DeleteFile(key string) error {
70 | var uploadPath = global.CONFIG.Upload.Path
71 | p := filepath.Join(uploadPath, key)
72 | if strings.Contains(p, uploadPath) {
73 | if err := os.Remove(p); err != nil {
74 | return errors.New("本地文件删除失败, err:" + err.Error())
75 | }
76 | }
77 | return nil
78 | }
79 |
--------------------------------------------------------------------------------
/api/v1/system/file.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/api/model/common/response"
5 | "aixinge/utils"
6 | "aixinge/utils/oss"
7 | "errors"
8 | "github.com/gofiber/fiber/v2"
9 | "io"
10 | "net/http"
11 | "strconv"
12 | )
13 |
14 | type File struct{}
15 |
16 | var ossClient = oss.NewLocal()
17 |
18 | // Download
19 | // @Tags File
20 | // @Summary 文件下载
21 | // @Security ApiKeyAuth
22 | // @Accept x-www-form-urlencoded
23 | // @Produce application/octet-stream
24 | // @Param id query uint64 true "主键"
25 | // @Success 200
26 | // @Router /v1/file/download [get]
27 | func (f *File) Download(c *fiber.Ctx) error {
28 | var id = c.Query("id")
29 | if len(id) == 0 {
30 | return errors.New("query param `id` not found")
31 | }
32 |
33 | id64, err := strconv.ParseInt(id, 10, 64)
34 | err, file := fileService.GetById(id64)
35 | if err != nil {
36 | return errors.New("not found file")
37 | }
38 | // 读取文件
39 | fileBytes, err := ossClient.GetObject(file.Path)
40 | if err != nil {
41 | return errors.New("read file error")
42 | }
43 |
44 | // 附件下载
45 | c.Attachment(file.Filename)
46 | return c.Send(fileBytes)
47 | }
48 |
49 | // Upload
50 | // @Tags File
51 | // @Summary 文件上传
52 | // @Security ApiKeyAuth
53 | // @accept multipart/form-data
54 | // @Produce application/json
55 | // @Param file formData file true "上传文件"
56 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"上传成功"}"
57 | // @Router /v1/file/upload [post]
58 | func (f *File) Upload(c *fiber.Ctx) error {
59 | header, err := c.FormFile("file")
60 | if err != nil {
61 | return response.FailWithMessage("上传文件不存在", c)
62 | }
63 |
64 | // 读取文件
65 | file, err := header.Open()
66 |
67 | // 读取文件字节信息
68 | fileBytes, err := io.ReadAll(file)
69 | if err != nil {
70 | return response.FailWithMessage(err.Error(), c)
71 | }
72 | contentType := http.DetectContentType(fileBytes)
73 |
74 | // 文件 MD5 值获取判断是否上传
75 | var fileMd5 = utils.GetByteMd5(fileBytes)
76 | err, dbFile := fileService.GetByMd5(fileMd5)
77 | if err == nil {
78 | return response.OkWithData(dbFile.ID, c)
79 | }
80 |
81 | // 存储文件
82 | filePath, md5, ext, uploadErr := ossClient.UploadFile(header)
83 | if uploadErr != nil {
84 | return response.FailWithMessage(err.Error(), c)
85 | }
86 |
87 | err, _file := fileService.Save(md5, filePath, ext, contentType, header.Filename, header.Size)
88 | if err != nil {
89 | return response.FailWithMessage(err.Error(), c)
90 | }
91 | return response.OkWithData(_file, c)
92 | }
93 |
--------------------------------------------------------------------------------
/utils/helper/aliyun.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import (
4 | "aixinge/utils"
5 | "crypto/hmac"
6 | "crypto/sha1"
7 | "encoding/base64"
8 | "fmt"
9 | "net/url"
10 | "sort"
11 | "strings"
12 | "time"
13 | )
14 |
15 | func GetUtcTime() string {
16 | now := time.Now()
17 | year, mon, day := now.UTC().Date()
18 | hour, min, sec := now.UTC().Clock()
19 | s := fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02dZ", year, mon, day, hour, min, sec)
20 | return s
21 | }
22 |
23 | func PercentEncode(str string) string {
24 | str = strings.Replace(str, "+", "%20", -1)
25 | str = strings.Replace(str, "*", "%2A", -1)
26 | str = strings.Replace(str, "%7E", "~", -1)
27 | return str
28 | }
29 |
30 | func InitCommonRequestParameters(accessKeyId, action, version string) map[string]string {
31 | var commonParameters = map[string]string{
32 | "Format": "JSON",
33 | "SignatureMethod": "HMAC-SHA1",
34 | "SignatureVersion": "1.0",
35 | }
36 | commonParameters["Action"] = action
37 | commonParameters["AccessKeyId"] = accessKeyId
38 | commonParameters["Version"] = version
39 | commonParameters["SignatureNonce"] = utils.Id().String()
40 | commonParameters["Timestamp"] = GetUtcTime()
41 | return commonParameters
42 | }
43 |
44 | func BuildUrlParams(params map[string]string) url.Values {
45 | keys := make([]string, 0, len(params))
46 | for k := range params {
47 | keys = append(keys, k)
48 | }
49 | sort.Strings(keys)
50 |
51 | urlParams := url.Values{}
52 | for _, k := range keys {
53 | urlParams.Add(k, params[k])
54 | }
55 |
56 | return urlParams
57 | }
58 |
59 | func BuildSignStr(requestType, standardRequestStr string) string {
60 | return requestType + "&" + url.QueryEscape("/") + "&" + "" + url.QueryEscape(standardRequestStr)
61 | }
62 |
63 | func BuildSignature(accessKeySecret, signStr string) string {
64 | key := []byte(accessKeySecret + "&")
65 | mac := hmac.New(sha1.New, key)
66 | mac.Write([]byte(signStr))
67 | res := base64.StdEncoding.EncodeToString(mac.Sum(nil))
68 | return res
69 | }
70 |
71 | func BuildOpenApiRequestUrl(action, version, requestType, endpoint, accessKeyId, accessKeySecret string, params map[string]string) string {
72 | requestParameters := InitCommonRequestParameters(accessKeyId, action, version)
73 | for k, v := range params {
74 | requestParameters[k] = v
75 | }
76 |
77 | urlParams := BuildUrlParams(requestParameters)
78 | encodeUrlParams := urlParams.Encode()
79 | percent := PercentEncode(encodeUrlParams)
80 | signStr := BuildSignStr(requestType, percent)
81 | signature := BuildSignature(accessKeySecret, signStr)
82 | return "https://" + endpoint + "/?" + encodeUrlParams + "&Signature=" + signature
83 | }
84 |
--------------------------------------------------------------------------------
/initialize/router.go:
--------------------------------------------------------------------------------
1 | package initialize
2 |
3 | import (
4 | "aixinge/api/router"
5 | _ "aixinge/docs"
6 | "aixinge/global"
7 | "aixinge/middleware"
8 | "aixinge/web"
9 | "fmt"
10 | swagger "github.com/arsmn/fiber-swagger/v2"
11 | "github.com/goccy/go-json"
12 | "github.com/gofiber/fiber/v2"
13 | "github.com/gofiber/fiber/v2/middleware/filesystem"
14 | "net/http"
15 | )
16 |
17 | func Routers() *fiber.App {
18 | var app = fiber.New(fiber.Config{
19 | DisableStartupMessage: true,
20 |
21 | // https://github.com/goccy/go-json
22 | JSONEncoder: json.Marshal,
23 | JSONDecoder: json.Unmarshal,
24 | })
25 | global.LOG.Debug("register swagger handler")
26 | app.Get("/swagger/*", swagger.HandlerDefault)
27 |
28 | global.LOG.Debug("register upload file handler")
29 |
30 | global.LOG.Debug("use middleware logger")
31 | app.Use(middleware.Logger())
32 |
33 | global.LOG.Debug("use middleware recover")
34 | app.Use(middleware.Recover())
35 | // 跨域
36 | global.LOG.Debug("use middleware cors")
37 | app.Use(middleware.Cors())
38 |
39 | // 获取路由组实例
40 | systemRouter := router.AppRouter.System
41 | messageRouter := router.AppRouter.Message
42 |
43 | // 获取context-path
44 | prefix := global.CONFIG.System.ContextPath
45 | if prefix == "" {
46 | fmt.Printf("context-path为默认值,路径为/ \n")
47 | prefix = "/"
48 | } else {
49 | fmt.Printf("context-path为%v \n", prefix)
50 | }
51 |
52 | prefix = prefix + "v1"
53 |
54 | // 注入免鉴权路由
55 | publicGroup := app.Group(prefix)
56 | {
57 | systemRouter.InitBaseRouter(publicGroup) // 注册
58 | }
59 |
60 | // 注入鉴权路由
61 | privateGroup := app.Group(prefix)
62 | privateGroup.Use(middleware.JWTAuth()).Use(middleware.RbacHandler())
63 | {
64 | /** 系统管理 */
65 | systemRouter.InitUserRouter(privateGroup) // 用户
66 | systemRouter.InitRoleRouter(privateGroup) // 角色
67 | systemRouter.InitMenuRouter(privateGroup) // 菜单
68 | systemRouter.InitFileRouter(privateGroup) // 文件
69 |
70 | /** 应用基础 */
71 | messageRouter.InitApplicationRouter(privateGroup) // 应用
72 | messageRouter.InitChannelRouter(privateGroup) // 消息渠道
73 | messageRouter.InitChannelTemplateRouter(privateGroup) // 渠道模板
74 | messageRouter.InitMailLogRouter(privateGroup) // 邮件日志
75 | messageRouter.InitMailTemplateRouter(privateGroup) // 邮件模板
76 | }
77 |
78 | global.LOG.Debug("register filesystem handler")
79 | app.Use("/", filesystem.New(filesystem.Config{
80 | Root: http.FS(web.Dist),
81 | Browse: true,
82 | Index: "index.html",
83 | NotFoundFile: "404.html",
84 | PathPrefix: "/dist",
85 | MaxAge: 3600,
86 | }))
87 |
88 | global.LOG.Debug("router register success")
89 | return app
90 | }
91 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module aixinge
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/arsmn/fiber-swagger/v2 v2.31.1
7 | github.com/fsnotify/fsnotify v1.6.0
8 | github.com/go-co-op/gocron v1.18.0
9 | github.com/goccy/go-json v0.9.11
10 | github.com/gofiber/fiber/v2 v2.40.1
11 | github.com/gofiber/websocket/v2 v2.1.1
12 | github.com/golang-jwt/jwt/v4 v4.4.2
13 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
14 | github.com/satori/go.uuid v1.2.0
15 | github.com/spf13/viper v1.13.0
16 | github.com/stretchr/testify v1.8.1
17 | github.com/swaggo/swag v1.8.8
18 | go.uber.org/zap v1.23.0
19 | gorm.io/driver/mysql v1.4.3
20 | gorm.io/gorm v1.24.0
21 | )
22 |
23 | require (
24 | github.com/KyleBanks/depth v1.2.1 // indirect
25 | github.com/andybalholm/brotli v1.0.4 // indirect
26 | github.com/davecgh/go-spew v1.1.1 // indirect
27 | github.com/fasthttp/websocket v1.5.0 // indirect
28 | github.com/go-openapi/jsonpointer v0.19.5 // indirect
29 | github.com/go-openapi/jsonreference v0.20.0 // indirect
30 | github.com/go-openapi/spec v0.20.7 // indirect
31 | github.com/go-openapi/swag v0.22.3 // indirect
32 | github.com/go-sql-driver/mysql v1.6.0 // indirect
33 | github.com/hashicorp/hcl v1.0.0 // indirect
34 | github.com/jinzhu/inflection v1.0.0 // indirect
35 | github.com/jinzhu/now v1.1.5 // indirect
36 | github.com/jonboulle/clockwork v0.1.0 // indirect
37 | github.com/josharian/intern v1.0.0 // indirect
38 | github.com/klauspost/compress v1.15.12 // indirect
39 | github.com/lestrrat-go/strftime v1.0.6 // indirect
40 | github.com/magiconair/properties v1.8.6 // indirect
41 | github.com/mailru/easyjson v0.7.7 // indirect
42 | github.com/mattn/go-colorable v0.1.13 // indirect
43 | github.com/mattn/go-isatty v0.0.16 // indirect
44 | github.com/mattn/go-runewidth v0.0.14 // indirect
45 | github.com/mitchellh/mapstructure v1.5.0 // indirect
46 | github.com/pelletier/go-toml v1.9.5 // indirect
47 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect
48 | github.com/pkg/errors v0.9.1 // indirect
49 | github.com/pmezard/go-difflib v1.0.0 // indirect
50 | github.com/rivo/uniseg v0.4.3 // indirect
51 | github.com/robfig/cron/v3 v3.0.1 // indirect
52 | github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 // indirect
53 | github.com/spf13/afero v1.9.2 // indirect
54 | github.com/spf13/cast v1.5.0 // indirect
55 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
56 | github.com/spf13/pflag v1.0.5 // indirect
57 | github.com/subosito/gotenv v1.4.1 // indirect
58 | github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect
59 | github.com/valyala/bytebufferpool v1.0.0 // indirect
60 | github.com/valyala/fasthttp v1.43.0 // indirect
61 | github.com/valyala/tcplisten v1.0.0 // indirect
62 | go.uber.org/atomic v1.10.0 // indirect
63 | go.uber.org/multierr v1.8.0 // indirect
64 | golang.org/x/net v0.2.0 // indirect
65 | golang.org/x/sync v0.1.0 // indirect
66 | golang.org/x/sys v0.3.0 // indirect
67 | golang.org/x/text v0.4.0 // indirect
68 | golang.org/x/tools v0.3.0 // indirect
69 | gopkg.in/ini.v1 v1.67.0 // indirect
70 | gopkg.in/yaml.v2 v2.4.0 // indirect
71 | gopkg.in/yaml.v3 v3.0.1 // indirect
72 | )
73 |
--------------------------------------------------------------------------------
/core/zap.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "aixinge/global"
5 | "aixinge/utils"
6 | "fmt"
7 | "go.uber.org/zap"
8 | "go.uber.org/zap/zapcore"
9 | "os"
10 | "time"
11 | )
12 |
13 | var level zapcore.Level
14 |
15 | func Zap() (logger *zap.Logger) {
16 | if ok, _ := utils.PathExists(global.CONFIG.Zap.Director); !ok { // 判断是否有Director文件夹
17 | fmt.Printf("create %v directory\n", global.CONFIG.Zap.Director)
18 | _ = os.Mkdir(global.CONFIG.Zap.Director, os.ModePerm)
19 | }
20 |
21 | switch global.CONFIG.Zap.Level { // 初始化配置文件的Level
22 | case "debug":
23 | level = zap.DebugLevel
24 | case "info":
25 | level = zap.InfoLevel
26 | case "warn":
27 | level = zap.WarnLevel
28 | case "error":
29 | level = zap.ErrorLevel
30 | case "dpanic":
31 | level = zap.DPanicLevel
32 | case "panic":
33 | level = zap.PanicLevel
34 | case "fatal":
35 | level = zap.FatalLevel
36 | default:
37 | level = zap.InfoLevel
38 | }
39 |
40 | if level == zap.DebugLevel || level == zap.ErrorLevel {
41 | logger = zap.New(getEncoderCore(), zap.AddStacktrace(level))
42 | } else {
43 | logger = zap.New(getEncoderCore())
44 | }
45 | if global.CONFIG.Zap.ShowLine {
46 | logger = logger.WithOptions(zap.AddCaller())
47 | }
48 | return logger
49 | }
50 |
51 | // getEncoderConfig 获取zapcore.EncoderConfig
52 | func getEncoderConfig() (config zapcore.EncoderConfig) {
53 | config = zapcore.EncoderConfig{
54 | MessageKey: "message",
55 | LevelKey: "level",
56 | TimeKey: "time",
57 | NameKey: "logger",
58 | CallerKey: "caller",
59 | StacktraceKey: global.CONFIG.Zap.StacktraceKey,
60 | LineEnding: zapcore.DefaultLineEnding,
61 | EncodeLevel: zapcore.LowercaseLevelEncoder,
62 | EncodeTime: CustomTimeEncoder,
63 | EncodeDuration: zapcore.SecondsDurationEncoder,
64 | EncodeCaller: zapcore.FullCallerEncoder,
65 | }
66 | switch {
67 | case global.CONFIG.Zap.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认)
68 | config.EncodeLevel = zapcore.LowercaseLevelEncoder
69 | case global.CONFIG.Zap.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色
70 | config.EncodeLevel = zapcore.LowercaseColorLevelEncoder
71 | case global.CONFIG.Zap.EncodeLevel == "CapitalLevelEncoder": // 大写编码器
72 | config.EncodeLevel = zapcore.CapitalLevelEncoder
73 | case global.CONFIG.Zap.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色
74 | config.EncodeLevel = zapcore.CapitalColorLevelEncoder
75 | default:
76 | config.EncodeLevel = zapcore.LowercaseLevelEncoder
77 | }
78 | return config
79 | }
80 |
81 | // getEncoder 获取zapcore.Encoder
82 | func getEncoder() zapcore.Encoder {
83 | if global.CONFIG.Zap.Format == "json" {
84 | return zapcore.NewJSONEncoder(getEncoderConfig())
85 | }
86 | return zapcore.NewConsoleEncoder(getEncoderConfig())
87 | }
88 |
89 | // getEncoderCore 获取Encoder的zapcore.Core
90 | func getEncoderCore() (core zapcore.Core) {
91 | writer, err := utils.GetWriteSyncer() // 使用file-rotateLogs进行日志分割
92 | if err != nil {
93 | fmt.Printf("Get Write Syncer Failed err:%v", err.Error())
94 | return
95 | }
96 | return zapcore.NewCore(getEncoder(), writer, level)
97 | }
98 |
99 | // CustomTimeEncoder 自定义日志输出时间格式
100 | func CustomTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
101 | enc.AppendString(t.Format(global.CONFIG.Zap.Prefix + "2006/01/02 - 15:04:05.000"))
102 | }
103 |
--------------------------------------------------------------------------------
/api/service/system/menu.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/system"
6 | systemReq "aixinge/api/model/system/request"
7 | systemRes "aixinge/api/model/system/response"
8 | "aixinge/global"
9 | "aixinge/utils"
10 | "aixinge/utils/snowflake"
11 | )
12 |
13 | type MenuService struct {
14 | }
15 |
16 | func (c *MenuService) Create(menu system.Menu) error {
17 | menu.ID = utils.Id()
18 | // 状态,1、正常 2、禁用
19 | menu.Status = 1
20 | if menu.ParentId < 2 {
21 | // 设置父类为 1
22 | menu.ParentId = 1
23 | }
24 | return global.DB.Create(&menu).Error
25 | }
26 |
27 | func (c *MenuService) Delete(idsReq request.IdsReq) error {
28 | return global.DB.Delete(&[]system.Menu{}, "id in ?", idsReq.Ids).Error
29 | }
30 |
31 | func (c *MenuService) Update(menu system.Menu) (error, system.Menu) {
32 | if menu.ParentId < 2 {
33 | // 设置父类为 1
34 | menu.ParentId = 1
35 | }
36 | return global.DB.Updates(&menu).Error, menu
37 | }
38 |
39 | func (c *MenuService) GetById(id snowflake.ID) (err error, menu system.Menu) {
40 | err = global.DB.Where("id = ?", id).First(&menu).Error
41 | return err, menu
42 | }
43 |
44 | func (c *MenuService) Page(info request.PageInfo) (err error, list interface{}, total int64) {
45 | limit := info.PageSize
46 | offset := info.PageSize * (info.Page - 1)
47 | db := global.DB.Model(&system.Menu{})
48 | var menuList []system.Menu
49 | err = db.Count(&total).Error
50 | err = db.Order("sort DESC").Limit(limit).Offset(offset).Find(&menuList).Error
51 | return err, menuList, total
52 | }
53 |
54 | func (c *MenuService) List(params systemReq.MenuParams) (err error, list interface{}) {
55 | db := global.DB.Model(&system.Menu{})
56 | if len(params.Name) > 0 {
57 | db.Where("name LIKE ?", params.Name)
58 | }
59 | var menuList []system.Menu
60 | err = db.Select("id", "parent_id", "name").Order("sort").Find(&menuList).Error
61 | return err, menuList
62 | }
63 |
64 | // AuthList 当前登录用户权限菜单
65 | func (c *MenuService) AuthList(customClaims *systemReq.TokenClaims) (err error, list interface{}) {
66 | db := global.DB.Model(&system.Menu{})
67 | var menuList []system.Menu
68 | db.Raw("select m.* from axg_menu m join axg_role_menu r on m.id=r.menu_id join axg_user_role u on r.role_id=u.role_id where m.status=1 and u.user_id=?",
69 | customClaims.ID).Scan(&menuList)
70 | return err, menuList
71 | }
72 |
73 | func (c *MenuService) ListTree(info systemReq.MenuPageParams) (error, []*systemRes.MenuTreeResponse) {
74 | db := global.DB.Model(&system.Menu{})
75 | if info.Title != "" {
76 | db.Where("title like ?", "%"+info.Title+"%")
77 | }
78 |
79 | if info.Status != 0 {
80 | db.Where("status = ?", info.Status)
81 | }
82 | var menuList []system.Menu
83 | _ = db.Order("sort ASC").Find(&menuList).Error
84 | return GetMenuTree(menuList, 0)
85 | }
86 |
87 | // GetMenuTree 递归获取树形菜单
88 | func GetMenuTree(menuList []system.Menu, parentId snowflake.ID) (error, []*systemRes.MenuTreeResponse) {
89 | tree := make([]*systemRes.MenuTreeResponse, 0)
90 | if len(menuList) > 0 {
91 | for _, item := range menuList {
92 | if item.ParentId == parentId {
93 | _, child := GetMenuTree(menuList, item.ID)
94 | node := &systemRes.MenuTreeResponse{
95 | Menu: item,
96 | Children: child,
97 | }
98 | tree = append(tree, node)
99 | }
100 | }
101 | }
102 | return nil, tree
103 | }
104 |
--------------------------------------------------------------------------------
/initialize/gorm.go:
--------------------------------------------------------------------------------
1 | package initialize
2 |
3 | import (
4 | "aixinge/config"
5 | "aixinge/global"
6 | "go.uber.org/zap"
7 | "gorm.io/driver/mysql"
8 | "gorm.io/gorm"
9 | "gorm.io/gorm/clause"
10 | "gorm.io/gorm/logger"
11 | "gorm.io/gorm/schema"
12 | "log"
13 | "os"
14 | "time"
15 | )
16 |
17 | func Gorm() *gorm.DB {
18 | return GormMysql()
19 | }
20 |
21 | func IsMysql() bool {
22 | return global.CONFIG.System.DbType == "mysql"
23 | }
24 |
25 | func GormMysql() *gorm.DB {
26 | m := global.CONFIG.Database
27 | if m.Dbname == "" {
28 | return nil
29 | }
30 | dsn := m.Username + ":" + m.Password + "@tcp(" + m.Path + ")/" + m.Dbname + "?" + m.Config
31 | mysqlConfig := mysql.Config{
32 | DSN: dsn, // DSN data source name
33 | DefaultStringSize: 256, // string 类型字段的默认长度
34 | DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
35 | DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
36 | DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
37 | SkipInitializeWithVersion: false, // 根据版本自动配置
38 |
39 | }
40 | db, err := gorm.Open(mysql.New(mysqlConfig), gormConfig())
41 | return GormInit(db, err, m)
42 | }
43 |
44 | func deleteCallback(db *gorm.DB) {
45 | if db.Error != nil {
46 | return
47 | }
48 |
49 | db.Statement.SQL.Grow(100)
50 | db.Statement.Clauses = make(map[string]clause.Clause)
51 | db.Statement.AddClauseIfNotExists(clause.Update{Table: clause.Table{Name: clause.CurrentTable}})
52 | db.Statement.AddClause(clause.Set{
53 | clause.Assignment{
54 | Column: clause.Column{Name: "deleted_on"}, // Change field name to anything you want
55 | Value: time.Now().Unix(),
56 | },
57 | })
58 | db.Statement.BuildClauses = []string{"UPDATE", "SET"}
59 | db.Statement.Build(db.Statement.BuildClauses...)
60 |
61 | res, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...)
62 | if db.AddError(err) == nil {
63 | db.RowsAffected, _ = res.RowsAffected()
64 | }
65 |
66 | log.Printf("SQL: %v\n", db.Statement.SQL.String()) // Like UPDATE `xxx` SET `deleted_on`=xxx
67 | }
68 |
69 | func GormInit(db *gorm.DB, err error, m config.Database) *gorm.DB {
70 | if err != nil {
71 | global.LOG.Error("database start error", zap.Any("err", err))
72 | os.Exit(0)
73 | return nil
74 | }
75 | sqlDB, _ := db.DB()
76 |
77 | // 设置空闲连接池中连接的最大数量
78 | sqlDB.SetMaxIdleConns(10)
79 | // 设置打开数据库连接的最大数量。
80 | sqlDB.SetMaxOpenConns(100)
81 | // 设置了连接可复用的最大时间。
82 | sqlDB.SetConnMaxLifetime(time.Hour)
83 | return db
84 | }
85 |
86 | func gormConfig() *gorm.Config {
87 | config := &gorm.Config{
88 | DisableForeignKeyConstraintWhenMigrating: true,
89 | NamingStrategy: schema.NamingStrategy{
90 | TablePrefix: "axg_", // 设置表前缀
91 | SingularTable: true, // 使用单数表名
92 | },
93 | }
94 | switch global.CONFIG.Database.LogMode {
95 | case "silent", "Silent":
96 | config.Logger = logger.Default.LogMode(logger.Silent)
97 | case "error", "Error":
98 | config.Logger = logger.Default.LogMode(logger.Error)
99 | case "warn", "Warn":
100 | config.Logger = logger.Default.LogMode(logger.Warn)
101 | case "info", "Info":
102 | config.Logger = logger.Default.LogMode(logger.Info)
103 | default:
104 | config.Logger = logger.Default.LogMode(logger.Info)
105 | }
106 | return config
107 | }
108 |
--------------------------------------------------------------------------------
/middleware/jwt.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "aixinge/api/model/common/response"
5 | "aixinge/api/model/system/request"
6 | "aixinge/global"
7 | "errors"
8 | "github.com/gofiber/fiber/v2"
9 | "github.com/golang-jwt/jwt/v4"
10 | )
11 |
12 | func JWTAuth() fiber.Handler {
13 | return func(c *fiber.Ctx) error {
14 | // jwt鉴权取头部信息 x-token 登录时回返回token信息,前端需要把token存储到cookie或者本地localStorage中
15 | token := c.Get("x-token")
16 | if token == "" {
17 | return response.FailWithMessage("未登录或非法访问", c)
18 | }
19 | j := NewJWT()
20 | // parseToken 解析token包含的信息
21 | claims, err := j.ParseToken(token)
22 | if err != nil {
23 | return response.Result(response.ExpireToken, fiber.Map{"reload": true}, err.Error(), c)
24 | }
25 | // UUID 校验合法性
26 | //if err, _ = service.AppService.SystemService.UserService.GetByUuid(claims.UUID.String()); err != nil {
27 | // return response.FailWithDetailed(fiber.Map{"reload": true}, err.Error(), c)
28 | //}
29 | c.Locals("claims", claims)
30 | return c.Next()
31 | }
32 | }
33 |
34 | type JWT struct {
35 | SigningKey []byte
36 | }
37 |
38 | var (
39 | TokenExpired = errors.New("Token is expired ")
40 | TokenNotValidYet = errors.New("Token not active yet ")
41 | TokenMalformed = errors.New("That's not even a token ")
42 | TokenInvalid = errors.New("Couldn't handle this token: ")
43 | )
44 |
45 | func NewJWT() *JWT {
46 | return &JWT{
47 | []byte(global.CONFIG.JWT.SigningKey),
48 | }
49 | }
50 |
51 | // CreateToken 创建一个token
52 | func (j *JWT) CreateToken(claims jwt.Claims) (string, error) {
53 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
54 | return token.SignedString(j.SigningKey)
55 | }
56 |
57 | func (j *JWT) ParseToken(tokenString string) (*request.TokenClaims, error) {
58 | token, err := jwt.ParseWithClaims(tokenString, &request.TokenClaims{}, func(token *jwt.Token) (i interface{}, e error) {
59 | return j.SigningKey, nil
60 | })
61 | if err != nil {
62 | if ve, ok := err.(*jwt.ValidationError); ok {
63 | if ve.Errors&jwt.ValidationErrorMalformed != 0 {
64 | return nil, TokenMalformed
65 | } else if ve.Errors&jwt.ValidationErrorExpired != 0 {
66 | // Token is expired
67 | return nil, TokenExpired
68 | } else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
69 | return nil, TokenNotValidYet
70 | } else {
71 | return nil, TokenInvalid
72 | }
73 | }
74 | }
75 | if token != nil {
76 | if claims, ok := token.Claims.(*request.TokenClaims); ok && token.Valid {
77 | return claims, nil
78 | }
79 | return nil, TokenInvalid
80 | }
81 | return nil, TokenInvalid
82 | }
83 |
84 | func (j *JWT) ParseRefreshToken(tokenString string) (*request.RefreshTokenClaims, error) {
85 | token, err := jwt.ParseWithClaims(tokenString, &request.RefreshTokenClaims{}, func(token *jwt.Token) (i interface{}, e error) {
86 | return j.SigningKey, nil
87 | })
88 | if err != nil {
89 | if ve, ok := err.(*jwt.ValidationError); ok {
90 | if ve.Errors&jwt.ValidationErrorMalformed != 0 {
91 | return nil, TokenMalformed
92 | } else if ve.Errors&jwt.ValidationErrorExpired != 0 {
93 | // Token is expired
94 | return nil, TokenExpired
95 | } else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
96 | return nil, TokenNotValidYet
97 | } else {
98 | return nil, TokenInvalid
99 | }
100 | }
101 | }
102 | if token != nil {
103 | if claims, ok := token.Claims.(*request.RefreshTokenClaims); ok && token.Valid {
104 | return claims, nil
105 | }
106 | return nil, TokenInvalid
107 | }
108 | return nil, TokenInvalid
109 | }
110 |
--------------------------------------------------------------------------------
/api/v1/message/application.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/common/response"
6 | "aixinge/api/model/message"
7 | messageRes "aixinge/api/model/message/response"
8 | "aixinge/api/model/validation"
9 | "aixinge/global"
10 | "github.com/gofiber/fiber/v2"
11 | "go.uber.org/zap"
12 | )
13 |
14 | type Application struct {
15 | }
16 |
17 | // Create
18 | // @Tags Application
19 | // @Summary 创建应用
20 | // @Security ApiKeyAuth
21 | // @accept application/json
22 | // @Produce application/json
23 | // @Param data body message.Application true "创建应用"
24 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"应用创建成功"}"
25 | // @Router /v1/app/create [post]
26 | func (a *Application) Create(c *fiber.Ctx) error {
27 | var app message.Application
28 | _ = c.BodyParser(&app)
29 | err := applicationService.Create(app)
30 | if err != nil {
31 | global.LOG.Error("创建应用失败!", zap.Any("err", err))
32 | return response.FailWithMessage("应用创建失败:"+err.Error(), c)
33 | }
34 | return response.OkWithMessage("应用创建成功!", c)
35 | }
36 |
37 | // Delete
38 | // @Tags Application
39 | // @Summary 删除应用
40 | // @Security ApiKeyAuth
41 | // @accept application/json
42 | // @Produce application/json
43 | // @Param data body request.IdsReq true "ID集合"
44 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
45 | // @Router /v1/app/delete [post]
46 | func (a *Application) Delete(c *fiber.Ctx) error {
47 | var idsReq request.IdsReq
48 | _ = c.BodyParser(&idsReq)
49 | if err := validation.Verify(idsReq, validation.Id); err != nil {
50 | return response.FailWithMessage(err.Error(), c)
51 | }
52 | if err := applicationService.Delete(idsReq); err != nil {
53 | global.LOG.Error("删除失败!", zap.Any("err", err))
54 | return response.FailWithMessage("删除失败", c)
55 | } else {
56 | return response.OkWithMessage("删除成功", c)
57 | }
58 | }
59 |
60 | // Update
61 | // @Tags Application
62 | // @Summary 更新应用信息
63 | // @Security ApiKeyAuth
64 | // @accept application/json
65 | // @Produce application/json
66 | // @Param data body message.Application true "应用信息"
67 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}"
68 | // @Router /v1/app/update [post]
69 | func (a *Application) Update(c *fiber.Ctx) error {
70 | var app message.Application
71 | _ = c.BodyParser(&app)
72 | if err := validation.Verify(app, validation.Id); err != nil {
73 | return response.FailWithMessage(err.Error(), c)
74 | }
75 |
76 | err, app := applicationService.Update(app)
77 | if err != nil {
78 | global.LOG.Error("更新失败!", zap.Any("err", err))
79 | return response.FailWithMessage("更新失败"+err.Error(), c)
80 | }
81 |
82 | return response.OkWithDetailed(messageRes.AppResponse{Application: app}, "更新成功", c)
83 | }
84 |
85 | // Get
86 | // @Tags Application
87 | // @Summary 根据id获取应用
88 | // @Security ApiKeyAuth
89 | // @accept application/json
90 | // @Produce application/json
91 | // @Param data body request.GetById true "应用ID"
92 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
93 | // @Router /v1/app/get [post]
94 | func (a *Application) Get(c *fiber.Ctx) error {
95 | var idInfo request.GetById
96 | _ = c.BodyParser(&idInfo)
97 | if err := validation.Verify(idInfo, validation.Id); err != nil {
98 | return response.FailWithMessage(err.Error(), c)
99 | }
100 | if err, app := applicationService.GetById(idInfo.ID); err != nil {
101 | global.LOG.Error("获取失败!", zap.Any("err", err))
102 | return response.FailWithMessage("获取失败", c)
103 | } else {
104 | return response.OkWithDetailed(messageRes.AppResponse{Application: app}, "获取成功", c)
105 | }
106 | }
107 |
108 | // Page
109 | // @Tags Application
110 | // @Summary 分页获取消息渠道
111 | // @Security ApiKeyAuth
112 | // @accept application/json
113 | // @Produce application/json
114 | // @Param data body request.PageInfo true "页码, 每页大小"
115 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
116 | // @Router /v1/app/page [post]
117 | func (a *Application) Page(c *fiber.Ctx) error {
118 | var pageInfo request.PageInfo
119 | _ = c.BodyParser(&pageInfo)
120 | if err := validation.Verify(pageInfo, validation.PageInfo); err != nil {
121 | return response.FailWithMessage(err.Error(), c)
122 | }
123 | if err, list, total := applicationService.Page(pageInfo); err != nil {
124 | global.LOG.Error("获取失败!", zap.Any("err", err))
125 | return response.FailWithMessage("获取失败", c)
126 | } else {
127 | return response.OkWithDetailed(response.PageResult{
128 | List: list,
129 | Total: total,
130 | Page: pageInfo.Page,
131 | PageSize: pageInfo.PageSize,
132 | }, "获取成功", c)
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/api/v1/message/channel.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/common/response"
6 | "aixinge/api/model/message"
7 | messageRes "aixinge/api/model/message/response"
8 | "aixinge/api/model/validation"
9 | "aixinge/global"
10 | "github.com/gofiber/fiber/v2"
11 | "go.uber.org/zap"
12 | )
13 |
14 | type Channel struct {
15 | }
16 |
17 | // Create
18 | // @Tags Channel
19 | // @Summary 创建消息渠道
20 | // @Security ApiKeyAuth
21 | // @accept application/json
22 | // @Produce application/json
23 | // @Param data body message.Channel true "创建消息渠道"
24 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"消息渠道创建成功"}"
25 | // @Router /v1/channel/create [post]
26 | func (b *Channel) Create(c *fiber.Ctx) error {
27 | var channel message.Channel
28 | _ = c.BodyParser(&channel)
29 | err := channelService.Create(channel)
30 | if err != nil {
31 | global.LOG.Error("创建消息渠道失败!", zap.Any("err", err))
32 | return response.FailWithMessage("消息渠道创建失败:"+err.Error(), c)
33 | }
34 | return response.OkWithMessage("消息渠道创建成功!", c)
35 | }
36 |
37 | // Delete
38 | // @Tags Channel
39 | // @Summary 删除消息渠道
40 | // @Security ApiKeyAuth
41 | // @accept application/json
42 | // @Produce application/json
43 | // @Param data body request.IdsReq true "ID集合"
44 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
45 | // @Router /v1/channel/delete [post]
46 | func (b *Channel) Delete(c *fiber.Ctx) error {
47 | var idsReq request.IdsReq
48 | _ = c.BodyParser(&idsReq)
49 | if err := validation.Verify(idsReq, validation.Id); err != nil {
50 | return response.FailWithMessage(err.Error(), c)
51 | }
52 | if err := channelService.Delete(idsReq); err != nil {
53 | global.LOG.Error("删除失败!", zap.Any("err", err))
54 | return response.FailWithMessage("删除失败", c)
55 | } else {
56 | return response.OkWithMessage("删除成功", c)
57 | }
58 | }
59 |
60 | // Update
61 | // @Tags Channel
62 | // @Summary 更新消息渠道
63 | // @Security ApiKeyAuth
64 | // @accept application/json
65 | // @Produce application/json
66 | // @Param data body message.Channel true "消息渠道"
67 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}"
68 | // @Router /v1/channel/update [post]
69 | func (b *Channel) Update(c *fiber.Ctx) error {
70 | var channel message.Channel
71 | _ = c.BodyParser(&channel)
72 | if err := validation.Verify(channel, validation.Id); err != nil {
73 | return response.FailWithMessage(err.Error(), c)
74 | }
75 |
76 | err, channel := channelService.Update(channel)
77 | if err != nil {
78 | global.LOG.Error("更新失败!", zap.Any("err", err))
79 | return response.FailWithMessage("更新失败"+err.Error(), c)
80 | }
81 |
82 | return response.OkWithDetailed(messageRes.ChannelResponse{Channel: channel}, "更新成功", c)
83 | }
84 |
85 | // Get
86 | // @Tags Channel
87 | // @Summary 根据id获取消息渠道
88 | // @Security ApiKeyAuth
89 | // @accept application/json
90 | // @Produce application/json
91 | // @Param data body request.GetById true "消息渠道ID"
92 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
93 | // @Router /v1/channel/get [post]
94 | func (b *Channel) Get(c *fiber.Ctx) error {
95 | var idInfo request.GetById
96 | _ = c.BodyParser(&idInfo)
97 | if err := validation.Verify(idInfo, validation.Id); err != nil {
98 | return response.FailWithMessage(err.Error(), c)
99 | }
100 | if err, chl := channelService.GetById(idInfo.ID); err != nil {
101 | global.LOG.Error("获取消息渠道失败!", zap.Any("err", err))
102 | return response.FailWithMessage("获取消息渠道失败", c)
103 | } else {
104 | return response.OkWithDetailed(messageRes.ChannelResponse{Channel: chl}, "获取消息渠道成功", c)
105 | }
106 | }
107 |
108 | // Page
109 | // @Tags Channel
110 | // @Summary 分页获取消息渠道
111 | // @Security ApiKeyAuth
112 | // @accept application/json
113 | // @Produce application/json
114 | // @Param data body request.PageInfo true "页码, 每页大小"
115 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
116 | // @Router /v1/channel/page [post]
117 | func (b *Channel) Page(c *fiber.Ctx) error {
118 | var pageInfo request.PageInfo
119 | _ = c.BodyParser(&pageInfo)
120 | if err := validation.Verify(pageInfo, validation.PageInfo); err != nil {
121 | return response.FailWithMessage(err.Error(), c)
122 | }
123 | if err, list, total := channelService.Page(pageInfo); err != nil {
124 | global.LOG.Error("获取失败!", zap.Any("err", err))
125 | return response.FailWithMessage("获取失败", c)
126 | } else {
127 | return response.OkWithDetailed(response.PageResult{
128 | List: list,
129 | Total: total,
130 | Page: pageInfo.Page,
131 | PageSize: pageInfo.PageSize,
132 | }, "获取成功", c)
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/api/service/system/user.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/system"
6 | systemReq "aixinge/api/model/system/request"
7 | "aixinge/global"
8 | "aixinge/utils"
9 | "aixinge/utils/snowflake"
10 | "errors"
11 |
12 | uuid "github.com/satori/go.uuid"
13 | "gorm.io/gorm"
14 | )
15 |
16 | type UserService struct{}
17 |
18 | func (b *UserService) Create(uc systemReq.UserCreate) (error, system.User) {
19 | var user system.User
20 | if !errors.Is(global.DB.Where("username = ?", uc.Username).First(&user).Error, gorm.ErrRecordNotFound) {
21 | // 判断用户名是否注册
22 | return errors.New("用户名已注册"), user
23 | }
24 | var u system.User
25 | u.ID = utils.Id()
26 | u.UUID = uuid.NewV4()
27 | u.Username = uc.Username
28 | u.Password = utils.GetByteMd5([]byte(uc.Password + u.UUID.String()))
29 | u.Nickname = uc.Nickname
30 | u.Status = 1
31 | return global.DB.Create(&u).Error, u
32 | }
33 |
34 | func (b *UserService) Delete(idsReq request.IdsReq) error {
35 | return global.DB.Delete(&[]system.User{}, "id in ?", idsReq.Ids).Error
36 | }
37 |
38 | func (b *UserService) Update(user system.User) (error, system.User) {
39 | return global.DB.Updates(&user).Error, user
40 | }
41 |
42 | func (b *UserService) Login(u *system.User) (err error, userInter *system.User) {
43 | var user system.User
44 | err = global.DB.Where("username = ?", u.Username).First(&user).Error
45 | pwd := utils.GetByteMd5([]byte(u.Password + user.UUID.String()))
46 | if err != nil || user.Password != pwd {
47 | return errors.New("用户密码错误"), userInter
48 | }
49 | return err, &user
50 | }
51 |
52 | func (b *UserService) ChangePassword(u *system.User, newPassword string) (err error, userInter *system.User) {
53 | var user system.User
54 | err = global.DB.Where("username = ?", u.Username).First(&user).Error
55 | if err != nil || user.Password != utils.GetByteMd5([]byte(u.Password+user.UUID.String())) {
56 | return errors.New("用户密码错误"), userInter
57 | }
58 | // 重置新密码
59 | err = global.DB.Model(&system.User{}).Where("id = ?", user.ID).Update("password", utils.GetByteMd5([]byte(newPassword+user.UUID.String()))).Error
60 | return err, u
61 | }
62 |
63 | func (b *UserService) AssignRole(params systemReq.UserRoleParams) (err error) {
64 | if params.ID < 1 {
65 | return errors.New("用户ID不能为空")
66 | }
67 | if len(params.RoleIds) == 0 {
68 | return errors.New("角色ID集合不能为空")
69 | }
70 | return global.DB.Transaction(func(tx *gorm.DB) error {
71 | db := tx.Model(&system.UserRole{})
72 | err = db.Delete("user_id = ?", params.ID).Error
73 | if err != nil {
74 | return errors.New("分配角色历史数据删除失败")
75 | }
76 | var userRole []system.UserRole
77 | for i := range params.RoleIds {
78 | var ur system.UserRole
79 | ur.UserId = params.ID
80 | ur.RoleId = params.RoleIds[i]
81 | userRole = append(userRole, ur)
82 | }
83 | err = db.CreateInBatches(&userRole, 100).Error
84 | if err != nil {
85 | return errors.New("分配角色保存失败")
86 | }
87 | return nil
88 | })
89 | }
90 |
91 | func (b *UserService) SelectedRoles(id snowflake.ID) (err error, list interface{}) {
92 | var roleIds []snowflake.ID
93 | var userRoleList []system.UserRole
94 | err = global.DB.Where("user_id=?", id).Find(&userRoleList).Error
95 | if len(userRoleList) > 0 {
96 | for i := range userRoleList {
97 | roleIds = append(roleIds, userRoleList[i].RoleId)
98 | }
99 | } else {
100 | roleIds = make([]snowflake.ID, 0)
101 | }
102 | return err, roleIds
103 | }
104 |
105 | func (b *UserService) GetByUuid(uuid string) (err error, user system.User) {
106 | err = global.DB.Where("uuid = ?", uuid).First(&user).Error
107 | return err, user
108 | }
109 |
110 | func (b *UserService) GetById(id snowflake.ID) (err error, user system.User) {
111 | err = global.DB.Where("id = ?", id).First(&user).Error
112 | return err, user
113 | }
114 |
115 | func (b *UserService) Page(info systemReq.UserPageParams) (err error, list interface{}, total int64) {
116 | limit := info.PageSize
117 | offset := info.PageSize * (info.Page - 1)
118 | db := global.DB.Model(&system.User{})
119 |
120 | if info.Username != "" {
121 | db.Where("username like ?", "%"+info.Username+"%")
122 | }
123 |
124 | if info.Status != 0 {
125 | db.Where("status = ?", info.Status)
126 | }
127 |
128 | var userList []system.User
129 | err = db.Count(&total).Error
130 | err = db.Limit(limit).Offset(offset).Find(&userList).Error
131 | return err, userList, total
132 | }
133 |
134 | func (b *UserService) List() (err error, list interface{}) {
135 | db := global.DB.Model(&system.User{})
136 | var userList []system.User
137 | err = db.Where("status=?", 1).Find(&userList).Error
138 | return err, userList
139 | }
140 |
--------------------------------------------------------------------------------
/api/v1/message/mailTemplate.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/common/response"
6 | "aixinge/api/model/message"
7 | messageRes "aixinge/api/model/message/response"
8 | "aixinge/api/model/validation"
9 | "aixinge/global"
10 | "github.com/gofiber/fiber/v2"
11 | "go.uber.org/zap"
12 | )
13 |
14 | type MailTemplate struct {
15 | }
16 |
17 | // Create
18 | // @Tags MailTemplate
19 | // @Summary 创建邮件模板
20 | // @Security ApiKeyAuth
21 | // @accept application/json
22 | // @Produce application/json
23 | // @Param data body message.MailTemplate true "创建邮件模板"
24 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"邮件模板创建成功"}"
25 | // @Router /v1/mail-template/create [post]
26 | func (e *MailTemplate) Create(c *fiber.Ctx) error {
27 | var mt message.MailTemplate
28 | _ = c.BodyParser(&mt)
29 | err := mailTemplateService.Create(mt)
30 | if err != nil {
31 | global.LOG.Error("创建邮件模板失败!", zap.Any("err", err))
32 | return response.FailWithMessage("邮件模板创建失败:"+err.Error(), c)
33 | }
34 | return response.OkWithMessage("邮件模板创建成功!", c)
35 | }
36 |
37 | // Delete
38 | // @Tags MailTemplate
39 | // @Summary 删除邮件模板
40 | // @Security ApiKeyAuth
41 | // @accept application/json
42 | // @Produce application/json
43 | // @Param data body request.IdsReq true "ID集合"
44 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
45 | // @Router /v1/mail-template/delete [post]
46 | func (e *MailTemplate) Delete(c *fiber.Ctx) error {
47 | var idsReq request.IdsReq
48 | _ = c.BodyParser(&idsReq)
49 | if err := validation.Verify(idsReq, validation.Id); err != nil {
50 | return response.FailWithMessage(err.Error(), c)
51 | }
52 | if err := mailTemplateService.Delete(idsReq); err != nil {
53 | global.LOG.Error("删除失败!", zap.Any("err", err))
54 | return response.FailWithMessage("删除失败", c)
55 | } else {
56 | return response.OkWithMessage("删除成功", c)
57 | }
58 | }
59 |
60 | // Update
61 | // @Tags MailTemplate
62 | // @Summary 更新邮件模板
63 | // @Security ApiKeyAuth
64 | // @accept application/json
65 | // @Produce application/json
66 | // @Param data body message.MailTemplate true "邮件模板"
67 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}"
68 | // @Router /v1/mail-template/update [post]
69 | func (e *MailTemplate) Update(c *fiber.Ctx) error {
70 | var mt message.MailTemplate
71 | _ = c.BodyParser(&mt)
72 | if err := validation.Verify(mt, validation.Id); err != nil {
73 | return response.FailWithMessage(err.Error(), c)
74 | }
75 |
76 | err, mt := mailTemplateService.Update(mt)
77 | if err != nil {
78 | global.LOG.Error("更新失败!", zap.Any("err", err))
79 | return response.FailWithMessage("更新失败"+err.Error(), c)
80 | }
81 |
82 | return response.OkWithDetailed(messageRes.MailTemplateResponse{MailTemplate: mt}, "更新成功", c)
83 | }
84 |
85 | // Get
86 | // @Tags MailTemplate
87 | // @Summary 根据id获取邮件模板
88 | // @Security ApiKeyAuth
89 | // @accept application/json
90 | // @Produce application/json
91 | // @Param data body request.GetById true "邮件模板ID"
92 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
93 | // @Router /v1/mail-template/get [post]
94 | func (e *MailTemplate) Get(c *fiber.Ctx) error {
95 | var idInfo request.GetById
96 | _ = c.BodyParser(&idInfo)
97 | if err := validation.Verify(idInfo, validation.Id); err != nil {
98 | return response.FailWithMessage(err.Error(), c)
99 | }
100 | if err, mt := mailTemplateService.GetById(idInfo.ID); err != nil {
101 | global.LOG.Error("获取邮件模板失败!", zap.Any("err", err))
102 | return response.FailWithMessage("获取邮件模板失败", c)
103 | } else {
104 | return response.OkWithDetailed(messageRes.MailTemplateResponse{MailTemplate: mt}, "获取邮件模板成功", c)
105 | }
106 | }
107 |
108 | // Page
109 | // @Tags MailTemplate
110 | // @Summary 分页获取邮件模板
111 | // @Security ApiKeyAuth
112 | // @accept application/json
113 | // @Produce application/json
114 | // @Param data body request.PageInfo true "页码, 每页大小"
115 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
116 | // @Router /v1/mail-template/page [post]
117 | func (e *MailTemplate) Page(c *fiber.Ctx) error {
118 | var pageInfo request.PageInfo
119 | _ = c.BodyParser(&pageInfo)
120 | if err := validation.Verify(pageInfo, validation.PageInfo); err != nil {
121 | return response.FailWithMessage(err.Error(), c)
122 | }
123 | if err, list, total := mailTemplateService.Page(pageInfo); err != nil {
124 | global.LOG.Error("获取失败!", zap.Any("err", err))
125 | return response.FailWithMessage("获取失败", c)
126 | } else {
127 | return response.OkWithDetailed(response.PageResult{
128 | List: list,
129 | Total: total,
130 | Page: pageInfo.Page,
131 | PageSize: pageInfo.PageSize,
132 | }, "获取成功", c)
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/api/service/system/role.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/system"
6 | systemReq "aixinge/api/model/system/request"
7 | "aixinge/global"
8 | "aixinge/utils"
9 | "aixinge/utils/snowflake"
10 | "errors"
11 |
12 | "gorm.io/gorm"
13 | )
14 |
15 | type RoleService struct{}
16 |
17 | func (t *RoleService) Create(role system.Role) error {
18 | role.ID = utils.Id()
19 | role.Status = 1
20 | return global.DB.Create(&role).Error
21 | }
22 |
23 | func (t *RoleService) Delete(idsReq request.IdsReq) error {
24 | return global.DB.Delete(&[]system.Role{}, "id in ?", idsReq.Ids).Error
25 | }
26 |
27 | func (t *RoleService) Update(role system.Role) (error, system.Role) {
28 | return global.DB.Updates(&role).Error, role
29 | }
30 |
31 | func (t *RoleService) AssignUser(params systemReq.RoleUserParams) (err error) {
32 | if params.ID < 1 {
33 | return errors.New("角色ID不能为空")
34 | }
35 | if len(params.UserIds) == 0 {
36 | return errors.New("用户ID集合不能为空")
37 | }
38 | return global.DB.Transaction(func(tx *gorm.DB) error {
39 | db := tx.Model(&system.UserRole{})
40 | err = db.Where("role_id = ?", params.ID).Delete(&system.UserRole{}).Error
41 | if err != nil {
42 | return errors.New("分配用户历史数据删除失败")
43 | }
44 | var userRole []system.UserRole
45 | for i := range params.UserIds {
46 | var rm system.UserRole
47 | rm.RoleId = params.ID
48 | rm.UserId = params.UserIds[i]
49 | userRole = append(userRole, rm)
50 | }
51 | err = db.CreateInBatches(&userRole, 100).Error
52 | if err != nil {
53 | return errors.New("分配用户保存失败")
54 | }
55 | return nil
56 | })
57 | }
58 |
59 | func (t *RoleService) SelectedUsers(id snowflake.ID) (err error, list interface{}) {
60 | var userIds []snowflake.ID
61 | var userRoleList []system.UserRole
62 | err = global.DB.Where("role_id=?", id).Find(&userRoleList).Error
63 | if len(userRoleList) > 0 {
64 | for i := range userRoleList {
65 | userIds = append(userIds, userRoleList[i].UserId)
66 | }
67 | }
68 | return err, userIds
69 | }
70 |
71 | func (t *RoleService) AssignMenu(params systemReq.RoleMenuParams) (err error) {
72 | if params.ID < 1 {
73 | return errors.New("角色ID不能为空")
74 | }
75 | if len(params.MenuIds) == 0 {
76 | return errors.New("菜单ID集合不能为空")
77 | }
78 | return global.DB.Transaction(func(tx *gorm.DB) error {
79 | db := tx.Model(&system.RoleMenu{})
80 | err = db.Where("role_id = ?", params.ID).Delete(&system.RoleMenu{}).Error
81 | if err != nil {
82 | return errors.New("分配菜单历史数据删除失败")
83 | }
84 | var roleMenu []system.RoleMenu
85 | for i := range params.MenuIds {
86 | var rm system.RoleMenu
87 | rm.RoleId = params.ID
88 | rm.MenuId = params.MenuIds[i]
89 | roleMenu = append(roleMenu, rm)
90 | }
91 | err = db.CreateInBatches(&roleMenu, 100).Error
92 | if err != nil {
93 | return errors.New("分配菜单保存失败")
94 | }
95 | return nil
96 | })
97 | }
98 |
99 | func (t *RoleService) SelectedMenus(id snowflake.ID) (err error, list interface{}) {
100 | var menuIds []snowflake.ID
101 | var roleMenuList []system.RoleMenu
102 | err = global.DB.Where("role_id=?", id).Find(&roleMenuList).Error
103 | if len(roleMenuList) > 0 {
104 | for i := range roleMenuList {
105 | menuIds = append(menuIds, roleMenuList[i].MenuId)
106 | }
107 | }
108 | return err, menuIds
109 | }
110 |
111 | func (t *RoleService) SelectedMenusDetail(id snowflake.ID) (err error, list interface{}) {
112 | var menuList []system.Menu
113 | db := global.DB.Model(&system.Menu{})
114 | db.Raw("select m.* from axg_menu m join axg_role_menu r on m.id=r.menu_id where m.status=1 and r.role_id=?", id).Scan(&menuList)
115 | return err, menuList
116 | }
117 |
118 | func (t *RoleService) GetById(id snowflake.ID) (err error, role system.Role) {
119 | err = global.DB.Where("id = ?", id).First(&role).Error
120 | return err, role
121 | }
122 |
123 | func (t *RoleService) GetByIds(idsReq request.IdsReq) (err error, list interface{}) {
124 | var roleList []system.Role
125 | err = global.DB.Where("id in ?", idsReq.Ids).Find(&roleList).Error
126 | return err, roleList
127 | }
128 |
129 | func (t *RoleService) Page(info request.PageInfo) (err error, list interface{}, total int64) {
130 | limit := info.PageSize
131 | offset := info.PageSize * (info.Page - 1)
132 | db := global.DB.Model(&system.Role{})
133 | var roleList []system.Role
134 | err = db.Count(&total).Error
135 | err = db.Order("sort DESC").Limit(limit).Offset(offset).Find(&roleList).Error
136 | return err, roleList, total
137 | }
138 |
139 | func (t *RoleService) List() (err error, list interface{}) {
140 | db := global.DB.Model(&system.Role{})
141 | var roleList []system.Role
142 | err = db.Where("status=?", 1).Order("sort DESC").Find(&roleList).Error
143 | return err, roleList
144 | }
145 |
--------------------------------------------------------------------------------
/wiki/sql/mysql_5.7_20221111.sql:
--------------------------------------------------------------------------------
1 |
2 | SET NAMES utf8;
3 | SET FOREIGN_KEY_CHECKS = 0;
4 |
5 | -- ----------------------------
6 | -- Table structure for axg_menu
7 | -- ----------------------------
8 | DROP TABLE IF EXISTS `axg_menu`;
9 | CREATE TABLE `axg_menu` (
10 | `id` bigint UNSIGNED NOT NULL,
11 | `created_at` datetime NOT NULL,
12 | `updated_at` datetime NULL DEFAULT NULL,
13 | `deleted_at` datetime NULL DEFAULT NULL,
14 | `parent_id` bigint NOT NULL DEFAULT 1,
15 | `path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
16 | `redirect` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
17 | `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
18 | `hidden` tinyint(1) NOT NULL DEFAULT 2,
19 | `component` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
20 | `sort` bigint NULL DEFAULT NULL,
21 | `is_frame` tinyint(1) NOT NULL DEFAULT 2,
22 | `status` bigint NOT NULL DEFAULT 1,
23 | `no_cache` tinyint(1) NOT NULL DEFAULT 1,
24 | `title` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
25 | `icon` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
26 | `remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
27 | PRIMARY KEY (`id`) USING BTREE
28 | ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
29 |
30 |
31 | -- ----------------------------
32 | -- Table structure for axg_role
33 | -- ----------------------------
34 | DROP TABLE IF EXISTS `axg_role`;
35 | CREATE TABLE `axg_role` (
36 | `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
37 | `created_at` datetime NULL DEFAULT NULL,
38 | `updated_at` datetime NULL DEFAULT NULL,
39 | `deleted_at` datetime NULL DEFAULT NULL,
40 | `name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
41 | `alias` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
42 | `remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
43 | `status` bigint NOT NULL DEFAULT 1,
44 | `sort` int NULL DEFAULT NULL,
45 | PRIMARY KEY (`id`) USING BTREE
46 | ) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
47 |
48 | -- ----------------------------
49 | -- Records of axg_role
50 | -- ----------------------------
51 | INSERT INTO `axg_role` VALUES (1, '2022-05-12 11:27:36', '2022-05-12 11:27:36', NULL, '产品经理', 'ttt', 'string', 1, 0);
52 | INSERT INTO `axg_role` VALUES (2, '2022-05-13 08:27:16', '2022-10-30 18:28:33', NULL, '管理员', 'admin', '系统管理员身份', 1, 0);
53 |
54 | -- ----------------------------
55 | -- Table structure for axg_role_menu
56 | -- ----------------------------
57 | DROP TABLE IF EXISTS `axg_role_menu`;
58 | CREATE TABLE `axg_role_menu` (
59 | `role_id` bigint NOT NULL,
60 | `menu_id` bigint NOT NULL
61 | ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
62 |
63 | -- ----------------------------
64 | -- Records of axg_role_menu
65 | -- ----------------------------
66 |
67 | -- ----------------------------
68 | -- Table structure for axg_user
69 | -- ----------------------------
70 | DROP TABLE IF EXISTS `axg_user`;
71 | CREATE TABLE `axg_user` (
72 | `id` bigint UNSIGNED NOT NULL,
73 | `created_at` datetime NULL DEFAULT NULL,
74 | `updated_at` datetime NULL DEFAULT NULL,
75 | `deleted_at` datetime NULL DEFAULT NULL,
76 | `uuid` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
77 | `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
78 | `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
79 | `nick_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '系统用户',
80 | `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
81 | `status` bigint NOT NULL DEFAULT 1,
82 | PRIMARY KEY (`id`) USING BTREE,
83 | INDEX `idx_users_deleted_at`(`deleted_at` ASC) USING BTREE
84 | ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
85 |
86 | -- ----------------------------
87 | -- Records of axg_user
88 | -- ----------------------------
89 | INSERT INTO `axg_user` VALUES (1, '2022-02-15 23:25:20', '2022-11-06 20:26:03', NULL, 'f5f10f02-d4fe-472a-b6ed-c84698a3c007', 'admin', '1751ad04bed81acd2d1bc5720b15f0f5', '超级管理员', '1589232642040004608', 1);
90 |
91 | -- ----------------------------
92 | -- Table structure for axg_user_role
93 | -- ----------------------------
94 | DROP TABLE IF EXISTS `axg_user_role`;
95 | CREATE TABLE `axg_user_role` (
96 | `user_id` bigint UNSIGNED NOT NULL,
97 | `role_id` bigint UNSIGNED NOT NULL
98 | ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
99 |
100 | -- ----------------------------
101 | -- Records of axg_user_role
102 | -- ----------------------------
103 |
104 | SET FOREIGN_KEY_CHECKS = 1;
105 |
--------------------------------------------------------------------------------
/initialize/logger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "aixinge/global"
5 | "context"
6 | "fmt"
7 | "gorm.io/gorm/logger"
8 | "gorm.io/gorm/utils"
9 | "io"
10 | "log"
11 | "os"
12 | "time"
13 | )
14 |
15 | type config struct {
16 | SlowThreshold time.Duration
17 | Colorful bool
18 | LogLevel logger.LogLevel
19 | }
20 |
21 | var (
22 | _ = New(log.New(io.Discard, "", log.LstdFlags), config{})
23 | Default = New(log.New(os.Stdout, "\r\n", log.LstdFlags), config{
24 | SlowThreshold: 200 * time.Millisecond,
25 | LogLevel: logger.Warn,
26 | Colorful: true,
27 | })
28 | _ = traceRecorder{Interface: Default, BeginAt: time.Now()}
29 | )
30 |
31 | func New(writer logger.Writer, config config) logger.Interface {
32 | var (
33 | infoStr = "%s\n[info] "
34 | warnStr = "%s\n[warn] "
35 | errStr = "%s\n[error] "
36 | traceStr = "%s\n[%.3fms] [rows:%v] %s\n"
37 | traceWarnStr = "%s %s\n[%.3fms] [rows:%v] %s\n"
38 | traceErrStr = "%s %s\n[%.3fms] [rows:%v] %s\n"
39 | )
40 |
41 | if config.Colorful {
42 | infoStr = logger.Green + "%s\n" + logger.Reset + logger.Green + "[info] " + logger.Reset
43 | warnStr = logger.BlueBold + "%s\n" + logger.Reset + logger.Magenta + "[warn] " + logger.Reset
44 | errStr = logger.Magenta + "%s\n" + logger.Reset + logger.Red + "[error] " + logger.Reset
45 | traceStr = logger.Green + "%s\n" + logger.Reset + logger.Yellow + "[%.3fms] " + logger.BlueBold + "[rows:%v]" + logger.Reset + " %s\n"
46 | traceWarnStr = logger.Green + "%s " + logger.Yellow + "%s\n" + logger.Reset + logger.RedBold + "[%.3fms] " + logger.Yellow + "[rows:%v]" + logger.Magenta + " %s\n" + logger.Reset
47 | traceErrStr = logger.RedBold + "%s " + logger.MagentaBold + "%s\n" + logger.Reset + logger.Yellow + "[%.3fms] " + logger.BlueBold + "[rows:%v]" + logger.Reset + " %s\n"
48 | }
49 |
50 | return &_logger{
51 | Writer: writer,
52 | config: config,
53 | infoStr: infoStr,
54 | warnStr: warnStr,
55 | errStr: errStr,
56 | traceStr: traceStr,
57 | traceWarnStr: traceWarnStr,
58 | traceErrStr: traceErrStr,
59 | }
60 | }
61 |
62 | type _logger struct {
63 | config
64 | logger.Writer
65 | infoStr, warnStr, errStr string
66 | traceStr, traceErrStr, traceWarnStr string
67 | }
68 |
69 | // LogMode log mode
70 | func (c *_logger) LogMode(level logger.LogLevel) logger.Interface {
71 | newLogger := *c
72 | newLogger.LogLevel = level
73 | return &newLogger
74 | }
75 |
76 | // Info print info
77 | func (c *_logger) Info(_ context.Context, message string, data ...interface{}) {
78 | if c.LogLevel >= logger.Info {
79 | c.Printf(c.infoStr+message, append([]interface{}{utils.FileWithLineNum()}, data...)...)
80 | }
81 | }
82 |
83 | // Warn print warn messages
84 | func (c *_logger) Warn(_ context.Context, message string, data ...interface{}) {
85 | if c.LogLevel >= logger.Warn {
86 | c.Printf(c.warnStr+message, append([]interface{}{utils.FileWithLineNum()}, data...)...)
87 | }
88 | }
89 |
90 | // Error print error messages
91 | func (c *_logger) Error(_ context.Context, message string, data ...interface{}) {
92 | if c.LogLevel >= logger.Error {
93 | c.Printf(c.errStr+message, append([]interface{}{utils.FileWithLineNum()}, data...)...)
94 | }
95 | }
96 |
97 | // Trace print sql message
98 | func (c *_logger) Trace(_ context.Context, begin time.Time, fc func() (string, int64), err error) {
99 | if c.LogLevel > 0 {
100 | elapsed := time.Since(begin)
101 | switch {
102 | case err != nil && c.LogLevel >= logger.Error:
103 | sql, rows := fc()
104 | if rows == -1 {
105 | c.Printf(c.traceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, "-", sql)
106 | } else {
107 | c.Printf(c.traceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, rows, sql)
108 | }
109 | case elapsed > c.SlowThreshold && c.SlowThreshold != 0 && c.LogLevel >= logger.Warn:
110 | sql, rows := fc()
111 | slowLog := fmt.Sprintf("SLOW SQL >= %v", c.SlowThreshold)
112 | if rows == -1 {
113 | c.Printf(c.traceWarnStr, utils.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, "-", sql)
114 | } else {
115 | c.Printf(c.traceWarnStr, utils.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, rows, sql)
116 | }
117 | case c.LogLevel >= logger.Info:
118 | sql, rows := fc()
119 | if rows == -1 {
120 | c.Printf(c.traceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, "-", sql)
121 | } else {
122 | c.Printf(c.traceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, rows, sql)
123 | }
124 | }
125 | }
126 | }
127 |
128 | func (c *_logger) Printf(message string, data ...interface{}) {
129 | if global.CONFIG.Database.LogZap {
130 | global.LOG.Info(fmt.Sprintf(message, data...))
131 | } else {
132 | c.Writer.Printf(message, data...)
133 | }
134 | }
135 |
136 | type traceRecorder struct {
137 | logger.Interface
138 | BeginAt time.Time
139 | SQL string
140 | RowsAffected int64
141 | Err error
142 | }
143 |
144 | func (t traceRecorder) New() *traceRecorder {
145 | return &traceRecorder{Interface: t.Interface, BeginAt: time.Now()}
146 | }
147 |
148 | func (t *traceRecorder) Trace(_ context.Context, begin time.Time, fc func() (string, int64), err error) {
149 | t.BeginAt = begin
150 | t.SQL, t.RowsAffected = fc()
151 | t.Err = err
152 | }
153 |
--------------------------------------------------------------------------------
/api/model/validation/validator.go:
--------------------------------------------------------------------------------
1 | package validation
2 |
3 | import (
4 | "errors"
5 | "reflect"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | type Rules map[string][]string
11 |
12 | type RulesMap map[string]Rules
13 |
14 | var CustomizeMap = make(map[string]Rules)
15 |
16 | // 注册自定义规则方案建议在路由初始化层即注册
17 |
18 | func RegisterRule(key string, rule Rules) (err error) {
19 | if CustomizeMap[key] != nil {
20 | return errors.New(key + "已注册,无法重复注册")
21 | } else {
22 | CustomizeMap[key] = rule
23 | return nil
24 | }
25 | }
26 |
27 | // 非空 不能为其对应类型的0值
28 |
29 | func NotEmpty() string {
30 | return "notEmpty"
31 | }
32 |
33 | // 小于入参(<) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
34 |
35 | func Lt(mark string) string {
36 | return "lt=" + mark
37 | }
38 |
39 | // 小于等于入参(<=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
40 |
41 | func Le(mark string) string {
42 | return "le=" + mark
43 | }
44 |
45 | // 等于入参(==) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
46 |
47 | func Eq(mark string) string {
48 | return "eq=" + mark
49 | }
50 |
51 | // 不等于入参(!=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
52 |
53 | func Ne(mark string) string {
54 | return "ne=" + mark
55 | }
56 |
57 | // 大于等于入参(>=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
58 |
59 | func Ge(mark string) string {
60 | return "ge=" + mark
61 | }
62 |
63 | // 大于入参(>) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
64 |
65 | func Gt(mark string) string {
66 | return "gt=" + mark
67 | }
68 |
69 | // 校验方法
70 |
71 | func Verify(st interface{}, roleMap Rules) (err error) {
72 | compareMap := map[string]bool{
73 | "lt": true,
74 | "le": true,
75 | "eq": true,
76 | "ne": true,
77 | "ge": true,
78 | "gt": true,
79 | }
80 |
81 | typ := reflect.TypeOf(st)
82 | val := reflect.ValueOf(st) // 获取reflect.Type类型
83 |
84 | kd := val.Kind() // 获取到st对应的类别
85 | if kd != reflect.Struct {
86 | return errors.New("expect struct")
87 | }
88 | num := val.NumField()
89 | // 遍历结构体的所有字段
90 | for i := 0; i < num; i++ {
91 | tagVal := typ.Field(i)
92 | val := val.Field(i)
93 | if len(roleMap[tagVal.Name]) > 0 {
94 | for _, v := range roleMap[tagVal.Name] {
95 | switch {
96 | case v == "notEmpty":
97 | if isBlank(val) {
98 | return errors.New(tagVal.Name + "值不能为空")
99 | }
100 | case compareMap[strings.Split(v, "=")[0]]:
101 | if !compareVerify(val, v) {
102 | return errors.New(tagVal.Name + "长度或值不在合法范围," + v)
103 | }
104 | }
105 | }
106 | }
107 | }
108 | return nil
109 | }
110 |
111 | // 长度和数字的校验方法 根据类型自动校验
112 |
113 | func compareVerify(value reflect.Value, VerifyStr string) bool {
114 | switch value.Kind() {
115 | case reflect.String, reflect.Slice, reflect.Array:
116 | return compare(value.Len(), VerifyStr)
117 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
118 | return compare(value.Uint(), VerifyStr)
119 | case reflect.Float32, reflect.Float64:
120 | return compare(value.Float(), VerifyStr)
121 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
122 | return compare(value.Int(), VerifyStr)
123 | default:
124 | return false
125 | }
126 | }
127 |
128 | // 非空校验
129 |
130 | func isBlank(value reflect.Value) bool {
131 | switch value.Kind() {
132 | case reflect.String:
133 | return value.Len() == 0
134 | case reflect.Bool:
135 | return !value.Bool()
136 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
137 | return value.Int() == 0
138 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
139 | return value.Uint() == 0
140 | case reflect.Float32, reflect.Float64:
141 | return value.Float() == 0
142 | case reflect.Interface, reflect.Ptr:
143 | return value.IsNil()
144 | }
145 | return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface())
146 | }
147 |
148 | // 比较函数
149 |
150 | func compare(value interface{}, VerifyStr string) bool {
151 | VerifyStrArr := strings.Split(VerifyStr, "=")
152 | val := reflect.ValueOf(value)
153 | switch val.Kind() {
154 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
155 | VInt, VErr := strconv.ParseInt(VerifyStrArr[1], 10, 64)
156 | if VErr != nil {
157 | return false
158 | }
159 | switch {
160 | case VerifyStrArr[0] == "lt":
161 | return val.Int() < VInt
162 | case VerifyStrArr[0] == "le":
163 | return val.Int() <= VInt
164 | case VerifyStrArr[0] == "eq":
165 | return val.Int() == VInt
166 | case VerifyStrArr[0] == "ne":
167 | return val.Int() != VInt
168 | case VerifyStrArr[0] == "ge":
169 | return val.Int() >= VInt
170 | case VerifyStrArr[0] == "gt":
171 | return val.Int() > VInt
172 | default:
173 | return false
174 | }
175 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
176 | VInt, VErr := strconv.Atoi(VerifyStrArr[1])
177 | if VErr != nil {
178 | return false
179 | }
180 | switch {
181 | case VerifyStrArr[0] == "lt":
182 | return val.Uint() < uint64(VInt)
183 | case VerifyStrArr[0] == "le":
184 | return val.Uint() <= uint64(VInt)
185 | case VerifyStrArr[0] == "eq":
186 | return val.Uint() == uint64(VInt)
187 | case VerifyStrArr[0] == "ne":
188 | return val.Uint() != uint64(VInt)
189 | case VerifyStrArr[0] == "ge":
190 | return val.Uint() >= uint64(VInt)
191 | case VerifyStrArr[0] == "gt":
192 | return val.Uint() > uint64(VInt)
193 | default:
194 | return false
195 | }
196 | case reflect.Float32, reflect.Float64:
197 | VFloat, VErr := strconv.ParseFloat(VerifyStrArr[1], 64)
198 | if VErr != nil {
199 | return false
200 | }
201 | switch {
202 | case VerifyStrArr[0] == "lt":
203 | return val.Float() < VFloat
204 | case VerifyStrArr[0] == "le":
205 | return val.Float() <= VFloat
206 | case VerifyStrArr[0] == "eq":
207 | return val.Float() == VFloat
208 | case VerifyStrArr[0] == "ne":
209 | return val.Float() != VFloat
210 | case VerifyStrArr[0] == "ge":
211 | return val.Float() >= VFloat
212 | case VerifyStrArr[0] == "gt":
213 | return val.Float() > VFloat
214 | default:
215 | return false
216 | }
217 | default:
218 | return false
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/api/v1/system/menu.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/common/response"
6 | "aixinge/api/model/system"
7 | systemReq "aixinge/api/model/system/request"
8 | systemRes "aixinge/api/model/system/response"
9 | "aixinge/api/model/validation"
10 | "aixinge/global"
11 | "github.com/gofiber/fiber/v2"
12 | "go.uber.org/zap"
13 | )
14 |
15 | type Menu struct {
16 | }
17 |
18 | // Create
19 | // @Tags Menu
20 | // @Summary 创建菜单
21 | // @Security ApiKeyAuth
22 | // @accept application/json
23 | // @Produce application/json
24 | // @Param data body system.Menu true "创建菜单"
25 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
26 | // @Router /v1/menu/create [post]
27 | func (b *Menu) Create(c *fiber.Ctx) error {
28 | var menu system.Menu
29 | _ = c.BodyParser(&menu)
30 | err := menuService.Create(menu)
31 | if err != nil {
32 | global.LOG.Error("注册失败!", zap.Any("err", err))
33 | return response.FailWithMessage("创建失败"+err.Error(), c)
34 | }
35 | return response.OkWithMessage("创建成功", c)
36 | }
37 |
38 | // Delete
39 | // @Tags Menu
40 | // @Summary 删除菜单
41 | // @Security ApiKeyAuth
42 | // @accept application/json
43 | // @Produce application/json
44 | // @Param data body request.IdsReq true "ID集合"
45 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
46 | // @Router /v1/menu/delete [post]
47 | func (b *Menu) Delete(c *fiber.Ctx) error {
48 | var idsReq request.IdsReq
49 | _ = c.BodyParser(&idsReq)
50 | if err := validation.Verify(idsReq, validation.Id); err != nil {
51 | return response.FailWithMessage(err.Error(), c)
52 | }
53 | if err := menuService.Delete(idsReq); err != nil {
54 | global.LOG.Error("删除失败!", zap.Any("err", err))
55 | return response.FailWithMessage("删除失败", c)
56 | } else {
57 | return response.OkWithMessage("删除成功", c)
58 | }
59 | }
60 |
61 | // Update
62 | // @Tags Menu
63 | // @Summary 更新菜单信息
64 | // @Security ApiKeyAuth
65 | // @accept application/json
66 | // @Produce application/json
67 | // @Param data body system.Menu true "菜单信息"
68 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}"
69 | // @Router /v1/menu/update [post]
70 | func (b *Menu) Update(c *fiber.Ctx) error {
71 | var menu system.Menu
72 | _ = c.BodyParser(&menu)
73 | if err := validation.Verify(menu, validation.Id); err != nil {
74 | return response.FailWithMessage(err.Error(), c)
75 | }
76 |
77 | err, menu := menuService.Update(menu)
78 | if err != nil {
79 | global.LOG.Error("更新失败!", zap.Any("err", err))
80 | return response.FailWithMessage("更新失败"+err.Error(), c)
81 | }
82 |
83 | return response.OkWithDetailed(systemRes.MenuResponse{Menu: menu}, "更新成功", c)
84 | }
85 |
86 | // Get
87 | // @Tags Menu
88 | // @Summary 根据id获取菜单
89 | // @Security ApiKeyAuth
90 | // @accept application/json
91 | // @Produce application/json
92 | // @Param data body request.GetById true "菜单ID"
93 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
94 | // @Router /v1/menu/get [post]
95 | func (b *Menu) Get(c *fiber.Ctx) error {
96 | var idInfo request.GetById
97 | _ = c.BodyParser(&idInfo)
98 | if err := validation.Verify(idInfo, validation.Id); err != nil {
99 | return response.FailWithMessage(err.Error(), c)
100 | }
101 | if err, menu := menuService.GetById(idInfo.ID); err != nil {
102 | global.LOG.Error("获取失败!", zap.Any("err", err))
103 | return response.FailWithMessage("获取失败", c)
104 | } else {
105 | return response.OkWithDetailed(systemRes.MenuResponse{Menu: menu}, "获取成功", c)
106 | }
107 | }
108 |
109 | // Page
110 | // @Tags Menu
111 | // @Summary 分页获取菜单列表
112 | // @Security ApiKeyAuth
113 | // @accept application/json
114 | // @Produce application/json
115 | // @Param data body request.PageInfo true "页码, 每页大小"
116 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
117 | // @Router /v1/menu/page [post]
118 | func (b *Menu) Page(c *fiber.Ctx) error {
119 | var pageInfo request.PageInfo
120 | _ = c.BodyParser(&pageInfo)
121 | if err := validation.Verify(pageInfo, validation.PageInfo); err != nil {
122 | return response.FailWithMessage(err.Error(), c)
123 | }
124 | if err, list, total := menuService.Page(pageInfo); err != nil {
125 | global.LOG.Error("获取失败!", zap.Any("err", err))
126 | return response.FailWithMessage("获取失败", c)
127 | } else {
128 | return response.OkWithDetailed(response.PageResult{
129 | List: list,
130 | Total: total,
131 | Page: pageInfo.Page,
132 | PageSize: pageInfo.PageSize,
133 | }, "获取成功", c)
134 | }
135 | }
136 |
137 | // List
138 | // @Tags Menu
139 | // @Summary 获取菜单列表
140 | // @Security ApiKeyAuth
141 | // @accept application/json
142 | // @Produce application/json
143 | // @Param data body systemReq.MenuParams true "查询参数"
144 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
145 | // @Router /v1/menu/list [post]
146 | func (b *Menu) List(c *fiber.Ctx) error {
147 | var params systemReq.MenuParams
148 | _ = c.BodyParser(¶ms)
149 | if err, list := menuService.List(params); err != nil {
150 | global.LOG.Error("获取失败!", zap.Any("err", err))
151 | return response.FailWithMessage("获取失败", c)
152 | } else {
153 | return response.OkWithDetailed(list, "获取成功", c)
154 | }
155 | }
156 |
157 | // Auth
158 | // @Tags Menu
159 | // @Summary 获取当前登录用户授权菜单
160 | // @Security ApiKeyAuth
161 | // @accept application/json
162 | // @Produce application/json
163 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
164 | // @Router /v1/menu/auth [post]
165 | func (b *Menu) Auth(c *fiber.Ctx) error {
166 | if err, list := menuService.AuthList(systemReq.GetUserInfo(c)); err != nil {
167 | global.LOG.Error("获取失败!", zap.Any("err", err))
168 | return response.FailWithMessage("获取失败", c)
169 | } else {
170 | return response.OkWithDetailed(list, "获取成功", c)
171 | }
172 | }
173 |
174 | // ListTree
175 | // @Tags Menu
176 | // @Summary 获取菜单列表树
177 | // @Security ApiKeyAuth
178 | // @accept application/json
179 | // @Produce application/json
180 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
181 | // @Router /v1/menu/list-tree [post]
182 | func (b *Menu) ListTree(c *fiber.Ctx) error {
183 | var pageInfo systemReq.MenuPageParams
184 | _ = c.BodyParser(&pageInfo)
185 | if err, listTree := menuService.ListTree(pageInfo); err != nil {
186 | global.LOG.Error("获取失败!", zap.Any("err", err))
187 | return response.FailWithMessage("获取失败", c)
188 | } else {
189 | return response.OkWithDetailed(listTree, "获取成功", c)
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/wiki/sql/mysql_20221122.sql:
--------------------------------------------------------------------------------
1 | create table if not exists axg_application
2 | (
3 | id bigint unsigned not null comment '主键 ID' primary key,
4 | created_at datetime null comment '创建时间',
5 | updated_at datetime null comment '更新时间',
6 | deleted_at datetime null comment '删除时间',
7 | name varchar(30) null comment '应用名称',
8 | app_key varchar(64) null comment '应用 KEY',
9 | app_secret varchar(64) null comment '应用密钥',
10 | remark varchar(255) null comment '应用备注',
11 | status int default 1 not null comment '状态(0-禁用、1-正常)'
12 | ) comment '应用信息表';
13 |
14 | create table if not exists axg_attachment
15 | (
16 | id bigint unsigned not null comment '文扩展名' primary key,
17 | created_at datetime not null comment '创建时间',
18 | updated_at datetime null comment '更新时间',
19 | deleted_at datetime null comment '删除时间',
20 | name varchar(255) null comment '文件名称',
21 | md5 varchar(255) not null comment 'MD5',
22 | path varchar(255) not null comment '存储路径',
23 | ext varchar(255) null comment '扩展名',
24 | content_type varchar(255) null comment 'ContentType',
25 | etag varchar(255) null comment 'HTTP ETag',
26 | data varchar(255) null comment '扩展数据'
27 | ) comment '上传附件表';
28 |
29 | create table if not exists axg_channel
30 | (
31 | id bigint unsigned not null comment '主键 ID' primary key,
32 | created_at datetime null comment '创建时间',
33 | updated_at datetime null comment '更新时间',
34 | deleted_at datetime null comment '删除时间',
35 | name varchar(30) null comment '消息渠道名称',
36 | type int null comment '消息类型(枚举)',
37 | provider int null comment '消息服务提供商',
38 | weight int default 0 not null comment '权重',
39 | config json null comment '消息渠道配置(JSON)',
40 | remark varchar(255) null comment '备注',
41 | status int default 1 not null comment '状态(0-禁用、1-正常)'
42 | ) comment '消息渠道配置表';
43 |
44 | create table if not exists axg_channel_template
45 | (
46 | channel_id bigint unsigned not null comment '渠道 ID',
47 | template_id bigint unsigned not null comment '模板 ID',
48 | type int not null comment '消息类型',
49 | `default` int not null comment '是否为默认渠道'
50 | ) comment '渠道与消息模板关联表';
51 |
52 | create table if not exists axg_mail_log
53 | (
54 | id bigint unsigned not null comment '主键 ID' primary key,
55 | created_at datetime null comment '创建时间',
56 | updated_at datetime null comment '更新时间',
57 | deleted_at datetime null comment '删除时间',
58 | app_id bigint not null comment '应用 ID',
59 | template_id bigint not null comment '邮件模板 ID',
60 | request_id varchar(255) null comment '唯一请求 ID',
61 | `to` json null comment '发件地址集合',
62 | cc json null comment '抄送地址集合',
63 | bcc json null comment '密送地址集合',
64 | parameters json null comment '邮件参数',
65 | content longtext null comment '邮件具体内容',
66 | attachments json not null comment '附件ID集合',
67 | status int null comment '发送状态',
68 | err_msg varchar(255) null comment '错误日志'
69 | ) comment '邮件发送日志表';
70 |
71 | create table if not exists axg_mail_template
72 | (
73 | id bigint unsigned not null comment '主键 ID' primary key,
74 | created_at datetime null comment '创建时间',
75 | updated_at datetime null comment '更新时间',
76 | deleted_at datetime null comment '删除时间',
77 | app_id bigint not null comment '应用 ID',
78 | name varchar(30) null comment '模板名称',
79 | content longtext null comment '模板内容',
80 | type int null comment '模板类型(1-文本、2-HTML)',
81 | attachments json not null comment '附件ID集合',
82 | remark varchar(255) null comment '备注',
83 | status int default 1 not null comment '状态(0-禁用、1-正常)'
84 | ) comment '邮件模板配置表';
85 |
86 | create table if not exists axg_menu
87 | (
88 | id bigint unsigned not null comment '主键 ID' primary key,
89 | created_at datetime not null comment '创建时间',
90 | updated_at datetime null comment '更新时间',
91 | deleted_at datetime null comment '删除时间',
92 | parent_id bigint default 1 not null comment '父级 ID',
93 | path varchar(255) charset utf8 null comment '菜单路径',
94 | redirect varchar(255) null,
95 | name varchar(100) charset utf8 null comment '菜单名称',
96 | hidden tinyint(1) default 2 not null comment '是否隐藏',
97 | component varchar(255) charset utf8 null comment '对应组件',
98 | sort bigint null comment '排序号',
99 | is_frame tinyint(1) default 2 not null comment '是否为 iframe',
100 | status bigint default 1 not null comment '状态(0-禁用、1-正常)',
101 | no_cache tinyint(1) default 1 not null comment '是否禁用缓存',
102 | title varchar(100) charset utf8 null comment '标题',
103 | icon varchar(50) charset utf8 null comment '图标',
104 | remark varchar(255) null comment '备注'
105 | ) comment '菜单表';
106 |
107 | create table if not exists axg_role
108 | (
109 | id bigint unsigned auto_increment comment '主键 ID' primary key,
110 | created_at datetime null comment '创建时间',
111 | updated_at datetime null comment '更新时间',
112 | deleted_at datetime null comment '删除时间',
113 | name varchar(30) null comment '角色名称',
114 | alias varchar(30) null comment '角色别名',
115 | remark varchar(255) null comment '备注',
116 | status bigint default 1 not null comment '状态(0-禁用、1-正常)',
117 | sort int null comment '排序号'
118 | ) comment '角色表';
119 |
120 | create table if not exists axg_role_menu
121 | (
122 | role_id bigint not null comment '角色 ID',
123 | menu_id bigint not null comment '菜单 ID'
124 | ) comment '角色菜单关联表';
125 |
126 | create table if not exists axg_sms_log
127 | (
128 | id bigint unsigned not null comment '主键 ID' primary key,
129 | created_at datetime null comment '创建时间',
130 | updated_at datetime null comment '更新时间',
131 | deleted_at datetime null comment '删除时间',
132 | app_id bigint null comment '应用 ID',
133 | template_id bigint null comment '模板 ID',
134 | request_id varchar(255) null comment '唯一请求 ID',
135 | `to` json null comment '接收号码集合',
136 | parameters json null comment '模板参数',
137 | content tinytext null comment '短信内容',
138 | remark varchar(255) null comment '备注',
139 | status int null comment '发送状态',
140 | err_msg varchar(255) null comment '错误日志'
141 | ) comment '短信发送日志表';
142 |
143 | create table if not exists axg_sms_signature
144 | (
145 | id bigint unsigned not null comment '主键 ID' primary key,
146 | created_at datetime null comment '创建时间',
147 | updated_at datetime null comment '更新时间',
148 | deleted_at datetime null comment '删除时间',
149 | app_id bigint null comment '应用 ID',
150 | name varchar(30) null comment '签名名称',
151 | config json null comment '签名所需额外配置',
152 | remark varchar(255) null comment '备注',
153 | status int default 1 not null comment '状态(0-禁用、1-正常)'
154 | ) comment '短信签名表';
155 |
156 | create table if not exists axg_sms_template
157 | (
158 | id bigint unsigned not null comment '主键 ID' primary key,
159 | created_at datetime null comment '创建时间',
160 | updated_at datetime null comment '更新时间',
161 | deleted_at datetime null comment '删除时间',
162 | app_id bigint null comment '应用 ID',
163 | type int null comment '消息类型(1-验证码、2-通知短信、3-推广短信)',
164 | name varchar(30) null comment '消息模板名称',
165 | content tinytext not null comment '模板内容',
166 | sign_id bigint null comment '关联签名 ID',
167 | config json null comment '模板所需额外配置',
168 | remark varchar(255) null comment '备注',
169 | status int default 1 not null comment '状态(0-禁用、1-正常)'
170 | ) comment '短信模板配置表';
171 |
172 | create table if not exists axg_user
173 | (
174 | id bigint unsigned not null comment '主键 ID' primary key,
175 | created_at datetime null comment '创建时间',
176 | updated_at datetime null comment '更新时间',
177 | deleted_at datetime null comment '删除时间',
178 | uuid varchar(255) null comment '唯一标识',
179 | username varchar(255) null comment '用户名',
180 | password varchar(255) null comment '密码',
181 | nick_name varchar(255) default '系统用户' null comment '昵称',
182 | avatar varchar(255) null comment '头像',
183 | status bigint default 1 not null comment '状态(0-禁用、1-正常)'
184 | ) comment '用户表' charset = utf8;
185 |
186 | create index idx_users_deleted_at on axg_user (deleted_at);
187 |
188 | create table if not exists axg_user_role
189 | (
190 | user_id bigint unsigned not null comment '用户 ID',
191 | role_id bigint unsigned not null comment '角色 ID'
192 | ) comment '用户角色关联表';
193 |
--------------------------------------------------------------------------------
/api/v1/system/role.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/common/response"
6 | "aixinge/api/model/system"
7 | systemReq "aixinge/api/model/system/request"
8 | systemRes "aixinge/api/model/system/response"
9 | "aixinge/api/model/validation"
10 | "aixinge/global"
11 | "github.com/gofiber/fiber/v2"
12 | "go.uber.org/zap"
13 | )
14 |
15 | type Role struct {
16 | }
17 |
18 | // Create
19 | // @Tags Role
20 | // @Summary 创建角色
21 | // @Security ApiKeyAuth
22 | // @accept application/json
23 | // @Produce application/json
24 | // @Param data body system.Role true "创建角色"
25 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
26 | // @Router /v1/role/create [post]
27 | func (b *Role) Create(c *fiber.Ctx) error {
28 | var role system.Role
29 | _ = c.BodyParser(&role)
30 |
31 | if err := roleService.Create(role); err != nil {
32 | global.LOG.Error("创建失败!", zap.Any("err", err))
33 | return response.FailWithMessage("创建失败", c)
34 | }
35 |
36 | return response.OkWithMessage("创建成功", c)
37 | }
38 |
39 | // Delete
40 | // @Tags Role
41 | // @Summary 删除角色
42 | // @Security ApiKeyAuth
43 | // @accept application/json
44 | // @Produce application/json
45 | // @Param data body request.IdsReq true "ID集合"
46 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
47 | // @Router /v1/role/delete [post]
48 | func (b *Role) Delete(c *fiber.Ctx) error {
49 | var idsReq request.IdsReq
50 | _ = c.BodyParser(&idsReq)
51 | if err := validation.Verify(idsReq, validation.Id); err != nil {
52 | return response.FailWithMessage(err.Error(), c)
53 | }
54 | if err := roleService.Delete(idsReq); err != nil {
55 | global.LOG.Error("删除失败!", zap.Any("err", err))
56 | return response.FailWithMessage("删除失败", c)
57 | } else {
58 | return response.OkWithMessage("删除成功", c)
59 | }
60 | }
61 |
62 | // Update
63 | // @Tags Role
64 | // @Summary 更新角色信息
65 | // @Security ApiKeyAuth
66 | // @accept application/json
67 | // @Produce application/json
68 | // @Param data body system.Role true "角色信息"
69 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}"
70 | // @Router /v1/role/update [post]
71 | func (b *Role) Update(c *fiber.Ctx) error {
72 | var role system.Role
73 | _ = c.BodyParser(&role)
74 | if err := validation.Verify(role, validation.Id); err != nil {
75 | return response.FailWithMessage(err.Error(), c)
76 | }
77 |
78 | err, role := roleService.Update(role)
79 | if err != nil {
80 | global.LOG.Error("更新失败!", zap.Any("err", err))
81 | return response.FailWithMessage("更新失败"+err.Error(), c)
82 | }
83 |
84 | return response.OkWithDetailed(systemRes.RoleResponse{Role: role}, "更新成功", c)
85 | }
86 |
87 | // AssignUser
88 | // @Tags Role
89 | // @Summary 角色分配用户
90 | // @Security ApiKeyAuth
91 | // @accept application/json
92 | // @Produce application/json
93 | // @Param data body systemReq.RoleUserParams true "角色ID、用户ID集合"
94 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
95 | // @Router /v1/role/assign-user [post]
96 | func (b *Role) AssignUser(c *fiber.Ctx) error {
97 | var params systemReq.RoleUserParams
98 | _ = c.BodyParser(¶ms)
99 | if err := roleService.AssignUser(params); err != nil {
100 | global.LOG.Error("角色分配用户失败!", zap.Any("err", err))
101 | return response.FailWithMessage(err.Error(), c)
102 | }
103 | return response.OkWithMessage("获取成功", c)
104 | }
105 |
106 | // SelectedUsers
107 | // @Tags Role
108 | // @Summary 根据id获取角色已分配的用户ID列表
109 | // @Security ApiKeyAuth
110 | // @accept application/json
111 | // @Produce application/json
112 | // @Param data body request.GetById true "角色ID"
113 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
114 | // @Router /v1/role/selected-users [post]
115 | func (b *Role) SelectedUsers(c *fiber.Ctx) error {
116 | var idInfo request.GetById
117 | _ = c.BodyParser(&idInfo)
118 | if err := validation.Verify(idInfo, validation.Id); err != nil {
119 | return response.FailWithMessage(err.Error(), c)
120 | }
121 | if err, userIds := roleService.SelectedUsers(idInfo.ID); err != nil {
122 | global.LOG.Error("获取角色分配用户ID列表失败", zap.Any("err", err))
123 | return response.FailWithMessage("获取失败", c)
124 | } else {
125 | return response.OkWithDetailed(userIds, "获取成功", c)
126 | }
127 | }
128 |
129 | // AssignMenu
130 | // @Tags Role
131 | // @Summary 角色分配菜单
132 | // @Security ApiKeyAuth
133 | // @accept application/json
134 | // @Produce application/json
135 | // @Param data body systemReq.RoleMenuParams true "角色ID、菜单ID集合"
136 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
137 | // @Router /v1/role/assign-menu [post]
138 | func (b *Role) AssignMenu(c *fiber.Ctx) error {
139 | var params systemReq.RoleMenuParams
140 | _ = c.BodyParser(¶ms)
141 | if err := roleService.AssignMenu(params); err != nil {
142 | global.LOG.Error("角色分配菜单失败!", zap.Any("err", err))
143 | return response.FailWithMessage("获取失败", c)
144 | }
145 | return response.OkWithMessage("获取成功", c)
146 | }
147 |
148 | // SelectedMenus
149 | // @Tags Role
150 | // @Summary 根据id获取角色已分配的菜单ID列表
151 | // @Security ApiKeyAuth
152 | // @accept application/json
153 | // @Produce application/json
154 | // @Param data body request.GetById true "角色ID"
155 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
156 | // @Router /v1/role/selected-menus [post]
157 | func (b *Role) SelectedMenus(c *fiber.Ctx) error {
158 | var idInfo request.GetById
159 | _ = c.BodyParser(&idInfo)
160 | if err := validation.Verify(idInfo, validation.Id); err != nil {
161 | return response.FailWithMessage(err.Error(), c)
162 | }
163 | if err, menuIds := roleService.SelectedMenus(idInfo.ID); err != nil {
164 | global.LOG.Error("获取角色分配菜单ID列表失败", zap.Any("err", err))
165 | return response.FailWithMessage("获取失败", c)
166 | } else {
167 | return response.OkWithDetailed(menuIds, "获取成功", c)
168 | }
169 | }
170 |
171 | // SelectedMenusDetail
172 | // @Tags Role
173 | // @Summary 根据id获取角色已分配的菜单详细信息列表
174 | // @Security ApiKeyAuth
175 | // @accept application/json
176 | // @Produce application/json
177 | // @Param data body request.GetById true "角色ID"
178 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
179 | // @Router /v1/role/selected-menus [post]
180 | func (b *Role) SelectedMenusDetail(c *fiber.Ctx) error {
181 | var idInfo request.GetById
182 | _ = c.BodyParser(&idInfo)
183 | if err := validation.Verify(idInfo, validation.Id); err != nil {
184 | return response.FailWithMessage(err.Error(), c)
185 | }
186 | if err, menuList := roleService.SelectedMenusDetail(idInfo.ID); err != nil {
187 | global.LOG.Error("获取角色分配菜单ID列表失败", zap.Any("err", err))
188 | return response.FailWithMessage("获取失败", c)
189 | } else {
190 | return response.OkWithDetailed(menuList, "获取成功", c)
191 | }
192 | }
193 |
194 | // Get
195 | // @Tags Role
196 | // @Summary 根据id获取角色
197 | // @Security ApiKeyAuth
198 | // @accept application/json
199 | // @Produce application/json
200 | // @Param data body request.GetById true "角色ID"
201 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
202 | // @Router /v1/role/get [post]
203 | func (b *Role) Get(c *fiber.Ctx) error {
204 | var idInfo request.GetById
205 | _ = c.BodyParser(&idInfo)
206 | if err := validation.Verify(idInfo, validation.Id); err != nil {
207 | return response.FailWithMessage(err.Error(), c)
208 | }
209 | if err, role := roleService.GetById(idInfo.ID); err != nil {
210 | global.LOG.Error("获取失败!", zap.Any("err", err))
211 | return response.FailWithMessage("获取失败", c)
212 | } else {
213 | return response.OkWithDetailed(systemRes.RoleResponse{Role: role}, "获取成功", c)
214 | }
215 | }
216 |
217 | // BatchGet
218 | // @Tags Role
219 | // @Summary 批量根据id获取角色
220 | // @Security ApiKeyAuth
221 | // @accept application/json
222 | // @Produce application/json
223 | // @Param data body request.IdsReq true "角色ID列表"
224 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
225 | // @Router /v1/role/batch-get [post]
226 | func (b *Role) BatchGet(c *fiber.Ctx) error {
227 | var idsReq request.IdsReq
228 | _ = c.BodyParser(&idsReq)
229 | if err := validation.Verify(idsReq, validation.Id); err != nil {
230 | return response.FailWithMessage(err.Error(), c)
231 | }
232 | if err, list := roleService.GetByIds(idsReq); err != nil {
233 | global.LOG.Error("获取失败!", zap.Any("err", err))
234 | return response.FailWithMessage("获取失败", c)
235 | } else {
236 | return response.OkWithDetailed(list, "获取成功", c)
237 | }
238 | }
239 |
240 | // Page
241 | // @Tags Role
242 | // @Summary 分页获取角色列表
243 | // @Security ApiKeyAuth
244 | // @accept application/json
245 | // @Produce application/json
246 | // @Param data body request.PageInfo true "页码, 每页大小"
247 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
248 | // @Router /v1/role/page [post]
249 | func (b *Role) Page(c *fiber.Ctx) error {
250 | var pageInfo request.PageInfo
251 | _ = c.BodyParser(&pageInfo)
252 | if err := validation.Verify(pageInfo, validation.PageInfo); err != nil {
253 | return response.FailWithMessage(err.Error(), c)
254 | }
255 | if err, list, total := roleService.Page(pageInfo); err != nil {
256 | global.LOG.Error("获取失败!", zap.Any("err", err))
257 | return response.FailWithMessage("获取失败", c)
258 | } else {
259 | return response.OkWithDetailed(response.PageResult{
260 | List: list,
261 | Total: total,
262 | Page: pageInfo.Page,
263 | PageSize: pageInfo.PageSize,
264 | }, "获取成功", c)
265 | }
266 | }
267 |
268 | // List
269 | // @Tags Role
270 | // @Summary 获取角色列表
271 | // @Security ApiKeyAuth
272 | // @accept application/json
273 | // @Produce application/json
274 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
275 | // @Router /v1/role/list [post]
276 | func (b *Role) List(c *fiber.Ctx) error {
277 | if err, list := roleService.List(); err != nil {
278 | global.LOG.Error("获取失败!", zap.Any("err", err))
279 | return response.FailWithMessage("获取失败", c)
280 | } else {
281 | return response.OkWithDetailed(list, "获取成功", c)
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/utils/snowflake/snowflake.go:
--------------------------------------------------------------------------------
1 | // Package snowflake provides a very simple Twitter snowflake generator and parser.
2 | package snowflake
3 |
4 | import (
5 | "encoding/base64"
6 | "encoding/binary"
7 | "errors"
8 | "fmt"
9 | "strconv"
10 | "sync"
11 | "time"
12 | )
13 |
14 | // https://github.com/bwmarrin/snowflake v0.3.0
15 | var (
16 | // Epoch is set to the twitter snowflake epoch of Nov 04 2010 01:42:54 UTC in milliseconds
17 | // You may customize this to set a different epoch for your application.
18 | Epoch int64 = 1288834974657
19 |
20 | // NodeBits holds the number of bits to use for Node
21 | // Remember, you have a total 22 bits to share between Node/Step
22 | NodeBits uint8 = 10
23 |
24 | // StepBits holds the number of bits to use for Step
25 | // Remember, you have a total 22 bits to share between Node/Step
26 | StepBits uint8 = 12
27 |
28 | // DEPRECATED: the below four variables will be removed in a future release.
29 | mu sync.Mutex
30 | nodeMax int64 = -1 ^ (-1 << NodeBits)
31 | nodeMask = nodeMax << StepBits
32 | stepMask int64 = -1 ^ (-1 << StepBits)
33 | timeShift = NodeBits + StepBits
34 | nodeShift = StepBits
35 | )
36 |
37 | const encodeBase32Map = "ybndrfg8ejkmcpqxot1uwisza345h769"
38 |
39 | var decodeBase32Map [256]byte
40 |
41 | const encodeBase58Map = "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"
42 |
43 | var decodeBase58Map [256]byte
44 |
45 | // A JSONSyntaxError is returned from UnmarshalJSON if an invalid ID is provided.
46 | type JSONSyntaxError struct{ original []byte }
47 |
48 | func (j JSONSyntaxError) Error() string {
49 | return fmt.Sprintf("invalid snowflake ID %q", string(j.original))
50 | }
51 |
52 | // ErrInvalidBase58 is returned by ParseBase58 when given an invalid []byte
53 | var ErrInvalidBase58 = errors.New("invalid base58")
54 |
55 | // ErrInvalidBase32 is returned by ParseBase32 when given an invalid []byte
56 | var ErrInvalidBase32 = errors.New("invalid base32")
57 |
58 | // Create maps for decoding Base58/Base32.
59 | // This speeds up the process tremendously.
60 | func init() {
61 |
62 | for i := 0; i < len(encodeBase58Map); i++ {
63 | decodeBase58Map[i] = 0xFF
64 | }
65 |
66 | for i := 0; i < len(encodeBase58Map); i++ {
67 | decodeBase58Map[encodeBase58Map[i]] = byte(i)
68 | }
69 |
70 | for i := 0; i < len(encodeBase32Map); i++ {
71 | decodeBase32Map[i] = 0xFF
72 | }
73 |
74 | for i := 0; i < len(encodeBase32Map); i++ {
75 | decodeBase32Map[encodeBase32Map[i]] = byte(i)
76 | }
77 | }
78 |
79 | // A Node struct holds the basic information needed for a snowflake generator
80 | // node
81 | type Node struct {
82 | mu sync.Mutex
83 | epoch time.Time
84 | time int64
85 | node int64
86 | step int64
87 |
88 | nodeMax int64
89 | nodeMask int64
90 | stepMask int64
91 | timeShift uint8
92 | nodeShift uint8
93 | }
94 |
95 | // An ID is a custom type used for a snowflake ID. This is used so we can
96 | // attach methods onto the ID.
97 | type ID int64
98 |
99 | // NewNode returns a new snowflake node that can be used to generate snowflake
100 | // IDs
101 | func NewNode(node int64) (*Node, error) {
102 |
103 | // re-calc in case custom NodeBits or StepBits were set
104 | // DEPRECATED: the below block will be removed in a future release.
105 | mu.Lock()
106 | nodeMax = -1 ^ (-1 << NodeBits)
107 | nodeMask = nodeMax << StepBits
108 | stepMask = -1 ^ (-1 << StepBits)
109 | timeShift = NodeBits + StepBits
110 | nodeShift = StepBits
111 | mu.Unlock()
112 |
113 | n := Node{}
114 | n.node = node
115 | n.nodeMax = -1 ^ (-1 << NodeBits)
116 | n.nodeMask = n.nodeMax << StepBits
117 | n.stepMask = -1 ^ (-1 << StepBits)
118 | n.timeShift = NodeBits + StepBits
119 | n.nodeShift = StepBits
120 |
121 | if n.node < 0 || n.node > n.nodeMax {
122 | return nil, errors.New("Node number must be between 0 and " + strconv.FormatInt(n.nodeMax, 10))
123 | }
124 |
125 | var curTime = time.Now()
126 | // add time.Duration to curTime to make sure we use the monotonic clock if available
127 | n.epoch = curTime.Add(time.Unix(Epoch/1000, (Epoch%1000)*1000000).Sub(curTime))
128 |
129 | return &n, nil
130 | }
131 |
132 | // Generate creates and returns a unique snowflake ID
133 | // To help guarantee uniqueness
134 | // - Make sure your system is keeping accurate system time
135 | // - Make sure you never have multiple nodes running with the same node ID
136 | func (n *Node) Generate() ID {
137 |
138 | n.mu.Lock()
139 |
140 | now := time.Since(n.epoch).Nanoseconds() / 1000000
141 |
142 | if now == n.time {
143 | n.step = (n.step + 1) & n.stepMask
144 |
145 | if n.step == 0 {
146 | for now <= n.time {
147 | now = time.Since(n.epoch).Nanoseconds() / 1000000
148 | }
149 | }
150 | } else {
151 | n.step = 0
152 | }
153 |
154 | n.time = now
155 |
156 | r := ID((now)<= 32 {
210 | b = append(b, encodeBase32Map[f%32])
211 | f /= 32
212 | }
213 | b = append(b, encodeBase32Map[f])
214 |
215 | for x, y := 0, len(b)-1; x < y; x, y = x+1, y-1 {
216 | b[x], b[y] = b[y], b[x]
217 | }
218 |
219 | return string(b)
220 | }
221 |
222 | // ParseBase32 parses a base32 []byte into a snowflake ID
223 | // NOTE: There are many different base32 implementations so becareful when
224 | // doing any interoperation.
225 | func ParseBase32(b []byte) (ID, error) {
226 |
227 | var id int64
228 |
229 | for i := range b {
230 | if decodeBase32Map[b[i]] == 0xFF {
231 | return -1, ErrInvalidBase32
232 | }
233 | id = id*32 + int64(decodeBase32Map[b[i]])
234 | }
235 |
236 | return ID(id), nil
237 | }
238 |
239 | // Base36 returns a base36 string of the snowflake ID
240 | func (f ID) Base36() string {
241 | return strconv.FormatInt(int64(f), 36)
242 | }
243 |
244 | // ParseBase36 converts a Base36 string into a snowflake ID
245 | func ParseBase36(id string) (ID, error) {
246 | i, err := strconv.ParseInt(id, 36, 64)
247 | return ID(i), err
248 | }
249 |
250 | // Base58 returns a base58 string of the snowflake ID
251 | func (f ID) Base58() string {
252 |
253 | if f < 58 {
254 | return string(encodeBase58Map[f])
255 | }
256 |
257 | b := make([]byte, 0, 11)
258 | for f >= 58 {
259 | b = append(b, encodeBase58Map[f%58])
260 | f /= 58
261 | }
262 | b = append(b, encodeBase58Map[f])
263 |
264 | for x, y := 0, len(b)-1; x < y; x, y = x+1, y-1 {
265 | b[x], b[y] = b[y], b[x]
266 | }
267 |
268 | return string(b)
269 | }
270 |
271 | // ParseBase58 parses a base58 []byte into a snowflake ID
272 | func ParseBase58(b []byte) (ID, error) {
273 |
274 | var id int64
275 |
276 | for i := range b {
277 | if decodeBase58Map[b[i]] == 0xFF {
278 | return -1, ErrInvalidBase58
279 | }
280 | id = id*58 + int64(decodeBase58Map[b[i]])
281 | }
282 |
283 | return ID(id), nil
284 | }
285 |
286 | // Base64 returns a base64 string of the snowflake ID
287 | func (f ID) Base64() string {
288 | return base64.StdEncoding.EncodeToString(f.Bytes())
289 | }
290 |
291 | // ParseBase64 converts a base64 string into a snowflake ID
292 | func ParseBase64(id string) (ID, error) {
293 | b, err := base64.StdEncoding.DecodeString(id)
294 | if err != nil {
295 | return -1, err
296 | }
297 | return ParseBytes(b)
298 |
299 | }
300 |
301 | // Bytes returns a byte slice of the snowflake ID
302 | func (f ID) Bytes() []byte {
303 | return []byte(f.String())
304 | }
305 |
306 | // ParseBytes converts a byte slice into a snowflake ID
307 | func ParseBytes(id []byte) (ID, error) {
308 | i, err := strconv.ParseInt(string(id), 10, 64)
309 | return ID(i), err
310 | }
311 |
312 | // IntBytes returns an array of bytes of the snowflake ID, encoded as a
313 | // big endian integer.
314 | func (f ID) IntBytes() [8]byte {
315 | var b [8]byte
316 | binary.BigEndian.PutUint64(b[:], uint64(f))
317 | return b
318 | }
319 |
320 | // ParseIntBytes converts an array of bytes encoded as big endian integer as
321 | // a snowflake ID
322 | func ParseIntBytes(id [8]byte) ID {
323 | return ID(int64(binary.BigEndian.Uint64(id[:])))
324 | }
325 |
326 | // Time returns an int64 unix timestamp in milliseconds of the snowflake ID time
327 | // DEPRECATED: the below function will be removed in a future release.
328 | func (f ID) Time() int64 {
329 | return (int64(f) >> timeShift) + Epoch
330 | }
331 |
332 | // Node returns an int64 of the snowflake ID node number
333 | // DEPRECATED: the below function will be removed in a future release.
334 | func (f ID) Node() int64 {
335 | return int64(f) & nodeMask >> nodeShift
336 | }
337 |
338 | // Step returns an int64 of the snowflake step (or sequence) number
339 | // DEPRECATED: the below function will be removed in a future release.
340 | func (f ID) Step() int64 {
341 | return int64(f) & stepMask
342 | }
343 |
344 | // MarshalJSON returns a json byte array string of the snowflake ID.
345 | func (f ID) MarshalJSON() ([]byte, error) {
346 | buff := make([]byte, 0, 22)
347 | buff = append(buff, '"')
348 | buff = strconv.AppendInt(buff, int64(f), 10)
349 | buff = append(buff, '"')
350 | return buff, nil
351 | }
352 |
353 | // UnmarshalJSON converts a json byte array of a snowflake ID into an ID type.
354 | func (f *ID) UnmarshalJSON(b []byte) error {
355 | if len(b) < 3 || b[0] != '"' || b[len(b)-1] != '"' {
356 | return JSONSyntaxError{b}
357 | }
358 |
359 | i, err := strconv.ParseInt(string(b[1:len(b)-1]), 10, 64)
360 | if err != nil {
361 | return err
362 | }
363 |
364 | *f = ID(i)
365 | return nil
366 | }
367 |
--------------------------------------------------------------------------------
/api/v1/system/user.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "aixinge/api/model/common/request"
5 | "aixinge/api/model/common/response"
6 | "aixinge/api/model/system"
7 | systemReq "aixinge/api/model/system/request"
8 | systemRes "aixinge/api/model/system/response"
9 | "aixinge/api/model/validation"
10 | "aixinge/global"
11 | "aixinge/middleware"
12 | "time"
13 |
14 | "github.com/gofiber/fiber/v2"
15 | "github.com/golang-jwt/jwt/v4"
16 | "go.uber.org/zap"
17 | )
18 |
19 | type User struct {
20 | }
21 |
22 | // Login
23 | // @Tags Base
24 | // @Summary 用户登录
25 | // @accept application/json
26 | // @Produce application/json
27 | // @Param data body systemReq.Login true "用户名, 密码, 验证码"
28 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"登陆成功"}"
29 | // @Router /v1/login [post]
30 | func (b *User) Login(c *fiber.Ctx) error {
31 | var l systemReq.Login
32 | _ = c.BodyParser(&l)
33 | if err := validation.Verify(l, validation.Login); err != nil {
34 | return response.FailWithMessage(err.Error(), c)
35 | }
36 | u := &system.User{Username: l.Username, Password: l.Password}
37 | if err, user := userService.Login(u); err != nil {
38 | global.LOG.Error("登陆失败! 用户名不存在或者密码错误!", zap.Any("err", err))
39 | return response.FailWithMessage("用户名不存在或者密码错误", c)
40 | } else {
41 | return b.tokenNext(c, *user)
42 | }
43 | }
44 |
45 | // 登录以后签发jwt
46 | func (b *User) tokenNext(c *fiber.Ctx, user system.User) error {
47 | j := &middleware.JWT{SigningKey: []byte(global.CONFIG.JWT.SigningKey)} // 唯一签名
48 | var expiresTime = global.CONFIG.JWT.ExpiresTime
49 | claims := systemReq.TokenClaims{
50 | UUID: user.UUID,
51 | ID: user.ID,
52 | Nickname: user.Nickname,
53 | Username: user.Username,
54 | RegisteredClaims: jwt.RegisteredClaims{
55 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expiresTime) * time.Minute)), // 过期时间 30分钟 配置文件
56 | IssuedAt: jwt.NewNumericDate(time.Now()), // 签发时间
57 | },
58 | }
59 | accessToken, err := j.CreateToken(claims)
60 | refreshToken, rtErr := j.CreateToken(systemReq.RefreshTokenClaims{
61 | ID: user.ID,
62 | RegisteredClaims: jwt.RegisteredClaims{
63 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(1) * time.Hour)), // 过期时间 1小时
64 | IssuedAt: jwt.NewNumericDate(time.Now()), // 签发时间
65 | },
66 | })
67 | if err != nil || rtErr != nil {
68 | global.LOG.Error("设置登录状态失败!", zap.Any("err", err))
69 | return response.FailWithMessage("设置登录状态失败", c)
70 | }
71 | return response.OkWithDetailed(systemRes.LoginResponse{
72 | User: user,
73 | Token: accessToken,
74 | RefreshToken: refreshToken,
75 | }, "登录成功", c)
76 | }
77 |
78 | // RefreshToken
79 | // @Tags Base
80 | // @Summary 刷新Token
81 | // @accept application/json
82 | // @Produce application/json
83 | // @Param data body systemReq.RefreshToken true "刷新票据"
84 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"刷新Token成功"}"
85 | // @Router /v1/refresh-token [post]
86 | func (b *User) RefreshToken(c *fiber.Ctx) error {
87 | var rt systemReq.RefreshToken
88 | err := c.BodyParser(&rt)
89 | if err != nil || rt.RefreshToken == "" {
90 | return response.FailWithMessage("未登录或非法访问", c)
91 | }
92 | j := middleware.NewJWT()
93 | claims, err := j.ParseToken(rt.RefreshToken)
94 | if err != nil {
95 | return response.Result(response.ExpireRefreshToken, fiber.Map{"reload": true}, err.Error(), c)
96 | }
97 | if err, user := userService.GetById(claims.ID); err != nil {
98 | return response.FailWithMessage("用户名不存在或者密码错误", c)
99 | } else {
100 | return b.tokenNext(c, user)
101 | }
102 | }
103 |
104 | // Create
105 | // @Tags User
106 | // @Summary 创建用户
107 | // @Security ApiKeyAuth
108 | // @accept application/json
109 | // @Produce application/json
110 | // @Param data body systemReq.UserCreate true "创建User"
111 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
112 | // @Router /v1/user/create [post]
113 | func (b *User) Create(c *fiber.Ctx) error {
114 | var uc systemReq.UserCreate
115 | _ = c.BodyParser(&uc)
116 | if err := validation.Verify(uc, validation.UserCreate); err != nil {
117 | return response.FailWithMessage(err.Error(), c)
118 | }
119 | err, userReturn := userService.Create(uc)
120 | if err != nil {
121 | global.LOG.Error("注册失败!", zap.Any("err", err))
122 | return response.FailWithDetailed(systemRes.UserResponse{User: userReturn}, "注册失败", c)
123 | } else {
124 | return response.OkWithDetailed(systemRes.UserResponse{User: userReturn}, "注册成功", c)
125 | }
126 | }
127 |
128 | // Delete
129 | // @Tags User
130 | // @Summary 删除用户
131 | // @Security ApiKeyAuth
132 | // @accept application/json
133 | // @Produce application/json
134 | // @Param data body request.IdsReq true "ID集合"
135 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
136 | // @Router /v1/user/delete [post]
137 | func (b *User) Delete(c *fiber.Ctx) error {
138 | var idsReq request.IdsReq
139 | _ = c.BodyParser(&idsReq)
140 | if err := validation.Verify(idsReq, validation.Id); err != nil {
141 | return response.FailWithMessage(err.Error(), c)
142 | }
143 | if err := userService.Delete(idsReq); err != nil {
144 | global.LOG.Error("删除失败!", zap.Any("err", err))
145 | return response.FailWithMessage("删除失败", c)
146 | } else {
147 | return response.OkWithMessage("删除成功", c)
148 | }
149 | }
150 |
151 | // Update
152 | // @Tags User
153 | // @Summary 更新用户信息
154 | // @Security ApiKeyAuth
155 | // @accept application/json
156 | // @Produce application/json
157 | // @Param data body system.User true "用户信息"
158 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}"
159 | // @Router /v1/user/update [post]
160 | func (b *User) Update(c *fiber.Ctx) error {
161 | var user system.User
162 | _ = c.BodyParser(&user)
163 | if err := validation.Verify(user, validation.Id); err != nil {
164 | return response.FailWithMessage(err.Error(), c)
165 | }
166 |
167 | err, user := userService.Update(user)
168 | if err != nil {
169 | global.LOG.Error("更新失败!", zap.Any("err", err))
170 | return response.FailWithMessage("更新失败"+err.Error(), c)
171 | }
172 |
173 | return response.OkWithDetailed(systemRes.UserResponse{User: user}, "更新成功", c)
174 | }
175 |
176 | // ChangePassword
177 | //
178 | // @Tags User
179 | // @Summary 用户修改密码
180 | // @Security ApiKeyAuth
181 | // @Produce application/json
182 | // @Param data body systemReq.ChangePasswordStruct true "用户名, 原密码, 新密码"
183 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}"
184 | // @Router /v1/user/change-password [post]
185 | func (b *User) ChangePassword(c *fiber.Ctx) error {
186 | var user systemReq.ChangePasswordStruct
187 | _ = c.BodyParser(&user)
188 | if err := validation.Verify(user, validation.ChangePassword); err != nil {
189 | return response.FailWithMessage(err.Error(), c)
190 | }
191 | u := &system.User{Username: user.Username, Password: user.Password}
192 | if err, _ := userService.ChangePassword(u, user.NewPassword); err != nil {
193 | global.LOG.Error("修改失败!", zap.Any("err", err))
194 | return response.FailWithMessage("修改失败,原密码与当前账户不符", c)
195 | } else {
196 | return response.OkWithMessage("修改成功", c)
197 | }
198 | }
199 |
200 | // AssignRole
201 | // @Tags User
202 | // @Summary 用户分配角色
203 | // @Security ApiKeyAuth
204 | // @accept application/json
205 | // @Produce application/json
206 | // @Param data body systemReq.UserRoleParams true "用户ID、角色ID集合"
207 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
208 | // @Router /v1/user/assign-role [post]
209 | func (b *User) AssignRole(c *fiber.Ctx) error {
210 | var params systemReq.UserRoleParams
211 | _ = c.BodyParser(¶ms)
212 | if err := userService.AssignRole(params); err != nil {
213 | global.LOG.Error("角色分配菜单失败", zap.Any("err", err))
214 | return response.FailWithMessage("获取失败", c)
215 | }
216 | return response.OkWithMessage("获取成功", c)
217 | }
218 |
219 | // SelectedRoles
220 | // @Tags User
221 | // @Summary 根据id获取用户已分配的角色ID列表
222 | // @Security ApiKeyAuth
223 | // @accept application/json
224 | // @Produce application/json
225 | // @Param data body request.GetById true "用户ID"
226 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
227 | // @Router /v1/user/selected-roles [post]
228 | func (b *User) SelectedRoles(c *fiber.Ctx) error {
229 | var idInfo request.GetById
230 | _ = c.BodyParser(&idInfo)
231 | if err := validation.Verify(idInfo, validation.Id); err != nil {
232 | return response.FailWithMessage(err.Error(), c)
233 | }
234 | if err, roleIds := userService.SelectedRoles(idInfo.ID); err != nil {
235 | global.LOG.Error("获取用户分配角色ID列表失败", zap.Any("err", err))
236 | return response.FailWithMessage("获取失败", c)
237 | } else {
238 | return response.OkWithDetailed(roleIds, "获取成功", c)
239 | }
240 | }
241 |
242 | // Get
243 | // @Tags User
244 | // @Summary 根据id获取用户
245 | // @Security ApiKeyAuth
246 | // @accept application/json
247 | // @Produce application/json
248 | // @Param data body request.GetById true "用户ID"
249 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
250 | // @Router /v1/user/get [post]
251 | func (b *User) Get(c *fiber.Ctx) error {
252 | var idInfo request.GetById
253 | _ = c.BodyParser(&idInfo)
254 | if err := validation.Verify(idInfo, validation.Id); err != nil {
255 | return response.FailWithMessage(err.Error(), c)
256 | }
257 | if err, user := userService.GetById(idInfo.ID); err != nil {
258 | global.LOG.Error("获取失败!", zap.Any("err", err))
259 | return response.FailWithMessage("获取失败", c)
260 | } else {
261 | return response.OkWithDetailed(systemRes.UserResponse{User: user}, "获取成功", c)
262 | }
263 | }
264 |
265 | // Page
266 | // @Tags User
267 | // @Summary 分页获取用户列表
268 | // @Security ApiKeyAuth
269 | // @accept application/json
270 | // @Produce application/json
271 | // @Param data body request.PageInfo true "页码, 每页大小"
272 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
273 | // @Router /v1/user/page [post]
274 | func (b *User) Page(c *fiber.Ctx) error {
275 | var pageInfo systemReq.UserPageParams
276 | _ = c.BodyParser(&pageInfo)
277 | if err := validation.Verify(pageInfo, validation.PageInfo); err != nil {
278 | return response.FailWithMessage(err.Error(), c)
279 | }
280 | if err, list, total := userService.Page(pageInfo); err != nil {
281 | global.LOG.Error("获取失败!", zap.Any("err", err))
282 | return response.FailWithMessage("获取失败", c)
283 | } else {
284 | return response.OkWithDetailed(response.PageResult{
285 | List: list,
286 | Total: total,
287 | Page: pageInfo.Page,
288 | PageSize: pageInfo.PageSize,
289 | }, "获取成功", c)
290 | }
291 | }
292 |
293 | // List
294 | // @Tags User
295 | // @Summary 获取用户列表
296 | // @Security ApiKeyAuth
297 | // @accept application/json
298 | // @Produce application/json
299 | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
300 | // @Router /v1/user/list [post]
301 | func (b *User) List(c *fiber.Ctx) error {
302 | if err, list := userService.List(); err != nil {
303 | global.LOG.Error("获取失败!", zap.Any("err", err))
304 | return response.FailWithMessage("获取失败", c)
305 | } else {
306 | return response.OkWithDetailed(list, "获取成功", c)
307 | }
308 | }
309 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2022 AiXinGe
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/core/websocket/ws.go:
--------------------------------------------------------------------------------
1 | package websocket
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "math/rand"
7 | "sync"
8 | "time"
9 |
10 | "github.com/gofiber/fiber/v2"
11 | "github.com/gofiber/websocket/v2"
12 | )
13 |
14 | // Source @url:https://github.com/gorilla/websocket/blob/master/conn.go#L61
15 | // The message types are defined in RFC 6455, section 11.8.
16 | const (
17 | // TextMessage denotes a text data message. The text message payload is
18 | // interpreted as UTF-8 encoded text data.
19 | TextMessage = 1
20 | // BinaryMessage denotes a binary data message.
21 | BinaryMessage = 2
22 | // CloseMessage denotes a close control message. The optional message
23 | // payload contains a numeric code and text. Use the FormatCloseMessage
24 | // function to format a close message payload.
25 | CloseMessage = 8
26 | // PingMessage denotes a ping control message. The optional message payload
27 | // is UTF-8 encoded text.
28 | PingMessage = 9
29 | // PongMessage denotes a pong control message. The optional message payload
30 | // is UTF-8 encoded text.
31 | PongMessage = 10
32 | )
33 |
34 | // Supported event list
35 | const (
36 | // EventMessage Fired when a Text/Binary message is received
37 | EventMessage = "message"
38 | // EventPing More details here:
39 | // @url https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Pings_and_Pongs_The_Heartbeat_of_WebSockets
40 | EventPing = "ping"
41 | EventPong = "pong"
42 | // EventDisconnect Fired on disconnection
43 | // The error provided in disconnection event
44 | // is defined in RFC 6455, section 11.7.
45 | // @url https://github.com/gofiber/websocket/blob/cd4720c435de415b864d975a9ca23a47eaf081ef/websocket.go#L192
46 | EventDisconnect = "disconnect"
47 | // EventConnect Fired on first connection
48 | EventConnect = "connect"
49 | // EventClose Fired when the connection is actively closed from the server
50 | EventClose = "close"
51 | // EventError Fired when some error appears useful also for debugging websockets
52 | EventError = "error"
53 | )
54 |
55 | var (
56 | // ErrorInvalidConnection The addressed websocket connection is not available anymore
57 | // error data is the uuid of that connection
58 | ErrorInvalidConnection = errors.New("message cannot be delivered invalid/gone connection")
59 | // ErrorUUIDDuplication The UUID already exists in the pool
60 | ErrorUUIDDuplication = errors.New("UUID already exists in the available connections pool")
61 | )
62 |
63 | var (
64 | PongTimeout = 1 * time.Second
65 | // RetrySendTimeout retry after 20 ms if there is an error
66 | RetrySendTimeout = 20 * time.Millisecond
67 | //MaxSendRetry define max retries if there are socket issues
68 | MaxSendRetry = 5
69 | // ReadTimeout Instead of reading in a for loop, try to avoid full CPU load taking some pause
70 | ReadTimeout = 10 * time.Millisecond
71 | )
72 |
73 | // Raw form of websocket message
74 | type message struct {
75 | // Message type
76 | mType int
77 | // Message data
78 | data []byte
79 | // Message send retries when error
80 | retries int
81 | }
82 |
83 | // EventPayload Event Payload is the object that
84 | // stores all the information about the event and
85 | // the connection
86 | type EventPayload struct {
87 | // The connection object
88 | Kws *Websocket
89 | // The name of the event
90 | Name string
91 | // Unique connection UUID
92 | SocketUUID string
93 | // Optional websocket attributes
94 | SocketAttributes map[string]interface{}
95 | // Optional error when are fired events like
96 | // - Disconnect
97 | // - Error
98 | Error error
99 | // Data is used on Message and on Error event
100 | Data []byte
101 | }
102 |
103 | type ws interface {
104 | IsAlive() bool
105 | GetUUID() string
106 | SetUUID(uuid string)
107 | SetAttribute(key string, attribute interface{})
108 | GetAttribute(key string) interface{}
109 | GetIntAttribute(key string) int
110 | GetStringAttribute(key string) string
111 | EmitToList(uuids []string, message []byte, mType ...int)
112 | EmitTo(uuid string, message []byte, mType ...int) error
113 | Broadcast(message []byte, except bool, mType ...int)
114 | Fire(event string, data []byte)
115 | Emit(message []byte, mType ...int)
116 | Kick(key string, message []byte, mType ...int) error
117 | Close()
118 | pong(ctx context.Context)
119 | write(messageType int, messageBytes []byte)
120 | run()
121 | read(ctx context.Context)
122 | disconnected(err error)
123 | createUUID() string
124 | randomUUID() string
125 | fireEvent(event string, data []byte, error error)
126 | }
127 |
128 | type Websocket struct {
129 | mu sync.RWMutex
130 | // The Fiber.Websocket connection
131 | ws *websocket.Conn
132 | // Define if the connection is alive or not
133 | isAlive bool
134 | // Queue of messages sent from the socket
135 | queue chan message
136 | // Channel to signal when this websocket is closed
137 | // so go routines will stop gracefully
138 | done chan struct{}
139 | // Attributes map collection for the connection
140 | attributes map[string]interface{}
141 | // Unique id of the connection
142 | UUID string
143 | // Wrap Fiber Locals function
144 | Locals func(key string) interface{}
145 | // Wrap Fiber Params function
146 | Params func(key string, defaultValue ...string) string
147 | // Wrap Fiber Query function
148 | Query func(key string, defaultValue ...string) string
149 | // Wrap Fiber Cookies function
150 | Cookies func(key string, defaultValue ...string) string
151 | }
152 |
153 | type safePool struct {
154 | sync.RWMutex
155 | // List of the connections alive
156 | conn map[string]ws
157 | }
158 |
159 | // Pool with the active connections
160 | var pool = safePool{
161 | conn: make(map[string]ws),
162 | }
163 |
164 | func (p *safePool) set(ws ws) {
165 | p.Lock()
166 | p.conn[ws.GetUUID()] = ws
167 | p.Unlock()
168 | }
169 |
170 | func (p *safePool) all() map[string]ws {
171 | p.RLock()
172 | ret := make(map[string]ws, 0)
173 | for uuid, kws := range p.conn {
174 | ret[uuid] = kws
175 | }
176 | p.RUnlock()
177 | return ret
178 | }
179 |
180 | func (p *safePool) get(key string) ws {
181 | p.RLock()
182 | ret, ok := p.conn[key]
183 | p.RUnlock()
184 | if !ok {
185 | panic("not found")
186 | }
187 | return ret
188 | }
189 |
190 | func (p *safePool) contains(key string) bool {
191 | p.RLock()
192 | _, ok := p.conn[key]
193 | p.RUnlock()
194 | return ok
195 | }
196 |
197 | func (p *safePool) delete(key string) {
198 | p.Lock()
199 | delete(p.conn, key)
200 | p.Unlock()
201 | }
202 |
203 | func (p *safePool) reset() {
204 | p.Lock()
205 | p.conn = make(map[string]ws)
206 | p.Unlock()
207 | }
208 |
209 | type safeListeners struct {
210 | sync.RWMutex
211 | list map[string][]eventCallback
212 | }
213 |
214 | func (l *safeListeners) set(event string, callback eventCallback) {
215 | l.Lock()
216 | listeners.list[event] = append(listeners.list[event], callback)
217 | l.Unlock()
218 | }
219 |
220 | func (l *safeListeners) get(event string) []eventCallback {
221 | l.RLock()
222 | defer l.RUnlock()
223 | if _, ok := l.list[event]; !ok {
224 | return make([]eventCallback, 0)
225 | }
226 |
227 | ret := make([]eventCallback, 0)
228 | for _, v := range l.list[event] {
229 | ret = append(ret, v)
230 | }
231 | return ret
232 | }
233 |
234 | // List of the listeners for the events
235 | var listeners = safeListeners{
236 | list: make(map[string][]eventCallback),
237 | }
238 |
239 | func New(callback func(kws *Websocket)) func(*fiber.Ctx) error {
240 | return websocket.New(func(c *websocket.Conn) {
241 | kws := &Websocket{
242 | ws: c,
243 | Locals: func(key string) interface{} {
244 | return c.Locals(key)
245 | },
246 | Params: func(key string, defaultValue ...string) string {
247 | return c.Params(key, defaultValue...)
248 | },
249 | Query: func(key string, defaultValue ...string) string {
250 | return c.Query(key, defaultValue...)
251 | },
252 | Cookies: func(key string, defaultValue ...string) string {
253 | return c.Cookies(key, defaultValue...)
254 | },
255 | queue: make(chan message, 100),
256 | done: make(chan struct{}, 1),
257 | attributes: make(map[string]interface{}),
258 | isAlive: true,
259 | }
260 |
261 | // Generate uuid
262 | kws.UUID = kws.createUUID()
263 |
264 | // register the connection into the pool
265 | pool.set(kws)
266 |
267 | // execute the callback of the socket initialization
268 | callback(kws)
269 |
270 | kws.fireEvent(EventConnect, nil, nil)
271 |
272 | // Run the loop for the given connection
273 | kws.run()
274 | })
275 | }
276 |
277 | func (kws *Websocket) GetUUID() string {
278 | kws.mu.RLock()
279 | defer kws.mu.RUnlock()
280 | return kws.UUID
281 | }
282 |
283 | func (kws *Websocket) SetUUID(uuid string) {
284 | kws.mu.Lock()
285 | defer kws.mu.Unlock()
286 |
287 | if pool.contains(uuid) {
288 | panic(ErrorUUIDDuplication)
289 | }
290 | kws.UUID = uuid
291 | }
292 |
293 | // SetAttribute Set a specific attribute for the specific socket connection
294 | func (kws *Websocket) SetAttribute(key string, attribute interface{}) {
295 | kws.mu.Lock()
296 | defer kws.mu.Unlock()
297 | kws.attributes[key] = attribute
298 | }
299 |
300 | // GetAttribute Get a specific attribute from the socket attributes
301 | func (kws *Websocket) GetAttribute(key string) interface{} {
302 | kws.mu.RLock()
303 | defer kws.mu.RUnlock()
304 | value, ok := kws.attributes[key]
305 | if ok {
306 | return value
307 | }
308 | return nil
309 | }
310 |
311 | // GetIntAttribute Convenience method to retrieve an attribute as an int.
312 | // Will panic if attribute is not an int.
313 | func (kws *Websocket) GetIntAttribute(key string) int {
314 | kws.mu.RLock()
315 | defer kws.mu.RUnlock()
316 | value, ok := kws.attributes[key]
317 | if ok {
318 | return value.(int)
319 | }
320 | return 0
321 | }
322 |
323 | // GetStringAttribute Convenience method to retrieve an attribute as a string.
324 | // Will panic if attribute is not an int.
325 | func (kws *Websocket) GetStringAttribute(key string) string {
326 | kws.mu.RLock()
327 | defer kws.mu.RUnlock()
328 | value, ok := kws.attributes[key]
329 | if ok {
330 | return value.(string)
331 | }
332 | return ""
333 | }
334 |
335 | // EmitToList Emit the message to a specific socket uuids list
336 | func (kws *Websocket) EmitToList(uuids []string, message []byte, mType ...int) {
337 | for _, uuid := range uuids {
338 | err := kws.EmitTo(uuid, message, mType...)
339 | if err != nil {
340 | kws.fireEvent(EventError, message, err)
341 | }
342 | }
343 | }
344 |
345 | // EmitToList Emit the message to a specific socket uuids list
346 | // Ignores all errors
347 | func EmitToList(uuids []string, message []byte, mType ...int) {
348 | for _, uuid := range uuids {
349 | _ = EmitTo(uuid, message, mType...)
350 | }
351 | }
352 |
353 | // EmitTo Emit to a specific socket connection
354 | func (kws *Websocket) EmitTo(uuid string, message []byte, mType ...int) error {
355 |
356 | if !pool.contains(uuid) || !pool.get(uuid).IsAlive() {
357 | kws.fireEvent(EventError, []byte(uuid), ErrorInvalidConnection)
358 | return ErrorInvalidConnection
359 | }
360 |
361 | pool.get(uuid).Emit(message, mType...)
362 | return nil
363 | }
364 |
365 | // EmitTo Emit to a specific socket connection
366 | func EmitTo(uuid string, message []byte, mType ...int) error {
367 | if !pool.contains(uuid) {
368 | return ErrorInvalidConnection
369 | }
370 |
371 | if !pool.get(uuid).IsAlive() {
372 | return ErrorInvalidConnection
373 | }
374 | pool.get(uuid).Emit(message, mType...)
375 | return nil
376 | }
377 |
378 | // Broadcast to all the active connections
379 | // except avoid broadcasting the message to itself
380 | func (kws *Websocket) Broadcast(message []byte, except bool, mType ...int) {
381 | for uuid := range pool.all() {
382 | if except && kws.UUID == uuid {
383 | continue
384 | }
385 | err := kws.EmitTo(uuid, message, mType...)
386 | if err != nil {
387 | kws.fireEvent(EventError, message, err)
388 | }
389 | }
390 | }
391 |
392 | // Broadcast to all the active connections
393 | func Broadcast(message []byte, mType ...int) {
394 | for _, kws := range pool.all() {
395 | kws.Emit(message, mType...)
396 | }
397 | }
398 |
399 | // Fire custom event
400 | func (kws *Websocket) Fire(event string, data []byte) {
401 | kws.fireEvent(event, data, nil)
402 | }
403 |
404 | // Fire custom event on all connections
405 | func Fire(event string, data []byte) {
406 | fireGlobalEvent(event, data, nil)
407 | }
408 |
409 | // Emit Emit/Write the message into the given connection
410 | func (kws *Websocket) Emit(message []byte, mType ...int) {
411 | t := TextMessage
412 | if len(mType) > 0 {
413 | t = mType[0]
414 | }
415 | kws.write(t, message)
416 | }
417 |
418 | func (kws *Websocket) Kick(key string, message []byte, mType ...int) error {
419 | _ws := pool.get(key)
420 | if _ws != nil && _ws.IsAlive() {
421 | _ws.EmitTo(key, message, mType...)
422 | _ws.Close()
423 | }
424 | return nil
425 | }
426 |
427 | // Close Actively close the connection from the server
428 | func (kws *Websocket) Close() {
429 | kws.write(CloseMessage, []byte("Connection closed"))
430 | kws.fireEvent(EventClose, nil, nil)
431 | }
432 |
433 | func (kws *Websocket) IsAlive() bool {
434 | kws.mu.RLock()
435 | defer kws.mu.RUnlock()
436 | return kws.isAlive
437 | }
438 |
439 | func (kws *Websocket) hasConn() bool {
440 | kws.mu.RLock()
441 | defer kws.mu.RUnlock()
442 | return kws.ws.Conn != nil
443 | }
444 |
445 | func (kws *Websocket) setAlive(alive bool) {
446 | kws.mu.Lock()
447 | defer kws.mu.Unlock()
448 | kws.isAlive = alive
449 | }
450 |
451 | func (kws *Websocket) queueLength() int {
452 | kws.mu.RLock()
453 | defer kws.mu.RUnlock()
454 | return len(kws.queue)
455 | }
456 |
457 | // pong writes a control message to the client
458 | func (kws *Websocket) pong(ctx context.Context) {
459 | timeoutTicker := time.Tick(PongTimeout)
460 | for {
461 | select {
462 | case <-timeoutTicker:
463 | kws.write(PongMessage, []byte{})
464 | case <-ctx.Done():
465 | return
466 | }
467 | }
468 | }
469 |
470 | // Add in message queue
471 | func (kws *Websocket) write(messageType int, messageBytes []byte) {
472 | kws.queue <- message{
473 | mType: messageType,
474 | data: messageBytes,
475 | retries: 0,
476 | }
477 | }
478 |
479 | // Send out message queue
480 | func (kws *Websocket) send(ctx context.Context) {
481 | for {
482 | select {
483 | case message := <-kws.queue:
484 | if !kws.hasConn() {
485 | if message.retries <= MaxSendRetry {
486 | // retry without blocking the sending thread
487 | go func() {
488 | time.Sleep(RetrySendTimeout)
489 | message.retries = message.retries + 1
490 | kws.queue <- message
491 | }()
492 | }
493 | continue
494 | }
495 |
496 | kws.mu.RLock()
497 | err := kws.ws.WriteMessage(message.mType, message.data)
498 | kws.mu.RUnlock()
499 |
500 | if err != nil {
501 | kws.disconnected(err)
502 | }
503 | case <-ctx.Done():
504 | return
505 | }
506 | }
507 | }
508 |
509 | // Start Pong/Read/Write functions
510 | //
511 | // Needs to be blocking, otherwise the connection would close.
512 | func (kws *Websocket) run() {
513 | ctx, cancelFunc := context.WithCancel(context.Background())
514 |
515 | go kws.pong(ctx)
516 | go kws.read(ctx)
517 | go kws.send(ctx)
518 |
519 | <-kws.done // block until one event is sent to the done channel
520 |
521 | cancelFunc()
522 | }
523 |
524 | // Listen for incoming messages
525 | // and filter by message type
526 | func (kws *Websocket) read(ctx context.Context) {
527 | timeoutTicker := time.Tick(ReadTimeout)
528 | for {
529 | select {
530 | case <-timeoutTicker:
531 | if !kws.hasConn() {
532 | continue
533 | }
534 |
535 | kws.mu.RLock()
536 | mtype, msg, err := kws.ws.ReadMessage()
537 | kws.mu.RUnlock()
538 |
539 | if mtype == PingMessage {
540 | kws.fireEvent(EventPing, nil, nil)
541 | continue
542 | }
543 |
544 | if mtype == PongMessage {
545 | kws.fireEvent(EventPong, nil, nil)
546 | continue
547 | }
548 |
549 | if mtype == CloseMessage {
550 | kws.disconnected(nil)
551 | return
552 | }
553 |
554 | if err != nil {
555 | kws.disconnected(err)
556 | return
557 | }
558 |
559 | // We have a message and we fire the message event
560 | kws.fireEvent(EventMessage, msg, nil)
561 | case <-ctx.Done():
562 | return
563 | }
564 | }
565 | }
566 |
567 | // When the connection closes, disconnected method
568 | func (kws *Websocket) disconnected(err error) {
569 | kws.fireEvent(EventDisconnect, nil, err)
570 |
571 | // may be called multiple times from different go routines
572 | if kws.IsAlive() {
573 | close(kws.done)
574 | }
575 | kws.setAlive(false)
576 |
577 | // Fire error event if the connection is
578 | // disconnected by an error
579 | if err != nil {
580 | kws.fireEvent(EventError, nil, err)
581 | }
582 |
583 | // Remove the socket from the pool
584 | pool.delete(kws.UUID)
585 | }
586 |
587 | // Create random UUID for each connection
588 | func (kws *Websocket) createUUID() string {
589 | uuid := kws.randomUUID()
590 |
591 | //make sure about the uniqueness of the uuid
592 | if pool.contains(uuid) {
593 | return kws.createUUID()
594 | }
595 | return uuid
596 | }
597 |
598 | // TODO implement Google UUID library instead of random string
599 | func (kws *Websocket) randomUUID() string {
600 |
601 | length := 100
602 | seed := rand.New(rand.NewSource(time.Now().UnixNano()))
603 | charset := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"
604 |
605 | b := make([]byte, length)
606 | for i := range b {
607 | b[i] = charset[seed.Intn(len(charset))]
608 | }
609 |
610 | return string(b)
611 | }
612 |
613 | // Fires event on all connections.
614 | func fireGlobalEvent(event string, data []byte, error error) {
615 | for _, kws := range pool.all() {
616 | kws.fireEvent(event, data, error)
617 | }
618 | }
619 |
620 | // Checks if there is at least a listener for a given event
621 | // and loop over the callbacks registered
622 | func (kws *Websocket) fireEvent(event string, data []byte, error error) {
623 | callbacks := listeners.get(event)
624 |
625 | for _, callback := range callbacks {
626 | callback(&EventPayload{
627 | Kws: kws,
628 | Name: event,
629 | SocketUUID: kws.UUID,
630 | SocketAttributes: kws.attributes,
631 | Data: data,
632 | Error: error,
633 | })
634 | }
635 | }
636 |
637 | type eventCallback func(payload *EventPayload)
638 |
639 | // On Add listener callback for an event into the listeners list
640 | func On(event string, callback eventCallback) {
641 | listeners.set(event, callback)
642 | }
643 |
--------------------------------------------------------------------------------