feat: Implement core backend infrastructure including configuration management, SQLite database with team owners and app settings, and initial owner-related APIs and frontend components.
This commit is contained in:
@@ -85,6 +85,9 @@ func main() {
|
||||
// 启动错误账号定期清理服务(需在配置中启用)
|
||||
api.StartErrorCleanerService()
|
||||
|
||||
// 启动母号封禁检查服务(需在配置中启用)
|
||||
api.StartBanCheckService()
|
||||
|
||||
// 启动服务器
|
||||
startServer(cfg)
|
||||
}
|
||||
@@ -121,6 +124,11 @@ func startServer(cfg *config.Config) {
|
||||
mux.HandleFunc("/api/db/owners/refetch-account-ids", api.CORS(api.HandleRefetchAccountIDs))
|
||||
mux.HandleFunc("/api/upload/validate", api.CORS(api.HandleUploadValidate))
|
||||
|
||||
// 母号封禁检查 API
|
||||
mux.HandleFunc("/api/db/owners/ban-check", api.CORS(api.HandleManualBanCheck)) // 手动触发检查
|
||||
mux.HandleFunc("/api/db/owners/ban-check/status", api.CORS(api.HandleBanCheckStatus)) // 检查状态
|
||||
mux.HandleFunc("/api/db/owners/ban-check/settings", api.CORS(api.HandleBanCheckSettings)) // 配置
|
||||
|
||||
// 注册测试 API
|
||||
mux.HandleFunc("/api/register/test", api.CORS(handleRegisterTest))
|
||||
|
||||
|
||||
417
backend/internal/api/owner_ban_check.go
Normal file
417
backend/internal/api/owner_ban_check.go
Normal file
@@ -0,0 +1,417 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"codex-pool/internal/config"
|
||||
"codex-pool/internal/database"
|
||||
"codex-pool/internal/invite"
|
||||
"codex-pool/internal/logger"
|
||||
)
|
||||
|
||||
// 封禁检查服务状态
|
||||
var (
|
||||
banCheckRunning bool
|
||||
banCheckStopChan chan struct{}
|
||||
banCheckMu sync.Mutex
|
||||
lastBanCheckTime time.Time
|
||||
banCheckTaskState BanCheckTaskState
|
||||
)
|
||||
|
||||
// BanCheckTaskState 封禁检查任务状态
|
||||
type BanCheckTaskState struct {
|
||||
Running bool `json:"running"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
Total int32 `json:"total"`
|
||||
Checked int32 `json:"checked"`
|
||||
Banned int32 `json:"banned"`
|
||||
Valid int32 `json:"valid"`
|
||||
Failed int32 `json:"failed"`
|
||||
}
|
||||
|
||||
// BanCheckResult 单个检查结果
|
||||
type BanCheckResult struct {
|
||||
Email string `json:"email"`
|
||||
Status string `json:"status"` // valid, banned, error
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// StartBanCheckService 启动定期封禁检查服务
|
||||
func StartBanCheckService() {
|
||||
banCheckMu.Lock()
|
||||
if banCheckRunning {
|
||||
banCheckMu.Unlock()
|
||||
return
|
||||
}
|
||||
banCheckRunning = true
|
||||
banCheckStopChan = make(chan struct{})
|
||||
banCheckMu.Unlock()
|
||||
|
||||
logger.Info("母号封禁检查服务已启动", "", "ban-check")
|
||||
|
||||
go func() {
|
||||
// 默认检查间隔 30 分钟
|
||||
checkInterval := 1800
|
||||
|
||||
for {
|
||||
// 动态读取检查间隔配置
|
||||
if database.Instance != nil {
|
||||
if val, _ := database.Instance.GetConfig("ban_check_interval"); val != "" {
|
||||
if v, err := strconv.Atoi(val); err == nil && v >= 60 {
|
||||
checkInterval = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-banCheckStopChan:
|
||||
logger.Info("母号封禁检查服务已停止", "", "ban-check")
|
||||
return
|
||||
case <-time.After(time.Duration(checkInterval) * time.Second):
|
||||
runScheduledBanCheck()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// StopBanCheckService 停止定期封禁检查服务
|
||||
func StopBanCheckService() {
|
||||
banCheckMu.Lock()
|
||||
defer banCheckMu.Unlock()
|
||||
|
||||
if banCheckRunning && banCheckStopChan != nil {
|
||||
close(banCheckStopChan)
|
||||
banCheckRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
// runScheduledBanCheck 执行定期封禁检查
|
||||
func runScheduledBanCheck() {
|
||||
if database.Instance == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否开启定期检查
|
||||
enabled := false
|
||||
if val, _ := database.Instance.GetConfig("ban_check_enabled"); val == "true" {
|
||||
enabled = true
|
||||
}
|
||||
if !enabled {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有任务在运行
|
||||
if banCheckTaskState.Running || teamProcessState.Running {
|
||||
logger.Info("有其他任务在运行,跳过定期封禁检查", "", "ban-check")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取检查间隔(小时)
|
||||
checkIntervalHours := 24
|
||||
if val, _ := database.Instance.GetConfig("ban_check_hours"); val != "" {
|
||||
if v, err := strconv.Atoi(val); err == nil && v > 0 {
|
||||
checkIntervalHours = v
|
||||
}
|
||||
}
|
||||
|
||||
// 获取需要检查的母号
|
||||
owners, err := database.Instance.GetOwnersForBanCheck(checkIntervalHours)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("获取待检查母号失败: %v", err), "", "ban-check")
|
||||
return
|
||||
}
|
||||
|
||||
if len(owners) == 0 {
|
||||
logger.Info("没有需要检查的母号", "", "ban-check")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("定期封禁检查: 发现 %d 个需要检查的母号", len(owners)), "", "ban-check")
|
||||
|
||||
// 执行检查
|
||||
go runBanCheckTask(owners, 2)
|
||||
}
|
||||
|
||||
// HandleBanCheckSettings 获取/设置封禁检查配置
|
||||
func HandleBanCheckSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if database.Instance == nil {
|
||||
Error(w, http.StatusInternalServerError, "数据库未初始化")
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
settings := map[string]interface{}{
|
||||
"enabled": false,
|
||||
"interval": 1800, // 检查服务间隔(秒)
|
||||
"check_hours": 24, // 多少小时后重新检查
|
||||
"last_check": lastBanCheckTime,
|
||||
"task_state": banCheckTaskState,
|
||||
"service_running": banCheckRunning,
|
||||
}
|
||||
|
||||
if val, _ := database.Instance.GetConfig("ban_check_enabled"); val == "true" {
|
||||
settings["enabled"] = true
|
||||
}
|
||||
if val, _ := database.Instance.GetConfig("ban_check_interval"); val != "" {
|
||||
if v, err := strconv.Atoi(val); err == nil {
|
||||
settings["interval"] = v
|
||||
}
|
||||
}
|
||||
if val, _ := database.Instance.GetConfig("ban_check_hours"); val != "" {
|
||||
if v, err := strconv.Atoi(val); err == nil {
|
||||
settings["check_hours"] = v
|
||||
}
|
||||
}
|
||||
|
||||
Success(w, settings)
|
||||
|
||||
case http.MethodPut:
|
||||
var req struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
Interval *int `json:"interval"`
|
||||
CheckHours *int `json:"check_hours"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, http.StatusBadRequest, "请求格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Enabled != nil {
|
||||
database.Instance.SetConfig("ban_check_enabled", strconv.FormatBool(*req.Enabled))
|
||||
}
|
||||
if req.Interval != nil && *req.Interval >= 60 {
|
||||
database.Instance.SetConfig("ban_check_interval", strconv.Itoa(*req.Interval))
|
||||
}
|
||||
if req.CheckHours != nil && *req.CheckHours > 0 {
|
||||
database.Instance.SetConfig("ban_check_hours", strconv.Itoa(*req.CheckHours))
|
||||
}
|
||||
|
||||
logger.Success("封禁检查配置已更新", "", "ban-check")
|
||||
Success(w, map[string]string{"message": "配置已更新"})
|
||||
|
||||
default:
|
||||
Error(w, http.StatusMethodNotAllowed, "不支持的方法")
|
||||
}
|
||||
}
|
||||
|
||||
// HandleManualBanCheck 手动触发封禁检查
|
||||
func HandleManualBanCheck(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
Error(w, http.StatusMethodNotAllowed, "不支持的方法")
|
||||
return
|
||||
}
|
||||
|
||||
if database.Instance == nil {
|
||||
Error(w, http.StatusInternalServerError, "数据库未初始化")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有任务在运行
|
||||
if banCheckTaskState.Running {
|
||||
Error(w, http.StatusConflict, "封禁检查任务正在运行中")
|
||||
return
|
||||
}
|
||||
|
||||
if teamProcessState.Running {
|
||||
Error(w, http.StatusConflict, "Team 处理任务正在运行中,请稍后再试")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
IDs []int64 `json:"ids"` // 指定检查的母号 ID,为空则检查所有有效母号
|
||||
Concurrency int `json:"concurrency"` // 并发数
|
||||
ForceCheck bool `json:"force_check"` // 是否强制检查(忽略上次检查时间)
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
// 允许空 body
|
||||
req.Concurrency = 2
|
||||
}
|
||||
|
||||
if req.Concurrency <= 0 {
|
||||
req.Concurrency = 2
|
||||
}
|
||||
if req.Concurrency > 5 {
|
||||
req.Concurrency = 5
|
||||
}
|
||||
|
||||
var owners []database.TeamOwner
|
||||
var err error
|
||||
|
||||
if len(req.IDs) > 0 {
|
||||
// 检查指定的母号
|
||||
for _, id := range req.IDs {
|
||||
ownerList, _, err := database.Instance.GetTeamOwners("", 1000, 0)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, o := range ownerList {
|
||||
if o.ID == id && o.Status == "valid" {
|
||||
owners = append(owners, o)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if req.ForceCheck {
|
||||
// 强制检查所有有效母号
|
||||
owners, err = database.Instance.GetPendingOwners()
|
||||
} else {
|
||||
// 检查需要检查的母号
|
||||
checkIntervalHours := 24
|
||||
if val, _ := database.Instance.GetConfig("ban_check_hours"); val != "" {
|
||||
if v, err := strconv.Atoi(val); err == nil && v > 0 {
|
||||
checkIntervalHours = v
|
||||
}
|
||||
}
|
||||
owners, err = database.Instance.GetOwnersForBanCheck(checkIntervalHours)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, fmt.Sprintf("获取母号失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(owners) == 0 {
|
||||
Success(w, map[string]interface{}{
|
||||
"message": "没有需要检查的母号",
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 异步执行检查
|
||||
go runBanCheckTask(owners, req.Concurrency)
|
||||
|
||||
Success(w, map[string]interface{}{
|
||||
"message": "封禁检查任务已启动",
|
||||
"total": len(owners),
|
||||
"concurrency": req.Concurrency,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleBanCheckStatus 获取封禁检查任务状态
|
||||
func HandleBanCheckStatus(w http.ResponseWriter, r *http.Request) {
|
||||
Success(w, banCheckTaskState)
|
||||
}
|
||||
|
||||
// runBanCheckTask 执行封禁检查任务
|
||||
func runBanCheckTask(owners []database.TeamOwner, concurrency int) {
|
||||
banCheckTaskState = BanCheckTaskState{
|
||||
Running: true,
|
||||
StartedAt: time.Now(),
|
||||
Total: int32(len(owners)),
|
||||
}
|
||||
defer func() {
|
||||
banCheckTaskState.Running = false
|
||||
lastBanCheckTime = time.Now()
|
||||
}()
|
||||
|
||||
logger.Info(fmt.Sprintf("开始封禁检查: 共 %d 个母号, 并发数: %d", len(owners), concurrency), "", "ban-check")
|
||||
|
||||
// 任务队列
|
||||
taskChan := make(chan database.TeamOwner, len(owners))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 获取代理配置
|
||||
proxy := ""
|
||||
if config.Global != nil {
|
||||
proxy = config.Global.GetProxy()
|
||||
}
|
||||
|
||||
// 启动 worker
|
||||
for w := 0; w < concurrency; w++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for owner := range taskChan {
|
||||
result := checkSingleOwnerBan(owner, proxy)
|
||||
|
||||
// 更新计数
|
||||
atomic.AddInt32(&banCheckTaskState.Checked, 1)
|
||||
switch result.Status {
|
||||
case "valid":
|
||||
atomic.AddInt32(&banCheckTaskState.Valid, 1)
|
||||
case "banned":
|
||||
atomic.AddInt32(&banCheckTaskState.Banned, 1)
|
||||
case "error":
|
||||
atomic.AddInt32(&banCheckTaskState.Failed, 1)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送任务
|
||||
for _, owner := range owners {
|
||||
taskChan <- owner
|
||||
}
|
||||
close(taskChan)
|
||||
|
||||
// 等待完成
|
||||
wg.Wait()
|
||||
|
||||
logger.Success(fmt.Sprintf("封禁检查完成: 共 %d, 有效 %d, 封禁 %d, 失败 %d",
|
||||
banCheckTaskState.Total, banCheckTaskState.Valid, banCheckTaskState.Banned, banCheckTaskState.Failed), "", "ban-check")
|
||||
}
|
||||
|
||||
// checkSingleOwnerBan 检查单个母号的封禁状态
|
||||
// 使用 accounts/check API 直接检测,不发送邀请
|
||||
func checkSingleOwnerBan(owner database.TeamOwner, proxy string) BanCheckResult {
|
||||
result := BanCheckResult{
|
||||
Email: owner.Email,
|
||||
Status: "error",
|
||||
}
|
||||
|
||||
// 创建检查器
|
||||
var checker *invite.TeamInviter
|
||||
if proxy != "" {
|
||||
checker = invite.NewWithProxy(owner.Token, proxy)
|
||||
} else {
|
||||
checker = invite.New(owner.Token)
|
||||
}
|
||||
|
||||
// 调用 accounts/check API 检测状态
|
||||
accountStatus := checker.CheckAccountStatus()
|
||||
|
||||
// 更新最后检查时间
|
||||
database.Instance.UpdateOwnerLastCheckedAtByEmail(owner.Email)
|
||||
|
||||
switch accountStatus.Status {
|
||||
case "active":
|
||||
// 账户正常
|
||||
logger.Info(fmt.Sprintf("母号有效: %s (plan: %s)", owner.Email, accountStatus.PlanType), owner.Email, "ban-check")
|
||||
result.Status = "valid"
|
||||
result.Message = fmt.Sprintf("母号状态正常 (plan: %s)", accountStatus.PlanType)
|
||||
|
||||
// 如果获取到了 account_id 且数据库中没有,则更新
|
||||
if accountStatus.AccountID != "" && owner.AccountID == "" {
|
||||
database.Instance.UpdateOwnerAccountID(owner.ID, accountStatus.AccountID)
|
||||
}
|
||||
|
||||
case "banned":
|
||||
// 账户被封禁
|
||||
logger.Warning(fmt.Sprintf("母号被封禁: %s - %s", owner.Email, accountStatus.Error), owner.Email, "ban-check")
|
||||
database.Instance.MarkOwnerAsInvalid(owner.Email)
|
||||
result.Status = "banned"
|
||||
result.Message = accountStatus.Error
|
||||
|
||||
case "token_expired":
|
||||
// Token 过期
|
||||
logger.Warning(fmt.Sprintf("母号 Token 过期: %s", owner.Email), owner.Email, "ban-check")
|
||||
database.Instance.MarkOwnerAsInvalid(owner.Email)
|
||||
result.Status = "banned"
|
||||
result.Message = "Token 已过期"
|
||||
|
||||
default:
|
||||
// 其他错误
|
||||
logger.Error(fmt.Sprintf("检查失败: %s - %s", owner.Email, accountStatus.Error), owner.Email, "ban-check")
|
||||
result.Message = accountStatus.Error
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -174,6 +174,9 @@ func InitFromDB() *Config {
|
||||
cfg.MailServices = services
|
||||
}
|
||||
}
|
||||
if v, _ := configDB.GetConfig("site_name"); v != "" {
|
||||
cfg.SiteName = v
|
||||
}
|
||||
|
||||
Global = cfg
|
||||
return cfg
|
||||
@@ -210,6 +213,10 @@ func SaveToDB() error {
|
||||
configDB.SetConfig("mail_services", string(data))
|
||||
}
|
||||
|
||||
if cfg.SiteName != "" {
|
||||
configDB.SetConfig("site_name", cfg.SiteName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,14 @@ import (
|
||||
|
||||
// TeamOwner 账号结构
|
||||
type TeamOwner struct {
|
||||
ID int64 `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
AccountID string `json:"account_id"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ID int64 `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
AccountID string `json:"account_id"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastCheckedAt *time.Time `json:"last_checked_at,omitempty"`
|
||||
}
|
||||
|
||||
// DB 数据库管理器
|
||||
@@ -40,10 +41,31 @@ func Init(dbPath string) error {
|
||||
return fmt.Errorf("创建表失败: %w", err)
|
||||
}
|
||||
|
||||
// 执行数据库迁移
|
||||
if err := Instance.migrate(); err != nil {
|
||||
fmt.Printf("[数据库] 迁移警告: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Printf("[数据库] SQLite 已连接: %s\n", dbPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrate 数据库迁移
|
||||
func (d *DB) migrate() error {
|
||||
// 添加 last_checked_at 列(如果不存在)
|
||||
_, err := d.db.Exec(`ALTER TABLE team_owners ADD COLUMN last_checked_at DATETIME`)
|
||||
if err != nil && !isColumnExistsError(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isColumnExistsError 判断是否是列已存在的错误
|
||||
func isColumnExistsError(err error) bool {
|
||||
return err != nil && (err.Error() == "duplicate column name: last_checked_at" ||
|
||||
err.Error() == "table team_owners already has a column named last_checked_at")
|
||||
}
|
||||
|
||||
// createTables 创建表
|
||||
func (d *DB) createTables() error {
|
||||
_, err := d.db.Exec(`
|
||||
@@ -54,9 +76,10 @@ func (d *DB) createTables() error {
|
||||
token TEXT,
|
||||
account_id TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'valid',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_checked_at DATETIME
|
||||
);
|
||||
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_team_owners_email ON team_owners(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_team_owners_status ON team_owners(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_team_owners_account_id ON team_owners(account_id);
|
||||
@@ -173,7 +196,7 @@ func (d *DB) AddTeamOwners(owners []TeamOwner) (int, error) {
|
||||
|
||||
// GetTeamOwners 获取列表
|
||||
func (d *DB) GetTeamOwners(status string, limit, offset int) ([]TeamOwner, int, error) {
|
||||
query := "SELECT id, email, password, token, account_id, status, created_at FROM team_owners WHERE 1=1"
|
||||
query := "SELECT id, email, password, token, account_id, status, created_at, last_checked_at FROM team_owners WHERE 1=1"
|
||||
countQuery := "SELECT COUNT(*) FROM team_owners WHERE 1=1"
|
||||
args := []interface{}{}
|
||||
|
||||
@@ -202,10 +225,14 @@ func (d *DB) GetTeamOwners(status string, limit, offset int) ([]TeamOwner, int,
|
||||
var owners []TeamOwner
|
||||
for rows.Next() {
|
||||
var owner TeamOwner
|
||||
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt)
|
||||
var lastCheckedAt sql.NullTime
|
||||
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt, &lastCheckedAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if lastCheckedAt.Valid {
|
||||
owner.LastCheckedAt = &lastCheckedAt.Time
|
||||
}
|
||||
owners = append(owners, owner)
|
||||
}
|
||||
|
||||
@@ -215,7 +242,7 @@ func (d *DB) GetTeamOwners(status string, limit, offset int) ([]TeamOwner, int,
|
||||
// GetPendingOwners 获取待处理(排除已使用和处理中的)
|
||||
func (d *DB) GetPendingOwners() ([]TeamOwner, error) {
|
||||
rows, err := d.db.Query(`
|
||||
SELECT id, email, password, token, account_id, status, created_at
|
||||
SELECT id, email, password, token, account_id, status, created_at, last_checked_at
|
||||
FROM team_owners WHERE status = 'valid'
|
||||
ORDER BY created_at ASC
|
||||
`)
|
||||
@@ -227,15 +254,62 @@ func (d *DB) GetPendingOwners() ([]TeamOwner, error) {
|
||||
var owners []TeamOwner
|
||||
for rows.Next() {
|
||||
var owner TeamOwner
|
||||
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt)
|
||||
var lastCheckedAt sql.NullTime
|
||||
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt, &lastCheckedAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if lastCheckedAt.Valid {
|
||||
owner.LastCheckedAt = &lastCheckedAt.Time
|
||||
}
|
||||
owners = append(owners, owner)
|
||||
}
|
||||
return owners, nil
|
||||
}
|
||||
|
||||
// GetOwnersForBanCheck 获取需要检查封禁状态的母号
|
||||
func (d *DB) GetOwnersForBanCheck(checkIntervalHours int) ([]TeamOwner, error) {
|
||||
// 获取 valid 状态且 超过检查间隔 或 从未检查过 的母号
|
||||
rows, err := d.db.Query(`
|
||||
SELECT id, email, password, token, account_id, status, created_at, last_checked_at
|
||||
FROM team_owners
|
||||
WHERE status = 'valid'
|
||||
AND (last_checked_at IS NULL OR last_checked_at < datetime('now', ?))
|
||||
ORDER BY last_checked_at ASC NULLS FIRST, created_at ASC
|
||||
`, fmt.Sprintf("-%d hours", checkIntervalHours))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var owners []TeamOwner
|
||||
for rows.Next() {
|
||||
var owner TeamOwner
|
||||
var lastCheckedAt sql.NullTime
|
||||
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt, &lastCheckedAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if lastCheckedAt.Valid {
|
||||
owner.LastCheckedAt = &lastCheckedAt.Time
|
||||
}
|
||||
owners = append(owners, owner)
|
||||
}
|
||||
return owners, nil
|
||||
}
|
||||
|
||||
// UpdateOwnerLastCheckedAt 更新母号的最后检查时间
|
||||
func (d *DB) UpdateOwnerLastCheckedAt(id int64) error {
|
||||
_, err := d.db.Exec("UPDATE team_owners SET last_checked_at = CURRENT_TIMESTAMP WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateOwnerLastCheckedAtByEmail 通过邮箱更新母号的最后检查时间
|
||||
func (d *DB) UpdateOwnerLastCheckedAtByEmail(email string) error {
|
||||
_, err := d.db.Exec("UPDATE team_owners SET last_checked_at = CURRENT_TIMESTAMP WHERE email = ?", email)
|
||||
return err
|
||||
}
|
||||
|
||||
// MarkOwnerAsUsed 标记 Owner 为已使用
|
||||
func (d *DB) MarkOwnerAsUsed(email string) error {
|
||||
_, err := d.db.Exec("UPDATE team_owners SET status = 'used' WHERE email = ?", email)
|
||||
@@ -290,7 +364,7 @@ func (d *DB) ClearUsedOwners() (int64, error) {
|
||||
// GetOwnersWithoutAccountID 获取缺少 account_id 的 owners
|
||||
func (d *DB) GetOwnersWithoutAccountID() ([]TeamOwner, error) {
|
||||
rows, err := d.db.Query(`
|
||||
SELECT id, email, password, token, account_id, status, created_at
|
||||
SELECT id, email, password, token, account_id, status, created_at, last_checked_at
|
||||
FROM team_owners WHERE account_id = '' OR account_id IS NULL
|
||||
ORDER BY created_at DESC
|
||||
`)
|
||||
@@ -302,10 +376,14 @@ func (d *DB) GetOwnersWithoutAccountID() ([]TeamOwner, error) {
|
||||
var owners []TeamOwner
|
||||
for rows.Next() {
|
||||
var owner TeamOwner
|
||||
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt)
|
||||
var lastCheckedAt sql.NullTime
|
||||
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt, &lastCheckedAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if lastCheckedAt.Valid {
|
||||
owner.LastCheckedAt = &lastCheckedAt.Time
|
||||
}
|
||||
owners = append(owners, owner)
|
||||
}
|
||||
return owners, nil
|
||||
|
||||
@@ -36,6 +36,14 @@ type AccountCheckResponse struct {
|
||||
} `json:"accounts"`
|
||||
}
|
||||
|
||||
// AccountStatus 账户状态检查结果
|
||||
type AccountStatus struct {
|
||||
Status string // active, banned, token_expired, error
|
||||
PlanType string // team, plus, free 等
|
||||
AccountID string // 账户 ID
|
||||
Error string // 错误信息
|
||||
}
|
||||
|
||||
// New 创建邀请器 (使用默认代理)
|
||||
func New(accessToken string) *TeamInviter {
|
||||
c, _ := client.New(DefaultProxy)
|
||||
@@ -59,6 +67,77 @@ func (t *TeamInviter) SetAccountID(accountID string) {
|
||||
t.accountID = accountID
|
||||
}
|
||||
|
||||
// CheckAccountStatus 检查账户状态(封禁检测)
|
||||
// 基于 check_ban.py 的逻辑:
|
||||
// - HTTP 200 + 有账户数据 → active
|
||||
// - HTTP 200 + 无账户数据 → banned
|
||||
// - HTTP 401 → token_expired
|
||||
// - HTTP 403 → banned
|
||||
func (t *TeamInviter) CheckAccountStatus() AccountStatus {
|
||||
result := AccountStatus{Status: "error"}
|
||||
|
||||
req, _ := http.NewRequest("GET", "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+t.accessToken)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")
|
||||
|
||||
resp, err := t.client.Do(req)
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("请求失败: %v", err)
|
||||
return result
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
var checkResp AccountCheckResponse
|
||||
if err := json.Unmarshal(body, &checkResp); err != nil {
|
||||
result.Error = fmt.Sprintf("解析失败: %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
// 检查是否有账户数据
|
||||
if len(checkResp.Accounts) == 0 {
|
||||
result.Status = "banned"
|
||||
result.Error = "无账户数据"
|
||||
return result
|
||||
}
|
||||
|
||||
// 查找非 default 的账户
|
||||
for accountID, info := range checkResp.Accounts {
|
||||
if accountID == "default" {
|
||||
continue
|
||||
}
|
||||
result.Status = "active"
|
||||
result.AccountID = accountID
|
||||
result.PlanType = info.Account.PlanType
|
||||
t.accountID = accountID
|
||||
return result
|
||||
}
|
||||
|
||||
// 只有 default 账户,也视为正常
|
||||
result.Status = "active"
|
||||
result.PlanType = "default"
|
||||
return result
|
||||
|
||||
case 401:
|
||||
result.Status = "token_expired"
|
||||
result.Error = "Token 已过期"
|
||||
return result
|
||||
|
||||
case 403:
|
||||
result.Status = "banned"
|
||||
result.Error = "账户被封禁 (403)"
|
||||
return result
|
||||
|
||||
default:
|
||||
result.Error = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// GetAccountID 获取 Team 的 account_id (workspace_id)
|
||||
func (t *TeamInviter) GetAccountID() (string, error) {
|
||||
req, _ := http.NewRequest("GET", "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27", nil)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square } from 'lucide-react'
|
||||
import { Card, CardHeader, CardTitle, CardContent, Button } from '../common'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square, ShieldCheck, Settings, Clock } from 'lucide-react'
|
||||
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../common'
|
||||
|
||||
interface TeamOwner {
|
||||
id: number
|
||||
@@ -8,6 +8,25 @@ interface TeamOwner {
|
||||
account_id: string
|
||||
status: string
|
||||
created_at: string
|
||||
last_checked_at?: string
|
||||
}
|
||||
|
||||
interface BanCheckTaskState {
|
||||
running: boolean
|
||||
started_at: string
|
||||
total: number
|
||||
checked: number
|
||||
banned: number
|
||||
valid: number
|
||||
failed: number
|
||||
}
|
||||
|
||||
interface BanCheckSettings {
|
||||
enabled: boolean
|
||||
interval: number
|
||||
check_hours: number
|
||||
service_running: boolean
|
||||
task_state: BanCheckTaskState
|
||||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
@@ -42,6 +61,12 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const limit = 20
|
||||
|
||||
// 封禁检查相关状态
|
||||
const [banCheckSettings, setBanCheckSettings] = useState<BanCheckSettings | null>(null)
|
||||
const [showBanCheckSettings, setShowBanCheckSettings] = useState(false)
|
||||
const [banCheckRunning, setBanCheckRunning] = useState(false)
|
||||
const [checkHours, setCheckHours] = useState(24)
|
||||
|
||||
const loadOwners = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@@ -72,6 +97,99 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
||||
setSelectedIds(new Set())
|
||||
}, [page, filter])
|
||||
|
||||
// 加载封禁检查配置
|
||||
const loadBanCheckSettings = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/db/owners/ban-check/settings')
|
||||
const data = await res.json()
|
||||
if (data.code === 0) {
|
||||
setBanCheckSettings(data.data)
|
||||
setCheckHours(data.data.check_hours || 24)
|
||||
setBanCheckRunning(data.data.task_state?.running || false)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load ban check settings:', e)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadBanCheckSettings()
|
||||
// 轮询检查状态
|
||||
const interval = setInterval(() => {
|
||||
if (banCheckRunning) {
|
||||
loadBanCheckSettings()
|
||||
}
|
||||
}, 2000)
|
||||
return () => clearInterval(interval)
|
||||
}, [loadBanCheckSettings, banCheckRunning])
|
||||
|
||||
// 手动触发封禁检查
|
||||
const handleBanCheck = async (forceCheck = false) => {
|
||||
if (banCheckRunning) return
|
||||
setBanCheckRunning(true)
|
||||
try {
|
||||
const body: { ids?: number[], force_check?: boolean } = {}
|
||||
if (selectedIds.size > 0) {
|
||||
body.ids = Array.from(selectedIds)
|
||||
}
|
||||
if (forceCheck) {
|
||||
body.force_check = true
|
||||
}
|
||||
const res = await fetch('/api/db/owners/ban-check', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.code === 0) {
|
||||
// 开始轮询状态
|
||||
const pollInterval = setInterval(async () => {
|
||||
const statusRes = await fetch('/api/db/owners/ban-check/status')
|
||||
const statusData = await statusRes.json()
|
||||
if (statusData.code === 0) {
|
||||
if (!statusData.data.running) {
|
||||
clearInterval(pollInterval)
|
||||
setBanCheckRunning(false)
|
||||
loadOwners()
|
||||
onStatsChange?.()
|
||||
loadBanCheckSettings()
|
||||
}
|
||||
}
|
||||
}, 2000)
|
||||
} else {
|
||||
alert(data.message || '启动检查失败')
|
||||
setBanCheckRunning(false)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to start ban check:', e)
|
||||
alert('启动检查失败')
|
||||
setBanCheckRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存封禁检查配置
|
||||
const handleSaveBanCheckSettings = async (enabled: boolean) => {
|
||||
try {
|
||||
const res = await fetch('/api/db/owners/ban-check/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
enabled,
|
||||
check_hours: checkHours,
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.code === 0) {
|
||||
loadBanCheckSettings()
|
||||
} else {
|
||||
alert(data.message || '保存失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to save ban check settings:', e)
|
||||
alert('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 单个删除
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('确认删除此账号?')) return
|
||||
@@ -247,6 +365,29 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
||||
>
|
||||
{refetching ? '获取中...' : '重新获取ID'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleBanCheck(false)}
|
||||
disabled={banCheckRunning}
|
||||
icon={<ShieldCheck className={`h-4 w-4 ${banCheckRunning ? 'animate-pulse' : ''}`} />}
|
||||
className="text-emerald-500 hover:text-emerald-600"
|
||||
>
|
||||
{banCheckRunning
|
||||
? `检查中 (${banCheckSettings?.task_state?.checked || 0}/${banCheckSettings?.task_state?.total || 0})`
|
||||
: selectedIds.size > 0
|
||||
? `检查封禁 (${selectedIds.size})`
|
||||
: '检查封禁'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowBanCheckSettings(!showBanCheckSettings)}
|
||||
icon={<Settings className="h-4 w-4" />}
|
||||
className={showBanCheckSettings ? 'text-blue-500' : 'text-slate-500 hover:text-slate-600'}
|
||||
>
|
||||
定期检查
|
||||
</Button>
|
||||
{selectedIds.size > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -280,6 +421,62 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{/* 封禁检查设置面板 */}
|
||||
{showBanCheckSettings && (
|
||||
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-800/50 border-b border-slate-200 dark:border-slate-700">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-slate-600 dark:text-slate-400">定期检查:</label>
|
||||
<button
|
||||
onClick={() => handleSaveBanCheckSettings(!banCheckSettings?.enabled)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
banCheckSettings?.enabled ? 'bg-emerald-500' : 'bg-slate-300 dark:bg-slate-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
banCheckSettings?.enabled ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-slate-400" />
|
||||
<label className="text-sm text-slate-600 dark:text-slate-400">检查间隔:</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={checkHours}
|
||||
onChange={(e) => setCheckHours(parseInt(e.target.value) || 24)}
|
||||
className="w-20 h-8 text-sm"
|
||||
min={1}
|
||||
max={168}
|
||||
/>
|
||||
<span className="text-sm text-slate-500">小时</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleSaveBanCheckSettings(banCheckSettings?.enabled || false)}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-auto text-xs text-slate-500">
|
||||
{banCheckSettings?.task_state?.running ? (
|
||||
<span className="text-emerald-500">
|
||||
检查中: {banCheckSettings.task_state.checked}/{banCheckSettings.task_state.total}
|
||||
(有效: {banCheckSettings.task_state.valid}, 封禁: {banCheckSettings.task_state.banned})
|
||||
</span>
|
||||
) : banCheckSettings?.enabled ? (
|
||||
<span>服务已启用</span>
|
||||
) : (
|
||||
<span>服务未启用</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardContent className="flex-1 overflow-hidden p-0">
|
||||
<div className="h-full overflow-auto">
|
||||
<table className="w-full text-sm">
|
||||
@@ -298,19 +495,20 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
||||
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400">Account ID</th>
|
||||
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400">状态</th>
|
||||
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400">创建时间</th>
|
||||
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400">上次检查</th>
|
||||
<th className="text-center p-3 font-medium text-slate-600 dark:text-slate-400">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center py-8 text-slate-500">
|
||||
<td colSpan={7} className="text-center py-8 text-slate-500">
|
||||
加载中...
|
||||
</td>
|
||||
</tr>
|
||||
) : owners.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center py-8 text-slate-500">
|
||||
<td colSpan={7} className="text-center py-8 text-slate-500">
|
||||
暂无数据
|
||||
</td>
|
||||
</tr>
|
||||
@@ -336,6 +534,9 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-3 text-slate-500 text-xs">{formatTime(owner.created_at)}</td>
|
||||
<td className="p-3 text-slate-500 text-xs">
|
||||
{owner.last_checked_at ? formatTime(owner.last_checked_at) : '-'}
|
||||
</td>
|
||||
<td className="p-3 text-center">
|
||||
<button
|
||||
onClick={() => handleDelete(owner.id)}
|
||||
|
||||
Reference in New Issue
Block a user