feat: 添加基金监控系统基础功能

添加基金监控系统相关文件,包括邮件发送功能、基金数据配置、测试脚本等。主要包含以下内容:

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>
This commit is contained in:
LL
2025-12-12 14:14:07 +08:00
commit 0cfefbebd8
106 changed files with 26164 additions and 0 deletions

566
recommend_fund.php Normal file
View File

@@ -0,0 +1,566 @@
<?php
/**
* 基金推荐页面 - 优化版
* 功能:基金推荐、真实数据展示、用户体验优化
*/
// 设置响应头
header('Content-Type: text/html; charset=utf-8');
// 设置时区
date_default_timezone_set('Asia/Shanghai');
// 启动会话以保存消息
session_start();
// 配置常量
const DATA_DIR = __DIR__ . '/data';
const CACHE_DIR = DATA_DIR . '/cache';
const FUND_CACHE_TTL = 300; // 基金数据缓存5分钟
const MAX_FUNDS_PER_IP = 3; // 每IP最大推荐数量
const MAX_TOTAL_FUNDS = 3000; // 系统最大基金数量
// 确保目录存在
function ensureDirsExist() {
if (!file_exists(DATA_DIR)) {
mkdir(DATA_DIR, 0755, true);
}
if (!file_exists(CACHE_DIR)) {
mkdir(CACHE_DIR, 0755, true);
}
}
ensureDirsExist();
/**
* 获取用户真实IP
* @return string 用户IP地址
*/
function getUserIP() {
$ip = $_SERVER['REMOTE_ADDR'];
// 检查代理IP
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
// 处理多个IP的情况逗号分隔
$ipList = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$ip = trim($ipList[0]);
}
return $ip;
}
/**
* 加载已推荐基金数据
* @return array 推荐基金数据
*/
function loadRecommendedFunds() {
$file = DATA_DIR . '/recommended_funds.json';
if (file_exists($file)) {
$data = file_get_contents($file);
return json_decode($data, true) ?? [];
}
return [];
}
/**
* 清理不存在的基金
* @return int 移除的基金数量
*/
function cleanNonExistentFunds() {
$funds = loadRecommendedFunds();
$fundsToKeep = [];
$fundsRemoved = 0;
foreach ($funds as $fund) {
if (isset($fund['fund_code']) && checkFundExists($fund['fund_code'])) {
$fundsToKeep[] = $fund;
} else {
$fundsRemoved++;
}
}
// 如果有基金被移除,保存更新后的列表
if ($fundsRemoved > 0) {
$file = DATA_DIR . '/recommended_funds.json';
file_put_contents($file, json_encode($fundsToKeep, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// 记录清理日志
logAction('clean_nonexistent_funds', ['funds_removed' => $fundsRemoved]);
}
return $fundsRemoved;
}
/**
* 记录操作日志
* @param string $action 操作类型
* @param array $data 操作数据
*/
function logAction($action, $data = []) {
$logFile = DATA_DIR . '/recommend_logs.json';
$logs = [];
if (file_exists($logFile)) {
$logs = json_decode(file_get_contents($logFile), true) ?? [];
}
$logs[] = array_merge([
'action' => $action,
'timestamp' => date('Y-m-d H:i:s')
], $data);
file_put_contents($logFile, json_encode($logs, JSON_PRETTY_PRINT));
}
/**
* 获取基金缓存文件路径
* @param string $fundCode 基金代码
* @return string 缓存文件路径
*/
function getFundCachePath($fundCode) {
return CACHE_DIR . "/fund_{$fundCode}.json";
}
/**
* 从缓存获取基金数据
* @param string $fundCode 基金代码
* @return array|false 基金数据或false
*/
function getFundFromCache($fundCode) {
$cacheFile = getFundCachePath($fundCode);
if (!file_exists($cacheFile)) {
return false;
}
// 检查缓存是否过期
if (time() - filemtime($cacheFile) > FUND_CACHE_TTL) {
return false;
}
$data = file_get_contents($cacheFile);
if ($data === false) {
return false;
}
$fundData = json_decode($data, true);
return is_array($fundData) ? $fundData : false;
}
/**
* 缓存基金数据
* @param string $fundCode 基金代码
* @param array $fundData 基金数据
*/
function cacheFundData($fundCode, $fundData) {
$cacheFile = getFundCachePath($fundCode);
file_put_contents($cacheFile, json_encode($fundData, JSON_UNESCAPED_UNICODE));
}
/**
* 从API获取基金信息
* @param string $fundCode 基金代码
* @return array|false 基金数据或false
*/
function fetchFundDataFromAPI($fundCode) {
$apiUrl = "http://fundgz.1234567.com.cn/js/{$fundCode}.js?" . time();
$context = stream_context_create([
'http' => [
'timeout' => 5,
'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;
}
// 解析返回的数据
if (preg_match('/jsonpgz\d*\((.*)\);?/', $response, $matches)) {
$data = json_decode($matches[1], true);
// 检查数据是否有效
if (is_array($data) && isset($data['fundcode']) && $data['fundcode'] == $fundCode) {
return $data;
}
}
return false;
}
/**
* 检查基金是否存在
* @param string $fundCode 基金代码
* @return bool 基金是否存在
*/
function checkFundExists($fundCode) {
// 基金代码格式验证
if (!preg_match('/^\d{6}$/', $fundCode)) {
return false;
}
// 尝试从缓存获取
$cachedData = getFundFromCache($fundCode);
if ($cachedData) {
return true;
}
// 从API获取
$fundData = fetchFundDataFromAPI($fundCode);
if ($fundData) {
// 缓存基金数据
cacheFundData($fundCode, $fundData);
return true;
}
return false;
}
/**
* 获取基金详细信息
* @param string $fundCode 基金代码
* @return array|false 基金详细数据或false
*/
function getFundInfo($fundCode) {
// 先尝试从缓存获取
$cachedData = getFundFromCache($fundCode);
if ($cachedData) {
return $cachedData;
}
// 从API获取
$fundData = fetchFundDataFromAPI($fundCode);
if ($fundData) {
// 缓存基金数据
cacheFundData($fundCode, $fundData);
return $fundData;
}
return false;
}
/**
* 保存推荐基金
* @param string $fundCode 基金代码
* @param string $ip 用户IP
* @return array 操作结果
*/
function saveRecommendedFund($fundCode, $ip) {
// 首先检查基金号是否存在
if (!checkFundExists($fundCode)) {
return ['success' => false, 'message' => '基金号不存在请输入正确的6位基金代码'];
}
// 检查数量限制
$funds = loadRecommendedFunds();
$timestamp = date('Y-m-d H:i:s');
// 检查基金总数是否已达到上限
if (count($funds) >= MAX_TOTAL_FUNDS) {
return ['success' => false, 'message' => "推荐基金数量已达到上限({MAX_TOTAL_FUNDS}支)"];
}
// 检查IP推荐次数和基金是否已被推荐
$ipCount = 0;
foreach ($funds as $fund) {
if ($fund['ip'] == $ip) {
$ipCount++;
}
if ($fund['fund_code'] == $fundCode) {
return ['success' => false, 'message' => '该基金已被推荐过'];
}
}
if ($ipCount >= MAX_FUNDS_PER_IP) {
return ['success' => false, 'message' => "每个IP最多只能推荐{MAX_FUNDS_PER_IP}只基金"];
}
// 获取基金信息
$fundInfo = getFundInfo($fundCode);
$fundName = $fundInfo ? ($fundInfo['name'] ?? '未知基金') : '未知基金';
// 添加新基金
$newFund = [
'fund_code' => $fundCode,
'ip' => $ip,
'timestamp' => $timestamp,
'channel' => '网站推荐',
'amount' => 1000,
'status' => 'pending', // 默认设为待审核,需要管理员批准
'fund_name' => $fundName // 保存基金名称
];
$funds[] = $newFund;
// 保存到文件
$file = DATA_DIR . '/recommended_funds.json';
file_put_contents($file, json_encode($funds, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// 记录操作日志
logAction('recommend_fund', [
'fund_code' => $fundCode,
'fund_name' => $fundName,
'ip' => $ip
]);
return ['success' => true, 'message' => '基金推荐成功!'];
}
// 处理推荐请求
// 从session中获取消息如果有
$message = isset($_SESSION['message']) ? $_SESSION['message'] : '';
$messageType = isset($_SESSION['messageType']) ? $_SESSION['messageType'] : '';
// 清除session中的消息防止刷新页面再次显示
unset($_SESSION['message'], $_SESSION['messageType']);
// 定期清理不存在的基金每10分钟执行一次
$cleanupFile = DATA_DIR . '/last_cleanup.txt';
$lastCleanup = @file_get_contents($cleanupFile);
$currentTime = time();
// 如果距离上次清理超过10分钟执行清理
if (!$lastCleanup || ($currentTime - $lastCleanup) > 600) {
$removedCount = cleanNonExistentFunds();
file_put_contents($cleanupFile, $currentTime);
// 如果有基金被清理,记录日志但不显示给用户
if ($removedCount > 0) {
error_log("清理了{$removedCount}个不存在的基金");
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['fund_code'])) {
$fundCode = trim($_POST['fund_code']);
$ip = getUserIP();
// 简单验证基金代码格式假设为6位数字
if (!preg_match('/^\d{6}$/', $fundCode)) {
$_SESSION['message'] = '基金代码格式不正确请输入6位数字';
$_SESSION['messageType'] = 'error';
} else {
$result = saveRecommendedFund($fundCode, $ip);
$_SESSION['message'] = $result['message'];
$_SESSION['messageType'] = $result['success'] ? 'success' : 'error';
}
// 重定向到同一页面实现POST-REDIRECT-GET模式
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
// 获取IP推荐次数
$userIp = getUserIP();
$recommendedFunds = loadRecommendedFunds();
$userRecommendCount = 0;
foreach ($recommendedFunds as $fund) {
if (isset($fund['ip']) && $fund['ip'] == $userIp) {
$userRecommendCount++;
}
}
$totalFunds = count($recommendedFunds);
// 预加载部分基金数据到页面中,提高初始加载速度
$preloadedFundData = [];
$preloadLimit = 10; // 预加载10个基金的数据
$preloadCount = 0;
foreach ($recommendedFunds as $fund) {
if ($preloadCount >= $preloadLimit) break;
$fundCode = $fund['fund_code'] ?? '';
if (!empty($fundCode)) {
$fundInfo = getFundInfo($fundCode);
if ($fundInfo) {
$preloadedFundData[$fundCode] = $fundInfo;
$preloadCount++;
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>基金推荐 - 组合基金监控</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="recommend_fund.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="header-content">
<h1><i class="fas fa-chart-line"></i> 基金推荐</h1>
<p>推荐您关注的优质基金</p>
</div>
</div>
<div class="recommend-container">
<a href="index.php" class="back-btn"><i class="fas fa-arrow-left"></i> 返回主页</a>
<div class="stats-display">
<div class="stat-item">
<div class="stat-icon"><i class="fas fa-user-check"></i></div>
<div class="stat-number"><?php echo $userRecommendCount; ?></div>
<div class="stat-label">您已推荐基金数</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="fas fa-rotate-left"></i></div>
<div class="stat-number"><?php echo 3 - $userRecommendCount; ?></div>
<div class="stat-label">剩余推荐次数</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="fas fa-list-ul"></i></div>
<div class="stat-number"><?php echo $totalFunds; ?></div>
<div class="stat-label">总推荐基金数</div>
</div>
</div>
<?php if ($message): ?>
<div class="message <?php echo $messageType; ?>">
<?php echo $message; ?>
</div>
<?php endif; ?>
<div class="info-box">
<strong><i class="fas fa-info-circle"></i> 推荐规则:</strong>
<ul>
<li>每个IP地址最多只能推荐3只基金</li>
<li>系统最多接受3000支基金推荐</li>
<li>基金代码为6位数字</li>
<li>推荐的基金将由管理员审核</li>
</ul>
</div>
<?php if ($userRecommendCount >= 3 || $totalFunds >= 3000): ?>
<div class="warning-box">
<strong><i class="fas fa-exclamation-triangle"></i> 提示:</strong>
<?php if ($userRecommendCount >= 3): ?>
您已达到每个IP最多推荐3只基金的限制。
<?php else: ?>
系统已达到最大推荐基金数量(3000支)。
<?php endif; ?>
</div>
<?php else: ?>
<form class="recommend-form" method="POST" action="">
<h2><i class="fas fa-plus-circle"></i> 推荐新基金</h2>
<div class="form-group">
<label for="fund_code">基金代码 (6位数字)</label>
<input type="text" id="fund_code" name="fund_code" placeholder="请输入基金代码" required>
</div>
<button type="submit" class="submit-btn">
<i class="fas fa-paper-plane"></i> 提交推荐
</button>
</form>
<?php endif; ?>
<?php if (!empty($recommendedFunds)): ?>
<div class="fund-list">
<h3><i class="fas fa-list"></i> 已推荐基金列表</h3>
<!-- 排序工具条 -->
<div class="sort-toolbar" aria-label="排序工具条">
<button class="sort-btn active" data-sort="time" data-order="desc" title="按最新推荐时间排序">
<i class="fas fa-clock"></i> 推荐时间
<span class="order-indicator">↓</span>
</button>
<button class="sort-btn" data-sort="count" data-order="desc" title="按基金数量排序">
<i class="fas fa-layer-group"></i> 基金数
<span class="order-indicator">↓</span>
</button>
<button class="sort-btn" data-sort="avg" data-order="desc" title="按平均涨幅排序">
<i class="fas fa-chart-line"></i> 平均涨幅
<span class="order-indicator">↓</span>
</button>
<button class="sort-reset" type="button" title="重置为推荐时间倒序">
<i class="fas fa-rotate-right"></i> 重置排序
</button>
</div>
<!-- IP分组下拉框 -->
<div class="dropdown-container">
<?php
// 按IP分组
$ipGroups = [];
foreach ($recommendedFunds as $fund) {
if (!isset($ipGroups[$fund['ip']])) {
$ipGroups[$fund['ip']] = ['funds' => [], 'latest_time' => ''];
}
$ipGroups[$fund['ip']]['funds'][] = $fund;
// 记录最新时间
if ($fund['timestamp'] > $ipGroups[$fund['ip']]['latest_time']) {
$ipGroups[$fund['ip']]['latest_time'] = $fund['timestamp'];
}
}
// 按最新时间排序
uasort($ipGroups, function($a, $b) {
return strcmp($b['latest_time'], $a['latest_time']);
});
foreach ($ipGroups as $ip => $group):
$isUserIp = ($ip == $userIp);
$ipClass = $isUserIp ? 'user-ip' : '';
?>
<div class="dropdown-item <?php echo $ipClass; ?>" data-ip="<?php echo htmlspecialchars($ip); ?>" data-latest-time="<?php echo htmlspecialchars($group['latest_time']); ?>" data-fund-count="<?php echo count($group['funds']); ?>" data-avg-change="">
<div class="dropdown-header" data-dropdown-id="ip-<?php echo md5($ip); ?>">
<div class="dropdown-info">
<strong>IP</strong><?php echo $ip; ?>
<?php if ($isUserIp): ?>
<span class="ip-badge"><i class="fas fa-user"></i> 我的IP</span>
<?php endif; ?>
<span class="dropdown-time"><strong>最新推荐时间:</strong><?php echo $group['latest_time']; ?></span>
<span class="dropdown-count"><strong>推荐基金数:</strong><?php echo count($group['funds']); ?></span>
<span class="dropdown-total-change">平均涨幅:<span id="total-change-<?php echo md5($ip); ?>">--</span></span>
</div>
<div class="dropdown-arrow">
<i class="fas fa-chevron-down"></i>
</div>
</div>
<div class="dropdown-content" id="ip-<?php echo md5($ip); ?>">
<table class="fund-details-table">
<thead>
<tr>
<th>基金代码</th>
<th>推荐时间</th>
<th>基金名称</th>
<th>当前涨幅</th>
</tr>
</thead>
<tbody>
<?php foreach ($group['funds'] as $fund): ?>
<tr>
<td><?php echo $fund['fund_code']; ?></td>
<td><?php echo $fund['timestamp']; ?></td>
<td><span class="fund-name" data-fund-code="<?php echo $fund['fund_code']; ?>">加载中...</span></td>
<td><span class="fund-change" data-fund-code="<?php echo $fund['fund_code']; ?>">--</span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<div class="footer">
<p><i class="fas fa-info-circle"></i> 数据仅供参考,实际净值以基金公司公布为准</p>
<p><i class="fas fa-info-circle"></i> &copy; 2025 Tsama</p>
</div>
</div>
<script>
// 预加载的基金数据
const preloadedFundData = <?php echo json_encode($preloadedFundData, JSON_UNESCAPED_UNICODE); ?>;
// 将预加载数据传递给recommend_fund.js
window.preloadedFundData = preloadedFundData;
</script>
<script src="recommend_fund.js"></script>
</body>
</html>