ed73bb470c
- Add created_by field to AppUser model to track who created the user - Set created_by when agent creates users - Extract getAgentUserIDs() helper: finds users who recharged with agent/sub-agent cards OR were created by agent/sub-agent - Extract getAgentAppIDs() helper: finds agent's authorized applications - Refactor handleGetUsers and checkAgentUserPermission to use getAgentUserIDs - Create agent-specific handleGetDevices: filter by agent user IDs + app IDs - Create agent-specific handleGetSessions: filter by agent user IDs + app IDs - Replace admin handler references with agent-specific handlers for GET routes
1634 lines
43 KiB
Go
1634 lines
43 KiB
Go
package agent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"verification-platform-backend/internal/database"
|
|
"verification-platform-backend/internal/model"
|
|
"verification-platform-backend/internal/router/admin"
|
|
"verification-platform-backend/internal/service"
|
|
"verification-platform-backend/pkg/response"
|
|
"verification-platform-backend/pkg/utils"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func SetupAgentRoutes(r *gin.RouterGroup) {
|
|
r.GET("/stats", handleGetStats)
|
|
r.GET("/apps", handleGetApps)
|
|
r.GET("/apps/:id", handleGetAppDetail)
|
|
r.GET("/cards", handleGetCards)
|
|
r.POST("/cards/generate", handleGenerateCards)
|
|
r.GET("/users", handleGetUsers)
|
|
r.POST("/users", handleCreateUser)
|
|
r.PUT("/users/:id", handleUpdateUser)
|
|
r.GET("/users/:id", handleGetUser)
|
|
r.PUT("/users/:id/status", handleUpdateUserStatus)
|
|
r.GET("/finance", handleGetFinance)
|
|
r.GET("/profile", handleGetProfile)
|
|
r.PUT("/profile", handleUpdateProfile)
|
|
r.GET("/cards/export", handleExportCards)
|
|
r.GET("/cloud-variables", handleGetCloudVariables)
|
|
r.GET("/cloud-variables/:id/records", handleGetCloudVariableRecords)
|
|
|
|
r.GET("/devices", handleGetDevices)
|
|
r.PUT("/devices/:id/status", admin.HandleUpdateDeviceStatus)
|
|
r.DELETE("/devices/:id", admin.HandleUnbindDevice)
|
|
r.POST("/devices/:id/force-offline", admin.HandleForceOfflineDevice)
|
|
|
|
r.GET("/sessions", handleGetSessions)
|
|
r.DELETE("/sessions/:id", admin.HandleDeleteSession)
|
|
}
|
|
|
|
func handleGetStats(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
var totalApps int64
|
|
database.DB.Model(&model.AgentApplication{}).Where("agent_id = ?", userID).Count(&totalApps)
|
|
|
|
// 统计卡密数:包括 agent_id 或 creator_id 等于当前用户的卡密
|
|
var totalCards int64
|
|
database.DB.Model(&model.Card{}).Where("agent_id = ? OR (agent_id IS NULL AND creator_id = ?)", userID, userID).Count(&totalCards)
|
|
|
|
var totalUsers int64
|
|
var agentApps []model.AgentApplication
|
|
database.DB.Where("agent_id = ?", userID).Find(&agentApps)
|
|
appIDs := make([]uint, 0)
|
|
for _, app := range agentApps {
|
|
appIDs = append(appIDs, app.ApplicationID)
|
|
}
|
|
if len(appIDs) > 0 {
|
|
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Count(&totalUsers)
|
|
}
|
|
|
|
var totalRevenue float64
|
|
database.DB.Model(&model.Card{}).Where("agent_id = ? OR (agent_id IS NULL AND creator_id = ?)", userID, userID).Select("COALESCE(SUM(price), 0)").Scan(&totalRevenue)
|
|
|
|
today := time.Now().Format("2006-01-02")
|
|
var todayCards int64
|
|
database.DB.Model(&model.Card{}).Where("(agent_id = ? OR (agent_id IS NULL AND creator_id = ?)) AND DATE(created_at) = ?", userID, userID, today).Count(&todayCards)
|
|
|
|
var todayRevenue float64
|
|
database.DB.Model(&model.Card{}).Where("(agent_id = ? OR (agent_id IS NULL AND creator_id = ?)) AND DATE(created_at) = ?", userID, userID, today).Select("COALESCE(SUM(price), 0)").Scan(&todayRevenue)
|
|
|
|
var agent model.User
|
|
database.DB.First(&agent, userID)
|
|
|
|
response.Success(c, gin.H{
|
|
"totalApps": totalApps,
|
|
"totalCards": totalCards,
|
|
"totalUsers": totalUsers,
|
|
"totalRevenue": totalRevenue,
|
|
"todayCards": todayCards,
|
|
"todayRevenue": todayRevenue,
|
|
"balance": agent.Balance,
|
|
})
|
|
}
|
|
|
|
func handleGetApps(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
var agentApps []model.AgentApplication
|
|
if err := database.DB.Where("agent_id = ?", userID).Preload("Application").Find(&agentApps).Error; err != nil {
|
|
response.Error(c, 500, "获取应用列表失败")
|
|
return
|
|
}
|
|
|
|
apps := make([]gin.H, 0)
|
|
for _, aa := range agentApps {
|
|
if aa.Application.ID > 0 {
|
|
apps = append(apps, gin.H{
|
|
"id": aa.Application.ID,
|
|
"name": aa.Application.Name,
|
|
"description": aa.Application.Description,
|
|
"status": aa.Application.Status,
|
|
"authorized_at": aa.CreatedAt,
|
|
})
|
|
}
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"apps": apps,
|
|
})
|
|
}
|
|
|
|
func handleGetAppDetail(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
appID := c.Param("id")
|
|
|
|
// 检查代理是否有该应用的授权
|
|
var agentApp model.AgentApplication
|
|
if err := database.DB.Where("agent_id = ? AND application_id = ?", userID, appID).Preload("Application").First(&agentApp).Error; err != nil {
|
|
response.Error(c, 404, "应用不存在或无权访问")
|
|
return
|
|
}
|
|
|
|
// 获取该应用可用的卡类
|
|
var cardTypes []model.CardType
|
|
if err := database.DB.Where("application_id = ?", appID).Find(&cardTypes).Error; err != nil {
|
|
response.Error(c, 500, "获取卡类列表失败")
|
|
return
|
|
}
|
|
|
|
// 获取代理对该应用的卡类权限和价格
|
|
var agentCardTypes []model.AgentApplicationCardType
|
|
database.DB.Where("agent_application_id = ?", agentApp.ID).Find(&agentCardTypes)
|
|
|
|
// 构建卡类列表,包含代理权限信息
|
|
cardTypesList := make([]gin.H, 0)
|
|
for _, ct := range cardTypes {
|
|
// 查找代理是否有该卡类的权限
|
|
var agentPrice float64
|
|
var canGenerate bool
|
|
for _, act := range agentCardTypes {
|
|
if act.CardTypeID == ct.ID {
|
|
agentPrice = act.Price
|
|
canGenerate = act.CanGenerate
|
|
break
|
|
}
|
|
}
|
|
|
|
// 只返回代理有权限的卡类
|
|
if canGenerate {
|
|
cardTypesList = append(cardTypesList, gin.H{
|
|
"id": ct.ID,
|
|
"name": ct.Name,
|
|
"billing_type": ct.RechargeType,
|
|
"price": agentPrice,
|
|
"value": ct.Value,
|
|
"duration_days": ct.Value,
|
|
})
|
|
}
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"app": gin.H{
|
|
"id": agentApp.Application.ID,
|
|
"name": agentApp.Application.Name,
|
|
"description": agentApp.Application.Description,
|
|
"status": agentApp.Application.Status,
|
|
},
|
|
"cardTypes": cardTypesList,
|
|
})
|
|
}
|
|
|
|
func handleGetCards(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
page := c.DefaultQuery("page", "1")
|
|
pageSize := c.DefaultQuery("page_size", "20")
|
|
applicationID := c.Query("application_id")
|
|
cardTypeID := c.Query("card_type_id")
|
|
status := c.Query("status")
|
|
search := c.Query("search")
|
|
startDate := c.Query("start_date")
|
|
endDate := c.Query("end_date")
|
|
|
|
baseCondition := "agent_id = ? OR (agent_id IS NULL AND creator_id = ?)"
|
|
|
|
var total int64
|
|
database.DB.Model(&model.Card{}).Where(baseCondition, userID, userID).Count(&total)
|
|
|
|
var unusedCount int64
|
|
database.DB.Model(&model.Card{}).Where("("+baseCondition+") AND status = ?", userID, userID, "unused").Count(&unusedCount)
|
|
|
|
var usedCount int64
|
|
database.DB.Model(&model.Card{}).Where("("+baseCondition+") AND status = ?", userID, userID, "used").Count(&usedCount)
|
|
|
|
var expiredCount int64
|
|
database.DB.Model(&model.Card{}).Where("("+baseCondition+") AND status = ?", userID, userID, "expired").Count(&expiredCount)
|
|
|
|
var bannedCount int64
|
|
database.DB.Model(&model.Card{}).Where("("+baseCondition+") AND status = ?", userID, userID, "banned").Count(&bannedCount)
|
|
|
|
query := database.DB.Model(&model.Card{}).Where(baseCondition, userID, userID)
|
|
|
|
if applicationID != "" {
|
|
appID, err := strconv.ParseUint(applicationID, 10, 32)
|
|
if err == nil {
|
|
query = query.Where("application_id = ?", uint(appID))
|
|
}
|
|
}
|
|
|
|
if cardTypeID != "" {
|
|
ctID, err := strconv.ParseUint(cardTypeID, 10, 32)
|
|
if err == nil {
|
|
query = query.Where("card_type_id = ?", uint(ctID))
|
|
}
|
|
}
|
|
|
|
if status != "" {
|
|
query = query.Where("status = ?", status)
|
|
}
|
|
|
|
if search != "" {
|
|
searchPattern := "%" + search + "%"
|
|
query = query.Where("card_key LIKE ?", searchPattern)
|
|
}
|
|
|
|
if startDate != "" {
|
|
query = query.Where("created_at >= ?", startDate+" 00:00:00")
|
|
}
|
|
|
|
if endDate != "" {
|
|
query = query.Where("created_at <= ?", endDate+" 23:59:59")
|
|
}
|
|
|
|
var filteredTotal int64
|
|
query.Count(&filteredTotal)
|
|
|
|
var cards []model.Card
|
|
offset := 0
|
|
if pageInt, err := strconv.Atoi(page); err == nil && pageInt > 1 {
|
|
offset = (pageInt - 1) * 20
|
|
}
|
|
|
|
limit := 20
|
|
if pageSizeInt, err := strconv.Atoi(pageSize); err == nil && pageSizeInt > 0 {
|
|
limit = pageSizeInt
|
|
}
|
|
|
|
query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&cards)
|
|
|
|
// 获取关联数据
|
|
cardTypeIDs := make([]uint, 0)
|
|
appIDs := make([]uint, 0)
|
|
for _, card := range cards {
|
|
cardTypeIDs = append(cardTypeIDs, card.CardTypeID)
|
|
appIDs = append(appIDs, card.ApplicationID)
|
|
}
|
|
|
|
cardTypeMap := make(map[uint]model.CardType)
|
|
if len(cardTypeIDs) > 0 {
|
|
var cardTypes []model.CardType
|
|
database.DB.Where("id IN ?", cardTypeIDs).Find(&cardTypes)
|
|
for _, ct := range cardTypes {
|
|
cardTypeMap[ct.ID] = ct
|
|
}
|
|
}
|
|
|
|
appMap := make(map[uint]model.Application)
|
|
if len(appIDs) > 0 {
|
|
var apps []model.Application
|
|
database.DB.Where("id IN ?", appIDs).Find(&apps)
|
|
for _, app := range apps {
|
|
appMap[app.ID] = app
|
|
}
|
|
}
|
|
|
|
// 构建返回数据
|
|
cardList := make([]gin.H, 0)
|
|
for _, card := range cards {
|
|
ct := cardTypeMap[card.CardTypeID]
|
|
app := appMap[card.ApplicationID]
|
|
|
|
cardList = append(cardList, gin.H{
|
|
"id": card.ID,
|
|
"code": card.CardKey,
|
|
"app_name": app.Name,
|
|
"card_type_name": ct.Name,
|
|
"status": card.Status,
|
|
"created_at": card.CreatedAt,
|
|
"used_at": card.UsedAt,
|
|
})
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"cards": cardList,
|
|
"total": total,
|
|
"filtered_total": filteredTotal,
|
|
"unused_count": unusedCount,
|
|
"used_count": usedCount,
|
|
"expired_count": expiredCount,
|
|
"banned_count": bannedCount,
|
|
})
|
|
}
|
|
|
|
func handleGenerateCards(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
var req struct {
|
|
ApplicationID uint `json:"application_id"`
|
|
AppID uint `json:"app_id"` // 兼容前端传的app_id
|
|
CardTypeID uint `json:"card_type_id"`
|
|
Quantity int `json:"quantity"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.Error(c, 400, "参数错误")
|
|
return
|
|
}
|
|
|
|
// 限制最大生成数量
|
|
if req.Quantity <= 0 || req.Quantity > 100 {
|
|
response.Error(c, 400, "生成数量必须在1-100之间")
|
|
return
|
|
}
|
|
|
|
// 兼容 app_id 和 application_id
|
|
appID := req.ApplicationID
|
|
if appID == 0 {
|
|
appID = req.AppID
|
|
}
|
|
|
|
var agentApp model.AgentApplication
|
|
if err := database.DB.Where("agent_id = ? AND application_id = ?", userID, appID).First(&agentApp).Error; err != nil {
|
|
response.Error(c, 403, "无权操作该应用")
|
|
return
|
|
}
|
|
|
|
// 检查代理是否有该卡类的生成权限
|
|
var agentCardType model.AgentApplicationCardType
|
|
if err := database.DB.Where("agent_application_id = ? AND card_type_id = ? AND can_generate = ?", agentApp.ID, req.CardTypeID, true).First(&agentCardType).Error; err != nil {
|
|
response.Error(c, 403, "无权生成该卡类")
|
|
return
|
|
}
|
|
|
|
// 计算总价格
|
|
totalPrice := agentCardType.Price * float64(req.Quantity)
|
|
|
|
// 获取代理余额并检查是否足够
|
|
var user model.User
|
|
if err := database.DB.First(&user, userID).Error; err != nil {
|
|
response.Error(c, 404, "用户不存在")
|
|
return
|
|
}
|
|
|
|
if user.Balance < totalPrice {
|
|
response.Error(c, 400, fmt.Sprintf("余额不足,当前余额: %.2f,需要: %.2f", user.Balance, totalPrice))
|
|
return
|
|
}
|
|
|
|
// 使用事务确保数据一致性
|
|
tx := database.DB.Begin()
|
|
|
|
cards := make([]model.Card, req.Quantity)
|
|
for i := 0; i < req.Quantity; i++ {
|
|
cards[i] = model.Card{
|
|
ApplicationID: appID,
|
|
CardTypeID: req.CardTypeID,
|
|
CardKey: generateCardCode(),
|
|
CreatorID: userID,
|
|
AgentID: &userID,
|
|
Status: "unused",
|
|
}
|
|
}
|
|
|
|
if err := tx.Create(&cards).Error; err != nil {
|
|
tx.Rollback()
|
|
response.Error(c, 500, "生成卡密失败")
|
|
return
|
|
}
|
|
|
|
// 扣除余额
|
|
if err := tx.Model(&user).Update("balance", user.Balance-totalPrice).Error; err != nil {
|
|
tx.Rollback()
|
|
response.Error(c, 500, "扣除余额失败")
|
|
return
|
|
}
|
|
|
|
// 记录消费记录
|
|
consumeRecord := model.RechargeRecord{
|
|
UserID: userID,
|
|
OrderNo: fmt.Sprintf("CARD%d%d", userID, time.Now().UnixNano()),
|
|
Amount: -totalPrice,
|
|
Status: "success",
|
|
PaymentType: "balance",
|
|
Remark: fmt.Sprintf("生成卡密 %d 张", req.Quantity),
|
|
}
|
|
if err := tx.Create(&consumeRecord).Error; err != nil {
|
|
tx.Rollback()
|
|
response.Error(c, 500, "记录消费失败")
|
|
return
|
|
}
|
|
|
|
tx.Commit()
|
|
|
|
// 返回生成的卡号
|
|
codes := make([]string, req.Quantity)
|
|
for i, card := range cards {
|
|
codes[i] = card.CardKey
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"count": req.Quantity,
|
|
"codes": codes,
|
|
"totalPrice": totalPrice,
|
|
"balance": user.Balance - totalPrice,
|
|
})
|
|
}
|
|
|
|
func generateCardCode() string {
|
|
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
b := make([]byte, 16)
|
|
for i := range b {
|
|
b[i] = charset[rand.Intn(len(charset))]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func handleGetUsers(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
appUserIDs := getAgentUserIDs(userID)
|
|
|
|
if len(appUserIDs) == 0 {
|
|
response.Success(c, gin.H{
|
|
"users": []interface{}{},
|
|
"total": 0,
|
|
"active_count": 0,
|
|
"disabled_count": 0,
|
|
"banned_count": 0,
|
|
})
|
|
return
|
|
}
|
|
|
|
if len(appUserIDs) == 0 {
|
|
response.Success(c, gin.H{
|
|
"users": []interface{}{},
|
|
"total": 0,
|
|
"active_count": 0,
|
|
"disabled_count": 0,
|
|
"banned_count": 0,
|
|
})
|
|
return
|
|
}
|
|
|
|
page := c.DefaultQuery("page", "1")
|
|
pageSize := c.DefaultQuery("page_size", "20")
|
|
|
|
var total int64
|
|
database.DB.Model(&model.AppUser{}).Where("id IN ?", appUserIDs).Count(&total)
|
|
|
|
var activeCount, disabledCount, bannedCount int64
|
|
database.DB.Model(&model.AppUser{}).Where("id IN ? AND status = ?", appUserIDs, "active").Count(&activeCount)
|
|
database.DB.Model(&model.AppUser{}).Where("id IN ? AND status = ?", appUserIDs, "disabled").Count(&disabledCount)
|
|
database.DB.Model(&model.AppUser{}).Where("id IN ? AND status = ?", appUserIDs, "banned").Count(&bannedCount)
|
|
|
|
var users []model.AppUser
|
|
offset := 0
|
|
if pageInt, err := strconv.Atoi(page); err == nil && pageInt > 1 {
|
|
offset = (pageInt - 1) * 20
|
|
}
|
|
|
|
limit := 20
|
|
if pageSizeInt, err := strconv.Atoi(pageSize); err == nil && pageSizeInt > 0 {
|
|
limit = pageSizeInt
|
|
}
|
|
|
|
database.DB.Preload("Application").Where("id IN ?", appUserIDs).Order("created_at DESC").Limit(limit).Offset(offset).Find(&users)
|
|
|
|
result := make([]gin.H, 0, len(users))
|
|
for _, u := range users {
|
|
var deviceCount int64
|
|
database.DB.Model(&model.UserDevice{}).Where("user_id = ?", u.ID).Count(&deviceCount)
|
|
|
|
onlineStatus := "offline"
|
|
if u.Status == "banned" {
|
|
onlineStatus = "banned"
|
|
} else if u.LastHeartbeatAt != nil {
|
|
lastHeartbeat := *u.LastHeartbeatAt
|
|
if time.Since(lastHeartbeat) < 5*time.Minute {
|
|
onlineStatus = "online"
|
|
}
|
|
}
|
|
|
|
item := gin.H{
|
|
"id": u.ID,
|
|
"username": u.Username,
|
|
"email": u.Email,
|
|
"status": u.Status,
|
|
"balance": u.Balance,
|
|
"expiry_at": u.ExpiryAt,
|
|
"last_login_at": u.LastLoginAt,
|
|
"last_heartbeat_at": u.LastHeartbeatAt,
|
|
"device_count": deviceCount,
|
|
"online_status": onlineStatus,
|
|
"application_id": u.ApplicationID,
|
|
"is_trial_user": u.IsTrialUser,
|
|
"created_at": u.CreatedAt,
|
|
}
|
|
if u.Application.ID > 0 {
|
|
item["application"] = gin.H{
|
|
"id": u.Application.ID,
|
|
"name": u.Application.Name,
|
|
"billing_type": u.Application.BillingType,
|
|
}
|
|
}
|
|
result = append(result, item)
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"users": result,
|
|
"total": total,
|
|
"active_count": activeCount,
|
|
"disabled_count": disabledCount,
|
|
"banned_count": bannedCount,
|
|
})
|
|
}
|
|
|
|
func handleGetFinance(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
var user model.User
|
|
if err := database.DB.First(&user, userID).Error; err != nil {
|
|
response.Error(c, 404, "用户不存在")
|
|
return
|
|
}
|
|
|
|
typeFilter := c.Query("type")
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
|
|
|
type TransactionItem struct {
|
|
ID uint `json:"id"`
|
|
Type string `json:"type"`
|
|
Amount float64 `json:"amount"`
|
|
Description string `json:"description"`
|
|
Balance float64 `json:"balance"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
var allTransactions []TransactionItem
|
|
|
|
var rechargeRecords []model.RechargeRecord
|
|
database.DB.Where("user_id = ? AND status = ?", userID, "success").Order("created_at DESC").Find(&rechargeRecords)
|
|
for _, r := range rechargeRecords {
|
|
txType := "recharge"
|
|
if r.PaymentType == "refund" || r.Remark != "" && strings.Contains(strings.ToLower(r.Remark), "refund") {
|
|
txType = "refund"
|
|
}
|
|
desc := r.Remark
|
|
if desc == "" {
|
|
desc = "充值"
|
|
}
|
|
allTransactions = append(allTransactions, TransactionItem{
|
|
ID: r.ID,
|
|
Type: txType,
|
|
Amount: r.Amount,
|
|
Description: desc,
|
|
Balance: user.Balance,
|
|
CreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
})
|
|
}
|
|
|
|
var consumptionRecords []model.ConsumptionRecord
|
|
database.DB.Where("user_id = ?", userID).Order("created_at DESC").Find(&consumptionRecords)
|
|
for _, r := range consumptionRecords {
|
|
desc := r.Description
|
|
if desc == "" {
|
|
desc = r.Content
|
|
}
|
|
if desc == "" {
|
|
desc = "消费"
|
|
}
|
|
allTransactions = append(allTransactions, TransactionItem{
|
|
ID: r.ID,
|
|
Type: "consume",
|
|
Amount: r.Amount,
|
|
Description: desc,
|
|
Balance: r.BalanceAfter,
|
|
CreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
})
|
|
}
|
|
|
|
sort.Slice(allTransactions, func(i, j int) bool {
|
|
return allTransactions[i].CreatedAt > allTransactions[j].CreatedAt
|
|
})
|
|
|
|
var filtered []TransactionItem
|
|
if typeFilter != "" {
|
|
for _, tx := range allTransactions {
|
|
if tx.Type == typeFilter {
|
|
filtered = append(filtered, tx)
|
|
}
|
|
}
|
|
} else {
|
|
filtered = allTransactions
|
|
}
|
|
|
|
total := len(filtered)
|
|
|
|
rechargeCount := 0
|
|
consumeCount := 0
|
|
var totalRecharge, totalConsume float64
|
|
for _, tx := range allTransactions {
|
|
if tx.Type == "recharge" || tx.Type == "refund" {
|
|
rechargeCount++
|
|
totalRecharge += tx.Amount
|
|
} else if tx.Type == "consume" {
|
|
consumeCount++
|
|
totalConsume += tx.Amount
|
|
}
|
|
}
|
|
|
|
offset := (page - 1) * pageSize
|
|
end := offset + pageSize
|
|
if offset > total {
|
|
offset = total
|
|
}
|
|
if end > total {
|
|
end = total
|
|
}
|
|
|
|
paginated := filtered[offset:end]
|
|
|
|
response.Success(c, gin.H{
|
|
"balance": user.Balance,
|
|
"transactions": paginated,
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": pageSize,
|
|
"recharge_count": rechargeCount,
|
|
"consume_count": consumeCount,
|
|
"total_recharge": totalRecharge,
|
|
"total_consume": totalConsume,
|
|
})
|
|
}
|
|
|
|
func handleGetProfile(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
var user model.User
|
|
if err := database.DB.First(&user, userID).Error; err != nil {
|
|
response.Error(c, 404, "用户不存在")
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"id": user.ID,
|
|
"username": user.Username,
|
|
"email": user.Email,
|
|
"avatar": user.Avatar,
|
|
"balance": user.Balance,
|
|
"can_create_agent": user.CanCreateAgent,
|
|
"can_view_cloud_data": user.CanViewCloudData,
|
|
})
|
|
}
|
|
|
|
func handleUpdateProfile(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
var req struct {
|
|
Email string `json:"email"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.Error(c, 400, "参数错误")
|
|
return
|
|
}
|
|
|
|
var user model.User
|
|
if err := database.DB.First(&user, userID).Error; err != nil {
|
|
response.Error(c, 404, "用户不存在")
|
|
return
|
|
}
|
|
|
|
if req.Email != "" {
|
|
user.Email = &req.Email
|
|
}
|
|
database.DB.Save(&user)
|
|
|
|
response.Success(c, nil)
|
|
}
|
|
|
|
func handleExportCards(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
token := c.Query("token")
|
|
if token == "" {
|
|
response.Error(c, 401, "未授权")
|
|
return
|
|
}
|
|
|
|
baseCondition := "agent_id = ? OR (agent_id IS NULL AND creator_id = ?)"
|
|
query := database.DB.Model(&model.Card{}).Where(baseCondition, userID, userID)
|
|
|
|
if applicationID := c.Query("application_id"); applicationID != "" {
|
|
appID, err := strconv.ParseUint(applicationID, 10, 32)
|
|
if err == nil {
|
|
query = query.Where("application_id = ?", uint(appID))
|
|
}
|
|
}
|
|
|
|
if cardTypeID := c.Query("card_type_id"); cardTypeID != "" {
|
|
ctID, err := strconv.ParseUint(cardTypeID, 10, 32)
|
|
if err == nil {
|
|
query = query.Where("card_type_id = ?", uint(ctID))
|
|
}
|
|
}
|
|
|
|
if status := c.Query("status"); status != "" {
|
|
query = query.Where("status = ?", status)
|
|
}
|
|
|
|
if startDate := c.Query("start_date"); startDate != "" {
|
|
query = query.Where("created_at >= ?", startDate+" 00:00:00")
|
|
}
|
|
|
|
if endDate := c.Query("end_date"); endDate != "" {
|
|
query = query.Where("created_at <= ?", endDate+" 23:59:59")
|
|
}
|
|
|
|
if ids := c.Query("ids"); ids != "" {
|
|
idList := []uint{}
|
|
for _, idStr := range splitIDs(ids) {
|
|
if id, err := strconv.ParseUint(idStr, 10, 32); err == nil {
|
|
idList = append(idList, uint(id))
|
|
}
|
|
}
|
|
if len(idList) > 0 {
|
|
query = database.DB.Model(&model.Card{}).Where("id IN ? AND ("+baseCondition+")", idList, userID, userID)
|
|
}
|
|
}
|
|
|
|
var cards []model.Card
|
|
query.Order("created_at DESC").Find(&cards)
|
|
|
|
cardTypeIDs := make([]uint, 0)
|
|
appIDs := make([]uint, 0)
|
|
for _, card := range cards {
|
|
cardTypeIDs = append(cardTypeIDs, card.CardTypeID)
|
|
appIDs = append(appIDs, card.ApplicationID)
|
|
}
|
|
|
|
cardTypeMap := make(map[uint]model.CardType)
|
|
if len(cardTypeIDs) > 0 {
|
|
var cardTypes []model.CardType
|
|
database.DB.Where("id IN ?", cardTypeIDs).Find(&cardTypes)
|
|
for _, ct := range cardTypes {
|
|
cardTypeMap[ct.ID] = ct
|
|
}
|
|
}
|
|
|
|
appMap := make(map[uint]model.Application)
|
|
if len(appIDs) > 0 {
|
|
var apps []model.Application
|
|
database.DB.Where("id IN ?", appIDs).Find(&apps)
|
|
for _, app := range apps {
|
|
appMap[app.ID] = app
|
|
}
|
|
}
|
|
|
|
c.Header("Content-Type", "text/csv; charset=utf-8")
|
|
c.Header("Content-Disposition", "attachment; filename=cards_export.csv")
|
|
|
|
c.Writer.Write([]byte("\xEF\xBB\xBF"))
|
|
c.Writer.Write([]byte("卡号,应用,卡类,状态,创建时间,使用时间\n"))
|
|
|
|
for _, card := range cards {
|
|
ct := cardTypeMap[card.CardTypeID]
|
|
app := appMap[card.ApplicationID]
|
|
statusMap := map[string]string{"unused": "未使用", "used": "已使用", "expired": "已过期", "banned": "已禁用"}
|
|
statusText := statusMap[card.Status]
|
|
if statusText == "" {
|
|
statusText = card.Status
|
|
}
|
|
usedAt := ""
|
|
if card.UsedAt != nil {
|
|
usedAt = card.UsedAt.Format("2006-01-02 15:04:05")
|
|
}
|
|
line := fmt.Sprintf("%s,%s,%s,%s,%s,%s\n",
|
|
card.CardKey,
|
|
app.Name,
|
|
ct.Name,
|
|
statusText,
|
|
card.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
usedAt,
|
|
)
|
|
c.Writer.Write([]byte(line))
|
|
}
|
|
}
|
|
|
|
func splitIDs(ids string) []string {
|
|
result := []string{}
|
|
for _, id := range strings.Split(ids, ",") {
|
|
id = strings.TrimSpace(id)
|
|
if id != "" {
|
|
result = append(result, id)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func handleGetCloudVariables(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
var agent model.User
|
|
if err := database.DB.First(&agent, userID).Error; err != nil {
|
|
response.Error(c, 404, "代理不存在")
|
|
return
|
|
}
|
|
|
|
if !agent.CanViewCloudData {
|
|
response.Error(c, 403, "无权查看云端变量")
|
|
return
|
|
}
|
|
|
|
var agentApps []model.AgentApplication
|
|
database.DB.Where("agent_id = ?", userID).Find(&agentApps)
|
|
appIDs := make([]uint, 0)
|
|
for _, app := range agentApps {
|
|
appIDs = append(appIDs, app.ApplicationID)
|
|
}
|
|
|
|
if len(appIDs) == 0 {
|
|
response.Success(c, gin.H{
|
|
"variables": []interface{}{},
|
|
"total": 0,
|
|
})
|
|
return
|
|
}
|
|
|
|
appIDFilter := c.Query("app_id")
|
|
var variables []model.CloudVariable
|
|
query := database.DB.Where("app_id IN ?", appIDs)
|
|
if appIDFilter != "" {
|
|
if aid, err := strconv.ParseUint(appIDFilter, 10, 32); err == nil {
|
|
query = database.DB.Where("app_id = ?", uint(aid))
|
|
}
|
|
}
|
|
query.Find(&variables)
|
|
|
|
appMap := make(map[uint]string)
|
|
var apps []model.Application
|
|
database.DB.Where("id IN ?", appIDs).Find(&apps)
|
|
for _, app := range apps {
|
|
appMap[app.ID] = app.Name
|
|
}
|
|
|
|
result := make([]gin.H, 0)
|
|
for _, v := range variables {
|
|
appName := ""
|
|
if v.AppID != nil {
|
|
appName = appMap[*v.AppID]
|
|
}
|
|
result = append(result, gin.H{
|
|
"id": v.ID,
|
|
"key": v.Key,
|
|
"app_id": v.AppID,
|
|
"application_name": appName,
|
|
"default_value": v.DefaultValue,
|
|
"var_type": v.VarType,
|
|
"max_records": v.MaxRecords,
|
|
"scope": v.Scope,
|
|
"write_permission": v.WritePermission,
|
|
"status": v.Status,
|
|
"description": v.Description,
|
|
"created_at": v.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
})
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"variables": result,
|
|
"total": len(result),
|
|
})
|
|
}
|
|
|
|
func handleGetCloudVariableRecords(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
var agent model.User
|
|
if err := database.DB.First(&agent, userID).Error; err != nil {
|
|
response.Error(c, 404, "代理不存在")
|
|
return
|
|
}
|
|
|
|
if !agent.CanViewCloudData {
|
|
response.Error(c, 403, "无权查看云端变量")
|
|
return
|
|
}
|
|
|
|
variableID := c.Param("id")
|
|
var variable model.CloudVariable
|
|
if err := database.DB.First(&variable, variableID).Error; err != nil {
|
|
response.Error(c, 404, "云端变量不存在")
|
|
return
|
|
}
|
|
|
|
var agentApps []model.AgentApplication
|
|
database.DB.Where("agent_id = ?", userID).Find(&agentApps)
|
|
appIDs := make(map[uint]bool)
|
|
for _, app := range agentApps {
|
|
appIDs[app.ApplicationID] = true
|
|
}
|
|
|
|
if variable.AppID == nil || !appIDs[*variable.AppID] {
|
|
response.Error(c, 403, "无权查看该变量的记录")
|
|
return
|
|
}
|
|
|
|
if variable.VarType != "stream" {
|
|
response.Error(c, 400, "该变量不是记录类型")
|
|
return
|
|
}
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize < 1 || pageSize > 100 {
|
|
pageSize = 20
|
|
}
|
|
|
|
startDate := c.Query("start_date")
|
|
endDate := c.Query("end_date")
|
|
|
|
var total int64
|
|
query := database.DB.Model(&model.CloudVariableRecord{}).Where("cloud_variable_id = ?", variable.ID)
|
|
if startDate != "" {
|
|
query = query.Where("created_at >= ?", startDate+" 00:00:00")
|
|
}
|
|
if endDate != "" {
|
|
query = query.Where("created_at <= ?", endDate+" 23:59:59")
|
|
}
|
|
query.Count(&total)
|
|
|
|
var records []model.CloudVariableRecord
|
|
offset := (page - 1) * pageSize
|
|
if err := query.Order("created_at DESC").Limit(pageSize).Offset(offset).Find(&records).Error; err != nil {
|
|
response.Error(c, 500, "获取记录失败")
|
|
return
|
|
}
|
|
|
|
result := make([]gin.H, len(records))
|
|
for i, r := range records {
|
|
var parsedData interface{}
|
|
if r.Data != "" {
|
|
json.Unmarshal([]byte(r.Data), &parsedData)
|
|
}
|
|
if parsedData == nil && r.Data != "" {
|
|
parsedData = r.Data
|
|
}
|
|
record := gin.H{
|
|
"id": r.ID,
|
|
"data": parsedData,
|
|
"created_at": r.CreatedAt,
|
|
}
|
|
if r.AppUserID != nil {
|
|
record["user_id"] = r.AppUserID
|
|
var appUser model.AppUser
|
|
if err := database.DB.Select("id, username").First(&appUser, *r.AppUserID).Error; err == nil {
|
|
record["user"] = gin.H{
|
|
"id": appUser.ID,
|
|
"username": appUser.Username,
|
|
}
|
|
}
|
|
}
|
|
result[i] = record
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"records": result,
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": pageSize,
|
|
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
|
})
|
|
}
|
|
|
|
func handleCreateUser(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
ApplicationID uint `json:"application_id"`
|
|
CardTypeID *uint `json:"card_type_id"`
|
|
CardQuantity int `json:"card_quantity"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.Error(c, 400, "参数错误")
|
|
return
|
|
}
|
|
|
|
if req.Username == "" {
|
|
response.Error(c, 400, "用户名不能为空")
|
|
return
|
|
}
|
|
if req.Password == "" {
|
|
response.Error(c, 400, "密码不能为空")
|
|
return
|
|
}
|
|
if req.ApplicationID == 0 {
|
|
response.Error(c, 400, "所属应用不能为空")
|
|
return
|
|
}
|
|
if req.CardQuantity < 1 {
|
|
req.CardQuantity = 1
|
|
}
|
|
if req.CardQuantity > 100 {
|
|
response.Error(c, 400, "卡密数量不能超过100")
|
|
return
|
|
}
|
|
|
|
var agentApp model.AgentApplication
|
|
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", req.ApplicationID, userID, true).First(&agentApp).Error; err != nil {
|
|
response.Error(c, 403, "无权限在该应用下创建用户")
|
|
return
|
|
}
|
|
|
|
var app model.Application
|
|
if err := database.DB.First(&app, req.ApplicationID).Error; err != nil {
|
|
response.Error(c, 404, "应用不存在")
|
|
return
|
|
}
|
|
|
|
var existingUser model.AppUser
|
|
if err := database.DB.Where("username = ? AND application_id = ?", req.Username, app.ID).First(&existingUser).Error; err == nil {
|
|
response.Error(c, 400, "用户已存在")
|
|
return
|
|
}
|
|
|
|
var cardType *model.CardType
|
|
var agentCardType *model.AgentApplicationCardType
|
|
if req.CardTypeID != nil && *req.CardTypeID > 0 {
|
|
var ct model.CardType
|
|
if err := database.DB.Where("id = ? AND application_id = ?", *req.CardTypeID, req.ApplicationID).First(&ct).Error; err != nil {
|
|
response.Error(c, 400, "卡密类型不存在或不属于该应用")
|
|
return
|
|
}
|
|
|
|
var act model.AgentApplicationCardType
|
|
if err := database.DB.Where("agent_application_id = ? AND card_type_id = ? AND can_generate = ?", agentApp.ID, *req.CardTypeID, true).First(&act).Error; err != nil {
|
|
response.Error(c, 403, "无权使用该卡类充值")
|
|
return
|
|
}
|
|
|
|
totalPrice := act.Price * float64(req.CardQuantity)
|
|
var agent model.User
|
|
if err := database.DB.First(&agent, userID).Error; err != nil {
|
|
response.Error(c, 404, "代理不存在")
|
|
return
|
|
}
|
|
if agent.Balance < totalPrice {
|
|
response.Error(c, 400, fmt.Sprintf("余额不足,当前余额: %.2f,需要: %.2f", agent.Balance, totalPrice))
|
|
return
|
|
}
|
|
|
|
cardType = &ct
|
|
agentCardType = &act
|
|
}
|
|
|
|
tx := database.DB.Begin()
|
|
|
|
user := model.AppUser{
|
|
Username: req.Username,
|
|
Email: req.Email,
|
|
Password: req.Password,
|
|
Avatar: "",
|
|
Status: "active",
|
|
ApplicationID: app.ID,
|
|
CreatedBy: &userID,
|
|
}
|
|
|
|
if err := tx.Create(&user).Error; err != nil {
|
|
tx.Rollback()
|
|
response.Error(c, 500, "创建用户失败")
|
|
return
|
|
}
|
|
|
|
var cards []model.Card
|
|
if cardType != nil {
|
|
now := time.Now()
|
|
for i := 0; i < req.CardQuantity; i++ {
|
|
cardKey := "CK" + utils.GenerateRandomString(16)
|
|
card := model.Card{
|
|
ApplicationID: req.ApplicationID,
|
|
CardTypeID: cardType.ID,
|
|
CardKey: cardKey,
|
|
CreatorID: userID,
|
|
AgentID: &userID,
|
|
AppUserID: &user.ID,
|
|
Status: "used",
|
|
}
|
|
card.UsedAt = &now
|
|
|
|
if err := tx.Create(&card).Error; err != nil {
|
|
tx.Rollback()
|
|
response.Error(c, 500, "生成卡密失败")
|
|
return
|
|
}
|
|
|
|
user.IsTrialUser = false
|
|
|
|
if cardType.Value == -1 {
|
|
if cardType.RechargeType == "subscription" {
|
|
permanentExpiry := time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
|
|
user.ExpiryAt = &permanentExpiry
|
|
user.Balance = -1
|
|
} else {
|
|
user.Balance = -1
|
|
user.ExpiryAt = nil
|
|
}
|
|
} else {
|
|
switch cardType.RechargeType {
|
|
case "subscription":
|
|
var baseTime time.Time
|
|
if user.ExpiryAt != nil && user.ExpiryAt.After(now) {
|
|
baseTime = *user.ExpiryAt
|
|
} else {
|
|
baseTime = now
|
|
}
|
|
var duration time.Duration
|
|
switch cardType.ValueUnit {
|
|
case "minute":
|
|
duration = time.Duration(cardType.Value) * time.Minute
|
|
case "hour":
|
|
duration = time.Duration(cardType.Value) * time.Hour
|
|
case "day":
|
|
duration = time.Duration(cardType.Value) * 24 * time.Hour
|
|
case "month":
|
|
duration = time.Duration(cardType.Value) * 30 * 24 * time.Hour
|
|
case "year":
|
|
duration = time.Duration(cardType.Value) * 365 * 24 * time.Hour
|
|
default:
|
|
duration = time.Duration(cardType.Value) * time.Second
|
|
}
|
|
newExpiry := baseTime.Add(duration)
|
|
user.ExpiryAt = &newExpiry
|
|
case "balance":
|
|
fallthrough
|
|
default:
|
|
user.Balance += cardType.Value
|
|
}
|
|
}
|
|
|
|
rechargeRecord := model.RechargeRecord{
|
|
UserID: user.ID,
|
|
OrderNo: generateAgentOrderNo("R"),
|
|
CardID: &card.ID,
|
|
CardCode: card.CardKey,
|
|
Amount: cardType.Price,
|
|
Status: "success",
|
|
PaymentType: "card",
|
|
Remark: "代理创建用户充值 - " + cardType.Name,
|
|
}
|
|
if err := tx.Create(&rechargeRecord).Error; err != nil {
|
|
tx.Rollback()
|
|
response.Error(c, 500, "创建充值记录失败")
|
|
return
|
|
}
|
|
|
|
cards = append(cards, card)
|
|
}
|
|
|
|
if err := tx.Save(&user).Error; err != nil {
|
|
tx.Rollback()
|
|
response.Error(c, 500, "充值失败")
|
|
return
|
|
}
|
|
|
|
totalPrice := agentCardType.Price * float64(req.CardQuantity)
|
|
if err := tx.Model(&model.User{}).Where("id = ?", userID).Update("balance", gorm.Expr("balance - ?", totalPrice)).Error; err != nil {
|
|
tx.Rollback()
|
|
response.Error(c, 500, "扣除余额失败")
|
|
return
|
|
}
|
|
|
|
consumeRecord := model.RechargeRecord{
|
|
UserID: userID,
|
|
OrderNo: generateAgentOrderNo("C"),
|
|
Amount: -totalPrice,
|
|
Status: "success",
|
|
PaymentType: "balance",
|
|
Remark: fmt.Sprintf("创建用户充值卡密: %s x%d", cardType.Name, req.CardQuantity),
|
|
}
|
|
if err := tx.Create(&consumeRecord).Error; err != nil {
|
|
tx.Rollback()
|
|
response.Error(c, 500, "记录消费失败")
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
response.Error(c, 500, "创建用户失败")
|
|
return
|
|
}
|
|
|
|
logDesc := fmt.Sprintf("代理创建用户: %s (应用: %s)", user.Username, app.Name)
|
|
if cardType != nil {
|
|
logDesc += fmt.Sprintf(",充值卡密: %s x%d", cardType.Name, req.CardQuantity)
|
|
}
|
|
service.LogOperation(c, "create", "app_user", &user.ID, logDesc, nil)
|
|
|
|
result := gin.H{
|
|
"user": user,
|
|
}
|
|
if len(cards) > 0 {
|
|
result["cards"] = cards
|
|
}
|
|
|
|
response.Success(c, result)
|
|
}
|
|
|
|
func generateAgentOrderNo(prefix string) string {
|
|
return prefix + time.Now().Format("20060102150405") + utils.GenerateRandomString(6)
|
|
}
|
|
|
|
func handleUpdateUserStatus(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
id := c.Param("id")
|
|
|
|
var req struct {
|
|
Status string `json:"status"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.Error(c, 400, "参数错误")
|
|
return
|
|
}
|
|
|
|
if req.Status != "active" && req.Status != "banned" {
|
|
response.Error(c, 400, "状态值无效,仅支持 active 或 banned")
|
|
return
|
|
}
|
|
|
|
var user model.AppUser
|
|
if err := database.DB.First(&user, id).Error; err != nil {
|
|
response.Error(c, 404, "用户不存在")
|
|
return
|
|
}
|
|
|
|
if !checkAgentUserPermission(userID, user.ID) {
|
|
response.Error(c, 403, "无权限操作该用户")
|
|
return
|
|
}
|
|
|
|
user.Status = req.Status
|
|
if err := database.DB.Save(&user).Error; err != nil {
|
|
response.Error(c, 500, "更新用户状态失败")
|
|
return
|
|
}
|
|
|
|
logDesc := fmt.Sprintf("代理更新用户状态: %s -> %s", user.Username, req.Status)
|
|
service.LogOperation(c, "update", "app_user", &user.ID, logDesc, nil)
|
|
|
|
response.Success(c, user)
|
|
}
|
|
|
|
func getAgentUserIDs(userID uint) []uint {
|
|
agentIDs := []uint{userID}
|
|
var childAgents []model.User
|
|
database.DB.Where("parent_agent_id = ? AND role = ?", userID, "agent").Find(&childAgents)
|
|
for _, child := range childAgents {
|
|
agentIDs = append(agentIDs, child.ID)
|
|
}
|
|
|
|
var cardIDs []uint
|
|
database.DB.Model(&model.Card{}).Where("agent_id IN ?", agentIDs).Pluck("id", &cardIDs)
|
|
|
|
userIDSet := make(map[uint]bool)
|
|
|
|
if len(cardIDs) > 0 {
|
|
var rechargedUserIDs []uint
|
|
database.DB.Model(&model.RechargeRecord{}).
|
|
Where("card_id IN ? AND status = ?", cardIDs, "success").
|
|
Distinct("user_id").
|
|
Pluck("user_id", &rechargedUserIDs)
|
|
for _, id := range rechargedUserIDs {
|
|
userIDSet[id] = true
|
|
}
|
|
}
|
|
|
|
var createdUserIDs []uint
|
|
database.DB.Model(&model.AppUser{}).
|
|
Where("created_by IN ?", agentIDs).
|
|
Pluck("id", &createdUserIDs)
|
|
for _, id := range createdUserIDs {
|
|
userIDSet[id] = true
|
|
}
|
|
|
|
result := make([]uint, 0, len(userIDSet))
|
|
for id := range userIDSet {
|
|
result = append(result, id)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func getAgentAppIDs(userID uint) []uint {
|
|
var agentApps []model.AgentApplication
|
|
database.DB.Where("agent_id = ? AND is_received = ?", userID, true).Find(&agentApps)
|
|
|
|
appIDs := make([]uint, 0, len(agentApps))
|
|
for _, agentApp := range agentApps {
|
|
appIDs = append(appIDs, agentApp.ApplicationID)
|
|
}
|
|
return appIDs
|
|
}
|
|
|
|
func checkAgentUserPermission(userID uint, appUserID uint) bool {
|
|
agentUserIDs := getAgentUserIDs(userID)
|
|
for _, id := range agentUserIDs {
|
|
if id == appUserID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func handleGetUser(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
id := c.Param("id")
|
|
|
|
var user model.AppUser
|
|
if err := database.DB.Preload("Application").First(&user, id).Error; err != nil {
|
|
response.Error(c, 404, "用户不存在")
|
|
return
|
|
}
|
|
|
|
if !checkAgentUserPermission(userID, user.ID) {
|
|
response.Error(c, 403, "无权限查看该用户")
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"user": user,
|
|
})
|
|
}
|
|
|
|
func handleUpdateUser(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
id := c.Param("id")
|
|
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.Error(c, 400, "参数错误")
|
|
return
|
|
}
|
|
|
|
var user model.AppUser
|
|
if err := database.DB.First(&user, id).Error; err != nil {
|
|
response.Error(c, 404, "用户不存在")
|
|
return
|
|
}
|
|
|
|
if !checkAgentUserPermission(userID, user.ID) {
|
|
response.Error(c, 403, "无权限修改该用户")
|
|
return
|
|
}
|
|
|
|
if req.Username != "" {
|
|
user.Username = req.Username
|
|
}
|
|
if req.Email != "" {
|
|
user.Email = req.Email
|
|
}
|
|
if req.Password != "" {
|
|
user.Password = req.Password
|
|
}
|
|
|
|
if err := database.DB.Save(&user).Error; err != nil {
|
|
response.Error(c, 500, "更新用户失败")
|
|
return
|
|
}
|
|
|
|
logDesc := fmt.Sprintf("代理更新用户: %s", user.Username)
|
|
service.LogOperation(c, "update", "app_user", &user.ID, logDesc, nil)
|
|
|
|
response.Success(c, user)
|
|
}
|
|
|
|
func handleGetDevices(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
appUserIDs := getAgentUserIDs(userID)
|
|
if len(appUserIDs) == 0 {
|
|
response.Success(c, gin.H{
|
|
"devices": []interface{}{},
|
|
"total": 0,
|
|
"banned_count": 0,
|
|
})
|
|
return
|
|
}
|
|
|
|
appIDs := getAgentAppIDs(userID)
|
|
if len(appIDs) == 0 {
|
|
response.Success(c, gin.H{
|
|
"devices": []interface{}{},
|
|
"total": 0,
|
|
"banned_count": 0,
|
|
})
|
|
return
|
|
}
|
|
|
|
statusFilter := c.Query("status")
|
|
applicationIDFilter := c.Query("application_id")
|
|
|
|
query := database.DB.Model(&model.UserDevice{}).
|
|
Where("user_id IN ? AND application_id IN ?", appUserIDs, appIDs)
|
|
|
|
if statusFilter != "" {
|
|
query = query.Where("status = ?", statusFilter)
|
|
}
|
|
if applicationIDFilter != "" {
|
|
appID, _ := strconv.Atoi(applicationIDFilter)
|
|
if appID > 0 {
|
|
query = query.Where("application_id = ?", appID)
|
|
}
|
|
}
|
|
|
|
var total int64
|
|
query.Count(&total)
|
|
|
|
var bannedCount int64
|
|
database.DB.Model(&model.UserDevice{}).
|
|
Where("user_id IN ? AND application_id IN ? AND status = ?", appUserIDs, appIDs, "banned").
|
|
Count(&bannedCount)
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
|
offset := (page - 1) * pageSize
|
|
|
|
var devices []model.UserDevice
|
|
findQuery := database.DB.Preload("User").Preload("Application").
|
|
Where("user_id IN ? AND application_id IN ?", appUserIDs, appIDs)
|
|
|
|
if statusFilter != "" {
|
|
findQuery = findQuery.Where("status = ?", statusFilter)
|
|
}
|
|
if applicationIDFilter != "" {
|
|
appID, _ := strconv.Atoi(applicationIDFilter)
|
|
if appID > 0 {
|
|
findQuery = findQuery.Where("application_id = ?", appID)
|
|
}
|
|
}
|
|
|
|
if err := findQuery.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&devices).Error; err != nil {
|
|
response.Error(c, 500, "获取设备列表失败")
|
|
return
|
|
}
|
|
|
|
deviceIDs := make([]uint, len(devices))
|
|
for i, d := range devices {
|
|
deviceIDs[i] = d.ID
|
|
}
|
|
|
|
onlineSessionMap := make(map[uint]int)
|
|
if len(deviceIDs) > 0 {
|
|
type SessionCountResult struct {
|
|
DeviceID uint
|
|
Count int
|
|
}
|
|
var sessionCounts []SessionCountResult
|
|
database.DB.Model(&model.DeviceSession{}).
|
|
Select("device_id, COUNT(*) as count").
|
|
Where("device_id IN ? AND last_heartbeat > ?", deviceIDs, time.Now().Add(-5*time.Minute)).
|
|
Group("device_id").
|
|
Find(&sessionCounts)
|
|
for _, sc := range sessionCounts {
|
|
onlineSessionMap[sc.DeviceID] = sc.Count
|
|
}
|
|
}
|
|
|
|
type DeviceWithDetails struct {
|
|
model.UserDevice
|
|
OnlineSessions int `json:"online_sessions"`
|
|
}
|
|
|
|
result := make([]DeviceWithDetails, 0, len(devices))
|
|
for _, device := range devices {
|
|
result = append(result, DeviceWithDetails{
|
|
UserDevice: device,
|
|
OnlineSessions: onlineSessionMap[device.ID],
|
|
})
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"devices": result,
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": pageSize,
|
|
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
|
|
"banned_count": bannedCount,
|
|
})
|
|
}
|
|
|
|
func handleGetSessions(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
appUserIDs := getAgentUserIDs(userID)
|
|
if len(appUserIDs) == 0 {
|
|
response.Success(c, gin.H{
|
|
"sessions": []interface{}{},
|
|
"total": 0,
|
|
})
|
|
return
|
|
}
|
|
|
|
appIDs := getAgentAppIDs(userID)
|
|
if len(appIDs) == 0 {
|
|
response.Success(c, gin.H{
|
|
"sessions": []interface{}{},
|
|
"total": 0,
|
|
})
|
|
return
|
|
}
|
|
|
|
appIDFilter := c.Query("app_id")
|
|
searchFilter := c.Query("search")
|
|
|
|
query := database.DB.Model(&model.DeviceSession{}).
|
|
Where("user_id IN ? AND application_id IN ?", appUserIDs, appIDs)
|
|
|
|
if appIDFilter != "" {
|
|
appID, _ := strconv.Atoi(appIDFilter)
|
|
if appID > 0 {
|
|
query = query.Where("application_id = ?", appID)
|
|
}
|
|
}
|
|
|
|
var total int64
|
|
query.Count(&total)
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
|
offset := (page - 1) * pageSize
|
|
|
|
var sessions []model.DeviceSession
|
|
findQuery := database.DB.Where("user_id IN ? AND application_id IN ?", appUserIDs, appIDs)
|
|
|
|
if appIDFilter != "" {
|
|
appID, _ := strconv.Atoi(appIDFilter)
|
|
if appID > 0 {
|
|
findQuery = findQuery.Where("application_id = ?", appID)
|
|
}
|
|
}
|
|
|
|
if err := findQuery.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&sessions).Error; err != nil {
|
|
response.Error(c, 500, "获取会话列表失败")
|
|
return
|
|
}
|
|
|
|
type SessionWithDetails struct {
|
|
model.DeviceSession
|
|
DeviceID string `json:"device_identifier"`
|
|
DeviceName string `json:"device_name"`
|
|
Username string `json:"username"`
|
|
AppName string `json:"app_name"`
|
|
IsOnline bool `json:"is_online"`
|
|
}
|
|
|
|
result := make([]SessionWithDetails, 0, len(sessions))
|
|
for _, session := range sessions {
|
|
var device model.UserDevice
|
|
if err := database.DB.First(&device, session.DeviceID).Error; err != nil {
|
|
continue
|
|
}
|
|
|
|
var user model.AppUser
|
|
if err := database.DB.First(&user, session.UserID).Error; err != nil {
|
|
continue
|
|
}
|
|
|
|
var app model.Application
|
|
if err := database.DB.First(&app, session.ApplicationID).Error; err != nil {
|
|
continue
|
|
}
|
|
|
|
heartbeatTimeout := app.HeartbeatTimeout
|
|
if heartbeatTimeout == 0 {
|
|
heartbeatTimeout = 300
|
|
}
|
|
timeoutThreshold := time.Now().Add(-time.Duration(heartbeatTimeout) * time.Second)
|
|
isOnline := session.LastHeartbeat != nil && session.LastHeartbeat.After(timeoutThreshold)
|
|
|
|
if searchFilter != "" {
|
|
searchLower := strings.ToLower(searchFilter)
|
|
instanceIDMatch := strings.Contains(strings.ToLower(session.InstanceID), searchLower)
|
|
deviceIDMatch := strings.Contains(strings.ToLower(device.DeviceID), searchLower)
|
|
usernameMatch := strings.Contains(strings.ToLower(user.Username), searchLower)
|
|
deviceNameMatch := strings.Contains(strings.ToLower(device.DeviceName), searchLower)
|
|
|
|
if !instanceIDMatch && !deviceIDMatch && !usernameMatch && !deviceNameMatch {
|
|
continue
|
|
}
|
|
}
|
|
|
|
result = append(result, SessionWithDetails{
|
|
DeviceSession: session,
|
|
DeviceID: device.DeviceID,
|
|
DeviceName: device.DeviceName,
|
|
Username: user.Username,
|
|
AppName: app.Name,
|
|
IsOnline: isOnline,
|
|
})
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"sessions": result,
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": pageSize,
|
|
})
|
|
}
|