feat: add admin panel with multi-chain support and notification system

- Add full admin REST API: auth, API keys, chains, chain tokens, wallets,
  orders, RPC nodes, settings, dashboard stats, notifications
- Add JWT-based admin authentication and API key auth middleware
- Add multi-chain listeners: BSC, ETH, Polygon, Plasma, EVM WebSocket
- Add RPC node health check job with automatic failover
- Add Telegram notification channel for order events
- Add installer for first-run database initialization
- Add new DB models: admin_user, api_key, chain, chain_token,
  rpc_node, settings, notification_channel
- Add order statistics aggregation (daily/monthly/by-chain)
- Serve built admin SPA from embedded www/ assets
- Add comprehensive test coverage for all new features
This commit is contained in:
line-6000
2026-04-22 02:28:31 +08:00
parent 097c716714
commit 6bb47d4b00
247 changed files with 9933 additions and 1279 deletions
+70
View File
@@ -0,0 +1,70 @@
// Package notify fans out domain events (order paid, order expired,
// etc.) to every NotificationChannel subscribed to the event. Senders
// are type-keyed (telegram/webhook/email/...) so adding a new push
// method means registering one function; no callers change.
package notify
import (
"encoding/json"
"sync"
"github.com/assimon/luuu/model/data"
"github.com/assimon/luuu/model/mdb"
"github.com/assimon/luuu/util/log"
)
// Sender delivers an already-rendered text message via a channel row.
// Config is the channel's Config JSON string (type-specific shape).
type Sender func(config, text string) error
var (
sendersMu sync.RWMutex
senders = map[string]Sender{}
)
// RegisterSender wires a Sender for a channel type. Called once at
// package init of each sender file (e.g. telegram_sender.go).
func RegisterSender(channelType string, s Sender) {
sendersMu.Lock()
senders[channelType] = s
sendersMu.Unlock()
}
func getSender(channelType string) (Sender, bool) {
sendersMu.RLock()
defer sendersMu.RUnlock()
s, ok := senders[channelType]
return s, ok
}
// Dispatch delivers text to every enabled channel subscribed to event.
// Per-channel failures are logged but don't abort the fan-out — one
// broken TG token shouldn't prevent webhook delivery.
func Dispatch(event, text string) {
channels, err := data.ListEnabledChannelsByEvent(event)
if err != nil {
log.Sugar.Errorf("[notify] list channels failed: %v", err)
return
}
for _, ch := range channels {
sender, ok := getSender(ch.Type)
if !ok {
log.Sugar.Warnf("[notify] no sender registered for type=%s (channel_id=%d)", ch.Type, ch.ID)
continue
}
go func(c mdb.NotificationChannel) {
if err := sender(c.Config, text); err != nil {
log.Sugar.Errorf("[notify] send failed type=%s channel_id=%d: %v", c.Type, c.ID, err)
}
}(ch)
}
}
// ParseConfig helper for senders: unmarshal channel Config JSON into
// an arbitrary struct, returning a typed error on invalid JSON.
func ParseConfig(raw string, out interface{}) error {
if raw == "" {
return ErrEmptyConfig
}
return json.Unmarshal([]byte(raw), out)
}