Files
admin 086c5c6573 perf: 性能优化与错误修复
- 修复 N+1 查询: users/devices/agents 批量 GROUP BY 替代循环查询
- 添加分页: cards/finance/agents/devices API
- Redis 初始化根据安装配置 redis.enabled 决定是否连接
- 修复 DefaultVal 解析错误: 使用 sql.NullString 处理 NULL 值
- Dashboard 优化: 替换 ECharts 世界地图为 Chart.js 环形饼图
- 适配前端 cards 页面新 API 响应格式
- 添加数据库索引优化查询性能
- 实现可配置的数据清理定时任务
2026-05-06 19:19:02 +08:00

863 lines
24 KiB
Go

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"
"gorm.io/gorm"
)
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":
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Model(&model.AppUser{}).Where("id = ?", user.ID).
Update("balance", gorm.Expr("balance + ?", float64(req.Amount))).Error; err != nil {
tx.Rollback()
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,
}
if err := tx.Create(&record).Error; err != nil {
tx.Rollback()
response.Error(c, 500, "充值失败")
return
}
tx.Commit()
database.DB.Where("id = ?", user.ID).First(&user)
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,
})
return
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
}
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Save(&user).Error; err != nil {
tx.Rollback()
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,
}
if err := tx.Create(&record).Error; err != nil {
tx.Rollback()
response.Error(c, 500, "充值失败")
return
}
tx.Commit()
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
}
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
result := tx.Model(&model.AppUser{}).Where("id = ? AND balance >= ?", user.ID, float64(req.Amount)).
Update("balance", gorm.Expr("balance - ?", float64(req.Amount)))
if result.Error != nil {
tx.Rollback()
service.LogVerification(c, &app.ID, &user.ID, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 保存失败 - %s", user.Username), "", result.Error)
response.Error(c, 500, "扣除失败")
return
}
if result.RowsAffected == 0 {
tx.Rollback()
response.Error(c, 400, "余额不足")
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,
}
if err := tx.Create(&record).Error; err != nil {
tx.Rollback()
response.Error(c, 500, "扣除失败")
return
}
tx.Commit()
database.DB.Where("id = ?", user.ID).First(&user)
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,
})
return
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
}
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Save(&user).Error; err != nil {
tx.Rollback()
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,
}
if err := tx.Create(&record).Error; err != nil {
tx.Rollback()
response.Error(c, 500, "扣除失败")
return
}
tx.Commit()
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(&notification).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(&notification).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)
}