245b85ad27
Co-authored-by: Cursor <cursoragent@cursor.com>
187 lines
5.5 KiB
Python
187 lines
5.5 KiB
Python
"""账户流水 SQLite 缓存."""
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from typing import Any, Optional
|
|
|
|
from lib.account_ledger.account_ledger_normalize import PAGE_SIZE, VALID_ACCOUNTS
|
|
|
|
|
|
def ensure_account_ledger_tables(conn) -> None:
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS account_ledger_entries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
account TEXT NOT NULL,
|
|
ccy TEXT NOT NULL,
|
|
amount REAL NOT NULL,
|
|
balance_after REAL,
|
|
kind TEXT,
|
|
raw_type TEXT,
|
|
symbol TEXT,
|
|
ref_id TEXT NOT NULL,
|
|
ts_ms INTEGER NOT NULL,
|
|
note TEXT,
|
|
synced_at REAL,
|
|
UNIQUE(account, ref_id, ccy, ts_ms)
|
|
)
|
|
"""
|
|
)
|
|
conn.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_account_ledger_acc_ts "
|
|
"ON account_ledger_entries(account, ts_ms DESC)"
|
|
)
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS account_ledger_meta (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT
|
|
)
|
|
"""
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def meta_get(conn, key: str, default: str = "") -> str:
|
|
row = conn.execute(
|
|
"SELECT value FROM account_ledger_meta WHERE key=?", (key,)
|
|
).fetchone()
|
|
if not row:
|
|
return default
|
|
try:
|
|
return str(row[0] if not hasattr(row, "keys") else row["value"])
|
|
except Exception:
|
|
return default
|
|
|
|
|
|
def meta_set(conn, key: str, value: str) -> None:
|
|
conn.execute(
|
|
"INSERT INTO account_ledger_meta(key, value) VALUES(?, ?) "
|
|
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
(key, str(value)),
|
|
)
|
|
|
|
|
|
def upsert_entries(conn, rows: list[dict[str, Any]]) -> int:
|
|
if not rows:
|
|
return 0
|
|
now = time.time()
|
|
n = 0
|
|
for r in rows:
|
|
try:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO account_ledger_entries(
|
|
account, ccy, amount, balance_after, kind, raw_type,
|
|
symbol, ref_id, ts_ms, note, synced_at
|
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
ON CONFLICT(account, ref_id, ccy, ts_ms) DO UPDATE SET
|
|
amount=excluded.amount,
|
|
balance_after=excluded.balance_after,
|
|
kind=excluded.kind,
|
|
raw_type=excluded.raw_type,
|
|
symbol=excluded.symbol,
|
|
note=excluded.note,
|
|
synced_at=excluded.synced_at
|
|
""",
|
|
(
|
|
r["account"],
|
|
r["ccy"],
|
|
float(r["amount"]),
|
|
r.get("balance_after"),
|
|
r.get("kind") or "other",
|
|
r.get("raw_type") or "",
|
|
r.get("symbol") or "",
|
|
r["ref_id"],
|
|
int(r["ts_ms"]),
|
|
r.get("note") or "",
|
|
now,
|
|
),
|
|
)
|
|
n += 1
|
|
except Exception:
|
|
continue
|
|
conn.commit()
|
|
return n
|
|
|
|
|
|
def query_entries(
|
|
conn,
|
|
*,
|
|
account: str,
|
|
start_ms: int,
|
|
end_ms: int,
|
|
page: int = 1,
|
|
page_size: int = PAGE_SIZE,
|
|
currencies: Optional[list[str]] = None,
|
|
) -> dict[str, Any]:
|
|
acc = (account or "").strip().lower()
|
|
if acc not in VALID_ACCOUNTS:
|
|
return {"items": [], "total": 0, "page": 1, "page_size": page_size, "pages": 0}
|
|
page = max(1, int(page or 1))
|
|
page_size = max(1, min(50, int(page_size or PAGE_SIZE)))
|
|
start_ms = int(start_ms)
|
|
end_ms = int(end_ms)
|
|
params: list[Any] = [acc, start_ms, end_ms]
|
|
ccy_sql = ""
|
|
if currencies:
|
|
ccy_list = [c.strip().upper() for c in currencies if c and str(c).strip()]
|
|
if ccy_list:
|
|
placeholders = ",".join("?" for _ in ccy_list)
|
|
ccy_sql = f" AND ccy IN ({placeholders})"
|
|
params.extend(ccy_list)
|
|
total = conn.execute(
|
|
f"SELECT COUNT(*) FROM account_ledger_entries "
|
|
f"WHERE account=? AND ts_ms>=? AND ts_ms<=?{ccy_sql}",
|
|
params,
|
|
).fetchone()[0]
|
|
total = int(total or 0)
|
|
pages = (total + page_size - 1) // page_size if total else 0
|
|
if pages and page > pages:
|
|
page = pages
|
|
offset = (page - 1) * page_size
|
|
rows = conn.execute(
|
|
f"""
|
|
SELECT account, ccy, amount, balance_after, kind, raw_type, symbol,
|
|
ref_id, ts_ms, note
|
|
FROM account_ledger_entries
|
|
WHERE account=? AND ts_ms>=? AND ts_ms<=?{ccy_sql}
|
|
ORDER BY ts_ms DESC, id DESC
|
|
LIMIT ? OFFSET ?
|
|
""",
|
|
params + [page_size, offset],
|
|
).fetchall()
|
|
items = []
|
|
for r in rows:
|
|
if hasattr(r, "keys"):
|
|
d = {k: r[k] for k in r.keys()}
|
|
else:
|
|
d = {
|
|
"account": r[0],
|
|
"ccy": r[1],
|
|
"amount": r[2],
|
|
"balance_after": r[3],
|
|
"kind": r[4],
|
|
"raw_type": r[5],
|
|
"symbol": r[6],
|
|
"ref_id": r[7],
|
|
"ts_ms": r[8],
|
|
"note": r[9],
|
|
}
|
|
from lib.account_ledger.account_ledger_normalize import kind_label_zh
|
|
|
|
d["kind_label"] = kind_label_zh(d.get("kind") or "")
|
|
items.append(d)
|
|
return {
|
|
"items": items,
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": page_size,
|
|
"pages": pages,
|
|
}
|
|
|
|
|
|
def prune_older_than(conn, min_ts_ms: int) -> None:
|
|
conn.execute("DELETE FROM account_ledger_entries WHERE ts_ms < ?", (int(min_ts_ms),))
|
|
conn.commit()
|