565 lines
19 KiB
Go
565 lines
19 KiB
Go
package admin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"hakemsho/internal/economy"
|
|
"hakemsho/internal/httpx"
|
|
)
|
|
|
|
// APIRoutes زیرروترِ JSON پنلِ ادمینِ Next.js را برمیگرداند (پشتِ همان Basic Auth).
|
|
// سمتِ کلاینت، هدرِ Authorization: Basic را ذخیره و در هر درخواست میفرستد.
|
|
func (h *Handler) APIRoutes() http.Handler {
|
|
r := chi.NewRouter()
|
|
r.Get("/me", h.apiMe)
|
|
r.Get("/dashboard", h.apiDashboard)
|
|
|
|
r.Get("/catalog", h.apiCatalog)
|
|
r.Post("/catalog/{kind}", h.apiCatalogUpsert)
|
|
r.Delete("/catalog/{kind}", h.apiCatalogDelete)
|
|
r.Post("/catalog/card/{id}/upload", h.uploadDeck) // multipart (اشتراکی با HTML)
|
|
|
|
r.Get("/users", h.apiUsers)
|
|
r.Post("/users/coins", h.apiUserCoins)
|
|
r.Post("/users/grant", h.apiUserGrant)
|
|
|
|
r.Get("/carpets", h.apiCarpets)
|
|
r.Post("/carpets", h.apiCarpetUpsert)
|
|
r.Delete("/carpets", h.apiCarpetDelete)
|
|
r.Post("/carpets/{id}/upload", h.carpetUpload) // multipart (اشتراکی)
|
|
|
|
r.Get("/frames", h.apiFrames)
|
|
r.Post("/frames", h.apiFrameUpsert)
|
|
r.Delete("/frames", h.apiFrameDelete)
|
|
|
|
r.Get("/rank-tiers", h.apiRankTiers)
|
|
r.Post("/rank-tiers", h.apiRankTierUpsert)
|
|
|
|
r.Get("/chat", h.apiChat)
|
|
r.Post("/chat/pack", h.apiChatPackUpsert)
|
|
r.Delete("/chat/pack", h.apiChatPackDelete)
|
|
r.Post("/chat/message", h.apiChatMessageAdd)
|
|
r.Delete("/chat/message", h.apiChatMessageDelete)
|
|
|
|
r.Get("/tournaments", h.apiTournaments)
|
|
r.Post("/tournaments", h.apiTournamentAdd)
|
|
r.Put("/tournaments", h.apiTournamentUpdate)
|
|
r.Delete("/tournaments", h.apiTournamentDelete)
|
|
|
|
r.Post("/season/reset", h.apiSeasonReset)
|
|
return r
|
|
}
|
|
|
|
func (h *Handler) apiMe(w http.ResponseWriter, _ *http.Request) {
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) apiDashboard(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
count := func(q string) int64 {
|
|
var n int64
|
|
_ = h.db.QueryRowContext(ctx, q).Scan(&n)
|
|
return n
|
|
}
|
|
httpx.JSON(w, http.StatusOK, map[string]any{
|
|
"users": count(`SELECT COUNT(*) FROM users`),
|
|
"purchases": count(`SELECT COUNT(*) FROM purchases WHERE status='verified'`),
|
|
"games": count(`SELECT COUNT(*) FROM game_history`),
|
|
"coins": count(`SELECT COALESCE(SUM(coins),0) FROM users`),
|
|
"tournaments": count(`SELECT COUNT(*) FROM tournaments`),
|
|
"season": h.eco.Season(ctx),
|
|
})
|
|
}
|
|
|
|
// --- کاتالوگِ فروشگاه ---
|
|
|
|
var catalogTable = map[string]string{
|
|
"coin": "coin_packages", "ticket": "ticket_packages", "card": "card_skins",
|
|
"booster": "boosters", "vip": "vip_packages", "tier": "table_tiers",
|
|
}
|
|
|
|
// catalogCols ستونهای قابلِ ویرایشِ هر نوع (بهجز sku که خودکار است).
|
|
var catalogCols = map[string][]string{
|
|
"coin": {"id", "title", "coins", "vip_days", "price_toman", "bonus_pct", "sort", "enabled"},
|
|
"ticket": {"id", "title", "tickets", "price_toman", "sort", "enabled"},
|
|
"card": {"id", "title", "price_coins", "sort", "enabled"},
|
|
"booster": {"id", "title", "multiplier", "hours", "price_toman", "sort", "enabled"},
|
|
"vip": {"id", "title", "months", "price_toman", "sort", "enabled"},
|
|
"tier": {"id", "title", "hands", "entry", "prize", "xp", "trophy", "rank_reward", "sort", "enabled"},
|
|
}
|
|
|
|
var catalogHasSKU = map[string]bool{"coin": true, "ticket": true, "booster": true, "vip": true}
|
|
|
|
func (h *Handler) apiCatalog(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
httpx.JSON(w, http.StatusOK, map[string]any{
|
|
"coin": h.rows(ctx, `SELECT id,title,coins,vip_days,price_toman,bonus_pct,sku,sort,enabled FROM coin_packages ORDER BY sort`,
|
|
"id", "title", "coins", "vip_days", "price_toman", "bonus_pct", "sku", "sort", "enabled"),
|
|
"ticket": h.rows(ctx, `SELECT id,title,tickets,price_toman,sku,sort,enabled FROM ticket_packages ORDER BY sort`,
|
|
"id", "title", "tickets", "price_toman", "sku", "sort", "enabled"),
|
|
"card": h.rows(ctx, `SELECT id,title,price_coins,sort,enabled FROM card_skins ORDER BY sort`,
|
|
"id", "title", "price_coins", "sort", "enabled"),
|
|
"booster": h.rows(ctx, `SELECT id,title,multiplier,hours,price_toman,sku,sort,enabled FROM boosters ORDER BY sort`,
|
|
"id", "title", "multiplier", "hours", "price_toman", "sku", "sort", "enabled"),
|
|
"vip": h.rows(ctx, `SELECT id,title,months,price_toman,sku,sort,enabled FROM vip_packages ORDER BY sort`,
|
|
"id", "title", "months", "price_toman", "sku", "sort", "enabled"),
|
|
"tier": h.rows(ctx, `SELECT id,title,hands,entry,prize,xp,trophy,rank_reward,sort,enabled FROM table_tiers ORDER BY sort`,
|
|
"id", "title", "hands", "entry", "prize", "xp", "trophy", "rank_reward", "sort", "enabled"),
|
|
})
|
|
}
|
|
|
|
// apiCatalogUpsert یک آیتم را میسازد یا بهروزرسانی میکند (INSERT OR REPLACE).
|
|
func (h *Handler) apiCatalogUpsert(w http.ResponseWriter, r *http.Request) {
|
|
kind := chi.URLParam(r, "kind")
|
|
table, ok := catalogTable[kind]
|
|
if !ok {
|
|
httpx.Error(w, http.StatusBadRequest, "unknown kind")
|
|
return
|
|
}
|
|
var body map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
httpx.Error(w, http.StatusBadRequest, "invalid body")
|
|
return
|
|
}
|
|
id, _ := body["id"].(string)
|
|
if strings.TrimSpace(id) == "" {
|
|
httpx.Error(w, http.StatusBadRequest, "id required")
|
|
return
|
|
}
|
|
|
|
cols := append([]string{}, catalogCols[kind]...)
|
|
vals := make([]any, 0, len(cols)+1)
|
|
for _, c := range cols {
|
|
vals = append(vals, coerceCol(c, body[c]))
|
|
}
|
|
// SKU: در ساخت خودکار تولید و در ویرایش حفظ میشود.
|
|
if catalogHasSKU[kind] {
|
|
var sku string
|
|
_ = h.db.QueryRowContext(r.Context(),
|
|
fmt.Sprintf(`SELECT sku FROM %s WHERE id=?`, table), id).Scan(&sku)
|
|
if sku == "" {
|
|
sku = newUUID()
|
|
}
|
|
cols = append(cols, "sku")
|
|
vals = append(vals, sku)
|
|
}
|
|
|
|
ph := strings.TrimRight(strings.Repeat("?,", len(cols)), ",")
|
|
q := fmt.Sprintf(`INSERT OR REPLACE INTO %s (%s) VALUES (%s)`, table, strings.Join(cols, ","), ph)
|
|
if _, err := h.db.ExecContext(r.Context(), q, vals...); err != nil {
|
|
httpx.Error(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
_ = h.eco.LoadCatalog(r.Context())
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) apiCatalogDelete(w http.ResponseWriter, r *http.Request) {
|
|
kind := chi.URLParam(r, "kind")
|
|
table, ok := catalogTable[kind]
|
|
if !ok {
|
|
httpx.Error(w, http.StatusBadRequest, "unknown kind")
|
|
return
|
|
}
|
|
id := r.URL.Query().Get("id")
|
|
if _, err := h.db.ExecContext(r.Context(),
|
|
fmt.Sprintf(`DELETE FROM %s WHERE id=?`, table), id); err != nil {
|
|
httpx.Error(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
_ = h.eco.LoadCatalog(r.Context())
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
// --- کاربران ---
|
|
|
|
func (h *Handler) apiUsers(w http.ResponseWriter, r *http.Request) {
|
|
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
|
ctx := r.Context()
|
|
var rows []map[string]any
|
|
if q == "" {
|
|
rows = h.rows(ctx,
|
|
`SELECT id,mobile,first_name,coins,rank_points,is_admin FROM users ORDER BY id DESC LIMIT 50`,
|
|
"id", "mobile", "first_name", "coins", "rank_points", "is_admin")
|
|
} else {
|
|
like := "%" + q + "%"
|
|
rows = h.rowsArgs(ctx,
|
|
`SELECT id,mobile,first_name,coins,rank_points,is_admin FROM users
|
|
WHERE mobile LIKE ? OR first_name LIKE ? OR CAST(id AS TEXT)=? ORDER BY id DESC LIMIT 50`,
|
|
[]any{like, like, q},
|
|
"id", "mobile", "first_name", "coins", "rank_points", "is_admin")
|
|
}
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"users": rows})
|
|
}
|
|
|
|
func (h *Handler) apiUserCoins(w http.ResponseWriter, r *http.Request) {
|
|
var b struct {
|
|
UserID int64 `json:"user_id"`
|
|
Amount int64 `json:"amount"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&b); err != nil || b.UserID == 0 {
|
|
httpx.Error(w, http.StatusBadRequest, "invalid body")
|
|
return
|
|
}
|
|
if err := h.eco.Adjust(r.Context(), b.UserID, economy.CurrencyCoin, b.Amount, "admin_adjust", "panel"); err != nil {
|
|
httpx.Error(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) apiUserGrant(w http.ResponseWriter, r *http.Request) {
|
|
var b struct {
|
|
UserID int64 `json:"user_id"`
|
|
Kind string `json:"kind"` // coin | ticket | vip
|
|
Amount int64 `json:"amount"`
|
|
VIPDays int `json:"vip_days"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&b); err != nil || b.UserID == 0 {
|
|
httpx.Error(w, http.StatusBadRequest, "invalid body")
|
|
return
|
|
}
|
|
ctx := r.Context()
|
|
var err error
|
|
switch b.Kind {
|
|
case "coin":
|
|
err = h.eco.Adjust(ctx, b.UserID, economy.CurrencyCoin, b.Amount, "admin_grant", "panel")
|
|
case "ticket":
|
|
err = h.eco.Adjust(ctx, b.UserID, economy.CurrencyTicket, b.Amount, "admin_grant", "panel")
|
|
case "vip":
|
|
_, err = h.db.ExecContext(ctx,
|
|
`UPDATE users SET vip_until = datetime('now', ?) WHERE id = ?`,
|
|
fmt.Sprintf("+%d days", b.VIPDays), b.UserID)
|
|
default:
|
|
httpx.Error(w, http.StatusBadRequest, "unknown kind")
|
|
return
|
|
}
|
|
if err != nil {
|
|
httpx.Error(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
// --- فرشها ---
|
|
|
|
func (h *Handler) apiCarpets(w http.ResponseWriter, r *http.Request) {
|
|
httpx.JSON(w, http.StatusOK, map[string]any{
|
|
"carpets": h.rows(r.Context(),
|
|
`SELECT id,title,price_coins,vip,sort,enabled FROM carpets ORDER BY sort`,
|
|
"id", "title", "price_coins", "vip", "sort", "enabled"),
|
|
})
|
|
}
|
|
|
|
func (h *Handler) apiCarpetUpsert(w http.ResponseWriter, r *http.Request) {
|
|
var b struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
PriceCoins int64 `json:"price_coins"`
|
|
VIP bool `json:"vip"`
|
|
Sort int `json:"sort"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&b); err != nil || strings.TrimSpace(b.ID) == "" ||
|
|
strings.ContainsAny(b.ID, `/\.`) {
|
|
httpx.Error(w, http.StatusBadRequest, "invalid body")
|
|
return
|
|
}
|
|
vip := 0
|
|
if b.VIP {
|
|
vip = 1
|
|
}
|
|
if _, err := h.db.ExecContext(r.Context(),
|
|
`INSERT OR REPLACE INTO carpets (id,title,price_coins,vip,sort,enabled) VALUES (?,?,?,?,?,1)`,
|
|
b.ID, b.Title, b.PriceCoins, vip, b.Sort); err != nil {
|
|
httpx.Error(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) apiCarpetDelete(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = h.db.ExecContext(r.Context(), `DELETE FROM carpets WHERE id=?`, r.URL.Query().Get("id"))
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
// --- قابهای آواتار ---
|
|
|
|
func (h *Handler) apiFrames(w http.ResponseWriter, r *http.Request) {
|
|
httpx.JSON(w, http.StatusOK, map[string]any{
|
|
"frames": h.rows(r.Context(),
|
|
`SELECT id,title,price_coins,vip,c1,c2,sort,enabled FROM avatar_frames ORDER BY sort`,
|
|
"id", "title", "price_coins", "vip", "c1", "c2", "sort", "enabled"),
|
|
})
|
|
}
|
|
|
|
func (h *Handler) apiFrameUpsert(w http.ResponseWriter, r *http.Request) {
|
|
var b struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
PriceCoins int64 `json:"price_coins"`
|
|
VIP bool `json:"vip"`
|
|
C1 string `json:"c1"`
|
|
C2 string `json:"c2"`
|
|
Sort int `json:"sort"`
|
|
Enabled *bool `json:"enabled"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&b); err != nil || strings.TrimSpace(b.ID) == "" {
|
|
httpx.Error(w, http.StatusBadRequest, "invalid body")
|
|
return
|
|
}
|
|
vip := 0
|
|
if b.VIP {
|
|
vip = 1
|
|
}
|
|
enabled := 1
|
|
if b.Enabled != nil && !*b.Enabled {
|
|
enabled = 0
|
|
}
|
|
// رنگها را نرمال کن (حذفِ # و فاصله).
|
|
clean := func(s string) string { return strings.TrimPrefix(strings.TrimSpace(s), "#") }
|
|
if _, err := h.db.ExecContext(r.Context(),
|
|
`INSERT OR REPLACE INTO avatar_frames (id,title,price_coins,vip,c1,c2,sort,enabled) VALUES (?,?,?,?,?,?,?,?)`,
|
|
b.ID, b.Title, b.PriceCoins, vip, clean(b.C1), clean(b.C2), b.Sort, enabled); err != nil {
|
|
httpx.Error(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) apiFrameDelete(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = h.db.ExecContext(r.Context(), `DELETE FROM avatar_frames WHERE id=?`, r.URL.Query().Get("id"))
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
// --- رتبهها (رنگ/برچسب؛ مجموعه ثابت است، فقط ویرایش) ---
|
|
|
|
func (h *Handler) apiRankTiers(w http.ResponseWriter, r *http.Request) {
|
|
httpx.JSON(w, http.StatusOK, map[string]any{
|
|
"rank_tiers": h.rows(r.Context(),
|
|
`SELECT id,label,c1,c2,sort FROM rank_tiers ORDER BY sort`,
|
|
"id", "label", "c1", "c2", "sort"),
|
|
})
|
|
}
|
|
|
|
func (h *Handler) apiRankTierUpsert(w http.ResponseWriter, r *http.Request) {
|
|
var b struct {
|
|
ID string `json:"id"`
|
|
Label string `json:"label"`
|
|
C1 string `json:"c1"`
|
|
C2 string `json:"c2"`
|
|
Sort int `json:"sort"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&b); err != nil || strings.TrimSpace(b.ID) == "" {
|
|
httpx.Error(w, http.StatusBadRequest, "invalid body")
|
|
return
|
|
}
|
|
clean := func(s string) string { return strings.TrimPrefix(strings.TrimSpace(s), "#") }
|
|
if _, err := h.db.ExecContext(r.Context(),
|
|
`INSERT OR REPLACE INTO rank_tiers (id,label,c1,c2,sort) VALUES (?,?,?,?,?)`,
|
|
b.ID, b.Label, clean(b.C1), clean(b.C2), b.Sort); err != nil {
|
|
httpx.Error(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
// --- چت ---
|
|
|
|
func (h *Handler) apiChat(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
httpx.JSON(w, http.StatusOK, map[string]any{
|
|
"packs": h.rows(ctx,
|
|
`SELECT id,title,kind,price_coins,vip,sort,enabled FROM chat_packs ORDER BY sort`,
|
|
"id", "title", "kind", "price_coins", "vip", "sort", "enabled"),
|
|
"messages": h.rows(ctx,
|
|
`SELECT id,pack_id,body,sort FROM chat_messages ORDER BY pack_id,sort`,
|
|
"id", "pack_id", "body", "sort"),
|
|
})
|
|
}
|
|
|
|
func (h *Handler) apiChatPackUpsert(w http.ResponseWriter, r *http.Request) {
|
|
var b struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Kind string `json:"kind"`
|
|
PriceCoins int64 `json:"price_coins"`
|
|
VIP bool `json:"vip"`
|
|
Sort int `json:"sort"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&b); err != nil || strings.TrimSpace(b.ID) == "" {
|
|
httpx.Error(w, http.StatusBadRequest, "invalid body")
|
|
return
|
|
}
|
|
if b.Kind != "emoji" {
|
|
b.Kind = "text"
|
|
}
|
|
vip := 0
|
|
if b.VIP {
|
|
vip = 1
|
|
}
|
|
if _, err := h.db.ExecContext(r.Context(),
|
|
`INSERT OR REPLACE INTO chat_packs (id,title,kind,price_coins,vip,sort,enabled) VALUES (?,?,?,?,?,?,1)`,
|
|
b.ID, b.Title, b.Kind, b.PriceCoins, vip, b.Sort); err != nil {
|
|
httpx.Error(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) apiChatPackDelete(w http.ResponseWriter, r *http.Request) {
|
|
id := r.URL.Query().Get("id")
|
|
_, _ = h.db.ExecContext(r.Context(), `DELETE FROM chat_messages WHERE pack_id=?`, id)
|
|
_, _ = h.db.ExecContext(r.Context(), `DELETE FROM chat_packs WHERE id=?`, id)
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) apiChatMessageAdd(w http.ResponseWriter, r *http.Request) {
|
|
var b struct {
|
|
PackID string `json:"pack_id"`
|
|
Body string `json:"body"`
|
|
Sort int `json:"sort"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&b); err != nil || b.PackID == "" || b.Body == "" {
|
|
httpx.Error(w, http.StatusBadRequest, "invalid body")
|
|
return
|
|
}
|
|
if _, err := h.db.ExecContext(r.Context(),
|
|
`INSERT INTO chat_messages (pack_id,body,sort) VALUES (?,?,?)`, b.PackID, b.Body, b.Sort); err != nil {
|
|
httpx.Error(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) apiChatMessageDelete(w http.ResponseWriter, r *http.Request) {
|
|
id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)
|
|
_, _ = h.db.ExecContext(r.Context(), `DELETE FROM chat_messages WHERE id=?`, id)
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
// --- تورنومنتها ---
|
|
|
|
func (h *Handler) apiTournaments(w http.ResponseWriter, r *http.Request) {
|
|
list, _ := h.eco.AdminListTournaments(r.Context())
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"tournaments": list})
|
|
}
|
|
|
|
type tournamentBody struct {
|
|
ID int64 `json:"id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
EntryFee int64 `json:"entry_fee"`
|
|
Prizes []int64 `json:"prizes"`
|
|
StartsAt string `json:"starts_at"` // datetime-local به وقتِ تهران
|
|
EndsAt string `json:"ends_at"`
|
|
}
|
|
|
|
// tournamentFromBody بدنه را به economy.Tournament (با زمانِ UTC) تبدیل میکند.
|
|
func tournamentFromBody(b tournamentBody) economy.Tournament {
|
|
iran := time.FixedZone("IRST", 12600)
|
|
parse := func(s string) time.Time {
|
|
t, err := time.ParseInLocation("2006-01-02T15:04", s, iran)
|
|
if err != nil {
|
|
return time.Now().UTC()
|
|
}
|
|
return t.UTC()
|
|
}
|
|
return economy.Tournament{
|
|
Title: b.Title, Description: b.Description, EntryFee: b.EntryFee,
|
|
Prizes: b.Prizes, StartsAt: parse(b.StartsAt), EndsAt: parse(b.EndsAt),
|
|
}
|
|
}
|
|
|
|
func (h *Handler) apiTournamentAdd(w http.ResponseWriter, r *http.Request) {
|
|
var b tournamentBody
|
|
if err := json.NewDecoder(r.Body).Decode(&b); err != nil || strings.TrimSpace(b.Title) == "" {
|
|
httpx.Error(w, http.StatusBadRequest, "invalid body")
|
|
return
|
|
}
|
|
if _, err := h.eco.CreateTournament(r.Context(), tournamentFromBody(b)); err != nil {
|
|
httpx.Error(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) apiTournamentUpdate(w http.ResponseWriter, r *http.Request) {
|
|
var b tournamentBody
|
|
if err := json.NewDecoder(r.Body).Decode(&b); err != nil || b.ID == 0 || strings.TrimSpace(b.Title) == "" {
|
|
httpx.Error(w, http.StatusBadRequest, "invalid body")
|
|
return
|
|
}
|
|
if err := h.eco.UpdateTournament(r.Context(), b.ID, tournamentFromBody(b)); err != nil {
|
|
httpx.Error(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) apiTournamentDelete(w http.ResponseWriter, r *http.Request) {
|
|
id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)
|
|
_ = h.eco.DeleteTournament(r.Context(), id)
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) apiSeasonReset(w http.ResponseWriter, r *http.Request) {
|
|
if err := h.eco.ResetSeason(r.Context()); err != nil {
|
|
httpx.Error(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
// coerceCol مقدارِ JSON را برای ستونِ کاتالوگ به نوعِ درست تبدیل میکند.
|
|
func coerceCol(col string, v any) any {
|
|
switch col {
|
|
case "id", "title":
|
|
s, _ := v.(string)
|
|
return s
|
|
case "enabled":
|
|
return boolToInt(v)
|
|
default: // اعداد
|
|
return toInt(v)
|
|
}
|
|
}
|
|
|
|
func toInt(v any) int64 {
|
|
switch n := v.(type) {
|
|
case float64:
|
|
return int64(n)
|
|
case int64:
|
|
return n
|
|
case int:
|
|
return int64(n)
|
|
case string:
|
|
i, _ := strconv.ParseInt(strings.TrimSpace(n), 10, 64)
|
|
return i
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func boolToInt(v any) int {
|
|
switch b := v.(type) {
|
|
case bool:
|
|
if b {
|
|
return 1
|
|
}
|
|
case float64:
|
|
if b != 0 {
|
|
return 1
|
|
}
|
|
case string:
|
|
if b == "true" || b == "1" || b == "on" {
|
|
return 1
|
|
}
|
|
}
|
|
return 0
|
|
}
|