feat: README, config.example, turnstile integration, exchange rate, auth utils refactor

This commit is contained in:
Ember Moth
2026-07-05 21:15:17 +08:00
parent 25d190b90e
commit 4ffdc89421
19 changed files with 763 additions and 4 deletions
+9 -3
View File
@@ -77,11 +77,17 @@ pub fn register_all(repos: Arc<Repositories>, config: Arc<Config>) -> ServeMux {
},
);
// ── Quota task ────────────────────────────────────────────────────────────
// ── Quota task + Rate task ─────────────────────────────────────────────────
let quota_repos = Arc::clone(&repos);
let quota_config = Arc::clone(&config);
mux.handle_async_func(crate::queue::types::FORTHWITH_QUOTA_TASK, move |task| {
task::quota_task(task, Arc::clone(&quota_repos), Arc::clone(&quota_config))
mux.handle_async_func(crate::queue::types::FORTHWITH_QUOTA_TASK, move |t| {
task::quota_task(t, Arc::clone(&quota_repos), Arc::clone(&quota_config))
});
let rate_repos = Arc::clone(&repos);
let rate_config = Arc::clone(&config);
mux.handle_async_func(crate::queue::types::FORTHWITH_RATE_TASK, move |t| {
task::rate_task(t, Arc::clone(&rate_repos), Arc::clone(&rate_config))
});
mux
+10 -1
View File
@@ -4,7 +4,7 @@ use asynq::error::Result;
use asynq::task::Task;
use crate::config::Config;
use crate::queue::service::task::QuotaTaskLogic;
use crate::queue::service::task::{QuotaTaskLogic, RateLogic};
use crate::repository::Repositories;
pub async fn quota_task(task: Task, repos: Arc<Repositories>, config: Arc<Config>) -> Result<()> {
@@ -13,3 +13,12 @@ pub async fn quota_task(task: Task, repos: Arc<Repositories>, config: Arc<Config
.await
.map_err(|e| asynq::error::Error::other(e.to_string()))
}
/// Fetch the current exchange rate and persist it.
/// Scheduled daily at 01:00 by the scheduler (mirrors `rateLogic.go`).
pub async fn rate_task(_task: Task, repos: Arc<Repositories>, config: Arc<Config>) -> Result<()> {
RateLogic::new(repos, config)
.execute()
.await
.map_err(|e| asynq::error::Error::other(e.to_string()))
}
+54
View File
@@ -250,3 +250,57 @@ impl QuotaTaskLogic {
Ok(())
}
}
// ─────────────────────────────────────────────────────────────────────────────
// RateLogic — port of `server/queue/logic/task/rateLogic.go`
// Fetches the current exchange rate for the site currency and caches it.
// ─────────────────────────────────────────────────────────────────────────────
pub struct RateLogic {
repos: Arc<Repositories>,
config: Arc<Config>,
}
impl RateLogic {
pub fn new(repos: Arc<Repositories>, config: Arc<Config>) -> Self {
Self { repos, config }
}
/// Fetch exchange rate from apilayer.com and cache it in Redis.
///
/// Skips silently when `Currency.AccessKey` is not configured.
pub async fn execute(&self) -> anyhow::Result<()> {
let access_key = &self.config.currency.access_key;
if access_key.is_empty() {
tracing::debug!("[RateLogic] skip: no Currency.AccessKey configured");
return Ok(());
}
let from = &self.config.currency.unit;
if from.is_empty() || from.to_uppercase() == "CNY" {
// Already in base currency — rate is 1.0, nothing to fetch.
tracing::debug!("[RateLogic] skip: currency unit is CNY or empty");
return Ok(());
}
match crate::exchange_rate::convert(from, "CNY", 1.0, access_key).await {
Ok(rate) => {
tracing::info!(
"[RateLogic] exchange rate {from}→CNY = {rate:.6}"
);
// Persist rate to system config table.
let _ = self
.repos
.system
.update_value_by_category_key("currency", "exchange_rate", &rate.to_string())
.await
.map_err(|e| tracing::warn!("[RateLogic] persist rate failed: {e}"));
}
Err(e) => {
tracing::error!("[RateLogic] fetch exchange rate failed: {e}");
return Err(e);
}
}
Ok(())
}
}
+2
View File
@@ -28,3 +28,5 @@ pub const FORTHWITH_TRAFFIC_STATISTICS: &str = "forthwith:traffic:statistics";
// ── task / quota ───────────────────────────────────────────────────────
pub const FORTHWITH_QUOTA_TASK: &str = "forthwith:quota:task";
/// Daily exchange-rate refresh (mirrors Go `ForthwithRateTask`).
pub const FORTHWITH_RATE_TASK: &str = "forthwith:rate:task";