package economy import "context" // Frame یک قابِ آواتار (کازمتیک). Owned یعنی قابلِ انتخاب. سبکِ رنگی سمتِ کلاینت است. type Frame struct { ID string `json:"id"` 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, c1, c2 FROM avatar_frames WHERE enabled = 1 ORDER BY sort`) if err != nil { return nil, err } defer rows.Close() var frames []Frame for rows.Next() { var f Frame var vip int if err := rows.Scan(&f.ID, &f.Title, &f.PriceCoins, &vip, &f.C1, &f.C2); err != nil { return nil, err } f.VIP = vip == 1 frames = append(frames, f) } if err := rows.Err(); err != nil { return nil, err } owned, err := s.ownedFrames(ctx, userID) if err != nil { return nil, err } isVIP := s.isVIP(ctx, userID) for i := range frames { f := &frames[i] f.Owned = (f.PriceCoins == 0 && !f.VIP) || (f.VIP && isVIP) || owned[f.ID] } return frames, nil } func (s *Service) ownedFrames(ctx context.Context, userID int64) (map[string]bool, error) { rows, err := s.db.QueryContext(ctx, `SELECT frame_id FROM user_frames WHERE user_id = ?`, userID) if err != nil { return nil, err } defer rows.Close() owned := map[string]bool{} for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, err } owned[id] = true } return owned, rows.Err() } // BuyFrame یک قاب را با سکه باز می‌کند. func (s *Service) BuyFrame(ctx context.Context, userID int64, frameID string) error { var price int64 var vip, enabled int err := s.db.QueryRowContext(ctx, `SELECT price_coins, vip, enabled FROM avatar_frames WHERE id = ?`, frameID). Scan(&price, &vip, &enabled) if err != nil || enabled != 1 { return ErrNotFound } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() var exists int _ = tx.QueryRowContext(ctx, `SELECT 1 FROM user_frames WHERE user_id = ? AND frame_id = ?`, userID, frameID).Scan(&exists) if exists == 1 { return ErrAlreadyOwned } if price > 0 { if err := adjustTx(ctx, tx, userID, CurrencyCoin, -price, "buy_frame", frameID); err != nil { return err } } if _, err := tx.ExecContext(ctx, `INSERT INTO user_frames (user_id, frame_id) VALUES (?, ?)`, userID, frameID); err != nil { return err } // خرید = مالکیت + انتخابِ خودکار. if _, err := tx.ExecContext(ctx, `UPDATE users SET selected_frame = ? WHERE id = ?`, frameID, userID); err != nil { return err } return tx.Commit() } // SelectFrame قابِ انتخابیِ کاربر را تنظیم می‌کند (باید مالکش باشد یا رایگان/VIP). func (s *Service) SelectFrame(ctx context.Context, userID int64, frameID string) error { frames, err := s.Frames(ctx, userID) if err != nil { return err } for _, f := range frames { if f.ID == frameID { if !f.Owned { return ErrNotOwned } _, err := s.db.ExecContext(ctx, `UPDATE users SET selected_frame = ? WHERE id = ?`, frameID, userID) return err } } return ErrNotFound }