Compare commits
	
		
			9 Commits
		
	
	
		
			cade85d576
			...
			dev/v1-s
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | f915e72cf5 | ||
|   | 33cb0aaa33 | ||
|   | 891982ede0 | ||
|   | bf2e1f2af6 | ||
|   | 6a5a45bf20 | ||
|   | 9e58839491 | ||
|   | 7e5e9cb32f | ||
|   | a34dbc60c4 | ||
|   | daf6c7c16a | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -4,3 +4,5 @@ | ||||
| .DS_Store | ||||
| .env | ||||
| conf.toml | ||||
| config.toml | ||||
| .fleet\ | ||||
|   | ||||
							
								
								
									
										112
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										112
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -1121,6 +1121,7 @@ dependencies = [ | ||||
|  "jsonwebtoken", | ||||
|  "once_cell", | ||||
|  "pbkdf2", | ||||
|  "rand 0.9.2", | ||||
|  "rand_core 0.9.3", | ||||
|  "sea-orm", | ||||
|  "serde", | ||||
| @@ -1392,9 +1393,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "indexmap" | ||||
| version = "2.9.0" | ||||
| version = "2.11.4" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" | ||||
| checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" | ||||
| dependencies = [ | ||||
|  "equivalent", | ||||
|  "hashbrown 0.15.3", | ||||
| @@ -1616,7 +1617,7 @@ dependencies = [ | ||||
|  "num-integer", | ||||
|  "num-iter", | ||||
|  "num-traits", | ||||
|  "rand", | ||||
|  "rand 0.8.5", | ||||
|  "smallvec", | ||||
|  "zeroize", | ||||
| ] | ||||
| @@ -1993,10 +1994,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
|  "rand_chacha", | ||||
|  "rand_chacha 0.3.1", | ||||
|  "rand_core 0.6.4", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "rand" | ||||
| version = "0.9.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" | ||||
| dependencies = [ | ||||
|  "rand_chacha 0.9.0", | ||||
|  "rand_core 0.9.3", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "rand_chacha" | ||||
| version = "0.3.1" | ||||
| @@ -2007,6 +2018,16 @@ dependencies = [ | ||||
|  "rand_core 0.6.4", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "rand_chacha" | ||||
| version = "0.9.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" | ||||
| dependencies = [ | ||||
|  "ppv-lite86", | ||||
|  "rand_core 0.9.3", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "rand_core" | ||||
| version = "0.6.4" | ||||
| @@ -2160,7 +2181,7 @@ dependencies = [ | ||||
|  "borsh", | ||||
|  "bytes", | ||||
|  "num-traits", | ||||
|  "rand", | ||||
|  "rand 0.8.5", | ||||
|  "rkyv", | ||||
|  "serde", | ||||
|  "serde_json", | ||||
| @@ -2252,9 +2273,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "sea-orm" | ||||
| version = "1.1.12" | ||||
| version = "1.1.16" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "18b7272b88bd608cd846de24f41b74a0315a135fe761b0aed4ec1ce6a6327a93" | ||||
| checksum = "335d87ec8e5c6eb4b2afb866dc53ed57a5cba314af63ce288db83047aa0fed4d" | ||||
| dependencies = [ | ||||
|  "async-stream", | ||||
|  "async-trait", | ||||
| @@ -2299,9 +2320,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "sea-orm-macros" | ||||
| version = "1.1.12" | ||||
| version = "1.1.16" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2c38255a6b2e6d1ae2d5df35696507a345f03c036ae32caeb0a3b922dbab610d" | ||||
| checksum = "68de7a2258410fd5e6ba319a4fe6c4af7811507fc714bbd76534ae6caa60f95f" | ||||
| dependencies = [ | ||||
|  "heck 0.5.0", | ||||
|  "proc-macro2", | ||||
| @@ -2407,18 +2428,28 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" | ||||
|  | ||||
| [[package]] | ||||
| name = "serde" | ||||
| version = "1.0.219" | ||||
| version = "1.0.226" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" | ||||
| checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" | ||||
| dependencies = [ | ||||
|  "serde_core", | ||||
|  "serde_derive", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "serde_core" | ||||
| version = "1.0.226" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" | ||||
| dependencies = [ | ||||
|  "serde_derive", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "serde_derive" | ||||
| version = "1.0.219" | ||||
| version = "1.0.226" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" | ||||
| checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
| @@ -2462,11 +2493,11 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "serde_spanned" | ||||
| version = "0.6.8" | ||||
| version = "1.0.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" | ||||
| checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" | ||||
| dependencies = [ | ||||
|  "serde", | ||||
|  "serde_core", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -2725,7 +2756,7 @@ dependencies = [ | ||||
|  "memchr", | ||||
|  "once_cell", | ||||
|  "percent-encoding", | ||||
|  "rand", | ||||
|  "rand 0.8.5", | ||||
|  "rsa", | ||||
|  "rust_decimal", | ||||
|  "serde", | ||||
| @@ -2769,7 +2800,7 @@ dependencies = [ | ||||
|  "memchr", | ||||
|  "num-bigint", | ||||
|  "once_cell", | ||||
|  "rand", | ||||
|  "rand 0.8.5", | ||||
|  "rust_decimal", | ||||
|  "serde", | ||||
|  "serde_json", | ||||
| @@ -3025,14 +3056,17 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "toml" | ||||
| version = "0.8.22" | ||||
| version = "0.9.7" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" | ||||
| checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" | ||||
| dependencies = [ | ||||
|  "serde", | ||||
|  "indexmap", | ||||
|  "serde_core", | ||||
|  "serde_spanned", | ||||
|  "toml_datetime", | ||||
|  "toml_edit", | ||||
|  "toml_datetime 0.7.2", | ||||
|  "toml_parser", | ||||
|  "toml_writer", | ||||
|  "winnow", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -3040,8 +3074,14 @@ name = "toml_datetime" | ||||
| version = "0.6.9" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" | ||||
|  | ||||
| [[package]] | ||||
| name = "toml_datetime" | ||||
| version = "0.7.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" | ||||
| dependencies = [ | ||||
|  "serde", | ||||
|  "serde_core", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -3051,18 +3091,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" | ||||
| dependencies = [ | ||||
|  "indexmap", | ||||
|  "serde", | ||||
|  "serde_spanned", | ||||
|  "toml_datetime", | ||||
|  "toml_write", | ||||
|  "toml_datetime 0.6.9", | ||||
|  "winnow", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "toml_write" | ||||
| version = "0.1.1" | ||||
| name = "toml_parser" | ||||
| version = "1.0.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" | ||||
| checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" | ||||
| dependencies = [ | ||||
|  "winnow", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "toml_writer" | ||||
| version = "1.0.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" | ||||
|  | ||||
| [[package]] | ||||
| name = "tower" | ||||
| @@ -3628,9 +3674,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" | ||||
|  | ||||
| [[package]] | ||||
| name = "winnow" | ||||
| version = "0.7.10" | ||||
| version = "0.7.13" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" | ||||
| checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" | ||||
| dependencies = [ | ||||
|  "memchr", | ||||
| ] | ||||
|   | ||||
| @@ -11,7 +11,7 @@ members = [".", "migration"] | ||||
| axum = { version = "0.8" } | ||||
| axum-extra = {version = "0.10", features = ["typed-header", "typed-routing"] } | ||||
| axum-macros = "0.5" | ||||
| sea-orm = { version = "1.1.12", features = [ | ||||
| sea-orm = { version = "1.1.16", features = [ | ||||
|     "sqlx-postgres", | ||||
|     "runtime-tokio-rustls", | ||||
|     "macros", | ||||
| @@ -23,7 +23,7 @@ tokio = { version = "1.0", features = ["full"] } | ||||
| tracing = "0.1" | ||||
| tracing-subscriber = { version = "0.3", features = ["env-filter"] } | ||||
| dotenvy = "0.15.7" | ||||
| toml = "0.8.22" | ||||
| toml = "0.9.7" | ||||
| clap = { version = "4.0", features = ["derive"] } | ||||
| pbkdf2 = { version = "0.12", features = ["simple"] } | ||||
| rand_core = { version = "0.9.3", features = ["std"] } | ||||
| @@ -31,3 +31,4 @@ jsonwebtoken = "9" | ||||
| once_cell = "1.21.3" | ||||
| tower-http = {version= "0.6", features = ["trace", "cors"] } | ||||
| tower = "0.5.2" | ||||
| rand = "0.9.2" | ||||
|   | ||||
| @@ -5,6 +5,7 @@ mod m20250525_000002_create_ledger_table_book; | ||||
| mod m20250525_000003_create_ledger_table_tag; | ||||
| mod m20250525_000004_create_ledger_table_account; | ||||
| mod m20250525_000005_create_ledger_table_transaction; | ||||
| mod m20250921_000001_create_ledger_table_transaction_tag_rel; | ||||
|  | ||||
| pub struct Migrator; | ||||
|  | ||||
| @@ -17,6 +18,7 @@ impl MigratorTrait for Migrator { | ||||
|             Box::new(m20250525_000003_create_ledger_table_tag::Migration), | ||||
|             Box::new(m20250525_000004_create_ledger_table_account::Migration), | ||||
|             Box::new(m20250525_000005_create_ledger_table_transaction::Migration), | ||||
|             Box::new(m20250921_000001_create_ledger_table_transaction_tag_rel::Migration), | ||||
|         ] | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,71 @@ | ||||
| use crate::sea_query; | ||||
| use sea_orm_migration::{MigrationName, MigrationTrait, SchemaManager}; | ||||
| use crate::{async_trait, ColumnDef, DbErr, Expr, Iden, Table}; | ||||
|  | ||||
| pub struct Migration; | ||||
|  | ||||
| impl MigrationName for crate::m20250921_000001_create_ledger_table_transaction_tag_rel::Migration { | ||||
|     fn name(&self) -> &str { | ||||
|         "m20250921_000001_create_ledger_table_transaction_tag_rel" // Make sure this matches with the file name | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
| impl MigrationTrait for crate::m20250921_000001_create_ledger_table_transaction_tag_rel::Migration { | ||||
|     async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | ||||
|         manager | ||||
|             .create_table( | ||||
|                 Table::create() | ||||
|                     .table(crate::m20250921_000001_create_ledger_table_transaction_tag_rel::TransactionTagRel::Table) | ||||
|                     .col( | ||||
|                         ColumnDef::new(crate::m20250921_000001_create_ledger_table_transaction_tag_rel::TransactionTagRel::Id) | ||||
|                             .big_integer() | ||||
|                             .not_null() | ||||
|                             .auto_increment() | ||||
|                             .primary_key(), | ||||
|                     ) | ||||
|                     .col(ColumnDef::new(crate::m20250921_000001_create_ledger_table_transaction_tag_rel::TransactionTagRel::Uid).big_integer().not_null()) | ||||
|                     .col(ColumnDef::new(crate::m20250921_000001_create_ledger_table_transaction_tag_rel::TransactionTagRel::TransactionId).big_integer().not_null()) | ||||
|                     .col(ColumnDef::new(crate::m20250921_000001_create_ledger_table_transaction_tag_rel::TransactionTagRel::TagId).big_integer().not_null()) | ||||
|                     .col( | ||||
|                         ColumnDef::new(crate::m20250921_000001_create_ledger_table_transaction_tag_rel::TransactionTagRel::IsDeleted) | ||||
|                             .boolean() | ||||
|                             .default(false) | ||||
|                             .not_null(), | ||||
|                     ) | ||||
|                     .col( | ||||
|                         ColumnDef::new(crate::m20250921_000001_create_ledger_table_transaction_tag_rel::TransactionTagRel::CreatedAt) | ||||
|                             .date_time() | ||||
|                             .default(Expr::current_timestamp()) | ||||
|                             .not_null(), | ||||
|                     ) | ||||
|                     .col( | ||||
|                         ColumnDef::new(crate::m20250921_000001_create_ledger_table_transaction_tag_rel::TransactionTagRel::UpdatedAt) | ||||
|                             .date_time() | ||||
|                             .default(Expr::current_timestamp()) | ||||
|                             .not_null(), | ||||
|                     ) | ||||
|                     .to_owned(), | ||||
|             ) | ||||
|             .await | ||||
|     } | ||||
|  | ||||
|     // Define how to rollback this migration: Drop the Bakery table. | ||||
|     async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | ||||
|         manager | ||||
|             .drop_table(Table::drop().table(crate::m20250921_000001_create_ledger_table_transaction_tag_rel::TransactionTagRel::Table).to_owned()) | ||||
|             .await | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Iden)] | ||||
| pub enum TransactionTagRel { | ||||
|     Table, | ||||
|     Id, | ||||
|     Uid, | ||||
|     TransactionId, | ||||
|     TagId, | ||||
|     IsDeleted, | ||||
|     CreatedAt, | ||||
|     UpdatedAt, | ||||
| } | ||||
							
								
								
									
										62
									
								
								src/api/account.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/api/account.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| use crate::middleware::auth::Claims; | ||||
| use crate::model::db::account::{ | ||||
|     ActiveModel as AccountActiveModel, Column as AccountColumn, Model as AccountModel, | ||||
| }; | ||||
| use crate::model::db::prelude::Account as AccountPrelude; | ||||
| use crate::model::http_body::account::{AccountReq, AccountResp}; | ||||
| use crate::model::http_body::common::SimpleResponse; | ||||
| use crate::AppState; | ||||
| use axum::extract::{Path, State}; | ||||
| use axum::http::StatusCode; | ||||
| use axum::routing::{get, post}; | ||||
| use axum::{Json, Router}; | ||||
| use sea_orm::sqlx::types::chrono::Local; | ||||
| use sea_orm::{ActiveModelTrait, DbErr, Iden, Set}; | ||||
|  | ||||
| pub fn get_nest_handlers() -> Router<crate::AppState> { | ||||
|     Router::new() | ||||
|         .route("/{id}/update", post(update_account_handler)) | ||||
|         .route("/{id}", get(get_account_by_id_handler)) | ||||
|         .route( | ||||
|             "/", | ||||
|             post(create_account_handler).get(get_all_accounts_handler), | ||||
|         ) | ||||
| } | ||||
|  | ||||
| async fn update_account_handler( | ||||
|     Path(id): Path<i64>, | ||||
|     state: State<AppState>, | ||||
|     claims: Claims, | ||||
|     Json(payload): Json<AccountReq>, | ||||
| ) -> Result<Json<SimpleResponse>, (StatusCode, String)> { | ||||
|     let uid: i64 = claims.uid.clone(); | ||||
|     let mut active_model: AccountActiveModel = AccountPrelude::find_by_id(id) | ||||
|         .filter(AccountColumn::Uid.eq(uid)) | ||||
|         .filter(AccountColumn::IsDeleted.eq(false)) | ||||
|         .one(&state.conn) | ||||
|         .await | ||||
|         .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; | ||||
|     match payload.name { | ||||
|         Some(n) => { | ||||
|             active_model.name = Set(n); | ||||
|         } | ||||
|         _ => {} | ||||
|     } | ||||
|  | ||||
|     active_model.updated_at = Set(Local::now().naive_utc()); | ||||
|  | ||||
|     active_model.update(&state.conn).await | ||||
|         .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; | ||||
|  | ||||
|     let resp = SimpleResponse{ | ||||
|         code: 0, | ||||
|         message: "".to_string() | ||||
|     }; | ||||
|     Ok(Json(resp)) | ||||
| } | ||||
|  | ||||
| async fn get_account_by_id_handler() {} | ||||
|  | ||||
| async fn create_account_handler() {} | ||||
|  | ||||
| async fn get_all_accounts_handler() {} | ||||
| @@ -12,7 +12,7 @@ use crate::model::db::book::Column as BookColumn; | ||||
| use crate::model::db::book::Model as BookModel; | ||||
| use crate::model::db::prelude::Book; | ||||
| use crate::model::http_body::book; | ||||
| use crate::model::http_body::book::{BookInfo, BookItem}; | ||||
| use crate::model::http_body::book::{BookInfo, BookResp}; | ||||
| use crate::model::http_body::common::SimpleResponse; | ||||
| use crate::AppState; | ||||
| use sea_orm::sqlx::types::chrono::Local; | ||||
| @@ -32,7 +32,7 @@ pub fn get_nest_handlers() -> Router<crate::AppState> { | ||||
| async fn get_all_books_handler( | ||||
|     state: State<AppState>, | ||||
|     claims: Claims, | ||||
| ) -> Result<Json<Vec<book::BookItem>>, (StatusCode, String)> { | ||||
| ) -> Result<Json<Vec<book::BookResp>>, (StatusCode, String)> { | ||||
|     let uid: i64 = claims.uid.clone(); | ||||
|     let all_books = Book::find() | ||||
|         .filter(BookColumn::Uid.eq(uid)) | ||||
| @@ -40,9 +40,9 @@ async fn get_all_books_handler( | ||||
|         .await | ||||
|         .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; | ||||
|  | ||||
|     let mut books: Vec<BookItem> = Vec::new(); | ||||
|     let mut books: Vec<BookResp> = Vec::new(); | ||||
|     for b in all_books { | ||||
|         let book_resp = BookItem { | ||||
|         let book_resp = BookResp { | ||||
|             id: b.id.into(), | ||||
|             name: b.name, | ||||
|         }; | ||||
| @@ -56,7 +56,7 @@ async fn get_book_by_id_handler( | ||||
|     Path(id): Path<i64>, | ||||
|     state: State<AppState>, | ||||
|     claims: Claims, | ||||
| ) -> Result<Json<BookItem>, (StatusCode, String)> { | ||||
| ) -> Result<Json<BookResp>, (StatusCode, String)> { | ||||
|     let uid: i64 = claims.uid.clone(); | ||||
|     let book_query = Book::find() | ||||
|         .filter(BookColumn::Uid.eq(uid)) | ||||
| @@ -65,10 +65,10 @@ async fn get_book_by_id_handler( | ||||
|         .await | ||||
|         .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; | ||||
|  | ||||
|     let book_resp: BookItem; | ||||
|     let book_resp: BookResp; | ||||
|     match book_query { | ||||
|         Some(b) => { | ||||
|             book_resp = BookItem { | ||||
|             book_resp = BookResp { | ||||
|                 id: b.id.into(), | ||||
|                 name: b.name, | ||||
|             }; | ||||
|   | ||||
							
								
								
									
										198
									
								
								src/api/book_test.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								src/api/book_test.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,198 @@ | ||||
| // #[cfg(test)] | ||||
| // mod tests { | ||||
| //     use super::*; | ||||
| //     use axum::{ | ||||
| //         http::{Request, StatusCode}, | ||||
| //         Router, | ||||
| //         routing::{get, put}, | ||||
| //         body::Body, | ||||
| //     }; | ||||
| //     use sea_orm::{ | ||||
| //         MockDatabase, MockExecResult, DatabaseConnection, DatabaseTransaction, | ||||
| //         entity::prelude::*, | ||||
| //         QueryFilter, Condition, DbErr, EntityTrait, | ||||
| //     }; | ||||
| //     use serde_json::{json, Value}; | ||||
| //     use tower::ServiceExt; | ||||
| //     use std::sync::Arc; | ||||
| // | ||||
| //     // 模拟 Book 实体 | ||||
| //     #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] | ||||
| //     #[sea_orm(table_name = "books")] | ||||
| //     pub struct Model { | ||||
| //         #[sea_orm(primary_key)] | ||||
| //         pub id: i32, | ||||
| //         pub title: String, | ||||
| //         pub author: String, | ||||
| //     } | ||||
| // | ||||
| //     #[derive(Copy, Clone, Debug, EnumIter)] | ||||
| //     pub enum Relation {} | ||||
| // | ||||
| //     impl Related<super::book::Entity> for Entity { | ||||
| //         fn to() -> RelationDef { | ||||
| //             panic!("No relations defined") | ||||
| //         } | ||||
| //     } | ||||
| // | ||||
| //     // 创建测试用的 Router | ||||
| //     async fn setup_router(db: DatabaseConnection) -> Router { | ||||
| //         Router::new() | ||||
| //             .route("/books/:id", get(get_book_by_id).put(update_book_by_id)) | ||||
| //             .route("/books", get(get_all_book)) | ||||
| //             .with_state(Arc::new(db)) | ||||
| //     } | ||||
| // | ||||
| //     // 测试 get_book_by_id | ||||
| //     #[tokio::test] | ||||
| //     async fn test_get_book_by_id() { | ||||
| //         // 设置模拟数据库 | ||||
| //         let db = MockDatabase::new(DatabaseBackend::Postgres) | ||||
| //             .append_query_results(vec![vec![Model { | ||||
| //                 id: 1, | ||||
| //                 title: "Test Book".to_string(), | ||||
| //                 author: "Test Author".to_string(), | ||||
| //             }]]) | ||||
| //             .into_connection(); | ||||
| // | ||||
| //         let app = setup_router(db).await; | ||||
| // | ||||
| //         // 构造请求 | ||||
| //         let request = Request::builder() | ||||
| //             .uri("/books/1") | ||||
| //             .method("GET") | ||||
| //             .body(Body::empty()) | ||||
| //             .unwrap(); | ||||
| // | ||||
| //         // 发送请求 | ||||
| //         let response = app.oneshot(request).await.unwrap(); | ||||
| //         assert_eq!(response.status(), StatusCode::OK); | ||||
| // | ||||
| //         // 解析响应 | ||||
| //         let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); | ||||
| //         let body: Value = serde_json::from_slice(&body).unwrap(); | ||||
| //         assert_eq!( | ||||
| //             body, | ||||
| //             json!({ | ||||
| //                 "id": 1, | ||||
| //                 "title": "Test Book", | ||||
| //                 "author": "Test Author" | ||||
| //             }) | ||||
| //         ); | ||||
| //     } | ||||
| // | ||||
| //     // 测试 get_book_by_id 未找到 | ||||
| //     #[tokio::test] | ||||
| //     async fn test_get_book_by_id_not_found() { | ||||
| //         let db = MockDatabase::new(DatabaseBackend::Postgres) | ||||
| //             .append_query_results(vec![vec![] as Vec<Model>]) | ||||
| //             .into_connection(); | ||||
| // | ||||
| //         let app = setup_router(db).await; | ||||
| // | ||||
| //         let request = Request::builder() | ||||
| //             .uri("/books/999") | ||||
| //             .method("GET") | ||||
| //             .body(Body::empty()) | ||||
| //             .unwrap(); | ||||
| // | ||||
| //         let response = app.oneshot(request).await.unwrap(); | ||||
| //         assert_eq!(response.status(), StatusCode::NOT_FOUND); | ||||
| //     } | ||||
| // | ||||
| //     // 测试 update_book_by_id | ||||
| //     #[tokio::test] | ||||
| //     async fn test_update_book_by_id() { | ||||
| //         let db = MockDatabase::new(DatabaseBackend::Postgres) | ||||
| //             .append_query_results(vec![vec![Model { | ||||
| //                 id: 1, | ||||
| //                 title: "Updated Book".to_string(), | ||||
| //                 author: "Updated Author".to_string(), | ||||
| //             }]]) | ||||
| //             .append_exec_results(vec![MockExecResult { | ||||
| //                 last_insert_id: 1, | ||||
| //                 rows_affected: 1, | ||||
| //             }]) | ||||
| //             .into_connection(); | ||||
| // | ||||
| //         let app = setup_router(db).await; | ||||
| // | ||||
| //         // 构造请求 | ||||
| //         let request = Request::builder() | ||||
| //             .uri("/books/1") | ||||
| //             .method("PUT") | ||||
| //             .header("Content-Type", "application/json") | ||||
| //             .body(Body::from( | ||||
| //                 json!({ | ||||
| //                     "title": "Updated Book", | ||||
| //                     "author": "Updated Author" | ||||
| //                 }) | ||||
| //                     .to_string(), | ||||
| //             )) | ||||
| //             .unwrap(); | ||||
| // | ||||
| //         // 发送请求 | ||||
| //         let response = app.oneshot(request).await.unwrap(); | ||||
| //         assert_eq!(response.status(), StatusCode::OK); | ||||
| // | ||||
| //         // 解析响应 | ||||
| //         let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); | ||||
| //         let body: Value = serde_json::from_slice(&body).unwrap(); | ||||
| //         assert_eq!( | ||||
| //             body, | ||||
| //             json!({ | ||||
| //                 "id": 1, | ||||
| //                 "title": "Updated Book", | ||||
| //                 "author": "Updated Author" | ||||
| //             }) | ||||
| //         ); | ||||
| //     } | ||||
| // | ||||
| //     // 测试 get_all_book | ||||
| //     #[tokio::test] | ||||
| //     async fn test_get_all_book() { | ||||
| //         let db = MockDatabase::new(DatabaseBackend::Postgres) | ||||
| //             .append_query_results(vec![vec![ | ||||
| //                 Model { | ||||
| //                     id: 1, | ||||
| //                     title: "Book 1".to_string(), | ||||
| //                     author: "Author 1".to_string(), | ||||
| //                 }, | ||||
| //                 Model { | ||||
| //                     id: 2, | ||||
| //                     title: "Book 2".to_string(), | ||||
| //                     author: "Author 2".to_string(), | ||||
| //                 }, | ||||
| //             ]]) | ||||
| //             .into_connection(); | ||||
| // | ||||
| //         let app = setup_router(db).await; | ||||
| // | ||||
| //         let request = Request::builder() | ||||
| //             .uri("/books") | ||||
| //             .method("GET") | ||||
| //             .body(Body::empty()) | ||||
| //             .unwrap(); | ||||
| // | ||||
| //         let response = app.oneshot(request).await.unwrap(); | ||||
| //         assert_eq!(response.status(), StatusCode::OK); | ||||
| // | ||||
| //         let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); | ||||
| //         let body: Value = serde_json::from_slice(&body).unwrap(); | ||||
| //         assert_eq!( | ||||
| //             body, | ||||
| //             json!([ | ||||
| //                 { | ||||
| //                     "id": 1, | ||||
| //                     "title": "Book 1", | ||||
| //                     "author": "Author 1" | ||||
| //                 }, | ||||
| //                 { | ||||
| //                     "id": 2, | ||||
| //                     "title": "Book 2", | ||||
| //                     "author": "Author 2" | ||||
| //                 } | ||||
| //             ]) | ||||
| //         ); | ||||
| //     } | ||||
| // } | ||||
| @@ -1,3 +1,5 @@ | ||||
| pub mod book; | ||||
| pub mod category; | ||||
| pub mod tag; | ||||
| pub mod transaction; | ||||
| pub mod account; | ||||
| @@ -11,7 +11,7 @@ use crate::model::db::tag::ActiveModel as TagActiveModel; | ||||
| use crate::model::db::tag::Column as TagColumn; | ||||
| use crate::model::db::tag::Model as TagModel; | ||||
| use crate::model::db::prelude::Tag; | ||||
| use crate::model::http_body::tag::{TagInfo, TagItem}; | ||||
| use crate::model::http_body::tag::{TagInfo, TagResp}; | ||||
| use crate::model::http_body::common::SimpleResponse; | ||||
| use crate::AppState; | ||||
| use sea_orm::sqlx::types::chrono::Local; | ||||
| @@ -31,7 +31,7 @@ pub fn get_nest_handlers() -> Router<crate::AppState> { | ||||
| async fn get_all_tags( | ||||
|     state: State<AppState>, | ||||
|     claims: Claims, | ||||
| ) -> Result<Json<Vec<TagItem>>, (StatusCode, String)> { | ||||
| ) -> Result<Json<Vec<TagResp>>, (StatusCode, String)> { | ||||
|     let uid: i64 = claims.uid.clone(); | ||||
|     let all_tags = Tag::find() | ||||
|         .filter(TagColumn::Uid.eq(uid)) | ||||
| @@ -39,9 +39,9 @@ async fn get_all_tags( | ||||
|         .await | ||||
|         .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; | ||||
|  | ||||
|     let mut tags: Vec<TagItem> = Vec::new(); | ||||
|     let mut tags: Vec<TagResp> = Vec::new(); | ||||
|     for b in all_tags { | ||||
|         let tag_resp = TagItem { | ||||
|         let tag_resp = TagResp { | ||||
|             id: b.id.into(), | ||||
|             name: b.name, | ||||
|         }; | ||||
| @@ -55,7 +55,7 @@ async fn get_tag_by_id( | ||||
|     Path(id): Path<i64>, | ||||
|     state: State<AppState>, | ||||
|     claims: Claims, | ||||
| ) -> Result<Json<TagItem>, (StatusCode, String)> { | ||||
| ) -> Result<Json<TagResp>, (StatusCode, String)> { | ||||
|     let uid: i64 = claims.uid.clone(); | ||||
|     let tag_query = Tag::find() | ||||
|         .filter(TagColumn::Uid.eq(uid)) | ||||
| @@ -64,10 +64,10 @@ async fn get_tag_by_id( | ||||
|         .await | ||||
|         .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; | ||||
|  | ||||
|     let tag_resp: TagItem; | ||||
|     let tag_resp: TagResp; | ||||
|     match tag_query { | ||||
|         Some(b) => { | ||||
|             tag_resp = TagItem { | ||||
|             tag_resp = TagResp { | ||||
|                 id: b.id.into(), | ||||
|                 name: b.name, | ||||
|             }; | ||||
|   | ||||
							
								
								
									
										217
									
								
								src/api/transaction.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								src/api/transaction.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,217 @@ | ||||
| use crate::middleware::auth::Claims; | ||||
| use crate::model::db::category::Column as CategoryColumn; | ||||
| use crate::model::db::prelude::Category as CategoryPrelude; | ||||
| use crate::model::db::prelude::Transaction; | ||||
| use crate::model::db::transaction::{ | ||||
|     ActiveModel as TransactionActiveModel, Column as TransactionColumn, Model as TransactionModel, | ||||
| }; | ||||
| use crate::model::http_body; | ||||
| use crate::model::http_body::book::BookInfo; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use crate::model::db::prelude::Tag as TagPrelude; | ||||
| use crate::model::db::tag::{ | ||||
|     ActiveModel as TagActiveModel, Column as TagColumn, Model as TagModel, | ||||
| }; | ||||
| use crate::model::http_body::category::CategoryResp; | ||||
| use crate::model::http_body::common::SimpleResponse; | ||||
| use crate::model::http_body::transaction::{TransactionReq, TransactionResp}; | ||||
| use crate::AppState; | ||||
| use axum::extract::{Path, State}; | ||||
| use axum::http::StatusCode; | ||||
| use axum::routing::{get, post}; | ||||
| use axum::{Json, Router}; | ||||
| use axum_macros::debug_handler; | ||||
| use sea_orm::sqlx::types::chrono::Local; | ||||
| use sea_orm::{ColumnTrait, DatabaseConnection}; | ||||
| use sea_orm::QueryFilter; | ||||
| use sea_orm::{entity::*, query::*}; | ||||
| use serde_json::error::Category; | ||||
| use std::ptr::null; | ||||
|  | ||||
| pub fn get_nest_handlers() -> Router<crate::AppState> { | ||||
|     Router::new() | ||||
|         .route("/{id}/update", post(update_transaction_handler)) | ||||
|         .route("/{id}", get(get_transaction_by_id_handler)) | ||||
|         .route( | ||||
|             "/", | ||||
|             post(create_transaction_handler).get(get_all_transactions_handler), | ||||
|         ) | ||||
| } | ||||
|  | ||||
| async fn update_transaction_handler( | ||||
|     Path(id): Path<i64>, | ||||
|     state: State<AppState>, | ||||
|     claims: Claims, | ||||
|     Json(payload): Json<TransactionReq>, | ||||
| ) -> Result<Json<SimpleResponse>, (StatusCode, String)> { | ||||
|     let uid: i64 = claims.uid; | ||||
|     let exist_transaction = Transaction::find() | ||||
|         .filter(TransactionColumn::Id.eq(id)) | ||||
|         .filter(TransactionColumn::Uid.eq(uid)) | ||||
|         .one(&state.conn) | ||||
|         .await; | ||||
|     let mut resp = SimpleResponse { | ||||
|         code: 0, | ||||
|         message: "".to_string(), | ||||
|     }; | ||||
|     let transaction: TransactionModel; | ||||
|     match exist_transaction { | ||||
|         Err(_) => { | ||||
|             resp.code = 1; | ||||
|             return Err(( | ||||
|                 StatusCode::INTERNAL_SERVER_ERROR, | ||||
|                 "connection_error".to_string(), | ||||
|             )); | ||||
|         } | ||||
|         Ok(tra) => match tra { | ||||
|             Some(tr) => { | ||||
|                 transaction = tr; | ||||
|             } | ||||
|             _ => return Err((StatusCode::NOT_FOUND, "Transaction not found".to_string())), | ||||
|         }, | ||||
|     } | ||||
|     let mut tr_active: TransactionActiveModel = transaction.into(); | ||||
|     match payload.description { | ||||
|         None => {} | ||||
|         Some(input_desc) => { | ||||
|             tr_active.description = Set(input_desc); | ||||
|         } | ||||
|     } | ||||
|     // TODO category | ||||
|     let new_category_id: i64 = match payload.category_id { | ||||
|         None => { | ||||
|             return Err(( | ||||
|                 StatusCode::BAD_REQUEST, | ||||
|                 "category_id is not valid".to_string(), | ||||
|             )) | ||||
|         } | ||||
|         Some(cid_string) => match cid_string.parse::<i64>() { | ||||
|             Ok(cid) => cid, | ||||
|             Err(_) => { | ||||
|                 return Err(( | ||||
|                     StatusCode::BAD_REQUEST, | ||||
|                     "category_id is not valid".to_string(), | ||||
|                 )) | ||||
|             } | ||||
|         }, | ||||
|     }; | ||||
|     let new_category_id_exist = CategoryPrelude::find() | ||||
|         .filter(CategoryColumn::Id.eq(new_category_id)) | ||||
|         .filter(CategoryColumn::Uid.eq(uid)) | ||||
|         .all(&state.conn) | ||||
|         .await; | ||||
|     match new_category_id_exist { | ||||
|         Ok(_) => {} | ||||
|         Err(_) => { | ||||
|             return Err((StatusCode::NOT_FOUND, "category_id not found".to_string())); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // TODO tags | ||||
|     let tag_exist = check_tags_exist(&state.conn, payload.tags).await; | ||||
|     let all_tag_exist: bool; | ||||
|     match tag_exist { | ||||
|         Ok(tag_res) => { | ||||
|             all_tag_exist = tag_res.values().all(|&exists| exists); | ||||
|         } | ||||
|         Err(_) => { | ||||
|             return Err((StatusCode::NOT_FOUND, "tag not found".to_string())); | ||||
|         } | ||||
|     } | ||||
|     if !all_tag_exist { | ||||
|         return Err((StatusCode::NOT_FOUND, "tag not found".to_string())); | ||||
|     } | ||||
|  | ||||
|     // TODO amounts | ||||
|  | ||||
|     // Update | ||||
|     tr_active.updated_at = Set(Local::now().naive_utc()); | ||||
|     let update_res = tr_active.update(&state.conn).await; | ||||
|     match update_res { | ||||
|         Ok(_) => { | ||||
|             resp.code = 0; | ||||
|         } | ||||
|         Err(_) => { | ||||
|             return Err(( | ||||
|                 StatusCode::INTERNAL_SERVER_ERROR, | ||||
|                 "transaction_update_failed".to_string(), | ||||
|             )); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(Json(resp)) | ||||
| } | ||||
|  | ||||
| async fn create_transaction_handler() {} | ||||
|  | ||||
| async fn get_transaction_by_id_handler( | ||||
|     Path(id): Path<i64>, | ||||
|     state: State<AppState>, | ||||
|     claims: Claims, | ||||
| ) -> Result<Json<TransactionResp>, (StatusCode, String)> { | ||||
|     let uid: i64 = claims.uid.clone(); | ||||
|     let transaction_query = Transaction::find() | ||||
|         .filter(TransactionColumn::Uid.eq(id)) | ||||
|         .filter(TransactionColumn::Id.eq(id)) | ||||
|         .one(&state.conn) | ||||
|         .await | ||||
|         .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; | ||||
|  | ||||
|     let response: TransactionResp; | ||||
|     match transaction_query { | ||||
|         None => { | ||||
|             return Err((StatusCode::NOT_FOUND, "Transaction not found".to_string())); | ||||
|         } | ||||
|         Some(x) => { | ||||
|             response = TransactionResp { | ||||
|                 id: x.id, | ||||
|                 description: x.description, | ||||
|                 category: CategoryResp { | ||||
|                     id: 0, | ||||
|                     name: "".to_string(), | ||||
|                     parent_id: 0, | ||||
|                 }, | ||||
|                 tags: vec![], | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     Ok(Json(response)) | ||||
| } | ||||
|  | ||||
| async fn get_all_transactions_handler() {} | ||||
|  | ||||
| // 批量检查 TagModel 是否存在 | ||||
| async fn check_tags_exist( | ||||
|     connection: &DatabaseConnection, | ||||
|     ids: Vec<String>, | ||||
| ) -> Result<HashMap<i64, bool>, String> { | ||||
|     // 将 Vec<String> 转换为 Vec<i64>,并处理可能的转换错误 | ||||
|     let ids_i64: Vec<i64> = ids | ||||
|         .into_iter() | ||||
|         .filter_map(|id| id.parse::<i64>().ok()) | ||||
|         .collect(); | ||||
|  | ||||
|     if ids_i64.is_empty() { | ||||
|         return Ok(HashMap::new()); | ||||
|     } | ||||
|  | ||||
|     // 构建 IN 查询条件 | ||||
|     let condition = Condition::any().add(TagColumn::Id.is_in(ids_i64.clone())); | ||||
|  | ||||
|     // 执行批量查询,获取存在的 TagModel | ||||
|     let found_tags = TagPrelude::find() | ||||
|         .filter(condition) | ||||
|         .all(connection) | ||||
|         .await | ||||
|         .map_err(|e| format!("Database error: {}", e))?; | ||||
|  | ||||
|     // 创建 HashMap 记录每个 ID 是否存在 | ||||
|     let mut result = HashMap::new(); | ||||
|     for id in ids_i64 { | ||||
|         result.insert(id, found_tags.iter().any(|tag| tag.id == id)); | ||||
|     } | ||||
|  | ||||
|     Ok(result) | ||||
| } | ||||
							
								
								
									
										1
									
								
								src/dal/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/dal/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| pub mod transaction; | ||||
							
								
								
									
										0
									
								
								src/dal/transaction.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/dal/transaction.rs
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										1
									
								
								src/dao/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/dao/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| pub mod transaction_tag_rel; | ||||
							
								
								
									
										130
									
								
								src/dao/transaction_tag_rel.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								src/dao/transaction_tag_rel.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| use sea_orm::{DatabaseConnection, EntityTrait, ActiveModelTrait, Set, QueryFilter, ColumnTrait, DbErr}; | ||||
| use sea_orm::sqlx::types::chrono::{Local}; | ||||
| use crate::model::db::prelude::TransactionTagRel as TransactionTagRelPrelude; | ||||
| use crate::model::db::transaction_tag_rel::{ | ||||
|     ActiveModel as TransactionTagRelActiveModel, | ||||
|     Model as TransactionTagRelModel, | ||||
|     Column as TransactionTagRelColumn, | ||||
| }; | ||||
|  | ||||
| // DAO struct for TransactionTagRel | ||||
| pub struct TransactionTagRelDAO { | ||||
|     db: DatabaseConnection, | ||||
| }  | ||||
|  | ||||
| impl TransactionTagRelDAO { | ||||
|     // Constructor | ||||
|     pub fn new(db: DatabaseConnection) -> Self { | ||||
|         Self { db } | ||||
|     } | ||||
|  | ||||
|     // Create a new TransactionTagRel | ||||
|     pub async fn create( | ||||
|         &self, | ||||
|         uid: i64, | ||||
|         transaction_id: i64, | ||||
|         tag_id: i64, | ||||
|     ) -> Result<TransactionTagRelModel, DbErr> { | ||||
|         let active_model = TransactionTagRelActiveModel { | ||||
|             uid: Set(uid), | ||||
|             transaction_id: Set(transaction_id), | ||||
|             tag_id: Set(tag_id), | ||||
|             is_deleted: Set(false), | ||||
|             // created_at: Set(Utc::now()), | ||||
|             // updated_at: Set(Utc::now()), | ||||
|             ..Default::default() // id is auto-incremented | ||||
|         }; | ||||
|  | ||||
|         active_model.insert(&self.db).await | ||||
|     } | ||||
|  | ||||
|     // Find by ID | ||||
|     pub async fn find_by_id(&self, id: i64, uid:i64) -> Result<Option<TransactionTagRelModel>, DbErr> { | ||||
|         TransactionTagRelPrelude::find_by_id(id) | ||||
|             .filter(TransactionTagRelColumn::Uid.eq(uid)) | ||||
|             .filter(TransactionTagRelColumn::IsDeleted.eq(false)) | ||||
|             .one(&self.db) | ||||
|             .await | ||||
|     } | ||||
|  | ||||
|     // Find all by transaction_id | ||||
|     pub async fn find_by_transaction_id( | ||||
|         &self, | ||||
|         transaction_id: i64, | ||||
|         uid: i64, | ||||
|     ) -> Result<Vec<TransactionTagRelModel>, DbErr> { | ||||
|         TransactionTagRelPrelude::find() | ||||
|             .filter(TransactionTagRelColumn::Uid.eq(uid)) | ||||
|             .filter(TransactionTagRelColumn::TransactionId.eq(transaction_id)) | ||||
|             .filter(TransactionTagRelColumn::IsDeleted.eq(false)) | ||||
|             .all(&self.db) | ||||
|             .await | ||||
|     } | ||||
|  | ||||
|     // Find all by tag_id | ||||
|     pub async fn find_by_tag_id( | ||||
|         &self, | ||||
|         tag_id: i64, | ||||
|         uid: i64, | ||||
|     ) -> Result<Vec<TransactionTagRelModel>, DbErr> { | ||||
|         TransactionTagRelPrelude::find() | ||||
|             .filter(TransactionTagRelColumn::Uid.eq(uid)) | ||||
|             .filter(TransactionTagRelColumn::TagId.eq(tag_id)) | ||||
|             .filter(TransactionTagRelColumn::IsDeleted.eq(false)) | ||||
|             .all(&self.db) | ||||
|             .await | ||||
|     } | ||||
|  | ||||
|     // Update a TransactionTagRel | ||||
|     pub async fn update( | ||||
|         &self, | ||||
|         id: i64, | ||||
|         uid: Option<i64>, | ||||
|         transaction_id: Option<i64>, | ||||
|         tag_id: Option<i64>, | ||||
|     ) -> Result<TransactionTagRelModel, DbErr> { | ||||
|         let mut active_model: TransactionTagRelActiveModel = TransactionTagRelPrelude::find_by_id(id) | ||||
|             .filter(TransactionTagRelColumn::Uid.eq(uid)) | ||||
|             .filter(TransactionTagRelColumn::IsDeleted.eq(false)) | ||||
|             .one(&self.db) | ||||
|             .await? | ||||
|             .ok_or(DbErr::RecordNotFound("TransactionTagRel not found".into()))? | ||||
|             .into(); | ||||
|  | ||||
|         if let Some(uid) = uid { | ||||
|             active_model.uid = Set(uid); | ||||
|         } | ||||
|         if let Some(transaction_id) = transaction_id { | ||||
|             active_model.transaction_id = Set(transaction_id); | ||||
|         } | ||||
|         if let Some(tag_id) = tag_id { | ||||
|             active_model.tag_id = Set(tag_id); | ||||
|         } | ||||
|         active_model.updated_at = Set(Local::now().naive_utc()); | ||||
|  | ||||
|         active_model.update(&self.db).await | ||||
|     } | ||||
|  | ||||
|     // Soft delete (set is_deleted = true) | ||||
|     pub async fn soft_delete(&self, id: i64, uid: i64) -> Result<(), DbErr> { | ||||
|         let mut active_model: TransactionTagRelActiveModel = TransactionTagRelPrelude::find_by_id(id) | ||||
|             .filter(TransactionTagRelColumn::Uid.eq(uid)) | ||||
|             .filter(TransactionTagRelColumn::IsDeleted.eq(false)) | ||||
|             .one(&self.db) | ||||
|             .await? | ||||
|             .ok_or(DbErr::RecordNotFound("TransactionTagRel not found".into()))? | ||||
|             .into(); | ||||
|  | ||||
|         active_model.is_deleted = Set(true); | ||||
|         active_model.updated_at = Set(Local::now().naive_utc()); | ||||
|         active_model.update(&self.db).await?; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     // Hard delete (optional, use with caution) | ||||
|     pub async fn hard_delete(&self, id: i64, uid: i64) -> Result<(), DbErr> { | ||||
|         TransactionTagRelPrelude::delete_by_id(id) | ||||
|             .exec(&self.db).await?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										40
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -1,8 +1,8 @@ | ||||
| use crate::middleware::auth; | ||||
| use axum::{http::Method, Router}; | ||||
| use clap::Parser; | ||||
| use sea_orm::{Database, DatabaseConnection}; | ||||
| use serde::Deserialize; | ||||
| use sea_orm::{Database, DatabaseConnection, Iden}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use tower::ServiceBuilder; | ||||
| use tower_http::cors::{Any, CorsLayer}; | ||||
| use tower_http::trace::TraceLayer; | ||||
| @@ -15,10 +15,12 @@ mod middleware; | ||||
| mod model; | ||||
| mod util; | ||||
| mod query; | ||||
| mod dao; | ||||
| mod dal; | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|     dotenvy::dotenv().unwrap(); | ||||
|     // dotenvy::dotenv().unwrap(); | ||||
|     // initialize tracing | ||||
|     tracing_subscriber::registry() | ||||
|         .with(tracing_subscriber::fmt::layer()) | ||||
| @@ -35,6 +37,9 @@ async fn main() { | ||||
|                 eprintln!("Failed to load config from {}", config_path); | ||||
|             } | ||||
|         } | ||||
|         Command::PrintExampleConfig {}=>{ | ||||
|             print_default_config().await; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -43,23 +48,23 @@ struct AppState { | ||||
|     conn: DatabaseConnection, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[derive(Deserialize,Serialize)] | ||||
| struct Key { | ||||
|     jwt: String, | ||||
|     user: String, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[derive(Deserialize,Serialize)] | ||||
| struct DatabaseConf { | ||||
|     connection: String, | ||||
| } | ||||
| #[derive(Deserialize)] | ||||
| #[derive(Deserialize,Serialize)] | ||||
| struct ServiceConf { | ||||
|     host: String, | ||||
|     port: u32, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[derive(Deserialize,Serialize)] | ||||
| struct Config { | ||||
|     service: ServiceConf, | ||||
|     database: DatabaseConf, | ||||
| @@ -78,6 +83,7 @@ enum Command { | ||||
|         #[arg(long = "conf")] | ||||
|         config_path: String, | ||||
|     }, | ||||
|     PrintExampleConfig {}, | ||||
| } | ||||
| async fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> { | ||||
|     let content = tokio::fs::read_to_string(path).await?; | ||||
| @@ -108,6 +114,7 @@ async fn start_server(config: &Config) { | ||||
|         .nest("/api/v1/book", api::book::get_nest_handlers()) | ||||
|         .nest("/api/v1/category", api::category::get_nested_handlers()) | ||||
|         .nest("/api/v1/tag", api::tag::get_nest_handlers()) | ||||
|         .nest("/api/v1/transaction", api::transaction::get_nest_handlers()) | ||||
|         .with_state(state) | ||||
|         .layer(global_layer); | ||||
|     let host = config.service.host.clone(); | ||||
| @@ -118,3 +125,22 @@ async fn start_server(config: &Config) { | ||||
|         .await | ||||
|         .expect("Service panic happened"); | ||||
| } | ||||
|  | ||||
| async fn print_default_config() { | ||||
|     let example_conf = Config{ | ||||
|         service: ServiceConf { | ||||
|             host: "localhost".to_string(), | ||||
|             port: 8080, | ||||
|         }, | ||||
|         database: DatabaseConf { | ||||
|             connection: "postgres://postgres:postgres@localhost/test_db".to_string(), | ||||
|         }, | ||||
|         keys: Key { | ||||
|             jwt: "THIS_IS_TEST_CONFIG".to_string(), | ||||
|             user: "test_user".to_string(), | ||||
|         }, | ||||
|     }; | ||||
|     // 序列化为 TOML 字符串 | ||||
|     let toml_string = toml::to_string(&example_conf); | ||||
|     println!("#This is an example config.\n{}", toml_string.unwrap()); | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11 | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.16 | ||||
|  | ||||
| use sea_orm::entity::prelude::*; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| @@ -9,7 +9,7 @@ pub struct Model { | ||||
|     #[sea_orm(primary_key)] | ||||
|     pub id: i64, | ||||
|     pub name: String, | ||||
|     pub r#type: i32, | ||||
|     pub account_type: i32, | ||||
|     pub uid: i64, | ||||
|     pub is_deleted: bool, | ||||
|     pub created_at: DateTime, | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11 | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.16 | ||||
|  | ||||
| use sea_orm::entity::prelude::*; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11 | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.16 | ||||
|  | ||||
| use sea_orm::entity::prelude::*; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11 | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.16 | ||||
|  | ||||
| pub mod prelude; | ||||
|  | ||||
| @@ -7,3 +7,4 @@ pub mod book; | ||||
| pub mod category; | ||||
| pub mod tag; | ||||
| pub mod transaction; | ||||
| pub mod transaction_tag_rel; | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11 | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.16 | ||||
|  | ||||
| pub use super::account::Entity as Account; | ||||
| pub use super::book::Entity as Book; | ||||
| pub use super::category::Entity as Category; | ||||
| pub use super::tag::Entity as Tag; | ||||
| pub use super::transaction::Entity as Transaction; | ||||
| pub use super::transaction_tag_rel::Entity as TransactionTagRel; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11 | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.16 | ||||
|  | ||||
| use sea_orm::entity::prelude::*; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11 | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.16 | ||||
|  | ||||
| use sea_orm::entity::prelude::*; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| @@ -9,7 +9,7 @@ pub struct Model { | ||||
|     #[sea_orm(primary_key)] | ||||
|     pub id: i64, | ||||
|     pub uid: i64, | ||||
|     pub r#type: i32, | ||||
|     pub transaction_type: i32, | ||||
|     pub book_id: i64, | ||||
|     pub category_id: i64, | ||||
|     pub description: String, | ||||
|   | ||||
							
								
								
									
										22
									
								
								src/model/db/transaction_tag_rel.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/model/db/transaction_tag_rel.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.16 | ||||
|  | ||||
| use sea_orm::entity::prelude::*; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] | ||||
| #[sea_orm(table_name = "transaction_tag_rel")] | ||||
| pub struct Model { | ||||
|     #[sea_orm(primary_key)] | ||||
|     pub id: i64, | ||||
|     pub uid: i64, | ||||
|     pub transaction_id: i64, | ||||
|     pub tag_id: i64, | ||||
|     pub is_deleted: bool, | ||||
|     pub created_at: DateTime, | ||||
|     pub updated_at: DateTime, | ||||
| } | ||||
|  | ||||
| #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | ||||
| pub enum Relation {} | ||||
|  | ||||
| impl ActiveModelBehavior for ActiveModel {} | ||||
							
								
								
									
										14
									
								
								src/model/http_body/account.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/model/http_body/account.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| use serde::{Serialize, Deserialize}; | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| pub struct AccountResp { | ||||
|     pub id: String, | ||||
|     pub name: String, | ||||
|     pub account_type: String, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Serialize, Debug)] | ||||
| pub struct AccountReq { | ||||
|     pub name: Option<String>, | ||||
|     pub account_type: Option<String>, | ||||
| } | ||||
| @@ -2,7 +2,7 @@ use serde::{Serialize, Deserialize}; | ||||
| use super::common::{number_stringify, OptionalI64}; | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| pub struct BookItem { | ||||
| pub struct BookResp { | ||||
|     #[serde(with="number_stringify")] | ||||
|     pub id: OptionalI64, | ||||
|     pub name: String, | ||||
|   | ||||
| @@ -1,5 +1,12 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use super::common::{number_stringify, OptionalI64}; | ||||
| #[derive(Serialize, Deserialize)] | ||||
| pub struct CategoryResp { | ||||
|     pub id: i64, | ||||
|     pub name: String, | ||||
|     pub parent_id: i64, | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Deserialize)] | ||||
| pub struct CategoryInfo { | ||||
|     #[serde(with="number_stringify")] | ||||
|   | ||||
| @@ -2,3 +2,5 @@ pub mod book; | ||||
| pub mod common; | ||||
| pub mod category; | ||||
| pub mod tag; | ||||
| pub mod transaction; | ||||
| pub mod account; | ||||
|   | ||||
| @@ -2,7 +2,7 @@ use serde::{Serialize, Deserialize}; | ||||
| use super::common::{number_stringify, OptionalI64}; | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| pub struct TagItem { | ||||
| pub struct TagResp { | ||||
|     #[serde(with="number_stringify")] | ||||
|     pub id: OptionalI64, | ||||
|     pub name: String, | ||||
|   | ||||
							
								
								
									
										25
									
								
								src/model/http_body/transaction.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/model/http_body/transaction.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
|  use serde::{Serialize, Deserialize}; | ||||
|  use crate::model::http_body::tag::TagInfo; | ||||
|  use crate::model::http_body::category::CategoryResp; | ||||
|  use crate::model::http_body::common::OptionalI64; | ||||
|  | ||||
|  #[derive(Serialize,Deserialize)] | ||||
|  pub struct TransactionResp { | ||||
|      pub id: i64, | ||||
|      pub description: String, | ||||
|      pub category: CategoryResp, | ||||
|      pub tags: Vec<TagInfo>, | ||||
|  } | ||||
|  | ||||
|  #[derive(Serialize,Deserialize)] | ||||
|  pub struct TransactionReq { | ||||
|      pub id: Option<String>, | ||||
|      pub description: Option<String>, | ||||
|      pub category_id: Option<String>, | ||||
|      pub tags: Vec<String>, | ||||
|  } | ||||
|   | ||||
|  pub struct TransactionAmountReq { | ||||
|      pub id: Option<String>, | ||||
|      pub account_id: Option<String>, | ||||
|  } | ||||
		Reference in New Issue
	
	Block a user