feat: add chat for backend
This commit is contained in:
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user