添加基金监控系统相关文件,包括邮件发送功能、基金数据配置、测试脚本等。主要包含以下内容: 1. 添加PHPMailer库及相关语言文件 2. 添加基金配置数据文件(fund_config.json, fund_names.json等) 3. 添加邮件发送测试脚本(test_email.php, test_fund_email.php等) 4. 添加.gitignore文件忽略不必要的文件 5. 添加composer.json配置依赖 Signed-off-by: LL <LL>
1342 lines
47 KiB
PHP
1342 lines
47 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');
|
||
|
||
// 引入工具类
|
||
require_once 'utils/FileManager.php';
|
||
require_once 'utils/ApiBase.php';
|
||
|
||
class FundMonitorAPI extends ApiBase {
|
||
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; // 单基金数据缓存(秒)
|
||
private $batchCacheTTL = 60; // 批量基金数据缓存(秒)
|
||
|
||
// 邮件配置文件路径
|
||
private $emailConfigFile = 'data/email_config.json';
|
||
|
||
// 邮件推送配置
|
||
private $emailConfig = [];
|
||
|
||
// 邮件推送时间配置
|
||
private $emailSchedule = [
|
||
'days' => [1, 2, 3, 4, 5], // 1-5代表周一到周五
|
||
'hour' => 14,
|
||
'minute' => 30
|
||
];
|
||
|
||
// 邮件推送状态文件
|
||
private $emailStatusFile = 'data/email_status.json';
|
||
|
||
public function __construct() {
|
||
// 确保数据目录存在
|
||
FileManager::ensureDirExists('data');
|
||
|
||
// 初始化统计文件(如果不存在)
|
||
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();
|
||
}
|
||
|
||
// 初始化邮件状态文件(如果不存在)
|
||
if (!file_exists($this->emailStatusFile)) {
|
||
$this->initEmailStatus();
|
||
}
|
||
|
||
// 加载邮箱配置
|
||
$this->loadEmailConfig();
|
||
}
|
||
|
||
/**
|
||
* 加载邮箱配置
|
||
*/
|
||
private function loadEmailConfig() {
|
||
if (file_exists($this->emailConfigFile)) {
|
||
$this->emailConfig = json_decode(file_get_contents($this->emailConfigFile), true);
|
||
} 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' => []
|
||
];
|
||
|
||
file_put_contents($this->emailConfigFile, json_encode($defaultEmailConfig, JSON_PRETTY_PRINT));
|
||
$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' => []
|
||
];
|
||
|
||
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' => []
|
||
];
|
||
|
||
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
|
||
];
|
||
file_put_contents($this->emailStatusFile, json_encode($initialStatus, JSON_PRETTY_PRINT));
|
||
}
|
||
|
||
/**
|
||
* 读取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']++;
|
||
// 保存错误日志
|
||
file_put_contents('email_error_log.txt', date('Y-m-d H:i:s') . "\n" . $errorLog . "\n\n", FILE_APPEND);
|
||
}
|
||
$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 = "<h2>基金情况推送</h2>";
|
||
$content .= "<p>推送时间:" . date('Y年m月d日 H:i:s') . "</p>";
|
||
|
||
// 总体统计
|
||
$summary = $fundData['summary'];
|
||
$content .= "<h3>总体统计</h3>";
|
||
$content .= "<ul>";
|
||
$content .= "<li>上涨基金:{$summary['up_count']}只</li>";
|
||
$content .= "<li>下跌基金:{$summary['down_count']}只</li>";
|
||
$content .= "<li>持平基金:{$summary['flat_count']}只</li>";
|
||
$content .= "<li>总投资:{$this->formatCurrency($summary['total_investment'])}</li>";
|
||
$content .= "<li>当前价值:{$this->formatCurrency($summary['total_current_value'])}</li>";
|
||
$content .= "<li>总盈亏:{$this->formatCurrency($summary['total_profit_loss'])}</li>";
|
||
$content .= "<li>盈亏比例:{$summary['total_profit_loss_rate']}%</li>";
|
||
$content .= "</ul>";
|
||
|
||
// 基金详情
|
||
$content .= "<h3>基金详情</h3>";
|
||
$content .= "<table border='1' cellpadding='5' cellspacing='0' style='border-collapse:collapse;'>";
|
||
$content .= "<tr style='background-color:#f0f0f0;'>";
|
||
$content .= "<th>基金代码</th><th>基金名称</th><th>当前净值</th><th>涨跌幅</th><th>昨日涨跌幅</th><th>投资金额</th><th>当前价值</th><th>盈亏</th>";
|
||
$content .= "</tr>";
|
||
|
||
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 .= "<tr>";
|
||
$content .= "<td>{$fundCode}</td>";
|
||
$content .= "<td>{$fund['name']}</td>";
|
||
$content .= "<td>{$fund['gsz']}</td>";
|
||
$content .= "<td style='color:{$changeColor};'>{$fund['gszzl']}%</td>";
|
||
$content .= "<td style='color:{$yesterdayChangeColor};'>{$fund['yesterday_change']}%</td>";
|
||
$content .= "<td>{$this->formatCurrency($investment)}</td>";
|
||
$content .= "<td>{$this->formatCurrency($currentValue)}</td>";
|
||
$content .= "<td style='color:{$profitLossColor};'>{$this->formatCurrency($profitLoss)} ({$this->formatNumber($profitLossRate)}%)</td>";
|
||
$content .= "</tr>";
|
||
}
|
||
$content .= "</table>";
|
||
|
||
// 渠道统计
|
||
$content .= "<h3>渠道统计</h3>";
|
||
$content .= "<table border='1' cellpadding='5' cellspacing='0' style='border-collapse:collapse;'>";
|
||
$content .= "<tr style='background-color:#f0f0f0;'>";
|
||
$content .= "<th>渠道</th><th>基金数量</th><th>投资金额</th><th>当前价值</th><th>盈亏</th>";
|
||
$content .= "</tr>";
|
||
|
||
foreach ($fundData['channelStats'] as $channelName => $channelData) {
|
||
$profitLoss = $channelData['total_current_value'] - $channelData['total_investment'];
|
||
$profitLossColor = $profitLoss > 0 ? '#009900' : '#ff0000';
|
||
|
||
$content .= "<tr>";
|
||
$content .= "<td>{$channelName}</td>";
|
||
$content .= "<td>{$channelData['fund_count']}</td>";
|
||
$content .= "<td>{$this->formatCurrency($channelData['total_investment'])}</td>";
|
||
$content .= "<td>{$this->formatCurrency($channelData['total_current_value'])}</td>";
|
||
$content .= "<td style='color:{$profitLossColor};'>{$this->formatCurrency($profitLoss)}</td>";
|
||
$content .= "</tr>";
|
||
}
|
||
$content .= "</table>";
|
||
|
||
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 (!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) {
|
||
// 保证目录存在
|
||
FileManager::ensureDirExists($this->cacheDir);
|
||
@file_put_contents($this->getFundCachePath($fundCode), json_encode($fundData, JSON_UNESCAPED_UNICODE));
|
||
}
|
||
|
||
/**
|
||
* 获取批量数据缓存路径
|
||
*/
|
||
private function getBatchCachePath($key) {
|
||
return $this->cacheDir . "/batch_{$key}.json";
|
||
}
|
||
|
||
/**
|
||
* 从缓存读取批量数据
|
||
*/
|
||
private function getFromBatchCache($key) {
|
||
$file = $this->getBatchCachePath($key);
|
||
if (!file_exists($file)) return false;
|
||
// 过期判断
|
||
if (time() - filemtime($file) > $this->batchCacheTTL) 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 cacheBatchData($key, $data) {
|
||
// 保证目录存在
|
||
FileManager::ensureDirExists($this->cacheDir);
|
||
@file_put_contents($this->getBatchCachePath($key), json_encode($data, JSON_UNESCAPED_UNICODE));
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* 加载访问记录
|
||
*/
|
||
private function loadVisits() {
|
||
return FileManager::loadJsonFile($this->dataFile, []);
|
||
}
|
||
|
||
/**
|
||
* 保存访问记录
|
||
*/
|
||
private function saveVisits($visits) {
|
||
return file_put_contents($this->dataFile, json_encode($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);
|
||
}
|
||
|
||
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 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);
|
||
|
||
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 = [];
|
||
$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 = @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() {
|
||
// 尝试加载批量缓存
|
||
$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()
|
||
]);
|
||
}
|
||
?>
|
||
|