tdm_server_rust/repository/
episode_statistics_assemble.rs1use crate::entity::episode::MemberStatistics;
4use crate::sea_entity::membertb;
5use chrono::{DateTime, NaiveDateTime, Utc};
6use sea_orm::{
7 ColumnTrait, ConnectionTrait, DatabaseConnection, DbBackend, EntityTrait, QueryFilter,
8 Statement, Value,
9};
10use std::collections::{HashMap, HashSet};
11
12#[derive(Debug, Clone)]
14pub struct PostCompletionStats {
15 pub translator_count: i64,
17 pub proofreader_count: i64,
19 pub letterer_count: i64,
21 pub reviewer_count: i64,
23}
24
25const ROLE_TIME_COLS: [(&str, &str); 4] = [
27 ("translatorUpdateTime", "translator"),
28 ("proofreaderUpdateTime", "proofreader"),
29 ("lettererUpdateTime", "letterer"),
30 ("reviewerUpdateTime", "reviewer"),
31];
32
33pub async fn statistic_count_in_range(
35 db: &DatabaseConnection,
36 start: DateTime<Utc>,
37 end: DateTime<Utc>,
38) -> crate::error::ApiResult<PostCompletionStats> {
39 let rows = statistic_counts_in_ranges(db, &[(start.naive_utc(), end.naive_utc())]).await?;
40 rows.into_iter()
41 .next()
42 .ok_or_else(|| crate::error::AppError::business("统计失败喵"))
43}
44
45pub async fn statistic_counts_in_ranges(
47 db: &DatabaseConnection,
48 ranges: &[(NaiveDateTime, NaiveDateTime)],
49) -> crate::error::ApiResult<Vec<PostCompletionStats>> {
50 if ranges.is_empty() {
51 return Ok(vec![]);
52 }
53 let mut select_parts: Vec<String> = Vec::new();
54 let mut values: Vec<Value> = Vec::new();
55 let mut param_idx = 1i32;
56 for (ri, (start, end)) in ranges.iter().enumerate() {
57 let p_start = param_idx;
58 param_idx += 1;
59 let p_end = param_idx;
60 param_idx += 1;
61 for (col, alias) in ROLE_TIME_COLS {
62 select_parts.push(format!(
63 r#"COUNT(*) FILTER (WHERE "{col}" >= ${p_start} AND "{col}" <= ${p_end})::bigint AS "r{ri}_{alias}""#
64 ));
65 }
66 values.push(Value::from(*start));
67 values.push(Value::from(*end));
68 }
69 let sql = format!("SELECT {} FROM mangaepisodedetail", select_parts.join(", "));
70 let row = db
71 .query_one(Statement::from_sql_and_values(
72 DbBackend::Postgres,
73 &sql,
74 values,
75 ))
76 .await?
77 .ok_or_else(|| crate::error::AppError::business("统计失败喵"))?;
78
79 let mut out = Vec::with_capacity(ranges.len());
80 for ri in 0..ranges.len() {
81 out.push(PostCompletionStats {
82 translator_count: row.try_get("", &format!("r{ri}_translator"))?,
83 proofreader_count: row.try_get("", &format!("r{ri}_proofreader"))?,
84 letterer_count: row.try_get("", &format!("r{ri}_letterer"))?,
85 reviewer_count: row.try_get("", &format!("r{ri}_reviewer"))?,
86 });
87 }
88 Ok(out)
89}
90
91pub async fn member_statistics_in_range(
93 db: &DatabaseConnection,
94 start: DateTime<Utc>,
95 end: DateTime<Utc>,
96 member_id: Option<i32>,
97) -> crate::error::ApiResult<Vec<MemberStatistics>> {
98 let start = start.naive_utc();
99 let end = end.naive_utc();
100 let (translator, proofreader, letterer, timer, reviewer) = tokio::try_join!(
101 role_member_counts_sql(
102 db,
103 "translatorId",
104 "translatorUpdateTime",
105 start,
106 end,
107 member_id,
108 ),
109 role_member_counts_sql(
110 db,
111 "proofreaderId",
112 "proofreaderUpdateTime",
113 start,
114 end,
115 member_id,
116 ),
117 role_member_counts_sql(
118 db,
119 "lettererId",
120 "lettererUpdateTime",
121 start,
122 end,
123 member_id,
124 ),
125 role_member_counts_sql(db, "timerId", "timerUpdateTime", start, end, member_id),
126 role_member_counts_sql(
127 db,
128 "reviewerId",
129 "reviewerUpdateTime",
130 start,
131 end,
132 member_id,
133 ),
134 )?;
135
136 let mut member_ids: HashSet<i32> = HashSet::new();
137 for map in [&translator, &proofreader, &letterer, &timer, &reviewer] {
138 member_ids.extend(map.keys().copied());
139 }
140 let mut active: Vec<i32> = member_ids
141 .into_iter()
142 .filter(|mid| {
143 translator.get(mid).copied().unwrap_or(0)
144 + proofreader.get(mid).copied().unwrap_or(0)
145 + letterer.get(mid).copied().unwrap_or(0)
146 + timer.get(mid).copied().unwrap_or(0)
147 + reviewer.get(mid).copied().unwrap_or(0)
148 > 0
149 })
150 .collect();
151 if active.is_empty() {
152 return Ok(vec![]);
153 }
154 active.sort_unstable();
155
156 let members: HashMap<i32, membertb::Model> = membertb::Entity::find()
157 .filter(membertb::Column::Id.is_in(active.clone()))
158 .all(db)
159 .await?
160 .into_iter()
161 .map(|m| (m.id, m))
162 .collect();
163
164 Ok(active
165 .into_iter()
166 .filter_map(|mid| {
167 let m = members.get(&mid)?;
168 Some(MemberStatistics {
169 user_name: Some(m.username.clone()),
170 user_id: Some(mid.to_string()),
171 translator_count: translator.get(&mid).copied().unwrap_or(0),
172 proofreader_count: proofreader.get(&mid).copied().unwrap_or(0),
173 letterer_count: letterer.get(&mid).copied().unwrap_or(0),
174 timer_count: timer.get(&mid).copied().unwrap_or(0),
175 reviewer_count: reviewer.get(&mid).copied().unwrap_or(0),
176 })
177 })
178 .collect())
179}
180
181async fn role_member_counts_sql(
183 db: &DatabaseConnection,
184 member_col: &str,
185 time_col: &str,
186 start: NaiveDateTime,
187 end: NaiveDateTime,
188 member_id: Option<i32>,
189) -> crate::error::ApiResult<HashMap<i32, i32>> {
190 let sql = if member_id.is_some() {
191 format!(
192 r#"SELECT e."{member_col}" AS member_id, COUNT(*)::int AS cnt
193 FROM mangaepisodedetail d
194 INNER JOIN mangaepisodetb e ON e."Id" = d."episodeId"
195 WHERE d."{time_col}" >= $1 AND d."{time_col}" < $2
196 AND e."{member_col}" = $3
197 GROUP BY e."{member_col}"
198 "#
199 )
200 } else {
201 format!(
202 r#"SELECT e."{member_col}" AS member_id, COUNT(*)::int AS cnt
203 FROM mangaepisodedetail d
204 INNER JOIN mangaepisodetb e ON e."Id" = d."episodeId"
205 WHERE d."{time_col}" >= $1 AND d."{time_col}" < $2
206 AND e."{member_col}" IS NOT NULL
207 GROUP BY e."{member_col}"
208 "#
209 )
210 };
211 let values: Vec<Value> = if let Some(id) = member_id {
212 vec![Value::from(start), Value::from(end), Value::from(id)]
213 } else {
214 vec![Value::from(start), Value::from(end)]
215 };
216 let rows = db
217 .query_all(Statement::from_sql_and_values(
218 DbBackend::Postgres,
219 &sql,
220 values,
221 ))
222 .await?;
223 let mut counts = HashMap::new();
224 for row in rows {
225 let mid: i32 = row.try_get("", "member_id")?;
226 let cnt: i32 = row.try_get("", "cnt")?;
227 counts.insert(mid, cnt);
228 }
229 Ok(counts)
230}