Files
jurong_circle_black/routes/cart.js

935 lines
28 KiB
JavaScript
Raw Normal View History

2025-09-02 09:29:20 +08:00
const express = require('express');
const { getDB } = require('../database');
const { auth } = require('../middleware/auth');
const router = express.Router();
/**
* @swagger
* tags:
* name: Cart
* description: 购物车管理相关接口
*/
/**
* @swagger
* components:
* schemas:
* CartItem:
* type: object
* properties:
* id:
* type: integer
* description: 购物车项ID
* user_id:
* type: integer
* description: 用户ID
* product_id:
* type: integer
* description: 商品ID
* quantity:
* type: integer
* description: 商品数量
* spec_combination_id:
* type: integer
* description: 商品规格组合ID
* created_at:
* type: string
* format: date-time
* description: 创建时间
* updated_at:
* type: string
* format: date-time
* description: 更新时间
* product:
* type: object
* properties:
* id:
* type: integer
* name:
* type: string
* price:
* type: integer
* points_price:
* type: integer
* rongdou_price:
* type: integer
* image_url:
* type: string
* stock:
* type: integer
* status:
* type: string
*/
/**
* @swagger
* /api/cart:
* get:
* summary: 获取购物车列表
* tags: [Cart]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取购物车成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* items:
* type: array
* items:
* $ref: '#/components/schemas/CartItem'
* total_count:
* type: integer
* description: 购物车商品总数量
* total_amount:
* type: integer
* description: 购物车总金额
* total_points:
* type: integer
* description: 购物车总积分
* total_rongdou:
* type: integer
* description: 购物车总融豆
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.get('/', auth, async (req, res) => {
try {
const userId = req.user.id;
// 获取购物车商品列表
const query = `
SELECT
c.id, c.user_id, c.product_id, c.quantity, c.specification_id,
c.created_at, c.updated_at,
p.name, p.price, p.points_price, p.rongdou_price, p.image_url,
p.stock, p.status, p.shop_name, p.shop_avatar,
psc.combination_key, psc.price_adjustment,
psc.points_adjustment, psc.rongdou_adjustment, psc.stock as spec_stock,
GROUP_CONCAT(CONCAT(sn.display_name, ':', sv.display_value) ORDER BY sn.sort_order SEPARATOR ' | ') as spec_display
FROM cart_items c
LEFT JOIN products p ON c.product_id = p.id
LEFT JOIN product_spec_combinations psc ON c.specification_id = psc.id
LEFT JOIN JSON_TABLE(psc.spec_values, '$[*]' COLUMNS (spec_value_id INT PATH '$')) jt ON psc.id IS NOT NULL
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 c.user_id = ? AND p.status = 'active'
GROUP BY c.id
ORDER BY c.created_at DESC
`;
const [cartItems] = await getDB().execute(query, [userId]);
// 计算总计信息
let totalCount = 0;
let totalAmount = 0;
let totalPoints = 0;
let totalRongdou = 0;
const items = cartItems.map(item => {
const finalPrice = item.price + (item.price_adjustment || 0);
const finalPointsPrice = item.points_price + (item.points_adjustment || 0);
const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0);
totalCount += item.quantity;
totalAmount += finalPrice * item.quantity;
totalPoints += finalPointsPrice * item.quantity;
totalRongdou += finalRongdouPrice * item.quantity;
return {
id: item.id,
user_id: item.user_id,
product_id: item.product_id,
quantity: item.quantity,
spec_combination_id: item.spec_combination_id,
created_at: item.created_at,
updated_at: item.updated_at,
product: {
id: item.product_id,
name: item.name,
price: finalPrice,
points_price: finalPointsPrice,
rongdou_price: finalRongdouPrice,
image_url: item.image_url,
stock: item.spec_combination_id ? item.spec_stock : item.stock,
status: item.status,
shop_name: item.shop_name,
shop_avatar: item.shop_avatar
},
specification: item.spec_combination_id ? {
id: item.spec_combination_id,
combination_key: item.combination_key,
spec_display: item.spec_display,
price_adjustment: item.price_adjustment,
points_adjustment: item.points_adjustment,
rongdou_adjustment: item.rongdou_adjustment
} : null
};
});
res.json({
success: true,
data: {
items,
total_count: totalCount,
total_amount: totalAmount,
total_points: totalPoints,
total_rongdou: totalRongdou
}
});
} catch (error) {
console.error('获取购物车失败:', error);
res.status(500).json({ success: false, message: '获取购物车失败' });
}
});
/**
* @swagger
* /api/cart:
* post:
* summary: 添加商品到购物车
* tags: [Cart]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* product_id:
* type: integer
* description: 商品ID
* quantity:
* type: integer
* description: 商品数量
* minimum: 1
* spec_combination_id:
* type: integer
* description: 商品规格组合ID可选
* required:
* - product_id
* - quantity
* responses:
* 201:
* description: 添加到购物车成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* type: object
* properties:
* cart_item_id:
* type: integer
* 400:
* description: 参数错误或库存不足
* 401:
* description: 未授权
* 404:
* description: 商品不存在或已下架
* 500:
* description: 服务器错误
*/
router.post('/add', auth, async (req, res) => {
const db = getDB();
await db.query('START TRANSACTION');
try {
const { productId, quantity, specificationId } = req.body;
const userId = req.user.id;
// 验证必填字段
if (!productId || !quantity || quantity < 1) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '请填写正确的商品信息和数量' });
}
// 检查商品是否存在且有效
const [products] = await db.execute(
'SELECT id, name, stock, status FROM products WHERE id = ?',
[productId]
);
if (products.length === 0 || products[0].status !== 'active') {
await db.query('ROLLBACK');
return res.status(404).json({ success: false, message: '商品不存在或已下架' });
}
const product = products[0];
let availableStock = product.stock;
// 如果指定了规格组合,检查规格组合库存
if (specificationId) {
const [specs] = await db.execute(
'SELECT id, stock, status FROM product_spec_combinations WHERE id = ? AND product_id = ?',
[specificationId, productId]
);
if (specs.length === 0 || specs[0].status !== 'active') {
await db.query('ROLLBACK');
return res.status(404).json({ success: false, message: '商品规格组合不存在或已下架' });
}
availableStock = specs[0].stock;
}
// 检查购物车中是否已存在相同商品和规格组合
const [existingItems] = await db.execute(
'SELECT id, quantity FROM cart_items WHERE user_id = ? AND product_id = ? AND (specification_id = ? OR (specification_id IS NULL AND ? IS NULL))',
[userId, productId, specificationId, specificationId]
);
let finalQuantity = quantity;
if (existingItems.length > 0) {
finalQuantity += existingItems[0].quantity;
}
// 检查库存是否足够
if (availableStock < finalQuantity) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '库存不足' });
}
let cartItemId;
if (existingItems.length > 0) {
// 更新现有购物车项的数量
await db.execute(
'UPDATE cart_items SET quantity = ?, updated_at = NOW() WHERE id = ?',
[finalQuantity, existingItems[0].id]
);
cartItemId = existingItems[0].id;
} else {
// 添加新的购物车项
const [result] = await db.execute(
'INSERT INTO cart_items (user_id, product_id, quantity, specification_id, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
[userId, productId, quantity, specificationId]
);
cartItemId = result.insertId;
}
await db.query('COMMIT');
res.status(201).json({
success: true,
message: '添加到购物车成功',
data: { cart_item_id: cartItemId }
});
} catch (error) {
await db.query('ROLLBACK');
console.error('添加到购物车失败:', error);
res.status(500).json({ success: false, message: '添加到购物车失败' });
}
});
/**
* @swagger
* /api/cart/{id}:
* put:
* summary: 更新购物车商品数量
* tags: [Cart]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 购物车项ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* quantity:
* type: integer
* description: 新的商品数量
* minimum: 1
* required:
* - quantity
* responses:
* 200:
* description: 更新购物车成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* 400:
* description: 参数错误或库存不足
* 401:
* description: 未授权
* 404:
* description: 购物车项不存在
* 500:
* description: 服务器错误
*/
router.put('/:id', auth, async (req, res) => {
const db = getDB();
await db.query('START TRANSACTION');
try {
const cartItemId = req.params.id;
const { quantity } = req.body;
const userId = req.user.id;
// 验证数量
if (!quantity || quantity < 1) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '商品数量必须大于0' });
}
// 检查购物车项是否存在且属于当前用户
const [cartItems] = await db.execute(
'SELECT id, product_id, specification_id FROM cart_items WHERE id = ? AND user_id = ?',
[cartItemId, userId]
);
console.log(cartItems,'cartItems');
if (cartItems.length === 0) {
await db.query('ROLLBACK');
return res.status(404).json({ success: false, message: '购物车项不存在' });
}
const cartItem = cartItems[0];
// 检查商品库存
const [products] = await db.execute(
'SELECT stock, status FROM products WHERE id = ?',
[cartItem.product_id]
);
if (products.length === 0 || products[0].status !== 'active') {
await db.query('ROLLBACK');
return res.status(404).json({ success: false, message: '商品不存在或已下架' });
}
let availableStock = products[0].stock;
// 如果有规格,检查规格库存
if (cartItem.specification_id) {
const [specs] = await db.execute(
'SELECT stock, status FROM product_spec_combinations WHERE id = ?',
[cartItem.specification_id]
);
if (specs.length === 0 || specs[0].status !== 'active') {
await db.query('ROLLBACK');
return res.status(404).json({ success: false, message: '商品规格不存在或已下架' });
}
availableStock = specs[0].stock;
}
// 检查库存是否足够
if (availableStock < quantity) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '库存不足' });
}
// 更新购物车项数量
await db.execute(
'UPDATE cart_items SET quantity = ?, updated_at = NOW() WHERE id = ?',
[quantity, cartItemId]
);
await db.query('COMMIT');
res.json({
success: true,
message: '更新购物车成功'
});
} catch (error) {
await db.query('ROLLBACK');
console.error('更新购物车失败:', error);
res.status(500).json({ success: false, message: '更新购物车失败' });
}
});
/**
* @swagger
* /api/cart/{id}:
* delete:
* summary: 删除购物车商品
* tags: [Cart]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 购物车项ID
* responses:
* 200:
* description: 删除购物车商品成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* 401:
* description: 未授权
* 404:
* description: 购物车项不存在
* 500:
* description: 服务器错误
*/
router.delete('/:id', auth, async (req, res) => {
try {
const cartItemId = req.params.id;
const userId = req.user.id;
// 检查购物车项是否存在且属于当前用户
const [cartItems] = await getDB().execute(
'SELECT id FROM cart_items WHERE id = ? AND user_id = ?',
[cartItemId, userId]
);
if (cartItems.length === 0) {
return res.status(404).json({ success: false, message: '购物车项不存在' });
}
// 删除购物车项
await getDB().execute(
'DELETE FROM cart_items WHERE id = ?',
[cartItemId]
);
res.json({
success: true,
message: '删除购物车商品成功'
});
} catch (error) {
console.error('删除购物车商品失败:', error);
res.status(500).json({ success: false, message: '删除购物车商品失败' });
}
});
/**
* @swagger
* /api/cart/batch:
* delete:
* summary: 批量删除购物车商品
* tags: [Cart]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* cart_item_ids:
* type: array
* items:
* type: integer
* description: 购物车项ID数组
* required:
* - cart_item_ids
* responses:
* 200:
* description: 批量删除购物车商品成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* type: object
* properties:
* deleted_count:
* type: integer
* description: 删除的商品数量
* 400:
* description: 参数错误
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.delete('/batch', auth, async (req, res) => {
try {
const { cart_item_ids } = req.body;
const userId = req.user.id;
// 验证参数
if (!cart_item_ids || !Array.isArray(cart_item_ids) || cart_item_ids.length === 0) {
return res.status(400).json({ success: false, message: '请选择要删除的商品' });
}
// 构建删除条件
const placeholders = cart_item_ids.map(() => '?').join(',');
const query = `DELETE FROM cart_items WHERE id IN (${placeholders}) AND user_id = ?`;
const params = [...cart_item_ids, userId];
const [result] = await getDB().execute(query, params);
res.json({
success: true,
message: '批量删除购物车商品成功',
data: {
deleted_count: result.affectedRows
}
});
} catch (error) {
console.error('批量删除购物车商品失败:', error);
res.status(500).json({ success: false, message: '批量删除购物车商品失败' });
}
});
/**
* @swagger
* /api/cart/clear:
* delete:
* summary: 清空购物车
* tags: [Cart]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 清空购物车成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.delete('/clear', auth, async (req, res) => {
try {
const userId = req.user.id;
// 清空用户购物车
await getDB().execute(
'DELETE FROM cart_items WHERE user_id = ?',
[userId]
);
res.json({
success: true,
message: '清空购物车成功'
});
} catch (error) {
console.error('清空购物车失败:', error);
res.status(500).json({ success: false, message: '清空购物车失败' });
}
});
/**
* @swagger
* /api/cart/count:
* get:
* summary: 获取购物车商品数量
* tags: [Cart]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取购物车商品数量成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* count:
* type: integer
* description: 购物车商品总数量
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.get('/count', auth, async (req, res) => {
try {
const userId = req.user.id;
// 获取购物车商品总数量
const [result] = await getDB().execute(
'SELECT SUM(quantity) as count FROM cart_items WHERE user_id = ?',
[userId]
);
const count = result[0].count || 0;
res.json({
success: true,
data: { count }
});
} catch (error) {
console.error('获取购物车商品数量失败:', error);
res.status(500).json({ success: false, message: '获取购物车商品数量失败' });
}
});
/**
* @swagger
* /api/cart/checkout:
* post:
* summary: 购物车结账
* tags: [Cart]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* cart_item_ids:
* type: array
* items:
* type: integer
* description: 要结账的购物车项ID数组
* shipping_address:
* type: string
* description: 收货地址
* required:
* - cart_item_ids
* - shipping_address
* responses:
* 201:
* description: 结账成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* type: object
* properties:
* order_id:
* type: integer
* order_no:
* type: string
* total_amount:
* type: integer
* total_points:
* type: integer
* total_rongdou:
* type: integer
* 400:
* description: 参数错误或库存不足
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.post('/checkout', auth, async (req, res) => {
const db = getDB();
await db.query('START TRANSACTION');
try {
const { cart_item_ids, shipping_address } = req.body;
const userId = req.user.id;
// 验证参数
if (!cart_item_ids || !Array.isArray(cart_item_ids) || cart_item_ids.length === 0) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '请选择要结账的商品' });
}
if (!shipping_address) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '请填写收货地址' });
}
// 获取购物车商品信息
const placeholders = cart_item_ids.map(() => '?').join(',');
const cartQuery = `
SELECT
c.id, c.product_id, c.quantity, c.spec_combination_id,
p.name, p.price, p.points_price, p.rongdou_price, p.stock, p.status,
psc.price_adjustment, psc.points_adjustment, psc.rongdou_adjustment, psc.stock as spec_stock
FROM cart_items c
LEFT JOIN products p ON c.product_id = p.id
LEFT JOIN product_spec_combinations psc ON c.spec_combination_id = psc.id
WHERE c.id IN (${placeholders}) AND c.user_id = ?
`;
const [cartItems] = await db.execute(cartQuery, [...cart_item_ids, userId]);
if (cartItems.length === 0) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '购物车商品不存在' });
}
// 验证商品状态和库存
let totalAmount = 0;
let totalPoints = 0;
let totalRongdou = 0;
for (const item of cartItems) {
if (item.status !== 'active') {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: `商品 ${item.name} 已下架` });
}
const availableStock = item.spec_combination_id ? item.spec_stock : item.stock;
if (availableStock < item.quantity) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: `商品 ${item.name} 库存不足` });
}
const finalPrice = item.price + (item.price_adjustment || 0);
const finalPointsPrice = item.points_price + (item.points_adjustment || 0);
const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0);
totalAmount += finalPrice * item.quantity;
totalPoints += finalPointsPrice * item.quantity;
totalRongdou += finalRongdouPrice * item.quantity;
}
// 检查用户积分和融豆是否足够
const [users] = await db.execute(
'SELECT points, rongdou FROM users WHERE id = ?',
[userId]
);
if (users.length === 0) {
await db.query('ROLLBACK');
return res.status(404).json({ success: false, message: '用户不存在' });
}
const user = users[0];
if (user.points < totalPoints) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '积分不足' });
}
if (user.rongdou < totalRongdou) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '融豆不足' });
}
// 生成订单号
const orderNo = 'ORD' + Date.now() + Math.random().toString(36).substr(2, 5).toUpperCase();
// 创建订单
const [orderResult] = await db.execute(
`INSERT INTO orders (order_no, user_id, total_amount, total_points, total_rongdou,
status, shipping_address, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 'pending', ?, NOW(), NOW())`,
[orderNo, userId, totalAmount, totalPoints, totalRongdou, shipping_address]
);
const orderId = orderResult.insertId;
// 创建订单项
for (const item of cartItems) {
const finalPrice = item.price + (item.price_adjustment || 0);
const finalPointsPrice = item.points_price + (item.points_adjustment || 0);
const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0);
await db.execute(
`INSERT INTO order_items (order_id, product_id, spec_combination_id, quantity,
price, points_price, rongdou_price, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())`,
[orderId, item.product_id, item.spec_combination_id, item.quantity,
finalPrice, finalPointsPrice, finalRongdouPrice]
);
// 更新库存
if (item.spec_combination_id) {
await db.execute(
'UPDATE product_spec_combinations SET stock = stock - ? WHERE id = ?',
[item.quantity, item.spec_combination_id]
);
} else {
await db.execute(
'UPDATE products SET stock = stock - ? WHERE id = ?',
[item.quantity, item.product_id]
);
}
}
// 扣除用户积分和融豆
await db.execute(
'UPDATE users SET points = points - ?, rongdou = rongdou - ? WHERE id = ?',
[totalPoints, totalRongdou, userId]
);
// 删除已结账的购物车项
const deletePlaceholders = cart_item_ids.map(() => '?').join(',');
await db.execute(
`DELETE FROM cart_items WHERE id IN (${deletePlaceholders}) AND user_id = ?`,
[...cart_item_ids, userId]
);
await db.query('COMMIT');
res.status(201).json({
success: true,
message: '结账成功',
data: {
order_id: orderId,
order_no: orderNo,
total_amount: totalAmount,
total_points: totalPoints,
total_rongdou: totalRongdou
}
});
} catch (error) {
await db.query('ROLLBACK');
console.error('购物车结账失败:', error);
res.status(500).json({ success: false, message: '结账失败' });
}
});
module.exports = router;