从 0 到后端闭环: Day2 跑通 Prisma 7 + NestJS + Redis 的实战记录

简介: 记录我在 AiTodos Day2 打通后端闭环的过程:完成 Prisma 7 迁移、NestJS + Fastify 接入,以及 AI 资讯/Todo/统计接口和 Redis 日缓存落地。

Day 2 实操清单(后端闭环日)

目标:将 apps/server 从基础 Fastify 迁移为 NestJS + Fastify,打通 Prisma + PostgreSQL + Redis,并完成 auth/profile/todo/ai-news/stats 核心 API 的最小闭环。


今日验收标准

做到以下 6 点即通过 Day 2:

  • 可以注册与登录
  • 可以读写个人信息
  • 可以读写待办
  • 可以读取 5 条 AI 资讯
  • 可以把 AI 资讯转成待办
  • 可以返回后台统计概览数据

step1. 执行顺序(建议严格按序)

  1. 启动 PostgreSQL + Redis
  2. 新建 packages/database 并接 Prisma
  3. 定义 schema + migration + seed(固定 5 条资讯)
  4. 迁移 apps/server 到 NestJS + Fastify(保留 /health
  5. 接 Redis 服务
  6. 创建业务模块:auth/user/profile/todo/ai-news/stats
  7. 开启 Swagger 并联调接口

step2. 环境准备与命令

3.1 安装依赖(根目录执行)

pnpm add -w @nestjs/common @nestjs/core @nestjs/platform-fastify @nestjs/config @nestjs/swagger class-validator class-transformer reflect-metadata rxjs bcryptjs jsonwebtoken ioredis @prisma/client
pnpm add -Dw prisma ts-node tsconfig-paths @types/jsonwebtoken
pnpm --filter @aitodos/database add @prisma/adapter-pg pg dotenv

3.2 启动数据库与缓存(Docker)

新增 infra/docker/docker-compose.day2.yml

services:
  postgres:
    image: postgres:16
    container_name: aitodos-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: aitodos
    ports:
      - "5432:5432"
    volumes:
      - ai_todos_pg_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    container_name: aitodos-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes
    volumes:
      - ai_todos_redis_data:/data

volumes:
  ai_todos_pg_data:
  ai_todos_redis_data:

执行:

docker compose -f infra/docker/docker-compose.day2.yml up -d
docker ps

配置说明:

  • Postgres 使用 5432,数据库名 aitodos
  • Redis 使用 6379
  • volume 确保容器重启后数据仍保留

3.3 配置 .env(根目录)

NODE_ENV=development
PORT=3000
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/aitodos?schema=public"
REDIS_URL="redis://localhost:6379"
JWT_SECRET="day2_local_secret"
JWT_EXPIRES_IN="7d"

配置说明:

  • DATABASE_URL:Prisma 数据源
  • REDIS_URL:ioredis 连接地址
  • JWT_SECRET:签发 token 的密钥(开发环境固定值)

step3. Prisma:packages/database

4.1 创建目录

mkdir packages\database
mkdir packages\database\prisma
mkdir packages\database\src

4.2 packages/database/package.json

{
   
  "name": "@aitodos/database",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "prisma": {
   
    "seed": "ts-node --esm prisma/seed.ts"
  },
  "scripts": {
   
    "prisma:generate": "prisma generate",
    "prisma:migrate": "prisma migrate dev",
    "prisma:seed": "prisma db seed"
  },
  "dependencies": {
   
    "@prisma/adapter-pg": "^7.8.0",
    "@prisma/client": "^7.8.0",
    "dotenv": "^16.6.1",
    "pg": "^8.16.3"
  },
  "devDependencies": {
   
    "prisma": "^7.8.0",
    "ts-node": "^10.9.2"
  }
}

4.3 packages/database/prisma.config.ts(Prisma 7 必需)

import path from "node:path";
import {
    fileURLToPath } from "node:url";
import dotenv from "dotenv";
import {
    defineConfig, env } from "prisma/config";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

dotenv.config({
   
  path: path.resolve(__dirname, "../../.env"),
});

export default defineConfig({
   
  schema: "prisma/schema.prisma",
  migrations: {
   
    path: "prisma/migrations",
    seed: "ts-node --esm prisma/seed.ts",
  },
  datasource: {
   
    url: env("DATABASE_URL"),
  },
});

说明:Prisma 7 不再推荐把 datasource.url 写在 schema 中,迁移连接放到 prisma.config.ts

4.4 packages/database/prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
}

enum UserRole {
  USER
  ADMIN
}

enum TodoStatus {
  TODO
  DOING
  DONE
}

model User {
  id           String      @id @default(cuid())
  email        String      @unique
  passwordHash String
  role         UserRole    @default(USER)
  createdAt    DateTime    @default(now())
  updatedAt    DateTime    @updatedAt

  profile      Profile?
  todos        Todo[]
  newsTodoLogs NewsTodoLog[]
}

model Profile {
  id        String   @id @default(cuid())
  userId    String   @unique
  name      String
  interests String[]
  pushTime  String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model Todo {
  id          String     @id @default(cuid())
  userId      String
  title       String
  description String?
  source      String     @default("manual")
  status      TodoStatus @default(TODO)
  createdAt   DateTime   @default(now())
  updatedAt   DateTime   @updatedAt

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model AiNews {
  id          String   @id @default(cuid())
  title       String
  summary     String
  url         String   @unique
  publishedAt DateTime
  tags        String[]
  createdAt   DateTime @default(now())
}

model NewsTodoLog {
  id        String   @id @default(cuid())
  userId    String
  aiNewsId  String
  todoId    String
  createdAt DateTime @default(now())

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
  @@index([aiNewsId])
}

说明:Prisma 7 下,schema.prisma 只保留 provider,不放 url

4.5 packages/database/src/client.ts(Prisma 7 adapter)

import path from "node:path";
import {
    fileURLToPath } from "node:url";
import dotenv from "dotenv";
import {
    PrismaClient } from "@prisma/client";
import {
    PrismaPg } from "@prisma/adapter-pg";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

dotenv.config({
   
  path: path.resolve(__dirname, "../../../.env"),
});

const connectionString = process.env.DATABASE_URL;

if (!connectionString) {
   
  throw new Error("DATABASE_URL is required");
}

export const prisma = new PrismaClient({
   
  adapter: new PrismaPg({
    connectionString }),
});

4.6 packages/database/prisma/seed.ts(固定 5 条资讯)

import path from "node:path";
import {
    fileURLToPath } from "node:url";
import dotenv from "dotenv";
import {
    PrismaClient } from "@prisma/client";
import {
    PrismaPg } from "@prisma/adapter-pg";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

dotenv.config({
   
  path: path.resolve(__dirname, "../../../.env"),
});

const connectionString = process.env.DATABASE_URL;

if (!connectionString) {
   
  throw new Error("DATABASE_URL is required");
}

const prisma = new PrismaClient({
   
  adapter: new PrismaPg({
    connectionString }),
});

const aiNews = [
  {
    title: "OpenAI 发布新一代模型更新", summary: "多模态稳定性提升", url: "https://example.com/news-1", publishedAt: new Date("2026-04-01"), tags: ["LLM", "Multimodal"] },
  {
    title: "Anthropic 发布 Agent 安全规范", summary: "更细粒度工具权限策略", url: "https://example.com/news-2", publishedAt: new Date("2026-04-02"), tags: ["Agent", "Security"] },
  {
    title: "Meta 开源视觉模型", summary: "图像理解任务效果提升", url: "https://example.com/news-3", publishedAt: new Date("2026-04-03"), tags: ["Vision", "OpenSource"] },
  {
    title: "微软扩展 Copilot 生态", summary: "支持更多工程流集成", url: "https://example.com/news-4", publishedAt: new Date("2026-04-04"), tags: ["Copilot", "Productivity"] },
  {
    title: "社区发布轻量 RAG 框架", summary: "中小团队低成本部署", url: "https://example.com/news-5", publishedAt: new Date("2026-04-05"), tags: ["RAG", "Framework"] }
];

async function main() {
   
  for (const item of aiNews) {
   
    await prisma.aiNews.upsert({
   
      where: {
    url: item.url },
      update: item,
      create: item
    });
  }
}

main().finally(() => prisma.$disconnect());

4.7 执行 migration + seed

pnpm install
pnpm --filter @aitodos/database prisma:generate
pnpm --filter @aitodos/database prisma:migrate -- --name init_day2
pnpm --filter @aitodos/database prisma:seed

step4. apps/server 迁移到 NestJS + Fastify

5.1 关键目录结构

apps/server/src/
├── main.ts
├── app.module.ts
├── common/
│   ├── prisma.service.ts
│   └── redis.service.ts
└── modules/
    ├── health/
    ├── auth/
    ├── user/
    ├── profile/
    ├── todo/
    ├── ai-news/
    └── stats/

5.2 apps/server/package.json(脚本重点)

{
   
  "scripts": {
   
    "dev": "tsx watch src/main.ts",
    "build": "tsc -p tsconfig.build.json",
    "start": "node dist/main.js",
    "type-check": "tsc --noEmit"
  },
  "dependencies": {
   
    "@aitodos/database": "workspace:*",
    "@aitodos/shared": "workspace:*"
  }
}

配置说明:

  • dev 改为监听 src/main.ts
  • 增加 @aitodos/database,服务直接复用 Prisma Client

5.3 apps/server/src/main.ts

import "reflect-metadata";
import {
    NestFactory } from "@nestjs/core";
import {
    FastifyAdapter, NestFastifyApplication } from "@nestjs/platform-fastify";
import {
    ValidationPipe } from "@nestjs/common";
import {
    DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import {
    AppModule } from "./app.module";

async function bootstrap() {
   
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter({
    logger: true })
  );

  app.setGlobalPrefix("api");
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true, transform: true }));

  const config = new DocumentBuilder()
    .setTitle("AiTodos API")
    .setDescription("Day2 backend closed-loop API")
    .setVersion("1.0.0")
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup("swagger", app, document);

  await app.listen(process.env.PORT ? Number(process.env.PORT) : 3000, "0.0.0.0");
}

bootstrap();

5.4 保留健康检查 GET /health

// modules/health/health.controller.ts
import {
    Controller, Get } from "@nestjs/common";

@Controller("health")
export class HealthController {
   
  @Get()
  getHealth() {
   
    return {
    ok: true, service: "server" };
  }
}

说明:虽然全局前缀是 /api,该接口实际路径为 /api/health


6. Redis 接入(最小可用)

apps/server/src/common/redis.service.ts

import {
    Injectable, OnModuleDestroy } from "@nestjs/common";
import Redis from "ioredis";

@Injectable()
export class RedisService implements OnModuleDestroy {
   
  private readonly client = new Redis(process.env.REDIS_URL ?? "redis://localhost:6379");

  get instance() {
   
    return this.client;
  }

  async onModuleDestroy() {
   
    await this.client.quit();
  }
}

配置说明:

  • 先实现连接与释放
  • Day 2 典型用途:token 黑名单、短期缓存、统计临时值

7. 业务模块接口清单(Day 2 最小集)

7.1 Auth

  • POST /api/auth/register
  • POST /api/auth/login
  • GET /api/auth/me

7.2 Profile

  • GET /api/profile
  • PUT /api/profile

7.3 Todo

  • GET /api/todos
  • POST /api/todos
  • PATCH /api/todos/:id
  • DELETE /api/todos/:id

7.4 AI News

  • GET /api/ai-news
  • POST /api/ai-news/:id/add-to-todo

7.5 Stats

  • GET /api/stats/overview

统计返回建议字段:

{
   
  "addedUserCount": 12,
  "newsToTodoCount": 35,
  "totalAiNewsCount": 120,
  "newsToTodoRate": 0.2917
}

8. 联调命令(Day 2 收尾)

# 1) 启动基础设施
docker compose -f infra/docker/docker-compose.day2.yml up -d

# 2) 执行迁移与 seed
pnpm --filter @aitodos/database prisma:migrate -- --name init_day2
pnpm --filter @aitodos/database prisma:seed

# 3) 启动服务
pnpm --filter @aitodos/server dev

联调地址:

  • 健康检查:http://localhost:3000/api/health
  • Swagger:http://localhost:3000/swagger

9. Day 2 当日产出物清单(打勾版)

  • [ ] packages/database 建立完成
  • [ ] Prisma migration 文件已生成
  • [ ] seed 写入 5 条 AI 资讯
  • [ ] apps/server 完成 NestJS + Fastify 迁移
  • [ ] /api/health 可访问
  • [ ] auth/profile/todo/ai-news/stats 模块接口可调
  • [ ] Swagger 可打开并可调试

10. 常见坑位提醒

  • 如果 Prisma 报连接错误,先确认 DATABASE_URL 和容器端口
  • 如果 Redis 连接失败,检查 REDIS_URL 与容器是否启动
  • 如果 Nest 启动时报 reflect-metadata,确认在 main.ts 首行引入
  • 如果 Swagger 空白,确认模块是否被 AppModule 正确引入
  • 如果 pnpm --filter 无法识别包,先检查 pnpm-workspace.yaml 是否包含 packages/*

11. 结论

Day 2 的本质不是“把模块都生成出来”,而是把后端闭环跑通

  • 能存(PostgreSQL)
  • 能查(Prisma + API)
  • 能缓存(Redis)
  • 能联调(Swagger)
  • 能完成“AI 资讯 -> 待办 -> 统计”主链路

只要这个闭环成立,Day 3 前端接入会非常顺畅。

相关文章
|
30天前
|
人工智能 缓存 前端开发
Day4-5:Web 双端适配与 Admin 系统全栈落地实录
本文档整合了 Day 4 与 Day 5 的开发进展,核心涵盖 Web 端响应式 UI 复现、云端资讯 API 接入,以及 Admin 管理系统的架构设计与模块化开发步骤,打通了从用户体验到后台管理的完整链路。
|
1月前
|
人工智能 运维 架构师
我在 AIP 智能体平台踩过的坑,都在这篇企业 AI 落地经验里了
软件架构师罗小东分享企业AI落地实战经验:聚焦AIP智能体平台建设中的真实坑点与解法——涵盖智能体全生命周期管理、多源知识库语义检索、MCP工具集成及多模型中立架构设计,强调“解决问题”而非堆砌功能。(239字)
|
1天前
|
数据采集 数据可视化 数据挖掘
表格魔法师:QoderWork CN 让脏数据秒变仪表盘
本文介绍如何使用阿里QoderWork CN桌面应用,通过内置xlsx技能自动化完成Excel数据清洗(统一日期格式、补全空值、去重等)与可视化(生成含仪表盘、日志、交互表格及图表的HTML报告),提升数据分析效率。
130 5
|
3天前
|
机器学习/深度学习 数据采集 人工智能
田间杂草检测数据集分享(适用于YOLO系列深度学习分类检测任务)
本数据集含4000张真实农田图像(小麦/玉米/水稻田),YOLO格式标注杂草目标,覆盖多天气、光照与视角,适用于YOLO系列等目标检测模型训练,助力智能除草与精准农业研究。(239字)
139 16
|
16天前
|
JSON JavaScript 前端开发
在TypeScript和JavaScript如何使用MetaMessage?
MetaMessage 是一种跨语言数据交换协议,支持 TypeScript/JavaScript(通过装饰器自动类型转换)、JSONC 文本与紧凑二进制 wire 格式,兼顾可读性、精度(如 bigint 表示 int64)与性能,旨在替代 JSON、Protobuf 等传统序列化方案。
209 125
|
2月前
|
Java 大数据 双11
一张图看懂 Java 能干什么——从淘宝下单到双11抢货,背后都是它
本文专为Java零基础小白打造,用通俗比喻讲清Java本质(“万能翻译官”)、跨平台特性及核心优势;解析其在电商、支付等真实场景的应用;破除“Java已死”误区,结合数据说明其持续强势;并给出清晰入门路径与实用学习建议,助你科学起步。
一张图看懂 Java 能干什么——从淘宝下单到双11抢货,背后都是它
|
1月前
|
NoSQL Redis Windows
redis-windows-master7使用步骤详解(附Redis配置与Windows服务注册)
`redis-windows-master7.zip`是Windows平台的免安装版Redis,解压即用,无需安装。适用于本地开发与测试,支持Win7/10/11(64位),默认端口6379,配置简单、启动快捷。(238字)
|
3天前
|
存储 人工智能 算法
告别无效刷屏!TrendRadar:最快30秒部署的开源热点助手,让你只看真正关心的新闻
TrendRadar 是一个轻量级、易部署的热点新闻聚合与推送工具。它能够从知乎、抖音、B站、微博、百度、华尔街见闻等11个主流平台抓取热搜榜单,然后根据你设定的关键词进行智能筛选,最终将你最关心的内容推送到手机或邮箱。
141 13
 告别无效刷屏!TrendRadar:最快30秒部署的开源热点助手,让你只看真正关心的新闻
|
3月前
|
消息中间件 NoSQL Redis
高可靠微服务消息设计:Outbox模式、延迟队列与Watermill集成实践
构建高可靠微服务,事件丢失和延迟任务一直是难题?本文带你从实战角度掌握 Outbox模式、延迟队列 及 Watermill+Redis Stream 集成方案,教你用Go打造可靠、可观测、毫秒级响应的事件驱动系统。
309 2
|
2天前
|
人工智能 缓存 弹性计算
阿里云服务器2核4G5M199元解析:独享型u1实例,性能、适用场景、购买和续费规则介绍
阿里云通用算力型u1实例(ecs.u1-c1m2.large)2核4G、5M带宽、80G ESSD Entry云盘,活动特惠价仅199元/年(官网价3498.36元),企业新老用户同享,续费同价至2027年3月31日,每人限购1台。该实例采用独享型架构,搭载Intel至强可扩展处理器,内网带宽1Gbit/s、收发包30万PPS、云盘IOPS 1万,性能稳定,适合企业官网、中小Web应用、轻量数据库及开发测试等场景。