# Navius 后端 Rust + axum,数据用 JSON 文件存储,不依赖数据库。 网站分两种:**系统内置**(只读,随项目发布)和**提交/推送**(可改可删)。 「不可修改」不是靠每处判断,而是靠**结构**保证:`system-*.json` 只在启动时读, 代码里没有任何写这两个文件的路径。 ## 运行 ```bash cd backend cargo run # 默认监听 127.0.0.1:8080,数据目录 ./data cargo test # 34 个单元测试 + 接口测试 cargo clippy --all-targets ``` ### 环境变量 | 变量 | 默认值 | 说明 | | --- | --- | --- | | `NAVIUS_ADDR` | `127.0.0.1:8080` | 监听地址 | | `NAVIUS_DATA_DIR` | `data` | 数据目录 | | `NAVIUS_API_TOKEN` | 未设置 | 推送接口的令牌;不设置时 `/api/sites/push` 不可用(提交接口不受影响) | ## 数据文件 ``` data/ └── nav.json # 站点信息 + 分组结构 + 网站,全在这一份文档里 ``` 侧栏的结构和网站数据放在同一个文件里,删除网站和「它的分组要不要一起清掉」 是同一次写入完成的——不会出现网站没了、侧栏还挂着一个空分组的中间状态。 写盘用「先写临时文件再改名」,不会因为写到一半断电把文件写坏。 ```json { "site": { "name": "Suxing Nav", "searchHint": "…", "submitUrl": "https://mattermost…" }, "groups": [ { "id": "common", "title": "常用推荐", "source": "system", "order": 10, "createdAt": "…", "updatedAt": "…", "sites": [ { "id": "github", "title": "GitHub", "url": "…", "source": "system", … } ], "children": [] } ] } ``` 启动时会自检,任何一条不通过就直接退出(避免带着坏数据继续跑): - `site.name` 不能为空; - 分组 id、网站 id 不能重复(两者共用一个 id 空间); - 分组最多两级; - **内置分组下只能放内置网站,用户分组下只能放用户网站**。 ### 内置与用户数据怎么隔离 以前是「系统文件只读、用户文件可写」,现在合并成一个文件,隔离改成靠 `source`: 写操作只会碰 `source != system` 的分组和网站,内置的那部分只会被原样读出来再写回去。 启动自检会拦住「手改 JSON 把内置网站标成用户来源」这类越权。 ## 接口 ### `GET /api/health` ```json { "sites": 5, "systemSites": 5, "userSites": 0, "categories": 8 } ``` ### `GET /api/nav` 侧栏 + 内容区需要的那棵树。每个网站上都带一个推导出来的 `editable` (`source != "system"`),前端据此决定要不要显示编辑/删除入口。 ```json { "site": { "name": "Suxing Nav", "searchHint": "输入关键字搜索全网资源..." }, "groups": [ { "id": "common", "title": "常用推荐", "source": "system", "editable": false, "sites": [ { "id": "github", "title": "GitHub", "url": "https://github.com", "description": "代码托管与开源社区", "categoryId": "common", "source": "system", "order": 10, "createdAt": "2026-09-13T10:29:48Z", "updatedAt": "2026-09-13T10:29:48Z", "submittedBy": "", "editable": false } ], "children": [] } ] } ``` `site` 来自 `data/site.json`:站点名和搜索框提示都由后端提供,前端不再写死。 ### `POST /api/sites` — 用户提交 **不接受 `categoryId`**,只接受分组名;后端只在「用户创建的分组」里查找或新建, 所以提交的内容不可能落进内置分组。 ```json { "title": "Hugging Face", "url": "https://huggingface.co", "description": "开源模型与数据集社区", "group": "AI 工具", "subGroup": "模型社区", "submittedBy": "an@example.com" } ``` 成功返回 201 和创建出来的条目。同一父级下同名的用户分组会**复用**, 不会每提交一次就多一个同名分组。 ### `POST /api/sites/push` — 接口推送 入参同上,但需要请求头 `X-Api-Token: `, 创建出来的条目 `source` 记为 `api`(id 用 `a-` 前缀)。 ### `PATCH /api/sites/{id}` — 修改 只改传进来的字段,可传 `title` / `url` / `description` / `group` / `subGroup`。 系统内置的条目返回 403;传 `group` 时同样只能在用户分组里找或建。 ### `DELETE /api/sites/{id}` — 删除 系统内置的条目返回 403,删掉了返回 204。 ### 错误响应 统一是 `{"error": "中文说明"}`: | 状态码 | 场景 | | --- | --- | | 400 | 入参不合法(标题为空/超长、url 非法或指向内网、缺 group 等) | | 403 | 改删系统内置条目、推送令牌不对 | | 404 | 找不到条目 | | 500 | 写盘失败、推送接口未配置令牌 | ## 字段长度限制 标题 60 字、描述 120 字、分组名 30 字、`submittedBy` 120 字,都按**字符**算 (中文一个字算一个)。集中在 `src/validate.rs` 顶部,改规则只改那一处。 ## 模块 ``` src/ ├── main.rs # 启动:读数据 → 自检 → 起 HTTP ├── api.rs # 路由、令牌校验、把仓库错误翻译成状态码 ├── store.rs # 内存态仓库:增删改、构建导航树、原子写盘 ├── model.rs # 落盘结构、请求入参、对外返回结构 ├── validate.rs # 校验与归一化 ├── error.rs # ApiError(→ HTTP)与 StoreError(→ 启动失败) └── util.rs # 时间格式化与 id 生成 ``` ## 已知取舍 - **跨域是全开的**(`CorsLayer::permissive`)。前端是 Flutter Web,开发时不同源; 上线后如果只从自己的域名访问,应改成只允许那个来源。 - **提交接口没有限流**。按需求「不需要审核、提交即上线」,公开写接口建议后面补 一层限流或简单的验证码。 - **`Cargo.lock` 目前在 `.gitignore` 里**。这是可执行程序,建议把它纳入版本管理, 保证每台机器装到的依赖版本一致。 - 前端目前还在用本地 mock 数据,接这个接口是下一步。