const express = require('express'); const router = express.Router(); const { getDB } = require('../database'); const { auth, adminAuth } = require('../middleware/auth'); /** * @swagger * components: * schemas: * SpecName: * type: object * properties: * id: * type: integer * name: * type: string * description: 规格名称(如:颜色、尺寸) * display_name: * type: string * description: 显示名称 * sort_order: * type: integer * description: 排序 * status: * type: string * enum: [active, inactive] * SpecValue: * type: object * properties: * id: * type: integer * spec_name_id: * type: integer * value: * type: string * description: 规格值(如:红色、XL) * display_value: * type: string * color_code: * type: string * description: 颜色代码 * image_url: * type: string * sort_order: * type: integer * status: * type: string * enum: [active, inactive] */ /** * @swagger * /specifications/names: * get: * summary: 获取所有规格名称 * tags: [Specifications] * parameters: * - 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: array * items: * $ref: '#/components/schemas/SpecName' */ router.get('/names', async (req, res) => { try { const { status = 'active' } = req.query; let query = 'SELECT * FROM spec_names'; const params = []; if (status) { query += ' WHERE status = ?'; params.push(status); } query += ' ORDER BY sort_order, id'; const [specNames] = await getDB().execute(query, params); res.json({ success: true, data: specNames }); } catch (error) { console.error('获取规格名称失败:', error); res.status(500).json({ success: false, message: '获取规格名称失败' }); } }); /** * @swagger * /specifications/names: * post: * summary: 创建规格名称 * tags: [Specifications] * security: * - bearerAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - name * - display_name * properties: * name: * type: string * description: 规格名称 * display_name: * type: string * description: 显示名称 * sort_order: * type: integer * default: 0 * responses: * 201: * description: 规格名称创建成功 */ router.post('/names', auth, adminAuth, async (req, res) => { try { const { name, display_name, sort_order = 0 } = req.body; if (!name || !display_name) { return res.status(400).json({ success: false, message: '规格名称和显示名称不能为空' }); } const [result] = await getDB().execute( 'INSERT INTO spec_names (name, display_name, sort_order) VALUES (?, ?, ?)', [name, display_name, sort_order] ); res.status(201).json({ success: true, message: '规格名称创建成功', data: { id: result.insertId } }); } catch (error) { if (error.code === 'ER_DUP_ENTRY') { return res.status(400).json({ success: false, message: '规格名称已存在' }); } console.error('创建规格名称失败:', error); res.status(500).json({ success: false, message: '创建规格名称失败' }); } }); /** * @swagger * /specifications/names/{id}: * delete: * summary: 删除规格名称 * tags: [Specifications] * 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 * example: 规格名称删除成功 * 400: * description: 该规格名称下还有规格值,无法删除 * 404: * description: 规格名称不存在 * 500: * description: 服务器错误 */ router.delete('/names/:id', auth, adminAuth, async (req, res) => { try { const { id } = req.params; // 检查规格名称是否存在 const [existingName] = await getDB().execute( 'SELECT id FROM spec_names WHERE id = ?', [id] ); if (existingName.length === 0) { return res.status(404).json({ success: false, message: '规格名称不存在' }); } // 检查该规格名称下是否还有规格值 const [specValues] = await getDB().execute( 'SELECT COUNT(*) as count FROM spec_values WHERE spec_name_id = ?', [id] ); if (specValues[0].count > 0) { return res.status(400).json({ success: false, message: '该规格名称下还有规格值,请先删除所有规格值' }); } // 删除规格名称 await getDB().execute( 'DELETE FROM spec_names WHERE id = ?', [id] ); res.json({ success: true, message: '规格名称删除成功' }); } catch (error) { console.error('删除规格名称失败:', error); res.status(500).json({ success: false, message: '删除规格名称失败' }); } }); /** * @swagger * /specifications/values: * get: * summary: 获取规格值列表 * tags: [Specifications] * parameters: * - in: query * name: spec_name_id * schema: * type: integer * description: 规格名称ID * - in: query * name: status * schema: * type: string * enum: [active, inactive] * description: 状态筛选 * responses: * 200: * description: 成功获取规格值列表 */ router.get('/values', async (req, res) => { try { const { spec_name_id, status = 'active' } = req.query; let query = ` 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 1=1 `; const params = []; if (spec_name_id) { query += ' AND sv.spec_name_id = ?'; params.push(spec_name_id); } if (status) { query += ' AND sv.status = ?'; params.push(status); } query += ' ORDER BY sv.spec_name_id, sv.sort_order, sv.id'; const [specValues] = await getDB().execute(query, params); res.json({ success: true, data: specValues }); } catch (error) { console.error('获取规格值失败:', error); res.status(500).json({ success: false, message: '获取规格值失败' }); } }); /** * @swagger * /specifications/values: * post: * summary: 创建规格值 * tags: [Specifications] * security: * - bearerAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - spec_name_id * - value * - display_value * properties: * spec_name_id: * type: integer * value: * type: string * display_value: * type: string * color_code: * type: string * image_url: * type: string * sort_order: * type: integer * default: 0 * responses: * 201: * description: 规格值创建成功 */ router.post('/values', auth, adminAuth, async (req, res) => { try { const { spec_name_id, value, display_value, color_code, image_url, sort_order = 0 } = req.body; if (!spec_name_id || !value || !display_value) { return res.status(400).json({ success: false, message: '规格名称ID、规格值和显示值不能为空' }); } const [result] = await getDB().execute( `INSERT INTO spec_values (spec_name_id, value, display_value, color_code, image_url, sort_order) VALUES (?, ?, ?, ?, ?, ?)`, [spec_name_id, value, display_value, color_code || null, image_url || null, sort_order] ); res.status(201).json({ success: true, message: '规格值创建成功', data: { id: result.insertId } }); } catch (error) { if (error.code === 'ER_DUP_ENTRY') { return res.status(400).json({ success: false, message: '该规格名称下的规格值已存在' }); } console.error('创建规格值失败:', error); res.status(500).json({ success: false, message: '创建规格值失败' }); } }); /** * @swagger * /specifications/combinations/{productId}: * get: * summary: 获取商品的规格组合 * tags: [Specifications] * parameters: * - in: path * name: productId * required: true * schema: * type: integer * description: 商品ID * - in: query * name: status * schema: * type: string * enum: [active, inactive] * description: 状态筛选 * responses: * 200: * description: 成功获取规格组合 */ router.get('/combinations/:productId', async (req, res) => { try { const { productId } = req.params; const { status = 'active' } = req.query; // 获取商品的规格组合 let query = ` SELECT psc.*, p.name as product_name, p.price as base_price, p.points_price as base_points_price, p.rongdou_price as base_rongdou_price FROM product_spec_combinations psc LEFT JOIN products p ON psc.product_id = p.id WHERE psc.product_id = ? `; const params = [productId]; if (status) { query += ' AND psc.status = ?'; params.push(status); } query += ' ORDER BY psc.combination_key'; const [combinations] = await getDB().execute(query, params); // 为每个组合获取详细的规格值信息 for (let combination of combinations) { let specValueIds; try { // 处理不同的数据格式 if (!combination.spec_values) { specValueIds = []; } else if (typeof combination.spec_values === 'string') { // 如果是字符串,尝试JSON解析,失败则按逗号分隔处理 try { specValueIds = JSON.parse(combination.spec_values); } catch { // 按逗号分隔的字符串处理 specValueIds = combination.spec_values.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)); } } else if (Buffer.isBuffer(combination.spec_values)) { // 如果是Buffer,转换为字符串后处理 const strValue = combination.spec_values.toString(); try { specValueIds = JSON.parse(strValue); } catch { specValueIds = strValue.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)); } } else { // 其他情况,尝试直接使用 specValueIds = Array.isArray(combination.spec_values) ? combination.spec_values : []; } } catch (error) { console.error('解析规格值失败:', combination.spec_values, error); 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 ); combination.spec_details = specDetails; } else { combination.spec_details = []; } // 计算实际价格 combination.actual_price = combination.base_price + (combination.price_adjustment || 0); combination.actual_points_price = combination.base_points_price + (combination.points_adjustment || 0); combination.actual_rongdou_price = combination.base_rongdou_price + (combination.rongdou_adjustment || 0); combination.is_available = combination.stock > 0; } res.json({ success: true, data: combinations }); } catch (error) { console.error('获取规格组合失败:', error); res.status(500).json({ success: false, message: '获取规格组合失败' }); } }); /** * @swagger * /specifications/combinations/{id}: * get: * summary: 获取单个规格组合详情 * tags: [Specifications] * 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 * data: * type: object * properties: * id: * type: integer * product_id: * type: integer * combination_key: * type: string * spec_values: * type: array * items: * type: integer * price_adjustment: * type: integer * points_adjustment: * type: integer * rongdou_adjustment: * type: integer * stock: * type: integer * sku_code: * type: string * barcode: * type: string * weight: * type: number * volume: * type: number * status: * type: string * spec_details: * type: array * items: * type: object * actual_price: * type: number * actual_points_price: * type: number * actual_rongdou_price: * type: number * is_available: * type: boolean * 404: * description: 规格组合不存在 * 500: * description: 服务器错误 * delete: * summary: 删除规格组合 * tags: [Specifications] * 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 * example: 规格组合删除成功 * 404: * description: 规格组合不存在 * 500: * description: 服务器错误 */ router.get('/combinations/:id', async (req, res) => { try { const { id } = req.params; // 获取规格组合详情 const [combinations] = await getDB().execute( `SELECT psc.*, p.name as product_name, p.price as base_price, p.points_price as base_points_price, p.rongdou_price as base_rongdou_price FROM product_spec_combinations psc LEFT JOIN products p ON psc.product_id = p.id WHERE psc.id = ?`, [id] ); if (combinations.length === 0) { return res.status(404).json({ success: false, message: '规格组合不存在' }); } const combination = combinations[0]; // 解析规格值并获取详细信息 let specValueIds; try { if (!combination.spec_values) { specValueIds = []; } else if (typeof combination.spec_values === 'string') { try { specValueIds = JSON.parse(combination.spec_values); } catch { specValueIds = combination.spec_values.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)); } } else if (Buffer.isBuffer(combination.spec_values)) { const strValue = combination.spec_values.toString(); try { specValueIds = JSON.parse(strValue); } catch { specValueIds = strValue.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)); } } else { specValueIds = Array.isArray(combination.spec_values) ? combination.spec_values : []; } } catch (error) { console.error('解析规格值失败:', combination.spec_values, error); 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 ); combination.spec_details = specDetails; } else { combination.spec_details = []; } // 计算实际价格 combination.actual_price = combination.base_price + (combination.price_adjustment || 0); combination.actual_points_price = combination.base_points_price + (combination.points_adjustment || 0); combination.actual_rongdou_price = combination.base_rongdou_price + (combination.rongdou_adjustment || 0); combination.is_available = combination.stock > 0; res.json({ success: true, data: combination }); } catch (error) { console.error('获取规格组合详情失败:', error); res.status(500).json({ success: false, message: '获取规格组合详情失败' }); } }); /** * @swagger * /specifications/combinations/{id}: * delete: * summary: 删除规格组合 * tags: [Specifications] * security: * - bearerAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: integer * description: 规格组合ID * responses: * 200: * description: 删除成功 * 404: * description: 规格组合不存在 */ router.delete('/combinations/:id', auth, adminAuth, async (req, res) => { try { const { id } = req.params; // 检查规格组合是否存在 const [existingCombination] = await getDB().execute( 'SELECT id FROM product_spec_combinations WHERE id = ?', [id] ); if (existingCombination.length === 0) { return res.status(404).json({ success: false, message: '规格组合不存在' }); } // 删除规格组合 await getDB().execute( 'DELETE FROM product_spec_combinations WHERE id = ?', [id] ); res.json({ success: true, message: '规格组合删除成功' }); } catch (error) { console.error('删除规格组合失败:', error); res.status(500).json({ success: false, message: '删除规格组合失败' }); } }); /** * @swagger * /specifications/combinations/{id}: * put: * summary: 更新规格组合 * tags: [Specifications] * security: * - bearerAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: integer * description: 规格组合ID * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * price_adjustment: * type: integer * points_adjustment: * type: integer * rongdou_adjustment: * type: integer * stock: * type: integer * sku_code: * type: string * barcode: * type: string * weight: * type: number * volume: * type: number * status: * type: string * enum: [active, inactive] * responses: * 200: * description: 规格组合更新成功 * 404: * description: 规格组合不存在 */ router.put('/combinations/:id', auth, adminAuth, async (req, res) => { try { const { id } = req.params; const { price_adjustment, points_adjustment, rongdou_adjustment, stock, sku_code, barcode, weight, volume, status } = req.body; // 检查规格组合是否存在 const [existing] = await getDB().execute( 'SELECT id FROM product_spec_combinations WHERE id = ?', [id] ); if (existing.length === 0) { return res.status(404).json({ success: false, message: '规格组合不存在' }); } // 构建更新字段 const updateFields = []; const updateValues = []; 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 (barcode !== undefined) { updateFields.push('barcode = ?'); updateValues.push(barcode); } if (weight !== undefined) { updateFields.push('weight = ?'); updateValues.push(weight); } if (volume !== undefined) { updateFields.push('volume = ?'); updateValues.push(volume); } if (status !== undefined) { updateFields.push('status = ?'); updateValues.push(status); } if (updateFields.length === 0) { return res.status(400).json({ success: false, message: '没有提供要更新的字段' }); } updateFields.push('updated_at = NOW()'); updateValues.push(id); const updateQuery = `UPDATE product_spec_combinations SET ${updateFields.join(', ')} WHERE id = ?`; await getDB().execute(updateQuery, updateValues); res.json({ success: true, message: '规格组合更新成功' }); } catch (error) { console.error('更新规格组合失败:', error); res.status(500).json({ success: false, message: '更新规格组合失败' }); } }); /** * @swagger * /specifications/combinations: * post: * summary: 创建商品规格组合 * tags: [Specifications] * security: * - bearerAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - product_id * - spec_values * properties: * product_id: * type: integer * spec_values: * type: array * items: * type: integer * description: 规格值ID数组 * price_adjustment: * type: integer * default: 0 * points_adjustment: * type: integer * default: 0 * rongdou_adjustment: * type: integer * default: 0 * stock: * type: integer * default: 0 * sku_code: * type: string * barcode: * type: string * weight: * type: number * volume: * type: number * responses: * 201: * description: 规格组合创建成功 */ router.post('/combinations', auth, adminAuth, async (req, res) => { try { const { product_id, spec_values, price_adjustment = 0, points_adjustment = 0, rongdou_adjustment = 0, stock = 0, sku_code, barcode, weight, volume } = req.body; if (!product_id || !spec_values || !Array.isArray(spec_values) || spec_values.length === 0) { return res.status(400).json({ success: false, message: '商品ID和规格值数组不能为空' }); } // 生成组合键 const sortedSpecValues = [...spec_values].sort((a, b) => a - b); const combinationKey = sortedSpecValues.join('-'); const [result] = await getDB().execute( `INSERT INTO product_spec_combinations (product_id, combination_key, spec_values, price_adjustment, points_adjustment, rongdou_adjustment, stock, sku_code, barcode, weight, volume) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ product_id, combinationKey, JSON.stringify(sortedSpecValues), price_adjustment, points_adjustment, rongdou_adjustment, stock, sku_code, barcode, weight, volume ] ); res.status(201).json({ success: true, message: '规格组合创建成功', data: { id: result.insertId, combination_key: combinationKey } }); } catch (error) { if (error.code === 'ER_DUP_ENTRY') { return res.status(400).json({ success: false, message: '该规格组合已存在' }); } console.error('创建规格组合失败:', error); res.status(500).json({ success: false, message: '创建规格组合失败' }); } }); /** * @swagger * /specifications/generate-combinations: * post: * summary: 为商品生成笛卡尔积规格组合 * tags: [Specifications] * security: * - bearerAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - product_id * - spec_name_ids * properties: * product_id: * type: integer * spec_name_ids: * type: array * items: * type: integer * description: 规格名称ID数组 * default_stock: * type: integer * default: 0 * description: 默认库存 * responses: * 201: * description: 规格组合生成成功 */ router.post('/generate-combinations', auth, adminAuth, async (req, res) => { try { const { product_id, spec_name_ids, default_stock = 0 } = req.body; if (!product_id || !spec_name_ids || !Array.isArray(spec_name_ids) || spec_name_ids.length === 0) { return res.status(400).json({ success: false, message: '商品ID和规格名称ID数组不能为空' }); } // 获取每个规格名称下的所有活跃规格值 const specValueGroups = []; for (const specNameId of spec_name_ids) { const [specValues] = await getDB().execute( 'SELECT id FROM spec_values WHERE spec_name_id = ? AND status = "active" ORDER BY sort_order, id', [specNameId] ); if (specValues.length === 0) { return res.status(400).json({ success: false, message: `规格名称ID ${specNameId} 下没有活跃的规格值` }); } specValueGroups.push(specValues.map(sv => sv.id)); } // 生成笛卡尔积 function cartesianProduct(arrays) { return arrays.reduce((acc, curr) => { const result = []; acc.forEach(a => { curr.forEach(c => { result.push([...a, c]); }); }); return result; }, [[]]); } const combinations = cartesianProduct(specValueGroups); // 生成所有组合键 const combinationData = combinations.map(combination => { const sortedCombination = [...combination].sort((a, b) => a - b); const combinationKey = sortedCombination.join('-'); return { combination: sortedCombination, key: combinationKey }; }); // 批量检查已存在的组合 const existingKeys = new Set(); if (combinationData.length > 0) { const keys = combinationData.map(item => item.key); const placeholders = keys.map(() => '?').join(','); const [existingCombinations] = await getDB().execute( `SELECT combination_key FROM product_spec_combinations WHERE product_id = ? AND combination_key IN (${placeholders})`, [product_id, ...keys] ); existingCombinations.forEach(row => { existingKeys.add(row.combination_key); }); } // 过滤出需要插入的新组合 const newCombinations = combinationData.filter(item => !existingKeys.has(item.key)); // 批量插入新的规格组合 let createdCount = 0; const skippedCount = combinationData.length - newCombinations.length; if (newCombinations.length > 0) { // 使用批量插入提高性能 const values = []; const placeholders = []; newCombinations.forEach(item => { values.push( product_id, item.key, JSON.stringify(item.combination), default_stock ); placeholders.push('(?, ?, ?, ?)'); }); const sql = `INSERT INTO product_spec_combinations (product_id, combination_key, spec_values, stock) VALUES ${placeholders.join(', ')}`; const [result] = await getDB().execute(sql, values); createdCount = result.affectedRows; } res.status(201).json({ success: true, message: '规格组合生成完成', data: { total_combinations: combinations.length, created: createdCount, skipped: skippedCount } }); } catch (error) { console.error('生成规格组合失败:', error); res.status(500).json({ success: false, message: '生成规格组合失败' }); } }); module.exports = router;