Files
back-hokm/internal/admin/admin.go
T
2026-06-15 20:54:05 +03:30

222 lines
7.5 KiB
Go

// پکیج admin یک پنل ادمینِ سرور‌سایدِ سبک (HTML) برای مدیریت فروشگاه و کاربران است.
// همه‌ی صفحات پشت Basic Auth هستند و در همان باینری Go سرو می‌شوند (بدون پروسه‌ی جدا).
package admin
import (
"context"
"database/sql"
"embed"
"html/template"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"hakemsho/internal/economy"
)
//go:embed templates/*.html
var tplFS embed.FS
type Handler struct {
db *sql.DB
eco *economy.Service
tpl *template.Template
}
func New(db *sql.DB, eco *economy.Service) *Handler {
tpl := template.Must(template.ParseFS(tplFS, "templates/*.html"))
return &Handler{db: db, eco: eco, tpl: tpl}
}
// Routes زیرروترِ /admin را با Basic Auth برمی‌گرداند.
func (h *Handler) Routes(user, pass string) http.Handler {
r := chi.NewRouter()
r.Use(middleware.BasicAuth("hakemsho-admin", map[string]string{user: pass}))
r.Get("/", h.dashboard)
r.Get("/shop", h.shop)
r.Post("/shop/{kind}", h.updateItem)
r.Get("/users", h.users)
r.Post("/users/coins", h.adjustCoins)
return r
}
func (h *Handler) render(w http.ResponseWriter, page string, data any) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := h.tpl.ExecuteTemplate(w, page, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// --- داشبورد ---
func (h *Handler) dashboard(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
}
data := 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`),
"Nav": "dashboard",
}
h.render(w, "dashboard.html", data)
}
// --- فروشگاه (ویرایش کاتالوگ) ---
func (h *Handler) shop(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
data := map[string]any{
"Nav": "shop",
"Coins": h.rows(ctx, `SELECT id,title,coins,vip_days,price_toman,bonus_pct,sort,enabled FROM coin_packages ORDER BY sort`,
"id", "title", "coins", "vip_days", "price_toman", "bonus_pct", "sort", "enabled"),
"Tickets": h.rows(ctx, `SELECT id,title,tickets,price_toman,sort,enabled FROM ticket_packages ORDER BY sort`,
"id", "title", "tickets", "price_toman", "sort", "enabled"),
"Cards": h.rows(ctx, `SELECT id,title,price_coins,sort,enabled FROM card_skins ORDER BY sort`,
"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"),
"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",
}
h.render(w, "shop.html", data)
}
// updateItem یک ردیفِ کاتالوگ را بر اساس kind به‌روزرسانی و کش را تازه می‌کند.
func (h *Handler) updateItem(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")
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 = `UPDATE coin_packages SET title=?,coins=?,vip_days=?,price_toman=?,bonus_pct=?,sort=?,enabled=? WHERE id=?`
args = []any{r.FormValue("title"), f("coins"), f("vip_days"), f("price_toman"), f("bonus_pct"), f("sort"), en, id}
case "ticket":
q = `UPDATE ticket_packages SET title=?,tickets=?,price_toman=?,sort=?,enabled=? WHERE id=?`
args = []any{r.FormValue("title"), f("tickets"), f("price_toman"), f("sort"), en, id}
case "card":
q = `UPDATE card_skins SET title=?,price_coins=?,sort=?,enabled=? WHERE id=?`
args = []any{r.FormValue("title"), f("price_coins"), f("sort"), en, id}
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 "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}
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)
}
// --- کاربران ---
func (h *Handler) users(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
q := r.URL.Query().Get("q")
query := `SELECT id, mobile, coins, tickets, xp, trophies, is_admin FROM users`
var args []any
if q != "" {
query += ` WHERE mobile LIKE ?`
args = append(args, "%"+q+"%")
}
query += ` ORDER BY id DESC LIMIT 100`
rows, err := h.db.QueryContext(ctx, query, args...)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
type userRow struct {
ID int64
Mobile string
Coins, Tickets, XP, Trophy int64
Level int
IsAdmin bool
}
var list []userRow
for rows.Next() {
var u userRow
var admin int
if err := rows.Scan(&u.ID, &u.Mobile, &u.Coins, &u.Tickets, &u.XP, &u.Trophy, &admin); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
u.IsAdmin = admin == 1
u.Level, _, _ = economy.LevelInfo(u.XP)
list = append(list, u)
}
h.render(w, "users.html", map[string]any{
"Nav": "users",
"Users": list,
"Q": q,
"Saved": r.URL.Query().Get("saved") == "1",
})
}
// adjustCoins سکه‌ی یک کاربر را افزایش/کاهش می‌دهد (لاگ در wallet_tx).
func (h *Handler) adjustCoins(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)
amount, _ := strconv.ParseInt(r.FormValue("amount"), 10, 64)
if userID > 0 && amount != 0 {
_ = h.eco.Adjust(r.Context(), userID, economy.CurrencyCoin, amount, "admin_adjust", "")
}
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)
if err != nil {
return nil
}
defer rs.Close()
var out []map[string]any
for rs.Next() {
vals := make([]any, len(cols))
ptrs := make([]any, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rs.Scan(ptrs...); err != nil {
return out
}
m := make(map[string]any, len(cols))
for i, c := range cols {
m[c] = vals[i]
}
out = append(out, m)
}
return out
}