statsFile)) { $this->initStats(); } // 初始化配置文件(如果不存在) if (!file_exists($this->configFile)) { $this->initConfig(); } // 初始化历史数据文件(如果不存在) if (!file_exists($this->historyFile)) { $this->initHistory(); } // 初始化操作日志文件(如果不存在) if (!file_exists($this->operationFile)) { $this->initOperationLog(); } // 初始化每日数据文件(如果不存在) if (!file_exists($this->dailyDataFile)) { $this->initDailyData(); } } /** * 初始化统计数据 */ private function initStats() { $initialStats = [ 'total_visits' => 0, 'today_visits' => 0, 'unique_visitors' => 0, 'today_unique_visitors' => 0, 'last_reset' => date('Y-m-d'), 'daily_stats' => [], 'unique_ips' => [] ]; file_put_contents($this->statsFile, json_encode($initialStats, JSON_PRETTY_PRINT)); } /** * 初始化配置文件 */ private function initConfig() { $defaultConfig = [ [ "channel" => "0", "fund_code" => "003766", "investment" => 1000 ], [ "channel" => "0", "fund_code" => "004206", "investment" => 1000 ], [ "channel" => "0", "fund_code" => "019432", "investment" => 1000 ], [ "channel" => "1", "fund_code" => "003766", "investment" => 1000 ], [ "channel" => "1", "fund_code" => "008327", "investment" => 1000 ], [ "channel" => "2", "fund_code" => "017811", "investment" => 1000 ] ]; file_put_contents($this->configFile, json_encode($defaultConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } /** * 初始化历史数据 */ private function initHistory() { $initialHistory = [ 'last_update_date' => date('Y-m-d'), 'yesterday_data' => [], 'today_data' => [] ]; file_put_contents($this->historyFile, json_encode($initialHistory, JSON_PRETTY_PRINT)); } /** * 初始化操作日志 */ private function initOperationLog() { $initialLog = [ 'operations' => [] ]; file_put_contents($this->operationFile, json_encode($initialLog, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } /** * 初始化每日数据 */ private function initDailyData() { $initialDailyData = [ 'last_update' => date('Y-m-d'), 'funds' => [] ]; file_put_contents($this->dailyDataFile, json_encode($initialDailyData, JSON_PRETTY_PRINT)); } /** * 加载基金配置 */ private function loadConfig() { if (!file_exists($this->configFile)) { $this->initConfig(); } $data = file_get_contents($this->configFile); if (!$data) { return []; } $config = json_decode($data, true); return is_array($config) ? $config : []; } /** * 加载历史数据 */ private function loadHistory() { if (!file_exists($this->historyFile)) { $this->initHistory(); } $data = file_get_contents($this->historyFile); if (!$data) { return $this->initHistory(); } $history = json_decode($data, true); return is_array($history) ? $history : $this->initHistory(); } /** * 保存历史数据 */ private function saveHistory($history) { return file_put_contents($this->historyFile, json_encode($history, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } /** * 更新历史数据 */ private function updateHistory($fundsData) { $history = $this->loadHistory(); $today = date('Y-m-d'); // 确保数组结构完整 if (!isset($history['yesterday_data']) || !is_array($history['yesterday_data'])) { $history['yesterday_data'] = []; } if (!isset($history['today_data']) || !is_array($history['today_data'])) { $history['today_data'] = []; } if (!isset($history['last_update_date'])) { $history['last_update_date'] = $today; } // 如果是新的一天,将昨天的数据移到历史记录中 if ($history['last_update_date'] !== $today) { // 将昨天的数据保存为历史数据 // 确保今天的数据不为空才移动,避免覆盖已有的历史数据 if (!empty($history['today_data'])) { $history['yesterday_data'] = $history['today_data']; } // 清空今天的数据,为新数据做准备 $history['today_data'] = []; // 更新最后更新日期 $history['last_update_date'] = $today; } // 保存今天的数据 foreach ($fundsData as $fundCode => $fund) { $history['today_data'][$fundCode] = [ 'date' => $today, 'change' => floatval($fund['gszzl']), 'name' => $fund['name'] ?? '未知基金' ]; } // 确保保存操作成功 if (!$this->saveHistory($history)) { // 保存失败时记录错误信息(可以根据需要扩展错误处理) error_log('Failed to save history data on ' . $today); } return $history; } /** * 获取基金的历史涨幅(昨天的数据) */ private function getFundHistory($fundCode) { $history = $this->loadHistory(); if (isset($history['yesterday_data'][$fundCode])) { return $history['yesterday_data'][$fundCode]['change']; } return null; // 没有历史数据 } /** * 缓存路径 */ private function getFundCachePath($fundCode) { return $this->cacheDir . "/fund_{$fundCode}.json"; } /** * 从缓存读取基金数据(短期) */ private function getFundFromCache($fundCode) { $file = $this->getFundCachePath($fundCode); if (!file_exists($file)) return false; // 过期判断 if (time() - filemtime($file) > $this->fundCacheTTL) return false; $raw = @file_get_contents($file); if ($raw === false) return false; $data = json_decode($raw, true); return is_array($data) ? $data : false; } /** * 写入缓存 */ private function cacheFundData($fundCode, $fundData) { // 保证目录存在 if (!is_dir($this->cacheDir)) { @mkdir($this->cacheDir, 0777, true); } @file_put_contents($this->getFundCachePath($fundCode), json_encode($fundData, JSON_UNESCAPED_UNICODE)); } /** * 记录操作日志 */ private function logOperation($type, $fundCode, $channel = null, $investment = null, $details = '') { $log = $this->loadOperationLog(); $operation = [ 'id' => uniqid(), 'type' => $type, // 'add', 'update', 'delete' 'fund_code' => $fundCode, 'channel' => $channel, 'investment' => $investment, 'details' => $details, 'timestamp' => time(), 'date' => date('Y-m-d H:i:s') ]; $log['operations'][] = $operation; // 只保留最近100条操作记录 if (count($log['operations']) > 100) { $log['operations'] = array_slice($log['operations'], -100); } file_put_contents($this->operationFile, json_encode($log, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); return $operation; } /** * 加载操作日志 */ private function loadOperationLog() { if (!file_exists($this->operationFile)) { $this->initOperationLog(); } $data = file_get_contents($this->operationFile); if (!$data) { return ['operations' => []]; } $log = json_decode($data, true); return is_array($log) ? $log : ['operations' => []]; } /** * 记录每日基金数据 */ private function recordDailyData($fundsData) { $dailyData = $this->loadDailyData(); $today = date('Y-m-d'); // 如果是新的一天,清理旧数据(只保留5天) if ($dailyData['last_update'] !== $today) { $fiveDaysAgo = date('Y-m-d', strtotime('-5 days')); foreach ($dailyData['funds'] as $fundCode => &$fundHistory) { $fundHistory = array_filter($fundHistory, function($record) use ($fiveDaysAgo) { return $record['date'] >= $fiveDaysAgo; }); // 重新索引数组 $fundHistory = array_values($fundHistory); } $dailyData['last_update'] = $today; } // 记录今天的数据 foreach ($fundsData as $fundCode => $fund) { if (!isset($dailyData['funds'][$fundCode])) { $dailyData['funds'][$fundCode] = []; } $todayRecord = [ 'date' => $today, 'dwjz' => $fund['dwjz'], // 单位净值 'gsz' => $fund['gsz'], // 估算净值 'gszzl' => floatval($fund['gszzl']), // 估算涨幅 'name' => $fund['name'] ?? '未知基金' ]; // 检查是否已经记录了今天的数据 $todayExists = false; foreach ($dailyData['funds'][$fundCode] as $record) { if ($record['date'] === $today) { $todayExists = true; break; } } if (!$todayExists) { $dailyData['funds'][$fundCode][] = $todayRecord; // 按日期排序 usort($dailyData['funds'][$fundCode], function($a, $b) { return strtotime($a['date']) - strtotime($b['date']); }); } } file_put_contents($this->dailyDataFile, json_encode($dailyData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); return $dailyData; } /** * 加载每日数据 */ private function loadDailyData() { if (!file_exists($this->dailyDataFile)) { $this->initDailyData(); } $data = file_get_contents($this->dailyDataFile); if (!$data) { return ['last_update' => date('Y-m-d'), 'funds' => []]; } $dailyData = json_decode($data, true); return is_array($dailyData) ? $dailyData : ['last_update' => date('Y-m-d'), 'funds' => []]; } /** * 获取基金的历史数据(5天内) */ private function getFundHistoryData($fundCode) { $dailyData = $this->loadDailyData(); if (isset($dailyData['funds'][$fundCode])) { return $dailyData['funds'][$fundCode]; } return []; } /** * 获取操作日志 */ public function getOperationLog($limit = 20) { try { $log = $this->loadOperationLog(); $operations = $log['operations']; // 按时间倒序排列 usort($operations, function($a, $b) { return $b['timestamp'] - $a['timestamp']; }); // 限制返回数量 $operations = array_slice($operations, 0, $limit); return [ 'success' => true, 'data' => $operations ]; } catch (Exception $e) { return [ 'success' => false, 'message' => '获取操作日志失败: ' . $e->getMessage() ]; } } /** * 获取基金历史图表数据 */ public function getFundChartData($fundCode) { try { $historyData = $this->getFundHistoryData($fundCode); // 格式化图表数据 $chartData = [ 'labels' => [], 'netValues' => [], 'changes' => [] ]; foreach ($historyData as $record) { $chartData['labels'][] = date('m-d', strtotime($record['date'])); $chartData['netValues'][] = floatval($record['gsz']); $chartData['changes'][] = floatval($record['gszzl']); } return [ 'success' => true, 'data' => $chartData ]; } catch (Exception $e) { return [ 'success' => false, 'message' => '获取图表数据失败: ' . $e->getMessage() ]; } } /** * 记录访问 */ private function recordVisit() { $clientIP = $this->getClientIP(); $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown'; $timestamp = time(); $date = date('Y-m-d H:i:s'); // 检查是否需要记录这次访问(避免短时间内重复计数) if (!$this->shouldRecordVisit($clientIP, $timestamp)) { return false; // 不需要记录 } $visitorData = [ 'ip' => $clientIP, 'user_agent' => $userAgent, 'timestamp' => $timestamp, 'date' => $date, 'referer' => $_SERVER['HTTP_REFERER'] ?? 'Direct' ]; // 保存访问记录 $visits = $this->loadVisits(); $visits[] = $visitorData; // 只保留最近1000条记录,平衡数据完整性和存储压力 if (count($visits) > 1000) { $visits = array_slice($visits, -1000); } file_put_contents($this->dataFile, json_encode($visits, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // 更新统计 $this->updateStats($clientIP); return true; } /** * 判断是否应该记录这次访问 * 同一IP在10分钟内的访问只记录一次 */ private function shouldRecordVisit($clientIP, $currentTimestamp) { $visits = $this->loadVisits(); $timeThreshold = 10 * 60; // 10分钟(秒) // 检查最近的访问记录 for ($i = count($visits) - 1; $i >= 0; $i--) { $visit = $visits[$i]; // 如果找到相同IP且时间间隔小于阈值,则不记录 if ($visit['ip'] === $clientIP && ($currentTimestamp - $visit['timestamp']) < $timeThreshold) { return false; } // 只检查最近的记录,避免遍历所有数据 if (($currentTimestamp - $visit['timestamp']) > $timeThreshold) { break; } } return true; } /** * 获取客户端IP */ private function getClientIP() { $ip = 'Unknown'; if (!empty($_SERVER['HTTP_CLIENT_IP'])) { $ip = $_SERVER['HTTP_CLIENT_IP']; } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; } elseif (!empty($_SERVER['REMOTE_ADDR'])) { $ip = $_SERVER['REMOTE_ADDR']; } // 处理多个IP的情况(如经过代理) if (strpos($ip, ',') !== false) { $ips = explode(',', $ip); $ip = trim($ips[0]); } return $ip; } /** * 加载访问记录 */ private function loadVisits() { if (!file_exists($this->dataFile)) { return []; } $data = @file_get_contents($this->dataFile); if (!$data) { return []; } $visits = json_decode($data, true); return is_array($visits) ? $visits : []; } /** * 加载统计数据 */ private function loadStats() { if (!file_exists($this->statsFile)) { $this->initStats(); } $data = @file_get_contents($this->statsFile); if (!$data) { $this->initStats(); $data = file_get_contents($this->statsFile); } $stats = json_decode($data, true); // 确保数据结构完整 if (!isset($stats['unique_ips'])) { $stats['unique_ips'] = []; } return $stats; } /** * 更新统计数据 */ private function updateStats($clientIP) { $stats = $this->loadStats(); $visits = $this->loadVisits(); // 检查是否需要重置今日统计 $today = date('Y-m-d'); if ($stats['last_reset'] !== $today) { $stats['today_visits'] = 0; $stats['today_unique_visitors'] = 0; $stats['last_reset'] = $today; } // 更新总访问量 $stats['total_visits'] = count($visits); $stats['today_visits']++; // 更新独立访客统计 $allIPs = array_column($visits, 'ip'); $uniqueIPs = array_unique($allIPs); $stats['unique_visitors'] = count($uniqueIPs); // 更新今日独立访客 $todayVisits = array_filter($visits, function($visit) use ($today) { return date('Y-m-d', $visit['timestamp']) === $today; }); $todayIPs = array_unique(array_column($todayVisits, 'ip')); $stats['today_unique_visitors'] = count($todayIPs); // 更新每日统计 $todayKey = date('Y-m-d'); if (!isset($stats['daily_stats'][$todayKey])) { $stats['daily_stats'][$todayKey] = 0; } $stats['daily_stats'][$todayKey]++; // 只保留最近30天的统计 if (count($stats['daily_stats']) > 30) { $stats['daily_stats'] = array_slice($stats['daily_stats'], -30, 30, true); } // 保存唯一IP列表(用于快速计算) $stats['unique_ips'] = array_values($uniqueIPs); file_put_contents($this->statsFile, json_encode($stats, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); return $stats; } /** * 获取访问统计 */ public function getVisitStats() { $stats = $this->loadStats(); $visits = $this->loadVisits(); // 获取最近访问记录(最新的10条) $recentVisits = array_slice($visits, -10); $recentVisits = array_reverse($recentVisits); // 最新的在前面 return [ 'success' => true, 'data' => [ 'total_visits' => $stats['total_visits'], 'today_visits' => $stats['today_visits'], 'unique_visitors' => $stats['unique_visitors'], 'today_unique_visitors' => $stats['today_unique_visitors'], 'last_reset' => $stats['last_reset'], 'daily_stats' => $stats['daily_stats'], 'recent_visits' => $recentVisits ] ]; } /** * 批量获取基金数据 */ public function getBatchFundData($fundCodes) { $fundsData = []; $errors = []; foreach ($fundCodes as $fundCode) { $fundData = $this->getSingleFundData($fundCode); if ($fundData) { $fundsData[$fundCode] = $fundData; } else { $errors[] = "基金 {$fundCode} 数据获取失败"; } // 高频请求时移除延迟 // usleep(150000); // 0.15秒 } return [ 'success' => $fundsData, 'errors' => $errors ]; } /** * 获取单个基金数据 */ private function getSingleFundData($fundCode) { // 先尝试短期缓存 $cached = $this->getFundFromCache($fundCode); if ($cached) return $cached; $apiUrl = "http://fundgz.1234567.com.cn/js/{$fundCode}.js?" . time(); $context = stream_context_create([ 'http' => [ 'timeout' => 10, 'header' => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\r\n", 'ignore_errors' => true ] ]); $response = @file_get_contents($apiUrl, false, $context); if (!$response) { return false; } // 兼容不同jsonp格式 if (preg_match('/jsonpgz\d*\((.*)\);?/', $response, $matches)) { $data = json_decode($matches[1], true); // 检查数据是否有效 if ($data && isset($data['fundcode']) && $data['fundcode'] == $fundCode) { // 写入缓存以便后续短期复用 $this->cacheFundData($fundCode, $data); return $data; } } return false; } /** * 计算各渠道统计信息 */ private function calculateChannelStats($fundsData, $fundChannelMap, $fundInvestmentMap) { $channelStats = []; // 初始化渠道统计 $channelNames = array_unique($fundChannelMap); foreach ($channelNames as $channelName) { $channelStats[$channelName] = [ 'count' => 0, 'totalChange' => 0, 'upCount' => 0, 'downCount' => 0, 'totalInvestment' => 0, 'totalCurrentValue' => 0, 'totalProfitLoss' => 0, 'funds' => [] ]; } // 统计各渠道数据 foreach ($fundsData as $fundCode => $fund) { $channelName = $fundChannelMap[$fundCode]; // 明确使用当前的估算涨跌幅(gszzl),而不是历史数据 $currentChange = floatval($fund['gszzl']); $channelStats[$channelName]['totalChange'] += $currentChange; // 使用配置的投资金额 $investment = $fundInvestmentMap[$fundCode] ?? 1000; // 更真实的基金计算方式 // 1. 获取单位净值(使用昨日的单位净值作为购买时的净值) $unitNetValue = floatval($fund['dwjz']) > 0 ? floatval($fund['dwjz']) : 1.0; // 2. 计算购买份额(投资金额 / 单位净值) $shares = $investment / $unitNetValue; // 3. 获取当前估算净值 $currentNetValue = floatval($fund['gsz']) > 0 ? floatval($fund['gsz']) : $unitNetValue; // 4. 计算当前价值(份额 * 当前估算净值) $currentValue = $shares * $currentNetValue; // 5. 计算盈亏 $profitLoss = $currentValue - $investment; // 正确计算昨日收盘时的价值 // 使用正确的公式:昨日单位净值 = 当前单位净值 / (1 + 今日涨幅/100) * (1 + 昨日涨幅/100) $todayChange = floatval($fund['gszzl']); $yesterdayChange = floatval($fund['yesterday_change'] ?? 0); // 计算昨日单位净值 // 如果今日涨幅为-100%(不可能),则使用替代方案 $yesterdayUnitNetValue = ($todayChange == -100) ? $unitNetValue : ($unitNetValue / (1 + $todayChange / 100)) * (1 + $yesterdayChange / 100); // 计算昨日价值 $yesterdayValue = $shares * $yesterdayUnitNetValue; // 7. 计算今日盈亏(从昨日收盘价到当前估值) $todayProfitLoss = $currentValue - $yesterdayValue; $channelStats[$channelName]['count']++; $channelStats[$channelName]['totalInvestment'] += $investment; $channelStats[$channelName]['totalCurrentValue'] += $currentValue; $channelStats[$channelName]['totalProfitLoss'] += $profitLoss; // 确保昨天涨幅数据也被包含在channelStats中 $formattedYesterdayChange = $yesterdayChange !== null ? ($yesterdayChange > 0 ? '+' : '') . $this->formatNumber($yesterdayChange) . '%' : '--'; $channelStats[$channelName]['funds'][$fundCode] = array_merge($fund, [ 'investment' => $investment, 'current_value' => $currentValue, 'profit_loss' => $profitLoss, 'shares' => $shares, 'unit_net_value' => $unitNetValue, 'current_net_value' => $currentNetValue, 'yesterday_unit_net_value' => $yesterdayUnitNetValue, // 添加昨日单位净值 'yesterday_value' => $yesterdayValue, // 添加昨日收盘价值 'today_profit_loss' => $todayProfitLoss, // 添加今日盈亏 'yesterday_change' => $yesterdayChange, // 添加昨日涨幅 'formatted_yesterday_change' => $formattedYesterdayChange // 添加格式化的昨日涨幅 ]); // 使用更严格的判断标准,避免浮点数精度问题 if ($currentChange > 0.001) { $channelStats[$channelName]['upCount']++; } elseif ($currentChange < -0.001) { $channelStats[$channelName]['downCount']++; } } // 计算平均涨跌幅和盈亏比例 foreach ($channelStats as $channelName => &$stats) { $stats['avgChange'] = $stats['count'] > 0 ? $stats['totalChange'] / $stats['count'] : 0; $stats['profitLossRate'] = $stats['totalInvestment'] > 0 ? ($stats['totalProfitLoss'] / $stats['totalInvestment']) * 100 : 0; } return $channelStats; } /** * 根据渠道索引获取渠道名称 */ private function getChannelName($channelIndex) { $channels = ["招商银行", "天天基金", "支付宝"]; return $channels[$channelIndex] ?? "未知渠道"; } /** * 获取渠道CSS类名 */ private function getChannelClass($channelName) { $channelMap = [ '招商银行' => 'cmb', '天天基金' => 'tt', '支付宝' => 'zfb' ]; return $channelMap[$channelName] ?? 'cmb'; } /** * 获取渠道图标 */ private function getChannelIcon($channelName) { $iconMap = [ '招商银行' => '🏦', '天天基金' => '📱', '支付宝' => '💙' ]; return $iconMap[$channelName] ?? '🏦'; } /** * 获取渠道描述 */ private function getChannelDescription($channelName) { $descMap = [ '招商银行' => '银行渠道,申购费率较低,服务稳定', '天天基金' => '专业基金平台,品种齐全,数据精准', '支付宝' => '便捷支付,操作简单,用户体验佳' ]; return $descMap[$channelName] ?? '基金购买渠道'; } /** * 格式化数字 */ private function formatNumber($number) { return number_format($number, 2); } /** * 格式化货币金额 */ private function formatCurrency($amount) { if ($amount >= 10000) { return number_format($amount / 10000, 2) . '万元'; } return number_format($amount, 2) . '元'; } /** * 获取基金数据 */ public function getFundData() { // 从配置文件读取基金配置 $config = $this->loadConfig(); if (empty($config)) { return [ 'success' => false, 'message' => '基金配置文件不存在或为空' ]; } // 按渠道分组基金代码 $channelFunds = []; foreach ($config as $fund) { $channelIndex = $fund['channel']; $fundCode = $fund['fund_code']; if (!isset($channelFunds[$channelIndex])) { $channelFunds[$channelIndex] = ["$channelIndex"]; } $channelFunds[$channelIndex][] = $fundCode; } // 重新索引数组 $channelFunds = array_values($channelFunds); // 合并所有基金代码 $allFundCodes = []; $fundChannelMap = []; // 基金代码到渠道的映射 $fundInvestmentMap = []; // 基金代码到投资金额的映射 foreach ($channelFunds as $channelData) { $channelIndex = $channelData[0]; $channelName = $this->getChannelName($channelIndex); for ($i = 1; $i < count($channelData); $i++) { $fundCode = $channelData[$i]; $allFundCodes[] = $fundCode; $fundChannelMap[$fundCode] = $channelName; // 查找对应的投资金额 foreach ($config as $fund) { if ($fund['fund_code'] === $fundCode && $fund['channel'] === $channelIndex) { $fundInvestmentMap[$fundCode] = $fund['investment']; break; } } } } // 获取基金数据 $result = $this->getBatchFundData($allFundCodes); $fundsData = $result['success']; $errors = $result['errors']; // 首先计算总体统计信息,确保使用原始的当前数据,避免任何数据混淆 // 总体统计信息 $total = count($fundsData); $upCount = 0; $downCount = 0; $flatCount = 0; $totalChange = 0; // 添加总体盈亏统计 $totalInvestment = 0; $totalCurrentValue = 0; $totalProfitLoss = 0; $totalProfitLossRate = 0; foreach ($fundsData as $fundCode => $fund) { // 明确使用当前的估算涨跌幅(gszzl),而不是历史数据 // gszzl字段是当前基金的实时估算涨跌幅 $currentChange = isset($fund['gszzl']) ? floatval($fund['gszzl']) : 0; $totalChange += $currentChange; // 精确判断当前涨跌状态 if ($currentChange > 0.001) $upCount++; // 当前上涨 elseif ($currentChange < -0.001) $downCount++; // 当前下跌 else $flatCount++; // 持平 } // 然后再进行其他操作 // 更新历史数据 - 记录今天的数据 $this->updateHistory($fundsData); // 记录每日数据 $this->recordDailyData($fundsData); // 为每个基金添加历史涨幅数据(昨天的数据) foreach ($fundsData as $fundCode => &$fund) { $yesterdayChange = $this->getFundHistory($fundCode); // 确保yesterday_change始终有值,即使没有历史数据也提供默认值 $fund['yesterday_change'] = $yesterdayChange !== null ? $yesterdayChange : 0; $fund['formatted_yesterday_change'] = $yesterdayChange !== null ? ($yesterdayChange > 0 ? '+' : '') . $this->formatNumber($yesterdayChange) . '%' : '--'; } // 计算各渠道统计信息 $channelStats = $this->calculateChannelStats($fundsData, $fundChannelMap, $fundInvestmentMap); // 使用配置的投资金额计算盈亏 - 更真实的计算方式 foreach ($fundsData as $fundCode => &$fund) { $investment = $fundInvestmentMap[$fundCode] ?? 1000; // 1. 获取单位净值 $unitNetValue = floatval($fund['dwjz']) > 0 ? floatval($fund['dwjz']) : 1.0; // 2. 计算购买份额 $shares = $investment / $unitNetValue; // 3. 获取当前估算净值 $currentNetValue = floatval($fund['gsz']) > 0 ? floatval($fund['gsz']) : $unitNetValue; // 4. 计算当前价值 $currentValue = $shares * $currentNetValue; $profitLoss = $currentValue - $investment; // 正确计算昨日收盘时的价值 // 使用正确的公式:昨日单位净值 = 当前单位净值 / (1 + 今日涨幅/100) * (1 + 昨日涨幅/100) $todayChange = floatval($fund['gszzl']); $yesterdayChange = floatval($fund['yesterday_change'] ?? 0); // 计算昨日单位净值 // 如果今日涨幅为-100%(不可能),则使用替代方案 $yesterdayUnitNetValue = ($todayChange == -100) ? $unitNetValue : ($unitNetValue / (1 + $todayChange / 100)) * (1 + $yesterdayChange / 100); // 计算昨日价值和今日盈亏 $yesterdayValue = $shares * $yesterdayUnitNetValue; $todayProfitLoss = $currentValue - $yesterdayValue; // 确保数据一致性 $fund['yesterday_unit_net_value'] = $yesterdayUnitNetValue; $totalInvestment += $investment; $totalCurrentValue += $currentValue; $totalProfitLoss += $profitLoss; } $avgChange = $total > 0 ? $totalChange / $total : 0; $totalProfitLossRate = $totalInvestment > 0 ? ($totalProfitLoss / $totalInvestment) * 100 : 0; // 格式化数据返回 return [ 'success' => true, 'data' => [ 'summary' => [ 'total' => $total, 'upCount' => $upCount, 'downCount' => $downCount, 'flatCount' => $flatCount, 'avgChange' => $avgChange, 'totalInvestment' => $totalInvestment, 'totalCurrentValue' => $totalCurrentValue, 'totalProfitLoss' => $totalProfitLoss, 'totalProfitLossRate' => $totalProfitLossRate, 'formattedTotalInvestment' => $this->formatCurrency($totalInvestment), 'formattedTotalCurrentValue' => $this->formatCurrency($totalCurrentValue), 'formattedTotalProfitLoss' => $this->formatCurrency($totalProfitLoss), 'formattedProfitLossRate' => $this->formatNumber($totalProfitLossRate), 'profitLossClass' => $totalProfitLoss >= 0 ? 'profit' : 'loss', 'profitLossIcon' => $totalProfitLoss >= 0 ? '📈' : '📉' ], 'channelStats' => $channelStats, 'fundsData' => $fundsData, 'fundChannelMap' => $fundChannelMap, 'errors' => $errors, 'timestamp' => date('Y-m-d H:i:s') ] ]; } /** * 处理API请求 */ public function handleRequest() { try { $method = $_SERVER['REQUEST_METHOD']; // 解析请求参数 $queryString = $_SERVER['QUERY_STRING'] ?? ''; parse_str($queryString, $params); $action = $params['action'] ?? 'fund_data'; // 记录访问(除了统计接口本身) if ($action !== 'get_stats') { $this->recordVisit(); } if ($method === 'GET') { switch ($action) { case 'fund_data': case 'get_fund_data': // 获取基金数据,同时支持get_fund_data和fund_data两种action // 检查是否提供了单个fund_code参数 $fundCode = isset($params['fund_code']) ? trim($params['fund_code']) : ''; if (!empty($fundCode)) { $fundData = $this->getSingleFundData($fundCode); if ($fundData) { $result = ['success' => true, 'data' => $fundData]; } else { $result = ['success' => false, 'message' => '获取基金数据失败: ' . $fundCode]; } } else { // 否则获取所有基金数据 $result = $this->getFundData(); } break; case 'get_stats': // 获取访问统计 $result = $this->getVisitStats(); break; case 'get_operation_log': $limit = isset($params['limit']) ? intval($params['limit']) : 20; $result = $this->getOperationLog($limit); break; case 'get_fund_chart': $fundCode = isset($params['fund_code']) ? trim($params['fund_code']) : ''; if (empty($fundCode)) { $result = ['success' => false, 'message' => '基金代码不能为空']; } else { $result = $this->getFundChartData($fundCode); } break; default: http_response_code(404); $result = ['error' => 'Endpoint not found']; } // 统一返回JSON响应 echo json_encode($result, JSON_UNESCAPED_UNICODE); } else { http_response_code(405); echo json_encode(['error' => 'Method not allowed'], JSON_UNESCAPED_UNICODE); } } catch (Exception $e) { // 捕获异常,返回错误响应 http_response_code(500); echo json_encode(['success' => false, 'message' => '服务器内部错误: ' . $e->getMessage()], JSON_UNESCAPED_UNICODE); } } } // 错误处理 try { // 处理API请求 $api = new FundMonitorAPI(); $api->handleRequest(); } catch (Exception $e) { http_response_code(500); echo json_encode([ 'success' => false, 'error' => '服务器内部错误: ' . $e->getMessage() ]); } ?>