bindbox-game/internal/api/activity/draw_logs_app.go
邹方成 c8b04e2bc6 feat(活动): 新增抽奖记录等级筛选功能并优化展示信息
refactor(抽奖记录): 重构抽奖记录列表接口,支持按等级筛选
新增用户昵称、头像及奖品名称、图片等展示字段
优化分页逻辑,默认返回最新100条记录

feat(游戏): 添加扫雷游戏验证和结算接口
新增游戏票据验证和结算相关接口定义及Swagger文档

docs(API): 更新Swagger文档
更新抽奖记录和游戏相关接口的文档描述

style(路由): 添加游戏路由注释
添加扫雷游戏接口路由的占位注释
2025-12-21 23:45:01 +08:00

324 lines
11 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 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
}
// 强制固定分页第一页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
}
// 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"`
}