Files
jurong_circle_black/routes/products.js

1359 lines
45 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const express = require('express');
const { getDB } = require('../database');
const { auth, adminAuth } = require('../middleware/auth');
const router = express.Router();
/**
* @swagger
* /api/products:
* get:
* summary: 获取商品列表
* description: 返回商品列表,支持分页、搜索、分类、状态过滤
* tags: [products]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* description: 页码默认1
* - in: query
* name: limit
* schema:
* type: integer
* description: 每页数量默认10最大100
* - in: query
* name: search
* schema:
* type: string
* description: 搜索商品名称
* - in: query
* name: category
* schema:
* type: string
* description: 分类名称
* - in: query
* name: status
* schema:
* type: string
* description: 商品状态active/inactive
* responses:
* 200:
* description: 成功返回商品列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: array
* items:
* type: object
* properties:
* id:
* type: string
* example: "1"
* name:
* type: string
* example: "商品A"
* rongdou_price:
* type: number
* example: 100
* points_price:
* type: number
* example: 1000
* stock:
* type: integer
* example: 100
* image:
* type: string
* example: "https://example.com/image.jpg"
* description:
* type: string
* example: "这是一个商品"
* status:
* type: string
* example: "active"
* category_id:
* type: string
* example: "1"
* created_at:
* type: string
* format: date-time
* example: "2023-01-01T00:00:00Z"
* updated_at:
* type: string
* format: date-time
* example: "2023-01-01T00:00:00Z"
* sales:
* type: integer
* example: 100
* images:
* type: array
* items:
* type: string
* example: "https://example.com/image.jpg"
* 400:
* description: 无效的分页参数
* 500:
* description: 服务器内部错误
*/
// 商品管理路由
router.get('/', async (req, res) => {
try {
const { page = 1, limit = 10, search = '', category = '', status = '' } = req.query;
// 确保参数为有效数字
const pageNum = Math.max(1, parseInt(page) || 1);
const limitNum = Math.max(1, Math.min(100, parseInt(limit) || 10)); // 限制最大100条
const offset = Math.max(0, (pageNum - 1) * limitNum);
let filteredProducts = []
console.log('分页参数:', { pageNum, limitNum, offset, search, category, status });
let whereClause = 'WHERE 1=1';
const params = [];
if (search) {
whereClause += ' AND name LIKE ?';
params.push(`%${search}%`);
}
if (status) {
whereClause += ' AND status = ?';
params.push(status);
} else {
whereClause += ' AND status = "active"';
}
// 获取总数
const countQuery = `SELECT COUNT(*) as total FROM products ${whereClause}`;
const [countResult] = await getDB().execute(countQuery, params);
const total = countResult[0].total;
// 获取商品列表
const query = `
SELECT id, name, rongdou_price, points_price, stock, image_url as image, description, status, payment_methods, created_at, updated_at, sales, images
FROM products
${whereClause}
ORDER BY created_at DESC
LIMIT ${limitNum} OFFSET ${offset}
`;
// 确保参数数组正确传递
const queryParams = [...params];
console.log('Query params:', queryParams, 'Query:', query);
const [products] = await getDB().execute(query, queryParams);
products.forEach(item=>{
item.payment_methods = JSON.parse(item.payment_methods)
item.images = JSON.parse(item.images)
})
if (category) {
// 先根据分类名称获取分类ID
const query = `SELECT * FROM category WHERE category_name = ?`
const [getCategory] = await getDB().execute(query, [category])
const [getSecondCategory] = await getDB().execute('SELECT * FROM category WHERE parent_id = ?', [getCategory[0].id])
const sumCategory = getCategory.concat(getSecondCategory).map(item=>item.id)
// 再根据分类ID获取商品ID
const getProductCategory = []
for (const item of sumCategory) {
const [getProductCategoryItem] = await getDB().execute('SELECT * FROM products_category WHERE category_id = ?', [item])
getProductCategory.push(...getProductCategoryItem)
}
const productIds = []
for (const item of getProductCategory) {
productIds.push(item.product_id)
}
filteredProducts = products.filter(item=>productIds.includes(item.id))
} else {
filteredProducts = products
}
res.json({
success: true,
data: {
products: filteredProducts,
// products: products,
pagination: {
page: pageNum,
limit: limitNum,
total,
pages: Math.ceil(total / limitNum)
}
}
});
} catch (error) {
console.error('获取商品列表失败:', error);
res.status(500).json({ success: false, message: '获取商品列表失败' });
}
});
// // 获取商品分类列表
// router.get('/categories', async (req, res) => {
// try {
// const [categories] = await getDB().execute(
// 'SELECT DISTINCT category FROM products WHERE status = "active" AND category IS NOT NULL'
// );
// res.json({
// success: true,
// data: {
// categories: categories.map(item => item.category)
// }
// });
// } catch (error) {
// console.error('获取商品分类失败:', error);
// res.status(500).json({ success: false, message: '获取商品分类失败' });
// }
// });
// 获取热销商品
router.get('/hot', async (req, res) => {
try {
// 从活跃商品中随机获取2个商品
const [products] = await getDB().execute(
`SELECT id, name, price, points_price, rongdou_price, stock,
image_url, images, description, shop_name, shop_avatar,
payment_methods, sales, rating, status, created_at, updated_at
FROM products
WHERE status = 'active' AND stock > 0
ORDER BY sales DESC
LIMIT 2`
);
// 格式化商品数据
const formattedProducts = products.map(product => ({
...product,
images: product.images ? JSON.parse(product.images) : (product.image_url ? [product.image_url] : []),
payment_methods: product.payment_methods ? JSON.parse(product.payment_methods) : ['points'],
// 保持向后兼容
points: product.points_price,
image: product.image_url
}));
res.json({
success: true,
data: {
products: formattedProducts
}
});
} catch (error) {
console.error('获取热销商品失败:', error);
res.status(500).json({ success: false, message: '获取热销商品失败' });
}
});
/**
* @swagger
* /products/flash-sale:
* get:
* summary: 获取秒杀商品
* tags: [Products]
* responses:
* 200:
* description: 成功获取秒杀商品
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* products:
* type: array
* items:
* $ref: '#/components/schemas/Product'
*/
router.get('/cheap', async (req, res) => {
try {
const [products] = await getDB().execute(
`SELECT id, start_time, end_time, flash_stock, flash_price, products_id
FROM flash_product
WHERE end_time > NOW() AND flash_stock > 0
ORDER BY RAND()
LIMIT 2`
);
const tempProducts = await Promise.all(products.map(async item=>{
const [product] = await getDB().execute(
`SELECT id, name, price, points_price, rongdou_price, stock,
image_url, images, description, shop_name, shop_avatar,
payment_methods, sales, rating, status, created_at, updated_at
FROM products
WHERE id = ?`,
[item.products_id]
)
item = {
...product[0],
images: product.images ? JSON.parse(product.images) : (product.image_url ? [product.image_url] : []),
payment_methods: product.payment_methods ? JSON.parse(product.payment_methods) : ['points'],
...item,
points: product.points_price,
image: product.image_url,
id: product[0].id,
}
return item
}))
res.json({
success: true,
data: {
products: tempProducts
}
});
} catch (error) {
console.error('获取秒杀商品失败:', error);
res.status(500).json({ success: false, message: '获取秒杀商品失败' });
}
});
/**
* @swagger
* /products/{id}:
* get:
* summary: 获取单个商品详情(包含增强规格信息)
* tags: [Products]
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: 商品ID
* responses:
* 200:
* description: 成功获取商品详情,包含完整的规格信息
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: object
* properties:
* product:
* type: object
* properties:
* id:
* type: integer
* name:
* type: string
* category:
* type: string
* price:
* type: number
* points_price:
* type: number
* rongdou_price:
* type: number
* stock:
* type: integer
* specifications:
* type: array
* description: 商品规格组合列表(笛卡尔积规格系统)
* items:
* type: object
* properties:
* id:
* type: integer
* description: 规格组合ID
* combination_key:
* type: string
* description: 规格组合键1-3-5
* spec_display:
* type: string
* description: 规格显示文本(如:颜色:红色 | 尺寸:XL
* spec_details:
* type: array
* description: 规格详细信息
* items:
* type: object
* properties:
* id:
* type: integer
* spec_name:
* type: string
* description: 规格名称
* spec_display_name:
* type: string
* description: 规格显示名称
* value:
* type: string
* description: 规格值
* display_value:
* type: string
* description: 规格显示值
* color_code:
* type: string
* description: 颜色代码
* image_url:
* type: string
* description: 规格图片
* price_adjustment:
* type: number
* description: 价格调整
* points_adjustment:
* type: number
* description: 积分调整
* rongdou_adjustment:
* type: number
* description: 融豆调整
* stock:
* type: integer
* description: 规格库存
* sku_code:
* type: string
* description: SKU编码
* barcode:
* type: string
* description: 条形码
* weight:
* type: number
* description: 重量
* volume:
* type: number
* description: 体积
* actual_price:
* type: number
* description: 实际价格(基础价格+调整)
* actual_points_price:
* type: number
* description: 实际积分价格
* actual_rongdou_price:
* type: number
* description: 实际融豆价格
* is_available:
* type: boolean
* description: 是否有库存
* specification_count:
* type: integer
* description: 规格总数
* available_specifications:
* type: integer
* description: 有库存的规格数量
* attributes:
* type: array
* description: 商品属性
* isFavorited:
* type: boolean
* description: 是否已收藏
* 404:
* description: 商品不存在
*/
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const userId = req.user?.id; // 可选的用户ID用于检查收藏状态
const query = `
SELECT id, name, price, points_price, rongdou_price, stock,
image_url, images, videos, description, details, shop_name, shop_avatar,
payment_methods, sales, rating, status, created_at, updated_at
FROM products
WHERE id = ? AND status = 'active'
`;
const [products] = await getDB().execute(query, [id]);
if (products.length === 0) {
return res.status(404).json({ success: false, message: '商品不存在' });
}
const product = products[0];
// 获取商品的规格组合(新的笛卡尔积规格系统)
const [specCombinations] = await getDB().execute(
`SELECT psc.*,
GROUP_CONCAT(CONCAT(sn.display_name, ':', sv.display_value) ORDER BY sn.sort_order SEPARATOR ' | ') as spec_display
FROM product_spec_combinations psc
LEFT JOIN JSON_TABLE(psc.spec_values, '$[*]' COLUMNS (spec_value_id INT PATH '$')) jt ON TRUE
LEFT JOIN spec_values sv ON jt.spec_value_id = sv.id
LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id
WHERE psc.product_id = ? AND psc.status = 'active'
GROUP BY psc.id
ORDER BY psc.combination_key`,
[id]
);
// console.log(123,specCombinations);
// 为每个规格组合获取详细的规格值信息
const enhancedSpecifications = [];
for (const combination of specCombinations) {
// 智能解析 spec_values 字段,兼容多种数据格式
let specValueIds = [];
try {
if (combination.spec_values) {
// 如果是 Buffer 对象,先转换为字符串
let specValuesStr = combination.spec_values;
if (Buffer.isBuffer(specValuesStr)) {
specValuesStr = specValuesStr.toString('utf8');
}
// 尝试 JSON 解析
if (typeof specValuesStr === 'string') {
specValuesStr = specValuesStr.trim();
if (specValuesStr.startsWith('[') && specValuesStr.endsWith(']')) {
// JSON 数组格式
specValueIds = JSON.parse(specValuesStr);
} else if (specValuesStr.includes(',')) {
// 逗号分隔的字符串格式
specValueIds = specValuesStr.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
} else if (specValuesStr && !isNaN(parseInt(specValuesStr))) {
// 单个数字
specValueIds = [parseInt(specValuesStr)];
}
} else if (Array.isArray(specValuesStr)) {
// 已经是数组
specValueIds = specValuesStr;
}
}
} catch (parseError) {
console.warn(`解析规格值失败 (combination_id: ${combination.id}):`, parseError.message);
specValueIds = [];
}
// 获取规格值详情
if (specValueIds && specValueIds.length > 0) {
const placeholders = specValueIds.map(() => '?').join(',');
const [specDetails] = await getDB().execute(
`SELECT sv.*, sn.name as spec_name, sn.display_name as spec_display_name
FROM spec_values sv
LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id
WHERE sv.id IN (${placeholders})
ORDER BY sn.sort_order, sv.sort_order`,
specValueIds
);
enhancedSpecifications.push({
id: combination.id,
combination_key: combination.combination_key,
spec_display: combination.spec_display,
spec_details: specDetails,
price_adjustment: combination.price_adjustment || 0,
points_adjustment: combination.points_adjustment || 0,
rongdou_adjustment: combination.rongdou_adjustment || 0,
stock: combination.stock,
sku_code: combination.sku_code,
barcode: combination.barcode,
weight: combination.weight,
volume: combination.volume,
actual_price: product.price + (combination.price_adjustment || 0),
actual_points_price: product.points_price + (combination.points_adjustment || 0),
actual_rongdou_price: product.rongdou_price + (combination.rongdou_adjustment || 0),
is_available: combination.stock > 0,
status: combination.status,
created_at: combination.created_at,
updated_at: combination.updated_at
});
}
}
// console.log(123,enhancedSpecifications);
// 获取商品属性
const [attributes] = await getDB().execute(
'SELECT * FROM product_attributes WHERE product_id = ? ORDER BY sort_order, id',
[id]
);
// 检查用户是否收藏了该商品
let isFavorited = false;
if (userId) {
const [favorites] = await getDB().execute(
'SELECT id FROM product_favorites WHERE user_id = ? AND product_id = ?',
[userId, id]
);
isFavorited = favorites.length > 0;
}
// 构建增强的商品数据
const enhancedProduct = {
...product,
images: (() => {
try {
if (product.images) {
let imagesStr = product.images;
if (Buffer.isBuffer(imagesStr)) {
imagesStr = imagesStr.toString('utf8');
}
if (typeof imagesStr === 'string') {
imagesStr = imagesStr.trim();
if (imagesStr.startsWith('[') && imagesStr.endsWith(']')) {
return JSON.parse(imagesStr);
}
}
}
return product.image_url ? [product.image_url] : [];
} catch (e) {
console.warn('解析商品图片失败:', e.message);
return product.image_url ? [product.image_url] : [];
}
})(),
videos: (() => {
try {
if (product.videos) {
let videosStr = product.videos;
if (Buffer.isBuffer(videosStr)) {
videosStr = videosStr.toString('utf8');
}
if (typeof videosStr === 'string') {
videosStr = videosStr.trim();
if (videosStr.startsWith('[') && videosStr.endsWith(']')) {
return JSON.parse(videosStr);
}
}
}
return [];
} catch (e) {
console.warn('解析商品视频失败:', e.message);
return [];
}
})(),
payment_methods: (() => {
try {
if (product.payment_methods) {
let methodsStr = product.payment_methods;
if (Buffer.isBuffer(methodsStr)) {
methodsStr = methodsStr.toString('utf8');
}
if (typeof methodsStr === 'string') {
methodsStr = methodsStr.trim();
if (methodsStr.startsWith('[') && methodsStr.endsWith(']')) {
return JSON.parse(methodsStr);
}
}
}
return ['points'];
} catch (e) {
console.warn('解析支付方式失败:', e.message);
return ['points'];
}
})(),
specifications: enhancedSpecifications,
attributes,
isFavorited,
// 规格统计信息
specification_count: enhancedSpecifications.length,
available_specifications: enhancedSpecifications.filter(spec => spec.is_available).length,
// 保持向后兼容
points: product.points_price,
image: product.image_url,
// tags: product.category ? [product.category] : []
};
res.json({
success: true,
data: { product: enhancedProduct }
});
} catch (error) {
console.error('获取商品详情失败:', error);
res.status(500).json({ success: false, message: '获取商品详情失败' });
}
});
// 创建商品(管理员权限)
router.post('/', auth, adminAuth, async (req, res) => {
try {
const {
name, description, price, points_price, rongdou_price = 0, stock, category,
image_url, images = [], videos = [], details, status = 'active',
shop_name, shop_avatar, payment_methods = ['points', 'rongdou', 'points_rongdou'],
specifications = [], attributes = []
} = req.body;
if (!name || !price || (!points_price && !rongdou_price) || stock === undefined) {
return res.status(400).json({ message: '商品名称、原价、积分价格或融豆价格、库存不能为空' });
}
const [result] = await getDB().execute(
`INSERT INTO products (name, description, price, points_price, rongdou_price, stock, category,
image_url, images, videos, details, shop_name, shop_avatar, payment_methods, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
[name, description, price, points_price, rongdou_price, stock, category || null,
image_url, JSON.stringify(images), JSON.stringify(videos), details,
shop_name, shop_avatar, JSON.stringify(payment_methods), status]
);
const productId = result.insertId;
// 添加商品属性
if (attributes && attributes.length > 0) {
for (const attr of attributes) {
await getDB().execute(
`INSERT INTO product_attributes (product_id, attribute_key, attribute_value, sort_order)
VALUES (?, ?, ?, ?)`,
[productId, attr.key, attr.value, attr.sort_order || 0]
);
}
}
res.status(201).json({
success: true,
message: '商品创建成功',
data: { productId }
});
} catch (error) {
console.error('创建商品错误:', error);
res.status(500).json({ message: '创建商品失败' });
}
});
// 更新商品(管理员权限)
router.put('/:id', auth, adminAuth, async (req, res) => {
try {
const productId = req.params.id;
const {
name, description, price, points_price, rongdou_price, stock, category,
image_url, images, videos, details, status, shop_name, shop_avatar, payment_methods,
specifications, attributes
} = req.body;
// 检查商品是否存在
const [products] = await getDB().execute(
'SELECT id FROM products WHERE id = ?',
[productId]
);
if (products.length === 0) {
return res.status(404).json({ message: '商品不存在' });
}
// 构建更新字段
const updateFields = [];
const updateValues = [];
if (name) {
updateFields.push('name = ?');
updateValues.push(name);
}
if (description !== undefined) {
updateFields.push('description = ?');
updateValues.push(description);
}
if (price !== undefined) {
updateFields.push('price = ?');
updateValues.push(price);
}
if (points_price !== undefined) {
updateFields.push('points_price = ?');
updateValues.push(points_price);
}
if (rongdou_price !== undefined) {
updateFields.push('rongdou_price = ?');
updateValues.push(rongdou_price);
}
if (stock !== undefined) {
updateFields.push('stock = ?');
updateValues.push(stock);
}
if (category !== undefined) {
updateFields.push('category = ?');
updateValues.push(category);
}
if (image_url !== undefined) {
updateFields.push('image_url = ?');
updateValues.push(image_url);
}
if (images !== undefined) {
updateFields.push('images = ?');
updateValues.push(JSON.stringify(images || []));
}
if (videos !== undefined) {
updateFields.push('videos = ?');
updateValues.push(JSON.stringify(videos || []));
}
if (details !== undefined) {
updateFields.push('details = ?');
updateValues.push(details);
}
if (shop_name !== undefined) {
updateFields.push('shop_name = ?');
updateValues.push(shop_name);
}
if (shop_avatar !== undefined) {
updateFields.push('shop_avatar = ?');
updateValues.push(shop_avatar);
}
if (payment_methods !== undefined) {
updateFields.push('payment_methods = ?');
updateValues.push(JSON.stringify(payment_methods || []));
}
if (status) {
updateFields.push('status = ?');
updateValues.push(status);
}
if (updateFields.length === 0) {
return res.status(400).json({ message: '没有要更新的字段' });
}
updateFields.push('updated_at = NOW()');
updateValues.push(productId);
await getDB().execute(
`UPDATE products SET ${updateFields.join(', ')} WHERE id = ?`,
updateValues
);
// 更新商品属性
if (attributes !== undefined) {
// 删除原有属性
await getDB().execute('DELETE FROM product_attributes WHERE product_id = ?', [productId]);
// 添加新属性
if (attributes && attributes.length > 0) {
for (const attr of attributes) {
await getDB().execute(
`INSERT INTO product_attributes (product_id, attribute_key, attribute_value, sort_order)
VALUES (?, ?, ?, ?)`,
[productId, attr.key, attr.value, attr.sort_order || 0]
);
}
}
}
res.json({
success: true,
message: '商品更新成功'
});
} catch (error) {
console.error('更新商品错误:', error);
res.status(500).json({ message: '更新商品失败' });
}
});
// 删除商品(管理员权限)
router.delete('/:id', auth, adminAuth, async (req, res) => {
try {
const { id } = req.params;
// 检查商品是否存在
const checkQuery = 'SELECT id FROM products WHERE id = ?';
const [existing] = await getDB().execute(checkQuery, [id]);
if (existing.length === 0) {
return res.status(404).json({ success: false, message: '商品不存在' });
}
// 检查是否有相关订单
const orderCheckQuery = 'SELECT id FROM orders WHERE product_id = ? LIMIT 1';
const [orders] = await getDB().execute(orderCheckQuery, [id]);
if (orders.length > 0) {
return res.status(400).json({ success: false, message: '该商品存在相关订单,无法删除' });
}
const query = 'DELETE FROM products WHERE id = ?';
await getDB().execute(query, [id]);
res.json({
success: true,
message: '商品删除成功'
});
} catch (error) {
console.error('删除商品失败:', error);
res.status(500).json({ success: false, message: '删除商品失败' });
}
});
// 获取商品统计信息(管理员权限)
router.get('/stats', auth, adminAuth, async (req, res) => {
try {
// 获取商品总数
const totalQuery = 'SELECT COUNT(*) as total FROM products';
const [totalResult] = await getDB().execute(totalQuery);
const totalProducts = totalResult[0].total;
// 获取活跃商品数
const activeQuery = 'SELECT COUNT(*) as total FROM products WHERE status = "active"';
const [activeResult] = await getDB().execute(activeQuery);
const activeProducts = activeResult[0].total;
// 获取库存不足商品数库存小于10
const lowStockQuery = 'SELECT COUNT(*) as total FROM products WHERE stock < 10';
const [lowStockResult] = await getDB().execute(lowStockQuery);
const lowStockProducts = lowStockResult[0].total;
// 获取本月新增商品数
const monthlyQuery = `
SELECT COUNT(*) as total
FROM products
WHERE YEAR(created_at) = YEAR(CURDATE()) AND MONTH(created_at) = MONTH(CURDATE())
`;
const [monthlyResult] = await getDB().execute(monthlyQuery);
const monthlyProducts = monthlyResult[0].total;
// 计算月增长率
const lastMonthQuery = `
SELECT COUNT(*) as total
FROM products
WHERE YEAR(created_at) = YEAR(DATE_SUB(CURDATE(), INTERVAL 1 MONTH))
AND MONTH(created_at) = MONTH(DATE_SUB(CURDATE(), INTERVAL 1 MONTH))
`;
const [lastMonthResult] = await getDB().execute(lastMonthQuery);
const lastMonthProducts = lastMonthResult[0].total;
const monthlyGrowth = lastMonthProducts > 0
? ((monthlyProducts - lastMonthProducts) / lastMonthProducts * 100).toFixed(1)
: 0;
res.json({
success: true,
data: {
stats: {
totalProducts,
activeProducts,
lowStockProducts,
monthlyProducts,
monthlyGrowth: parseFloat(monthlyGrowth)
}
}
});
} catch (error) {
console.error('获取商品统计失败:', error);
res.status(500).json({ success: false, message: '获取商品统计失败' });
}
});
// 获取商品评论
router.get('/:id/reviews', async (req, res) => {
try {
const { id } = req.params;
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const offset = (page - 1) * limit;
// 获取评论列表
const [reviews] = await getDB().execute(
`SELECT pr.id, pr.rating, pr.comment as content, pr.images, pr.created_at as createdAt,
u.username as user_name, u.avatar as user_avatar
FROM product_reviews pr
JOIN users u ON pr.user_id = u.id
WHERE pr.product_id = ?
ORDER BY pr.created_at DESC
LIMIT ${limit} OFFSET ${offset}`,
[id]
);
// 获取评论总数
const [countResult] = await getDB().execute(
'SELECT COUNT(*) as total FROM product_reviews WHERE product_id = ?',
[id]
);
// 计算平均评分
const [avgResult] = await getDB().execute(
'SELECT AVG(rating) as avg_rating FROM product_reviews WHERE product_id = ?',
[id]
);
// 格式化评论数据
const formattedReviews = reviews.map(review => ({
id: review.id,
user: {
name: review.user_name,
avatar: review.user_avatar
},
rating: review.rating,
content: review.content,
createdAt: review.createdAt,
images: review.images ? JSON.parse(review.images) : null
}));
res.json({
success: true,
data: {
reviews: formattedReviews,
total: countResult[0].total,
averageRating: avgResult[0].avg_rating ? parseFloat(avgResult[0].avg_rating).toFixed(1) : 0,
pagination: {
page,
limit,
total: countResult[0].total,
totalPages: Math.ceil(countResult[0].total / limit)
}
}
});
} catch (error) {
console.error('获取商品评论失败:', error);
res.status(500).json({ success: false, message: '获取商品评论失败' });
}
});
// 获取推荐商品
router.get('/:id/recommended', async (req, res) => {
try {
// 获取商品id
const id = parseInt(req.params.id);
// 通过商品id获取该商品的分类id
const categoryQuery = `
SELECT category_id FROM products_category WHERE product_id = ?
`;
const [currentCategory] = await getDB().execute(categoryQuery, [id]);
let parentId = null;
let categoryIds = currentCategory.map(item => item.category_id);
if(currentCategory.length > 0 && currentCategory[0] !== null) {
// 获取该商品的一级分类id
const item = currentCategory[0];
const levelQuery = `
SELECT * FROM category WHERE id = ? AND level = 1
`;
const [fatherCategory] = await getDB().execute(levelQuery, [item.category_id]);
if(fatherCategory.length > 0) {
// 该商品为一级分类商品,不存在二级分类
parentId = fatherCategory[0].id;
categoryIds.push(parentId);
} else {
// 获取该商品的二级分类id
const secondlevelQuery = `
SELECT * FROM category WHERE id = ? AND level = 2
`;
const [secondlevelCategory] = await getDB().execute(secondlevelQuery, [item.category_id]);
if(secondlevelCategory.length > 0) {
// 通过二级分类获取一级分类id
parentId = secondlevelCategory[0].parent_id;
categoryIds.push(parentId);
}
}
}
// categoryIds目前存储该商品的一级分类id及其目录下所有二级分类的id
const allSecondCategoryIdsQuery = `
SELECT id FROM category WHERE parent_id = ? AND level = 2
`;
const [secondCategoryIds] = await getDB().execute(allSecondCategoryIdsQuery, [parentId]);
categoryIds.push(...secondCategoryIds.map(item => item.id));
let recommendedProducts = [];
let filteredRecommendProductIds = [];
// 如果有分类ID先获取同类商品
if (categoryIds.length > 0) {
const recommendId = `
SELECT * FROM products_category WHERE category_id IN (${categoryIds.map(() => '?').join(',')})
`;
const [recommendProductIds] = await getDB().execute(recommendId, categoryIds);
filteredRecommendProductIds = [...new Set(recommendProductIds.map(item => item.product_id))];
// 获取同类别的其他商品作为推荐
if (filteredRecommendProductIds.length > 0) {
const query = `
SELECT id, name, price, points_price as points,
stock, image_url as image, description
FROM products
WHERE id IN (${filteredRecommendProductIds.map(() => '?').join(',')}) AND id != ?
`;
const [categoryProducts] = await getDB().execute(query, [...filteredRecommendProductIds, id]);
recommendedProducts = categoryProducts;
}
}
// 如果同类别商品不足,补充其他热门商品
if (recommendedProducts.length < 6) {
let recommendQuery = `
SELECT products_id FROM recommend_product
WHERE 1 = 1
`;
if (filteredRecommendProductIds.length > 0) {
recommendQuery += ` AND products_id NOT IN (${filteredRecommendProductIds.map(() => '?').join(',')})`;
}
recommendQuery += ` ORDER BY RAND() LIMIT ${6 - recommendedProducts.length}`;
// 根据是否有排除ID来传递参数
const queryParams = filteredRecommendProductIds.length > 0
? [...filteredRecommendProductIds]
: [];
const [recommendProductIds] = await getDB().execute(recommendQuery, queryParams);
filteredRecommendProductIds.push(...recommendProductIds.map(item => item.products_id));
for (const item of recommendProductIds) {
const recommendQuery = `
SELECT id, name, price, points_price as points,
stock, image_url as image, description
FROM products
WHERE id = ?
`;
const [recommendProduct] = await getDB().execute(recommendQuery, [item.products_id]);
recommendedProducts.push(recommendProduct[0]);
}
if (recommendProductIds.length < 6) {
// 补充其他热门商品
const additionalQuery = `
SELECT id, name, price, points_price as points,
stock, image_url as image, description
FROM products
WHERE id NOT IN (${filteredRecommendProductIds.map(() => '?').join(',')})
ORDER BY RAND()
LIMIT ${6 - recommendedProducts.length}
`;
const [additionalProducts] = await getDB().execute(
additionalQuery,
filteredRecommendProductIds
);
recommendedProducts.push(...additionalProducts);
}
}
res.json({
success: true,
data: {
products: recommendedProducts
}
});
} catch (error) {
console.error('获取推荐商品失败:', error);
res.status(500).json({ success: false, message: '获取推荐商品失败' });
}
});
// 收藏商品
router.post('/:id/favorite', auth, async (req, res) => {
try {
const productId = req.params.id;
const userId = req.user.id;
// 检查商品是否存在
const [products] = await getDB().execute('SELECT id FROM products WHERE id = ?', [productId]);
if (products.length === 0) {
return res.status(404).json({ message: '商品不存在' });
}
// 检查是否已收藏
const [existing] = await getDB().execute(
'SELECT id FROM product_favorites WHERE user_id = ? AND product_id = ?',
[userId, productId]
);
if (existing.length > 0) {
return res.status(400).json({ message: '商品已收藏' });
}
await getDB().execute(
'INSERT INTO product_favorites (user_id, product_id, created_at) VALUES (?, ?, NOW())',
[userId, productId]
);
res.json({
success: true,
message: '收藏成功'
});
} catch (error) {
console.error('收藏商品错误:', error);
res.status(500).json({ message: '收藏失败' });
}
});
// 取消收藏商品
router.delete('/:id/favorite', auth, async (req, res) => {
try {
const productId = req.params.id;
const userId = req.user.id;
const [result] = await getDB().execute(
'DELETE FROM product_favorites WHERE user_id = ? AND product_id = ?',
[userId, productId]
);
if (result.affectedRows === 0) {
return res.status(404).json({ message: '未收藏该商品' });
}
res.json({
success: true,
message: '取消收藏成功'
});
} catch (error) {
console.error('取消收藏错误:', error);
res.status(500).json({ message: '取消收藏失败' });
}
});
// 获取用户收藏的商品列表
router.get('/favorites', auth, async (req, res) => {
try {
const userId = req.user.id;
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const offset = (page - 1) * limit;
const [favorites] = await getDB().execute(
`SELECT p.*, pf.created_at as favorite_time
FROM product_favorites pf
JOIN products p ON pf.product_id = p.id
WHERE pf.user_id = ? AND p.status = 'active'
ORDER BY pf.created_at DESC
LIMIT ${limit} OFFSET ${offset}`,
[userId]
);
const [countResult] = await getDB().execute(
`SELECT COUNT(*) as total
FROM product_favorites pf
JOIN products p ON pf.product_id = p.id
WHERE pf.user_id = ? AND p.status = 'active'`,
[userId]
);
res.json({
success: true,
data: {
products: favorites.map(product => ({
...product,
images: product.images ? JSON.parse(product.images) : [],
videos: product.videos ? JSON.parse(product.videos) : [],
payment_methods: product.payment_methods ? JSON.parse(product.payment_methods) : []
})),
pagination: {
page,
limit,
total: countResult[0].total,
totalPages: Math.ceil(countResult[0].total / limit)
}
}
});
} catch (error) {
console.error('获取收藏列表错误:', error);
res.status(500).json({ message: '获取收藏列表失败' });
}
});
// 获取商品属性
router.get('/:id/attributes', async (req, res) => {
try {
const productId = req.params.id;
const [attributes] = await getDB().execute(
'SELECT * FROM product_attributes WHERE product_id = ? ORDER BY sort_order, id',
[productId]
);
res.json({
success: true,
data: attributes
});
} catch (error) {
console.error('获取商品属性错误:', error);
res.status(500).json({ message: '获取商品属性失败' });
}
});
// 创建商品评论
router.post('/:id/reviews', auth, async (req, res) => {
try {
const productId = req.params.id;
const userId = req.user.id;
const { orderId, rating, comment, images = [] } = req.body;
// 验证必填字段
if (!orderId || !rating || rating < 1 || rating > 5) {
return res.status(400).json({ message: '订单ID和评分(1-5)不能为空' });
}
// 检查订单是否存在且属于当前用户
const [orders] = await getDB().execute(
`SELECT o.id FROM orders o
JOIN order_items oi ON o.id = oi.order_id
WHERE o.id = ? AND o.user_id = ? AND oi.product_id = ? AND o.status = 'delivered'`,
[orderId, userId, productId]
);
if (orders.length === 0) {
return res.status(400).json({ message: '只能评价已完成的订单商品' });
}
// 检查是否已经评价过
const [existingReviews] = await getDB().execute(
'SELECT id FROM product_reviews WHERE product_id = ? AND user_id = ? AND order_id = ?',
[productId, userId, orderId]
);
if (existingReviews.length > 0) {
return res.status(400).json({ message: '该商品已评价过' });
}
// 创建评论
const [result] = await getDB().execute(
`INSERT INTO product_reviews (product_id, user_id, order_id, rating, comment, images, created_at)
VALUES (?, ?, ?, ?, ?, ?, NOW())`,
[productId, userId, orderId, rating, comment, JSON.stringify(images)]
);
// 更新商品平均评分
const [avgResult] = await getDB().execute(
'SELECT AVG(rating) as avg_rating FROM product_reviews WHERE product_id = ?',
[productId]
);
await getDB().execute(
'UPDATE products SET rating = ? WHERE id = ?',
[parseFloat(avgResult[0].avg_rating).toFixed(2), productId]
);
res.status(201).json({
success: true,
message: '评价成功',
data: { reviewId: result.insertId }
});
} catch (error) {
console.error('创建商品评论错误:', error);
res.status(500).json({ message: '评价失败' });
}
});
module.exports = router;