415 lines
14 KiB
Go
415 lines
14 KiB
Go
package weekstar
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"math"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"chatapp3-golang/internal/model"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// GetPublicPage 返回 H5 聚合页数据,优先读取当前生效配置,其次回退到最近未来周配置。
|
|
func (s *WeekStarService) GetPublicPage(ctx context.Context, sysOrigin string) (*WeekStarPageResponse, error) {
|
|
sysOrigin = s.normalizeSysOrigin(sysOrigin)
|
|
now := time.Now().In(s.location)
|
|
|
|
activeConfig, giftRows, rewardRows, err := s.loadActiveConfigBundle(ctx, sysOrigin, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
activityStatus := weekStarActivityStatusDisabled
|
|
displayConfig := activeConfig
|
|
if displayConfig == nil {
|
|
displayConfig, giftRows, rewardRows, err = s.loadUpcomingConfigBundle(ctx, sysOrigin, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if displayConfig == nil {
|
|
return &WeekStarPageResponse{
|
|
ActivityStatus: weekStarActivityStatusDisabled,
|
|
QualifyingGifts: []WeekStarGiftConfigPayload{},
|
|
LastWeekTop3: []WeekStarRankingUserView{},
|
|
ThisWeekTop10: []WeekStarRankingUserView{},
|
|
RewardPreview: []WeekStarRewardConfigPayload{},
|
|
CurrentPeriodText: "",
|
|
LastPeriodText: "",
|
|
CountdownMillis: 0,
|
|
}, nil
|
|
}
|
|
activityStatus = weekStarActivityStatusNotStarted
|
|
} else {
|
|
activityStatus = weekStarActivityStatusOngoing
|
|
}
|
|
|
|
// 页面优先围绕当前生效周期展示;未开始时改为围绕 startAt 推导周期。
|
|
referenceTime := now
|
|
if activityStatus == weekStarActivityStatusNotStarted {
|
|
referenceTime = s.fromStorageWallClock(displayConfig.StartAt)
|
|
}
|
|
cycleStart, cycleEnd := s.cycleBounds(referenceTime)
|
|
currentPeriodStart := maxTime(cycleStart, s.fromStorageWallClock(displayConfig.StartAt))
|
|
currentPeriodEnd := minTime(cycleEnd, s.fromStorageWallClock(displayConfig.EndAt))
|
|
|
|
// 历史前三优先走最近一次已结算快照,避免受实时榜单波动影响。
|
|
lastAnchor, err := s.loadLatestSettledCycle(ctx, sysOrigin, currentPeriodStart)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
lastWeekTop3 := []WeekStarRankingUserView{}
|
|
lastPeriodText := ""
|
|
if lastAnchor != nil {
|
|
lastWeekTop3, err = s.listSnapshotRanking(ctx, lastAnchor.ConfigID, lastAnchor.CycleKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
lastPeriodText = formatPeriodText(
|
|
s.fromStorageWallClock(lastAnchor.PeriodStartAt),
|
|
s.fromStorageWallClock(lastAnchor.PeriodEndAt),
|
|
)
|
|
}
|
|
|
|
// 本周榜只在活动进行中展示实时榜。
|
|
thisWeekTop10 := []WeekStarRankingUserView{}
|
|
if activityStatus == weekStarActivityStatusOngoing {
|
|
thisWeekTop10, err = s.loadLiveRanking(ctx, displayConfig.ID, s.cycleKey(cycleStart), 10)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
rewardPreview, err := s.buildRewardPreview(ctx, rewardRows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
countdownTarget := currentPeriodEnd
|
|
if activityStatus == weekStarActivityStatusNotStarted {
|
|
countdownTarget = s.fromStorageWallClock(displayConfig.StartAt)
|
|
}
|
|
countdownMillis := maxInt64(0, countdownTarget.UnixMilli()-now.UnixMilli())
|
|
|
|
return &WeekStarPageResponse{
|
|
ActivityStatus: activityStatus,
|
|
CurrentPeriodText: formatPeriodText(currentPeriodStart, currentPeriodEnd),
|
|
LastPeriodText: lastPeriodText,
|
|
CountdownMillis: countdownMillis,
|
|
QualifyingGifts: buildGiftPayloads(giftRows),
|
|
LastWeekTop3: lastWeekTop3,
|
|
ThisWeekTop10: thisWeekTop10,
|
|
RewardPreview: rewardPreview,
|
|
}, nil
|
|
}
|
|
|
|
// CurrentRanking 返回后台查看用的当前实时榜,实时榜只在当前有生效配置时读取。
|
|
func (s *WeekStarService) CurrentRanking(ctx context.Context, sysOrigin string, limit int) (*WeekStarCurrentRankResponse, error) {
|
|
sysOrigin = s.normalizeSysOrigin(sysOrigin)
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
|
|
now := time.Now().In(s.location)
|
|
configRow, giftRows, _, err := s.loadActiveConfigBundle(ctx, sysOrigin, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if configRow == nil {
|
|
return &WeekStarCurrentRankResponse{
|
|
SysOrigin: sysOrigin,
|
|
ActivityStatus: weekStarActivityStatusDisabled,
|
|
Timezone: s.cfg.WeekStar.Timezone,
|
|
GiftConfigs: []WeekStarGiftConfigPayload{},
|
|
Records: []WeekStarRankingUserView{},
|
|
}, nil
|
|
}
|
|
|
|
cycleStart, cycleEnd := s.cycleBounds(now)
|
|
periodStart := maxTime(cycleStart, s.fromStorageWallClock(configRow.StartAt))
|
|
periodEnd := minTime(cycleEnd, s.fromStorageWallClock(configRow.EndAt))
|
|
records, err := s.loadLiveRanking(ctx, configRow.ID, s.cycleKey(cycleStart), int64(limit))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &WeekStarCurrentRankResponse{
|
|
ConfigID: configRow.ID,
|
|
SysOrigin: sysOrigin,
|
|
ActivityStatus: weekStarActivityStatusOngoing,
|
|
CycleKey: s.cycleKey(cycleStart),
|
|
PeriodStartAt: formatDateTime(periodStart),
|
|
PeriodEndAt: formatDateTime(periodEnd),
|
|
Timezone: configRow.Timezone,
|
|
GiftConfigs: buildGiftPayloads(giftRows),
|
|
Records: records,
|
|
}, nil
|
|
}
|
|
|
|
// PageSnapshots 按系统或配置分页查询历史快照,支持跨多周活动查看冻结榜单。
|
|
func (s *WeekStarService) PageSnapshots(ctx context.Context, sysOrigin string, configID int64, cycleKey string, cursor, limit int) (map[string]any, error) {
|
|
page, size := normalizePage(cursor, limit)
|
|
query := s.repo.DB.WithContext(ctx).
|
|
Table("week_star_rank_snapshot AS snapshot").
|
|
Joins("JOIN week_star_activity_config AS config ON config.id = snapshot.config_id")
|
|
|
|
if configID > 0 {
|
|
query = query.Where("snapshot.config_id = ?", configID)
|
|
} else {
|
|
query = query.Where("config.sys_origin = ?", s.normalizeSysOrigin(sysOrigin))
|
|
}
|
|
if strings.TrimSpace(cycleKey) != "" {
|
|
query = query.Where("snapshot.cycle_key = ?", strings.TrimSpace(cycleKey))
|
|
}
|
|
|
|
var total int64
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rows []model.WeekStarRankSnapshot
|
|
if total > 0 {
|
|
if err := query.
|
|
Select("snapshot.*").
|
|
Order("snapshot.period_end_at desc").
|
|
Order("snapshot.rank asc").
|
|
Offset((page - 1) * size).
|
|
Limit(size).
|
|
Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
records := make([]WeekStarSnapshotView, 0, len(rows))
|
|
for _, row := range rows {
|
|
records = append(records, WeekStarSnapshotView{
|
|
ID: row.ID,
|
|
ConfigID: row.ConfigID,
|
|
CycleKey: row.CycleKey,
|
|
PeriodStartAt: formatDateTime(s.fromStorageWallClock(row.PeriodStartAt)),
|
|
PeriodEndAt: formatDateTime(s.fromStorageWallClock(row.PeriodEndAt)),
|
|
UserID: row.UserID,
|
|
UserAvatar: row.UserAvatar,
|
|
UserNickname: row.UserNickname,
|
|
Account: row.Account,
|
|
CountryCode: row.CountryCode,
|
|
CountryName: row.CountryName,
|
|
Rank: row.Rank,
|
|
ScoreGold: row.ScoreGold,
|
|
})
|
|
}
|
|
return map[string]any{"records": records, "total": total}, nil
|
|
}
|
|
|
|
// PageRewardRecords 按系统或配置分页查询发奖记录,支持失败补发前的筛选和定位。
|
|
func (s *WeekStarService) PageRewardRecords(ctx context.Context, sysOrigin string, configID int64, cycleKey, status string, cursor, limit int) (map[string]any, error) {
|
|
page, size := normalizePage(cursor, limit)
|
|
query := s.repo.DB.WithContext(ctx).
|
|
Table("week_star_reward_record AS record").
|
|
Joins("JOIN week_star_activity_config AS config ON config.id = record.config_id")
|
|
|
|
if configID > 0 {
|
|
query = query.Where("record.config_id = ?", configID)
|
|
} else {
|
|
query = query.Where("config.sys_origin = ?", s.normalizeSysOrigin(sysOrigin))
|
|
}
|
|
if strings.TrimSpace(cycleKey) != "" {
|
|
query = query.Where("record.cycle_key = ?", strings.TrimSpace(cycleKey))
|
|
}
|
|
if strings.TrimSpace(status) != "" {
|
|
query = query.Where("record.status = ?", strings.ToUpper(strings.TrimSpace(status)))
|
|
}
|
|
|
|
var total int64
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rows []model.WeekStarRewardRecord
|
|
if total > 0 {
|
|
if err := query.
|
|
Select("record.*").
|
|
Order("record.create_time desc").
|
|
Order("record.rank asc").
|
|
Offset((page - 1) * size).
|
|
Limit(size).
|
|
Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
records := make([]WeekStarRewardRecordView, 0, len(rows))
|
|
for _, row := range rows {
|
|
records = append(records, toWeekStarRewardRecordView(row))
|
|
}
|
|
return map[string]any{"records": records, "total": total}, nil
|
|
}
|
|
|
|
// RetryRewardRecord 对失败的发奖记录提交一次后台补发任务。
|
|
func (s *WeekStarService) RetryRewardRecord(ctx context.Context, id int64) (*WeekStarRewardRecordView, error) {
|
|
if id <= 0 {
|
|
return nil, NewAppError(http.StatusBadRequest, "invalid_id", "id is required")
|
|
}
|
|
var record model.WeekStarRewardRecord
|
|
err := s.repo.DB.WithContext(ctx).Where("id = ?", id).First(&record).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, NewAppError(http.StatusNotFound, "reward_record_not_found", "reward record not found")
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if record.Status != weekStarRewardStatusFailed {
|
|
return nil, NewAppError(http.StatusBadRequest, "reward_record_not_retryable", "only failed records can be retried")
|
|
}
|
|
if err := s.enqueueRewardRetryTask(ctx, record.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
result := toWeekStarRewardRecordView(record)
|
|
return &result, nil
|
|
}
|
|
|
|
// listSnapshotRanking 读取某个配置某个周期已经冻结的榜单快照。
|
|
func (s *WeekStarService) listSnapshotRanking(ctx context.Context, configID int64, cycleKey string) ([]WeekStarRankingUserView, error) {
|
|
if strings.TrimSpace(cycleKey) == "" {
|
|
return []WeekStarRankingUserView{}, nil
|
|
}
|
|
var rows []model.WeekStarRankSnapshot
|
|
if err := s.repo.DB.WithContext(ctx).
|
|
Where("config_id = ? AND cycle_key = ?", configID, cycleKey).
|
|
Order("`rank` asc").
|
|
Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]WeekStarRankingUserView, 0, len(rows))
|
|
for _, row := range rows {
|
|
result = append(result, WeekStarRankingUserView{
|
|
UserID: row.UserID,
|
|
UserAvatar: row.UserAvatar,
|
|
UserNickname: row.UserNickname,
|
|
Account: row.Account,
|
|
CountryCode: row.CountryCode,
|
|
CountryName: row.CountryName,
|
|
Rank: row.Rank,
|
|
ScoreGold: row.ScoreGold,
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// loadLiveRanking 从 Redis 实时榜读取前 N 名,并补齐用户资料用于 H5 和结算展示。
|
|
func (s *WeekStarService) loadLiveRanking(ctx context.Context, configID int64, cycleKey string, limit int64) ([]WeekStarRankingUserView, error) {
|
|
if limit <= 0 {
|
|
return []WeekStarRankingUserView{}, nil
|
|
}
|
|
zs, err := s.repo.Redis.ZRevRangeWithScores(ctx, s.rankRedisKey(configID, cycleKey), 0, limit-1).Result()
|
|
if err != nil && !errors.Is(err, redis.Nil) {
|
|
return nil, err
|
|
}
|
|
if len(zs) == 0 {
|
|
return []WeekStarRankingUserView{}, nil
|
|
}
|
|
|
|
userIDs := make([]int64, 0, len(zs))
|
|
rankScores := make([]weekStarRankScore, 0, len(zs))
|
|
for index, item := range zs {
|
|
userID, ok := parseRedisMemberInt64(item.Member)
|
|
if !ok || userID <= 0 {
|
|
continue
|
|
}
|
|
userIDs = append(userIDs, userID)
|
|
rankScores = append(rankScores, weekStarRankScore{
|
|
UserID: userID,
|
|
ScoreGold: int64(math.Round(item.Score)),
|
|
Rank: index + 1,
|
|
})
|
|
}
|
|
profileMap, err := s.lookupUserProfiles(ctx, userIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]WeekStarRankingUserView, 0, len(rankScores))
|
|
for _, item := range rankScores {
|
|
profile := profileMap[item.UserID]
|
|
result = append(result, WeekStarRankingUserView{
|
|
UserID: item.UserID,
|
|
UserAvatar: profile.UserAvatar,
|
|
UserNickname: profile.UserNickname,
|
|
Account: profile.Account,
|
|
CountryCode: profile.CountryCode,
|
|
CountryName: profile.CountryName,
|
|
Rank: item.Rank,
|
|
ScoreGold: item.ScoreGold,
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// lookupUserProfiles 优先查本地资料,查不到时回源 Java 用户资料接口兜底。
|
|
func (s *WeekStarService) lookupUserProfiles(ctx context.Context, userIDs []int64) (map[int64]weekStarUserProfile, error) {
|
|
result := make(map[int64]weekStarUserProfile, len(userIDs))
|
|
if len(userIDs) == 0 {
|
|
return result, nil
|
|
}
|
|
var rows []model.UserBaseInfo
|
|
if err := s.repo.DB.WithContext(ctx).Where("id IN ?", userIDs).Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
for _, row := range rows {
|
|
result[row.ID] = weekStarUserProfile{
|
|
UserID: row.ID,
|
|
Account: row.Account,
|
|
UserAvatar: row.UserAvatar,
|
|
UserNickname: row.UserNickname,
|
|
CountryCode: row.CountryCode,
|
|
CountryName: row.CountryName,
|
|
}
|
|
}
|
|
for _, userID := range userIDs {
|
|
if _, exists := result[userID]; exists {
|
|
continue
|
|
}
|
|
profile, err := s.java.GetUserProfile(ctx, userID)
|
|
if err != nil {
|
|
result[userID] = weekStarUserProfile{UserID: userID}
|
|
continue
|
|
}
|
|
result[userID] = weekStarUserProfile{
|
|
UserID: userID,
|
|
UserAvatar: profile.UserAvatar,
|
|
UserNickname: profile.UserNickname,
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// loadLatestSettledCycle 读取当前展示配置之前最近一次已结算周期,用于展示历史前三。
|
|
func (s *WeekStarService) loadLatestSettledCycle(ctx context.Context, sysOrigin string, before time.Time) (*weekStarSettledCycleAnchor, error) {
|
|
sysOrigin = s.normalizeSysOrigin(sysOrigin)
|
|
var anchor weekStarSettledCycleAnchor
|
|
result := s.repo.DB.WithContext(ctx).
|
|
Table("week_star_rank_snapshot AS snapshot").
|
|
Select("snapshot.config_id, snapshot.cycle_key, snapshot.period_start_at, snapshot.period_end_at").
|
|
Joins("JOIN week_star_activity_config AS config ON config.id = snapshot.config_id").
|
|
Where("config.sys_origin = ?", sysOrigin).
|
|
Where("snapshot.period_end_at <= ?", s.toStorageWallClock(before)).
|
|
Order("snapshot.period_end_at desc").
|
|
Order("snapshot.config_id desc").
|
|
Limit(1).
|
|
Scan(&anchor)
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
if result.RowsAffected == 0 {
|
|
return nil, nil
|
|
}
|
|
return &anchor, nil
|
|
}
|