埋点分析系统 - 开发记忆文档
本文档记录埋点分析系统的完整架构、已实现功能、关键文件、已知问题和开发规范,供后续优化升级时参考,避免破坏已完成的功能。
一、系统架构概览
埋点分析系统/
├── backend/ # Flask 后端
│ ├── app.py # 主应用入口,蓝图注册 + 全局鉴权拦截
│ ├── config.py # 多环境配置(生产/测试)
│ ├── database.py # ClickHouse 连接池 + 线程锁
│ ├── utils.py # 工具函数(日期、项目过滤、响应格式)
│ └── api/ # API 蓝图模块
│ ├── auth.py # 登录认证(Token 管理)
│ ├── overview.py # 概览看板
│ ├── events.py # 事件分析
│ ├── elements.py # 元素内容分析
│ ├── flow.py # 流量分析(环比/同比)
│ ├── users.py # 用户分析
│ ├── screens.py # 页面路径分析
│ └── detail.py # 数据明细
├── frontend/ # React + TypeScript + Vite 前端
│ ├── src/
│ │ ├── App.tsx # 主应用,路由 + 状态管理
│ │ ├── api/index.ts # Axios 实例 + API 封装
│ │ ├── contexts/AuthContext.tsx # 认证上下文
│ │ ├── components/ # 通用组件
│ │ │ ├── Sidebar.tsx # 侧边栏(环境切换 + 项目选择 + 菜单)
│ │ │ ├── Header.tsx # 顶栏(日期选择 + 用户菜单)
│ │ │ ├── ProjectSelector.tsx # 项目下拉多选
│ │ │ ├── ProtectedRoute.tsx # 路由守卫
│ │ │ ├── Card.tsx # 卡片容器
│ │ │ ├── KpiCard.tsx # KPI 指标卡
│ │ │ └── Loading.tsx # 加载/空状态
│ │ ├── pages/ # 页面组件
│ │ │ ├── Login.tsx # 登录页
│ │ │ ├── Overview.tsx # 概览看板
│ │ │ ├── Events.tsx # 事件分析
│ │ │ ├── ElementsContent.tsx # 元素内容分析
│ │ │ ├── Users.tsx # 用户分析
│ │ │ ├── Screens.tsx # 页面路径
│ │ │ └── Detail.tsx # 数据明细
│ │ ├── config/chart.ts # Chart.js 配置
│ │ ├── utils/index.ts # 工具函数
│ │ └── index.css # 全局样式 + CSS 变量
│ └── dist/ # 构建产物(Flask 直接 serve)
├── tools/node/ # 便携版 Node.js v24.18.1
└── 生产优化文档.md
技术栈
- 后端: Flask + clickhouse-driver + Flask-CORS
- 前端: React 19 + TypeScript 6 + Vite 8 + Chart.js + lucide-react
- 数据库: ClickHouse (TCP 9000)
- 认证: Token-based(内存存储,12 小时 TTL)
二、已实现功能清单
1. 登录认证体系
- 后端: api/auth.py
/api/auth/loginPOST — 登录,返回 token/api/auth/infoGET — 获取用户信息/api/auth/logoutPOST — 登出- Token 生成: MD5(username + 随机盐),存内存字典 + 线程锁
- 全局鉴权: app.py
before_request拦截所有/api/*,白名单: login/health/info - 前端:
- AuthContext.tsx — 全局认证状态
- Login.tsx — 分屏式登录页
- ProtectedRoute.tsx — 路由守卫
- api/index.ts — 请求拦截器自动注入
Authorization: Bearer - 登录账号: admin / clklog(环境变量
AUTHADMINUSER/AUTHADMINPASSWORD可覆盖)
2. 环境切换(左上角)
- 位置: Sidebar 顶部,Logo 下方,项目选择上方
- 后端: app.py
/api/env/list、/api/env/switch - 前端: Sidebar.tsx 按钮组切换
- 机制: 前端 localStorage 存
X-Envheader,后端before_request读取并设到 thread-local - 环境配置: config.py
- 生产: 114.80.38.24:9000 / clklog / log_analysis
- 测试: 192.168.20.147:9000 / clklog / log_analysis
- 关键: 切换环境后调用
loadProjects({ resetSelection: true })强制刷新项目列表
3. 项目选择
- 位置: Sidebar,环境切换下方
- 后端:
/api/projects返回项目列表(按 event_count 降序) - 前端: ProjectSelector.tsx 多选下拉
- 使用
createPortal渲染到document.body,避免父容器裁切 position: fixed+ 动态计算坐标,彻底避免层叠上下文问题- 支持全选/取消全选/搜索
- 关键:
withProjects()工具函数将数组转为逗号分隔字符串传给后端
4. 概览看板
- 路由:
/ - 后端: api/overview.py + api/flow.py
- 模块: KPI 卡片、流量概览(本期/上期/环比/同比对比表)、事件趋势、事件类型、Top 事件、设备分布、地域分布、新老访客、搜索词、来源网站
- 前端: Overview.tsx 使用
Promise.allSettled并行加载 9 个接口
5. 事件分析
- 路由:
/events - 后端: api/events.py
- 模块: 事件分布、趋势对比、事件列表(分页+搜索)
6. 元素内容分析 ★新增
- 路由:
/elements - 后端: api/elements.py
- 接口:
/api/elements/kpi— 4 大指标(总点击/独立元素/用户数/页面数)/api/elements/top-contents— 元素内容排行(分页+搜索)/api/elements/types— 元素类型分布/api/elements/screen-heatmap— 各页面元素热区/api/elements/trend— 点击趋势- 前端: ElementsContent.tsx
- 数据字段:
elementcontent(元素文本,如"返回"、"全部A股")、elementtype(如 UIButton、button)、elementname(通常为空,用 elementcontent 代替)
7. 用户分析
- 路由:
/users - 后端: api/users.py
8. 页面路径分析
- 路由:
/screens - 后端: api/screens.py
- 注意: 使用
elementcontent和elementtype替代空的element_name
9. 数据明细
- 路由:
/detail - 后端: api/detail.py
三、关键配置与规范
数据库连接
- 协议: clickhouse-driver 仅支持 TCP (端口 9000),不支持 HTTP (8123)
- 连接池: database.py 5 分钟自动刷新
- 线程锁:
threading.Lock()防止并发查询报错 - 密码: 环境变量
CLICKHOUSE_PASSWORD,生产密码Ql@clklog2026 - Monkey-patch:
Connection.disconnect可能抛AttributeError(socket 为 None),已打补丁
后端 API 规范
- 所有 SQL 使用参数化查询
%(param)s,禁止 f-string 拼接用户输入 LIMIT/OFFSET可直接插入 SQL(老版本 clickhouse-driver 不支持参数化)- 日期/项目过滤: 使用
getdaterange()+getprojectfilter_params()装饰器 - 响应格式:
successresponse(data)/errorresponse(msg, code) - CORS: 限制为
http://localhost:3000(环境变量CORS_ORIGINS可覆盖) - Flask debug 模式必须关闭(防止多进程监听同端口)
前端规范
- 构建:
npm run build(tsc + vite build),需要 Node.js 18+ - 便携 Node:
tools/node/node-v24.18.1-win-x64/(系统未装 Node.js 时的备用方案) - 构建命令:
$env:Path = 'E:\AI工作台\埋点分析系统\tools\node\node-v24.18.1-win-x64;' + $env:Path; npm run build - 类型导入:
verbatimModuleSyntax开启,类型必须用import type - Chart.js:
drawBorder已废弃,改用border: { display: false } - 多接口并行: 使用
Promise.allSettled而非Promise.all - 全局状态消除:
selectedProjects通过 props 传递,不用模块级变量 - KpiCard icon: 传 JSX 节点
,不能传组件引用Icon
CSS 关键约束
.glass-card:hover不可使用transform(会创建层叠上下文,破坏下拉菜单).app-sidebar需要z-index: 50+overflow: visible(backdrop-filter 会创建层叠上下文)- 环境切换和项目选择容器需要
shrink-0(防止 flex 压缩) - ProjectSelector 下拉使用
createPortal+position: fixed(彻底避免裁切)
四、已解决的疑难问题
| 问题 | 根因 | 解决方案 |
|---|---|---|
| 环境切换/项目选择不可用(第1次) | .glass-card:hover 的 transform: translateY(-1px) 创建层叠上下文 | 移除 transform |
| 环境切换/项目选择不可用(第2次) | backdrop-filter 创建层叠上下文 + 下拉被父容器裁切 | sidebar 加 z-index:50 + overflow:visible;下拉用 Portal+fixed |
| 切换环境后项目不刷新 | setSelectedProjects 异步,loadProjects 读到旧 state | 改用 loadProjects({ resetSelection: true }) |
| 端口被旧进程占用 | Flask debug 模式启动多进程 | 关闭 debug,手动 kill 旧进程 |
| ClickHouse 连接失败 (516) | 缺少密码环境变量 | 设置 CLICKHOUSE_PASSWORD |
| ClickHouse 协议错误 | 用 HTTP 8123 连 TCP-only 驱动 | 改用 9000 端口 |
| element_name 字段为空 | 手机端不采集此字段 | 用 elementcontent + elementtype 替代 |
| LAG 窗口函数不支持 | ClickHouse 老版本 | 改用 groupArray + 数组函数 |
| LIMIT 参数化失败 | 老版本 clickhouse-driver | 直接插入 SQL 字符串 |
| CSV 逗号未转义 | 无 escape 函数 | 新增 csv_escape |
| N+1 查询 | 每个事件类型单独查询 | 合并为 GROUP BY stat_date, event |
| TypeScript JSX 报错 | 缺少 @types/react + tsconfig 无 jsx | 安装类型包 + 配置 "jsx": "react-jsx" |
| Top 活跃用户无数据 | 生产库 eventsessionid 全部为空,AND eventsessionid != '' 过滤掉所有数据 | 移除 session 过滤,改按 distinct_id 分组,单独查询 model 和注册日期 |
| 留存率显示 null | 后端 API 硬编码返回 None,未实现计算 | 实现7日留存:7天前新增用户队列与今日活跃用户 LEFT JOIN 计算 |
| 页面路径搜索无结果 | SQL 别名 any(title) as title 与 WHERE 的 title LIKE 冲突,ClickHouse 报错 | 别名改为 screen_title,避免别名冲突 |
| 页面路径搜索大小写不匹配 | ClickHouse LIKE 默认大小写敏感 | 使用 lower() 函数实现大小写不敏感匹配 |
| 各页面元素热区布局混乱 | 2列网格布局导致卡片高度不统一 | 改为单列表格布局,4列:页面名称/总点击/元素数/热门元素Top5 |
五、启动与构建
启动后端
cd E:\AI工作台\埋点分析系统\backend
$env:AUTH_ADMIN_USER='admin'; $env:AUTH_ADMIN_PASSWORD='clklog'
python app.py
# 服务运行于 http://127.0.0.1:5000
构建前端
$env:Path = 'E:\AI工作台\埋点分析系统\tools\node\node-v24.18.1-win-x64;' + $env:Path
cd E:\AI工作台\埋点分析系统\frontend
npm run build
# 产物输出到 dist/,Flask 直接 serve
验证后端接口
# 登录获取 token
curl.exe -s --noproxy '127.0.0.1' -X POST 'http://127.0.0.1:5000/api/auth/login' -H 'Content-Type: application/json' -d '{"username":"admin","password":"clklog"}'
# 用 token 访问接口
curl.exe -s --noproxy '127.0.0.1' 'http://127.0.0.1:5000/api/elements/kpi?start_date=2026-07-23&end_date=2026-07-29' -H 'Authorization: Bearer <TOKEN>'
六、数据库表结构要点
表: clklog.log_analysis
关键字段:
| 字段 | 说明 | 备注 |
|---|---|---|
stat_date | 分区日期 | 用于日期过滤 |
log_time | 日志时间 | 用于排序 |
project_name | 项目名 | 项目过滤 |
event | 事件类型 | $AppClick / $AppViewScreen / $SignUp 等 |
distinct_id | 用户 ID | 去重计数 |
screen_name | 页面名 | 页面分析 |
element_content | 元素文本 | 核心字段,如"返回"、"全部A股" |
element_type | 元素类型 | 如 UIButton / button / QLOtherButton |
element_name | 元素名 | 通常为空,用 element_content 代替 |
eventsessionid | 会话 ID | 流量分析用(生产库全为空字符串,不可作为过滤条件) |
client_ip | 客户端 IP | IP 去重 |
device, os, province, city | 设备/系统/地域 | 用户分析用 |
已知数据问题:
- 存在 2030-2052 年的异常分区
- 排序键为
distinct_id(不合理,与高频查询条件不匹配) - 缺少数据跳过索引
七、后续优化建议(未实施)
- 数据库索引优化: 添加 7 个数据跳过索引(event/screenname/province 用 set,city/distinctid 用 bloom_filter)
- 排序键优化: 改为
(projectname, statdate, event, distinct_id)复合排序键 - 清理异常分区: 删除 2030 年及以后的分区
- 生产部署: 使用 Gunicorn WSGI 服务器,配置 Dockerfile
- 安全加固: 密码加密存储(当前明文),JWT 替代内存 Token
- 监控告警: 接入 Prometheus + Grafana
八、修改注意事项
修改任何功能前,务必阅读本节,避免破坏已有功能。
- 修改 Sidebar 时: 不要给
.glass-card:hover加transform;环境/项目容器必须有shrink-0 - 修改 ProjectSelector 时: 下拉必须用
createPortal+position: fixed,不要改回position: absolute - 修改 API 时: 所有用户输入必须参数化,
LIMIT/OFFSET例外可直接插入 - 修改 app.py 时: 新蓝图必须注册;
before_request鉴权白名单要同步更新 - 修改 tsconfig 时: 必须有
"jsx": "react-jsx",否则所有 .tsx 报错 - 修改 CSS 时:
.app-sidebar必须有z-index: 50+overflow: visible - 新增页面时: 在 App.tsx 添加 lazy import + pageTitles + Route;在 Sidebar.tsx navItems 添加菜单项
- 新增 API 模块时: 在 api/index.ts 添加 elementsApi 风格的封装;在 backend/api/ 创建蓝图;在 app.py 注册
- 构建前: 确保便携 Node.js 在 PATH 中,或系统已安装 Node.js 18+
- 启动前: 确保端口 5000 无旧进程占用(
netstat -ano | findstr ':5000') - ClickHouse SQL: 聚合函数别名不要与 WHERE 字段同名(如
any(title) as title与title LIKE冲突) - ClickHouse LIKE: 默认大小写敏感,需用
lower()实现不敏感匹配 - 搜索功能: 所有页面搜索统一为点击按钮/回车触发,使用
keywordInput/keyword双状态分离
九、近期变更记录(2026-07-30)
9.1 元素分析页面优化
- 菜单重命名: 「元素内容分析」→「元素分析」(Sidebar.tsx)
- 热区布局: 各页面元素热区从 2 列网格改为单列表格 (ElementsContent.tsx)
9.2 用户分析修复
- Top 活跃用户 (users.py):
- 移除了
eventsessionid != ''过滤条件(生产库该字段全空) - 分组维度从
distinctid, model改为仅distinctid(避免同用户多设备拆分) - model 改为单独查询取出现次数最多的值
- 注册日期改为只查 Top 20 用户(IN 条件),不再全表扫描
- 返回条数从 10 条改为 20 条
- 7日留存率 (users.py):
- 算法:
7天前新增用户中今天仍活跃的人数 / 7天前新增用户总数 × 100% - 队列日期 =
enddate - 7天,基于isfirst_day = 'true'筛选 - 上期对比: 再往前7天的队列,上期队列为0时 trend 返回 null
9.3 全局搜索功能优化
- 4个页面统一改为点击搜索按钮/回车执行(原先输入即触发):
- Events.tsx — 搜索事件名称
- Screens.tsx — 搜索页面名称
- Detail.tsx — 搜索事件/页面
- ElementsContent.tsx — 搜索元素内容
- 实现方式: 分离
keywordInput(输入框绑定值)和keyword(提交后的搜索值),只有点击按钮或按回车才将 input 同步到 keyword 触发 API 请求
9.4 页面路径搜索修复
- 文件: screens.py
- Bug 1: SQL 别名
any(title) as title与 WHERE 子句title LIKE冲突 → 改为screen_title - Bug 2: ClickHouse
LIKE大小写敏感 → 使用lower(screen_name) LIKE lower(%(keyword)s)
十、近期变更记录(2026-07-31)
10.1 用户分析顶部看板样式统一
- 文件: Users.tsx
- 变更: 将 grid-cols-6 的 6 卡片改为 grid-cols-4 的 4 个 KPI 卡片(累计/新增/活跃/7日留存),WAU/MAU 移至趋势卡片标题栏右侧,加载态改为骨架屏
10.2 数据明细页增加 app_name 和元素下拉筛选
- 后端: detail.py 的 /events、/export、/filters 接口新增 appname、elementcontent 过滤参数
- 前端: Detail.tsx 新增"全部应用""全部元素"下拉框
10.3 数据明细下拉框优化
- /filters 所有查询改用 GROUP BY + ORDER BY count(*) DESC 降序,LIMIT 30
- 前端下拉框统一使用 flex: 1 + minWidth: '120px' 实现等宽平均分配
10.4 留存 KPI 修复
- 后端: retention.py
- 新增 getdate_range 统一返回 datetime.date 类型
- getmaxstatdate 改用 maxIf(statdate, statdate <= today()) 排除未来脏数据(最远到 2050 年)
- KPI 改为周期内所有有效 cohort 的平均留存率(旧逻辑只用 effectiveend - N 天的单个 cohort,与 startdate 无关,导致切换统计周期时数值不变)
- 前端: Retention.tsx 增加 formatRetention 函数将 null 显示为 '-'
10.5 项目下拉框修复
- 文件: ProjectSelector.tsx
- Bug: 下拉框通过 createPortal 渲染到 document.body,点击外部检测仅检查 dropdownRef,导致点击下拉项被误判为外部点击而关闭
- 修复: 新增 portalRef 跟踪 portal 容器,handleClickOutside 同时检查 buttonRef、dropdownRef、portalRef 三个 ref,仅当点击目标不在三者内时才关闭下拉框
- 增强: 新增确定按钮和 tempSelected 临时状态,选择后点击确定按钮主动刷新页面
10.6 修改密码功能 ★新增
- 前端: Header.tsx 用户下拉菜单新增"修改密码"项;新增 ChangePasswordModal.tsx 组件(旧密码/新密码/确认密码,含长度和一致性校验)
- 后端: auth.py 新增 /api/auth/change-password 接口(@login_required 保护)
10.7 系统用户管理模块 ★新增
- 后端:
- userstore.py — SQLite 存储,SHA-256 密码哈希,首次启动自动初始化 admin 账号
- api/sysusers.py — CRUD/权限/角色/重置密码/启禁用,全部 @admin_required 保护
- auth.py — 登录改为查 SQLite,兼容旧环境变量方式
- 前端:
- Sidebar.tsx — 新增"系统用户"菜单(仅 admin 可见)
- SysUsers.tsx — 管理页面(搜索/新建/编辑/权限设置/重置密码/启禁用/删除)
- 功能: 用户名(非必填)、登录名(必填,仅字母/数字/下划线)、手机号(非必填)、密码(可快捷选择默认 123456)、角色(admin/user)、项目权限、环境权限
- 安全限制: 不能删除/禁用最后一个启用的管理员,不能对自身账号执行删除/禁用/重置密码
- Bug 修复: status 字段是字符串 'active'/'disabled',前端不能用 number === 1 比较;非管理员直接访问 /sysusers 路由需重定向
10.8 部署打包 ★新增
- 打包脚本: build-package.ps1 — 一键打包,排除 pycache、venv、users.db
- 部署配置: deploy/ 目录
- Dockerfile — 单镜像方案(前后端 dist 一体,gunicorn 启动)
- docker-compose.yml — 编排文件(含数据卷持久化 + 健康检查)
- .env.example — 环境变量模板(已修复端口 8123→9000)
- scripts/start.sh / stop.sh — Linux 启动/停止脚本
- scripts/start.bat / stop.bat — Windows 启动/停止脚本
- DEPLOY.md — 详细部署指南
- UPGRADE.md — 升级指南(含回滚方案)
- 交付包: dist/tracker-analytics-v1.1.0.zip(57 个文件,0.25 MB)
- 修复: .env.example 端口错误(8123→9000)、Dockerfile 未包含前端 dist、启动方式从 python app.py 改为 gunicorn
十一、新增注意事项(2026-07-31)
修改任何功能前,务必阅读本节,避免破坏已有功能。
- 系统用户管理 API: 所有 /api/sysusers/* 接口必须使用 @adminrequired 装饰器,不能用 @loginrequired
- sys_users 表 status 字段: 是字符串 'active'/'disabled',前端不能用 number === 1 比较
- createPortal 下拉框: 点击外部检测必须同时检查 portalRef,否则点击下拉项会被误判为外部点击而关闭
- 留存 KPI 计算: 必须按选定周期内所有有效 cohort 的平均留存率计算,不能只取结束日的单个 cohort
- 生产库 statdate 脏数据: 存在未来日期(最远到 2050 年),必须用 maxIf(statdate, stat_date <= today()) 排除
- PowerShell 脚本编码: 含中文和特殊字符(如树形图 ├ │ └)的 here-string 会因编码问题解析失败,应改用数组拼接或独立文件
- Docker 部署: 必须使用包含前后端的单镜像 Dockerfile,不能只打包后端(Flask 需要托管 frontend/dist)
- .env.example 端口: CLICKHOUSE_PORT 必须是 9000(TCP 原生协议),不是 8123(HTTP)
- Token 内存存储: Flask 重启后所有 token 失效,用户需重新登录