package admin import ( "errors" "strconv" "strings" "time" "git.echol.cn/loser/Go-Web-Template/server/global" appModel "git.echol.cn/loser/Go-Web-Template/server/model/app" "git.echol.cn/loser/Go-Web-Template/server/model/common/response" systemSvc "git.echol.cn/loser/Go-Web-Template/server/service/system" "git.echol.cn/loser/Go-Web-Template/server/utils" "github.com/gin-gonic/gin" "github.com/google/uuid" "gorm.io/gorm" "gorm.io/gorm/clause" ) type AppUsersApi struct{} type appUserListItem struct { ID uint `json:"id"` UUID string `json:"uuid"` Username string `json:"username"` Enable int `json:"enable"` NickName string `json:"nickName,omitempty"` AccountBalanceLi int64 `json:"accountBalanceLi"` GameCoinBalanceLi int64 `json:"gameCoinBalanceLi"` LastLoginAt *time.Time `json:"lastLoginAt,omitempty"` LastActiveAt *time.Time `json:"lastActiveAt,omitempty"` IsOnline bool `json:"isOnline"` } type createAppUserRequest struct { Username string `json:"username" binding:"required,min=3,max=64"` Password string `json:"password" binding:"required,min=6,max=128"` Enable *bool `json:"enable"` NickName string `json:"nickName"` WelcomePhrase string `json:"welcomePhrase"` } type updateAppUserRequest struct { Enable *bool `json:"enable"` WelcomePhrase *string `json:"welcomePhrase"` } type resetPasswordRequest struct { NewPassword string `json:"newPassword" binding:"required,min=6,max=128"` } type appUserDetailItem struct { ID uint `json:"id"` UUID string `json:"uuid"` Username string `json:"username"` Enable int `json:"enable"` NickName string `json:"nickName,omitempty"` WelcomePhrase string `json:"welcomePhrase,omitempty"` } type appUserOverviewResponse struct { User struct { ID uint `json:"id"` UUID string `json:"uuid"` Username string `json:"username"` Enable int `json:"enable"` NickName string `json:"nickName,omitempty"` } `json:"user"` Asset struct { AccountBalanceLi int64 `json:"accountBalanceLi"` GameCoinBalanceLi int64 `json:"gameCoinBalanceLi"` } `json:"asset"` Activity struct { LastLoginAt *time.Time `json:"lastLoginAt,omitempty"` LastActiveAt *time.Time `json:"lastActiveAt,omitempty"` LastActiveIP string `json:"lastActiveIp,omitempty"` IsOnline bool `json:"isOnline"` } `json:"activity"` RecentLoginLogs []appModel.AppLoginLog `json:"recentLoginLogs"` RecentTx []appModel.AppAssetTransaction `json:"recentTx"` Invite struct { InviterUserID *uint `json:"inviterUserId,omitempty"` InviterUsername string `json:"inviterUsername,omitempty"` InvitedCount int64 `json:"invitedCount"` HasUnusedCode bool `json:"hasUnusedCode"` UnusedCodeLast4 string `json:"unusedCodeLast4,omitempty"` UnusedExpiresAt *time.Time `json:"unusedExpiresAt,omitempty"` } `json:"invite"` } // GetAppUsersList // @Tags AppUsersAdmin // @Summary 后台获取玩家列表 // @Security ApiKeyAuth // @Produce application/json // @Param page query int false "页码" default(1) // @Param pageSize query int false "每页大小" default(10) // @Param keyword query string false "用户名关键字" // @Param isOnline query bool false "是否在线(基于 lastActiveAt 与在线窗口判断)" // @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" // @Router /admin/app-users [get] func (a *AppUsersApi) GetAppUsersList(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) if page <= 0 { page = 1 } if pageSize <= 0 || pageSize > 200 { pageSize = 10 } keyword := c.Query("keyword") var isOnlineFilter *bool if raw := strings.TrimSpace(c.Query("isOnline")); raw != "" { if v, err := strconv.ParseBool(raw); err == nil { isOnlineFilter = &v } } db := global.GVA_DB.Model(&appModel.AppUser{}) if keyword != "" { db = db.Where("username ILIKE ?", "%"+keyword+"%") } // 在线状态过滤:通过 join 活跃表,按窗口阈值判断 windowMin := systemSvc.GetAppOnlineWindowMinutes() now := time.Now() if isOnlineFilter != nil { threshold := now.Add(-time.Duration(windowMin) * time.Minute) db = db.Joins("LEFT JOIN app_user_activity act ON act.app_user_id = app_users.id") db = db.Select("app_users.*") if *isOnlineFilter { db = db.Where("act.last_active_at IS NOT NULL AND act.last_active_at >= ?", threshold) } else { db = db.Where("act.last_active_at IS NULL OR act.last_active_at < ?", threshold) } } var total int64 if err := db.Count(&total).Error; err != nil { response.FailWithMessage(err.Error(), c) return } var users []appModel.AppUser if err := db.Order("id desc").Limit(pageSize).Offset((page - 1) * pageSize).Find(&users).Error; err != nil { response.FailWithMessage(err.Error(), c) return } ids := make([]uint, 0, len(users)) for _, u := range users { ids = append(ids, u.ID) } profiles := map[uint]appModel.AppUserProfile{} if len(ids) > 0 { var list []appModel.AppUserProfile _ = global.GVA_DB.Where("app_user_id IN ?", ids).Find(&list).Error for _, p := range list { profiles[p.AppUserID] = p } } assets := map[uint]appModel.AppUserAsset{} if len(ids) > 0 { var list []appModel.AppUserAsset _ = global.GVA_DB.Where("app_user_id IN ?", ids).Find(&list).Error for _, a := range list { assets[a.AppUserID] = a } } activities := map[uint]appModel.AppUserActivity{} if len(ids) > 0 { var list []appModel.AppUserActivity _ = global.GVA_DB.Where("app_user_id IN ?", ids).Find(&list).Error for _, a := range list { activities[a.AppUserID] = a } } items := make([]appUserListItem, 0, len(users)) for _, u := range users { p := profiles[u.ID] asset := assets[u.ID] act := activities[u.ID] isOnline := false if act.LastActiveAt != nil && windowMin > 0 { isOnline = now.Sub(*act.LastActiveAt) <= time.Duration(windowMin)*time.Minute } items = append(items, appUserListItem{ ID: u.ID, UUID: u.UUID.String(), Username: u.Username, Enable: u.Enable, NickName: p.NickName, AccountBalanceLi: asset.AccountBalanceLi, GameCoinBalanceLi: asset.GameCoinBalanceLi, LastLoginAt: act.LastLoginAt, LastActiveAt: act.LastActiveAt, IsOnline: isOnline, }) } response.OkWithDetailed(response.PageResult{ List: items, Total: total, Page: page, PageSize: pageSize, }, "获取成功", c) } type rechargeRequest struct { AssetType string `json:"assetType" binding:"required"` RechargeType string `json:"rechargeType" binding:"required"` DeltaLi int64 `json:"deltaLi" binding:"required"` Reason string `json:"reason" binding:"required"` } // RechargeAppUser // @Tags AppUsersAdmin // @Summary 后台手动充值(账户余额/游戏币,单位:厘) // @Security ApiKeyAuth // @Produce application/json // @Param id path int true "玩家ID" // @Param data body rechargeRequest true "充值信息" // @Success 200 {object} response.Response{msg=string} "充值成功" // @Router /admin/app-users/{id}/recharge [post] func (a *AppUsersApi) RechargeAppUser(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if id <= 0 { response.FailWithMessage("参数错误", c) return } var req rechargeRequest if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } req.AssetType = strings.TrimSpace(req.AssetType) req.RechargeType = strings.TrimSpace(req.RechargeType) req.Reason = strings.TrimSpace(req.Reason) if req.Reason == "" { response.FailWithMessage("原因不能为空", c) return } if req.DeltaLi == 0 { response.FailWithMessage("变动金额不能为 0", c) return } if req.AssetType != "account" && req.AssetType != "game_coin" { response.FailWithMessage("assetType 不合法", c) return } if req.RechargeType != "offline" && req.RechargeType != "online_not_arrived" && req.RechargeType != "activity_subsidy" && req.RechargeType != "manual_adjustment" { response.FailWithMessage("rechargeType 不合法", c) return } if req.RechargeType == "activity_subsidy" && req.AssetType != "game_coin" { response.FailWithMessage("活动补贴仅支持充值游戏币", c) return } operatorID := utils.GetUserID(c) if operatorID == 0 { response.FailWithMessage("未登录", c) return } if err := global.GVA_DB.Transaction(func(tx *gorm.DB) error { // 确保玩家存在 var user appModel.AppUser if err := tx.First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("用户不存在") } return err } var asset appModel.AppUserAsset err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("app_user_id = ?", user.ID).First(&asset).Error if errors.Is(err, gorm.ErrRecordNotFound) { asset = appModel.AppUserAsset{AppUserID: user.ID} if err := tx.Create(&asset).Error; err != nil { return err } if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("app_user_id = ?", user.ID).First(&asset).Error; err != nil { return err } } else if err != nil { return err } var before, after int64 if req.AssetType == "account" { before = asset.AccountBalanceLi after = before + req.DeltaLi if after < 0 { return errors.New("账户余额不足") } asset.AccountBalanceLi = after if err := tx.Model(&asset).Select("account_balance_li").Updates(&asset).Error; err != nil { return err } } else { before = asset.GameCoinBalanceLi after = before + req.DeltaLi if after < 0 { return errors.New("游戏币余额不足") } asset.GameCoinBalanceLi = after if err := tx.Model(&asset).Select("game_coin_balance_li").Updates(&asset).Error; err != nil { return err } } txRec := appModel.AppAssetTransaction{ AppUserID: user.ID, AssetType: appModel.AppAssetType(req.AssetType), RechargeType: appModel.AppRechargeType(req.RechargeType), DeltaLi: req.DeltaLi, BeforeLi: before, AfterLi: after, Reason: req.Reason, OperatorUserID: operatorID, CreatedAt: time.Now(), } return tx.Create(&txRec).Error }); err != nil { response.FailWithMessage(err.Error(), c) return } response.OkWithMessage("充值成功", c) } // GetAppUserById // @Tags AppUsersAdmin // @Summary 后台获取玩家详情 // @Security ApiKeyAuth // @Produce application/json // @Param id path int true "玩家ID" // @Success 200 {object} response.Response{data=appUserDetailItem,msg=string} "获取成功" // @Router /admin/app-users/{id} [get] func (a *AppUsersApi) GetAppUserById(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if id <= 0 { response.FailWithMessage("参数错误", c) return } var user appModel.AppUser if err := global.GVA_DB.First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { response.FailWithMessage("用户不存在", c) return } response.FailWithMessage(err.Error(), c) return } var profile appModel.AppUserProfile _ = global.GVA_DB.Where("app_user_id = ?", user.ID).First(&profile).Error response.OkWithData(appUserDetailItem{ ID: user.ID, UUID: user.UUID.String(), Username: user.Username, Enable: user.Enable, NickName: profile.NickName, WelcomePhrase: profile.WelcomePhrase, }, c) } // GetAppUserOverview // @Tags AppUsersAdmin // @Summary 后台获取玩家聚合详情(概览) // @Security ApiKeyAuth // @Produce application/json // @Param id path int true "玩家ID" // @Success 200 {object} response.Response{data=appUserOverviewResponse,msg=string} "获取成功" // @Router /admin/app-users/{id}/overview [get] func (a *AppUsersApi) GetAppUserOverview(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if id <= 0 { response.FailWithMessage("参数错误", c) return } var user appModel.AppUser if err := global.GVA_DB.First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { response.FailWithMessage("用户不存在", c) return } response.FailWithMessage(err.Error(), c) return } var profile appModel.AppUserProfile _ = global.GVA_DB.Where("app_user_id = ?", user.ID).First(&profile).Error var asset appModel.AppUserAsset _ = global.GVA_DB.Where("app_user_id = ?", user.ID).First(&asset).Error var act appModel.AppUserActivity _ = global.GVA_DB.Where("app_user_id = ?", user.ID).First(&act).Error windowMin := systemSvc.GetAppOnlineWindowMinutes() now := time.Now() isOnline := false if act.LastActiveAt != nil && windowMin > 0 { isOnline = now.Sub(*act.LastActiveAt) <= time.Duration(windowMin)*time.Minute } var recentLoginLogs []appModel.AppLoginLog _ = global.GVA_DB.Where("app_user_id = ?", user.ID).Order("id desc").Limit(20).Find(&recentLoginLogs).Error var recentTx []appModel.AppAssetTransaction _ = global.GVA_DB.Where("app_user_id = ?", user.ID).Order("id desc").Limit(20).Find(&recentTx).Error // invite summary var invitedCount int64 _ = global.GVA_DB.Model(&appModel.AppUserInviteRelation{}).Where("inviter_user_id = ?", user.ID).Count(&invitedCount).Error var inviterUserID *uint var inviterUsername string { var rel appModel.AppUserInviteRelation if err := global.GVA_DB.Where("invitee_user_id = ?", user.ID).First(&rel).Error; err == nil { inviterUserID = &rel.InviterUserID var inviter appModel.AppUser _ = global.GVA_DB.Select("id, username").First(&inviter, rel.InviterUserID).Error inviterUsername = inviter.Username } } hasUnusedCode := false var unusedLast4 string var unusedExpiresAt *time.Time { var code appModel.AppInviteCode err := global.GVA_DB. Where("created_by_user_id = ? AND status = ?", user.ID, appModel.InviteCodeStatusUnused). Order("id desc"). First(&code).Error if err == nil { hasUnusedCode = true unusedLast4 = code.CodeLast4 unusedExpiresAt = code.ExpiresAt } } var resp appUserOverviewResponse resp.User.ID = user.ID resp.User.UUID = user.UUID.String() resp.User.Username = user.Username resp.User.Enable = user.Enable resp.User.NickName = profile.NickName resp.Asset.AccountBalanceLi = asset.AccountBalanceLi resp.Asset.GameCoinBalanceLi = asset.GameCoinBalanceLi resp.Activity.LastLoginAt = act.LastLoginAt resp.Activity.LastActiveAt = act.LastActiveAt resp.Activity.LastActiveIP = act.LastActiveIP resp.Activity.IsOnline = isOnline resp.RecentLoginLogs = recentLoginLogs resp.RecentTx = recentTx resp.Invite.InviterUserID = inviterUserID resp.Invite.InviterUsername = inviterUsername resp.Invite.InvitedCount = invitedCount resp.Invite.HasUnusedCode = hasUnusedCode resp.Invite.UnusedCodeLast4 = unusedLast4 resp.Invite.UnusedExpiresAt = unusedExpiresAt response.OkWithDetailed(resp, "获取成功", c) } // CreateAppUser // @Tags AppUsersAdmin // @Summary 后台手动新增玩家 // @Security ApiKeyAuth // @Produce application/json // @Param data body createAppUserRequest true "玩家信息" // @Success 200 {object} response.Response{msg=string} "创建成功" // @Router /admin/app-users [post] func (a *AppUsersApi) CreateAppUser(c *gin.Context) { var req createAppUserRequest if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } req.Username = strings.TrimSpace(req.Username) req.NickName = strings.TrimSpace(req.NickName) req.WelcomePhrase = strings.TrimSpace(req.WelcomePhrase) enable := 1 if req.Enable != nil && !*req.Enable { enable = 2 } if err := global.GVA_DB.Transaction(func(tx *gorm.DB) error { var existed appModel.AppUser if err := tx.Where("username = ?", req.Username).First(&existed).Error; err == nil { return errors.New("用户名已存在") } else if !errors.Is(err, gorm.ErrRecordNotFound) { return err } user := appModel.AppUser{ UUID: uuid.New(), Username: req.Username, Password: utils.BcryptHash(req.Password), Enable: enable, } if err := tx.Create(&user).Error; err != nil { return err } profile := appModel.AppUserProfile{ AppUserID: user.ID, WelcomePhrase: req.WelcomePhrase, NickName: firstNonEmpty(req.NickName, req.Username), } return tx.Create(&profile).Error }); err != nil { response.FailWithMessage(err.Error(), c) return } response.OkWithMessage("创建成功", c) } // UpdateAppUser // @Tags AppUsersAdmin // @Summary 编辑玩家(仅启用状态/欢迎词) // @Security ApiKeyAuth // @Produce application/json // @Param id path int true "玩家ID" // @Param data body updateAppUserRequest true "更新字段" // @Success 200 {object} response.Response{msg=string} "更新成功" // @Router /admin/app-users/{id} [patch] func (a *AppUsersApi) UpdateAppUser(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if id <= 0 { response.FailWithMessage("参数错误", c) return } var req updateAppUserRequest if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } if err := global.GVA_DB.Transaction(func(tx *gorm.DB) error { var user appModel.AppUser if err := tx.First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("用户不存在") } return err } if req.Enable != nil { if *req.Enable { user.Enable = 1 } else { user.Enable = 2 } if err := tx.Model(&user).Select("enable").Updates(&user).Error; err != nil { return err } } if req.WelcomePhrase != nil { wp := strings.TrimSpace(*req.WelcomePhrase) var profile appModel.AppUserProfile err := tx.Where("app_user_id = ?", user.ID).First(&profile).Error if errors.Is(err, gorm.ErrRecordNotFound) { profile = appModel.AppUserProfile{ AppUserID: user.ID, WelcomePhrase: wp, NickName: user.Username, } return tx.Create(&profile).Error } if err != nil { return err } profile.WelcomePhrase = wp return tx.Model(&profile).Select("welcome_phrase").Updates(&profile).Error } return nil }); err != nil { response.FailWithMessage(err.Error(), c) return } response.OkWithMessage("更新成功", c) } // ResetAppUserPassword // @Tags AppUsersAdmin // @Summary 重置玩家密码 // @Security ApiKeyAuth // @Produce application/json // @Param id path int true "玩家ID" // @Param data body resetPasswordRequest true "新密码" // @Success 200 {object} response.Response{msg=string} "重置成功" // @Router /admin/app-users/{id}/reset-password [post] func (a *AppUsersApi) ResetAppUserPassword(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if id <= 0 { response.FailWithMessage("参数错误", c) return } var req resetPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } var user appModel.AppUser if err := global.GVA_DB.First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { response.FailWithMessage("用户不存在", c) return } response.FailWithMessage(err.Error(), c) return } if err := global.GVA_DB.Model(&user).Update("password", utils.BcryptHash(req.NewPassword)).Error; err != nil { response.FailWithMessage(err.Error(), c) return } response.OkWithMessage("重置成功", c) } // ToggleAppUserEnable // @Tags AppUsersAdmin // @Summary 启用/停用玩家 // @Security ApiKeyAuth // @Produce application/json // @Param id path int true "玩家ID" // @Success 200 {object} response.Response{msg=string} "操作成功" // @Router /admin/app-users/{id}/toggle-enable [post] func (a *AppUsersApi) ToggleAppUserEnable(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if id <= 0 { response.FailWithMessage("参数错误", c) return } var user appModel.AppUser if err := global.GVA_DB.First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { response.FailWithMessage("用户不存在", c) return } response.FailWithMessage(err.Error(), c) return } next := 1 if user.Enable == 1 { next = 2 } if err := global.GVA_DB.Model(&user).Update("enable", next).Error; err != nil { response.FailWithMessage(err.Error(), c) return } response.OkWithMessage("操作成功", c) } // DeleteAppUser // @Tags AppUsersAdmin // @Summary 软删除玩家 // @Security ApiKeyAuth // @Produce application/json // @Param id path int true "玩家ID" // @Success 200 {object} response.Response{msg=string} "删除成功" // @Router /admin/app-users/{id} [delete] func (a *AppUsersApi) DeleteAppUser(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if id <= 0 { response.FailWithMessage("参数错误", c) return } var user appModel.AppUser if err := global.GVA_DB.First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { response.FailWithMessage("用户不存在", c) return } response.FailWithMessage(err.Error(), c) return } if err := global.GVA_DB.Delete(&user).Error; err != nil { response.FailWithMessage(err.Error(), c) return } response.OkWithMessage("删除成功", c) } func firstNonEmpty(values ...string) string { for _, v := range values { if v != "" { return v } } return "" }