新增商品及其变体的后端业务逻辑
分类: 科技创建于: 6/17/2025
好的,要实现新增商品及其变体的后端业务逻辑,我们通常会在商品相关的 API 路由中添加一个 POST
方法。这个 POST
方法将接收一个商品及其变体的完整数据结构,并在数据库中执行一系列插入操作,同时确保数据的一致性(通过事务)。
我将修改之前提供的 pages/api/products/index.js
文件,在其内部增加 POST
方法来处理商品创建请求。
pages/api/products/index.js
(更新后的代码)
这个更新后的代码将允许您通过 POST
请求创建新商品和它们的变体。它会处理以下逻辑:
- 接收商品及变体数据:从请求体中解析商品名称、描述、价格、主图 URL、状态以及所有变体的详细信息。
- 事务管理:使用 PostgreSQL 事务,确保商品、商品变体以及变体与属性值的关联操作要么全部成功,要么全部回滚。
- 商品创建:将商品信息插入到
Products
表。 - 变体创建:遍历每个变体,将其插入到
ProductVariants
表,并关联到刚刚创建的商品。 - 属性值处理:对于每个变体关联的属性(例如“颜色”、“尺寸”),它会:
- 查找对应的属性定义(在
Attributes
表中,我们假设这些属性如“Color”、“Size”是预先定义的,如果不存在将报错)。 - 查找或创建属性值(在
AttributeValues
表中,如果该属性的特定值如“Red”、“Large”不存在,则会创建它)。 - 将变体与对应的属性值关联起来(在
ProductVariantAttributeValues
表中)。
- 查找对应的属性定义(在
- 响应:成功创建后返回新商品的 ID 和相关信息。
1// pages/api/products/index.js 2import db from '../../../lib/db'; 3 4export default async function handler(req, res) { 5 if (req.method === 'GET') { 6 // 现有 GET 逻辑,用于获取所有商品 7 try { 8 const products = await db.query(` 9 SELECT 10 p.id, 11 p.name, 12 p.description, 13 p.base_price, 14 p.main_image_url, 15 p.status, 16 p.created_at, 17 p.updated_at, 18 COALESCE( 19 json_agg( 20 json_build_object( 21 'id', pv.id, 22 'sku_code', pv.sku_code, 23 'price', pv.price, 24 'stock_quantity', pv.stock_quantity, 25 'image_url', pv.image_url, 26 'is_default', pv.is_default, 27 'status', pv.status, 28 'attributes', ( 29 SELECT 30 COALESCE( 31 json_agg( 32 json_build_object( 33 'attribute_id', a.id, -- 包含 attribute_id 方便前端使用 34 'attribute_name', a.name, 35 'attribute_value_id', av.id, -- 包含 attribute_value_id 36 'value', av.value, 37 'display_value', av.display_value, 38 'meta', av.meta 39 ) ORDER BY a.display_order 40 ) FILTER (WHERE a.id IS NOT NULL), '[]'::json 41 ) 42 FROM 43 ProductVariantAttributeValues pvav_sub 44 JOIN 45 AttributeValues av ON pvav_sub.attribute_value_id = av.id 46 JOIN 47 Attributes a ON av.attribute_id = a.id 48 WHERE 49 pvav_sub.variant_id = pv.id 50 ) 51 ) ORDER BY pv.is_default DESC, pv.sku_code 52 ) FILTER (WHERE pv.id IS NOT NULL), '[]'::json 53 ) AS variants 54 FROM 55 Products p 56 LEFT JOIN 57 ProductVariants pv ON p.id = pv.product_id 58 WHERE 59 p.status = 'active' -- 只获取激活的商品 60 GROUP BY 61 p.id 62 ORDER BY 63 p.created_at DESC; 64 `); 65 66 res.status(200).json(products.rows); 67 } catch (error) { 68 console.error('Error fetching products:', error); 69 res.status(500).json({ message: 'Internal server error' }); 70 } 71 } else if (req.method === 'POST') { 72 // 新增 POST 逻辑,用于创建商品及其变体 73 const { name, description, base_price, main_image_url, status, variants } = req.body; 74 75 // 基本输入校验 76 if (!name || typeof base_price === 'undefined' || !variants || !Array.isArray(variants) || variants.length === 0) { 77 return res.status(400).json({ message: 'Product name, base price, and at least one variant are required.' }); 78 } 79 80 const client = await db.connect(); // 从连接池获取一个客户端,用于事务 81 82 try { 83 await client.query('BEGIN'); // 开始事务 84 85 // 1. 插入商品到 Products 表 86 const productResult = await client.query( 87 `INSERT INTO Products (name, description, base_price, main_image_url, status) 88 VALUES ($1, $2, $3, $4, $5) RETURNING id, created_at;`, 89 [name, description || null, base_price, main_image_url || null, status || 'active'] // 允许 description, main_image_url 为空,status 默认为 'active' 90 ); 91 const productId = productResult.rows[0].id; 92 93 const createdVariants = []; // 用于存储创建成功的变体信息,以便响应 94 95 // 2. 遍历并插入每个变体 96 for (const variant of variants) { 97 const { sku_code, price, stock_quantity, image_url, is_default, attributes } = variant; 98 99 // 变体数据校验 100 if (!sku_code || typeof price === 'undefined' || typeof stock_quantity === 'undefined') { 101 throw new Error('Each variant must have sku_code, price, and stock_quantity.'); 102 } 103 104 // 插入到 ProductVariants 表 105 const variantResult = await client.query( 106 `INSERT INTO ProductVariants (product_id, sku_code, price, stock_quantity, image_url, is_default, status) 107 VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id;`, 108 [productId, sku_code, price, stock_quantity, image_url || null, is_default || false, 'active'] // 变体状态默认为 'active' 109 ); 110 const variantId = variantResult.rows[0].id; 111 const variantAttributesSnapshot = []; // 用于存储变体属性快照,以便响应 112 113 // 3. 遍历并处理变体的属性 114 if (attributes && Array.isArray(attributes)) { 115 for (const attr of attributes) { 116 const { attribute_name, value, display_value, meta } = attr; 117 118 if (!attribute_name || typeof value === 'undefined') { 119 throw new Error('Each attribute must have attribute_name and value.'); 120 } 121 122 // 查找 Attributes 表中的 attribute_id 123 const attributeResult = await client.query( 124 `SELECT id FROM Attributes WHERE name = $1;`, 125 [attribute_name] 126 ); 127 128 let attributeId; 129 if (attributeResult.rows.length === 0) { 130 // 如果属性名不存在,抛出错误。通常 Attributes 表中的属性是预定义的。 131 throw new Error(`Attribute name "${attribute_name}" does not exist. Please create it in the Attributes table first.`); 132 // 如果允许自动创建属性,则可以在这里插入新属性: 133 // const newAttr = await client.query( 134 // `INSERT INTO Attributes (name, type) VALUES ($1, $2) RETURNING id;`, 135 // [attribute_name, 'text'] // 默认类型,可以考虑在输入中暴露类型 136 // ); 137 // attributeId = newAttr.rows[0].id; 138 } else { 139 attributeId = attributeResult.rows[0].id; 140 } 141 142 // 查找或创建 AttributeValue 143 const attributeValueResult = await client.query( 144 `SELECT id FROM AttributeValues WHERE attribute_id = $1 AND value = $2;`, 145 [attributeId, value] 146 ); 147 148 let attributeValueId; 149 if (attributeValueResult.rows.length === 0) { 150 // 如果属性值不存在,则创建新的 AttributeValue 151 const newAttributeValue = await client.query( 152 `INSERT INTO AttributeValues (attribute_id, value, display_value, meta) 153 VALUES ($1, $2, $3, $4) RETURNING id;`, 154 [attributeId, value, display_value || value, meta || '{}'] // display_value 默认为 value,meta 默认为空 JSON 155 ); 156 attributeValueId = newAttributeValue.rows[0].id; 157 } else { 158 // 如果属性值已存在,则使用现有的 159 attributeValueId = attributeValueResult.rows[0].id; 160 } 161 162 // 插入到 ProductVariantAttributeValues 关联表 163 await client.query( 164 `INSERT INTO ProductVariantAttributeValues (variant_id, attribute_value_id) 165 VALUES ($1, $2);`, 166 [variantId, attributeValueId] 167 ); 168 169 variantAttributesSnapshot.push({ 170 attribute_name, 171 value, 172 display_value: display_value || value, 173 meta: meta || {} 174 }); 175 } 176 } 177 // 将创建的变体信息添加到列表中,用于响应 178 createdVariants.push({ 179 id: variantId, 180 sku_code: sku_code, 181 price: price, 182 stock_quantity: stock_quantity, 183 image_url: image_url, 184 is_default: is_default, 185 attributes: variantAttributesSnapshot // 附加属性快照 186 }); 187 } 188 189 await client.query('COMMIT'); // 提交事务 190 191 res.status(201).json({ 192 message: 'Product and variants created successfully', 193 productId: productId, 194 productName: name, 195 basePrice: base_price, 196 mainImageUrl: main_image_url, 197 createdAt: productResult.rows[0].created_at, 198 variants: createdVariants // 返回创建的变体信息 199 }); 200 201 } catch (error) { 202 await client.query('ROLLBACK'); // 出现错误时回滚事务 203 console.error('Error creating product and variants:', error); 204 res.status(400).json({ message: error.message || 'Failed to create product and variants.' }); 205 } finally { 206 client.release(); // 释放客户端回连接池 207 } 208 209 } else { 210 res.setHeader('Allow', ['GET', 'POST']); 211 res.status(405).end(`Method ${req.method} Not Allowed`); 212 } 213}
如何测试新的 POST
接口
-
确保您的数据库中有预设的
Attributes
:
在测试之前,请确保您的Attributes
表中至少有像Color
和Size
这样的属性定义。如果您的数据库中还没有,请运行以下 SQL:1-- 插入或确认 Attributes 表中的数据 2INSERT INTO Attributes (id, name, type, display_order) VALUES 3(uuid_generate_v4(), 'Color', 'text', 1), 4(uuid_generate_v4(), 'Size', 'text', 2) 5ON CONFLICT (name) DO NOTHING; -- 如果已存在同名属性,则不执行任何操作
-
启动 Next.js 开发服务器:
在您的项目根目录下运行:1npm run dev 2# 或者 yarn dev
-
发送 POST 请求:
您可以使用 Postman、Insomnia 或curl
等工具向http://localhost:3000/api/products
发送一个POST
请求。请求头 (Headers):
Content-Type: application/json
请求体 (Body - JSON 格式):
1{ 2 "name": "Luxury Leather Wallet", 3 "description": "High-quality leather wallet with multiple card slots.", 4 "base_price": 79.99, 5 "main_image_url": "http://example.com/wallet_main.jpg", 6 "status": "active", 7 "variants": [ 8 { 9 "sku_code": "WALLET-BLK-STD", 10 "price": 85.00, 11 "stock_quantity": 30, 12 "image_url": "http://example.com/wallet_black.jpg", 13 "is_default": true, 14 "attributes": [ 15 { "attribute_name": "Color", "value": "Black", "display_value": "黑色", "meta": { "hex": "#000000" } } 16 ] 17 }, 18 { 19 "sku_code": "WALLET-BRN-STD", 20 "price": 82.00, 21 "stock_quantity": 20, 22 "image_url": "http://example.com/wallet_brown.jpg", 23 "is_default": false, 24 "attributes": [ 25 { "attribute_name": "Color", "value": "Brown", "display_value": "棕色", "meta": { "hex": "#A52A2A" } } 26 ] 27 } 28 ] 29}
如果请求成功,您将收到
201 Created
状态码以及一个包含新创建商品 ID 和变体信息的 JSON 响应。
如果数据不符合要求(例如,缺少必要字段,或者 SKU 重复,或者attribute_name
不存在),您将收到400 Bad Request
状态码和相应的错误消息。
这个 POST
接口提供了一个全面的解决方案,用于在您的 Next.js 后端创建新的电商商品及其所有相关的变体和属性。