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

364 lines
12 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 (
"fmt"
"math"
"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"
)
// CreditHandler 积分处理器
type CreditHandler struct {
db *gorm.DB
creditSvc *service.CreditService
settingSvc *service.SettingService
}
// NewCreditHandler 创建积分处理器
func NewCreditHandler(db *gorm.DB, creditSvc *service.CreditService, settingSvc *service.SettingService) *CreditHandler {
return &CreditHandler{db: db, creditSvc: creditSvc, settingSvc: settingSvc}
}
// dailyResetTime 返回今天的重置时间点(凌晨 6 点)
// 如果当前时间在 6 点之前,则返回昨天 6 点
func dailyResetTime() time.Time {
now := time.Now()
reset := time.Date(now.Year(), now.Month(), now.Day(), 6, 0, 0, 0, now.Location())
if now.Before(reset) {
reset = reset.AddDate(0, 0, -1)
}
return reset
}
// Balance 查询余额(GET /api/credits/balance)
func (h *CreditHandler) Balance(c *gin.Context) {
user := middleware.GetUser(c)
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"))
// 计算今日已注册数量(从凌晨 6 点算起)
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 // -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)
}
}
c.JSON(http.StatusOK, dto.BalanceResp{
Credits: credits,
FreeTrialRemaining: freeRemaining,
FreeTrialUsed: freeUsed,
CostPerReg: 1,
Mode: mode,
Display: display,
RegistrationsAvailable: registrationsAvailable,
NewapiBalance: newapiBalance,
NewapiBalanceDisplay: newapiBalanceDisplay,
UnitPrice: unitPrice,
UnitPriceDisplay: unitPriceDisplay,
NewapiAvailable: newapiAvailable,
PlatformPrices: buildPlatformPrices(h.settingSvc, costUSD),
FreeTrial: &dto.FreeTrialResp{
Eligible: trialEligible,
Remaining: trialRemaining,
Total: trialTotal,
},
Limits: &dto.LimitsResp{
MaxTarget: maxTarget,
MaxThreads: maxThreads,
DailyRegLimit: dailyLimit,
DailyUsed: int(dailyUsed),
DailyRemaining: dailyRemaining,
PlatformGrokEnabled: h.settingSvc.Get("platform_grok_enabled", "true") == "true",
PlatformOpenaiEnabled: h.settingSvc.Get("platform_openai_enabled", "true") == "true",
PlatformKiroEnabled: h.settingSvc.Get("platform_kiro_enabled", "true") == "true",
PlatformGeminiEnabled: h.settingSvc.Get("platform_gemini_enabled", "true") == "true",
ThreadTiers: h.settingSvc.Get("thread_tiers", ""),
PlatformGrokFreeUntil: h.settingSvc.Get("platform_grok_free_until", ""),
PlatformOpenaiFreeUntil: h.settingSvc.Get("platform_openai_free_until", ""),
PlatformKiroFreeUntil: h.settingSvc.Get("platform_kiro_free_until", ""),
PlatformGeminiFreeUntil: h.settingSvc.Get("platform_gemini_free_until", ""),
FreeMode: buildFreeModeStatus(h.db, h.settingSvc, user.ID),
PlatformLimits: buildPlatformLimits(h.db, h.settingSvc, user.ID),
},
})
}
// History 交易记录(GET /api/credits/history)
func (h *CreditHandler) History(c *gin.Context) {
user := middleware.GetUser(c)
var txs []model.CreditTransaction
h.db.Where("user_id = ?", user.ID).
Order("created_at DESC").
Limit(50).
Find(&txs)
c.JSON(http.StatusOK, txs)
}
// Redeem 兑换码(POST /api/credits/redeem)
func (h *CreditHandler) Redeem(c *gin.Context) {
user := middleware.GetUser(c)
var req dto.RedeemReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
credits, err := h.creditSvc.RedeemCode(h.db, user.ID, req.Code)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "兑换成功", "credits": credits})
}
// ClaimFreeTrial 领取免费试用(POST /api/credits/free-trial)
func (h *CreditHandler) ClaimFreeTrial(c *gin.Context) {
user := middleware.GetUser(c)
count, err := h.creditSvc.ClaimFreeTrial(h.db, user.ID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "领取成功", "count": count})
}
// Purchase 购买注册次数(POST /api/credits/purchase)
func (h *CreditHandler) Purchase(c *gin.Context) {
user := middleware.GetUser(c)
var req dto.PurchaseReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "请输入有效的购买数量(1-10000)"})
return
}
if err := h.creditSvc.PurchaseCredits(h.db, user.ID, req.Amount, req.Platform); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
// 返回更新后的余额(复用 Balance 逻辑)
h.Balance(c)
}
// buildPlatformPrices 构建各平台单价映射(优先用平台专属价,降级全局价)
func buildPlatformPrices(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
}
// buildFreeModeStatus 构建各平台的免费模式状态(仅限时免费期内的平台)
// 包级函数,供 CreditHandler 和 InitHandler 共用
func buildFreeModeStatus(db *gorm.DB, settingSvc *service.SettingService, userID uint) map[string]*dto.FreeModeInfo {
freeMode := map[string]*dto.FreeModeInfo{}
resetTime := dailyResetTime()
for _, platform := range []string{"grok", "openai", "kiro", "gemini"} {
if !isPlatformFreeByHandler(settingSvc, platform) {
continue
}
prefix := "platform_" + platform + "_free_"
dailyLimit, _ := strconv.Atoi(settingSvc.Get(prefix+"daily_limit", "5"))
taskLimit, _ := strconv.Atoi(settingSvc.Get(prefix+"task_limit", "2"))
cooldownMin, _ := strconv.Atoi(settingSvc.Get(prefix+"cooldown", "30"))
// 查今日免费使用量
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)
dailyRemaining := -1
if dailyLimit > 0 {
dailyRemaining = dailyLimit - int(dailyFreeUsed)
if dailyRemaining < 0 {
dailyRemaining = 0
}
}
// 查冷却剩余(上一轮用满 task_limit 才触发,没用满不冷却)
cooldownRemainingSec := 0
lastTaskMaxed := false
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 {
lastTaskMaxed = true
cooldownEnd := lastTask.StoppedAt.Add(time.Duration(cooldownMin) * time.Minute)
if time.Now().Before(cooldownEnd) {
cooldownRemainingSec = int(time.Until(cooldownEnd).Seconds())
}
}
}
// 判断是否可用
available := true
reason := ""
dailyLimitReached := dailyLimit > 0 && dailyRemaining <= 0
if dailyLimitReached {
// 今日额度用完
available = false
reason = "今日免费额度已用完"
} else if lastTaskMaxed && cooldownRemainingSec > 0 {
// 上一轮用满了 task_limit,冷却中
available = false
reason = fmt.Sprintf("上一轮已用满 %d 个,冷却中还需 %d 分钟", taskLimit, cooldownRemainingSec/60+1)
}
freeMode[platform] = &dto.FreeModeInfo{
Available: available,
DailyUsed: int(dailyFreeUsed),
DailyLimit: dailyLimit,
DailyRemaining: dailyRemaining,
TaskLimit: taskLimit,
CooldownSec: cooldownMin * 60,
CooldownRemaining: cooldownRemainingSec,
Reason: reason,
}
}
if len(freeMode) == 0 {
return nil
}
return freeMode
}
// buildPlatformLimits 构建各平台的独立限制(付费/免费均生效)
// 包级函数,供 CreditHandler 和 InitHandler 共用
func buildPlatformLimits(db *gorm.DB, settingSvc *service.SettingService, userID uint) map[string]*dto.PlatformLimitInfo {
result := map[string]*dto.PlatformLimitInfo{}
resetTime := dailyResetTime()
for _, platform := range []string{"grok", "openai", "kiro", "gemini"} {
taskLimit := settingSvc.GetInt("platform_"+platform+"_task_limit", 0)
dailyLimit := settingSvc.GetInt("platform_"+platform+"_daily_limit", 0)
// 两个都是 0 表示该平台没有独立限制,跳过
if taskLimit == 0 && dailyLimit == 0 {
continue
}
dailyUsed := 0
dailyRemaining := -1 // -1 = 不限
if dailyLimit > 0 {
var used int64
db.Model(&model.Task{}).
Where("user_id = ? AND platform = ? AND created_at >= ?", userID, platform, resetTime).
Select("COALESCE(SUM(success_count), 0)").Scan(&used)
dailyUsed = int(used)
dailyRemaining = dailyLimit - dailyUsed
if dailyRemaining < 0 {
dailyRemaining = 0
}
}
result[platform] = &dto.PlatformLimitInfo{
TaskLimit: taskLimit,
DailyLimit: dailyLimit,
DailyUsed: dailyUsed,
DailyRemaining: dailyRemaining,
}
}
if len(result) == 0 {
return nil
}
return result
}
// isPlatformFreeByHandler 判断平台是否限时免费(handler 层复用)
func isPlatformFreeByHandler(settingSvc *service.SettingService, platform string) bool {
dateStr := settingSvc.Get("platform_"+platform+"_free_until", "")
if dateStr == "" {
return false
}
deadline, err := time.Parse("2006-01-02", dateStr)
if err != nil {
return false
}
return time.Now().Before(deadline.AddDate(0, 0, 1))
}