feat: add chat for backend

This commit is contained in:
2026-06-25 23:34:41 +03:30
parent 9a86d2bd4d
commit d290ba86bb
7 changed files with 406 additions and 0 deletions
+102
View File
@@ -51,6 +51,11 @@ func (h *Handler) Routes(user, pass string) http.Handler {
r.Post("/users/coins", h.adjustCoins)
r.Post("/users/grant", h.grantToUser)
r.Post("/season/reset", h.resetSeason)
r.Get("/chat", h.chatAdmin)
r.Post("/chat/pack/add", h.chatPackAdd)
r.Post("/chat/pack/delete", h.chatPackDelete)
r.Post("/chat/message/add", h.chatMessageAdd)
r.Post("/chat/message/delete", h.chatMessageDelete)
return r
}
@@ -438,6 +443,103 @@ func (h *Handler) grantToUser(w http.ResponseWriter, r *http.Request) {
}
// rows یک کوئری را به []map[string]any تبدیل می‌کند (برای رندرِ عمومیِ جدول‌ها).
// --- مدیریتِ بسته‌های چت و پیام‌ها ---
func (h *Handler) chatAdmin(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
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")
// پیام‌های هر بسته را زیرِ همان بسته قرار می‌دهیم.
for _, p := range packs {
id, _ := p["id"].(string)
p["messages"] = h.messagesOf(ctx, id)
}
h.render(w, "chat.html", map[string]any{
"Nav": "chat",
"Packs": packs,
"Saved": r.URL.Query().Get("saved") == "1",
})
}
func (h *Handler) messagesOf(ctx context.Context, packID string) []map[string]any {
rs, err := h.db.QueryContext(ctx,
`SELECT id,body,sort FROM chat_messages WHERE pack_id=? ORDER BY sort,id`, packID)
if err != nil {
return nil
}
defer rs.Close()
var out []map[string]any
for rs.Next() {
var id, sort int
var body string
if err := rs.Scan(&id, &body, &sort); err != nil {
return out
}
out = append(out, map[string]any{"id": id, "body": body, "sort": sort})
}
return out
}
func (h *Handler) chatPackAdd(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
id := r.FormValue("id")
if id == "" {
http.Error(w, "id required", http.StatusBadRequest)
return
}
num := func(k string) int { v, _ := strconv.Atoi(r.FormValue(k)); return v }
vip := 0
if r.FormValue("vip") == "on" {
vip = 1
}
kind := r.FormValue("kind")
if kind != "emoji" {
kind = "text"
}
_, err := h.db.ExecContext(r.Context(),
`INSERT OR REPLACE INTO chat_packs (id,title,kind,price_coins,vip,sort,enabled) VALUES (?,?,?,?,?,?,1)`,
id, r.FormValue("title"), kind, num("price_coins"), vip, num("sort"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/admin/chat?saved=1", http.StatusSeeOther)
}
func (h *Handler) chatPackDelete(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
id := r.FormValue("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)
http.Redirect(w, r, "/admin/chat?saved=1", http.StatusSeeOther)
}
func (h *Handler) chatMessageAdd(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
pack := r.FormValue("pack_id")
body := r.FormValue("body")
if pack == "" || body == "" {
http.Redirect(w, r, "/admin/chat", http.StatusSeeOther)
return
}
sort, _ := strconv.Atoi(r.FormValue("sort"))
_, err := h.db.ExecContext(r.Context(),
`INSERT INTO chat_messages (pack_id,body,sort) VALUES (?,?,?)`, pack, body, sort)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/admin/chat?saved=1", http.StatusSeeOther)
}
func (h *Handler) chatMessageDelete(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
id, _ := strconv.Atoi(r.FormValue("id"))
_, _ = h.db.ExecContext(r.Context(), `DELETE FROM chat_messages WHERE id=?`, id)
http.Redirect(w, r, "/admin/chat?saved=1", http.StatusSeeOther)
}
func (h *Handler) rows(ctx context.Context, query string, cols ...string) []map[string]any {
rs, err := h.db.QueryContext(ctx, query)
if err != nil {
+71
View File
@@ -0,0 +1,71 @@
<!doctype html>
<html dir="rtl" lang="fa">
{{template "head" .}}
<body>
{{template "nav" .}}
<div class="wrap">
<h1>بسته‌های چت</h1>
{{if .Saved}}<div class="saved">انجام شد ✓</div>{{end}}
<p style="color:#9aa;font-size:13px">
هر بسته شاملِ چند پیامِ آماده یا شکلک است. بسته با قیمتِ ۰ و بدونِ VIP برای
همه رایگان است؛ تیکِ VIP یعنی برای کاربرانِ VIP رایگان (و برای بقیه با قیمتِ سکه).
</p>
<h2>افزودن / ویرایش بسته</h2>
<table>
<tr><th>شناسه</th><th>عنوان</th><th>نوع</th><th>قیمت (سکه)</th><th>VIP رایگان</th><th>ترتیب</th><th></th></tr>
{{range .Packs}}
<tr><form method="post" action="/admin/chat/pack/add">
<td>{{.id}}<input type="hidden" name="id" value="{{.id}}"></td>
<td><input name="title" value="{{.title}}"></td>
<td>
<select name="kind">
<option value="text" {{if eq .kind "text"}}selected{{end}}>متن</option>
<option value="emoji" {{if eq .kind "emoji"}}selected{{end}}>شکلک</option>
</select>
</td>
<td><input name="price_coins" value="{{.price_coins}}" style="width:80px"></td>
<td><input type="checkbox" name="vip" {{if .vip}}checked{{end}}></td>
<td><input name="sort" value="{{.sort}}" style="width:50px"></td>
<td>
<button>ذخیره</button>
<button formaction="/admin/chat/pack/delete" formnovalidate class="del">حذف</button>
</td>
</form></tr>
{{end}}
<tr class="addrow"><form method="post" action="/admin/chat/pack/add">
<td><input name="id" placeholder="شناسه" required></td>
<td><input name="title" placeholder="عنوان"></td>
<td><select name="kind"><option value="text">متن</option><option value="emoji">شکلک</option></select></td>
<td><input name="price_coins" value="0" style="width:80px"></td>
<td><input type="checkbox" name="vip"></td>
<td><input name="sort" value="0" style="width:50px"></td>
<td><button>افزودن بسته</button></td>
</form></tr>
</table>
{{range .Packs}}
<h2>پیام‌های «{{.title}}» <small style="color:#9aa">({{.id}})</small></h2>
<table>
<tr><th>متن / شکلک</th><th>ترتیب</th><th></th></tr>
{{range .messages}}
<tr><form method="post" action="/admin/chat/message/delete">
<td>{{.body}}</td>
<td>{{.sort}}</td>
<td>
<input type="hidden" name="id" value="{{.id}}">
<button class="del">حذف</button>
</td>
</form></tr>
{{end}}
<tr class="addrow"><form method="post" action="/admin/chat/message/add">
<td><input type="hidden" name="pack_id" value="{{.id}}"><input name="body" placeholder="متن یا شکلک" required style="width:100%"></td>
<td><input name="sort" value="0" style="width:50px"></td>
<td><button>افزودن پیام</button></td>
</form></tr>
</table>
{{end}}
</div>
</body>
</html>
+1
View File
@@ -33,6 +33,7 @@
<nav>
<a href="/admin" class="{{if eq .Nav "dashboard"}}active{{end}}">داشبورد</a>
<a href="/admin/shop" class="{{if eq .Nav "shop"}}active{{end}}">فروشگاه</a>
<a href="/admin/chat" class="{{if eq .Nav "chat"}}active{{end}}">چت</a>
<a href="/admin/users" class="{{if eq .Nav "users"}}active{{end}}">کاربران</a>
</nav>
{{end}}