升级商城逻辑

This commit is contained in:
2025-09-02 09:29:20 +08:00
parent 16bfc525c2
commit 49eed40ad0
30 changed files with 22710 additions and 1339 deletions

View File

@@ -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) => {