[1, 2, 3, 4, 5], // 1-5代表周一到周五
'hour' => 14,
'minute' => 30
];
// 邮件推送状态文件
private $emailStatusFile = 'data/email_status.json';
public function __construct() {
// 确保数据目录存在
FileManager::ensureDirExists('data');
// 初始化统计文件(如果不存在)
if (!FileManager::fileExists($this->statsFile)) {
$this->initStats();
}
// 初始化配置文件(如果不存在)
if (!FileManager::fileExists($this->configFile)) {
$this->initConfig();
}
// 初始化历史数据文件(如果不存在)
if (!FileManager::fileExists($this->historyFile)) {
$this->initHistory();
}
// 初始化操作日志文件(如果不存在)
if (!FileManager::fileExists($this->operationFile)) {
$this->initOperationLog();
}
// 初始化每日数据文件(如果不存在)
if (!FileManager::fileExists($this->dailyDataFile)) {
$this->initDailyData();
}
// 初始化邮件状态文件(如果不存在)
if (!FileManager::fileExists($this->emailStatusFile)) {
$this->initEmailStatus();
}
// 加载邮箱配置
$this->loadEmailConfig();
}
/**
* 加载邮箱配置
*/
private function loadEmailConfig() {
if (FileManager::fileExists($this->emailConfigFile)) {
$this->emailConfig = FileManager::loadJsonFile($this->emailConfigFile);
} else {
// 如果配置文件不存在,使用默认配置
$this->initEmailConfig();
}
}
/**
* 初始化邮箱配置文件
*/
private function initEmailConfig() {
$defaultEmailConfig = [
'smtp_server' => 'smtp.qq.com',
'smtp_port' => 465,
'smtp_secure' => 'ssl',
'username' => '',
'password' => '',
'from_email' => '',
'from_name' => '基金监控系统',
'to_emails' => []
];
FileManager::saveJsonFile($this->emailConfigFile, $defaultEmailConfig);
$this->emailConfig = $defaultEmailConfig;
}
/**
* 初始化统计数据
*/
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' => []
];
FileManager::saveJsonFile($this->statsFile, $initialStats);
}
/**
* 初始化配置文件
*/
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
]
];
FileManager::saveJsonFile($this->configFile, $defaultConfig);
}
/**
* 初始化历史数据
*/
private function initHistory() {
$initialHistory = [
'last_update_date' => date('Y-m-d'),
'yesterday_data' => [],
'today_data' => []
];
FileManager::saveJsonFile($this->historyFile, $initialHistory);
}
/**
* 初始化操作日志
*/
private function initOperationLog() {
$initialLog = [
'operations' => []
];
FileManager::saveJsonFile($this->operationFile, $initialLog);
}
/**
* 初始化每日数据
*/
private function initDailyData() {
$initialDailyData = [
'last_update' => date('Y-m-d'),
'funds' => []
];
FileManager::saveJsonFile($this->dailyDataFile, $initialDailyData);
}
/**
* 初始化邮件状态文件
*/
private function initEmailStatus() {
$initialStatus = [
'last_sent_date' => null,
'last_sent_time' => null,
'sent_count' => 0,
'failed_count' => 0
];
FileManager::saveJsonFile($this->emailStatusFile, $initialStatus);
}
/**
* 读取SMTP服务器多行响应的私有方法
* @param resource $socket Socket连接资源
* @return string 完整的响应内容
*/
private function readSmtpResponse($socket) {
$response = '';
do {
$line = fgets($socket, 512);
$response .= $line;
} while (substr($line, 3, 1) == '-' && !feof($socket));
return $response;
}
/**
* 发送邮件 - 使用Socket直接连接SMTP服务器
*/
private function sendEmail($subject, $body) {
$config = $this->emailConfig;
$toEmails = $config['to_emails'];
// SMTP服务器配置
$smtpServer = $config['smtp_server'];
$smtpPort = $config['smtp_port'];
$smtpSecure = $config['smtp_secure'];
$smtpUser = $config['username'];
$smtpPassword = $config['password'];
$fromName = $config['from_name'];
$fromEmail = $config['from_email'];
// 发送邮件到所有收件人
$success = true;
$errorLog = "";
foreach ($toEmails as $toEmail) {
// 创建Socket连接
if ($smtpSecure == 'ssl') {
$socket = fsockopen("ssl://$smtpServer", $smtpPort, $errno, $errstr, 30);
} else {
$socket = fsockopen($smtpServer, $smtpPort, $errno, $errstr, 30);
}
if (!$socket) {
$errorLog .= "Failed to connect to $smtpServer:$smtpPort. Error: $errstr ($errno)\n";
$success = false;
continue;
}
// 读取SMTP服务器响应
$response = $this->readSmtpResponse($socket);
$errorLog .= "1. Connection: $response\n";
// 发送EHLO命令
fputs($socket, "EHLO localhost\r\n");
$response = $this->readSmtpResponse($socket);
$errorLog .= "2. EHLO: $response\n";
// 发送认证命令
fputs($socket, "AUTH LOGIN\r\n");
$response = $this->readSmtpResponse($socket);
$errorLog .= "3. AUTH LOGIN: $response\n";
// 发送用户名
fputs($socket, base64_encode($smtpUser) . "\r\n");
$response = $this->readSmtpResponse($socket);
$errorLog .= "4. Username: $response\n";
// 发送密码
fputs($socket, base64_encode($smtpPassword) . "\r\n");
$response = $this->readSmtpResponse($socket);
$errorLog .= "5. Password: $response\n";
// 发送MAIL FROM命令
fputs($socket, "MAIL FROM: <$fromEmail>\r\n");
$response = $this->readSmtpResponse($socket);
$errorLog .= "6. MAIL FROM: $response\n";
// 发送RCPT TO命令
fputs($socket, "RCPT TO: <$toEmail>\r\n");
$response = $this->readSmtpResponse($socket);
$errorLog .= "7. RCPT TO: $response\n";
// 发送DATA命令
fputs($socket, "DATA\r\n");
$response = $this->readSmtpResponse($socket);
$errorLog .= "8. DATA: $response\n";
// 发送邮件内容
$emailContent = "Subject: $subject\r\n";
$emailContent .= "From: $fromName <$fromEmail>\r\n";
$emailContent .= "To: $toEmail\r\n";
$emailContent .= "MIME-Version: 1.0\r\n";
$emailContent .= "Content-Type: text/html; charset=UTF-8\r\n";
$emailContent .= "\r\n";
$emailContent .= $body;
$emailContent .= "\r\n.\r\n";
fputs($socket, $emailContent);
$dataResponse = $this->readSmtpResponse($socket);
$errorLog .= "9. Content sent: $dataResponse\n";
// 发送QUIT命令
fputs($socket, "QUIT\r\n");
$quitResponse = $this->readSmtpResponse($socket);
$errorLog .= "10. QUIT: $quitResponse\n";
// 关闭Socket连接
fclose($socket);
// 检查DATA命令的响应码是否为250(成功)
if (substr($dataResponse, 0, 3) != '250') {
$success = false;
}
}
// 更新邮件状态
$status = $this->loadEmailStatus();
if ($success) {
$status['last_sent_date'] = date('Y-m-d');
$status['last_sent_time'] = date('H:i:s');
$status['sent_count']++;
} else {
$status['failed_count']++;
// 保存错误日志
FileManager::appendToFile('email_error_log.txt', date('Y-m-d H:i:s') . "\n" . $errorLog . "\n\n");
}
$this->saveEmailStatus($status);
return $success;
}
/**
* 加载邮件状态
*/
private function loadEmailStatus() {
return FileManager::loadJsonFile($this->emailStatusFile, [], [$this, 'initEmailStatus']);
}
/**
* 保存邮件状态
*/
private function saveEmailStatus($status) {
FileManager::saveJsonFile($this->emailStatusFile, $status);
}
/**
* 格式化基金数据为邮件内容
*/
private function formatFundEmailContent($fundData) {
$content = "
基金情况推送
";
$content .= "推送时间:" . date('Y年m月d日 H:i:s') . "
";
// 总体统计
$summary = $fundData['summary'];
$content .= "总体统计
";
$content .= "";
$content .= "- 上涨基金:{$summary['up_count']}只
";
$content .= "- 下跌基金:{$summary['down_count']}只
";
$content .= "- 持平基金:{$summary['flat_count']}只
";
$content .= "- 总投资:{$this->formatCurrency($summary['total_investment'])}
";
$content .= "- 当前价值:{$this->formatCurrency($summary['total_current_value'])}
";
$content .= "- 总盈亏:{$this->formatCurrency($summary['total_profit_loss'])}
";
$content .= "- 盈亏比例:{$summary['total_profit_loss_rate']}%
";
$content .= "
";
// 基金详情
$content .= "基金详情
";
$content .= "";
$content .= "";
$content .= "| 基金代码 | 基金名称 | 当前净值 | 涨跌幅 | 昨日涨跌幅 | 投资金额 | 当前价值 | 盈亏 | ";
$content .= "
";
foreach ($fundData['fundsData'] as $fundCode => $fund) {
$investment = $fundData['fundChannelMap'][$fundCode] ?? 0;
$currentValue = isset($fund['current_value']) ? $fund['current_value'] : 0;
$profitLoss = $currentValue - $investment;
$profitLossRate = $investment > 0 ? ($profitLoss / $investment) * 100 : 0;
// 涨跌颜色
$changeColor = floatval($fund['gszzl']) > 0 ? '#009900' : '#ff0000';
$yesterdayChangeColor = floatval($fund['yesterday_change']) > 0 ? '#009900' : '#ff0000';
$profitLossColor = $profitLoss > 0 ? '#009900' : '#ff0000';
$content .= "";
$content .= "| {$fundCode} | ";
$content .= "{$fund['name']} | ";
$content .= "{$fund['gsz']} | ";
$content .= "{$fund['gszzl']}% | ";
$content .= "{$fund['yesterday_change']}% | ";
$content .= "{$this->formatCurrency($investment)} | ";
$content .= "{$this->formatCurrency($currentValue)} | ";
$content .= "{$this->formatCurrency($profitLoss)} ({$this->formatNumber($profitLossRate)}%) | ";
$content .= "
";
}
$content .= "
";
// 渠道统计
$content .= "渠道统计
";
$content .= "";
$content .= "";
$content .= "| 渠道 | 基金数量 | 投资金额 | 当前价值 | 盈亏 | ";
$content .= "
";
foreach ($fundData['channelStats'] as $channelName => $channelData) {
$profitLoss = $channelData['total_current_value'] - $channelData['total_investment'];
$profitLossColor = $profitLoss > 0 ? '#009900' : '#ff0000';
$content .= "";
$content .= "| {$channelName} | ";
$content .= "{$channelData['fund_count']} | ";
$content .= "{$this->formatCurrency($channelData['total_investment'])} | ";
$content .= "{$this->formatCurrency($channelData['total_current_value'])} | ";
$content .= "{$this->formatCurrency($profitLoss)} | ";
$content .= "
";
}
$content .= "
";
return $content;
}
/**
* 判断是否应该发送邮件
*/
private function shouldSendEmail() {
$now = new DateTime();
$currentDay = intval($now->format('w'));
$currentHour = intval($now->format('H'));
$currentMinute = intval($now->format('i'));
$schedule = $this->emailSchedule;
$status = $this->loadEmailStatus();
// 检查是否在设定的工作日
if (!in_array($currentDay, $schedule['days'])) {
return false;
}
// 检查是否在设定的时间范围内(14:30)
if ($currentHour != $schedule['hour'] || $currentMinute < $schedule['minute']) {
return false;
}
// 检查今天是否已经发送过邮件
if ($status['last_sent_date'] == date('Y-m-d')) {
return false;
}
return true;
}
/**
* 发送基金状态邮件
*/
public function sendFundStatusEmail() {
if (!$this->shouldSendEmail()) {
return false;
}
// 获取基金数据
$fundData = $this->getFundData();
// 格式化邮件内容
$subject = "基金情况推送 - " . date('Y年m月d日');
$body = $this->formatFundEmailContent($fundData);
// 发送邮件
return $this->sendEmail($subject, $body);
}
/**
* 测试邮件发送功能
*/
public function testEmail($subject, $body) {
return $this->sendEmail($subject, $body);
}
/**
* 加载基金配置
*/
private function loadConfig() {
return FileManager::loadJsonFile($this->configFile, [], [$this, 'initConfig']);
}
/**
* 加载历史数据
*/
private function loadHistory() {
return FileManager::loadJsonFile($this->historyFile, [], [$this, 'initHistory']);
}
/**
* 保存历史数据
*/
private function saveHistory($history) {
return FileManager::saveJsonFile($this->historyFile, $history);
}
/**
* 更新历史数据
*/
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 (!FileManager::fileExists($file)) return false;
// 过期判断
if (time() - FileManager::getFileMTime($file) > $this->fundCacheTTL) return false;
return FileManager::loadJsonFile($file, false);
}
/**
* 写入缓存
*/
private function cacheFundData($fundCode, $fundData) {
// 保证目录存在
FileManager::ensureDirExists($this->cacheDir);
FileManager::saveJsonFile($this->getFundCachePath($fundCode), $fundData);
}
/**
* 获取批量数据缓存路径
*/
private function getBatchCachePath($key) {
return $this->cacheDir . "/batch_{$key}.json";
}
/**
* 从缓存读取批量数据
*/
private function getFromBatchCache($key) {
$file = $this->getBatchCachePath($key);
if (!FileManager::fileExists($file)) return false;
// 过期判断
if (time() - FileManager::getFileMTime($file) > $this->batchCacheTTL) return false;
$data = FileManager::loadJsonFile($file);
return is_array($data) ? $data : false;
}
/**
* 写入批量数据缓存
*/
private function cacheBatchData($key, $data) {
// 保证目录存在
FileManager::ensureDirExists($this->cacheDir);
FileManager::saveJsonFile($this->getBatchCachePath($key), $data, JSON_UNESCAPED_UNICODE);
}
/**
* 加载访问记录
*/
private function loadVisits() {
return FileManager::loadJsonFile($this->dataFile, []);
}
/**
* 保存访问记录
*/
private function saveVisits($visits) {
return FileManager::saveJsonFile($this->dataFile, $visits);
}
/**
* 记录访问
*/
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);
}
FileManager::saveJsonFile($this->dataFile, $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 loadStats() {
$stats = FileManager::loadJsonFile($this->statsFile, [], [$this, 'initStats']);
// 确保数据结构完整
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);
FileManager::saveJsonFile($this->statsFile, $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 = [];
$needToFetch = [];
// 先从缓存获取可用的数据
foreach ($fundCodes as $fundCode) {
$fundData = $this->getFundFromCache($fundCode);
if ($fundData) {
$fundsData[$fundCode] = $fundData;
} else {
$needToFetch[] = $fundCode;
}
}
// 只获取缓存中不存在的数据
if (!empty($needToFetch)) {
foreach ($needToFetch 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 = @FileManager::loadFile($apiUrl);
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() {
// 尝试加载批量缓存
$batchCacheKey = 'batch_fund_data';
$batchCached = $this->getFromBatchCache($batchCacheKey);
if ($batchCached) {
return $batchCached;
}
// 从配置文件读取基金配置
$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;
// 格式化数据返回
$result = [
'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')
]
];
// 缓存批量结果
$this->cacheBatchData($batchCacheKey, $result);
return $result;
}
/**
* 处理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'];
}
// 检查是否需要发送基金情况邮件
$this->sendFundStatusEmail();
// 统一返回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()
]);
}
?>