feat: add configurable verification and redemption codes
This commit is contained in:
112
src/api/admin.rs
112
src/api/admin.rs
@@ -4,7 +4,9 @@ use crate::error::{AppError, ErrorCode};
|
||||
use crate::services::billing;
|
||||
use crate::services::mail;
|
||||
use crate::services::settings;
|
||||
use crate::services::settings::{MailConfigStored, MailCustomSmtp, StripeConfigStored};
|
||||
use crate::services::settings::{
|
||||
AuthConfigStored, MailConfigStored, MailCustomSmtp, StripeConfigStored,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
|
||||
use axum::extract::{ConnectInfo, Path, Query, State};
|
||||
@@ -36,6 +38,8 @@ pub fn router() -> Router<AppState> {
|
||||
.route("/admin/mail", get(get_mail_config))
|
||||
.route("/admin/mail", put(update_mail_config))
|
||||
.route("/admin/mail/test", post(test_mail))
|
||||
.route("/admin/auth", get(get_auth_config))
|
||||
.route("/admin/auth", put(update_auth_config))
|
||||
.route("/admin/config", get(get_config))
|
||||
.route("/admin/config", put(update_config))
|
||||
}
|
||||
@@ -180,7 +184,13 @@ async fn get_stats(
|
||||
.map_err(|err| AppError::new(ErrorCode::Internal, "查询用量统计失败").with_source(err))?;
|
||||
|
||||
let active_subscriptions: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM subscriptions WHERE status IN ('active', 'trialing', 'past_due')",
|
||||
r#"
|
||||
SELECT COUNT(*)
|
||||
FROM subscriptions
|
||||
WHERE status IN ('active', 'trialing', 'past_due')
|
||||
AND current_period_start <= NOW()
|
||||
AND current_period_end > NOW()
|
||||
"#,
|
||||
)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
@@ -689,8 +699,9 @@ struct GrantCreditsResponse {
|
||||
period_end: DateTime<Utc>,
|
||||
used_units: i32,
|
||||
bonus_units: i32,
|
||||
total_units: i32,
|
||||
remaining_units: i32,
|
||||
redeemed_units: i64,
|
||||
total_units: i64,
|
||||
remaining_units: i64,
|
||||
}
|
||||
|
||||
async fn grant_credits(
|
||||
@@ -722,6 +733,9 @@ async fn grant_credits(
|
||||
SELECT id, plan_id, current_period_start, current_period_end
|
||||
FROM subscriptions
|
||||
WHERE user_id = $1
|
||||
AND status IN ('active', 'trialing')
|
||||
AND current_period_start <= NOW()
|
||||
AND current_period_end > NOW()
|
||||
ORDER BY current_period_end DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
@@ -781,6 +795,7 @@ async fn grant_credits(
|
||||
struct UsageRow {
|
||||
used_units: i32,
|
||||
bonus_units: i32,
|
||||
grant_used_units: i32,
|
||||
}
|
||||
|
||||
let usage = sqlx::query_as::<_, UsageRow>(
|
||||
@@ -789,7 +804,7 @@ async fn grant_credits(
|
||||
SET bonus_units = bonus_units + $1,
|
||||
updated_at = NOW()
|
||||
WHERE user_id = $2 AND period_start = $3 AND period_end = $4
|
||||
RETURNING used_units, bonus_units
|
||||
RETURNING used_units, bonus_units, grant_used_units
|
||||
"#,
|
||||
)
|
||||
.bind(req.units)
|
||||
@@ -818,8 +833,27 @@ async fn grant_credits(
|
||||
.await
|
||||
.map_err(|err| AppError::new(ErrorCode::Internal, "提交事务失败").with_source(err))?;
|
||||
|
||||
let total_units = plan_units + usage.bonus_units;
|
||||
let remaining = (total_units - usage.used_units).max(0);
|
||||
let redeemed_units: i64 = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT COALESCE(SUM(remaining_units), 0)::bigint
|
||||
FROM unit_grants
|
||||
WHERE user_id = $1
|
||||
AND starts_at <= NOW()
|
||||
AND expires_at > NOW()
|
||||
AND remaining_units > 0
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.map_err(|err| AppError::new(ErrorCode::Internal, "查询兑换额度失败").with_source(err))?;
|
||||
let base_total = i64::from(plan_units.saturating_add(usage.bonus_units));
|
||||
let base_used = i64::from(usage.used_units.saturating_sub(usage.grant_used_units));
|
||||
let remaining = base_total
|
||||
.saturating_sub(base_used)
|
||||
.max(0)
|
||||
.saturating_add(redeemed_units);
|
||||
let total_units = i64::from(usage.used_units).saturating_add(remaining);
|
||||
|
||||
Ok(Json(Envelope {
|
||||
success: true,
|
||||
@@ -829,6 +863,7 @@ async fn grant_credits(
|
||||
period_end,
|
||||
used_units: usage.used_units,
|
||||
bonus_units: usage.bonus_units,
|
||||
redeemed_units,
|
||||
total_units,
|
||||
remaining_units: remaining,
|
||||
},
|
||||
@@ -1274,6 +1309,69 @@ async fn update_stripe_config(
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AuthConfigView {
|
||||
email_verification_required: bool,
|
||||
}
|
||||
|
||||
async fn get_auth_config(
|
||||
State(state): State<AppState>,
|
||||
jar: axum_extra::extract::cookie::CookieJar,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<Envelope<AuthConfigView>>, AppError> {
|
||||
let ip = context::client_ip(&headers, addr.ip());
|
||||
let (_jar, _admin_id) = require_admin(&state, jar, &headers, ip).await?;
|
||||
let config = settings::load_system_config::<AuthConfigStored>(&state, "auth")
|
||||
.await?
|
||||
.unwrap_or(AuthConfigStored {
|
||||
email_verification_required: true,
|
||||
});
|
||||
|
||||
Ok(Json(Envelope {
|
||||
success: true,
|
||||
data: AuthConfigView {
|
||||
email_verification_required: config.email_verification_required,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AuthConfigRequest {
|
||||
email_verification_required: bool,
|
||||
}
|
||||
|
||||
async fn update_auth_config(
|
||||
State(state): State<AppState>,
|
||||
jar: axum_extra::extract::cookie::CookieJar,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<AuthConfigRequest>,
|
||||
) -> Result<Json<Envelope<AuthConfigView>>, AppError> {
|
||||
let ip = context::client_ip(&headers, addr.ip());
|
||||
let (_jar, admin_id) = require_admin(&state, jar, &headers, ip).await?;
|
||||
let config = AuthConfigStored {
|
||||
email_verification_required: req.email_verification_required,
|
||||
};
|
||||
settings::upsert_system_config(
|
||||
&state,
|
||||
"auth",
|
||||
serde_json::to_value(&config).map_err(|err| {
|
||||
AppError::new(ErrorCode::Internal, "序列化认证配置失败").with_source(err)
|
||||
})?,
|
||||
Some("认证功能开关"),
|
||||
Some(admin_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(Envelope {
|
||||
success: true,
|
||||
data: AuthConfigView {
|
||||
email_verification_required: config.email_verification_required,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct MailConfigView {
|
||||
enabled: bool,
|
||||
|
||||
Reference in New Issue
Block a user