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 }