02-技术架构.md 11 KB

技术架构

hs-data 平台的技术栈、目录结构、接口契约、计算与非功能性需求。

内容
文档版本 v1.0
文档状态 待评审
更新日期 2026-06-21

修订记录

版本 日期 修订内容
v1.0 2026-06-21 初版

1. 技术栈

MVP 使用前后端分离架构:

  • 前端:React 19 + Vite + TypeScript + Tailwind CSS v4 + shadcn/ui(Radix-based)+ ECharts(漏斗图)+ TanStack Query。
  • 后端:FastAPI(Python 3.11+)+ Pydantic v2 + SQLAlchemy(async)。
  • 存储:PostgreSQL 16(MVP 漏斗读 ads_trd_group_funnel 预聚合宽表,见 docs/03 §11)。

本项目不使用 Next.js。原因是内部数据后台不需要 SEO/SSR;前端用 React/Vite 更轻,UI 走 shadcn/ui 拷贝式组件 + Tailwind 工具类。

2026-06-24 修订:前端 UI 库由 Ant Design 5 改为 shadcn/ui + Tailwind v4(参见 CHANGELOG)。ECharts 漏斗图保留。 原 Ant Design 方案因观感不达预期被替换。bitmap 相关存储格式(原 bytea + roaring bitmap)随漏斗方案转向预聚合宽表已退出 MVP,挪到后续"泛用漏斗"阶段(见 docs/03 v2 头注)。

视觉/交互定稿见 docs/04-设计规范(神策风 mint 主题、布局、漏斗图、控件、抗抖滚动模型),已锁定。

2. 仓库结构

采用 monorepo:

apps/
  web/        # 前端应用
  api/        # FastAPI 后端服务
docs/         # 产品、技术、数据契约文档
infra/        # 本地开发和部署配置

MVP 阶段不拆分多个 Git 仓库。前后端、文档和本地基础设施放在同一个仓库,方便联调、交付和 AI 协作。

3. 前端职责

前端负责产品交互和展示(拼团固定漏斗,v3):

  • 周期选择:单日(可回溯历史日,上限昨日)/ 近 7 天 / 近 30 天。
  • 单日的历史日期不可选今天及未来(T+1)。
  • 请求后端漏斗查询接口。
  • 使用 ECharts 展示漏斗图。
  • 使用 shadcn/ui + Tailwind 展示表格、控件、空状态和错误提示。
  • data_status(ready / missing)显式渲染,缺失不补零。

前端不解析底层存储,只消费接口返回的 UV / 转化率 / 流失率。

4. 后端职责

后端负责漏斗查询 API:

  • 提供 POST /api/funnels/query(契约见 §5)。
  • 校验 period(枚举)与 snapshot_dt(仅单日有效、上限昨日)。
  • 从 PostgreSQL 读取预聚合宽表(ads_trd_group_funnel_daily / _rolling,见 docs/03 §11),按 period 选对应列,不读 bitmap、不做 OR、不跨天聚合
  • 返回每层 UV、相邻转化率/流失率、data_status(ready / missing)。

后端不存储埋点明细,不负责数仓产出逻辑。

5. 查询 API

v3(2026-06-24 修订):MVP = 拼团漏斗,数据源拆为两张表(ads_trd_group_funnel_daily + ads_trd_group_funnel_rolling,见 docs/03 §11)。 固定 5 步:启动 start → 曝光 show拼团详情 detail → 下单 order → 成功 paid。 单日支持历史日期(daily 表留全历史);近 7/30 天只有最新 as-of(rolling 表覆盖式 1 行)。数据 T+1,最大可查日始终昨日。 原 v1"泛用漏斗"(任意步骤 + bitmap)仍挪后续,详见 §6。

接口:

POST /api/funnels/query

请求字段:

{ "period": "day", "snapshot_dt": "2026-06-20" }
  • periodday(单日) | last_7d | last_30d
  • snapshot_dt(可选,yyyy-MM-dd):仅对 day 有意义。省略=最新(昨日),给值=该历史日;上限昨日(T+1),今天/未来 → 422。last_7d/last_30d 忽略该字段。
  • MVP 不接受任意 steps、不接受自定义日期范围。

响应字段:

{
  "period": "day",
  "snapshot_dt": "20260620",
  "results": [
    { "step_index": 1, "name": "启动",     "event_key": "start",  "uv": 10000, "conversion_rate": null, "dropoff_rate": null },
    { "step_index": 2, "name": "曝光",     "event_key": "show",   "uv": 8200,  "conversion_rate": 0.82, "dropoff_rate": 0.18 },
    { "step_index": 3, "name": "拼团详情", "event_key": "detail", "uv": 5100,  "conversion_rate": 0.62, "dropoff_rate": 0.38 },
    { "step_index": 4, "name": "下单",     "event_key": "order",  "uv": 2200,  "conversion_rate": 0.43, "dropoff_rate": 0.57 },
    { "step_index": 5, "name": "成功",     "event_key": "paid",   "uv": 1800,  "conversion_rate": 0.82, "dropoff_rate": 0.18 }
  ],
  "data_status": "ready"
}
  • snapshot_dt:实际取数那行的 dt(yyyyMMdd)。单日=该天;近 7/30 天=rolling 的 as-of 日(即"数据截至"日)。
  • step_index 从 1 开始;第 1 步 conversion_rate/dropoff_ratenull
  • 相邻转化率 = uv[i]/uv[i-1],流失率 = 1-转化率;uv[i-1]==0 时为 null
  • data_statusready | missing(目标行不存在或对应列为空时 missing,不补零)。

5.1 趋势 API(按日折线)

漏斗页「趋势」Tab 用:同一批 5 个事件的按日时间序列(事件 UV 折线 + 相邻转换率折线)。数据源就是 ads_trd_group_funnel_daily(天然时序),无需新表/ETL

POST /api/funnels/trend

请求字段(均可选,yyyy-MM-dd):

{ "start_dt": "2026-05-23", "end_dt": "2026-06-21" }
  • 都省略 → 最新可用日往前 30 天(近30天,相对最新可用日而非今天,因数据滞后)。
  • end_dt 上限昨日(T+1),今天/未来 → 422;start_dt > end_dt → 422。

响应字段:

{
  "start_dt": "20260523",
  "end_dt": "20260621",
  "data_status": "ready",
  "points": [
    { "dt": "20260523", "results": [ /* 同 §5 的 5 步 FunnelStepResult */ ] }
  ]
}
  • pointsdt 升序;每个点复用 §5 的 results(uv + conversion 服务端算,口径与漏斗页完全一致)。
  • 前导全 0 天(上线前爬坡)裁掉;start_dt/end_dt 反映裁剪后的实际首尾,无数据时为 null + missing
  • 范围内缺口日(ETL 漏跑)= 直接缺该 dt 点;前端按连续日期轴补 null 断线(不补零)。

6. 计算策略

MVP(拼团漏斗,v3)

数据源两张表(docs/03 §11),按 period 路由:

  • period = day → 读 ads_trd_group_funnel_daily;给 snapshot_dtWHERE dt=:dt,否则 ORDER BY dt DESC LIMIT 1;取 uv_start/show/detail/order/paid 五列。
  • period = last_7d → 读 ads_trd_group_funnel_rolling(唯一行),取 uv_*_7d
  • period = last_30d → 读 ads_trd_group_funnel_rolling(唯一行),取 uv_*_30d

UV 直接取列值;相邻转化率由 UV 计算。不读 bitmap、不做 OR、不做跨天聚合。 单日可回溯历史,近 7/30 天仅最新 as-of。

后续(泛用漏斗,v1 设计,暂不实现)

  • 任意步骤事件 + 自定义范围最大 15 天。
  • 标准周期优先读预计算结果;自定义范围读每日 bitmap,逐步骤跨天 OR 后取 cardinality。
  • 依赖 daily_event_bitmap / period_event_bitmap(docs/03 §4、§5),这两表 MVP 暂不落地。

7. 性能策略

系统是内部低并发数据平台,MVP 不引入额外缓存层。

后端性能策略(拼团两表,v3):

  • FastAPI 多 worker 部署。
  • 查询是单行宽表读取 + 取列值(取最新 dt 行或指定单日 dt),无 bitmap 计算、无跨天聚合,极轻。

MVP 不引入:

  • Redis。
  • Celery。
  • ClickHouse。

8. 测试策略

后端测试(对照实现):

  • period 校验:day / last_7d / last_30d 接受,非法值 422。
  • 单日:无 snapshot_dt 取最新行;给 snapshot_dt 取该日;不存在的日 → missing;snapshot_dt > 昨日(今天/未来)→ 422。
  • 近 7/30 天:从 rolling 唯一行取 *_7d / *_30d
  • 转化率:第 1 层 null;uv[i]/uv[i-1];uv[i-1]==0null
  • data_status:ready / missing;snapshot_dt 回显正确。

前端测试:

  • 周期选择(单日 / 近 7 天 / 近 30 天),默认单日。
  • 单日日历:今天/未来不可选;选历史日重查。
  • 图表渲染、表格渲染。
  • missing 数据缺失空态、错误态。

9. 前端模块结构与路由

导航信息架构(L1/L2/L3)以 01-产品需求 §2 为准。本节只讲路由落地。

前端是多模块单页应用,左侧导航按 5 个 L1 能力域组织(行为分析 / 指标体系 / 画像体系 / 数据看板 / 营销触达,见 docs/01 §2.1):

导航分组手动展开/折叠、多个同时展开、进入某页自动展开其所在分支(NavTree.tsx,交互见 docs/01 §2.3)。

  • 布局壳:统一外框(顶栏 + 左侧 L1 导航 + 内容区),各模块挂在内容区。
  • 三级路由:L1 能力域一级路由;有可用模块的域下挂 L2(分析模块)、L3(报表/实例)。
    • MVP 仅 行为分析 > 漏斗分析 > 拼团漏斗 一条 L1→L2→L3 链渲染完整分析页(当前实现路由 /funnel 即此 L3,后续可规整为 /behavior/funnel/group)。该 L3 页内含 漏斗 / 趋势 两视图 Tab:漏斗=固定 5 步快照(§5);趋势=事件 UV 与相邻转换率的按日折线(§5.1)。
    • 其余 L1/L2 路由渲染统一的"待开发"占位组件。
  • 占位机制:非可用模块复用同一占位组件(src/modules/placeholder/),文案统一"待开发",可正常进入、不报错、不空白。
  • L2 子菜单(域内多模块)与 L3 报表列表后续按路线图补;当前 MVP 导航可先平铺 7 个 L1 + 漏斗页,不强求展开全部 L2。

10. 部署架构

线上服务器 100.64.0.62(CentOS 7,glibc 2.17),内网访问 http://100.64.0.62:8080/

  • 单进程同源托管:服务器跑 uvicorn(app.main:app),用 STATIC_DIR 环境变量同源托管前端 SPA(~/hs-data,实时读盘)+ /api,端口 8080。前端相对 /api 调用走同源,免 CORS、免反向代理,只有一个常驻进程。本地开发/测试不设 STATIC_DIR,uvicorn 仅出 /api,前端由 Vite dev 提供。
  • 隔离运行时:服务器装用户态 Miniconda Python 3.11(~/miniconda3,旧版 py311_24.7.1 以兼容 glibc 2.17),依赖走 manylinux 轮子(pip --only-binary,无需编译器);不动系统 Python(3.6/3.7,其他项目依赖)。
  • 取数:apps/api/.env(gitignored)配 DATABASE_URL(直连内网 PG 100.64.0.10:25432)+ DB_SCHEMA=ads + USE_FAKE_DATA=false;session.py 给连接挂 search_path=ads
  • 两份 clone:~/hs-data(deploy 分支,纯前端产物)+ ~/hs-api(feature 分支,后端源码)。
  • 发布链路(infra/look.sh,纯 git):本地 test + build(VITE_USE_MOCK=false) → 推 feature(源码)+ deploy(产物)→ 服务器 git pull 更新 ~/hs-data(uvicorn 实时读盘,前端改动无需重启)+ ~/serve-hs-api.sh(nohup setsid,幂等)确保后端在跑。
  • 后端代码更新(非前端):需在服务器 cd ~/hs-api && git pull重启 uvicorn(pkill -f "uvicorn app.main:app"; ~/serve-hs-api.sh)——look.sh 不自动重启健康的后端以免抖动。