From 69ef47412d8a8a9b34771dd307644e239dcdfb8a Mon Sep 17 00:00:00 2001 From: LL Date: Fri, 12 Dec 2025 17:10:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=9F=BA=E9=87=91?= =?UTF-8?q?=E9=82=AE=E4=BB=B6=E5=AE=9A=E6=97=B6=E5=8F=91=E9=80=81=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E9=82=AE=E4=BB=B6=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增宝塔面板定时任务脚本 cron_send_email.php - 添加手动发送邮件按钮到管理界面 - 使用PHPMailer替代原始SMTP实现 - 优化邮件内容格式,按涨跌幅排序基金数据 - 增加邮件发送时间窗口判断逻辑 - 更新项目文档说明定时任务配置 --- admin.js | 40 ++++++ admin.php | 1 + admin_api.php | 71 ++++++++-- api.php | 283 +++++++++++++++++++--------------------- cron_email_log.txt | 1 + cron_send_email.php | 32 +++++ data/email_status.json | 4 +- data/fund_history.json | 22 ++-- data/operation_log.json | 43 ++++-- data/stats.json | 13 +- data/visits.json | 21 +++ send_fund_email.php | 44 +++++++ 项目说明文档.md | 17 ++- 13 files changed, 395 insertions(+), 197 deletions(-) create mode 100644 cron_email_log.txt create mode 100644 cron_send_email.php create mode 100644 send_fund_email.php diff --git a/admin.js b/admin.js index 91534df..dea78d5 100644 --- a/admin.js +++ b/admin.js @@ -2,6 +2,46 @@ let fundsData = []; let fundChart = null; +// 页面加载完成后初始化 + +// 邮件发送按钮点击事件 +document.addEventListener('DOMContentLoaded', function() { + const sendBtn = document.getElementById('sendFundEmail'); + if (sendBtn) { + sendBtn.addEventListener('click', async function() { + this.disabled = true; + this.innerHTML = ' 发送中...'; + 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() { loadFundsData(); diff --git a/admin.php b/admin.php index e37b799..387db1f 100644 --- a/admin.php +++ b/admin.php @@ -193,6 +193,7 @@ if (isset($_GET['action']) && $_GET['action'] === 'logout') {

邮箱设置

+
diff --git a/admin_api.php b/admin_api.php index a325e56..2230bba 100644 --- a/admin_api.php +++ b/admin_api.php @@ -1,9 +1,59 @@ 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 { private $configFile = 'data/fund_config.json'; private $operationFile = 'data/operation_log.json'; @@ -1119,14 +1169,17 @@ class FundAdminAPI { } // 错误处理 -try { - $api = new FundAdminAPI(); - $api->handleRequest(); -} catch (Exception $e) { - http_response_code(500); - echo json_encode([ - 'success' => false, - 'message' => '服务器内部错误: ' . $e->getMessage() - ]); +// 只有非sendFundEmail请求才会执行到这里 +if (!$isSendFundEmailRequest) { + try { + $api = new FundAdminAPI(); + $api->handleRequest(); + } catch (Exception $e) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'message' => '服务器内部错误: ' . $e->getMessage() + ]); + } } ?> \ No newline at end of file diff --git a/api.php b/api.php index 2897385..c98d88f 100644 --- a/api.php +++ b/api.php @@ -33,6 +33,9 @@ class FundMonitorAPI { private $emailStatusFile = 'data/email_status.json'; public function __construct() { + // 设置时区为上海 + date_default_timezone_set('Asia/Shanghai'); + // 确保数据目录存在 if (!is_dir('data')) { mkdir('data', 0755, true); @@ -224,9 +227,13 @@ class FundMonitorAPI { } /** - * 发送邮件 - 使用Socket直接连接SMTP服务器 + * 发送邮件 - 使用PHPMailer库实现 */ 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; $toEmails = $config['to_emails']; @@ -238,92 +245,46 @@ class FundMonitorAPI { $smtpPassword = $config['password']; $fromName = $config['from_name']; $fromEmail = $config['from_email']; - - // 发送邮件到所有收件人 + + $mail = new PHPMailer\PHPMailer\PHPMailer(true); $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; + + try { + // 服务器配置 + $mail->isSMTP(); + $mail->Host = $smtpServer; + $mail->SMTPAuth = true; + $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); } + + // 邮件内容 + $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; + $errorLog .= "邮件发送失败: " . $mail->ErrorInfo . "\n"; + $errorLog .= "异常信息: " . $e->getMessage() . "\n"; } - + // 更新邮件状态 $status = $this->loadEmailStatus(); if ($success) { @@ -336,7 +297,7 @@ class FundMonitorAPI { 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; } @@ -365,66 +326,39 @@ class FundMonitorAPI { $summary = $fundData['summary']; $content .= "

总体统计

"; $content .= ""; // 基金详情 $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 .= ""; - $content .= ""; - $content .= ""; - $content .= ""; - $content .= ""; - $content .= ""; - $content .= ""; - $content .= ""; - $content .= ""; + // 确保有基金数据 + if (isset($fundData['fundsData']) && !empty($fundData['fundsData'])) { + foreach ($fundData['fundsData'] as $fundCode => $fund) { + // 涨跌颜色 + $changeColor = floatval($fund['gszzl'] ?? 0) > 0 ? '#009900' : '#ff0000'; + $yesterdayChangeColor = floatval($fund['yesterday_change'] ?? 0) > 0 ? '#009900' : '#ff0000'; + + $content .= ""; + $content .= ""; + $content .= ""; + $content .= ""; + $content .= ""; + $content .= ""; + $content .= ""; + } + } else { + $content .= ""; } $content .= "
基金代码基金名称当前净值涨跌幅昨日涨跌幅投资金额当前价值盈亏基金代码基金名称当前净值昨日涨跌幅涨跌幅
{$fundCode}{$fund['name']}{$fund['gsz']}{$fund['gszzl']}%{$fund['yesterday_change']}%{$this->formatCurrency($investment)}{$this->formatCurrency($currentValue)}{$this->formatCurrency($profitLoss)} ({$this->formatNumber($profitLossRate)}%)
{$fundCode}{$fund['name']}" . number_format(floatval($fund['gsz'] ?? 0), 4) . "" . number_format(floatval($fund['yesterday_change'] ?? 0), 2) . "%" . number_format(floatval($fund['gszzl'] ?? 0), 2) . "%
暂无基金数据
"; - // 渠道统计 - $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 .= ""; - $content .= ""; - $content .= ""; - $content .= ""; - $content .= ""; - $content .= ""; - } - $content .= "
渠道基金数量投资金额当前价值盈亏
{$channelName}{$channelData['fund_count']}{$this->formatCurrency($channelData['total_investment'])}{$this->formatCurrency($channelData['total_current_value'])}{$this->formatCurrency($profitLoss)}
"; + // 渠道统计部分已移除 return $content; } @@ -433,6 +367,9 @@ class FundMonitorAPI { * 判断是否应该发送邮件 */ private function shouldSendEmail() { + // 设置时区为北京时间 + date_default_timezone_set('Asia/Shanghai'); + $now = new DateTime(); $currentDay = intval($now->format('w')); $currentHour = intval($now->format('H')); @@ -446,8 +383,18 @@ class FundMonitorAPI { return false; } - // 检查是否在设定的时间范围内(14:30) - if ($currentHour != $schedule['hour'] || $currentMinute < $schedule['minute']) { + // 检查是否在设定的时间窗口内(14:30 ± 5分钟) + // 这样可以避免因为服务器时间稍微偏差或者定时任务执行延迟而导致的问题 + $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; } @@ -461,21 +408,46 @@ class FundMonitorAPI { /** * 发送基金状态邮件 + * @param bool $forceSend 是否强制发送(绕过时间和日期限制) */ - public function sendFundStatusEmail() { - if (!$this->shouldSendEmail()) { + public function sendFundStatusEmail($forceSend = false) { + if (!$forceSend && !$this->shouldSendEmail()) { return false; } // 获取基金数据 $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日'); - $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; $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 = [ 'success' => true, @@ -1523,16 +1503,19 @@ class FundMonitorAPI { } } -// 错误处理 -try { - // 处理API请求 - $api = new FundMonitorAPI(); - $api->handleRequest(); -} catch (Exception $e) { - http_response_code(500); - echo json_encode([ - 'success' => false, - 'error' => '服务器内部错误: ' . $e->getMessage() - ]); +// 只有当直接访问此文件时才处理API请求 +if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) { + // 错误处理 + try { + // 处理API请求 + $api = new FundMonitorAPI(); + $api->handleRequest(); + } catch (Exception $e) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => '服务器内部错误: ' . $e->getMessage() + ]); + } } ?> diff --git a/cron_email_log.txt b/cron_email_log.txt new file mode 100644 index 0000000..501b798 --- /dev/null +++ b/cron_email_log.txt @@ -0,0 +1 @@ +2025-12-12 15:52:12 - 邮件发送成功 diff --git a/cron_send_email.php b/cron_send_email.php new file mode 100644 index 0000000..28fc9a1 --- /dev/null +++ b/cron_send_email.php @@ -0,0 +1,32 @@ +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); +} diff --git a/data/email_status.json b/data/email_status.json index 818364b..49f970e 100644 --- a/data/email_status.json +++ b/data/email_status.json @@ -1,6 +1,6 @@ { "last_sent_date": "2025-12-12", - "last_sent_time": "04:17:24", - "sent_count": 7, + "last_sent_time": "15:52:12", + "sent_count": 26, "failed_count": 6 } \ No newline at end of file diff --git a/data/fund_history.json b/data/fund_history.json index 9890e31..656241b 100644 --- a/data/fund_history.json +++ b/data/fund_history.json @@ -61,57 +61,57 @@ "today_data": { "003766": { "date": "2025-12-12", - "change": 0.6, + "change": 0.97, "name": "广发创业板ETF发起式联接C" }, "008327": { "date": "2025-12-12", - "change": 1.63, + "change": 1.38, "name": "东财通信C" }, "012863": { "date": "2025-12-12", - "change": -0.63, + "change": 0.18, "name": "汇添富中证电池主题ETF发起式联接C" }, "023350": { "date": "2025-12-12", - "change": 0.56, + "change": -0.29, "name": "诺安多策略混合C" }, "017560": { "date": "2025-12-12", - "change": 0.48, + "change": 2.1, "name": "华安上证科创板芯片ETF发起式联接C" }, "011815": { "date": "2025-12-12", - "change": 0.58, + "change": 0.69, "name": "恒越优势精选混合A" }, "003598": { "date": "2025-12-12", - "change": 0.47, + "change": 0.61, "name": "华商润丰灵活配置混合A" }, "004206": { "date": "2025-12-12", - "change": 0.5, + "change": 0.77, "name": "华商元亨混合A" }, "022365": { "date": "2025-12-12", - "change": 1.14, + "change": 1.19, "name": "永赢科技智选混合发起C" }, "022364": { "date": "2025-12-12", - "change": 1.14, + "change": 1.19, "name": "永赢科技智选混合发起A" }, "011103": { "date": "2025-12-12", - "change": 1.08, + "change": 1.98, "name": "天弘中证光伏产业指数C" } }, diff --git a/data/operation_log.json b/data/operation_log.json index 259422e..3f75bd7 100644 --- a/data/operation_log.json +++ b/data/operation_log.json @@ -1,75 +1,90 @@ { "0": { - "timestamp": "2025-12-12 04:47:50", + "timestamp": "2025-12-12 08:46:41", "action": "管理员登录", "ip": "::1" }, "1": { - "timestamp": "2025-12-12 04:17:07", + "timestamp": "2025-12-12 08:05:44", "action": "管理员登录", "ip": "::1" }, "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", "action": "管理员登录", "ip": "113.87.139.184" }, - "3": { + "6": { "timestamp": "2025-11-06 08:31:27", "action": "管理员登录", "ip": "113.87.138.191" }, - "4": { + "7": { "timestamp": "2025-11-05 14:45:57", "action": "管理员登录", "ip": "113.87.138.191" }, - "5": { + "8": { "timestamp": "2025-11-05 14:34:30", "action": "管理员登录", "ip": "113.87.138.191" }, - "6": { + "9": { "timestamp": "2025-11-05 09:47:16", "action": "管理员登录", "ip": "113.87.138.191" }, - "7": { + "10": { "timestamp": "2025-11-01 23:18:46", "action": "管理员登录", "ip": "113.87.139.206" }, - "8": { + "11": { "timestamp": "2025-10-31 23:11:13", "action": "管理员登录", "ip": "113.84.8.124" }, - "9": { + "12": { "timestamp": "2025-10-31 10:20:41", "action": "管理员注销", "ip": "113.87.137.255" }, - "10": { + "13": { "timestamp": "2025-10-31 10:18:31", "action": "管理员登录", "ip": "113.87.137.255" }, - "11": { + "14": { "timestamp": "2025-10-31 09:08:47", "action": "管理员登录", "ip": "113.87.137.255" }, - "12": { + "15": { "timestamp": "2025-10-30 21:05:23", "action": "管理员登录", "ip": "113.87.139.170" }, - "13": { + "16": { "timestamp": "2025-10-30 17:28:24", "action": "管理员注销", "ip": "113.87.137.255" }, - "14": { + "17": { "timestamp": "2025-10-30 17:14:21", "action": "管理员登录", "ip": "113.87.137.255" diff --git a/data/stats.json b/data/stats.json index 22469c6..23e43c1 100644 --- a/data/stats.json +++ b/data/stats.json @@ -1,7 +1,7 @@ { - "total_visits": 618, - "today_visits": 5, - "unique_visitors": 64, + "total_visits": 621, + "today_visits": 8, + "unique_visitors": 65, "last_reset": "2025-12-12", "daily_stats": { "2025-10-28": 55, @@ -23,7 +23,7 @@ "2025-11-15": 1, "2025-11-17": 5, "2025-11-18": 1, - "2025-12-12": 5 + "2025-12-12": 8 }, "unique_ips": [ "113.87.138.21", @@ -89,7 +89,8 @@ "113.87.136.32", "205.169.39.16", "113.84.33.13", - "::1" + "::1", + "Unknown" ], - "today_unique_visitors": 1 + "today_unique_visitors": 2 } \ No newline at end of file diff --git a/data/visits.json b/data/visits.json index bb06bd1..9674487 100644 --- a/data/visits.json +++ b/data/visits.json @@ -4324,5 +4324,26 @@ "timestamp": 1765511264, "date": "2025-12-12 04:47:44", "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" } ] \ No newline at end of file diff --git a/send_fund_email.php b/send_fund_email.php new file mode 100644 index 0000000..9b59a76 --- /dev/null +++ b/send_fund_email.php @@ -0,0 +1,44 @@ + 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); \ No newline at end of file diff --git a/项目说明文档.md b/项目说明文档.md index 3c173fe..0cf57a6 100644 --- a/项目说明文档.md +++ b/项目说明文档.md @@ -176,6 +176,11 @@ - 系统默认在工作日14:30自动发送基金状态邮件 - 可通过`test_fund_email.php`手动测试邮件发送 - 可在`api.php`中修改发送时间配置 +- **宝塔面板定时触发**:使用专用脚本`cron_send_email.php`支持宝塔面板定时任务调用,无需管理员权限 + ```bash + C:/xampp/php/php.exe C:/xampp/htdocs/2/cron_send_email.php + ``` + (需根据实际安装路径调整PHP和脚本路径) ## 8. 代码结构和核心类 @@ -185,12 +190,12 @@ **主要方法:** -- `__construct()`:类初始化,创建必要的目录和配置文件 +- `__construct()`:类初始化,创建必要的目录和配置文件,设置时区 - `loadEmailConfig()`:加载邮箱配置 - `sendEmail()`:使用Socket直接连接SMTP服务器发送邮件 -- `shouldSendEmail()`:判断是否应该发送邮件(时间条件检查) +- `shouldSendEmail()`:判断是否应该发送邮件(时间条件检查,支持±5分钟误差) - `sendFundStatusEmail()`:发送基金状态邮件 -- `getFundData()`:获取基金实时数据 +- `getFundData()`:获取基金实时数据(按涨跌幅从高到低排序) - `getFundChartData()`:获取基金历史净值数据 - `updateStats()`:更新访问统计 @@ -338,6 +343,8 @@ function logDebug($message) { | 1.1 | 2025-02-20 | 添加邮件推送功能 | | 1.2 | 2025-03-10 | 优化基金数据缓存机制 | | 1.3 | 2025-04-05 | 添加深色主题支持 | +| 1.4 | 2025-12-12 | 更新邮件内容格式:将"涨跌幅"与"昨日涨跌幅"列互换位置,并确保基金数据按涨跌幅从高到低排序 | +| 1.5 | 2025-12-12 | 优化邮件推送时间判断逻辑,添加时区设置,创建专用定时任务脚本 `cron_send_email.php` 支持宝塔面板定时触发 ## 14. 联系方式 @@ -345,8 +352,8 @@ function logDebug($message) { --- -**文档更新日期**:2025-04-10 -**文档版本**:1.0 +**文档更新日期**:2025-12-12 +**文档版本**:1.5 ---