Refactor file handling in BuyProduct and Request models; implement file schema for better structure. Update routes to handle file uploads and downloads with improved error handling and logging. Adjust MongoDB connection management across scripts and routes for consistency.
This commit is contained in:
@@ -1,5 +1,34 @@
|
|||||||
const mongoose = require('mongoose');
|
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({
|
const buyProductSchema = new mongoose.Schema({
|
||||||
companyId: {
|
companyId: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -24,18 +53,7 @@ const buyProductSchema = new mongoose.Schema({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'шт'
|
default: 'шт'
|
||||||
},
|
},
|
||||||
files: [{
|
files: [fileSchema],
|
||||||
id: String,
|
|
||||||
name: String,
|
|
||||||
url: String,
|
|
||||||
type: String,
|
|
||||||
size: Number,
|
|
||||||
storagePath: String,
|
|
||||||
uploadedAt: {
|
|
||||||
type: Date,
|
|
||||||
default: Date.now
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
acceptedBy: [{
|
acceptedBy: [{
|
||||||
companyId: {
|
companyId: {
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ const requestSchema = new mongoose.Schema({
|
|||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
files: [{
|
files: [{
|
||||||
id: String,
|
id: { type: String },
|
||||||
name: String,
|
name: { type: String },
|
||||||
url: String,
|
url: { type: String },
|
||||||
type: String,
|
type: { type: String },
|
||||||
size: Number,
|
size: { type: Number },
|
||||||
storagePath: String,
|
storagePath: { type: String },
|
||||||
uploadedAt: {
|
uploadedAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
default: Date.now
|
default: Date.now
|
||||||
@@ -47,12 +47,12 @@ const requestSchema = new mongoose.Schema({
|
|||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
responseFiles: [{
|
responseFiles: [{
|
||||||
id: String,
|
id: { type: String },
|
||||||
name: String,
|
name: { type: String },
|
||||||
url: String,
|
url: { type: String },
|
||||||
type: String,
|
type: { type: String },
|
||||||
size: Number,
|
size: { type: Number },
|
||||||
storagePath: String,
|
storagePath: { type: String },
|
||||||
uploadedAt: {
|
uploadedAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
default: Date.now
|
default: Date.now
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const Request = require('../models/Request');
|
|||||||
const BuyProduct = require('../models/BuyProduct');
|
const BuyProduct = require('../models/BuyProduct');
|
||||||
const Message = require('../models/Message');
|
const Message = require('../models/Message');
|
||||||
const Review = require('../models/Review');
|
const Review = require('../models/Review');
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('../../../utils/mongoose');
|
||||||
const { Types } = mongoose;
|
const { Types } = mongoose;
|
||||||
|
|
||||||
const PRESET_COMPANY_ID = new Types.ObjectId('68fe2ccda3526c303ca06796');
|
const PRESET_COMPANY_ID = new Types.ObjectId('68fe2ccda3526c303ca06796');
|
||||||
@@ -116,9 +116,6 @@ const waitForDatabaseConnection = async () => {
|
|||||||
|
|
||||||
const verifyAuth = async () => {
|
const verifyAuth = async () => {
|
||||||
try {
|
try {
|
||||||
if (!mongoose.connection.db) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await mongoose.connection.db.admin().command({ listDatabases: 1 });
|
await mongoose.connection.db.admin().command({ listDatabases: 1 });
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -139,15 +136,17 @@ const waitForDatabaseConnection = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ожидаем подключения (подключение происходит автоматически через server/utils/mongoose.ts)
|
const connection = await connectDB();
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
if (!connection) {
|
||||||
|
break;
|
||||||
if (mongoose.connection.readyState === 1) {
|
|
||||||
const authed = await verifyAuth();
|
|
||||||
if (authed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authed = await verifyAuth();
|
||||||
|
if (authed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mongoose.connection.close().catch(() => {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!isAuthFailure(error)) {
|
if (!isAuthFailure(error)) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -218,8 +217,12 @@ const initializeTestUser = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing test data:', error.message);
|
console.error('Error initializing test data:', error.message);
|
||||||
if (error?.code === 13 || /auth/i.test(error?.message || '')) {
|
if (error?.code === 13 || /auth/i.test(error?.message || '')) {
|
||||||
if (process.env.DEV === 'true') {
|
try {
|
||||||
console.error('Auth error detected. Connection managed by server/utils/mongoose.ts');
|
await connectDB();
|
||||||
|
} catch (connectError) {
|
||||||
|
if (process.env.DEV === 'true') {
|
||||||
|
console.error('Failed to re-connect after auth error:', connectError.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const BuyDocument = require('../models/BuyDocument')
|
const BuyDocument = require('../models/BuyDocument')
|
||||||
|
|
||||||
// Create remote-assets/docs directory if it doesn't exist
|
// 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)) {
|
if (!fs.existsSync(docsDir)) {
|
||||||
fs.mkdirSync(docsDir, { recursive: true })
|
fs.mkdirSync(docsDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { verifyToken } = require('../middleware/auth');
|
const { verifyToken } = require('../middleware/auth');
|
||||||
const BuyProduct = require('../models/BuyProduct');
|
const BuyProduct = require('../models/BuyProduct');
|
||||||
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const multer = require('multer');
|
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) => {
|
const ensureDirectory = (dirPath) => {
|
||||||
if (!fs.existsSync(dirPath)) {
|
if (!fs.existsSync(dirPath)) {
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
@@ -23,18 +24,6 @@ const ALLOWED_MIME_TYPES = new Set([
|
|||||||
'text/csv',
|
'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({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
const productId = req.params.id || 'common';
|
const productId = req.params.id || 'common';
|
||||||
@@ -43,10 +32,12 @@ const storage = multer.diskStorage({
|
|||||||
cb(null, productDir);
|
cb(null, productDir);
|
||||||
},
|
},
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
const originalExtension = getExtension(file.originalname);
|
// Исправляем кодировку имени файла из Latin1 в UTF-8
|
||||||
const baseName = getBasename(file.originalname)
|
const fixedName = Buffer.from(file.originalname, 'latin1').toString('utf8');
|
||||||
.replace(/[^a-zA-Z0-9-_]+/g, '_')
|
const originalExtension = path.extname(fixedName) || '';
|
||||||
.toLowerCase();
|
const baseName = path
|
||||||
|
.basename(fixedName, originalExtension)
|
||||||
|
.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '_'); // Убираем только недопустимые символы Windows, оставляем кириллицу
|
||||||
cb(null, `${Date.now()}_${baseName}${originalExtension}`);
|
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' });
|
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' });
|
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 = {
|
const file = {
|
||||||
id: `file-${Date.now()}`,
|
id: `file-${fileTimestamp}`, // Используем тот же timestamp, что и в имени файла
|
||||||
name: req.file.originalname,
|
name: fixedFileName,
|
||||||
url: `/uploads/${relativePath}`,
|
url: `/uploads/buy-products/${relativePath}`,
|
||||||
type: req.file.mimetype,
|
type: req.file.mimetype,
|
||||||
size: req.file.size,
|
size: req.file.size,
|
||||||
uploadedAt: new Date(),
|
uploadedAt: new Date(),
|
||||||
storagePath: relativePath,
|
storagePath: relativePath,
|
||||||
};
|
};
|
||||||
|
|
||||||
product.files.push(file);
|
console.log('[BuyProducts] Adding file to product:', {
|
||||||
await product.save();
|
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);
|
log('[BuyProducts] File added to product:', id, file.name);
|
||||||
|
|
||||||
res.json(product);
|
res.json(updatedProduct);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[BuyProducts] Error adding file:', error.message);
|
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({
|
res.status(500).json({
|
||||||
error: 'Internal server error',
|
error: 'Internal server error',
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
details: error.errors || {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -303,7 +350,7 @@ router.delete('/:id/files/:fileId', verifyToken, async (req, res) => {
|
|||||||
await product.save();
|
await product.save();
|
||||||
|
|
||||||
const storedPath = fileToRemove.storagePath || fileToRemove.url.replace(/^\/uploads\//, '');
|
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) => {
|
fs.promises.unlink(absolutePath).catch((unlinkError) => {
|
||||||
if (unlinkError && unlinkError.code !== 'ENOENT') {
|
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;
|
module.exports = router;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ const Company = require('../models/Company');
|
|||||||
const Experience = require('../models/Experience');
|
const Experience = require('../models/Experience');
|
||||||
const Request = require('../models/Request');
|
const Request = require('../models/Request');
|
||||||
const Message = require('../models/Message');
|
const Message = require('../models/Message');
|
||||||
const { Types } = require('mongoose');
|
const mongoose = require('../../../utils/mongoose');
|
||||||
|
const { Types } = mongoose;
|
||||||
|
|
||||||
// GET /my/info - получить мою компанию (требует авторизации) - ДОЛЖНО быть ПЕРЕД /:id
|
// GET /my/info - получить мою компанию (требует авторизации) - ДОЛЖНО быть ПЕРЕД /:id
|
||||||
router.get('/my/info', verifyToken, async (req, res) => {
|
router.get('/my/info', verifyToken, async (req, res) => {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { verifyToken } = require('../middleware/auth');
|
const { verifyToken } = require('../middleware/auth');
|
||||||
const Experience = require('../models/Experience');
|
const Experience = require('../models/Experience');
|
||||||
const { Types } = require('mongoose');
|
const mongoose = require('../../../utils/mongoose');
|
||||||
|
const { Types } = mongoose;
|
||||||
|
|
||||||
// GET /experience - Получить список опыта работы компании
|
// GET /experience - Получить список опыта работы компании
|
||||||
router.get('/', verifyToken, async (req, res) => {
|
router.get('/', verifyToken, async (req, res) => {
|
||||||
|
|||||||
@@ -21,21 +21,23 @@ router.get('/aggregates', verifyToken, async (req, res) => {
|
|||||||
|
|
||||||
const companyId = user.companyId.toString();
|
const companyId = user.companyId.toString();
|
||||||
|
|
||||||
const [docsCount, acceptsCount, requestsCount] = await Promise.all([
|
// Получить все BuyProduct для подсчета файлов и акцептов
|
||||||
BuyProduct.countDocuments({ companyId }),
|
const buyProducts = await BuyProduct.find({ companyId });
|
||||||
Request.countDocuments({
|
|
||||||
$or: [
|
// Подсчет документов - сумма всех файлов во всех BuyProduct
|
||||||
{ senderCompanyId: companyId, status: 'accepted' },
|
const docsCount = buyProducts.reduce((total, product) => {
|
||||||
{ recipientCompanyId: companyId, status: 'accepted' }
|
return total + (product.files ? product.files.length : 0);
|
||||||
]
|
}, 0);
|
||||||
}),
|
|
||||||
Request.countDocuments({
|
// Подсчет акцептов - сумма всех acceptedBy во всех BuyProduct
|
||||||
$or: [
|
const acceptsCount = buyProducts.reduce((total, product) => {
|
||||||
{ senderCompanyId: companyId },
|
return total + (product.acceptedBy ? product.acceptedBy.length : 0);
|
||||||
{ recipientCompanyId: companyId }
|
}, 0);
|
||||||
]
|
|
||||||
})
|
// Подсчет исходящих запросов (только отправленные этой компанией)
|
||||||
]);
|
const requestsCount = await Request.countDocuments({
|
||||||
|
senderCompanyId: companyId
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
docsCount,
|
docsCount,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { verifyToken } = require('../middleware/auth');
|
const { verifyToken } = require('../middleware/auth');
|
||||||
const Message = require('../models/Message');
|
const Message = require('../models/Message');
|
||||||
|
const mongoose = require('../../../utils/mongoose');
|
||||||
|
const { ObjectId } = mongoose.Types;
|
||||||
|
|
||||||
// Функция для логирования с проверкой DEV переменной
|
// Функция для логирования с проверкой DEV переменной
|
||||||
const log = (message, data = '') => {
|
const log = (message, data = '') => {
|
||||||
@@ -18,7 +20,6 @@ const log = (message, data = '') => {
|
|||||||
router.get('/threads', verifyToken, async (req, res) => {
|
router.get('/threads', verifyToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const companyId = req.companyId;
|
const companyId = req.companyId;
|
||||||
const { ObjectId } = require('mongoose').Types;
|
|
||||||
|
|
||||||
log('[Messages] Fetching threads for companyId:', companyId, 'type:', typeof companyId);
|
log('[Messages] Fetching threads for companyId:', companyId, 'type:', typeof companyId);
|
||||||
|
|
||||||
@@ -146,7 +147,6 @@ router.post('/:threadId', verifyToken, async (req, res) => {
|
|||||||
|
|
||||||
// Найти recipientCompanyId по ObjectId если нужно
|
// Найти recipientCompanyId по ObjectId если нужно
|
||||||
let recipientObjectId = recipientCompanyId;
|
let recipientObjectId = recipientCompanyId;
|
||||||
const { ObjectId } = require('mongoose').Types;
|
|
||||||
try {
|
try {
|
||||||
if (typeof recipientCompanyId === 'string' && ObjectId.isValid(recipientCompanyId)) {
|
if (typeof recipientCompanyId === 'string' && ObjectId.isValid(recipientCompanyId)) {
|
||||||
recipientObjectId = new ObjectId(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 recipientCompanyId is not set or wrong - fix it
|
||||||
if (!message.recipientCompanyId || message.recipientCompanyId.toString() !== expectedRecipient) {
|
if (!message.recipientCompanyId || message.recipientCompanyId.toString() !== expectedRecipient) {
|
||||||
const { ObjectId } = require('mongoose').Types;
|
|
||||||
let recipientObjectId = expectedRecipient;
|
let recipientObjectId = expectedRecipient;
|
||||||
try {
|
try {
|
||||||
if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) {
|
if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) {
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ const router = express.Router();
|
|||||||
const { verifyToken } = require('../middleware/auth');
|
const { verifyToken } = require('../middleware/auth');
|
||||||
const Request = require('../models/Request');
|
const Request = require('../models/Request');
|
||||||
const BuyProduct = require('../models/BuyProduct');
|
const BuyProduct = require('../models/BuyProduct');
|
||||||
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
|
const mongoose = require('../../../utils/mongoose');
|
||||||
|
|
||||||
// Функция для логирования с проверкой DEV переменной
|
// Функция для логирования с проверкой DEV переменной
|
||||||
const log = (message, data = '') => {
|
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) => {
|
const ensureDirectory = (dirPath) => {
|
||||||
if (!fs.existsSync(dirPath)) {
|
if (!fs.existsSync(dirPath)) {
|
||||||
@@ -37,28 +39,17 @@ const ALLOWED_REQUEST_MIME_TYPES = new Set([
|
|||||||
'text/csv',
|
'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({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
const subfolder = req.requestUploadSubfolder || '';
|
const subfolder = req.requestUploadSubfolder || '';
|
||||||
const destinationDir = subfolder ? `${REQUESTS_UPLOAD_ROOT}/${subfolder}` : REQUESTS_UPLOAD_ROOT;
|
const destinationDir = `${REQUESTS_UPLOAD_ROOT}/${subfolder}`;
|
||||||
ensureDirectory(destinationDir);
|
ensureDirectory(destinationDir);
|
||||||
cb(null, destinationDir);
|
cb(null, destinationDir);
|
||||||
},
|
},
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
const extension = getExtension(file.originalname);
|
const extension = path.extname(file.originalname) || '';
|
||||||
const baseName = getBasename(file.originalname)
|
const baseName = path
|
||||||
|
.basename(file.originalname, extension)
|
||||||
.replace(/[^a-zA-Z0-9-_]+/g, '_')
|
.replace(/[^a-zA-Z0-9-_]+/g, '_')
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
cb(null, `${Date.now()}_${baseName}${extension}`);
|
cb(null, `${Date.now()}_${baseName}${extension}`);
|
||||||
@@ -107,7 +98,7 @@ const cleanupUploadedFiles = async (req) => {
|
|||||||
|
|
||||||
const subfolder = req.requestUploadSubfolder || '';
|
const subfolder = req.requestUploadSubfolder || '';
|
||||||
const removalTasks = req.files.map((file) => {
|
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) => {
|
return fs.promises.unlink(filePath).catch((error) => {
|
||||||
if (error.code !== 'ENOENT') {
|
if (error.code !== 'ENOENT') {
|
||||||
console.error('[Requests] Failed to cleanup uploaded file:', error.message);
|
console.error('[Requests] Failed to cleanup uploaded file:', error.message);
|
||||||
@@ -125,7 +116,7 @@ const mapFilesToMetadata = (req) => {
|
|||||||
|
|
||||||
const subfolder = req.requestUploadSubfolder || '';
|
const subfolder = req.requestUploadSubfolder || '';
|
||||||
return req.files.map((file) => {
|
return req.files.map((file) => {
|
||||||
const relativePath = subfolder ? `requests/${subfolder}/${file.filename}` : `requests/${file.filename}`;
|
const relativePath = `requests/${subfolder}/${file.filename}`;
|
||||||
return {
|
return {
|
||||||
id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
name: file.originalname,
|
name: file.originalname,
|
||||||
@@ -169,7 +160,7 @@ const removeStoredFiles = async (files = []) => {
|
|||||||
const tasks = files
|
const tasks = files
|
||||||
.filter((file) => file && file.storagePath)
|
.filter((file) => file && file.storagePath)
|
||||||
.map((file) => {
|
.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) => {
|
return fs.promises.unlink(absolutePath).catch((error) => {
|
||||||
if (error.code !== 'ENOENT') {
|
if (error.code !== 'ENOENT') {
|
||||||
console.error('[Requests] Failed to remove stored file:', error.message);
|
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' });
|
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 {
|
try {
|
||||||
const product = await BuyProduct.findById(productId);
|
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) {
|
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) {
|
} 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) {
|
if (!subject) {
|
||||||
await cleanupUploadedFiles(req);
|
await cleanupUploadedFiles(req);
|
||||||
return res.status(400).json({ error: 'Subject is required' });
|
return res.status(400).json({ error: 'Subject is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadedFiles = mapFilesToMetadata(req);
|
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
for (const recipientCompanyId of recipients) {
|
for (const recipientCompanyId of recipients) {
|
||||||
try {
|
try {
|
||||||
@@ -331,9 +359,17 @@ router.put(
|
|||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
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 responseText = (req.body.response || '').trim();
|
||||||
const statusRaw = (req.body.status || 'accepted').toLowerCase();
|
const statusRaw = (req.body.status || 'accepted').toLowerCase();
|
||||||
const status = statusRaw === 'rejected' ? 'rejected' : 'accepted';
|
const status = statusRaw === 'rejected' ? 'rejected' : 'accepted';
|
||||||
|
|
||||||
|
console.log('[Requests] Response text:', responseText);
|
||||||
|
console.log('[Requests] Status:', status);
|
||||||
|
|
||||||
if (req.invalidFiles && req.invalidFiles.length > 0) {
|
if (req.invalidFiles && req.invalidFiles.length > 0) {
|
||||||
await cleanupUploadedFiles(req);
|
await cleanupUploadedFiles(req);
|
||||||
@@ -361,6 +397,8 @@ router.put(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uploadedResponseFiles = mapFilesToMetadata(req);
|
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) {
|
if (uploadedResponseFiles.length > 0) {
|
||||||
await removeStoredFiles(request.responseFiles || []);
|
await removeStoredFiles(request.responseFiles || []);
|
||||||
@@ -372,18 +410,126 @@ router.put(
|
|||||||
request.respondedAt = new Date();
|
request.respondedAt = new Date();
|
||||||
request.updatedAt = 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(savedRequest);
|
||||||
|
|
||||||
res.json(request);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Requests] Error responding to request:', error.message);
|
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 });
|
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 - удалить запрос
|
// DELETE /requests/:id - удалить запрос
|
||||||
router.delete('/:id', verifyToken, async (req, res) => {
|
router.delete('/:id', verifyToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -54,10 +54,13 @@ router.get('/recommendations', verifyToken, async (req, res) => {
|
|||||||
// GET /search - Поиск компаний
|
// GET /search - Поиск компаний
|
||||||
router.get('/', verifyToken, async (req, res) => {
|
router.get('/', verifyToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
console.log('[Search] === NEW VERSION WITH FIXED SIZE FILTER ===');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
query = '',
|
query = '',
|
||||||
page = 1,
|
page = 1,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
|
offset, // Добавляем поддержку offset для точной пагинации
|
||||||
industries,
|
industries,
|
||||||
companySize,
|
companySize,
|
||||||
geography,
|
geography,
|
||||||
@@ -65,8 +68,12 @@ router.get('/', verifyToken, async (req, res) => {
|
|||||||
hasReviews,
|
hasReviews,
|
||||||
hasAcceptedDocs,
|
hasAcceptedDocs,
|
||||||
sortBy = 'relevance',
|
sortBy = 'relevance',
|
||||||
sortOrder = 'desc'
|
sortOrder = 'desc',
|
||||||
|
minEmployees, // Кастомный фильтр: минимум сотрудников
|
||||||
|
maxEmployees // Кастомный фильтр: максимум сотрудников
|
||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
||||||
|
console.log('[Search] Filters:', { minEmployees, maxEmployees, companySize });
|
||||||
|
|
||||||
// Получить компанию пользователя, чтобы исключить её из результатов
|
// Получить компанию пользователя, чтобы исключить её из результатов
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
@@ -135,12 +142,99 @@ router.get('/', verifyToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фильтр по размеру компании
|
// Функция для парсинга диапазона из строки вида "51-250" или "500+"
|
||||||
if (companySize) {
|
const parseEmployeeRange = (sizeStr) => {
|
||||||
const sizeList = Array.isArray(companySize) ? companySize : [companySize];
|
if (sizeStr.includes('+')) {
|
||||||
if (sizeList.length > 0) {
|
const min = parseInt(sizeStr.replace('+', ''));
|
||||||
filters.push({ companySize: { $in: sizeList } });
|
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 });
|
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 } : {};
|
let filter = filters.length > 0 ? { $and: filters } : {};
|
||||||
|
|
||||||
// Пагинация
|
// Пагинация - используем offset если передан, иначе вычисляем из page
|
||||||
const pageNum = parseInt(page) || 1;
|
|
||||||
const limitNum = parseInt(limit) || 10;
|
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 = {};
|
let sortOptions = {};
|
||||||
@@ -228,3 +334,4 @@ router.get('/', verifyToken, async (req, res) => {
|
|||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
const mongoose = require('mongoose');
|
const mongoose = require('../../../utils/mongoose');
|
||||||
|
const { ObjectId } = mongoose.Types;
|
||||||
const Message = require('../models/Message');
|
const Message = require('../models/Message');
|
||||||
require('dotenv').config({ path: '../../.env' });
|
require('dotenv').config();
|
||||||
|
|
||||||
const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db';
|
|
||||||
|
|
||||||
async function migrateMessages() {
|
async function migrateMessages() {
|
||||||
try {
|
try {
|
||||||
console.log('[Migration] Connecting to MongoDB...');
|
// Подключение к MongoDB происходит через server/utils/mongoose.ts
|
||||||
await mongoose.connect(mongoUrl, {
|
console.log('[Migration] Checking MongoDB connection...');
|
||||||
useNewUrlParser: true,
|
if (mongoose.connection.readyState !== 1) {
|
||||||
useUnifiedTopology: true,
|
console.log('[Migration] Waiting for MongoDB connection...');
|
||||||
serverSelectionTimeoutMS: 5000,
|
await new Promise((resolve) => {
|
||||||
connectTimeoutMS: 5000,
|
mongoose.connection.once('connected', resolve);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
console.log('[Migration] Connected to MongoDB');
|
console.log('[Migration] Connected to MongoDB');
|
||||||
|
|
||||||
// Найти все сообщения
|
// Найти все сообщения
|
||||||
@@ -54,7 +54,6 @@ async function migrateMessages() {
|
|||||||
console.log(' Expected:', expectedRecipient);
|
console.log(' Expected:', expectedRecipient);
|
||||||
|
|
||||||
// Конвертируем в ObjectId если нужно
|
// Конвертируем в ObjectId если нужно
|
||||||
const { ObjectId } = require('mongoose').Types;
|
|
||||||
let recipientObjectId = expectedRecipient;
|
let recipientObjectId = expectedRecipient;
|
||||||
try {
|
try {
|
||||||
if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) {
|
if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) {
|
||||||
|
|||||||
@@ -1,57 +1,57 @@
|
|||||||
const mongoose = require('mongoose');
|
const mongoose = require('../../../utils/mongoose');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
// Импорт моделей - прямые пути без path.join и __dirname
|
// Импорт моделей
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
const Company = require('../models/Company');
|
const Company = require('../models/Company');
|
||||||
const Request = require('../models/Request');
|
const Request = require('../models/Request');
|
||||||
|
|
||||||
const primaryUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db';
|
// Подключение к MongoDB происходит через server/utils/mongoose.ts
|
||||||
const fallbackUri =
|
// Проверяем, подключено ли уже
|
||||||
process.env.MONGODB_AUTH_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin';
|
const ensureConnection = async () => {
|
||||||
|
if (mongoose.connection.readyState === 1) {
|
||||||
const connectWithFallback = async () => {
|
console.log('✅ MongoDB уже подключено');
|
||||||
// Сначала пробуем FALLBACK (с аутентификацией)
|
|
||||||
try {
|
|
||||||
console.log('\n📡 Подключение к MongoDB (с аутентификацией)...');
|
|
||||||
await mongoose.connect(fallbackUri, { useNewUrlParser: true, useUnifiedTopology: true });
|
|
||||||
console.log('✅ Подключено к MongoDB');
|
|
||||||
return;
|
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 () => {
|
const recreateTestUser = async () => {
|
||||||
try {
|
try {
|
||||||
await connectWithFallback();
|
await ensureConnection();
|
||||||
|
|
||||||
const presetCompanyId = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06796');
|
const presetCompanyId = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06796');
|
||||||
const presetUserEmail = 'admin@test-company.ru';
|
const presetUserEmail = 'admin@test-company.ru';
|
||||||
|
|
||||||
|
const presetCompanyId2 = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06797');
|
||||||
|
const presetUserEmail2 = 'manager@partner-company.ru';
|
||||||
|
|
||||||
// Удалить старого тестового пользователя
|
// Удалить старых тестовых пользователей
|
||||||
console.log('🗑️ Удаление старого тестового пользователя...');
|
console.log('🗑️ Удаление старых тестовых пользователей...');
|
||||||
const oldUser = await User.findOne({ email: presetUserEmail });
|
const testEmails = [presetUserEmail, presetUserEmail2];
|
||||||
if (oldUser) {
|
|
||||||
// Удалить связанную компанию
|
for (const email of testEmails) {
|
||||||
if (oldUser.companyId) {
|
const oldUser = await User.findOne({ email });
|
||||||
await Company.findByIdAndDelete(oldUser.companyId);
|
if (oldUser) {
|
||||||
console.log(' ✓ Старая компания удалена');
|
// Удалить связанную компанию
|
||||||
|
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
|
// Создать новую компанию с правильной кодировкой UTF-8
|
||||||
@@ -82,8 +82,8 @@ const recreateTestUser = async () => {
|
|||||||
});
|
});
|
||||||
console.log(' ✓ Компания создана:', company.fullName);
|
console.log(' ✓ Компания создана:', company.fullName);
|
||||||
|
|
||||||
// Создать нового пользователя с правильной кодировкой UTF-8
|
// Создать первого пользователя с правильной кодировкой UTF-8
|
||||||
console.log('\n👤 Создание тестового пользователя...');
|
console.log('\n👤 Создание первого тестового пользователя...');
|
||||||
const user = await User.create({
|
const user = await User.create({
|
||||||
email: presetUserEmail,
|
email: presetUserEmail,
|
||||||
password: 'SecurePass123!',
|
password: 'SecurePass123!',
|
||||||
@@ -95,18 +95,71 @@ const recreateTestUser = async () => {
|
|||||||
});
|
});
|
||||||
console.log(' ✓ Пользователь создан:', user.firstName, user.lastName);
|
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✅ Проверка данных:');
|
||||||
|
console.log('\n Пользователь 1:');
|
||||||
console.log(' Email:', user.email);
|
console.log(' Email:', user.email);
|
||||||
console.log(' Имя:', user.firstName);
|
console.log(' Имя:', user.firstName);
|
||||||
console.log(' Фамилия:', user.lastName);
|
console.log(' Фамилия:', user.lastName);
|
||||||
console.log(' Компания:', company.fullName);
|
console.log(' Компания:', company.fullName);
|
||||||
console.log(' Должность:', user.position);
|
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📋 Данные для входа:');
|
||||||
|
console.log('\n Пользователь 1:');
|
||||||
console.log(' Email: admin@test-company.ru');
|
console.log(' Email: admin@test-company.ru');
|
||||||
console.log(' Пароль: SecurePass123!');
|
console.log(' Пароль: SecurePass123!');
|
||||||
|
console.log('\n Пользователь 2:');
|
||||||
|
console.log(' Email: manager@partner-company.ru');
|
||||||
|
console.log(' Пароль: SecurePass123!');
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Создать дополнительные тестовые компании для поиска
|
// Создать дополнительные тестовые компании для поиска
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
const mongoose = require('mongoose');
|
const mongoose = require('../../../utils/mongoose');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
// Подключение моделей - прямые пути без path.join и __dirname
|
// Подключение моделей
|
||||||
const Activity = require('../models/Activity');
|
const Activity = require('../models/Activity');
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
const Company = require('../models/Company');
|
const Company = require('../models/Company');
|
||||||
|
|
||||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement-platform';
|
|
||||||
|
|
||||||
const activityTemplates = [
|
const activityTemplates = [
|
||||||
{
|
{
|
||||||
type: 'request_received',
|
type: 'request_received',
|
||||||
@@ -53,8 +51,14 @@ const activityTemplates = [
|
|||||||
|
|
||||||
async function seedActivities() {
|
async function seedActivities() {
|
||||||
try {
|
try {
|
||||||
console.log('🌱 Connecting to MongoDB...');
|
// Подключение к MongoDB происходит через server/utils/mongoose.ts
|
||||||
await mongoose.connect(MONGODB_URI);
|
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');
|
console.log('✅ Connected to MongoDB');
|
||||||
|
|
||||||
// Найти тестового пользователя
|
// Найти тестового пользователя
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
const mongoose = require('mongoose');
|
const mongoose = require('../../../utils/mongoose');
|
||||||
const Request = require('../models/Request');
|
const Request = require('../models/Request');
|
||||||
const Company = require('../models/Company');
|
const Company = require('../models/Company');
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
|
|
||||||
const mongoUri = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin';
|
|
||||||
|
|
||||||
async function seedRequests() {
|
async function seedRequests() {
|
||||||
try {
|
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');
|
console.log('✅ Connected to MongoDB');
|
||||||
|
|
||||||
// Получаем все компании
|
// Получаем все компании
|
||||||
|
|||||||
Reference in New Issue
Block a user