| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- IndexDebtMixin extracted from IndexManager.
- """
- from __future__ import annotations
- import json
- from datetime import datetime
- from typing import Any, Dict, List, Optional
- class IndexDebtMixin:
- def create_override_contract(self, contract: OverrideContractMeta) -> int:
- """
- 创建或更新 Override Contract
- 使用 SQLite 的 INSERT ... ON CONFLICT ... DO UPDATE 实现原子 UPSERT:
- - 并发安全,无需显式锁
- - 保持 id 不变,避免 chase_debt.override_contract_id 悬挂
- - 完全冻结终态:已 fulfilled/cancelled 的合约所有字段都不会被修改
- 兼容性:支持 SQLite 3.24+(ON CONFLICT 语法),不依赖 RETURNING(3.35+)
- 返回合约 ID
- """
- with self._get_conn() as conn:
- cursor = conn.cursor()
- # 使用 ON CONFLICT 实现原子 UPSERT(SQLite 3.24+)
- # 终态完全冻结:fulfilled/cancelled 状态下所有字段都保持不变
- cursor.execute(
- """
- INSERT INTO override_contracts
- (chapter, constraint_type, constraint_id, rationale_type,
- rationale_text, payback_plan, due_chapter, status)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- ON CONFLICT(chapter, constraint_type, constraint_id) DO UPDATE SET
- rationale_type = CASE
- WHEN override_contracts.status IN ('fulfilled', 'cancelled')
- THEN override_contracts.rationale_type
- ELSE excluded.rationale_type
- END,
- rationale_text = CASE
- WHEN override_contracts.status IN ('fulfilled', 'cancelled')
- THEN override_contracts.rationale_text
- ELSE excluded.rationale_text
- END,
- payback_plan = CASE
- WHEN override_contracts.status IN ('fulfilled', 'cancelled')
- THEN override_contracts.payback_plan
- ELSE excluded.payback_plan
- END,
- due_chapter = CASE
- WHEN override_contracts.status IN ('fulfilled', 'cancelled')
- THEN override_contracts.due_chapter
- ELSE excluded.due_chapter
- END,
- status = CASE
- WHEN override_contracts.status IN ('fulfilled', 'cancelled')
- THEN override_contracts.status
- ELSE excluded.status
- END
- """,
- (
- contract.chapter,
- contract.constraint_type,
- contract.constraint_id,
- contract.rationale_type,
- contract.rationale_text,
- contract.payback_plan,
- contract.due_chapter,
- contract.status,
- ),
- )
- # 不使用 RETURNING(需要 SQLite 3.35+),改用查询获取 id
- cursor.execute(
- """
- SELECT id FROM override_contracts
- WHERE chapter = ? AND constraint_type = ? AND constraint_id = ?
- """,
- (contract.chapter, contract.constraint_type, contract.constraint_id),
- )
- row = cursor.fetchone()
- if not row:
- # UPSERT 后查不到记录是异常情况,不应发生
- raise RuntimeError(
- f"Override Contract UPSERT 后无法获取 id: "
- f"chapter={contract.chapter}, type={contract.constraint_type}, "
- f"id={contract.constraint_id}"
- )
- contract_id = row[0]
- conn.commit()
- return contract_id
- def get_pending_overrides(self, before_chapter: int = None) -> List[Dict]:
- """获取待偿还的Override Contracts"""
- with self._get_conn() as conn:
- cursor = conn.cursor()
- if before_chapter:
- cursor.execute(
- """
- SELECT * FROM override_contracts
- WHERE status = 'pending' AND due_chapter <= ?
- ORDER BY due_chapter ASC
- """,
- (before_chapter,),
- )
- else:
- cursor.execute("""
- SELECT * FROM override_contracts
- WHERE status = 'pending'
- ORDER BY due_chapter ASC
- """)
- return [dict(row) for row in cursor.fetchall()]
- def get_overdue_overrides(self, current_chapter: int) -> List[Dict]:
- """获取已逾期的Override Contracts"""
- with self._get_conn() as conn:
- cursor = conn.cursor()
- cursor.execute(
- """
- SELECT * FROM override_contracts
- WHERE status = 'pending' AND due_chapter < ?
- ORDER BY due_chapter ASC
- """,
- (current_chapter,),
- )
- return [dict(row) for row in cursor.fetchall()]
- def fulfill_override(self, contract_id: int) -> bool:
- """标记Override Contract为已偿还"""
- with self._get_conn() as conn:
- cursor = conn.cursor()
- cursor.execute(
- """
- UPDATE override_contracts SET
- status = 'fulfilled',
- fulfilled_at = CURRENT_TIMESTAMP
- WHERE id = ?
- """,
- (contract_id,),
- )
- conn.commit()
- return cursor.rowcount > 0
- def get_chapter_overrides(self, chapter: int) -> List[Dict]:
- """获取某章创建的Override Contracts"""
- with self._get_conn() as conn:
- cursor = conn.cursor()
- cursor.execute(
- """
- SELECT * FROM override_contracts WHERE chapter = ?
- """,
- (chapter,),
- )
- return [dict(row) for row in cursor.fetchall()]
- # ==================== v5.3 追读力债务操作 ====================
- def create_debt(self, debt: ChaseDebtMeta) -> int:
- """
- 创建追读力债务
- 返回债务 ID
- """
- with self._get_conn() as conn:
- cursor = conn.cursor()
- cursor.execute(
- """
- INSERT INTO chase_debt
- (debt_type, original_amount, current_amount, interest_rate,
- source_chapter, due_chapter, override_contract_id, status)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- debt.debt_type,
- debt.original_amount,
- debt.current_amount,
- debt.interest_rate,
- debt.source_chapter,
- debt.due_chapter,
- debt.override_contract_id if debt.override_contract_id else None,
- debt.status,
- ),
- )
- conn.commit()
- debt_id = cursor.lastrowid
- # 记录创建事件
- self._record_debt_event(
- cursor,
- debt_id,
- "created",
- debt.original_amount,
- debt.source_chapter,
- f"创建债务: {debt.debt_type}",
- )
- conn.commit()
- return debt_id
- def get_active_debts(self) -> List[Dict]:
- """获取所有活跃债务"""
- with self._get_conn() as conn:
- cursor = conn.cursor()
- cursor.execute("""
- SELECT * FROM chase_debt
- WHERE status = 'active'
- ORDER BY due_chapter ASC
- """)
- return [dict(row) for row in cursor.fetchall()]
- def get_overdue_debts(self, current_chapter: int) -> List[Dict]:
- """获取已逾期的债务(包括 active 但已过期的,以及已标记为 overdue 的)"""
- with self._get_conn() as conn:
- cursor = conn.cursor()
- cursor.execute(
- """
- SELECT * FROM chase_debt
- WHERE (status = 'overdue')
- OR (status = 'active' AND due_chapter < ?)
- ORDER BY due_chapter ASC
- """,
- (current_chapter,),
- )
- return [dict(row) for row in cursor.fetchall()]
- def get_total_debt_balance(self) -> float:
- """获取总债务余额(包括 active 和 overdue)"""
- with self._get_conn() as conn:
- cursor = conn.cursor()
- cursor.execute("""
- SELECT COALESCE(SUM(current_amount), 0) FROM chase_debt
- WHERE status IN ('active', 'overdue')
- """)
- return cursor.fetchone()[0]
- def accrue_interest(self, current_chapter: int) -> Dict[str, Any]:
- """
- 计算利息(每章调用一次)
- - 对 active 和 overdue 债务都计息(逾期债务继续累积利息)
- - 使用 debt_events 表防止同一章重复计息
- - 检查逾期并更新状态
- 返回: {debts_processed, total_interest, new_overdues, skipped_already_processed}
- """
- result = {
- "debts_processed": 0,
- "total_interest": 0.0,
- "new_overdues": 0,
- "skipped_already_processed": 0,
- }
- with self._get_conn() as conn:
- cursor = conn.cursor()
- # 获取所有未偿还债务(active + overdue 都继续计息)
- cursor.execute("""
- SELECT * FROM chase_debt WHERE status IN ('active', 'overdue')
- """)
- debts = cursor.fetchall()
- for debt in debts:
- debt_id = debt["id"]
- current_amount = debt["current_amount"]
- interest_rate = debt["interest_rate"]
- due_chapter = debt["due_chapter"]
- debt_status = debt["status"]
- # 检查本章是否已计息(防止重复调用)
- cursor.execute(
- """
- SELECT 1 FROM debt_events
- WHERE debt_id = ? AND chapter = ? AND event_type = 'interest_accrued'
- """,
- (debt_id, current_chapter),
- )
- if cursor.fetchone():
- result["skipped_already_processed"] += 1
- continue
- # 计算利息
- interest = current_amount * interest_rate
- new_amount = current_amount + interest
- # 更新债务
- cursor.execute(
- """
- UPDATE chase_debt SET
- current_amount = ?,
- updated_at = CURRENT_TIMESTAMP
- WHERE id = ?
- """,
- (new_amount, debt_id),
- )
- # 记录利息事件
- self._record_debt_event(
- cursor,
- debt_id,
- "interest_accrued",
- interest,
- current_chapter,
- f"利息: {interest:.2f} (利率: {interest_rate * 100:.0f}%)",
- )
- result["debts_processed"] += 1
- result["total_interest"] += interest
- # 检查是否逾期(仅对 active 状态的债务)
- if debt_status == "active" and current_chapter > due_chapter:
- cursor.execute(
- """
- UPDATE chase_debt SET status = 'overdue'
- WHERE id = ? AND status = 'active'
- """,
- (debt_id,),
- )
- if cursor.rowcount > 0:
- result["new_overdues"] += 1
- self._record_debt_event(
- cursor,
- debt_id,
- "overdue",
- new_amount,
- current_chapter,
- f"债务逾期 (截止: 第{due_chapter}章)",
- )
- conn.commit()
- return result
- def pay_debt(self, debt_id: int, amount: float, chapter: int) -> Dict[str, Any]:
- """
- 偿还债务
- - 校验 amount > 0
- - 完全偿还时,使用原子 UPDATE 检查并标记关联 Override 为 fulfilled
- (并发安全:用 NOT EXISTS 子查询确保所有债务都已清零)
- 返回: {remaining, fully_paid, override_fulfilled}
- """
- # 校验偿还金额
- if amount <= 0:
- return {
- "remaining": 0,
- "fully_paid": False,
- "error": "偿还金额必须大于0",
- }
- with self._get_conn() as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT current_amount, override_contract_id FROM chase_debt WHERE id = ?",
- (debt_id,),
- )
- row = cursor.fetchone()
- if not row:
- return {"remaining": 0, "fully_paid": False, "error": "债务不存在"}
- current = row["current_amount"]
- override_contract_id = row["override_contract_id"]
- remaining = max(0, current - amount)
- override_fulfilled = False
- if remaining == 0:
- # 完全偿还
- cursor.execute(
- """
- UPDATE chase_debt SET
- current_amount = 0,
- status = 'paid',
- updated_at = CURRENT_TIMESTAMP
- WHERE id = ?
- """,
- (debt_id,),
- )
- self._record_debt_event(
- cursor, debt_id, "full_payment", amount, chapter, "债务已完全偿还"
- )
- # 原子检查并标记 Override 为 fulfilled
- # 使用 NOT EXISTS 子查询确保并发安全:只有当确实没有未清债务时才更新
- if override_contract_id:
- cursor.execute(
- """
- UPDATE override_contracts SET
- status = 'fulfilled',
- fulfilled_at = CURRENT_TIMESTAMP
- WHERE id = ?
- AND status = 'pending'
- AND NOT EXISTS (
- SELECT 1 FROM chase_debt
- WHERE override_contract_id = ?
- AND status IN ('active', 'overdue')
- )
- """,
- (override_contract_id, override_contract_id),
- )
- if cursor.rowcount > 0:
- override_fulfilled = True
- else:
- # 部分偿还
- cursor.execute(
- """
- UPDATE chase_debt SET
- current_amount = ?,
- updated_at = CURRENT_TIMESTAMP
- WHERE id = ?
- """,
- (remaining, debt_id),
- )
- self._record_debt_event(
- cursor,
- debt_id,
- "partial_payment",
- amount,
- chapter,
- f"部分偿还,剩余: {remaining:.2f}",
- )
- conn.commit()
- return {
- "remaining": remaining,
- "fully_paid": remaining == 0,
- "override_fulfilled": override_fulfilled,
- }
- def _record_debt_event(
- self,
- cursor,
- debt_id: int,
- event_type: str,
- amount: float,
- chapter: int,
- note: str = "",
- ):
- """记录债务事件(内部方法)"""
- cursor.execute(
- """
- INSERT INTO debt_events (debt_id, event_type, amount, chapter, note)
- VALUES (?, ?, ?, ?, ?)
- """,
- (debt_id, event_type, amount, chapter, note),
- )
- def get_debt_history(self, debt_id: int) -> List[Dict]:
- """获取债务的事件历史"""
- with self._get_conn() as conn:
- cursor = conn.cursor()
- cursor.execute(
- """
- SELECT * FROM debt_events
- WHERE debt_id = ?
- ORDER BY created_at ASC
- """,
- (debt_id,),
- )
- return [dict(row) for row in cursor.fetchall()]
- # ==================== v5.3 章节追读力元数据操作 ====================
- def get_debt_summary(self) -> Dict[str, Any]:
- """获取债务汇总信息"""
- with self._get_conn() as conn:
- cursor = conn.cursor()
- # 活跃债务
- cursor.execute("""
- SELECT COUNT(*) as count, COALESCE(SUM(current_amount), 0) as total
- FROM chase_debt WHERE status = 'active'
- """)
- active = cursor.fetchone()
- # 逾期债务
- cursor.execute("""
- SELECT COUNT(*) as count, COALESCE(SUM(current_amount), 0) as total
- FROM chase_debt WHERE status = 'overdue'
- """)
- overdue = cursor.fetchone()
- # 待偿还Override
- cursor.execute("""
- SELECT COUNT(*) FROM override_contracts WHERE status = 'pending'
- """)
- pending_overrides = cursor.fetchone()[0]
- return {
- "active_debts": active["count"],
- "active_total": active["total"],
- "overdue_debts": overdue["count"],
- "overdue_total": overdue["total"],
- "pending_overrides": pending_overrides,
- "total_balance": active["total"] + overdue["total"],
- }
- # ==================== 批量操作 ====================
|