bindbox-game/internal/api/activity/draw_logs_app.go
2026-02-08 17:19:27 +08:00

346 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"
"fmt"
"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"`
CreatedAt time.Time `json:"created_at"`
}
type listDrawLogsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []drawLogItem `json:"list"`
}
// ListDrawLogs 抽奖记录列表
// @Summary 抽奖记录列表
// @Description 查看指定活动期数的抽奖记录支持等级筛选返回最近100条过滤掉5分钟内的数据
// @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
}
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 100
}
now := time.Now()
// 计算5分钟前的时间点 (用于延迟显示)
fiveMinutesAgo := now.Add(-5 * time.Minute)
// 强制获取最新的 100 条数据 (Service 层限制最大 100)
// 忽略前端传入的 Page/PageSize总是获取第一页的 100 条
fetchPageSize := 100
fetchPage := 1
items, total, err := h.activity.ListDrawLogs(ctx.RequestContext(), issueID, fetchPage, fetchPageSize, 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{})
var filteredItems []*model.ActivityDrawLogs
for _, v := range items {
// 过滤掉太新的数据 (5分钟延迟)
if v.CreatedAt.After(fiveMinutesAgo) {
continue
}
// 数量限制为 100 条
if len(filteredItems) >= 100 {
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 = filteredItems
total = int64(len(items))
// 批量查询用户信息
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 {
// 不再使用 r.Name只通过 ProductID 关联查询商品名称
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 pn, ok := productNameMap[pid]; ok && pn != "" {
rewardNameMap[rid] = pn
}
}
}
}
}
}
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,
CreatedAt: v.CreatedAt,
}
}
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)
// 收集所有 ProductID 用于批量查询商品名称
productIDs := make([]int64, 0, len(rewards))
rewardProductMap := make(map[int64]int64) // rewardID -> productID
for _, r := range rewards {
if r.ProductID > 0 {
productIDs = append(productIDs, r.ProductID)
rewardProductMap[r.ID] = r.ProductID
}
}
// 批量查询商品名称
productNameMap := make(map[int64]string)
if len(productIDs) > 0 {
products, _ := h.readDB.Products.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Products.ID.In(productIDs...)).Find()
for _, p := range products {
productNameMap[p.ID] = p.Name
}
}
// 构建等级名称映射
for _, r := range rewards {
if _, ok := levelNameMap[r.Level]; !ok {
// 使用商品名称作为等级名称
if r.ProductID > 0 {
if pn, ok := productNameMap[r.ProductID]; ok && pn != "" {
levelNameMap[r.Level] = pn
}
}
// 如果没有商品名称,使用等级编号
if levelNameMap[r.Level] == "" {
levelNameMap[r.Level] = fmt.Sprintf("等级%d", r.Level)
}
}
}
// 3. 分组 (恢复 5 分钟过滤)
groupsMap := make(map[int32][]drawLogItem)
fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
for _, v := range logs {
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"`
}