Files
regplatformm/internal/handler/task_handler.go
T
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

456 lines
16 KiB
Go
Raw 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"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/xiaolajiaoyyds/regplatformm/internal/dto"
"github.com/xiaolajiaoyyds/regplatformm/internal/middleware"
"github.com/xiaolajiaoyyds/regplatformm/internal/model"
"github.com/xiaolajiaoyyds/regplatformm/internal/service"
"gorm.io/gorm"
)
// TaskHandler 任务处理器
type TaskHandler struct {
db *gorm.DB
engine *service.TaskEngine
creditSvc *service.CreditService
settingSvc *service.SettingService
}
// NewTaskHandler 创建任务处理器
func NewTaskHandler(db *gorm.DB, engine *service.TaskEngine, creditSvc *service.CreditService, settingSvc *service.SettingService) *TaskHandler {
return &TaskHandler{db: db, engine: engine, creditSvc: creditSvc, settingSvc: settingSvc}
}
// Create 创建任务(POST /api/tasks)
func (h *TaskHandler) Create(c *gin.Context) {
user := middleware.GetUser(c)
var req dto.CreateTaskReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
// ── 校验系统限制 ──────────────────────────────────────────────
// 平台开关
platformKey := "platform_" + req.Platform + "_enabled"
if h.settingSvc.Get(platformKey, "true") != "true" {
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("%s 平台注册已关闭", req.Platform)})
return
}
maxTarget, _ := strconv.Atoi(h.settingSvc.Get("max_target", "1000"))
maxThreads, _ := strconv.Atoi(h.settingSvc.Get("max_threads", "16"))
if maxTarget > 0 && req.Target > maxTarget {
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("注册数量不能超过 %d", maxTarget)})
return
}
// 自动计算并发线程数(用户不传或传 0 时,从配置读取阶梯)
if req.Threads <= 0 {
req.Threads = calcThreadsByTiers(h.settingSvc, req.Target)
}
if maxThreads > 0 && req.Threads > maxThreads {
req.Threads = maxThreads
}
// ── 校验每日注册上限(凌晨 6 点重置)──────────────────────────
dailyLimit, _ := strconv.Atoi(h.settingSvc.Get("daily_reg_limit", "0"))
if dailyLimit > 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)
if int(dailyUsed)+req.Target > dailyLimit {
remaining := dailyLimit - int(dailyUsed)
if remaining < 0 {
remaining = 0
}
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("今日注册已达上限(%d/%d),凌晨 6 点重置,剩余 %d 次", dailyUsed, dailyLimit, remaining)})
return
}
}
// ── 校验平台级注册上限(付费/免费均生效)──────────────────────
platformTaskLimit := h.settingSvc.GetInt("platform_"+req.Platform+"_task_limit", 0)
if platformTaskLimit > 0 && req.Target > platformTaskLimit {
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("%s 平台单任务最多注册 %d 个", req.Platform, platformTaskLimit)})
return
}
platformDailyLimit := h.settingSvc.GetInt("platform_"+req.Platform+"_daily_limit", 0)
if platformDailyLimit > 0 {
resetTime := dailyResetTime()
var platformDailyUsed int64
h.db.Model(&model.Task{}).
Where("user_id = ? AND platform = ? AND created_at >= ?", user.ID, req.Platform, resetTime).
Select("COALESCE(SUM(success_count), 0)").Scan(&platformDailyUsed)
if int(platformDailyUsed)+req.Target > platformDailyLimit {
remaining := platformDailyLimit - int(platformDailyUsed)
if remaining < 0 {
remaining = 0
}
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("%s 今日注册已达上限(%d/%d),凌晨 6 点重置,剩余 %d 次", req.Platform, platformDailyUsed, platformDailyLimit, remaining)})
return
}
}
// 判断平台是否限时免费
platformFree := isPlatformFree(h.settingSvc, req.Platform)
// ── 免费模式 vs 付费模式判定 ──────────────────────────────────────
// mode: "free" = 用户选择免费(受限),"paid" = 用户选择付费(无限制),空 = 自动
useFreeTier := false
if platformFree {
if req.Mode == "paid" {
// 用户主动选择付费,走正常积分流程
useFreeTier = false
} else {
// mode=="free" 或空值:尝试使用免费额度
if err := h.validateFreeMode(h.db, user.ID, req.Platform, req.Target); err != nil {
if req.Mode == "free" {
// 用户明确要求免费但不满足条件,拒绝
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
// mode 为空(自动模式):免费不够用,降级到付费
useFreeTier = false
} else {
useFreeTier = true
}
}
}
// 创建任务记录
creditsReserved := req.Target
if useFreeTier {
creditsReserved = 0
}
// 查询用户指定的代理
var proxyURL string
if req.ProxyID > 0 {
var userProxy model.UserProxy
if err := h.db.Where("id = ? AND user_id = ?", req.ProxyID, user.ID).First(&userProxy).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "指定的代理不存在"})
return
}
proxyURL = userProxy.URL()
}
task := model.Task{
UserID: user.ID,
Platform: req.Platform,
Status: "pending",
TargetCount: req.Target,
ThreadCount: req.Threads,
CreditsReserved: creditsReserved,
ProxyURL: proxyURL,
}
tx := h.db.Begin()
if err := tx.Create(&task).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "任务创建失败"})
return
}
// 预扣积分(免费模式跳过)
if !useFreeTier {
if err := h.creditSvc.ReserveCredits(tx, user.ID, task.ID, req.Target); err != nil {
tx.Rollback()
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
}
if err := tx.Commit().Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "任务提交失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"task_id": task.ID,
"platform": task.Platform,
"target": task.TargetCount,
"threads": task.ThreadCount,
"status": task.Status,
})
}
// Start 启动任务(POST /api/tasks/:id/start)
func (h *TaskHandler) Start(c *gin.Context) {
user := middleware.GetUser(c)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "无效的任务 ID"})
return
}
var task model.Task
if err := h.db.Where("id = ? AND user_id = ?", taskID, user.ID).First(&task).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "任务不存在"})
return
}
if task.Status != "pending" && task.Status != "stopped" {
c.JSON(http.StatusBadRequest, gin.H{"detail": "任务状态不允许启动"})
return
}
// 快速同步检查:该平台是否已有运行中任务(避免长阻塞后才报错)
if rt := h.engine.GetStatus(user.ID, task.Platform); rt != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "该平台已有运行中的任务"})
return
}
// StartTask 快速返回排队/启动状态,重量级操作在内部 goroutine 执行
ok, msg := h.engine.StartTask(user.ID, task.ID, task.Platform, task.TargetCount, task.ThreadCount)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"detail": msg})
return
}
c.JSON(http.StatusOK, gin.H{"message": msg, "task_id": task.ID})
}
// Stop 停止任务(POST /api/tasks/:id/stop)
// 支持停止运行中 (running) 和排队中 (queued/pending) 的任务
func (h *TaskHandler) Stop(c *gin.Context) {
user := middleware.GetUser(c)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "无效的任务 ID"})
return
}
var task model.Task
if err := h.db.Where("id = ? AND user_id = ?", taskID, user.ID).First(&task).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "任务不存在"})
return
}
// 运行中任务:走引擎停止流程
if h.engine.StopTask(user.ID, task.Platform) {
c.JSON(http.StatusOK, gin.H{"message": "任务停止中"})
return
}
// 排队中 / pending 任务:直接在 DB 取消 + 退款
if task.Status == "queued" || task.Status == "pending" {
if err := 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)
// 退还预扣积分
if fresh.CreditsReserved > 0 {
h.creditSvc.RefundCredits(tx, user.ID, fresh.ID, int(fresh.CreditsReserved))
}
return nil
}); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "任务状态已变更,请刷新页面"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "排队已取消,积分已退还"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"detail": "没有运行中的任务"})
}
// Current 获取当前运行中任务(GET /api/tasks/current?platform=)
func (h *TaskHandler) Current(c *gin.Context) {
user := middleware.GetUser(c)
platform := c.Query("platform")
// 查询数据库中的运行中/排队任务
query := h.db.Where("user_id = ? AND status IN ?", user.ID, []string{"running", "stopping", "queued", "pending"})
if platform != "" {
query = query.Where("platform = ?", platform)
}
var task model.Task
if err := query.Order("created_at DESC").First(&task).Error; err != nil {
c.JSON(http.StatusOK, nil)
return
}
// 检查孤儿任务(DB 显示 running/stopping 但内存无对应任务)—— 事务保护防双倍退款
if (task.Status == "running" || task.Status == "stopping") && 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 IN ?", task.ID, []string{"running", "stopping"}).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
})
}
resp := dto.TaskStatus{
TaskID: task.ID,
Platform: task.Platform,
Target: task.TargetCount,
Threads: task.ThreadCount,
CreditsReserved: task.CreditsReserved,
SuccessCount: task.SuccessCount,
FailCount: task.FailCount,
Status: task.Status,
IsDone: task.IsDone(),
}
// 排队中任务:计算实时排队位置和预计等待时间
if task.Status == "queued" || task.Status == "pending" {
var queuePos int64
h.db.Model(&model.Task{}).
Where("status IN ? AND platform = ? AND created_at < ?", []string{"queued", "pending"}, task.Platform, task.CreatedAt).
Count(&queuePos)
queuePos++ // 自己也算一个位置
resp.QueuePosition = int(queuePos)
resp.QueueWaitSec = h.engine.EstimateWaitTime(int(queuePos), task.Platform)
}
c.JSON(http.StatusOK, resp)
}
// History 历史任务(GET /api/tasks/history)
func (h *TaskHandler) History(c *gin.Context) {
user := middleware.GetUser(c)
var tasks []model.Task
h.db.Where("user_id = ?", user.ID).
Order("created_at DESC").
Limit(20).
Find(&tasks)
results := make([]dto.TaskStatus, 0, len(tasks))
for _, t := range tasks {
results = append(results, dto.TaskStatus{
TaskID: t.ID,
Platform: t.Platform,
Target: t.TargetCount,
Threads: t.ThreadCount,
CreditsReserved: t.CreditsReserved,
SuccessCount: t.SuccessCount,
FailCount: t.FailCount,
Status: t.Status,
IsDone: t.IsDone(),
})
}
c.JSON(http.StatusOK, results)
}
// calcThreadsByTiers 从配置读取线程阶梯,匹配目标数量返回线程数
func calcThreadsByTiers(settingSvc *service.SettingService, target int) int {
tiersJSON := settingSvc.Get("thread_tiers", "")
if tiersJSON != "" {
var tiers []struct {
Max int `json:"max"`
Threads int `json:"threads"`
}
if err := json.Unmarshal([]byte(tiersJSON), &tiers); err == nil && len(tiers) > 0 {
for _, tier := range tiers {
if target <= tier.Max {
return tier.Threads
}
}
// 超过所有阶梯,使用最后一档
return tiers[len(tiers)-1].Threads
}
}
// 降级:硬编码默认值
switch {
case target <= 5:
return 1
case target <= 20:
return 2
case target <= 100:
return 3
case target <= 300:
return 5
case target <= 500:
return 8
default:
return 12
}
}
// isPlatformFree 判断平台是否在限时免费期内
func isPlatformFree(settingSvc *service.SettingService, platform string) bool {
key := "platform_" + platform + "_free_until"
dateStr := settingSvc.Get(key, "")
if dateStr == "" {
return false
}
deadline, err := time.Parse("2006-01-02", dateStr)
if err != nil {
return false
}
// 截止日当天全天有效(截止日+1天的零点之前)
return time.Now().Before(deadline.AddDate(0, 0, 1))
}
// validateFreeMode 校验免费模式的三重限制:单任务上限、每日上限、冷却时间
// 冷却逻辑:上一轮免费任务用满 task_limit → 触发冷却;没用满 → 不冷却
// 返回 nil 表示可以使用免费模式,返回 error 说明原因
func (h *TaskHandler) validateFreeMode(db *gorm.DB, userID uint, platform string, target int) error {
prefix := "platform_" + platform + "_free_"
// ── 1. 单任务上限 ────────────────────────────────────────────────
taskLimit := h.settingSvc.GetInt(prefix+"task_limit", 2)
if taskLimit > 0 && target > taskLimit {
return fmt.Errorf("免费模式单次最多注册 %d 个,如需更多请切换付费模式", taskLimit)
}
// ── 2. 每日上限(凌晨 6 点重置)────────────────────────────────────
dailyLimit := h.settingSvc.GetInt(prefix+"daily_limit", 5)
resetTime := dailyResetTime()
var dailyFreeUsed int64
db.Model(&model.Task{}).
Where("user_id = ? AND platform = ? AND credits_reserved = 0 AND created_at >= ?", userID, platform, resetTime).
Select("COALESCE(SUM(target_count), 0)").Scan(&dailyFreeUsed)
if dailyLimit > 0 && int(dailyFreeUsed)+target > dailyLimit {
remaining := dailyLimit - int(dailyFreeUsed)
if remaining < 0 {
remaining = 0
}
return fmt.Errorf("今日免费额度已用 %d/%d,剩余 %d 次,如需更多请切换付费模式", dailyFreeUsed, dailyLimit, remaining)
}
// ── 3. 冷却:上一轮用满 task_limit 才触发,没用满不冷却 ──────────────
cooldownMin := h.settingSvc.GetInt(prefix+"cooldown", 30)
if cooldownMin > 0 && taskLimit > 0 {
var lastTask model.Task
err := db.Where("user_id = ? AND platform = ? AND credits_reserved = 0 AND stopped_at IS NOT NULL", userID, platform).
Order("stopped_at DESC").First(&lastTask).Error
if err == nil && lastTask.StoppedAt != nil && lastTask.TargetCount >= taskLimit {
// 上一轮用满了 task_limit → 检查冷却窗口
cooldownEnd := lastTask.StoppedAt.Add(time.Duration(cooldownMin) * time.Minute)
if time.Now().Before(cooldownEnd) {
remainingMin := int(time.Until(cooldownEnd).Minutes()) + 1
return fmt.Errorf("上一轮免费注册已用满 %d 个,冷却中还需 %d 分钟,或切换付费模式", taskLimit, remainingMin)
}
}
}
return nil
}