Files
jurong_circle_black/routes/products.js

1143 lines
38 KiB
JavaScript
Raw Normal View History

2025-08-26 10:06:23 +08:00
const express = require('express');
const { getDB } = require('../database');
const { auth, adminAuth } = require('../middleware/auth');
const router = express.Router();
2025-09-02 09:29:20 +08:00
// 商品管理路由
2025-08-26 10:06:23 +08:00
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);
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 (category) {
whereClause += ' AND category = ?';
params.push(category);
}
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 = `
2025-09-02 09:29:20 +08:00
SELECT id, name, rongdou_price, category, points_price as points, stock, image_url as image, description, status, payment_methods, created_at, updated_at
2025-08-26 10:06:23 +08:00
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);
2025-09-02 09:29:20 +08:00
products.forEach(item=>{
item.payment_methods = JSON.parse(item.payment_methods)
})
console.log('查询结果:', products);
2025-08-26 10:06:23 +08:00
res.json({
success: true,
data: {
products,
pagination: {
page: pageNum,
limit: limitNum,
total,
pages: Math.ceil(total / limitNum)
}
}
});
} catch (error) {
console.error('获取商品列表失败:', error);
res.status(500).json({ success: false, message: '获取商品列表失败' });
}
});
2025-09-02 09:29:20 +08:00
// 获取商品分类列表
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, category, 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 RAND()
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: '获取热销商品失败' });
}
});
2025-08-28 09:14:56 +08:00
/**
* @swagger
2025-09-02 09:29:20 +08:00
* /products/flash-sale:
2025-08-28 09:14:56 +08:00
* get:
2025-09-02 09:29:20 +08:00
* summary: 获取秒杀商品
2025-08-28 09:14:56 +08:00
* tags: [Products]
* responses:
* 200:
2025-09-02 09:29:20 +08:00
* description: 成功获取秒杀商品
2025-08-28 09:14:56 +08:00
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
2025-09-02 09:29:20 +08:00
* products:
2025-08-28 09:14:56 +08:00
* type: array
* items:
2025-09-02 09:29:20 +08:00
* $ref: '#/components/schemas/Product'
2025-08-28 09:14:56 +08:00
*/
2025-09-02 09:29:20 +08:00
router.get('/cheap', async (req, res) => {
2025-08-26 10:06:23 +08:00
try {
2025-09-02 09:29:20 +08:00
// 从活跃商品中随机获取2个商品作为秒杀商品
const [products] = await getDB().execute(
`SELECT id, name, category, 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 RAND()
LIMIT 2`
2025-08-26 10:06:23 +08:00
);
2025-09-02 09:29:20 +08:00
// 格式化商品数据,为秒杀商品添加特殊标识
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'],
// 秒杀商品特殊处理价格打8折
flash_sale_price: Math.floor(product.price * 0.8),
flash_sale_points: Math.floor(product.points_price * 0.8),
flash_sale_rongdou: Math.floor(product.rongdou_price * 0.8),
is_flash_sale: true,
// 保持向后兼容
points: product.points_price,
image: product.image_url
}));
2025-08-26 10:06:23 +08:00
res.json({
success: true,
data: {
2025-09-02 09:29:20 +08:00
products: formattedProducts
2025-08-26 10:06:23 +08:00
}
});
} catch (error) {
2025-09-02 09:29:20 +08:00
console.error('获取秒杀商品失败:', error);
res.status(500).json({ success: false, message: '获取秒杀商品失败' });
2025-08-26 10:06:23 +08:00
}
});
2025-08-28 09:14:56 +08:00
/**
* @swagger
* /products/{id}:
* get:
2025-09-02 09:29:20 +08:00
* summary: 获取单个商品详情包含增强规格信息
2025-08-28 09:14:56 +08:00
* tags: [Products]
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: 商品ID
* responses:
* 200:
2025-09-02 09:29:20 +08:00
* description: 成功获取商品详情包含完整的规格信息
2025-08-28 09:14:56 +08:00
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
2025-09-02 09:29:20 +08:00
* example: true
2025-08-28 09:14:56 +08:00
* data:
2025-09-02 09:29:20 +08:00
* 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: 是否已收藏
2025-08-28 09:14:56 +08:00
* 404:
* description: 商品不存在
*/
2025-08-26 10:06:23 +08:00
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
2025-08-28 09:14:56 +08:00
const userId = req.user?.id; // 可选的用户ID用于检查收藏状态
2025-08-26 10:06:23 +08:00
const query = `
2025-08-28 09:14:56 +08:00
SELECT id, name, category, 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
2025-08-26 10:06:23 +08:00
FROM products
2025-08-28 09:14:56 +08:00
WHERE id = ? AND status = 'active'
2025-08-26 10:06:23 +08:00
`;
const [products] = await getDB().execute(query, [id]);
if (products.length === 0) {
return res.status(404).json({ success: false, message: '商品不存在' });
}
const product = products[0];
2025-08-28 09:14:56 +08:00
2025-09-02 09:29:20 +08:00
// 获取商品的规格组合(新的笛卡尔积规格系统)
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`,
2025-08-28 09:14:56 +08:00
[id]
);
2025-09-02 09:29:20 +08:00
// 为每个规格组合获取详细的规格值信息
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
});
}
}
2025-08-28 09:14:56 +08:00
// 获取商品属性
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;
}
// 构建增强的商品数据
2025-08-26 10:06:23 +08:00
const enhancedProduct = {
...product,
2025-09-02 09:29:20 +08:00
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,
2025-08-28 09:14:56 +08:00
attributes,
isFavorited,
2025-09-02 09:29:20 +08:00
// 规格统计信息
specification_count: enhancedSpecifications.length,
available_specifications: enhancedSpecifications.filter(spec => spec.is_available).length,
2025-08-28 09:14:56 +08:00
// 保持向后兼容
points: product.points_price,
image: product.image_url,
tags: product.category ? [product.category] : []
2025-08-26 10:06:23 +08:00
};
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 {
2025-08-28 09:14:56 +08:00
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;
2025-08-26 10:06:23 +08:00
2025-08-28 09:14:56 +08:00
if (!name || !price || (!points_price && !rongdou_price) || stock === undefined) {
return res.status(400).json({ message: '商品名称、原价、积分价格或融豆价格、库存不能为空' });
2025-08-26 10:06:23 +08:00
}
const [result] = await getDB().execute(
2025-08-28 09:14:56 +08:00
`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]
2025-08-26 10:06:23 +08:00
);
2025-08-28 09:14:56 +08:00
const productId = result.insertId;
2025-09-02 09:29:20 +08:00
2025-08-28 09:14:56 +08:00
// 添加商品属性
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]
);
}
}
2025-08-26 10:06:23 +08:00
res.status(201).json({
success: true,
message: '商品创建成功',
2025-08-28 09:14:56 +08:00
data: { productId }
2025-08-26 10:06:23 +08:00
});
} catch (error) {
console.error('创建商品错误:', error);
res.status(500).json({ message: '创建商品失败' });
}
});
// 更新商品(管理员权限)
router.put('/:id', auth, adminAuth, async (req, res) => {
try {
const productId = req.params.id;
2025-08-28 09:14:56 +08:00
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;
2025-08-26 10:06:23 +08:00
// 检查商品是否存在
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);
}
2025-08-28 09:14:56 +08:00
if (rongdou_price !== undefined) {
updateFields.push('rongdou_price = ?');
updateValues.push(rongdou_price);
}
2025-08-26 10:06:23 +08:00
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);
}
2025-08-28 09:14:56 +08:00
if (images !== undefined) {
updateFields.push('images = ?');
updateValues.push(JSON.stringify(images || []));
}
if (videos !== undefined) {
updateFields.push('videos = ?');
updateValues.push(JSON.stringify(videos || []));
}
2025-08-26 10:06:23 +08:00
if (details !== undefined) {
updateFields.push('details = ?');
updateValues.push(details);
}
2025-08-28 09:14:56 +08:00
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 || []));
}
2025-08-26 10:06:23 +08:00
if (status) {
2025-08-28 09:14:56 +08:00
updateFields.push('status = ?');
2025-08-26 10:06:23 +08:00
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
);
2025-09-02 09:29:20 +08:00
2025-08-28 09:14:56 +08:00
// 更新商品属性
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]
);
}
}
}
2025-08-26 10:06:23 +08:00
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;
2025-08-28 09:14:56 +08:00
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const offset = (page - 1) * limit;
2025-08-26 10:06:23 +08:00
2025-08-28 09:14:56 +08:00
// 获取评论列表
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
2025-09-02 09:29:20 +08:00
LIMIT ${limit} OFFSET ${offset}`,
[id]
2025-08-28 09:14:56 +08:00
);
// 获取评论总数
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
2025-08-26 10:06:23 +08:00
},
2025-08-28 09:14:56 +08:00
rating: review.rating,
content: review.content,
createdAt: review.createdAt,
images: review.images ? JSON.parse(review.images) : null
}));
2025-08-26 10:06:23 +08:00
res.json({
success: true,
data: {
2025-08-28 09:14:56 +08:00
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)
}
2025-08-26 10:06:23 +08:00
}
});
} catch (error) {
console.error('获取商品评论失败:', error);
res.status(500).json({ success: false, message: '获取商品评论失败' });
}
});
// 获取推荐商品
router.get('/:id/recommended', async (req, res) => {
try {
const id = parseInt(req.params.id);
// 获取同类别的其他商品作为推荐
const query = `
SELECT p2.id, p2.name, p2.category, p2.price, p2.points_price as points,
p2.stock, p2.image_url as image, p2.description
FROM products p1
JOIN products p2 ON p1.category = p2.category
WHERE p1.id = ? AND p2.id != ? AND p2.status = 'active'
ORDER BY RAND()
LIMIT 6
`;
const [recommendedProducts] = await getDB().execute(query, [id, id]);
// 如果同类别商品不足,补充其他热门商品
if (recommendedProducts.length < 6) {
const remainingCount = 6 - recommendedProducts.length;
if (remainingCount > 0) {
const additionalQuery = `
SELECT id, name, category, price, points_price as points,
stock, image_url as image, description
FROM products
WHERE id != ? AND status = 'active'
ORDER BY RAND()
LIMIT ${remainingCount}
`;
const [additionalProducts] = await getDB().execute(
additionalQuery,
[id]
);
recommendedProducts.push(...additionalProducts);
}
}
res.json({
success: true,
data: {
products: recommendedProducts
}
});
} catch (error) {
console.error('获取推荐商品失败:', error);
res.status(500).json({ success: false, message: '获取推荐商品失败' });
}
});
2025-08-28 09:14:56 +08:00
// 收藏商品
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
2025-09-02 09:29:20 +08:00
LIMIT ${limit} OFFSET ${offset}`,
[userId]
2025-08-28 09:14:56 +08:00
);
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: '获取收藏列表失败' });
}
});
2025-09-02 09:29:20 +08:00
2025-08-28 09:14:56 +08:00
// 获取商品属性
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: '评价失败' });
}
});
2025-08-26 10:06:23 +08:00
module.exports = router;