阅读视图

发现新文章,点击刷新页面。

在 Nestjs 中使用 Drizzle ORM

依赖安装

pnpm add drizzle-orm pg dotenv
pnpm add -D drizzle-kit tsx @types/pg

准备 DrizzleModule

这里我们都在 /src/drizzle 目录中操作

创建 drizzle.provider.ts 文件

import { Provider } from '@nestjs/common';
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';

import { ConfigService } from '@/config/config.service';

import { Schema, schema } from './schema';

export const PG_CONNECTION = 'PG_CONNECTION';

export const DrizzleProvider: Provider = {
    provide: PG_CONNECTION,
    inject: [ConfigService],
    useFactory(configService: ConfigService) {
        const connectionString = configService.database.DATABASE_URL;
        const pool = new Pool({ connectionString });
        return drizzle(pool, { schema, logger: true }) as NodePgDatabase<Schema>;
    },
};

创建 drizzle.service.ts 文件

有了这个以后,我们可以在后续的模块中直接注入该模块 constructor(private readonly drizzle: DrizzleService) {},而不是每次都要写 constructor(@Inject(PG_CONNECTION) readonly db: NodePgDatabase<Schema>) {} 这么一长串内容

import { Inject, Injectable } from '@nestjs/common';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';

import type { Schema } from './schema';

import { PG_CONNECTION } from './drizzle.provider';

@Injectable()
export class DrizzleService {
    constructor(@Inject(PG_CONNECTION) readonly db: NodePgDatabase<Schema>) {}
}

创建 drizzle.module.ts 文件

import { Global, Module } from '@nestjs/common';

import { DrizzleProvider } from './drizzle.provider';
import { DrizzleService } from './drizzle.service';

@Global()
@Module({
    imports: [],
    providers: [DrizzleService, DrizzleProvider],
    exports: [DrizzleService],
})
export class DrizzleModule {}

创建数据库 Schema

创建一个 user.entity.ts

import { relations } from 'drizzle-orm';
import { pgTable, serial, timestamp, varchar } from 'drizzle-orm/pg-core';
import { createSelectSchema } from 'drizzle-zod';
import { z } from 'zod/v4';

export const timestamps = {
    createdAt: timestamp('created_at').notNull().defaultNow(),
    updatedAt: timestamp('updated_at').notNull().defaultNow(),
};

export const user = pgTable('user', {
    id: serial().primaryKey(),
    username: varchar().notNull().unique(),
    password: varchar().notNull(),
    ...timestamps,
});

export const selectUserSchema = createSelectSchema(user);
export type SelectUser = z.infer<typeof selectUserSchema>;

创建 schema.ts 文件

import { user } from './user.entity';

export type { SelectUser } from './user.entity';

export const schema = {
    user
}

export type Schema = typeof schema;
export type SchemaName = keyof Schema;

这样我们的 DrizzleModule 就准备好了

drizzle 相关的配置

我们要在项目根目录下创建 drizzle.config.ts 文件,这个文件是 DrizzleOrm 的配置文件

import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
    schema: './src/drizzle/schema/**.entity.ts', // 这里是数据库 schema 文件的位置
    out: './drizzle/migrations', // 数据库迁移文件生成的地址
    dialect: 'postgresql', // 数据库驱动
    dbCredentials: {
        url: process.env.DATABASE_URL!,
    },
});

package.json 中的脚本

{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:push": "drizzle-kit push",
    "db:studio": "drizzle-kit studio",
  },
}
  • db:generate 声明时或后续 Schema 更改时基于 Drizzle Schema 生成 SQL 迁移
  • db:migrate 运行迁移后,Drizzle Kit 会将成功应用的迁移记录保存到数据库中。
  • db:push 允许您直接将您的架构和后续架构更改推送到数据库
  • db:studio 本地数据库可视化面板

生成 schema 并将改动同步到数据库

pnpm run db:generate
pnpm run db:migrate

成功后我们可以使用 pnpm run db:studio 查看数据库中的内容。

在 app.module.ts 中引入 DrizzleModule

@Module({
    imports: [
    // ...
        DrizzleModule,
    ],
})
export class AppModule {}

在模块中使用,这里用 UserModule 作为例子

@Injectable()
export class UserService {
    constructor(private readonly drizzle: DrizzleService) {}

    async create(createUserDto: CreateUserDto) {
        const result = await this.drizzle.db.insert(schema.user).values(createUserDto).returning();

        return result;
    }

    async findOne(id: number) {
        const [user] = await this.drizzle.db.select().from(schema.user).where(eq(schema.user.id, id));
        return user;
    }

    async update(id: number, updateUserDto: UpdateUserDto) {
        const user = this.drizzle.db
            .update(schema.user)
            .set(updateUserDto)
            .where(eq(schema.user.id, id));
        return user;
    }

    async remove(id: number) {
        const [user] = await this.drizzle.db
            .delete(schema.user)
            .where(eq(schema.user.id, id))
            .returning();
        return user;
    }
}

附加内容

vscode 插件推荐

vscode-drizzle-orm 这个插件可以让我们看到 drizzle 生成好的数据库模型图

数据库填充

在开发中我们通常都要填充一些模拟数据用来测试,那么在 nestjs + drizzle 这个组合下我们如何操作呢?

我们还是在 src/drizzle 文件夹中操作

创建 db.ts

import 'dotenv/config';
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';

import { Schema, schema } from './schema';

const connectionString = process.env.DATABASE_URL;

if (!connectionString) {
    throw new Error('DATABASE_URL is not defined');
}

const pool = new Pool({ connectionString });
export const db: NodePgDatabase<Schema> = drizzle(pool, { schema, logger: true });

export type db = NodePgDatabase<Schema>;

创建 seed 相关文件

import 'dotenv/config';
import { Table } from 'drizzle-orm';

import { db } from './db';
import { schema } from './schema';
import { seedUser } from './seeds/user.seed';

// 清空数据库中的数据
async function clearTable() {
    // eslint-disable-next-line drizzle/enforce-delete-with-where
    return await Promise.all(Object.values(schema).map((table: Table) => db.delete(table).execute()));
}

async function seed() {
    // 每次填充前先将数据库中的数据清空
    await clearTable();

    // 填充一些测试数据用来使用
    await seedUser(db);
}

seed().catch((e) => {
    console.error(e);
    process.exit(0);
});
import { hashSync } from 'bcrypt';

import { db } from '@/drizzle/db';

import { schema } from '../schema';

export async function seedUser(db: db) {
    await db.insert(schema.user).values([
        { username: 'admin', password: hashSync('123456', 10) },
        { username: 'user', password: hashSync('123456', 10) },
    ]);
}

在 package.json 中添加命令

{
  "db:seed": "ts-node ./src/drizzle/seed.ts",
}

我们就可以运行 pnpm run db:seed 来进行数据库填充了

NestJS入门(1)——TODO项目创建及概念初步了解

通过实现一个TODO需求来了解NestJS,需要实现的功能包括:

  1. 用户注册、登录
  2. 备忘录创建、修改、查询、删除

创建项目

❗注意:Node版本需≥20

全局安装 Nest CLI 脚手架,并初始化项目,项目名称为 DoraemonNotebook (哆啦A梦记事本)

$ npm i -g @nestjs/cli 
$ nest new DoraemonNotebook

了解项目结构及基础概念

创建完成后项目的目录结构如图:

image.png

其中核心文件为 src 目录下的几个文件:

文件 说明
app.controller.spec.ts 控制器的单元测试
app.controller.ts 一个具有单个路由的基本控制器
app.module.ts 应用程序的根模块
app.service.ts 一个具有单个方法的基本服务
main.ts 应用程序的入口文件,它使用核心函数 NestFactory 来创建 Nest 应用程序实例

根据这几个核心文件及数据流向我们来了解一下NestJS中的几个核心概念,在入口文件中我们首先会接触到根模块 AppModule

模块

image.png

Nest项目中至少需要一个根模块,即目前的 app.module.ts 文件,它是 Nest 构建应用程序的起点。我们在组织程序功能架构时推荐使用模块作为组织组件的方式,将一组密切相关的功能作为一个模块,方便管理。

在模块中我们可以看到引入了 AppControllerAppService,接下来我们先了解控制器(AppController)的概念。

控制器

image.png

控制器类似于前端的路由,它告诉程序请求将用哪个控制器处理,一个控制器可以具有多个路由,每个路由执行不同的操作。 具体的操作实现也就是业务逻辑我们一般放在提供者(AppService)中实现。

提供者

提供者(Provider)负责封装特定功能(如业务逻辑、数据访问、工具函数等),并可通过依赖注入(@Injectable())的方式在控制器或其他服务中使用。

默认情况下提供者是单例模式,全局共享实例状态,也可以通过声明作用域将生命周期改为请求作用域(@Injectable({scope: Scope.REQUEST}))、临时作用域(@Injectable({scope: Scope.TRANSIENT}))。

其他

本章只是简单了解NestJS的目录及架构,其实还有很多概念没介绍到,比如装饰器、依赖注入、中间件、异常过滤器、拦截器等,后续的文章中我们将通过在完成实际TODO需求中一一使用并详细讲解。

项目启动

项目开发过程中我们可以通过以下命令启动程序:

$ npm run start:dev

此命令将监视您的文件,自动重新编译并重新加载服务器。通过浏览器访问 http://localhost:3000,我们可以看到返回的 Hello World! 消息。

【开发AGIC】Vue3+NestJS+DeepSeek AI作业批改系统(已开源)

前言

大家好,我是一诺。国庆假期带儿子回老家带了几天,鬼天气忽冷忽热的 我和儿子都给整生病了。

也没有出去玩,除了在家带娃,空闲时间开源了能豆ai批改助手

说下产品背景

我有个朋友是英语老师,常熬夜批改作文。平时在用 DeepSeek 批改作业,可没办法批量修改,很特别麻烦。就希望能有个可以统一管理作业,沉淀教学数据的工具。

于是就有了"能豆AI"这个产品,集成DeepSeek分析能力,实现了学生在线提交作业 → AI实时批改 → 教师人工核实批改的完整业务闭环。

核心功能-ai批改作业

ca3c8ae8-2f25-48af-a05a-f7db06a437c1.gif

为什么叫能豆AI。

我在设计 logo ,不知道叫什么好。这时我儿子在扶着墙学走路,他刚9个月刚会爬就想尝试走~ ,那就叫"能豆AI"吧,因为我儿子的小名叫豆豆。

在线预览

在线体验 ai.dslcv.com/

仓库地址

  1. github ➡️ github.com/yinuoguan/n…
  2. gitee ➡️ gitee.com/wang-tians-…
  3. 接口文档 http://124.222.166.174:3002/api/docs

架构图

总体架构图

业务流程

技术选型

前端技术栈

选择 Vue 3 + TypeScript 作为前端框架,之前一诺维护的老项目都是vue2, 这次全面使用Vue3开发,发现用 Composition API 写起来更灵活,特别是处理复杂的业务逻辑时。

Vuex 用来管理全局状态,比如用户登录信息。局部状态还是放在组件里,这样代码更清晰。

Element Plus 是因为组件比较全面。配合 Tailwind CSS 做一些个性化的样式调整,开发速度很快。

后端技术栈

后端用的是 NestJS,说实话一开始也考虑过 Express,但 NestJS 的装饰器和模块化设计确实香,代码组织得很清晰。

JWT 做身份认证,无状态的,扩展性好。Swagger 自动生成 API 文档。

数据存储

MongoDB 选择的原因很简单:作业数据结构比较灵活,不同类型的作业字段差异很大,用文档数据库比关系型数据库方便多了。而且 MongoDB 的查询也够用,性能也不错。

Redis 主要用来缓存一些热点数据和存储用户会话,毕竟内存数据库速度快,用户体验好。

AI大模型

DeepSeek 是主力,性价比真的很高,批改质量也不错。关键是 API 调用稳定,价格也能接受。

后来又集成了豆包,主要是想让 AI 的反馈更温馨一点,豆包在情感表达这块做得比较好,学生看到反馈不会那么有压力。

两个模型配合使用,DeepSeek 负责专业的内容分析,豆包负责鼓励和引导,效果比单用一个模型好很多。

核心模块

1.班级管理

这是系统的基础模块,解决了教师管理多个班级的痛点:

核心功能:

  • ✅ 创建班级:支持自定义班级名称、描述和邀请码
  • ✅ 学生管理:通过邀请码机制,学生可以自主加入班级
  • ✅ 状态管理:可以暂停/激活学生,灵活管理班级人员
  • ✅ 实时统计:学生数量和作业完成情况一目了然

2. 作业提交与批改模块

状态流转管理:

核心特性:

  • 学生端:专用提交和查看功能,界面简洁易用
  • 教师端:批改和统计管理功能,支持批量操作
  • 管理员端:AI批改和日志管理,系统监控

3. AI 批改集成

这是系统的技术亮点,与 DeepSeek 的深度集成:

AI批改能力:

mindmap
    root((AI批改))
        DeepSeek
            语法检查
            逻辑分析
            内容评估
        豆包
            情感识别
            温馨反馈
            学习引导
        评分
            多维度评价
            个性化建议
            数据洞察

AI批改流程:

image.png

批改质量保障:

  • 多轮提示词优化,确保批改一致性
  • 人工复核机制,AI + 人工双重保障
  • 批改日志记录,便于分析和改进
  • 异步处理机制,不阻塞用户操作

4. 权限管理系统

graph TB
    A[超级管理员] --> B[系统配置]
    A --> C[用户管理]
    A --> D[数据监控]
    
    E[教师] --> F[班级管理]
    E --> G[作业发布]
    E --> H[批改审核]
    
    I[学生] --> J[加入班级]
    I --> K[提交作业]
    I --> L[查看成绩]

功能展示

管理员端功能

主要功能:

  • 系统配置管理:AI模型参数调整,批改规则配置
  • 用户权限管理:教师和学生账号管理,权限分配
  • 数据统计分析:批改效率统计,系统使用情况分析
  • 批改日志查看:AI批改过程追踪,质量监控

控制台看板

大模型配置 && 用户管理

教师端功能

主要功能:

  • 班级创建与管理:一键创建班级,邀请码分享
  • 作业发布与管理:灵活的作业类型,截止时间设置
  • 批改结果查看:AI初评结果查看,人工复核操作
  • 学生成绩统计:班级整体表现分析,个人进步追踪

工作台

创建班级

添加学生

发布作业

配置AI批改规则

作业详情

批改作业

学生端功能

主要功能:

  • 班级加入:通过邀请码快速加入班级
  • 作业提交:支持草稿保存,多次修改提交
  • 批改结果查看:详细的AI评语和教师点评
  • 学习进度追踪:个人作业历史,成绩变化趋势

激活账户

学习中心

班级作业

提交作业

查看结果

AI点评和老师批注

❌