diff --git a/server/initialize/plugin.go b/server/initialize/plugin.go index acc19421..7c5ae5e4 100644 --- a/server/initialize/plugin.go +++ b/server/initialize/plugin.go @@ -2,6 +2,7 @@ package initialize import ( "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/notify" email "github.com/flipped-aurora/gva-plugins/email" // 在线仓库模式go //"github.com/flipped-aurora/gin-vue-admin/server/plugin/email" // 本地插件仓库地址模式 "github.com/flipped-aurora/gin-vue-admin/server/plugin/example_plugin" @@ -30,4 +31,10 @@ func InstallPlugin(PublicGroup *gin.RouterGroup, PrivateGroup *gin.RouterGroup) global.GVA_CONFIG.Email.Port, global.GVA_CONFIG.Email.IsSSL, )) + + // 钉钉通知,暂时开放权限 + PluginInit(PublicGroup, notify.CreateDDPlug( + "https://oapi.dingtalk.com/robot/send", + "8ded23f91917dc4f6275f44ba5ef243e6ed1d2cc74de83f01a6f5f5f39905671", + "SECaecf452bd6e671ab0d47469c3ad933e32fcc47b335333049a1b8961606192f38")) } diff --git a/server/plugin/email/README.MD b/server/plugin/email/README.MD index 646b24f2..17202838 100644 --- a/server/plugin/email/README.MD +++ b/server/plugin/email/README.MD @@ -1,4 +1,5 @@ ## GVA 邮件发送功能插件 +#### 开发者:GIN-VUE-ADMIN 官方 ### 使用步骤 @@ -27,8 +28,9 @@ true, )) -#### 2. 配置说明 +### 2. 配置说明 +#### 2-1 全局配置结构体说明 //其中 Form 和 Secret 通常来说就是用户名和密码 type Email struct { @@ -40,8 +42,17 @@ Port int // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 IsSSL bool // 是否SSL 是否开启SSL } +#### 2-2 入参结构说明 + //其中 Form 和 Secret 通常来说就是用户名和密码 + + type Email struct { + To string `json:"to"` // 邮件发送给谁 + Subject string `json:"subject"` // 邮件标题 + Body string `json:"body"` // 邮件内容 + } + -### 方法API +### 3. 方法API utils.EmailTest(邮件标题,邮件主体) 发送测试邮件 例:utils.EmailTest("测试邮件","测试邮件") @@ -49,3 +60,16 @@ 例:utils.ErrorToEmail("测试邮件","测试邮件") utils.Email(目标邮箱多个的话用逗号分隔,邮件标题,邮件主体) 发送测试邮件 例:utils.Email(”a.qq.com,b.qq.com“,"测试邮件","测试邮件") + +### 4. 可直接调用的接口 + + 测试接口: /email/emailTest [post] 已配置swagger + + 发送邮件接口接口: /email/emailSend [post] 已配置swagger + 入参: + type Email struct { + To string `json:"to"` // 邮件发送给谁 + Subject string `json:"subject"` // 邮件标题 + Body string `json:"body"` // 邮件内容 + } + diff --git a/server/plugin/email/api/sys_email.go b/server/plugin/email/api/sys_email.go index f21cd59a..03e13afa 100644 --- a/server/plugin/email/api/sys_email.go +++ b/server/plugin/email/api/sys_email.go @@ -3,6 +3,7 @@ package api import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + email_response "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/model/response" "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/service" "github.com/gin-gonic/gin" "go.uber.org/zap" @@ -25,3 +26,21 @@ func (s *EmailApi) EmailTest(c *gin.Context) { response.OkWithData("发送成功", c) } } + +// @Tags System +// @Summary 发送邮件 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body email_response.Email true "发送邮件必须的参数" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}" +// @Router /email/sendEmail [post] +func (s *EmailApi) SendEmail(c *gin.Context) { + var email email_response.Email + _ = c.ShouldBindJSON(&email) + if err := service.ServiceGroupApp.SendEmail(email.To, email.Subject, email.Body); err != nil { + global.GVA_LOG.Error("发送失败!", zap.Any("err", err)) + response.FailWithMessage("发送失败", c) + } else { + response.OkWithData("发送成功", c) + } +} diff --git a/server/plugin/email/model/response/email.go b/server/plugin/email/model/response/email.go new file mode 100644 index 00000000..ed254750 --- /dev/null +++ b/server/plugin/email/model/response/email.go @@ -0,0 +1,7 @@ +package response + +type Email struct { + To string `json:"to"` // 邮件发送给谁 + Subject string `json:"subject"` // 邮件标题 + Body string `json:"body"` // 邮件内容 +} diff --git a/server/plugin/email/router/sys_email.go b/server/plugin/email/router/sys_email.go index 339f68e8..518bb0bb 100644 --- a/server/plugin/email/router/sys_email.go +++ b/server/plugin/email/router/sys_email.go @@ -12,7 +12,9 @@ type EmailRouter struct { func (s *EmailRouter) InitEmailRouter(Router *gin.RouterGroup) { emailRouter := Router.Use(middleware.OperationRecord()) var EmailApi = api.ApiGroupApp.EmailApi.EmailTest + var SendEmail = api.ApiGroupApp.EmailApi.SendEmail { - emailRouter.POST("emailTest", EmailApi) // 发送测试邮件 + emailRouter.POST("emailTest", EmailApi) // 发送测试邮件 + emailRouter.POST("sendEmail", SendEmail) // 发送邮件 } } diff --git a/server/plugin/email/service/sys_email.go b/server/plugin/email/service/sys_email.go index b031b379..6659fc6f 100644 --- a/server/plugin/email/service/sys_email.go +++ b/server/plugin/email/service/sys_email.go @@ -18,3 +18,16 @@ func (e *EmailService) EmailTest() (err error) { err = utils.EmailTest(subject, body) return err } + +//@author: [maplepie](https://github.com/maplepie) +//@function: EmailTest +//@description: 发送邮件测试 +//@return: err error +//@params to string 收件人 +//@params subject string 标题(主题) +//@params body string 邮件内容 + +func (e *EmailService) SendEmail(to, subject, body string) (err error) { + err = utils.Email(to, subject, body) + return err +} diff --git a/server/plugin/notify/README.MD b/server/plugin/notify/README.MD new file mode 100644 index 00000000..4b8d8f84 --- /dev/null +++ b/server/plugin/notify/README.MD @@ -0,0 +1,74 @@ +## GVA 钉钉群通知插件 + +本插件用于向钉钉群推送消息 + +### 1. 使用场景 + +- 当服务运行异常时,可以向钉钉推送异常信息,便于及时发现解决问题 +- 推送一些关键业务的运行日志等 + +### 2. 配置说明 + +钉钉 token 等相关信息的获取,请参考 [钉钉官网](https://developers.dingtalk.com/document/robots/custom-robot-access?spm=ding_open_doc.document.0.0.7f8710afbfzduV#topic-2026027) + +在`plugin/notify/global/global.go` 文件中配置钉钉通知的URL ,Token 等 + +```go + // 在gin-vue-admin 主程序的initialize中的plugin的InstallPlugin 函数中写入如下代码 + PluginInit(PublicGroup, notify.CreateDDPlug( + URL, + Token, + 密钥)) +} +``` + +### 3 参数说明 +#### 3-1 全局参数说明 + +```go + Url string `mapstructure:"url" json:"url" yaml:"url"` // Url + Token string `mapstructure:"token" json:"token" yaml:"token"` // access_token + Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 +``` +#### 3-2 请求入参说明 +```go + + +``` + +### 3方法API(可调用方法) +```go + +//content 发送的内容 +//atMobiles 需要艾特的人的手机号 +//isAtAll 是否艾特全体 +SendTextMessage(content string,atMobiles []string,isAtAll bool) + +//content 发送的内容 +//title 内容标题 +//picUrl 配图 +//messageUrl 点击跳转路径 +SendLinkMessage(content,title,picUrl,messageUrl string) + +//content 发送的内容(markdown语法) +//title 内容标题 +//atMobiles 需要艾特的人的手机号 +//isAtAll 是否艾特全体 +SendMarkdownMessage(content,title string,atMobiles []string,isAtAll bool) + +``` + +### 4. 可直接调用接口 + + 发送文字消息接口: /notify/sendTextMessage [post] 已配置swagger + 发送图文链接消息接口: /notify/sendLinkMessage [post] 已配置swagger + 发送markdown消息接口: /notify/sendMarkdownMessage [post] 已配置swagger + + 入参: + type Email struct { + To string `json:"to"` // 邮件发送给谁 + Subject string `json:"subject"` // 邮件标题 + Body string `json:"body"` // 邮件内容 + } + + diff --git a/server/plugin/notify/api/api.go b/server/plugin/notify/api/api.go new file mode 100644 index 00000000..e52102cb --- /dev/null +++ b/server/plugin/notify/api/api.go @@ -0,0 +1,67 @@ +package api + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + notify_response "github.com/flipped-aurora/gin-vue-admin/server/plugin/notify/model/response" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/notify/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type Api struct { +} + +// @Tags Notify +// @Summary 发送文字消息接口 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body notify_response.TextNotify true "发送文字消息的参数" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}" +// @Router /notify/sendTextMessage [post] +func (s *Api) SendTextMessage(c *gin.Context) { + var textNotify notify_response.TextNotify + _ = c.ShouldBindJSON(&textNotify) + if err := service.ServiceGroupApp.SendTextMessage(textNotify.Content, textNotify.AtMobiles, textNotify.IsAtAll); err != nil { + global.GVA_LOG.Error("发送失败!", zap.Any("err", err)) + response.FailWithMessage("发送失败", c) + } else { + response.OkWithData("发送成功", c) + } +} + +// @Tags Notify +// @Summary 发送图文链接消息接口 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body notify_response.LinkNotify true "发送图文链接消息的参数" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}" +// @Router /notify/sendLinkMessage [post] +func (s *Api) SendLinkMessage(c *gin.Context) { + var linkNotify notify_response.LinkNotify + _ = c.ShouldBindJSON(&linkNotify) + if err := service.ServiceGroupApp.SendLinkMessage(linkNotify.Content, linkNotify.Title, linkNotify.PicUrl, linkNotify.MessageUrl); err != nil { + global.GVA_LOG.Error("发送失败!", zap.Any("err", err)) + response.FailWithMessage("发送失败", c) + } else { + response.OkWithData("发送成功", c) + } +} + +// @Tags Notify +// @Summary 发送markdown消息接口 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body notify_response.MarkdownNotify true "发送markdown消息的参数" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}" +// @Router /notify/sendMarkdownMessage [post] +func (s *Api) SendMarkdownMessage(c *gin.Context) { + var markdownNotify notify_response.MarkdownNotify + _ = c.ShouldBindJSON(&markdownNotify) + if err := service.ServiceGroupApp.SendMarkdownMessage(markdownNotify.Content, markdownNotify.Title, markdownNotify.AtMobiles, markdownNotify.IsAtAll); err != nil { + global.GVA_LOG.Error("发送失败!", zap.Any("err", err)) + response.FailWithMessage("发送失败", c) + } else { + response.OkWithData("发送成功", c) + } +} diff --git a/server/plugin/notify/api/enter.go b/server/plugin/notify/api/enter.go new file mode 100644 index 00000000..5fe286c1 --- /dev/null +++ b/server/plugin/notify/api/enter.go @@ -0,0 +1,7 @@ +package api + +type ApiGroup struct { + Api +} + +var ApiGroupApp = new(ApiGroup) diff --git a/server/plugin/notify/config/dingding.go b/server/plugin/notify/config/dingding.go new file mode 100644 index 00000000..7ce67b1f --- /dev/null +++ b/server/plugin/notify/config/dingding.go @@ -0,0 +1,7 @@ +package config + +type DingDing struct { + Url string `mapstructure:"url" json:"url" yaml:"url"` // Url + Token string `mapstructure:"token" json:"token" yaml:"token"` // access_token + Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 +} diff --git a/server/plugin/notify/global/global.go b/server/plugin/notify/global/global.go new file mode 100644 index 00000000..12e15ca4 --- /dev/null +++ b/server/plugin/notify/global/global.go @@ -0,0 +1,5 @@ +package global + +import "github.com/flipped-aurora/gin-vue-admin/server/plugin/notify/config" + +var GlobalConfig_ = &config.DingDing{} diff --git a/server/plugin/notify/main.go b/server/plugin/notify/main.go new file mode 100644 index 00000000..2c2d9391 --- /dev/null +++ b/server/plugin/notify/main.go @@ -0,0 +1,28 @@ +package notify + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/plugin/notify/global" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/notify/router" + "github.com/gin-gonic/gin" +) + +type ddPlugin struct { + Secret string + Token string + Url string +} + +func CreateDDPlug(url, Token, Secret string) *ddPlugin { + global.GlobalConfig_.Url = url + global.GlobalConfig_.Token = Token + global.GlobalConfig_.Secret = Secret + return &ddPlugin{} +} + +func (*ddPlugin) Register(group *gin.RouterGroup) { + router.RouterGroupApp.InitRouter(group) +} + +func (*ddPlugin) RouterPath() string { + return "notify" +} diff --git a/server/plugin/notify/model/response/notify.go b/server/plugin/notify/model/response/notify.go new file mode 100644 index 00000000..1555ae58 --- /dev/null +++ b/server/plugin/notify/model/response/notify.go @@ -0,0 +1,21 @@ +package response + +type TextNotify struct { // 文字信息 + Content string `json:"content"` // 发送的内容 + AtMobiles []string `json:"atMobiles"` // 需要艾特的人的手机号 + IsAtAll bool `json:"isAtAll"` // 是否艾特全体 +} + +type LinkNotify struct { // 图文链接信息 + Content string `json:"content"` // 发送的内容 + Title string `json:"title"` // 内容标题 + PicUrl string `json:"picUrl"` // 配图 + MessageUrl string `json:"messageUrl"` // 点击跳转路径 +} + +type MarkdownNotify struct { // markdown信息 + Title string `json:"title"` // 内容标题 + Content string `json:"content"` // 发送的内容 + AtMobiles []string `json:"atMobiles"` // 需要艾特的人的手机号 + IsAtAll bool `json:"isAtAll"` // 是否艾特全体 +} diff --git a/server/plugin/notify/router/enter.go b/server/plugin/notify/router/enter.go new file mode 100644 index 00000000..30aca42c --- /dev/null +++ b/server/plugin/notify/router/enter.go @@ -0,0 +1,7 @@ +package router + +type RouterGroup struct { + NotifyRouter +} + +var RouterGroupApp = new(RouterGroup) diff --git a/server/plugin/notify/router/router.go b/server/plugin/notify/router/router.go new file mode 100644 index 00000000..b500d52c --- /dev/null +++ b/server/plugin/notify/router/router.go @@ -0,0 +1,18 @@ +package router + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/notify/api" + "github.com/gin-gonic/gin" +) + +type NotifyRouter struct { +} + +func (s *NotifyRouter) InitRouter(Router *gin.RouterGroup) { + router := Router.Use(middleware.OperationRecord()) + var SendTextMessage = api.ApiGroupApp.Api.SendTextMessage + { + router.POST("sendTextMessage", SendTextMessage) + } +} diff --git a/server/plugin/notify/service/enter.go b/server/plugin/notify/service/enter.go new file mode 100644 index 00000000..0eb00088 --- /dev/null +++ b/server/plugin/notify/service/enter.go @@ -0,0 +1,7 @@ +package service + +type ServiceGroup struct { + NotifyService +} + +var ServiceGroupApp = new(ServiceGroup) diff --git a/server/plugin/notify/service/notify.go b/server/plugin/notify/service/notify.go new file mode 100644 index 00000000..4e3045f6 --- /dev/null +++ b/server/plugin/notify/service/notify.go @@ -0,0 +1,157 @@ +package service + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/notify/global" + "io/ioutil" + "net/http" + "net/url" + "time" +) + +type NotifyService struct { +} + +//@author: [Espoir](https://github.com/nightsimon) +//@function: SendTextMessage +//@description: 发送钉钉文字信息 +//@params content string发送的文字内容 +//@params atMobiles []string 艾特的手机号 +//@params isAtAll bool 是否艾特全体 +//@return: err error + +func (e *NotifyService) SendTextMessage(content string, atMobiles []string, isAtAll bool) (err error) { + msg := map[string]interface{}{ + "msgtype": "text", + "text": map[string]string{ + "content": content, + }, + "at": map[string]interface{}{ + "atMobiles": atMobiles, + "isAtAll": isAtAll, + }, + } + return SendMessage(msg) +} + +//@author: [Espoir](https://github.com/nightsimon) +//@function: SendLinkMessage +//@description: 发送钉钉图文链接信息 +//@params content string 发送的文字内容 +//@params title string 发送的标题 +//@params picUrl string 艾特的手机号 +//@params messageUrl string 是否艾特全体 +//@return: err error + +func (e *NotifyService) SendLinkMessage(content, title, picUrl, messageUrl string) (err error) { + msg := map[string]interface{}{ + "msgtype": "link", + "link": map[string]string{ + "text": content, + "title": title, + "picUrl": picUrl, + "messageUrl": messageUrl, + }, + } + return SendMessage(msg) +} + +//@author: [Espoir](https://github.com/nightsimon) +//@function: SendMarkdownMessage +//@description: 发送钉钉Markdown信息 +//@params content 发送的文字内容 +//@params title 发送的标题 +//@params atMobiles []string 艾特的手机号 +//@params isAtAll bool 是否艾特全体 +//@return: err error + +func (e *NotifyService) SendMarkdownMessage(content, title string, atMobiles []string, isAtAll bool) (err error) { + msg := map[string]interface{}{ + "msgtype": "markdown", + "markdown": map[string]string{ + "text": content, + "title": title, + }, + "at": map[string]interface{}{ + "atMobiles": atMobiles, + "isAtAll": isAtAll, + }, + } + return SendMessage(msg) +} + +func SendMessage(msg interface{}) error { + body := bytes.NewBuffer(nil) + err := json.NewEncoder(body).Encode(msg) + if err != nil { + return fmt.Errorf("msg json failed, msg: %v, err: %v", msg, err.Error()) + } + + value := url.Values{} + value.Set("access_token", global.GlobalConfig_.Token) + if global.GlobalConfig_.Secret != "" { + t := time.Now().UnixNano() / 1e6 + value.Set("timestamp", fmt.Sprintf("%d", t)) + value.Set("sign", sign(t, global.GlobalConfig_.Secret)) + } + + request, err := http.NewRequest(http.MethodPost, global.GlobalConfig_.Url, body) + if err != nil { + return fmt.Errorf("error request: %v", err.Error()) + } + request.URL.RawQuery = value.Encode() + request.Header.Add("Content-Type", "application/json") + res, err := (&http.Client{}).Do(request) + if err != nil { + return fmt.Errorf("send dingTalk message failed, error: %v", err.Error()) + } + defer func() { _ = res.Body.Close() }() + result, err := ioutil.ReadAll(res.Body) + + if res.StatusCode != 200 { + return fmt.Errorf("send dingTalk message failed, %s", httpError(request, res, result, "http code is not 200")) + } + if err != nil { + return fmt.Errorf("send dingTalk message failed, %s", httpError(request, res, result, err.Error())) + } + + type response struct { + ErrCode int `json:"errcode"` + } + var ret response + + if err := json.Unmarshal(result, &ret); err != nil { + return fmt.Errorf("send dingTalk message failed, %s", httpError(request, res, result, err.Error())) + } + + if ret.ErrCode != 0 { + return fmt.Errorf("send dingTalk message failed, %s", httpError(request, res, result, "errcode is not 0")) + } + + return nil +} + +func httpError(request *http.Request, response *http.Response, body []byte, error string) string { + return fmt.Sprintf( + "http request failure, error: %s, status code: %d, %s %s, body:\n%s", + error, + response.StatusCode, + request.Method, + request.URL.String(), + string(body), + ) +} +func sign(t int64, secret string) string { + strToHash := fmt.Sprintf("%d\n%s", t, secret) + hmac256 := hmac.New(sha256.New, []byte(secret)) + hmac256.Write([]byte(strToHash)) + data := hmac256.Sum(nil) + return base64.StdEncoding.EncodeToString(data) +} + +// 其余方法请参考 https://developers.dingtalk.com/document/robots/custom-robot-access?spm=ding_open_doc.document.0.0.7f8710afbfzduV#topic-2026027 diff --git a/server/plugin/notify/utils/utils.go b/server/plugin/notify/utils/utils.go new file mode 100644 index 00000000..d4b585bf --- /dev/null +++ b/server/plugin/notify/utils/utils.go @@ -0,0 +1 @@ +package utils