多平台账号批量注册系统,支持 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
348 lines
11 KiB
Go
348 lines
11 KiB
Go
package handler
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/xiaolajiaoyyds/regplatformm/internal/middleware"
|
||
"github.com/xiaolajiaoyyds/regplatformm/internal/model"
|
||
"github.com/xiaolajiaoyyds/regplatformm/internal/pkg/tempmail"
|
||
"github.com/xiaolajiaoyyds/regplatformm/internal/service"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// resultItem 结果列表响应项(ListAll / ListArchived 共用)
|
||
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"`
|
||
Disabled bool `json:"disabled"`
|
||
DisabledReason string `json:"disabled_reason,omitempty"`
|
||
DisabledAt *string `json:"disabled_at,omitempty"`
|
||
LastValidatedAt *string `json:"last_validated_at,omitempty"`
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
|
||
// toResultItems 将 model.TaskResult 切片转换为 API 响应项
|
||
func toResultItems(results []model.TaskResult) []resultItem {
|
||
items := make([]resultItem, 0, len(results))
|
||
for _, r := range results {
|
||
item := resultItem{
|
||
ID: r.ID,
|
||
Email: r.Email,
|
||
Platform: r.Platform,
|
||
CredentialData: json.RawMessage(r.CredentialData),
|
||
SSOToken: r.SSOToken,
|
||
NsfwEnabled: r.NsfwEnabled,
|
||
Disabled: r.Disabled,
|
||
DisabledReason: r.DisabledReason,
|
||
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
|
||
}
|
||
if r.DisabledAt != nil {
|
||
s := r.DisabledAt.Format("2006-01-02 15:04:05")
|
||
item.DisabledAt = &s
|
||
}
|
||
if r.LastValidatedAt != nil {
|
||
s := r.LastValidatedAt.Format("2006-01-02 15:04:05")
|
||
item.LastValidatedAt = &s
|
||
}
|
||
items = append(items, item)
|
||
}
|
||
return items
|
||
}
|
||
|
||
// ResultHandler 结果处理器
|
||
type ResultHandler struct {
|
||
db *gorm.DB
|
||
settingSvc *service.SettingService
|
||
}
|
||
|
||
// NewResultHandler 创建结果处理器
|
||
func NewResultHandler(db *gorm.DB, settingSvc *service.SettingService) *ResultHandler {
|
||
return &ResultHandler{db: db, settingSvc: settingSvc}
|
||
}
|
||
|
||
// GetResults 获取任务结果(GET /api/results/:taskId)
|
||
func (h *ResultHandler) GetResults(c *gin.Context) {
|
||
user := middleware.GetUser(c)
|
||
taskID, err := strconv.ParseUint(c.Param("taskId"), 10, 32)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"detail": "无效的任务 ID"})
|
||
return
|
||
}
|
||
|
||
var results []model.TaskResult
|
||
if err := h.db.Where("task_id = ? AND user_id = ?", taskID, user.ID).
|
||
Order("created_at DESC").
|
||
Find(&results).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "查询失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, results)
|
||
}
|
||
|
||
// Export 导出结果(GET /api/results/:taskId/export)
|
||
func (h *ResultHandler) Export(c *gin.Context) {
|
||
user := middleware.GetUser(c)
|
||
taskID, err := strconv.ParseUint(c.Param("taskId"), 10, 32)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"detail": "无效的任务 ID"})
|
||
return
|
||
}
|
||
|
||
var results []model.TaskResult
|
||
if err := h.db.Where("task_id = ? AND user_id = ?", taskID, user.ID).Find(&results).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "查询失败"})
|
||
return
|
||
}
|
||
|
||
if len(results) == 0 {
|
||
c.JSON(http.StatusNotFound, gin.H{"detail": "没有结果可导出"})
|
||
return
|
||
}
|
||
|
||
// 判断平台,选择导出格式
|
||
platform := ""
|
||
if len(results) > 0 {
|
||
platform = results[0].Platform
|
||
}
|
||
|
||
var lines []string
|
||
for _, r := range results {
|
||
if platform == "grok" && r.SSOToken != "" {
|
||
lines = append(lines, r.SSOToken)
|
||
} else {
|
||
lines = append(lines, string(r.CredentialData))
|
||
}
|
||
}
|
||
|
||
content := strings.Join(lines, "\n")
|
||
// 清理文件名中的特殊字符,防止 Content-Disposition 头注入
|
||
safePlatform := strings.Map(func(r rune) rune {
|
||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
|
||
return r
|
||
}
|
||
return '_'
|
||
}, platform)
|
||
filename := fmt.Sprintf("task_%d_%s_results.txt", taskID, safePlatform)
|
||
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||
c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte(content))
|
||
}
|
||
|
||
// ListAll 所有未归档结果(GET /api/results?platform=&page=&page_size=)
|
||
// 支持分页:page 从 1 开始,page_size 默认 100,page_size=-1 返回全量(向后兼容)
|
||
func (h *ResultHandler) ListAll(c *gin.Context) {
|
||
user := middleware.GetUser(c)
|
||
platform := c.Query("platform")
|
||
|
||
base := h.db.Where("user_id = ? AND is_archived = ?", user.ID, false)
|
||
if platform != "" {
|
||
base = base.Where("platform = ?", platform)
|
||
}
|
||
|
||
// 总数(必须用 Session 克隆,避免 Count 污染后续查询的内部状态)
|
||
var total int64
|
||
base.Session(&gorm.Session{}).Model(&model.TaskResult{}).Count(&total)
|
||
|
||
// 分页参数
|
||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "100"))
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
|
||
query := base.Order("created_at DESC")
|
||
// page_size=-1 或不传 page 参数时向后兼容,返回全量
|
||
if pageSize > 0 {
|
||
query = query.Offset((page - 1) * pageSize).Limit(pageSize)
|
||
}
|
||
|
||
var results []model.TaskResult
|
||
query.Find(&results)
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"items": toResultItems(results),
|
||
"total": total,
|
||
"page": page,
|
||
"page_size": pageSize,
|
||
})
|
||
}
|
||
|
||
// ArchiveAll 归档结果(POST /api/results/archive)
|
||
// 支持 body {"platform":"grok"} 按平台归档,不传则归档全部
|
||
// 返回本次归档数量 archived_count
|
||
func (h *ResultHandler) ArchiveAll(c *gin.Context) {
|
||
user := middleware.GetUser(c)
|
||
|
||
var req struct {
|
||
Platform string `json:"platform"`
|
||
}
|
||
_ = c.ShouldBindJSON(&req)
|
||
|
||
query := h.db.Model(&model.TaskResult{}).
|
||
Where("user_id = ? AND is_archived = ?", user.ID, false)
|
||
if req.Platform != "" {
|
||
query = query.Where("platform = ?", req.Platform)
|
||
}
|
||
result := query.Update("is_archived", true)
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "已归档",
|
||
"archived_count": result.RowsAffected,
|
||
})
|
||
}
|
||
|
||
// ListArchived 已归档结果(GET /api/results/archived?platform=&page=&page_size=&count_only=true)
|
||
// count_only=true 时仅返回总数(不查具体数据,极快)
|
||
// 支持分页:page 从 1 开始,page_size 默认 50,page_size=-1 返回全量(导出用)
|
||
func (h *ResultHandler) ListArchived(c *gin.Context) {
|
||
user := middleware.GetUser(c)
|
||
platform := c.Query("platform")
|
||
|
||
base := h.db.Where("user_id = ? AND is_archived = ?", user.ID, true)
|
||
if platform != "" {
|
||
base = base.Where("platform = ?", platform)
|
||
}
|
||
|
||
// 总数(必须用 Session 克隆,避免 Count 污染后续查询的内部状态)
|
||
var total int64
|
||
base.Session(&gorm.Session{}).Model(&model.TaskResult{}).Count(&total)
|
||
|
||
// count_only 模式:只返回总数
|
||
if c.Query("count_only") == "true" {
|
||
c.JSON(http.StatusOK, gin.H{"total": total})
|
||
return
|
||
}
|
||
|
||
// 分页参数
|
||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
|
||
query := base.Order("created_at DESC")
|
||
if pageSize > 0 {
|
||
query = query.Offset((page - 1) * pageSize).Limit(pageSize)
|
||
}
|
||
|
||
var results []model.TaskResult
|
||
query.Find(&results)
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"items": toResultItems(results),
|
||
"total": total,
|
||
"page": page,
|
||
"page_size": pageSize,
|
||
})
|
||
}
|
||
|
||
// FetchOTP 获取邮箱验证码(GET /api/email/otp?email=xxx)
|
||
// 自动从数据库反查 provider 信息,前端只需传 email
|
||
func (h *ResultHandler) FetchOTP(c *gin.Context) {
|
||
email := c.Query("email")
|
||
if email == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 email 参数"})
|
||
return
|
||
}
|
||
|
||
// 优先从 query 参数获取 provider(向后兼容),否则从数据库反查
|
||
provider := c.Query("provider")
|
||
meta := map[string]string{"provider": provider}
|
||
|
||
if provider == "" {
|
||
// 从数据库查找该邮箱的最新注册结果,提取 email_provider 和 email_meta
|
||
var result model.TaskResult
|
||
if err := h.db.Where("email = ?", email).Order("created_at DESC").First(&result).Error; err == nil {
|
||
var credData map[string]interface{}
|
||
if err := json.Unmarshal(result.CredentialData, &credData); err == nil {
|
||
if ep, ok := credData["mail_provider"].(string); ok && ep != "" {
|
||
provider = ep
|
||
meta["provider"] = provider
|
||
} else if ep, ok := credData["provider"].(string); ok && ep != "" {
|
||
provider = ep
|
||
meta["provider"] = provider
|
||
} else if ep, ok := credData["email_provider"].(string); ok && ep != "" {
|
||
provider = ep
|
||
meta["provider"] = provider
|
||
}
|
||
if em, ok := credData["email_meta"].(map[string]interface{}); ok {
|
||
for k, v := range em {
|
||
if s, ok := v.(string); ok {
|
||
meta[k] = s
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 数据库也没有,按域名猜测
|
||
if provider == "" {
|
||
provider = detectProviderByDomain(email)
|
||
meta["provider"] = provider
|
||
}
|
||
} else {
|
||
// query 参数模式(向后兼容):从 URL 读取 meta 字段
|
||
meta["token"] = c.Query("token")
|
||
meta["sid_token"] = c.Query("sid_token")
|
||
meta["account_id"] = c.Query("account_id")
|
||
meta["base_url"] = c.Query("base_url")
|
||
meta["admin_token"] = c.Query("admin_token")
|
||
}
|
||
|
||
cfg := map[string]string{
|
||
"yydsmail_base_url": h.settingSvc.Get("yydsmail_base_url", ""),
|
||
"yydsmail_api_key": h.settingSvc.Get("yydsmail_api_key", ""),
|
||
}
|
||
|
||
if provider == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"detail": "无法识别邮箱 provider"})
|
||
return
|
||
}
|
||
if tempmail.NormalizeProviderNameForAPI(provider) == "yydsmail" && cfg["yydsmail_api_key"] == "" {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "YYDS Mail API Key 未配置"})
|
||
return
|
||
}
|
||
|
||
// 单次尝试获取验证码(不轮询,前端按需点击重试)
|
||
code, err := tempmail.FetchVerificationCodeByProvider(c.Request.Context(), provider, cfg, email, meta)
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": "", "error": "暂无验证码"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"code": code})
|
||
}
|
||
|
||
// detectProviderByDomain 根据邮箱域名自动识别 provider
|
||
func detectProviderByDomain(email string) string {
|
||
return "yydsmail"
|
||
}
|
||
|
||
// ReEnable 恢复被禁用的账号(POST /api/results/:id/re-enable)
|
||
func (h *ResultHandler) ReEnable(c *gin.Context) {
|
||
user := middleware.GetUser(c)
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"detail": "无效的 ID"})
|
||
return
|
||
}
|
||
|
||
result := h.db.Model(&model.TaskResult{}).
|
||
Where("id = ? AND user_id = ? AND disabled = ?", id, user.ID, true).
|
||
Updates(map[string]interface{}{
|
||
"disabled": false,
|
||
"disabled_reason": "",
|
||
"disabled_at": nil,
|
||
})
|
||
if result.RowsAffected == 0 {
|
||
c.JSON(http.StatusNotFound, gin.H{"detail": "账号不存在或未被禁用"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"message": "账号已恢复"})
|
||
}
|