iOS 提交

This commit is contained in:
Developer
2026-06-27 04:57:00 +08:00
parent 10a917adf6
commit b8d0bd39b5
20 changed files with 5403 additions and 247 deletions

View File

@@ -276,6 +276,11 @@ class Feed extends Api
}
$cached = Cache::get($cacheKey);
if ($cached && !$hasSeenIds) {
// v2.6: 缓存命中后重新附加当前用户的 is_liked/is_favorited 状态
// 缓存中的 per-user 字段可能来自其他用户,不可信,必须按当前用户重查
if (isset($cached['list']) && !empty($cached['list'])) {
$this->_batchAttachInteractionCounts($cached['list'], $this->_getUserId());
}
$this->success('成功(cache)', $cached);
return;
}
@@ -421,6 +426,11 @@ class Feed extends Api
unset($item);
}
// 批量附加互动计数(like_count/favorite_count/comment_count/share_count)
// 修复: 原仅 /api/feed/detail 返回这些字段,列表接口缺失导致客户端按钮显示 0
// v2.6: 同时回填当前用户的 is_liked/is_favorited 状态,解决已收藏句子重复出现时状态丢失
$this->_batchAttachInteractionCounts($items, $this->_getUserId());
$result = [
'list' => $items,
'total' => $total,
@@ -432,7 +442,18 @@ class Feed extends Api
];
if (!$hasSeenIds) {
Cache::set($cacheKey, $result, 60);
// v2.6: 缓存前剥离 per-user 字段(is_liked/is_favorited)
// 这些字段是用户私有的,缓存后其他用户命中会串状态
// 缓存命中时由 _batchAttachInteractionCounts 重新按当前用户查询
$cacheResult = $result;
if (isset($cacheResult['list'])) {
foreach ($cacheResult['list'] as &$cacheItem) {
$cacheItem['is_liked'] = false;
$cacheItem['is_favorited'] = false;
}
unset($cacheItem);
}
Cache::set($cacheKey, $cacheResult, 60);
}
$this->success('成功', $result);
@@ -2682,41 +2703,102 @@ class Feed extends Api
/**
* @name 清除Feed相关缓存
* @desc 在互动操作写入后主动清除相关缓存,确保数据一致性
* @lastUpdate v2.5 修复 _lite 后缀缓存未清理 + Cache::rm 不生效的 bug
* 1) 增加 _lite 后缀变体循环
* 2) 在 Cache::rm 后调用 _unlinkCacheFile 兜底,直接删除缓存文件
* 绕过 opcache 缓存旧代码 / ThinkPHP complex 驱动异常
*/
private function _clearFeedCache($feedType, $feedId)
{
try {
Cache::rm('feed_stats');
Cache::rm('feed_channels');
Cache::rm('feed_weight_config');
Cache::rm('feed_weight_config_api');
Cache::rm("feed_count_{$feedType}");
Cache::rm("feed_recommend_guest_20");
// 单键缓存
$singleKeys = [
'feed_stats',
'feed_channels',
'feed_weight_config',
'feed_weight_config_api',
"feed_count_{$feedType}",
'feed_recommend_guest_20',
];
foreach ($singleKeys as $sk) {
Cache::rm($sk);
$this->_unlinkCacheFile($sk);
}
// 列表缓存:覆盖 newest/hottest × 5 页 × 4 种 limit × 普通与 _lite × 平台后缀
$sorts = ['newest', 'hottest'];
$limits = [10, 20, 30, 50];
// 修复 v2.5: 必须覆盖客户端所有可能的 limit 值(含 5否则 limit=5 的缓存不会被清理
$limits = [5, 10, 15, 20, 25, 30, 40, 50];
$liteVariants = ['', '_lite']; // lite 模式变体
for ($p = 1; $p <= 5; $p++) {
foreach ($sorts as $sort) {
foreach ($limits as $lim) {
Cache::rm("feed_list_{$feedType}_{$sort}_{$p}_{$lim}");
Cache::rm("feed_list_all_{$sort}_{$p}_{$lim}");
// 带平台后缀
foreach (self::$platforms as $pKey => $pName) {
Cache::rm("feed_list_{$feedType}_{$sort}_{$p}_{$lim}_plat_{$pKey}");
Cache::rm("feed_list_all_{$sort}_{$p}_{$lim}_plat_{$pKey}");
foreach ($liteVariants as $liteSuffix) {
$k1 = "feed_list_{$feedType}_{$sort}_{$p}_{$lim}{$liteSuffix}";
$k2 = "feed_list_all_{$sort}_{$p}_{$lim}{$liteSuffix}";
Cache::rm($k1);
Cache::rm($k2);
$this->_unlinkCacheFile($k1);
$this->_unlinkCacheFile($k2);
// 带平台后缀
foreach (self::$platforms as $pKey => $pName) {
$pk1 = "feed_list_{$feedType}_{$sort}_{$p}_{$lim}{$liteSuffix}_plat_{$pKey}";
$pk2 = "feed_list_all_{$sort}_{$p}_{$lim}{$liteSuffix}_plat_{$pKey}";
Cache::rm($pk1);
Cache::rm($pk2);
$this->_unlinkCacheFile($pk1);
$this->_unlinkCacheFile($pk2);
}
}
}
}
}
Cache::rm("feed_trending_{$feedType}_20");
Cache::rm("feed_trending_all_20");
// trending 缓存
$trendingKeys = [
"feed_trending_{$feedType}_20",
"feed_trending_all_20",
];
foreach ($trendingKeys as $tk) {
Cache::rm($tk);
$this->_unlinkCacheFile($tk);
}
foreach (self::$platforms as $pKey => $pName) {
Cache::rm("feed_trending_{$feedType}_20_{$pKey}");
Cache::rm("feed_trending_all_20_{$pKey}");
Cache::rm("feed_recommend_guest_20_plat_{$pKey}");
$platKeys = [
"feed_trending_{$feedType}_20_{$pKey}",
"feed_trending_all_20_{$pKey}",
"feed_recommend_guest_20_plat_{$pKey}",
];
foreach ($platKeys as $pk) {
Cache::rm($pk);
$this->_unlinkCacheFile($pk);
}
}
} catch (\Exception $e) {}
}
/**
* @name 兜底删除缓存文件
* @desc Cache::rm 在某些环境opcache 缓存旧代码 / complex 驱动异常)下不删除文件,
* 本方法直接按 ThinkPHP 5 File 驱动的路径规则计算缓存文件路径并 unlink
* @param string $cacheKey 缓存 key与 Cache::rm 入参一致)
* @return bool 文件已删除返回 true文件不存在或删除失败返回 false
*/
private function _unlinkCacheFile($cacheKey)
{
if (!defined('CACHE_PATH') || empty($cacheKey)) {
return false;
}
try {
$hash = md5($cacheKey);
$file = CACHE_PATH . substr($hash, 0, 2) . '/' . substr($hash, 2) . '.php';
if (is_file($file) && @unlink($file)) {
return true;
}
} catch (\Exception $e) {}
return false;
}
/**
* @name 清除权重配置缓存
* @desc 在install/sync操作后清除权重相关缓存
@@ -2730,7 +2812,8 @@ class Feed extends Api
Cache::rm('feed_stats');
Cache::rm('feed_channels');
$sorts = ['newest', 'hottest'];
$limits = [10, 20, 30, 50];
// 修复 v2.5: 必须覆盖客户端所有可能的 limit 值(含 5否则 limit=5 的缓存不会被清理
$limits = [5, 10, 15, 20, 25, 30, 40, 50];
for ($p = 1; $p <= 5; $p++) {
foreach ($sorts as $sort) {
foreach ($limits as $lim) {
@@ -2946,9 +3029,136 @@ class Feed extends Api
}
}
// 批量附加互动计数(like_count/favorite_count/comment_count/share_count)
// 修复: favorites/likes/history/readlater 列表也需返回真实计数
// v2.6: 同时回填当前用户的 is_liked/is_favorited 状态
$this->_batchAttachInteractionCounts($items, $this->_getUserId());
return $items;
}
/**
* @name 批量附加互动计数
* @desc 一次性查询 feed_interaction 表,按 (feed_type, feed_id, action) 分组 COUNT
* 回填到 items 的 like_count/favorite_count/comment_count/share_count 字段。
* 避免 N+1 查询,列表接口(list/favorites/likes/history/readlater)统一调用。
* @param array &$items 待附加计数的 items 数组(引用传递)
* @lastUpdate v2.4 新增; 修复客户端点赞按钮显示 0 的问题
*/
private function _batchAttachInteractionCounts(&$items, $userId = 0)
{
if (empty($items) || !is_array($items)) {
return;
}
// 收集所有 (feed_type, feed_id) 对
$pairs = [];
foreach ($items as $item) {
$feedType = $item['feed_type'] ?? '';
$feedId = intval($item['id'] ?? 0);
if (empty($feedType) || $feedId <= 0) {
continue;
}
$pairs[$feedType][] = $feedId;
}
if (empty($pairs)) {
// 无有效对,仍填充 0 防止客户端解析 null
foreach ($items as &$item) {
if (!isset($item['like_count'])) $item['like_count'] = 0;
if (!isset($item['favorite_count'])) $item['favorite_count'] = 0;
if (!isset($item['comment_count'])) $item['comment_count'] = 0;
if (!isset($item['share_count'])) $item['share_count'] = 0;
// v2.6: 回填当前用户互动状态(未登录默认 false
if (!isset($item['is_liked'])) $item['is_liked'] = false;
if (!isset($item['is_favorited'])) $item['is_favorited'] = false;
}
unset($item);
return;
}
// 构造 OR 查询条件: ((feed_type='poetry' AND feed_id IN (...)) OR (feed_type='wisdom' AND feed_id IN (...)) ...)
// 仅查询 like/favorite/comment/share 四种 action
$countMap = []; // key: "feed_type_feed_id" => ["like"=>n, "favorite"=>n, ...]
try {
foreach ($pairs as $feedType => $ids) {
$ids = array_unique($ids);
$rows = Db::name('feed_interaction')
->field('feed_id, action, COUNT(*) AS cnt')
->where('feed_type', $feedType)
->where('feed_id', 'in', $ids)
->where('action', 'in', ['like', 'favorite', 'comment', 'share'])
->group('feed_id, action')
->select();
foreach ($rows as $row) {
$fid = intval($row['feed_id']);
$action = $row['action'];
$cnt = intval($row['cnt']);
$key = $feedType . '_' . $fid;
if (!isset($countMap[$key])) {
$countMap[$key] = [
'like' => 0,
'favorite' => 0,
'comment' => 0,
'share' => 0,
];
}
$countMap[$key][$action] = $cnt;
}
}
} catch (\Exception $e) {
// 查询失败时降级为 0
}
// v2.6 新增:批量查询当前用户的互动状态,回填 is_liked/is_favorited
// 解决list 接口不返回用户个人状态,导致已收藏句子重复出现时显示"未收藏"
$userActionMap = []; // key: "feed_type_feed_id" => ["like"=>bool, "favorite"=>bool]
if (!empty($userId)) {
try {
foreach ($pairs as $feedType => $ids) {
$ids = array_unique($ids);
$userRows = Db::name('feed_interaction')
->field('feed_id, action')
->where('user_id', $userId)
->where('feed_type', $feedType)
->where('feed_id', 'in', $ids)
->where('action', 'in', ['like', 'favorite'])
->select();
foreach ($userRows as $row) {
$fid = intval($row['feed_id']);
$action = $row['action'];
$key = $feedType . '_' . $fid;
if (!isset($userActionMap[$key])) {
$userActionMap[$key] = [
'like' => false,
'favorite' => false,
];
}
$userActionMap[$key][$action] = true;
}
}
} catch (\Exception $e) {
// 查询失败时降级为 false
}
}
// 回填到 items
foreach ($items as &$item) {
$feedType = $item['feed_type'] ?? '';
$feedId = intval($item['id'] ?? 0);
$key = $feedType . '_' . $feedId;
$counts = $countMap[$key] ?? null;
$item['like_count'] = $counts ? $counts['like'] : 0;
$item['favorite_count'] = $counts ? $counts['favorite'] : 0;
$item['comment_count'] = $counts ? $counts['comment'] : 0;
$item['share_count'] = $counts ? $counts['share'] : 0;
// v2.6: 回填当前用户互动状态
$userActions = $userActionMap[$key] ?? null;
$item['is_liked'] = $userActions ? $userActions['like'] : false;
$item['is_favorited'] = $userActions ? $userActions['favorite'] : false;
}
unset($item);
}
/**
* @name 获取权重配置
* @desc 从数据库读取管理员设置的推荐权重配置,带缓存