Skip to content

一、项目概览 (Overview)

1.1 项目定位与设计哲学

TodoApp 是一款面向个人用户的跨平台待办事项应用,同时支持 Windows 11 桌面端与 Android 移动端,并通过自建后端服务实现多端数据同步。

定位:小而美

这个项目不追求功能的堆砌,而是在一个清晰的边界内把核心体验做到极致:创建待办、设置优先级与截止时间、标记完成、多端同步。没有看板、没有项目管理、没有协作功能。功能少,但每一个功能都经过完整的工程设计,包括离线支持、实时同步。

哲学:离线优先,而非云优先

大多数 To-Do 应用的设计假设用户永远在线,一旦断网,应用就会显示加载失败或直接拒绝操作。TodoApp 反其道而行之——本地数据永远是第一公民,所有操作优先写入本地数据库,云端同步是后台静默进行的附加能力。

这个选择来自一个朴素的认知:待办事项是高频、轻量的操作,用户不应该因为地铁里没信号就无法记录一个想法。


1.2 功能清单

待办管理

  • 创建待办,支持标题、详细备注、优先级(高/中/低)、截止时间四个维度
  • 编辑和删除待办
  • 一键标记完成 / 撤销完成
  • 点击待办卡片查看详情弹窗(含编辑、删除入口)

多维度筛选与排序

  • 按状态筛选:全部 / 待处理 / 已完成
  • 按优先级筛选:优先处理(高优先级)
  • 关键字实时搜索
  • 多维排序:按优先级、截止时间、创建时间

账号体系

  • 邮箱注册与登录
  • 忘记密码:发送验证码至邮箱,重置密码
  • 访客模式:不登录也可正常使用,登录后可将本地数据迁移至账号

多端实时同步

  • 登录同一账号,Win11 端与 Android 端数据实时同步
  • 任意一端的增、改、删操作,几秒内在其他端自动刷新
  • Client-ID 过滤机制:发起变更的设备不会收到冗余的同步推送

离线支持

  • 断网状态下可正常增删改查,操作全部写入本地数据库
  • 重新联网后自动将离线期间的操作按序同步至服务器
  • 同步队列(Outbox Pattern)保证操作不丢失、不重复

到期提醒

  • 距截止时间 15 分钟时推送临期通知
  • 到达截止时间时推送逾期通知
  • Windows 端使用系统托盘通知,Android 端使用系统推送通知

日志与调试

  • 应用内日志查看器(Win11 弹窗 / Android 内置页面),支持按级别过滤
  • 后端日志 Web 界面(/logs/),支持关键字搜索与级别过滤
  • 全链路 Correlation ID,一个 ID 串联前端操作、后端处理、SSE 广播三段日志

1.3 技术栈全景图

总览

层级技术用途
前端 UIFlutter (Dart)跨平台界面与业务逻辑
本地数据库SQLite + Drift本地数据持久化与离线队列
状态管理setState + Stream轻量响应式 UI 更新
网络层HTTP + SSEREST 写入 + 实时推送接收
后端框架Dart ShelfREST API 与 SSE 服务
云端数据库PostgreSQL多端共享数据存储
鉴权机制JWT无状态身份认证
反向代理NginxHTTPS 终止与请求转发
容器化Docker + Docker Compose后端服务部署与管理
日志(前端)talker结构化日志与 UI 查看器
日志(后端)自研 ServerLogger文件持久化与 Web 查看接口

技术详解

Flutter (Dart) · 前端框架

Flutter 是 Google 开源的跨平台 UI 框架,使用 Dart 语言编写。与 React Native 不同,Flutter 不依赖平台原生组件,而是自带一套渲染引擎(基于 Skia/Impeller)直接在画布上绘制每一个像素。这带来两个核心优势:

  1. 像素级一致性:在 Windows、Android、iOS 上渲染出来的界面完全一致,不会出现平台差异导致的样式错乱。
  2. 高性能:Flutter 的渲染管线针对 60fps/120fps 优化,列表滚动和动画极为流畅。

Dart 是一门强类型、面向对象的语言,语法接近 Java/Kotlin,上手容易。它的 async/awaitStream 机制让异步编程写起来像同步代码一样直观。

本项目用 Flutter 3.x 开发,同一套代码编译出 Windows .exe 和 Android .apk 两个产物,核心业务逻辑零重复。


SQLite + Drift · 本地数据库

SQLite 是一个嵌入式关系型数据库,整个数据库是本地的一个 .sqlite 文件,无需安装服务端进程,开箱即用。它是全球使用量最大的数据库,每部 Android/iOS 手机、每个浏览器内部都嵌入了 SQLite。

在 Flutter 中直接使用 SQLite 需要手写 SQL 语句,容易出错且难以维护。Drift(原名 Moor)是 Flutter 生态中最成熟的 SQLite ORM(对象关系映射)框架,它的特点是:

  • 类型安全:表结构用 Dart 类定义,查询条件在编译时检查类型,不会出现运行时 SQL 拼接错误
  • 响应式查询watchAll() 返回 Stream,数据变更时 UI 自动更新,不需要手动刷新
  • 代码生成:表结构定义好后,Drift 自动生成增删改查的样板代码

本项目的数据库包含三张表:todos(待办数据)、sync_queue(离线操作队列)、kv_store(键值元数据,如最后同步时间)。


HTTP + SSE · 网络通信

项目采用两种通信方式的组合,各司其职:

HTTP REST(主动请求)

REST 是目前最主流的 Web API 设计规范,基于 HTTP 协议的标准方法(GET/POST/PATCH/DELETE)来表达对资源的操作。本项目所有的登录、增删改查操作都通过 REST 接口完成。REST 的特点是请求-响应模型:客户端发起请求,服务端返回结果,一次交互结束。

SSE(Server-Sent Events,服务端推送事件)

SSE 是 HTTP/1.1 标准中定义的一种服务端向客户端单向推送数据的机制。客户端建立一个长连接后保持不断开,服务端可以随时通过这条连接向客户端发送文本消息。

与 WebSocket 相比,SSE 的优势是:

  • 更简单:基于普通 HTTP,不需要协议升级,Nginx 等代理天然支持(需关闭缓冲)
  • 天然断线重连:浏览器和 HTTP 客户端库内置了断线重连逻辑
  • 够用:本项目的实时通知是单向的(服务端 → 客户端),不需要 WebSocket 的双向能力

本项目的使用场景:当某个设备修改了待办,后端更新数据库后,通过 SSE 向该用户的所有其他在线设备广播一个 todos_updated 事件,接收端收到后触发增量同步拉取最新数据。


Dart Shelf · 后端框架

Shelf 是 Dart 官方维护的轻量级 HTTP 服务器框架,类似 Node.js 生态中的 Express。它的核心概念非常简单:

  • Handler:一个接收 Request、返回 Response 的函数
  • Middleware:包裹在 Handler 外层的拦截器,用于实现认证、日志、CORS 等横切逻辑
  • Pipeline:将多个 Middleware 串联起来,请求依次经过每一层

选择 Dart Shelf 而非 Node.js/Go 的核心原因是前后端同语言(都是 Dart),数据模型、工具函数可以直接复用,降低学习负担。

本项目的后端直接使用 Dart 原生的 HttpServer(而非通过 shelf_io 包装)来处理 SSE 连接,原因详见第五章踩坑实录。


PostgreSQL · 云端数据库

PostgreSQL(简称 PG)是功能最完善的开源关系型数据库,以稳定性、标准兼容性和扩展性著称,被广泛用于生产环境。

相比 MySQL,PostgreSQL 在 JSON 支持、全文搜索、并发控制等高级特性上更强;相比 SQLite,PostgreSQL 是独立的服务端进程,支持多客户端并发连接,适合作为多端共享的云端数据中心。

本项目中,PostgreSQL 运行在 Docker 容器里,后端通过连接池与之通信。待办数据、用户账号、密码重置验证码都存储在 PG 中。


JWT · 鉴权机制

JWT(JSON Web Token) 是一种无状态的身份认证方案。它的工作原理是:

  1. 用户登录成功后,服务端生成一个经过签名的 Token 字符串,返回给客户端
  2. 客户端将 Token 存储在本地,此后每次请求都在 HTTP Header 中携带:Authorization: Bearer <token>
  3. 服务端收到请求后,验证 Token 签名是否合法、是否过期,即可确认请求者身份,无需查数据库

与传统 Session 方案(服务端存储登录状态)相比,JWT 的优势是无状态——服务端不需要维护任何会话数据,天然支持水平扩展。

本项目使用双 Token 机制

  • access_token:有效期 1 小时,用于日常接口鉴权
  • refresh_token:有效期 30 天,仅用于换取新的 access_token

当 access_token 过期时,客户端自动用 refresh_token 静默刷新,用户无感知。


Nginx · 反向代理

Nginx 是全球使用最广泛的 Web 服务器和反向代理,以极低的内存占用和极高的并发处理能力著称。

在本项目中,Nginx 承担三个职责:

  1. HTTPS 终止:对外暴露 443 端口,处理 TLS 加密握手,与 Let's Encrypt 配合实现免费 SSL 证书自动续期。后端 Dart 服务只处理明文 HTTP,不需要关心 SSL。
  2. 反向代理:将外部请求转发到内网的 Dart 服务(127.0.0.1:8080),后端端口不直接对外暴露,更安全。
  3. SSE 专属配置:为 /events/ 路径关闭 proxy_buffering,确保服务端推送的数据能实时到达客户端,不被 Nginx 缓冲积压。

Docker + Docker Compose · 容器化部署

Docker 是目前最流行的容器化技术。容器可以理解为一个轻量的虚拟机——它打包了应用代码及其所有依赖(运行时、库、配置),无论在哪台机器上运行,行为完全一致。"在我电脑上能跑"的问题从此消失。

Docker Compose 是 Docker 官方提供的多容器编排工具,用一个 docker-compose.yml 文件描述所有服务(后端 API + PostgreSQL)的配置、网络、挂载目录,一条命令(docker compose up -d)即可启动整套后端环境。

本项目的后端由两个容器组成:

  • todo_api:Dart Shelf 后端服务,编译为原生二进制运行
  • todo_db:PostgreSQL 数据库,数据目录挂载到宿主机持久化

talker · 前端日志

talker 是 Flutter 生态中功能最完善的日志框架,本项目选用它的核心原因是它内置了一个可以直接嵌入 App 的日志查看界面(TalkerScreen),在 Android 上无需连接电脑也能实时查看、过滤日志。

talker 支持多种输出目标(控制台、文件、自定义),本项目配置了同时写入控制台和本地日志文件,日志文件按天滚动,保留 7 天。


自研 ServerLogger · 后端日志

后端日志没有引入第三方框架,而是用约 100 行 Dart 代码实现了一个轻量的结构化日志工具,功能包括:

  • 统一格式:时间戳 [级别] [标签] 消息
  • 写入文件:按天生成日志文件,保留 7 天,挂载到宿主机目录
  • Web 查看:GET /logs/ 接口返回 HTML 日志页面,支持按级别过滤和关键字搜索
  • 安全保护:Basic Auth 认证 + 7 天 Cookie + 失败限速(5 次失败锁定 15 分钟)

1.4 用户数据安全保护

用户数据的安全贯穿从客户端到服务端的每一个环节,以下是各层的具体保护措施。

传输层:全链路 HTTPS 加密

所有客户端与服务端之间的通信,包括 REST 请求和 SSE 长连接,均强制走 HTTPS(TLS 1.2/1.3)。明文 HTTP 请求会被 Nginx 直接 301 重定向到 HTTPS,无法绕过。

TLS 证书由 Let's Encrypt 免费签发,每 90 天自动续期,证书过期风险为零。这意味着数据在网络传输过程中始终处于加密状态,即使流量被中间节点截获,也无法读取其内容。

密码存储:bcrypt 哈希,永不明文

用户密码在服务端从不以明文形式存储。注册或修改密码时,后端使用 bcrypt 算法对密码进行哈希处理后再存入数据库。

bcrypt 是专为密码存储设计的哈希算法,有两个关键特性:

  • 不可逆:哈希结果无法被反推出原始密码
  • 加盐(Salt):每次哈希都引入随机盐值,相同的密码每次生成的哈希都不同,防止彩虹表攻击

即使数据库被拖库,攻击者拿到的也只是 bcrypt 哈希,无法还原用户的真实密码。

身份认证:JWT 双 Token 机制

登录成功后,服务端签发两个 Token:

  • access_token(有效期 1 小时):用于日常接口鉴权,存储在客户端内存和本地持久化存储中
  • refresh_token(有效期 30 天):仅用于换取新的 access_token,权限更敏感

access_token 有效期极短,即使泄露,攻击者的利用窗口也只有不到 1 小时。过期后客户端自动用 refresh_token 静默刷新,用户无感知。

JWT 使用 HMAC-SHA256 算法签名,签名密钥(JWT_SECRET)仅存在于服务端环境变量中,客户端无法伪造合法的 Token。

敏感配置:环境变量隔离,不进代码仓库

所有敏感配置(数据库密码、JWT 密钥、邮件服务 API Key、日志认证密码)均通过 .env 文件注入,.env 文件列入 .gitignore,永远不会被提交到代码仓库。

代码仓库中只有 .env.example 占位符文件,格式如下:

env
JWT_SECRET=<your-jwt-secret>
DB_PASSWORD=<your-db-password>
RESEND_API_KEY=<your-resend-api-key>
LOG_USER=<your-log-admin-username>
LOG_PASS=<your-log-admin-password>
LOG_SECRET=<your-log-cookie-signing-secret>

后端端口:不直接对外暴露

Dart 后端服务绑定在 127.0.0.1:8080(本机回环地址),只有 Nginx 能访问它,外部网络无法直接连接 8080 端口。对外只暴露 Nginx 的 80 和 443 端口。

日志脱敏

后端日志记录请求路径、状态码、耗时和用户 ID(UUID 格式),不记录用户密码、Token 原文、待办内容正文等敏感字段。日志查看接口(/logs/)受 Basic Auth 认证保护,并有失败限速机制防止暴力破解。