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-10-17 17:26:37 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @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]
|
|
|
|
|
|
);
|
2025-10-17 17:26:37 +08:00
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-17 17:26:37 +08:00
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
2025-10-17 17:26:37 +08:00
|
|
|
|
let recommendQuery = `
|
2025-10-14 16:25:16 +08:00
|
|
|
|
SELECT products_id FROM recommend_product
|
2025-10-17 17:26:37 +08:00
|
|
|
|
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-17 17:26:37 +08:00
|
|
|
|
|
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]);
|
|
|
|
|
|
}
|
2025-10-17 17:26:37 +08:00
|
|
|
|
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;
|