feat: support dingtalk push

This commit is contained in:
engigu
2024-01-06 17:44:45 +08:00
parent 2730bb97ca
commit e6f7c2c64e
26 changed files with 418 additions and 121 deletions
+3 -3
View File
@@ -3,6 +3,7 @@ module message-nest
go 1.20
require (
github.com/blinkbean/dingtalk v0.0.0-20230927120905-796332ac4ba1
github.com/dgrijalva/jwt-go v3.1.0+incompatible
github.com/gin-gonic/gin v1.9.1
github.com/go-ini/ini v1.32.1-0.20180214101753-32e4be5f41bb
@@ -11,7 +12,6 @@ require (
github.com/go-playground/validator/v10 v10.16.0
github.com/google/uuid v1.5.0
github.com/jinzhu/gorm v0.0.0-20180213101209-6e1387b44c64
github.com/markbates/pkger v0.17.1
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.3
github.com/unknwon/com v1.0.1
@@ -19,7 +19,6 @@ require (
)
require (
github.com/blinkbean/dingtalk v0.0.0-20230927120905-796332ac4ba1 // indirect
github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
@@ -28,12 +27,12 @@ require (
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-sql-driver/mysql v1.4.1-0.20190510102335-877a9775f068 // indirect
github.com/gobuffalo/here v0.6.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d // indirect
github.com/jinzhu/now v1.0.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lib/pq v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -52,6 +51,7 @@ require (
google.golang.org/appengine v1.6.3 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/ini.v1 v1.47.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
-7
View File
@@ -37,8 +37,6 @@ github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqR
github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.4.1-0.20190510102335-877a9775f068 h1:q2kwd9Bcgl2QpSi/Wjcx9jzwyICt3EWTP5to43QhwaA=
github.com/go-sql-driver/mysql v1.4.1-0.20190510102335-877a9775f068/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI=
github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
@@ -75,8 +73,6 @@ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno=
github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
@@ -104,7 +100,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -161,8 +156,6 @@ gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AW
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/ini.v1 v1.47.0 h1:XAVsOWcIxjm6JVEbCbSZgSBZIF0BrCzXs4orAQr6uc8=
gopkg.in/ini.v1 v1.47.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+5 -1
View File
@@ -14,7 +14,11 @@ type SendTasksIns struct {
// InsEmailConfig 实例里面的邮箱config
type InsEmailConfig struct {
ToAccount string `json:"to_account" validate:"required,email" label:"收件邮箱"`
Title string `json:"title" validate:"required,max=150" label:"邮箱标题"`
//Title string `json:"title" validate:"required,max=150" label:"邮箱标题"`
}
// InsEmailConfig 实例里面的邮箱config
type InsDtalkConfig struct {
}
// ManyAddTaskIns 批量添加实例
+1
View File
@@ -122,6 +122,7 @@ func GetTasksIns(id string) (TaskIns, error) {
Select(fmt.Sprintf("%s.*, %s.name as way_name", insTable, waysTable)).
Joins(fmt.Sprintf("JOIN %s ON %s.way_id = %s.id", waysTable, insTable, waysTable)).
Where(fmt.Sprintf("%s.task_id = ?", insTable), id).
Order(fmt.Sprintf("%s.created_on DESC", insTable)).
Scan(&taskIns)
taskResult.ID = task.ID
+83 -3
View File
@@ -1,9 +1,89 @@
package message
type DTalkMessage struct {
WebhookUrl string
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
)
type response struct {
Code int `json:"errcode"`
Msg string `json:"errmsg"`
}
func (d *DTalkMessage) send(content string) {
type Dtalk struct {
AccessToken string
Secret string
}
func (t *Dtalk) Request(msg interface{}) ([]byte, error) {
b, err := json.Marshal(msg)
if err != nil {
return nil, err
}
resp, err := http.Post(t.getURL(), "application/json", bytes.NewBuffer(b))
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var r response
err = json.Unmarshal(body, &r)
if err != nil {
return body, err
}
if r.Code != 0 {
return body, errors.New(fmt.Sprintf("response error: %s", string(body)))
}
return body, err
}
// SendMessageText Function to send message
func (t *Dtalk) SendMessageText(text string, at ...string) ([]byte, error) {
msg := map[string]interface{}{
"msgtype": "text",
"text": map[string]string{
"content": text,
},
}
resp, err := t.Request(msg)
return resp, err
}
func (t *Dtalk) SendMessageMarkdown(title, text string, at ...string) ([]byte, error) {
msg := map[string]interface{}{
"msgtype": "markdown",
"markdown": map[string]string{
"title": title,
"text": text,
},
}
resp, err := t.Request(msg)
return resp, err
}
func (t *Dtalk) hmacSha256(stringToSign string, secret string) string {
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(stringToSign))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func (t *Dtalk) getURL() string {
wh := "https://oapi.dingtalk.com/robot/send?access_token=" + t.AccessToken
timestamp := time.Now().UnixNano() / 1e6
stringToSign := fmt.Sprintf("%d\n%s", timestamp, t.Secret)
sign := t.hmacSha256(stringToSign, t.Secret)
url := fmt.Sprintf("%s&timestamp=%d&sign=%s", wh, timestamp, sign)
return url
}
+3 -1
View File
@@ -11,6 +11,7 @@ import (
type SendMessageReq struct {
TaskID string `json:"task_id" validate:"required,len=36" label:"任务id"`
Text string `json:"text" validate:"required" label:"文本内容"`
Title string `json:"title" label:"消息标题"`
HTML string `json:"html" label:"html内容"`
MarkDown string `json:"markdown" label:"markdown内容"`
Mode string `json:"mode" label:"是否异步发送"`
@@ -31,11 +32,12 @@ func DoSendMassage(c *gin.Context) {
msgService := send_message_service.SendMessageService{
TaskID: req.TaskID,
Title: req.Title,
Text: req.Text,
HTML: req.HTML,
MarkDown: req.MarkDown,
}
if req.Mode != "async" {
if req.Mode == "sync" {
// 同步发送
err := msgService.Send()
if err != "" {
+7
View File
@@ -44,6 +44,7 @@ func ClearLogs() {
// 启动注册清除任务定时任务
func (cs *CronService) InitLogsCronRun() {
// 注册任务
setting, err := models.GetSettingByKey(constant.LogsCleanSectionName, constant.LogsCleanCronKeyName)
if err != nil {
logging.Logger.Error(fmt.Sprintf("获取日志的cron失败,原因:%s", err))
@@ -52,6 +53,12 @@ func (cs *CronService) InitLogsCronRun() {
Schedule: setting.Value,
Job: ClearLogs,
})
// 添加任务
err = models.AddSendTaskWithID("日志定时清除", constant.CleanLogsTaskId, "admin")
if err != nil {
logging.Logger.Error(fmt.Sprintf("添加日志定时清除任务失败,原因:%s", err))
}
}
// 更新清除任务定时任务
+4
View File
@@ -30,6 +30,10 @@ func (sw *SendTaskInsService) ValidateDiffIns(ins models.SendTasksIns) (string,
_, Msg := app.CommonPlaygroundValid(emailConfig)
return Msg, emailConfig
}
if ins.WayType == "Dtalk" {
var Config models.InsDtalkConfig
return "", Config
}
return "未知的渠道的config校验", empty
}
@@ -0,0 +1,45 @@
package send_message_service
import (
"fmt"
"message-nest/models"
"message-nest/pkg/message"
"message-nest/service/send_ins_service"
"message-nest/service/send_way_service"
)
type DtalkService struct {
}
// SendDtalkMessage 执行发送钉钉
func (s *DtalkService) SendDtalkMessage(auth send_way_service.WayDetailDTalk, ins models.SendTasksIns, typeC string, title string, content string) string {
insService := send_ins_service.SendTaskInsService{}
errStr, c := insService.ValidateDiffIns(ins)
if errStr != "" {
return errStr
}
_, ok := c.(models.InsDtalkConfig)
if !ok {
return "钉钉config校验失败"
}
errMsg := ""
cli := message.Dtalk{
AccessToken: auth.AccessToken,
Secret: auth.Secret,
}
if typeC == "text" {
_, err := cli.SendMessageText(content)
if err != nil {
errMsg = fmt.Sprintf("发送失败:%s", ins.ContentType)
}
} else if typeC == "markdown" {
_, err := cli.SendMessageMarkdown(title, content)
if err != nil {
errMsg = fmt.Sprintf("发送失败:%s", ins.ContentType)
}
} else {
errMsg = fmt.Sprintf("未知的钉钉发送内容类型:%s", ins.ContentType)
}
return errMsg
}
+3 -3
View File
@@ -12,7 +12,7 @@ type EmailService struct {
}
// SendTaskEmail 执行发送邮件
func (s *EmailService) SendTaskEmail(auth send_way_service.WayDetailEmail, ins models.SendTasksIns, typeC string, content string) string {
func (s *EmailService) SendTaskEmail(auth send_way_service.WayDetailEmail, ins models.SendTasksIns, typeC string, title string, content string) string {
insService := send_ins_service.SendTaskInsService{}
errStr, c := insService.ValidateDiffIns(ins)
if errStr != "" {
@@ -27,9 +27,9 @@ func (s *EmailService) SendTaskEmail(auth send_way_service.WayDetailEmail, ins m
errMsg := ""
emailer.Init(auth.Server, auth.Port, auth.Account, auth.Passwd)
if typeC == "text" {
errMsg = emailer.SendTextMessage(config.ToAccount, config.Title, content)
errMsg = emailer.SendTextMessage(config.ToAccount, title, content)
} else if typeC == "html" {
errMsg = emailer.SendHtmlMessage(config.ToAccount, config.Title, content)
errMsg = emailer.SendHtmlMessage(config.ToAccount, title, content)
} else {
errMsg = fmt.Sprintf("未知的邮件发送内容类型:%s", ins.ContentType)
}
+14 -3
View File
@@ -23,6 +23,7 @@ func errStrIsSuccess(errStr string) int {
type SendMessageService struct {
TaskID string
Title string
Text string
HTML string
MarkDown string
@@ -46,6 +47,7 @@ func (sm *SendMessageService) Send() string {
errStr := ""
sm.LogsAndStatusMark(fmt.Sprintf("开始任务[%s]的发送", sm.TaskID), sm.Status)
sm.LogsAndStatusMark(fmt.Sprintf("发送标题:%s \n", sm.Title), sm.Status)
sendTaskService := send_task_service.SendTaskService{
ID: sm.TaskID,
@@ -93,13 +95,21 @@ func (sm *SendMessageService) Send() string {
// 邮箱类型的实例发送
emailAuth, ok := msgObj.(send_way_service.WayDetailEmail)
if ok {
//continue
es := EmailService{}
errMsg := es.SendTaskEmail(emailAuth, ins.SendTasksIns, typeC, content)
errMsg := es.SendTaskEmail(emailAuth, ins.SendTasksIns, typeC, sm.Title, content)
sm.LogsAndStatusMark(sm.TransError(errMsg), errStrIsSuccess(errMsg))
continue
}
sm.LogsAndStatusMark(fmt.Sprintf("未知渠道的发信实例: %s", ins.ID), sm.Status)
// 钉钉类型的实例发送
dtalkAuth, ok := msgObj.(send_way_service.WayDetailDTalk)
if ok {
es := DtalkService{}
errMsg := es.SendDtalkMessage(dtalkAuth, ins.SendTasksIns, typeC, sm.Title, content)
sm.LogsAndStatusMark(sm.TransError(errMsg), errStrIsSuccess(errMsg))
continue
}
sm.LogsAndStatusMark(fmt.Sprintf("发送失败:未知渠道的发信实例: %s\n", ins.ID), SendFail)
}
@@ -165,6 +175,7 @@ func (sm *SendMessageService) GetSendMsg(ins models.SendTasksIns) (string, strin
logging.Logger.Error("text节点数据为空!")
return "text", ""
} else {
logging.Logger.Error(fmt.Sprintf("没有找到%s对应的消息,使用text消息替代!", ins.ContentType))
return "text", content
}
} else {
+15 -2
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/blinkbean/dingtalk"
"message-nest/models"
"message-nest/pkg/app"
"message-nest/pkg/message"
@@ -33,7 +34,9 @@ type WayDetailEmail struct {
// WayDetailDTalk 钉钉渠道明细字段
type WayDetailDTalk struct {
WebhookUrl string `validate:"required,url" label:"钉钉webhookUrl地址"`
AccessToken string `json:"access_token" validate:"required,max=100" label:"钉钉access_token"`
Keys string `json:"keys" validate:"max=200" label:"钉钉关键字"`
Secret string `json:"secret" validate:"max=100" label:"钉钉加签秘钥"`
}
func (sw *SendWay) GetByID() (interface{}, error) {
@@ -106,12 +109,22 @@ func (sw *SendWay) ValidateDiffWay() (string, interface{}) {
// TestSendWay 尝试带发信测试连通性
func (sw *SendWay) TestSendWay(msgObj interface{}) string {
testMsg := "This is a test message from message-nest."
emailAuth, ok := msgObj.(WayDetailEmail)
if ok {
var emailer message.EmailMessage
emailer.Init(emailAuth.Server, 465, emailAuth.Account, emailAuth.Passwd)
errMsg := emailer.SendTextMessage(emailAuth.Account, "test", "This is a test email from message-nest.")
errMsg := emailer.SendTextMessage(emailAuth.Account, "test email", testMsg)
return errMsg
}
dtalkAuth, ok := msgObj.(WayDetailDTalk)
if ok {
cli := dingtalk.InitDingTalkWithSecret(dtalkAuth.AccessToken, dtalkAuth.Secret)
err := cli.SendTextMessage(testMsg + dtalkAuth.Keys)
if err != nil {
return fmt.Sprintf("发送失败:%s", err)
}
return ""
}
return fmt.Sprintf("未知的发信渠道校验: %s", sw.Type)
}
+39 -9
View File
@@ -13,24 +13,54 @@ const CONSTANT = {
type: 'Email',
label: '邮箱',
inputs: [
{ subLabel: 'smtp服务地址', value: '', col: 'server' },
{ subLabel: 'smtp服务端口', value: '', col: 'port' },
{ subLabel: '邮箱账号', value: '', col: 'account' },
{ subLabel: '邮箱密码', value: '', col: 'passwd' },
{ subLabel: '渠道名', value: '', col: 'name' },
]
{ subLabel: 'smtp服务地址', value: '', col: 'server', desc: "smtp@xyz.com" },
{ subLabel: 'smtp服务端口', value: '', col: 'port', desc: "port" },
{ subLabel: '邮箱账号', value: '', col: 'account', desc: "邮箱账号" },
{ subLabel: '邮箱密码', value: '', col: 'passwd', desc: "邮箱密码" },
{ subLabel: '渠道名', value: '', col: 'name', desc: "想要设置的渠道名字" },
],
taskInsRadios: [
{ subLabel: 'text', content: 'text' },
{ subLabel: 'html', content: 'html' },
],
taskInsInputs: [
{ value: '', col: 'to_account', desc: "目的邮箱账号(发给谁)" },
// { value: '', col: 'title', desc: "邮箱标题" },
],
},
{
type: 'Dtalk',
label: '钉钉',
inputs: [
{ subLabel: 'webhook地址', value: '', col: 'webhook_url' },
{ subLabel: '渠道名', value: '', col: 'name' },
]
{ subLabel: 'access_token', value: '', col: 'access_token', desc: "钉钉webhook中的access_token" },
// { subLabel: '关键字过滤', value: '', col: 'keys', desc: "使用的关键字过滤,多个使用|隔开" },
{ subLabel: '加签', value: '', col: 'secret', desc: "加签的签名,SEC开头" },
{ subLabel: '渠道名', value: '', col: 'name', desc: "想要设置的渠道名字" },
],
tips: {
text: "输入框说明", desc: "钉钉支持加签和关键字过滤,如果是配置了关键字过滤,只需要消息里面包含了关键字,就会发送"
},
taskInsRadios: [
{ subLabel: 'text', content: 'text' },
{ subLabel: 'markdown', content: 'markdown' },
],
taskInsInputs: [
// { value: '', col: 'to_account', desc: "目的邮箱账号(发给fsfsf谁)" },
// { value: '', col: 'title', desc: "邮箱标题fsfsf" },
],
},
],
LOGO: `<svg t="1702547210136" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1861" width="200" height="200"><path d="M970.091313 224.033616a14.201535 14.201535 0 0 0-14.100687-3.631838c-88.419556-13.429657-142.919111-12.953859-248.242424 23.762747-69.454869 24.217859-109.056 79.873293-135.267556 116.712728-6.692202 9.403475-18.949172 26.625293-23.77309 29.509818-5.70699-1.303273-21.929374-16.036202-31.762101-24.965172-28.626747-25.994343-64.252121-58.353778-106.767516-58.757172-0.312889-0.005172-0.627071-0.005172-0.939959-0.005171-29.62101 0-54.798222 13.434828-72.857859 38.89907-13.222788 18.651798-17.914828 37.388929-18.106182 38.176323a14.31402 14.31402 0 0 0 2.893576 12.515556 14.336 14.336 0 0 0 11.782465 5.095434l0.409858-0.024565c19.192242-1.152 59.141172-3.545212 65.807516 81.857939 4.288646 54.899071 29.742545 108.631919 69.833697 147.419798 45.499475 44.025535 106.237414 67.298263 175.650909 67.298263 98.97503 0 166.222869-24.480323 205.207272-45.015919 43.172202-22.742626 62.621737-45.918384 63.429819-46.898425a14.170505 14.170505 0 0 0 1.529535-15.874586 14.191192 14.191192 0 0 0-14.166626-7.337373c-37.924202 4.443798-92.767677 6.09099-138.666667-11.298909-7.580444-2.872889-13.878303-5.893172-18.959515-8.691071 20.273131-9.964606 51.000889-28.383677 83.621495-59.93503 34.186343-33.065374 37.722505-69.908687 40.838464-102.423273 2.297535-23.919192 4.666182-48.651636 18.500526-73.091879 28.807758-50.883232 66.645333-74.489535 74.560646-79.035475a14.201535 14.201535 0 0 0 12.135434-7.776969 14.193778 14.193778 0 0 0-2.59103-16.484849z" fill="" p-id="1862"></path><path d="M674.834101 716.481939c-33.422222 4.90796-74.989899 16.884364-117.22602-8.102787-34.843152-20.613172-53.056646-20.993293-77.783919-18.49794-0.156444 0.015515-0.312889 0.015515-0.469334 0.034909-0.274101 0.033616-0.548202 0.060768-0.822303 0.094384-4.697212 0.487434-9.65301 1.065374-14.995394 1.62004-6.702545 0.649051-13.414141 1.19596-20.133495 1.634263-75.333818 4.102465-136.860444-5.381172-163.012525-10.404202-49.488162-9.872808-86.57196-23.921778-108.946101-33.970424-59.832889-26.868364-87.080081-56.671677-87.080081-72.989738 0-1.873455 0.384-2.222545 0.787394-2.585858 2.196687-1.974303 10.616242-6.520242 41.302626-6.004364 24.793212 0.418909 56.970343 3.858101 94.308849 7.853253 40.722101 4.348121 86.873212 9.283232 136.348444 11.686788 6.913293 0.333576 13.959758 0.625778 20.93899 0.858505 9.510788 0.307717 17.545051-7.182222 17.868283-16.707233 0.319354-9.535354-7.175758-17.550222-16.705939-17.868282a1386.24 1386.24 0 0 1-20.419233-0.839112c-48.521051-2.358303-94.161455-7.236525-134.434909-11.544565-90.868364-9.712485-139.353212-13.818828-162.333737 6.833131-8.02004 7.21196-12.262141 17.004606-12.262141 28.317737 0 0.672323 0.029737 1.348525 0.065939 2.026021l0.005172 0.100848c-0.045253 0.524929-0.071111 1.039515-0.071111 1.539879 0 41.464242 9.561212 81.661414 28.414707 119.484768 17.493333 35.088808 42.251636 66.663434 73.583192 93.927434 9.169455-6.551273 20.155475-13.604202 32.524929-20.050748 49.737697-25.921939 97.838545-29.339152 139.099798-9.884444 101.590626 47.906909 142.060606 47.837091 206.198949-0.364606 5.70699-4.287354 13.814949-3.140525 18.103596 2.567758 4.289939 5.708283 3.140525 13.813657-2.567757 18.103595-38.425859 28.877576-69.968162 41.279354-105.143596 41.280647-0.484848 0-0.969697-0.002586-1.457132-0.007758-32.760242-0.316768-69.311354-11.381657-126.16404-38.190545-51.905939-24.47903-106.215434 1.008485-139.548444 23.465374 64.611556 47.922424 146.341495 74.101657 232.704 74.101656 117.55701 0 226.204444-48.934788 292.761858-131.362909l0.002586-0.002586c18.79402-14.959192 12.028121-41.362101-23.442101-36.152889z" fill="" p-id="1863"></path></svg>`,
}
CONSTANT.WAYS_DATA_MAP = {};
CONSTANT.WAYS_DATA.forEach(element => {
CONSTANT.WAYS_DATA_MAP[element.type] = element
});
console.log('xxxxxxxx', CONSTANT.WAYS_DATA_MAP)
export { CONSTANT }
+39
View File
@@ -0,0 +1,39 @@
class CommonUtils {
// static getCurlString(task_id, options) {
// let data = { task_id: task_id };
// data.title = 'message title';
// data.text = 'Hello World!';
// if (options.html) {
// data.html = '<h1> Hello World! </h1>';
// }
// if (options.markdown) {
// data.html = '**Hello World!**';
// }
// let dataStr = JSON.stringify(data, null, 4);
// let example = `curl -X POST --location '${gethttpOrigin()}/api/v1/message/send' \\
// --header 'Content-Type: application/json' \\
// --data '${dataStr}'`;
// return example;
// }
static formatInsConfigDisplay = (scope) => {
if (!scope.row.config) {
return ""
}
if (scope.row.way_type == "Email") {
let config = JSON.parse(scope.row.config)
let info = `发送账号:${config.to_account}`
return info
} else {
return "暂无"
}
}
}
export { CommonUtils };
+5 -4
View File
@@ -8,13 +8,14 @@ class ApiStrGenerate {
static getCurlString(task_id, options) {
let data = { task_id: task_id, mode: "async" };
let data = { task_id: task_id };
data.title = 'message title';
data.text = 'Hello World!';
if (options.html) {
if (options.html ) {
data.html = '<h1> Hello World! </h1>';
}
if (options.markdown) {
data.html = '** Hello World! **';
if (options.markdown ) {
data.markdown = '**Hello World!**';
}
let dataStr = JSON.stringify(data, null, 4);
let example = `curl -X POST --location '${gethttpOrigin()}/api/v1/message/send' \\
+32
View File
@@ -0,0 +1,32 @@
<template>
<div class="tips">
<el-text size="small">{{ text }}</el-text>
<el-tooltip v-if="desc" placement="top">
<template #content>
<div v-html="desc"></div>
</template>
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import { QuestionFilled } from '@element-plus/icons-vue'
export default defineComponent({
components: {
QuestionFilled,
},
props: {
text: String,
desc: String,
},
methods: {
},
});
</script>
<style scoped></style>
@@ -6,7 +6,7 @@
</div>
<hr />
<div ref="refContainer">
<el-table :data="tableData" empty-text="发信日志为空" :row-style="rowStyle()">
<el-table :data="tableData" stripe empty-text="发信日志为空" :row-style="rowStyle()">
<el-table-column label="ID" prop="id" width="100px" />
<el-table-column label="任务名" prop="task_name" show-overflow-tooltip width="150px" />
<el-table-column label="发信日志" prop="log">
@@ -13,16 +13,24 @@
<hr />
<div ref="refContainer">
<el-table :data="tableData" empty-text="发信任务为空" :row-style="rowStyle()">
<el-table :data="tableData" stripe empty-text="发信任务为空" :row-style="rowStyle()">
<el-table-column label="ID" prop="id" width="320px" />
<el-table-column label="任务名" prop="name" />
<el-table-column label="创建时间" prop="created_on" />
<el-table-column fixed="right" label="操作" width="190px">
<template #default="scope">
<el-button link size="small" type="primary" @click="handleViewAPI(scope.$index, scope.row)">接口</el-button>
<el-button link size="small" type="primary" @click="handleViewLogs(scope.$index, scope.row)">日志</el-button>
<el-button link size="small" type="primary" @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<tableDeleteButton @customHandleDelete="handleDelete(scope.$index, scope.row)" />
<div v-if="sysKeptTaskIds.includes(scope.row.id)">
<el-button link size="small" type="primary"
@click="handleViewLogs(scope.$index, scope.row)">日志</el-button>
<el-text class="mx-1" type="info" size="small" style="margin-left: 15px;">系统保留任务</el-text>
</div>
<div v-if="!sysKeptTaskIds.includes(scope.row.id)">
<el-button link size="small" type="primary" @click="handleViewAPI(scope.$index, scope.row)">接口</el-button>
<el-button link size="small" type="primary"
@click="handleViewLogs(scope.$index, scope.row)">日志</el-button>
<el-button link size="small" type="primary" @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<tableDeleteButton @customHandleDelete="handleDelete(scope.$index, scope.row)" />
</div>
</template>
</el-table-column>
</el-table>
@@ -75,7 +83,7 @@ export default {
// confirmBtnVisible: false,
tableData: [],
total: CONSTANT.TOTAL,
pageSize: CONSTANT.PAGE_SIZE,
pageSize: CONSTANT.PAGE_SIZE,
currPage: CONSTANT.PAGE,
displayCols: [
{ 'col': 'id', 'label': '任务ID' },
@@ -86,7 +94,9 @@ export default {
options: [
{ label: '邮箱', value: 'Email' },
{ label: '钉钉', value: 'Dtalk' }
]
],
sysKeptTaskIds: [CONSTANT.LOG_TASK_ID],
});
const handleEdit = (index, row) => {
@@ -122,7 +132,7 @@ export default {
}
const handleViewLogs = (index, row) => {
router.push('/sendlogs?name=' + row.name, { replace: true });
router.push('/sendlogs?taskid=' + row.id, { replace: true });
}
const filterFunc = async () => {
@@ -37,19 +37,21 @@
<el-text class="mx-1" size="small">渠道创建时间{{ currWayTmp.created_on }}</el-text><br />
</div>
<el-radio-group v-model="currInsInput.content_type" class="ml-4">
<el-radio label="text" size="small">text</el-radio>
<el-radio label="html" size="small">html</el-radio>
<el-radio-group v-model="currInsInputContentType">
<el-radio v-for="item in CONSTANT.WAYS_DATA_MAP[currWayTmp.type].taskInsRadios" :label="item.subLabel"
size="small">
{{ item.content }}
</el-radio>
</el-radio-group>
<div>
<el-input v-model="currInsInput.toAccount" placeholder="目的邮箱账号(发给谁)" size="small"
style="width: 200px; margin: 10px 40px 5px 0;" class="searchInput"></el-input>
<el-input v-model="currInsInput.title" placeholder="邮箱标题" size="small"
style="width: 200px; margin: 0px 40px 5px 0;" class="searchInput"></el-input>
<el-button @click="clickStore()" size="small" style="width: 200px">暂存</el-button>
<el-input v-model="currInsInput[item.col]"
v-for="item in CONSTANT.WAYS_DATA_MAP[currWayTmp.type].taskInsInputs" :placeholder="item.desc" size="small"
style="width: 200px; margin: 10px 40px 5px 0;" class="searchInput">
</el-input>
</div>
<el-button @click="clickStore()" size="small" style="width: 200px">暂存</el-button>
</div>
</div>
@@ -97,6 +99,7 @@ import { QuestionFilled } from '@element-plus/icons-vue'
import { v4 as uuidv4 } from 'uuid';
import { usePageState } from '@/store/page_sate.js';
import { request } from '@/api/api'
import { CONSTANT } from '@/constant'
export default defineComponent({
@@ -114,9 +117,8 @@ export default defineComponent({
isShowAddBox: false,
searchWayID: '',
currWayTmp: {},
currInsInput: {
content_type: 'text'
},
currInsInput: {},
currInsInputContentType: 'text',
currTaskInput: {
taskName: '',
taskId: uuidv4(),
@@ -139,7 +141,8 @@ export default defineComponent({
// 页面每次弹出,重置数据
const resetPageInitData = () => {
state.insTableData = [];
state.currInsInput = { content_type: 'text' };
state.currInsInputContentType = 'text';
state.currInsInput = {};
state.currWayTmp = {};
state.searchWayID = '';
state.isShowAddBox = false;
@@ -163,8 +166,8 @@ export default defineComponent({
way_id: state.currWayTmp.id,
way_type: state.currWayTmp.type,
way_name: state.currWayTmp.name,
content_type: state.currInsInput.content_type,
config: JSON.stringify({ to_account: state.currInsInput.toAccount, title: state.currInsInput.title })
content_type: state.currInsInputContentType,
config: JSON.stringify(state.currInsInput)
};
state.insTableData.push(insData);
}
@@ -175,6 +178,10 @@ export default defineComponent({
state.isShowAddBox = Boolean(data.data);
if (data.data) {
state.currWayTmp = data.data;
// 初始化currInsInput
CONSTANT.WAYS_DATA_MAP[data.data.type].taskInsInputs.forEach(element => {
state.currInsInput[element.col] = ""
});
}
}
@@ -184,8 +191,6 @@ export default defineComponent({
return postData
}
const handleSubmit = async () => {
let postData = getFinalData();
const rsp = await request.post('/sendtasks/ins/addmany', postData);
@@ -196,17 +201,12 @@ export default defineComponent({
}
const formatExtraInfo = (scope) => {
if (!scope.row.config) {
return ""
}
let config = JSON.parse(scope.row.config)
let info = `发送账号:${config.to_account}\n标题:${config.title}`
return info
return CommonUtils.formatInsConfigDisplay(scope);
}
return {
...toRefs(state), handleCancer, handleSubmit,
searchID, formatExtraInfo,
searchID, formatExtraInfo, CONSTANT,
clickStore, insRowStyle
};
},
@@ -2,8 +2,6 @@
<el-dialog v-model="isShow" width="58%" :close-on-press-escape="false" :before-close="() => { }" :show-close="false">
<template #header="">
<el-text class="mx-1">编辑发信任务</el-text>
<el-tooltip placement="top">
</el-tooltip>
</template>
<div class="add-top">
@@ -29,15 +27,19 @@
<el-text class="mx-1" size="small">渠道创建时间{{ currWayTmp.created_on }}</el-text><br />
</div>
<el-radio-group v-model="currInsInput.content_type" class="ml-4">
<el-radio label="text" size="small">text</el-radio>
<el-radio label="html" size="small">html</el-radio>
<el-radio-group v-model="currInsInputContentType">
<el-radio v-for="item in CONSTANT.WAYS_DATA_MAP[currWayTmp.type].taskInsRadios" :label="item.subLabel"
size="small">
{{ item.content }}
</el-radio>
</el-radio-group>
<div>
<el-input v-model="currInsInput.toAccount" placeholder="目的邮箱账号(发给谁)" size="small"
style="width: 200px; margin: 10px 40px 5px 0;" class="searchInput"></el-input>
<el-input v-model="currInsInput.title" placeholder="邮箱标题" size="small"
style="width: 200px; margin: 0px 40px 5px 0;" class="searchInput"></el-input>
<el-input v-model="currInsInput[item.col]"
v-for="item in CONSTANT.WAYS_DATA_MAP[currWayTmp.type].taskInsInputs" :placeholder="item.desc" size="small"
style="width: 200px; margin: 10px 40px 5px 0;" class="searchInput">
</el-input>
</div>
<div>
<el-button @click="handleAddSubmit()" size="small" style="width: 200px">添加</el-button>
@@ -87,6 +89,8 @@ import { usePageState } from '@/store/page_sate.js';
import { request } from '@/api/api'
import tableDeleteButton from '@/views/common/tableDeleteButton.vue'
import { ElMessage } from 'element-plus'
import { CONSTANT } from '@/constant'
import { CommonUtils } from "@/util/commonUtils.js";
export default defineComponent({
@@ -104,6 +108,7 @@ export default defineComponent({
isShow: false,
isShowAddBox: false,
searchWayID: '',
currInsInputContentType: '',
currWayTmp: {},
currInsInput: {},
currTaskInput: {
@@ -140,7 +145,9 @@ export default defineComponent({
state.insTableData = [];
state.currInsInput = {};
state.currWayTmp = {};
state.currInsInput = {};
state.searchWayID = '';
state.currInsInputContentType = '';
state.isShowAddBox = false;
state.currTaskInput = {
taskName: '',
@@ -162,12 +169,7 @@ export default defineComponent({
}
const formatExtraInfo = (scope) => {
if (!scope.row.config) {
return ""
}
let config = JSON.parse(scope.row.config)
let info = `发送账号:${config.to_account}\n标题:${config.title}`
return info
return CommonUtils.formatInsConfigDisplay(scope);
}
const searchID = async () => {
@@ -186,8 +188,8 @@ export default defineComponent({
way_id: state.currWayTmp.id,
way_type: state.currWayTmp.type,
way_name: state.currWayTmp.name,
content_type: state.currInsInput.content_type,
config: JSON.stringify({ to_account: state.currInsInput.toAccount, title: state.currInsInput.title })
content_type: state.currInsInputContentType,
config: JSON.stringify(state.currInsInput)
}
return postData
}
@@ -210,7 +212,7 @@ export default defineComponent({
}
return {
...toRefs(state), handleCancer, handleAddSubmit, handleEditTask,
...toRefs(state), handleCancer, handleAddSubmit, handleEditTask,CONSTANT,
searchID, handleDelete, insRowStyle, formatExtraInfo
};
},
@@ -8,9 +8,9 @@
<br />
实例的内容类型大体上可以可以分为texthtmlmarkdown
<br />
发送的消息会优先现在相应的类型消息进行发送如果没有将使用传的text消息进行发送
发送的消息会优先现在相应的类型消息进行发送如果没有将使用传的text消息进行发送
<br />
** text节点必传指定mode=async将步发送否则同步发送
** text节点必传指定mode=sync将步发送默认异步发送
</template>
<el-icon>
<QuestionFilled />
@@ -40,6 +40,7 @@ import { usePageState } from '@/store/page_sate.js';
import Prism from "prismjs";
import { ApiStrGenerate } from "@/util/viewApi.js";
import { QuestionFilled } from '@element-plus/icons-vue'
import { request } from '@/api/api'
export default defineComponent({
components: {
@@ -70,10 +71,24 @@ export default defineComponent({
}
}
const renderApiString = () => {
// 获取接口查看格式
const getViewOptions = async (taskId) => {
let params = { id: taskId };
const rsp = await request.get('/sendtasks/ins/gettask', { params: params });
let insTableData = await rsp.data.data.ins_data;
let viewOptions = {}
insTableData.forEach(element => {
viewOptions[element.content_type] = 1;
});
return viewOptions
}
const renderApiString = async () => {
let task_id = pageState.ShowDialogData[props.componentName].rowData.id;
let options = await getViewOptions(task_id)
if (state.activeName == 'curl') {
state.currCode = ApiStrGenerate.getCurlString(task_id, {});
state.currCode = ApiStrGenerate.getCurlString(task_id, options);
}
setTimeout(() => {
Prism.highlightAll()
@@ -18,7 +18,7 @@
<hr />
<div ref="refContainer">
<el-table :data="tableData" empty-text="发信渠道为空" :row-style="rowStyle()">
<el-table :data="tableData" stripe empty-text="发信渠道为空" :row-style="rowStyle()">
<el-table-column label="ID" width="320px">
<template #default="scope">
{{ scope.row.id }}
@@ -8,34 +8,23 @@
<el-form label-width="100px" v-for="item in waysLabelData">
<el-tab-pane :label="item.label" :name="item.type">
<el-form-item :label="one.subLabel" v-for="one in item.inputs">
<el-input v-model="one.value" />
<el-input size="small" v-model="one.value" :placeholder="one.desc" />
</el-form-item>
<textTips v-if="item.tips" :text="item.tips.text" :desc="item.tips.desc" />
</el-tab-pane>
</el-form>
</el-tabs>
<template #footer>
<span class="dialog-footer">
<el-button @click="testMseeageDialogVisible = true" size="small">测试发信</el-button>
<el-button @click="handleCancer()" size="small">取消</el-button>
<testSendButton @customhandleSubmit="handleTest()" />
<el-button type="primary" size="small" @click="handleSubmit()">
确定添加
</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="testMseeageDialogVisible" width="50%" align-center>
<span>将发送一条测试信息将注意查收</span>
<template #footer>
<span class="dialog-footer">
<el-button size="small" @click="testMseeageDialogVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="(testMseeageDialogVisible = false) || handleTest()">
确定发送
</el-button>
</span>
</template>
</el-dialog>
</template>
<script>
@@ -45,9 +34,15 @@ import { request } from '@/api/api'
import { CONSTANT } from '@/constant'
import { _ } from 'lodash';
import { ElMessage } from 'element-plus'
import textTips from '@/views/common/textTips.vue'
import testSendButton from './testSendButton.vue'
export default defineComponent({
components: {
textTips,
testSendButton,
},
props: {
componentName: String
},
@@ -9,7 +9,7 @@
<el-form label-width="100px" v-for="item in waysLabelData">
<!-- <el-tab-pane :label="item.label" :name="item.type"> -->
<el-form-item :label="one.subLabel" v-for="one in item.inputs">
<el-input v-model="one.value" />
<el-input size="small" v-model="one.value" :placeholder="one.desc" />
</el-form-item>
<!-- </el-tab-pane> -->
</el-form>
@@ -18,26 +18,14 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="testMseeageDialogVisible = true" size="small">测试发信</el-button>
<el-button @click="handleCancer()" size="small">取消</el-button>
<testSendButton @customhandleSubmit="handleTest()" />
<el-button type="primary" size="small" @click="handleSubmit()">
确定编辑
</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="testMseeageDialogVisible" width="50%" align-center>
<span>将发送一条测试信息将注意查收</span>
<template #footer>
<span class="dialog-footer">
<el-button size="small" @click="testMseeageDialogVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="(testMseeageDialogVisible = false) || handleTest()">
确定发送
</el-button>
</span>
</template>
</el-dialog>
</template>
<script>
@@ -47,9 +35,13 @@ import { request } from '@/api/api'
import { CONSTANT } from '@/constant'
import { _ } from 'lodash';
import { ElMessage } from 'element-plus'
import testSendButton from './testSendButton.vue'
export default defineComponent({
components: {
testSendButton,
},
props: {
componentName: String
},
@@ -57,7 +49,6 @@ export default defineComponent({
const pageState = usePageState();
const state = reactive({
isShow: false,
testMseeageDialogVisible: false,
waysLabelData: [],
editData: {},
@@ -0,0 +1,22 @@
<template>
<el-popconfirm width="400px" confirm-button-text="确定" cancel-button-text="算了" icon-color="#626AEF"
title="将往添加的渠道发送一条测试消息,请注意查收!" @confirm="handleSubmit()">
<template #reference>
<el-button size="small" type="primary">点击测试</el-button>
</template>
</el-popconfirm>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
methods: {
handleSubmit() {
this.$emit('customhandleSubmit');
}
}
});
</script>
@@ -4,7 +4,7 @@
<div class="buttom">
<div class="tips">
<el-text size="small">版本功能说明</el-text>
<el-tooltip placement="top">
<el-tooltip v-if="desc" placement="top">
<template #content>
<div v-html="desc"></div>
</template>