feat: add chat for backend

This commit is contained in:
2026-06-25 23:34:41 +03:30
parent 9a86d2bd4d
commit d290ba86bb
7 changed files with 406 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
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()
}
+43
View File
@@ -87,6 +87,49 @@ func (h *Handler) BuyCard(w http.ResponseWriter, r *http.Request) {
}
}
// ChatPacks — GET /api/chat-packs (بسته‌های پیام/شکلک + مالکیتِ کاربر)
func (h *Handler) ChatPacks(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
packs, err := h.svc.ChatPacks(r.Context(), id)
if err != nil {
httpx.Error(w, http.StatusInternalServerError, "server error")
return
}
httpx.JSON(w, http.StatusOK, map[string]any{"packs": packs})
}
// BuyChatPack — POST /api/shop/buy-chat-pack
func (h *Handler) BuyChatPack(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
var req struct {
PackID string `json:"pack_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpx.Error(w, http.StatusBadRequest, "invalid body")
return
}
switch err := h.svc.BuyChatPack(r.Context(), id, req.PackID); {
case err == nil:
httpx.JSON(w, http.StatusOK, map[string]string{"message": "ok"})
case errors.Is(err, ErrNotFound):
httpx.Error(w, http.StatusNotFound, "pack not found")
case errors.Is(err, ErrAlreadyOwned):
httpx.Error(w, http.StatusConflict, "already owned")
case errors.Is(err, ErrInsufficient):
httpx.Error(w, http.StatusPaymentRequired, "insufficient coins")
default:
httpx.Error(w, http.StatusInternalServerError, "server error")
}
}
// SelectCard — POST /api/shop/select-card
func (h *Handler) SelectCard(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)