Files
codexautopool/backend/internal/api/s2a_clean.go

225 lines
5.8 KiB
Go

fpackage api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"codex-pool/internal/config"
"codex-pool/internal/logger"
)
// S2AAccountItem S2A 账号信息
type S2AAccountItem struct {
ID int `json:"id"`
Email string `json:"email"`
Status string `json:"status"`
}
// S2AAccountsResponse S2A 账号列表响应
type S2AAccountsResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Items []S2AAccountItem `json:"items"`
Total int `json:"total"`
Pages int `json:"pages"`
} `json:"data"`
}
// S2ADeleteResponse S2A 删除响应
type S2ADeleteResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
// HandleCleanErrorAccounts POST /api/s2a/clean-errors - 批量删除错误账号
func HandleCleanErrorAccounts(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
Error(w, http.StatusMethodNotAllowed, "仅支持 POST")
return
}
if config.Global == nil || config.Global.S2AApiBase == "" || config.Global.S2AAdminKey == "" {
Error(w, http.StatusBadRequest, "S2A 配置未设置")
return
}
logger.Info("开始清理错误账号...", "", "s2a")
// Step 1: 获取所有错误账号
errorAccounts, err := fetchAllErrorAccounts()
if err != nil {
logger.Error(fmt.Sprintf("获取错误账号列表失败: %v", err), "", "s2a")
Error(w, http.StatusInternalServerError, fmt.Sprintf("获取错误账号列表失败: %v", err))
return
}
if len(errorAccounts) == 0 {
logger.Info("没有错误账号需要清理", "", "s2a")
Success(w, map[string]interface{}{
"message": "没有错误账号需要清理",
"total": 0,
"success": 0,
"failed": 0,
})
return
}
logger.Info(fmt.Sprintf("找到 %d 个错误账号,开始删除...", len(errorAccounts)), "", "s2a")
// Step 2: 逐条删除
success := 0
failed := 0
var details []map[string]interface{}
for _, account := range errorAccounts {
err := deleteS2AAccount(account.ID)
if err != nil {
failed++
details = append(details, map[string]interface{}{
"id": account.ID,
"email": account.Email,
"success": false,
"error": err.Error(),
})
logger.Warning(fmt.Sprintf("删除账号失败: ID=%d, Email=%s, Error=%v", account.ID, account.Email, err), account.Email, "s2a")
} else {
success++
details = append(details, map[string]interface{}{
"id": account.ID,
"email": account.Email,
"success": true,
})
logger.Success(fmt.Sprintf("删除账号成功: ID=%d, Email=%s", account.ID, account.Email), account.Email, "s2a")
}
}
logger.Success(fmt.Sprintf("清理错误账号完成: 成功=%d, 失败=%d, 总数=%d", success, failed, len(errorAccounts)), "", "s2a")
Success(w, map[string]interface{}{
"message": fmt.Sprintf("清理完成: 成功 %d, 失败 %d", success, failed),
"total": len(errorAccounts),
"success": success,
"failed": failed,
"details": details,
})
}
// fetchAllErrorAccounts 分页获取所有错误账号
func fetchAllErrorAccounts() ([]S2AAccountItem, error) {
var allAccounts []S2AAccountItem
page := 1
pageSize := 100
client := &http.Client{Timeout: 30 * time.Second}
for {
url := fmt.Sprintf("%s/api/v1/admin/accounts?page=%d&page_size=%d&status=error&timezone=Asia/Shanghai",
config.Global.S2AApiBase, page, pageSize)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("创建请求失败: %v", err)
}
setS2AHeaders(req)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
var result S2AAccountsResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("API 错误: %s", result.Message)
}
if len(result.Data.Items) == 0 {
break
}
allAccounts = append(allAccounts, result.Data.Items...)
logger.Info(fmt.Sprintf("获取错误账号: 第 %d 页, 本页 %d 个, 累计 %d 个",
page, len(result.Data.Items), len(allAccounts)), "", "s2a")
if page >= result.Data.Pages {
break
}
page++
}
return allAccounts, nil
}
// deleteS2AAccount 删除单个 S2A 账号
func deleteS2AAccount(accountID int) error {
client := &http.Client{Timeout: 30 * time.Second}
url := fmt.Sprintf("%s/api/v1/admin/accounts/%d", config.Global.S2AApiBase, accountID)
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return fmt.Errorf("创建请求失败: %v", err)
}
setS2AHeaders(req)
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("读取响应失败: %v", err)
}
var result S2ADeleteResponse
if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("解析响应失败: %v", err)
}
if result.Code != 0 {
return fmt.Errorf("%s", result.Message)
}
return nil
}
// setS2AHeaders 设置 S2A 请求头
func setS2AHeaders(req *http.Request) {
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
// 认证方式: x-api-key 或 authorization
if config.Global.S2AAdminKey != "" {
req.Header.Set("X-API-Key", config.Global.S2AAdminKey)
}
// 也设置 Authorization 作为备用
req.Header.Set("Authorization", "Bearer "+config.Global.S2AAdminKey)
}