Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/i18n_pack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ const ZH_KEY_COUNT: usize = 786;
const EN_KEY_COUNT: usize = 786;
const STATIC_ATTR_COUNT: usize = 330;
const STATIC_ATTR_DISTINCT: usize = 305;
const T_LITERAL_COUNT: usize = 535;
const T_LITERAL_DISTINCT: usize = 429;
const T_LITERAL_COUNT: usize = 538;
const T_LITERAL_DISTINCT: usize = 431;

/// 切出语言包区段(起点标记 → 终点标记,含起点)。
fn pack_region<'a>(src: &'a str, start_mark: &str, end_mark: &str) -> &'a str {
Expand Down
95 changes: 95 additions & 0 deletions src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1904,6 +1904,101 @@ mod tests {
assert_eq!(s, StatusCode::BAD_REQUEST);
}

/// `total_txs` —— 运营概览「交易量」卡的数据源。
///
/// 这个字段自 `85982e8`(PR #80)起就在算、就在返回(`ops.rs:101`/`:175`),
/// 但**两侧都没有任何断言**:v1.22 的零 mock 重构(`89963f3`)删掉 mock 分支的
/// 那张卡后,前端漏了重接,而这个字段照旧返回,于是谁都发现不了。
/// 本测试把「返回了」与「口径是全库 / 累计全部类型」同时钉住。
#[tokio::test]
async fn ops_runtime_total_txs_is_global_and_all_types() {
let st = test_state("opstxs");
let ops_bearer = login_bearer(&st, "ops@aitokenpool.local", "ops1234").await;
// 空库 → 0(先证有值,再证口径,避免「恒为 0 也算过」)
let (s, body) = get(st.clone(), "/api/ops/runtime", Some(&ops_bearer)).await;
assert_eq!(s, StatusCode::OK, "ops runtime 应 200: {body}");
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["total_txs"], 0, "空库应为 0: {body}");

// 造 3 条、**跨两个用户**:admin 的 topup(走真实充值接口)+ demo 的 consume / earn。
// 跨用户是刻意的:这样「全局 3」既不同于 demo 自己的 2,也不同于 consume-only 的 1,
// 两种退化的口径都能被这条断言抓住。
let uid = |email: &str| -> i64 {
st.db
.lock()
.unwrap()
.query_row("SELECT id FROM users WHERE email = ?1", [email], |r| {
r.get(0)
})
.unwrap()
};
let demo_id = uid("demo@aitokenpool.local");
let admin_id = uid("admin@aitokenpool.local");
let (s2, body2) = post(
st.clone(),
"/api/ops/credits",
&format!(r#"{{"user_id":{admin_id},"amount":77}}"#),
Some(&ops_bearer),
)
.await;
assert_eq!(s2, StatusCode::OK, "充值应 200: {body2}");
{
let conn = st.db.lock().unwrap();
conn.execute(
"INSERT INTO transactions (user_id, counterpart, key_id, model, tokens, pts, type, status) \
VALUES (?1, 'x', 1, 'm', 10, -2.0, 'consume', '成功')",
[demo_id],
)
.unwrap();
conn.execute(
"INSERT INTO transactions (user_id, counterpart, key_id, model, tokens, pts, type, status) \
VALUES (?1, 'x', 1, 'm', 10, 1.0, 'earn', '成功')",
[demo_id],
)
.unwrap();
}
let (_, body) = get(st.clone(), "/api/ops/runtime", Some(&ops_bearer)).await;
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(
v["total_txs"], 3,
"全库 3 条(admin 的 topup + demo 的 consume/earn)——'累计全部类型': {body}"
);
// 反面一:按 type 过滤 → 只有 1 条
let consume_only: i64 = st
.db
.lock()
.unwrap()
.query_row(
"SELECT COUNT(*) FROM transactions WHERE type = 'consume'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(consume_only, 1, "前置条件:consume 仅 1 条");
assert_ne!(
v["total_txs"].as_i64().unwrap(),
consume_only,
"total_txs 不能退化成按 type 过滤: {body}"
);
// 反面二:按用户过滤 → 最多 2 条(demo)
let max_per_user: i64 = st
.db
.lock()
.unwrap()
.query_row(
"SELECT COALESCE(MAX(c), 0) FROM (SELECT COUNT(*) AS c FROM transactions GROUP BY user_id)",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(max_per_user, 2, "前置条件:单用户最多 2 条");
assert_ne!(
v["total_txs"].as_i64().unwrap(),
max_per_user,
"total_txs 不能退化成按用户过滤: {body}"
);
}

#[tokio::test]
async fn usage_three_group_aggregation() {
let st = test_state("usage3");
Expand Down
1 change: 1 addition & 0 deletions ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,4 +346,5 @@ ui/
- **page-head crumb**:8 个视图统一 `.crumb`(`/ 设置` `/ 管理视图` `/ 运营视图`),与仪表盘/市场/共享/钱包/交易一致;
- **管理视图**(4 tab 不变):成员管理 pane 增加 `#emp-search` 成员搜索(成员名 / 邮箱 / 部门,`hl()` 高亮)+ 表格「角色 / 部门 / 永久点数 / 赠送点数 / 可用」列;用量报表用 `.card-grid-3` 三栏;组织管理与模型管理的工具栏为「搜索 + `.grow` + 主按钮(`btn-sm`)」,与原型一致;
- **运营视图**(2 tab 不变):运行概览新增四张卡片——**服务版本**(`/api/ops/runtime` 的 `version`,取自 `env!("CARGO_PKG_VERSION")`,与 `/healthz` 同源;前端**不得**写死版本号)与**运行时长**(`uptime_secs` / `uptime_days` / `uptime_hours` / `uptime_minutes` / `uptime_secs_rest`,进位在后端 `split_uptime` 完成,前端 `fmtUptime` 只挑「最高两个非零位」并取 `ops.uptime.{days,hours,minutes,seconds}` 单位)、**今日调用量(按小时)** `.bar-list`(`today_hours`,服务端 0-23 全量补零;GROUP BY 会省略无调用的小时,不补零会让柱子整体左移,与交易页 `txTrendDays` 同款坑)与**上游 key 健康** `.mini-list`(`key_health` 按厂商聚合 total/on/off,三态 pill:健康 / N 个异常 / 全部失败);成员充值的搜索框移到卡片标题行右侧(原型 `.spread`);
- **交易量卡(第 9 张,回归修复)**:`total_txs`(`ops.stats.trades` + `cnt.trades` + `ops.stats.trades.sub`)自 `85982e8`(PR #80)起就在 `/api/ops/runtime` 返回(全库 `COUNT(*)`,累计**全部类型**),但 v1.22 零 mock 重构 `89963f3` 删掉 mock 分支那张卡时漏了重接。**这不是原型对齐**——原型没有这张卡(原型 4 张,实现 9 张,多出的卡是刻意的)。值一律取自响应,**不取** `D.TRANSACTIONS`;
- **零 mock 不破**:以上数据全部来自真实端点,加载失败仍走空态 + 重试(`.mini-item` / `.bar-row` 只在有真实数据时才渲染)。
10 changes: 5 additions & 5 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AITokenPool</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%234ecdc4'/%3E%3Cstop offset='1' stop-color='%232a9d8f'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='64' height='64' rx='14' fill='url(%23g)'/%3E%3Ctext x='32' y='43' font-family='-apple-system,Segoe UI,Arial,sans-serif' font-size='30' font-weight='800' text-anchor='middle' fill='%23062021'%3EAT%3C/text%3E%3C/svg%3E">
<link rel="stylesheet" href="css/style.css?v=20260912-1">
<link rel="stylesheet" href="css/style.css?v=20260912-2">
</head>
<body>

Expand Down Expand Up @@ -842,9 +842,9 @@ <h3 id="chat-title">使用模型</h3>
</div>
</div>

<script src="js/api.js?v=20260912-1"></script>
<script src="js/data.js?v=20260912-1"></script>
<script src="js/i18n.js?v=20260912-1"></script>
<script src="js/app.js?v=20260912-1"></script>
<script src="js/api.js?v=20260912-2"></script>
<script src="js/data.js?v=20260912-2"></script>
<script src="js/i18n.js?v=20260912-2"></script>
<script src="js/app.js?v=20260912-2"></script>
</body>
</html>
4 changes: 4 additions & 0 deletions ui/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2561,6 +2561,10 @@
stat(T("ops.stats.uptime"), esc(fmtUptime(rt)), T("ops.stats.uptime.sub")),
stat(T("ops.stats.users"), T("cnt.people", { n: rt.users }), T("ops.stats.users.sub")),
stat(T("ops.stats.keys"), T("cnt.keys", { n: rt.active_keys }), T("ops.stats.keys.sub.on")),
// 交易量:total_txs 自 85982e8(PR #80)起就一直在算、在返回(全库 COUNT(*)),
// 但 v1.22 零 mock 重构(89963f3)删掉 mock 分支那一行时漏了重接——这是**回归**,
// 不是新功能,也不是原型对齐(原型没有这张卡)。零 mock 不破:值取自响应,不取 D.TRANSACTIONS。
stat(T("ops.stats.trades"), T("cnt.trades", { n: rt.total_txs }), T("ops.stats.trades.sub")),
stat(T("ops.stats.calls"), T("cnt.calls", { n: rt.month_calls }), T("ops.stats.calls.sub")),
stat(T("ops.stats.in"), "+" + D.fmt(rt.month_in) + " " + T("common.points"), T("ops.stats.in.sub")),
stat(T("ops.stats.out"), "-" + D.fmt(rt.month_out) + " " + T("common.points"), T("ops.stats.out.sub")),
Expand Down
Loading