✨ 新增几大中心功能
This commit is contained in:
234
server/api/v1/admin/app_asset_transactions.go
Normal file
234
server/api/v1/admin/app_asset_transactions.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"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"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AppAssetTransactionsApi struct{}
|
||||
|
||||
type appAssetTransactionListItem struct {
|
||||
ID uint `json:"id"`
|
||||
AppUserID uint `json:"appUserId"`
|
||||
Username string `json:"username"`
|
||||
AssetType string `json:"assetType"`
|
||||
RechargeType string `json:"rechargeType"`
|
||||
DeltaLi int64 `json:"deltaLi"`
|
||||
BeforeLi int64 `json:"beforeLi"`
|
||||
AfterLi int64 `json:"afterLi"`
|
||||
Reason string `json:"reason"`
|
||||
OperatorUserID uint `json:"operatorUserId"`
|
||||
OperatorName string `json:"operatorName"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// GetAppAssetTransactionsList
|
||||
// @Tags AppAssetTransactionsAdmin
|
||||
// @Summary 后台获取玩家资产流水列表(单位:厘)
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce application/json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param pageSize query int false "每页大小" default(10)
|
||||
// @Param userId query int false "玩家ID"
|
||||
// @Param username query string false "用户名(模糊)"
|
||||
// @Param assetType query string false "资产类型:account/game_coin"
|
||||
// @Param rechargeType query string false "变动类型(充值类型):offline/online_not_arrived/activity_subsidy/manual_adjustment"
|
||||
// @Param operatorUserId query int false "操作人ID"
|
||||
// @Param keyword query string false "关键字(原因模糊)"
|
||||
// @Param startAt query string false "开始时间(ISO8601/RFC3339)"
|
||||
// @Param endAt query string false "结束时间(ISO8601/RFC3339)"
|
||||
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
|
||||
// @Router /admin/app-asset-transactions [get]
|
||||
func (a *AppAssetTransactionsApi) GetAppAssetTransactionsList(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
|
||||
}
|
||||
|
||||
var (
|
||||
userID uint
|
||||
operatorUserID uint
|
||||
)
|
||||
if raw := strings.TrimSpace(c.Query("userId")); raw != "" {
|
||||
if v, err := strconv.Atoi(raw); err == nil && v > 0 {
|
||||
userID = uint(v)
|
||||
}
|
||||
}
|
||||
if raw := strings.TrimSpace(c.Query("operatorUserId")); raw != "" {
|
||||
if v, err := strconv.Atoi(raw); err == nil && v > 0 {
|
||||
operatorUserID = uint(v)
|
||||
}
|
||||
}
|
||||
|
||||
username := strings.TrimSpace(c.Query("username"))
|
||||
assetType := strings.TrimSpace(c.Query("assetType"))
|
||||
rechargeType := strings.TrimSpace(c.Query("rechargeType"))
|
||||
keyword := strings.TrimSpace(c.Query("keyword"))
|
||||
|
||||
var startAt *time.Time
|
||||
if raw := strings.TrimSpace(c.Query("startAt")); raw != "" {
|
||||
if t, err := time.Parse(time.RFC3339, raw); err == nil {
|
||||
startAt = &t
|
||||
}
|
||||
}
|
||||
var endAt *time.Time
|
||||
if raw := strings.TrimSpace(c.Query("endAt")); raw != "" {
|
||||
if t, err := time.Parse(time.RFC3339, raw); err == nil {
|
||||
endAt = &t
|
||||
}
|
||||
}
|
||||
|
||||
// tx + join app_users + join sys_users
|
||||
db := global.GVA_DB.Table("app_asset_transactions tx").
|
||||
Joins("JOIN app_users u ON u.id = tx.app_user_id").
|
||||
Joins("LEFT JOIN sys_users su ON su.id = tx.operator_user_id")
|
||||
|
||||
if userID > 0 {
|
||||
db = db.Where("tx.app_user_id = ?", userID)
|
||||
}
|
||||
if username != "" {
|
||||
db = db.Where("u.username ILIKE ?", "%"+username+"%")
|
||||
}
|
||||
if assetType != "" {
|
||||
db = db.Where("tx.asset_type = ?", assetType)
|
||||
}
|
||||
if rechargeType != "" {
|
||||
db = db.Where("tx.recharge_type = ?", rechargeType)
|
||||
}
|
||||
if operatorUserID > 0 {
|
||||
db = db.Where("tx.operator_user_id = ?", operatorUserID)
|
||||
}
|
||||
if keyword != "" {
|
||||
db = db.Where("tx.reason ILIKE ?", "%"+keyword+"%")
|
||||
}
|
||||
if startAt != nil {
|
||||
db = db.Where("tx.created_at >= ?", *startAt)
|
||||
}
|
||||
if endAt != nil {
|
||||
db = db.Where("tx.created_at <= ?", *endAt)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
type row struct {
|
||||
appModel.AppAssetTransaction
|
||||
Username string `gorm:"column:username"`
|
||||
OperatorName string `gorm:"column:operator_name"`
|
||||
}
|
||||
var rows []row
|
||||
if err := db.Select([]string{
|
||||
"tx.id",
|
||||
"tx.app_user_id",
|
||||
"tx.asset_type",
|
||||
"tx.recharge_type",
|
||||
"tx.delta_li",
|
||||
"tx.before_li",
|
||||
"tx.after_li",
|
||||
"tx.reason",
|
||||
"tx.operator_user_id",
|
||||
"tx.created_at",
|
||||
"u.username AS username",
|
||||
"su.username AS operator_name",
|
||||
}).Order("tx.id DESC").Limit(pageSize).Offset((page - 1) * pageSize).Scan(&rows).Error; err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]appAssetTransactionListItem, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
items = append(items, appAssetTransactionListItem{
|
||||
ID: r.ID,
|
||||
AppUserID: r.AppUserID,
|
||||
Username: r.Username,
|
||||
AssetType: string(r.AssetType),
|
||||
RechargeType: string(r.RechargeType),
|
||||
DeltaLi: r.DeltaLi,
|
||||
BeforeLi: r.BeforeLi,
|
||||
AfterLi: r.AfterLi,
|
||||
Reason: r.Reason,
|
||||
OperatorUserID: r.OperatorUserID,
|
||||
OperatorName: r.OperatorName,
|
||||
CreatedAt: r.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
response.OkWithDetailed(response.PageResult{
|
||||
List: items,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// GetAppAssetTransactionById
|
||||
// @Tags AppAssetTransactionsAdmin
|
||||
// @Summary 后台获取玩家资产流水详情
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce application/json
|
||||
// @Param id path int true "流水ID"
|
||||
// @Success 200 {object} response.Response{data=appAssetTransactionListItem,msg=string} "获取成功"
|
||||
// @Router /admin/app-asset-transactions/{id} [get]
|
||||
func (a *AppAssetTransactionsApi) GetAppAssetTransactionById(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id <= 0 {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
|
||||
type row struct {
|
||||
appModel.AppAssetTransaction
|
||||
Username string `gorm:"column:username"`
|
||||
OperatorName string `gorm:"column:operator_name"`
|
||||
}
|
||||
var r row
|
||||
err := global.GVA_DB.Table("app_asset_transactions tx").
|
||||
Joins("JOIN app_users u ON u.id = tx.app_user_id").
|
||||
Joins("LEFT JOIN sys_users su ON su.id = tx.operator_user_id").
|
||||
Select([]string{
|
||||
"tx.id",
|
||||
"tx.app_user_id",
|
||||
"tx.asset_type",
|
||||
"tx.recharge_type",
|
||||
"tx.delta_li",
|
||||
"tx.before_li",
|
||||
"tx.after_li",
|
||||
"tx.reason",
|
||||
"tx.operator_user_id",
|
||||
"tx.created_at",
|
||||
"u.username AS username",
|
||||
"su.username AS operator_name",
|
||||
}).Where("tx.id = ?", id).Scan(&r).Error
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithDetailed(appAssetTransactionListItem{
|
||||
ID: r.ID,
|
||||
AppUserID: r.AppUserID,
|
||||
Username: r.Username,
|
||||
AssetType: string(r.AssetType),
|
||||
RechargeType: string(r.RechargeType),
|
||||
DeltaLi: r.DeltaLi,
|
||||
BeforeLi: r.BeforeLi,
|
||||
AfterLi: r.AfterLi,
|
||||
Reason: r.Reason,
|
||||
OperatorUserID: r.OperatorUserID,
|
||||
OperatorName: r.OperatorName,
|
||||
CreatedAt: r.CreatedAt,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
301
server/api/v1/admin/app_invite_codes.go
Normal file
301
server/api/v1/admin/app_invite_codes.go
Normal 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)
|
||||
}
|
||||
138
server/api/v1/admin/app_login_logs.go
Normal file
138
server/api/v1/admin/app_login_logs.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"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"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AppLoginLogsApi struct{}
|
||||
|
||||
type appLoginLogListItem struct {
|
||||
ID uint `json:"id"`
|
||||
AppUserID uint `json:"appUserId"`
|
||||
Username string `json:"username"`
|
||||
IP string `json:"ip"`
|
||||
Referer string `json:"referer,omitempty"`
|
||||
UserAgent string `json:"userAgent,omitempty"`
|
||||
Status bool `json:"status"`
|
||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type appLoginLogListResponse struct {
|
||||
List []appLoginLogListItem `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
}
|
||||
|
||||
// GetAppLoginLogsList
|
||||
// @Tags AppLoginLogsAdmin
|
||||
// @Summary 后台获取玩家登录日志列表
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce application/json
|
||||
// @Param page query int true "页码"
|
||||
// @Param pageSize query int true "每页条数"
|
||||
// @Param username query string false "用户名"
|
||||
// @Param ip query string false "IP"
|
||||
// @Param status query bool false "状态:true成功 false失败"
|
||||
// @Param startAt query string false "开始时间(ISO8601/RFC3339)"
|
||||
// @Param endAt query string false "结束时间(ISO8601/RFC3339)"
|
||||
// @Success 200 {object} response.Response{data=appLoginLogListResponse,msg=string} "获取成功"
|
||||
// @Router /admin/app-login-logs [get]
|
||||
func (a *AppLoginLogsApi) GetAppLoginLogsList(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
|
||||
}
|
||||
|
||||
username := strings.TrimSpace(c.Query("username"))
|
||||
ip := strings.TrimSpace(c.Query("ip"))
|
||||
|
||||
var status *bool
|
||||
if raw := strings.TrimSpace(c.Query("status")); raw != "" {
|
||||
v, err := strconv.ParseBool(raw)
|
||||
if err == nil {
|
||||
status = &v
|
||||
}
|
||||
}
|
||||
|
||||
var startAt *time.Time
|
||||
if raw := strings.TrimSpace(c.Query("startAt")); raw != "" {
|
||||
if t, err := time.Parse(time.RFC3339, raw); err == nil {
|
||||
startAt = &t
|
||||
}
|
||||
}
|
||||
var endAt *time.Time
|
||||
if raw := strings.TrimSpace(c.Query("endAt")); raw != "" {
|
||||
if t, err := time.Parse(time.RFC3339, raw); err == nil {
|
||||
endAt = &t
|
||||
}
|
||||
}
|
||||
|
||||
db := global.GVA_DB.Model(&appModel.AppLoginLog{})
|
||||
if username != "" {
|
||||
db = db.Where("username LIKE ?", "%"+username+"%")
|
||||
}
|
||||
if ip != "" {
|
||||
db = db.Where("ip LIKE ?", "%"+ip+"%")
|
||||
}
|
||||
if status != nil {
|
||||
db = db.Where("status = ?", *status)
|
||||
}
|
||||
if startAt != nil {
|
||||
db = db.Where("created_at >= ?", *startAt)
|
||||
}
|
||||
if endAt != nil {
|
||||
db = db.Where("created_at <= ?", *endAt)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
var rows []appModel.AppLoginLog
|
||||
if err := db.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&rows).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
rows = []appModel.AppLoginLog{}
|
||||
} else {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]appLoginLogListItem, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
items = append(items, appLoginLogListItem{
|
||||
ID: r.ID,
|
||||
AppUserID: r.AppUserID,
|
||||
Username: r.Username,
|
||||
IP: r.IP,
|
||||
Referer: r.Referer,
|
||||
UserAgent: r.UserAgent,
|
||||
Status: r.Status,
|
||||
ErrorMessage: r.ErrorMessage,
|
||||
CreatedAt: r.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
response.OkWithDetailed(appLoginLogListResponse{
|
||||
List: items,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
283
server/api/v1/admin/app_risk_tags.go
Normal file
283
server/api/v1/admin/app_risk_tags.go
Normal file
@@ -0,0 +1,283 @@
|
||||
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"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AppRiskTagsApi struct{}
|
||||
|
||||
// GetAppRiskTagsList
|
||||
// @Tags AppRiskTagsAdmin
|
||||
// @Summary 后台获取风险标签列表
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce application/json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param pageSize query int false "每页大小" default(50)
|
||||
// @Param keyword query string false "关键字(名称模糊)"
|
||||
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
|
||||
// @Router /admin/app-risk-tags [get]
|
||||
func (a *AppRiskTagsApi) GetAppRiskTagsList(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "50"))
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 || pageSize > 200 {
|
||||
pageSize = 50
|
||||
}
|
||||
keyword := strings.TrimSpace(c.Query("keyword"))
|
||||
|
||||
db := global.GVA_DB.Model(&appModel.AppRiskTag{})
|
||||
if keyword != "" {
|
||||
db = db.Where("name LIKE ?", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
response.FailWithMessage("获取失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
var list []appModel.AppRiskTag
|
||||
if err := db.Order("level desc, id desc").Limit(pageSize).Offset((page - 1) * pageSize).Find(&list).Error; err != nil {
|
||||
response.FailWithMessage("获取失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithData(response.PageResult{List: list, Total: total, Page: page, PageSize: pageSize}, c)
|
||||
}
|
||||
|
||||
type upsertRiskTagRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Level int `json:"level" binding:"required"`
|
||||
Color string `json:"color"`
|
||||
Desc string `json:"desc"`
|
||||
}
|
||||
|
||||
// CreateAppRiskTag
|
||||
// @Tags AppRiskTagsAdmin
|
||||
// @Summary 创建风险标签
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param body body upsertRiskTagRequest true "参数"
|
||||
// @Success 200 {object} response.Response{msg=string} "创建成功"
|
||||
// @Router /admin/app-risk-tags [post]
|
||||
func (a *AppRiskTagsApi) CreateAppRiskTag(c *gin.Context) {
|
||||
var req upsertRiskTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
req.Name = strings.TrimSpace(req.Name)
|
||||
if req.Name == "" {
|
||||
response.FailWithMessage("name 不能为空", c)
|
||||
return
|
||||
}
|
||||
if req.Level < 1 || req.Level > 5 {
|
||||
response.FailWithMessage("level 必须在 1-5", c)
|
||||
return
|
||||
}
|
||||
rec := appModel.AppRiskTag{
|
||||
Name: req.Name,
|
||||
Level: req.Level,
|
||||
Color: strings.TrimSpace(req.Color),
|
||||
Desc: strings.TrimSpace(req.Desc),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := global.GVA_DB.Create(&rec).Error; err != nil {
|
||||
response.FailWithMessage("创建失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("创建成功", c)
|
||||
}
|
||||
|
||||
// UpdateAppRiskTag
|
||||
// @Tags AppRiskTagsAdmin
|
||||
// @Summary 更新风险标签
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param id path int true "标签ID"
|
||||
// @Param body body upsertRiskTagRequest true "参数"
|
||||
// @Success 200 {object} response.Response{msg=string} "更新成功"
|
||||
// @Router /admin/app-risk-tags/{id} [put]
|
||||
func (a *AppRiskTagsApi) UpdateAppRiskTag(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id <= 0 {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
var req upsertRiskTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
req.Name = strings.TrimSpace(req.Name)
|
||||
if req.Name == "" {
|
||||
response.FailWithMessage("name 不能为空", c)
|
||||
return
|
||||
}
|
||||
if req.Level < 1 || req.Level > 5 {
|
||||
response.FailWithMessage("level 必须在 1-5", c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := global.GVA_DB.Model(&appModel.AppRiskTag{}).Where("id = ?", uint(id)).Updates(map[string]any{
|
||||
"name": req.Name,
|
||||
"level": req.Level,
|
||||
"color": strings.TrimSpace(req.Color),
|
||||
"desc": strings.TrimSpace(req.Desc),
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
response.FailWithMessage("更新失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("更新成功", c)
|
||||
}
|
||||
|
||||
// DeleteAppRiskTag
|
||||
// @Tags AppRiskTagsAdmin
|
||||
// @Summary 删除风险标签
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce application/json
|
||||
// @Param id path int true "标签ID"
|
||||
// @Success 200 {object} response.Response{msg=string} "删除成功"
|
||||
// @Router /admin/app-risk-tags/{id} [delete]
|
||||
func (a *AppRiskTagsApi) DeleteAppRiskTag(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id <= 0 {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
if err := global.GVA_DB.Delete(&appModel.AppRiskTag{}, uint(id)).Error; err != nil {
|
||||
response.FailWithMessage("删除失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("删除成功", c)
|
||||
}
|
||||
|
||||
type userTagRow struct {
|
||||
appModel.AppUserRiskTag
|
||||
TagName string `json:"tagName" gorm:"column:tag_name"`
|
||||
TagLevel int `json:"tagLevel" gorm:"column:tag_level"`
|
||||
TagColor string `json:"tagColor" gorm:"column:tag_color"`
|
||||
}
|
||||
|
||||
// GetAppUserRiskTags
|
||||
// @Tags AppRiskTagsAdmin
|
||||
// @Summary 获取玩家风险标签
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce application/json
|
||||
// @Param id path int true "玩家ID"
|
||||
// @Success 200 {object} response.Response{data=[]userTagRow,msg=string} "获取成功"
|
||||
// @Router /admin/app-users/{id}/risk-tags [get]
|
||||
func (a *AppRiskTagsApi) GetAppUserRiskTags(c *gin.Context) {
|
||||
userID, _ := strconv.Atoi(c.Param("id"))
|
||||
if userID <= 0 {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
var list []userTagRow
|
||||
if err := global.GVA_DB.
|
||||
Table("app_user_risk_tags").
|
||||
Joins("LEFT JOIN app_risk_tags t ON t.id = app_user_risk_tags.tag_id").
|
||||
Select("app_user_risk_tags.*, t.name AS tag_name, t.level AS tag_level, t.color AS tag_color").
|
||||
Where("app_user_risk_tags.app_user_id = ?", uint(userID)).
|
||||
Order("app_user_risk_tags.id desc").
|
||||
Scan(&list).Error; err != nil {
|
||||
response.FailWithMessage("获取失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(list, c)
|
||||
}
|
||||
|
||||
type setUserTagRequest struct {
|
||||
TagID uint `json:"tagId" binding:"required"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// AddAppUserRiskTag
|
||||
// @Tags AppRiskTagsAdmin
|
||||
// @Summary 给玩家打标
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param id path int true "玩家ID"
|
||||
// @Param body body setUserTagRequest true "参数"
|
||||
// @Success 200 {object} response.Response{msg=string} "操作成功"
|
||||
// @Router /admin/app-users/{id}/risk-tags [post]
|
||||
func (a *AppRiskTagsApi) AddAppUserRiskTag(c *gin.Context) {
|
||||
userID, _ := strconv.Atoi(c.Param("id"))
|
||||
if userID <= 0 {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
var req setUserTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.TagID == 0 {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
operatorID := utils.GetUserID(c)
|
||||
if operatorID == 0 {
|
||||
response.FailWithMessage("未登录", c)
|
||||
return
|
||||
}
|
||||
|
||||
// ensure tag exists
|
||||
var tag appModel.AppRiskTag
|
||||
if err := global.GVA_DB.First(&tag, req.TagID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.FailWithMessage("标签不存在", c)
|
||||
return
|
||||
}
|
||||
response.FailWithMessage("操作失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
rec := appModel.AppUserRiskTag{
|
||||
AppUserID: uint(userID),
|
||||
TagID: req.TagID,
|
||||
OperatorUserID: operatorID,
|
||||
Remark: strings.TrimSpace(req.Remark),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := global.GVA_DB.Create(&rec).Error; err != nil {
|
||||
response.FailWithMessage("操作失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("已打标", c)
|
||||
}
|
||||
|
||||
// RemoveAppUserRiskTag
|
||||
// @Tags AppRiskTagsAdmin
|
||||
// @Summary 移除玩家标签
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce application/json
|
||||
// @Param id path int true "关系ID"
|
||||
// @Success 200 {object} response.Response{msg=string} "操作成功"
|
||||
// @Router /admin/app-user-risk-tags/{id} [delete]
|
||||
func (a *AppRiskTagsApi) RemoveAppUserRiskTag(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id <= 0 {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
if err := global.GVA_DB.Delete(&appModel.AppUserRiskTag{}, uint(id)).Error; err != nil {
|
||||
response.FailWithMessage("操作失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("已移除", c)
|
||||
}
|
||||
|
||||
704
server/api/v1/admin/app_users.go
Normal file
704
server/api/v1/admin/app_users.go
Normal file
@@ -0,0 +1,704 @@
|
||||
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 ""
|
||||
}
|
||||
528
server/api/v1/admin/app_withdraw_orders.go
Normal file
528
server/api/v1/admin/app_withdraw_orders.go
Normal file
@@ -0,0 +1,528 @@
|
||||
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"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type AppWithdrawOrdersApi struct{}
|
||||
|
||||
type appWithdrawOrderListItem struct {
|
||||
ID uint `json:"id"`
|
||||
AppUserID uint `json:"appUserId"`
|
||||
Username string `json:"username"`
|
||||
AmountLi int64 `json:"amountLi"`
|
||||
FeeLi int64 `json:"feeLi"`
|
||||
NetLi int64 `json:"netLi"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
|
||||
OperatorUserID uint `json:"operatorUserId"`
|
||||
ApprovedBy uint `json:"approvedBy"`
|
||||
ApprovedAt *time.Time `json:"approvedAt"`
|
||||
PaidBy uint `json:"paidBy"`
|
||||
PaidAt *time.Time `json:"paidAt"`
|
||||
}
|
||||
|
||||
// GetAppWithdrawOrdersList
|
||||
// @Tags AppWithdrawOrdersAdmin
|
||||
// @Summary 后台获取提现订单列表(单位:厘)
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce application/json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param pageSize query int false "每页大小" default(10)
|
||||
// @Param userId query int false "玩家ID"
|
||||
// @Param username query string false "用户名(模糊)"
|
||||
// @Param status query string false "状态:pending/approved/rejected/paid/failed"
|
||||
// @Param startAt query string false "开始时间(ISO8601/RFC3339)"
|
||||
// @Param endAt query string false "结束时间(ISO8601/RFC3339)"
|
||||
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
|
||||
// @Router /admin/app-withdraw-orders [get]
|
||||
func (a *AppWithdrawOrdersApi) GetAppWithdrawOrdersList(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
|
||||
}
|
||||
|
||||
var userID uint
|
||||
if raw := strings.TrimSpace(c.Query("userId")); raw != "" {
|
||||
if v, err := strconv.Atoi(raw); err == nil && v > 0 {
|
||||
userID = uint(v)
|
||||
}
|
||||
}
|
||||
username := strings.TrimSpace(c.Query("username"))
|
||||
status := strings.TrimSpace(c.Query("status"))
|
||||
|
||||
var startAt *time.Time
|
||||
if raw := strings.TrimSpace(c.Query("startAt")); raw != "" {
|
||||
if t, err := time.Parse(time.RFC3339, raw); err == nil {
|
||||
startAt = &t
|
||||
}
|
||||
}
|
||||
var endAt *time.Time
|
||||
if raw := strings.TrimSpace(c.Query("endAt")); raw != "" {
|
||||
if t, err := time.Parse(time.RFC3339, raw); err == nil {
|
||||
endAt = &t
|
||||
}
|
||||
}
|
||||
|
||||
db := global.GVA_DB.Model(&appModel.AppWithdrawOrder{})
|
||||
if userID > 0 {
|
||||
db = db.Where("app_user_id = ?", userID)
|
||||
}
|
||||
if username != "" {
|
||||
db = db.Where("username LIKE ?", "%"+username+"%")
|
||||
}
|
||||
if status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
}
|
||||
if startAt != nil {
|
||||
db = db.Where("created_at >= ?", *startAt)
|
||||
}
|
||||
if endAt != nil {
|
||||
db = db.Where("created_at <= ?", *endAt)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
response.FailWithMessage("获取失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
var list []appWithdrawOrderListItem
|
||||
if err := db.
|
||||
Order("id desc").
|
||||
Limit(pageSize).
|
||||
Offset((page - 1) * pageSize).
|
||||
Select("id, app_user_id, username, amount_li, fee_li, net_li, status, created_at, operator_user_id, approved_by, approved_at, paid_by, paid_at").
|
||||
Scan(&list).Error; err != nil {
|
||||
response.FailWithMessage("获取失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithData(response.PageResult{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// GetAppWithdrawOrderById
|
||||
// @Tags AppWithdrawOrdersAdmin
|
||||
// @Summary 后台获取提现订单详情(单位:厘)
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce application/json
|
||||
// @Param id path int true "订单ID"
|
||||
// @Success 200 {object} response.Response{data=appModel.AppWithdrawOrder,msg=string} "获取成功"
|
||||
// @Router /admin/app-withdraw-orders/{id} [get]
|
||||
func (a *AppWithdrawOrdersApi) GetAppWithdrawOrderById(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id <= 0 {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
var rec appModel.AppWithdrawOrder
|
||||
if err := global.GVA_DB.First(&rec, uint(id)).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.FailWithMessage("订单不存在", c)
|
||||
return
|
||||
}
|
||||
response.FailWithMessage("获取失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(rec, c)
|
||||
}
|
||||
|
||||
type createWithdrawOrderRequest struct {
|
||||
AppUserID uint `json:"appUserId" binding:"required"`
|
||||
AmountLi int64 `json:"amountLi" binding:"required"`
|
||||
FeeLi int64 `json:"feeLi"`
|
||||
Channel string `json:"channel"`
|
||||
PayeeAccount string `json:"payeeAccount"`
|
||||
PayeeRealName string `json:"payeeRealName"`
|
||||
ApplyRemark string `json:"applyRemark"`
|
||||
}
|
||||
|
||||
// CreateAppWithdrawOrder
|
||||
// @Tags AppWithdrawOrdersAdmin
|
||||
// @Summary 后台创建提现订单(会冻结账户余额)
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param body body createWithdrawOrderRequest true "参数"
|
||||
// @Success 200 {object} response.Response{msg=string} "创建成功"
|
||||
// @Router /admin/app-withdraw-orders [post]
|
||||
func (a *AppWithdrawOrdersApi) CreateAppWithdrawOrder(c *gin.Context) {
|
||||
var req createWithdrawOrderRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
if req.AmountLi <= 0 {
|
||||
response.FailWithMessage("amountLi 必须 > 0", c)
|
||||
return
|
||||
}
|
||||
if req.FeeLi < 0 {
|
||||
response.FailWithMessage("feeLi 不合法", c)
|
||||
return
|
||||
}
|
||||
net := req.AmountLi - req.FeeLi
|
||||
if net <= 0 {
|
||||
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, req.AppUserID).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
|
||||
}
|
||||
|
||||
available := asset.AccountBalanceLi - asset.AccountFrozenLi
|
||||
if available < req.AmountLi {
|
||||
return errors.New("可提现余额不足")
|
||||
}
|
||||
asset.AccountFrozenLi += req.AmountLi
|
||||
if err := tx.Model(&asset).Select("account_frozen_li").Updates(&asset).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
order := appModel.AppWithdrawOrder{
|
||||
AppUserID: user.ID,
|
||||
Username: user.Username,
|
||||
AmountLi: req.AmountLi,
|
||||
FeeLi: req.FeeLi,
|
||||
NetLi: net,
|
||||
Status: appModel.AppWithdrawOrderStatusPending,
|
||||
Channel: strings.TrimSpace(req.Channel),
|
||||
PayeeAccount: strings.TrimSpace(req.PayeeAccount),
|
||||
PayeeRealName: strings.TrimSpace(req.PayeeRealName),
|
||||
ApplyRemark: strings.TrimSpace(req.ApplyRemark),
|
||||
OperatorUserID: operatorID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
return tx.Create(&order).Error
|
||||
}); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMessage("创建成功", c)
|
||||
}
|
||||
|
||||
type auditWithdrawOrderRequest struct {
|
||||
AuditRemark string `json:"auditRemark"`
|
||||
RejectReason string `json:"rejectReason"`
|
||||
}
|
||||
|
||||
// ApproveAppWithdrawOrder
|
||||
// @Tags AppWithdrawOrdersAdmin
|
||||
// @Summary 审核通过提现订单
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param id path int true "订单ID"
|
||||
// @Param body body auditWithdrawOrderRequest false "参数"
|
||||
// @Success 200 {object} response.Response{msg=string} "操作成功"
|
||||
// @Router /admin/app-withdraw-orders/{id}/approve [post]
|
||||
func (a *AppWithdrawOrdersApi) ApproveAppWithdrawOrder(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id <= 0 {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
operatorID := utils.GetUserID(c)
|
||||
if operatorID == 0 {
|
||||
response.FailWithMessage("未登录", c)
|
||||
return
|
||||
}
|
||||
var req auditWithdrawOrderRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
|
||||
now := time.Now()
|
||||
if err := global.GVA_DB.Model(&appModel.AppWithdrawOrder{}).
|
||||
Where("id = ? AND status = ?", uint(id), appModel.AppWithdrawOrderStatusPending).
|
||||
Updates(map[string]any{
|
||||
"status": appModel.AppWithdrawOrderStatusApproved,
|
||||
"audit_remark": strings.TrimSpace(req.AuditRemark),
|
||||
"approved_by": operatorID,
|
||||
"approved_at": &now,
|
||||
"operator_user_id": operatorID,
|
||||
}).Error; err != nil {
|
||||
response.FailWithMessage("操作失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("已审核通过", c)
|
||||
}
|
||||
|
||||
// RejectAppWithdrawOrder
|
||||
// @Tags AppWithdrawOrdersAdmin
|
||||
// @Summary 审核拒绝提现订单(会解冻)
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param id path int true "订单ID"
|
||||
// @Param body body auditWithdrawOrderRequest true "参数"
|
||||
// @Success 200 {object} response.Response{msg=string} "操作成功"
|
||||
// @Router /admin/app-withdraw-orders/{id}/reject [post]
|
||||
func (a *AppWithdrawOrdersApi) RejectAppWithdrawOrder(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id <= 0 {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
operatorID := utils.GetUserID(c)
|
||||
if operatorID == 0 {
|
||||
response.FailWithMessage("未登录", c)
|
||||
return
|
||||
}
|
||||
var req auditWithdrawOrderRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
reason := strings.TrimSpace(req.RejectReason)
|
||||
if reason == "" {
|
||||
response.FailWithMessage("rejectReason 不能为空", c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||
var order appModel.AppWithdrawOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, uint(id)).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("订单不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if order.Status != appModel.AppWithdrawOrderStatusPending {
|
||||
return errors.New("当前状态不可拒绝")
|
||||
}
|
||||
|
||||
var asset appModel.AppUserAsset
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("app_user_id = ?", order.AppUserID).First(&asset).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if asset.AccountFrozenLi < order.AmountLi {
|
||||
return errors.New("冻结余额异常")
|
||||
}
|
||||
asset.AccountFrozenLi -= order.AmountLi
|
||||
if err := tx.Model(&asset).Select("account_frozen_li").Updates(&asset).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return tx.Model(&order).Updates(map[string]any{
|
||||
"status": appModel.AppWithdrawOrderStatusRejected,
|
||||
"reject_reason": reason,
|
||||
"audit_remark": strings.TrimSpace(req.AuditRemark),
|
||||
"approved_by": operatorID,
|
||||
"approved_at": &now,
|
||||
"operator_user_id": operatorID,
|
||||
}).Error
|
||||
}); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("已拒绝并解冻", c)
|
||||
}
|
||||
|
||||
type payWithdrawOrderRequest struct {
|
||||
ExternalTxID string `json:"externalTxId"`
|
||||
FailureReason string `json:"failureReason"`
|
||||
}
|
||||
|
||||
// MarkPaidAppWithdrawOrder
|
||||
// @Tags AppWithdrawOrdersAdmin
|
||||
// @Summary 确认已打款(会扣减余额并解冻)
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param id path int true "订单ID"
|
||||
// @Param body body payWithdrawOrderRequest false "参数"
|
||||
// @Success 200 {object} response.Response{msg=string} "操作成功"
|
||||
// @Router /admin/app-withdraw-orders/{id}/mark-paid [post]
|
||||
func (a *AppWithdrawOrdersApi) MarkPaidAppWithdrawOrder(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id <= 0 {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
operatorID := utils.GetUserID(c)
|
||||
if operatorID == 0 {
|
||||
response.FailWithMessage("未登录", c)
|
||||
return
|
||||
}
|
||||
var req payWithdrawOrderRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
|
||||
if err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||
var order appModel.AppWithdrawOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, uint(id)).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("订单不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if order.Status != appModel.AppWithdrawOrderStatusApproved {
|
||||
return errors.New("当前状态不可打款确认")
|
||||
}
|
||||
|
||||
var asset appModel.AppUserAsset
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("app_user_id = ?", order.AppUserID).First(&asset).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if asset.AccountFrozenLi < order.AmountLi {
|
||||
return errors.New("冻结余额异常")
|
||||
}
|
||||
before := asset.AccountBalanceLi
|
||||
if before < order.AmountLi {
|
||||
return errors.New("账户余额不足")
|
||||
}
|
||||
asset.AccountFrozenLi -= order.AmountLi
|
||||
asset.AccountBalanceLi -= order.AmountLi
|
||||
if err := tx.Model(&asset).Select("account_frozen_li", "account_balance_li").Updates(&asset).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
after := asset.AccountBalanceLi
|
||||
txRec := appModel.AppAssetTransaction{
|
||||
AppUserID: order.AppUserID,
|
||||
AssetType: appModel.AppAssetTypeAccount,
|
||||
RechargeType: appModel.AppRechargeType("withdraw_paid"),
|
||||
DeltaLi: -order.AmountLi,
|
||||
BeforeLi: before,
|
||||
AfterLi: after,
|
||||
Reason: "提现打款(订单#" + strconv.Itoa(int(order.ID)) + ")",
|
||||
OperatorUserID: operatorID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := tx.Create(&txRec).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return tx.Model(&order).Updates(map[string]any{
|
||||
"status": appModel.AppWithdrawOrderStatusPaid,
|
||||
"paid_by": operatorID,
|
||||
"paid_at": &now,
|
||||
"external_tx_id": strings.TrimSpace(req.ExternalTxID),
|
||||
"operator_user_id": operatorID,
|
||||
}).Error
|
||||
}); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMessage("已确认打款", c)
|
||||
}
|
||||
|
||||
// MarkFailedAppWithdrawOrder
|
||||
// @Tags AppWithdrawOrdersAdmin
|
||||
// @Summary 标记打款失败(会解冻)
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param id path int true "订单ID"
|
||||
// @Param body body payWithdrawOrderRequest true "参数"
|
||||
// @Success 200 {object} response.Response{msg=string} "操作成功"
|
||||
// @Router /admin/app-withdraw-orders/{id}/mark-failed [post]
|
||||
func (a *AppWithdrawOrdersApi) MarkFailedAppWithdrawOrder(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id <= 0 {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
operatorID := utils.GetUserID(c)
|
||||
if operatorID == 0 {
|
||||
response.FailWithMessage("未登录", c)
|
||||
return
|
||||
}
|
||||
var req payWithdrawOrderRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
reason := strings.TrimSpace(req.FailureReason)
|
||||
if reason == "" {
|
||||
response.FailWithMessage("failureReason 不能为空", c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||
var order appModel.AppWithdrawOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, uint(id)).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("订单不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if order.Status != appModel.AppWithdrawOrderStatusApproved {
|
||||
return errors.New("当前状态不可标记失败")
|
||||
}
|
||||
|
||||
var asset appModel.AppUserAsset
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("app_user_id = ?", order.AppUserID).First(&asset).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if asset.AccountFrozenLi < order.AmountLi {
|
||||
return errors.New("冻结余额异常")
|
||||
}
|
||||
asset.AccountFrozenLi -= order.AmountLi
|
||||
if err := tx.Model(&asset).Select("account_frozen_li").Updates(&asset).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Model(&order).Updates(map[string]any{
|
||||
"status": appModel.AppWithdrawOrderStatusFailed,
|
||||
"failure_reason": reason,
|
||||
"external_tx_id": strings.TrimSpace(req.ExternalTxID),
|
||||
"operator_user_id": operatorID,
|
||||
}).Error
|
||||
}); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMessage("已标记失败并解冻", c)
|
||||
}
|
||||
|
||||
22
server/api/v1/admin/dashboard.go
Normal file
22
server/api/v1/admin/dashboard.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"git.echol.cn/loser/Go-Web-Template/server/model/admin"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/model/common/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type DashboardApi struct{}
|
||||
|
||||
// GetDashboardStats 运营仪表盘聚合统计(占位实现,避免前端 404;后续可接真实聚合查询)。
|
||||
func (a *DashboardApi) GetDashboardStats(c *gin.Context) {
|
||||
stats := admin.DashboardStats{
|
||||
Feedback: admin.DashboardFeedbackStats{
|
||||
PendingItems: []admin.DashboardFeedbackPendingItem{},
|
||||
},
|
||||
Creators: admin.DashboardCreatorStats{
|
||||
TopCreators: []admin.DashboardTopCreator{},
|
||||
},
|
||||
}
|
||||
response.OkWithData(stats, c)
|
||||
}
|
||||
11
server/api/v1/admin/enter.go
Normal file
11
server/api/v1/admin/enter.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package admin
|
||||
|
||||
type ApiGroup struct {
|
||||
DashboardApi
|
||||
AppUsersApi
|
||||
AppInviteCodesApi
|
||||
AppLoginLogsApi
|
||||
AppAssetTransactionsApi
|
||||
AppWithdrawOrdersApi
|
||||
AppRiskTagsApi
|
||||
}
|
||||
37
server/api/v1/app/app_auth.go
Normal file
37
server/api/v1/app/app_auth.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"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"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AppAuthApi struct{}
|
||||
|
||||
func (a *AppAuthApi) Me(c *gin.Context) {
|
||||
uid := utils.GetUserID(c)
|
||||
if uid == 0 {
|
||||
response.NoAuth("未登录或非法访问,请登录", c)
|
||||
return
|
||||
}
|
||||
var user appModel.AppUser
|
||||
if err := global.GVA_DB.First(&user, uid).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.NoAuth("用户不存在", c)
|
||||
return
|
||||
}
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(gin.H{
|
||||
"id": user.ID,
|
||||
"uuid": user.UUID,
|
||||
"username": user.Username,
|
||||
"enable": user.Enable,
|
||||
}, c)
|
||||
}
|
||||
7
server/api/v1/app/enter.go
Normal file
7
server/api/v1/app/enter.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package app
|
||||
|
||||
type ApiGroup struct {
|
||||
PublicAuthApi
|
||||
AppAuthApi
|
||||
InviteCodeApi
|
||||
}
|
||||
50
server/api/v1/app/invite_code.go
Normal file
50
server/api/v1/app/invite_code.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.echol.cn/loser/Go-Web-Template/server/model/common/response"
|
||||
appSvc "git.echol.cn/loser/Go-Web-Template/server/service/app"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type InviteCodeApi struct{}
|
||||
|
||||
type inviteCodeCurrentResponse struct {
|
||||
HasUnused bool `json:"hasUnused"`
|
||||
Record interface{} `json:"record,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateInviteCode 玩家生成邀请码(返回明文)
|
||||
func (a *InviteCodeApi) GenerateInviteCode(c *gin.Context) {
|
||||
uid := utils.GetUserID(c)
|
||||
res, err := appSvc.AppInviteCodeServiceApp.Generate(uid)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(res, c)
|
||||
}
|
||||
|
||||
// CurrentInviteCode 获取当前未使用邀请码(不返回明文)
|
||||
func (a *InviteCodeApi) CurrentInviteCode(c *gin.Context) {
|
||||
uid := utils.GetUserID(c)
|
||||
rec, err := appSvc.AppInviteCodeServiceApp.GetCurrentUnused(uid)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if rec == nil {
|
||||
response.OkWithData(inviteCodeCurrentResponse{HasUnused: false}, c)
|
||||
return
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"id": rec.ID,
|
||||
"codeLast4": rec.CodeLast4,
|
||||
"status": rec.Status,
|
||||
"expiresAt": rec.ExpiresAt,
|
||||
"createdAt": rec.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
response.OkWithData(inviteCodeCurrentResponse{HasUnused: true, Record: payload}, c)
|
||||
}
|
||||
122
server/api/v1/app/public_auth.go
Normal file
122
server/api/v1/app/public_auth.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.echol.cn/loser/Go-Web-Template/server/global"
|
||||
appModel "git.echol.cn/loser/Go-Web-Template/server/model/app"
|
||||
appReq "git.echol.cn/loser/Go-Web-Template/server/model/app/request"
|
||||
appRes "git.echol.cn/loser/Go-Web-Template/server/model/app/response"
|
||||
commonResp "git.echol.cn/loser/Go-Web-Template/server/model/common/response"
|
||||
systemReq "git.echol.cn/loser/Go-Web-Template/server/model/system/request"
|
||||
appSvc "git.echol.cn/loser/Go-Web-Template/server/service/app"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
)
|
||||
|
||||
type PublicAuthApi struct{}
|
||||
|
||||
func (a *PublicAuthApi) RegisterStatus(c *gin.Context) {
|
||||
mode := appRes.RegisterMode(appSvc.GetRegisterMode())
|
||||
requireCaptcha := appSvc.GetRegisterRequireCaptcha()
|
||||
expireHours := appSvc.GetInviteExpireHours()
|
||||
payload := appRes.RegisterStatusResponse{
|
||||
Mode: mode,
|
||||
RequireCaptcha: requireCaptcha,
|
||||
InviteExpireHours: expireHours,
|
||||
}
|
||||
if mode == appRes.RegisterModeClosed {
|
||||
payload.Message = "暂不开放注册"
|
||||
}
|
||||
commonResp.OkWithData(payload, c)
|
||||
}
|
||||
|
||||
func (a *PublicAuthApi) Register(c *gin.Context) {
|
||||
var req appReq.PublicRegister
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
commonResp.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
mode := appSvc.GetRegisterMode()
|
||||
if mode == appSvc.RegisterModeClosed {
|
||||
commonResp.FailWithMessage("注册未开放", c)
|
||||
return
|
||||
}
|
||||
|
||||
// 可选验证码(复用 base64Captcha 默认内存 store)
|
||||
if appSvc.GetRegisterRequireCaptcha() {
|
||||
if req.Captcha == "" || req.CaptchaId == "" || !base64Captcha.DefaultMemStore.Verify(req.CaptchaId, req.Captcha, true) {
|
||||
commonResp.FailWithMessage("验证码错误", c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
req.Username = strings.TrimSpace(req.Username)
|
||||
req.WelcomePhrase = strings.TrimSpace(req.WelcomePhrase)
|
||||
|
||||
if err := appSvc.AppRegisterServiceApp.Register(req, mode); err != nil {
|
||||
commonResp.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
commonResp.OkWithMessage("注册成功", c)
|
||||
}
|
||||
|
||||
func (a *PublicAuthApi) Login(c *gin.Context) {
|
||||
var req appReq.Login
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
commonResp.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
req.Username = strings.TrimSpace(req.Username)
|
||||
|
||||
user, err := appSvc.AppAuthServiceApp.Login(req.Username, req.Password)
|
||||
if err != nil {
|
||||
// 记录失败登录日志(不阻断业务返回)
|
||||
_ = global.GVA_DB.Create(&appModel.AppLoginLog{
|
||||
AppUserID: 0,
|
||||
Username: req.Username,
|
||||
IP: c.ClientIP(),
|
||||
Referer: c.GetHeader("Referer"),
|
||||
UserAgent: c.GetHeader("User-Agent"),
|
||||
Status: false,
|
||||
ErrorMessage: err.Error(),
|
||||
}).Error
|
||||
commonResp.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 记录最后登录与活跃
|
||||
_ = appSvc.AppActivityServiceApp.RecordLogin(user.ID, c.ClientIP())
|
||||
// 记录成功登录日志(不阻断业务返回)
|
||||
_ = global.GVA_DB.Create(&appModel.AppLoginLog{
|
||||
AppUserID: user.ID,
|
||||
Username: user.Username,
|
||||
IP: c.ClientIP(),
|
||||
Referer: c.GetHeader("Referer"),
|
||||
UserAgent: c.GetHeader("User-Agent"),
|
||||
Status: true,
|
||||
}).Error
|
||||
|
||||
j := utils.NewJWT()
|
||||
claims := j.CreateAppClaims(utilsBaseClaimsFromAppUser(user))
|
||||
token, err := j.CreateToken(claims)
|
||||
if err != nil {
|
||||
commonResp.FailWithMessage("获取token失败", c)
|
||||
return
|
||||
}
|
||||
commonResp.OkWithData(gin.H{
|
||||
"token": token,
|
||||
}, c)
|
||||
}
|
||||
|
||||
func utilsBaseClaimsFromAppUser(user *appModel.AppUser) systemReq.BaseClaims {
|
||||
return systemReq.BaseClaims{
|
||||
UUID: user.UUID,
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
NickName: user.Username,
|
||||
AuthorityId: 0,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"git.echol.cn/loser/Go-Web-Template/server/api/v1/admin"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/api/v1/app"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/api/v1/common"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/api/v1/system"
|
||||
)
|
||||
@@ -10,4 +12,6 @@ var ApiGroupApp = new(ApiGroup)
|
||||
type ApiGroup struct {
|
||||
SystemApiGroup system.ApiGroup
|
||||
CommonApiGroup common.ApiGroup
|
||||
AdminApiGroup admin.ApiGroup
|
||||
AppApiGroup app.ApiGroup
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ type ApiGroup struct {
|
||||
JwtApi
|
||||
BaseApi
|
||||
SystemApi
|
||||
PublicAuthApi
|
||||
InviteCodeApi
|
||||
CasbinApi
|
||||
SystemApiApi
|
||||
AuthorityApi
|
||||
@@ -41,4 +43,6 @@ var (
|
||||
sysErrorService = service.ServiceGroupApp.SystemServiceGroup.SysErrorService
|
||||
loginLogService = service.ServiceGroupApp.SystemServiceGroup.LoginLogService
|
||||
apiTokenService = service.ServiceGroupApp.SystemServiceGroup.ApiTokenService
|
||||
inviteCodeService = service.ServiceGroupApp.SystemServiceGroup.InviteCodeService
|
||||
publicRegisterService = service.ServiceGroupApp.SystemServiceGroup.PublicRegisterService
|
||||
)
|
||||
|
||||
63
server/api/v1/system/invite_code.go
Normal file
63
server/api/v1/system/invite_code.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.echol.cn/loser/Go-Web-Template/server/model/common/response"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type InviteCodeApi struct{}
|
||||
|
||||
type inviteCodeCurrentResponse struct {
|
||||
HasUnused bool `json:"hasUnused"`
|
||||
Record interface{} `json:"record,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateInviteCode
|
||||
// @Tags InviteCode
|
||||
// @Summary 生成邀请码(同一时间仅允许一个未使用码)
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce application/json
|
||||
// @Success 200 {object} response.Response{data=system.InviteCodeGenerated,msg=string} "生成成功"
|
||||
// @Router /user/invite-code/generate [post]
|
||||
func (a *InviteCodeApi) GenerateInviteCode(c *gin.Context) {
|
||||
uid := utils.GetUserID(c)
|
||||
expireHours := inviteCodeService.InviteExpireHours()
|
||||
res, err := inviteCodeService.Generate(uid, expireHours)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(res, c)
|
||||
}
|
||||
|
||||
// CurrentInviteCode
|
||||
// @Tags InviteCode
|
||||
// @Summary 获取当前未使用邀请码(不返回明文)
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce application/json
|
||||
// @Success 200 {object} response.Response{data=inviteCodeCurrentResponse,msg=string} "获取成功"
|
||||
// @Router /user/invite-code/current [get]
|
||||
func (a *InviteCodeApi) CurrentInviteCode(c *gin.Context) {
|
||||
uid := utils.GetUserID(c)
|
||||
rec, err := inviteCodeService.GetCurrentUnused(uid)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if rec == nil {
|
||||
response.OkWithData(inviteCodeCurrentResponse{HasUnused: false}, c)
|
||||
return
|
||||
}
|
||||
// 仅返回必要字段(不返回 hash)
|
||||
payload := map[string]interface{}{
|
||||
"id": rec.ID,
|
||||
"codeLast4": rec.CodeLast4,
|
||||
"status": rec.Status,
|
||||
"expiresAt": rec.ExpiresAt,
|
||||
"createdAt": rec.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
response.OkWithData(inviteCodeCurrentResponse{HasUnused: true, Record: payload}, c)
|
||||
}
|
||||
28
server/api/v1/system/public_auth.go
Normal file
28
server/api/v1/system/public_auth.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"git.echol.cn/loser/Go-Web-Template/server/model/common/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type PublicAuthApi struct{}
|
||||
|
||||
// RegisterStatus
|
||||
// @Tags PublicAuth
|
||||
// @Summary (废弃)获取注册开放状态
|
||||
// @Produce application/json
|
||||
// @Success 200 {object} response.Response{msg=string} "已废弃"
|
||||
// @Router /public/auth/register/status [get]
|
||||
func (a *PublicAuthApi) RegisterStatus(c *gin.Context) {
|
||||
response.FailWithMessage("该接口已废弃,请使用 GET /public/app/register/status", c)
|
||||
}
|
||||
|
||||
// PublicRegister
|
||||
// @Tags PublicAuth
|
||||
// @Summary (废弃)用户前端注册
|
||||
// @Produce application/json
|
||||
// @Success 200 {object} response.Response{msg=string} "已废弃"
|
||||
// @Router /public/auth/register [post]
|
||||
func (a *PublicAuthApi) PublicRegister(c *gin.Context) {
|
||||
response.FailWithMessage("该接口已废弃,请使用 POST /public/app/register", c)
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.echol.cn/loser/Go-Web-Template/server/global"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/model/common/response"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/model/system"
|
||||
systemReq "git.echol.cn/loser/Go-Web-Template/server/model/system/request"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -33,6 +37,18 @@ func (sysParamsApi *SysParamsApi) CreateSysParams(c *gin.Context) {
|
||||
response.FailWithMessage("创建失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
// write audit (best-effort)
|
||||
operatorID := utils.GetUserID(c)
|
||||
if operatorID > 0 {
|
||||
_ = global.GVA_DB.Create(&system.SysParamChangeLog{
|
||||
ParamID: sysParams.ID,
|
||||
Key: sysParams.Key,
|
||||
Action: system.SysParamChangeActionCreate,
|
||||
OldValue: "",
|
||||
NewValue: sysParams.Value,
|
||||
OperatorUserID: operatorID,
|
||||
}).Error
|
||||
}
|
||||
response.OkWithMessage("创建成功", c)
|
||||
}
|
||||
|
||||
@@ -47,12 +63,26 @@ func (sysParamsApi *SysParamsApi) CreateSysParams(c *gin.Context) {
|
||||
// @Router /sysParams/deleteSysParams [delete]
|
||||
func (sysParamsApi *SysParamsApi) DeleteSysParams(c *gin.Context) {
|
||||
ID := c.Query("ID")
|
||||
// read before delete for audit
|
||||
var before system.SysParams
|
||||
_ = global.GVA_DB.Where("id = ?", ID).First(&before).Error
|
||||
err := sysParamsService.DeleteSysParams(ID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("删除失败!", zap.Error(err))
|
||||
response.FailWithMessage("删除失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
operatorID := utils.GetUserID(c)
|
||||
if operatorID > 0 && before.ID > 0 {
|
||||
_ = global.GVA_DB.Create(&system.SysParamChangeLog{
|
||||
ParamID: before.ID,
|
||||
Key: before.Key,
|
||||
Action: system.SysParamChangeActionDelete,
|
||||
OldValue: before.Value,
|
||||
NewValue: "",
|
||||
OperatorUserID: operatorID,
|
||||
}).Error
|
||||
}
|
||||
response.OkWithMessage("删除成功", c)
|
||||
}
|
||||
|
||||
@@ -91,15 +121,97 @@ func (sysParamsApi *SysParamsApi) UpdateSysParams(c *gin.Context) {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
// read before update for audit
|
||||
var before system.SysParams
|
||||
_ = global.GVA_DB.Where("id = ?", sysParams.ID).First(&before).Error
|
||||
err = sysParamsService.UpdateSysParams(sysParams)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("更新失败!", zap.Error(err))
|
||||
response.FailWithMessage("更新失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
operatorID := utils.GetUserID(c)
|
||||
if operatorID > 0 && before.ID > 0 {
|
||||
_ = global.GVA_DB.Create(&system.SysParamChangeLog{
|
||||
ParamID: before.ID,
|
||||
Key: before.Key,
|
||||
Action: system.SysParamChangeActionUpdate,
|
||||
OldValue: before.Value,
|
||||
NewValue: sysParams.Value,
|
||||
OperatorUserID: operatorID,
|
||||
}).Error
|
||||
}
|
||||
response.OkWithMessage("更新成功", c)
|
||||
}
|
||||
|
||||
// GetSysParamChangeLogList 分页获取参数变更审计
|
||||
// @Tags SysParams
|
||||
// @Summary 分页获取参数变更审计
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param pageSize query int false "每页大小" default(10)
|
||||
// @Param key query string false "key(模糊)"
|
||||
// @Param operatorUserId query int false "操作人ID"
|
||||
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
|
||||
// @Router /sysParams/getChangeLogList [get]
|
||||
func (sysParamsApi *SysParamsApi) GetSysParamChangeLogList(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
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(c.Query("key"))
|
||||
var operatorID uint
|
||||
if raw := strings.TrimSpace(c.Query("operatorUserId")); raw != "" {
|
||||
if v, err := strconv.Atoi(raw); err == nil && v > 0 {
|
||||
operatorID = uint(v)
|
||||
}
|
||||
}
|
||||
|
||||
db := global.GVA_DB.Model(&system.SysParamChangeLog{})
|
||||
if key != "" {
|
||||
db = db.Where("key LIKE ?", "%"+key+"%")
|
||||
}
|
||||
if operatorID > 0 {
|
||||
db = db.Where("operator_user_id = ?", operatorID)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
response.FailWithMessage("获取失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
type row struct {
|
||||
system.SysParamChangeLog
|
||||
OperatorName string `json:"operatorName" gorm:"column:operator_name"`
|
||||
}
|
||||
var list []row
|
||||
if err := db.
|
||||
Joins("LEFT JOIN sys_users su ON su.id = sys_param_change_logs.operator_user_id").
|
||||
Select("sys_param_change_logs.*, su.username AS operator_name").
|
||||
Order("sys_param_change_logs.id desc").
|
||||
Limit(pageSize).
|
||||
Offset((page - 1) * pageSize).
|
||||
Scan(&list).Error; err != nil {
|
||||
response.FailWithMessage("获取失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithDetailed(response.PageResult{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// FindSysParams 用id查询参数
|
||||
// @Tags SysParams
|
||||
// @Summary 用id查询参数
|
||||
|
||||
Reference in New Issue
Block a user