1use crate::entity::member::{InvitationCode, Member, MemberCache, MemberEpisode, Post};
6use chrono::{DateTime, Utc};
7use sqlx::{MySql, Pool, Row};
8use std::collections::HashMap;
9
10pub struct MemberRepository {
12 pool: Pool<MySql>,
14}
15
16impl MemberRepository {
17 pub fn new(pool: Pool<MySql>) -> Self {
19 Self { pool }
20 }
21
22 #[tracing::instrument(skip_all, level = "debug")]
33 pub async fn get_by_id(&self, id: i32) -> crate::error::ApiResult<Member> {
34 self.get_auth_snapshot(id).await
35 }
36
37 #[tracing::instrument(skip_all, level = "debug")]
39 pub async fn get_auth_snapshot(&self, id: i32) -> crate::error::ApiResult<Member> {
40 let row = sqlx::query(
41 "SELECT m.Id, m.username, m.intern, m.email, m.registrationTime, m.lastSubmitTime, \
42 GROUP_CONCAT(p.post ORDER BY p.post) AS postIdList \
43 FROM membertb m \
44 LEFT JOIN memberpost mp ON m.Id = mp.memberId \
45 LEFT JOIN post p ON p.post = mp.postId \
46 WHERE m.Id = ? \
47 GROUP BY m.Id, m.username, m.intern, m.email, m.registrationTime, m.lastSubmitTime",
48 )
49 .bind(id)
50 .fetch_optional(&self.pool)
51 .await?
52 .ok_or_else(|| crate::error::AppError::business("组员不存在喵"))?;
53 let mut m = row_to_member(&row);
54 m.post_ids = parse_post_id_list(row.try_get("postIdList").ok());
55 m.posts = m.post_ids.iter().map(|p| Post { post: *p }).collect();
56 Ok(m)
57 }
58
59 #[tracing::instrument(skip_all, level = "debug")]
70 pub async fn find_for_login(
71 &self,
72 username: &str,
73 ) -> crate::error::ApiResult<Option<(Member, String)>> {
74 let row = sqlx::query(
75 "SELECT Id, username, password, intern, email, registrationTime,\
76 lastSubmitTime FROM membertb WHERE username = ? OR email = ?",
77 )
78 .bind(username)
79 .bind(username)
80 .fetch_optional(&self.pool)
81 .await?;
82 Ok(row.map(|r| {
83 let pwd: String = r.get("password");
84 (row_to_member(&r), pwd)
85 }))
86 }
87
88 #[tracing::instrument(skip_all, level = "debug")]
90 pub async fn page_list(
91 &self,
92 username: Option<&str>,
93 post: Option<i16>,
94 intern: Option<i16>,
95 email: Option<&str>,
96 page: i32,
97 page_size: i32,
98 ) -> crate::error::ApiResult<(i64, Vec<Member>)> {
99 let mut filter_sql = String::new();
100 append_list_filters(&mut filter_sql, username, post, intern, email);
101
102 let join_sql = " FROM membertb m INNER JOIN memberpost mp ON mp.memberId = m.Id";
103 let where_sql = format!(" WHERE 1=1{filter_sql}");
104 let count_sql = format!("SELECT COUNT(DISTINCT m.Id) AS cnt{join_sql}{where_sql}");
105
106 let page = page.max(1);
107 let page_size = page_size.max(1);
108 let offset = (page - 1) * page_size;
109 let page_sql = format!(
110 "SELECT m.Id, m.username, m.intern, m.registrationTime, m.lastSubmitTime, m.email, \
111 COUNT(DISTINCT ms.Id) AS stationCount, \
112 GROUP_CONCAT(DISTINCT p.post ORDER BY p.post) AS postIdList \
113 {join_sql} \
114 LEFT JOIN post p ON p.post = mp.postId \
115 LEFT JOIN mangastationed ms ON ms.memberId = m.Id \
116 {where_sql} \
117 GROUP BY m.Id, m.username, m.intern, m.registrationTime, m.lastSubmitTime, m.email \
118 ORDER BY m.lastSubmitTime DESC, m.Id LIMIT ? OFFSET ?"
119 );
120
121 let pool_count = self.pool.clone();
122 let pool_page = self.pool.clone();
123 let mut count_q = sqlx::query(&count_sql);
124 count_q = bind_list_filters(count_q, username, post, intern, email);
125 let mut page_q = sqlx::query(&page_sql);
126 page_q = bind_list_filters(page_q, username, post, intern, email);
127 page_q = page_q.bind(page_size).bind(offset);
128
129 let (count_row, page_rows) =
130 tokio::try_join!(count_q.fetch_one(&pool_count), page_q.fetch_all(&pool_page),)?;
131 let total: i64 = count_row.get("cnt");
132 if page_rows.is_empty() {
133 return Ok((total, Vec::new()));
134 }
135
136 let mut members = Vec::with_capacity(page_rows.len());
137 for row in page_rows {
138 let mut m = row_to_member(&row);
139 if let Ok(c) = row.try_get::<i64, _>("stationCount") {
140 m.station_count = Some(c as i32);
141 }
142 m.post_ids = parse_post_id_list(row.try_get("postIdList").ok());
143 m.posts = m.post_ids.iter().map(|p| Post { post: *p }).collect();
144 members.push(m);
145 }
146 Ok((total, members))
147 }
148
149 #[tracing::instrument(skip_all, level = "debug")]
151 pub async fn all_cache(&self) -> crate::error::ApiResult<Vec<MemberCache>> {
152 let rows = sqlx::query(
153 "SELECT m.Id, m.username, m.intern, m.email, \
154 GROUP_CONCAT(DISTINCT p.post ORDER BY p.post) AS postIdList \
155 FROM membertb m \
156 LEFT JOIN memberpost mp ON m.Id = mp.memberId \
157 LEFT JOIN post p ON p.post = mp.postId \
158 GROUP BY m.Id, m.username, m.intern, m.email \
159 ORDER BY m.Id",
160 )
161 .fetch_all(&self.pool)
162 .await?;
163 Ok(rows
164 .into_iter()
165 .map(|row| MemberCache {
166 id: row.get("Id"),
167 username: row.try_get("username").ok(),
168 intern: row.get("intern"),
169 email: row.try_get("email").ok(),
170 posts: parse_post_id_list(row.try_get("postIdList").ok()),
171 })
172 .collect())
173 }
174
175 #[tracing::instrument(skip_all, level = "debug")]
177 pub async fn delete_members(&self, ids: &[i32]) -> crate::error::ApiResult<()> {
178 if ids.is_empty() {
179 return Ok(());
180 }
181 let placeholders = ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
182 let related = sqlx::query(&format!(
183 "SELECT Id FROM mangaepisodetb WHERE providerId IN ({p}) OR translatorId IN ({p}) \
184 OR proofreaderId IN ({p}) OR lettererId IN ({p}) OR reviewerId IN ({p}) LIMIT 1",
185 p = placeholders
186 ))
187 .fetch_optional(&self.pool)
188 .await?;
189 if related.is_some() {
190 return Err(crate::error::AppError::business("该组员已经绑定漫画了喵!"));
191 }
192 for id in ids {
193 sqlx::query("DELETE FROM memberpost WHERE memberId = ?")
194 .bind(id)
195 .execute(&self.pool)
196 .await?;
197 sqlx::query("DELETE FROM membertb WHERE Id = ?")
198 .bind(id)
199 .execute(&self.pool)
200 .await?;
201 }
202 Ok(())
203 }
204
205 #[tracing::instrument(skip_all, level = "debug")]
207 pub async fn insert_member(&self, m: &Member) -> crate::error::ApiResult<i32> {
208 let r = sqlx::query(
209 "INSERT INTO membertb(username, password, intern, email, registrationTime) VALUES (?, ?, ?, ?, NOW())",
210 )
211 .bind(&m.username)
212 .bind(&m.password)
213 .bind(m.intern)
214 .bind(&m.email)
215 .execute(&self.pool)
216 .await?;
217 Ok(r.last_insert_id() as i32)
218 }
219
220 #[tracing::instrument(skip_all, level = "debug")]
222 pub async fn update_member(&self, m: &Member) -> crate::error::ApiResult<()> {
223 sqlx::query(
224 "UPDATE membertb SET username = COALESCE(?, username), password = COALESCE(?, password), \
225 intern = COALESCE(?, intern), email = COALESCE(?, email) WHERE Id = ?",
226 )
227 .bind(&m.username)
228 .bind(&m.password)
229 .bind(m.intern)
230 .bind(&m.email)
231 .bind(m.id)
232 .execute(&self.pool)
233 .await?;
234 Ok(())
235 }
236
237 #[tracing::instrument(skip_all, level = "debug")]
239 pub async fn replace_posts(
240 &self,
241 member_id: i32,
242 post_ids: &[i32],
243 ) -> crate::error::ApiResult<()> {
244 sqlx::query("DELETE FROM memberpost WHERE memberId = ?")
245 .bind(member_id)
246 .execute(&self.pool)
247 .await?;
248 for pid in post_ids {
249 sqlx::query("INSERT INTO memberpost(memberId, postId) VALUES (?, ?)")
250 .bind(member_id)
251 .bind(pid)
252 .execute(&self.pool)
253 .await?;
254 }
255 Ok(())
256 }
257
258 #[tracing::instrument(skip_all, level = "debug")]
260 pub async fn update_password(&self, id: i32, new_pwd: &str) -> crate::error::ApiResult<()> {
261 sqlx::query("UPDATE membertb SET password = ? WHERE Id = ?")
262 .bind(new_pwd)
263 .bind(id)
264 .execute(&self.pool)
265 .await?;
266 Ok(())
267 }
268
269 #[tracing::instrument(skip_all, level = "debug")]
271 pub async fn get_password(&self, id: i32) -> crate::error::ApiResult<String> {
272 let row = sqlx::query("SELECT password FROM membertb WHERE Id = ?")
273 .bind(id)
274 .fetch_one(&self.pool)
275 .await?;
276 Ok(row.get("password"))
277 }
278
279 #[tracing::instrument(skip_all, level = "debug")]
281 pub async fn exists_username(&self, username: &str) -> crate::error::ApiResult<bool> {
282 let row = sqlx::query("SELECT username FROM membertb WHERE username = ?")
283 .bind(username)
284 .fetch_optional(&self.pool)
285 .await?;
286 Ok(row.is_some())
287 }
288
289 #[tracing::instrument(skip_all, level = "debug")]
291 pub async fn exists_email(&self, email: &str) -> crate::error::ApiResult<bool> {
292 let row = sqlx::query("SELECT email FROM membertb WHERE email = ?")
293 .bind(email)
294 .fetch_optional(&self.pool)
295 .await?;
296 Ok(row.is_some())
297 }
298
299 #[tracing::instrument(skip_all, level = "debug")]
301 pub async fn is_valid_invitation(&self, code: i32) -> crate::error::ApiResult<bool> {
302 let row = sqlx::query("SELECT code FROM invitationcode WHERE code = ?")
303 .bind(code)
304 .fetch_optional(&self.pool)
305 .await?;
306 Ok(row.is_some())
307 }
308
309 #[tracing::instrument(skip_all, level = "debug")]
311 pub async fn list_invitation_codes(&self) -> crate::error::ApiResult<Vec<InvitationCode>> {
312 let rows = sqlx::query("SELECT Id, code FROM invitationcode")
313 .fetch_all(&self.pool)
314 .await?;
315 Ok(rows
316 .into_iter()
317 .map(|r| InvitationCode {
318 id: Some(r.get("Id")),
319 code: r.get("code"),
320 })
321 .collect())
322 }
323
324 #[tracing::instrument(skip_all, level = "debug")]
326 pub async fn delete_invitation(&self, id: i32) -> crate::error::ApiResult<()> {
327 sqlx::query("DELETE FROM invitationcode WHERE Id = ?")
328 .bind(id)
329 .execute(&self.pool)
330 .await?;
331 Ok(())
332 }
333
334 #[tracing::instrument(skip_all, level = "debug")]
336 pub async fn add_invitation(&self, code: i32) -> crate::error::ApiResult<()> {
337 sqlx::query("INSERT INTO invitationcode(code) VALUES (?)")
338 .bind(code)
339 .execute(&self.pool)
340 .await?;
341 Ok(())
342 }
343
344 #[tracing::instrument(skip_all, level = "debug")]
346 pub async fn page_member_episodes(
347 &self,
348 member_id: i32,
349 page: i32,
350 page_size: i32,
351 ) -> crate::error::ApiResult<(i64, Vec<crate::entity::member::MemberEpisodeVo>)> {
352 use crate::repository::episode_mapper::{
353 row_to_member_episode, EPISODE_WORKFLOW_FROM, EPISODE_WORKFLOW_SELECT,
354 };
355 let where_sql = "WHERE me.providerId = ? OR me.translatorId = ? OR me.proofreaderId = ? \
356 OR me.lettererId = ? OR me.timerId = ? OR me.reviewerId = ?";
357 let count_sql = format!("SELECT COUNT(*) AS cnt {EPISODE_WORKFLOW_FROM} {where_sql}");
358 let page = page.max(1);
359 let page_size = page_size.max(1);
360 let offset = (page - 1) * page_size;
361 let data_sql = format!(
362 "SELECT {EPISODE_WORKFLOW_SELECT}, m.mangaTranName AS mangaName, m.category \
363 {EPISODE_WORKFLOW_FROM} \
364 LEFT JOIN mangatb m ON m.Id = me.mangaId \
365 {where_sql} ORDER BY me.Id DESC LIMIT ? OFFSET ?"
366 );
367
368 let pool_count = self.pool.clone();
369 let pool_data = self.pool.clone();
370 let mut count_q = sqlx::query(&count_sql);
371 let mut data_q = sqlx::query(&data_sql);
372 for _ in 0..6 {
373 count_q = count_q.bind(member_id);
374 data_q = data_q.bind(member_id);
375 }
376 data_q = data_q.bind(page_size).bind(offset);
377 let (count_row, rows) =
378 tokio::try_join!(count_q.fetch_one(&pool_count), data_q.fetch_all(&pool_data),)?;
379 let total: i64 = count_row.get("cnt");
380 Ok((total, rows.iter().map(row_to_member_episode).collect()))
381 }
382
383 #[tracing::instrument(skip_all, level = "debug")]
385 pub async fn list_member_episodes(
386 &self,
387 member_id: i32,
388 ) -> crate::error::ApiResult<Vec<crate::entity::member::MemberEpisodeVo>> {
389 use crate::repository::episode_mapper::{
390 row_to_member_episode, EPISODE_WORKFLOW_FROM, EPISODE_WORKFLOW_SELECT,
391 };
392 let sql = format!(
393 "SELECT {EPISODE_WORKFLOW_SELECT}, m.mangaTranName AS mangaName, m.category \
394 {EPISODE_WORKFLOW_FROM} \
395 LEFT JOIN mangatb m ON m.Id = me.mangaId \
396 WHERE me.providerId = ? OR me.translatorId = ? OR me.proofreaderId = ? \
397 OR me.lettererId = ? OR me.timerId = ? OR me.reviewerId = ? \
398 ORDER BY me.Id DESC"
399 );
400 let rows = sqlx::query(&sql)
401 .bind(member_id)
402 .bind(member_id)
403 .bind(member_id)
404 .bind(member_id)
405 .bind(member_id)
406 .bind(member_id)
407 .fetch_all(&self.pool)
408 .await?;
409 Ok(rows.iter().map(row_to_member_episode).collect())
410 }
411
412 #[tracing::instrument(skip_all, level = "debug")]
414 pub async fn take_episode(&self, ep: &MemberEpisode) -> crate::error::ApiResult<()> {
415 let col = post_column(&ep.my_name)?;
416 let sql = format!("UPDATE mangaepisodetb SET {col} = ?, updateTime = NOW() WHERE Id = ?");
417 sqlx::query(&sql)
418 .bind(ep.member_id)
419 .bind(ep.episode_id)
420 .execute(&self.pool)
421 .await?;
422 let detail_prefix = post_detail_prefix(&ep.my_name)?;
423 let detail_col = format!("{detail_prefix}SetupTime");
424 let detail_sql =
425 format!("UPDATE mangaepisodedetail SET {detail_col} = NOW() WHERE episodeId = ?");
426 sqlx::query(&detail_sql)
427 .bind(ep.episode_id)
428 .execute(&self.pool)
429 .await?;
430 sqlx::query(
431 "UPDATE mangatb SET updateTime = NOW() WHERE Id = (SELECT mangaId FROM mangaepisodetb WHERE Id = ?)",
432 )
433 .bind(ep.episode_id)
434 .execute(&self.pool)
435 .await?;
436 Ok(())
437 }
438
439 #[tracing::instrument(skip_all, level = "debug")]
441 pub async fn submit_episode(&self, ep: &MemberEpisode) -> crate::error::ApiResult<()> {
442 let detail_prefix = post_detail_prefix(&ep.my_name)?;
443 let detail_col = format!("{detail_prefix}UpdateTime");
444 let detail_sql =
445 format!("UPDATE mangaepisodedetail SET {detail_col} = NOW() WHERE episodeId = ?");
446 sqlx::query(&detail_sql)
447 .bind(ep.episode_id)
448 .execute(&self.pool)
449 .await?;
450 sqlx::query("UPDATE membertb SET lastSubmitTime = NOW() WHERE Id = ?")
451 .bind(ep.member_id)
452 .execute(&self.pool)
453 .await?;
454 sqlx::query("UPDATE mangaepisodetb SET updateTime = NOW() WHERE Id = ?")
455 .bind(ep.episode_id)
456 .execute(&self.pool)
457 .await?;
458 sqlx::query(
459 "UPDATE mangatb SET updateTime = NOW() WHERE Id = (SELECT mangaId FROM mangaepisodetb WHERE Id = ?)",
460 )
461 .bind(ep.episode_id)
462 .execute(&self.pool)
463 .await?;
464 sqlx::query("INSERT INTO workreminder(episodeId, memberId, myName) VALUES (?, ?, ?)")
465 .bind(ep.episode_id)
466 .bind(ep.member_id)
467 .bind(&ep.my_name)
468 .execute(&self.pool)
469 .await?;
470 Ok(())
471 }
472
473 #[tracing::instrument(skip_all, level = "debug")]
475 pub async fn get_member_reminder_rss(
476 &self,
477 ) -> crate::error::ApiResult<Vec<crate::entity::rss::MemberReminderRssRow>> {
478 let rows = sqlx::query(
479 "SELECT Id, username, intern, email, lastSubmitTime FROM membertb \
480 WHERE lastSubmitTime <= DATE_SUB(NOW(), INTERVAL 3 MONTH) \
481 ORDER BY lastSubmitTime",
482 )
483 .fetch_all(&self.pool)
484 .await?;
485 Ok(rows
486 .iter()
487 .map(|r| crate::entity::rss::MemberReminderRssRow {
488 id: r.get("Id"),
489 username: r.try_get("username").ok(),
490 intern: r.get("intern"),
491 email: r.try_get("email").ok(),
492 last_submit_time: r.try_get("lastSubmitTime").ok(),
493 })
494 .collect())
495 }
496
497 #[tracing::instrument(skip_all, level = "debug")]
499 pub async fn get_post_ids_by_member(
500 &self,
501 member_id: i32,
502 ) -> crate::error::ApiResult<Vec<i32>> {
503 let rows = sqlx::query("SELECT postId FROM memberpost WHERE memberId = ?")
504 .bind(member_id)
505 .fetch_all(&self.pool)
506 .await?;
507 Ok(rows.iter().map(|r| r.get("postId")).collect())
508 }
509
510 #[tracing::instrument(skip_all, level = "debug")]
512 pub async fn stationed_manga_post_groups(
513 &self,
514 member_id: i32,
515 ) -> crate::error::ApiResult<HashMap<i32, Vec<i32>>> {
516 let rows = sqlx::query(
517 "SELECT ms.mangaId, ms.post FROM mangastationed ms \
518 JOIN mangatb m ON m.Id = ms.mangaId WHERE ms.memberId = ? AND ms.status = 1",
519 )
520 .bind(member_id)
521 .fetch_all(&self.pool)
522 .await?;
523 let mut map: HashMap<i32, Vec<i32>> = HashMap::new();
524 for r in rows {
525 let manga_id: i32 = r.get("mangaId");
526 let post: i32 = r.get("post");
527 map.entry(manga_id).or_default().push(post);
528 }
529 Ok(map)
530 }
531
532 #[tracing::instrument(skip_all, level = "debug")]
534 pub async fn get_posts_map(
535 &self,
536 member_ids: &[i32],
537 ) -> crate::error::ApiResult<std::collections::HashMap<i32, Vec<i32>>> {
538 self.get_post_ids_map(member_ids).await
539 }
540
541 #[tracing::instrument(skip_all, level = "debug")]
543 async fn get_post_ids_map(
544 &self,
545 member_ids: &[i32],
546 ) -> crate::error::ApiResult<HashMap<i32, Vec<i32>>> {
547 if member_ids.is_empty() {
548 return Ok(HashMap::new());
549 }
550 let placeholders = member_ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
551 let sql = format!(
552 "SELECT memberpost.memberId AS memberId, p.post AS post \
553 FROM post p JOIN memberpost ON p.post = memberpost.postId \
554 WHERE memberpost.memberId IN ({placeholders})"
555 );
556 let mut q = sqlx::query(&sql);
557 for id in member_ids {
558 q = q.bind(id);
559 }
560 let rows = q.fetch_all(&self.pool).await?;
561 let mut map: HashMap<i32, Vec<i32>> = HashMap::new();
562 for row in rows {
563 let member_id: i32 = row.get("memberId");
564 let post: i32 = row.get("post");
565 map.entry(member_id).or_default().push(post);
566 }
567 Ok(map)
568 }
569}
570
571fn append_list_filters(
573 sql: &mut String,
574 username: Option<&str>,
575 post: Option<i16>,
576 intern: Option<i16>,
577 email: Option<&str>,
578) {
579 if username.is_some() {
580 sql.push_str(" AND m.username LIKE CONCAT('%', ?, '%')");
581 }
582 if post.is_some() {
583 sql.push_str(" AND m.Id IN (SELECT memberId FROM memberpost WHERE postId = ?)");
584 }
585 if intern.is_some() {
586 sql.push_str(" AND m.intern = ?");
587 }
588 if email.is_some() {
589 sql.push_str(" AND m.email = ?");
590 }
591}
592
593fn bind_list_filters<'q>(
595 mut q: sqlx::query::Query<'q, MySql, sqlx::mysql::MySqlArguments>,
596 username: Option<&'q str>,
597 post: Option<i16>,
598 intern: Option<i16>,
599 email: Option<&'q str>,
600) -> sqlx::query::Query<'q, MySql, sqlx::mysql::MySqlArguments> {
601 if let Some(u) = username {
602 q = q.bind(u);
603 }
604 if let Some(p) = post {
605 q = q.bind(p);
606 }
607 if let Some(i) = intern {
608 q = q.bind(i);
609 }
610 if let Some(e) = email {
611 q = q.bind(e);
612 }
613 q
614}
615
616fn parse_post_id_list(raw: Option<String>) -> Vec<i32> {
618 raw.map(|s| {
619 s.split(',')
620 .filter_map(|part| part.trim().parse::<i32>().ok())
621 .collect()
622 })
623 .unwrap_or_default()
624}
625
626fn row_to_member(row: &sqlx::mysql::MySqlRow) -> Member {
627 Member {
628 id: row.get("Id"),
629 username: row.try_get("username").ok(),
630 password: row.try_get("password").ok(),
631 intern: row.get("intern"),
632 email: row.try_get("email").ok(),
633 registration_time: row.try_get::<DateTime<Utc>, _>("registrationTime").ok(),
634 last_submit_time: row.try_get::<DateTime<Utc>, _>("lastSubmitTime").ok(),
635 posts: vec![],
636 post_ids: vec![],
637 station_count: None,
638 station_id: None,
639 post: None,
640 status: None,
641 manga_id: None,
642 manga_name: None,
643 }
644}
645
646fn post_column(name: &str) -> crate::error::ApiResult<&'static str> {
647 match name {
648 "translator" => Ok("translatorId"),
649 "proofreader" => Ok("proofreaderId"),
650 "letterer" => Ok("lettererId"),
651 "timer" => Ok("timerId"),
652 "reviewer" => Ok("reviewerId"),
653 "provider" => Ok("providerId"),
654 _ => Err(crate::error::AppError::business("未知岗位喵")),
655 }
656}
657
658fn post_detail_prefix(name: &str) -> crate::error::ApiResult<&'static str> {
660 match name {
661 "translator" => Ok("translator"),
662 "proofreader" => Ok("proofreader"),
663 "letterer" => Ok("letterer"),
664 "timer" => Ok("timer"),
665 "reviewer" => Ok("reviewer"),
666 "provider" => Ok("provider"),
667 _ => Err(crate::error::AppError::business("未知岗位喵")),
668 }
669}