mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-08 02:36:16 +00:00
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:
+62
-29
@@ -3,9 +3,8 @@ package mq
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -19,6 +18,29 @@ import (
|
||||
"github.com/assimon/luuu/util/sign"
|
||||
)
|
||||
|
||||
// resolveOrderApiKey returns the api_keys row that signed the order.
|
||||
// Every order must carry an ApiKeyID (the default key is always seeded
|
||||
// at bootstrap, and the middleware/EPAY inline flow always stamps it).
|
||||
// If the row is missing or disabled, we return an error — using any
|
||||
// other secret would produce a signature the merchant can't verify.
|
||||
// The admin can resend the callback after fixing the key.
|
||||
func resolveOrderApiKey(order *mdb.Orders) (*mdb.ApiKey, error) {
|
||||
if order.ApiKeyID == 0 {
|
||||
return nil, fmt.Errorf("order trade_id=%s has no api_key_id", order.TradeId)
|
||||
}
|
||||
row, err := data.GetApiKeyByID(order.ApiKeyID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lookup api_key_id=%d: %w", order.ApiKeyID, err)
|
||||
}
|
||||
if row.ID == 0 {
|
||||
return nil, fmt.Errorf("api_key_id=%d not found (deleted?)", order.ApiKeyID)
|
||||
}
|
||||
if row.Status != mdb.ApiKeyStatusEnable {
|
||||
return nil, fmt.Errorf("api_key_id=%d is disabled", order.ApiKeyID)
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
|
||||
const batchSize = 100
|
||||
|
||||
const sqliteBusyRetryAttempts = 3
|
||||
@@ -165,56 +187,62 @@ func processCallback(tradeID string) {
|
||||
}
|
||||
|
||||
func sendOrderCallback(order *mdb.Orders) error {
|
||||
apiKeyRow, err := resolveOrderApiKey(order)
|
||||
if err != nil || apiKeyRow == nil || apiKeyRow.ID == 0 {
|
||||
return errors.New("no api key row available for callback")
|
||||
}
|
||||
|
||||
switch order.PaymentType {
|
||||
case mdb.PaymentTypeEpay:
|
||||
// 构造 EPay 标准回调参数
|
||||
// EPAY uses pid (integer) and secret_key as "key".
|
||||
pidInt, convErr := strconv.Atoi(apiKeyRow.Pid)
|
||||
if convErr != nil {
|
||||
return fmt.Errorf("epay pid not numeric: %s", apiKeyRow.Pid)
|
||||
}
|
||||
notifyData := response.OrderNotifyResponseEpay{
|
||||
PID: config.GetEpayPid(),
|
||||
TradeNo: order.TradeId, // epusdt 订单号作为 EPay 平台订单号
|
||||
OutTradeNo: order.OrderId, // 注意:EPay 回调要求商户订单号使用 out_trade_no 参数
|
||||
|
||||
PID: pidInt,
|
||||
TradeNo: order.TradeId,
|
||||
OutTradeNo: order.OrderId,
|
||||
Type: "alipay",
|
||||
Name: order.Name,
|
||||
Money: fmt.Sprintf("%.4f", order.Amount),
|
||||
TradeStatus: "TRADE_SUCCESS",
|
||||
}
|
||||
|
||||
signstr2, err := sign.Get(notifyData, config.GetEpayKey())
|
||||
signstr2, err := sign.Get(notifyData, apiKeyRow.SecretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用 form-encoded POST(EPay 标准协议格式)
|
||||
formData := url.Values{
|
||||
"pid": {fmt.Sprintf("%d", notifyData.PID)},
|
||||
"trade_no": {notifyData.TradeNo},
|
||||
"out_trade_no": {notifyData.OutTradeNo},
|
||||
"type": {notifyData.Type},
|
||||
"name": {notifyData.Name},
|
||||
"money": {notifyData.Money},
|
||||
"trade_status": {notifyData.TradeStatus},
|
||||
"sign": {signstr2},
|
||||
"sign_type": {"MD5"},
|
||||
formData := map[string]string{
|
||||
"pid": fmt.Sprintf("%d", notifyData.PID),
|
||||
"trade_no": notifyData.TradeNo,
|
||||
"out_trade_no": notifyData.OutTradeNo,
|
||||
"type": notifyData.Type,
|
||||
"name": notifyData.Name,
|
||||
"money": notifyData.Money,
|
||||
"trade_status": notifyData.TradeStatus,
|
||||
"sign": signstr2,
|
||||
"sign_type": "MD5",
|
||||
}
|
||||
|
||||
resp, err := http.PostForm(order.NotifyUrl, formData)
|
||||
epayResp, err := http_client.GetHttpClient().R().SetQueryParams(formData).Get(order.NotifyUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Sugar.Infof("[mq] epay notify_url response status: %d, body: %s", epayResp.StatusCode(), string(epayResp.Body()))
|
||||
if epayResp.StatusCode() != http.StatusOK {
|
||||
return errors.New(epayResp.Status())
|
||||
}
|
||||
if !isCallbackAck(epayResp.Body()) {
|
||||
return errors.New("not ok")
|
||||
}
|
||||
|
||||
fmt.Printf("notify_url response status: %d, body: %s\n", resp.StatusCode, string(responseBody))
|
||||
|
||||
default:
|
||||
|
||||
client := http_client.GetHttpClient()
|
||||
orderResp := response.OrderNotifyResponse{
|
||||
Pid: apiKeyRow.Pid,
|
||||
TradeId: order.TradeId,
|
||||
OrderId: order.OrderId,
|
||||
Amount: order.Amount,
|
||||
@@ -224,7 +252,7 @@ func sendOrderCallback(order *mdb.Orders) error {
|
||||
BlockTransactionId: order.BlockTransactionId,
|
||||
Status: mdb.StatusPaySuccess,
|
||||
}
|
||||
signature, err := sign.Get(orderResp, config.GetApiAuthToken())
|
||||
signature, err := sign.Get(orderResp, apiKeyRow.SecretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -240,7 +268,7 @@ func sendOrderCallback(order *mdb.Orders) error {
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return errors.New(resp.Status())
|
||||
}
|
||||
if string(resp.Body()) != "ok" {
|
||||
if !isCallbackAck(resp.Body()) {
|
||||
return errors.New("not ok")
|
||||
}
|
||||
}
|
||||
@@ -248,6 +276,11 @@ func sendOrderCallback(order *mdb.Orders) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isCallbackAck(body []byte) bool {
|
||||
ack := strings.ToLower(strings.TrimSpace(string(body)))
|
||||
return ack == "ok" || ack == "success"
|
||||
}
|
||||
|
||||
func cleanupExpiredTransactionLocks() {
|
||||
if err := data.CleanupExpiredTransactionLocks(); err != nil {
|
||||
log.Sugar.Errorf("[mq] cleanup expired transaction locks failed: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user