feat: Prisma 用户落库、迁移与启动环境加载

- Prisma 7 + adapter-pg;prisma.config 与 users 初始迁移\n- AppModule 挂载 PrismaModule;PrismaService 仅依赖 DATABASE_URL\n- main 入口 dotenv/config,避免 Prisma 早于 Config 读 env\n- 短信登录 upsert User;默认昵称 Chat+手机号后四位\n- README / project-solution:目录、迁移规范、用户 avatar_url 说明\n- 依赖:dotenv、@prisma/adapter-pg、pg

Made-with: Cursor
This commit is contained in:
2026-04-22 01:21:11 +08:00
parent 6cc89062e1
commit bc13417efd
12 changed files with 325 additions and 21 deletions

View File

@@ -4,6 +4,7 @@ import { LoggerModule } from 'nestjs-pino';
import { ClientAppModule } from './apps/client-app/client-app.module';
import { AdminAppModule } from './apps/admin-app/admin-app.module';
import { RedisModule } from './apps/shared-domain/cache/redis.module';
import { PrismaModule } from './prisma/prisma.module';
import configuration from './config/configuration';
import { validateEnv } from './config/validation';
@@ -25,6 +26,7 @@ import { validateEnv } from './config/validation';
: undefined,
},
}),
PrismaModule,
RedisModule,
ClientAppModule,
AdminAppModule,

View File

@@ -9,6 +9,7 @@ import { JwtService } from '@nestjs/jwt';
import { randomUUID } from 'crypto';
import { SmsService } from '@shared/sms/sms.service';
import { RedisService } from '@shared/cache/redis.service';
import { PrismaService } from '@prisma/prisma.service';
interface AccessPayload {
sub: string;
@@ -41,6 +42,8 @@ export class ClientAuthService {
private readonly smsService: SmsService,
@Inject(RedisService)
private readonly redisService: RedisService,
@Inject(PrismaService)
private readonly prisma: PrismaService,
) {}
async sendSmsCode(phone: string, scene: string) {
@@ -89,7 +92,21 @@ export class ClientAuthService {
}
await this.redisService.del(key);
const userId = `u_${phone}`;
const user = await this.prisma.user.upsert({
where: { phone },
update: {
updatedAt: new Date(),
},
create: {
phone,
nickname: `Chat${phone.slice(-4)}`,
},
});
if (user.status !== 1) {
throw new UnauthorizedException('用户已被禁用');
}
const userId = String(user.id);
const accessToken = await this.signAccessToken({
sub: userId,
phone,
@@ -108,6 +125,8 @@ export class ClientAuthService {
user: {
id: userId,
phone,
nickname: user.nickname ?? '',
avatarUrl: user.avatarUrl ?? '',
},
};
}
@@ -126,9 +145,23 @@ export class ClientAuthService {
throw new UnauthorizedException('token 类型错误');
}
let userId: bigint;
try {
userId = BigInt(payload.sub);
} catch {
throw new UnauthorizedException('refreshToken 用户标识无效');
}
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { id: true, phone: true, status: true },
});
if (!user || user.status !== 1) {
throw new UnauthorizedException('用户不存在或已禁用');
}
const accessToken = await this.signAccessToken({
sub: payload.sub,
phone: payload.sub.replace('u_', ''),
sub: String(user.id),
phone: user.phone,
role: 'client',
type: 'access',
});

View File

@@ -1,3 +1,4 @@
import 'dotenv/config';
import 'reflect-metadata';
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';

View File

@@ -1,18 +1,23 @@
import {
INestApplication,
Injectable,
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
// Prisma v7 默认导出 PrismaClient
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { PrismaClient } = require('@prisma/client');
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
constructor() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error('DATABASE_URL 未配置Prisma 无法初始化');
}
const adapter = new PrismaPg({ connectionString: databaseUrl });
super({
adapter,
});
}
async onModuleInit() {
await this.$connect();
}
@@ -20,11 +25,5 @@ export class PrismaService
async onModuleDestroy() {
await this.$disconnect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}