从 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 前端接入会非常顺畅。

相关文章
|
6天前
|
人工智能 自然语言处理 数据可视化
阿里云百炼上线DeepSeek-V4:API 价格与官网一致,百万Tokens输入最低1元、输出最低2元
阿里云百炼正式推出DeepSeek-V4系列模型,包含DeepSeek-V4-pro和DeepSeek-V4-flash两款,全模型享受低至4.5折优惠,API价格与DeepSeek官网一致,百万Tokens输入最低1元、输出最低2元。该系列模型覆盖全面、成本优化显著且使用便捷,支持跨模型使用,承诺消费越高、周期越长,折扣越大。活动提供多种包月、包季套餐及特惠价格,满足不同用户需求,助力企业和开发者轻松实现AI落地,广泛应用于电商营销、广告创作、短剧漫剧、AI Coding、智能客服及AI办公等领域。
|
存储 小程序 前端开发
微信小程序与Java后端实现微信授权登录功能
微信小程序极大地简化了登录注册流程。对于用户而言,仅仅需要点击授权按钮,便能够完成登录操作,无需经历繁琐的注册步骤以及输入账号密码等一系列复杂操作,这种便捷的登录方式极大地提升了用户的使用体验
3831 12
|
5月前
|
监控 安全 Unix
iOS 崩溃排查不再靠猜!这份分层捕获指南请收好
从 Mach 内核异常到 NSException,从堆栈遍历到僵尸对象检测,阿里云 RUM iOS SDK 基于 KSCrash 构建了一套完整、异步安全、生产可用的崩溃捕获体系,让每一个线上崩溃都能被精准定位。
1417 121
|
3月前
|
人工智能 API 机器人
OpenClaw 用户部署和使用指南汇总
本文档为OpenClaw(原MoltBot)官方使用指南,涵盖一键部署(阿里云轻量服务器年仅68元)、钉钉/飞书/企微等多平台AI员工搭建、典型场景实践及高频问题FAQ。同步更新产品化修复进展,助力用户高效落地7×24小时主动执行AI助手。
28728 238
|
11小时前
|
人工智能 运维 安全
我对AI智能体平台架构设计经验
软件架构师罗小东,深耕AI智能体平台架构设计与工程落地。本文系统阐述AIP五层架构(应用层、平台层、支撑层、运营层、运维层),聚焦分层边界、能力抽象、运行约束与可信保障,强调“可控性、可扩展性、可维护性”的务实平衡,为AI工程化提供可复用的实践范式。(239字)
我对AI智能体平台架构设计经验
|
22小时前
|
人工智能 缓存 前端开发
从 0 到 1:AI Todos 项目 Day1 实战——用 pnpm + Turbo 搭建可迭代的 Monorepo 基线
记录AI待办(AiTodos)Monorepo项目的首日基础工程搭建全过程,涵盖pnpm工作区配置、目录骨架初始化、Turbo任务编排、TypeScript统一配置、Vite/Fastify应用初始化、跨包共享(shared/api-sdk/store)及标准化脚本建设,完成可运行、可检查、可扩展的现代化前端工程基线。
|
2天前
|
存储 人工智能 NoSQL
让 Agent 拥有记忆 —— 表格存储记忆服务邀测指南
本文将介绍表格存储记忆服务的产品能力、接入方式和接口说明,帮助您快速了解和体验表格存储记忆服务的相关功能。
|
存储 NoSQL Cloud Native
【福利】阿里云数据库电子书合集开放下载!从开源数据库到阿里巴巴实战,一键全包!
这次社区为大家打包带来阿里云数据库团队出品的8本电子书,这些电子书不仅涵盖了MySQL、MongoDB等流行数据库的技术详解及阿里云实战经验,还囊括了前沿数据库技术的深入分析和探讨,绝对让你大呼过瘾!最最关键的是,这些电子书全部都可以免费下载!
12427 0
【福利】阿里云数据库电子书合集开放下载!从开源数据库到阿里巴巴实战,一键全包!
|
3天前
|
人工智能 JSON 供应链
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
LucianaiB分享零成本畅用JVS Claw教程(学生认证享7个月使用权),并开源GeoMind项目——将JVS改造为科研与产业地理情报可视化AI助手,支持飞书文档解析、地理编码与腾讯地图可视化,助力产业关系图谱构建。
23297 2
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」

热门文章

最新文章