refactor(orders): 重构订单列表查询逻辑,支持按消耗状态筛选 feat(orders): 订单列表返回新增活动分类与玩法类型信息 fix(orders): 修复订单支付时间空指针问题 docs(swagger): 更新订单相关接口文档 test(matching): 添加对对碰奖励匹配测试用例 chore: 清理无用脚本文件
988 lines
32 KiB
Go
988 lines
32 KiB
Go
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"`
|
||
ID *int64 `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
|
||
}
|
||
q := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().
|
||
LeftJoin(h.readDB.Channels, h.readDB.Channels.ID.EqCol(h.readDB.Users.ChannelID)).
|
||
Select(h.readDB.Users.ALL, h.readDB.Channels.Name.As("channel_name"), h.readDB.Channels.Code.As("channel_code"))
|
||
|
||
// 应用搜索条件
|
||
if req.ID != nil {
|
||
q = q.Where(h.readDB.Users.ID.Eq(*req.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
|
||
}
|
||
}
|
||
|
||
// 批量查询道具卡数量(未使用的)
|
||
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)
|
||
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) // 包括今天共7天
|
||
|
||
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)). // 2=已支付
|
||
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)). // 2=已支付
|
||
Where(h.readDB.Orders.CreatedAt.Gte(sevenDayStart)).
|
||
Group(h.readDB.Orders.UserID).
|
||
Scan(&sevenRes)
|
||
for _, r := range sevenRes {
|
||
sevenDayConsume[r.UserID] = r.Amount
|
||
}
|
||
}
|
||
|
||
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"),
|
||
DouyinID: v.DouyinID,
|
||
ChannelName: v.ChannelName,
|
||
ChannelCode: v.ChannelCode,
|
||
CouponsCount: couponCounts[v.ID],
|
||
ItemCardsCount: cardCounts[v.ID],
|
||
TodayConsume: todayConsume[v.ID],
|
||
SevenDayConsume: sevenDayConsume[v.ID],
|
||
}
|
||
}
|
||
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, 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)
|
||
// @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"`
|
||
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 := 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,
|
||
h.readDB.UserCoupons.BalanceAmount,
|
||
).
|
||
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,
|
||
BalanceAmount: v.BalanceAmount,
|
||
}
|
||
}
|
||
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"`
|
||
DouyinID string `json:"douyin_id"`
|
||
ChannelName string `json:"channel_name"`
|
||
ChannelCode string `json:"channel_code"`
|
||
CouponsCount int64 `json:"coupons_count"`
|
||
ItemCardsCount int64 `json:"item_cards_count"`
|
||
TodayConsume int64 `json:"today_consume"`
|
||
SevenDayConsume int64 `json:"seven_day_consume"`
|
||
}
|
||
|
||
// 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.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 {
|
||
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.user.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.user.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.user.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)
|
||
}
|
||
}
|