返回文章列表 →

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
模块的开发流程
  1. 明确模块的业务需求和接口
这个模块负责什么?
有哪些接口?
接口接收什么参数?
返回什么数据?
有哪些权限和业务规则?

POST   /expenses       创建账目
GET    /expenses       分页查询账目
GET    /expenses/:id   查询账目详情
PATCH  /expenses/:id   修改账目
DELETE /expenses/:id   删除账目
  1. 创建模块骨架
src/modules/expenses/
├─ dto/
├─ entities/
├─ expenses.controller.ts
├─ expenses.service.ts
└─ expenses.module.ts
  1. 定义 Entity
  2. 定义 DTO
  3. 编写Service(需要单元测试)
  4. 编写 Controller
  5. 补齐 Module 注册
  6. 测试完整链路
模块分类
  1. 用户模块只放操作用户相关的业务逻辑如查询用户、创建用户,登录、注册、忘记/重置密码放鉴权模块
  2. 因为用户名或密码不匹配无法登陆,返回“用户名或密码错误”,不要返回具体是哪个错误,这样会暴露信息,登录失败使用UnauthorizedException异常
  3. 多个方法有很多重复的地方,只有小部分差别。前端一般是合并成一个方法,根据入参执行不同的逻辑。比如操作方法,根据入参不同分别走新增、编辑、删除。而后端不建议这样,后端要求业务逻辑区分明确,所以通常是把重复的地方提取为单独的方法A,业务上该写多少个方法还写多少个,在这些方法中调用方法A。
  4. 使用jwt token,前端传递的access token前面还需加上Bearer,即:Authorization: Bearer token…。加Bearer是规范,本质没什么意义
  5. 如果是token守卫,不能把它为设置为全局的,不然登录/注册都没法通过。因为没有登录时一定是没有token的,会造成“必须先登录才能登录”。如果非要设置成全局,需要添加公共接口装饰器,把登录、注册相关接口不受token守卫限制
  6. 鉴权有多种,常用的为sessionId、双token
  7. 双token实现退出登录:需要把refresh token的sessionId也放到access token的payload中,退出登录时根据sessionId找到refresh token对应的有效会话把它销毁,同时守卫里添加对access token对应的有效会话进行查询,没有查询到就无法登录。
业务逻辑

发送验证码的完整顺序

1. 标准化邮箱
2. 检查发送频率
3. 生成验证码
4. 计算验证码HMAC
5. 保存新验证码记录
7. 发送邮件
8. 发送失败则标记该记录失效
9. 使旧验证码失效

双token意义

  1. 提高被攻击门槛

ts

简写

constructor(public readonly code: number) {}

// 相当于
readonly code: number;

constructor(code: number) {
  this.code = code;
}