├── 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 | ![](https://gitee.com/aixinge/aixinge/raw/master/wiki/img/Git-Commit-Template-Open.jpg) 64 | ![](https://gitee.com/aixinge/aixinge/raw/master/wiki/img/Git-Commit-Template-Use.jpg) 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 | ![](https://gitee.com/aixinge/aixinge/raw/master/wiki/img/Feature-V1.png) 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 | --------------------------------------------------------------------------------