Skip to main content

tibba_model_token/
recharge.rs

1// Copyright 2026 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::{
16    Error, JsonSnafu, ModelListParams, Schema, SchemaAllowCreate, SchemaAllowEdit, SchemaOption,
17    SchemaOptionValue, SchemaType, SchemaView, SqlxSnafu, format_datetime,
18};
19use serde::{Deserialize, Serialize};
20use snafu::ResultExt;
21use sqlx::FromRow;
22use sqlx::{Pool, Postgres, QueryBuilder};
23use std::collections::HashMap;
24use tibba_model::Model;
25use time::PrimitiveDateTime;
26
27type Result<T> = std::result::Result<T, Error>;
28
29#[derive(FromRow)]
30struct TokenRechargeSchema {
31    id: i64,
32    user_id: i64,
33    amount: i64,
34    source: i16,
35    order_id: String,
36    expired_at: Option<PrimitiveDateTime>,
37    remark: String,
38    created_by: i64,
39    created: PrimitiveDateTime,
40    modified: PrimitiveDateTime,
41}
42
43#[derive(Debug, Clone, Deserialize, Serialize)]
44pub struct TokenRecharge {
45    pub id: i64,
46    pub user_id: i64,
47    pub amount: i64,
48    pub source: i16,
49    pub order_id: String,
50    pub expired_at: Option<String>,
51    pub remark: String,
52    pub created_by: i64,
53    pub created: String,
54    pub modified: String,
55}
56
57impl From<TokenRechargeSchema> for TokenRecharge {
58    fn from(s: TokenRechargeSchema) -> Self {
59        Self {
60            id: s.id,
61            user_id: s.user_id,
62            amount: s.amount,
63            source: s.source,
64            order_id: s.order_id,
65            expired_at: s.expired_at.map(format_datetime),
66            remark: s.remark,
67            created_by: s.created_by,
68            created: format_datetime(s.created),
69            modified: format_datetime(s.modified),
70        }
71    }
72}
73
74#[derive(Debug, Clone, Deserialize, Default)]
75pub struct TokenRechargeInsertParams {
76    pub user_id: i64,
77    pub amount: i64,
78    pub source: i16,
79    pub order_id: Option<String>,
80    pub expired_at: Option<String>,
81    pub remark: Option<String>,
82    pub created_by: Option<i64>,
83}
84
85#[derive(Default)]
86pub struct TokenRechargeModel {}
87
88impl TokenRechargeModel {
89    /// 按用户 ID 查询充值记录列表(分页)。
90    pub async fn list_by_user(
91        &self,
92        pool: &Pool<Postgres>,
93        user_id: i64,
94        page: u64,
95        limit: u64,
96    ) -> Result<Vec<TokenRecharge>> {
97        let limit = limit.min(200);
98        let offset = (page.max(1) - 1) * limit;
99        let rows = sqlx::query_as::<_, TokenRechargeSchema>(
100            r#"SELECT * FROM token_recharges WHERE user_id = $1 AND deleted_at IS NULL ORDER BY id DESC LIMIT $2 OFFSET $3"#,
101        )
102        .bind(user_id)
103        .bind(limit as i64)
104        .bind(offset as i64)
105        .fetch_all(pool)
106        .await
107        .context(SqlxSnafu)?;
108        Ok(rows.into_iter().map(Into::into).collect())
109    }
110
111    /// 按订单号查询(用于支付回调去重)。
112    pub async fn get_by_order_id(
113        &self,
114        pool: &Pool<Postgres>,
115        order_id: &str,
116    ) -> Result<Option<TokenRecharge>> {
117        let result = sqlx::query_as::<_, TokenRechargeSchema>(
118            r#"SELECT * FROM token_recharges WHERE order_id = $1 AND deleted_at IS NULL LIMIT 1"#,
119        )
120        .bind(order_id)
121        .fetch_optional(pool)
122        .await
123        .context(SqlxSnafu)?;
124        Ok(result.map(Into::into))
125    }
126}
127
128impl Model for TokenRechargeModel {
129    type Output = TokenRecharge;
130    fn new() -> Self {
131        Self::default()
132    }
133
134    async fn schema_view(&self, _pool: &Pool<Postgres>) -> SchemaView {
135        let source_options = vec![
136            SchemaOption {
137                label: "购买".to_string(),
138                value: SchemaOptionValue::Integer(1),
139            },
140            SchemaOption {
141                label: "赠送".to_string(),
142                value: SchemaOptionValue::Integer(2),
143            },
144            SchemaOption {
145                label: "退款".to_string(),
146                value: SchemaOptionValue::Integer(3),
147            },
148            SchemaOption {
149                label: "管理员调整".to_string(),
150                value: SchemaOptionValue::Integer(4),
151            },
152        ];
153        SchemaView {
154            schemas: vec![
155                Schema::new_id(),
156                Schema {
157                    filterable: true,
158                    ..Schema::new_user_search("user_id")
159                },
160                Schema {
161                    name: "amount".to_string(),
162                    category: SchemaType::Number,
163                    required: true,
164                    ..Default::default()
165                },
166                Schema {
167                    name: "source".to_string(),
168                    category: SchemaType::Number,
169                    required: true,
170                    options: Some(source_options),
171                    filterable: true,
172                    ..Default::default()
173                },
174                Schema {
175                    name: "order_id".to_string(),
176                    category: SchemaType::String,
177                    ..Default::default()
178                },
179                Schema {
180                    name: "expired_at".to_string(),
181                    category: SchemaType::Date,
182                    ..Default::default()
183                },
184                Schema::new_remark(),
185                Schema {
186                    name: "created_by".to_string(),
187                    category: SchemaType::Number,
188                    read_only: true,
189                    ..Default::default()
190                },
191                Schema::new_created(),
192                Schema::new_filterable_modified(),
193            ],
194            allow_edit: SchemaAllowEdit {
195                roles: vec!["su".to_string(), "admin".to_string()],
196                ..Default::default()
197            },
198            allow_create: SchemaAllowCreate {
199                roles: vec!["su".to_string(), "admin".to_string()],
200                ..Default::default()
201            },
202        }
203    }
204
205    async fn insert(&self, pool: &Pool<Postgres>, mut data: serde_json::Value) -> Result<u64> {
206        // user_id 支持前端以字符串形式传入
207        if let Some(obj) = data.as_object_mut() {
208            if let Some(id_str) = obj.get("user_id").and_then(|v| v.as_str()) {
209                if let Ok(id) = id_str.parse::<i64>() {
210                    obj.insert("user_id".to_string(), id.into());
211                }
212            }
213        }
214        let p: TokenRechargeInsertParams = serde_json::from_value(data).context(JsonSnafu)?;
215        let row: (i64,) = sqlx::query_as(
216            r#"INSERT INTO token_recharges (user_id, amount, source, order_id, remark, created_by)
217               VALUES ($1, $2, $3, $4, $5, $6) RETURNING id"#,
218        )
219        .bind(p.user_id)
220        .bind(p.amount)
221        .bind(p.source)
222        .bind(p.order_id.unwrap_or_default())
223        .bind(p.remark.unwrap_or_default())
224        .bind(p.created_by.unwrap_or(0))
225        .fetch_one(pool)
226        .await
227        .context(SqlxSnafu)?;
228        Ok(row.0 as u64)
229    }
230
231    async fn get_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<Option<Self::Output>> {
232        let result = sqlx::query_as::<_, TokenRechargeSchema>(
233            r#"SELECT * FROM token_recharges WHERE id = $1 AND deleted_at IS NULL"#,
234        )
235        .bind(id as i64)
236        .fetch_optional(pool)
237        .await
238        .context(SqlxSnafu)?;
239        Ok(result.map(Into::into))
240    }
241
242    async fn delete_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<()> {
243        sqlx::query(
244            r#"UPDATE token_recharges SET deleted_at = NOW(), modified = NOW() WHERE id = $1 AND deleted_at IS NULL"#,
245        )
246        .bind(id as i64)
247        .execute(pool)
248        .await
249        .context(SqlxSnafu)?;
250        Ok(())
251    }
252
253    async fn count(&self, pool: &Pool<Postgres>, params: &ModelListParams) -> Result<i64> {
254        let mut qb: QueryBuilder<Postgres> =
255            QueryBuilder::new("SELECT COUNT(*) FROM token_recharges");
256        self.push_conditions(&mut qb, params)?;
257        let row: (i64,) = qb
258            .build_query_as()
259            .fetch_one(pool)
260            .await
261            .context(SqlxSnafu)?;
262        Ok(row.0)
263    }
264
265    async fn list(
266        &self,
267        pool: &Pool<Postgres>,
268        params: &ModelListParams,
269    ) -> Result<Vec<Self::Output>> {
270        let mut qb: QueryBuilder<Postgres> = QueryBuilder::new("SELECT * FROM token_recharges");
271        self.push_conditions(&mut qb, params)?;
272        params.push_pagination(&mut qb);
273        let rows = qb
274            .build_query_as::<TokenRechargeSchema>()
275            .fetch_all(pool)
276            .await
277            .context(SqlxSnafu)?;
278        Ok(rows.into_iter().map(Into::into).collect())
279    }
280
281    fn push_filter_conditions<'args>(
282        &self,
283        qb: &mut QueryBuilder<'args, Postgres>,
284        filters: &HashMap<String, String>,
285    ) -> Result<()> {
286        if let Some(user_id) = filters.get("user_id") {
287            if let Ok(v) = user_id.parse::<i64>() {
288                qb.push(" AND user_id = ").push_bind(v);
289            }
290        }
291        if let Some(source) = filters.get("source") {
292            if let Ok(v) = source.parse::<i16>() {
293                qb.push(" AND source = ").push_bind(v);
294            }
295        }
296        Ok(())
297    }
298}