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

217 lines
8.7 KiB
Markdown
Raw 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.
# 后端数据结构设计:网站与分组
> 本文是设计稿。实现时按「侧栏结构和网站绑定到一个文件、删除时同步清理」调整过,
> 现状见 [backend/README.md](../backend/README.md)。
## 1. 需求与边界
- 后端是 Rust,**数据用 JSON 文件存储**,不引入数据库。
- 网站分两类:
- **系统内置**:随项目发布,只读,只有项目维护者能改。
- **可修改**:用户提交、或第三方通过接口推送进来的,能改、能删。
- **不需要审核**:提交即上线。
- **图标沿用当前逻辑**:前端取名称首字渲染,后端不存图标。
- **不统计访问量**。
- **提交的网站不能放进内置分组**:提交时必须自带分组名(或分组 + 子分组名),
后端只在「用户创建的分组」里找或建;内置分组只有项目维护者自己维护。
## 2. 一个网站条目的完整结构
系统内置的一条(`index3.html` 里的 GitHub):
```json
{
"id": "github",
"title": "GitHub",
"url": "https://github.com",
"description": "代码托管与开源社区",
"categoryId": "common",
"source": "system",
"order": 10,
"createdAt": "2026-09-13T10:29:48+08:00",
"updatedAt": "2026-09-13T10:29:48+08:00",
"submittedBy": ""
}
```
用户提交的一条:
```json
{
"id": "s-20260919-8f3a1c",
"title": "Hugging Face",
"url": "https://huggingface.co",
"description": "开源模型与数据集社区",
"categoryId": "g-20260919-4d21ab",
"source": "submission",
"order": 0,
"createdAt": "2026-09-19T14:02:11+08:00",
"updatedAt": "2026-09-19T14:02:11+08:00",
"submittedBy": "[email protected]"
}
```
## 3. 字段说明
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string | 是 | 唯一标识,同时是接口路径里的 id。命名规则见 3.1。 |
| `title` | string | 是 | 显示名称。前端取它的首字当图标。上限 60 字符。 |
| `url` | string | 是 | 跳转地址,必须是可解析的 `http` / `https`。 |
| `description` | string | 否 | 一句话描述,卡片副标题。上限 120 字符。 |
| `categoryId` | string | 是 | 所属分组或子分组的 id。**必须指向用户创建的分组**(见第 5 节)。 |
| `source` | enum | 是 | `system` / `submission` / `api`。它同时决定这条能不能被修改。 |
| `order` | int | 否 | 同一分组内的排序值,小的在前,默认 0。内置条目靠它固定顺序,提交进来的默认追加到末尾。 |
| `createdAt` | string | 是 | 创建时间,ISO 8601 带时区。 |
| `updatedAt` | string | 是 | 最后修改时间,ISO 8601 带时区。 |
| `submittedBy` | string | 否 | 提交来源标识:用户提交放邮箱或用户名,接口推送放调用方标识,内置为空。 |
### 3.1 id 命名规则
| 来源 | 规则 | 例子 |
| --- | --- | --- |
| 系统内置网站 | 手写,语义化,小写字母 + 数字 + `-` | `github`、`can-i-use` |
| 用户提交网站 | 后端生成:`s-` + 日期 + 随机后缀 | `s-20260919-8f3a1c` |
| 接口推送网站 | 后端生成:`a-` + 日期 + 随机后缀 | `a-20260919-2b77d0` |
| 用户创建分组 | 后端生成:`g-` + 日期 + 随机后缀 | `g-20260919-4d21ab` |
前缀分开是为了**一眼看出这条是谁写的**,也从生成规则上杜绝和内置 id 撞车。
### 3.2 source:唯一决定「能不能改」的字段
```rust
pub enum SiteSource {
System, // 系统内置,只读
Submission, // 用户提交
Api, // 接口推送
}
```
- 只有 `source == "system"` 的条目不可修改:修改、删除接口一律返回 403。
- 不单独存 `editable` 字段,它由 `source` 推导。存两份状态迟早不一致,
比如手改 JSON 把 `source` 改成 `system`、`editable` 却还是 `true`。
- 访客接口返回时附带派生的 `editable` 给前端用,不落盘。
## 4. 提交时为什么必须自带分组
分组也是同样的两套:
```json
{
"id": "materials",
"title": "素材资源",
"parentId": "",
"source": "system",
"order": 20,
"createdAt": "2026-09-13T10:29:48+08:00",
"updatedAt": "2026-09-13T10:29:48+08:00"
}
```
```json
{
"id": "design",
"title": "UI 设计",
"parentId": "materials",
"source": "system",
"order": 10,
"createdAt": "2026-09-13T10:29:48+08:00",
"updatedAt": "2026-09-13T10:29:48+08:00"
}
```
- `parentId` 为空 = 一级分组;不为空 = 子分组,指向它所属的一级分组。
- 层级上限两级,和侧栏的结构一致(`index3.html` 就是两级)。
- `source == "system"` 的分组只看不改,由项目维护者手写 JSON 维护。
- `source != "system"` 的分组只可能由提交产生。
### 4.1 提交接口的入参
提交方**不能指定 `categoryId`**,只能给分组名:
```json
{
"title": "Hugging Face",
"url": "https://huggingface.co",
"description": "开源模型与数据集社区",
"group": "AI 工具",
"subGroup": "模型社区",
"submittedBy": "[email protected]"
}
```
后端按这个顺序处理:
1. 在**用户创建的分组**里按名字找一级分组 `AI 工具`;找不到就新建一个
(`source = "submission"`,id 自动生成)。
2. 如果带了 `subGroup`,在 `AI 工具` 下面按名字找子分组;找不到就新建。
3. 新建网站条目,`categoryId` 指向解析出来的子分组(没带 `subGroup` 就指向一级分组)。
规则上:
- 内置分组**永远不参与**第 1、2 步的查找,所以提交的内容不可能落进内置分组。
- 同一个父级下同名的用户分组**复用**,不会每提交一次就多出一个同名分组。
如果希望「每次提交都新建」,把查找那一步去掉即可。
- 修改接口同样不允许把已有条目挪进内置分组,判定标准和创建时一致。
## 5. 两种数据如何隔离
```
backend/data/
└── nav.json # 站点信息 + 分组结构(含网站),一份文档
```
- 读:启动时加载进内存,之后都从内存读,不再碰磁盘。
- 写:整份文档一起写回,但改的只会是 `source != system` 的那部分。
- 网站直接挂在分组的 `sites` 里,不再自己记 `categoryId`——
归属由位置表达,删掉分组不会留下指向空处的引用。
- 删除网站时,如果它所在的用户分组空了,会在**同一次写入**里一起清掉。
- JSON 用 2 空格缩进、保留中文原文,方便直接看 diff。
### 5.1 启动时的自检
启动时校验一遍,不通过就直接启动失败(防止手改 JSON 越权):
- `system-*.json` 里的条目 `source` 必须全是 `system`;
`user-*.json` 里必须全不是。
- 每个网站的 `categoryId` 必须存在,并且**分组来源要和网站来源一致**:
内置网站只能挂在系统分组下,提交的网站只能挂在用户分组下。
- 分组 `parentId` 指向的分组必须存在,且不能出现两级以上、不能自己指向自己。
## 6. 校验规则
写入(用户提交 / 接口推送)时逐条校验,任何一条不过就整体拒绝并返回原因:
| 规则 | 说明 |
| --- | --- |
| `id` 唯一 | 与已有网站都不冲突 |
| `title` 非空、≤ 60 字符 | 去掉首尾空白后判断 |
| `url` 合法 | 可解析,协议必须是 `http` / `https`,且不能是 localhost 与内网地址 |
| `description` ≤ 120 字符 | 允许为空 |
| `group` 非空、≤ 30 字符 | 没有它就没地方放,直接拒绝 |
| `subGroup` ≤ 30 字符 | 可选;给了就必须和 `group` 一起解析成两级 |
| `source` 只能是 `submission` / `api` | 外部写入不允许声称自己是 `system` |
## 7. 和前端现有模型的关系
前端 `LinkItem` 目前用到 `id` / `title` / `url` / `description` 四个字段,
本次结构是它的超集,前端不改也能读出来;`categoryId`、`source`、`editable`
等字段等前端要用时再接。
字段名统一用 **camelCase**,和前端 Dart 的 `fromJson` 对齐,前端不用做名字转换;
Rust 侧用 `#[serde(rename_all = "camelCase")]` 映射。
**新加字段一律带 `#[serde(default)]`**,这样以后往结构里加字段,老 JSON 仍然能读,
不需要写迁移脚本。
## 8. 这次没有放进结构里的东西
按「先保留当前逻辑」处理,以下都不存;以后要用再加字段(加字段不需要迁移):
| 没放的 | 原因 | 以后要加时 |
| --- | --- | --- |
| `icon` | 前端当前是取 `title` 首字渲染 | 加 `icon` 字段,存 favicon 地址或 emoji |
| `tags` | 当前侧栏和卡片都没有标签 | 加 `tags: []`,提交接口再放开这个入参 |
| `visits` | 不统计访问量 | 要么由后端跳转接口累加,要么接第三方统计 |
| `status` | 不需要审核,提交即上线 | 加 `status` 字段并给默认值 `published` |