R1 背景与目标
CafeDaily 由开源项目 brew-guide(coffee.chu3.top,作者 Chu3)品牌重构而来,是一款面向手冲与精品咖啡的冲煮伴侣:分段冲煮计时、咖啡豆(豆仓)管理与赏味期、冲煮方案参数、品鉴风味评分与统计。产品以中文界面为主。
历史架构是 local-first(本地优先):数据先存本地 IndexedDB,启动时由 dataLayer.initializeDataLayer() 把各 Zustand store 从本地水合,再异步与云端同步。本轮重构对这一架构作出明确方向变更——转为 server-authoritative(服务端权威):
移除本地存储作为数据源。PostgreSQL 是唯一事实来源(single source of truth),前端不再把 IndexedDB 当作主数据存储;所有业务读写都经 Express API 落库。客户端仅保留极小的会话态与缓存(内存缓存、SWR 式请求缓存、鉴权 token),它们不是权威数据。同时移除 / 弃用遗留 Supabase 同步层与本地优先的双向同步。
本轮重构有两个并行目标,二者都不改动核心业务逻辑:
- UI/UX 视觉重构。当前界面全部硬编码 Tailwind
neutral-*灰阶(4572 处、散布 179 个文件),零语义 token,导致明暗主题、品牌辉光与状态色无法统一演进。目标是引入「冲煮实验台 / Brew Lab」设计系统,用语义 token 收敛视觉,让「读数」成为界面主角。 - 数据架构演进。从「本地优先 + Supabase 同步」转为「服务端权威」:自建后端(Node.js 20 + Express 4,位于
~/cafedaily-docker/backend)+ PostgreSQL 16 成为唯一后端与事实来源,前端退化为纯客户端消费者,摆脱第三方 BaaS 依赖、掌握数据主权。
重构不是重写。前端仍是 Next.js 16(App Router、Turbopack)+ React 19 + TypeScript,静态导出(output: 'export')为 PWA,并通过 Capacitor(iOS/Android)与 Tauri(桌面)多端分发。设计系统与数据架构演进都必须落在既有代码骨架内。这一转向有明确取舍:弱网 / 离线体验下降,需要以请求缓存、乐观更新、断网提示与失败重试来缓解(见 R8、R9、R11)。
R2 范围 Scope
纳入范围(In-scope)
- 设计系统层:定义语义 token、Brew Lab 色板与字体系统,替换硬编码
neutral-*,统一明暗双主题。 - 组件视觉一致性:计时刻度盘、豆仓卡片、方案参数、品鉴风味、统计图表在同一套刻度语言下重绘。
- 数据架构转向:把所有业务读写从「本地 IndexedDB 优先」改为「经 Express API(
/api/*)落 PostgreSQL 16」,前端不再持有权威数据。 - 客户端缓存层:以请求级缓存(SWR / 内存缓存)、乐观更新、断网提示与失败重试,替代原本由本地优先提供的即时性与离线韧性。
- 迁移与下线:移除本地存储作为数据源,弃用 / 下线遗留 Supabase 同步(见 R8)。
- 自建后端接入:Express API 的鉴权、多租户资源读写、分页与过滤。
- 无障碍与响应式:对比度、键盘可达、
prefers-reduced-motion、360px 无横向溢出。
不在本轮范围(Out-of-scope)
- 不重写业务逻辑:冲煮算法、赏味期计算、评分模型保持现状。
- 不引入新的核心功能域(如社交 / 电商 / AI 推荐),仅收敛既有能力的呈现与存储。
- 不做 UI 框架迁移:仍用 Tailwind + framer-motion + gsap,不换栈。
- 本轮不实现「只读离线缓存」等完整离线方案,仅在缓存与降级层面缓解弱网(列为未来可选,见 R11)。
- 本轮不实现多语言 i18n 的完整落地,仅保证架构不阻塞(中文优先,见 R9)。
视觉重构以「零业务回归」为红线:任何 token 替换都不得改变计算结果或业务语义。数据架构转向以「服务端为唯一事实来源」为红线:任何客户端缓存都不得被当作可信持久层,断网期间的本地暂存仅用于乐观 UI 与重试队列,且须在恢复后与服务端对账。
R3 用户与场景
核心用户是手冲 / 精品咖啡爱好者——他们关心「这杯为什么好喝,下次怎么复现」。产品要闭合一条完整的冲煮记录环:
- 冲煮前 · 选豆选方案。从豆仓挑一支处于赏味期的豆子,套用一个冲煮方案(粉量、粉水比、水温、研磨度、分段注水时间轴)。
- 冲煮中 · 跟着刻度盘走。注水刻度盘按分段(闷蒸 / 注水 / 滴滤)计时,实时显示已注水量、目标量与流速。
- 冲煮后 · 记录风味。用风味雷达(酸质 / 甜感 / 苦度 / 醇厚度)与总评打分,写下品鉴笔记,可选记录 TDS 与萃取率。
- 长期 · 统计与迭代。回看统计,发现「哪支豆、哪个参数、哪个水温」稳定产出高分,形成自己的配方库。
使用场景常在弱网或短时离线(厨房、户外、通勤记录)发生。转向服务端权威后,这些场景的默认体验会下降——读写依赖网络。冲煮计时本身是纯客户端计算、不受影响;但「保存记录、拉取豆仓」等操作在断网时需要乐观更新与重试队列兜底,并向用户如实提示网络状态(见 R8、R9)。
R4 功能需求 Functional
下列需求沿用产品既有能力,本轮以新设计系统重绘、并统一接入服务端权威的 API 数据层。优先级:MUST 必须 · SHOULD 应当 · COULD 可选。
taste_acidity / sweetness / bitterness / body 与总评 rating,配风味雷达与风味标签。MUSTuser_id 隔离。MUSTequipment)与磨豆机(grinder),供方案与记录引用,读写经 API。COULDbean_images / note_images),上传至后端挂载卷。COULDR5 设计系统需求 Design System
这是本轮最核心的前端视觉需求:把 4572 处硬编码 neutral-*(散布 179 个文件)替换为语义 token,使明暗主题与状态色可从单一来源演进。
语义 token(取代原始灰阶)
- 背景与表面:
--bg/--surface/--surface-2。 - 文字层级:
--ink/--ink-2/--ink-dim。 - 分隔线:
--line/--line-strong。强调:--accent/--accent-ink。
Brew Lab 色板(语义即状态)
颜色对应萃取的真实状态,而非情绪装饰:
#D2914A:数据与强调的读数辉光,仅在一处发力。强调#6F7D53:新鲜 / 萃取不足 / 正向状态。正向#9E3B2A:陈化 / 萃取过度 / 风险状态。风险字体系统
.display(Fraunces 衬线):仅少量标题,克制出场。.data(Space Mono 等宽):所有数字、代码、标签、读数,赋予产品仪表口吻。- 正文用系统中文栈(PingFang / Noto Sans SC),保证中文可读性。
主题与无障碍
- 明暗双主题:默认
<html class="dark">,由 next-themes(class 策略)驱动;评审站的切换由app.js接管。 - 对比度:正文与背景须满足 WCAG AA(正文 ≥ 4.5:1,大字 ≥ 3:1)。
- 键盘可达:所有交互元素可聚焦,
:focus-visible由全局样式统一处理。 - 动效收敛:尊重
prefers-reduced-motion,优先用 CSS;脚本需检测matchMedia。 - 状态可见:加载 / 空态 / 断网 / 重试等网络态需有统一的视觉表达(骨架、提示条、状态色),因为服务端权威下这些态更频繁地暴露给用户。
采用「token 先行 + 分批替换」:先在设计系统层定义并冻结 token,再按功能模块逐文件替换 neutral-*,每批以视觉快照回归验证。禁止一次性全局 sed 替换以免破坏边缘用例。
R6 数据模型 Data Model
后端为 PostgreSQL 16(Docker postgres:16-alpine),启用 pgcrypto,主键为 gen_random_uuid(),时间戳用 TIMESTAMPTZ,updated_at 由触发器自动更新。核心业务表以 deleted_at 软删除,并通过 user_id … ON DELETE CASCADE 做多租户隔离。转向服务端权威后,此库是唯一事实来源,无客户端持久副本作为权威。
| 表 | 关键字段 | 关系 / 说明 |
|---|---|---|
| users | id, email, password_hash, username | 账号主体。email 唯一索引;密码经 bcrypt 存储。 |
| sessions | token_hash, expires_at, revoked | 会话表存在,但当前 JWT 无状态、未使用(见 R11)。 |
| coffee_beans | origin, region, estate, variety, process, roast_level, roast_date, capacity, remaining, price, notes | 豆仓。user_id 外键;软删除 deleted_at;部分索引 WHERE deleted_at IS NULL。 |
| brewing_methods | params (JSONB), name | 冲煮方案。params 建 GIN 索引,支持参数检索。 |
| brewing_notes | taste_acidity, sweetness, bitterness, body, rating, brew_date | 品鉴笔记。软删除;brew_date 建索引。 |
| flavor_ratings | note_id, 风味维度值 | 关联 note_id(属于笔记,而非咖啡豆)。 |
| equipment | user_id, 器具属性 | 器具 / 自定义器具。 |
| grinders | user_id, 磨豆机属性 | 磨豆机档位供方案引用。 |
| bean_images / note_images | 父 id, url | 豆 / 笔记附图,文件落至后端挂载卷 ./backend/uploads。 |
索引覆盖齐全:email、按 user_id 的部分索引(WHERE deleted_at IS NULL)、brew_date、以及 params 的 GIN 索引。
R7 API 需求 Endpoints
后端为 Express 4(CommonJS),监听端口 3001,路由挂在 /api/*。所有业务数据只能经这些端点读写——它们是前端唯一的数据入口。资源路由以 authenticate 中间件保护,按 user_id 做多租户隔离;列表统一分页 page / limit(≤100)/ offset。
| 方法 · 路径 | 说明 | 鉴权 |
|---|---|---|
| GET /api/health | 健康检查。 | 公开 |
| POST /api/auth/register | 注册,bcrypt 哈希(SALT_ROUNDS=12)。 | 公开 |
| POST /api/auth/login | 登录,签发 JWT(HS256,7d)。 | 公开 |
| GET /api/auth/me | 返回当前用户信息。 | Bearer |
| GET /api/beans | 豆仓列表;支持 search 与 favorite 过滤 + 分页。 | Bearer |
| POST /api/beans | 新建咖啡豆。 | Bearer |
| PUT /api/beans/:id | 更新,按 user_id 校验归属。 | Bearer |
| DEL /api/beans/:id | 软删除(置 deleted_at)。 | Bearer |
| GET /api/notes | 品鉴笔记列表 + 分页。 | Bearer |
| GET /api/methods | 冲煮方案,支持 params 检索。 | Bearer |
| GET /api/equipment | 器具列表。 | Bearer |
| GET /api/grinders | 磨豆机列表。 | Bearer |
| POST /api/ai/recognize | 多模态识别代理:接图片/文本,转发到 OpenAI 兼容视觉模型(默认 doubao-seed-2.0-mini),返回结构化 JSON。密钥仅在服务端。 | Bearer · 限额 |
| POST /api/ai/chat | 冲煮助手对话代理:情境感知建议、方案生成、人格归纳。 | Bearer · 限额 |
| POST /api/caffeine/recognize | 连锁咖啡识别:识别品牌(星巴克/瑞幸/M Stand/Nowwa/Manner/皮爷等)与品类,返回估算咖啡因(mg)。走 /api/ai 视觉模型。 | Bearer · 限额 |
| POST /api/caffeine/log | 记录今日一杯(品牌/品类/咖啡因估值/心情/时间),写入 PG。 | Bearer |
| GET /api/caffeine/today | 今日摄入汇总、体内残留曲线(半衰期≈5h)与警戒状态。 | Bearer |
认证约定:JWT payload 含 sub / email / username / role,有效期 7 天;前端存 localStorage 键 cafedaily_token(此为客户端仅保留的鉴权态之一),请求头 Authorization: Bearer <token>。apiClient 在 401 时清除 token 并派发 auth:unauthorized 事件。中间件提供 authenticate / optionalAuth / requireAdmin / requireRole / requireOwnership。
R7.5 AI 代理与 RBAC
AI 能力(后端代理)
本轮 AI 能力(多模态入库、OCR、冲煮助手对话、人格归纳、粒径/水质分析)统一走后端代理:新增 /api/ai/* 端点,由 Express 服务端持有 API 密钥并转发到 OpenAI 兼容的视觉/对话模型(默认 doubao-seed-2.0-mini,国内可用)。这在既有 src/lib/api/beanRecognition.ts(当前为客户端 BYOK:/chat/completions + Bearer + 可配 base URL/model/prompt)之上演进,把密钥从客户端收归服务端。
/api/ai/*,密钥仅存服务端环境变量。MUSTmethodRecognitionPrompt,服务端封装为可版本化的 prompt。SHOULD/api/ai/* 端点尚未实现,是本轮新增架构面;当前仅有客户端 BYOK 版识别。旧 qwen-vl-max 模型已弃用。
RBAC(基于角色的访问控制)
三种角色:爱好者 管理者。角色写入 JWT role,由 requireRole / requireAdmin 强制校验。
users 表尚无 role 列,需迁移补列并回填默认 hobbyist;中间件 requireRole/requireAdmin/requireOwnership 已就绪但缺数据来源——属已知漂移(见 R11)。
R8 数据架构策略 Data Strategy
本章取代原「本地优先 + Supabase 同步」策略,确立 服务端权威(server-authoritative) 为本轮数据架构的基线。
/api/* → PG;禁止把 IndexedDB / localStorage 当作业务数据持久层。MUSTsrc/lib/supabase(遗留 23 文件,含 realtime)与本地优先双向同步;dataLayer 改为薄的 API 客户端封装。MUST转向服务端权威后,原本「离线完整可用」的能力不再成立:断网时写操作只能排队、读操作只能命中缓存或失败。这是本轮明确接受的取舍,换取数据主权与一致性。缓解手段是 D-03~D-05(请求缓存 / 乐观更新 / 断网提示 / 重试),彻底离线可用则留待 D-07 的只读缓存或未来重新评估(见 R11 · GAP-05)。
R9 非功能需求 NFR
性能
- 静态导出(
output: 'export')+ Workbox SW 预缓存应用外壳(app shell);但业务数据首屏依赖网络请求,须以骨架屏与请求缓存优化感知延迟。 - 请求级缓存与去重(SWR 式):避免重复拉取、支持后台重新验证,降低服务端往返对交互的影响。
- 关键读数(计时 / 重量)以
requestAnimationFrame更新,等宽tabular-nums防抖动;计时为纯客户端计算,不受网络影响。
网络韧性(取代原「离线 / 本地优先」)
- 弱网降级:读走缓存兜底、写走乐观更新 + 重试队列;断网有明确提示(对应 R8 · D-03~D-05、F-08)。
- 请求超时与重试:设置合理超时与指数退避重试;避免请求风暴与重复写入(幂等考量)。
- 一致性:恢复网络后以服务端为准对账,回滚失败的乐观态,保证用户看到的最终状态与库一致。
安全 / 可用性 / 可维护性
- 安全见 R10;可用性满足 WCAG AA、键盘可达、reduced-motion(见 R5)。
- 可维护性:语义 token 单一来源、组件复用,把 4572 处散点收敛为可治理的设计系统;数据层收敛为单一 API 客户端,去除本地优先与 Supabase 的双路径复杂度。
国际化(i18n)
- 中文优先:UI 文案以中文为主,句子化、主动语态。
- 架构不阻塞未来多语言:文案与组件解耦,数字 / 单位用
.data等宽体呈现。
R10 安全需求 Security
- 密码:bcryptjs 哈希,
SALT_ROUNDS = 12,绝不明文或可逆存储。 - 令牌:JWT(HS256),payload
sub / email / username / role,有效期 7d。 - 多租户隔离:资源路由用
authenticate保护,一律按user_id过滤,禁止越权读写。服务端权威使隔离更关键——它是唯一的访问控制关口。 - CORS:限定来源
https://cafedaily.top。 - helmet:设置安全响应头。反向代理由系统 nginx 承担,
/api/代理到 Dockercafedaily-api:3001。 - 传输:Let's Encrypt / certbot(webroot
/var/www/certbot)自动续期 TLS。
JWT_SECRET 存在不安全默认值 'default-secret-change-in-production';token 存 localStorage(XSS 风险面,服务端权威下 token 是唯一凭证,风险更集中);当前无 refresh token、无令牌吊销、无速率限制。详见 R11。
R11 已知缺口与技术债 Known Gaps
以下为真实存在、尚未端到端联调完成的问题,如实记录以保证文档可信度,并作为后续 roadmap 输入。
beans.js 读写 roastery / is_favorite / purchase_date / tasting_notes / description 等列,但 01-schema.sql 的 coffee_beans 并无这些列(实际有 origin / region / estate / variety / process / roast_level / roast_date / capacity / remaining / price / notes)。
此外 flavor_ratings 关联 note_id,但 beans.js 用 bean_id 查它;bean_images 无 sort_order 列却按其排序。结论:后端与 schema 尚未对齐,明显未端到端联调。转向服务端权威后这一缺口更紧迫——API 是唯一数据路径,路由必须先对齐 schema 才能上线。
JWT payload 含 role、/api/auth/me 选取 avatar_url,但 users 表既无 role 也无 avatar_url 列 —— 需补列或加迁移脚本,否则相关查询会失败。
sessions 表(token_hash / expires_at / revoked)已存在,但当前 JWT 为无状态、并未使用该表。需二选一:实现刷新令牌 / 吊销机制,或删除该表以免误导。
JWT_SECRET 有不安全默认值 'default-secret-change-in-production';token 存 localStorage(XSS 可窃取);无 refresh token、无速率限制。需强制注入密钥、评估存储位置并补齐限流。
由「本地优先」转「服务端权威」后,原本的完整离线可用不再成立——断网时读可能失败、写只能排队。本轮不承诺离线可用,仅以请求缓存、乐观更新、断网提示与失败重试缓解(R8 · D-03~D-05)。需实现并验证这套降级链路;「只读离线缓存」(D-07)作为未来可选项,待评估必要性后再排期。
前端仍带 local-first 数据层与 Supabase 遗留(src/lib/supabase,23 文件,含 realtime)。需系统性移除本地权威存储与双写路径、把 dataLayer 收敛为 API 客户端,并确保下线过程中的历史本地数据有一次性迁移到服务端的路径(否则用户既有本地数据可能丢失)。
R12 验收标准 Acceptance
- 设计系统:
neutral-*硬编码降至 0(或迁移到明确白名单),明暗主题切换无失色、无对比度回退;视觉快照回归通过。 - 零业务回归:数据层与 token 替换后,冲煮计时、赏味期计算、评分与统计结果与重构前一致。
- 无障碍:正文对比度满足 WCAG AA,全流程键盘可操作,reduced-motion 下无强制动画,360px 无横向溢出。
- 服务端权威落地:所有业务读写经
/api/*→ PG;代码库中不再存在把 IndexedDB / Supabase 当作业务数据源的路径;dataLayer为纯 API 客户端。 - 后端对齐:GAP-01 / GAP-02 修复——路由列名与 schema 一致、
flavor_ratings按note_id关联、users补齐role / avatar_url;/api/beans增删改查端到端联调通过。 - 安全基线:
JWT_SECRET强制由环境注入、无默认值;CORS 限定生产域;关键写接口具备基础限流。 - 弱网降级:断网 / 请求失败时有明确提示;写操作乐观更新 + 重试并在恢复后对账成功、无重复或丢失;缓存命中的读取可被后台重新验证。
- 迁移安全:Supabase 与本地优先下线过程有一次性把历史本地数据迁移到服务端的路径,且不丢数据。