fix: 跳过推荐空缓存

This commit is contained in:
jxxghp
2026-06-13 08:09:49 +08:00
parent 7d582cc4d8
commit ab9eeedb3e
2 changed files with 106 additions and 53 deletions
+24 -24
View File
@@ -105,7 +105,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
ImageHelper().fetch_image(url=url) ImageHelper().fetch_image(url=url)
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
def tmdb_movies(self, sort_by: Optional[str] = "popularity.desc", def tmdb_movies(self, sort_by: Optional[str] = "popularity.desc",
with_genres: Optional[str] = "", with_genres: Optional[str] = "",
with_original_language: Optional[str] = "", with_original_language: Optional[str] = "",
@@ -131,7 +131,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [movie.to_dict() for movie in movies] if movies else [] return [movie.to_dict() for movie in movies] if movies else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
def tmdb_tvs(self, sort_by: Optional[str] = "popularity.desc", def tmdb_tvs(self, sort_by: Optional[str] = "popularity.desc",
with_genres: Optional[str] = "", with_genres: Optional[str] = "",
with_original_language: Optional[str] = "zh|en|ja|ko", with_original_language: Optional[str] = "zh|en|ja|ko",
@@ -166,7 +166,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [info.to_dict() for info in infos] if infos else [] return [info.to_dict() for info in infos] if infos else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
def bangumi_calendar(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: def bangumi_calendar(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
Bangumi每日放送 Bangumi每日放送
@@ -175,7 +175,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in medias[(page - 1) * count: page * count]] if medias else [] return [media.to_dict() for media in medias[(page - 1) * count: page * count]] if medias else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
def douban_movie_showing(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: def douban_movie_showing(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
豆瓣正在热映 豆瓣正在热映
@@ -184,7 +184,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in movies] if movies else [] return [media.to_dict() for media in movies] if movies else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
def douban_movies(self, sort: Optional[str] = "R", tags: Optional[str] = "", def douban_movies(self, sort: Optional[str] = "R", tags: Optional[str] = "",
page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
@@ -195,7 +195,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in movies] if movies else [] return [media.to_dict() for media in movies] if movies else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
def douban_tvs(self, sort: Optional[str] = "R", tags: Optional[str] = "", def douban_tvs(self, sort: Optional[str] = "R", tags: Optional[str] = "",
page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
@@ -206,7 +206,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in tvs] if tvs else [] return [media.to_dict() for media in tvs] if tvs else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
def douban_movie_top250(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: def douban_movie_top250(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
豆瓣电影TOP250 豆瓣电影TOP250
@@ -215,7 +215,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in movies] if movies else [] return [media.to_dict() for media in movies] if movies else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
def douban_tv_weekly_chinese(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: def douban_tv_weekly_chinese(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
豆瓣国产剧集榜 豆瓣国产剧集榜
@@ -224,7 +224,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in tvs] if tvs else [] return [media.to_dict() for media in tvs] if tvs else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
def douban_tv_weekly_global(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: def douban_tv_weekly_global(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
豆瓣全球剧集榜 豆瓣全球剧集榜
@@ -233,7 +233,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in tvs] if tvs else [] return [media.to_dict() for media in tvs] if tvs else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
def douban_tv_animation(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: def douban_tv_animation(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
豆瓣热门动漫 豆瓣热门动漫
@@ -242,7 +242,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in tvs] if tvs else [] return [media.to_dict() for media in tvs] if tvs else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
def douban_movie_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: def douban_movie_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
豆瓣热门电影 豆瓣热门电影
@@ -251,7 +251,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in movies] if movies else [] return [media.to_dict() for media in movies] if movies else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
def douban_tv_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: def douban_tv_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
豆瓣热门电视剧 豆瓣热门电视剧
@@ -260,7 +260,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in tvs] if tvs else [] return [media.to_dict() for media in tvs] if tvs else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
async def async_tmdb_movies(self, sort_by: Optional[str] = "popularity.desc", async def async_tmdb_movies(self, sort_by: Optional[str] = "popularity.desc",
with_genres: Optional[str] = "", with_genres: Optional[str] = "",
with_original_language: Optional[str] = "", with_original_language: Optional[str] = "",
@@ -286,7 +286,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [movie.to_dict() for movie in movies] if movies else [] return [movie.to_dict() for movie in movies] if movies else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
async def async_tmdb_tvs(self, sort_by: Optional[str] = "popularity.desc", async def async_tmdb_tvs(self, sort_by: Optional[str] = "popularity.desc",
with_genres: Optional[str] = "", with_genres: Optional[str] = "",
with_original_language: Optional[str] = "zh|en|ja|ko", with_original_language: Optional[str] = "zh|en|ja|ko",
@@ -321,7 +321,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [info.to_dict() for info in infos] if infos else [] return [info.to_dict() for info in infos] if infos else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
async def async_bangumi_calendar(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: async def async_bangumi_calendar(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
异步Bangumi每日放送 异步Bangumi每日放送
@@ -330,7 +330,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in medias[(page - 1) * count: page * count]] if medias else [] return [media.to_dict() for media in medias[(page - 1) * count: page * count]] if medias else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
async def async_douban_movie_showing(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: async def async_douban_movie_showing(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
异步豆瓣正在热映 异步豆瓣正在热映
@@ -339,7 +339,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in movies] if movies else [] return [media.to_dict() for media in movies] if movies else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
async def async_douban_movies(self, sort: Optional[str] = "R", tags: Optional[str] = "", async def async_douban_movies(self, sort: Optional[str] = "R", tags: Optional[str] = "",
page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
@@ -350,7 +350,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in movies] if movies else [] return [media.to_dict() for media in movies] if movies else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
async def async_douban_tvs(self, sort: Optional[str] = "R", tags: Optional[str] = "", async def async_douban_tvs(self, sort: Optional[str] = "R", tags: Optional[str] = "",
page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
@@ -361,7 +361,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in tvs] if tvs else [] return [media.to_dict() for media in tvs] if tvs else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
async def async_douban_movie_top250(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: async def async_douban_movie_top250(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
异步豆瓣电影TOP250 异步豆瓣电影TOP250
@@ -370,7 +370,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in movies] if movies else [] return [media.to_dict() for media in movies] if movies else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
async def async_douban_tv_weekly_chinese(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: async def async_douban_tv_weekly_chinese(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
异步豆瓣国产剧集榜 异步豆瓣国产剧集榜
@@ -379,7 +379,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in tvs] if tvs else [] return [media.to_dict() for media in tvs] if tvs else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
async def async_douban_tv_weekly_global(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: async def async_douban_tv_weekly_global(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
异步豆瓣全球剧集榜 异步豆瓣全球剧集榜
@@ -388,7 +388,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in tvs] if tvs else [] return [media.to_dict() for media in tvs] if tvs else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
async def async_douban_tv_animation(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: async def async_douban_tv_animation(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
异步豆瓣热门动漫 异步豆瓣热门动漫
@@ -397,7 +397,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in tvs] if tvs else [] return [media.to_dict() for media in tvs] if tvs else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
async def async_douban_movie_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: async def async_douban_movie_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
异步豆瓣热门电影 异步豆瓣热门电影
@@ -406,7 +406,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
return [media.to_dict() for media in movies] if movies else [] return [media.to_dict() for media in movies] if movies else []
@log_execution_time(logger=logger) @log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region) @cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
async def async_douban_tv_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]: async def async_douban_tv_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
""" """
异步豆瓣热门电视剧 异步豆瓣热门电视剧
+79 -26
View File
@@ -1,42 +1,95 @@
import asyncio import asyncio
from unittest import TestCase from typing import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest
from app.chain.recommend import RecommendChain from app.chain.recommend import RecommendChain
from app.core.cache import TTLCache from app.core.cache import TTLCache
class RecommendChainTest(TestCase): SYNC_EMPTY_CACHE_CASES = [
def tearDown(self): ("tmdb_movies", "app.chain.recommend.TmdbChain", "tmdb_discover"),
""" ("tmdb_tvs", "app.chain.recommend.TmdbChain", "tmdb_discover"),
清理推荐缓存,避免缓存装饰器状态影响其他用例。 ("tmdb_trending", "app.chain.recommend.TmdbChain", "tmdb_trending"),
""" ("bangumi_calendar", "app.chain.recommend.BangumiChain", "calendar"),
RecommendChain.tmdb_trending.cache_clear() ("douban_movie_showing", "app.chain.recommend.DoubanChain", "movie_showing"),
asyncio.run(RecommendChain.async_tmdb_trending.cache_clear()) ("douban_movies", "app.chain.recommend.DoubanChain", "douban_discover"),
("douban_tvs", "app.chain.recommend.DoubanChain", "douban_discover"),
("douban_movie_top250", "app.chain.recommend.DoubanChain", "movie_top250"),
("douban_tv_weekly_chinese", "app.chain.recommend.DoubanChain", "tv_weekly_chinese"),
("douban_tv_weekly_global", "app.chain.recommend.DoubanChain", "tv_weekly_global"),
("douban_tv_animation", "app.chain.recommend.DoubanChain", "tv_animation"),
("douban_movie_hot", "app.chain.recommend.DoubanChain", "movie_hot"),
("douban_tv_hot", "app.chain.recommend.DoubanChain", "tv_hot"),
]
ASYNC_EMPTY_CACHE_CASES = [
("async_tmdb_movies", "app.chain.recommend.TmdbChain"),
("async_tmdb_tvs", "app.chain.recommend.TmdbChain"),
("async_tmdb_trending", "app.chain.recommend.TmdbChain"),
("async_bangumi_calendar", "app.chain.recommend.BangumiChain"),
("async_douban_movie_showing", "app.chain.recommend.DoubanChain"),
("async_douban_movies", "app.chain.recommend.DoubanChain"),
("async_douban_tvs", "app.chain.recommend.DoubanChain"),
("async_douban_movie_top250", "app.chain.recommend.DoubanChain"),
("async_douban_tv_weekly_chinese", "app.chain.recommend.DoubanChain"),
("async_douban_tv_weekly_global", "app.chain.recommend.DoubanChain"),
("async_douban_tv_animation", "app.chain.recommend.DoubanChain"),
("async_douban_movie_hot", "app.chain.recommend.DoubanChain"),
("async_douban_tv_hot", "app.chain.recommend.DoubanChain"),
]
def clear_recommend_cache() -> None:
"""清理推荐缓存,避免缓存装饰器状态影响用例。"""
TTLCache(region=RecommendChain.recommend_cache_region).clear() TTLCache(region=RecommendChain.recommend_cache_region).clear()
def test_tmdb_trending_does_not_cache_empty_result(self):
""" @pytest.fixture(autouse=True)
TMDB流行趋势返回空列表时不应缓存,避免一次接口异常后长时间固定为空。 def isolated_recommend_cache() -> Generator[None, None, None]:
""" """每个用例前后都清空推荐缓存。"""
clear_recommend_cache()
yield
clear_recommend_cache()
@pytest.mark.parametrize(
("method_name", "chain_target", "backend_method"),
SYNC_EMPTY_CACHE_CASES,
)
def test_sync_recommend_methods_do_not_cache_empty_result(
method_name: str,
chain_target: str,
backend_method: str,
) -> None:
"""同步推荐来源返回空列表时不应缓存。"""
chain = RecommendChain() chain = RecommendChain()
with patch("app.chain.recommend.TmdbChain") as tmdb_chain: recommend_method = getattr(chain, method_name)
tmdb_chain.return_value.tmdb_trending.side_effect = [[], []]
self.assertEqual(chain.tmdb_trending(page=1), []) with patch(chain_target) as backend_chain:
self.assertEqual(chain.tmdb_trending(page=1), []) backend_call = getattr(backend_chain.return_value, backend_method)
backend_call.side_effect = [[], []]
self.assertEqual(tmdb_chain.return_value.tmdb_trending.call_count, 2) assert recommend_method(page=1) == []
assert recommend_method(page=1) == []
def test_async_tmdb_trending_does_not_cache_empty_result(self): assert backend_call.call_count == 2
"""
异步TMDB流行趋势返回空列表时也不应缓存。
""" @pytest.mark.parametrize(("method_name", "chain_target"), ASYNC_EMPTY_CACHE_CASES)
def test_async_recommend_methods_do_not_cache_empty_result(
method_name: str,
chain_target: str,
) -> None:
"""异步推荐来源返回空列表时不应缓存。"""
chain = RecommendChain() chain = RecommendChain()
with patch("app.chain.recommend.TmdbChain") as tmdb_chain: recommend_method = getattr(chain, method_name)
tmdb_chain.return_value.async_run_module = AsyncMock(side_effect=[[], []])
self.assertEqual(asyncio.run(chain.async_tmdb_trending(page=1)), []) with patch(chain_target) as backend_chain:
self.assertEqual(asyncio.run(chain.async_tmdb_trending(page=1)), []) backend_chain.return_value.async_run_module = AsyncMock(side_effect=[[], []])
self.assertEqual(tmdb_chain.return_value.async_run_module.call_count, 2) assert asyncio.run(recommend_method(page=1)) == []
assert asyncio.run(recommend_method(page=1)) == []
assert backend_chain.return_value.async_run_module.call_count == 2