|
|
|
|
const express = require('express');
|
|
|
|
|
const bodyParser = require('body-parser');
|
|
|
|
|
const cors = require('cors');
|
|
|
|
|
const mysql = require('mysql2/promise');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
const http = require('http');
|
|
|
|
|
const WebSocket = require('ws');
|
|
|
|
|
const OssUploader = require('./oss-uploader');
|
|
|
|
|
const app = express();
|
|
|
|
|
const PORT = 3000;
|
|
|
|
|
|
|
|
|
|
// 创建HTTP服务器
|
|
|
|
|
const server = http.createServer(app);
|
|
|
|
|
// 创建WebSocket服务器
|
|
|
|
|
const wss = new WebSocket.Server({ server });
|
|
|
|
|
|
|
|
|
|
// 客户端连接集合
|
|
|
|
|
const clients = new Set();
|
|
|
|
|
|
|
|
|
|
// WebSocket连接处理
|
|
|
|
|
wss.on('connection', (ws) => {
|
|
|
|
|
console.log('WebSocket客户端已连接');
|
|
|
|
|
// 将新连接的客户端添加到集合中
|
|
|
|
|
clients.add(ws);
|
|
|
|
|
|
|
|
|
|
// 接收消息处理
|
|
|
|
|
ws.on('message', (message) => {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(message);
|
|
|
|
|
console.log('收到WebSocket消息:', data);
|
|
|
|
|
|
|
|
|
|
// 处理不同类型的消息
|
|
|
|
|
switch (data.type) {
|
|
|
|
|
case 'login':
|
|
|
|
|
// 登录消息,存储用户信息
|
|
|
|
|
ws.userId = data.userId;
|
|
|
|
|
console.log(`用户 ${data.userId} 已登录`);
|
|
|
|
|
break;
|
|
|
|
|
case 'pong':
|
|
|
|
|
// 心跳响应,无需处理
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
console.log('未知消息类型:', data.type);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('解析WebSocket消息失败:', error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 连接关闭处理
|
|
|
|
|
ws.on('close', () => {
|
|
|
|
|
console.log('WebSocket客户端已断开连接');
|
|
|
|
|
clients.delete(ws);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 连接错误处理
|
|
|
|
|
ws.on('error', (error) => {
|
|
|
|
|
console.error('WebSocket连接错误:', error);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 广播消息给所有客户端
|
|
|
|
|
function broadcastMessage(message) {
|
|
|
|
|
const messageStr = JSON.stringify(message);
|
|
|
|
|
clients.forEach((client) => {
|
|
|
|
|
if (client.readyState === WebSocket.OPEN) {
|
|
|
|
|
client.send(messageStr);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 广播消息给特定用户
|
|
|
|
|
function broadcastToUser(userId, message) {
|
|
|
|
|
const messageStr = JSON.stringify(message);
|
|
|
|
|
clients.forEach((client) => {
|
|
|
|
|
if (client.readyState === WebSocket.OPEN && client.userId === userId) {
|
|
|
|
|
client.send(messageStr);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 启动心跳机制,每30秒发送一次ping消息
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
broadcastMessage({ type: 'ping' });
|
|
|
|
|
// console.log('发送心跳消息: ping');
|
|
|
|
|
}, 30000);
|
|
|
|
|
|
|
|
|
|
// 自动下架任务 - 每分钟检查一次
|
|
|
|
|
setInterval(async () => {
|
|
|
|
|
try {
|
|
|
|
|
console.log('检查需要自动下架的商品...');
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
|
|
|
|
|
const currentTime = new Date();
|
|
|
|
|
|
|
|
|
|
// 查找所有状态为published的商品
|
|
|
|
|
const [publishedProducts] = await connection.query(
|
|
|
|
|
'SELECT id, autoOfflineTime, autoOfflineHours, created_at, updated_at FROM products WHERE status = ?',
|
|
|
|
|
['published']
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const productsToOffline = [];
|
|
|
|
|
|
|
|
|
|
// 检查每个商品是否需要自动下架
|
|
|
|
|
for (const product of publishedProducts) {
|
|
|
|
|
let shouldOffline = false;
|
|
|
|
|
|
|
|
|
|
if (product.autoOfflineTime) {
|
|
|
|
|
// 情况1:有明确的autoOfflineTime
|
|
|
|
|
const autoOfflineTime = new Date(product.autoOfflineTime);
|
|
|
|
|
if (currentTime >= autoOfflineTime) {
|
|
|
|
|
shouldOffline = true;
|
|
|
|
|
}
|
|
|
|
|
} else if (product.autoOfflineHours && !isNaN(product.autoOfflineHours) && product.autoOfflineHours > 0) {
|
|
|
|
|
// 情况2:只有autoOfflineHours,计算自动下架时间
|
|
|
|
|
const baseTime = product.updated_at ? new Date(product.updated_at) : new Date(product.created_at);
|
|
|
|
|
const offlineTime = new Date(baseTime.getTime() + product.autoOfflineHours * 60 * 60 * 1000);
|
|
|
|
|
if (currentTime >= offlineTime) {
|
|
|
|
|
shouldOffline = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shouldOffline) {
|
|
|
|
|
productsToOffline.push(product.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (productsToOffline.length > 0) {
|
|
|
|
|
console.log(`发现${productsToOffline.length}个商品需要自动下架`);
|
|
|
|
|
|
|
|
|
|
// 批量更新商品状态为sold_out
|
|
|
|
|
for (const productId of productsToOffline) {
|
|
|
|
|
await connection.query(
|
|
|
|
|
'UPDATE products SET status = ?, label = 1, updated_at = NOW() WHERE id = ?',
|
|
|
|
|
['sold_out', productId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 发送WebSocket消息通知所有客户端
|
|
|
|
|
broadcastMessage({
|
|
|
|
|
type: 'supply_status_change',
|
|
|
|
|
supplyId: productId,
|
|
|
|
|
action: 'unpublish',
|
|
|
|
|
status: 'sold_out'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('自动下架任务完成');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connection.release();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('自动下架任务失败:', error.message);
|
|
|
|
|
}
|
|
|
|
|
}, 60000); // 每分钟检查一次
|
|
|
|
|
|
|
|
|
|
// 配置CORS
|
|
|
|
|
app.use(cors());
|
|
|
|
|
app.use((req, res, next) => {
|
|
|
|
|
res.header('Access-Control-Allow-Origin', '*');
|
|
|
|
|
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
|
|
|
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
|
|
|
res.status(200).end();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
app.use(bodyParser.json({ limit: '10mb' }));
|
|
|
|
|
app.use(express.static(path.join(__dirname)));
|
|
|
|
|
|
|
|
|
|
// 数据库配置 - 从环境变量获取,与docker-compose.yml保持一致
|
|
|
|
|
const dbConfig = {
|
|
|
|
|
host: process.env.DB_HOST || '1.95.162.61',
|
|
|
|
|
user: process.env.DB_USER || 'root',
|
|
|
|
|
password: process.env.DB_PASSWORD || 'schl@2025',
|
|
|
|
|
database: process.env.DB_NAME || 'wechat_app',
|
|
|
|
|
waitForConnections: true,
|
|
|
|
|
connectionLimit: 20, // 增加连接池大小,提高并发处理能力
|
|
|
|
|
queueLimit: 0,
|
|
|
|
|
connectTimeout: 10000, // 增加连接超时时间(mysql2支持的选项)
|
|
|
|
|
timezone: '+08:00' // 设置为北京时间时区
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// userlogin数据库配置 - 从环境变量获取
|
|
|
|
|
const userLoginDbConfig = {
|
|
|
|
|
host: process.env.DB_HOST || '1.95.162.61',
|
|
|
|
|
user: process.env.DB_USER || 'root',
|
|
|
|
|
password: process.env.DB_PASSWORD || 'schl@2025',
|
|
|
|
|
database: process.env.USER_LOGIN_DB_NAME || 'userlogin',
|
|
|
|
|
waitForConnections: true,
|
|
|
|
|
connectionLimit: 20, // 增加连接池大小
|
|
|
|
|
queueLimit: 0,
|
|
|
|
|
connectTimeout: 10000, // 增加连接超时时间(mysql2支持的选项)
|
|
|
|
|
timezone: '+08:00' // 设置为北京时间时区
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 创建数据库连接池
|
|
|
|
|
let pool;
|
|
|
|
|
let userLoginPool;
|
|
|
|
|
|
|
|
|
|
// 初始化数据库连接
|
|
|
|
|
async function initDatabase() {
|
|
|
|
|
try {
|
|
|
|
|
pool = mysql.createPool(dbConfig);
|
|
|
|
|
console.log('wechat_app数据库连接池创建成功');
|
|
|
|
|
|
|
|
|
|
// 初始化userlogin数据库连接池
|
|
|
|
|
userLoginPool = mysql.createPool(userLoginDbConfig);
|
|
|
|
|
console.log('userlogin数据库连接池创建成功');
|
|
|
|
|
|
|
|
|
|
// 测试wechat_app连接
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
console.log('wechat_app数据库连接测试成功');
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
// 测试userlogin连接
|
|
|
|
|
const userLoginConnection = await userLoginPool.getConnection();
|
|
|
|
|
console.log('userlogin数据库连接测试成功');
|
|
|
|
|
userLoginConnection.release();
|
|
|
|
|
|
|
|
|
|
// 确保数据库结构
|
|
|
|
|
await ensureDatabaseSchema();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('数据库初始化失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
// 如果初始化失败,尝试重新初始化
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
console.log('尝试重新初始化数据库连接...');
|
|
|
|
|
initDatabase();
|
|
|
|
|
}, 5000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 通用响应函数
|
|
|
|
|
function sendResponse(res, success, data = null, message = '') {
|
|
|
|
|
res.json({
|
|
|
|
|
success,
|
|
|
|
|
data,
|
|
|
|
|
message
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 导出函数供测试使用
|
|
|
|
|
module.exports.sendResponse = sendResponse;
|
|
|
|
|
|
|
|
|
|
// 获取货源列表API
|
|
|
|
|
app.get('/api/supplies', async (req, res) => {
|
|
|
|
|
console.log('收到获取货源列表请求:', req.query);
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
// 支持search和keyword两种参数名,确保兼容性
|
|
|
|
|
const { page = 1, pageSize = 10, search = '', keyword = '', status = '', phoneNumber = '' } = req.query;
|
|
|
|
|
// 如果提供了keyword参数,优先使用keyword
|
|
|
|
|
const actualSearch = keyword || search;
|
|
|
|
|
const offset = (page - 1) * pageSize;
|
|
|
|
|
|
|
|
|
|
// 构建基础查询,添加LEFT JOIN获取用户信息
|
|
|
|
|
let query = 'SELECT p.*, u.phoneNumber, u.nickName FROM products p LEFT JOIN users u ON p.sellerId = u.userId';
|
|
|
|
|
let countQuery = 'SELECT COUNT(*) as total FROM products p LEFT JOIN users u ON p.sellerId = u.userId';
|
|
|
|
|
let whereClause = '';
|
|
|
|
|
let params = [];
|
|
|
|
|
|
|
|
|
|
// 默认过滤掉status为hidden的货源,实现软删除效果
|
|
|
|
|
whereClause += ` WHERE status != 'hidden'`;
|
|
|
|
|
|
|
|
|
|
// 添加搜索条件
|
|
|
|
|
if (actualSearch) {
|
|
|
|
|
whereClause += ` AND (p.id LIKE ? OR p.productId LIKE ? OR p.productName LIKE ?)`;
|
|
|
|
|
params.push(`%${actualSearch}%`, `%${actualSearch}%`, `%${actualSearch}%`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加手机号搜索
|
|
|
|
|
if (phoneNumber) {
|
|
|
|
|
whereClause += ` AND u.phoneNumber LIKE ?`;
|
|
|
|
|
params.push(`%${phoneNumber}%`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加状态筛选
|
|
|
|
|
if (status) {
|
|
|
|
|
whereClause += ` AND status = ?`;
|
|
|
|
|
params.push(status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加sellerId筛选,只返回指定用户的货源
|
|
|
|
|
const { sellerId } = req.query;
|
|
|
|
|
if (sellerId) {
|
|
|
|
|
whereClause += ` AND p.sellerId = ?`;
|
|
|
|
|
params.push(sellerId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 执行查询
|
|
|
|
|
const [results] = await connection.query(
|
|
|
|
|
`${query}${whereClause} ORDER BY p.id DESC LIMIT ? OFFSET ?`,
|
|
|
|
|
[...params, parseInt(pageSize), offset]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 获取总数
|
|
|
|
|
const [countResults] = await connection.query(
|
|
|
|
|
`${countQuery}${whereClause}`,
|
|
|
|
|
params
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
// 处理返回结果中的imageUrls字段
|
|
|
|
|
const processedResults = results.map(product => {
|
|
|
|
|
// 处理imageUrls字段
|
|
|
|
|
let imageUrls = [];
|
|
|
|
|
|
|
|
|
|
if (product.imageUrls) {
|
|
|
|
|
if (typeof product.imageUrls === 'string') {
|
|
|
|
|
// 尝试解析为JSON数组
|
|
|
|
|
try {
|
|
|
|
|
let parsedImages = JSON.parse(product.imageUrls);
|
|
|
|
|
|
|
|
|
|
// 检查是否是JSON字符串的字符串表示(转义的JSON)
|
|
|
|
|
if (typeof parsedImages === 'string' &&
|
|
|
|
|
(parsedImages.startsWith('[') || parsedImages.startsWith('{'))) {
|
|
|
|
|
// 进行第二次解析
|
|
|
|
|
parsedImages = JSON.parse(parsedImages);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(parsedImages)) {
|
|
|
|
|
imageUrls = parsedImages;
|
|
|
|
|
} else if (typeof parsedImages === 'string') {
|
|
|
|
|
// 如果解析结果是字符串,可能是单个URL
|
|
|
|
|
imageUrls = [parsedImages];
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// 解析失败,尝试按逗号分隔
|
|
|
|
|
if (product.imageUrls.includes(',')) {
|
|
|
|
|
imageUrls = product.imageUrls.split(',').map(url => url.trim());
|
|
|
|
|
} else {
|
|
|
|
|
// 作为单个URL处理
|
|
|
|
|
imageUrls = [product.imageUrls.trim()];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (Array.isArray(product.imageUrls)) {
|
|
|
|
|
// 已经是数组,直接使用
|
|
|
|
|
imageUrls = product.imageUrls;
|
|
|
|
|
} else {
|
|
|
|
|
// 其他类型,转换为字符串数组
|
|
|
|
|
imageUrls = [String(product.imageUrls)];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 过滤并处理无效的URL:移除反引号并验证
|
|
|
|
|
imageUrls = imageUrls
|
|
|
|
|
.filter(url => {
|
|
|
|
|
if (!url) return false;
|
|
|
|
|
const processedUrl = url.replace(/`/g, '').trim();
|
|
|
|
|
return processedUrl.startsWith('http://') || processedUrl.startsWith('https://');
|
|
|
|
|
})
|
|
|
|
|
// 对每个有效URL进行处理,移除反引号
|
|
|
|
|
.map(url => url.replace(/`/g, '').trim());
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...product,
|
|
|
|
|
imageUrls
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 返回结果
|
|
|
|
|
sendResponse(res, true, {
|
|
|
|
|
list: processedResults,
|
|
|
|
|
total: countResults[0].total,
|
|
|
|
|
page: parseInt(page),
|
|
|
|
|
pageSize: parseInt(pageSize)
|
|
|
|
|
}, '获取货源列表成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取货源列表失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '获取货源列表失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 审核通过API
|
|
|
|
|
app.post('/api/supplies/:id/approve', async (req, res) => {
|
|
|
|
|
console.log('收到审核通过请求:', req.params.id, req.body);
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
const { remark = '' } = req.body;
|
|
|
|
|
const productId = req.params.id;
|
|
|
|
|
|
|
|
|
|
// 开始事务
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
|
|
|
|
// 检查当前状态
|
|
|
|
|
const [currentProduct] = await connection.query(
|
|
|
|
|
'SELECT status FROM products WHERE id = ?',
|
|
|
|
|
[productId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (currentProduct.length === 0) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '货源不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查状态是否可审核,只允许审核中的状态进行审核操作
|
|
|
|
|
if (!['pending_review'].includes(currentProduct[0].status)) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '该货源已审核,无需重复操作');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新状态为已审核
|
|
|
|
|
await connection.query(
|
|
|
|
|
'UPDATE products SET status = ?, audit_time = ? WHERE id = ?',
|
|
|
|
|
['published', new Date(), productId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 记录日志
|
|
|
|
|
await connection.query(
|
|
|
|
|
'INSERT INTO audit_logs (supply_id, action, user_id, remark, created_at) VALUES (?, ?, ?, ?, ?)',
|
|
|
|
|
[productId, 'approve', 'system', remark, new Date()]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 提交事务
|
|
|
|
|
await connection.commit();
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, null, '审核通过成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('审核通过失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '审核通过失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('正在注册拒绝审核API路由: /api/supplies/:id/reject');
|
|
|
|
|
|
|
|
|
|
// 审核拒绝API
|
|
|
|
|
app.post('/api/supplies/:id/reject', async (req, res) => {
|
|
|
|
|
console.log('收到审核拒绝请求:', req.params.id, req.body);
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
// 同时支持reason和rejectReason参数,保持向后兼容
|
|
|
|
|
const { reason, rejectReason = '', remark = '' } = req.body;
|
|
|
|
|
// 如果有reason参数,则使用reason,否则使用rejectReason
|
|
|
|
|
const actualRejectReason = reason || rejectReason;
|
|
|
|
|
const productId = req.params.id;
|
|
|
|
|
|
|
|
|
|
// 开始事务
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
|
|
|
|
// 检查当前状态
|
|
|
|
|
const [currentProduct] = await connection.query(
|
|
|
|
|
'SELECT status FROM products WHERE id = ?',
|
|
|
|
|
[productId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (currentProduct.length === 0) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '货源不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查状态是否可审核,只允许审核中的状态进行审核操作
|
|
|
|
|
if (!['pending_review'].includes(currentProduct[0].status)) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '当前状态不允许审核拒绝');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新状态和拒绝理由
|
|
|
|
|
await connection.query(
|
|
|
|
|
'UPDATE products SET status = ?, rejectReason = ?, audit_time = ? WHERE id = ?',
|
|
|
|
|
['rejected', actualRejectReason, new Date(), productId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 记录日志
|
|
|
|
|
await connection.query(
|
|
|
|
|
'INSERT INTO audit_logs (supply_id, action, user_id, remark, created_at) VALUES (?, ?, ?, ?, ?)',
|
|
|
|
|
[productId, 'reject', 'system', remark, new Date()]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 提交事务
|
|
|
|
|
await connection.commit();
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, null, '审核拒绝成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('审核拒绝失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '审核拒绝失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 获取供应商列表API已删除
|
|
|
|
|
|
|
|
|
|
// 供应商审核通过API已删除
|
|
|
|
|
|
|
|
|
|
// 供应商审核拒绝API已删除
|
|
|
|
|
|
|
|
|
|
// 供应商开始合作API已删除
|
|
|
|
|
|
|
|
|
|
// 供应商终止合作API已删除
|
|
|
|
|
|
|
|
|
|
console.log('正在注册测试API路由: /api/test-db');
|
|
|
|
|
|
|
|
|
|
// 测试数据库连接API
|
|
|
|
|
app.get('/api/test-db', async (req, res) => {
|
|
|
|
|
console.log('收到数据库连接测试请求');
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
const [results] = await connection.query('SELECT 1 + 1 as solution');
|
|
|
|
|
connection.release();
|
|
|
|
|
sendResponse(res, true, results[0], '数据库连接成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('数据库连接测试失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '数据库连接测试失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 登录API
|
|
|
|
|
app.post('/api/login', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const { projectName, userName, password } = req.body;
|
|
|
|
|
|
|
|
|
|
// 验证参数
|
|
|
|
|
if (!projectName || !userName || !password) {
|
|
|
|
|
return sendResponse(res, false, null, '职位名称、用户名和密码不能为空');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1. 验证职位名称是否为允许的类型
|
|
|
|
|
const allowedProjectNames = ['采购员', '管理员'];
|
|
|
|
|
if (!allowedProjectNames.includes(projectName)) {
|
|
|
|
|
return sendResponse(res, false, null, '仅允许采购员和管理员登录');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 在login表中验证登录信息
|
|
|
|
|
const userLoginConnection = await userLoginPool.getConnection();
|
|
|
|
|
const [loginResult] = await userLoginConnection.query(
|
|
|
|
|
'SELECT id, projectName, userName, managerId FROM login WHERE projectName = ? AND userName = ? AND password = ?',
|
|
|
|
|
[projectName, userName, password]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (loginResult.length === 0) {
|
|
|
|
|
userLoginConnection.release();
|
|
|
|
|
return sendResponse(res, false, null, '职位名称、用户名或密码错误');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loginInfo = loginResult[0];
|
|
|
|
|
const { managerId } = loginInfo;
|
|
|
|
|
|
|
|
|
|
// 2. 在personnel表中查询手机号码
|
|
|
|
|
const [personnelResult] = await userLoginConnection.query(
|
|
|
|
|
'SELECT phoneNumber, name FROM personnel WHERE managerId = ?',
|
|
|
|
|
[managerId]
|
|
|
|
|
);
|
|
|
|
|
userLoginConnection.release();
|
|
|
|
|
|
|
|
|
|
if (personnelResult.length === 0) {
|
|
|
|
|
return sendResponse(res, false, null, '未找到对应的员工信息');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { phoneNumber, name } = personnelResult[0];
|
|
|
|
|
|
|
|
|
|
// 3. 在users表中查询userId
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
const [userResult] = await connection.query(
|
|
|
|
|
'SELECT userId FROM users WHERE phoneNumber = ?',
|
|
|
|
|
[phoneNumber]
|
|
|
|
|
);
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
let userId = null;
|
|
|
|
|
if (userResult.length > 0) {
|
|
|
|
|
userId = userResult[0].userId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. 生成token(简单实现,实际项目中应使用JWT等安全机制)
|
|
|
|
|
const token = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
|
|
|
|
|
|
// 5. 返回登录结果
|
|
|
|
|
const userInfo = {
|
|
|
|
|
id: loginInfo.id,
|
|
|
|
|
projectName: loginInfo.projectName,
|
|
|
|
|
userName: loginInfo.userName,
|
|
|
|
|
managerId: loginInfo.managerId,
|
|
|
|
|
name: name,
|
|
|
|
|
phoneNumber: phoneNumber,
|
|
|
|
|
userId: userId
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, { userInfo, token }, '登录成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('登录失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '登录失败,请稍后重试');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 获取联系人数据API
|
|
|
|
|
app.get('/api/contacts', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
// 直接从userlogin数据库的Personnel表查询联系人信息,只查询工位名为销售员的联系人
|
|
|
|
|
const userLoginConnection = await userLoginPool.getConnection();
|
|
|
|
|
const [personnelData] = await userLoginConnection.query(
|
|
|
|
|
'SELECT id, projectName, alias, phoneNumber FROM Personnel WHERE projectName = "销售员" AND phoneNumber IS NOT NULL AND phoneNumber != ""'
|
|
|
|
|
);
|
|
|
|
|
userLoginConnection.release();
|
|
|
|
|
|
|
|
|
|
if (personnelData.length === 0) {
|
|
|
|
|
sendResponse(res, true, [], '没有找到联系人信息');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建联系人数据数组,保持与原API相同的返回格式,使用实际的数据库ID
|
|
|
|
|
const contacts = personnelData.map((person) => ({
|
|
|
|
|
id: person.id, // 使用数据库中的实际ID
|
|
|
|
|
salesPerson: person.projectName, // 销售员
|
|
|
|
|
name: person.alias, // 联系人别名
|
|
|
|
|
phoneNumber: person.phoneNumber // 电话号码
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, contacts, '联系人数据获取成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取联系人数据失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '获取联系人数据失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 供应商审核通过API - /api/suppliers/:id/approve
|
|
|
|
|
console.log('正在注册供应商审核通过API路由: /api/suppliers/:id/approve');
|
|
|
|
|
app.post('/api/suppliers/:id/approve', async (req, res) => {
|
|
|
|
|
console.log('收到供应商审核通过请求:', req.params);
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
const userId = req.params.id;
|
|
|
|
|
|
|
|
|
|
if (!userId) {
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '用户ID不能为空');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 开始事务
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
|
|
|
|
// 检查当前状态
|
|
|
|
|
const [currentUser] = await connection.query(
|
|
|
|
|
'SELECT partnerstatus FROM users WHERE userId = ?',
|
|
|
|
|
[userId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (currentUser.length === 0) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '供应商不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentUser[0].partnerstatus !== 'underreview') {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '当前状态不允许审核通过');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新状态和审核时间
|
|
|
|
|
await connection.query(
|
|
|
|
|
'UPDATE users SET partnerstatus = ?, audit_time = ? WHERE userId = ?',
|
|
|
|
|
['approved', new Date(), userId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 提交事务
|
|
|
|
|
await connection.commit();
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, null, '供应商审核通过成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('供应商审核通过失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '供应商审核通过失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 供应商审核拒绝API - /api/suppliers/:id/reject
|
|
|
|
|
console.log('正在注册供应商审核拒绝API路由: /api/suppliers/:id/reject');
|
|
|
|
|
app.post('/api/suppliers/:id/reject', async (req, res) => {
|
|
|
|
|
console.log('收到供应商审核拒绝请求:', req.params, req.body);
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
const userId = req.params.id;
|
|
|
|
|
const { rejectReason } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!userId) {
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '用户ID不能为空');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!rejectReason) {
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '审核失败原因不能为空');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 开始事务
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
|
|
|
|
// 检查当前状态
|
|
|
|
|
const [currentUser] = await connection.query(
|
|
|
|
|
'SELECT partnerstatus FROM users WHERE userId = ?',
|
|
|
|
|
[userId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (currentUser.length === 0) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '供应商不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentUser[0].partnerstatus !== 'underreview') {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '当前状态不允许审核拒绝');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新状态、审核失败原因和审核时间
|
|
|
|
|
await connection.query(
|
|
|
|
|
'UPDATE users SET partnerstatus = ?, reasonforfailure = ?, audit_time = ? WHERE userId = ?',
|
|
|
|
|
['reviewfailed', rejectReason, new Date(), userId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 提交事务
|
|
|
|
|
await connection.commit();
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, null, '供应商审核拒绝成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('供应商审核拒绝失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '供应商审核拒绝失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 供应商开始合作API - /api/suppliers/:id/cooperate
|
|
|
|
|
console.log('正在注册供应商开始合作API路由: /api/suppliers/:id/cooperate');
|
|
|
|
|
app.post('/api/suppliers/:id/cooperate', async (req, res) => {
|
|
|
|
|
console.log('收到供应商开始合作请求:', req.params);
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
const userId = req.params.id;
|
|
|
|
|
|
|
|
|
|
if (!userId) {
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '用户ID不能为空');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 开始事务
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
|
|
|
|
// 检查当前状态
|
|
|
|
|
const [currentUser] = await connection.query(
|
|
|
|
|
'SELECT partnerstatus FROM users WHERE userId = ?',
|
|
|
|
|
[userId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (currentUser.length === 0) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '供应商不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentUser[0].partnerstatus !== 'approved') {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '只有审核通过的供应商才能开始合作');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新状态
|
|
|
|
|
await connection.query(
|
|
|
|
|
'UPDATE users SET partnerstatus = ? WHERE userId = ?',
|
|
|
|
|
['incooperation', userId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 提交事务
|
|
|
|
|
await connection.commit();
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, null, '供应商开始合作成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('供应商开始合作失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '供应商开始合作失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 供应商终止合作API - /api/suppliers/:id/terminate
|
|
|
|
|
console.log('正在注册供应商终止合作API路由: /api/suppliers/:id/terminate');
|
|
|
|
|
app.post('/api/suppliers/:id/terminate', async (req, res) => {
|
|
|
|
|
console.log('收到供应商终止合作请求:', req.params, req.body);
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
const userId = req.params.id;
|
|
|
|
|
const { reason } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!userId) {
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '用户ID不能为空');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 开始事务
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
|
|
|
|
// 检查当前状态
|
|
|
|
|
const [currentUser] = await connection.query(
|
|
|
|
|
'SELECT partnerstatus FROM users WHERE userId = ?',
|
|
|
|
|
[userId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (currentUser.length === 0) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '供应商不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentUser[0].partnerstatus !== 'approved' && currentUser[0].partnerstatus !== 'incooperation') {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '只有审核通过或合作中的供应商才能终止合作');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新状态和终止原因
|
|
|
|
|
await connection.query(
|
|
|
|
|
'UPDATE users SET partnerstatus = ?, terminate_reason = ? WHERE userId = ?',
|
|
|
|
|
['notcooperative', reason, userId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 提交事务
|
|
|
|
|
await connection.commit();
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, null, '供应商终止合作成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('供应商终止合作失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '供应商终止合作失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 导入图片处理工具
|
|
|
|
|
const ImageProcessor = require('./image-processor');
|
|
|
|
|
|
|
|
|
|
// 创建货源API - /api/supplies/create
|
|
|
|
|
console.log('正在注册创建货源API路由: /api/supplies/create');
|
|
|
|
|
app.post('/api/supplies/create', async (req, res) => {
|
|
|
|
|
console.log('收到创建货源请求:', req.body);
|
|
|
|
|
let connection;
|
|
|
|
|
try {
|
|
|
|
|
connection = await pool.getConnection();
|
|
|
|
|
const { productName, costprice, quantity, grossWeight, yolk, specification, quality, region, imageUrls, sellerId, supplyStatus, description, sourceType, contactId, category, producting } = req.body;
|
|
|
|
|
|
|
|
|
|
// 开始事务
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
|
|
|
|
// 验证必填字段
|
|
|
|
|
if (!productName || !costprice || !quantity || !supplyStatus || !sourceType) {
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '商品名称、采购价、最小起订量、货源状态和货源类型不能为空');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果sellerId为空,设置一个默认值
|
|
|
|
|
if (!sellerId) {
|
|
|
|
|
sellerId = 'default_seller';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 移除重复货源检测限制,允许创建相同货源
|
|
|
|
|
console.log('移除重复货源检测限制,允许创建相同货源...');
|
|
|
|
|
|
|
|
|
|
// 处理联系人信息
|
|
|
|
|
let productContact = '';
|
|
|
|
|
let contactPhone = '';
|
|
|
|
|
if (contactId) {
|
|
|
|
|
console.log('开始处理联系人信息,contactId:', contactId);
|
|
|
|
|
// 从userlogin数据库获取联系人信息
|
|
|
|
|
const userLoginConnection = await userLoginPool.getConnection();
|
|
|
|
|
const [personnelData] = await userLoginConnection.query(
|
|
|
|
|
'SELECT alias, phoneNumber FROM Personnel WHERE projectName = "销售员" AND phoneNumber IS NOT NULL AND phoneNumber != "" AND id = ?',
|
|
|
|
|
[parseInt(contactId)] // 使用contactId直接查询对应的联系人
|
|
|
|
|
);
|
|
|
|
|
userLoginConnection.release();
|
|
|
|
|
|
|
|
|
|
console.log('查询到的联系人数据:', personnelData);
|
|
|
|
|
if (personnelData && personnelData.length > 0) {
|
|
|
|
|
productContact = personnelData[0].alias || '';
|
|
|
|
|
contactPhone = personnelData[0].phoneNumber || '';
|
|
|
|
|
console.log('获取到的联系人信息:', productContact, contactPhone);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('准备插入的联系人信息:', productContact, contactPhone);
|
|
|
|
|
|
|
|
|
|
// 生成唯一的productId
|
|
|
|
|
const productId = `product_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
|
|
|
|
|
|
|
|
|
// 处理媒体文件上传(图片和视频)
|
|
|
|
|
let uploadedImageUrls = [];
|
|
|
|
|
if (Array.isArray(imageUrls) && imageUrls.length > 0) {
|
|
|
|
|
console.log('开始处理媒体文件上传,共', imageUrls.length, '个文件');
|
|
|
|
|
|
|
|
|
|
for (const mediaUrl of imageUrls) {
|
|
|
|
|
if (mediaUrl.startsWith('data:image/') || mediaUrl.startsWith('data:video/')) {
|
|
|
|
|
// 处理DataURL
|
|
|
|
|
let base64Data, ext, fileType;
|
|
|
|
|
if (mediaUrl.startsWith('data:image/')) {
|
|
|
|
|
// 图片类型
|
|
|
|
|
base64Data = mediaUrl.replace(/^data:image\/(png|jpeg|jpg|gif);base64,/, '');
|
|
|
|
|
ext = mediaUrl.match(/^data:image\/(png|jpeg|jpg|gif);base64,/)?.[1] || 'png';
|
|
|
|
|
fileType = 'image';
|
|
|
|
|
} else {
|
|
|
|
|
// 视频类型
|
|
|
|
|
base64Data = mediaUrl.replace(/^data:video\/(mp4|mov|avi|wmv|flv);base64,/, '');
|
|
|
|
|
ext = mediaUrl.match(/^data:video\/(mp4|mov|avi|wmv|flv);base64,/)?.[1] || 'mp4';
|
|
|
|
|
fileType = 'video';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let buffer = Buffer.from(base64Data, 'base64');
|
|
|
|
|
const filename = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}.${ext}`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 不再添加水印,前端已处理
|
|
|
|
|
console.log('【水印处理】前端已添加水印,跳过后端水印处理');
|
|
|
|
|
|
|
|
|
|
// 使用OSS上传媒体文件
|
|
|
|
|
const ossUrl = await OssUploader.uploadBuffer(buffer, filename, `products/${productName || 'general'}`, fileType);
|
|
|
|
|
uploadedImageUrls.push(ossUrl);
|
|
|
|
|
console.log(`${fileType}上传成功:`, ossUrl);
|
|
|
|
|
} catch (uploadError) {
|
|
|
|
|
console.error(`${fileType}上传失败:`, uploadError.message);
|
|
|
|
|
// 继续上传其他文件,不中断流程
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 已经是URL,直接使用
|
|
|
|
|
uploadedImageUrls.push(mediaUrl);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
console.log('媒体文件处理完成,成功上传', uploadedImageUrls.length, '个文件');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建商品数据
|
|
|
|
|
const productData = {
|
|
|
|
|
productId,
|
|
|
|
|
sellerId: sellerId, // 使用前端传入的sellerId
|
|
|
|
|
productName,
|
|
|
|
|
category: category || '', // 添加种类
|
|
|
|
|
// 不再使用price字段,移除price字段
|
|
|
|
|
costprice: costprice || '', // 添加采购价
|
|
|
|
|
quantity: quantity, // 保持为逗号分隔的字符串,不转换为整数
|
|
|
|
|
grossWeight,
|
|
|
|
|
yolk,
|
|
|
|
|
specification,
|
|
|
|
|
quality,
|
|
|
|
|
producting: producting || '', // 添加产品包装
|
|
|
|
|
region,
|
|
|
|
|
status: 'published', // 直接上架,而不是审核中
|
|
|
|
|
supplyStatus: supplyStatus || '', // 预售/现货
|
|
|
|
|
sourceType: sourceType || '', // 平台货源/三方认证/三方未认证
|
|
|
|
|
description: description || '',
|
|
|
|
|
rejectReason: '',
|
|
|
|
|
imageUrls: uploadedImageUrls.length > 0 ? JSON.stringify(uploadedImageUrls) : '[]',
|
|
|
|
|
created_at: new Date(),
|
|
|
|
|
product_contact: productContact, // 添加联系人名称
|
|
|
|
|
contact_phone: contactPhone, // 添加联系人电话
|
|
|
|
|
autoOfflineTime: req.body.autoOfflineTime, // 自动下架时间
|
|
|
|
|
autoOfflineDays: req.body.autoOfflineDays, // 自动下架天数
|
|
|
|
|
autoOfflineHours: req.body.autoOfflineHours // 自动下架小时数
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 插入商品数据
|
|
|
|
|
let result;
|
|
|
|
|
try {
|
|
|
|
|
// 检查products表是否有quality字段
|
|
|
|
|
const [columns] = await connection.query('SHOW COLUMNS FROM products LIKE ?', ['quality']);
|
|
|
|
|
let insertQuery;
|
|
|
|
|
let insertParams;
|
|
|
|
|
|
|
|
|
|
if (columns.length === 0) {
|
|
|
|
|
// 没有quality字段,不包含quality字段的插入
|
|
|
|
|
insertQuery = 'INSERT INTO products (productId, sellerId, productName, category, freshness, costprice, quantity, grossWeight, yolk, specification, producting, region, status, supplyStatus, sourceType, description, rejectReason, imageUrls, created_at, audit_time, product_contact, contact_phone, autoOfflineTime, autoOfflineDays, autoOfflineHours) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
|
|
|
|
insertParams = [
|
|
|
|
|
productId, productData.sellerId, productName, category || '', req.body.freshness || '', costprice || '', quantity, grossWeight,
|
|
|
|
|
yolk, specification, producting, region, productData.status, productData.supplyStatus, productData.sourceType,
|
|
|
|
|
productData.description, productData.rejectReason, productData.imageUrls, new Date(), new Date(),
|
|
|
|
|
productContact, contactPhone, req.body.autoOfflineTime, req.body.autoOfflineDays, req.body.autoOfflineHours // 添加联系人信息和自动下架时间、小时数
|
|
|
|
|
];
|
|
|
|
|
} else {
|
|
|
|
|
// 有quality字段,包含quality字段的插入
|
|
|
|
|
insertQuery = 'INSERT INTO products (productId, sellerId, productName, category, freshness, costprice, quantity, grossWeight, yolk, specification, producting, quality, region, status, supplyStatus, sourceType, description, rejectReason, imageUrls, created_at, audit_time, product_contact, contact_phone, autoOfflineTime, autoOfflineDays, autoOfflineHours) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
|
|
|
|
insertParams = [
|
|
|
|
|
productId, productData.sellerId, productName, category || '', req.body.freshness || '', costprice || '', quantity, grossWeight,
|
|
|
|
|
yolk, specification, producting, quality, region, productData.status, productData.supplyStatus,
|
|
|
|
|
productData.sourceType, productData.description, productData.rejectReason, productData.imageUrls,
|
|
|
|
|
new Date(), new Date(), productContact, contactPhone, req.body.autoOfflineTime, req.body.autoOfflineDays, req.body.autoOfflineHours // 添加联系人信息和自动下架时间、小时数
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result = await connection.query(insertQuery, insertParams);
|
|
|
|
|
} catch (insertError) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
console.error('插入商品数据失败:', insertError.message);
|
|
|
|
|
console.error('SQL错误:', insertError.sqlMessage);
|
|
|
|
|
return sendResponse(res, false, null, `创建货源失败: ${insertError.sqlMessage}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 提交事务
|
|
|
|
|
await connection.commit();
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
// 发送WebSocket消息通知所有客户端
|
|
|
|
|
broadcastMessage({
|
|
|
|
|
type: 'supply_create',
|
|
|
|
|
supplyId: result.insertId,
|
|
|
|
|
action: 'create',
|
|
|
|
|
data: {
|
|
|
|
|
id: result.insertId,
|
|
|
|
|
productId: productId,
|
|
|
|
|
productName,
|
|
|
|
|
category: category || '',
|
|
|
|
|
// 不再使用price字段,移除price字段
|
|
|
|
|
costprice: costprice || '',
|
|
|
|
|
quantity,
|
|
|
|
|
grossWeight,
|
|
|
|
|
yolk,
|
|
|
|
|
specification,
|
|
|
|
|
producting: producting || '',
|
|
|
|
|
region,
|
|
|
|
|
status: 'published',
|
|
|
|
|
supplyStatus: supplyStatus || '',
|
|
|
|
|
sourceType: sourceType || '',
|
|
|
|
|
description: description || '',
|
|
|
|
|
sellerId,
|
|
|
|
|
imageUrls: uploadedImageUrls
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, { productId: result.insertId }, '货源创建成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (connection) {
|
|
|
|
|
try {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
} catch (rollbackError) {
|
|
|
|
|
console.error('回滚失败:', rollbackError.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
console.error('创建货源失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, `创建货源失败: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 媒体文件上传API - /api/upload-media
|
|
|
|
|
console.log('正在注册媒体文件上传API路由: /api/upload-media');
|
|
|
|
|
app.post('/api/upload-media', async (req, res) => {
|
|
|
|
|
console.log('收到媒体文件上传请求:', req.body);
|
|
|
|
|
try {
|
|
|
|
|
const { fileData, fileName, folder = 'general' } = req.body;
|
|
|
|
|
|
|
|
|
|
// 验证参数
|
|
|
|
|
if (!fileData) {
|
|
|
|
|
return sendResponse(res, false, null, '文件数据不能为空');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let base64Data, ext, fileType;
|
|
|
|
|
if (fileData.startsWith('data:image/')) {
|
|
|
|
|
// 图片类型
|
|
|
|
|
base64Data = fileData.replace(/^data:image\/(png|jpeg|jpg|gif);base64,/, '');
|
|
|
|
|
ext = fileData.match(/^data:image\/(png|jpeg|jpg|gif);base64,/)?.[1] || 'png';
|
|
|
|
|
fileType = 'image';
|
|
|
|
|
} else if (fileData.startsWith('data:video/')) {
|
|
|
|
|
// 视频类型
|
|
|
|
|
base64Data = fileData.replace(/^data:video\/(mp4|mov|avi|wmv|flv);base64,/, '');
|
|
|
|
|
ext = fileData.match(/^data:video\/(mp4|mov|avi|wmv|flv);base64,/)?.[1] || 'mp4';
|
|
|
|
|
fileType = 'video';
|
|
|
|
|
} else {
|
|
|
|
|
return sendResponse(res, false, null, '不支持的文件类型');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let buffer = Buffer.from(base64Data, 'base64');
|
|
|
|
|
const filename = fileName || `${Date.now()}-${Math.random().toString(36).substr(2, 9)}.${ext}`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 使用OSS上传媒体文件
|
|
|
|
|
const ossUrl = await OssUploader.uploadBuffer(buffer, filename, `uploads/${folder}`, fileType);
|
|
|
|
|
console.log(`${fileType}上传成功:`, ossUrl);
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, {
|
|
|
|
|
url: ossUrl,
|
|
|
|
|
fileType: fileType,
|
|
|
|
|
message: `${fileType}上传成功`
|
|
|
|
|
}, `${fileType}上传成功`);
|
|
|
|
|
} catch (uploadError) {
|
|
|
|
|
console.error(`${fileType}上传失败:`, uploadError.message);
|
|
|
|
|
sendResponse(res, false, null, `${fileType}上传失败: ${uploadError.message}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('媒体文件上传API错误:', error.message);
|
|
|
|
|
sendResponse(res, false, null, `媒体文件上传失败: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 图片上传API - /api/upload-image(兼容旧接口)
|
|
|
|
|
console.log('正在注册图片上传API路由: /api/upload-image');
|
|
|
|
|
app.post('/api/upload-image', async (req, res) => {
|
|
|
|
|
console.log('收到图片上传请求,转发到媒体文件上传API');
|
|
|
|
|
// 将请求转发到媒体文件上传API
|
|
|
|
|
const uploadMediaHandler = app._router.stack.find(layer =>
|
|
|
|
|
layer.route && layer.route.path === '/api/upload-media' && layer.route.methods['post']
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (uploadMediaHandler && uploadMediaHandler.route && uploadMediaHandler.route.stack[0]) {
|
|
|
|
|
return uploadMediaHandler.route.stack[0].handle(req, res);
|
|
|
|
|
} else {
|
|
|
|
|
sendResponse(res, false, null, '图片上传API内部错误');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 视频上传API - /api/upload-video(专门的视频上传接口)
|
|
|
|
|
console.log('正在注册视频上传API路由: /api/upload-video');
|
|
|
|
|
app.post('/api/upload-video', async (req, res) => {
|
|
|
|
|
console.log('收到视频上传请求,转发到媒体文件上传API');
|
|
|
|
|
// 将请求转发到媒体文件上传API
|
|
|
|
|
const uploadMediaHandler = app._router.stack.find(layer =>
|
|
|
|
|
layer.route && layer.route.path === '/api/upload-media' && layer.route.methods['post']
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (uploadMediaHandler && uploadMediaHandler.route && uploadMediaHandler.route.stack[0]) {
|
|
|
|
|
return uploadMediaHandler.route.stack[0].handle(req, res);
|
|
|
|
|
} else {
|
|
|
|
|
sendResponse(res, false, null, '视频上传API内部错误');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 获取审核失败原因API - /api/supplies/:id/reject-reason
|
|
|
|
|
console.log('正在注册获取审核失败原因API路由: /api/supplies/:id/reject-reason');
|
|
|
|
|
app.get('/api/supplies/:id/reject-reason', async (req, res) => {
|
|
|
|
|
console.log('收到获取审核失败原因请求:', req.params.id);
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
const supplyId = req.params.id;
|
|
|
|
|
|
|
|
|
|
// 查询该货源的拒绝原因
|
|
|
|
|
const [supply] = await connection.query(
|
|
|
|
|
'SELECT rejectReason FROM products WHERE id = ?',
|
|
|
|
|
[supplyId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
if (supply.length === 0) {
|
|
|
|
|
return sendResponse(res, false, null, '货源不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, {
|
|
|
|
|
rejectReason: supply[0].rejectReason
|
|
|
|
|
}, '获取审核失败原因成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取审核失败原因失败:', error.message);
|
|
|
|
|
sendResponse(res, false, null, '获取审核失败原因失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 下架货源API - /api/supplies/:id/unpublish
|
|
|
|
|
console.log('正在注册下架货源API路由: /api/supplies/:id/unpublish');
|
|
|
|
|
app.post('/api/supplies/:id/unpublish', async (req, res) => {
|
|
|
|
|
console.log('收到下架货源请求:', req.params.id);
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
const productId = req.params.id;
|
|
|
|
|
|
|
|
|
|
// 开始事务
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
|
|
|
|
// 检查当前状态
|
|
|
|
|
const [currentProduct] = await connection.query(
|
|
|
|
|
'SELECT status FROM products WHERE id = ?',
|
|
|
|
|
[productId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (currentProduct.length === 0) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '货源不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新状态为已下架,并将标签设置为已锁定
|
|
|
|
|
await connection.query(
|
|
|
|
|
'UPDATE products SET status = ?, label = 1, updated_at = NOW() WHERE id = ?',
|
|
|
|
|
['sold_out', productId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 提交事务
|
|
|
|
|
await connection.commit();
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
// 发送WebSocket消息通知所有客户端
|
|
|
|
|
broadcastMessage({
|
|
|
|
|
type: 'supply_status_change',
|
|
|
|
|
supplyId: productId,
|
|
|
|
|
action: 'unpublish',
|
|
|
|
|
status: 'sold_out'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, null, '货源下架成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('下架货源失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '货源下架失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 上架货源API - /api/supplies/:id/publish
|
|
|
|
|
console.log('正在注册上架货源API路由: /api/supplies/:id/publish');
|
|
|
|
|
app.post('/api/supplies/:id/publish', async (req, res) => {
|
|
|
|
|
console.log('收到上架货源请求:', req.params.id);
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
const productId = req.params.id;
|
|
|
|
|
|
|
|
|
|
// 开始事务
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
|
|
|
|
// 检查当前状态和label
|
|
|
|
|
const [currentProduct] = await connection.query(
|
|
|
|
|
'SELECT status, label FROM products WHERE id = ?',
|
|
|
|
|
[productId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (currentProduct.length === 0) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '货源不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查是否被锁定
|
|
|
|
|
if (currentProduct[0].label === 1) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '货源已被锁定,无法上架');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新状态为已发布(直接上架,不需要审核),同时更新updated_at和autoOfflineHours
|
|
|
|
|
await connection.query(
|
|
|
|
|
'UPDATE products SET status = ?, updated_at = NOW() WHERE id = ?',
|
|
|
|
|
['published', productId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 提交事务
|
|
|
|
|
await connection.commit();
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
// 发送WebSocket消息通知所有客户端
|
|
|
|
|
broadcastMessage({
|
|
|
|
|
type: 'supply_status_change',
|
|
|
|
|
supplyId: productId,
|
|
|
|
|
action: 'publish',
|
|
|
|
|
status: 'published'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, null, '货源上架成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('上架货源失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '货源上架失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 删除货源API - /api/supplies/:id/delete
|
|
|
|
|
console.log('正在注册删除货源API路由: /api/supplies/:id/delete');
|
|
|
|
|
app.post('/api/supplies/:id/delete', async (req, res) => {
|
|
|
|
|
console.log('收到删除货源请求:', req.params.id);
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
const productId = req.params.id;
|
|
|
|
|
|
|
|
|
|
// 开始事务
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
|
|
|
|
// 检查当前状态
|
|
|
|
|
const [currentProduct] = await connection.query(
|
|
|
|
|
'SELECT status FROM products WHERE id = ?',
|
|
|
|
|
[productId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (currentProduct.length === 0) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '货源不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 将货源状态改为hidden,而不是删除
|
|
|
|
|
await connection.query(
|
|
|
|
|
'UPDATE products SET status = ? WHERE id = ?',
|
|
|
|
|
['hidden', productId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 提交事务
|
|
|
|
|
await connection.commit();
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
// 发送WebSocket消息通知所有客户端
|
|
|
|
|
broadcastMessage({
|
|
|
|
|
type: 'supply_update',
|
|
|
|
|
supplyId: productId,
|
|
|
|
|
action: 'update'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, null, '货源已删除');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('删除货源失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '货源删除失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 更新货源状态API - /api/supplies/:id
|
|
|
|
|
console.log('正在注册更新货源状态API路由: /api/supplies/:id');
|
|
|
|
|
app.put('/api/supplies/:id', async (req, res) => {
|
|
|
|
|
console.log('收到更新货源状态请求:', req.params.id, req.body);
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
const productId = req.params.id;
|
|
|
|
|
const { status } = req.body;
|
|
|
|
|
|
|
|
|
|
// 验证状态参数
|
|
|
|
|
if (!status) {
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '状态不能为空');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 开始事务
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
|
|
|
|
// 检查当前状态
|
|
|
|
|
const [currentProduct] = await connection.query(
|
|
|
|
|
'SELECT status FROM products WHERE id = ?',
|
|
|
|
|
[productId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (currentProduct.length === 0) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '货源不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新状态,当状态为sold_out时同时锁定label
|
|
|
|
|
if (status === 'sold_out') {
|
|
|
|
|
await connection.query(
|
|
|
|
|
'UPDATE products SET status = ?, label = 1, updated_at = NOW() WHERE id = ?',
|
|
|
|
|
[status, productId]
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
await connection.query(
|
|
|
|
|
'UPDATE products SET status = ?, updated_at = NOW() WHERE id = ?',
|
|
|
|
|
[status, productId]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 提交事务
|
|
|
|
|
await connection.commit();
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
// 发送WebSocket消息通知所有客户端
|
|
|
|
|
broadcastMessage({
|
|
|
|
|
type: 'supply_status_change',
|
|
|
|
|
supplyId: productId,
|
|
|
|
|
action: 'update_status',
|
|
|
|
|
status: status
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, null, '状态更新成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('更新货源状态失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '更新货源状态失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 编辑货源API - /api/supplies/:id/edit
|
|
|
|
|
console.log('正在注册编辑货源API路由: /api/supplies/:id/edit');
|
|
|
|
|
app.put('/api/supplies/:id/edit', async (req, res) => {
|
|
|
|
|
console.log('收到编辑货源请求:', req.params.id);
|
|
|
|
|
console.log('请求体中的category:', req.body.category);
|
|
|
|
|
console.log('请求体中的sourceType:', req.body.sourceType);
|
|
|
|
|
console.log('请求体中的freshness:', req.body.freshness);
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
const productId = req.params.id;
|
|
|
|
|
const { productName, costprice, quantity, grossWeight, yolk, specification, supplyStatus, description, region, contactId, producting, imageUrls, autoOfflineTime, category, sourceType, freshness, autoOfflineHours } = req.body;
|
|
|
|
|
|
|
|
|
|
// 开始事务
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
|
|
|
|
// 检查当前状态和label
|
|
|
|
|
const [currentProduct] = await connection.query(
|
|
|
|
|
'SELECT status, label FROM products WHERE id = ?',
|
|
|
|
|
[productId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (currentProduct.length === 0) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '货源不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查是否被锁定
|
|
|
|
|
if (currentProduct[0].label === 1) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
connection.release();
|
|
|
|
|
return sendResponse(res, false, null, '货源已被锁定,无法编辑');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理联系人信息:只在有新联系人ID且能查询到时才更新,否则保持原有值
|
|
|
|
|
let productContact = null;
|
|
|
|
|
let contactPhone = null;
|
|
|
|
|
|
|
|
|
|
// 先查询原有联系人信息
|
|
|
|
|
const [existingProduct] = await connection.query(
|
|
|
|
|
'SELECT product_contact, contact_phone FROM products WHERE id = ?',
|
|
|
|
|
[productId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 如果没有提供contactId或contactId为空,使用原有联系人信息
|
|
|
|
|
if (!contactId || contactId === '') {
|
|
|
|
|
productContact = existingProduct[0].product_contact;
|
|
|
|
|
contactPhone = existingProduct[0].contact_phone;
|
|
|
|
|
} else {
|
|
|
|
|
// 从userlogin数据库获取联系人信息
|
|
|
|
|
const userLoginConnection = await userLoginPool.getConnection();
|
|
|
|
|
const [personnelData] = await userLoginConnection.query(
|
|
|
|
|
'SELECT alias, phoneNumber FROM Personnel WHERE projectName = "销售员" AND phoneNumber IS NOT NULL AND phoneNumber != "" AND id = ?',
|
|
|
|
|
[parseInt(contactId)] // 使用contactId直接查询对应的联系人
|
|
|
|
|
);
|
|
|
|
|
userLoginConnection.release();
|
|
|
|
|
|
|
|
|
|
if (personnelData && personnelData.length > 0) {
|
|
|
|
|
// 成功查询到联系人,使用新联系人信息
|
|
|
|
|
productContact = personnelData[0].alias || '';
|
|
|
|
|
contactPhone = personnelData[0].phoneNumber || '';
|
|
|
|
|
} else {
|
|
|
|
|
// 未查询到联系人,使用原有联系人信息
|
|
|
|
|
productContact = existingProduct[0].product_contact;
|
|
|
|
|
contactPhone = existingProduct[0].contact_phone;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理媒体文件上传(图片和视频)
|
|
|
|
|
let uploadedImageUrls = [];
|
|
|
|
|
if (imageUrls && Array.isArray(imageUrls) && imageUrls.length > 0) {
|
|
|
|
|
console.log('开始处理编辑媒体文件上传,共', imageUrls.length, '个文件');
|
|
|
|
|
|
|
|
|
|
for (const mediaUrl of imageUrls) {
|
|
|
|
|
if (mediaUrl.startsWith('data:image/') || mediaUrl.startsWith('data:video/')) {
|
|
|
|
|
// 处理DataURL
|
|
|
|
|
let base64Data, ext, fileType;
|
|
|
|
|
if (mediaUrl.startsWith('data:image/')) {
|
|
|
|
|
// 图片类型
|
|
|
|
|
base64Data = mediaUrl.replace(/^data:image\/(png|jpeg|jpg|gif);base64,/, '');
|
|
|
|
|
ext = mediaUrl.match(/^data:image\/(png|jpeg|jpg|gif);base64,/)?.[1] || 'png';
|
|
|
|
|
fileType = 'image';
|
|
|
|
|
} else {
|
|
|
|
|
// 视频类型
|
|
|
|
|
base64Data = mediaUrl.replace(/^data:video\/(mp4|mov|avi|wmv|flv);base64,/, '');
|
|
|
|
|
ext = mediaUrl.match(/^data:video\/(mp4|mov|avi|wmv|flv);base64,/)?.[1] || 'mp4';
|
|
|
|
|
fileType = 'video';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let buffer = Buffer.from(base64Data, 'base64');
|
|
|
|
|
const filename = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}.${ext}`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 不再添加水印,前端已处理
|
|
|
|
|
console.log('【水印处理】前端已添加水印,跳过后端水印处理');
|
|
|
|
|
|
|
|
|
|
// 使用OSS上传媒体文件
|
|
|
|
|
const ossUrl = await OssUploader.uploadBuffer(buffer, filename, `products/${productName || 'general'}`, fileType);
|
|
|
|
|
uploadedImageUrls.push(ossUrl);
|
|
|
|
|
console.log(`${fileType}上传成功:`, ossUrl);
|
|
|
|
|
} catch (uploadError) {
|
|
|
|
|
console.error(`${fileType}上传失败:`, uploadError.message);
|
|
|
|
|
// 继续上传其他文件,不中断流程
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 已经是URL,直接使用
|
|
|
|
|
uploadedImageUrls.push(mediaUrl);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
console.log('媒体文件处理完成,成功上传', uploadedImageUrls.length, '个文件');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理数量:保存所有数量值,与规格保持一致
|
|
|
|
|
let quantityValue = quantity; // 直接保存前端提交的数量字符串
|
|
|
|
|
// 如果是数字,转换为字符串
|
|
|
|
|
if (typeof quantity === 'number') {
|
|
|
|
|
quantityValue = quantity.toString();
|
|
|
|
|
} else if (typeof quantity === 'string' && !isNaN(parseFloat(quantity)) && isFinite(quantity)) {
|
|
|
|
|
// 如果是单个数字字符串,直接使用
|
|
|
|
|
quantityValue = quantity;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 只有当有新上传的图片时,才更新imageUrls,否则保留原有值
|
|
|
|
|
let imageUrlsToUpdate = null;
|
|
|
|
|
if (uploadedImageUrls.length > 0) {
|
|
|
|
|
imageUrlsToUpdate = JSON.stringify(uploadedImageUrls);
|
|
|
|
|
} else {
|
|
|
|
|
// 如果没有新上传的图片,查询原有图片URL
|
|
|
|
|
const [existingProduct] = await connection.query(
|
|
|
|
|
'SELECT imageUrls FROM products WHERE id = ?',
|
|
|
|
|
[productId]
|
|
|
|
|
);
|
|
|
|
|
imageUrlsToUpdate = existingProduct[0].imageUrls;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 打印用于调试的更新参数
|
|
|
|
|
console.log('更新参数调试:');
|
|
|
|
|
console.log('productName:', productName);
|
|
|
|
|
// 不再使用price字段,移除price调试日志
|
|
|
|
|
console.log('costprice:', costprice || '');
|
|
|
|
|
console.log('quantityValue:', quantityValue);
|
|
|
|
|
console.log('grossWeight:', grossWeight);
|
|
|
|
|
console.log('yolk:', yolk);
|
|
|
|
|
console.log('specification:', specification);
|
|
|
|
|
console.log('producting:', producting);
|
|
|
|
|
console.log('supplyStatus:', supplyStatus);
|
|
|
|
|
console.log('description:', description);
|
|
|
|
|
console.log('region:', region);
|
|
|
|
|
console.log('category:', category);
|
|
|
|
|
console.log('sourceType:', sourceType);
|
|
|
|
|
console.log('freshness:', freshness);
|
|
|
|
|
console.log('productContact:', productContact);
|
|
|
|
|
console.log('contactPhone:', contactPhone);
|
|
|
|
|
console.log('imageUrlsToUpdate:', imageUrlsToUpdate);
|
|
|
|
|
console.log('autoOfflineTime:', autoOfflineTime || null);
|
|
|
|
|
console.log('autoOfflineHours:', autoOfflineHours || 24);
|
|
|
|
|
console.log('productId:', productId);
|
|
|
|
|
|
|
|
|
|
// 更新货源信息
|
|
|
|
|
const updateQuery = `
|
|
|
|
|
UPDATE products
|
|
|
|
|
SET productName = ?, costprice = ?, quantity = ?, grossWeight = ?,
|
|
|
|
|
yolk = ?, specification = ?, producting = ?, supplyStatus = ?, description = ?, region = ?,
|
|
|
|
|
category = ?, sourceType = ?, freshness = ?,
|
|
|
|
|
product_contact = ?, contact_phone = ?, imageUrls = ?,
|
|
|
|
|
autoOfflineTime = ?, autoOfflineHours = ?, updated_at = ?
|
|
|
|
|
WHERE id = ?
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const [result] = await connection.query(updateQuery, [
|
|
|
|
|
productName, costprice || '', quantityValue, grossWeight,
|
|
|
|
|
yolk, specification, producting, supplyStatus, description, region,
|
|
|
|
|
category, sourceType, freshness,
|
|
|
|
|
productContact, contactPhone, imageUrlsToUpdate,
|
|
|
|
|
autoOfflineTime || null, // 自动下架时间
|
|
|
|
|
autoOfflineHours || 24, // 默认24小时
|
|
|
|
|
new Date(), // 更新updated_at字段
|
|
|
|
|
productId
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
console.log('更新结果:', result);
|
|
|
|
|
|
|
|
|
|
// 提交事务
|
|
|
|
|
await connection.commit();
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
// 发送WebSocket消息通知所有客户端
|
|
|
|
|
broadcastMessage({
|
|
|
|
|
type: 'supply_update',
|
|
|
|
|
supplyId: productId,
|
|
|
|
|
action: 'update',
|
|
|
|
|
data: {
|
|
|
|
|
id: productId,
|
|
|
|
|
productName,
|
|
|
|
|
// 不再使用price字段,移除price字段
|
|
|
|
|
costprice: costprice || '',
|
|
|
|
|
quantity,
|
|
|
|
|
grossWeight,
|
|
|
|
|
yolk,
|
|
|
|
|
specification,
|
|
|
|
|
category,
|
|
|
|
|
sourceType,
|
|
|
|
|
freshness,
|
|
|
|
|
producting,
|
|
|
|
|
supplyStatus,
|
|
|
|
|
description,
|
|
|
|
|
region,
|
|
|
|
|
imageUrls: uploadedImageUrls.length > 0 ? uploadedImageUrls : imageUrls
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, null, '货源编辑成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('编辑货源失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '货源编辑失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 供应商列表查询API - /api/suppliers
|
|
|
|
|
console.log('正在注册供应商列表查询API路由: /api/suppliers');
|
|
|
|
|
app.get('/api/suppliers', async (req, res) => {
|
|
|
|
|
console.log('收到供应商列表查询请求:', req.query);
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
const { page = 1, pageSize = 10, status = '', keyword = '', phoneNumber = '' } = req.query;
|
|
|
|
|
|
|
|
|
|
// 构建查询条件
|
|
|
|
|
let whereClause = '';
|
|
|
|
|
let params = [];
|
|
|
|
|
|
|
|
|
|
// 添加状态筛选
|
|
|
|
|
if (status) {
|
|
|
|
|
whereClause += ` WHERE partnerstatus = ?`;
|
|
|
|
|
params.push(status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加关键词搜索
|
|
|
|
|
if (keyword) {
|
|
|
|
|
whereClause += status ? ' AND' : ' WHERE';
|
|
|
|
|
whereClause += ` (username LIKE ? OR company LIKE ? OR phoneNumber LIKE ?)`;
|
|
|
|
|
params.push(`%${keyword}%`, `%${keyword}%`, `%${keyword}%`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加手机号搜索(优先级高于keyword中的手机号搜索)
|
|
|
|
|
if (phoneNumber) {
|
|
|
|
|
whereClause += (status || keyword) ? ' AND' : ' WHERE';
|
|
|
|
|
whereClause += ` phoneNumber LIKE ?`;
|
|
|
|
|
params.push(`%${phoneNumber}%`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取总数
|
|
|
|
|
const [totalResult] = await connection.query(
|
|
|
|
|
`SELECT COUNT(*) as total FROM users${whereClause}`,
|
|
|
|
|
params
|
|
|
|
|
);
|
|
|
|
|
const total = totalResult[0].total;
|
|
|
|
|
|
|
|
|
|
// 计算分页
|
|
|
|
|
const offset = (page - 1) * pageSize;
|
|
|
|
|
params.push(parseInt(pageSize), offset);
|
|
|
|
|
|
|
|
|
|
// 查询供应商列表
|
|
|
|
|
const [suppliers] = await connection.query(
|
|
|
|
|
`SELECT userId, phoneNumber, province, city, district, detailedaddress, company, collaborationid, cooperation, businesslicenseurl, proofurl, brandurl, partnerstatus, reasonforfailure, reject_reason, terminate_reason, audit_time
|
|
|
|
|
FROM users${whereClause}
|
|
|
|
|
ORDER BY audit_time DESC LIMIT ? OFFSET ?`,
|
|
|
|
|
params
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, {
|
|
|
|
|
list: suppliers,
|
|
|
|
|
total,
|
|
|
|
|
page: parseInt(page),
|
|
|
|
|
pageSize: parseInt(pageSize)
|
|
|
|
|
}, '查询成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('供应商列表查询失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '供应商列表查询失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 管理员统计 - 获取货源创建统计数据
|
|
|
|
|
app.get('/api/admin/stats/supplies', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const { filter } = req.query;
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
|
|
|
|
|
// 计算时间范围
|
|
|
|
|
let timeCondition = '';
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
|
|
|
|
if (filter === 'today') {
|
|
|
|
|
// 今天
|
|
|
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
|
|
|
timeCondition = `AND created_at >= '${today.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
|
|
|
} else if (filter === 'yesterday') {
|
|
|
|
|
// 昨天
|
|
|
|
|
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
|
|
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
|
|
|
timeCondition = `AND created_at >= '${yesterday.toISOString().slice(0, 19).replace('T', ' ')}' AND created_at < '${today.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
|
|
|
} else if (filter === 'beforeYesterday') {
|
|
|
|
|
// 前天
|
|
|
|
|
const beforeYesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 2);
|
|
|
|
|
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
|
|
|
|
timeCondition = `AND created_at >= '${beforeYesterday.toISOString().slice(0, 19).replace('T', ' ')}' AND created_at < '${yesterday.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
|
|
|
} else if (filter === 'week') {
|
|
|
|
|
// 本周
|
|
|
|
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
|
|
|
timeCondition = `AND created_at >= '${weekAgo.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
|
|
|
} else if (filter === 'month') {
|
|
|
|
|
// 本月
|
|
|
|
|
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
|
|
|
timeCondition = `AND created_at >= '${monthAgo.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
|
|
|
} else if (filter === 'custom') {
|
|
|
|
|
// 自定义时间范围
|
|
|
|
|
const { startDate, endDate } = req.query;
|
|
|
|
|
if (startDate) {
|
|
|
|
|
// 开始日期,格式化为YYYY-MM-DD 00:00:00
|
|
|
|
|
const start = new Date(startDate);
|
|
|
|
|
start.setHours(0, 0, 0, 0);
|
|
|
|
|
timeCondition += `AND created_at >= '${start.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
|
|
|
}
|
|
|
|
|
if (endDate) {
|
|
|
|
|
// 结束日期,格式化为YYYY-MM-DD 23:59:59
|
|
|
|
|
const end = new Date(endDate);
|
|
|
|
|
end.setHours(23, 59, 59, 999);
|
|
|
|
|
timeCondition += `AND created_at <= '${end.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取每个卖家创建的货源数量,关联users表获取nickName,过滤掉hidden状态的货源
|
|
|
|
|
// 替换created_at为p.created_at,避免歧义
|
|
|
|
|
const chartTimeCondition = timeCondition.replace(/created_at/g, 'p.created_at');
|
|
|
|
|
const [chartData] = await connection.query(`
|
|
|
|
|
SELECT p.sellerId, u.nickName, COUNT(*) as count
|
|
|
|
|
FROM products p
|
|
|
|
|
LEFT JOIN users u ON p.sellerId = u.userId
|
|
|
|
|
WHERE 1=1 AND p.status != 'hidden' ${chartTimeCondition}
|
|
|
|
|
GROUP BY p.sellerId, u.nickName
|
|
|
|
|
ORDER BY count DESC
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
// 获取总体统计信息,过滤掉hidden状态的货源
|
|
|
|
|
const [totalSuppliesResult] = await connection.query(`
|
|
|
|
|
SELECT COUNT(*) as total FROM products WHERE 1=1 AND status != 'hidden' ${timeCondition}
|
|
|
|
|
`);
|
|
|
|
|
const totalSupplies = totalSuppliesResult[0].total;
|
|
|
|
|
|
|
|
|
|
const [totalUsersResult] = await connection.query(`
|
|
|
|
|
SELECT COUNT(DISTINCT sellerId) as total FROM products WHERE 1=1 AND status != 'hidden' ${timeCondition}
|
|
|
|
|
`);
|
|
|
|
|
const totalUsers = totalUsersResult[0].total;
|
|
|
|
|
|
|
|
|
|
const avgPerUser = totalUsers > 0 ? totalSupplies / totalUsers : 0;
|
|
|
|
|
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, {
|
|
|
|
|
chartData,
|
|
|
|
|
stats: {
|
|
|
|
|
totalSupplies,
|
|
|
|
|
totalUsers,
|
|
|
|
|
avgPerUser
|
|
|
|
|
}
|
|
|
|
|
}, '获取统计数据成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取统计数据失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '获取统计数据失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 管理员统计 - 获取指定卖家的货源列表
|
|
|
|
|
app.get('/api/admin/supplies', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const { sellerId, filter } = req.query;
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
|
|
|
|
|
// 计算时间范围
|
|
|
|
|
let timeCondition = '';
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
|
|
|
|
if (filter === 'today') {
|
|
|
|
|
// 今天
|
|
|
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
|
|
|
timeCondition = `AND created_at >= '${today.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
|
|
|
} else if (filter === 'yesterday') {
|
|
|
|
|
// 昨天
|
|
|
|
|
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
|
|
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
|
|
|
timeCondition = `AND created_at >= '${yesterday.toISOString().slice(0, 19).replace('T', ' ')}' AND created_at < '${today.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
|
|
|
} else if (filter === 'beforeYesterday') {
|
|
|
|
|
// 前天
|
|
|
|
|
const beforeYesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 2);
|
|
|
|
|
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
|
|
|
|
timeCondition = `AND created_at >= '${beforeYesterday.toISOString().slice(0, 19).replace('T', ' ')}' AND created_at < '${yesterday.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
|
|
|
} else if (filter === 'week') {
|
|
|
|
|
// 本周
|
|
|
|
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
|
|
|
timeCondition = `AND created_at >= '${weekAgo.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
|
|
|
} else if (filter === 'month') {
|
|
|
|
|
// 本月
|
|
|
|
|
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
|
|
|
timeCondition = `AND created_at >= '${monthAgo.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
|
|
|
} else if (filter === 'custom') {
|
|
|
|
|
// 自定义时间范围
|
|
|
|
|
const { startDate, endDate } = req.query;
|
|
|
|
|
if (startDate) {
|
|
|
|
|
// 开始日期,格式化为YYYY-MM-DD 00:00:00
|
|
|
|
|
const start = new Date(startDate);
|
|
|
|
|
start.setHours(0, 0, 0, 0);
|
|
|
|
|
timeCondition += `AND created_at >= '${start.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
|
|
|
}
|
|
|
|
|
if (endDate) {
|
|
|
|
|
// 结束日期,格式化为YYYY-MM-DD 23:59:59
|
|
|
|
|
const end = new Date(endDate);
|
|
|
|
|
end.setHours(23, 59, 59, 999);
|
|
|
|
|
timeCondition += `AND created_at <= '${end.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取指定卖家的货源列表,关联users表获取创建人姓名,过滤掉hidden状态的货源
|
|
|
|
|
// 替换created_at为p.created_at,避免歧义
|
|
|
|
|
const suppliesTimeCondition = timeCondition.replace(/created_at/g, 'p.created_at');
|
|
|
|
|
const [supplies] = await connection.query(`
|
|
|
|
|
SELECT p.*, u.nickName
|
|
|
|
|
FROM products p
|
|
|
|
|
LEFT JOIN users u ON p.sellerId = u.userId
|
|
|
|
|
WHERE p.sellerId = ? AND p.status != 'hidden' ${suppliesTimeCondition}
|
|
|
|
|
ORDER BY p.created_at DESC
|
|
|
|
|
`, [sellerId]);
|
|
|
|
|
|
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
|
|
sendResponse(res, true, { supplies }, '获取货源列表成功');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取货源列表失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
sendResponse(res, false, null, '获取货源列表失败');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 首页路由 - 根据角色重定向
|
|
|
|
|
app.get('/', (req, res) => {
|
|
|
|
|
// 注意:这里无法直接获取localStorage中的用户信息,因为localStorage是客户端的
|
|
|
|
|
// 实际生产环境中,应该通过cookie或token来获取用户角色信息
|
|
|
|
|
// 这里我们默认跳转到登录页面,让客户端处理角色跳转
|
|
|
|
|
res.sendFile(path.join(__dirname, 'login.html'));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 货源管理页面路由
|
|
|
|
|
app.get('/management', (req, res) => {
|
|
|
|
|
res.sendFile(path.join(__dirname, 'Management.html'));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 错误处理中间件
|
|
|
|
|
app.use((err, req, res, next) => {
|
|
|
|
|
console.error('服务器错误:', err.message);
|
|
|
|
|
console.error('错误详情:', err);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: '服务器内部错误'
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 启动服务器
|
|
|
|
|
async function startServer() {
|
|
|
|
|
try {
|
|
|
|
|
await initDatabase();
|
|
|
|
|
|
|
|
|
|
server.listen(PORT, () => {
|
|
|
|
|
console.log(`服务器已启动,监听端口 ${PORT}`);
|
|
|
|
|
console.log(`访问地址: http://localhost:${PORT}`);
|
|
|
|
|
console.log(`WebSocket服务已启动,端口: ${PORT}`);
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('服务器启动失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
// 如果启动失败,尝试重新启动
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
console.log('尝试重新启动服务器...');
|
|
|
|
|
startServer();
|
|
|
|
|
}, 5000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确保数据库结构
|
|
|
|
|
async function ensureDatabaseSchema() {
|
|
|
|
|
console.log('开始执行数据库结构检查...');
|
|
|
|
|
try {
|
|
|
|
|
const connection = await pool.getConnection();
|
|
|
|
|
console.log('获取数据库连接成功');
|
|
|
|
|
|
|
|
|
|
// 检查users表是否有必要的字段
|
|
|
|
|
console.log('检查users表是否有partnerstatus字段...');
|
|
|
|
|
const [partnerStatusColumns] = await connection.query(
|
|
|
|
|
'SHOW COLUMNS FROM `users` LIKE ?',
|
|
|
|
|
['partnerstatus']
|
|
|
|
|
);
|
|
|
|
|
console.log('检查表字段结果:', partnerStatusColumns.length > 0 ? '已存在' : '不存在');
|
|
|
|
|
|
|
|
|
|
if (partnerStatusColumns.length === 0) {
|
|
|
|
|
console.log('添加partnerstatus字段到users表...');
|
|
|
|
|
await connection.query(
|
|
|
|
|
'ALTER TABLE `users` ADD COLUMN partnerstatus VARCHAR(50) DEFAULT "underreview" COMMENT "合作商状态"'
|
|
|
|
|
);
|
|
|
|
|
console.log('partnerstatus字段添加成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('检查users表是否有reject_reason字段...');
|
|
|
|
|
const [rejectReasonColumns] = await connection.query(
|
|
|
|
|
'SHOW COLUMNS FROM `users` LIKE ?',
|
|
|
|
|
['reject_reason']
|
|
|
|
|
);
|
|
|
|
|
console.log('检查表字段结果:', rejectReasonColumns.length > 0 ? '已存在' : '不存在');
|
|
|
|
|
|
|
|
|
|
if (rejectReasonColumns.length === 0) {
|
|
|
|
|
console.log('添加reject_reason字段到users表...');
|
|
|
|
|
await connection.query(
|
|
|
|
|
'ALTER TABLE `users` ADD COLUMN reject_reason TEXT COMMENT "拒绝理由"'
|
|
|
|
|
);
|
|
|
|
|
console.log('reject_reason字段添加成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('检查users表是否有terminate_reason字段...');
|
|
|
|
|
const [terminateReasonColumns] = await connection.query(
|
|
|
|
|
'SHOW COLUMNS FROM `users` LIKE ?',
|
|
|
|
|
['terminate_reason']
|
|
|
|
|
);
|
|
|
|
|
console.log('检查表字段结果:', terminateReasonColumns.length > 0 ? '已存在' : '不存在');
|
|
|
|
|
|
|
|
|
|
if (terminateReasonColumns.length === 0) {
|
|
|
|
|
console.log('添加terminate_reason字段到users表...');
|
|
|
|
|
await connection.query(
|
|
|
|
|
'ALTER TABLE `users` ADD COLUMN terminate_reason TEXT COMMENT "终止合作理由"'
|
|
|
|
|
);
|
|
|
|
|
console.log('terminate_reason字段添加成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('检查users表是否有audit_time字段...');
|
|
|
|
|
const [userAuditTimeColumns] = await connection.query(
|
|
|
|
|
'SHOW COLUMNS FROM `users` LIKE ?',
|
|
|
|
|
['audit_time']
|
|
|
|
|
);
|
|
|
|
|
console.log('检查表字段结果:', userAuditTimeColumns.length > 0 ? '已存在' : '不存在');
|
|
|
|
|
|
|
|
|
|
if (userAuditTimeColumns.length === 0) {
|
|
|
|
|
console.log('添加audit_time字段到users表...');
|
|
|
|
|
await connection.query(
|
|
|
|
|
'ALTER TABLE `users` ADD COLUMN audit_time DATETIME COMMENT "审核时间"'
|
|
|
|
|
);
|
|
|
|
|
console.log('audit_time字段添加成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查表是否有rejectReason字段
|
|
|
|
|
console.log('检查表products是否有rejectReason字段...');
|
|
|
|
|
const [columns] = await connection.query(
|
|
|
|
|
'SHOW COLUMNS FROM `products` LIKE ?',
|
|
|
|
|
['rejectReason']
|
|
|
|
|
);
|
|
|
|
|
console.log('检查表字段结果:', columns.length > 0 ? '已存在' : '不存在');
|
|
|
|
|
|
|
|
|
|
if (columns.length === 0) {
|
|
|
|
|
console.log('添加rejectReason字段到products表...');
|
|
|
|
|
await connection.query(
|
|
|
|
|
'ALTER TABLE `products` ADD COLUMN rejectReason TEXT COMMENT "拒绝理由"'
|
|
|
|
|
);
|
|
|
|
|
console.log('rejectReason字段添加成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查表是否有autoOfflineTime字段
|
|
|
|
|
console.log('检查表products是否有autoOfflineTime字段...');
|
|
|
|
|
const [autoOfflineTimeColumns] = await connection.query(
|
|
|
|
|
'SHOW COLUMNS FROM `products` LIKE ?',
|
|
|
|
|
['autoOfflineTime']
|
|
|
|
|
);
|
|
|
|
|
console.log('检查表字段结果:', autoOfflineTimeColumns.length > 0 ? '已存在' : '不存在');
|
|
|
|
|
|
|
|
|
|
if (autoOfflineTimeColumns.length === 0) {
|
|
|
|
|
console.log('添加autoOfflineTime字段到products表...');
|
|
|
|
|
await connection.query(
|
|
|
|
|
'ALTER TABLE `products` ADD COLUMN autoOfflineTime DATETIME COMMENT "自动下架时间"'
|
|
|
|
|
);
|
|
|
|
|
console.log('autoOfflineTime字段添加成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查表是否有autoOfflineDays字段
|
|
|
|
|
console.log('检查表products是否有autoOfflineDays字段...');
|
|
|
|
|
const [autoOfflineDaysColumns] = await connection.query(
|
|
|
|
|
'SHOW COLUMNS FROM `products` LIKE ?',
|
|
|
|
|
['autoOfflineDays']
|
|
|
|
|
);
|
|
|
|
|
console.log('检查表字段结果:', autoOfflineDaysColumns.length > 0 ? '已存在' : '不存在');
|
|
|
|
|
|
|
|
|
|
if (autoOfflineDaysColumns.length === 0) {
|
|
|
|
|
console.log('添加autoOfflineDays字段到products表...');
|
|
|
|
|
await connection.query(
|
|
|
|
|
'ALTER TABLE `products` ADD COLUMN autoOfflineDays INT COMMENT "自动下架天数"'
|
|
|
|
|
);
|
|
|
|
|
console.log('autoOfflineDays字段添加成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查表是否有autoOfflineHours字段
|
|
|
|
|
console.log('检查表products是否有autoOfflineHours字段...');
|
|
|
|
|
const [autoOfflineHoursColumns] = await connection.query(
|
|
|
|
|
'SHOW COLUMNS FROM `products` LIKE ?',
|
|
|
|
|
['autoOfflineHours']
|
|
|
|
|
);
|
|
|
|
|
console.log('检查表字段结果:', autoOfflineHoursColumns.length > 0 ? '已存在' : '不存在');
|
|
|
|
|
|
|
|
|
|
if (autoOfflineHoursColumns.length === 0) {
|
|
|
|
|
console.log('添加autoOfflineHours字段到products表...');
|
|
|
|
|
await connection.query(
|
|
|
|
|
'ALTER TABLE `products` ADD COLUMN autoOfflineHours FLOAT COMMENT "自动下架小时数"'
|
|
|
|
|
);
|
|
|
|
|
console.log('autoOfflineHours字段添加成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查表是否有supplyStatus字段
|
|
|
|
|
console.log('检查表products是否有supplyStatus字段...');
|
|
|
|
|
const [supplyStatusColumns] = await connection.query(
|
|
|
|
|
'SHOW COLUMNS FROM `products` LIKE ?',
|
|
|
|
|
['supplyStatus']
|
|
|
|
|
);
|
|
|
|
|
console.log('检查表字段结果:', supplyStatusColumns.length > 0 ? '已存在' : '不存在');
|
|
|
|
|
|
|
|
|
|
if (supplyStatusColumns.length === 0) {
|
|
|
|
|
console.log('添加supplyStatus字段到products表...');
|
|
|
|
|
await connection.query(
|
|
|
|
|
'ALTER TABLE `products` ADD COLUMN supplyStatus VARCHAR(50) COMMENT "货源状态"'
|
|
|
|
|
);
|
|
|
|
|
console.log('supplyStatus字段添加成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查表是否有sourceType字段
|
|
|
|
|
console.log('检查表products是否有sourceType字段...');
|
|
|
|
|
const [sourceTypeColumns] = await connection.query(
|
|
|
|
|
'SHOW COLUMNS FROM `products` LIKE ?',
|
|
|
|
|
['sourceType']
|
|
|
|
|
);
|
|
|
|
|
console.log('检查表字段结果:', sourceTypeColumns.length > 0 ? '已存在' : '不存在');
|
|
|
|
|
|
|
|
|
|
if (sourceTypeColumns.length === 0) {
|
|
|
|
|
console.log('添加sourceType字段到products表...');
|
|
|
|
|
await connection.query(
|
|
|
|
|
'ALTER TABLE `products` ADD COLUMN sourceType VARCHAR(50) COMMENT "货源类型"'
|
|
|
|
|
);
|
|
|
|
|
console.log('sourceType字段添加成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查表是否有category字段
|
|
|
|
|
console.log('检查表products是否有category字段...');
|
|
|
|
|
const [categoryColumns] = await connection.query(
|
|
|
|
|
'SHOW COLUMNS FROM `products` LIKE ?',
|
|
|
|
|
['category']
|
|
|
|
|
);
|
|
|
|
|
console.log('检查表字段结果:', categoryColumns.length > 0 ? '已存在' : '不存在');
|
|
|
|
|
|
|
|
|
|
if (categoryColumns.length === 0) {
|
|
|
|
|
console.log('添加category字段到products表...');
|
|
|
|
|
await connection.query(
|
|
|
|
|
'ALTER TABLE `products` ADD COLUMN category VARCHAR(50) COMMENT "种类"'
|
|
|
|
|
);
|
|
|
|
|
console.log('category字段添加成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查表是否有audit_time字段
|
|
|
|
|
console.log('检查表products是否有audit_time字段...');
|
|
|
|
|
const [auditTimeColumns] = await connection.query(
|
|
|
|
|
'SHOW COLUMNS FROM `products` LIKE ?',
|
|
|
|
|
['audit_time']
|
|
|
|
|
);
|
|
|
|
|
console.log('检查表字段结果:', auditTimeColumns.length > 0 ? '已存在' : '不存在');
|
|
|
|
|
|
|
|
|
|
if (auditTimeColumns.length === 0) {
|
|
|
|
|
console.log('添加audit_time字段到products表...');
|
|
|
|
|
await connection.query(
|
|
|
|
|
'ALTER TABLE `products` ADD COLUMN audit_time DATETIME COMMENT "审核时间"'
|
|
|
|
|
);
|
|
|
|
|
console.log('audit_time字段添加成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查表是否有producting字段
|
|
|
|
|
console.log('检查表products是否有producting字段...');
|
|
|
|
|
const [productingColumns] = await connection.query(
|
|
|
|
|
'SHOW COLUMNS FROM `products` LIKE ?',
|
|
|
|
|
['producting']
|
|
|
|
|
);
|
|
|
|
|
console.log('检查表字段结果:', productingColumns.length > 0 ? '已存在' : '不存在');
|
|
|
|
|
|
|
|
|
|
if (productingColumns.length === 0) {
|
|
|
|
|
console.log('添加producting字段到products表...');
|
|
|
|
|
await connection.query(
|
|
|
|
|
'ALTER TABLE `products` ADD COLUMN producting VARCHAR(255) COMMENT "产品包装"'
|
|
|
|
|
);
|
|
|
|
|
console.log('producting字段添加成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查audit_logs表是否存在,如果不存在则创建
|
|
|
|
|
console.log('检查audit_logs表是否存在...');
|
|
|
|
|
const [tables] = await connection.query(
|
|
|
|
|
"SHOW TABLES LIKE 'audit_logs'"
|
|
|
|
|
);
|
|
|
|
|
console.log('检查表存在性结果:', tables.length > 0 ? '已存在' : '不存在');
|
|
|
|
|
|
|
|
|
|
if (tables.length === 0) {
|
|
|
|
|
console.log('创建audit_logs表...');
|
|
|
|
|
await connection.query(`
|
|
|
|
|
CREATE TABLE audit_logs (
|
|
|
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
|
|
|
supply_id VARCHAR(50) NOT NULL,
|
|
|
|
|
action VARCHAR(20) NOT NULL COMMENT 'approve或reject',
|
|
|
|
|
user_id VARCHAR(50) NOT NULL COMMENT '操作人ID',
|
|
|
|
|
remark TEXT COMMENT '备注信息',
|
|
|
|
|
created_at DATETIME NOT NULL,
|
|
|
|
|
INDEX idx_supply_id (supply_id),
|
|
|
|
|
INDEX idx_created_at (created_at)
|
|
|
|
|
) COMMENT '审核操作日志表'
|
|
|
|
|
`);
|
|
|
|
|
console.log('audit_logs表创建成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connection.release();
|
|
|
|
|
console.log('数据库结构检查完成');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('数据库结构检查失败:', error.message);
|
|
|
|
|
console.error('错误详情:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 启动服务器
|
|
|
|
|
startServer();
|
|
|
|
|
|
|
|
|
|
// 优雅关闭
|
|
|
|
|
process.on('SIGINT', async () => {
|
|
|
|
|
console.log('正在关闭服务器...');
|
|
|
|
|
if (pool) {
|
|
|
|
|
try {
|
|
|
|
|
await pool.end();
|
|
|
|
|
console.log('数据库连接池已关闭');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('关闭数据库连接池失败:', error.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
console.log('服务器已关闭');
|
|
|
|
|
process.exit(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
module.exports = app;
|