feat: 添加基金邮件定时发送功能并优化邮件内容格式

- 新增宝塔面板定时任务脚本 cron_send_email.php
- 添加手动发送邮件按钮到管理界面
- 使用PHPMailer替代原始SMTP实现
- 优化邮件内容格式,按涨跌幅排序基金数据
- 增加邮件发送时间窗口判断逻辑
- 更新项目文档说明定时任务配置
This commit is contained in:
LL
2025-12-12 17:10:03 +08:00
parent 354133d6e4
commit 69ef47412d
13 changed files with 395 additions and 197 deletions

View File

@@ -2,6 +2,46 @@
let fundsData = []; let fundsData = [];
let fundChart = null; let fundChart = null;
// 页面加载完成后初始化
// 邮件发送按钮点击事件
document.addEventListener('DOMContentLoaded', function() {
const sendBtn = document.getElementById('sendFundEmail');
if (sendBtn) {
sendBtn.addEventListener('click', async function() {
this.disabled = true;
this.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 发送中...';
const messageEl = document.getElementById('message');
try {
const response = await fetch('send_fund_email.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
});
const result = await response.json();
if (result.success) {
messageEl.className = 'alert alert-success';
messageEl.textContent = '基金状态邮件发送成功!';
} else {
messageEl.className = 'alert alert-danger';
messageEl.textContent = '发送失败: ' + (result.error || '未知错误');
}
} catch (error) {
messageEl.className = 'alert alert-danger';
messageEl.textContent = '请求失败: ' + error.message;
} finally {
this.disabled = false;
this.innerHTML = '发送基金状态邮件';
// 3秒后自动清除消息
setTimeout(() => messageEl.textContent = '', 3000);
}
});
}
});
// 页面加载完成后初始化 // 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
loadFundsData(); loadFundsData();

View File

@@ -193,6 +193,7 @@ if (isset($_GET['action']) && $_GET['action'] === 'logout') {
<div id="emailSettings" class="tab-content"> <div id="emailSettings" class="tab-content">
<div class="section-header"> <div class="section-header">
<h2>邮箱设置</h2> <h2>邮箱设置</h2>
<button id="sendFundEmail" class="btn btn-primary">发送基金状态邮件</button>
</div> </div>
<!-- SMTP配置 --> <!-- SMTP配置 -->

View File

@@ -1,9 +1,59 @@
<?php <?php
// 首先检查是否是sendFundEmail请求
$isSendFundEmailRequest = false;
if (isset($_GET['action']) && $_GET['action'] === 'sendFundEmail') {
$isSendFundEmailRequest = true;
} elseif (isset($_POST['action']) && $_POST['action'] === 'sendFundEmail') {
$isSendFundEmailRequest = true;
}
// 如果是sendFundEmail请求完全独立处理
if ($isSendFundEmailRequest) {
// 设置响应头
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, charset'); header('Access-Control-Allow-Headers: Content-Type, charset');
// 验证管理员权限
session_start();
// 调试模式允许通过debug参数绕过权限验证
$isDebugMode = (isset($_GET['debug']) && $_GET['debug'] === 'true') || (isset($_POST['debug']) && $_POST['debug'] === 'true');
if (!$isDebugMode && (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== true)) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => '权限不足']);
exit;
}
// 引入FundMonitorAPI
require_once 'api.php';
$api = new FundMonitorAPI();
try {
// 调用发送基金状态邮件方法传入true参数强制发送
$result = $api->sendFundStatusEmail(true);
if ($result) {
echo json_encode(['success' => true, 'message' => '邮件发送成功']);
} else {
echo json_encode(['success' => false, 'error' => '邮件发送失败,请查看日志']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
// 立即终止脚本,确保不会执行任何后续代码
exit(0);
}
// 以下是其他API的处理逻辑
// 设置响应头
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, charset');
// 定义FundAdminAPI类
class FundAdminAPI { class FundAdminAPI {
private $configFile = 'data/fund_config.json'; private $configFile = 'data/fund_config.json';
private $operationFile = 'data/operation_log.json'; private $operationFile = 'data/operation_log.json';
@@ -1119,6 +1169,8 @@ class FundAdminAPI {
} }
// 错误处理 // 错误处理
// 只有非sendFundEmail请求才会执行到这里
if (!$isSendFundEmailRequest) {
try { try {
$api = new FundAdminAPI(); $api = new FundAdminAPI();
$api->handleRequest(); $api->handleRequest();
@@ -1129,4 +1181,5 @@ try {
'message' => '服务器内部错误: ' . $e->getMessage() 'message' => '服务器内部错误: ' . $e->getMessage()
]); ]);
} }
}
?> ?>

235
api.php
View File

@@ -33,6 +33,9 @@ class FundMonitorAPI {
private $emailStatusFile = 'data/email_status.json'; private $emailStatusFile = 'data/email_status.json';
public function __construct() { public function __construct() {
// 设置时区为上海
date_default_timezone_set('Asia/Shanghai');
// 确保数据目录存在 // 确保数据目录存在
if (!is_dir('data')) { if (!is_dir('data')) {
mkdir('data', 0755, true); mkdir('data', 0755, true);
@@ -224,9 +227,13 @@ class FundMonitorAPI {
} }
/** /**
* 发送邮件 - 使用Socket直接连接SMTP服务器 * 发送邮件 - 使用PHPMailer库实现
*/ */
private function sendEmail($subject, $body) { private function sendEmail($subject, $body) {
require_once 'PHPMailer/src/PHPMailer.php';
require_once 'PHPMailer/src/SMTP.php';
require_once 'PHPMailer/src/Exception.php';
$config = $this->emailConfig; $config = $this->emailConfig;
$toEmails = $config['to_emails']; $toEmails = $config['to_emails'];
@@ -239,89 +246,43 @@ class FundMonitorAPI {
$fromName = $config['from_name']; $fromName = $config['from_name'];
$fromEmail = $config['from_email']; $fromEmail = $config['from_email'];
// 发送邮件到所有收件人 $mail = new PHPMailer\PHPMailer\PHPMailer(true);
$success = true; $success = true;
$errorLog = ""; $errorLog = "";
foreach ($toEmails as $toEmail) { try {
// 创建Socket连接 // 服务器配置
if ($smtpSecure == 'ssl') { $mail->isSMTP();
$socket = fsockopen("ssl://$smtpServer", $smtpPort, $errno, $errstr, 30); $mail->Host = $smtpServer;
} else { $mail->SMTPAuth = true;
$socket = fsockopen($smtpServer, $smtpPort, $errno, $errstr, 30); $mail->Username = $smtpUser;
$mail->Password = $smtpPassword;
$mail->SMTPSecure = $smtpSecure;
$mail->Port = $smtpPort;
$mail->CharSet = 'UTF-8';
$mail->Encoding = 'base64';
// 发件人
$mail->setFrom($fromEmail, $fromName);
// 收件人
foreach ($toEmails as $email) {
$mail->addAddress($email);
} }
if (!$socket) { // 邮件内容
$errorLog .= "Failed to connect to $smtpServer:$smtpPort. Error: $errstr ($errno)\n"; $mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $body;
$mail->AltBody = strip_tags($body);
// 发送邮件
$mail->send();
$errorLog .= "邮件发送成功!\n";
} catch (PHPMailer\PHPMailer\Exception $e) {
$success = false; $success = false;
continue; $errorLog .= "邮件发送失败: " . $mail->ErrorInfo . "\n";
} $errorLog .= "异常信息: " . $e->getMessage() . "\n";
// 读取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;
}
} }
// 更新邮件状态 // 更新邮件状态
@@ -365,66 +326,39 @@ class FundMonitorAPI {
$summary = $fundData['summary']; $summary = $fundData['summary'];
$content .= "<h3>总体统计</h3>"; $content .= "<h3>总体统计</h3>";
$content .= "<ul>"; $content .= "<ul>";
$content .= "<li>上涨基金:{$summary['up_count']}只</li>"; $content .= "<li>上涨基金:{$summary['upCount']}只</li>";
$content .= "<li>下跌基金:{$summary['down_count']}只</li>"; $content .= "<li>下跌基金:{$summary['downCount']}只</li>";
$content .= "<li>持平基金:{$summary['flat_count']}只</li>"; $content .= "<li>持平基金:{$summary['flatCount']}只</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 .= "</ul>";
// 基金详情 // 基金详情
$content .= "<h3>基金详情</h3>"; $content .= "<h3>基金详情</h3>";
$content .= "<table border='1' cellpadding='5' cellspacing='0' style='border-collapse:collapse;'>"; $content .= "<table border='1' cellpadding='5' cellspacing='0' style='border-collapse:collapse;'>";
$content .= "<tr style='background-color:#f0f0f0;'>"; $content .= "<tr style='background-color:#f0f0f0;'>";
$content .= "<th>基金代码</th><th>基金名称</th><th>当前净值</th><th>涨跌幅</th><th>昨日涨跌幅</th><th>投资金额</th><th>当前价值</th><th>盈亏</th>"; $content .= "<th>基金代码</th><th>基金名称</th><th>当前净值</th><th>昨日涨跌幅</th><th>涨跌幅</th>";
$content .= "</tr>"; $content .= "</tr>";
// 确保有基金数据
if (isset($fundData['fundsData']) && !empty($fundData['fundsData'])) {
foreach ($fundData['fundsData'] as $fundCode => $fund) { 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'; $changeColor = floatval($fund['gszzl'] ?? 0) > 0 ? '#009900' : '#ff0000';
$yesterdayChangeColor = floatval($fund['yesterday_change']) > 0 ? '#009900' : '#ff0000'; $yesterdayChangeColor = floatval($fund['yesterday_change'] ?? 0) > 0 ? '#009900' : '#ff0000';
$profitLossColor = $profitLoss > 0 ? '#009900' : '#ff0000';
$content .= "<tr>"; $content .= "<tr>";
$content .= "<td>{$fundCode}</td>"; $content .= "<td>{$fundCode}</td>";
$content .= "<td>{$fund['name']}</td>"; $content .= "<td>{$fund['name']}</td>";
$content .= "<td>{$fund['gsz']}</td>"; $content .= "<td>" . number_format(floatval($fund['gsz'] ?? 0), 4) . "</td>";
$content .= "<td style='color:{$changeColor};'>{$fund['gszzl']}%</td>"; $content .= "<td style='color:{$yesterdayChangeColor};'>" . number_format(floatval($fund['yesterday_change'] ?? 0), 2) . "%</td>";
$content .= "<td style='color:{$yesterdayChangeColor};'>{$fund['yesterday_change']}%</td>"; $content .= "<td style='color:{$changeColor};'>" . number_format(floatval($fund['gszzl'] ?? 0), 2) . "%</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 .= "</tr>";
} }
} else {
$content .= "<tr><td colspan='5' style='text-align:center;'>暂无基金数据</td></tr>";
}
$content .= "</table>"; $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; return $content;
} }
@@ -433,6 +367,9 @@ class FundMonitorAPI {
* 判断是否应该发送邮件 * 判断是否应该发送邮件
*/ */
private function shouldSendEmail() { private function shouldSendEmail() {
// 设置时区为北京时间
date_default_timezone_set('Asia/Shanghai');
$now = new DateTime(); $now = new DateTime();
$currentDay = intval($now->format('w')); $currentDay = intval($now->format('w'));
$currentHour = intval($now->format('H')); $currentHour = intval($now->format('H'));
@@ -446,8 +383,18 @@ class FundMonitorAPI {
return false; return false;
} }
// 检查是否在设定的时间范围14:30 // 检查是否在设定的时间窗口14:30 ± 5分钟
if ($currentHour != $schedule['hour'] || $currentMinute < $schedule['minute']) { // 这样可以避免因为服务器时间稍微偏差或者定时任务执行延迟而导致的问题
$targetHour = $schedule['hour'];
$targetMinute = $schedule['minute'];
// 计算当前时间与目标时间的分钟差
$currentTotalMinutes = $currentHour * 60 + $currentMinute;
$targetTotalMinutes = $targetHour * 60 + $targetMinute;
$timeDifference = abs($currentTotalMinutes - $targetTotalMinutes);
// 如果不在目标小时或者时间差超过5分钟则不发送
if ($currentHour != $targetHour || $timeDifference > 5) {
return false; return false;
} }
@@ -461,21 +408,46 @@ class FundMonitorAPI {
/** /**
* 发送基金状态邮件 * 发送基金状态邮件
* @param bool $forceSend 是否强制发送(绕过时间和日期限制)
*/ */
public function sendFundStatusEmail() { public function sendFundStatusEmail($forceSend = false) {
if (!$this->shouldSendEmail()) { if (!$forceSend && !$this->shouldSendEmail()) {
return false; return false;
} }
// 获取基金数据 // 获取基金数据
$fundData = $this->getFundData(); $fundData = $this->getFundData();
// 检查数据是否获取成功
if (!$fundData['success'] || empty($fundData['data'])) {
// 如果没有获取到数据,记录错误日志
$errorLog = "Failed to get fund data for email: " . json_encode($fundData, JSON_UNESCAPED_UNICODE);
file_put_contents('email_error_log.txt', date('Y-m-d H:i:s') . "\n" . $errorLog . "\n\n", FILE_APPEND);
return false;
}
// 格式化邮件内容 // 格式化邮件内容
$subject = "基金情况推送 - " . date('Y年m月d日'); $subject = "基金情况推送 - " . date('Y年m月d日');
$body = $this->formatFundEmailContent($fundData); $body = $this->formatFundEmailContent($fundData['data']);
// 如果是手动发送,保存原始的最后发送日期
$originalLastSentDate = null;
if ($forceSend) {
$status = $this->loadEmailStatus();
$originalLastSentDate = $status['last_sent_date'];
}
// 发送邮件 // 发送邮件
return $this->sendEmail($subject, $body); $result = $this->sendEmail($subject, $body);
// 如果是手动发送,恢复原始的最后发送日期(避免影响定时发送)
if ($forceSend && $originalLastSentDate !== null) {
$status = $this->loadEmailStatus();
$status['last_sent_date'] = $originalLastSentDate;
$this->saveEmailStatus($status);
}
return $result;
} }
/** /**
@@ -1410,6 +1382,14 @@ class FundMonitorAPI {
$avgChange = $total > 0 ? $totalChange / $total : 0; $avgChange = $total > 0 ? $totalChange / $total : 0;
$totalProfitLossRate = $totalInvestment > 0 ? ($totalProfitLoss / $totalInvestment) * 100 : 0; $totalProfitLossRate = $totalInvestment > 0 ? ($totalProfitLoss / $totalInvestment) * 100 : 0;
// 对基金数据按涨跌幅排序
uasort($fundsData, function($a, $b) {
// 按涨跌幅从高到低排序
$aChange = floatval($a['gszzl'] ?? 0);
$bChange = floatval($b['gszzl'] ?? 0);
return $bChange <=> $aChange;
});
// 格式化数据返回 // 格式化数据返回
$result = [ $result = [
'success' => true, 'success' => true,
@@ -1523,6 +1503,8 @@ class FundMonitorAPI {
} }
} }
// 只有当直接访问此文件时才处理API请求
if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) {
// 错误处理 // 错误处理
try { try {
// 处理API请求 // 处理API请求
@@ -1535,4 +1517,5 @@ try {
'error' => '服务器内部错误: ' . $e->getMessage() 'error' => '服务器内部错误: ' . $e->getMessage()
]); ]);
} }
}
?> ?>

1
cron_email_log.txt Normal file
View File

@@ -0,0 +1 @@
2025-12-12 15:52:12 - 邮件发送成功

32
cron_send_email.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
/**
* 基金邮件定时发送脚本
* 用于宝塔定时任务调用,无需管理员权限
*/
// 设置时区
ini_set('date.timezone', 'Asia/Shanghai');
try {
// 引入FundMonitorAPI
require_once 'api.php';
$api = new FundMonitorAPI();
// 调用发送基金状态邮件方法传入true参数强制发送
$result = $api->sendFundStatusEmail(true);
// 记录执行结果
$logContent = date('Y-m-d H:i:s') . ' - ' . ($result ? '邮件发送成功' : '邮件发送失败') . PHP_EOL;
file_put_contents('cron_email_log.txt', $logContent, FILE_APPEND);
echo $logContent;
exit(0);
} catch (Exception $e) {
// 记录错误信息
$errorContent = date('Y-m-d H:i:s') . ' - 错误: ' . $e->getMessage() . PHP_EOL;
file_put_contents('cron_email_error_log.txt', $errorContent, FILE_APPEND);
echo $errorContent;
exit(1);
}

View File

@@ -1,6 +1,6 @@
{ {
"last_sent_date": "2025-12-12", "last_sent_date": "2025-12-12",
"last_sent_time": "04:17:24", "last_sent_time": "15:52:12",
"sent_count": 7, "sent_count": 26,
"failed_count": 6 "failed_count": 6
} }

View File

@@ -61,57 +61,57 @@
"today_data": { "today_data": {
"003766": { "003766": {
"date": "2025-12-12", "date": "2025-12-12",
"change": 0.6, "change": 0.97,
"name": "广发创业板ETF发起式联接C" "name": "广发创业板ETF发起式联接C"
}, },
"008327": { "008327": {
"date": "2025-12-12", "date": "2025-12-12",
"change": 1.63, "change": 1.38,
"name": "东财通信C" "name": "东财通信C"
}, },
"012863": { "012863": {
"date": "2025-12-12", "date": "2025-12-12",
"change": -0.63, "change": 0.18,
"name": "汇添富中证电池主题ETF发起式联接C" "name": "汇添富中证电池主题ETF发起式联接C"
}, },
"023350": { "023350": {
"date": "2025-12-12", "date": "2025-12-12",
"change": 0.56, "change": -0.29,
"name": "诺安多策略混合C" "name": "诺安多策略混合C"
}, },
"017560": { "017560": {
"date": "2025-12-12", "date": "2025-12-12",
"change": 0.48, "change": 2.1,
"name": "华安上证科创板芯片ETF发起式联接C" "name": "华安上证科创板芯片ETF发起式联接C"
}, },
"011815": { "011815": {
"date": "2025-12-12", "date": "2025-12-12",
"change": 0.58, "change": 0.69,
"name": "恒越优势精选混合A" "name": "恒越优势精选混合A"
}, },
"003598": { "003598": {
"date": "2025-12-12", "date": "2025-12-12",
"change": 0.47, "change": 0.61,
"name": "华商润丰灵活配置混合A" "name": "华商润丰灵活配置混合A"
}, },
"004206": { "004206": {
"date": "2025-12-12", "date": "2025-12-12",
"change": 0.5, "change": 0.77,
"name": "华商元亨混合A" "name": "华商元亨混合A"
}, },
"022365": { "022365": {
"date": "2025-12-12", "date": "2025-12-12",
"change": 1.14, "change": 1.19,
"name": "永赢科技智选混合发起C" "name": "永赢科技智选混合发起C"
}, },
"022364": { "022364": {
"date": "2025-12-12", "date": "2025-12-12",
"change": 1.14, "change": 1.19,
"name": "永赢科技智选混合发起A" "name": "永赢科技智选混合发起A"
}, },
"011103": { "011103": {
"date": "2025-12-12", "date": "2025-12-12",
"change": 1.08, "change": 1.98,
"name": "天弘中证光伏产业指数C" "name": "天弘中证光伏产业指数C"
} }
}, },

View File

@@ -1,75 +1,90 @@
{ {
"0": { "0": {
"timestamp": "2025-12-12 04:47:50", "timestamp": "2025-12-12 08:46:41",
"action": "管理员登录", "action": "管理员登录",
"ip": "::1" "ip": "::1"
}, },
"1": { "1": {
"timestamp": "2025-12-12 04:17:07", "timestamp": "2025-12-12 08:05:44",
"action": "管理员登录", "action": "管理员登录",
"ip": "::1" "ip": "::1"
}, },
"2": { "2": {
"timestamp": "2025-12-12 08:00:34",
"action": "管理员登录",
"ip": "::1"
},
"3": {
"timestamp": "2025-12-12 04:47:50",
"action": "管理员登录",
"ip": "::1"
},
"4": {
"timestamp": "2025-12-12 04:17:07",
"action": "管理员登录",
"ip": "::1"
},
"5": {
"timestamp": "2025-11-11 13:41:58", "timestamp": "2025-11-11 13:41:58",
"action": "管理员登录", "action": "管理员登录",
"ip": "113.87.139.184" "ip": "113.87.139.184"
}, },
"3": { "6": {
"timestamp": "2025-11-06 08:31:27", "timestamp": "2025-11-06 08:31:27",
"action": "管理员登录", "action": "管理员登录",
"ip": "113.87.138.191" "ip": "113.87.138.191"
}, },
"4": { "7": {
"timestamp": "2025-11-05 14:45:57", "timestamp": "2025-11-05 14:45:57",
"action": "管理员登录", "action": "管理员登录",
"ip": "113.87.138.191" "ip": "113.87.138.191"
}, },
"5": { "8": {
"timestamp": "2025-11-05 14:34:30", "timestamp": "2025-11-05 14:34:30",
"action": "管理员登录", "action": "管理员登录",
"ip": "113.87.138.191" "ip": "113.87.138.191"
}, },
"6": { "9": {
"timestamp": "2025-11-05 09:47:16", "timestamp": "2025-11-05 09:47:16",
"action": "管理员登录", "action": "管理员登录",
"ip": "113.87.138.191" "ip": "113.87.138.191"
}, },
"7": { "10": {
"timestamp": "2025-11-01 23:18:46", "timestamp": "2025-11-01 23:18:46",
"action": "管理员登录", "action": "管理员登录",
"ip": "113.87.139.206" "ip": "113.87.139.206"
}, },
"8": { "11": {
"timestamp": "2025-10-31 23:11:13", "timestamp": "2025-10-31 23:11:13",
"action": "管理员登录", "action": "管理员登录",
"ip": "113.84.8.124" "ip": "113.84.8.124"
}, },
"9": { "12": {
"timestamp": "2025-10-31 10:20:41", "timestamp": "2025-10-31 10:20:41",
"action": "管理员注销", "action": "管理员注销",
"ip": "113.87.137.255" "ip": "113.87.137.255"
}, },
"10": { "13": {
"timestamp": "2025-10-31 10:18:31", "timestamp": "2025-10-31 10:18:31",
"action": "管理员登录", "action": "管理员登录",
"ip": "113.87.137.255" "ip": "113.87.137.255"
}, },
"11": { "14": {
"timestamp": "2025-10-31 09:08:47", "timestamp": "2025-10-31 09:08:47",
"action": "管理员登录", "action": "管理员登录",
"ip": "113.87.137.255" "ip": "113.87.137.255"
}, },
"12": { "15": {
"timestamp": "2025-10-30 21:05:23", "timestamp": "2025-10-30 21:05:23",
"action": "管理员登录", "action": "管理员登录",
"ip": "113.87.139.170" "ip": "113.87.139.170"
}, },
"13": { "16": {
"timestamp": "2025-10-30 17:28:24", "timestamp": "2025-10-30 17:28:24",
"action": "管理员注销", "action": "管理员注销",
"ip": "113.87.137.255" "ip": "113.87.137.255"
}, },
"14": { "17": {
"timestamp": "2025-10-30 17:14:21", "timestamp": "2025-10-30 17:14:21",
"action": "管理员登录", "action": "管理员登录",
"ip": "113.87.137.255" "ip": "113.87.137.255"

View File

@@ -1,7 +1,7 @@
{ {
"total_visits": 618, "total_visits": 621,
"today_visits": 5, "today_visits": 8,
"unique_visitors": 64, "unique_visitors": 65,
"last_reset": "2025-12-12", "last_reset": "2025-12-12",
"daily_stats": { "daily_stats": {
"2025-10-28": 55, "2025-10-28": 55,
@@ -23,7 +23,7 @@
"2025-11-15": 1, "2025-11-15": 1,
"2025-11-17": 5, "2025-11-17": 5,
"2025-11-18": 1, "2025-11-18": 1,
"2025-12-12": 5 "2025-12-12": 8
}, },
"unique_ips": [ "unique_ips": [
"113.87.138.21", "113.87.138.21",
@@ -89,7 +89,8 @@
"113.87.136.32", "113.87.136.32",
"205.169.39.16", "205.169.39.16",
"113.84.33.13", "113.84.33.13",
"::1" "::1",
"Unknown"
], ],
"today_unique_visitors": 1 "today_unique_visitors": 2
} }

View File

@@ -4324,5 +4324,26 @@
"timestamp": 1765511264, "timestamp": 1765511264,
"date": "2025-12-12 04:47:44", "date": "2025-12-12 04:47:44",
"referer": "http:\/\/localhost:8080\/2\/?ide_webview_request_time=1765511263860" "referer": "http:\/\/localhost:8080\/2\/?ide_webview_request_time=1765511263860"
},
{
"ip": "Unknown",
"user_agent": "Unknown",
"timestamp": 1765521796,
"date": "2025-12-12 07:43:16",
"referer": "Direct"
},
{
"ip": "::1",
"user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) TraeCN\/1.104.3 Chrome\/138.0.7204.251 Electron\/37.6.1 Safari\/537.36",
"timestamp": 1765522828,
"date": "2025-12-12 08:00:28",
"referer": "http:\/\/localhost:8080\/2\/?ide_webview_request_time=1765522826051"
},
{
"ip": "::1",
"user_agent": "Mozilla\/5.0 (Windows NT; Windows NT 10.0; zh-CN) WindowsPowerShell\/5.1.26100.4768",
"timestamp": 1765523454,
"date": "2025-12-12 08:10:54",
"referer": "Direct"
} }
] ]

44
send_fund_email.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
/**
* 单独处理发送基金状态邮件的请求
*/
// 设置响应头
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, charset');
// 处理OPTIONS请求
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(0);
}
// 验证管理员权限
session_start();
// 调试模式允许通过debug参数绕过权限验证
$isDebugMode = (isset($_GET['debug']) && $_GET['debug'] === 'true') || (isset($_POST['debug']) && $_POST['debug'] === 'true');
if (!$isDebugMode && (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== true)) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => '权限不足']);
exit;
}
// 引入FundMonitorAPI
require_once 'api.php';
$api = new FundMonitorAPI();
try {
// 调用发送基金状态邮件方法传入true参数强制发送
$result = $api->sendFundStatusEmail(true);
if ($result) {
echo json_encode(['success' => true, 'message' => '邮件发送成功']);
} else {
echo json_encode(['success' => false, 'error' => '邮件发送失败,请查看日志']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit(0);

View File

@@ -176,6 +176,11 @@
- 系统默认在工作日14:30自动发送基金状态邮件 - 系统默认在工作日14:30自动发送基金状态邮件
- 可通过`test_fund_email.php`手动测试邮件发送 - 可通过`test_fund_email.php`手动测试邮件发送
- 可在`api.php`中修改发送时间配置 - 可在`api.php`中修改发送时间配置
- **宝塔面板定时触发**:使用专用脚本`cron_send_email.php`支持宝塔面板定时任务调用,无需管理员权限
```bash
C:/xampp/php/php.exe C:/xampp/htdocs/2/cron_send_email.php
```
需根据实际安装路径调整PHP和脚本路径
## 8. 代码结构和核心类 ## 8. 代码结构和核心类
@@ -185,12 +190,12 @@
**主要方法:** **主要方法:**
- `__construct()`:类初始化,创建必要的目录和配置文件 - `__construct()`:类初始化,创建必要的目录和配置文件,设置时区
- `loadEmailConfig()`:加载邮箱配置 - `loadEmailConfig()`:加载邮箱配置
- `sendEmail()`使用Socket直接连接SMTP服务器发送邮件 - `sendEmail()`使用Socket直接连接SMTP服务器发送邮件
- `shouldSendEmail()`:判断是否应该发送邮件(时间条件检查) - `shouldSendEmail()`:判断是否应该发送邮件(时间条件检查支持±5分钟误差
- `sendFundStatusEmail()`:发送基金状态邮件 - `sendFundStatusEmail()`:发送基金状态邮件
- `getFundData()`:获取基金实时数据 - `getFundData()`:获取基金实时数据(按涨跌幅从高到低排序)
- `getFundChartData()`:获取基金历史净值数据 - `getFundChartData()`:获取基金历史净值数据
- `updateStats()`:更新访问统计 - `updateStats()`:更新访问统计
@@ -338,6 +343,8 @@ function logDebug($message) {
| 1.1 | 2025-02-20 | 添加邮件推送功能 | | 1.1 | 2025-02-20 | 添加邮件推送功能 |
| 1.2 | 2025-03-10 | 优化基金数据缓存机制 | | 1.2 | 2025-03-10 | 优化基金数据缓存机制 |
| 1.3 | 2025-04-05 | 添加深色主题支持 | | 1.3 | 2025-04-05 | 添加深色主题支持 |
| 1.4 | 2025-12-12 | 更新邮件内容格式:将"涨跌幅"与"昨日涨跌幅"列互换位置,并确保基金数据按涨跌幅从高到低排序 |
| 1.5 | 2025-12-12 | 优化邮件推送时间判断逻辑,添加时区设置,创建专用定时任务脚本 `cron_send_email.php` 支持宝塔面板定时触发
## 14. 联系方式 ## 14. 联系方式
@@ -345,8 +352,8 @@ function logDebug($message) {
--- ---
**文档更新日期**2025-04-10 **文档更新日期**2025-12-12
**文档版本**1.0 **文档版本**1.5
--- ---