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" appSvc "git.echol.cn/loser/Go-Web-Template/server/service/app" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type AppInviteCodesApi struct{} type appInviteCodeListItem struct { ID uint `json:"id"` CreatedByUserID uint `json:"createdByUserId"` CodeLast4 string `json:"codeLast4"` Status appModel.InviteCodeStatus `json:"status"` ExpiresAt *time.Time `json:"expiresAt,omitempty"` UsedByUserID *uint `json:"usedByUserId,omitempty"` UsedAt *time.Time `json:"usedAt,omitempty"` CreatedAt time.Time `json:"createdAt"` } type appInviteCodeDetailResponse struct { Code appModel.AppInviteCode `json:"code"` Inviter struct { ID uint `json:"id"` Username string `json:"username"` } `json:"inviter"` Invitee *struct { ID uint `json:"id"` Username string `json:"username"` } `json:"invitee,omitempty"` Relation *appModel.AppUserInviteRelation `json:"relation,omitempty"` } type issueInviteCodeRequest struct { UserID uint `json:"userId" binding:"required"` ExpireHours *int `json:"expireHours"` } type issueInviteCodeResponse struct { Code string `json:"code"` CodeLast4 string `json:"codeLast4"` ExpiresAt *time.Time `json:"expiresAt,omitempty"` } type clearUnusedRequest struct { UserID uint `json:"userId" binding:"required"` Scope string `json:"scope"` // active|all } // GetAppInviteCodesList // @Tags AppInviteCodesAdmin // @Summary 后台获取玩家邀请码列表 // @Security ApiKeyAuth // @Produce application/json // @Param page query int false "页码" default(1) // @Param pageSize query int false "每页大小" default(10) // @Param status query string false "状态 unused|used|revoked|expired" // @Param createdByUserId query int false "邀请人用户ID" // @Param usedByUserId query int false "使用人用户ID" // @Param codeLast4 query string false "末4位" // @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" // @Router /admin/app-invite-codes [get] func (a *AppInviteCodesApi) GetAppInviteCodesList(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 } status := strings.TrimSpace(c.Query("status")) createdByUserID, _ := strconv.Atoi(c.Query("createdByUserId")) usedByUserID, _ := strconv.Atoi(c.Query("usedByUserId")) codeLast4 := strings.TrimSpace(c.Query("codeLast4")) db := global.GVA_DB.Model(&appModel.AppInviteCode{}) if status != "" && status != "expired" { db = db.Where("status = ?", status) } if createdByUserID > 0 { db = db.Where("created_by_user_id = ?", createdByUserID) } if usedByUserID > 0 { db = db.Where("used_by_user_id = ?", usedByUserID) } if codeLast4 != "" { db = db.Where("code_last4 = ?", codeLast4) } // 仅查询“已过期”视图:expires_at < now AND status=unused if status == "expired" { now := time.Now() db = db.Where("status = ?", appModel.InviteCodeStatusUnused).Where("expires_at IS NOT NULL AND expires_at <= ?", now) } var total int64 if err := db.Count(&total).Error; err != nil { response.FailWithMessage(err.Error(), c) return } var list []appModel.AppInviteCode if err := db.Order("id desc").Limit(pageSize).Offset((page - 1) * pageSize).Find(&list).Error; err != nil { response.FailWithMessage(err.Error(), c) return } items := make([]appInviteCodeListItem, 0, len(list)) for _, rec := range list { items = append(items, appInviteCodeListItem{ ID: rec.ID, CreatedByUserID: rec.CreatedByUserID, CodeLast4: rec.CodeLast4, Status: rec.Status, ExpiresAt: rec.ExpiresAt, UsedByUserID: rec.UsedByUserID, UsedAt: rec.UsedAt, CreatedAt: rec.CreatedAt, }) } response.OkWithDetailed(response.PageResult{ List: items, Total: total, Page: page, PageSize: pageSize, }, "获取成功", c) } // GetAppInviteCodeById // @Tags AppInviteCodesAdmin // @Summary 后台获取玩家邀请码详情 // @Security ApiKeyAuth // @Produce application/json // @Param id path int true "邀请码ID" // @Success 200 {object} response.Response{data=appInviteCodeDetailResponse,msg=string} "获取成功" // @Router /admin/app-invite-codes/{id} [get] func (a *AppInviteCodesApi) GetAppInviteCodeById(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if id <= 0 { response.FailWithMessage("参数错误", c) return } var code appModel.AppInviteCode if err := global.GVA_DB.First(&code, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { response.FailWithMessage("记录不存在", c) return } response.FailWithMessage(err.Error(), c) return } var inviter appModel.AppUser _ = global.GVA_DB.Select("id, username").First(&inviter, code.CreatedByUserID).Error var relation appModel.AppUserInviteRelation relErr := global.GVA_DB.Where("invite_code_id = ?", code.ID).First(&relation).Error var invitee *appModel.AppUser = nil if relErr == nil { var u appModel.AppUser if err := global.GVA_DB.Select("id, username").First(&u, relation.InviteeUserID).Error; err == nil { invitee = &u } } payload := appInviteCodeDetailResponse{Code: code} payload.Inviter.ID = inviter.ID payload.Inviter.Username = inviter.Username if invitee != nil { payload.Invitee = &struct { ID uint `json:"id"` Username string `json:"username"` }{ID: invitee.ID, Username: invitee.Username} } if relErr == nil { payload.Relation = &relation } response.OkWithData(payload, c) } // RevokeAppInviteCode // @Tags AppInviteCodesAdmin // @Summary 作废玩家邀请码 // @Security ApiKeyAuth // @Produce application/json // @Param id path int true "邀请码ID" // @Success 200 {object} response.Response{msg=string} "作废成功" // @Router /admin/app-invite-codes/{id}/revoke [post] func (a *AppInviteCodesApi) RevokeAppInviteCode(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if id <= 0 { response.FailWithMessage("参数错误", c) return } var code appModel.AppInviteCode if err := global.GVA_DB.First(&code, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { response.FailWithMessage("记录不存在", c) return } response.FailWithMessage(err.Error(), c) return } if code.Status == appModel.InviteCodeStatusUsed { response.FailWithMessage("已使用的邀请码不可作废", c) return } if code.Status == appModel.InviteCodeStatusRevoked { response.OkWithMessage("已作废", c) return } if err := global.GVA_DB.Model(&code).Update("status", appModel.InviteCodeStatusRevoked).Error; err != nil { response.FailWithMessage(err.Error(), c) return } response.OkWithMessage("作废成功", c) } // IssueAppInviteCodeForUser // @Tags AppInviteCodesAdmin // @Summary 后台代用户生成邀请码(返回明文一次性) // @Security ApiKeyAuth // @Produce application/json // @Param data body issueInviteCodeRequest true "用户ID" // @Success 200 {object} response.Response{data=issueInviteCodeResponse,msg=string} "生成成功" // @Router /admin/app-invite-codes/issue [post] func (a *AppInviteCodesApi) IssueAppInviteCodeForUser(c *gin.Context) { var req issueInviteCodeRequest if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } // 先强制作废该用户所有 active unused now := time.Now() revokeDB := global.GVA_DB.Model(&appModel.AppInviteCode{}). Where("created_by_user_id = ? AND status = ?", req.UserID, appModel.InviteCodeStatusUnused). Where(global.GVA_DB.Where("expires_at IS NULL").Or("expires_at > ?", now)). Update("status", appModel.InviteCodeStatusRevoked) if revokeDB.Error != nil { response.FailWithMessage(revokeDB.Error.Error(), c) return } // 复用现有生成逻辑(按系统参数 expireHours),若你传了 expireHours,本期先忽略,后续可扩展为临时覆盖。 res, err := appSvc.AppInviteCodeServiceApp.Generate(req.UserID) if err != nil { // 若生成被“已有未使用码”拦住,说明 revoke 未覆盖(例如过期时间为空/逻辑差异),直接返回错误即可。 response.FailWithMessage(err.Error(), c) return } response.OkWithData(issueInviteCodeResponse{Code: res.Code, CodeLast4: res.CodeLast4, ExpiresAt: res.ExpiresAt}, c) } // ClearUnusedInviteCodes // @Tags AppInviteCodesAdmin // @Summary 强制清空用户未使用邀请码(批量作废) // @Security ApiKeyAuth // @Produce application/json // @Param data body clearUnusedRequest true "用户ID" // @Success 200 {object} response.Response{msg=string} "清空成功" // @Router /admin/app-invite-codes/clear-unused [post] func (a *AppInviteCodesApi) ClearUnusedInviteCodes(c *gin.Context) { var req clearUnusedRequest if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } scope := strings.TrimSpace(req.Scope) if scope == "" { scope = "active" } db := global.GVA_DB.Model(&appModel.AppInviteCode{}). Where("created_by_user_id = ? AND status = ?", req.UserID, appModel.InviteCodeStatusUnused) if scope == "active" { now := time.Now() db = db.Where(global.GVA_DB.Where("expires_at IS NULL").Or("expires_at > ?", now)) } if err := db.Update("status", appModel.InviteCodeStatusRevoked).Error; err != nil { response.FailWithMessage(err.Error(), c) return } response.OkWithMessage("清空成功", c) }