// admin_draws.go 实现后台抽奖记录(发放流水)接口: // // GET /v1/admin/lottery/draws — 分页列表(谁/何时/中了什么/发放状态与结果) // // 自动奖(vpn_duration/commission)的实际发放结果取自 lottery_grant_ledger.payload; // 人工奖(crypto/physical/manual_other)的领奖进展在 /claims 里看。 package lottery import ( "context" "encoding/json" "strings" "time" modelLottery "github.com/perfect-panel/server/internal/model/lottery" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) type ListLotteryDrawsLogic struct { logger.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewListLotteryDrawsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListLotteryDrawsLogic { return &ListLotteryDrawsLogic{Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx} } // ListLotteryDraws 按 activity/user/win/dispatch_state/prize_type/时间窗过滤, // 附带奖品快照 + 用户邮箱 + 发放账本结果,避免前端 N+1。 func (l *ListLotteryDrawsLogic) ListLotteryDraws(req *types.ListAdminLotteryDrawsRequest) (*types.ListAdminLotteryDrawsResponse, error) { if currentAdminId(l.ctx) == 0 { return nil, xerr.NewErrCode(xerr.ErrorTokenInvalid) } page, size := req.Page, req.Size if page <= 0 { page = 1 } if size <= 0 || size > 200 { size = 20 } db := l.svcCtx.DB.WithContext(l.ctx).Model(&modelLottery.Draw{}) if req.ActivityId > 0 { db = db.Where("activity_id = ?", req.ActivityId) } if req.UserId > 0 { db = db.Where("user_id = ?", req.UserId) } switch strings.TrimSpace(req.Win) { case "1": db = db.Where("is_win = ?", true) case "0": db = db.Where("is_win = ?", false) } if s := strings.TrimSpace(req.DispatchState); s != "" { db = db.Where("dispatch_state = ?", s) } // prize_type 挂在快照表上,用 EXISTS 子查询过滤(避免 JOIN 影响分页去重)。 if pt := strings.TrimSpace(req.PrizeType); pt != "" { db = db.Where("EXISTS (SELECT 1 FROM lottery_prize_snapshot s WHERE s.draw_id = lottery_draw.id AND s.type = ?)", pt) } if req.From > 0 { db = db.Where("drawn_at >= ?", time.Unix(req.From, 0)) } if req.To > 0 { db = db.Where("drawn_at < ?", time.Unix(req.To, 0)) } var total int64 if err := db.Count(&total).Error; err != nil { return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) } var rows []modelLottery.Draw if err := db.Order("id DESC").Limit(size).Offset((page - 1) * size).Find(&rows).Error; err != nil { return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) } drawIds := make([]int64, 0, len(rows)) userIds := make([]int64, 0, len(rows)) for _, d := range rows { drawIds = append(drawIds, d.Id) userIds = append(userIds, d.UserId) } snaps, err := l.loadSnapshots(drawIds) if err != nil { return nil, err } ledgers, err := l.loadLedgers(drawIds) if err != nil { return nil, err } users, err := l.loadUsers(userIds) if err != nil { return nil, err } resp := &types.ListAdminLotteryDrawsResponse{Total: total, List: make([]types.AdminLotteryDraw, 0, len(rows))} for _, d := range rows { resp.List = append(resp.List, drawToAdminView(d, snaps[d.Id], ledgers[d.Id], users[d.UserId])) } return resp, nil } func (l *ListLotteryDrawsLogic) loadSnapshots(drawIds []int64) (map[int64]modelLottery.PrizeSnapshot, error) { if len(drawIds) == 0 { return nil, nil } var snaps []modelLottery.PrizeSnapshot if err := l.svcCtx.DB.WithContext(l.ctx).Where("draw_id IN ?", drawIds).Find(&snaps).Error; err != nil { return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) } out := make(map[int64]modelLottery.PrizeSnapshot, len(snaps)) for _, s := range snaps { out[s.DrawId] = s } return out, nil } func (l *ListLotteryDrawsLogic) loadLedgers(drawIds []int64) (map[int64]modelLottery.GrantLedger, error) { if len(drawIds) == 0 { return nil, nil } var rows []modelLottery.GrantLedger if err := l.svcCtx.DB.WithContext(l.ctx).Where("draw_id IN ?", drawIds).Find(&rows).Error; err != nil { return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) } out := make(map[int64]modelLottery.GrantLedger, len(rows)) for _, r := range rows { out[r.DrawId] = r // draw ↔ ledger 一对一(一次抽奖至多一条发放) } return out, nil } func (l *ListLotteryDrawsLogic) loadUsers(ids []int64) (map[int64]string, error) { if len(ids) == 0 { return nil, nil } type row struct { UserId int64 Email string } var rows []row if err := l.svcCtx.DB.WithContext(l.ctx). Table("user_auth_methods"). Select("user_id AS user_id, auth_identifier AS email"). Where("auth_type = ? AND user_id IN ?", "email", ids). Order("created_at ASC"). Scan(&rows).Error; err != nil { return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) } out := make(map[int64]string, len(rows)) for _, r := range rows { if _, ok := out[r.UserId]; ok { continue } out[r.UserId] = r.Email } return out, nil } // drawToAdminView 组装单条抽奖记录后台视图。 func drawToAdminView(d modelLottery.Draw, snap modelLottery.PrizeSnapshot, ledger modelLottery.GrantLedger, email string) types.AdminLotteryDraw { view := types.AdminLotteryDraw{ DrawId: d.Id, ActivityId: d.ActivityId, IsWin: d.IsWin, DispatchState: d.DispatchState, DispatchError: d.DispatchError, DrawnAt: d.DrawnAt.Unix(), CreatedAt: d.CreatedAt.Unix(), User: types.AdminLotteryDrawUser{ Id: d.UserId, Email: email, }, } if d.DispatchedAt != nil { view.DispatchedAt = d.DispatchedAt.Unix() } if snap.DrawId != 0 || snap.Type != "" { view.Prize = &types.AdminLotteryDrawPrize{ Slot: snap.Slot, Type: snap.Type, Name: snap.Name, Config: json.RawMessage(defaultRawIfEmpty(snap.Config, "{}")), } } if ledger.Id != 0 { view.GrantAmount = ledger.Amount if ledger.Payload != "" { var p struct { Message string `json:"message"` } if err := json.Unmarshal([]byte(ledger.Payload), &p); err == nil { view.GrantMessage = p.Message } } } return view }