Files
hi-server/internal/logic/admin/lottery/admin_draws.go
T
shanshanzhong147 e8e3a3a72b feat(#4): 后台抽奖记录列表 GET /admin/lottery/draws
- 分页列出 lottery_draw,join 奖品快照 + 用户邮箱 + 发放账本(grant_ledger)
- 支持 activity/user/win/dispatch_state/prize_type/时间窗过滤
- 展示中奖人/奖品/发放状态/发放结果(如"已新建订阅并加 30 天")

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 23:36:53 -07:00

205 lines
6.2 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.
// 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
}