Files
jj2/recommend_fund.php
LL 6c168246ca refactor: 使用FileManager统一文件操作
将分散在各处的文件操作统一封装到FileManager工具类中,包括文件读写、目录创建、JSON处理等操作
2025-12-12 14:22:40 +08:00

553 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 基金推荐页面 - 优化版
* 功能:基金推荐、真实数据展示、用户体验优化
*/
// 设置响应头
header('Content-Type: text/html; charset=utf-8');
// 引入工具类
require_once __DIR__ . '/utils/FileManager.php';
// 设置时区
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() {
FileManager::ensureDirExists(DATA_DIR);
FileManager::ensureDirExists(CACHE_DIR);
}
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';
return FileManager::loadJsonFile($file, []);
}
/**
* 清理不存在的基金
* @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';
FileManager::saveJsonFile($file, $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 = FileManager::loadJsonFile($logFile, []);
$logs[] = array_merge([
'action' => $action,
'timestamp' => date('Y-m-d H:i:s')
], $data);
FileManager::saveJsonFile($logFile, $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 (!FileManager::fileExists($cacheFile)) {
return false;
}
// 检查缓存是否过期
if (time() - FileManager::getFileMTime($cacheFile) > FUND_CACHE_TTL) {
return false;
}
$fundData = FileManager::loadJsonFile($cacheFile);
return is_array($fundData) ? $fundData : false;
}
/**
* 缓存基金数据
* @param string $fundCode 基金代码
* @param array $fundData 基金数据
*/
function cacheFundData($fundCode, $fundData) {
$cacheFile = getFundCachePath($fundCode);
FileManager::saveJsonFile($cacheFile, $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 = @FileManager::loadFile($apiUrl);
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';
FileManager::saveJsonFile($file, $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 = @FileManager::loadFile($cleanupFile);
$currentTime = time();
// 如果距离上次清理超过10分钟执行清理
if (!$lastCleanup || ($currentTime - $lastCleanup) > 600) {
$removedCount = cleanNonExistentFunds();
FileManager::saveFile($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>