diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetListOutputDto.cs index 6938581..6c3574e 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetListOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetListOutputDto.cs @@ -12,7 +12,10 @@ public class LabelGetListOutputDto public string ProductCategoryName { get; set; } = string.Empty; - public string ProductName { get; set; } = string.Empty; + /// + /// 同一个标签绑定的产品名称,用 “,” 分割 + /// + public string Products { get; set; } = string.Empty; public string TemplateName { get; set; } = string.Empty; 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 index bc1128a..64f188e 100644 --- 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 @@ -16,9 +16,15 @@ public class LabelPreviewResolveInputVo public string? ProductId { get; set; } /// + /// 业务基准时间(用于 DATE/TIME 元素的渲染计算) + /// 不传则默认使用服务器当前时间 + /// + public DateTime? BaseTime { get; set; } + + /// /// 打印输入(前端传,用于 PRINT_INPUT 元素) /// key 建议使用模板元素的 InputKey /// - public Dictionary? PrintInputJson { get; set; } + public Dictionary? PrintInputJson { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Picture/PictureUploadInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Picture/PictureUploadInputVo.cs new file mode 100644 index 0000000..4294c14 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Picture/PictureUploadInputVo.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace FoodLabeling.Application.Contracts.Dtos.Picture; + +public class PictureUploadInputVo +{ + [FromForm(Name = "file")] + public IFormFile File { get; set; } = default!; + + /// + /// 可选子目录(相对路径),例如:category、category/2026-03 + /// + [FromForm(Name = "subDir")] + public string? SubDir { get; set; } +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Picture/PictureUploadOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Picture/PictureUploadOutputDto.cs new file mode 100644 index 0000000..910fa90 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Picture/PictureUploadOutputDto.cs @@ -0,0 +1,14 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Picture; + +public class PictureUploadOutputDto +{ + /// + /// 可直接保存到业务表的访问 URL(相对路径) + /// + public string Url { get; set; } = string.Empty; + + public string FileName { get; set; } = string.Empty; + + public long Size { get; set; } +} + 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 index 2f5c40c..16a4335 100644 --- 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 @@ -6,7 +6,7 @@ public class ProductCreateInputVo public string ProductName { get; set; } = string.Empty; - public string? CategoryName { get; set; } + public string? CategoryId { 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/Product/ProductGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetListOutputDto.cs index f172144..cb2b5d7 100644 --- 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 @@ -8,6 +8,8 @@ public class ProductGetListOutputDto public string ProductName { get; set; } = string.Empty; + public string? CategoryId { get; set; } + public string? CategoryName { 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/Product/ProductGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetOutputDto.cs index c307077..4308143 100644 --- 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 @@ -10,6 +10,8 @@ public class ProductGetOutputDto public string ProductName { get; set; } = string.Empty; + public string? CategoryId { get; set; } + public string? CategoryName { 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/ProductCategory/ProductCategoryCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryCreateInputVo.cs new file mode 100644 index 0000000..0dd9931 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryCreateInputVo.cs @@ -0,0 +1,18 @@ +namespace FoodLabeling.Application.Contracts.Dtos.ProductCategory; + +/// +/// 产品模块:新增类别入参 +/// +public class ProductCategoryCreateInputVo +{ + public string CategoryCode { get; set; } = string.Empty; + + public string CategoryName { get; set; } = string.Empty; + + public string? CategoryPhotoUrl { get; set; } + + public bool State { get; set; } = true; + + public int OrderNum { get; set; } = 0; +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListInputVo.cs new file mode 100644 index 0000000..4828537 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListInputVo.cs @@ -0,0 +1,20 @@ +using Volo.Abp.Application.Dtos; + +namespace FoodLabeling.Application.Contracts.Dtos.ProductCategory; + +/// +/// 产品模块:类别分页列表入参 +/// +public class ProductCategoryGetListInputVo : PagedAndSortedResultRequestDto +{ + /// + /// 模糊搜索(CategoryCode/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/ProductCategory/ProductCategoryGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListOutputDto.cs new file mode 100644 index 0000000..ced7d81 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListOutputDto.cs @@ -0,0 +1,22 @@ +namespace FoodLabeling.Application.Contracts.Dtos.ProductCategory; + +/// +/// 产品模块:类别列表行 +/// +public class ProductCategoryGetListOutputDto +{ + public string Id { get; set; } = string.Empty; + + public string CategoryCode { get; set; } = string.Empty; + + public string CategoryName { get; set; } = string.Empty; + + public string? CategoryPhotoUrl { get; set; } + + public bool State { get; set; } + + public int OrderNum { get; set; } + + public DateTime LastEdited { get; set; } +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs new file mode 100644 index 0000000..3c673a5 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs @@ -0,0 +1,20 @@ +namespace FoodLabeling.Application.Contracts.Dtos.ProductCategory; + +/// +/// 产品模块:类别详情 +/// +public class ProductCategoryGetOutputDto +{ + public string Id { get; set; } = string.Empty; + + public string CategoryCode { get; set; } = string.Empty; + + public string CategoryName { get; set; } = string.Empty; + + public string? CategoryPhotoUrl { get; set; } + + public bool State { get; set; } + + public int OrderNum { get; set; } +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryUpdateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryUpdateInputVo.cs new file mode 100644 index 0000000..36e6d11 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryUpdateInputVo.cs @@ -0,0 +1,9 @@ +namespace FoodLabeling.Application.Contracts.Dtos.ProductCategory; + +/// +/// 产品模块:编辑类别入参 +/// +public class ProductCategoryUpdateInputVo : ProductCategoryCreateInputVo +{ +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelCategoryTreeNodeDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelCategoryTreeNodeDto.cs new file mode 100644 index 0000000..4e2ce5d --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelCategoryTreeNodeDto.cs @@ -0,0 +1,17 @@ +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; + +/// +/// 第一级:标签分类(fl_label_category) +/// +public class UsAppLabelCategoryTreeNodeDto +{ + public string Id { get; set; } = string.Empty; + + public string CategoryName { get; set; } = string.Empty; + + public string? CategoryPhotoUrl { get; set; } + + public int OrderNum { get; set; } + + public List ProductCategories { get; set; } = new(); +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPreviewDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPreviewDto.cs new file mode 100644 index 0000000..b9206a5 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPreviewDto.cs @@ -0,0 +1,36 @@ +using FoodLabeling.Application.Contracts.Dtos.Label; + +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; + +/// +/// App 标签预览出参(顶部信息 + 预览模板结构) +/// +public class UsAppLabelPreviewDto +{ + public string LocationId { get; set; } = string.Empty; + + public string LabelCode { get; set; } = string.Empty; + + public string? TemplateCode { get; set; } + + public string? LabelSizeText { get; set; } + + public string? TypeName { get; set; } + + public string? ProductName { get; set; } + + public string? ProductCategoryName { get; set; } + + public string? LabelCategoryName { get; set; } + + /// + /// 预览图(base64 png,可空;若为空,客户端可用 Template 自行渲染) + /// + public string? PreviewImageBase64Png { get; set; } + + /// + /// 预览模板结构(与 LabelCanvas/LabelPreviewOnly 结构尽量一致) + /// + public LabelTemplatePreviewDto Template { get; set; } = new(); +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPreviewInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPreviewInputVo.cs new file mode 100644 index 0000000..97290de --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPreviewInputVo.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; + +/// +/// App 标签预览入参 +/// +public class UsAppLabelPreviewInputVo +{ + /// + /// 门店Id(fl_label.LocationId) + /// + public string LocationId { get; set; } = string.Empty; + + /// + /// 标签编码(fl_label.LabelCode) + /// + public string LabelCode { get; set; } = string.Empty; + + /// + /// 选择用于预览的产品Id(fl_product.Id) + /// 不传则默认取该标签绑定的第一个产品 + /// + public string? ProductId { get; set; } + + /// + /// 业务基准时间(用于 DATE/TIME 等元素的计算) + /// + public DateTime? BaseTime { get; set; } + + /// + /// 打印输入(用于 PRINT_INPUT 元素) + /// + public Dictionary? PrintInputJson { get; set; } +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintInputVo.cs new file mode 100644 index 0000000..6c8bb17 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintInputVo.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; + +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; + +/// +/// App 打印入参 +/// +public class UsAppLabelPrintInputVo +{ + /// + /// 门店Id(fl_label.LocationId) + /// + public string LocationId { get; set; } = string.Empty; + + /// + /// 标签编码(fl_label.LabelCode) + /// + public string LabelCode { get; set; } = string.Empty; + + /// + /// 选择用于打印的产品Id(fl_product.Id) + /// 不传则默认取该标签绑定的第一个产品 + /// + public string? ProductId { get; set; } + + /// + /// 打印份数(<=0 则按 1 处理) + /// + public int PrintQuantity { get; set; } = 1; + + /// + /// 业务基准时间(用于 DATE/TIME 等元素的计算) + /// + public DateTime? BaseTime { get; set; } + + /// + /// 打印输入(用于 PRINT_INPUT 元素) + /// + public Dictionary? PrintInputJson { get; set; } + + /// + /// 打印机Id(可选,若业务需要追踪) + /// + public string? PrinterId { get; set; } + + /// + /// 打印机蓝牙 MAC(可选) + /// + public string? PrinterMac { get; set; } + + /// + /// 打印机地址(可选) + /// + public string? PrinterAddress { get; set; } +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintOutputDto.cs new file mode 100644 index 0000000..d7c202c --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintOutputDto.cs @@ -0,0 +1,12 @@ +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; + +/// +/// App 打印出参 +/// +public class UsAppLabelPrintOutputDto +{ + public string TaskId { get; set; } = string.Empty; + + public int PrintQuantity { get; set; } +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelTypeNodeDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelTypeNodeDto.cs new file mode 100644 index 0000000..0334bc5 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelTypeNodeDto.cs @@ -0,0 +1,21 @@ +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; + +/// +/// 第四级:标签种类(对应一条可打印的标签实例) +/// +public class UsAppLabelTypeNodeDto +{ + public string LabelTypeId { get; set; } = string.Empty; + + public string TypeName { get; set; } = string.Empty; + + public int OrderNum { get; set; } + + /// 业务标签编码,预览/打印流程使用 + public string LabelCode { get; set; } = string.Empty; + + public string? TemplateCode { get; set; } + + /// 模板物理尺寸描述,如 2"x2" + public string? LabelSizeText { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingProductNodeDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingProductNodeDto.cs new file mode 100644 index 0000000..9e31138 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingProductNodeDto.cs @@ -0,0 +1,23 @@ +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; + +/// +/// 第三级:产品 +/// +public class UsAppLabelingProductNodeDto +{ + public string ProductId { get; set; } = string.Empty; + + public string ProductName { get; set; } = string.Empty; + + public string ProductCode { get; set; } = string.Empty; + + public string? ProductImageUrl { get; set; } + + /// 副标题(无独立业务字段时:有编码显示编码,否则「无」) + public string Subtitle { get; set; } = string.Empty; + + public int LabelTypeCount { get; set; } + + /// 第四级:该产品在当前标签分类+门店下可选的标签种类 + public List LabelTypes { get; set; } = new(); +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingTreeInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingTreeInputVo.cs new file mode 100644 index 0000000..081fae8 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingTreeInputVo.cs @@ -0,0 +1,16 @@ +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; + +/// +/// App Labeling 四级列表入参 +/// +public class UsAppLabelingTreeInputVo +{ + /// 当前门店 Id(location.Id,Guid 字符串) + public string LocationId { get; set; } = string.Empty; + + /// 关键词(匹配标签分类/产品分类/产品名/标签类型/标签名称) + public string? Keyword { get; set; } + + /// 仅展示某一标签分类(侧边栏选中时传);不传则返回全部分类 + public string? LabelCategoryId { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppProductCategoryNodeDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppProductCategoryNodeDto.cs new file mode 100644 index 0000000..fd93dae --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppProductCategoryNodeDto.cs @@ -0,0 +1,20 @@ +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; + +/// +/// 第二级:产品分类(fl_product.CategoryId join fl_product_category) +/// +public class UsAppProductCategoryNodeDto +{ + /// 产品分类Id;当产品未归类或分类不存在时为空 + public string? CategoryId { get; set; } + + /// 产品分类图片地址;当产品未归类或分类不存在时为空 + public string? CategoryPhotoUrl { get; set; } + + /// 分类显示名;空为「无」 + public string Name { get; set; } = string.Empty; + + public int ItemCount { get; set; } + + public List Products { get; set; } = new(); +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPictureAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPictureAppService.cs new file mode 100644 index 0000000..2d265af --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPictureAppService.cs @@ -0,0 +1,9 @@ +using FoodLabeling.Application.Contracts.Dtos.Picture; + +namespace FoodLabeling.Application.Contracts.IServices; + +public interface IPictureAppService +{ + Task UploadCategoryAsync(PictureUploadInputVo input); +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductCategoryAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductCategoryAppService.cs new file mode 100644 index 0000000..74f8bee --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductCategoryAppService.cs @@ -0,0 +1,22 @@ +using FoodLabeling.Application.Contracts.Dtos.Common; +using FoodLabeling.Application.Contracts.Dtos.ProductCategory; +using Volo.Abp.Application.Services; + +namespace FoodLabeling.Application.Contracts.IServices; + +/// +/// 产品模块:类别(Categories)接口 +/// +public interface IProductCategoryAppService : IApplicationService +{ + Task> GetListAsync(ProductCategoryGetListInputVo input); + + Task GetAsync(string id); + + Task CreateAsync(ProductCategoryCreateInputVo input); + + Task UpdateAsync(string id, ProductCategoryUpdateInputVo input); + + Task DeleteAsync(string id); +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs new file mode 100644 index 0000000..0006eec --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs @@ -0,0 +1,25 @@ +using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; +using Volo.Abp.Application.Services; + +namespace FoodLabeling.Application.Contracts.IServices; + +/// +/// App Labeling:四级列表(标签分类 → 产品分类 → 产品 → 标签种类) +/// +public interface IUsAppLabelingAppService : IApplicationService +{ + /// + /// 获取当前门店下四级嵌套树,供移动端 Labeling 首页使用 + /// + Task> GetLabelingTreeAsync(UsAppLabelingTreeInputVo input); + + /// + /// App 打印预览:按标签编码解析模板并返回顶部展示字段 + 预览模板结构 + /// + Task PreviewAsync(UsAppLabelPreviewInputVo input); + + /// + /// App 打印:创建打印任务并落库打印明细(fl_label_print_task / fl_label_print_data) + /// + Task PrintAsync(UsAppLabelPrintInputVo input); +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelPrintDataDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelPrintDataDbEntity.cs new file mode 100644 index 0000000..415b402 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelPrintDataDbEntity.cs @@ -0,0 +1,40 @@ +using SqlSugar; + +namespace FoodLabeling.Application.Services.DbModels; + +/// +/// 标签打印数据明细(对应表:fl_label_print_data) +/// +[SugarTable("fl_label_print_data")] +public class FlLabelPrintDataDbEntity +{ + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } = string.Empty; + + public bool IsDeleted { get; set; } + + public DateTime CreationTime { get; set; } + + public string? CreatorId { get; set; } + + public string? LastModifierId { get; set; } + + public DateTime? LastModificationTime { get; set; } + + public string ConcurrencyStamp { get; set; } = string.Empty; + + public string TaskId { get; set; } = string.Empty; + + public int? CopyIndex { get; set; } + + /// + /// 原始打印输入(json 字段,直接保存为字符串) + /// + public string? PrintInputJson { get; set; } + + /// + /// 解析后的可打印数据(建议保存为 json 字符串) + /// + public string? RenderDataJson { get; set; } +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelPrintTaskDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelPrintTaskDbEntity.cs new file mode 100644 index 0000000..8b73e7e --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelPrintTaskDbEntity.cs @@ -0,0 +1,46 @@ +using SqlSugar; + +namespace FoodLabeling.Application.Services.DbModels; + +/// +/// 标签打印任务(对应表:fl_label_print_task) +/// +[SugarTable("fl_label_print_task")] +public class FlLabelPrintTaskDbEntity +{ + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } = string.Empty; + + public bool IsDeleted { get; set; } + + public DateTime CreationTime { get; set; } + + public string? CreatorId { get; set; } + + public string? LastModifierId { get; set; } + + public DateTime? LastModificationTime { get; set; } + + public string ConcurrencyStamp { get; set; } = string.Empty; + + public string? LocationId { get; set; } + + public string? LabelCode { get; set; } + + public string? ProductId { get; set; } + + public string? LabelTypeId { get; set; } + + public string? TemplateCode { get; set; } + + public int PrintQuantity { get; set; } + + public DateTime? BaseTime { get; set; } + + public string? PrinterId { get; set; } + + public string? PrinterMac { get; set; } + + public string? PrinterAddress { get; set; } +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryDbEntity.cs new file mode 100644 index 0000000..1f51a75 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryDbEntity.cs @@ -0,0 +1,33 @@ +using SqlSugar; + +namespace FoodLabeling.Application.Services.DbModels; + +[SugarTable("fl_product_category")] +public class FlProductCategoryDbEntity +{ + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } = string.Empty; + + public bool IsDeleted { get; set; } + + public DateTime CreationTime { get; set; } + + public string? CreatorId { get; set; } + + public string? LastModifierId { get; set; } + + public DateTime? LastModificationTime { get; set; } + + public string ConcurrencyStamp { get; set; } = string.Empty; + + public string CategoryCode { get; set; } = string.Empty; + + public string CategoryName { get; set; } = string.Empty; + + public string? CategoryPhotoUrl { get; set; } + + public bool State { get; set; } + + public int OrderNum { get; set; } +} + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductDbEntity.cs index c4c7d45..f58cd55 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductDbEntity.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductDbEntity.cs @@ -14,7 +14,7 @@ public class FlProductDbEntity public string ProductName { get; set; } = string.Empty; - public string? CategoryName { get; set; } + public string? CategoryId { get; set; } public string? ProductImageUrl { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs index 63270c0..178285c 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs @@ -40,55 +40,124 @@ public class LabelAppService : ApplicationService, ILabelAppService var labelTypeId = input.LabelTypeId?.Trim(); var templateCode = input.TemplateCode?.Trim(); - // 先查 label-product 映射(按产品) - var query = _dbContext.SqlSugarClient.Queryable( - (lp, l, p, c, t, tpl) => - lp.LabelId == l.Id && - lp.ProductId == p.Id && - l.LabelCategoryId == c.Id && - l.LabelTypeId == t.Id && - l.TemplateId == tpl.Id) - .Where((lp, l, p, c, t, tpl) => l.IsDeleted == false) - .Where((lp, l, p, c, t, tpl) => !c.IsDeleted) - .Where((lp, l, p, c, t, tpl) => !t.IsDeleted) - .Where((lp, l, p, c, t, tpl) => !tpl.IsDeleted) - .Where((lp, l, p, c, t, tpl) => !p.IsDeleted) - .WhereIF(!string.IsNullOrWhiteSpace(productId), (lp, l, p, c, t, tpl) => lp.ProductId == productId) - .WhereIF(!string.IsNullOrWhiteSpace(locationId), (lp, l, p, c, t, tpl) => l.LocationId == locationId) - .WhereIF(!string.IsNullOrWhiteSpace(labelCategoryId), (lp, l, p, c, t, tpl) => l.LabelCategoryId == labelCategoryId) - .WhereIF(!string.IsNullOrWhiteSpace(labelTypeId), (lp, l, p, c, t, tpl) => l.LabelTypeId == labelTypeId) - .WhereIF(!string.IsNullOrWhiteSpace(templateCode), (lp, l, p, c, t, tpl) => tpl.TemplateCode == templateCode) - .WhereIF(!string.IsNullOrWhiteSpace(keyword), - (lp, l, p, c, t, tpl) => + // 目标:列表每行是“标签”,同一个标签下的 products 以 “,” 拼接展示 + // 因此需要按 label 维度分页(避免 label-product join 导致重复行与分页错乱)。 + + var labelIdsQuery = _dbContext.SqlSugarClient.Queryable() + .Where(l => !l.IsDeleted) + .WhereIF(!string.IsNullOrWhiteSpace(locationId), l => l.LocationId == locationId) + .WhereIF(!string.IsNullOrWhiteSpace(labelCategoryId), l => l.LabelCategoryId == labelCategoryId) + .WhereIF(!string.IsNullOrWhiteSpace(labelTypeId), l => l.LabelTypeId == labelTypeId) + .WhereIF(input.State != null, l => l.State == input.State); + + if (!string.IsNullOrWhiteSpace(templateCode)) + { + labelIdsQuery = labelIdsQuery + .InnerJoin((l, tpl) => l.TemplateId == tpl.Id) + .Where((l, tpl) => !tpl.IsDeleted && tpl.TemplateCode == templateCode) + .Select((l, tpl) => l); + } + + // 按产品筛选:存在 label-product 关联即可 + if (!string.IsNullOrWhiteSpace(productId)) + { + labelIdsQuery = labelIdsQuery + .InnerJoin((l, lp) => lp.LabelId == l.Id) + .Where((l, lp) => lp.ProductId == productId) + .Select((l, lp) => l); + } + + // 关键字:匹配 labelName/categoryName/typeName/templateName/productName + if (!string.IsNullOrWhiteSpace(keyword)) + { + labelIdsQuery = labelIdsQuery + .LeftJoin((l, c) => l.LabelCategoryId == c.Id) + .LeftJoin((l, c, t) => l.LabelTypeId == t.Id) + .LeftJoin((l, c, t, tpl) => l.TemplateId == tpl.Id) + .LeftJoin((l, c, t, tpl, lp) => lp.LabelId == l.Id) + .LeftJoin((l, c, t, tpl, lp, p) => lp.ProductId == p.Id) + .Where((l, c, t, tpl, lp, p) => l.LabelName.Contains(keyword!) || - p.ProductName.Contains(keyword!) || - c.CategoryName.Contains(keyword!) || - t.TypeName.Contains(keyword!)) - .WhereIF(input.State != null, (lp, l, p, c, t, tpl) => l.State == input.State) - .OrderByDescending((lp, l, p, c, t, tpl) => l.LastModificationTime ?? l.CreationTime); + (c.CategoryName != null && c.CategoryName.Contains(keyword!)) || + (t.TypeName != null && t.TypeName.Contains(keyword!)) || + (tpl.TemplateName != null && tpl.TemplateName.Contains(keyword!)) || + (p.ProductName != null && p.ProductName.Contains(keyword!))) + .Select((l, c, t, tpl, lp, p) => l); + } + // 排序(优先外部 Sorting,否则按最后编辑倒序) if (!string.IsNullOrWhiteSpace(input.Sorting)) { - query = query.OrderBy(input.Sorting); + labelIdsQuery = labelIdsQuery.OrderBy(input.Sorting); + } + else + { + labelIdsQuery = labelIdsQuery.OrderByDescending(l => l.LastModificationTime ?? l.CreationTime); } - var entities = await query.Select((lp, l, p, c, t, tpl) => new + var pageLabelIds = await labelIdsQuery + .Select(l => l.Id) + .Distinct() + .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); + + if (pageLabelIds.Count == 0) + { + return new PagedResultWithPageDto { - LabelCode = l.LabelCode, - LabelName = l.LabelName, - LocationId = l.LocationId, - LocationName = (string?)null, // later fill + PageIndex = 1, + PageSize = input.MaxResultCount, + TotalCount = total, + TotalPages = 0, + Items = new List() + }; + } + + // 查询标签基础信息(分类/类型/模板) + var labelRows = await _dbContext.SqlSugarClient + .Queryable( + (l, c, t, tpl) => l.LabelCategoryId == c.Id && l.LabelTypeId == t.Id && l.TemplateId == tpl.Id) + .Where((l, c, t, tpl) => pageLabelIds.Contains(l.Id)) + .Where((l, c, t, tpl) => !l.IsDeleted && !c.IsDeleted && !t.IsDeleted && !tpl.IsDeleted) + .Select((l, c, t, tpl) => new + { + l.Id, + l.LabelCode, + l.LabelName, + l.LocationId, LabelCategoryName = c.CategoryName, - ProductCategoryName = p.CategoryName, - ProductName = p.ProductName, - TemplateName = tpl.TemplateName, LabelTypeName = t.TypeName, - State = l.State, + TemplateName = tpl.TemplateName, + l.State, LastEdited = l.LastModificationTime ?? l.CreationTime }) - .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); + .ToListAsync(); + + // 按分页顺序输出 + var labelMap = labelRows.ToDictionary(x => x.Id, x => x); + var orderedLabels = pageLabelIds.Where(id => labelMap.ContainsKey(id)).Select(id => labelMap[id]).ToList(); + + // 查询 products 并拼接 + var productRows = await _dbContext.SqlSugarClient + .Queryable() + .InnerJoin((lp, l) => lp.LabelId == l.Id) + .InnerJoin((lp, l, p) => lp.ProductId == p.Id) + .LeftJoin((lp, l, p, pc) => p.CategoryId == pc.Id) + .Where((lp, l, p, pc) => pageLabelIds.Contains(lp.LabelId)) + .Where((lp, l, p, pc) => !l.IsDeleted && !p.IsDeleted) + .Select((lp, l, p, pc) => new { lp.LabelId, p.ProductName, ProductCategoryName = pc.CategoryName }) + .ToListAsync(); - var locationIds = entities + var productsMap = productRows + .GroupBy(x => x.LabelId) + .ToDictionary( + g => g.Key, + g => new + { + Products = string.Join(",", g.Select(x => x.ProductName ?? string.Empty).Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()), + ProductCategoryName = string.Join(",", g.Select(x => x.ProductCategoryName ?? string.Empty).Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()) + }); + + var locationIds = orderedLabels .Select(x => x.LocationId) .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x!.Trim()) @@ -109,21 +178,23 @@ public class LabelAppService : ApplicationService, ILabelAppService } } - var items = entities.Select(x => + var items = orderedLabels.Select(x => { var locationName = string.Empty; if (!string.IsNullOrWhiteSpace(x.LocationId) && locationMap.TryGetValue(x.LocationId!, out var loc)) { locationName = loc.LocationName ?? loc.LocationCode; } + var products = productsMap.TryGetValue(x.Id, out var prod) ? prod.Products : string.Empty; + var productCategoryNameValue = productsMap.TryGetValue(x.Id, out var prod2) ? prod2.ProductCategoryName : string.Empty; return new LabelGetListOutputDto { Id = x.LabelCode ?? string.Empty, LabelName = x.LabelName ?? string.Empty, LocationName = string.IsNullOrWhiteSpace(locationName) ? "无" : locationName, LabelCategoryName = x.LabelCategoryName ?? string.Empty, - ProductCategoryName = x.ProductCategoryName ?? string.Empty, - ProductName = x.ProductName ?? string.Empty, + ProductCategoryName = string.IsNullOrWhiteSpace(productCategoryNameValue) ? "无" : productCategoryNameValue, + Products = products, TemplateName = x.TemplateName ?? string.Empty, LabelTypeName = x.LabelTypeName ?? string.Empty, State = x.State, @@ -406,6 +477,8 @@ public class LabelAppService : ApplicationService, ILabelAppService throw new UserFriendlyException("labelCode不能为空"); } + var baseTime = input?.BaseTime ?? DateTime.Now; + var label = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.LabelCode == labelCode); if (label is null) @@ -572,14 +645,14 @@ public class LabelAppService : ApplicationService, ILabelAppService case "DATE": { var offsetDays = cfg.TryGetValue("offsetDays", out var od) ? TryGetInt(od, 0) : 0; - var dt = DateTime.Today.AddDays(offsetDays); + var dt = baseTime.Date.AddDays(offsetDays); UpsertConfigValue(cfg, "format", dt.ToString("yyyy-MM-dd")); cfg.Remove("inputType"); } break; case "TIME": { - var dt = DateTime.Now; + var dt = baseTime; UpsertConfigValue(cfg, "format", dt.ToString("HH:mm")); cfg.Remove("inputType"); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PictureAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PictureAppService.cs new file mode 100644 index 0000000..379a233 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PictureAppService.cs @@ -0,0 +1,114 @@ +using FoodLabeling.Application.Contracts.Dtos.Picture; +using FoodLabeling.Application.Contracts.IServices; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; +using Volo.Abp; +using Volo.Abp.Application.Services; +using Volo.Abp.Guids; + +namespace FoodLabeling.Application.Services; + +public class PictureAppService : ApplicationService, IPictureAppService +{ + private const long MaxSizeBytes = 5 * 1024 * 1024; + private static readonly HashSet AllowedExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".jpg", + ".jpeg", + ".png", + ".webp", + ".gif" + }; + + private readonly IGuidGenerator _guidGenerator; + private readonly IHostEnvironment _hostEnvironment; + + public PictureAppService(IGuidGenerator guidGenerator, IHostEnvironment hostEnvironment) + { + _guidGenerator = guidGenerator; + _hostEnvironment = hostEnvironment; + } + + /// + /// 上传类别图片(保存到 /www/wwwroot/FoodLabelingManagementUs/picture) + /// + /// 返回的 Url 可直接保存到 CategoryPhotoUrl。 + [HttpPost] + [Consumes("multipart/form-data")] + [Route("/api/app/picture/category/upload")] + public async Task UploadCategoryAsync([FromForm] PictureUploadInputVo input) + { + if (input.File is null || input.File.Length <= 0) + { + throw new UserFriendlyException("请选择要上传的图片文件"); + } + + if (input.File.Length > MaxSizeBytes) + { + throw new UserFriendlyException("图片大小不能超过5MB"); + } + + var ext = Path.GetExtension(input.File.FileName ?? string.Empty); + if (string.IsNullOrWhiteSpace(ext) || !AllowedExtensions.Contains(ext)) + { + throw new UserFriendlyException("仅支持上传 jpg/jpeg/png/webp/gif 格式图片"); + } + + var subDir = NormalizeSubDir(input.SubDir); + var saveRoot = ResolvePictureRoot(); + var saveDir = string.IsNullOrWhiteSpace(subDir) ? saveRoot : Path.Combine(saveRoot, subDir); + Directory.CreateDirectory(saveDir); + + var fileName = $"{DateTime.Now:yyyyMMddHHmmss}_{_guidGenerator.Create():N}{ext.ToLowerInvariant()}"; + var savePath = Path.Combine(saveDir, fileName); + + await using (var stream = new FileStream(savePath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + { + await input.File.CopyToAsync(stream); + } + + var url = BuildPictureUrl(subDir, fileName); + return new PictureUploadOutputDto + { + Url = url, + FileName = fileName, + Size = input.File.Length + }; + } + + private string ResolvePictureRoot() + { + var linuxPictureRoot = "/www/wwwroot/FoodLabelingManagementUs/picture"; + var webRootPicture = Path.Combine(_hostEnvironment.ContentRootPath, "wwwroot", "FoodLabelingManagementUs", "picture"); + return Directory.Exists(linuxPictureRoot) ? linuxPictureRoot : webRootPicture; + } + + private static string NormalizeSubDir(string? subDir) + { + if (string.IsNullOrWhiteSpace(subDir)) + { + return string.Empty; + } + + var s = subDir.Trim().Replace('\\', '/'); + while (s.StartsWith('/')) + { + s = s[1..]; + } + + if (s.Contains("..", StringComparison.Ordinal)) + { + throw new UserFriendlyException("subDir 不能包含 .."); + } + + return s; + } + + private static string BuildPictureUrl(string? subDir, string fileName) + { + var s = string.IsNullOrWhiteSpace(subDir) ? string.Empty : $"/{subDir.Trim().Replace('\\', '/').Trim('/')}"; + return $"/picture{s}/{fileName}"; + } +} + 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 index 17474d5..0bd17df 100644 --- 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 @@ -31,14 +31,22 @@ public class ProductAppService : ApplicationService, IProductAppService RefAsync total = 0; var keyword = input.Keyword?.Trim(); - var query = _dbContext.SqlSugarClient.Queryable() + 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(keyword)) + { + query = query + .LeftJoin((p, c) => p.CategoryId == c.Id) + .Where((p, c) => + p.ProductCode.Contains(keyword!) || + p.ProductName.Contains(keyword!) || + (c.CategoryName != null && c.CategoryName.Contains(keyword!))) + .Select((p, c) => p); + } + if (!string.IsNullOrWhiteSpace(input.Sorting)) { query = query.OrderBy(input.Sorting); @@ -65,15 +73,41 @@ public class ProductAppService : ApplicationService, IProductAppService countMap = countRows.ToDictionary(x => x.ProductId, x => (long)x.Count); } - var items = entities.Select(x => new ProductGetListOutputDto + var categoryIds = entities + .Select(x => x.CategoryId) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x!.Trim()) + .Distinct() + .ToList(); + + var categoryMap = new Dictionary(); + if (categoryIds.Count > 0) { - 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 + var categories = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted && categoryIds.Contains(x.Id)) + .ToListAsync(); + categoryMap = categories.ToDictionary(x => x.Id, x => x); + } + + var items = entities.Select(x => + { + var categoryName = "无"; + if (!string.IsNullOrWhiteSpace(x.CategoryId) && categoryMap.TryGetValue(x.CategoryId.Trim(), out var c)) + { + categoryName = string.IsNullOrWhiteSpace(c.CategoryName) ? "无" : c.CategoryName.Trim(); + } + + return new ProductGetListOutputDto + { + Id = x.Id, + ProductCode = x.ProductCode, + ProductName = x.ProductName, + CategoryId = x.CategoryId, + CategoryName = 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); @@ -95,6 +129,17 @@ public class ProductAppService : ApplicationService, IProductAppService throw new UserFriendlyException("产品不存在"); } + string? categoryName = "无"; + if (!string.IsNullOrWhiteSpace(entity.CategoryId)) + { + var c = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => !x.IsDeleted && x.Id == entity.CategoryId); + if (c is not null && !string.IsNullOrWhiteSpace(c.CategoryName)) + { + categoryName = c.CategoryName.Trim(); + } + } + var locationIds = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.ProductId == productId) .Select(x => x.LocationId) @@ -106,7 +151,8 @@ public class ProductAppService : ApplicationService, IProductAppService Id = entity.Id, ProductCode = entity.ProductCode, ProductName = entity.ProductName, - CategoryName = entity.CategoryName, + CategoryId = entity.CategoryId, + CategoryName = categoryName, ProductImageUrl = entity.ProductImageUrl, State = entity.State, LocationIds = locationIds @@ -136,7 +182,7 @@ public class ProductAppService : ApplicationService, IProductAppService IsDeleted = false, ProductCode = code, ProductName = name, - CategoryName = input.CategoryName?.Trim(), + CategoryId = input.CategoryId?.Trim(), ProductImageUrl = input.ProductImageUrl?.Trim(), State = input.State }; @@ -177,7 +223,7 @@ public class ProductAppService : ApplicationService, IProductAppService entity.ProductCode = code; entity.ProductName = name; - entity.CategoryName = input.CategoryName?.Trim(); + entity.CategoryId = input.CategoryId?.Trim(); entity.ProductImageUrl = input.ProductImageUrl?.Trim(); entity.State = input.State; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs new file mode 100644 index 0000000..3a6fdf6 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs @@ -0,0 +1,237 @@ +using FoodLabeling.Application.Contracts.Dtos.Common; +using FoodLabeling.Application.Contracts.Dtos.ProductCategory; +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 Yi.Framework.SqlSugarCore.Abstractions; + +namespace FoodLabeling.Application.Services; + +/// +/// 产品模块:类别(Categories)服务,对外仅在 food-labeling-us 暴露 +/// +public class ProductCategoryAppService : ApplicationService, IProductCategoryAppService +{ + private readonly ISqlSugarDbContext _dbContext; + private readonly IGuidGenerator _guidGenerator; + + public ProductCategoryAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator) + { + _dbContext = dbContext; + _guidGenerator = guidGenerator; + } + + /// + /// 类别分页列表 + /// + public async Task> GetListAsync(ProductCategoryGetListInputVo input) + { + RefAsync total = 0; + var keyword = input.Keyword?.Trim(); + + var query = _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted) + .WhereIF(!string.IsNullOrWhiteSpace(keyword), + x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!)) + .WhereIF(input.State != null, x => x.State == input.State); + + // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column + // 同时避免将 input.Sorting 原样拼接到 SQL(存在注入风险) + if (!string.IsNullOrWhiteSpace(input.Sorting)) + { + var sorting = input.Sorting.Trim(); + if (sorting.Equals("OrderNum desc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderByDescending(x => x.OrderNum); + } + else if (sorting.Equals("OrderNum asc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.OrderNum); + } + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderByDescending(x => x.CreationTime); + } + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.CreationTime); + } + else + { + // 不识别的排序统一走默认 + query = query.OrderByDescending(x => x.OrderNum).OrderByDescending(x => x.CreationTime); + } + } + else + { + query = query.OrderByDescending(x => x.OrderNum).OrderByDescending(x => x.CreationTime); + } + + var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); + + var items = entities.Select(x => new ProductCategoryGetListOutputDto + { + Id = x.Id, + CategoryCode = x.CategoryCode, + CategoryName = x.CategoryName, + CategoryPhotoUrl = x.CategoryPhotoUrl, + State = x.State, + OrderNum = x.OrderNum, + LastEdited = x.LastModificationTime ?? x.CreationTime + }).ToList(); + + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); + } + + /// + /// 类别详情 + /// + public async Task GetAsync(string id) + { + var entity = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => x.Id == id && !x.IsDeleted); + if (entity is null) + { + throw new UserFriendlyException("类别不存在"); + } + + return MapToGetOutput(entity); + } + + /// + /// 新增类别 + /// + public async Task CreateAsync(ProductCategoryCreateInputVo input) + { + var code = input.CategoryCode?.Trim(); + var name = input.CategoryName?.Trim(); + if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(name)) + { + throw new UserFriendlyException("类别编码和名称不能为空"); + } + + var duplicated = await _dbContext.SqlSugarClient.Queryable() + .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); + if (duplicated) + { + throw new UserFriendlyException("类别编码或名称已存在"); + } + + var now = DateTime.Now; + var currentUserId = CurrentUser?.Id?.ToString(); + var entity = new FlProductCategoryDbEntity + { + Id = _guidGenerator.Create().ToString(), + IsDeleted = false, + CreationTime = now, + CreatorId = currentUserId, + LastModifierId = currentUserId, + LastModificationTime = now, + ConcurrencyStamp = _guidGenerator.Create().ToString("N"), + CategoryCode = code, + CategoryName = name, + CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(), + State = input.State, + OrderNum = input.OrderNum + }; + + await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); + return await GetAsync(entity.Id); + } + + /// + /// 编辑类别 + /// + public async Task UpdateAsync(string id, ProductCategoryUpdateInputVo input) + { + var entity = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => x.Id == id && !x.IsDeleted); + if (entity is null) + { + throw new UserFriendlyException("类别不存在"); + } + + var code = input.CategoryCode?.Trim(); + var name = input.CategoryName?.Trim(); + if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(name)) + { + throw new UserFriendlyException("类别编码和名称不能为空"); + } + + var duplicated = await _dbContext.SqlSugarClient.Queryable() + .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); + if (duplicated) + { + throw new UserFriendlyException("类别编码或名称已存在"); + } + + entity.CategoryCode = code; + entity.CategoryName = name; + entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(); + entity.State = input.State; + entity.OrderNum = input.OrderNum; + entity.LastModificationTime = DateTime.Now; + entity.LastModifierId = CurrentUser?.Id?.ToString(); + + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + return await GetAsync(id); + } + + /// + /// 删除类别(逻辑删除) + /// + public async Task DeleteAsync(string id) + { + var entity = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => x.Id == id && !x.IsDeleted); + if (entity is null) + { + return; + } + + // 若被产品引用则不允许删除 + var usedByProduct = await _dbContext.SqlSugarClient.Queryable() + .AnyAsync(x => !x.IsDeleted && x.CategoryId == id); + if (usedByProduct) + { + throw new UserFriendlyException("该类别已被产品引用,无法删除"); + } + + entity.IsDeleted = true; + entity.LastModificationTime = DateTime.Now; + entity.LastModifierId = CurrentUser?.Id?.ToString(); + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + } + + private static ProductCategoryGetOutputDto MapToGetOutput(FlProductCategoryDbEntity x) + { + return new ProductCategoryGetOutputDto + { + Id = x.Id, + CategoryCode = x.CategoryCode, + CategoryName = x.CategoryName, + CategoryPhotoUrl = x.CategoryPhotoUrl, + State = x.State, + OrderNum = x.OrderNum + }; + } + + 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/UsAppLabelingAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs new file mode 100644 index 0000000..7b0d54d --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs @@ -0,0 +1,533 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FoodLabeling.Application.Contracts.Dtos.Label; +using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; +using FoodLabeling.Application.Contracts.IServices; +using FoodLabeling.Application.Services.DbModels; +using Microsoft.AspNetCore.Authorization; +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; + +/// +/// App Labeling:四级列表(标签分类 → 产品分类 → 产品 → 标签种类) +/// +public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppService +{ + private readonly ISqlSugarDbContext _dbContext; + private readonly ILabelAppService _labelAppService; + private readonly IGuidGenerator _guidGenerator; + + public UsAppLabelingAppService(ISqlSugarDbContext dbContext, ILabelAppService labelAppService, IGuidGenerator guidGenerator) + { + _dbContext = dbContext; + _labelAppService = labelAppService; + _guidGenerator = guidGenerator; + } + + /// + /// 获取当前门店下四级嵌套数据 + /// + /// + /// L1 标签分类 fl_label_category;L2 产品分类 fl_product.CategoryId join fl_product_category; + /// L3 产品;L4 与该门店、该标签分类、该产品关联的标签实例(fl_label + fl_label_type)。 + /// + [Authorize] + public virtual async Task> GetLabelingTreeAsync(UsAppLabelingTreeInputVo input) + { + if (string.IsNullOrWhiteSpace(input.LocationId)) + { + throw new UserFriendlyException("门店Id不能为空"); + } + + var locationId = input.LocationId.Trim(); + var keyword = input.Keyword?.Trim(); + var filterCategoryId = input.LabelCategoryId?.Trim(); + + var productIds = await _dbContext.SqlSugarClient.Queryable() + .Where(x => x.LocationId == locationId) + .Select(x => x.ProductId) + .ToListAsync(); + + if (productIds.Count == 0) + { + return new List(); + } + + var query = BuildLabelingJoinQuery(locationId, productIds, filterCategoryId, keyword); + + var raw = await query + .Select((lp, l, p, c, t, tpl, pc) => new LabelingTreeRow + { + LabelCategoryId = c.Id, + LabelCategoryName = c.CategoryName, + LabelCategoryPhotoUrl = c.CategoryPhotoUrl, + LabelCategoryOrderNum = c.OrderNum, + ProductCategoryId = p.CategoryId, + ProductCategoryName = pc.CategoryName, + ProductCategoryPhotoUrl = pc.CategoryPhotoUrl, + ProductId = p.Id, + ProductName = p.ProductName, + ProductCode = p.ProductCode, + ProductImageUrl = p.ProductImageUrl, + LabelTypeId = t.Id, + TypeName = t.TypeName, + TypeOrderNum = t.OrderNum, + LabelCode = l.LabelCode, + TemplateCode = tpl.TemplateCode, + TemplateWidth = tpl.Width, + TemplateHeight = tpl.Height, + TemplateUnit = tpl.Unit + }) + .ToListAsync(); + + if (raw.Count == 0) + { + return new List(); + } + + var byL1 = raw.GroupBy(x => new + { + x.LabelCategoryId, + x.LabelCategoryName, + x.LabelCategoryPhotoUrl, + x.LabelCategoryOrderNum + }).OrderBy(g => g.Key.LabelCategoryOrderNum).ThenBy(g => g.Key.LabelCategoryName); + + var result = new List(); + foreach (var g1 in byL1) + { + var l1 = new UsAppLabelCategoryTreeNodeDto + { + Id = g1.Key.LabelCategoryId, + CategoryName = g1.Key.LabelCategoryName ?? string.Empty, + CategoryPhotoUrl = g1.Key.LabelCategoryPhotoUrl, + OrderNum = g1.Key.LabelCategoryOrderNum, + ProductCategories = new List() + }; + + var byL2 = g1.GroupBy(x => + { + var categoryId = NormalizeNullableId(x.ProductCategoryId); + if (categoryId is null) + { + return new + { + CategoryId = (string?)null, + CategoryName = "无", + CategoryPhotoUrl = (string?)null + }; + } + + var categoryName = NormalizeCategoryName(x.ProductCategoryName); + var categoryPhotoUrl = NormalizeNullableUrl(x.ProductCategoryPhotoUrl); + return new + { + CategoryId = (string?)categoryId, + CategoryName = categoryName, + CategoryPhotoUrl = categoryPhotoUrl + }; + }) + .OrderBy(g => g.Key.CategoryName); + + foreach (var g2 in byL2) + { + var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName); + var l2 = new UsAppProductCategoryNodeDto + { + CategoryId = g2.Key.CategoryId, + CategoryPhotoUrl = g2.Key.CategoryPhotoUrl, + Name = g2.Key.CategoryName, + ItemCount = productsGrouped.Count(), + Products = new List() + }; + + foreach (var g3 in productsGrouped) + { + var first = g3.First(); + var typeNodes = g3 + .GroupBy(r => r.LabelCode) + .Select(gr => BuildLabelTypeNode(gr.First())) + .OrderBy(t => t.OrderNum) + .ThenBy(t => t.TypeName) + .ToList(); + + var subtitle = string.IsNullOrWhiteSpace(first.ProductCode?.Trim()) + ? "无" + : first.ProductCode!.Trim(); + + l2.Products.Add(new UsAppLabelingProductNodeDto + { + ProductId = first.ProductId, + ProductName = first.ProductName ?? string.Empty, + ProductCode = first.ProductCode ?? string.Empty, + ProductImageUrl = first.ProductImageUrl, + Subtitle = subtitle, + LabelTypeCount = typeNodes.Count, + LabelTypes = typeNodes + }); + } + + l1.ProductCategories.Add(l2); + } + + result.Add(l1); + } + + return result; + } + + /// + /// App 打印预览:按标签编码解析模板并返回顶部展示字段 + 预览模板结构 + /// + /// + /// 示例请求: + /// ```json + /// { + /// "locationId": "LOC001", + /// "labelCode": "LBL0001", + /// "productId": "PROD001", + /// "baseTime": "2026-03-26T10:30:00", + /// "printInputJson": { + /// "price": "12.99" + /// } + /// } + /// ``` + /// + /// 预览入参 + /// 顶部字段 + 预览模板结构 + /// 成功 + /// 参数错误/数据不存在 + /// 服务器错误 + [Authorize] + public virtual async Task PreviewAsync(UsAppLabelPreviewInputVo input) + { + if (input is null) + { + throw new UserFriendlyException("入参不能为空"); + } + + var locationId = input.LocationId?.Trim(); + if (string.IsNullOrWhiteSpace(locationId)) + { + throw new UserFriendlyException("门店Id不能为空"); + } + + var labelCode = input.LabelCode?.Trim(); + if (string.IsNullOrWhiteSpace(labelCode)) + { + throw new UserFriendlyException("labelCode不能为空"); + } + + var labelRow = await _dbContext.SqlSugarClient + .Queryable( + (l, c, t, tpl) => l.LabelCategoryId == c.Id && l.LabelTypeId == t.Id && l.TemplateId == tpl.Id) + .Where((l, c, t, tpl) => !l.IsDeleted && l.State) + .Where((l, c, t, tpl) => !c.IsDeleted && c.State) + .Where((l, c, t, tpl) => !t.IsDeleted && t.State) + .Where((l, c, t, tpl) => !tpl.IsDeleted) + .Where((l, c, t, tpl) => l.LabelCode == labelCode) + .Select((l, c, t, tpl) => new + { + l.LabelCode, + l.LocationId, + LabelCategoryName = c.CategoryName, + TypeName = t.TypeName, + TemplateCode = tpl.TemplateCode, + TemplateWidth = tpl.Width, + TemplateHeight = tpl.Height, + TemplateUnit = tpl.Unit + }) + .FirstAsync(); + + if (labelRow is null) + { + throw new UserFriendlyException("标签不存在或不可用"); + } + + if (!string.Equals(labelRow.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase)) + { + throw new UserFriendlyException("该标签不属于当前门店"); + } + + var template = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo + { + LabelCode = labelCode, + ProductId = input.ProductId?.Trim(), + BaseTime = input.BaseTime, + PrintInputJson = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value) + }); + + var productName = string.Empty; + var productCategoryName = "无"; + if (!string.IsNullOrWhiteSpace(input.ProductId)) + { + var pid = input.ProductId.Trim(); + var p = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => !x.IsDeleted && x.State && x.Id == pid); + if (p is not null) + { + productName = p.ProductName ?? string.Empty; + if (!string.IsNullOrWhiteSpace(p.CategoryId)) + { + var pc = await _dbContext.SqlSugarClient.Queryable() + .FirstAsync(x => !x.IsDeleted && x.State && x.Id == p.CategoryId); + productCategoryName = NormalizeCategoryName(pc?.CategoryName); + } + } + } + + return new UsAppLabelPreviewDto + { + LocationId = locationId, + LabelCode = labelCode, + TemplateCode = labelRow.TemplateCode, + LabelSizeText = FormatLabelSize(labelRow.TemplateWidth, labelRow.TemplateHeight, labelRow.TemplateUnit), + TypeName = labelRow.TypeName, + ProductName = string.IsNullOrWhiteSpace(productName) ? null : productName, + ProductCategoryName = productCategoryName, + LabelCategoryName = labelRow.LabelCategoryName, + PreviewImageBase64Png = null, + Template = template + }; + } + + /// + /// App 打印:创建打印任务并落库打印明细(fl_label_print_task / fl_label_print_data) + /// + /// 打印入参 + /// 任务Id + [Authorize] + [UnitOfWork] + public virtual async Task PrintAsync(UsAppLabelPrintInputVo input) + { + if (input is null) + { + throw new UserFriendlyException("入参不能为空"); + } + + var locationId = input.LocationId?.Trim(); + if (string.IsNullOrWhiteSpace(locationId)) + { + throw new UserFriendlyException("门店Id不能为空"); + } + + var labelCode = input.LabelCode?.Trim(); + if (string.IsNullOrWhiteSpace(labelCode)) + { + throw new UserFriendlyException("labelCode不能为空"); + } + + var quantity = input.PrintQuantity <= 0 ? 1 : input.PrintQuantity; + + // 校验 label + location,并补齐一些顶部字段用于任务表落库 + var labelRow = await _dbContext.SqlSugarClient + .Queryable( + (l, t, tpl) => l.LabelTypeId == t.Id && l.TemplateId == tpl.Id) + .Where((l, t, tpl) => !l.IsDeleted && l.State) + .Where((l, t, tpl) => !t.IsDeleted && t.State) + .Where((l, t, tpl) => !tpl.IsDeleted) + .Where((l, t, tpl) => l.LabelCode == labelCode) + .Select((l, t, tpl) => new + { + l.LocationId, + l.LabelTypeId, + TemplateCode = tpl.TemplateCode + }) + .FirstAsync(); + + if (labelRow is null) + { + throw new UserFriendlyException("标签不存在或不可用"); + } + + if (!string.Equals(labelRow.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase)) + { + throw new UserFriendlyException("该标签不属于当前门店"); + } + + // 解析模板 elements(与预览一致的渲染数据) + var resolvedTemplate = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo + { + LabelCode = labelCode, + ProductId = input.ProductId?.Trim(), + BaseTime = input.BaseTime, + PrintInputJson = input.PrintInputJson + }); + + var printInputJsonStr = input.PrintInputJson is null + ? null + : JsonSerializer.Serialize(input.PrintInputJson); + var renderDataJsonStr = JsonSerializer.Serialize(resolvedTemplate); + + var now = DateTime.Now; + var currentUserId = CurrentUser?.Id?.ToString(); + var taskId = _guidGenerator.Create().ToString(); + + var task = new FlLabelPrintTaskDbEntity + { + Id = taskId, + IsDeleted = false, + CreationTime = now, + CreatorId = currentUserId, + ConcurrencyStamp = _guidGenerator.Create().ToString("N"), + LocationId = locationId, + LabelCode = labelCode, + ProductId = input.ProductId?.Trim(), + LabelTypeId = labelRow.LabelTypeId, + TemplateCode = labelRow.TemplateCode, + PrintQuantity = quantity, + BaseTime = input.BaseTime, + PrinterId = input.PrinterId?.Trim(), + PrinterMac = input.PrinterMac?.Trim(), + PrinterAddress = input.PrinterAddress?.Trim() + }; + + await _dbContext.SqlSugarClient.Insertable(task).ExecuteCommandAsync(); + + var dataRows = Enumerable.Range(1, quantity).Select(i => new FlLabelPrintDataDbEntity + { + Id = _guidGenerator.Create().ToString(), + IsDeleted = false, + CreationTime = now, + CreatorId = currentUserId, + ConcurrencyStamp = _guidGenerator.Create().ToString("N"), + TaskId = taskId, + CopyIndex = i, + PrintInputJson = printInputJsonStr, + RenderDataJson = renderDataJsonStr + }).ToList(); + + await _dbContext.SqlSugarClient.Insertable(dataRows).ExecuteCommandAsync(); + + return new UsAppLabelPrintOutputDto + { + TaskId = taskId, + PrintQuantity = quantity + }; + } + + private ISugarQueryable BuildLabelingJoinQuery( + string locationId, + List productIds, + string? filterCategoryId, + string? keyword) + { + var q = _dbContext.SqlSugarClient + .Queryable() + .InnerJoin((lp, l) => lp.LabelId == l.Id) + .InnerJoin((lp, l, p) => lp.ProductId == p.Id) + .InnerJoin((lp, l, p, c) => l.LabelCategoryId == c.Id) + .InnerJoin((lp, l, p, c, t) => l.LabelTypeId == t.Id) + .InnerJoin((lp, l, p, c, t, tpl) => l.TemplateId == tpl.Id) + .LeftJoin((lp, l, p, c, t, tpl, pc) => p.CategoryId == pc.Id) + .Where((lp, l, p, c, t, tpl, pc) => productIds.Contains(p.Id)) + .Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId) + .Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State) + .Where((lp, l, p, c, t, tpl, pc) => !p.IsDeleted && p.State) + .Where((lp, l, p, c, t, tpl, pc) => !c.IsDeleted && c.State) + .Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State) + .Where((lp, l, p, c, t, tpl, pc) => !tpl.IsDeleted) + .WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId) + .WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) => + (l.LabelName != null && l.LabelName.Contains(keyword!)) || + (p.ProductName != null && p.ProductName.Contains(keyword!)) || + (pc.CategoryName != null && pc.CategoryName.Contains(keyword!)) || + (c.CategoryName != null && c.CategoryName.Contains(keyword!)) || + (t.TypeName != null && t.TypeName.Contains(keyword!)) || + (l.LabelCode != null && l.LabelCode.Contains(keyword!))); + + return q; + } + + private sealed class LabelingTreeRow + { + public string LabelCategoryId { get; set; } = string.Empty; + + public string? LabelCategoryName { get; set; } + + public string? LabelCategoryPhotoUrl { get; set; } + + public int LabelCategoryOrderNum { get; set; } + + public string? ProductCategoryId { get; set; } + + public string? ProductCategoryName { get; set; } + + public string? ProductCategoryPhotoUrl { get; set; } + + public string ProductId { get; set; } = string.Empty; + + public string? ProductName { get; set; } + + public string? ProductCode { get; set; } + + public string? ProductImageUrl { get; set; } + + public string LabelTypeId { get; set; } = string.Empty; + + public string? TypeName { get; set; } + + public int TypeOrderNum { get; set; } + + public string LabelCode { get; set; } = string.Empty; + + public string? TemplateCode { get; set; } + + public decimal TemplateWidth { get; set; } + + public decimal TemplateHeight { get; set; } + + public string TemplateUnit { get; set; } = "inch"; + } + + private static string NormalizeCategoryName(string? categoryName) + { + var s = categoryName?.Trim(); + return string.IsNullOrWhiteSpace(s) ? "无" : s; + } + + private static string? NormalizeNullableId(string? id) + { + var s = id?.Trim(); + return string.IsNullOrWhiteSpace(s) ? null : s; + } + + private static string? NormalizeNullableUrl(string? url) + { + var s = url?.Trim(); + return string.IsNullOrWhiteSpace(s) ? null : s; + } + + private static UsAppLabelTypeNodeDto BuildLabelTypeNode(LabelingTreeRow r) + { + return new UsAppLabelTypeNodeDto + { + LabelTypeId = r.LabelTypeId, + TypeName = r.TypeName ?? string.Empty, + OrderNum = r.TypeOrderNum, + LabelCode = r.LabelCode ?? string.Empty, + TemplateCode = r.TemplateCode, + LabelSizeText = FormatLabelSize(r.TemplateWidth, r.TemplateHeight, r.TemplateUnit) + }; + } + + private static string? FormatLabelSize(decimal w, decimal h, string unit) + { + var u = (unit ?? "inch").Trim().ToLowerInvariant(); + var ws = w.ToString(CultureInfo.InvariantCulture); + var hs = h.ToString(CultureInfo.InvariantCulture); + return u is "inch" or "in" + ? $"{ws}\"x{hs}\"" + : $"{ws}x{hs}{u}"; + } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs index d8ccbaa..d23327c 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; +using Microsoft.Extensions.FileProviders; using StackExchange.Redis; using Volo.Abp.AspNetCore.Auditing; using Volo.Abp.AspNetCore.Authentication.JwtBearer; @@ -397,6 +398,19 @@ namespace Yi.Abp.Web }); app.UseDirectoryBrowser("/api/app/wwwroot"); + // 类别图片静态资源(物理目录 /www/wwwroot/FoodLabelingManagementUs/picture) + // - Linux:通常可直接写入与访问 + // - Windows:如该目录不存在,则使用项目 wwwroot/FoodLabelingManagementUs/picture 作为落盘目录 + var linuxPictureRoot = "/www/wwwroot/FoodLabelingManagementUs/picture"; + var webRootPicture = Path.Combine(env.WebRootPath ?? string.Empty, "FoodLabelingManagementUs", "picture"); + var pictureRoot = Directory.Exists(linuxPictureRoot) ? linuxPictureRoot : webRootPicture; + Directory.CreateDirectory(pictureRoot); + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(pictureRoot), + RequestPath = "/picture" + }); + app.Properties.Add("_AbpExceptionHandlingMiddleware_Added", false); //工作单元 app.UseUnitOfWork(); diff --git a/项目相关文档/产品模块Categories接口对接说明.md b/项目相关文档/产品模块Categories接口对接说明.md new file mode 100644 index 0000000..37ad481 --- /dev/null +++ b/项目相关文档/产品模块Categories接口对接说明.md @@ -0,0 +1,204 @@ +# 产品模块 Categories(类别)接口对接说明(美国版) + +## 概述 + +本模块用于平台端(H5)Products → **Categories** 页签的数据对接。 + +- **模块**:`food-labeling-us` +- **接口前缀**:宿主统一前缀为 `/api/app` +- **分类表**:`fl_product_category` +- **关联字段**:`fl_product.category_id` → `fl_product_category.id` +- **图片字段**:`CategoryPhotoUrl`(前端字段:`categoryPhotoUrl`) + +> 说明:本文以 Swagger 为准(本地示例:`http://localhost:19001/swagger`,搜索 `ProductCategory`)。 + +--- + +## 接口 1:类别分页列表 + +### HTTP + +- **方法**:`GET` +- **路径**:`/api/app/product-category` +- **鉴权**:需要登录(Header:`Authorization: Bearer {token}`) + +### 入参(Query 参数) + +| 参数名 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `skipCount` | number | 是 | 跳过条数(分页) | +| `maxResultCount` | number | 是 | 每页条数(分页) | +| `sorting` | string | 否 | 排序字段(如 `OrderNum desc`),不传则按 `OrderNum desc, CreationTime desc` | +| `keyword` | string | 否 | 模糊搜索(匹配 `CategoryCode/CategoryName`) | +| `state` | boolean | 否 | 启用状态过滤 | + +### 请求示例 + +```http +GET /api/app/product-category?skipCount=0&maxResultCount=10&keyword=Prep HTTP/1.1 +Host: localhost:19001 +Authorization: Bearer eyJhbGciOi... +``` + +### 出参(PagedResultWithPageDto) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `pageIndex` | number | 当前页(从 1 开始) | +| `pageSize` | number | 每页条数 | +| `totalCount` | number | 总数 | +| `totalPages` | number | 总页数 | +| `items` | array | 当前页数据 | + +`items[]` 字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | string | 主键 | +| `categoryCode` | string | 类别编码 | +| `categoryName` | string | 类别名称 | +| `categoryPhotoUrl` | string \| null | 类别图片 URL(建议用 `/picture/...`) | +| `state` | boolean | 是否启用 | +| `orderNum` | number | 排序 | +| `lastEdited` | string | 最后编辑时间 | + +### 响应示例 + +```json +{ + "pageIndex": 1, + "pageSize": 10, + "totalCount": 1, + "totalPages": 1, + "items": [ + { + "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", + "categoryCode": "CAT_PREP", + "categoryName": "Prep", + "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", + "state": true, + "orderNum": 100, + "lastEdited": "2026-03-25 12:30:10" + } + ] +} +``` + +--- + +## 接口 2:类别详情 + +### HTTP + +- **方法**:`GET` +- **路径**:`/api/app/product-category/{id}` + +### 请求示例 + +```http +GET /api/app/product-category/a2696b9e-2277-11f1-b4c6-00163e0c7c4f HTTP/1.1 +Host: localhost:19001 +Authorization: Bearer eyJhbGciOi... +``` + +### 响应示例(ProductCategoryGetOutputDto) + +```json +{ + "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", + "categoryCode": "CAT_PREP", + "categoryName": "Prep", + "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", + "state": true, + "orderNum": 100 +} +``` + +--- + +## 接口 3:新增类别 + +### HTTP + +- **方法**:`POST` +- **路径**:`/api/app/product-category` +- **Content-Type**:`application/json` + +### 入参(Body JSON:ProductCategoryCreateInputVo) + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `categoryCode` | string | 是 | 类别编码(唯一) | +| `categoryName` | string | 是 | 类别名称(唯一) | +| `categoryPhotoUrl` | string \| null | 否 | 图片 URL(建议先上传图片拿到 `/picture/...` 再保存) | +| `state` | boolean | 否 | 是否启用(默认 true) | +| `orderNum` | number | 否 | 排序(默认 0) | + +### 请求示例 + +```json +{ + "categoryCode": "CAT_PREP", + "categoryName": "Prep", + "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", + "state": true, + "orderNum": 100 +} +``` + +--- + +## 接口 4:编辑类别 + +### HTTP + +- **方法**:`PUT` +- **路径**:`/api/app/product-category/{id}` +- **Content-Type**:`application/json` + +### 请求示例 + +```json +{ + "categoryCode": "CAT_PREP", + "categoryName": "Prep", + "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", + "state": true, + "orderNum": 100 +} +``` + +--- + +## 接口 5:删除类别(逻辑删除) + +### HTTP + +- **方法**:`DELETE` +- **路径**:`/api/app/product-category/{id}` + +### 约束 + +- 若该类别已被 `fl_label` 引用(`fl_label.LabelCategoryId = id`),删除会失败并返回友好提示:`该类别已被标签引用,无法删除`。 + +### 请求示例 + +```http +DELETE /api/app/product-category/a2696b9e-2277-11f1-b4c6-00163e0c7c4f HTTP/1.1 +Host: localhost:19001 +Authorization: Bearer eyJhbGciOi... +``` + +--- + +## 配套:类别图片上传接口 + +类别图片上传接口见文档: + +- `项目相关文档/平台端Categories图片上传接口说明.md` + +推荐前端流程: + +1. 调用上传接口 `POST /api/app/picture/category/upload` 拿到响应 `url` +2. 新增/编辑类别时把 `categoryPhotoUrl` 设为该 `url` + diff --git a/项目相关文档/平台端Categories图片上传接口说明.md b/项目相关文档/平台端Categories图片上传接口说明.md new file mode 100644 index 0000000..edeb7d7 --- /dev/null +++ b/项目相关文档/平台端Categories图片上传接口说明.md @@ -0,0 +1,102 @@ +# 平台端 Categories 图片上传接口说明 + +## 概述 + +平台端(H5)Products 模块的 **Categories** 页面,类别图片字段为 **`CategoryPhotoUrl`**(后端已在 `fl_label_category` 与分类 CRUD 接口中贯通)。 + +图片上传由 `food-labeling-us` 模块提供上传接口,文件会保存到服务器目录,并通过静态资源路径 `/picture/...` 直接访问。 + +--- + +## 接口:上传类别图片 + +### HTTP + +- **方法**:`POST` +- **路径**:`/api/app/picture/category/upload` +- **Content-Type**:`multipart/form-data` +- **鉴权**:需要登录(Header:`Authorization: Bearer {token}`) + +### 表单参数(multipart/form-data) + +| 参数名 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `file` | file | 是 | 图片文件 | +| `subDir` | string | 否 | 可选子目录(相对路径),例如 `category`、`category/2026-03`;**禁止包含 `..`** | + +### 限制 + +- **大小**:最大 5MB +- **格式**:仅支持 `jpg/jpeg/png/webp/gif` +- **文件名策略**:后端自动生成唯一文件名(避免覆盖) + +### 请求示例(curl) + +Windows(PowerShell/命令行注意路径转义): + +```bash +curl -X POST "http://localhost:19001/api/app/picture/category/upload" ^ + -H "Authorization: Bearer " ^ + -F "file=@C:\\tmp\\category.png" ^ + -F "subDir=category" +``` + +### 请求示例(Postman/Apifox) + +- 选择 `POST` +- URL:`http://localhost:19001/api/app/picture/category/upload` +- Headers:`Authorization: Bearer ` +- Body:`form-data` + - Key=`file`,类型选 `File`,选择图片文件 + - Key=`subDir`,类型 `Text`,填 `category`(可选) + +### 响应体(PictureUploadOutputDto) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `url` | string | 图片访问的相对路径,可直接保存到 `CategoryPhotoUrl`(例如:`/picture/category/xxx.png`) | +| `fileName` | string | 服务器保存的文件名 | +| `size` | number | 文件大小(字节) | + +### 响应示例 + +```json +{ + "url": "/picture/category/20260325123010_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.png", + "fileName": "20260325123010_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.png", + "size": 123456 +} +``` + +--- + +## 文件保存位置与访问方式 + +### 落盘目录 + +后端会按环境自动选择落盘目录(不存在会自动创建): + +- 优先:`/www/wwwroot/FoodLabelingManagementUs/picture` +- 否则(Windows 本地开发):`<项目根>/wwwroot/FoodLabelingManagementUs/picture` + +### 访问 URL + +静态资源映射为: + +- `GET /picture/{subDir}/{fileName}` + +举例: + +- `http://localhost:19001/picture/category/20260325123010_xxx.png` + +--- + +## 如何写入 Categories(CategoryPhotoUrl) + +推荐前端流程: + +1. 调用本上传接口,拿到返回的 `url` +2. 再调用分类新增/编辑接口,把 `categoryPhotoUrl` 设置为该 `url` + +> 说明:分类 CRUD 已支持 `CategoryPhotoUrl` 字段;你只需要在页面表单里新增该字段即可。 + diff --git a/项目相关文档/标签模块接口对接说明.md b/项目相关文档/标签模块接口对接说明.md index 5707218..bbcdd08 100644 --- a/项目相关文档/标签模块接口对接说明.md +++ b/项目相关文档/标签模块接口对接说明.md @@ -6,7 +6,7 @@ Swagger 地址: - `http://localhost:19001/swagger` 说明: -- 接口最终 URL 以 Swagger 展示为准(可在 Swagger 里搜索 `LabelCategory / LabelType / LabelMultipleOption / LabelTemplate / Label`)。 +- 接口最终 URL 以 Swagger 展示为准(可在 Swagger 里搜索 `LabelCategory / LabelType / LabelMultipleOption / LabelTemplate / Label / UsAppLabeling`)。 - 本模块后端接口以各 AppService 的方法签名自动暴露。 - 返回分页统一包含 `PageIndex / PageSize / TotalCount / TotalPages / Items`。 @@ -22,6 +22,7 @@ Swagger 地址: - `label-multiple-option` - `label-template` - `label` + - `us-app-labeling`(App 端 Labeling 四级树) --- @@ -355,7 +356,8 @@ Swagger 地址: ## 接口 5:Labels(按产品展示多个标签) 说明: -- 列表接口按 `ProductId` 查询,一个产品会对应多条标签记录。 +- 列表接口以“标签”为维度分页展示(同一个标签会绑定多个产品)。 +- 列表支持按 `ProductId` 过滤:仅返回“绑定了该产品”的标签。 - 标签详情/编辑/删除的 `id` 使用 `fl_label.LabelCode`。 ### 5.1 分页列表(按产品) @@ -379,6 +381,11 @@ Swagger 地址: } ``` +列表出参要点(`LabelGetListOutputDto`): + +- `products`:同一个标签下绑定的产品名称,用 `,` 分割(例如:`Chicken,Sandwich`) +- 其他字段与之前一致:`labelName/locationName/category/type/template/state/lastEdited...` + ### 5.2 详情 方法:`GET /api/app/label/{id}` @@ -582,3 +589,297 @@ Swagger 地址: 入参: - `id`:门店Id +--- + +## 接口 8:App Labeling 四级列表(门店打标页) + +**场景**:美国版 UniApp「Labeling」页:左侧 **标签分类(Label Category)** → 主区域按 **产品分类(Product Category)** 折叠分组 → **产品(Product)** 卡片 → 点选后底部弹层展示 **标签种类(Label Type)**。 + +**实现**:`UsAppLabelingAppService.GetLabelingTreeAsync`,约定式 API 控制器名 **`us-app-labeling`**(与 `UsAppAuth` → `us-app-auth` 同规则)。 + +### 8.1 获取四级嵌套树 + +#### HTTP + +- **方法**:`GET` +- **路径**:`/api/app/us-app-labeling/labeling-tree`(若与 Swagger 不一致,**以 Swagger 为准**) +- **鉴权**:需要登录(`Authorization: Bearer ...`);可使用 App 登录或 Web 账号 Token,需能通过 `[Authorize]`。当前用户可选门店列表见 **`/api/app/us-app-auth/my-locations`**(说明见 `美国版App登录接口说明.md`)。 + +#### 入参(Query:`UsAppLabelingTreeInputVo`) + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| `locationId` | string | 是 | 当前门店 Id(`location.Id`,与 `fl_location_product.LocationId`、`fl_label.LocationId` 一致) | +| `keyword` | string | 否 | 模糊过滤:标签名、产品名、产品分类、**标签分类**名、标签类型名、`labelCode` 等(实现见服务内 `WhereIF`) | +| `labelCategoryId` | string | 否 | 侧边栏只展示某一 **标签分类** 时传入;不传则返回当前门店下出现的全部标签分类节点 | + +#### 数据范围与表关联(便于联调对照) + +- **门店内商品**:`fl_location_product`(先有 `locationId` 下的 `productId` 集合)。 +- **参与连接的表**:`fl_label_product`(标签-产品)、`fl_label`(`LocationId` 须为当前门店且未删除、启用)、`fl_product`、`fl_label_category`、`fl_label_type`、`fl_label_template`。 +- **第二级「产品分类」**:来自 `fl_product.CategoryName`,trim 后为空则归并为显示名 **`无`**。 +- **第四级去重**:同一产品在同一标签分类、同一门店下,多条 `fl_label` 若 **`labelCode` 相同**,只保留一条用于列表(预览/打印仍用返回的 `labelCode` 等业务字段)。 + +#### 出参(`List`) + +若宿主对成功结果有统一包装,业务数组一般在 **`data`** 中;下列为 **解包后的数组项** 结构。 + +**L1 `UsAppLabelCategoryTreeNodeDto`(标签分类)** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | string | `fl_label_category.Id` | +| `categoryName` | string | 分类名称 | +| `categoryPhotoUrl` | string \| null | 分类图标/图 | +| `orderNum` | number | 排序 | +| `productCategories` | array | 第二级列表(见下表) | + +**L2 `UsAppProductCategoryNodeDto`(产品分类)** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 | +| `categoryPhotoUrl` | string \| null | 产品分类图片地址;产品未归类或分类不存在时为空 | +| `name` | string | 产品分类显示名;空源数据为 **`无`** | +| `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) | +| `products` | array | 第三级产品列表(见下表) | + +**L3 `UsAppLabelingProductNodeDto`(产品)** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `productId` | string | `fl_product.Id` | +| `productName` | string | 产品名称 | +| `productCode` | string | 产品编码 | +| `productImageUrl` | string \| null | 主图 | +| `subtitle` | string | 卡片副标题:**有 `productCode` 则显示编码,否则「无」**(与原型「Basic」等独立文案不同,需另行扩展字段时再对齐) | +| `labelTypeCount` | number | 第四级条数,可用于角标「N Types」 | +| `labelTypes` | array | 第四级(见下表) | + +**L4 `UsAppLabelTypeNodeDto`(标签种类 / 可选项)** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `labelTypeId` | string | `fl_label_type.Id` | +| `typeName` | string | 类型名称(如 Defrost) | +| `orderNum` | number | 排序 | +| `labelCode` | string | 业务标签编码,后续预览、打印流程使用 | +| `templateCode` | string \| null | 关联模板编码 | +| `labelSizeText` | string \| null | 尺寸文案;`inch` 常用格式如 `2"x2"` | + +#### 错误与边界 + +- `locationId` 为空:返回友好错误 **「门店Id不能为空」**。 +- 门店下无关联产品:返回 **空数组** `[]`。 +- 有产品但无任何符合条件的标签关联:返回 **空数组** `[]`。 + +#### 请求示例 + +```http +GET /api/app/us-app-labeling/labeling-tree?locationId=11111111-1111-1111-1111-111111111111&labelCategoryId=a2696b9e-2277-11f1-b4c6-00163e0c7c4f&keyword=Chicken HTTP/1.1 +Host: localhost:19001 +Authorization: Bearer eyJhbGciOi... +``` + +**curl**(Token 取自登录响应的 `data.token` 整段,已含 `Bearer ` 前缀时直接放入 Header): + +```bash +curl -X GET "http://localhost:19001/api/app/us-app-labeling/labeling-tree?locationId=11111111-1111-1111-1111-111111111111" \ + -H "Authorization: " +``` + +#### 响应结构示例(解包后) + +```json +[ + { + "id": "cat-prep-id", + "categoryName": "Prep", + "categoryPhotoUrl": "/picture/...", + "orderNum": 1, + "productCategories": [ + { + "categoryId": "pc-meat-id", + "categoryPhotoUrl": "/picture/product-category/20260325123010_xxx.png", + "name": "Meat", + "itemCount": 1, + "products": [ + { + "productId": "prod-chicken-id", + "productName": "Chicken", + "productCode": "CHK-001", + "productImageUrl": "/picture/...", + "subtitle": "CHK-001", + "labelTypeCount": 3, + "labelTypes": [ + { + "labelTypeId": "lt-defrost", + "typeName": "Defrost", + "orderNum": 1, + "labelCode": "LBL_CHICKEN_DEFROST", + "templateCode": "TPL_2X2", + "labelSizeText": "2\"x2\"" + } + ] + } + ] + } + ] + } +] +``` + +> 前端 Axios 若项目约定 **GET 使用 `data` 配置对象** 传参,请仍绑定到与上述 Query 同名的字段(`locationId`、`keyword`、`labelCategoryId`),与 URL Query 等价即可。 + +### 8.2 App 打印预览(elements 渲染结构) + +**场景**:用户选择某个 Product + Label Type 进入「Label Preview」页面,需要把模板预览区域渲染出来。 +后端根据 `labelCode` 读取模板(`fl_label_template` + `fl_label_template_element`),并将 AUTO_DB / PRINT_INPUT 的值渲染回每个 element 的 `config`,前端按 `elements` 自行绘制预览。 + +#### HTTP + +- **方法**:`POST` +- **路径**:`/api/app/us-app-labeling/preview`(若与 Swagger 不一致,**以 Swagger 为准**) +- **鉴权**:需要登录(`Authorization: Bearer ...`) + +#### 入参(Body:`UsAppLabelPreviewInputVo`) + +| 参数名(JSON) | 类型 | 必填 | 说明 | +|---|---|---|---| +| `locationId` | string | 是 | 门店Id(校验 `fl_label.LocationId` 必须一致) | +| `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) | +| `productId` | string | 否 | 预览用产品Id;不传则默认取该标签绑定的第一个产品(用于 AUTO_DB 数据填充) | +| `baseTime` | string | 否 | 业务基准时间(用于 DATE/TIME 元素计算;不传则用服务器当前时间) | +| `printInputJson` | object | 否 | 打印输入(用于 PRINT_INPUT 元素),key 建议与模板元素 `inputKey` 对齐 | + +#### 出参(`UsAppLabelPreviewDto`) + +除顶部信息外,核心是 `template`(供前端画布渲染): + +- `template`:`LabelTemplatePreviewDto` + - `width` / `height` / `unit`:模板物理尺寸 + - `elements[]`:元素数组(对齐前端 editor JSON:`id/type/x/y/width/height/rotation/border/zIndex/orderNum/config`) + +`elements[].config` 内常用字段(示例): + +- 文本类(如 `TEXT_PRODUCT` / `TEXT_STATIC` / `TEXT_PRICE`):`config.text` +- 条码/二维码(`BARCODE` / `QRCODE`):`config.data` +- 日期/时间(`DATE` / `TIME`):`config.format`(后端已计算并写回) + +#### 数据来源说明 + +- 模板头:`fl_label_template` +- 模板元素:`fl_label_template_element`(按 `OrderNum` + `ZIndex` 排序) +- 标签归属:`fl_label`(校验 `labelCode` 存在且 `LocationId == locationId`) + +#### 错误与边界 + +- `locationId` 为空:友好错误 **「门店Id不能为空」**。 +- `labelCode` 为空:友好错误 **「labelCode不能为空」**。 +- 标签不存在:友好错误 **「标签不存在」**。 +- 模板不存在:友好错误 **「模板不存在」**。 +- 标签不属于当前门店:友好错误 **「该标签不属于当前门店」**。 +- 标签未绑定产品且未传 `productId`:友好错误 **「该标签未绑定产品,无法预览」**。 + +#### 请求示例 + +```json +{ + "locationId": "11111111-1111-1111-1111-111111111111", + "labelCode": "LBL_CHICKEN_DEFROST", + "productId": "22222222-2222-2222-2222-222222222222", + "baseTime": "2026-03-26T10:30:00", + "printInputJson": { + "price": "12.99" + } +} +``` + +**curl:** + +```bash +curl -X POST "http://localhost:19001/api/app/us-app-labeling/preview" \ + -H "Authorization: " \ + -H "Content-Type: application/json" \ + -d '{"locationId":"11111111-1111-1111-1111-111111111111","labelCode":"LBL_CHICKEN_DEFROST","productId":"22222222-2222-2222-2222-222222222222","baseTime":"2026-03-26T10:30:00","printInputJson":{"price":"12.99"}}' +``` + +--- + +## 接口 9:App 打印(落库打印任务与明细) + +**场景**:移动端预览确认后点击 **Print**。后端负责把“本次打印”写入数据库,方便追溯/统计/重打。 + +### 9.1 创建打印任务并写入明细 + +#### HTTP + +- **方法**:`POST` +- **路径**:`/api/app/us-app-labeling/print`(若与 Swagger 不一致,**以 Swagger 为准**) +- **鉴权**:需要登录(`Authorization: Bearer ...`) + +#### 入参(Body:`UsAppLabelPrintInputVo`) + +| 参数名(JSON) | 类型 | 必填 | 说明 | +|---|---|---|---| +| `locationId` | string | 是 | 门店Id(校验 `fl_label.LocationId` 必须一致) | +| `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) | +| `productId` | string | 否 | 打印用产品Id;不传则默认取该标签绑定的第一个产品(用于模板解析) | +| `printQuantity` | number | 否 | 打印份数;`<=0` 按 1 处理 | +| `baseTime` | string | 否 | 业务基准时间(用于 DATE/TIME 元素计算) | +| `printInputJson` | object | 否 | 打印输入(用于模板 PRINT_INPUT 元素),key 建议与模板元素 `inputKey` 对齐 | +| `printerId` | string | 否 | 打印机Id(可选,用于追踪) | +| `printerMac` | string | 否 | 打印机蓝牙 MAC(可选) | +| `printerAddress` | string | 否 | 打印机地址(可选) | + +#### 数据落库说明 + +- **任务表**:`fl_label_print_task` + - 插入 1 条任务记录(`locationId / labelCode / productId / labelTypeId / templateCode / printQuantity / baseTime / printer...` 等)。 +- **明细表**:`fl_label_print_data` + - 按 `printQuantity` 插入 N 条明细记录(`copyIndex = 1..N`)。 + - `printInputJson`:保存本次打印的原始输入(JSON 字符串)。 + - `renderDataJson`:保存本次解析后的模板预览结构(`LabelTemplatePreviewDto`,包含 resolved 后的 `elements[].config`),供追溯/重打使用。 + +> 模板解析的数据源来自 `fl_label_template` + `fl_label_template_element`,与预览接口一致。 + +#### 出参(`UsAppLabelPrintOutputDto`) + +| 字段 | 类型 | 说明 | +|---|---|---| +| `taskId` | string | 打印任务Id(用于后续查询/重打/统计) | +| `printQuantity` | number | 实际写入的份数 | + +#### 错误与边界 + +- `locationId` 为空:友好错误 **「门店Id不能为空」**。 +- `labelCode` 为空:友好错误 **「labelCode不能为空」**。 +- 标签不存在/不可用:友好错误 **「标签不存在或不可用」**。 +- 标签不属于当前门店:友好错误 **「该标签不属于当前门店」**。 +- 标签未绑定产品且未传 `productId`:友好错误 **「该标签未绑定产品,无法预览」**(模板解析阶段抛出)。 + +#### 请求示例 + +```json +{ + "locationId": "11111111-1111-1111-1111-111111111111", + "labelCode": "LBL_CHICKEN_DEFROST", + "productId": "22222222-2222-2222-2222-222222222222", + "printQuantity": 2, + "baseTime": "2026-03-26T10:30:00", + "printInputJson": { + "price": "12.99" + }, + "printerMac": "AA:BB:CC:DD:EE:FF" +} +``` + +**curl:** + +```bash +curl -X POST "http://localhost:19001/api/app/us-app-labeling/print" \ + -H "Authorization: " \ + -H "Content-Type: application/json" \ + -d '{"locationId":"11111111-1111-1111-1111-111111111111","labelCode":"LBL_CHICKEN_DEFROST","productId":"22222222-2222-2222-2222-222222222222","printQuantity":2,"baseTime":"2026-03-26T10:30:00","printInputJson":{"price":"12.99"}}' +``` +