e8e3a3a72b
- 分页列出 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>
205 lines
6.2 KiB
Go
205 lines
6.2 KiB
Go
// 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
|
||
}
|