2137 lines
71 KiB
Python
2137 lines
71 KiB
Python
"""
|
||
InsightFlow Phase 8 - 订阅与计费系统模块
|
||
|
||
功能:
|
||
1. 多层级订阅计划(Free/Pro/Enterprise)
|
||
2. 按量计费(转录时长、存储空间、API 调用次数)
|
||
3. 支付集成(Stripe、支付宝、微信支付)
|
||
4. 发票管理、退款处理、账单历史
|
||
|
||
作者: InsightFlow Team
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
import sqlite3
|
||
import uuid
|
||
from dataclasses import dataclass
|
||
from datetime import datetime, timedelta
|
||
from enum import StrEnum
|
||
from typing import Any
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class SubscriptionStatus(StrEnum):
|
||
"""订阅状态"""
|
||
|
||
ACTIVE = "active" # 活跃
|
||
CANCELLED = "cancelled" # 已取消
|
||
EXPIRED = "expired" # 已过期
|
||
PAST_DUE = "past_due" # 逾期
|
||
TRIAL = "trial" # 试用中
|
||
PENDING = "pending" # 待支付
|
||
|
||
|
||
class PaymentProvider(StrEnum):
|
||
"""支付提供商"""
|
||
|
||
STRIPE = "stripe" # Stripe
|
||
ALIPAY = "alipay" # 支付宝
|
||
WECHAT = "wechat" # 微信支付
|
||
BANK_TRANSFER = "bank_transfer" # 银行转账
|
||
|
||
|
||
class PaymentStatus(StrEnum):
|
||
"""支付状态"""
|
||
|
||
PENDING = "pending" # 待支付
|
||
PROCESSING = "processing" # 处理中
|
||
COMPLETED = "completed" # 已完成
|
||
FAILED = "failed" # 失败
|
||
REFUNDED = "refunded" # 已退款
|
||
PARTIAL_REFUNDED = "partial_refunded" # 部分退款
|
||
|
||
|
||
class InvoiceStatus(StrEnum):
|
||
"""发票状态"""
|
||
|
||
DRAFT = "draft" # 草稿
|
||
ISSUED = "issued" # 已开具
|
||
PAID = "paid" # 已支付
|
||
OVERDUE = "overdue" # 逾期
|
||
VOID = "void" # 作废
|
||
CREDIT_NOTE = "credit_note" # 贷项通知单
|
||
|
||
|
||
class RefundStatus(StrEnum):
|
||
"""退款状态"""
|
||
|
||
PENDING = "pending" # 待处理
|
||
APPROVED = "approved" # 已批准
|
||
REJECTED = "rejected" # 已拒绝
|
||
COMPLETED = "completed" # 已完成
|
||
FAILED = "failed" # 失败
|
||
|
||
|
||
@dataclass
|
||
class SubscriptionPlan:
|
||
"""订阅计划数据类"""
|
||
|
||
id: str
|
||
name: str
|
||
tier: str # free/pro/enterprise
|
||
description: str
|
||
price_monthly: float # 月付价格
|
||
price_yearly: float # 年付价格
|
||
currency: str # CNY/USD
|
||
features: list[str] # 功能列表
|
||
limits: dict[str, Any] # 资源限制
|
||
is_active: bool
|
||
created_at: datetime
|
||
updated_at: datetime
|
||
metadata: dict[str, Any]
|
||
|
||
|
||
@dataclass
|
||
class Subscription:
|
||
"""订阅数据类"""
|
||
|
||
id: str
|
||
tenant_id: str
|
||
plan_id: str
|
||
status: str
|
||
current_period_start: datetime
|
||
current_period_end: datetime
|
||
cancel_at_period_end: bool
|
||
canceled_at: datetime | None
|
||
trial_start: datetime | None
|
||
trial_end: datetime | None
|
||
payment_provider: str | None
|
||
provider_subscription_id: str | None # 支付提供商的订阅ID
|
||
created_at: datetime
|
||
updated_at: datetime
|
||
metadata: dict[str, Any]
|
||
|
||
|
||
@dataclass
|
||
class UsageRecord:
|
||
"""用量记录数据类"""
|
||
|
||
id: str
|
||
tenant_id: str
|
||
resource_type: str # transcription/storage/api_call
|
||
quantity: float # 使用量
|
||
unit: str # minutes/mb/count
|
||
recorded_at: datetime
|
||
cost: float # 费用
|
||
description: str | None
|
||
metadata: dict[str, Any]
|
||
|
||
|
||
@dataclass
|
||
class Payment:
|
||
"""支付记录数据类"""
|
||
|
||
id: str
|
||
tenant_id: str
|
||
subscription_id: str | None
|
||
invoice_id: str | None
|
||
amount: float
|
||
currency: str
|
||
provider: str
|
||
provider_payment_id: str | None
|
||
status: str
|
||
payment_method: str | None
|
||
payment_details: dict[str, Any]
|
||
paid_at: datetime | None
|
||
failed_at: datetime | None
|
||
failure_reason: str | None
|
||
created_at: datetime
|
||
updated_at: datetime
|
||
|
||
|
||
@dataclass
|
||
class Invoice:
|
||
"""发票数据类"""
|
||
|
||
id: str
|
||
tenant_id: str
|
||
subscription_id: str | None
|
||
invoice_number: str
|
||
status: str
|
||
amount_due: float
|
||
amount_paid: float
|
||
currency: str
|
||
period_start: datetime
|
||
period_end: datetime
|
||
description: str
|
||
line_items: list[dict[str, Any]]
|
||
due_date: datetime
|
||
paid_at: datetime | None
|
||
voided_at: datetime | None
|
||
void_reason: str | None
|
||
created_at: datetime
|
||
updated_at: datetime
|
||
|
||
|
||
@dataclass
|
||
class Refund:
|
||
"""退款数据类"""
|
||
|
||
id: str
|
||
tenant_id: str
|
||
payment_id: str
|
||
invoice_id: str | None
|
||
amount: float
|
||
currency: str
|
||
reason: str
|
||
status: str
|
||
requested_by: str
|
||
requested_at: datetime
|
||
approved_by: str | None
|
||
approved_at: str | None
|
||
completed_at: datetime | None
|
||
provider_refund_id: str | None
|
||
metadata: dict[str, Any]
|
||
created_at: datetime
|
||
updated_at: datetime
|
||
|
||
|
||
@dataclass
|
||
class BillingHistory:
|
||
"""账单历史数据类"""
|
||
|
||
id: str
|
||
tenant_id: str
|
||
type: str # subscription/usage/payment/refund
|
||
amount: float
|
||
currency: str
|
||
description: str
|
||
reference_id: str # 关联的订阅/支付/退款ID
|
||
balance_after: float # 操作后余额
|
||
created_at: datetime
|
||
metadata: dict[str, Any]
|
||
|
||
|
||
class SubscriptionManager:
|
||
"""订阅与计费管理器"""
|
||
|
||
# 默认订阅计划配置
|
||
DEFAULT_PLANS = {
|
||
"free": {
|
||
"name": "Free",
|
||
"tier": "free",
|
||
"description": "免费版,适合个人用户试用",
|
||
"price_monthly": 0.0,
|
||
"price_yearly": 0.0,
|
||
"currency": "CNY",
|
||
"features": ["basic_analysis", "export_png", "3_projects", "100_mb_storage", "60_min_transcription"],
|
||
"limits": {
|
||
"max_projects": 3,
|
||
"max_storage_mb": 100,
|
||
"max_transcription_minutes": 60,
|
||
"max_api_calls_per_day": 100,
|
||
"max_team_members": 2,
|
||
"max_entities": 100,
|
||
},
|
||
},
|
||
"pro": {
|
||
"name": "Pro",
|
||
"tier": "pro",
|
||
"description": "专业版,适合小型团队",
|
||
"price_monthly": 99.0,
|
||
"price_yearly": 990.0,
|
||
"currency": "CNY",
|
||
"features": [
|
||
"all_free_features",
|
||
"advanced_analysis",
|
||
"export_all_formats",
|
||
"api_access",
|
||
"webhooks",
|
||
"collaboration",
|
||
"20_projects",
|
||
"10_gb_storage",
|
||
"600_min_transcription",
|
||
],
|
||
"limits": {
|
||
"max_projects": 20,
|
||
"max_storage_mb": 10240,
|
||
"max_transcription_minutes": 600,
|
||
"max_api_calls_per_day": 10000,
|
||
"max_team_members": 10,
|
||
"max_entities": 1000,
|
||
},
|
||
},
|
||
"enterprise": {
|
||
"name": "Enterprise",
|
||
"tier": "enterprise",
|
||
"description": "企业版,适合大型企业",
|
||
"price_monthly": 999.0,
|
||
"price_yearly": 9990.0,
|
||
"currency": "CNY",
|
||
"features": [
|
||
"all_pro_features",
|
||
"unlimited_projects",
|
||
"unlimited_storage",
|
||
"unlimited_transcription",
|
||
"priority_support",
|
||
"custom_integration",
|
||
"sla_guarantee",
|
||
"dedicated_manager",
|
||
],
|
||
"limits": {
|
||
"max_projects": -1,
|
||
"max_storage_mb": -1,
|
||
"max_transcription_minutes": -1,
|
||
"max_api_calls_per_day": -1,
|
||
"max_team_members": -1,
|
||
"max_entities": -1,
|
||
},
|
||
},
|
||
}
|
||
|
||
# 按量计费单价(CNY)
|
||
USAGE_PRICING = {
|
||
"transcription": {"unit": "minute", "price": 0.5, "free_quota": 60}, # 0.5元/分钟 # 每月免费额度
|
||
"storage": {"unit": "gb", "price": 10.0, "free_quota": 0.1}, # 10元/GB/月 # 100MB免费
|
||
"api_call": {"unit": "1000_calls", "price": 5.0, "free_quota": 1000}, # 5元/1000次 # 每月免费1000次
|
||
"export": {"unit": "page", "price": 0.1, "free_quota": 100}, # 0.1元/页(PDF导出)
|
||
}
|
||
|
||
def __init__(self, db_path: str = "insightflow.db"):
|
||
self.db_path = db_path
|
||
self._init_db()
|
||
self._init_default_plans()
|
||
|
||
def _get_connection(self) -> sqlite3.Connection:
|
||
"""获取数据库连接"""
|
||
conn = sqlite3.connect(self.db_path)
|
||
conn.row_factory = sqlite3.Row
|
||
return conn
|
||
|
||
def _init_db(self) -> None:
|
||
"""初始化数据库表"""
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
|
||
# 订阅计划表
|
||
cursor.execute("""
|
||
CREATE TABLE IF NOT EXISTS subscription_plans (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
tier TEXT UNIQUE NOT NULL,
|
||
description TEXT,
|
||
price_monthly REAL DEFAULT 0,
|
||
price_yearly REAL DEFAULT 0,
|
||
currency TEXT DEFAULT 'CNY',
|
||
features TEXT DEFAULT '[]',
|
||
limits TEXT DEFAULT '{}',
|
||
is_active INTEGER DEFAULT 1,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
metadata TEXT DEFAULT '{}'
|
||
)
|
||
""")
|
||
|
||
# 订阅表
|
||
cursor.execute("""
|
||
CREATE TABLE IF NOT EXISTS subscriptions (
|
||
id TEXT PRIMARY KEY,
|
||
tenant_id TEXT NOT NULL,
|
||
plan_id TEXT NOT NULL,
|
||
status TEXT DEFAULT 'pending',
|
||
current_period_start TIMESTAMP,
|
||
current_period_end TIMESTAMP,
|
||
cancel_at_period_end INTEGER DEFAULT 0,
|
||
canceled_at TIMESTAMP,
|
||
trial_start TIMESTAMP,
|
||
trial_end TIMESTAMP,
|
||
payment_provider TEXT,
|
||
provider_subscription_id TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
metadata TEXT DEFAULT '{}',
|
||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (plan_id) REFERENCES subscription_plans(id)
|
||
)
|
||
""")
|
||
|
||
# 用量记录表
|
||
cursor.execute("""
|
||
CREATE TABLE IF NOT EXISTS usage_records (
|
||
id TEXT PRIMARY KEY,
|
||
tenant_id TEXT NOT NULL,
|
||
resource_type TEXT NOT NULL,
|
||
quantity REAL DEFAULT 0,
|
||
unit TEXT NOT NULL,
|
||
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
cost REAL DEFAULT 0,
|
||
description TEXT,
|
||
metadata TEXT DEFAULT '{}',
|
||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
|
||
)
|
||
""")
|
||
|
||
# 支付记录表
|
||
cursor.execute("""
|
||
CREATE TABLE IF NOT EXISTS payments (
|
||
id TEXT PRIMARY KEY,
|
||
tenant_id TEXT NOT NULL,
|
||
subscription_id TEXT,
|
||
invoice_id TEXT,
|
||
amount REAL NOT NULL,
|
||
currency TEXT DEFAULT 'CNY',
|
||
provider TEXT NOT NULL,
|
||
provider_payment_id TEXT,
|
||
status TEXT DEFAULT 'pending',
|
||
payment_method TEXT,
|
||
payment_details TEXT DEFAULT '{}',
|
||
paid_at TIMESTAMP,
|
||
failed_at TIMESTAMP,
|
||
failure_reason TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE SET NULL,
|
||
FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE SET NULL
|
||
)
|
||
""")
|
||
|
||
# 发票表
|
||
cursor.execute("""
|
||
CREATE TABLE IF NOT EXISTS invoices (
|
||
id TEXT PRIMARY KEY,
|
||
tenant_id TEXT NOT NULL,
|
||
subscription_id TEXT,
|
||
invoice_number TEXT UNIQUE NOT NULL,
|
||
status TEXT DEFAULT 'draft',
|
||
amount_due REAL DEFAULT 0,
|
||
amount_paid REAL DEFAULT 0,
|
||
currency TEXT DEFAULT 'CNY',
|
||
period_start TIMESTAMP,
|
||
period_end TIMESTAMP,
|
||
description TEXT,
|
||
line_items TEXT DEFAULT '[]',
|
||
due_date TIMESTAMP,
|
||
paid_at TIMESTAMP,
|
||
voided_at TIMESTAMP,
|
||
void_reason TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE SET NULL
|
||
)
|
||
""")
|
||
|
||
# 退款表
|
||
cursor.execute("""
|
||
CREATE TABLE IF NOT EXISTS refunds (
|
||
id TEXT PRIMARY KEY,
|
||
tenant_id TEXT NOT NULL,
|
||
payment_id TEXT NOT NULL,
|
||
invoice_id TEXT,
|
||
amount REAL NOT NULL,
|
||
currency TEXT DEFAULT 'CNY',
|
||
reason TEXT,
|
||
status TEXT DEFAULT 'pending',
|
||
requested_by TEXT NOT NULL,
|
||
requested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
approved_by TEXT,
|
||
approved_at TIMESTAMP,
|
||
completed_at TIMESTAMP,
|
||
provider_refund_id TEXT,
|
||
metadata TEXT DEFAULT '{}',
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (payment_id) REFERENCES payments(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE SET NULL
|
||
)
|
||
""")
|
||
|
||
# 账单历史表
|
||
cursor.execute("""
|
||
CREATE TABLE IF NOT EXISTS billing_history (
|
||
id TEXT PRIMARY KEY,
|
||
tenant_id TEXT NOT NULL,
|
||
type TEXT NOT NULL,
|
||
amount REAL NOT NULL,
|
||
currency TEXT DEFAULT 'CNY',
|
||
description TEXT,
|
||
reference_id TEXT,
|
||
balance_after REAL DEFAULT 0,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
metadata TEXT DEFAULT '{}',
|
||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
|
||
)
|
||
""")
|
||
|
||
# 创建索引
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_subscriptions_tenant ON subscriptions(tenant_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_subscriptions_plan ON subscriptions(plan_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_usage_tenant ON usage_records(tenant_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_usage_type ON usage_records(resource_type)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_usage_recorded ON usage_records(recorded_at)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_payments_tenant ON payments(tenant_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_invoices_tenant ON invoices(tenant_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_invoices_status ON invoices(status)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_invoices_number ON invoices(invoice_number)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_refunds_tenant ON refunds(tenant_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_refunds_status ON refunds(status)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_billing_tenant ON billing_history(tenant_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_billing_created ON billing_history(created_at)")
|
||
|
||
conn.commit()
|
||
logger.info("Subscription tables initialized successfully")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error initializing subscription tables: {e}")
|
||
raise
|
||
finally:
|
||
conn.close()
|
||
|
||
def _init_default_plans(self) -> None:
|
||
"""初始化默认订阅计划"""
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
|
||
for tier, plan_data in self.DEFAULT_PLANS.items():
|
||
cursor.execute(
|
||
"""
|
||
INSERT OR IGNORE INTO subscription_plans
|
||
(id, name, tier, description, price_monthly, price_yearly, currency,
|
||
features, limits, is_active, created_at, updated_at, metadata)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""",
|
||
(
|
||
str(uuid.uuid4()),
|
||
plan_data["name"],
|
||
plan_data["tier"],
|
||
plan_data["description"],
|
||
plan_data["price_monthly"],
|
||
plan_data["price_yearly"],
|
||
plan_data["currency"],
|
||
json.dumps(plan_data["features"]),
|
||
json.dumps(plan_data["limits"]),
|
||
1,
|
||
datetime.now(),
|
||
datetime.now(),
|
||
json.dumps({}),
|
||
),
|
||
)
|
||
|
||
conn.commit()
|
||
logger.info("Default subscription plans initialized")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error initializing default plans: {e}")
|
||
finally:
|
||
conn.close()
|
||
|
||
# ==================== 订阅计划管理 ====================
|
||
|
||
def get_plan(self, plan_id: str) -> SubscriptionPlan | None:
|
||
"""获取订阅计划"""
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT * FROM subscription_plans WHERE id = ?", (plan_id,))
|
||
row = cursor.fetchone()
|
||
|
||
if row:
|
||
return self._row_to_plan(row)
|
||
return None
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def get_plan_by_tier(self, tier: str) -> SubscriptionPlan | None:
|
||
"""通过层级获取订阅计划"""
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT * FROM subscription_plans WHERE tier = ? AND is_active = 1", (tier,))
|
||
row = cursor.fetchone()
|
||
|
||
if row:
|
||
return self._row_to_plan(row)
|
||
return None
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def list_plans(self, include_inactive: bool = False) -> list[SubscriptionPlan]:
|
||
"""列出所有订阅计划"""
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
|
||
if include_inactive:
|
||
cursor.execute("SELECT * FROM subscription_plans ORDER BY price_monthly")
|
||
else:
|
||
cursor.execute("SELECT * FROM subscription_plans WHERE is_active = 1 ORDER BY price_monthly")
|
||
|
||
rows = cursor.fetchall()
|
||
return [self._row_to_plan(row) for row in rows]
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def create_plan(
|
||
self,
|
||
name: str,
|
||
tier: str,
|
||
description: str,
|
||
price_monthly: float,
|
||
price_yearly: float,
|
||
currency: str = "CNY",
|
||
features: list[str] = None,
|
||
limits: dict[str, Any] = None,
|
||
) -> SubscriptionPlan:
|
||
"""创建新订阅计划"""
|
||
conn = self._get_connection()
|
||
try:
|
||
plan_id = str(uuid.uuid4())
|
||
|
||
plan = SubscriptionPlan(
|
||
id=plan_id,
|
||
name=name,
|
||
tier=tier,
|
||
description=description,
|
||
price_monthly=price_monthly,
|
||
price_yearly=price_yearly,
|
||
currency=currency,
|
||
features=features or [],
|
||
limits=limits or {},
|
||
is_active=True,
|
||
created_at=datetime.now(),
|
||
updated_at=datetime.now(),
|
||
metadata={},
|
||
)
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
INSERT INTO subscription_plans
|
||
(id, name, tier, description, price_monthly, price_yearly, currency,
|
||
features, limits, is_active, created_at, updated_at, metadata)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""",
|
||
(
|
||
plan.id,
|
||
plan.name,
|
||
plan.tier,
|
||
plan.description,
|
||
plan.price_monthly,
|
||
plan.price_yearly,
|
||
plan.currency,
|
||
json.dumps(plan.features),
|
||
json.dumps(plan.limits),
|
||
int(plan.is_active),
|
||
plan.created_at,
|
||
plan.updated_at,
|
||
json.dumps(plan.metadata),
|
||
),
|
||
)
|
||
|
||
conn.commit()
|
||
logger.info(f"Subscription plan created: {plan_id} ({name})")
|
||
return plan
|
||
|
||
except Exception as e:
|
||
conn.rollback()
|
||
logger.error(f"Error creating plan: {e}")
|
||
raise
|
||
finally:
|
||
conn.close()
|
||
|
||
def update_plan(self, plan_id: str, **kwargs) -> SubscriptionPlan | None:
|
||
"""更新订阅计划"""
|
||
conn = self._get_connection()
|
||
try:
|
||
plan = self.get_plan(plan_id)
|
||
if not plan:
|
||
return None
|
||
|
||
updates = []
|
||
params = []
|
||
|
||
allowed_fields = [
|
||
"name",
|
||
"description",
|
||
"price_monthly",
|
||
"price_yearly",
|
||
"currency",
|
||
"features",
|
||
"limits",
|
||
"is_active",
|
||
]
|
||
|
||
for key, value in kwargs.items():
|
||
if key in allowed_fields:
|
||
updates.append(f"{key} = ?")
|
||
if key in ["features", "limits"]:
|
||
params.append(json.dumps(value) if value else "{}")
|
||
elif key == "is_active":
|
||
params.append(int(value))
|
||
else:
|
||
params.append(value)
|
||
|
||
if not updates:
|
||
return plan
|
||
|
||
updates.append("updated_at = ?")
|
||
params.append(datetime.now())
|
||
params.append(plan_id)
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
f"""
|
||
UPDATE subscription_plans SET {', '.join(updates)}
|
||
WHERE id = ?
|
||
""",
|
||
params,
|
||
)
|
||
|
||
conn.commit()
|
||
return self.get_plan(plan_id)
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
# ==================== 订阅管理 ====================
|
||
|
||
def create_subscription(
|
||
self,
|
||
tenant_id: str,
|
||
plan_id: str,
|
||
payment_provider: str | None = None,
|
||
trial_days: int = 0,
|
||
billing_cycle: str = "monthly",
|
||
) -> Subscription:
|
||
"""创建新订阅"""
|
||
conn = self._get_connection()
|
||
try:
|
||
# 检查是否已有活跃订阅
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
SELECT * FROM subscriptions
|
||
WHERE tenant_id = ? AND status IN ('active', 'trial', 'pending')
|
||
""",
|
||
(tenant_id,),
|
||
)
|
||
|
||
existing = cursor.fetchone()
|
||
if existing:
|
||
raise ValueError(f"Tenant {tenant_id} already has an active subscription")
|
||
|
||
# 获取计划信息
|
||
plan = self.get_plan(plan_id)
|
||
if not plan:
|
||
raise ValueError(f"Plan {plan_id} not found")
|
||
|
||
subscription_id = str(uuid.uuid4())
|
||
now = datetime.now()
|
||
|
||
# 计算周期
|
||
if billing_cycle == "yearly":
|
||
period_end = now + timedelta(days=365)
|
||
else:
|
||
period_end = now + timedelta(days=30)
|
||
|
||
# 试用处理
|
||
trial_start = None
|
||
trial_end = None
|
||
if trial_days > 0:
|
||
trial_start = now
|
||
trial_end = now + timedelta(days=trial_days)
|
||
status = SubscriptionStatus.TRIAL.value
|
||
else:
|
||
status = SubscriptionStatus.PENDING.value
|
||
|
||
subscription = Subscription(
|
||
id=subscription_id,
|
||
tenant_id=tenant_id,
|
||
plan_id=plan_id,
|
||
status=status,
|
||
current_period_start=now,
|
||
current_period_end=period_end,
|
||
cancel_at_period_end=False,
|
||
canceled_at=None,
|
||
trial_start=trial_start,
|
||
trial_end=trial_end,
|
||
payment_provider=payment_provider,
|
||
provider_subscription_id=None,
|
||
created_at=now,
|
||
updated_at=now,
|
||
metadata={"billing_cycle": billing_cycle},
|
||
)
|
||
|
||
cursor.execute(
|
||
"""
|
||
INSERT INTO subscriptions
|
||
(id, tenant_id, plan_id, status, current_period_start, current_period_end,
|
||
cancel_at_period_end, canceled_at, trial_start, trial_end,
|
||
payment_provider, provider_subscription_id, created_at, updated_at, metadata)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""",
|
||
(
|
||
subscription.id,
|
||
subscription.tenant_id,
|
||
subscription.plan_id,
|
||
subscription.status,
|
||
subscription.current_period_start,
|
||
subscription.current_period_end,
|
||
int(subscription.cancel_at_period_end),
|
||
subscription.canceled_at,
|
||
subscription.trial_start,
|
||
subscription.trial_end,
|
||
subscription.payment_provider,
|
||
subscription.provider_subscription_id,
|
||
subscription.created_at,
|
||
subscription.updated_at,
|
||
json.dumps(subscription.metadata),
|
||
),
|
||
)
|
||
|
||
# 创建发票
|
||
amount = plan.price_yearly if billing_cycle == "yearly" else plan.price_monthly
|
||
if amount > 0 and trial_days == 0:
|
||
self._create_invoice_internal(
|
||
conn,
|
||
tenant_id,
|
||
subscription_id,
|
||
amount,
|
||
plan.currency,
|
||
now,
|
||
period_end,
|
||
f"{plan.name} Subscription ({billing_cycle})",
|
||
)
|
||
|
||
# 记录账单历史
|
||
self._add_billing_history_internal(
|
||
conn,
|
||
tenant_id,
|
||
"subscription",
|
||
0,
|
||
plan.currency,
|
||
f"Subscription created: {plan.name}",
|
||
subscription_id,
|
||
0,
|
||
)
|
||
|
||
conn.commit()
|
||
logger.info(f"Subscription created: {subscription_id} for tenant {tenant_id}")
|
||
return subscription
|
||
|
||
except Exception as e:
|
||
conn.rollback()
|
||
logger.error(f"Error creating subscription: {e}")
|
||
raise
|
||
finally:
|
||
conn.close()
|
||
|
||
def get_subscription(self, subscription_id: str) -> Subscription | None:
|
||
"""获取订阅信息"""
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT * FROM subscriptions WHERE id = ?", (subscription_id,))
|
||
row = cursor.fetchone()
|
||
|
||
if row:
|
||
return self._row_to_subscription(row)
|
||
return None
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def get_tenant_subscription(self, tenant_id: str) -> Subscription | None:
|
||
"""获取租户的当前订阅"""
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
SELECT * FROM subscriptions
|
||
WHERE tenant_id = ? AND status IN ('active', 'trial', 'past_due', 'pending')
|
||
ORDER BY created_at DESC LIMIT 1
|
||
""",
|
||
(tenant_id,),
|
||
)
|
||
row = cursor.fetchone()
|
||
|
||
if row:
|
||
return self._row_to_subscription(row)
|
||
return None
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def update_subscription(self, subscription_id: str, **kwargs) -> Subscription | None:
|
||
"""更新订阅"""
|
||
conn = self._get_connection()
|
||
try:
|
||
subscription = self.get_subscription(subscription_id)
|
||
if not subscription:
|
||
return None
|
||
|
||
updates = []
|
||
params = []
|
||
|
||
allowed_fields = [
|
||
"status",
|
||
"current_period_start",
|
||
"current_period_end",
|
||
"cancel_at_period_end",
|
||
"canceled_at",
|
||
"trial_end",
|
||
"payment_provider",
|
||
"provider_subscription_id",
|
||
]
|
||
|
||
for key, value in kwargs.items():
|
||
if key in allowed_fields:
|
||
updates.append(f"{key} = ?")
|
||
if key == "cancel_at_period_end":
|
||
params.append(int(value))
|
||
else:
|
||
params.append(value)
|
||
|
||
if not updates:
|
||
return subscription
|
||
|
||
updates.append("updated_at = ?")
|
||
params.append(datetime.now())
|
||
params.append(subscription_id)
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
f"""
|
||
UPDATE subscriptions SET {', '.join(updates)}
|
||
WHERE id = ?
|
||
""",
|
||
params,
|
||
)
|
||
|
||
conn.commit()
|
||
return self.get_subscription(subscription_id)
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def cancel_subscription(self, subscription_id: str, at_period_end: bool = True) -> Subscription | None:
|
||
"""取消订阅"""
|
||
conn = self._get_connection()
|
||
try:
|
||
subscription = self.get_subscription(subscription_id)
|
||
if not subscription:
|
||
return None
|
||
|
||
now = datetime.now()
|
||
|
||
if at_period_end:
|
||
# 在周期结束时取消
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
UPDATE subscriptions
|
||
SET cancel_at_period_end = 1, canceled_at = ?, updated_at = ?
|
||
WHERE id = ?
|
||
""",
|
||
(now, now, subscription_id),
|
||
)
|
||
else:
|
||
# 立即取消
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
UPDATE subscriptions
|
||
SET status = 'cancelled', canceled_at = ?, updated_at = ?
|
||
WHERE id = ?
|
||
""",
|
||
(now, now, subscription_id),
|
||
)
|
||
|
||
# 记录账单历史
|
||
self._add_billing_history_internal(
|
||
conn,
|
||
subscription.tenant_id,
|
||
"subscription",
|
||
0,
|
||
"CNY",
|
||
f"Subscription cancelled{' (at period end)' if at_period_end else ''}",
|
||
subscription_id,
|
||
0,
|
||
)
|
||
|
||
conn.commit()
|
||
logger.info(f"Subscription cancelled: {subscription_id}")
|
||
return self.get_subscription(subscription_id)
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def change_plan(self, subscription_id: str, new_plan_id: str, prorate: bool = True) -> Subscription | None:
|
||
"""更改订阅计划"""
|
||
conn = self._get_connection()
|
||
try:
|
||
subscription = self.get_subscription(subscription_id)
|
||
if not subscription:
|
||
return None
|
||
|
||
old_plan = self.get_plan(subscription.plan_id)
|
||
new_plan = self.get_plan(new_plan_id)
|
||
|
||
if not new_plan:
|
||
raise ValueError(f"Plan {new_plan_id} not found")
|
||
|
||
now = datetime.now()
|
||
|
||
# 按比例计算差价(简化实现)
|
||
if prorate and old_plan:
|
||
# 这里应该实现实际的按比例计算逻辑
|
||
pass
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
UPDATE subscriptions
|
||
SET plan_id = ?, updated_at = ?
|
||
WHERE id = ?
|
||
""",
|
||
(new_plan_id, now, subscription_id),
|
||
)
|
||
|
||
# 记录账单历史
|
||
self._add_billing_history_internal(
|
||
conn,
|
||
subscription.tenant_id,
|
||
"subscription",
|
||
0,
|
||
new_plan.currency,
|
||
f"Plan changed from {old_plan.name if old_plan else 'unknown'} to {new_plan.name}",
|
||
subscription_id,
|
||
0,
|
||
)
|
||
|
||
conn.commit()
|
||
logger.info(f"Subscription plan changed: {subscription_id} -> {new_plan_id}")
|
||
return self.get_subscription(subscription_id)
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
# ==================== 用量计费 ====================
|
||
|
||
def record_usage(
|
||
self,
|
||
tenant_id: str,
|
||
resource_type: str,
|
||
quantity: float,
|
||
unit: str,
|
||
description: str | None = None,
|
||
metadata: dict | None = None,
|
||
) -> UsageRecord:
|
||
"""记录用量"""
|
||
conn = self._get_connection()
|
||
try:
|
||
# 计算费用
|
||
cost = self._calculate_usage_cost(resource_type, quantity)
|
||
|
||
record_id = str(uuid.uuid4())
|
||
record = UsageRecord(
|
||
id=record_id,
|
||
tenant_id=tenant_id,
|
||
resource_type=resource_type,
|
||
quantity=quantity,
|
||
unit=unit,
|
||
recorded_at=datetime.now(),
|
||
cost=cost,
|
||
description=description,
|
||
metadata=metadata or {},
|
||
)
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
INSERT INTO usage_records
|
||
(id, tenant_id, resource_type, quantity, unit, recorded_at, cost, description, metadata)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""",
|
||
(
|
||
record.id,
|
||
record.tenant_id,
|
||
record.resource_type,
|
||
record.quantity,
|
||
record.unit,
|
||
record.recorded_at,
|
||
record.cost,
|
||
record.description,
|
||
json.dumps(record.metadata),
|
||
),
|
||
)
|
||
|
||
conn.commit()
|
||
return record
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def get_usage_summary(
|
||
self, tenant_id: str, start_date: datetime | None = None, end_date: datetime | None = None
|
||
) -> dict[str, Any]:
|
||
"""获取用量汇总"""
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
|
||
query = """
|
||
SELECT
|
||
resource_type,
|
||
SUM(quantity) as total_quantity,
|
||
SUM(cost) as total_cost,
|
||
COUNT(*) as record_count
|
||
FROM usage_records
|
||
WHERE tenant_id = ?
|
||
"""
|
||
params = [tenant_id]
|
||
|
||
if start_date:
|
||
query += " AND recorded_at >= ?"
|
||
params.append(start_date)
|
||
if end_date:
|
||
query += " AND recorded_at <= ?"
|
||
params.append(end_date)
|
||
|
||
query += " GROUP BY resource_type"
|
||
|
||
cursor.execute(query, params)
|
||
rows = cursor.fetchall()
|
||
|
||
summary = {}
|
||
total_cost = 0
|
||
|
||
for row in rows:
|
||
summary[row["resource_type"]] = {
|
||
"quantity": row["total_quantity"],
|
||
"cost": row["total_cost"],
|
||
"records": row["record_count"],
|
||
}
|
||
total_cost += row["total_cost"]
|
||
|
||
return {
|
||
"tenant_id": tenant_id,
|
||
"period": {
|
||
"start": start_date.isoformat() if start_date else None,
|
||
"end": end_date.isoformat() if end_date else None,
|
||
},
|
||
"breakdown": summary,
|
||
"total_cost": total_cost,
|
||
}
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def _calculate_usage_cost(self, resource_type: str, quantity: float) -> float:
|
||
"""计算用量费用"""
|
||
pricing = self.USAGE_PRICING.get(resource_type)
|
||
if not pricing:
|
||
return 0.0
|
||
|
||
# 扣除免费额度
|
||
chargeable = max(0, quantity - pricing.get("free_quota", 0))
|
||
|
||
# 计算费用
|
||
if pricing["unit"] == "1000_calls":
|
||
return (chargeable / 1000) * pricing["price"]
|
||
else:
|
||
return chargeable * pricing["price"]
|
||
|
||
# ==================== 支付管理 ====================
|
||
|
||
def create_payment(
|
||
self,
|
||
tenant_id: str,
|
||
amount: float,
|
||
currency: str,
|
||
provider: str,
|
||
subscription_id: str | None = None,
|
||
invoice_id: str | None = None,
|
||
payment_method: str | None = None,
|
||
payment_details: dict | None = None,
|
||
) -> Payment:
|
||
"""创建支付记录"""
|
||
conn = self._get_connection()
|
||
try:
|
||
payment_id = str(uuid.uuid4())
|
||
now = datetime.now()
|
||
|
||
payment = Payment(
|
||
id=payment_id,
|
||
tenant_id=tenant_id,
|
||
subscription_id=subscription_id,
|
||
invoice_id=invoice_id,
|
||
amount=amount,
|
||
currency=currency,
|
||
provider=provider,
|
||
provider_payment_id=None,
|
||
status=PaymentStatus.PENDING.value,
|
||
payment_method=payment_method,
|
||
payment_details=payment_details or {},
|
||
paid_at=None,
|
||
failed_at=None,
|
||
failure_reason=None,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
INSERT INTO payments
|
||
(id, tenant_id, subscription_id, invoice_id, amount, currency,
|
||
provider, provider_payment_id, status, payment_method, payment_details,
|
||
paid_at, failed_at, failure_reason, created_at, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""",
|
||
(
|
||
payment.id,
|
||
payment.tenant_id,
|
||
payment.subscription_id,
|
||
payment.invoice_id,
|
||
payment.amount,
|
||
payment.currency,
|
||
payment.provider,
|
||
payment.provider_payment_id,
|
||
payment.status,
|
||
payment.payment_method,
|
||
json.dumps(payment.payment_details),
|
||
payment.paid_at,
|
||
payment.failed_at,
|
||
payment.failure_reason,
|
||
payment.created_at,
|
||
payment.updated_at,
|
||
),
|
||
)
|
||
|
||
conn.commit()
|
||
return payment
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def confirm_payment(self, payment_id: str, provider_payment_id: str | None = None) -> Payment | None:
|
||
"""确认支付完成"""
|
||
conn = self._get_connection()
|
||
try:
|
||
payment = self._get_payment_internal(conn, payment_id)
|
||
if not payment:
|
||
return None
|
||
|
||
now = datetime.now()
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
UPDATE payments
|
||
SET status = 'completed', provider_payment_id = ?, paid_at = ?, updated_at = ?
|
||
WHERE id = ?
|
||
""",
|
||
(provider_payment_id, now, now, payment_id),
|
||
)
|
||
|
||
# 如果有关联发票,更新发票状态
|
||
if payment.invoice_id:
|
||
cursor.execute(
|
||
"""
|
||
UPDATE invoices
|
||
SET status = 'paid', amount_paid = amount_due, paid_at = ?
|
||
WHERE id = ?
|
||
""",
|
||
(now, payment.invoice_id),
|
||
)
|
||
|
||
# 如果有关联订阅,激活订阅
|
||
if payment.subscription_id:
|
||
cursor.execute(
|
||
"""
|
||
UPDATE subscriptions
|
||
SET status = 'active', updated_at = ?
|
||
WHERE id = ? AND status = 'pending'
|
||
""",
|
||
(now, payment.subscription_id),
|
||
)
|
||
|
||
# 记录账单历史
|
||
self._add_billing_history_internal(
|
||
conn,
|
||
payment.tenant_id,
|
||
"payment",
|
||
payment.amount,
|
||
payment.currency,
|
||
f"Payment completed via {payment.provider}",
|
||
payment_id,
|
||
0, # 余额更新应该在账户管理中处理
|
||
)
|
||
|
||
conn.commit()
|
||
logger.info(f"Payment confirmed: {payment_id}")
|
||
return self._get_payment_internal(conn, payment_id)
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def fail_payment(self, payment_id: str, reason: str) -> Payment | None:
|
||
"""标记支付失败"""
|
||
conn = self._get_connection()
|
||
try:
|
||
now = datetime.now()
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
UPDATE payments
|
||
SET status = 'failed', failure_reason = ?, failed_at = ?, updated_at = ?
|
||
WHERE id = ?
|
||
""",
|
||
(reason, now, now, payment_id),
|
||
)
|
||
|
||
conn.commit()
|
||
return self._get_payment_internal(conn, payment_id)
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def get_payment(self, payment_id: str) -> Payment | None:
|
||
"""获取支付记录"""
|
||
conn = self._get_connection()
|
||
try:
|
||
return self._get_payment_internal(conn, payment_id)
|
||
finally:
|
||
conn.close()
|
||
|
||
def list_payments(
|
||
self, tenant_id: str, status: str | None = None, limit: int = 100, offset: int = 0
|
||
) -> list[Payment]:
|
||
"""列出支付记录"""
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
|
||
query = "SELECT * FROM payments WHERE tenant_id = ?"
|
||
params = [tenant_id]
|
||
|
||
if status:
|
||
query += " AND status = ?"
|
||
params.append(status)
|
||
|
||
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
||
params.extend([limit, offset])
|
||
|
||
cursor.execute(query, params)
|
||
rows = cursor.fetchall()
|
||
|
||
return [self._row_to_payment(row) for row in rows]
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def _get_payment_internal(self, conn: sqlite3.Connection, payment_id: str) -> Payment | None:
|
||
"""内部方法:获取支付记录"""
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT * FROM payments WHERE id = ?", (payment_id,))
|
||
row = cursor.fetchone()
|
||
|
||
if row:
|
||
return self._row_to_payment(row)
|
||
return None
|
||
|
||
# ==================== 发票管理 ====================
|
||
|
||
def _create_invoice_internal(
|
||
self,
|
||
conn: sqlite3.Connection,
|
||
tenant_id: str,
|
||
subscription_id: str | None,
|
||
amount: float,
|
||
currency: str,
|
||
period_start: datetime,
|
||
period_end: datetime,
|
||
description: str,
|
||
line_items: list[dict] | None = None,
|
||
) -> Invoice:
|
||
"""内部方法:创建发票"""
|
||
invoice_id = str(uuid.uuid4())
|
||
invoice_number = self._generate_invoice_number()
|
||
now = datetime.now()
|
||
due_date = now + timedelta(days=7) # 7天付款期限
|
||
|
||
invoice = Invoice(
|
||
id=invoice_id,
|
||
tenant_id=tenant_id,
|
||
subscription_id=subscription_id,
|
||
invoice_number=invoice_number,
|
||
status=InvoiceStatus.DRAFT.value,
|
||
amount_due=amount,
|
||
amount_paid=0,
|
||
currency=currency,
|
||
period_start=period_start,
|
||
period_end=period_end,
|
||
description=description,
|
||
line_items=line_items or [{"description": description, "amount": amount}],
|
||
due_date=due_date,
|
||
paid_at=None,
|
||
voided_at=None,
|
||
void_reason=None,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
INSERT INTO invoices
|
||
(id, tenant_id, subscription_id, invoice_number, status, amount_due, amount_paid,
|
||
currency, period_start, period_end, description, line_items, due_date,
|
||
paid_at, voided_at, void_reason, created_at, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""",
|
||
(
|
||
invoice.id,
|
||
invoice.tenant_id,
|
||
invoice.subscription_id,
|
||
invoice.invoice_number,
|
||
invoice.status,
|
||
invoice.amount_due,
|
||
invoice.amount_paid,
|
||
invoice.currency,
|
||
invoice.period_start,
|
||
invoice.period_end,
|
||
invoice.description,
|
||
json.dumps(invoice.line_items),
|
||
invoice.due_date,
|
||
invoice.paid_at,
|
||
invoice.voided_at,
|
||
invoice.void_reason,
|
||
invoice.created_at,
|
||
invoice.updated_at,
|
||
),
|
||
)
|
||
|
||
return invoice
|
||
|
||
def get_invoice(self, invoice_id: str) -> Invoice | None:
|
||
"""获取发票"""
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT * FROM invoices WHERE id = ?", (invoice_id,))
|
||
row = cursor.fetchone()
|
||
|
||
if row:
|
||
return self._row_to_invoice(row)
|
||
return None
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def get_invoice_by_number(self, invoice_number: str) -> Invoice | None:
|
||
"""通过发票号获取发票"""
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT * FROM invoices WHERE invoice_number = ?", (invoice_number,))
|
||
row = cursor.fetchone()
|
||
|
||
if row:
|
||
return self._row_to_invoice(row)
|
||
return None
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def list_invoices(
|
||
self, tenant_id: str, status: str | None = None, limit: int = 100, offset: int = 0
|
||
) -> list[Invoice]:
|
||
"""列出发票"""
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
|
||
query = "SELECT * FROM invoices WHERE tenant_id = ?"
|
||
params = [tenant_id]
|
||
|
||
if status:
|
||
query += " AND status = ?"
|
||
params.append(status)
|
||
|
||
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
||
params.extend([limit, offset])
|
||
|
||
cursor.execute(query, params)
|
||
rows = cursor.fetchall()
|
||
|
||
return [self._row_to_invoice(row) for row in rows]
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def void_invoice(self, invoice_id: str, reason: str) -> Invoice | None:
|
||
"""作废发票"""
|
||
conn = self._get_connection()
|
||
try:
|
||
invoice = self.get_invoice(invoice_id)
|
||
if not invoice:
|
||
return None
|
||
|
||
if invoice.status == InvoiceStatus.PAID.value:
|
||
raise ValueError("Cannot void a paid invoice")
|
||
|
||
now = datetime.now()
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
UPDATE invoices
|
||
SET status = 'void', voided_at = ?, void_reason = ?, updated_at = ?
|
||
WHERE id = ?
|
||
""",
|
||
(now, reason, now, invoice_id),
|
||
)
|
||
|
||
conn.commit()
|
||
return self.get_invoice(invoice_id)
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def _generate_invoice_number(self) -> str:
|
||
"""生成发票号"""
|
||
now = datetime.now()
|
||
prefix = f"INV-{now.strftime('%Y%m')}"
|
||
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
SELECT COUNT(*) as count FROM invoices
|
||
WHERE invoice_number LIKE ?
|
||
""",
|
||
(f"{prefix}%",),
|
||
)
|
||
row = cursor.fetchone()
|
||
count = row["count"] + 1
|
||
|
||
return f"{prefix}-{count:06d}"
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
# ==================== 退款管理 ====================
|
||
|
||
def request_refund(self, tenant_id: str, payment_id: str, amount: float, reason: str, requested_by: str) -> Refund:
|
||
"""申请退款"""
|
||
conn = self._get_connection()
|
||
try:
|
||
# 验证支付记录
|
||
payment = self._get_payment_internal(conn, payment_id)
|
||
if not payment:
|
||
raise ValueError(f"Payment {payment_id} not found")
|
||
|
||
if payment.tenant_id != tenant_id:
|
||
raise ValueError("Payment does not belong to this tenant")
|
||
|
||
if payment.status != PaymentStatus.COMPLETED.value:
|
||
raise ValueError("Can only refund completed payments")
|
||
|
||
if amount > payment.amount:
|
||
raise ValueError("Refund amount cannot exceed payment amount")
|
||
|
||
refund_id = str(uuid.uuid4())
|
||
now = datetime.now()
|
||
|
||
refund = Refund(
|
||
id=refund_id,
|
||
tenant_id=tenant_id,
|
||
payment_id=payment_id,
|
||
invoice_id=payment.invoice_id,
|
||
amount=amount,
|
||
currency=payment.currency,
|
||
reason=reason,
|
||
status=RefundStatus.PENDING.value,
|
||
requested_by=requested_by,
|
||
requested_at=now,
|
||
approved_by=None,
|
||
approved_at=None,
|
||
completed_at=None,
|
||
provider_refund_id=None,
|
||
metadata={},
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
INSERT INTO refunds
|
||
(id, tenant_id, payment_id, invoice_id, amount, currency, reason, status,
|
||
requested_by, requested_at, approved_by, approved_at, completed_at,
|
||
provider_refund_id, metadata, created_at, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""",
|
||
(
|
||
refund.id,
|
||
refund.tenant_id,
|
||
refund.payment_id,
|
||
refund.invoice_id,
|
||
refund.amount,
|
||
refund.currency,
|
||
refund.reason,
|
||
refund.status,
|
||
refund.requested_by,
|
||
refund.requested_at,
|
||
refund.approved_by,
|
||
refund.approved_at,
|
||
refund.completed_at,
|
||
refund.provider_refund_id,
|
||
json.dumps(refund.metadata),
|
||
refund.created_at,
|
||
refund.updated_at,
|
||
),
|
||
)
|
||
|
||
conn.commit()
|
||
logger.info(f"Refund requested: {refund_id} for payment {payment_id}")
|
||
return refund
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def approve_refund(self, refund_id: str, approved_by: str) -> Refund | None:
|
||
"""批准退款"""
|
||
conn = self._get_connection()
|
||
try:
|
||
refund = self._get_refund_internal(conn, refund_id)
|
||
if not refund:
|
||
return None
|
||
|
||
if refund.status != RefundStatus.PENDING.value:
|
||
raise ValueError("Can only approve pending refunds")
|
||
|
||
now = datetime.now()
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
UPDATE refunds
|
||
SET status = 'approved', approved_by = ?, approved_at = ?, updated_at = ?
|
||
WHERE id = ?
|
||
""",
|
||
(approved_by, now, now, refund_id),
|
||
)
|
||
|
||
conn.commit()
|
||
return self._get_refund_internal(conn, refund_id)
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def complete_refund(self, refund_id: str, provider_refund_id: str | None = None) -> Refund | None:
|
||
"""完成退款"""
|
||
conn = self._get_connection()
|
||
try:
|
||
refund = self._get_refund_internal(conn, refund_id)
|
||
if not refund:
|
||
return None
|
||
|
||
now = datetime.now()
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
UPDATE refunds
|
||
SET status = 'completed', provider_refund_id = ?, completed_at = ?, updated_at = ?
|
||
WHERE id = ?
|
||
""",
|
||
(provider_refund_id, now, now, refund_id),
|
||
)
|
||
|
||
# 更新原支付记录状态
|
||
cursor.execute(
|
||
"""
|
||
UPDATE payments
|
||
SET status = 'refunded', updated_at = ?
|
||
WHERE id = ?
|
||
""",
|
||
(now, refund.payment_id),
|
||
)
|
||
|
||
# 记录账单历史
|
||
self._add_billing_history_internal(
|
||
conn,
|
||
refund.tenant_id,
|
||
"refund",
|
||
-refund.amount,
|
||
refund.currency,
|
||
f"Refund processed: {refund.reason}",
|
||
refund_id,
|
||
0,
|
||
)
|
||
|
||
conn.commit()
|
||
logger.info(f"Refund completed: {refund_id}")
|
||
return self._get_refund_internal(conn, refund_id)
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def reject_refund(self, refund_id: str, reason: str) -> Refund | None:
|
||
"""拒绝退款"""
|
||
conn = self._get_connection()
|
||
try:
|
||
refund = self._get_refund_internal(conn, refund_id)
|
||
if not refund:
|
||
return None
|
||
|
||
now = datetime.now()
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
UPDATE refunds
|
||
SET status = 'rejected', metadata = json_set(metadata, '$.rejection_reason', ?), updated_at = ?
|
||
WHERE id = ?
|
||
""",
|
||
(reason, now, refund_id),
|
||
)
|
||
|
||
conn.commit()
|
||
return self._get_refund_internal(conn, refund_id)
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def get_refund(self, refund_id: str) -> Refund | None:
|
||
"""获取退款记录"""
|
||
conn = self._get_connection()
|
||
try:
|
||
return self._get_refund_internal(conn, refund_id)
|
||
finally:
|
||
conn.close()
|
||
|
||
def list_refunds(
|
||
self, tenant_id: str, status: str | None = None, limit: int = 100, offset: int = 0
|
||
) -> list[Refund]:
|
||
"""列出退款记录"""
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
|
||
query = "SELECT * FROM refunds WHERE tenant_id = ?"
|
||
params = [tenant_id]
|
||
|
||
if status:
|
||
query += " AND status = ?"
|
||
params.append(status)
|
||
|
||
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
||
params.extend([limit, offset])
|
||
|
||
cursor.execute(query, params)
|
||
rows = cursor.fetchall()
|
||
|
||
return [self._row_to_refund(row) for row in rows]
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
def _get_refund_internal(self, conn: sqlite3.Connection, refund_id: str) -> Refund | None:
|
||
"""内部方法:获取退款记录"""
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT * FROM refunds WHERE id = ?", (refund_id,))
|
||
row = cursor.fetchone()
|
||
|
||
if row:
|
||
return self._row_to_refund(row)
|
||
return None
|
||
|
||
# ==================== 账单历史 ====================
|
||
|
||
def _add_billing_history_internal(
|
||
self,
|
||
conn: sqlite3.Connection,
|
||
tenant_id: str,
|
||
type: str,
|
||
amount: float,
|
||
currency: str,
|
||
description: str,
|
||
reference_id: str,
|
||
balance_after: float,
|
||
):
|
||
"""内部方法:添加账单历史"""
|
||
history_id = str(uuid.uuid4())
|
||
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
INSERT INTO billing_history
|
||
(id, tenant_id, type, amount, currency, description, reference_id, balance_after, created_at, metadata)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""",
|
||
(
|
||
history_id,
|
||
tenant_id,
|
||
type,
|
||
amount,
|
||
currency,
|
||
description,
|
||
reference_id,
|
||
balance_after,
|
||
datetime.now(),
|
||
json.dumps({}),
|
||
),
|
||
)
|
||
|
||
def get_billing_history(
|
||
self,
|
||
tenant_id: str,
|
||
start_date: datetime | None = None,
|
||
end_date: datetime | None = None,
|
||
limit: int = 100,
|
||
offset: int = 0,
|
||
) -> list[BillingHistory]:
|
||
"""获取账单历史"""
|
||
conn = self._get_connection()
|
||
try:
|
||
cursor = conn.cursor()
|
||
|
||
query = "SELECT * FROM billing_history WHERE tenant_id = ?"
|
||
params = [tenant_id]
|
||
|
||
if start_date:
|
||
query += " AND created_at >= ?"
|
||
params.append(start_date)
|
||
if end_date:
|
||
query += " AND created_at <= ?"
|
||
params.append(end_date)
|
||
|
||
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
||
params.extend([limit, offset])
|
||
|
||
cursor.execute(query, params)
|
||
rows = cursor.fetchall()
|
||
|
||
return [self._row_to_billing_history(row) for row in rows]
|
||
|
||
finally:
|
||
conn.close()
|
||
|
||
# ==================== 支付提供商集成 ====================
|
||
|
||
def create_stripe_checkout_session(
|
||
self, tenant_id: str, plan_id: str, success_url: str, cancel_url: str, billing_cycle: str = "monthly"
|
||
) -> dict[str, Any]:
|
||
"""创建 Stripe Checkout 会话(占位实现)"""
|
||
# 这里应该集成 Stripe SDK
|
||
# 简化实现,返回模拟数据
|
||
return {
|
||
"session_id": f"cs_{uuid.uuid4().hex[:24]}",
|
||
"url": f"https://checkout.stripe.com/mock/{uuid.uuid4().hex[:24]}",
|
||
"status": "created",
|
||
"provider": "stripe",
|
||
}
|
||
|
||
def create_alipay_order(self, tenant_id: str, plan_id: str, billing_cycle: str = "monthly") -> dict[str, Any]:
|
||
"""创建支付宝订单(占位实现)"""
|
||
# 这里应该集成支付宝 SDK
|
||
plan = self.get_plan(plan_id)
|
||
amount = plan.price_yearly if billing_cycle == "yearly" else plan.price_monthly
|
||
|
||
return {
|
||
"order_id": f"ALI{datetime.now().strftime('%Y%m%d%H%M%S')}{uuid.uuid4().hex[:8].upper()}",
|
||
"amount": amount,
|
||
"currency": plan.currency,
|
||
"qr_code_url": f"https://qr.alipay.com/mock/{uuid.uuid4().hex[:16]}",
|
||
"status": "pending",
|
||
"provider": "alipay",
|
||
}
|
||
|
||
def create_wechat_order(self, tenant_id: str, plan_id: str, billing_cycle: str = "monthly") -> dict[str, Any]:
|
||
"""创建微信支付订单(占位实现)"""
|
||
# 这里应该集成微信支付 SDK
|
||
plan = self.get_plan(plan_id)
|
||
amount = plan.price_yearly if billing_cycle == "yearly" else plan.price_monthly
|
||
|
||
return {
|
||
"order_id": f"WX{datetime.now().strftime('%Y%m%d%H%M%S')}{uuid.uuid4().hex[:8].upper()}",
|
||
"amount": amount,
|
||
"currency": plan.currency,
|
||
"prepay_id": f"wx{uuid.uuid4().hex[:32]}",
|
||
"status": "pending",
|
||
"provider": "wechat",
|
||
}
|
||
|
||
def handle_webhook(self, provider: str, payload: dict[str, Any]) -> bool:
|
||
"""处理支付提供商的 Webhook(占位实现)"""
|
||
# 这里应该实现实际的 Webhook 处理逻辑
|
||
logger.info(f"Received webhook from {provider}: {payload.get('event_type', 'unknown')}")
|
||
|
||
event_type = payload.get("event_type", "")
|
||
|
||
if provider == "stripe":
|
||
if event_type == "checkout.session.completed":
|
||
# 处理支付完成
|
||
pass
|
||
elif event_type == "invoice.payment_failed":
|
||
# 处理支付失败
|
||
pass
|
||
|
||
elif provider in ["alipay", "wechat"]:
|
||
if payload.get("trade_status") == "TRADE_SUCCESS":
|
||
# 处理支付完成
|
||
pass
|
||
|
||
return True
|
||
|
||
# ==================== 辅助方法 ====================
|
||
|
||
def _row_to_plan(self, row: sqlite3.Row) -> SubscriptionPlan:
|
||
"""数据库行转换为 SubscriptionPlan 对象"""
|
||
return SubscriptionPlan(
|
||
id=row["id"],
|
||
name=row["name"],
|
||
tier=row["tier"],
|
||
description=row["description"] or "",
|
||
price_monthly=row["price_monthly"],
|
||
price_yearly=row["price_yearly"],
|
||
currency=row["currency"],
|
||
features=json.loads(row["features"] or "[]"),
|
||
limits=json.loads(row["limits"] or "{}"),
|
||
is_active=bool(row["is_active"]),
|
||
created_at=(
|
||
datetime.fromisoformat(row["created_at"]) if isinstance(row["created_at"], str) else row["created_at"]
|
||
),
|
||
updated_at=(
|
||
datetime.fromisoformat(row["updated_at"]) if isinstance(row["updated_at"], str) else row["updated_at"]
|
||
),
|
||
metadata=json.loads(row["metadata"] or "{}"),
|
||
)
|
||
|
||
def _row_to_subscription(self, row: sqlite3.Row) -> Subscription:
|
||
"""数据库行转换为 Subscription 对象"""
|
||
return Subscription(
|
||
id=row["id"],
|
||
tenant_id=row["tenant_id"],
|
||
plan_id=row["plan_id"],
|
||
status=row["status"],
|
||
current_period_start=(
|
||
datetime.fromisoformat(row["current_period_start"])
|
||
if row["current_period_start"] and isinstance(row["current_period_start"], str)
|
||
else row["current_period_start"]
|
||
),
|
||
current_period_end=(
|
||
datetime.fromisoformat(row["current_period_end"])
|
||
if row["current_period_end"] and isinstance(row["current_period_end"], str)
|
||
else row["current_period_end"]
|
||
),
|
||
cancel_at_period_end=bool(row["cancel_at_period_end"]),
|
||
canceled_at=(
|
||
datetime.fromisoformat(row["canceled_at"])
|
||
if row["canceled_at"] and isinstance(row["canceled_at"], str)
|
||
else row["canceled_at"]
|
||
),
|
||
trial_start=(
|
||
datetime.fromisoformat(row["trial_start"])
|
||
if row["trial_start"] and isinstance(row["trial_start"], str)
|
||
else row["trial_start"]
|
||
),
|
||
trial_end=(
|
||
datetime.fromisoformat(row["trial_end"])
|
||
if row["trial_end"] and isinstance(row["trial_end"], str)
|
||
else row["trial_end"]
|
||
),
|
||
payment_provider=row["payment_provider"],
|
||
provider_subscription_id=row["provider_subscription_id"],
|
||
created_at=(
|
||
datetime.fromisoformat(row["created_at"]) if isinstance(row["created_at"], str) else row["created_at"]
|
||
),
|
||
updated_at=(
|
||
datetime.fromisoformat(row["updated_at"]) if isinstance(row["updated_at"], str) else row["updated_at"]
|
||
),
|
||
metadata=json.loads(row["metadata"] or "{}"),
|
||
)
|
||
|
||
def _row_to_usage(self, row: sqlite3.Row) -> UsageRecord:
|
||
"""数据库行转换为 UsageRecord 对象"""
|
||
return UsageRecord(
|
||
id=row["id"],
|
||
tenant_id=row["tenant_id"],
|
||
resource_type=row["resource_type"],
|
||
quantity=row["quantity"],
|
||
unit=row["unit"],
|
||
recorded_at=(
|
||
datetime.fromisoformat(row["recorded_at"])
|
||
if isinstance(row["recorded_at"], str)
|
||
else row["recorded_at"]
|
||
),
|
||
cost=row["cost"],
|
||
description=row["description"],
|
||
metadata=json.loads(row["metadata"] or "{}"),
|
||
)
|
||
|
||
def _row_to_payment(self, row: sqlite3.Row) -> Payment:
|
||
"""数据库行转换为 Payment 对象"""
|
||
return Payment(
|
||
id=row["id"],
|
||
tenant_id=row["tenant_id"],
|
||
subscription_id=row["subscription_id"],
|
||
invoice_id=row["invoice_id"],
|
||
amount=row["amount"],
|
||
currency=row["currency"],
|
||
provider=row["provider"],
|
||
provider_payment_id=row["provider_payment_id"],
|
||
status=row["status"],
|
||
payment_method=row["payment_method"],
|
||
payment_details=json.loads(row["payment_details"] or "{}"),
|
||
paid_at=(
|
||
datetime.fromisoformat(row["paid_at"])
|
||
if row["paid_at"] and isinstance(row["paid_at"], str)
|
||
else row["paid_at"]
|
||
),
|
||
failed_at=(
|
||
datetime.fromisoformat(row["failed_at"])
|
||
if row["failed_at"] and isinstance(row["failed_at"], str)
|
||
else row["failed_at"]
|
||
),
|
||
failure_reason=row["failure_reason"],
|
||
created_at=(
|
||
datetime.fromisoformat(row["created_at"]) if isinstance(row["created_at"], str) else row["created_at"]
|
||
),
|
||
updated_at=(
|
||
datetime.fromisoformat(row["updated_at"]) if isinstance(row["updated_at"], str) else row["updated_at"]
|
||
),
|
||
)
|
||
|
||
def _row_to_invoice(self, row: sqlite3.Row) -> Invoice:
|
||
"""数据库行转换为 Invoice 对象"""
|
||
return Invoice(
|
||
id=row["id"],
|
||
tenant_id=row["tenant_id"],
|
||
subscription_id=row["subscription_id"],
|
||
invoice_number=row["invoice_number"],
|
||
status=row["status"],
|
||
amount_due=row["amount_due"],
|
||
amount_paid=row["amount_paid"],
|
||
currency=row["currency"],
|
||
period_start=(
|
||
datetime.fromisoformat(row["period_start"])
|
||
if row["period_start"] and isinstance(row["period_start"], str)
|
||
else row["period_start"]
|
||
),
|
||
period_end=(
|
||
datetime.fromisoformat(row["period_end"])
|
||
if row["period_end"] and isinstance(row["period_end"], str)
|
||
else row["period_end"]
|
||
),
|
||
description=row["description"],
|
||
line_items=json.loads(row["line_items"] or "[]"),
|
||
due_date=(
|
||
datetime.fromisoformat(row["due_date"])
|
||
if row["due_date"] and isinstance(row["due_date"], str)
|
||
else row["due_date"]
|
||
),
|
||
paid_at=(
|
||
datetime.fromisoformat(row["paid_at"])
|
||
if row["paid_at"] and isinstance(row["paid_at"], str)
|
||
else row["paid_at"]
|
||
),
|
||
voided_at=(
|
||
datetime.fromisoformat(row["voided_at"])
|
||
if row["voided_at"] and isinstance(row["voided_at"], str)
|
||
else row["voided_at"]
|
||
),
|
||
void_reason=row["void_reason"],
|
||
created_at=(
|
||
datetime.fromisoformat(row["created_at"]) if isinstance(row["created_at"], str) else row["created_at"]
|
||
),
|
||
updated_at=(
|
||
datetime.fromisoformat(row["updated_at"]) if isinstance(row["updated_at"], str) else row["updated_at"]
|
||
),
|
||
)
|
||
|
||
def _row_to_refund(self, row: sqlite3.Row) -> Refund:
|
||
"""数据库行转换为 Refund 对象"""
|
||
return Refund(
|
||
id=row["id"],
|
||
tenant_id=row["tenant_id"],
|
||
payment_id=row["payment_id"],
|
||
invoice_id=row["invoice_id"],
|
||
amount=row["amount"],
|
||
currency=row["currency"],
|
||
reason=row["reason"],
|
||
status=row["status"],
|
||
requested_by=row["requested_by"],
|
||
requested_at=(
|
||
datetime.fromisoformat(row["requested_at"])
|
||
if isinstance(row["requested_at"], str)
|
||
else row["requested_at"]
|
||
),
|
||
approved_by=row["approved_by"],
|
||
approved_at=(
|
||
datetime.fromisoformat(row["approved_at"])
|
||
if row["approved_at"] and isinstance(row["approved_at"], str)
|
||
else row["approved_at"]
|
||
),
|
||
completed_at=(
|
||
datetime.fromisoformat(row["completed_at"])
|
||
if row["completed_at"] and isinstance(row["completed_at"], str)
|
||
else row["completed_at"]
|
||
),
|
||
provider_refund_id=row["provider_refund_id"],
|
||
metadata=json.loads(row["metadata"] or "{}"),
|
||
created_at=(
|
||
datetime.fromisoformat(row["created_at"]) if isinstance(row["created_at"], str) else row["created_at"]
|
||
),
|
||
updated_at=(
|
||
datetime.fromisoformat(row["updated_at"]) if isinstance(row["updated_at"], str) else row["updated_at"]
|
||
),
|
||
)
|
||
|
||
def _row_to_billing_history(self, row: sqlite3.Row) -> BillingHistory:
|
||
"""数据库行转换为 BillingHistory 对象"""
|
||
return BillingHistory(
|
||
id=row["id"],
|
||
tenant_id=row["tenant_id"],
|
||
type=row["type"],
|
||
amount=row["amount"],
|
||
currency=row["currency"],
|
||
description=row["description"],
|
||
reference_id=row["reference_id"],
|
||
balance_after=row["balance_after"],
|
||
created_at=(
|
||
datetime.fromisoformat(row["created_at"]) if isinstance(row["created_at"], str) else row["created_at"]
|
||
),
|
||
metadata=json.loads(row["metadata"] or "{}"),
|
||
)
|
||
|
||
|
||
# 全局订阅管理器实例
|
||
subscription_manager = None
|
||
|
||
|
||
def get_subscription_manager(db_path: str = "insightflow.db") -> SubscriptionManager:
|
||
"""获取订阅管理器实例(单例模式)"""
|
||
global subscription_manager
|
||
if subscription_manager is None:
|
||
subscription_manager = SubscriptionManager(db_path)
|
||
return subscription_manager
|