index_debt_mixin.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. IndexDebtMixin extracted from IndexManager.
  5. """
  6. from __future__ import annotations
  7. import json
  8. from datetime import datetime
  9. from typing import Any, Dict, List, Optional
  10. class IndexDebtMixin:
  11. def create_override_contract(self, contract: OverrideContractMeta) -> int:
  12. """
  13. 创建或更新 Override Contract
  14. 使用 SQLite 的 INSERT ... ON CONFLICT ... DO UPDATE 实现原子 UPSERT:
  15. - 并发安全,无需显式锁
  16. - 保持 id 不变,避免 chase_debt.override_contract_id 悬挂
  17. - 完全冻结终态:已 fulfilled/cancelled 的合约所有字段都不会被修改
  18. 兼容性:支持 SQLite 3.24+(ON CONFLICT 语法),不依赖 RETURNING(3.35+)
  19. 返回合约 ID
  20. """
  21. with self._get_conn() as conn:
  22. cursor = conn.cursor()
  23. # 使用 ON CONFLICT 实现原子 UPSERT(SQLite 3.24+)
  24. # 终态完全冻结:fulfilled/cancelled 状态下所有字段都保持不变
  25. cursor.execute(
  26. """
  27. INSERT INTO override_contracts
  28. (chapter, constraint_type, constraint_id, rationale_type,
  29. rationale_text, payback_plan, due_chapter, status)
  30. VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  31. ON CONFLICT(chapter, constraint_type, constraint_id) DO UPDATE SET
  32. rationale_type = CASE
  33. WHEN override_contracts.status IN ('fulfilled', 'cancelled')
  34. THEN override_contracts.rationale_type
  35. ELSE excluded.rationale_type
  36. END,
  37. rationale_text = CASE
  38. WHEN override_contracts.status IN ('fulfilled', 'cancelled')
  39. THEN override_contracts.rationale_text
  40. ELSE excluded.rationale_text
  41. END,
  42. payback_plan = CASE
  43. WHEN override_contracts.status IN ('fulfilled', 'cancelled')
  44. THEN override_contracts.payback_plan
  45. ELSE excluded.payback_plan
  46. END,
  47. due_chapter = CASE
  48. WHEN override_contracts.status IN ('fulfilled', 'cancelled')
  49. THEN override_contracts.due_chapter
  50. ELSE excluded.due_chapter
  51. END,
  52. status = CASE
  53. WHEN override_contracts.status IN ('fulfilled', 'cancelled')
  54. THEN override_contracts.status
  55. ELSE excluded.status
  56. END
  57. """,
  58. (
  59. contract.chapter,
  60. contract.constraint_type,
  61. contract.constraint_id,
  62. contract.rationale_type,
  63. contract.rationale_text,
  64. contract.payback_plan,
  65. contract.due_chapter,
  66. contract.status,
  67. ),
  68. )
  69. # 不使用 RETURNING(需要 SQLite 3.35+),改用查询获取 id
  70. cursor.execute(
  71. """
  72. SELECT id FROM override_contracts
  73. WHERE chapter = ? AND constraint_type = ? AND constraint_id = ?
  74. """,
  75. (contract.chapter, contract.constraint_type, contract.constraint_id),
  76. )
  77. row = cursor.fetchone()
  78. if not row:
  79. # UPSERT 后查不到记录是异常情况,不应发生
  80. raise RuntimeError(
  81. f"Override Contract UPSERT 后无法获取 id: "
  82. f"chapter={contract.chapter}, type={contract.constraint_type}, "
  83. f"id={contract.constraint_id}"
  84. )
  85. contract_id = row[0]
  86. conn.commit()
  87. return contract_id
  88. def get_pending_overrides(self, before_chapter: int = None) -> List[Dict]:
  89. """获取待偿还的Override Contracts"""
  90. with self._get_conn() as conn:
  91. cursor = conn.cursor()
  92. if before_chapter:
  93. cursor.execute(
  94. """
  95. SELECT * FROM override_contracts
  96. WHERE status = 'pending' AND due_chapter <= ?
  97. ORDER BY due_chapter ASC
  98. """,
  99. (before_chapter,),
  100. )
  101. else:
  102. cursor.execute("""
  103. SELECT * FROM override_contracts
  104. WHERE status = 'pending'
  105. ORDER BY due_chapter ASC
  106. """)
  107. return [dict(row) for row in cursor.fetchall()]
  108. def get_overdue_overrides(self, current_chapter: int) -> List[Dict]:
  109. """获取已逾期的Override Contracts"""
  110. with self._get_conn() as conn:
  111. cursor = conn.cursor()
  112. cursor.execute(
  113. """
  114. SELECT * FROM override_contracts
  115. WHERE status = 'pending' AND due_chapter < ?
  116. ORDER BY due_chapter ASC
  117. """,
  118. (current_chapter,),
  119. )
  120. return [dict(row) for row in cursor.fetchall()]
  121. def fulfill_override(self, contract_id: int) -> bool:
  122. """标记Override Contract为已偿还"""
  123. with self._get_conn() as conn:
  124. cursor = conn.cursor()
  125. cursor.execute(
  126. """
  127. UPDATE override_contracts SET
  128. status = 'fulfilled',
  129. fulfilled_at = CURRENT_TIMESTAMP
  130. WHERE id = ?
  131. """,
  132. (contract_id,),
  133. )
  134. conn.commit()
  135. return cursor.rowcount > 0
  136. def get_chapter_overrides(self, chapter: int) -> List[Dict]:
  137. """获取某章创建的Override Contracts"""
  138. with self._get_conn() as conn:
  139. cursor = conn.cursor()
  140. cursor.execute(
  141. """
  142. SELECT * FROM override_contracts WHERE chapter = ?
  143. """,
  144. (chapter,),
  145. )
  146. return [dict(row) for row in cursor.fetchall()]
  147. # ==================== v5.3 追读力债务操作 ====================
  148. def create_debt(self, debt: ChaseDebtMeta) -> int:
  149. """
  150. 创建追读力债务
  151. 返回债务 ID
  152. """
  153. with self._get_conn() as conn:
  154. cursor = conn.cursor()
  155. cursor.execute(
  156. """
  157. INSERT INTO chase_debt
  158. (debt_type, original_amount, current_amount, interest_rate,
  159. source_chapter, due_chapter, override_contract_id, status)
  160. VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  161. """,
  162. (
  163. debt.debt_type,
  164. debt.original_amount,
  165. debt.current_amount,
  166. debt.interest_rate,
  167. debt.source_chapter,
  168. debt.due_chapter,
  169. debt.override_contract_id if debt.override_contract_id else None,
  170. debt.status,
  171. ),
  172. )
  173. conn.commit()
  174. debt_id = cursor.lastrowid
  175. # 记录创建事件
  176. self._record_debt_event(
  177. cursor,
  178. debt_id,
  179. "created",
  180. debt.original_amount,
  181. debt.source_chapter,
  182. f"创建债务: {debt.debt_type}",
  183. )
  184. conn.commit()
  185. return debt_id
  186. def get_active_debts(self) -> List[Dict]:
  187. """获取所有活跃债务"""
  188. with self._get_conn() as conn:
  189. cursor = conn.cursor()
  190. cursor.execute("""
  191. SELECT * FROM chase_debt
  192. WHERE status = 'active'
  193. ORDER BY due_chapter ASC
  194. """)
  195. return [dict(row) for row in cursor.fetchall()]
  196. def get_overdue_debts(self, current_chapter: int) -> List[Dict]:
  197. """获取已逾期的债务(包括 active 但已过期的,以及已标记为 overdue 的)"""
  198. with self._get_conn() as conn:
  199. cursor = conn.cursor()
  200. cursor.execute(
  201. """
  202. SELECT * FROM chase_debt
  203. WHERE (status = 'overdue')
  204. OR (status = 'active' AND due_chapter < ?)
  205. ORDER BY due_chapter ASC
  206. """,
  207. (current_chapter,),
  208. )
  209. return [dict(row) for row in cursor.fetchall()]
  210. def get_total_debt_balance(self) -> float:
  211. """获取总债务余额(包括 active 和 overdue)"""
  212. with self._get_conn() as conn:
  213. cursor = conn.cursor()
  214. cursor.execute("""
  215. SELECT COALESCE(SUM(current_amount), 0) FROM chase_debt
  216. WHERE status IN ('active', 'overdue')
  217. """)
  218. return cursor.fetchone()[0]
  219. def accrue_interest(self, current_chapter: int) -> Dict[str, Any]:
  220. """
  221. 计算利息(每章调用一次)
  222. - 对 active 和 overdue 债务都计息(逾期债务继续累积利息)
  223. - 使用 debt_events 表防止同一章重复计息
  224. - 检查逾期并更新状态
  225. 返回: {debts_processed, total_interest, new_overdues, skipped_already_processed}
  226. """
  227. result = {
  228. "debts_processed": 0,
  229. "total_interest": 0.0,
  230. "new_overdues": 0,
  231. "skipped_already_processed": 0,
  232. }
  233. with self._get_conn() as conn:
  234. cursor = conn.cursor()
  235. # 获取所有未偿还债务(active + overdue 都继续计息)
  236. cursor.execute("""
  237. SELECT * FROM chase_debt WHERE status IN ('active', 'overdue')
  238. """)
  239. debts = cursor.fetchall()
  240. for debt in debts:
  241. debt_id = debt["id"]
  242. current_amount = debt["current_amount"]
  243. interest_rate = debt["interest_rate"]
  244. due_chapter = debt["due_chapter"]
  245. debt_status = debt["status"]
  246. # 检查本章是否已计息(防止重复调用)
  247. cursor.execute(
  248. """
  249. SELECT 1 FROM debt_events
  250. WHERE debt_id = ? AND chapter = ? AND event_type = 'interest_accrued'
  251. """,
  252. (debt_id, current_chapter),
  253. )
  254. if cursor.fetchone():
  255. result["skipped_already_processed"] += 1
  256. continue
  257. # 计算利息
  258. interest = current_amount * interest_rate
  259. new_amount = current_amount + interest
  260. # 更新债务
  261. cursor.execute(
  262. """
  263. UPDATE chase_debt SET
  264. current_amount = ?,
  265. updated_at = CURRENT_TIMESTAMP
  266. WHERE id = ?
  267. """,
  268. (new_amount, debt_id),
  269. )
  270. # 记录利息事件
  271. self._record_debt_event(
  272. cursor,
  273. debt_id,
  274. "interest_accrued",
  275. interest,
  276. current_chapter,
  277. f"利息: {interest:.2f} (利率: {interest_rate * 100:.0f}%)",
  278. )
  279. result["debts_processed"] += 1
  280. result["total_interest"] += interest
  281. # 检查是否逾期(仅对 active 状态的债务)
  282. if debt_status == "active" and current_chapter > due_chapter:
  283. cursor.execute(
  284. """
  285. UPDATE chase_debt SET status = 'overdue'
  286. WHERE id = ? AND status = 'active'
  287. """,
  288. (debt_id,),
  289. )
  290. if cursor.rowcount > 0:
  291. result["new_overdues"] += 1
  292. self._record_debt_event(
  293. cursor,
  294. debt_id,
  295. "overdue",
  296. new_amount,
  297. current_chapter,
  298. f"债务逾期 (截止: 第{due_chapter}章)",
  299. )
  300. conn.commit()
  301. return result
  302. def pay_debt(self, debt_id: int, amount: float, chapter: int) -> Dict[str, Any]:
  303. """
  304. 偿还债务
  305. - 校验 amount > 0
  306. - 完全偿还时,使用原子 UPDATE 检查并标记关联 Override 为 fulfilled
  307. (并发安全:用 NOT EXISTS 子查询确保所有债务都已清零)
  308. 返回: {remaining, fully_paid, override_fulfilled}
  309. """
  310. # 校验偿还金额
  311. if amount <= 0:
  312. return {
  313. "remaining": 0,
  314. "fully_paid": False,
  315. "error": "偿还金额必须大于0",
  316. }
  317. with self._get_conn() as conn:
  318. cursor = conn.cursor()
  319. cursor.execute(
  320. "SELECT current_amount, override_contract_id FROM chase_debt WHERE id = ?",
  321. (debt_id,),
  322. )
  323. row = cursor.fetchone()
  324. if not row:
  325. return {"remaining": 0, "fully_paid": False, "error": "债务不存在"}
  326. current = row["current_amount"]
  327. override_contract_id = row["override_contract_id"]
  328. remaining = max(0, current - amount)
  329. override_fulfilled = False
  330. if remaining == 0:
  331. # 完全偿还
  332. cursor.execute(
  333. """
  334. UPDATE chase_debt SET
  335. current_amount = 0,
  336. status = 'paid',
  337. updated_at = CURRENT_TIMESTAMP
  338. WHERE id = ?
  339. """,
  340. (debt_id,),
  341. )
  342. self._record_debt_event(
  343. cursor, debt_id, "full_payment", amount, chapter, "债务已完全偿还"
  344. )
  345. # 原子检查并标记 Override 为 fulfilled
  346. # 使用 NOT EXISTS 子查询确保并发安全:只有当确实没有未清债务时才更新
  347. if override_contract_id:
  348. cursor.execute(
  349. """
  350. UPDATE override_contracts SET
  351. status = 'fulfilled',
  352. fulfilled_at = CURRENT_TIMESTAMP
  353. WHERE id = ?
  354. AND status = 'pending'
  355. AND NOT EXISTS (
  356. SELECT 1 FROM chase_debt
  357. WHERE override_contract_id = ?
  358. AND status IN ('active', 'overdue')
  359. )
  360. """,
  361. (override_contract_id, override_contract_id),
  362. )
  363. if cursor.rowcount > 0:
  364. override_fulfilled = True
  365. else:
  366. # 部分偿还
  367. cursor.execute(
  368. """
  369. UPDATE chase_debt SET
  370. current_amount = ?,
  371. updated_at = CURRENT_TIMESTAMP
  372. WHERE id = ?
  373. """,
  374. (remaining, debt_id),
  375. )
  376. self._record_debt_event(
  377. cursor,
  378. debt_id,
  379. "partial_payment",
  380. amount,
  381. chapter,
  382. f"部分偿还,剩余: {remaining:.2f}",
  383. )
  384. conn.commit()
  385. return {
  386. "remaining": remaining,
  387. "fully_paid": remaining == 0,
  388. "override_fulfilled": override_fulfilled,
  389. }
  390. def _record_debt_event(
  391. self,
  392. cursor,
  393. debt_id: int,
  394. event_type: str,
  395. amount: float,
  396. chapter: int,
  397. note: str = "",
  398. ):
  399. """记录债务事件(内部方法)"""
  400. cursor.execute(
  401. """
  402. INSERT INTO debt_events (debt_id, event_type, amount, chapter, note)
  403. VALUES (?, ?, ?, ?, ?)
  404. """,
  405. (debt_id, event_type, amount, chapter, note),
  406. )
  407. def get_debt_history(self, debt_id: int) -> List[Dict]:
  408. """获取债务的事件历史"""
  409. with self._get_conn() as conn:
  410. cursor = conn.cursor()
  411. cursor.execute(
  412. """
  413. SELECT * FROM debt_events
  414. WHERE debt_id = ?
  415. ORDER BY created_at ASC
  416. """,
  417. (debt_id,),
  418. )
  419. return [dict(row) for row in cursor.fetchall()]
  420. # ==================== v5.3 章节追读力元数据操作 ====================
  421. def get_debt_summary(self) -> Dict[str, Any]:
  422. """获取债务汇总信息"""
  423. with self._get_conn() as conn:
  424. cursor = conn.cursor()
  425. # 活跃债务
  426. cursor.execute("""
  427. SELECT COUNT(*) as count, COALESCE(SUM(current_amount), 0) as total
  428. FROM chase_debt WHERE status = 'active'
  429. """)
  430. active = cursor.fetchone()
  431. # 逾期债务
  432. cursor.execute("""
  433. SELECT COUNT(*) as count, COALESCE(SUM(current_amount), 0) as total
  434. FROM chase_debt WHERE status = 'overdue'
  435. """)
  436. overdue = cursor.fetchone()
  437. # 待偿还Override
  438. cursor.execute("""
  439. SELECT COUNT(*) FROM override_contracts WHERE status = 'pending'
  440. """)
  441. pending_overrides = cursor.fetchone()[0]
  442. return {
  443. "active_debts": active["count"],
  444. "active_total": active["total"],
  445. "overdue_debts": overdue["count"],
  446. "overdue_total": overdue["total"],
  447. "pending_overrides": pending_overrides,
  448. "total_balance": active["total"] + overdue["total"],
  449. }
  450. # ==================== 批量操作 ====================