Files
pure-note/docs/decisions.md
T

9.0 KiB
Raw Blame History

实施决策记录

本文档记录按 docs/design.md(v1.1)实施过程中的全部自行决策与偏差说明。 原则:不偏离设计主线;凡设计未明确或环境受限处,选择最小侵入方案并在此留档。 日期:2026-09-08

1. 环境与依赖

# 决策 依据
D1 Go 工具链使用 1.26.6(设计写 1.27.1) 本机已装版本。所用特性(1.22+ ServeMux 方法/通配符路由、slog、go:embed)全部满足,无升级必要
D2 依赖版本按设计锁定值解析:modernc.org/sqlite v1.58.0、goldmark v1.8.6、bluemonday v1.0.27、x/crypto v0.56.0、x/time v0.15.0 设计附录 A「实际开发时以 go mod tidy 解析到的版本为准」——解析结果与设计核实值完全一致
D3 新增第 6 个直接依赖 golang.org/x/term v0.45.0 pn init 交互式口令输入需要关闭回显。设计 §2.2 预算「直接依赖 ≤6(Go 侧 5 个)」,6 个在预算内;PN_ADMIN_PASSWORD 环境变量仍是非交互主通道
D4 前端 TypeScript 用 5.9(设计允许 7.x 或回退 5.9) 直接采用设计声明的保守回退路径,规避第三方类型兼容风险(CodeMirror/react-markdown 生态)
D5 前端其余依赖与设计一致:react 19.2.8、vite 8.2.2、tailwind 4.3.3、react-router 8.3.1、@tanstack/react-query 5.102.8、react-markdown 10.1.0 等,npm 解析 0 漏洞 npm install 实测

2. 后端行为细化

# 决策 依据
D6 backup / gc 子命令不执行迁移、不做 user_version 守卫(只有 serve/init 迁移) 维护命令应是纯数据操作:在陌生(更新)schema 上跑迁移反而危险。设计 §10.4 的守卫语境是「启动服务」
D7 init 在口令已存在时拒绝并提示走管理界面改密(无 --force) 防止误操作覆盖口令;单管理员场景下改密有专门界面(§7.1 POST /api/admin/password)
D8 GET /api/site 公开端点(返回 site_title/site_desc/page_size 白名单三键) 设计 §8.1 首页要显示「站点标题」,但 §7.1 公共 API 无设置出口;/api/admin/settings 需要会话。新增白名单只读端点,永不含 admin_password_hash(测试守护)
D9 GET /api/notes/{slug} 响应含 prev/next(相邻公开笔记 slug+title) §8.1 详情页要求「上一篇/下一篇」;放在详情响应里避免前端额外请求与分页边界问题。排序与公开列表一致(updated_at DESC, id DESC)
D10 RSS description = 全文服务端渲染(goldmark→bluemonday),条目上限 50;pubDate 用 RFC 822(RFC1123Z) §7.5 要求服务端渲染;全量 description 是博客 RSS 常规做法。注意从 ListPublicNotesFull 取(列表查询不含全文,曾因此修复空 description 缺陷)
D11 管理列表 GET /api/admin/notes 不分页,一次返回全部正常笔记 设计未定义该端点分页参数;§11 边界 1 万篇内无压力。公开列表 GET /api/notes 保持分页
D12 Origin/Referer 校验严格模式:非 GET/HEAD/OPTIONS 请求若 Origin 与 Referer 均缺失也拒绝(403) 浏览器跨站/同站 fetch 都会带 Origin;缺失意味着非浏览器伪造。设计 T2「所有非 GET 请求」从严解释。测试覆盖 login/logout/admin 三处
D13 登录限流 fail-only 实现:预检(桶空 → 429+Retry-After,跳过 Argon2)+ 失败时消耗两维度令牌;per-IP 10次/5min + per-账号 5次/10min;改密复用同一组限流器 §7.2/§7.3。「预检不消费」保证成功登录不计费;429 时 Retry-After 取两维度较大值
D14 全局限流参数取 20 req/s、burst 40/桶上限 4096、TTL 10 分钟 设计只说「宽松令牌桶」,此参数对个人博客足够宽松
D15 image_refs 重建按设计正则 /api/images/(\d+) 全量扫描去重;悬空引用(图片不存在)忽略 §6.2 原文
D16 图片上传双重校验:客户端 Content-Type 白名单 → 魔数(PNG/JPEG/WebP/GIF,SVG 拒绝)→ PNG/JPEG/GIF 再过 stdlib 解码(拦截截断流);WebP 仅魔数(stdlib 不支持) §7.4「不信任客户端声明」。多一层解码校验属纵深,不改变接口语义
D17 摘要为空时服务端自动生成:Markdown→HTML→剥标签→压空白→截 200 字符 §6.1 summary「可空则截取正文」
D18 slug 生成:ASCII 部分小写连字符化;结果为空才退化为 post-YYYYMMDD;唯一性查重含回收站,冲突自动 -2/-3;POST 创建忽略客户端 slug 一律自动生成;PUT 允许手改(格式校验 + 排除自身查重,冲突 409 + field:"slug") §8.2。创建路径「自动生成 + 自解冲突」,手改冲突走 409——与「首次保存即定稿」一致:新建后前端立即拿到真实 slug
D19 回收站中的笔记对所有公开出口(含管理员经 /api/notes/{slug})一律 404,仅 /api/admin/trash 可见 §13 判定规则「回收站仅经 /api/admin/trash」(管理员私有预览只适用于未删除的私有笔记,§1.3)
D20 设置 PUT 用 DisallowUnknownFields 严格白名单:未知键 400(含试图写 admin_password_hash) §9.3「PUT 无法写入非白名单键」
D21 改密成功不失效当前会话 §7.3-7 明示(单管理员仅本会话)

3. 前端

# 决策 依据
D22 未使用 shadcn CLI / Radix 原语:按 shadcn「复制式组件」理念手写所需的最小组件集(按钮/输入/开关/模态/布局),样式走 Tailwind 类 无头环境下 CLI 交互链路风险高、收益低;设计定位是「复制式组件零运行时」,手写与 CLI 产物等价且依赖面更小。设计「按需叠加 Radix 原语」——本项目需求内为零需要
D23 index.html 内嵌 Go template 占位符 {{.Title}} 等;vite 原样保留,服务端渲染时经 html/template 自动转义;字段由服务端保证非空(站点默认回退) §8.3-4。副作用:vite dev 直连时标签页标题显示占位符原文,纯开发态外观问题
D24 rehype-sanitize 在 GitHub 默认 schema 上扩展:input 补 ['checked', true](默认 schema 已含 type=checkbox/disabled)、code/span 追加 hljs* className 白名单;a 组件强制 target=_blank rel="nofollow noopener noreferrer" §8.2 渲染管线。schema 快照测试守护「禁 script/iframe/style/事件属性」
D25 新建笔记首次手动保存时才 POST 创建(自动保存仅对已存在笔记生效),避免半空草稿泛滥 §8.2 自动保存防抖 2s 的安全解释;「草稿即私有」不受影响
D26 编辑器工具栏提供加粗/斜体/链接/代码/表格插入;图片粘贴/拖拽上传后插入 ![name](/api/images/{id}) §8.2

4. 测试与验收

# 决策 依据
D27 §13 各测试组全部落为 Go 集成测试(httptest + 临时目录真实 SQLite):可见性矩阵(主体 × 状态 × 出口表驱动)、迁移守卫、登录/改密/会话轮换、CSRF、上传、回收站+gc、slug、设置白名单;webui 用 fstest.MapFS 单测 meta 转义/缓存头/fallback 设计「其他测试组」要求 httptest + 临时目录真实 SQLite
D28 构建冒烟 = scripts/smoke.sh(make smoke):真实构建 → init → serve → 断言 SPA script/link 200 + 正确 MIME、meta 注入、安全头、私有不可见 §8.3-6「真实构建 → 启动二进制 → 请求任一公开 slug 页面」;比 Go test 内嵌前端产物更贴近 CI 语义
D29 测试注入限流器:httpapi.NewWithLimiters 允许测试替换高容量桶;登录限流测试单独用真实参数构造器 避免全局限流 429 干扰矩阵测试,同时保留限流本身的专项测试(两全)
D30 夹具图片用「合法 PNG + IEND 后差异化尾部」绕开 sha256 去重合并——去重本身另用同字节上传断言 实测发现同字节图片被去重合并为同一行(正确行为,§14 已预告「去重会合并」),矩阵需要四张不同图

5. 其他

# 决策 依据
D31 CSP v1 采用设计原样(style-src 含 'unsafe-inline',CodeMirror style-mod 所需);nonce 硬化按设计留 M3+ 待办,未实施 §9.2/§14
D32 安全头(含 HSTS)对全部响应统一下发 §9.2 为全局中间件;HTTP 开发模式下 HSTS 无副作用
D33 webui 资产缓存:assets/(内容 hash 命名)immutable 一年;其他静态文件 1h;index.html no-cache;缺失资产 404 不回退 HTML §8.3-5 + 防止把 JS 404 伪装成 SPA 页面造成误判
D34 无内嵌 index.html(M0 占位)时 HTML 路径返回 503 占位说明页 部署缺产物属于配置错误,503 比 200 空页更诚实
D35 robots.txt 额外 Disallow: /admin §7.1 仅要求「允许全部 + sitemap 指向」;Disallow /admin 是 SEO 常规增强,不构成功能偏离