feat: add streak
This commit is contained in:
+77
-20
@@ -61,15 +61,19 @@ type Profile struct {
|
||||
RankTier string `json:"rank_tier"` // bronze..king
|
||||
RankIndex int `json:"rank_index"` // ۰..۴
|
||||
Avatar string `json:"avatar"` // seed آواتار (برای نمایش سرِ میز)
|
||||
DailyStreak int `json:"daily_streak"`
|
||||
BestStreak int `json:"best_streak"`
|
||||
DailyReady bool `json:"daily_ready"` // آیا امروز سکهی روزانه قابلِ دریافت است
|
||||
NextDaily int64 `json:"next_daily"` // جایزهی روزانهی بعدی (پیشنمایش)
|
||||
}
|
||||
|
||||
// GetProfile وضعیت اقتصادی کاربر را میخواند.
|
||||
func (s *Service) GetProfile(ctx context.Context, userID int64) (*Profile, error) {
|
||||
var p Profile
|
||||
var vipUntil, avatar sql.NullString
|
||||
var vipUntil, avatar, lastDaily sql.NullString
|
||||
err := s.db.QueryRowContext(ctx,
|
||||
`SELECT coins, tickets, xp, trophies, vip_until, selected_card, selected_carpet, rank_points, avatar FROM users WHERE id = ?`, userID).
|
||||
Scan(&p.Coins, &p.Tickets, &p.XP, &p.Trophies, &vipUntil, &p.SelectedCard, &p.SelectedCarpet, &p.RankPoints, &avatar)
|
||||
`SELECT coins, tickets, xp, trophies, vip_until, selected_card, selected_carpet, rank_points, avatar, daily_streak, best_streak, last_daily_at FROM users WHERE id = ?`, userID).
|
||||
Scan(&p.Coins, &p.Tickets, &p.XP, &p.Trophies, &vipUntil, &p.SelectedCard, &p.SelectedCarpet, &p.RankPoints, &avatar, &p.DailyStreak, &p.BestStreak, &lastDaily)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
@@ -83,6 +87,21 @@ func (s *Service) GetProfile(ctx context.Context, userID int64) (*Profile, error
|
||||
p.VIPUntil = &vipUntil.String
|
||||
p.VIP = parseTS(vipUntil.String).After(nowUTC())
|
||||
}
|
||||
// وضعیتِ سکهی روزانه: اگر امروز (تهران) نگرفته، قابلِ دریافت است. اگر یک روز
|
||||
// جا افتاده باشد، استریکِ بعدی از ۱ شروع میشود.
|
||||
today := tehranDate(nowUTC())
|
||||
nextStreak := p.DailyStreak + 1
|
||||
if lastDaily.Valid {
|
||||
lastDate := tehranDate(parseTS(lastDaily.String))
|
||||
p.DailyReady = lastDate != today
|
||||
if lastDate != tehranDate(nowUTC().Add(-24*time.Hour)) && lastDate != today {
|
||||
nextStreak = 1 // روزی جا افتاده ⇒ ریست
|
||||
}
|
||||
} else {
|
||||
p.DailyReady = true
|
||||
nextStreak = 1
|
||||
}
|
||||
p.NextDaily = streakReward(nextStreak)
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
@@ -94,32 +113,70 @@ func (s *Service) isVIP(ctx context.Context, userID int64) bool {
|
||||
return vu.Valid && parseTS(vu.String).After(nowUTC())
|
||||
}
|
||||
|
||||
// ClaimDaily سکه روزانه را در صورت گذشت ۲۴ ساعت میدهد.
|
||||
func (s *Service) ClaimDaily(ctx context.Context, userID int64) (int64, error) {
|
||||
// tehranDate تاریخِ تقویمیِ تهران (YYYY-MM-DD) را برمیگرداند. مرزِ «روز» برای
|
||||
// استریک، نیمهشبِ تهران است تا با حسِ کاربر بخواند.
|
||||
func tehranDate(t time.Time) string { return t.In(iranZone).Format("2006-01-02") }
|
||||
|
||||
// streakReward جایزهی روزِ n اُمِ استریک: پایه + پله (سقفِ روز ۷) + پاداشِ هفتگی.
|
||||
func streakReward(streak int) int64 {
|
||||
n := streak
|
||||
if n > 7 {
|
||||
n = 7
|
||||
}
|
||||
r := int64(DailyReward) + int64(n-1)*50 // روز۱=۲۰۰ … روز۷=۵۰۰
|
||||
if streak%7 == 0 { // هر ۷ روز پیاپی، پاداشِ ویژه
|
||||
r += 500
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ClaimDaily سکهی روزانه را (یکبار در هر روزِ تقویمیِ تهران) میدهد و استریک را
|
||||
// بهروزرسانی میکند. برمیگرداند: جایزه، استریکِ جدید، بهترین استریک.
|
||||
func (s *Service) ClaimDaily(ctx context.Context, userID int64) (reward int64, streak int, best int, err error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
cutoff := nowUTC().Add(-24 * time.Hour).Format(tsLayout)
|
||||
res, err := tx.ExecContext(ctx,
|
||||
`UPDATE users SET last_daily_at = ?
|
||||
WHERE id = ? AND (last_daily_at IS NULL OR last_daily_at <= ?)`,
|
||||
nowStr(), userID, cutoff)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
var last sql.NullString
|
||||
if err = tx.QueryRowContext(ctx,
|
||||
`SELECT last_daily_at, daily_streak, best_streak FROM users WHERE id = ?`, userID).
|
||||
Scan(&last, &streak, &best); err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
return 0, ErrTooSoon
|
||||
|
||||
today := tehranDate(nowUTC())
|
||||
if last.Valid {
|
||||
lastDate := tehranDate(parseTS(last.String))
|
||||
if lastDate == today {
|
||||
return 0, 0, 0, ErrTooSoon // امروز قبلاً گرفته
|
||||
}
|
||||
if lastDate == tehranDate(nowUTC().Add(-24*time.Hour)) {
|
||||
streak++ // دیروز گرفته ⇒ ادامهی استریک
|
||||
} else {
|
||||
streak = 1 // یک روز جا افتاده ⇒ ریست
|
||||
}
|
||||
} else {
|
||||
streak = 1
|
||||
}
|
||||
if err := adjustTx(ctx, tx, userID, CurrencyCoin, DailyReward, "daily_reward", ""); err != nil {
|
||||
return 0, err
|
||||
if streak > best {
|
||||
best = streak
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, err
|
||||
reward = streakReward(streak)
|
||||
|
||||
if _, err = tx.ExecContext(ctx,
|
||||
`UPDATE users SET last_daily_at = ?, daily_streak = ?, best_streak = ? WHERE id = ?`,
|
||||
nowStr(), streak, best, userID); err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
return DailyReward, nil
|
||||
if err = adjustTx(ctx, tx, userID, CurrencyCoin, reward, "daily_reward", ""); err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
return reward, streak, best, nil
|
||||
}
|
||||
|
||||
// ClaimAdReward پس از تأیید تبلیغ rewarded، سکه میدهد (با ضد تکرار و سقف روزانه).
|
||||
|
||||
Reference in New Issue
Block a user