feat: implement core backend for team owner management with SQLite, auto-add APIs, and a frontend owner list component.

This commit is contained in:
2026-01-30 20:20:35 +08:00
parent 3c5bb04d82
commit 10cda012af
6 changed files with 125 additions and 14 deletions

View File

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

View File

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

View File

@@ -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(`

View File

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