# 技术架构 > 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: ```text 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。 接口: ```text POST /api/funnels/query ``` 请求字段: ```json { "period": "day", "snapshot_dt": "2026-06-20" } ``` - `period` ∈ `day`(单日) | `last_7d` | `last_30d`。 - `snapshot_dt`(可选,`yyyy-MM-dd`):**仅对 `day` 有意义**。省略=最新(昨日),给值=该历史日;上限昨日(T+1),今天/未来 → 422。`last_7d`/`last_30d` 忽略该字段。 - MVP 不接受任意 `steps`、不接受自定义日期范围。 响应字段: ```json { "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_rate` 为 `null`。 - 相邻转化率 = `uv[i]/uv[i-1]`,流失率 = `1-转化率`;`uv[i-1]==0` 时为 `null`。 - `data_status` ∈ `ready` | `missing`(目标行不存在或对应列为空时 `missing`,不补零)。 ### 5.1 趋势 API(按日折线) 漏斗页「趋势」Tab 用:同一批 5 个事件的**按日**时间序列(事件 UV 折线 + 相邻转换率折线)。数据源就是 `ads_trd_group_funnel_daily`(天然时序),**无需新表/ETL**。 ```text POST /api/funnels/trend ``` 请求字段(均可选,`yyyy-MM-dd`): ```json { "start_dt": "2026-05-23", "end_dt": "2026-06-21" } ``` - 都省略 → **最新可用日往前 30 天**(`近30天`,相对最新可用日而非今天,因数据滞后)。 - `end_dt` 上限昨日(T+1),今天/未来 → 422;`start_dt > end_dt` → 422。 响应字段: ```json { "start_dt": "20260523", "end_dt": "20260621", "data_status": "ready", "points": [ { "dt": "20260523", "results": [ /* 同 §5 的 5 步 FunnelStepResult */ ] } ] } ``` - `points` 按 `dt` 升序;每个点复用 §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_dt` → `WHERE 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]==0` → `null`。 - `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` 不自动重启健康的后端以免抖动。