升级商城逻辑
This commit is contained in:
@@ -100,16 +100,20 @@ const { auth } = require('../middleware/auth');
|
||||
router.get('/', auth, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
|
||||
|
||||
const [addresses] = await getDB().execute(
|
||||
`SELECT ua.*, al.name as label_name, al.color as label_color
|
||||
`SELECT ua.*, al.name as label_name, al.color as label_color,
|
||||
p.name as province_name, c.name as city_name, d.name as district_name
|
||||
FROM user_addresses ua
|
||||
LEFT JOIN address_labels al ON ua.label_id = al.id
|
||||
WHERE ua.user_id = ? AND ua.deleted_at IS NULL
|
||||
LEFT JOIN address_labels al ON ua.label = al.id
|
||||
LEFT JOIN china_regions p ON ua.province = p.code
|
||||
LEFT JOIN china_regions c ON ua.city = c.code
|
||||
LEFT JOIN china_regions d ON ua.district = d.code
|
||||
WHERE ua.user_id = ?
|
||||
ORDER BY ua.is_default DESC, ua.created_at DESC`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: addresses
|
||||
@@ -158,7 +162,7 @@ router.get('/:id', auth, async (req, res) => {
|
||||
try {
|
||||
const addressId = req.params.id;
|
||||
const userId = req.user.id;
|
||||
|
||||
|
||||
const [addresses] = await getDB().execute(
|
||||
`SELECT ua.*, al.name as label_name, al.color as label_color
|
||||
FROM user_addresses ua
|
||||
@@ -166,11 +170,11 @@ router.get('/:id', auth, async (req, res) => {
|
||||
WHERE ua.id = ? AND ua.user_id = ? AND ua.deleted_at IS NULL`,
|
||||
[addressId, userId]
|
||||
);
|
||||
|
||||
|
||||
if (addresses.length === 0) {
|
||||
return res.status(404).json({ message: '收货地址不存在' });
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: addresses[0]
|
||||
@@ -259,41 +263,36 @@ router.post('/', auth, async (req, res) => {
|
||||
recipient_name,
|
||||
phone,
|
||||
province_code,
|
||||
province_name,
|
||||
city_code,
|
||||
city_name,
|
||||
district_code,
|
||||
district_name,
|
||||
detailed_address,
|
||||
postal_code,
|
||||
label_id,
|
||||
is_default = false
|
||||
} = req.body;
|
||||
|
||||
|
||||
// 验证必填字段
|
||||
if (!recipient_name || !phone || !province_code || !city_code || !district_code || !detailed_address) {
|
||||
return res.status(400).json({ message: '收件人姓名、电话、省市区和详细地址不能为空' });
|
||||
}
|
||||
|
||||
|
||||
// 如果设置为默认地址,先取消其他默认地址
|
||||
if (is_default) {
|
||||
await getDB().execute(
|
||||
'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND deleted_at IS NULL',
|
||||
'UPDATE user_addresses SET is_default = false WHERE user_id = ? ',
|
||||
[userId]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const [result] = await getDB().execute(
|
||||
`INSERT INTO user_addresses (
|
||||
user_id, recipient_name, phone, province_code, province_name, city_code, city_name,
|
||||
district_code, district_name, detailed_address, postal_code, label_id, is_default, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
|
||||
user_id, receiver_name, receiver_phone, province, city,
|
||||
district, detailed_address, is_default, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
|
||||
[
|
||||
userId, recipient_name, phone, province_code, province_name, city_code, city_name,
|
||||
district_code, district_name, detailed_address, postal_code, label_id, is_default
|
||||
userId, recipient_name, phone, province_code, city_code,
|
||||
district_code, detailed_address, is_default
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '收货地址创建成功',
|
||||
@@ -383,48 +382,45 @@ router.put('/:id', auth, async (req, res) => {
|
||||
recipient_name,
|
||||
phone,
|
||||
province_code,
|
||||
province_name,
|
||||
city_code,
|
||||
city_name,
|
||||
district_code,
|
||||
district_name,
|
||||
detailed_address,
|
||||
postal_code,
|
||||
label_id,
|
||||
is_default
|
||||
} = req.body;
|
||||
|
||||
if (!recipient_name || !phone || !province_code || !city_code || !district_code || !detailed_address) {
|
||||
return res.status(400).json({ message: '收件人姓名、电话、省市区和详细地址不能为空' });
|
||||
}
|
||||
|
||||
// 检查地址是否存在且属于当前用户
|
||||
const [existing] = await getDB().execute(
|
||||
'SELECT id FROM user_addresses WHERE id = ? AND user_id = ? AND deleted_at IS NULL',
|
||||
'SELECT id FROM user_addresses WHERE id = ? AND user_id = ? ',
|
||||
[addressId, userId]
|
||||
);
|
||||
|
||||
|
||||
if (existing.length === 0) {
|
||||
return res.status(404).json({ message: '收货地址不存在' });
|
||||
}
|
||||
|
||||
|
||||
// 如果设置为默认地址,先取消其他默认地址
|
||||
if (is_default) {
|
||||
await getDB().execute(
|
||||
'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND id != ? AND deleted_at IS NULL',
|
||||
'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND id != ? ',
|
||||
[userId, addressId]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const [result] = await getDB().execute(
|
||||
`UPDATE user_addresses SET
|
||||
recipient_name = ?, phone = ?, province_code = ?, province_name = ?,
|
||||
city_code = ?, city_name = ?, district_code = ?, district_name = ?,
|
||||
detailed_address = ?, postal_code = ?, label_id = ?, is_default = ?, updated_at = NOW()
|
||||
receiver_name = ?, receiver_phone = ?, province = ?, city = ?,
|
||||
district = ?, detailed_address = ?, is_default = ?, updated_at = NOW()
|
||||
WHERE id = ? AND user_id = ?`,
|
||||
[
|
||||
recipient_name, phone, province_code, province_name, city_code, city_name,
|
||||
district_code, district_name, detailed_address, postal_code, label_id, is_default,
|
||||
recipient_name, phone, province_code, city_code,
|
||||
district_code, detailed_address, is_default,
|
||||
addressId, userId
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '收货地址更新成功'
|
||||
@@ -439,7 +435,7 @@ router.put('/:id', auth, async (req, res) => {
|
||||
* @swagger
|
||||
* /addresses/{id}:
|
||||
* delete:
|
||||
* summary: 删除收货地址(软删除)
|
||||
* summary: 删除收货地址
|
||||
* tags: [Addresses]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
@@ -475,16 +471,16 @@ router.delete('/:id', auth, async (req, res) => {
|
||||
try {
|
||||
const addressId = req.params.id;
|
||||
const userId = req.user.id;
|
||||
|
||||
|
||||
const [result] = await getDB().execute(
|
||||
'UPDATE user_addresses SET deleted_at = NOW() WHERE id = ? AND user_id = ? AND deleted_at IS NULL',
|
||||
'DELETE FROM user_addresses WHERE id = ? AND user_id = ?',
|
||||
[addressId, userId]
|
||||
);
|
||||
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ message: '收货地址不存在' });
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '收货地址删除成功'
|
||||
@@ -535,29 +531,29 @@ router.put('/:id/default', auth, async (req, res) => {
|
||||
try {
|
||||
const addressId = req.params.id;
|
||||
const userId = req.user.id;
|
||||
|
||||
|
||||
// 检查地址是否存在且属于当前用户
|
||||
const [existing] = await getDB().execute(
|
||||
'SELECT id FROM user_addresses WHERE id = ? AND user_id = ? AND deleted_at IS NULL',
|
||||
[addressId, userId]
|
||||
);
|
||||
|
||||
|
||||
if (existing.length === 0) {
|
||||
return res.status(404).json({ message: '收货地址不存在' });
|
||||
}
|
||||
|
||||
|
||||
// 取消其他默认地址
|
||||
await getDB().execute(
|
||||
'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND deleted_at IS NULL',
|
||||
[userId]
|
||||
);
|
||||
|
||||
|
||||
// 设置当前地址为默认
|
||||
await getDB().execute(
|
||||
'UPDATE user_addresses SET is_default = true WHERE id = ? AND user_id = ?',
|
||||
[addressId, userId]
|
||||
);
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '默认地址设置成功'
|
||||
|
||||
935
routes/cart.js
Normal file
935
routes/cart.js
Normal file
@@ -0,0 +1,935 @@
|
||||
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;
|
||||
1377
routes/orders.js
1377
routes/orders.js
File diff suppressed because it is too large
Load Diff
@@ -4,123 +4,7 @@ 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;
|
||||
@@ -159,7 +43,7 @@ router.get('/', async (req, res) => {
|
||||
|
||||
// 获取商品列表
|
||||
const query = `
|
||||
SELECT id, name, category, points_price as points, stock, image_url as image, description, status, created_at, updated_at
|
||||
SELECT id, name, rongdou_price, category, points_price as points, stock, image_url as image, description, status, payment_methods, created_at, updated_at
|
||||
FROM products
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
@@ -170,7 +54,10 @@ router.get('/', async (req, res) => {
|
||||
const queryParams = [...params];
|
||||
console.log('Query params:', queryParams, 'Query:', query);
|
||||
const [products] = await getDB().execute(query, queryParams);
|
||||
|
||||
products.forEach(item=>{
|
||||
item.payment_methods = JSON.parse(item.payment_methods)
|
||||
})
|
||||
console.log('查询结果:', products);
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -189,30 +76,7 @@ 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(
|
||||
@@ -231,11 +95,111 @@ router.get('/categories', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 获取热销商品
|
||||
router.get('/hot', async (req, res) => {
|
||||
try {
|
||||
// 从活跃商品中随机获取2个商品
|
||||
const [products] = await getDB().execute(
|
||||
`SELECT id, name, category, price, points_price, rongdou_price, stock,
|
||||
image_url, images, description, shop_name, shop_avatar,
|
||||
payment_methods, sales, rating, status, created_at, updated_at
|
||||
FROM products
|
||||
WHERE status = 'active' AND stock > 0
|
||||
ORDER BY RAND()
|
||||
LIMIT 2`
|
||||
);
|
||||
|
||||
// 格式化商品数据
|
||||
const formattedProducts = products.map(product => ({
|
||||
...product,
|
||||
images: product.images ? JSON.parse(product.images) : (product.image_url ? [product.image_url] : []),
|
||||
payment_methods: product.payment_methods ? JSON.parse(product.payment_methods) : ['points'],
|
||||
// 保持向后兼容
|
||||
points: product.points_price,
|
||||
image: product.image_url
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
products: formattedProducts
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取热销商品失败:', error);
|
||||
res.status(500).json({ success: false, message: '获取热销商品失败' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /products/flash-sale:
|
||||
* get:
|
||||
* summary: 获取秒杀商品
|
||||
* tags: [Products]
|
||||
* 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'
|
||||
*/
|
||||
router.get('/cheap', async (req, res) => {
|
||||
try {
|
||||
// 从活跃商品中随机获取2个商品作为秒杀商品
|
||||
const [products] = await getDB().execute(
|
||||
`SELECT id, name, category, price, points_price, rongdou_price, stock,
|
||||
image_url, images, description, shop_name, shop_avatar,
|
||||
payment_methods, sales, rating, status, created_at, updated_at
|
||||
FROM products
|
||||
WHERE status = 'active' AND stock > 0
|
||||
ORDER BY RAND()
|
||||
LIMIT 2`
|
||||
);
|
||||
|
||||
// 格式化商品数据,为秒杀商品添加特殊标识
|
||||
const formattedProducts = products.map(product => ({
|
||||
...product,
|
||||
images: product.images ? JSON.parse(product.images) : (product.image_url ? [product.image_url] : []),
|
||||
payment_methods: product.payment_methods ? JSON.parse(product.payment_methods) : ['points'],
|
||||
// 秒杀商品特殊处理:价格打8折
|
||||
flash_sale_price: Math.floor(product.price * 0.8),
|
||||
flash_sale_points: Math.floor(product.points_price * 0.8),
|
||||
flash_sale_rongdou: Math.floor(product.rongdou_price * 0.8),
|
||||
is_flash_sale: true,
|
||||
// 保持向后兼容
|
||||
points: product.points_price,
|
||||
image: product.image_url
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
products: formattedProducts
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取秒杀商品失败:', error);
|
||||
res.status(500).json({ success: false, message: '获取秒杀商品失败' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /products/{id}:
|
||||
* get:
|
||||
* summary: 获取单个商品详情
|
||||
* summary: 获取单个商品详情(包含增强规格信息)
|
||||
* tags: [Products]
|
||||
* parameters:
|
||||
* - in: path
|
||||
@@ -246,7 +210,7 @@ router.get('/categories', async (req, res) => {
|
||||
* description: 商品ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取商品详情
|
||||
* description: 成功获取商品详情,包含完整的规格信息
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
@@ -254,8 +218,116 @@ router.get('/categories', async (req, res) => {
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Product'
|
||||
* type: object
|
||||
* properties:
|
||||
* product:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* name:
|
||||
* type: string
|
||||
* category:
|
||||
* type: string
|
||||
* price:
|
||||
* type: number
|
||||
* points_price:
|
||||
* type: number
|
||||
* rongdou_price:
|
||||
* type: number
|
||||
* stock:
|
||||
* type: integer
|
||||
* specifications:
|
||||
* type: array
|
||||
* description: 商品规格组合列表(笛卡尔积规格系统)
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 规格组合ID
|
||||
* combination_key:
|
||||
* type: string
|
||||
* description: 规格组合键(如:1-3-5)
|
||||
* spec_display:
|
||||
* type: string
|
||||
* description: 规格显示文本(如:颜色:红色 | 尺寸:XL)
|
||||
* spec_details:
|
||||
* type: array
|
||||
* description: 规格详细信息
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* spec_name:
|
||||
* type: string
|
||||
* description: 规格名称
|
||||
* spec_display_name:
|
||||
* type: string
|
||||
* description: 规格显示名称
|
||||
* value:
|
||||
* type: string
|
||||
* description: 规格值
|
||||
* display_value:
|
||||
* type: string
|
||||
* description: 规格显示值
|
||||
* color_code:
|
||||
* type: string
|
||||
* description: 颜色代码
|
||||
* image_url:
|
||||
* type: string
|
||||
* description: 规格图片
|
||||
* price_adjustment:
|
||||
* type: number
|
||||
* description: 价格调整
|
||||
* points_adjustment:
|
||||
* type: number
|
||||
* description: 积分调整
|
||||
* rongdou_adjustment:
|
||||
* type: number
|
||||
* description: 融豆调整
|
||||
* stock:
|
||||
* type: integer
|
||||
* description: 规格库存
|
||||
* sku_code:
|
||||
* type: string
|
||||
* description: SKU编码
|
||||
* barcode:
|
||||
* type: string
|
||||
* description: 条形码
|
||||
* weight:
|
||||
* type: number
|
||||
* description: 重量
|
||||
* volume:
|
||||
* type: number
|
||||
* description: 体积
|
||||
* actual_price:
|
||||
* type: number
|
||||
* description: 实际价格(基础价格+调整)
|
||||
* actual_points_price:
|
||||
* type: number
|
||||
* description: 实际积分价格
|
||||
* actual_rongdou_price:
|
||||
* type: number
|
||||
* description: 实际融豆价格
|
||||
* is_available:
|
||||
* type: boolean
|
||||
* description: 是否有库存
|
||||
* specification_count:
|
||||
* type: integer
|
||||
* description: 规格总数
|
||||
* available_specifications:
|
||||
* type: integer
|
||||
* description: 有库存的规格数量
|
||||
* attributes:
|
||||
* type: array
|
||||
* description: 商品属性
|
||||
* isFavorited:
|
||||
* type: boolean
|
||||
* description: 是否已收藏
|
||||
* 404:
|
||||
* description: 商品不存在
|
||||
*/
|
||||
@@ -280,12 +352,92 @@ router.get('/:id', async (req, res) => {
|
||||
|
||||
const product = products[0];
|
||||
|
||||
// 获取商品规格
|
||||
const [specifications] = await getDB().execute(
|
||||
'SELECT * FROM product_specifications WHERE product_id = ? ORDER BY id',
|
||||
// 获取商品的规格组合(新的笛卡尔积规格系统)
|
||||
const [specCombinations] = await getDB().execute(
|
||||
`SELECT psc.*,
|
||||
GROUP_CONCAT(CONCAT(sn.display_name, ':', sv.display_value) ORDER BY sn.sort_order SEPARATOR ' | ') as spec_display
|
||||
FROM product_spec_combinations psc
|
||||
LEFT JOIN JSON_TABLE(psc.spec_values, '$[*]' COLUMNS (spec_value_id INT PATH '$')) jt ON TRUE
|
||||
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 psc.product_id = ? AND psc.status = 'active'
|
||||
GROUP BY psc.id
|
||||
ORDER BY psc.combination_key`,
|
||||
[id]
|
||||
);
|
||||
|
||||
// 为每个规格组合获取详细的规格值信息
|
||||
const enhancedSpecifications = [];
|
||||
for (const combination of specCombinations) {
|
||||
// 智能解析 spec_values 字段,兼容多种数据格式
|
||||
let specValueIds = [];
|
||||
try {
|
||||
if (combination.spec_values) {
|
||||
// 如果是 Buffer 对象,先转换为字符串
|
||||
let specValuesStr = combination.spec_values;
|
||||
if (Buffer.isBuffer(specValuesStr)) {
|
||||
specValuesStr = specValuesStr.toString('utf8');
|
||||
}
|
||||
|
||||
// 尝试 JSON 解析
|
||||
if (typeof specValuesStr === 'string') {
|
||||
specValuesStr = specValuesStr.trim();
|
||||
if (specValuesStr.startsWith('[') && specValuesStr.endsWith(']')) {
|
||||
// JSON 数组格式
|
||||
specValueIds = JSON.parse(specValuesStr);
|
||||
} else if (specValuesStr.includes(',')) {
|
||||
// 逗号分隔的字符串格式
|
||||
specValueIds = specValuesStr.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
|
||||
} else if (specValuesStr && !isNaN(parseInt(specValuesStr))) {
|
||||
// 单个数字
|
||||
specValueIds = [parseInt(specValuesStr)];
|
||||
}
|
||||
} else if (Array.isArray(specValuesStr)) {
|
||||
// 已经是数组
|
||||
specValueIds = specValuesStr;
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn(`解析规格值失败 (combination_id: ${combination.id}):`, parseError.message);
|
||||
specValueIds = [];
|
||||
}
|
||||
|
||||
// 获取规格值详情
|
||||
if (specValueIds && specValueIds.length > 0) {
|
||||
const placeholders = specValueIds.map(() => '?').join(',');
|
||||
const [specDetails] = await getDB().execute(
|
||||
`SELECT sv.*, sn.name as spec_name, sn.display_name as spec_display_name
|
||||
FROM spec_values sv
|
||||
LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id
|
||||
WHERE sv.id IN (${placeholders})
|
||||
ORDER BY sn.sort_order, sv.sort_order`,
|
||||
specValueIds
|
||||
);
|
||||
|
||||
enhancedSpecifications.push({
|
||||
id: combination.id,
|
||||
combination_key: combination.combination_key,
|
||||
spec_display: combination.spec_display,
|
||||
spec_details: specDetails,
|
||||
price_adjustment: combination.price_adjustment || 0,
|
||||
points_adjustment: combination.points_adjustment || 0,
|
||||
rongdou_adjustment: combination.rongdou_adjustment || 0,
|
||||
stock: combination.stock,
|
||||
sku_code: combination.sku_code,
|
||||
barcode: combination.barcode,
|
||||
weight: combination.weight,
|
||||
volume: combination.volume,
|
||||
actual_price: product.price + (combination.price_adjustment || 0),
|
||||
actual_points_price: product.points_price + (combination.points_adjustment || 0),
|
||||
actual_rongdou_price: product.rongdou_price + (combination.rongdou_adjustment || 0),
|
||||
is_available: combination.stock > 0,
|
||||
status: combination.status,
|
||||
created_at: combination.created_at,
|
||||
updated_at: combination.updated_at
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商品属性
|
||||
const [attributes] = await getDB().execute(
|
||||
'SELECT * FROM product_attributes WHERE product_id = ? ORDER BY sort_order, id',
|
||||
@@ -305,12 +457,72 @@ router.get('/:id', async (req, res) => {
|
||||
// 构建增强的商品数据
|
||||
const enhancedProduct = {
|
||||
...product,
|
||||
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,
|
||||
images: (() => {
|
||||
try {
|
||||
if (product.images) {
|
||||
let imagesStr = product.images;
|
||||
if (Buffer.isBuffer(imagesStr)) {
|
||||
imagesStr = imagesStr.toString('utf8');
|
||||
}
|
||||
if (typeof imagesStr === 'string') {
|
||||
imagesStr = imagesStr.trim();
|
||||
if (imagesStr.startsWith('[') && imagesStr.endsWith(']')) {
|
||||
return JSON.parse(imagesStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return product.image_url ? [product.image_url] : [];
|
||||
} catch (e) {
|
||||
console.warn('解析商品图片失败:', e.message);
|
||||
return product.image_url ? [product.image_url] : [];
|
||||
}
|
||||
})(),
|
||||
videos: (() => {
|
||||
try {
|
||||
if (product.videos) {
|
||||
let videosStr = product.videos;
|
||||
if (Buffer.isBuffer(videosStr)) {
|
||||
videosStr = videosStr.toString('utf8');
|
||||
}
|
||||
if (typeof videosStr === 'string') {
|
||||
videosStr = videosStr.trim();
|
||||
if (videosStr.startsWith('[') && videosStr.endsWith(']')) {
|
||||
return JSON.parse(videosStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
console.warn('解析商品视频失败:', e.message);
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
payment_methods: (() => {
|
||||
try {
|
||||
if (product.payment_methods) {
|
||||
let methodsStr = product.payment_methods;
|
||||
if (Buffer.isBuffer(methodsStr)) {
|
||||
methodsStr = methodsStr.toString('utf8');
|
||||
}
|
||||
if (typeof methodsStr === 'string') {
|
||||
methodsStr = methodsStr.trim();
|
||||
if (methodsStr.startsWith('[') && methodsStr.endsWith(']')) {
|
||||
return JSON.parse(methodsStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ['points'];
|
||||
} catch (e) {
|
||||
console.warn('解析支付方式失败:', e.message);
|
||||
return ['points'];
|
||||
}
|
||||
})(),
|
||||
specifications: enhancedSpecifications,
|
||||
attributes,
|
||||
isFavorited,
|
||||
// 规格统计信息
|
||||
specification_count: enhancedSpecifications.length,
|
||||
available_specifications: enhancedSpecifications.filter(spec => spec.is_available).length,
|
||||
// 保持向后兼容
|
||||
points: product.points_price,
|
||||
image: product.image_url,
|
||||
@@ -352,19 +564,7 @@ router.post('/', auth, adminAuth, async (req, res) => {
|
||||
|
||||
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) {
|
||||
@@ -499,25 +699,7 @@ 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) {
|
||||
@@ -655,8 +837,8 @@ router.get('/:id/reviews', async (req, res) => {
|
||||
JOIN users u ON pr.user_id = u.id
|
||||
WHERE pr.product_id = ?
|
||||
ORDER BY pr.created_at DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
[id, limit, offset]
|
||||
LIMIT ${limit} OFFSET ${offset}`,
|
||||
[id]
|
||||
);
|
||||
|
||||
// 获取评论总数
|
||||
@@ -832,8 +1014,8 @@ router.get('/favorites', auth, async (req, res) => {
|
||||
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]
|
||||
LIMIT ${limit} OFFSET ${offset}`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
const [countResult] = await getDB().execute(
|
||||
@@ -867,163 +1049,13 @@ router.get('/favorites', auth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 获取商品规格
|
||||
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) => {
|
||||
|
||||
@@ -120,12 +120,37 @@ router.get('/zhejiang', async (req, res) => {
|
||||
*/
|
||||
router.get('/provinces', async (req, res) => {
|
||||
try {
|
||||
// 递归获取子区域的函数
|
||||
async function getChildrenRecursively(parentCode, level) {
|
||||
const [children] = await getDB().execute(
|
||||
`SELECT code, name as label, level FROM china_regions
|
||||
WHERE parent_code = ? AND level = ?
|
||||
ORDER BY code`,
|
||||
[parentCode, level]
|
||||
);
|
||||
|
||||
// 为每个子区域递归获取其子区域
|
||||
for (let child of children) {
|
||||
if (level < 3) { // 最多到区县级别(level 3)
|
||||
child.children = await getChildrenRecursively(child.code, level + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
// 获取所有省份
|
||||
const [provinces] = await getDB().execute(
|
||||
`SELECT code, name FROM china_regions
|
||||
`SELECT code, name as label, level FROM china_regions
|
||||
WHERE level = 1
|
||||
ORDER BY code`
|
||||
);
|
||||
|
||||
// 为每个省份递归获取城市和区县
|
||||
for (let province of provinces) {
|
||||
province.children = await getChildrenRecursively(province.code, 2);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: provinces
|
||||
|
||||
1096
routes/specifications.js
Normal file
1096
routes/specifications.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -821,8 +821,8 @@ router.get('/user/:userId', authenticateToken, async (req, res) => {
|
||||
LEFT JOIN users to_user ON t.to_user_id = to_user.id
|
||||
${whereClause}
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, listParams);
|
||||
LIMIT ${limitNum} OFFSET ${offset}
|
||||
`, countParams);
|
||||
|
||||
const [countResult] = await db.execute(`
|
||||
SELECT COUNT(*) as total FROM transfers t ${whereClause}
|
||||
@@ -1556,10 +1556,11 @@ router.get('/daily-stats',
|
||||
u.balance,
|
||||
COALESCE(yesterday_out.amount, 0) as yesterday_out_amount,
|
||||
COALESCE(today_in.amount, 0) as today_in_amount,
|
||||
COALESCE(confirmed_from.confirmed_amount, 0) as confirmed_from_amount,
|
||||
CASE
|
||||
WHEN (COALESCE(yesterday_out.amount, 0) - COALESCE(today_in.amount, 0)) > ABS(u.balance)
|
||||
WHEN (COALESCE(u.balance, 0) +COALESCE(confirmed_from.confirmed_amount, 0) ) > ABS(u.balance)
|
||||
THEN ABS(u.balance)
|
||||
ELSE (COALESCE(yesterday_out.amount, 0) - COALESCE(today_in.amount, 0))
|
||||
ELSE (COALESCE(u.balance, 0)+ COALESCE(confirmed_from.confirmed_amount, 0) )
|
||||
END as balance_needed
|
||||
FROM users u
|
||||
LEFT JOIN (
|
||||
@@ -1580,13 +1581,29 @@ router.get('/daily-stats',
|
||||
AND status IN ('confirmed', 'received')
|
||||
GROUP BY to_user_id
|
||||
) today_in ON u.id = today_in.to_user_id
|
||||
left join (
|
||||
select
|
||||
from_user_id,
|
||||
sum(amount) as confirmed_amount
|
||||
from
|
||||
transfers
|
||||
where
|
||||
status = 'received'
|
||||
and created_at >= ?
|
||||
and created_at <= ?
|
||||
group by
|
||||
from_user_id
|
||||
) as confirmed_from on u.id = confirmed_from.from_user_id
|
||||
WHERE u.role != 'admin'
|
||||
AND u.is_system_account != 1
|
||||
AND yesterday_out.amount > 0
|
||||
AND u.balance < 0
|
||||
ORDER BY balance_needed DESC, yesterday_out_amount DESC
|
||||
`, [yesterdayStartStr, yesterdayEndStr, todayStartStr, todayEndStr]);
|
||||
userStats = userStats.filter(item=>item.balance_needed >= 100)
|
||||
`, [yesterdayStartStr, yesterdayEndStr, todayStartStr, todayEndStr, todayStartStr, todayEndStr]);
|
||||
// userStats = userStats.filter(item=>item.balance_needed >= 100)
|
||||
userStats.forEach(item=>{
|
||||
item.balance_needed = Math.abs(item.balance_needed)
|
||||
})
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
|
||||
593
routes/users.js
593
routes/users.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user