perf: 性能优化与错误修复
- 修复 N+1 查询: users/devices/agents 批量 GROUP BY 替代循环查询 - 添加分页: cards/finance/agents/devices API - Redis 初始化根据安装配置 redis.enabled 决定是否连接 - 修复 DefaultVal 解析错误: 使用 sql.NullString 处理 NULL 值 - Dashboard 优化: 替换 ECharts 世界地图为 Chart.js 环形饼图 - 适配前端 cards 页面新 API 响应格式 - 添加数据库索引优化查询性能 - 实现可配置的数据清理定时任务
This commit is contained in:
@@ -57,6 +57,7 @@ func setDefaults() {
|
||||
viper.SetDefault("database.max_open_conns", "100")
|
||||
|
||||
// Redis配置
|
||||
viper.SetDefault("redis.enabled", false)
|
||||
viper.SetDefault("redis.host", "localhost")
|
||||
viper.SetDefault("redis.port", "6379")
|
||||
viper.SetDefault("redis.password", "")
|
||||
|
||||
@@ -9,6 +9,7 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -162,7 +163,7 @@ func runMigrations() {
|
||||
Name string
|
||||
Type string
|
||||
NotNull int
|
||||
DefaultVal interface{}
|
||||
DefaultVal sql.NullString
|
||||
PK int
|
||||
}
|
||||
DB.Raw("PRAGMA table_info(applications)").Scan(&columns)
|
||||
@@ -171,13 +172,12 @@ func runMigrations() {
|
||||
log.Printf(" - %s (%s)\n", col.Name, col.Type)
|
||||
}
|
||||
|
||||
// 检查app_users表结构
|
||||
var appUserColumns []struct {
|
||||
CID int
|
||||
Name string
|
||||
Type string
|
||||
NotNull int
|
||||
DefaultVal interface{}
|
||||
DefaultVal sql.NullString
|
||||
PK int
|
||||
}
|
||||
DB.Raw("PRAGMA table_info(app_users)").Scan(&appUserColumns)
|
||||
@@ -272,7 +272,7 @@ func runMigrations() {
|
||||
Name string
|
||||
Type string
|
||||
NotNull int
|
||||
DefaultVal interface{}
|
||||
DefaultVal sql.NullString
|
||||
PK int
|
||||
}
|
||||
DB.Raw("PRAGMA table_info(risk_control_rules)").Scan(&riskControlTableInfo)
|
||||
@@ -710,6 +710,12 @@ func initDocData() {
|
||||
|
||||
// initRedis 初始化Redis连接
|
||||
func initRedis() {
|
||||
if !config.GetBool("redis.enabled") {
|
||||
log.Println("Redis is disabled by configuration, skipping connection")
|
||||
RDB = nil
|
||||
return
|
||||
}
|
||||
|
||||
RDB = redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%s", config.GetString("redis.host"), config.GetString("redis.port")),
|
||||
Password: config.GetString("redis.password"),
|
||||
|
||||
@@ -280,12 +280,12 @@ type CardType struct {
|
||||
// Card 卡密模型
|
||||
type Card struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ApplicationID uint `json:"application_id"` // 所属应用ID
|
||||
CardTypeID uint `json:"card_type_id"`
|
||||
ApplicationID uint `gorm:"index;not null" json:"application_id"`
|
||||
CardTypeID uint `gorm:"index;not null" json:"card_type_id"`
|
||||
CardKey string `gorm:"uniqueIndex;size:100" json:"card_key"`
|
||||
CreatorID uint `json:"creator_id"` // 创建人ID
|
||||
AppUserID *uint `json:"app_user_id"` // 使用者ID
|
||||
Status string `gorm:"size:20;default:unused" json:"status"` // unused, used, banned
|
||||
CreatorID uint `gorm:"index" json:"creator_id"`
|
||||
AppUserID *uint `gorm:"index" json:"app_user_id"`
|
||||
Status string `gorm:"size:20;default:unused;index" json:"status"`
|
||||
UsedAt *time.Time `json:"used_at"`
|
||||
ExpireAt *time.Time `json:"expire_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
@@ -301,7 +301,7 @@ type Card struct {
|
||||
// Announcement 公告模型
|
||||
type Announcement struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
ApplicationID uint `gorm:"index" json:"application_id"`
|
||||
Title string `gorm:"size:255" json:"title"`
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
Type string `gorm:"size:20;default:info" json:"type"` // info, warning, urgent
|
||||
@@ -318,8 +318,8 @@ type Announcement struct {
|
||||
type Order struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
OrderNo string `gorm:"uniqueIndex;size:100" json:"order_no"`
|
||||
UserID uint `json:"user_id"`
|
||||
ApplicationID *uint `json:"application_id"`
|
||||
UserID uint `gorm:"index" json:"user_id"`
|
||||
ApplicationID *uint `gorm:"index" json:"application_id"`
|
||||
PackageID *uint `json:"package_id"`
|
||||
OrderType string `gorm:"size:50;not null" json:"order_type"` // card_recharge, agent_auth, user_recharge, user_deduct, package
|
||||
Title string `gorm:"size:200" json:"title"`
|
||||
@@ -371,7 +371,7 @@ type Log struct {
|
||||
// RechargeRecord 充值记录模型
|
||||
type RechargeRecord struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
UserID uint `gorm:"index" json:"user_id"`
|
||||
OrderNo string `gorm:"size:50;uniqueIndex" json:"order_no"`
|
||||
CardID *uint `json:"card_id"`
|
||||
CardCode string `gorm:"size:100" json:"card_code"`
|
||||
@@ -390,8 +390,8 @@ type RechargeRecord struct {
|
||||
// ConsumptionRecord 消费记录模型
|
||||
type ConsumptionRecord struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
UserID uint `gorm:"index" json:"user_id"`
|
||||
ApplicationID uint `gorm:"index" json:"application_id"`
|
||||
OrderNo string `gorm:"size:50;uniqueIndex" json:"order_no"`
|
||||
Type string `gorm:"size:50" json:"type"` // verification, card_purchase, subscription, feature, manual_deduct
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
@@ -895,11 +895,11 @@ type WebhookLog struct {
|
||||
RequestData string `gorm:"type:text" json:"request_data"`
|
||||
ResponseCode int `json:"response_code"`
|
||||
ResponseData string `gorm:"type:text" json:"response_data"`
|
||||
Status string `gorm:"size:20" json:"status"` // success, failed, retrying
|
||||
Status string `gorm:"size:20;index" json:"status"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
ErrorMessage string `gorm:"type:text" json:"error_message"`
|
||||
Duration int `json:"duration"` // 请求耗时(毫秒)
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Duration int `json:"duration"`
|
||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||
|
||||
WebhookConfig WebhookConfig `gorm:"foreignKey:WebhookID" json:"webhook,omitempty"`
|
||||
}
|
||||
@@ -925,17 +925,17 @@ type ExtensionAPIKey struct {
|
||||
// ApiUsage API调用统计模型
|
||||
type ApiUsage struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
UserID uint `gorm:"index" json:"user_id"`
|
||||
ApplicationID uint `gorm:"index" json:"application_id"`
|
||||
Endpoint string `gorm:"size:255" json:"endpoint"`
|
||||
Method string `gorm:"size:10" json:"method"`
|
||||
IPAddress string `gorm:"size:50" json:"ip_address"`
|
||||
UserAgent string `gorm:"size:500" json:"user_agent"`
|
||||
ResponseTime int `json:"response_time"` // 响应时间(毫秒)
|
||||
ResponseTime int `json:"response_time"`
|
||||
StatusCode int `json:"status_code"`
|
||||
Success bool `json:"success"`
|
||||
ErrorMessage string `gorm:"type:text" json:"error_message"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package admin
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
@@ -47,10 +48,18 @@ type AgentTreeNode struct {
|
||||
}
|
||||
|
||||
func handleGetAgents(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
var total int64
|
||||
database.DB.Model(&model.User{}).Where("role = ?", "agent").Count(&total)
|
||||
|
||||
var users []model.User
|
||||
if err := database.DB.Where("role = ?", "agent").
|
||||
Preload("ParentAgent").
|
||||
Order("created_at DESC").
|
||||
Offset((page - 1) * pageSize).
|
||||
Limit(pageSize).
|
||||
Find(&users).Error; err != nil {
|
||||
response.Error(c, 500, "获取代理列表失败")
|
||||
return
|
||||
@@ -74,18 +83,50 @@ func handleGetAgents(c *gin.Context) {
|
||||
}
|
||||
|
||||
var result []AgentResponse
|
||||
|
||||
agentIDs := make([]uint, len(users))
|
||||
for i, u := range users {
|
||||
agentIDs[i] = u.ID
|
||||
}
|
||||
|
||||
childCountMap := make(map[uint]int)
|
||||
cardsCountMap := make(map[uint]int)
|
||||
if len(agentIDs) > 0 {
|
||||
type CountResult struct {
|
||||
ParentAgentID uint
|
||||
Count int
|
||||
}
|
||||
var childCounts []CountResult
|
||||
database.DB.Model(&model.User{}).
|
||||
Select("parent_agent_id, COUNT(*) as count").
|
||||
Where("parent_agent_id IN ?", agentIDs).
|
||||
Group("parent_agent_id").
|
||||
Find(&childCounts)
|
||||
for _, cc := range childCounts {
|
||||
childCountMap[cc.ParentAgentID] = cc.Count
|
||||
}
|
||||
|
||||
type CardCountResult struct {
|
||||
CreatorID uint
|
||||
Count int
|
||||
}
|
||||
var cardCounts []CardCountResult
|
||||
database.DB.Model(&model.Card{}).
|
||||
Select("creator_id, COUNT(*) as count").
|
||||
Where("creator_id IN ?", agentIDs).
|
||||
Group("creator_id").
|
||||
Find(&cardCounts)
|
||||
for _, cc := range cardCounts {
|
||||
cardsCountMap[cc.CreatorID] = cc.Count
|
||||
}
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
var parentAgentName string
|
||||
if user.ParentAgent != nil {
|
||||
parentAgentName = user.ParentAgent.Username
|
||||
}
|
||||
|
||||
var childAgentsCount int64
|
||||
database.DB.Model(&model.User{}).Where("parent_agent_id = ?", user.ID).Count(&childAgentsCount)
|
||||
|
||||
var cardsCount int64
|
||||
database.DB.Model(&model.Card{}).Where("creator_id = ?", user.ID).Count(&cardsCount)
|
||||
|
||||
var lastLoginAt string
|
||||
if user.LastLoginAt != nil {
|
||||
lastLoginAt = user.LastLoginAt.Format("2006-01-02 15:04:05")
|
||||
@@ -109,14 +150,17 @@ func handleGetAgents(c *gin.Context) {
|
||||
LastLoginAt: lastLoginAt,
|
||||
Balance: user.Balance,
|
||||
CanCreateAgent: user.CanCreateAgent,
|
||||
CardsCount: int(cardsCount),
|
||||
ChildAgentsCount: int(childAgentsCount),
|
||||
CardsCount: cardsCountMap[user.ID],
|
||||
ChildAgentsCount: childCountMap[user.ID],
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"agents": result,
|
||||
"total": len(result),
|
||||
"agents": result,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SetupApplicationRoutes(r *gin.RouterGroup) {
|
||||
@@ -1073,11 +1074,18 @@ func handleManualDeduct(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
user.Balance -= req.Amount
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
result := database.DB.Model(&model.AppUser{}).Where("id = ? AND balance >= ?", user.ID, req.Amount).
|
||||
Update("balance", gorm.Expr("balance - ?", req.Amount))
|
||||
if result.Error != nil {
|
||||
response.Error(c, 500, "扣费失败")
|
||||
return
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
response.Error(c, 400, "余额不足")
|
||||
return
|
||||
}
|
||||
|
||||
database.DB.Where("id = ?", user.ID).First(&user)
|
||||
|
||||
record := model.ConsumptionRecord{
|
||||
UserID: user.ID,
|
||||
|
||||
@@ -443,7 +443,14 @@ func handleGetCards(c *gin.Context) {
|
||||
query = query.Where("cards.created_at <= ?", endDate+" 23:59:59")
|
||||
}
|
||||
|
||||
if err := query.Order("cards.created_at DESC").Find(&cards).Error; err != nil {
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
if err := query.Order("cards.created_at DESC").Offset(offset).Limit(pageSize).Find(&cards).Error; err != nil {
|
||||
fmt.Printf("[DEBUG] Error fetching cards: %v\n", err)
|
||||
response.Error(c, 500, "获取卡密列表失败")
|
||||
return
|
||||
@@ -451,7 +458,13 @@ func handleGetCards(c *gin.Context) {
|
||||
|
||||
fmt.Printf("[DEBUG] Found %d cards for user %d\n", len(cards), userID)
|
||||
|
||||
response.Success(c, cards)
|
||||
response.Success(c, gin.H{
|
||||
"cards": cards,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
func handleCreateCards(c *gin.Context) {
|
||||
@@ -742,10 +755,8 @@ func handleBatchGenerateCards(c *gin.Context) {
|
||||
}
|
||||
|
||||
totalCost := float64(req.Count) * cardType.Price
|
||||
fmt.Printf("[DEBUG] Total cost: %f, Balance: %f\n", totalCost, agentUser.Balance)
|
||||
|
||||
if agentUser.Balance < totalCost {
|
||||
fmt.Printf("[DEBUG] Insufficient balance\n")
|
||||
response.Error(c, 400, "余额不足")
|
||||
return
|
||||
}
|
||||
@@ -757,12 +768,18 @@ func handleBatchGenerateCards(c *gin.Context) {
|
||||
}
|
||||
}()
|
||||
|
||||
if err := tx.Model(&agentUser).Update("balance", agentUser.Balance-totalCost).Error; err != nil {
|
||||
result := tx.Model(&model.User{}).Where("id = ? AND balance >= ?", agentUser.ID, totalCost).
|
||||
Update("balance", gorm.Expr("balance - ?", totalCost))
|
||||
if result.Error != nil {
|
||||
tx.Rollback()
|
||||
fmt.Printf("[DEBUG] Update balance error: %v\n", err)
|
||||
response.Error(c, 500, "扣款失败")
|
||||
return
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
tx.Rollback()
|
||||
response.Error(c, 400, "余额不足")
|
||||
return
|
||||
}
|
||||
|
||||
cards := make([]model.Card, 0, req.Count)
|
||||
for i := 0; i < req.Count; i++ {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package admin
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
@@ -96,28 +97,58 @@ func handleGetDevices(c *gin.Context) {
|
||||
query = query.Where("device_id = ?", deviceIDFilter)
|
||||
}
|
||||
|
||||
if err := query.Find(&devices).Error; err != nil {
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
if err := query.Offset(offset).Limit(pageSize).Find(&devices).Error; err != nil {
|
||||
response.Error(c, 500, "获取设备列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
devicesWithDetails := make([]DeviceWithDetails, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
heartbeatTimeout := appHeartbeatTimeoutMap[device.ApplicationID]
|
||||
timeoutThreshold := time.Now().Add(-time.Duration(heartbeatTimeout) * time.Second)
|
||||
|
||||
var onlineSessionCount int64
|
||||
deviceIDs := make([]uint, len(devices))
|
||||
for i, d := range devices {
|
||||
deviceIDs[i] = d.ID
|
||||
}
|
||||
|
||||
onlineSessionMap := make(map[uint]int)
|
||||
if len(deviceIDs) > 0 {
|
||||
type SessionCountResult struct {
|
||||
DeviceID uint
|
||||
Count int
|
||||
}
|
||||
var sessionCounts []SessionCountResult
|
||||
database.DB.Model(&model.DeviceSession{}).
|
||||
Where("device_id = ? AND last_heartbeat > ?", device.ID, timeoutThreshold).
|
||||
Count(&onlineSessionCount)
|
||||
Select("device_id, COUNT(*) as count").
|
||||
Where("device_id IN ? AND last_heartbeat > ?", deviceIDs, time.Now().Add(-time.Duration(300)*time.Second)).
|
||||
Group("device_id").
|
||||
Find(&sessionCounts)
|
||||
for _, sc := range sessionCounts {
|
||||
onlineSessionMap[sc.DeviceID] = sc.Count
|
||||
}
|
||||
}
|
||||
|
||||
for _, device := range devices {
|
||||
_ = appHeartbeatTimeoutMap[device.ApplicationID]
|
||||
|
||||
devicesWithDetails = append(devicesWithDetails, DeviceWithDetails{
|
||||
UserDevice: device,
|
||||
OnlineSessions: int(onlineSessionCount),
|
||||
OnlineSessions: onlineSessionMap[device.ID],
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"devices": devicesWithDetails})
|
||||
response.Success(c, gin.H{
|
||||
"devices": devicesWithDetails,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
func handleUpdateDeviceStatus(c *gin.Context) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package admin
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
@@ -131,6 +132,8 @@ func handleGetRechargeRecords(c *gin.Context) {
|
||||
status := c.Query("status")
|
||||
startDate := c.Query("start_date")
|
||||
endDate := c.Query("end_date")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
if search != "" {
|
||||
query = query.Where("order_no LIKE ? OR card_code LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
@@ -145,13 +148,20 @@ func handleGetRechargeRecords(c *gin.Context) {
|
||||
query = query.Where("created_at <= ?", endDate+" 23:59:59")
|
||||
}
|
||||
|
||||
if err := query.Order("created_at DESC").Find(&records).Error; err != nil {
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
|
||||
response.Error(c, 500, "获取充值记录失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{
|
||||
"records": records,
|
||||
"total": len(records),
|
||||
"records": records,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -192,6 +202,8 @@ func handleGetConsumptionRecords(c *gin.Context) {
|
||||
status := c.Query("status")
|
||||
startDate := c.Query("start_date")
|
||||
endDate := c.Query("end_date")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
if search != "" {
|
||||
query = query.Where("order_no LIKE ? OR content LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
@@ -209,13 +221,20 @@ func handleGetConsumptionRecords(c *gin.Context) {
|
||||
query = query.Where("created_at <= ?", endDate+" 23:59:59")
|
||||
}
|
||||
|
||||
if err := query.Order("created_at DESC").Find(&records).Error; err != nil {
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
|
||||
response.Error(c, 500, "获取消费记录失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{
|
||||
"records": records,
|
||||
"total": len(records),
|
||||
"records": records,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/scheduler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -25,6 +26,9 @@ func SetupSystemSettingsRoutes(r *gin.RouterGroup) {
|
||||
settings.PUT("/payment", handleUpdatePaymentSettings)
|
||||
settings.GET("/email", handleGetEmailSettings)
|
||||
settings.PUT("/email", handleUpdateEmailSettings)
|
||||
settings.GET("/cleanup", handleGetCleanupSettings)
|
||||
settings.PUT("/cleanup", handleUpdateCleanupSettings)
|
||||
settings.POST("/cleanup/run", handleRunCleanup)
|
||||
}
|
||||
|
||||
paymentChannels := r.Group("/payment-channels")
|
||||
@@ -172,6 +176,88 @@ func handleGetSystemSettings(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
type CleanupSettingsResponse struct {
|
||||
EnableAutoCleanup bool `json:"enable_auto_cleanup"`
|
||||
CleanupIntervalHours int `json:"cleanup_interval_hours"`
|
||||
CaptchaRetentionDays int `json:"captcha_retention_days"`
|
||||
VerifyCodeRetentionDays int `json:"verify_code_retention_days"`
|
||||
ApiUsageRetentionDays int `json:"api_usage_retention_days"`
|
||||
WebhookLogRetentionDays int `json:"webhook_log_retention_days"`
|
||||
DeviceSessionRetentionDays int `json:"device_session_retention_days"`
|
||||
}
|
||||
|
||||
func handleGetCleanupSettings(c *gin.Context) {
|
||||
cfg := scheduler.GetCleanupConfig()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"data": CleanupSettingsResponse{
|
||||
EnableAutoCleanup: cfg.EnableAutoCleanup,
|
||||
CleanupIntervalHours: cfg.CleanupIntervalHours,
|
||||
CaptchaRetentionDays: cfg.CaptchaRetentionDays,
|
||||
VerifyCodeRetentionDays: cfg.VerifyCodeRetentionDays,
|
||||
ApiUsageRetentionDays: cfg.ApiUsageRetentionDays,
|
||||
WebhookLogRetentionDays: cfg.WebhookLogRetentionDays,
|
||||
DeviceSessionRetentionDays: cfg.DeviceSessionRetentionDays,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func handleUpdateCleanupSettings(c *gin.Context) {
|
||||
var req CleanupSettingsResponse
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "无效的请求数据",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
settings := []struct {
|
||||
Key string
|
||||
Value string
|
||||
}{
|
||||
{"enable_auto_cleanup", fmt.Sprintf("%v", req.EnableAutoCleanup)},
|
||||
{"cleanup_interval_hours", fmt.Sprintf("%d", req.CleanupIntervalHours)},
|
||||
{"captcha_retention_days", fmt.Sprintf("%d", req.CaptchaRetentionDays)},
|
||||
{"verify_code_retention_days", fmt.Sprintf("%d", req.VerifyCodeRetentionDays)},
|
||||
{"api_usage_retention_days", fmt.Sprintf("%d", req.ApiUsageRetentionDays)},
|
||||
{"webhook_log_retention_days", fmt.Sprintf("%d", req.WebhookLogRetentionDays)},
|
||||
{"device_session_retention_days", fmt.Sprintf("%d", req.DeviceSessionRetentionDays)},
|
||||
}
|
||||
|
||||
for _, s := range settings {
|
||||
var setting model.Setting
|
||||
result := database.DB.Where("category = ? AND key = ?", "cleanup", s.Key).First(&setting)
|
||||
if result.Error == nil {
|
||||
setting.Value = s.Value
|
||||
database.DB.Save(&setting)
|
||||
} else {
|
||||
setting = model.Setting{
|
||||
Category: "cleanup",
|
||||
Key: s.Key,
|
||||
Value: s.Value,
|
||||
}
|
||||
database.DB.Create(&setting)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"message": "保存成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleRunCleanup(c *gin.Context) {
|
||||
cfg := scheduler.GetCleanupConfig()
|
||||
scheduler.RunCleanup(cfg)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"message": "清理完成",
|
||||
})
|
||||
}
|
||||
|
||||
func parseSettingInt(value string, defaultValue int) int {
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"verification-platform-backend/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserWithStatus struct {
|
||||
@@ -192,6 +193,28 @@ func handleGetUsers(c *gin.Context) {
|
||||
offlineCount := 0
|
||||
bannedCount := 0
|
||||
|
||||
userIDs := make([]uint, len(users))
|
||||
for i, u := range users {
|
||||
userIDs[i] = u.ID
|
||||
}
|
||||
|
||||
deviceCountMap := make(map[uint]int)
|
||||
if len(userIDs) > 0 {
|
||||
type DeviceCountResult struct {
|
||||
UserID uint
|
||||
Count int
|
||||
}
|
||||
var deviceCounts []DeviceCountResult
|
||||
database.DB.Model(&model.UserDevice{}).
|
||||
Select("user_id, COUNT(*) as count").
|
||||
Where("user_id IN ?", userIDs).
|
||||
Group("user_id").
|
||||
Find(&deviceCounts)
|
||||
for _, dc := range deviceCounts {
|
||||
deviceCountMap[dc.UserID] = dc.Count
|
||||
}
|
||||
}
|
||||
|
||||
usersWithStatus := make([]UserWithStatus, 0, len(users))
|
||||
for _, user := range users {
|
||||
log.Printf("[DEBUG] User ID=%d, Username=%s, LastLoginAt=%v, LastHeartbeatAt=%v", user.ID, user.Username, user.LastLoginAt, user.LastHeartbeatAt)
|
||||
@@ -214,15 +237,12 @@ func handleGetUsers(c *gin.Context) {
|
||||
bannedCount++
|
||||
}
|
||||
|
||||
var deviceCount int64
|
||||
database.DB.Model(&model.UserDevice{}).Where("user_id = ?", user.ID).Count(&deviceCount)
|
||||
|
||||
log.Printf("[DEBUG] 用户 %s (ID=%d) 余额: %f", user.Username, user.ID, user.Balance)
|
||||
|
||||
usersWithStatus = append(usersWithStatus, UserWithStatus{
|
||||
AppUser: user,
|
||||
OnlineStatus: onlineStatus,
|
||||
DeviceCount: int(deviceCount),
|
||||
DeviceCount: deviceCountMap[user.ID],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -705,22 +725,35 @@ func handleUpdateExpiry(c *gin.Context) {
|
||||
}
|
||||
} else {
|
||||
if req.Type == "recharge" {
|
||||
appUser.Balance += req.Amount
|
||||
if err := database.DB.Model(&model.AppUser{}).Where("id = ?", appUser.ID).
|
||||
Update("balance", gorm.Expr("balance + ?", req.Amount)).Error; err != nil {
|
||||
response.Error(c, 500, "充值失败")
|
||||
return
|
||||
}
|
||||
} else if req.Type == "deduct" {
|
||||
if appUser.Balance < req.Amount {
|
||||
result := database.DB.Model(&model.AppUser{}).Where("id = ? AND balance >= ?", appUser.ID, req.Amount).
|
||||
Update("balance", gorm.Expr("balance - ?", req.Amount))
|
||||
if result.Error != nil {
|
||||
response.Error(c, 500, "扣费失败")
|
||||
return
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
response.Error(c, 400, "余额不足")
|
||||
return
|
||||
}
|
||||
appUser.Balance -= req.Amount
|
||||
} else {
|
||||
response.Error(c, 400, "操作类型错误")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&appUser).Error; err != nil {
|
||||
response.Error(c, 500, "更新失败")
|
||||
return
|
||||
if app.BillingType == "balance" {
|
||||
database.DB.Where("id = ?", appUser.ID).First(&appUser)
|
||||
} else {
|
||||
if err := database.DB.Save(&appUser).Error; err != nil {
|
||||
response.Error(c, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, appUser)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SetupAccountRoutes(r *gin.RouterGroup) {
|
||||
@@ -126,14 +127,15 @@ func handleAppHeartbeat(c *gin.Context) {
|
||||
}
|
||||
log.Printf("[DEBUG] User %d subscription valid, skip balance deduction", user.ID)
|
||||
} else {
|
||||
if user.Balance >= app.DeductionAmount {
|
||||
user.Balance -= app.DeductionAmount
|
||||
log.Printf("[DEBUG] Deducted %.2f from user %d, new balance: %.2f", app.DeductionAmount, user.ID, user.Balance)
|
||||
} else {
|
||||
result := database.DB.Model(&model.AppUser{}).Where("id = ? AND balance >= ?", user.ID, app.DeductionAmount).
|
||||
Update("balance", gorm.Expr("balance - ?", app.DeductionAmount))
|
||||
if result.Error != nil || result.RowsAffected == 0 {
|
||||
log.Printf("[DEBUG] User %d has insufficient balance: %.2f < %.2f", user.ID, user.Balance, app.DeductionAmount)
|
||||
response.Error(c, 403, "余额不足")
|
||||
return
|
||||
}
|
||||
database.DB.Where("id = ?", user.ID).First(&user)
|
||||
log.Printf("[DEBUG] Deducted %.2f from user %d, new balance: %.2f", app.DeductionAmount, user.ID, user.Balance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SetupDynamicRoutes(r *gin.RouterGroup) {
|
||||
@@ -408,9 +409,8 @@ func handleExtendTime(appID uint, actionMap map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
user.Balance += float64(days)
|
||||
|
||||
return database.DB.Save(&user).Error
|
||||
return database.DB.Model(&model.AppUser{}).Where("id = ?", user.ID).
|
||||
Update("balance", gorm.Expr("balance + ?", float64(days))).Error
|
||||
}
|
||||
|
||||
func handleDeductPoints(appID uint, actionMap map[string]interface{}) error {
|
||||
@@ -433,12 +433,16 @@ func handleDeductPoints(appID uint, actionMap map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
user.Balance -= points
|
||||
if user.Balance < 0 {
|
||||
user.Balance = 0
|
||||
result := database.DB.Model(&model.AppUser{}).Where("id = ? AND balance >= ?", user.ID, points).
|
||||
Update("balance", gorm.Expr("balance - ?", points))
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
return database.DB.Save(&user).Error
|
||||
if result.RowsAffected == 0 {
|
||||
return database.DB.Model(&model.AppUser{}).Where("id = ?", user.ID).
|
||||
Update("balance", 0).Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleUpdateUserVariable(appID *uint, userID *uint, actionMap map[string]interface{}) error {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SetupRoutes(r *gin.RouterGroup) {
|
||||
@@ -265,7 +266,45 @@ func handleRechargeUser(c *gin.Context) {
|
||||
newExpiry := baseTime.Add(duration)
|
||||
user.ExpiryAt = &newExpiry
|
||||
case "balance":
|
||||
user.Balance += float64(req.Amount)
|
||||
tx := database.DB.Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := tx.Model(&model.AppUser{}).Where("id = ?", user.ID).
|
||||
Update("balance", gorm.Expr("balance + ?", float64(req.Amount))).Error; err != nil {
|
||||
tx.Rollback()
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_recharge_failed", fmt.Sprintf("扩展API充值失败: 保存失败 - %s", user.Username), "", err)
|
||||
response.Error(c, 500, "充值失败")
|
||||
return
|
||||
}
|
||||
|
||||
record := model.RechargeRecord{
|
||||
UserID: user.ID,
|
||||
OrderNo: fmt.Sprintf("EXT%d%d", time.Now().Unix(), user.ID),
|
||||
Amount: float64(req.Amount),
|
||||
Status: "success",
|
||||
PaymentType: "extension_api",
|
||||
Remark: req.Description,
|
||||
}
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
tx.Rollback()
|
||||
response.Error(c, 500, "充值失败")
|
||||
return
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
database.DB.Where("id = ?", user.ID).First(&user)
|
||||
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_recharge", fmt.Sprintf("扩展API充值: 用户%s, 类型:%s, 数量:%d", user.Username, req.Type, req.Amount), "", nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "充值成功",
|
||||
"user": user,
|
||||
})
|
||||
return
|
||||
default:
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_recharge_failed", fmt.Sprintf("扩展API充值失败: 类型无效 - %s", req.Type), "", fmt.Errorf("充值类型无效"))
|
||||
response.Error(c, 400, "充值类型无效,仅支持days或balance类型")
|
||||
@@ -362,7 +401,53 @@ func handleDeductUser(c *gin.Context) {
|
||||
response.Error(c, 400, "余额不足")
|
||||
return
|
||||
}
|
||||
user.Balance -= float64(req.Amount)
|
||||
|
||||
tx := database.DB.Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
result := tx.Model(&model.AppUser{}).Where("id = ? AND balance >= ?", user.ID, float64(req.Amount)).
|
||||
Update("balance", gorm.Expr("balance - ?", float64(req.Amount)))
|
||||
if result.Error != nil {
|
||||
tx.Rollback()
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 保存失败 - %s", user.Username), "", result.Error)
|
||||
response.Error(c, 500, "扣除失败")
|
||||
return
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
tx.Rollback()
|
||||
response.Error(c, 400, "余额不足")
|
||||
return
|
||||
}
|
||||
|
||||
record := model.ConsumptionRecord{
|
||||
UserID: user.ID,
|
||||
OrderNo: fmt.Sprintf("EXT%d%d", time.Now().Unix(), user.ID),
|
||||
Type: req.Type,
|
||||
Amount: float64(req.Amount),
|
||||
Status: "success",
|
||||
PaymentType: "extension_api",
|
||||
Remark: req.Description,
|
||||
}
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
tx.Rollback()
|
||||
response.Error(c, 500, "扣除失败")
|
||||
return
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
database.DB.Where("id = ?", user.ID).First(&user)
|
||||
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_deduct", fmt.Sprintf("扩展API扣费: 用户%s, 类型:%s, 数量:%d", user.Username, req.Type, req.Amount), "", nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "扣除成功",
|
||||
"user": user,
|
||||
})
|
||||
return
|
||||
default:
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 类型无效 - %s", req.Type), "", fmt.Errorf("扣除类型无效"))
|
||||
response.Error(c, 400, "扣除类型无效,仅支持days或balance类型")
|
||||
|
||||
@@ -325,6 +325,7 @@ func generateConfigFile(dbType string, req SetupRequest, jwtSecret string) strin
|
||||
|
||||
content.WriteString("# Redis配置\n")
|
||||
content.WriteString("redis:\n")
|
||||
content.WriteString(fmt.Sprintf(" enabled: %v\n", req.UseRedis))
|
||||
if req.UseRedis {
|
||||
content.WriteString(fmt.Sprintf(" host: %s\n", req.RedisHost))
|
||||
content.WriteString(fmt.Sprintf(" port: %s\n", req.RedisPort))
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
)
|
||||
|
||||
type CleanupConfig struct {
|
||||
EnableAutoCleanup bool
|
||||
CleanupIntervalHours int
|
||||
CaptchaRetentionDays int
|
||||
VerifyCodeRetentionDays int
|
||||
ApiUsageRetentionDays int
|
||||
WebhookLogRetentionDays int
|
||||
DeviceSessionRetentionDays int
|
||||
}
|
||||
|
||||
var defaultConfig = CleanupConfig{
|
||||
EnableAutoCleanup: false,
|
||||
CleanupIntervalHours: 24,
|
||||
CaptchaRetentionDays: 1,
|
||||
VerifyCodeRetentionDays: 7,
|
||||
ApiUsageRetentionDays: 30,
|
||||
WebhookLogRetentionDays: 30,
|
||||
DeviceSessionRetentionDays: 7,
|
||||
}
|
||||
|
||||
func GetCleanupConfig() CleanupConfig {
|
||||
cfg := defaultConfig
|
||||
|
||||
var settings []model.Setting
|
||||
database.DB.Where("category = ?", "cleanup").Find(&settings)
|
||||
|
||||
for _, s := range settings {
|
||||
switch s.Key {
|
||||
case "enable_auto_cleanup":
|
||||
cfg.EnableAutoCleanup = s.Value == "true"
|
||||
case "cleanup_interval_hours":
|
||||
cfg.CleanupIntervalHours = parseVal(s.Value, defaultConfig.CleanupIntervalHours)
|
||||
case "captcha_retention_days":
|
||||
cfg.CaptchaRetentionDays = parseVal(s.Value, defaultConfig.CaptchaRetentionDays)
|
||||
case "verify_code_retention_days":
|
||||
cfg.VerifyCodeRetentionDays = parseVal(s.Value, defaultConfig.VerifyCodeRetentionDays)
|
||||
case "api_usage_retention_days":
|
||||
cfg.ApiUsageRetentionDays = parseVal(s.Value, defaultConfig.ApiUsageRetentionDays)
|
||||
case "webhook_log_retention_days":
|
||||
cfg.WebhookLogRetentionDays = parseVal(s.Value, defaultConfig.WebhookLogRetentionDays)
|
||||
case "device_session_retention_days":
|
||||
cfg.DeviceSessionRetentionDays = parseVal(s.Value, defaultConfig.DeviceSessionRetentionDays)
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func parseVal(value string, defaultVal int) int {
|
||||
if value == "" {
|
||||
return defaultVal
|
||||
}
|
||||
var result int
|
||||
if _, err := fmt.Sscanf(value, "%d", &result); err != nil || result <= 0 {
|
||||
return defaultVal
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func StartCleanupScheduler() {
|
||||
go func() {
|
||||
for {
|
||||
cfg := GetCleanupConfig()
|
||||
interval := time.Duration(cfg.CleanupIntervalHours) * time.Hour
|
||||
if interval < time.Hour {
|
||||
interval = time.Hour
|
||||
}
|
||||
time.Sleep(interval)
|
||||
|
||||
if !cfg.EnableAutoCleanup {
|
||||
continue
|
||||
}
|
||||
|
||||
RunCleanup(cfg)
|
||||
}
|
||||
}()
|
||||
log.Println("[Scheduler] Cleanup scheduler started")
|
||||
}
|
||||
|
||||
func RunCleanup(cfg CleanupConfig) {
|
||||
now := time.Now()
|
||||
totalCleaned := 0
|
||||
|
||||
if cfg.CaptchaRetentionDays > 0 {
|
||||
cutoff := now.AddDate(0, 0, -cfg.CaptchaRetentionDays)
|
||||
result := database.DB.Where("expires_at < ?", cutoff).Delete(&model.Captcha{})
|
||||
if result.RowsAffected > 0 {
|
||||
log.Printf("[Cleanup] Deleted %d expired captchas", result.RowsAffected)
|
||||
totalCleaned += int(result.RowsAffected)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.VerifyCodeRetentionDays > 0 {
|
||||
cutoff := now.AddDate(0, 0, -cfg.VerifyCodeRetentionDays)
|
||||
emailResult := database.DB.Where("expires_at < ? AND used = ?", cutoff, true).Delete(&model.EmailVerifyCode{})
|
||||
smsResult := database.DB.Where("expires_at < ? AND used = ?", cutoff, true).Delete(&model.SmsVerifyCode{})
|
||||
count := emailResult.RowsAffected + smsResult.RowsAffected
|
||||
if count > 0 {
|
||||
log.Printf("[Cleanup] Deleted %d expired verify codes", count)
|
||||
totalCleaned += int(count)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.ApiUsageRetentionDays > 0 {
|
||||
cutoff := now.AddDate(0, 0, -cfg.ApiUsageRetentionDays)
|
||||
result := database.DB.Where("created_at < ?", cutoff).Delete(&model.ApiUsage{})
|
||||
if result.RowsAffected > 0 {
|
||||
log.Printf("[Cleanup] Deleted %d old api usage records", result.RowsAffected)
|
||||
totalCleaned += int(result.RowsAffected)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.WebhookLogRetentionDays > 0 {
|
||||
cutoff := now.AddDate(0, 0, -cfg.WebhookLogRetentionDays)
|
||||
result := database.DB.Where("created_at < ?", cutoff).Delete(&model.WebhookLog{})
|
||||
if result.RowsAffected > 0 {
|
||||
log.Printf("[Cleanup] Deleted %d old webhook logs", result.RowsAffected)
|
||||
totalCleaned += int(result.RowsAffected)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.DeviceSessionRetentionDays > 0 {
|
||||
cutoff := now.AddDate(0, 0, -cfg.DeviceSessionRetentionDays)
|
||||
result := database.DB.Where("last_heartbeat < ? AND last_heartbeat IS NOT NULL", cutoff).Delete(&model.DeviceSession{})
|
||||
if result.RowsAffected > 0 {
|
||||
log.Printf("[Cleanup] Deleted %d expired device sessions", result.RowsAffected)
|
||||
totalCleaned += int(result.RowsAffected)
|
||||
}
|
||||
}
|
||||
|
||||
if totalCleaned > 0 {
|
||||
log.Printf("[Cleanup] Total cleaned: %d records", totalCleaned)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user