Skip to main content

tdm_server_rust/cache/
stationed_mangas_cache.rs

1//! 组员常驻漫画列表短 TTL 缓存
2//!
3//! ## 使用说明
4//!
5//! - 读取:[`load_stationed_mangas`](`MemberService::find_stationed_mangas` 调用)
6//! - 写入:查询后自动回填
7//! - 失效:常驻增删改时调用 [`invalidate_stationed_mangas`]
8//! - TTL:环境变量 `STATIONED_MANGAS_CACHE_SECS`,默认 60 秒
9
10use crate::{
11    app::AppState,
12    common::result::ResultBody,
13    entity::member::StationedManga,
14    error::AppError,
15    repository::{manga_repo::MangaRepository, member_repo::MemberRepository},
16    utils::fast_json,
17};
18use std::sync::Arc;
19
20/// 常驻漫画列表缓存标记类型
21pub struct StationedMangasCache;
22
23/// 结构化值缓存命名空间
24const VALUE_NS: &str = "stationed_mangas";
25
26/// 读取 TTL 秒数
27fn cache_ttl_secs() -> u64 {
28    std::env::var("STATIONED_MANGAS_CACHE_SECS")
29        .ok()
30        .and_then(|v| v.parse().ok())
31        .unwrap_or(60)
32}
33
34/// 创建常驻漫画缓存实例
35pub fn new_stationed_mangas_cache() -> StationedMangasCache {
36    StationedMangasCache
37}
38
39/// 常驻漫画 JSON 缓存键
40pub fn stationed_mangas_cache_key(member_id: i32) -> String {
41    format!("stationed_mangas:{member_id}")
42}
43
44/// 从缓存读取组员常驻漫画 JSON,未命中返回 `None`
45pub async fn get_stationed_mangas_json_cached(state: &AppState, key: &str) -> Option<Arc<Vec<u8>>> {
46    state.redis.get_json_bytes(key).await
47}
48
49/// 从缓存读取组员常驻漫画,未命中返回 `None`
50pub async fn get_stationed_mangas_cached(
51    state: &AppState,
52    member_id: i32,
53) -> Option<Vec<StationedManga>> {
54    state
55        .redis
56        .get_value(VALUE_NS, &member_id.to_string())
57        .await
58}
59
60/// 写入组员常驻漫画缓存
61pub async fn set_stationed_mangas_cached(
62    state: &AppState,
63    member_id: i32,
64    data: Vec<StationedManga>,
65) {
66    state
67        .redis
68        .set_value_ttl(VALUE_NS, &member_id.to_string(), &data, cache_ttl_secs())
69        .await;
70}
71
72/// 写入组员常驻漫画 JSON 缓存
73pub async fn set_stationed_mangas_json_cached(
74    state: &AppState,
75    key: String,
76    data: Vec<StationedManga>,
77) {
78    let body = ResultBody::success_data(&data);
79    let json = match fast_json::to_vec(&body) {
80        Ok(bytes) => Arc::new(bytes),
81        Err(_) => return,
82    };
83    state
84        .redis
85        .set_json_bytes_ttl(&key, json.as_ref(), cache_ttl_secs())
86        .await;
87}
88
89/// 使指定组员的常驻漫画缓存失效
90pub async fn invalidate_stationed_mangas(state: &AppState, member_id: i32) {
91    state
92        .redis
93        .del_value(VALUE_NS, &member_id.to_string())
94        .await;
95    state
96        .redis
97        .del_json(&stationed_mangas_cache_key(member_id))
98        .await;
99}
100
101/// 查询并回填常驻漫画缓存
102pub async fn load_stationed_mangas(
103    state: &AppState,
104    member_id: i32,
105) -> Result<Vec<StationedManga>, AppError> {
106    if let Some(cached) = get_stationed_mangas_cached(state, member_id).await {
107        return Ok(cached);
108    }
109    let member_repo = MemberRepository::new(state.db.clone());
110    let manga_repo = MangaRepository::new(state.db.clone());
111    let groups = member_repo.stationed_manga_post_groups(member_id).await?;
112    if groups.is_empty() {
113        set_stationed_mangas_cached(state, member_id, vec![]).await;
114        return Ok(vec![]);
115    }
116    let manga_ids: Vec<i32> = groups.keys().copied().collect();
117    let mangas = manga_repo.get_manga_responses_by_ids(&manga_ids).await?;
118    let mut list = Vec::with_capacity(groups.len());
119    for (manga_id, posts) in groups {
120        if let Some(manga) = mangas.get(&manga_id) {
121            list.push(StationedManga {
122                manga: manga.clone(),
123                posts,
124            });
125        }
126    }
127    list.sort_by(|a, b| b.manga.update_time.cmp(&a.manga.update_time));
128    set_stationed_mangas_cached(state, member_id, list.clone()).await;
129    Ok(list)
130}