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 | 16 | public string? ProductId { get; set; } |
| 17 | 17 | |
| 18 | 18 | /// <summary> |
| 19 | + /// 业务基准时间(用于 DATE/TIME 元素的渲染计算) | |
| 20 | + /// 不传则默认使用服务器当前时间 | |
| 21 | + /// </summary> | |
| 22 | + public DateTime? BaseTime { get; set; } | |
| 23 | + | |
| 24 | + /// <summary> | |
| 19 | 25 | /// 打印输入(前端传,用于 PRINT_INPUT 元素) |
| 20 | 26 | /// key 建议使用模板元素的 InputKey |
| 21 | 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
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetListOutputDto.cs
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetOutputDto.cs
美国版/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
美国版/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 | 137 | |
| 138 | 138 | // 查询 products 并拼接 |
| 139 | 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 | 147 | .ToListAsync(); |
| 146 | 148 | |
| 147 | 149 | var productsMap = productRows |
| ... | ... | @@ -151,7 +153,7 @@ public class LabelAppService : ApplicationService, ILabelAppService |
| 151 | 153 | g => new |
| 152 | 154 | { |
| 153 | 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 | 159 | var locationIds = orderedLabels |
| ... | ... | @@ -474,6 +476,8 @@ public class LabelAppService : ApplicationService, ILabelAppService |
| 474 | 476 | throw new UserFriendlyException("labelCode不能为空"); |
| 475 | 477 | } |
| 476 | 478 | |
| 479 | + var baseTime = input?.BaseTime ?? DateTime.Now; | |
| 480 | + | |
| 477 | 481 | var label = await _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>() |
| 478 | 482 | .FirstAsync(x => !x.IsDeleted && x.LabelCode == labelCode); |
| 479 | 483 | if (label is null) |
| ... | ... | @@ -640,14 +644,14 @@ public class LabelAppService : ApplicationService, ILabelAppService |
| 640 | 644 | case "DATE": |
| 641 | 645 | { |
| 642 | 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 | 648 | UpsertConfigValue(cfg, "format", dt.ToString("yyyy-MM-dd")); |
| 645 | 649 | cfg.Remove("inputType"); |
| 646 | 650 | } |
| 647 | 651 | break; |
| 648 | 652 | case "TIME": |
| 649 | 653 | { |
| 650 | - var dt = DateTime.Now; | |
| 654 | + var dt = baseTime; | |
| 651 | 655 | UpsertConfigValue(cfg, "format", dt.ToString("HH:mm")); |
| 652 | 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 | 30 | RefAsync<int> total = 0; |
| 31 | 31 | var keyword = input.Keyword?.Trim(); |
| 32 | 32 | |
| 33 | - var query = _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() | |
| 33 | + var query = _dbContext.SqlSugarClient | |
| 34 | + .Queryable<FlProductDbEntity>() | |
| 34 | 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 | 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 | 49 | if (!string.IsNullOrWhiteSpace(input.Sorting)) |
| 42 | 50 | { |
| 43 | 51 | query = query.OrderBy(input.Sorting); |
| ... | ... | @@ -64,15 +72,41 @@ public class ProductAppService : ApplicationService, IProductAppService |
| 64 | 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 | 110 | }).ToList(); |
| 77 | 111 | |
| 78 | 112 | return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); |
| ... | ... | @@ -94,6 +128,17 @@ public class ProductAppService : ApplicationService, IProductAppService |
| 94 | 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 | 142 | var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLocationProductDbEntity>() |
| 98 | 143 | .Where(x => x.ProductId == productId) |
| 99 | 144 | .Select(x => x.LocationId) |
| ... | ... | @@ -105,7 +150,8 @@ public class ProductAppService : ApplicationService, IProductAppService |
| 105 | 150 | Id = entity.Id, |
| 106 | 151 | ProductCode = entity.ProductCode, |
| 107 | 152 | ProductName = entity.ProductName, |
| 108 | - CategoryName = entity.CategoryName, | |
| 153 | + CategoryId = entity.CategoryId, | |
| 154 | + CategoryName = categoryName, | |
| 109 | 155 | ProductImageUrl = entity.ProductImageUrl, |
| 110 | 156 | State = entity.State, |
| 111 | 157 | LocationIds = locationIds |
| ... | ... | @@ -135,7 +181,7 @@ public class ProductAppService : ApplicationService, IProductAppService |
| 135 | 181 | IsDeleted = false, |
| 136 | 182 | ProductCode = code, |
| 137 | 183 | ProductName = name, |
| 138 | - CategoryName = input.CategoryName?.Trim(), | |
| 184 | + CategoryId = input.CategoryId?.Trim(), | |
| 139 | 185 | ProductImageUrl = input.ProductImageUrl?.Trim(), |
| 140 | 186 | State = input.State |
| 141 | 187 | }; |
| ... | ... | @@ -176,7 +222,7 @@ public class ProductAppService : ApplicationService, IProductAppService |
| 176 | 222 | |
| 177 | 223 | entity.ProductCode = code; |
| 178 | 224 | entity.ProductName = name; |
| 179 | - entity.CategoryName = input.CategoryName?.Trim(); | |
| 225 | + entity.CategoryId = input.CategoryId?.Trim(); | |
| 180 | 226 | entity.ProductImageUrl = input.ProductImageUrl?.Trim(); |
| 181 | 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 | 32 | RefAsync<int> total = 0; |
| 33 | 33 | var keyword = input.Keyword?.Trim(); |
| 34 | 34 | |
| 35 | - var query = _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() | |
| 35 | + var query = _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() | |
| 36 | 36 | .Where(x => !x.IsDeleted) |
| 37 | 37 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), |
| 38 | 38 | x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!)) |
| 39 | 39 | .WhereIF(input.State != null, x => x.State == input.State); |
| 40 | 40 | |
| 41 | + // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column | |
| 42 | + // 同时避免将 input.Sorting 原样拼接到 SQL(存在注入风险) | |
| 41 | 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 | 68 | else |
| 46 | 69 | { |
| ... | ... | @@ -68,7 +91,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 68 | 91 | /// </summary> |
| 69 | 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 | 95 | .FirstAsync(x => x.Id == id && !x.IsDeleted); |
| 73 | 96 | if (entity is null) |
| 74 | 97 | { |
| ... | ... | @@ -90,16 +113,24 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 90 | 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 | 117 | .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); |
| 95 | 118 | if (duplicated) |
| 96 | 119 | { |
| 97 | 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 | 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 | 134 | CategoryCode = code, |
| 104 | 135 | CategoryName = name, |
| 105 | 136 | CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(), |
| ... | ... | @@ -116,7 +147,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 116 | 147 | /// </summary> |
| 117 | 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 | 151 | .FirstAsync(x => x.Id == id && !x.IsDeleted); |
| 121 | 152 | if (entity is null) |
| 122 | 153 | { |
| ... | ... | @@ -130,7 +161,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 130 | 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 | 165 | .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); |
| 135 | 166 | if (duplicated) |
| 136 | 167 | { |
| ... | ... | @@ -154,19 +185,19 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 154 | 185 | /// </summary> |
| 155 | 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 | 189 | .FirstAsync(x => x.Id == id && !x.IsDeleted); |
| 159 | 190 | if (entity is null) |
| 160 | 191 | { |
| 161 | 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 | 203 | entity.IsDeleted = true; |
| ... | ... | @@ -175,7 +206,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 175 | 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 | 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 | 6 | |
| 7 | 7 | - **模块**:`food-labeling-us` |
| 8 | 8 | - **接口前缀**:宿主统一前缀为 `/api/app` |
| 9 | -- **分类表**:`fl_label_category` | |
| 9 | +- **分类表**:`fl_product_category` | |
| 10 | +- **关联字段**:`fl_product.category_id` → `fl_product_category.id` | |
| 10 | 11 | - **图片字段**:`CategoryPhotoUrl`(前端字段:`categoryPhotoUrl`) |
| 11 | 12 | |
| 12 | 13 | > 说明:本文以 Swagger 为准(本地示例:`http://localhost:19001/swagger`,搜索 `ProductCategory`)。 | ... | ... |
项目相关文档/标签模块接口对接说明.md
| ... | ... | @@ -6,7 +6,7 @@ Swagger 地址: |
| 6 | 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 | 10 | - 本模块后端接口以各 AppService 的方法签名自动暴露。 |
| 11 | 11 | - 返回分页统一包含 `PageIndex / PageSize / TotalCount / TotalPages / Items`。 |
| 12 | 12 | |
| ... | ... | @@ -22,6 +22,7 @@ Swagger 地址: |
| 22 | 22 | - `label-multiple-option` |
| 23 | 23 | - `label-template` |
| 24 | 24 | - `label` |
| 25 | + - `us-app-labeling`(App 端 Labeling 四级树) | |
| 25 | 26 | |
| 26 | 27 | --- |
| 27 | 28 | |
| ... | ... | @@ -588,3 +589,297 @@ Swagger 地址: |
| 588 | 589 | 入参: |
| 589 | 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 | + | ... | ... |