feat: add carpet
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package economy
|
||||
|
||||
import "context"
|
||||
|
||||
// Carpet یک فرشِ ایرانی که بهجای میزِ بازی استفاده میشود. Owned یعنی قابلِ انتخاب.
|
||||
type Carpet struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
PriceCoins int64 `json:"price_coins"`
|
||||
VIP bool `json:"vip"`
|
||||
Owned bool `json:"owned"`
|
||||
}
|
||||
|
||||
// Carpets فرشهای فعال را با وضعیتِ مالکیتِ کاربر برمیگرداند.
|
||||
func (s *Service) Carpets(ctx context.Context, userID int64) ([]Carpet, error) {
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
`SELECT id, title, price_coins, vip FROM carpets WHERE enabled = 1 ORDER BY sort`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var carpets []Carpet
|
||||
for rows.Next() {
|
||||
var c Carpet
|
||||
var vip int
|
||||
if err := rows.Scan(&c.ID, &c.Title, &c.PriceCoins, &vip); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.VIP = vip == 1
|
||||
carpets = append(carpets, c)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
owned, err := s.ownedCarpets(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isVIP := s.isVIP(ctx, userID)
|
||||
for i := range carpets {
|
||||
c := &carpets[i]
|
||||
c.Owned = (c.PriceCoins == 0 && !c.VIP) || (c.VIP && isVIP) || owned[c.ID]
|
||||
}
|
||||
return carpets, nil
|
||||
}
|
||||
|
||||
func (s *Service) ownedCarpets(ctx context.Context, userID int64) (map[string]bool, error) {
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
`SELECT carpet_id FROM user_carpets 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()
|
||||
}
|
||||
|
||||
// BuyCarpet یک فرش را با سکه باز میکند.
|
||||
func (s *Service) BuyCarpet(ctx context.Context, userID int64, carpetID string) error {
|
||||
var price int64
|
||||
var vip, enabled int
|
||||
err := s.db.QueryRowContext(ctx,
|
||||
`SELECT price_coins, vip, enabled FROM carpets WHERE id = ?`, carpetID).
|
||||
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_carpets WHERE user_id = ? AND carpet_id = ?`, userID, carpetID).Scan(&exists)
|
||||
if exists == 1 {
|
||||
return ErrAlreadyOwned
|
||||
}
|
||||
if price > 0 {
|
||||
if err := adjustTx(ctx, tx, userID, CurrencyCoin, -price, "buy_carpet", carpetID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
`INSERT INTO user_carpets (user_id, carpet_id) VALUES (?, ?)`, userID, carpetID); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// SelectCarpet فرشِ انتخابیِ کاربر را تنظیم میکند (باید مالکش باشد یا رایگان/VIP).
|
||||
func (s *Service) SelectCarpet(ctx context.Context, userID int64, carpetID string) error {
|
||||
carpets, err := s.Carpets(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range carpets {
|
||||
if c.ID == carpetID {
|
||||
if !c.Owned {
|
||||
return ErrNotOwned
|
||||
}
|
||||
_, err := s.db.ExecContext(ctx,
|
||||
`UPDATE users SET selected_carpet = ? WHERE id = ?`, carpetID, userID)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return ErrNotFound
|
||||
}
|
||||
@@ -55,7 +55,8 @@ type Profile struct {
|
||||
XPNeed int64 `json:"xp_for_next"`
|
||||
VIP bool `json:"vip"`
|
||||
VIPUntil *string `json:"vip_until"`
|
||||
SelectedCard string `json:"selected_card"`
|
||||
SelectedCard string `json:"selected_card"`
|
||||
SelectedCarpet string `json:"selected_carpet"`
|
||||
RankPoints int64 `json:"rank_points"`
|
||||
RankTier string `json:"rank_tier"` // bronze..king
|
||||
RankIndex int `json:"rank_index"` // ۰..۴
|
||||
@@ -66,8 +67,8 @@ func (s *Service) GetProfile(ctx context.Context, userID int64) (*Profile, error
|
||||
var p Profile
|
||||
var vipUntil sql.NullString
|
||||
err := s.db.QueryRowContext(ctx,
|
||||
`SELECT coins, tickets, xp, trophies, vip_until, selected_card, rank_points FROM users WHERE id = ?`, userID).
|
||||
Scan(&p.Coins, &p.Tickets, &p.XP, &p.Trophies, &vipUntil, &p.SelectedCard, &p.RankPoints)
|
||||
`SELECT coins, tickets, xp, trophies, vip_until, selected_card, selected_carpet, rank_points FROM users WHERE id = ?`, userID).
|
||||
Scan(&p.Coins, &p.Tickets, &p.XP, &p.Trophies, &vipUntil, &p.SelectedCard, &p.SelectedCarpet, &p.RankPoints)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
@@ -87,6 +87,73 @@ func (h *Handler) BuyCard(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Carpets — GET /api/carpets (فرشها + مالکیت + فرشِ انتخابی)
|
||||
func (h *Handler) Carpets(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := uid(r)
|
||||
if !ok {
|
||||
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
carpets, err := h.svc.Carpets(r.Context(), id)
|
||||
if err != nil {
|
||||
httpx.Error(w, http.StatusInternalServerError, "server error")
|
||||
return
|
||||
}
|
||||
selected := "classic"
|
||||
if p, _ := h.svc.GetProfile(r.Context(), id); p != nil && p.SelectedCarpet != "" {
|
||||
selected = p.SelectedCarpet
|
||||
}
|
||||
httpx.JSON(w, http.StatusOK, map[string]any{
|
||||
"carpets": carpets,
|
||||
"selected": selected,
|
||||
})
|
||||
}
|
||||
|
||||
// BuyCarpet — POST /api/shop/buy-carpet
|
||||
func (h *Handler) BuyCarpet(w http.ResponseWriter, r *http.Request) {
|
||||
h.carpetAction(w, r, false)
|
||||
}
|
||||
|
||||
// SelectCarpet — POST /api/shop/select-carpet
|
||||
func (h *Handler) SelectCarpet(w http.ResponseWriter, r *http.Request) {
|
||||
h.carpetAction(w, r, true)
|
||||
}
|
||||
|
||||
func (h *Handler) carpetAction(w http.ResponseWriter, r *http.Request, sel bool) {
|
||||
id, ok := uid(r)
|
||||
if !ok {
|
||||
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
CarpetID string `json:"carpet_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpx.Error(w, http.StatusBadRequest, "invalid body")
|
||||
return
|
||||
}
|
||||
var err error
|
||||
if sel {
|
||||
err = h.svc.SelectCarpet(r.Context(), id, req.CarpetID)
|
||||
} else {
|
||||
err = h.svc.BuyCarpet(r.Context(), id, req.CarpetID)
|
||||
}
|
||||
switch {
|
||||
case err == nil:
|
||||
httpx.JSON(w, http.StatusOK, map[string]string{"message": "ok"})
|
||||
case errors.Is(err, ErrNotFound):
|
||||
httpx.Error(w, http.StatusNotFound, "carpet not found")
|
||||
case errors.Is(err, ErrAlreadyOwned):
|
||||
httpx.Error(w, http.StatusConflict, "already owned")
|
||||
case errors.Is(err, ErrNotOwned):
|
||||
httpx.Error(w, http.StatusForbidden, "carpet not owned")
|
||||
case errors.Is(err, ErrInsufficient):
|
||||
httpx.Error(w, http.StatusPaymentRequired, "insufficient coins")
|
||||
default:
|
||||
httpx.Error(w, http.StatusInternalServerError, "server error")
|
||||
}
|
||||
}
|
||||
|
||||
// ChatPacks — GET /api/chat-packs (بستههای پیام/شکلک + مالکیتِ کاربر)
|
||||
func (h *Handler) ChatPacks(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := uid(r)
|
||||
|
||||
Reference in New Issue
Block a user