feat(billing): user ledger list + summary queries
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use crate::error::AppResult;
|
||||
use sqlx::{SqlitePool, Transaction};
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::{Row, SqlitePool, Transaction};
|
||||
|
||||
/// One balance-changing event. Written to `credit_ledger` in the same
|
||||
/// transaction that updates the cached balance, so ledger and cache never diverge.
|
||||
@@ -143,6 +144,77 @@ pub async fn apply_points_delta(
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
/// List a user's ledger rows, newest first, optional track/entry_type filter + paging.
|
||||
pub async fn list_user_ledger(
|
||||
db: &SqlitePool,
|
||||
user_id: i64,
|
||||
track: Option<&str>,
|
||||
entry_type: Option<&str>,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> AppResult<Vec<Value>> {
|
||||
ensure_ledger_schema(db).await?;
|
||||
let limit = limit.clamp(1, 500);
|
||||
let offset = offset.max(0);
|
||||
let rows = sqlx::query(
|
||||
"SELECT track, entry_type, amount, balance_after, model, operator, ref_type, ref_id, note, created_at
|
||||
FROM credit_ledger
|
||||
WHERE user_id = ?
|
||||
AND (? IS NULL OR track = ?)
|
||||
AND (? IS NULL OR entry_type = ?)
|
||||
ORDER BY id DESC LIMIT ? OFFSET ?",
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(track)
|
||||
.bind(track)
|
||||
.bind(entry_type)
|
||||
.bind(entry_type)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| {
|
||||
json!({
|
||||
"track": r.try_get::<String, _>("track").unwrap_or_default(),
|
||||
"entryType": r.try_get::<String, _>("entry_type").unwrap_or_default(),
|
||||
"amount": r.try_get::<f64, _>("amount").unwrap_or(0.0),
|
||||
"balanceAfter": r.try_get::<f64, _>("balance_after").unwrap_or(0.0),
|
||||
"model": r.try_get::<Option<String>, _>("model").ok().flatten(),
|
||||
"operator": r.try_get::<Option<String>, _>("operator").ok().flatten(),
|
||||
"refType": r.try_get::<Option<String>, _>("ref_type").ok().flatten(),
|
||||
"note": r.try_get::<Option<String>, _>("note").ok().flatten(),
|
||||
"createdAt": r.try_get::<Option<String>, _>("created_at").ok().flatten(),
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Aggregate a user's ledger by (track, entry_type): sum amount + count.
|
||||
pub async fn summarize_user_ledger(db: &SqlitePool, user_id: i64) -> AppResult<Vec<Value>> {
|
||||
ensure_ledger_schema(db).await?;
|
||||
let rows = sqlx::query(
|
||||
"SELECT track, entry_type, COALESCE(SUM(amount),0) AS total, COUNT(*) AS cnt
|
||||
FROM credit_ledger WHERE user_id = ?
|
||||
GROUP BY track, entry_type ORDER BY track, entry_type",
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| {
|
||||
json!({
|
||||
"track": r.try_get::<String, _>("track").unwrap_or_default(),
|
||||
"entryType": r.try_get::<String, _>("entry_type").unwrap_or_default(),
|
||||
"total": r.try_get::<f64, _>("total").unwrap_or(0.0),
|
||||
"count": r.try_get::<i64, _>("cnt").unwrap_or(0),
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -207,4 +279,17 @@ mod tests {
|
||||
let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM credit_ledger WHERE user_id=9 AND track='points'").fetch_one(&pool).await.unwrap();
|
||||
assert_eq!(n, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_and_summarize_user_ledger() {
|
||||
let pool = mem_pool().await;
|
||||
apply_points_delta(&pool, 9, 100.0, "recharge", None, Some("system"), Some("rr"), None).await.unwrap();
|
||||
apply_points_delta(&pool, 9, -10.0, "consume", Some("wf:1"), Some("system"), Some("run"), None).await.unwrap();
|
||||
// 明细:user 9 两行,track=points
|
||||
let rows = list_user_ledger(&pool, 9, None, None, 50, 0).await.unwrap();
|
||||
assert_eq!(rows.len(), 2);
|
||||
// 聚合:points 轨 consume 合计 -10
|
||||
let sum = summarize_user_ledger(&pool, 9).await.unwrap();
|
||||
assert!(sum.iter().any(|v| v["track"]=="points" && v["entryType"]=="consume"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user