转盘抽奖修改
This commit is contained in:
parent
78bddf406d
commit
c4d4f03ddf
@ -101,7 +101,7 @@ func registerWheelRoutes(engine *gin.Engine, cfg config.Config, javaClient authG
|
|||||||
})
|
})
|
||||||
residentGroup.GET("/record/page", func(c *gin.Context) {
|
residentGroup.GET("/record/page", func(c *gin.Context) {
|
||||||
cursor, limit := parseCursorLimit(c)
|
cursor, limit := parseCursorLimit(c)
|
||||||
resp, err := wheelService.PageRecords(
|
resp, err := wheelService.PageAdminRecords(
|
||||||
c.Request.Context(),
|
c.Request.Context(),
|
||||||
c.Query("sysOrigin"),
|
c.Query("sysOrigin"),
|
||||||
c.Query("category"),
|
c.Query("category"),
|
||||||
@ -109,6 +109,8 @@ func registerWheelRoutes(engine *gin.Engine, cfg config.Config, javaClient authG
|
|||||||
strings.TrimSpace(c.Query("status")),
|
strings.TrimSpace(c.Query("status")),
|
||||||
cursor,
|
cursor,
|
||||||
limit,
|
limit,
|
||||||
|
c.Query("startTime"),
|
||||||
|
c.Query("endTime"),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
@ -217,11 +219,15 @@ func legacyRewardsHandler(wheelService *wheel.Service, category string) gin.Hand
|
|||||||
items := make([]gin.H, 0, len(pool.Rewards))
|
items := make([]gin.H, 0, len(pool.Rewards))
|
||||||
for _, reward := range pool.Rewards {
|
for _, reward := range pool.Rewards {
|
||||||
items = append(items, gin.H{
|
items = append(items, gin.H{
|
||||||
"id": reward.ID,
|
"id": reward.ID,
|
||||||
"value": legacyRewardValue(reward),
|
"animationUrl": reward.ResourceURL,
|
||||||
|
"resourceUrl": reward.ResourceURL,
|
||||||
|
"value": legacyRewardValue(reward),
|
||||||
"merchandise": gin.H{
|
"merchandise": gin.H{
|
||||||
"name": legacyRewardName(reward),
|
"animationUrl": reward.ResourceURL,
|
||||||
"coverUrl": legacyRewardCover(reward),
|
"coverUrl": legacyRewardCover(reward),
|
||||||
|
"name": legacyRewardName(reward),
|
||||||
|
"resourceUrl": reward.ResourceURL,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -243,10 +249,12 @@ func legacyDrawHandler(wheelService *wheel.Service, category string) gin.Handler
|
|||||||
rewards := make([]gin.H, 0, len(resp.Rewards))
|
rewards := make([]gin.H, 0, len(resp.Rewards))
|
||||||
for _, reward := range resp.Rewards {
|
for _, reward := range resp.Rewards {
|
||||||
rewards = append(rewards, gin.H{
|
rewards = append(rewards, gin.H{
|
||||||
"coverUrl": legacyDrawRewardCover(reward),
|
"animationUrl": reward.ResourceURL,
|
||||||
"count": reward.Count,
|
"coverUrl": legacyDrawRewardCover(reward),
|
||||||
"name": legacyDrawRewardName(reward),
|
"count": reward.Count,
|
||||||
"value": reward.Value,
|
"name": legacyDrawRewardName(reward),
|
||||||
|
"resourceUrl": reward.ResourceURL,
|
||||||
|
"value": reward.Value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
legacyOK(c, gin.H{
|
legacyOK(c, gin.H{
|
||||||
@ -276,10 +284,12 @@ func legacyHistoryHandler(wheelService *wheel.Service, category string) gin.Hand
|
|||||||
"drawTimes": record.DrawTimes,
|
"drawTimes": record.DrawTimes,
|
||||||
"rewardValue": legacyRecordValue(record),
|
"rewardValue": legacyRecordValue(record),
|
||||||
"rewards": []gin.H{{
|
"rewards": []gin.H{{
|
||||||
"coverUrl": legacyRecordCover(record),
|
"animationUrl": record.ResourceURL,
|
||||||
"count": 1,
|
"coverUrl": legacyRecordCover(record),
|
||||||
"name": legacyRecordName(record),
|
"count": 1,
|
||||||
"value": legacyRecordValue(record),
|
"name": legacyRecordName(record),
|
||||||
|
"resourceUrl": record.ResourceURL,
|
||||||
|
"value": legacyRecordValue(record),
|
||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
118
internal/router/wheel_routes_test.go
Normal file
118
internal/router/wheel_routes_test.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/repo"
|
||||||
|
wheelservice "chatapp3-golang/internal/service/wheel"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWheelLegacyRewardsExposeDynamicResourceURL(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
db := newRouterWheelDB(t)
|
||||||
|
now := time.Now()
|
||||||
|
resourceID := int64(2045029471274201089)
|
||||||
|
resourceURL := "https://cdn.example.com/reward.svga"
|
||||||
|
coverURL := "https://cdn.example.com/reward.png"
|
||||||
|
|
||||||
|
if err := db.Create(&model.WheelConfig{
|
||||||
|
ID: 1,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Enabled: true,
|
||||||
|
Timezone: "Asia/Shanghai",
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create wheel config: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&model.WheelPoolConfig{
|
||||||
|
ID: 10,
|
||||||
|
ConfigID: 1,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Category: "CLASSIC",
|
||||||
|
Enabled: true,
|
||||||
|
PriceOneGold: 500,
|
||||||
|
Sort: 1,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create wheel pool: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&model.WheelRewardConfig{
|
||||||
|
ID: 100,
|
||||||
|
ConfigID: 1,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Category: "CLASSIC",
|
||||||
|
RewardType: "RESOURCE",
|
||||||
|
ResourceID: &resourceID,
|
||||||
|
ResourceType: "GIFT",
|
||||||
|
ResourceName: "Dynamic Gift",
|
||||||
|
ResourceURL: resourceURL,
|
||||||
|
CoverURL: coverURL,
|
||||||
|
DurationDays: 7,
|
||||||
|
DisplayGoldAmount: 1000,
|
||||||
|
Probability: 1000000,
|
||||||
|
Sort: 1,
|
||||||
|
Enabled: true,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create wheel reward: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
service := wheelservice.NewService(config.Config{}, db, nil)
|
||||||
|
engine := NewRouter(config.Config{}, &repo.Repository{}, appPopupAuthStub{}, Services{Wheel: service})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/client/api/v1/probability/lucky-box/rewards", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d body = %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
if payload["code"] != float64(0) {
|
||||||
|
t.Fatalf("code = %#v body = %s", payload["code"], rec.Body.String())
|
||||||
|
}
|
||||||
|
data, ok := payload["data"].([]any)
|
||||||
|
if !ok || len(data) == 0 {
|
||||||
|
t.Fatalf("data = %#v, want rewards", payload["data"])
|
||||||
|
}
|
||||||
|
item := data[0].(map[string]any)
|
||||||
|
if item["resourceUrl"] != resourceURL || item["animationUrl"] != resourceURL {
|
||||||
|
t.Fatalf("dynamic urls = resource:%#v animation:%#v", item["resourceUrl"], item["animationUrl"])
|
||||||
|
}
|
||||||
|
merchandise := item["merchandise"].(map[string]any)
|
||||||
|
if merchandise["resourceUrl"] != resourceURL || merchandise["animationUrl"] != resourceURL {
|
||||||
|
t.Fatalf("merchandise dynamic urls = resource:%#v animation:%#v", merchandise["resourceUrl"], merchandise["animationUrl"])
|
||||||
|
}
|
||||||
|
if merchandise["coverUrl"] != coverURL {
|
||||||
|
t.Fatalf("coverUrl = %#v, want %q", merchandise["coverUrl"], coverURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRouterWheelDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&model.WheelConfig{}, &model.WheelPoolConfig{}, &model.WheelRewardConfig{}); err != nil {
|
||||||
|
t.Fatalf("migrate wheel tables: %v", err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
@ -272,7 +272,7 @@ func newWheelDrawTestService(t *testing.T) (*Service, *fakeWheelGateway, *gorm.D
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("open sqlite: %v", err)
|
t.Fatalf("open sqlite: %v", err)
|
||||||
}
|
}
|
||||||
if err := db.AutoMigrate(&model.WheelConfig{}, &model.WheelPoolConfig{}, &model.WheelRewardConfig{}, &model.WheelDrawRecord{}, &model.ActivityIdempotency{}); err != nil {
|
if err := db.AutoMigrate(&model.WheelConfig{}, &model.WheelPoolConfig{}, &model.WheelRewardConfig{}, &model.WheelDrawRecord{}, &model.ActivityIdempotency{}, &model.UserBaseInfo{}); err != nil {
|
||||||
t.Fatalf("migrate sqlite: %v", err)
|
t.Fatalf("migrate sqlite: %v", err)
|
||||||
}
|
}
|
||||||
gateway := &fakeWheelGateway{
|
gateway := &fakeWheelGateway{
|
||||||
|
|||||||
@ -2,12 +2,17 @@ package wheel
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"chatapp3-golang/internal/model"
|
"chatapp3-golang/internal/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PageRecords returns admin draw records.
|
// PageRecords returns draw records without admin-only enrichment.
|
||||||
func (s *Service) PageRecords(
|
func (s *Service) PageRecords(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sysOrigin string,
|
sysOrigin string,
|
||||||
@ -60,6 +65,80 @@ func (s *Service) PageRecords(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PageAdminRecords returns admin draw records with user info and range summary.
|
||||||
|
func (s *Service) PageAdminRecords(
|
||||||
|
ctx context.Context,
|
||||||
|
sysOrigin string,
|
||||||
|
category string,
|
||||||
|
userID int64,
|
||||||
|
status string,
|
||||||
|
cursor int,
|
||||||
|
limit int,
|
||||||
|
startTime string,
|
||||||
|
endTime string,
|
||||||
|
) (*RecordPageResponse, error) {
|
||||||
|
cursor, limit = normalizeRecordPage(cursor, limit)
|
||||||
|
filter, err := newWheelAdminRecordFilter(sysOrigin, category, userID, status, startTime, endTime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if filter.SysOrigin == "" {
|
||||||
|
return nil, NewAppError(400, "bad_request", "sysOrigin is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := s.adminRecordQuery(ctx, filter)
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []model.WheelDrawRecord
|
||||||
|
if err := s.adminRecordQuery(ctx, filter).
|
||||||
|
Order("create_time desc, id desc").
|
||||||
|
Offset((cursor - 1) * limit).
|
||||||
|
Limit(limit).
|
||||||
|
Find(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, err := s.adminRecordSummary(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userInfos, err := s.adminRecordUserInfos(ctx, rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userDrawTimes, err := s.adminRecordUserDrawTimes(ctx, filter, rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
records := make([]DrawRecordPayload, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
payload := drawRecordPayload(row)
|
||||||
|
if user, ok := userInfos[row.UserID]; ok {
|
||||||
|
payload.UserShortID = user.Account
|
||||||
|
payload.UserAvatar = user.UserAvatar
|
||||||
|
payload.UserNickname = user.UserNickname
|
||||||
|
}
|
||||||
|
payload.UserTotalDrawTimes = userDrawTimes[row.UserID]
|
||||||
|
records = append(records, payload)
|
||||||
|
}
|
||||||
|
return &RecordPageResponse{
|
||||||
|
Records: records,
|
||||||
|
Total: total,
|
||||||
|
Current: cursor,
|
||||||
|
Size: limit,
|
||||||
|
TotalPaidGold: summary.TotalPaidGold,
|
||||||
|
TotalRewardGold: summary.TotalRewardGold,
|
||||||
|
TotalDrawTimes: summary.TotalDrawTimes,
|
||||||
|
ParticipantCount: summary.ParticipantCount,
|
||||||
|
StartTime: formatDateTime(filter.StartTime),
|
||||||
|
EndTime: formatDateTime(filter.EndTime),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// PageUserHistory returns app draw history for the authenticated user.
|
// PageUserHistory returns app draw history for the authenticated user.
|
||||||
func (s *Service) PageUserHistory(ctx context.Context, user AuthUser, cursor int, limit int) (*RecordPageResponse, error) {
|
func (s *Service) PageUserHistory(ctx context.Context, user AuthUser, cursor int, limit int) (*RecordPageResponse, error) {
|
||||||
return s.PageRecords(ctx, user.SysOrigin, "", user.UserID, drawStatusSuccess, cursor, limit)
|
return s.PageRecords(ctx, user.SysOrigin, "", user.UserID, drawStatusSuccess, cursor, limit)
|
||||||
@ -160,3 +239,205 @@ func displayValueForRecord(row model.WheelDrawRecord) int64 {
|
|||||||
}
|
}
|
||||||
return row.DisplayGoldAmount
|
return row.DisplayGoldAmount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type wheelAdminRecordFilter struct {
|
||||||
|
SysOrigin string
|
||||||
|
Category string
|
||||||
|
UserID int64
|
||||||
|
Status string
|
||||||
|
StartTime time.Time
|
||||||
|
EndTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type wheelAdminRecordSummary struct {
|
||||||
|
TotalPaidGold int64
|
||||||
|
TotalRewardGold int64
|
||||||
|
TotalDrawTimes int64
|
||||||
|
ParticipantCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWheelAdminRecordFilter(sysOrigin string, category string, userID int64, status string, startRaw string, endRaw string) (wheelAdminRecordFilter, error) {
|
||||||
|
start, end, err := parseWheelAdminRecordTimeRange(startRaw, endRaw)
|
||||||
|
if err != nil {
|
||||||
|
return wheelAdminRecordFilter{}, err
|
||||||
|
}
|
||||||
|
return wheelAdminRecordFilter{
|
||||||
|
SysOrigin: normalizeSysOrigin(sysOrigin),
|
||||||
|
Category: strings.ToUpper(strings.TrimSpace(category)),
|
||||||
|
UserID: userID,
|
||||||
|
Status: strings.ToUpper(strings.TrimSpace(status)),
|
||||||
|
StartTime: start,
|
||||||
|
EndTime: end,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseWheelAdminRecordTimeRange(startRaw string, endRaw string) (time.Time, time.Time, error) {
|
||||||
|
startRaw = strings.TrimSpace(startRaw)
|
||||||
|
endRaw = strings.TrimSpace(endRaw)
|
||||||
|
now := time.Now()
|
||||||
|
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||||
|
if startRaw == "" && endRaw == "" {
|
||||||
|
return todayStart, todayStart.Add(24*time.Hour - time.Nanosecond), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var start time.Time
|
||||||
|
var end time.Time
|
||||||
|
var err error
|
||||||
|
if startRaw != "" {
|
||||||
|
start, err = parseWheelAdminRecordTime(startRaw, false)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, time.Time{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if endRaw != "" {
|
||||||
|
end, err = parseWheelAdminRecordTime(endRaw, true)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, time.Time{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if startRaw == "" {
|
||||||
|
start = time.Date(end.Year(), end.Month(), end.Day(), 0, 0, 0, 0, end.Location())
|
||||||
|
}
|
||||||
|
if endRaw == "" {
|
||||||
|
end = time.Date(start.Year(), start.Month(), start.Day(), 23, 59, 59, int(time.Second-time.Nanosecond), start.Location())
|
||||||
|
}
|
||||||
|
if end.Before(start) {
|
||||||
|
return time.Time{}, time.Time{}, NewAppError(http.StatusBadRequest, "bad_time_range", "endTime must be greater than startTime")
|
||||||
|
}
|
||||||
|
return start, end, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseWheelAdminRecordTime(raw string, endOfDay bool) (time.Time, error) {
|
||||||
|
for _, layout := range []string{
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
time.RFC3339,
|
||||||
|
"2006-01-02T15:04:05.000Z07:00",
|
||||||
|
"2006-01-02",
|
||||||
|
} {
|
||||||
|
var parsed time.Time
|
||||||
|
var err error
|
||||||
|
if strings.Contains(layout, "Z07:00") {
|
||||||
|
parsed, err = time.Parse(layout, raw)
|
||||||
|
} else {
|
||||||
|
parsed, err = time.ParseInLocation(layout, raw, time.Local)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
if layout == "2006-01-02" && endOfDay {
|
||||||
|
parsed = parsed.Add(24*time.Hour - time.Nanosecond)
|
||||||
|
}
|
||||||
|
return parsed, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}, NewAppError(http.StatusBadRequest, "bad_time", fmt.Sprintf("invalid time: %s", raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) adminRecordQuery(ctx context.Context, filter wheelAdminRecordFilter) *gorm.DB {
|
||||||
|
query := s.db.WithContext(ctx).Model(&model.WheelDrawRecord{}).
|
||||||
|
Where("sys_origin = ? AND create_time >= ? AND create_time <= ?", filter.SysOrigin, filter.StartTime, filter.EndTime)
|
||||||
|
if strings.TrimSpace(filter.Category) != "" {
|
||||||
|
query = query.Where("category = ?", normalizeCategory(filter.Category))
|
||||||
|
}
|
||||||
|
if filter.UserID > 0 {
|
||||||
|
query = query.Where("user_id = ?", filter.UserID)
|
||||||
|
}
|
||||||
|
if filter.Status != "" {
|
||||||
|
query = query.Where("status = ?", filter.Status)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) adminRecordSummary(ctx context.Context, filter wheelAdminRecordFilter) (wheelAdminRecordSummary, error) {
|
||||||
|
var summary wheelAdminRecordSummary
|
||||||
|
if err := s.adminRecordQuery(ctx, filter).
|
||||||
|
Select("COALESCE(SUM(CASE WHEN status = ? THEN CASE WHEN reward_type = ? THEN gold_amount ELSE display_gold_amount END ELSE 0 END), 0)", drawStatusSuccess, rewardTypeGold).
|
||||||
|
Scan(&summary.TotalRewardGold).Error; err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
if err := s.adminRecordQuery(ctx, filter).
|
||||||
|
Select("COUNT(DISTINCT user_id)").
|
||||||
|
Scan(&summary.ParticipantCount).Error; err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
|
||||||
|
drawRows := s.adminRecordQuery(ctx, filter).
|
||||||
|
Select("draw_no, MAX(paid_gold) AS paid_gold, MAX(draw_times) AS draw_times").
|
||||||
|
Group("draw_no")
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Table("(?) AS draw_rows", drawRows).
|
||||||
|
Select("COALESCE(SUM(paid_gold), 0), COALESCE(SUM(draw_times), 0)").
|
||||||
|
Row().
|
||||||
|
Scan(&summary.TotalPaidGold, &summary.TotalDrawTimes); err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
return summary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) adminRecordUserInfos(ctx context.Context, rows []model.WheelDrawRecord) (map[int64]model.UserBaseInfo, error) {
|
||||||
|
ids := make([]int64, 0, len(rows))
|
||||||
|
seen := make(map[int64]struct{}, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
if row.UserID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[row.UserID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[row.UserID] = struct{}{}
|
||||||
|
ids = append(ids, row.UserID)
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return map[int64]model.UserBaseInfo{}, nil
|
||||||
|
}
|
||||||
|
var users []model.UserBaseInfo
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Where("id IN ?", ids).
|
||||||
|
Find(&users).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make(map[int64]model.UserBaseInfo, len(users))
|
||||||
|
for _, user := range users {
|
||||||
|
result[user.ID] = user
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) adminRecordUserDrawTimes(ctx context.Context, filter wheelAdminRecordFilter, pageRows []model.WheelDrawRecord) (map[int64]int64, error) {
|
||||||
|
ids := make([]int64, 0, len(pageRows))
|
||||||
|
seen := make(map[int64]struct{}, len(pageRows))
|
||||||
|
for _, row := range pageRows {
|
||||||
|
if row.UserID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[row.UserID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[row.UserID] = struct{}{}
|
||||||
|
ids = append(ids, row.UserID)
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return map[int64]int64{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
drawRows := s.adminRecordQuery(ctx, filter).
|
||||||
|
Where("user_id IN ?", ids).
|
||||||
|
Select("user_id, draw_no, MAX(draw_times) AS draw_times").
|
||||||
|
Group("user_id, draw_no")
|
||||||
|
|
||||||
|
type userDrawTotal struct {
|
||||||
|
UserID int64
|
||||||
|
TotalDrawTimes int64
|
||||||
|
}
|
||||||
|
var rows []userDrawTotal
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Table("(?) AS user_draw_rows", drawRows).
|
||||||
|
Select("user_id, COALESCE(SUM(draw_times), 0) AS total_draw_times").
|
||||||
|
Group("user_id").
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make(map[int64]int64, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
result[row.UserID] = row.TotalDrawTimes
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|||||||
185
internal/service/wheel/records_test.go
Normal file
185
internal/service/wheel/records_test.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package wheel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPageAdminRecordsIncludesUserInfoAndRangeSummary(t *testing.T) {
|
||||||
|
service, _, db := newWheelDrawTestService(t)
|
||||||
|
now := time.Now()
|
||||||
|
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||||
|
recordTime := todayStart.Add(time.Hour)
|
||||||
|
oldRecordTime := todayStart.Add(-time.Hour)
|
||||||
|
giftID := int64(9001)
|
||||||
|
|
||||||
|
if err := db.Create([]model.UserBaseInfo{
|
||||||
|
{
|
||||||
|
ID: 1001,
|
||||||
|
Account: "short1001",
|
||||||
|
UserAvatar: "https://example.com/a.png",
|
||||||
|
UserNickname: "Alice",
|
||||||
|
OriginSys: "LIKEI",
|
||||||
|
CreateTime: recordTime,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 1002,
|
||||||
|
Account: "short1002",
|
||||||
|
UserAvatar: "https://example.com/b.png",
|
||||||
|
UserNickname: "Bob",
|
||||||
|
OriginSys: "LIKEI",
|
||||||
|
CreateTime: recordTime,
|
||||||
|
},
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed users: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := []model.WheelDrawRecord{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
DrawNo: "WHEEL-1001-1",
|
||||||
|
EventID: "event-1",
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Category: categoryClassic,
|
||||||
|
UserID: 1001,
|
||||||
|
DrawTimes: 10,
|
||||||
|
PaidGold: 1000,
|
||||||
|
RewardType: rewardTypeResource,
|
||||||
|
ResourceID: &giftID,
|
||||||
|
ResourceType: resourceTypeGift,
|
||||||
|
ResourceName: "Gift A",
|
||||||
|
DisplayGoldAmount: 50,
|
||||||
|
Probability: 500000,
|
||||||
|
Status: drawStatusSuccess,
|
||||||
|
CreateTime: recordTime,
|
||||||
|
UpdateTime: recordTime,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
DrawNo: "WHEEL-1001-1",
|
||||||
|
EventID: "event-2",
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Category: categoryClassic,
|
||||||
|
UserID: 1001,
|
||||||
|
DrawTimes: 10,
|
||||||
|
PaidGold: 1000,
|
||||||
|
RewardType: rewardTypeGold,
|
||||||
|
GoldAmount: 70,
|
||||||
|
DisplayGoldAmount: 999,
|
||||||
|
Probability: 500000,
|
||||||
|
Status: drawStatusSuccess,
|
||||||
|
CreateTime: recordTime.Add(time.Minute),
|
||||||
|
UpdateTime: recordTime.Add(time.Minute),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 3,
|
||||||
|
DrawNo: "WHEEL-1002-1",
|
||||||
|
EventID: "event-3",
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Category: categoryClassic,
|
||||||
|
UserID: 1002,
|
||||||
|
DrawTimes: 1,
|
||||||
|
PaidGold: 100,
|
||||||
|
RewardType: rewardTypeResource,
|
||||||
|
ResourceID: &giftID,
|
||||||
|
ResourceType: resourceTypeGift,
|
||||||
|
ResourceName: "Gift Failed",
|
||||||
|
DisplayGoldAmount: 999,
|
||||||
|
Probability: 500000,
|
||||||
|
Status: drawStatusFailed,
|
||||||
|
CreateTime: recordTime.Add(2 * time.Minute),
|
||||||
|
UpdateTime: recordTime.Add(2 * time.Minute),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 4,
|
||||||
|
DrawNo: "WHEEL-1002-2",
|
||||||
|
EventID: "event-4",
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Category: categoryClassic,
|
||||||
|
UserID: 1002,
|
||||||
|
DrawTimes: 1,
|
||||||
|
PaidGold: 200,
|
||||||
|
RewardType: rewardTypeResource,
|
||||||
|
ResourceID: &giftID,
|
||||||
|
ResourceType: resourceTypeGift,
|
||||||
|
ResourceName: "Gift B",
|
||||||
|
DisplayGoldAmount: 30,
|
||||||
|
Probability: 500000,
|
||||||
|
Status: drawStatusSuccess,
|
||||||
|
CreateTime: recordTime.Add(3 * time.Minute),
|
||||||
|
UpdateTime: recordTime.Add(3 * time.Minute),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 5,
|
||||||
|
DrawNo: "WHEEL-OLD",
|
||||||
|
EventID: "event-old",
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Category: categoryClassic,
|
||||||
|
UserID: 1001,
|
||||||
|
DrawTimes: 50,
|
||||||
|
PaidGold: 5000,
|
||||||
|
RewardType: rewardTypeResource,
|
||||||
|
ResourceID: &giftID,
|
||||||
|
ResourceType: resourceTypeGift,
|
||||||
|
ResourceName: "Old Gift",
|
||||||
|
DisplayGoldAmount: 500,
|
||||||
|
Probability: 500000,
|
||||||
|
Status: drawStatusSuccess,
|
||||||
|
CreateTime: oldRecordTime,
|
||||||
|
UpdateTime: oldRecordTime,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := db.Create(&rows).Error; err != nil {
|
||||||
|
t.Fatalf("seed records: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := service.PageAdminRecords(context.Background(), "LIKEI", "", 0, "", 1, 20, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PageAdminRecords() error = %v", err)
|
||||||
|
}
|
||||||
|
if resp.Total != 4 {
|
||||||
|
t.Fatalf("total = %d, want 4", resp.Total)
|
||||||
|
}
|
||||||
|
if resp.TotalPaidGold != 1300 {
|
||||||
|
t.Fatalf("total paid = %d, want 1300", resp.TotalPaidGold)
|
||||||
|
}
|
||||||
|
if resp.TotalRewardGold != 150 {
|
||||||
|
t.Fatalf("total reward = %d, want 150", resp.TotalRewardGold)
|
||||||
|
}
|
||||||
|
if resp.TotalDrawTimes != 12 {
|
||||||
|
t.Fatalf("total draw times = %d, want 12", resp.TotalDrawTimes)
|
||||||
|
}
|
||||||
|
if resp.ParticipantCount != 2 {
|
||||||
|
t.Fatalf("participant count = %d, want 2", resp.ParticipantCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
var user1001 *DrawRecordPayload
|
||||||
|
var user1002 *DrawRecordPayload
|
||||||
|
for index := range resp.Records {
|
||||||
|
record := &resp.Records[index]
|
||||||
|
switch record.UserID {
|
||||||
|
case 1001:
|
||||||
|
user1001 = record
|
||||||
|
case 1002:
|
||||||
|
user1002 = record
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if user1001 == nil {
|
||||||
|
t.Fatal("missing user 1001 record")
|
||||||
|
}
|
||||||
|
if user1001.UserShortID != "short1001" || user1001.UserAvatar == "" || user1001.UserNickname != "Alice" {
|
||||||
|
t.Fatalf("user 1001 info = %+v", user1001)
|
||||||
|
}
|
||||||
|
if user1001.UserTotalDrawTimes != 10 {
|
||||||
|
t.Fatalf("user 1001 total draw times = %d, want 10", user1001.UserTotalDrawTimes)
|
||||||
|
}
|
||||||
|
if user1002 == nil {
|
||||||
|
t.Fatal("missing user 1002 record")
|
||||||
|
}
|
||||||
|
if user1002.UserTotalDrawTimes != 2 {
|
||||||
|
t.Fatalf("user 1002 total draw times = %d, want 2", user1002.UserTotalDrawTimes)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -274,39 +274,49 @@ type DrawRewardPayload struct {
|
|||||||
|
|
||||||
// DrawRecordPayload is one draw record row.
|
// DrawRecordPayload is one draw record row.
|
||||||
type DrawRecordPayload struct {
|
type DrawRecordPayload struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
DrawNo string `json:"drawNo"`
|
DrawNo string `json:"drawNo"`
|
||||||
EventID string `json:"eventId,omitempty"`
|
EventID string `json:"eventId,omitempty"`
|
||||||
UserID int64 `json:"userId"`
|
UserID int64 `json:"userId"`
|
||||||
SysOrigin string `json:"sysOrigin"`
|
UserShortID string `json:"userShortId,omitempty"`
|
||||||
Category string `json:"category"`
|
UserAvatar string `json:"userAvatar,omitempty"`
|
||||||
DrawTimes int `json:"drawTimes"`
|
UserNickname string `json:"userNickname,omitempty"`
|
||||||
PaidGold int64 `json:"paidGold"`
|
UserTotalDrawTimes int64 `json:"userTotalDrawTimes,omitempty"`
|
||||||
RewardConfigID int64 `json:"rewardConfigId,omitempty"`
|
SysOrigin string `json:"sysOrigin"`
|
||||||
RewardType string `json:"rewardType"`
|
Category string `json:"category"`
|
||||||
ResourceID ResourceID `json:"resourceId,omitempty"`
|
DrawTimes int `json:"drawTimes"`
|
||||||
ResourceType string `json:"resourceType,omitempty"`
|
PaidGold int64 `json:"paidGold"`
|
||||||
ResourceName string `json:"resourceName,omitempty"`
|
RewardConfigID int64 `json:"rewardConfigId,omitempty"`
|
||||||
ResourceURL string `json:"resourceUrl,omitempty"`
|
RewardType string `json:"rewardType"`
|
||||||
CoverURL string `json:"coverUrl,omitempty"`
|
ResourceID ResourceID `json:"resourceId,omitempty"`
|
||||||
DurationDays int `json:"durationDays,omitempty"`
|
ResourceType string `json:"resourceType,omitempty"`
|
||||||
DisplayGoldAmount int64 `json:"displayGoldAmount,omitempty"`
|
ResourceName string `json:"resourceName,omitempty"`
|
||||||
GoldAmount int64 `json:"goldAmount,omitempty"`
|
ResourceURL string `json:"resourceUrl,omitempty"`
|
||||||
Probability int `json:"probability"`
|
CoverURL string `json:"coverUrl,omitempty"`
|
||||||
Status string `json:"status"`
|
DurationDays int `json:"durationDays,omitempty"`
|
||||||
RetryCount int `json:"retryCount,omitempty"`
|
DisplayGoldAmount int64 `json:"displayGoldAmount,omitempty"`
|
||||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
GoldAmount int64 `json:"goldAmount,omitempty"`
|
||||||
GrantTime string `json:"grantTime,omitempty"`
|
Probability int `json:"probability"`
|
||||||
CreateTime string `json:"createTime,omitempty"`
|
Status string `json:"status"`
|
||||||
UpdateTime string `json:"updateTime,omitempty"`
|
RetryCount int `json:"retryCount,omitempty"`
|
||||||
|
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||||
|
GrantTime string `json:"grantTime,omitempty"`
|
||||||
|
CreateTime string `json:"createTime,omitempty"`
|
||||||
|
UpdateTime string `json:"updateTime,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordPageResponse is an admin record page.
|
// RecordPageResponse is an admin record page.
|
||||||
type RecordPageResponse struct {
|
type RecordPageResponse struct {
|
||||||
Records []DrawRecordPayload `json:"records"`
|
Records []DrawRecordPayload `json:"records"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
Current int `json:"current"`
|
Current int `json:"current"`
|
||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
|
TotalPaidGold int64 `json:"totalPaidGold,omitempty"`
|
||||||
|
TotalRewardGold int64 `json:"totalRewardGold,omitempty"`
|
||||||
|
TotalDrawTimes int64 `json:"totalDrawTimes,omitempty"`
|
||||||
|
ParticipantCount int64 `json:"participantCount,omitempty"`
|
||||||
|
StartTime string `json:"startTime,omitempty"`
|
||||||
|
EndTime string `json:"endTime,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeSysOrigin(sysOrigin string) string {
|
func normalizeSysOrigin(sysOrigin string) string {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user