docs(deploy): document PV/UV stats and real client IP via nginx realip

This commit is contained in:
kairee committed 2026-11-01 15:40:22 +08:00
1 parent b215f8eb09
commit ff5e05998a
11 files changed
+308 -6

No files matched your search

+32 -1
View File
@@ -217,6 +217,7 @@ PRAGMA synchronous = NORMAL;
│ ├── scheduler.py # 内置任务调度
│ ├── logging_json.py # 脱敏 JSON 日志
│ ├── classify.py # 标题归一化、时长解析与打分
│ ├── client_ip.py # 来源 IP 提取、路由归一化、机器人判定(纯函数)
│ ├── video_ref.py # BV/av/URL/b23.tv 引用解析(纯函数)
│ ├── timeutil.py # 上海时区与每日时间
│ ├── bilibili/
@@ -238,8 +239,12 @@ PRAGMA synchronous = NORMAL;
│ └── web/ # P2 只读看板
│ ├── server.py # HTTP、路由、鉴权、静态资源
│ ├── queries.py # 看板 SQL 与数据整形
│ ├── visits.py # 访问统计内存缓冲与批量落库
│ └── static/ # 原生 JS/CSS、ECharts、预览页面
│
├── deploy/
│ └── nginx/ # 反代示例:真实来源 IP(realip)与站点配置
│
├── tools/
│ ├── seed_preview_db.py # 复制真实库并回填 7 天预览数据
│ └── import_fans_csv.py # 从 B 站粉丝历史 CSV 回填 up_stat
@@ -329,6 +334,7 @@ PRAGMA synchronous = NORMAL;
- `snapshot` 主键为 `(stat_date, aid)`,保证同日幂等;
- `up_stat` 主键为 `(stat_date, mid)`;
- `visit` 访问统计表主键为 `(stat_date, ip, path)`,`hits` 累加;数据先落内存缓冲、批量 upsert,`PV = SUM(hits)`、`UV = COUNT(DISTINCT ip)`;
- `folder` / `folder_resource` 保留为历史数据与重排序参考,**不再由服务写入**;
- `up_blacklist` 只在发现任务中生效,不影响已入库视频;
- `seen_aid` 永久保留搜索去重标记,候选过期删除后也不重复通知;
@@ -499,6 +505,7 @@ P3 实现说明:
### 9.1 API 原则
- P2 看板**只读**:业务接口只注册 GET,写方法统一返回 `405`;
- 仅显式放行两个 POST 端点:`POST /login`(建立会话)与 `POST /api/visit`(访问统计埋点);二者都不修改业务数据,其余 POST/PUT/PATCH/DELETE 仍统一 `405`;
- 数据管理(入库、忽略、拉黑、移出库、移除黑名单)继续通过 Telegram、CLI 或数据库完成;
- `/api/health` 和 `/api/session` 可匿名;
- 首页、榜单、视频/UP、粉丝页面和接口访客可见;
@@ -525,6 +532,8 @@ P3 实现说明:
| `GET` | `/api/candidates` | 候选池(只读) |
| `GET` | `/api/system` | 最近任务、下次任务、备份、黑名单 |
| `GET` | `/api/about` | 数据说明页当前口径(配置阈值、权重、覆盖度;无凭据) |
| `POST` | `/api/visit` | 访问统计埋点(匿名):归一化路径后写内存缓冲,定期批量落库 |
| `GET` | `/api/stats` | 访问统计(需登录):每日 PV/UV 序列、Top 页面,合并未落库缓冲 |
### 9.2 页面
@@ -538,7 +547,7 @@ P3 实现说明:
| UP 页 | 该 UP 的头像、mid、首RE时间、最近发布时间、全部视频、聚合趋势、单视频对比、总时长与播放·小时、入库时与当前粉丝数。 |
| 粉丝页 | 粉丝总数、日增、7 日净增、累计增长(标注起始日期);支持预设和自选日期,范围超过一年时按每月峰值日期聚合;最近记录默认 14 天并显示“较上一成功日”增量;不叠加视频数据。 |
| 候选池 | 待确认候选、规则命中、剩余保留时间;操作指向 Telegram/CLI。 |
| 系统状态 | 最近扫描、同步、快照、下次运行、备份、鉴权和黑名单。 |
| 系统状态 | 最近扫描、同步、快照、下次运行、备份、鉴权、黑名单与访问统计(近 30 天 PV/UV 与 Top 页面)。 |
### 9.3 预览模式
@@ -550,6 +559,15 @@ P3 实现说明:
- 重新生成预览库前必须停止预览服务,避免 Windows 文件占用;
- 第 9 轮起本地不再保留 `data/app.db` 真实副本;重新生成预览时用 `--source` 指向 `data/backup/` 中的真实库备份(如 `app-local-before-reseed-20261101.db`)。
### 9.4 访问统计与真实来源 IP
- 前端在整页加载与站内路由切换时通过 `navigator.sendBeacon("/api/visit")` 上报路径(`sort`/筛选等同路径重渲染不重复计数);
- 服务端 `reaction_board/web/visits.py` 把上报按 `(日期, IP, 路由)` 聚合到内存缓冲,后台线程每 `web.track.flush_seconds`(默认 60 秒)批量 upsert 到 `visit` 表;`server_close()` 时做最后一次 flush;
- `/api/stats` 合并未落库缓冲,因此系统页基本实时;`keep_days` 到期数据由后台按日清理;
- 真实 IP 由 nginx 用 `realip` 模块按 CDN 网段归一化后写入 `X-Real-IP`;应用(`reaction_board/client_ip.py`)仅在 socket 对端属于 `web.track.trusted_proxies` 时采信请求头,否则回退对端,避免直连请求伪造 `CF-Connecting-IP`/`EO-Connecting-IP`/`X-Forwarded-For`;
- 直连公网 EIP、Cloudflare、EdgeOne 三种来源的 nginx 配置与网段刷新脚本见 `deploy/nginx/`;配置项见 README 的 `web.track` 说明;
- 进程被强杀时最多丢失一个刷新周期的上报(可接受)。
---
## 10. 热度与趋势口径
@@ -632,6 +650,8 @@ uv run python tests/test_moves.py
- Telegram 用户与 chat ID 授权校验;
- 看板查询的筛选、分页、增长冷启动与状态过滤;
- 只读路由、鉴权、静态资源路径穿越和预览标记校验;
- 来源 IP 提取(直连 EIP/Cloudflare/EdgeOne、不可信对端伪造、IPv6、带端口、回退)与路由归一化、机器人判定;
- 访问统计内存缓冲(聚合、跨日、单键上限、flush 幂等、写失败并回、保留期清理)与 `POST /api/visit`、`GET /api/stats` 契约;
- 预览库生成的确定性、单调性、异常样本与真实库隔离;
- 粉丝历史 CSV 解析、同日去重、缺口计数与幂等/替换回填;
- UP 粉丝快照:首次写入不可覆盖、同轮去重、失败不中断、`live_since` 实时/回填标记、`backfill` 的 dry-run 与 limit;
@@ -759,6 +779,17 @@ uv run python tests/test_moves.py
- `/api/ups` 返回 `up_face` 与 `follower_latest`,`/api/ups/{mid}` 返回 `up_face`;
- 自动化测试更新:`test_migrations.py`、`test_database.py`、`test_web_queries.py`、`test_bilibili_client.py`、`test_library.py`、`test_snapshot.py`、`web_fixtures.py`。
### 第 12 轮:访问统计(PV/UV)✅(2026-11-01)
- 迁移 v7:新增 `visit(stat_date, ip, path, hits, first_at, last_at)` 聚合表;
- `reaction_board/client_ip.py`:来源 IP 提取、路由归一化与机器人判定纯函数(三种来源 + 伪造防护);
- `reaction_board/web/visits.py`:访问上报先落内存缓冲,后台每分钟批量 upsert,支持单键上限与保留期清理,关闭时最后一次 flush;
- 前端 `navigator.sendBeacon("/api/visit")` 埋点;Web 只读边界放宽为显式 POST 白名单(`/api/visit` + `/login`);
- `config.json` 新增 `web.track`(`enabled`/`header_order`/`trusted_proxies`/`count_bots`/`flush_seconds`/`keep_days`/`max_hits_per_ip_path_per_day`);
- 系统状态页展示近 30 天 PV/UV 与 Top 页面(`GET /api/stats`,需登录,合并未落库缓冲);
- `deploy/nginx/` 提供直连 EIP / Cloudflare / EdgeOne 的 realip 与站点配置、网段刷新脚本和说明;
- 自动化测试新增 `tests/test_client_ip.py`、`tests/test_visits.py`,并扩展 `test_config.py`、`test_web_queries.py`、`test_web_server.py`、`test_migrations.py` 等。
### P4:可选部署
- 反向代理、TLS、远程访问鉴权;
+30 -3
View File
@@ -22,6 +22,7 @@
| 搜索发现与候选池 | 可用(P3) | 每小时搜索前 2 页,分类打分后写入候选池。 |
| Telegram 通知与入库 | 可用(P3) | 入库 / 忽略 / 拉黑按钮;私聊消息手动入库。 |
| 本地数据看板 | 可用(P2/P9) | 只读总览、视频榜、UP 主榜、内容效率、视频/UP 详情、粉丝曲线、数据说明、候选池和系统状态。 |
| 访问统计(PV/UV) | 可用 | 前端埋点上报,按客户端 IP 与页面去重;先落内存、每分钟批量入库;系统页展示近 30 天。 |
## 核心工作流
@@ -176,10 +177,11 @@ uv run python app.py serve --preview --config data/preview/config.json
- 粉丝页只绘制粉丝曲线,不叠加视频数据;支持 7/30/90/全部与自选日期,范围超过一年时按每月峰值日期聚合;最近记录默认 14 天并显示较上一成功日的增量;
- 前端使用 History API 的干净路径路由(`/videos`、`/ups`、`/video/{aid}`、`/up/{mid}`、`/about`),不使用 `#/` hash;旧 `/rank` 链接仍可用;
- 候选池和系统状态只读展示,操作入口指向 Telegram/CLI;
- 系统状态页展示访问统计(近 30 天每日 PV/UV 与 Top 页面):前端在整页加载与站内路由切换时通过 `navigator.sendBeacon("/api/visit")` 上报,服务端按客户端 IP 与归一化路由聚合,先写内存缓冲、每分钟批量落库(详见 [访问统计与真实来源 IP](#访问统计与真实来源-ip));
- 首页、视频榜、UP 主榜、视频/UP 详情、粉丝页和数据说明页对访客可见;
- 配置 `web.auth_token` 后,“候选”和“系统”页面及其接口需要登录,启用登录页和 HttpOnly Cookie;访客的底部导航只显示公开页面;
- 本地回环地址且未配置 token 时视为无鉴权,全部页面可见;非回环地址且未配置 token 时拒绝启动;
- 所有业务接口只接受 GET,写方法返回 `405`;
- 所有业务接口只接受 GET,写方法返回 `405`;仅显式放行 `POST /api/visit`(访问统计埋点)与 `POST /login`(会话),其余 POST/PUT/PATCH/DELETE 仍统一 `405`;
- 首页/榜单聚合结果缓存 1 小时,粉丝结果缓存 24 小时;快照、发现、入库/忽略/拉黑、候选清理和粉丝 CSV 导入会立即让对应缓存失效。
### 预览数据
@@ -227,6 +229,28 @@ uv run python tools/import_fans_csv.py --csv E:\Downloads\history.csv --replace
- 写入前建议先备份 `data/app.db`,例如 `data/backup/app-pre-fans-YYYYMMDD.db`;
- 容器内运行时用 `--db /data/app.db` 或 `--config /config/config.json`,避免默认去找 `/app/config.json`。
## 访问统计与真实来源 IP
看板提供基础访问统计(PV/UV)。前端在整页加载与站内路由切换时通过
`navigator.sendBeacon("/api/visit")` 上报路径;服务端按 `(日期, 客户端 IP, 归一化路由)`
聚合,**先写入进程内缓冲、每分钟批量落库**到 `visit` 表,`/system` 系统状态页展示近
30 天每日 PV/UV 与 Top 页面。
统计按客户端 IP 去重,因此反代必须还原真实客户端 IP。链路为
`域名 → CDN → 公网 EIP → 云服务器内网 → 宿主机 nginx → 127.0.0.1:8080(容器)`:
容器内看到的 socket 对端是 Docker 网关,真实 IP 只能来自 nginx 注入的请求头。
- nginx 用 `realip` 模块按 CDN 网段把真实 IP 归一化到 `$remote_addr`,再写
`X-Real-IP` 传给容器;直连公网 EIP 时 `$remote_addr` 本身就是客户端;
- 应用只读 `X-Real-IP`,且仅当 socket 对端属于可信代理网段时采信,否则回退对端,
因此直连请求伪造的 `CF-Connecting-IP` / `EO-Connecting-IP` / `X-Forwarded-For` 无效;
- 三种来源(直连 EIP / Cloudflare / EdgeOne)的完整 nginx 配置、网段刷新脚本与
配置项说明见 [`deploy/nginx/README.md`](deploy/nginx/README.md)。
`config.json` 中的 `web.track` 控制统计行为:`enabled` / `header_order` /
`trusted_proxies` / `count_bots` / `flush_seconds` / `keep_days` /
`max_hits_per_ip_path_per_day`;默认过滤常见爬虫 UA、每分钟落库、保留 180 天。
## 快速部署(Docker)
仓库根目录已包含 `Dockerfile`、`compose.example.yml`、`.dockerignore` 和 `config.docker.example.json`。Compose 文件是示例,需要显式指定:
@@ -298,6 +322,7 @@ Copy-Item config.example.json config.json
- `track_fans`:需要每日追踪粉丝数的 UP mid;
- `discover`:搜索关键词、页数与时间窗(`keywords` / `pages` / `window_days`);
- `storage`:数据库与备份目录;
- `web`:绑定地址、端口、访问令牌与 `web.track`(访问统计开关、可信代理网段、上报头顺序、落库周期与保留期);
- `schedule` / `notify` / `classify` / `heat_weights`:任务节奏、通知阈值、分类规则和热度权重。
配置每 60 秒检查一次 mtime,凭据、分类规则等变更无需重启即可在下次任务读取;但 `schedule.discover_minutes` 与 `schedule.snapshot_at` 在启动时固化,修改后需重启 `run` 进程。
@@ -398,6 +423,8 @@ uv run python tests/test_auth.py # Cookie 合并、设备指纹、Set
uv run python tests/test_moves.py # 排序移动脚本:随机排列与头部用例
uv run python tests/test_favsort_http.py # favsort httpx 请求头与匿名回退
uv run python tests/test_config.py # 配置加载、校验、ENV 覆盖与热加载
uv run python tests/test_client_ip.py # 来源 IP 提取、路由归一化与机器人判定(纯函数)
uv run python tests/test_visits.py # 访问统计内存缓冲、批量落库与保留期清理
uv run python tests/test_migrations.py # 迁移、备份、回滚与保留期
uv run python tests/test_database.py # 单写队列、事务与并发
uv run python tests/test_repository.py # 收藏夹/视频状态机与黑名单
@@ -416,8 +443,8 @@ uv run python tests/test_timeutil.py # 上海时区与每日任务时间
uv run python tests/test_analytics.py # 热度、互动率、增长与封面
uv run python tests/test_preview_seed.py # 预览库回填、确定性与安全边界
uv run python tests/test_fans_csv.py # 粉丝 CSV 解析、替换与幂等回填
uv run python tests/test_web_queries.py # 看板查询层与冷启动
uv run python tests/test_web_server.py # 只读路由、鉴权、静态资源与预览模式
uv run python tests/test_web_queries.py # 看板查询层、冷启动与访问统计聚合
uv run python tests/test_web_server.py # 只读路由、鉴权、静态资源、访问统计与预览模式
uv run python tests/test_scheduler.py # 内置调度器到期/失败隔离
uv run python tests/test_provider.py # 凭据变化重建客户端与配置 revision
uv run python tests/test_favsort_run.py # 排序参数校验与混合资源类型
+19 -1
View File
@@ -20,7 +20,25 @@
"host": "0.0.0.0",
"port": 8080,
"auth_token": "replace-with-a-long-random-token",
"public_base_url": ""
"public_base_url": "",
"track": {
"enabled": true,
"header_order": ["X-Real-IP", "X-Forwarded-For"],
"trusted_proxies": [
"127.0.0.0/8",
"::1/128",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16",
"fc00::/7",
"fe80::/10"
],
"count_bots": false,
"flush_seconds": 60,
"keep_days": 180,
"max_hits_per_ip_path_per_day": 0
}
},
"storage": {
"database_path": "/data/app.db",
+19 -1
View File
@@ -30,7 +30,25 @@
"host": "127.0.0.1",
"port": 8080,
"auth_token": "replace-with-a-long-random-token",
"public_base_url": ""
"public_base_url": "",
"track": {
"enabled": true,
"header_order": ["X-Real-IP", "X-Forwarded-For"],
"trusted_proxies": [
"127.0.0.0/8",
"::1/128",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16",
"fc00::/7",
"fe80::/10"
],
"count_bots": false,
"flush_seconds": 60,
"keep_days": 180,
"max_hits_per_ip_path_per_day": 0
}
},
"storage": {
"database_path": "data/app.db",
+105
View File
@@ -0,0 +1,105 @@
# 访问统计与真实来源 IP
看板新增了基础访问统计(PV/UV)。前端在页面访问时通过
`POST /api/visit`(`navigator.sendBeacon`)上报,服务端先写入进程内缓冲、
每分钟批量落库到 `visit` 表(迁移 v7)。`/system` 系统状态页展示近 30 天
PV/UV 与 Top 页面。
统计按客户端 IP 去重,因此**必须在反代层还原真实客户端 IP**。
## 链路与 socket 对端
```text
浏览器 → 域名 → CDN(Cloudflare/EdgeOne) → 公网 EIP → 10.10.8.x(云服务器内网)
→ 宿主机 nginx → proxy_pass http://127.0.0.1:8080 → 容器
```
容器内 socket 对端经端口映射 SNAT 后是 Docker 网关(如 `172.17.0.1`),
不是真实客户端。因此真实 IP 只能来自 nginx 注入的请求头,且有三条来源:
| 场景 | nginx 处理 |
|---|---|
| 域名直接解析到公网 EIP | `$remote_addr` 即客户端,nginx 直接写 `X-Real-IP` |
| 经 Cloudflare | `realip` + `CF-Connecting-IP`(仅当对端属于 CF 网段才采信) |
| 经 EdgeOne | `realip` + `EO-Connecting-IP`(仅当对端属于 EdgeOne 网段才采信) |
应用只读 `X-Real-IP`,且**仅当 socket 对端属于可信代理网段**
(默认回环 + RFC1918 + Docker 网段,可配置)时才采信,否则回退到对端。
这样直连 EIP 的请求即使伪造 `CF-Connecting-IP` / `EO-Connecting-IP` /
`X-Forwarded-For` 也无法污染统计。
> 关键:用 `realip` 模块按 CDN 网段校验,而不是无条件相信 CDN 头。
> 若无条件相信,任何能直连公网 EIP 的人都能伪造来源 IP。
## 文件
| 文件 | 用途 |
|---|---|
| `reaction-board.conf` | 站点 `server {}`,反代到 `127.0.0.1:8080` 并注入真实 IP 头 |
| `realip-cloudflare.conf` | Cloudflare 场景,放在 `http {}` 内 |
| `realip-edgeone.conf` | EdgeOne 场景,放在 `http {}` 内 |
| `update-cloudflare-ips.sh` | 生成 Cloudflare 网段清单 `cloudflare-ips.conf` |
| `update-edgeone-ips.sh` | 由运维维护的 CIDR 清单生成 `edgeone-ips.conf` |
按实际来源**二选一**引入对应的 `realip-*.conf`(放在 `nginx.conf` 的
`http {}` 内),再 `include` 站点配置。
## Cloudflare
```nginx
# nginx.conf 的 http {} 内
include /etc/nginx/realip-cloudflare.conf;
include /etc/nginx/conf.d/reaction-board.conf;
```
```bash
sudo bash deploy/nginx/update-cloudflare-ips.sh
```
Cloudflare 的 IP 段变化很少,建议加 cron 每月刷新一次。
## EdgeOne
EdgeOne 节点网段会变动,需从 EdgeOne 控制台或 API 获取后维护成 CIDR 清单:
```bash
# source 为每行一个 CIDR 的清单
sudo bash deploy/nginx/update-edgeone-ips.sh /path/to/edgeone-cidrs.txt
```
```nginx
# nginx.conf 的 http {} 内
include /etc/nginx/realip-edgeone.conf;
include /etc/nginx/conf.d/reaction-board.conf;
```
若无法自动同步网段,可退化为:在安全组/防火墙只放行 EdgeOne 节点访问源站,
此时可放宽为直接采信 `EO-Connecting-IP`;但**同时保留直连公网 EIP 访问**的
场景下,仍建议使用 `realip` 网段校验。
## 应用侧配置
```jsonc
{
"web": {
"track": {
"enabled": true,
"header_order": ["X-Real-IP", "X-Forwarded-For"],
"trusted_proxies": [
"127.0.0.0/8", "::1/128",
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16",
"fc00::/7", "fe80::/10"
],
"count_bots": false,
"flush_seconds": 60,
"keep_days": 180,
"max_hits_per_ip_path_per_day": 0
}
}
}
```
- `flush_seconds`:内存缓冲落库周期;设为 `0` 可关闭后台刷新(仅调试用)。
- `count_bots`:是否统计命中常见爬虫/监控 UA 的请求,默认过滤。
- `max_hits_per_ip_path_per_day`:单个(IP, 页面, 日期)计数上限,`0` 表示不限。
- `keep_days`:保留天数,后台按日清理更早的记录。
+26
View File
@@ -0,0 +1,26 @@
# 站点反代:域名 → CDN → 公网 EIP → 本机 nginx → 127.0.0.1:8080(容器)。
#
# 配合 realip-*.conf(放在 http {} 内)先把真实客户端 IP 归一化到 $remote_addr,
# 再通过 X-Real-IP 传给容器。应用仅在 socket 对端属于可信代理网段时采信该头。
server {
listen 80;
server_name board.example.com; # TODO: 改成你的域名
# 回源 HTTP(CDN 侧终结 TLS)。若要 nginx 直接对公网提供 HTTPS,
# 另建 443 server 并配置证书,下列 proxy_set_header 保持不变。
client_max_body_size 1m;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; # 归一化后的真实客户端 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 追加真实对端
proxy_set_header X-Forwarded-Proto $scheme;
location / {
proxy_pass http://127.0.0.1:8080;
}
# POST /api/visit(访问统计埋点)与其余只读接口走同一上游,无需额外配置。
}
+12
View File
@@ -0,0 +1,12 @@
# 放到 nginx.conf 的 http {} 内(或 http 级 include)。
#
# 仅当与 nginx 建立连接的对端属于下列 Cloudflare 网段时,才采信
# CF-Connecting-IP;直连公网 EIP 的请求无法伪造该头。
#
# 网段清单由 update-cloudflare-ips.sh 生成到 /etc/nginx/cloudflare-ips.conf。
# 若暂时未生成,可先注释掉 include(此时 CDN 访客会被记为 CF 节点 IP)。
real_ip_header CF-Connecting-IP;
real_ip_recursive on;
include /etc/nginx/cloudflare-ips.conf;
+16
View File
@@ -0,0 +1,16 @@
# 放到 nginx.conf 的 http {} 内(或 http 级 include)。
#
# 仅当与 nginx 建立连接的对端属于 EdgeOne 节点网段时,才采信
# EO-Connecting-IP;直连公网 EIP 的请求无法伪造该头。
#
# 网段清单 /etc/nginx/edgeone-ips.conf 由 update-edgeone-ips.sh 生成,
# 源数据从 EdgeOne 控制台或 API 导出(会变动,建议定期刷新)。
real_ip_header EO-Connecting-IP;
real_ip_recursive on;
include /etc/nginx/edgeone-ips.conf;
# 备选:用 EdgeOne 的 X-Forwarded-For(首段为真实客户端)配合网段递归提取。
# real_ip_header X-Forwarded-For;
# real_ip_recursive on;
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# 生成 Cloudflare 节点网段清单,供 realip-cloudflare.conf 使用。
#
# 用法: sudo bash deploy/nginx/update-cloudflare-ips.sh [输出文件]
# 默认输出: /etc/nginx/cloudflare-ips.conf
set -euo pipefail
OUT="${1:-/etc/nginx/cloudflare-ips.conf}"
{
for url in https://www.cloudflare.com/ips-v4 https://www.cloudflare.com/ips-v6; do
curl -fsSL "$url" | sed '/^[[:space:]]*$/d; s/^/set_real_ip_from /; s/$/;/'
done
} > "${OUT}.tmp"
mv "${OUT}.tmp" "$OUT"
echo "已写入 $OUT"
if command -v nginx >/dev/null 2>&1; then
nginx -t && nginx -s reload
fi
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# 由 EdgeOne 节点 CIDR 清单生成 realip 配置,供 realip-edgeone.conf 使用。
#
# EdgeOne 节点网段会变动,需从 EdgeOne 控制台或 API 导出为「每行一个 CIDR」
# 的清单后传入本脚本(不硬编码易过期的网段)。
#
# 用法: sudo bash deploy/nginx/update-edgeone-ips.sh <cidr-list-file> [输出文件]
# 默认输出: /etc/nginx/edgeone-ips.conf
set -euo pipefail
SOURCE="${1:?用法: update-edgeone-ips.sh <cidr-list-file> [输出文件]}"
OUT="${2:-/etc/nginx/edgeone-ips.conf}"
sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d; s|^|set_real_ip_from |; s|$|;|' "$SOURCE" > "${OUT}.tmp"
mv "${OUT}.tmp" "$OUT"
echo "已写入 $OUT"
if command -v nginx >/dev/null 2>&1; then
nginx -t && nginx -s reload
fi
+8
View File
@@ -883,5 +883,13 @@ P0、P1、P2、P3、第 9 轮与第 10 轮已实施;P2 交付了只读本地
2. 基于粉丝数的"破圈"维度(播放/入库时粉丝数、粉丝分段、UP 中位数)——数据已就绪,榜单未实现;
3. P4(容器化 + 反代)继续保持可选。
第 12 轮(2026-11-01)交付:**基础访问统计(PV/UV)**:
1. 迁移 v7 新增 `visit(stat_date, ip, path, hits, first_at, last_at)` 聚合表;
2. 前端 `sendBeacon` 埋点 `POST /api/visit`;服务端内存缓冲、每分钟批量落库,关闭时 flush;
3. 来源 IP 兼容「域名直连公网 EIP / Cloudflare / EdgeOne」:nginx 用 realip 按 CDN 网段归一化到 `X-Real-IP`,应用仅在 socket 对端可信时采信(见 `deploy/nginx/`);
4. `/system` 系统状态页展示近 30 天 PV/UV 与 Top 页面;Web 只读边界放宽为显式 POST 白名单;
5. 纯函数 `client_ip.py` 与内存缓冲 `web/visits.py` 均有离线单元测试。
> 本地数据:第 9 轮起本地只保留可再生的预览库 `data/preview/app.db`(`tools/seed_preview_db.py` 生成),
> 原 `data/app.db` 真实副本已删除;需要重新生成或恢复时,用 `--source data/backup/app-local-before-reseed-20261101.db`。