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::{DateTime, NaiveDateTime, Utc};
19use dashmap::{mapref::entry::Entry, DashMap};
20use sea_orm::TransactionTrait;
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_SAME_TYPE_UNAVAILABLE: i16 = 3;
31const TICKET_STATUS_WIN: i16 = 4;
33const REWARD_HOST_MEMBER_ID: i32 = 154;
35
36static TRANSFER_REQUESTS: OnceLock<DashMap<String, ()>> = OnceLock::new();
38
39pub struct RewardService;
41
42impl RewardService {
43 #[tracing::instrument(skip_all, level = "debug")]
45 pub async fn get_reward_balance(
46 state: &AppState,
47 member_id: i32,
48 event_id: i32,
49 ) -> ApiResult<EventBalanceDto> {
50 let repo = RewardRepository::new(state.db.clone());
51 if !repo.check_account_exists(member_id).await? {
52 repo.create_balance_account(member_id, event_id).await?;
53 }
54 repo.get_reward_balance(member_id, event_id).await
55 }
56
57 #[tracing::instrument(skip_all, level = "debug")]
59 pub async fn get_rewards_list(state: &AppState) -> ApiResult<Vec<Rewardtb>> {
60 RewardRepository::new(state.db.clone())
61 .list_rewards(1)
62 .await
63 }
64
65 #[tracing::instrument(skip_all, level = "debug")]
73 pub async fn exchange(
74 state: &AppState,
75 member_id: i32,
76 reward_id: i32,
77 exchange_number: i32,
78 event_id: i32,
79 ) -> ApiResult<()> {
80 let repo = RewardRepository::new(state.db.clone());
81 let reward = repo
82 .get_reward_by_id(reward_id)
83 .await?
84 .ok_or_else(|| AppError::business("奖品不存在喵"))?;
85 if is_deadline_expired(reward.dead_line) {
86 return Err(AppError::business("已经超过兑换时间了哦~"));
87 }
88
89 let price = reward.price.unwrap_or(0) * exchange_number;
90 match reward.reward_category.unwrap_or_default() {
91 REWARD_CATEGORY_EXCHANGE => {
92 if repo.deduct_exchange_balance(member_id, price).await? == 0 {
93 return Err(AppError::business("余额不足喵,下次再加油吧~"));
94 }
95 repo.insert_reward_record(member_id, reward_id, exchange_number)
96 .await?;
97 }
98 REWARD_CATEGORY_LUCKY => {
99 let ticket_number = repo
100 .get_ticket_number(member_id)
101 .await?
102 .ok_or_else(|| AppError::business("这位猫娘没有奖券呢,下次一起参加活动吧~"))?;
103 if ticket_number.total < 12 {
104 return Err(AppError::business("奖券没有达到参与标准哦,下次加油吧~"));
105 }
106 if ticket_number.lucky_balance < price {
107 return Err(AppError::business("抽奖券不够啦,请耐心等待结果哟亲亲~"));
108 }
109 if repo.deduct_lucky_balance(member_id, price).await? == 0 {
110 return Err(AppError::business("剩余抽奖券不足了哦~"));
111 }
112 repo.insert_lucky_tickets(
113 member_id,
114 reward_id,
115 event_id,
116 reward.physical_category,
117 exchange_number,
118 )
119 .await?;
120 }
121 _ => {
122 return Err(AppError::business(
123 "奖品类型有误哦,请确认后再兑换或联系幻廊",
124 ));
125 }
126 }
127 Ok(())
128 }
129
130 #[tracing::instrument(skip_all, level = "debug")]
132 pub async fn get_reward_record(
133 state: &AppState,
134 member_id: i32,
135 ) -> ApiResult<Vec<RewardRecordResultDto>> {
136 RewardRepository::new(state.db.clone())
137 .select_reward_list(member_id)
138 .await
139 }
140
141 #[tracing::instrument(skip_all, level = "debug")]
143 pub async fn get_reward_tickets(
144 state: &AppState,
145 member_id: i32,
146 ) -> ApiResult<Vec<RewardTicketResultDto>> {
147 RewardRepository::new(state.db.clone())
148 .get_reward_tickets(member_id)
149 .await
150 }
151
152 #[tracing::instrument(skip_all, level = "debug")]
154 pub async fn get_tickets_details(
155 state: &AppState,
156 member_id: i32,
157 ) -> ApiResult<Vec<RewardTicketsDetailDto>> {
158 RewardRepository::new(state.db.clone())
159 .get_ticket_details(member_id)
160 .await
161 }
162
163 #[tracing::instrument(skip_all, level = "debug")]
165 pub async fn select_winner(
166 state: &AppState,
167 member_id: i32,
168 reward_id: i32,
169 event_id: i32,
170 winner_number: i32,
171 ) -> ApiResult<Vec<RewardWinnerDto>> {
172 if member_id != REWARD_HOST_MEMBER_ID {
173 return Err(AppError::business("请不要冒充主持人哦~"));
174 }
175 let db = crate::db::from_sqlx_pool(state.db.clone());
176 let txn = db.begin().await?;
177 let (physical_category, mut stock) =
178 RewardRepository::get_reward_draw_info_with(&txn, reward_id)
179 .await?
180 .ok_or_else(|| AppError::business("奖品不存在,请确认选项或联系管理员"))?;
181 let mut winners = Vec::new();
182
183 for _ in 0..winner_number.max(0) {
184 let Some(winner) = RewardRepository::select_random_winner_with(
185 &txn,
186 reward_id,
187 event_id,
188 TICKET_STATUS_AVAILABLE,
189 )
190 .await?
191 else {
192 break;
193 };
194 RewardRepository::block_winner_with(
195 &txn,
196 event_id,
197 TICKET_STATUS_SAME_TYPE_UNAVAILABLE,
198 winner.member_id,
199 physical_category,
200 )
201 .await?;
202 let record_id =
203 RewardRepository::insert_reward_record_with(&txn, winner.member_id, reward_id, 1)
204 .await?;
205 if let Some(ticket_id) = winner.ticket_id.as_deref() {
206 RewardRepository::update_ticket_status_with(
207 &txn,
208 ticket_id,
209 TICKET_STATUS_WIN,
210 record_id,
211 )
212 .await?;
213 }
214 winners.push(winner);
215 }
216
217 let winner_count = winners.len() as i32;
218 if RewardRepository::decrement_stock_with(&txn, reward_id, event_id, winner_count).await?
219 != 1
220 {
221 return Err(AppError::business("奖品信息不正确或者名额不足啦"));
222 }
223 stock -= winner_count;
224 if stock == 0 {
225 RewardRepository::mark_non_win_tickets_with(&txn, reward_id).await?;
226 }
227
228 txn.commit().await?;
229 Ok(winners)
230 }
231
232 #[tracing::instrument(skip_all, level = "debug")]
234 pub async fn change_end_time(
235 state: &AppState,
236 member_id: i32,
237 event_id: &str,
238 end_time: &str,
239 ) -> ApiResult<()> {
240 if member_id != REWARD_HOST_MEMBER_ID {
241 return Err(AppError::business("请不要冒充主持人哦~"));
242 }
243 let event_id: i32 = event_id
244 .parse()
245 .map_err(|_| AppError::business("活动ID无效喵"))?;
246 let repo = RewardRepository::new(state.db.clone());
247 if repo.has_opened_rewards(event_id).await? {
248 return Err(AppError::business(
249 "已经开过奖了,再修改时间会导致违反抽奖规则的情况出现,商量一下具体怎么处理吧喵~",
250 ));
251 }
252 let end_time = DateTime::parse_from_rfc3339(end_time.trim())
253 .or_else(|_| {
254 let normalized = end_time.replace('T', " ");
255 NaiveDateTime::parse_from_str(normalized.trim(), "%Y-%m-%d %H:%M:%S")
256 .map(|naive| naive.and_utc().fixed_offset())
257 })
258 .map_err(|_| AppError::business("时间格式不正确设置不正确,转换失败,请联系幻廊"))?
259 .with_timezone(&Utc);
260 repo.update_event_deadline(event_id, end_time).await?;
261 Ok(())
262 }
263
264 #[tracing::instrument(skip_all, level = "debug")]
266 pub async fn transfer(
267 state: &AppState,
268 from_id: i32,
269 body: RewardTicketTransferRequest,
270 ) -> ApiResult<()> {
271 let repo = RewardRepository::new(state.db.clone());
272 if body.target_member_id == 0 {
273 return Err(AppError::business("怎么没有设置目标组员呀"));
274 }
275 if body.amount == 0 {
276 return Err(AppError::business("别转账0张券为难程序员呀"));
277 }
278 let request_key = format!(
279 "{}_{}_{}_{}_{}",
280 body.event_id, from_id, body.target_member_id, body.ticket_type, body.amount
281 );
282 let _guard = TransferRequestGuard::try_acquire(request_key)?;
283 if !repo.check_account_exists(body.target_member_id).await? {
284 return Err(AppError::business("转账目标的组员不存在,请确认后再执行"));
285 }
286 if !repo
287 .check_balance_account_exists(body.target_member_id, body.event_id)
288 .await?
289 {
290 repo.create_balance_account(body.target_member_id, body.event_id)
291 .await?;
292 }
293 if repo
294 .transfer_decr(from_id, body.event_id, body.ticket_type, body.amount)
295 .await?
296 == 0
297 {
298 return Err(AppError::business("余额不足喵"));
299 }
300 repo.transfer_incr(
301 body.target_member_id,
302 body.event_id,
303 body.ticket_type,
304 body.amount,
305 )
306 .await?;
307 repo.insert_transfer_log(
308 from_id,
309 body.target_member_id,
310 body.event_id,
311 body.ticket_type,
312 body.amount,
313 )
314 .await
315 }
316
317 #[tracing::instrument(skip_all, level = "debug")]
319 pub async fn get_transfer_log(
320 state: &AppState,
321 member_id: i32,
322 ) -> ApiResult<Vec<RewardTicketTransferLog>> {
323 RewardRepository::new(state.db.clone())
324 .get_transfer_logs(member_id)
325 .await
326 }
327}
328
329fn is_deadline_expired(dead_line: Option<DateTime<Utc>>) -> bool {
331 dead_line.is_some_and(|deadline| Utc::now() > deadline)
332}
333
334fn transfer_request_map() -> &'static DashMap<String, ()> {
336 TRANSFER_REQUESTS.get_or_init(DashMap::new)
337}
338
339struct TransferRequestGuard {
341 key: String,
343}
344
345impl TransferRequestGuard {
346 fn try_acquire(key: String) -> ApiResult<Self> {
348 match transfer_request_map().entry(key.clone()) {
349 Entry::Vacant(entry) => {
350 entry.insert(());
351 Ok(Self { key })
352 }
353 Entry::Occupied(_) => Err(AppError::business("正在转账中,请稍后")),
354 }
355 }
356}
357
358impl Drop for TransferRequestGuard {
359 fn drop(&mut self) {
361 if let Some(map) = TRANSFER_REQUESTS.get() {
362 map.remove(&self.key);
363 }
364 }
365}