Files
Go-Web-Template/server/api/v1/admin/app_invite_codes.go
2026-04-23 15:29:07 +08:00

302 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}