feat: 实现管理后台 A3 注册群消息概览与 metrics 门禁

This commit is contained in:
Nixevol
2026-09-30 08:12:19 +08:00
parent 19a8ec95fa
commit 77d2dbddb1
13 changed files with 1593 additions and 82 deletions
+113
View File
@@ -0,0 +1,113 @@
package admin
import (
"database/sql"
"errors"
"net/http"
"strconv"
"time"
"git.asio.asia/nixevol/NixMsg/internal/httpx"
)
func (h *Handler) handleOverview(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var total, selfCount, disabled, online int
err := h.db.Read.QueryRowContext(ctx, `
SELECT
COUNT(*),
COALESCE(SUM(CASE WHEN source = 'self' THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN enabled = 0 THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE
WHEN online_since IS NOT NULL
AND (offline_since IS NULL OR online_since > offline_since) THEN 1
ELSE 0 END), 0)
FROM endpoints`).Scan(&total, &selfCount, &disabled, &online)
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
var groupsTotal int
if err := h.db.Read.QueryRowContext(ctx, `SELECT COUNT(*) FROM groups`).Scan(&groupsTotal); err != nil {
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
var pending int
if err := h.db.Read.QueryRowContext(ctx,
`SELECT COUNT(*) FROM deliveries WHERE state = 'pending'`).Scan(&pending); err != nil {
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
var scheduled int
if err := h.db.Read.QueryRowContext(ctx,
`SELECT COUNT(*) FROM messages WHERE state = 'scheduled'`).Scan(&scheduled); err != nil {
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
httpx.WriteOK(w, map[string]any{
"version": h.version,
"endpoints_total": total,
"endpoints_self": selfCount,
"endpoints_online": online,
"endpoints_disabled": disabled,
"groups_total": groupsTotal,
"messages_pending": pending,
"messages_scheduled": scheduled,
"uptime_ms": time.Since(h.startedAt).Milliseconds(),
})
}
func (h *Handler) handleSettingsGet(w http.ResponseWriter, _ *http.Request) {
cfg := h.cfg
httpx.WriteOK(w, map[string]any{
"listen": cfg.Listen,
"admin_listen": cfg.AdminListen,
"session_idle_days": cfg.SessionIdleDays,
"record_retention_days": cfg.RecordRetentionDays,
"idempotency_hours": cfg.IdempotencyHours,
"receipt_retention_days": cfg.ReceiptRetentionDays,
"sqlite_synchronous": cfg.SQLiteSynchronous,
"limits": map[string]any{
"max_body_bytes": cfg.Limits.MaxBodyBytes,
"max_meta_bytes": cfg.Limits.MaxMetaBytes,
"max_frame_bytes": cfg.Limits.MaxFrameBytes,
"max_ttl_seconds": cfg.Limits.MaxTTLSeconds,
"max_schedule_seconds": cfg.Limits.MaxScheduleSeconds,
"max_group_members": cfg.Limits.MaxGroupMembers,
"grace_seconds": cfg.Limits.GraceSeconds,
"ack_timeout_seconds": cfg.Limits.AckTimeoutSeconds,
"delivery_window": cfg.Limits.DeliveryWindow,
"receipt_window": cfg.Limits.ReceiptWindow,
"requests_per_second": cfg.Limits.RequestsPerSecond,
"max_pending_per_sender": cfg.Limits.MaxPendingPerSender,
"max_pending_per_receiver": cfg.Limits.MaxPendingPerReceiver,
},
})
}
func nullInt64API(v sql.NullInt64) any {
if !v.Valid {
return nil
}
return v.Int64
}
func parseInt64Query(s string) (int64, bool) {
if s == "" {
return 0, false
}
n, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, false
}
return n, true
}
func isNoRows(err error) bool {
return errors.Is(err, sql.ErrNoRows)
}