一、背景
念账是一个 Flutter 记账客户端,第一版是快速原型,账号和账本数据都存在本地 SQLite 里。本次给它配上服务端,顺带将账号体系的功能一并完善了。
二、技术选型
| 选择 | 职责 | 为什么选它 |
|---|---|---|
| Node.js | 后端开发语言 | 选择一个我比较熟悉的技术栈。除此之外,Node.js 生态成熟,配合 TypeScript 获得类型安全 |
| Fastify | HTTP 服务器框架 | API 设计严格,开发者明确定义请求和响应的数据格式,减少开发调试的不确定性。另外,它相对于 Express 性能更好 |
| PostgreSQL | 关系型数据库 | 本项目用 MySQL 也能做,但我更倾向一款类型和约束更严格、扩展能力更强的数据库 |
| pnpm | 包管理器,安装与管理依赖 | 相比 npm / yarn 更快、更省磁盘;依赖隔离严格,只有显式声明的依赖才能引入,避免误用别人顺带装进来的包 |
| JSON Web Token(JWT)+ Refresh Token | 认证方案,识别用户身份并维持登录态 | JWT 无状态、验证签名即可识别身份,适合移动端;配合可主动吊销的 Refresh Token 维持登录态 |
| Vitest + Supertest | 测试工具,跑单元测试与接口集成测试 | Vitest 原生支持 ECMAScript Modules(ESM)/ TypeScript、快且兼容 Jest;Supertest 以真实 HTTP 请求验证接口行为 |
三、工程结构
项目采用经典的三层分层架构(Controller--Service--Repository),并按 feature-first 组织目录:每个业务模块自包含从路由到表结构的完整链路,与业务无关的基础设施放 core/,跨模块共享的放 shared/。模块之间不互相引用,需要共享就下沉。
三层各司其职:routes + controller 是表现层 ,负责 HTTP 输入输出;service 是应用/业务层 ,承载业务逻辑;repository + schema 是持久层 ,用 Repository 模式把 SQL 和表映射封在内部。这套 controller → service → repository 的组合,和 Spring Boot、NestJS 等框架的默认分层一脉相承,是最主流的 Web 应用架构。
这套结构强调依赖单向:上层依赖下层,反向不允许,业务逻辑与技术细节尽量分离。controller 只依赖 service、不直接碰数据库,各层职责单一,业务逻辑也能独立测试。
四、认证体系
JWT(JSON Web Token)是一种把用户信息和有效期打包并签名的令牌格式,服务端验证签名即可识别身份、无需查库。Access Token 就是短期的 JWT;Refresh Token 则是长期的不透明随机串,库里只存哈希,因此可主动吊销。刷新时做轮换:旧 Refresh 立即作废、签发新的一对,防止重放;登出吊销对应 Refresh(幂等)。
客户端把提前刷新、401 兜底重放、并发单飞全部封进拦截器,业务层无感知。安全默认值:密码 bcrypt 加盐;登录失败统一报"账号或密码错误",不泄露账号是否存在;所有令牌只存哈希。
五、邮箱验证码:注册与找回密码的统一机制
注册和找回密码要做的事其实是同一个套路:先往邮箱发一个验证码,用户提交后校验,通过就授予相应权限------注册是"创建账号",找回密码是"允许改密码"。既然是同一套路,就用一套机制承载,只用一个"用途(purpose)"字段区分这两种场景,不用维护两套代码。
验证码发到邮箱时是明文,但落库前会先做哈希,库里不存明文。哈希时额外掺入三样东西做盐:服务端密钥、邮箱、用途。加盐一是避免不同用户、不同场景的相同验证码在库里长成一样,二是让攻击者即使拿到数据库,也无法反推出验证码。
规则围绕"短时效、一次性、可频控":10 分钟有效、用后作废、同邮箱发送有间隔与每日上限、失败到阈值作废。
六、RESTful API
接口采用常见的 RESTful 约定:统一 /api 前缀、资源用复数、多词用短横线,路径嵌套最多两层(如"用户 / 记账条目",不再往"条目下的分类"套),让调用方看路径就能猜到用途。
每个请求都经过同一条流水线:认证中间件认出用户 → Zod 校验参数 → controller → service → repository。认证和校验集中在入口,业务代码不用重复处理。
用户数据隔离:所有读写都强制带当前用户,按 ID 操作也一样,而且这条约束只放在仓储层这一个通道上执行,不会在业务分支里漏掉。越权访问统一返回"资源不存在"而非"无权限",避免泄露资源是否存在。
金额用整数分、日期用 epoch 天,避免浮点误差和时区歧义;时间范围查询强制有界;汇总交给数据库;响应统一成 {code, message, data}。
七、错误处理、日志与配置
错误的传递和处理:业务层抛携带业务码、文案、状态的领域错误,全局处理器接住后分类返回;代码异常统一给笼统文案,堆栈只进日志,不暴露内部细节。
日志用 Pino,结构化并携带请求 ID;开发可读、生产 JSON、测试静默。密码、令牌、验证码一律不落日志。配置全部走环境变量,有默认值或启动校验,敏感项不硬编码、不进仓库。
八、测试策略
单元测试覆盖纯逻辑和可隔离的业务规则,比如密码哈希、令牌校验、验证码处理、service 分支。外部依赖用替身注入,跑得快、定位准。
集成测试在真实应用实例上通过 Supertest 发 HTTP 请求,覆盖注册、登录、刷新、记账增删改查等完整链路,验证各层拼起来是否真的通。
原则:测业务规则而不是测框架,隔离外部依赖。
- 数据库用独立测试库,每个用例执行前清空数据、彼此独立;
- 用例命名统一为"方法 + 场景 + 期望";
- 核心逻辑覆盖率目标不低于 90%。