package app import ( "encoding/json" "net/http" "strconv" "time" "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/validation" "bindbox-game/internal/repository/mysql/model" ) type listDrawLogsRequest struct { Page int `form:"page"` PageSize int `form:"page_size"` Level *int32 `form:"level"` } type drawLogItem struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` UserName string `json:"user_name"` Avatar string `json:"avatar"` IssueID int64 `json:"issue_id"` OrderID int64 `json:"order_id"` RewardID int64 `json:"reward_id"` RewardName string `json:"reward_name"` RewardImage string `json:"reward_image"` IsWinner int32 `json:"is_winner"` Level int32 `json:"level"` CurrentLevel int32 `json:"current_level"` } type listDrawLogsResponse struct { Page int `json:"page"` PageSize int `json:"page_size"` Total int64 `json:"total"` List []drawLogItem `json:"list"` } // ListDrawLogs 抽奖记录列表 // @Summary 抽奖记录列表 // @Description 查看指定活动期数的抽奖记录,支持等级筛选(默认返回最新的100条,不支持自定义翻页) // @Tags APP端.活动 // @Accept json // @Produce json // @Param activity_id path integer true "活动ID" // @Param issue_id path integer true "期ID" // @Param level query int false "奖品等级过滤" // @Success 200 {object} listDrawLogsResponse // @Failure 400 {object} code.Failure // @Router /api/app/activities/{activity_id}/issues/{issue_id}/draw_logs [get] func (h *handler) ListDrawLogs() core.HandlerFunc { return func(ctx core.Context) { req := new(listDrawLogsRequest) res := new(listDrawLogsResponse) if err := ctx.ShouldBindForm(req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } issueID, err := strconv.ParseInt(ctx.Param("issue_id"), 10, 64) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递期ID")) return } activityID, _ := strconv.ParseInt(ctx.Param("activity_id"), 10, 64) if activityID > 0 { act, err := h.activity.GetActivity(ctx.RequestContext(), activityID) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.GetActivityError, err.Error())) return } if act.Status != 1 { ctx.AbortWithError(core.Error(http.StatusNotFound, code.GetActivityError, "该活动已下线")) return } } // 强制固定分页:第一页,100条 page := 1 pageSize := 100 // 计算5分钟前的时间点 fiveMinutesAgo := time.Now().Add(-5 * time.Minute) // 考虑到需要过滤掉5分钟内的数据,我们稍微多取一些数据,以确保过滤后能有足够的数据展示 // 但为了防止数据量过大,还是做一个硬性限制,比如取前200条 fetchSize := 200 items, _, err := h.activity.ListDrawLogs(ctx.RequestContext(), issueID, 1, fetchSize, req.Level) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListDrawLogsError, err.Error())) return } // 收集ID用于批量查询 var userIDs []int64 var rewardIDs []int64 userSet := make(map[int64]struct{}) rewardSet := make(map[int64]struct{}) // 过滤并收集ID var filteredItems []*model.ActivityDrawLogs for _, v := range items { // 过滤掉5分钟内的记录 if v.CreatedAt.After(fiveMinutesAgo) { continue } // 如果已经收集够了100条,就不再收集了 if len(filteredItems) >= pageSize { break } filteredItems = append(filteredItems, v) if v.UserID > 0 { if _, ok := userSet[v.UserID]; !ok { userSet[v.UserID] = struct{}{} userIDs = append(userIDs, v.UserID) } } if v.RewardID > 0 { if _, ok := rewardSet[v.RewardID]; !ok { rewardSet[v.RewardID] = struct{}{} rewardIDs = append(rewardIDs, v.RewardID) } } } // 更新items为过滤后的列表 items = filteredItems total := int64(len(items)) // 更新total为实际返回数量 // 批量查询用户信息 userNameMap := make(map[int64]string) userAvatarMap := make(map[int64]string) if len(userIDs) > 0 { users, err := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Users.ID.In(userIDs...)).Find() if err == nil { for _, u := range users { userNameMap[u.ID] = u.Nickname userAvatarMap[u.ID] = u.Avatar } } } // 批量查询奖品与商品信息 rewardNameMap := make(map[int64]string) rewardImageMap := make(map[int64]string) if len(rewardIDs) > 0 { rewards, err := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.ActivityRewardSettings.ID.In(rewardIDs...)).Find() if err == nil { var productIDs []int64 productSet := make(map[int64]struct{}) rewardProductMap := make(map[int64]int64) for _, r := range rewards { rewardNameMap[r.ID] = r.Name if r.ProductID > 0 { if _, ok := productSet[r.ProductID]; !ok { productSet[r.ProductID] = struct{}{} productIDs = append(productIDs, r.ProductID) } rewardProductMap[r.ID] = r.ProductID } } if len(productIDs) > 0 { products, err := h.readDB.Products.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Products.ID.In(productIDs...)).Find() if err == nil { productImageMap := make(map[int64]string) productNameMap := make(map[int64]string) for _, p := range products { first := "" if p.ImagesJSON != "" { var arr []string _ = json.Unmarshal([]byte(p.ImagesJSON), &arr) if len(arr) > 0 { first = arr[0] } } productImageMap[p.ID] = first productNameMap[p.ID] = p.Name } // 填充奖品图片(如果奖品没有名字,也可以用商品名字兜底,但这里先只做图片) for rid, pid := range rewardProductMap { rewardImageMap[rid] = productImageMap[pid] // 如果配置里的奖品名为空,尝试用商品名 if rewardNameMap[rid] == "" { rewardNameMap[rid] = productNameMap[pid] } } } } } } res.Page = page res.PageSize = pageSize res.Total = total res.List = make([]drawLogItem, len(items)) for i, v := range items { res.List[i] = drawLogItem{ ID: v.ID, UserID: v.UserID, UserName: userNameMap[v.UserID], Avatar: userAvatarMap[v.UserID], IssueID: v.IssueID, OrderID: v.OrderID, RewardID: v.RewardID, RewardName: rewardNameMap[v.RewardID], RewardImage: rewardImageMap[v.RewardID], IsWinner: v.IsWinner, Level: v.Level, CurrentLevel: v.CurrentLevel, } } ctx.Payload(res) } } // ListDrawLogsByLevel 按奖品等级分类的抽奖记录 // @Summary 按奖品等级分类的抽奖记录 // @Description 查看指定活动期数的抽奖记录,按奖品等级分组返回 // @Tags APP端.活动 // @Accept json // @Produce json // @Param activity_id path integer true "活动ID" // @Param issue_id path integer true "期ID" // @Success 200 {object} listDrawLogsByLevelResponse // @Failure 400 {object} code.Failure // @Router /api/app/activities/{activity_id}/issues/{issue_id}/draw_logs_grouped [get] func (h *handler) ListDrawLogsByLevel() core.HandlerFunc { return func(ctx core.Context) { issueID, err := strconv.ParseInt(ctx.Param("issue_id"), 10, 64) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递期ID")) return } activityID, _ := strconv.ParseInt(ctx.Param("activity_id"), 10, 64) if activityID > 0 { act, err := h.activity.GetActivity(ctx.RequestContext(), activityID) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.GetActivityError, err.Error())) return } if act.Status != 1 { ctx.AbortWithError(core.Error(http.StatusNotFound, code.GetActivityError, "该活动已下线")) return } } // 1. 获取所有中奖记录 // 我们假设这里不需要分页,或者分页逻辑比较复杂(每个等级分页?)。 // 根据需求描述“按奖品等级进行归类”,通常 implied 展示所有或者前N个。 // 这里暂且获取所有(或者一个较大的限制),然后在内存中分组。 // 如果数据量巨大,需要由 Service 层提供 Group By 查询。 // 考虑到单期中奖人数通常有限(除非是大规模活动),先尝试获取列表后分组。 logs, _, err := h.activity.ListDrawLogs(ctx.RequestContext(), issueID, 1, 1000, nil) // 假设最多显示1000条中奖记录 if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListDrawLogsError, err.Error())) return } // 2. 获取奖品配置以获取等级名称 rewards, err := h.activity.ListIssueRewards(ctx.RequestContext(), issueID) if err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.GetActivityError, err.Error())) return } levelNameMap := make(map[int32]string) for _, r := range rewards { // 通常 Level 1 是大奖,名字如 "一等奖" // 也有可能 ActivityRewardSettings 里没有直接存 "一等奖" 这种字样,而是 Name="iPhone 15"。 // 如果需要 "一等奖" 这种分类名,可能需要额外配置或从 Name 推断。 // 此处暂且使用 Reward 的 Name 作为 fallback,或者如果有 LevelName 字段最好。 // 查看 model 定义,ActivityRewardSettings 只有 Name。 // 假如用户希望看到的是 "一等奖", "二等奖",我们需要确立 Level 到 显示名的映射。 // 现阶段简单起见,我们将同一 Level 的奖品视为一组。 // 组名可以使用该 Level 下任意一个奖品的 Name,或者如果不一致,则可能需要前端映射。 // 为了通用性,我们返回 level 值,并尝试找到一个代表性的 Name。 if _, ok := levelNameMap[r.Level]; !ok { levelNameMap[r.Level] = r.Name // 简单取第一个遇到的名字 } } // 3. 分组 (只显示5分钟前的记录) groupsMap := make(map[int32][]drawLogItem) fiveMinutesAgo := time.Now().Add(-5 * time.Minute) for _, v := range logs { // 过滤掉5分钟内的记录 if v.CreatedAt.After(fiveMinutesAgo) { continue } if v.IsWinner == 1 { item := drawLogItem{ ID: v.ID, UserID: v.UserID, IssueID: v.IssueID, OrderID: v.OrderID, RewardID: v.RewardID, IsWinner: v.IsWinner, Level: v.Level, CurrentLevel: v.CurrentLevel, } groupsMap[v.Level] = append(groupsMap[v.Level], item) } } // 4. 构造响应 var resp listDrawLogsByLevelResponse for level, items := range groupsMap { group := drawLogGroup{ Level: level, LevelName: levelNameMap[level], List: items, } resp.Groups = append(resp.Groups, group) } // 排序 Groups (Level 升序? 也就是 1等奖在前) // 简单的冒泡排序或 slice sort for i := 0; i < len(resp.Groups)-1; i++ { for j := 0; j < len(resp.Groups)-1-i; j++ { if resp.Groups[j].Level > resp.Groups[j+1].Level { resp.Groups[j], resp.Groups[j+1] = resp.Groups[j+1], resp.Groups[j] } } } ctx.Payload(resp) } } type drawLogGroup struct { Level int32 `json:"level"` LevelName string `json:"level_name"` List []drawLogItem `json:"list"` } type listDrawLogsByLevelResponse struct { Groups []drawLogGroup `json:"groups"` }