- Added Express.js admin panel on port 3001 (ADMIN_PORT env) - Dashboard: stats (users, products, purchases, revenue) - Users: list, details, ban/unban toggle - Products: CRUD by category - Wallets: list with balances - Purchases: history with filters - Audit log: view audit trail - Auth: token-based login with ADMIN_SECRET env var - Migrated sqlite3 → better-sqlite3 - database.js: async adapter (runAsync/allAsync/getAsync) - purchaseService.js: lastID → lastInsertRowid - userService.js: lastID → lastInsertRowid - Removed sqlite3 from package.json - Fixed: dotenv/config import added to index.js - Fixed: ENCRYPTION_KEY validation (32+ char hex) - Fixed: Dockerfile multi-stage build (no python needed) - Fixed: Docker DNS (network: host in build) - Fixed: docker-compose port 3001, healthcheck on 3001 - Added express, cookie-parser, pino-pretty, better-sqlite3 deps
220 lines
8.4 KiB
JavaScript
220 lines
8.4 KiB
JavaScript
// 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; |