diff --git a/routes/category.js b/routes/category.js index d3a08dd..7977d65 100644 --- a/routes/category.js +++ b/routes/category.js @@ -2,6 +2,45 @@ const express = require('express'); const { getDB } = require('../database'); const router = express.Router(); + +/** + * @swagger + * /api/category: + * get: + * summary: 获取一级分类及其二级分类 + * description: 返回所有一级分类,每个一级分类包含其下的二级分类 + * tags: [category] + * responses: + * 200: + * description: 成功返回一级分类及其二级分类 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: object + * properties: + * name: + * type: string + * example: "一级分类1" + * relative: + * type: array + * items: + * type: object + * properties: + * name: + * type: string + * example: "二级分类1-1" + * img: + * type: string + * example: "https://example.com/image.jpg" + */ router.get('/', async (req, res) => { try { const db = await getDB(); diff --git a/routes/coupon.js b/routes/coupon.js index 898f997..5ac06c6 100644 --- a/routes/coupon.js +++ b/routes/coupon.js @@ -2,7 +2,58 @@ const express = require('express'); const router = express.Router(); const { getDB } = require('../database'); - +/** + * @swagger + * /api/coupon: + * get: + * summary: 获取用户优惠券 + * description: 返回用户所有优惠券,包含是否已领取状态 + * tags: [coupon] + * parameters: + * - in: query + * name: user_id + * schema: + * type: string + * required: true + * description: 用户ID + * 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: "满减优惠券" + * discount: + * type: number + * example: 100 + * remain: + * type: number + * example: 10 + * got: + * type: boolean + * example: false + * description: 是否已领取 + * + * 400: + * description: 缺少用户ID参数 + * 500: + * description: 服务器内部错误 + */ router.get('/', async (req, res) => { try { const useId = req.query.user_id; @@ -33,6 +84,43 @@ router.get('/', async (req, res) => { res.status(500).json({ error: '获取优惠券失败' }); } }); + +/** + * @swagger + * /api/coupon/{id}: + * get: + * summary: 用户领取优惠券 + * description: 用户通过优惠券ID领取优惠券,优惠券数量减一 + * tags: [coupon] + * parameters: + * - in: path + * name: id + * schema: + * type: string + * required: true + * description: 用户ID + * - in: query + * name: coupon_id + * schema: + * type: string + * required: true + * description: 优惠券ID + * responses: + * 200: + * description: 成功领取优惠券 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * 400: + * description: 缺少用户ID或优惠券ID参数 + * 500: + * description: 服务器内部错误 + */ router.get('/:id', async (req, res) => { try { const db = getDB(); @@ -68,6 +156,62 @@ router.get('/:id', async (req, res) => { } }); +/** + * @swagger + * /api/coupon/user/{id}: + * get: + * summary: 获取用户优惠券 + * description: 返回用户领取的优惠券,包括优惠券信息和商品信息 + * tags: [coupon] + * parameters: + * - in: path + * name: id + * schema: + * type: string + * required: true + * description: 用户ID + * 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: "满减优惠券" + * discount: + * type: number + * example: 100 + * products_id: + * type: array + * items: + * type: string + * example: "1" + * remain: + * type: number + * example: 100 + * get_time: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * 400: + * description: 缺少用户ID参数 + * 500: + * description: 服务器内部错误 + */ router.get('/user/:id', async (req, res) => { try { const db = getDB(); diff --git a/routes/orders.js b/routes/orders.js index 141ff67..23d91b5 100644 --- a/routes/orders.js +++ b/routes/orders.js @@ -385,7 +385,7 @@ router.get('/:id', auth, async (req, res) => { } // 处理地址信息 - console.log(order.address,'order.address'); + // console.log(order.address,'order.address'); if (order.address) { try { @@ -442,6 +442,23 @@ router.post('/create-from-cart', auth, async (req, res) => { return res.status(400).json({ success: false, message: '购物车商品不存在' }); } + for (const item of cartItems) { + + const [stock] = await getDB().execute( + `SELECT stock FROM product_spec_combinations WHERE id = ?`, + [item.specification_id] + ); + + if (stock[0].stock < item.quantity) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: `商品 ${item.name} 库存不足` }); + } + await db.execute( + `UPDATE product_spec_combinations SET stock = stock - ? WHERE id = ?`, + [item.quantity, item.specification_id] + ); + } + // 验证商品状态和库存,计算总价和支付方式 let totalAmount = 0; let totalPoints = 0; @@ -612,9 +629,9 @@ router.put('/:id/cancel', auth, async (req, res) => { const order = orders[0]; - if (order.status !== 'pending') { + if (order.status !== 'pending' && order.status !== 'pre_order') { await db.query('ROLLBACK'); - return res.status(400).json({ success: false, message: '只能取消待处理的订单' }); + return res.status(400).json({ success: false, message: '只能取消待处理或待支付的订单' }); } // 退还用户积分 @@ -630,6 +647,19 @@ router.put('/:id/cancel', auth, async (req, res) => { [userId, order.total_points] ); + console.log(12345,order.id); + const [orderDetails] = await db.execute( + 'SELECT * FROM order_items WHERE order_id = ?', + [order.id] + ); + console.log(12345,orderDetails); + // 返还库存 + await db.execute( + 'UPDATE product_spec_combinations SET stock = stock + ? WHERE id = ?', + [orderDetails[0].quantity, orderDetails[0].spec_combination_id] + ); + + // 更新订单状态 await db.execute( 'UPDATE orders SET status = "cancelled", updated_at = NOW() WHERE id = ?', @@ -937,7 +967,7 @@ router.get('/pending-payment/:id', auth, async (req, res) => { `SELECT oi.id, oi.product_id, oi.quantity, oi.price, oi.points_price, oi.rongdou_price, oi.spec_combination_id, - p.name as product_name, p.image_url, p.description, + p.name as product_name, p.image_url, p.description, p.payment_methods, psc.spec_values as spec_info FROM order_items oi LEFT JOIN products p ON oi.product_id = p.id @@ -961,7 +991,10 @@ router.get('/pending-payment/:id', auth, async (req, res) => { success: true, data: { ...order, - items: orderItems + items: orderItems.map(item => ({ + ...item, + payment_methods: JSON.parse(item.payment_methods) + })) } }); } catch (error) { @@ -1061,7 +1094,7 @@ router.post('/confirm-payment', auth, async (req, res) => { try { await connection.beginTransaction(); - const { orderId: order_id, addressId: address_id, couponRecordId } = req.body; + const { orderId: order_id, addressId: address_id, couponRecordId, paymentMethod } = req.body; const userId = req.user.id; // 验证必填字段 @@ -1089,29 +1122,29 @@ router.post('/confirm-payment', auth, async (req, res) => { const order = orders[0]; // 解析支付方式 - let allPaymentMethods = []; - // console.log(typeof order.payment_methods_list); + // let allPaymentMethods = []; + // // console.log(typeof order.payment_methods_list); - if (order.payment_methods_list) { - try { - // 数据库中存储的是序列化的JSON字符串,直接解析 + // if (order.payment_methods_list) { + // try { + // // 数据库中存储的是序列化的JSON字符串,直接解析 - allPaymentMethods = JSON.parse(JSON.parse(order.payment_methods_list)); - } catch (e) { - console.error('解析支付方式失败:', e, 'raw data:', order.payment_methods_list); - allPaymentMethods = []; - } - } + // allPaymentMethods = JSON.parse(JSON.parse(order.payment_methods_list)); + // } catch (e) { + // console.error('解析支付方式失败:', e, 'raw data:', order.payment_methods_list); + // allPaymentMethods = []; + // } + // } - // 去重支付方式 - allPaymentMethods = [...new Set(allPaymentMethods)]; + // // 去重支付方式 + // allPaymentMethods = [...new Set(allPaymentMethods)]; - // 判断支付方式类型 - const hasPoints = allPaymentMethods.includes('points') || allPaymentMethods.includes('points_rongdou'); - const hasRongdou = allPaymentMethods.includes('rongdou') || allPaymentMethods.includes('points_rongdou'); - const isComboPayment = allPaymentMethods.includes('points_rongdou'); + // // 判断支付方式类型 + // const hasPoints = allPaymentMethods.includes('points') || allPaymentMethods.includes('points_rongdou'); + // const hasRongdou = allPaymentMethods.includes('rongdou') || allPaymentMethods.includes('points_rongdou'); + // const isComboPayment = allPaymentMethods.includes('points_rongdou'); - console.log('订单支付方式:', allPaymentMethods, { hasPoints, hasRongdou, isComboPayment }); + // console.log('订单支付方式:', allPaymentMethods, { hasPoints, hasRongdou, isComboPayment }); // 获取收货地址信息 const [addresses] = await connection.execute( @@ -1142,85 +1175,184 @@ router.post('/confirm-payment', auth, async (req, res) => { return res.status(400).json({ success: false, message: '融豆不足' }); } user.balance = Math.abs(user.balance); - - // 根据支付方式处理扣费逻辑 - let totalRongdouNeeded = order.total_rongdou; // 需要的融豆总数 - let pointsToDeduct = 0; // 需要扣除的积分 - let rongdouToDeduct = 0; // 需要扣除的融豆 - - if (!hasRongdou && !hasPoints) { - await connection.rollback(); - return res.status(400).json({ success: false, message: '商品支付方式配置错误' }); - } - - if (hasPoints && !hasRongdou) { - // 只支持积分支付,按10000积分=1融豆转换 - const pointsNeeded = totalRongdouNeeded * 10000; - if (user.points < pointsNeeded) { - await connection.rollback(); - return res.status(400).json({ success: false, message: '积分不足' }); - } - pointsToDeduct = pointsNeeded; - rongdouToDeduct = 0; - } else if (!hasPoints && hasRongdou) { - // 只支持融豆支付 - if (user.balance < totalRongdouNeeded) { - await connection.rollback(); - return res.status(400).json({ success: false, message: '融豆不足' }); - } - pointsToDeduct = 0; - rongdouToDeduct = totalRongdouNeeded; - } else if (hasPoints && hasRongdou) { - // 组合支付:先扣积分,不足部分用融豆 - const availablePointsInRongdou = Math.floor(user.points / 10000); // 积分可转换的融豆数 - - if (availablePointsInRongdou >= totalRongdouNeeded) { - // 积分足够支付全部 - pointsToDeduct = totalRongdouNeeded * 10000; - rongdouToDeduct = 0; - } else { - // 积分不够,需要组合支付 - pointsToDeduct = availablePointsInRongdou * 10000; - rongdouToDeduct = totalRongdouNeeded - availablePointsInRongdou; - - if (user.balance < rongdouToDeduct) { + + // 开始扣钱 + switch (paymentMethod) { + case 'points': + // 积分支付逻辑 + if (user.points < order.total_points) { await connection.rollback(); - return res.status(400).json({ success: false, message: '积分和融豆余额不足' }); + return res.status(400).json({ success: false, message: '积分不足' }); } - } + await connection.execute( + 'UPDATE users SET points = points - ? WHERE id = ?', + [order.total_points, userId] + ); + + // 记录积分变动历史 + await connection.execute( + `INSERT INTO points_history (user_id, type, amount, description, order_id) + VALUES (?, 'spend', ?, ?, ?)`, + [userId, order.total_points, `订单支付 - ${order.order_no}`, order_id] + ); + + // 供应商分佣 + // await connection.execute( + // 'UPDATE users SET points = points + ? WHERE id = ?', + // [order.total_points * 0.1, order.shop_id] + // ); + // console.log(123,order) + + break; + case 'beans': + // 融豆支付逻辑 + if (user.balance < order.total_rongdou) { + await connection.rollback(); + return res.status(400).json({ success: false, message: '融豆不足' }); + } + await connection.execute( + 'UPDATE users SET balance = balance + ? WHERE id = ?', + [order.total_rongdou, userId] + ); + + // 记录融豆变动历史 + await connection.execute( + `INSERT INTO points_history (user_id, type, amount, description, order_id) + VALUES (?, 'spend', ?, ?, ?)`, + [userId, order.total_rongdou, `订单支付 - ${order.order_no}`, order_id] + ); + break; + case 'mixed': + // 积分和融豆组合支付逻辑 + + if(user.points < order.total_points) { + const needPoints = (user.points/10000).floor(0) * 10000; + const needBeans = order.total_rongdou - needPoints/10000; + if(user.balance < needBeans) { + await connection.rollback(); + return res.status(400).json({ success: false, message: '融豆不足' }); + } + + await connection.execute( + 'UPDATE users SET points = points - ? WHERE id = ?', + [needPoints, userId] + ); + + // 记录积分变动历史 + await connection.execute( + `INSERT INTO points_history (user_id, type, amount, description, order_id) + VALUES (?, 'spend', ?, ?, ?)`, + [userId, needPoints, `订单支付 - ${order.order_no}`, order_id] + ); + + await connection.execute( + 'UPDATE users SET balance = balance + ? WHERE id = ?', + [needBeans, userId] + ); + + // 记录融豆变动历史 + await connection.execute( + `INSERT INTO points_history (user_id, type, amount, description, order_id) + VALUES (?, 'spend', ?, ?, ?)`, + [userId, needBeans, `订单支付 - ${order.order_no}`, order_id] + ); + } else { + await connection.execute( + 'UPDATE users SET points = points - ? WHERE id = ?', + [order.total_points, userId] + ); + // 记录积分变动历史 + await connection.execute( + `INSERT INTO points_history (user_id, type, amount, description, order_id) + VALUES (?, 'spend', ?, ?, ?)`, + [userId, order.total_points, `订单支付 - ${order.order_no}`, order_id] + ); + } + + break; + default: + await connection.rollback(); + return res.status(400).json({ success: false, message: '支付方式配置错误' }); } - console.log('扣费计算:', { totalRongdouNeeded, pointsToDeduct, rongdouToDeduct, userPoints: user.points, userBalance: user.balance }); + // // 根据支付方式处理扣费逻辑 + // let totalRongdouNeeded = order.total_rongdou; // 需要的融豆总数 + // let pointsToDeduct = 0; // 需要扣除的积分 + // let rongdouToDeduct = 0; // 需要扣除的融豆 + + // if (!hasRongdou && !hasPoints) { + // await connection.rollback(); + // return res.status(400).json({ success: false, message: '商品支付方式配置错误' }); + // } + + // if (hasPoints && !hasRongdou) { + // // 只支持积分支付,按10000积分=1融豆转换 + // const pointsNeeded = totalRongdouNeeded * 10000; + // if (user.points < pointsNeeded) { + // await connection.rollback(); + // return res.status(400).json({ success: false, message: '积分不足' }); + // } + // pointsToDeduct = pointsNeeded; + // rongdouToDeduct = 0; + // } else if (!hasPoints && hasRongdou) { + // // 只支持融豆支付 + // if (user.balance < totalRongdouNeeded) { + // await connection.rollback(); + // return res.status(400).json({ success: false, message: '融豆不足' }); + // } + // pointsToDeduct = 0; + // rongdouToDeduct = totalRongdouNeeded; + // } else if (hasPoints && hasRongdou) { + // // 组合支付:先扣积分,不足部分用融豆 + // const availablePointsInRongdou = Math.floor(user.points / 10000); // 积分可转换的融豆数 + + // if (availablePointsInRongdou >= totalRongdouNeeded) { + // // 积分足够支付全部 + // pointsToDeduct = totalRongdouNeeded * 10000; + // rongdouToDeduct = 0; + // } else { + // // 积分不够,需要组合支付 + // pointsToDeduct = availablePointsInRongdou * 10000; + // rongdouToDeduct = totalRongdouNeeded - availablePointsInRongdou; + + // if (user.balance < rongdouToDeduct) { + // await connection.rollback(); + // return res.status(400).json({ success: false, message: '积分和融豆余额不足' }); + // } + // } + // } + + // console.log('扣费计算:', { totalRongdouNeeded, pointsToDeduct, rongdouToDeduct, userPoints: user.points, userBalance: user.balance }); // 扣除积分 - if (pointsToDeduct > 0) { - await connection.execute( - 'UPDATE users SET points = points - ? WHERE id = ?', - [pointsToDeduct, userId] - ); + // if (pointsToDeduct > 0) { + // await connection.execute( + // 'UPDATE users SET points = points - ? WHERE id = ?', + // [pointsToDeduct, userId] + // ); - // 记录积分变动历史 - await connection.execute( - `INSERT INTO points_history (user_id, type, amount, description, order_id) - VALUES (?, 'spend', ?, ?, ?)`, - [userId, pointsToDeduct, `订单支付 - ${order.order_no}`, order_id] - ); - } + // // 记录积分变动历史 + // await connection.execute( + // `INSERT INTO points_history (user_id, type, amount, description, order_id) + // VALUES (?, 'spend', ?, ?, ?)`, + // [userId, pointsToDeduct, `订单支付 - ${order.order_no}`, order_id] + // ); + // } - // 扣除融豆 - if (rongdouToDeduct > 0) { - await connection.execute( - 'UPDATE users SET balance = balance + ? WHERE id = ?', - [rongdouToDeduct, userId] - ); + // // 扣除融豆 + // if (rongdouToDeduct > 0) { + // await connection.execute( + // 'UPDATE users SET balance = balance + ? WHERE id = ?', + // [rongdouToDeduct, userId] + // ); - // 记录融豆变动历史 - await connection.execute( - `INSERT INTO rongdou_history (user_id, type, amount, description, order_id) - VALUES (?, 'spend', ?, ?, ?)`, - [userId, rongdouToDeduct, `订单支付 - ${order.order_no}`, order_id] - ); - } + // // 记录融豆变动历史 + // await connection.execute( + // `INSERT INTO rongdou_history (user_id, type, amount, description, order_id) + // VALUES (?, 'spend', ?, ?, ?)`, + // [userId, rongdouToDeduct, `订单支付 - ${order.order_no}`, order_id] + // ); + // } // 更新优惠券记录 if (couponRecordId) { diff --git a/routes/products.js b/routes/products.js index bad609e..37f52fe 100644 --- a/routes/products.js +++ b/routes/products.js @@ -4,6 +4,103 @@ const { auth, adminAuth } = require('../middleware/auth'); const router = express.Router(); +/** + * @swagger + * /api/products: + * get: + * summary: 获取商品列表 + * description: 返回商品列表,支持分页、搜索、分类、状态过滤 + * tags: [products] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * description: 页码,默认1 + * - in: query + * name: limit + * schema: + * type: integer + * description: 每页数量,默认10,最大100 + * - in: query + * name: search + * schema: + * type: string + * description: 搜索商品名称 + * - in: query + * name: category + * schema: + * type: string + * description: 分类名称 + * - in: query + * name: status + * schema: + * type: string + * description: 商品状态(active/inactive) + * responses: + * 200: + * description: 成功返回商品列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * example: "1" + * name: + * type: string + * example: "商品A" + * rongdou_price: + * type: number + * example: 100 + * points_price: + * type: number + * example: 1000 + * stock: + * type: integer + * example: 100 + * image: + * type: string + * example: "https://example.com/image.jpg" + * description: + * type: string + * example: "这是一个商品" + * status: + * type: string + * example: "active" + * category_id: + * type: string + * example: "1" + * created_at: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00Z" + * updated_at: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00Z" + * sales: + * type: integer + * example: 100 + * images: + * type: array + * items: + * type: string + * example: "https://example.com/image.jpg" + * 400: + * description: 无效的分页参数 + * 500: + * description: 服务器内部错误 + */ // 商品管理路由 router.get('/', async (req, res) => { try { @@ -396,6 +493,8 @@ router.get('/:id', async (req, res) => { ORDER BY psc.combination_key`, [id] ); + + // console.log(123,specCombinations); // 为每个规格组合获取详细的规格值信息 const enhancedSpecifications = []; @@ -468,6 +567,8 @@ router.get('/:id', async (req, res) => { }); } } + + // console.log(123,enhancedSpecifications); // 获取商品属性 const [attributes] = await getDB().execute( @@ -994,14 +1095,25 @@ router.get('/:id/recommended', async (req, res) => { // 如果同类别商品不足,补充其他热门商品 if (recommendedProducts.length < 6) { - const recommendQuery = ` + let recommendQuery = ` SELECT products_id FROM recommend_product - WHERE products_id NOT IN (${filteredRecommendProductIds.map(() => '?').join(',')}) - ORDER BY RAND() - LIMIT ${6 - recommendedProducts.length} - ` - const [recommendProductIds] = await getDB().execute(recommendQuery, [...filteredRecommendProductIds]); + 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); filteredRecommendProductIds.push(...recommendProductIds.map(item => item.products_id)); + for (const item of recommendProductIds) { const recommendQuery = ` SELECT id, name, price, points_price as points, @@ -1012,7 +1124,7 @@ router.get('/:id/recommended', async (req, res) => { const [recommendProduct] = await getDB().execute(recommendQuery, [item.products_id]); recommendedProducts.push(recommendProduct[0]); } - if (recommendProductIds.length + recommendedProducts.length < 6) { + if (recommendProductIds.length < 6) { // 补充其他热门商品 const additionalQuery = ` SELECT id, name, price, points_price as points, diff --git a/routes/regions.js b/routes/regions.js index 6bded1a..af35d1f 100644 --- a/routes/regions.js +++ b/routes/regions.js @@ -163,7 +163,7 @@ router.get('/provinces', async (req, res) => { }); global.provinces = regionsByLevel[1]; }else { - console.log('1111') + // console.log('1111') regionsByLevel[1] = global.provinces; } diff --git a/server.js b/server.js index ee3dee3..27d7e27 100644 --- a/server.js +++ b/server.js @@ -88,6 +88,95 @@ const limiter = rateLimit({ }); app.use('/api', limiter); + + +// ============ 添加 Swagger 文档路由 ============ +const swaggerUi = require('swagger-ui-express'); +const specs = require('./swagger'); + +// 创建自定义的 HTML 页面来解决兼容性问题 +const swaggerHtml = ` + + +
+ +