package handler import ( "bytes" "encoding/json" "io" "net/http" "strconv" "sql-platform/internal/model" "github.com/gin-gonic/gin" ) // ChannelResponse — config as JSON object for frontend type ChannelResponse struct { ID int `json:"id"` Name string `json:"name"` ChannelType string `json:"channel_type"` Config map[string]interface{} `json:"config"` IsEnabled bool `json:"is_enabled"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } func toChannelResponse(ch model.NotificationChannel) ChannelResponse { var cfg map[string]interface{} if ch.Config != "" { _ = json.Unmarshal([]byte(ch.Config), &cfg) } if cfg == nil { cfg = map[string]interface{}{} } enabled := ch.IsEnabled != nil && *ch.IsEnabled return ChannelResponse{ ID: ch.ID, Name: ch.Name, ChannelType: ch.ChannelType, Config: cfg, IsEnabled: enabled, CreatedAt: ch.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: ch.UpdatedAt.Format("2006-01-02T15:04:05Z"), } } // GET /api/v1/notifications func ListNotifications(c *gin.Context) { var channels []model.NotificationChannel model.DB.Order("id").Find(&channels) result := make([]ChannelResponse, 0, len(channels)) for _, ch := range channels { result = append(result, toChannelResponse(ch)) } c.JSON(http.StatusOK, result) } // POST /api/v1/notifications func CreateNotification(c *gin.Context) { var input struct { Name string `json:"name"` ChannelType string `json:"channel_type"` Config map[string]interface{} `json:"config"` IsEnabled bool `json:"is_enabled"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "参数错误"}) return } if input.Name == "" { c.JSON(http.StatusBadRequest, gin.H{"detail": "名称不能为空"}) return } cfgBytes, _ := json.Marshal(input.Config) enabled := input.IsEnabled ch := model.NotificationChannel{ Name: input.Name, ChannelType: input.ChannelType, Config: string(cfgBytes), IsEnabled: &enabled, } if err := model.DB.Create(&ch).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": "创建失败"}) return } c.JSON(http.StatusOK, toChannelResponse(ch)) } // PUT /api/v1/notifications/:id func UpdateNotification(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var ch model.NotificationChannel if err := model.DB.First(&ch, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "通知渠道不存在"}) return } var input map[string]interface{} if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "参数错误"}) return } if name, ok := input["name"].(string); ok { ch.Name = name } if ct, ok := input["channel_type"].(string); ok { ch.ChannelType = ct } if cfg, ok := input["config"].(map[string]interface{}); ok { cfgBytes, _ := json.Marshal(cfg) ch.Config = string(cfgBytes) } if enabled, ok := input["is_enabled"].(bool); ok { ch.IsEnabled = &enabled } model.DB.Save(&ch) c.JSON(http.StatusOK, toChannelResponse(ch)) } // DELETE /api/v1/notifications/:id func DeleteNotification(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) model.DB.Delete(&model.NotificationChannel{}, id) c.JSON(http.StatusOK, gin.H{"message": "已删除"}) } // POST /api/v1/notifications/:id/test func TestNotificationSend(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var ch model.NotificationChannel if err := model.DB.First(&ch, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "通知渠道不存在"}) return } var cfg map[string]interface{} _ = json.Unmarshal([]byte(ch.Config), &cfg) webhookURL, _ := cfg["webhook_url"].(string) if webhookURL == "" { c.JSON(http.StatusBadRequest, gin.H{"detail": "Webhook URL 未配置"}) return } title := "SQL审核平台 - 通知测试" body := "这是一条测试消息,如果收到说明通知渠道配置正确。" var sendErr error switch ch.ChannelType { case "webhook_feishu": sendErr = sendFeishuWebhook(webhookURL, title, body) case "dingtalk": secret, _ := cfg["secret"].(string) sendErr = sendDingtalkWebhook(webhookURL, secret, title, body) default: c.JSON(http.StatusBadRequest, gin.H{"detail": "不支持的通知类型: " + ch.ChannelType}) return } if sendErr != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": "发送失败: " + sendErr.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "message": "测试消息已发送"}) } func sendFeishuWebhook(webhookURL, title, body string) error { payload := map[string]interface{}{ "msg_type": "interactive", "card": map[string]interface{}{ "elements": []map[string]interface{}{ {"tag": "markdown", "content": body}, }, }, } return postJSON(webhookURL, payload) } func sendDingtalkWebhook(webhookURL, secret, title, body string) error { dingBody := title + "\n\n" + body payload := map[string]interface{}{ "msgtype": "markdown", "markdown": map[string]string{ "title": title, "text": dingBody, }, } return postJSON(webhookURL, payload) } func postJSON(url string, payload interface{}) error { data, _ := json.Marshal(payload) resp, err := http.Post(url, "application/json", bytes.NewReader(data)) if err != nil { return err } defer resp.Body.Close() io.Copy(io.Discard, resp.Body) return nil }