Skip to main content

tdm_server_rust/repository/
manga_benefit_repo.rs

1//! 漫画特典数据访问层 (Manga Benefit Repository)
2//!
3//! 封装 `manga_benefit` 表的增删改查(SeaORM)。
4
5use crate::db::DbConn;
6use crate::sea_entity::manga_benefit;
7use chrono::{DateTime, Utc};
8use sea_orm::{
9    ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, Set,
10};
11use sqlx::postgres::PgPool;
12
13/// 特典数据库行(仓储层 DTO)
14#[derive(Debug, Clone)]
15pub struct BenefitRow {
16    /// 主键
17    pub id: i32,
18    /// 漫画 ID
19    pub manga_id: i32,
20    /// 卷号
21    pub volume_number: i32,
22    /// 卷标题
23    pub volume_title: Option<String>,
24    /// 店铺
25    pub store_name: Option<String>,
26    /// 特典名
27    pub benefit_name: Option<String>,
28    /// 标签
29    pub benefit_tag: Option<String>,
30    /// 图片
31    pub img_url: Option<String>,
32    /// 类型
33    pub r#type: Option<i16>,
34    /// 发布时间
35    pub publish_time: Option<DateTime<Utc>>,
36}
37
38/// 新增/更新特典输入
39#[derive(Debug, Clone)]
40pub struct BenefitUpsert {
41    /// 漫画 ID
42    pub manga_id: i32,
43    /// 卷号
44    pub volume_number: i32,
45    /// 卷标题
46    pub volume_title: Option<String>,
47    /// 店铺
48    pub store_name: Option<String>,
49    /// 特典名
50    pub benefit_name: Option<String>,
51    /// 标签
52    pub benefit_tag: Option<String>,
53    /// 类型
54    pub r#type: Option<i16>,
55    /// 图片 URL
56    pub img_url: Option<String>,
57    /// 发布时间
58    pub publish_time: Option<DateTime<Utc>>,
59}
60
61/// 特典仓储
62pub struct MangaBenefitRepository {
63    /// SeaORM 数据库连接
64    db: DbConn,
65}
66
67impl MangaBenefitRepository {
68    /// 从 `PgPool` 构造
69    pub fn new(pool: PgPool) -> Self {
70        Self {
71            db: crate::db::from_sqlx_pool(pool),
72        }
73    }
74
75    /// 从 `DatabaseConnection` 构造
76    pub fn from_db(db: DatabaseConnection) -> Self {
77        Self { db }
78    }
79
80    /// 按漫画 ID 查询未删除特典(按卷号、ID 排序)
81    #[tracing::instrument(skip_all, level = "debug")]
82    pub async fn list_by_manga_id(
83        &self,
84        manga_id: i32,
85    ) -> crate::error::ApiResult<Vec<BenefitRow>> {
86        let rows = manga_benefit::Entity::find()
87            .filter(manga_benefit::Column::MangaId.eq(manga_id))
88            .filter(manga_benefit::Column::DeletedAt.is_null())
89            .order_by_asc(manga_benefit::Column::VolumeNumber)
90            .order_by_asc(manga_benefit::Column::Id)
91            .all(&self.db)
92            .await?;
93        Ok(rows.into_iter().map(model_to_row).collect())
94    }
95
96    /// 按 ID 查询未删除特典
97    #[tracing::instrument(skip_all, level = "debug")]
98    pub async fn get_by_id(&self, id: i32) -> crate::error::ApiResult<Option<BenefitRow>> {
99        let row = manga_benefit::Entity::find_by_id(id)
100            .filter(manga_benefit::Column::DeletedAt.is_null())
101            .one(&self.db)
102            .await?;
103        Ok(row.map(model_to_row))
104    }
105
106    /// 新增特典
107    #[tracing::instrument(skip_all, level = "debug")]
108    pub async fn insert(&self, input: &BenefitUpsert) -> crate::error::ApiResult<i32> {
109        let now = chrono::Utc::now().naive_utc();
110        let model = manga_benefit::ActiveModel {
111            manga_id: Set(input.manga_id),
112            volume_number: Set(input.volume_number),
113            volume_title: Set(input.volume_title.clone()),
114            store_name: Set(input.store_name.clone()),
115            benefit_name: Set(input.benefit_name.clone()),
116            benefit_tag: Set(input.benefit_tag.clone()),
117            r#type: Set(input.r#type),
118            img_url: Set(input.img_url.clone()),
119            publish_time: Set(input.publish_time.map(|t| t.naive_utc())),
120            created_at: Set(Some(now)),
121            updated_at: Set(Some(now)),
122            deleted_at: Set(None),
123            ..Default::default()
124        };
125        let inserted = model.insert(&self.db).await?;
126        Ok(inserted.id)
127    }
128
129    /// 更新特典
130    #[tracing::instrument(skip_all, level = "debug")]
131    pub async fn update(&self, id: i32, input: &BenefitUpsert) -> crate::error::ApiResult<()> {
132        let existing = manga_benefit::Entity::find_by_id(id)
133            .filter(manga_benefit::Column::DeletedAt.is_null())
134            .one(&self.db)
135            .await?
136            .ok_or_else(|| crate::error::AppError::business("要更新的特典不存在"))?;
137        let mut am: manga_benefit::ActiveModel = existing.into();
138        am.manga_id = Set(input.manga_id);
139        am.volume_number = Set(input.volume_number);
140        am.volume_title = Set(input.volume_title.clone());
141        am.store_name = Set(input.store_name.clone());
142        am.benefit_name = Set(input.benefit_name.clone());
143        am.benefit_tag = Set(input.benefit_tag.clone());
144        am.r#type = Set(input.r#type);
145        am.img_url = Set(input.img_url.clone());
146        am.publish_time = Set(input.publish_time.map(|t| t.naive_utc()));
147        am.updated_at = Set(Some(chrono::Utc::now().naive_utc()));
148        am.update(&self.db).await?;
149        Ok(())
150    }
151
152    /// 软删除特典
153    #[tracing::instrument(skip_all, level = "debug")]
154    pub async fn soft_delete(&self, id: i32) -> crate::error::ApiResult<()> {
155        let existing = manga_benefit::Entity::find_by_id(id)
156            .filter(manga_benefit::Column::DeletedAt.is_null())
157            .one(&self.db)
158            .await?
159            .ok_or_else(|| crate::error::AppError::business("特典不存在"))?;
160        let mut am: manga_benefit::ActiveModel = existing.into();
161        am.deleted_at = Set(Some(chrono::Utc::now().naive_utc()));
162        am.updated_at = Set(Some(chrono::Utc::now().naive_utc()));
163        am.update(&self.db).await?;
164        Ok(())
165    }
166}
167
168/// SeaORM Model 映射为特典行
169fn model_to_row(m: manga_benefit::Model) -> BenefitRow {
170    BenefitRow {
171        id: m.id,
172        manga_id: m.manga_id,
173        volume_number: m.volume_number,
174        volume_title: m.volume_title,
175        store_name: m.store_name,
176        benefit_name: m.benefit_name,
177        benefit_tag: m.benefit_tag,
178        img_url: m.img_url,
179        r#type: m.r#type,
180        publish_time: m.publish_time.map(|t| t.and_utc()),
181    }
182}