关于虚拟货币兑换页面的经历及 API 调用失败问题总结

有能力的自己部署试试!反正小白的我最终也没能解决这个问题…

图片[1]-关于虚拟货币兑换页面的经历及 API 调用失败问题总结-Baili Blog

我基于自身需求制作了一款虚拟货币兑换页面,页面支持 BTC、ETH、USDT 等 8 种主流虚拟货币的双向兑换,包含金额输入、币种互换、实时汇率提示和汇率走势图等核心功能,有相关技术能力的朋友可以自行部署使用。不过在部署这类页面的过程中,API 调用失败是较为常见的问题,可能导致页面输入无响应、币种切换失效、走势图空白等情况,以下整理了 API 调用失败的几类常见诱因,供大家参考:

1.首先是网络访问层面的限制。虚拟货币相关 API 多分为国内外两类,若选择国外接口,国内服务器若无外网访问权限,会直接导致 API 请求无法抵达服务器;即便选择国内 API,也可能因服务器运营商的网络策略、防火墙拦截等问题,造成请求超时或被拒绝,最终无法获取汇率数据。

2.其次是服务器环境配置缺失。这类页面的 API 调用通常依赖 PHP 的 CURL 扩展,若服务器未安装或未启用该扩展,HTTP 请求无法正常发起,API 自然无法返回数据;此外,PHP 版本与代码不兼容、缺少必要的 SSL 证书配置等,也会导致 API 请求过程中出现连接错误,中断数据传输。

3.再者是 API 本身的访问规则限制。部分虚拟货币 API 会对请求频率、请求来源 IP 做限制,若部署后短时间内频繁刷新页面,可能触发接口的限流机制;同时,缺少合规的请求头(如 User-Agent)、API 参数格式错误等,也会被接口服务器判定为无效请求,拒绝返回数据。

4.还有文件部署的基础问题。将页面文件上传至服务器后,若文件权限设置不当(如未赋予 www 用户读写权限)、访问路径错误(如未通过 HTTP 协议访问,直接双击本地文件),会导致后端接口无法被正常调用,进而表现为 API 请求失败,页面交互功能全部失效。

以上是部署虚拟货币兑换页面时,API 调用失败的常见潜在原因。有部署需求的朋友可结合自身服务器环境,从这些维度提前排查,能有效减少页面无响应的情况。

代码

<?php
// ==================== PHP 后端逻辑 ====================
if (isset($_GET['action']) && $_GET['action'] === 'get_rates') {
    header("Access-Control-Allow-Origin: *");
    header("Content-Type: application/json; charset=UTF-8");

    // 兜底汇率(保证页面无论如何都能交互)
    $backup_rates = [
        'BTC'  => 70000,
        'ETH'  => 2100,
        'TRX'  => 0.35,
        'DOGE' => 0.093,
        'BNB'  => 647,
        'LTC'  => 54,
        'HTX'  => 0.000002,
        'USDT' => 1
    ];

    // 检查 CURL 是否可用
    if (!function_exists('curl_init')) {
        echo json_encode([
            'status' => 'success',
            'rates'  => $backup_rates,
            'msg'    => 'CURL 未开启,使用本地兜底汇率'
        ]);
        exit;
    }

    // ========== 使用币安 API 获取实时价格(更稳定) ==========
    // 币种与币安交易对的映射(都是对 USDT 的交易对)
    $coin_map = [
        'BTC'  => 'BTCUSDT',
        'ETH'  => 'ETHUSDT',
        'TRX'  => 'TRXUSDT',
        'DOGE' => 'DOGEUSDT',
        'BNB'  => 'BNBUSDT',
        'LTC'  => 'LTCUSDT',
        'HTX'  => 'HTUSDT',
        'USDT' => 'USDTUSDT'  // 这个交易对不存在,我们会特殊处理
    ];

    // 构建请求 URL:只请求实际存在的交易对
    $symbols = array_values($coin_map);
    $api_symbols = array_filter($symbols, function($s) { return $s !== 'USDTUSDT'; });
    $api_url = "https://api.binance.com/api/v3/ticker/price?symbols=" . urlencode('["' . implode('","', $api_symbols) . '"]');

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $api_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 5);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    ]);
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    // 处理币安返回的数据
    if ($http_code === 200 && $response) {
        $data = json_decode($response, true);
        if (is_array($data)) {
            // 将返回的数组转换成 symbol => price 的映射
            $prices = [];
            foreach ($data as $item) {
                $prices[$item['symbol']] = floatval($item['price']);
            }

            $result = [];
            foreach ($coin_map as $short => $symbol) {
                if ($short === 'USDT') {
                    $result['USDT'] = 1.0;  // USDT 价格固定为 1
                } else {
                    $result[$short] = $prices[$symbol] ?? $backup_rates[$short];
                }
            }

            echo json_encode([
                'status' => 'success',
                'rates'  => $result,
                'time'   => date('Y-m-d H:i:s')
            ]);
            exit;
        }
    }

    // 如果币安 API 也失败了,用兜底汇率
    echo json_encode([
        'status' => 'success',
        'rates'  => $backup_rates,
        'msg'    => 'API 访问失败,使用本地兜底汇率'
    ]);
    exit;
}

// ==================== HTML 前端代码 ====================
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数字货币汇率换算器</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
    <script src="https://cdn.bootcdn.net/ajax/libs/chart.js/4.4.8/chart.umd.js"></script>
    <style>
        body {
            background-color: #f8f9fa;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            padding: 40px 20px;
        }
        .container {
            max-width: 1000px;
            margin: 0 auto;
        }
        h1 {
            text-align: center;
            font-weight: 600;
            color: #212529;
            margin-bottom: 40px;
        }
        .exchange-card {
            background: #fff;
            border-radius: 12px;
            padding: 24px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            margin-bottom: 30px;
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 20px;
        }
        .coin-input-group {
            flex: 1;
            min-width: 280px;
        }
        .coin-selector {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-bottom: 8px;
        }
        .coin-icon {
            width: 24px;
            height: 24px;
            border-radius: 50%;
            object-fit: cover;
        }
        .coin-name {
            font-weight: 500;
            color: #212529;
            display: flex;
            flex-direction: column;
        }
        .coin-name small {
            font-size: 12px;
            color: #6c757d;
            font-weight: 400;
        }
        .coin-amount {
            width: 100%;
            border: none;
            font-size: 24px;
            font-weight: 600;
            text-align: right;
            padding: 8px 0;
            background: transparent;
            outline: none;
        }
        .swap-btn {
            background: #e9ecef;
            border: none;
            width: 40px;
            height: 40px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            flex-shrink: 0;
        }
        .swap-btn:hover {
            background: #dee2e6;
        }
        .rate-tip {
            font-size: 14px;
            color: #6c757d;
            margin-top: 8px;
        }
        .tip-box {
            background: #e9f5ff;
            color: #0066cc;
            padding: 10px;
            border-radius: 8px;
            margin-bottom: 20px;
            text-align: center;
            font-size: 14px;
        }
        .chart-card {
            background: #fff;
            border-radius: 12px;
            padding: 24px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
        }
        .chart-title {
            font-size: 18px;
            font-weight: 500;
            margin-bottom: 20px;
            color: #212529;
        }
        #priceChart {
            width: 100%;
            height: 400px;
        }
        @media (max-width: 768px) {
            .exchange-card {
                flex-direction: column;
                gap: 20px;
            }
            .coin-input-group {
                width: 100%;
            }
            .coin-amount {
                font-size: 20px;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>数字货币汇率换算器</h1>
        
        <!-- 提示框(显示当前汇率来源) -->
        <div id="tipBox" class="tip-box" style="display: none;"></div>

        <!-- 兑换核心区域 -->
        <div class="exchange-card">
            <div class="coin-input-group">
                <div class="coin-selector">
                    <img src="https://cdn.jsdelivr.net/gh/atomiclabs/cryptocurrency-icons@1a63530be6e374711a8554f31b17e4cb92c25fa5/32/color/btc.png" 
                         class="coin-icon" id="fromCoinIcon">
                    <div class="coin-name">
                        <span id="fromCoinCode">BTC</span>
                        <small id="fromCoinName">比特币</small>
                    </div>
                    <select id="fromCoin" class="form-select form-select-sm ms-auto" style="width: auto;">
                        <option value="BTC" data-name="比特币" data-icon="btc.png">BTC</option>
                        <option value="ETH" data-name="以太坊" data-icon="eth.png">ETH</option>
                        <option value="TRX" data-name="波场" data-icon="trx.png">TRX</option>
                        <option value="DOGE" data-name="狗狗币" data-icon="doge.png">DOGE</option>
                        <option value="BNB" data-name="币安币" data-icon="bnb.png">BNB</option>
                        <option value="LTC" data-name="莱特币" data-icon="ltc.png">LTC</option>
                        <option value="HTX" data-name="火币币" data-icon="ht.png">HTX</option>
                        <option value="USDT" data-name="泰达币" data-icon="usdt.png">USDT</option>
                    </select>
                </div>
                <input type="number" id="amount" class="coin-amount" value="10" min="0.00000001" step="0.00000001">
                <div class="rate-tip" id="fromRateTip">1 BTC ≈ 0 USDT</div>
            </div>

            <button class="swap-btn" id="swapBtn">
                <i class="fas fa-exchange-alt"></i>
            </button>

            <div class="coin-input-group">
                <div class="coin-selector">
                    <img src="https://cdn.jsdelivr.net/gh/atomiclabs/cryptocurrency-icons@1a63530be6e374711a8554f31b17e4cb92c25fa5/32/color/usdt.png" 
                         class="coin-icon" id="toCoinIcon">
                    <div class="coin-name">
                        <span id="toCoinCode">USDT</span>
                        <small id="toCoinName">泰达币</small>
                    </div>
                    <select id="toCoin" class="form-select form-select-sm ms-auto" style="width: auto;">
                        <option value="USDT" data-name="泰达币" data-icon="usdt.png">USDT</option>
                        <option value="BTC" data-name="比特币" data-icon="btc.png">BTC</option>
                        <option value="ETH" data-name="以太坊" data-icon="eth.png">ETH</option>
                        <option value="TRX" data-name="波场" data-icon="trx.png">TRX</option>
                        <option value="DOGE" data-name="狗狗币" data-icon="doge.png">DOGE</option>
                        <option value="BNB" data-name="币安币" data-icon="bnb.png">BNB</option>
                        <option value="LTC" data-name="莱特币" data-icon="ltc.png">LTC</option>
                        <option value="HTX" data-name="火币币" data-icon="ht.png">HTX</option>
                    </select>
                </div>
                <input type="number" id="resultAmount" class="coin-amount" readonly value="0">
                <div class="rate-tip" id="toRateTip">1 USDT ≈ 0 BTC</div>
            </div>
        </div>

        <!-- 汇率走势图 -->
        <div class="chart-card">
            <div class="chart-title">汇率走势图</div>
            <canvas id="priceChart"></canvas>
        </div>
    </div>

<script>
    // ========== 全局变量 ==========
    let coinRates = { // 兜底汇率
        BTC: 70000, ETH: 3800, TRX: 0.05, DOGE: 0.12,
        BNB: 500, LTC: 120, HTX: 8, USDT: 1
    };
    let priceHistory = [];
    let chart = null;

    const coinInfo = {
        BTC: { name: '比特币', icon: 'btc.png' },
        ETH: { name: '以太坊', icon: 'eth.png' },
        TRX: { name: '波场', icon: 'trx.png' },
        DOGE: { name: '狗狗币', icon: 'doge.png' },
        BNB: { name: '币安币', icon: 'bnb.png' },
        LTC: { name: '莱特币', icon: 'ltc.png' },
        HTX: { name: '火币', icon: 'ht.png' },
        USDT: { name: '泰达币', icon: 'usdt.png' }
    };

    // ========== 工具函数 ==========
    function showTip(msg) {
        const tipBox = document.getElementById('tipBox');
        if (!tipBox) return;
        tipBox.textContent = msg;
        tipBox.style.display = 'block';
    }

    // 安全地获取元素值
    function getFromCoin() { return document.getElementById('fromCoin')?.value || 'BTC'; }
    function getToCoin()   { return document.getElementById('toCoin')?.value || 'USDT'; }
    function getAmount()   { return parseFloat(document.getElementById('amount')?.value) || 0; }

    // ========== 核心业务函数 ==========
    function updateRateTips() {
        try {
            const fromCoin = getFromCoin();
            const toCoin = getToCoin();
            const fromRate = (coinRates[fromCoin] / coinRates[toCoin]).toFixed(8);
            const toRate = (coinRates[toCoin] / coinRates[fromCoin]).toFixed(8);
            const fromTip = document.getElementById('fromRateTip');
            const toTip = document.getElementById('toRateTip');
            if (fromTip) fromTip.textContent = `1 ${fromCoin} ≈ ${fromRate} ${toCoin}`;
            if (toTip) toTip.textContent = `1 ${toCoin} ≈ ${toRate} ${fromCoin}`;
        } catch (e) { console.warn('更新汇率提示失败', e); }
    }

    function calculate() {
        try {
            const fromCoin = getFromCoin();
            const toCoin = getToCoin();
            const amount = getAmount();
            const total = (amount * coinRates[fromCoin] / coinRates[toCoin]).toFixed(8);
            const resultInput = document.getElementById('resultAmount');
            if (resultInput) resultInput.value = total;
        } catch (e) { console.warn('计算失败', e); }
    }

    function updateCoinSelector() {
        try {
            // 更新图标和名称
            const fromCoin = getFromCoin();
            const toCoin = getToCoin();
            const fromIcon = document.getElementById('fromCoinIcon');
            const toIcon = document.getElementById('toCoinIcon');
            const fromCode = document.getElementById('fromCoinCode');
            const toCode = document.getElementById('toCoinCode');
            const fromName = document.getElementById('fromCoinName');
            const toName = document.getElementById('toCoinName');

            if (fromCode) fromCode.textContent = fromCoin;
            if (toCode) toCode.textContent = toCoin;
            if (fromName) fromName.textContent = coinInfo[fromCoin]?.name || '';
            if (toName) toName.textContent = coinInfo[toCoin]?.name || '';

            const setIcon = (imgEl, coin) => {
                if (!imgEl) return;
                const url = `https://cdn.jsdelivr.net/gh/atomiclabs/cryptocurrency-icons@1a63530be6e374711a8554f31b17e4cb92c25fa5/32/color/${coinInfo[coin]?.icon || 'generic.png'}`;
                imgEl.src = url;
                imgEl.onerror = function() {
                    // 如果 CDN 图标加载失败,隐藏图标或使用占位符
                    this.style.display = 'none';
                    // 也可以换成 base64 占位图,但简单起见隐藏
                };
            };
            setIcon(fromIcon, fromCoin);
            setIcon(toIcon, toCoin);

            // 重新计算与更新提示
            calculate();
            updateRateTips();
        } catch (e) { console.warn('更新选择器失败', e); }
    }

    // ========== 图表相关(安全初始化) ==========
    function initChart() {
        // 首先检查 Chart.js 是否已加载
        if (typeof Chart === 'undefined') {
            console.warn('Chart.js 未加载,图表功能不可用');
            showTip('图表库未加载,走势图暂时不可用');
            return;
        }
        const canvas = document.getElementById('priceChart');
        if (!canvas) {
            console.warn('找不到 priceChart 元素');
            return;
        }
        try {
            const ctx = canvas.getContext('2d');
            chart = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: [],
                    datasets: [{
                        label: '兑换汇率',
                        data: [],
                        borderColor: '#f9b115',
                        backgroundColor: 'rgba(249, 177, 21, 0.1)',
                        borderWidth: 2,
                        tension: 0.4,
                        fill: true
                    }]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    scales: {
                        y: { title: { display: true, text: '兑换价格' }, grid: { color: 'rgba(0,0,0,0.05)' } },
                        x: { grid: { display: false } }
                    },
                    plugins: { legend: { display: false } }
                }
            });
            // 立即添加一个初始数据点,防止空白
            updateChart();
        } catch (e) {
            console.error('图表初始化失败', e);
            showTip('图表加载失败,但不影响汇率换算');
            chart = null;
        }
    }

    function updateChart() {
        if (!chart) return;
        try {
            const fromCoin = getFromCoin();
            const toCoin = getToCoin();
            const currentRate = coinRates[fromCoin] / coinRates[toCoin];
            const now = new Date();
            const timeLabel = `${now.getMinutes().toString().padStart(2,'0')}:${now.getSeconds().toString().padStart(2,'0')}`;
            priceHistory.push({ time: timeLabel, rate: currentRate });
            if (priceHistory.length > 30) priceHistory.shift();
            chart.data.labels = priceHistory.map(item => item.time);
            chart.data.datasets[0].data = priceHistory.map(item => item.rate);
            chart.update();
        } catch (e) { console.warn('更新图表失败', e); }
    }

    // ========== 数据获取 ==========
    async function fetchRates() {
        try {
            const currentUrl = window.location.href.split('?')[0];
            const res = await fetch(currentUrl + '?action=get_rates');
            const data = await res.json();
            if (data.status === 'success' && data.rates) {
                coinRates = data.rates;
                showTip(data.msg || '使用实时汇率数据');
            } else {
                showTip('使用本地兜底汇率(API返回异常)');
            }
        } catch (err) {
            showTip('使用本地兜底汇率(网络请求失败)');
        }
        // 不管成功与否,都刷新界面
        updateRateTips();
        calculate();
        updateChart();
    }

    function swapCoins() {
        const fromSelect = document.getElementById('fromCoin');
        const toSelect = document.getElementById('toCoin');
        const amountInput = document.getElementById('amount');
        const resultInput = document.getElementById('resultAmount');
        if (!fromSelect || !toSelect) return;
        // 交换币种的同时交换金额
        const oldFrom = fromSelect.value;
        const oldAmount = amountInput?.value || '0';
        const oldResult = resultInput?.value || '0';
        fromSelect.value = toSelect.value;
        toSelect.value = oldFrom;
        if (amountInput) amountInput.value = oldResult;
        if (resultInput) resultInput.value = oldAmount;
        updateCoinSelector(); // 里面会调用 calculate 和 updateRateTips
    }

    // ========== 初始化入口 ==========
    window.addEventListener('load', function() {
        // 1. 先尝试初始化图表(即使失败也不影响后续)
        initChart();

        // 2. 绑定事件(这里确保一定执行)
        const fromSelect = document.getElementById('fromCoin');
        const toSelect = document.getElementById('toCoin');
        const amountInput = document.getElementById('amount');
        const swapBtn = document.getElementById('swapBtn');

        if (fromSelect) fromSelect.onchange = updateCoinSelector;
        if (toSelect) toSelect.onchange = updateCoinSelector;
        if (amountInput) amountInput.oninput = calculate;
        if (swapBtn) swapBtn.onclick = swapCoins;

        // 3. 初始化界面显示
        updateCoinSelector();   // 设置初始图标、名称
        calculate();
        updateRateTips();

        // 4. 拉取真实汇率并开启定时刷新
        fetchRates();
        setInterval(fetchRates, 15000); // 改为15秒,减少请求频率

        // 额外:如果图表成功,之后每15秒更新走势图(与汇率同步)
    });
</script>
</body>
</html>
© 版权声明
THE END
喜欢就支持一下吧
点赞11赞赏 分享
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容