Files
jurong_circle_shopping_black/routes/specifications.js

1096 lines
32 KiB
JavaScript
Raw Normal View History

2025-09-24 10:02:03 +08:00
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;