- 后端: FastAPI + SQLAlchemy + SQLite, JWT认证, 代理支持的AirwallexClient - 前端: React 18 + Vite + Ant Design 5, 中文界面 - 功能: 卡片管理, 持卡人管理, 交易记录, API令牌, 系统设置, 审计日志 - 第三方API: X-API-Key认证, 权限控制 - Docker部署: docker-compose编排前后端
259 lines
9.0 KiB
Python
259 lines
9.0 KiB
Python
"""Airwallex API service layer."""
|
|
import json
|
|
import logging
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.db_models import SystemSetting
|
|
from app.proxy_client import ProxiedAirwallexClient
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Cached client instance
|
|
_client_instance: Optional[ProxiedAirwallexClient] = None
|
|
_client_config_hash: Optional[str] = None
|
|
|
|
|
|
def _get_setting(db: Session, key: str, default: str = "") -> str:
|
|
"""Get a system setting value."""
|
|
setting = db.query(SystemSetting).filter(SystemSetting.key == key).first()
|
|
return setting.value if setting else default
|
|
|
|
|
|
def _build_proxy_url(db: Session) -> Optional[str]:
|
|
"""Build proxy URL from settings."""
|
|
proxy_ip = _get_setting(db, "proxy_ip")
|
|
proxy_port = _get_setting(db, "proxy_port")
|
|
if not proxy_ip or not proxy_port:
|
|
return None
|
|
proxy_user = _get_setting(db, "proxy_username")
|
|
proxy_pass = _get_setting(db, "proxy_password")
|
|
if proxy_user and proxy_pass:
|
|
return f"http://{proxy_user}:{proxy_pass}@{proxy_ip}:{proxy_port}"
|
|
return f"http://{proxy_ip}:{proxy_port}"
|
|
|
|
|
|
def get_client(db: Session) -> ProxiedAirwallexClient:
|
|
"""Get or create an Airwallex client with current settings."""
|
|
global _client_instance, _client_config_hash
|
|
|
|
client_id = _get_setting(db, "airwallex_client_id")
|
|
api_key = _get_setting(db, "airwallex_api_key")
|
|
base_url = _get_setting(db, "airwallex_base_url", "https://api.airwallex.com/")
|
|
proxy_url = _build_proxy_url(db)
|
|
|
|
if not client_id or not api_key:
|
|
raise ValueError("Airwallex credentials not configured. Please set client_id and api_key in Settings.")
|
|
|
|
config_hash = f"{client_id}:{api_key}:{base_url}:{proxy_url}"
|
|
if _client_instance and _client_config_hash == config_hash:
|
|
return _client_instance
|
|
|
|
# Close old client
|
|
if _client_instance:
|
|
try:
|
|
_client_instance.close()
|
|
except Exception:
|
|
pass
|
|
|
|
_client_instance = ProxiedAirwallexClient(
|
|
proxy_url=proxy_url,
|
|
client_id=client_id,
|
|
api_key=api_key,
|
|
base_url=base_url,
|
|
)
|
|
_client_config_hash = config_hash
|
|
return _client_instance
|
|
|
|
|
|
def ensure_authenticated(db: Session) -> ProxiedAirwallexClient:
|
|
"""Get client and ensure it's authenticated."""
|
|
client = get_client(db)
|
|
client.authenticate()
|
|
return client
|
|
|
|
|
|
# ─── Card operations ────────────────────────────────────────────────
|
|
|
|
def list_cards(
|
|
db: Session,
|
|
page_num: int = 0,
|
|
page_size: int = 20,
|
|
cardholder_id: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
) -> Dict[str, Any]:
|
|
"""List cards with optional filters."""
|
|
client = ensure_authenticated(db)
|
|
params: Dict[str, Any] = {"page_num": page_num, "page_size": page_size}
|
|
if cardholder_id:
|
|
params["cardholder_id"] = cardholder_id
|
|
if status:
|
|
params["status"] = status
|
|
|
|
cards = client.issuing_card.list_with_filters(**params)
|
|
return {
|
|
"items": [c.to_dict() if hasattr(c, "to_dict") else c.__dict__ for c in cards],
|
|
"page_num": page_num,
|
|
"page_size": page_size,
|
|
"has_more": len(cards) == page_size,
|
|
}
|
|
|
|
|
|
def create_card(db: Session, card_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Create a new card."""
|
|
from airwallex.models.issuing_card import CardCreateRequest
|
|
|
|
client = ensure_authenticated(db)
|
|
request = CardCreateRequest(**card_data)
|
|
card = client.issuing_card.create_card(request)
|
|
return card.to_dict() if hasattr(card, "to_dict") else card.__dict__
|
|
|
|
|
|
def get_card(db: Session, card_id: str) -> Dict[str, Any]:
|
|
"""Get card by ID."""
|
|
client = ensure_authenticated(db)
|
|
card = client.issuing_card.get(card_id)
|
|
return card.to_dict() if hasattr(card, "to_dict") else card.__dict__
|
|
|
|
|
|
def get_card_details(db: Session, card_id: str) -> Dict[str, Any]:
|
|
"""Get sensitive card details (card number, CVV, expiry)."""
|
|
client = ensure_authenticated(db)
|
|
details = client.issuing_card.get_card_details(card_id)
|
|
return details.to_dict() if hasattr(details, "to_dict") else details.__dict__
|
|
|
|
|
|
def update_card(db: Session, card_id: str, update_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Update card attributes."""
|
|
from airwallex.models.issuing_card import CardUpdateRequest
|
|
|
|
client = ensure_authenticated(db)
|
|
request = CardUpdateRequest(**update_data)
|
|
card = client.issuing_card.update_card(card_id, request)
|
|
return card.to_dict() if hasattr(card, "to_dict") else card.__dict__
|
|
|
|
|
|
# ─── Cardholder operations ──────────────────────────────────────────
|
|
|
|
def list_cardholders(
|
|
db: Session,
|
|
page_num: int = 0,
|
|
page_size: int = 20,
|
|
) -> Dict[str, Any]:
|
|
"""List cardholders."""
|
|
client = ensure_authenticated(db)
|
|
cardholders = client.issuing_cardholder.list(page_num=page_num, page_size=page_size)
|
|
return {
|
|
"items": [c.to_dict() if hasattr(c, "to_dict") else c.__dict__ for c in cardholders],
|
|
"page_num": page_num,
|
|
"page_size": page_size,
|
|
"has_more": len(cardholders) == page_size,
|
|
}
|
|
|
|
|
|
def create_cardholder(db: Session, cardholder_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Create a new cardholder."""
|
|
from airwallex.models.issuing_cardholder import CardholderCreateRequest
|
|
|
|
client = ensure_authenticated(db)
|
|
request = CardholderCreateRequest(**cardholder_data)
|
|
cardholder = client.issuing_cardholder.create_cardholder(request)
|
|
return cardholder.to_dict() if hasattr(cardholder, "to_dict") else cardholder.__dict__
|
|
|
|
|
|
# ─── Transaction operations ─────────────────────────────────────────
|
|
|
|
def list_transactions(
|
|
db: Session,
|
|
page_num: int = 0,
|
|
page_size: int = 20,
|
|
card_id: Optional[str] = None,
|
|
from_created_at: Optional[str] = None,
|
|
to_created_at: Optional[str] = None,
|
|
) -> Dict[str, Any]:
|
|
"""List transactions."""
|
|
client = ensure_authenticated(db)
|
|
params: Dict[str, Any] = {"page_num": page_num, "page_size": page_size}
|
|
if card_id:
|
|
params["card_id"] = card_id
|
|
if from_created_at:
|
|
params["from_created_at"] = from_created_at
|
|
if to_created_at:
|
|
params["to_created_at"] = to_created_at
|
|
|
|
txns = client.issuing_transaction.list_with_filters(**params)
|
|
return {
|
|
"items": [t.to_dict() if hasattr(t, "to_dict") else t.__dict__ for t in txns],
|
|
"page_num": page_num,
|
|
"page_size": page_size,
|
|
"has_more": len(txns) == page_size,
|
|
}
|
|
|
|
|
|
def list_authorizations(
|
|
db: Session,
|
|
page_num: int = 0,
|
|
page_size: int = 20,
|
|
card_id: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
from_created_at: Optional[str] = None,
|
|
to_created_at: Optional[str] = None,
|
|
) -> Dict[str, Any]:
|
|
"""List authorizations."""
|
|
client = ensure_authenticated(db)
|
|
params: Dict[str, Any] = {"page_num": page_num, "page_size": page_size}
|
|
if card_id:
|
|
params["card_id"] = card_id
|
|
if status:
|
|
params["status"] = status
|
|
if from_created_at:
|
|
params["from_created_at"] = from_created_at
|
|
if to_created_at:
|
|
params["to_created_at"] = to_created_at
|
|
|
|
auths = client.issuing_authorization.list_with_filters(**params)
|
|
return {
|
|
"items": [a.to_dict() if hasattr(a, "to_dict") else a.__dict__ for a in auths],
|
|
"page_num": page_num,
|
|
"page_size": page_size,
|
|
"has_more": len(auths) == page_size,
|
|
}
|
|
|
|
|
|
# ─── Account / Balance ──────────────────────────────────────────────
|
|
|
|
def get_balance(db: Session) -> Optional[Dict[str, Any]]:
|
|
"""Get account balance. Returns None if not available."""
|
|
try:
|
|
client = ensure_authenticated(db)
|
|
# Try to get global account balance via the balances endpoint
|
|
response = client._request("GET", "api/v1/balances/current")
|
|
return response.json()
|
|
except Exception as e:
|
|
logger.warning("Failed to fetch balance: %s", e)
|
|
return None
|
|
|
|
|
|
# ─── Config ──────────────────────────────────────────────────────────
|
|
|
|
def get_issuing_config(db: Session) -> Optional[Dict[str, Any]]:
|
|
"""Get issuing configuration."""
|
|
try:
|
|
client = ensure_authenticated(db)
|
|
config = client.issuing_config.get_config()
|
|
return config.to_dict() if hasattr(config, "to_dict") else config.__dict__
|
|
except Exception as e:
|
|
logger.warning("Failed to fetch issuing config: %s", e)
|
|
return None
|
|
|
|
|
|
def test_connection(db: Session) -> Dict[str, Any]:
|
|
"""Test Airwallex API connection."""
|
|
try:
|
|
client = get_client(db)
|
|
client.authenticate()
|
|
return {"success": True, "message": "Connection successful"}
|
|
except Exception as e:
|
|
return {"success": False, "message": str(e)}
|