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 .= "- 上涨基金:{$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 .= "- 上涨基金:{$summary['upCount']}只
";
+ $content .= "- 下跌基金:{$summary['downCount']}只
";
+ $content .= "- 持平基金:{$summary['flatCount']}只
";
$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 .= "| {$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 .= "
";
+ // 确保有基金数据
+ 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 .= "| {$fundCode} | ";
+ $content .= "{$fund['name']} | ";
+ $content .= "" . number_format(floatval($fund['gsz'] ?? 0), 4) . " | ";
+ $content .= "" . number_format(floatval($fund['yesterday_change'] ?? 0), 2) . "% | ";
+ $content .= "" . number_format(floatval($fund['gszzl'] ?? 0), 2) . "% | ";
+ $content .= "
";
+ }
+ } else {
+ $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;
}
@@ -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
---