Skip to main content

tibba_model/
user.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, Model, ModelListParams, Schema, SchemaAllowEdit, SchemaOption,
17    SchemaOptionValue, SchemaType, SchemaView, SqlxSnafu, Status, format_datetime,
18    new_schema_options,
19};
20use serde::{Deserialize, Serialize};
21use snafu::ResultExt;
22use sqlx::FromRow;
23use sqlx::types::Json;
24use sqlx::{Pool, Postgres, QueryBuilder};
25use std::collections::HashMap;
26use time::PrimitiveDateTime;
27type Result<T> = std::result::Result<T, Error>;
28
29pub const ROLE_ADMIN: &str = "admin";
30pub const ROLE_SUPER_ADMIN: &str = "su";
31
32#[derive(FromRow)]
33struct UserSchema {
34    id: i64,
35    status: i16,
36    created: PrimitiveDateTime,
37    modified: PrimitiveDateTime,
38    account: String,
39    password: String,
40    nickname: Option<String>,
41    phone: Option<String>,
42    roles: Option<Json<Vec<String>>>,
43    groups: Option<Json<Vec<String>>>,
44    remark: Option<String>,
45    email: Option<String>,
46    avatar: Option<String>,
47    last_login_at: Option<PrimitiveDateTime>,
48}
49
50#[derive(Deserialize, Serialize)]
51pub struct User {
52    pub id: i64,
53    pub status: i16,
54    pub created: String,
55    pub modified: String,
56    pub account: String,
57    #[serde(skip_serializing)]
58    pub password: String,
59    pub nickname: Option<String>,
60    pub phone: Option<String>,
61    pub roles: Option<Vec<String>>,
62    pub groups: Option<Vec<String>>,
63    pub remark: Option<String>,
64    pub email: Option<String>,
65    pub avatar: Option<String>,
66    pub last_login_at: Option<String>,
67}
68
69impl From<UserSchema> for User {
70    fn from(user: UserSchema) -> Self {
71        Self {
72            id: user.id,
73            status: user.status,
74            created: format_datetime(user.created),
75            modified: format_datetime(user.modified),
76            account: user.account,
77            password: user.password,
78            nickname: user.nickname,
79            phone: user.phone,
80            roles: user.roles.map(|roles| roles.0),
81            groups: user.groups.map(|groups| groups.0),
82            remark: user.remark,
83            email: user.email,
84            avatar: user.avatar,
85            last_login_at: user.last_login_at.map(format_datetime),
86        }
87    }
88}
89
90#[derive(Debug, Clone, Deserialize, Default)]
91pub struct UserUpdateParams {
92    pub nickname: Option<String>,
93    pub phone: Option<String>,
94    pub email: Option<String>,
95    pub avatar: Option<String>,
96    pub roles: Option<Vec<String>>,
97    pub groups: Option<Vec<String>>,
98    pub status: Option<i16>,
99}
100
101pub struct UserModel {}
102
103impl Model for UserModel {
104    type Output = User;
105    fn new() -> Self {
106        Self {}
107    }
108    fn keyword(&self) -> String {
109        "account".to_string()
110    }
111    async fn schema_view(&self, _pool: &Pool<Postgres>) -> SchemaView {
112        SchemaView {
113            schemas: vec![
114                Schema::new_id(),
115                Schema {
116                    name: "account".to_string(),
117                    category: SchemaType::String,
118                    read_only: true,
119                    required: true,
120                    identity: true,
121                    ..Default::default()
122                },
123                Schema::new_status(),
124                Schema {
125                    name: "nickname".to_string(),
126                    category: SchemaType::String,
127                    ..Default::default()
128                },
129                Schema {
130                    name: "phone".to_string(),
131                    category: SchemaType::String,
132                    ..Default::default()
133                },
134                Schema {
135                    name: "roles".to_string(),
136                    category: SchemaType::Strings,
137                    options: Some(new_schema_options(&[ROLE_ADMIN, ROLE_SUPER_ADMIN])),
138                    ..Default::default()
139                },
140                Schema {
141                    name: "groups".to_string(),
142                    category: SchemaType::Strings,
143                    options: Some(new_schema_options(&["it", "marketing"])),
144                    ..Default::default()
145                },
146                Schema {
147                    name: "last_login_at".to_string(),
148                    category: SchemaType::Date,
149                    read_only: true,
150                    ..Default::default()
151                },
152                Schema::new_created(),
153                Schema::new_modified(),
154            ],
155            allow_edit: SchemaAllowEdit {
156                owner: true,
157                roles: vec![ROLE_SUPER_ADMIN.to_string()],
158                ..Default::default()
159            },
160            ..Default::default()
161        }
162    }
163    async fn get_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<Option<Self::Output>> {
164        let result = sqlx::query_as::<_, UserSchema>(
165            r#"SELECT * FROM users WHERE id = $1 AND deleted_at IS NULL"#,
166        )
167        .bind(id as i64)
168        .fetch_optional(pool)
169        .await
170        .context(SqlxSnafu)?;
171
172        Ok(result.map(|user| user.into()))
173    }
174    async fn delete_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<()> {
175        sqlx::query(
176            r#"UPDATE users SET deleted_at = NOW(), modified = NOW() WHERE id = $1 AND deleted_at IS NULL"#
177        )
178            .bind(id as i64)
179            .execute(pool)
180            .await
181            .context(SqlxSnafu)?;
182        Ok(())
183    }
184    async fn update_by_id(
185        &self,
186        pool: &Pool<Postgres>,
187        id: u64,
188        data: serde_json::Value,
189    ) -> Result<()> {
190        let params: UserUpdateParams = serde_json::from_value(data).context(JsonSnafu)?;
191        let _ = sqlx::query(
192            r#"
193            UPDATE users SET
194                email = COALESCE($1, email),
195                avatar = COALESCE($2, avatar),
196                roles = COALESCE($3, roles),
197                groups = COALESCE($4, groups),
198                status = COALESCE($5, status),
199                nickname = COALESCE($6, nickname),
200                phone = COALESCE($7, phone),
201                modified = NOW()
202            WHERE id = $8 AND deleted_at IS NULL
203            "#,
204        )
205        .bind(params.email.as_deref())
206        .bind(params.avatar.as_deref())
207        .bind(params.roles.map(Json))
208        .bind(params.groups.map(Json))
209        .bind(params.status)
210        .bind(params.nickname.as_deref())
211        .bind(params.phone.as_deref())
212        .bind(id as i64)
213        .execute(pool)
214        .await
215        .context(SqlxSnafu)?;
216
217        Ok(())
218    }
219    fn push_filter_conditions<'args>(
220        &self,
221        qb: &mut QueryBuilder<'args, Postgres>,
222        filters: &HashMap<String, String>,
223    ) -> Result<()> {
224        if let Some(status) = filters.get("status").and_then(|s| s.parse::<i16>().ok()) {
225            qb.push(" AND status = ");
226            qb.push_bind(status);
227        }
228        if let Some(role) = filters.get("role") {
229            qb.push(" AND roles @> ");
230            qb.push_bind(Json(vec![role.clone()]));
231            qb.push("::jsonb");
232        }
233        if let Some(group) = filters.get("group") {
234            qb.push(" AND groups @> ");
235            qb.push_bind(Json(vec![group.clone()]));
236            qb.push("::jsonb");
237        }
238        Ok(())
239    }
240
241    async fn count(&self, pool: &Pool<Postgres>, params: &ModelListParams) -> Result<i64> {
242        let mut qb = QueryBuilder::new("SELECT COUNT(*) FROM users");
243        self.push_conditions(&mut qb, params)?;
244        let count = qb
245            .build_query_scalar::<i64>()
246            .fetch_one(pool)
247            .await
248            .context(SqlxSnafu)?;
249        Ok(count)
250    }
251
252    async fn list(
253        &self,
254        pool: &Pool<Postgres>,
255        params: &ModelListParams,
256    ) -> Result<Vec<Self::Output>> {
257        let mut qb = QueryBuilder::new("SELECT * FROM users");
258        self.push_conditions(&mut qb, params)?;
259        params.push_pagination(&mut qb);
260        let result = qb
261            .build_query_as::<UserSchema>()
262            .fetch_all(pool)
263            .await
264            .context(SqlxSnafu)?;
265        Ok(result.into_iter().map(|u| u.into()).collect())
266    }
267    async fn search_options(
268        &self,
269        pool: &Pool<Postgres>,
270        keyword: Option<String>,
271    ) -> Result<Vec<SchemaOption>> {
272        let params = ModelListParams {
273            keyword,
274            limit: 20,
275            page: 1,
276            ..Default::default()
277        };
278        let users = self.list(pool, &params).await?;
279        Ok(users
280            .into_iter()
281            .map(|u| SchemaOption {
282                label: u.account,
283                value: SchemaOptionValue::String(u.id.to_string()),
284            })
285            .collect())
286    }
287}
288
289impl UserModel {
290    pub async fn register(
291        &self,
292        pool: &Pool<Postgres>,
293        account: &str,
294        password: &str,
295    ) -> Result<u64> {
296        // Get current time for created_at and updated_at
297
298        // Insert user and return the last insert ID
299        let row: (i64,) = sqlx::query_as(
300            r#"
301            INSERT INTO users (
302                status, account, password
303            ) VALUES (
304                $1, $2, $3
305            ) RETURNING id
306            "#,
307        )
308        .bind(Status::Enabled as i16)
309        .bind(account)
310        .bind(password)
311        .fetch_one(pool)
312        .await
313        .context(SqlxSnafu)?;
314
315        Ok(row.0 as u64)
316    }
317
318    pub async fn get_by_account(
319        &self,
320        pool: &Pool<Postgres>,
321        account: &str,
322    ) -> Result<Option<User>> {
323        let result = sqlx::query_as::<_, UserSchema>(
324            r#"SELECT * FROM users WHERE account = $1 AND deleted_at IS NULL"#,
325        )
326        .bind(account)
327        .fetch_optional(pool)
328        .await
329        .context(SqlxSnafu)?;
330
331        Ok(result.map(|user| user.into()))
332    }
333
334    pub async fn update_by_account(
335        &self,
336        pool: &Pool<Postgres>,
337        account: &str,
338        params: UserUpdateParams,
339    ) -> Result<()> {
340        let _ = sqlx::query(
341            r#"
342            UPDATE users SET
343                email = COALESCE($1, email),
344                avatar = COALESCE($2, avatar),
345                nickname = COALESCE($3, nickname),
346                phone = COALESCE($4, phone),
347                modified = NOW()
348            WHERE account = $5 AND deleted_at IS NULL
349            "#,
350        )
351        .bind(params.email.as_deref())
352        .bind(params.avatar.as_deref())
353        .bind(params.nickname.as_deref())
354        .bind(params.phone.as_deref())
355        .bind(account)
356        .execute(pool)
357        .await
358        .context(SqlxSnafu)?;
359        Ok(())
360    }
361
362    /// 登录成功后更新 last_login_at 为当前时间。
363    pub async fn update_last_login_at(&self, pool: &Pool<Postgres>, account: &str) -> Result<()> {
364        sqlx::query(
365            r#"UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE account = $1 AND deleted_at IS NULL"#,
366        )
367        .bind(account)
368        .execute(pool)
369        .await
370        .context(SqlxSnafu)?;
371        Ok(())
372    }
373}