feat(cache): add user cache invalidation for avatar, cover, and beatmapset updates

This commit is contained in:
MingxuanGame
2025-11-08 18:14:51 +00:00
parent 0be3e903d4
commit 5c2687e1e4
6 changed files with 38 additions and 14 deletions

View File

@@ -1,6 +1,7 @@
import hashlib import hashlib
from typing import Annotated from typing import Annotated
from app.dependencies.cache import UserCacheService
from app.dependencies.database import Database from app.dependencies.database import Database
from app.dependencies.storage import StorageService from app.dependencies.storage import StorageService
from app.dependencies.user import ClientUser from app.dependencies.user import ClientUser
@@ -17,6 +18,7 @@ async def upload_avatar(
content: Annotated[bytes, File(...)], content: Annotated[bytes, File(...)],
current_user: ClientUser, current_user: ClientUser,
storage: StorageService, storage: StorageService,
cache_service: UserCacheService,
): ):
"""上传用户头像 """上传用户头像
@@ -49,6 +51,8 @@ async def upload_avatar(
current_user.avatar_url = url current_user.avatar_url = url
await session.commit() await session.commit()
await cache_service.invalidate_user_cache(current_user.id)
return { return {
"url": url, "url": url,
"filehash": filehash, "filehash": filehash,

View File

@@ -2,6 +2,7 @@ import hashlib
from typing import Annotated from typing import Annotated
from app.database.user import UserProfileCover from app.database.user import UserProfileCover
from app.dependencies.cache import UserCacheService
from app.dependencies.database import Database from app.dependencies.database import Database
from app.dependencies.storage import StorageService from app.dependencies.storage import StorageService
from app.dependencies.user import ClientUser from app.dependencies.user import ClientUser
@@ -18,6 +19,7 @@ async def upload_cover(
content: Annotated[bytes, File(...)], content: Annotated[bytes, File(...)],
current_user: ClientUser, current_user: ClientUser,
storage: StorageService, storage: StorageService,
cache_service: UserCacheService,
): ):
"""上传用户头图 """上传用户头图
@@ -50,6 +52,8 @@ async def upload_cover(
current_user.cover = UserProfileCover(url=url) current_user.cover = UserProfileCover(url=url)
await session.commit() await session.commit()
await cache_service.invalidate_user_cache(current_user.id)
return { return {
"url": url, "url": url,
"filehash": filehash, "filehash": filehash,

View File

@@ -14,7 +14,8 @@ from app.database.user_preference import (
UserListView, UserListView,
UserPreference, UserPreference,
) )
from app.dependencies.database import Database, Redis from app.dependencies.cache import UserCacheService
from app.dependencies.database import Database
from app.dependencies.user import ClientUser from app.dependencies.user import ClientUser
from app.models.score import GameMode from app.models.score import GameMode
from app.models.user import Page from app.models.user import Page
@@ -26,7 +27,6 @@ from app.models.userpage import (
ValidateBBCodeResponse, ValidateBBCodeResponse,
) )
from app.service.bbcode_service import bbcode_service from app.service.bbcode_service import bbcode_service
from app.service.user_cache_service import get_user_cache_service
from app.utils import hex_to_hue, utcnow from app.utils import hex_to_hue, utcnow
from .router import router from .router import router
@@ -41,6 +41,7 @@ async def user_rename(
session: Database, session: Database,
new_name: Annotated[str, Body(..., description="新的用户名")], new_name: Annotated[str, Body(..., description="新的用户名")],
current_user: ClientUser, current_user: ClientUser,
cache_service: UserCacheService,
): ):
"""修改用户名 """修改用户名
@@ -81,6 +82,9 @@ async def user_rename(
} }
session.add(rename_event) session.add(rename_event)
await session.commit() await session.commit()
await cache_service.invalidate_user_cache(current_user.id)
return None return None
@@ -95,6 +99,7 @@ async def update_userpage(
request: UpdateUserpageRequest, request: UpdateUserpageRequest,
session: Database, session: Database,
current_user: ClientUser, current_user: ClientUser,
cache_service: UserCacheService,
): ):
"""更新用户页面内容""" """更新用户页面内容"""
if await current_user.is_restricted(session): if await current_user.is_restricted(session):
@@ -110,6 +115,8 @@ async def update_userpage(
await session.commit() await session.commit()
await session.refresh(current_user) await session.refresh(current_user)
await cache_service.invalidate_user_cache(current_user.id)
# 返回官方格式的响应只包含html # 返回官方格式的响应只包含html
return UpdateUserpageResponse(html=processed_page["html"]) return UpdateUserpageResponse(html=processed_page["html"])
@@ -295,13 +302,11 @@ async def change_user_preference(
request: Preferences, request: Preferences,
session: Database, session: Database,
current_user: ClientUser, current_user: ClientUser,
redis: Redis, cache_service: UserCacheService,
): ):
if await current_user.is_restricted(session): if await current_user.is_restricted(session):
raise HTTPException(403, "Your account is restricted and cannot perform this action.") raise HTTPException(403, "Your account is restricted and cannot perform this action.")
cache_service = get_user_cache_service(redis)
await current_user.awaitable_attrs.user_preference await current_user.awaitable_attrs.user_preference
user_pref: UserPreference | None = current_user.user_preference user_pref: UserPreference | None = current_user.user_preference
if user_pref is None: if user_pref is None:
@@ -355,15 +360,14 @@ async def overwrite_user_preference(
request: Preferences, request: Preferences,
session: Database, session: Database,
current_user: ClientUser, current_user: ClientUser,
redis: Redis, cache_service: UserCacheService,
): ):
if await current_user.is_restricted(session): if await current_user.is_restricted(session):
raise HTTPException(403, "Your account is restricted and cannot perform this action.") raise HTTPException(403, "Your account is restricted and cannot perform this action.")
await Preferences.clear(current_user, []) await Preferences.clear(current_user, [])
await change_user_preference(request, session, current_user, redis) await change_user_preference(request, session, current_user, cache_service)
cache_service = get_user_cache_service(redis)
await cache_service.invalidate_user_cache(current_user.id) await cache_service.invalidate_user_cache(current_user.id)
await session.commit() await session.commit()
@@ -379,13 +383,12 @@ async def delete_user_preference(
session: Database, session: Database,
current_user: ClientUser, current_user: ClientUser,
fields: list[str], fields: list[str],
redis: Redis, cache_service: UserCacheService,
): ):
if await current_user.is_restricted(session): if await current_user.is_restricted(session):
raise HTTPException(403, "Your account is restricted and cannot perform this action.") raise HTTPException(403, "Your account is restricted and cannot perform this action.")
await Preferences.clear(current_user, fields) await Preferences.clear(current_user, fields)
cache_service = get_user_cache_service(redis)
await cache_service.invalidate_user_cache(current_user.id) await cache_service.invalidate_user_cache(current_user.id)
await session.commit() await session.commit()

View File

@@ -5,7 +5,7 @@ from urllib.parse import parse_qs
from app.database import Beatmap, Beatmapset, BeatmapsetResp, FavouriteBeatmapset, User from app.database import Beatmap, Beatmapset, BeatmapsetResp, FavouriteBeatmapset, User
from app.database.beatmapset import SearchBeatmapsetsResp from app.database.beatmapset import SearchBeatmapsetsResp
from app.dependencies.beatmap_download import DownloadService from app.dependencies.beatmap_download import DownloadService
from app.dependencies.cache import BeatmapsetCacheService from app.dependencies.cache import BeatmapsetCacheService, UserCacheService
from app.dependencies.database import Database, Redis, with_db from app.dependencies.database import Database, Redis, with_db
from app.dependencies.fetcher import Fetcher from app.dependencies.fetcher import Fetcher
from app.dependencies.geoip import IPAddress, get_geoip_helper from app.dependencies.geoip import IPAddress, get_geoip_helper
@@ -222,6 +222,7 @@ async def download_beatmapset(
) )
async def favourite_beatmapset( async def favourite_beatmapset(
db: Database, db: Database,
cache_service: UserCacheService,
beatmapset_id: Annotated[int, Path(..., description="谱面集 ID")], beatmapset_id: Annotated[int, Path(..., description="谱面集 ID")],
action: Annotated[ action: Annotated[
Literal["favourite", "unfavourite"], Literal["favourite", "unfavourite"],
@@ -247,3 +248,4 @@ async def favourite_beatmapset(
else: else:
await db.delete(existing_favourite) await db.delete(existing_favourite)
await db.commit() await db.commit()
await cache_service.invalidate_user_beatmapsets_cache(current_user.id)

View File

@@ -17,6 +17,7 @@ from app.database.events import Event
from app.database.score import LegacyScoreResp, Score, ScoreResp, get_user_first_scores from app.database.score import LegacyScoreResp, Score, ScoreResp, get_user_first_scores
from app.database.user import ALL_INCLUDED, SEARCH_INCLUDED from app.database.user import ALL_INCLUDED, SEARCH_INCLUDED
from app.dependencies.api_version import APIVersion from app.dependencies.api_version import APIVersion
from app.dependencies.cache import UserCacheService
from app.dependencies.database import Database, get_redis from app.dependencies.database import Database, get_redis
from app.dependencies.user import get_current_user, get_optional_user from app.dependencies.user import get_current_user, get_optional_user
from app.helpers.asset_proxy_helper import asset_proxy_response from app.helpers.asset_proxy_helper import asset_proxy_response
@@ -406,15 +407,13 @@ async def get_user_info(
async def get_user_beatmapsets( async def get_user_beatmapsets(
session: Database, session: Database,
background_task: BackgroundTasks, background_task: BackgroundTasks,
cache_service: UserCacheService,
user_id: Annotated[int, Path(description="用户 ID")], user_id: Annotated[int, Path(description="用户 ID")],
type: Annotated[BeatmapsetType, Path(description="谱面集类型")], type: Annotated[BeatmapsetType, Path(description="谱面集类型")],
current_user: Annotated[User, Security(get_current_user, scopes=["public"])], current_user: Annotated[User, Security(get_current_user, scopes=["public"])],
limit: Annotated[int, Query(ge=1, le=1000, description="返回条数 (1-1000)")] = 100, limit: Annotated[int, Query(ge=1, le=1000, description="返回条数 (1-1000)")] = 100,
offset: Annotated[int, Query(ge=0, description="偏移量")] = 0, offset: Annotated[int, Query(ge=0, description="偏移量")] = 0,
): ):
redis = get_redis()
cache_service = get_user_cache_service(redis)
# 先尝试从缓存获取 # 先尝试从缓存获取
cached_result = await cache_service.get_user_beatmapsets_from_cache(user_id, type.value, limit, offset) cached_result = await cache_service.get_user_beatmapsets_from_cache(user_id, type.value, limit, offset)
if cached_result is not None: if cached_result is not None:

View File

@@ -279,6 +279,18 @@ class UserCacheService:
except Exception as e: except Exception as e:
logger.error(f"Error invalidating user scores cache: {e}") logger.error(f"Error invalidating user scores cache: {e}")
async def invalidate_user_beatmapsets_cache(self, user_id: int):
"""使用户谱面集缓存失效"""
try:
# 删除用户谱面集相关缓存
pattern = f"user:{user_id}:beatmapsets:*"
keys = await self.redis.keys(pattern)
if keys:
await self.redis.delete(*keys)
logger.info(f"Invalidated {len(keys)} beatmapset cache entries for user {user_id}")
except Exception as e:
logger.error(f"Error invalidating user beatmapsets cache: {e}")
async def preload_user_cache(self, session: AsyncSession, user_ids: list[int]): async def preload_user_cache(self, session: AsyncSession, user_ids: list[int]):
"""预加载用户缓存""" """预加载用户缓存"""
if self._refreshing: if self._refreshing: