Skip to main content

tdm_server_rust/cache/
task_tracking_cache.rs

1//! 稿件监控列表短 TTL 缓存
2//!
3//! 缓存任务看板的待做稿件列表、组员任务计数与待发布漫画分页。
4//! 数据变化频繁(每次接稿/交稿都触发刷新),TTL 默认 60 秒。
5//!
6//! ## 缓存
7//!
8//! | 缓存 | Key | 内容 |
9//! |------|-----|------|
10//! | [`EpisodeTasksCache`] | `"episode_tasks"` | 各岗位待做稿件分桶 + 预序列化 JSON |
11//! | [`MemberTaskCountsCache`] | `"member_task_counts"` | 每人各岗位任务计数 |
12//! | [`PendingMangaTasksCache`] | 按分页参数 | 待发布/待审稿漫画分页 |
13//!
14//! ## 失效策略
15//!
16//! 接稿/交稿后调用 [`invalidate_task_tracking`] 同时失效全部缓存。
17
18use crate::{
19    app::AppState,
20    common::{page_bean::PageBean, result::ResultBody},
21    entity::episode::{MemberTaskCount, PendingMangaTask, TaskTrackingResponse},
22    utils::fast_json,
23};
24use std::sync::Arc;
25
26/// 待做稿件缓存键
27const EPISODE_TASKS_KEY: &str = "episode_tasks";
28/// 组员任务计数缓存键
29const MEMBER_TASK_COUNTS_KEY: &str = "member_task_counts";
30/// 结构化值缓存命名空间
31const VALUE_NS: &str = "task_tracking";
32
33/// 待做稿件缓存条目(数据 + 预序列化 JSON 响应体)
34#[derive(Clone)]
35pub struct EpisodeTasksCacheEntry {
36    /// 任务看板分桶数据
37    pub data: TaskTrackingResponse,
38    /// `ResultBody` 序列化后的 JSON 字节
39    pub json: Arc<Vec<u8>>,
40}
41
42/// 组员任务计数缓存条目(数据 + 预序列化 JSON)
43#[derive(Clone)]
44pub struct MemberTaskCountsCacheEntry {
45    /// 组员任务计数列表
46    pub data: Vec<MemberTaskCount>,
47    /// `ResultBody` 序列化后的 JSON 字节
48    pub json: Arc<Vec<u8>>,
49}
50
51/// 待做稿件响应缓存类型
52pub struct EpisodeTasksCache;
53/// 组员任务计数缓存类型
54pub struct MemberTaskCountsCache;
55/// 待发布漫画分页缓存类型
56pub struct PendingMangaTasksCache;
57
58/// 读取 TTL 秒数(环境变量 `TASK_TRACKING_CACHE_SECS`,默认 60)
59fn cache_ttl_secs() -> u64 {
60    std::env::var("TASK_TRACKING_CACHE_SECS")
61        .ok()
62        .and_then(|v| v.parse().ok())
63        .unwrap_or(60)
64}
65
66/// 创建待做稿件缓存实例
67pub fn new_episode_tasks_cache() -> EpisodeTasksCache {
68    EpisodeTasksCache
69}
70
71/// 创建组员任务计数缓存
72pub fn new_member_task_counts_cache() -> MemberTaskCountsCache {
73    MemberTaskCountsCache
74}
75
76/// 创建待发布漫画分页缓存
77pub fn new_pending_manga_tasks_cache() -> PendingMangaTasksCache {
78    PendingMangaTasksCache
79}
80
81/// 待发布漫画缓存键
82pub fn pending_manga_cache_key(page: i32, page_size: i32, manga_tran_name: Option<&str>) -> String {
83    format!(
84        "pending:{page}:{page_size}:{}",
85        manga_tran_name.unwrap_or("")
86    )
87}
88
89/// 读取缓存的待做稿件 JSON,未命中返回 `None`
90pub async fn get_episode_tasks_json_cached(state: &AppState) -> Option<Arc<Vec<u8>>> {
91    state.redis.get_json_bytes(EPISODE_TASKS_KEY).await
92}
93
94/// 读取缓存的待做稿件,未命中返回 `None`
95pub async fn get_episode_tasks_cached(state: &AppState) -> Option<TaskTrackingResponse> {
96    state.redis.get_value(VALUE_NS, EPISODE_TASKS_KEY).await
97}
98
99/// 写入待做稿件缓存(含预序列化 JSON)
100pub async fn set_episode_tasks_cached(state: &AppState, data: TaskTrackingResponse) {
101    let body = ResultBody::success_data(&data);
102    let json = match fast_json::to_vec(&body) {
103        Ok(bytes) => Arc::new(bytes),
104        Err(_) => return,
105    };
106    state
107        .redis
108        .set_json_bytes_ttl(EPISODE_TASKS_KEY, json.as_ref(), cache_ttl_secs())
109        .await;
110    state
111        .redis
112        .set_value_ttl(VALUE_NS, EPISODE_TASKS_KEY, &data, cache_ttl_secs())
113        .await;
114}
115
116/// 读取缓存的组员任务计数 JSON,未命中返回 `None`
117pub async fn get_member_task_counts_json_cached(state: &AppState) -> Option<Arc<Vec<u8>>> {
118    state.redis.get_json_bytes(MEMBER_TASK_COUNTS_KEY).await
119}
120
121/// 读取缓存的组员任务计数,未命中返回 `None`
122pub async fn get_member_task_counts_cached(state: &AppState) -> Option<Vec<MemberTaskCount>> {
123    state
124        .redis
125        .get_value(VALUE_NS, MEMBER_TASK_COUNTS_KEY)
126        .await
127}
128
129/// 写入组员任务计数缓存(含预序列化 JSON)
130pub async fn set_member_task_counts_cached(state: &AppState, data: Vec<MemberTaskCount>) {
131    let body = ResultBody::success_data(&data);
132    let json = match fast_json::to_vec(&body) {
133        Ok(bytes) => Arc::new(bytes),
134        Err(_) => return,
135    };
136    state
137        .redis
138        .set_json_bytes_ttl(MEMBER_TASK_COUNTS_KEY, json.as_ref(), cache_ttl_secs())
139        .await;
140    state
141        .redis
142        .set_value_ttl(VALUE_NS, MEMBER_TASK_COUNTS_KEY, &data, cache_ttl_secs())
143        .await;
144}
145
146/// 使稿件监控全部缓存同时失效
147///
148/// 接稿/交稿后调用,确保下次查询获取最新数据。
149pub async fn invalidate_task_tracking(state: &AppState) {
150    state.redis.del_json(EPISODE_TASKS_KEY).await;
151    state.redis.del_json(MEMBER_TASK_COUNTS_KEY).await;
152    state.redis.del_value(VALUE_NS, EPISODE_TASKS_KEY).await;
153    state
154        .redis
155        .del_value(VALUE_NS, MEMBER_TASK_COUNTS_KEY)
156        .await;
157    state.redis.del_value_pattern(VALUE_NS, "pending:*").await;
158}
159
160/// 读取缓存的待发布漫画分页
161pub async fn get_pending_manga_tasks_cached(
162    state: &AppState,
163    key: &str,
164) -> Option<PageBean<PendingMangaTask>> {
165    state.redis.get_value(VALUE_NS, key).await
166}
167
168/// 写入待发布漫画分页缓存
169pub async fn set_pending_manga_tasks_cached(
170    state: &AppState,
171    key: String,
172    data: PageBean<PendingMangaTask>,
173) {
174    state
175        .redis
176        .set_value_ttl(VALUE_NS, &key, &data, cache_ttl_secs())
177        .await;
178}