""" 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") -> None: 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, ) -> None: """内部方法:添加账单历史""" 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