feat: implement core backend for team owner management with SQLite, auto-add APIs, and a frontend owner list component.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user