package economy import "context" // ChatPack یک بسته‌ی پیام/شکلکِ چت. Owned یعنی کاربر می‌تواند از آن استفاده کند // (رایگان، یا VIP برای بسته‌ی vip، یا خریداری‌شده). type ChatPack struct { ID string `json:"id"` Title string `json:"title"` Kind string `json:"kind"` // text | emoji PriceCoins int64 `json:"price_coins"` VIP bool `json:"vip"` // برای VIP رایگان است Owned bool `json:"owned"` Messages []string `json:"messages"` } // ChatPacks بسته‌های فعال را همراه پیام‌ها و وضعیتِ مالکیتِ کاربر برمی‌گرداند. func (s *Service) ChatPacks(ctx context.Context, userID int64) ([]ChatPack, error) { rows, err := s.db.QueryContext(ctx, `SELECT id, title, kind, price_coins, vip FROM chat_packs WHERE enabled = 1 ORDER BY sort`) if err != nil { return nil, err } defer rows.Close() var packs []ChatPack for rows.Next() { var p ChatPack var vip int if err := rows.Scan(&p.ID, &p.Title, &p.Kind, &p.PriceCoins, &vip); err != nil { return nil, err } p.VIP = vip == 1 packs = append(packs, p) } if err := rows.Err(); err != nil { return nil, err } owned, err := s.ownedChatPacks(ctx, userID) if err != nil { return nil, err } isVIP := s.isVIP(ctx, userID) for i := range packs { p := &packs[i] // رایگان (بدون قیمت و غیرِ vip)، یا VIP برای بسته‌ی vip، یا خریداری‌شده. p.Owned = (p.PriceCoins == 0 && !p.VIP) || (p.VIP && isVIP) || owned[p.ID] msgs, err := s.packMessages(ctx, p.ID) if err != nil { return nil, err } p.Messages = msgs } return packs, nil } func (s *Service) packMessages(ctx context.Context, packID string) ([]string, error) { rows, err := s.db.QueryContext(ctx, `SELECT body FROM chat_messages WHERE pack_id = ? ORDER BY sort, id`, packID) if err != nil { return nil, err } defer rows.Close() msgs := []string{} for rows.Next() { var b string if err := rows.Scan(&b); err != nil { return nil, err } msgs = append(msgs, b) } return msgs, rows.Err() } func (s *Service) ownedChatPacks(ctx context.Context, userID int64) (map[string]bool, error) { rows, err := s.db.QueryContext(ctx, `SELECT pack_id FROM user_chat_packs 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() } // BuyChatPack یک بسته‌ی چت را با سکه باز می‌کند. بسته‌های رایگان/قبلاً‌مالک خطا // می‌دهند؛ بسته‌ی VIP برای کاربرِ VIP خودبه‌خود در دسترس است (نیازی به خرید نیست). func (s *Service) BuyChatPack(ctx context.Context, userID int64, packID string) error { var price int64 var vip, enabled int err := s.db.QueryRowContext(ctx, `SELECT price_coins, vip, enabled FROM chat_packs WHERE id = ?`, packID). 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_chat_packs WHERE user_id = ? AND pack_id = ?`, userID, packID).Scan(&exists) if exists == 1 { return ErrAlreadyOwned } if price > 0 { if err := adjustTx(ctx, tx, userID, CurrencyCoin, -price, "buy_chat_pack", packID); err != nil { return err } } if _, err := tx.ExecContext(ctx, `INSERT INTO user_chat_packs (user_id, pack_id) VALUES (?, ?)`, userID, packID); err != nil { return err } return tx.Commit() }