Files
jurong_circle_black/routes/specifications.js
2025-09-02 09:29:20 +08:00

1096 lines
32 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;