mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 18:26:16 +00:00
6bb47d4b00
- 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
280 lines
9.9 KiB
Go
280 lines
9.9 KiB
Go
package admin
|
|
|
|
import (
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/assimon/luuu/model/dao"
|
|
"github.com/assimon/luuu/model/data"
|
|
"github.com/assimon/luuu/model/mdb"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// AdminAddWalletRequest is the payload for adding a wallet via admin.
|
|
type AdminAddWalletRequest struct {
|
|
Network string `json:"network" validate:"required" example:"tron"`
|
|
Address string `json:"address" validate:"required" example:"TTestTronAddress001"`
|
|
Remark string `json:"remark" example:"主钱包"`
|
|
}
|
|
|
|
// AdminUpdateWalletRequest is the payload for updating a wallet remark.
|
|
type AdminUpdateWalletRequest struct {
|
|
Remark *string `json:"remark" example:"已更新的备注"`
|
|
}
|
|
|
|
// AdminChangeWalletStatusRequest is the payload for toggling wallet status.
|
|
type AdminChangeWalletStatusRequest struct {
|
|
// 状态 1=启用 2=禁用
|
|
Status int `json:"status" validate:"required|in:1,2" enums:"1,2" example:"1"`
|
|
}
|
|
|
|
// AdminBatchImportRequest is the payload for batch importing wallets.
|
|
type AdminBatchImportRequest struct {
|
|
Network string `json:"network" validate:"required" example:"tron"`
|
|
// Addresses listed as observation-only wallets. Private-key import
|
|
// and API-managed modes are out of scope for v1.
|
|
Addresses []string `json:"addresses" validate:"required" example:"TAddr001,TAddr002,TAddr003"`
|
|
}
|
|
|
|
// WalletListItem extends WalletAddress with order count.
|
|
type WalletListItem struct {
|
|
mdb.WalletAddress
|
|
OrderCount int64 `json:"order_count" example:"5"`
|
|
}
|
|
|
|
// AdminListWallets extends the read model with per-wallet order counts
|
|
// so the UI can render the "订单数" column without an N+1 follow-up.
|
|
// @Summary List wallets (admin)
|
|
// @Description Returns wallets with per-wallet order counts
|
|
// @Tags Admin Wallets
|
|
// @Security AdminJWT
|
|
// @Produce json
|
|
// @Param network query string false "Network filter"
|
|
// @Param status query int false "Status filter (1=enabled, 2=disabled)"
|
|
// @Param keyword query string false "Search by address or remark"
|
|
// @Success 200 {object} response.ApiResponse{data=[]admin.WalletListItem}
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/wallets [get]
|
|
func (c *BaseAdminController) AdminListWallets(ctx echo.Context) error {
|
|
network := strings.ToLower(strings.TrimSpace(ctx.QueryParam("network")))
|
|
status := strings.TrimSpace(ctx.QueryParam("status"))
|
|
keyword := strings.TrimSpace(ctx.QueryParam("keyword"))
|
|
|
|
tx := dao.Mdb.Model(&mdb.WalletAddress{})
|
|
if network != "" {
|
|
tx = tx.Where("network = ?", network)
|
|
}
|
|
if status != "" {
|
|
if s, err := strconv.Atoi(status); err == nil {
|
|
tx = tx.Where("status = ?", s)
|
|
}
|
|
}
|
|
if keyword != "" {
|
|
kw := "%" + keyword + "%"
|
|
tx = tx.Where("address LIKE ? OR remark LIKE ?", kw, kw)
|
|
}
|
|
var rows []mdb.WalletAddress
|
|
if err := tx.Order("id DESC").Find(&rows).Error; err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
|
|
counts, err := data.CountOrdersByAddress()
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
out := make([]WalletListItem, 0, len(rows))
|
|
for _, r := range rows {
|
|
out = append(out, WalletListItem{WalletAddress: r, OrderCount: counts[r.Address]})
|
|
}
|
|
return c.SucJson(ctx, out)
|
|
}
|
|
|
|
// AdminAddWallet wraps the existing create with admin-exposed Remark +
|
|
// Source=manual so audit fields are populated.
|
|
// @Summary Add wallet (admin)
|
|
// @Description Create a wallet with admin-specific fields (remark, source=manual)
|
|
// @Tags Admin Wallets
|
|
// @Security AdminJWT
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body admin.AdminAddWalletRequest true "Wallet payload"
|
|
// @Success 200 {object} response.ApiResponse{data=mdb.WalletAddress}
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/wallets [post]
|
|
func (c *BaseAdminController) AdminAddWallet(ctx echo.Context) error {
|
|
req := new(AdminAddWalletRequest)
|
|
if err := ctx.Bind(req); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
if err := c.ValidateStruct(ctx, req); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
row, err := data.AddWalletAddressWithNetwork(req.Network, req.Address)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
// Patch remark/source separately to avoid changing the existing
|
|
// package-level AddWalletAddressWithNetwork signature (used by
|
|
// Telegram bot + legacy endpoint).
|
|
fields := map[string]interface{}{"source": mdb.WalletSourceManual}
|
|
if req.Remark != "" {
|
|
fields["remark"] = req.Remark
|
|
}
|
|
_ = dao.Mdb.Model(&mdb.WalletAddress{}).Where("id = ?", row.ID).Updates(fields).Error
|
|
row.Remark = req.Remark
|
|
row.Source = mdb.WalletSourceManual
|
|
return c.SucJson(ctx, row)
|
|
}
|
|
|
|
// AdminGetWallet returns detail including order count for one wallet.
|
|
// @Summary Get wallet (admin)
|
|
// @Description Returns wallet detail with order count
|
|
// @Tags Admin Wallets
|
|
// @Security AdminJWT
|
|
// @Produce json
|
|
// @Param id path int true "Wallet ID"
|
|
// @Success 200 {object} response.ApiResponse{data=admin.WalletListItem}
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/wallets/{id} [get]
|
|
func (c *BaseAdminController) AdminGetWallet(ctx echo.Context) error {
|
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
wallet, err := data.GetWalletAddressById(id)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
if wallet.ID == 0 {
|
|
return c.FailJson(ctx, errors.New("wallet not found"))
|
|
}
|
|
counts, _ := data.CountOrdersByAddress()
|
|
return c.SucJson(ctx, WalletListItem{WalletAddress: *wallet, OrderCount: counts[wallet.Address]})
|
|
}
|
|
|
|
// AdminUpdateWallet only patches remark (status has its own endpoint).
|
|
// @Summary Update wallet (admin)
|
|
// @Description Patch wallet remark
|
|
// @Tags Admin Wallets
|
|
// @Security AdminJWT
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path int true "Wallet ID"
|
|
// @Param request body admin.AdminUpdateWalletRequest true "Fields to update"
|
|
// @Success 200 {object} response.ApiResponse
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/wallets/{id} [patch]
|
|
func (c *BaseAdminController) AdminUpdateWallet(ctx echo.Context) error {
|
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
req := new(AdminUpdateWalletRequest)
|
|
if err := ctx.Bind(req); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
fields := map[string]interface{}{}
|
|
if req.Remark != nil {
|
|
fields["remark"] = *req.Remark
|
|
}
|
|
if len(fields) == 0 {
|
|
return c.SucJson(ctx, nil)
|
|
}
|
|
if err := dao.Mdb.Model(&mdb.WalletAddress{}).Where("id = ?", id).Updates(fields).Error; err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
return c.SucJson(ctx, nil)
|
|
}
|
|
|
|
// AdminChangeWalletStatus reuses existing helper.
|
|
// @Summary Change wallet status (admin)
|
|
// @Description Toggle enable/disable for a wallet (1=enabled, 2=disabled)
|
|
// @Tags Admin Wallets
|
|
// @Security AdminJWT
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path int true "Wallet ID"
|
|
// @Param request body admin.AdminChangeWalletStatusRequest true "Status payload"
|
|
// @Success 200 {object} response.ApiResponse
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/wallets/{id}/status [post]
|
|
func (c *BaseAdminController) AdminChangeWalletStatus(ctx echo.Context) error {
|
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
req := new(AdminChangeWalletStatusRequest)
|
|
if err := ctx.Bind(req); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
if err := c.ValidateStruct(ctx, req); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
if err := data.ChangeWalletAddressStatus(id, req.Status); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
return c.SucJson(ctx, nil)
|
|
}
|
|
|
|
// AdminDeleteWallet soft-deletes the wallet.
|
|
// @Summary Delete wallet (admin)
|
|
// @Description Soft-delete a wallet
|
|
// @Tags Admin Wallets
|
|
// @Security AdminJWT
|
|
// @Produce json
|
|
// @Param id path int true "Wallet ID"
|
|
// @Success 200 {object} response.ApiResponse
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/wallets/{id} [delete]
|
|
func (c *BaseAdminController) AdminDeleteWallet(ctx echo.Context) error {
|
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
if err := data.DeleteWalletAddressById(id); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
return c.SucJson(ctx, nil)
|
|
}
|
|
|
|
// AdminBatchImportWallets accepts a list of addresses and creates
|
|
// them all as observation wallets. Per-row failures are collected and
|
|
// returned so the UI can surface which addresses already existed.
|
|
// @Summary Batch import wallets
|
|
// @Description Import multiple wallet addresses at once. Per-row status is returned.
|
|
// @Tags Admin Wallets
|
|
// @Security AdminJWT
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body admin.AdminBatchImportRequest true "Batch import payload"
|
|
// @Success 200 {object} response.ApiResponse{data=[]admin.BatchImportResult}
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/wallets/batch-import [post]
|
|
func (c *BaseAdminController) AdminBatchImportWallets(ctx echo.Context) error {
|
|
req := new(AdminBatchImportRequest)
|
|
if err := ctx.Bind(req); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
if err := c.ValidateStruct(ctx, req); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
type result struct {
|
|
Address string `json:"address"`
|
|
OK bool `json:"ok"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
out := make([]result, 0, len(req.Addresses))
|
|
for _, addr := range req.Addresses {
|
|
row, err := data.AddWalletAddressWithNetwork(req.Network, addr)
|
|
if err != nil {
|
|
out = append(out, result{Address: addr, OK: false, Error: err.Error()})
|
|
continue
|
|
}
|
|
_ = dao.Mdb.Model(&mdb.WalletAddress{}).Where("id = ?", row.ID).
|
|
Update("source", mdb.WalletSourceImport).Error
|
|
out = append(out, result{Address: addr, OK: true})
|
|
}
|
|
return c.SucJson(ctx, out)
|
|
}
|