2160 lines
73 KiB
Python
2160 lines
73 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
InsightFlow Growth Manager - Phase 8 Task 5
|
||
运营与增长工具模块
|
||
- 用户行为分析(Mixpanel/Amplitude 集成)
|
||
- A/B 测试框架
|
||
- 邮件营销自动化
|
||
- 推荐系统(邀请返利、团队升级激励)
|
||
|
||
作者: InsightFlow Team
|
||
"""
|
||
|
||
import asyncio
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import random
|
||
import re
|
||
import sqlite3
|
||
import uuid
|
||
from dataclasses import dataclass
|
||
from datetime import datetime, timedelta
|
||
from enum import StrEnum
|
||
from typing import Any
|
||
|
||
import httpx
|
||
|
||
# Database path
|
||
DB_PATH = os.path.join(os.path.dirname(__file__), "insightflow.db")
|
||
|
||
|
||
class EventType(StrEnum):
|
||
"""事件类型"""
|
||
|
||
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(StrEnum):
|
||
"""实验状态"""
|
||
|
||
DRAFT = "draft" # 草稿
|
||
RUNNING = "running" # 运行中
|
||
PAUSED = "paused" # 暂停
|
||
COMPLETED = "completed" # 已完成
|
||
ARCHIVED = "archived" # 已归档
|
||
|
||
|
||
class TrafficAllocationType(StrEnum):
|
||
"""流量分配类型"""
|
||
|
||
RANDOM = "random" # 随机分配
|
||
STRATIFIED = "stratified" # 分层分配
|
||
TARGETED = "targeted" # 定向分配
|
||
|
||
|
||
class EmailTemplateType(StrEnum):
|
||
"""邮件模板类型"""
|
||
|
||
WELCOME = "welcome" # 欢迎邮件
|
||
ONBOARDING = "onboarding" # 引导邮件
|
||
FEATURE_ANNOUNCEMENT = "feature_announcement" # 功能公告
|
||
CHURN_RECOVERY = "churn_recovery" # 流失挽回
|
||
UPGRADE_PROMPT = "upgrade_prompt" # 升级提示
|
||
REFERRAL = "referral" # 推荐邀请
|
||
NEWSLETTER = "newsletter" # 新闻通讯
|
||
|
||
|
||
class EmailStatus(StrEnum):
|
||
"""邮件状态"""
|
||
|
||
DRAFT = "draft" # 草稿
|
||
SCHEDULED = "scheduled" # 已计划
|
||
SENDING = "sending" # 发送中
|
||
SENT = "sent" # 已发送
|
||
DELIVERED = "delivered" # 已送达
|
||
OPENED = "opened" # 已打开
|
||
CLICKED = "clicked" # 已点击
|
||
BOUNCED = "bounced" # 退信
|
||
FAILED = "failed" # 失败
|
||
|
||
|
||
class WorkflowTriggerType(StrEnum):
|
||
"""工作流触发类型"""
|
||
|
||
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(StrEnum):
|
||
"""推荐状态"""
|
||
|
||
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: str | None
|
||
device_info: dict[str, str] # 设备信息
|
||
referrer: str | None
|
||
utm_source: str | None
|
||
utm_medium: str | None
|
||
utm_campaign: str | None
|
||
|
||
|
||
@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: datetime | None
|
||
end_date: datetime | None
|
||
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: str | None
|
||
from_name: str
|
||
from_email: str
|
||
reply_to: str | None
|
||
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: datetime | None
|
||
started_at: datetime | None
|
||
completed_at: datetime | None
|
||
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: datetime | None
|
||
delivered_at: datetime | None
|
||
opened_at: datetime | None
|
||
clicked_at: datetime | None
|
||
ip_address: str | None
|
||
user_agent: str | None
|
||
error_message: str | None
|
||
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: str | None # 被推荐人
|
||
referral_code: str
|
||
status: ReferralStatus
|
||
referrer_rewarded: bool
|
||
referee_rewarded: bool
|
||
referrer_reward_value: float
|
||
referee_reward_value: float
|
||
converted_at: datetime | None
|
||
rewarded_at: datetime | None
|
||
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) -> None:
|
||
"""获取数据库连接"""
|
||
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) -> UserProfile | None:
|
||
"""获取用户画像"""
|
||
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
|
||
) -> FunnelAnalysis | None:
|
||
"""分析漏斗转化率"""
|
||
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) -> Experiment | None:
|
||
"""获取实验详情"""
|
||
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
|
||
) -> str | None:
|
||
"""为用户分配实验变体"""
|
||
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
|
||
) -> str | None:
|
||
"""定向分配(基于目标受众条件)"""
|
||
# 检查用户是否符合目标受众条件
|
||
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) -> Experiment | None:
|
||
"""启动实验"""
|
||
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) -> Experiment | None:
|
||
"""停止实验"""
|
||
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) -> EmailTemplate | None:
|
||
"""获取邮件模板"""
|
||
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) -> ReferralProgram | None:
|
||
"""获取推荐计划"""
|
||
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
|