新增几大中心功能

This commit is contained in:
Administrator
2026-04-23 15:29:07 +08:00
parent 5c2a146a44
commit c6354ee065
123 changed files with 13074 additions and 229 deletions

View File

@@ -0,0 +1,301 @@
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)
}