feature/worker #111

Merged
primakov merged 190 commits from feature/worker into master 2025-12-05 16:59:42 +03:00
15 changed files with 630 additions and 184 deletions
Showing only changes of commit 284be82e1e - Show all commits

View File

@@ -1,5 +1,34 @@
const mongoose = require('mongoose');
// Явно определяем схему для файлов
const fileSchema = new mongoose.Schema({
id: {
type: String,
required: true
},
name: {
type: String,
required: true
},
url: {
type: String,
required: true
},
type: {
type: String,
required: true
},
size: {
type: Number,
required: true
},
storagePath: String,
uploadedAt: {
type: Date,
default: Date.now
}
}, { _id: false });
const buyProductSchema = new mongoose.Schema({
companyId: {
type: String,
@@ -24,18 +53,7 @@ const buyProductSchema = new mongoose.Schema({
type: String,
default: 'шт'
},
files: [{
id: String,
name: String,
url: String,
type: String,
size: Number,
storagePath: String,
uploadedAt: {
type: Date,
default: Date.now
}
}],
files: [fileSchema],
acceptedBy: [{
companyId: {
type: mongoose.Schema.Types.ObjectId,

View File

@@ -22,12 +22,12 @@ const requestSchema = new mongoose.Schema({
required: true
},
files: [{
id: String,
name: String,
url: String,
type: String,
size: Number,
storagePath: String,
id: { type: String },
name: { type: String },
url: { type: String },
type: { type: String },
size: { type: Number },
storagePath: { type: String },
uploadedAt: {
type: Date,
default: Date.now
@@ -47,12 +47,12 @@ const requestSchema = new mongoose.Schema({
default: null
},
responseFiles: [{
id: String,
name: String,
url: String,
type: String,
size: Number,
storagePath: String,
id: { type: String },
name: { type: String },
url: { type: String },
type: { type: String },
size: { type: Number },
storagePath: { type: String },
uploadedAt: {
type: Date,
default: Date.now

View File

@@ -7,7 +7,7 @@ const Request = require('../models/Request');
const BuyProduct = require('../models/BuyProduct');
const Message = require('../models/Message');
const Review = require('../models/Review');
const mongoose = require('mongoose');
const mongoose = require('../../../utils/mongoose');
const { Types } = mongoose;
const PRESET_COMPANY_ID = new Types.ObjectId('68fe2ccda3526c303ca06796');
@@ -116,9 +116,6 @@ const waitForDatabaseConnection = async () => {
const verifyAuth = async () => {
try {
if (!mongoose.connection.db) {
return false;
}
await mongoose.connection.db.admin().command({ listDatabases: 1 });
return true;
} catch (error) {
@@ -139,15 +136,17 @@ const waitForDatabaseConnection = async () => {
}
try {
// Ожидаем подключения (подключение происходит автоматически через server/utils/mongoose.ts)
await new Promise(resolve => setTimeout(resolve, 500));
if (mongoose.connection.readyState === 1) {
const authed = await verifyAuth();
if (authed) {
return;
}
const connection = await connectDB();
if (!connection) {
break;
}
const authed = await verifyAuth();
if (authed) {
return;
}
await mongoose.connection.close().catch(() => {});
} catch (error) {
if (!isAuthFailure(error)) {
throw error;
@@ -218,8 +217,12 @@ const initializeTestUser = async () => {
} catch (error) {
console.error('Error initializing test data:', error.message);
if (error?.code === 13 || /auth/i.test(error?.message || '')) {
if (process.env.DEV === 'true') {
console.error('Auth error detected. Connection managed by server/utils/mongoose.ts');
try {
await connectDB();
} catch (connectError) {
if (process.env.DEV === 'true') {
console.error('Failed to re-connect after auth error:', connectError.message);
}
}
}
}

View File

@@ -1,10 +1,11 @@
const express = require('express')
const fs = require('fs')
const path = require('path')
const router = express.Router()
const BuyDocument = require('../models/BuyDocument')
// Create remote-assets/docs directory if it doesn't exist
const docsDir = 'server/remote-assets/docs'
const docsDir = 'server/routers/remote-assets/docs'
if (!fs.existsSync(docsDir)) {
fs.mkdirSync(docsDir, { recursive: true })
}

View File

@@ -2,9 +2,10 @@ const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const BuyProduct = require('../models/BuyProduct');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
const UPLOADS_ROOT = 'server/remote-assets/uploads/buy-products';
const UPLOADS_ROOT = 'server/routers/remote-assets/uploads/buy-products';
const ensureDirectory = (dirPath) => {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
@@ -23,18 +24,6 @@ const ALLOWED_MIME_TYPES = new Set([
'text/csv',
]);
const getExtension = (filename) => {
const lastDot = filename.lastIndexOf('.');
return lastDot > 0 ? filename.slice(lastDot) : '';
};
const getBasename = (filename) => {
const lastDot = filename.lastIndexOf('.');
const name = lastDot > 0 ? filename.slice(0, lastDot) : filename;
const lastSlash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'));
return lastSlash >= 0 ? name.slice(lastSlash + 1) : name;
};
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const productId = req.params.id || 'common';
@@ -43,10 +32,12 @@ const storage = multer.diskStorage({
cb(null, productDir);
},
filename: (req, file, cb) => {
const originalExtension = getExtension(file.originalname);
const baseName = getBasename(file.originalname)
.replace(/[^a-zA-Z0-9-_]+/g, '_')
.toLowerCase();
// Исправляем кодировку имени файла из Latin1 в UTF-8
const fixedName = Buffer.from(file.originalname, 'latin1').toString('utf8');
const originalExtension = path.extname(fixedName) || '';
const baseName = path
.basename(fixedName, originalExtension)
.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '_'); // Убираем только недопустимые символы Windows, оставляем кириллицу
cb(null, `${Date.now()}_${baseName}${originalExtension}`);
},
});
@@ -241,7 +232,16 @@ router.post('/:id/files', verifyToken, handleSingleFileUpload, async (req, res)
}
// Только владелец товара может добавить файл
if (product.companyId.toString() !== req.companyId.toString()) {
const productCompanyId = product.companyId?.toString() || product.companyId;
const requestCompanyId = req.companyId?.toString() || req.companyId;
console.log('[BuyProducts] Comparing company IDs:', {
productCompanyId,
requestCompanyId,
match: productCompanyId === requestCompanyId
});
if (productCompanyId !== requestCompanyId) {
return res.status(403).json({ error: 'Not authorized' });
}
@@ -253,28 +253,75 @@ router.post('/:id/files', verifyToken, handleSingleFileUpload, async (req, res)
return res.status(400).json({ error: 'File is required' });
}
const relativePath = `buy-products/${id}/${req.file.filename}`;
// Исправляем кодировку имени файла из Latin1 в UTF-8
const fixedFileName = Buffer.from(req.file.originalname, 'latin1').toString('utf8');
// Извлекаем timestamp из имени файла, созданного multer (формат: {timestamp}_{name}.ext)
const fileTimestamp = req.file.filename.split('_')[0];
// storagePath относительно UPLOADS_ROOT (который уже включает 'buy-products')
const relativePath = `${id}/${req.file.filename}`;
const file = {
id: `file-${Date.now()}`,
name: req.file.originalname,
url: `/uploads/${relativePath}`,
id: `file-${fileTimestamp}`, // Используем тот же timestamp, что и в имени файла
name: fixedFileName,
url: `/uploads/buy-products/${relativePath}`,
type: req.file.mimetype,
size: req.file.size,
uploadedAt: new Date(),
storagePath: relativePath,
};
product.files.push(file);
await product.save();
console.log('[BuyProducts] Adding file to product:', {
productId: id,
fileName: file.name,
fileSize: file.size,
filePath: relativePath
});
console.log('[BuyProducts] File object:', JSON.stringify(file, null, 2));
// Используем findByIdAndUpdate вместо save() для избежания проблем с валидацией
let updatedProduct;
try {
console.log('[BuyProducts] Calling findByIdAndUpdate with id:', id);
updatedProduct = await BuyProduct.findByIdAndUpdate(
id,
{
$push: { files: file },
$set: { updatedAt: new Date() }
},
{ new: true, runValidators: false }
);
console.log('[BuyProducts] findByIdAndUpdate completed');
} catch (updateError) {
console.error('[BuyProducts] findByIdAndUpdate error:', {
message: updateError.message,
name: updateError.name,
code: updateError.code
});
throw updateError;
}
if (!updatedProduct) {
throw new Error('Failed to update product with file');
}
console.log('[BuyProducts] File added successfully to product:', id);
log('[BuyProducts] File added to product:', id, file.name);
res.json(product);
res.json(updatedProduct);
} catch (error) {
console.error('[BuyProducts] Error adding file:', error.message);
console.error('[BuyProducts] Error stack:', error.stack);
console.error('[BuyProducts] Error name:', error.name);
if (error.errors) {
console.error('[BuyProducts] Validation errors:', JSON.stringify(error.errors, null, 2));
}
res.status(500).json({
error: 'Internal server error',
message: error.message,
details: error.errors || {},
});
}
});
@@ -303,7 +350,7 @@ router.delete('/:id/files/:fileId', verifyToken, async (req, res) => {
await product.save();
const storedPath = fileToRemove.storagePath || fileToRemove.url.replace(/^\/uploads\//, '');
const absolutePath = `server/remote-assets/uploads/${storedPath}`;
const absolutePath = `server/routers/remote-assets/uploads/${storedPath}`;
fs.promises.unlink(absolutePath).catch((unlinkError) => {
if (unlinkError && unlinkError.code !== 'ENOENT') {
@@ -391,4 +438,65 @@ router.get('/:id/acceptances', verifyToken, async (req, res) => {
}
});
// GET /buy-products/download/:id/:fileId - скачать файл
router.get('/download/:id/:fileId', verifyToken, async (req, res) => {
try {
console.log('[BuyProducts] Download request received:', {
productId: req.params.id,
fileId: req.params.fileId,
userId: req.userId,
companyId: req.companyId,
headers: req.headers.authorization
});
const { id, fileId } = req.params;
const product = await BuyProduct.findById(id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
const file = product.files.find((f) => f.id === fileId);
if (!file) {
return res.status(404).json({ error: 'File not found' });
}
// Создаем абсолютный путь к файлу
const filePath = path.resolve(UPLOADS_ROOT, file.storagePath);
console.log('[BuyProducts] Trying to download file:', {
fileId: file.id,
fileName: file.name,
storagePath: file.storagePath,
absolutePath: filePath,
exists: fs.existsSync(filePath)
});
// Проверяем существование файла
if (!fs.existsSync(filePath)) {
console.error('[BuyProducts] File not found on disk:', filePath);
return res.status(404).json({ error: 'File not found on disk' });
}
// Устанавливаем правильные заголовки для скачивания с поддержкой кириллицы
const encodedFileName = encodeURIComponent(file.name);
res.setHeader('Content-Type', file.type || 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`);
res.setHeader('Content-Length', file.size);
// Отправляем файл
res.sendFile(filePath, (err) => {
if (err) {
console.error('[BuyProducts] Error sending file:', err.message);
if (!res.headersSent) {
res.status(500).json({ error: 'Error downloading file' });
}
}
});
} catch (error) {
console.error('[BuyProducts] Error downloading file:', error.message);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;

View File

@@ -5,7 +5,8 @@ const Company = require('../models/Company');
const Experience = require('../models/Experience');
const Request = require('../models/Request');
const Message = require('../models/Message');
const { Types } = require('mongoose');
const mongoose = require('../../../utils/mongoose');
const { Types } = mongoose;
// GET /my/info - получить мою компанию (требует авторизации) - ДОЛЖНО быть ПЕРЕД /:id
router.get('/my/info', verifyToken, async (req, res) => {

View File

@@ -2,7 +2,8 @@ const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Experience = require('../models/Experience');
const { Types } = require('mongoose');
const mongoose = require('../../../utils/mongoose');
const { Types } = mongoose;
// GET /experience - Получить список опыта работы компании
router.get('/', verifyToken, async (req, res) => {

View File

@@ -21,21 +21,23 @@ router.get('/aggregates', verifyToken, async (req, res) => {
const companyId = user.companyId.toString();
const [docsCount, acceptsCount, requestsCount] = await Promise.all([
BuyProduct.countDocuments({ companyId }),
Request.countDocuments({
$or: [
{ senderCompanyId: companyId, status: 'accepted' },
{ recipientCompanyId: companyId, status: 'accepted' }
]
}),
Request.countDocuments({
$or: [
{ senderCompanyId: companyId },
{ recipientCompanyId: companyId }
]
})
]);
// Получить все BuyProduct для подсчета файлов и акцептов
const buyProducts = await BuyProduct.find({ companyId });
// Подсчет документов - сумма всех файлов во всех BuyProduct
const docsCount = buyProducts.reduce((total, product) => {
return total + (product.files ? product.files.length : 0);
}, 0);
// Подсчет акцептов - сумма всех acceptedBy во всех BuyProduct
const acceptsCount = buyProducts.reduce((total, product) => {
return total + (product.acceptedBy ? product.acceptedBy.length : 0);
}, 0);
// Подсчет исходящих запросов (только отправленные этой компанией)
const requestsCount = await Request.countDocuments({
senderCompanyId: companyId
});
res.json({
docsCount,

View File

@@ -2,6 +2,8 @@ const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Message = require('../models/Message');
const mongoose = require('../../../utils/mongoose');
const { ObjectId } = mongoose.Types;
// Функция для логирования с проверкой DEV переменной
const log = (message, data = '') => {
@@ -18,7 +20,6 @@ const log = (message, data = '') => {
router.get('/threads', verifyToken, async (req, res) => {
try {
const companyId = req.companyId;
const { ObjectId } = require('mongoose').Types;
log('[Messages] Fetching threads for companyId:', companyId, 'type:', typeof companyId);
@@ -146,7 +147,6 @@ router.post('/:threadId', verifyToken, async (req, res) => {
// Найти recipientCompanyId по ObjectId если нужно
let recipientObjectId = recipientCompanyId;
const { ObjectId } = require('mongoose').Types;
try {
if (typeof recipientCompanyId === 'string' && ObjectId.isValid(recipientCompanyId)) {
recipientObjectId = new ObjectId(recipientCompanyId);
@@ -210,7 +210,6 @@ router.post('/admin/migrate-fix-recipients', async (req, res) => {
// If recipientCompanyId is not set or wrong - fix it
if (!message.recipientCompanyId || message.recipientCompanyId.toString() !== expectedRecipient) {
const { ObjectId } = require('mongoose').Types;
let recipientObjectId = expectedRecipient;
try {
if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) {

View File

@@ -3,8 +3,10 @@ const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Request = require('../models/Request');
const BuyProduct = require('../models/BuyProduct');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
const mongoose = require('../../../utils/mongoose');
// Функция для логирования с проверкой DEV переменной
const log = (message, data = '') => {
@@ -17,7 +19,7 @@ const log = (message, data = '') => {
}
};
const REQUESTS_UPLOAD_ROOT = 'server/remote-assets/uploads/requests';
const REQUESTS_UPLOAD_ROOT = 'server/routers/remote-assets/uploads/requests';
const ensureDirectory = (dirPath) => {
if (!fs.existsSync(dirPath)) {
@@ -37,28 +39,17 @@ const ALLOWED_REQUEST_MIME_TYPES = new Set([
'text/csv',
]);
const getExtension = (filename) => {
const lastDot = filename.lastIndexOf('.');
return lastDot > 0 ? filename.slice(lastDot) : '';
};
const getBasename = (filename) => {
const lastDot = filename.lastIndexOf('.');
const name = lastDot > 0 ? filename.slice(0, lastDot) : filename;
const lastSlash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'));
return lastSlash >= 0 ? name.slice(lastSlash + 1) : name;
};
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const subfolder = req.requestUploadSubfolder || '';
const destinationDir = subfolder ? `${REQUESTS_UPLOAD_ROOT}/${subfolder}` : REQUESTS_UPLOAD_ROOT;
const destinationDir = `${REQUESTS_UPLOAD_ROOT}/${subfolder}`;
ensureDirectory(destinationDir);
cb(null, destinationDir);
},
filename: (req, file, cb) => {
const extension = getExtension(file.originalname);
const baseName = getBasename(file.originalname)
const extension = path.extname(file.originalname) || '';
const baseName = path
.basename(file.originalname, extension)
.replace(/[^a-zA-Z0-9-_]+/g, '_')
.toLowerCase();
cb(null, `${Date.now()}_${baseName}${extension}`);
@@ -107,7 +98,7 @@ const cleanupUploadedFiles = async (req) => {
const subfolder = req.requestUploadSubfolder || '';
const removalTasks = req.files.map((file) => {
const filePath = subfolder ? `${REQUESTS_UPLOAD_ROOT}/${subfolder}/${file.filename}` : `${REQUESTS_UPLOAD_ROOT}/${file.filename}`;
const filePath = `${REQUESTS_UPLOAD_ROOT}/${subfolder}/${file.filename}`;
return fs.promises.unlink(filePath).catch((error) => {
if (error.code !== 'ENOENT') {
console.error('[Requests] Failed to cleanup uploaded file:', error.message);
@@ -125,7 +116,7 @@ const mapFilesToMetadata = (req) => {
const subfolder = req.requestUploadSubfolder || '';
return req.files.map((file) => {
const relativePath = subfolder ? `requests/${subfolder}/${file.filename}` : `requests/${file.filename}`;
const relativePath = `requests/${subfolder}/${file.filename}`;
return {
id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: file.originalname,
@@ -169,7 +160,7 @@ const removeStoredFiles = async (files = []) => {
const tasks = files
.filter((file) => file && file.storagePath)
.map((file) => {
const absolutePath = `server/remote-assets/uploads/${file.storagePath}`;
const absolutePath = `server/routers/remote-assets/uploads/${file.storagePath}`;
return fs.promises.unlink(absolutePath).catch((error) => {
if (error.code !== 'ENOENT') {
console.error('[Requests] Failed to remove stored file:', error.message);
@@ -255,24 +246,61 @@ router.post(
return res.status(400).json({ error: 'At least one recipient is required' });
}
if (!subject && productId) {
let uploadedFiles = mapFilesToMetadata(req);
console.log('========================');
console.log('[Requests] Initial uploadedFiles:', uploadedFiles.length);
console.log('[Requests] ProductId:', productId);
// Если есть productId, получаем данные товара
if (productId) {
try {
const product = await BuyProduct.findById(productId);
console.log('[Requests] Product found:', product ? product.name : 'null');
console.log('[Requests] Product files count:', product?.files?.length || 0);
if (product && product.files) {
console.log('[Requests] Product files:', JSON.stringify(product.files, null, 2));
}
if (product) {
subject = product.name;
// Берем subject из товара, если не указан
if (!subject) {
subject = product.name;
}
// Если файлы не загружены вручную, используем файлы из товара
if (uploadedFiles.length === 0 && product.files && product.files.length > 0) {
console.log('[Requests] ✅ Copying files from product...');
// Копируем файлы из товара, изменяя путь для запроса
uploadedFiles = product.files.map(file => ({
id: file.id || `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: file.name,
url: file.url,
type: file.type,
size: file.size,
uploadedAt: file.uploadedAt || new Date(),
storagePath: file.storagePath || file.url.replace('/uploads/', ''),
}));
console.log('[Requests] ✅ Using', uploadedFiles.length, 'files from product:', productId);
console.log('[Requests] ✅ Copied files:', JSON.stringify(uploadedFiles, null, 2));
} else {
console.log('[Requests] ❌ NOT copying files. uploadedFiles.length:', uploadedFiles.length, 'product.files.length:', product.files?.length || 0);
}
}
} catch (lookupError) {
console.error('[Requests] Failed to lookup product for subject:', lookupError.message);
console.error('[Requests] Failed to lookup product:', lookupError.message);
console.error(lookupError.stack);
}
}
console.log('[Requests] Final uploadedFiles for saving:', JSON.stringify(uploadedFiles, null, 2));
console.log('========================');
if (!subject) {
await cleanupUploadedFiles(req);
return res.status(400).json({ error: 'Subject is required' });
}
const uploadedFiles = mapFilesToMetadata(req);
const results = [];
for (const recipientCompanyId of recipients) {
try {
@@ -331,9 +359,17 @@ router.put(
async (req, res) => {
try {
const { id } = req.params;
console.log('[Requests] PUT /requests/:id called with id:', id);
console.log('[Requests] Request body:', req.body);
console.log('[Requests] Files:', req.files);
console.log('[Requests] CompanyId:', req.companyId);
const responseText = (req.body.response || '').trim();
const statusRaw = (req.body.status || 'accepted').toLowerCase();
const status = statusRaw === 'rejected' ? 'rejected' : 'accepted';
console.log('[Requests] Response text:', responseText);
console.log('[Requests] Status:', status);
if (req.invalidFiles && req.invalidFiles.length > 0) {
await cleanupUploadedFiles(req);
@@ -361,6 +397,8 @@ router.put(
}
const uploadedResponseFiles = mapFilesToMetadata(req);
console.log('[Requests] Uploaded response files count:', uploadedResponseFiles.length);
console.log('[Requests] Uploaded response files:', JSON.stringify(uploadedResponseFiles, null, 2));
if (uploadedResponseFiles.length > 0) {
await removeStoredFiles(request.responseFiles || []);
@@ -372,18 +410,126 @@ router.put(
request.respondedAt = new Date();
request.updatedAt = new Date();
await request.save();
let savedRequest;
try {
savedRequest = await request.save();
log('[Requests] Request responded:', id);
} catch (saveError) {
console.error('[Requests] Mongoose save failed, trying direct MongoDB update:', saveError.message);
// Fallback: использовать MongoDB драйвер напрямую
const updateData = {
response: responseText,
status: status,
respondedAt: new Date(),
updatedAt: new Date()
};
if (uploadedResponseFiles.length > 0) {
updateData.responseFiles = uploadedResponseFiles;
}
const result = await mongoose.connection.collection('requests').findOneAndUpdate(
{ _id: new mongoose.Types.ObjectId(id) },
{ $set: updateData },
{ returnDocument: 'after' }
);
if (!result) {
throw new Error('Failed to update request');
}
savedRequest = result;
log('[Requests] Request responded via direct MongoDB update:', id);
}
log('[Requests] Request responded:', id);
res.json(request);
res.json(savedRequest);
} catch (error) {
console.error('[Requests] Error responding to request:', error.message);
console.error('[Requests] Error stack:', error.stack);
if (error.name === 'ValidationError') {
console.error('[Requests] Validation errors:', JSON.stringify(error.errors, null, 2));
}
res.status(500).json({ error: error.message });
}
}
);
// GET /requests/download/:id/:fileId - скачать файл ответа
router.get('/download/:id/:fileId', verifyToken, async (req, res) => {
try {
console.log('[Requests] Download request received:', {
requestId: req.params.id,
fileId: req.params.fileId,
userId: req.userId,
companyId: req.companyId,
});
const { id, fileId } = req.params;
const request = await Request.findById(id);
if (!request) {
return res.status(404).json({ error: 'Request not found' });
}
// Проверяем, что пользователь имеет доступ к запросу (отправитель или получатель)
if (request.senderCompanyId !== req.companyId && request.recipientCompanyId !== req.companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
// Ищем файл в responseFiles или в обычных files
let file = request.responseFiles?.find((f) => f.id === fileId);
if (!file) {
file = request.files?.find((f) => f.id === fileId);
}
if (!file) {
return res.status(404).json({ error: 'File not found' });
}
// Создаем абсолютный путь к файлу
// Если storagePath не начинается с 'requests/', значит это файл из buy-products
let fullPath = file.storagePath;
if (!fullPath.startsWith('requests/')) {
fullPath = `buy-products/${fullPath}`;
}
const filePath = path.resolve(`server/routers/remote-assets/uploads/${fullPath}`);
console.log('[Requests] Trying to download file:', {
fileId: file.id,
fileName: file.name,
storagePath: file.storagePath,
absolutePath: filePath,
exists: fs.existsSync(filePath),
});
// Проверяем существование файла
if (!fs.existsSync(filePath)) {
console.error('[Requests] File not found on disk:', filePath);
return res.status(404).json({ error: 'File not found on disk' });
}
// Устанавливаем правильные заголовки для скачивания с поддержкой кириллицы
const encodedFileName = encodeURIComponent(file.name);
res.setHeader('Content-Type', file.type || 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`);
res.setHeader('Content-Length', file.size);
// Отправляем файл
res.sendFile(filePath, (err) => {
if (err) {
console.error('[Requests] Error sending file:', err.message);
if (!res.headersSent) {
res.status(500).json({ error: 'Error sending file' });
}
} else {
log('[Requests] File downloaded:', file.name);
}
});
} catch (error) {
console.error('[Requests] Error downloading file:', error.message);
if (!res.headersSent) {
res.status(500).json({ error: error.message });
}
}
});
// DELETE /requests/:id - удалить запрос
router.delete('/:id', verifyToken, async (req, res) => {
try {

View File

@@ -54,10 +54,13 @@ router.get('/recommendations', verifyToken, async (req, res) => {
// GET /search - Поиск компаний
router.get('/', verifyToken, async (req, res) => {
try {
console.log('[Search] === NEW VERSION WITH FIXED SIZE FILTER ===');
const {
query = '',
page = 1,
limit = 10,
offset, // Добавляем поддержку offset для точной пагинации
industries,
companySize,
geography,
@@ -65,8 +68,12 @@ router.get('/', verifyToken, async (req, res) => {
hasReviews,
hasAcceptedDocs,
sortBy = 'relevance',
sortOrder = 'desc'
sortOrder = 'desc',
minEmployees, // Кастомный фильтр: минимум сотрудников
maxEmployees // Кастомный фильтр: максимум сотрудников
} = req.query;
console.log('[Search] Filters:', { minEmployees, maxEmployees, companySize });
// Получить компанию пользователя, чтобы исключить её из результатов
const User = require('../models/User');
@@ -135,12 +142,99 @@ router.get('/', verifyToken, async (req, res) => {
}
}
// Фильтр по размеру компании
if (companySize) {
const sizeList = Array.isArray(companySize) ? companySize : [companySize];
if (sizeList.length > 0) {
filters.push({ companySize: { $in: sizeList } });
// Функция для парсинга диапазона из строки вида "51-250" или "500+"
const parseEmployeeRange = (sizeStr) => {
if (sizeStr.includes('+')) {
const min = parseInt(sizeStr.replace('+', ''));
return { min, max: Infinity };
}
const parts = sizeStr.split('-');
return {
min: parseInt(parts[0]),
max: parts[1] ? parseInt(parts[1]) : parseInt(parts[0])
};
};
// Функция для проверки пересечения двух диапазонов
const rangesOverlap = (range1, range2) => {
return range1.min <= range2.max && range1.max >= range2.min;
};
// Фильтр по размеру компании (чекбоксы) или кастомный диапазон
// Важно: этот фильтр должен получить все компании для корректной работы пересечения диапазонов
let sizeFilteredIds = null;
if ((companySize && companySize.length > 0) || minEmployees || maxEmployees) {
// Получаем все компании (без других фильтров, так как размер компании - это property-based фильтр)
const allCompanies = await Company.find({});
log('[Search] Employee size filter - checking companies:', allCompanies.length);
let matchingIds = [];
// Если есть кастомный диапазон - используем его
if (minEmployees || maxEmployees) {
const customRange = {
min: minEmployees ? parseInt(minEmployees, 10) : 0,
max: maxEmployees ? parseInt(maxEmployees, 10) : Infinity
};
log('[Search] Custom employee range filter:', customRange);
matchingIds = allCompanies
.filter(company => {
if (!company.companySize) {
log('[Search] Company has no size:', company.fullName);
return false;
}
const companyRange = parseEmployeeRange(company.companySize);
const overlaps = rangesOverlap(companyRange, customRange);
log('[Search] Checking overlap:', {
company: company.fullName,
companyRange,
customRange,
overlaps
});
return overlaps;
})
.map(c => c._id);
log('[Search] Matching companies by custom range:', matchingIds.length);
}
// Иначе используем чекбоксы
else if (companySize && companySize.length > 0) {
const sizeList = Array.isArray(companySize) ? companySize : [companySize];
log('[Search] Company size checkboxes filter:', sizeList);
matchingIds = allCompanies
.filter(company => {
if (!company.companySize) {
return false;
}
const companyRange = parseEmployeeRange(company.companySize);
// Проверяем пересечение с любым из выбранных диапазонов
const matches = sizeList.some(selectedSize => {
const filterRange = parseEmployeeRange(selectedSize);
const overlaps = rangesOverlap(companyRange, filterRange);
log('[Search] Check:', company.fullName, companyRange, 'vs', filterRange, '=', overlaps);
return overlaps;
});
return matches;
})
.map(c => c._id);
log('[Search] Matching companies by size checkboxes:', matchingIds.length);
}
// Сохраняем ID для дальнейшей фильтрации
sizeFilteredIds = matchingIds;
log('[Search] Size filtered IDs count:', sizeFilteredIds.length);
}
// Фильтр по географии
@@ -170,13 +264,25 @@ router.get('/', verifyToken, async (req, res) => {
filters.push({ verified: true });
}
// Применяем фильтр по размеру компании (если был задан)
if (sizeFilteredIds !== null) {
if (sizeFilteredIds.length > 0) {
filters.push({ _id: { $in: sizeFilteredIds } });
log('[Search] Applied size filter, IDs:', sizeFilteredIds.length);
} else {
// Если нет подходящих компаний по размеру, возвращаем пустой результат
filters.push({ _id: null });
log('[Search] No companies match size criteria');
}
}
// Комбинировать все фильтры
let filter = filters.length > 0 ? { $and: filters } : {};
// Пагинация
const pageNum = parseInt(page) || 1;
// Пагинация - используем offset если передан, иначе вычисляем из page
const limitNum = parseInt(limit) || 10;
const skip = (pageNum - 1) * limitNum;
const skip = offset !== undefined ? parseInt(offset) : ((parseInt(page) || 1) - 1) * limitNum;
const pageNum = offset !== undefined ? Math.floor(skip / limitNum) + 1 : parseInt(page) || 1;
// Сортировка
let sortOptions = {};
@@ -228,3 +334,4 @@ router.get('/', verifyToken, async (req, res) => {
module.exports = router;

View File

@@ -1,18 +1,18 @@
const mongoose = require('mongoose');
const mongoose = require('../../../utils/mongoose');
const { ObjectId } = mongoose.Types;
const Message = require('../models/Message');
require('dotenv').config({ path: '../../.env' });
const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db';
require('dotenv').config();
async function migrateMessages() {
try {
console.log('[Migration] Connecting to MongoDB...');
await mongoose.connect(mongoUrl, {
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 5000,
});
// Подключение к MongoDB происходит через server/utils/mongoose.ts
console.log('[Migration] Checking MongoDB connection...');
if (mongoose.connection.readyState !== 1) {
console.log('[Migration] Waiting for MongoDB connection...');
await new Promise((resolve) => {
mongoose.connection.once('connected', resolve);
});
}
console.log('[Migration] Connected to MongoDB');
// Найти все сообщения
@@ -54,7 +54,6 @@ async function migrateMessages() {
console.log(' Expected:', expectedRecipient);
// Конвертируем в ObjectId если нужно
const { ObjectId } = require('mongoose').Types;
let recipientObjectId = expectedRecipient;
try {
if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) {

View File

@@ -1,57 +1,57 @@
const mongoose = require('mongoose');
const mongoose = require('../../../utils/mongoose');
require('dotenv').config();
// Импорт моделей - прямые пути без path.join и __dirname
// Импорт моделей
const User = require('../models/User');
const Company = require('../models/Company');
const Request = require('../models/Request');
const primaryUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db';
const fallbackUri =
process.env.MONGODB_AUTH_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin';
const connectWithFallback = async () => {
// Сначала пробуем FALLBACK (с аутентификацией)
try {
console.log('\n📡 Подключение к MongoDB (с аутентификацией)...');
await mongoose.connect(fallbackUri, { useNewUrlParser: true, useUnifiedTopology: true });
console.log('✅ Подключено к MongoDB');
// Подключение к MongoDB происходит через server/utils/mongoose.ts
// Проверяем, подключено ли уже
const ensureConnection = async () => {
if (mongoose.connection.readyState === 1) {
console.log('✅ MongoDB уже подключено');
return;
} catch (fallbackError) {
console.log('❌ Ошибка подключения с аутентификацией:', fallbackError.message);
}
// Если не получилось, пробуем без аутентификации
try {
console.log('\n📡 Подключение к MongoDB (без аутентификации)...');
await mongoose.connect(primaryUri, { useNewUrlParser: true, useUnifiedTopology: true });
console.log('✅ Подключено к MongoDB');
} catch (primaryError) {
console.error('❌ Не удалось подключиться к MongoDB:', primaryError.message);
throw primaryError;
}
console.log('⏳ Ожидание подключения к MongoDB...');
await new Promise((resolve) => {
if (mongoose.connection.readyState === 1) {
resolve();
} else {
mongoose.connection.once('connected', resolve);
}
});
console.log('✅ Подключено к MongoDB');
};
const recreateTestUser = async () => {
try {
await connectWithFallback();
await ensureConnection();
const presetCompanyId = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06796');
const presetUserEmail = 'admin@test-company.ru';
const presetCompanyId2 = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06797');
const presetUserEmail2 = 'manager@partner-company.ru';
// Удалить старого тестового пользователя
console.log('🗑️ Удаление старого тестового пользователя...');
const oldUser = await User.findOne({ email: presetUserEmail });
if (oldUser) {
// Удалить связанную компанию
if (oldUser.companyId) {
await Company.findByIdAndDelete(oldUser.companyId);
console.log(' ✓ Старая компания удалена');
// Удалить старых тестовых пользователей
console.log('🗑️ Удаление старых тестовых пользователей...');
const testEmails = [presetUserEmail, presetUserEmail2];
for (const email of testEmails) {
const oldUser = await User.findOne({ email });
if (oldUser) {
// Удалить связанную компанию
if (oldUser.companyId) {
await Company.findByIdAndDelete(oldUser.companyId);
console.log(` ✓ Старая компания для ${email} удалена`);
}
await User.findByIdAndDelete(oldUser._id);
console.log(` ✓ Старый пользователь ${email} удален`);
} else {
console.log(` Пользователь ${email} не найден`);
}
await User.findByIdAndDelete(oldUser._id);
console.log(' ✓ Старый пользователь удален');
} else {
console.log(' Старый пользователь не найден');
}
// Создать новую компанию с правильной кодировкой UTF-8
@@ -82,8 +82,8 @@ const recreateTestUser = async () => {
});
console.log(' ✓ Компания создана:', company.fullName);
// Создать нового пользователя с правильной кодировкой UTF-8
console.log('\n👤 Создание тестового пользователя...');
// Создать первого пользователя с правильной кодировкой UTF-8
console.log('\n👤 Создание первого тестового пользователя...');
const user = await User.create({
email: presetUserEmail,
password: 'SecurePass123!',
@@ -95,18 +95,71 @@ const recreateTestUser = async () => {
});
console.log(' ✓ Пользователь создан:', user.firstName, user.lastName);
// Создать вторую компанию
console.log('\n🏢 Создание второй тестовой компании...');
const company2 = await Company.create({
_id: presetCompanyId2,
fullName: 'ООО "Партнер"',
shortName: 'Партнер',
inn: '9876543210',
ogrn: '1089876543210',
legalForm: 'ООО',
industry: 'Торговля',
companySize: '11-50',
website: 'https://partner-company.ru',
phone: '+7 (495) 987-65-43',
email: 'info@partner-company.ru',
description: 'Надежный партнер для бизнеса',
legalAddress: 'г. Санкт-Петербург, пр. Невский, д. 100',
actualAddress: 'г. Санкт-Петербург, пр. Невский, д. 100',
foundedYear: 2018,
employeeCount: '11-50',
revenue: 'До 60 млн ₽',
rating: 4.3,
reviews: 5,
verified: true,
partnerGeography: ['spb', 'russia_all'],
slogan: 'Качество и надежность',
});
console.log(' ✓ Компания создана:', company2.fullName);
// Создать второго пользователя
console.log('\n👤 Создание второго тестового пользователя...');
const user2 = await User.create({
email: presetUserEmail2,
password: 'SecurePass123!',
firstName: 'Петр',
lastName: 'Петров',
position: 'Менеджер',
phone: '+7 (495) 987-65-43',
companyId: company2._id,
});
console.log(' ✓ Пользователь создан:', user2.firstName, user2.lastName);
// Проверка что данные сохранены правильно
console.log('\n✅ Проверка данных:');
console.log('\n Пользователь 1:');
console.log(' Email:', user.email);
console.log(' Имя:', user.firstName);
console.log(' Фамилия:', user.lastName);
console.log(' Компания:', company.fullName);
console.log(' Должность:', user.position);
console.log('\n Пользователь 2:');
console.log(' Email:', user2.email);
console.log(' Имя:', user2.firstName);
console.log(' Фамилия:', user2.lastName);
console.log(' Компания:', company2.fullName);
console.log(' Должность:', user2.position);
console.log('\n✅ ГОТОВО! Тестовый пользователь создан с правильной кодировкой UTF-8');
console.log('\n✅ ГОТОВО! Тестовые пользователи созданы с правильной кодировкой UTF-8');
console.log('\n📋 Данные для входа:');
console.log('\n Пользователь 1:');
console.log(' Email: admin@test-company.ru');
console.log(' Пароль: SecurePass123!');
console.log('\n Пользователь 2:');
console.log(' Email: manager@partner-company.ru');
console.log(' Пароль: SecurePass123!');
console.log('');
// Создать дополнительные тестовые компании для поиска

View File

@@ -1,13 +1,11 @@
const mongoose = require('mongoose');
const mongoose = require('../../../utils/mongoose');
require('dotenv').config();
// Подключение моделей - прямые пути без path.join и __dirname
// Подключение моделей
const Activity = require('../models/Activity');
const User = require('../models/User');
const Company = require('../models/Company');
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement-platform';
const activityTemplates = [
{
type: 'request_received',
@@ -53,8 +51,14 @@ const activityTemplates = [
async function seedActivities() {
try {
console.log('🌱 Connecting to MongoDB...');
await mongoose.connect(MONGODB_URI);
// Подключение к MongoDB происходит через server/utils/mongoose.ts
console.log('🌱 Checking MongoDB connection...');
if (mongoose.connection.readyState !== 1) {
console.log('⏳ Waiting for MongoDB connection...');
await new Promise((resolve) => {
mongoose.connection.once('connected', resolve);
});
}
console.log('✅ Connected to MongoDB');
// Найти тестового пользователя

View File

@@ -1,13 +1,17 @@
const mongoose = require('mongoose');
const mongoose = require('../../../utils/mongoose');
const Request = require('../models/Request');
const Company = require('../models/Company');
const User = require('../models/User');
const mongoUri = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin';
async function seedRequests() {
try {
await mongoose.connect(mongoUri);
// Подключение к MongoDB происходит через server/utils/mongoose.ts
if (mongoose.connection.readyState !== 1) {
console.log('⏳ Waiting for MongoDB connection...');
await new Promise((resolve) => {
mongoose.connection.once('connected', resolve);
});
}
console.log('✅ Connected to MongoDB');
// Получаем все компании