diff --git a/internal/admin/api.go b/internal/admin/api.go index 66977e5..8bae7a3 100644 --- a/internal/admin/api.go +++ b/internal/admin/api.go @@ -39,6 +39,9 @@ func (h *Handler) APIRoutes() http.Handler { r.Post("/frames", h.apiFrameUpsert) r.Delete("/frames", h.apiFrameDelete) + r.Get("/rank-tiers", h.apiRankTiers) + r.Post("/rank-tiers", h.apiRankTierUpsert) + r.Get("/chat", h.apiChat) r.Post("/chat/pack", h.apiChatPackUpsert) r.Delete("/chat/pack", h.apiChatPackDelete) @@ -291,8 +294,8 @@ func (h *Handler) apiCarpetDelete(w http.ResponseWriter, r *http.Request) { func (h *Handler) apiFrames(w http.ResponseWriter, r *http.Request) { httpx.JSON(w, http.StatusOK, map[string]any{ "frames": h.rows(r.Context(), - `SELECT id,title,price_coins,vip,sort,enabled FROM avatar_frames ORDER BY sort`, - "id", "title", "price_coins", "vip", "sort", "enabled"), + `SELECT id,title,price_coins,vip,c1,c2,sort,enabled FROM avatar_frames ORDER BY sort`, + "id", "title", "price_coins", "vip", "c1", "c2", "sort", "enabled"), }) } @@ -302,6 +305,8 @@ func (h *Handler) apiFrameUpsert(w http.ResponseWriter, r *http.Request) { Title string `json:"title"` PriceCoins int64 `json:"price_coins"` VIP bool `json:"vip"` + C1 string `json:"c1"` + C2 string `json:"c2"` Sort int `json:"sort"` Enabled *bool `json:"enabled"` } @@ -317,9 +322,11 @@ func (h *Handler) apiFrameUpsert(w http.ResponseWriter, r *http.Request) { if b.Enabled != nil && !*b.Enabled { enabled = 0 } + // رنگ‌ها را نرمال کن (حذفِ # و فاصله). + clean := func(s string) string { return strings.TrimPrefix(strings.TrimSpace(s), "#") } if _, err := h.db.ExecContext(r.Context(), - `INSERT OR REPLACE INTO avatar_frames (id,title,price_coins,vip,sort,enabled) VALUES (?,?,?,?,?,?)`, - b.ID, b.Title, b.PriceCoins, vip, b.Sort, enabled); err != nil { + `INSERT OR REPLACE INTO avatar_frames (id,title,price_coins,vip,c1,c2,sort,enabled) VALUES (?,?,?,?,?,?,?,?)`, + b.ID, b.Title, b.PriceCoins, vip, clean(b.C1), clean(b.C2), b.Sort, enabled); err != nil { httpx.Error(w, http.StatusInternalServerError, err.Error()) return } @@ -331,6 +338,38 @@ func (h *Handler) apiFrameDelete(w http.ResponseWriter, r *http.Request) { httpx.JSON(w, http.StatusOK, map[string]any{"ok": true}) } +// --- رتبه‌ها (رنگ/برچسب؛ مجموعه ثابت است، فقط ویرایش) --- + +func (h *Handler) apiRankTiers(w http.ResponseWriter, r *http.Request) { + httpx.JSON(w, http.StatusOK, map[string]any{ + "rank_tiers": h.rows(r.Context(), + `SELECT id,label,c1,c2,sort FROM rank_tiers ORDER BY sort`, + "id", "label", "c1", "c2", "sort"), + }) +} + +func (h *Handler) apiRankTierUpsert(w http.ResponseWriter, r *http.Request) { + var b struct { + ID string `json:"id"` + Label string `json:"label"` + C1 string `json:"c1"` + C2 string `json:"c2"` + Sort int `json:"sort"` + } + if err := json.NewDecoder(r.Body).Decode(&b); err != nil || strings.TrimSpace(b.ID) == "" { + httpx.Error(w, http.StatusBadRequest, "invalid body") + return + } + clean := func(s string) string { return strings.TrimPrefix(strings.TrimSpace(s), "#") } + if _, err := h.db.ExecContext(r.Context(), + `INSERT OR REPLACE INTO rank_tiers (id,label,c1,c2,sort) VALUES (?,?,?,?,?)`, + b.ID, b.Label, clean(b.C1), clean(b.C2), b.Sort); err != nil { + httpx.Error(w, http.StatusInternalServerError, err.Error()) + return + } + httpx.JSON(w, http.StatusOK, map[string]any{"ok": true}) +} + // --- چت --- func (h *Handler) apiChat(w http.ResponseWriter, r *http.Request) { diff --git a/internal/economy/frame.go b/internal/economy/frame.go index ba90d35..b11d276 100644 --- a/internal/economy/frame.go +++ b/internal/economy/frame.go @@ -8,13 +8,42 @@ type Frame struct { Title string `json:"title"` PriceCoins int64 `json:"price_coins"` VIP bool `json:"vip"` + C1 string `json:"c1"` // رنگِ گرادیان (hex بدونِ #)؛ خالی ⇒ قابِ رتبه + C2 string `json:"c2"` Owned bool `json:"owned"` } +// RankTierStyle رنگ و برچسبِ یک رتبه (برنز..پادشاه). سمتِ کلاینت hardcode نیست. +type RankTierStyle struct { + ID string `json:"id"` + Label string `json:"label"` + C1 string `json:"c1"` + C2 string `json:"c2"` +} + +// RankTiers رنگ/برچسبِ همه‌ی رتبه‌ها را برمی‌گرداند (برای کشِ کلاینت). +func (s *Service) RankTiers(ctx context.Context) ([]RankTierStyle, error) { + rows, err := s.db.QueryContext(ctx, + `SELECT id, label, c1, c2 FROM rank_tiers ORDER BY sort`) + if err != nil { + return nil, err + } + defer rows.Close() + var tiers []RankTierStyle + for rows.Next() { + var t RankTierStyle + if err := rows.Scan(&t.ID, &t.Label, &t.C1, &t.C2); err != nil { + return nil, err + } + tiers = append(tiers, t) + } + return tiers, rows.Err() +} + // Frames قاب‌های فعال را با وضعیتِ مالکیتِ کاربر برمی‌گرداند. func (s *Service) Frames(ctx context.Context, userID int64) ([]Frame, error) { rows, err := s.db.QueryContext(ctx, - `SELECT id, title, price_coins, vip FROM avatar_frames WHERE enabled = 1 ORDER BY sort`) + `SELECT id, title, price_coins, vip, c1, c2 FROM avatar_frames WHERE enabled = 1 ORDER BY sort`) if err != nil { return nil, err } @@ -24,7 +53,7 @@ func (s *Service) Frames(ctx context.Context, userID int64) ([]Frame, error) { for rows.Next() { var f Frame var vip int - if err := rows.Scan(&f.ID, &f.Title, &f.PriceCoins, &vip); err != nil { + if err := rows.Scan(&f.ID, &f.Title, &f.PriceCoins, &vip, &f.C1, &f.C2); err != nil { return nil, err } f.VIP = vip == 1 diff --git a/internal/economy/handler.go b/internal/economy/handler.go index 445f0a4..6dc7dbf 100644 --- a/internal/economy/handler.go +++ b/internal/economy/handler.go @@ -171,7 +171,12 @@ func (h *Handler) Frames(w http.ResponseWriter, r *http.Request) { if p, _ := h.svc.GetProfile(r.Context(), id); p != nil { selected = p.SelectedFrame } - httpx.JSON(w, http.StatusOK, map[string]any{"frames": frames, "selected": selected}) + tiers, _ := h.svc.RankTiers(r.Context()) + httpx.JSON(w, http.StatusOK, map[string]any{ + "frames": frames, + "selected": selected, + "rank_tiers": tiers, + }) } // BuyFrame — POST /api/shop/buy-frame diff --git a/internal/store/migrations/015_frame_colors.sql b/internal/store/migrations/015_frame_colors.sql new file mode 100644 index 0000000..23ca572 --- /dev/null +++ b/internal/store/migrations/015_frame_colors.sql @@ -0,0 +1,11 @@ +-- رنگِ گرادیانِ هر قاب سمتِ سرور تعریف می‌شود (hex بدونِ #)، تا ادمین بتواند قابِ +-- جدید با رنگِ دلخواه بسازد بدونِ به‌روزرسانیِ اپ. خالی ⇒ قابِ رتبه. +ALTER TABLE avatar_frames ADD COLUMN c1 TEXT NOT NULL DEFAULT ''; +ALTER TABLE avatar_frames ADD COLUMN c2 TEXT NOT NULL DEFAULT ''; + +UPDATE avatar_frames SET c1='F3D27A', c2='B8860B' WHERE id='gold'; +UPDATE avatar_frames SET c1='FF9A3C', c2='B81D2A' WHERE id='fire'; +UPDATE avatar_frames SET c1='7BEAA5', c2='1B7A4A' WHERE id='emerald'; +UPDATE avatar_frames SET c1='8FD3F0', c2='1E5FA8' WHERE id='ocean'; +UPDATE avatar_frames SET c1='F7A8C4', c2='A83060' WHERE id='rose'; +UPDATE avatar_frames SET c1='CE9BEA', c2='6A2FA0' WHERE id='royal'; diff --git a/internal/store/migrations/016_rank_tiers.sql b/internal/store/migrations/016_rank_tiers.sql new file mode 100644 index 0000000..cae4157 --- /dev/null +++ b/internal/store/migrations/016_rank_tiers.sql @@ -0,0 +1,15 @@ +-- رنگ و برچسبِ رتبه‌ها از سرور می‌آید (تا هیچ رنگی سمتِ کلاینت hardcode نباشد). +CREATE TABLE IF NOT EXISTS rank_tiers ( + id TEXT PRIMARY KEY, + label TEXT NOT NULL, + c1 TEXT NOT NULL, + c2 TEXT NOT NULL, + sort INTEGER NOT NULL DEFAULT 0 +); + +INSERT INTO rank_tiers (id, label, c1, c2, sort) VALUES + ('bronze', 'برنز', 'CD7F32', '8C5A2B', 1), + ('silver', 'نقره', 'BFC6CC', '8A949B', 2), + ('gold', 'طلا', 'FFD54F', 'B8860B', 3), + ('diamond', 'الماس', '5BE0E6', '1E8A99', 4), + ('king', 'پادشاه', 'B388FF', '6A1B9A', 5);