// userService.js import db from "../config/database.js"; import Wallet from "../models/Wallet.js"; import WalletUtils from "../utils/walletUtils.js"; import logger from "../utils/logger.js"; const ALLOWED_USER_FIELDS = new Set([ 'telegram_id', 'username', 'country', 'city', 'district', 'status', 'total_balance', 'bonus_balance' ]); class UserService { // Функция для нормализации telegram_id static normalizeTelegramId(telegramId) { if (typeof telegramId === 'number') { // Если это число, преобразуем его в строку и удаляем ".0" return telegramId.toString().replace(/\.0$/, ''); } // Если это уже строка, возвращаем как есть return telegramId.toString(); } // Функция для валидации telegram_id static validateTelegramId(telegramId) { if (typeof telegramId !== 'string') { throw new Error('telegram_id должен быть строкой'); } if (telegramId.includes('.0')) { throw new Error('telegram_id не должен содержать ".0"'); } } static async createUser(userData) { try { // Нормализуем и валидируем telegram_id const normalizedTelegramId = this.normalizeTelegramId(userData?.telegram_id); this.validateTelegramId(normalizedTelegramId); // Обновляем значение telegram_id в объекте userData userData.telegram_id = normalizedTelegramId; // Проверяем, существует ли пользователь с таким telegram_id const existingUser = await this.getUserByTelegramId(normalizedTelegramId); if (existingUser) { logger.info({ telegramId: normalizedTelegramId }, 'User already exists'); return existingUser.id; } // Подготавливаем данные для вставки в базу данных const fields = Object.keys(userData).filter(key => ALLOWED_USER_FIELDS.has(key)); const values = fields.map(key => userData[key]); const marks = Array(fields.length).fill('?'); if (fields.length === 0) { throw new Error('No valid fields provided for user creation'); } const query = ` INSERT INTO users (${fields.join(', ')}) VALUES (${marks.join(', ')}) `; // Выполняем запрос к базе данных await db.runAsync('BEGIN TRANSACTION'); const result = await db.runAsync(query, values); await db.runAsync('COMMIT'); return result.lastInsertRowid; } catch (error) { await db.runAsync('ROLLBACK'); logger.error({ err: error }, 'Error creating user'); throw error; } } static async getUserByUserId(userId) { try { return await db.getAsync( 'SELECT * FROM users WHERE id = ?', [String(userId)] ); } catch (error) { logger.error({ err: error }, 'Error getting user'); throw error; } } static async getUserByTelegramId(telegramId) { try { const normalizedTelegramId = this.normalizeTelegramId(telegramId); return await db.getAsync( 'SELECT * FROM users WHERE telegram_id = ?', [normalizedTelegramId] ); } catch (error) { logger.error({ err: error }, 'Error getting user'); throw error; } } static async getDetailedUserByTelegramId(telegramId) { try { const normalizedTelegramId = this.normalizeTelegramId(telegramId); return await db.getAsync(` SELECT u.*, COUNT(DISTINCT p.id) as purchase_count, (SELECT COALESCE(SUM(p2.total_price), 0) FROM purchases p2 WHERE p2.user_id = u.id) as total_spent, COUNT(DISTINCT cw.id) as crypto_wallet_count, COUNT(DISTINCT cw2.id) as archived_wallet_count FROM users u LEFT JOIN purchases p ON u.id = p.user_id LEFT JOIN crypto_wallets cw ON u.id = cw.user_id AND cw.wallet_type NOT LIKE '%#_%' ESCAPE '#' LEFT JOIN crypto_wallets cw2 ON u.id = cw2.user_id AND cw2.wallet_type LIKE '%#_%' ESCAPE '#' WHERE u.telegram_id = ? GROUP BY u.id `, [normalizedTelegramId]); } catch (error) { logger.error({ err: error }, 'Error getting user stats'); throw error; } } static async updateUser(userId, newUserData) {} static async deleteUser() {} static async recalculateUserBalanceByTelegramId(telegramId) { const normalizedTelegramId = this.normalizeTelegramId(telegramId); const user = await this.getUserByTelegramId(normalizedTelegramId); if (!user) { return; } try { // Получаем все крипто-балансы пользователя const cryptoBalances = await db.allAsync( `SELECT wallet_type, balance FROM crypto_wallets WHERE user_id = ?`, [user.id] ); // Получаем актуальные курсы криптовалют const prices = await WalletUtils.getCryptoPrices(); // Пересчитываем балансы в доллары let totalCryptoBalance = 0; for (const wallet of cryptoBalances) { totalCryptoBalance += WalletUtils.convertToUsd(wallet.wallet_type, wallet.balance, prices); } // Получаем сумму всех покупок в крипте const cryptoPurchases = await db.getAsync( `SELECT SUM(total_price) as total_sum FROM purchases WHERE user_id = ? AND wallet_type LIKE 'crypto%'`, [user.id] ); // Вычитаем сумму покупок из общего крипто-баланса const remainingBalance = totalCryptoBalance - (cryptoPurchases?.total_sum || 0); // Обновляем поле total_balance в таблице users await db.runAsync( `UPDATE users SET total_balance = ? WHERE id = ?`, [remainingBalance, user.id] ); logger.debug({ userId: user.id, remainingBalance }, 'Updated total_balance'); } catch (error) { logger.error({ err: error }, 'Error recalculating user balance'); throw error; } } static async updateUserLocation(telegramId, country, city, district) { const normalizedTelegramId = this.normalizeTelegramId(telegramId); await db.runAsync( 'UPDATE users SET country = ?, city = ?, district = ? WHERE telegram_id = ?', [country, city, district, normalizedTelegramId] ); } static async updateUserStatus(telegramId, status) { const normalizedTelegramId = this.normalizeTelegramId(telegramId); try { await db.runAsync('BEGIN TRANSACTION'); // Update user status await db.runAsync('UPDATE users SET status = ? WHERE telegram_id = ?', [status, normalizedTelegramId]); // Commit transaction await db.runAsync('COMMIT'); } catch (e) { await db.runAsync("ROLLBACK"); logger.error({ err: e }, 'Error deleting user'); throw e; } } static async getUserBalance(userId) { try { const user = await this.getUserByUserId(userId); if (!user) { throw new Error('User not found'); } // Возвращаем сумму доступного крипто-баланса и бонусного баланса return user.total_balance + user.bonus_balance; } catch (error) { logger.error({ err: error }, 'Error getting user balance'); throw error; } } } export default UserService;