一、 项目初始化与环境配置
在后端项目根目录(server/)下执行初始化命令:
| |
执行后,系统会在 server/ 目录下自动生成以下目录和文件:
prisma/目录:存放数据库配置文件。schema.prisma:Prisma 的核心数据模型与配置文件。
prisma.config.ts:Prisma 的全局配置文件。.env:环境变量配置文件(用于存放数据库连接串等敏感信息)。
二、 Prisma 全局配置与数据库选型
进入 prisma/schema.prisma 文件,首先配置生成器和数据源:
| |
1. 核心配置项解析
output:指定 Prisma Client 及代码声明文件、类型提示文件的生成目录。本架构将其导出至公共库libs/shared中以便复用。moduleFormat = "cjs"(关键点):版本差异说明:在 Prisma 7.0+ 及更高版本中,默认生成的模块格式已调整为 ESM。由于 NestJS 编译打包后的文件遵循 CJS (CommonJS) 规范,直接使用 ESM 会引发模块引入冲突。因此,必须显式将其改回
cjs。
2. 数据库选型:为什么选择 PostgreSQL 而不是 MySQL?
- 原生枚举支持:PostgreSQL 天然支持真正的
enum类型;而 MySQL 的枚举在底层实质上是一堆字符串,表现力和性能稍逊。 - 丰富的数据类型:PostgreSQL 原生支持对象(JSON/JSONB)、数组(Array)以及纯粹的布尔值(True/False);而 MySQL 存储对象和数组时通常需要转换为字符串(或使用特定的 JSON 类型,但生态支持略逊于 PG)。
- 性能优势:在处理复杂查询、高并发以及大规模数据写入(如本项目的海量单词导入)时,PostgreSQL 具有更强的性能和稳定性。
三、 数据模型设计与业务逻辑详解
1. 用户表 (User) 与 ID 策略对比
| |
💡 装饰器与修饰符说明
?:代表该字段为可选字段(Nullable),在数据库中允许为NULL。@id:声明该字段为表的主键。@unique:声明唯一约束,该列的值在全表不可重复。@default(now()):在创建数据时自动填入当前服务器时间。@updatedAt:自动化钩子,只要该行数据的任何一个字段发生变更,底层会自动更新该时间。
🛡️ 主键自增策略对比 (autoincrement vs uuid vs cuid)
autoincrement()(类如 1, 2, 3…):- 缺点:容易被爬虫根据 ID 规律遍历数据;在分布式多库多表架构下无法保证唯一性。
uuid()(全球唯一哈希):- 缺点:由 MAC 地址、时间戳等生成,由于内部字符无序,导致数据库聚簇索引离散,在高并发写入和排序时性能一般。
cuid()(推荐):- 优点:专门为水平扩展和现代 Web 应用设计的唯一 ID。它基于时间戳和计数器生成,天生支持排序。在保持全球唯一性的同时,避免了索引碎片化,底层读写性能最佳。
2. 单词模块:多对多关系的拆解与索引优化
用户与单词之间是多对多关系(一个用户可以背多个单词,一个单词可以被多个用户背)。这里通过引入中间表 WordBookRecord 将其拆解为两个一对多关系。
| |
💡 索引优化逻辑
在 WordBook 表中添加 @@index。因为后续业务中会有大量基于 word、tag 或两者组合的检索请求。提前建立索引可以避免全表扫描,极大提升查询效率。
3. 支付与课程模块:四表联查与状态流转
| |
🛡️ 核心业务架构设计解析
双订单号设计 (
outTradeNo与tradeNo) 的必要性:outTradeNo是我方系统生成的订单号;tradeNo是支付宝侧生成的流水号。- 兜底机制:一旦我方系统发生故障、数据库数据受损或订单号失效,只要有支付宝官方的
tradeNo,我们依然可以通过官方渠道为用户办理退款或实施人工对账,这是金融级支付方案的必备容错逻辑。
为何引入
CourseRecord中间表:Course表是静态的、公共的,所有人看到的课程内容、老师、价格完全一致。- 我们绝不能直接修改
Course表去记录购买状态。引入CourseRecord可以将“用户”与“课程”解耦,用来动态记录特定用户对特定课程的生命周期状态(是否购买、学习进度等)。
支付回调与事务(Transaction)流转链路:
- 步骤1:用户选定课程点击下单 $\rightarrow$ 系统创建
PaymentRecord(状态为NOT_PAY)和CourseRecord(isPurchased: false)。 - 步骤2:用户调起支付宝并完成支付 $\rightarrow$ 支付宝异步通知(Notify)我方后端接口。
- 步骤3:后端捕获回调数据,获取
outTradeNo和tradeNo。 - 步骤4(核心):开启数据库事务。将
PaymentRecord的状态改为TRADE_SUCCESS,同时将关联的CourseRecord中的isPurchased改为true。事务确保这两步操作要么同时成功,要么同时失败,杜绝“付了钱却没解锁课程”的账实不符问题。
- 步骤1:用户选定课程点击下单 $\rightarrow$ 系统创建
四表级联查询的长处: 由于模型间建立了清晰的关联链(
User$\rightarrow$PaymentRecord$\rightarrow$CourseRecord$\rightarrow$Course),使用 Prisma 可以非常轻松地实现四表联查。Prisma 会自动将这种深层嵌套的关系平铺、序列化为规整的 JSON 树状结构直接返回给前端,极大地简化了聚合查询的代码量。
四、 数据库连接与 NestJS 集成
1. 依赖安装
在后端安装 Prisma、PostgreSQL 驱动及环境变量管理插件:
| |
2. 环境变量配置 (.env)
配置 PostgreSQL 的标准连接串(需提前在数据库软件中创建名为 english 的库):
| |
3. 执行数据库迁移与客户端生成
| |
4. 在 NestJS 中封装 Prisma 服务
在 libs/shared/src/prisma/prisma.service.ts 中编写单例服务,利用 @prisma/adapter-pg 驱动驱动连接:
| |
五、 海量数据导入脚本(77万条单词 CSV 离线流式写入)
该脚本用于将大型 ecdict.csv(GBK编码)的数据,通过流式读取、编码转换、批量缓存等策略高效安全地写入到 PostgreSQL 中。
| |
Photo by Pawel Czerwinski on Unsplash
