wzxwhxcz commited on
Commit
3516e13
·
verified ·
1 Parent(s): a8f4091

Upload 9 files

Browse files
internal/handler/account.go ADDED
@@ -0,0 +1,774 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handler
2
+
3
+ import (
4
+ "fmt"
5
+ "log"
6
+ "net/http"
7
+ "strconv"
8
+ "strings"
9
+ "time"
10
+
11
+ "github.com/gin-gonic/gin"
12
+ "zencoder-2api/internal/database"
13
+ "zencoder-2api/internal/model"
14
+ "zencoder-2api/internal/service"
15
+ )
16
+
17
+ type AccountHandler struct{}
18
+
19
+ func NewAccountHandler() *AccountHandler {
20
+ return &AccountHandler{}
21
+ }
22
+
23
+ func (h *AccountHandler) List(c *gin.Context) {
24
+ page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
25
+ size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
26
+
27
+ // 兼容旧的 category 参数,优先使用 status
28
+ status := c.DefaultQuery("status", "")
29
+ if status == "" {
30
+ category := c.DefaultQuery("category", "normal")
31
+ if category == "abnormal" {
32
+ status = "cooling"
33
+ } else {
34
+ status = category
35
+ }
36
+ }
37
+
38
+ if page < 1 {
39
+ page = 1
40
+ }
41
+ if size < 1 {
42
+ size = 10
43
+ }
44
+
45
+ var accounts []model.Account
46
+ var total int64
47
+
48
+ query := database.GetDB().Model(&model.Account{})
49
+ if status != "all" {
50
+ query = query.Where("status = ?", status)
51
+ }
52
+
53
+ if err := query.Count(&total).Error; err != nil {
54
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
55
+ return
56
+ }
57
+
58
+ offset := (page - 1) * size
59
+ if err := query.Offset(offset).Limit(size).Order("id desc").Find(&accounts).Error; err != nil {
60
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
61
+ return
62
+ }
63
+
64
+ // 调试日志:输出冷却账号的信息
65
+ if status == "cooling" {
66
+ for _, acc := range accounts {
67
+ if !acc.CoolingUntil.IsZero() {
68
+ log.Printf("[DEBUG] 冷却账号 %s (ID:%d) - CoolingUntil: %s (UTC), 现在: %s (UTC)",
69
+ acc.Email, acc.ID,
70
+ acc.CoolingUntil.Format("2006-01-02 15:04:05"),
71
+ time.Now().UTC().Format("2006-01-02 15:04:05"))
72
+ }
73
+ }
74
+ }
75
+
76
+ // Calculate Stats
77
+ var stats struct {
78
+ TotalAccounts int64 `json:"total_accounts"`
79
+ NormalAccounts int64 `json:"normal_accounts"` // 原 active_accounts
80
+ BannedAccounts int64 `json:"banned_accounts"`
81
+ ErrorAccounts int64 `json:"error_accounts"`
82
+ CoolingAccounts int64 `json:"cooling_accounts"`
83
+ DisabledAccounts int64 `json:"disabled_accounts"`
84
+ TodayUsage float64 `json:"today_usage"`
85
+ TotalUsage float64 `json:"total_usage"`
86
+ }
87
+
88
+ db := database.GetDB()
89
+
90
+ db.Model(&model.Account{}).Count(&stats.TotalAccounts)
91
+ db.Model(&model.Account{}).Where("status = ?", "normal").Count(&stats.NormalAccounts)
92
+ db.Model(&model.Account{}).Where("status = ?", "banned").Count(&stats.BannedAccounts)
93
+ db.Model(&model.Account{}).Where("status = ?", "error").Count(&stats.ErrorAccounts)
94
+ db.Model(&model.Account{}).Where("status = ?", "cooling").Count(&stats.CoolingAccounts)
95
+ db.Model(&model.Account{}).Where("status = ?", "disabled").Count(&stats.DisabledAccounts)
96
+
97
+ db.Model(&model.Account{}).Select("COALESCE(SUM(daily_used), 0)").Scan(&stats.TodayUsage)
98
+ db.Model(&model.Account{}).Select("COALESCE(SUM(total_used), 0)").Scan(&stats.TotalUsage)
99
+
100
+ // 兼容前端旧字段
101
+ statsMap := map[string]interface{}{
102
+ "total_accounts": stats.TotalAccounts,
103
+ "active_accounts": stats.NormalAccounts,
104
+ "banned_accounts": stats.BannedAccounts,
105
+ "error_accounts": stats.ErrorAccounts,
106
+ "cooling_accounts": stats.CoolingAccounts,
107
+ "disabled_accounts": stats.DisabledAccounts,
108
+ "today_usage": stats.TodayUsage,
109
+ "total_usage": stats.TotalUsage,
110
+ }
111
+
112
+ c.JSON(http.StatusOK, gin.H{
113
+ "items": accounts,
114
+ "total": total,
115
+ "page": page,
116
+ "size": size,
117
+ "stats": statsMap,
118
+ })
119
+ }
120
+
121
+ type BatchCategoryRequest struct {
122
+ IDs []uint `json:"ids"`
123
+ Category string `json:"category"` // 前端可能还传 category
124
+ Status string `json:"status"`
125
+ }
126
+
127
+ func (h *AccountHandler) BatchUpdateCategory(c *gin.Context) {
128
+ var req BatchCategoryRequest
129
+ if err := c.ShouldBindJSON(&req); err != nil {
130
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
131
+ return
132
+ }
133
+
134
+ if len(req.IDs) == 0 {
135
+ c.JSON(http.StatusBadRequest, gin.H{"error": "no ids provided"})
136
+ return
137
+ }
138
+
139
+ status := req.Status
140
+ if status == "" {
141
+ status = req.Category
142
+ }
143
+
144
+ updates := map[string]interface{}{
145
+ "status": status,
146
+ // 兼容旧字段
147
+ "category": status,
148
+ }
149
+
150
+ switch status {
151
+ case "normal":
152
+ updates["is_active"] = true
153
+ updates["is_cooling"] = false
154
+ case "cooling":
155
+ updates["is_active"] = true // cooling 也是 active 的一种? 不,cooling 不参与轮询
156
+ updates["is_cooling"] = true
157
+ case "disabled":
158
+ updates["is_active"] = false
159
+ updates["is_cooling"] = false
160
+ default: // banned, error
161
+ updates["is_active"] = false
162
+ updates["is_cooling"] = false
163
+ }
164
+
165
+ if err := database.GetDB().Model(&model.Account{}).Where("id IN ?", req.IDs).Updates(updates).Error; err != nil {
166
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
167
+ return
168
+ }
169
+
170
+ // 触发 refresh? 为了性能这里不触发,等待自动刷新
171
+ c.JSON(http.StatusOK, gin.H{"message": "updated", "count": len(req.IDs)})
172
+ }
173
+
174
+ type MoveAllRequest struct {
175
+ FromStatus string `json:"from_status"`
176
+ ToStatus string `json:"to_status"`
177
+ }
178
+
179
+ // BatchMoveAll 一键移动某个分类的所有账号到另一个分类
180
+ func (h *AccountHandler) BatchMoveAll(c *gin.Context) {
181
+ var req MoveAllRequest
182
+ if err := c.ShouldBindJSON(&req); err != nil {
183
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
184
+ return
185
+ }
186
+
187
+ if req.FromStatus == "" || req.ToStatus == "" {
188
+ c.JSON(http.StatusBadRequest, gin.H{"error": "from_status and to_status are required"})
189
+ return
190
+ }
191
+
192
+ if req.FromStatus == req.ToStatus {
193
+ c.JSON(http.StatusBadRequest, gin.H{"error": "from_status and to_status cannot be the same"})
194
+ return
195
+ }
196
+
197
+ updates := map[string]interface{}{
198
+ "status": req.ToStatus,
199
+ // 兼容旧字段
200
+ "category": req.ToStatus,
201
+ }
202
+
203
+ // 根据目标状态设置相应的标志
204
+ switch req.ToStatus {
205
+ case "normal":
206
+ updates["is_active"] = true
207
+ updates["is_cooling"] = false
208
+ updates["error_count"] = 0
209
+ updates["ban_reason"] = ""
210
+ case "cooling":
211
+ updates["is_active"] = false
212
+ updates["is_cooling"] = true
213
+ updates["ban_reason"] = ""
214
+ case "disabled":
215
+ updates["is_active"] = false
216
+ updates["is_cooling"] = false
217
+ updates["ban_reason"] = ""
218
+ case "banned":
219
+ updates["is_active"] = false
220
+ updates["is_cooling"] = false
221
+ case "error":
222
+ updates["is_active"] = false
223
+ updates["is_cooling"] = false
224
+ default:
225
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid to_status"})
226
+ return
227
+ }
228
+
229
+ // 执行批量更新
230
+ result := database.GetDB().Model(&model.Account{}).Where("status = ?", req.FromStatus).Updates(updates)
231
+ if result.Error != nil {
232
+ c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
233
+ return
234
+ }
235
+
236
+ log.Printf("[批量移动] 从 %s 移动到 %s,影响 %d 个账号", req.FromStatus, req.ToStatus, result.RowsAffected)
237
+
238
+ c.JSON(http.StatusOK, gin.H{
239
+ "message": "moved successfully",
240
+ "moved_count": result.RowsAffected,
241
+ "from_status": req.FromStatus,
242
+ "to_status": req.ToStatus,
243
+ })
244
+ }
245
+
246
+ type BatchRefreshTokenRequest struct {
247
+ IDs []uint `json:"ids"` // 选中的账号IDs,如果为空则刷新所有账号
248
+ All bool `json:"all"` // 是否刷新所有账号
249
+ }
250
+
251
+ type BatchDeleteRequest struct {
252
+ IDs []uint `json:"ids"` // 选中的账号IDs
253
+ DeleteAll bool `json:"delete_all"` // 是否删除分类中的所有账号
254
+ Status string `json:"status"` // 要删除的分类状态
255
+ }
256
+
257
+ // BatchRefreshToken 批量刷新账号token
258
+ func (h *AccountHandler) BatchRefreshToken(c *gin.Context) {
259
+ var req BatchRefreshTokenRequest
260
+ if err := c.ShouldBindJSON(&req); err != nil {
261
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
262
+ return
263
+ }
264
+
265
+ // 设置流式响应头
266
+ c.Header("Content-Type", "text/event-stream")
267
+ c.Header("Cache-Control", "no-cache")
268
+ c.Header("Connection", "keep-alive")
269
+ c.Header("X-Accel-Buffering", "no")
270
+
271
+ flusher, ok := c.Writer.(http.Flusher)
272
+ if !ok {
273
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "流式传输不支持"})
274
+ return
275
+ }
276
+
277
+ var accounts []model.Account
278
+ var err error
279
+
280
+ // 根据请求类型获取要刷新的账号
281
+ if req.All {
282
+ // 刷新所有状态为normal且有refresh_token的账号
283
+ err = database.GetDB().Where("status = ? AND (client_id != '' AND client_secret != '')", "normal").Find(&accounts).Error
284
+ if err != nil {
285
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "获取账号列表失败: " + err.Error()})
286
+ return
287
+ }
288
+ log.Printf("[批量刷新Token] 准备刷新所有正常账号,共 %d 个", len(accounts))
289
+ } else if len(req.IDs) > 0 {
290
+ // 刷新选中的账号
291
+ err = database.GetDB().Where("id IN ? AND (client_id != '' AND client_secret != '')", req.IDs).Find(&accounts).Error
292
+ if err != nil {
293
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "获取选中账号失败: " + err.Error()})
294
+ return
295
+ }
296
+ log.Printf("[批量刷新Token] 准备刷新选中账号,共 %d 个", len(accounts))
297
+ } else {
298
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请选择要刷新的账号或选择刷新所有账号"})
299
+ return
300
+ }
301
+
302
+ if len(accounts) == 0 {
303
+ c.JSON(http.StatusBadRequest, gin.H{"error": "没有找到可刷新的账号"})
304
+ return
305
+ }
306
+
307
+ // 发送开始消息
308
+ fmt.Fprintf(c.Writer, "data: {\"type\":\"start\",\"total\":%d}\n\n", len(accounts))
309
+ flusher.Flush()
310
+
311
+ successCount := 0
312
+ failCount := 0
313
+
314
+ // 逐个刷新token
315
+ for i, account := range accounts {
316
+ log.Printf("[批量刷新Token] 开始刷新第 %d/%d 个账号: %s (ID:%d)", i+1, len(accounts), account.ClientID, account.ID)
317
+
318
+ // 使用OAuth client credentials刷新token
319
+ if err := service.RefreshAccountToken(&account); err != nil {
320
+ failCount++
321
+ errMsg := fmt.Sprintf("刷新失败: %v", err)
322
+
323
+ // 检查是否是账号锁定错误
324
+ if lockoutErr, ok := err.(*service.AccountLockoutError); ok {
325
+ errMsg = fmt.Sprintf("账号被锁定已自动标记为封禁: %s", lockoutErr.Body)
326
+ log.Printf("[批量刷新Token] 第 %d/%d 个账号被锁定: %s - %s", i+1, len(accounts), account.ClientID, lockoutErr.Body)
327
+ } else {
328
+ log.Printf("[批量刷新Token] 第 %d/%d 个账号刷新失败: %s - %v", i+1, len(accounts), account.ClientID, err)
329
+ }
330
+
331
+ fmt.Fprintf(c.Writer, "data: {\"type\":\"error\",\"index\":%d,\"account_id\":\"%s\",\"message\":\"%s\"}\n\n", i+1, account.ClientID, errMsg)
332
+ flusher.Flush()
333
+ } else {
334
+ successCount++
335
+ log.Printf("[批量刷新Token] 第 %d/%d 个账号刷新成功: %s (ID:%d)", i+1, len(accounts), account.ClientID, account.ID)
336
+ fmt.Fprintf(c.Writer, "data: {\"type\":\"success\",\"index\":%d,\"account_id\":\"%s\",\"email\":\"%s\"}\n\n", i+1, account.ClientID, account.Email)
337
+ flusher.Flush()
338
+ }
339
+
340
+ // 添加延迟避免请求过快
341
+ if i < len(accounts)-1 {
342
+ time.Sleep(200 * time.Millisecond)
343
+ }
344
+ }
345
+
346
+ // 发送完成消息
347
+ log.Printf("[批量刷新Token] 完成: 成功 %d 个, 失败 %d 个", successCount, failCount)
348
+ fmt.Fprintf(c.Writer, "data: {\"type\":\"complete\",\"success\":%d,\"fail\":%d}\n\n", successCount, failCount)
349
+ flusher.Flush()
350
+ }
351
+
352
+ func (h *AccountHandler) Create(c *gin.Context) {
353
+ var req model.AccountRequest
354
+ if err := c.ShouldBindJSON(&req); err != nil {
355
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
356
+ return
357
+ }
358
+
359
+ // 生成模式 - 固定生成1个账号
360
+ if req.GenerateMode {
361
+ // 检查是否提供了 refresh_token 或 access_token
362
+ if req.RefreshToken == "" && req.Token == "" {
363
+ c.JSON(http.StatusBadRequest, gin.H{"error": "生成模式需要提供 access_token 或 RefreshToken"})
364
+ return
365
+ }
366
+
367
+ // 优先使用 access_token,如果同时提供了两个字段
368
+ var masterToken string
369
+
370
+ if req.Token != "" {
371
+ // 直接使用提供的 access_token
372
+ masterToken = req.Token
373
+ log.Printf("[生成凭证] 使用提供的 access_token")
374
+ } else {
375
+ // 使用 refresh_token 获取 access_token
376
+ tokenResp, err := service.RefreshAccessToken(req.RefreshToken, req.Proxy)
377
+ if err != nil {
378
+ c.JSON(http.StatusBadRequest, gin.H{"error": "RefreshToken 无效: " + err.Error()})
379
+ return
380
+ }
381
+ masterToken = tokenResp.AccessToken
382
+ log.Printf("[生成凭证] 通过 RefreshToken 获取了 access_token")
383
+ }
384
+
385
+ log.Printf("[生成凭证] 开始生成账号凭证")
386
+
387
+ // 生成凭证
388
+ cred, err := service.GenerateCredential(masterToken)
389
+ if err != nil {
390
+ c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("生成失败: %v", err)})
391
+ return
392
+ }
393
+
394
+ log.Printf("[生成凭证] 凭证生成成功: ClientID=%s", cred.ClientID)
395
+
396
+ // 创建账号
397
+ account := model.Account{
398
+ ClientID: cred.ClientID,
399
+ ClientSecret: cred.Secret,
400
+ Proxy: req.Proxy,
401
+ IsActive: true,
402
+ Status: "normal",
403
+ }
404
+
405
+ // 使用生成的client_id和client_secret获取token
406
+ // 使用OAuth client credentials方式刷新token,使用 https://fe.zencoder.ai/oauth/token
407
+ if _, err := service.RefreshToken(&account); err != nil {
408
+ c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("认证失败: %v", err)})
409
+ return
410
+ }
411
+
412
+ // 解析 Token 获取详细信息
413
+ if payload, err := service.ParseJWT(account.AccessToken); err == nil {
414
+ account.Email = payload.Email
415
+ account.SubscriptionStartDate = service.GetSubscriptionDate(payload)
416
+
417
+ if payload.Expiration > 0 {
418
+ account.TokenExpiry = time.Unix(payload.Expiration, 0)
419
+ }
420
+
421
+ plan := payload.CustomClaims.Plan
422
+ if plan != "" {
423
+ plan = strings.ToUpper(plan[:1]) + plan[1:]
424
+ }
425
+ if plan != "" {
426
+ account.PlanType = model.PlanType(plan)
427
+ }
428
+ }
429
+ if account.PlanType == "" {
430
+ account.PlanType = model.PlanFree
431
+ }
432
+
433
+ // 检查是否已存在
434
+ var existing model.Account
435
+ var count int64
436
+ database.GetDB().Model(&model.Account{}).Where("client_id = ?", account.ClientID).Count(&count)
437
+ if count > 0 {
438
+ // 获取现有账号
439
+ database.GetDB().Where("client_id = ?", account.ClientID).First(&existing)
440
+ // 更新现有账号
441
+ existing.AccessToken = account.AccessToken
442
+ existing.TokenExpiry = account.TokenExpiry
443
+ existing.PlanType = account.PlanType
444
+ existing.Email = account.Email
445
+ existing.SubscriptionStartDate = account.SubscriptionStartDate
446
+ existing.IsActive = true
447
+ existing.Status = "normal" // 重新激活
448
+ existing.ClientSecret = account.ClientSecret
449
+ if account.Proxy != "" {
450
+ existing.Proxy = account.Proxy
451
+ }
452
+
453
+ if err := database.GetDB().Save(&existing).Error; err != nil {
454
+ c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新失败: %v", err)})
455
+ return
456
+ }
457
+
458
+ log.Printf("[添加账号] 账号更新成功: ClientID=%s, Email=%s, Plan=%s", existing.ClientID, existing.Email, existing.PlanType)
459
+ c.JSON(http.StatusOK, existing)
460
+ } else {
461
+ // 创建新账号
462
+ if err := database.GetDB().Create(&account).Error; err != nil {
463
+ c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建失败: %v", err)})
464
+ return
465
+ }
466
+
467
+ log.Printf("[添加账号] 新账号创建成功: ClientID=%s, Email=%s, Plan=%s", account.ClientID, account.Email, account.PlanType)
468
+ c.JSON(http.StatusCreated, account)
469
+ }
470
+ return
471
+ }
472
+
473
+ // 原有的单个账号添加逻辑 - 现在使用 refresh_token
474
+ account := model.Account{
475
+ Proxy: req.Proxy,
476
+ IsActive: true,
477
+ Status: "normal",
478
+ }
479
+
480
+ // 优先使用 access_token,如果同时提供了两个字段则不使用 refresh_token
481
+ if req.Token != "" && req.RefreshToken != "" {
482
+ log.Printf("[凭证模式] 同时提供了 access_token 和 RefreshToken,优先使用 access_token")
483
+ }
484
+
485
+ if req.Token != "" {
486
+ // JWT Parsing Logic
487
+ payload, err := service.ParseJWT(req.Token)
488
+ if err != nil {
489
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的Token: " + err.Error()})
490
+ return
491
+ }
492
+
493
+ account.AccessToken = req.Token
494
+ // 优先使用ClientID字段,如果没有则使用Subject
495
+ if payload.ClientID != "" {
496
+ account.ClientID = payload.ClientID
497
+ } else {
498
+ account.ClientID = payload.Subject
499
+ }
500
+
501
+ account.Email = payload.Email
502
+ account.SubscriptionStartDate = service.GetSubscriptionDate(payload)
503
+
504
+ if payload.Expiration > 0 {
505
+ account.TokenExpiry = time.Unix(payload.Expiration, 0)
506
+ } else {
507
+ account.TokenExpiry = time.Now().Add(24 * time.Hour) // 默认24小时
508
+ }
509
+
510
+ // Map PlanType
511
+ plan := payload.CustomClaims.Plan
512
+
513
+ // Simple normalization
514
+ if plan != "" {
515
+ plan = strings.ToUpper(plan[:1]) + plan[1:]
516
+ }
517
+ account.PlanType = model.PlanType(plan)
518
+ if account.PlanType == "" {
519
+ account.PlanType = model.PlanFree
520
+ }
521
+
522
+ // Placeholder for secret since it's required by DB but not in JWT
523
+ account.ClientSecret = "jwt-login"
524
+ } else if req.RefreshToken != "" {
525
+ // 只提供了 refresh_token,使用它来获取 access_token
526
+ tokenResp, err := service.RefreshAccessToken(req.RefreshToken, req.Proxy)
527
+ if err != nil {
528
+ c.JSON(http.StatusBadRequest, gin.H{"error": "RefreshToken 无效: " + err.Error()})
529
+ return
530
+ }
531
+
532
+ account.AccessToken = tokenResp.AccessToken
533
+ account.RefreshToken = tokenResp.RefreshToken
534
+ account.TokenExpiry = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
535
+
536
+ // 解析 Token 获取详细信息
537
+ if payload, err := service.ParseJWT(tokenResp.AccessToken); err == nil {
538
+ // 设置 Email
539
+ if payload.Email != "" {
540
+ account.Email = payload.Email
541
+ } else if tokenResp.Email != "" {
542
+ account.Email = tokenResp.Email
543
+ }
544
+
545
+ // 设置 ClientID - 优先使用 Email 作为唯一标识符
546
+ if payload.Email != "" {
547
+ account.ClientID = payload.Email
548
+ } else if payload.Subject != "" {
549
+ account.ClientID = payload.Subject
550
+ } else if payload.ClientID != "" {
551
+ account.ClientID = payload.ClientID
552
+ }
553
+
554
+ account.SubscriptionStartDate = service.GetSubscriptionDate(payload)
555
+
556
+ // Map PlanType
557
+ plan := payload.CustomClaims.Plan
558
+ if plan != "" {
559
+ plan = strings.ToUpper(plan[:1]) + plan[1:]
560
+ }
561
+ account.PlanType = model.PlanType(plan)
562
+ if account.PlanType == "" {
563
+ account.PlanType = model.PlanFree
564
+ }
565
+
566
+ log.Printf("[凭证模式-RefreshToken] 解析JWT成功: ClientID=%s, Email=%s, Plan=%s",
567
+ account.ClientID, account.Email, account.PlanType)
568
+ } else {
569
+ log.Printf("[凭证模式-RefreshToken] 解析JWT失败: %v", err)
570
+ // 如果JWT解析失败,使用 tokenResp 中的信息
571
+ if tokenResp.UserID != "" {
572
+ account.ClientID = tokenResp.UserID
573
+ account.Email = tokenResp.UserID
574
+ }
575
+ }
576
+
577
+ // 生成一个占位 ClientSecret
578
+ account.ClientSecret = "refresh-token-login"
579
+
580
+ // 确保 ClientID 不为空
581
+ if account.ClientID == "" {
582
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无法获取用户信息,请检查RefreshToken是否有效"})
583
+ return
584
+ }
585
+
586
+ } else {
587
+ // Old Logic
588
+ if req.PlanType == "" {
589
+ req.PlanType = model.PlanFree
590
+ }
591
+ account.ClientID = req.ClientID
592
+ account.ClientSecret = req.ClientSecret
593
+ account.Email = req.Email
594
+ account.PlanType = req.PlanType
595
+
596
+ // 验证Token是否能正确获取
597
+ if _, err := service.RefreshToken(&account); err != nil {
598
+ c.JSON(http.StatusBadRequest, gin.H{"error": "认证失败: " + err.Error()})
599
+ return
600
+ }
601
+
602
+ // 解析Token获取详细信息
603
+ if payload, err := service.ParseJWT(account.AccessToken); err == nil {
604
+ if account.Email == "" {
605
+ account.Email = payload.Email
606
+ }
607
+ account.SubscriptionStartDate = service.GetSubscriptionDate(payload)
608
+
609
+ if payload.Expiration > 0 {
610
+ account.TokenExpiry = time.Unix(payload.Expiration, 0)
611
+ }
612
+
613
+ plan := payload.CustomClaims.Plan
614
+ if plan != "" {
615
+ plan = strings.ToUpper(plan[:1]) + plan[1:]
616
+ }
617
+ if plan != "" {
618
+ account.PlanType = model.PlanType(plan)
619
+ }
620
+ }
621
+ if account.PlanType == "" {
622
+ account.PlanType = model.PlanFree
623
+ }
624
+ }
625
+
626
+ // Check if account exists - 使用 Count 避免 record not found 警告
627
+ var existing model.Account
628
+ var count int64
629
+ database.GetDB().Model(&model.Account{}).Where("client_id = ?", account.ClientID).Count(&count)
630
+ if count > 0 {
631
+ // 获取现有账号
632
+ database.GetDB().Where("client_id = ?", account.ClientID).First(&existing)
633
+ // Update existing
634
+ existing.AccessToken = account.AccessToken
635
+ existing.RefreshToken = account.RefreshToken // 更新 refresh_token
636
+ existing.TokenExpiry = account.TokenExpiry
637
+ existing.PlanType = account.PlanType
638
+ existing.Email = account.Email
639
+ existing.SubscriptionStartDate = account.SubscriptionStartDate
640
+ existing.IsActive = true
641
+ existing.Status = "normal"
642
+ if account.Proxy != "" {
643
+ existing.Proxy = account.Proxy
644
+ }
645
+ // If secret was provided manually, update it. If placeholder, keep existing.
646
+ if account.ClientSecret != "jwt-login" && account.ClientSecret != "refresh-token-login" {
647
+ existing.ClientSecret = account.ClientSecret
648
+ }
649
+
650
+ if err := database.GetDB().Save(&existing).Error; err != nil {
651
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
652
+ return
653
+ }
654
+ c.JSON(http.StatusOK, existing)
655
+ return
656
+ }
657
+
658
+ if err := database.GetDB().Create(&account).Error; err != nil {
659
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
660
+ return
661
+ }
662
+
663
+ c.JSON(http.StatusCreated, account)
664
+ }
665
+
666
+ func (h *AccountHandler) Update(c *gin.Context) {
667
+ id := c.Param("id")
668
+ var account model.Account
669
+ if err := database.GetDB().First(&account, id).Error; err != nil {
670
+ c.JSON(http.StatusNotFound, gin.H{"error": "account not found"})
671
+ return
672
+ }
673
+
674
+ var req model.AccountRequest
675
+ if err := c.ShouldBindJSON(&req); err != nil {
676
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
677
+ return
678
+ }
679
+
680
+ account.Email = req.Email
681
+ account.PlanType = req.PlanType
682
+ account.Proxy = req.Proxy
683
+
684
+ if err := database.GetDB().Save(&account).Error; err != nil {
685
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
686
+ return
687
+ }
688
+
689
+ c.JSON(http.StatusOK, account)
690
+ }
691
+
692
+ // BatchDelete 批量删除账号
693
+ func (h *AccountHandler) BatchDelete(c *gin.Context) {
694
+ var req BatchDeleteRequest
695
+ if err := c.ShouldBindJSON(&req); err != nil {
696
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
697
+ return
698
+ }
699
+
700
+ var deletedCount int64
701
+
702
+ if req.DeleteAll {
703
+ // 删除指定分类的所有账号
704
+ if req.Status == "" {
705
+ c.JSON(http.StatusBadRequest, gin.H{"error": "delete_all模式需要指定status"})
706
+ return
707
+ }
708
+
709
+ // 执行删除操作
710
+ result := database.GetDB().Where("status = ?", req.Status).Delete(&model.Account{})
711
+ if result.Error != nil {
712
+ c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
713
+ return
714
+ }
715
+
716
+ deletedCount = result.RowsAffected
717
+ log.Printf("[批量删除] 删除分类 %s 的所有账号,共删除 %d 个", req.Status, deletedCount)
718
+
719
+ } else {
720
+ // 删除选中的账号
721
+ if len(req.IDs) == 0 {
722
+ c.JSON(http.StatusBadRequest, gin.H{"error": "未选择要删除的账号"})
723
+ return
724
+ }
725
+
726
+ // 执行删除操作
727
+ result := database.GetDB().Where("id IN ?", req.IDs).Delete(&model.Account{})
728
+ if result.Error != nil {
729
+ c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
730
+ return
731
+ }
732
+
733
+ deletedCount = result.RowsAffected
734
+ log.Printf("[批量删除] 删除选中的 %d 个账号,实际删除 %d 个", len(req.IDs), deletedCount)
735
+ }
736
+
737
+ c.JSON(http.StatusOK, gin.H{
738
+ "message": "批量删除成功",
739
+ "deleted_count": deletedCount,
740
+ })
741
+ }
742
+
743
+ func (h *AccountHandler) Delete(c *gin.Context) {
744
+ id := c.Param("id")
745
+ if err := database.GetDB().Delete(&model.Account{}, id).Error; err != nil {
746
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
747
+ return
748
+ }
749
+ c.JSON(http.StatusOK, gin.H{"message": "deleted"})
750
+ }
751
+
752
+ func (h *AccountHandler) Toggle(c *gin.Context) {
753
+ id := c.Param("id")
754
+ var account model.Account
755
+ if err := database.GetDB().First(&account, id).Error; err != nil {
756
+ c.JSON(http.StatusNotFound, gin.H{"error": "account not found"})
757
+ return
758
+ }
759
+
760
+ // 切换 Disabled / Normal
761
+ if account.Status == "disabled" || !account.IsActive {
762
+ account.Status = "normal"
763
+ account.IsActive = true
764
+ account.IsCooling = false
765
+ account.ErrorCount = 0
766
+ } else {
767
+ account.Status = "disabled"
768
+ account.IsActive = false
769
+ }
770
+
771
+ database.GetDB().Save(&account)
772
+
773
+ c.JSON(http.StatusOK, account)
774
+ }
internal/handler/anthropic.go ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handler
2
+
3
+ import (
4
+ "context"
5
+ "crypto/rand"
6
+ "encoding/hex"
7
+ "errors"
8
+ "fmt"
9
+ "io"
10
+ "net/http"
11
+
12
+ "zencoder-2api/internal/service"
13
+
14
+ "github.com/gin-gonic/gin"
15
+ )
16
+
17
+ type AnthropicHandler struct {
18
+ svc *service.AnthropicService
19
+ }
20
+
21
+ func NewAnthropicHandler() *AnthropicHandler {
22
+ return &AnthropicHandler{svc: service.NewAnthropicService()}
23
+ }
24
+
25
+ // generateTraceID 生成一个随机的 trace ID
26
+ func generateAnthropicTraceID() string {
27
+ b := make([]byte, 16)
28
+ rand.Read(b)
29
+ return hex.EncodeToString(b)
30
+ }
31
+
32
+ // Messages 处理 POST /v1/messages
33
+ func (h *AnthropicHandler) Messages(c *gin.Context) {
34
+ body, err := io.ReadAll(c.Request.Body)
35
+ if err != nil {
36
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
37
+ return
38
+ }
39
+
40
+ // 传递原始请求头给service层,用于错误日志记录
41
+ ctx := context.WithValue(c.Request.Context(), "originalHeaders", c.Request.Header)
42
+
43
+ if err := h.svc.MessagesProxy(ctx, c.Writer, body); err != nil {
44
+ h.handleError(c, err)
45
+ }
46
+ }
47
+
48
+ // handleError 统一处理错误,特别是没有可用账号的错误
49
+ func (h *AnthropicHandler) handleError(c *gin.Context, err error) {
50
+ if errors.Is(err, service.ErrNoAvailableAccount) || errors.Is(err, service.ErrNoPermission) {
51
+ traceID := generateAnthropicTraceID()
52
+ errMsg := fmt.Sprintf("没有可用token(traceid: %s)", traceID)
53
+ c.JSON(http.StatusServiceUnavailable, gin.H{"error": errMsg})
54
+ return
55
+ }
56
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
57
+ }
internal/handler/external.go ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handler
2
+
3
+ import (
4
+ "fmt"
5
+ "log"
6
+ "net/http"
7
+ "strings"
8
+ "time"
9
+
10
+ "github.com/gin-gonic/gin"
11
+ "zencoder-2api/internal/database"
12
+ "zencoder-2api/internal/model"
13
+ "zencoder-2api/internal/service"
14
+ )
15
+
16
+ type ExternalHandler struct{}
17
+
18
+ func NewExternalHandler() *ExternalHandler {
19
+ return &ExternalHandler{}
20
+ }
21
+
22
+ // ExternalTokenRequest 外部API提交token请求结构
23
+ type ExternalTokenRequest struct {
24
+ AccessToken string `json:"access_token"` // OAuth获取的access_token
25
+ RefreshToken string `json:"refresh_token"` // OAuth获取的refresh_token
26
+ Proxy string `json:"proxy"` // 可选的代理设置
27
+ }
28
+
29
+ // ExternalTokenResponse 外部API响应结构
30
+ type ExternalTokenResponse struct {
31
+ Success bool `json:"success"`
32
+ Message string `json:"message"`
33
+ Account *model.Account `json:"account,omitempty"`
34
+ Error string `json:"error,omitempty"`
35
+ }
36
+
37
+ // SubmitTokens 外部API接口:接收OAuth token信息并生成账号记录
38
+ func (h *ExternalHandler) SubmitTokens(c *gin.Context) {
39
+ var req ExternalTokenRequest
40
+ if err := c.ShouldBindJSON(&req); err != nil {
41
+ c.JSON(http.StatusBadRequest, ExternalTokenResponse{
42
+ Success: false,
43
+ Error: "请求格式错误: " + err.Error(),
44
+ })
45
+ return
46
+ }
47
+
48
+ // 验证必要字段
49
+ if req.AccessToken == "" && req.RefreshToken == "" {
50
+ c.JSON(http.StatusBadRequest, ExternalTokenResponse{
51
+ Success: false,
52
+ Error: "必须提供 access_token 或 refresh_token",
53
+ })
54
+ return
55
+ }
56
+
57
+ log.Printf("[外部API] 收到token提交请求,access_token长度: %d, refresh_token长度: %d",
58
+ len(req.AccessToken), len(req.RefreshToken))
59
+
60
+ // 优先使用 access_token,如果同时提供了两个字段
61
+ var masterToken string
62
+
63
+ if req.AccessToken != "" {
64
+ // 直接使用提供的 access_token
65
+ masterToken = req.AccessToken
66
+ log.Printf("[外部API] 使用提供的 access_token")
67
+ } else {
68
+ // 使用 refresh_token 获取 access_token
69
+ tokenResp, err := service.RefreshAccessToken(req.RefreshToken, req.Proxy)
70
+ if err != nil {
71
+ c.JSON(http.StatusBadRequest, ExternalTokenResponse{
72
+ Success: false,
73
+ Error: "RefreshToken 无效: " + err.Error(),
74
+ })
75
+ return
76
+ }
77
+ masterToken = tokenResp.AccessToken
78
+ log.Printf("[外部API] 通过 RefreshToken 获取了 access_token")
79
+ }
80
+
81
+ log.Printf("[外部API] 开始生成账号凭证")
82
+
83
+ // 生成凭证
84
+ cred, err := service.GenerateCredential(masterToken)
85
+ if err != nil {
86
+ c.JSON(http.StatusInternalServerError, ExternalTokenResponse{
87
+ Success: false,
88
+ Error: fmt.Sprintf("生成失败: %v", err),
89
+ })
90
+ return
91
+ }
92
+
93
+ log.Printf("[外部API] 凭证生成成功: ClientID=%s", cred.ClientID)
94
+
95
+ // 创建账号
96
+ account := model.Account{
97
+ ClientID: cred.ClientID,
98
+ ClientSecret: cred.Secret,
99
+ Proxy: req.Proxy,
100
+ IsActive: true,
101
+ Status: "normal",
102
+ }
103
+
104
+ // 使用生成的client_id和client_secret获取token,带重试机制
105
+ // 使用OAuth client credentials方式刷新token,使用 https://fe.zencoder.ai/oauth/token
106
+ maxRetries := 3
107
+ retryDelay := 2 * time.Second
108
+ var lastErr error
109
+
110
+ for attempt := 1; attempt <= maxRetries; attempt++ {
111
+ log.Printf("[外部API] 尝试获取token,第 %d/%d 次", attempt, maxRetries)
112
+
113
+ if _, err := service.RefreshToken(&account); err != nil {
114
+ lastErr = err
115
+ log.Printf("[外部API] 第 %d 次获取token失败: %v", attempt, err)
116
+
117
+ if attempt < maxRetries {
118
+ log.Printf("[外部API] 等待 %v 后重试", retryDelay)
119
+ time.Sleep(retryDelay)
120
+ continue
121
+ }
122
+ } else {
123
+ log.Printf("[外部API] 第 %d 次获取token成功", attempt)
124
+ lastErr = nil
125
+ break
126
+ }
127
+ }
128
+
129
+ if lastErr != nil {
130
+ c.JSON(http.StatusBadRequest, ExternalTokenResponse{
131
+ Success: false,
132
+ Error: fmt.Sprintf("认证失败(重试 %d 次后): %v", maxRetries, lastErr),
133
+ })
134
+ return
135
+ }
136
+
137
+ // 解析 Token 获取详细信息
138
+ if payload, err := service.ParseJWT(account.AccessToken); err == nil {
139
+ account.Email = payload.Email
140
+ account.SubscriptionStartDate = service.GetSubscriptionDate(payload)
141
+
142
+ if payload.Expiration > 0 {
143
+ account.TokenExpiry = time.Unix(payload.Expiration, 0)
144
+ }
145
+
146
+ plan := payload.CustomClaims.Plan
147
+ if plan != "" {
148
+ plan = strings.ToUpper(plan[:1]) + plan[1:]
149
+ }
150
+ if plan != "" {
151
+ account.PlanType = model.PlanType(plan)
152
+ }
153
+ }
154
+ if account.PlanType == "" {
155
+ account.PlanType = model.PlanFree
156
+ }
157
+
158
+ // 检查是否已存在
159
+ var existing model.Account
160
+ var count int64
161
+ database.GetDB().Model(&model.Account{}).Where("client_id = ?", account.ClientID).Count(&count)
162
+ if count > 0 {
163
+ // 获取现有账号
164
+ database.GetDB().Where("client_id = ?", account.ClientID).First(&existing)
165
+ // 更新现有账号
166
+ existing.AccessToken = account.AccessToken
167
+ existing.TokenExpiry = account.TokenExpiry
168
+ existing.PlanType = account.PlanType
169
+ existing.Email = account.Email
170
+ existing.SubscriptionStartDate = account.SubscriptionStartDate
171
+ existing.IsActive = true
172
+ existing.Status = "normal" // 重新激活
173
+ existing.ClientSecret = account.ClientSecret
174
+ if account.Proxy != "" {
175
+ existing.Proxy = account.Proxy
176
+ }
177
+
178
+ if err := database.GetDB().Save(&existing).Error; err != nil {
179
+ c.JSON(http.StatusInternalServerError, ExternalTokenResponse{
180
+ Success: false,
181
+ Error: fmt.Sprintf("更新失败: %v", err),
182
+ })
183
+ return
184
+ }
185
+
186
+ log.Printf("[外部API] 账号更新成功: ClientID=%s, Email=%s, Plan=%s", existing.ClientID, existing.Email, existing.PlanType)
187
+ c.JSON(http.StatusOK, ExternalTokenResponse{
188
+ Success: true,
189
+ Message: "账号更新成功",
190
+ Account: &existing,
191
+ })
192
+ } else {
193
+ // 创建新账号
194
+ if err := database.GetDB().Create(&account).Error; err != nil {
195
+ c.JSON(http.StatusInternalServerError, ExternalTokenResponse{
196
+ Success: false,
197
+ Error: fmt.Sprintf("创建失败: %v", err),
198
+ })
199
+ return
200
+ }
201
+
202
+ log.Printf("[外部API] 新账号创建成功: ClientID=%s, Email=%s, Plan=%s", account.ClientID, account.Email, account.PlanType)
203
+ c.JSON(http.StatusCreated, ExternalTokenResponse{
204
+ Success: true,
205
+ Message: "账号创建成功",
206
+ Account: &account,
207
+ })
208
+ }
209
+ }
internal/handler/gemini.go ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handler
2
+
3
+ import (
4
+ "crypto/rand"
5
+ "encoding/hex"
6
+ "errors"
7
+ "fmt"
8
+ "io"
9
+ "net/http"
10
+ "strings"
11
+
12
+ "github.com/gin-gonic/gin"
13
+ "zencoder-2api/internal/service"
14
+ )
15
+
16
+ type GeminiHandler struct {
17
+ svc *service.GeminiService
18
+ }
19
+
20
+ func NewGeminiHandler() *GeminiHandler {
21
+ return &GeminiHandler{svc: service.NewGeminiService()}
22
+ }
23
+
24
+ // generateTraceID 生成一个随机的 trace ID
25
+ func generateGeminiTraceID() string {
26
+ b := make([]byte, 16)
27
+ rand.Read(b)
28
+ return hex.EncodeToString(b)
29
+ }
30
+
31
+ // HandleRequest 处理 POST /v1beta/models/*path
32
+ // 路径格式: /model:action 例如 /gemini-3-flash-preview:streamGenerateContent
33
+ func (h *GeminiHandler) HandleRequest(c *gin.Context) {
34
+ path := c.Param("path")
35
+ // 去掉开头的斜杠
36
+ path = strings.TrimPrefix(path, "/")
37
+
38
+ // 解析 model:action
39
+ parts := strings.SplitN(path, ":", 2)
40
+ if len(parts) != 2 {
41
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path format"})
42
+ return
43
+ }
44
+
45
+ modelName := parts[0]
46
+ action := parts[1]
47
+
48
+ body, err := io.ReadAll(c.Request.Body)
49
+ if err != nil {
50
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
51
+ return
52
+ }
53
+
54
+ switch action {
55
+ case "generateContent":
56
+ if err := h.svc.GenerateContentProxy(c.Request.Context(), c.Writer, modelName, body); err != nil {
57
+ h.handleError(c, err)
58
+ }
59
+ case "streamGenerateContent":
60
+ if err := h.svc.StreamGenerateContentProxy(c.Request.Context(), c.Writer, modelName, body); err != nil {
61
+ h.handleError(c, err)
62
+ }
63
+ default:
64
+ c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported action: " + action})
65
+ }
66
+ }
67
+
68
+ // handleError 统一处理错误,特别是没有可用账号的错误
69
+ func (h *GeminiHandler) handleError(c *gin.Context, err error) {
70
+ if errors.Is(err, service.ErrNoAvailableAccount) || errors.Is(err, service.ErrNoPermission) {
71
+ traceID := generateGeminiTraceID()
72
+ errMsg := fmt.Sprintf("没有可用token(traceid: %s)", traceID)
73
+ c.JSON(http.StatusServiceUnavailable, gin.H{"error": errMsg})
74
+ return
75
+ }
76
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
77
+ }
internal/handler/grok.go ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handler
2
+
3
+ import (
4
+ "crypto/rand"
5
+ "encoding/hex"
6
+ "errors"
7
+ "fmt"
8
+ "io"
9
+ "net/http"
10
+
11
+ "zencoder-2api/internal/service"
12
+
13
+ "github.com/gin-gonic/gin"
14
+ )
15
+
16
+ type GrokHandler struct {
17
+ svc *service.GrokService
18
+ }
19
+
20
+ func NewGrokHandler() *GrokHandler {
21
+ return &GrokHandler{svc: service.NewGrokService()}
22
+ }
23
+
24
+ // generateTraceID 生成一个随机的 trace ID
25
+ func generateGrokTraceID() string {
26
+ b := make([]byte, 16)
27
+ rand.Read(b)
28
+ return hex.EncodeToString(b)
29
+ }
30
+
31
+ // ChatCompletions 处理 POST /v1/chat/completions (xAI)
32
+ func (h *GrokHandler) ChatCompletions(c *gin.Context) {
33
+ body, err := io.ReadAll(c.Request.Body)
34
+ if err != nil {
35
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
36
+ return
37
+ }
38
+
39
+ if err := h.svc.ChatCompletionsProxy(c.Request.Context(), c.Writer, body); err != nil {
40
+ h.handleError(c, err)
41
+ }
42
+ }
43
+
44
+ // handleError 统一处理错误,特别是没有可用账号的错误
45
+ func (h *GrokHandler) handleError(c *gin.Context, err error) {
46
+ if errors.Is(err, service.ErrNoAvailableAccount) || errors.Is(err, service.ErrNoPermission) {
47
+ traceID := generateGrokTraceID()
48
+ errMsg := fmt.Sprintf("没有可用token(traceid: %s)", traceID)
49
+ c.JSON(http.StatusServiceUnavailable, gin.H{"error": errMsg})
50
+ return
51
+ }
52
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
53
+ }
internal/handler/oauth.go ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handler
2
+
3
+ import (
4
+ "bytes"
5
+ "crypto/rand"
6
+ "crypto/sha256"
7
+ "encoding/base64"
8
+ "encoding/json"
9
+ "fmt"
10
+ "net/http"
11
+ "net/url"
12
+ "sync"
13
+ "time"
14
+
15
+ "github.com/gin-gonic/gin"
16
+ )
17
+
18
+ // PKCESession 存储PKCE会话信息
19
+ type PKCESession struct {
20
+ CodeVerifier string
21
+ CreatedAt time.Time
22
+ }
23
+
24
+ // PKCESessionStore 内存中存储PKCE会话
25
+ type PKCESessionStore struct {
26
+ sync.RWMutex
27
+ sessions map[string]*PKCESession
28
+ }
29
+
30
+ // 全局PKCE会话存储
31
+ var pkceStore = &PKCESessionStore{
32
+ sessions: make(map[string]*PKCESession),
33
+ }
34
+
35
+ // OAuthHandler OAuth相关处理器
36
+ type OAuthHandler struct {
37
+ }
38
+
39
+ // NewOAuthHandler 创建OAuth处理器
40
+ func NewOAuthHandler() *OAuthHandler {
41
+ // 启动清理过期会话的定时器
42
+ go cleanupExpiredSessions()
43
+
44
+ return &OAuthHandler{}
45
+ }
46
+
47
+ // StartOAuthForRT 开始OAuth流程获取RT
48
+ func (h *OAuthHandler) StartOAuthForRT(c *gin.Context) {
49
+ // 生成PKCE参数
50
+ codeVerifier, err := generateCodeVerifier(32)
51
+ if err != nil {
52
+ c.JSON(http.StatusInternalServerError, gin.H{
53
+ "error": "生成PKCE参数失败",
54
+ })
55
+ return
56
+ }
57
+
58
+ // 生成code_challenge
59
+ codeChallenge := generateCodeChallenge(codeVerifier)
60
+
61
+ // 生成会话ID
62
+ sessionID, err := generateSessionID()
63
+ if err != nil {
64
+ c.JSON(http.StatusInternalServerError, gin.H{
65
+ "error": "生成会话ID失败",
66
+ })
67
+ return
68
+ }
69
+
70
+ // 存储会话
71
+ pkceStore.Lock()
72
+ pkceStore.sessions[sessionID] = &PKCESession{
73
+ CodeVerifier: codeVerifier,
74
+ CreatedAt: time.Now(),
75
+ }
76
+ pkceStore.Unlock()
77
+
78
+ // 获取回调URL
79
+ scheme := "http"
80
+ if c.Request.TLS != nil {
81
+ scheme = "https"
82
+ }
83
+ host := c.Request.Host
84
+
85
+ callbackURL := fmt.Sprintf("%s://%s/api/oauth/callback-rt?session=%s",
86
+ scheme, host, sessionID)
87
+
88
+ // 构建state参数
89
+ state := map[string]string{
90
+ "redirectUri": callbackURL,
91
+ "codeChallenge": codeChallenge,
92
+ "sessionId": sessionID,
93
+ }
94
+ stateJSON, _ := json.Marshal(state)
95
+
96
+ // 构建授权URL
97
+ params := url.Values{
98
+ "state": {string(stateJSON)},
99
+ "response_type": {"code"},
100
+ "client_id": {"5948a5c5-4b30-4465-a3f2-2136ea53ea0a"},
101
+ "scope": {"openid profile email"},
102
+ "redirect_uri": {"https://auth.zencoder.ai/extension/auth-success"},
103
+ "code_challenge": {codeChallenge},
104
+ "code_challenge_method": {"S256"},
105
+ }
106
+
107
+ authURL := fmt.Sprintf("https://fe.zencoder.ai/oauth/authorize?%s", params.Encode())
108
+
109
+ // 重定向到授权页面
110
+ c.Redirect(http.StatusFound, authURL)
111
+ }
112
+
113
+ // CallbackOAuthForRT 处理OAuth回调
114
+ func (h *OAuthHandler) CallbackOAuthForRT(c *gin.Context) {
115
+ code := c.Query("code")
116
+ sessionID := c.Query("session")
117
+
118
+ // 验证参数
119
+ if code == "" || sessionID == "" {
120
+ h.renderCallbackPage(c, false, "", "", "缺少必要参数")
121
+ return
122
+ }
123
+
124
+ // 获取会话
125
+ pkceStore.RLock()
126
+ session, exists := pkceStore.sessions[sessionID]
127
+ pkceStore.RUnlock()
128
+
129
+ if !exists {
130
+ h.renderCallbackPage(c, false, "", "", "会话已过期,请重新获取")
131
+ return
132
+ }
133
+
134
+ // 交换token
135
+ tokenResp, err := h.exchangeCodeForToken(code, session.CodeVerifier)
136
+ if err != nil {
137
+ h.renderCallbackPage(c, false, "", "", fmt.Sprintf("获取Token失败: %v", err))
138
+ return
139
+ }
140
+
141
+ // 清理会话
142
+ pkceStore.Lock()
143
+ delete(pkceStore.sessions, sessionID)
144
+ pkceStore.Unlock()
145
+
146
+ // 渲染成功页面,传递access token和refresh token
147
+ h.renderCallbackPage(c, true, tokenResp.AccessToken, tokenResp.RefreshToken, "")
148
+ }
149
+
150
+ // exchangeCodeForToken 用授权码换取token
151
+ func (h *OAuthHandler) exchangeCodeForToken(code, codeVerifier string) (*OAuthTokenResponse, error) {
152
+ tokenURL := "https://auth.zencoder.ai/api/frontegg/oauth/token"
153
+
154
+ payload := map[string]string{
155
+ "code": code,
156
+ "redirect_uri": "https://auth.zencoder.ai/extension/auth-success",
157
+ "code_verifier": codeVerifier,
158
+ "grant_type": "authorization_code",
159
+ }
160
+
161
+ body, _ := json.Marshal(payload)
162
+
163
+ req, err := http.NewRequest("POST", tokenURL, bytes.NewReader(body))
164
+ if err != nil {
165
+ return nil, err
166
+ }
167
+
168
+ // 设置请求头
169
+ req.Header.Set("Content-Type", "application/json")
170
+ req.Header.Set("x-frontegg-sdk", "@frontegg/nextjs@9.2.10")
171
+ req.Header.Set("x-frontegg-framework", "next@15.3.8")
172
+ req.Header.Set("Origin", "https://auth.zencoder.ai")
173
+ req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
174
+
175
+ client := &http.Client{Timeout: 30 * time.Second}
176
+ resp, err := client.Do(req)
177
+ if err != nil {
178
+ return nil, err
179
+ }
180
+ defer resp.Body.Close()
181
+
182
+ if resp.StatusCode != http.StatusOK {
183
+ return nil, fmt.Errorf("token exchange failed with status %d", resp.StatusCode)
184
+ }
185
+
186
+ var tokenResp OAuthTokenResponse
187
+ if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
188
+ return nil, err
189
+ }
190
+
191
+ return &tokenResp, nil
192
+ }
193
+
194
+ // renderCallbackPage 渲染回调页面
195
+ func (h *OAuthHandler) renderCallbackPage(c *gin.Context, success bool, accessToken, refreshToken, errorMsg string) {
196
+ html := `
197
+ <!DOCTYPE html>
198
+ <html lang="zh-CN">
199
+ <head>
200
+ <meta charset="UTF-8">
201
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
202
+ <title>OAuth认证</title>
203
+ <script src="https://cdn.tailwindcss.com"></script>
204
+ </head>
205
+ <body class="bg-gray-50 dark:bg-gray-900 min-h-screen flex items-center justify-center">
206
+ <div class="max-w-md w-full mx-4">
207
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8">
208
+ `
209
+
210
+ if success {
211
+ html += fmt.Sprintf(`
212
+ <div class="text-center">
213
+ <div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
214
+ <svg class="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
215
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
216
+ </svg>
217
+ </div>
218
+ <h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">认证成功!</h2>
219
+ <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">正在返回并填充Token...</p>
220
+ </div>
221
+ <script>
222
+ // 发送消息给父窗口
223
+ if (window.opener) {
224
+ window.opener.postMessage({
225
+ type: 'oauth-rt-complete',
226
+ success: true,
227
+ accessToken: '%s',
228
+ refreshToken: '%s'
229
+ }, window.location.origin);
230
+
231
+ // 2秒后关闭窗口
232
+ setTimeout(() => {
233
+ window.close();
234
+ }, 2000);
235
+ }
236
+ </script>
237
+ `, accessToken, refreshToken)
238
+ } else {
239
+ html += fmt.Sprintf(`
240
+ <div class="text-center">
241
+ <div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
242
+ <svg class="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
243
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
244
+ </svg>
245
+ </div>
246
+ <h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">认证失败</h2>
247
+ <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">%s</p>
248
+ <button onclick="window.close()" class="mt-4 px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors">
249
+ 关闭窗口
250
+ </button>
251
+ </div>
252
+ <script>
253
+ // 发送错误消息给父窗口
254
+ if (window.opener) {
255
+ window.opener.postMessage({
256
+ type: 'oauth-rt-complete',
257
+ success: false,
258
+ error: '%s'
259
+ }, window.location.origin);
260
+ }
261
+ </script>
262
+ `, errorMsg, errorMsg)
263
+ }
264
+
265
+ html += `
266
+ </div>
267
+ </div>
268
+ </body>
269
+ </html>
270
+ `
271
+
272
+ c.Header("Content-Type", "text/html; charset=utf-8")
273
+ c.String(http.StatusOK, html)
274
+ }
275
+
276
+ // OAuthTokenResponse OAuth token响应
277
+ type OAuthTokenResponse struct {
278
+ AccessToken string `json:"access_token"`
279
+ RefreshToken string `json:"refresh_token"`
280
+ TokenType string `json:"token_type"`
281
+ ExpiresIn int `json:"expires_in"`
282
+ }
283
+
284
+ // generateCodeVerifier 生成PKCE code_verifier
285
+ func generateCodeVerifier(length int) (string, error) {
286
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
287
+ result := make([]byte, length)
288
+ randomBytes := make([]byte, length)
289
+
290
+ if _, err := rand.Read(randomBytes); err != nil {
291
+ return "", err
292
+ }
293
+
294
+ for i := 0; i < length; i++ {
295
+ result[i] = chars[int(randomBytes[i])%len(chars)]
296
+ }
297
+
298
+ return string(result), nil
299
+ }
300
+
301
+ // generateCodeChallenge 生成PKCE code_challenge
302
+ func generateCodeChallenge(codeVerifier string) string {
303
+ hash := sha256.Sum256([]byte(codeVerifier))
304
+ return base64.RawURLEncoding.EncodeToString(hash[:])
305
+ }
306
+
307
+ // generateSessionID 生成会话ID
308
+ func generateSessionID() (string, error) {
309
+ b := make([]byte, 16)
310
+ if _, err := rand.Read(b); err != nil {
311
+ return "", err
312
+ }
313
+ return base64.URLEncoding.EncodeToString(b), nil
314
+ }
315
+
316
+ // cleanupExpiredSessions 清理过期的PKCE会话
317
+ func cleanupExpiredSessions() {
318
+ ticker := time.NewTicker(5 * time.Minute)
319
+ defer ticker.Stop()
320
+
321
+ for range ticker.C {
322
+ pkceStore.Lock()
323
+ now := time.Now()
324
+ for id, session := range pkceStore.sessions {
325
+ if now.Sub(session.CreatedAt) > 10*time.Minute {
326
+ delete(pkceStore.sessions, id)
327
+ }
328
+ }
329
+ pkceStore.Unlock()
330
+ }
331
+ }
internal/handler/openai.go ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handler
2
+
3
+ import (
4
+ "crypto/rand"
5
+ "encoding/hex"
6
+ "encoding/json"
7
+ "errors"
8
+ "fmt"
9
+ "io"
10
+ "net/http"
11
+
12
+ "github.com/gin-gonic/gin"
13
+ "zencoder-2api/internal/model"
14
+ "zencoder-2api/internal/service"
15
+ )
16
+
17
+ type OpenAIHandler struct {
18
+ svc *service.OpenAIService
19
+ grokSvc *service.GrokService
20
+ }
21
+
22
+ func NewOpenAIHandler() *OpenAIHandler {
23
+ return &OpenAIHandler{
24
+ svc: service.NewOpenAIService(),
25
+ grokSvc: service.NewGrokService(),
26
+ }
27
+ }
28
+
29
+ // generateTraceID 生成一个随机的 trace ID
30
+ func generateTraceID() string {
31
+ b := make([]byte, 16)
32
+ rand.Read(b)
33
+ return hex.EncodeToString(b)
34
+ }
35
+
36
+ // ChatCompletions 处理 POST /v1/chat/completions
37
+ func (h *OpenAIHandler) ChatCompletions(c *gin.Context) {
38
+ body, err := io.ReadAll(c.Request.Body)
39
+ if err != nil {
40
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
41
+ return
42
+ }
43
+
44
+ // 解析模型名以确定使用哪个服务
45
+ var req struct {
46
+ Model string `json:"model"`
47
+ }
48
+ if err := json.Unmarshal(body, &req); err != nil {
49
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
50
+ return
51
+ }
52
+
53
+ // 根据模型的 ProviderID 分流
54
+ zenModel, exists := model.GetZenModel(req.Model)
55
+ if !exists {
56
+ // 模型不存在,返回错误
57
+ h.handleError(c, service.ErrNoAvailableAccount)
58
+ return
59
+ }
60
+ if zenModel.ProviderID == "xai" {
61
+ // Grok 模型使用 xAI 服务
62
+ if err := h.grokSvc.ChatCompletionsProxy(c.Request.Context(), c.Writer, body); err != nil {
63
+ h.handleError(c, err)
64
+ }
65
+ return
66
+ }
67
+
68
+ // 其他模型使用 OpenAI 服务
69
+ if err := h.svc.ChatCompletionsProxy(c.Request.Context(), c.Writer, body); err != nil {
70
+ h.handleError(c, err)
71
+ }
72
+ }
73
+
74
+ // Responses 处理 POST /v1/responses
75
+ func (h *OpenAIHandler) Responses(c *gin.Context) {
76
+ body, err := io.ReadAll(c.Request.Body)
77
+ if err != nil {
78
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
79
+ return
80
+ }
81
+
82
+ if err := h.svc.ResponsesProxy(c.Request.Context(), c.Writer, body); err != nil {
83
+ h.handleError(c, err)
84
+ }
85
+ }
86
+
87
+ // handleError 统一处理错误,特别是没有可用账号的错误
88
+ func (h *OpenAIHandler) handleError(c *gin.Context, err error) {
89
+ if errors.Is(err, service.ErrNoAvailableAccount) || errors.Is(err, service.ErrNoPermission) {
90
+ traceID := generateTraceID()
91
+ errMsg := fmt.Sprintf("没有可用token(traceid: %s)", traceID)
92
+ c.JSON(http.StatusServiceUnavailable, gin.H{"error": errMsg})
93
+ return
94
+ }
95
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
96
+ }
internal/handler/token.go ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handler
2
+
3
+ import (
4
+ "net/http"
5
+ "strconv"
6
+ "strings"
7
+ "time"
8
+
9
+ "github.com/gin-gonic/gin"
10
+ "zencoder-2api/internal/database"
11
+ "zencoder-2api/internal/model"
12
+ "zencoder-2api/internal/service"
13
+ )
14
+
15
+ type TokenHandler struct{}
16
+
17
+ func NewTokenHandler() *TokenHandler {
18
+ return &TokenHandler{}
19
+ }
20
+
21
+ // ListTokenRecords 获取所有token记录
22
+ func (h *TokenHandler) ListTokenRecords(c *gin.Context) {
23
+ records, err := service.GetAllTokenRecords()
24
+ if err != nil {
25
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
26
+ return
27
+ }
28
+
29
+ // 获取每个token的生成任务统计
30
+ var enrichedRecords []map[string]interface{}
31
+ for _, record := range records {
32
+ // 统计该token的任务信息
33
+ var taskStats struct {
34
+ TotalTasks int64 `json:"total_tasks"`
35
+ TotalSuccess int64 `json:"total_success"`
36
+ TotalFail int64 `json:"total_fail"`
37
+ RunningTasks int64 `json:"running_tasks"`
38
+ }
39
+
40
+ db := database.GetDB()
41
+ db.Model(&model.GenerationTask{}).Where("token_record_id = ?", record.ID).Count(&taskStats.TotalTasks)
42
+ db.Model(&model.GenerationTask{}).Where("token_record_id = ?", record.ID).
43
+ Select("COALESCE(SUM(success_count), 0)").Scan(&taskStats.TotalSuccess)
44
+ db.Model(&model.GenerationTask{}).Where("token_record_id = ?", record.ID).
45
+ Select("COALESCE(SUM(fail_count), 0)").Scan(&taskStats.TotalFail)
46
+ db.Model(&model.GenerationTask{}).Where("token_record_id = ? AND status = ?", record.ID, "running").
47
+ Count(&taskStats.RunningTasks)
48
+
49
+ // 解析JWT获取用户信息
50
+ var email string
51
+ var planType string
52
+ var subscriptionStartDate time.Time
53
+ if record.Token != "" {
54
+ if payload, err := service.ParseJWT(record.Token); err == nil {
55
+ email = payload.Email
56
+ planType = payload.CustomClaims.Plan
57
+ if planType != "" {
58
+ planType = strings.ToUpper(planType[:1]) + planType[1:]
59
+ }
60
+ // 获取订阅开始时间
61
+ subscriptionStartDate = service.GetSubscriptionDate(payload)
62
+ }
63
+ }
64
+
65
+ enrichedRecord := map[string]interface{}{
66
+ "id": record.ID,
67
+ "description": record.Description,
68
+ "generated_count": record.GeneratedCount,
69
+ "last_generated_at": record.LastGeneratedAt,
70
+ "auto_generate": record.AutoGenerate,
71
+ "threshold": record.Threshold,
72
+ "generate_batch": record.GenerateBatch,
73
+ "is_active": record.IsActive,
74
+ "created_at": record.CreatedAt,
75
+ "updated_at": record.UpdatedAt,
76
+ "token_expiry": record.TokenExpiry,
77
+ "status": record.Status,
78
+ "ban_reason": record.BanReason,
79
+ "email": email,
80
+ "plan_type": planType,
81
+ "subscription_start_date": subscriptionStartDate,
82
+ "has_refresh_token": record.RefreshToken != "",
83
+ "total_tasks": taskStats.TotalTasks,
84
+ "total_success": taskStats.TotalSuccess,
85
+ "total_fail": taskStats.TotalFail,
86
+ "running_tasks": taskStats.RunningTasks,
87
+ }
88
+ enrichedRecords = append(enrichedRecords, enrichedRecord)
89
+ }
90
+
91
+ c.JSON(http.StatusOK, gin.H{
92
+ "items": enrichedRecords,
93
+ "total": len(enrichedRecords),
94
+ })
95
+ }
96
+
97
+ // UpdateTokenRecord 更新token记录配置
98
+ func (h *TokenHandler) UpdateTokenRecord(c *gin.Context) {
99
+ id := c.Param("id")
100
+ tokenID, err := strconv.ParseUint(id, 10, 32)
101
+ if err != nil {
102
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
103
+ return
104
+ }
105
+
106
+ var req struct {
107
+ AutoGenerate *bool `json:"auto_generate"`
108
+ Threshold *int `json:"threshold"`
109
+ GenerateBatch *int `json:"generate_batch"`
110
+ IsActive *bool `json:"is_active"`
111
+ Description string `json:"description"`
112
+ }
113
+
114
+ if err := c.ShouldBindJSON(&req); err != nil {
115
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
116
+ return
117
+ }
118
+
119
+ updates := make(map[string]interface{})
120
+ if req.AutoGenerate != nil {
121
+ updates["auto_generate"] = *req.AutoGenerate
122
+ }
123
+ if req.Threshold != nil {
124
+ updates["threshold"] = *req.Threshold
125
+ }
126
+ if req.GenerateBatch != nil {
127
+ updates["generate_batch"] = *req.GenerateBatch
128
+ }
129
+ if req.IsActive != nil {
130
+ updates["is_active"] = *req.IsActive
131
+ }
132
+ if req.Description != "" {
133
+ updates["description"] = req.Description
134
+ }
135
+
136
+ if err := service.UpdateTokenRecord(uint(tokenID), updates); err != nil {
137
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
138
+ return
139
+ }
140
+
141
+ c.JSON(http.StatusOK, gin.H{"message": "updated"})
142
+ }
143
+
144
+ // GetGenerationTasks 获取生成任务历史
145
+ func (h *TokenHandler) GetGenerationTasks(c *gin.Context) {
146
+ tokenRecordID := c.Query("token_record_id")
147
+ var tokenID uint
148
+ if tokenRecordID != "" {
149
+ id, err := strconv.ParseUint(tokenRecordID, 10, 32)
150
+ if err != nil {
151
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid token_record_id"})
152
+ return
153
+ }
154
+ tokenID = uint(id)
155
+ }
156
+
157
+ tasks, err := service.GetGenerationTasks(tokenID)
158
+ if err != nil {
159
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
160
+ return
161
+ }
162
+
163
+ c.JSON(http.StatusOK, gin.H{
164
+ "items": tasks,
165
+ "total": len(tasks),
166
+ })
167
+ }
168
+
169
+ // TriggerGeneration 手动触发生成
170
+ func (h *TokenHandler) TriggerGeneration(c *gin.Context) {
171
+ id := c.Param("id")
172
+ tokenID, err := strconv.ParseUint(id, 10, 32)
173
+ if err != nil {
174
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
175
+ return
176
+ }
177
+
178
+ if err := service.ManualTriggerGeneration(uint(tokenID)); err != nil {
179
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
180
+ return
181
+ }
182
+
183
+ c.JSON(http.StatusOK, gin.H{"message": "生成任务已触发"})
184
+ }
185
+
186
+ // GetPoolStatus 获取号池状态
187
+ func (h *TokenHandler) GetPoolStatus(c *gin.Context) {
188
+ db := database.GetDB()
189
+
190
+ var stats struct {
191
+ TotalAccounts int64 `json:"total_accounts"`
192
+ NormalAccounts int64 `json:"normal_accounts"`
193
+ CoolingAccounts int64 `json:"cooling_accounts"`
194
+ BannedAccounts int64 `json:"banned_accounts"`
195
+ ErrorAccounts int64 `json:"error_accounts"`
196
+ DisabledAccounts int64 `json:"disabled_accounts"`
197
+ ActiveTokens int64 `json:"active_tokens"`
198
+ RunningTasks int64 `json:"running_tasks"`
199
+ }
200
+
201
+ // 统计账号状态
202
+ db.Model(&model.Account{}).Count(&stats.TotalAccounts)
203
+ db.Model(&model.Account{}).Where("status = ?", "normal").Count(&stats.NormalAccounts)
204
+ db.Model(&model.Account{}).Where("status = ?", "cooling").Count(&stats.CoolingAccounts)
205
+ db.Model(&model.Account{}).Where("status = ?", "banned").Count(&stats.BannedAccounts)
206
+ db.Model(&model.Account{}).Where("status = ?", "error").Count(&stats.ErrorAccounts)
207
+ db.Model(&model.Account{}).Where("status = ?", "disabled").Count(&stats.DisabledAccounts)
208
+
209
+ // 统计激活的token
210
+ db.Model(&model.TokenRecord{}).Where("is_active = ?", true).Count(&stats.ActiveTokens)
211
+
212
+ // 统计运行中的任务
213
+ db.Model(&model.GenerationTask{}).Where("status = ?", "running").Count(&stats.RunningTasks)
214
+
215
+ c.JSON(http.StatusOK, stats)
216
+ }
217
+
218
+ // DeleteTokenRecord 删除token记录
219
+ func (h *TokenHandler) DeleteTokenRecord(c *gin.Context) {
220
+ id := c.Param("id")
221
+ tokenID, err := strconv.ParseUint(id, 10, 32)
222
+ if err != nil {
223
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
224
+ return
225
+ }
226
+
227
+ // 开启事务,确保删除操作的原子性
228
+ db := database.GetDB()
229
+ tx := db.Begin()
230
+
231
+ // 先删除所有关联的生成任务历史记录
232
+ if err := tx.Where("token_record_id = ?", tokenID).Delete(&model.GenerationTask{}).Error; err != nil {
233
+ tx.Rollback()
234
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "删除关联任务失败: " + err.Error()})
235
+ return
236
+ }
237
+
238
+ // 删除token记录本身
239
+ if err := tx.Delete(&model.TokenRecord{}, tokenID).Error; err != nil {
240
+ tx.Rollback()
241
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "删除token记录失败: " + err.Error()})
242
+ return
243
+ }
244
+
245
+ // 提交事务
246
+ if err := tx.Commit().Error; err != nil {
247
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "提交事务失败: " + err.Error()})
248
+ return
249
+ }
250
+
251
+ c.JSON(http.StatusOK, gin.H{"message": "token及其所有历史记录已删除"})
252
+ }
253
+
254
+ // RefreshTokenRecord 刷新token记录
255
+ func (h *TokenHandler) RefreshTokenRecord(c *gin.Context) {
256
+ id := c.Param("id")
257
+ tokenID, err := strconv.ParseUint(id, 10, 32)
258
+ if err != nil {
259
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
260
+ return
261
+ }
262
+
263
+ // 调用service层的刷新函数
264
+ if err := service.RefreshTokenAndAccounts(uint(tokenID)); err != nil {
265
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
266
+ return
267
+ }
268
+
269
+ c.JSON(http.StatusOK, gin.H{"message": "Token刷新成功,相关账号刷新已启动"})
270
+ }