Files
Navius/backend/README.md
CNWei 039ea70edf feat: 实现导航页基础功能
- 添加导航条目和分类数据模型
- 实现导航卡片网格展示
- 支持点击卡片打开链接
- 添加主题样式(渐变色、卡片阴影)
- 区分系统默认和自定义条目
- 添加分类统计信息"
2026-09-19 23:17:36 +08:00

169 lines
6.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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": "[email protected]"
}
```
成功返回 201 和创建出来的条目。同一父级下同名的用户分组会**复用**,
不会每提交一次就多一个同名分组。
### `POST /api/sites/push` — 接口推送
入参同上,但需要请求头 `X-Api-Token: <NAVIUS_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 数据,接这个接口是下一步。