diff --git a/backend/cmd/main.go b/backend/cmd/main.go index f65fad3..365734d 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -111,6 +111,7 @@ func startServer(cfg *config.Config) { mux.HandleFunc("/api/db/owners", api.CORS(handleGetOwners)) mux.HandleFunc("/api/db/owners/stats", api.CORS(handleGetOwnerStats)) mux.HandleFunc("/api/db/owners/clear", api.CORS(handleClearOwners)) + mux.HandleFunc("/api/db/owners/clear-used", api.CORS(handleClearUsedOwners)) // 清理已使用 mux.HandleFunc("/api/db/owners/delete/", api.CORS(handleDeleteOwner)) // DELETE /api/db/owners/delete/{id} mux.HandleFunc("/api/db/owners/batch-delete", api.CORS(handleBatchDeleteOwners)) // POST 批量删除 mux.HandleFunc("/api/db/owners/refetch-account-ids", api.CORS(api.HandleRefetchAccountIDs)) @@ -609,7 +610,24 @@ func handleGetOwners(w http.ResponseWriter, r *http.Request) { return } - owners, total, err := database.Instance.GetTeamOwners("", 50, 0) + // 读取分页参数 + query := r.URL.Query() + limit := 20 + offset := 0 + status := query.Get("status") + + if l := query.Get("limit"); l != "" { + if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { + limit = parsed + } + } + if o := query.Get("offset"); o != "" { + if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { + offset = parsed + } + } + + owners, total, err := database.Instance.GetTeamOwners(status, limit, offset) if err != nil { api.Error(w, http.StatusInternalServerError, fmt.Sprintf("查询失败: %v", err)) return @@ -645,6 +663,25 @@ func handleClearOwners(w http.ResponseWriter, r *http.Request) { api.Success(w, map[string]string{"message": "已清空"}) } +// handleClearUsedOwners POST /api/db/owners/clear-used - 清理已使用的母号 +func handleClearUsedOwners(w http.ResponseWriter, r *http.Request) { + if database.Instance == nil { + api.Error(w, http.StatusInternalServerError, "数据库未初始化") + return + } + + deleted, err := database.Instance.ClearUsedOwners() + if err != nil { + api.Error(w, http.StatusInternalServerError, fmt.Sprintf("清理失败: %v", err)) + return + } + + api.Success(w, map[string]interface{}{ + "message": fmt.Sprintf("已清理 %d 个已使用的母号", deleted), + "deleted": deleted, + }) +} + // handleDeleteOwner DELETE /api/db/owners/delete/{id} - 删除单个 owner func handleDeleteOwner(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete && r.Method != http.MethodPost { diff --git a/backend/internal/api/auto_add.go b/backend/internal/api/auto_add.go index b4a3f7c..f6f03f8 100644 --- a/backend/internal/api/auto_add.go +++ b/backend/internal/api/auto_add.go @@ -206,12 +206,21 @@ func getS2AAccountCount() (int, error) { } var result struct { - NormalAccounts int `json:"normal_accounts"` + Code int `json:"code"` + Message string `json:"message"` + Data struct { + NormalAccounts int `json:"normal_accounts"` + } `json:"data"` } if err := decodeJSON(resp.Body, &result); err != nil { return 0, err } - return result.NormalAccounts, nil + // S2A 返回 code=0 表示成功 + if result.Code != 0 { + return 0, fmt.Errorf("S2A 返回错误: %s", result.Message) + } + + return result.Data.NormalAccounts, nil } diff --git a/backend/internal/api/team_process.go b/backend/internal/api/team_process.go index 48104ab..8c2b977 100644 --- a/backend/internal/api/team_process.go +++ b/backend/internal/api/team_process.go @@ -292,7 +292,8 @@ func processSingleTeam(idx int, req TeamProcessRequest) TeamProcessResult { Errors: make([]string, 0), } - logPrefix := fmt.Sprintf("[Team %d]", idx+1) + // 固定宽度的 Team 编号 (支持到 Team 99) + logPrefix := fmt.Sprintf("[Team %2d]", idx+1) logger.Info(fmt.Sprintf("%s 开始处理 | 母号: %s", logPrefix, owner.Email), owner.Email, "team") // 标记 owner 为处理中 @@ -385,12 +386,23 @@ func processSingleTeam(idx int, req TeamProcessRequest) TeamProcessResult { // 重试时使用新邮箱 currentEmail = mail.GenerateEmail() currentPassword = register.GeneratePassword() - logger.Warning(fmt.Sprintf("%s [成员 %d] 重试,新邮箱: %s", logPrefix, memberIdx+1, currentEmail), currentEmail, "team") + logger.Warning(fmt.Sprintf("%s [成员 %d] 重试, 新邮箱: %s", logPrefix, memberIdx+1, currentEmail), currentEmail, "team") } // 发送邀请 if err := inviter.SendInvites([]string{currentEmail}); err != nil { + errStr := err.Error() logger.Error(fmt.Sprintf("%s [成员 %d] 邀请失败: %v", logPrefix, memberIdx+1, err), currentEmail, "team") + + // 检测 Team 已达邀请上限 + if strings.Contains(errStr, "maximum number of seats") { + logger.Warning(fmt.Sprintf("%s Team 邀请已满,标记母号为已使用", logPrefix), owner.Email, "team") + if database.Instance != nil { + database.Instance.MarkOwnerAsUsed(owner.Email) + } + // 跳出重试,该成员不再处理 + return false + } continue } @@ -538,7 +550,7 @@ func processSingleTeam(idx int, req TeamProcessRequest) TeamProcessResult { result.Errors = append(result.Errors, fmt.Sprintf("Owner S2A: %v", err)) } else { result.AddedToS2A++ - logger.Success(fmt.Sprintf("%s [母号] ✓ 入库成功", logPrefix), owner.Email, "team") + logger.Success(fmt.Sprintf("%s [母号 ] ✓ 入库成功", logPrefix), owner.Email, "team") } } } diff --git a/backend/internal/database/sqlite.go b/backend/internal/database/sqlite.go index 80a8419..05ecc83 100644 --- a/backend/internal/database/sqlite.go +++ b/backend/internal/database/sqlite.go @@ -189,7 +189,8 @@ func (d *DB) GetTeamOwners(status string, limit, offset int) ([]TeamOwner, int, return nil, 0, err } - query += " ORDER BY created_at DESC LIMIT ? OFFSET ?" + // 排序:已使用(used, pooled)排前面,其次按创建时间降序 + query += " ORDER BY CASE WHEN status IN ('used', 'pooled') THEN 0 ELSE 1 END, created_at DESC LIMIT ? OFFSET ?" args = append(args, limit, offset) rows, err := d.db.Query(query, args...) @@ -277,6 +278,15 @@ func (d *DB) ClearTeamOwners() error { return err } +// ClearUsedOwners 清理已使用的母号(status = 'used' 或 'pooled') +func (d *DB) ClearUsedOwners() (int64, error) { + result, err := d.db.Exec("DELETE FROM team_owners WHERE status IN ('used', 'pooled')") + if err != nil { + return 0, err + } + return result.RowsAffected() +} + // GetOwnersWithoutAccountID 获取缺少 account_id 的 owners func (d *DB) GetOwnersWithoutAccountID() ([]TeamOwner, error) { rows, err := d.db.Query(` diff --git a/backend/internal/logger/logger.go b/backend/internal/logger/logger.go index f0336c5..330e9a6 100644 --- a/backend/internal/logger/logger.go +++ b/backend/internal/logger/logger.go @@ -119,19 +119,27 @@ func log(level, message, email, module string) { color := "" switch level { case "info": - prefix = "INFO" + prefix = "INFO " color = colorCyan case "success": prefix = "SUCCESS" color = colorGreen case "error": - prefix = "ERROR" + prefix = "ERROR " color = colorRed case "warning": - prefix = "WARN" + prefix = "WARN " color = colorYellow } + // 模块名固定宽度(8字符) + moduleStr := module + if len(moduleStr) < 8 { + moduleStr = moduleStr + strings.Repeat(" ", 8-len(moduleStr)) + } else if len(moduleStr) > 8 { + moduleStr = moduleStr[:8] + } + // 如果是 Team 相关日志,消息使用 Team 颜色 msgColor := colorReset if teamColor != "" { @@ -139,15 +147,20 @@ func log(level, message, email, module string) { } if email != "" { - fmt.Printf("%s%s%s %s[%s]%s [%s] %s - %s%s%s\n", + // 截断长邮箱,保持对齐 + emailDisplay := email + if len(emailDisplay) > 35 { + emailDisplay = emailDisplay[:32] + "..." + } + fmt.Printf("%s%s%s %s[%s]%s [%s] %s%s%s\n", colorGray, timestamp, colorReset, color, prefix, colorReset, - module, email, msgColor, message, colorReset) + moduleStr, msgColor, message, colorReset) } else { fmt.Printf("%s%s%s %s[%s]%s [%s] %s%s%s\n", colorGray, timestamp, colorReset, color, prefix, colorReset, - module, msgColor, message, colorReset) + moduleStr, msgColor, message, colorReset) } } diff --git a/frontend/src/components/upload/OwnerList.tsx b/frontend/src/components/upload/OwnerList.tsx index 8733d2d..49b9f52 100644 --- a/frontend/src/components/upload/OwnerList.tsx +++ b/frontend/src/components/upload/OwnerList.tsx @@ -16,14 +16,16 @@ const statusColors: Record = { pooled: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', processing: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400', invalid: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', + used: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400', } const statusLabels: Record = { valid: '有效', registered: '已注册', pooled: '已入库', - processing: 'processing', + processing: '处理中', invalid: '无效', + used: '已使用', } interface OwnerListProps { @@ -131,6 +133,24 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) { } } + const handleClearUsed = async () => { + if (!confirm('确认清理所有已使用的母号?')) return + try { + const res = await fetch('/api/db/owners/clear-used', { method: 'POST' }) + const data = await res.json() + if (data.code === 0) { + alert(data.data.message) + loadOwners() + onStatsChange?.() + } else { + alert(data.message || '清理失败') + } + } catch (e) { + console.error('Failed to clear used:', e) + alert('清理失败') + } + } + const [refetching, setRefetching] = useState(false) const handleRefetchAccountIds = async () => { @@ -212,6 +232,7 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) { + )} +