feat: Airwallex 发卡管理后台完整实现

- 后端: FastAPI + SQLAlchemy + SQLite, JWT认证, 代理支持的AirwallexClient
- 前端: React 18 + Vite + Ant Design 5, 中文界面
- 功能: 卡片管理, 持卡人管理, 交易记录, API令牌, 系统设置, 审计日志
- 第三方API: X-API-Key认证, 权限控制
- Docker部署: docker-compose编排前后端
This commit is contained in:
zqq61
2026-03-15 23:05:08 +08:00
commit 4f53889a8e
98 changed files with 10847 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
"""Cards management router."""
import json
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy.orm import Session
from sqlalchemy import func
from datetime import datetime, timezone
from app.auth import get_current_user, AdminUser
from app.database import get_db
from app.models.db_models import CardLog, SystemSetting
from app.services import airwallex_service
from app.services.audit_log import create_card_log, create_audit_log
router = APIRouter(prefix="/api/cards", tags=["cards"])
@router.get("")
def list_cards(
page_num: int = Query(0, ge=0),
page_size: int = Query(20, ge=1, le=100),
cardholder_id: Optional[str] = None,
status: Optional[str] = None,
db: Session = Depends(get_db),
user: AdminUser = Depends(get_current_user),
):
"""List cards with optional filters."""
return airwallex_service.list_cards(db, page_num, page_size, cardholder_id, status)
@router.post("")
def create_card(
request: Request,
card_data: dict,
db: Session = Depends(get_db),
user: AdminUser = Depends(get_current_user),
):
"""Create a new card."""
# Check daily limit
today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
today_count = (
db.query(func.count(CardLog.id))
.filter(CardLog.action == "create_card", CardLog.status == "success", CardLog.created_at >= today_start)
.scalar()
) or 0
limit_setting = db.query(SystemSetting).filter(SystemSetting.key == "daily_card_limit").first()
daily_limit = int(limit_setting.value) if limit_setting else 100
if today_count >= daily_limit:
raise HTTPException(status_code=429, detail=f"Daily card creation limit ({daily_limit}) reached")
try:
result = airwallex_service.create_card(db, card_data)
create_card_log(
db,
action="create_card",
status="success",
operator=user.username,
card_id=result.get("card_id", result.get("id", "")),
cardholder_id=card_data.get("cardholder_id", ""),
request_data=json.dumps(card_data),
response_data=json.dumps(result, default=str),
)
create_audit_log(
db,
action="create_card",
resource_type="card",
resource_id=result.get("card_id", result.get("id", "")),
operator=user.username,
ip_address=request.client.host if request.client else "",
details=f"Created card for cardholder {card_data.get('cardholder_id', '')}",
)
return result
except Exception as e:
create_card_log(
db,
action="create_card",
status="failed",
operator=user.username,
cardholder_id=card_data.get("cardholder_id", ""),
request_data=json.dumps(card_data),
response_data=str(e),
)
raise HTTPException(status_code=400, detail=str(e))
@router.get("/{card_id}")
def get_card(
card_id: str,
db: Session = Depends(get_db),
user: AdminUser = Depends(get_current_user),
):
"""Get card by ID."""
try:
return airwallex_service.get_card(db, card_id)
except Exception as e:
raise HTTPException(status_code=404, detail=str(e))
@router.get("/{card_id}/details")
def get_card_details(
card_id: str,
request: Request,
db: Session = Depends(get_db),
user: AdminUser = Depends(get_current_user),
):
"""Get sensitive card details (card number, CVV, expiry)."""
create_audit_log(
db,
action="view_card_details",
resource_type="card",
resource_id=card_id,
operator=user.username,
ip_address=request.client.host if request.client else "",
)
try:
return airwallex_service.get_card_details(db, card_id)
except Exception as e:
raise HTTPException(status_code=404, detail=str(e))
@router.put("/{card_id}")
def update_card(
card_id: str,
update_data: dict,
request: Request,
db: Session = Depends(get_db),
user: AdminUser = Depends(get_current_user),
):
"""Update card attributes."""
try:
result = airwallex_service.update_card(db, card_id, update_data)
create_audit_log(
db,
action="update_card",
resource_type="card",
resource_id=card_id,
operator=user.username,
ip_address=request.client.host if request.client else "",
details=json.dumps(update_data),
)
return result
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))