完善订单相关逻辑和商品推荐逻辑,添加接口注释

This commit is contained in:
dzl
2025-10-17 17:26:37 +08:00
parent f9a757046c
commit e535e8a9c5
6 changed files with 619 additions and 103 deletions

View File

@@ -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();

View File

@@ -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();

View File

@@ -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(
@@ -1143,84 +1176,183 @@ router.post('/confirm-payment', auth, async (req, res) => {
}
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) {

View File

@@ -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 {
@@ -397,6 +494,8 @@ router.get('/:id', async (req, res) => {
[id]
);
// console.log(123,specCombinations);
// 为每个规格组合获取详细的规格值信息
const enhancedSpecifications = [];
for (const combination of specCombinations) {
@@ -469,6 +568,8 @@ router.get('/:id', async (req, res) => {
}
}
// console.log(123,enhancedSpecifications);
// 获取商品属性
const [attributes] = await getDB().execute(
'SELECT * FROM product_attributes WHERE product_id = ? ORDER BY sort_order, id',
@@ -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,

View File

@@ -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;
}

View File

@@ -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 = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>融豆商城 API文档</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui.css" />
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
.swagger-ui .topbar {
display: none;
}
.swagger-ui .info .title {
color: #3b4151;
font-family: sans-serif;
font-size: 36px;
margin: 0;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-standalone-preset.js"></script>
<script>
// 兼容性处理
if (!Object.hasOwn) {
Object.hasOwn = function(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
};
}
window.onload = function() {
const ui = SwaggerUIBundle({
url: '/api-docs.json',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
persistAuthorization: true
});
window.ui = ui;
}
</script>
</body>
</html>
`;
// 自定义 Swagger 路由
app.get('/api-docs', (req, res) => {
res.send(swaggerHtml);
});
// 提供 JSON 格式的文档
app.get('/api-docs.json', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.send(specs);
});
// ============ Swagger 配置结束 ============
// 静态文件服务 - 必须在API路由之前
// 为uploads路径配置CORS和静态文件服务
app.use('/uploads', express.static(path.join(__dirname, 'uploads'), {