package extension import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "io" "strconv" "strings" "time" "verification-platform-backend/internal/database" "verification-platform-backend/internal/model" "verification-platform-backend/internal/service" "verification-platform-backend/pkg/response" "github.com/gin-gonic/gin" ) func SetupRoutes(r *gin.RouterGroup) { ext := r.Group("/ext") ext.Use(ExtensionAuthMiddleware()) { // 用户相关 ext.GET("/user/:userId", handleGetUser) ext.GET("/users", handleGetUsers) ext.POST("/user/:userId/recharge", handleRechargeUser) ext.POST("/user/:userId/deduct", handleDeductUser) // 用户变量相关 ext.GET("/user/:userId/variables", handleGetUserVariables) ext.POST("/user/:userId/variables", handleUpdateUserVariables) // 卡密相关 ext.GET("/cards", handleGetCards) ext.POST("/cards/generate", handleGenerateCards) ext.GET("/card/:cardId", handleGetCard) // 通知相关 ext.POST("/notification", handleSendNotification) ext.POST("/notification/batch", handleSendBatchNotification) // 应用信息 ext.GET("/app/info", handleGetAppInfo) ext.GET("/app/stats", handleGetAppStats) // 应用变量相关 ext.GET("/app/variables", handleGetAppVariables) ext.POST("/app/variables", handleUpdateAppVariables) } } func ExtensionAuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { accessKey := c.GetHeader("X-Access-Key") signature := c.GetHeader("X-Signature") timestamp := c.GetHeader("X-Timestamp") if accessKey == "" || signature == "" || timestamp == "" { response.Error(c, 401, "缺少认证信息") c.Abort() return } ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { response.Error(c, 401, "时间戳格式错误") c.Abort() return } if time.Now().Unix()-ts > 300 { response.Error(c, 401, "请求已过期") c.Abort() return } var apiKey model.ExtensionAPIKey if err := database.DB.Where("access_key = ? AND status = ?", accessKey, "active"). Preload("Application").First(&apiKey).Error; err != nil { response.Error(c, 401, "API密钥无效") c.Abort() return } if apiKey.ExpiresAt != nil && apiKey.ExpiresAt.Before(time.Now()) { response.Error(c, 401, "API密钥已过期") c.Abort() return } bodyBytes, _ := io.ReadAll(c.Request.Body) c.Set("requestBody", bodyBytes) c.Request.Body = io.NopCloser(strings.NewReader(string(bodyBytes))) stringToSign := fmt.Sprintf("%s%s%s%s", c.Request.Method, c.Request.URL.Path, timestamp, string(bodyBytes)) expectedSignature := generateSignature(apiKey.SecretKey, stringToSign) if !hmac.Equal([]byte(signature), []byte(expectedSignature)) { response.Error(c, 401, "签名验证失败") c.Abort() return } c.Set("apiKey", apiKey) c.Set("applicationID", apiKey.ApplicationID) var app model.Application if err := database.DB.First(&app, apiKey.ApplicationID).Error; err == nil { c.Set("user_id", app.UserID) c.Set("app_id", apiKey.ApplicationID) var user model.User if err := database.DB.Preload("CurrentPackage").First(&user, app.UserID).Error; err == nil { if user.CurrentPackageID != nil { var permission model.PackagePermission if err := database.DB.Where("package_id = ?", user.CurrentPackageID).First(&permission).Error; err == nil { now := time.Now() if user.ApiCallsResetAt == nil || now.Sub(*user.ApiCallsResetAt) >= 24*time.Hour { user.ApiCallsUsed = 0 user.ApiCallsResetAt = &now database.DB.Save(&user) } if user.ApiCallsUsed >= permission.MaxApiCalls { response.Error(c, 403, fmt.Sprintf("API调用次数已达上限(%d次/天),请升级套餐", permission.MaxApiCalls)) c.Abort() return } } } } } now := time.Now() database.DB.Model(&apiKey).Update("last_used_at", now) c.Next() if _, exists := c.Get("app_id"); exists { if userID, exists := c.Get("user_id"); exists { var user model.User if err := database.DB.First(&user, userID).Error; err == nil { user.ApiCallsUsed++ database.DB.Save(&user) } } } } } func generateSignature(secretKey, data string) string { h := hmac.New(sha256.New, []byte(secretKey)) h.Write([]byte(data)) return hex.EncodeToString(h.Sum(nil)) } func handleGetUser(c *gin.Context) { appID := c.GetUint("applicationID") userID := c.Param("userId") var user model.AppUser if err := database.DB.Where("id = ? AND application_id = ?", userID, appID).First(&user).Error; err != nil { response.Error(c, 404, "用户不存在") return } response.Success(c, gin.H{ "id": user.ID, "username": user.Username, "email": user.Email, "status": user.Status, "balance": user.Balance, "expiry_at": user.ExpiryAt, "lastLoginAt": user.LastLoginAt, "lastHeartbeatAt": user.LastHeartbeatAt, "isTrialUser": user.IsTrialUser, "createdAt": user.CreatedAt, }) } func handleGetUsers(c *gin.Context) { appID := c.GetUint("applicationID") page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) status := c.Query("status") search := c.Query("search") var users []model.AppUser var total int64 query := database.DB.Model(&model.AppUser{}).Where("application_id = ?", appID) if status != "" { query = query.Where("status = ?", status) } if search != "" { query = query.Where("username LIKE ? OR email LIKE ?", "%"+search+"%", "%"+search+"%") } query.Count(&total) offset := (page - 1) * pageSize if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&users).Error; err != nil { response.Error(c, 500, "获取用户列表失败") return } response.Success(c, gin.H{ "users": users, "total": total, "page": page, "page_size": pageSize, "total_page": (total + int64(pageSize) - 1) / int64(pageSize), }) } func handleRechargeUser(c *gin.Context) { appID := c.GetUint("applicationID") userID := c.Param("userId") var req struct { Amount int `json:"amount" binding:"required"` Type string `json:"type" binding:"required"` // balance, days Description string `json:"description"` } if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, 400, "参数错误: "+err.Error()) return } var app model.Application if err := database.DB.First(&app, appID).Error; err != nil { response.Error(c, 404, "应用不存在") return } var user model.AppUser if err := database.DB.Where("id = ? AND application_id = ?", userID, appID).First(&user).Error; err != nil { service.LogVerification(c, &app.ID, nil, "extension_recharge_failed", fmt.Sprintf("扩展API充值失败: 用户不存在 - %s", userID), "", fmt.Errorf("用户不存在")) response.Error(c, 404, "用户不存在") return } if user.Balance == -1 { service.LogVerification(c, &app.ID, &user.ID, "extension_recharge_failed", fmt.Sprintf("扩展API充值失败: 用户已是永久会员 - %s", user.Username), "", fmt.Errorf("该用户为永久会员,无需充值")) response.Error(c, 400, "该用户为永久会员,无需充值") return } now := time.Now() switch req.Type { case "days": var baseTime time.Time if user.ExpiryAt != nil && user.ExpiryAt.After(now) { baseTime = *user.ExpiryAt } else { baseTime = now } duration := time.Duration(req.Amount) * 24 * time.Hour newExpiry := baseTime.Add(duration) user.ExpiryAt = &newExpiry case "balance": user.Balance += float64(req.Amount) default: service.LogVerification(c, &app.ID, &user.ID, "extension_recharge_failed", fmt.Sprintf("扩展API充值失败: 类型无效 - %s", req.Type), "", fmt.Errorf("充值类型无效")) response.Error(c, 400, "充值类型无效,仅支持days或balance类型") return } if err := database.DB.Save(&user).Error; err != nil { service.LogVerification(c, &app.ID, &user.ID, "extension_recharge_failed", fmt.Sprintf("扩展API充值失败: 保存失败 - %s", user.Username), "", err) response.Error(c, 500, "充值失败") return } record := model.RechargeRecord{ UserID: user.ID, OrderNo: fmt.Sprintf("EXT%d%d", time.Now().Unix(), user.ID), Amount: float64(req.Amount), Status: "success", PaymentType: "extension_api", Remark: req.Description, } database.DB.Create(&record) service.LogVerification(c, &app.ID, &user.ID, "extension_recharge", fmt.Sprintf("扩展API充值: 用户%s, 类型:%s, 数量:%d", user.Username, req.Type, req.Amount), "", nil) response.Success(c, gin.H{ "message": "充值成功", "user": user, }) } func handleDeductUser(c *gin.Context) { appID := c.GetUint("applicationID") userID := c.Param("userId") var req struct { Amount int `json:"amount" binding:"required"` Type string `json:"type" binding:"required"` // balance, days Description string `json:"description"` } if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, 400, "参数错误: "+err.Error()) return } var app model.Application database.DB.First(&app, appID) var user model.AppUser if err := database.DB.Where("id = ? AND application_id = ?", userID, appID).First(&user).Error; err != nil { service.LogVerification(c, &app.ID, nil, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 用户不存在 - %s", userID), "", fmt.Errorf("用户不存在")) response.Error(c, 404, "用户不存在") return } if user.Balance == -1 { service.LogVerification(c, &app.ID, &user.ID, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 用户已是永久会员 - %s", user.Username), "", fmt.Errorf("该用户为永久会员,无法扣除")) response.Error(c, 400, "该用户为永久会员,无法扣除") return } now := time.Now() switch req.Type { case "days": if user.ExpiryAt == nil || user.ExpiryAt.Before(now) { service.LogVerification(c, &app.ID, &user.ID, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 用户订阅已过期 - %s", user.Username), "", fmt.Errorf("用户订阅已过期")) response.Error(c, 400, "用户订阅已过期") return } duration := time.Duration(req.Amount) * 24 * time.Hour newExpiry := user.ExpiryAt.Add(-duration) if newExpiry.Before(now) { newExpiry = now } user.ExpiryAt = &newExpiry case "balance": if user.Balance < float64(req.Amount) { service.LogVerification(c, &app.ID, &user.ID, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 余额不足 - %s", user.Username), "", fmt.Errorf("余额不足")) response.Error(c, 400, "余额不足") return } user.Balance -= float64(req.Amount) default: service.LogVerification(c, &app.ID, &user.ID, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 类型无效 - %s", req.Type), "", fmt.Errorf("扣除类型无效")) response.Error(c, 400, "扣除类型无效,仅支持days或balance类型") return } if err := database.DB.Save(&user).Error; err != nil { service.LogVerification(c, &app.ID, &user.ID, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 保存失败 - %s", user.Username), "", err) response.Error(c, 500, "扣除失败") return } record := model.ConsumptionRecord{ UserID: user.ID, OrderNo: fmt.Sprintf("EXT%d%d", time.Now().Unix(), user.ID), Type: req.Type, Amount: float64(req.Amount), Status: "success", PaymentType: "extension_api", Remark: req.Description, } database.DB.Create(&record) service.LogVerification(c, &app.ID, &user.ID, "extension_deduct", fmt.Sprintf("扩展API扣费: 用户%s, 类型:%s, 数量:%d", user.Username, req.Type, req.Amount), "", nil) response.Success(c, gin.H{ "message": "扣除成功", "user": user, }) } func handleGetUserVariables(c *gin.Context) { appID := c.GetUint("applicationID") userID := c.Param("userId") var user model.AppUser if err := database.DB.Where("id = ? AND application_id = ?", userID, appID).First(&user).Error; err != nil { response.Error(c, 404, "用户不存在") return } var variables []model.CloudVariable if err := database.DB.Where("app_id = ?", appID).Find(&variables).Error; err != nil { response.Error(c, 500, "获取变量定义失败") return } var userVariables []model.UserVariable if err := database.DB.Where("user_id = ? AND app_id = ?", userID, appID).Find(&userVariables).Error; err != nil { response.Error(c, 500, "获取用户变量失败") return } userVarMap := make(map[string]string) for _, uv := range userVariables { userVarMap[uv.VarName] = uv.VarValue } result := make(map[string]interface{}) for _, v := range variables { if v.Scope == "app" { result[v.Key] = gin.H{ "value": v.DefaultValue, "scope": "app", } } else { value := v.DefaultValue if uv, ok := userVarMap[v.Key]; ok { value = uv } result[v.Key] = gin.H{ "value": value, "scope": "user", } } } response.Success(c, result) } func handleUpdateUserVariables(c *gin.Context) { appID := c.GetUint("applicationID") userID := c.Param("userId") var user model.AppUser if err := database.DB.Where("id = ? AND application_id = ?", userID, appID).First(&user).Error; err != nil { response.Error(c, 404, "用户不存在") return } var req struct { Variables map[string]string `json:"variables"` } if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, 400, "参数错误") return } var variables []model.CloudVariable if err := database.DB.Where("app_id = ?", appID).Find(&variables).Error; err != nil { response.Error(c, 500, "获取变量定义失败") return } varMap := make(map[string]model.CloudVariable) for _, v := range variables { varMap[v.Key] = v } for key, value := range req.Variables { variable, exists := varMap[key] if !exists { continue } if variable.Scope == "app" { variable.DefaultValue = value database.DB.Save(&variable) } else { var userVar model.UserVariable if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, appID, key).First(&userVar).Error; err != nil { userVar = model.UserVariable{ UserID: user.ID, AppID: appID, VarName: key, VarValue: value, VarType: variable.VarType, } database.DB.Create(&userVar) } else { userVar.VarValue = value database.DB.Save(&userVar) } } } response.Success(c, gin.H{ "message": "更新成功", }) } func handleGetAppVariables(c *gin.Context) { appID := c.GetUint("applicationID") var variables []model.CloudVariable if err := database.DB.Where("app_id = ? AND scope = ?", appID, "app").Find(&variables).Error; err != nil { response.Error(c, 500, "获取应用变量失败") return } result := make(map[string]string) for _, v := range variables { result[v.Key] = v.DefaultValue } response.Success(c, result) } func handleUpdateAppVariables(c *gin.Context) { appID := c.GetUint("applicationID") var req struct { Variables map[string]string `json:"variables"` } if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, 400, "参数错误") return } for key, value := range req.Variables { var variable model.CloudVariable if err := database.DB.Where("app_id = ? AND key = ? AND scope = ?", appID, key, "app").First(&variable).Error; err == nil { variable.DefaultValue = value database.DB.Save(&variable) } } response.Success(c, gin.H{ "message": "更新成功", }) } func handleGetCards(c *gin.Context) { appID := c.GetUint("applicationID") page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) status := c.Query("status") cardTypeID := c.Query("card_type_id") var cards []model.Card var total int64 query := database.DB.Model(&model.Card{}).Where("application_id = ?", appID) if status != "" { query = query.Where("status = ?", status) } if cardTypeID != "" { query = query.Where("card_type_id = ?", cardTypeID) } query.Count(&total) offset := (page - 1) * pageSize if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&cards).Error; err != nil { response.Error(c, 500, "获取卡密列表失败") return } response.Success(c, gin.H{ "cards": cards, "total": total, "page": page, "page_size": pageSize, "total_page": (total + int64(pageSize) - 1) / int64(pageSize), }) } func handleGenerateCards(c *gin.Context) { appID := c.GetUint("applicationID") var req struct { CardTypeID uint `json:"card_type_id" binding:"required"` Count int `json:"count" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, 400, "参数错误: "+err.Error()) return } var cardType model.CardType if err := database.DB.Where("id = ? AND application_id = ?", req.CardTypeID, appID).First(&cardType).Error; err != nil { response.Error(c, 404, "卡密类型不存在") return } cards := make([]model.Card, req.Count) for i := 0; i < req.Count; i++ { cardKey := generateCardKey() cards[i] = model.Card{ ApplicationID: appID, CardTypeID: req.CardTypeID, CardKey: cardKey, Status: "unused", } } if err := database.DB.Create(&cards).Error; err != nil { response.Error(c, 500, "生成卡密失败") return } response.Success(c, gin.H{ "message": "生成成功", "count": req.Count, "cards": cards, }) } func handleGetCard(c *gin.Context) { appID := c.GetUint("applicationID") cardID := c.Param("cardId") var card model.Card if err := database.DB.Where("id = ? AND application_id = ?", cardID, appID).First(&card).Error; err != nil { response.Error(c, 404, "卡密不存在") return } response.Success(c, card) } func handleSendNotification(c *gin.Context) { appID := c.GetUint("applicationID") var req struct { UserID uint `json:"user_id" binding:"required"` Title string `json:"title" binding:"required"` Content string `json:"content" binding:"required"` Type string `json:"type"` } if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, 400, "参数错误: "+err.Error()) return } var user model.AppUser if err := database.DB.Where("id = ? AND application_id = ?", req.UserID, appID).First(&user).Error; err != nil { response.Error(c, 404, "用户不存在") return } notification := model.Announcement{ ApplicationID: appID, Title: req.Title, Content: req.Content, Type: req.Type, Status: "active", } if err := database.DB.Create(¬ification).Error; err != nil { response.Error(c, 500, "发送通知失败") return } response.Success(c, gin.H{ "message": "发送成功", "notification": notification, }) } func handleSendBatchNotification(c *gin.Context) { appID := c.GetUint("applicationID") var req struct { Title string `json:"title" binding:"required"` Content string `json:"content" binding:"required"` Type string `json:"type"` } if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, 400, "参数错误: "+err.Error()) return } notification := model.Announcement{ ApplicationID: appID, Title: req.Title, Content: req.Content, Type: req.Type, Status: "active", } if err := database.DB.Create(¬ification).Error; err != nil { response.Error(c, 500, "发送通知失败") return } response.Success(c, gin.H{ "message": "发送成功", "notification": notification, }) } func handleGetAppInfo(c *gin.Context) { appID := c.GetUint("applicationID") var app model.Application if err := database.DB.First(&app, appID).Error; err != nil { response.Error(c, 404, "应用不存在") return } response.Success(c, gin.H{ "id": app.ID, "name": app.Name, "description": app.Description, "billing_type": app.BillingType, "encrypt_type": app.EncryptType, "bind_type": app.BindType, "max_devices": app.MaxDevices, "multi_open_mode": app.MultiOpenMode, "enable_trial": app.EnableTrial, "trial_balance": app.TrialBalance, "status": app.Status, "created_at": app.CreatedAt, }) } func handleGetAppStats(c *gin.Context) { appID := c.GetUint("applicationID") var userCount, activeUserCount, cardCount, usedCardCount int64 database.DB.Model(&model.AppUser{}).Where("application_id = ?", appID).Count(&userCount) database.DB.Model(&model.AppUser{}).Where("application_id = ? AND status = ?", appID, "active").Count(&activeUserCount) database.DB.Model(&model.Card{}).Where("application_id = ?", appID).Count(&cardCount) database.DB.Model(&model.Card{}).Where("application_id = ? AND status = ?", appID, "used").Count(&usedCardCount) response.Success(c, gin.H{ "user_count": userCount, "active_user_count": activeUserCount, "card_count": cardCount, "used_card_count": usedCardCount, }) } func generateCardKey() string { const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" b := make([]byte, 16) for i := range b { b[i] = charset[i%len(charset)] } return string(b) }