""" 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, # 0.5元/分钟 "free_quota": 60 # 每月免费额度 }, "storage": { "unit": "gb", "price": 10.0, # 10元/GB/月 "free_quota": 0.1 # 100MB免费 }, "api_call": { "unit": "1000_calls", "price": 5.0, # 5元/1000次 "free_quota": 1000 # 每月免费1000次 }, "export": { "unit": "page", "price": 0.1, # 0.1元/页(PDF导出) "free_quota": 100 } } 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): """初始化数据库表""" 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): """初始化默认订阅计划""" 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