1166 lines
40 KiB
PHP
1166 lines
40 KiB
PHP
<?php
|
||
header('Content-Type: application/json');
|
||
header('Access-Control-Allow-Origin: *');
|
||
header('Access-Control-Allow-Methods: GET, POST');
|
||
header('Access-Control-Allow-Headers: Content-Type');
|
||
|
||
class FundMonitorAPI {
|
||
private $dataFile = 'data/visits.json';
|
||
private $statsFile = 'data/stats.json';
|
||
private $configFile = 'data/fund_config.json';
|
||
private $historyFile = 'data/fund_history.json';
|
||
private $operationFile = 'data/operation_log.json';
|
||
private $dailyDataFile = 'data/fund_daily_data.json';
|
||
// 简短缓存,降低频繁请求带来的压力(仅用于监控页实时数据)
|
||
private $cacheDir = 'data/cache';
|
||
private $fundCacheTTL = 15; // 单基金数据缓存(秒)
|
||
|
||
public function __construct() {
|
||
// 确保数据目录存在
|
||
if (!is_dir('data')) {
|
||
mkdir('data', 0755, true);
|
||
}
|
||
|
||
// 初始化统计文件(如果不存在)
|
||
if (!file_exists($this->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()
|
||
]);
|
||
}
|
||
?>
|