Files
SingBox-Gopanel/internal/handler/admin_traffic_reset_api.go
CN-JS-HuiBai 25fd919477
All checks were successful
build / build (api, amd64, linux) (push) Successful in -43s
build / build (api, arm64, linux) (push) Successful in -44s
build / build (api.exe, amd64, windows) (push) Successful in -43s
API功能性修复
2026-04-17 15:13:43 +08:00

334 lines
8.4 KiB
Go

package handler
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"time"
"xboard-go/internal/database"
"xboard-go/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// AdminTrafficResetFetch returns traffic reset logs in the shape expected by the admin UI.
func AdminTrafficResetFetch(c *gin.Context) {
page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
perPage := parsePositiveInt(c.DefaultQuery("per_page", "20"), 20)
if perPage > 1000 {
perPage = 1000
}
query := database.DB.Model(&model.TrafficResetLog{}).
Preload("User").
Order("created_at DESC, id DESC")
if userEmail := strings.TrimSpace(c.Query("user_email")); userEmail != "" {
query = query.Joins("JOIN v2_user ON v2_user.id = v2_traffic_reset_logs.user_id").
Where("v2_user.email LIKE ?", "%"+userEmail+"%")
}
if resetType := strings.TrimSpace(c.Query("reset_type")); resetType != "" {
query = query.Where("v2_traffic_reset_logs.reset_type = ?", resetType)
}
if triggerSource := strings.TrimSpace(c.Query("trigger_source")); triggerSource != "" {
query = query.Where("v2_traffic_reset_logs.trigger_source = ?", triggerSource)
}
if keyword := strings.TrimSpace(c.Query("keyword")); keyword != "" {
query = query.Joins("LEFT JOIN v2_user AS keyword_user ON keyword_user.id = v2_traffic_reset_logs.user_id").
Where("keyword_user.email LIKE ? OR CAST(v2_traffic_reset_logs.user_id AS CHAR) = ?", "%"+keyword+"%", keyword)
}
if startDate := strings.TrimSpace(c.Query("start_date")); startDate != "" {
if ts, ok := parseDateStart(startDate); ok {
query = query.Where("v2_traffic_reset_logs.created_at >= ?", ts)
}
}
if endDate := strings.TrimSpace(c.Query("end_date")); endDate != "" {
if ts, ok := parseDateEnd(endDate); ok {
query = query.Where("v2_traffic_reset_logs.created_at <= ?", ts)
}
}
var total int64
if err := query.Count(&total).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to count traffic reset logs")
return
}
var logs []model.TrafficResetLog
if err := query.Offset((page - 1) * perPage).Limit(perPage).Find(&logs).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch traffic reset logs")
return
}
items := make([]gin.H, 0, len(logs))
for _, log := range logs {
items = append(items, serializeTrafficResetLog(log))
}
c.JSON(http.StatusOK, gin.H{
"data": items,
"total": total,
})
}
// AdminTrafficResetUser manually resets a user's traffic and records the action in logs.
func AdminTrafficResetUser(c *gin.Context) {
var payload struct {
UserID int `json:"user_id"`
Reason string `json:"reason"`
}
if err := c.ShouldBindJSON(&payload); err != nil || payload.UserID <= 0 {
Fail(c, http.StatusBadRequest, "user_id is required")
return
}
now := time.Now().Unix()
var user model.User
err := database.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("id = ?", payload.UserID).First(&user).Error; err != nil {
return err
}
oldUpload := int64(user.U)
oldDownload := int64(user.D)
oldTotal := oldUpload + oldDownload
updates := map[string]any{
"u": 0,
"d": 0,
"last_reset_at": now,
"reset_count": gorm.Expr("reset_count + 1"),
"updated_at": now,
}
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).Updates(updates).Error; err != nil {
return err
}
metadata, err := marshalTrafficResetMetadata(strings.TrimSpace(payload.Reason))
if err != nil {
return err
}
entry := model.TrafficResetLog{
UserID: user.ID,
ResetType: "manual",
ResetTime: now,
OldUpload: oldUpload,
OldDownload: oldDownload,
OldTotal: oldTotal,
NewUpload: 0,
NewDownload: 0,
NewTotal: 0,
TriggerSource: "manual",
Metadata: metadata,
CreatedAt: now,
UpdatedAt: now,
}
return tx.Create(&entry).Error
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
Fail(c, http.StatusBadRequest, "user does not exist")
return
}
Fail(c, http.StatusInternalServerError, "failed to reset traffic")
return
}
Success(c, true)
}
// AdminTrafficResetUserHistory returns summary and recent reset history for one user.
func AdminTrafficResetUserHistory(c *gin.Context) {
userID := parsePositiveInt(c.Param("id"), 0)
if userID <= 0 {
Fail(c, http.StatusBadRequest, "user id is required")
return
}
limit := parsePositiveInt(c.DefaultQuery("limit", "10"), 10)
if limit > 100 {
limit = 100
}
var user model.User
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
Fail(c, http.StatusBadRequest, "user does not exist")
return
}
Fail(c, http.StatusInternalServerError, "failed to load user")
return
}
var logs []model.TrafficResetLog
if err := database.DB.Where("user_id = ?", userID).
Order("created_at DESC, id DESC").
Limit(limit).
Find(&logs).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch traffic reset history")
return
}
history := make([]gin.H, 0, len(logs))
for _, log := range logs {
history = append(history, serializeTrafficResetLog(log))
}
Success(c, gin.H{
"user": gin.H{
"id": user.ID,
"email": user.Email,
"reset_count": user.ResetCount,
"last_reset_at": int64Value(user.LastResetAt),
"next_reset_at": int64Value(user.NextResetAt),
},
"history": history,
})
}
func serializeTrafficResetLog(log model.TrafficResetLog) gin.H {
email := ""
if log.User != nil {
email = log.User.Email
}
return gin.H{
"id": log.ID,
"user_id": log.UserID,
"user_email": email,
"reset_type": log.ResetType,
"reset_type_name": trafficResetTypeName(log.ResetType),
"reset_time": log.ResetTime,
"trigger_source": log.TriggerSource,
"trigger_source_name": trafficResetTriggerSourceName(log.TriggerSource),
"old_traffic": gin.H{
"upload": log.OldUpload,
"download": log.OldDownload,
"total": log.OldTotal,
"formatted": formatTrafficValue(log.OldTotal),
},
"new_traffic": gin.H{
"upload": log.NewUpload,
"download": log.NewDownload,
"total": log.NewTotal,
"formatted": formatTrafficValue(log.NewTotal),
},
"metadata": decodeTrafficResetMetadata(log.Metadata),
"created_at": log.CreatedAt,
"updated_at": log.UpdatedAt,
}
}
func trafficResetTypeName(resetType string) string {
switch resetType {
case "manual":
return "Manual"
case "monthly":
return "Monthly"
case "yearly":
return "Yearly"
case "first_day_month":
return "First Day of Month"
case "first_day_year":
return "First Day of Year"
default:
if resetType == "" {
return "-"
}
return strings.ReplaceAll(resetType, "_", " ")
}
}
func trafficResetTriggerSourceName(source string) string {
switch source {
case "manual":
return "Manual"
case "cron":
return "Cron"
case "auto":
return "Auto"
case "order":
return "Order"
case "gift_card":
return "Gift Card"
default:
if source == "" {
return "-"
}
return strings.ReplaceAll(source, "_", " ")
}
}
func formatTrafficValue(bytes int64) string {
if bytes <= 0 {
return "0 B"
}
units := []string{"B", "KB", "MB", "GB", "TB", "PB"}
size := float64(bytes)
unitIndex := 0
for size >= 1024 && unitIndex < len(units)-1 {
size /= 1024
unitIndex++
}
if size >= 100 {
return strconv.FormatFloat(size, 'f', 0, 64) + " " + units[unitIndex]
}
formatted := strconv.FormatFloat(size, 'f', 2, 64)
formatted = strings.TrimRight(strings.TrimRight(formatted, "0"), ".")
return formatted + " " + units[unitIndex]
}
func parseDateStart(raw string) (int64, bool) {
date, err := time.ParseInLocation("2006-01-02", strings.TrimSpace(raw), time.Local)
if err != nil {
return 0, false
}
return date.Unix(), true
}
func parseDateEnd(raw string) (int64, bool) {
date, err := time.ParseInLocation("2006-01-02", strings.TrimSpace(raw), time.Local)
if err != nil {
return 0, false
}
return date.Add(24*time.Hour - time.Second).Unix(), true
}
func marshalTrafficResetMetadata(reason string) (string, error) {
if reason == "" {
return "", nil
}
payload, err := json.Marshal(map[string]string{
"reason": reason,
})
if err != nil {
return "", err
}
return string(payload), nil
}
func decodeTrafficResetMetadata(raw string) any {
if strings.TrimSpace(raw) == "" {
return nil
}
var value any
if err := json.Unmarshal([]byte(raw), &value); err != nil {
return raw
}
return value
}