xiaolongmr commited on
Commit
5bee2d2
·
1 Parent(s): fbc3dbc

用量流水库接入存储桶快照持久化(与账号/卡密库同一机制)

Browse files
services/credit_ledger_service.py CHANGED
@@ -30,14 +30,16 @@ except ImportError: # pragma: no cover - Python 3.9+ normally has zoneinfo.
30
  ZoneInfo = None # type: ignore[assignment]
31
 
32
  from services.config import DATA_DIR
 
33
 
34
- # 落盘位置与项目其它运行时数据保持一致:优先 CHATGPT2API_CREDIT_LEDGER_FILE,
35
- # 否则跟随 DATA_DIR(受 CHATGPT2API_PERSIST_DIR / CHATGPT2API_DATA_DIR 控制)。
36
- # ⚠️ 容器类部署(HF Space 免费版)磁盘是临时的,重建即清空;只有把 DATA_DIR
37
- # 指到持久卷(如 HF 付费持久存储的 /data)或用外部存储才能跨重启保留。
 
38
  LEDGER_FILE = Path(
39
  str(os.getenv("CHATGPT2API_CREDIT_LEDGER_FILE") or "").strip()
40
- or (DATA_DIR / "credit_ledger.db")
41
  )
42
 
43
  _CHINA_TZ = ZoneInfo("Asia/Shanghai") if ZoneInfo else timezone(timedelta(hours=8))
 
30
  ZoneInfo = None # type: ignore[assignment]
31
 
32
  from services.config import DATA_DIR
33
+ from services.sqlite_mirror_service import sqlite_mirror_service
34
 
35
+ # 落盘位置:优先 CHATGPT2API_CREDIT_LEDGER_FILE,否则交给 sqlite 镜像服务 ——
36
+ # 与号池账号库(accounts.db)、用户 / 卡密库(users.db)完全同一套机制:
37
+ # 活库跑在容器本地磁盘,定时把通过完整性校验的快照写进挂载的存储桶
38
+ # (HF Storage Bucket 挂在 /app/persist),容器重建后启动时自动从最新快照恢复,
39
+ # 所以重新部署 / 重启都不会清空。
40
  LEDGER_FILE = Path(
41
  str(os.getenv("CHATGPT2API_CREDIT_LEDGER_FILE") or "").strip()
42
+ or sqlite_mirror_service.runtime_file(DATA_DIR, "credit_ledger.db")
43
  )
44
 
45
  _CHINA_TZ = ZoneInfo("Asia/Shanghai") if ZoneInfo else timezone(timedelta(hours=8))
services/sqlite_mirror_service.py CHANGED
@@ -31,6 +31,12 @@ _SCHEMAS: dict[str, tuple[str, tuple[str, ...]]] = {
31
  "CREATE TABLE IF NOT EXISTS shop_ledger (id INTEGER PRIMARY KEY AUTOINCREMENT, ledger_id VARCHAR(64) NOT NULL UNIQUE, created_at VARCHAR(64), user_id VARCHAR(255), data TEXT NOT NULL)",
32
  ("id", "ledger_id", "created_at", "user_id", "data"),
33
  ),
 
 
 
 
 
 
34
  }
35
 
36
 
 
31
  "CREATE TABLE IF NOT EXISTS shop_ledger (id INTEGER PRIMARY KEY AUTOINCREMENT, ledger_id VARCHAR(64) NOT NULL UNIQUE, created_at VARCHAR(64), user_id VARCHAR(255), data TEXT NOT NULL)",
32
  ("id", "ledger_id", "created_at", "user_id", "data"),
33
  ),
34
+ # 生图用量流水(data/credit_ledger.db)。它不在上面那几个库里,但同样跑在这套
35
+ # 镜像机制下;登记 schema 是为了数据库损坏时能被尽力抢救,而不是只能丢掉。
36
+ "credit_usage": (
37
+ "CREATE TABLE IF NOT EXISTS credit_usage (id INTEGER PRIMARY KEY AUTOINCREMENT, created_ts REAL NOT NULL, day TEXT NOT NULL, identity_id TEXT NOT NULL, role TEXT NOT NULL, model TEXT NOT NULL, status TEXT NOT NULL, image_count INTEGER NOT NULL DEFAULT 0, credits REAL NOT NULL DEFAULT 0, unit_price REAL NOT NULL DEFAULT 0, task_id TEXT NOT NULL DEFAULT '')",
38
+ ("id", "created_ts", "day", "identity_id", "role", "model", "status", "image_count", "credits", "unit_price", "task_id"),
39
+ ),
40
  }
41
 
42
 
test/test_credit_ledger_service.py CHANGED
@@ -164,19 +164,21 @@ class CreditLedgerServiceTests(unittest.TestCase):
164
 
165
 
166
  class CreditLedgerPathTests(unittest.TestCase):
167
- """流水库必须跟随 DATA_DIR:容器部署(HF Space)只有把 DATA_DIR 指到持久卷才能跨重启保留。"""
168
 
169
- def test_path_follows_data_dir_with_env_override(self) -> None:
170
  import importlib
171
  import os
172
  from unittest.mock import patch
173
 
174
  import services.credit_ledger_service as module
 
175
 
176
  try:
177
  with patch.dict(os.environ, {"CHATGPT2API_CREDIT_LEDGER_FILE": ""}, clear=False):
178
  reloaded = importlib.reload(module)
179
- self.assertEqual(reloaded.LEDGER_FILE, reloaded.DATA_DIR / "credit_ledger.db")
 
180
  override = Path(tempfile.mkdtemp()) / "custom.db"
181
  with patch.dict(os.environ, {"CHATGPT2API_CREDIT_LEDGER_FILE": str(override)}):
182
  reloaded = importlib.reload(module)
 
164
 
165
 
166
  class CreditLedgerPathTests(unittest.TestCase):
167
+ """流水库必须与账号库 / 用户卡密库走同一套持久化(sqlite 镜像 → 存储桶)。"""
168
 
169
+ def test_path_uses_sqlite_mirror_and_env_override(self) -> None:
170
  import importlib
171
  import os
172
  from unittest.mock import patch
173
 
174
  import services.credit_ledger_service as module
175
+ from services.sqlite_mirror_service import sqlite_mirror_service
176
 
177
  try:
178
  with patch.dict(os.environ, {"CHATGPT2API_CREDIT_LEDGER_FILE": ""}, clear=False):
179
  reloaded = importlib.reload(module)
180
+ expected = sqlite_mirror_service.runtime_file(reloaded.DATA_DIR, "credit_ledger.db")
181
+ self.assertEqual(reloaded.LEDGER_FILE, expected)
182
  override = Path(tempfile.mkdtemp()) / "custom.db"
183
  with patch.dict(os.environ, {"CHATGPT2API_CREDIT_LEDGER_FILE": str(override)}):
184
  reloaded = importlib.reload(module)