docs: 细化计费第3期(消费明细)TDD Task11-14
This commit is contained in:
@@ -567,3 +567,128 @@ pub async fn apply_points_delta(
|
||||
|
||||
### Task 10:第 2 期全量回归
|
||||
- [ ] `cargo test 2>&1 | grep "test result"` 全 ok;`cargo build -j 2 | tail -2` Finished。
|
||||
|
||||
---
|
||||
|
||||
## 第 3 期:A 消费明细(细化 TDD)
|
||||
|
||||
文件:`src/services/billing/ledger.rs`(加查询函数)、`src/routes/user/mod.rs`(加 2 路由)、`src/web/api.ts`(加 client)、`src/web/pages/UsageRecords.tsx`(扩展)。
|
||||
|
||||
### Task 11:ledger 查询函数(明细 + 聚合)
|
||||
|
||||
**Files:** Modify `src/services/billing/ledger.rs`
|
||||
|
||||
- [ ] **Step1 失败测试**(ledger.rs tests mod):
|
||||
```rust
|
||||
#[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"));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step2** `cargo test -p boosapi --lib list_and_summarize_user_ledger 2>&1|tail -6` FAIL。
|
||||
|
||||
- [ ] **Step3 实现**(ledger.rs,tests 之前):
|
||||
```rust
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// 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 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 (?1_track IS NULL OR track = ?1_track)
|
||||
AND (?1_type IS NULL OR entry_type = ?1_type)
|
||||
ORDER BY id DESC LIMIT ? OFFSET ?"
|
||||
.replace("?1_track", "?2").replace("?1_type", "?3")
|
||||
)
|
||||
.bind(user_id).bind(track).bind(entry_type).bind(limit).bind(offset)
|
||||
.fetch_all(db).await?;
|
||||
use sqlx::Row;
|
||||
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?;
|
||||
use sqlx::Row;
|
||||
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())
|
||||
}
|
||||
```
|
||||
注:上面 SQL 的 named-param replace 写法若 sqlx 不接受,改用直白的可选过滤:用 `track.is_none() OR track = ?` 形式,bind 顺序 user_id, track, track, entry_type, entry_type, limit, offset(每个 Option bind 两次)。子代理按 sqlx 实际行为调整到能编译+测试过。
|
||||
|
||||
- [ ] **Step4** 测试 PASS + `cargo build -j 2`。
|
||||
- [ ] **Step5** `git add src/services/billing/ledger.rs && git commit -m "feat(billing): user ledger list + summary queries"`
|
||||
|
||||
### Task 12:user 侧 billing 接口
|
||||
|
||||
**Files:** Modify `src/routes/user/mod.rs`(加 2 路由 + 2 handler)
|
||||
|
||||
- [ ] **Step1 失败测试**:user mod tests(若有 test_state/建表 helper 则复用)——构造 user JWT + ledger 数据,GET /api/user/billing/ledger 返回行,/summary 返回聚合。若 user mod 无现成集成测试框架,改为直接单测 handler 逻辑或跳测试靠 ledger.rs 的 Task11 测试覆盖,仅验证编译 + 路由注册。
|
||||
- [ ] **Step2** FAIL/编译失败。
|
||||
- [ ] **Step3 实现**:
|
||||
```rust
|
||||
async fn get_billing_ledger(State(state): State<AppState>, headers: HeaderMap, Query(q): Query<BillingLedgerQuery>) -> AppResult<Json<Value>> {
|
||||
let user_id = current_user_id(&headers, &state)?;
|
||||
let rows = crate::services::billing::ledger::list_user_ledger(
|
||||
&state.db, user_id, q.track.as_deref(), q.entry_type.as_deref(),
|
||||
q.limit.unwrap_or(100), q.offset.unwrap_or(0)).await?;
|
||||
Ok(Json(json!({ "success": true, "records": rows })))
|
||||
}
|
||||
async fn get_billing_summary(State(state): State<AppState>, headers: HeaderMap) -> AppResult<Json<Value>> {
|
||||
let user_id = current_user_id(&headers, &state)?;
|
||||
let agg = crate::services::billing::ledger::summarize_user_ledger(&state.db, user_id).await?;
|
||||
Ok(Json(json!({ "success": true, "summary": agg })))
|
||||
}
|
||||
```
|
||||
+ `BillingLedgerQuery{ track:Option<String>, entry_type:Option<String>, limit:Option<i64>, offset:Option<i64> }`(serde Deserialize, rename_all camelCase 视前端传参)。
|
||||
+ routes() 注册 `.route("/api/user/billing/ledger", get(get_billing_ledger)).route("/api/user/billing/summary", get(get_billing_summary))`。
|
||||
- [ ] **Step4** `cargo test -p boosapi --lib 2>&1|command grep "test result"` + `cargo build -j 2`。
|
||||
- [ ] **Step5** `git add src/routes/user/mod.rs && git commit -m "feat(billing): user billing ledger + summary endpoints"`
|
||||
|
||||
### Task 13:前端 api + UsageRecords 双轨展示
|
||||
|
||||
**Files:** Modify `src/web/api.ts`、`src/web/pages/UsageRecords.tsx`
|
||||
|
||||
- [ ] **Step1**:api.ts 的 userApi 加 `getBillingLedger(params)` → GET /api/user/billing/ledger、`getBillingSummary()` → /summary。
|
||||
- [ ] **Step2**:UsageRecords.tsx 加一个"积分/USD"双轨切换 tab + 聚合汇总卡(用 getBillingSummary)+ ledger 明细列表(getBillingLedger)。保留现有 logs 视图作为 USD 明细来源之一,新增积分明细来自 ledger。具体 UI 复用现有 usage-records-* 样式。
|
||||
- [ ] **Step3**:`npm run typecheck:web` 干净 + 现有 UsageRecords 测试不破坏(`npx vitest run src/web/pages/UsageRecords`)。
|
||||
- [ ] **Step4** `git add src/web/api.ts src/web/pages/UsageRecords.tsx && git commit -m "feat(billing): UsageRecords dual-track ledger view"`
|
||||
|
||||
### Task 14:第3期回归
|
||||
- [ ] `cargo test 2>&1|command grep "test result"` 全ok;`cargo build -j 2`;`npm run typecheck`。
|
||||
|
||||
Reference in New Issue
Block a user