Рефакторинг хранения пользователей: переход с JSON на PostgreSQL через Prisma

This commit is contained in:
Zwuck
2025-12-16 15:52:20 +05:00
parent ed95f7c115
commit 1ac12251f1
3 changed files with 41 additions and 80 deletions

View File

@@ -13,7 +13,7 @@ export class BotService {
async start(@Ctx() ctx: Context) {
const user = ctx.from;
if (user) {
this.usersService.create({
await this.usersService.create({
id: user.id,
fullName: `${user.first_name} ${user.last_name || ''}`.trim(),
});
@@ -41,7 +41,7 @@ export class BotService {
if (!user) return;
const hours = parseInt(ctx.match[1]);
this.usersService.update(user.id, { frequency: hours });
await this.usersService.update(user.id, { frequency: hours });
await ctx.answerCbQuery();
await ctx.editMessageText(`Отлично! Теперь я буду присылать цитаты каждые ${hours} ч.`);

View File

@@ -2,8 +2,9 @@ import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { InjectBot } from 'nestjs-telegraf';
import { Context, Telegraf } from 'telegraf';
import { UsersService, User } from '../users/users.service';
import { UsersService } from '../users/users.service';
import { QuotesService } from '../quotes/quotes.service';
import { User } from '@prisma/client';
@Injectable()
export class SchedulerService {
@@ -19,7 +20,7 @@ export class SchedulerService {
@Cron('0 * * * *')
async handleCron() {
this.logger.debug('Hourly cron triggered. Checking users...');
const users = this.usersService.findAll();
const users = await this.usersService.findAll();
const now = new Date();
for (const user of users) {
@@ -33,24 +34,11 @@ export class SchedulerService {
const diffMs = now.getTime() - lastSent.getTime();
const diffHours = diffMs / (1000 * 60 * 60);
// Allow a small buffer for timing execution, e.g. if diff is 0.99 hours but frequency is 1?
// Cron is exact hour. If last sent was 10:00:05 and now is 11:00:00. diff is slightly less than 1.
// But we update `lastQuoteSentAt` to `now` (execution time).
// If we update to `now`, then next time it will be exact hour difference.
// Let's use strict >= logic but maybe `frequency - 0.1`?
// Actually, if we update `lastQuoteSentAt` to `now.toISOString()` which is the cron time approx.
// It should be fine. If we missed by a second, it waits another hour.
// User won't mind waiting 1 more hour initially or we can be generous.
// Let's stick to `>= user.frequency`.
// Initial user `lastQuoteSentAt` is registration time.
// If reg at 10:30. Cron at 11:00. Diff 0.5. No quote.
// Cron at 12:00. Diff 1.5. Quote! (if freq 1).
// Seems correct usage.
if (diffHours >= user.frequency) {
await this.sendQuote(user);
// Updating user lastQuoteSentAt to now
this.usersService.update(user.id, { lastQuoteSentAt: now.toISOString() });
// Update expects telegramId. user.telegramId is BigInt.
await this.usersService.update(Number(user.telegramId), { lastQuoteSentAt: now });
}
}
@@ -62,9 +50,9 @@ export class SchedulerService {
message += `\n\n- ${quote.author}`;
}
await this.bot.telegram.sendMessage(user.id, message);
await this.bot.telegram.sendMessage(user.telegramId.toString(), message);
} catch (error) {
this.logger.error(`Failed to send quote to user ${user.id}: ${error.message}`);
this.logger.error(`Failed to send quote to user ${user.telegramId}: ${error.message}`);
}
}
}

View File

@@ -1,73 +1,46 @@
import { Injectable } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';
export interface User {
id: number;
fullName: string;
createdAt: string;
frequency: number; // in hours, default 1
lastQuoteSentAt: string; // ISO string
}
import { PrismaService } from '../prisma/prisma.service';
import { User } from '@prisma/client';
@Injectable()
export class UsersService {
private readonly filePath = path.resolve(__dirname, '..', 'data', 'users.json');
constructor(private readonly prisma: PrismaService) { }
private readUsers(): User[] {
if (!fs.existsSync(this.filePath)) {
return [];
}
const data = fs.readFileSync(this.filePath, 'utf8');
try {
return JSON.parse(data);
} catch (e) {
return [];
}
async findAll(): Promise<User[]> {
return this.prisma.user.findMany();
}
private writeUsers(users: User[]): void {
// Ensure directory exists
const dir = path.dirname(this.filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(this.filePath, JSON.stringify(users, null, 2), 'utf8');
async findOne(telegramId: number): Promise<User | null> {
return this.prisma.user.findUnique({
where: { telegramId: BigInt(telegramId) },
});
}
findAll(): User[] {
return this.readUsers();
async create(user: { id: number; fullName: string; username?: string }): Promise<User> {
return this.prisma.user.upsert({
where: { telegramId: BigInt(user.id) },
update: {
fio: user.fullName,
username: user.username,
},
create: {
telegramId: BigInt(user.id),
fio: user.fullName,
username: user.username,
frequency: 1,
// createdAt and lastQuoteSentAt have defaults
},
});
}
findOne(id: number): User | undefined {
return this.readUsers().find((u) => u.id === id);
}
async update(telegramId: number, updateData: Partial<User> & { frequency?: number }): Promise<User> {
// We need to be careful with BigInt serialization if we blindly spread updateData
// For now, we only expect frequency updates from the bot.
const data: any = { ...updateData };
create(user: Partial<User> & { id: number; fullName: string }): void {
const users = this.readUsers();
const existingUser = users.find((u) => u.id === user.id);
const newUser: User = {
...user,
createdAt: user.createdAt || new Date().toISOString(),
frequency: user.frequency || 1,
lastQuoteSentAt: user.lastQuoteSentAt || new Date().toISOString(),
};
if (!existingUser) {
users.push(newUser);
this.writeUsers(users);
} else {
// Option: update existing user if re-registering?
// For now, assume id persistence.
}
}
update(id: number, updateData: Partial<User>): void {
const users = this.readUsers();
const index = users.findIndex(u => u.id === id);
if (index !== -1) {
users[index] = { ...users[index], ...updateData };
this.writeUsers(users);
}
return this.prisma.user.update({
where: { telegramId: BigInt(telegramId) },
data: data,
});
}
}