Skip to content

Commit bcfc225

Browse files
authored
fix: use local timezone and robust DST handling in usage stats (#500)
- Change from UTC to local timezone for daily/hourly trends - Use SQLite 'localtime' modifier for date grouping - Replace single().unwrap() with earliest().unwrap_or_else() to handle DST transition edge cases gracefully
1 parent 443e23c commit bcfc225

File tree

1 file changed

+31
-14
lines changed

1 file changed

+31
-14
lines changed

src-tauri/src/services/usage_stats.rs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
use crate::database::{lock_conn, Database};
66
use crate::error::AppError;
7-
use chrono::{Duration, Utc};
7+
use chrono::{Duration, Local, TimeZone};
88
use rusqlite::{params, Connection, OptionalExtension};
99
use serde::{Deserialize, Serialize};
1010
use serde_json::Value;
@@ -186,8 +186,17 @@ impl Database {
186186
let conn = lock_conn!(self.conn);
187187

188188
if days <= 1 {
189+
let today = Local::now().date_naive();
190+
let start_of_today = today.and_hms_opt(0, 0, 0).unwrap();
191+
// 使用 earliest() 处理 DST 切换时的歧义时间,fallback 到当前时间减一天
192+
let start_ts = Local
193+
.from_local_datetime(&start_of_today)
194+
.earliest()
195+
.unwrap_or_else(|| Local::now() - Duration::days(1))
196+
.timestamp();
197+
189198
let sql = "SELECT
190-
strftime('%Y-%m-%dT%H:00:00Z', datetime(created_at, 'unixepoch')) as bucket,
199+
strftime('%Y-%m-%dT%H:00:00', datetime(created_at, 'unixepoch', 'localtime')) as bucket,
191200
COUNT(*) as request_count,
192201
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0) as total_cost,
193202
COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
@@ -196,12 +205,12 @@ impl Database {
196205
COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
197206
COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens
198207
FROM proxy_request_logs
199-
WHERE created_at >= strftime('%s', 'now', '-1 day')
208+
WHERE created_at >= ?
200209
GROUP BY bucket
201210
ORDER BY bucket ASC";
202211

203212
let mut stmt = conn.prepare(sql)?;
204-
let rows = stmt.query_map([], |row| {
213+
let rows = stmt.query_map([start_ts], |row| {
205214
Ok(DailyStats {
206215
date: row.get(0)?,
207216
request_count: row.get::<_, i64>(1)? as u64,
@@ -221,12 +230,11 @@ impl Database {
221230
}
222231

223232
let mut stats = Vec::new();
224-
let today = Utc::now().date_naive();
225233
for hour in 0..24 {
226234
let bucket = today
227235
.and_hms_opt(hour, 0, 0)
228236
.unwrap()
229-
.format("%Y-%m-%dT%H:00:00Z")
237+
.format("%Y-%m-%dT%H:00:00")
230238
.to_string();
231239

232240
if let Some(stat) = buckets.remove(&bucket) {
@@ -246,8 +254,19 @@ impl Database {
246254
}
247255
Ok(stats)
248256
} else {
257+
let today = Local::now().date_naive();
258+
let start_day =
259+
today - Duration::days((days.saturating_sub(1)) as i64);
260+
let start_of_window = start_day.and_hms_opt(0, 0, 0).unwrap();
261+
// 使用 earliest() 处理 DST 切换时的歧义时间,fallback 到当前时间减 days 天
262+
let start_ts = Local
263+
.from_local_datetime(&start_of_window)
264+
.earliest()
265+
.unwrap_or_else(|| Local::now() - Duration::days(days as i64))
266+
.timestamp();
267+
249268
let sql = "SELECT
250-
date(created_at, 'unixepoch') as bucket,
269+
strftime('%Y-%m-%dT00:00:00', datetime(created_at, 'unixepoch', 'localtime')) as bucket,
251270
COUNT(*) as request_count,
252271
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0) as total_cost,
253272
COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
@@ -256,12 +275,12 @@ impl Database {
256275
COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
257276
COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens
258277
FROM proxy_request_logs
259-
WHERE created_at >= strftime('%s', 'now', ?)
278+
WHERE created_at >= ?
260279
GROUP BY bucket
261280
ORDER BY bucket ASC";
262281

263282
let mut stmt = conn.prepare(sql)?;
264-
let rows = stmt.query_map([format!("-{days} days")], |row| {
283+
let rows = stmt.query_map([start_ts], |row| {
265284
Ok(DailyStats {
266285
date: row.get(0)?,
267286
request_count: row.get::<_, i64>(1)? as u64,
@@ -281,12 +300,10 @@ impl Database {
281300
}
282301

283302
let mut stats = Vec::new();
284-
let start_day =
285-
Utc::now().date_naive() - Duration::days((days.saturating_sub(1)) as i64);
286303

287304
for i in 0..days {
288305
let day = start_day + Duration::days(i as i64);
289-
let key = day.format("%Y-%m-%d").to_string();
306+
let key = day.format("%Y-%m-%dT00:00:00").to_string();
290307
if let Some(stat) = map.remove(&key) {
291308
stats.push(stat);
292309
} else {
@@ -617,7 +634,7 @@ impl Database {
617634
"SELECT COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0)
618635
FROM proxy_request_logs
619636
WHERE provider_id = ? AND app_type = ?
620-
AND date(created_at, 'unixepoch') = date('now')",
637+
AND date(datetime(created_at, 'unixepoch', 'localtime')) = date('now', 'localtime')",
621638
params![provider_id, app_type],
622639
|row| row.get(0),
623640
)
@@ -629,7 +646,7 @@ impl Database {
629646
"SELECT COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0)
630647
FROM proxy_request_logs
631648
WHERE provider_id = ? AND app_type = ?
632-
AND strftime('%Y-%m', created_at, 'unixepoch') = strftime('%Y-%m', 'now')",
649+
AND strftime('%Y-%m', datetime(created_at, 'unixepoch', 'localtime')) = strftime('%Y-%m', 'now', 'localtime')",
633650
params![provider_id, app_type],
634651
|row| row.get(0),
635652
)

0 commit comments

Comments
 (0)