Forráskód Böngészése

docs: 新增 docs/07 项目结构与模块 + api README 中文化 + 对齐 README/01/04 到现状

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tianyu.chu 1 hónapja
szülő
commit
04f902b90f
6 módosított fájl, 239 hozzáadás és 135 törlés
  1. 10 0
      CHANGELOG.md
  2. 13 23
      README.md
  3. 58 101
      apps/api/README.md
  4. 16 3
      docs/01-产品需求-MVP.md
  5. 8 8
      docs/04-设计规范.md
  6. 134 0
      docs/07-项目结构与模块.md

+ 10 - 0
CHANGELOG.md

@@ -4,6 +4,16 @@
 
 ## 2026-06-29
 
+### 新增
+- **`docs/07-项目结构与模块`**:新接手者的"地图"——顶层目录树、apps/web · apps/api · packages · infra 各模块与关键文件职责、线上/本地运行数据流、构建与 `/look` 部署流程、本地怎么跑。
+
+### 文档
+- **`apps/api/README` 翻成中文并更新**:补趋势端点、`DB_SCHEMA=ads`/search_path、假数据 ~95 天、`STATIC_DIR` 单进程同源托管、真实数据由上游 ETL 写入(不经 seed/alembic)。
+- **对齐滞后文档到现状**:
+  - `README`:部署段从"hypothetical nginx/conda+node20"改为实际(`/look` + uvicorn:8080 单进程同源 + 隔离 Miniconda),infra 目录说明更正,加 docs/07 指路 + 趋势两视图。
+  - `docs/04`:时间控件去"单日"字样、空态文案「数据缺失」→「暂无数据」、范围文案去"不含今日"、补趋势视图与图例/断线规范、补 `漏斗/趋势` Tab。
+  - `docs/01` §4:补 §4.4 趋势视图;§4.3 同步(默认昨日只显日期、范围文案、空态「暂无数据」)。
+
 ### 变更
 - **单日默认改回「昨日」**(`apps/web`):去掉 06-26 引入的"最新可用日"默认(原为省略 `snapshot_dt` → 后端取 `MAX(dt)`)。改回默认查昨日并发 `snapshot_dt`;昨日无数据就如实显示空状态——**空就空,是上游问题,不在应用侧兜底**。
   - 起因:上游 daily 跑批从 6/27 起写入全 0 行,`MAX(dt)` 落到空行 → 默认显示 0000。

+ 13 - 23
README.md

@@ -1,8 +1,9 @@
 # hs-data 内部数据平台
 
-面向内部的一站式数据平台门户。**MVP 已端到端交付一个可用模块:拼团漏斗**(`行为分析 > 漏斗分析 > 拼团漏斗`),其余能力域进导航、出"待开发"占位。
+面向内部的一站式数据平台门户。**MVP 已端到端交付一个可用模块:拼团漏斗**(`行为分析 > 漏斗分析 > 拼团漏斗`,含**漏斗**与**趋势**两视图),其余能力域进导航、出"待开发"占位。
 
-> 产品与信息架构见 [docs/01](docs/01-产品需求-MVP.md);技术架构与契约见 [docs/02](docs/02-技术架构.md) / [docs/03](docs/03-数据契约.md);视觉设计定稿见 [docs/04](docs/04-设计规范.md);协作约定见 [docs/05](docs/05-agent协作准则.md) 与根 [CLAUDE.md](CLAUDE.md)。改动留痕见 [CHANGELOG.md](CHANGELOG.md)。
+> **新接手先读 [docs/07 项目结构与模块](docs/07-项目结构与模块.md)**(目录层级 / 模块职责 / 运行与部署流程)。
+> 产品与信息架构见 [docs/01](docs/01-产品需求-MVP.md);技术架构与契约见 [docs/02](docs/02-技术架构.md) / [docs/03](docs/03-数据契约.md);视觉设计见 [docs/04](docs/04-设计规范.md);接口见 [docs/06](docs/06-接口文档.md);协作约定见 [docs/05](docs/05-agent协作准则.md) 与根 [CLAUDE.md](CLAUDE.md)。改动留痕见 [CHANGELOG.md](CHANGELOG.md)。
 
 ## 能力域(信息架构,5 个 L1)
 
@@ -25,12 +26,14 @@
 │   └── api/            # 后端(FastAPI · 只读查询服务)
 ├── packages/
 │   └── api-types/      # 由后端 OpenAPI 生成的 TS 类型(勿手改)
-├── docs/               # 产品 / 技术 / 数据契约 / 设计 / 协作
-├── infra/              # docker-compose(PG)、setup.sh(服务器装环境)
+├── docs/               # 产品 / 技术 / 数据契约 / 设计 / 协作 / 接口 / 结构
+├── infra/              # 部署与本地基础设施(look.sh / serve-hs-api.sh / serve.py / docker-compose)
 ├── CLAUDE.md           # 给 AI 与人的开发硬约束
 └── CHANGELOG.md
 ```
 
+> 各模块/文件职责、运行与部署数据流详见 [docs/07 项目结构与模块](docs/07-项目结构与模块.md)。
+
 ## 技术栈
 
 | 层 | 选型 |
@@ -83,25 +86,12 @@ cd apps/api && .venv/Scripts/pytest -q             # pytest
 
 日常:从 `feature` 切个人分支 → 改 → push → **PR 合入 `feature`**(管理员合并,合并后自动删)。
 
-## 部署到服务器(对外提供 web 服务)
-
-目标服务器为 **CentOS 7**(glibc 2.17,自带 Node 16 / Python 3.6 / 无 docker)。Node 20 / Python 3.11 的官方二进制在此跑不起来,两种部署法:
+## 部署到服务器
 
-**① conda 自建环境(源码留 git,服务器自行构建/运行)**
-```bash
-# 服务器一次性:装 Miniconda,再
-conda create -n hs python=3.11 nodejs=20 -y && conda activate hs
-git clone http://git.hobbystocks.cn/tianyu.chu/hs-data.git ~/hs-data && cd ~/hs-data
-bash infra/setup.sh           # 装依赖
-corepack pnpm@10 --filter @hs-data/web build      # 出 dist/
-# 用 nginx 或 python -m http.server 托管 apps/web/dist
-```
+目标服务器 **CentOS 7**(glibc 2.17),内网 **http://100.64.0.62:8080/**。因 glibc 太老跑不了现代前端构建,采用**本地构建 → git 送达 → 服务器托管+跑后端**:
 
-**② 本地构建 + 服务器只托管静态**(服务器不需要 Node/Python)
-```bash
-# 本地(Windows)
-corepack pnpm@10 --filter @hs-data/web build       # 出 apps/web/dist
-# 把 dist/ 传到服务器,用 nginx / python3 -m http.server 托管
-```
+- **一键部署**:`/look`(即 `bash infra/look.sh`)—— web 测试+构建 → 推 `feature`(源码)+ `deploy`(产物)到 GitLab → 服务器 `git pull` → 上线。**仅显式触发**。
+- **单进程同源**:服务器用隔离 Miniconda 的 Python 3.11 跑 **uvicorn**,以 `STATIC_DIR` 同源托管前端 SPA(`~/hs-data` 产物,实时读盘)+ `/api`,端口 8080。前端相对 `/api` 调用 → 免 CORS、免反代。
+- 纯前端改动免重启(实时读盘);仅当 `apps/api` 变更才重启后端。不动系统 Python(3.6/3.7,其他项目依赖)。
 
-前端单靠 mock 数据即可独立提供 web 服务;后端(FastAPI)按需用 conda 的 Python 3.11 起 `uvicorn`,或接真实 `ads_trd_group_funnel_*` 两表
+> 完整部署架构(隔离环境、两份 clone、按需重启、取数)见 [docs/02 §10](docs/02-技术架构.md) 与 [docs/07](docs/07-项目结构与模块.md) §7–8。

+ 58 - 101
apps/api/README.md

@@ -1,156 +1,113 @@
-# hs-data API
+# hs-data 后端(API)
 
-FastAPI backend for the hs-data platform. MVP delivers one module: the **拼团
-(group-buy) funnel** (启动 → 曝光 → 拼团详情 → 下单 → 成功) backed by two
-pre-aggregated tables:
+hs-data 平台的 FastAPI 只读查询服务。MVP 交付一个模块:**拼团漏斗**(启动 → 曝光 → 拼团详情 → 下单 → 成功),数据源是两张预聚合表:
 
-- `ads_trd_group_funnel_daily` — single day, keeps full history (daily
-  incremental insert of a new `dt`). Backs `period=day`.
-- `ads_trd_group_funnel_rolling` — rolling 7d/30d, only one row (daily
-  overwrite, no history). Backs `period=last_7d` / `last_30d`.
+- `ads_trd_group_funnel_daily` —— 单日、保留全历史(每天增量插一行 `dt`)。支撑 `period=day`。
+- `ads_trd_group_funnel_rolling` —— 滚动 7d/30d、只有一行(每天覆盖,无历史)。支撑 `period=last_7d` / `last_30d`。
 
-Data is T+1: today's data is not computed yet, so the max queryable day is
-always yesterday.
+数据 **T+1**:今天的数据还没算出来,最大可查日始终是昨日。两表在 `ads` schema 下;ORM 表名不带 schema,靠连接 `search_path=ads` 解析(见 `db/session.py`)。
 
-## Tech stack
+> 完整接口契约见 [docs/06-接口文档](../../docs/06-接口文档.md);取数与口径见 [docs/02 §5–6](../../docs/02-技术架构.md);表结构见 [docs/03 §11](../../docs/03-数据契约.md)。
 
-Python 3.11+, FastAPI, Pydantic v2, SQLAlchemy 2.x (async) + asyncpg, Alembic,
-pytest.
+## 技术栈
 
-## Setup
+Python 3.11+、FastAPI、Pydantic v2、SQLAlchemy 2.x(async)+ asyncpg、Alembic、pytest。
+
+## 安装
 
 ```bash
 cd apps/api
 python -m venv .venv
-. .venv/bin/activate          # Windows: .venv\Scripts\activate
+.venv\Scripts\activate         # macOS/Linux: . .venv/bin/activate
 pip install -e ".[dev]"
 ```
 
-## Configuration
+## 配置
 
-Two environment variables (see `.env.example`):
+环境变量(放 `apps/api/.env`,已 gitignored;勿提交密码):
 
 ```
-# true  -> serve realistic in-memory data (no DB needed). DEFAULT for now.
-# false -> read the real group-buy funnel tables from Postgres.
+# true  → 内存假数据(无需 DB),目前默认。
+# false → 读真实拼团漏斗两表。
 USE_FAKE_DATA=true
 
-# Async SQLAlchemy URL (only used when USE_FAKE_DATA=false).
-DATABASE_URL=postgresql+asyncpg://hsdata:hsdata@localhost:5432/hsdata
+# 异步 SQLAlchemy URL(仅 USE_FAKE_DATA=false 时使用)。
+DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/db
+
+# 表所在 schema,默认 ads;通过连接 search_path 生效。
+DB_SCHEMA=ads
 ```
 
-### Fake-data fallback
+### 假数据兜底
 
-So the page can be seen before any Postgres exists, `USE_FAKE_DATA` defaults to
-`true`. In that mode the funnel API serves realistic data through the **same
-repository interface** as the real source, so route and service code is
-identical:
+为了没有 PostgreSQL 也能看页面,`USE_FAKE_DATA` 默认 `true`。该模式通过**与真实源相同的仓库接口**提供数据,路由/服务代码完全一致:
 
-- **Daily history** — ~10 descending daily rows ending yesterday, so the
-  single-day date picker has history and different `snapshot_dt` values return
-  different data. A `snapshot_dt` with no matching row → `missing`.
-- **Rolling** — one as-of-yesterday row for the 7d/30d windows.
+- **日表历史** —— 约 95 天递减日行(到昨日为止;最老若干天为全 0 模拟上线前爬坡),让单日日历有历史、不同 `snapshot_dt` 返回不同数据,也让趋势的"裁前导 0"被覆盖到。无匹配行的 `snapshot_dt` → `missing`。
+- **滚动行** —— 一行 as-of 昨日,供 7d/30d。
 
-Set `USE_FAKE_DATA=false` to query the real tables.
+设 `USE_FAKE_DATA=false` 即查真实表。
 
-## Run the server
+## 启动
 
-No infrastructure (fake data, default):
+无基础设施(默认假数据):
 
 ```bash
 uvicorn app.main:app --reload --port 8000
 ```
 
-Against a real Postgres:
+接真实 PostgreSQL:在 `.env` 配好 `DATABASE_URL` + `USE_FAKE_DATA=false` 后直接起(真实数据由上游 ETL 写入,**不需** alembic/seed):
 
 ```bash
-export USE_FAKE_DATA=false                  # Windows: $env:USE_FAKE_DATA="false"
-export DATABASE_URL=postgresql+asyncpg://hsdata:hsdata@localhost:5432/hsdata
-alembic upgrade head
-python -m scripts.seed                      # insert daily history + rolling row
-uvicorn app.main:app --reload --port 8000
+uvicorn app.main:app --port 8000
 ```
 
-OpenAPI docs at <http://localhost:8000/docs>. Health probe at `/health`.
+OpenAPI 文档在 <http://localhost:8000/docs>,探活 `/health`。
 
-## Database migration
+> **线上**:同一个进程用 `STATIC_DIR` 环境变量同源托管前端 SPA(`~/hs-data` 产物)+ `/api`,端口 8080,取代独立静态服。本地/测试不设 `STATIC_DIR` 则只出 `/api`。见 docs/02 §10。
+
+## 本地建表 / 灌样例(仅本地假 PG 用)
 
 ```bash
-alembic upgrade head    # creates the daily + rolling tables
+alembic upgrade head        # 建 daily + rolling 两表
+python -m scripts.seed      # 灌 ~95 天日行 + 一行滚动
 ```
 
-## Seed sample data
-
-Inserts ~10 historical daily rows (ending yesterday) into the daily table plus
-one rolling row as-of yesterday, all with clean descending group-buy funnels:
+## 导出 OpenAPI(无需 DB)
 
 ```bash
-python -m scripts.seed
+python -m scripts.export_openapi   # 写 apps/api/openapi.json
 ```
 
-## Export OpenAPI schema (no DB needed)
+## 接口
 
-```bash
-python -m scripts.export_openapi   # writes apps/api/openapi.json
-```
+### 1. 漏斗查询 `POST /api/funnels/query`
 
-## API
+请求 `{ "period": "day", "snapshot_dt": "2026-06-20" }`
 
-`POST /api/funnels/query`
+- `period` ∈ `day` | `last_7d` | `last_30d`,其它值 → 422。
+- `snapshot_dt`(可选 `YYYY-MM-DD`):**仅 `day` 有意义**。省略 → 最新日行(`MAX(dt)`);给值 → 该历史日。必须 ≤ 昨日(T+1),今天/未来 → 422;`last_7d`/`last_30d` 忽略。
 
-Request:
+响应每步含 `step_index / name / event_key / uv / conversion_rate / dropoff_rate`,外加 `period / snapshot_dt / data_status`。
 
-```json
-{ "period": "day", "snapshot_dt": "2026-06-20" }
-```
+### 2. 趋势查询 `POST /api/funnels/trend`(按日折线)
 
-- `period` ∈ `day` | `last_7d` | `last_30d`. Any other value → HTTP 422.
-- `snapshot_dt` (optional ISO `YYYY-MM-DD`): **only meaningful for `day`**.
-  Omitted → latest daily row (yesterday); given → that historical day. Must be
-  ≤ yesterday (T+1); today/future → HTTP 422. Ignored for `last_7d` / `last_30d`.
-
-Response:
-
-```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"
-}
-```
+请求 `{ "start_dt": "2026-05-23", "end_dt": "2026-06-21" }`(均可选)
+
+- 都省略 → **最新可用日往前 30 天**。`end_dt` ≤ 昨日,否则 422;`start_dt > end_dt` → 422。
+- 响应 `points[]` 按 `dt` 升序,每点复用上面的 `results`(口径一致);**裁前导全 0 天**;范围内缺口日直接缺点(前端断线、不补零)。
+
+### 口径与规则
+
+- `period=day` → `ads_trd_group_funnel_daily`(给 `dt` 取该行,否则 `MAX(dt)`);`last_7d`/`last_30d` → `ads_trd_group_funnel_rolling` 唯一行的 `*_7d`/`*_30d`。
+- 不读 bitmap、不做 OR、不跨天聚合。
+- `step_index` 从 1 开始;第 1 步转化率/流失率为 `null`。
+- `conversion_rate[i] = uv[i]/uv[i-1]`,`dropoff_rate[i] = 1 - conversion_rate[i]`;`uv[i-1]==0` 时均为 `null`(不除零)。
+- `data_status` ∈ {`ready`, `missing`}:目标行不存在或对应列为 NULL → `missing`,**绝不补零**。
 
-### Routing & rules
-
-- `period=day` → `ads_trd_group_funnel_daily`. Given `snapshot_dt` → `WHERE
-  dt=:dt`; otherwise latest `ORDER BY dt DESC LIMIT 1`. Columns
-  `uv_start/show/detail/order/paid`.
-- `period=last_7d` → `ads_trd_group_funnel_rolling` (single row), columns `uv_*_7d`.
-- `period=last_30d` → same rolling row, columns `uv_*_30d`.
-- No bitmaps, no OR, no cross-day aggregation.
-- `snapshot_dt` (response) is the `dt` (yyyyMMdd) of the row actually used: the
-  day for `day`, the rolling row's as-of dt for 7d/30d. `null` when missing.
-- `step_index` starts at 1. Step 1 has `null` conversion/dropoff rates.
-- `conversion_rate[i] = uv[i] / uv[i-1]`; `dropoff_rate[i] = 1 - conversion_rate[i]`.
-  If `uv[i-1] == 0`, both are `null` (no division by zero).
-- `data_status` ∈ {`ready`, `missing`}: `ready` when the target row exists and
-  the period's columns are non-null; `missing` when there is no row OR the period
-  columns are NULL. Missing data is never silently zero-filled.
-
-## Tests
+## 测试
 
 ```bash
 pytest
 ```
 
-Period validation, day vs rolling routing, `snapshot_dt` selection/validation
-(today/future → 422; non-existent dt → missing), conversion math, `data_status`,
-the fake data source (daily history + rolling), the SQLAlchemy repo against
-in-memory SQLite, and the exact API response shape are all tested without
-Postgres.
+覆盖:period 校验、day vs rolling 路由、`snapshot_dt` 选择/校验(今天/未来 → 422;不存在 → missing)、趋势范围/裁前导0/缺口/校验、转化口径、`data_status`、假数据源、SQLAlchemy 仓库(内存 SQLite)、以及接口响应结构——全程无需 Postgres。

+ 16 - 3
docs/01-产品需求-MVP.md

@@ -127,11 +127,24 @@ hs-data 是一个**面向内部的一站式数据平台门户**:把数仓产出
 
 ### 4.3 页面说明(视觉/交互定稿见 `docs/04`)
 
-- **时间控件**(标题行右):单日日历常驻 + 近 7 天/近 30 天 Tabs。
+L3 页含 **`漏斗 / 趋势` 两视图 Tab**。
+
+**漏斗视图**:
+
+- **时间控件**(标题行右):单日(默认昨日,按钮只显日期)+ 近 7 天 / 近 30 天 segmented。
 - **结果图表区**:ECharts 漏斗图,每层 UV + 块比例。
 - **结果表格区**:步骤名 / UV / 转化率 / 流失率。
-- **快照说明**:单日"数据快照日:YYYY-MM-DD";近 7/30 天"数据截至 YYYY-MM-DD(近 N 天)"。
-- **状态**:`ready` 出图表+表格;`missing` 出"数据缺失"空态(**不补零**);传输错误出错误态。
+- **范围文案**:单日不显;近 7/30 天显示 `起 ~ 止` 日期范围。
+- **状态**:`ready` 出图表+表格;`missing` 出"暂无数据"空态(**不补零**);传输错误出错误态。
+
+### 4.4 趋势视图(按日折线)
+
+同一批 5 个事件的**按日**时间序列,数据源即 `ads_trd_group_funnel_daily`(天然时序)。
+
+- **两张折线**:事件 UV(5 步)+ 相邻转换率(4 段),图例可点开关。
+- **范围**:近 30 天(默认,相对最新可用日)/ 自定义起止(上限昨日)。
+- 前导全 0 天(上线前)裁掉;范围内缺口日断线、**不补零**。
+- 接口见 `docs/02` §5.1 / `docs/06` §3。
 
 ## 5. 演进路线图
 

+ 8 - 8
docs/04-设计规范.md

@@ -67,14 +67,14 @@
 
 ## 5. 控件与文案
 
-- **时间控件**(漏斗页标题行右侧)—— **统一 segmented 控件** `TimePeriodSelect`,三选一:`📅 单日 YYYY-MM-DD | 近 7 天 | 近 30 天`
-  - **选中态 = mint 实色 + 白字**(`bg-primary`),由 React 状态直接驱动(`cn(SEG, on?ON:OFF)`),**不依赖 shadcn Tabs/Button 的 `data-active` 变体**——避免选中态在浅页上隐形。任一选中都同款 mint,给明确"当前在此周期"反馈
-  - 「单日」段点开日历(popover)可选历史日:默认昨日、**上限昨日**(T+1,今天/未来禁选)、历史不设下限;选日即进单日模式
-  - 教训:此前用 shadcn Tabs 默认选中态(白底+微阴影)+ Button variant,选中高亮在浅色页几乎不可见;改为自绘 segmented + 状态驱动后彻底解决
-- **快照/截至说明**(结果区底部小灰字):
-  - 单日 → 「数据快照日:`YYYY-MM-DD`」
-  - 近 7/30 天 → 「数据截至 `YYYY-MM-DD`(近 7 天 / 近 30 天)」,点明窗口不含今天
-- **数据状态**:`ready` 出图表+表格;`missing` 出「数据缺失」空态(**不补零**);传输错误出 destructive Alert
+- **视图 Tab**(漏斗页标题行右侧)—— `漏斗 / 趋势` 两视图切换。**选中态 mint 实色由 React 状态驱动**,不用 shadcn Tabs 的 `data-active`(radix 用 `data-state`,会让选中态隐形)
+- **时间控件 · 漏斗视图** —— 统一 segmented `TimePeriodSelect`,三选一:`📅 YYYY-MM-DD | 近 7 天 | 近 30 天`(单日段只显日期,**不写"单日"字样**)
+  - **选中态 = mint 实色 + 白字**(`bg-primary`),React 状态直接驱动(`cn(SEG, on?ON:OFF)`),不依赖 shadcn `data-active`——避免选中态在浅页上隐形
+  - 单日段点开日历(popover)选历史日:**默认昨日**、上限昨日(T+1,今天/未来禁选)、历史不设下限。昨日无数据就显示空态(**空就空,不在应用侧兜底**)
+- **时间控件 · 趋势视图** —— `TrendRangeSelect`:`近 30 天(默认) | 自定义起止`(自定义日历 range,上限锁最新可用日)。
+- **范围文案**(控件下方小灰字,**固定行高占位防抖**):漏斗单日不显;近 7/30 天与趋势显示 `起 ~ 止` 日期范围(不写"不含今日")
+- **数据状态**:`ready` 出图表(+表格);`missing` 出**「暂无数据」**空态(📥 图标 + 一行字,**不补零**);传输错误出 destructive Alert
+- **趋势图**:UV 折线(5 步)+ 相邻转换率折线(4 段),图例可点开关;缺口断线(`connectNulls:false`,不补零)
 - **Mock 徽章**:虚线灰边 + amber 小 dot「Mock 模式」(低饱和,不抢主色)。
 - 漏斗页副标题:「拼团漏斗:启动 → 曝光 → 拼团详情 → 下单 → 成功」。
 

+ 134 - 0
docs/07-项目结构与模块.md

@@ -0,0 +1,134 @@
+# 项目结构与模块
+
+> 给新接手者的"地图":目录层级、各模块职责、运行与部署流程。细节散落各专题文档,本文只做总览 + 指路。
+> 关联:产品/IA [docs/01](01-产品需求-MVP.md)、技术架构与契约 [docs/02](02-技术架构.md)、数据契约 [docs/03](03-数据契约.md)、设计规范 [docs/04](04-设计规范.md)、接口 [docs/06](06-接口文档.md)。
+
+## 1. 这是什么
+
+hs-data 是**内部一站式数据平台**(monorepo)。当前 MVP 端到端做通**一个** L3 报表——**拼团漏斗**(`行为分析 > 漏斗分析 > 拼团漏斗`),含两个视图:**漏斗**(固定 5 步快照)与**趋势**(按日折线)。其余能力域进导航、出"待开发"占位。
+
+## 2. 顶层目录
+
+```
+hs-data/
+├── apps/
+│   ├── web/            # 前端 SPA(React 19 + Vite + Tailwind v4 + shadcn/ui + ECharts)
+│   └── api/            # 后端只读查询服务(FastAPI + SQLAlchemy async + asyncpg)
+├── packages/
+│   └── api-types/      # 由后端 OpenAPI 生成的 TS 类型(契约同步用)
+├── docs/               # 产品 / 技术 / 数据契约 / 设计 / 协作 / 接口 / 本文
+├── infra/              # 部署与本地基础设施脚本(look.sh / serve*.sh / serve.py / docker-compose)
+├── CLAUDE.md           # 开发硬约束(给 AI 与人)
+└── CHANGELOG.md        # 改动留痕(按日期,顶部最新)
+```
+
+pnpm workspace(`pnpm-workspace.yaml`)管 `apps/*` + `packages/*`;`pnpm` 用 corepack 调用(`corepack pnpm@10 …`),无需全局装。
+
+## 3. apps/web —— 前端
+
+多模块单页应用:统一外壳(顶栏 + 左侧三级导航 + 内容区),业务模块挂内容区。当前只有漏斗模块是实模块,其余是占位。
+
+```
+apps/web/src/
+├── main.tsx / App.tsx          # 入口 + 路由装配
+├── routes/domains.ts           # L1/L2/L3 导航与路由数据(5 能力域,见 docs/01 §2)
+├── layout/                     # 外壳:AppLayout / NavTree(可折叠导航)/ Breadcrumb / ThemeToggle
+├── api/
+│   ├── funnel.ts               # 查询客户端 + mock(queryFunnel / queryFunnelTrend);USE_MOCK 开关
+│   ├── types.ts                # 前端契约类型(手维护,镜像后端;见 packages/api-types)
+│   └── queryClient.ts          # TanStack Query 客户端
+├── modules/
+│   ├── funnel/                 # ★ 拼团漏斗(唯一实模块)
+│   │   ├── FunnelPage.tsx      #   L3 页:漏斗 / 趋势 两视图 Tab(React 状态驱动)
+│   │   ├── FunnelView.tsx      #   漏斗视图:单日(默认昨日)/ 近7 / 近30 快照
+│   │   ├── TrendView.tsx       #   趋势视图:UV 折线 + 转换率折线,范围 近30天/自定义
+│   │   ├── period.ts           #   固定 5 步定义、日期工具、范围/区间助手
+│   │   ├── format.ts           #   UV / 比率格式化(null → "—",不补零)
+│   │   └── components/         #   TimePeriodSelect / TrendRangeSelect / FunnelChart / TrendChart / FunnelResult / ResultTable
+│   └── placeholder/            # "待开发"占位页(非可用模块统一复用)
+└── components/ui/              # shadcn/ui 原语(button/card/calendar/tabs/popover/…)
+```
+
+要点:ECharts 图(`FunnelChart`/`TrendChart`)**懒加载**拆独立 chunk;数据状态三态显式渲染——`ready` 出图、`missing` 出「暂无数据」(不补零)、传输错误出 Alert。
+
+## 4. apps/api —— 后端
+
+只读查询服务。无写入、无 bitmap、无跨天聚合;数据源是预聚合的两张 ADS 表。详见 [apps/api/README.md](../apps/api/README.md)。
+
+```
+apps/api/app/
+├── main.py                 # FastAPI 入口;按 STATIC_DIR 可选同源托管 SPA(线上单进程)
+├── config.py               # 设置(DATABASE_URL / DB_SCHEMA=ads / USE_FAKE_DATA)
+├── schemas.py              # Pydantic 契约(FunnelQuery* / FunnelTrend*)
+├── api/funnels.py          # 路由:POST /api/funnels/query、/api/funnels/trend
+├── services/
+│   ├── funnel.py           # 编排 + 转化口径 + 校验 + 趋势裁前导0;FunnelRepository 协议
+│   └── repository.py       # SqlAlchemy 仓库(连真表)+ Fake 仓库(USE_FAKE_DATA)
+└── db/
+    ├── models.py           # ORM:ads_trd_group_funnel_daily / _rolling(无 schema,靠 search_path=ads)
+    └── session.py          # async engine;真实连接挂 search_path=ads
+```
+
+`scripts/`:`export_openapi.py`(导 OpenAPI,无需 DB)、`seed.py`(本地假 PG 灌样例);`alembic/`(本地建表迁移)。**线上真实数据由上游 ETL 写入,不经 seed/alembic。**
+
+## 5. packages/api-types
+
+后端 `GET /openapi.json` → 生成前端 TS 类型,作为契约同步手段(`corepack pnpm@10 gen:api-types`)。当前前端实际用手维护的 `apps/web/src/api/types.ts`(刻意镜像契约,便于后续切换为生成类型)。
+
+## 6. infra
+
+| 文件 | 用途 |
+|---|---|
+| `look.sh` | **一键部署**:web 测试+构建 → 推 `feature`(源码)+`deploy`(产物)→ 服务器拉取 → 上线(仅 `/look` 显式触发) |
+| `serve-hs-api.sh` | 服务器单进程启动器(uvicorn 同源托管 SPA+API,端口 8080,脱离会话存活) |
+| `serve.py` | 旧纯静态 SPA 服(已被 uvicorn 同源托管取代,保留备用) |
+| `serve-start.sh` | 静态服守护启动器(历史,未启用) |
+| `setup.sh` | 服务器装依赖(历史脚手架) |
+| `docker-compose.yml` | 本地起 PostgreSQL(本地接真库时用) |
+
+## 7. 运行流程
+
+**线上**(单进程同源,见 docs/02 §10):
+
+```
+浏览器 ──HTTP──▶ 服务器 100.64.0.62:8080 (uvicorn, ~/miniconda3 的 Python 3.11)
+                   ├── GET /、/assets/*  → 托管 ~/hs-data 的 SPA 产物(STATIC_DIR,实时读盘)
+                   └── POST /api/funnels/* → 查 PostgreSQL 100.64.0.10:25432 (schema=ads)
+```
+
+前端相对 `/api` 同源调用 → 免 CORS、免反代;只有一个常驻进程。
+
+**本地开发**:Vite dev(:5173)跑前端,默认 `USE_MOCK` 直接出假数据,无需后端;需要时另起 uvicorn(:8000),Vite 代理 `/api` 过去。
+
+**数据**:T+1,最大可查日为昨日。单日可回溯历史;近7/30天为滚动窗口(rolling 表唯一行,只有最新 as-of)。口径见 docs/02 §5–6。
+
+## 8. 构建与部署流程(`/look`)
+
+```
+本地(Windows)
+  pnpm test + build(VITE_USE_MOCK=false)
+       │  产物 apps/web/dist
+       ▼
+  git push:  feature(源码)  +  deploy(纯产物分支)   ──▶ GitLab(git.hobbystocks.cn)
+                                                              │
+服务器(CentOS 7)  git pull                                    ▼
+  ~/hs-data  ← deploy 分支(SPA 产物,uvicorn 实时读盘,前端改动免重启)
+  ~/hs-api   ← feature 分支(后端源码;仅当 apps/api 变更才重启 uvicorn)
+```
+
+CentOS 7 glibc 2.17 跑不了现代前端构建,故**本地构建、git 送达、服务器只托管+跑后端**;Python 3.11 用隔离 Miniconda,不动系统环境。细节见 docs/02 §10。
+
+## 9. 本地怎么跑
+
+```bash
+corepack pnpm@10 install
+corepack pnpm@10 --filter @hs-data/web dev     # 前端(mock)→ http://localhost:5173
+
+# 后端(可选,默认假数据)
+cd apps/api && python -m venv .venv && .venv/Scripts/python -m pip install -e ".[dev]"
+.venv/Scripts/uvicorn app.main:app --reload --port 8000   # http://localhost:8000/docs
+
+# 测试
+corepack pnpm@10 --filter @hs-data/web test    # 前端 Vitest
+cd apps/api && .venv/Scripts/pytest -q          # 后端 pytest
+```