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:
boos
2026-05-17 13:29:17 +08:00
parent af05b40799
commit 2793a7990d
2 changed files with 209 additions and 0 deletions
+207
View File
@@ -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 &params {
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()
}
+2
View File
@@ -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)
} }