
简介这套系统是一套支持私有部署的云端存储双链笔记软件完整源码包同时集成个人博客功能可跨桌面端、网页端及移动端使用适合看重数据隐私与知识关联的开发者、博主或小型团队用于自托管部署。资源包内共包含1023个文件整体体积约23.74MB。其中545个Java源文件对应后端服务与接口实现102个Vue文件与78个TypeScript文件构成前端界面与类型系统另有SCSS样式表、Markdown说明文档、配置文件等目录结构完整。从资源预览可知项目内已整理接口文档与模块聚合说明便于快速上手同时包含错误码表、搜索脚本等模块方便理解业务逻辑。目前已吸引361人学习浏览。通过这套资源既能学习双链笔记的链接结构设计、实时同步与私有云存储的工程实现也能参考其前后端分离架构直接部署或定制扩展从而搭建个性化的个人知识管理与博客平台。1. 私有部署双链笔记的架构边界与选型判断把数据主权握在自己手里同时保留多端编辑和对外发布能力是这类私有部署双链笔记软件最核心的吸引力。在 Windows 和 Mac 桌面端编辑的内容会完整落在你指定的服务器或 NAS 目录网页客户端、网页移动端通过浏览器访问同一份数据被标记为发布状态的笔记以个人博客形式公开输出。对 IT 从业者来说私有部署带来的首要收益不只是隐私——自建服务器同样要打补丁、做快照——而是数据的可迁移性和生命周期可控。选型阶段要提前想清楚三件事正文数据以什么格式落地、多端之间用哪条链路保持一致、博客部分走静态导出还是服务端渲染。下面按这三个方向展开。2. 云端存储的数据组织与私有化部署最小方案从数据目录到 Docker 编排2.1 目录即数据库双链笔记的数据文件为什么适合落在文件系统上标题里的“云端存储”加上“私有部署”实际上把存储层拆成了两个角色。第一个角色是编辑器直接读写的本地工作目录第二个角色是服务端统一管理的内容目录。如果数据全部只写进数据库双链笔记中高频发生的小片段变更会带来大量随机写入把正文按块分散在文件系统里则可以借助普通文件快照、rsync 和后续迁移工具做备份恢复。这类软件的常见做法是在数据目录下按文档 ID 或按时间切分目录每个目录内部保存块的纯文本内容、属性与索引文件同时保留一个 Markdown 导出区给用户直接读取。存储选型直接影响“能否在不运行服务端的情况下直接读数据”。这里有一个容易被忽视的原则凡是内容能用文本编辑器打开的部分优先用文件系统凡是关系型查询密集的部分交给数据库凡是临时状态放进缓存。存储层次典型载体承担任务故障影响范围块级数据目录本地文件系统、NAS 挂载卷承载双链笔记的正文块、属性与版本差异损坏需从备份恢复建议开启每日快照关系型数据库SQLite 或 PostgreSQL管理文档树、标签、引用索引与发布状态损坏影响查询能力多数实现可重建索引缓存层Redis登录会话、最近访问列表、搜索热数据丢失后重建即可不影响正文提到 Redis 需要提醒一句在 Windows 上运行 Redis 服务不是官方支持场景社区移植版可用但稳定做法是在 WSL2 或 Docker Desktop 里跑 Linux 容器如果只是单机部署笔记服务完全不引入 Redis 也能通过文件缓存运行只是活跃会话和搜索热数据会落到本地磁盘。2.2 Docker Compose 一键拉起云存储服务正文、索引与缓存的分层部署准备好云服务器或 NAS 后最常见的落地方式是用一个 compose 文件把应用服务、正文存储和缓存进程放到同一条网络里。以下编排解决的是“最小可用”问题不引入额外的权限系统和服务发现适合单机私有部署。version: 3.8 services: notes-server: # 镜像地址需要替换成你所选笔记服务的最新发行版 image: notes/self-hosted:latest container_name: notes-server restart: unless-stopped ports: - 6806:6806 environment: - DB_TYPEsqlite - CACHE_TYPEredis - REDIS_ADDRnotes-cache:6379 volumes: - ./data:/opt/notes/data # 双链笔记的正文与索引存储 - ./backup:/opt/notes/backup # 定时导出的归档目录 depends_on: - notes-cache notes-cache: image: redis:7-alpine container_name: notes-cache restart: unless-stopped command: redis-server --appendonly yes --maxmemory 384mb --maxmemory-policy allkeys-lru volumes: - ./cache:/data执行docker compose up -d后数据卷与容器进程分离这几处参数值得逐条确认./data挂载的是包含正文、块结构、属性索引在内的完整数据目录升级镜像或重建容器时数据不丢notes-cache用 Redis 承载搜索热数据和会话状态将易失数据与磁盘写入分开restart: unless-stopped避免服务器重启后需要人工再拉起主服务暴露6806端口后续通过 Caddy 或 Nginx 反代对外开放。如果服务器内存小于 512MBRedis 的maxmemory-policy allkeys-lru会频繁驱逐缓存项导致二次打开笔记速度变慢但不会破坏块数据在这种硬件条件下把CACHE_TYPE改为sqlite也能跑只是并发访问时表现稍差。另一个常见问题是 Docker Desktop 的路径挂载Windows 上容器路径解析对中文目录支持不好将项目目录统一放在C:\apps\notes这类纯英文路径可以绕开一半以上的权限和路径报错。Mac 上安装 Docker 后需要确认设置里的“文件共享”包含了项目所在目录否则启动容器时会报挂载失败。提示升级笔记服务镜像前先在./backup目录手动触发一次完整导出。容器重建失败时这套备份可以直接导入新实例避免在排错过程中反复试错。2.3 反向代理与 WebSocket 透传网页端访问与连接稳定性设置服务端跑起来后网页客户端和移动端通过 HTTP 访问。真实对外提供服务时不建议直接暴露 6806 端口而是用 Nginx 加一层 HTTPS 反向代理。双链编辑器的保存过程通常依赖 WebSocket 推送增量变更因此 Nginx 必须转发Upgrade头。server { listen 80; server_name notes.example.com; location / { proxy_pass http://127.0.0.1:6806; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_read_timeout 3600s; } }proxy_set_header Upgrade和Connection upgrade让 WebSocket 长连接能穿透反向代理proxy_read_timeout 3600s防止长时间未操作的网页端连接被服务端切断如果按“仅内网可用”的方式部署可以直接在安全组限制来源 IP 段省略反向代理这一层。部署完成的验证方式很直接浏览器打开首页登录后创建一个带双链语法的临时笔记保存、刷新、再打开确认块内容和反向链接都存在就说明正文存储和网络链路都是通的。若是在局域网 NAS 上部署则先确认 NAS 的防火墙规则允许来自桌面端和手机所在网段的入站连接。3. Windows、Mac、网页客户端与移动端的多端接入配置多端接入的难度不在安装包本身而在每一端连接服务端时的会话策略、本地缓存与权限差异。桌面客户端通常按“本地工作目录 服务端同步”的模式运行网页端则依赖浏览器中的会话与缓存。3.1 Windows 桌面端最小环境与数据目录指定在 Windows 桌面端安装后首次启动会要求设置工作空间目录。凡是笔记正文和附件所在的位置我都建议放在非系统盘并且单独建一层目录结构D:\notes\ ├─ workspace\ # 当前编辑中的文档数据 └─ vault\ # 对外可读的 Markdown 导出目录启动前确认两件事该目录可写服务端地址可达。如果 Windows 安全日志中持续出现进程访问被拒绝的记录通常是“受控文件夹访问”拦住了程序写入目录将其加入允许列表即可不必关闭整个安全防护。若你是通过 Windows 的 Docker Desktop 做本地测试记得先执行docker compose ps确认容器状态Docker Desktop 最常见的异常是默认 WSL2 内核未更新运行wsl --update后问题通常能解决。这里还要注意客户端的更新策略不要盲目跟随最新版先看更新日志里是否改了数据目录结构。双链笔记从老版本升级到新版本时如果新版本改了数据库索引格式旧客户端阅读新索引会报错反过来新客户端读取旧索引一般会触发自动迁移。因此 Windows 桌面端和网页端尽量保持同一版本线迁移前先在服务端做一次完整导出。3.2 Mac 桌面端权限处理与安装链路Mac 端的安装包一般为 dmg拖入“应用程序”后首次启动会遇到 Gatekeeper 拦截。在系统设置 → 隐私与安全性中允许该应用或右键点击应用图标选择“打开”这一步和 typora mac 等桌面应用的安装路径基本一致。如果启动后无法读取文稿目录需要在隐私与安全性里赋予“完全磁盘访问权限”否则双链笔记中依赖文件拖拽的附件功能会失效。在 Apple Silicon 上应优先选择 arm64 构建若误装了 x86_64 版本可以在简介中勾选“使用 Rosetta 打开”但内存占用会高一层。mac 安装 homebrew 报错的问题如果反弹到编译依赖大概率与 Xcode Command Line Tools 版本不匹配有关执行xcode-select --install之后基本能绕过。Mac 端还有一个容易被忽略的路径如果你用 Spotlight 索引笔记目录大量小块文件的元数据刷新会拉高 CPU 占用。把笔记工作目录加入 Spotlight 的排除列表界面的滚动手感会明显改善。3.3 网页客户端与移动端的同域访问与会话保持网页端和移动端浏览器访问同一个域名。要点是保持 Cookie 的SameSiteLax与 Token 过期策略一致否则移动端 Safari 会出现“登录过期但桌面端正常”的假象。另一个常见场景是弱网环境 WebSocket 长连接会被运营商层静默回收需要客户端有心跳。// 移动端最小心跳检测每 30 秒确认长连接存活 const ws new WebSocket(wss://notes.example.com/ws); const timer setInterval(() { if (ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ type: ping, ts: Date.now() })); } }, 30000); ws.onclose () { clearInterval(timer); // 长连接断开时把编辑器切到 HTTP 增量提交模式 syncViaHttp(); };这段逻辑的意图是ping消息让移动网络下的中间链路识别到连接处于活跃状态避免空闲连接被回收onclose触发回退逻辑保证用户在电梯或地下车库等场景下保存的内容不丢。实际生产环境中如果发现移动端经常丢保存优先检查是否配了 HTTPS 证书链再检查 Nginx 的proxy_read_timeout。网页移动端的离线浏览能力一般依赖 LocalStorage 或 IndexedDB 缓存最近打开的块。当浏览器提示存储不足时清掉的往往是图片附件缓存正文块不受影响下次联网重新打开笔记即可补齐。4. 双链引用、反向链接与全文检索的实现要点4.1 双链标记的存储结构与块级引用规则双链笔记和普通 Markdown 链接的核心差异在于引用不是锁在文件名上而是锁在一个全局唯一的块 ID 上。拿最简例子说当在 A 笔记中写下((20240701-abcdef))这个 ID 对应 B 笔记的某个段落B 段落前后插入新内容不影响 A 笔记里的引用。反观路径式链接一旦原文件改名或移动目录位置旧链接就失效了。数据落地时块 ID 通常直接写进正文文件或属性区不同实现对格式的取舍不同。引用方式引用粒度目标重命名后迁移成本路径链接整个文档需要回写所有来源随目录搬迁整体破碎块 ID 引用段落、列表项、代码块无需处理服务端自动解析重建路径块 ID 若当作纯字符串直接暴露给用户重新排版时代价很高成熟方案都在数据库层保存“块 ID → 文档 ID → 路径”的映射。实施时可以把每个块当作一个文档片段来处理文档导入导出时这个映射表也要同步迁移。很多从主流通用笔记工具迁过来的人会问“为什么导出 Markdown 后全是((xxxx))这种怪串”答案就在这里这是双链笔记的原始表达阅读器需要先经过一次链接改写才能变成可读的 Markdown。4.2 反向链接与关系图渲染从数据表到前端力导向图反向链接的本质是一张引用边表。每次保存时后台解析新增的双链标记并写入 refs 表当用户打开某个块的上下文菜单时前端发出查询请求拉取所有引用该块的文档。-- 查询引用了指定块 ID 的全部文档及其来源块 SELECT d.id, d.title, r.source_block_id FROM refs r JOIN documents d ON d.id r.document_id WHERE r.target_block_id 20240701-abcdef ORDER BY d.updated_at DESC LIMIT 50;参数说明refs表保存的是“从哪个文档的哪个块引用到哪个目标块”的边target_block_id是目标块的 IDORDER BY d.updated_at DESC保证最近更新的引用来源排在最前LIMIT 50控制反链面板首屏渲染数量避免一次拉取几千条边导致白屏。如果这个查询在数据量超过 10 万行时明显变慢多为target_block_id列缺少索引补一个普通 B-tree 索引即可降到几十毫秒。关系图则把文档和块当作节点引用边当作连线在前端用 Canvas 或 SVG 渲染。参数调节上斥力常取 200~400引力 0.1 比较稳文档总数超过 500 时把引力降到 0.05布局才能拉开层次。渲染性能的瓶颈不在节点数而在边的数量——一次性绘制几千条边必然卡可靠策略是先按连通分量裁剪只渲染当前视图范围内的子图。4.3 全文检索与相关度排序BM25 在笔记检索中的调参要点内容积累得越多越依赖全文检索。这类系统一般在启动时把正文块写入 FTS 索引并保留文档 ID、块 ID、标题三个字段参与排序。检索时通过 BM25 打分完成相关度排序。-- FTS5 检索包含目标词的块按文档聚合显示 SELECT document_id, snippet(notes_fts, 1, [, ], …, 12) AS snippet, bm25(notes_fts, 10.0, 5.0) AS score FROM notes_fts WHERE notes_fts MATCH 私有部署 ORDER BY score DESC LIMIT 30;参数说明bm25(notes_fts, 10.0, 5.0)中两个数字分别对应标题列和正文列的权重命中标题的记录会获得更高相关度snippet函数截取命中位置前后 12 个词用于搜索结果摘要MATCH 私有部署表示按不带 OR 的精确短语匹配避免四字词被拆散。FTS5 默认对中文按单字切分搜索“私有部署”不会匹配“私有化部署”的变体表达。解决思路有两种一是配置带中文分词的扩展词库二是把同义词表挂到查询层。中小规模笔记库用同义词表更省事在查询前把“私有部署”扩展成“私有部署 OR 私有化部署”就能在不动索引结构的前提下覆盖变体词。从实际使用经验看标题未命中但正文命中的文档若总排在前列说明标题列的权重偏小把标题权重调到正文的 1.5~2 倍搜索结果会更贴近真实意图。5. 博客发布功能从私有笔记到公网输出5.1 发布标记、内容过滤与链接改写规则双链笔记里的内容并非全部适合对外发布手动复制粘贴又不现实。常见做法是给笔记加一个“发布”属性发布管线只导出带该属性的文档同时过滤掉含草稿、会议记录等标签的内容。导出时还要把块引用统一改写成普通 Markdown 链接((20240701-abcdef))这类 ID 串读者看不懂正确做法是解析映射表后生成指向对应页面的相对路径遇到目标未发布的引用则直接丢弃并记入日志。5.2 定制首屏与部署技巧博客部分采用纯静态方案时每次发布都是一次全量构建。笔记数量少还好积累到千篇以上全量构建的时间会明显拖慢更新节奏。我一般会把构建拆成两步只处理 24 小时内有变更的文档。# 第一步从服务端导出最近有变更的 Markdown 文件 ./publish export --since 24h --output ./output # 第二步用静态站点生成器构建博客 cd ./output blog-builder build --draftsfalse--since 24h控制导出时间窗口避免全量扫描--draftsfalse过滤掉草稿状态内容确保未完成文章不会被发布。自动化部署可以把它挂进 CI 的定时任务配合 Git 提交触发构建。博客与笔记之间形成闭环后发布长文的成本会低到“加个标记、跑两条命令”的程度写作频率自然就上来了。本文还有配套的精品资源点击获取