([]);
useEffect(() => {
if (!open) return;
@@ -977,14 +990,14 @@ function ProductFormDialog({
setProductImageUrl(editing.productImageUrl ?? "");
setState(editing.state !== false);
const lids = locationMap.get(editing.id) ?? [];
- setLocationId(lids[0] ?? "");
+ setLocationIds([...new Set(lids.filter(Boolean))]);
} else {
setProductCode("");
setProductName("");
setCategoryId("");
setProductImageUrl("");
setState(true);
- setLocationId("");
+ setLocationIds([]);
}
}, [open, editing, locationMap]);
@@ -993,8 +1006,9 @@ function ProductFormDialog({
toast.error("Validation", { description: "Product code and name are required." });
return;
}
- if (!locationId.trim()) {
- toast.error("Validation", { description: "Select a store to bind this product." });
+ const storeIds = [...new Set(locationIds.map((x) => x.trim()).filter(Boolean))];
+ if (storeIds.length === 0) {
+ toast.error("Validation", { description: "Select at least one store to bind this product." });
return;
}
@@ -1011,10 +1025,14 @@ function ProductFormDialog({
if (editing) {
await updateProduct(editing.id, body as ProductUpdateInput);
const prev = locationMap.get(editing.id) ?? [];
- await syncProductStoreBinding(editing.id, locationId.trim(), prev);
+ await syncProductStoreBinding(editing.id, storeIds, prev);
} else {
const created = await createProduct(body);
- await createProductLocation({ locationId: locationId.trim(), productIds: [created.id] });
+ await Promise.all(
+ storeIds.map((locId) =>
+ createProductLocation({ locationId: locId, productIds: [created.id] }),
+ ),
+ );
}
toast.success(editing ? "Product updated." : "Product created.");
onSaved();
@@ -1033,7 +1051,9 @@ function ProductFormDialog({
{editing ? "Edit Product" : "Add New Product"}
- {editing ? "Update product and store binding." : "Create a product and bind it to a store."}
+ {editing
+ ? "Update product and store bindings (one row per product–store pair)."
+ : "Create a product and bind it to one or more stores (one row per pair)."}
@@ -1080,12 +1100,12 @@ function ProductFormDialog({
/>
-
- Bind to store(s) *
+
diff --git a/项目相关文档/本次新增与优化接口汇总.md b/项目相关文档/本次新增与优化接口汇总.md
index 5d7b960..fece249 100644
--- a/项目相关文档/本次新增与优化接口汇总.md
+++ b/项目相关文档/本次新增与优化接口汇总.md
@@ -7,6 +7,7 @@
> - App `labeling-tree`:L1 标签分类返回 `buttonAppearance`
> - Web 管理端 `auth-session`:**当前用户菜单与权限**、**退出登录**(食品标签-美国版模块)
> - Web `rbac-menu` 列表/详情:补充返回 `routerName`、`router`
+> - 产品 **Products**:`POST/PUT /api/app/product` 支持可选 Body 字段 **`locationIds`**(多门店批量绑定 / 编辑时整表替换关联),详见 `项目相关文档/标签模块接口对接说明.md` **§6**
>
> 其余标签打印相关接口不在本文范围内。
diff --git a/项目相关文档/标签模块接口对接说明.md b/项目相关文档/标签模块接口对接说明.md
index 9c36c2f..6cb7c8d 100644
--- a/项目相关文档/标签模块接口对接说明.md
+++ b/项目相关文档/标签模块接口对接说明.md
@@ -574,6 +574,10 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
入参:
- `id`:产品Id(`fl_product.Id`)
+返回(`ProductGetOutputDto`,与实现一致的主要字段):
+- `id`、`productCode`、`productName`、`categoryId`、`categoryName`、`productImageUrl`、`state`
+- **`locationIds`**:`string[]`,该产品在 **`fl_location_product`** 中绑定的门店 Id(去重);无关联时为空数组
+
### 6.3 新增产品
方法:`POST /api/app/product`
@@ -583,15 +587,30 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
{
"productCode": "PRD_TEST_001",
"productName": "Chicken",
- "categoryName": "Meat",
+ "categoryId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
"productImageUrl": "https://example.com/img.png",
- "state": true
+ "state": true,
+ "locationIds": [
+ "11111111-1111-1111-1111-111111111111",
+ "22222222-2222-2222-2222-222222222222"
+ ]
}
```
+字段说明:
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `productCode` | string | 是 | 产品编码 |
+| `productName` | string | 是 | 产品名称 |
+| `categoryId` | string \| null | 否 | 产品分类 Id(`fl_product_category.id`) |
+| `productImageUrl` | string \| null | 否 | 主图 URL |
+| `state` | bool | 否 | 默认 `true` |
+| **`locationIds`** | `string[]` \| **省略** | 否 | **可选。** 有该字段时:在同一事务内按列表批量写入 **`fl_location_product`**(**每个门店 Id 一行**,即「一产品一门店一条关联」)。**请求体中省略该字段**时:本接口不写门店关联,仍可通过 **§7 Product-Location** 维护。传空数组 `[]` 表示新建产品后不绑定任何门店。 |
+
校验:
-- `productCode/productName` 不能为空
+- `productCode` / `productName` 不能为空
- `productCode` 不能与未删除的数据重复
+- 若传入 **`locationIds`** 且含非空项:每个 Id 须为合法 Guid,且对应门店存在于 **`Location`** 主数据且未删除;否则返回友好错误(如「门店Id格式不正确」「门店不存在」)
### 6.4 编辑产品
@@ -599,7 +618,13 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
入参:
- Path:`id` 为当前产品Id(`fl_product.Id`)
-- Body:字段同新增(`ProductUpdateInputVo`)
+- Body:字段同新增(`ProductUpdateInputVo`,继承 `ProductCreateInputVo`)
+
+**`locationIds` 行为(与新增不同,请注意):**
+- **请求体中省略 `locationIds` 属性**:不修改 **`fl_location_product`**(仅更新 `fl_product` 主表字段;兼容原「先 PUT 产品再调 §7 同步门店」的调用方式)。
+- **请求体中包含 `locationIds` 属性**(含空数组 `[]`):对该产品的门店关联做 **整表替换**——先删除本产品下全部 **`fl_location_product`** 行,再按列表逐条插入;`[]` 表示解除该产品与所有门店的关联。
+
+其它校验同 **§6.3**(含门店存在性校验,当 `locationIds` 含非空项时)。
### 6.5 删除(逻辑删除)
@@ -613,6 +638,7 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
说明:
- 关联表:`fl_location_product`
+- 也可在 **§6.3 / §6.4** 通过产品 Body 的 **`locationIds`** 一次性维护本产品在各门店的关联(与 §7 写入同一张表);二者可并存,按需选择调用方式。
- 关联按门店进行批量替换:
- `Create`:在门店下新增未存在的 product 关联
- `Update`:替换该门店下全部关联(先删后建)