package admin import ( "math" "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"` ID string `form:"id"` } 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 } u := h.readDB.Users c := h.readDB.Channels q := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB(). LeftJoin(c, c.ID.EqCol(u.ChannelID)). Select( u.ALL, c.Name.As("channel_name"), c.Code.As("channel_code"), ) // 应用搜索条件 if req.ID != "" { if id, err := strconv.ParseInt(req.ID, 10, 64); err == nil { q = q.Where(h.readDB.Users.ID.Eq(id)) } } 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 } type result struct { model.Users ChannelName string ChannelCode string } var rows []result if err := q.Order(h.readDB.Users.ID.Desc()).Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Scan(&rows); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 20102, err.Error())) return } // 获取用户ID列表以批量查询资产 userIDs := make([]int64, len(rows)) for i, v := range rows { userIDs[i] = v.ID } // 批量查询优惠券数量(未使用的) couponCounts := make(map[int64]int64) if len(userIDs) > 0 { type countResult struct { UserID int64 Count int64 } var counts []countResult h.readDB.UserCoupons.WithContext(ctx.RequestContext()).ReadDB(). Select(h.readDB.UserCoupons.UserID, h.readDB.UserCoupons.ID.Count().As("count")). Where(h.readDB.UserCoupons.UserID.In(userIDs...)). Where(h.readDB.UserCoupons.Status.Eq(1)). // 1=未使用 Group(h.readDB.UserCoupons.UserID). Scan(&counts) for _, c := range counts { couponCounts[c.UserID] = c.Count } } // 批量查询积分余额 pointBalances := make(map[int64]int64) if len(userIDs) > 0 { type balResult struct { UserID int64 Points int64 } var bRes []balResult h.readDB.UserPoints.WithContext(ctx.RequestContext()).ReadDB(). Select(h.readDB.UserPoints.UserID, h.readDB.UserPoints.Points.Sum().As("points")). Where(h.readDB.UserPoints.UserID.In(userIDs...)). Group(h.readDB.UserPoints.UserID). Scan(&bRes) for _, b := range bRes { pointBalances[b.UserID] = int64(h.userSvc.CentsToPointsFloat(ctx.RequestContext(), b.Points)) } } // 批量查询优惠券数量(未使用的) cardCounts := make(map[int64]int64) if len(userIDs) > 0 { type countResult struct { UserID int64 Count int64 } var counts []countResult h.readDB.UserItemCards.WithContext(ctx.RequestContext()).ReadDB(). Select(h.readDB.UserItemCards.UserID, h.readDB.UserItemCards.ID.Count().As("count")). Where(h.readDB.UserItemCards.UserID.In(userIDs...)). Where(h.readDB.UserItemCards.Status.Eq(1)). // 1=未使用 Group(h.readDB.UserItemCards.UserID). Scan(&counts) for _, c := range counts { cardCounts[c.UserID] = c.Count } } // 批量查询消费统计 todayConsume := make(map[int64]int64) sevenDayConsume := make(map[int64]int64) thirtyDayConsume := make(map[int64]int64) totalConsume := make(map[int64]int64) if len(userIDs) > 0 { now := time.Now() todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) sevenDayStart := todayStart.AddDate(0, 0, -6) thirtyDayStart := todayStart.AddDate(0, 0, -29) type consumeResult struct { UserID int64 Amount int64 } // 当日消费 var todayRes []consumeResult h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB(). Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")). Where(h.readDB.Orders.UserID.In(userIDs...)). Where(h.readDB.Orders.Status.Eq(2)). Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5) Where(h.readDB.Orders.CreatedAt.Gte(todayStart)). Group(h.readDB.Orders.UserID). Scan(&todayRes) for _, r := range todayRes { todayConsume[r.UserID] = r.Amount } // 近7天消费 var sevenRes []consumeResult h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB(). Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")). Where(h.readDB.Orders.UserID.In(userIDs...)). Where(h.readDB.Orders.Status.Eq(2)). Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5) Where(h.readDB.Orders.CreatedAt.Gte(sevenDayStart)). Group(h.readDB.Orders.UserID). Scan(&sevenRes) for _, r := range sevenRes { sevenDayConsume[r.UserID] = r.Amount } // 近30天消费 var thirtyRes []consumeResult h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB(). Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")). Where(h.readDB.Orders.UserID.In(userIDs...)). Where(h.readDB.Orders.Status.Eq(2)). Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5) Where(h.readDB.Orders.CreatedAt.Gte(thirtyDayStart)). Group(h.readDB.Orders.UserID). Scan(&thirtyRes) for _, r := range thirtyRes { thirtyDayConsume[r.UserID] = r.Amount } // 累计消费 var totalRes []consumeResult h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB(). Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")). Where(h.readDB.Orders.UserID.In(userIDs...)). Where(h.readDB.Orders.Status.Eq(2)). Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5) Group(h.readDB.Orders.UserID). Scan(&totalRes) for _, r := range totalRes { totalConsume[r.UserID] = r.Amount } } // 批量查询邀请人数 inviteCounts := make(map[int64]int64) if len(userIDs) > 0 { type countResult struct { InviterID int64 Count int64 } var counts []countResult h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB(). Select(h.readDB.Users.InviterID, h.readDB.Users.ID.Count().As("count")). Where(h.readDB.Users.InviterID.In(userIDs...)). Group(h.readDB.Users.InviterID). Scan(&counts) for _, c := range counts { inviteCounts[c.InviterID] = c.Count } } // 批量查询次数卡数量 gamePassCounts := make(map[int64]int64) if len(userIDs) > 0 { type countResult struct { UserID int64 Count int64 } var counts []countResult now := time.Now() h.readDB.UserGamePasses.WithContext(ctx.RequestContext()).ReadDB(). Select(h.readDB.UserGamePasses.UserID, h.readDB.UserGamePasses.Remaining.Sum().As("count")). Where(h.readDB.UserGamePasses.UserID.In(userIDs...)). Where(h.readDB.UserGamePasses.Remaining.Gt(0)). Where(h.readDB.UserGamePasses.Where(h.readDB.UserGamePasses.ExpiredAt.Gt(now)).Or(h.readDB.UserGamePasses.ExpiredAt.IsNull())). Group(h.readDB.UserGamePasses.UserID). Scan(&counts) for _, c := range counts { gamePassCounts[c.UserID] = c.Count } } // 批量查询游戏资格数量 gameTicketCounts := make(map[int64]int64) if len(userIDs) > 0 { type countResult struct { UserID int64 Count int64 } var counts []countResult h.readDB.UserGameTickets.WithContext(ctx.RequestContext()).ReadDB(). Select(h.readDB.UserGameTickets.UserID, h.readDB.UserGameTickets.Available.Sum().As("count")). Where(h.readDB.UserGameTickets.UserID.In(userIDs...)). Group(h.readDB.UserGameTickets.UserID). Scan(&counts) for _, c := range counts { gameTicketCounts[c.UserID] = c.Count } } // 批量查询持有商品价值 inventoryValues := make(map[int64]int64) if len(userIDs) > 0 { type invResult struct { UserID int64 Value int64 } var invRes []invResult h.readDB.UserInventory.WithContext(ctx.RequestContext()).ReadDB(). LeftJoin(h.readDB.Products, h.readDB.Products.ID.EqCol(h.readDB.UserInventory.ProductID)). Select(h.readDB.UserInventory.UserID, h.readDB.Products.Price.Sum().As("value")). Where(h.readDB.UserInventory.UserID.In(userIDs...)). Where(h.readDB.UserInventory.Status.Eq(1)). // 1=持有 Group(h.readDB.UserInventory.UserID). Scan(&invRes) for _, r := range invRes { inventoryValues[r.UserID] = r.Value } } // 批量查询优惠券价值(余额之和) couponValues := make(map[int64]int64) if len(userIDs) > 0 { type valResult struct { UserID int64 Value int64 } var vRes []valResult h.readDB.UserCoupons.WithContext(ctx.RequestContext()).ReadDB(). Select(h.readDB.UserCoupons.UserID, h.readDB.UserCoupons.BalanceAmount.Sum().As("value")). Where(h.readDB.UserCoupons.UserID.In(userIDs...)). Where(h.readDB.UserCoupons.Status.Eq(1)). Group(h.readDB.UserCoupons.UserID). Scan(&vRes) for _, v := range vRes { couponValues[v.UserID] = v.Value } } // 批量查询道具卡价值 itemCardValues := make(map[int64]int64) if len(userIDs) > 0 { type valResult struct { UserID int64 Value int64 } var vRes []valResult h.readDB.UserItemCards.WithContext(ctx.RequestContext()).ReadDB(). LeftJoin(h.readDB.SystemItemCards, h.readDB.SystemItemCards.ID.EqCol(h.readDB.UserItemCards.CardID)). Select(h.readDB.UserItemCards.UserID, h.readDB.SystemItemCards.Price.Sum().As("value")). Where(h.readDB.UserItemCards.UserID.In(userIDs...)). Where(h.readDB.UserItemCards.Status.Eq(1)). Group(h.readDB.UserItemCards.UserID). Scan(&vRes) for _, v := range vRes { itemCardValues[v.UserID] = v.Value } } // 批量查询所有用户的邀请人昵称 inviterNicknames := make(map[int64]string) inviterIDs := make([]int64, 0) for _, v := range rows { if v.InviterID > 0 { inviterIDs = append(inviterIDs, v.InviterID) } } if len(inviterIDs) > 0 { inviters, _ := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.In(inviterIDs...)).Find() for _, inv := range inviters { inviterNicknames[inv.ID] = inv.Nickname } } rsp.Page = req.Page rsp.PageSize = req.PageSize rsp.Total = total rsp.List = make([]adminUserItem, len(rows)) for i, v := range rows { pointsBal := pointBalances[v.ID] invVal := inventoryValues[v.ID] cpVal := couponValues[v.ID] icVal := itemCardValues[v.ID] gpCount := gamePassCounts[v.ID] gtCount := gameTicketCounts[v.ID] // 总资产估值逻辑:积分余额 + 商品价值 + 优惠券价值 + 道具卡价值 + 次数卡(2元/次) // 游戏资格不计入估值(购买其他商品赠送,无实际价值) assetVal := pointsBal*100 + invVal + cpVal + icVal + gpCount*200 rsp.List[i] = adminUserItem{ ID: v.ID, Nickname: v.Nickname, Avatar: v.Avatar, InviteCode: v.InviteCode, InviterID: v.InviterID, InviterNickname: inviterNicknames[v.InviterID], CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"), DouyinID: v.DouyinID, DouyinUserID: v.DouyinUserID, Remark: v.Remark, ChannelName: v.ChannelName, ChannelCode: v.ChannelCode, PointsBalance: pointsBal, CouponsCount: couponCounts[v.ID], ItemCardsCount: cardCounts[v.ID], TodayConsume: todayConsume[v.ID], SevenDayConsume: sevenDayConsume[v.ID], ThirtyDayConsume: thirtyDayConsume[v.ID], TotalConsume: totalConsume[v.ID], InviteCount: inviteCounts[v.ID], GamePassCount: gpCount, GameTicketCount: gtCount, InventoryValue: invVal, TotalAssetValue: assetVal, Status: v.Status, } } 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.userSvc.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"` Keyword string `form:"keyword"` // 搜索关键词(商品名称) Status int32 `form:"status"` // 状态筛选:0=全部, 1=持有, 2=作废, 3=已使用 } 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.userSvc.ListOrdersWithItems(ctx.RequestContext(), userID, 0, nil, 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) // @Param keyword query string false "搜索关键词" // @Param status query int false "状态筛选: 0=全部, 1=持有, 2=作废, 3=已使用" // @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 } // 处理分页参数 if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 { req.PageSize = 20 } if req.PageSize > 100 { req.PageSize = 100 } // 如果有搜索关键词,使用带搜索的查询 if req.Keyword != "" { // 联表查询以支持按商品名称搜索 ui := h.readDB.UserInventory p := h.readDB.Products // Check if keyword is numeric numKeyword, errNum := strconv.ParseInt(req.Keyword, 10, 64) // Count query logic countQ := h.readDB.UserInventory.WithContext(ctx.RequestContext()).ReadDB(). LeftJoin(p, p.ID.EqCol(ui.ProductID)). Where(ui.UserID.Eq(userID)) // 应用状态筛选 if req.Status > 0 { countQ = countQ.Where(ui.Status.Eq(req.Status)) } else { // 默认只过滤掉已软删除的记录(如果有的话,status=2是作废,通常后台要能看到作废的,所以这里如果不传status默认查所有非删除的?) // 既然是管理端,如果不传status,应该显示所有状态的记录 } if errNum == nil { // Keyword is numeric, search by name OR ID OR OrderID countQ = countQ.Where( ui.Where(p.Name.Like("%" + req.Keyword + "%")). Or(ui.ID.Eq(numKeyword)). Or(ui.OrderID.Eq(numKeyword)), ) } else { // Keyword is not numeric, search by name only countQ = countQ.Where(p.Name.Like("%" + req.Keyword + "%")) } total, err := countQ.Count() if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error())) return } // 查询资产数据 type inventoryRow struct { ID int64 UserID int64 ProductID int64 OrderID int64 ActivityID int64 RewardID int64 Status int32 Remark string CreatedAt string UpdatedAt string ProductName string ProductImages string ProductPrice int64 } var rows []inventoryRow sql := ` SELECT ui.id, ui.user_id, ui.product_id, ui.order_id, ui.activity_id, ui.reward_id, ui.status, ui.remark, ui.created_at, ui.updated_at, p.name as product_name, p.images_json as product_images, p.price as product_price FROM user_inventory ui LEFT JOIN products p ON p.id = ui.product_id WHERE ui.user_id = ? ` var args []interface{} args = append(args, userID) if req.Status > 0 { sql += " AND ui.status = ?" args = append(args, req.Status) } if errNum == nil { sql += " AND (p.name LIKE ? OR ui.id = ? OR ui.order_id = ?)" args = append(args, "%"+req.Keyword+"%", numKeyword, numKeyword) } else { sql += " AND p.name LIKE ?" args = append(args, "%"+req.Keyword+"%") } sql += " ORDER BY ui.id DESC LIMIT ? OFFSET ?" args = append(args, req.PageSize, (req.Page-1)*req.PageSize) err = h.repo.GetDbR().Raw(sql, args...).Scan(&rows).Error if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error())) return } // 转换结果 items := make([]*user.InventoryWithProduct, len(rows)) for i, r := range rows { items[i] = &user.InventoryWithProduct{ UserInventory: &model.UserInventory{ ID: r.ID, UserID: r.UserID, ProductID: r.ProductID, OrderID: r.OrderID, ActivityID: r.ActivityID, RewardID: r.RewardID, Status: r.Status, Remark: r.Remark, }, ProductName: r.ProductName, ProductImages: r.ProductImages, ProductPrice: r.ProductPrice, } } rsp.Page = req.Page rsp.PageSize = req.PageSize rsp.Total = total rsp.List = items ctx.Payload(rsp) return } // 无搜索关键词时使用原有逻辑 rows, total, err := h.userSvc.ListInventoryWithProduct(ctx.RequestContext(), userID, req.Page, req.PageSize, req.Status) 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.userSvc.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"` BalanceAmount int64 `json:"balance_amount"` } 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 BalanceAmount int64 } q := base. 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, ). LeftJoin(h.readDB.SystemCoupons, h.readDB.SystemCoupons.ID.EqCol(h.readDB.UserCoupons.CouponID)). 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, BalanceAmount: v.BalanceAmount, } } ctx.Payload(rsp) } } type AuditLogItem struct { CreatedAt string `json:"created_at"` // 时间 Category string `json:"category"` // 大类: points/order/shipping/draw SubType string `json:"sub_type"` // 子类: action/status AmountStr string `json:"amount_str"` // 金额/数值变化 (带符号字符串) RefInfo string `json:"ref_info"` // 关联信息 (RefID/OrderNo/ExpressNo) DetailInfo string `json:"detail_info"` // 详细描述 (Remark/PrizeName) } type listAuditLogsResponse struct { Page int `json:"page"` PageSize int `json:"page_size"` Total int64 `json:"total"` // 由于UNION ALL分页较难精确Count Total,这里可能返回估算值或分步Count,为简化MVP先只做翻页不用Total或者Total设为0 List []AuditLogItem `json:"list"` } // ListUserAuditLogs 查看用户行为审计日志 // @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 "每页数量" default(20) // @Success 200 {object} listAuditLogsResponse // @Failure 400 {object} code.Failure // @Router /api/admin/users/{user_id}/audit [get] // @Security LoginVerifyToken func (h *handler) ListUserAuditLogs() core.HandlerFunc { return func(ctx core.Context) { req := new(listInvitesRequest) // 复用分页参数结构 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 } if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 { req.PageSize = 20 } offset := (req.Page - 1) * req.PageSize limit := req.PageSize var logs []AuditLogItem // 构建 UNION ALL 查询 // 1. 积分流水 // 2. 订单记录 // 3. 发货记录 // 4. 抽奖记录 (只看中奖的? 或者全部? 这里先只看中奖 IsWinner=1 避免数据量太大) sql := ` SELECT * FROM ( -- 1. Points Ledger SELECT created_at, 'points' as category, CONVERT(action USING utf8mb4) as sub_type, CAST(points AS CHAR) as amount_str, CONCAT(CONVERT(ref_table USING utf8mb4), ':', CONVERT(ref_id USING utf8mb4)) as ref_info, CONVERT(remark USING utf8mb4) as detail_info FROM user_points_ledger WHERE user_id = ? UNION ALL -- 2. Orders SELECT created_at, 'order' as category, 'paid' as sub_type, CAST(actual_amount AS CHAR) as amount_str, CONVERT(order_no USING utf8mb4) as ref_info, CONVERT(remark USING utf8mb4) as detail_info FROM orders WHERE user_id = ? AND status = 2 UNION ALL -- 3. Shipping Records SELECT created_at, 'shipping' as category, CAST(status AS CHAR) as sub_type, CAST(quantity AS CHAR) as amount_str, CONCAT(IFNULL(CONVERT(express_code USING utf8mb4),''), ':', IFNULL(CONVERT(express_no USING utf8mb4),'')) as ref_info, CONVERT(remark USING utf8mb4) as detail_info FROM shipping_records WHERE user_id = ? UNION ALL -- 4. Draw Logs (Winners) SELECT l.created_at, 'draw' as category, IF(l.is_winner=1, 'win', 'lose') as sub_type, CAST(1 AS CHAR) as amount_str, CAST(l.order_id AS CHAR) as ref_info, CONCAT( '游戏: ', IFNULL(CONVERT(act.name USING utf8mb4), '未知'), ' | 奖品: ', IFNULL(CONVERT(prod.name USING utf8mb4), '未知'), ' | 级别: ', CASE l.level WHEN 1 THEN 'S' WHEN 2 THEN 'A' WHEN 3 THEN 'B' WHEN 4 THEN 'C' ELSE CAST(l.level AS CHAR) END ) as detail_info FROM activity_draw_logs l LEFT JOIN activity_issues issue ON l.issue_id = issue.id LEFT JOIN activities act ON issue.activity_id = act.id LEFT JOIN activity_reward_settings reward ON l.reward_id = reward.id LEFT JOIN products prod ON reward.product_id = prod.id WHERE l.user_id = ? AND l.is_winner = 1 ) as combined_logs ORDER BY created_at DESC LIMIT ? OFFSET ? ` if err := h.repo.GetDbR().Raw(sql, userID, userID, userID, userID, limit, offset).Scan(&logs).Error; err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 20107, err.Error())) return } // 格式化处理 (Optional) for i := range logs { // 将时间标准化 if t, err := time.Parse(time.RFC3339, logs[i].CreatedAt); err == nil { logs[i].CreatedAt = t.Format("2006-01-02 15:04:05") } // 翻译 Shipping Status 等 (可选项,也可以前端做) if logs[i].Category == "shipping" { switch logs[i].SubType { case "1": logs[i].SubType = "待发货" case "2": logs[i].SubType = "已发货" case "3": logs[i].SubType = "已签收" case "4": logs[i].SubType = "异常" } } } ctx.Payload(&listAuditLogsResponse{ Page: req.Page, PageSize: req.PageSize, Total: 0, // 为了性能暂时忽略 List: logs, }) } } func nullableToString(s *string) string { if s == nil { return "" } return *s } type listPointsRequest struct { Page int `form:"page"` PageSize int `form:"page_size"` } type adminUserPointsLedgerItem struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` Action string `json:"action"` Points float64 `json:"points"` // 改为 float64 支持小数积分 RefTable string `json:"ref_table"` RefID string `json:"ref_id"` Remark string `json:"remark"` CreatedAt string `json:"created_at"` } type listPointsResponse struct { Page int `json:"page"` PageSize int `json:"page_size"` Total int64 `json:"total"` List []adminUserPointsLedgerItem `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.userSvc.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 // Convert ledger items rsp.List = make([]adminUserPointsLedgerItem, len(items)) for i, v := range items { rsp.List[i] = adminUserPointsLedgerItem{ ID: v.ID, UserID: v.UserID, Action: v.Action, Points: h.userSvc.CentsToPointsFloat(ctx.RequestContext(), v.Points), RefTable: v.RefTable, RefID: v.RefID, Remark: v.Remark, CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"), } } 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"` InviterNickname string `json:"inviter_nickname"` // 邀请人昵称 CreatedAt string `json:"created_at"` DouyinID string `json:"douyin_id"` DouyinUserID string `json:"douyin_user_id"` // 用户的抖音账号ID Remark string `json:"remark"` // 备注 ChannelName string `json:"channel_name"` ChannelCode string `json:"channel_code"` PointsBalance int64 `json:"points_balance"` CouponsCount int64 `json:"coupons_count"` ItemCardsCount int64 `json:"item_cards_count"` TodayConsume int64 `json:"today_consume"` SevenDayConsume int64 `json:"seven_day_consume"` ThirtyDayConsume int64 `json:"thirty_day_consume"` // 近30天消费 TotalConsume int64 `json:"total_consume"` // 累计消费 InviteCount int64 `json:"invite_count"` // 邀请人数 GamePassCount int64 `json:"game_pass_count"` // 次数卡数量 GameTicketCount int64 `json:"game_ticket_count"` // 游戏资格数量 InventoryValue int64 `json:"inventory_value"` // 持有商品总价值 TotalAssetValue int64 `json:"total_asset_value"` // 总资产估值 Status int32 `json:"status"` // 用户状态:1正常 2禁用 3黑名单 } // ListAppUsers 管理端用户列表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.userSvc.GetPointsBalance(ctx.RequestContext(), userID) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 20107, err.Error())) return } rsp.Balance = int64(h.userSvc.CentsToPointsFloat(ctx.RequestContext(), total)) ctx.Payload(rsp) } } type addPointsRequest struct { Points float64 `json:"points"` // 正数=增加,负数=扣减 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 } if req.Points == 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "积分变动值不能为0")) return } userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) return } // 将浮点数积分转换为分(Cents) // 1 积分 = 1 元 = 100 分 // 使用 math.Round 避免精度问题 pointsCents := int64(math.Round(req.Points * 100)) // 如果是扣减积分,先检查余额 if pointsCents < 0 { balance, _ := h.userSvc.GetPointsBalance(ctx.RequestContext(), userID) deductCents := -pointsCents if balance < deductCents { ctx.AbortWithError(core.Error(http.StatusBadRequest, 20108, "积分余额不足,无法扣减")) return } } var validStart *time.Time var validEnd *time.Time now := time.Now() // 只有增加积分时才设置有效期 if pointsCents > 0 { validStart = &now if req.ValidDays != nil && *req.ValidDays > 0 { ve := now.Add(time.Duration(*req.ValidDays) * 24 * time.Hour) validEnd = &ve } } if err := h.userSvc.AddPoints(ctx.RequestContext(), userID, pointsCents, 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.userSvc.AddCoupon(ctx.RequestContext(), userID, req.CouponID); err != nil { msg := err.Error() if msg == "unsupported data" { msg = "发券失败:模板配额已满" } ctx.AbortWithError(core.Error(http.StatusBadRequest, 20109, msg)) return } rsp.Success = true ctx.Payload(rsp) } } type voidUserCouponRequest struct { } func (h *handler) VoidUserCoupon() core.HandlerFunc { return func(ctx core.Context) { req := new(voidUserCouponRequest) if err := ctx.ShouldBindJSON(req); err != nil && err.Error() != "EOF" { 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 } ucid, err := strconv.ParseInt(ctx.Param("user_coupon_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 } adminID := int64(ctx.SessionUserInfo().Id) if err := h.userSvc.VoidUserCoupon(ctx.RequestContext(), adminID, userID, ucid); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 20109, err.Error())) return } ctx.Payload(simpleMessageResponse{Message: "操作成功"}) } } type voidUserItemCardRequest struct { } func (h *handler) VoidUserItemCard() core.HandlerFunc { return func(ctx core.Context) { req := new(voidUserItemCardRequest) if err := ctx.ShouldBindJSON(req); err != nil && err.Error() != "EOF" { 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 } icid, err := strconv.ParseInt(ctx.Param("user_item_card_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 } adminID := int64(ctx.SessionUserInfo().Id) if err := h.userSvc.VoidUserItemCard(ctx.RequestContext(), adminID, userID, icid); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 20109, err.Error())) return } ctx.Payload(simpleMessageResponse{Message: "操作成功"}) } } type voidUserInventoryRequest struct { } func (h *handler) VoidUserInventory() core.HandlerFunc { return func(ctx core.Context) { req := new(voidUserInventoryRequest) if err := ctx.ShouldBindJSON(req); err != nil && err.Error() != "EOF" { 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 } invID, err := strconv.ParseInt(ctx.Param("inventory_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 } adminID := int64(ctx.SessionUserInfo().Id) if err := h.userSvc.VoidUserInventory(ctx.RequestContext(), adminID, userID, invID); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 20109, err.Error())) return } ctx.Payload(simpleMessageResponse{Message: "操作成功"}) } } type adminUserTitleItem struct { ID int64 `json:"id"` TitleID int64 `json:"title_id"` Name string `json:"name"` Description string `json:"description"` ObtainedAt string `json:"obtained_at"` ExpiresAt string `json:"expires_at"` Status int32 `json:"status"` } type listUserTitlesResponse struct { List []adminUserTitleItem `json:"list"` } func (h *handler) ListUserTitles() core.HandlerFunc { return func(ctx core.Context) { rsp := new(listUserTitlesResponse) userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) return } type row struct { ID int64 TitleID int64 Active int32 ObtainedAt *string ExpiresAt *string Name string Description string } q := h.readDB.UserTitles.WithContext(ctx.RequestContext()).ReadDB(). LeftJoin(h.readDB.SystemTitles, h.readDB.SystemTitles.ID.EqCol(h.readDB.UserTitles.TitleID)). Select( h.readDB.UserTitles.ID, h.readDB.UserTitles.TitleID, h.readDB.UserTitles.Active, h.readDB.UserTitles.ObtainedAt, h.readDB.UserTitles.ExpiresAt, h.readDB.SystemTitles.Name, h.readDB.SystemTitles.Description, ). Where(h.readDB.UserTitles.UserID.Eq(userID)). Order(h.readDB.UserTitles.ID.Desc()) var rows []row if err := q.Scan(&rows); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 20110, err.Error())) return } rsp.List = make([]adminUserTitleItem, len(rows)) for i, v := range rows { rsp.List[i] = adminUserTitleItem{ ID: v.ID, TitleID: v.TitleID, Name: v.Name, Description: v.Description, ObtainedAt: nullableToString(v.ObtainedAt), ExpiresAt: nullableToString(v.ExpiresAt), Status: v.Active, } } ctx.Payload(rsp) } } type listUserCouponUsageRequest struct { Page int `form:"page"` PageSize int `form:"page_size"` } type adminUserCouponUsageItem struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` UserCouponID int64 `json:"user_coupon_id"` ChangeAmount int64 `json:"change_amount"` BalanceAfter int64 `json:"balance_after"` OrderID int64 `json:"order_id"` Action string `json:"action"` CreatedAt string `json:"created_at"` } type listUserCouponUsageResponse struct { Page int `json:"page"` PageSize int `json:"page_size"` Total int64 `json:"total"` List []adminUserCouponUsageItem `json:"list"` } func (h *handler) ListUserCouponUsage() core.HandlerFunc { return func(ctx core.Context) { req := new(listUserCouponUsageRequest) rsp := new(listUserCouponUsageResponse) 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 } userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) return } ucid, err := strconv.ParseInt(ctx.Param("user_coupon_id"), 10, 64) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递持券ID")) return } var total int64 db := h.repo.GetDbR().Model(&model.UserCouponLedger{}).Where("user_id = ? AND user_coupon_id = ?", userID, ucid) _ = db.Count(&total).Error var list []model.UserCouponLedger _ = db.Order("id DESC").Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find(&list).Error rows := make([]adminUserCouponUsageItem, len(list)) for i, v := range list { rows[i] = adminUserCouponUsageItem{ ID: v.ID, UserID: v.UserID, UserCouponID: v.UserCouponID, ChangeAmount: v.ChangeAmount, BalanceAfter: v.BalanceAfter, OrderID: v.OrderID, Action: v.Action, CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"), } } rsp.Page = req.Page rsp.PageSize = req.PageSize rsp.Total = total if rows == nil { rows = []adminUserCouponUsageItem{} } rsp.List = rows ctx.Payload(rsp) } } // LinkUserDouyinRequest 关联用户抖音账号请求 type LinkUserDouyinRequest struct { DouyinUserID string `json:"douyin_user_id" binding:"required"` } // UpdateUserDouyinID 更新用户的抖音账号ID // @Summary 更新用户抖音ID // @Description 管理员绑定或修改用户的抖音账号ID // @Tags 管理端.用户 // @Accept json // @Produce json // @Param user_id path integer true "用户ID" // @Param body body LinkUserDouyinRequest true "抖音用户ID" // @Success 200 {object} map[string]any // @Failure 400 {object} code.Failure // @Router /api/admin/users/{user_id}/douyin_user_id [put] // @Security LoginVerifyToken func (h *handler) UpdateUserDouyinID() core.HandlerFunc { return func(ctx core.Context) { userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "用户ID无效")) return } req := new(LinkUserDouyinRequest) if err := ctx.ShouldBindJSON(req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } // 更新用户抖音ID _, err = h.writeDB.Users.WithContext(ctx.RequestContext()). Where(h.writeDB.Users.ID.Eq(userID)). Update(h.writeDB.Users.DouyinUserID, req.DouyinUserID) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 20301, "更新失败: "+err.Error())) return } ctx.Payload(map[string]any{ "success": true, "message": "抖音ID更新成功", }) } } // updateUserRemarkRequest 更新用户备注请求 type updateUserRemarkRequest struct { Remark string `json:"remark"` } // UpdateUserRemark 更新用户备注 // @Summary 更新用户备注 // @Description 管理员修改用户备注 // @Tags 管理端.用户 // @Accept json // @Produce json // @Param user_id path integer true "用户ID" // @Param body body updateUserRemarkRequest true "备注信息" // @Success 200 {object} map[string]any // @Failure 400 {object} code.Failure // @Router /api/admin/users/{user_id}/remark [put] // @Security LoginVerifyToken func (h *handler) UpdateUserRemark() core.HandlerFunc { return func(ctx core.Context) { userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "用户ID无效")) return } req := new(updateUserRemarkRequest) if err := ctx.ShouldBindJSON(req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } // 更新用户备注 _, err = h.writeDB.Users.WithContext(ctx.RequestContext()). Where(h.writeDB.Users.ID.Eq(userID)). Update(h.writeDB.Users.Remark, req.Remark) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 20302, "更新失败: "+err.Error())) return } ctx.Payload(map[string]any{ "success": true, "message": "备注更新成功", }) } } type updateUserStatusRequest struct { Status int32 `json:"status" form:"status"` // 1=正常 2=禁用 3=黑名单 } // UpdateUserStatus 修改用户状态 // @Summary 修改用户状态 // @Description 管理员修改用户状态(1正常 2禁用 3黑名单) // @Tags 管理端.用户 // @Accept json // @Produce json // @Param user_id path integer true "用户ID" // @Param body body updateUserStatusRequest true "状态信息" // @Success 200 {object} map[string]any // @Failure 400 {object} code.Failure // @Router /api/admin/users/{user_id}/status [put] // @Security LoginVerifyToken func (h *handler) UpdateUserStatus() core.HandlerFunc { return func(ctx core.Context) { req := new(updateUserStatusRequest) 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 req.Status != 1 && req.Status != 2 && req.Status != 3 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的状态值")) return } // 使用 Updates 以支持更新为 0 (虽然这里status不为0) 但 gorm Update 单列更安全 _, err = h.writeDB.Users.WithContext(ctx.RequestContext()). Where(h.writeDB.Users.ID.Eq(userID)). Update(h.writeDB.Users.Status, req.Status) if err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error())) return } ctx.Payload(map[string]any{"success": true}) } }