feat: add message template

This commit is contained in:
engigu
2025-12-06 00:28:18 +08:00
parent ff194daef2
commit a511512793
21 changed files with 2608 additions and 155 deletions
+336
View File
@@ -0,0 +1,336 @@
package v1
import (
"fmt"
"github.com/gin-gonic/gin"
"message-nest/models"
"message-nest/pkg/app"
"message-nest/pkg/e"
"message-nest/pkg/util"
"message-nest/service/message_template_service"
"message-nest/service/send_ins_service"
"net/http"
)
// GetMessageTemplateList 获取消息模板列表
func GetMessageTemplateList(c *gin.Context) {
appG := app.Gin{C: c}
text := c.Query("text")
status := c.Query("status")
offset, limit := util.GetPageSize(c)
templateService := message_template_service.MessageTemplateService{
Text: text,
Status: status,
PageNum: offset,
PageSize: limit,
}
templates, err := templateService.GetAll()
if err != nil {
appG.CResponse(http.StatusInternalServerError, "获取消息模板失败!", nil)
return
}
count, err := templateService.Count()
if err != nil {
appG.CResponse(http.StatusInternalServerError, "获取消息模板总数失败!", nil)
return
}
appG.CResponse(http.StatusOK, "获取消息模板成功", map[string]interface{}{
"lists": templates,
"total": count,
})
}
// GetMessageTemplate 获取单个消息模板
func GetMessageTemplate(c *gin.Context) {
appG := app.Gin{C: c}
id := c.Query("id")
templateService := message_template_service.MessageTemplateService{
ID: id,
}
exists, err := templateService.ExistByID()
if err != nil {
appG.CResponse(http.StatusInternalServerError, "查询模板失败!", nil)
return
}
if !exists {
appG.CResponse(http.StatusNotFound, "模板不存在!", nil)
return
}
template, err := templateService.Get()
if err != nil {
appG.CResponse(http.StatusInternalServerError, "获取模板详情失败!", nil)
return
}
appG.CResponse(http.StatusOK, "获取模板详情成功", template)
}
// AddMessageTemplate 添加消息模板
func AddMessageTemplate(c *gin.Context) {
appG := app.Gin{C: c}
var req struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
TextTemplate string `json:"text_template"`
HTMLTemplate string `json:"html_template"`
MarkdownTemplate string `json:"markdown_template"`
Placeholders string `json:"placeholders"`
AtMobiles string `json:"at_mobiles"`
AtUserIds string `json:"at_user_ids"`
IsAtAll bool `json:"is_at_all"`
Status string `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
appG.CResponse(http.StatusBadRequest, "参数错误:"+err.Error(), nil)
return
}
if req.TextTemplate == "" && req.HTMLTemplate == "" && req.MarkdownTemplate == "" {
appG.CResponse(http.StatusBadRequest, "至少需要填写一种格式的模板内容", nil)
return
}
if req.Status == "" {
req.Status = "enabled"
}
templateService := message_template_service.MessageTemplateService{
Name: req.Name,
Description: req.Description,
TextTemplate: req.TextTemplate,
HTMLTemplate: req.HTMLTemplate,
MarkdownTemplate: req.MarkdownTemplate,
Placeholders: req.Placeholders,
AtMobiles: req.AtMobiles,
AtUserIds: req.AtUserIds,
IsAtAll: req.IsAtAll,
Status: req.Status,
}
if err := templateService.Add(); err != nil {
appG.CResponse(http.StatusInternalServerError, "添加模板失败:"+err.Error(), nil)
return
}
appG.CResponse(http.StatusOK, "添加模板成功", nil)
}
// EditMessageTemplate 编辑消息模板
func EditMessageTemplate(c *gin.Context) {
appG := app.Gin{C: c}
var req struct {
ID string `json:"id" binding:"required"`
Name string `json:"name" binding:"required"`
Description string `json:"description"`
TextTemplate string `json:"text_template"`
HTMLTemplate string `json:"html_template"`
MarkdownTemplate string `json:"markdown_template"`
Placeholders string `json:"placeholders"`
AtMobiles string `json:"at_mobiles"`
AtUserIds string `json:"at_user_ids"`
IsAtAll bool `json:"is_at_all"`
Status string `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
appG.CResponse(http.StatusBadRequest, "参数错误:"+err.Error(), nil)
return
}
if req.TextTemplate == "" && req.HTMLTemplate == "" && req.MarkdownTemplate == "" {
appG.CResponse(http.StatusBadRequest, "至少需要填写一种格式的模板内容", nil)
return
}
templateService := message_template_service.MessageTemplateService{
ID: req.ID,
}
exists, err := templateService.ExistByID()
if err != nil {
appG.CResponse(http.StatusInternalServerError, "查询模板失败!", nil)
return
}
if !exists {
appG.CResponse(http.StatusNotFound, "模板不存在!", nil)
return
}
templateService.Name = req.Name
templateService.Description = req.Description
templateService.TextTemplate = req.TextTemplate
templateService.HTMLTemplate = req.HTMLTemplate
templateService.MarkdownTemplate = req.MarkdownTemplate
templateService.Placeholders = req.Placeholders
templateService.AtMobiles = req.AtMobiles
templateService.AtUserIds = req.AtUserIds
templateService.IsAtAll = req.IsAtAll
templateService.Status = req.Status
if err := templateService.Update(); err != nil {
appG.CResponse(http.StatusInternalServerError, "更新模板失败:"+err.Error(), nil)
return
}
appG.CResponse(http.StatusOK, "更新模板成功", nil)
}
// DeleteMessageTemplate 删除消息模板
func DeleteMessageTemplate(c *gin.Context) {
appG := app.Gin{C: c}
var req struct {
ID string `json:"id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
appG.CResponse(http.StatusBadRequest, "参数错误:"+err.Error(), nil)
return
}
templateService := message_template_service.MessageTemplateService{
ID: req.ID,
}
exists, err := templateService.ExistByID()
if err != nil {
appG.CResponse(http.StatusInternalServerError, "查询模板失败!", nil)
return
}
if !exists {
appG.CResponse(http.StatusNotFound, "模板不存在!", nil)
return
}
if err := templateService.Delete(); err != nil {
appG.CResponse(http.StatusInternalServerError, "删除模板失败:"+err.Error(), nil)
return
}
appG.CResponse(http.StatusOK, "删除模板成功", nil)
}
// PreviewMessageTemplate 预览消息模板
func PreviewMessageTemplate(c *gin.Context) {
appG := app.Gin{C: c}
var req struct {
ID string `json:"id" binding:"required"`
Params map[string]string `json:"params"`
}
if err := c.ShouldBindJSON(&req); err != nil {
appG.CResponse(http.StatusBadRequest, "参数错误:"+err.Error(), nil)
return
}
templateService := message_template_service.MessageTemplateService{
ID: req.ID,
}
exists, err := templateService.ExistByID()
if err != nil {
appG.CResponse(http.StatusInternalServerError, "查询模板失败!", nil)
return
}
if !exists {
appG.CResponse(http.StatusNotFound, "模板不存在!", nil)
return
}
preview, err := templateService.PreviewTemplate(req.Params)
if err != nil {
appG.CResponse(http.StatusInternalServerError, "预览模板失败:"+err.Error(), nil)
return
}
appG.CResponse(http.StatusOK, "预览模板成功", preview)
}
// GetTemplateWithIns 获取模板及其关联的实例
func GetTemplateWithIns(c *gin.Context) {
appG := app.Gin{C: c}
id := c.Query("id")
if id == "" {
appG.CResponse(http.StatusBadRequest, "模板ID为空!", nil)
return
}
// 获取模板信息
template, err := models.GetMessageTemplateByID(id)
if err != nil {
appG.CResponse(http.StatusBadRequest, "获取模板信息失败!", nil)
return
}
// 获取关联的实例列表
insList, err := models.GetTemplateInsList(id)
if err != nil {
appG.CResponse(http.StatusBadRequest, "获取实例列表失败!", nil)
return
}
result := map[string]interface{}{
"template": template,
"ins_list": insList,
}
appG.CResponse(http.StatusOK, "获取模板信息成功", result)
}
// TemplateInsReq 模板实例请求结构
type TemplateInsReq struct {
ID string `json:"id" validate:"required,len=12" label:"实例id"`
TemplateID string `json:"template_id" validate:"required" label:"模板id"`
WayID string `json:"way_id" validate:"required,len=12" label:"渠道id"`
ContentType string `json:"content_type" validate:"required,max=100" label:"实例内容类型"`
Config string `json:"config" validate:"" label:"任务配置"`
Extra string `json:"extra" validate:"" label:"任务额外信息"`
WayType string `json:"way_type" validate:"required,max=100" label:"渠道类型"`
}
// AddTemplateIns 添加模板关联的实例
func AddTemplateIns(c *gin.Context) {
var (
appG = app.Gin{C: c}
req TemplateInsReq
)
errCode, errStr := app.BindJsonAndPlayValid(c, &req)
if errCode != e.SUCCESS {
appG.CResponse(errCode, errStr, nil)
return
}
sendTaskInsService := send_ins_service.SendTaskInsService{}
err := sendTaskInsService.AddOne(models.SendTasksIns{
UUIDModel: models.UUIDModel{ID: req.ID},
TemplateID: req.TemplateID,
WayID: req.WayID,
WayType: req.WayType,
ContentType: req.ContentType,
Config: req.Config,
Extra: req.Extra,
})
if err != "" {
appG.CResponse(http.StatusBadRequest, fmt.Sprintf("添加实例失败!错误原因:%s", err), nil)
return
}
appG.CResponse(http.StatusOK, "添加实例成功!", nil)
}
+1
View File
@@ -61,6 +61,7 @@ func DoSendMassage(c *gin.Context) {
}
msgService := send_message_service.SendMessageService{
SendMode: send_message_service.SendModeTask, // 明确标记为任务模式
TaskID: taskID,
Title: req.Title,
Text: req.Text,
+146
View File
@@ -0,0 +1,146 @@
package v2
import (
"fmt"
"message-nest/models"
"message-nest/pkg/app"
"message-nest/pkg/e"
utilpkg "message-nest/pkg/util"
"message-nest/service/send_message_service"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
type SendMessageByTemplateReq struct {
Token string `json:"token" validate:"required" label:"模板token"`
Title string `json:"title" validate:"required" label:"消息标题"`
Placeholders map[string]interface{} `json:"placeholders" label:"占位符"`
}
// DoSendMessageByTemplate 使用模板发送消息
func DoSendMessageByTemplate(c *gin.Context) {
var (
appG = app.Gin{C: c}
req SendMessageByTemplateReq
)
errCode, errMsg := app.BindJsonAndPlayValid(c, &req)
if errCode != e.SUCCESS {
appG.CResponse(errCode, errMsg, nil)
return
}
// 解析 token 为模板 ID
templateID, err := utilpkg.DecryptTokenHex(req.Token, 71) // 71 为简单对称密钥
if err != nil {
appG.CResponse(http.StatusBadRequest, fmt.Sprintf("token解析失败:%v", err), nil)
return
}
// 获取模板
template, err := models.GetMessageTemplateByID(templateID)
if err != nil {
appG.CResponse(http.StatusBadRequest, fmt.Sprintf("模板不存在:%s", err), nil)
return
}
// 检查模板状态
if template.Status != "enabled" {
appG.CResponse(http.StatusBadRequest, "模板已禁用", nil)
return
}
// 替换占位符
textContent := replacePlaceholders(template.TextTemplate, req.Placeholders)
htmlContent := replacePlaceholders(template.HTMLTemplate, req.Placeholders)
markdownContent := replacePlaceholders(template.MarkdownTemplate, req.Placeholders)
// 解析@提醒配置
var atMobiles []string
var atUserIds []string
if template.AtMobiles != "" {
atMobiles = strings.Split(template.AtMobiles, ",")
// 去除空格
for i := range atMobiles {
atMobiles[i] = strings.TrimSpace(atMobiles[i])
}
}
if template.AtUserIds != "" {
atUserIds = strings.Split(template.AtUserIds, ",")
// 去除空格
for i := range atUserIds {
atUserIds[i] = strings.TrimSpace(atUserIds[i])
}
}
// 获取模板关联的实例列表
insList, err := models.GetTemplateInsList(templateID)
if err != nil || len(insList) == 0 {
appG.CResponse(http.StatusBadRequest, "模板没有配置发送实例", nil)
return
}
// 过滤启用的实例
var enabledCount int
for _, ins := range insList {
if ins.Enable == 1 {
enabledCount++
}
}
if enabledCount == 0 {
appG.CResponse(http.StatusBadRequest, "模板没有启用的发送实例", nil)
return
}
// 使用发送服务进行发送
// 将模板ID作为TaskID传入,用于日志记录
msgService := send_message_service.SendMessageService{
SendMode: send_message_service.SendModeTemplate, // 明确标记为模板模式
TaskID: templateID, // 使用模板ID作为TaskID(用于日志记录)
TemplateID: templateID, // 模板ID
Name: template.Name, // 模板名称
Title: req.Title,
Text: textContent,
HTML: htmlContent,
MarkDown: markdownContent,
CallerIp: c.ClientIP(),
AtMobiles: atMobiles,
AtUserIds: atUserIds,
AtAll: template.IsAtAll,
DefaultLogger: logrus.WithFields(logrus.Fields{
"prefix": "[Template Send]",
}),
}
// 发送前检查
task, err := msgService.SendPreCheck()
if err != nil {
appG.CResponse(http.StatusBadRequest, fmt.Sprintf("发送检查不通过:%s", err), nil)
return
}
// 异步发送
msgService.AsyncSend(task)
appG.CResponse(http.StatusOK, "success", map[string]interface{}{
"token": req.Token,
"count": enabledCount,
})
}
// replacePlaceholders 替换模板中的占位符
func replacePlaceholders(template string, placeholders map[string]interface{}) string {
if template == "" || placeholders == nil {
return template
}
result := template
for key, value := range placeholders {
placeholder := fmt.Sprintf("{{%s}}", key)
result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value))
}
return result
}
+21
View File
@@ -8,6 +8,7 @@ import (
"message-nest/pkg/setting"
"message-nest/routers/api"
"message-nest/routers/api/v1"
"message-nest/routers/api/v2"
"net/http"
)
@@ -98,6 +99,26 @@ func InitRouter(f embed.FS) *gin.Engine {
// hostedMessage
apiV1.GET("/hostedmessages/list", v1.GetHostMessageList)
// messageTemplate
apiV1.GET("/templates/list", v1.GetMessageTemplateList)
apiV1.GET("/templates/get", v1.GetMessageTemplate)
apiV1.POST("/templates/add", v1.AddMessageTemplate)
apiV1.POST("/templates/edit", v1.EditMessageTemplate)
apiV1.POST("/templates/delete", v1.DeleteMessageTemplate)
apiV1.POST("/templates/preview", v1.PreviewMessageTemplate)
// messageTemplate instances
apiV1.GET("/templates/ins/get", v1.GetTemplateWithIns)
apiV1.POST("/templates/ins/addone", v1.AddTemplateIns)
}
// API v2
apiV2 := app.Group("/api/v2")
apiV2.Use(middleware.JWT())
{
// message/send - 使用模板发送消息
apiV2.POST("/message/send", v2.DoSendMessageByTemplate)
}
return app