Commit 536d25c4ac2797d8a0ab3c8538dfba7aad187d77
1 parent
28dc179d
打印预览,产品分类接口实现
Showing
24 changed files
with
1346 additions
and
43 deletions
美国版/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 | @@ -16,9 +16,15 @@ public class LabelPreviewResolveInputVo | ||
| 16 | public string? ProductId { get; set; } | 16 | public string? ProductId { get; set; } |
| 17 | 17 | ||
| 18 | /// <summary> | 18 | /// <summary> |
| 19 | + /// 业务基准时间(用于 DATE/TIME 元素的渲染计算) | ||
| 20 | + /// 不传则默认使用服务器当前时间 | ||
| 21 | + /// </summary> | ||
| 22 | + public DateTime? BaseTime { get; set; } | ||
| 23 | + | ||
| 24 | + /// <summary> | ||
| 19 | /// 打印输入(前端传,用于 PRINT_INPUT 元素) | 25 | /// 打印输入(前端传,用于 PRINT_INPUT 元素) |
| 20 | /// key 建议使用模板元素的 InputKey | 26 | /// key 建议使用模板元素的 InputKey |
| 21 | /// </summary> | 27 | /// </summary> |
| 22 | - public Dictionary<string, object>? PrintInputJson { get; set; } | 28 | + public Dictionary<string, object?>? PrintInputJson { get; set; } |
| 23 | } | 29 | } |
| 24 | 30 |
美国版/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 | @@ -6,7 +6,7 @@ public class ProductCreateInputVo | ||
| 6 | 6 | ||
| 7 | public string ProductName { get; set; } = string.Empty; | 7 | public string ProductName { get; set; } = string.Empty; |
| 8 | 8 | ||
| 9 | - public string? CategoryName { get; set; } | 9 | + public string? CategoryId { get; set; } |
| 10 | 10 | ||
| 11 | public string? ProductImageUrl { get; set; } | 11 | public string? ProductImageUrl { get; set; } |
| 12 | 12 |
美国版/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 | @@ -8,6 +8,8 @@ public class ProductGetListOutputDto | ||
| 8 | 8 | ||
| 9 | public string ProductName { get; set; } = string.Empty; | 9 | public string ProductName { get; set; } = string.Empty; |
| 10 | 10 | ||
| 11 | + public string? CategoryId { get; set; } | ||
| 12 | + | ||
| 11 | public string? CategoryName { get; set; } | 13 | public string? CategoryName { get; set; } |
| 12 | 14 | ||
| 13 | public string? ProductImageUrl { get; set; } | 15 | public string? ProductImageUrl { get; set; } |
美国版/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 | @@ -10,6 +10,8 @@ public class ProductGetOutputDto | ||
| 10 | 10 | ||
| 11 | public string ProductName { get; set; } = string.Empty; | 11 | public string ProductName { get; set; } = string.Empty; |
| 12 | 12 | ||
| 13 | + public string? CategoryId { get; set; } | ||
| 14 | + | ||
| 13 | public string? CategoryName { get; set; } | 15 | public string? CategoryName { get; set; } |
| 14 | 16 | ||
| 15 | public string? ProductImageUrl { get; set; } | 17 | public string? ProductImageUrl { get; set; } |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelCategoryTreeNodeDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | ||
| 2 | + | ||
| 3 | +/// <summary> | ||
| 4 | +/// 第一级:标签分类(fl_label_category) | ||
| 5 | +/// </summary> | ||
| 6 | +public class UsAppLabelCategoryTreeNodeDto | ||
| 7 | +{ | ||
| 8 | + public string Id { get; set; } = string.Empty; | ||
| 9 | + | ||
| 10 | + public string CategoryName { get; set; } = string.Empty; | ||
| 11 | + | ||
| 12 | + public string? CategoryPhotoUrl { get; set; } | ||
| 13 | + | ||
| 14 | + public int OrderNum { get; set; } | ||
| 15 | + | ||
| 16 | + public List<UsAppProductCategoryNodeDto> ProductCategories { get; set; } = new(); | ||
| 17 | +} |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPreviewDto.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Label; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | ||
| 4 | + | ||
| 5 | +/// <summary> | ||
| 6 | +/// App 标签预览出参(顶部信息 + 预览模板结构) | ||
| 7 | +/// </summary> | ||
| 8 | +public class UsAppLabelPreviewDto | ||
| 9 | +{ | ||
| 10 | + public string LocationId { get; set; } = string.Empty; | ||
| 11 | + | ||
| 12 | + public string LabelCode { get; set; } = string.Empty; | ||
| 13 | + | ||
| 14 | + public string? TemplateCode { get; set; } | ||
| 15 | + | ||
| 16 | + public string? LabelSizeText { get; set; } | ||
| 17 | + | ||
| 18 | + public string? TypeName { get; set; } | ||
| 19 | + | ||
| 20 | + public string? ProductName { get; set; } | ||
| 21 | + | ||
| 22 | + public string? ProductCategoryName { get; set; } | ||
| 23 | + | ||
| 24 | + public string? LabelCategoryName { get; set; } | ||
| 25 | + | ||
| 26 | + /// <summary> | ||
| 27 | + /// 预览图(base64 png,可空;若为空,客户端可用 Template 自行渲染) | ||
| 28 | + /// </summary> | ||
| 29 | + public string? PreviewImageBase64Png { get; set; } | ||
| 30 | + | ||
| 31 | + /// <summary> | ||
| 32 | + /// 预览模板结构(与 LabelCanvas/LabelPreviewOnly 结构尽量一致) | ||
| 33 | + /// </summary> | ||
| 34 | + public LabelTemplatePreviewDto Template { get; set; } = new(); | ||
| 35 | +} | ||
| 36 | + |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPreviewInputVo.cs
0 → 100644
| 1 | +using System; | ||
| 2 | +using System.Collections.Generic; | ||
| 3 | + | ||
| 4 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | ||
| 5 | + | ||
| 6 | +/// <summary> | ||
| 7 | +/// App 标签预览入参 | ||
| 8 | +/// </summary> | ||
| 9 | +public class UsAppLabelPreviewInputVo | ||
| 10 | +{ | ||
| 11 | + /// <summary> | ||
| 12 | + /// 门店Id(fl_label.LocationId) | ||
| 13 | + /// </summary> | ||
| 14 | + public string LocationId { get; set; } = string.Empty; | ||
| 15 | + | ||
| 16 | + /// <summary> | ||
| 17 | + /// 标签编码(fl_label.LabelCode) | ||
| 18 | + /// </summary> | ||
| 19 | + public string LabelCode { get; set; } = string.Empty; | ||
| 20 | + | ||
| 21 | + /// <summary> | ||
| 22 | + /// 选择用于预览的产品Id(fl_product.Id) | ||
| 23 | + /// 不传则默认取该标签绑定的第一个产品 | ||
| 24 | + /// </summary> | ||
| 25 | + public string? ProductId { get; set; } | ||
| 26 | + | ||
| 27 | + /// <summary> | ||
| 28 | + /// 业务基准时间(用于 DATE/TIME 等元素的计算) | ||
| 29 | + /// </summary> | ||
| 30 | + public DateTime? BaseTime { get; set; } | ||
| 31 | + | ||
| 32 | + /// <summary> | ||
| 33 | + /// 打印输入(用于 PRINT_INPUT 元素) | ||
| 34 | + /// </summary> | ||
| 35 | + public Dictionary<string, object?>? PrintInputJson { get; set; } | ||
| 36 | +} | ||
| 37 | + |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintInputVo.cs
0 → 100644
| 1 | +using System; | ||
| 2 | +using System.Collections.Generic; | ||
| 3 | + | ||
| 4 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | ||
| 5 | + | ||
| 6 | +/// <summary> | ||
| 7 | +/// App 打印入参 | ||
| 8 | +/// </summary> | ||
| 9 | +public class UsAppLabelPrintInputVo | ||
| 10 | +{ | ||
| 11 | + /// <summary> | ||
| 12 | + /// 门店Id(fl_label.LocationId) | ||
| 13 | + /// </summary> | ||
| 14 | + public string LocationId { get; set; } = string.Empty; | ||
| 15 | + | ||
| 16 | + /// <summary> | ||
| 17 | + /// 标签编码(fl_label.LabelCode) | ||
| 18 | + /// </summary> | ||
| 19 | + public string LabelCode { get; set; } = string.Empty; | ||
| 20 | + | ||
| 21 | + /// <summary> | ||
| 22 | + /// 选择用于打印的产品Id(fl_product.Id) | ||
| 23 | + /// 不传则默认取该标签绑定的第一个产品 | ||
| 24 | + /// </summary> | ||
| 25 | + public string? ProductId { get; set; } | ||
| 26 | + | ||
| 27 | + /// <summary> | ||
| 28 | + /// 打印份数(<=0 则按 1 处理) | ||
| 29 | + /// </summary> | ||
| 30 | + public int PrintQuantity { get; set; } = 1; | ||
| 31 | + | ||
| 32 | + /// <summary> | ||
| 33 | + /// 业务基准时间(用于 DATE/TIME 等元素的计算) | ||
| 34 | + /// </summary> | ||
| 35 | + public DateTime? BaseTime { get; set; } | ||
| 36 | + | ||
| 37 | + /// <summary> | ||
| 38 | + /// 打印输入(用于 PRINT_INPUT 元素) | ||
| 39 | + /// </summary> | ||
| 40 | + public Dictionary<string, object?>? PrintInputJson { get; set; } | ||
| 41 | + | ||
| 42 | + /// <summary> | ||
| 43 | + /// 打印机Id(可选,若业务需要追踪) | ||
| 44 | + /// </summary> | ||
| 45 | + public string? PrinterId { get; set; } | ||
| 46 | + | ||
| 47 | + /// <summary> | ||
| 48 | + /// 打印机蓝牙 MAC(可选) | ||
| 49 | + /// </summary> | ||
| 50 | + public string? PrinterMac { get; set; } | ||
| 51 | + | ||
| 52 | + /// <summary> | ||
| 53 | + /// 打印机地址(可选) | ||
| 54 | + /// </summary> | ||
| 55 | + public string? PrinterAddress { get; set; } | ||
| 56 | +} | ||
| 57 | + |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintOutputDto.cs
0 → 100644
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelTypeNodeDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | ||
| 2 | + | ||
| 3 | +/// <summary> | ||
| 4 | +/// 第四级:标签种类(对应一条可打印的标签实例) | ||
| 5 | +/// </summary> | ||
| 6 | +public class UsAppLabelTypeNodeDto | ||
| 7 | +{ | ||
| 8 | + public string LabelTypeId { get; set; } = string.Empty; | ||
| 9 | + | ||
| 10 | + public string TypeName { get; set; } = string.Empty; | ||
| 11 | + | ||
| 12 | + public int OrderNum { get; set; } | ||
| 13 | + | ||
| 14 | + /// <summary>业务标签编码,预览/打印流程使用</summary> | ||
| 15 | + public string LabelCode { get; set; } = string.Empty; | ||
| 16 | + | ||
| 17 | + public string? TemplateCode { get; set; } | ||
| 18 | + | ||
| 19 | + /// <summary>模板物理尺寸描述,如 2"x2"</summary> | ||
| 20 | + public string? LabelSizeText { get; set; } | ||
| 21 | +} |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingProductNodeDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | ||
| 2 | + | ||
| 3 | +/// <summary> | ||
| 4 | +/// 第三级:产品 | ||
| 5 | +/// </summary> | ||
| 6 | +public class UsAppLabelingProductNodeDto | ||
| 7 | +{ | ||
| 8 | + public string ProductId { get; set; } = string.Empty; | ||
| 9 | + | ||
| 10 | + public string ProductName { get; set; } = string.Empty; | ||
| 11 | + | ||
| 12 | + public string ProductCode { get; set; } = string.Empty; | ||
| 13 | + | ||
| 14 | + public string? ProductImageUrl { get; set; } | ||
| 15 | + | ||
| 16 | + /// <summary>副标题(无独立业务字段时:有编码显示编码,否则「无」)</summary> | ||
| 17 | + public string Subtitle { get; set; } = string.Empty; | ||
| 18 | + | ||
| 19 | + public int LabelTypeCount { get; set; } | ||
| 20 | + | ||
| 21 | + /// <summary>第四级:该产品在当前标签分类+门店下可选的标签种类</summary> | ||
| 22 | + public List<UsAppLabelTypeNodeDto> LabelTypes { get; set; } = new(); | ||
| 23 | +} |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingTreeInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | ||
| 2 | + | ||
| 3 | +/// <summary> | ||
| 4 | +/// App Labeling 四级列表入参 | ||
| 5 | +/// </summary> | ||
| 6 | +public class UsAppLabelingTreeInputVo | ||
| 7 | +{ | ||
| 8 | + /// <summary>当前门店 Id(location.Id,Guid 字符串)</summary> | ||
| 9 | + public string LocationId { get; set; } = string.Empty; | ||
| 10 | + | ||
| 11 | + /// <summary>关键词(匹配标签分类/产品分类/产品名/标签类型/标签名称)</summary> | ||
| 12 | + public string? Keyword { get; set; } | ||
| 13 | + | ||
| 14 | + /// <summary>仅展示某一标签分类(侧边栏选中时传);不传则返回全部分类</summary> | ||
| 15 | + public string? LabelCategoryId { get; set; } | ||
| 16 | +} |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppProductCategoryNodeDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | ||
| 2 | + | ||
| 3 | +/// <summary> | ||
| 4 | +/// 第二级:产品分类(fl_product.CategoryId join fl_product_category) | ||
| 5 | +/// </summary> | ||
| 6 | +public class UsAppProductCategoryNodeDto | ||
| 7 | +{ | ||
| 8 | + /// <summary>产品分类Id;当产品未归类或分类不存在时为空</summary> | ||
| 9 | + public string? CategoryId { get; set; } | ||
| 10 | + | ||
| 11 | + /// <summary>产品分类图片地址;当产品未归类或分类不存在时为空</summary> | ||
| 12 | + public string? CategoryPhotoUrl { get; set; } | ||
| 13 | + | ||
| 14 | + /// <summary>分类显示名;空为「无」</summary> | ||
| 15 | + public string Name { get; set; } = string.Empty; | ||
| 16 | + | ||
| 17 | + public int ItemCount { get; set; } | ||
| 18 | + | ||
| 19 | + public List<UsAppLabelingProductNodeDto> Products { get; set; } = new(); | ||
| 20 | +} |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | ||
| 2 | +using Volo.Abp.Application.Services; | ||
| 3 | + | ||
| 4 | +namespace FoodLabeling.Application.Contracts.IServices; | ||
| 5 | + | ||
| 6 | +/// <summary> | ||
| 7 | +/// App Labeling:四级列表(标签分类 → 产品分类 → 产品 → 标签种类) | ||
| 8 | +/// </summary> | ||
| 9 | +public interface IUsAppLabelingAppService : IApplicationService | ||
| 10 | +{ | ||
| 11 | + /// <summary> | ||
| 12 | + /// 获取当前门店下四级嵌套树,供移动端 Labeling 首页使用 | ||
| 13 | + /// </summary> | ||
| 14 | + Task<List<UsAppLabelCategoryTreeNodeDto>> GetLabelingTreeAsync(UsAppLabelingTreeInputVo input); | ||
| 15 | + | ||
| 16 | + /// <summary> | ||
| 17 | + /// App 打印预览:按标签编码解析模板并返回顶部展示字段 + 预览模板结构 | ||
| 18 | + /// </summary> | ||
| 19 | + Task<UsAppLabelPreviewDto> PreviewAsync(UsAppLabelPreviewInputVo input); | ||
| 20 | + | ||
| 21 | + /// <summary> | ||
| 22 | + /// App 打印:创建打印任务并落库打印明细(fl_label_print_task / fl_label_print_data) | ||
| 23 | + /// </summary> | ||
| 24 | + Task<UsAppLabelPrintOutputDto> PrintAsync(UsAppLabelPrintInputVo input); | ||
| 25 | +} |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelPrintDataDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | ||
| 4 | + | ||
| 5 | +/// <summary> | ||
| 6 | +/// 标签打印数据明细(对应表:fl_label_print_data) | ||
| 7 | +/// </summary> | ||
| 8 | +[SugarTable("fl_label_print_data")] | ||
| 9 | +public class FlLabelPrintDataDbEntity | ||
| 10 | +{ | ||
| 11 | + [SugarColumn(IsPrimaryKey = true)] | ||
| 12 | + public string Id { get; set; } = string.Empty; | ||
| 13 | + | ||
| 14 | + public bool IsDeleted { get; set; } | ||
| 15 | + | ||
| 16 | + public DateTime CreationTime { get; set; } | ||
| 17 | + | ||
| 18 | + public string? CreatorId { get; set; } | ||
| 19 | + | ||
| 20 | + public string? LastModifierId { get; set; } | ||
| 21 | + | ||
| 22 | + public DateTime? LastModificationTime { get; set; } | ||
| 23 | + | ||
| 24 | + public string ConcurrencyStamp { get; set; } = string.Empty; | ||
| 25 | + | ||
| 26 | + public string TaskId { get; set; } = string.Empty; | ||
| 27 | + | ||
| 28 | + public int? CopyIndex { get; set; } | ||
| 29 | + | ||
| 30 | + /// <summary> | ||
| 31 | + /// 原始打印输入(json 字段,直接保存为字符串) | ||
| 32 | + /// </summary> | ||
| 33 | + public string? PrintInputJson { get; set; } | ||
| 34 | + | ||
| 35 | + /// <summary> | ||
| 36 | + /// 解析后的可打印数据(建议保存为 json 字符串) | ||
| 37 | + /// </summary> | ||
| 38 | + public string? RenderDataJson { get; set; } | ||
| 39 | +} | ||
| 40 | + |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelPrintTaskDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | ||
| 4 | + | ||
| 5 | +/// <summary> | ||
| 6 | +/// 标签打印任务(对应表:fl_label_print_task) | ||
| 7 | +/// </summary> | ||
| 8 | +[SugarTable("fl_label_print_task")] | ||
| 9 | +public class FlLabelPrintTaskDbEntity | ||
| 10 | +{ | ||
| 11 | + [SugarColumn(IsPrimaryKey = true)] | ||
| 12 | + public string Id { get; set; } = string.Empty; | ||
| 13 | + | ||
| 14 | + public bool IsDeleted { get; set; } | ||
| 15 | + | ||
| 16 | + public DateTime CreationTime { get; set; } | ||
| 17 | + | ||
| 18 | + public string? CreatorId { get; set; } | ||
| 19 | + | ||
| 20 | + public string? LastModifierId { get; set; } | ||
| 21 | + | ||
| 22 | + public DateTime? LastModificationTime { get; set; } | ||
| 23 | + | ||
| 24 | + public string ConcurrencyStamp { get; set; } = string.Empty; | ||
| 25 | + | ||
| 26 | + public string? LocationId { get; set; } | ||
| 27 | + | ||
| 28 | + public string? LabelCode { get; set; } | ||
| 29 | + | ||
| 30 | + public string? ProductId { get; set; } | ||
| 31 | + | ||
| 32 | + public string? LabelTypeId { get; set; } | ||
| 33 | + | ||
| 34 | + public string? TemplateCode { get; set; } | ||
| 35 | + | ||
| 36 | + public int PrintQuantity { get; set; } | ||
| 37 | + | ||
| 38 | + public DateTime? BaseTime { get; set; } | ||
| 39 | + | ||
| 40 | + public string? PrinterId { get; set; } | ||
| 41 | + | ||
| 42 | + public string? PrinterMac { get; set; } | ||
| 43 | + | ||
| 44 | + public string? PrinterAddress { get; set; } | ||
| 45 | +} | ||
| 46 | + |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | ||
| 4 | + | ||
| 5 | +[SugarTable("fl_product_category")] | ||
| 6 | +public class FlProductCategoryDbEntity | ||
| 7 | +{ | ||
| 8 | + [SugarColumn(IsPrimaryKey = true)] | ||
| 9 | + public string Id { get; set; } = string.Empty; | ||
| 10 | + | ||
| 11 | + public bool IsDeleted { get; set; } | ||
| 12 | + | ||
| 13 | + public DateTime CreationTime { get; set; } | ||
| 14 | + | ||
| 15 | + public string? CreatorId { get; set; } | ||
| 16 | + | ||
| 17 | + public string? LastModifierId { get; set; } | ||
| 18 | + | ||
| 19 | + public DateTime? LastModificationTime { get; set; } | ||
| 20 | + | ||
| 21 | + public string ConcurrencyStamp { get; set; } = string.Empty; | ||
| 22 | + | ||
| 23 | + public string CategoryCode { get; set; } = string.Empty; | ||
| 24 | + | ||
| 25 | + public string CategoryName { get; set; } = string.Empty; | ||
| 26 | + | ||
| 27 | + public string? CategoryPhotoUrl { get; set; } | ||
| 28 | + | ||
| 29 | + public bool State { get; set; } | ||
| 30 | + | ||
| 31 | + public int OrderNum { get; set; } | ||
| 32 | +} | ||
| 33 | + |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductDbEntity.cs
| @@ -14,7 +14,7 @@ public class FlProductDbEntity | @@ -14,7 +14,7 @@ public class FlProductDbEntity | ||
| 14 | 14 | ||
| 15 | public string ProductName { get; set; } = string.Empty; | 15 | public string ProductName { get; set; } = string.Empty; |
| 16 | 16 | ||
| 17 | - public string? CategoryName { get; set; } | 17 | + public string? CategoryId { get; set; } |
| 18 | 18 | ||
| 19 | public string? ProductImageUrl { get; set; } | 19 | public string? ProductImageUrl { get; set; } |
| 20 | 20 |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs
| @@ -137,11 +137,13 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -137,11 +137,13 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 137 | 137 | ||
| 138 | // 查询 products 并拼接 | 138 | // 查询 products 并拼接 |
| 139 | var productRows = await _dbContext.SqlSugarClient | 139 | var productRows = await _dbContext.SqlSugarClient |
| 140 | - .Queryable<FlLabelProductDbEntity, FlLabelDbEntity, FlProductDbEntity>( | ||
| 141 | - (lp, l, p) => lp.LabelId == l.Id && lp.ProductId == p.Id) | ||
| 142 | - .Where((lp, l, p) => pageLabelIds.Contains(lp.LabelId)) | ||
| 143 | - .Where((lp, l, p) => !l.IsDeleted && !p.IsDeleted) | ||
| 144 | - .Select((lp, l, p) => new { lp.LabelId, p.ProductName, p.CategoryName }) | 140 | + .Queryable<FlLabelProductDbEntity>() |
| 141 | + .InnerJoin<FlLabelDbEntity>((lp, l) => lp.LabelId == l.Id) | ||
| 142 | + .InnerJoin<FlProductDbEntity>((lp, l, p) => lp.ProductId == p.Id) | ||
| 143 | + .LeftJoin<FlProductCategoryDbEntity>((lp, l, p, pc) => p.CategoryId == pc.Id) | ||
| 144 | + .Where((lp, l, p, pc) => pageLabelIds.Contains(lp.LabelId)) | ||
| 145 | + .Where((lp, l, p, pc) => !l.IsDeleted && !p.IsDeleted) | ||
| 146 | + .Select((lp, l, p, pc) => new { lp.LabelId, p.ProductName, ProductCategoryName = pc.CategoryName }) | ||
| 145 | .ToListAsync(); | 147 | .ToListAsync(); |
| 146 | 148 | ||
| 147 | var productsMap = productRows | 149 | var productsMap = productRows |
| @@ -151,7 +153,7 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -151,7 +153,7 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 151 | g => new | 153 | g => new |
| 152 | { | 154 | { |
| 153 | Products = string.Join(",", g.Select(x => x.ProductName ?? string.Empty).Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()), | 155 | Products = string.Join(",", g.Select(x => x.ProductName ?? string.Empty).Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()), |
| 154 | - ProductCategoryName = string.Join(",", g.Select(x => x.CategoryName ?? string.Empty).Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()) | 156 | + ProductCategoryName = string.Join(",", g.Select(x => x.ProductCategoryName ?? string.Empty).Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()) |
| 155 | }); | 157 | }); |
| 156 | 158 | ||
| 157 | var locationIds = orderedLabels | 159 | var locationIds = orderedLabels |
| @@ -474,6 +476,8 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -474,6 +476,8 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 474 | throw new UserFriendlyException("labelCode不能为空"); | 476 | throw new UserFriendlyException("labelCode不能为空"); |
| 475 | } | 477 | } |
| 476 | 478 | ||
| 479 | + var baseTime = input?.BaseTime ?? DateTime.Now; | ||
| 480 | + | ||
| 477 | var label = await _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>() | 481 | var label = await _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>() |
| 478 | .FirstAsync(x => !x.IsDeleted && x.LabelCode == labelCode); | 482 | .FirstAsync(x => !x.IsDeleted && x.LabelCode == labelCode); |
| 479 | if (label is null) | 483 | if (label is null) |
| @@ -640,14 +644,14 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -640,14 +644,14 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 640 | case "DATE": | 644 | case "DATE": |
| 641 | { | 645 | { |
| 642 | var offsetDays = cfg.TryGetValue("offsetDays", out var od) ? TryGetInt(od, 0) : 0; | 646 | var offsetDays = cfg.TryGetValue("offsetDays", out var od) ? TryGetInt(od, 0) : 0; |
| 643 | - var dt = DateTime.Today.AddDays(offsetDays); | 647 | + var dt = baseTime.Date.AddDays(offsetDays); |
| 644 | UpsertConfigValue(cfg, "format", dt.ToString("yyyy-MM-dd")); | 648 | UpsertConfigValue(cfg, "format", dt.ToString("yyyy-MM-dd")); |
| 645 | cfg.Remove("inputType"); | 649 | cfg.Remove("inputType"); |
| 646 | } | 650 | } |
| 647 | break; | 651 | break; |
| 648 | case "TIME": | 652 | case "TIME": |
| 649 | { | 653 | { |
| 650 | - var dt = DateTime.Now; | 654 | + var dt = baseTime; |
| 651 | UpsertConfigValue(cfg, "format", dt.ToString("HH:mm")); | 655 | UpsertConfigValue(cfg, "format", dt.ToString("HH:mm")); |
| 652 | cfg.Remove("inputType"); | 656 | cfg.Remove("inputType"); |
| 653 | } | 657 | } |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs
| @@ -30,14 +30,22 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -30,14 +30,22 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 30 | RefAsync<int> total = 0; | 30 | RefAsync<int> total = 0; |
| 31 | var keyword = input.Keyword?.Trim(); | 31 | var keyword = input.Keyword?.Trim(); |
| 32 | 32 | ||
| 33 | - var query = _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() | 33 | + var query = _dbContext.SqlSugarClient |
| 34 | + .Queryable<FlProductDbEntity>() | ||
| 34 | .Where(x => !x.IsDeleted) | 35 | .Where(x => !x.IsDeleted) |
| 35 | - .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => | ||
| 36 | - x.ProductCode.Contains(keyword!) || | ||
| 37 | - x.ProductName.Contains(keyword!) || | ||
| 38 | - (x.CategoryName != null && x.CategoryName.Contains(keyword!))) | ||
| 39 | .WhereIF(input.State != null, x => x.State == input.State); | 36 | .WhereIF(input.State != null, x => x.State == input.State); |
| 40 | 37 | ||
| 38 | + if (!string.IsNullOrWhiteSpace(keyword)) | ||
| 39 | + { | ||
| 40 | + query = query | ||
| 41 | + .LeftJoin<FlProductCategoryDbEntity>((p, c) => p.CategoryId == c.Id) | ||
| 42 | + .Where((p, c) => | ||
| 43 | + p.ProductCode.Contains(keyword!) || | ||
| 44 | + p.ProductName.Contains(keyword!) || | ||
| 45 | + (c.CategoryName != null && c.CategoryName.Contains(keyword!))) | ||
| 46 | + .Select((p, c) => p); | ||
| 47 | + } | ||
| 48 | + | ||
| 41 | if (!string.IsNullOrWhiteSpace(input.Sorting)) | 49 | if (!string.IsNullOrWhiteSpace(input.Sorting)) |
| 42 | { | 50 | { |
| 43 | query = query.OrderBy(input.Sorting); | 51 | query = query.OrderBy(input.Sorting); |
| @@ -64,15 +72,41 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -64,15 +72,41 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 64 | countMap = countRows.ToDictionary(x => x.ProductId, x => (long)x.Count); | 72 | countMap = countRows.ToDictionary(x => x.ProductId, x => (long)x.Count); |
| 65 | } | 73 | } |
| 66 | 74 | ||
| 67 | - var items = entities.Select(x => new ProductGetListOutputDto | 75 | + var categoryIds = entities |
| 76 | + .Select(x => x.CategoryId) | ||
| 77 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | ||
| 78 | + .Select(x => x!.Trim()) | ||
| 79 | + .Distinct() | ||
| 80 | + .ToList(); | ||
| 81 | + | ||
| 82 | + var categoryMap = new Dictionary<string, FlProductCategoryDbEntity>(); | ||
| 83 | + if (categoryIds.Count > 0) | ||
| 68 | { | 84 | { |
| 69 | - Id = x.Id, | ||
| 70 | - ProductCode = x.ProductCode, | ||
| 71 | - ProductName = x.ProductName, | ||
| 72 | - CategoryName = x.CategoryName, | ||
| 73 | - ProductImageUrl = x.ProductImageUrl, | ||
| 74 | - State = x.State, | ||
| 75 | - NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0 | 85 | + var categories = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() |
| 86 | + .Where(x => !x.IsDeleted && categoryIds.Contains(x.Id)) | ||
| 87 | + .ToListAsync(); | ||
| 88 | + categoryMap = categories.ToDictionary(x => x.Id, x => x); | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + var items = entities.Select(x => | ||
| 92 | + { | ||
| 93 | + var categoryName = "无"; | ||
| 94 | + if (!string.IsNullOrWhiteSpace(x.CategoryId) && categoryMap.TryGetValue(x.CategoryId.Trim(), out var c)) | ||
| 95 | + { | ||
| 96 | + categoryName = string.IsNullOrWhiteSpace(c.CategoryName) ? "无" : c.CategoryName.Trim(); | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + return new ProductGetListOutputDto | ||
| 100 | + { | ||
| 101 | + Id = x.Id, | ||
| 102 | + ProductCode = x.ProductCode, | ||
| 103 | + ProductName = x.ProductName, | ||
| 104 | + CategoryId = x.CategoryId, | ||
| 105 | + CategoryName = categoryName, | ||
| 106 | + ProductImageUrl = x.ProductImageUrl, | ||
| 107 | + State = x.State, | ||
| 108 | + NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0 | ||
| 109 | + }; | ||
| 76 | }).ToList(); | 110 | }).ToList(); |
| 77 | 111 | ||
| 78 | return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); | 112 | return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); |
| @@ -94,6 +128,17 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -94,6 +128,17 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 94 | throw new UserFriendlyException("产品不存在"); | 128 | throw new UserFriendlyException("产品不存在"); |
| 95 | } | 129 | } |
| 96 | 130 | ||
| 131 | + string? categoryName = "无"; | ||
| 132 | + if (!string.IsNullOrWhiteSpace(entity.CategoryId)) | ||
| 133 | + { | ||
| 134 | + var c = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() | ||
| 135 | + .FirstAsync(x => !x.IsDeleted && x.Id == entity.CategoryId); | ||
| 136 | + if (c is not null && !string.IsNullOrWhiteSpace(c.CategoryName)) | ||
| 137 | + { | ||
| 138 | + categoryName = c.CategoryName.Trim(); | ||
| 139 | + } | ||
| 140 | + } | ||
| 141 | + | ||
| 97 | var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLocationProductDbEntity>() | 142 | var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLocationProductDbEntity>() |
| 98 | .Where(x => x.ProductId == productId) | 143 | .Where(x => x.ProductId == productId) |
| 99 | .Select(x => x.LocationId) | 144 | .Select(x => x.LocationId) |
| @@ -105,7 +150,8 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -105,7 +150,8 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 105 | Id = entity.Id, | 150 | Id = entity.Id, |
| 106 | ProductCode = entity.ProductCode, | 151 | ProductCode = entity.ProductCode, |
| 107 | ProductName = entity.ProductName, | 152 | ProductName = entity.ProductName, |
| 108 | - CategoryName = entity.CategoryName, | 153 | + CategoryId = entity.CategoryId, |
| 154 | + CategoryName = categoryName, | ||
| 109 | ProductImageUrl = entity.ProductImageUrl, | 155 | ProductImageUrl = entity.ProductImageUrl, |
| 110 | State = entity.State, | 156 | State = entity.State, |
| 111 | LocationIds = locationIds | 157 | LocationIds = locationIds |
| @@ -135,7 +181,7 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -135,7 +181,7 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 135 | IsDeleted = false, | 181 | IsDeleted = false, |
| 136 | ProductCode = code, | 182 | ProductCode = code, |
| 137 | ProductName = name, | 183 | ProductName = name, |
| 138 | - CategoryName = input.CategoryName?.Trim(), | 184 | + CategoryId = input.CategoryId?.Trim(), |
| 139 | ProductImageUrl = input.ProductImageUrl?.Trim(), | 185 | ProductImageUrl = input.ProductImageUrl?.Trim(), |
| 140 | State = input.State | 186 | State = input.State |
| 141 | }; | 187 | }; |
| @@ -176,7 +222,7 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -176,7 +222,7 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 176 | 222 | ||
| 177 | entity.ProductCode = code; | 223 | entity.ProductCode = code; |
| 178 | entity.ProductName = name; | 224 | entity.ProductName = name; |
| 179 | - entity.CategoryName = input.CategoryName?.Trim(); | 225 | + entity.CategoryId = input.CategoryId?.Trim(); |
| 180 | entity.ProductImageUrl = input.ProductImageUrl?.Trim(); | 226 | entity.ProductImageUrl = input.ProductImageUrl?.Trim(); |
| 181 | entity.State = input.State; | 227 | entity.State = input.State; |
| 182 | 228 |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs
| @@ -32,15 +32,38 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -32,15 +32,38 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 32 | RefAsync<int> total = 0; | 32 | RefAsync<int> total = 0; |
| 33 | var keyword = input.Keyword?.Trim(); | 33 | var keyword = input.Keyword?.Trim(); |
| 34 | 34 | ||
| 35 | - var query = _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() | 35 | + var query = _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() |
| 36 | .Where(x => !x.IsDeleted) | 36 | .Where(x => !x.IsDeleted) |
| 37 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), | 37 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), |
| 38 | x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!)) | 38 | x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!)) |
| 39 | .WhereIF(input.State != null, x => x.State == input.State); | 39 | .WhereIF(input.State != null, x => x.State == input.State); |
| 40 | 40 | ||
| 41 | + // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column | ||
| 42 | + // 同时避免将 input.Sorting 原样拼接到 SQL(存在注入风险) | ||
| 41 | if (!string.IsNullOrWhiteSpace(input.Sorting)) | 43 | if (!string.IsNullOrWhiteSpace(input.Sorting)) |
| 42 | { | 44 | { |
| 43 | - query = query.OrderBy(input.Sorting); | 45 | + var sorting = input.Sorting.Trim(); |
| 46 | + if (sorting.Equals("OrderNum desc", StringComparison.OrdinalIgnoreCase)) | ||
| 47 | + { | ||
| 48 | + query = query.OrderByDescending(x => x.OrderNum); | ||
| 49 | + } | ||
| 50 | + else if (sorting.Equals("OrderNum asc", StringComparison.OrdinalIgnoreCase)) | ||
| 51 | + { | ||
| 52 | + query = query.OrderBy(x => x.OrderNum); | ||
| 53 | + } | ||
| 54 | + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase)) | ||
| 55 | + { | ||
| 56 | + query = query.OrderByDescending(x => x.CreationTime); | ||
| 57 | + } | ||
| 58 | + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase)) | ||
| 59 | + { | ||
| 60 | + query = query.OrderBy(x => x.CreationTime); | ||
| 61 | + } | ||
| 62 | + else | ||
| 63 | + { | ||
| 64 | + // 不识别的排序统一走默认 | ||
| 65 | + query = query.OrderByDescending(x => x.OrderNum).OrderByDescending(x => x.CreationTime); | ||
| 66 | + } | ||
| 44 | } | 67 | } |
| 45 | else | 68 | else |
| 46 | { | 69 | { |
| @@ -68,7 +91,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -68,7 +91,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 68 | /// </summary> | 91 | /// </summary> |
| 69 | public async Task<ProductCategoryGetOutputDto> GetAsync(string id) | 92 | public async Task<ProductCategoryGetOutputDto> GetAsync(string id) |
| 70 | { | 93 | { |
| 71 | - var entity = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() | 94 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() |
| 72 | .FirstAsync(x => x.Id == id && !x.IsDeleted); | 95 | .FirstAsync(x => x.Id == id && !x.IsDeleted); |
| 73 | if (entity is null) | 96 | if (entity is null) |
| 74 | { | 97 | { |
| @@ -90,16 +113,24 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -90,16 +113,24 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 90 | throw new UserFriendlyException("类别编码和名称不能为空"); | 113 | throw new UserFriendlyException("类别编码和名称不能为空"); |
| 91 | } | 114 | } |
| 92 | 115 | ||
| 93 | - var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() | 116 | + var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() |
| 94 | .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); | 117 | .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); |
| 95 | if (duplicated) | 118 | if (duplicated) |
| 96 | { | 119 | { |
| 97 | throw new UserFriendlyException("类别编码或名称已存在"); | 120 | throw new UserFriendlyException("类别编码或名称已存在"); |
| 98 | } | 121 | } |
| 99 | 122 | ||
| 100 | - var entity = new FlLabelCategoryDbEntity | 123 | + var now = DateTime.Now; |
| 124 | + var currentUserId = CurrentUser?.Id?.ToString(); | ||
| 125 | + var entity = new FlProductCategoryDbEntity | ||
| 101 | { | 126 | { |
| 102 | Id = _guidGenerator.Create().ToString(), | 127 | Id = _guidGenerator.Create().ToString(), |
| 128 | + IsDeleted = false, | ||
| 129 | + CreationTime = now, | ||
| 130 | + CreatorId = currentUserId, | ||
| 131 | + LastModifierId = currentUserId, | ||
| 132 | + LastModificationTime = now, | ||
| 133 | + ConcurrencyStamp = _guidGenerator.Create().ToString("N"), | ||
| 103 | CategoryCode = code, | 134 | CategoryCode = code, |
| 104 | CategoryName = name, | 135 | CategoryName = name, |
| 105 | CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(), | 136 | CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(), |
| @@ -116,7 +147,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -116,7 +147,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 116 | /// </summary> | 147 | /// </summary> |
| 117 | public async Task<ProductCategoryGetOutputDto> UpdateAsync(string id, ProductCategoryUpdateInputVo input) | 148 | public async Task<ProductCategoryGetOutputDto> UpdateAsync(string id, ProductCategoryUpdateInputVo input) |
| 118 | { | 149 | { |
| 119 | - var entity = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() | 150 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() |
| 120 | .FirstAsync(x => x.Id == id && !x.IsDeleted); | 151 | .FirstAsync(x => x.Id == id && !x.IsDeleted); |
| 121 | if (entity is null) | 152 | if (entity is null) |
| 122 | { | 153 | { |
| @@ -130,7 +161,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -130,7 +161,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 130 | throw new UserFriendlyException("类别编码和名称不能为空"); | 161 | throw new UserFriendlyException("类别编码和名称不能为空"); |
| 131 | } | 162 | } |
| 132 | 163 | ||
| 133 | - var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() | 164 | + var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() |
| 134 | .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); | 165 | .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); |
| 135 | if (duplicated) | 166 | if (duplicated) |
| 136 | { | 167 | { |
| @@ -154,19 +185,19 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -154,19 +185,19 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 154 | /// </summary> | 185 | /// </summary> |
| 155 | public async Task DeleteAsync(string id) | 186 | public async Task DeleteAsync(string id) |
| 156 | { | 187 | { |
| 157 | - var entity = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() | 188 | + var entity = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() |
| 158 | .FirstAsync(x => x.Id == id && !x.IsDeleted); | 189 | .FirstAsync(x => x.Id == id && !x.IsDeleted); |
| 159 | if (entity is null) | 190 | if (entity is null) |
| 160 | { | 191 | { |
| 161 | return; | 192 | return; |
| 162 | } | 193 | } |
| 163 | 194 | ||
| 164 | - // 若被标签引用则不允许删除(保持与 LabelCategory 一致的约束) | ||
| 165 | - var used = await _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>() | ||
| 166 | - .AnyAsync(x => !x.IsDeleted && x.LabelCategoryId == id); | ||
| 167 | - if (used) | 195 | + // 若被产品引用则不允许删除 |
| 196 | + var usedByProduct = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() | ||
| 197 | + .AnyAsync(x => !x.IsDeleted && x.CategoryId == id); | ||
| 198 | + if (usedByProduct) | ||
| 168 | { | 199 | { |
| 169 | - throw new UserFriendlyException("该类别已被标签引用,无法删除"); | 200 | + throw new UserFriendlyException("该类别已被产品引用,无法删除"); |
| 170 | } | 201 | } |
| 171 | 202 | ||
| 172 | entity.IsDeleted = true; | 203 | entity.IsDeleted = true; |
| @@ -175,7 +206,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -175,7 +206,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 175 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | 206 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 176 | } | 207 | } |
| 177 | 208 | ||
| 178 | - private static ProductCategoryGetOutputDto MapToGetOutput(FlLabelCategoryDbEntity x) | 209 | + private static ProductCategoryGetOutputDto MapToGetOutput(FlProductCategoryDbEntity x) |
| 179 | { | 210 | { |
| 180 | return new ProductCategoryGetOutputDto | 211 | return new ProductCategoryGetOutputDto |
| 181 | { | 212 | { |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs
0 → 100644
| 1 | +using System; | ||
| 2 | +using System.Collections.Generic; | ||
| 3 | +using System.Globalization; | ||
| 4 | +using System.Linq; | ||
| 5 | +using System.Text.Json; | ||
| 6 | +using System.Threading.Tasks; | ||
| 7 | +using FoodLabeling.Application.Contracts.Dtos.Label; | ||
| 8 | +using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | ||
| 9 | +using FoodLabeling.Application.Contracts.IServices; | ||
| 10 | +using FoodLabeling.Application.Services.DbModels; | ||
| 11 | +using Microsoft.AspNetCore.Authorization; | ||
| 12 | +using SqlSugar; | ||
| 13 | +using Volo.Abp; | ||
| 14 | +using Volo.Abp.Application.Services; | ||
| 15 | +using Volo.Abp.Guids; | ||
| 16 | +using Volo.Abp.Uow; | ||
| 17 | +using Yi.Framework.SqlSugarCore.Abstractions; | ||
| 18 | + | ||
| 19 | +namespace FoodLabeling.Application.Services; | ||
| 20 | + | ||
| 21 | +/// <summary> | ||
| 22 | +/// App Labeling:四级列表(标签分类 → 产品分类 → 产品 → 标签种类) | ||
| 23 | +/// </summary> | ||
| 24 | +public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppService | ||
| 25 | +{ | ||
| 26 | + private readonly ISqlSugarDbContext _dbContext; | ||
| 27 | + private readonly ILabelAppService _labelAppService; | ||
| 28 | + private readonly IGuidGenerator _guidGenerator; | ||
| 29 | + | ||
| 30 | + public UsAppLabelingAppService(ISqlSugarDbContext dbContext, ILabelAppService labelAppService, IGuidGenerator guidGenerator) | ||
| 31 | + { | ||
| 32 | + _dbContext = dbContext; | ||
| 33 | + _labelAppService = labelAppService; | ||
| 34 | + _guidGenerator = guidGenerator; | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + /// <summary> | ||
| 38 | + /// 获取当前门店下四级嵌套数据 | ||
| 39 | + /// </summary> | ||
| 40 | + /// <remarks> | ||
| 41 | + /// L1 标签分类 fl_label_category;L2 产品分类 fl_product.CategoryId join fl_product_category; | ||
| 42 | + /// L3 产品;L4 与该门店、该标签分类、该产品关联的标签实例(fl_label + fl_label_type)。 | ||
| 43 | + /// </remarks> | ||
| 44 | + [Authorize] | ||
| 45 | + public virtual async Task<List<UsAppLabelCategoryTreeNodeDto>> GetLabelingTreeAsync(UsAppLabelingTreeInputVo input) | ||
| 46 | + { | ||
| 47 | + if (string.IsNullOrWhiteSpace(input.LocationId)) | ||
| 48 | + { | ||
| 49 | + throw new UserFriendlyException("门店Id不能为空"); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + var locationId = input.LocationId.Trim(); | ||
| 53 | + var keyword = input.Keyword?.Trim(); | ||
| 54 | + var filterCategoryId = input.LabelCategoryId?.Trim(); | ||
| 55 | + | ||
| 56 | + var productIds = await _dbContext.SqlSugarClient.Queryable<FlLocationProductDbEntity>() | ||
| 57 | + .Where(x => x.LocationId == locationId) | ||
| 58 | + .Select(x => x.ProductId) | ||
| 59 | + .ToListAsync(); | ||
| 60 | + | ||
| 61 | + if (productIds.Count == 0) | ||
| 62 | + { | ||
| 63 | + return new List<UsAppLabelCategoryTreeNodeDto>(); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + var query = BuildLabelingJoinQuery(locationId, productIds, filterCategoryId, keyword); | ||
| 67 | + | ||
| 68 | + var raw = await query | ||
| 69 | + .Select((lp, l, p, c, t, tpl, pc) => new LabelingTreeRow | ||
| 70 | + { | ||
| 71 | + LabelCategoryId = c.Id, | ||
| 72 | + LabelCategoryName = c.CategoryName, | ||
| 73 | + LabelCategoryPhotoUrl = c.CategoryPhotoUrl, | ||
| 74 | + LabelCategoryOrderNum = c.OrderNum, | ||
| 75 | + ProductCategoryId = p.CategoryId, | ||
| 76 | + ProductCategoryName = pc.CategoryName, | ||
| 77 | + ProductCategoryPhotoUrl = pc.CategoryPhotoUrl, | ||
| 78 | + ProductId = p.Id, | ||
| 79 | + ProductName = p.ProductName, | ||
| 80 | + ProductCode = p.ProductCode, | ||
| 81 | + ProductImageUrl = p.ProductImageUrl, | ||
| 82 | + LabelTypeId = t.Id, | ||
| 83 | + TypeName = t.TypeName, | ||
| 84 | + TypeOrderNum = t.OrderNum, | ||
| 85 | + LabelCode = l.LabelCode, | ||
| 86 | + TemplateCode = tpl.TemplateCode, | ||
| 87 | + TemplateWidth = tpl.Width, | ||
| 88 | + TemplateHeight = tpl.Height, | ||
| 89 | + TemplateUnit = tpl.Unit | ||
| 90 | + }) | ||
| 91 | + .ToListAsync(); | ||
| 92 | + | ||
| 93 | + if (raw.Count == 0) | ||
| 94 | + { | ||
| 95 | + return new List<UsAppLabelCategoryTreeNodeDto>(); | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + var byL1 = raw.GroupBy(x => new | ||
| 99 | + { | ||
| 100 | + x.LabelCategoryId, | ||
| 101 | + x.LabelCategoryName, | ||
| 102 | + x.LabelCategoryPhotoUrl, | ||
| 103 | + x.LabelCategoryOrderNum | ||
| 104 | + }).OrderBy(g => g.Key.LabelCategoryOrderNum).ThenBy(g => g.Key.LabelCategoryName); | ||
| 105 | + | ||
| 106 | + var result = new List<UsAppLabelCategoryTreeNodeDto>(); | ||
| 107 | + foreach (var g1 in byL1) | ||
| 108 | + { | ||
| 109 | + var l1 = new UsAppLabelCategoryTreeNodeDto | ||
| 110 | + { | ||
| 111 | + Id = g1.Key.LabelCategoryId, | ||
| 112 | + CategoryName = g1.Key.LabelCategoryName ?? string.Empty, | ||
| 113 | + CategoryPhotoUrl = g1.Key.LabelCategoryPhotoUrl, | ||
| 114 | + OrderNum = g1.Key.LabelCategoryOrderNum, | ||
| 115 | + ProductCategories = new List<UsAppProductCategoryNodeDto>() | ||
| 116 | + }; | ||
| 117 | + | ||
| 118 | + var byL2 = g1.GroupBy(x => | ||
| 119 | + { | ||
| 120 | + var categoryId = NormalizeNullableId(x.ProductCategoryId); | ||
| 121 | + if (categoryId is null) | ||
| 122 | + { | ||
| 123 | + return new | ||
| 124 | + { | ||
| 125 | + CategoryId = (string?)null, | ||
| 126 | + CategoryName = "无", | ||
| 127 | + CategoryPhotoUrl = (string?)null | ||
| 128 | + }; | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + var categoryName = NormalizeCategoryName(x.ProductCategoryName); | ||
| 132 | + var categoryPhotoUrl = NormalizeNullableUrl(x.ProductCategoryPhotoUrl); | ||
| 133 | + return new | ||
| 134 | + { | ||
| 135 | + CategoryId = (string?)categoryId, | ||
| 136 | + CategoryName = categoryName, | ||
| 137 | + CategoryPhotoUrl = categoryPhotoUrl | ||
| 138 | + }; | ||
| 139 | + }) | ||
| 140 | + .OrderBy(g => g.Key.CategoryName); | ||
| 141 | + | ||
| 142 | + foreach (var g2 in byL2) | ||
| 143 | + { | ||
| 144 | + var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName); | ||
| 145 | + var l2 = new UsAppProductCategoryNodeDto | ||
| 146 | + { | ||
| 147 | + CategoryId = g2.Key.CategoryId, | ||
| 148 | + CategoryPhotoUrl = g2.Key.CategoryPhotoUrl, | ||
| 149 | + Name = g2.Key.CategoryName, | ||
| 150 | + ItemCount = productsGrouped.Count(), | ||
| 151 | + Products = new List<UsAppLabelingProductNodeDto>() | ||
| 152 | + }; | ||
| 153 | + | ||
| 154 | + foreach (var g3 in productsGrouped) | ||
| 155 | + { | ||
| 156 | + var first = g3.First(); | ||
| 157 | + var typeNodes = g3 | ||
| 158 | + .GroupBy(r => r.LabelCode) | ||
| 159 | + .Select(gr => BuildLabelTypeNode(gr.First())) | ||
| 160 | + .OrderBy(t => t.OrderNum) | ||
| 161 | + .ThenBy(t => t.TypeName) | ||
| 162 | + .ToList(); | ||
| 163 | + | ||
| 164 | + var subtitle = string.IsNullOrWhiteSpace(first.ProductCode?.Trim()) | ||
| 165 | + ? "无" | ||
| 166 | + : first.ProductCode!.Trim(); | ||
| 167 | + | ||
| 168 | + l2.Products.Add(new UsAppLabelingProductNodeDto | ||
| 169 | + { | ||
| 170 | + ProductId = first.ProductId, | ||
| 171 | + ProductName = first.ProductName ?? string.Empty, | ||
| 172 | + ProductCode = first.ProductCode ?? string.Empty, | ||
| 173 | + ProductImageUrl = first.ProductImageUrl, | ||
| 174 | + Subtitle = subtitle, | ||
| 175 | + LabelTypeCount = typeNodes.Count, | ||
| 176 | + LabelTypes = typeNodes | ||
| 177 | + }); | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + l1.ProductCategories.Add(l2); | ||
| 181 | + } | ||
| 182 | + | ||
| 183 | + result.Add(l1); | ||
| 184 | + } | ||
| 185 | + | ||
| 186 | + return result; | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + /// <summary> | ||
| 190 | + /// App 打印预览:按标签编码解析模板并返回顶部展示字段 + 预览模板结构 | ||
| 191 | + /// </summary> | ||
| 192 | + /// <remarks> | ||
| 193 | + /// 示例请求: | ||
| 194 | + /// ```json | ||
| 195 | + /// { | ||
| 196 | + /// "locationId": "LOC001", | ||
| 197 | + /// "labelCode": "LBL0001", | ||
| 198 | + /// "productId": "PROD001", | ||
| 199 | + /// "baseTime": "2026-03-26T10:30:00", | ||
| 200 | + /// "printInputJson": { | ||
| 201 | + /// "price": "12.99" | ||
| 202 | + /// } | ||
| 203 | + /// } | ||
| 204 | + /// ``` | ||
| 205 | + /// </remarks> | ||
| 206 | + /// <param name="input">预览入参</param> | ||
| 207 | + /// <returns>顶部字段 + 预览模板结构</returns> | ||
| 208 | + /// <response code="200">成功</response> | ||
| 209 | + /// <response code="400">参数错误/数据不存在</response> | ||
| 210 | + /// <response code="500">服务器错误</response> | ||
| 211 | + [Authorize] | ||
| 212 | + public virtual async Task<UsAppLabelPreviewDto> PreviewAsync(UsAppLabelPreviewInputVo input) | ||
| 213 | + { | ||
| 214 | + if (input is null) | ||
| 215 | + { | ||
| 216 | + throw new UserFriendlyException("入参不能为空"); | ||
| 217 | + } | ||
| 218 | + | ||
| 219 | + var locationId = input.LocationId?.Trim(); | ||
| 220 | + if (string.IsNullOrWhiteSpace(locationId)) | ||
| 221 | + { | ||
| 222 | + throw new UserFriendlyException("门店Id不能为空"); | ||
| 223 | + } | ||
| 224 | + | ||
| 225 | + var labelCode = input.LabelCode?.Trim(); | ||
| 226 | + if (string.IsNullOrWhiteSpace(labelCode)) | ||
| 227 | + { | ||
| 228 | + throw new UserFriendlyException("labelCode不能为空"); | ||
| 229 | + } | ||
| 230 | + | ||
| 231 | + var labelRow = await _dbContext.SqlSugarClient | ||
| 232 | + .Queryable<FlLabelDbEntity, FlLabelCategoryDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity>( | ||
| 233 | + (l, c, t, tpl) => l.LabelCategoryId == c.Id && l.LabelTypeId == t.Id && l.TemplateId == tpl.Id) | ||
| 234 | + .Where((l, c, t, tpl) => !l.IsDeleted && l.State) | ||
| 235 | + .Where((l, c, t, tpl) => !c.IsDeleted && c.State) | ||
| 236 | + .Where((l, c, t, tpl) => !t.IsDeleted && t.State) | ||
| 237 | + .Where((l, c, t, tpl) => !tpl.IsDeleted) | ||
| 238 | + .Where((l, c, t, tpl) => l.LabelCode == labelCode) | ||
| 239 | + .Select((l, c, t, tpl) => new | ||
| 240 | + { | ||
| 241 | + l.LabelCode, | ||
| 242 | + l.LocationId, | ||
| 243 | + LabelCategoryName = c.CategoryName, | ||
| 244 | + TypeName = t.TypeName, | ||
| 245 | + TemplateCode = tpl.TemplateCode, | ||
| 246 | + TemplateWidth = tpl.Width, | ||
| 247 | + TemplateHeight = tpl.Height, | ||
| 248 | + TemplateUnit = tpl.Unit | ||
| 249 | + }) | ||
| 250 | + .FirstAsync(); | ||
| 251 | + | ||
| 252 | + if (labelRow is null) | ||
| 253 | + { | ||
| 254 | + throw new UserFriendlyException("标签不存在或不可用"); | ||
| 255 | + } | ||
| 256 | + | ||
| 257 | + if (!string.Equals(labelRow.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase)) | ||
| 258 | + { | ||
| 259 | + throw new UserFriendlyException("该标签不属于当前门店"); | ||
| 260 | + } | ||
| 261 | + | ||
| 262 | + var template = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo | ||
| 263 | + { | ||
| 264 | + LabelCode = labelCode, | ||
| 265 | + ProductId = input.ProductId?.Trim(), | ||
| 266 | + BaseTime = input.BaseTime, | ||
| 267 | + PrintInputJson = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value) | ||
| 268 | + }); | ||
| 269 | + | ||
| 270 | + var productName = string.Empty; | ||
| 271 | + var productCategoryName = "无"; | ||
| 272 | + if (!string.IsNullOrWhiteSpace(input.ProductId)) | ||
| 273 | + { | ||
| 274 | + var pid = input.ProductId.Trim(); | ||
| 275 | + var p = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() | ||
| 276 | + .FirstAsync(x => !x.IsDeleted && x.State && x.Id == pid); | ||
| 277 | + if (p is not null) | ||
| 278 | + { | ||
| 279 | + productName = p.ProductName ?? string.Empty; | ||
| 280 | + if (!string.IsNullOrWhiteSpace(p.CategoryId)) | ||
| 281 | + { | ||
| 282 | + var pc = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() | ||
| 283 | + .FirstAsync(x => !x.IsDeleted && x.State && x.Id == p.CategoryId); | ||
| 284 | + productCategoryName = NormalizeCategoryName(pc?.CategoryName); | ||
| 285 | + } | ||
| 286 | + } | ||
| 287 | + } | ||
| 288 | + | ||
| 289 | + return new UsAppLabelPreviewDto | ||
| 290 | + { | ||
| 291 | + LocationId = locationId, | ||
| 292 | + LabelCode = labelCode, | ||
| 293 | + TemplateCode = labelRow.TemplateCode, | ||
| 294 | + LabelSizeText = FormatLabelSize(labelRow.TemplateWidth, labelRow.TemplateHeight, labelRow.TemplateUnit), | ||
| 295 | + TypeName = labelRow.TypeName, | ||
| 296 | + ProductName = string.IsNullOrWhiteSpace(productName) ? null : productName, | ||
| 297 | + ProductCategoryName = productCategoryName, | ||
| 298 | + LabelCategoryName = labelRow.LabelCategoryName, | ||
| 299 | + PreviewImageBase64Png = null, | ||
| 300 | + Template = template | ||
| 301 | + }; | ||
| 302 | + } | ||
| 303 | + | ||
| 304 | + /// <summary> | ||
| 305 | + /// App 打印:创建打印任务并落库打印明细(fl_label_print_task / fl_label_print_data) | ||
| 306 | + /// </summary> | ||
| 307 | + /// <param name="input">打印入参</param> | ||
| 308 | + /// <returns>任务Id</returns> | ||
| 309 | + [Authorize] | ||
| 310 | + [UnitOfWork] | ||
| 311 | + public virtual async Task<UsAppLabelPrintOutputDto> PrintAsync(UsAppLabelPrintInputVo input) | ||
| 312 | + { | ||
| 313 | + if (input is null) | ||
| 314 | + { | ||
| 315 | + throw new UserFriendlyException("入参不能为空"); | ||
| 316 | + } | ||
| 317 | + | ||
| 318 | + var locationId = input.LocationId?.Trim(); | ||
| 319 | + if (string.IsNullOrWhiteSpace(locationId)) | ||
| 320 | + { | ||
| 321 | + throw new UserFriendlyException("门店Id不能为空"); | ||
| 322 | + } | ||
| 323 | + | ||
| 324 | + var labelCode = input.LabelCode?.Trim(); | ||
| 325 | + if (string.IsNullOrWhiteSpace(labelCode)) | ||
| 326 | + { | ||
| 327 | + throw new UserFriendlyException("labelCode不能为空"); | ||
| 328 | + } | ||
| 329 | + | ||
| 330 | + var quantity = input.PrintQuantity <= 0 ? 1 : input.PrintQuantity; | ||
| 331 | + | ||
| 332 | + // 校验 label + location,并补齐一些顶部字段用于任务表落库 | ||
| 333 | + var labelRow = await _dbContext.SqlSugarClient | ||
| 334 | + .Queryable<FlLabelDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity>( | ||
| 335 | + (l, t, tpl) => l.LabelTypeId == t.Id && l.TemplateId == tpl.Id) | ||
| 336 | + .Where((l, t, tpl) => !l.IsDeleted && l.State) | ||
| 337 | + .Where((l, t, tpl) => !t.IsDeleted && t.State) | ||
| 338 | + .Where((l, t, tpl) => !tpl.IsDeleted) | ||
| 339 | + .Where((l, t, tpl) => l.LabelCode == labelCode) | ||
| 340 | + .Select((l, t, tpl) => new | ||
| 341 | + { | ||
| 342 | + l.LocationId, | ||
| 343 | + l.LabelTypeId, | ||
| 344 | + TemplateCode = tpl.TemplateCode | ||
| 345 | + }) | ||
| 346 | + .FirstAsync(); | ||
| 347 | + | ||
| 348 | + if (labelRow is null) | ||
| 349 | + { | ||
| 350 | + throw new UserFriendlyException("标签不存在或不可用"); | ||
| 351 | + } | ||
| 352 | + | ||
| 353 | + if (!string.Equals(labelRow.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase)) | ||
| 354 | + { | ||
| 355 | + throw new UserFriendlyException("该标签不属于当前门店"); | ||
| 356 | + } | ||
| 357 | + | ||
| 358 | + // 解析模板 elements(与预览一致的渲染数据) | ||
| 359 | + var resolvedTemplate = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo | ||
| 360 | + { | ||
| 361 | + LabelCode = labelCode, | ||
| 362 | + ProductId = input.ProductId?.Trim(), | ||
| 363 | + BaseTime = input.BaseTime, | ||
| 364 | + PrintInputJson = input.PrintInputJson | ||
| 365 | + }); | ||
| 366 | + | ||
| 367 | + var printInputJsonStr = input.PrintInputJson is null | ||
| 368 | + ? null | ||
| 369 | + : JsonSerializer.Serialize(input.PrintInputJson); | ||
| 370 | + var renderDataJsonStr = JsonSerializer.Serialize(resolvedTemplate); | ||
| 371 | + | ||
| 372 | + var now = DateTime.Now; | ||
| 373 | + var currentUserId = CurrentUser?.Id?.ToString(); | ||
| 374 | + var taskId = _guidGenerator.Create().ToString(); | ||
| 375 | + | ||
| 376 | + var task = new FlLabelPrintTaskDbEntity | ||
| 377 | + { | ||
| 378 | + Id = taskId, | ||
| 379 | + IsDeleted = false, | ||
| 380 | + CreationTime = now, | ||
| 381 | + CreatorId = currentUserId, | ||
| 382 | + ConcurrencyStamp = _guidGenerator.Create().ToString("N"), | ||
| 383 | + LocationId = locationId, | ||
| 384 | + LabelCode = labelCode, | ||
| 385 | + ProductId = input.ProductId?.Trim(), | ||
| 386 | + LabelTypeId = labelRow.LabelTypeId, | ||
| 387 | + TemplateCode = labelRow.TemplateCode, | ||
| 388 | + PrintQuantity = quantity, | ||
| 389 | + BaseTime = input.BaseTime, | ||
| 390 | + PrinterId = input.PrinterId?.Trim(), | ||
| 391 | + PrinterMac = input.PrinterMac?.Trim(), | ||
| 392 | + PrinterAddress = input.PrinterAddress?.Trim() | ||
| 393 | + }; | ||
| 394 | + | ||
| 395 | + await _dbContext.SqlSugarClient.Insertable(task).ExecuteCommandAsync(); | ||
| 396 | + | ||
| 397 | + var dataRows = Enumerable.Range(1, quantity).Select(i => new FlLabelPrintDataDbEntity | ||
| 398 | + { | ||
| 399 | + Id = _guidGenerator.Create().ToString(), | ||
| 400 | + IsDeleted = false, | ||
| 401 | + CreationTime = now, | ||
| 402 | + CreatorId = currentUserId, | ||
| 403 | + ConcurrencyStamp = _guidGenerator.Create().ToString("N"), | ||
| 404 | + TaskId = taskId, | ||
| 405 | + CopyIndex = i, | ||
| 406 | + PrintInputJson = printInputJsonStr, | ||
| 407 | + RenderDataJson = renderDataJsonStr | ||
| 408 | + }).ToList(); | ||
| 409 | + | ||
| 410 | + await _dbContext.SqlSugarClient.Insertable(dataRows).ExecuteCommandAsync(); | ||
| 411 | + | ||
| 412 | + return new UsAppLabelPrintOutputDto | ||
| 413 | + { | ||
| 414 | + TaskId = taskId, | ||
| 415 | + PrintQuantity = quantity | ||
| 416 | + }; | ||
| 417 | + } | ||
| 418 | + | ||
| 419 | + private ISugarQueryable<FlLabelProductDbEntity, FlLabelDbEntity, FlProductDbEntity, FlLabelCategoryDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity, FlProductCategoryDbEntity> BuildLabelingJoinQuery( | ||
| 420 | + string locationId, | ||
| 421 | + List<string> productIds, | ||
| 422 | + string? filterCategoryId, | ||
| 423 | + string? keyword) | ||
| 424 | + { | ||
| 425 | + var q = _dbContext.SqlSugarClient | ||
| 426 | + .Queryable<FlLabelProductDbEntity>() | ||
| 427 | + .InnerJoin<FlLabelDbEntity>((lp, l) => lp.LabelId == l.Id) | ||
| 428 | + .InnerJoin<FlProductDbEntity>((lp, l, p) => lp.ProductId == p.Id) | ||
| 429 | + .InnerJoin<FlLabelCategoryDbEntity>((lp, l, p, c) => l.LabelCategoryId == c.Id) | ||
| 430 | + .InnerJoin<FlLabelTypeDbEntity>((lp, l, p, c, t) => l.LabelTypeId == t.Id) | ||
| 431 | + .InnerJoin<FlLabelTemplateDbEntity>((lp, l, p, c, t, tpl) => l.TemplateId == tpl.Id) | ||
| 432 | + .LeftJoin<FlProductCategoryDbEntity>((lp, l, p, c, t, tpl, pc) => p.CategoryId == pc.Id) | ||
| 433 | + .Where((lp, l, p, c, t, tpl, pc) => productIds.Contains(p.Id)) | ||
| 434 | + .Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId) | ||
| 435 | + .Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State) | ||
| 436 | + .Where((lp, l, p, c, t, tpl, pc) => !p.IsDeleted && p.State) | ||
| 437 | + .Where((lp, l, p, c, t, tpl, pc) => !c.IsDeleted && c.State) | ||
| 438 | + .Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State) | ||
| 439 | + .Where((lp, l, p, c, t, tpl, pc) => !tpl.IsDeleted) | ||
| 440 | + .WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId) | ||
| 441 | + .WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) => | ||
| 442 | + (l.LabelName != null && l.LabelName.Contains(keyword!)) || | ||
| 443 | + (p.ProductName != null && p.ProductName.Contains(keyword!)) || | ||
| 444 | + (pc.CategoryName != null && pc.CategoryName.Contains(keyword!)) || | ||
| 445 | + (c.CategoryName != null && c.CategoryName.Contains(keyword!)) || | ||
| 446 | + (t.TypeName != null && t.TypeName.Contains(keyword!)) || | ||
| 447 | + (l.LabelCode != null && l.LabelCode.Contains(keyword!))); | ||
| 448 | + | ||
| 449 | + return q; | ||
| 450 | + } | ||
| 451 | + | ||
| 452 | + private sealed class LabelingTreeRow | ||
| 453 | + { | ||
| 454 | + public string LabelCategoryId { get; set; } = string.Empty; | ||
| 455 | + | ||
| 456 | + public string? LabelCategoryName { get; set; } | ||
| 457 | + | ||
| 458 | + public string? LabelCategoryPhotoUrl { get; set; } | ||
| 459 | + | ||
| 460 | + public int LabelCategoryOrderNum { get; set; } | ||
| 461 | + | ||
| 462 | + public string? ProductCategoryId { get; set; } | ||
| 463 | + | ||
| 464 | + public string? ProductCategoryName { get; set; } | ||
| 465 | + | ||
| 466 | + public string? ProductCategoryPhotoUrl { get; set; } | ||
| 467 | + | ||
| 468 | + public string ProductId { get; set; } = string.Empty; | ||
| 469 | + | ||
| 470 | + public string? ProductName { get; set; } | ||
| 471 | + | ||
| 472 | + public string? ProductCode { get; set; } | ||
| 473 | + | ||
| 474 | + public string? ProductImageUrl { get; set; } | ||
| 475 | + | ||
| 476 | + public string LabelTypeId { get; set; } = string.Empty; | ||
| 477 | + | ||
| 478 | + public string? TypeName { get; set; } | ||
| 479 | + | ||
| 480 | + public int TypeOrderNum { get; set; } | ||
| 481 | + | ||
| 482 | + public string LabelCode { get; set; } = string.Empty; | ||
| 483 | + | ||
| 484 | + public string? TemplateCode { get; set; } | ||
| 485 | + | ||
| 486 | + public decimal TemplateWidth { get; set; } | ||
| 487 | + | ||
| 488 | + public decimal TemplateHeight { get; set; } | ||
| 489 | + | ||
| 490 | + public string TemplateUnit { get; set; } = "inch"; | ||
| 491 | + } | ||
| 492 | + | ||
| 493 | + private static string NormalizeCategoryName(string? categoryName) | ||
| 494 | + { | ||
| 495 | + var s = categoryName?.Trim(); | ||
| 496 | + return string.IsNullOrWhiteSpace(s) ? "无" : s; | ||
| 497 | + } | ||
| 498 | + | ||
| 499 | + private static string? NormalizeNullableId(string? id) | ||
| 500 | + { | ||
| 501 | + var s = id?.Trim(); | ||
| 502 | + return string.IsNullOrWhiteSpace(s) ? null : s; | ||
| 503 | + } | ||
| 504 | + | ||
| 505 | + private static string? NormalizeNullableUrl(string? url) | ||
| 506 | + { | ||
| 507 | + var s = url?.Trim(); | ||
| 508 | + return string.IsNullOrWhiteSpace(s) ? null : s; | ||
| 509 | + } | ||
| 510 | + | ||
| 511 | + private static UsAppLabelTypeNodeDto BuildLabelTypeNode(LabelingTreeRow r) | ||
| 512 | + { | ||
| 513 | + return new UsAppLabelTypeNodeDto | ||
| 514 | + { | ||
| 515 | + LabelTypeId = r.LabelTypeId, | ||
| 516 | + TypeName = r.TypeName ?? string.Empty, | ||
| 517 | + OrderNum = r.TypeOrderNum, | ||
| 518 | + LabelCode = r.LabelCode ?? string.Empty, | ||
| 519 | + TemplateCode = r.TemplateCode, | ||
| 520 | + LabelSizeText = FormatLabelSize(r.TemplateWidth, r.TemplateHeight, r.TemplateUnit) | ||
| 521 | + }; | ||
| 522 | + } | ||
| 523 | + | ||
| 524 | + private static string? FormatLabelSize(decimal w, decimal h, string unit) | ||
| 525 | + { | ||
| 526 | + var u = (unit ?? "inch").Trim().ToLowerInvariant(); | ||
| 527 | + var ws = w.ToString(CultureInfo.InvariantCulture); | ||
| 528 | + var hs = h.ToString(CultureInfo.InvariantCulture); | ||
| 529 | + return u is "inch" or "in" | ||
| 530 | + ? $"{ws}\"x{hs}\"" | ||
| 531 | + : $"{ws}x{hs}{u}"; | ||
| 532 | + } | ||
| 533 | +} |
项目相关文档/产品模块Categories接口对接说明.md
| @@ -6,7 +6,8 @@ | @@ -6,7 +6,8 @@ | ||
| 6 | 6 | ||
| 7 | - **模块**:`food-labeling-us` | 7 | - **模块**:`food-labeling-us` |
| 8 | - **接口前缀**:宿主统一前缀为 `/api/app` | 8 | - **接口前缀**:宿主统一前缀为 `/api/app` |
| 9 | -- **分类表**:`fl_label_category` | 9 | +- **分类表**:`fl_product_category` |
| 10 | +- **关联字段**:`fl_product.category_id` → `fl_product_category.id` | ||
| 10 | - **图片字段**:`CategoryPhotoUrl`(前端字段:`categoryPhotoUrl`) | 11 | - **图片字段**:`CategoryPhotoUrl`(前端字段:`categoryPhotoUrl`) |
| 11 | 12 | ||
| 12 | > 说明:本文以 Swagger 为准(本地示例:`http://localhost:19001/swagger`,搜索 `ProductCategory`)。 | 13 | > 说明:本文以 Swagger 为准(本地示例:`http://localhost:19001/swagger`,搜索 `ProductCategory`)。 |
项目相关文档/标签模块接口对接说明.md
| @@ -6,7 +6,7 @@ Swagger 地址: | @@ -6,7 +6,7 @@ Swagger 地址: | ||
| 6 | - `http://localhost:19001/swagger` | 6 | - `http://localhost:19001/swagger` |
| 7 | 7 | ||
| 8 | 说明: | 8 | 说明: |
| 9 | -- 接口最终 URL 以 Swagger 展示为准(可在 Swagger 里搜索 `LabelCategory / LabelType / LabelMultipleOption / LabelTemplate / Label`)。 | 9 | +- 接口最终 URL 以 Swagger 展示为准(可在 Swagger 里搜索 `LabelCategory / LabelType / LabelMultipleOption / LabelTemplate / Label / UsAppLabeling`)。 |
| 10 | - 本模块后端接口以各 AppService 的方法签名自动暴露。 | 10 | - 本模块后端接口以各 AppService 的方法签名自动暴露。 |
| 11 | - 返回分页统一包含 `PageIndex / PageSize / TotalCount / TotalPages / Items`。 | 11 | - 返回分页统一包含 `PageIndex / PageSize / TotalCount / TotalPages / Items`。 |
| 12 | 12 | ||
| @@ -22,6 +22,7 @@ Swagger 地址: | @@ -22,6 +22,7 @@ Swagger 地址: | ||
| 22 | - `label-multiple-option` | 22 | - `label-multiple-option` |
| 23 | - `label-template` | 23 | - `label-template` |
| 24 | - `label` | 24 | - `label` |
| 25 | + - `us-app-labeling`(App 端 Labeling 四级树) | ||
| 25 | 26 | ||
| 26 | --- | 27 | --- |
| 27 | 28 | ||
| @@ -588,3 +589,297 @@ Swagger 地址: | @@ -588,3 +589,297 @@ Swagger 地址: | ||
| 588 | 入参: | 589 | 入参: |
| 589 | - `id`:门店Id | 590 | - `id`:门店Id |
| 590 | 591 | ||
| 592 | +--- | ||
| 593 | + | ||
| 594 | +## 接口 8:App Labeling 四级列表(门店打标页) | ||
| 595 | + | ||
| 596 | +**场景**:美国版 UniApp「Labeling」页:左侧 **标签分类(Label Category)** → 主区域按 **产品分类(Product Category)** 折叠分组 → **产品(Product)** 卡片 → 点选后底部弹层展示 **标签种类(Label Type)**。 | ||
| 597 | + | ||
| 598 | +**实现**:`UsAppLabelingAppService.GetLabelingTreeAsync`,约定式 API 控制器名 **`us-app-labeling`**(与 `UsAppAuth` → `us-app-auth` 同规则)。 | ||
| 599 | + | ||
| 600 | +### 8.1 获取四级嵌套树 | ||
| 601 | + | ||
| 602 | +#### HTTP | ||
| 603 | + | ||
| 604 | +- **方法**:`GET` | ||
| 605 | +- **路径**:`/api/app/us-app-labeling/labeling-tree`(若与 Swagger 不一致,**以 Swagger 为准**) | ||
| 606 | +- **鉴权**:需要登录(`Authorization: Bearer ...`);可使用 App 登录或 Web 账号 Token,需能通过 `[Authorize]`。当前用户可选门店列表见 **`/api/app/us-app-auth/my-locations`**(说明见 `美国版App登录接口说明.md`)。 | ||
| 607 | + | ||
| 608 | +#### 入参(Query:`UsAppLabelingTreeInputVo`) | ||
| 609 | + | ||
| 610 | +| 参数名 | 类型 | 必填 | 说明 | | ||
| 611 | +|--------|------|------|------| | ||
| 612 | +| `locationId` | string | 是 | 当前门店 Id(`location.Id`,与 `fl_location_product.LocationId`、`fl_label.LocationId` 一致) | | ||
| 613 | +| `keyword` | string | 否 | 模糊过滤:标签名、产品名、产品分类、**标签分类**名、标签类型名、`labelCode` 等(实现见服务内 `WhereIF`) | | ||
| 614 | +| `labelCategoryId` | string | 否 | 侧边栏只展示某一 **标签分类** 时传入;不传则返回当前门店下出现的全部标签分类节点 | | ||
| 615 | + | ||
| 616 | +#### 数据范围与表关联(便于联调对照) | ||
| 617 | + | ||
| 618 | +- **门店内商品**:`fl_location_product`(先有 `locationId` 下的 `productId` 集合)。 | ||
| 619 | +- **参与连接的表**:`fl_label_product`(标签-产品)、`fl_label`(`LocationId` 须为当前门店且未删除、启用)、`fl_product`、`fl_label_category`、`fl_label_type`、`fl_label_template`。 | ||
| 620 | +- **第二级「产品分类」**:来自 `fl_product.CategoryName`,trim 后为空则归并为显示名 **`无`**。 | ||
| 621 | +- **第四级去重**:同一产品在同一标签分类、同一门店下,多条 `fl_label` 若 **`labelCode` 相同**,只保留一条用于列表(预览/打印仍用返回的 `labelCode` 等业务字段)。 | ||
| 622 | + | ||
| 623 | +#### 出参(`List<UsAppLabelCategoryTreeNodeDto>`) | ||
| 624 | + | ||
| 625 | +若宿主对成功结果有统一包装,业务数组一般在 **`data`** 中;下列为 **解包后的数组项** 结构。 | ||
| 626 | + | ||
| 627 | +**L1 `UsAppLabelCategoryTreeNodeDto`(标签分类)** | ||
| 628 | + | ||
| 629 | +| 字段 | 类型 | 说明 | | ||
| 630 | +|------|------|------| | ||
| 631 | +| `id` | string | `fl_label_category.Id` | | ||
| 632 | +| `categoryName` | string | 分类名称 | | ||
| 633 | +| `categoryPhotoUrl` | string \| null | 分类图标/图 | | ||
| 634 | +| `orderNum` | number | 排序 | | ||
| 635 | +| `productCategories` | array | 第二级列表(见下表) | | ||
| 636 | + | ||
| 637 | +**L2 `UsAppProductCategoryNodeDto`(产品分类)** | ||
| 638 | + | ||
| 639 | +| 字段 | 类型 | 说明 | | ||
| 640 | +|------|------|------| | ||
| 641 | +| `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 | | ||
| 642 | +| `categoryPhotoUrl` | string \| null | 产品分类图片地址;产品未归类或分类不存在时为空 | | ||
| 643 | +| `name` | string | 产品分类显示名;空源数据为 **`无`** | | ||
| 644 | +| `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) | | ||
| 645 | +| `products` | array | 第三级产品列表(见下表) | | ||
| 646 | + | ||
| 647 | +**L3 `UsAppLabelingProductNodeDto`(产品)** | ||
| 648 | + | ||
| 649 | +| 字段 | 类型 | 说明 | | ||
| 650 | +|------|------|------| | ||
| 651 | +| `productId` | string | `fl_product.Id` | | ||
| 652 | +| `productName` | string | 产品名称 | | ||
| 653 | +| `productCode` | string | 产品编码 | | ||
| 654 | +| `productImageUrl` | string \| null | 主图 | | ||
| 655 | +| `subtitle` | string | 卡片副标题:**有 `productCode` 则显示编码,否则「无」**(与原型「Basic」等独立文案不同,需另行扩展字段时再对齐) | | ||
| 656 | +| `labelTypeCount` | number | 第四级条数,可用于角标「N Types」 | | ||
| 657 | +| `labelTypes` | array | 第四级(见下表) | | ||
| 658 | + | ||
| 659 | +**L4 `UsAppLabelTypeNodeDto`(标签种类 / 可选项)** | ||
| 660 | + | ||
| 661 | +| 字段 | 类型 | 说明 | | ||
| 662 | +|------|------|------| | ||
| 663 | +| `labelTypeId` | string | `fl_label_type.Id` | | ||
| 664 | +| `typeName` | string | 类型名称(如 Defrost) | | ||
| 665 | +| `orderNum` | number | 排序 | | ||
| 666 | +| `labelCode` | string | 业务标签编码,后续预览、打印流程使用 | | ||
| 667 | +| `templateCode` | string \| null | 关联模板编码 | | ||
| 668 | +| `labelSizeText` | string \| null | 尺寸文案;`inch` 常用格式如 `2"x2"` | | ||
| 669 | + | ||
| 670 | +#### 错误与边界 | ||
| 671 | + | ||
| 672 | +- `locationId` 为空:返回友好错误 **「门店Id不能为空」**。 | ||
| 673 | +- 门店下无关联产品:返回 **空数组** `[]`。 | ||
| 674 | +- 有产品但无任何符合条件的标签关联:返回 **空数组** `[]`。 | ||
| 675 | + | ||
| 676 | +#### 请求示例 | ||
| 677 | + | ||
| 678 | +```http | ||
| 679 | +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 | ||
| 680 | +Host: localhost:19001 | ||
| 681 | +Authorization: Bearer eyJhbGciOi... | ||
| 682 | +``` | ||
| 683 | + | ||
| 684 | +**curl**(Token 取自登录响应的 `data.token` 整段,已含 `Bearer ` 前缀时直接放入 Header): | ||
| 685 | + | ||
| 686 | +```bash | ||
| 687 | +curl -X GET "http://localhost:19001/api/app/us-app-labeling/labeling-tree?locationId=11111111-1111-1111-1111-111111111111" \ | ||
| 688 | + -H "Authorization: <data.token>" | ||
| 689 | +``` | ||
| 690 | + | ||
| 691 | +#### 响应结构示例(解包后) | ||
| 692 | + | ||
| 693 | +```json | ||
| 694 | +[ | ||
| 695 | + { | ||
| 696 | + "id": "cat-prep-id", | ||
| 697 | + "categoryName": "Prep", | ||
| 698 | + "categoryPhotoUrl": "/picture/...", | ||
| 699 | + "orderNum": 1, | ||
| 700 | + "productCategories": [ | ||
| 701 | + { | ||
| 702 | + "categoryId": "pc-meat-id", | ||
| 703 | + "categoryPhotoUrl": "/picture/product-category/20260325123010_xxx.png", | ||
| 704 | + "name": "Meat", | ||
| 705 | + "itemCount": 1, | ||
| 706 | + "products": [ | ||
| 707 | + { | ||
| 708 | + "productId": "prod-chicken-id", | ||
| 709 | + "productName": "Chicken", | ||
| 710 | + "productCode": "CHK-001", | ||
| 711 | + "productImageUrl": "/picture/...", | ||
| 712 | + "subtitle": "CHK-001", | ||
| 713 | + "labelTypeCount": 3, | ||
| 714 | + "labelTypes": [ | ||
| 715 | + { | ||
| 716 | + "labelTypeId": "lt-defrost", | ||
| 717 | + "typeName": "Defrost", | ||
| 718 | + "orderNum": 1, | ||
| 719 | + "labelCode": "LBL_CHICKEN_DEFROST", | ||
| 720 | + "templateCode": "TPL_2X2", | ||
| 721 | + "labelSizeText": "2\"x2\"" | ||
| 722 | + } | ||
| 723 | + ] | ||
| 724 | + } | ||
| 725 | + ] | ||
| 726 | + } | ||
| 727 | + ] | ||
| 728 | + } | ||
| 729 | +] | ||
| 730 | +``` | ||
| 731 | + | ||
| 732 | +> 前端 Axios 若项目约定 **GET 使用 `data` 配置对象** 传参,请仍绑定到与上述 Query 同名的字段(`locationId`、`keyword`、`labelCategoryId`),与 URL Query 等价即可。 | ||
| 733 | + | ||
| 734 | +### 8.2 App 打印预览(elements 渲染结构) | ||
| 735 | + | ||
| 736 | +**场景**:用户选择某个 Product + Label Type 进入「Label Preview」页面,需要把模板预览区域渲染出来。 | ||
| 737 | +后端根据 `labelCode` 读取模板(`fl_label_template` + `fl_label_template_element`),并将 AUTO_DB / PRINT_INPUT 的值渲染回每个 element 的 `config`,前端按 `elements` 自行绘制预览。 | ||
| 738 | + | ||
| 739 | +#### HTTP | ||
| 740 | + | ||
| 741 | +- **方法**:`POST` | ||
| 742 | +- **路径**:`/api/app/us-app-labeling/preview`(若与 Swagger 不一致,**以 Swagger 为准**) | ||
| 743 | +- **鉴权**:需要登录(`Authorization: Bearer ...`) | ||
| 744 | + | ||
| 745 | +#### 入参(Body:`UsAppLabelPreviewInputVo`) | ||
| 746 | + | ||
| 747 | +| 参数名(JSON) | 类型 | 必填 | 说明 | | ||
| 748 | +|---|---|---|---| | ||
| 749 | +| `locationId` | string | 是 | 门店Id(校验 `fl_label.LocationId` 必须一致) | | ||
| 750 | +| `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) | | ||
| 751 | +| `productId` | string | 否 | 预览用产品Id;不传则默认取该标签绑定的第一个产品(用于 AUTO_DB 数据填充) | | ||
| 752 | +| `baseTime` | string | 否 | 业务基准时间(用于 DATE/TIME 元素计算;不传则用服务器当前时间) | | ||
| 753 | +| `printInputJson` | object | 否 | 打印输入(用于 PRINT_INPUT 元素),key 建议与模板元素 `inputKey` 对齐 | | ||
| 754 | + | ||
| 755 | +#### 出参(`UsAppLabelPreviewDto`) | ||
| 756 | + | ||
| 757 | +除顶部信息外,核心是 `template`(供前端画布渲染): | ||
| 758 | + | ||
| 759 | +- `template`:`LabelTemplatePreviewDto` | ||
| 760 | + - `width` / `height` / `unit`:模板物理尺寸 | ||
| 761 | + - `elements[]`:元素数组(对齐前端 editor JSON:`id/type/x/y/width/height/rotation/border/zIndex/orderNum/config`) | ||
| 762 | + | ||
| 763 | +`elements[].config` 内常用字段(示例): | ||
| 764 | + | ||
| 765 | +- 文本类(如 `TEXT_PRODUCT` / `TEXT_STATIC` / `TEXT_PRICE`):`config.text` | ||
| 766 | +- 条码/二维码(`BARCODE` / `QRCODE`):`config.data` | ||
| 767 | +- 日期/时间(`DATE` / `TIME`):`config.format`(后端已计算并写回) | ||
| 768 | + | ||
| 769 | +#### 数据来源说明 | ||
| 770 | + | ||
| 771 | +- 模板头:`fl_label_template` | ||
| 772 | +- 模板元素:`fl_label_template_element`(按 `OrderNum` + `ZIndex` 排序) | ||
| 773 | +- 标签归属:`fl_label`(校验 `labelCode` 存在且 `LocationId == locationId`) | ||
| 774 | + | ||
| 775 | +#### 错误与边界 | ||
| 776 | + | ||
| 777 | +- `locationId` 为空:友好错误 **「门店Id不能为空」**。 | ||
| 778 | +- `labelCode` 为空:友好错误 **「labelCode不能为空」**。 | ||
| 779 | +- 标签不存在:友好错误 **「标签不存在」**。 | ||
| 780 | +- 模板不存在:友好错误 **「模板不存在」**。 | ||
| 781 | +- 标签不属于当前门店:友好错误 **「该标签不属于当前门店」**。 | ||
| 782 | +- 标签未绑定产品且未传 `productId`:友好错误 **「该标签未绑定产品,无法预览」**。 | ||
| 783 | + | ||
| 784 | +#### 请求示例 | ||
| 785 | + | ||
| 786 | +```json | ||
| 787 | +{ | ||
| 788 | + "locationId": "11111111-1111-1111-1111-111111111111", | ||
| 789 | + "labelCode": "LBL_CHICKEN_DEFROST", | ||
| 790 | + "productId": "22222222-2222-2222-2222-222222222222", | ||
| 791 | + "baseTime": "2026-03-26T10:30:00", | ||
| 792 | + "printInputJson": { | ||
| 793 | + "price": "12.99" | ||
| 794 | + } | ||
| 795 | +} | ||
| 796 | +``` | ||
| 797 | + | ||
| 798 | +**curl:** | ||
| 799 | + | ||
| 800 | +```bash | ||
| 801 | +curl -X POST "http://localhost:19001/api/app/us-app-labeling/preview" \ | ||
| 802 | + -H "Authorization: <data.token>" \ | ||
| 803 | + -H "Content-Type: application/json" \ | ||
| 804 | + -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"}}' | ||
| 805 | +``` | ||
| 806 | + | ||
| 807 | +--- | ||
| 808 | + | ||
| 809 | +## 接口 9:App 打印(落库打印任务与明细) | ||
| 810 | + | ||
| 811 | +**场景**:移动端预览确认后点击 **Print**。后端负责把“本次打印”写入数据库,方便追溯/统计/重打。 | ||
| 812 | + | ||
| 813 | +### 9.1 创建打印任务并写入明细 | ||
| 814 | + | ||
| 815 | +#### HTTP | ||
| 816 | + | ||
| 817 | +- **方法**:`POST` | ||
| 818 | +- **路径**:`/api/app/us-app-labeling/print`(若与 Swagger 不一致,**以 Swagger 为准**) | ||
| 819 | +- **鉴权**:需要登录(`Authorization: Bearer ...`) | ||
| 820 | + | ||
| 821 | +#### 入参(Body:`UsAppLabelPrintInputVo`) | ||
| 822 | + | ||
| 823 | +| 参数名(JSON) | 类型 | 必填 | 说明 | | ||
| 824 | +|---|---|---|---| | ||
| 825 | +| `locationId` | string | 是 | 门店Id(校验 `fl_label.LocationId` 必须一致) | | ||
| 826 | +| `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) | | ||
| 827 | +| `productId` | string | 否 | 打印用产品Id;不传则默认取该标签绑定的第一个产品(用于模板解析) | | ||
| 828 | +| `printQuantity` | number | 否 | 打印份数;`<=0` 按 1 处理 | | ||
| 829 | +| `baseTime` | string | 否 | 业务基准时间(用于 DATE/TIME 元素计算) | | ||
| 830 | +| `printInputJson` | object | 否 | 打印输入(用于模板 PRINT_INPUT 元素),key 建议与模板元素 `inputKey` 对齐 | | ||
| 831 | +| `printerId` | string | 否 | 打印机Id(可选,用于追踪) | | ||
| 832 | +| `printerMac` | string | 否 | 打印机蓝牙 MAC(可选) | | ||
| 833 | +| `printerAddress` | string | 否 | 打印机地址(可选) | | ||
| 834 | + | ||
| 835 | +#### 数据落库说明 | ||
| 836 | + | ||
| 837 | +- **任务表**:`fl_label_print_task` | ||
| 838 | + - 插入 1 条任务记录(`locationId / labelCode / productId / labelTypeId / templateCode / printQuantity / baseTime / printer...` 等)。 | ||
| 839 | +- **明细表**:`fl_label_print_data` | ||
| 840 | + - 按 `printQuantity` 插入 N 条明细记录(`copyIndex = 1..N`)。 | ||
| 841 | + - `printInputJson`:保存本次打印的原始输入(JSON 字符串)。 | ||
| 842 | + - `renderDataJson`:保存本次解析后的模板预览结构(`LabelTemplatePreviewDto`,包含 resolved 后的 `elements[].config`),供追溯/重打使用。 | ||
| 843 | + | ||
| 844 | +> 模板解析的数据源来自 `fl_label_template` + `fl_label_template_element`,与预览接口一致。 | ||
| 845 | + | ||
| 846 | +#### 出参(`UsAppLabelPrintOutputDto`) | ||
| 847 | + | ||
| 848 | +| 字段 | 类型 | 说明 | | ||
| 849 | +|---|---|---| | ||
| 850 | +| `taskId` | string | 打印任务Id(用于后续查询/重打/统计) | | ||
| 851 | +| `printQuantity` | number | 实际写入的份数 | | ||
| 852 | + | ||
| 853 | +#### 错误与边界 | ||
| 854 | + | ||
| 855 | +- `locationId` 为空:友好错误 **「门店Id不能为空」**。 | ||
| 856 | +- `labelCode` 为空:友好错误 **「labelCode不能为空」**。 | ||
| 857 | +- 标签不存在/不可用:友好错误 **「标签不存在或不可用」**。 | ||
| 858 | +- 标签不属于当前门店:友好错误 **「该标签不属于当前门店」**。 | ||
| 859 | +- 标签未绑定产品且未传 `productId`:友好错误 **「该标签未绑定产品,无法预览」**(模板解析阶段抛出)。 | ||
| 860 | + | ||
| 861 | +#### 请求示例 | ||
| 862 | + | ||
| 863 | +```json | ||
| 864 | +{ | ||
| 865 | + "locationId": "11111111-1111-1111-1111-111111111111", | ||
| 866 | + "labelCode": "LBL_CHICKEN_DEFROST", | ||
| 867 | + "productId": "22222222-2222-2222-2222-222222222222", | ||
| 868 | + "printQuantity": 2, | ||
| 869 | + "baseTime": "2026-03-26T10:30:00", | ||
| 870 | + "printInputJson": { | ||
| 871 | + "price": "12.99" | ||
| 872 | + }, | ||
| 873 | + "printerMac": "AA:BB:CC:DD:EE:FF" | ||
| 874 | +} | ||
| 875 | +``` | ||
| 876 | + | ||
| 877 | +**curl:** | ||
| 878 | + | ||
| 879 | +```bash | ||
| 880 | +curl -X POST "http://localhost:19001/api/app/us-app-labeling/print" \ | ||
| 881 | + -H "Authorization: <data.token>" \ | ||
| 882 | + -H "Content-Type: application/json" \ | ||
| 883 | + -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"}}' | ||
| 884 | +``` | ||
| 885 | + |