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 admin;
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod checkin;
|
||||||
mod comfyui;
|
mod comfyui;
|
||||||
mod events;
|
mod events;
|
||||||
mod health;
|
mod health;
|
||||||
@@ -16,5 +17,6 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
.merge(comfyui::routes())
|
.merge(comfyui::routes())
|
||||||
.merge(auth::routes())
|
.merge(auth::routes())
|
||||||
.merge(events::routes())
|
.merge(events::routes())
|
||||||
|
.merge(checkin::routes())
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user