1359 lines
45 KiB
JavaScript
1359 lines
45 KiB
JavaScript
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; |