Files
xiaolajiaoyyds 6f71df4849 Initial open-source release
多平台账号批量注册系统,支持 OpenAI / Grok / Kiro / Gemini 平台的全自动注册。

Features:
- 用户名/密码注册登录(首个用户自动管理员)
- JWT 认证 + bcrypt 密码加密
- 积分系统与多用户管理
- HuggingFace Space 弹性 Worker 节点管理
- Cloudflare Worker 负载均衡
- 实时日志推送(SSE/WebSocket)
- 管理后台(用户管理、系统设置、代理池、卡池)

Tech Stack: Go 1.25 / Gin / GORM / PostgreSQL / Vue 3 / Vite / TailwindCSS
2026-03-22 03:44:42 +08:00

611 lines
19 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"encoding/json"
"fmt"
"math"
"net/http"
"strconv"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/xiaolajiaoyyds/regplatformm/internal/middleware"
"github.com/xiaolajiaoyyds/regplatformm/internal/model"
"github.com/xiaolajiaoyyds/regplatformm/internal/service"
"gorm.io/gorm"
)
// InitHandler 页面初始化批量接口,一次返回所有数据
type InitHandler struct {
db *gorm.DB
creditSvc *service.CreditService
settingSvc *service.SettingService
engine *service.TaskEngine
}
// NewInitHandler 创建初始化处理器
func NewInitHandler(db *gorm.DB, creditSvc *service.CreditService, settingSvc *service.SettingService, engine *service.TaskEngine) *InitHandler {
return &InitHandler{db: db, creditSvc: creditSvc, settingSvc: settingSvc, engine: engine}
}
// Init 页面初始化(GET /api/init)
// 并行查询用户、余额、当前任务、结果、交易记录、公告,一次返回
func (h *InitHandler) Init(c *gin.Context) {
user := middleware.GetUser(c)
if user == nil {
c.JSON(http.StatusUnauthorized, gin.H{"detail": "未登录"})
return
}
var wg sync.WaitGroup
resp := make(map[string]interface{})
var mu sync.Mutex
// 1. 用户信息(直接从 middleware 获取,无需查库)
resp["user"] = gin.H{
"id": user.ID,
"newapi_id": user.NewapiID,
"username": user.Username,
"name": user.Name,
"email": user.Email,
"avatar_url": user.AvatarURL,
"trust_level": user.TrustLevel,
"role": user.Role,
"credits": user.Credits,
"newapi_quota": user.NewapiQuota,
"free_trial_used": user.FreeTrialUsed,
"free_trial_remaining": user.FreeTrialRemaining,
"is_admin": user.IsAdmin,
}
// 2. 余额(并行)
wg.Add(1)
go func() {
defer wg.Done()
credits, freeRemaining, freeUsed := h.creditSvc.GetBalance(user.ID)
maxTarget, _ := strconv.Atoi(h.settingSvc.Get("max_target", "1000"))
maxThreads, _ := strconv.Atoi(h.settingSvc.Get("max_threads", "16"))
dailyLimit, _ := strconv.Atoi(h.settingSvc.Get("daily_reg_limit", "0"))
resetTime := dailyResetTime()
var dailyUsed int64
h.db.Model(&model.Task{}).
Where("user_id = ? AND created_at >= ?", user.ID, resetTime).
Select("COALESCE(SUM(success_count), 0)").Scan(&dailyUsed)
dailyRemaining := -1
if dailyLimit > 0 {
dailyRemaining = dailyLimit - int(dailyUsed)
if dailyRemaining < 0 {
dailyRemaining = 0
}
}
registrationsAvailable := credits + freeRemaining
freeTrialCount := h.settingSvc.GetInt("free_trial_count", 2)
freeTrialEnabled := h.settingSvc.Get("free_trial_enabled", "true") == "true"
trialEligible := !freeUsed && freeTrialEnabled && freeTrialCount > 0
trialRemaining := 0
trialTotal := 0
if freeUsed {
trialRemaining = freeRemaining
} else if trialEligible {
trialRemaining = freeTrialCount
trialTotal = freeTrialCount
}
// New-API 余额(实时查询)
var newapiBalance float64
var newapiBalanceDisplay string
var unitPrice float64
var unitPriceDisplay string
var newapiAvailable int
mode := "local"
display := fmt.Sprintf("%d 次", registrationsAvailable)
costStr := h.settingSvc.Get("newapi_cost_per_reg", "0.004")
costUSD, _ := strconv.ParseFloat(costStr, 64)
if costUSD <= 0 {
costUSD = 0.004
}
baseURL := h.settingSvc.Get("newapi_base_url", "")
if baseURL != "" && user.NewapiID != 0 {
mode = "newapi"
unitPrice = costUSD
unitPriceDisplay = fmt.Sprintf("$%.4g", costUSD)
quotaRaw, balUSD, err := h.creditSvc.GetNewAPIBalance(user)
if err == nil {
newapiBalance = balUSD
newapiBalanceDisplay = fmt.Sprintf("$%.2f", balUSD)
newapiAvailable = int(math.Floor(float64(quotaRaw) / (costUSD * 500000)))
display = fmt.Sprintf("$%.2f", balUSD)
} else {
newapiBalanceDisplay = "$0"
display = fmt.Sprintf("%d 次", registrationsAvailable)
}
}
mu.Lock()
resp["balance"] = gin.H{
"credits": credits,
"free_trial_remaining": freeRemaining,
"free_trial_used": freeUsed,
"cost_per_reg": 1,
"mode": mode,
"display": display,
"registrations_available": registrationsAvailable,
"newapi_balance": newapiBalance,
"newapi_balance_display": newapiBalanceDisplay,
"unit_price": unitPrice,
"unit_price_display": unitPriceDisplay,
"newapi_available": newapiAvailable,
"platform_prices": buildInitPlatformPrices(h.settingSvc, costUSD),
"free_trial": gin.H{
"eligible": trialEligible,
"remaining": trialRemaining,
"total": trialTotal,
},
"limits": gin.H{
"max_target": maxTarget,
"max_threads": maxThreads,
"daily_reg_limit": dailyLimit,
"daily_used": int(dailyUsed),
"daily_remaining": dailyRemaining,
"platform_grok_enabled": h.settingSvc.Get("platform_grok_enabled", "true") == "true",
"platform_openai_enabled": h.settingSvc.Get("platform_openai_enabled", "true") == "true",
"platform_kiro_enabled": h.settingSvc.Get("platform_kiro_enabled", "true") == "true",
"platform_gemini_enabled": h.settingSvc.Get("platform_gemini_enabled", "true") == "true",
"thread_tiers": h.settingSvc.Get("thread_tiers", ""),
"platform_grok_free_until": h.settingSvc.Get("platform_grok_free_until", ""),
"platform_openai_free_until": h.settingSvc.Get("platform_openai_free_until", ""),
"platform_kiro_free_until": h.settingSvc.Get("platform_kiro_free_until", ""),
"platform_gemini_free_until": h.settingSvc.Get("platform_gemini_free_until", ""),
"free_mode": buildFreeModeStatus(h.db, h.settingSvc, user.ID),
"platform_limits": buildPlatformLimits(h.db, h.settingSvc, user.ID),
},
}
mu.Unlock()
}()
// 3. 当前任务(并行)
wg.Add(1)
go func() {
defer wg.Done()
var task model.Task
err := h.db.Where("user_id = ? AND status IN ?", user.ID, []string{"running", "queued", "pending"}).
Order("created_at DESC").First(&task).Error
if err != nil {
// 没有活跃任务,尝试取最近一条已结束的任务(保留顶栏计数)
err2 := h.db.Where("user_id = ?", user.ID).
Order("created_at DESC").First(&task).Error
if err2 != nil {
mu.Lock()
resp["current_task"] = nil
mu.Unlock()
return
}
}
// 检查孤儿任务(事务保护,防止并发双重退款)
if task.Status == "running" && h.engine.GetStatus(user.ID, task.Platform) == nil {
h.db.Transaction(func(tx *gorm.DB) error {
var fresh model.Task
if err := tx.Where("id = ? AND status = ?", task.ID, "running").First(&fresh).Error; err != nil {
return err
}
now := time.Now()
fresh.Status = "stopped"
fresh.StoppedAt = &now
tx.Save(&fresh)
refund := fresh.CreditsReserved - fresh.SuccessCount
if refund > 0 {
h.creditSvc.RefundCredits(tx, user.ID, fresh.ID, refund)
}
task = fresh
return nil
})
}
// 清理孤儿排队任务:超过 10 分钟仍未调度的 queued/pending 视为孤儿
if (task.Status == "queued" || task.Status == "pending") && time.Since(task.CreatedAt) > 10*time.Minute {
h.db.Transaction(func(tx *gorm.DB) error {
var fresh model.Task
if err := tx.Where("id = ? AND status IN ?", task.ID, []string{"queued", "pending"}).First(&fresh).Error; err != nil {
return err
}
now := time.Now()
fresh.Status = "stopped"
fresh.StoppedAt = &now
tx.Save(&fresh)
refund := fresh.CreditsReserved - fresh.SuccessCount
if refund > 0 {
h.creditSvc.RefundCredits(tx, user.ID, fresh.ID, refund)
}
task = fresh
return nil
})
}
mu.Lock()
resp["current_task"] = gin.H{
"task_id": task.ID,
"platform": task.Platform,
"target": task.TargetCount,
"threads": task.ThreadCount,
"credits_reserved": task.CreditsReserved,
"success_count": task.SuccessCount,
"fail_count": task.FailCount,
"status": task.Status,
"is_done": task.IsDone(),
}
mu.Unlock()
}()
// 4. 注册结果(并行)
wg.Add(1)
go func() {
defer wg.Done()
var results []model.TaskResult
// 最多加载 200 条未归档结果,避免全量拉取阻塞首屏
h.db.Where("user_id = ? AND is_archived = ?", user.ID, false).
Order("created_at DESC").Limit(200).Find(&results)
type resultItem struct {
ID uint `json:"id"`
Email string `json:"email"`
Platform string `json:"platform"`
CredentialData json.RawMessage `json:"credential_data"`
SSOToken string `json:"auth_token,omitempty"`
NsfwEnabled bool `json:"feature_enabled"`
CreatedAt string `json:"created_at"`
}
items := make([]resultItem, 0, len(results))
for _, r := range results {
items = append(items, resultItem{
ID: r.ID,
Email: r.Email,
Platform: r.Platform,
CredentialData: json.RawMessage(r.CredentialData),
SSOToken: r.SSOToken,
NsfwEnabled: r.NsfwEnabled,
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
mu.Lock()
resp["results"] = items
mu.Unlock()
}()
// 5. 交易记录(并行)
wg.Add(1)
go func() {
defer wg.Done()
var txs []model.CreditTransaction
h.db.Where("user_id = ?", user.ID).
Order("created_at DESC").Limit(50).Find(&txs)
mu.Lock()
resp["tx_history"] = txs
mu.Unlock()
}()
// 6. 公告(并行)
wg.Add(1)
go func() {
defer wg.Done()
var list []model.Announcement
h.db.Order("created_at DESC").Limit(20).Find(&list)
mu.Lock()
resp["announcements"] = list
mu.Unlock()
}()
// 7. 全局统计(并行)
wg.Add(1)
go func() {
defer wg.Done()
stats := h.buildGlobalStats()
mu.Lock()
resp["global_stats"] = stats
mu.Unlock()
}()
// 8. 用户总成功/失败统计 + 各平台平均耗时(并行)
wg.Add(1)
go func() {
defer wg.Done()
var totalSuccess, totalFail int64
h.db.Model(&model.Task{}).Where("user_id = ?", user.ID).
Select("COALESCE(SUM(success_count), 0)").Scan(&totalSuccess)
h.db.Model(&model.Task{}).Where("user_id = ?", user.ID).
Select("COALESCE(SUM(fail_count), 0)").Scan(&totalFail)
// 各平台成功/失败统计
type platformStatRow struct {
Platform string `json:"platform"`
Success int64 `json:"success"`
Fail int64 `json:"fail"`
}
var platformRows []platformStatRow
h.db.Model(&model.Task{}).Where("user_id = ?", user.ID).
Select("platform, COALESCE(SUM(success_count), 0) as success, COALESCE(SUM(fail_count), 0) as fail").
Group("platform").Scan(&platformRows)
platformMap := map[string]gin.H{}
for _, pr := range platformRows {
platformMap[pr.Platform] = gin.H{"success": pr.Success, "fail": pr.Fail}
}
// 各平台平均每个注册耗时(秒),取最近30个已完成任务
type avgRow struct {
Platform string `json:"platform"`
AvgSecPer float64 `json:"avg_sec_per"`
}
var rows []avgRow
h.db.Raw(`
SELECT platform,
AVG(
EXTRACT(EPOCH FROM stopped_at - started_at)
/ NULLIF(success_count, 0)
) AS avg_sec_per
FROM tasks
WHERE success_count > 0 AND started_at IS NOT NULL AND stopped_at IS NOT NULL
AND status IN ('completed','stopped')
GROUP BY platform
`).Scan(&rows)
avgMap := map[string]float64{}
for _, r := range rows {
if r.AvgSecPer > 0 {
avgMap[r.Platform] = r.AvgSecPer
}
}
mu.Lock()
resp["user_stats"] = gin.H{
"total_success": totalSuccess,
"total_fail": totalFail,
"avg_sec_per_reg": avgMap,
"by_platform": platformMap,
}
mu.Unlock()
}()
// 9. 最近完成的注册(公共展示,并行)
wg.Add(1)
go func() {
defer wg.Done()
items := h.buildRecentCompletions()
mu.Lock()
resp["recent_completions"] = items
mu.Unlock()
}()
wg.Wait()
c.JSON(http.StatusOK, resp)
}
// GlobalStats 全局统计(GET /api/stats/global)
// 面向所有已登录用户,用于顶栏全局大屏
func (h *InitHandler) GlobalStats(c *gin.Context) {
c.JSON(http.StatusOK, h.buildGlobalStats())
}
// LatestCompletions 最新完成的任务(GET /api/stats/latest-completions?after=<unix>)
// 供首页 toast 轮询,返回 after 之后完成的任务(全局,所有用户)
func (h *InitHandler) LatestCompletions(c *gin.Context) {
afterStr := c.Query("after")
after := time.Now().Add(-10 * time.Second) // 默认最近 10 秒
if afterStr != "" {
if ts, err := strconv.ParseInt(afterStr, 10, 64); err == nil {
after = time.Unix(ts, 0)
}
}
// 上下界保护:最多回溯 5 分钟,不接受未来时间戳
minAfter := time.Now().Add(-5 * time.Minute)
if after.Before(minAfter) {
after = minAfter
}
if after.After(time.Now()) {
after = time.Now().Add(-10 * time.Second)
}
var tasks []model.Task
h.db.Where("status IN ? AND stopped_at > ? AND success_count > 0",
[]string{"completed", "stopped"}, after).
Order("stopped_at DESC").Limit(5).Find(&tasks)
// 批量查用户信息(昵称 + 头像)
userIDs := make([]uint, 0, len(tasks))
for _, t := range tasks {
userIDs = append(userIDs, t.UserID)
}
type userBrief struct {
ID uint
Username string
Name string
AvatarURL string
}
userBriefMap := make(map[uint]userBrief)
if len(userIDs) > 0 {
var users []model.User
h.db.Where("id IN ?", userIDs).Select("id, username, name, avatar_url").Find(&users)
for _, u := range users {
userBriefMap[u.ID] = userBrief{ID: u.ID, Username: u.Username, Name: u.Name, AvatarURL: u.AvatarURL}
}
}
type completionItem struct {
TaskID uint `json:"task_id"`
Username string `json:"username"`
Name string `json:"name"`
AvatarURL string `json:"avatar_url"`
Platform string `json:"platform"`
Success int `json:"success_count"`
StoppedAt int64 `json:"stopped_at"`
}
items := make([]completionItem, 0, len(tasks))
for _, t := range tasks {
if t.StoppedAt == nil {
continue
}
ub := userBriefMap[t.UserID]
items = append(items, completionItem{
TaskID: t.ID,
Username: ub.Username,
Name: ub.Name,
AvatarURL: ub.AvatarURL,
Platform: t.Platform,
Success: t.SuccessCount,
StoppedAt: t.StoppedAt.Unix(),
})
}
c.JSON(http.StatusOK, gin.H{"items": items})
}
// buildGlobalStats 构建全局统计数据
func (h *InitHandler) buildGlobalStats() gin.H {
resetTime := dailyResetTime()
// 运行中任务数
var runningTasks int64
h.db.Model(&model.Task{}).Where("status = ?", "running").Count(&runningTasks)
// 排队中任务数
var queuedTasks int64
h.db.Model(&model.Task{}).Where("status = ?", "queued").Count(&queuedTasks)
// 活跃用户数(有运行中/排队中任务的独立用户)
var activeUsers int64
h.db.Model(&model.Task{}).Where("status IN ?", []string{"running", "queued"}).Distinct("user_id").Count(&activeUsers)
// 今日各平台注册数
type platformStat struct {
Platform string `json:"platform"`
Count int64 `json:"count"`
}
var todayStats []platformStat
h.db.Model(&model.TaskResult{}).
Where("created_at >= ?", resetTime).
Select("platform, COUNT(*) as count").
Group("platform").
Scan(&todayStats)
todayTotal := int64(0)
todayPlatforms := map[string]int64{}
for _, ps := range todayStats {
todayPlatforms[ps.Platform] = ps.Count
todayTotal += ps.Count
}
// 今日成功率
var todaySuccess, todayFail int64
h.db.Model(&model.Task{}).Where("created_at >= ?", resetTime).
Select("COALESCE(SUM(success_count), 0)").Scan(&todaySuccess)
h.db.Model(&model.Task{}).Where("created_at >= ?", resetTime).
Select("COALESCE(SUM(fail_count), 0)").Scan(&todayFail)
var successRate float64
if todaySuccess+todayFail > 0 {
successRate = float64(todaySuccess) / float64(todaySuccess+todayFail) * 100
}
// 总注册数
var totalResults int64
h.db.Model(&model.TaskResult{}).Count(&totalResults)
return gin.H{
"running_tasks": runningTasks,
"queued_tasks": queuedTasks,
"active_users": activeUsers,
"today_total": todayTotal,
"today_platforms": todayPlatforms,
"today_success": todaySuccess,
"today_fail": todayFail,
"success_rate": successRate,
"total_results": totalResults,
}
}
// buildRecentCompletions 构建最近完成的注册列表(全站公共展示)
// 每用户只展示最新一条,按完成时间倒序,最多 10 条
func (h *InitHandler) buildRecentCompletions() []gin.H {
var tasks []model.Task
h.db.Raw(`
SELECT * FROM (
SELECT DISTINCT ON (user_id) *
FROM tasks
WHERE status IN ('completed', 'stopped')
AND success_count > 0
AND stopped_at IS NOT NULL
ORDER BY user_id, stopped_at DESC
) sub
ORDER BY stopped_at DESC
LIMIT 10
`).Scan(&tasks)
userIDs := make([]uint, 0, len(tasks))
for _, t := range tasks {
userIDs = append(userIDs, t.UserID)
}
type userInfo struct {
ID uint
Username string
Name string
AvatarURL string
}
userInfoMap := make(map[uint]userInfo)
if len(userIDs) > 0 {
var users []model.User
h.db.Where("id IN ?", userIDs).Select("id, username, name, avatar_url").Find(&users)
for _, u := range users {
userInfoMap[u.ID] = userInfo{ID: u.ID, Username: u.Username, Name: u.Name, AvatarURL: u.AvatarURL}
}
}
items := make([]gin.H, 0, len(tasks))
for _, t := range tasks {
ts := int64(0)
if t.StoppedAt != nil {
ts = t.StoppedAt.Unix()
}
durationSec := 0
if t.StartedAt != nil && t.StoppedAt != nil {
durationSec = int(t.StoppedAt.Sub(*t.StartedAt).Seconds())
}
ui := userInfoMap[t.UserID]
items = append(items, gin.H{
"task_id": t.ID,
"username": ui.Username,
"name": ui.Name,
"avatar_url": ui.AvatarURL,
"platform": t.Platform,
"success_count": t.SuccessCount,
"target_count": t.TargetCount,
"duration_sec": durationSec,
"stopped_at": ts,
})
}
return items
}
// RecentCompletions 最近完成的注册(GET /api/stats/recent-completions)
// 全站公共展示,返回最新 10 条成功的注册任务
func (h *InitHandler) RecentCompletions(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"items": h.buildRecentCompletions()})
}
// buildInitPlatformPrices 构建各平台单价映射(Init 接口用)
func buildInitPlatformPrices(settingSvc *service.SettingService, globalCost float64) map[string]float64 {
prices := map[string]float64{}
for _, p := range []string{"grok", "openai", "kiro", "gemini"} {
key := "platform_" + p + "_unit_price"
val := settingSvc.Get(key, "")
if val != "" {
if parsed, err := strconv.ParseFloat(val, 64); err == nil && parsed > 0 {
prices[p] = parsed
continue
}
}
prices[p] = globalCost
}
return prices
}