- 告警设置拆分为独立页面,支持钉钉/飞书/企业微信/Bark/通用Webhook 多渠道 - 通知策略:低于阈值后按间隔重复提醒,达到最大次数停止,余额恢复自动重置 - 旧 webhook 配置自动迁移为通用渠道,渠道支持测试发送 - 优化告警话术:去商户ID展示、数字千分位格式化
263 lines
7.6 KiB
Go
263 lines
7.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"affiliate_dash/internal/middleware"
|
|
"affiliate_dash/internal/model"
|
|
"affiliate_dash/internal/pkg/response"
|
|
"affiliate_dash/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const uploadMaxBytes = 5 * 1024 * 1024 // 单张图片最大 5MB
|
|
|
|
// RechargeHandler 提供积分充值申请(商户侧)与审核入账(管理员侧)能力。
|
|
type RechargeHandler struct {
|
|
rechargeSvc *service.RechargeService
|
|
}
|
|
|
|
func NewRechargeHandler(rechargeSvc *service.RechargeService) *RechargeHandler {
|
|
return &RechargeHandler{rechargeSvc: rechargeSvc}
|
|
}
|
|
|
|
type rechargeCreateReq struct {
|
|
AmountCNY int64 `json:"amount_cny" binding:"required"` // 人民币,单位:分
|
|
Vouchers []string `json:"vouchers" binding:"required"`
|
|
Note string `json:"note"`
|
|
}
|
|
|
|
// CreateApplication POST /api/merchant/recharge/applications
|
|
func (h *RechargeHandler) CreateApplication(c *gin.Context) {
|
|
var req rechargeCreateReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "参数错误:amount_cny 与 vouchers 必填")
|
|
return
|
|
}
|
|
app, err := h.rechargeSvc.CreateRecharge(service.CreateRechargeInput{
|
|
MerchantID: middleware.GetMerchantID(c),
|
|
ActorUserID: middleware.GetUserID(c),
|
|
AmountCNY: req.AmountCNY,
|
|
Vouchers: req.Vouchers,
|
|
Note: req.Note,
|
|
})
|
|
if err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
response.OK(c, app)
|
|
}
|
|
|
|
// ListApplications GET /api/merchant/recharge/applications(当前商户)
|
|
func (h *RechargeHandler) ListApplications(c *gin.Context) {
|
|
page, size := pageParams(c)
|
|
list, total, err := h.rechargeSvc.ListRechargeApplications(middleware.GetMerchantID(c), page, size, c.Query("status"))
|
|
if err != nil {
|
|
response.ServerError(c, err.Error())
|
|
return
|
|
}
|
|
response.Page(c, list, total, page, size)
|
|
}
|
|
|
|
// AdminListApplications GET /api/platform/recharge/applications(全部商户)
|
|
func (h *RechargeHandler) AdminListApplications(c *gin.Context) {
|
|
page, size := pageParams(c)
|
|
list, total, err := h.rechargeSvc.ListAllRechargeApplications(page, size, c.Query("status"))
|
|
if err != nil {
|
|
response.ServerError(c, err.Error())
|
|
return
|
|
}
|
|
response.Page(c, list, total, page, size)
|
|
}
|
|
|
|
type rechargeReviewReq struct {
|
|
Approved bool `json:"approved" binding:"required"`
|
|
ReviewNote string `json:"review_note"`
|
|
}
|
|
|
|
// AdminReviewApplication POST /api/platform/recharge/applications/:id/review
|
|
// 通过后由后端按 1 元 = 100 积分自动入账到商户钱包,幂等键为申请单号。
|
|
func (h *RechargeHandler) AdminReviewApplication(c *gin.Context) {
|
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
if id == 0 {
|
|
response.BadRequest(c, "申请 ID 无效")
|
|
return
|
|
}
|
|
var req rechargeReviewReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "参数错误:approved 必填")
|
|
return
|
|
}
|
|
app, err := h.rechargeSvc.ReviewRecharge(service.ReviewRechargeInput{
|
|
ApplicationID: uint(id),
|
|
Approved: req.Approved,
|
|
ReviewNote: req.ReviewNote,
|
|
ActorUserID: middleware.GetUserID(c),
|
|
})
|
|
if err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
h.rechargeSvc.CheckLowBalanceAndNotify(app.MerchantID)
|
|
response.OK(c, app)
|
|
}
|
|
|
|
// GetAlertConfig GET /api/merchant/recharge/alert-config
|
|
func (h *RechargeHandler) GetAlertConfig(c *gin.Context) {
|
|
cfg, err := h.rechargeSvc.GetAlertConfig(middleware.GetMerchantID(c))
|
|
if err != nil {
|
|
response.ServerError(c, err.Error())
|
|
return
|
|
}
|
|
response.OK(c, cfg)
|
|
}
|
|
|
|
type alertConfigReq struct {
|
|
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
|
|
func (h *RechargeHandler) SaveAlertConfig(c *gin.Context) {
|
|
var req alertConfigReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "参数错误")
|
|
return
|
|
}
|
|
cfg, err := h.rechargeSvc.SaveAlertConfig(middleware.GetMerchantID(c), service.SaveAlertInput{
|
|
Enabled: req.Enabled,
|
|
ThresholdPoints: req.ThresholdPoints,
|
|
NotifyIntervalMinutes: req.NotifyIntervalMinutes,
|
|
MaxNotifications: req.MaxNotifications,
|
|
})
|
|
if err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
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")
|
|
if err != nil {
|
|
response.BadRequest(c, "请选择要上传的图片")
|
|
return
|
|
}
|
|
file, err := fileHeader.Open()
|
|
if err != nil {
|
|
response.BadRequest(c, "读取上传文件失败")
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
url, err := h.rechargeSvc.SaveUploadFile(file, fileHeader.Filename, uploadMaxBytes)
|
|
if err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
response.OK(c, gin.H{"url": url})
|
|
}
|