余额告警独立页面与多渠道通知:钉钉/飞书/企业微信/Bark,支持通知频率策略

- 告警设置拆分为独立页面,支持钉钉/飞书/企业微信/Bark/通用Webhook 多渠道
- 通知策略:低于阈值后按间隔重复提醒,达到最大次数停止,余额恢复自动重置
- 旧 webhook 配置自动迁移为通用渠道,渠道支持测试发送
- 优化告警话术:去商户ID展示、数字千分位格式化
This commit is contained in:
yml2213
2026-08-04 13:56:26 +08:00
parent 941e7ad79a
commit ca58c2bcaa
13 changed files with 1154 additions and 156 deletions
+108 -6
View File
@@ -4,6 +4,7 @@ import (
"strconv"
"affiliate_dash/internal/middleware"
"affiliate_dash/internal/model"
"affiliate_dash/internal/pkg/response"
"affiliate_dash/internal/service"
@@ -113,9 +114,10 @@ func (h *RechargeHandler) GetAlertConfig(c *gin.Context) {
}
type alertConfigReq struct {
Enabled bool `json:"enabled"`
ThresholdPoints int64 `json:"threshold_points"`
WebhookURL string `json:"webhook_url"`
Enabled bool `json:"enabled"`
ThresholdPoints int64 `json:"threshold_points"`
NotifyIntervalMinutes int `json:"notify_interval_minutes"`
MaxNotifications int `json:"max_notifications"`
}
// SaveAlertConfig PUT /api/merchant/recharge/alert-config
@@ -126,9 +128,10 @@ func (h *RechargeHandler) SaveAlertConfig(c *gin.Context) {
return
}
cfg, err := h.rechargeSvc.SaveAlertConfig(middleware.GetMerchantID(c), service.SaveAlertInput{
Enabled: req.Enabled,
ThresholdPoints: req.ThresholdPoints,
WebhookURL: req.WebhookURL,
Enabled: req.Enabled,
ThresholdPoints: req.ThresholdPoints,
NotifyIntervalMinutes: req.NotifyIntervalMinutes,
MaxNotifications: req.MaxNotifications,
})
if err != nil {
response.BadRequest(c, err.Error())
@@ -137,6 +140,105 @@ func (h *RechargeHandler) SaveAlertConfig(c *gin.Context) {
response.OK(c, cfg)
}
// ListAlertChannels GET /api/merchant/alert-channels
func (h *RechargeHandler) ListAlertChannels(c *gin.Context) {
list, err := h.rechargeSvc.ListAlertChannels(middleware.GetMerchantID(c))
if err != nil {
response.ServerError(c, err.Error())
return
}
response.OK(c, list)
}
type alertChannelReq struct {
ChannelType string `json:"channel_type" binding:"required"`
Name string `json:"name"`
WebhookURL string `json:"webhook_url"`
Server string `json:"server"`
Key string `json:"key"`
Enabled *bool `json:"enabled"`
}
func (r alertChannelReq) toInput() service.AlertChannelInput {
enabled := true
if r.Enabled != nil {
enabled = *r.Enabled
}
return service.AlertChannelInput{
ChannelType: r.ChannelType,
Name: r.Name,
Enabled: enabled,
Config: model.AlertChannelConfig{
WebhookURL: r.WebhookURL,
Server: r.Server,
Key: r.Key,
},
}
}
// CreateAlertChannel POST /api/merchant/alert-channels
func (h *RechargeHandler) CreateAlertChannel(c *gin.Context) {
var req alertChannelReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:channel_type 必填")
return
}
channel, err := h.rechargeSvc.CreateAlertChannel(middleware.GetMerchantID(c), req.toInput())
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, channel)
}
// UpdateAlertChannel PUT /api/merchant/alert-channels/:id
func (h *RechargeHandler) UpdateAlertChannel(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if id == 0 {
response.BadRequest(c, "渠道 ID 无效")
return
}
var req alertChannelReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:channel_type 必填")
return
}
channel, err := h.rechargeSvc.UpdateAlertChannel(middleware.GetMerchantID(c), uint(id), req.toInput())
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, channel)
}
// DeleteAlertChannel DELETE /api/merchant/alert-channels/:id
func (h *RechargeHandler) DeleteAlertChannel(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if id == 0 {
response.BadRequest(c, "渠道 ID 无效")
return
}
if err := h.rechargeSvc.DeleteAlertChannel(middleware.GetMerchantID(c), uint(id)); err != nil {
response.ServerError(c, err.Error())
return
}
response.OK(c, nil)
}
// TestAlertChannel POST /api/merchant/alert-channels/:id/test
func (h *RechargeHandler) TestAlertChannel(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if id == 0 {
response.BadRequest(c, "渠道 ID 无效")
return
}
if err := h.rechargeSvc.TestAlertChannel(middleware.GetMerchantID(c), uint(id)); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, nil)
}
// Upload POST /api/upload 上传凭证图片(multipart 字段名 file)。
func (h *RechargeHandler) Upload(c *gin.Context) {
fileHeader, err := c.FormFile("file")