Files
2025-10-22 17:26:50 +08:00

937 lines
28 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/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: '清空购物车成功'
});
console.log(11111111111111)
} 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: '结账失败' });
}
});
/**
* @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 {
console.log(111111111)
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: '删除购物车商品失败' });
}
});
module.exports = router;