567 lines
20 KiB
PHP
567 lines
20 KiB
PHP
<?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> © 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>
|