新增商品及其变体的后端业务逻辑

分类: 科技创建于: 6/17/2025

好的,要实现新增商品及其变体的后端业务逻辑,我们通常会在商品相关的 API 路由中添加一个 POST 方法。这个 POST 方法将接收一个商品及其变体的完整数据结构,并在数据库中执行一系列插入操作,同时确保数据的一致性(通过事务)。

我将修改之前提供的 pages/api/products/index.js 文件,在其内部增加 POST 方法来处理商品创建请求。

pages/api/products/index.js (更新后的代码)

这个更新后的代码将允许您通过 POST 请求创建新商品和它们的变体。它会处理以下逻辑:

  1. 接收商品及变体数据:从请求体中解析商品名称、描述、价格、主图 URL、状态以及所有变体的详细信息。
  2. 事务管理:使用 PostgreSQL 事务,确保商品、商品变体以及变体与属性值的关联操作要么全部成功,要么全部回滚。
  3. 商品创建:将商品信息插入到 Products 表。
  4. 变体创建:遍历每个变体,将其插入到 ProductVariants 表,并关联到刚刚创建的商品。
  5. 属性值处理:对于每个变体关联的属性(例如“颜色”、“尺寸”),它会:
    • 查找对应的属性定义(在 Attributes 表中,我们假设这些属性如“Color”、“Size”是预先定义的,如果不存在将报错)。
    • 查找或创建属性值(在 AttributeValues 表中,如果该属性的特定值如“Red”、“Large”不存在,则会创建它)。
    • 将变体与对应的属性值关联起来(在 ProductVariantAttributeValues 表中)。
  6. 响应:成功创建后返回新商品的 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 接口

  1. 确保您的数据库中有预设的 Attributes:
    在测试之前,请确保您的 Attributes 表中至少有像 ColorSize 这样的属性定义。如果您的数据库中还没有,请运行以下 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; -- 如果已存在同名属性,则不执行任何操作
  2. 启动 Next.js 开发服务器:
    在您的项目根目录下运行:

    1npm run dev
    2# 或者 yarn dev
  3. 发送 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 后端创建新的电商商品及其所有相关的变体和属性。