1use crate::{
10 app::AppState,
11 entity::reward::{
12 EventBalanceDto, RewardRecordResultDto, RewardTicketResultDto, RewardTicketTransferLog,
13 RewardTicketTransferRequest, RewardTicketsDetailDto, RewardWinnerDto, Rewardtb,
14 },
15 error::{ApiResult, AppError},
16 repository::reward_repo::RewardRepository,
17};
18use chrono::NaiveDateTime;
19use dashmap::{mapref::entry::Entry, DashMap};
20use sqlx::Row;
21use std::sync::OnceLock;
22
23const REWARD_CATEGORY_EXCHANGE: i16 = 2;
25const REWARD_CATEGORY_LUCKY: i16 = 3;
27const TICKET_STATUS_AVAILABLE: i16 = 1;
29const TICKET_STATUS_NON_WIN: i16 = 2;
31const TICKET_STATUS_SAME_TYPE_UNAVAILABLE: i16 = 3;
33const TICKET_STATUS_WIN: i16 = 4;
35const REWARD_HOST_MEMBER_ID: i32 = 154;
37
38static TRANSFER_REQUESTS: OnceLock<DashMap<String, ()>> = OnceLock::new();
40
41pub struct RewardService;
43
44impl RewardService {
45 #[tracing::instrument(skip_all, level = "debug")]
47 pub async fn get_reward_balance(
48 state: &AppState,
49 member_id: i32,
50 event_id: i32,
51 ) -> ApiResult<EventBalanceDto> {
52 let repo = RewardRepository::new(state.db.clone());
53 if !repo.check_account_exists(member_id).await? {
54 repo.create_balance_account(member_id, event_id).await?;
55 }
56 repo.get_reward_balance(member_id, event_id).await
57 }
58
59 #[tracing::instrument(skip_all, level = "debug")]
61 pub async fn get_rewards_list(state: &AppState) -> ApiResult<Vec<Rewardtb>> {
62 RewardRepository::new(state.db.clone())
63 .list_rewards(1)
64 .await
65 }
66
67 #[tracing::instrument(skip_all, level = "debug")]
75 pub async fn exchange(
76 state: &AppState,
77 member_id: i32,
78 reward_id: i32,
79 exchange_number: i32,
80 event_id: i32,
81 ) -> ApiResult<()> {
82 let repo = RewardRepository::new(state.db.clone());
83 let reward = repo
84 .get_reward_by_id(reward_id)
85 .await?
86 .ok_or_else(|| AppError::business("奖品不存在喵"))?;
87 if is_deadline_expired(reward.dead_line) {
88 return Err(AppError::business("已经超过兑换时间了哦~"));
89 }
90
91 let price = reward.price.unwrap_or(0) * exchange_number;
92 match reward.reward_category.unwrap_or_default() {
93 REWARD_CATEGORY_EXCHANGE => {
94 if repo.deduct_exchange_balance(member_id, price).await? == 0 {
95 return Err(AppError::business("余额不足喵,下次再加油吧~"));
96 }
97 repo.insert_reward_record(member_id, reward_id, exchange_number)
98 .await?;
99 }
100 REWARD_CATEGORY_LUCKY => {
101 let ticket_number = repo
102 .get_ticket_number(member_id)
103 .await?
104 .ok_or_else(|| AppError::business("这位猫娘没有奖券呢,下次一起参加活动吧~"))?;
105 if ticket_number.total < 12 {
106 return Err(AppError::business("奖券没有达到参与标准哦,下次加油吧~"));
107 }
108 if ticket_number.lucky_balance < price {
109 return Err(AppError::business("抽奖券不够啦,请耐心等待结果哟亲亲~"));
110 }
111 if repo.deduct_lucky_balance(member_id, price).await? == 0 {
112 return Err(AppError::business("剩余抽奖券不足了哦~"));
113 }
114 repo.insert_lucky_tickets(
115 member_id,
116 reward_id,
117 event_id,
118 reward.physical_category,
119 exchange_number,
120 )
121 .await?;
122 }
123 _ => {
124 return Err(AppError::business(
125 "奖品类型有误哦,请确认后再兑换或联系幻廊",
126 ));
127 }
128 }
129 Ok(())
130 }
131
132 #[tracing::instrument(skip_all, level = "debug")]
134 pub async fn get_reward_record(
135 state: &AppState,
136 member_id: i32,
137 ) -> ApiResult<Vec<RewardRecordResultDto>> {
138 RewardRepository::new(state.db.clone())
139 .select_reward_list(member_id)
140 .await
141 }
142
143 #[tracing::instrument(skip_all, level = "debug")]
145 pub async fn get_reward_tickets(
146 state: &AppState,
147 member_id: i32,
148 ) -> ApiResult<Vec<RewardTicketResultDto>> {
149 RewardRepository::new(state.db.clone())
150 .get_reward_tickets(member_id)
151 .await
152 }
153
154 #[tracing::instrument(skip_all, level = "debug")]
156 pub async fn get_tickets_details(
157 state: &AppState,
158 member_id: i32,
159 ) -> ApiResult<Vec<RewardTicketsDetailDto>> {
160 RewardRepository::new(state.db.clone())
161 .get_ticket_details(member_id)
162 .await
163 }
164
165 #[tracing::instrument(skip_all, level = "debug")]
167 pub async fn select_winner(
168 state: &AppState,
169 member_id: i32,
170 reward_id: i32,
171 event_id: i32,
172 winner_number: i32,
173 ) -> ApiResult<Vec<RewardWinnerDto>> {
174 if member_id != REWARD_HOST_MEMBER_ID {
175 return Err(AppError::business("请不要冒充主持人哦~"));
176 }
177 let mut tx = state.db.begin().await?;
178 let reward_row = sqlx::query(
179 "SELECT rewardId, physicalCategory, stock FROM rewardtb WHERE rewardId = ?",
180 )
181 .bind(reward_id)
182 .fetch_optional(&mut *tx)
183 .await?
184 .ok_or_else(|| AppError::business("奖品不存在,请确认选项或联系管理员"))?;
185 let physical_category: Option<i16> = reward_row.try_get("physicalCategory").ok();
186 let mut stock: i32 = reward_row.try_get("stock").unwrap_or(0);
187 let mut winners = Vec::new();
188
189 for _ in 0..winner_number.max(0) {
190 let winner_row = sqlx::query(
191 "SELECT rt.memberId, m.username AS memberName, rt.rewardTicketId AS ticketId \
192 FROM rewardtickettb rt LEFT JOIN membertb m ON rt.memberId = m.Id \
193 WHERE rt.rewardId = ? AND rt.fromEventId = ? AND rt.status = ? \
194 ORDER BY RAND() LIMIT 1",
195 )
196 .bind(reward_id)
197 .bind(event_id)
198 .bind(TICKET_STATUS_AVAILABLE)
199 .fetch_optional(&mut *tx)
200 .await?;
201
202 let Some(winner_row) = winner_row else {
203 break;
204 };
205 let winner = RewardWinnerDto {
206 member_id: winner_row.get("memberId"),
207 member_name: winner_row.try_get("memberName").ok(),
208 ticket_id: winner_row.try_get("ticketId").ok(),
209 };
210 sqlx::query(
211 "UPDATE rewardtickettb SET status = ? \
212 WHERE memberId = ? AND fromEventId = ? AND physicalCategory = ?",
213 )
214 .bind(TICKET_STATUS_SAME_TYPE_UNAVAILABLE)
215 .bind(winner.member_id)
216 .bind(event_id)
217 .bind(physical_category)
218 .execute(&mut *tx)
219 .await?;
220
221 let record = sqlx::query(
222 "INSERT INTO rewardrecordtb(memberId, rewardId, exchangeNumber, trackingNumber, exchangeTime) \
223 VALUES (?, ?, 1, '想起来了的话会填的喵~', NOW())",
224 )
225 .bind(winner.member_id)
226 .bind(reward_id)
227 .execute(&mut *tx)
228 .await?;
229
230 if let Some(ticket_id) = winner.ticket_id.as_deref() {
231 sqlx::query(
232 "UPDATE rewardtickettb SET status = ?, rewardRecordID = ? WHERE rewardTicketId = ?",
233 )
234 .bind(TICKET_STATUS_WIN)
235 .bind(record.last_insert_id() as i64)
236 .bind(ticket_id)
237 .execute(&mut *tx)
238 .await?;
239 }
240 winners.push(winner);
241 }
242
243 let winner_count = winners.len() as i32;
244 let stock_update = sqlx::query(
245 "UPDATE rewardtb SET stock = stock - ? WHERE rewardId = ? AND fromEventId = ? AND stock >= ?",
246 )
247 .bind(winner_count)
248 .bind(reward_id)
249 .bind(event_id)
250 .bind(winner_count)
251 .execute(&mut *tx)
252 .await?;
253 if stock_update.rows_affected() != 1 {
254 return Err(AppError::business("奖品信息不正确或者名额不足啦"));
255 }
256 stock -= winner_count;
257 if stock == 0 {
258 sqlx::query("UPDATE rewardtickettb SET status = ? WHERE rewardId = ? AND status = ?")
259 .bind(TICKET_STATUS_NON_WIN)
260 .bind(reward_id)
261 .bind(TICKET_STATUS_AVAILABLE)
262 .execute(&mut *tx)
263 .await?;
264 }
265
266 tx.commit().await?;
267 Ok(winners)
268 }
269
270 #[tracing::instrument(skip_all, level = "debug")]
272 pub async fn change_end_time(
273 state: &AppState,
274 member_id: i32,
275 event_id: &str,
276 end_time: &str,
277 ) -> ApiResult<()> {
278 if member_id != REWARD_HOST_MEMBER_ID {
279 return Err(AppError::business("请不要冒充主持人哦~"));
280 }
281 let event_id: i32 = event_id
282 .parse()
283 .map_err(|_| AppError::business("活动ID无效喵"))?;
284 let repo = RewardRepository::new(state.db.clone());
285 if repo.has_opened_rewards(event_id).await? {
286 return Err(AppError::business(
287 "已经开过奖了,再修改时间会导致违反抽奖规则的情况出现,商量一下具体怎么处理吧喵~",
288 ));
289 }
290 let new_end_time = NaiveDateTime::parse_from_str(end_time, "%Y-%m-%d %H:%M:%S")
291 .map_err(|_| AppError::business("时间格式不正确设置不正确,转换失败,请联系幻廊"))?;
292 repo.update_event_deadline(event_id, new_end_time).await?;
293 Ok(())
294 }
295
296 #[tracing::instrument(skip_all, level = "debug")]
298 pub async fn transfer(
299 state: &AppState,
300 from_id: i32,
301 body: RewardTicketTransferRequest,
302 ) -> ApiResult<()> {
303 let repo = RewardRepository::new(state.db.clone());
304 if body.target_member_id == 0 {
305 return Err(AppError::business("怎么没有设置目标组员呀"));
306 }
307 if body.amount == 0 {
308 return Err(AppError::business("别转账0张券为难程序员呀"));
309 }
310 let request_key = format!(
311 "{}_{}_{}_{}_{}",
312 body.event_id, from_id, body.target_member_id, body.ticket_type, body.amount
313 );
314 let _guard = TransferRequestGuard::try_acquire(request_key)?;
315 if !repo.check_account_exists(body.target_member_id).await? {
316 return Err(AppError::business("转账目标的组员不存在,请确认后再执行"));
317 }
318 if !repo
319 .check_balance_account_exists(body.target_member_id, body.event_id)
320 .await?
321 {
322 repo.create_balance_account(body.target_member_id, body.event_id)
323 .await?;
324 }
325 if repo
326 .transfer_decr(from_id, body.event_id, body.ticket_type, body.amount)
327 .await?
328 == 0
329 {
330 return Err(AppError::business("余额不足喵"));
331 }
332 repo.transfer_incr(
333 body.target_member_id,
334 body.event_id,
335 body.ticket_type,
336 body.amount,
337 )
338 .await?;
339 repo.insert_transfer_log(
340 from_id,
341 body.target_member_id,
342 body.event_id,
343 body.ticket_type,
344 body.amount,
345 )
346 .await
347 }
348
349 #[tracing::instrument(skip_all, level = "debug")]
351 pub async fn get_transfer_log(
352 state: &AppState,
353 member_id: i32,
354 ) -> ApiResult<Vec<RewardTicketTransferLog>> {
355 RewardRepository::new(state.db.clone())
356 .get_transfer_logs(member_id)
357 .await
358 }
359}
360
361fn is_deadline_expired(dead_line: Option<NaiveDateTime>) -> bool {
363 dead_line.is_some_and(|deadline| {
364 let now = chrono::Utc::now()
365 .with_timezone(&crate::utils::shanghai_time::shanghai_offset())
366 .naive_local();
367 now > deadline
368 })
369}
370
371fn transfer_request_map() -> &'static DashMap<String, ()> {
373 TRANSFER_REQUESTS.get_or_init(DashMap::new)
374}
375
376struct TransferRequestGuard {
378 key: String,
380}
381
382impl TransferRequestGuard {
383 fn try_acquire(key: String) -> ApiResult<Self> {
385 match transfer_request_map().entry(key.clone()) {
386 Entry::Vacant(entry) => {
387 entry.insert(());
388 Ok(Self { key })
389 }
390 Entry::Occupied(_) => Err(AppError::business("正在转账中,请稍后")),
391 }
392 }
393}
394
395impl Drop for TransferRequestGuard {
396 fn drop(&mut self) {
398 if let Some(map) = TRANSFER_REQUESTS.get() {
399 map.remove(&self.key);
400 }
401 }
402}