bindbox-game/internal/api/admin/users_admin.go
邹方成 1b5a715a22 feat: 添加环境变量支持并增强系统标题效果验证
feat(security): 支持通过环境变量配置主密钥和JWT密钥
refactor(router): 移除开发便捷路由接口
feat(admin): 添加超级管理员权限检查
feat(titles): 增加系统标题效果参数验证逻辑
2025-11-16 11:51:47 +08:00

621 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

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

package admin
import (
"net/http"
"strconv"
"time"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
"bindbox-game/internal/service/user"
)
type listUsersRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
Nickname string `form:"nickname"`
InviteCode string `form:"inviteCode"`
StartDate string `form:"startDate"`
EndDate string `form:"endDate"`
}
type listUsersResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []adminUserItem `json:"list"`
}
// ListAppUsers 管理端用户列表
// @Summary 管理端用户列表
// @Description 查看APP端用户分页列表
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Param nickname query string false "用户昵称"
// @Param inviteCode query string false "邀请码"
// @Param startDate query string false "开始日期(YYYY-MM-DD)"
// @Param endDate query string false "结束日期(YYYY-MM-DD)"
// @Success 200 {object} listUsersResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users [get]
// @Security LoginVerifyToken
func (h *handler) ListAppUsers() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listUsersRequest)
rsp := new(listUsersResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
if req.PageSize > 100 {
req.PageSize = 100
}
q := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB()
// 应用搜索条件
if req.Nickname != "" {
q = q.Where(h.readDB.Users.Nickname.Like("%" + req.Nickname + "%"))
}
if req.InviteCode != "" {
q = q.Where(h.readDB.Users.InviteCode.Eq(req.InviteCode))
}
if req.StartDate != "" {
if startTime, err := time.Parse("2006-01-02", req.StartDate); err == nil {
q = q.Where(h.readDB.Users.CreatedAt.Gte(startTime))
}
}
if req.EndDate != "" {
if endTime, err := time.Parse("2006-01-02", req.EndDate); err == nil {
// 设置结束时间为当天的23:59:59
endTime = endTime.Add(24 * time.Hour).Add(-time.Second)
q = q.Where(h.readDB.Users.CreatedAt.Lte(endTime))
}
}
total, err := q.Count()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20101, err.Error()))
return
}
rows, err := q.Order(h.readDB.Users.ID.Desc()).Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20102, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = make([]adminUserItem, len(rows))
for i, v := range rows {
rsp.List[i] = adminUserItem{ID: v.ID, Nickname: v.Nickname, Avatar: v.Avatar, InviteCode: v.InviteCode, InviterID: v.InviterID, CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05")}
}
ctx.Payload(rsp)
}
}
type listInvitesRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listInvitesResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []adminUserItem `json:"list"`
}
// ListUserInvites 查看用户邀请列表
// @Summary 查看用户邀请列表
// @Description 查看指定用户邀请的用户列表
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listInvitesResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/invites [get]
// @Security LoginVerifyToken
func (h *handler) ListUserInvites() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listInvitesRequest)
rsp := new(listInvitesResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
rows, total, err := h.user.ListInvites(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20103, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = make([]adminUserItem, len(rows))
for i, v := range rows {
rsp.List[i] = adminUserItem{ID: v.ID, Nickname: v.Nickname, Avatar: v.Avatar, InviteCode: v.InviteCode, InviterID: v.InviterID, CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05")}
}
ctx.Payload(rsp)
}
}
type listOrdersRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listOrdersResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []*user.OrderWithItems `json:"list"`
}
type listInventoryRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listInventoryResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []*user.InventoryWithProduct `json:"list"`
}
// ListUserOrders 查看用户订单列表
// @Summary 查看用户订单列表
// @Description 查看指定用户的订单记录
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listOrdersResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/orders [get]
// @Security LoginVerifyToken
func (h *handler) ListUserOrders() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listOrdersRequest)
rsp := new(listOrdersResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
items, total, err := h.user.ListOrdersWithItems(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20104, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = items
ctx.Payload(rsp)
}
}
// 查看用户资产列表
// @Summary 查看用户资产列表
// @Description 查看指定用户的资产记录
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listInventoryResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/inventory [get]
// @Security LoginVerifyToken
func (h *handler) ListUserInventory() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listInventoryRequest)
rsp := new(listInventoryResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
rows, total, err := h.user.ListInventoryWithProduct(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = rows
ctx.Payload(rsp)
}
}
type listUserItemCardsRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listUserItemCardsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []*user.ItemCardWithTemplate `json:"list"`
}
// ListUserItemCards 查看用户道具卡列表
// @Summary 查看用户道具卡列表
// @Description 查看指定用户的道具卡持有记录
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listUserItemCardsResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/item_cards [get]
// @Security LoginVerifyToken
func (h *handler) ListUserItemCards() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listUserItemCardsRequest)
rsp := new(listUserItemCardsResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
items, total, err := h.user.ListUserItemCardsWithTemplate(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20106, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = items
ctx.Payload(rsp)
}
}
type listCouponsRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type adminUserCouponItem struct {
ID int64 `json:"id"`
CouponID int64 `json:"coupon_id"`
Status int32 `json:"status"`
UsedOrderID int64 `json:"used_order_id"`
UsedAt string `json:"used_at"`
ValidStart string `json:"valid_start"`
ValidEnd string `json:"valid_end"`
Name string `json:"name"`
ScopeType int32 `json:"scope_type"`
DiscountType int32 `json:"discount_type"`
DiscountValue int64 `json:"discount_value"`
MinSpend int64 `json:"min_spend"`
}
type listCouponsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []adminUserCouponItem `json:"list"`
}
// ListUserCoupons 查看用户优惠券列表
// @Summary 查看用户优惠券列表
// @Description 查看指定用户持有的优惠券列表
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listCouponsResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/coupons [get]
// @Security LoginVerifyToken
func (h *handler) ListUserCoupons() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listCouponsRequest)
rsp := new(listCouponsResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
// 统计总数
base := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.UserCoupons.UserID.Eq(userID))
total, err := base.Count()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
return
}
// 联表查询 system_coupons 获取优惠券模板信息
type row struct {
ID int64
CouponID int64
Status int32
UsedOrderID int64
UsedAt *string
ValidStart *string
ValidEnd *string
Name string
ScopeType int32
DiscountType int32
DiscountValue int64
MinSpend int64
}
q := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).ReadDB().
LeftJoin(h.readDB.SystemCoupons, h.readDB.SystemCoupons.ID.EqCol(h.readDB.UserCoupons.CouponID)).
Select(
h.readDB.UserCoupons.ID, h.readDB.UserCoupons.CouponID, h.readDB.UserCoupons.Status,
h.readDB.UserCoupons.UsedOrderID, h.readDB.UserCoupons.UsedAt, h.readDB.UserCoupons.ValidStart, h.readDB.UserCoupons.ValidEnd,
h.readDB.SystemCoupons.Name, h.readDB.SystemCoupons.ScopeType, h.readDB.SystemCoupons.DiscountType,
h.readDB.SystemCoupons.DiscountValue, h.readDB.SystemCoupons.MinSpend,
).
Where(h.readDB.UserCoupons.UserID.Eq(userID)).
Order(h.readDB.UserCoupons.ID.Desc()).
Limit(req.PageSize).Offset((req.Page-1)*req.PageSize)
var rows []row
if err := q.Scan(&rows); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = make([]adminUserCouponItem, len(rows))
for i, v := range rows {
rsp.List[i] = adminUserCouponItem{
ID: v.ID,
CouponID: v.CouponID,
Status: v.Status,
UsedOrderID: v.UsedOrderID,
UsedAt: nullableToString(v.UsedAt),
ValidStart: nullableToString(v.ValidStart),
ValidEnd: nullableToString(v.ValidEnd),
Name: v.Name,
ScopeType: v.ScopeType,
DiscountType: v.DiscountType,
DiscountValue: v.DiscountValue,
MinSpend: v.MinSpend,
}
}
ctx.Payload(rsp)
}
}
func nullableToString(s *string) string {
if s == nil {
return ""
}
return *s
}
type listPointsRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listPointsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []*model.UserPointsLedger `json:"list"`
}
// ListUserPoints 查看用户积分记录
// @Summary 查看用户积分记录
// @Description 查看指定用户的积分流水记录
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listPointsResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/points [get]
// @Security LoginVerifyToken
func (h *handler) ListUserPoints() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listPointsRequest)
rsp := new(listPointsResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
items, total, err := h.user.ListPointsLedger(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20106, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = items
ctx.Payload(rsp)
}
}
type pointsBalanceResponse struct {
Balance int64 `json:"balance"`
}
type adminUserItem struct {
ID int64 `json:"id"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
InviteCode string `json:"invite_code"`
InviterID int64 `json:"inviter_id"`
CreatedAt string `json:"created_at"`
}
// GetUserPointsBalance 查看用户积分余额
// @Summary 查看用户积分余额
// @Description 查看指定用户当前积分余额(过滤过期)
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Success 200 {object} pointsBalanceResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/points/balance [get]
// @Security LoginVerifyToken
func (h *handler) GetUserPointsBalance() core.HandlerFunc {
return func(ctx core.Context) {
rsp := new(pointsBalanceResponse)
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
total, err := h.user.GetPointsBalance(ctx.RequestContext(), userID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20107, err.Error()))
return
}
rsp.Balance = total
ctx.Payload(rsp)
}
}
type addPointsRequest struct {
Points int64 `json:"points" binding:"required"`
Kind string `json:"kind"`
Remark string `json:"remark"`
ValidDays *int `json:"valid_days"`
}
type addPointsResponse struct {
Success bool `json:"success"`
}
// AddUserPoints 给用户添加积分
// @Summary 给用户添加积分
// @Description 管理端为指定用户发放积分,支持设置有效期
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param RequestBody body addPointsRequest true "请求参数"
// @Success 200 {object} addPointsResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/points/add [post]
// @Security LoginVerifyToken
func (h *handler) AddUserPoints() core.HandlerFunc {
return func(ctx core.Context) {
req := new(addPointsRequest)
rsp := new(addPointsResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
var validStart *time.Time
var validEnd *time.Time
now := time.Now()
validStart = &now
if req.ValidDays != nil && *req.ValidDays > 0 {
ve := now.Add(time.Duration(*req.ValidDays) * 24 * time.Hour)
validEnd = &ve
}
if err := h.user.AddPoints(ctx.RequestContext(), userID, req.Points, req.Kind, req.Remark, validStart, validEnd); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20108, err.Error()))
return
}
rsp.Success = true
ctx.Payload(rsp)
}
}
type addCouponRequest struct {
CouponID int64 `json:"coupon_id" binding:"required"`
}
type addCouponResponse struct {
Success bool `json:"success"`
}
// AddUserCoupon 给用户添加优惠券
// @Summary 给用户添加优惠券
// @Description 管理端为指定用户发放优惠券
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param RequestBody body addCouponRequest true "请求参数"
// @Success 200 {object} addCouponResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/coupons/add [post]
// @Security LoginVerifyToken
func (h *handler) AddUserCoupon() core.HandlerFunc {
return func(ctx core.Context) {
req := new(addCouponRequest)
rsp := new(addCouponResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
if err := h.user.AddCoupon(ctx.RequestContext(), userID, req.CouponID); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20109, err.Error()))
return
}
rsp.Success = true
ctx.Payload(rsp)
}
}