You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
11339 lines
373 KiB
11339 lines
373 KiB
// 时间处理辅助函数 - 获取当前时间
|
|
// 由于Sequelize已设置时区为+08:00,不需要手动加8小时
|
|
function getCurrentTime() {
|
|
return new Date();
|
|
}
|
|
|
|
function getCurrentTimeISOString() {
|
|
return getCurrentTime().toISOString();
|
|
}
|
|
|
|
function getCurrentTimeTimestamp() {
|
|
return getCurrentTime().getTime();
|
|
}
|
|
|
|
// 为保持向后兼容,保留原函数名
|
|
const getBeijingTime = getCurrentTime;
|
|
const getBeijingTimeISOString = getCurrentTimeISOString;
|
|
const getBeijingTimeTimestamp = getCurrentTimeTimestamp;
|
|
|
|
// 类型处理辅助函数 - 添加于修复聊天功能
|
|
function ensureStringId(id) {
|
|
return String(id).trim();
|
|
}
|
|
|
|
function validateUserId(userId) {
|
|
if (!userId || userId === 0 || userId === '0') {
|
|
throw new Error('无效的userId: 不能为空或为0');
|
|
}
|
|
if (typeof userId !== 'string') {
|
|
console.warn('警告: userId应该是字符串类型,当前类型:', typeof userId, '值:', userId);
|
|
return String(userId).trim();
|
|
}
|
|
return userId.trim();
|
|
}
|
|
|
|
function validateManagerId(managerId) {
|
|
if (!managerId || managerId === 0 || managerId === '0' || managerId === 'user') {
|
|
throw new Error('无效的managerId: 不能为空、为0或为"user"');
|
|
}
|
|
// 只允许数字类型的客服ID
|
|
if (!/^\d+$/.test(String(managerId))) {
|
|
throw new Error('无效的managerId: 必须是数字类型');
|
|
}
|
|
// 确保managerId也是字符串类型
|
|
return String(managerId).trim();
|
|
}
|
|
|
|
// ECS服务器示例代码 - Node.js版 (MySQL版本)
|
|
const express = require('express');
|
|
const crypto = require('crypto');
|
|
const bodyParser = require('body-parser');
|
|
const { Sequelize, DataTypes, Model, Op } = require('sequelize');
|
|
const multer = require('multer');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const OssUploader = require('./oss-uploader');
|
|
const WebSocket = require('ws');
|
|
require('dotenv').config();
|
|
|
|
// 创建Express应用
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3003;
|
|
|
|
// 配置HTTP服务器连接限制
|
|
const http = require('http');
|
|
const server = http.createServer(app);
|
|
|
|
// 创建WebSocket服务器
|
|
const wss = new WebSocket.Server({ server });
|
|
|
|
// 连接管理器 - 存储所有活跃的WebSocket连接
|
|
const connections = new Map();
|
|
|
|
// 用户在线状态管理器
|
|
const onlineUsers = new Map(); // 存储用户ID到连接的映射
|
|
const onlineManagers = new Map(); // 存储客服ID到连接的映射
|
|
|
|
// 配置连接管理
|
|
server.maxConnections = 20; // 增加最大连接数限制
|
|
|
|
// 优化连接处理
|
|
app.use((req, res, next) => {
|
|
// 确保响应头包含正确的连接信息
|
|
res.setHeader('Connection', 'keep-alive');
|
|
res.setHeader('Keep-Alive', 'timeout=5, max=100');
|
|
next();
|
|
});
|
|
|
|
// 中间件
|
|
app.use(bodyParser.json());
|
|
|
|
// 添加CORS头,解决跨域问题
|
|
app.use((req, res, next) => {
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
if (req.method === 'OPTIONS') {
|
|
return res.sendStatus(200);
|
|
}
|
|
next();
|
|
});
|
|
|
|
// 测试接口 - 用于验证请求是否到达后端
|
|
app.get('/api/test', (req, res) => {
|
|
res.json({
|
|
success: true,
|
|
message: '后端服务正常运行',
|
|
timestamp: new Date().toISOString(),
|
|
headers: req.headers
|
|
});
|
|
});
|
|
|
|
// 测试POST接口
|
|
app.post('/api/test/post', (req, res) => {
|
|
console.log('===== 测试POST接口被调用 =====');
|
|
console.log('1. 收到请求体:', JSON.stringify(req.body, null, 2));
|
|
console.log('2. 请求头:', req.headers);
|
|
console.log('================================');
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'POST请求成功接收',
|
|
receivedData: req.body,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
});
|
|
|
|
|
|
// Eggbar 帖子创建接口
|
|
app.post('/api/eggbar/posts', async (req, res) => {
|
|
try {
|
|
console.log('===== 收到帖子创建请求 =====');
|
|
console.log('1. 收到请求体:', JSON.stringify(req.body, null, 2));
|
|
|
|
const { user_id, content, images, topic, phone, avatar_url } = req.body;
|
|
|
|
// 数据验证
|
|
if (!user_id) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少用户ID'
|
|
});
|
|
}
|
|
|
|
// 处理图片数据
|
|
let imagesData = null;
|
|
if (images && Array.isArray(images) && images.length > 0) {
|
|
imagesData = images;
|
|
}
|
|
|
|
// 创建帖子记录
|
|
const newPost = await EggbarPost.create({
|
|
user_id: user_id,
|
|
phone: phone || null,
|
|
avatar_url: avatar_url || null,
|
|
content: content || null,
|
|
images: imagesData,
|
|
topic: topic || null
|
|
});
|
|
|
|
console.log('2. 帖子创建成功,ID:', newPost.id);
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '帖子发布成功',
|
|
data: {
|
|
postId: newPost.id
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('创建帖子失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '创建帖子失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Eggbar 帖子列表接口
|
|
app.get('/api/eggbar/posts', async (req, res) => {
|
|
try {
|
|
console.log('===== 收到帖子列表请求 =====');
|
|
|
|
// 获取分页参数
|
|
const page = parseInt(req.query.page) || 1;
|
|
const pageSize = parseInt(req.query.pageSize) || 10;
|
|
const offset = (page - 1) * pageSize;
|
|
|
|
console.log('查询参数:', {
|
|
page,
|
|
pageSize,
|
|
offset
|
|
});
|
|
|
|
// 使用新的 Sequelize 实例查询 eggbar 数据库
|
|
const tempSequelize = new Sequelize('eggbar', dbConfig.user, dbConfig.password, {
|
|
host: dbConfig.host,
|
|
port: dbConfig.port,
|
|
dialect: 'mysql',
|
|
logging: console.log,
|
|
timezone: '+08:00'
|
|
});
|
|
|
|
// 从数据库获取帖子列表
|
|
const posts = await tempSequelize.query(
|
|
'SELECT * FROM eggbar_posts ORDER BY created_at DESC',
|
|
{
|
|
type: tempSequelize.QueryTypes.SELECT
|
|
}
|
|
);
|
|
|
|
console.log('数据库查询结果数量:', posts.length);
|
|
console.log('数据库查询结果:', posts);
|
|
|
|
// 关闭临时连接
|
|
await tempSequelize.close();
|
|
|
|
console.log('原始查询结果:', posts);
|
|
console.log('查询结果类型:', typeof posts);
|
|
console.log('是否为数组:', Array.isArray(posts));
|
|
|
|
// 确保查询结果为数组
|
|
const postsArray = Array.isArray(posts) ? posts : (posts ? [posts] : []);
|
|
|
|
// 手动处理分页
|
|
const totalCount = postsArray.length;
|
|
const startIndex = (page - 1) * pageSize;
|
|
const endIndex = startIndex + pageSize;
|
|
const paginatedPosts = postsArray.slice(startIndex, endIndex);
|
|
|
|
console.log('查询结果:', {
|
|
postCount: paginatedPosts.length,
|
|
totalCount: totalCount
|
|
});
|
|
|
|
// 格式化响应数据
|
|
let formattedPosts = paginatedPosts.map(post => {
|
|
// 解析images字段,确保它是一个数组
|
|
let images = [];
|
|
if (post.images) {
|
|
if (typeof post.images === 'string') {
|
|
try {
|
|
images = JSON.parse(post.images);
|
|
if (!Array.isArray(images)) {
|
|
images = [];
|
|
}
|
|
} catch (e) {
|
|
images = [];
|
|
}
|
|
} else if (Array.isArray(post.images)) {
|
|
images = post.images;
|
|
} else {
|
|
images = [];
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: post.id,
|
|
user_id: post.user_id,
|
|
phone: post.phone,
|
|
avatar_url: post.avatar_url,
|
|
content: post.content,
|
|
images: images,
|
|
topic: post.topic,
|
|
likes: post.likes || 0,
|
|
comments: post.comments || 0,
|
|
shares: post.shares || 0,
|
|
status: post.status,
|
|
created_at: post.created_at,
|
|
updated_at: post.updated_at
|
|
};
|
|
});
|
|
|
|
// 获取当前用户信息
|
|
const phone = req.query.phone || req.headers['x-phone'];
|
|
const userId = req.query.userId || req.headers['x-user-id'];
|
|
|
|
// 根据status字段和用户信息过滤动态
|
|
formattedPosts = formattedPosts.filter(post => {
|
|
// 获取帖子状态,默认为0
|
|
const postStatus = post.status || 0;
|
|
|
|
// status=1:审核通过,所有用户都可见
|
|
if (postStatus === 1) {
|
|
return true;
|
|
}
|
|
|
|
// status=0(待审核)或2(拒绝):只有发布者可见
|
|
// 检查是否是发布者(通过phone或userId)
|
|
const isPublisherByPhone = phone && post.phone === phone;
|
|
const isPublisherByUserId = userId && post.user_id === userId;
|
|
|
|
return isPublisherByPhone || isPublisherByUserId;
|
|
});
|
|
|
|
// 检查用户是否已点赞
|
|
if (phone) {
|
|
try {
|
|
// 批量检查点赞状态
|
|
const postsWithLikedStatus = await Promise.all(formattedPosts.map(async post => {
|
|
const existingLike = await EggbarLike.findOne({
|
|
where: {
|
|
post_id: post.id,
|
|
phone: phone
|
|
}
|
|
});
|
|
return {
|
|
...post,
|
|
liked: !!existingLike
|
|
};
|
|
}));
|
|
formattedPosts = postsWithLikedStatus;
|
|
} catch (error) {
|
|
console.warn('批量检查点赞状态时出错:', error);
|
|
// 如果出错,给所有帖子添加默认未点赞状态
|
|
formattedPosts = formattedPosts.map(post => ({
|
|
...post,
|
|
liked: false
|
|
}));
|
|
}
|
|
} else {
|
|
// 没有电话号码,给所有帖子添加默认未点赞状态
|
|
formattedPosts = formattedPosts.map(post => ({
|
|
...post,
|
|
liked: false
|
|
}));
|
|
}
|
|
|
|
console.log('5. 帖子列表格式化完成,带点赞状态');
|
|
console.log('格式化后的数据数量:', formattedPosts.length);
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '获取帖子列表成功',
|
|
data: {
|
|
posts: formattedPosts,
|
|
pagination: {
|
|
page,
|
|
pageSize,
|
|
total: totalCount,
|
|
totalPages: Math.ceil(totalCount / pageSize)
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('获取帖子列表失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取帖子列表失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 获取封面图片列表接口
|
|
app.get('/api/cover', async (req, res) => {
|
|
try {
|
|
console.log('===== 获取封面图片列表接口被调用 =====');
|
|
|
|
// 从数据库获取所有封面数据
|
|
const covers = await Cover.findAll();
|
|
console.log('获取到封面数据:', covers.length, '条');
|
|
|
|
res.json({
|
|
success: true,
|
|
covers: covers,
|
|
message: '获取封面图片列表成功',
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
} catch (error) {
|
|
console.error('获取封面图片列表失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取封面图片列表失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 订单列表API端点 - 根据电话号码查询订单
|
|
app.post('/api/orders/list', async (req, res) => {
|
|
try {
|
|
const { phoneNumber, startDate, endDate, paymentStatus, page = 1, pageSize = 10 } = req.body;
|
|
console.log('获取订单列表请求:', { phoneNumber, startDate, endDate, paymentStatus, page, pageSize });
|
|
console.log('请求体完整内容:', req.body);
|
|
|
|
if (!phoneNumber) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '用户电话号码不能为空'
|
|
});
|
|
}
|
|
|
|
// 构建查询条件
|
|
const whereCondition = {
|
|
phone: phoneNumber
|
|
};
|
|
|
|
// 添加时间范围过滤
|
|
if (startDate) {
|
|
whereCondition.order_date = {
|
|
...whereCondition.order_date,
|
|
[Op.gte]: startDate
|
|
};
|
|
console.log('添加开始日期过滤:', startDate);
|
|
}
|
|
|
|
if (endDate) {
|
|
whereCondition.order_date = {
|
|
...whereCondition.order_date,
|
|
[Op.lte]: endDate
|
|
};
|
|
console.log('添加结束日期过滤:', endDate);
|
|
}
|
|
|
|
// 添加付款状态过滤
|
|
if (paymentStatus) {
|
|
whereCondition.payment_status = paymentStatus;
|
|
console.log('添加付款状态过滤:', paymentStatus);
|
|
}
|
|
|
|
console.log('最终查询条件:', whereCondition);
|
|
|
|
// 计算分页偏移量
|
|
const offset = (page - 1) * pageSize;
|
|
console.log('分页参数:', { page, pageSize, offset });
|
|
|
|
// 查询trade_library数据库中的订单主表,根据电话号码和时间范围过滤
|
|
const orders = await JdSalesMain.findAll({
|
|
where: whereCondition,
|
|
include: [
|
|
{
|
|
model: JdSalesSub,
|
|
as: 'subItems',
|
|
attributes: [
|
|
'sub_id',
|
|
'product_name',
|
|
'batch_no',
|
|
'unit',
|
|
'yolk',
|
|
'specification',
|
|
'sales_pieces',
|
|
'sales_weight',
|
|
'unit_price',
|
|
'sales_amount',
|
|
'discount_amount',
|
|
'rebate_amount',
|
|
'discount_reason'
|
|
]
|
|
}
|
|
],
|
|
order: [['order_date', 'DESC']],
|
|
limit: pageSize,
|
|
offset: offset
|
|
});
|
|
|
|
console.log('查询到的订单数量:', orders.length);
|
|
|
|
// 格式化响应数据
|
|
const formattedOrders = orders.map(order => {
|
|
const orderData = order.toJSON();
|
|
|
|
// 确保order_date是字符串格式
|
|
if (orderData.order_date instanceof Date) {
|
|
orderData.order_date = orderData.order_date.toISOString().split('T')[0];
|
|
}
|
|
|
|
// 确保数值字段正确格式化
|
|
orderData.total_pieces = parseInt(orderData.total_pieces) || 0;
|
|
orderData.total_weight = parseFloat(orderData.total_weight) || 0;
|
|
orderData.total_amount = parseFloat(orderData.total_amount) || 0;
|
|
|
|
// 格式化子项数据
|
|
if (orderData.subItems) {
|
|
orderData.subItems = orderData.subItems.map(subItem => ({
|
|
...subItem,
|
|
sales_pieces: parseInt(subItem.sales_pieces) || 0,
|
|
sales_weight: parseFloat(subItem.sales_weight) || 0,
|
|
unit_price: parseFloat(subItem.unit_price) || 0,
|
|
sales_amount: parseFloat(subItem.sales_amount) || 0,
|
|
discount_amount: parseFloat(subItem.discount_amount) || 0,
|
|
rebate_amount: parseFloat(subItem.rebate_amount) || 0
|
|
}));
|
|
}
|
|
|
|
return orderData;
|
|
});
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '获取订单列表成功',
|
|
data: {
|
|
orders: formattedOrders,
|
|
totalCount: formattedOrders.length
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('获取订单列表失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取订单列表失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 订单详情API端点 - 根据订单ID查询订单详情
|
|
app.get('/api/orders/detail/:dataid', async (req, res) => {
|
|
try {
|
|
const { dataid } = req.params;
|
|
console.log('获取订单详情请求:', { dataid });
|
|
|
|
if (!dataid) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '订单ID不能为空'
|
|
});
|
|
}
|
|
|
|
// 查询订单详情
|
|
const order = await JdSalesMain.findOne({
|
|
where: {
|
|
dataid
|
|
},
|
|
include: [
|
|
{
|
|
model: JdSalesSub,
|
|
as: 'subItems',
|
|
attributes: [
|
|
'sub_id',
|
|
'product_name',
|
|
'batch_no',
|
|
'unit',
|
|
'yolk',
|
|
'specification',
|
|
'sales_pieces',
|
|
'sales_weight',
|
|
'unit_price',
|
|
'sales_amount',
|
|
'discount_amount',
|
|
'rebate_amount',
|
|
'discount_reason'
|
|
]
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!order) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '订单不存在'
|
|
});
|
|
}
|
|
|
|
// 格式化响应数据
|
|
const orderData = order.toJSON();
|
|
|
|
// 确保order_date是字符串格式
|
|
if (orderData.order_date instanceof Date) {
|
|
orderData.order_date = orderData.order_date.toISOString().split('T')[0];
|
|
}
|
|
|
|
// 确保数值字段正确格式化
|
|
orderData.total_pieces = parseInt(orderData.total_pieces) || 0;
|
|
orderData.total_weight = parseFloat(orderData.total_weight) || 0;
|
|
orderData.total_amount = parseFloat(orderData.total_amount) || 0;
|
|
|
|
// 格式化子项数据
|
|
if (orderData.subItems) {
|
|
orderData.subItems = orderData.subItems.map(subItem => ({
|
|
...subItem,
|
|
sales_pieces: parseInt(subItem.sales_pieces) || 0,
|
|
sales_weight: parseFloat(subItem.sales_weight) || 0,
|
|
unit_price: parseFloat(subItem.unit_price) || 0,
|
|
sales_amount: parseFloat(subItem.sales_amount) || 0,
|
|
discount_amount: parseFloat(subItem.discount_amount) || 0,
|
|
rebate_amount: parseFloat(subItem.rebate_amount) || 0
|
|
}));
|
|
}
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '获取订单详情成功',
|
|
data: orderData
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('获取订单详情失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取订单详情失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 订单统计API端点 - 获取完整的订单统计数据(不分页)
|
|
app.post('/api/orders/statistics', async (req, res) => {
|
|
try {
|
|
const { phoneNumber, startDate, endDate, paymentStatus } = req.body;
|
|
console.log('获取订单统计请求:', { phoneNumber, startDate, endDate, paymentStatus });
|
|
|
|
if (!phoneNumber) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '用户电话号码不能为空'
|
|
});
|
|
}
|
|
|
|
// 构建查询条件
|
|
const whereCondition = {
|
|
phone: phoneNumber
|
|
};
|
|
|
|
// 添加时间范围过滤
|
|
if (startDate) {
|
|
whereCondition.order_date = {
|
|
...whereCondition.order_date,
|
|
[Op.gte]: startDate
|
|
};
|
|
}
|
|
|
|
if (endDate) {
|
|
whereCondition.order_date = {
|
|
...whereCondition.order_date,
|
|
[Op.lte]: endDate
|
|
};
|
|
}
|
|
|
|
// 添加付款状态过滤
|
|
if (paymentStatus) {
|
|
whereCondition.payment_status = paymentStatus;
|
|
}
|
|
|
|
console.log('统计查询条件:', whereCondition);
|
|
|
|
// 查询所有符合条件的订单(不分页)
|
|
const allOrders = await JdSalesMain.findAll({
|
|
where: whereCondition,
|
|
order: [['order_date', 'DESC']]
|
|
});
|
|
|
|
console.log('统计查询到的订单数量:', allOrders.length);
|
|
|
|
// 计算统计信息
|
|
let totalOrders = allOrders.length;
|
|
let totalAmount = 0;
|
|
let totalPieces = 0;
|
|
let totalWeight = 0;
|
|
let unpaidAmount = 0;
|
|
let paidAmount = 0;
|
|
|
|
allOrders.forEach(order => {
|
|
const orderData = order.toJSON();
|
|
const amount = parseFloat(orderData.total_amount) || 0;
|
|
const pieces = parseInt(orderData.total_pieces) || 0;
|
|
const weight = parseFloat(orderData.total_weight) || 0;
|
|
|
|
totalAmount += amount;
|
|
totalPieces += pieces;
|
|
totalWeight += weight;
|
|
|
|
if (orderData.payment_status === '未收款') {
|
|
unpaidAmount += amount;
|
|
} else if (orderData.payment_status === '全款') {
|
|
paidAmount += amount;
|
|
}
|
|
});
|
|
|
|
const statistics = {
|
|
totalOrders,
|
|
totalAmount: Math.round(totalAmount * 100) / 100,
|
|
totalPieces,
|
|
totalWeight: Math.round(totalWeight * 1000) / 1000,
|
|
unpaidAmount: Math.round(unpaidAmount * 100) / 100,
|
|
paidAmount: Math.round(paidAmount * 100) / 100
|
|
};
|
|
|
|
console.log('计算的统计信息:', statistics);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '获取订单统计成功',
|
|
data: statistics
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('获取订单统计失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取订单统计失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 创建动态接口(已废弃 - 使用上面的实际实现)
|
|
app.post('/api/eggbar/posts/deprecated', async (req, res) => {
|
|
try {
|
|
console.log('===== 创建动态接口被调用 =====');
|
|
console.log('收到的请求数据:', req.body);
|
|
|
|
const { content, images, topic } = req.body;
|
|
|
|
// 验证参数
|
|
if (!content && !topic) {
|
|
return res.json({
|
|
success: false,
|
|
message: '文本内容和话题至少需要填写一项'
|
|
});
|
|
}
|
|
|
|
// 模拟创建动态
|
|
console.log('创建动态成功:', {
|
|
content,
|
|
images,
|
|
topic
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '发布成功',
|
|
data: {
|
|
content,
|
|
images,
|
|
topic
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('创建动态失败:', error);
|
|
res.json({
|
|
success: false,
|
|
message: '发布失败,请重试',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 创建临时文件夹用于存储上传的文件
|
|
const uploadTempDir = path.join(__dirname, 'temp-uploads');
|
|
if (!fs.existsSync(uploadTempDir)) {
|
|
fs.mkdirSync(uploadTempDir, { recursive: true });
|
|
}
|
|
|
|
// 配置multer中间件
|
|
const storage = multer.diskStorage({
|
|
destination: (req, file, cb) => {
|
|
cb(null, uploadTempDir);
|
|
},
|
|
filename: (req, file, cb) => {
|
|
// 生成唯一文件名
|
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
const extname = path.extname(file.originalname).toLowerCase();
|
|
cb(null, file.fieldname + '-' + uniqueSuffix + extname);
|
|
}
|
|
});
|
|
|
|
// 为了解决URL重复问题,添加GET方法支持和URL修复处理
|
|
app.get('/api/wechat/getOpenid', async (req, res) => {
|
|
// 无论URL格式如何,都返回正确的响应格式
|
|
res.json({
|
|
success: false,
|
|
code: 405,
|
|
message: '请使用POST方法访问此接口',
|
|
data: {}
|
|
});
|
|
});
|
|
|
|
// 添加全局中间件处理URL重复问题
|
|
app.use((req, res, next) => {
|
|
const url = req.url;
|
|
|
|
// 检测URL中是否有重复的模式
|
|
const repeatedPattern = /(\/api\/wechat\/getOpenid).*?(\1)/i;
|
|
if (repeatedPattern.test(url)) {
|
|
// 重定向到正确的URL
|
|
const correctedUrl = url.replace(repeatedPattern, '$1');
|
|
console.log(`检测到URL重复: ${url} -> 重定向到: ${correctedUrl}`);
|
|
res.redirect(307, correctedUrl); // 使用307保持原始请求方法
|
|
return;
|
|
}
|
|
|
|
next();
|
|
});
|
|
|
|
// 配置文件过滤函数,只允许上传图片
|
|
const fileFilter = (req, file, cb) => {
|
|
const allowedMimeTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
|
|
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif'];
|
|
const extname = path.extname(file.originalname).toLowerCase();
|
|
|
|
if (allowedMimeTypes.includes(file.mimetype) && allowedExtensions.includes(extname)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('不支持的文件类型,仅支持JPG、PNG和GIF图片'), false);
|
|
}
|
|
};
|
|
|
|
// 创建multer实例
|
|
const upload = multer({
|
|
storage: storage,
|
|
limits: {
|
|
fileSize: 10 * 1024 * 1024 // 限制文件大小为10MB
|
|
},
|
|
fileFilter: fileFilter
|
|
});
|
|
|
|
// Eggbar 图片上传接口
|
|
app.post('/api/eggbar/upload', upload.single('image'), async (req, res) => {
|
|
try {
|
|
console.log('===== 收到图片上传请求 =====');
|
|
console.log('1. 文件信息:', req.file);
|
|
console.log('2. 表单数据:', req.body);
|
|
|
|
if (!req.file) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: '没有收到文件'
|
|
});
|
|
}
|
|
|
|
const tempFilePath = req.file.path;
|
|
|
|
try {
|
|
// 使用OSS上传图片
|
|
const imageUrl = await OssUploader.uploadFile(tempFilePath, 'eggbar', 'image');
|
|
|
|
console.log('3. 图片上传成功,URL:', imageUrl);
|
|
|
|
// 删除临时文件
|
|
if (fs.existsSync(tempFilePath)) {
|
|
fs.unlinkSync(tempFilePath);
|
|
console.log('4. 临时文件已删除:', tempFilePath);
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '图片上传成功',
|
|
imageUrl: imageUrl
|
|
});
|
|
} finally {
|
|
// 确保临时文件被删除,即使OSS上传失败
|
|
if (tempFilePath && fs.existsSync(tempFilePath)) {
|
|
try {
|
|
fs.unlinkSync(tempFilePath);
|
|
console.log('临时文件已清理:', tempFilePath);
|
|
} catch (cleanupError) {
|
|
console.warn('清理临时文件时出错:', cleanupError);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('上传图片失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '上传图片失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Eggbar 点赞接口
|
|
app.post('/api/eggbar/posts/:postId/like', async (req, res) => {
|
|
try {
|
|
const postId = parseInt(req.params.postId);
|
|
const { phone } = req.body;
|
|
|
|
console.log('===== 收到点赞请求 =====');
|
|
console.log('1. 动态ID:', postId);
|
|
console.log('2. 电话号码:', phone);
|
|
|
|
// 数据验证
|
|
if (!postId || !phone) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要参数'
|
|
});
|
|
}
|
|
|
|
// 检查动态是否存在
|
|
const post = await EggbarPost.findByPk(postId);
|
|
if (!post) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '动态不存在'
|
|
});
|
|
}
|
|
|
|
// 检查用户是否已经点过赞
|
|
const existingLike = await EggbarLike.findOne({
|
|
where: {
|
|
post_id: postId,
|
|
phone: phone
|
|
}
|
|
});
|
|
|
|
let isLiked = false;
|
|
let newLikeCount = post.likes || 0;
|
|
|
|
if (existingLike) {
|
|
// 已经点过赞,取消点赞
|
|
await existingLike.destroy();
|
|
newLikeCount = Math.max(0, newLikeCount - 1);
|
|
isLiked = false;
|
|
console.log('3. 取消点赞成功');
|
|
} else {
|
|
// 没点过赞,添加点赞
|
|
await EggbarLike.create({
|
|
post_id: postId,
|
|
phone: phone
|
|
});
|
|
newLikeCount = newLikeCount + 1;
|
|
isLiked = true;
|
|
console.log('3. 点赞成功');
|
|
}
|
|
|
|
// 更新动态的点赞数
|
|
await EggbarPost.update(
|
|
{ likes: newLikeCount },
|
|
{ where: { id: postId } }
|
|
);
|
|
|
|
console.log('4. 点赞数更新成功,新点赞数:', newLikeCount);
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: isLiked ? '点赞成功' : '取消点赞成功',
|
|
data: {
|
|
isLiked: isLiked,
|
|
likes: newLikeCount
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('点赞操作失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '点赞操作失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 添加请求日志中间件,捕获所有到达服务器的请求(必须放在bodyParser之后)
|
|
app.use((req, res, next) => {
|
|
// 使用统一的时间处理函数获取当前时间
|
|
const beijingTime = getBeijingTime();
|
|
const formattedTime = beijingTime.toISOString().replace('Z', '+08:00');
|
|
|
|
console.log(`[${formattedTime}] 收到请求: ${req.method} ${req.url}`);
|
|
console.log('请求头:', req.headers);
|
|
console.log('请求体:', req.body);
|
|
next();
|
|
});
|
|
|
|
// 商品毛重处理中间件 - 确保所有返回的商品数据中毛重字段保持原始值
|
|
app.use((req, res, next) => {
|
|
// 保存原始的json方法
|
|
const originalJson = res.json;
|
|
|
|
// 重写json方法来处理响应数据
|
|
res.json = function (data) {
|
|
// 处理商品数据的通用函数
|
|
const processProduct = (product) => {
|
|
if (product && typeof product === 'object') {
|
|
// 【关键修复】处理非数字字段的还原逻辑 - 支持毛重、价格和数量
|
|
|
|
// 还原非数字毛重值
|
|
if (product.grossWeight === 0.01 && product.isNonNumericGrossWeight && product.originalGrossWeight) {
|
|
product.grossWeight = String(product.originalGrossWeight);
|
|
console.log('中间件还原非数字毛重:', {
|
|
productId: product.productId,
|
|
original: product.originalGrossWeight,
|
|
final: product.grossWeight
|
|
});
|
|
}
|
|
// 正常处理:空值或其他值
|
|
else if (product.grossWeight === null || product.grossWeight === undefined || product.grossWeight === '') {
|
|
product.grossWeight = ''; // 空值设置为空字符串
|
|
} else {
|
|
product.grossWeight = String(product.grossWeight); // 确保是字符串类型
|
|
}
|
|
|
|
// 【新增】还原非数字价格值
|
|
if (product.price === 0.01 && product.isNonNumericPrice && product.originalPrice) {
|
|
product.price = String(product.originalPrice);
|
|
console.log('中间件还原非数字价格:', {
|
|
productId: product.productId,
|
|
original: product.originalPrice,
|
|
final: product.price
|
|
});
|
|
}
|
|
|
|
// 【新增】还原非数字数量值
|
|
if (product.quantity === 1 && product.isNonNumericQuantity && product.originalQuantity) {
|
|
product.quantity = String(product.originalQuantity);
|
|
console.log('中间件还原非数字数量:', {
|
|
productId: product.productId,
|
|
original: product.originalQuantity,
|
|
final: product.quantity
|
|
});
|
|
}
|
|
}
|
|
return product;
|
|
};
|
|
|
|
// 检查数据中是否包含商品列表
|
|
if (data && typeof data === 'object') {
|
|
// 处理/products/list接口的响应
|
|
if (data.products && Array.isArray(data.products)) {
|
|
data.products = data.products.map(processProduct);
|
|
}
|
|
|
|
// 处理/data字段中的商品列表
|
|
if (data.data && data.data.products && Array.isArray(data.data.products)) {
|
|
data.data.products = data.data.products.map(processProduct);
|
|
}
|
|
|
|
// 处理单个商品详情
|
|
if (data.data && data.data.product) {
|
|
data.data.product = processProduct(data.data.product);
|
|
}
|
|
|
|
// 处理直接的商品对象
|
|
if (data.product) {
|
|
data.product = processProduct(data.product);
|
|
}
|
|
}
|
|
|
|
// 调用原始的json方法
|
|
return originalJson.call(this, data);
|
|
};
|
|
|
|
next();
|
|
});
|
|
|
|
// 使用绝对路径加载环境变量(path模块已在文件顶部导入)
|
|
const envPath = path.resolve(__dirname, '.env');
|
|
console.log('正在从绝对路径加载.env文件:', envPath);
|
|
const dotenv = require('dotenv');
|
|
const result = dotenv.config({ path: envPath });
|
|
|
|
if (result.error) {
|
|
console.error('加载.env文件失败:', result.error.message);
|
|
} else {
|
|
console.log('.env文件加载成功');
|
|
console.log('解析的环境变量数量:', Object.keys(result.parsed || {}).length);
|
|
}
|
|
|
|
// 手动设置默认密码,确保密码被传递
|
|
if (!process.env.DB_PASSWORD || process.env.DB_PASSWORD === '') {
|
|
process.env.DB_PASSWORD = 'schl@2025';
|
|
console.log('已手动设置默认密码');
|
|
}
|
|
|
|
// 打印环境变量检查
|
|
console.log('环境变量检查:');
|
|
console.log('DB_HOST:', process.env.DB_HOST);
|
|
console.log('DB_PORT:', process.env.DB_PORT);
|
|
console.log('DB_DATABASE:', process.env.DB_DATABASE);
|
|
console.log('DB_USER:', process.env.DB_USER);
|
|
console.log('DB_PASSWORD长度:', process.env.DB_PASSWORD ? process.env.DB_PASSWORD.length : '0');
|
|
console.log('DB_PASSWORD值:', process.env.DB_PASSWORD ? '已设置(保密)' : '未设置');
|
|
|
|
// 从.env文件直接读取配置
|
|
const dbConfig = {
|
|
host: process.env.DB_HOST || '1.95.162.61',
|
|
port: process.env.DB_PORT || 3306,
|
|
database: process.env.DB_DATABASE || 'wechat_app',
|
|
user: process.env.DB_USER || 'root',
|
|
password: process.env.DB_PASSWORD || ''
|
|
};
|
|
|
|
console.log('数据库连接配置:');
|
|
console.log(JSON.stringify(dbConfig, null, 2));
|
|
|
|
// MySQL数据库连接配置 - 为不同数据源创建独立连接
|
|
// 1. wechat_app数据源连接
|
|
const wechatAppSequelize = new Sequelize(
|
|
'wechat_app',
|
|
dbConfig.user,
|
|
dbConfig.password,
|
|
{
|
|
host: dbConfig.host,
|
|
port: dbConfig.port,
|
|
dialect: 'mysql',
|
|
pool: {
|
|
max: 10,
|
|
min: 0,
|
|
acquire: 30000,
|
|
idle: 10000
|
|
},
|
|
logging: console.log,
|
|
define: {
|
|
timestamps: false
|
|
},
|
|
timezone: '+08:00' // 设置时区为UTC+8
|
|
}
|
|
);
|
|
|
|
// 2. userlogin数据源连接
|
|
const userLoginSequelize = new Sequelize(
|
|
'userlogin',
|
|
dbConfig.user,
|
|
dbConfig.password,
|
|
{
|
|
host: dbConfig.host,
|
|
port: dbConfig.port,
|
|
dialect: 'mysql',
|
|
pool: {
|
|
max: 10,
|
|
min: 0,
|
|
acquire: 30000,
|
|
idle: 10000
|
|
},
|
|
logging: console.log,
|
|
define: {
|
|
timestamps: false
|
|
},
|
|
timezone: '+08:00' // 设置时区为UTC+8
|
|
}
|
|
);
|
|
|
|
// 3. eggbar数据源连接
|
|
const eggbarSequelize = new Sequelize(
|
|
'eggbar',
|
|
dbConfig.user,
|
|
dbConfig.password,
|
|
{
|
|
host: dbConfig.host,
|
|
port: dbConfig.port,
|
|
dialect: 'mysql',
|
|
pool: {
|
|
max: 10,
|
|
min: 0,
|
|
acquire: 30000,
|
|
idle: 10000
|
|
},
|
|
logging: console.log,
|
|
define: {
|
|
timestamps: false
|
|
},
|
|
timezone: '+08:00' // 设置时区为UTC+8
|
|
}
|
|
);
|
|
|
|
// 4. trade_library数据源连接 - 用于订单查询
|
|
const tradeLibrarySequelize = new Sequelize(
|
|
'trade_library',
|
|
dbConfig.user,
|
|
dbConfig.password,
|
|
{
|
|
host: dbConfig.host,
|
|
port: dbConfig.port,
|
|
dialect: 'mysql',
|
|
pool: {
|
|
max: 10,
|
|
min: 0,
|
|
acquire: 30000,
|
|
idle: 10000
|
|
},
|
|
logging: console.log,
|
|
define: {
|
|
timestamps: false
|
|
},
|
|
timezone: '+08:00' // 设置时区为UTC+8
|
|
}
|
|
);
|
|
|
|
// 为保持兼容性,保留默认sequelize引用(指向wechat_app)
|
|
const sequelize = wechatAppSequelize;
|
|
const ChatConversation = sequelize.define('ChatConversation', {
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
primaryKey: true,
|
|
autoIncrement: true
|
|
},
|
|
conversation_id: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false,
|
|
unique: true
|
|
},
|
|
userId: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false
|
|
},
|
|
managerId: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false
|
|
},
|
|
last_message: {
|
|
type: DataTypes.TEXT
|
|
},
|
|
last_message_time: {
|
|
type: DataTypes.DATE
|
|
},
|
|
unread_count: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0
|
|
},
|
|
cs_unread_count: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0
|
|
},
|
|
status: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 1
|
|
},
|
|
user_online: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0
|
|
},
|
|
cs_online: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0
|
|
},
|
|
created_at: {
|
|
type: DataTypes.DATE
|
|
},
|
|
updated_at: {
|
|
type: DataTypes.DATE
|
|
}
|
|
}, {
|
|
tableName: 'chat_conversations',
|
|
timestamps: false
|
|
});
|
|
|
|
// 定义消息模型
|
|
const ChatMessage = sequelize.define('ChatMessage', {
|
|
message_id: {
|
|
type: DataTypes.STRING,
|
|
primaryKey: true
|
|
},
|
|
conversation_id: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false
|
|
},
|
|
sender_type: {
|
|
type: DataTypes.INTEGER,
|
|
allowNull: false
|
|
},
|
|
sender_id: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false
|
|
},
|
|
receiver_id: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false
|
|
},
|
|
content_type: {
|
|
type: DataTypes.INTEGER,
|
|
allowNull: false
|
|
},
|
|
content: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: false
|
|
},
|
|
file_url: {
|
|
type: DataTypes.STRING
|
|
},
|
|
file_size: {
|
|
type: DataTypes.INTEGER
|
|
},
|
|
duration: {
|
|
type: DataTypes.INTEGER
|
|
},
|
|
is_read: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0
|
|
},
|
|
status: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 1
|
|
},
|
|
created_at: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW
|
|
},
|
|
updated_at: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW
|
|
}
|
|
}, {
|
|
tableName: 'chat_messages',
|
|
timestamps: false
|
|
});
|
|
|
|
// 定义评论模型
|
|
const comments = sequelize.define('Comment', {
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
primaryKey: true,
|
|
autoIncrement: true,
|
|
comment: '唯一标识'
|
|
},
|
|
productId: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: false,
|
|
comment: '产品id'
|
|
},
|
|
phoneNumber: {
|
|
type: DataTypes.INTEGER,
|
|
allowNull: false,
|
|
comment: '评论者电话号码'
|
|
},
|
|
comments: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: false,
|
|
comment: '评论内容'
|
|
},
|
|
time: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW,
|
|
comment: '评论时间'
|
|
},
|
|
like: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
comment: '点赞数'
|
|
},
|
|
hate: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
comment: '点踩数'
|
|
},
|
|
review: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
comment: '审核字段'
|
|
}
|
|
}, {
|
|
tableName: 'comments',
|
|
timestamps: false
|
|
});
|
|
|
|
// 微信小程序配置
|
|
const WECHAT_CONFIG = {
|
|
APPID: process.env.WECHAT_APPID || 'your-wechat-appid',
|
|
APPSECRET: process.env.WECHAT_APPSECRET || 'your-wechat-appsecret',
|
|
TOKEN: process.env.WECHAT_TOKEN || 'your-wechat-token'
|
|
};
|
|
|
|
// 显示当前使用的数据库配置(用于调试)
|
|
console.log('当前数据库连接配置:');
|
|
console.log(' 主机:', process.env.DB_HOST || 'localhost');
|
|
console.log(' 端口:', process.env.DB_PORT || 3306);
|
|
console.log(' 数据库名:', process.env.DB_DATABASE || 'wechat_app');
|
|
console.log(' 用户名:', process.env.DB_USER || 'root');
|
|
console.log(' 密码:', process.env.DB_PASSWORD === undefined || process.env.DB_PASSWORD === '' ? '无密码' : '******');
|
|
|
|
// 测试数据库连接
|
|
async function testDbConnection() {
|
|
try {
|
|
await sequelize.authenticate();
|
|
console.log('数据库连接成功');
|
|
} catch (error) {
|
|
console.error('数据库连接失败:', error);
|
|
console.error('\n请检查以下几点:');
|
|
console.error('1. MySQL服务是否已经启动');
|
|
console.error('2. wechat_app数据库是否已创建');
|
|
console.error('3. .env文件中的数据库用户名和密码是否正确');
|
|
console.error('4. 用户名是否有足够的权限访问数据库');
|
|
console.error('\n如果是首次配置,请参考README文件中的数据库设置指南。');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
testDbConnection();
|
|
|
|
// 获取用户会话列表接口
|
|
app.get('/api/conversations/user/:userId', async (req, res) => {
|
|
try {
|
|
const { userId } = req.params;
|
|
console.log(`获取用户 ${userId} 的会话列表`);
|
|
|
|
// 从数据库获取用户的所有会话
|
|
const conversations = await ChatConversation.findAll({
|
|
where: {
|
|
userId: userId
|
|
},
|
|
order: [[Sequelize.literal('last_message_time'), 'DESC']]
|
|
});
|
|
|
|
console.log(`找到 ${conversations.length} 个会话`);
|
|
|
|
// 格式化响应数据
|
|
const formattedConversations = conversations.map(conv => {
|
|
// 获取最后消息的时间
|
|
const lastMessageTime = conv.last_message_time ? new Date(conv.last_message_time) : null;
|
|
|
|
return {
|
|
conversation_id: conv.conversation_id,
|
|
user_id: conv.userId,
|
|
manager_id: conv.managerId,
|
|
last_message: conv.last_message || '',
|
|
last_message_time: lastMessageTime ? new Date(lastMessageTime.getTime() + 8 * 60 * 60 * 1000).toISOString() : null,
|
|
unread_count: conv.unread_count || 0,
|
|
status: conv.status
|
|
};
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '获取会话列表成功',
|
|
data: formattedConversations
|
|
});
|
|
} catch (error) {
|
|
console.error('获取会话列表失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取会话列表失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 定义数据模型
|
|
|
|
// 用户模型
|
|
class User extends Model { }
|
|
User.init({
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
autoIncrement: true,
|
|
primaryKey: true
|
|
},
|
|
openid: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false
|
|
},
|
|
userId: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false,
|
|
unique: true
|
|
},
|
|
name: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: true, // 昵称,可选
|
|
comment: '昵称'
|
|
},
|
|
avatarUrl: {
|
|
type: DataTypes.TEXT
|
|
},
|
|
nickName: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false, // 数据库NOT NULL: 联系人
|
|
comment: '联系人'
|
|
},
|
|
phoneNumber: {
|
|
type: DataTypes.STRING(20),
|
|
allowNull: false // 电话号码,必填
|
|
},
|
|
type: {
|
|
type: DataTypes.STRING(20),
|
|
allowNull: false // 用户身份(buyer/seller/both),必填
|
|
},
|
|
gender: {
|
|
type: DataTypes.INTEGER
|
|
},
|
|
country: {
|
|
type: DataTypes.STRING(50)
|
|
},
|
|
province: {
|
|
type: DataTypes.STRING(50),
|
|
allowNull: false, // 数据库NOT NULL: 省份
|
|
comment: '省份'
|
|
},
|
|
city: {
|
|
type: DataTypes.STRING(50),
|
|
allowNull: false, // 数据库NOT NULL: 城市
|
|
comment: '城市'
|
|
},
|
|
district: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: false, // 数据库NOT NULL: 区域
|
|
comment: '区域'
|
|
},
|
|
detailedaddress: {
|
|
type: DataTypes.STRING(255) // 详细地址
|
|
},
|
|
language: {
|
|
type: DataTypes.STRING(20)
|
|
},
|
|
session_key: {
|
|
type: DataTypes.STRING(255)
|
|
},
|
|
// 客户信息相关字段
|
|
company: {
|
|
type: DataTypes.STRING(255) // 客户公司
|
|
},
|
|
region: {
|
|
type: DataTypes.STRING(255) // 客户地区
|
|
},
|
|
level: {
|
|
type: DataTypes.STRING(255),
|
|
defaultValue: 'company-sea-pools' // 客户等级,默认值为company-sea-pools
|
|
},
|
|
demand: {
|
|
type: DataTypes.TEXT // 基本需求
|
|
},
|
|
spec: {
|
|
type: DataTypes.TEXT // 规格
|
|
},
|
|
// 入驻相关字段
|
|
collaborationid: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: false, // 数据库NOT NULL: 合作商身份
|
|
comment: '合作商身份'
|
|
},
|
|
cooperation: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: false, // 数据库NOT NULL: 合作模式
|
|
comment: '合作模式'
|
|
},
|
|
businesslicenseurl: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: false, // 数据库NOT NULL: 营业执照
|
|
comment: '营业执照'
|
|
},
|
|
proofurl: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: false, // 数据库NOT NULL: 证明材料
|
|
comment: '证明材料:鸡场--动物检疫合格证,贸易商--法人身份证'
|
|
},
|
|
brandurl: {
|
|
type: DataTypes.TEXT, // 品牌授权链文件(可为空)
|
|
comment: '品牌授权链文件'
|
|
},
|
|
// 合作状态相关字段
|
|
partnerstatus: {
|
|
type: DataTypes.STRING(255) // 合作商状态
|
|
},
|
|
// 身份证认证状态字段
|
|
idcardstatus: {
|
|
type: DataTypes.INTEGER, // 0: 待审核, 1: 审核通过, 2: 审核失败
|
|
defaultValue: null, // 默认值为null
|
|
comment: '身份证认证状态'
|
|
},
|
|
// 授权区域字段 - 用于存储用户位置信息
|
|
authorized_region: {
|
|
type: DataTypes.TEXT // 存储用户位置信息的JSON字符串
|
|
},
|
|
reason: {
|
|
type: DataTypes.TEXT // 审核失败原因
|
|
},
|
|
agreement: {
|
|
type: DataTypes.TEXT // 合作商协议
|
|
},
|
|
reject_reason: {
|
|
type: DataTypes.TEXT // 拒绝理由
|
|
},
|
|
terminate_reason: {
|
|
type: DataTypes.TEXT // 终止合作理由
|
|
},
|
|
audit_time: {
|
|
type: DataTypes.DATE // 审核时间
|
|
},
|
|
followup: {
|
|
type: DataTypes.TEXT // 临时跟进
|
|
},
|
|
notice: {
|
|
type: DataTypes.STRING(255) // 通知提醒
|
|
},
|
|
// 新增字段:点击估价次数
|
|
appraisalnum: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
comment: '点击估价次数'
|
|
},
|
|
// 新增字段:点击对比价格次数
|
|
comparenum: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
comment: '点击对比价格次数'
|
|
},
|
|
idcard1: {
|
|
type: DataTypes.TEXT, // 身份证正面
|
|
comment: '身份证正面'
|
|
},
|
|
idcard2: {
|
|
type: DataTypes.TEXT, // 身份证反面
|
|
comment: '身份证反面'
|
|
},
|
|
// 时间字段
|
|
created_at: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW
|
|
},
|
|
updated_at: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW,
|
|
onUpdate: Sequelize.NOW
|
|
},
|
|
// 新增字段:提交时间
|
|
newtime: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW
|
|
},
|
|
|
|
}, {
|
|
sequelize,
|
|
modelName: 'User',
|
|
tableName: 'users',
|
|
timestamps: false
|
|
});
|
|
|
|
// 商品模型
|
|
class Product extends Model { }
|
|
Product.init({
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
autoIncrement: true,
|
|
primaryKey: true
|
|
},
|
|
productId: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false
|
|
},
|
|
sellerId: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false
|
|
},
|
|
productName: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: false
|
|
},
|
|
region: {
|
|
type: DataTypes.STRING(100),
|
|
},
|
|
price: {
|
|
type: DataTypes.STRING(10),
|
|
allowNull: false
|
|
},
|
|
quantity: {
|
|
type: DataTypes.INTEGER,
|
|
allowNull: false
|
|
},
|
|
grossWeight: {
|
|
type: DataTypes.STRING(100),
|
|
},
|
|
yolk: {
|
|
type: DataTypes.STRING(100),
|
|
},
|
|
specification: {
|
|
type: DataTypes.STRING(255),
|
|
},
|
|
// 联系人信息
|
|
product_contact: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: true,
|
|
comment: '联系人'
|
|
},
|
|
// 联系人电话信息
|
|
contact_phone: {
|
|
type: DataTypes.STRING(20),
|
|
allowNull: true,
|
|
comment: '联系人电话'
|
|
},
|
|
status: {
|
|
type: DataTypes.STRING(20),
|
|
defaultValue: 'pending_review',
|
|
validate: {
|
|
isIn: [['pending_review', 'reviewed', 'published', 'sold_out', 'rejected', 'hidden']]
|
|
}
|
|
},
|
|
rejectReason: {
|
|
type: DataTypes.TEXT
|
|
},
|
|
// 添加图片URL字段
|
|
imageUrls: {
|
|
type: DataTypes.TEXT,
|
|
get() {
|
|
const value = this.getDataValue('imageUrls');
|
|
return value ? JSON.parse(value) : [];
|
|
},
|
|
set(value) {
|
|
this.setDataValue('imageUrls', JSON.stringify(value));
|
|
}
|
|
},
|
|
// 新增预约相关字段
|
|
reservedCount: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
allowNull: false,
|
|
comment: '已有几人想要'
|
|
},
|
|
// 新增查看次数字段
|
|
frequency: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
allowNull: false,
|
|
comment: '商品查看次数'
|
|
},
|
|
created_at: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW
|
|
},
|
|
updated_at: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW,
|
|
onUpdate: Sequelize.NOW
|
|
},
|
|
sourceType: {
|
|
type: DataTypes.STRING(50),
|
|
allowNull: true,
|
|
comment: '货源类型'
|
|
},
|
|
supplyStatus: {
|
|
type: DataTypes.STRING(50),
|
|
allowNull: true,
|
|
comment: '供应状态'
|
|
},
|
|
category: {
|
|
type: DataTypes.STRING(50),
|
|
allowNull: true,
|
|
comment: '商品种类'
|
|
},
|
|
label: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
comment: '下架标识,1表示已下架'
|
|
},
|
|
description: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: true,
|
|
comment: '货源描述'
|
|
},
|
|
// 产品日志字段,用于记录价格变更等信息
|
|
product_log: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: true,
|
|
comment: '产品日志',
|
|
get() {
|
|
const value = this.getDataValue('product_log');
|
|
return value ? JSON.parse(value) : [];
|
|
},
|
|
set(value) {
|
|
// 检查value是否已经是JSON字符串,如果是则直接使用,否则序列化
|
|
if (typeof value === 'string' && (value.startsWith('[') || value.startsWith('{'))) {
|
|
// 已经是JSON字符串,直接保存
|
|
this.setDataValue('product_log', value);
|
|
} else {
|
|
// 需要序列化
|
|
this.setDataValue('product_log', JSON.stringify(value));
|
|
}
|
|
}
|
|
},
|
|
// 规格状态字段
|
|
spec_status: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: true,
|
|
comment: '规格状态',
|
|
defaultValue: '0'
|
|
},
|
|
// 讲价字段
|
|
bargaining: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
allowNull: false,
|
|
comment: '是否允许讲价,0允许,1不允许'
|
|
}
|
|
}, {
|
|
sequelize,
|
|
modelName: 'Product',
|
|
tableName: 'products',
|
|
timestamps: false
|
|
});
|
|
|
|
// 购物车模型
|
|
class CartItem extends Model { }
|
|
CartItem.init({
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
autoIncrement: true,
|
|
primaryKey: true
|
|
},
|
|
userId: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false,
|
|
unique: true
|
|
},
|
|
productId: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false
|
|
},
|
|
productName: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: false
|
|
},
|
|
specification: {
|
|
type: DataTypes.STRING(255)
|
|
},
|
|
quantity: {
|
|
type: DataTypes.INTEGER,
|
|
allowNull: false
|
|
},
|
|
grossWeight: {
|
|
type: DataTypes.STRING(255)
|
|
},
|
|
yolk: {
|
|
type: DataTypes.STRING(100)
|
|
},
|
|
price: {
|
|
type: DataTypes.STRING(255)
|
|
},
|
|
selected: {
|
|
type: DataTypes.BOOLEAN,
|
|
defaultValue: true
|
|
},
|
|
added_at: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW
|
|
}
|
|
}, {
|
|
sequelize,
|
|
modelName: 'CartItem',
|
|
tableName: 'cart_items',
|
|
timestamps: false
|
|
});
|
|
|
|
// 收藏模型
|
|
class Favorite extends Model { }
|
|
Favorite.init({
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
autoIncrement: true,
|
|
primaryKey: true
|
|
},
|
|
user_phone: {
|
|
type: DataTypes.STRING(20),
|
|
allowNull: false
|
|
},
|
|
productId: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false
|
|
},
|
|
date: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW
|
|
}
|
|
}, {
|
|
sequelize,
|
|
modelName: 'Favorite',
|
|
tableName: 'favorites',
|
|
timestamps: false
|
|
});
|
|
|
|
// 评论模型 - 对应comments表
|
|
class Comment extends Model { }
|
|
Comment.init({
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
primaryKey: true,
|
|
comment: '唯一标识'
|
|
},
|
|
productId: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: false,
|
|
comment: '产品id'
|
|
},
|
|
phoneNumber: {
|
|
type: DataTypes.STRING(20), // 使用STRING类型存储电话号码,支持11位及以上格式
|
|
allowNull: false,
|
|
comment: '评论者电话号码'
|
|
},
|
|
comments: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: false,
|
|
comment: '评论内容'
|
|
},
|
|
time: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW,
|
|
comment: '评论时间'
|
|
},
|
|
like: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
comment: '点赞数'
|
|
},
|
|
hate: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
comment: '点踩数'
|
|
},
|
|
review: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
comment: '审核字段'
|
|
}
|
|
}, {
|
|
sequelize,
|
|
modelName: 'Comment',
|
|
tableName: 'comments',
|
|
timestamps: false,
|
|
hooks: {
|
|
beforeCreate: async (comment) => {
|
|
// 生成唯一ID
|
|
const maxId = await Comment.max('id');
|
|
comment.id = (maxId || 0) + 1;
|
|
}
|
|
}
|
|
});
|
|
|
|
// 联系人表模型
|
|
class Contact extends Model { }
|
|
Contact.init({
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
autoIncrement: true,
|
|
primaryKey: true
|
|
},
|
|
userId: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false,
|
|
unique: true
|
|
},
|
|
name: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false, // 联系人
|
|
comment: '联系人'
|
|
},
|
|
phoneNumber: {
|
|
type: DataTypes.STRING(20),
|
|
allowNull: false // 手机号
|
|
},
|
|
wechat: {
|
|
type: DataTypes.STRING(100) // 微信号
|
|
},
|
|
account: {
|
|
type: DataTypes.STRING(100) // 账户
|
|
},
|
|
accountNumber: {
|
|
type: DataTypes.STRING(100) // 账号
|
|
},
|
|
bank: {
|
|
type: DataTypes.STRING(100) // 开户行
|
|
},
|
|
address: {
|
|
type: DataTypes.TEXT // 地址
|
|
},
|
|
created_at: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW
|
|
},
|
|
updated_at: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW,
|
|
onUpdate: Sequelize.NOW
|
|
}
|
|
}, {
|
|
sequelize,
|
|
modelName: 'Contact',
|
|
tableName: 'contacts',
|
|
timestamps: false
|
|
});
|
|
|
|
// 用户管理表模型
|
|
class UserManagement extends Model { }
|
|
UserManagement.init({
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
autoIncrement: true,
|
|
primaryKey: true
|
|
},
|
|
userId: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false,
|
|
unique: true
|
|
},
|
|
managerId: {
|
|
type: DataTypes.STRING(100),
|
|
defaultValue: null // 经理ID,默认值为null
|
|
},
|
|
company: {
|
|
type: DataTypes.STRING(255),
|
|
defaultValue: null // 公司,默认值为null
|
|
},
|
|
department: {
|
|
type: DataTypes.STRING(255),
|
|
defaultValue: null // 部门,默认值为null
|
|
},
|
|
organization: {
|
|
type: DataTypes.STRING(255),
|
|
defaultValue: null // 组织,默认值为null
|
|
},
|
|
role: {
|
|
type: DataTypes.STRING(100),
|
|
defaultValue: null // 角色,默认值为null
|
|
},
|
|
root: {
|
|
type: DataTypes.STRING(100),
|
|
defaultValue: null // 根节点,默认值为null
|
|
},
|
|
created_at: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW
|
|
},
|
|
updated_at: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW,
|
|
onUpdate: Sequelize.NOW
|
|
}
|
|
}, {
|
|
sequelize,
|
|
modelName: 'UserManagement',
|
|
tableName: 'usermanagements',
|
|
timestamps: false
|
|
});
|
|
|
|
// 用户踪迹表模型
|
|
class UserTrace extends Model { }
|
|
UserTrace.init({
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
autoIncrement: true,
|
|
primaryKey: true
|
|
},
|
|
phoneNumber: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: true,
|
|
comment: '电话号码'
|
|
},
|
|
userId: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false,
|
|
comment: '客户ID'
|
|
},
|
|
operationTime: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW,
|
|
comment: '操作时间'
|
|
},
|
|
originalData: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: true,
|
|
comment: '原始数据JSON'
|
|
}
|
|
}, {
|
|
sequelize,
|
|
modelName: 'UserTrace',
|
|
tableName: 'usertraces',
|
|
timestamps: false
|
|
});
|
|
|
|
// 价格资源模型 - 用于估价功能(匹配实际数据库表结构)
|
|
class Resources extends Model { }
|
|
Resources.init({
|
|
id: {
|
|
type: DataTypes.INTEGER.UNSIGNED,
|
|
autoIncrement: true,
|
|
primaryKey: true
|
|
},
|
|
category: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: true,
|
|
comment: '品种/分类'
|
|
},
|
|
region: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: true,
|
|
comment: '地区'
|
|
},
|
|
price1: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: true,
|
|
comment: '商品价格上限'
|
|
},
|
|
price2: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: true,
|
|
comment: '商品价格下限'
|
|
},
|
|
time: {
|
|
type: DataTypes.DATEONLY,
|
|
allowNull: false,
|
|
comment: '价格日期'
|
|
}
|
|
}, {
|
|
sequelize,
|
|
modelName: 'Resources',
|
|
tableName: 'resources',
|
|
timestamps: false
|
|
});
|
|
|
|
// 封面模型 - 用于存储各种封面图片
|
|
class Cover extends Model { }
|
|
Cover.init({
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
primaryKey: true,
|
|
allowNull: false
|
|
},
|
|
coverurl: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: false,
|
|
comment: '封面图片'
|
|
},
|
|
description: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: true,
|
|
comment: '图片描述'
|
|
}
|
|
}, {
|
|
sequelize,
|
|
modelName: 'Cover',
|
|
tableName: 'cover',
|
|
timestamps: false
|
|
});
|
|
|
|
// goods_root模型 - 用于存储小品种和大贸易的创建者
|
|
class GoodsRoot extends Model { }
|
|
GoodsRoot.init({
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
primaryKey: true,
|
|
autoIncrement: true
|
|
},
|
|
name: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false,
|
|
comment: '创建者姓名'
|
|
},
|
|
root: {
|
|
type: DataTypes.STRING(50),
|
|
allowNull: false,
|
|
comment: '分类:小品种或大贸易'
|
|
}
|
|
}, {
|
|
sequelize,
|
|
modelName: 'GoodsRoot',
|
|
tableName: 'goods_root',
|
|
timestamps: false
|
|
});
|
|
|
|
// 简道云销售订单主表模型(新添加)
|
|
class JdSalesMain extends Model { }
|
|
JdSalesMain.init({
|
|
dataid: {
|
|
type: DataTypes.STRING(50),
|
|
primaryKey: true,
|
|
allowNull: false,
|
|
comment: '主表自增主键(简道云插件关联用)'
|
|
},
|
|
sales_no: {
|
|
type: DataTypes.STRING(50),
|
|
allowNull: false,
|
|
unique: true,
|
|
comment: '销售单号(业务唯一编号)'
|
|
},
|
|
order_date: {
|
|
type: DataTypes.DATEONLY,
|
|
allowNull: false,
|
|
comment: '下单日期'
|
|
},
|
|
customer_company: {
|
|
type: DataTypes.STRING(200),
|
|
allowNull: false,
|
|
comment: '客户公司'
|
|
},
|
|
contact_person: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false,
|
|
comment: '联系人'
|
|
},
|
|
phone: {
|
|
type: DataTypes.STRING(20),
|
|
allowNull: true,
|
|
comment: '联系电话(允许带区号等特殊字符,用字符串存储)'
|
|
},
|
|
salesman: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: true,
|
|
comment: '销售员'
|
|
},
|
|
merchandiser: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: true,
|
|
comment: '跟单员'
|
|
},
|
|
address: {
|
|
type: DataTypes.STRING(500),
|
|
allowNull: true,
|
|
comment: '地址'
|
|
},
|
|
total_pieces: {
|
|
type: DataTypes.INTEGER,
|
|
allowNull: false,
|
|
defaultValue: 0,
|
|
comment: '总件数'
|
|
},
|
|
total_weight: {
|
|
type: DataTypes.DECIMAL(10, 3),
|
|
allowNull: false,
|
|
defaultValue: 0.000,
|
|
comment: '总斤数(保留3位小数)'
|
|
},
|
|
total_amount: {
|
|
type: DataTypes.DECIMAL(12, 2),
|
|
allowNull: false,
|
|
defaultValue: 0.00,
|
|
comment: '总金额(保留2位小数)'
|
|
},
|
|
vehicle_scale: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: true,
|
|
comment: '车辆规模'
|
|
},
|
|
payment_voucher: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: true,
|
|
comment: '支付凭证(存储照片路径)'
|
|
},
|
|
invoice: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: true,
|
|
comment: '发票(存储照片路径)'
|
|
},
|
|
payment_status: {
|
|
type: DataTypes.STRING(50),
|
|
allowNull: true,
|
|
comment: '付款状态(如:未付款/部分付款/已付清)'
|
|
},
|
|
order_status: {
|
|
type: DataTypes.STRING(50),
|
|
allowNull: true
|
|
},
|
|
chepai: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: true,
|
|
comment: '车牌'
|
|
},
|
|
QR_code: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: true,
|
|
comment: '二维码'
|
|
}
|
|
}, {
|
|
sequelize: tradeLibrarySequelize,
|
|
modelName: 'JdSalesMain',
|
|
tableName: 'jd_sales_main',
|
|
timestamps: false,
|
|
indexes: [
|
|
{
|
|
unique: true,
|
|
fields: ['sales_no'],
|
|
name: 'idx_sales_no',
|
|
comment: '销售单号唯一索引(避免重复)'
|
|
}
|
|
]
|
|
});
|
|
|
|
// 简道云销售订单明细表模型(新添加)
|
|
class JdSalesSub extends Model { }
|
|
JdSalesSub.init({
|
|
sub_id: {
|
|
type: DataTypes.INTEGER,
|
|
primaryKey: true,
|
|
autoIncrement: true,
|
|
comment: '子表自增主键'
|
|
},
|
|
dataid: {
|
|
type: DataTypes.STRING(50),
|
|
allowNull: false,
|
|
comment: '关联主表dataid'
|
|
},
|
|
product_name: {
|
|
type: DataTypes.STRING(200),
|
|
allowNull: false,
|
|
comment: '产品名称'
|
|
},
|
|
batch_no: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: true,
|
|
comment: '批次号'
|
|
},
|
|
unit: {
|
|
type: DataTypes.STRING(50),
|
|
allowNull: true,
|
|
comment: '单位(件,斤)'
|
|
},
|
|
yolk: {
|
|
type: DataTypes.STRING(50),
|
|
allowNull: true,
|
|
comment: '蛋黄(描述信息)'
|
|
},
|
|
specification: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: true,
|
|
comment: '规格'
|
|
},
|
|
sales_pieces: {
|
|
type: DataTypes.INTEGER,
|
|
allowNull: false,
|
|
defaultValue: 0,
|
|
comment: '销售件数'
|
|
},
|
|
sales_weight: {
|
|
type: DataTypes.DECIMAL(10, 3),
|
|
allowNull: false,
|
|
defaultValue: 0.000,
|
|
comment: '销售斤数(保留3位小数)'
|
|
},
|
|
unit_price: {
|
|
type: DataTypes.DECIMAL(10, 2),
|
|
allowNull: false,
|
|
defaultValue: 0.00,
|
|
comment: '单价(保留2位小数)'
|
|
},
|
|
purchase_price: {
|
|
type: DataTypes.DECIMAL(10, 2),
|
|
allowNull: true,
|
|
comment: '采购价(保留2位小数)'
|
|
},
|
|
sales_amount: {
|
|
type: DataTypes.DECIMAL(12, 2),
|
|
allowNull: false,
|
|
defaultValue: 0.00,
|
|
comment: '销售金额'
|
|
},
|
|
discount_amount: {
|
|
type: DataTypes.DECIMAL(10, 2),
|
|
allowNull: true,
|
|
defaultValue: 0.00,
|
|
comment: '优惠金额'
|
|
},
|
|
rebate_amount: {
|
|
type: DataTypes.DECIMAL(10, 2),
|
|
allowNull: true,
|
|
defaultValue: 0.00,
|
|
comment: '折扣金额'
|
|
},
|
|
purchase_amount: {
|
|
type: DataTypes.DECIMAL(12, 2),
|
|
allowNull: true,
|
|
comment: '采购金额'
|
|
},
|
|
discount_reason: {
|
|
type: DataTypes.STRING(500),
|
|
allowNull: true,
|
|
comment: '优惠原因'
|
|
}
|
|
}, {
|
|
sequelize: tradeLibrarySequelize,
|
|
modelName: 'JdSalesSub',
|
|
tableName: 'jd_sales_sub',
|
|
timestamps: false,
|
|
indexes: [
|
|
{
|
|
fields: ['dataid'],
|
|
name: 'idx_main_id',
|
|
comment: '关联主表索引(加速查询)'
|
|
}
|
|
]
|
|
});
|
|
|
|
// Eggbar 帖子模型 - 用于存储用户发布的动态
|
|
class EggbarPost extends Model { }
|
|
EggbarPost.init({
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
primaryKey: true,
|
|
autoIncrement: true,
|
|
comment: '帖子ID'
|
|
},
|
|
user_id: {
|
|
type: DataTypes.STRING(100),
|
|
allowNull: false,
|
|
comment: '用户ID'
|
|
},
|
|
phone: {
|
|
type: DataTypes.STRING(20),
|
|
allowNull: true,
|
|
comment: '用户电话号码'
|
|
},
|
|
avatar_url: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: true,
|
|
comment: '用户头像URL'
|
|
},
|
|
content: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: true,
|
|
comment: '动态内容'
|
|
},
|
|
images: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: true,
|
|
comment: '图片URL数组',
|
|
get() {
|
|
const value = this.getDataValue('images');
|
|
return value ? JSON.parse(value) : [];
|
|
},
|
|
set(value) {
|
|
this.setDataValue('images', JSON.stringify(value));
|
|
}
|
|
},
|
|
topic: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: true,
|
|
comment: '话题'
|
|
},
|
|
likes: {
|
|
type: DataTypes.INTEGER,
|
|
allowNull: true,
|
|
defaultValue: 0,
|
|
comment: '点赞数'
|
|
},
|
|
comments: {
|
|
type: DataTypes.INTEGER,
|
|
allowNull: true,
|
|
defaultValue: 0,
|
|
comment: '评论数'
|
|
},
|
|
shares: {
|
|
type: DataTypes.INTEGER,
|
|
allowNull: true,
|
|
defaultValue: 0,
|
|
comment: '分享数'
|
|
},
|
|
status: {
|
|
type: DataTypes.ENUM('active', 'inactive'),
|
|
allowNull: true,
|
|
defaultValue: 'active',
|
|
comment: '状态'
|
|
},
|
|
created_at: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW,
|
|
comment: '创建时间'
|
|
},
|
|
updated_at: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW,
|
|
onUpdate: Sequelize.NOW,
|
|
comment: '更新时间'
|
|
}
|
|
}, {
|
|
sequelize: eggbarSequelize,
|
|
modelName: 'EggbarPost',
|
|
tableName: 'eggbar_posts',
|
|
timestamps: false
|
|
});
|
|
|
|
// Eggbar 点赞模型 - 用于存储用户点赞记录
|
|
class EggbarLike extends Model { }
|
|
EggbarLike.init({
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
primaryKey: true,
|
|
autoIncrement: true,
|
|
comment: '点赞记录ID'
|
|
},
|
|
post_id: {
|
|
type: DataTypes.INTEGER,
|
|
allowNull: false,
|
|
comment: '动态ID'
|
|
},
|
|
phone: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: false,
|
|
comment: '电话号码'
|
|
},
|
|
created_at: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: Sequelize.NOW,
|
|
comment: '创建时间'
|
|
}
|
|
}, {
|
|
sequelize: eggbarSequelize,
|
|
modelName: 'EggbarLike',
|
|
tableName: 'eggbar_likes',
|
|
timestamps: false
|
|
});
|
|
|
|
// 定义模型之间的关联关系
|
|
|
|
// 用户和商品的一对多关系 (卖家发布商品)
|
|
User.hasMany(Product, {
|
|
foreignKey: 'sellerId', // 外键字段名
|
|
sourceKey: 'userId', // 源键,使用userId字段(STRING类型)而非默认的id字段(INTEGER类型)
|
|
as: 'products', // 别名,用于关联查询
|
|
onDelete: 'CASCADE', // 级联删除
|
|
onUpdate: 'CASCADE' // 级联更新
|
|
});
|
|
|
|
Product.belongsTo(User, {
|
|
foreignKey: 'sellerId',
|
|
targetKey: 'userId', // 目标键,使用userId字段(STRING类型)而非默认的id字段(INTEGER类型)
|
|
as: 'seller' // 别名,用于关联查询
|
|
});
|
|
|
|
// 用户和购物车项的一对多关系 (买家的购物需求/购物车)
|
|
User.hasMany(CartItem, {
|
|
foreignKey: 'userId',
|
|
as: 'cartItems', // 用户的购物车(购物需求)列表
|
|
onDelete: 'CASCADE', // 级联删除
|
|
onUpdate: 'CASCADE' // 级联更新
|
|
});
|
|
|
|
CartItem.belongsTo(User, {
|
|
foreignKey: 'userId',
|
|
as: 'buyer' // 别名,明确表示这是购物需求的买家
|
|
});
|
|
|
|
// 商品和购物车项的一对多关系 (商品被添加到购物车)
|
|
Product.hasMany(CartItem, {
|
|
foreignKey: 'productId',
|
|
as: 'cartItems', // 商品出现在哪些购物车中
|
|
onDelete: 'CASCADE', // 级联删除
|
|
onUpdate: 'CASCADE' // 级联更新
|
|
});
|
|
|
|
// 收藏与商品的关系 (收藏属于商品)
|
|
Favorite.belongsTo(Product, {
|
|
foreignKey: 'productId',
|
|
targetKey: 'productId', // 目标键,使用productId字段(STRING类型)而非默认的id字段(INTEGER类型)
|
|
as: 'Product' // 别名,与收藏列表API中使用的一致
|
|
});
|
|
|
|
// 商品与收藏的一对多关系 (商品可以被多个用户收藏)
|
|
Product.hasMany(Favorite, {
|
|
foreignKey: 'productId',
|
|
as: 'favorites', // 别名,表示商品的收藏列表
|
|
onDelete: 'CASCADE', // 级联删除
|
|
onUpdate: 'CASCADE' // 级联更新
|
|
});
|
|
|
|
CartItem.belongsTo(Product, {
|
|
foreignKey: 'productId',
|
|
as: 'product' // 购物车项中的商品
|
|
});
|
|
|
|
// 用户和联系人的一对多关系
|
|
User.hasMany(Contact, {
|
|
foreignKey: 'userId',
|
|
as: 'contacts', // 用户的联系人列表
|
|
onDelete: 'CASCADE', // 级联删除
|
|
onUpdate: 'CASCADE' // 级联更新
|
|
});
|
|
|
|
Contact.belongsTo(User, {
|
|
foreignKey: 'userId',
|
|
as: 'user' // 联系人所属用户
|
|
});
|
|
|
|
// 用户和用户管理的一对一关系
|
|
User.hasOne(UserManagement, {
|
|
foreignKey: 'userId',
|
|
as: 'management', // 用户的管理信息
|
|
onDelete: 'CASCADE', // 级联删除
|
|
onUpdate: 'CASCADE' // 级联更新
|
|
});
|
|
|
|
UserManagement.belongsTo(User, {
|
|
foreignKey: 'userId',
|
|
as: 'user' // 管理信息所属用户
|
|
});
|
|
|
|
// 简道云销售订单主表和明细表的关联关系
|
|
JdSalesMain.hasMany(JdSalesSub, {
|
|
foreignKey: 'dataid',
|
|
sourceKey: 'dataid',
|
|
as: 'subItems',
|
|
onDelete: 'CASCADE',
|
|
onUpdate: 'CASCADE'
|
|
});
|
|
|
|
JdSalesSub.belongsTo(JdSalesMain, {
|
|
foreignKey: 'dataid',
|
|
targetKey: 'dataid',
|
|
as: 'mainOrder'
|
|
});
|
|
|
|
// 同步数据库模型到MySQL
|
|
async function syncDatabase() {
|
|
try {
|
|
// 重要修复:完全禁用外键约束创建和表结构修改
|
|
// 由于Product.sellerId(STRING)和User.id(INTEGER)类型不兼容,我们需要避免Sequelize尝试创建外键约束
|
|
// 使用alter: false和hooks: false来完全避免任何表结构修改操作
|
|
await sequelize.sync({
|
|
force: false, // 不强制重新创建表
|
|
alter: false, // 禁用alter操作,避免修改现有表结构
|
|
hooks: false, // 禁用所有钩子,防止任何表结构修改尝试
|
|
logging: true // 启用同步过程的日志,便于调试
|
|
});
|
|
console.log('数据库模型同步成功(已禁用外键约束创建)');
|
|
} catch (error) {
|
|
console.error('数据库模型同步失败:', error);
|
|
// 即使同步失败也继续运行,因为我们只需要API功能
|
|
console.log('数据库模型同步失败,但服务器继续运行,使用现有表结构');
|
|
|
|
// 增强的错误处理:如果是外键约束错误,提供更明确的信息
|
|
if (error.original && error.original.code === 'ER_FK_INCOMPATIBLE_COLUMNS') {
|
|
console.log('提示:外键约束不兼容错误已被忽略,这是预期行为,因为我们使用userId而非id进行关联');
|
|
console.log('系统将使用应用层关联而非数据库外键约束');
|
|
}
|
|
}
|
|
}
|
|
|
|
syncDatabase();
|
|
|
|
// 解密微信加密数据
|
|
function decryptData(encryptedData, sessionKey, iv) {
|
|
try {
|
|
// Base64解码
|
|
const sessionKeyBuf = Buffer.from(sessionKey, 'base64');
|
|
const encryptedDataBuf = Buffer.from(encryptedData, 'base64');
|
|
const ivBuf = Buffer.from(iv, 'base64');
|
|
|
|
// AES解密
|
|
const decipher = crypto.createDecipheriv('aes-128-cbc', sessionKeyBuf, ivBuf);
|
|
decipher.setAutoPadding(true);
|
|
let decoded = decipher.update(encryptedDataBuf, 'binary', 'utf8');
|
|
decoded += decipher.final('utf8');
|
|
|
|
// 解析JSON
|
|
return JSON.parse(decoded);
|
|
} catch (error) {
|
|
console.error('解密失败:', error);
|
|
// 提供更具体的错误信息
|
|
if (error.code === 'ERR_OSSL_BAD_DECRYPT') {
|
|
throw new Error('登录信息已过期,请重新登录');
|
|
} else if (error.name === 'SyntaxError') {
|
|
throw new Error('数据格式错误,解密结果无效');
|
|
} else {
|
|
throw new Error('解密失败,请重试');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 获取微信session_key
|
|
async function getSessionKey(code) {
|
|
const axios = require('axios');
|
|
const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${WECHAT_CONFIG.APPID}&secret=${WECHAT_CONFIG.APPSECRET}&js_code=${code}&grant_type=authorization_code`;
|
|
|
|
try {
|
|
const response = await axios.get(url);
|
|
return response.data;
|
|
} catch (error) {
|
|
console.error('获取session_key失败:', error);
|
|
throw new Error('获取session_key失败');
|
|
}
|
|
}
|
|
|
|
// 创建用户关联记录函数 - 自动为用户创建contacts和usermanagements表的关联记录
|
|
async function createUserAssociations(user) {
|
|
try {
|
|
if (!user || !user.userId) {
|
|
console.error('无效的用户数据,无法创建关联记录');
|
|
return false;
|
|
}
|
|
|
|
console.log('为用户创建关联记录:', user.userId);
|
|
|
|
// 使用事务确保操作原子性
|
|
await sequelize.transaction(async (transaction) => {
|
|
// 1. 处理联系人记录 - 使用INSERT ... ON DUPLICATE KEY UPDATE确保无论如何都只保留一条记录
|
|
const currentTime = getBeijingTime();
|
|
await sequelize.query(
|
|
`INSERT INTO contacts (userId, name, phoneNumber, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
name = VALUES(name),
|
|
phoneNumber = VALUES(phoneNumber),
|
|
updated_at = ?`,
|
|
{
|
|
replacements: [user.userId, user.name || '默认联系人', user.phoneNumber || '', currentTime, currentTime, currentTime],
|
|
transaction: transaction
|
|
}
|
|
);
|
|
console.log('联系人记录已处理(创建或更新):', user.userId);
|
|
|
|
// 2. 处理用户管理记录 - 使用相同策略
|
|
await sequelize.query(
|
|
`INSERT INTO usermanagements (userId, created_at, updated_at)
|
|
VALUES (?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
updated_at = ?`,
|
|
{
|
|
replacements: [user.userId, currentTime, currentTime, currentTime],
|
|
transaction: transaction
|
|
}
|
|
);
|
|
console.log('用户管理记录已处理(创建或更新):', user.userId);
|
|
});
|
|
|
|
console.log('用户关联记录处理成功:', user.userId);
|
|
return true;
|
|
} catch (error) {
|
|
console.error('创建用户关联记录失败:', error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// API路由
|
|
|
|
// 专门用于更新用户位置信息的API端点
|
|
app.post('/api/user/update-location', async (req, res) => {
|
|
try {
|
|
const { openid, latitude, longitude, address } = req.body;
|
|
|
|
if (!openid) {
|
|
return res.json({ success: false, message: '缺少openid' });
|
|
}
|
|
|
|
if (!latitude || !longitude) {
|
|
return res.json({ success: false, message: '缺少位置信息' });
|
|
}
|
|
|
|
const locationData = { latitude, longitude };
|
|
const locationJson = JSON.stringify(locationData);
|
|
|
|
console.log('更新用户位置信息:', { openid, locationJson });
|
|
|
|
// 准备要更新的数据
|
|
const updateData = {
|
|
updated_at: getBeijingTime()
|
|
};
|
|
|
|
// 如果有地址信息,将地址存储到authorized_region字段
|
|
if (address) {
|
|
updateData.authorized_region = address;
|
|
} else {
|
|
// 如果没有地址信息,才存储经纬度
|
|
updateData.authorized_region = locationJson;
|
|
}
|
|
|
|
const updateResult = await User.update(
|
|
updateData,
|
|
{ where: { openid } }
|
|
);
|
|
|
|
console.log('位置更新结果:', updateResult);
|
|
|
|
if (updateResult[0] > 0) {
|
|
// 查询更新后的用户数据
|
|
const updatedUser = await User.findOne({
|
|
where: { openid },
|
|
attributes: ['authorized_region', 'detailedaddress']
|
|
});
|
|
console.log('更新后的位置信息:', updatedUser.authorized_region);
|
|
console.log('更新后的地址信息:', updatedUser.detailedaddress);
|
|
|
|
return res.json({
|
|
success: true,
|
|
message: '位置和地址信息更新成功',
|
|
data: {
|
|
authorized_region: updatedUser.authorized_region,
|
|
address: updatedUser.detailedaddress
|
|
}
|
|
});
|
|
} else {
|
|
return res.json({ success: false, message: '用户不存在或位置信息未更新' });
|
|
}
|
|
} catch (error) {
|
|
console.error('更新位置信息失败:', error);
|
|
res.json({ success: false, message: '更新位置信息失败', error: error.message });
|
|
}
|
|
});
|
|
|
|
// 上传用户信息
|
|
app.post('/api/user/upload', async (req, res) => {
|
|
try {
|
|
const userData = req.body;
|
|
console.log('收到用户信息上传请求:', userData);
|
|
|
|
// 使用微信小程序拉取唯一电话号码的插件,不再需要检查手机号冲突
|
|
// 检查用户是否已存在,如果是更新操作,不需要强制要求手机号
|
|
let existingUser = await User.findOne({
|
|
where: { openid: userData.openid }
|
|
});
|
|
|
|
// 只有在创建新用户时才强制要求手机号
|
|
if (!existingUser && !userData.phoneNumber) {
|
|
return res.json({
|
|
success: false,
|
|
code: 400,
|
|
message: '创建新用户时必须提供手机号信息',
|
|
data: {}
|
|
});
|
|
}
|
|
|
|
// 查找用户是否已存在
|
|
let user = await User.findOne({
|
|
where: { openid: userData.openid }
|
|
});
|
|
|
|
// 详细日志记录收到的位置数据
|
|
console.log('处理位置数据前:', {
|
|
authorized_region: userData.authorized_region,
|
|
type: typeof userData.authorized_region,
|
|
isObject: typeof userData.authorized_region === 'object',
|
|
hasLatLng: userData.authorized_region && (userData.authorized_region.latitude || userData.authorized_region.longitude)
|
|
});
|
|
|
|
if (user) {
|
|
// 更新用户信息,确保authorized_region字段正确处理
|
|
const updateData = { ...userData, updated_at: getBeijingTime() };
|
|
|
|
// 特别处理authorized_region字段,只有当它是对象时才转换为JSON字符串
|
|
if (updateData.authorized_region && typeof updateData.authorized_region === 'object') {
|
|
updateData.authorized_region = JSON.stringify(updateData.authorized_region);
|
|
} else if (updateData.authorized_region === null || updateData.authorized_region === undefined) {
|
|
// 如果是null或undefined,设置为空字符串
|
|
updateData.authorized_region = '';
|
|
} else if (typeof updateData.authorized_region === 'string' && updateData.authorized_region.trim() === '') {
|
|
// 如果是空字符串,保持为空字符串
|
|
updateData.authorized_region = '';
|
|
} else if (typeof updateData.authorized_region === 'string') {
|
|
// 如果已经是字符串,保持不变
|
|
console.log('authorized_region已经是字符串,无需转换:', updateData.authorized_region);
|
|
}
|
|
|
|
console.log('更新用户数据:', { authorized_region: updateData.authorized_region });
|
|
|
|
await User.update(updateData, {
|
|
where: { openid: userData.openid }
|
|
});
|
|
user = await User.findOne({ where: { openid: userData.openid } });
|
|
|
|
console.log('更新后用户数据:', { authorized_region: user.authorized_region });
|
|
|
|
// 使用统一的关联记录创建函数
|
|
await createUserAssociations(user);
|
|
} else {
|
|
// 创建新用户,确保authorized_region字段正确处理
|
|
const createData = {
|
|
...userData,
|
|
notice: 'new', // 创建用户时固定设置notice为new
|
|
created_at: getBeijingTime(),
|
|
updated_at: getBeijingTime()
|
|
};
|
|
|
|
// 特别处理authorized_region字段,只有当它是对象时才转换为JSON字符串
|
|
if (createData.authorized_region && typeof createData.authorized_region === 'object') {
|
|
createData.authorized_region = JSON.stringify(createData.authorized_region);
|
|
} else if (createData.authorized_region === null || createData.authorized_region === undefined) {
|
|
// 如果是null或undefined,设置为空字符串
|
|
createData.authorized_region = '';
|
|
} else if (typeof createData.authorized_region === 'string' && createData.authorized_region.trim() === '') {
|
|
// 如果是空字符串,保持为空字符串
|
|
createData.authorized_region = '';
|
|
} else if (typeof createData.authorized_region === 'string') {
|
|
// 如果已经是字符串,保持不变
|
|
console.log('authorized_region已经是字符串,无需转换:', createData.authorized_region);
|
|
}
|
|
|
|
console.log('创建用户数据:', { authorized_region: createData.authorized_region });
|
|
|
|
user = await User.create(createData);
|
|
|
|
console.log('创建后用户数据:', { authorized_region: user.authorized_region });
|
|
|
|
// 使用统一的关联记录创建函数
|
|
await createUserAssociations(user);
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '用户信息保存成功',
|
|
data: {
|
|
userId: user.userId
|
|
},
|
|
phoneNumberConflict: false
|
|
});
|
|
} catch (error) {
|
|
console.error('保存用户信息失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '保存用户信息失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 解密手机号
|
|
app.post('/api/user/decodePhone', async (req, res) => {
|
|
try {
|
|
const { encryptedData, iv, openid } = req.body;
|
|
|
|
// 参数校验
|
|
if (!encryptedData || !iv || !openid) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要的参数'
|
|
});
|
|
}
|
|
|
|
// 查找用户的session_key
|
|
const user = await User.findOne({ where: { openid } });
|
|
|
|
if (!user || !user.session_key) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
code: 401,
|
|
message: '用户未登录,请先登录',
|
|
needRelogin: true
|
|
});
|
|
}
|
|
|
|
// 解密手机号
|
|
let decryptedData, phoneNumber;
|
|
try {
|
|
decryptedData = decryptData(encryptedData, user.session_key, iv);
|
|
phoneNumber = decryptedData.phoneNumber;
|
|
} catch (decryptError) {
|
|
// 解密失败,可能是session_key过期,建议重新登录
|
|
return res.status(401).json({
|
|
success: false,
|
|
code: 401,
|
|
message: decryptError.message || '手机号解密失败',
|
|
needRelogin: true
|
|
});
|
|
}
|
|
|
|
// 检查手机号是否已被其他用户使用
|
|
const existingUserWithPhone = await User.findOne({
|
|
where: {
|
|
phoneNumber: phoneNumber,
|
|
openid: { [Sequelize.Op.ne]: openid } // 排除当前用户
|
|
}
|
|
});
|
|
|
|
if (existingUserWithPhone) {
|
|
// 手机号已被其他用户使用,不更新手机号
|
|
console.warn(`手机号 ${phoneNumber} 已被其他用户使用,用户ID: ${existingUserWithPhone.userId}`);
|
|
|
|
// 返回成功,但不更新手机号,提示用户
|
|
return res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '手机号已被其他账号绑定',
|
|
phoneNumber: user.phoneNumber, // 返回原手机号
|
|
isNewPhone: false
|
|
});
|
|
}
|
|
|
|
// 更新用户手机号
|
|
await User.update(
|
|
{
|
|
phoneNumber: phoneNumber,
|
|
updated_at: getBeijingTime()
|
|
},
|
|
{
|
|
where: { openid }
|
|
}
|
|
);
|
|
|
|
// 更新用户手机号后,更新关联记录
|
|
const updatedUser = await User.findOne({ where: { openid } });
|
|
await createUserAssociations(updatedUser);
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '手机号解密成功',
|
|
phoneNumber: phoneNumber,
|
|
isNewPhone: true
|
|
});
|
|
} catch (error) {
|
|
console.error('手机号解密失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '手机号解密失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 处理微信登录,获取openid和session_key
|
|
// POST方法实现
|
|
app.post('/api/wechat/getOpenid', async (req, res) => {
|
|
try {
|
|
const { code } = req.body;
|
|
|
|
if (!code) {
|
|
return res.json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要参数code',
|
|
data: {}
|
|
});
|
|
}
|
|
|
|
// 获取openid和session_key
|
|
const wxData = await getSessionKey(code);
|
|
|
|
if (wxData.errcode) {
|
|
return res.json({
|
|
success: false,
|
|
code: 400,
|
|
message: `微信接口错误: ${wxData.errmsg}`,
|
|
data: {}
|
|
});
|
|
}
|
|
|
|
const { openid, session_key, unionid } = wxData;
|
|
|
|
// 生成userId
|
|
const userId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
// 查找用户是否已存在
|
|
let user = await User.findOne({
|
|
where: { openid }
|
|
});
|
|
|
|
if (user) {
|
|
// 更新用户session_key
|
|
await User.update(
|
|
{
|
|
session_key: session_key,
|
|
updated_at: getBeijingTime()
|
|
},
|
|
{
|
|
where: { openid }
|
|
}
|
|
);
|
|
} else {
|
|
// 创建新用户
|
|
// 支持从客户端传入type参数,如果没有则默认为buyer
|
|
const userType = req.body.type || 'buyer';
|
|
await User.create({
|
|
openid,
|
|
userId,
|
|
session_key,
|
|
name: '微信用户', // 临时占位,等待用户授权
|
|
nickName: '微信用户', // 数据库NOT NULL字段
|
|
phoneNumber: '', // 使用空字符串代替临时手机号,后续由微信小程序拉取的真实手机号更新
|
|
type: userType, // 使用客户端传入的类型或默认买家身份
|
|
province: '', // 默认空字符串
|
|
city: '', // 默认空字符串
|
|
district: '', // 默认空字符串
|
|
proofurl: '', // 默认空字符串
|
|
collaborationid: '', // 默认空字符串
|
|
cooperation: '', // 默认空字符串
|
|
businesslicenseurl: '', // 默认空字符串
|
|
notice: 'new', // 创建用户时固定设置notice为new
|
|
created_at: getBeijingTime(),
|
|
updated_at: getBeijingTime()
|
|
});
|
|
|
|
// 为新创建的用户创建关联记录
|
|
const newUser = { userId, openid, name: '微信用户', phoneNumber: '' };
|
|
await createUserAssociations(newUser);
|
|
}
|
|
|
|
// 确保返回的data字段始终存在且不为空
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '获取openid成功',
|
|
data: {
|
|
openid: openid || '',
|
|
userId: user ? user.userId : userId,
|
|
session_key: session_key || '',
|
|
unionid: unionid || ''
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('获取openid失败:', error);
|
|
// 错误情况下也确保返回data字段
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取openid失败',
|
|
error: error.message,
|
|
data: {}
|
|
});
|
|
}
|
|
});
|
|
|
|
// 验证用户登录状态
|
|
app.post('/api/user/validate', async (req, res) => {
|
|
try {
|
|
const { openid } = req.body;
|
|
|
|
if (!openid) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少openid参数'
|
|
});
|
|
}
|
|
|
|
// 查找用户
|
|
const user = await User.findOne({
|
|
where: { openid },
|
|
attributes: ['openid', 'userId', 'name', 'avatarUrl', 'phoneNumber', 'type', 'partnerstatus', 'reason', 'idcardstatus']
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
code: 401,
|
|
message: '用户未登录'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '验证成功',
|
|
data: user
|
|
});
|
|
} catch (error) {
|
|
console.error('验证用户登录状态失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '验证失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 获取用户信息
|
|
app.post('/api/user/get', async (req, res) => {
|
|
try {
|
|
const { openid } = req.body;
|
|
|
|
if (!openid) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少openid参数'
|
|
});
|
|
}
|
|
|
|
// 查找用户
|
|
const user = await User.findOne({
|
|
where: { openid },
|
|
include: [
|
|
{
|
|
model: Contact,
|
|
as: 'contacts',
|
|
attributes: ['id', 'name', 'phoneNumber', 'wechat', 'account', 'accountNumber', 'bank', 'address']
|
|
},
|
|
{
|
|
model: UserManagement,
|
|
as: 'management',
|
|
attributes: ['id', 'managerId', 'department', 'organization', 'role', 'root']
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '用户不存在'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '获取用户信息成功',
|
|
data: user
|
|
});
|
|
} catch (error) {
|
|
console.error('获取用户信息失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取用户信息失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 获取goods_root数据
|
|
app.post('/api/goods/root', async (req, res) => {
|
|
try {
|
|
console.log('===== 获取goods_root数据请求 =====');
|
|
console.log('1. 收到请求体:', JSON.stringify(req.body, null, 2));
|
|
|
|
// 从数据库获取goods_root表中的所有数据
|
|
const roots = await GoodsRoot.findAll();
|
|
console.log('2. 数据库查询结果数量:', roots.length);
|
|
console.log('3. 数据库查询结果:', roots);
|
|
|
|
// 格式化响应数据
|
|
const formattedRoots = roots.map(root => ({
|
|
id: root.id,
|
|
name: root.name,
|
|
root: root.root
|
|
}));
|
|
|
|
console.log('4. 格式化后的结果:', formattedRoots);
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '获取goods_root数据成功',
|
|
roots: formattedRoots
|
|
});
|
|
} catch (error) {
|
|
console.error('获取goods_root数据失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取goods_root数据失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 添加用户踪迹记录
|
|
app.post('/api/user-trace/add', async (req, res) => {
|
|
try {
|
|
const { phoneNumber, userId, originalData } = req.body;
|
|
|
|
if (!userId || !originalData) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要参数userId或originalData'
|
|
});
|
|
}
|
|
|
|
// 创建用户踪迹记录
|
|
const trace = await UserTrace.create({
|
|
phoneNumber: phoneNumber || '',
|
|
userId: userId,
|
|
operationTime: new Date(),
|
|
originalData: JSON.stringify(originalData)
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '用户踪迹记录添加成功',
|
|
data: trace
|
|
});
|
|
} catch (error) {
|
|
console.error('添加用户踪迹记录失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '添加用户踪迹记录失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 更新用户信息
|
|
app.post('/api/user/update', async (req, res) => {
|
|
try {
|
|
const { openid, ...updateData } = req.body;
|
|
|
|
if (!openid) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少openid参数'
|
|
});
|
|
}
|
|
|
|
// 查找用户
|
|
const user = await User.findOne({
|
|
where: { openid }
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '用户不存在'
|
|
});
|
|
}
|
|
|
|
// 更新用户信息
|
|
await User.update(
|
|
{
|
|
...updateData,
|
|
updated_at: getBeijingTime()
|
|
},
|
|
{
|
|
where: { openid }
|
|
}
|
|
);
|
|
|
|
// 获取更新后的用户信息
|
|
const updatedUser = await User.findOne({
|
|
where: { openid }
|
|
});
|
|
|
|
// 使用统一的关联记录创建函数
|
|
await createUserAssociations(updatedUser);
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '更新用户信息成功',
|
|
data: updatedUser
|
|
});
|
|
} catch (error) {
|
|
console.error('更新用户信息失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '更新用户信息失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 获取商品分类列表 - 返回不重复的分类
|
|
app.get('/api/product/categories', async (req, res) => {
|
|
try {
|
|
const { openid } = req.query;
|
|
|
|
console.log('获取商品分类列表, openid:', openid || '未提供');
|
|
|
|
// 使用 Sequelize 的 distinct 查询获取不重复的分类
|
|
const products = await Product.findAll({
|
|
attributes: [
|
|
[Sequelize.col('category'), 'category']
|
|
],
|
|
where: {
|
|
category: {
|
|
[Sequelize.Op.ne]: null,
|
|
[Sequelize.Op.ne]: ''
|
|
}
|
|
},
|
|
group: ['category']
|
|
});
|
|
|
|
// 提取分类数组
|
|
const categories = products.map(p => p.category).filter(c => c);
|
|
|
|
console.log('获取到的分类列表:', categories);
|
|
|
|
res.json({
|
|
success: true,
|
|
categories: ['全部', ...categories] // 添加"全部"选项
|
|
});
|
|
} catch (error) {
|
|
console.error('获取分类列表失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取分类列表失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 获取商品列表 - 优化版本确保状态筛选正确应用
|
|
app.post('/api/product/list', async (req, res) => {
|
|
try {
|
|
const { openid, status, keyword, category, page = 1, pageSize = 20, testMode = false, viewMode = 'shopping' } = req.body;
|
|
|
|
// 查找用户 - 如果提供了openid,则查找用户信息,否则允许匿名访问
|
|
let user = null;
|
|
if (openid) { // 只要提供了openid,就查找用户
|
|
user = await User.findOne({ where: { openid } });
|
|
|
|
// 注意:这里不再检查用户是否存在,允许openid无效的情况
|
|
if (!user) {
|
|
console.log('提供的openid无效,将以匿名方式访问');
|
|
}
|
|
}
|
|
|
|
console.log(`当前请求模式: ${viewMode},用户类型: ${user ? user.type : '测试模式'}`);
|
|
|
|
// 根据viewMode决定是否限制sellerId,无论是否为测试模式
|
|
console.log(`处理viewMode: ${viewMode},确保在shopping模式下不添加sellerId过滤`);
|
|
|
|
// 构建查询条件 - 根据viewMode决定是否包含sellerId
|
|
const where = {};
|
|
|
|
if (viewMode === 'seller') {
|
|
if (user) {
|
|
// 任何用户(包括测试模式)都只能查看自己的商品
|
|
console.log(`货源页面 - 用户 ${user.userId} 只能查看自己的商品,testMode: ${testMode}`);
|
|
where.sellerId = user.userId;
|
|
} else {
|
|
// 没有用户信息的情况
|
|
console.log('错误:没有用户信息,严格限制不返回任何商品');
|
|
where.sellerId = 'INVALID_USER_ID'; // 确保返回空结果
|
|
}
|
|
} else if (viewMode === 'shopping') {
|
|
// 购物页面:明确不设置sellerId,允许查看所有用户的商品
|
|
console.log('购物模式 - 不限制sellerId,允许查看所有用户的商品');
|
|
// 不添加任何sellerId过滤条件
|
|
}
|
|
// 其他模式:不限制sellerId
|
|
|
|
// 状态筛选 - 直接构建到where对象中,确保不会丢失
|
|
console.log(`当前用户类型: ${user ? user.type : '未知'},请求状态: ${status || '未指定'},测试模式: ${testMode}`);
|
|
|
|
// 初始化status筛选条件,确保总是有有效的状态过滤
|
|
let statusCondition = {};
|
|
|
|
// 如果有指定status参数,按参数筛选但同时排除hidden
|
|
if (status) {
|
|
console.log(`按状态筛选商品: status=${status},并排除hidden状态`);
|
|
if (status === 'all') {
|
|
// 特殊情况:请求所有商品但仍然排除hidden
|
|
statusCondition = { [Sequelize.Op.not]: 'hidden' };
|
|
} else if (Array.isArray(status)) {
|
|
// 如果status是数组,确保不包含hidden
|
|
const validStatuses = status.filter(s => s !== 'hidden');
|
|
if (validStatuses.length > 0) {
|
|
statusCondition = { [Sequelize.Op.in]: validStatuses };
|
|
} else {
|
|
statusCondition = { [Sequelize.Op.not]: 'hidden' };
|
|
}
|
|
} else {
|
|
// 单个状态值,确保不是hidden
|
|
if (status !== 'hidden') {
|
|
statusCondition = { [Sequelize.Op.eq]: status };
|
|
} else {
|
|
// 如果明确请求hidden状态,也返回空结果
|
|
statusCondition = { [Sequelize.Op.not]: 'hidden' };
|
|
}
|
|
}
|
|
} else {
|
|
// 没有指定status参数时 - 直接在where对象中设置状态筛选
|
|
if (user && (user.type === 'seller' || user.type === 'both') && viewMode === 'seller' && !testMode) {
|
|
// 卖家用户查看自己的商品列表
|
|
console.log(`卖家用户 ${user.userId} (类型:${user.type}) 查看自己的所有商品,但排除hidden状态`);
|
|
// 卖家可以查看自己的所有商品,但仍然排除hidden状态
|
|
statusCondition = { [Sequelize.Op.not]: 'hidden' };
|
|
} else {
|
|
// 未登录用户、买家用户或购物模式
|
|
console.log(`未登录用户、买家或购物模式,使用默认状态筛选: pending_review/reviewed/published`);
|
|
// 默认显示审核中、已审核和已发布的商品,排除hidden和sold_out状态
|
|
statusCondition = { [Sequelize.Op.in]: ['pending_review', 'reviewed', 'published'] };
|
|
}
|
|
}
|
|
|
|
// 确保设置有效的status查询条件
|
|
where.status = statusCondition;
|
|
console.log(`设置的status查询条件:`, JSON.stringify(statusCondition, null, 2));
|
|
|
|
console.log(`构建的完整查询条件:`, JSON.stringify(where, null, 2));
|
|
|
|
// 关键词搜索 - 同时搜索多个字段
|
|
if (keyword) {
|
|
where[Sequelize.Op.or] = [
|
|
{ productName: { [Sequelize.Op.like]: `%${keyword}%` } },
|
|
{ specification: { [Sequelize.Op.like]: `%${keyword}%` } },
|
|
{ region: { [Sequelize.Op.like]: `%${keyword}%` } },
|
|
{ grossWeight: { [Sequelize.Op.like]: `%${keyword}%` } },
|
|
{ yolk: { [Sequelize.Op.like]: `%${keyword}%` } }
|
|
];
|
|
}
|
|
|
|
// 分类筛选
|
|
if (category) {
|
|
where.category = { [Sequelize.Op.eq]: category };
|
|
}
|
|
|
|
// 计算偏移量
|
|
const offset = (page - 1) * pageSize;
|
|
|
|
// 查询商品列表 - 直接使用Product表中的reservedCount字段
|
|
const { count, rows: products } = await Product.findAndCountAll({
|
|
where,
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'seller',
|
|
attributes: ['userId', 'name', 'nickName', 'avatarUrl']
|
|
}
|
|
],
|
|
attributes: [
|
|
'id',
|
|
'productId',
|
|
'sellerId',
|
|
'productName',
|
|
'price',
|
|
'costprice',
|
|
'quantity',
|
|
'grossWeight',
|
|
'yolk',
|
|
'specification',
|
|
'created_at',
|
|
'updated_at',
|
|
'imageUrls',
|
|
'status',
|
|
'region',
|
|
'sourceType',
|
|
'supplyStatus',
|
|
'category',
|
|
'producting',
|
|
'description',
|
|
'frequency',
|
|
'product_log',
|
|
'spec_status'
|
|
],
|
|
order: [['created_at', 'DESC']],
|
|
limit: pageSize,
|
|
offset
|
|
});
|
|
|
|
// 添加详细日志,记录查询结果
|
|
console.log(`商品列表查询结果 - 商品数量: ${count}, 商品列表长度: ${products.length}`);
|
|
if (products.length > 0) {
|
|
console.log(`第一个商品数据:`, JSON.stringify(products[0], null, 2));
|
|
}
|
|
|
|
// 处理商品列表中的grossWeight字段,确保是数字类型,同时反序列化imageUrls
|
|
const processedProducts = await Promise.all(products.map(async product => {
|
|
const productJSON = product.toJSON();
|
|
|
|
// 确保created_at字段存在并转换为正确格式
|
|
if (!productJSON.created_at) {
|
|
console.log('商品缺少created_at字段,使用默认值');
|
|
productJSON.created_at = getBeijingTimeISOString();
|
|
} else {
|
|
// 确保created_at是字符串格式,不进行多余的时区转换
|
|
if (productJSON.created_at instanceof Date) {
|
|
productJSON.created_at = productJSON.created_at.toISOString();
|
|
} else if (typeof productJSON.created_at !== 'string') {
|
|
productJSON.created_at = new Date(productJSON.created_at).toISOString();
|
|
}
|
|
}
|
|
|
|
// 详细分析毛重字段
|
|
const grossWeightDetails = {
|
|
type: typeof productJSON.grossWeight,
|
|
isEmpty: productJSON.grossWeight === '' || productJSON.grossWeight === null || productJSON.grossWeight === undefined,
|
|
isString: typeof productJSON.grossWeight === 'string',
|
|
value: productJSON.grossWeight === '' || productJSON.grossWeight === null || productJSON.grossWeight === undefined ? '' : String(productJSON.grossWeight)
|
|
};
|
|
|
|
// 确保grossWeight值是字符串类型
|
|
productJSON.grossWeight = String(grossWeightDetails.value);
|
|
|
|
// 查询该商品的收藏人数 - 从favorites表中统计
|
|
const favoriteCount = await Favorite.count({
|
|
where: {
|
|
productId: productJSON.productId
|
|
}
|
|
});
|
|
|
|
// 使用查询到的收藏人数更新reservedCount字段
|
|
productJSON.reservedCount = favoriteCount;
|
|
|
|
// 重要修复:反序列化imageUrls字段,确保前端收到的是数组
|
|
if (productJSON.imageUrls && typeof productJSON.imageUrls === 'string') {
|
|
try {
|
|
console.log('【imageUrls修复】尝试反序列化JSON字符串:', productJSON.imageUrls);
|
|
|
|
// 增强修复:在反序列化前先清理可能有问题的JSON字符串
|
|
let cleanJsonStr = productJSON.imageUrls;
|
|
|
|
// 1. 移除多余的反斜杠
|
|
cleanJsonStr = cleanJsonStr.replace(/\\\\/g, '\\');
|
|
|
|
// 2. 移除可能导致JSON解析错误的字符(如反引号)
|
|
cleanJsonStr = cleanJsonStr.replace(/[`]/g, '');
|
|
|
|
// 3. 尝试反序列化清理后的字符串
|
|
const parsedImageUrls = JSON.parse(cleanJsonStr);
|
|
|
|
if (Array.isArray(parsedImageUrls)) {
|
|
// 4. 对数组中的每个URL应用清理函数
|
|
productJSON.imageUrls = parsedImageUrls.map(url => {
|
|
if (typeof url === 'string') {
|
|
// 移除URL中的反斜杠和特殊字符
|
|
return url.replace(/\\/g, '').replace(/[`]/g, '').trim();
|
|
}
|
|
return '';
|
|
}).filter(url => url && url.length > 0); // 过滤掉空URL
|
|
|
|
console.log('【imageUrls修复】反序列化成功,清理后得到数组长度:', productJSON.imageUrls.length);
|
|
} else {
|
|
console.warn('【imageUrls修复】反序列化结果不是数组,使用空数组');
|
|
productJSON.imageUrls = [];
|
|
}
|
|
} catch (error) {
|
|
console.error('【imageUrls修复】反序列化失败:', error);
|
|
// 简单处理:直接设置为空数组
|
|
productJSON.imageUrls = [];
|
|
}
|
|
} else if (!Array.isArray(productJSON.imageUrls)) {
|
|
console.warn('【imageUrls修复】imageUrls不是数组,使用空数组');
|
|
productJSON.imageUrls = [];
|
|
}
|
|
|
|
// 确保seller对象的nickName字段正确返回
|
|
if (productJSON.seller) {
|
|
// 确保seller对象结构正确,处理nickName字段
|
|
console.log('seller对象各字段值:');
|
|
console.log('- seller.userId:', productJSON.seller.userId);
|
|
console.log('- seller.nickName:', productJSON.seller.nickName);
|
|
console.log('- seller.name:', productJSON.seller.name);
|
|
|
|
// 按照用户要求,只使用users表里的nickName
|
|
const nickName = productJSON.seller.nickName || productJSON.seller.name || '未知';
|
|
console.log('最终确定的nickName:', nickName);
|
|
|
|
productJSON.seller = {
|
|
...productJSON.seller,
|
|
// 只使用nickName字段
|
|
nickName: nickName,
|
|
sellerNickName: nickName, // 确保sellerNickName字段存在
|
|
sellerName: nickName
|
|
};
|
|
} else {
|
|
// 如果没有seller对象,创建默认对象
|
|
productJSON.seller = {
|
|
nickName: '未知',
|
|
sellerNickName: '未知',
|
|
sellerName: '未知'
|
|
};
|
|
}
|
|
|
|
// 确保created_at字段存在
|
|
console.log('productJSON.created_at:', productJSON.created_at);
|
|
if (!productJSON.created_at) {
|
|
console.warn('商品缺少created_at字段,使用默认值');
|
|
productJSON.created_at = getBeijingTimeISOString();
|
|
}
|
|
|
|
// 处理产品日志字段,确保返回数组格式
|
|
if (productJSON.product_log) {
|
|
console.log('【产品日志】原始product_log:', productJSON.product_log, '类型:', typeof productJSON.product_log);
|
|
if (typeof productJSON.product_log === 'string') {
|
|
try {
|
|
productJSON.product_log = JSON.parse(productJSON.product_log);
|
|
console.log('【产品日志】反序列化后的product_log:', productJSON.product_log, '类型:', typeof productJSON.product_log);
|
|
// 确保是数组格式
|
|
if (!Array.isArray(productJSON.product_log)) {
|
|
productJSON.product_log = [productJSON.product_log];
|
|
console.log('【产品日志】转换为数组:', productJSON.product_log);
|
|
}
|
|
} catch (parseError) {
|
|
console.error('【产品日志】反序列化失败:', parseError);
|
|
// 如果解析失败,将字符串作为单个日志条目
|
|
productJSON.product_log = [productJSON.product_log];
|
|
console.log('【产品日志】转换为单条日志:', productJSON.product_log);
|
|
}
|
|
} else if (!Array.isArray(productJSON.product_log)) {
|
|
// 如果不是字符串也不是数组,转换为数组
|
|
productJSON.product_log = [productJSON.product_log];
|
|
console.log('【产品日志】转换为数组:', productJSON.product_log);
|
|
}
|
|
} else {
|
|
// 如果没有日志,返回空数组
|
|
productJSON.product_log = [];
|
|
console.log('【产品日志】没有日志,返回空数组');
|
|
}
|
|
|
|
// 记录第一个商品的转换信息用于调试
|
|
if (products.indexOf(product) === 0) {
|
|
console.log('商品列表 - 第一个商品毛重字段处理:');
|
|
console.log('- 原始值:', grossWeightDetails.value, '类型:', grossWeightDetails.type);
|
|
console.log('- 转换后的值:', productJSON.grossWeight, '类型:', typeof productJSON.grossWeight);
|
|
console.log('- reservedCount值:', productJSON.reservedCount, '类型:', typeof productJSON.reservedCount);
|
|
console.log('- seller信息:', JSON.stringify(productJSON.seller));
|
|
console.log('- product_log信息:', JSON.stringify(productJSON.product_log));
|
|
}
|
|
|
|
return productJSON;
|
|
}));
|
|
|
|
// 准备响应数据 - 修改格式以匹配前端期望
|
|
const responseData = {
|
|
success: true,
|
|
code: 200,
|
|
message: '获取商品列表成功',
|
|
products: processedProducts,
|
|
total: count,
|
|
page: page,
|
|
pageSize: pageSize,
|
|
totalPages: Math.ceil(count / pageSize)
|
|
};
|
|
|
|
console.log(`准备返回的响应数据格式:`, JSON.stringify(responseData, null, 2).substring(0, 500) + '...');
|
|
|
|
// 添加详细的查询条件日志
|
|
console.log(`最终查询条件:`, JSON.stringify(where, null, 2));
|
|
|
|
res.json(responseData);
|
|
} catch (error) {
|
|
console.error('获取商品列表失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取商品列表失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 上传商品 - 支持图片上传到阿里云OSS,修复支持多文件同时上传
|
|
app.post('/api/products/upload', upload.array('images', 10), async (req, res) => {
|
|
// 【全局保护】提前检测是否是仅图片上传的请求
|
|
const isAddImagesOnly = req.body.action === 'add_images_only' || req.body.isUpdate === 'true';
|
|
const existingProductId = req.body.productId;
|
|
|
|
if (isAddImagesOnly && existingProductId) {
|
|
console.log('【提前路由】检测到仅图片上传请求,直接处理');
|
|
return await handleAddImagesToExistingProduct(req, res, existingProductId, req.files || []);
|
|
}
|
|
|
|
let productData;
|
|
let uploadedFiles = req.files || []; // 修复:使用req.files而不是空数组
|
|
let imageUrls = [];
|
|
let product;
|
|
let tempFilesToClean = []; // 存储需要清理的临时文件路径
|
|
|
|
try {
|
|
// 【关键修复】首先检查是否是仅上传图片到已存在商品的请求
|
|
const isAddImagesOnly = req.body.action === 'add_images_only' || req.body.isUpdate === 'true';
|
|
const existingProductId = req.body.productId;
|
|
|
|
if (isAddImagesOnly && existingProductId) {
|
|
console.log('【图片更新模式】仅添加图片到已存在商品,商品ID:', existingProductId);
|
|
return await handleAddImagesToExistingProduct(req, res, existingProductId, uploadedFiles);
|
|
}
|
|
|
|
// 【关键修复】安全解析 productData - 修复 undefined 错误
|
|
try {
|
|
// 检查 productData 是否存在且不是字符串 'undefined'
|
|
if (req.body.productData && req.body.productData !== 'undefined' && req.body.productData !== undefined) {
|
|
productData = JSON.parse(req.body.productData);
|
|
console.log('成功解析 productData:', productData);
|
|
} else {
|
|
console.log('【关键修复】productData 不存在或为 undefined,使用 req.body');
|
|
productData = req.body;
|
|
|
|
// 对于仅图片上传的情况,需要特别处理
|
|
if (req.body.action === 'add_images_only' && req.body.productId) {
|
|
console.log('【图片上传模式】检测到仅图片上传请求,构建基础 productData');
|
|
productData = {
|
|
productId: req.body.productId,
|
|
sellerId: req.body.openid,
|
|
action: 'add_images_only'
|
|
};
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('解析 productData 失败,使用 req.body:', e.message);
|
|
productData = req.body;
|
|
}
|
|
|
|
// ========== 【新增】详细的地区字段调试信息 ==========
|
|
console.log('【地区字段调试】开始处理地区字段');
|
|
console.log('【地区字段调试】原始productData.region:', productData.region, '类型:', typeof productData.region);
|
|
console.log('【地区字段调试】原始请求体中的region字段:', req.body.region);
|
|
|
|
// 【新增】处理地区字段 - 增强版
|
|
if (productData.region) {
|
|
console.log('【地区字段调试】检测到地区字段:', productData.region, '类型:', typeof productData.region);
|
|
// 确保地区字段是字符串类型
|
|
if (typeof productData.region !== 'string') {
|
|
console.log('【地区字段调试】地区字段不是字符串,转换为字符串:', String(productData.region));
|
|
productData.region = String(productData.region);
|
|
}
|
|
console.log('【地区字段调试】处理后的地区字段:', productData.region, '类型:', typeof productData.region);
|
|
} else {
|
|
console.log('【地区字段调试】未检测到地区字段,设置为默认值或空');
|
|
productData.region = productData.region || ''; // 确保有默认值
|
|
console.log('【地区字段调试】设置默认值后的地区字段:', productData.region);
|
|
}
|
|
|
|
// 检查是否从其他来源传递了地区信息
|
|
if (req.body.region && !productData.region) {
|
|
console.log('【地区字段调试】从请求体中发现地区字段:', req.body.region);
|
|
productData.region = req.body.region;
|
|
}
|
|
|
|
console.log('【地区字段调试】最终确定的地区字段:', productData.region);
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
console.log('收到商品上传请求,处理后的 productData:', productData);
|
|
|
|
// 检查是否是简化上传模式(单步创建)
|
|
const isNewProduct = productData.isNewProduct === true;
|
|
console.log('是否为新商品创建:', isNewProduct);
|
|
|
|
// 改进的毛重字段处理逻辑,与编辑API保持一致
|
|
// 详细分析毛重字段
|
|
const grossWeightDetails = {
|
|
value: productData.grossWeight === '' || productData.grossWeight === null || productData.grossWeight === undefined ? '' : String(productData.grossWeight),
|
|
type: typeof productData.grossWeight,
|
|
isEmpty: productData.grossWeight === '' || productData.grossWeight === null || productData.grossWeight === undefined,
|
|
isString: typeof productData.grossWeight === 'string'
|
|
};
|
|
|
|
// 详细的日志记录
|
|
console.log('上传商品 - 毛重字段详细分析:');
|
|
console.log('- 原始值:', productData.grossWeight, '类型:', typeof productData.grossWeight);
|
|
console.log('- 是否为空值:', grossWeightDetails.isEmpty);
|
|
console.log('- 是否为字符串类型:', grossWeightDetails.isString);
|
|
console.log('- 转换后的值:', grossWeightDetails.value, '类型:', typeof grossWeightDetails.value);
|
|
|
|
// 确保grossWeight值是字符串类型,直接使用处理后的值
|
|
productData.grossWeight = grossWeightDetails.value;
|
|
console.log('上传商品 - 最终存储的毛重值:', productData.grossWeight, '类型:', typeof productData.grossWeight);
|
|
|
|
// 验证必要字段
|
|
if (!productData.sellerId || !productData.productName || !productData.price || !productData.quantity) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要的商品信息'
|
|
});
|
|
}
|
|
|
|
// 处理图片上传逻辑 - 使用批量上传确保多图片上传成功
|
|
try {
|
|
console.log('===== 图片处理逻辑 =====');
|
|
console.log('- 上传文件数量:', uploadedFiles.length);
|
|
console.log('- 是否有productData:', Boolean(productData));
|
|
|
|
// 首先处理所有上传的文件
|
|
if (uploadedFiles.length > 0) {
|
|
console.log('处理上传文件...');
|
|
|
|
// 创建已上传URL集合,用于避免重复
|
|
const uploadedFileUrls = new Set();
|
|
|
|
// 准备文件路径数组
|
|
const filePaths = uploadedFiles.map(file => {
|
|
// 添加到临时文件清理列表
|
|
tempFilesToClean.push(file.path);
|
|
return file.path;
|
|
});
|
|
|
|
// 使用商品名称作为文件夹名,确保每个商品的图片独立存储
|
|
// 移除商品名称中的特殊字符,确保可以作为合法的文件夹名
|
|
const safeProductName = productData.productName
|
|
.replace(/[\/:*?"<>|]/g, '_') // 移除不合法的文件名字符
|
|
.substring(0, 50); // 限制长度
|
|
|
|
// 构建基础文件夹路径
|
|
const folderPath = `products/${safeProductName}`;
|
|
|
|
console.log(`准备批量上传到文件夹: ${folderPath}`);
|
|
|
|
// 创建自定义的文件上传函数,添加详细错误处理和连接测试
|
|
async function customUploadFile(filePath, folder) {
|
|
try {
|
|
console.log(`===== customUploadFile 开始 =====`);
|
|
console.log(`文件路径: ${filePath}`);
|
|
console.log(`目标文件夹: ${folder}`);
|
|
|
|
// 引入必要的模块
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const { createHash } = require('crypto');
|
|
|
|
// 确保文件存在
|
|
const fileExists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
if (!fileExists) {
|
|
throw new Error(`文件不存在: ${filePath}`);
|
|
}
|
|
console.log('文件存在性检查通过');
|
|
|
|
// 获取文件信息
|
|
const stats = await fs.promises.stat(filePath);
|
|
console.log(`文件大小: ${stats.size} 字节, 创建时间: ${stats.birthtime}`);
|
|
|
|
// 获取文件扩展名
|
|
const extname = path.extname(filePath).toLowerCase();
|
|
if (!extname) {
|
|
throw new Error(`无法获取文件扩展名: ${filePath}`);
|
|
}
|
|
console.log(`文件扩展名: ${extname}`);
|
|
|
|
// 基于文件内容计算MD5哈希值,实现文件级去重
|
|
console.log('开始计算文件MD5哈希值...');
|
|
const hash = createHash('md5');
|
|
const stream = fs.createReadStream(filePath);
|
|
await new Promise((resolve, reject) => {
|
|
stream.on('error', reject);
|
|
stream.on('data', chunk => hash.update(chunk));
|
|
stream.on('end', () => resolve());
|
|
});
|
|
const fileHash = hash.digest('hex');
|
|
console.log(`文件哈希值计算完成: ${fileHash}`);
|
|
|
|
// 构建OSS文件路径
|
|
const uniqueFilename = `${fileHash}${extname}`;
|
|
const ossFilePath = `${folder}/${uniqueFilename}`;
|
|
console.log(`准备上传到OSS路径: ${ossFilePath}`);
|
|
|
|
// 直接创建OSS客户端
|
|
console.log('正在直接创建OSS客户端...');
|
|
const OSS = require('ali-oss');
|
|
const ossConfig = require('./oss-config');
|
|
|
|
// 打印OSS配置(敏感信息隐藏)
|
|
console.log('OSS配置信息:');
|
|
console.log(`- region: ${ossConfig.region}`);
|
|
console.log(`- bucket: ${ossConfig.bucket}`);
|
|
console.log(`- accessKeyId: ${ossConfig.accessKeyId ? '已配置' : '未配置'}`);
|
|
console.log(`- accessKeySecret: ${ossConfig.accessKeySecret ? '已配置' : '未配置'}`);
|
|
|
|
// 使用配置创建OSS客户端
|
|
const ossClient = new OSS({
|
|
region: ossConfig.region,
|
|
accessKeyId: ossConfig.accessKeyId,
|
|
accessKeySecret: ossConfig.accessKeySecret,
|
|
bucket: ossConfig.bucket
|
|
});
|
|
|
|
console.log('OSS客户端创建成功');
|
|
|
|
// 测试OSS连接
|
|
console.log('正在测试OSS连接...');
|
|
try {
|
|
await ossClient.list({ max: 1 });
|
|
console.log('OSS连接测试成功');
|
|
} catch (connectionError) {
|
|
console.error('OSS连接测试失败:', connectionError.message);
|
|
throw new Error(`OSS连接失败,请检查配置和网络: ${connectionError.message}`);
|
|
}
|
|
|
|
// 上传文件,明确设置为公共读权限
|
|
console.log(`开始上传文件到OSS...`);
|
|
console.log(`上传参数: { filePath: ${ossFilePath}, localPath: ${filePath} }`);
|
|
|
|
// 添加超时控制
|
|
const uploadPromise = ossClient.put(ossFilePath, filePath, {
|
|
headers: {
|
|
'x-oss-object-acl': 'public-read'
|
|
},
|
|
acl: 'public-read'
|
|
});
|
|
|
|
// 设置30秒超时
|
|
const result = await Promise.race([
|
|
uploadPromise,
|
|
new Promise((_, reject) => setTimeout(() => reject(new Error('上传超时')), 30000))
|
|
]);
|
|
|
|
console.log(`文件上传成功!`);
|
|
console.log(`- OSS响应:`, JSON.stringify(result));
|
|
console.log(`- 返回URL: ${result.url}`);
|
|
|
|
// 验证URL
|
|
if (!result.url) {
|
|
throw new Error('上传成功但未返回有效URL');
|
|
}
|
|
|
|
console.log(`===== customUploadFile 成功完成 =====`);
|
|
return result.url;
|
|
} catch (error) {
|
|
console.error('文件上传失败:', error.message);
|
|
console.error('错误类型:', error.name);
|
|
console.error('错误详情:', error);
|
|
console.error('错误堆栈:', error.stack);
|
|
throw new Error(`文件上传到OSS失败: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// 改进的上传逻辑:使用逐个上传,添加更详细的日志和错误处理
|
|
console.log('开始逐个上传文件,数量:', filePaths.length);
|
|
let uploadResults = [];
|
|
|
|
for (let i = 0; i < filePaths.length; i++) {
|
|
console.log(`=== 开始处理文件 ${i + 1}/${filePaths.length} ===`);
|
|
console.log(`文件路径: ${filePaths[i]}`);
|
|
|
|
try {
|
|
// 检查文件是否存在并可访问
|
|
const fs = require('fs');
|
|
if (!fs.existsSync(filePaths[i])) {
|
|
throw new Error(`文件不存在或无法访问: ${filePaths[i]}`);
|
|
}
|
|
|
|
const stats = fs.statSync(filePaths[i]);
|
|
console.log(`文件大小: ${stats.size} 字节`);
|
|
|
|
console.log(`调用customUploadFile上传文件...`);
|
|
const uploadedUrl = await customUploadFile(filePaths[i], folderPath);
|
|
|
|
console.log(`文件 ${i + 1} 上传成功: ${uploadedUrl}`);
|
|
uploadResults.push({ fileIndex: i, success: true, url: uploadedUrl });
|
|
|
|
if (uploadedUrl && !uploadedFileUrls.has(uploadedUrl)) {
|
|
imageUrls.push(uploadedUrl);
|
|
uploadedFileUrls.add(uploadedUrl);
|
|
console.log(`已添加URL到结果数组,当前总数量: ${imageUrls.length}`);
|
|
} else if (uploadedFileUrls.has(uploadedUrl)) {
|
|
console.log(`文件 ${i + 1} 的URL已存在,跳过重复添加: ${uploadedUrl}`);
|
|
} else {
|
|
console.error(`文件 ${i + 1} 上传成功但返回的URL为空或无效`);
|
|
}
|
|
} catch (singleError) {
|
|
console.error(`文件 ${i + 1} 上传失败:`);
|
|
console.error(`错误信息:`, singleError.message);
|
|
console.error(`失败文件路径: ${filePaths[i]}`);
|
|
console.error(`错误堆栈:`, singleError.stack);
|
|
uploadResults.push({ fileIndex: i, success: false, error: singleError.message });
|
|
// 继续上传下一个文件,不中断整个流程
|
|
}
|
|
console.log(`=== 文件 ${i + 1}/${filePaths.length} 处理完成 ===\n`);
|
|
}
|
|
|
|
console.log(`文件上传处理完成,成功上传${imageUrls.length}/${filePaths.length}个文件`);
|
|
console.log(`上传详细结果:`, JSON.stringify(uploadResults, null, 2));
|
|
}
|
|
|
|
// 处理productData中的imageUrls,但需要避免重复添加
|
|
// 注意:我们只处理不在已上传文件URL中的图片
|
|
if (productData && productData.imageUrls && Array.isArray(productData.imageUrls)) {
|
|
console.log('处理productData中的imageUrls,避免重复');
|
|
|
|
// 创建已上传文件URL的集合,包含已经通过文件上传的URL
|
|
const uploadedFileUrls = new Set(imageUrls);
|
|
|
|
productData.imageUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
const trimmedUrl = url.trim();
|
|
// 只有当这个URL还没有被添加时才添加它
|
|
if (!uploadedFileUrls.has(trimmedUrl)) {
|
|
imageUrls.push(trimmedUrl);
|
|
uploadedFileUrls.add(trimmedUrl);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
console.log('最终收集到的图片URL数量:', imageUrls.length);
|
|
// 确保imageUrls是数组类型
|
|
if (!Array.isArray(imageUrls)) {
|
|
imageUrls = [];
|
|
console.log('警告: imageUrls不是数组,已重置为空数组');
|
|
}
|
|
} catch (uploadError) {
|
|
console.error('图片处理失败:', uploadError);
|
|
|
|
// 清理临时文件
|
|
cleanTempFiles(tempFilesToClean);
|
|
|
|
// 如果至少有一张图片上传成功,我们仍然可以继续创建商品
|
|
if (imageUrls.length > 0) {
|
|
console.log(`部分图片上传成功,共${imageUrls.length}张,继续创建商品`);
|
|
// 继续执行,不返回错误
|
|
} else {
|
|
// 如果所有图片都上传失败,才返回错误
|
|
return res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '图片上传失败',
|
|
error: uploadError.message
|
|
});
|
|
}
|
|
}
|
|
|
|
// 【关键修复】增强图片URL收集逻辑 - 从所有可能的来源收集图片URL
|
|
// 创建一个统一的URL集合,用于去重
|
|
const allImageUrlsSet = new Set(imageUrls);
|
|
|
|
// 【新增】检查是否是递归上传的一部分,并获取previousImageUrls
|
|
if (productData && productData.previousImageUrls && Array.isArray(productData.previousImageUrls)) {
|
|
console.log('【关键修复】检测到previousImageUrls,数量:', productData.previousImageUrls.length);
|
|
productData.previousImageUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
allImageUrlsSet.add(url.trim());
|
|
console.log('【关键修复】从previousImageUrls添加URL:', url.trim());
|
|
}
|
|
});
|
|
}
|
|
|
|
// 1. 处理additionalImageUrls
|
|
if (req.body.additionalImageUrls) {
|
|
try {
|
|
let additionalUrls = [];
|
|
if (typeof req.body.additionalImageUrls === 'string') {
|
|
// 尝试解析JSON字符串
|
|
try {
|
|
additionalUrls = JSON.parse(req.body.additionalImageUrls);
|
|
} catch (jsonError) {
|
|
// 如果解析失败,检查是否是单个URL字符串
|
|
if (req.body.additionalImageUrls.trim() !== '') {
|
|
additionalUrls = [req.body.additionalImageUrls.trim()];
|
|
}
|
|
}
|
|
} else {
|
|
additionalUrls = req.body.additionalImageUrls;
|
|
}
|
|
|
|
if (Array.isArray(additionalUrls) && additionalUrls.length > 0) {
|
|
console.log('【关键修复】添加额外的图片URL,数量:', additionalUrls.length);
|
|
// 添加到统一集合
|
|
additionalUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
allImageUrlsSet.add(url.trim());
|
|
}
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('处理additionalImageUrls时出错:', error);
|
|
}
|
|
}
|
|
|
|
// 2. 处理uploadedImageUrls
|
|
if (req.body.uploadedImageUrls) {
|
|
try {
|
|
let uploadedUrls = [];
|
|
if (typeof req.body.uploadedImageUrls === 'string') {
|
|
// 尝试解析JSON字符串
|
|
try {
|
|
uploadedUrls = JSON.parse(req.body.uploadedImageUrls);
|
|
} catch (jsonError) {
|
|
// 如果解析失败,检查是否是单个URL字符串
|
|
if (req.body.uploadedImageUrls.trim() !== '') {
|
|
uploadedUrls = [req.body.uploadedImageUrls.trim()];
|
|
}
|
|
}
|
|
} else {
|
|
uploadedUrls = req.body.uploadedImageUrls;
|
|
}
|
|
|
|
if (Array.isArray(uploadedUrls) && uploadedUrls.length > 0) {
|
|
console.log('【关键修复】检测到已上传的图片URLs,数量:', uploadedUrls.length);
|
|
// 添加到统一集合
|
|
uploadedUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
allImageUrlsSet.add(url.trim());
|
|
}
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('处理uploadedImageUrls时出错:', error);
|
|
}
|
|
}
|
|
|
|
// 3. 处理allImageUrls
|
|
if (req.body.allImageUrls) {
|
|
try {
|
|
let allUrls = [];
|
|
if (typeof req.body.allImageUrls === 'string') {
|
|
try {
|
|
allUrls = JSON.parse(req.body.allImageUrls);
|
|
} catch (jsonError) {
|
|
if (req.body.allImageUrls.trim() !== '') {
|
|
allUrls = [req.body.allImageUrls.trim()];
|
|
}
|
|
}
|
|
} else {
|
|
allUrls = req.body.allImageUrls;
|
|
}
|
|
|
|
if (Array.isArray(allUrls) && allUrls.length > 0) {
|
|
console.log('【关键修复】处理allImageUrls,数量:', allUrls.length);
|
|
allUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
allImageUrlsSet.add(url.trim());
|
|
}
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('处理allImageUrls时出错:', error);
|
|
}
|
|
}
|
|
|
|
// 4. 从productData中提取imageUrls
|
|
if (productData && (productData.imageUrls || productData.images || productData.allImageUrls)) {
|
|
const productImageUrls = [];
|
|
if (productData.imageUrls) {
|
|
if (Array.isArray(productData.imageUrls)) {
|
|
productImageUrls.push(...productData.imageUrls);
|
|
} else if (typeof productData.imageUrls === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(productData.imageUrls);
|
|
if (Array.isArray(parsed)) {
|
|
productImageUrls.push(...parsed);
|
|
} else {
|
|
productImageUrls.push(productData.imageUrls);
|
|
}
|
|
} catch (e) {
|
|
productImageUrls.push(productData.imageUrls);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (productData.images) {
|
|
if (Array.isArray(productData.images)) {
|
|
productImageUrls.push(...productData.images);
|
|
} else if (typeof productData.images === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(productData.images);
|
|
if (Array.isArray(parsed)) {
|
|
productImageUrls.push(...parsed);
|
|
} else {
|
|
productImageUrls.push(productData.images);
|
|
}
|
|
} catch (e) {
|
|
productImageUrls.push(productData.images);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (productData.allImageUrls) {
|
|
if (Array.isArray(productData.allImageUrls)) {
|
|
productImageUrls.push(...productData.allImageUrls);
|
|
} else if (typeof productData.allImageUrls === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(productData.allImageUrls);
|
|
if (Array.isArray(parsed)) {
|
|
productImageUrls.push(...parsed);
|
|
} else {
|
|
productImageUrls.push(productData.allImageUrls);
|
|
}
|
|
} catch (e) {
|
|
productImageUrls.push(productData.allImageUrls);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (productImageUrls.length > 0) {
|
|
console.log('【关键修复】从productData中提取图片URLs,数量:', productImageUrls.length);
|
|
productImageUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
allImageUrlsSet.add(url.trim());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// 增强处理:添加清理和标准化函数,移除反引号、多余空格、多余反斜杠和所有可能导致JSON解析错误的字符
|
|
function cleanAndStandardizeUrl(url) {
|
|
if (!url || typeof url !== 'string') return '';
|
|
// 1. 移除所有反斜杠(防止JSON解析错误)
|
|
// 2. 移除反引号和多余空格
|
|
// 3. 确保URL格式正确
|
|
return url
|
|
.replace(/\\/g, '') // 移除所有反斜杠
|
|
.replace(/[`\s]/g, '') // 移除反引号和空格
|
|
.trim(); // 清理前后空白
|
|
}
|
|
|
|
// 将图片URL添加到商品数据中
|
|
productData.imageUrls = Array.from(allImageUrlsSet)
|
|
.map(cleanAndStandardizeUrl) // 清理每个URL
|
|
.filter(url => url && url.trim() !== '');
|
|
|
|
console.log('【调试5】添加到商品数据前imageUrls长度:', productData.imageUrls.length);
|
|
console.log('【调试6】添加到商品数据前imageUrls数据:', JSON.stringify(productData.imageUrls));
|
|
console.log('【调试7】第二层去重后productData.imageUrls长度:', productData.imageUrls.length);
|
|
console.log('【调试8】第二层去重后productData.imageUrls数据:', JSON.stringify(productData.imageUrls));
|
|
console.log('【调试8.1】去重差异检测:', imageUrls.length - productData.imageUrls.length, '个重复URL被移除');
|
|
console.log('商品数据中最终的图片URL数量:', productData.imageUrls.length);
|
|
console.log('商品数据中最终的图片URL列表:', productData.imageUrls);
|
|
|
|
// 检查sellerId是否为openid,如果是则查找对应的userId
|
|
let actualSellerId = productData.sellerId;
|
|
|
|
// 【测试模式】如果是测试环境或者明确指定了测试sellerId,跳过验证
|
|
const isTestMode = productData.sellerId === 'test_seller_openid' || process.env.NODE_ENV === 'test';
|
|
|
|
if (isTestMode) {
|
|
console.log('测试模式:跳过sellerId验证');
|
|
actualSellerId = 'test_user_id'; // 使用测试userId
|
|
} else {
|
|
// 如果sellerId看起来像一个openid(包含特殊字符如'-'),则尝试查找对应的userId
|
|
if (productData.sellerId.includes('-')) {
|
|
console.log('sellerId看起来像openid,尝试查找对应的userId');
|
|
const user = await User.findOne({
|
|
where: {
|
|
openid: productData.sellerId
|
|
}
|
|
});
|
|
|
|
if (user && user.userId) {
|
|
console.log(`找到了对应的userId: ${user.userId}`);
|
|
actualSellerId = user.userId;
|
|
} else {
|
|
console.error(`未找到对应的用户记录,openid: ${productData.sellerId}`);
|
|
// 清理临时文件
|
|
cleanTempFiles(tempFilesToClean);
|
|
|
|
return res.status(401).json({
|
|
success: false,
|
|
code: 401,
|
|
message: '找不到对应的用户记录,请重新登录',
|
|
needRelogin: true // 添加重新登录标志,前端检测到这个标志时弹出登录窗口
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 【关键修复】增强递归上传支持 - 更可靠的会话ID和商品匹配
|
|
// 检查是否是递归上传的一部分,增加更多判断条件
|
|
const isSingleUpload = req.body.isSingleUpload === 'true';
|
|
const isRecursiveUpload = req.body.isRecursiveUpload === 'true';
|
|
const uploadIndex = parseInt(req.body.uploadIndex || req.body.currentImageIndex) || 0;
|
|
const totalImages = parseInt(req.body.totalImages || req.body.totalImageCount) || 1;
|
|
let hasMultipleImages = req.body.hasMultipleImages === 'true';
|
|
const totalImageCount = parseInt(req.body.totalImageCount || req.body.totalImages) || 1;
|
|
|
|
// 【关键修复】增加明确的多图片标记
|
|
const isMultiImageUpload = totalImageCount > 1 || req.body.hasMultipleImages === 'true';
|
|
|
|
console.log(`【递归上传信息】isSingleUpload=${isSingleUpload}, isRecursiveUpload=${isRecursiveUpload}`);
|
|
console.log(`【递归上传信息】uploadIndex=${uploadIndex}, totalImages=${totalImages}, totalImageCount=${totalImageCount}`);
|
|
console.log(`【递归上传信息】hasMultipleImages=${hasMultipleImages}, isMultiImageUpload=${isMultiImageUpload}`);
|
|
|
|
// 【重要修复】确保hasMultipleImages被正确识别
|
|
if (totalImageCount > 1) {
|
|
console.log('【重要】强制设置hasMultipleImages为true,因为总图片数量大于1');
|
|
hasMultipleImages = true;
|
|
}
|
|
|
|
// 【关键修复】增强的会话ID处理,优先使用前端预生成的会话ID
|
|
// 从多个来源获取会话ID,提高可靠性
|
|
let sessionId = req.body.sessionId || req.body.productId || req.body.uploadSessionId || productData.sessionId || productData.productId;
|
|
|
|
// 【重要修复】如果是多图上传,强制确保有会话ID
|
|
if (isMultiImageUpload && (!sessionId || !sessionId.startsWith('session_'))) {
|
|
// 生成新的会话ID
|
|
sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
console.log(`【会话ID修复】为多图上传生成新的会话ID: ${sessionId}`);
|
|
}
|
|
|
|
console.log(`【会话跟踪】最终使用的会话ID: ${sessionId || '无'}`);
|
|
|
|
// 如果是递归上传,尝试从数据库中查找匹配的临时ID或productId
|
|
let existingProduct = null;
|
|
let productId = null;
|
|
|
|
// 【关键修复】增强的商品查找逻辑,优先查找已存在的商品记录
|
|
if ((isSingleUpload || isRecursiveUpload || isMultiImageUpload) && sessionId) {
|
|
// 如果是递归上传且有会话ID,尝试查找已存在的商品
|
|
try {
|
|
console.log(`【商品查找】尝试查找已存在的商品记录,会话ID: ${sessionId}`);
|
|
// 【重要修复】同时匹配productId和sessionId字段
|
|
existingProduct = await Product.findOne({
|
|
where: {
|
|
[Op.or]: [
|
|
{ productId: sessionId },
|
|
{ sessionId: sessionId },
|
|
{ uploadSessionId: sessionId }
|
|
],
|
|
sellerId: actualSellerId
|
|
}
|
|
});
|
|
|
|
if (existingProduct) {
|
|
console.log(`【商品查找】找到已存在的商品记录,将更新而非创建新商品`);
|
|
productId = sessionId;
|
|
} else {
|
|
// 如果精确匹配失败,尝试查找该用户最近创建的商品(可能会话ID不匹配但需要关联)
|
|
console.log(`【商品查找】精确匹配失败,尝试查找该用户最近创建的商品`);
|
|
const recentProducts = await Product.findAll({
|
|
where: {
|
|
sellerId: actualSellerId,
|
|
// 查找最近5分钟内创建的商品
|
|
created_at: {
|
|
[Op.gt]: new Date(getBeijingTimeTimestamp() - 5 * 60 * 1000)
|
|
}
|
|
},
|
|
order: [['created_at', 'DESC']],
|
|
limit: 3
|
|
});
|
|
|
|
if (recentProducts && recentProducts.length > 0) {
|
|
// 优先选择最近创建的商品
|
|
existingProduct = recentProducts[0];
|
|
productId = existingProduct.productId;
|
|
console.log(`【商品查找】找到用户最近创建的商品,productId: ${productId}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(`【商品查找错误】查找已存在商品时出错:`, error);
|
|
}
|
|
}
|
|
|
|
// 如果没有找到已存在的商品或没有会话ID,生成新的商品ID
|
|
if (!productId) {
|
|
productId = `product_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
console.log(`生成新的商品ID: ${productId}`);
|
|
}
|
|
|
|
// 创建商品,使用实际的sellerId,并确保有默认状态
|
|
// 再次确认图片URLs没有重复(第三层去重)- 添加URL指纹比对
|
|
console.log('【调试9】创建商品前productData.imageUrls数据:', JSON.stringify(productData.imageUrls));
|
|
console.log('【调试10】创建商品前productData.imageUrls长度:', productData.imageUrls.length);
|
|
|
|
// 【关键修复】重写图片URL合并逻辑,确保递归上传时正确累积所有图片
|
|
// 使用已声明的allImageUrlsSet来存储所有图片URL,自动去重
|
|
|
|
// 【重要】优先处理现有商品中的图片(递归上传的累积基础)
|
|
if (existingProduct && existingProduct.imageUrls) {
|
|
try {
|
|
let existingUrls = [];
|
|
if (typeof existingProduct.imageUrls === 'string') {
|
|
existingUrls = JSON.parse(existingProduct.imageUrls);
|
|
} else if (Array.isArray(existingProduct.imageUrls)) {
|
|
existingUrls = existingProduct.imageUrls;
|
|
}
|
|
|
|
if (Array.isArray(existingUrls)) {
|
|
existingUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim()) {
|
|
allImageUrlsSet.add(cleanAndStandardizeUrl(url));
|
|
}
|
|
});
|
|
console.log('【关键修复】从现有商品记录获取已保存图片URLs,有效数量:', existingUrls.filter(Boolean).length);
|
|
}
|
|
} catch (e) {
|
|
console.error('解析现有商品图片URLs出错:', e);
|
|
}
|
|
}
|
|
|
|
console.log('【递归上传关键】现有商品图片URL数量:', allImageUrlsSet.size);
|
|
|
|
// 【关键修复】处理当前上传的图片文件
|
|
if (imageUrls && Array.isArray(imageUrls)) {
|
|
imageUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim()) {
|
|
allImageUrlsSet.add(cleanAndStandardizeUrl(url));
|
|
}
|
|
});
|
|
console.log('【关键修复】添加当前上传的图片URLs,数量:', imageUrls.length);
|
|
}
|
|
|
|
// 【关键修复】处理previousImageUrls(前端传递的已上传图片列表)
|
|
if (productData.previousImageUrls && Array.isArray(productData.previousImageUrls)) {
|
|
productData.previousImageUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim()) {
|
|
allImageUrlsSet.add(cleanAndStandardizeUrl(url));
|
|
}
|
|
});
|
|
console.log('【关键修复】处理previousImageUrls,数量:', productData.previousImageUrls.length);
|
|
}
|
|
|
|
// 【关键修复】处理additionalImageUrls
|
|
if (req.body.additionalImageUrls) {
|
|
try {
|
|
const additionalUrls = JSON.parse(req.body.additionalImageUrls);
|
|
if (Array.isArray(additionalUrls)) {
|
|
additionalUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim()) {
|
|
allImageUrlsSet.add(cleanAndStandardizeUrl(url));
|
|
}
|
|
});
|
|
console.log('【关键修复】处理additionalImageUrls,数量:', additionalUrls.length);
|
|
}
|
|
} catch (e) {
|
|
console.error('解析additionalImageUrls出错:', e);
|
|
}
|
|
}
|
|
|
|
// 【关键修复】处理uploadedImageUrls
|
|
if (req.body.uploadedImageUrls) {
|
|
try {
|
|
const uploadedUrls = JSON.parse(req.body.uploadedImageUrls);
|
|
if (Array.isArray(uploadedUrls)) {
|
|
uploadedUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim()) {
|
|
allImageUrlsSet.add(cleanAndStandardizeUrl(url));
|
|
}
|
|
});
|
|
console.log('【关键修复】处理uploadedImageUrls,数量:', uploadedUrls.length);
|
|
}
|
|
} catch (e) {
|
|
console.error('解析uploadedImageUrls出错:', e);
|
|
}
|
|
}
|
|
|
|
// 【关键修复】处理productData中的imageUrls
|
|
if (productData.imageUrls && Array.isArray(productData.imageUrls)) {
|
|
productData.imageUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim()) {
|
|
allImageUrlsSet.add(cleanAndStandardizeUrl(url));
|
|
}
|
|
});
|
|
console.log('【关键修复】处理productData.imageUrls,数量:', productData.imageUrls.length);
|
|
}
|
|
|
|
// 转换为数组
|
|
const combinedImageUrls = Array.from(allImageUrlsSet);
|
|
console.log('【关键修复】合并所有来源的图片URLs后,总数量:', combinedImageUrls.length);
|
|
console.log('【递归上传关键】当前累积的图片URLs:', JSON.stringify(combinedImageUrls));
|
|
|
|
// 【关键修复】由于已经使用Set去重,这里简化处理,只做清理和过滤
|
|
const finalImageUrls = combinedImageUrls
|
|
.filter(url => url && typeof url === 'string' && url.trim() !== ''); // 过滤空值
|
|
|
|
// 【关键修复】确保至少保存第一张图片,即使检测到重复
|
|
if (finalImageUrls.length === 0 && combinedImageUrls.length > 0) {
|
|
console.log('【重要警告】所有URL都被标记为重复,但至少保留第一张图片');
|
|
const firstValidUrl = combinedImageUrls
|
|
.map(cleanAndStandardizeUrl)
|
|
.find(url => url && url.trim() !== '');
|
|
if (firstValidUrl) {
|
|
finalImageUrls.push(firstValidUrl);
|
|
console.log('已保留第一张有效图片URL:', firstValidUrl);
|
|
}
|
|
}
|
|
console.log('【调试11】第三层去重后finalImageUrls长度:', finalImageUrls.length);
|
|
console.log('【调试12】第三层去重后finalImageUrls列表:', JSON.stringify(finalImageUrls));
|
|
console.log('【调试12.1】去重差异检测:', productData.imageUrls.length - finalImageUrls.length, '个重复URL被移除');
|
|
console.log('创建商品前最终去重后的图片URL数量:', finalImageUrls.length);
|
|
|
|
// 【关键修复】添加调试信息,确保finalImageUrls是正确的数组
|
|
console.log('【关键调试】finalImageUrls类型:', typeof finalImageUrls);
|
|
console.log('【关键调试】finalImageUrls是否为数组:', Array.isArray(finalImageUrls));
|
|
console.log('【关键调试】finalImageUrls长度:', finalImageUrls.length);
|
|
console.log('【关键调试】finalImageUrls内容:', JSON.stringify(finalImageUrls));
|
|
|
|
// 确保imageUrls在存储前正确序列化为JSON字符串
|
|
console.log('【关键修复】将imageUrls数组序列化为JSON字符串存储到数据库');
|
|
console.log('【递归上传关键】最终要存储的图片URL数量:', finalImageUrls.length);
|
|
console.log('【递归上传关键】最终要存储的图片URL列表:', JSON.stringify(finalImageUrls));
|
|
|
|
// 【重要修复】支持更新已存在的商品
|
|
let productToSave;
|
|
let isUpdate = false;
|
|
|
|
// ========== 【新增】创建商品前的地区字段详细调试 ==========
|
|
console.log('【地区字段调试】创建商品前 - 检查地区字段状态');
|
|
console.log('【地区字段调试】productData.region:', productData.region, '类型:', typeof productData.region);
|
|
console.log('【地区字段调试】existingProduct:', existingProduct ? '存在' : '不存在');
|
|
if (existingProduct) {
|
|
console.log('【地区字段调试】existingProduct.region:', existingProduct.region, '类型:', typeof existingProduct.region);
|
|
}
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
if (existingProduct) {
|
|
// 更新已存在的商品
|
|
isUpdate = true;
|
|
// 合并现有图片URL和新的图片URL
|
|
let existingImageUrls = [];
|
|
try {
|
|
if (existingProduct.imageUrls) {
|
|
const imageUrlsValue = existingProduct.imageUrls;
|
|
// 关键修复:防御性检查,避免解析 undefined
|
|
if (imageUrlsValue && imageUrlsValue !== 'undefined' && imageUrlsValue !== undefined) {
|
|
if (typeof imageUrlsValue === 'string') {
|
|
existingImageUrls = JSON.parse(imageUrlsValue);
|
|
} else if (Array.isArray(imageUrlsValue)) {
|
|
existingImageUrls = imageUrlsValue;
|
|
}
|
|
|
|
// 确保是数组
|
|
if (!Array.isArray(existingImageUrls)) {
|
|
existingImageUrls = existingImageUrls ? [existingImageUrls] : [];
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('解析现有商品图片URL失败:', e);
|
|
existingImageUrls = [];
|
|
}
|
|
|
|
// 额外的安全检查
|
|
if (!Array.isArray(existingImageUrls)) {
|
|
console.warn('existingImageUrls 不是数组,重置为空数组');
|
|
existingImageUrls = [];
|
|
}
|
|
// 【关键修复】增强的图片URL合并逻辑,确保保留所有图片
|
|
// 先创建一个Set包含所有来源的图片URL
|
|
const allUrlsSet = new Set();
|
|
|
|
// 添加现有商品的图片URL
|
|
existingImageUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
allUrlsSet.add(url.trim());
|
|
}
|
|
});
|
|
|
|
// 添加当前上传的图片URL
|
|
finalImageUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
allUrlsSet.add(url.trim());
|
|
}
|
|
});
|
|
|
|
// 【重要】尝试从请求参数中获取更多图片URL
|
|
const additionalUrlsSources = [
|
|
req.body.additionalImageUrls,
|
|
req.body.uploadedImageUrls,
|
|
req.body.allImageUrls,
|
|
productData.previousImageUrls,
|
|
productData.imageUrls,
|
|
productData.allImageUrls
|
|
];
|
|
|
|
additionalUrlsSources.forEach(source => {
|
|
if (source) {
|
|
try {
|
|
let urls = [];
|
|
if (typeof source === 'string') {
|
|
try {
|
|
urls = JSON.parse(source);
|
|
} catch (e) {
|
|
if (source.trim() !== '') {
|
|
urls = [source.trim()];
|
|
}
|
|
}
|
|
} else if (Array.isArray(source)) {
|
|
urls = source;
|
|
}
|
|
|
|
if (Array.isArray(urls)) {
|
|
urls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
allUrlsSet.add(url.trim());
|
|
}
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error('处理额外URL源时出错:', e);
|
|
}
|
|
}
|
|
});
|
|
|
|
const mergedImageUrls = Array.from(allUrlsSet);
|
|
console.log(`【会话合并】合并现有图片(${existingImageUrls.length})和新图片(${finalImageUrls.length}),总数: ${mergedImageUrls.length}`);
|
|
console.log(`【会话合并】合并后的图片列表:`, JSON.stringify(mergedImageUrls));
|
|
console.log(`【会话合并】会话ID: ${sessionId}`);
|
|
|
|
// ========== 【新增】更新商品时的地区字段调试 ==========
|
|
console.log('【地区字段调试】更新商品 - 准备合并地区字段');
|
|
console.log('【地区字段调试】productData.region:', productData.region);
|
|
console.log('【地区字段调试】existingProduct.region:', existingProduct.region);
|
|
const finalRegion = productData.region || existingProduct.region || '';
|
|
console.log('【地区字段调试】最终确定的地区字段:', finalRegion);
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
productToSave = {
|
|
...existingProduct.dataValues,
|
|
...productData,
|
|
imageUrls: JSON.stringify(mergedImageUrls),
|
|
allImageUrls: JSON.stringify(mergedImageUrls), // 额外保存一份,增强兼容性
|
|
sellerId: actualSellerId,
|
|
status: productData.status || existingProduct.status || 'pending_review',
|
|
region: finalRegion, // 使用调试确定的地区字段
|
|
updated_at: getBeijingTime(),
|
|
// 【重要修复】确保保存会话ID
|
|
sessionId: sessionId || existingProduct.dataValues.sessionId,
|
|
uploadSessionId: sessionId || existingProduct.dataValues.uploadSessionId,
|
|
// 标记多图片
|
|
hasMultipleImages: mergedImageUrls.length > 1,
|
|
totalImages: mergedImageUrls.length,
|
|
// 确保保留原始的创建时间
|
|
created_at: existingProduct.dataValues.created_at || getBeijingTime()
|
|
};
|
|
|
|
// ========== 【新增】保存前的地区字段验证 ==========
|
|
console.log('【地区字段调试】保存前验证 - productToSave.region:', productToSave.region, '类型:', typeof productToSave.region);
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
console.log('【关键调试】即将更新的商品数据:', {
|
|
...productToSave,
|
|
imageUrls: 'JSON字符串长度: ' + productToSave.imageUrls.length,
|
|
sellerId: '已处理(隐藏具体ID)',
|
|
region: productToSave.region // 特别显示地区字段
|
|
});
|
|
} else {
|
|
// 创建新商品
|
|
// ========== 【新增】创建新商品时的地区字段调试 ==========
|
|
console.log('【地区字段调试】创建新商品 - 准备设置地区字段');
|
|
console.log('【地区字段调试】productData.region:', productData.region);
|
|
const finalRegion = productData.region || '';
|
|
console.log('【地区字段调试】最终确定的地区字段:', finalRegion);
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
productToSave = {
|
|
...productData,
|
|
imageUrls: JSON.stringify(finalImageUrls), // 关键修复:序列化为JSON字符串
|
|
allImageUrls: JSON.stringify(finalImageUrls), // 额外保存一份,增强兼容性
|
|
sellerId: actualSellerId, // 使用查找到的userId或原始openid
|
|
status: productData.status || 'pending_review', // 确保有默认状态为pending_review
|
|
region: finalRegion, // 使用调试确定的地区字段
|
|
productId,
|
|
// 【重要修复】确保保存会话ID
|
|
sessionId: sessionId,
|
|
uploadSessionId: sessionId,
|
|
// 标记多图片
|
|
hasMultipleImages: finalImageUrls.length > 1,
|
|
totalImages: finalImageUrls.length,
|
|
created_at: getBeijingTime(),
|
|
updated_at: getBeijingTime()
|
|
};
|
|
|
|
// ========== 【新增】保存前的地区字段验证 ==========
|
|
console.log('【地区字段调试】保存前验证 - productToSave.region:', productToSave.region, '类型:', typeof productToSave.region);
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
// 记录要创建的商品数据(过滤敏感信息)
|
|
console.log('【关键调试】即将创建的商品数据:', {
|
|
...productToSave,
|
|
imageUrls: 'JSON字符串长度: ' + productToSave.imageUrls.length,
|
|
sellerId: '已处理(隐藏具体ID)',
|
|
region: productToSave.region // 特别显示地区字段
|
|
});
|
|
}
|
|
|
|
// 根据是否是更新操作执行不同的数据库操作
|
|
if (isUpdate) {
|
|
console.log(`【会话更新】执行商品更新,productId: ${productId}`);
|
|
// 确保imageUrls是正确的JSON字符串
|
|
if (!productToSave.imageUrls || typeof productToSave.imageUrls !== 'string') {
|
|
console.error('【严重错误】imageUrls不是字符串格式,重新序列化');
|
|
productToSave.imageUrls = JSON.stringify(finalImageUrls);
|
|
}
|
|
|
|
// ========== 【新增】数据库操作前的地区字段最终检查 ==========
|
|
console.log('【地区字段调试】数据库更新前最终检查 - region:', productToSave.region);
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
product = await Product.update(productToSave, {
|
|
where: {
|
|
productId: productId
|
|
}
|
|
});
|
|
// 查询更新后的商品完整信息
|
|
product = await Product.findOne({
|
|
where: {
|
|
productId: productId
|
|
}
|
|
});
|
|
console.log(`【会话更新】商品更新成功,productId: ${productId}`);
|
|
} else {
|
|
console.log(`【会话创建】执行商品创建,productId: ${productId}`);
|
|
// 确保imageUrls是正确的JSON字符串
|
|
if (!productToSave.imageUrls || typeof productToSave.imageUrls !== 'string') {
|
|
console.error('【严重错误】imageUrls不是字符串格式,重新序列化');
|
|
productToSave.imageUrls = JSON.stringify(finalImageUrls);
|
|
}
|
|
|
|
// ========== 【新增】数据库创建前的地区字段最终检查 ==========
|
|
console.log('【地区字段调试】数据库创建前最终检查 - region:', productToSave.region);
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
product = await Product.create(productToSave);
|
|
console.log(`【会话创建】商品创建成功,productId: ${productId}`);
|
|
}
|
|
|
|
// ========== 【新增】数据库操作后的地区字段验证 ==========
|
|
console.log('【地区字段调试】数据库操作后验证');
|
|
if (product) {
|
|
console.log('【地区字段调试】从数据库返回的商品地区字段:', product.region, '类型:', typeof product.region);
|
|
} else {
|
|
console.error('【地区字段调试】数据库操作后商品对象为空');
|
|
}
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
console.log('【成功】商品操作完成,productId:', product.productId);
|
|
|
|
// 【关键修复】确保返回给前端的响应包含完整的图片URL列表
|
|
// 从数据库中解析出图片URLs数组
|
|
// 使用let来允许重新赋值
|
|
let dbResponseImageUrls = [];
|
|
try {
|
|
// 【重要修复】首先尝试从数据库中获取最新的完整图片列表
|
|
if (product.imageUrls) {
|
|
if (typeof product.imageUrls === 'string') {
|
|
dbResponseImageUrls = JSON.parse(product.imageUrls);
|
|
if (!Array.isArray(dbResponseImageUrls)) {
|
|
dbResponseImageUrls = [dbResponseImageUrls];
|
|
}
|
|
} else if (Array.isArray(product.imageUrls)) {
|
|
dbResponseImageUrls = product.imageUrls;
|
|
}
|
|
|
|
console.log('【数据库读取】从数据库读取的图片URLs数量:', dbResponseImageUrls.length);
|
|
}
|
|
|
|
// 如果数据库中没有或者为空,使用我们收集的finalImageUrls
|
|
if (!dbResponseImageUrls || dbResponseImageUrls.length === 0) {
|
|
dbResponseImageUrls = finalImageUrls;
|
|
console.log('【备用方案】使用收集的finalImageUrls,数量:', dbResponseImageUrls.length);
|
|
}
|
|
|
|
// 【重要修复】确保去重和清理
|
|
const urlSet = new Set();
|
|
dbResponseImageUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
urlSet.add(url.trim());
|
|
}
|
|
});
|
|
dbResponseImageUrls = Array.from(urlSet);
|
|
|
|
// 确保数组格式正确
|
|
if (!Array.isArray(dbResponseImageUrls)) {
|
|
dbResponseImageUrls = [dbResponseImageUrls];
|
|
}
|
|
|
|
console.log('【最终响应】去重后返回给前端的图片URLs数量:', dbResponseImageUrls.length);
|
|
} catch (e) {
|
|
console.error('解析响应图片URLs出错:', e);
|
|
// 如果解析失败,使用我们收集的finalImageUrls
|
|
dbResponseImageUrls = finalImageUrls;
|
|
}
|
|
|
|
console.log('【关键修复】返回给前端的图片URL数量:', dbResponseImageUrls.length);
|
|
console.log('【关键修复】返回给前端的图片URL列表:', JSON.stringify(dbResponseImageUrls));
|
|
console.log('【递归上传关键】响应中包含的累积图片数量:', dbResponseImageUrls.length);
|
|
|
|
// 继续执行后续代码,确保返回完整的商品信息和图片URLs
|
|
|
|
// 【关键修复】确保返回完整的响应数据,包含所有图片URLs
|
|
// 准备响应数据,包含完整的图片URL列表
|
|
const customResponseData = {
|
|
success: true,
|
|
message: isUpdate ? '商品更新成功' : '商品创建成功',
|
|
code: 200,
|
|
// 【关键修复】直接返回完整的图片URL数组在多个位置,确保前端能正确获取
|
|
imageUrls: dbResponseImageUrls, // 顶层直接返回
|
|
allImageUrls: dbResponseImageUrls, // 增强兼容性
|
|
imageUrl: dbResponseImageUrls[0] || null, // 保持向后兼容
|
|
data: {
|
|
product: {
|
|
...product.toJSON(),
|
|
imageUrls: dbResponseImageUrls, // 确保在product对象中也包含完整URL列表
|
|
allImageUrls: dbResponseImageUrls,
|
|
imageUrl: dbResponseImageUrls[0] || null
|
|
},
|
|
imageUrls: dbResponseImageUrls, // 在data对象中也包含一份
|
|
allImageUrls: dbResponseImageUrls
|
|
},
|
|
product: {
|
|
...product.toJSON(),
|
|
imageUrls: dbResponseImageUrls, // 在顶层product对象中也包含
|
|
allImageUrls: dbResponseImageUrls,
|
|
imageUrl: dbResponseImageUrls[0] || null
|
|
},
|
|
// 【关键修复】添加会话和上传状态信息
|
|
uploadInfo: {
|
|
sessionId: sessionId,
|
|
productId: productId,
|
|
isMultiImageUpload: isMultiImageUpload,
|
|
isRecursiveUpload: isRecursiveUpload,
|
|
isFinalUpload: req.body.isFinalUpload === 'true',
|
|
currentIndex: uploadIndex,
|
|
totalImages: totalImages,
|
|
uploadedCount: dbResponseImageUrls.length,
|
|
hasMultipleImages: dbResponseImageUrls.length > 1
|
|
},
|
|
sessionId: productId, // 返回会话ID
|
|
productId: productId,
|
|
isRecursiveUpload: isRecursiveUpload,
|
|
uploadIndex: uploadIndex,
|
|
totalImages: totalImages,
|
|
hasMultipleImages: hasMultipleImages,
|
|
// 添加完整的调试信息
|
|
debugInfo: {
|
|
imageUrlsCount: dbResponseImageUrls.length,
|
|
isUpdate: isUpdate,
|
|
sessionInfo: {
|
|
sessionId: sessionId,
|
|
productId: productId
|
|
}
|
|
}
|
|
};
|
|
|
|
console.log('【响应准备】最终返回的响应数据结构:', {
|
|
success: customResponseData.success,
|
|
imageUrlsCount: customResponseData.imageUrls.length,
|
|
dataImageUrlsCount: customResponseData.data.imageUrls.length,
|
|
productImageUrlsCount: customResponseData.product.imageUrls.length
|
|
});
|
|
|
|
// 查询完整商品信息以确保返回正确的毛重值和图片URLs
|
|
product = await Product.findOne({
|
|
where: { productId },
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'seller',
|
|
attributes: ['userId', 'name', 'avatarUrl']
|
|
}
|
|
]
|
|
});
|
|
|
|
// 【关键修复】在发送响应前,再次确认数据库中的图片URLs是否正确存储
|
|
if (product && product.imageUrls) {
|
|
let dbImageUrls;
|
|
try {
|
|
dbImageUrls = typeof product.imageUrls === 'string' ? JSON.parse(product.imageUrls) : product.imageUrls;
|
|
if (Array.isArray(dbImageUrls)) {
|
|
console.log(`【数据库验证】从数据库读取的图片URLs数量: ${dbImageUrls.length}`);
|
|
// 使用数据库中的最新URL列表更新响应
|
|
dbResponseImageUrls = dbImageUrls;
|
|
}
|
|
} catch (e) {
|
|
console.error('【数据库验证】解析数据库中的imageUrls失败:', e);
|
|
}
|
|
}
|
|
|
|
// 【增强的最终检查】确保返回给前端的图片URLs包含所有上传的图片
|
|
console.log('【最终检查】开始最终图片URLs检查');
|
|
|
|
// 【关键修复】确保URL列表格式正确且去重
|
|
let responseImageUrls = [];
|
|
try {
|
|
// 优先使用数据库响应的URL列表
|
|
responseImageUrls = dbResponseImageUrls;
|
|
if (!Array.isArray(responseImageUrls)) {
|
|
responseImageUrls = [responseImageUrls];
|
|
}
|
|
|
|
// 最后一次去重和清理
|
|
const finalUrlSet = new Set();
|
|
responseImageUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
finalUrlSet.add(url.trim());
|
|
}
|
|
});
|
|
responseImageUrls = Array.from(finalUrlSet);
|
|
|
|
console.log(`【最终响应】将返回给前端的图片URLs数量: ${responseImageUrls.length}`);
|
|
} catch (e) {
|
|
console.error('【最终响应】处理imageUrls失败:', e);
|
|
responseImageUrls = [];
|
|
}
|
|
|
|
// 更新customResponseData中的图片URL列表为最新的去重结果
|
|
customResponseData.imageUrls = responseImageUrls;
|
|
customResponseData.allImageUrls = responseImageUrls;
|
|
customResponseData.imageUrl = responseImageUrls[0] || null;
|
|
|
|
if (customResponseData.data) {
|
|
customResponseData.data.imageUrls = responseImageUrls;
|
|
customResponseData.data.allImageUrls = responseImageUrls;
|
|
if (customResponseData.data.product) {
|
|
customResponseData.data.product.imageUrls = responseImageUrls;
|
|
customResponseData.data.product.allImageUrls = responseImageUrls;
|
|
customResponseData.data.product.imageUrl = responseImageUrls[0] || null;
|
|
}
|
|
}
|
|
|
|
if (customResponseData.product) {
|
|
customResponseData.product.imageUrls = responseImageUrls;
|
|
customResponseData.product.allImageUrls = responseImageUrls;
|
|
customResponseData.product.imageUrl = responseImageUrls[0] || null;
|
|
}
|
|
|
|
customResponseData.uploadInfo.uploadedCount = responseImageUrls.length;
|
|
customResponseData.uploadInfo.hasMultipleImages = responseImageUrls.length > 1;
|
|
customResponseData.debugInfo.imageUrlsCount = responseImageUrls.length;
|
|
|
|
// ========== 【新增】响应前的地区字段最终验证 ==========
|
|
console.log('【地区字段调试】发送响应前最终验证');
|
|
if (customResponseData.product) {
|
|
console.log('【地区字段调试】响应中product.region:', customResponseData.product.region);
|
|
}
|
|
if (customResponseData.data && customResponseData.data.product) {
|
|
console.log('【地区字段调试】响应中data.product.region:', customResponseData.data.product.region);
|
|
}
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
console.log('【响应准备】最终返回的响应数据结构:', {
|
|
success: customResponseData.success,
|
|
imageUrlsCount: customResponseData.imageUrls.length,
|
|
dataImageUrlsCount: customResponseData.data?.imageUrls?.length,
|
|
productImageUrlsCount: customResponseData.product?.imageUrls?.length
|
|
});
|
|
|
|
// 更新用户类型:如果字段为空则填入seller,如果为buyer则修改为both
|
|
// 只在创建新商品时执行,不更新商品时执行
|
|
if (!isUpdate) {
|
|
try {
|
|
// 获取当前用户信息
|
|
const currentUser = await User.findOne({ where: { userId: actualSellerId } });
|
|
if (currentUser) {
|
|
console.log('更新用户类型前 - 当前用户类型:', currentUser.type, '用户ID:', currentUser.userId);
|
|
|
|
// 检查用户类型并根据需求更新
|
|
if ((currentUser.type === null || currentUser.type === undefined || currentUser.type === '') || currentUser.type === 'buyer') {
|
|
let newType = '';
|
|
if (currentUser.type === 'buyer') {
|
|
newType = 'both';
|
|
} else {
|
|
newType = 'seller';
|
|
}
|
|
|
|
// 更新用户类型
|
|
await User.update(
|
|
{ type: newType },
|
|
{ where: { userId: actualSellerId } }
|
|
);
|
|
console.log('用户类型更新成功 - 用户ID:', currentUser.userId, '旧类型:', currentUser.type, '新类型:', newType);
|
|
} else {
|
|
console.log('不需要更新用户类型 - 用户ID:', currentUser.userId, '当前类型:', currentUser.type);
|
|
}
|
|
}
|
|
} catch (updateError) {
|
|
console.error('更新用户类型失败:', updateError);
|
|
// 不影响商品发布结果,仅记录错误
|
|
}
|
|
}
|
|
|
|
// 发送最终增强的响应
|
|
res.status(200).json(customResponseData);
|
|
} catch (err) {
|
|
console.error('【错误】在添加商品时出错:', err);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '上传失败,请稍后重试',
|
|
code: 500,
|
|
error: err.message
|
|
});
|
|
} finally {
|
|
// 确保临时文件被清理
|
|
if (tempFilesToClean.length > 0) {
|
|
try {
|
|
cleanTempFiles(tempFilesToClean);
|
|
} catch (cleanupError) {
|
|
console.warn('清理临时文件时出错:', cleanupError);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 【关键修复】在 handleAddImagesToExistingProduct 函数中加强图片合并逻辑
|
|
async function handleAddImagesToExistingProduct(req, res, existingProductId, uploadedFiles) {
|
|
let transaction;
|
|
let tempFilesToClean = [];
|
|
try {
|
|
console.log('【图片更新模式】开始处理图片上传到已存在商品,商品ID:', existingProductId);
|
|
|
|
// 收集需要清理的临时文件路径
|
|
for (const file of uploadedFiles) {
|
|
tempFilesToClean.push(file.path);
|
|
}
|
|
|
|
// 使用事务确保数据一致性
|
|
transaction = await sequelize.transaction();
|
|
|
|
// 查找现有商品并锁定行,防止并发问题
|
|
const existingProduct = await Product.findOne({
|
|
where: { productId: existingProductId },
|
|
lock: transaction.LOCK.UPDATE,
|
|
transaction
|
|
});
|
|
|
|
if (!existingProduct) {
|
|
await transaction.rollback();
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: '商品不存在',
|
|
code: 404
|
|
});
|
|
}
|
|
|
|
console.log('【图片更新模式】找到现有商品:', existingProduct.productName);
|
|
|
|
// 【关键修复】重新解析现有图片URL,确保正确获取所有图片
|
|
let existingImageUrls = [];
|
|
try {
|
|
if (existingProduct.imageUrls) {
|
|
const imageUrlsData = existingProduct.imageUrls;
|
|
console.log('【图片解析】原始imageUrls数据:', imageUrlsData, '类型:', typeof imageUrlsData);
|
|
|
|
if (typeof imageUrlsData === 'string') {
|
|
existingImageUrls = JSON.parse(imageUrlsData);
|
|
console.log('【图片解析】解析后的数组:', existingImageUrls, '长度:', existingImageUrls.length);
|
|
} else if (Array.isArray(imageUrlsData)) {
|
|
existingImageUrls = imageUrlsData;
|
|
}
|
|
|
|
// 确保是数组
|
|
if (!Array.isArray(existingImageUrls)) {
|
|
console.warn('【图片解析】existingImageUrls不是数组,重置为空数组');
|
|
existingImageUrls = [];
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('【图片解析】解析现有商品图片URL失败:', e);
|
|
existingImageUrls = [];
|
|
}
|
|
|
|
console.log('【图片合并】现有图片URL数量:', existingImageUrls.length);
|
|
|
|
// 处理新图片上传
|
|
let newImageUrls = [];
|
|
if (uploadedFiles.length > 0) {
|
|
console.log('开始上传图片到已存在商品,数量:', uploadedFiles.length);
|
|
|
|
const safeProductName = (existingProduct.productName || 'product')
|
|
.replace(/[\/:*?"<>|]/g, '_')
|
|
.substring(0, 50);
|
|
|
|
const folderPath = `products/${safeProductName}`;
|
|
|
|
// 【关键修复】批量上传所有图片
|
|
const uploadPromises = uploadedFiles.map(async (file, index) => {
|
|
try {
|
|
console.log(`上传第${index + 1}/${uploadedFiles.length}张图片`);
|
|
|
|
// 使用 OssUploader 上传图片
|
|
const uploadedUrl = await OssUploader.uploadFile(file.path, folderPath);
|
|
|
|
if (uploadedUrl) {
|
|
console.log(`图片 ${index + 1} 上传成功:`, uploadedUrl);
|
|
|
|
// 上传成功后删除临时文件
|
|
try {
|
|
await fs.promises.unlink(file.path);
|
|
console.log(`已删除临时文件: ${file.path}`);
|
|
} catch (deleteError) {
|
|
console.warn(`删除临时文件失败: ${file.path}`, deleteError);
|
|
}
|
|
|
|
return uploadedUrl;
|
|
}
|
|
} catch (uploadError) {
|
|
console.error(`图片 ${index + 1} 上传失败:`, uploadError);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
const uploadResults = await Promise.all(uploadPromises);
|
|
newImageUrls = uploadResults.filter(url => url !== null);
|
|
console.log(`成功上传 ${newImageUrls.length}/${uploadedFiles.length} 张新图片`);
|
|
}
|
|
|
|
// 【关键修复】合并图片URL(去重)
|
|
const allUrlsSet = new Set();
|
|
|
|
// 添加现有图片URL
|
|
existingImageUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
allUrlsSet.add(url.trim());
|
|
console.log('【图片合并】添加现有URL:', url.trim());
|
|
}
|
|
});
|
|
|
|
// 添加新上传的图片URL
|
|
newImageUrls.forEach(url => {
|
|
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
allUrlsSet.add(url.trim());
|
|
console.log('【图片合并】添加新URL:', url.trim());
|
|
}
|
|
});
|
|
|
|
const mergedImageUrls = Array.from(allUrlsSet);
|
|
console.log('【图片更新】最终合并后图片URL数量:', mergedImageUrls.length);
|
|
console.log('【图片更新】合并后的图片URL列表:', mergedImageUrls);
|
|
|
|
// 【关键修复】验证JSON序列化结果
|
|
const imageUrlsJson = JSON.stringify(mergedImageUrls);
|
|
console.log('【JSON验证】序列化后的JSON字符串:', imageUrlsJson);
|
|
console.log('【JSON验证】JSON字符串长度:', imageUrlsJson.length);
|
|
|
|
// 更新商品图片
|
|
await Product.update({
|
|
imageUrls: imageUrlsJson,
|
|
allImageUrls: imageUrlsJson,
|
|
updated_at: getBeijingTime(),
|
|
hasMultipleImages: mergedImageUrls.length > 1,
|
|
totalImages: mergedImageUrls.length
|
|
}, {
|
|
where: { productId: existingProductId },
|
|
transaction
|
|
});
|
|
|
|
// 提交事务
|
|
await transaction.commit();
|
|
|
|
// 返回成功响应
|
|
return res.status(200).json({
|
|
success: true,
|
|
message: `图片上传成功,共${newImageUrls.length}张新图片,总计${mergedImageUrls.length}张图片`,
|
|
code: 200,
|
|
imageUrls: mergedImageUrls,
|
|
allImageUrls: mergedImageUrls,
|
|
productId: existingProductId,
|
|
uploadedCount: newImageUrls.length,
|
|
totalCount: mergedImageUrls.length
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('【图片更新模式】处理图片上传时出错:', error);
|
|
if (transaction) {
|
|
await transaction.rollback();
|
|
}
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: '图片上传失败',
|
|
code: 500,
|
|
error: error.message
|
|
});
|
|
} finally {
|
|
// 确保临时文件被清理
|
|
if (tempFilesToClean.length > 0) {
|
|
try {
|
|
cleanTempFiles(tempFilesToClean);
|
|
} catch (cleanupError) {
|
|
console.warn('清理临时文件时出错:', cleanupError);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 其他路由...
|
|
|
|
// 收藏相关API端点
|
|
// 添加收藏
|
|
app.post('/api/favorites/add', async (req, res) => {
|
|
try {
|
|
const { user_phone, productId } = req.body;
|
|
|
|
console.log('收到添加收藏请求:', { user_phone, productId });
|
|
|
|
// 验证参数
|
|
if (!user_phone || !productId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要参数',
|
|
data: { user_phone, productId }
|
|
});
|
|
}
|
|
|
|
// 检查是否已存在收藏记录
|
|
const existingFavorite = await Favorite.findOne({
|
|
where: {
|
|
user_phone,
|
|
productId
|
|
}
|
|
});
|
|
|
|
if (existingFavorite) {
|
|
return res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '已经收藏过该商品',
|
|
data: existingFavorite
|
|
});
|
|
}
|
|
|
|
// 创建新的收藏记录
|
|
const newFavorite = await Favorite.create({
|
|
user_phone,
|
|
productId,
|
|
date: req.body.date || getBeijingTime() // 使用前端传递的时间或当前UTC+8时间
|
|
});
|
|
|
|
console.log('收藏添加成功:', newFavorite);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '收藏添加成功',
|
|
data: newFavorite
|
|
});
|
|
} catch (error) {
|
|
console.error('添加收藏失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '添加收藏失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 取消收藏
|
|
app.post('/api/favorites/remove', async (req, res) => {
|
|
try {
|
|
const { user_phone, productId } = req.body;
|
|
|
|
console.log('收到取消收藏请求:', { user_phone, productId });
|
|
|
|
// 验证参数
|
|
if (!user_phone || !productId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要参数'
|
|
});
|
|
}
|
|
|
|
// 删除收藏记录
|
|
const result = await Favorite.destroy({
|
|
where: {
|
|
user_phone,
|
|
productId
|
|
}
|
|
});
|
|
|
|
if (result === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '收藏记录不存在'
|
|
});
|
|
}
|
|
|
|
console.log('收藏取消成功:', { user_phone, productId });
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '收藏取消成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('取消收藏失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '取消收藏失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 获取收藏列表
|
|
app.post('/api/favorites/list', async (req, res) => {
|
|
try {
|
|
const { user_phone } = req.body;
|
|
|
|
console.log('收到获取收藏列表请求:', { user_phone });
|
|
|
|
// 验证参数
|
|
if (!user_phone) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要参数'
|
|
});
|
|
}
|
|
|
|
// 获取收藏列表,并关联查询商品信息
|
|
const favorites = await Favorite.findAll({
|
|
where: { user_phone },
|
|
include: [
|
|
{
|
|
model: Product,
|
|
as: 'Product', // 与关联定义中的别名一致
|
|
attributes: ['productId', 'productName', 'price', 'quantity', 'grossWeight', 'imageUrls', 'created_at', 'specification', 'yolk', 'sourceType', 'supplyStatus'],
|
|
required: false // 使用LEFT JOIN,即使商品不存在也会返回收藏记录
|
|
}
|
|
],
|
|
order: [['date', 'DESC']]
|
|
});
|
|
|
|
console.log('获取收藏列表成功,数量:', favorites.length);
|
|
|
|
// 处理图片URL,确保是数组格式(与商品详情API保持一致的处理逻辑)
|
|
const processedFavorites = favorites.map(favorite => {
|
|
const favoriteJSON = favorite.toJSON();
|
|
if (favoriteJSON.Product) {
|
|
// 关键修复:将存储在数据库中的JSON字符串反序列化为JavaScript数组
|
|
if (favoriteJSON.Product.imageUrls) {
|
|
if (typeof favoriteJSON.Product.imageUrls === 'string') {
|
|
console.log('【关键修复】将数据库中的JSON字符串反序列化为JavaScript数组');
|
|
try {
|
|
favoriteJSON.Product.imageUrls = JSON.parse(favoriteJSON.Product.imageUrls);
|
|
console.log('反序列化后的imageUrls类型:', typeof favoriteJSON.Product.imageUrls);
|
|
} catch (parseError) {
|
|
console.error('反序列化imageUrls失败:', parseError);
|
|
// 如果解析失败,使用空数组确保前端不会崩溃
|
|
favoriteJSON.Product.imageUrls = [];
|
|
}
|
|
} else if (!Array.isArray(favoriteJSON.Product.imageUrls)) {
|
|
// 如果不是数组类型,也转换为空数组
|
|
favoriteJSON.Product.imageUrls = [];
|
|
}
|
|
} else {
|
|
// 确保imageUrls始终是数组
|
|
favoriteJSON.Product.imageUrls = [];
|
|
}
|
|
}
|
|
return favoriteJSON;
|
|
});
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '获取收藏列表成功',
|
|
data: {
|
|
favorites: processedFavorites,
|
|
count: processedFavorites.length
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('获取收藏列表失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取收藏列表失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 辅助函数:清理临时文件
|
|
function cleanTempFiles(filePaths) {
|
|
if (!filePaths || filePaths.length === 0) {
|
|
return;
|
|
}
|
|
|
|
for (const filePath of filePaths) {
|
|
try {
|
|
if (filePath && fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
console.log('临时文件已清理:', filePath);
|
|
} else {
|
|
console.log('跳过清理不存在的文件:', filePath || 'undefined');
|
|
}
|
|
} catch (err) {
|
|
console.error('清理临时文件失败:', err);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 增加商品查看次数
|
|
app.post('/api/products/increment-frequency', async (req, res) => {
|
|
try {
|
|
const { productId } = req.body;
|
|
|
|
if (!productId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少productId参数'
|
|
});
|
|
}
|
|
|
|
// 增加商品查看次数
|
|
const result = await Product.increment('frequency', {
|
|
where: {
|
|
productId,
|
|
status: { [Sequelize.Op.not]: 'hidden' }
|
|
}
|
|
});
|
|
|
|
console.log('Product.increment result:', result);
|
|
console.log('Product.increment result[0]:', result[0]);
|
|
console.log('Product.increment result type:', typeof result);
|
|
|
|
// 检查是否找到并更新了商品
|
|
let updatedCount = 0;
|
|
if (Array.isArray(result)) {
|
|
updatedCount = result[0] || 0;
|
|
} else if (result && result[Sequelize.Op.increment]) {
|
|
// 处理Sequelize 6+的返回格式
|
|
updatedCount = result[Sequelize.Op.increment] || 0;
|
|
}
|
|
|
|
console.log('Updated count:', updatedCount);
|
|
|
|
if (updatedCount === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '商品不存在或已被隐藏'
|
|
});
|
|
}
|
|
|
|
// 查询更新后的商品信息
|
|
console.log('查询更新后的商品信息,productId:', productId);
|
|
const updatedProduct = await Product.findOne({
|
|
attributes: ['productId', 'productName', 'frequency'],
|
|
where: { productId }
|
|
});
|
|
|
|
console.log('查询到的更新后商品信息:', updatedProduct);
|
|
|
|
if (!updatedProduct) {
|
|
// 这种情况不应该发生,因为我们已经检查了increment操作是否成功
|
|
return res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '服务器错误,无法获取更新后的商品信息'
|
|
});
|
|
}
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '商品查看次数增加成功',
|
|
data: {
|
|
productId: updatedProduct.productId,
|
|
productName: updatedProduct.productName,
|
|
frequency: updatedProduct.frequency
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('增加商品查看次数失败:', error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '服务器错误,增加商品查看次数失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 获取商品详情
|
|
app.post('/api/products/detail', async (req, res) => {
|
|
try {
|
|
const { productId } = req.body;
|
|
|
|
if (!productId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少productId参数'
|
|
});
|
|
}
|
|
|
|
// 查询商品详情 - 排除hidden状态商品,直接使用Product表中的reservedCount字段
|
|
const product = await Product.findOne({
|
|
attributes: ['productId', 'productName', 'price', 'quantity', 'grossWeight', 'imageUrls', 'created_at', 'specification', 'yolk', 'sourceType', 'supplyStatus', 'producting', 'product_contact', 'contact_phone', 'region', 'freshness', 'costprice','description', 'frequency', 'product_log', 'spec_status', 'status', 'label', 'bargaining'],
|
|
where: {
|
|
productId,
|
|
status: { [Sequelize.Op.not]: 'hidden' }
|
|
},
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'seller',
|
|
attributes: ['userId', 'name', 'avatarUrl']
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!product) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '商品不存在'
|
|
});
|
|
}
|
|
|
|
// 查询收藏人数 - 从favorites表中统计该商品的收藏数量
|
|
const favoriteCount = await Favorite.count({
|
|
where: {
|
|
productId: productId
|
|
}
|
|
});
|
|
|
|
// 对返回的商品数据进行处理
|
|
let updatedProduct = { ...product.toJSON() };
|
|
|
|
// 关键修复:将存储在数据库中的JSON字符串反序列化为JavaScript数组
|
|
if (updatedProduct.imageUrls && typeof updatedProduct.imageUrls === 'string') {
|
|
console.log('【关键修复】将数据库中的JSON字符串反序列化为JavaScript数组');
|
|
try {
|
|
updatedProduct.imageUrls = JSON.parse(updatedProduct.imageUrls);
|
|
console.log('反序列化后的imageUrls类型:', typeof updatedProduct.imageUrls);
|
|
} catch (parseError) {
|
|
console.error('反序列化imageUrls失败:', parseError);
|
|
// 如果解析失败,使用空数组确保前端不会崩溃
|
|
updatedProduct.imageUrls = [];
|
|
}
|
|
}
|
|
|
|
// 处理产品日志字段,确保返回数组格式
|
|
if (updatedProduct.product_log) {
|
|
console.log('【产品日志】原始product_log:', updatedProduct.product_log, '类型:', typeof updatedProduct.product_log);
|
|
if (typeof updatedProduct.product_log === 'string') {
|
|
try {
|
|
updatedProduct.product_log = JSON.parse(updatedProduct.product_log);
|
|
console.log('【产品日志】反序列化后的product_log:', updatedProduct.product_log, '类型:', typeof updatedProduct.product_log);
|
|
// 确保是数组格式
|
|
if (!Array.isArray(updatedProduct.product_log)) {
|
|
updatedProduct.product_log = [updatedProduct.product_log];
|
|
console.log('【产品日志】转换为数组:', updatedProduct.product_log);
|
|
}
|
|
} catch (parseError) {
|
|
console.error('【产品日志】反序列化失败:', parseError);
|
|
// 如果解析失败,将字符串作为单个日志条目
|
|
updatedProduct.product_log = [updatedProduct.product_log];
|
|
console.log('【产品日志】转换为单条日志:', updatedProduct.product_log);
|
|
}
|
|
} else if (!Array.isArray(updatedProduct.product_log)) {
|
|
// 如果不是字符串也不是数组,转换为数组
|
|
updatedProduct.product_log = [updatedProduct.product_log];
|
|
console.log('【产品日志】转换为数组:', updatedProduct.product_log);
|
|
}
|
|
} else {
|
|
// 如果没有日志,返回空数组
|
|
updatedProduct.product_log = [];
|
|
console.log('【产品日志】没有日志,返回空数组');
|
|
}
|
|
|
|
// 详细分析毛重字段
|
|
const grossWeightDetails = {
|
|
type: typeof updatedProduct.grossWeight,
|
|
isEmpty: updatedProduct.grossWeight === '' || updatedProduct.grossWeight === null || updatedProduct.grossWeight === undefined,
|
|
isString: typeof updatedProduct.grossWeight === 'string',
|
|
value: updatedProduct.grossWeight === '' || updatedProduct.grossWeight === null || updatedProduct.grossWeight === undefined ? '' : String(updatedProduct.grossWeight),
|
|
isStoredSpecialValue: typeof updatedProduct.grossWeight === 'number' && updatedProduct.grossWeight === 0.01
|
|
};
|
|
|
|
// 详细的日志记录
|
|
console.log('商品详情 - 毛重字段详细分析:');
|
|
console.log('- 原始值:', updatedProduct.grossWeight, '类型:', typeof updatedProduct.grossWeight);
|
|
console.log('- 是否为空值:', grossWeightDetails.isEmpty);
|
|
console.log('- 是否为字符串类型:', grossWeightDetails.isString);
|
|
console.log('- 是否为特殊存储值:', grossWeightDetails.isStoredSpecialValue);
|
|
console.log('- 转换后的值:', grossWeightDetails.value, '类型:', typeof grossWeightDetails.value);
|
|
|
|
// 从数据库读取时的特殊处理:如果是特殊值0.01,表示原始是非数字字符串
|
|
if (grossWeightDetails.isStoredSpecialValue && updatedProduct.originalGrossWeight) {
|
|
// 使用存储的原始非数字毛重字符串
|
|
updatedProduct.grossWeight = String(updatedProduct.originalGrossWeight);
|
|
console.log('检测到特殊存储值,还原原始非数字毛重:', updatedProduct.grossWeight);
|
|
} else {
|
|
// 确保grossWeight值是字符串类型
|
|
updatedProduct.grossWeight = String(grossWeightDetails.value);
|
|
}
|
|
|
|
// 设置收藏人数 - 从favorites表统计得到
|
|
updatedProduct.reservedCount = favoriteCount;
|
|
|
|
console.log('商品详情 - 最终返回的毛重值:', updatedProduct.grossWeight, '类型:', typeof updatedProduct.grossWeight);
|
|
console.log('商品详情 - 返回的收藏人数:', updatedProduct.reservedCount, '类型:', typeof updatedProduct.reservedCount);
|
|
console.log('商品详情 - producting字段:', updatedProduct.producting, '类型:', typeof updatedProduct.producting);
|
|
console.log('商品详情 - description字段:', updatedProduct.description, '类型:', typeof updatedProduct.description);
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '获取商品详情成功',
|
|
data: updatedProduct
|
|
});
|
|
} catch (error) {
|
|
console.error('获取商品详情失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取商品详情失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 获取商品评论列表
|
|
app.post('/api/comments/get', async (req, res) => {
|
|
try {
|
|
const { productId } = req.body;
|
|
|
|
if (!productId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少productId参数'
|
|
});
|
|
}
|
|
|
|
// 查询商品评论
|
|
const comments = await Comment.findAll({
|
|
where: { productId },
|
|
order: [['time', 'DESC']] // 按时间倒序排列
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '获取评论列表成功',
|
|
data: comments
|
|
});
|
|
} catch (error) {
|
|
console.error('获取评论列表失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取评论列表失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 提交评论
|
|
app.post('/api/comments/add', async (req, res) => {
|
|
try {
|
|
const { productId, phoneNumber, comments, review = 0 } = req.body;
|
|
|
|
if (!productId || !phoneNumber || !comments) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要参数'
|
|
});
|
|
}
|
|
|
|
// 创建评论
|
|
const newComment = await Comment.create({
|
|
productId,
|
|
phoneNumber,
|
|
comments,
|
|
review,
|
|
time: new Date()
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '提交评论成功',
|
|
data: newComment
|
|
});
|
|
} catch (error) {
|
|
console.error('提交评论失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '提交评论失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 更新评论点赞数
|
|
app.post('/api/comments/updateLike', async (req, res) => {
|
|
try {
|
|
const { id, like } = req.body;
|
|
|
|
if (!id || like === undefined) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要参数'
|
|
});
|
|
}
|
|
|
|
// 更新点赞数
|
|
await Comment.update(
|
|
{ like },
|
|
{ where: { id } }
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '更新点赞数成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('更新点赞数失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '更新点赞数失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 更新评论点踩数
|
|
app.post('/api/comments/updateHate', async (req, res) => {
|
|
try {
|
|
const { id, hate } = req.body;
|
|
|
|
if (!id || hate === undefined) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要参数'
|
|
});
|
|
}
|
|
|
|
// 更新点踩数
|
|
await Comment.update(
|
|
{ hate },
|
|
{ where: { id } }
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '更新点踩数成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('更新点踩数失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '更新点踩数失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 删除评论
|
|
app.post('/api/comments/delete', async (req, res) => {
|
|
try {
|
|
const { commentId, currentUserPhone, commentPhone } = req.body;
|
|
|
|
// 参数验证
|
|
if (!commentId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少评论ID参数'
|
|
});
|
|
}
|
|
|
|
// 验证当前用户是否有权限删除该评论
|
|
// 只有评论的所有者才能删除评论
|
|
if (currentUserPhone !== commentPhone) {
|
|
return res.status(403).json({
|
|
success: false,
|
|
code: 403,
|
|
message: '无权删除该评论'
|
|
});
|
|
}
|
|
|
|
// 从数据库中删除评论
|
|
const result = await Comment.destroy({
|
|
where: { id: commentId }
|
|
});
|
|
|
|
// 检查是否删除成功
|
|
if (result === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '评论不存在或已被删除'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '删除评论成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('删除评论失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '删除评论失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 修改商品
|
|
app.post('/api/products/edit', async (req, res) => {
|
|
try {
|
|
const { productId, ...updateData } = req.body;
|
|
const { sellerId } = req.body;
|
|
|
|
if (!productId || !sellerId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少productId或sellerId参数'
|
|
});
|
|
}
|
|
|
|
// 查找商品
|
|
const product = await Product.findOne({
|
|
where: { productId }
|
|
});
|
|
|
|
if (!product) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '商品不存在'
|
|
});
|
|
}
|
|
|
|
// 检查是否为卖家本人
|
|
if (product.sellerId !== sellerId) {
|
|
return res.status(403).json({
|
|
success: false,
|
|
code: 403,
|
|
message: '您无权修改此商品'
|
|
});
|
|
}
|
|
|
|
// 更新商品信息
|
|
await Product.update(
|
|
{
|
|
...updateData,
|
|
updated_at: getBeijingTime()
|
|
},
|
|
{
|
|
where: { productId }
|
|
}
|
|
);
|
|
|
|
// 获取更新后的商品信息
|
|
const updatedProduct = await Product.findOne({
|
|
where: { productId }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '修改商品成功',
|
|
data: updatedProduct
|
|
});
|
|
} catch (error) {
|
|
console.error('修改商品失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '修改商品失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 删除商品 - 将商品状态设置为hidden表示已删除
|
|
app.post('/api/products/delete', async (req, res) => {
|
|
console.log('收到删除商品请求:', req.body);
|
|
try {
|
|
const { productId, sellerId } = req.body;
|
|
|
|
if (!productId || !sellerId) {
|
|
console.error('删除商品失败: 缺少productId或sellerId参数');
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少productId或sellerId参数'
|
|
});
|
|
}
|
|
|
|
// 查找商品
|
|
const product = await Product.findOne({
|
|
where: { productId }
|
|
});
|
|
|
|
if (!product) {
|
|
console.error('删除商品失败: 商品不存在');
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '商品不存在'
|
|
});
|
|
}
|
|
|
|
// 检查是否为卖家本人
|
|
if (product.sellerId !== sellerId) {
|
|
console.error('删除商品失败: 权限不足 - 卖家ID不匹配', { expected: product.sellerId, actual: sellerId });
|
|
return res.status(403).json({
|
|
success: false,
|
|
code: 403,
|
|
message: '您无权删除此商品'
|
|
});
|
|
}
|
|
|
|
console.log('准备更新商品状态为hidden,当前状态:', product.status);
|
|
|
|
// 直接使用商品实例更新状态
|
|
product.status = 'hidden';
|
|
product.updated_at = getBeijingTime();
|
|
|
|
try {
|
|
// 先尝试保存商品实例
|
|
await product.save();
|
|
console.log('删除商品成功(使用save方法):', { productId: product.productId, newStatus: product.status });
|
|
} catch (saveError) {
|
|
console.error('使用save方法更新失败,尝试使用update方法:', saveError);
|
|
|
|
// 如果保存失败,尝试使用update方法
|
|
try {
|
|
const updateResult = await Product.update(
|
|
{ status: 'hidden', updated_at: getBeijingTime() },
|
|
{ where: { productId } }
|
|
);
|
|
console.log('删除商品成功(使用update方法):', { productId, updateResult });
|
|
} catch (updateError) {
|
|
console.error('使用update方法也失败:', updateError);
|
|
|
|
// 如果update方法也失败,尝试直接执行SQL语句绕过ORM验证
|
|
try {
|
|
await sequelize.query(
|
|
'UPDATE products SET status = :status, updated_at = :updatedAt WHERE productId = :productId',
|
|
{
|
|
replacements: {
|
|
status: 'hidden',
|
|
updatedAt: getBeijingTime(),
|
|
productId: productId
|
|
}
|
|
}
|
|
);
|
|
console.log('删除商品成功(使用原始SQL):', { productId });
|
|
} catch (sqlError) {
|
|
console.error('使用原始SQL也失败:', sqlError);
|
|
throw new Error('所有更新方法都失败: ' + sqlError.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 从购物车中移除该商品
|
|
const destroyResult = await CartItem.destroy({
|
|
where: { productId }
|
|
});
|
|
console.log('从购物车移除商品结果:', destroyResult);
|
|
|
|
// 重新查询商品以确保返回最新状态
|
|
const updatedProduct = await Product.findOne({
|
|
where: { productId }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '删除商品成功',
|
|
product: {
|
|
productId: updatedProduct.productId,
|
|
status: updatedProduct.status
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('删除商品失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '删除商品失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 下架商品 - 修改label字段为1
|
|
app.post('/api/products/unpublish', async (req, res) => {
|
|
console.log('收到下架商品请求:', req.body);
|
|
try {
|
|
const { productId } = req.body;
|
|
|
|
if (!productId) {
|
|
console.error('下架商品失败: 缺少productId参数');
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少productId参数'
|
|
});
|
|
}
|
|
|
|
// 查找商品
|
|
const product = await Product.findOne({
|
|
where: { productId }
|
|
});
|
|
|
|
if (!product) {
|
|
console.error('下架商品失败: 商品不存在');
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '商品不存在'
|
|
});
|
|
}
|
|
|
|
console.log('准备下架商品,productId:', productId, ',当前label:', product.label);
|
|
|
|
// 直接使用商品实例更新label字段和status字段
|
|
product.label = 1;
|
|
product.status = 'sold_out';
|
|
product.updated_at = getBeijingTime();
|
|
|
|
try {
|
|
await product.save();
|
|
console.log('下架商品成功(使用save方法):', { productId: product.productId, newLabel: product.label, newStatus: product.status });
|
|
} catch (saveError) {
|
|
console.error('使用save方法更新失败,尝试使用update方法:', saveError);
|
|
|
|
try {
|
|
const updateResult = await Product.update(
|
|
{ label: 1, status: 'sold_out', updated_at: getBeijingTime() },
|
|
{ where: { productId } }
|
|
);
|
|
console.log('下架商品成功(使用update方法):', { productId, updateResult });
|
|
} catch (updateError) {
|
|
console.error('使用update方法也失败:', updateError);
|
|
throw new Error('下架商品失败: ' + updateError.message);
|
|
}
|
|
}
|
|
|
|
// 重新查询商品以确保返回最新状态
|
|
const updatedProduct = await Product.findOne({
|
|
where: { productId }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '下架商品成功',
|
|
product: {
|
|
productId: updatedProduct.productId,
|
|
label: updatedProduct.label
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('下架商品失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '下架商品失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 测试路由:用于调试购物车数据格式 - 增强版,包含完整的数量处理测试
|
|
app.post('/api/cart/test-format', async (req, res) => {
|
|
try {
|
|
console.log('收到测试购物车请求 - 完整请求体:');
|
|
console.log(JSON.stringify(req.body, null, 2));
|
|
console.log('请求头:', req.headers);
|
|
|
|
// 从请求体中提取openid和商品数据(与实际路由相同的逻辑)
|
|
let openid, productData;
|
|
|
|
// 处理各种可能的数据结构
|
|
if (req.body.openid) {
|
|
openid = req.body.openid;
|
|
productData = req.body.product || req.body;
|
|
} else {
|
|
// 尝试从不同位置获取openid
|
|
openid = req.body.userInfo?.openId || req.body.userInfo?.openid || req.body.userId;
|
|
productData = req.body.product || req.body;
|
|
}
|
|
|
|
console.log('提取到的openid:', openid);
|
|
console.log('提取到的商品数据:', productData ? JSON.stringify(productData, null, 2) : '无');
|
|
|
|
// 获取商品ID
|
|
const productId = productData ? (productData.productId || productData.id) : null;
|
|
console.log('提取到的productId:', productId);
|
|
|
|
// 增强的数量字段检测和处理逻辑
|
|
const quantityFields = ['quantity', 'count', 'amount', 'num', 'qty', 'stock', 'countValue', 'quantityValue'];
|
|
console.log('所有可能的数量字段详细信息:');
|
|
|
|
// 初始化数量处理结果
|
|
let finalQuantity = 1;
|
|
let quantitySource = 'default';
|
|
let foundValidQuantity = false;
|
|
|
|
// 遍历并检测所有可能的数量字段
|
|
for (const field of quantityFields) {
|
|
if (productData && productData[field] !== undefined && productData[field] !== null && productData[field] !== '') {
|
|
const value = productData[field];
|
|
const type = typeof value;
|
|
console.log(` ${field}: ${value} 类型: ${type}`);
|
|
|
|
// 尝试转换为数字并验证
|
|
let numericValue;
|
|
if (type === 'string') {
|
|
// 清理字符串,移除非数字字符
|
|
const cleanStr = String(value).replace(/[^\d.]/g, '');
|
|
console.log(` 清理字符串后: "${cleanStr}"`);
|
|
numericValue = parseFloat(cleanStr);
|
|
} else {
|
|
numericValue = Number(value);
|
|
}
|
|
|
|
// 验证数字有效性
|
|
if (!isNaN(numericValue) && isFinite(numericValue) && numericValue > 0) {
|
|
// 标准化为整数
|
|
finalQuantity = Math.floor(numericValue);
|
|
quantitySource = field;
|
|
foundValidQuantity = true;
|
|
console.log(` ✓ 成功转换为有效数量: ${finalQuantity} (来自${field}字段)`);
|
|
break;
|
|
} else {
|
|
console.log(` ✗ 无法转换为有效数字`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 确保数量至少为1
|
|
finalQuantity = Math.max(1, finalQuantity);
|
|
|
|
console.log('数量处理结果汇总:');
|
|
console.log(` 最终处理的数量值: ${finalQuantity}`);
|
|
console.log(` 数量来源字段: ${quantitySource}`);
|
|
console.log(` 是否找到有效数量: ${foundValidQuantity}`);
|
|
|
|
// 构建详细的响应数据,包含处理结果
|
|
res.json({
|
|
success: true,
|
|
message: '测试请求成功接收',
|
|
receivedData: req.body,
|
|
processingResults: {
|
|
openid: openid,
|
|
productId: productId,
|
|
detectedQuantity: {
|
|
value: finalQuantity,
|
|
source: quantitySource,
|
|
isValid: foundValidQuantity
|
|
},
|
|
productDataStructure: productData ? Object.keys(productData) : []
|
|
},
|
|
timestamp: getBeijingTimeISOString()
|
|
});
|
|
} catch (error) {
|
|
console.error('测试路由出错:', error);
|
|
console.error('详细错误信息:', { name: error.name, message: error.message, stack: error.stack });
|
|
res.status(400).json({
|
|
success: false,
|
|
message: '测试路由处理失败',
|
|
error: error.message,
|
|
requestData: req.body,
|
|
timestamp: getBeijingTimeISOString()
|
|
});
|
|
}
|
|
});
|
|
|
|
// 添加商品到购物车
|
|
app.post('/api/cart/add', async (req, res) => {
|
|
// 增加全局错误捕获,确保即使在try-catch外部的错误也能被处理
|
|
try {
|
|
console.log('收到添加到购物车请求 - 开始处理', req.url);
|
|
let cartData = req.body;
|
|
console.log('收到添加到购物车请求数据 - 完整请求体:');
|
|
console.log(JSON.stringify(req.body, null, 2));
|
|
console.log('请求头:', req.headers);
|
|
console.log('请求IP:', req.ip);
|
|
console.log('请求URL:', req.url);
|
|
console.log('请求方法:', req.method);
|
|
|
|
// 兼容客户端请求格式:客户端可能将数据封装在product对象中,并且使用openid而不是userId
|
|
if (cartData.product && !cartData.productId) {
|
|
// 从product对象中提取数据
|
|
const productData = cartData.product;
|
|
console.log('从product对象提取数据:', productData);
|
|
// 打印所有可能包含数量信息的字段
|
|
console.log('productData中可能的数量字段:');
|
|
console.log(' quantity:', productData.quantity, '类型:', typeof productData.quantity);
|
|
console.log(' count:', productData.count, '类型:', typeof productData.count);
|
|
console.log(' amount:', productData.amount, '类型:', typeof productData.amount);
|
|
console.log(' num:', productData.num, '类型:', typeof productData.num);
|
|
console.log(' quantityValue:', productData.quantityValue, '类型:', typeof productData.quantityValue);
|
|
console.log(' qty:', productData.qty, '类型:', typeof productData.qty);
|
|
console.log(' stock:', productData.stock, '类型:', typeof productData.stock);
|
|
console.log(' countValue:', productData.countValue, '类型:', typeof productData.countValue);
|
|
console.log(' product.quantity:', productData['product.quantity'], '类型:', typeof productData['product.quantity']);
|
|
console.log('客户端提供的openid:', cartData.openid);
|
|
|
|
// 使用openid作为userId
|
|
cartData = {
|
|
userId: cartData.openid || productData.userId,
|
|
productId: productData.productId || productData.id,
|
|
productName: productData.productName || productData.name,
|
|
// 优化的数量处理逻辑 - 确保获取有效数量并转换为数字
|
|
quantity: (() => {
|
|
let finalQuantity = 1; // 默认数量
|
|
let foundQuantity = false;
|
|
|
|
// 定义所有可能的数量字段名称,按优先级排序
|
|
const quantityFields = ['quantity', 'count', 'amount', 'num', 'qty', 'stock', 'countValue', 'quantityValue'];
|
|
|
|
// 遍历所有可能的数量字段,找到第一个有效值
|
|
for (const field of quantityFields) {
|
|
if (productData[field] !== undefined && productData[field] !== null && productData[field] !== '') {
|
|
console.log(`找到数量字段: ${field} = ${productData[field]} (类型: ${typeof productData[field]})`);
|
|
|
|
// 无论是什么类型,先尝试转换为数字
|
|
let numericValue;
|
|
if (typeof productData[field] === 'string') {
|
|
// 对于字符串,先移除所有非数字字符(保留小数点)
|
|
const cleanStr = String(productData[field]).replace(/[^\d.]/g, '');
|
|
console.log(`清理字符串后的数量: "${cleanStr}"`);
|
|
numericValue = parseFloat(cleanStr);
|
|
} else {
|
|
numericValue = Number(productData[field]);
|
|
}
|
|
|
|
// 验证是否是有效数字
|
|
if (!isNaN(numericValue) && isFinite(numericValue) && numericValue > 0) {
|
|
// 确保是整数
|
|
finalQuantity = Math.floor(numericValue);
|
|
console.log(`成功转换并标准化数量值: ${finalQuantity}`);
|
|
foundQuantity = true;
|
|
break;
|
|
} else {
|
|
console.log(`字段 ${field} 的值无法转换为有效数字: ${productData[field]}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 如果遍历完所有字段仍未找到有效数量
|
|
if (!foundQuantity) {
|
|
console.log('未找到有效数量字段,使用默认值1');
|
|
}
|
|
|
|
// 最后确保数量至少为1
|
|
finalQuantity = Math.max(1, finalQuantity);
|
|
console.log(`最终确定的数量值: ${finalQuantity} (类型: ${typeof finalQuantity})`);
|
|
return finalQuantity;
|
|
})(),
|
|
price: productData.price,
|
|
specification: productData.specification || productData.spec || '',
|
|
grossWeight: productData.grossWeight || productData.weight,
|
|
yolk: productData.yolk || productData.variety || '',
|
|
testMode: productData.testMode || cartData.testMode
|
|
};
|
|
console.log('即将用于创建/更新购物车项的最终数量值:', cartData.quantity);
|
|
console.log('转换后的购物车数据:', cartData);
|
|
|
|
// 检查转换后的userId是否存在于users表中
|
|
try {
|
|
console.log('开始查询用户信息,openid:', cartData.userId);
|
|
const user = await User.findOne({
|
|
where: { openid: cartData.userId }
|
|
});
|
|
if (user) {
|
|
console.log(`找到对应的用户记录: openid=${cartData.userId}, userId=${user.userId}`);
|
|
// 修正:使用数据库中真实的userId而不是openid
|
|
cartData.userId = user.userId;
|
|
console.log('修正后的userId:', cartData.userId);
|
|
} else {
|
|
console.error(`未找到openid为 ${cartData.userId} 的用户记录,无法添加到购物车`);
|
|
// 重要:找不到用户时返回错误,避免使用无效的userId导致外键约束失败
|
|
return res.status(401).json({
|
|
success: false,
|
|
code: 401,
|
|
message: '找不到对应的用户记录,请重新登录',
|
|
error: `未找到用户记录: ${cartData.userId}`,
|
|
needRelogin: true // 添加重新登录标志
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('查询用户信息失败:', error);
|
|
// 查询失败时也返回错误
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '查询用户信息失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
// 验证必要字段
|
|
if (!cartData.userId || !cartData.productId || !cartData.productName || !cartData.quantity) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要的购物车信息',
|
|
missingFields: [
|
|
!cartData.userId ? 'userId' : '',
|
|
!cartData.productId ? 'productId' : '',
|
|
!cartData.productName ? 'productName' : '',
|
|
!cartData.quantity ? 'quantity' : ''
|
|
].filter(Boolean)
|
|
});
|
|
}
|
|
|
|
// 先验证用户ID是否存在于users表中
|
|
try {
|
|
const userExists = await User.findOne({
|
|
where: { userId: cartData.userId }
|
|
});
|
|
if (!userExists) {
|
|
console.error(`用户ID ${cartData.userId} 不存在于users表中`);
|
|
return res.status(401).json({
|
|
success: false,
|
|
code: 401,
|
|
message: '找不到对应的用户记录,请重新登录',
|
|
error: `用户ID ${cartData.userId} 不存在`,
|
|
needRelogin: true // 添加重新登录标志
|
|
});
|
|
} else {
|
|
console.log(`用户ID ${cartData.userId} 存在于users表中,用户验证通过`);
|
|
}
|
|
} catch (error) {
|
|
console.error('验证用户ID失败:', error);
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '验证用户信息失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
|
|
// 检查商品是否存在以及是否为hidden状态
|
|
console.log(`检查商品ID: ${cartData.productId} 是否存在于products表中`);
|
|
const product = await Product.findOne({
|
|
where: {
|
|
productId: cartData.productId
|
|
}
|
|
});
|
|
|
|
if (!product) {
|
|
console.error(`商品ID ${cartData.productId} 不存在于products表中`);
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '商品不存在或已被移除',
|
|
error: `未找到商品ID: ${cartData.productId}`
|
|
});
|
|
} else {
|
|
console.log(`商品ID ${cartData.productId} 存在于products表中,商品名称: ${product.productName}`);
|
|
}
|
|
|
|
if (product.status === 'hidden') {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '该商品已下架,无法添加到购物车'
|
|
});
|
|
}
|
|
|
|
// 在testMode下,不执行实际的数据库操作,直接返回成功
|
|
if (cartData.testMode) {
|
|
console.log('测试模式:跳过实际的数据库操作');
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '测试模式:添加到购物车成功',
|
|
data: {
|
|
userId: cartData.userId,
|
|
productId: cartData.productId,
|
|
productName: cartData.productName,
|
|
quantity: cartData.quantity
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 检查是否已存在相同商品
|
|
const existingItem = await CartItem.findOne({
|
|
where: {
|
|
userId: cartData.userId,
|
|
productId: cartData.productId
|
|
}
|
|
});
|
|
|
|
// 添加try-catch捕获外键约束错误
|
|
try {
|
|
console.log(`准备创建/更新购物车项: userId=${cartData.userId}, productId=${cartData.productId}`);
|
|
if (existingItem) {
|
|
// 添加详细的时间调试日志
|
|
const updateCurrentDate = getBeijingTime();
|
|
const updateUtcTime = updateCurrentDate.toISOString();
|
|
console.log('购物车更新 - 当前时间对象:', updateCurrentDate);
|
|
console.log('购物车更新 - 转换后UTC时间:', updateUtcTime);
|
|
console.log('购物车更新 - 时区设置:', sequelize.options.timezone);
|
|
// 已存在,更新数量
|
|
await CartItem.update(
|
|
{
|
|
quantity: existingItem.quantity + cartData.quantity,
|
|
updated_at: getBeijingTimeISOString()
|
|
},
|
|
{
|
|
where: {
|
|
id: existingItem.id
|
|
}
|
|
}
|
|
);
|
|
console.log(`更新购物车项成功: id=${existingItem.id}, 新数量=${existingItem.quantity + cartData.quantity}`);
|
|
|
|
// 同步更新用户表的updated_at为UTC时间
|
|
const userUtcTime = getBeijingTimeISOString();
|
|
console.log('用户表更新 - UTC时间:', userUtcTime);
|
|
await User.update(
|
|
{ updated_at: userUtcTime },
|
|
{ where: { userId: cartData.userId } }
|
|
);
|
|
console.log(`用户${cartData.userId}的updated_at已更新为UTC时间: ${userUtcTime}`);
|
|
} else {
|
|
// 不存在,创建新购物车项
|
|
console.log('创建新购物车项,所有字段:');
|
|
// 添加详细的时间调试日志
|
|
const currentDate = getBeijingTime();
|
|
const utcTime = getBeijingTimeISOString();
|
|
console.log('购物车创建 - 当前时间对象:', currentDate);
|
|
console.log('购物车创建 - 转换后UTC时间:', utcTime);
|
|
console.log('购物车创建 - 时区设置:', sequelize.options.timezone);
|
|
console.log(' userId:', cartData.userId);
|
|
console.log(' productId:', cartData.productId);
|
|
console.log(' productName:', cartData.productName);
|
|
console.log(' 最终写入数据库的quantity值:', cartData.quantity, '类型:', typeof cartData.quantity);
|
|
console.log(' price:', cartData.price);
|
|
console.log(' specification:', cartData.specification);
|
|
console.log(' grossWeight:', cartData.grossWeight);
|
|
console.log(' yolk:', cartData.yolk);
|
|
// 重要:在创建前再次验证数据完整性
|
|
if (!cartData.userId || !cartData.productId) {
|
|
throw new Error(`数据不完整: userId=${cartData.userId}, productId=${cartData.productId}`);
|
|
}
|
|
await CartItem.create({
|
|
...cartData,
|
|
added_at: getBeijingTimeISOString()
|
|
});
|
|
console.log(`创建购物车项成功: userId=${cartData.userId}, productId=${cartData.productId}`);
|
|
|
|
// 同步更新用户表的updated_at为UTC时间
|
|
const updateUserUtcTime = getBeijingTimeISOString()
|
|
console.log('用户表更新 - UTC时间:', updateUserUtcTime);
|
|
await User.update(
|
|
{ updated_at: updateUserUtcTime },
|
|
{ where: { userId: cartData.userId } }
|
|
);
|
|
console.log(`用户${cartData.userId}的updated_at已更新为UTC时间: ${updateUserUtcTime}`);
|
|
}
|
|
} catch (createError) {
|
|
console.error('创建/更新购物车项失败,可能是外键约束问题:', createError);
|
|
console.error('详细错误信息:', {
|
|
name: createError.name,
|
|
message: createError.message,
|
|
stack: createError.stack,
|
|
sql: createError.sql || '无SQL信息',
|
|
userId: cartData.userId,
|
|
productId: cartData.productId
|
|
});
|
|
|
|
// 检测是否是外键约束错误
|
|
if (createError.name === 'SequelizeForeignKeyConstraintError' || createError.message.includes('foreign key')) {
|
|
// 区分是用户ID还是商品ID问题
|
|
let errorField = 'productId';
|
|
let errorMessage = '商品信息已更新,请刷新页面后重试';
|
|
|
|
if (createError.message.includes('userId') || createError.message.includes('user') || createError.message.toLowerCase().includes('user')) {
|
|
errorField = 'userId';
|
|
errorMessage = '用户信息无效,请重新登录后重试';
|
|
}
|
|
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: errorMessage,
|
|
error: `外键约束错误: ${errorField} 不存在或已失效`,
|
|
details: {
|
|
userId: cartData.userId,
|
|
productId: cartData.productId
|
|
}
|
|
});
|
|
}
|
|
|
|
// 其他类型的错误也返回400状态码,避免500错误
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '添加购物车项失败,请稍后重试',
|
|
error: createError.message,
|
|
details: {
|
|
userId: cartData.userId,
|
|
productId: cartData.productId
|
|
}
|
|
});
|
|
}
|
|
|
|
// 更新商品的预约人数 - 与selected状态同步的实现
|
|
try {
|
|
console.log(`尝试更新商品预约人数: productId=${cartData.productId}`);
|
|
|
|
// 检查用户类型,如果是seller则更新为both - 增强版
|
|
try {
|
|
console.log(`开始执行用户类型转换检查: userId=${cartData.userId}`);
|
|
const user = await User.findOne({ where: { userId: cartData.userId } });
|
|
|
|
if (!user) {
|
|
console.error(`用户类型转换失败: 未找到用户ID=${cartData.userId}`);
|
|
} else if (!user.type) {
|
|
console.error(`用户类型转换失败: 用户类型字段为空 - userId=${cartData.userId}`);
|
|
// 为安全起见,如果类型为空,设置为both
|
|
await User.update({ type: 'both' }, { where: { userId: cartData.userId } });
|
|
console.log(`已修复空类型字段: userId=${cartData.userId} 设置为 both`);
|
|
} else if (user.type === 'seller') {
|
|
console.log(`用户类型转换: userId=${cartData.userId} 从 ${user.type} 转为 both`);
|
|
const updateResult = await User.update({ type: 'both' }, { where: { userId: cartData.userId } });
|
|
console.log(`用户类型更新结果: 影响行数=${updateResult[0]}`);
|
|
|
|
// 验证更新是否成功
|
|
const updatedUser = await User.findOne({ where: { userId: cartData.userId } });
|
|
console.log(`用户类型更新验证: 更新后类型=${updatedUser.type}`);
|
|
} else {
|
|
console.log(`用户类型无需转换: userId=${cartData.userId} 类型=${user.type}`);
|
|
}
|
|
} catch (userUpdateError) {
|
|
console.error(`更新用户类型失败:`, userUpdateError);
|
|
console.error(`详细错误信息:`, { name: userUpdateError.name, message: userUpdateError.message, stack: userUpdateError.stack });
|
|
// 继续执行,不中断主要流程
|
|
}
|
|
|
|
// 只有当购物车项是选中状态时,才增加商品的预约人数
|
|
const isSelected = cartData.selected !== undefined ? cartData.selected : true; // 默认选中
|
|
if (isSelected) {
|
|
// 直接更新商品预约人数
|
|
await Product.increment('reservedCount', { by: 1, where: { productId: cartData.productId } });
|
|
|
|
// 更新后重新查询以获取实际的reservedCount值
|
|
const updatedProduct = await Product.findOne({ where: { productId: cartData.productId } });
|
|
if (updatedProduct) {
|
|
console.log(`商品预约人数更新成功: productId=${cartData.productId}, 新数量=${updatedProduct.reservedCount}`);
|
|
} else {
|
|
console.log(`商品预约人数更新成功,但无法获取更新后的值: productId=${cartData.productId}`);
|
|
}
|
|
} else {
|
|
console.log(`购物车项未选中,不更新商品预约人数: productId=${cartData.productId}`);
|
|
}
|
|
} catch (updateError) {
|
|
console.error(`更新商品预约人数失败:`, updateError);
|
|
// 继续执行,不中断主要流程
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '添加到购物车成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('添加到购物车失败:', error);
|
|
console.error('全局错误捕获,详细信息:', {
|
|
name: error.name,
|
|
message: error.message,
|
|
stack: error.stack,
|
|
sql: error.sql || '无SQL信息'
|
|
});
|
|
|
|
// 增强的错误处理 - 强制所有错误返回400状态码
|
|
console.error('全局错误处理 - 捕获到未处理的错误:', error);
|
|
const statusCode = 400; // 强制所有错误返回400状态码,避免前端显示500错误
|
|
let errorMessage = '添加到购物车失败';
|
|
|
|
// 更精确地检测外键约束错误
|
|
if (error.name === 'SequelizeForeignKeyConstraintError' ||
|
|
error.message.toLowerCase().includes('foreign key') ||
|
|
error.message.toLowerCase().includes('constraint fails') ||
|
|
error.message.toLowerCase().includes('constraint')) {
|
|
errorMessage = '添加到购物车失败:商品或用户信息已更新,请刷新页面后重试';
|
|
console.error('检测到外键约束相关错误,返回400状态码');
|
|
}
|
|
|
|
console.log(`准备返回错误响应 - 状态码: ${statusCode}, 消息: ${errorMessage}`);
|
|
|
|
// 确保响应能够正确发送
|
|
try {
|
|
res.status(statusCode).json({
|
|
success: false,
|
|
code: statusCode,
|
|
message: errorMessage,
|
|
error: error.message,
|
|
errorDetails: {
|
|
name: error.name,
|
|
message: error.message,
|
|
stack: error.stack,
|
|
sql: error.sql || '无SQL信息'
|
|
}
|
|
});
|
|
} catch (resError) {
|
|
console.error('发送错误响应失败:', resError);
|
|
// 即使发送响应失败,也尝试以文本格式发送
|
|
try {
|
|
res.status(400).send('添加到购物车失败,请刷新页面后重试');
|
|
} catch (finalError) {
|
|
console.error('无法发送任何响应:', finalError);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 获取购物车信息
|
|
app.post('/api/cart/get', async (req, res) => {
|
|
try {
|
|
const { userId } = req.body;
|
|
|
|
if (!userId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少userId参数'
|
|
});
|
|
}
|
|
|
|
// 查询购物车信息 - 排除关联商品为hidden或sold_out状态的项
|
|
const cartItems = await CartItem.findAll({
|
|
where: { userId },
|
|
include: [
|
|
{
|
|
model: Product,
|
|
as: 'product',
|
|
attributes: ['productName', 'price', 'quantity', 'status', 'specification', 'grossWeight', 'yolk'],
|
|
where: {
|
|
status: { [Sequelize.Op.notIn]: ['hidden', 'sold_out'] }
|
|
},
|
|
// 修复连接条件,使用正确的 Sequelize 连接语法
|
|
on: {
|
|
productId: {
|
|
[Sequelize.Op.col]: 'CartItem.productId'
|
|
}
|
|
}
|
|
}
|
|
],
|
|
order: [['added_at', 'DESC']]
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '获取购物车信息成功',
|
|
data: {
|
|
cartItems
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('获取购物车信息失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取购物车信息失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 更新购物车项
|
|
app.post('/api/cart/update', async (req, res) => {
|
|
try {
|
|
const { id, quantity, selected } = req.body;
|
|
|
|
if (!id) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少id参数'
|
|
});
|
|
}
|
|
|
|
// 构建更新数据
|
|
const updateData = {};
|
|
if (quantity !== undefined) updateData.quantity = quantity;
|
|
updateData.updated_at = getBeijingTime();
|
|
|
|
// 如果selected状态发生变化,先获取当前购物车项信息
|
|
let cartItem = null;
|
|
if (selected !== undefined) {
|
|
cartItem = await CartItem.findOne({ where: { id } });
|
|
}
|
|
|
|
// 更新购物车项
|
|
if (selected !== undefined) updateData.selected = selected;
|
|
await CartItem.update(updateData, {
|
|
where: { id }
|
|
});
|
|
|
|
// 如果selected状态发生变化,同步更新商品的预约人数
|
|
if (selected !== undefined && cartItem) {
|
|
try {
|
|
const change = selected && !cartItem.selected ? 1 : (!selected && cartItem.selected ? -1 : 0);
|
|
if (change !== 0) {
|
|
console.log(`同步更新商品预约人数: productId=${cartItem.productId}, change=${change}`);
|
|
const product = await Product.findOne({ where: { productId: cartItem.productId } });
|
|
if (product) {
|
|
// 确保reservedCount不会变为负数
|
|
const newReservedCount = Math.max(0, (product.reservedCount || 0) + change);
|
|
await Product.update(
|
|
{ reservedCount: newReservedCount },
|
|
{ where: { productId: cartItem.productId } }
|
|
);
|
|
console.log(`商品预约人数更新成功: productId=${cartItem.productId}, 新数量=${newReservedCount}`);
|
|
}
|
|
}
|
|
} catch (syncError) {
|
|
console.error(`同步更新商品预约人数失败:`, syncError);
|
|
// 继续执行,不中断主要流程
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '更新购物车成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('更新购物车失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '更新购物车失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 删除购物车项
|
|
app.post('/api/cart/delete', async (req, res) => {
|
|
try {
|
|
const { id } = req.body;
|
|
|
|
if (!id) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少id参数'
|
|
});
|
|
}
|
|
|
|
// 先获取要删除的购物车项信息,检查是否为选中状态
|
|
const cartItem = await CartItem.findOne({ where: { id } });
|
|
|
|
// 删除购物车项
|
|
await CartItem.destroy({
|
|
where: { id }
|
|
});
|
|
|
|
// 如果购物车项是选中状态,同步减少商品的预约人数
|
|
if (cartItem && cartItem.selected) {
|
|
try {
|
|
console.log(`删除选中的购物车项,同步减少商品预约人数: productId=${cartItem.productId}`);
|
|
const product = await Product.findOne({ where: { productId: cartItem.productId } });
|
|
if (product) {
|
|
// 确保reservedCount不会变为负数
|
|
const newReservedCount = Math.max(0, (product.reservedCount || 0) - 1);
|
|
await Product.update(
|
|
{ reservedCount: newReservedCount },
|
|
{ where: { productId: cartItem.productId } }
|
|
);
|
|
console.log(`商品预约人数更新成功: productId=${cartItem.productId}, 新数量=${newReservedCount}`);
|
|
}
|
|
} catch (syncError) {
|
|
console.error(`同步减少商品预约人数失败:`, syncError);
|
|
// 继续执行,不中断主要流程
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '删除购物车项成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('删除购物车项失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '删除购物车项失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 清空购物车
|
|
app.post('/api/cart/clear', async (req, res) => {
|
|
try {
|
|
const { userId } = req.body;
|
|
|
|
if (!userId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少userId参数'
|
|
});
|
|
}
|
|
|
|
// 先获取用户所有被选中的购物车项
|
|
const selectedCartItems = await CartItem.findAll({
|
|
where: {
|
|
userId,
|
|
selected: true
|
|
}
|
|
});
|
|
|
|
// 清空购物车
|
|
await CartItem.destroy({
|
|
where: { userId }
|
|
});
|
|
|
|
// 如果有被选中的购物车项,同步减少对应商品的预约人数
|
|
if (selectedCartItems.length > 0) {
|
|
try {
|
|
console.log(`清空购物车,同步减少${selectedCartItems.length}个商品的预约人数`);
|
|
|
|
// 统计每个商品需要减少的预约人数
|
|
const productReservations = {};
|
|
selectedCartItems.forEach(item => {
|
|
productReservations[item.productId] = (productReservations[item.productId] || 0) + 1;
|
|
});
|
|
|
|
// 批量更新商品的预约人数
|
|
for (const [productId, count] of Object.entries(productReservations)) {
|
|
const product = await Product.findOne({ where: { productId } });
|
|
if (product) {
|
|
// 确保reservedCount不会变为负数
|
|
const newReservedCount = Math.max(0, (product.reservedCount || 0) - count);
|
|
await Product.update(
|
|
{ reservedCount: newReservedCount },
|
|
{ where: { productId } }
|
|
);
|
|
console.log(`商品预约人数更新成功: productId=${productId}, 减少=${count}, 新数量=${newReservedCount}`);
|
|
}
|
|
}
|
|
} catch (syncError) {
|
|
console.error(`同步减少商品预约人数失败:`, syncError);
|
|
// 继续执行,不中断主要流程
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '清空购物车成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('清空购物车失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '清空购物车失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 测试连接接口
|
|
app.get('/api/test-connection', async (req, res) => {
|
|
try {
|
|
// 检查数据库连接
|
|
await sequelize.authenticate();
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '服务器连接成功,数据库可用',
|
|
timestamp: getBeijingTimeISOString(),
|
|
serverInfo: {
|
|
port: PORT,
|
|
nodeVersion: process.version,
|
|
database: 'MySQL',
|
|
status: 'running'
|
|
}
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '服务器连接失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// OSS连接测试接口
|
|
app.get('/api/test-oss-connection', async (req, res) => {
|
|
try {
|
|
console.log('收到OSS连接测试请求');
|
|
const OssUploader = require('./oss-uploader');
|
|
const result = await OssUploader.testConnection();
|
|
|
|
res.status(result.success ? 200 : 500).json({
|
|
...result,
|
|
timestamp: getBeijingTimeISOString(),
|
|
code: result.success ? 200 : 500
|
|
});
|
|
} catch (error) {
|
|
console.error('OSS连接测试接口错误:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: 'OSS连接测试接口执行失败',
|
|
error: error.message,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
});
|
|
|
|
// 用户类型调试接口 - 增强版:用于排查用户类型和商品显示问题
|
|
app.post('/api/user/debug', async (req, res) => {
|
|
try {
|
|
const { openid } = req.body;
|
|
|
|
console.log('收到用户调试请求,openid:', openid);
|
|
|
|
if (!openid) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少openid参数'
|
|
});
|
|
}
|
|
|
|
// 查询用户信息
|
|
const user = await User.findOne({
|
|
where: { openid },
|
|
attributes: ['openid', 'userId', 'name', 'phoneNumber', 'type']
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '用户不存在',
|
|
debugInfo: {
|
|
searchCriteria: { openid },
|
|
timestamp: getBeijingTimeISOString()
|
|
}
|
|
});
|
|
}
|
|
|
|
// 查询该用户的商品统计信息
|
|
const totalProducts = await Product.count({ where: { sellerId: user.userId } });
|
|
const pendingProducts = await Product.count({
|
|
where: {
|
|
sellerId: user.userId,
|
|
status: 'pending_review'
|
|
}
|
|
});
|
|
const reviewedProducts = await Product.count({
|
|
where: {
|
|
sellerId: user.userId,
|
|
status: 'reviewed'
|
|
}
|
|
});
|
|
const publishedProducts = await Product.count({
|
|
where: {
|
|
sellerId: user.userId,
|
|
status: 'published'
|
|
}
|
|
});
|
|
const soldOutProducts = await Product.count({
|
|
where: {
|
|
sellerId: user.userId,
|
|
status: 'sold_out'
|
|
}
|
|
});
|
|
|
|
// 判断用户是否有权限查看所有商品
|
|
const canViewAllProducts = ['seller', 'both', 'admin'].includes(user.type);
|
|
|
|
// 获取该用户的最新5个商品信息(用于调试)
|
|
const latestProducts = await Product.findAll({
|
|
where: { sellerId: user.userId },
|
|
limit: 5,
|
|
order: [['created_at', 'DESC']],
|
|
attributes: ['productId', 'productName', 'status', 'created_at']
|
|
});
|
|
|
|
const responseData = {
|
|
success: true,
|
|
code: 200,
|
|
message: '获取用户调试信息成功',
|
|
userInfo: user,
|
|
productStats: {
|
|
total: totalProducts,
|
|
pendingReview: pendingProducts,
|
|
reviewed: reviewedProducts,
|
|
published: publishedProducts,
|
|
soldOut: soldOutProducts
|
|
},
|
|
permissionInfo: {
|
|
canViewAllProducts: canViewAllProducts,
|
|
userType: user.type,
|
|
allowedTypesForViewingAllProducts: ['seller', 'both', 'admin']
|
|
},
|
|
latestProducts: latestProducts,
|
|
debugInfo: {
|
|
userCount: await User.count(),
|
|
totalProductsInSystem: await Product.count(),
|
|
timestamp: new Date().toISOString(),
|
|
serverTime: getBeijingTime().toLocaleString('zh-CN')
|
|
}
|
|
};
|
|
|
|
console.log('调试信息返回数据:', JSON.stringify(responseData, null, 2).substring(0, 500) + '...');
|
|
res.json(responseData);
|
|
} catch (error) {
|
|
console.error('获取用户调试信息失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取用户调试信息失败',
|
|
error: error.message,
|
|
debugInfo: {
|
|
errorStack: error.stack,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// 下架商品接口 - 将商品状态设置为sold_out表示已下架
|
|
app.post('/api/product/hide', async (req, res) => {
|
|
console.log('收到下架商品请求:', req.body);
|
|
|
|
try {
|
|
const { openid, productId } = req.body;
|
|
|
|
// 验证请求参数
|
|
if (!openid || !productId) {
|
|
console.error('下架商品失败: 缺少必要参数');
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要参数: openid和productId都是必需的'
|
|
});
|
|
}
|
|
|
|
// 查找用户
|
|
const user = await User.findOne({ where: { openid } });
|
|
|
|
if (!user) {
|
|
console.error('下架商品失败: 用户不存在');
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '用户不存在'
|
|
});
|
|
}
|
|
|
|
console.log('找到用户信息:', { userId: user.userId, name: user.name });
|
|
|
|
// 查找商品并验证所有权 - 直接使用userId,因为商品创建时使用的就是userId
|
|
const product = await Product.findOne({
|
|
where: {
|
|
productId: productId,
|
|
sellerId: user.userId
|
|
}
|
|
});
|
|
|
|
if (!product) {
|
|
console.error('下架商品失败: 商品不存在或不属于当前用户');
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '商品不存在或不属于当前用户'
|
|
});
|
|
}
|
|
|
|
// 记录当前状态,用于调试
|
|
console.log('当前商品状态:', product.status, '允许的状态列表:', Product.rawAttributes.status.validate.isIn);
|
|
console.log('商品所属卖家ID:', product.sellerId);
|
|
console.log('用户ID信息对比:', { userId: user.userId, id: user.id });
|
|
|
|
console.log('准备更新商品状态为sold_out,当前状态:', product.status);
|
|
|
|
// 更新商品状态为已下架(sold_out) - 尝试多种更新方式确保成功
|
|
try {
|
|
// 方法1: 直接保存实例
|
|
product.status = 'sold_out';
|
|
product.updated_at = getBeijingTime();
|
|
await product.save();
|
|
console.log('商品下架成功(使用save方法):', { productId: product.productId, newStatus: product.status });
|
|
} catch (saveError) {
|
|
console.error('使用save方法更新失败,尝试使用update方法:', saveError);
|
|
|
|
try {
|
|
// 方法2: 使用update方法
|
|
const updateResult = await Product.update(
|
|
{ status: 'sold_out', updated_at: getBeijingTime() },
|
|
{ where: { productId: productId, sellerId: user.userId } }
|
|
);
|
|
console.log('商品下架成功(使用update方法):', { productId: productId, sellerIdType: typeof user.userId, updateResult });
|
|
} catch (updateError) {
|
|
console.error('使用update方法也失败:', updateError);
|
|
|
|
try {
|
|
// 方法3: 直接执行SQL语句绕过ORM验证
|
|
const replacements = {
|
|
status: 'sold_out',
|
|
updatedAt: getBeijingTime(),
|
|
productId: productId,
|
|
sellerId: user.userId
|
|
};
|
|
|
|
await sequelize.query(
|
|
'UPDATE products SET status = :status, updated_at = :updatedAt WHERE productId = :productId AND sellerId = :sellerId',
|
|
{
|
|
replacements: replacements
|
|
}
|
|
);
|
|
console.log('商品下架成功(使用原始SQL):', { productId: product.productId, productName: product.productName });
|
|
} catch (sqlError) {
|
|
console.error('使用原始SQL也失败:', sqlError);
|
|
throw new Error('所有更新方法都失败: ' + sqlError.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 重新查询商品以确保返回最新状态
|
|
const updatedProduct = await Product.findOne({
|
|
where: {
|
|
productId: productId,
|
|
sellerId: product.sellerId // 使用找到的商品的sellerId进行查询
|
|
}
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '商品下架成功',
|
|
product: {
|
|
productId: updatedProduct.productId,
|
|
productName: updatedProduct.productName,
|
|
status: updatedProduct.status
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('下架商品过程发生异常:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '下架商品失败: ' + error.message,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 发布商品API
|
|
app.post('/api/product/publish', async (req, res) => {
|
|
console.log('收到发布商品请求:', req.body); // 记录完整请求体
|
|
|
|
try {
|
|
const { openid, product } = req.body;
|
|
|
|
// 验证必填字段
|
|
console.log('验证请求参数: openid=', !!openid, ', product=', !!product);
|
|
if (!openid || !product) {
|
|
console.error('缺少必要参数: openid=', openid, 'product=', product);
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要的参数(openid或product对象)'
|
|
});
|
|
}
|
|
|
|
// 详细检查每个必填字段并记录其类型和值
|
|
console.log('商品字段详细检查:');
|
|
console.log('- productName: 存在=', !!product.productName, '类型=', typeof product.productName, '值=', product.productName);
|
|
console.log('- price: 存在=', !!product.price, '类型=', typeof product.price, '值=', product.price, '转换为数字=', parseFloat(product.price));
|
|
console.log('- quantity: 存在=', !!product.quantity, '类型=', typeof product.quantity, '值=', product.quantity, '转换为数字=', parseInt(product.quantity));
|
|
console.log('- grossWeight: 存在=', !!product.grossWeight, '类型=', typeof product.grossWeight, '值=', product.grossWeight, '转换为数字=', parseFloat(product.grossWeight));
|
|
|
|
// 收集所有验证错误和字段值详情
|
|
const validationErrors = [];
|
|
const fieldDetails = {};
|
|
|
|
// 检查商品名称
|
|
fieldDetails.productName = {
|
|
value: product.productName,
|
|
type: typeof product.productName,
|
|
isEmpty: !product.productName || product.productName.trim() === ''
|
|
};
|
|
if (fieldDetails.productName.isEmpty) {
|
|
console.error('商品名称为空');
|
|
validationErrors.push('商品名称为必填项,不能为空或仅包含空格');
|
|
}
|
|
|
|
// 检查价格
|
|
fieldDetails.price = {
|
|
value: product.price,
|
|
type: typeof product.price,
|
|
isString: typeof product.price === 'string',
|
|
isValid: product.price !== null && product.price !== undefined && product.price !== ''
|
|
};
|
|
if (!product.price || product.price === '') {
|
|
console.error('价格为空');
|
|
validationErrors.push('价格为必填项');
|
|
} else {
|
|
// 确保价格是字符串类型
|
|
product.price = String(product.price);
|
|
}
|
|
|
|
// 检查数量
|
|
fieldDetails.quantity = {
|
|
value: product.quantity,
|
|
type: typeof product.quantity,
|
|
isNumeric: !isNaN(parseFloat(product.quantity)) && isFinite(product.quantity),
|
|
parsedValue: Math.floor(parseFloat(product.quantity)),
|
|
isValid: !isNaN(parseFloat(product.quantity)) && isFinite(product.quantity) && parseFloat(product.quantity) > 0
|
|
};
|
|
if (!product.quantity) {
|
|
console.error('数量为空');
|
|
validationErrors.push('数量为必填项');
|
|
} else if (!fieldDetails.quantity.isNumeric) {
|
|
console.error('数量不是有效数字: quantity=', product.quantity);
|
|
validationErrors.push('数量必须是有效数字格式');
|
|
} else if (fieldDetails.quantity.parsedValue <= 0) {
|
|
console.error('数量小于等于0: quantity=', product.quantity, '转换为数字后=', fieldDetails.quantity.parsedValue);
|
|
validationErrors.push('数量必须大于0');
|
|
}
|
|
|
|
// 改进的毛重字段处理逻辑 - 处理非数字字符串
|
|
const grossWeightDetails = {
|
|
type: typeof product.grossWeight,
|
|
isEmpty: product.grossWeight === '' || product.grossWeight === null || product.grossWeight === undefined,
|
|
isString: typeof product.grossWeight === 'string',
|
|
value: product.grossWeight === '' || product.grossWeight === null || product.grossWeight === undefined ? '' : String(product.grossWeight),
|
|
isNonNumeric: typeof product.grossWeight === 'string' && product.grossWeight.trim() !== '' && isNaN(parseFloat(product.grossWeight.trim()))
|
|
};
|
|
|
|
// 详细的日志记录
|
|
console.log('发布商品 - 毛重字段详细分析:');
|
|
console.log('- 原始值:', product.grossWeight, '类型:', typeof product.grossWeight);
|
|
console.log('- 是否为空值:', grossWeightDetails.isEmpty);
|
|
console.log('- 是否为字符串类型:', grossWeightDetails.isString);
|
|
console.log('- 是否为非数字字符串:', grossWeightDetails.isNonNumeric);
|
|
console.log('- 转换后的值:', grossWeightDetails.value, '类型:', typeof grossWeightDetails.value);
|
|
|
|
// 处理非数字毛重:添加特殊标记处理
|
|
if (grossWeightDetails.isEmpty) {
|
|
product.grossWeight = '';
|
|
} else if (grossWeightDetails.isNonNumeric) {
|
|
// 非数字字符串处理:使用特殊标记存储
|
|
// 在数字字段中存储0.01,并通过特殊属性标记为非数字
|
|
product.grossWeight = 0.01;
|
|
product.isNonNumericGrossWeight = true;
|
|
product.originalGrossWeight = String(grossWeightDetails.value);
|
|
console.log('检测到非数字毛重,使用特殊处理:', { original: grossWeightDetails.value, stored: product.grossWeight });
|
|
} else {
|
|
// 数字字符串或数字,直接存储
|
|
product.grossWeight = parseFloat(product.grossWeight) || 0;
|
|
}
|
|
|
|
// 确保商品名称不超过数据库字段长度限制
|
|
if (product.productName && product.productName.length > 255) {
|
|
console.error('商品名称过长: 长度=', product.productName.length);
|
|
validationErrors.push('商品名称不能超过255个字符');
|
|
}
|
|
|
|
// 如果有验证错误,一次性返回所有错误信息和字段详情
|
|
if (validationErrors.length > 0) {
|
|
console.error('验证失败 - 详细信息:', JSON.stringify({
|
|
errors: validationErrors,
|
|
fieldDetails: fieldDetails
|
|
}, null, 2));
|
|
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '请填写完整信息',
|
|
errors: validationErrors,
|
|
detailedMessage: validationErrors.join('; '),
|
|
fieldDetails: fieldDetails
|
|
});
|
|
}
|
|
|
|
// 查找用户
|
|
console.log('开始查找用户: openid=', openid);
|
|
const user = await User.findOne({ where: { openid } });
|
|
|
|
if (!user) {
|
|
console.error('用户不存在: openid=', openid);
|
|
return res.status(401).json({
|
|
success: false,
|
|
code: 401,
|
|
message: '找不到对应的用户记录,请重新登录',
|
|
needRelogin: true
|
|
});
|
|
}
|
|
|
|
console.log('找到用户:', { userId: user.userId, name: user.name, type: user.type });
|
|
|
|
// 验证用户类型
|
|
console.log(`验证用户类型: 用户ID=${user.userId}, 类型=${user.type}`);
|
|
if (user.type !== 'seller' && user.type !== 'both') {
|
|
console.error(`商品发布失败: 用户${user.userId}类型为${user.type},需要seller或both类型`);
|
|
return res.status(403).json({
|
|
success: false,
|
|
code: 403,
|
|
message: '只有卖家才能发布商品,请在个人资料中修改用户类型'
|
|
});
|
|
}
|
|
|
|
// 生成商品ID
|
|
const productId = `product_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
console.log('生成商品ID:', productId);
|
|
|
|
// 确保grossWeight值是字符串类型
|
|
const finalGrossWeight = String(grossWeightDetails.value);
|
|
console.log('发布商品 - 最终存储的毛重值:', finalGrossWeight, '类型:', typeof finalGrossWeight);
|
|
|
|
// 创建商品
|
|
console.log('准备创建商品:', {
|
|
productName: product.productName,
|
|
price: product.price,
|
|
quantity: product.quantity,
|
|
grossWeight: finalGrossWeight,
|
|
sellerId: user.userId,
|
|
intendedStatus: 'pending_review' // 明确记录预期的状态
|
|
});
|
|
|
|
// 记录状态设置的详细日志
|
|
console.log('🛡️ 状态控制: 强制设置新商品状态为 pending_review');
|
|
console.log('🛡️ 状态控制: 忽略任何可能的自动发布逻辑');
|
|
|
|
const newProduct = await Product.create({
|
|
productId: productId,
|
|
sellerId: user.userId,
|
|
productName: product.productName,
|
|
price: product.price,
|
|
quantity: product.quantity,
|
|
grossWeight: finalGrossWeight, // 使用最终转换的数字值
|
|
yolk: product.yolk || '',
|
|
specification: product.specification || '',
|
|
status: 'pending_review', // 严格设置为待审核状态
|
|
created_at: getBeijingTime(),
|
|
updated_at: getBeijingTime()
|
|
});
|
|
|
|
// 立即验证创建后的状态
|
|
console.log('✅ 商品创建后状态验证:', {
|
|
productId: productId,
|
|
actualStatus: newProduct.status,
|
|
expectedStatus: 'pending_review',
|
|
statusMatch: newProduct.status === 'pending_review'
|
|
});
|
|
|
|
// 查询完整商品信息以确保返回正确的毛重值和状态
|
|
const createdProduct = await Product.findOne({
|
|
where: { productId },
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'seller',
|
|
attributes: ['userId', 'name', 'avatarUrl']
|
|
}
|
|
]
|
|
});
|
|
|
|
// 再次验证数据库中的状态
|
|
if (createdProduct) {
|
|
console.log('发布商品 - 数据库查询后grossWeight:', createdProduct.grossWeight, '类型:', typeof createdProduct.grossWeight);
|
|
console.log('✅ 数据库查询后状态验证:', {
|
|
productId: productId,
|
|
databaseStatus: createdProduct.status,
|
|
expectedStatus: 'pending_review',
|
|
statusMatch: createdProduct.status === 'pending_review'
|
|
});
|
|
|
|
// 安全检查:如果状态不是pending_review,强制更新回pending_review
|
|
if (createdProduct.status !== 'pending_review') {
|
|
console.log('⚠️ 警告: 发现状态不一致,强制更新回pending_review');
|
|
await Product.update(
|
|
{ status: 'pending_review', updated_at: getBeijingTime() },
|
|
{ where: { productId } }
|
|
);
|
|
|
|
// 更新后重新查询以确保状态正确
|
|
const updatedProduct = await Product.findOne({ where: { productId } });
|
|
console.log('✅ 强制更新后状态:', updatedProduct.status);
|
|
|
|
// 更新返回对象的状态
|
|
createdProduct.status = 'pending_review';
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '商品发布成功',
|
|
product: createdProduct,
|
|
productId: productId
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('发布商品失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '发布商品失败: ' + error.message,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 服务器启动由start-server.js负责
|
|
// 这里不再单独启动服务器,避免端口冲突
|
|
// 如需直接测试此文件,请使用:node test-fixed-sync.js
|
|
// 如需完整服务器测试,请使用:node full-server-test.js
|
|
|
|
// 编辑商品API - 用于审核失败商品重新编辑
|
|
app.post('/api/product/edit', async (req, res) => {
|
|
console.log('=== 编辑商品请求开始 ===');
|
|
console.log('【请求信息】收到编辑商品请求 - 详细信息:');
|
|
console.log('- 请求路径:', req.url);
|
|
console.log('- 请求方法:', req.method);
|
|
console.log('- 请求完整body:', JSON.stringify(req.body, null, 2));
|
|
console.log('- 服务器端口:', PORT);
|
|
|
|
try {
|
|
// 【关键修复】根据前端数据结构解析参数
|
|
let openid = req.body.openid;
|
|
let productId = req.body.productId;
|
|
let status = req.body.status;
|
|
let product = req.body.product;
|
|
|
|
console.log('【参数解析】前端发送的数据结构:', {
|
|
openid,
|
|
productId,
|
|
status,
|
|
product: !!product,
|
|
productKeys: product ? Object.keys(product) : '无product'
|
|
});
|
|
|
|
// ========== 【新增】调试:打印productId的类型和值 ==========
|
|
console.log('【调试】productId类型:', typeof productId, '值:', productId);
|
|
console.log('【调试】openid类型:', typeof openid, '值:', openid);
|
|
// ========== 调试结束 ==========
|
|
|
|
// ========== 【新增】编辑商品时的地区字段调试 ==========
|
|
console.log('【地区字段调试】编辑商品 - 开始处理');
|
|
console.log('【地区字段调试】请求体中的region字段:', req.body.region);
|
|
if (product) {
|
|
console.log('【地区字段调试】product对象中的region字段:', product.region, '类型:', typeof product.region);
|
|
}
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
// 验证必填字段
|
|
if (!openid || !productId || !product) {
|
|
console.error('【参数验证】缺少必要参数:', {
|
|
openid: !!openid,
|
|
productId: !!productId,
|
|
product: !!product
|
|
});
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要的参数(openid、productId或product对象)'
|
|
});
|
|
}
|
|
|
|
// 查找用户
|
|
console.log('【用户查找】查找用户 openid=', openid);
|
|
const user = await User.findOne({ where: { openid } });
|
|
|
|
if (!user) {
|
|
console.error('【用户查找】用户不存在: openid=', openid);
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '用户不存在,请先登录'
|
|
});
|
|
}
|
|
|
|
console.log('【用户查找】找到用户:', { userId: user.userId, openid: user.openid });
|
|
|
|
// 查找商品
|
|
console.log('【商品查找】查找商品 productId=', productId, 'sellerId=', user.userId);
|
|
const existingProduct = await Product.findOne({
|
|
where: {
|
|
productId: productId,
|
|
sellerId: user.userId
|
|
}
|
|
});
|
|
|
|
if (!existingProduct) {
|
|
console.error('【商品查找】编辑商品失败: 商品不存在或不属于当前用户');
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '商品不存在或不属于当前用户'
|
|
});
|
|
}
|
|
|
|
// ========== 【新增】编辑商品时的现有商品地区字段调试 ==========
|
|
console.log('【地区字段调试】找到现有商品:', {
|
|
productId: existingProduct.productId,
|
|
productName: existingProduct.productName,
|
|
existingRegion: existingProduct.region, // 特别显示现有地区字段
|
|
regionType: typeof existingProduct.region
|
|
});
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
console.log('【商品查找】找到商品:', {
|
|
productId: existingProduct.productId,
|
|
productName: existingProduct.productName,
|
|
status: existingProduct.status,
|
|
sellerId: existingProduct.sellerId
|
|
});
|
|
|
|
// 验证商品状态是否允许编辑
|
|
if (!['rejected', 'sold_out', 'pending_review', 'reviewed'].includes(existingProduct.status)) {
|
|
console.error('【状态验证】编辑商品失败: 商品状态不允许编辑', {
|
|
productId: productId,
|
|
sellerId: user.userId,
|
|
allowedStatuses: ['rejected', 'sold_out', 'pending_review', 'reviewed'],
|
|
actualStatus: existingProduct.status
|
|
});
|
|
return res.status(403).json({
|
|
success: false,
|
|
code: 403,
|
|
message: '只有审核失败、已下架、审核中或已审核的商品才能编辑',
|
|
debugInfo: {
|
|
allowedStatuses: ['rejected', 'sold_out', 'pending_review', 'reviewed'],
|
|
actualStatus: existingProduct.status
|
|
}
|
|
});
|
|
}
|
|
|
|
console.log('【状态验证】允许编辑商品: productId=' + productId + ', status=' + existingProduct.status + ', sellerId=' + user.userId);
|
|
|
|
// ========== 【修复版】图片处理逻辑开始 ==========
|
|
console.log('=== 开始图片处理 ===');
|
|
|
|
// 【关键修复】安全地获取和转换 existingImageUrls
|
|
let existingImageUrls = [];
|
|
console.log('【图片处理】开始处理商品图片...');
|
|
|
|
try {
|
|
// 从数据库获取现有图片URL
|
|
const rawImageUrls = existingProduct.imageUrls;
|
|
console.log('【图片处理】数据库中的原始imageUrls:', rawImageUrls, '类型:', typeof rawImageUrls);
|
|
|
|
if (Array.isArray(rawImageUrls)) {
|
|
// 如果已经是数组,直接使用
|
|
existingImageUrls = rawImageUrls;
|
|
console.log('【图片处理】existingImageUrls 已经是数组,长度:', existingImageUrls.length);
|
|
} else if (typeof rawImageUrls === 'string') {
|
|
// 如果是字符串,尝试解析JSON
|
|
console.log('【图片处理】existingImageUrls 是字符串,尝试解析JSON');
|
|
try {
|
|
const parsed = JSON.parse(rawImageUrls);
|
|
if (Array.isArray(parsed)) {
|
|
existingImageUrls = parsed;
|
|
console.log('【图片处理】成功解析JSON字符串为数组,长度:', existingImageUrls.length);
|
|
} else {
|
|
console.warn('【图片处理】解析后的JSON不是数组,使用空数组:', parsed);
|
|
existingImageUrls = [];
|
|
}
|
|
} catch (parseError) {
|
|
console.error('【图片处理】JSON解析失败,使用空数组:', parseError);
|
|
existingImageUrls = [];
|
|
}
|
|
} else if (rawImageUrls) {
|
|
// 如果是其他非空值,包装成数组
|
|
console.warn('【图片处理】非数组非字符串类型,包装成数组:', rawImageUrls);
|
|
existingImageUrls = [rawImageUrls].filter(Boolean);
|
|
} else {
|
|
// 空值情况
|
|
console.log('【图片处理】空值,使用空数组');
|
|
existingImageUrls = [];
|
|
}
|
|
} catch (error) {
|
|
console.error('【图片处理】获取existingImageUrls时发生错误:', error);
|
|
existingImageUrls = [];
|
|
}
|
|
|
|
// 【关键修复】确保 newImageUrls 是数组
|
|
let newImageUrls = [];
|
|
try {
|
|
// 【重要修复】检查前端是否传递了 imageUrls
|
|
if (product.imageUrls === undefined || product.imageUrls === null) {
|
|
console.log('【图片处理】前端未传递imageUrls字段,保留现有图片');
|
|
// 如果前端没有传递imageUrls,说明用户不想修改图片,保留现有图片
|
|
newImageUrls = [...existingImageUrls];
|
|
} else {
|
|
newImageUrls = Array.isArray(product.imageUrls) ? product.imageUrls : [];
|
|
}
|
|
console.log('【图片处理】新提交图片:', newImageUrls, '数量:', newImageUrls.length);
|
|
} catch (error) {
|
|
console.error('【图片处理】处理newImageUrls时发生错误:', error);
|
|
// 发生错误时,保留现有图片
|
|
newImageUrls = [...existingImageUrls];
|
|
}
|
|
|
|
console.log('【图片处理】最终确认 - 现有图片:', existingImageUrls, '类型:', typeof existingImageUrls, '是数组:', Array.isArray(existingImageUrls), '长度:', existingImageUrls.length);
|
|
console.log('【图片处理】最终确认 - 新提交图片:', newImageUrls, '类型:', typeof newImageUrls, '是数组:', Array.isArray(newImageUrls), '长度:', newImageUrls.length);
|
|
|
|
// 【关键修复】找出真正被删除的图片 - 只有当新图片数组不为空时才进行比较
|
|
let deletedImageUrls = [];
|
|
if (newImageUrls.length > 0) {
|
|
// 只有在新提交了图片时才删除图片
|
|
deletedImageUrls = existingImageUrls.filter(url => !newImageUrls.includes(url));
|
|
console.log('【图片处理】被删除的图片:', deletedImageUrls, '数量:', deletedImageUrls.length);
|
|
} else {
|
|
// 如果新图片数组为空,说明前端可能没有传递图片数据,不应该删除任何图片
|
|
console.log('【图片处理】新提交图片为空,不删除任何现有图片');
|
|
deletedImageUrls = [];
|
|
}
|
|
|
|
// 【关键修复】构建最终的图片URL数组
|
|
let finalImageUrls = [];
|
|
if (newImageUrls.length > 0) {
|
|
// 如果前端提交了新的图片,使用新的图片URL
|
|
finalImageUrls = [...new Set(newImageUrls)];
|
|
} else {
|
|
// 如果前端没有提交图片,保留现有图片
|
|
finalImageUrls = [...existingImageUrls];
|
|
console.log('【图片处理】使用现有图片,数量:', finalImageUrls.length);
|
|
}
|
|
|
|
console.log('【图片处理】最终图片URL:', finalImageUrls, '长度:', finalImageUrls.length);
|
|
|
|
// 只有在确实有图片被删除时才执行OSS删除操作
|
|
if (deletedImageUrls.length > 0) {
|
|
console.log('【OSS删除】开始删除被移除的图片...');
|
|
|
|
const deletePromises = [];
|
|
|
|
for (const deletedUrl of deletedImageUrls) {
|
|
try {
|
|
// 从完整的URL中提取OSS文件路径
|
|
const urlObj = new URL(deletedUrl);
|
|
let ossFilePath = urlObj.pathname;
|
|
|
|
// 移除开头的斜杠
|
|
if (ossFilePath.startsWith('/')) {
|
|
ossFilePath = ossFilePath.substring(1);
|
|
}
|
|
|
|
console.log('【OSS删除】准备删除文件:', ossFilePath, '原始URL:', deletedUrl);
|
|
|
|
// 异步执行删除,不阻塞主流程
|
|
const deletePromise = OssUploader.deleteFile(ossFilePath)
|
|
.then(() => {
|
|
console.log('【OSS删除】✅ 成功删除文件:', ossFilePath);
|
|
return { success: true, file: ossFilePath };
|
|
})
|
|
.catch(deleteError => {
|
|
// 【增强错误处理】区分权限错误和其他错误
|
|
if (deleteError.code === 'OSS_ACCESS_DENIED' ||
|
|
deleteError.message.includes('permission') ||
|
|
deleteError.originalError?.code === 'AccessDenied') {
|
|
console.error('【OSS删除】❌ 权限不足,无法删除文件:', ossFilePath);
|
|
console.error('【OSS删除】❌ 错误详情:', deleteError.message);
|
|
|
|
// 返回特殊标记,表示权限问题
|
|
return {
|
|
success: false,
|
|
file: ossFilePath,
|
|
error: deleteError.message,
|
|
permissionDenied: true // 标记为权限问题
|
|
};
|
|
} else {
|
|
console.error('【OSS删除】❌ 其他错误删除文件失败:', ossFilePath, '错误:', deleteError.message);
|
|
return {
|
|
success: false,
|
|
file: ossFilePath,
|
|
error: deleteError.message
|
|
};
|
|
}
|
|
});
|
|
|
|
deletePromises.push(deletePromise);
|
|
} catch (urlParseError) {
|
|
console.error('【OSS删除】解析URL失败:', deletedUrl, '错误:', urlParseError.message);
|
|
}
|
|
}
|
|
|
|
// 等待所有删除操作完成
|
|
if (deletePromises.length > 0) {
|
|
console.log('【OSS删除】等待删除操作完成,共', deletePromises.length, '个文件');
|
|
const deleteResults = await Promise.allSettled(deletePromises);
|
|
|
|
const successfulDeletes = deleteResults.filter(result =>
|
|
result.status === 'fulfilled' && result.value && result.value.success
|
|
).length;
|
|
|
|
const permissionDeniedDeletes = deleteResults.filter(result =>
|
|
result.status === 'fulfilled' && result.value && result.value.permissionDenied
|
|
).length;
|
|
|
|
const otherFailedDeletes = deleteResults.filter(result =>
|
|
result.status === 'fulfilled' && result.value && !result.value.success && !result.value.permissionDenied
|
|
).length;
|
|
|
|
console.log('【OSS删除】删除操作统计:');
|
|
console.log('【OSS删除】✅ 成功删除:', successfulDeletes, '个文件');
|
|
console.log('【OSS删除】❌ 权限不足:', permissionDeniedDeletes, '个文件');
|
|
console.log('【OSS删除】⚠️ 其他失败:', otherFailedDeletes, '个文件');
|
|
}
|
|
} else {
|
|
console.log('【OSS删除】没有需要删除的图片');
|
|
}
|
|
|
|
// 更新商品数据中的图片URL
|
|
product.imageUrls = finalImageUrls;
|
|
// ========== 【修复版】图片处理逻辑结束 ==========
|
|
|
|
// 详细检查每个必填字段并记录其类型和值
|
|
console.log('=== 开始字段验证 ===');
|
|
console.log('【字段验证】商品字段详细检查:');
|
|
console.log('- productName: 存在=', !!product.productName, '类型=', typeof product.productName, '值=', product.productName);
|
|
console.log('- price: 存在=', !!product.price, '类型=', typeof product.price, '值=', product.price);
|
|
console.log('- quantity: 存在=', !!product.quantity, '类型=', typeof product.quantity, '值=', product.quantity);
|
|
console.log('- grossWeight: 存在=', !!product.grossWeight, '类型=', typeof product.grossWeight, '值=', product.grossWeight, '转换为数字=', parseFloat(product.grossWeight));
|
|
|
|
// ========== 【新增】编辑商品时的地区字段详细检查 ==========
|
|
console.log('- region: 存在=', !!product.region, '类型=', typeof product.region, '值=', product.region);
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
// 收集所有验证错误
|
|
const validationErrors = [];
|
|
|
|
// 检查商品名称
|
|
if (!product.productName || product.productName.trim() === '') {
|
|
validationErrors.push('商品名称为必填项,不能为空或仅包含空格');
|
|
} else if (product.productName.length > 255) {
|
|
validationErrors.push('商品名称不能超过255个字符');
|
|
}
|
|
|
|
// 【关键修复】价格字段处理 - 采用与毛重类似的灵活处理方式
|
|
let finalPrice = product.price;
|
|
let isNonNumericPrice = false;
|
|
let originalPrice = null;
|
|
|
|
// 检查价格是否为空
|
|
if (!product.price) {
|
|
validationErrors.push('价格为必填项');
|
|
} else {
|
|
// 处理非数字价格值
|
|
if (typeof product.price === 'string' &&
|
|
isNaN(parseFloat(product.price)) &&
|
|
!isFinite(product.price)) {
|
|
|
|
// 标记为非数字价格,但保留原始值以支持中文输入
|
|
isNonNumericPrice = true;
|
|
originalPrice = product.price;
|
|
finalPrice = originalPrice; // 保留原始值以支持中文输入
|
|
|
|
console.log('【字段验证】编辑商品 - 发现非数字价格(支持中文):', originalPrice);
|
|
console.log('【字段验证】编辑商品 - 保留原始值:', {
|
|
isNonNumericPrice,
|
|
originalPrice,
|
|
finalPrice
|
|
});
|
|
}
|
|
}
|
|
|
|
// 【关键修复】数量字段处理 - 采用与毛重类似的灵活处理方式
|
|
let finalQuantity = product.quantity;
|
|
let isNonNumericQuantity = false;
|
|
let originalQuantity = null;
|
|
|
|
// 检查数量是否为空
|
|
if (!product.quantity) {
|
|
validationErrors.push('数量为必填项');
|
|
} else {
|
|
// 处理非数字数量值
|
|
if (typeof product.quantity === 'string' &&
|
|
isNaN(parseInt(product.quantity)) &&
|
|
!isFinite(product.quantity)) {
|
|
|
|
// 标记为非数字数量,但保留原始值以支持中文输入
|
|
isNonNumericQuantity = true;
|
|
originalQuantity = product.quantity;
|
|
finalQuantity = originalQuantity; // 保留原始值以支持中文输入
|
|
|
|
console.log('【字段验证】编辑商品 - 发现非数字数量(支持中文):', originalQuantity);
|
|
console.log('【字段验证】编辑商品 - 保留原始值:', {
|
|
isNonNumericQuantity,
|
|
originalQuantity,
|
|
finalQuantity
|
|
});
|
|
}
|
|
}
|
|
|
|
// 增强的毛重字段处理逻辑 - 与发布商品保持一致
|
|
const grossWeightDetails = {
|
|
type: typeof product.grossWeight,
|
|
isEmpty: product.grossWeight === '' || product.grossWeight === null || product.grossWeight === undefined,
|
|
isString: typeof product.grossWeight === 'string',
|
|
value: product.grossWeight === '' || product.grossWeight === null || product.grossWeight === undefined ? '' : String(product.grossWeight),
|
|
isNonNumeric: false // 新增字段:标记是否为非数字值
|
|
};
|
|
|
|
console.log('【字段验证】编辑商品 - 毛重字段详细分析:');
|
|
console.log('- 原始值:', product.grossWeight, '类型:', typeof product.grossWeight);
|
|
console.log('- 是否为空值:', grossWeightDetails.isEmpty);
|
|
console.log('- 是否为字符串类型:', grossWeightDetails.isString);
|
|
console.log('- 转换后的值:', grossWeightDetails.value, '类型:', typeof grossWeightDetails.value);
|
|
|
|
// 【关键修复】非数字毛重处理逻辑 - 与发布商品接口保持一致
|
|
let finalGrossWeight = grossWeightDetails.value;
|
|
let isNonNumericGrossWeight = false;
|
|
let originalGrossWeight = null;
|
|
|
|
// 处理非空非数字的毛重值 - 修改为保留原始值以支持中文输入
|
|
if (!grossWeightDetails.isEmpty &&
|
|
grossWeightDetails.isString &&
|
|
isNaN(parseFloat(grossWeightDetails.value)) &&
|
|
!isFinite(grossWeightDetails.value)) {
|
|
|
|
// 标记为非数字毛重,但保留原始值
|
|
isNonNumericGrossWeight = true;
|
|
originalGrossWeight = grossWeightDetails.value;
|
|
finalGrossWeight = originalGrossWeight; // 保留原始值以支持中文输入
|
|
grossWeightDetails.isNonNumeric = true;
|
|
|
|
console.log('【字段验证】编辑商品 - 发现非数字毛重(支持中文):', originalGrossWeight);
|
|
console.log('【字段验证】编辑商品 - 保留原始值:', {
|
|
isNonNumericGrossWeight,
|
|
originalGrossWeight,
|
|
finalGrossWeight
|
|
});
|
|
}
|
|
|
|
// 确保最终值是字符串类型
|
|
finalGrossWeight = String(finalGrossWeight);
|
|
console.log('【字段验证】编辑商品 - 最终存储的毛重值:', finalGrossWeight, '类型:', typeof finalGrossWeight);
|
|
|
|
// 如果有验证错误,返回错误信息
|
|
if (validationErrors.length > 0) {
|
|
console.error('【字段验证】验证失败 - 错误:', validationErrors.join('; '));
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '请填写完整信息',
|
|
errors: validationErrors
|
|
});
|
|
}
|
|
|
|
// 准备更新的商品数据
|
|
// 【关键修复】根据前端数据结构确定最终状态
|
|
const finalStatus = status && status.trim() !== '' ? status : existingProduct.status;
|
|
|
|
// 如果是重新提交审核的情况
|
|
let isResubmit = false;
|
|
if (status === 'pending_review' && ['rejected', 'sold_out'].includes(existingProduct.status)) {
|
|
isResubmit = true;
|
|
console.log('【状态转换】检测到重新提交审核操作');
|
|
}
|
|
|
|
// ========== 【新增】编辑商品时的地区字段处理 ==========
|
|
console.log('【地区字段调试】准备更新商品数据 - 处理地区字段');
|
|
const finalRegion = product.region || existingProduct.region || '';
|
|
console.log('【地区字段调试】最终确定的地区字段:', finalRegion, '来源:',
|
|
product.region ? '新提交的数据' :
|
|
existingProduct.region ? '现有商品数据' : '默认空值');
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
// 增强数据同步逻辑
|
|
const updatedProductData = {
|
|
productName: product.productName,
|
|
// 【关键修复】修改价格字段处理,与毛重保持一致 - 非数字值保持字符串类型
|
|
price: isNonNumericPrice ? finalPrice : parseFloat(finalPrice),
|
|
// 【关键修复】修改数量字段处理,与毛重保持一致 - 非数字值保持字符串类型
|
|
quantity: isNonNumericQuantity ? finalQuantity : parseInt(finalQuantity, 10),
|
|
grossWeight: finalGrossWeight,
|
|
yolk: product.yolk,
|
|
specification: product.specification,
|
|
region: finalRegion, // 使用调试确定的地区字段
|
|
imageUrls: product.imageUrls, // 【重要】使用处理后的图片URL
|
|
status: finalStatus,
|
|
// 【关键修复】添加非数字毛重标记和原始值 - 与发布商品接口保持一致
|
|
isNonNumericGrossWeight: isNonNumericGrossWeight,
|
|
originalGrossWeight: originalGrossWeight,
|
|
// 【新增】添加非数字价格和数量标记和原始值
|
|
isNonNumericPrice: isNonNumericPrice,
|
|
originalPrice: originalPrice,
|
|
isNonNumericQuantity: isNonNumericQuantity,
|
|
originalQuantity: originalQuantity,
|
|
// 如果是重新提交审核,清除拒绝原因
|
|
rejectReason: isResubmit ? null : existingProduct.rejectReason,
|
|
updated_at: getBeijingTime()
|
|
};
|
|
|
|
// 【新增】更新前的最终数据验证
|
|
console.log('【数据更新】更新前最终数据验证:');
|
|
console.log('- 价格字段:', updatedProductData.price, '类型:', typeof updatedProductData.price, '是否非数字标记:', updatedProductData.isNonNumericPrice);
|
|
console.log('- 数量字段:', updatedProductData.quantity, '类型:', typeof updatedProductData.quantity, '是否非数字标记:', updatedProductData.isNonNumericQuantity);
|
|
console.log('- 毛重字段:', updatedProductData.grossWeight, '类型:', typeof updatedProductData.grossWeight, '是否非数字标记:', updatedProductData.isNonNumericGrossWeight);
|
|
|
|
// ========== 【新增】更新数据前的地区字段验证 ==========
|
|
console.log('【地区字段调试】更新数据中的region字段:', updatedProductData.region, '类型:', typeof updatedProductData.region);
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
console.log('【数据更新】准备更新商品数据:', {
|
|
productId,
|
|
oldStatus: existingProduct.status,
|
|
newStatus: updatedProductData.status,
|
|
isResubmit: isResubmit,
|
|
updatedFields: Object.keys(updatedProductData)
|
|
});
|
|
|
|
// 更新商品 - 使用最可靠的save方法
|
|
let updatedCount = 0;
|
|
try {
|
|
console.log('【数据更新】使用save方法更新商品数据');
|
|
|
|
// 直接更新现有商品实例的数据
|
|
for (const key in updatedProductData) {
|
|
if (updatedProductData.hasOwnProperty(key)) {
|
|
existingProduct[key] = updatedProductData[key];
|
|
}
|
|
}
|
|
|
|
// ========== 【新增】保存前的地区字段最终检查 ==========
|
|
console.log('【地区字段调试】保存前最终检查 - existingProduct.region:', existingProduct.region, '类型:', typeof existingProduct.region);
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
// 强制保存所有更改
|
|
await existingProduct.save({ validate: false });
|
|
updatedCount = 1;
|
|
console.log('【数据更新】使用save方法成功更新商品数据');
|
|
} catch (saveError) {
|
|
console.error('【数据更新】使用save方法更新商品数据失败:', saveError);
|
|
|
|
// 如果save方法失败,尝试使用update方法
|
|
try {
|
|
[updatedCount] = await Product.update(updatedProductData, {
|
|
where: {
|
|
productId: productId,
|
|
sellerId: user.userId
|
|
}
|
|
});
|
|
console.log('【数据更新】常规update方法执行结果,受影响行数:', updatedCount);
|
|
} catch (updateError) {
|
|
console.error('【数据更新】常规update方法更新商品数据失败:', updateError);
|
|
throw new Error('商品更新失败: ' + updateError.message);
|
|
}
|
|
}
|
|
|
|
// 检查更新是否成功
|
|
if (updatedCount === 0) {
|
|
console.error('【数据更新】商品更新失败: 没有找到匹配的商品或权限不足');
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '商品更新失败: 没有找到匹配的商品或权限不足'
|
|
});
|
|
}
|
|
|
|
// 获取更新后的商品信息
|
|
const updatedProduct = await Product.findOne({ where: { productId: productId } });
|
|
|
|
if (!updatedProduct) {
|
|
console.error('【数据更新】无法获取更新后的商品信息');
|
|
return res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '商品更新成功但无法获取更新后的信息'
|
|
});
|
|
}
|
|
|
|
// ========== 【新增】更新后的地区字段验证 ==========
|
|
console.log('【地区字段调试】更新后验证 - 从数据库读取的商品地区字段:', updatedProduct.region, '类型:', typeof updatedProduct.region);
|
|
// ========== 地区字段调试结束 ==========
|
|
|
|
console.log('【数据更新】商品编辑成功:', {
|
|
productId: productId,
|
|
productName: product.productName,
|
|
oldStatus: existingProduct.status,
|
|
newStatus: updatedProduct.status,
|
|
grossWeight: updatedProduct.grossWeight,
|
|
imageUrls: updatedProduct.imageUrls,
|
|
region: updatedProduct.region // 特别显示地区字段
|
|
});
|
|
|
|
// 根据新的状态生成适当的返回消息
|
|
let returnMessage = '';
|
|
if (updatedProduct.status === 'pending_review') {
|
|
returnMessage = isResubmit ? '商品编辑成功,已重新提交审核' : '商品编辑成功,等待审核';
|
|
} else if (updatedProduct.status === 'published') {
|
|
returnMessage = '商品编辑成功,已上架';
|
|
} else {
|
|
returnMessage = '商品编辑成功';
|
|
}
|
|
|
|
console.log('=== 编辑商品请求完成 ===');
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: returnMessage,
|
|
product: updatedProduct
|
|
});
|
|
} catch (error) {
|
|
console.error('【错误处理】编辑商品过程发生异常:', error);
|
|
console.error('【错误处理】错误堆栈:', error.stack);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '编辑商品失败: ' + error.message,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// ==================== 入驻相关API ====================
|
|
|
|
// 定义入驻申请数据模型
|
|
const SettlementApplication = sequelize.define('SettlementApplication', {
|
|
id: {
|
|
type: DataTypes.INTEGER,
|
|
primaryKey: true,
|
|
autoIncrement: true
|
|
},
|
|
applicationId: {
|
|
type: DataTypes.STRING,
|
|
unique: true,
|
|
allowNull: false
|
|
},
|
|
userId: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false
|
|
},
|
|
openid: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false
|
|
},
|
|
identityType: {
|
|
type: DataTypes.ENUM('individual', 'enterprise'),
|
|
allowNull: false
|
|
},
|
|
cooperationMode: {
|
|
type: DataTypes.ENUM('supplier', 'buyer', 'both'),
|
|
allowNull: false
|
|
},
|
|
contactName: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false
|
|
},
|
|
contactPhone: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false
|
|
},
|
|
region: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false
|
|
},
|
|
businessLicense: {
|
|
type: DataTypes.TEXT
|
|
},
|
|
animalQuarantine: {
|
|
type: DataTypes.TEXT
|
|
},
|
|
brandAuth: {
|
|
type: DataTypes.TEXT
|
|
},
|
|
status: {
|
|
type: DataTypes.ENUM('pending', 'approved', 'rejected', 'withdrawn'),
|
|
defaultValue: 'pending'
|
|
},
|
|
rejectReason: {
|
|
type: DataTypes.TEXT
|
|
},
|
|
submittedAt: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: DataTypes.NOW
|
|
},
|
|
reviewedAt: {
|
|
type: DataTypes.DATE
|
|
}
|
|
}, {
|
|
tableName: 'settlement_applications',
|
|
timestamps: false
|
|
});
|
|
|
|
// 获取入驻状态
|
|
app.get('/api/settlement/status/:userId', async (req, res) => {
|
|
try {
|
|
const { userId } = req.params;
|
|
|
|
console.log('获取入驻状态请求:', { userId });
|
|
|
|
// 查找用户的入驻申请
|
|
const application = await SettlementApplication.findOne({
|
|
where: { userId },
|
|
order: [['submittedAt', 'DESC']]
|
|
});
|
|
|
|
if (!application) {
|
|
return res.json({
|
|
success: true,
|
|
code: 200,
|
|
data: {
|
|
hasApplication: false,
|
|
status: null,
|
|
message: '暂无入驻申请'
|
|
}
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
data: {
|
|
hasApplication: true,
|
|
applicationId: application.applicationId,
|
|
status: application.status,
|
|
identityType: application.identityType,
|
|
cooperationMode: application.cooperationMode,
|
|
submittedAt: application.submittedAt,
|
|
reviewedAt: application.reviewedAt,
|
|
rejectReason: application.rejectReason,
|
|
message: getStatusMessage(application.status)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('获取入驻状态失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取入驻状态失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 提交入驻申请
|
|
app.post('/api/settlement/submit', async (req, res) => {
|
|
try {
|
|
// 【调试】详细记录API接收到的原始数据
|
|
console.log('===== 入驻API调试开始 =====');
|
|
console.log('1. 原始请求体:', JSON.stringify(req.body, null, 2));
|
|
console.log('2. 请求头:', req.headers);
|
|
console.log('3. 请求方法:', req.method);
|
|
console.log('4. 请求URL:', req.url);
|
|
|
|
const { openid,
|
|
collaborationid,
|
|
cooperation,
|
|
company,
|
|
phoneNumber,
|
|
province,
|
|
city,
|
|
district,
|
|
detailedaddress,
|
|
businesslicenseurl,
|
|
proofurl,
|
|
brandurl
|
|
} = req.body;
|
|
|
|
// 【调试】验证解构后的数据值
|
|
console.log('5. 解构后的数据验证:');
|
|
console.log(' - openid:', openid, typeof openid);
|
|
console.log(' - collaborationid:', collaborationid, typeof collaborationid);
|
|
console.log(' - cooperation:', cooperation, typeof cooperation);
|
|
console.log(' - company:', company, typeof company);
|
|
console.log(' - phoneNumber:', phoneNumber, typeof phoneNumber);
|
|
console.log(' - province:', province, typeof province);
|
|
console.log(' - city:', city, typeof city);
|
|
console.log(' - district:', district, typeof district);
|
|
console.log(' - detailedaddress:', detailedaddress, typeof detailedaddress);
|
|
console.log(' - businesslicenseurl:', businesslicenseurl, typeof businesslicenseurl);
|
|
console.log(' - proofurl:', proofurl, typeof proofurl);
|
|
console.log(' - brandurl:', brandurl, typeof brandurl);
|
|
|
|
// 验证必填字段
|
|
if (!openid || !collaborationid || !cooperation || !company || !phoneNumber || !province || !city || !district) {
|
|
console.error('6. 必填字段验证失败:', {
|
|
openid: !!openid,
|
|
collaborationid: !!collaborationid,
|
|
cooperation: !!cooperation,
|
|
company: !!company,
|
|
phoneNumber: !!phoneNumber,
|
|
province: !!province,
|
|
city: !!city,
|
|
district: !!district
|
|
});
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '请填写完整的申请信息'
|
|
});
|
|
}
|
|
|
|
console.log('6. 必填字段验证通过');
|
|
|
|
// 查找用户信息
|
|
const user = await User.findOne({ where: { openid } });
|
|
console.log('7. 查找用户结果:', user ? {
|
|
userId: user.userId,
|
|
openid: user.openid,
|
|
name: user.name,
|
|
collaborationid: user.collaborationid,
|
|
cooperation: user.cooperation,
|
|
partnerstatus: user.partnerstatus
|
|
} : '未找到用户');
|
|
|
|
if (!user) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '用户不存在'
|
|
});
|
|
}
|
|
|
|
// 检查用户是否已有入驻信息且状态为审核中
|
|
if (user.collaborationid && user.partnerstatus === 'underreview') {
|
|
console.log('8. 用户已有待审核的入驻申请,拒绝重复提交');
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '您已有待审核的入驻申请,请勿重复提交'
|
|
});
|
|
}
|
|
|
|
console.log('8. 用户状态检查通过,允许提交');
|
|
|
|
// 更新用户表中的入驻信息
|
|
// 转换collaborationid为中文(使用明确的英文标识以避免混淆)
|
|
let collaborationidCN = collaborationid;
|
|
const collaborationidMap = {
|
|
'chicken': '鸡场',
|
|
'trader': '贸易商',
|
|
'wholesale': '贸易商'
|
|
};
|
|
|
|
if (collaborationidMap.hasOwnProperty(collaborationid)) {
|
|
collaborationidCN = collaborationidMap[collaborationid];
|
|
} else {
|
|
// 如果传入的不是预期的英文值,直接使用传入值(可能是中文)
|
|
collaborationidCN = collaborationid;
|
|
}
|
|
|
|
console.log('9. collaborationid转换结果:', {
|
|
原值: collaborationid,
|
|
转换后: collaborationidCN,
|
|
映射表: collaborationidMap
|
|
});
|
|
|
|
// 转换cooperation为中文合作模式(使用明确的英文标识以避免混淆)
|
|
let cooperationCN = cooperation;
|
|
const cooperationMap = {
|
|
'resource_delegation': '资源委托',
|
|
'self_define_sales': '自主定义销售',
|
|
'regional_exclusive': '区域包场合作',
|
|
'other': '其他',
|
|
'wholesale': '资源委托',
|
|
'self_define': '自主定义销售',
|
|
// 添加中文值映射(前端直接传递中文值)
|
|
'代销业务': '代销业务',
|
|
'采销联盟合作': '采销联盟合作',
|
|
'包场合作': '包场合作'
|
|
};
|
|
|
|
if (cooperationMap.hasOwnProperty(cooperation)) {
|
|
cooperationCN = cooperationMap[cooperation];
|
|
} else {
|
|
// 如果传入的不是预期的英文值,直接使用传入值(可能是中文)
|
|
cooperationCN = cooperation;
|
|
}
|
|
|
|
console.log('10. cooperation转换结果:', {
|
|
原值: cooperation,
|
|
转换后: cooperationCN,
|
|
映射表: cooperationMap
|
|
});
|
|
|
|
// 构建更新数据对象,确保字段名称与数据库表结构完全匹配
|
|
// 特别注意:数据库中以下字段为NOT NULL约束
|
|
const updateData = {
|
|
nickName: user.nickName || String(company || '未知联系人'), // 数据库NOT NULL: 联系人
|
|
collaborationid: String(collaborationidCN || '未选择'), // 数据库NOT NULL: 合作商身份
|
|
cooperation: String(cooperationCN || '未选择'), // 数据库NOT NULL: 合作模式
|
|
company: String(company || ''), // 公司名称
|
|
phoneNumber: String(phoneNumber || ''), // 电话号码
|
|
province: String(province || ''), // 数据库NOT NULL: 省份
|
|
city: String(city || ''), // 数据库NOT NULL: 城市
|
|
district: String(district || ''), // 数据库NOT NULL: 区域
|
|
detailedaddress: String(detailedaddress || ''), // 详细地址
|
|
businesslicenseurl: String(!businesslicenseurl || businesslicenseurl.trim() === '' ? '未上传' : businesslicenseurl), // 数据库NOT NULL: 营业执照
|
|
proofurl: String(!proofurl || proofurl.trim() === '' ? '未上传' : proofurl), // 数据库NOT NULL: 证明材料
|
|
brandurl: String(!brandurl || brandurl.trim() === '' ? '' : brandurl), // 品牌授权链文件(可为空)
|
|
partnerstatus: 'underreview', // 合作商状态明确设置为审核中
|
|
updated_at: getBeijingTime(),
|
|
newtime: getBeijingTime() // 新增字段:提交时间
|
|
};
|
|
|
|
console.log('11. 构建的updateData对象:', JSON.stringify(updateData, null, 2));
|
|
console.log('12. 准备执行数据库更新,openid:', openid);
|
|
|
|
// 【调试】在更新前记录当前数据库状态
|
|
const beforeUpdateUser = await User.findOne({ where: { openid: openid } });
|
|
console.log('13. 更新前数据库状态:', beforeUpdateUser ? {
|
|
userId: beforeUpdateUser.userId,
|
|
openid: beforeUpdateUser.openid,
|
|
collaborationid: beforeUpdateUser.collaborationid,
|
|
cooperation: beforeUpdateUser.cooperation,
|
|
company: beforeUpdateUser.company,
|
|
phoneNumber: beforeUpdateUser.phoneNumber,
|
|
province: beforeUpdateUser.province,
|
|
city: beforeUpdateUser.city,
|
|
district: beforeUpdateUser.district,
|
|
detailedaddress: beforeUpdateUser.detailedaddress,
|
|
businesslicenseurl: beforeUpdateUser.businesslicenseurl,
|
|
proofurl: beforeUpdateUser.proofurl,
|
|
brandurl: beforeUpdateUser.brandurl,
|
|
partnerstatus: beforeUpdateUser.partnerstatus,
|
|
updated_at: beforeUpdateUser.updated_at
|
|
} : '未找到用户记录');
|
|
|
|
// 使用Sequelize的update方法更新数据
|
|
const updateResult = await User.update(updateData, {
|
|
where: { openid: openid }
|
|
});
|
|
|
|
console.log('14. Sequelize更新操作结果:', {
|
|
affectedRows: updateResult[0],
|
|
affectedCount: updateResult[1],
|
|
成功: updateResult[0] > 0
|
|
});
|
|
|
|
if (updateResult[0] === 0) {
|
|
console.error('15. 更新失败,未找到匹配的用户记录或没有更新任何字段');
|
|
return res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '更新用户信息失败,未找到匹配的记录'
|
|
});
|
|
}
|
|
|
|
// 【调试】在更新后记录数据库状态
|
|
const afterUpdateUser = await User.findOne({ where: { openid: openid } });
|
|
console.log('16. 更新后数据库状态:', afterUpdateUser ? {
|
|
userId: afterUpdateUser.userId,
|
|
openid: afterUpdateUser.openid,
|
|
collaborationid: afterUpdateUser.collaborationid,
|
|
cooperation: afterUpdateUser.cooperation,
|
|
company: afterUpdateUser.company,
|
|
phoneNumber: afterUpdateUser.phoneNumber,
|
|
province: afterUpdateUser.province,
|
|
city: afterUpdateUser.city,
|
|
district: afterUpdateUser.district,
|
|
detailedaddress: afterUpdateUser.detailedaddress,
|
|
businesslicenseurl: afterUpdateUser.businesslicenseurl,
|
|
proofurl: afterUpdateUser.proofurl,
|
|
brandurl: afterUpdateUser.brandurl,
|
|
partnerstatus: afterUpdateUser.partnerstatus,
|
|
updated_at: afterUpdateUser.updated_at
|
|
} : '未找到用户记录');
|
|
|
|
// 【调试】对比更新前后的差异
|
|
if (beforeUpdateUser && afterUpdateUser) {
|
|
console.log('17. 更新前后数据对比:');
|
|
const changedFields = {};
|
|
Object.keys(updateData).forEach(field => {
|
|
const beforeValue = beforeUpdateUser[field];
|
|
const afterValue = afterUpdateUser[field];
|
|
if (beforeValue !== afterValue) {
|
|
changedFields[field] = {
|
|
更新前: beforeValue,
|
|
更新后: afterValue
|
|
};
|
|
}
|
|
});
|
|
console.log('18. 实际发生变化的字段:', changedFields);
|
|
}
|
|
|
|
// 验证更新是否成功
|
|
const updatedUser = await User.findOne({ where: { openid: openid } });
|
|
console.log('19. 验证更新后的用户状态:', updatedUser ? updatedUser.partnerstatus : '未找到用户');
|
|
|
|
// 双重确认:如果状态仍不是underreview,再次更新
|
|
if (updatedUser && updatedUser.partnerstatus !== 'underreview') {
|
|
console.warn('20. 检测到状态未更新正确,执行二次更新,当前状态:', updatedUser.partnerstatus);
|
|
await User.update({
|
|
partnerstatus: 'underreview'
|
|
}, {
|
|
where: { openid: openid }
|
|
});
|
|
|
|
// 【调试】二次更新后的状态
|
|
const finalUpdateUser = await User.findOne({ where: { openid: openid } });
|
|
console.log('21. 二次更新后的用户状态:', finalUpdateUser ? finalUpdateUser.partnerstatus : '未找到用户');
|
|
}
|
|
|
|
console.log('22. 用户入驻信息更新成功,用户ID:', user.userId);
|
|
console.log('===== 入驻API调试结束 =====');
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '入驻申请提交成功,请等待审核',
|
|
data: {
|
|
status: 'pending'
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('提交入驻申请失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '提交入驻申请失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 上传入驻文件
|
|
app.post('/api/settlement/upload', upload.single('file'), async (req, res) => {
|
|
let tempFilePath = null;
|
|
try {
|
|
const { openid, fileType } = req.body;
|
|
|
|
console.log('收到入驻文件上传请求:', { openid, fileType });
|
|
|
|
if (!openid || !fileType) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '参数不完整'
|
|
});
|
|
}
|
|
|
|
if (!req.file) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '未接收到文件'
|
|
});
|
|
}
|
|
|
|
tempFilePath = req.file.path;
|
|
|
|
// 上传文件到OSS - 使用静态方法调用
|
|
// 注意:OssUploader.uploadFile直接返回URL字符串,而不是包含url属性的对象
|
|
const fileUrl = await OssUploader.uploadFile(tempFilePath, `settlement/${fileType}/${Date.now()}_${req.file.originalname}`);
|
|
|
|
|
|
|
|
// 确保返回的URL是干净的字符串,移除可能存在的反引号和空格
|
|
const cleanFileUrl = String(fileUrl).replace(/[` ]/g, '');
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '文件上传成功',
|
|
data: {
|
|
fileUrl: cleanFileUrl,
|
|
fileType: fileType
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('入驻文件上传失败:', error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '文件上传失败: ' + error.message
|
|
});
|
|
} finally {
|
|
// 确保临时文件被清理
|
|
if (tempFilePath && fs.existsSync(tempFilePath)) {
|
|
try {
|
|
fs.unlinkSync(tempFilePath);
|
|
console.log('临时文件已清理:', tempFilePath);
|
|
} catch (cleanupError) {
|
|
console.warn('清理临时文件时出错:', cleanupError);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 撤回入驻申请
|
|
app.post('/api/settlement/withdraw', async (req, res) => {
|
|
try {
|
|
const { openid } = req.body;
|
|
|
|
console.log('撤回入驻申请请求:', { openid });
|
|
|
|
// 查找用户
|
|
const user = await User.findOne({ where: { openid } });
|
|
if (!user) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '用户不存在'
|
|
});
|
|
}
|
|
|
|
// 新的业务逻辑:只要partnerstatus为underreview,就表示有待审核的申请
|
|
// 不再依赖notice字段来确认申请ID
|
|
if (user.partnerstatus !== 'underreview') {
|
|
// 打印详细信息帮助调试
|
|
console.log('入驻申请检查失败:', {
|
|
partnerstatus: user.partnerstatus,
|
|
message: '用户未处于审核中状态'
|
|
});
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '未找到申请记录'
|
|
});
|
|
}
|
|
|
|
// 更新用户状态为未提交
|
|
await User.update({
|
|
notice: null, // 清除notice字段
|
|
partnerstatus: '' // 清空合作商状态
|
|
}, { where: { openid } });
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '入驻申请已撤回'
|
|
});
|
|
} catch (error) {
|
|
console.error('撤回入驻申请失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '撤回入驻申请失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 重新提交入驻申请
|
|
app.post('/api/settlement/resubmit/:applicationId', async (req, res) => {
|
|
try {
|
|
const { applicationId } = req.params;
|
|
const { openid } = req.body;
|
|
|
|
console.log('重新提交入驻申请请求:', { applicationId, openid });
|
|
|
|
// 查找用户
|
|
const user = await User.findOne({ where: { openid } });
|
|
if (!user) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '用户不存在'
|
|
});
|
|
}
|
|
|
|
// 查找申请
|
|
const application = await SettlementApplication.findOne({
|
|
where: {
|
|
applicationId,
|
|
userId: user.userId
|
|
}
|
|
});
|
|
|
|
if (!application) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '入驻申请不存在'
|
|
});
|
|
}
|
|
|
|
if (application.status !== 'rejected') {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '只能重新提交被拒绝的申请'
|
|
});
|
|
}
|
|
|
|
// 更新状态为待审核
|
|
await application.update({
|
|
status: 'pending',
|
|
rejectReason: null,
|
|
reviewedAt: null
|
|
});
|
|
|
|
// 将用户的partnerstatus设置为Null
|
|
await user.update({
|
|
partnerstatus: null,
|
|
reason: null
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '入驻申请已重新提交,请等待审核'
|
|
});
|
|
} catch (error) {
|
|
console.error('重新提交入驻申请失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '重新提交入驻申请失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 辅助函数:获取状态消息
|
|
function getStatusMessage(status) {
|
|
const statusMessages = {
|
|
'pending': '待审核',
|
|
'approved': '已通过',
|
|
'rejected': '已拒绝',
|
|
'withdrawn': '已撤回'
|
|
};
|
|
return statusMessages[status] || status;
|
|
}
|
|
|
|
// 导入并执行商品联系人更新函数
|
|
const updateProductContacts = require('./update-product-contacts');
|
|
const { time } = require('console');
|
|
|
|
// 添加API接口:更新商品联系人信息
|
|
app.post('/api/products/update-contacts', async (req, res) => {
|
|
try {
|
|
await updateProductContacts();
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '商品联系人信息更新成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('更新商品联系人信息失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '更新商品联系人信息失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 添加API接口:快速更新商品价格和联系人信息(不验证sellerId)
|
|
app.post('/api/products/quick-update', async (req, res) => {
|
|
console.log('【快速更新】收到请求:', req.body);
|
|
try {
|
|
const { productId, price, product_contact, contact_phone, description, bargaining } = req.body;
|
|
|
|
if (!productId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少productId参数'
|
|
});
|
|
}
|
|
|
|
// 查找商品
|
|
const product = await Product.findOne({
|
|
where: { productId }
|
|
});
|
|
|
|
if (!product) {
|
|
console.error('【快速更新】商品不存在:', productId);
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '商品不存在'
|
|
});
|
|
}
|
|
|
|
console.log('【快速更新】找到商品:', product.productName);
|
|
|
|
// 只更新指定的字段
|
|
const updateFields = {
|
|
updated_at: getBeijingTime()
|
|
};
|
|
|
|
if (price !== undefined) {
|
|
updateFields.price = price;
|
|
console.log('【快速更新】更新价格:', price);
|
|
}
|
|
|
|
if (product_contact !== undefined) {
|
|
updateFields.product_contact = product_contact;
|
|
console.log('【快速更新】更新联系人:', product_contact);
|
|
}
|
|
|
|
if (contact_phone !== undefined) {
|
|
updateFields.contact_phone = contact_phone;
|
|
console.log('【快速更新】更新联系电话:', contact_phone);
|
|
}
|
|
|
|
if (description !== undefined) {
|
|
updateFields.description = description;
|
|
console.log('【快速更新】更新货源描述:', description);
|
|
}
|
|
|
|
if (bargaining !== undefined) {
|
|
updateFields.bargaining = bargaining;
|
|
console.log('【快速更新】更新讲价状态:', bargaining);
|
|
}
|
|
|
|
// 执行更新
|
|
await Product.update(updateFields, {
|
|
where: { productId }
|
|
});
|
|
|
|
console.log('【快速更新】商品更新成功:', productId);
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '更新成功',
|
|
data: {
|
|
productId,
|
|
price: updateFields.price,
|
|
product_contact: updateFields.product_contact,
|
|
contact_phone: updateFields.contact_phone
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('【快速更新】更新商品失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '更新商品失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 添加API接口:更新产品日志
|
|
app.post('/api/products/update-log', async (req, res) => {
|
|
console.log('【更新产品日志】收到请求:', req.body);
|
|
try {
|
|
const { productId, product_log } = req.body;
|
|
|
|
if (!productId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少productId参数'
|
|
});
|
|
}
|
|
|
|
// 查找商品
|
|
const product = await Product.findOne({
|
|
where: { productId }
|
|
});
|
|
|
|
if (!product) {
|
|
console.error('【更新产品日志】商品不存在:', productId);
|
|
return res.status(404).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '商品不存在'
|
|
});
|
|
}
|
|
|
|
console.log('【更新产品日志】找到商品:', product.productName);
|
|
|
|
// 更新产品日志
|
|
await Product.update({
|
|
product_log: product_log,
|
|
updated_at: getBeijingTime()
|
|
}, {
|
|
where: { productId }
|
|
});
|
|
|
|
console.log('【更新产品日志】商品日志更新成功:', productId);
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '日志更新成功',
|
|
data: {
|
|
productId
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('【更新产品日志】更新商品日志失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '更新商品日志失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// REST API接口 - 获取用户会话列表
|
|
app.get('/api/conversations/user/:userId', async (req, res) => {
|
|
try {
|
|
const userId = req.params.userId;
|
|
const conversations = await getUserConversations(userId);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
data: conversations
|
|
});
|
|
} catch (error) {
|
|
console.error('获取用户会话列表失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取会话列表失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// REST API接口 - 获取客服会话列表
|
|
app.get('/api/conversations/manager/:managerId', async (req, res) => {
|
|
try {
|
|
const managerId = req.params.managerId;
|
|
const conversations = await getManagerConversations(managerId);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
data: conversations
|
|
});
|
|
} catch (error) {
|
|
console.error('获取客服会话列表失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取会话列表失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// REST API接口 - 获取会话历史消息
|
|
app.get('/api/conversations/:conversationId/messages', async (req, res) => {
|
|
try {
|
|
const conversationId = req.params.conversationId;
|
|
const page = parseInt(req.query.page) || 1;
|
|
const limit = parseInt(req.query.limit) || 50;
|
|
const offset = (page - 1) * limit;
|
|
|
|
const [messages] = await sequelize.query(
|
|
`SELECT * FROM chat_messages
|
|
WHERE conversation_id = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?`,
|
|
{ replacements: [conversationId, limit, offset] }
|
|
);
|
|
|
|
// 反转顺序,使最早的消息在前
|
|
messages.reverse();
|
|
|
|
// 获取消息总数
|
|
const [[totalCount]] = await sequelize.query(
|
|
'SELECT COUNT(*) as count FROM chat_messages WHERE conversation_id = ?',
|
|
{ replacements: [conversationId] }
|
|
);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
data: {
|
|
messages,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total: totalCount.count,
|
|
totalPages: Math.ceil(totalCount.count / limit)
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('获取历史消息失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取历史消息失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// REST API接口 - 获取聊天记录(兼容客户端接口)
|
|
app.post('/api/chat/messages', async (req, res) => {
|
|
try {
|
|
const { chat_id, user_phone, before, limit = 50 } = req.body;
|
|
|
|
console.log('收到聊天记录请求:', { chat_id, user_phone, before, limit });
|
|
|
|
let query = `SELECT * FROM chat_messages
|
|
WHERE (sender_phone = ? AND receiver_phone = ?)
|
|
OR (sender_phone = ? AND receiver_phone = ?)`;
|
|
let replacements = [user_phone, chat_id, chat_id, user_phone];
|
|
|
|
// 如果有before参数,添加时间过滤
|
|
if (before) {
|
|
query += ' AND created_at < ?';
|
|
replacements.push(before);
|
|
}
|
|
|
|
// 按时间排序,最新的消息在前
|
|
query += ' ORDER BY created_at DESC LIMIT ?';
|
|
replacements.push(parseInt(limit));
|
|
|
|
console.log('执行SQL:', query);
|
|
console.log('替换参数:', replacements);
|
|
|
|
const [messages] = await sequelize.query(query, { replacements });
|
|
|
|
// 反转顺序,使最早的消息在前
|
|
messages.reverse();
|
|
|
|
console.log('查询到的消息数量:', messages.length);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
data: messages
|
|
});
|
|
} catch (error) {
|
|
console.error('获取聊天记录失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取聊天记录失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// REST API接口 - 发送聊天消息
|
|
app.post('/api/chat/send', async (req, res) => {
|
|
try {
|
|
const { sender_phone, receiver_phone, content } = req.body;
|
|
|
|
console.log('收到发送消息请求:', { sender_phone, receiver_phone, content });
|
|
|
|
// 验证必填字段
|
|
if (!sender_phone || !receiver_phone || !content) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '发送者电话、接收者电话和消息内容不能为空'
|
|
});
|
|
}
|
|
|
|
// 插入消息到数据库
|
|
const query = `INSERT INTO chat_messages (sender_phone, receiver_phone, content, created_at)
|
|
VALUES (?, ?, ?, NOW())`;
|
|
const replacements = [sender_phone, receiver_phone, content];
|
|
|
|
console.log('执行SQL:', query);
|
|
console.log('替换参数:', replacements);
|
|
|
|
const [result] = await sequelize.query(query, { replacements });
|
|
|
|
console.log('消息发送成功,插入结果:', result);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '消息发送成功',
|
|
data: {
|
|
id: result.insertId,
|
|
sender_phone,
|
|
receiver_phone,
|
|
content,
|
|
created_at: new Date().toISOString()
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('发送消息失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '发送消息失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// REST API接口 - 标记消息已读
|
|
app.post('/api/conversations/:conversationId/read', async (req, res) => {
|
|
try {
|
|
const conversationId = req.params.conversationId;
|
|
const { userId, managerId, type } = req.body;
|
|
|
|
const now = getBeijingTime();
|
|
let updateField;
|
|
|
|
if (type === 'user') {
|
|
// 用户标记客服消息为已读
|
|
updateField = 'unread_count';
|
|
await sequelize.query(
|
|
'UPDATE chat_messages SET is_read = 1, read_time = ? WHERE conversation_id = ? AND sender_type = 2',
|
|
{ replacements: [now, conversationId] }
|
|
);
|
|
} else if (type === 'manager') {
|
|
// 客服标记用户消息为已读
|
|
updateField = 'cs_unread_count';
|
|
await sequelize.query(
|
|
'UPDATE chat_messages SET is_read = 1, read_time = ? WHERE conversation_id = ? AND sender_type = 1',
|
|
{ replacements: [now, conversationId] }
|
|
);
|
|
} else {
|
|
throw new Error('无效的类型');
|
|
}
|
|
|
|
// 重置未读计数
|
|
await sequelize.query(
|
|
`UPDATE chat_conversations SET ${updateField} = 0 WHERE conversation_id = ?`,
|
|
{ replacements: [conversationId] }
|
|
);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '已标记为已读'
|
|
});
|
|
} catch (error) {
|
|
console.error('标记已读失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '标记已读失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// REST API接口 - 获取在线统计信息
|
|
app.get('/api/online-stats', async (req, res) => {
|
|
try {
|
|
const stats = getOnlineStats();
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
data: stats
|
|
});
|
|
} catch (error) {
|
|
console.error('获取在线统计失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取在线统计失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
// REST API接口 - 获取客服列表 - 支持按角色类型查询(销售员/采购员)
|
|
app.get('/api/managers', async (req, res) => {
|
|
try {
|
|
// 获取请求参数中的角色类型(seller=销售员,buyer=采购员)
|
|
const { type } = req.query;
|
|
console.log('获取客服列表请求,类型:', type);
|
|
|
|
// 根据角色类型确定查询条件
|
|
let whereClause = '';
|
|
let replacements = [];
|
|
|
|
if (type === 'buyer') {
|
|
// 如果类型为buyer,查询所有与采购相关的职位
|
|
whereClause = 'WHERE (projectName = ? OR projectName = ? OR projectName = ?) AND phoneNumber IS NOT NULL AND phoneNumber != ""';
|
|
replacements = ['采购', '采购员', '采购主管'];
|
|
} else {
|
|
// 默认查询所有与销售相关的职位
|
|
whereClause = 'WHERE (projectName = ? OR projectName = ?) AND phoneNumber IS NOT NULL AND phoneNumber != ""';
|
|
replacements = ['销售', '销售员'];
|
|
}
|
|
|
|
// 查询userlogin数据库中的personnel表,获取指定工位且有电话号码的用户
|
|
// 根据表结构选择所有需要的字段,包括新增的字段
|
|
const [personnelData] = await sequelize.query(
|
|
`SELECT id, managerId, managercompany, managerdepartment, organization, projectName, name, alias, phoneNumber, avatarUrl, responsible_area, label, egg_section, information FROM userlogin.personnel ${whereClause} ORDER BY id ASC`,
|
|
{ replacements: replacements }
|
|
);
|
|
|
|
// 查询chat_online_status表获取客服在线状态(添加错误处理)
|
|
let onlineStatusData = [];
|
|
try {
|
|
const [statusResults] = await sequelize.query(
|
|
'SELECT userId, is_online FROM chat_online_status WHERE type = 2', // type=2表示客服
|
|
{ type: sequelize.QueryTypes.SELECT }
|
|
);
|
|
onlineStatusData = statusResults || [];
|
|
} catch (statusError) {
|
|
console.warn('获取在线状态失败(可能是表不存在):', statusError.message);
|
|
// 表不存在或查询失败时,使用空数组
|
|
onlineStatusData = [];
|
|
}
|
|
|
|
// 创建在线状态映射
|
|
const onlineStatusMap = {};
|
|
// 检查onlineStatusData是否存在,防止undefined调用forEach
|
|
if (Array.isArray(onlineStatusData)) {
|
|
onlineStatusData.forEach(status => {
|
|
onlineStatusMap[status.userId] = status.is_online === 1;
|
|
});
|
|
}
|
|
|
|
// 将获取的数据映射为前端需要的格式,添加online状态(综合考虑内存中的onlineManagers和数据库中的状态)
|
|
const isManagerOnline = (id, managerId) => {
|
|
// 转换ID为字符串以便正确比较
|
|
const stringId = String(id);
|
|
const stringManagerId = managerId ? String(managerId) : null;
|
|
|
|
console.log(`检查客服在线状态: id=${id}(${stringId}), managerId=${managerId}(${stringManagerId})`);
|
|
|
|
// 首先从内存中的onlineManagers检查(实时性更好)
|
|
if (onlineManagers && typeof onlineManagers.has === 'function') {
|
|
// 检查id或managerId是否在onlineManagers中
|
|
if (onlineManagers.has(stringId) || (stringManagerId && onlineManagers.has(stringManagerId))) {
|
|
console.log(`客服在线(内存检查): id=${id}`);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// 其次从数据库查询结果检查
|
|
const dbStatus = onlineStatusMap[stringId] || (stringManagerId && onlineStatusMap[stringManagerId]) || false;
|
|
console.log(`客服在线状态(数据库): id=${id}, status=${dbStatus}`);
|
|
return dbStatus;
|
|
};
|
|
|
|
const managers = personnelData.map((person, index) => ({
|
|
id: person.id,
|
|
managerId: person.managerId || `PM${String(index + 1).padStart(3, '0')}`,
|
|
managercompany: person.managercompany || '未知公司',
|
|
managerdepartment: person.managerdepartment || '采购部',
|
|
organization: person.organization || '采购组',
|
|
projectName: person.projectName || '采购员',
|
|
name: person.name || '未知',
|
|
alias: person.alias || person.name || '未知',
|
|
phoneNumber: person.phoneNumber || '',
|
|
avatar: person.avatarUrl || '', // 使用表中的avatarUrl字段
|
|
avatarUrl: person.avatarUrl || '', // 兼容前端的avatarUrl字段
|
|
egg_section: person.egg_section || '', // 添加鸡蛋分字段
|
|
responsible_area: person.responsible_area || '', // 添加负责区域字段
|
|
label: person.label || '', // 添加标签字段
|
|
information: person.information || '', // 添加个人信息字段
|
|
online: isManagerOnline(person.id, person.managerId) // 综合检查在线状态
|
|
}));
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
data: managers
|
|
});
|
|
} catch (error) {
|
|
console.error('获取客服列表失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取客服列表失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
|
|
// 认证处理函数
|
|
async function handleAuth(ws, data) {
|
|
// 详细日志记录原始认证数据
|
|
console.log('📱 收到认证请求:', JSON.stringify(data));
|
|
|
|
// 优化认证数据提取,支持多种格式
|
|
// 1. 直接从data中提取(最简单的格式)
|
|
let managerId = data.managerId;
|
|
let userId = data.userId;
|
|
let type = data.type;
|
|
let userType = data.userType; // 明确提取userType字段
|
|
|
|
// 2. 如果没有找到,尝试从data.data中提取
|
|
if (!managerId && !userId && data.data) {
|
|
managerId = data.data.managerId;
|
|
userId = data.data.userId;
|
|
type = data.data.type;
|
|
userType = data.data.userType || data.data.type; // 从data.data中提取userType
|
|
console.log('🔄 从data.data中提取认证信息');
|
|
}
|
|
|
|
// 3. 兼容之前的payload逻辑
|
|
const payload = data.data || data;
|
|
if (!managerId) managerId = payload.managerId;
|
|
if (!userId) userId = payload.userId;
|
|
if (!type) type = payload.type;
|
|
if (!userType) userType = payload.userType || payload.type || 'unknown'; // 确保userType有值
|
|
|
|
// 字符串化ID以确保类型一致性
|
|
if (userId) userId = String(userId).trim();
|
|
if (managerId) managerId = String(managerId).trim();
|
|
|
|
console.log('🔍 最终提取的认证信息:', {
|
|
managerId,
|
|
userId,
|
|
type,
|
|
userType,
|
|
hasManagerId: !!managerId,
|
|
hasUserId: !!userId,
|
|
hasType: !!type,
|
|
hasUserType: !!userType
|
|
});
|
|
|
|
// 确定最终的用户类型 - userType优先级高于type
|
|
const finalUserType = String(userType).toLowerCase();
|
|
|
|
const deviceInfo = payload.deviceInfo || {};
|
|
const connection = connections.get(ws.connectionId);
|
|
|
|
if (!connection) {
|
|
ws.send(JSON.stringify({
|
|
type: 'auth_error',
|
|
message: '连接已断开'
|
|
}));
|
|
return;
|
|
}
|
|
|
|
console.log(`🔍 最终用户类型判断: finalUserType=${finalUserType}`);
|
|
|
|
// 验证用户或客服身份 - 使用finalUserType进行判断,不再重复定义userType
|
|
if ((finalUserType === 'user' || finalUserType.includes('customer')) && userId) {
|
|
// 改进的用户认证逻辑,支持字符串ID并增加容错性
|
|
try {
|
|
// 首先尝试数据库验证
|
|
let userExists = false;
|
|
try {
|
|
const [existingUsers] = await sequelize.query(
|
|
'SELECT userId FROM users WHERE userId = ? LIMIT 1',
|
|
{ replacements: [userId] }
|
|
);
|
|
userExists = existingUsers && existingUsers.length > 0;
|
|
|
|
if (userExists) {
|
|
console.log(`✅ 用户ID验证成功: userId=${userId} 存在于数据库中`);
|
|
} else {
|
|
console.log(`ℹ️ 用户ID在数据库中不存在: userId=${userId},但仍然允许连接`);
|
|
}
|
|
} catch (dbError) {
|
|
console.warn(`⚠️ 用户数据库验证失败,但继续处理认证: ${dbError.message}`);
|
|
}
|
|
|
|
// 查询用户是否在personnel表中存在,以确定managerId
|
|
try {
|
|
const [personnelData] = await sequelize.query(
|
|
'SELECT id FROM userlogin.personnel WHERE userId = ? LIMIT 1',
|
|
{ replacements: [userId] }
|
|
);
|
|
|
|
if (personnelData && personnelData.length > 0) {
|
|
console.log(`✅ 用户在personnel表中存在,managerId=${personnelData[0].id}`);
|
|
} else {
|
|
console.log(`ℹ️ 用户不在personnel表中,为普通用户`);
|
|
}
|
|
} catch (personnelError) {
|
|
console.warn(`⚠️ Personnel表查询失败,但继续处理认证: ${personnelError.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ 用户验证过程中发生严重错误:', error);
|
|
// 即使出错也尝试继续,只记录错误不中断认证
|
|
}
|
|
|
|
// 继续设置连接信息,确保使用字符串ID
|
|
connection.userId = userId;
|
|
connection.isUser = true;
|
|
connection.userType = 'user';
|
|
onlineUsers.set(userId, ws);
|
|
|
|
// 尝试更新在线状态,但不中断认证流程
|
|
try {
|
|
await updateUserOnlineStatus(userId, 1);
|
|
await updateChatOnlineStatus(userId, 1, ws.connectionId, deviceInfo);
|
|
} catch (statusError) {
|
|
console.warn(`⚠️ 更新在线状态失败,但认证继续: ${statusError.message}`);
|
|
}
|
|
|
|
// 发送认证成功消息
|
|
ws.send(JSON.stringify({
|
|
type: 'auth_success',
|
|
payload: { userId, type: 'user' }
|
|
}));
|
|
|
|
console.log(`✅ 用户认证成功: userId=${userId}, userType=${finalUserType} (已标准化为'user')`);
|
|
} else if (finalUserType === 'manager' || finalUserType.includes('customer_service')) {
|
|
// 客服认证逻辑改进,增加容错性
|
|
let stringManagerId;
|
|
if (managerId) {
|
|
stringManagerId = String(managerId).trim();
|
|
} else if (userId) {
|
|
// 如果没有提供managerId但提供了userId,尝试使用userId作为managerId
|
|
stringManagerId = String(userId).trim();
|
|
console.log(`⚠️ 客服认证使用userId作为managerId: ${stringManagerId}`);
|
|
} else {
|
|
// 缺少必要的managerId或userId
|
|
ws.send(JSON.stringify({
|
|
type: 'auth_error',
|
|
message: '客服认证失败:缺少必要的managerId或userId'
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// 检查managerId是否在personnel表中存在,增加容错机制
|
|
try {
|
|
let managerExists = false;
|
|
try {
|
|
const [existingManagers] = await sequelize.query(
|
|
'SELECT id FROM userlogin.personnel WHERE id = ? LIMIT 1',
|
|
{ replacements: [stringManagerId] }
|
|
);
|
|
|
|
managerExists = existingManagers && existingManagers.length > 0;
|
|
|
|
if (!managerExists) {
|
|
console.warn(`⚠️ 客服ID在personnel表中不存在: managerId=${stringManagerId}`);
|
|
// 不再直接拒绝,而是允许继续连接但记录警告
|
|
}
|
|
} catch (dbError) {
|
|
console.warn(`⚠️ 客服数据库验证失败,但继续处理认证: ${dbError.message}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ 客服验证过程中发生严重错误:', error);
|
|
// 即使出错也尝试继续,只记录错误不中断认证
|
|
}
|
|
|
|
connection.managerId = stringManagerId;
|
|
connection.isManager = true;
|
|
connection.userType = 'manager'; // 添加userType字段确保与其他函数兼容性
|
|
|
|
// 检查并记录添加前的状态
|
|
console.log(`📝 添加客服前onlineManagers状态: has(${stringManagerId})=${onlineManagers.has(stringManagerId)}`);
|
|
|
|
onlineManagers.set(stringManagerId, ws);
|
|
await updateManagerOnlineStatus(stringManagerId, 1);
|
|
// 更新chat_online_status表
|
|
await updateChatOnlineStatus(stringManagerId, 2, ws.connectionId, deviceInfo);
|
|
|
|
// 发送认证成功消息 - 使用字符串化的managerId确保一致
|
|
ws.send(JSON.stringify({
|
|
type: 'auth_success',
|
|
payload: { managerId: stringManagerId, type: 'manager' }
|
|
}));
|
|
|
|
console.log(`✅ 客服认证成功: managerId=${stringManagerId}, userType=${finalUserType} (已标准化为'manager')`);
|
|
|
|
// 添加onlineManagers内容检查日志
|
|
console.log(`客服 ${stringManagerId} 已连接,onlineManagers键:${Array.from(onlineManagers.keys()).join(', ')}`);
|
|
console.log(`onlineManagers.has('22') = ${onlineManagers.has('22')}`);
|
|
} else {
|
|
// 无效的认证信息
|
|
ws.send(JSON.stringify({
|
|
type: 'auth_error',
|
|
message: '无效的认证信息'
|
|
}));
|
|
}
|
|
}
|
|
|
|
// 更新chat_online_status表
|
|
async function updateChatOnlineStatus(userId, type, socketId, deviceInfo) {
|
|
try {
|
|
const now = getBeijingTime();
|
|
// 使用INSERT ... ON DUPLICATE KEY UPDATE语法确保只更新或插入一条记录
|
|
await sequelize.query(
|
|
`INSERT INTO chat_online_status
|
|
(userId, type, socket_id, is_online, last_heartbeat, device_info, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
socket_id = VALUES(socket_id),
|
|
is_online = VALUES(is_online),
|
|
last_heartbeat = VALUES(last_heartbeat),
|
|
device_info = VALUES(device_info),
|
|
updated_at = VALUES(updated_at)`,
|
|
{
|
|
replacements: [
|
|
userId,
|
|
type, // 1:普通用户 2:客服
|
|
socketId,
|
|
1, // 在线状态
|
|
now,
|
|
JSON.stringify(deviceInfo),
|
|
now,
|
|
now
|
|
]
|
|
}
|
|
);
|
|
console.log(`更新chat_online_status成功: userId=${userId}, type=${type}, socketId=${socketId}`);
|
|
} catch (error) {
|
|
console.error('更新chat_online_status失败:', error);
|
|
}
|
|
}
|
|
|
|
// 更新用户在线状态
|
|
async function updateUserOnlineStatus(userId, status) {
|
|
try {
|
|
// 更新chat_conversations表中用户的在线状态
|
|
await sequelize.query(
|
|
'UPDATE chat_conversations SET user_online = ? WHERE userId = ?',
|
|
{ replacements: [status, userId] }
|
|
);
|
|
|
|
// 通知相关客服用户状态变化
|
|
const conversations = await sequelize.query(
|
|
'SELECT DISTINCT managerId FROM chat_conversations WHERE userId = ?',
|
|
{ replacements: [userId] }
|
|
);
|
|
|
|
conversations[0].forEach(conv => {
|
|
const managerWs = onlineManagers.get(conv.managerId);
|
|
if (managerWs) {
|
|
managerWs.send(JSON.stringify({
|
|
type: 'user_status_change',
|
|
payload: { userId, online: status === 1 }
|
|
}));
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('更新用户在线状态失败:', error);
|
|
}
|
|
}
|
|
|
|
// 更新客服在线状态
|
|
async function updateManagerOnlineStatus(managerId, status) {
|
|
try {
|
|
// 更新chat_conversations表中客服的在线状态
|
|
await sequelize.query(
|
|
'UPDATE chat_conversations SET cs_online = ? WHERE managerId = ?',
|
|
{ replacements: [status, managerId] }
|
|
);
|
|
|
|
// 同步更新客服表中的在线状态 - 注释掉因为online字段不存在
|
|
// await sequelize.query(
|
|
// 'UPDATE userlogin.personnel SET online = ? WHERE id = ?',
|
|
// { replacements: [status, managerId] }
|
|
// );
|
|
|
|
// 检查并更新用户表中的type字段,确保客服用户类型为manager
|
|
// 使用独立的数据源连接进行跨库操作
|
|
try {
|
|
// 1. 使用userLoginSequelize查询personnel表获取电话号码
|
|
console.log(`正在查询客服信息: managerId=${managerId}`);
|
|
const personnelResult = await userLoginSequelize.query(
|
|
'SELECT phoneNumber FROM personnel WHERE id = ? OR managerId = ?',
|
|
{ replacements: [managerId, managerId], type: userLoginSequelize.QueryTypes.SELECT }
|
|
);
|
|
|
|
if (personnelResult && personnelResult.length > 0) {
|
|
const phoneNumber = personnelResult[0].phoneNumber;
|
|
console.log(`找到客服电话号码: ${phoneNumber}`);
|
|
|
|
if (phoneNumber) {
|
|
// 2. 使用wechatAppSequelize更新users表中的type字段
|
|
console.log(`准备更新用户类型: 手机号=${phoneNumber}`);
|
|
const updateResult = await wechatAppSequelize.query(
|
|
'UPDATE users SET type = ? WHERE phoneNumber = ? AND type = ?',
|
|
{ replacements: ['manager', phoneNumber, 'customer'] }
|
|
);
|
|
|
|
const affectedRows = updateResult[1].affectedRows;
|
|
if (affectedRows > 0) {
|
|
console.log(`✓ 成功更新用户类型: 客服ID=${managerId}, 手机号=${phoneNumber}, 用户类型从customer更新为manager`);
|
|
} else {
|
|
console.log(`✓ 用户类型无需更新: 客服ID=${managerId}, 手机号=${phoneNumber}, 可能已经是manager类型`);
|
|
}
|
|
}
|
|
} else {
|
|
console.log(`未找到客服信息: managerId=${managerId}`);
|
|
}
|
|
} catch (err) {
|
|
console.error(`❌ 更新用户类型失败: ${err.message}`);
|
|
console.error(`错误详情:`, err);
|
|
}
|
|
|
|
// 通知相关用户客服状态变化
|
|
const conversations = await sequelize.query(
|
|
'SELECT DISTINCT userId FROM chat_conversations WHERE managerId = ?',
|
|
{ replacements: [managerId] }
|
|
);
|
|
|
|
conversations[0].forEach(conv => {
|
|
const userWs = onlineUsers.get(conv.userId);
|
|
if (userWs) {
|
|
userWs.send(JSON.stringify({
|
|
type: 'manager_status_change',
|
|
payload: { managerId, online: status === 1 }
|
|
}));
|
|
}
|
|
});
|
|
|
|
// 通知其他客服状态变化
|
|
onlineManagers.forEach((ws, id) => {
|
|
if (id !== managerId) {
|
|
ws.send(JSON.stringify({
|
|
type: 'manager_status_change',
|
|
payload: { managerId, online: status === 1 }
|
|
}));
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('更新客服在线状态失败:', error);
|
|
}
|
|
}
|
|
|
|
// 添加定时检查连接状态的函数
|
|
function startConnectionMonitoring() {
|
|
// 每30秒检查一次连接状态
|
|
setInterval(async () => {
|
|
try {
|
|
// 检查所有连接的活跃状态
|
|
const now = Date.now();
|
|
connections.forEach((connection, connectionId) => {
|
|
const { ws, lastActive = now } = connection;
|
|
|
|
// 如果超过60秒没有活动,关闭连接
|
|
if (now - lastActive > 60000) {
|
|
console.log(`关闭超时连接: ${connectionId}`);
|
|
try {
|
|
ws.close();
|
|
} catch (e) {
|
|
console.error('关闭连接失败:', e);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 发送广播心跳给所有在线用户和客服
|
|
onlineUsers.forEach(ws => {
|
|
try {
|
|
ws.send(JSON.stringify({ type: 'heartbeat' }));
|
|
} catch (e) {
|
|
console.error('发送心跳失败给用户:', e);
|
|
}
|
|
});
|
|
|
|
onlineManagers.forEach(ws => {
|
|
try {
|
|
ws.send(JSON.stringify({ type: 'heartbeat' }));
|
|
} catch (e) {
|
|
console.error('发送心跳失败给客服:', e);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('连接监控错误:', error);
|
|
}
|
|
}, 30000);
|
|
}
|
|
|
|
// 更新连接的最后活动时间
|
|
function updateConnectionActivity(connectionId) {
|
|
const connection = connections.get(connectionId);
|
|
if (connection) {
|
|
connection.lastActive = Date.now();
|
|
}
|
|
}
|
|
|
|
// 获取在线统计信息
|
|
function getOnlineStats() {
|
|
return {
|
|
totalConnections: connections.size,
|
|
onlineUsers: onlineUsers.size,
|
|
onlineManagers: onlineManagers.size,
|
|
activeConnections: Array.from(connections.entries()).filter(([_, conn]) => {
|
|
return (conn.isUser && conn.userId) || (conn.isManager && conn.managerId);
|
|
}).length
|
|
};
|
|
}
|
|
|
|
// 会话管理函数
|
|
// 创建或获取现有会话
|
|
async function createOrGetConversation(userId, managerId) {
|
|
// 修复: 确保ID类型一致
|
|
// 关键修复:明确区分userId和managerId,确保userId是普通用户ID,managerId是客服ID
|
|
let finalUserId = validateUserId(userId);
|
|
let finalManagerId = validateManagerId(managerId);
|
|
|
|
// 关键修复:验证managerId不是无效值(如"user")
|
|
if (finalManagerId === 'user' || finalManagerId === '0' || !finalManagerId) {
|
|
console.error('严重错误: 尝试使用无效的managerId创建会话:', managerId);
|
|
throw new Error('无效的客服ID');
|
|
}
|
|
|
|
// 关键修复:验证userId不是无效值
|
|
if (!finalUserId || finalUserId === '0') {
|
|
console.error('严重错误: 尝试使用无效的userId创建会话:', userId);
|
|
throw new Error('无效的用户ID');
|
|
}
|
|
|
|
// 关键修复:确保userId和managerId不会被错误交换
|
|
// 如果userId是数字而managerId不是数字,说明参数顺序可能错误
|
|
if (/^\d+$/.test(finalUserId) && !/^\d+$/.test(finalManagerId)) {
|
|
console.error('严重错误: 检测到userId和managerId可能被错误交换,userId:', finalUserId, 'managerId:', finalManagerId);
|
|
throw new Error('无效的用户ID或客服ID,可能存在参数顺序错误');
|
|
}
|
|
|
|
// 设置重试参数,防止竞态条件导致的重复创建
|
|
let attempts = 0;
|
|
const maxAttempts = 3;
|
|
|
|
while (attempts < maxAttempts) {
|
|
try {
|
|
// 尝试查找已存在的会话 - 双向检查,避免重复创建
|
|
const [existingConversations] = await sequelize.query(
|
|
'SELECT * FROM chat_conversations WHERE (userId = ? AND managerId = ?) OR (userId = ? AND managerId = ?) LIMIT 1',
|
|
{ replacements: [finalUserId, finalManagerId, finalManagerId, finalUserId] }
|
|
);
|
|
|
|
if (existingConversations && existingConversations.length > 0) {
|
|
const conversation = existingConversations[0];
|
|
// 如果会话已结束,重新激活
|
|
if (conversation.status !== 1) {
|
|
await sequelize.query(
|
|
'UPDATE chat_conversations SET status = 1 WHERE conversation_id = ?',
|
|
{ replacements: [conversation.conversation_id] }
|
|
);
|
|
conversation.status = 1;
|
|
}
|
|
return conversation;
|
|
}
|
|
|
|
// 创建新会话
|
|
const conversationId = crypto.randomUUID();
|
|
const now = getBeijingTime();
|
|
|
|
await sequelize.query(
|
|
`INSERT INTO chat_conversations
|
|
(conversation_id, userId, managerId, status, user_online, cs_online, created_at, updated_at)
|
|
VALUES (?, ?, ?, 1, ?, ?, ?, ?)`,
|
|
{
|
|
replacements: [
|
|
conversationId,
|
|
finalUserId,
|
|
finalManagerId,
|
|
onlineUsers.has(finalUserId) ? 1 : 0,
|
|
onlineManagers.has(finalManagerId) ? 1 : 0,
|
|
now,
|
|
now
|
|
]
|
|
}
|
|
);
|
|
|
|
// 返回新创建的会话
|
|
return {
|
|
conversation_id: conversationId,
|
|
userId: finalUserId,
|
|
managerId: finalManagerId,
|
|
status: 1,
|
|
user_online: onlineUsers.has(finalUserId) ? 1 : 0,
|
|
cs_online: onlineManagers.has(finalManagerId) ? 1 : 0,
|
|
created_at: now,
|
|
updated_at: now
|
|
};
|
|
} catch (error) {
|
|
console.error(`创建或获取会话失败 (尝试 ${attempts + 1}/${maxAttempts}):`, error);
|
|
attempts++;
|
|
// 如果不是最后一次尝试,短暂延迟后重试
|
|
if (attempts < maxAttempts) {
|
|
await new Promise(resolve => setTimeout(resolve, 100 * attempts));
|
|
} else {
|
|
// 最后一次尝试也失败了,抛出错误
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 所有尝试都失败后,再次查询以获取可能已存在的会话
|
|
const [finalConversations] = await sequelize.query(
|
|
'SELECT * FROM chat_conversations WHERE userId = ? AND managerId = ? LIMIT 1',
|
|
{ replacements: [finalUserId, finalManagerId] }
|
|
);
|
|
|
|
if (finalConversations && finalConversations.length > 0) {
|
|
return finalConversations[0];
|
|
}
|
|
|
|
throw new Error('无法创建或获取会话,所有尝试均失败');
|
|
}
|
|
|
|
// 获取用户的所有会话
|
|
async function getUserConversations(userId) {
|
|
try {
|
|
// 第一步:获取所有会话
|
|
const [conversations] = await sequelize.query(
|
|
`SELECT * FROM chat_conversations
|
|
WHERE userId = ?
|
|
ORDER BY last_message_time DESC, created_at DESC`,
|
|
{ replacements: [userId] }
|
|
);
|
|
|
|
// 第二步:对于每个会话,单独获取用户和客服信息
|
|
for (let i = 0; i < conversations.length; i++) {
|
|
const conversation = conversations[i];
|
|
|
|
// 获取用户信息
|
|
const [users] = await sequelize.query(
|
|
'SELECT nickName, avatarUrl FROM users WHERE userId = ?',
|
|
{ replacements: [conversation.userId] }
|
|
);
|
|
if (users && users.length > 0) {
|
|
conversation.userNickName = users[0].nickName;
|
|
conversation.userAvatar = users[0].avatarUrl;
|
|
}
|
|
|
|
// 获取客服信息
|
|
try {
|
|
const [personnel] = await sequelize.query(
|
|
'SELECT name FROM userlogin.personnel WHERE id = ?',
|
|
{ replacements: [conversation.managerId] }
|
|
);
|
|
if (personnel && personnel.length > 0) {
|
|
conversation.managerName = personnel[0].name;
|
|
} else {
|
|
// 客服信息不存在时,使用默认名称
|
|
conversation.managerName = `客服${conversation.managerId}`;
|
|
}
|
|
} catch (error) {
|
|
console.warn('获取客服信息失败,使用默认名称:', error.message);
|
|
// 客服信息获取失败时,使用默认名称
|
|
conversation.managerName = `客服${conversation.managerId}`;
|
|
}
|
|
}
|
|
return conversations;
|
|
} catch (error) {
|
|
console.error('获取用户会话失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 获取客服的所有会话
|
|
async function getManagerConversations(managerId) {
|
|
try {
|
|
// 分两步查询,避免跨数据库表连接时的排序规则问题
|
|
// 第一步:获取所有会话
|
|
const [conversations] = await sequelize.query(
|
|
`SELECT * FROM chat_conversations WHERE managerId = ?
|
|
ORDER BY last_message_time DESC, created_at DESC`,
|
|
{ replacements: [managerId] }
|
|
);
|
|
|
|
// 第二步:对于每个会话,单独获取用户和客服信息
|
|
for (let i = 0; i < conversations.length; i++) {
|
|
const conversation = conversations[i];
|
|
|
|
// 获取用户信息
|
|
const [users] = await sequelize.query(
|
|
'SELECT nickName, avatarUrl FROM users WHERE userId = ?',
|
|
{ replacements: [conversation.userId] }
|
|
);
|
|
if (users && users.length > 0) {
|
|
conversation.userNickName = users[0].nickName;
|
|
conversation.userAvatar = users[0].avatarUrl;
|
|
}
|
|
|
|
// 获取客服信息
|
|
const [personnel] = await sequelize.query(
|
|
'SELECT name FROM userlogin.personnel WHERE id = ?',
|
|
{ replacements: [conversation.managerId] }
|
|
);
|
|
if (personnel && personnel.length > 0) {
|
|
conversation.managerName = personnel[0].name;
|
|
}
|
|
}
|
|
return conversations;
|
|
} catch (error) {
|
|
console.error('获取客服会话失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 消息处理函数
|
|
// 处理聊天消息
|
|
async function handleChatMessage(ws, payload) {
|
|
console.log('===== 开始处理聊天消息 =====');
|
|
console.log('收到聊天消息 - 原始payload:', payload);
|
|
console.log('连接信息 - connectionId:', ws.connectionId);
|
|
|
|
// 添加详细的连接信息日志
|
|
const connection = connections.get(ws.connectionId);
|
|
console.log('连接详情 - isUser:', connection?.isUser, 'isManager:', connection?.isManager);
|
|
|
|
// 基本参数验证
|
|
if (!payload) {
|
|
console.error('错误: payload为空');
|
|
ws.send(JSON.stringify({
|
|
type: 'error',
|
|
message: '消息数据不完整,缺少必要字段'
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (!payload.content) {
|
|
console.error('错误: payload.content为空');
|
|
ws.send(JSON.stringify({
|
|
type: 'error',
|
|
message: '消息内容不能为空'
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// 打印解构前的payload字段
|
|
console.log('Payload字段检查:');
|
|
console.log('- conversationId存在:', 'conversationId' in payload);
|
|
console.log('- content存在:', 'content' in payload);
|
|
console.log('- contentType存在:', 'contentType' in payload);
|
|
console.log('- messageId存在:', 'messageId' in payload);
|
|
|
|
const { conversationId, content, contentType = 1, fileUrl, fileSize, duration } = payload;
|
|
|
|
console.log('解构后的值:');
|
|
console.log('- conversationId:', conversationId);
|
|
console.log('- content:', content?.substring(0, 20) + '...');
|
|
console.log('- contentType:', contentType);
|
|
|
|
if (!connection) {
|
|
console.error('错误: 连接不存在');
|
|
ws.send(JSON.stringify({
|
|
type: 'error',
|
|
message: '连接已失效'
|
|
}));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 确定发送者和接收者信息
|
|
let senderId, receiverId, senderType;
|
|
let conversation;
|
|
|
|
if (connection.isUser) {
|
|
// 用户发送消息给客服
|
|
senderId = validateUserId(connection.userId);
|
|
senderType = 1;
|
|
|
|
// 关键验证:确保senderId有效
|
|
if (!senderId || senderId === 0 || senderId === '0') {
|
|
console.error('严重错误: 用户连接的userId无效:', { userId: senderId });
|
|
throw new Error('用户认证信息不完整,无法发送消息');
|
|
}
|
|
|
|
console.log('处理用户消息:', { userId: senderId, conversationId, hasManagerId: !!payload.managerId });
|
|
|
|
// 如果没有提供会话ID,则查找或创建会话
|
|
if (!conversationId) {
|
|
if (!payload.managerId) {
|
|
throw new Error('未指定客服ID');
|
|
}
|
|
receiverId = validateManagerId(payload.managerId);
|
|
|
|
// 确保senderId有效且不是测试ID
|
|
if (!senderId || senderId.includes('test_')) {
|
|
console.error('错误: 尝试使用无效或测试用户ID创建会话:', senderId);
|
|
throw new Error('用户认证信息无效,无法创建会话');
|
|
}
|
|
|
|
console.log('创建新会话:', { userId: senderId, managerId: receiverId });
|
|
conversation = await createOrGetConversation(senderId, receiverId);
|
|
// 验证创建的会话信息
|
|
console.log('创建的会话详情:', conversation);
|
|
if (!conversation || !conversation.conversation_id) {
|
|
console.error('错误: 创建会话失败或返回无效的会话信息');
|
|
throw new Error('创建会话失败');
|
|
}
|
|
|
|
// 强制设置正确的userId,确保不会出现0或空值
|
|
conversation.userId = senderId;
|
|
conversation.conversation_id = conversation.conversation_id || conversation.conversationId;
|
|
|
|
// 立即验证并修复会话中的用户ID
|
|
if (conversation.userId !== senderId) {
|
|
console.warn('警告: 会话创建后userId不匹配,立即修复');
|
|
await sequelize.query(
|
|
'UPDATE chat_conversations SET userId = ? WHERE conversation_id = ?',
|
|
{ replacements: [senderId, conversation.conversation_id] }
|
|
);
|
|
conversation.userId = senderId;
|
|
}
|
|
|
|
// 验证并修复数据库中的会话userId
|
|
if (conversation.userId !== senderId) {
|
|
console.warn('警告: 数据库返回的userId与连接的userId不匹配,正在修复');
|
|
await sequelize.query(
|
|
'UPDATE chat_conversations SET userId = ? WHERE conversation_id = ?',
|
|
{ replacements: [senderId, conversation.conversation_id] }
|
|
);
|
|
console.log(`已修复会话用户ID: conversationId=${conversation.conversation_id}, 设置为${senderId}`);
|
|
}
|
|
} else {
|
|
// 获取会话信息以确定接收者
|
|
console.log('查询现有会话:', { conversationId });
|
|
|
|
// 检查是否是临时会话ID
|
|
if (conversationId && conversationId.startsWith('temp_')) {
|
|
console.log('检测到临时会话ID,需要创建真实会话:', conversationId);
|
|
|
|
// 从临时会话ID中提取信息
|
|
// 支持前端临时会话ID格式: temp_[currentUserId]_[targetId]_[timestamp]
|
|
const tempIdParts = conversationId.split('_');
|
|
if (tempIdParts.length >= 4) {
|
|
// 根据连接类型确定正确的userId和managerId
|
|
let tempUserId, tempManagerId;
|
|
|
|
if (connection.isUser) {
|
|
// 用户连接: currentUserId是用户ID,targetId是客服ID
|
|
tempUserId = tempIdParts[1];
|
|
tempManagerId = tempIdParts[2];
|
|
} else if (connection.isManager) {
|
|
// 客服连接: currentUserId是客服ID,targetId是用户ID
|
|
tempManagerId = tempIdParts[1];
|
|
tempUserId = tempIdParts[2];
|
|
} else {
|
|
// 默认情况,尝试判断哪个是用户ID哪个是客服ID
|
|
if (tempIdParts[1].includes('user_')) {
|
|
tempUserId = tempIdParts[1];
|
|
tempManagerId = tempIdParts[2];
|
|
} else {
|
|
tempUserId = tempIdParts[2];
|
|
tempManagerId = tempIdParts[1];
|
|
}
|
|
}
|
|
|
|
console.log('从临时ID提取信息:', { tempManagerId, tempUserId });
|
|
|
|
// 创建或获取真实会话
|
|
conversation = await createOrGetConversation(tempUserId, tempManagerId);
|
|
console.log('创建的真实会话:', conversation);
|
|
receiverId = tempManagerId;
|
|
} else {
|
|
console.error('无法解析临时会话ID:', conversationId);
|
|
throw new Error('无效的临时会话ID格式');
|
|
}
|
|
} else {
|
|
// 正常查询现有会话
|
|
const [conversations] = await sequelize.query(
|
|
'SELECT * FROM chat_conversations WHERE conversation_id = ?',
|
|
{ replacements: [conversationId] }
|
|
);
|
|
if (!conversations || conversations.length === 0) {
|
|
console.warn('会话不存在,尝试从userId和managerId创建');
|
|
|
|
// 尝试从payload中获取managerId
|
|
if (payload.managerId) {
|
|
receiverId = validateManagerId(payload.managerId);
|
|
console.log('尝试使用payload中的managerId创建会话:', { userId: senderId, managerId: receiverId });
|
|
conversation = await createOrGetConversation(senderId, receiverId);
|
|
} else {
|
|
throw new Error('会话不存在且无法确定客服ID');
|
|
}
|
|
} else {
|
|
conversation = conversations[0];
|
|
console.log('查询到的会话详情:', conversation);
|
|
// 关键修复:确保receiverId是有效的客服ID,不是"user"或其他无效值
|
|
receiverId = conversation.managerId;
|
|
if (receiverId === 'user') {
|
|
console.error('错误: 会话的managerId是"user",使用payload中的managerId替代');
|
|
if (payload.managerId) {
|
|
receiverId = validateManagerId(payload.managerId);
|
|
} else {
|
|
throw new Error('会话的managerId无效,且payload中没有提供有效的managerId');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 验证会话的userId是否与当前用户匹配,不匹配则修复
|
|
// 关键修复:只有当会话的userId不是当前用户ID,并且会话的managerId不是当前用户ID时,才更新会话的userId
|
|
// 避免将用户ID和客服ID错误地交换
|
|
if (conversation.userId !== senderId && conversation.managerId !== senderId) {
|
|
console.error(`错误: 会话userId(${conversation.userId})与当前用户ID(${senderId})不匹配`);
|
|
// 更新会话的userId为当前用户ID
|
|
await sequelize.query(
|
|
'UPDATE chat_conversations SET userId = ? WHERE conversation_id = ?',
|
|
{ replacements: [senderId, conversationId] }
|
|
);
|
|
conversation.userId = senderId;
|
|
console.log(`已修复会话用户ID: conversationId=${conversationId}, 设置为${senderId}`);
|
|
}
|
|
}
|
|
} else if (connection.isManager) {
|
|
// 客服发送消息给用户
|
|
senderId = validateManagerId(connection.managerId);
|
|
senderType = 2;
|
|
|
|
console.log('处理客服消息 - 详细信息:');
|
|
console.log('- managerId:', senderId);
|
|
console.log('- conversationId:', conversationId);
|
|
|
|
// 检查conversationId是否有效
|
|
if (!conversationId) {
|
|
console.error('错误: 客服消息缺少conversationId');
|
|
throw new Error('消息数据不完整,缺少必要字段');
|
|
}
|
|
|
|
// 获取会话信息以确定接收者
|
|
console.log('查询会话信息:', conversationId);
|
|
const [conversations] = await sequelize.query(
|
|
'SELECT * FROM chat_conversations WHERE conversation_id = ?',
|
|
{ replacements: [conversationId] }
|
|
);
|
|
if (!conversations || conversations.length === 0) {
|
|
throw new Error('会话不存在');
|
|
}
|
|
conversation = conversations[0];
|
|
receiverId = conversation.userId;
|
|
|
|
// 检查receiverId是否有效
|
|
console.log('从会话获取的receiverId:', receiverId);
|
|
|
|
// 修复方案:如果receiverId无效,我们需要查找正确的用户ID
|
|
if (!receiverId || receiverId === 0 || receiverId === '0') {
|
|
console.error('错误: 会话中的用户ID无效(0或为空),正在尝试修复');
|
|
|
|
// 查找该会话中的所有消息,尝试从用户发送的消息中获取正确的用户ID
|
|
const [messages] = await sequelize.query(
|
|
'SELECT sender_id FROM chat_messages WHERE conversation_id = ? AND sender_type = 1 LIMIT 1',
|
|
{ replacements: [conversationId] }
|
|
);
|
|
|
|
if (messages && messages.length > 0 && messages[0].sender_id && messages[0].sender_id !== 0) {
|
|
// 找到正确的用户ID,更新会话信息
|
|
const correctUserId = messages[0].sender_id;
|
|
receiverId = correctUserId;
|
|
|
|
// 更新数据库中的会话信息,修复userId为空的问题
|
|
await sequelize.query(
|
|
'UPDATE chat_conversations SET userId = ? WHERE conversation_id = ?',
|
|
{ replacements: [correctUserId, conversationId] }
|
|
);
|
|
|
|
console.log(`✅ 成功修复会话用户ID: conversationId=${conversationId}, 从0更新为${correctUserId}`);
|
|
} else {
|
|
// 如果找不到正确的用户ID,则抛出错误,不允许存储无效消息
|
|
console.error('❌ 无法找到有效的用户ID,消息发送失败');
|
|
throw new Error('会话用户信息不完整,无法发送消息');
|
|
}
|
|
}
|
|
|
|
// 确保会话对象中的userId也是正确的
|
|
conversation.userId = receiverId;
|
|
} else {
|
|
throw new Error('未认证的连接');
|
|
}
|
|
|
|
// 确保会话存在
|
|
if (!conversation) {
|
|
console.error('错误: 会话对象不存在');
|
|
throw new Error('会话信息无效');
|
|
}
|
|
|
|
// 获取会话ID,处理字段名差异
|
|
const convId = conversation.conversation_id || conversation.conversationId;
|
|
if (!convId) {
|
|
console.error('错误: 会话缺少有效的ID', conversation);
|
|
throw new Error('会话信息无效');
|
|
}
|
|
|
|
// 直接使用传入的senderId,确保始终有效
|
|
console.log('会话中的用户ID:', senderId);
|
|
if (!senderId || senderId === 0 || senderId === '0') {
|
|
console.error('错误: 用户ID无效');
|
|
throw new Error('用户信息不完整');
|
|
}
|
|
|
|
// 统一会话信息格式,强制使用正确的字段名
|
|
// 关键修复:保持原始会话的userId和managerId不变,只统一字段名
|
|
conversation = {
|
|
conversation_id: convId,
|
|
userId: conversation.userId,
|
|
managerId: conversation.managerId,
|
|
...conversation
|
|
};
|
|
|
|
// 生成消息ID和时间戳
|
|
const messageId = payload.messageId || crypto.randomUUID(); // 允许前端提供messageId
|
|
const now = getBeijingTime();
|
|
|
|
console.log('准备存储消息:', {
|
|
messageId,
|
|
conversationId: conversation.conversation_id,
|
|
senderType,
|
|
senderId,
|
|
receiverId
|
|
});
|
|
|
|
try {
|
|
// 关键修复:确保storeMessage被正确调用
|
|
const storeResult = await storeMessage({
|
|
messageId,
|
|
conversationId: conversation.conversation_id,
|
|
senderType,
|
|
senderId,
|
|
receiverId,
|
|
contentType,
|
|
content,
|
|
fileUrl,
|
|
fileSize,
|
|
duration,
|
|
createdAt: now
|
|
});
|
|
|
|
console.log('✅ 消息存储成功:', storeResult);
|
|
console.log('开始更新会话信息...');
|
|
} catch (storeError) {
|
|
console.error('❌ 消息存储失败:', storeError.message);
|
|
throw storeError; // 重新抛出错误,确保上层捕获
|
|
}
|
|
|
|
// 更新会话最后消息
|
|
await updateConversationLastMessage(conversation.conversation_id, content, now);
|
|
|
|
// 更新未读计数
|
|
if (connection.isUser) {
|
|
await updateUnreadCount(conversation.conversation_id, 'cs_unread_count', 1);
|
|
console.log('更新客服未读数:', { conversationId: conversation.conversation_id });
|
|
} else {
|
|
await updateUnreadCount(conversation.conversation_id, 'unread_count', 1);
|
|
console.log('更新用户未读数:', { conversationId: conversation.conversation_id });
|
|
}
|
|
|
|
// 构造消息对象
|
|
const messageData = {
|
|
messageId,
|
|
conversationId: conversation.conversation_id,
|
|
senderType,
|
|
senderId,
|
|
receiverId,
|
|
contentType,
|
|
content,
|
|
fileUrl,
|
|
fileSize,
|
|
duration,
|
|
isRead: 0,
|
|
status: 1,
|
|
createdAt: now
|
|
};
|
|
|
|
// 发送消息给接收者
|
|
let receiverWs;
|
|
if (senderType === 1) {
|
|
// 用户发送给客服
|
|
receiverWs = onlineManagers.get(receiverId);
|
|
console.log(`尝试转发消息给客服 ${receiverId},客服是否在线:`, !!receiverWs);
|
|
} else {
|
|
// 客服发送给用户
|
|
receiverWs = onlineUsers.get(receiverId);
|
|
console.log(`尝试转发消息给用户 ${receiverId},用户是否在线:`, !!receiverWs);
|
|
}
|
|
|
|
// 处理特殊情况:当发送者和接收者是同一个人(既是用户又是客服)
|
|
if (!receiverWs && senderId == receiverId) {
|
|
if (senderType === 1) {
|
|
// 用户发送消息给自己的客服身份
|
|
receiverWs = onlineManagers.get(senderId);
|
|
} else {
|
|
// 客服发送消息给自己的用户身份
|
|
receiverWs = onlineUsers.get(senderId);
|
|
}
|
|
console.log('处理同一会话内消息转发:', { senderId, hasReceiverWs: !!receiverWs });
|
|
}
|
|
|
|
if (receiverWs) {
|
|
try {
|
|
receiverWs.send(JSON.stringify({
|
|
type: 'new_message',
|
|
payload: messageData
|
|
}));
|
|
console.log('消息转发成功');
|
|
} catch (sendError) {
|
|
console.error('转发消息失败:', sendError);
|
|
// 转发失败不影响消息存储,只记录错误
|
|
}
|
|
} else {
|
|
console.log('接收者不在线,消息已存储但未实时推送');
|
|
}
|
|
|
|
// 发送确认给发送者
|
|
ws.send(JSON.stringify({
|
|
type: 'message_sent',
|
|
payload: {
|
|
messageId,
|
|
status: 'success',
|
|
conversationId: conversation.conversation_id
|
|
}
|
|
}));
|
|
|
|
console.log('消息处理完成:', { messageId, status: 'success' });
|
|
|
|
} catch (error) {
|
|
console.error('处理聊天消息失败:', {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
payload: { conversationId, content: content ? content.substring(0, 50) + '...' : '无内容' }
|
|
});
|
|
ws.send(JSON.stringify({
|
|
type: 'error',
|
|
message: '消息发送失败: ' + error.message
|
|
}));
|
|
}
|
|
}
|
|
|
|
// 存储消息到数据库
|
|
async function storeMessage(messageData) {
|
|
const { messageId, conversationId, senderType, senderId, receiverId,
|
|
contentType, content, fileUrl, fileSize, duration, createdAt } = messageData;
|
|
|
|
// 参数验证
|
|
if (!messageId || !conversationId || !senderType || !senderId || !receiverId || !content) {
|
|
throw new Error('消息数据不完整,缺少必要字段');
|
|
}
|
|
|
|
// 确保所有ID都是字符串类型,并添加额外的验证
|
|
const stringSenderId = validateUserId(senderId);
|
|
const stringReceiverId = String(receiverId).trim();
|
|
const stringConversationId = String(conversationId).trim();
|
|
const stringMessageId = String(messageId).trim();
|
|
|
|
// 验证senderId不是测试ID或无效ID
|
|
if (stringSenderId && stringSenderId.includes('test_')) {
|
|
console.warn('警告: 检测到使用测试ID发送消息:', stringSenderId);
|
|
// 不阻止消息发送,但记录警告
|
|
}
|
|
|
|
// 确保senderId不为空或0
|
|
if (!stringSenderId || stringSenderId === '0' || stringSenderId === 'null' || stringSenderId === 'undefined') {
|
|
throw new Error('无效的发送者ID');
|
|
}
|
|
|
|
try {
|
|
console.log('开始存储消息到数据库:', {
|
|
messageId: stringMessageId,
|
|
conversationId: stringConversationId,
|
|
senderType: senderType === 1 ? '用户' : '客服',
|
|
senderId: stringSenderId,
|
|
receiverId: stringReceiverId,
|
|
contentType
|
|
});
|
|
|
|
const result = await sequelize.query(
|
|
`INSERT INTO chat_messages
|
|
(message_id, conversation_id, sender_type, sender_id, receiver_id,
|
|
content_type, content, file_url, file_size, duration, is_read, status,
|
|
created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 1, ?, ?)`,
|
|
{
|
|
replacements: [
|
|
stringMessageId,
|
|
stringConversationId,
|
|
senderType,
|
|
stringSenderId,
|
|
stringReceiverId,
|
|
contentType || 1, // 默认文本消息
|
|
content,
|
|
fileUrl || null,
|
|
fileSize || null,
|
|
duration || null,
|
|
createdAt || getBeijingTime(),
|
|
createdAt || getBeijingTime()
|
|
]
|
|
}
|
|
);
|
|
|
|
// 记录影响行数,确认插入成功
|
|
const affectedRows = result[1] && result[1].affectedRows ? result[1].affectedRows : 0;
|
|
console.log(`消息存储成功: messageId=${messageId}, 影响行数=${affectedRows}`);
|
|
|
|
return { success: true, messageId, affectedRows };
|
|
} catch (error) {
|
|
console.error('存储消息到数据库失败:', {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
messageData: {
|
|
messageId,
|
|
conversationId,
|
|
senderType,
|
|
senderId,
|
|
receiverId
|
|
}
|
|
});
|
|
throw new Error(`消息存储失败: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// 更新会话最后消息
|
|
async function updateConversationLastMessage(conversationId, lastMessage, timestamp) {
|
|
try {
|
|
// 关键修复:在更新最后消息前,先检查会话是否存在
|
|
const [conversations] = await sequelize.query(
|
|
'SELECT * FROM chat_conversations WHERE conversation_id = ? LIMIT 1',
|
|
{ replacements: [conversationId] }
|
|
);
|
|
|
|
if (conversations && conversations.length > 0) {
|
|
// 只有当会话存在时,才更新最后消息
|
|
await sequelize.query(
|
|
'UPDATE chat_conversations SET last_message = ?, last_message_time = ?, updated_at = ? WHERE conversation_id = ?',
|
|
{ replacements: [lastMessage, timestamp, timestamp, conversationId] }
|
|
);
|
|
console.log(`更新会话最后消息成功: conversationId=${conversationId}, lastMessage=${lastMessage}`);
|
|
} else {
|
|
// 如果会话不存在,不创建新会话,只记录警告
|
|
console.warn(`警告: 尝试更新不存在的会话最后消息: conversationId=${conversationId}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('更新会话最后消息失败:', error);
|
|
// 不抛出错误,避免影响消息存储
|
|
}
|
|
}
|
|
|
|
// 更新未读计数
|
|
async function updateUnreadCount(conversationId, countField, increment) {
|
|
try {
|
|
await sequelize.query(
|
|
`UPDATE chat_conversations
|
|
SET ${countField} = ${countField} + ?, updated_at = ?
|
|
WHERE conversation_id = ?`,
|
|
{ replacements: [increment, getBeijingTime(), conversationId] }
|
|
);
|
|
} catch (error) {
|
|
console.error('更新未读计数失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 处理未读消息标记
|
|
async function handleMarkRead(ws, payload) {
|
|
console.log('收到标记已读请求:', { payload, connectionId: ws.connectionId });
|
|
|
|
const { conversationId, messageIds } = payload;
|
|
const connection = connections.get(ws.connectionId);
|
|
|
|
if (!connection) {
|
|
console.error('连接不存在,无法标记已读');
|
|
return;
|
|
}
|
|
|
|
if (!conversationId) {
|
|
console.error('未提供会话ID');
|
|
ws.send(JSON.stringify({
|
|
type: 'error',
|
|
message: '未提供会话ID'
|
|
}));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const now = getBeijingTime();
|
|
let countField;
|
|
let updateQuery;
|
|
let updateParams;
|
|
|
|
if (connection.isUser || connection.userType === 'user') {
|
|
// 用户标记客服消息为已读
|
|
countField = 'unread_count';
|
|
console.log('用户标记消息已读:', { conversationId, userId: connection.userId });
|
|
|
|
if (messageIds && Array.isArray(messageIds) && messageIds.length > 0) {
|
|
// 标记特定消息为已读
|
|
updateQuery = 'UPDATE chat_messages SET is_read = 1, read_time = ? WHERE conversation_id = ? AND sender_type = 2 AND message_id IN (?)';
|
|
updateParams = [now, conversationId, messageIds];
|
|
} else {
|
|
// 标记所有消息为已读
|
|
updateQuery = 'UPDATE chat_messages SET is_read = 1, read_time = ? WHERE conversation_id = ? AND sender_type = 2';
|
|
updateParams = [now, conversationId];
|
|
}
|
|
} else if (connection.isManager || connection.userType === 'manager') {
|
|
// 客服标记用户消息为已读
|
|
countField = 'cs_unread_count';
|
|
console.log('客服标记消息已读:', { conversationId, managerId: connection.managerId });
|
|
|
|
if (messageIds && Array.isArray(messageIds) && messageIds.length > 0) {
|
|
// 标记特定消息为已读
|
|
updateQuery = 'UPDATE chat_messages SET is_read = 1, read_time = ? WHERE conversation_id = ? AND sender_type = 1 AND message_id IN (?)';
|
|
updateParams = [now, conversationId, messageIds];
|
|
} else {
|
|
// 标记所有消息为已读
|
|
updateQuery = 'UPDATE chat_messages SET is_read = 1, read_time = ? WHERE conversation_id = ? AND sender_type = 1';
|
|
updateParams = [now, conversationId];
|
|
}
|
|
} else {
|
|
throw new Error('未知的连接类型');
|
|
}
|
|
|
|
// 执行消息已读更新
|
|
await sequelize.query(updateQuery, { replacements: updateParams });
|
|
|
|
// 重置未读计数
|
|
await sequelize.query(
|
|
`UPDATE chat_conversations SET ${countField} = 0 WHERE conversation_id = ?`,
|
|
{ replacements: [conversationId] }
|
|
);
|
|
|
|
console.log('消息已读状态更新成功:', { conversationId, countField });
|
|
|
|
// 发送确认
|
|
ws.send(JSON.stringify({
|
|
type: 'marked_read',
|
|
payload: { conversationId, messageIds }
|
|
}));
|
|
|
|
// 通知对方已读状态(可选)
|
|
// 这里可以根据需要添加向对方发送已读状态通知的逻辑
|
|
|
|
} catch (error) {
|
|
console.error('标记消息已读失败:', {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
conversationId
|
|
});
|
|
ws.send(JSON.stringify({
|
|
type: 'error',
|
|
message: '标记已读失败: ' + error.message
|
|
}));
|
|
}
|
|
}
|
|
|
|
// 处理会话相关消息
|
|
async function handleSessionMessage(ws, data) {
|
|
// 兼容不同格式的消息数据
|
|
// 关键修复:同时支持type和action字段
|
|
const action = data.action || data.type || (data.data && data.data.action) || (data.payload && data.payload.action) || 'list'; // 默认action为'list'
|
|
const conversationId = data.conversationId || (data.data && data.data.conversationId) || (data.payload && data.payload.conversationId);
|
|
const connection = connections.get(ws.connectionId);
|
|
|
|
if (!connection) {
|
|
ws.send(JSON.stringify({
|
|
type: 'error',
|
|
message: '未认证的连接'
|
|
}));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
switch (action) {
|
|
case 'get_conversations':
|
|
case 'list':
|
|
// 获取会话列表,支持'list'和'get_conversations'两种操作
|
|
console.log('获取会话列表请求:', { action, isUser: connection.isUser || connection.userType === 'user', isManager: connection.isManager || connection.userType === 'manager' });
|
|
let conversations;
|
|
try {
|
|
if (connection.isUser || connection.userType === 'user') {
|
|
const userId = connection.userId || (connection.userType === 'user' && connection.userId);
|
|
if (!userId) {
|
|
throw new Error('用户ID不存在');
|
|
}
|
|
conversations = await getUserConversations(userId);
|
|
console.log('用户会话列表获取成功:', { userId, conversationCount: conversations.length });
|
|
} else if (connection.isManager || connection.userType === 'manager') {
|
|
const managerId = connection.managerId || (connection.userType === 'manager' && connection.managerId);
|
|
if (!managerId) {
|
|
throw new Error('客服ID不存在');
|
|
}
|
|
conversations = await getManagerConversations(managerId);
|
|
console.log('客服会话列表获取成功:', { managerId, conversationCount: conversations.length });
|
|
} else {
|
|
throw new Error('未知的连接类型');
|
|
}
|
|
|
|
// 为每个会话更新在线状态
|
|
if (conversations && conversations.length > 0) {
|
|
const updatedConversations = await Promise.all(conversations.map(async (conv) => {
|
|
// 检查用户是否在线
|
|
const userOnline = onlineUsers.has(conv.userId) ? 1 : 0;
|
|
// 检查客服是否在线
|
|
const csOnline = onlineManagers.has(conv.managerId) ? 1 : 0;
|
|
|
|
// 如果在线状态有变化,更新数据库
|
|
if (conv.user_online !== userOnline || conv.cs_online !== csOnline) {
|
|
await sequelize.query(
|
|
'UPDATE chat_conversations SET user_online = ?, cs_online = ? WHERE conversation_id = ?',
|
|
{ replacements: [userOnline, csOnline, conv.conversation_id] }
|
|
);
|
|
conv.user_online = userOnline;
|
|
conv.cs_online = csOnline;
|
|
}
|
|
|
|
return conv;
|
|
}));
|
|
conversations = updatedConversations;
|
|
}
|
|
|
|
// 支持两种响应格式,确保兼容性
|
|
if (action === 'list') {
|
|
// 兼容测试脚本的响应格式
|
|
ws.send(JSON.stringify({
|
|
type: 'session_list',
|
|
data: conversations || []
|
|
}));
|
|
} else {
|
|
// 原有响应格式
|
|
ws.send(JSON.stringify({
|
|
type: 'conversations_list',
|
|
payload: { conversations: conversations || [] }
|
|
}));
|
|
}
|
|
|
|
console.log('会话列表推送成功:', { action, responseType: action === 'list' ? 'session_list' : 'conversations_list' });
|
|
} catch (error) {
|
|
console.error('获取会话列表失败:', { error: error.message, action });
|
|
ws.send(JSON.stringify({
|
|
type: 'error',
|
|
message: '获取会话列表失败: ' + error.message
|
|
}));
|
|
}
|
|
break;
|
|
|
|
case 'get_messages':
|
|
// 获取会话历史消息
|
|
if (!conversationId) {
|
|
throw new Error('未指定会话ID');
|
|
}
|
|
|
|
const page = parseInt(data.page || (data.data && data.data.page) || (data.payload && data.payload.page)) || 1;
|
|
const limit = parseInt(data.limit || (data.data && data.data.limit) || (data.payload && data.payload.limit)) || 50;
|
|
const offset = (page - 1) * limit;
|
|
|
|
console.log('获取会话消息:', { conversationId, page, limit, offset });
|
|
|
|
try {
|
|
// 查询消息
|
|
const [messages] = await sequelize.query(
|
|
`SELECT * FROM chat_messages
|
|
WHERE conversation_id = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?`,
|
|
{ replacements: [conversationId, limit, offset] }
|
|
);
|
|
|
|
// 反转顺序,使最早的消息在前
|
|
messages.reverse();
|
|
|
|
// 获取消息总数
|
|
const [[totalCount]] = await sequelize.query(
|
|
'SELECT COUNT(*) as count FROM chat_messages WHERE conversation_id = ?',
|
|
{ replacements: [conversationId] }
|
|
);
|
|
|
|
ws.send(JSON.stringify({
|
|
type: 'messages_list',
|
|
payload: {
|
|
messages,
|
|
conversationId,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total: totalCount.count,
|
|
totalPages: Math.ceil(totalCount.count / limit)
|
|
}
|
|
}
|
|
}));
|
|
|
|
console.log('消息获取成功:', { conversationId, messageCount: messages.length });
|
|
|
|
// 如果是客服查看消息,自动将未读消息标记为已读
|
|
if (connection.isManager) {
|
|
const readTime = getBeijingTime();
|
|
await sequelize.query(
|
|
'UPDATE chat_messages SET is_read = 1, read_time = ? WHERE conversation_id = ? AND is_read = 0 AND sender_type = 1',
|
|
{ replacements: [readTime, conversationId] }
|
|
);
|
|
|
|
// 更新会话未读数
|
|
await sequelize.query(
|
|
'UPDATE chat_conversations SET cs_unread_count = 0 WHERE conversation_id = ?',
|
|
{ replacements: [conversationId] }
|
|
);
|
|
|
|
console.log('客服查看后更新未读状态:', { conversationId });
|
|
}
|
|
} catch (error) {
|
|
console.error('获取消息失败:', { conversationId, error: error.message });
|
|
ws.send(JSON.stringify({
|
|
type: 'error',
|
|
message: '获取消息失败: ' + error.message
|
|
}));
|
|
}
|
|
break;
|
|
|
|
case 'close_conversation':
|
|
// 关闭会话
|
|
if (!conversationId) {
|
|
throw new Error('未指定会话ID');
|
|
}
|
|
|
|
const status = connection.isUser ? 3 : 2;
|
|
await sequelize.query(
|
|
'UPDATE chat_conversations SET status = ? WHERE conversation_id = ?',
|
|
{ replacements: [status, conversationId] }
|
|
);
|
|
|
|
ws.send(JSON.stringify({
|
|
type: 'conversation_closed',
|
|
payload: { conversationId }
|
|
}));
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
console.error('处理会话消息失败:', error);
|
|
ws.send(JSON.stringify({
|
|
type: 'error',
|
|
message: error.message
|
|
}));
|
|
}
|
|
}
|
|
|
|
// 新增:获取聊天列表数据接口 - 根据用户手机号查询
|
|
app.post('/api/chat/list', async (req, res) => {
|
|
try {
|
|
const { user_phone } = req.body;
|
|
console.log('获取聊天列表 - user_phone:', user_phone);
|
|
|
|
if (!user_phone) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '用户手机号不能为空'
|
|
});
|
|
}
|
|
|
|
// 从wechat_app数据库的chat_list表中查询用户的聊天记录
|
|
const chatList = await sequelize.query(
|
|
'SELECT * FROM chat_list WHERE user_phone = ?',
|
|
{
|
|
replacements: [user_phone],
|
|
type: Sequelize.QueryTypes.SELECT
|
|
}
|
|
);
|
|
|
|
console.log('找到聊天记录数量:', chatList.length);
|
|
|
|
// 如果没有聊天记录,返回空数组
|
|
if (chatList.length === 0) {
|
|
return res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '暂无聊天记录',
|
|
data: []
|
|
});
|
|
}
|
|
|
|
// 返回聊天列表数据
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '获取聊天列表成功',
|
|
data: chatList
|
|
});
|
|
} catch (error) {
|
|
console.error('获取聊天列表失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取聊天列表失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 新增:删除聊天记录
|
|
app.post('/api/chat/delete', async (req, res) => {
|
|
try {
|
|
const { user_phone, manager_phone } = req.body;
|
|
console.log('删除聊天记录 - user_phone:', user_phone, 'manager_phone:', manager_phone);
|
|
|
|
if (!user_phone || !manager_phone) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '用户手机号和业务员手机号不能为空'
|
|
});
|
|
}
|
|
|
|
// 删除chat_list表中的记录
|
|
const deleteResult = await sequelize.query(
|
|
'DELETE FROM chat_list WHERE user_phone = ? AND manager_phone = ?',
|
|
{ replacements: [user_phone, manager_phone], type: sequelize.QueryTypes.DELETE }
|
|
);
|
|
|
|
console.log('删除聊天记录结果:', deleteResult);
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '删除成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('删除聊天记录时出错:', error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '删除失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 新增:添加聊天记录到chat_list表
|
|
app.post('/api/chat/updateUnread', async (req, res) => {
|
|
try {
|
|
const { chat_id, increment = 1, user_phone, manager_phone } = req.body;
|
|
console.log('更新未读消息数 - chat_id:', chat_id, 'increment:', increment, 'user_phone:', user_phone, 'manager_phone:', manager_phone);
|
|
|
|
// 支持两种更新方式:根据chat_id或根据user_phone+manager_phone
|
|
if (!chat_id && !(user_phone && manager_phone)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '聊天ID或用户手机号+客服手机号不能为空'
|
|
});
|
|
}
|
|
|
|
let query, replacements;
|
|
if (chat_id) {
|
|
// 根据chat_id更新
|
|
query = 'UPDATE chat_list SET unread = unread + ? WHERE id = ?';
|
|
replacements = [increment, chat_id];
|
|
} else {
|
|
// 根据user_phone和manager_phone更新
|
|
query = 'UPDATE chat_list SET unread = unread + ? WHERE (user_phone = ? AND manager_phone = ?) OR (user_phone = ? AND manager_phone = ?)';
|
|
replacements = [increment, user_phone, manager_phone, manager_phone, user_phone];
|
|
}
|
|
|
|
// 更新聊天列表的未读消息数
|
|
await sequelize.query(query, { replacements });
|
|
|
|
console.log('✅ 更新未读消息数成功');
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '更新未读消息数成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('更新未读消息数失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '更新未读消息数失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
app.post('/api/chat/add', async (req, res) => {
|
|
try {
|
|
const { user_phone, manager_phone, unread = 0 } = req.body;
|
|
console.log('添加聊天记录 - user_phone:', user_phone, 'manager_phone:', manager_phone);
|
|
|
|
if (!user_phone || !manager_phone) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '用户手机号和业务员手机号不能为空'
|
|
});
|
|
}
|
|
|
|
// 检查chat_list表是否存在,如果不存在则创建
|
|
try {
|
|
await sequelize.query(
|
|
`CREATE TABLE IF NOT EXISTS chat_list (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
user_phone VARCHAR(20) NOT NULL,
|
|
manager_phone VARCHAR(20) NOT NULL,
|
|
unread INT DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
INDEX idx_user_phone (user_phone),
|
|
INDEX idx_manager_phone (manager_phone),
|
|
UNIQUE KEY idx_user_manager (user_phone, manager_phone)
|
|
) ENGINE=InnoDB;`
|
|
);
|
|
console.log('✅ chat_list表检查/创建成功');
|
|
} catch (createError) {
|
|
console.warn('创建chat_list表时出错(可能已存在):', createError.message);
|
|
}
|
|
|
|
// 如果表已存在但没有唯一索引,尝试添加
|
|
try {
|
|
await sequelize.query(
|
|
'ALTER TABLE chat_list ADD CONSTRAINT IF NOT EXISTS idx_user_manager UNIQUE (user_phone, manager_phone)'
|
|
);
|
|
console.log('✅ 尝试添加唯一索引成功');
|
|
} catch (indexError) {
|
|
console.warn('添加唯一索引失败(可能已存在):', indexError.message);
|
|
}
|
|
|
|
// 先查询是否已存在两条记录
|
|
const [existingRecords] = await sequelize.query(
|
|
`SELECT user_phone, manager_phone FROM chat_list WHERE
|
|
(user_phone = ? AND manager_phone = ?) OR
|
|
(user_phone = ? AND manager_phone = ?)`,
|
|
{ replacements: [user_phone, manager_phone, manager_phone, user_phone] }
|
|
);
|
|
|
|
// 统计现有记录
|
|
const hasRecord1 = existingRecords.some(record =>
|
|
record.user_phone === user_phone && record.manager_phone === manager_phone
|
|
);
|
|
const hasRecord2 = existingRecords.some(record =>
|
|
record.user_phone === manager_phone && record.manager_phone === user_phone
|
|
);
|
|
|
|
console.log('记录检查结果 - 记录1存在:', hasRecord1, '记录2存在:', hasRecord2);
|
|
|
|
// 只插入不存在的记录
|
|
let insertedCount = 0;
|
|
if (!hasRecord1) {
|
|
await sequelize.query(
|
|
'INSERT INTO chat_list (user_phone, manager_phone, unread) VALUES (?, ?, ?)',
|
|
{ replacements: [user_phone, manager_phone, unread] }
|
|
);
|
|
insertedCount++;
|
|
console.log('✅ 插入记录1成功: user_phone -> manager_phone');
|
|
} else {
|
|
console.log('ℹ️ 记录1已存在: user_phone -> manager_phone');
|
|
}
|
|
|
|
if (!hasRecord2) {
|
|
await sequelize.query(
|
|
'INSERT INTO chat_list (user_phone, manager_phone, unread) VALUES (?, ?, ?)',
|
|
{ replacements: [manager_phone, user_phone, unread] }
|
|
);
|
|
insertedCount++;
|
|
console.log('✅ 插入记录2成功: manager_phone -> user_phone');
|
|
} else {
|
|
console.log('ℹ️ 记录2已存在: manager_phone -> user_phone');
|
|
}
|
|
|
|
console.log(`✅ 聊天记录处理完成,新插入 ${insertedCount} 条记录`);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: '聊天记录添加成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('添加聊天记录失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '添加聊天记录失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 新增:获取业务员信息接口 - 根据手机号查询
|
|
app.post('/api/personnel/get', async (req, res) => {
|
|
try {
|
|
const { phone } = req.body;
|
|
console.log('获取业务员信息 - phone:', phone);
|
|
|
|
if (!phone) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '手机号不能为空'
|
|
});
|
|
}
|
|
|
|
// 从userlogin数据库的personnel表中查询业务员信息
|
|
const personnel = await userLoginSequelize.query(
|
|
'SELECT * FROM personnel WHERE phoneNumber = ?',
|
|
{
|
|
replacements: [phone],
|
|
type: Sequelize.QueryTypes.SELECT
|
|
}
|
|
);
|
|
|
|
console.log('找到业务员信息数量:', personnel.length);
|
|
|
|
if (personnel.length === 0) {
|
|
return res.status(200).json({
|
|
success: false,
|
|
code: 404,
|
|
message: '未找到该手机号对应的业务员信息'
|
|
});
|
|
}
|
|
|
|
// 返回所有匹配的业务员信息
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '获取业务员信息成功',
|
|
data: personnel
|
|
});
|
|
} catch (error) {
|
|
console.error('获取业务员信息失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '获取业务员信息失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 估价API端点 - 基于resources表的历史价格数据
|
|
app.post('/api/evaluate/price', async (req, res) => {
|
|
try {
|
|
// 获取原始请求体,并检查编码问题
|
|
console.log('原始请求体:', JSON.stringify(req.body));
|
|
|
|
// 提取参数并进行编码检查
|
|
const { month, day, region, breed } = req.body;
|
|
|
|
// 记录详细的参数信息
|
|
console.log('请求参数详细信息:', {
|
|
month: {
|
|
value: month,
|
|
type: typeof month,
|
|
isEmpty: !month,
|
|
isValid: !!month
|
|
},
|
|
day: {
|
|
value: day,
|
|
type: typeof day,
|
|
isEmpty: !day,
|
|
isValid: !!day
|
|
},
|
|
region: {
|
|
value: region,
|
|
type: typeof region,
|
|
isEmpty: !region,
|
|
isValid: !!region,
|
|
isEncoded: /[?%]/.test(region || '')
|
|
},
|
|
breed: {
|
|
value: breed,
|
|
type: typeof breed,
|
|
isEmpty: !breed,
|
|
isValid: !!breed,
|
|
isEncoded: /[?%]/.test(breed || '')
|
|
}
|
|
});
|
|
|
|
// 检查参数有效性
|
|
if (!month || !day || !region || !breed) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要参数:month, day, region, breed'
|
|
});
|
|
}
|
|
|
|
// 构建查询日期
|
|
const currentYear = new Date().getFullYear();
|
|
const targetDate = new Date(currentYear, parseInt(month) - 1, parseInt(day));
|
|
const previousYear = currentYear - 1;
|
|
const twoYearsAgo = currentYear - 2;
|
|
|
|
// 格式化日期为YYYY-MM-DD
|
|
const formatDate = (date) => {
|
|
// 确保日期格式正确
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
};
|
|
|
|
// 构建查询日期列表
|
|
const queryDates = [
|
|
formatDate(new Date(previousYear, parseInt(month) - 1, parseInt(day))),
|
|
formatDate(new Date(twoYearsAgo, parseInt(month) - 1, parseInt(day))),
|
|
formatDate(new Date(currentYear, parseInt(month) - 1, parseInt(day) - 1))
|
|
];
|
|
|
|
console.log('查询日期列表:', queryDates);
|
|
console.log('查询参数:', {
|
|
region: region,
|
|
category: breed,
|
|
timeIn: queryDates
|
|
});
|
|
|
|
// 查询历史价格数据(使用time字段和category字段)
|
|
const historicalData = await Resources.findAll({
|
|
where: {
|
|
region: region,
|
|
category: breed, // 使用category字段映射breed参数
|
|
time: {
|
|
[Op.in]: queryDates
|
|
}
|
|
},
|
|
order: [['time', 'DESC']]
|
|
});
|
|
|
|
console.log('查询到的历史数据数量:', historicalData.length);
|
|
|
|
// 如果找到了数据,输出详细数据用于调试
|
|
if (historicalData.length > 0) {
|
|
console.log('历史数据详情:', historicalData.map(item => ({
|
|
id: item.id,
|
|
category: item.category,
|
|
region: item.region,
|
|
time: item.time,
|
|
price1: item.price1,
|
|
price2: item.price2
|
|
})));
|
|
}
|
|
|
|
// 如果没有足够的历史数据,返回默认值
|
|
if (historicalData.length === 0) {
|
|
return res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '数据不足,使用默认估价',
|
|
data: {
|
|
estimatedPrice: 4.50,
|
|
priceRange: '4.05 - 4.95',
|
|
confidence: '75%',
|
|
trend: 'stable',
|
|
changePercent: 0,
|
|
dataSource: 'default',
|
|
explanation: '由于历史数据不足,使用默认价格估算'
|
|
}
|
|
});
|
|
}
|
|
|
|
// 处理历史价格数据(使用price1和price2字段)
|
|
const priceData = historicalData.map(item => {
|
|
// 计算平均价格作为实际价格
|
|
const price1 = parseFloat(item.price1) || 0;
|
|
const price2 = parseFloat(item.price2) || 0;
|
|
const actualPrice = (price1 + price2) / 2 || 4.5; // 如果没有价格数据,使用默认价格4.5
|
|
|
|
return {
|
|
date: item.time,
|
|
price: actualPrice,
|
|
price1: price1,
|
|
price2: price2,
|
|
year: new Date(item.time).getFullYear()
|
|
};
|
|
});
|
|
|
|
// 计算价格趋势
|
|
const latestPrice = priceData[0]?.price || 4.5;
|
|
const previousYearPrice = priceData.find(p => p.year === previousYear)?.price || latestPrice;
|
|
const twoYearsAgoPrice = priceData.find(p => p.year === twoYearsAgo)?.price || latestPrice;
|
|
const previousDayPrice = priceData.find(p => p.year === currentYear)?.price || latestPrice;
|
|
|
|
// 计算涨幅(与前一天比较)
|
|
const priceChange = latestPrice - previousDayPrice;
|
|
const changePercent = previousDayPrice > 0 ? (priceChange / previousDayPrice) * 100 : 0;
|
|
|
|
// 判断趋势
|
|
let trend = 'stable';
|
|
if (changePercent > 0.2) {
|
|
trend = 'rising';
|
|
} else if (changePercent < -0.2) {
|
|
trend = 'falling';
|
|
}
|
|
|
|
// 计算预估价格(基于历史数据和趋势)
|
|
let estimatedPrice = latestPrice;
|
|
if (changePercent > 0.2) {
|
|
// 涨幅超过0.2%,小幅上涨
|
|
estimatedPrice = latestPrice * (1 + Math.min(changePercent / 100, 0.02));
|
|
} else if (changePercent < -0.2) {
|
|
// 跌幅超过0.2%,小幅下跌
|
|
estimatedPrice = latestPrice * (1 + Math.max(changePercent / 100, -0.02));
|
|
}
|
|
|
|
// 确保价格合理范围
|
|
estimatedPrice = Math.max(estimatedPrice, 3.0); // 最低3元/斤
|
|
estimatedPrice = Math.min(estimatedPrice, 8.0); // 最高8元/斤
|
|
|
|
// 计算价格区间
|
|
const priceRangeMin = (estimatedPrice * 0.9).toFixed(2);
|
|
const priceRangeMax = (estimatedPrice * 1.1).toFixed(2);
|
|
|
|
// 计算置信度
|
|
let confidence = '80%';
|
|
if (historicalData.length >= 3) {
|
|
confidence = '90%';
|
|
} else if (historicalData.length >= 2) {
|
|
confidence = '85%';
|
|
}
|
|
|
|
// 构建响应数据
|
|
const responseData = {
|
|
estimatedPrice: estimatedPrice.toFixed(2),
|
|
priceRange: `${priceRangeMin} - ${priceRangeMax}`,
|
|
confidence: confidence,
|
|
trend: trend,
|
|
changePercent: changePercent.toFixed(2),
|
|
dataSource: 'historical',
|
|
explanation: `基于${historicalData.length}条历史数据,结合当前趋势进行估算`,
|
|
historicalPrices: priceData,
|
|
comparison: {
|
|
vsPreviousDay: {
|
|
price: previousDayPrice.toFixed(2),
|
|
change: priceChange.toFixed(2),
|
|
changePercent: changePercent.toFixed(2) + '%'
|
|
},
|
|
vsLastYear: {
|
|
price: previousYearPrice.toFixed(2),
|
|
change: (latestPrice - previousYearPrice).toFixed(2),
|
|
changePercent: ((latestPrice - previousYearPrice) / previousYearPrice * 100).toFixed(2) + '%'
|
|
}
|
|
}
|
|
};
|
|
|
|
console.log('估价结果:', responseData);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
code: 200,
|
|
message: '估价成功',
|
|
data: responseData
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('估价API错误:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '估价失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 订单查询接口 - 根据用户电话号码查询订单
|
|
app.get('/api/orders', async (req, res) => {
|
|
try {
|
|
const { phone } = req.query;
|
|
|
|
if (!phone) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少phone参数'
|
|
});
|
|
}
|
|
|
|
console.log('查询订单:', { phone });
|
|
|
|
// 查询主表订单信息
|
|
const orders = await tradeLibrarySequelize.query(
|
|
`SELECT * FROM jd_sales_main WHERE phone = ? ORDER BY order_date DESC`,
|
|
{
|
|
replacements: [phone],
|
|
type: tradeLibrarySequelize.QueryTypes.SELECT
|
|
}
|
|
);
|
|
|
|
// 查询每个订单的子表信息
|
|
const ordersWithDetails = await Promise.all(orders.map(async (order) => {
|
|
const details = await tradeLibrarySequelize.query(
|
|
`SELECT * FROM jd_sales_sub WHERE dataid = ?`,
|
|
{
|
|
replacements: [order.dataid],
|
|
type: tradeLibrarySequelize.QueryTypes.SELECT
|
|
}
|
|
);
|
|
|
|
return {
|
|
...order,
|
|
details
|
|
};
|
|
}));
|
|
|
|
console.log('订单查询结果:', { count: ordersWithDetails.length });
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '订单查询成功',
|
|
data: {
|
|
orders: ordersWithDetails,
|
|
total: ordersWithDetails.length
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('订单查询失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '订单查询失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 新增:增加用户估价点击次数接口
|
|
app.post('/api/user/increment-appraisal', async (req, res) => {
|
|
try {
|
|
const { openid, userId } = req.body;
|
|
console.log('增加估价次数请求:', { openid, userId });
|
|
|
|
if (!openid || !userId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要参数'
|
|
});
|
|
}
|
|
|
|
// 更新用户的估价点击次数(使用 COALESCE 处理 NULL 值)
|
|
const [result] = await sequelize.query(
|
|
'UPDATE users SET appraisalnum = COALESCE(appraisalnum, 0) + 1 WHERE openid = ? OR userId = ?',
|
|
{ replacements: [openid, userId] }
|
|
);
|
|
|
|
console.log('增加估价次数结果:', result, '影响行数:', result.affectedRows);
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '增加估价次数成功',
|
|
affectedRows: result.affectedRows
|
|
});
|
|
} catch (error) {
|
|
console.error('增加估价次数失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '增加估价次数失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 新增:增加用户对比价格点击次数接口
|
|
app.post('/api/user/increment-compare', async (req, res) => {
|
|
try {
|
|
const { openid, userId } = req.body;
|
|
console.log('增加对比价格次数请求:', { openid, userId });
|
|
|
|
if (!openid || !userId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
code: 400,
|
|
message: '缺少必要参数'
|
|
});
|
|
}
|
|
|
|
// 更新用户的对比价格点击次数(使用 COALESCE 处理 NULL 值)
|
|
const [result] = await sequelize.query(
|
|
'UPDATE users SET comparenum = COALESCE(comparenum, 0) + 1 WHERE openid = ? OR userId = ?',
|
|
{ replacements: [openid, userId] }
|
|
);
|
|
|
|
console.log('增加对比价格次数结果:', result, '影响行数:', result.affectedRows);
|
|
|
|
res.json({
|
|
success: true,
|
|
code: 200,
|
|
message: '增加对比价格次数成功',
|
|
affectedRows: result.affectedRows
|
|
});
|
|
} catch (error) {
|
|
console.error('增加对比价格次数失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
code: 500,
|
|
message: '增加对比价格次数失败: ' + error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// 在服务器启动前执行商品联系人更新
|
|
updateProductContacts().then(() => {
|
|
console.log('\n📦 商品联系人信息更新完成!');
|
|
}).catch(error => {
|
|
console.error('\n❌ 商品联系人信息更新失败:', error.message);
|
|
}).finally(() => {
|
|
// 无论更新成功与否,都启动服务器
|
|
// 启动服务器监听 - 使用配置好的http server对象
|
|
// 监听0.0.0.0以允许通过所有网络接口访问(包括IPv4地址)
|
|
// 启动连接监控
|
|
startConnectionMonitoring();
|
|
console.log('连接监控服务已启动');
|
|
|
|
// 调试API - 查看resources表结构
|
|
app.get('/api/debug/resources-table', async (req, res) => {
|
|
try {
|
|
// 查询表结构
|
|
const [tableStructure] = await sequelize.query('DESCRIBE resources');
|
|
console.log('Resources表结构:', tableStructure);
|
|
|
|
// 查询示例数据
|
|
const sampleData = await sequelize.query('SELECT * FROM resources LIMIT 5');
|
|
console.log('Resources示例数据:', sampleData[0]);
|
|
|
|
res.json({
|
|
success: true,
|
|
tableStructure: tableStructure,
|
|
sampleData: sampleData[0]
|
|
});
|
|
} catch (error) {
|
|
console.error('调试API错误:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`\n🚀 服务器启动成功,监听端口 ${PORT}`);
|
|
console.log(`API 服务地址: http://localhost:${PORT}`);
|
|
console.log(`API 通过IP访问地址: http://192.168.0.98:${PORT}`);
|
|
console.log(`WebSocket 服务地址: ws://localhost:${PORT}`);
|
|
console.log(`服务器最大连接数限制: ${server.maxConnections}`);
|
|
console.log(`WebSocket 服务器已启动,等待连接...`);
|
|
});
|
|
});
|
|
|
|
// 导出模型和Express应用供其他模块使用
|
|
module.exports = {
|
|
User,
|
|
Product,
|
|
CartItem,
|
|
Resources,
|
|
sequelize,
|
|
createUserAssociations,
|
|
app,
|
|
PORT
|
|
};
|