1use crate::{
12 app::AppState,
13 cache::{
14 get_auth_snapshot_cached, invalidate_auth_snapshot, invalidate_member_all,
15 invalidate_task_tracking, member_all_cache_key,
16 },
17 common::PageBean,
18 entity::{
19 enums::MemberInternEnum,
20 member::{
21 InvitationCode, Member, MemberAddRequest, MemberCache, MemberEpisode, MemberEpisodeVo,
22 MemberListVo, MemberLoginRequest, MemberSelfUpdateRequest, MemberUpdateRequest, Reg,
23 ResetPassword, StationedManga,
24 },
25 },
26 error::{ApiResult, AppError},
27 repository::{manga_repo::MangaRepository, member_repo::MemberRepository},
28 service::rss_service::{RssRefreshScope, RssService},
29 utils::{jwt::JwtUtil, page::slice_rows},
30};
31use bcrypt::{hash, verify, DEFAULT_COST};
32use std::sync::Arc;
33
34pub struct MemberService;
39
40impl MemberService {
41 #[tracing::instrument(skip_all, level = "debug")]
56 pub async fn login(state: &AppState, req: MemberLoginRequest) -> ApiResult<Option<String>> {
57 let repo = MemberRepository::new(state.db.clone());
58 let Some((member, pwd_hash)) = repo.find_for_login(&req.username).await? else {
59 return Ok(None);
60 };
61 if !verify(&req.password, &pwd_hash).unwrap_or(false) {
62 return Ok(None);
63 }
64 if MemberInternEnum::is_left(member.intern) {
65 return Err(AppError::login_expired("你已经退出了提灯喵猫娘化计划喵……"));
66 }
67 let util = JwtUtil::new(&state.config.jwt.sign_key, state.config.jwt.expire_ms);
68 let jwt = util
69 .generate(member.id)
70 .map_err(|e| AppError::Internal(e.to_string()))?;
71 Ok(Some(jwt))
72 }
73
74 #[tracing::instrument(skip_all, level = "debug")]
76 pub async fn register_member(state: &AppState, reg: Reg) -> ApiResult<()> {
77 let repo = MemberRepository::new(state.db.clone());
78 if !repo.is_valid_invitation(reg.invitationcode).await? {
79 return Err(AppError::business("邀请码无效喵!"));
80 }
81 Self::check_duplicate(&repo, ®.username, ®.email).await?;
82 let mut member = Member {
83 username: Some(reg.username),
84 email: Some(reg.email),
85 password: Some(Self::encode_pwd(®.password)),
86 intern: 1,
87 ..Default::default()
88 };
89 Self::add_new_member(&repo, &mut member).await?;
90 Self::invalidate_member_caches(state, &[member.id]).await;
91 Ok(())
92 }
93
94 #[tracing::instrument(skip_all, level = "debug")]
96 pub async fn page(
97 state: &AppState,
98 page: i32,
99 page_size: i32,
100 username: Option<String>,
101 post: Option<i16>,
102 intern: Option<i16>,
103 email: Option<String>,
104 ) -> ApiResult<PageBean<MemberListVo>> {
105 let repo = MemberRepository::new(state.db.clone());
106 let (total, rows) = repo
107 .page_list(
108 username.as_deref(),
109 post,
110 intern,
111 email.as_deref(),
112 page,
113 page_size,
114 )
115 .await?;
116 Ok(slice_rows(
117 rows.into_iter().map(MemberListVo::from).collect(),
118 total,
119 ))
120 }
121
122 #[tracing::instrument(skip_all, level = "debug")]
124 pub async fn get_member_list(state: &AppState) -> ApiResult<Vec<MemberCache>> {
125 let key = member_all_cache_key();
126 if let Some(cached) = state.member_all_cache.get(key).await {
127 return Ok((*cached).clone());
128 }
129 let list = MemberRepository::new(state.db.clone()).all_cache().await?;
130 state
131 .member_all_cache
132 .insert(key.to_string(), Arc::new(list.clone()))
133 .await;
134 Ok(list)
135 }
136
137 #[tracing::instrument(skip_all, level = "debug")]
139 pub async fn delete(state: &AppState, ids: Vec<i32>) -> ApiResult<()> {
140 MemberRepository::new(state.db.clone())
141 .delete_members(&ids)
142 .await?;
143 Self::invalidate_member_caches(state, &ids).await;
144 Ok(())
145 }
146
147 #[tracing::instrument(skip_all, level = "debug")]
149 pub async fn new_member(state: &AppState, req: MemberAddRequest) -> ApiResult<()> {
150 let repo = MemberRepository::new(state.db.clone());
151 Self::check_duplicate(&repo, &req.username, &req.email).await?;
152 let mut member = Member {
153 username: Some(req.username),
154 password: Some(Self::encode_pwd(&req.password)),
155 email: Some(req.email),
156 intern: req.intern,
157 ..Default::default()
158 };
159 Self::add_new_member(&repo, &mut member).await?;
160 let id = member.id;
161 repo.replace_posts(id, &req.post_ids).await?;
162 Self::invalidate_member_caches(state, &[id]).await;
163 Ok(())
164 }
165
166 #[tracing::instrument(skip_all, level = "debug")]
168 pub async fn get_member_by_id(state: &AppState, id: i32) -> ApiResult<Member> {
169 get_auth_snapshot_cached(state, id).await
170 }
171
172 #[tracing::instrument(skip_all, level = "debug")]
174 pub async fn update_member(state: &AppState, req: MemberUpdateRequest) -> ApiResult<()> {
175 let repo = MemberRepository::new(state.db.clone());
176 let mut member = repo.get_by_id(req.id).await?;
177 if let Some(u) = req.username {
178 member.username = Some(u);
179 }
180 if let Some(p) = req.password {
181 member.password = Some(Self::encode_pwd(&p));
182 }
183 if let Some(e) = req.email {
184 member.email = Some(e);
185 }
186 if let Some(i) = req.intern {
187 member.intern = i;
188 }
189 repo.update_member(&member).await?;
190 if let Some(post_ids) = req.post_ids {
191 repo.replace_posts(req.id, &post_ids).await?;
192 }
193 Self::invalidate_member_caches(state, &[req.id]).await;
194 Ok(())
195 }
196
197 #[tracing::instrument(skip_all, level = "debug")]
199 pub async fn update_member_self(
200 state: &AppState,
201 req: MemberSelfUpdateRequest,
202 ) -> ApiResult<()> {
203 let repo = MemberRepository::new(state.db.clone());
204 let mut member = repo.get_by_id(req.id).await?;
205 if let Some(u) = req.username {
206 member.username = Some(u);
207 }
208 if let Some(e) = req.email {
209 member.email = Some(e);
210 }
211 repo.update_member(&member).await?;
212 Self::invalidate_member_caches(state, &[req.id]).await;
213 Ok(())
214 }
215
216 #[tracing::instrument(skip_all, level = "debug")]
218 pub async fn find_stationed_mangas(
219 state: &AppState,
220 id: i32,
221 ) -> ApiResult<Vec<StationedManga>> {
222 let member_repo = MemberRepository::new(state.db.clone());
223 let manga_repo = MangaRepository::new(state.db.clone());
224 let groups = member_repo.stationed_manga_post_groups(id).await?;
225 if groups.is_empty() {
226 return Ok(Vec::new());
227 }
228 let manga_ids: Vec<i32> = groups.keys().copied().collect();
229 let mangas = manga_repo.get_manga_responses_by_ids(&manga_ids).await?;
230 let mut list = Vec::with_capacity(groups.len());
231 for (manga_id, posts) in groups {
232 if let Some(manga) = mangas.get(&manga_id) {
233 list.push(StationedManga {
234 manga: manga.clone(),
235 posts,
236 });
237 }
238 }
239 list.sort_by(|a, b| b.manga.update_time.cmp(&a.manga.update_time));
240 Ok(list)
241 }
242
243 #[tracing::instrument(skip_all, level = "debug")]
245 pub async fn update_member_password(state: &AppState, reset: ResetPassword) -> ApiResult<()> {
246 let repo = MemberRepository::new(state.db.clone());
247 let old_hash = repo.get_password(reset.id).await?;
248 if !verify(&reset.old_pwd, &old_hash).unwrap_or(false) {
249 return Err(AppError::business("原密码不正确喵"));
250 }
251 repo.update_password(reset.id, &Self::encode_pwd(&reset.new_pwd))
252 .await?;
253 Self::invalidate_member_caches(state, &[reset.id]).await;
254 Ok(())
255 }
256
257 #[tracing::instrument(skip_all, level = "debug")]
259 pub async fn page_episode(
260 state: &AppState,
261 page: i32,
262 page_size: i32,
263 id: i32,
264 ) -> ApiResult<PageBean<MemberEpisodeVo>> {
265 let (total, rows) = MemberRepository::new(state.db.clone())
266 .page_member_episodes(id, page, page_size)
267 .await?;
268 Ok(slice_rows(rows, total))
269 }
270
271 #[tracing::instrument(skip_all, level = "debug")]
273 pub async fn take_episode(state: &AppState, ep: MemberEpisode) -> ApiResult<()> {
274 MemberRepository::new(state.db.clone())
275 .take_episode(&ep)
276 .await?;
277 invalidate_task_tracking(state).await;
278 Ok(())
279 }
280
281 #[tracing::instrument(skip_all, level = "debug")]
283 pub async fn submit_episode(state: &AppState, ep: MemberEpisode) -> ApiResult<()> {
284 MemberRepository::new(state.db.clone())
285 .submit_episode(&ep)
286 .await?;
287 invalidate_task_tracking(state).await;
288 RssService::refresh(
289 state,
290 RssRefreshScope::WorkflowSubmit {
291 post_name: ep.my_name.clone(),
292 },
293 );
294 Ok(())
295 }
296
297 #[tracing::instrument(skip_all, level = "debug")]
299 pub async fn get_invitation_codes(state: &AppState) -> ApiResult<Vec<InvitationCode>> {
300 MemberRepository::new(state.db.clone())
301 .list_invitation_codes()
302 .await
303 }
304
305 #[tracing::instrument(skip_all, level = "debug")]
307 pub async fn delete_invitation_code(state: &AppState, id: i32) -> ApiResult<()> {
308 MemberRepository::new(state.db.clone())
309 .delete_invitation(id)
310 .await
311 }
312
313 #[tracing::instrument(skip_all, level = "debug")]
315 pub async fn add_invitation_code(state: &AppState, code: InvitationCode) -> ApiResult<()> {
316 MemberRepository::new(state.db.clone())
317 .add_invitation(code.code)
318 .await
319 }
320
321 #[tracing::instrument(skip_all, level = "debug")]
322 async fn check_duplicate(
323 repo: &MemberRepository,
324 username: &str,
325 email: &str,
326 ) -> ApiResult<()> {
327 if repo.exists_username(username).await? {
328 return Err(AppError::unique("用户名"));
329 }
330 if repo.exists_email(email).await? {
331 return Err(AppError::unique("邮箱"));
332 }
333 Ok(())
334 }
335
336 #[tracing::instrument(skip_all, level = "debug")]
337 async fn add_new_member(repo: &MemberRepository, member: &mut Member) -> ApiResult<()> {
338 let id = repo.insert_member(member).await?;
339 member.id = id;
340 repo.replace_posts(id, &[5]).await?;
341 Ok(())
342 }
343
344 fn encode_pwd(pwd: &str) -> String {
345 hash(pwd, DEFAULT_COST).unwrap_or_else(|_| pwd.to_string())
346 }
347
348 #[tracing::instrument(skip_all, level = "debug")]
350 async fn invalidate_member_caches(state: &AppState, member_ids: &[i32]) {
351 invalidate_member_all(state).await;
352 for id in member_ids {
353 invalidate_auth_snapshot(state, *id).await;
354 }
355 }
356}