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.StartErrorCleanerService()
|
||||||
|
|
||||||
|
// 启动母号封禁检查服务(需在配置中启用)
|
||||||
|
api.StartBanCheckService()
|
||||||
|
|
||||||
// 启动服务器
|
// 启动服务器
|
||||||
startServer(cfg)
|
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/db/owners/refetch-account-ids", api.CORS(api.HandleRefetchAccountIDs))
|
||||||
mux.HandleFunc("/api/upload/validate", api.CORS(api.HandleUploadValidate))
|
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
|
// 注册测试 API
|
||||||
mux.HandleFunc("/api/register/test", api.CORS(handleRegisterTest))
|
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
|
cfg.MailServices = services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if v, _ := configDB.GetConfig("site_name"); v != "" {
|
||||||
|
cfg.SiteName = v
|
||||||
|
}
|
||||||
|
|
||||||
Global = cfg
|
Global = cfg
|
||||||
return cfg
|
return cfg
|
||||||
@@ -210,6 +213,10 @@ func SaveToDB() error {
|
|||||||
configDB.SetConfig("mail_services", string(data))
|
configDB.SetConfig("mail_services", string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.SiteName != "" {
|
||||||
|
configDB.SetConfig("site_name", cfg.SiteName)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ import (
|
|||||||
|
|
||||||
// TeamOwner 账号结构
|
// TeamOwner 账号结构
|
||||||
type TeamOwner struct {
|
type TeamOwner struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
Token string `json:"token,omitempty"`
|
Token string `json:"token,omitempty"`
|
||||||
AccountID string `json:"account_id"`
|
AccountID string `json:"account_id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
LastCheckedAt *time.Time `json:"last_checked_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DB 数据库管理器
|
// DB 数据库管理器
|
||||||
@@ -40,10 +41,31 @@ func Init(dbPath string) error {
|
|||||||
return fmt.Errorf("创建表失败: %w", err)
|
return fmt.Errorf("创建表失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 执行数据库迁移
|
||||||
|
if err := Instance.migrate(); err != nil {
|
||||||
|
fmt.Printf("[数据库] 迁移警告: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("[数据库] SQLite 已连接: %s\n", dbPath)
|
fmt.Printf("[数据库] SQLite 已连接: %s\n", dbPath)
|
||||||
return nil
|
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 创建表
|
// createTables 创建表
|
||||||
func (d *DB) createTables() error {
|
func (d *DB) createTables() error {
|
||||||
_, err := d.db.Exec(`
|
_, err := d.db.Exec(`
|
||||||
@@ -54,9 +76,10 @@ func (d *DB) createTables() error {
|
|||||||
token TEXT,
|
token TEXT,
|
||||||
account_id TEXT NOT NULL,
|
account_id TEXT NOT NULL,
|
||||||
status TEXT DEFAULT 'valid',
|
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_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_status ON team_owners(status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_team_owners_account_id ON team_owners(account_id);
|
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 获取列表
|
// GetTeamOwners 获取列表
|
||||||
func (d *DB) GetTeamOwners(status string, limit, offset int) ([]TeamOwner, int, error) {
|
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"
|
countQuery := "SELECT COUNT(*) FROM team_owners WHERE 1=1"
|
||||||
args := []interface{}{}
|
args := []interface{}{}
|
||||||
|
|
||||||
@@ -202,10 +225,14 @@ func (d *DB) GetTeamOwners(status string, limit, offset int) ([]TeamOwner, int,
|
|||||||
var owners []TeamOwner
|
var owners []TeamOwner
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var owner TeamOwner
|
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 {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if lastCheckedAt.Valid {
|
||||||
|
owner.LastCheckedAt = &lastCheckedAt.Time
|
||||||
|
}
|
||||||
owners = append(owners, owner)
|
owners = append(owners, owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +242,7 @@ func (d *DB) GetTeamOwners(status string, limit, offset int) ([]TeamOwner, int,
|
|||||||
// GetPendingOwners 获取待处理(排除已使用和处理中的)
|
// GetPendingOwners 获取待处理(排除已使用和处理中的)
|
||||||
func (d *DB) GetPendingOwners() ([]TeamOwner, error) {
|
func (d *DB) GetPendingOwners() ([]TeamOwner, error) {
|
||||||
rows, err := d.db.Query(`
|
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'
|
FROM team_owners WHERE status = 'valid'
|
||||||
ORDER BY created_at ASC
|
ORDER BY created_at ASC
|
||||||
`)
|
`)
|
||||||
@@ -227,15 +254,62 @@ func (d *DB) GetPendingOwners() ([]TeamOwner, error) {
|
|||||||
var owners []TeamOwner
|
var owners []TeamOwner
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var owner TeamOwner
|
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 {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if lastCheckedAt.Valid {
|
||||||
|
owner.LastCheckedAt = &lastCheckedAt.Time
|
||||||
|
}
|
||||||
owners = append(owners, owner)
|
owners = append(owners, owner)
|
||||||
}
|
}
|
||||||
return owners, nil
|
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 为已使用
|
// MarkOwnerAsUsed 标记 Owner 为已使用
|
||||||
func (d *DB) MarkOwnerAsUsed(email string) error {
|
func (d *DB) MarkOwnerAsUsed(email string) error {
|
||||||
_, err := d.db.Exec("UPDATE team_owners SET status = 'used' WHERE email = ?", email)
|
_, 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
|
// GetOwnersWithoutAccountID 获取缺少 account_id 的 owners
|
||||||
func (d *DB) GetOwnersWithoutAccountID() ([]TeamOwner, error) {
|
func (d *DB) GetOwnersWithoutAccountID() ([]TeamOwner, error) {
|
||||||
rows, err := d.db.Query(`
|
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
|
FROM team_owners WHERE account_id = '' OR account_id IS NULL
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
`)
|
`)
|
||||||
@@ -302,10 +376,14 @@ func (d *DB) GetOwnersWithoutAccountID() ([]TeamOwner, error) {
|
|||||||
var owners []TeamOwner
|
var owners []TeamOwner
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var owner TeamOwner
|
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 {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if lastCheckedAt.Valid {
|
||||||
|
owner.LastCheckedAt = &lastCheckedAt.Time
|
||||||
|
}
|
||||||
owners = append(owners, owner)
|
owners = append(owners, owner)
|
||||||
}
|
}
|
||||||
return owners, nil
|
return owners, nil
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ type AccountCheckResponse struct {
|
|||||||
} `json:"accounts"`
|
} `json:"accounts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AccountStatus 账户状态检查结果
|
||||||
|
type AccountStatus struct {
|
||||||
|
Status string // active, banned, token_expired, error
|
||||||
|
PlanType string // team, plus, free 等
|
||||||
|
AccountID string // 账户 ID
|
||||||
|
Error string // 错误信息
|
||||||
|
}
|
||||||
|
|
||||||
// New 创建邀请器 (使用默认代理)
|
// New 创建邀请器 (使用默认代理)
|
||||||
func New(accessToken string) *TeamInviter {
|
func New(accessToken string) *TeamInviter {
|
||||||
c, _ := client.New(DefaultProxy)
|
c, _ := client.New(DefaultProxy)
|
||||||
@@ -59,6 +67,77 @@ func (t *TeamInviter) SetAccountID(accountID string) {
|
|||||||
t.accountID = accountID
|
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)
|
// GetAccountID 获取 Team 的 account_id (workspace_id)
|
||||||
func (t *TeamInviter) GetAccountID() (string, error) {
|
func (t *TeamInviter) GetAccountID() (string, error) {
|
||||||
req, _ := http.NewRequest("GET", "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27", nil)
|
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 { useState, useEffect, useCallback } from 'react'
|
||||||
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square } from 'lucide-react'
|
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square, ShieldCheck, Settings, Clock } from 'lucide-react'
|
||||||
import { Card, CardHeader, CardTitle, CardContent, Button } from '../common'
|
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../common'
|
||||||
|
|
||||||
interface TeamOwner {
|
interface TeamOwner {
|
||||||
id: number
|
id: number
|
||||||
@@ -8,6 +8,25 @@ interface TeamOwner {
|
|||||||
account_id: string
|
account_id: string
|
||||||
status: string
|
status: string
|
||||||
created_at: 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> = {
|
const statusColors: Record<string, string> = {
|
||||||
@@ -42,6 +61,12 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
|||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
const limit = 20
|
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 () => {
|
const loadOwners = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
@@ -72,6 +97,99 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
|||||||
setSelectedIds(new Set())
|
setSelectedIds(new Set())
|
||||||
}, [page, filter])
|
}, [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) => {
|
const handleDelete = async (id: number) => {
|
||||||
if (!confirm('确认删除此账号?')) return
|
if (!confirm('确认删除此账号?')) return
|
||||||
@@ -247,6 +365,29 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
|||||||
>
|
>
|
||||||
{refetching ? '获取中...' : '重新获取ID'}
|
{refetching ? '获取中...' : '重新获取ID'}
|
||||||
</Button>
|
</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 && (
|
{selectedIds.size > 0 && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -280,6 +421,62 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</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">
|
<CardContent className="flex-1 overflow-hidden p-0">
|
||||||
<div className="h-full overflow-auto">
|
<div className="h-full overflow-auto">
|
||||||
<table className="w-full text-sm">
|
<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">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-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>
|
<th className="text-center p-3 font-medium text-slate-600 dark:text-slate-400">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="text-center py-8 text-slate-500">
|
<td colSpan={7} className="text-center py-8 text-slate-500">
|
||||||
加载中...
|
加载中...
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : owners.length === 0 ? (
|
) : owners.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="text-center py-8 text-slate-500">
|
<td colSpan={7} className="text-center py-8 text-slate-500">
|
||||||
暂无数据
|
暂无数据
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -336,6 +534,9 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 text-slate-500 text-xs">{formatTime(owner.created_at)}</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">
|
<td className="p-3 text-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(owner.id)}
|
onClick={() => handleDelete(owner.id)}
|
||||||
|
|||||||
Reference in New Issue
Block a user