aaxaxax's picture
force update files
1de7911
package handler
import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"zencoder-2api/internal/database"
"zencoder-2api/internal/model"
"zencoder-2api/internal/service"
)
type AccountHandler struct{}
func NewAccountHandler() *AccountHandler {
return &AccountHandler{}
}
func (h *AccountHandler) List(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
// 兼容旧的 category 参数,优先使用 status
status := c.DefaultQuery("status", "")
if status == "" {
category := c.DefaultQuery("category", "normal")
if category == "abnormal" {
status = "cooling"
} else {
status = category
}
}
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
var accounts []model.Account
var total int64
query := database.GetDB().Model(&model.Account{})
if status != "all" {
query = query.Where("status = ?", status)
}
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
offset := (page - 1) * size
if err := query.Offset(offset).Limit(size).Order("id desc").Find(&accounts).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 调试日志:输出冷却账号的信息
if status == "cooling" {
for _, acc := range accounts {
if !acc.CoolingUntil.IsZero() {
log.Printf("[DEBUG] 冷却账号 %s (ID:%d) - CoolingUntil: %s (UTC), 现在: %s (UTC)",
acc.Email, acc.ID,
acc.CoolingUntil.Format("2006-01-02 15:04:05"),
time.Now().UTC().Format("2006-01-02 15:04:05"))
}
}
}
// Calculate Stats
var stats struct {
TotalAccounts int64 `json:"total_accounts"`
NormalAccounts int64 `json:"normal_accounts"` // 原 active_accounts
BannedAccounts int64 `json:"banned_accounts"`
ErrorAccounts int64 `json:"error_accounts"`
CoolingAccounts int64 `json:"cooling_accounts"`
DisabledAccounts int64 `json:"disabled_accounts"`
TodayUsage float64 `json:"today_usage"`
TotalUsage float64 `json:"total_usage"`
}
db := database.GetDB()
db.Model(&model.Account{}).Count(&stats.TotalAccounts)
db.Model(&model.Account{}).Where("status = ?", "normal").Count(&stats.NormalAccounts)
db.Model(&model.Account{}).Where("status = ?", "banned").Count(&stats.BannedAccounts)
db.Model(&model.Account{}).Where("status = ?", "error").Count(&stats.ErrorAccounts)
db.Model(&model.Account{}).Where("status = ?", "cooling").Count(&stats.CoolingAccounts)
db.Model(&model.Account{}).Where("status = ?", "disabled").Count(&stats.DisabledAccounts)
db.Model(&model.Account{}).Select("COALESCE(SUM(daily_used), 0)").Scan(&stats.TodayUsage)
db.Model(&model.Account{}).Select("COALESCE(SUM(total_used), 0)").Scan(&stats.TotalUsage)
// 兼容前端旧字段
statsMap := map[string]interface{}{
"total_accounts": stats.TotalAccounts,
"active_accounts": stats.NormalAccounts,
"banned_accounts": stats.BannedAccounts,
"error_accounts": stats.ErrorAccounts,
"cooling_accounts": stats.CoolingAccounts,
"disabled_accounts": stats.DisabledAccounts,
"today_usage": stats.TodayUsage,
"total_usage": stats.TotalUsage,
}
c.JSON(http.StatusOK, gin.H{
"items": accounts,
"total": total,
"page": page,
"size": size,
"stats": statsMap,
})
}
type BatchCategoryRequest struct {
IDs []uint `json:"ids"`
Category string `json:"category"` // 前端可能还传 category
Status string `json:"status"`
}
func (h *AccountHandler) BatchUpdateCategory(c *gin.Context) {
var req BatchCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(req.IDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no ids provided"})
return
}
status := req.Status
if status == "" {
status = req.Category
}
updates := map[string]interface{}{
"status": status,
// 兼容旧字段
"category": status,
}
switch status {
case "normal":
updates["is_active"] = true
updates["is_cooling"] = false
case "cooling":
updates["is_active"] = true // cooling 也是 active 的一种? 不,cooling 不参与轮询
updates["is_cooling"] = true
case "disabled":
updates["is_active"] = false
updates["is_cooling"] = false
default: // banned, error
updates["is_active"] = false
updates["is_cooling"] = false
}
if err := database.GetDB().Model(&model.Account{}).Where("id IN ?", req.IDs).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 触发 refresh? 为了性能这里不触发,等待自动刷新
c.JSON(http.StatusOK, gin.H{"message": "updated", "count": len(req.IDs)})
}
type MoveAllRequest struct {
FromStatus string `json:"from_status"`
ToStatus string `json:"to_status"`
}
// BatchMoveAll 一键移动某个分类的所有账号到另一个分类
func (h *AccountHandler) BatchMoveAll(c *gin.Context) {
var req MoveAllRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.FromStatus == "" || req.ToStatus == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "from_status and to_status are required"})
return
}
if req.FromStatus == req.ToStatus {
c.JSON(http.StatusBadRequest, gin.H{"error": "from_status and to_status cannot be the same"})
return
}
updates := map[string]interface{}{
"status": req.ToStatus,
// 兼容旧字段
"category": req.ToStatus,
}
// 根据目标状态设置相应的标志
switch req.ToStatus {
case "normal":
updates["is_active"] = true
updates["is_cooling"] = false
updates["error_count"] = 0
updates["ban_reason"] = ""
case "cooling":
updates["is_active"] = false
updates["is_cooling"] = true
updates["ban_reason"] = ""
case "disabled":
updates["is_active"] = false
updates["is_cooling"] = false
updates["ban_reason"] = ""
case "banned":
updates["is_active"] = false
updates["is_cooling"] = false
case "error":
updates["is_active"] = false
updates["is_cooling"] = false
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid to_status"})
return
}
// 执行批量更新
result := database.GetDB().Model(&model.Account{}).Where("status = ?", req.FromStatus).Updates(updates)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
return
}
log.Printf("[批量移动] 从 %s 移动到 %s,影响 %d 个账号", req.FromStatus, req.ToStatus, result.RowsAffected)
c.JSON(http.StatusOK, gin.H{
"message": "moved successfully",
"moved_count": result.RowsAffected,
"from_status": req.FromStatus,
"to_status": req.ToStatus,
})
}
type BatchRefreshTokenRequest struct {
IDs []uint `json:"ids"` // 选中的账号IDs,如果为空则刷新所有账号
All bool `json:"all"` // 是否刷新所有账号
}
type BatchDeleteRequest struct {
IDs []uint `json:"ids"` // 选中的账号IDs
DeleteAll bool `json:"delete_all"` // 是否删除分类中的所有账号
Status string `json:"status"` // 要删除的分类状态
}
// BatchRefreshToken 批量刷新账号token
func (h *AccountHandler) BatchRefreshToken(c *gin.Context) {
var req BatchRefreshTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 设置流式响应头
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
flusher, ok := c.Writer.(http.Flusher)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "流式传输不支持"})
return
}
var accounts []model.Account
var err error
// 根据请求类型获取要刷新的账号
if req.All {
// 刷新所有状态为normal且有refresh_token的账号
err = database.GetDB().Where("status = ? AND (client_id != '' AND client_secret != '')", "normal").Find(&accounts).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取账号列表失败: " + err.Error()})
return
}
log.Printf("[批量刷新Token] 准备刷新所有正常账号,共 %d 个", len(accounts))
} else if len(req.IDs) > 0 {
// 刷新选中的账号
err = database.GetDB().Where("id IN ? AND (client_id != '' AND client_secret != '')", req.IDs).Find(&accounts).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取选中账号失败: " + err.Error()})
return
}
log.Printf("[批量刷新Token] 准备刷新选中账号,共 %d 个", len(accounts))
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择要刷新的账号或选择刷新所有账号"})
return
}
if len(accounts) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "没有找到可刷新的账号"})
return
}
// 发送开始消息
fmt.Fprintf(c.Writer, "data: {\"type\":\"start\",\"total\":%d}\n\n", len(accounts))
flusher.Flush()
successCount := 0
failCount := 0
// 逐个刷新token
for i, account := range accounts {
log.Printf("[批量刷新Token] 开始刷新第 %d/%d 个账号: %s (ID:%d)", i+1, len(accounts), account.ClientID, account.ID)
// 使用OAuth client credentials刷新token
if err := service.RefreshAccountToken(&account); err != nil {
failCount++
errMsg := fmt.Sprintf("刷新失败: %v", err)
// 检查是否是账号锁定错误
if lockoutErr, ok := err.(*service.AccountLockoutError); ok {
errMsg = fmt.Sprintf("账号被锁定已自动标记为封禁: %s", lockoutErr.Body)
log.Printf("[批量刷新Token] 第 %d/%d 个账号被锁定: %s - %s", i+1, len(accounts), account.ClientID, lockoutErr.Body)
} else {
log.Printf("[批量刷新Token] 第 %d/%d 个账号刷新失败: %s - %v", i+1, len(accounts), account.ClientID, err)
}
fmt.Fprintf(c.Writer, "data: {\"type\":\"error\",\"index\":%d,\"account_id\":\"%s\",\"message\":\"%s\"}\n\n", i+1, account.ClientID, errMsg)
flusher.Flush()
} else {
successCount++
log.Printf("[批量刷新Token] 第 %d/%d 个账号刷新成功: %s (ID:%d)", i+1, len(accounts), account.ClientID, account.ID)
fmt.Fprintf(c.Writer, "data: {\"type\":\"success\",\"index\":%d,\"account_id\":\"%s\",\"email\":\"%s\"}\n\n", i+1, account.ClientID, account.Email)
flusher.Flush()
}
// 添加延迟避免请求过快
if i < len(accounts)-1 {
time.Sleep(200 * time.Millisecond)
}
}
// 发送完成消息
log.Printf("[批量刷新Token] 完成: 成功 %d 个, 失败 %d 个", successCount, failCount)
fmt.Fprintf(c.Writer, "data: {\"type\":\"complete\",\"success\":%d,\"fail\":%d}\n\n", successCount, failCount)
flusher.Flush()
}
func (h *AccountHandler) Create(c *gin.Context) {
var req model.AccountRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 生成模式 - 固定生成1个账号
if req.GenerateMode {
// 检查是否提供了 refresh_token 或 access_token
if req.RefreshToken == "" && req.Token == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "生成模式需要提供 access_token 或 RefreshToken"})
return
}
// 优先使用 access_token,如果同时提供了两个字段
var masterToken string
if req.Token != "" {
// 直接使用提供的 access_token
masterToken = req.Token
log.Printf("[生成凭证] 使用提供的 access_token")
} else {
// 使用 refresh_token 获取 access_token
tokenResp, err := service.RefreshAccessToken(req.RefreshToken, req.Proxy)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "RefreshToken 无效: " + err.Error()})
return
}
masterToken = tokenResp.AccessToken
log.Printf("[生成凭证] 通过 RefreshToken 获取了 access_token")
}
log.Printf("[生成凭证] 开始生成账号凭证")
// 生成凭证
cred, err := service.GenerateCredential(masterToken)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("生成失败: %v", err)})
return
}
log.Printf("[生成凭证] 凭证生成成功: ClientID=%s", cred.ClientID)
// 创建账号
account := model.Account{
ClientID: cred.ClientID,
ClientSecret: cred.Secret,
Proxy: req.Proxy,
IsActive: true,
Status: "normal",
}
// 使用生成的client_id和client_secret获取token
// 使用OAuth client credentials方式刷新token,使用 https://fe.zencoder.ai/oauth/token
if _, err := service.RefreshToken(&account); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("认证失败: %v", err)})
return
}
// 解析 Token 获取详细信息
if payload, err := service.ParseJWT(account.AccessToken); err == nil {
account.Email = payload.Email
account.SubscriptionStartDate = service.GetSubscriptionDate(payload)
if payload.Expiration > 0 {
account.TokenExpiry = time.Unix(payload.Expiration, 0)
}
plan := payload.CustomClaims.Plan
if plan != "" {
plan = strings.ToUpper(plan[:1]) + plan[1:]
}
if plan != "" {
account.PlanType = model.PlanType(plan)
}
}
if account.PlanType == "" {
account.PlanType = model.PlanFree
}
// 检查是否已存在
var existing model.Account
var count int64
database.GetDB().Model(&model.Account{}).Where("client_id = ?", account.ClientID).Count(&count)
if count > 0 {
// 获取现有账号
database.GetDB().Where("client_id = ?", account.ClientID).First(&existing)
// 更新现有账号
existing.AccessToken = account.AccessToken
existing.TokenExpiry = account.TokenExpiry
existing.PlanType = account.PlanType
existing.Email = account.Email
existing.SubscriptionStartDate = account.SubscriptionStartDate
existing.IsActive = true
existing.Status = "normal" // 重新激活
existing.ClientSecret = account.ClientSecret
if account.Proxy != "" {
existing.Proxy = account.Proxy
}
if err := database.GetDB().Save(&existing).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新失败: %v", err)})
return
}
log.Printf("[添加账号] 账号更新成功: ClientID=%s, Email=%s, Plan=%s", existing.ClientID, existing.Email, existing.PlanType)
c.JSON(http.StatusOK, existing)
} else {
// 创建新账号
if err := database.GetDB().Create(&account).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建失败: %v", err)})
return
}
log.Printf("[添加账号] 新账号创建成功: ClientID=%s, Email=%s, Plan=%s", account.ClientID, account.Email, account.PlanType)
c.JSON(http.StatusCreated, account)
}
return
}
// 原有的单个账号添加逻辑 - 现在使用 refresh_token
account := model.Account{
Proxy: req.Proxy,
IsActive: true,
Status: "normal",
}
// 优先使用 access_token,如果同时提供了两个字段则不使用 refresh_token
if req.Token != "" && req.RefreshToken != "" {
log.Printf("[凭证模式] 同时提供了 access_token 和 RefreshToken,优先使用 access_token")
}
if req.Token != "" {
// JWT Parsing Logic
payload, err := service.ParseJWT(req.Token)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的Token: " + err.Error()})
return
}
account.AccessToken = req.Token
// 优先使用ClientID字段,如果没有则使用Subject
if payload.ClientID != "" {
account.ClientID = payload.ClientID
} else {
account.ClientID = payload.Subject
}
account.Email = payload.Email
account.SubscriptionStartDate = service.GetSubscriptionDate(payload)
if payload.Expiration > 0 {
account.TokenExpiry = time.Unix(payload.Expiration, 0)
} else {
account.TokenExpiry = time.Now().Add(24 * time.Hour) // 默认24小时
}
// Map PlanType
plan := payload.CustomClaims.Plan
// Simple normalization
if plan != "" {
plan = strings.ToUpper(plan[:1]) + plan[1:]
}
account.PlanType = model.PlanType(plan)
if account.PlanType == "" {
account.PlanType = model.PlanFree
}
// Placeholder for secret since it's required by DB but not in JWT
account.ClientSecret = "jwt-login"
} else if req.RefreshToken != "" {
// 只提供了 refresh_token,使用它来获取 access_token
tokenResp, err := service.RefreshAccessToken(req.RefreshToken, req.Proxy)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "RefreshToken 无效: " + err.Error()})
return
}
account.AccessToken = tokenResp.AccessToken
account.RefreshToken = tokenResp.RefreshToken
account.TokenExpiry = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
// 解析 Token 获取详细信息
if payload, err := service.ParseJWT(tokenResp.AccessToken); err == nil {
// 设置 Email
if payload.Email != "" {
account.Email = payload.Email
} else if tokenResp.Email != "" {
account.Email = tokenResp.Email
}
// 设置 ClientID - 优先使用 Email 作为唯一标识符
if payload.Email != "" {
account.ClientID = payload.Email
} else if payload.Subject != "" {
account.ClientID = payload.Subject
} else if payload.ClientID != "" {
account.ClientID = payload.ClientID
}
account.SubscriptionStartDate = service.GetSubscriptionDate(payload)
// Map PlanType
plan := payload.CustomClaims.Plan
if plan != "" {
plan = strings.ToUpper(plan[:1]) + plan[1:]
}
account.PlanType = model.PlanType(plan)
if account.PlanType == "" {
account.PlanType = model.PlanFree
}
log.Printf("[凭证模式-RefreshToken] 解析JWT成功: ClientID=%s, Email=%s, Plan=%s",
account.ClientID, account.Email, account.PlanType)
} else {
log.Printf("[凭证模式-RefreshToken] 解析JWT失败: %v", err)
// 如果JWT解析失败,使用 tokenResp 中的信息
if tokenResp.UserID != "" {
account.ClientID = tokenResp.UserID
account.Email = tokenResp.UserID
}
}
// 生成一个占位 ClientSecret
account.ClientSecret = "refresh-token-login"
// 确保 ClientID 不为空
if account.ClientID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "无法获取用户信息,请检查RefreshToken是否有效"})
return
}
} else {
// Old Logic
if req.PlanType == "" {
req.PlanType = model.PlanFree
}
account.ClientID = req.ClientID
account.ClientSecret = req.ClientSecret
account.Email = req.Email
account.PlanType = req.PlanType
// 验证Token是否能正确获取
if _, err := service.RefreshToken(&account); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "认证失败: " + err.Error()})
return
}
// 解析Token获取详细信息
if payload, err := service.ParseJWT(account.AccessToken); err == nil {
if account.Email == "" {
account.Email = payload.Email
}
account.SubscriptionStartDate = service.GetSubscriptionDate(payload)
if payload.Expiration > 0 {
account.TokenExpiry = time.Unix(payload.Expiration, 0)
}
plan := payload.CustomClaims.Plan
if plan != "" {
plan = strings.ToUpper(plan[:1]) + plan[1:]
}
if plan != "" {
account.PlanType = model.PlanType(plan)
}
}
if account.PlanType == "" {
account.PlanType = model.PlanFree
}
}
// Check if account exists - 使用 Count 避免 record not found 警告
var existing model.Account
var count int64
database.GetDB().Model(&model.Account{}).Where("client_id = ?", account.ClientID).Count(&count)
if count > 0 {
// 获取现有账号
database.GetDB().Where("client_id = ?", account.ClientID).First(&existing)
// Update existing
existing.AccessToken = account.AccessToken
existing.RefreshToken = account.RefreshToken // 更新 refresh_token
existing.TokenExpiry = account.TokenExpiry
existing.PlanType = account.PlanType
existing.Email = account.Email
existing.SubscriptionStartDate = account.SubscriptionStartDate
existing.IsActive = true
existing.Status = "normal"
if account.Proxy != "" {
existing.Proxy = account.Proxy
}
// If secret was provided manually, update it. If placeholder, keep existing.
if account.ClientSecret != "jwt-login" && account.ClientSecret != "refresh-token-login" {
existing.ClientSecret = account.ClientSecret
}
if err := database.GetDB().Save(&existing).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, existing)
return
}
if err := database.GetDB().Create(&account).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, account)
}
func (h *AccountHandler) Update(c *gin.Context) {
id := c.Param("id")
var account model.Account
if err := database.GetDB().First(&account, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "account not found"})
return
}
var req model.AccountRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
account.Email = req.Email
account.PlanType = req.PlanType
account.Proxy = req.Proxy
if err := database.GetDB().Save(&account).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, account)
}
// BatchDelete 批量删除账号
func (h *AccountHandler) BatchDelete(c *gin.Context) {
var req BatchDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var deletedCount int64
if req.DeleteAll {
// 删除指定分类的所有账号
if req.Status == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "delete_all模式需要指定status"})
return
}
// 执行删除操作
result := database.GetDB().Where("status = ?", req.Status).Delete(&model.Account{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
return
}
deletedCount = result.RowsAffected
log.Printf("[批量删除] 删除分类 %s 的所有账号,共删除 %d 个", req.Status, deletedCount)
} else {
// 删除选中的账号
if len(req.IDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "未选择要删除的账号"})
return
}
// 执行删除操作
result := database.GetDB().Where("id IN ?", req.IDs).Delete(&model.Account{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
return
}
deletedCount = result.RowsAffected
log.Printf("[批量删除] 删除选中的 %d 个账号,实际删除 %d 个", len(req.IDs), deletedCount)
}
c.JSON(http.StatusOK, gin.H{
"message": "批量删除成功",
"deleted_count": deletedCount,
})
}
func (h *AccountHandler) Delete(c *gin.Context) {
id := c.Param("id")
if err := database.GetDB().Delete(&model.Account{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
func (h *AccountHandler) Toggle(c *gin.Context) {
id := c.Param("id")
var account model.Account
if err := database.GetDB().First(&account, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "account not found"})
return
}
// 切换 Disabled / Normal
if account.Status == "disabled" || !account.IsActive {
account.Status = "normal"
account.IsActive = true
account.IsCooling = false
account.ErrorCount = 0
} else {
account.Status = "disabled"
account.IsActive = false
}
database.GetDB().Save(&account)
c.JSON(http.StatusOK, account)
}