Files
2026-04-27 17:22:56 +08:00

281 lines
6.8 KiB
Go

package service
import (
"fmt"
"time"
"verification-platform-backend/internal/database"
"verification-platform-backend/internal/model"
)
type UsageAlertService struct{}
func NewUsageAlertService() *UsageAlertService {
return &UsageAlertService{}
}
type AlertType string
const (
AlertTypeAPICalls AlertType = "api_calls"
AlertTypeStorage AlertType = "storage"
AlertTypePackageExp AlertType = "package_expiry"
)
type AlertLevel string
const (
AlertLevelWarning AlertLevel = "warning"
AlertLevelCritical AlertLevel = "critical"
)
type UsageAlert struct {
UserID uint `json:"user_id"`
Type AlertType `json:"type"`
Level AlertLevel `json:"level"`
Message string `json:"message"`
Usage float64 `json:"usage"`
Limit float64 `json:"limit"`
Percent float64 `json:"percent"`
CreatedAt time.Time `json:"created_at"`
}
func (s *UsageAlertService) CheckAndCreateAlerts(userID uint) ([]UsageAlert, error) {
var alerts []UsageAlert
var user model.User
if err := database.DB.Preload("CurrentPackage").First(&user, userID).Error; err != nil {
return nil, err
}
if user.CurrentPackageID == nil {
return alerts, nil
}
var permission model.PackagePermission
if err := database.DB.Where("package_id = ?", user.CurrentPackageID).First(&permission).Error; err != nil {
return nil, err
}
if alert := s.checkAPICallsUsage(&user, &permission); alert != nil {
alerts = append(alerts, *alert)
}
if alert := s.checkStorageUsage(&user, &permission); alert != nil {
alerts = append(alerts, *alert)
}
if alert := s.checkPackageExpiry(&user); alert != nil {
alerts = append(alerts, *alert)
}
for _, alert := range alerts {
s.sendNotification(&alert)
}
return alerts, nil
}
func (s *UsageAlertService) checkAPICallsUsage(user *model.User, permission *model.PackagePermission) *UsageAlert {
if permission.MaxApiCalls == 0 {
return nil
}
usagePercent := float64(user.ApiCallsUsed) / float64(permission.MaxApiCalls) * 100
var lastAlert model.UsageAlertRecord
hasRecentAlert := database.DB.Where(
"user_id = ? AND type = ? AND created_at > ?",
user.ID,
AlertTypeAPICalls,
time.Now().Add(-24*time.Hour),
).First(&lastAlert).Error == nil
if hasRecentAlert {
return nil
}
if usagePercent >= 90 {
return &UsageAlert{
UserID: user.ID,
Type: AlertTypeAPICalls,
Level: AlertLevelCritical,
Message: fmt.Sprintf("API调用次数已使用 %.1f%%,即将达到上限", usagePercent),
Usage: float64(user.ApiCallsUsed),
Limit: float64(permission.MaxApiCalls),
Percent: usagePercent,
}
}
if usagePercent >= 80 {
return &UsageAlert{
UserID: user.ID,
Type: AlertTypeAPICalls,
Level: AlertLevelWarning,
Message: fmt.Sprintf("API调用次数已使用 %.1f%%,请注意用量", usagePercent),
Usage: float64(user.ApiCallsUsed),
Limit: float64(permission.MaxApiCalls),
Percent: usagePercent,
}
}
return nil
}
func (s *UsageAlertService) checkStorageUsage(user *model.User, permission *model.PackagePermission) *UsageAlert {
if permission.MaxStorage == 0 {
return nil
}
usagePercent := float64(user.StorageUsed) / float64(int64(permission.MaxStorage)*1024*1024) * 100
var lastAlert model.UsageAlertRecord
hasRecentAlert := database.DB.Where(
"user_id = ? AND type = ? AND created_at > ?",
user.ID,
AlertTypeStorage,
time.Now().Add(-24*time.Hour),
).First(&lastAlert).Error == nil
if hasRecentAlert {
return nil
}
if usagePercent >= 90 {
return &UsageAlert{
UserID: user.ID,
Type: AlertTypeStorage,
Level: AlertLevelCritical,
Message: fmt.Sprintf("存储空间已使用 %.1f%%,即将达到上限", usagePercent),
Usage: float64(user.StorageUsed) / 1024 / 1024,
Limit: float64(permission.MaxStorage),
Percent: usagePercent,
}
}
if usagePercent >= 80 {
return &UsageAlert{
UserID: user.ID,
Type: AlertTypeStorage,
Level: AlertLevelWarning,
Message: fmt.Sprintf("存储空间已使用 %.1f%%,请注意用量", usagePercent),
Usage: float64(user.StorageUsed) / 1024 / 1024,
Limit: float64(permission.MaxStorage),
Percent: usagePercent,
}
}
return nil
}
func (s *UsageAlertService) checkPackageExpiry(user *model.User) *UsageAlert {
var userPackage model.UserPackage
if err := database.DB.Where(
"user_id = ? AND package_id = ? AND status = ?",
user.ID,
user.CurrentPackageID,
"active",
).First(&userPackage).Error; err != nil {
return nil
}
if userPackage.ExpiredAt == nil {
return nil
}
daysUntilExpiry := time.Until(*userPackage.ExpiredAt).Hours() / 24
var lastAlert model.UsageAlertRecord
hasRecentAlert := database.DB.Where(
"user_id = ? AND type = ? AND created_at > ?",
user.ID,
AlertTypePackageExp,
time.Now().Add(-24*time.Hour),
).First(&lastAlert).Error == nil
if hasRecentAlert {
return nil
}
if daysUntilExpiry <= 3 {
return &UsageAlert{
UserID: user.ID,
Type: AlertTypePackageExp,
Level: AlertLevelCritical,
Message: fmt.Sprintf("套餐将在 %.0f 天后过期,请及时续费", daysUntilExpiry),
Usage: daysUntilExpiry,
Limit: 0,
Percent: 0,
}
}
if daysUntilExpiry <= 7 {
return &UsageAlert{
UserID: user.ID,
Type: AlertTypePackageExp,
Level: AlertLevelWarning,
Message: fmt.Sprintf("套餐将在 %.0f 天后过期,请及时续费", daysUntilExpiry),
Usage: daysUntilExpiry,
Limit: 0,
Percent: 0,
}
}
return nil
}
func (s *UsageAlertService) sendNotification(alert *UsageAlert) {
alertRecord := model.UsageAlertRecord{
UserID: alert.UserID,
Type: string(alert.Type),
Level: string(alert.Level),
Message: alert.Message,
Usage: alert.Usage,
Limit: alert.Limit,
Percent: alert.Percent,
Status: "sent",
CreatedAt: time.Now(),
}
database.DB.Create(&alertRecord)
notification := model.Notification{
UserID: alert.UserID,
Title: "用量告警",
Content: alert.Message,
Type: "usage_alert",
IsRead: false,
CreatedAt: time.Now(),
}
database.DB.Create(&notification)
}
func (s *UsageAlertService) CheckAllUsers() error {
var users []model.User
if err := database.DB.Where("current_package_id IS NOT NULL").Find(&users).Error; err != nil {
return err
}
for _, user := range users {
_, err := s.CheckAndCreateAlerts(user.ID)
if err != nil {
fmt.Printf("检查用户 %d 用量告警失败: %v\n", user.ID, err)
}
}
return nil
}
func (s *UsageAlertService) GetUserAlerts(userID uint, limit int) ([]model.UsageAlertRecord, error) {
var alerts []model.UsageAlertRecord
err := database.DB.Where("user_id = ?", userID).
Order("created_at DESC").
Limit(limit).
Find(&alerts).Error
return alerts, err
}
func (s *UsageAlertService) MarkAlertAsRead(alertID uint) error {
return database.DB.Model(&model.UsageAlertRecord{}).
Where("id = ?", alertID).
Update("status", "read").Error
}