feat: add cards templete

This commit is contained in:
2026-06-24 10:47:54 +03:30
parent 60494946d6
commit b1c7bfdd06
124 changed files with 422 additions and 30 deletions
+32 -7
View File
@@ -50,14 +50,16 @@ type VIPPackage struct {
}
// TableTier نوع میز: تعداد دست، ورودی، جایزه، XP و جام.
// RankReward > 0 یعنی میزِ رتبه‌بندی است (به برنده امتیازِ رتبه می‌دهد).
type TableTier struct {
ID string `json:"id"`
Title string `json:"title"`
Hands int `json:"hands"`
Entry int64 `json:"entry"`
Prize int64 `json:"prize"`
XP int `json:"xp"`
Trophy int `json:"trophy"`
ID string `json:"id"`
Title string `json:"title"`
Hands int `json:"hands"`
Entry int64 `json:"entry"`
Prize int64 `json:"prize"`
XP int `json:"xp"`
Trophy int `json:"trophy"`
RankReward int `json:"rank_reward"`
}
// Catalog کل کاتالوگ فروشگاه (برای GET /api/shop). از دیتابیس بارگذاری می‌شود.
@@ -70,6 +72,29 @@ type Catalog struct {
TableTiers []TableTier `json:"table_tiers"`
}
// rankTiers آستانه‌ی امتیازِ هر نشانِ رتبه (از پایین به بالا).
var rankTiers = []struct {
Name string
Min int64
}{
{"bronze", 0},
{"silver", 500},
{"gold", 1500},
{"diamond", 3000},
{"king", 5000},
}
// RankTier نام و اندیسِ نشانِ رتبه را از امتیازِ رتبه برمی‌گرداند.
func RankTier(points int64) (name string, index int) {
name, index = rankTiers[0].Name, 0
for i, t := range rankTiers {
if points >= t.Min {
name, index = t.Name, i
}
}
return name, index
}
// LevelInfo سطح فعلی، XP درون‌سطح و XP لازم برای سطح بعد را از XP کل محاسبه می‌کند.
// نیاز هر سطح: ۵۰ × شماره سطح (مثلاً سطح ۸ ⇒ ۴۰۰).
func LevelInfo(totalXP int64) (level int, into int64, need int64) {
+2 -2
View File
@@ -83,13 +83,13 @@ func (s *Service) LoadCatalog(ctx context.Context) error {
rows.Close()
rows, err = s.db.QueryContext(ctx,
`SELECT id, title, hands, entry, prize, xp, trophy FROM table_tiers WHERE enabled = 1 ORDER BY sort`)
`SELECT id, title, hands, entry, prize, xp, trophy, rank_reward FROM table_tiers WHERE enabled = 1 ORDER BY sort`)
if err != nil {
return err
}
for rows.Next() {
var p TableTier
if err := rows.Scan(&p.ID, &p.Title, &p.Hands, &p.Entry, &p.Prize, &p.XP, &p.Trophy); err != nil {
if err := rows.Scan(&p.ID, &p.Title, &p.Hands, &p.Entry, &p.Prize, &p.XP, &p.Trophy, &p.RankReward); err != nil {
rows.Close()
return err
}
+6 -2
View File
@@ -56,6 +56,9 @@ type Profile struct {
VIP bool `json:"vip"`
VIPUntil *string `json:"vip_until"`
SelectedCard string `json:"selected_card"`
RankPoints int64 `json:"rank_points"`
RankTier string `json:"rank_tier"` // bronze..king
RankIndex int `json:"rank_index"` // ۰..۴
}
// GetProfile وضعیت اقتصادی کاربر را می‌خواند.
@@ -63,8 +66,8 @@ func (s *Service) GetProfile(ctx context.Context, userID int64) (*Profile, error
var p Profile
var vipUntil sql.NullString
err := s.db.QueryRowContext(ctx,
`SELECT coins, tickets, xp, trophies, vip_until, selected_card FROM users WHERE id = ?`, userID).
Scan(&p.Coins, &p.Tickets, &p.XP, &p.Trophies, &vipUntil, &p.SelectedCard)
`SELECT coins, tickets, xp, trophies, vip_until, selected_card, rank_points FROM users WHERE id = ?`, userID).
Scan(&p.Coins, &p.Tickets, &p.XP, &p.Trophies, &vipUntil, &p.SelectedCard, &p.RankPoints)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
@@ -72,6 +75,7 @@ func (s *Service) GetProfile(ctx context.Context, userID int64) (*Profile, error
return nil, err
}
p.Level, p.XPInto, p.XPNeed = LevelInfo(p.XP)
p.RankTier, p.RankIndex = RankTier(p.RankPoints)
if vipUntil.Valid {
p.VIPUntil = &vipUntil.String
p.VIP = parseTS(vipUntil.String).After(nowUTC())
+17
View File
@@ -176,6 +176,23 @@ func (h *Handler) TablesInfo(w http.ResponseWriter, r *http.Request) {
})
}
// Leaderboard — GET /api/leaderboard (جدولِ رتبه‌بندیِ فصل)
func (h *Handler) Leaderboard(w http.ResponseWriter, r *http.Request) {
if _, ok := uid(r); !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
list, err := h.svc.Leaderboard(r.Context(), 50)
if err != nil {
httpx.Error(w, http.StatusInternalServerError, "server error")
return
}
httpx.JSON(w, http.StatusOK, map[string]any{
"season": h.svc.Season(r.Context()),
"entries": list,
})
}
// Daily — POST /api/rewards/daily
func (h *Handler) Daily(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
+88
View File
@@ -0,0 +1,88 @@
package economy
import (
"context"
"database/sql"
)
// RankEntry یک ردیف از جدولِ رتبه‌بندی.
type RankEntry struct {
Rank int `json:"rank"`
Name string `json:"name"`
Avatar string `json:"avatar"`
RankPoints int64 `json:"rank_points"`
Tier string `json:"tier"`
}
// AwardRank امتیازِ رتبه را پس از یک بازیِ رتبه‌بندی به‌روزرسانی می‌کند.
// برنده +reward، بازنده reward/2 (کفِ صفر). برای میزِ غیرِرتبه‌بندی بی‌اثر است.
func (s *Service) AwardRank(ctx context.Context, userID int64, reward int, won bool) error {
if reward <= 0 {
return nil
}
delta := reward
if !won {
delta = -(reward / 2)
}
_, err := s.db.ExecContext(ctx,
`UPDATE users SET rank_points = MAX(0, rank_points + ?) WHERE id = ?`,
delta, userID)
return err
}
// Leaderboard فهرستِ برترین بازیکنان بر اساس امتیازِ رتبه را برمی‌گرداند.
func (s *Service) Leaderboard(ctx context.Context, limit int) ([]RankEntry, error) {
if limit <= 0 || limit > 100 {
limit = 50
}
rows, err := s.db.QueryContext(ctx,
`SELECT first_name, avatar, rank_points FROM users
WHERE rank_points > 0 ORDER BY rank_points DESC LIMIT ?`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]RankEntry, 0, limit)
rank := 0
for rows.Next() {
var name, avatar sql.NullString
var pts int64
if err := rows.Scan(&name, &avatar, &pts); err != nil {
return nil, err
}
rank++
tier, _ := RankTier(pts)
nm := name.String
if nm == "" {
nm = "بازیکن"
}
out = append(out, RankEntry{
Rank: rank, Name: nm, Avatar: avatar.String, RankPoints: pts, Tier: tier,
})
}
return out, rows.Err()
}
// ResetSeason امتیازِ رتبه‌ی همه‌ی کاربران را صفر کرده و شماره‌ی فصل را زیاد می‌کند.
func (s *Service) ResetSeason(ctx context.Context) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, `UPDATE users SET rank_points = 0`); err != nil {
return err
}
if _, err := tx.ExecContext(ctx,
`UPDATE app_settings SET value = value + 1 WHERE key = 'season'`); err != nil {
return err
}
return tx.Commit()
}
// Season شماره‌ی فصلِ جاری را برمی‌گرداند.
func (s *Service) Season(ctx context.Context) int {
var v int
_ = s.db.QueryRowContext(ctx, `SELECT value FROM app_settings WHERE key = 'season'`).Scan(&v)
return v
}