Files
telegram-shop/src/services/userService.js
NW 4657b1dfb5 feat: web admin panel + better-sqlite3 migration + Docker fixes
- 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
2026-06-22 10:54:01 +01:00

220 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;