334 lines
8.4 KiB
Go
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
|
|
}
|