修改商城逻辑

This commit is contained in:
2025-08-28 09:14:56 +08:00
parent a1944a573e
commit 691789d5d3
28 changed files with 10842 additions and 292 deletions

View File

@@ -4,7 +4,123 @@ const { auth, adminAuth } = require('../middleware/auth');
const router = express.Router();
// 获取商品列表
/**
* @swagger
* tags:
* name: Products
* description: 商品管理API
*/
/**
* @swagger
* components:
* schemas:
* Product:
* type: object
* required:
* - name
* - points_price
* - stock
* properties:
* id:
* type: integer
* description: 商品ID
* name:
* type: string
* description: 商品名称
* category:
* type: string
* description: 商品分类
* points_price:
* type: integer
* description: 积分价格
* stock:
* type: integer
* description: 库存数量
* image_url:
* type: string
* description: 商品图片URL
* description:
* type: string
* description: 商品描述
* status:
* type: string
* description: 商品状态
* enum: [active, inactive]
* created_at:
* type: string
* format: date-time
* description: 创建时间
* updated_at:
* type: string
* format: date-time
* description: 更新时间
*/
/**
* @swagger
* /products:
* get:
* summary: 获取商品列表
* tags: [Products]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* - in: query
* name: category
* schema:
* type: string
* description: 商品分类
* - in: query
* name: status
* schema:
* type: string
* enum: [active, inactive]
* description: 商品状态
* 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'
* pagination:
* type: object
* properties:
* page:
* type: integer
* limit:
* type: integer
* total:
* type: integer
* pages:
* type: integer
*/
router.get('/', async (req, res) => {
try {
const { page = 1, limit = 10, search = '', category = '', status = '' } = req.query;
@@ -73,7 +189,30 @@ router.get('/', async (req, res) => {
}
});
// 获取商品分类列表
/**
* @swagger
* /products/categories:
* get:
* summary: 获取商品分类列表
* tags: [Products]
* responses:
* 200:
* description: 成功获取商品分类列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* categories:
* type: array
* items:
* type: string
*/
router.get('/categories', async (req, res) => {
try {
const [categories] = await getDB().execute(
@@ -92,15 +231,45 @@ router.get('/categories', async (req, res) => {
}
});
// 获取单个商品详情
/**
* @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
* data:
* $ref: '#/components/schemas/Product'
* 404:
* description: 商品不存在
*/
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const userId = req.user?.id; // 可选的用户ID用于检查收藏状态
const query = `
SELECT id, name, category, price, points_price as points, stock, image_url as image, description, details, status, created_at, updated_at
SELECT id, name, category, price, points_price, rongdou_price, stock,
image_url, images, videos, description, details, shop_name, shop_avatar,
payment_methods, sales, rating, status, created_at, updated_at
FROM products
WHERE id = ?
WHERE id = ? AND status = 'active'
`;
const [products] = await getDB().execute(query, [id]);
@@ -109,16 +278,43 @@ router.get('/:id', async (req, res) => {
return res.status(404).json({ success: false, message: '商品不存在' });
}
// 增强商品数据,添加前端需要的字段
const product = products[0];
// 获取商品规格
const [specifications] = await getDB().execute(
'SELECT * FROM product_specifications WHERE product_id = ? ORDER BY id',
[id]
);
// 获取商品属性
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: product.image ? [product.image] : ['/imgs/default-product.png'], // 将单个图片转为数组
tags: product.category ? [product.category] : [], // 将分类作为标签
sales: Math.floor(Math.random() * 1000) + 100, // 模拟销量数据
rating: (Math.random() * 2 + 3).toFixed(1), // 模拟评分 3-5分
originalPoints: product.points + Math.floor(Math.random() * 100), // 模拟原价
discount: Math.floor(Math.random() * 3 + 7) // 模拟折扣 7-9折
images: product.images ? JSON.parse(product.images) : (product.image_url ? [product.image_url] : []),
videos: product.videos ? JSON.parse(product.videos) : [],
payment_methods: product.payment_methods ? JSON.parse(product.payment_methods) : ['points'],
specifications,
attributes,
isFavorited,
// 保持向后兼容
points: product.points_price,
image: product.image_url,
tags: product.category ? [product.category] : []
};
res.json({
@@ -134,23 +330,57 @@ router.get('/:id', async (req, res) => {
// 创建商品(管理员权限)
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;
const { name, description, price, points_price, stock, category, image_url, details, status = 'active' } = req.body;
if (!name || !price || !points_price || stock === undefined) {
return res.status(400).json({ message: '商品名称、原价、积分价格和库存不能为空' });
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, stock, category, image_url, details, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
[name, description, price, points_price, stock, category || null, image_url, details, status]
`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 (specifications && specifications.length > 0) {
for (const spec of specifications) {
await getDB().execute(
`INSERT INTO product_specifications (product_id, spec_name, spec_value, price_adjustment,
points_adjustment, rongdou_adjustment, stock, sku_code)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[productId, spec.name, spec.value, spec.price_adjustment || 0,
spec.points_adjustment || 0, spec.rongdou_adjustment || 0,
spec.stock || 0, spec.sku_code || null]
);
}
}
// 添加商品属性
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: result.insertId }
data: { productId }
});
} catch (error) {
console.error('创建商品错误:', error);
@@ -161,9 +391,12 @@ router.post('/', auth, adminAuth, async (req, res) => {
// 更新商品(管理员权限)
router.put('/:id', auth, adminAuth, async (req, res) => {
try {
const productId = req.params.id;
const { name, description, price, points_price, stock, category, image_url, details, status } = req.body;
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(
@@ -199,6 +432,11 @@ router.put('/:id', auth, adminAuth, async (req, res) => {
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);
@@ -214,13 +452,38 @@ router.put('/:id', auth, adminAuth, async (req, res) => {
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 = ?');
updateFields.push('status = ?');
updateValues.push(status);
}
@@ -236,6 +499,43 @@ router.put('/:id', auth, adminAuth, async (req, res) => {
updateValues
);
// 更新商品规格
if (specifications !== undefined) {
// 删除原有规格
await getDB().execute('DELETE FROM product_specifications WHERE product_id = ?', [productId]);
// 添加新规格
if (specifications && specifications.length > 0) {
for (const spec of specifications) {
await getDB().execute(
`INSERT INTO product_specifications (product_id, spec_name, spec_value, price_adjustment,
points_adjustment, rongdou_adjustment, stock, sku_code)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[productId, spec.name, spec.value, spec.price_adjustment || 0,
spec.points_adjustment || 0, spec.rongdou_adjustment || 0,
spec.stock || 0, spec.sku_code || null]
);
}
}
}
// 更新商品属性
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: '商品更新成功'
@@ -343,51 +643,59 @@ router.get('/stats', auth, adminAuth, async (req, res) => {
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 mockReviews = [
{
id: 1,
user: {
name: '用户1',
avatar: null
},
rating: 5,
content: '商品质量很好,非常满意!',
createdAt: '2024-01-15 10:30:00',
images: null
// 获取评论列表
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 ? OFFSET ?`,
[id, limit, offset]
);
// 获取评论总数
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
},
{
id: 2,
user: {
name: '用户2',
avatar: null
},
rating: 4,
content: '性价比不错,值得购买。',
createdAt: '2024-01-14 15:20:00',
images: null
},
{
id: 3,
user: {
name: '用户3',
avatar: null
},
rating: 5,
content: '发货速度快,包装精美。',
createdAt: '2024-01-13 09:45:00',
images: null
}
];
rating: review.rating,
content: review.content,
createdAt: review.createdAt,
images: review.images ? JSON.parse(review.images) : null
}));
res.json({
success: true,
data: {
reviews: mockReviews,
total: mockReviews.length,
averageRating: 4.7
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) {
@@ -448,4 +756,356 @@ router.get('/:id/recommended', async (req, res) => {
}
});
// 收藏商品
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 ? OFFSET ?`,
[userId, limit, offset]
);
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/specifications', async (req, res) => {
try {
const productId = req.params.id;
const [specifications] = await getDB().execute(
'SELECT id, spec_name as name, spec_value as value, price_adjustment, points_adjustment, rongdou_adjustment, stock, sku_code, created_at, updated_at FROM product_specifications WHERE product_id = ? ORDER BY id',
[productId]
);
res.json({
success: true,
data: specifications
});
} catch (error) {
console.error('获取商品规格错误:', error);
res.status(500).json({ message: '获取商品规格失败' });
}
});
// 创建商品规格(管理员权限)
router.post('/:id/specifications', auth, adminAuth, async (req, res) => {
try {
const productId = req.params.id;
const { name, value, price_adjustment = 0, points_adjustment = 0, rongdou_adjustment = 0, stock = 0, sku_code } = req.body;
if (!name || !value) {
return res.status(400).json({ message: '规格名称和规格值不能为空' });
}
// 检查商品是否存在
const [products] = await getDB().execute('SELECT id FROM products WHERE id = ?', [productId]);
if (products.length === 0) {
return res.status(404).json({ message: '商品不存在' });
}
const [result] = await getDB().execute(
`INSERT INTO product_specifications (product_id, spec_name, spec_value, price_adjustment,
points_adjustment, rongdou_adjustment, stock, sku_code, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
[productId, name, value, price_adjustment, points_adjustment, rongdou_adjustment, stock, sku_code || null]
);
res.status(201).json({
success: true,
message: '规格创建成功',
data: { id: result.insertId }
});
} catch (error) {
console.error('创建商品规格错误:', error);
res.status(500).json({ message: '创建商品规格失败' });
}
});
// 更新商品规格(管理员权限)
router.put('/:id/specifications/:specId', auth, adminAuth, async (req, res) => {
try {
const { id: productId, specId } = req.params;
const { name, value, price_adjustment, points_adjustment, rongdou_adjustment, stock, sku_code } = req.body;
// 检查规格是否存在
const [specs] = await getDB().execute(
'SELECT id FROM product_specifications WHERE id = ? AND product_id = ?',
[specId, productId]
);
if (specs.length === 0) {
return res.status(404).json({ message: '规格不存在' });
}
// 构建更新字段
const updateFields = [];
const updateValues = [];
if (name !== undefined) {
updateFields.push('spec_name = ?');
updateValues.push(name);
}
if (value !== undefined) {
updateFields.push('spec_value = ?');
updateValues.push(value);
}
if (price_adjustment !== undefined) {
updateFields.push('price_adjustment = ?');
updateValues.push(price_adjustment);
}
if (points_adjustment !== undefined) {
updateFields.push('points_adjustment = ?');
updateValues.push(points_adjustment);
}
if (rongdou_adjustment !== undefined) {
updateFields.push('rongdou_adjustment = ?');
updateValues.push(rongdou_adjustment);
}
if (stock !== undefined) {
updateFields.push('stock = ?');
updateValues.push(stock);
}
if (sku_code !== undefined) {
updateFields.push('sku_code = ?');
updateValues.push(sku_code);
}
if (updateFields.length === 0) {
return res.status(400).json({ message: '没有提供要更新的字段' });
}
updateFields.push('updated_at = NOW()');
updateValues.push(specId);
await getDB().execute(
`UPDATE product_specifications SET ${updateFields.join(', ')} WHERE id = ?`,
updateValues
);
res.json({
success: true,
message: '规格更新成功'
});
} catch (error) {
console.error('更新商品规格错误:', error);
res.status(500).json({ message: '更新商品规格失败' });
}
});
// 删除商品规格(管理员权限)
router.delete('/:id/specifications/:specId', auth, adminAuth, async (req, res) => {
try {
const { id: productId, specId } = req.params;
// 检查规格是否存在
const [specs] = await getDB().execute(
'SELECT id FROM product_specifications WHERE id = ? AND product_id = ?',
[specId, productId]
);
if (specs.length === 0) {
return res.status(404).json({ message: '规格不存在' });
}
await getDB().execute('DELETE FROM product_specifications WHERE id = ?', [specId]);
res.json({
success: true,
message: '规格删除成功'
});
} 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;