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

Navius 后端

Rust + axum,数据用 JSON 文件存储,不依赖数据库。

网站分两种:系统内置(只读,随项目发布)和提交/推送(可改可删)。 「不可修改」不是靠每处判断,而是靠结构保证:system-*.json 只在启动时读, 代码里没有任何写这两个文件的路径。

运行

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                 # 站点信息 + 分组结构 + 网站,全在这一份文档里

侧栏的结构和网站数据放在同一个文件里,删除网站和「它的分组要不要一起清掉」 是同一次写入完成的——不会出现网站没了、侧栏还挂着一个空分组的中间状态。 写盘用「先写临时文件再改名」,不会因为写到一半断电把文件写坏。

{
  "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

{ "sites": 5, "systemSites": 5, "userSites": 0, "categories": 8 }

GET /api/nav

侧栏 + 内容区需要的那棵树。每个网站上都带一个推导出来的 editable (source != "system"),前端据此决定要不要显示编辑/删除入口。

{
  "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,只接受分组名;后端只在「用户创建的分组」里查找或新建, 所以提交的内容不可能落进内置分组。

{
  "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 数据,接这个接口是下一步。