Skip to main content

tdm_server_rust/cache/
list_page_json_cache.rs

1//! 通用列表分页 JSON 短 TTL 缓存(漫画列表 / 杂志漫画 / 话数分页 / 藏宝处)
2//!
3//! ## 使用说明
4//!
5//! - 读:[`get_list_page_json_cached`]
6//! - 写:[`set_list_page_json_cached`] / [`set_result_json_cached`]
7//! - 突变失效:统一走 [`crate::cache::invalidate::on_*`] helper
8
9use crate::{
10    app::AppState,
11    cache::two_tier,
12    common::{page_bean::PageBean, result::ResultBody},
13    utils::fast_json,
14};
15use std::sync::Arc;
16
17/// 列表分页 JSON 缓存标记类型
18pub struct ListPageJsonCache;
19
20/// 读取 TTL 秒数
21fn cache_ttl_secs() -> u64 {
22    std::env::var("LIST_PAGE_CACHE_SECS")
23        .ok()
24        .and_then(|v| v.parse().ok())
25        .unwrap_or(60)
26}
27
28/// 创建列表分页 JSON 缓存
29pub fn new_list_page_json_cache() -> ListPageJsonCache {
30    ListPageJsonCache
31}
32
33/// 漫画列表缓存键
34pub fn manga_page_cache_key(
35    _state: &AppState,
36    page: i32,
37    page_size: i32,
38    manga_tran_name: Option<&str>,
39    manga_ori_name: Option<&str>,
40    category: Option<i16>,
41    manga_status: Option<i16>,
42    author_name: Option<&str>,
43    magazine_name: Option<&str>,
44    sort_prop: &str,
45    sort_order: &str,
46) -> String {
47    format!(
48        "manga_page:{page}:{page_size}:{}:{}:{}:{}:{}:{}:{sort_prop}:{sort_order}",
49        manga_tran_name.unwrap_or(""),
50        manga_ori_name.unwrap_or(""),
51        category.map(|v| v.to_string()).unwrap_or_default(),
52        manga_status.map(|v| v.to_string()).unwrap_or_default(),
53        author_name.unwrap_or(""),
54        magazine_name.unwrap_or(""),
55    )
56}
57
58/// 杂志漫画列表缓存键
59pub fn magazine_manga_cache_key(
60    _state: &AppState,
61    magazine_id: i32,
62    page: i32,
63    page_size: i32,
64) -> String {
65    format!("magazine_manga:{magazine_id}:{page}:{page_size}")
66}
67
68/// 漫画话数分页缓存键
69pub fn manga_episode_page_cache_key(
70    _state: &AppState,
71    manga_id: i32,
72    page: i32,
73    page_size: i32,
74    episode_type: &str,
75) -> String {
76    manga_episode_page_cache_key_value(manga_id, page, page_size, episode_type)
77}
78
79fn manga_episode_page_cache_key_value(
80    manga_id: i32,
81    page: i32,
82    page_size: i32,
83    episode_type: &str,
84) -> String {
85    format!("manga_episodes:{manga_id}:{episode_type}:{page}:{page_size}")
86}
87
88/// 藏宝处分页缓存键
89pub fn uploaded_submit_cache_key(
90    _state: &AppState,
91    page: i32,
92    page_size: i32,
93    manga_tran_name: Option<&str>,
94    username: Option<&str>,
95) -> String {
96    format!(
97        "uploaded_submit:{page}:{page_size}:{}:{}",
98        manga_tran_name.unwrap_or(""),
99        username.unwrap_or(""),
100    )
101}
102
103/// 读取预序列化 JSON(Redis-only)
104pub async fn get_list_page_json_cached(state: &AppState, key: &str) -> Option<Arc<Vec<u8>>> {
105    two_tier::get_json(&state.redis, key).await
106}
107
108/// 写入预序列化 JSON(Redis-only)
109pub async fn set_list_page_json_cached<T: serde::Serialize>(
110    state: &AppState,
111    key: String,
112    page: PageBean<T>,
113) {
114    let body = ResultBody::success_data(&page);
115    let json = match fast_json::to_vec(&body) {
116        Ok(bytes) => Arc::new(bytes),
117        Err(_) => return,
118    };
119    two_tier::set_json(&state.redis, &key, json.as_ref(), cache_ttl_secs()).await;
120}
121
122/// 漫画话数简单列表缓存键
123pub fn manga_episode_simple_cache_key(_state: &AppState, manga_id: i32) -> String {
124    format!("manga_episodes_simple:{manga_id}")
125}
126
127/// 组员分页缓存键
128pub fn member_page_cache_key(
129    _state: &AppState,
130    page: i32,
131    page_size: i32,
132    username: Option<&str>,
133    post: Option<i16>,
134    intern: Option<i16>,
135    email: Option<&str>,
136) -> String {
137    format!(
138        "member_page:{page}:{page_size}:{}:{}:{}:{}",
139        username.unwrap_or(""),
140        post.map(|v| v.to_string()).unwrap_or_default(),
141        intern.map(|v| v.to_string()).unwrap_or_default(),
142        email.unwrap_or(""),
143    )
144}
145
146/// 写入任意 success_data 预序列化 JSON
147pub async fn set_result_json_cached<T: serde::Serialize>(state: &AppState, key: String, data: T) {
148    let body = ResultBody::success_data(&data);
149    let json = match fast_json::to_vec(&body) {
150        Ok(bytes) => Arc::new(bytes),
151        Err(_) => return,
152    };
153    two_tier::set_json(&state.redis, &key, json.as_ref(), cache_ttl_secs()).await;
154}
155
156/// 使指定漫画的话数列表缓存失效
157pub async fn invalidate_manga_episode_caches(state: &AppState, manga_id: i32) {
158    state
159        .redis
160        .del_json_pattern(&format!("manga_episodes:{manga_id}:*"))
161        .await;
162    state
163        .redis
164        .del_json(&format!("manga_episodes_simple:{manga_id}"))
165        .await;
166}
167
168/// 使藏宝处分页缓存失效
169pub async fn invalidate_uploaded_submit_caches(state: &AppState) {
170    state.redis.del_json_pattern("uploaded_submit:*").await;
171}
172
173/// 使漫画列表分页缓存失效
174pub async fn invalidate_manga_page_caches(state: &AppState) {
175    state.redis.del_json_pattern("manga_page:*").await;
176}
177
178/// 使杂志漫画分页缓存失效
179pub async fn invalidate_magazine_manga_caches(state: &AppState) {
180    state.redis.del_json_pattern("magazine_manga:*").await;
181}
182
183/// 使组员分页缓存失效
184pub async fn invalidate_member_page_caches(state: &AppState) {
185    state.redis.del_json_pattern("member_page:*").await;
186}
187
188#[cfg(test)]
189mod episode_cache_key_tests {
190    use super::manga_episode_page_cache_key_value;
191
192    #[test]
193    fn episode_type_separates_page_cache_keys() {
194        let main = manga_episode_page_cache_key_value(7, 1, 10, "MAIN");
195        let extra = manga_episode_page_cache_key_value(7, 1, 10, "EXTRA");
196        assert_ne!(main, extra);
197    }
198}