你的公众号内容,哪个渠道带来了最多付费用户?——OPC自动化数据分析系统搭建
开篇:感觉,是创业者最大的敌人
你每天都在发文章、接用户、做产品,忙得脚不沾地。
打开后台看了一眼:粉丝在涨,阅读有量,偶尔还有人付费。感觉还不错?等一下——哪篇文章带来的用户转化率最高?哪个渠道获客成本最低?用户从看到付费,到底流失在哪一步?
如果你说不出具体数字,那你就是在凭感觉经营。
这种感觉,在只有10个用户的时候没问题。但到了100个、1000个用户的时候,感觉会骗你。你以为某篇文章带来很多用户,实际上可能只有目录页的一半转化。你以为某个渠道效果很好,实际付费转化率远不如另一个你压根没关注的流量入口。
在上一篇文章([告别手动对接——OPC的自动化产品交付系统](../articles/article-18.md))中,我们完成了「付款→开通→通知」全自动,让交付变成了睡后收入。但自动化之后的下一步是:让系统告诉你做什么。
今天,我们搭建一套完整的数据分析引擎——从事件采集到存储,从漏斗分析到自动日报推送,4层架构全部可运行。你只要跟着搭完,每天早上打开企业微信就能看到昨日关键数据。
一、整体架构:4层数据驱动引擎
┌─────────────────────────────────────────────────┐
│ Layer 1: 数据采集 │
│ → 页面浏览事件 | 内容互动事件 │
│ → 转化事件 | 渠道来源 (UTM/Referrer) │
├─────────────────────────────────────────────────┤
│ Layer 2: 数据存储 │
│ → PostgreSQL events表 + daily_metrics物化视图 │
├─────────────────────────────────────────────────┤
│ Layer 3: 分析引擎 │
│ → 漏斗分析 | 内容分析 | 渠道归因 | 趋势 │
├─────────────────────────────────────────────────┤
│ Layer 4: 输出层 │
│ → 自动日报 (cron → 企业微信) | 简易看板 | 告警 │
└─────────────────────────────────────────────────┘
每层职责单一、数据单向流动,OPC一个人就能维护。下图是完整架构,建议保存对照看下文:

下面我逐层拆开讲。
二、Layer 1:数据采集——埋点要轻,字段要精
很多人的数据系统从一开始就错了——埋了太多无关事件,结果数据一多就炸了,啥也分析不出来。
OPC 的数据采集原则:只埋你真正关心的转化事件。
一个典型的内容付费转化漏斗只需要5个事件:
| 事件名称 | 触发时机 | 核心字段 |
|---|---|---|
page_view | 用户打开页面 | 文章ID、来源渠道、客户端 |
read_progress | 阅读超过50% | 文章ID、阅读时长 |
signup | 用户注册/填写表单 | 来源渠道、目标页面 |
trial_start | 开始试用产品 | 产品ID、来源 |
payment_done | 完成支付 | 订单ID、金额、产品 |
实现方式很简单——一个统一的 /api/track 端点,PostgreSQL 直接写入:
# analytics/tracker.py — 事件追踪核心
import os
from datetime import datetime
import psycopg2
from psycopg2.extras import Json
DB_DSN = "postgresql://user:pass@localhost:5432/analytics"
# 预定义事件类型和业务含义
EVENT_TYPES = {
"page_view": ["article_id", "source", "user_agent", "ip_hash"],
"read_progress": ["article_id", "duration_sec", "progress_pct"],
"signup": ["source", "target_page", "email_hash"],
"trial_start": ["product_id", "source", "plan_type"],
"payment_done": ["order_id", "amount_cents", "product_id", "coupon"],
}
def track_event(user_id: str, event_type: str, payload: dict) -> dict:
"""轻量事件追踪——同步写入,毫秒级完成"""
if event_type not in EVENT_TYPES:
return {"error": f"unknown event: {event_type}"}
conn = psycopg2.connect(DB_DSN)
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO events (event_id, user_id, event_type, payload, created_at)
VALUES (%s, %s, %s, %s, %s)
""", (
os.urandom(16).hex(),
user_id,
event_type,
Json({k: payload.get(k) for k in EVENT_TYPES[event_type]}),
datetime.utcnow()
))
conn.commit()
return {"ok": True}
except Exception as e:
conn.rollback()
return {"error": str(e)}
finally:
conn.close()
注意:同步写入是故意的。OPC 的流量级别不需要消息队列,直接写数据库比引入 Kafka 简单10倍,而且不出问题。等你日均10万事件以上再考虑异步,在那之前——简单就是生产力。
前端埋点更简单,一个 fetch 完事:
// analytics/tracker.js — 前端埋点代码
function track(type, extra = {}) {
const payload = {
type: type,
article_id: document.querySelector('[data-article-id]')?.dataset.articleId,
source: new URLSearchParams(location.search).get('utm_source') || 'direct',
user_agent: navigator.userAgent,
timestamp: Date.now()
};
// 合并额外参数
Object.assign(payload, extra);
fetch('/api/track', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
keepalive: true // 页面关闭时也能发出
});
}
三、Layer 2:数据存储——表设计决定了你能问什么问题
数据库是分析引擎的基石。表设计错了,你永远跑不出想要的报表。
-- analytics/schema.sql — 核心表结构
CREATE TABLE IF NOT EXISTS events (
event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id TEXT NOT NULL,
event_type TEXT NOT NULL, -- page_view | signup | payment_done
payload JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 这是最重要的索引——90%的查询按事件类型+时间范围做漏斗分析
CREATE INDEX IF NOT EXISTS idx_events_type_time
ON events (event_type, created_at DESC);
-- 按用户查历史行为的索引
CREATE INDEX IF NOT EXISTS idx_events_user
ON events (user_id, created_at DESC);
有了这张表,所有分析都基于同源数据,永远不会出现「阅读页说5000,付费页说300」的数据打架问题。
物化视图——对 OPC 来说比实时查询重要10倍。 因为你的数据不需要秒级更新,每天算一次就够。物化视图把复杂计算固化下来,查询瞬间返回:
-- analytics/mviews.sql — 每日漏斗物化视图
CREATE MATERIALIZED VIEW IF NOT EXISTS mv_daily_funnel AS
WITH funnel AS (
SELECT
date_trunc('day', created_at)::date AS day,
event_type,
COUNT(DISTINCT user_id) AS users
FROM events
WHERE created_at >= NOW() - INTERVAL '90 days'
GROUP BY day, event_type
)
SELECT * FROM funnel
ORDER BY day DESC, event_type;
-- 每天早上6点自动刷新(cron任务)
-- 0 6 * * * psql $DB_DSN -c "REFRESH MATERIALIZED VIEW mv_daily_funnel;"
设计决策:为什么用物化视图而不是直接查?
假设你有5万条事件数据,直接查漏斗需要扫描全表聚合,耗时3-5秒。物化视图把结果缓存成50行,查询5毫秒返回。而且数据每天只变化一次,物化视图的「延迟」完全不是问题。
四、Layer 3:分析引擎——从数据到洞察有3步
有了数据,怎么变成你早上5秒就能看懂的洞察?我搭了3个核心分析模块:
4.1 漏斗分析——用户到底流失在哪
# analytics/funnel.py — 漏斗分析引擎
FUNNEL_STEPS = ["page_view", "read_progress", "signup", "trial_start", "payment_done"]
def get_funnel(days: int = 30) -> list[dict]:
"""获取完整转化漏斗"""
conn = psycopg2.connect(DB_DSN)
result = []
try:
with conn.cursor() as cur:
for i, step in enumerate(FUNNEL_STEPS):
cur.execute("""
SELECT COUNT(DISTINCT user_id)
FROM events
WHERE event_type = %s
AND created_at >= NOW() - INTERVAL '%s days'
""", (step, str(days)))
count = cur.fetchone()[0]
prev_count = result[-1]["users"] if result else count
conversion = round(count / prev_count * 100, 1) if prev_count > 0 else 0
result.append({
"step": step,
"order": i + 1,
"step_label": {
"page_view": "浏览文章",
"read_progress": "深度阅读",
"signup": "注册/填表",
"trial_start": "开始试用",
"payment_done": "完成付费",
}.get(step, step),
"users": count,
"from_prev_pct": conversion,
})
return result
finally:
conn.close()
# 输出示例(非杜撰数据,仅展示格式):
# [
# {"step": "page_view", "users": 8420, "from_prev_pct": 100.0},
# {"step": "read_progress", "users": 3150, "from_prev_pct": 37.4},
# {"step": "signup", "users": 420, "from_prev_pct": 13.3},
# {"step": "trial_start", "users": 180, "from_prev_pct": 42.9},
# {"step": "payment_done", "users": 45, "from_prev_pct": 25.0},
# ]
4.2 内容表现分析——哪些文章在「带货」
# analytics/content_performance.py — 内容分析
def get_content_performance(days: int = 30) -> list[dict]:
conn = psycopg2.connect(DB_DSN)
try:
with conn.cursor() as cur:
cur.execute("""
SELECT
payload->>'article_id' AS article_id,
COUNT(DISTINCT CASE WHEN event_type = 'page_view' THEN user_id END) AS views,
COUNT(DISTINCT CASE WHEN event_type = 'read_progress' THEN user_id END) AS deep_reads,
COUNT(DISTINCT CASE WHEN event_type = 'payment_done' THEN user_id END) AS conversions
FROM events
WHERE created_at >= NOW() - INTERVAL '%s days'
AND event_type IN ('page_view', 'read_progress', 'payment_done')
GROUP BY payload->>'article_id'
ORDER BY views DESC
""", (str(days),))
rows = cur.fetchall()
result = []
for row in rows:
views = row[1] or 0
result.append({
"article_id": row[0],
"views": views,
"deep_reads": row[2] or 0,
"deep_read_rate": round((row[2] or 0) / views * 100, 1) if views > 0 else 0,
"conversions": row[3] or 0,
"conversion_rate": round((row[3] or 0) / views * 100, 2) if views > 0 else 0,
})
# 标记高转化文章
avg_rate = sum(r["conversion_rate"] for r in result) / len(result) if result else 0
for r in result:
r["is_high_performer"] = r["conversion_rate"] > avg_rate * 1.5
return result
finally:
conn.close()
关键洞察:平均转化率只能告诉你大概水平,而 is_high_performer 标记能直接告诉你——哪篇文章的读者更愿意付费。这就是「内容即渠道」的核心数据支撑。
4.3 渠道归因——来自哪里的用户付了最多钱
# analytics/channel_attribution.py — 渠道归因
def get_channel_breakdown(days: int = 30) -> list[dict]:
conn = psycopg2.connect(DB_DSN)
try:
with conn.cursor() as cur:
cur.execute("""
SELECT
COALESCE(payload->>'source', 'direct') AS channel,
COUNT(DISTINCT e.user_id) AS visitors,
COUNT(DISTINCT p.user_id) AS payers
FROM events e
LEFT JOIN events p
ON e.user_id = p.user_id AND p.event_type = 'payment_done'
WHERE e.event_type = 'page_view'
AND e.created_at >= NOW() - INTERVAL '%s days'
GROUP BY channel
ORDER BY payers DESC
""", (str(days),))
result = []
for row in cur.fetchall():
visitors = row[1] or 0
payers = row[2] or 0
result.append({
"channel": row[0],
"visitors": visitors,
"payers": payers,
"conversion_rate": round(payers / visitors * 100, 2) if visitors > 0 else 0,
})
return result
finally:
conn.close()
这个查询最核心的价值是:它直接把「来源渠道」和「最终付费」关联起来。不是看哪个渠道带来了最多流量,而是看哪个渠道带来了最多付费用户。两者可能完全不同。
五、Layer 4:输出层——让数据主动找你
分析引擎搭好了,但你不可能每天早上手动跑一遍 SQL。数据必须主动来找你。
5.1 自动日报——每天早上发到企业微信
# analytics/reporter.py — 日报自动生成
import requests
WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"
def build_daily_report() -> str:
"""组装日报内容(纯文本,无需排版系统)"""
funnel = get_funnel(days=1) # 昨日数据
top_content = get_content_performance(days=1)[:3] # 昨日TOP3文章
lines = [
f" 昨日数据简报 ({__import__('datetime').date.today() - __import__('datetime').timedelta(days=1)})",
"",
"【用户转化漏斗】",
]
for s in funnel:
lines.append(f" {s['step_label']}: {s['users']}人 → 上一步{s['from_prev_pct']}%")
if funnel:
overall = round(funnel[-1]["users"] / funnel[0]["users"] * 100, 2) if funnel[0]["users"] > 0 else 0
lines.append(f" 整体转化率: {overall}%")
lines.extend(["", "【内容表现TOP3】", "排行 | 文章ID | 阅读 | 深度阅读率 | 转化率"])
for i, c in enumerate(top_content):
lines.append(f" #{i+1} | {c['article_id']} | {c['views']}次 | {c['deep_read_rate']}% | {c['conversion_rate']}%")
return "\n".join(lines)
def send_report():
report = build_daily_report()
payload = {"msgtype": "text", "text": {"content": report}}
requests.post(WEBHOOK_URL, json=payload)
preview = report[:80].replace("\n", " ")
print(f"日报已发送: {preview}")
if __name__ == "__main__":
send_report()
配合 cron 定时任务:
# crontab -e 添加以下行
# 每天早上8:00发送日报
0 8 * * * cd /home/opc/analytics && python3 reporter.py >> /var/log/daily_report.log 2>&1
5.2 简易看板——一张页面看完整数据
不需要 Grafana,一个 FastAPI 页面就够了:
# analytics/dashboard.py — 简易看板
import fastapi
from fastapi.responses import HTMLResponse as RespHTML
app = fastapi.FastAPI()
@app.get("/dashboard", response_class=RespHTML)
async def dashboard():
funnel = get_funnel(days=30)
content = get_content_performance(days=30)
channels = get_channel_breakdown(days=30)
html = f"""<!DOCTYPE html>
<html><head><meta charset="utf-8">
<title>OPC 数据看板</title>
<style>
body {{ font-family: system-ui; max-width: 900px; margin: 40px auto; padding: 0 20px; }}
h1 {{ color: #1e293b; }}
table {{ width: 100%; border-collapse: collapse; margin: 20px 0; }}
th, td {{ padding: 8px 12px; text-align: center; border-bottom: 1px solid #e2e8f0; }}
th {{ background: #f1f5f9; color: #475569; font-size: 13px; font-weight: 600; }}
.good {{ color: #059669; }}
.warn {{ color: #d97706; }}
.bad {{ color: #dc2626; }}
.section {{ margin: 40px 0; }}
.bar-container {{ display: flex; align-items: center; gap: 8px; }}
.bar {{ height: 20px; background: #3b82f6; border-radius: 4px; }}
</style></head><body>
<h1> OPC 数据看板</h1>
<p>最近30天 | 更新于 {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
<div class="section"><h2>用户转化漏斗</h2><table><tr><th>步骤</th><th>用户数</th><th>上一步转化率</th><th>整体转化率</th></tr>
"""
for i, s in enumerate(funnel):
overall_pct = round(s['users'] / funnel[0]['users'] * 100, 1) if funnel[0]['users'] > 0 else 0
html += f"<tr><td>{s['step_label']}</td><td>{s['users']}</td><td>{s['from_prev_pct']}%</td><td>{overall_pct}%</td></tr>"
html += """</table></div>
<div class="section"><h2>内容表现排行</h2><table><tr><th>#</th><th>文章</th><th>阅读</th><th>深度阅读率</th><th>付费转化率</th><th>标签</th></tr>"""
for i, c in enumerate(content[:10]):
tag = "⭐ 高转化" if c['is_high_performer'] else ""
html += f"<tr><td>{i+1}</td><td>{c['article_id'][:20]}</td><td>{c['views']}</td><td>{c['deep_read_rate']}%</td><td>{c['conversion_rate']}%</td><td>{tag}</td></tr>"
html += """</table></div>
<div class="section"><h2>渠道归因</h2><table><tr><th>渠道</th><th>访客</th><th>付费用户</th><th>转化率</th></tr>"""
for ch in channels:
icon = "✅" if ch['conversion_rate'] > 1 else "⚠️"
html += f"<tr><td>{ch['channel']}</td><td>{ch['visitors']}</td><td>{ch['payers']}</td><td>{icon} {ch['conversion_rate']}%</td></tr>"
html += "</table></div></body></html>"
return html
启动看板:
uvicorn analytics.dashboard:app --host 0.0.0.0 --port 8001
打开 http://localhost:8001/dashboard 就能看到完整的运营数据看板。
六、进阶思考:数据系统的「物理化落地」
这三件事是我搭建数据系统后最重要的经验:
1. 「看到」比「好看」重要100倍
不要追求漂亮的可视化。Grafana 图表虽好,但配置一次就要1小时,而且 OPC 真的没时间天天看炫酷的大屏。
日报是企业微信文本消息,5秒读完。看板是纯HTML的一张页面,打开就是数据。简单到不需要任何训练,这就是能落地的数据系统。
2. 数据一致性是分析的命脉
事件名称一定要规范统一。page_view 不能有时候叫 pageVisit、有时候叫 page_view_event。
我在代码里用了 EVENT_TYPES 字典做白名单——定义之外的事件类型一律拒绝写入。这比任何规范和文档都管用,因为它是物理强制的。
3. 先做物化视图,再做实时查询
OPC 的数据量级(几千到几万用户)根本不需要实时查询。每天一次 REFRESH MATERIALIZED VIEW 就够了。物化视图把查询时间从秒级降到毫秒级,而且你看日报的时候不需要等数据加载。
如果日报生成超过3秒,你的 OPC 数据系统就有问题。
七、总结与下一步
今天我们搭建了一套完整的自动化数据分析引擎:
- Layer 1(数据采集):
/api/track端点 + 前端埋点代码,轻量无侵入 - Layer 2(数据存储):PostgreSQL 事件表 + 物化视图,查询从秒级降到毫秒级
- Layer 3(分析引擎):漏斗分析 + 内容表现 + 渠道归因,三条核心 SQL 覆盖90%的分析场景
- Layer 4(输出层):企业微信日报 + 简易看板,数据主动找你不是你找数据
你现在拥有了「获客→转化→交付→分析」的完整闭环。前4篇你搭好了内容和产品系统,这篇你装上了眼睛——数据会告诉你下一篇文章写什么、哪个渠道该投入、哪个环节需要优化。
但这只是开始。知道了问题,怎么自动优化?
下一篇预告:一个人也能做A/B测试——AI驱动的自动化优化系统
有了数据,你知道某篇文章转化率低,但不知道怎么改?某个渠道效果差,要不要换?下一篇,我们搭建一套自动化 A/B 测试系统——让 AI 帮你设计实验、自动分流、自动收敛,并自动执行最优方案。OPC 一人也能跑出科学增长节奏。
关于作者:魏无记,AI 和数智化实践者。专注 Agent 工程化与 Loop Engineering 研究以及数智化转型。公众号持续更新 Agent 工程化实战系列和数智化转型相关知识实践——每篇都是保姆级教程照做就行。

浙公网安备 33010602011771号