Files
jurong_circle_black/routes/products.js

1373 lines
46 KiB
JavaScript
Raw Permalink 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();
/**
* @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: 服务器内部错误
*/
2025-09-02 09:29:20 +08:00
// 商品管理路由
2025-08-26 10:06:23 +08:00
router.get('/', async (req, res) => {
try {
2025-10-23 17:26:42 +08:00
const { page = 1, limit = 10, search = '', category = '', status = '', sort } = req.query;
2025-08-26 10:06:23 +08:00
// 确保参数为有效数字
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);
2025-10-11 17:33:00 +08:00
let filteredProducts = []
2025-08-26 10:06:23 +08:00
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"';
}
2025-10-23 17:26:42 +08:00
switch (sort) {
case 'price_desc':
whereClause += ' ORDER BY sale_price DESC'
break;
case 'price_asc':
whereClause += ' ORDER BY sale_price ASC'
break;
case 'sales_desc':
whereClause += ' ORDER BY sales DESC'
break;
default:
whereClause += ' ORDER BY created_at DESC'
break;
}
2025-08-26 10:06:23 +08:00
// 获取总数
const countQuery = `SELECT COUNT(*) as total FROM products ${whereClause}`;
const [countResult] = await getDB().execute(countQuery, params);
const total = countResult[0].total;
// 获取商品列表
const query = `
2025-10-15 17:26:20 +08:00
SELECT id, name, rongdou_price, points_price, stock, image_url as image, description, status, payment_methods, created_at, updated_at, sales, images
2025-08-26 10:06:23 +08:00
FROM products
${whereClause}
LIMIT ${limitNum} OFFSET ${offset}
`;
// 确保参数数组正确传递
const queryParams = [...params];
2025-10-23 17:26:42 +08:00
// console.log('Query params:', queryParams, 'Query:', query);
2025-08-26 10:06:23 +08:00
const [products] = await getDB().execute(query, queryParams);
2025-10-11 17:33:00 +08:00
2025-09-02 09:29:20 +08:00
products.forEach(item=>{
item.payment_methods = JSON.parse(item.payment_methods)
2025-10-15 17:26:20 +08:00
item.images = JSON.parse(item.images)
2025-09-02 09:29:20 +08:00
})
2025-10-11 17:33:00 +08:00
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
}
2025-08-26 10:06:23 +08:00
res.json({
success: true,
data: {
2025-10-11 17:33:00 +08:00
products: filteredProducts,
// products: products,
2025-08-26 10:06:23 +08:00
pagination: {
page: pageNum,
limit: limitNum,
total,
pages: Math.ceil(total / limitNum)
}
}
});
} catch (error) {
console.error('获取商品列表失败:', error);
res.status(500).json({ success: false, message: '获取商品列表失败' });
}
});
2025-10-11 17:33:00 +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: '获取商品分类失败' });
// }
// });
2025-09-02 09:29:20 +08:00
// 获取热销商品
router.get('/hot', async (req, res) => {
try {
// 从活跃商品中随机获取2个商品
const [products] = await getDB().execute(
2025-10-11 17:33:00 +08:00
`SELECT id, name, price, points_price, rongdou_price, stock,
2025-09-02 09:29:20 +08:00
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
2025-09-28 11:49:36 +08:00
ORDER BY sales DESC
2025-09-02 09:29:20 +08:00
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-28 11:49:36 +08:00
2025-09-02 09:29:20 +08:00
const [products] = await getDB().execute(
2025-09-28 11:49:36 +08:00
`SELECT id, start_time, end_time, flash_stock, flash_price, products_id
FROM flash_product
WHERE end_time > NOW() AND flash_stock > 0
2025-09-02 09:29:20 +08:00
ORDER BY RAND()
LIMIT 2`
2025-08-26 10:06:23 +08:00
);
2025-09-28 11:49:36 +08:00
const tempProducts = await Promise.all(products.map(async item=>{
const [product] = await getDB().execute(
2025-10-11 17:33:00 +08:00
`SELECT id, name, price, points_price, rongdou_price, stock,
2025-09-28 11:49:36 +08:00
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
}))
2025-09-02 09:29:20 +08:00
2025-08-26 10:06:23 +08:00
res.json({
success: true,
data: {
2025-09-28 11:49:36 +08:00
products: tempProducts
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-10-11 17:33:00 +08:00
SELECT id, name, price, points_price, rongdou_price, stock,
2025-08-28 09:14:56 +08:00
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]
);
// console.log(123,specCombinations);
2025-08-28 09:14:56 +08:00
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
});
}
}
// console.log(123,enhancedSpecifications);
2025-09-02 09:29:20 +08:00
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,
2025-10-11 17:33:00 +08:00
// 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 {
2025-10-13 17:28:29 +08:00
// 获取商品id
2025-08-26 10:06:23 +08:00
const id = parseInt(req.params.id);
2025-10-13 17:28:29 +08:00
// 通过商品id获取该商品的分类id
const categoryQuery = `
SELECT category_id FROM products_category WHERE product_id = ?
2025-08-26 10:06:23 +08:00
`;
2025-10-13 17:28:29 +08:00
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 = [];
2025-10-14 09:17:47 +08:00
let filteredRecommendProductIds = [];
2025-10-13 17:28:29 +08:00
// 如果有分类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);
2025-10-14 09:17:47 +08:00
filteredRecommendProductIds = [...new Set(recommendProductIds.map(item => item.product_id))];
2025-10-13 17:28:29 +08:00
// 获取同类别的其他商品作为推荐
if (filteredRecommendProductIds.length > 0) {
const query = `
2025-10-11 17:33:00 +08:00
SELECT id, name, price, points_price as points,
2025-08-26 10:06:23 +08:00
stock, image_url as image, description
FROM products
2025-10-13 17:28:29 +08:00
WHERE id IN (${filteredRecommendProductIds.map(() => '?').join(',')}) AND id != ?
2025-08-26 10:06:23 +08:00
`;
2025-10-13 17:28:29 +08:00
const [categoryProducts] = await getDB().execute(query, [...filteredRecommendProductIds, id]);
recommendedProducts = categoryProducts;
2025-08-26 10:06:23 +08:00
}
}
2025-10-13 17:28:29 +08:00
// 如果同类别商品不足,补充其他热门商品
2025-10-14 09:17:47 +08:00
if (recommendedProducts.length < 6) {
2025-10-14 16:25:16 +08:00
let recommendQuery = `
2025-10-14 16:25:16 +08:00
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);
2025-10-14 16:25:16 +08:00
filteredRecommendProductIds.push(...recommendProductIds.map(item => item.products_id));
2025-10-14 16:25:16 +08:00
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) {
2025-10-14 16:25:16 +08:00
// 补充其他热门商品
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);
}
2025-10-14 09:17:47 +08:00
}
2025-08-26 10:06:23 +08:00
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;