diff --git a/cmd/server/main.go b/cmd/server/main.go
index b53a317..8194a5f 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -92,9 +92,12 @@ func main() {
r.Group(func(r chi.Router) {
r.Use(jwt.Middleware)
r.Get("/me", authH.Me)
+ r.Post("/profile", authH.UpdateProfile)
// کیفپول و فروشگاه
r.Get("/wallet", ecoH.Wallet)
+ r.Get("/stats", ecoH.Stats)
+ r.Get("/tables/info", ecoH.TablesInfo)
r.Get("/shop", ecoH.Shop)
r.Post("/shop/buy-card", ecoH.BuyCard)
r.Post("/shop/select-card", ecoH.SelectCard)
diff --git a/cmd/server/settler.go b/cmd/server/settler.go
index ca542fb..8bb14fe 100644
--- a/cmd/server/settler.go
+++ b/cmd/server/settler.go
@@ -47,6 +47,31 @@ func (g gameSettler) RecordGame(room, playersJSON string, winnerTeam int, kot bo
}
}
+// RecordHand آمار یک هَندِ پایانیافته را برای کاربر ثبت میکند.
+func (g gameSettler) RecordHand(userID int64, won, kot, asHakem bool) {
+ g.eco.RecordHand(context.Background(), userID, won, kot, asHakem)
+}
+
+// RecordCut آمار بُرِش با حکم را برای کاربر ثبت میکند.
+func (g gameSettler) RecordCut(userID int64) {
+ g.eco.RecordCut(context.Background(), userID)
+}
+
+// RecordGameResult آمار پایان بازی (برد/باخت) را ثبت میکند.
+func (g gameSettler) RecordGameResult(userID int64, won bool) {
+ g.eco.RecordGameResult(context.Background(), userID, won)
+}
+
+// ChargePrivateTable یک میز خصوصیِ رایگان را برای کاربر مصرف میکند.
+func (g gameSettler) ChargePrivateTable(userID int64) error {
+ return g.eco.ConsumePrivateTable(context.Background(), userID)
+}
+
+// PrivateTableInfo باقیماندهی میزهای رایگان و نامحدود بودن را برمیگرداند.
+func (g gameSettler) PrivateTableInfo(userID int64) (int, bool) {
+ return g.eco.PrivateTableInfo(context.Background(), userID)
+}
+
// TargetHands تعداد هَندِ لازم برای برد بازی در این tier (مثلاً مبتدی=۳).
func (g gameSettler) TargetHands(tier string) int {
t := g.eco.FindTier(tier)
diff --git a/internal/admin/admin.go b/internal/admin/admin.go
index 834583d..799a619 100644
--- a/internal/admin/admin.go
+++ b/internal/admin/admin.go
@@ -36,9 +36,12 @@ func (h *Handler) Routes(user, pass string) http.Handler {
r.Use(middleware.BasicAuth("hakemsho-admin", map[string]string{user: pass}))
r.Get("/", h.dashboard)
r.Get("/shop", h.shop)
+ r.Post("/shop/{kind}/add", h.addItem)
+ r.Post("/shop/{kind}/delete", h.deleteItem)
r.Post("/shop/{kind}", h.updateItem)
r.Get("/users", h.users)
r.Post("/users/coins", h.adjustCoins)
+ r.Post("/users/grant", h.grantToUser)
return r
}
@@ -82,6 +85,8 @@ func (h *Handler) shop(w http.ResponseWriter, r *http.Request) {
"id", "title", "price_coins", "sort", "enabled"),
"Boosters": h.rows(ctx, `SELECT id,title,multiplier,hours,price_toman,sort,enabled FROM boosters ORDER BY sort`,
"id", "title", "multiplier", "hours", "price_toman", "sort", "enabled"),
+ "VIP": h.rows(ctx, `SELECT id,title,months,price_toman,sort,enabled FROM vip_packages ORDER BY sort`,
+ "id", "title", "months", "price_toman", "sort", "enabled"),
"Tiers": h.rows(ctx, `SELECT id,title,hands,entry,prize,xp,trophy,sort,enabled FROM table_tiers ORDER BY sort`,
"id", "title", "hands", "entry", "prize", "xp", "trophy", "sort", "enabled"),
"Saved": r.URL.Query().Get("saved") == "1",
@@ -118,6 +123,9 @@ func (h *Handler) updateItem(w http.ResponseWriter, r *http.Request) {
case "booster":
q = `UPDATE boosters SET title=?,multiplier=?,hours=?,price_toman=?,sort=?,enabled=? WHERE id=?`
args = []any{r.FormValue("title"), f("multiplier"), f("hours"), f("price_toman"), f("sort"), en, id}
+ case "vip":
+ q = `UPDATE vip_packages SET title=?,months=?,price_toman=?,sort=?,enabled=? WHERE id=?`
+ args = []any{r.FormValue("title"), f("months"), f("price_toman"), f("sort"), en, id}
case "tier":
q = `UPDATE table_tiers SET title=?,hands=?,entry=?,prize=?,xp=?,trophy=?,sort=?,enabled=? WHERE id=?`
args = []any{r.FormValue("title"), f("hands"), f("entry"), f("prize"), f("xp"), f("trophy"), f("sort"), en, id}
@@ -134,6 +142,82 @@ func (h *Handler) updateItem(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/admin/shop?saved=1", http.StatusSeeOther)
}
+// addItem یک ردیفِ جدید به کاتالوگ اضافه میکند (افزودن محصول).
+func (h *Handler) addItem(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, "bad form", http.StatusBadRequest)
+ return
+ }
+ kind := chi.URLParam(r, "kind")
+ id := r.FormValue("id")
+ if id == "" {
+ http.Error(w, "id required", http.StatusBadRequest)
+ return
+ }
+ f := func(k string) int { v, _ := strconv.Atoi(r.FormValue(k)); return v }
+ en := 0
+ if r.FormValue("enabled") == "on" {
+ en = 1
+ }
+
+ var q string
+ var args []any
+ switch kind {
+ case "coin":
+ q = `INSERT INTO coin_packages (id,title,coins,vip_days,price_toman,bonus_pct,sort,enabled) VALUES (?,?,?,?,?,?,?,?)`
+ args = []any{id, r.FormValue("title"), f("coins"), f("vip_days"), f("price_toman"), f("bonus_pct"), f("sort"), en}
+ case "ticket":
+ q = `INSERT INTO ticket_packages (id,title,tickets,price_toman,sort,enabled) VALUES (?,?,?,?,?,?)`
+ args = []any{id, r.FormValue("title"), f("tickets"), f("price_toman"), f("sort"), en}
+ case "card":
+ q = `INSERT INTO card_skins (id,title,price_coins,sort,enabled) VALUES (?,?,?,?,?)`
+ args = []any{id, r.FormValue("title"), f("price_coins"), f("sort"), en}
+ case "booster":
+ q = `INSERT INTO boosters (id,title,multiplier,hours,price_toman,sort,enabled) VALUES (?,?,?,?,?,?)`
+ args = []any{id, r.FormValue("title"), f("multiplier"), f("hours"), f("price_toman"), f("sort"), en}
+ case "vip":
+ q = `INSERT INTO vip_packages (id,title,months,price_toman,sort,enabled) VALUES (?,?,?,?,?,?)`
+ args = []any{id, r.FormValue("title"), f("months"), f("price_toman"), f("sort"), en}
+ case "tier":
+ q = `INSERT INTO table_tiers (id,title,hands,entry,prize,xp,trophy,sort,enabled) VALUES (?,?,?,?,?,?,?,?,?)`
+ args = []any{id, r.FormValue("title"), f("hands"), f("entry"), f("prize"), f("xp"), f("trophy"), f("sort"), en}
+ default:
+ http.Error(w, "unknown kind", http.StatusBadRequest)
+ return
+ }
+
+ if _, err := h.db.ExecContext(r.Context(), q, args...); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ _ = h.eco.LoadCatalog(r.Context())
+ http.Redirect(w, r, "/admin/shop?saved=1", http.StatusSeeOther)
+}
+
+// deleteItem یک ردیفِ کاتالوگ را حذف میکند.
+func (h *Handler) deleteItem(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, "bad form", http.StatusBadRequest)
+ return
+ }
+ tables := map[string]string{
+ "coin": "coin_packages", "ticket": "ticket_packages", "card": "card_skins",
+ "booster": "boosters", "vip": "vip_packages", "tier": "table_tiers",
+ }
+ tbl, ok := tables[chi.URLParam(r, "kind")]
+ if !ok {
+ http.Error(w, "unknown kind", http.StatusBadRequest)
+ return
+ }
+ if _, err := h.db.ExecContext(r.Context(),
+ "DELETE FROM "+tbl+" WHERE id = ?", r.FormValue("id")); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ _ = h.eco.LoadCatalog(r.Context())
+ http.Redirect(w, r, "/admin/shop?saved=1", http.StatusSeeOther)
+}
+
// --- کاربران ---
func (h *Handler) users(w http.ResponseWriter, r *http.Request) {
@@ -194,6 +278,42 @@ func (h *Handler) adjustCoins(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/admin/users?saved=1", http.StatusSeeOther)
}
+// grantToUser یک محصول (سکه/بلیط/VIP/کارت) را به کاربر اعطا میکند.
+func (h *Handler) grantToUser(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, "bad form", http.StatusBadRequest)
+ return
+ }
+ userID, _ := strconv.ParseInt(r.FormValue("user_id"), 10, 64)
+ kind := r.FormValue("kind")
+ value := r.FormValue("value")
+ ctx := r.Context()
+ if userID <= 0 {
+ http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
+ return
+ }
+
+ var err error
+ switch kind {
+ case "coins":
+ amount, _ := strconv.ParseInt(value, 10, 64)
+ err = h.eco.Adjust(ctx, userID, economy.CurrencyCoin, amount, "admin_grant", "")
+ case "tickets":
+ amount, _ := strconv.ParseInt(value, 10, 64)
+ err = h.eco.Adjust(ctx, userID, economy.CurrencyTicket, amount, "admin_grant", "")
+ case "vip":
+ days, _ := strconv.Atoi(value)
+ err = h.eco.GrantVIPDays(ctx, userID, days)
+ case "card":
+ err = h.eco.GrantCard(ctx, userID, value)
+ }
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ http.Redirect(w, r, "/admin/users?saved=1", http.StatusSeeOther)
+}
+
// rows یک کوئری را به []map[string]any تبدیل میکند (برای رندرِ عمومیِ جدولها).
func (h *Handler) rows(ctx context.Context, query string, cols ...string) []map[string]any {
rs, err := h.db.QueryContext(ctx, query)
diff --git a/internal/admin/templates/shop.html b/internal/admin/templates/shop.html
index 436537e..65c7ab8 100644
--- a/internal/admin/templates/shop.html
+++ b/internal/admin/templates/shop.html
@@ -6,10 +6,11 @@
فروشگاه
{{if .Saved}}
تغییرات ذخیره شد ✓
{{end}}
+
برای ویرایش مقدارها «ذخیره»، برای حذف «حذف» و در ردیف آخر هر جدول محصول جدید «افزودن».
بستههای سکه
- | شناسه | عنوان | سکه | VIP (روز) | قیمت (تومان) | بونوس٪ | ترتیب | فعال | |
+ | شناسه | عنوان | سکه | VIP (روز) | قیمت (تومان) | بونوس٪ | ترتیب | فعال | |
{{range .Coins}}
{{end}}
+
بستههای بلیط
- | شناسه | عنوان | بلیط | قیمت (تومان) | ترتیب | فعال | |
+ | شناسه | عنوان | بلیط | قیمت (تومان) | ترتیب | فعال | |
{{range .Tickets}}
{{end}}
+
اسکین کارتها
- | شناسه | عنوان | قیمت (سکه) | ترتیب | فعال | |
+ | شناسه | عنوان | قیمت (سکه) | ترتیب | فعال | |
{{range .Cards}}
{{end}}
+
بوسترها
- | شناسه | عنوان | ضریب | ساعت | قیمت (تومان) | ترتیب | فعال | |
+ | شناسه | عنوان | ضریب | ساعت | قیمت (تومان) | ترتیب | فعال | |
{{range .Boosters}}
{{end}}
+
+
+
+
اشتراکهای VIP
+
+ | شناسه | عنوان | ماه | قیمت (تومان) | ترتیب | فعال | |
+ {{range .VIP}}
+
+ {{end}}
+
انواع میز
- | شناسه | عنوان | دست | ورودی | جایزه | XP | جام | ترتیب | فعال | |
+ | شناسه | عنوان | دست | ورودی | جایزه | XP | جام | ترتیب | فعال | |
{{range .Tiers}}
{{end}}
+