diff --git a/label-template-template-1773988794039 (1).json b/label-template-template-1773999322132.json
index bbbacba..ad57772 100644
--- a/label-template-template-1773988794039 (1).json
+++ b/label-template-template-1773999322132.json
@@ -1,33 +1,19 @@
{
- "id": "template-1773988794039",
+ "id": "template-1773999322132",
"name": "未命名模板",
"labelType": "PRICE",
- "unit": "inch",
- "width": 4,
- "height": 6,
+ "unit": "cm",
+ "width": 21,
+ "height": 29.7,
"appliedLocation": "ALL",
"showRuler": true,
"showGrid": true,
"elements": [
{
- "id": "el-1773989351080-vqc03nr",
- "type": "IMAGE",
- "x": 32,
- "y": 24,
- "width": 60,
- "height": 60,
- "rotation": "horizontal",
- "border": "none",
- "config": {
- "src": "",
- "scaleMode": "contain"
- }
- },
- {
- "id": "el-1773989452538-0ejrxoe",
+ "id": "el-1773999335969-ugvoy6v",
"type": "TEXT_STATIC",
- "x": 32,
- "y": 104,
+ "x": 392,
+ "y": 48,
"width": 120,
"height": 24,
"rotation": "horizontal",
@@ -41,10 +27,10 @@
}
},
{
- "id": "el-1773989466493-ibbroio",
+ "id": "el-1773999339450-cc9y2lv",
"type": "QRCODE",
- "x": 32,
- "y": 136,
+ "x": 200,
+ "y": 24,
"width": 80,
"height": 80,
"rotation": "horizontal",
@@ -55,10 +41,10 @@
}
},
{
- "id": "el-1773989469008-f1l39qj",
+ "id": "el-1773999341556-c795unj",
"type": "BARCODE",
- "x": 0,
- "y": 224,
+ "x": 8,
+ "y": 48,
"width": 160,
"height": 48,
"rotation": "horizontal",
@@ -71,20 +57,20 @@
}
},
{
- "id": "el-1773989473436-j7fdeh2",
+ "id": "el-1773999346493-onxkjxn",
"type": "BLANK",
- "x": 32,
- "y": 288,
- "width": 48,
- "height": 32,
+ "x": 40,
+ "y": 120,
+ "width": 40,
+ "height": 24,
"rotation": "horizontal",
"border": "none",
"config": {}
},
{
- "id": "el-1773989483341-ifwcyjj",
+ "id": "el-1773999351836-igrbrib",
"type": "TEXT_PRICE",
- "x": 152,
+ "x": 520,
"y": 24,
"width": 80,
"height": 24,
@@ -101,10 +87,24 @@
}
},
{
- "id": "el-1773989498031-e4d61j8",
+ "id": "el-1773999357328-0jof4kh",
+ "type": "IMAGE",
+ "x": 480,
+ "y": 96,
+ "width": 60,
+ "height": 60,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "src": "",
+ "scaleMode": "contain"
+ }
+ },
+ {
+ "id": "el-1773999360132-bwle4nb",
"type": "IMAGE",
- "x": 192,
- "y": 56,
+ "x": 152,
+ "y": 120,
"width": 60,
"height": 60,
"rotation": "horizontal",
@@ -115,10 +115,10 @@
}
},
{
- "id": "el-1773989505076-1lxccx7",
+ "id": "el-1773999364492-yxs89ov",
"type": "TEXT_PRODUCT",
- "x": 200,
- "y": 136,
+ "x": 272,
+ "y": 120,
"width": 120,
"height": 24,
"rotation": "horizontal",
@@ -132,10 +132,10 @@
}
},
{
- "id": "el-1773989509805-ax3392v",
+ "id": "el-1773999367121-3uesy24",
"type": "TEXT_STATIC",
- "x": 192,
- "y": 160,
+ "x": 48,
+ "y": 216,
"width": 120,
"height": 24,
"rotation": "horizontal",
@@ -149,10 +149,10 @@
}
},
{
- "id": "el-1773989512993-xt8bg7q",
+ "id": "el-1773999369332-fib58lw",
"type": "QRCODE",
- "x": 184,
- "y": 184,
+ "x": 160,
+ "y": 208,
"width": 80,
"height": 80,
"rotation": "horizontal",
@@ -163,10 +163,10 @@
}
},
{
- "id": "el-1773989525383-eji8p2s",
+ "id": "el-1773999371501-p1ot3id",
"type": "BARCODE",
- "x": 0,
- "y": 288,
+ "x": 280,
+ "y": 224,
"width": 160,
"height": 48,
"rotation": "horizontal",
@@ -179,10 +179,10 @@
}
},
{
- "id": "el-1773989540159-dr2avdf",
+ "id": "el-1773999374282-ux37cow",
"type": "NUTRITION",
- "x": 184,
- "y": 280,
+ "x": 480,
+ "y": 200,
"width": 200,
"height": 120,
"rotation": "horizontal",
@@ -196,10 +196,10 @@
}
},
{
- "id": "el-1773989549679-mcxrdnw",
+ "id": "el-1773999377353-n90ved8",
"type": "TEXT_PRICE",
- "x": 24,
- "y": 352,
+ "x": 664,
+ "y": 152,
"width": 80,
"height": 24,
"rotation": "horizontal",
@@ -213,6 +213,113 @@
"fontWeight": "bold",
"textAlign": "right"
}
+ },
+ {
+ "id": "el-1773999389265-sukmurs",
+ "type": "DATE",
+ "x": 40,
+ "y": 320,
+ "width": 120,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "format": "YYYY-MM-DD",
+ "offsetDays": 0
+ }
+ },
+ {
+ "id": "el-1773999392135-3ub707o",
+ "type": "TIME",
+ "x": 144,
+ "y": 320,
+ "width": 100,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "format": "HH:mm",
+ "offsetDays": 0
+ }
+ },
+ {
+ "id": "el-1773999394947-3y8li6m",
+ "type": "DURATION",
+ "x": 272,
+ "y": 320,
+ "width": 120,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "format": "YYYY-MM-DD",
+ "offsetDays": 3
+ }
+ },
+ {
+ "id": "el-1773999399456-nbglmfa",
+ "type": "IMAGE",
+ "x": 464,
+ "y": 304,
+ "width": 60,
+ "height": 60,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "src": "",
+ "scaleMode": "contain"
+ }
+ },
+ {
+ "id": "el-1773999401976-wzmsx8f",
+ "type": "TEXT_STATIC",
+ "x": 600,
+ "y": 320,
+ "width": 120,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "text": "文本",
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "normal",
+ "textAlign": "left"
+ }
+ },
+ {
+ "id": "el-1773999407826-91if0px",
+ "type": "TEXT_STATIC",
+ "x": 40,
+ "y": 368,
+ "width": 120,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "text": "文本",
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "normal",
+ "textAlign": "left"
+ }
+ },
+ {
+ "id": "el-1773999417908-lje3b3w",
+ "type": "TEXT_STATIC",
+ "x": 200,
+ "y": 368,
+ "width": 120,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "text": "文本",
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "normal",
+ "textAlign": "left"
+ }
}
]
}
\ No newline at end of file
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelPreviewResolveInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelPreviewResolveInputVo.cs
new file mode 100644
index 0000000..bc1128a
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelPreviewResolveInputVo.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+namespace FoodLabeling.Application.Contracts.Dtos.Label;
+
+public class LabelPreviewResolveInputVo
+{
+ ///
+ /// 标签编码(fl_label.LabelCode)
+ ///
+ public string LabelCode { get; set; } = string.Empty;
+
+ ///
+ /// 选择用于预览的产品Id(fl_product.Id)
+ /// 如果不传,默认取该标签绑定的第一个产品
+ ///
+ public string? ProductId { get; set; }
+
+ ///
+ /// 打印输入(前端传,用于 PRINT_INPUT 元素)
+ /// key 建议使用模板元素的 InputKey
+ ///
+ public Dictionary? PrintInputJson { get; set; }
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelTemplatePreviewDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelTemplatePreviewDto.cs
new file mode 100644
index 0000000..52577cb
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelTemplatePreviewDto.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using FoodLabeling.Application.Contracts.Dtos.LabelTemplate;
+
+namespace FoodLabeling.Application.Contracts.Dtos.Label;
+
+///
+/// 预览输出:与前端 LabelCanvas/LabelPreviewOnly 的 LabelTemplate 结构尽量一致
+///
+public class LabelTemplatePreviewDto
+{
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ [JsonPropertyName("labelType")]
+ public string LabelType { get; set; } = string.Empty;
+
+ [JsonPropertyName("unit")]
+ public string Unit { get; set; } = string.Empty;
+
+ [JsonPropertyName("width")]
+ public decimal Width { get; set; }
+
+ [JsonPropertyName("height")]
+ public decimal Height { get; set; }
+
+ [JsonPropertyName("appliedLocation")]
+ public string AppliedLocation { get; set; } = "ALL";
+
+ [JsonPropertyName("showRuler")]
+ public bool ShowRuler { get; set; }
+
+ [JsonPropertyName("showGrid")]
+ public bool ShowGrid { get; set; }
+
+ [JsonPropertyName("elements")]
+ public List Elements { get; set; } = new();
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs
new file mode 100644
index 0000000..2f5c40c
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs
@@ -0,0 +1,15 @@
+namespace FoodLabeling.Application.Contracts.Dtos.Product;
+
+public class ProductCreateInputVo
+{
+ public string ProductCode { get; set; } = string.Empty;
+
+ public string ProductName { get; set; } = string.Empty;
+
+ public string? CategoryName { get; set; }
+
+ public string? ProductImageUrl { get; set; }
+
+ public bool State { get; set; } = true;
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetListInputVo.cs
new file mode 100644
index 0000000..b6e26fb
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetListInputVo.cs
@@ -0,0 +1,21 @@
+using FoodLabeling.Application.Contracts.Dtos.Common;
+using Volo.Abp.Application.Dtos;
+
+namespace FoodLabeling.Application.Contracts.Dtos.Product;
+
+///
+/// 产品分页查询入参
+///
+public class ProductGetListInputVo : PagedAndSortedResultRequestDto
+{
+ ///
+ /// 模糊搜索(ProductCode/ProductName/CategoryName)
+ ///
+ public string? Keyword { get; set; }
+
+ ///
+ /// 启用状态
+ ///
+ public bool? State { get; set; }
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetListOutputDto.cs
new file mode 100644
index 0000000..f172144
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetListOutputDto.cs
@@ -0,0 +1,22 @@
+namespace FoodLabeling.Application.Contracts.Dtos.Product;
+
+public class ProductGetListOutputDto
+{
+ public string Id { get; set; } = string.Empty;
+
+ public string ProductCode { get; set; } = string.Empty;
+
+ public string ProductName { get; set; } = string.Empty;
+
+ public string? CategoryName { get; set; }
+
+ public string? ProductImageUrl { get; set; }
+
+ public bool State { get; set; }
+
+ ///
+ /// 该产品关联的标签数量(fl_label_product + fl_label)
+ ///
+ public long NoOfLabels { get; set; }
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetOutputDto.cs
new file mode 100644
index 0000000..c307077
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetOutputDto.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+namespace FoodLabeling.Application.Contracts.Dtos.Product;
+
+public class ProductGetOutputDto
+{
+ public string Id { get; set; } = string.Empty;
+
+ public string ProductCode { get; set; } = string.Empty;
+
+ public string ProductName { get; set; } = string.Empty;
+
+ public string? CategoryName { get; set; }
+
+ public string? ProductImageUrl { get; set; }
+
+ public bool State { get; set; }
+
+ ///
+ /// 该产品关联的门店Id列表(来自 fl_location_product)
+ ///
+ public List LocationIds { get; set; } = new();
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductUpdateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductUpdateInputVo.cs
new file mode 100644
index 0000000..94b8a58
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductUpdateInputVo.cs
@@ -0,0 +1,6 @@
+namespace FoodLabeling.Application.Contracts.Dtos.Product;
+
+public class ProductUpdateInputVo : ProductCreateInputVo
+{
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationCreateInputVo.cs
new file mode 100644
index 0000000..52b68b0
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationCreateInputVo.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace FoodLabeling.Application.Contracts.Dtos.ProductLocation;
+
+public class ProductLocationCreateInputVo
+{
+ ///
+ /// 门店Id
+ ///
+ public string LocationId { get; set; } = string.Empty;
+
+ ///
+ /// 产品Id列表
+ ///
+ public List ProductIds { get; set; } = new();
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationGetListInputVo.cs
new file mode 100644
index 0000000..f0864f1
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationGetListInputVo.cs
@@ -0,0 +1,21 @@
+using FoodLabeling.Application.Contracts.Dtos.Common;
+using Volo.Abp.Application.Dtos;
+
+namespace FoodLabeling.Application.Contracts.Dtos.ProductLocation;
+
+///
+/// 产品-门店分页查询入参
+///
+public class ProductLocationGetListInputVo : PagedAndSortedResultRequestDto
+{
+ ///
+ /// 门店Id(location.Id,string 表示)
+ ///
+ public string? LocationId { get; set; }
+
+ ///
+ /// 产品Id(fl_product.Id,string)
+ ///
+ public string? ProductId { get; set; }
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationGetListOutputDto.cs
new file mode 100644
index 0000000..4f9ad76
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationGetListOutputDto.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace FoodLabeling.Application.Contracts.Dtos.ProductLocation;
+
+public class ProductLocationGetListOutputDto
+{
+ public string Id { get; set; } = string.Empty;
+
+ public string LocationId { get; set; } = string.Empty;
+ public string? LocationCode { get; set; }
+ public string? LocationName { get; set; }
+
+ public string ProductId { get; set; } = string.Empty;
+ public string? ProductCode { get; set; }
+ public string? ProductName { get; set; }
+ public string? ProductImageUrl { get; set; }
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationGetOutputDto.cs
new file mode 100644
index 0000000..c945a01
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationGetOutputDto.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace FoodLabeling.Application.Contracts.Dtos.ProductLocation;
+
+public class ProductLocationGetOutputDto
+{
+ public string LocationId { get; set; } = string.Empty;
+
+ public List Products { get; set; } = new();
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationUpdateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationUpdateInputVo.cs
new file mode 100644
index 0000000..416e356
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationUpdateInputVo.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace FoodLabeling.Application.Contracts.Dtos.ProductLocation;
+
+public class ProductLocationUpdateInputVo
+{
+ ///
+ /// 产品Id列表(将替换当前门店下的全部产品关联)
+ ///
+ public List ProductIds { get; set; } = new();
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs
new file mode 100644
index 0000000..c0f0cbd
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs
@@ -0,0 +1,38 @@
+using FoodLabeling.Application.Contracts.Dtos.Common;
+using FoodLabeling.Application.Contracts.Dtos.Product;
+using Volo.Abp.Application.Dtos;
+using Volo.Abp.Application.Services;
+
+namespace FoodLabeling.Application.Contracts.IServices;
+
+///
+/// 产品管理接口(Products,fl_product)
+///
+public interface IProductAppService : IApplicationService
+{
+ ///
+ /// 产品分页列表
+ ///
+ Task> GetListAsync(ProductGetListInputVo input);
+
+ ///
+ /// 产品详情
+ ///
+ Task GetAsync(string id);
+
+ ///
+ /// 新增产品
+ ///
+ Task CreateAsync(ProductCreateInputVo input);
+
+ ///
+ /// 编辑产品
+ ///
+ Task UpdateAsync(string id, ProductUpdateInputVo input);
+
+ ///
+ /// 删除产品(逻辑删除)
+ ///
+ Task DeleteAsync(string id);
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductLocationAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductLocationAppService.cs
new file mode 100644
index 0000000..cea35f1
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductLocationAppService.cs
@@ -0,0 +1,38 @@
+using FoodLabeling.Application.Contracts.Dtos.Common;
+using FoodLabeling.Application.Contracts.Dtos.ProductLocation;
+using Volo.Abp.Application.Dtos;
+using Volo.Abp.Application.Services;
+
+namespace FoodLabeling.Application.Contracts.IServices;
+
+///
+/// 产品-门店关联管理(fl_location_product)
+///
+public interface IProductLocationAppService : IApplicationService
+{
+ ///
+ /// 关联分页列表(可按门店Id/产品Id过滤)
+ ///
+ Task> GetListAsync(ProductLocationGetListInputVo input);
+
+ ///
+ /// 门店下的全部产品(id=LocationId)
+ ///
+ Task GetAsync(string id);
+
+ ///
+ /// 新增/批量建立门店与产品的关联
+ ///
+ Task CreateAsync(ProductLocationCreateInputVo input);
+
+ ///
+ /// 编辑:替换该门店下的全部产品关联
+ ///
+ Task UpdateAsync(string id, ProductLocationUpdateInputVo input);
+
+ ///
+ /// 删除:删除该门店的全部产品关联
+ ///
+ Task DeleteAsync(string id);
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLocationProductDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLocationProductDbEntity.cs
new file mode 100644
index 0000000..1b2d1ad
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLocationProductDbEntity.cs
@@ -0,0 +1,15 @@
+using SqlSugar;
+
+namespace FoodLabeling.Application.Services.DbModels;
+
+[SugarTable("fl_location_product")]
+public class FlLocationProductDbEntity
+{
+ [SugarColumn(IsPrimaryKey = true)]
+ public string Id { get; set; } = string.Empty;
+
+ public string LocationId { get; set; } = string.Empty;
+
+ public string ProductId { get; set; } = string.Empty;
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs
new file mode 100644
index 0000000..3dd5abe
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs
@@ -0,0 +1,222 @@
+using FoodLabeling.Application.Contracts.Dtos.Common;
+using FoodLabeling.Application.Contracts.Dtos.Product;
+using FoodLabeling.Application.Contracts.IServices;
+using FoodLabeling.Application.Services.DbModels;
+using SqlSugar;
+using Volo.Abp;
+using Volo.Abp.Application.Services;
+using Volo.Abp.Guids;
+using Volo.Abp.Uow;
+using Yi.Framework.SqlSugarCore.Abstractions;
+
+namespace FoodLabeling.Application.Services;
+
+///
+/// 产品管理(Products)
+///
+public class ProductAppService : ApplicationService, IProductAppService
+{
+ private readonly ISqlSugarDbContext _dbContext;
+ private readonly IGuidGenerator _guidGenerator;
+
+ public ProductAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator)
+ {
+ _dbContext = dbContext;
+ _guidGenerator = guidGenerator;
+ }
+
+ public async Task> GetListAsync(ProductGetListInputVo input)
+ {
+ RefAsync total = 0;
+ var keyword = input.Keyword?.Trim();
+
+ var query = _dbContext.SqlSugarClient.Queryable()
+ .Where(x => !x.IsDeleted)
+ .WhereIF(!string.IsNullOrWhiteSpace(keyword), x =>
+ x.ProductCode.Contains(keyword!) ||
+ x.ProductName.Contains(keyword!) ||
+ (x.CategoryName != null && x.CategoryName.Contains(keyword!)))
+ .WhereIF(input.State != null, x => x.State == input.State);
+
+ if (!string.IsNullOrWhiteSpace(input.Sorting))
+ {
+ query = query.OrderBy(input.Sorting);
+ }
+ else
+ {
+ query = query.OrderByDescending(x => x.ProductName);
+ }
+
+ var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
+ var ids = entities.Select(x => x.Id).ToList();
+
+ // Count labels for each product
+ var countMap = new Dictionary();
+ if (ids.Count > 0)
+ {
+ var countRows = await _dbContext.SqlSugarClient.Queryable(
+ (lp, l) => lp.LabelId == l.Id)
+ .Where((lp, l) => !l.IsDeleted && ids.Contains(lp.ProductId))
+ .GroupBy((lp, l) => lp.ProductId)
+ .Select((lp, l) => new { ProductId = lp.ProductId, Count = SqlFunc.AggregateCount(lp.Id) })
+ .ToListAsync();
+
+ countMap = countRows.ToDictionary(x => x.ProductId, x => (long)x.Count);
+ }
+
+ var items = entities.Select(x => new ProductGetListOutputDto
+ {
+ Id = x.Id,
+ ProductCode = x.ProductCode,
+ ProductName = x.ProductName,
+ CategoryName = x.CategoryName,
+ ProductImageUrl = x.ProductImageUrl,
+ State = x.State,
+ NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0
+ }).ToList();
+
+ return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items);
+ }
+
+ public async Task GetAsync(string id)
+ {
+ var productId = id?.Trim();
+ if (string.IsNullOrWhiteSpace(productId))
+ {
+ throw new UserFriendlyException("产品Id不能为空");
+ }
+
+ var entity = await _dbContext.SqlSugarClient.Queryable()
+ .FirstAsync(x => !x.IsDeleted && x.Id == productId);
+
+ if (entity is null)
+ {
+ throw new UserFriendlyException("产品不存在");
+ }
+
+ var locationIds = await _dbContext.SqlSugarClient.Queryable()
+ .Where(x => x.ProductId == productId)
+ .Select(x => x.LocationId)
+ .Distinct()
+ .ToListAsync();
+
+ return new ProductGetOutputDto
+ {
+ Id = entity.Id,
+ ProductCode = entity.ProductCode,
+ ProductName = entity.ProductName,
+ CategoryName = entity.CategoryName,
+ ProductImageUrl = entity.ProductImageUrl,
+ State = entity.State,
+ LocationIds = locationIds
+ };
+ }
+
+ [UnitOfWork]
+ public async Task CreateAsync(ProductCreateInputVo input)
+ {
+ var code = input.ProductCode?.Trim();
+ var name = input.ProductName?.Trim();
+ if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(name))
+ {
+ throw new UserFriendlyException("产品编码和名称不能为空");
+ }
+
+ var duplicated = await _dbContext.SqlSugarClient.Queryable()
+ .AnyAsync(x => !x.IsDeleted && (x.ProductCode == code));
+ if (duplicated)
+ {
+ throw new UserFriendlyException("产品编码已存在");
+ }
+
+ var entity = new FlProductDbEntity
+ {
+ Id = _guidGenerator.Create().ToString(),
+ IsDeleted = false,
+ ProductCode = code,
+ ProductName = name,
+ CategoryName = input.CategoryName?.Trim(),
+ ProductImageUrl = input.ProductImageUrl?.Trim(),
+ State = input.State
+ };
+
+ await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
+ return await GetAsync(entity.Id);
+ }
+
+ [UnitOfWork]
+ public async Task UpdateAsync(string id, ProductUpdateInputVo input)
+ {
+ var productId = id?.Trim();
+ if (string.IsNullOrWhiteSpace(productId))
+ {
+ throw new UserFriendlyException("产品Id不能为空");
+ }
+
+ var entity = await _dbContext.SqlSugarClient.Queryable()
+ .FirstAsync(x => !x.IsDeleted && x.Id == productId);
+ if (entity is null)
+ {
+ throw new UserFriendlyException("产品不存在");
+ }
+
+ var code = input.ProductCode?.Trim();
+ var name = input.ProductName?.Trim();
+ if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(name))
+ {
+ throw new UserFriendlyException("产品编码和名称不能为空");
+ }
+
+ var duplicated = await _dbContext.SqlSugarClient.Queryable()
+ .AnyAsync(x => !x.IsDeleted && x.Id != productId && x.ProductCode == code);
+ if (duplicated)
+ {
+ throw new UserFriendlyException("产品编码已存在");
+ }
+
+ entity.ProductCode = code;
+ entity.ProductName = name;
+ entity.CategoryName = input.CategoryName?.Trim();
+ entity.ProductImageUrl = input.ProductImageUrl?.Trim();
+ entity.State = input.State;
+
+ await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
+ return await GetAsync(productId);
+ }
+
+ [UnitOfWork]
+ public async Task DeleteAsync(string id)
+ {
+ var productId = id?.Trim();
+ if (string.IsNullOrWhiteSpace(productId))
+ {
+ throw new UserFriendlyException("产品Id不能为空");
+ }
+
+ var entity = await _dbContext.SqlSugarClient.Queryable()
+ .FirstAsync(x => !x.IsDeleted && x.Id == productId);
+ if (entity is null)
+ {
+ throw new UserFriendlyException("产品不存在");
+ }
+
+ entity.IsDeleted = true;
+ await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
+ }
+
+ private static PagedResultWithPageDto BuildPagedResult(int skipCount, int maxResultCount, int total, List items)
+ {
+ var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount;
+ var pageIndex = pageSize <= 0 ? 1 : (skipCount / pageSize) + 1;
+ var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize);
+ return new PagedResultWithPageDto
+ {
+ PageIndex = pageIndex,
+ PageSize = pageSize,
+ TotalCount = total,
+ TotalPages = totalPages,
+ Items = items
+ };
+ }
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductLocationAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductLocationAppService.cs
new file mode 100644
index 0000000..786d5b1
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductLocationAppService.cs
@@ -0,0 +1,334 @@
+using FoodLabeling.Application.Contracts.Dtos.Common;
+using FoodLabeling.Application.Contracts.Dtos.ProductLocation;
+using FoodLabeling.Application.Contracts.IServices;
+using FoodLabeling.Application.Services.DbModels;
+using FoodLabeling.Domain.Entities;
+using SqlSugar;
+using Volo.Abp;
+using Volo.Abp.Application.Services;
+using Volo.Abp.Guids;
+using Volo.Abp.Uow;
+using Yi.Framework.SqlSugarCore.Abstractions;
+
+namespace FoodLabeling.Application.Services;
+
+///
+/// 产品-门店关联管理(fl_location_product)
+///
+public class ProductLocationAppService : ApplicationService, IProductLocationAppService
+{
+ private readonly ISqlSugarDbContext _dbContext;
+ private readonly IGuidGenerator _guidGenerator;
+
+ public ProductLocationAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator)
+ {
+ _dbContext = dbContext;
+ _guidGenerator = guidGenerator;
+ }
+
+ public async Task> GetListAsync(ProductLocationGetListInputVo input)
+ {
+ RefAsync total = 0;
+
+ var locationId = input.LocationId?.Trim();
+ var productId = input.ProductId?.Trim();
+
+ var query = _dbContext.SqlSugarClient
+ .Queryable((lp, p) => lp.ProductId == p.Id)
+ .Where((lp, p) => p.IsDeleted == false);
+
+ if (!string.IsNullOrWhiteSpace(locationId))
+ {
+ query = query.Where((lp, p) => lp.LocationId == locationId);
+ }
+
+ if (!string.IsNullOrWhiteSpace(productId))
+ {
+ query = query.Where((lp, p) => lp.ProductId == productId);
+ }
+
+ // 默认排序
+ query = string.IsNullOrWhiteSpace(input.Sorting)
+ ? query.OrderBy((lp, p) => p.ProductName)
+ : query.OrderBy(input.Sorting);
+
+ var entities = await query
+ .Select((lp, p) => new ProductLocationGetListOutputDto
+ {
+ Id = lp.Id,
+ LocationId = lp.LocationId,
+ ProductId = p.Id,
+ ProductCode = p.ProductCode,
+ ProductName = p.ProductName,
+ ProductImageUrl = p.ProductImageUrl,
+ LocationCode = null,
+ LocationName = null
+ })
+ .ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
+
+ // 拉取门店信息用于输出
+ var locationIdSet = entities
+ .Select(x => x.LocationId)
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Distinct()
+ .ToList();
+
+ var locationGuidList = locationIdSet
+ .Where(x => Guid.TryParse(x, out _))
+ .Select(Guid.Parse)
+ .Distinct()
+ .ToList();
+
+ var locationMap = new Dictionary();
+ if (locationGuidList.Count > 0)
+ {
+ var locations = await _dbContext.SqlSugarClient.Queryable()
+ .Where(x => !x.IsDeleted)
+ .Where(x => locationGuidList.Contains(x.Id))
+ .ToListAsync();
+
+ locationMap = locations.ToDictionary(x => x.Id, x => x);
+ }
+
+ var items = entities.Select(x =>
+ {
+ string? locationCode = null;
+ string? locationName = null;
+ if (Guid.TryParse(x.LocationId, out var gid) && locationMap.TryGetValue(gid, out var loc))
+ {
+ locationCode = loc.LocationCode;
+ locationName = loc.LocationName ?? loc.LocationCode;
+ }
+
+ return new ProductLocationGetListOutputDto
+ {
+ Id = x.Id,
+ LocationId = x.LocationId,
+ LocationCode = locationCode,
+ LocationName = locationName,
+ ProductId = x.ProductId,
+ ProductCode = x.ProductCode,
+ ProductName = x.ProductName,
+ ProductImageUrl = x.ProductImageUrl
+ };
+ }).ToList();
+
+ return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items);
+ }
+
+ public async Task GetAsync(string id)
+ {
+ var locationId = id?.Trim();
+ if (string.IsNullOrWhiteSpace(locationId))
+ {
+ throw new UserFriendlyException("门店Id不能为空");
+ }
+
+ var rows = await _dbContext.SqlSugarClient
+ .Queryable((lp, p) => lp.ProductId == p.Id)
+ .Where((lp, p) => lp.LocationId == locationId && !p.IsDeleted)
+ .Select((lp, p) => new ProductLocationGetListOutputDto
+ {
+ Id = lp.Id,
+ LocationId = lp.LocationId,
+ ProductId = p.Id,
+ ProductCode = p.ProductCode,
+ ProductName = p.ProductName,
+ ProductImageUrl = p.ProductImageUrl
+ })
+ .ToListAsync();
+
+ if (rows.Count == 0)
+ {
+ return new ProductLocationGetOutputDto
+ {
+ LocationId = locationId,
+ Products = new List()
+ };
+ }
+
+ // 门店信息
+ string? locationCode = null;
+ string? locationName = null;
+ if (Guid.TryParse(locationId, out var gid))
+ {
+ var loc = await _dbContext.SqlSugarClient.Queryable()
+ .FirstAsync(x => !x.IsDeleted && x.Id == gid);
+ if (loc != null)
+ {
+ locationCode = loc.LocationCode;
+ locationName = loc.LocationName ?? loc.LocationCode;
+ }
+ }
+
+ var products = rows.Select(x => new ProductLocationGetListOutputDto
+ {
+ Id = x.Id,
+ LocationId = x.LocationId,
+ LocationCode = locationCode,
+ LocationName = locationName,
+ ProductId = x.ProductId,
+ ProductCode = x.ProductCode,
+ ProductName = x.ProductName,
+ ProductImageUrl = x.ProductImageUrl
+ }).ToList();
+
+ return new ProductLocationGetOutputDto
+ {
+ LocationId = locationId,
+ Products = products
+ };
+ }
+
+ [UnitOfWork]
+ public async Task CreateAsync(ProductLocationCreateInputVo input)
+ {
+ var locationId = input.LocationId?.Trim();
+ if (string.IsNullOrWhiteSpace(locationId))
+ {
+ throw new UserFriendlyException("门店Id不能为空");
+ }
+
+ var productIds = input.ProductIds?
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => x.Trim())
+ .Distinct()
+ .ToList() ?? new List();
+
+ if (productIds.Count == 0)
+ {
+ throw new UserFriendlyException("ProductIds至少1个");
+ }
+
+ // 校验门店存在
+ if (!Guid.TryParse(locationId, out var gid))
+ {
+ throw new UserFriendlyException("门店Id格式不正确");
+ }
+
+ var locExists = await _dbContext.SqlSugarClient.Queryable()
+ .AnyAsync(x => !x.IsDeleted && x.Id == gid);
+ if (!locExists)
+ {
+ throw new UserFriendlyException("门店不存在");
+ }
+
+ // 校验产品存在
+ var productExistsCount = await _dbContext.SqlSugarClient.Queryable()
+ .Where(x => !x.IsDeleted && productIds.Contains(x.Id))
+ .CountAsync();
+ if (productExistsCount != productIds.Count)
+ {
+ throw new UserFriendlyException("产品不存在");
+ }
+
+ // 去重:避免唯一键重复
+ var existingProductIds = await _dbContext.SqlSugarClient.Queryable()
+ .Where(x => x.LocationId == locationId && productIds.Contains(x.ProductId))
+ .Select(x => x.ProductId)
+ .Distinct()
+ .ToListAsync();
+
+ var newProductIds = productIds.Where(x => !existingProductIds.Contains(x)).ToList();
+ if (newProductIds.Count == 0)
+ {
+ return await GetAsync(locationId);
+ }
+
+ var rows = newProductIds.Select(pid => new FlLocationProductDbEntity
+ {
+ Id = _guidGenerator.Create().ToString(),
+ LocationId = locationId,
+ ProductId = pid
+ }).ToList();
+
+ await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
+ return await GetAsync(locationId);
+ }
+
+ [UnitOfWork]
+ public async Task UpdateAsync(string id, ProductLocationUpdateInputVo input)
+ {
+ var locationId = id?.Trim();
+ if (string.IsNullOrWhiteSpace(locationId))
+ {
+ throw new UserFriendlyException("门店Id不能为空");
+ }
+
+ var productIds = input.ProductIds?
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => x.Trim())
+ .Distinct()
+ .ToList() ?? new List();
+
+ if (productIds.Count == 0)
+ {
+ throw new UserFriendlyException("ProductIds至少1个");
+ }
+
+ if (!Guid.TryParse(locationId, out var gid))
+ {
+ throw new UserFriendlyException("门店Id格式不正确");
+ }
+
+ var locExists = await _dbContext.SqlSugarClient.Queryable()
+ .AnyAsync(x => !x.IsDeleted && x.Id == gid);
+ if (!locExists)
+ {
+ throw new UserFriendlyException("门店不存在");
+ }
+
+ var productExistsCount = await _dbContext.SqlSugarClient.Queryable()
+ .Where(x => !x.IsDeleted && productIds.Contains(x.Id))
+ .CountAsync();
+ if (productExistsCount != productIds.Count)
+ {
+ throw new UserFriendlyException("产品不存在");
+ }
+
+ // 替换:先删后建
+ await _dbContext.SqlSugarClient.Deleteable()
+ .Where(x => x.LocationId == locationId)
+ .ExecuteCommandAsync();
+
+ var rows = productIds.Select(pid => new FlLocationProductDbEntity
+ {
+ Id = _guidGenerator.Create().ToString(),
+ LocationId = locationId,
+ ProductId = pid
+ }).ToList();
+
+ await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
+ return await GetAsync(locationId);
+ }
+
+ [UnitOfWork]
+ public async Task DeleteAsync(string id)
+ {
+ var locationId = id?.Trim();
+ if (string.IsNullOrWhiteSpace(locationId))
+ {
+ throw new UserFriendlyException("门店Id不能为空");
+ }
+
+ await _dbContext.SqlSugarClient.Deleteable()
+ .Where(x => x.LocationId == locationId)
+ .ExecuteCommandAsync();
+ }
+
+ private static PagedResultWithPageDto BuildPagedResult(int skipCount, int maxResultCount, int total, List items)
+ {
+ var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount;
+ var pageIndex = pageSize <= 0 ? 1 : (skipCount / pageSize) + 1;
+ var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize);
+ return new PagedResultWithPageDto
+ {
+ PageIndex = pageIndex,
+ PageSize = pageSize,
+ TotalCount = total,
+ TotalPages = totalPages,
+ Items = items
+ };
+ }
+}
+
diff --git a/项目相关文档/标签模块接口对接说明.md b/项目相关文档/标签模块接口对接说明.md
new file mode 100644
index 0000000..5707218
--- /dev/null
+++ b/项目相关文档/标签模块接口对接说明.md
@@ -0,0 +1,584 @@
+## 概述
+
+美国版后端采用 ABP 动态接口(ConventionalControllers),宿主统一前缀为 `api/app`。
+Swagger 地址:
+
+- `http://localhost:19001/swagger`
+
+说明:
+- 接口最终 URL 以 Swagger 展示为准(可在 Swagger 里搜索 `LabelCategory / LabelType / LabelMultipleOption / LabelTemplate / Label`)。
+- 本模块后端接口以各 AppService 的方法签名自动暴露。
+- 返回分页统一包含 `PageIndex / PageSize / TotalCount / TotalPages / Items`。
+
+---
+
+## Swagger 中如何找到
+
+1. 启动后端宿主(`Yi.Abp.Web`),端口 `19001`。
+2. 打开 `http://localhost:19001/swagger`。
+3. 在接口分组里搜索以下关键词之一:
+ - `label-category`
+ - `label-type`
+ - `label-multiple-option`
+ - `label-template`
+ - `label`
+
+---
+
+## 接口 1:Label Categories(标签分类)
+
+### 1.1 分页列表
+
+方法:`GET /api/app/label-category`
+
+入参(`LabelCategoryGetListInputVo`,查询参数):
+
+- `skipCount`(int)
+- `maxResultCount`(int)
+- `sorting`(string,可选)
+- `keyword`(string,可选)
+- `state`(boolean,可选)
+
+示例(查询参数):
+
+```json
+{
+ "skipCount": 0,
+ "maxResultCount": 10,
+ "keyword": "Prep",
+ "state": true
+}
+```
+
+### 1.2 详情
+
+方法:`GET /api/app/label-category/{id}`
+
+入参:
+
+- `id`:分类 Id(字符串)
+
+### 1.3 新增
+
+方法:`POST /api/app/label-category`
+
+入参(Body:`LabelCategoryCreateInputVo`):
+
+```json
+{
+ "categoryCode": "CAT_PREP",
+ "categoryName": "Prep",
+ "categoryPhotoUrl": "https://cdn.example.com/cat-prep.png",
+ "state": true,
+ "orderNum": 1
+}
+```
+
+### 1.4 编辑
+
+方法:`PUT /api/app/label-category/{id}`
+
+入参(Body:`LabelCategoryUpdateInputVo`,字段同创建):
+
+```json
+{
+ "categoryCode": "CAT_PREP",
+ "categoryName": "Prep",
+ "categoryPhotoUrl": null,
+ "state": true,
+ "orderNum": 2
+}
+```
+
+### 1.5 删除(逻辑删除)
+
+方法:`DELETE /api/app/label-category/{id}`
+
+入参:
+
+- `id`:分类 Id(字符串)
+
+删除校验:
+- 若该分类已被 `fl_label` 引用,则抛出友好错误,禁止删除。
+
+---
+
+## 接口 2:Label Types(标签类型)
+
+### 2.1 分页列表
+
+方法:`GET /api/app/label-type`
+
+入参(`LabelTypeGetListInputVo`,查询参数):
+
+```json
+{
+ "skipCount": 0,
+ "maxResultCount": 10,
+ "keyword": "Defrost",
+ "state": true
+}
+```
+
+### 2.2 详情
+
+方法:`GET /api/app/label-type/{id}`
+
+入参:
+
+- `id`:类型 Id(字符串)
+
+### 2.3 新增
+
+方法:`POST /api/app/label-type`
+
+入参(Body:`LabelTypeCreateInputVo`):
+
+```json
+{
+ "typeCode": "TYPE_DEFROST",
+ "typeName": "Defrost",
+ "state": true,
+ "orderNum": 1
+}
+```
+
+### 2.4 编辑
+
+方法:`PUT /api/app/label-type/{id}`
+
+入参(Body:`LabelTypeUpdateInputVo`,字段同创建):
+
+```json
+{
+ "typeCode": "TYPE_DEFROST",
+ "typeName": "Defrost",
+ "state": true,
+ "orderNum": 2
+}
+```
+
+### 2.5 删除(逻辑删除)
+
+方法:`DELETE /api/app/label-type/{id}`
+
+删除校验:
+- 若该类型已被 `fl_label` 引用,则禁止删除。
+
+---
+
+## 接口 3:Multiple Options(多选项字典)
+
+### 3.1 分页列表
+
+方法:`GET /api/app/label-multiple-option`
+
+入参(`LabelMultipleOptionGetListInputVo`,查询参数):
+
+```json
+{
+ "skipCount": 0,
+ "maxResultCount": 10,
+ "keyword": "Allergens",
+ "state": true
+}
+```
+
+### 3.2 详情
+
+方法:`GET /api/app/label-multiple-option/{id}`
+
+入参:
+
+- `id`:多选项 Id(字符串)
+
+### 3.3 新增
+
+方法:`POST /api/app/label-multiple-option`
+
+入参(Body:`LabelMultipleOptionCreateInputVo`):
+
+```json
+{
+ "optionCode": "OPT_ALLERGENS",
+ "optionName": "Allergens",
+ "optionValuesJson": ["Peanuts", "Dairy", "Gluten", "Soy"],
+ "state": true,
+ "orderNum": 1
+}
+```
+
+### 3.4 编辑
+
+方法:`PUT /api/app/label-multiple-option/{id}`
+
+入参(Body:`LabelMultipleOptionUpdateInputVo`,字段同创建):
+
+```json
+{
+ "optionCode": "OPT_ALLERGENS",
+ "optionName": "Allergens",
+ "optionValuesJson": ["Peanuts", "Dairy"],
+ "state": true,
+ "orderNum": 2
+}
+```
+
+### 3.5 删除(逻辑删除)
+
+方法:`DELETE /api/app/label-multiple-option/{id}`
+
+---
+
+## 接口 4:Label Templates(标签模板)
+
+说明:
+- 模板标识入参 `id` 使用 `fl_label_template.TemplateCode`。
+- 创建/编辑的 Body 字段名对齐你前端 editor JSON(`id/name/appliedLocation/elements/config`)。
+
+### 4.1 分页列表
+
+方法:`GET /api/app/label-template`
+
+入参(`LabelTemplateGetListInputVo`,查询参数):
+
+```json
+{
+ "skipCount": 0,
+ "maxResultCount": 10,
+ "keyword": "测试模板",
+ "locationId": "11111111-1111-1111-1111-111111111111",
+ "labelType": "PRICE",
+ "state": true
+}
+```
+
+### 4.2 详情
+
+方法:`GET /api/app/label-template/{id}`
+
+入参:
+
+- `id`:模板编码 `TemplateCode`(字符串)
+
+### 4.3 新增模板
+
+方法:`POST /api/app/label-template`
+
+入参(Body:`LabelTemplateCreateInputVo`):
+
+```json
+{
+ "id": "TPL_TEST_001",
+ "name": "测试模板-价格签(4x6)",
+ "labelType": "PRICE",
+ "unit": "inch",
+ "width": 4,
+ "height": 6,
+ "appliedLocation": "ALL",
+ "showRuler": true,
+ "showGrid": true,
+ "state": true,
+ "elements": [
+ {
+ "id": "el-fixed-title",
+ "type": "TEXT_STATIC",
+ "x": 32,
+ "y": 24,
+ "width": 160,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "zIndex": 1,
+ "orderNum": 1,
+ "valueSourceType": "FIXED",
+ "isRequiredInput": false,
+ "config": {
+ "text": "商品名",
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "bold",
+ "textAlign": "left"
+ }
+ }
+ ],
+ "appliedLocationIds": []
+}
+```
+
+说明:
+- 当 `appliedLocation=SPECIFIED` 时,`appliedLocationIds` 必须至少选择一个门店。
+
+### 4.4 编辑模板
+
+方法:`PUT /api/app/label-template/{id}`
+
+入参:
+- Path:`id` 是当前模板编码(TemplateCode)
+- Body:字段同新增(`id/name/elements/...`)
+
+示例(编辑:同样字段,appliedLocation 切到 SPECIFIED):
+
+```json
+{
+ "id": "TPL_TEST_001",
+ "name": "测试模板-价格签(4x6) v2",
+ "labelType": "PRICE",
+ "unit": "inch",
+ "width": 4,
+ "height": 6,
+ "appliedLocation": "SPECIFIED",
+ "showRuler": true,
+ "showGrid": true,
+ "state": true,
+ "elements": [],
+ "appliedLocationIds": ["11111111-1111-1111-1111-111111111111"]
+}
+```
+
+版本:
+- `VersionNo` 会在编辑时自动 `+1`。
+- `elements` 会按传入内容全量重建。
+
+### 4.5 删除(逻辑删除)
+
+方法:`DELETE /api/app/label-template/{id}`
+
+入参:
+- `id`:模板编码 `TemplateCode`
+
+删除校验:
+- 若该模板已被 `fl_label` 引用,则禁止删除。
+
+---
+
+## 接口 5:Labels(按产品展示多个标签)
+
+说明:
+- 列表接口按 `ProductId` 查询,一个产品会对应多条标签记录。
+- 标签详情/编辑/删除的 `id` 使用 `fl_label.LabelCode`。
+
+### 5.1 分页列表(按产品)
+
+方法:`GET /api/app/label`
+
+入参(`LabelGetListInputVo`,查询参数):
+
+```json
+{
+ "skipCount": 0,
+ "maxResultCount": 10,
+ "sorting": "",
+ "keyword": "早餐",
+ "locationId": "11111111-1111-1111-1111-111111111111",
+ "productId": "22222222-2222-2222-2222-222222222222",
+ "labelCategoryId": "33333333-3333-3333-3333-333333333333",
+ "labelTypeId": "44444444-4444-4444-4444-444444444444",
+ "templateCode": "TPL_TEST_001",
+ "state": true
+}
+```
+
+### 5.2 详情
+
+方法:`GET /api/app/label/{id}`
+
+入参:
+- `id`:标签编码 `LabelCode`
+
+返回:
+- `productIds`:该标签绑定的产品Id 列表
+
+### 5.3 新增标签
+
+方法:`POST /api/app/label`
+
+入参(Body:`LabelCreateInputVo`):
+
+```json
+{
+ "labelCode": "LBL_TEST_001",
+ "labelName": "早餐标签",
+ "templateCode": "TPL_TEST_001",
+ "locationId": "11111111-1111-1111-1111-111111111111",
+ "labelCategoryId": "33333333-3333-3333-3333-333333333333",
+ "labelTypeId": "44444444-4444-4444-4444-444444444444",
+ "productIds": ["22222222-2222-2222-2222-222222222222"],
+ "labelInfoJson": { "note": "测试标签1" },
+ "state": true
+}
+```
+
+校验:
+- `productIds` 至少 1 个
+- `templateCode/locationId/labelCategoryId/labelTypeId` 不能为空
+
+### 5.4 编辑标签
+
+方法:`PUT /api/app/label/{id}`
+
+入参:
+- Path:`id` 为当前标签编码 `LabelCode`
+- Body:字段同创建(`LabelUpdateInputVo`)
+
+```json
+{
+ "labelName": "早餐标签 v2",
+ "templateCode": "TPL_TEST_001",
+ "locationId": "11111111-1111-1111-1111-111111111111",
+ "labelCategoryId": "33333333-3333-3333-3333-333333333333",
+ "labelTypeId": "44444444-4444-4444-4444-444444444444",
+ "productIds": ["22222222-2222-2222-2222-222222222222"],
+ "labelInfoJson": { "note": "测试标签1 v2" },
+ "state": true
+}
+```
+
+关联维护:
+- `fl_label_product` 会按新 `productIds` 重建。
+
+### 5.5 删除标签(逻辑删除)
+
+方法:`DELETE /api/app/label/{id}`
+
+入参:
+- `id`:标签编码 `LabelCode`
+
+删除行为:
+- 逻辑删除 `fl_label`
+- 删除该标签对应的 `fl_label_product` 关联
+
+---
+## 接口 6:Products(产品)
+
+说明:
+- 产品表:`fl_product`
+- 删除为逻辑删除:`IsDeleted = true`
+
+### 6.1 分页列表
+
+方法:`GET /api/app/product`
+
+入参(`ProductGetListInputVo`,查询参数):
+```json
+{
+ "skipCount": 0,
+ "maxResultCount": 10,
+ "sorting": "",
+ "keyword": "Chicken",
+ "state": true
+}
+```
+
+### 6.2 详情
+
+方法:`GET /api/app/product/{id}`
+
+入参:
+- `id`:产品Id(`fl_product.Id`)
+
+### 6.3 新增产品
+
+方法:`POST /api/app/product`
+
+入参(Body:`ProductCreateInputVo`):
+```json
+{
+ "productCode": "PRD_TEST_001",
+ "productName": "Chicken",
+ "categoryName": "Meat",
+ "productImageUrl": "https://example.com/img.png",
+ "state": true
+}
+```
+
+校验:
+- `productCode/productName` 不能为空
+- `productCode` 不能与未删除的数据重复
+
+### 6.4 编辑产品
+
+方法:`PUT /api/app/product/{id}`
+
+入参:
+- Path:`id` 为当前产品Id(`fl_product.Id`)
+- Body:字段同新增(`ProductUpdateInputVo`)
+
+### 6.5 删除(逻辑删除)
+
+方法:`DELETE /api/app/product/{id}`
+
+入参:
+- `id`:产品Id
+
+---
+## 接口 7:Product-Location(门店-产品关联)
+
+说明:
+- 关联表:`fl_location_product`
+- 关联按门店进行批量替换:
+ - `Create`:在门店下新增未存在的 product 关联
+ - `Update`:替换该门店下全部关联(先删后建)
+ - `Delete`:删除该门店下全部关联
+
+### 7.1 分页列表
+
+方法:`GET /api/app/product-location`
+
+入参(`ProductLocationGetListInputVo`,查询参数):
+```json
+{
+ "skipCount": 0,
+ "maxResultCount": 10,
+ "sorting": "",
+ "locationId": "11111111-1111-1111-1111-111111111111",
+ "productId": "22222222-2222-2222-2222-222222222222"
+}
+```
+
+### 7.2 获取门店下全部产品
+
+方法:`GET /api/app/product-location/{id}`
+
+入参:
+- `id`:门店Id(`location.Id`,string 表示)
+
+返回:
+- 门店Id + 该门店关联的产品列表
+
+### 7.3 新增/建立门店关联
+
+方法:`POST /api/app/product-location`
+
+入参(Body:`ProductLocationCreateInputVo`):
+```json
+{
+ "locationId": "11111111-1111-1111-1111-111111111111",
+ "productIds": ["22222222-2222-2222-2222-222222222222"]
+}
+```
+
+校验:
+- `locationId` 对应门店必须存在
+- `productIds` 必须都存在于 `fl_product` 且未删除
+
+### 7.4 编辑/替换门店关联
+
+方法:`PUT /api/app/product-location/{id}`
+
+入参:
+- Path:`id` 为门店Id
+- Body:`ProductLocationUpdateInputVo`
+```json
+{
+ "productIds": ["22222222-2222-2222-2222-222222222222"]
+}
+```
+
+### 7.5 删除门店关联(按门店删除全部)
+
+方法:`DELETE /api/app/product-location/{id}`
+
+入参:
+- `id`:门店Id
+