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) }