feat: initial commit

This commit is contained in:
2026-06-23 20:29:02 +08:00
commit ea8f6066c4
217 changed files with 60754 additions and 0 deletions
@@ -0,0 +1,504 @@
#!/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 语法),不依赖 RETURNING3.35+
返回合约 ID
"""
with self._get_conn() as conn:
cursor = conn.cursor()
# 使用 ON CONFLICT 实现原子 UPSERTSQLite 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"],
}
# ==================== 批量操作 ====================