init
This commit is contained in:
66
src/auth.rs
Normal file
66
src/auth.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use bcrypt::{hash, verify, DEFAULT_COST};
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
|
||||
// JWT Claims 结构体
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub sub: String, // 用户名
|
||||
pub user_id: i32,
|
||||
pub exp: usize, // 过期时间
|
||||
}
|
||||
|
||||
// 密码加密
|
||||
pub fn hash_password(password: &str) -> Result<String, bcrypt::BcryptError> {
|
||||
hash(password, DEFAULT_COST)
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
pub fn verify_password(password: &str, hash: &str) -> Result<bool, bcrypt::BcryptError> {
|
||||
verify(password, hash)
|
||||
}
|
||||
|
||||
// 生成JWT令牌
|
||||
pub fn generate_token(user_id: i32, username: &str) -> Result<String, jsonwebtoken::errors::Error> {
|
||||
let secret = env::var("JWT_SECRET").unwrap_or_else(|_| "default_secret_key".to_string());
|
||||
|
||||
let expiration = chrono::Utc::now()
|
||||
.checked_add_signed(chrono::Duration::hours(24))
|
||||
.expect("valid timestamp")
|
||||
.timestamp() as usize;
|
||||
|
||||
let claims = Claims {
|
||||
sub: username.to_string(),
|
||||
user_id,
|
||||
exp: expiration,
|
||||
};
|
||||
|
||||
encode(
|
||||
&Header::default(),
|
||||
&claims,
|
||||
&EncodingKey::from_secret(secret.as_ref()),
|
||||
)
|
||||
}
|
||||
|
||||
// 验证JWT令牌
|
||||
pub fn verify_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
|
||||
let secret = env::var("JWT_SECRET").unwrap_or_else(|_| "default_secret_key".to_string());
|
||||
|
||||
let token_data = decode::<Claims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(secret.as_ref()),
|
||||
&Validation::default(),
|
||||
)?;
|
||||
|
||||
Ok(token_data.claims)
|
||||
}
|
||||
|
||||
// 从Authorization头部提取令牌
|
||||
pub fn extract_token_from_header(auth_header: &str) -> Option<&str> {
|
||||
if auth_header.starts_with("Bearer ") {
|
||||
Some(&auth_header[7..])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
26
src/bin/debug_password.rs
Normal file
26
src/bin/debug_password.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use bcrypt::{hash, verify, DEFAULT_COST};
|
||||
|
||||
fn main() {
|
||||
let password = "admin123";
|
||||
|
||||
// 生成新的哈希
|
||||
match hash(password, DEFAULT_COST) {
|
||||
Ok(new_hash) => {
|
||||
println!("新生成的哈希: {}", new_hash);
|
||||
|
||||
// 验证新哈希
|
||||
match verify(password, &new_hash) {
|
||||
Ok(is_valid) => println!("新哈希验证结果: {}", is_valid),
|
||||
Err(e) => println!("新哈希验证错误: {}", e),
|
||||
}
|
||||
},
|
||||
Err(e) => println!("生成哈希错误: {}", e),
|
||||
}
|
||||
|
||||
// 测试数据库中的哈希
|
||||
let db_hash = "$2b$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi";
|
||||
match verify(password, db_hash) {
|
||||
Ok(is_valid) => println!("数据库哈希验证结果: {}", is_valid),
|
||||
Err(e) => println!("数据库哈希验证错误: {}", e),
|
||||
}
|
||||
}
|
||||
102
src/database.rs
Normal file
102
src/database.rs
Normal file
@ -0,0 +1,102 @@
|
||||
use sea_orm::*;
|
||||
use std::env;
|
||||
|
||||
pub async fn init() -> DatabaseConnection {
|
||||
let mut db_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
|
||||
// 确保数据库连接URL包含字符集参数
|
||||
if !db_url.contains("charset") {
|
||||
if db_url.contains("?") {
|
||||
db_url.push_str("&charset=utf8mb4");
|
||||
} else {
|
||||
db_url.push_str("?charset=utf8mb4");
|
||||
}
|
||||
}
|
||||
|
||||
let mut opt = ConnectOptions::new(db_url);
|
||||
opt.max_connections(100)
|
||||
.min_connections(5)
|
||||
.acquire_timeout(std::time::Duration::from_secs(8))
|
||||
.idle_timeout(std::time::Duration::from_secs(8))
|
||||
.max_lifetime(std::time::Duration::from_secs(8))
|
||||
.sqlx_logging(true);
|
||||
|
||||
// 尝试连接数据库,如果失败则使用空连接
|
||||
let db = match Database::connect(opt).await {
|
||||
Ok(db) => {
|
||||
tracing::info!("Database connected successfully");
|
||||
// 设置数据库字符集
|
||||
set_charset(&db).await;
|
||||
// Run migrations
|
||||
run_migrations(&db).await;
|
||||
db
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to connect to database: {}", e);
|
||||
// 创建一个模拟的数据库连接用于测试
|
||||
panic!("Database connection failed: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
async fn set_charset(db: &DatabaseConnection) {
|
||||
// 设置连接字符集为UTF-8
|
||||
let charset_sqls = vec![
|
||||
"SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci",
|
||||
"SET character_set_client = utf8mb4",
|
||||
"SET character_set_connection = utf8mb4",
|
||||
"SET character_set_results = utf8mb4",
|
||||
"SET collation_connection = utf8mb4_unicode_ci"
|
||||
];
|
||||
|
||||
for sql in charset_sqls {
|
||||
match db.execute(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::MySql,
|
||||
sql.to_owned()
|
||||
)).await {
|
||||
Ok(_) => tracing::info!("Executed charset SQL: {}", sql),
|
||||
Err(e) => tracing::error!("Failed to execute charset SQL {}: {}", sql, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_migrations(db: &DatabaseConnection) {
|
||||
// 首先移除邮箱的唯一约束(如果存在)
|
||||
let drop_unique_sql = r#"
|
||||
ALTER TABLE `attendees` DROP INDEX `email`;
|
||||
"#;
|
||||
|
||||
// 尝试删除唯一约束,如果不存在则忽略错误
|
||||
let _ = db.execute(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::MySql,
|
||||
drop_unique_sql.to_owned()
|
||||
)).await;
|
||||
|
||||
let sql = r#"
|
||||
CREATE TABLE IF NOT EXISTS `attendees` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(100) NOT NULL,
|
||||
`email` VARCHAR(100) NOT NULL,
|
||||
`phone` VARCHAR(20) NULL,
|
||||
`company` VARCHAR(100) NULL,
|
||||
`position` VARCHAR(100) NULL,
|
||||
`checked_in` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
`checkin_time` TIMESTAMP NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX `idx_email` (`email`),
|
||||
INDEX `idx_checked_in` (`checked_in`),
|
||||
INDEX `idx_name_phone` (`name`, `phone`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
"#;
|
||||
|
||||
match db.execute(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::MySql,
|
||||
sql.to_owned()
|
||||
)).await {
|
||||
Ok(_) => tracing::info!("Migrations executed successfully"),
|
||||
Err(e) => tracing::error!("Failed to execute migrations: {}", e),
|
||||
}
|
||||
}
|
||||
4
src/lib.rs
Normal file
4
src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod auth;
|
||||
pub mod database;
|
||||
pub mod models;
|
||||
pub mod routes;
|
||||
58
src/main.rs
Normal file
58
src/main.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use axum::{
|
||||
routing::{get, post, delete, put},
|
||||
Router,
|
||||
};
|
||||
use tower_http::cors::{CorsLayer, Any};
|
||||
use std::net::SocketAddr;
|
||||
use tracing_subscriber;
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
|
||||
mod auth;
|
||||
mod database;
|
||||
mod models;
|
||||
mod routes;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Load environment variables
|
||||
dotenv().ok();
|
||||
|
||||
// Initialize tracing
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Initialize database
|
||||
let db = database::init().await;
|
||||
|
||||
// Build the application with routes
|
||||
let app = Router::new()
|
||||
.route("/", get(root))
|
||||
.route("/api/attendees", post(routes::create_attendee))
|
||||
.route("/api/attendees", get(routes::get_attendees))
|
||||
.route("/api/attendees/:id", get(routes::get_attendee))
|
||||
.route("/api/attendees/:id/checkin", post(routes::checkin_attendee))
|
||||
.route("/api/attendees/batch-delete", delete(routes::batch_delete_attendees))
|
||||
.route("/api/attendees/batch-reset-checkin", post(routes::batch_reset_checkin))
|
||||
.route("/api/attendees/import", post(routes::import_attendees_from_excel))
|
||||
.route("/api/attendees/export", get(routes::export_attendees_to_excel))
|
||||
.route("/api/auth/register", post(routes::register_user))
|
||||
.route("/api/auth/login", post(routes::login_user))
|
||||
.route("/api/config", get(routes::get_configs))
|
||||
.route("/api/config/:key", get(routes::get_config))
|
||||
.route("/api/config/:key", put(routes::update_config))
|
||||
.layer(CorsLayer::new().allow_origin(Any).allow_methods(Any).allow_headers(Any))
|
||||
.with_state(db);
|
||||
|
||||
// Run the server
|
||||
let addr = env::var("SERVER_ADDR").unwrap_or("127.0.0.1:3001".to_string());
|
||||
let socket_addr: SocketAddr = addr.parse().expect("Invalid server address");
|
||||
tracing::info!("Server running on {}", socket_addr);
|
||||
axum::Server::bind(&socket_addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn root() -> &'static str {
|
||||
"签到系统 API Server"
|
||||
}
|
||||
123
src/models.rs
Normal file
123
src/models.rs
Normal file
@ -0,0 +1,123 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
// 参会者模型
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)]
|
||||
#[sea_orm(table_name = "attendees")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub email: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub company: Option<String>,
|
||||
pub position: Option<String>,
|
||||
pub checked_in: bool,
|
||||
pub checkin_time: Option<DateTime<Utc>>,
|
||||
#[sea_orm(default_value = "CURRENT_TIMESTAMP")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
#[sea_orm(default_value = "CURRENT_TIMESTAMP")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
// 配置模型
|
||||
pub mod config {
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)]
|
||||
#[sea_orm(table_name = "config")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub config_key: String,
|
||||
pub config_value: String,
|
||||
pub description: Option<String>,
|
||||
#[sea_orm(default_value = "CURRENT_TIMESTAMP")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
#[sea_orm(default_value = "CURRENT_TIMESTAMP")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
}
|
||||
|
||||
// 用户模型
|
||||
pub mod users {
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)]
|
||||
#[sea_orm(table_name = "users")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub password_hash: String,
|
||||
pub email: Option<String>,
|
||||
pub is_active: bool,
|
||||
#[sea_orm(default_value = "CURRENT_TIMESTAMP")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
#[sea_orm(default_value = "CURRENT_TIMESTAMP")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
}
|
||||
|
||||
// 登录请求结构体
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct LoginRequest {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
// 登录响应结构体
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct LoginResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub token: Option<String>,
|
||||
pub user: Option<UserInfo>,
|
||||
}
|
||||
|
||||
// 用户信息结构体
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UserInfo {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub email: Option<String>,
|
||||
}
|
||||
|
||||
// 注册请求结构体
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RegisterRequest {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub email: Option<String>,
|
||||
}
|
||||
|
||||
// 配置更新请求结构体
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ConfigUpdateRequest {
|
||||
pub config_key: String,
|
||||
pub config_value: String,
|
||||
}
|
||||
|
||||
// 配置响应结构体
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ConfigResponse {
|
||||
pub config_key: String,
|
||||
pub config_value: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
778
src/routes.rs
Normal file
778
src/routes.rs
Normal file
@ -0,0 +1,778 @@
|
||||
use axum::{
|
||||
extract::{Path, State, Query, Multipart},
|
||||
http::StatusCode,
|
||||
response::{Json, Response},
|
||||
body::Bytes,
|
||||
http::header,
|
||||
};
|
||||
use sea_orm::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::models::{Entity as Attendee, ActiveModel as ActiveAttendee, Model as AttendeeModel};
|
||||
use crate::models::{users, config, LoginRequest, LoginResponse, RegisterRequest, UserInfo, ConfigUpdateRequest, ConfigResponse};
|
||||
use crate::auth::{hash_password, verify_password, generate_token};
|
||||
use calamine::{Reader, open_workbook_auto_from_rs, Sheets};
|
||||
use std::io::Cursor;
|
||||
use rust_xlsxwriter::*;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateAttendee {
|
||||
pub name: String,
|
||||
pub email: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub company: Option<String>,
|
||||
pub position: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AttendeeQuery {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
pub name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub company: Option<String>,
|
||||
pub position: Option<String>,
|
||||
pub checked_in: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BatchDeleteRequest {
|
||||
pub ids: Vec<i32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BatchResetCheckinRequest {
|
||||
pub ids: Vec<i32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PaginatedResponse<T> {
|
||||
pub data: Vec<T>,
|
||||
pub total: u64,
|
||||
pub page: u64,
|
||||
pub page_size: u64,
|
||||
pub total_pages: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AttendeeResponse {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub email: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub company: Option<String>,
|
||||
pub position: Option<String>,
|
||||
pub checked_in: bool,
|
||||
pub checkin_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<AttendeeModel> for AttendeeResponse {
|
||||
fn from(model: AttendeeModel) -> Self {
|
||||
Self {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
email: model.email,
|
||||
phone: model.phone,
|
||||
company: model.company,
|
||||
position: model.position,
|
||||
checked_in: model.checked_in,
|
||||
checkin_time: model.checkin_time,
|
||||
created_at: model.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_attendee(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Json(payload): Json<CreateAttendee>,
|
||||
) -> Result<(StatusCode, Json<AttendeeResponse>), (StatusCode, String)> {
|
||||
let active_model = ActiveAttendee {
|
||||
name: Set(payload.name),
|
||||
email: Set(payload.email),
|
||||
phone: Set(payload.phone),
|
||||
company: Set(payload.company),
|
||||
position: Set(payload.position),
|
||||
checked_in: Set(false), // 注册时默认未签到
|
||||
checkin_time: Set(None), // 注册时无签到时间
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let model = active_model
|
||||
.insert(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?;
|
||||
|
||||
Ok((StatusCode::CREATED, Json(model.into())))
|
||||
}
|
||||
|
||||
pub async fn get_attendees(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Query(params): Query<AttendeeQuery>,
|
||||
) -> Result<Json<PaginatedResponse<AttendeeResponse>>, (StatusCode, String)> {
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(10);
|
||||
let offset = (page - 1) * page_size;
|
||||
|
||||
// 构建查询条件
|
||||
let mut query = Attendee::find();
|
||||
|
||||
if let Some(name) = ¶ms.name {
|
||||
if !name.is_empty() {
|
||||
query = query.filter(crate::models::Column::Name.contains(name));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(email) = ¶ms.email {
|
||||
if !email.is_empty() {
|
||||
query = query.filter(crate::models::Column::Email.contains(email));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(phone) = ¶ms.phone {
|
||||
if !phone.is_empty() {
|
||||
query = query.filter(crate::models::Column::Phone.contains(phone));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(company) = ¶ms.company {
|
||||
if !company.is_empty() {
|
||||
query = query.filter(crate::models::Column::Company.contains(company));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(position) = ¶ms.position {
|
||||
if !position.is_empty() {
|
||||
query = query.filter(crate::models::Column::Position.contains(position));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(checked_in) = params.checked_in {
|
||||
query = query.filter(crate::models::Column::CheckedIn.eq(checked_in));
|
||||
}
|
||||
|
||||
// 获取总数(应用查询条件)
|
||||
let total = query.clone()
|
||||
.count(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?;
|
||||
|
||||
// 获取分页数据
|
||||
let models = query
|
||||
.order_by_desc(crate::models::Column::CreatedAt)
|
||||
.offset(offset)
|
||||
.limit(page_size)
|
||||
.all(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?;
|
||||
|
||||
let attendees: Vec<AttendeeResponse> = models.into_iter().map(|m| m.into()).collect();
|
||||
|
||||
let total_pages = (total + page_size - 1) / page_size;
|
||||
|
||||
let response = PaginatedResponse {
|
||||
data: attendees,
|
||||
total,
|
||||
page,
|
||||
page_size,
|
||||
total_pages,
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
pub async fn get_attendee(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
) -> Result<Json<AttendeeResponse>, (StatusCode, String)> {
|
||||
let model = Attendee::find_by_id(id)
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "参会者未找到".to_string()))?;
|
||||
|
||||
Ok(Json(model.into()))
|
||||
}
|
||||
|
||||
pub async fn checkin_attendee(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
) -> Result<Json<AttendeeResponse>, (StatusCode, String)> {
|
||||
let model = Attendee::find_by_id(id)
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "参会者未找到".to_string()))?;
|
||||
|
||||
let mut active_model: ActiveAttendee = model.into();
|
||||
active_model.checked_in = Set(true);
|
||||
active_model.checkin_time = Set(Some(chrono::Utc::now()));
|
||||
|
||||
let updated_model = active_model
|
||||
.update(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?;
|
||||
|
||||
Ok(Json(updated_model.into()))
|
||||
}
|
||||
|
||||
pub async fn batch_delete_attendees(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Json(payload): Json<BatchDeleteRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
if payload.ids.is_empty() {
|
||||
return Err((StatusCode::BAD_REQUEST, "删除ID列表不能为空".to_string()));
|
||||
}
|
||||
|
||||
let delete_result = Attendee::delete_many()
|
||||
.filter(crate::models::Column::Id.is_in(payload.ids.clone()))
|
||||
.exec(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"deleted_count": delete_result.rows_affected,
|
||||
"message": format!("成功删除 {} 条记录", delete_result.rows_affected)
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn batch_reset_checkin(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Json(payload): Json<BatchResetCheckinRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
if payload.ids.is_empty() {
|
||||
return Err((StatusCode::BAD_REQUEST, "重置ID列表不能为空".to_string()));
|
||||
}
|
||||
|
||||
// 批量重置签到状态 - 使用单独的更新操作
|
||||
for id in payload.ids.iter() {
|
||||
let model = Attendee::find_by_id(*id)
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?;
|
||||
|
||||
if let Some(attendee) = model {
|
||||
let mut active_model: ActiveAttendee = attendee.into();
|
||||
active_model.checked_in = Set(false);
|
||||
active_model.checkin_time = Set(None);
|
||||
|
||||
active_model
|
||||
.update(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"updated_count": payload.ids.len(),
|
||||
"message": format!("成功重置 {} 条签到记录", payload.ids.len())
|
||||
})))
|
||||
}
|
||||
|
||||
// 用户注册
|
||||
pub async fn register_user(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Json(payload): Json<RegisterRequest>,
|
||||
) -> Result<Json<LoginResponse>, (StatusCode, String)> {
|
||||
// 检查用户名是否已存在
|
||||
let existing_user = users::Entity::find()
|
||||
.filter(users::Column::Username.eq(&payload.username))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?;
|
||||
|
||||
if existing_user.is_some() {
|
||||
return Ok(Json(LoginResponse {
|
||||
success: false,
|
||||
message: "用户名已存在".to_string(),
|
||||
token: None,
|
||||
user: None,
|
||||
}));
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
let password_hash = hash_password(&payload.password)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("密码加密失败: {}", e)))?;
|
||||
|
||||
// 创建新用户
|
||||
let new_user = users::ActiveModel {
|
||||
username: Set(payload.username.clone()),
|
||||
password_hash: Set(password_hash),
|
||||
email: Set(payload.email.clone()),
|
||||
is_active: Set(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let user = new_user
|
||||
.insert(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("创建用户失败: {}", e)))?;
|
||||
|
||||
// 生成JWT令牌
|
||||
let token = generate_token(user.id, &user.username)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("生成令牌失败: {}", e)))?;
|
||||
|
||||
Ok(Json(LoginResponse {
|
||||
success: true,
|
||||
message: "注册成功".to_string(),
|
||||
token: Some(token),
|
||||
user: Some(UserInfo {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
// 用户登录
|
||||
pub async fn login_user(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Json(payload): Json<LoginRequest>,
|
||||
) -> Result<Json<LoginResponse>, (StatusCode, String)> {
|
||||
// 查找用户
|
||||
let user = users::Entity::find()
|
||||
.filter(users::Column::Username.eq(&payload.username))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?;
|
||||
|
||||
let user = match user {
|
||||
Some(user) => user,
|
||||
None => {
|
||||
return Ok(Json(LoginResponse {
|
||||
success: false,
|
||||
message: "用户名或密码错误".to_string(),
|
||||
token: None,
|
||||
user: None,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// 验证密码
|
||||
let is_valid = verify_password(&payload.password, &user.password_hash)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("密码验证失败: {}", e)))?;
|
||||
|
||||
if !is_valid {
|
||||
return Ok(Json(LoginResponse {
|
||||
success: false,
|
||||
message: "用户名或密码错误".to_string(),
|
||||
token: None,
|
||||
user: None,
|
||||
}));
|
||||
}
|
||||
|
||||
// 检查用户是否激活
|
||||
if !user.is_active {
|
||||
return Ok(Json(LoginResponse {
|
||||
success: false,
|
||||
message: "用户账户已被禁用".to_string(),
|
||||
token: None,
|
||||
user: None,
|
||||
}));
|
||||
}
|
||||
|
||||
// 生成JWT令牌
|
||||
let token = generate_token(user.id, &user.username)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("生成令牌失败: {}", e)))?;
|
||||
|
||||
Ok(Json(LoginResponse {
|
||||
success: true,
|
||||
message: "登录成功".to_string(),
|
||||
token: Some(token),
|
||||
user: Some(UserInfo {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ImportResult {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub imported_count: usize,
|
||||
pub failed_count: usize,
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn import_attendees_from_excel(
|
||||
State(db): State<DatabaseConnection>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<ImportResult>, (StatusCode, String)> {
|
||||
let mut imported_count = 0;
|
||||
let mut failed_count = 0;
|
||||
let mut errors = Vec::new();
|
||||
|
||||
// 获取上传的文件
|
||||
while let Some(field) = multipart.next_field().await.map_err(|e| {
|
||||
(StatusCode::BAD_REQUEST, format!("解析文件失败: {}", e))
|
||||
})? {
|
||||
if field.name() == Some("file") {
|
||||
let data = field.bytes().await.map_err(|e| {
|
||||
(StatusCode::BAD_REQUEST, format!("读取文件数据失败: {}", e))
|
||||
})?;
|
||||
|
||||
// 解析Excel文件
|
||||
let cursor = Cursor::new(data.to_vec());
|
||||
let mut workbook: Sheets<_> = open_workbook_auto_from_rs(cursor).map_err(|e| {
|
||||
(StatusCode::BAD_REQUEST, format!("打开Excel文件失败: {}", e))
|
||||
})?;
|
||||
|
||||
// 获取第一个工作表
|
||||
let worksheet_names = workbook.sheet_names();
|
||||
if worksheet_names.is_empty() {
|
||||
return Err((StatusCode::BAD_REQUEST, "Excel文件中没有工作表".to_string()));
|
||||
}
|
||||
|
||||
let range = match workbook.worksheet_range(&worksheet_names[0]) {
|
||||
Some(Ok(range)) => range,
|
||||
Some(Err(e)) => return Err((StatusCode::BAD_REQUEST, format!("读取工作表失败: {}", e))),
|
||||
None => return Err((StatusCode::BAD_REQUEST, "工作表不存在".to_string())),
|
||||
};
|
||||
|
||||
// 解析数据行(跳过标题行)
|
||||
let mut row_index = 0;
|
||||
for row in range.rows() {
|
||||
row_index += 1;
|
||||
|
||||
// 跳过标题行
|
||||
if row_index == 1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查行是否为空
|
||||
if row.is_empty() || row.iter().all(|cell| cell.is_empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析参会者数据
|
||||
// 假设Excel列顺序为: 姓名, 邮箱, 手机号, 公司, 职位
|
||||
let name = row.get(0).map(|v| v.to_string()).unwrap_or_default().trim().to_string();
|
||||
let email = row.get(1).map(|v| v.to_string()).unwrap_or_default().trim().to_string();
|
||||
let email = if email.is_empty() { None } else { Some(email) };
|
||||
let phone = row.get(2).map(|v| v.to_string()).filter(|s| !s.trim().is_empty());
|
||||
let company = row.get(3).map(|v| v.to_string()).filter(|s| !s.trim().is_empty());
|
||||
let position = row.get(4).map(|v| v.to_string()).filter(|s| !s.trim().is_empty());
|
||||
|
||||
// 验证必填字段
|
||||
if name.is_empty() {
|
||||
errors.push(format!("第{}行: 姓名不能为空", row_index));
|
||||
failed_count += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查姓名和手机号组合是否已存在
|
||||
let mut query = Attendee::find().filter(crate::models::Column::Name.eq(&name));
|
||||
|
||||
// 如果有手机号,则同时检查姓名和手机号的组合
|
||||
if let Some(ref phone_value) = phone {
|
||||
query = query.filter(crate::models::Column::Phone.eq(phone_value));
|
||||
|
||||
let existing = query
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库查询失败: {}", e)))?;
|
||||
|
||||
if existing.is_some() {
|
||||
errors.push(format!("第{}行: 姓名 {} 和手机号 {} 的组合已存在", row_index, name, phone_value));
|
||||
failed_count += 1;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// 如果没有手机号,只检查姓名
|
||||
let existing = query
|
||||
.filter(crate::models::Column::Phone.is_null())
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库查询失败: {}", e)))?;
|
||||
|
||||
if existing.is_some() {
|
||||
errors.push(format!("第{}行: 姓名 {} (无手机号)已存在", row_index, name));
|
||||
failed_count += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建参会者记录
|
||||
let active_model = ActiveAttendee {
|
||||
name: Set(name),
|
||||
email: Set(email),
|
||||
phone: Set(phone),
|
||||
company: Set(company),
|
||||
position: Set(position),
|
||||
checked_in: Set(false),
|
||||
checkin_time: Set(None),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match active_model.insert(&db).await {
|
||||
Ok(_) => imported_count += 1,
|
||||
Err(e) => {
|
||||
errors.push(format!("第{}行: 保存失败 - {}", row_index, e));
|
||||
failed_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break; // 只处理第一个文件
|
||||
}
|
||||
}
|
||||
|
||||
let message = if failed_count == 0 {
|
||||
format!("成功导入 {} 条记录", imported_count)
|
||||
} else {
|
||||
format!("导入完成:成功 {} 条,失败 {} 条", imported_count, failed_count)
|
||||
};
|
||||
|
||||
Ok(Json(ImportResult {
|
||||
success: failed_count == 0,
|
||||
message,
|
||||
imported_count,
|
||||
failed_count,
|
||||
errors,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn export_attendees_to_excel(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Query(params): Query<AttendeeQuery>,
|
||||
) -> Result<(StatusCode, [(String, String); 2], Bytes), (StatusCode, String)> {
|
||||
// 构建查询条件
|
||||
let mut query = Attendee::find();
|
||||
|
||||
if let Some(name) = ¶ms.name {
|
||||
query = query.filter(crate::models::Column::Name.contains(name));
|
||||
}
|
||||
if let Some(email) = ¶ms.email {
|
||||
query = query.filter(crate::models::Column::Email.contains(email));
|
||||
}
|
||||
if let Some(phone) = ¶ms.phone {
|
||||
query = query.filter(crate::models::Column::Phone.contains(phone));
|
||||
}
|
||||
if let Some(company) = ¶ms.company {
|
||||
query = query.filter(crate::models::Column::Company.contains(company));
|
||||
}
|
||||
if let Some(position) = ¶ms.position {
|
||||
query = query.filter(crate::models::Column::Position.contains(position));
|
||||
}
|
||||
if let Some(checked_in) = params.checked_in {
|
||||
query = query.filter(crate::models::Column::CheckedIn.eq(checked_in));
|
||||
}
|
||||
|
||||
// 获取所有符合条件的参会者
|
||||
let attendees = query.all(&db).await.map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("数据库查询失败: {}", e))
|
||||
})?;
|
||||
|
||||
// 创建 Excel 文件
|
||||
let mut workbook = Workbook::new();
|
||||
let worksheet = workbook.add_worksheet();
|
||||
|
||||
// 写入表头 - 按照前端界面字段顺序
|
||||
let headers = ["ID", "姓名", "手机号", "公司", "职位", "签到状态", "签到时间", "邮箱", "注册时间"];
|
||||
for (col, header) in headers.iter().enumerate() {
|
||||
worksheet.write_string(0, col as u16, *header).map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("写入表头失败: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
// 写入数据 - 按照前端界面字段顺序
|
||||
for (row, attendee) in attendees.iter().enumerate() {
|
||||
let row_idx = (row + 1) as u32;
|
||||
|
||||
// ID
|
||||
worksheet.write_number(row_idx, 0, attendee.id as f64).map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("写入数据失败: {}", e))
|
||||
})?;
|
||||
|
||||
// 姓名
|
||||
worksheet.write_string(row_idx, 1, &attendee.name).map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("写入数据失败: {}", e))
|
||||
})?;
|
||||
|
||||
// 手机号
|
||||
worksheet.write_string(row_idx, 2, attendee.phone.as_deref().unwrap_or("")).map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("写入数据失败: {}", e))
|
||||
})?;
|
||||
|
||||
// 公司
|
||||
worksheet.write_string(row_idx, 3, attendee.company.as_deref().unwrap_or("")).map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("写入数据失败: {}", e))
|
||||
})?;
|
||||
|
||||
// 职位
|
||||
worksheet.write_string(row_idx, 4, attendee.position.as_deref().unwrap_or("")).map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("写入数据失败: {}", e))
|
||||
})?;
|
||||
|
||||
// 签到状态
|
||||
worksheet.write_string(row_idx, 5, if attendee.checked_in { "已签到" } else { "未签到" }).map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("写入数据失败: {}", e))
|
||||
})?;
|
||||
|
||||
// 签到时间 - 转换为北京时间 (UTC+8)
|
||||
let checkin_time_str = attendee.checkin_time
|
||||
.map(|t| {
|
||||
// 将UTC时间转换为北京时间 (UTC+8)
|
||||
let beijing_time = t + chrono::Duration::hours(8);
|
||||
beijing_time.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
})
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
worksheet.write_string(row_idx, 6, &checkin_time_str).map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("写入数据失败: {}", e))
|
||||
})?;
|
||||
|
||||
// 邮箱
|
||||
let email_str = attendee.email.as_deref().unwrap_or("-");
|
||||
worksheet.write_string(row_idx, 7, email_str).map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("写入数据失败: {}", e))
|
||||
})?;
|
||||
|
||||
// 注册时间 - 转换为北京时间 (UTC+8)
|
||||
let created_at_str = {
|
||||
let beijing_time = attendee.created_at + chrono::Duration::hours(8);
|
||||
beijing_time.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
};
|
||||
worksheet.write_string(row_idx, 8, &created_at_str).map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("写入数据失败: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
// 获取 Excel 文件数据
|
||||
let excel_data = workbook.save_to_buffer().map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("生成Excel文件失败: {}", e))
|
||||
})?;
|
||||
|
||||
// 生成文件名
|
||||
let filename = format!("attendees_{}.xlsx", chrono::Utc::now().format("%Y%m%d_%H%M%S"));
|
||||
|
||||
// 设置响应头
|
||||
let headers = [
|
||||
(header::CONTENT_TYPE.as_str().to_string(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet".to_string()),
|
||||
(header::CONTENT_DISPOSITION.as_str().to_string(), format!("attachment; filename=\"{}\"", filename)),
|
||||
];
|
||||
|
||||
Ok((StatusCode::OK, headers, Bytes::from(excel_data)))
|
||||
}
|
||||
|
||||
// 获取所有配置
|
||||
pub async fn get_configs(
|
||||
State(db): State<DatabaseConnection>,
|
||||
) -> Response<String> {
|
||||
let configs = match config::Entity::find()
|
||||
.all(&db)
|
||||
.await {
|
||||
Ok(configs) => configs,
|
||||
Err(e) => {
|
||||
return Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
|
||||
.body(format!(r#"{{"error": "数据库错误: {}"}}"#, e))
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
let config_responses: Vec<ConfigResponse> = configs
|
||||
.into_iter()
|
||||
.map(|config| ConfigResponse {
|
||||
config_key: config.config_key,
|
||||
config_value: config.config_value,
|
||||
description: config.description,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let json_str = match serde_json::to_string(&config_responses) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
return Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
|
||||
.body(format!(r#"{{"error": "序列化错误: {}"}}"#, e))
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
|
||||
.body(json_str)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// 获取单个配置
|
||||
pub async fn get_config(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Path(key): Path<String>,
|
||||
) -> Result<Json<ConfigResponse>, (StatusCode, String)> {
|
||||
let config = config::Entity::find()
|
||||
.filter(config::Column::ConfigKey.eq(&key))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?;
|
||||
|
||||
match config {
|
||||
Some(config) => Ok(Json(ConfigResponse {
|
||||
config_key: config.config_key,
|
||||
config_value: config.config_value,
|
||||
description: config.description,
|
||||
})),
|
||||
None => Err((StatusCode::NOT_FOUND, "配置项不存在".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
pub async fn update_config(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Path(key): Path<String>,
|
||||
Json(payload): Json<ConfigUpdateRequest>,
|
||||
) -> Result<Json<ConfigResponse>, (StatusCode, String)> {
|
||||
// 查找现有配置
|
||||
let existing_config = config::Entity::find()
|
||||
.filter(config::Column::ConfigKey.eq(&key))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?;
|
||||
|
||||
match existing_config {
|
||||
Some(config) => {
|
||||
// 更新现有配置
|
||||
let mut active_config: config::ActiveModel = config.into();
|
||||
active_config.config_value = Set(payload.config_value.clone());
|
||||
active_config.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
let updated_config = active_config
|
||||
.update(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?;
|
||||
|
||||
Ok(Json(ConfigResponse {
|
||||
config_key: updated_config.config_key,
|
||||
config_value: updated_config.config_value,
|
||||
description: updated_config.description,
|
||||
}))
|
||||
}
|
||||
None => {
|
||||
// 创建新配置
|
||||
let new_config = config::ActiveModel {
|
||||
config_key: Set(payload.config_key),
|
||||
config_value: Set(payload.config_value),
|
||||
description: Set(None),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
updated_at: Set(chrono::Utc::now()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let created_config = new_config
|
||||
.insert(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {}", e)))?;
|
||||
|
||||
Ok(Json(ConfigResponse {
|
||||
config_key: created_config.config_key,
|
||||
config_value: created_config.config_value,
|
||||
description: created_config.description,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user