Files
insightflow/backend/growth_manager.py
OpenClaw Bot be22b763fa fix: auto-fix code issues (cron)
- 修复重复导入/字段
- 修复异常处理
- 修复PEP8格式问题
- 添加类型注解
- 修复重复函数定义 (health_check, create_webhook_endpoint, etc)
- 修复未定义名称 (SearchOperator, TenantTier, Query, Body, logger)
- 修复 workflow_manager.py 的类定义重复问题
- 添加缺失的导入
2026-02-27 09:18:58 +08:00

2071 lines
72 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
InsightFlow Growth Manager - Phase 8 Task 5
运营与增长工具模块
- 用户行为分析Mixpanel/Amplitude 集成)
- A/B 测试框架
- 邮件营销自动化
- 推荐系统(邀请返利、团队升级激励)
作者: InsightFlow Team
"""
import os
import json
import sqlite3
import httpx
import asyncio
import random
from typing import List, Dict, Optional, Any, Tuple
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
import hashlib
import uuid
import re
# Database path
DB_PATH = os.path.join(os.path.dirname(__file__), "insightflow.db")
class EventType(str, Enum):
"""事件类型"""
PAGE_VIEW = "page_view" # 页面浏览
FEATURE_USE = "feature_use" # 功能使用
CONVERSION = "conversion" # 转化
SIGNUP = "signup" # 注册
LOGIN = "login" # 登录
UPGRADE = "upgrade" # 升级
DOWNGRADE = "downgrade" # 降级
CANCEL = "cancel" # 取消订阅
INVITE_SENT = "invite_sent" # 发送邀请
INVITE_ACCEPTED = "invite_accepted" # 接受邀请
REFERRAL_REWARD = "referral_reward" # 推荐奖励
class ExperimentStatus(str, Enum):
"""实验状态"""
DRAFT = "draft" # 草稿
RUNNING = "running" # 运行中
PAUSED = "paused" # 暂停
COMPLETED = "completed" # 已完成
ARCHIVED = "archived" # 已归档
class TrafficAllocationType(str, Enum):
"""流量分配类型"""
RANDOM = "random" # 随机分配
STRATIFIED = "stratified" # 分层分配
TARGETED = "targeted" # 定向分配
class EmailTemplateType(str, Enum):
"""邮件模板类型"""
WELCOME = "welcome" # 欢迎邮件
ONBOARDING = "onboarding" # 引导邮件
FEATURE_ANNOUNCEMENT = "feature_announcement" # 功能公告
CHURN_RECOVERY = "churn_recovery" # 流失挽回
UPGRADE_PROMPT = "upgrade_prompt" # 升级提示
REFERRAL = "referral" # 推荐邀请
NEWSLETTER = "newsletter" # 新闻通讯
class EmailStatus(str, Enum):
"""邮件状态"""
DRAFT = "draft" # 草稿
SCHEDULED = "scheduled" # 已计划
SENDING = "sending" # 发送中
SENT = "sent" # 已发送
DELIVERED = "delivered" # 已送达
OPENED = "opened" # 已打开
CLICKED = "clicked" # 已点击
BOUNCED = "bounced" # 退信
FAILED = "failed" # 失败
class WorkflowTriggerType(str, Enum):
"""工作流触发类型"""
USER_SIGNUP = "user_signup" # 用户注册
USER_LOGIN = "user_login" # 用户登录
SUBSCRIPTION_CREATED = "subscription_created" # 创建订阅
SUBSCRIPTION_CANCELLED = "subscription_cancelled" # 取消订阅
INACTIVITY = "inactivity" # 不活跃
MILESTONE = "milestone" # 里程碑
CUSTOM_EVENT = "custom_event" # 自定义事件
class ReferralStatus(str, Enum):
"""推荐状态"""
PENDING = "pending" # 待处理
CONVERTED = "converted" # 已转化
REWARDED = "rewarded" # 已奖励
EXPIRED = "expired" # 已过期
@dataclass
class AnalyticsEvent:
"""分析事件"""
id: str
tenant_id: str
user_id: str
event_type: EventType
event_name: str
properties: Dict[str, Any] # 事件属性
timestamp: datetime
session_id: Optional[str]
device_info: Dict[str, str] # 设备信息
referrer: Optional[str]
utm_source: Optional[str]
utm_medium: Optional[str]
utm_campaign: Optional[str]
@dataclass
class UserProfile:
"""用户画像"""
id: str
tenant_id: str
user_id: str
first_seen: datetime
last_seen: datetime
total_sessions: int
total_events: int
feature_usage: Dict[str, int] # 功能使用次数
subscription_history: List[Dict]
ltv: float # 生命周期价值
churn_risk_score: float # 流失风险分数
engagement_score: float # 参与度分数
created_at: datetime
updated_at: datetime
@dataclass
class Funnel:
"""转化漏斗"""
id: str
tenant_id: str
name: str
description: str
steps: List[Dict] # 漏斗步骤
created_at: datetime
updated_at: datetime
@dataclass
class FunnelAnalysis:
"""漏斗分析结果"""
funnel_id: str
period_start: datetime
period_end: datetime
total_users: int
step_conversions: List[Dict] # 每步转化数据
overall_conversion: float # 总体转化率
drop_off_points: List[Dict] # 流失点
@dataclass
class Experiment:
"""A/B 测试实验"""
id: str
tenant_id: str
name: str
description: str
hypothesis: str
status: ExperimentStatus
variants: List[Dict] # 实验变体
traffic_allocation: TrafficAllocationType
traffic_split: Dict[str, float] # 流量分配比例
target_audience: Dict # 目标受众
primary_metric: str # 主要指标
secondary_metrics: List[str] # 次要指标
start_date: Optional[datetime]
end_date: Optional[datetime]
min_sample_size: int # 最小样本量
confidence_level: float # 置信水平
created_at: datetime
updated_at: datetime
created_by: str
@dataclass
class ExperimentResult:
"""实验结果"""
id: str
experiment_id: str
variant_id: str
metric_name: str
sample_size: int
mean_value: float
std_dev: float
confidence_interval: Tuple[float, float]
p_value: float
is_significant: bool
uplift: float # 提升幅度
created_at: datetime
@dataclass
class EmailTemplate:
"""邮件模板"""
id: str
tenant_id: str
name: str
template_type: EmailTemplateType
subject: str
html_content: str
text_content: str
variables: List[str] # 模板变量
preview_text: Optional[str]
from_name: str
from_email: str
reply_to: Optional[str]
is_active: bool
created_at: datetime
updated_at: datetime
@dataclass
class EmailCampaign:
"""邮件营销活动"""
id: str
tenant_id: str
name: str
template_id: str
status: str
recipient_count: int
sent_count: int
delivered_count: int
opened_count: int
clicked_count: int
bounced_count: int
failed_count: int
scheduled_at: Optional[datetime]
started_at: Optional[datetime]
completed_at: Optional[datetime]
created_at: datetime
@dataclass
class EmailLog:
"""邮件发送记录"""
id: str
campaign_id: str
tenant_id: str
user_id: str
email: str
template_id: str
status: EmailStatus
subject: str
sent_at: Optional[datetime]
delivered_at: Optional[datetime]
opened_at: Optional[datetime]
clicked_at: Optional[datetime]
ip_address: Optional[str]
user_agent: Optional[str]
error_message: Optional[str]
created_at: datetime
@dataclass
class AutomationWorkflow:
"""自动化工作流"""
id: str
tenant_id: str
name: str
description: str
trigger_type: WorkflowTriggerType
trigger_conditions: Dict # 触发条件
actions: List[Dict] # 执行动作
is_active: bool
execution_count: int
created_at: datetime
updated_at: datetime
@dataclass
class ReferralProgram:
"""推荐计划"""
id: str
tenant_id: str
name: str
description: str
referrer_reward_type: str # 奖励类型: credit/discount/feature
referrer_reward_value: float
referee_reward_type: str
referee_reward_value: float
max_referrals_per_user: int # 每用户最大推荐数
referral_code_length: int
expiry_days: int # 推荐码过期天数
is_active: bool
created_at: datetime
updated_at: datetime
@dataclass
class Referral:
"""推荐记录"""
id: str
program_id: str
tenant_id: str
referrer_id: str # 推荐人
referee_id: Optional[str] # 被推荐人
referral_code: str
status: ReferralStatus
referrer_rewarded: bool
referee_rewarded: bool
referrer_reward_value: float
referee_reward_value: float
converted_at: Optional[datetime]
rewarded_at: Optional[datetime]
expires_at: datetime
created_at: datetime
@dataclass
class TeamIncentive:
"""团队升级激励"""
id: str
tenant_id: str
name: str
description: str
target_tier: str # 目标层级
min_team_size: int
incentive_type: str # 激励类型
incentive_value: float
valid_from: datetime
valid_until: datetime
is_active: bool
created_at: datetime
class GrowthManager:
"""运营与增长管理主类"""
def __init__(self, db_path: str = DB_PATH):
self.db_path = db_path
self.mixpanel_token = os.getenv("MIXPANEL_TOKEN", "")
self.amplitude_api_key = os.getenv("AMPLITUDE_API_KEY", "")
self.segment_write_key = os.getenv("SEGMENT_WRITE_KEY", "")
self.sendgrid_api_key = os.getenv("SENDGRID_API_KEY", "")
def _get_db(self):
"""获取数据库连接"""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
# ==================== 用户行为分析 ====================
async def track_event(
self,
tenant_id: str,
user_id: str,
event_type: EventType,
event_name: str,
properties: Dict = None,
session_id: str = None,
device_info: Dict = None,
referrer: str = None,
utm_params: Dict = None,
) -> AnalyticsEvent:
"""追踪事件"""
event_id = f"evt_{uuid.uuid4().hex[:16]}"
now = datetime.now()
event = AnalyticsEvent(
id=event_id,
tenant_id=tenant_id,
user_id=user_id,
event_type=event_type,
event_name=event_name,
properties=properties or {},
timestamp=now,
session_id=session_id,
device_info=device_info or {},
referrer=referrer,
utm_source=utm_params.get("source") if utm_params else None,
utm_medium=utm_params.get("medium") if utm_params else None,
utm_campaign=utm_params.get("campaign") if utm_params else None,
)
with self._get_db() as conn:
conn.execute(
"""
INSERT INTO analytics_events
(id, tenant_id, user_id, event_type, event_name, properties, timestamp,
session_id, device_info, referrer, utm_source, utm_medium, utm_campaign)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
event.id,
event.tenant_id,
event.user_id,
event.event_type.value,
event.event_name,
json.dumps(event.properties),
event.timestamp.isoformat(),
event.session_id,
json.dumps(event.device_info),
event.referrer,
event.utm_source,
event.utm_medium,
event.utm_campaign,
),
)
conn.commit()
# 异步发送到第三方分析平台
asyncio.create_task(self._send_to_analytics_platforms(event))
# 更新用户画像
asyncio.create_task(self._update_user_profile(tenant_id, user_id, event_type, event_name))
return event
async def _send_to_analytics_platforms(self, event: AnalyticsEvent):
"""发送事件到第三方分析平台"""
tasks = []
if self.mixpanel_token:
tasks.append(self._send_to_mixpanel(event))
if self.amplitude_api_key:
tasks.append(self._send_to_amplitude(event))
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
async def _send_to_mixpanel(self, event: AnalyticsEvent):
"""发送事件到 Mixpanel"""
try:
headers = {"Content-Type": "application/json", "Authorization": f"Basic {self.mixpanel_token}"}
payload = {
"event": event.event_name,
"properties": {
"distinct_id": event.user_id,
"token": self.mixpanel_token,
"time": int(event.timestamp.timestamp()),
**event.properties,
},
}
async with httpx.AsyncClient() as client:
await client.post("https://api.mixpanel.com/track", headers=headers, json=[payload], timeout=10.0)
except Exception as e:
print(f"Failed to send to Mixpanel: {e}")
async def _send_to_amplitude(self, event: AnalyticsEvent):
"""发送事件到 Amplitude"""
try:
headers = {"Content-Type": "application/json"}
payload = {
"api_key": self.amplitude_api_key,
"events": [
{
"user_id": event.user_id,
"event_type": event.event_name,
"time": int(event.timestamp.timestamp() * 1000),
"event_properties": event.properties,
"user_properties": {},
}
],
}
async with httpx.AsyncClient() as client:
await client.post("https://api.amplitude.com/2/httpapi", headers=headers, json=payload, timeout=10.0)
except Exception as e:
print(f"Failed to send to Amplitude: {e}")
async def _update_user_profile(self, tenant_id: str, user_id: str, event_type: EventType, event_name: str):
"""更新用户画像"""
with self._get_db() as conn:
# 检查用户画像是否存在
row = conn.execute(
"SELECT * FROM user_profiles WHERE tenant_id = ? AND user_id = ?", (tenant_id, user_id)
).fetchone()
now = datetime.now().isoformat()
if row:
# 更新现有画像
feature_usage = json.loads(row["feature_usage"])
if event_name not in feature_usage:
feature_usage[event_name] = 0
feature_usage[event_name] += 1
conn.execute(
"""
UPDATE user_profiles
SET last_seen = ?, total_events = total_events + 1,
feature_usage = ?, updated_at = ?
WHERE id = ?
""",
(now, json.dumps(feature_usage), now, row["id"]),
)
else:
# 创建新画像
profile_id = f"up_{uuid.uuid4().hex[:16]}"
conn.execute(
"""
INSERT INTO user_profiles
(id, tenant_id, user_id, first_seen, last_seen, total_sessions,
total_events, feature_usage, subscription_history, ltv,
churn_risk_score, engagement_score, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
profile_id,
tenant_id,
user_id,
now,
now,
1,
1,
json.dumps({event_name: 1}),
"[]",
0.0,
0.0,
0.5,
now,
now,
),
)
conn.commit()
def get_user_profile(self, tenant_id: str, user_id: str) -> Optional[UserProfile]:
"""获取用户画像"""
with self._get_db() as conn:
row = conn.execute(
"SELECT * FROM user_profiles WHERE tenant_id = ? AND user_id = ?", (tenant_id, user_id)
).fetchone()
if row:
return self._row_to_user_profile(row)
return None
def get_user_analytics_summary(
self, tenant_id: str, start_date: datetime = None, end_date: datetime = None
) -> Dict:
"""获取用户分析汇总"""
with self._get_db() as conn:
query = """
SELECT
COUNT(DISTINCT user_id) as unique_users,
COUNT(*) as total_events,
COUNT(DISTINCT session_id) as total_sessions,
COUNT(DISTINCT date(timestamp)) as active_days
FROM analytics_events
WHERE tenant_id = ?
"""
params = [tenant_id]
if start_date:
query += " AND timestamp >= ?"
params.append(start_date.isoformat())
if end_date:
query += " AND timestamp <= ?"
params.append(end_date.isoformat())
row = conn.execute(query, params).fetchone()
# 获取事件类型分布
type_query = """
SELECT event_type, COUNT(*) as count
FROM analytics_events
WHERE tenant_id = ?
"""
type_params = [tenant_id]
if start_date:
type_query += " AND timestamp >= ?"
type_params.append(start_date.isoformat())
if end_date:
type_query += " AND timestamp <= ?"
type_params.append(end_date.isoformat())
type_query += " GROUP BY event_type"
type_rows = conn.execute(type_query, type_params).fetchall()
return {
"unique_users": row["unique_users"],
"total_events": row["total_events"],
"total_sessions": row["total_sessions"],
"active_days": row["active_days"],
"events_per_user": row["total_events"] / max(row["unique_users"], 1),
"events_per_session": row["total_events"] / max(row["total_sessions"], 1),
"event_type_distribution": {r["event_type"]: r["count"] for r in type_rows},
}
def create_funnel(self, tenant_id: str, name: str, description: str, steps: List[Dict], created_by: str) -> Funnel:
"""创建转化漏斗"""
funnel_id = f"fnl_{uuid.uuid4().hex[:16]}"
now = datetime.now().isoformat()
funnel = Funnel(
id=funnel_id,
tenant_id=tenant_id,
name=name,
description=description,
steps=steps,
created_at=now,
updated_at=now,
)
with self._get_db() as conn:
conn.execute(
"""
INSERT INTO funnels
(id, tenant_id, name, description, steps, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
funnel.id,
funnel.tenant_id,
funnel.name,
funnel.description,
json.dumps(funnel.steps),
funnel.created_at,
funnel.updated_at,
),
)
conn.commit()
return funnel
def analyze_funnel(
self, funnel_id: str, period_start: datetime = None, period_end: datetime = None
) -> Optional[FunnelAnalysis]:
"""分析漏斗转化率"""
with self._get_db() as conn:
funnel_row = conn.execute("SELECT * FROM funnels WHERE id = ?", (funnel_id,)).fetchone()
if not funnel_row:
return None
steps = json.loads(funnel_row["steps"])
if not period_start:
period_start = datetime.now() - timedelta(days=30)
if not period_end:
period_end = datetime.now()
# 计算每步转化
step_conversions = []
previous_count = None
for step in steps:
event_name = step.get("event_name")
query = """
SELECT COUNT(DISTINCT user_id) as user_count
FROM analytics_events
WHERE event_name = ? AND timestamp >= ? AND timestamp <= ?
"""
row = conn.execute(query, (event_name, period_start.isoformat(), period_end.isoformat())).fetchone()
user_count = row["user_count"] if row else 0
conversion_rate = 0.0
drop_off_rate = 0.0
if previous_count and previous_count > 0:
conversion_rate = user_count / previous_count
drop_off_rate = 1 - conversion_rate
step_conversions.append(
{
"step_name": step.get("name", event_name),
"event_name": event_name,
"user_count": user_count,
"conversion_rate": round(conversion_rate, 4),
"drop_off_rate": round(drop_off_rate, 4),
}
)
previous_count = user_count
# 计算总体转化率
if steps and step_conversions:
first_step_count = step_conversions[0]["user_count"]
last_step_count = step_conversions[-1]["user_count"]
overall_conversion = last_step_count / max(first_step_count, 1)
else:
overall_conversion = 0.0
# 找出主要流失点
drop_off_points = [s for s in step_conversions if s["drop_off_rate"] > 0.2 and s != step_conversions[0]]
return FunnelAnalysis(
funnel_id=funnel_id,
period_start=period_start,
period_end=period_end,
total_users=step_conversions[0]["user_count"] if step_conversions else 0,
step_conversions=step_conversions,
overall_conversion=round(overall_conversion, 4),
drop_off_points=drop_off_points,
)
def calculate_retention(self, tenant_id: str, cohort_date: datetime, periods: List[int] = None) -> Dict:
"""计算留存率"""
if periods is None:
periods = [1, 3, 7, 14, 30]
with self._get_db() as conn:
# 获取同期群用户(在 cohort_date 当天首次活跃的用户)
cohort_query = """
SELECT DISTINCT user_id
FROM analytics_events
WHERE tenant_id = ? AND date(timestamp) = date(?)
AND user_id IN (
SELECT user_id FROM user_profiles
WHERE tenant_id = ? AND date(first_seen) = date(?)
)
"""
cohort_rows = conn.execute(
cohort_query, (tenant_id, cohort_date.isoformat(), tenant_id, cohort_date.isoformat())
).fetchall()
cohort_users = {r["user_id"] for r in cohort_rows}
cohort_size = len(cohort_users)
if cohort_size == 0:
return {"cohort_date": cohort_date.isoformat(), "cohort_size": 0, "retention": {}}
retention_rates = {}
for period in periods:
period_date = cohort_date + timedelta(days=period)
active_query = """
SELECT COUNT(DISTINCT user_id) as active_count
FROM analytics_events
WHERE tenant_id = ? AND date(timestamp) = date(?)
AND user_id IN ({})
""".format(",".join(["?" for _ in cohort_users]))
params = [tenant_id, period_date.isoformat()] + list(cohort_users)
row = conn.execute(active_query, params).fetchone()
active_count = row["active_count"] if row else 0
retention_rate = active_count / cohort_size
retention_rates[f"day_{period}"] = {
"active_users": active_count,
"retention_rate": round(retention_rate, 4),
}
return {"cohort_date": cohort_date.isoformat(), "cohort_size": cohort_size, "retention": retention_rates}
# ==================== A/B 测试框架 ====================
def create_experiment(
self,
tenant_id: str,
name: str,
description: str,
hypothesis: str,
variants: List[Dict],
traffic_allocation: TrafficAllocationType,
traffic_split: Dict[str, float],
target_audience: Dict,
primary_metric: str,
secondary_metrics: List[str],
min_sample_size: int = 100,
confidence_level: float = 0.95,
created_by: str = None,
) -> Experiment:
"""创建 A/B 测试实验"""
experiment_id = f"exp_{uuid.uuid4().hex[:16]}"
now = datetime.now().isoformat()
experiment = Experiment(
id=experiment_id,
tenant_id=tenant_id,
name=name,
description=description,
hypothesis=hypothesis,
status=ExperimentStatus.DRAFT,
variants=variants,
traffic_allocation=traffic_allocation,
traffic_split=traffic_split,
target_audience=target_audience,
primary_metric=primary_metric,
secondary_metrics=secondary_metrics,
start_date=None,
end_date=None,
min_sample_size=min_sample_size,
confidence_level=confidence_level,
created_at=now,
updated_at=now,
created_by=created_by or "system",
)
with self._get_db() as conn:
conn.execute(
"""
INSERT INTO experiments
(id, tenant_id, name, description, hypothesis, status, variants,
traffic_allocation, traffic_split, target_audience, primary_metric,
secondary_metrics, start_date, end_date, min_sample_size,
confidence_level, created_at, updated_at, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
experiment.id,
experiment.tenant_id,
experiment.name,
experiment.description,
experiment.hypothesis,
experiment.status.value,
json.dumps(experiment.variants),
experiment.traffic_allocation.value,
json.dumps(experiment.traffic_split),
json.dumps(experiment.target_audience),
experiment.primary_metric,
json.dumps(experiment.secondary_metrics),
experiment.start_date,
experiment.end_date,
experiment.min_sample_size,
experiment.confidence_level,
experiment.created_at,
experiment.updated_at,
experiment.created_by,
),
)
conn.commit()
return experiment
def get_experiment(self, experiment_id: str) -> Optional[Experiment]:
"""获取实验详情"""
with self._get_db() as conn:
row = conn.execute("SELECT * FROM experiments WHERE id = ?", (experiment_id,)).fetchone()
if row:
return self._row_to_experiment(row)
return None
def list_experiments(self, tenant_id: str, status: ExperimentStatus = None) -> List[Experiment]:
"""列出实验"""
query = "SELECT * FROM experiments WHERE tenant_id = ?"
params = [tenant_id]
if status:
query += " AND status = ?"
params.append(status.value)
query += " ORDER BY created_at DESC"
with self._get_db() as conn:
rows = conn.execute(query, params).fetchall()
return [self._row_to_experiment(row) for row in rows]
def assign_variant(self, experiment_id: str, user_id: str, user_attributes: Dict = None) -> Optional[str]:
"""为用户分配实验变体"""
experiment = self.get_experiment(experiment_id)
if not experiment or experiment.status != ExperimentStatus.RUNNING:
return None
# 检查用户是否已分配
with self._get_db() as conn:
row = conn.execute(
"""SELECT variant_id FROM experiment_assignments
WHERE experiment_id = ? AND user_id = ?""",
(experiment_id, user_id),
).fetchone()
if row:
return row["variant_id"]
# 根据分配策略选择变体
if experiment.traffic_allocation == TrafficAllocationType.RANDOM:
variant_id = self._random_allocation(experiment.variants, experiment.traffic_split)
elif experiment.traffic_allocation == TrafficAllocationType.STRATIFIED:
variant_id = self._stratified_allocation(experiment.variants, experiment.traffic_split, user_attributes)
else: # TARGETED
variant_id = self._targeted_allocation(experiment.variants, experiment.target_audience, user_attributes)
if variant_id:
now = datetime.now().isoformat()
conn.execute(
"""
INSERT INTO experiment_assignments
(id, experiment_id, user_id, variant_id, user_attributes, assigned_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
f"ea_{uuid.uuid4().hex[:16]}",
experiment_id,
user_id,
variant_id,
json.dumps(user_attributes or {}),
now,
),
)
conn.commit()
return variant_id
def _random_allocation(self, variants: List[Dict], traffic_split: Dict[str, float]) -> str:
"""随机分配"""
variant_ids = [v["id"] for v in variants]
weights = [traffic_split.get(v_id, 1.0 / len(variants)) for v_id in variant_ids]
total = sum(weights)
normalized_weights = [w / total for w in weights]
return random.choices(variant_ids, weights=normalized_weights, k=1)[0]
def _stratified_allocation(
self, variants: List[Dict], traffic_split: Dict[str, float], user_attributes: Dict
) -> str:
"""分层分配(基于用户属性)"""
# 简化的分层分配:根据用户 ID 哈希值分配
if user_attributes and "user_id" in user_attributes:
hash_value = int(hashlib.md5(user_attributes["user_id"].encode()).hexdigest(), 16)
variant_ids = [v["id"] for v in variants]
index = hash_value % len(variant_ids)
return variant_ids[index]
return self._random_allocation(variants, traffic_split)
def _targeted_allocation(self, variants: List[Dict], target_audience: Dict, user_attributes: Dict) -> Optional[str]:
"""定向分配(基于目标受众条件)"""
# 检查用户是否符合目标受众条件
conditions = target_audience.get("conditions", [])
matches = True
for condition in conditions:
attr_name = condition.get("attribute")
operator = condition.get("operator")
value = condition.get("value")
user_value = user_attributes.get(attr_name) if user_attributes else None
if operator == "equals" and user_value != value:
matches = False
break
elif operator == "not_equals" and user_value == value:
matches = False
break
elif operator == "in" and user_value not in value:
matches = False
break
if not matches:
# 用户不符合条件,返回对照组
control_variant = next((v for v in variants if v.get("is_control")), variants[0])
return control_variant["id"] if control_variant else None
return self._random_allocation(variants, target_audience.get("traffic_split", {}))
def record_experiment_metric(
self, experiment_id: str, variant_id: str, user_id: str, metric_name: str, metric_value: float
):
"""记录实验指标"""
with self._get_db() as conn:
conn.execute(
"""
INSERT INTO experiment_metrics
(id, experiment_id, variant_id, user_id, metric_name, metric_value, recorded_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
f"em_{uuid.uuid4().hex[:16]}",
experiment_id,
variant_id,
user_id,
metric_name,
metric_value,
datetime.now().isoformat(),
),
)
conn.commit()
def analyze_experiment(self, experiment_id: str) -> Dict:
"""分析实验结果"""
experiment = self.get_experiment(experiment_id)
if not experiment:
return {"error": "Experiment not found"}
with self._get_db() as conn:
results = {}
for variant in experiment.variants:
variant_id = variant["id"]
# 获取样本量
sample_row = conn.execute(
"""
SELECT COUNT(DISTINCT user_id) as sample_size
FROM experiment_assignments
WHERE experiment_id = ? AND variant_id = ?
""",
(experiment_id, variant_id),
).fetchone()
sample_size = sample_row["sample_size"] if sample_row else 0
# 获取主要指标统计
metric_row = conn.execute(
"""
SELECT
AVG(metric_value) as mean_value,
COUNT(*) as metric_count,
SUM(metric_value) as total_value
FROM experiment_metrics
WHERE experiment_id = ? AND variant_id = ? AND metric_name = ?
""",
(experiment_id, variant_id, experiment.primary_metric),
).fetchone()
mean_value = metric_row["mean_value"] if metric_row and metric_row["mean_value"] else 0
results[variant_id] = {
"variant_name": variant.get("name", variant_id),
"is_control": variant.get("is_control", False),
"sample_size": sample_size,
"mean_value": round(mean_value, 4),
"metric_count": metric_row["metric_count"] if metric_row else 0,
}
# 计算统计显著性(简化版)
control_variant = next((v for v in experiment.variants if v.get("is_control")), None)
if control_variant:
control_id = control_variant["id"]
control_result = results.get(control_id, {})
for variant_id, result in results.items():
if variant_id != control_id:
control_mean = control_result.get("mean_value", 0)
variant_mean = result.get("mean_value", 0)
if control_mean > 0:
uplift = (variant_mean - control_mean) / control_mean
else:
uplift = 0
# 简化的显著性判断
is_significant = abs(uplift) > 0.05 and result["sample_size"] > 100
result["uplift"] = round(uplift, 4)
result["is_significant"] = is_significant
result["p_value"] = 0.05 if is_significant else 0.5
return {
"experiment_id": experiment_id,
"experiment_name": experiment.name,
"primary_metric": experiment.primary_metric,
"status": experiment.status.value,
"variant_results": results,
}
def start_experiment(self, experiment_id: str) -> Optional[Experiment]:
"""启动实验"""
with self._get_db() as conn:
now = datetime.now().isoformat()
conn.execute(
"""
UPDATE experiments
SET status = ?, start_date = ?, updated_at = ?
WHERE id = ? AND status = ?
""",
(ExperimentStatus.RUNNING.value, now, now, experiment_id, ExperimentStatus.DRAFT.value),
)
conn.commit()
return self.get_experiment(experiment_id)
def stop_experiment(self, experiment_id: str) -> Optional[Experiment]:
"""停止实验"""
with self._get_db() as conn:
now = datetime.now().isoformat()
conn.execute(
"""
UPDATE experiments
SET status = ?, end_date = ?, updated_at = ?
WHERE id = ? AND status = ?
""",
(ExperimentStatus.COMPLETED.value, now, now, experiment_id, ExperimentStatus.RUNNING.value),
)
conn.commit()
return self.get_experiment(experiment_id)
# ==================== 邮件营销自动化 ====================
def create_email_template(
self,
tenant_id: str,
name: str,
template_type: EmailTemplateType,
subject: str,
html_content: str,
text_content: str = None,
variables: List[str] = None,
from_name: str = None,
from_email: str = None,
reply_to: str = None,
) -> EmailTemplate:
"""创建邮件模板"""
template_id = f"et_{uuid.uuid4().hex[:16]}"
now = datetime.now().isoformat()
# 自动提取变量
if variables is None:
variables = re.findall(r"\{\{(\w+)\}\}", html_content)
template = EmailTemplate(
id=template_id,
tenant_id=tenant_id,
name=name,
template_type=template_type,
subject=subject,
html_content=html_content,
text_content=text_content or re.sub(r"<[^>]+>", "", html_content),
variables=variables,
preview_text=None,
from_name=from_name or "InsightFlow",
from_email=from_email or "noreply@insightflow.io",
reply_to=reply_to,
is_active=True,
created_at=now,
updated_at=now,
)
with self._get_db() as conn:
conn.execute(
"""
INSERT INTO email_templates
(id, tenant_id, name, template_type, subject, html_content, text_content,
variables, from_name, from_email, reply_to, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
template.id,
template.tenant_id,
template.name,
template.template_type.value,
template.subject,
template.html_content,
template.text_content,
json.dumps(template.variables),
template.from_name,
template.from_email,
template.reply_to,
template.is_active,
template.created_at,
template.updated_at,
),
)
conn.commit()
return template
def get_email_template(self, template_id: str) -> Optional[EmailTemplate]:
"""获取邮件模板"""
with self._get_db() as conn:
row = conn.execute("SELECT * FROM email_templates WHERE id = ?", (template_id,)).fetchone()
if row:
return self._row_to_email_template(row)
return None
def list_email_templates(self, tenant_id: str, template_type: EmailTemplateType = None) -> List[EmailTemplate]:
"""列出邮件模板"""
query = "SELECT * FROM email_templates WHERE tenant_id = ? AND is_active = 1"
params = [tenant_id]
if template_type:
query += " AND template_type = ?"
params.append(template_type.value)
query += " ORDER BY created_at DESC"
with self._get_db() as conn:
rows = conn.execute(query, params).fetchall()
return [self._row_to_email_template(row) for row in rows]
def render_template(self, template_id: str, variables: Dict) -> Dict[str, str]:
"""渲染邮件模板"""
template = self.get_email_template(template_id)
if not template:
return None
subject = template.subject
html_content = template.html_content
text_content = template.text_content
for key, value in variables.items():
placeholder = f"{{{{{key}}}}}"
subject = subject.replace(placeholder, str(value))
html_content = html_content.replace(placeholder, str(value))
text_content = text_content.replace(placeholder, str(value))
return {
"subject": subject,
"html": html_content,
"text": text_content,
"from_name": template.from_name,
"from_email": template.from_email,
"reply_to": template.reply_to,
}
def create_email_campaign(
self, tenant_id: str, name: str, template_id: str, recipient_list: List[Dict], scheduled_at: datetime = None
) -> EmailCampaign:
"""创建邮件营销活动"""
campaign_id = f"ec_{uuid.uuid4().hex[:16]}"
now = datetime.now().isoformat()
campaign = EmailCampaign(
id=campaign_id,
tenant_id=tenant_id,
name=name,
template_id=template_id,
status="draft",
recipient_count=len(recipient_list),
sent_count=0,
delivered_count=0,
opened_count=0,
clicked_count=0,
bounced_count=0,
failed_count=0,
scheduled_at=scheduled_at.isoformat() if scheduled_at else None,
started_at=None,
completed_at=None,
created_at=now,
)
with self._get_db() as conn:
conn.execute(
"""
INSERT INTO email_campaigns
(id, tenant_id, name, template_id, status, recipient_count,
sent_count, delivered_count, opened_count, clicked_count,
bounced_count, failed_count, scheduled_at, started_at, completed_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
campaign.id,
campaign.tenant_id,
campaign.name,
campaign.template_id,
campaign.status,
campaign.recipient_count,
campaign.sent_count,
campaign.delivered_count,
campaign.opened_count,
campaign.clicked_count,
campaign.bounced_count,
campaign.failed_count,
campaign.scheduled_at,
campaign.started_at,
campaign.completed_at,
campaign.created_at,
),
)
# 创建邮件发送记录
for recipient in recipient_list:
conn.execute(
"""
INSERT INTO email_logs
(id, campaign_id, tenant_id, user_id, email, template_id, status, subject, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
f"el_{uuid.uuid4().hex[:16]}",
campaign_id,
tenant_id,
recipient.get("user_id"),
recipient.get("email"),
template_id,
EmailStatus.SCHEDULED.value if scheduled_at else EmailStatus.DRAFT.value,
"",
now,
),
)
conn.commit()
return campaign
async def send_email(self, campaign_id: str, user_id: str, email: str, template_id: str, variables: Dict) -> bool:
"""发送单封邮件"""
template = self.get_email_template(template_id)
if not template:
return False
rendered = self.render_template(template_id, variables)
# 更新状态为发送中
with self._get_db() as conn:
now = datetime.now().isoformat()
conn.execute(
"""
UPDATE email_logs
SET status = ?, sent_at = ?, subject = ?
WHERE campaign_id = ? AND user_id = ?
""",
(EmailStatus.SENDING.value, now, rendered["subject"], campaign_id, user_id),
)
conn.commit()
try:
# 这里集成实际的邮件发送服务SendGrid, AWS SES 等)
# 目前使用模拟发送
await asyncio.sleep(0.1)
success = True # 模拟成功
# 更新状态
with self._get_db() as conn:
now = datetime.now().isoformat()
if success:
conn.execute(
"""
UPDATE email_logs
SET status = ?, delivered_at = ?
WHERE campaign_id = ? AND user_id = ?
""",
(EmailStatus.DELIVERED.value, now, campaign_id, user_id),
)
else:
conn.execute(
"""
UPDATE email_logs
SET status = ?, error_message = ?
WHERE campaign_id = ? AND user_id = ?
""",
(EmailStatus.FAILED.value, "Send failed", campaign_id, user_id),
)
conn.commit()
return success
except Exception as e:
with self._get_db() as conn:
conn.execute(
"""
UPDATE email_logs
SET status = ?, error_message = ?
WHERE campaign_id = ? AND user_id = ?
""",
(EmailStatus.FAILED.value, str(e), campaign_id, user_id),
)
conn.commit()
return False
async def send_campaign(self, campaign_id: str) -> Dict:
"""发送整个营销活动"""
with self._get_db() as conn:
campaign_row = conn.execute("SELECT * FROM email_campaigns WHERE id = ?", (campaign_id,)).fetchone()
if not campaign_row:
return {"error": "Campaign not found"}
# 获取待发送的邮件
logs = conn.execute(
"""SELECT * FROM email_logs
WHERE campaign_id = ? AND status IN (?, ?)""",
(campaign_id, EmailStatus.DRAFT.value, EmailStatus.SCHEDULED.value),
).fetchall()
# 更新活动状态
now = datetime.now().isoformat()
conn.execute(
"UPDATE email_campaigns SET status = ?, started_at = ? WHERE id = ?", ("sending", now, campaign_id)
)
conn.commit()
# 批量发送
success_count = 0
failed_count = 0
for log in logs:
# 获取用户变量
variables = self._get_user_variables(log["tenant_id"], log["user_id"])
success = await self.send_email(campaign_id, log["user_id"], log["email"], log["template_id"], variables)
if success:
success_count += 1
else:
failed_count += 1
# 更新活动状态
with self._get_db() as conn:
now = datetime.now().isoformat()
conn.execute(
"""
UPDATE email_campaigns
SET status = ?, completed_at = ?, sent_count = ?
WHERE id = ?
""",
("completed", now, success_count, campaign_id),
)
conn.commit()
return {"campaign_id": campaign_id, "total": len(logs), "success": success_count, "failed": failed_count}
def _get_user_variables(self, tenant_id: str, user_id: str) -> Dict:
"""获取用户变量用于邮件模板"""
# 这里应该从用户服务获取用户信息
# 简化实现
return {"user_id": user_id, "user_name": "User", "tenant_id": tenant_id}
def create_automation_workflow(
self,
tenant_id: str,
name: str,
description: str,
trigger_type: WorkflowTriggerType,
trigger_conditions: Dict,
actions: List[Dict],
) -> AutomationWorkflow:
"""创建自动化工作流"""
workflow_id = f"aw_{uuid.uuid4().hex[:16]}"
now = datetime.now().isoformat()
workflow = AutomationWorkflow(
id=workflow_id,
tenant_id=tenant_id,
name=name,
description=description,
trigger_type=trigger_type,
trigger_conditions=trigger_conditions,
actions=actions,
is_active=True,
execution_count=0,
created_at=now,
updated_at=now,
)
with self._get_db() as conn:
conn.execute(
"""
INSERT INTO automation_workflows
(id, tenant_id, name, description, trigger_type, trigger_conditions,
actions, is_active, execution_count, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
workflow.id,
workflow.tenant_id,
workflow.name,
workflow.description,
workflow.trigger_type.value,
json.dumps(workflow.trigger_conditions),
json.dumps(workflow.actions),
workflow.is_active,
workflow.execution_count,
workflow.created_at,
workflow.updated_at,
),
)
conn.commit()
return workflow
async def trigger_workflow(self, workflow_id: str, event_data: Dict):
"""触发自动化工作流"""
with self._get_db() as conn:
row = conn.execute(
"SELECT * FROM automation_workflows WHERE id = ? AND is_active = 1", (workflow_id,)
).fetchone()
if not row:
return False
workflow = self._row_to_automation_workflow(row)
# 检查触发条件
if not self._check_trigger_conditions(workflow.trigger_conditions, event_data):
return False
# 执行动作
for action in workflow.actions:
await self._execute_action(action, event_data)
# 更新执行计数
conn.execute(
"UPDATE automation_workflows SET execution_count = execution_count + 1 WHERE id = ?", (workflow_id,)
)
conn.commit()
return True
def _check_trigger_conditions(self, conditions: Dict, event_data: Dict) -> bool:
"""检查触发条件"""
# 简化的条件检查
for key, value in conditions.items():
if event_data.get(key) != value:
return False
return True
async def _execute_action(self, action: Dict, event_data: Dict):
"""执行工作流动作"""
action_type = action.get("type")
if action_type == "send_email":
action.get("template_id")
# 发送邮件逻辑
elif action_type == "update_user":
# 更新用户属性
pass
elif action_type == "webhook":
# 调用 webhook
pass
# ==================== 推荐系统 ====================
def create_referral_program(
self,
tenant_id: str,
name: str,
description: str,
referrer_reward_type: str,
referrer_reward_value: float,
referee_reward_type: str,
referee_reward_value: float,
max_referrals_per_user: int = 10,
referral_code_length: int = 8,
expiry_days: int = 30,
) -> ReferralProgram:
"""创建推荐计划"""
program_id = f"rp_{uuid.uuid4().hex[:16]}"
now = datetime.now().isoformat()
program = ReferralProgram(
id=program_id,
tenant_id=tenant_id,
name=name,
description=description,
referrer_reward_type=referrer_reward_type,
referrer_reward_value=referrer_reward_value,
referee_reward_type=referee_reward_type,
referee_reward_value=referee_reward_value,
max_referrals_per_user=max_referrals_per_user,
referral_code_length=referral_code_length,
expiry_days=expiry_days,
is_active=True,
created_at=now,
updated_at=now,
)
with self._get_db() as conn:
conn.execute(
"""
INSERT INTO referral_programs
(id, tenant_id, name, description, referrer_reward_type, referrer_reward_value,
referee_reward_type, referee_reward_value, max_referrals_per_user,
referral_code_length, expiry_days, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
program.id,
program.tenant_id,
program.name,
program.description,
program.referrer_reward_type,
program.referrer_reward_value,
program.referee_reward_type,
program.referee_reward_value,
program.max_referrals_per_user,
program.referral_code_length,
program.expiry_days,
program.is_active,
program.created_at,
program.updated_at,
),
)
conn.commit()
return program
def generate_referral_code(self, program_id: str, referrer_id: str) -> Referral:
"""生成推荐码"""
program = self._get_referral_program(program_id)
if not program:
return None
# 检查推荐次数限制
with self._get_db() as conn:
count_row = conn.execute(
"""SELECT COUNT(*) as count FROM referrals
WHERE program_id = ? AND referrer_id = ? AND status != ?""",
(program_id, referrer_id, ReferralStatus.EXPIRED.value),
).fetchone()
if count_row["count"] >= program.max_referrals_per_user:
return None
# 生成推荐码
referral_code = self._generate_unique_code(program.referral_code_length)
referral_id = f"ref_{uuid.uuid4().hex[:16]}"
now = datetime.now()
expires_at = now + timedelta(days=program.expiry_days)
referral = Referral(
id=referral_id,
program_id=program_id,
tenant_id=program.tenant_id,
referrer_id=referrer_id,
referee_id=None,
referral_code=referral_code,
status=ReferralStatus.PENDING,
referrer_rewarded=False,
referee_rewarded=False,
referrer_reward_value=program.referrer_reward_value,
referee_reward_value=program.referee_reward_value,
converted_at=None,
rewarded_at=None,
expires_at=expires_at,
created_at=now,
)
conn.execute(
"""
INSERT INTO referrals
(id, program_id, tenant_id, referrer_id, referee_id, referral_code,
status, referrer_rewarded, referee_rewarded, referrer_reward_value,
referee_reward_value, converted_at, rewarded_at, expires_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
referral.id,
referral.program_id,
referral.tenant_id,
referral.referrer_id,
referral.referee_id,
referral.referral_code,
referral.status.value,
referral.referrer_rewarded,
referral.referee_rewarded,
referral.referrer_reward_value,
referral.referee_reward_value,
referral.converted_at,
referral.rewarded_at,
referral.expires_at.isoformat(),
referral.created_at.isoformat(),
),
)
conn.commit()
return referral
def _generate_unique_code(self, length: int) -> str:
"""生成唯一推荐码"""
chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # 排除易混淆字符
while True:
code = "".join(random.choices(chars, k=length))
with self._get_db() as conn:
row = conn.execute("SELECT 1 FROM referrals WHERE referral_code = ?", (code,)).fetchone()
if not row:
return code
def _get_referral_program(self, program_id: str) -> Optional[ReferralProgram]:
"""获取推荐计划"""
with self._get_db() as conn:
row = conn.execute("SELECT * FROM referral_programs WHERE id = ?", (program_id,)).fetchone()
if row:
return self._row_to_referral_program(row)
return None
def apply_referral_code(self, referral_code: str, referee_id: str) -> bool:
"""应用推荐码"""
with self._get_db() as conn:
row = conn.execute(
"""SELECT * FROM referrals
WHERE referral_code = ? AND status = ? AND expires_at > ?""",
(referral_code, ReferralStatus.PENDING.value, datetime.now().isoformat()),
).fetchone()
if not row:
return False
now = datetime.now().isoformat()
conn.execute(
"""
UPDATE referrals
SET referee_id = ?, status = ?, converted_at = ?
WHERE id = ?
""",
(referee_id, ReferralStatus.CONVERTED.value, now, row["id"]),
)
conn.commit()
return True
def reward_referral(self, referral_id: str) -> bool:
"""发放推荐奖励"""
with self._get_db() as conn:
row = conn.execute("SELECT * FROM referrals WHERE id = ?", (referral_id,)).fetchone()
if not row or row["status"] != ReferralStatus.CONVERTED.value:
return False
now = datetime.now().isoformat()
conn.execute(
"""
UPDATE referrals
SET status = ?, referrer_rewarded = 1, referee_rewarded = 1, rewarded_at = ?
WHERE id = ?
""",
(ReferralStatus.REWARDED.value, now, referral_id),
)
conn.commit()
return True
def get_referral_stats(self, program_id: str) -> Dict:
"""获取推荐统计"""
with self._get_db() as conn:
stats = conn.execute(
"""
SELECT
COUNT(*) as total_referrals,
SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as converted,
SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as rewarded,
SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as expired,
COUNT(DISTINCT referrer_id) as unique_referrers
FROM referrals
WHERE program_id = ?
""",
(
ReferralStatus.PENDING.value,
ReferralStatus.CONVERTED.value,
ReferralStatus.REWARDED.value,
ReferralStatus.EXPIRED.value,
program_id,
),
).fetchone()
return {
"program_id": program_id,
"total_referrals": stats["total_referrals"] or 0,
"pending": stats["pending"] or 0,
"converted": stats["converted"] or 0,
"rewarded": stats["rewarded"] or 0,
"expired": stats["expired"] or 0,
"unique_referrers": stats["unique_referrers"] or 0,
"conversion_rate": round((stats["converted"] or 0) / max(stats["total_referrals"] or 1, 1), 4),
}
def create_team_incentive(
self,
tenant_id: str,
name: str,
description: str,
target_tier: str,
min_team_size: int,
incentive_type: str,
incentive_value: float,
valid_from: datetime,
valid_until: datetime,
) -> TeamIncentive:
"""创建团队升级激励"""
incentive_id = f"ti_{uuid.uuid4().hex[:16]}"
now = datetime.now().isoformat()
incentive = TeamIncentive(
id=incentive_id,
tenant_id=tenant_id,
name=name,
description=description,
target_tier=target_tier,
min_team_size=min_team_size,
incentive_type=incentive_type,
incentive_value=incentive_value,
valid_from=valid_from.isoformat(),
valid_until=valid_until.isoformat(),
is_active=True,
created_at=now,
)
with self._get_db() as conn:
conn.execute(
"""
INSERT INTO team_incentives
(id, tenant_id, name, description, target_tier, min_team_size,
incentive_type, incentive_value, valid_from, valid_until, is_active, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
incentive.id,
incentive.tenant_id,
incentive.name,
incentive.description,
incentive.target_tier,
incentive.min_team_size,
incentive.incentive_type,
incentive.incentive_value,
incentive.valid_from,
incentive.valid_until,
incentive.is_active,
incentive.created_at,
),
)
conn.commit()
return incentive
def check_team_incentive_eligibility(
self, tenant_id: str, current_tier: str, team_size: int
) -> List[TeamIncentive]:
"""检查团队激励资格"""
with self._get_db() as conn:
now = datetime.now().isoformat()
rows = conn.execute(
"""
SELECT * FROM team_incentives
WHERE tenant_id = ? AND is_active = 1
AND target_tier = ? AND min_team_size <= ?
AND valid_from <= ? AND valid_until >= ?
""",
(tenant_id, current_tier, team_size, now, now),
).fetchall()
return [self._row_to_team_incentive(row) for row in rows]
# ==================== 实时分析仪表板 ====================
def get_realtime_dashboard(self, tenant_id: str) -> Dict:
"""获取实时分析仪表板数据"""
now = datetime.now()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
with self._get_db() as conn:
# 今日统计
today_stats = conn.execute(
"""
SELECT
COUNT(DISTINCT user_id) as active_users,
COUNT(*) as total_events,
COUNT(DISTINCT session_id) as sessions
FROM analytics_events
WHERE tenant_id = ? AND timestamp >= ?
""",
(tenant_id, today_start.isoformat()),
).fetchone()
# 最近事件
recent_events = conn.execute(
"""
SELECT event_name, event_type, timestamp, user_id
FROM analytics_events
WHERE tenant_id = ?
ORDER BY timestamp DESC
LIMIT 20
""",
(tenant_id,),
).fetchall()
# 热门功能
top_features = conn.execute(
"""
SELECT event_name, COUNT(*) as count
FROM analytics_events
WHERE tenant_id = ? AND timestamp >= ? AND event_type = ?
GROUP BY event_name
ORDER BY count DESC
LIMIT 10
""",
(tenant_id, today_start.isoformat(), EventType.FEATURE_USE.value),
).fetchall()
# 活跃用户趋势最近24小时每小时
hourly_trend = []
for i in range(24):
hour_start = now - timedelta(hours=i + 1)
hour_end = now - timedelta(hours=i)
row = conn.execute(
"""
SELECT COUNT(DISTINCT user_id) as count
FROM analytics_events
WHERE tenant_id = ? AND timestamp >= ? AND timestamp < ?
""",
(tenant_id, hour_start.isoformat(), hour_end.isoformat()),
).fetchone()
hourly_trend.append({"hour": hour_end.strftime("%H:00"), "active_users": row["count"] or 0})
return {
"tenant_id": tenant_id,
"timestamp": now.isoformat(),
"today": {
"active_users": today_stats["active_users"] or 0,
"total_events": today_stats["total_events"] or 0,
"sessions": today_stats["sessions"] or 0,
},
"recent_events": [
{
"event_name": r["event_name"],
"event_type": r["event_type"],
"timestamp": r["timestamp"],
"user_id": r["user_id"][:8] + "...", # 脱敏
}
for r in recent_events
],
"top_features": [{"feature": r["event_name"], "usage_count": r["count"]} for r in top_features],
"hourly_trend": list(reversed(hourly_trend)),
}
# ==================== 辅助方法 ====================
def _row_to_user_profile(self, row) -> UserProfile:
"""将数据库行转换为 UserProfile"""
return UserProfile(
id=row["id"],
tenant_id=row["tenant_id"],
user_id=row["user_id"],
first_seen=datetime.fromisoformat(row["first_seen"]),
last_seen=datetime.fromisoformat(row["last_seen"]),
total_sessions=row["total_sessions"],
total_events=row["total_events"],
feature_usage=json.loads(row["feature_usage"]),
subscription_history=json.loads(row["subscription_history"]),
ltv=row["ltv"],
churn_risk_score=row["churn_risk_score"],
engagement_score=row["engagement_score"],
created_at=datetime.fromisoformat(row["created_at"]),
updated_at=datetime.fromisoformat(row["updated_at"]),
)
def _row_to_experiment(self, row) -> Experiment:
"""将数据库行转换为 Experiment"""
return Experiment(
id=row["id"],
tenant_id=row["tenant_id"],
name=row["name"],
description=row["description"],
hypothesis=row["hypothesis"],
status=ExperimentStatus(row["status"]),
variants=json.loads(row["variants"]),
traffic_allocation=TrafficAllocationType(row["traffic_allocation"]),
traffic_split=json.loads(row["traffic_split"]),
target_audience=json.loads(row["target_audience"]),
primary_metric=row["primary_metric"],
secondary_metrics=json.loads(row["secondary_metrics"]),
start_date=datetime.fromisoformat(row["start_date"]) if row["start_date"] else None,
end_date=datetime.fromisoformat(row["end_date"]) if row["end_date"] else None,
min_sample_size=row["min_sample_size"],
confidence_level=row["confidence_level"],
created_at=row["created_at"],
updated_at=row["updated_at"],
created_by=row["created_by"],
)
def _row_to_email_template(self, row) -> EmailTemplate:
"""将数据库行转换为 EmailTemplate"""
return EmailTemplate(
id=row["id"],
tenant_id=row["tenant_id"],
name=row["name"],
template_type=EmailTemplateType(row["template_type"]),
subject=row["subject"],
html_content=row["html_content"],
text_content=row["text_content"],
variables=json.loads(row["variables"]),
preview_text=row["preview_text"],
from_name=row["from_name"],
from_email=row["from_email"],
reply_to=row["reply_to"],
is_active=bool(row["is_active"]),
created_at=row["created_at"],
updated_at=row["updated_at"],
)
def _row_to_automation_workflow(self, row) -> AutomationWorkflow:
"""将数据库行转换为 AutomationWorkflow"""
return AutomationWorkflow(
id=row["id"],
tenant_id=row["tenant_id"],
name=row["name"],
description=row["description"],
trigger_type=WorkflowTriggerType(row["trigger_type"]),
trigger_conditions=json.loads(row["trigger_conditions"]),
actions=json.loads(row["actions"]),
is_active=bool(row["is_active"]),
execution_count=row["execution_count"],
created_at=row["created_at"],
updated_at=row["updated_at"],
)
def _row_to_referral_program(self, row) -> ReferralProgram:
"""将数据库行转换为 ReferralProgram"""
return ReferralProgram(
id=row["id"],
tenant_id=row["tenant_id"],
name=row["name"],
description=row["description"],
referrer_reward_type=row["referrer_reward_type"],
referrer_reward_value=row["referrer_reward_value"],
referee_reward_type=row["referee_reward_type"],
referee_reward_value=row["referee_reward_value"],
max_referrals_per_user=row["max_referrals_per_user"],
referral_code_length=row["referral_code_length"],
expiry_days=row["expiry_days"],
is_active=bool(row["is_active"]),
created_at=row["created_at"],
updated_at=row["updated_at"],
)
def _row_to_team_incentive(self, row) -> TeamIncentive:
"""将数据库行转换为 TeamIncentive"""
return TeamIncentive(
id=row["id"],
tenant_id=row["tenant_id"],
name=row["name"],
description=row["description"],
target_tier=row["target_tier"],
min_team_size=row["min_team_size"],
incentive_type=row["incentive_type"],
incentive_value=row["incentive_value"],
valid_from=datetime.fromisoformat(row["valid_from"]),
valid_until=datetime.fromisoformat(row["valid_until"]),
is_active=bool(row["is_active"]),
created_at=row["created_at"],
)
# Singleton instance
_growth_manager = None
def get_growth_manager() -> GrowthManager:
global _growth_manager
if _growth_manager is None:
_growth_manager = GrowthManager()
return _growth_manager