Skip to main content

tdm_server_rust/service/
reward_service.rs

1//! 奖励业务服务 (Reward Service)
2//!
3//! 悬赏/奖励/抽奖系统的业务逻辑:
4//! - 活动余额查询
5//! - 奖品兑换与库存扣减
6//! - 奖券抽选(随机中奖者)
7//! - 券转账与日志
8
9use 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
23/// 兑换类奖品
24const REWARD_CATEGORY_EXCHANGE: i16 = 2;
25/// 抽奖类奖品
26const REWARD_CATEGORY_LUCKY: i16 = 3;
27/// 有效且待开奖
28const TICKET_STATUS_AVAILABLE: i16 = 1;
29/// 因中同类奖作废
30const TICKET_STATUS_SAME_TYPE_UNAVAILABLE: i16 = 3;
31/// 已中奖
32const TICKET_STATUS_WIN: i16 = 4;
33/// 抽奖主持人组员 ID
34const REWARD_HOST_MEMBER_ID: i32 = 154;
35
36/// 正在处理中的转账请求
37static TRANSFER_REQUESTS: OnceLock<DashMap<String, ()>> = OnceLock::new();
38
39/// 奖励服务
40pub struct RewardService;
41
42impl RewardService {
43    /// 查询活动余额
44    #[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    /// 奖品列表(默认活动 1)
58    #[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    /// 兑换奖品:兑换类写获奖记录,抽奖类写入奖池。
66    ///
67    /// # Errors
68    ///
69    /// - `AppError::business("奖品不存在喵")` — reward_id 无效
70    /// - `AppError::business("余额不足喵")` — 兑换余额不足
71    /// - `AppError::Database` — 数据库操作失败
72    #[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    /// 兑换记录
131    #[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    /// 奖券列表
142    #[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    /// 奖券详情
153    #[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    /// 抽取中奖者
164    #[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    /// 修改活动结束时间(对齐 Java:更新 rewardtb.deadLine)
233    #[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    /// 转账
265    #[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    /// 转账日志
318    #[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
329/// 判断奖品是否超过兑换截止时间(UTC instant 比较)
330fn is_deadline_expired(dead_line: Option<DateTime<Utc>>) -> bool {
331    dead_line.is_some_and(|deadline| Utc::now() > deadline)
332}
333
334/// 获取全局转账请求表
335fn transfer_request_map() -> &'static DashMap<String, ()> {
336    TRANSFER_REQUESTS.get_or_init(DashMap::new)
337}
338
339/// 转账防重复请求守卫
340struct TransferRequestGuard {
341    /// 防重复请求键
342    key: String,
343}
344
345impl TransferRequestGuard {
346    /// 尝试占用转账请求键
347    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    /// 释放转账请求键
360    fn drop(&mut self) {
361        if let Some(map) = TRANSFER_REQUESTS.get() {
362            map.remove(&self.key);
363        }
364    }
365}