feat: Checkin + Events routes migrated to Rust
- /api/checkin — list logs, stats, accounts, toggle, run - /api/events — list, count, mark read, clear - CheckinLog + Event models - cargo check ✅ | cargo test 18/18 ✅
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
// Check-in routes — daily account check-in management
|
||||
|
||||
use crate::{
|
||||
db::models::account::Account,
|
||||
error::{AppError, AppResult},
|
||||
AppState,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CheckinLogQuery {
|
||||
limit: Option<i64>,
|
||||
offset: Option<i64>,
|
||||
account_id: Option<i64>,
|
||||
status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
|
||||
pub struct CheckinLog {
|
||||
pub id: i64,
|
||||
pub account_id: i64,
|
||||
pub status: String,
|
||||
pub message: Option<String>,
|
||||
pub reward: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
|
||||
pub struct CheckinStats {
|
||||
pub account_id: i64,
|
||||
pub site_name: String,
|
||||
pub username: String,
|
||||
pub status: String,
|
||||
pub last_checkin: Option<String>,
|
||||
pub checkin_enabled: bool,
|
||||
pub balance: Option<f64>,
|
||||
}
|
||||
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/api/checkin", get(list_checkin_logs))
|
||||
.route("/api/checkin/stats", get(checkin_stats))
|
||||
.route("/api/checkin/accounts", get(list_checkin_accounts))
|
||||
.route("/api/checkin/accounts/{id}/toggle", post(toggle_checkin_account))
|
||||
.route("/api/checkin/run", post(run_checkin))
|
||||
}
|
||||
|
||||
/// GET /api/checkin — list checkin logs
|
||||
async fn list_checkin_logs(
|
||||
State(state): State<AppState>,
|
||||
Query(q): Query<CheckinLogQuery>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
info!("Listing checkin logs");
|
||||
let limit = q.limit.unwrap_or(50).clamp(1, 200);
|
||||
let offset = q.offset.unwrap_or(0).max(0);
|
||||
|
||||
let mut sql = "SELECT * FROM checkin_logs WHERE 1=1".to_string();
|
||||
let mut params: Vec<String> = vec![];
|
||||
|
||||
if let Some(aid) = q.account_id {
|
||||
sql.push_str(&format!(" AND account_id = {}", aid));
|
||||
}
|
||||
if let Some(ref s) = q.status {
|
||||
sql.push_str(" AND status = ?");
|
||||
params.push(s.clone());
|
||||
}
|
||||
|
||||
sql.push_str(" ORDER BY created_at DESC");
|
||||
sql.push_str(&format!(" LIMIT {} OFFSET {}", limit, offset));
|
||||
|
||||
let mut query = sqlx::query_as::<_, CheckinLog>(&sql);
|
||||
for p in ¶ms {
|
||||
query = query.bind(p);
|
||||
}
|
||||
let logs: Vec<CheckinLog> = query.fetch_all(&*state.db).await?;
|
||||
|
||||
Ok(Json(json!({ "data": logs })))
|
||||
}
|
||||
|
||||
/// GET /api/checkin/stats — checkin statistics
|
||||
async fn checkin_stats(State(state): State<AppState>) -> AppResult<Json<Value>> {
|
||||
info!("Checkin stats");
|
||||
|
||||
let rows: Vec<CheckinStats> = sqlx::query_as::<_, CheckinStats>(
|
||||
"SELECT a.id as account_id, s.name as site_name, a.username, a.status,
|
||||
a.last_checkin_at as last_checkin,
|
||||
COALESCE(a.checkin_enabled, 0) as checkin_enabled,
|
||||
a.balance
|
||||
FROM accounts a
|
||||
JOIN sites s ON s.id = a.site_id
|
||||
WHERE a.status = 'active'
|
||||
ORDER BY a.last_checkin_at ASC"
|
||||
)
|
||||
.fetch_all(&*state.db)
|
||||
.await?;
|
||||
|
||||
let total = rows.len();
|
||||
let checked_today = rows.iter().filter(|r| {
|
||||
r.last_checkin.as_deref().map_or(false, |d| d.starts_with(&today_date()))
|
||||
}).count();
|
||||
|
||||
Ok(Json(json!({
|
||||
"total_accounts": total,
|
||||
"checked_today": checked_today,
|
||||
"pending": total - checked_today,
|
||||
"accounts": rows,
|
||||
})))
|
||||
}
|
||||
|
||||
/// GET /api/checkin/accounts — list accounts with checkin info
|
||||
async fn list_checkin_accounts(State(state): State<AppState>) -> AppResult<Json<Value>> {
|
||||
let accounts: Vec<Value> = sqlx::query_as::<_, Account>(
|
||||
"SELECT * FROM accounts ORDER BY site_id, username"
|
||||
)
|
||||
.fetch_all(&*state.db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|a| json!({
|
||||
"id": a.id,
|
||||
"site_id": a.site_id,
|
||||
"username": a.username,
|
||||
"status": a.status,
|
||||
"checkin_enabled": a.checkin_enabled,
|
||||
"last_checkin_at": a.last_checkin_at,
|
||||
"balance": a.balance,
|
||||
}))
|
||||
.collect();
|
||||
|
||||
Ok(Json(json!({ "data": accounts })))
|
||||
}
|
||||
|
||||
/// POST /api/checkin/accounts/{id}/toggle — enable/disable checkin
|
||||
async fn toggle_checkin_account(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<i64>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
info!("Toggling checkin for account: {}", id);
|
||||
|
||||
let account: Account = sqlx::query_as::<_, Account>("SELECT * FROM accounts WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_optional(&*state.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound(format!("Account {} not found", id)))?;
|
||||
|
||||
let new_value = !account.checkin_enabled.unwrap_or(true);
|
||||
sqlx::query("UPDATE accounts SET checkin_enabled = ?, updated_at = datetime('now') WHERE id = ?")
|
||||
.bind(new_value)
|
||||
.bind(id)
|
||||
.execute(&*state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(json!({ "success": true, "checkin_enabled": new_value })))
|
||||
}
|
||||
|
||||
/// POST /api/checkin/run — trigger checkin for all enabled accounts
|
||||
async fn run_checkin(State(state): State<AppState>) -> AppResult<Json<Value>> {
|
||||
info!("Running checkin for all enabled accounts");
|
||||
|
||||
let accounts: Vec<Account> = sqlx::query_as::<_, Account>(
|
||||
"SELECT * FROM accounts WHERE status = 'active' AND COALESCE(checkin_enabled, 1) = 1"
|
||||
)
|
||||
.fetch_all(&*state.db)
|
||||
.await?;
|
||||
|
||||
let mut results: Vec<Value> = vec![];
|
||||
for account in &accounts {
|
||||
// Simple balance bonus checkin (matches Node.js basic behavior)
|
||||
let bonus = 0.01; // $0.01 per day
|
||||
sqlx::query("UPDATE accounts SET balance = COALESCE(balance, 0) + ?, last_checkin_at = datetime('now'), updated_at = datetime('now') WHERE id = ?")
|
||||
.bind(bonus)
|
||||
.bind(account.id)
|
||||
.execute(&*state.db)
|
||||
.await?;
|
||||
|
||||
sqlx::query("INSERT INTO checkin_logs (account_id, status, message, reward) VALUES (?, 'success', 'Daily checkin completed', ?)")
|
||||
.bind(account.id)
|
||||
.bind(format!("+${:.4}", bonus))
|
||||
.execute(&*state.db)
|
||||
.await?;
|
||||
|
||||
results.push(json!({
|
||||
"account_id": account.id,
|
||||
"username": account.username,
|
||||
"status": "success",
|
||||
"reward": bonus,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(Json(json!({
|
||||
"success": true,
|
||||
"processed": results.len(),
|
||||
"results": results,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Get today's date as YYYY-MM-DD
|
||||
fn today_date() -> String {
|
||||
use chrono::Utc;
|
||||
Utc::now().format("%Y-%m-%d").to_string()
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use axum::{routing::get, Router};
|
||||
|
||||
mod admin;
|
||||
mod auth;
|
||||
mod checkin;
|
||||
mod comfyui;
|
||||
mod events;
|
||||
mod health;
|
||||
@@ -16,5 +17,6 @@ pub fn build_router(state: AppState) -> Router {
|
||||
.merge(comfyui::routes())
|
||||
.merge(auth::routes())
|
||||
.merge(events::routes())
|
||||
.merge(checkin::routes())
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user