Files
Airwallex/services/airwallex_service.go
zqq61 faba565c66 feat: Go 重写后端,替换 Python FastAPI
用 Go (Gin + GORM + SQLite) 重写整个后端:
- 单二进制部署,不依赖 Python/pip/SDK
- net/http 原生客户端,无 Cloudflare TLS 指纹问题
- 多阶段 Dockerfile:Node 构建前端 + Go 构建后端 + Alpine 运行
- 内存占用从 ~95MB 降至 ~3MB
- 完整保留所有 API 路由、JWT 认证、API Key 权限、审计日志
2026-03-16 02:11:48 +08:00

430 lines
11 KiB
Go

package services
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"sync"
"time"
"airwallex-admin/airwallex"
"airwallex-admin/models"
"golang.org/x/net/proxy"
"gorm.io/gorm"
)
var (
clientInstance *airwallex.Client
clientConfigHash string
clientMu sync.Mutex
)
// buildProxyURL reads proxy settings from the database and constructs a proxy URL.
func buildProxyURL(db *gorm.DB) string {
proxyIP := models.GetSetting(db, "proxy_ip")
proxyPort := models.GetSetting(db, "proxy_port")
if proxyIP == "" || proxyPort == "" {
return ""
}
proxyType := models.GetSetting(db, "proxy_type")
if proxyType == "" {
proxyType = "http"
}
proxyUser := models.GetSetting(db, "proxy_username")
proxyPass := models.GetSetting(db, "proxy_password")
var userInfo string
if proxyUser != "" {
if proxyPass != "" {
userInfo = proxyUser + ":" + proxyPass + "@"
} else {
userInfo = proxyUser + "@"
}
}
return fmt.Sprintf("%s://%s%s:%s", proxyType, userInfo, proxyIP, proxyPort)
}
// getConfigHash computes an MD5 hash of the current Airwallex configuration.
func getConfigHash(db *gorm.DB) string {
clientID := models.GetSetting(db, "airwallex_client_id")
apiKey := models.GetSetting(db, "airwallex_api_key")
baseURL := models.GetSetting(db, "airwallex_base_url")
loginAs := models.GetSetting(db, "airwallex_login_as")
proxyURL := buildProxyURL(db)
raw := clientID + apiKey + baseURL + loginAs + proxyURL
hash := md5.Sum([]byte(raw))
return fmt.Sprintf("%x", hash)
}
// GetClient returns a cached or newly created Airwallex client based on current settings.
func GetClient(db *gorm.DB) (*airwallex.Client, error) {
clientMu.Lock()
defer clientMu.Unlock()
clientID := models.GetSetting(db, "airwallex_client_id")
apiKey := models.GetSetting(db, "airwallex_api_key")
if clientID == "" || apiKey == "" {
return nil, fmt.Errorf("Airwallex credentials not configured")
}
baseURL := models.GetSetting(db, "airwallex_base_url")
if baseURL == "" {
baseURL = "https://api.airwallex.com"
}
loginAs := models.GetSetting(db, "airwallex_login_as")
proxyURL := buildProxyURL(db)
hash := fmt.Sprintf("%x", md5.Sum([]byte(clientID+apiKey+baseURL+loginAs+proxyURL)))
if clientInstance != nil && hash == clientConfigHash {
return clientInstance, nil
}
clientInstance = airwallex.NewClient(clientID, apiKey, baseURL, loginAs, proxyURL)
clientConfigHash = hash
return clientInstance, nil
}
// EnsureAuthenticated returns a client that has a valid authentication token.
func EnsureAuthenticated(db *gorm.DB) (*airwallex.Client, error) {
client, err := GetClient(db)
if err != nil {
return nil, err
}
if err := client.Authenticate(); err != nil {
return nil, err
}
return client, nil
}
// ListCards retrieves a paginated list of issuing cards.
func ListCards(db *gorm.DB, pageNum, pageSize int, cardholderID, status string) (map[string]interface{}, error) {
client, err := EnsureAuthenticated(db)
if err != nil {
return nil, err
}
params := url.Values{}
params.Set("page_num", fmt.Sprintf("%d", pageNum))
params.Set("page_size", fmt.Sprintf("%d", pageSize))
if cardholderID != "" {
params.Set("cardholder_id", cardholderID)
}
if status != "" {
params.Set("status", status)
}
result, err := client.ListCards(params)
if err != nil {
return nil, err
}
return map[string]interface{}{
"items": result.Items,
"has_more": result.HasMore,
"page_num": pageNum,
"page_size": pageSize,
}, nil
}
// CreateCard creates a new issuing card.
func CreateCard(db *gorm.DB, cardData map[string]interface{}) (map[string]interface{}, error) {
client, err := EnsureAuthenticated(db)
if err != nil {
return nil, err
}
return client.CreateCard(cardData)
}
// GetCard retrieves a single card by ID.
func GetCard(db *gorm.DB, cardID string) (map[string]interface{}, error) {
client, err := EnsureAuthenticated(db)
if err != nil {
return nil, err
}
return client.GetCard(cardID)
}
// GetCardDetails retrieves sensitive card details by card ID.
func GetCardDetails(db *gorm.DB, cardID string) (map[string]interface{}, error) {
client, err := EnsureAuthenticated(db)
if err != nil {
return nil, err
}
return client.GetCardDetails(cardID)
}
// UpdateCard updates an existing card by ID.
func UpdateCard(db *gorm.DB, cardID string, data map[string]interface{}) (map[string]interface{}, error) {
client, err := EnsureAuthenticated(db)
if err != nil {
return nil, err
}
return client.UpdateCard(cardID, data)
}
// ListCardholders retrieves a paginated list of cardholders.
func ListCardholders(db *gorm.DB, pageNum, pageSize int) (map[string]interface{}, error) {
client, err := EnsureAuthenticated(db)
if err != nil {
return nil, err
}
params := url.Values{}
params.Set("page_num", fmt.Sprintf("%d", pageNum))
params.Set("page_size", fmt.Sprintf("%d", pageSize))
result, err := client.ListCardholders(params)
if err != nil {
return nil, err
}
return map[string]interface{}{
"items": result.Items,
"has_more": result.HasMore,
"page_num": pageNum,
"page_size": pageSize,
}, nil
}
// CreateCardholder creates a new cardholder.
func CreateCardholder(db *gorm.DB, data map[string]interface{}) (map[string]interface{}, error) {
client, err := EnsureAuthenticated(db)
if err != nil {
return nil, err
}
return client.CreateCardholder(data)
}
// ListTransactions retrieves a paginated list of issuing transactions.
func ListTransactions(db *gorm.DB, pageNum, pageSize int, cardID, fromDate, toDate string) (map[string]interface{}, error) {
client, err := EnsureAuthenticated(db)
if err != nil {
return nil, err
}
params := url.Values{}
params.Set("page_num", fmt.Sprintf("%d", pageNum))
params.Set("page_size", fmt.Sprintf("%d", pageSize))
if cardID != "" {
params.Set("card_id", cardID)
}
if fromDate != "" {
params.Set("from_created_at", fromDate)
}
if toDate != "" {
params.Set("to_created_at", toDate)
}
result, err := client.ListTransactions(params)
if err != nil {
return nil, err
}
return map[string]interface{}{
"items": result.Items,
"has_more": result.HasMore,
"page_num": pageNum,
"page_size": pageSize,
}, nil
}
// ListAuthorizations retrieves a paginated list of issuing authorizations.
func ListAuthorizations(db *gorm.DB, pageNum, pageSize int, cardID, status, fromDate, toDate string) (map[string]interface{}, error) {
client, err := EnsureAuthenticated(db)
if err != nil {
return nil, err
}
params := url.Values{}
params.Set("page_num", fmt.Sprintf("%d", pageNum))
params.Set("page_size", fmt.Sprintf("%d", pageSize))
if cardID != "" {
params.Set("card_id", cardID)
}
if status != "" {
params.Set("status", status)
}
if fromDate != "" {
params.Set("from_created_at", fromDate)
}
if toDate != "" {
params.Set("to_created_at", toDate)
}
result, err := client.ListAuthorizations(params)
if err != nil {
return nil, err
}
return map[string]interface{}{
"items": result.Items,
"has_more": result.HasMore,
"page_num": pageNum,
"page_size": pageSize,
}, nil
}
// GetBalance retrieves the current account balance.
func GetBalance(db *gorm.DB) (map[string]interface{}, error) {
client, err := EnsureAuthenticated(db)
if err != nil {
return nil, err
}
result, err := client.GetBalance()
if err != nil {
return nil, nil
}
return result, nil
}
// GetIssuingConfig retrieves the issuing configuration.
func GetIssuingConfig(db *gorm.DB) (map[string]interface{}, error) {
client, err := EnsureAuthenticated(db)
if err != nil {
return nil, err
}
return client.GetIssuingConfig()
}
// TestConnection tests the Airwallex API connection.
func TestConnection(db *gorm.DB) map[string]interface{} {
_, err := EnsureAuthenticated(db)
if err != nil {
return map[string]interface{}{
"success": false,
"message": err.Error(),
}
}
return map[string]interface{}{
"success": true,
"message": "Connection successful",
}
}
// buildHTTPClientWithProxy creates an http.Client configured with the given proxy URL.
func buildHTTPClientWithProxy(proxyURL string) *http.Client {
httpClient := &http.Client{Timeout: 15 * time.Second}
if proxyURL == "" {
return httpClient
}
parsed, err := url.Parse(proxyURL)
if err != nil {
return httpClient
}
switch parsed.Scheme {
case "socks5", "socks5h":
dialer, err := proxy.FromURL(parsed, proxy.Direct)
if err != nil {
return httpClient
}
contextDialer, ok := dialer.(proxy.ContextDialer)
if ok {
httpClient.Transport = &http.Transport{
DialContext: contextDialer.DialContext,
}
} else {
httpClient.Transport = &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
},
}
}
case "http", "https":
httpClient.Transport = &http.Transport{
Proxy: http.ProxyURL(parsed),
}
}
return httpClient
}
// fetchIPInfo fetches IP geolocation info from ip-api.com using the provided HTTP client.
func fetchIPInfo(httpClient *http.Client) (ip, country, city, isp, status string, err error) {
resp, err := httpClient.Get("http://ip-api.com/json/?lang=zh-CN")
if err != nil {
return "", "", "", "", "fail", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", "", "", "fail", err
}
var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
return "", "", "", "", "fail", err
}
getString := func(key string) string {
if v, ok := data[key].(string); ok {
return v
}
return ""
}
return getString("query"), getString("country"), getString("city"), getString("isp"), getString("status"), nil
}
// TestProxy tests both proxy and direct connectivity, returning IP geolocation info.
func TestProxy(db *gorm.DB) map[string]interface{} {
result := map[string]interface{}{
"proxy_configured": false,
"success": false,
}
proxyURL := buildProxyURL(db)
result["proxy_url"] = proxyURL
if proxyURL != "" {
result["proxy_configured"] = true
proxyClient := buildHTTPClientWithProxy(proxyURL)
ip, country, city, isp, status, err := fetchIPInfo(proxyClient)
if err != nil {
result["proxy_status"] = "fail"
result["proxy_ip"] = err.Error()
} else {
result["proxy_ip"] = ip
result["proxy_country"] = country
result["proxy_city"] = city
result["proxy_isp"] = isp
result["proxy_status"] = status
if status == "success" {
result["success"] = true
}
}
}
// Direct connection test
directClient := &http.Client{Timeout: 15 * time.Second}
ip, country, city, isp, status, err := fetchIPInfo(directClient)
if err != nil {
result["direct_status"] = "fail"
result["direct_ip"] = err.Error()
} else {
result["direct_ip"] = ip
result["direct_country"] = country
result["direct_city"] = city
result["direct_isp"] = isp
result["direct_status"] = status
}
return result
}