release: bump version to 0.52.0
This commit is contained in:
146
docs/plans/data-products-layer-guard-redesign-plan.md
Normal file
146
docs/plans/data-products-layer-guard-redesign-plan.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# 数据产品流水线、数据源批量运维与抗击穿图层接口计划
|
||||
|
||||
## Summary
|
||||
|
||||
前端展示分两类数据:
|
||||
|
||||
- **图层数据**:按 viewport、bbox、zoom、limit 返回,可降级、截断、缓存,用来保护服务器。
|
||||
- **聚合面板统计**:必须是全量统计,不受当前 viewport 限制,但不能实时扫全表;通过产品状态表或预计算统计提供。
|
||||
|
||||
也就是说:地图上低 zoom 可以只画摘要或局部数据,但面板里的“总船只数、总海缆数、BGP 活跃事件数、卫星数”等应该代表全局数据产品状态。
|
||||
|
||||
## Implementation Status
|
||||
|
||||
- 已新增 `POST /api/v1/datasources/trigger-batch`,支持按选中 `source_ids` 或筛选条件批量触发,并返回 `triggered/skipped/failed`。
|
||||
- 已改造 `/datasources` 页面,支持产品域、层级、启用状态、最近执行状态、是否已有数据和关键词筛选,并支持复选框批量采集。
|
||||
- 已新增 `/api/v1/data-products` 和 `/api/v1/data-products/{product_id}/status`,聚合面板可以读取全量/全局统计口径。
|
||||
- 已新增 `/api/v1/layers/*` 受控图层接口骨架,要求 `bbox/zoom/limit`,响应包含 `visible_count/returned_count/diagnostics`。
|
||||
- 非船只图层当前先复用已有 GeoJSON 转换再做保护层;下一步应把 cables/BGP/satellites 的 bbox 过滤继续下推到各自产品查询,避免转换前仍加载过多候选。
|
||||
|
||||
## Key Changes
|
||||
|
||||
- 新增数据产品状态/统计层:
|
||||
- 每个产品维护全量统计:总实体数、活跃数、最近更新时间、使用源、缺失源、冲突数、构建状态。
|
||||
- 统计在采集成功或产品投影完成后更新,不在用户打开页面时临时全表聚合。
|
||||
- 前端聚合面板统一读取产品统计接口,而不是从图层返回量推断总数。
|
||||
- 新增接口:
|
||||
- `GET /api/v1/data-products`
|
||||
- `GET /api/v1/data-products/{product_id}/status`
|
||||
- `GET /api/v1/layers/{product}/...`
|
||||
- `POST /api/v1/datasources/trigger-batch`
|
||||
- `/layers/*` 只负责可视化数据:
|
||||
- 支持 bbox、zoom、limit、since。
|
||||
- 可以返回 `degraded`、`truncated`、`cache_hit`。
|
||||
- 返回 `visible_count` 和 `returned_count`,但不作为全量统计来源。
|
||||
- `/data-products/*/status` 负责全量统计:
|
||||
- 返回 `total_count`、`active_count`、`source_counts`、`last_built_at`、`health`。
|
||||
- 数据来自预计算状态或轻量索引统计。
|
||||
- 即使图层降级,统计也保持全量口径。
|
||||
|
||||
## Product Processing
|
||||
|
||||
- 船只:
|
||||
- 图层:bbox snapshot + WS 聚合流,受限返回。
|
||||
- 统计:全量唯一 MMSI、最近窗口活跃 MMSI、AISStream/BarentsWatch/source counts。
|
||||
- 海缆:
|
||||
- 图层:viewport 内 cable segments/landing points,低 zoom 可简化路线。
|
||||
- 统计:全量 cable count、landing point count、relation count、graph 构建状态。
|
||||
- 处理:专用 cable graph assembler,区分路线源、登陆点源、关系源、补充源,不使用统一字段融合函数。
|
||||
- BGP:
|
||||
- 图层:active incidents/anomalies/collectors,按窗口和 limit 返回。
|
||||
- 统计:全量活跃事件、最近 24h/7d 事件数、collector 数、incident/anomaly 分布。
|
||||
- 处理:专用事件流水线,区分 observation、anomaly、incident、geo hint、infrastructure inference。
|
||||
- 卫星:
|
||||
- 图层:可见卫星或受控 limit。
|
||||
- 统计:全量卫星数、最新 TLE epoch、源覆盖情况。
|
||||
- 处理:按 NORAD id 生成轨道快照,TLE epoch 最新优先。
|
||||
|
||||
## Data Source Page
|
||||
|
||||
- 增加筛选:
|
||||
- 产品类型、启用/禁用、最近成功/失败/运行中/未执行、已采集/未采集、凭证状态、文本搜索。
|
||||
- 增加复选框批量操作:
|
||||
- 批量启用、禁用、采集、强制采集。
|
||||
- 一键采集改为:采集全部启用源、采集筛选结果、采集选中源。
|
||||
- 后端 batch 逻辑:
|
||||
- 禁用源 skipped。
|
||||
- 运行中源按 force 处理。
|
||||
- 单个失败不影响其他源。
|
||||
- 返回 `triggered`、`skipped`、`failed`,并包含每个 source 的原因和 task_id。
|
||||
|
||||
## Protection Rules
|
||||
|
||||
- 所有 `/layers/*` 接口必须有保护层:
|
||||
- limit clamp。
|
||||
- bbox/zoom 校验。
|
||||
- 低 zoom 降级。
|
||||
- 短 TTL 缓存。
|
||||
- 慢查询超时。
|
||||
- diagnostics 返回降级原因。
|
||||
- 全量统计不走图层查询:
|
||||
- 不允许为了面板统计在请求时 `.all()` 全量加载。
|
||||
- 统计由采集/投影任务异步更新。
|
||||
- 统计缺失时返回 `unknown` 或 `stale`,不触发重型实时计算。
|
||||
- 缓存失效规则:
|
||||
- 采集成功后失效对应产品缓存。
|
||||
- 海缆 graph cache 在路线、登陆点或关系源成功采集后失效。
|
||||
- BGP incident/anomaly 生成后失效 BGP layer cache。
|
||||
- 船只实时流使用短 TTL 或 viewport 级缓存,不清全局缓存。
|
||||
- 接口观测:
|
||||
- 记录每个 layer endpoint 的耗时、返回数量、是否降级、是否缓存命中、limit 是否被 clamp。
|
||||
- 对高频 viewport 请求增加简单 per-IP 或 per-user rate limit。
|
||||
|
||||
## Frontend UX
|
||||
|
||||
- 数据源页:
|
||||
- 顶部统计可作为快捷筛选入口:全部、启用、禁用、运行中、失败、未采集。
|
||||
- 表格左侧增加复选框。
|
||||
- 工具栏显示“已选择 N 个”,并提供批量按钮。
|
||||
- 筛选结果和选中结果分清楚,避免误触发全部源。
|
||||
- 批量采集完成后弹出摘要:触发、跳过、失败数量,可展开查看原因。
|
||||
- 设置/配置页:
|
||||
- “采集器设置”改为“数据产品配置”。
|
||||
- 产品内按源角色分组展示,而不是简单列出 collector。
|
||||
- 海缆显示路线源、登陆点源、关系源、补充源。
|
||||
- BGP 显示实时观测源、历史回填源、地理 hint 源、检测输出。
|
||||
- 船只显示实时 AIS、轮询 AIS、自定义补充源。
|
||||
- Earth 图层交互:
|
||||
- 聚合面板统计读取 `/data-products/*/status`,保持全量口径。
|
||||
- 图层面板展示当前图层是否降级、截断、缓存命中。
|
||||
- 对象详情展示来源证据:
|
||||
- 船只:字段来源、冲突。
|
||||
- 海缆:路线源、登陆点源、关系源。
|
||||
- BGP:事件证据、分组依据、地理推断依据。
|
||||
- 产品 degraded 时仍显示可用部分,并提示缺失源角色。
|
||||
|
||||
## Test Plan
|
||||
|
||||
- 图层接口:
|
||||
- 大 limit 被 clamp。
|
||||
- 低 zoom 降级。
|
||||
- 大数据集不全量内存过滤。
|
||||
- diagnostics 正确说明截断、缓存、降级。
|
||||
- 全量统计:
|
||||
- 面板统计不受 bbox 影响。
|
||||
- 图层返回 1000 条时,产品统计仍显示全量总数。
|
||||
- 统计陈旧时返回 `stale=true` 和 `last_built_at`。
|
||||
- 采集成功后对应产品统计刷新。
|
||||
- 数据源批量:
|
||||
- 筛选、选中、批量采集行为正确。
|
||||
- skipped/failed/triggered 分组正确。
|
||||
- 禁用源在 batch 中被 skipped。
|
||||
- 运行中源按 force 参数处理。
|
||||
- batch 单源失败不阻断整体。
|
||||
- 产品处理:
|
||||
- 海缆缺 relation 时产品状态 degraded,但 cable layer 可用。
|
||||
- BGP observation 不直接变成前端 marker,必须经过 anomaly/incident 投影。
|
||||
- 船只 bbox snapshot 和 WS 节流继续有效。
|
||||
- 卫星列表不返回无限轨道点。
|
||||
|
||||
## Assumptions
|
||||
|
||||
- 前端聚合面板以后只读 `/data-products/*/status`。
|
||||
- 地图图层只读 `/layers/*`。
|
||||
- 统计可以短暂 stale,但不能因实时全量统计击穿服务器。
|
||||
- 保留现有 collector,不为了重构而删除 BarentsWatch 或其他源。
|
||||
- 当前开发阶段允许前端从旧 `/visualization/geo/*` 迁移到 `/layers/*`。
|
||||
116
docs/plans/docs-audience-split-plan.md
Normal file
116
docs/plans/docs-audience-split-plan.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# 文档受众分层重构计划
|
||||
|
||||
**状态**:待实施
|
||||
**创建日期**:2026-05-12
|
||||
**核心目标**:把 `docs/technical/{zh,en}/manual.md` 拆成"纯客户视角"的使用手册,把 `planet.sh`、日志、LAN、故障排查这类运维内容迁到独立 `ops-runbook.md`,并把分层规则写进 `documentation-coverage-rules.md` 和 `.claude/commands/docs.md`,让以后写文档时自动按受众归档。
|
||||
|
||||
## 背景
|
||||
|
||||
当前 `manual.md` 把客户实际使用和开发/运维操作混在一份文档里:开头 200 多行讲的是 `planet.sh start/stop/restart/log/health/createuser/--allow-lan`、AI Provider 镜像构建、`netsh portproxy` 和故障排查顺序,后面才进入 Earth、控制台、AI、Docs 这些客户真正会用到的功能。
|
||||
|
||||
客户读到一半会被 shell 命令吓住,开发者想找运维细节又要在大段 UI 操作里翻。`documentation-coverage-rules.md` 现在也没有受众分层规则,未来文档继续混着写。
|
||||
|
||||
本计划假定客户已经能拿到账号登录使用 — 注册/验证流程本身见 [用户公开注册与邮箱验证计划](/home/ray/dev/linkong/planet/docs/plans/user-registration-email-verification-plan.md)。
|
||||
|
||||
## 新的文档地形
|
||||
|
||||
| 文档 | 受众 | Gatekeeper 组 | 范围 |
|
||||
| --- | --- | --- | --- |
|
||||
| `manual.md` (zh+en) | 纯客户/最终用户 | `public` | 注册、登录、账户、设置 UI、collector 配置、AI 配置、Console 页面、Earth、Docs 浏览 |
|
||||
| `quickstart.md` (zh+en) | 纯客户 | `public` | "我刚拿到 Planet 怎么开始用" — 打开 URL → 注册 → 验证 → 登录 → 第一次配置 |
|
||||
| `ops-runbook.md` (zh+en, **新增**) | 运维/部署人员 | `docs_admin` | `planet.sh` 完整命令、健康检查、日志位置、LAN/portproxy、故障排查顺序、createuser CLI、Bun 构建约定 |
|
||||
| `ops-planet-sh-startup.md` (已存在) | 运维 | `docs_admin` | 启动性能、AI Provider 镜像、`PLANET_LOAD_ZSHRC_ENV` 深度调优 — 保持不动 |
|
||||
| 现有 `*-context.md` / `backend-*.md` | 二次开发者 | `docs_developer` | 保持现状 |
|
||||
|
||||
`backend-system-service-control.md` 偏后端服务控制原理,**不**和 `ops-runbook.md` 重复 — runbook 讲"运维要敲什么命令",service-control 讲"后端怎么实现服务管控"。
|
||||
|
||||
## manual.md 重写后的章节顺序(客户旅程)
|
||||
|
||||
1. **欢迎与入口** — Planet 是什么、几个入口(Earth 公开 / Console 需登录 / Docs / API)
|
||||
2. **注册账户** — 打开 `/login` → 点"注册" → 填用户名/邮箱/密码 → 收邮件 → 输入 6 位验证码 → 登录
|
||||
3. **登录与找回密码** — 登录页、忘记密码流程
|
||||
4. **账户设置** — 修改密码、修改邮箱(需重新验证)、查看权限组、登出
|
||||
5. **Console 总览** — 左侧菜单结构、各路由用途
|
||||
6. **配置数据采集器** — `/settings?tab=collector_credentials`:选择 collector、连接测试、保存凭证;BarentsWatch / AISStream 两个典型例子
|
||||
7. **配置 AI 凭证** — `/ai?tab=providers`:默认 provider、模型、Base URL、API Key、本地代理;工具 tab(WebSearch、OCR)
|
||||
8. **系统设置** — `/settings` 其他子 tab(系统设置、电视直播源、SMTP 邮件)
|
||||
9. **用户管理(管理员)** — `/users`:创建、删除、改角色、Gatekeeper 权限组
|
||||
10. **数据探索** — `/datasources`、`/data`、`/bgp`、`/alerts/*`
|
||||
11. **AI 测试台** — `/ai?tab=playground`
|
||||
12. **Earth 公开页面** — 现 manual.md 的 Earth 章节原样保留(图层、图例、搜索、位置候选、设置、视角、动捕、巡航、移动端)
|
||||
13. **Docs 文档站** — 当前 Docs 章节保留(权限组说明)
|
||||
|
||||
不再出现:`planet.sh`、`./planet.sh log`、`netsh portproxy`、`source ~/.zshrc && bun run build`、"故障排查顺序"、"开发命令约定"。
|
||||
|
||||
## quickstart.md 重写
|
||||
|
||||
当前 quickstart 假设读者会自己 `git clone` 然后 `./planet.sh start`,这是给开发者看的。改为:
|
||||
|
||||
- 打开管理员给你的 URL
|
||||
- 注册账号 + 邮箱验证
|
||||
- 登录后第一次做什么(建议先到 `/settings?tab=collector_credentials` 配一个 collector,再到 `/ai` 配模型)
|
||||
- 看 Earth
|
||||
|
||||
部署/开发的 quickstart 内容并入 `ops-runbook.md` 的"首次部署"小节,**不**再单独出 `ops-quickstart.md`,避免新增维护点。
|
||||
|
||||
## ops-runbook.md 内容大纲
|
||||
|
||||
抽自现 manual.md,重新组织:
|
||||
|
||||
1. 首次启动 — `./planet.sh start`、默认账号(`admin/admin123`、`linkong/12345678`,引用 `b15d097b` 引入的 `DEFAULT_LOGIN_USERS`)
|
||||
2. 启停与按模块重启 — `start/stop/restart` 及 `-b -f -a -d`
|
||||
3. 健康检查 — `./planet.sh health`
|
||||
4. 日志 — `./planet.sh log` 及 `-f -b -a`,日志文件路径
|
||||
5. 创建用户(CLI 兜底)— `./planet.sh createuser`;说明这是公开注册不可用(SMTP 未配置)时的兜底
|
||||
6. 局域网/WSL 访问 — `--allow-lan`、`netsh portproxy`、防火墙
|
||||
7. AI Provider 环境变量与构建 — `aiprovider/.env`、`~/.zshrc`、`PLANET_LOAD_ZSHRC_ENV`
|
||||
8. 故障排查顺序 — 现 manual 末尾那段,原样搬来
|
||||
9. 开发命令约定 — Bun、`bun run build`、为什么不用 npm
|
||||
|
||||
## documentation-coverage-rules.md 增量
|
||||
|
||||
在现有"覆盖清单"末尾新增一段:
|
||||
|
||||
> **受众分层(强制)**
|
||||
>
|
||||
> - 客户/最终用户能在浏览器里完成的操作 → 只写到 `manual.md` / `quickstart.md`
|
||||
> - 需要 SSH/shell/Docker/`planet.sh`/日志文件路径/端口转发 → 只写到 `ops-runbook.md`(或现有 `ops-*.md`),**禁止**出现在 manual/quickstart
|
||||
> - 同一动作两种入口(如"创建用户"既能 UI 也能 CLI)→ UI 路径写 manual.md,CLI 路径写 ops-runbook.md,互相用一句话相互引用
|
||||
> - 新增客户可见 UI 流 → 同时更新 `manual.md` zh+en 与 `docs-content.ts`
|
||||
> - 新增 ops 命令或脚本 → 只更新 `ops-runbook.md` zh+en
|
||||
|
||||
## .claude/commands/docs.md 增量
|
||||
|
||||
在 "Step 2 — Decide Scope" 后插一段:
|
||||
|
||||
> **Document Audience Routing (Planet)**
|
||||
>
|
||||
> 在 Planet 仓库内,写文档前先判断动作的执行者:
|
||||
>
|
||||
> - 浏览器 UI 用户 → `docs/technical/{zh,en}/manual.md` / `quickstart.md`
|
||||
> - shell/容器/运维 → `docs/technical/{zh,en}/ops-runbook.md` 或现有 `ops-*.md`
|
||||
> - 二次开发者 → 现有 `*-context.md` / `backend-*.md`
|
||||
>
|
||||
> 永远不要把 shell 命令、日志路径、Docker 操作写进 manual/quickstart;永远不要把 UI 截图/按钮路径写进 ops-*。
|
||||
|
||||
## 关键文件清单
|
||||
|
||||
- `docs/technical/zh/manual.md` & `en/manual.md` — 重写
|
||||
- `docs/technical/zh/quickstart.md` & `en/quickstart.md` — 重写
|
||||
- `docs/technical/zh/ops-runbook.md` & `en/ops-runbook.md` *(新)*
|
||||
- `docs/documentation-coverage-rules.md` — 加受众分层段
|
||||
- `.claude/commands/docs.md` — 加 Document Audience Routing 段
|
||||
- `frontend/src/pages/Docs/docs-content.ts` — 注册 `ops-runbook` 到 `DOCS_METADATA`(`docs_admin` 组)
|
||||
|
||||
## 依赖
|
||||
|
||||
manual.md 的"注册账户"和"登录与找回密码"两章需要前后端注册/验证流程已经落地,否则文档会描述不存在的功能。注册功能本身见 [用户公开注册与邮箱验证计划](/home/ray/dev/linkong/planet/docs/plans/user-registration-email-verification-plan.md)。建议先实现注册再重写 manual,避免文档与代码错位。
|
||||
|
||||
## 验证
|
||||
|
||||
- `rg -n 'planet\.sh' docs/technical/zh/manual.md docs/technical/en/manual.md docs/technical/zh/quickstart.md docs/technical/en/quickstart.md` 应该为空
|
||||
- `rg -n '注册账户|register|邮箱验证' docs/technical/zh/manual.md docs/technical/en/manual.md` 应该有命中
|
||||
- `rg -n 'planet\.sh' docs/technical/zh/ops-runbook.md docs/technical/en/ops-runbook.md` 应该有命中
|
||||
- `frontend/src/pages/Docs/docs-content.ts` 中 `ops-runbook` 出现且分组为 `docs_admin`
|
||||
- zh/en manual 章节标题对齐(按 `documentation-coverage-rules.md` 现有要求)
|
||||
- Docs 站点访问:未登录看 manual/quickstart 正常;非 `docs_admin` 用户看不到 `ops-runbook`;`admin` 能看到
|
||||
@@ -176,10 +176,12 @@ freshness:
|
||||
|
||||
## 聚合接口
|
||||
|
||||
状态更新:开发期已直接切换到新船只快照接口。旧 `/api/v1/visualization/geo/vessels` 不再兼容返回数据,而是返回 `410 Gone`;新的 Earth 船只首屏应调用 `/api/v1/vessels/snapshot`,实时更新走 `/ws` 的 `vessels` 订阅。
|
||||
|
||||
现有展示接口应逐步改为消费聚合服务,而不是自己直接拼 `VesselPosition + VesselStatic`。
|
||||
|
||||
```text
|
||||
GET /api/v1/visualization/geo/vessels
|
||||
GET /api/v1/vessels/snapshot?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000
|
||||
GET /api/v1/visualization/vessels/{mmsi}
|
||||
GET /api/v1/visualization/vessels/{mmsi}/track
|
||||
GET /api/v1/visualization/vessels/{mmsi}/conflicts
|
||||
@@ -317,7 +319,7 @@ VesselFinder 等服务里的船只图片不属于 AIS 实时数据本身。图
|
||||
目标是让展示接口开始消费聚合结果,但前端形状保持兼容。
|
||||
|
||||
1. 实现 AIS 聚合服务,先兼容读取现有表,再逐步切换到原始观测层。
|
||||
2. 将 `/geo/vessels` 和 `/vessels/{mmsi}` 改为走聚合服务。
|
||||
2. 将船只列表迁移到 `/api/v1/vessels/snapshot`,并让 `/vessels/{mmsi}` 走聚合服务。
|
||||
3. 将 `/vessels/{mmsi}/track` 改为走轨迹聚合逻辑。
|
||||
4. 返回 `field_sources`、`selected_reasons`、`quality_flags`、`conflict_count`。
|
||||
5. 加入 freshness fallback 和异常位置保护。
|
||||
@@ -342,13 +344,13 @@ VesselFinder 等服务里的船只图片不属于 AIS 实时数据本身。图
|
||||
3. 聚合结果返回 `source_summary`,展示每艘船的来源、观测数量、最新观测时间、传输模式和消息类型。
|
||||
4. 保留 `field_sources` 和 `selected_reasons`,用于解释动态字段来自实时流、静态字段来自可用非空来源。
|
||||
5. 船名标准化会读取 AISStream `MetaData.ShipName`;船型展示会从 `vessel_type_name` 和 AIS 数字 `vessel_type` 共同归一化,保证 marker 颜色、详情卡、hover 和搜索结果一致。
|
||||
6. `/geo/vessels` 不再默认限制 5000 艘;不传 `limit` 或传 `limit=0` 表示全量返回,前端默认也不再二次裁剪到 5000。
|
||||
6. 当前实现已转向 `/api/v1/vessels/snapshot`:必须带 bbox / zoom,默认 `limit=1000`,最大 `limit=5000`,不再支持旧 `/geo/vessels` 全量返回。
|
||||
|
||||
### v3.1 — 聚合完整性修复(v4 前置)
|
||||
### v3.1 — 聚合完整性修复(已被新快照接口取代)
|
||||
|
||||
目标是先保证“所有已采集到的船都能显示”,BarentsWatch 不因为接入 AISStream 而被 raw observation 聚合结果遮蔽。
|
||||
原目标是先保证“所有已采集到的船都能显示”,BarentsWatch 不因为接入 AISStream 而被 raw observation 聚合结果遮蔽。开发期产品尚未上线后,决策调整为直接淘汰 legacy 船只表兜底:船只快照只读取 `ais_raw_observations` 聚合结果,旧 `vessel_position + vessel_static` 不再合并进 `/api/v1/vessels/snapshot`。
|
||||
|
||||
当前风险是 `/geo/vessels` 只要 raw observation 聚合返回非空,就直接使用 raw 聚合结果,不再补读兼容层 `vessel_position + vessel_static`。如果 raw observation 中只存在 AISStream 的几百艘船,或 BarentsWatch 历史数据没有完整回填到 raw 层,最终 Earth 就会只显示 AISStream 子集。
|
||||
因此以下 legacy merge 要求作废,保留在文档中只作为历史决策记录:
|
||||
|
||||
1. `/geo/vessels` 必须合并 raw observation 聚合结果和 legacy latest position 结果。
|
||||
2. raw 与 legacy 同一 MMSI 同时存在时只显示一艘,优先使用 raw 聚合结果及其 `field_sources` / `selected_reasons`。
|
||||
@@ -416,7 +418,7 @@ REST collector 的自然状态是 `fetch -> transform -> save -> progress 0..100
|
||||
- `freshness.realtime_stream_seconds` / `polling_seconds` 必须为非负整数;
|
||||
- `mode=locked` 必须带非空 `locked_source`。
|
||||
4. 聚合服务 `vessel_ais_aggregation.py` 在 `_select_position_observation` 中按 `freshness` 把过期实时流降级到 stale 候选;在 `_select_static_field` 中按 `field_rules.mode = source_priority / locked / newest / non_empty` 选源。
|
||||
5. 聚合输出每条 vessel 携带 `aggregation_strategy_version`,并在 `/geo/vessels` GeoJSON properties + `/vessels/{mmsi}` 详情中暴露。
|
||||
5. 聚合输出每条 vessel 携带 `aggregation_strategy_version`,并在 `/api/v1/vessels/snapshot` GeoJSON properties + `/vessels/{mmsi}` 详情中暴露。
|
||||
6. API:
|
||||
- `GET /api/v1/vessel-aggregation/strategy`
|
||||
- `PUT /api/v1/vessel-aggregation/strategy`(校验失败 400)
|
||||
@@ -460,8 +462,8 @@ REST collector 的自然状态是 `fetch -> transform -> save -> progress 0..100
|
||||
- 明显异常位置不会进入默认展示轨迹,并会留下 `quality_flags`。
|
||||
- 同一时间窗口内多来源相近轨迹点只展示一个点。
|
||||
- AISStream 重连或回放导致的重复消息不会重复进入聚合结果。
|
||||
- raw observation 聚合结果和 legacy latest position 结果会按 MMSI 合并,BarentsWatch-only 船只不会因为 AISStream 子集存在而消失。
|
||||
- 不传 `limit` 或传 `limit=0` 时,`/geo/vessels` 全量返回合并后的船只集合。
|
||||
- `/api/v1/vessels/snapshot` 只读取 AIS raw observation 聚合结果;legacy latest position 不再参与船只快照。
|
||||
- `/api/v1/visualization/geo/vessels` 返回 `410 Gone`,客户端必须迁移到新 snapshot API。
|
||||
- AISStream 长连接收到新船、位置变化和航向变化后,会通过内部 `/ws` 的 `vessels` channel 推送增量。
|
||||
- AISStream streaming 状态不会显示成固定百分比完成进度条,也不会在收到一批消息后误报采集完成。
|
||||
- `mmsi`、`imo`、`callsign` 等身份编号在前端不显示千分位符。
|
||||
|
||||
@@ -216,7 +216,7 @@ hover、locked、dimmed 可通过更新少量 instance attribute 实现,不再
|
||||
|
||||
### 1. 请求视口范围
|
||||
|
||||
前端请求 `/api/v1/visualization/geo/vessels` 时带上当前视口 `bbox`,减少无关船只。
|
||||
前端请求 `/api/v1/vessels/snapshot` 时必须带上当前视口 `bbox`、`zoom` 和受控 `limit`,减少无关船只。旧 `/api/v1/visualization/geo/vessels` 已下线并返回 `410 Gone`。
|
||||
|
||||
### 2. 后端排序策略
|
||||
|
||||
|
||||
155
docs/plans/user-registration-email-verification-plan.md
Normal file
155
docs/plans/user-registration-email-verification-plan.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# 用户公开注册与邮箱验证计划
|
||||
|
||||
**状态**:待实施
|
||||
**创建日期**:2026-05-12
|
||||
**核心目标**:给 Planet 增加公开注册流程 + 邮箱验证码 + 忘记密码,让客户无需管理员介入就能开通账号;同时把 SMTP 邮件作为可复用基础服务接入 `/settings`。
|
||||
|
||||
## 背景
|
||||
|
||||
当前认证只暴露 `/auth/login`、`/auth/refresh`、`/auth/logout`、`/auth/me`(`backend/app/api/v1/auth.py`),账号只能由 `super_admin` 在 `/users` 后台创建。User 模型 `backend/app/models/user.py` 没有 `email_verified` 字段,仓库也没有任何 SMTP/邮件发送基础设施。
|
||||
|
||||
客户旅程想从"打开浏览器→注册→验证→登录"开始走(见 [文档受众分层重构计划](/home/ray/dev/linkong/planet/docs/plans/docs-audience-split-plan.md)),就必须先把这条链路在代码里跑通。
|
||||
|
||||
注册策略(已确认):
|
||||
|
||||
- 开放公开注册,任何人可在 `/register` 自助开通
|
||||
- 默认角色 `viewer`
|
||||
- 邮箱验证后立即可登录(无需管理员审批)
|
||||
- 验证仅走 SMTP 邮件,6 位数字码,10 分钟 TTL
|
||||
|
||||
## 数据模型
|
||||
|
||||
`backend/app/models/user.py` 加两列:
|
||||
|
||||
```python
|
||||
email_verified = Column(Boolean, default=False, nullable=False)
|
||||
pending_email = Column(String(255), nullable=True) # 改邮箱时临时落地待验证地址
|
||||
```
|
||||
|
||||
迁移路径:仓库目前没看到 alembic 目录,沿用 `backend/app/db/session.py` 的初始化风格在启动时跑 `ALTER TABLE users ADD COLUMN IF NOT EXISTS ...`。先确认是否存在 alembic,若有则正规迁移。
|
||||
|
||||
不另建 `verification_codes` 表 — OTP 走 **Redis**(系统已有 Redis,token blacklist 也走 Redis):
|
||||
|
||||
```
|
||||
key: otp:{purpose}:{email} purpose ∈ {register, verify_email, reset_password}
|
||||
value: { code_hash: bcrypt, attempts: int, issued_at: ts }
|
||||
TTL: 600 秒
|
||||
```
|
||||
|
||||
`{purpose}:{email}` 同时配一个限流键 `otp_rate:{purpose}:{email}`,TTL 60 秒,用于"60 秒内禁止重发"。
|
||||
|
||||
## 服务拆分
|
||||
|
||||
按项目 `services/` 单职责风格拆两个:
|
||||
|
||||
**`backend/app/services/otp.py`**(通用 OTP 原语,未来 2FA / 手机号验证可直接复用):
|
||||
|
||||
```python
|
||||
async def issue_code(email: str, purpose: OtpPurpose) -> str # 生成 6 位、写 Redis、返回明码(调用方负责送达)
|
||||
async def verify_code(email: str, purpose: OtpPurpose, code: str) -> bool # 校验并消耗
|
||||
async def check_resend_allowed(email: str, purpose: OtpPurpose) -> None # 抛 RateLimited 异常
|
||||
```
|
||||
|
||||
- 6 位数字,密码学随机
|
||||
- Redis 存 `bcrypt(code)`,不存明码
|
||||
- 校验失败计数 ≥ 5 直接失效该 key
|
||||
- 重发触发即失效旧 code
|
||||
|
||||
**`backend/app/services/email.py`**(通用 SMTP 发送,告警/摘要等后续可复用):
|
||||
|
||||
```python
|
||||
async def send_email(to: str, subject: str, html: str, text: str | None = None) -> None
|
||||
async def send_verification_email(to: str, code: str, purpose: OtpPurpose) -> None # 模板封装
|
||||
```
|
||||
|
||||
- 用 `aiosmtplib` 异步发送
|
||||
- 从 `system_settings` 的 `smtp` 命名空间读配置(host/port/username/password/from/use_tls)
|
||||
- 未配置抛 `EmailNotConfiguredError`
|
||||
- 模板用简单 HTML + 纯文本双段,按 `purpose` 切换文案
|
||||
|
||||
编排("签码 → 发邮件")在 `api/v1/auth.py` 端点里调两个服务,不在 service 内互相调用,保持单测可单独 mock。
|
||||
|
||||
## 后端端点
|
||||
|
||||
新增到 `backend/app/api/v1/auth.py`:
|
||||
|
||||
| 端点 | 入参 | 行为 |
|
||||
| --- | --- | --- |
|
||||
| `POST /auth/register` | `username, email, password` | 用户名/邮箱查重 → 写 User `is_active=True, email_verified=False, role="viewer"` → 调 `otp.issue_code(email, "register")` → 调 `email.send_verification_email` |
|
||||
| `POST /auth/verify-email` | `email, code` | `otp.verify_code` → 置 `email_verified=True` → 直接返回 access/refresh token |
|
||||
| `POST /auth/resend-code` | `email, purpose` | `check_resend_allowed` → `issue_code` → `send_verification_email` |
|
||||
| `POST /auth/forgot-password` | `email` | 即便邮箱不存在也返回 200(防枚举);存在则签 `reset_password` 码并发邮件 |
|
||||
| `POST /auth/reset-password` | `email, code, new_password` | `verify_code(..., "reset_password")` → `user.set_password(new_password)` |
|
||||
|
||||
`/auth/login` 改造:邮箱未验证用户登录返回 `403 { code: "EMAIL_NOT_VERIFIED", email }`,前端拿到后跳验证页。
|
||||
|
||||
## SMTP 设置
|
||||
|
||||
复用 `backend/app/api/v1/settings.py` 现有 setting store,新增 `smtp` 命名空间:
|
||||
|
||||
- `smtp_host`、`smtp_port`、`smtp_username`、`smtp_password`、`smtp_from`、`smtp_from_name`、`smtp_use_tls`
|
||||
- 密码走与 collector 凭证相同的加密路径(看 `backend/app/services/` 是否已有 `credentials_encryption` 之类工具,若有直接复用)
|
||||
- `POST /settings/smtp/test` — 用当前未保存的入参试发一封到指定地址,不落库
|
||||
|
||||
未配置 SMTP 时 `/auth/register` 应返回明确错误 `503 { code: "EMAIL_PROVIDER_NOT_CONFIGURED" }`,提示管理员先去 `/settings` 配 SMTP 或用 `./planet.sh createuser` 兜底。
|
||||
|
||||
## 前端
|
||||
|
||||
**新页面**:
|
||||
|
||||
- `frontend/src/pages/Register/Register.tsx` — 两步表单:(1) 用户名/邮箱/密码 (2) 6 位验证码;60s 重发冷却;验证成功写 token,自动跳 `/admin`
|
||||
- `frontend/src/pages/VerifyEmail/VerifyEmail.tsx` — 给登录拦截 `EMAIL_NOT_VERIFIED` 时落地的页,仅"输码 + 重发"
|
||||
- `frontend/src/pages/ForgotPassword/ForgotPassword.tsx` — 两步:(1) 输邮箱 (2) 输码 + 新密码
|
||||
|
||||
**改动**:
|
||||
|
||||
- `frontend/src/pages/Login/Login.tsx` — 表单下加"注册账号"、"忘记密码"链接;接 `EMAIL_NOT_VERIFIED` 跳 `/verify-email`
|
||||
- `frontend/src/pages/Settings/Settings.tsx` — 新增 SMTP 子 tab(host/port/username/password/from/TLS + 测试发送按钮),用工作区里新建的 `frontend/src/components/ConnectionTestInput/` 做连通测试输入
|
||||
- 路由表(`frontend/src/App.tsx` 或 `frontend/src/router/*`)— 加 `/register`、`/forgot-password`、`/verify-email`
|
||||
|
||||
## 关键文件清单
|
||||
|
||||
后端:
|
||||
|
||||
- `backend/app/models/user.py` — 加字段
|
||||
- `backend/app/schemas/user.py` — 新增 `UserRegister`、`VerifyCode`、`ResetPasswordRequest` schema
|
||||
- `backend/app/api/v1/auth.py` — 新端点 + 登录校验
|
||||
- `backend/app/services/otp.py` *(新)*
|
||||
- `backend/app/services/email.py` *(新)*
|
||||
- `backend/app/api/v1/settings.py` — SMTP 命名空间 + 测试发送
|
||||
- `backend/app/core/config.py` — SMTP 默认值/特性开关
|
||||
- `backend/app/db/session.py` — DDL 兜底(若无 alembic)
|
||||
|
||||
前端:
|
||||
|
||||
- `frontend/src/pages/Register/Register.tsx` *(新)*
|
||||
- `frontend/src/pages/VerifyEmail/VerifyEmail.tsx` *(新)*
|
||||
- `frontend/src/pages/ForgotPassword/ForgotPassword.tsx` *(新)*
|
||||
- `frontend/src/pages/Login/Login.tsx`
|
||||
- `frontend/src/pages/Settings/Settings.tsx`
|
||||
- 路由文件
|
||||
|
||||
## 实施顺序
|
||||
|
||||
1. 后端:User 模型字段 + DDL 兜底
|
||||
2. 后端:`services/otp.py`(先纯单测跑通)
|
||||
3. 后端:`services/email.py`(用 MailHog 本地试发)
|
||||
4. 后端:`/auth/register` + `/auth/verify-email` + `/auth/resend-code` + 登录拦截
|
||||
5. 后端:`/auth/forgot-password` + `/auth/reset-password`
|
||||
6. 后端:`/settings/smtp` 命名空间 + 测试发送
|
||||
7. 前端:`Settings.tsx` 加 SMTP 子 tab
|
||||
8. 前端:Register / VerifyEmail / ForgotPassword 页 + Login 入口
|
||||
|
||||
文档同步在 [文档受众分层重构计划](/home/ray/dev/linkong/planet/docs/plans/docs-audience-split-plan.md) 落地。
|
||||
|
||||
## 验证
|
||||
|
||||
- **后端单测**(仿 `backend/tests/test_settings_ai_provider.py`):
|
||||
- 注册端点用户名/邮箱查重
|
||||
- OTP 过期、错码计数、重发限流
|
||||
- 邮箱未验证用户登录返回 `EMAIL_NOT_VERIFIED`
|
||||
- SMTP 未配置时注册端点返回 `EMAIL_PROVIDER_NOT_CONFIGURED`
|
||||
- 忘记密码对不存在邮箱仍返回 200
|
||||
- **后端集测**:本机起 MailHog 或 Mailtrap,把 SMTP 指到上面,跑 register → 收码 → verify → login 一遍
|
||||
- **前端**:`source ~/.zshrc && bun run build`;启 dev server 走 `/register` → `/verify-email` → `/admin` 全流程,再试 `/forgot-password`
|
||||
- **手测**:新邮箱注册 → 收码 → 输错 → 重发 → 输对 → 登录 → 改密码 → 用新密码再登;管理员在 `/settings` 改 SMTP → 测试发送
|
||||
Reference in New Issue
Block a user