bindbox-game/internal/api/activity/draw_logs_app.go
2026-01-27 01:33:32 +08:00

358 lines
12 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条不支持自定义翻页
// @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)
// 计算当天零点 (用于仅显示当天数据)
startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
// [修改] 强制获取当天最新的 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 {
// 1. 过滤掉太新的数据 (5分钟延迟)
if v.CreatedAt.After(fiveMinutesAgo) {
continue
}
// 2. 过滤掉非当天的数据 (当天零点之前)
if v.CreatedAt.Before(startOfToday) {
// 因为是按时间倒序返回的,一旦遇到早于今天的,后续的更早,直接结束
break
}
// 3. 数量限制 (虽然 Service 取了 100这里再保个底或者遵循前端 pageSize?
// 需求是 "获取当天的 最新100 个数据",这里我们以 filteredItems 为准,
// 如果前端 pageSize 传了比如 20是否应该只给 20
// 按照通常逻辑,列表接口应遵循 pageSize。但在这种定制逻辑下用户似乎想要的是“当天数据的视图”。
// 保持原逻辑:遵循 pageSize 限制输出数量,但我们上面强行取了 100 作为源数据。
// 如果用户原本想看 100 条,前端传 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 = 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"`
}