Note
经验记录
行业相关经验记录
PRD
产品需求文档。写需求文档通常先写头和尾:需求背景,为什么有这个需求。需求目标:这个需求要解决哪些问题或要达到什么目标。再写具体的业务规则。
需求背景
需求目标
功能1
业务规则
...
验收标准
功能2
业务规则
...
验收标准
...
验收标准
流程
产品想法
↓
需求分析
↓
PRD / 产品需求
↓
需求评审
↓
⭐技术分析与设计:把业务需求抽象成正确的业务模型,再把业务模型转换成合理的技术设计。需求中有哪些对象、它们之间的关系是什么,每个对象有什么属性,之后进行设计。技术方案选择、有哪些表它们有哪些字段,它们之间的关系是,以及实现需求每一步的业务逻辑有哪些
↓
任务拆分:设计开发任务
↓
开发
↓
测试
↓
上线
↓
维护 / 迭代
PRD示例
技术设计示例
开发任务示例
后端开发
流程
最开始是创建后端项目和数据库,接着配置后端项目,测试是否能够连接上数据库。然后通过在后端项目中创建数据模型实体,通过Migration创建数据库表,写对应业务逻辑。不推荐先手动建表,手动修改如果后续需求变更维护起来比较麻烦。并不是所有表建完以后才开始业务逻辑,推荐“按模块/功能纵向开发”,开发完成一个模块(包含所涉及的表、业务逻辑)再开发下一个模块。全栈开发的话,后端开发完一个模块接着写对应的前端页面。如果开发完整个后端再写前端,联调可能会存在一些问题,比如返回字段不方便展示、登录 Token、跨域等联调问题、需要的接口结构与最初想象不同。
目录
service-nest/
├─ src/
│ ├─ config/ # 环境变量与应用配置
│ │ ├─ app.config.ts # 端口、环境、接口前缀
│ │ ├─ database.config.ts # 数据库配置(已有)
│ │ ├─ jwt.config.ts
│ │ ├─ mail.config.ts
│ │ └─ env.validation.ts # 环境变量校验
│ │
│ ├─ common/ # 跨业务模块复用的代码
│ │ ├─ constants/
│ │ ├─ decorators/
│ │ ├─ dto/
│ │ │ ├─ api-response.dto.ts # 通用响应结构
│ │ │ └─ pagination.dto.ts
│ │ ├─ enums/
│ │ ├─ exceptions/ # 自定义业务异常
│ │ ├─ filters/
│ │ │ └─ http-exception.filter.ts # 统一异常响应
│ │ ├─ guards/
│ │ ├─ interceptors/
│ │ │ ├─ response.interceptor.ts # 统一成功响应
│ │ │ └─ logging.interceptor.ts # 请求访问日志
│ │ ├─ middleware/
│ │ │ └─ request-id.middleware.ts
│ │ ├─ pipes/
│ │ └─ types/
│ │ └─ api-response.interface.ts
│ │
│ ├─ infrastructure/ # 第三方基础设施封装
│ │ └─ logger/
│ │ ├─ logger.module.ts
│ │ ├─ logger.service.ts
│ │ └─ logger.config.ts
│ │
│ ├─ modules/ # 业务模块
│ │ ├─ users/
│ │ ├─ expenses/
│ │ ├─ categories/
│ │ └─ auth/
│ │
│ ├─ app.module.ts
│ └─ main.ts
│
├─ logs/ # 本地运行产生的日志文件
└─ .env.example
| 内容 | 推荐目录 |
|---|---|
| 数据库、JWT、邮件、端口配置 | src/config/ |
| 环境变量格式与必填校验 | src/config/env.validation.ts |
| 统一成功响应 | src/common/interceptors/response.interceptor.ts |
| 响应类型定义 | src/common/types/ 或 src/common/dto/ |
| 统一异常响应 | src/common/filters/http-exception.filter.ts |
| 自定义业务异常 | src/common/exceptions/ |
| 请求日志处理 | src/common/interceptors/logging.interceptor.ts |
| Winston/Pino 等日志实现 | src/infrastructure/logger/ |
| 实际生成的日志文件 | 项目根目录 logs/,并加入 .gitignore |
| 仅属于某个模块(如users)的 DTO、异常等 | 放 src/modules/users/ 内,不要放进 common |
公共配置
先把后端需要使用的公共配置封装好再开始写业务模块。如响应结构、日志、拦截器
数据库处理
库和表的创建
数据库手动创建,表通过后端代码中注册实体来创建,后续的表的维护也是通过后端代码来操作,不要手动修改。通常,数据库 charset 选 utf8mb4 ,collation 选 utf8mb4_unicode_ci 。通过后端代码创建新并注册实体后,需要重启项目才会创建该表。
用户表关于id和uuid的问题
通常id用于数据库内部关联,也可以暴露给外部,如提供给前端、URL、第三方系统。如果不想暴露连续数字等信息,可以创建publicId(如uuid)暴露给外部。
关于关联表
因为多对多关系建立的关联表不需要主键id,只需要放两张表的主键就行,这两张表的主键都是唯一的所以采用组合主键
时间的存储
时间存储使用 DATETIME ,不推荐 varchar 。
- 数据库无法严格校验它是不是合法时间。
- 需要手动拼接和解析字符串。
- 容易出现时区问题。
- 容易写入不同格式。
- 时间加减、过期判断、范围查询不方便。
- 不能充分使用 MySQL 的时间函数和时间类型索引。
modules
后端模块分类根据业务来拆分,一个模块不一定有实体或DTO,有些模块是用于服务其他模块,和它一起组成业务模块,所以本身不具备实体。如:
记录日常消费的支出和收入。
1.存在用户概念,每个用户的数据隔离,用户通过邮箱注册,需要邮箱验证码验证,后续通过邮箱号和密码登录。
2.消费和收入都有类别归属概念,比如早餐属于日常三餐这一类
3.还会有统计图,显示指定的消费在某一时间段的占比
模块整理为
| 模块 | 负责内容 |
|---|---|
UsersModule |
用户资料、用户实体、邮箱唯一性、账号状态 |
AuthModule |
注册、验证码校验、登录、密码加密、JWT、AuthGuard |
CategoriesModule |
收支分类的增删改查 |
TransactionsModule |
收入和支出记录的增删改查 |
StatisticsModule |
时间范围统计、分类占比、趋势统计 |
MailModule |
发送邮件、验证码邮件模板等基础设施 |
邮件模块就没有实体和dto
模块结构
基本结构如下,根据实际需求拓展文件/文件夹
├── modules/
│ ├── xxx/
│ │ ├── dto/
│ │ ├── entities/
│ │ │ └── xxx.entity.ts
│ │ ├── xxx.controller.ts
│ │ ├── xxx.service.ts
│ │ └── xxx.module.t
模块的开发流程
- 明确模块的业务需求和接口
这个模块负责什么?
有哪些接口?
接口接收什么参数?
返回什么数据?
有哪些权限和业务规则?
POST /expenses 创建账目
GET /expenses 分页查询账目
GET /expenses/:id 查询账目详情
PATCH /expenses/:id 修改账目
DELETE /expenses/:id 删除账目
- 创建模块骨架
src/modules/expenses/
├─ dto/
├─ entities/
├─ expenses.controller.ts
├─ expenses.service.ts
└─ expenses.module.ts
- 定义 Entity
- 定义 DTO
- 编写Service(需要单元测试)
- 编写 Controller
- 补齐 Module 注册
- 测试完整链路
模块分类
- 用户模块只放操作用户相关的业务逻辑如查询用户、创建用户,登录、注册、忘记/重置密码放鉴权模块
- 因为用户名或密码不匹配无法登陆,返回“用户名或密码错误”,不要返回具体是哪个错误,这样会暴露信息,登录失败使用UnauthorizedException异常
- 多个方法有很多重复的地方,只有小部分差别。前端一般是合并成一个方法,根据入参执行不同的逻辑。比如操作方法,根据入参不同分别走新增、编辑、删除。而后端不建议这样,后端要求业务逻辑区分明确,所以通常是把重复的地方提取为单独的方法A,业务上该写多少个方法还写多少个,在这些方法中调用方法A。
- 使用jwt token,前端传递的access token前面还需加上Bearer,即:Authorization: Bearer token…。加Bearer是规范,本质没什么意义
- 如果是token守卫,不能把它为设置为全局的,不然登录/注册都没法通过。因为没有登录时一定是没有token的,会造成“必须先登录才能登录”。如果非要设置成全局,需要添加公共接口装饰器,把登录、注册相关接口不受token守卫限制
- 鉴权有多种,常用的为sessionId、双token
- 双token实现退出登录:需要把refresh token的sessionId也放到access token的payload中,退出登录时根据sessionId找到refresh token对应的有效会话把它销毁,同时守卫里添加对access token对应的有效会话进行查询,没有查询到就无法登录。
业务逻辑
发送验证码的完整顺序
1. 标准化邮箱
2. 检查发送频率
3. 生成验证码
4. 计算验证码HMAC
5. 保存新验证码记录
7. 发送邮件
8. 发送失败则标记该记录失效
9. 使旧验证码失效
双token意义
- 提高被攻击门槛
ts
简写
constructor(public readonly code: number) {}
// 相当于
readonly code: number;
constructor(code: number) {
this.code = code;
}