using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Contracts.Dtos.Label; using FoodLabeling.Application.Contracts.Dtos.LabelTemplate; using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; using FoodLabeling.Application.Contracts.IServices; using FoodLabeling.Application.Helpers; using FoodLabeling.Application.Services.DbModels; using FoodLabeling.Domain.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SqlSugar; using Volo.Abp; using Volo.Abp.Application.Services; using Volo.Abp.Guids; using Volo.Abp.Uow; using Yi.Framework.Rbac.Domain.Entities; using Yi.Framework.SqlSugarCore.Abstractions; namespace FoodLabeling.Application.Services; /// /// App Labeling:四级列表(标签分类 → 产品分类 → 产品 → 标签种类) /// public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppService { private readonly ISqlSugarDbContext _dbContext; private readonly ILabelAppService _labelAppService; private readonly IGuidGenerator _guidGenerator; private readonly ISqlSugarRepository _userRepository; public UsAppLabelingAppService( ISqlSugarDbContext dbContext, ILabelAppService labelAppService, IGuidGenerator guidGenerator, ISqlSugarRepository userRepository) { _dbContext = dbContext; _labelAppService = labelAppService; _guidGenerator = guidGenerator; _userRepository = userRepository; } /// /// 获取当前门店下四级嵌套数据 /// /// /// L1 标签分类 fl_label_category;L2 产品分类 fl_product.CategoryId join fl_product_category; /// L3 产品;L4 与该门店、该标签分类、该产品关联的标签实例(fl_label + fl_label_type)。 /// [Authorize] public virtual async Task> GetLabelingTreeAsync(UsAppLabelingTreeInputVo input) { if (string.IsNullOrWhiteSpace(input.LocationId)) { throw new UserFriendlyException("门店Id不能为空"); } var locationId = input.LocationId.Trim(); var keyword = input.Keyword?.Trim(); var filterCategoryId = input.LabelCategoryId?.Trim(); var productIds = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.LocationId == locationId) .Select(x => x.ProductId) .ToListAsync(); if (productIds.Count == 0) { return new List(); } var query = BuildLabelingJoinQuery(locationId, productIds, filterCategoryId, keyword); var raw = await query .Select((lp, l, p, c, t, tpl, pc) => new LabelingTreeRow { LabelCategoryId = c.Id, LabelCategoryName = c.CategoryName, LabelCategoryPhotoUrl = c.CategoryPhotoUrl, LabelCategoryOrderNum = c.OrderNum, ProductCategoryId = p.CategoryId, ProductCategoryName = pc.CategoryName, ProductCategoryPhotoUrl = pc.CategoryPhotoUrl, ProductId = p.Id, ProductName = p.ProductName, ProductCode = p.ProductCode, ProductImageUrl = p.ProductImageUrl, LabelTypeId = t.Id, TypeName = t.TypeName, TypeOrderNum = t.OrderNum, LabelCode = l.LabelCode, TemplateCode = tpl.TemplateCode, TemplateWidth = tpl.Width, TemplateHeight = tpl.Height, TemplateUnit = tpl.Unit }) .ToListAsync(); if (raw.Count == 0) { return new List(); } var byL1 = raw.GroupBy(x => new { x.LabelCategoryId, x.LabelCategoryName, x.LabelCategoryPhotoUrl, x.LabelCategoryOrderNum }).OrderBy(g => g.Key.LabelCategoryOrderNum).ThenBy(g => g.Key.LabelCategoryName); var result = new List(); foreach (var g1 in byL1) { var l1 = new UsAppLabelCategoryTreeNodeDto { Id = g1.Key.LabelCategoryId, CategoryName = g1.Key.LabelCategoryName ?? string.Empty, CategoryPhotoUrl = g1.Key.LabelCategoryPhotoUrl, OrderNum = g1.Key.LabelCategoryOrderNum, ProductCategories = new List() }; var byL2 = g1.GroupBy(x => { var categoryId = NormalizeNullableId(x.ProductCategoryId); if (categoryId is null) { return new { CategoryId = (string?)null, CategoryName = "无", CategoryPhotoUrl = (string?)null }; } var categoryName = NormalizeCategoryName(x.ProductCategoryName); var categoryPhotoUrl = NormalizeNullableUrl(x.ProductCategoryPhotoUrl); return new { CategoryId = (string?)categoryId, CategoryName = categoryName, CategoryPhotoUrl = categoryPhotoUrl }; }) .OrderBy(g => g.Key.CategoryName); foreach (var g2 in byL2) { var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName); var l2 = new UsAppProductCategoryNodeDto { CategoryId = g2.Key.CategoryId, CategoryPhotoUrl = g2.Key.CategoryPhotoUrl, Name = g2.Key.CategoryName, ItemCount = productsGrouped.Count(), Products = new List() }; foreach (var g3 in productsGrouped) { var first = g3.First(); var typeNodes = g3 .GroupBy(r => r.LabelCode) .Select(gr => BuildLabelTypeNode(gr.First())) .OrderBy(t => t.OrderNum) .ThenBy(t => t.TypeName) .ToList(); var subtitle = string.IsNullOrWhiteSpace(first.ProductCode?.Trim()) ? "无" : first.ProductCode!.Trim(); l2.Products.Add(new UsAppLabelingProductNodeDto { ProductId = first.ProductId, ProductName = first.ProductName ?? string.Empty, ProductCode = first.ProductCode ?? string.Empty, ProductImageUrl = first.ProductImageUrl, Subtitle = subtitle, LabelTypeCount = typeNodes.Count, LabelTypes = typeNodes }); } l1.ProductCategories.Add(l2); } result.Add(l1); } return result; } /// /// App 打印预览:按标签编码解析模板并返回顶部展示字段 + 预览模板结构 /// /// /// 示例请求: /// ```json /// { /// "locationId": "LOC001", /// "labelCode": "LBL0001", /// "productId": "PROD001", /// "baseTime": "2026-03-26T10:30:00", /// "printInputJson": { /// "price": "12.99" /// } /// } /// ``` /// /// 预览入参 /// 顶部字段 + 预览模板结构 /// 成功 /// 参数错误/数据不存在 /// 服务器错误 [Authorize] public virtual async Task PreviewAsync(UsAppLabelPreviewInputVo input) { if (input is null) { throw new UserFriendlyException("入参不能为空"); } var locationId = input.LocationId?.Trim(); if (string.IsNullOrWhiteSpace(locationId)) { throw new UserFriendlyException("门店Id不能为空"); } var labelCode = input.LabelCode?.Trim(); if (string.IsNullOrWhiteSpace(labelCode)) { throw new UserFriendlyException("labelCode不能为空"); } var labelRow = await _dbContext.SqlSugarClient .Queryable( (l, c, t, tpl) => l.LabelCategoryId == c.Id && l.LabelTypeId == t.Id && l.TemplateId == tpl.Id) .Where((l, c, t, tpl) => !l.IsDeleted && l.State) .Where((l, c, t, tpl) => !c.IsDeleted && c.State) .Where((l, c, t, tpl) => !t.IsDeleted && t.State) .Where((l, c, t, tpl) => !tpl.IsDeleted) .Where((l, c, t, tpl) => l.LabelCode == labelCode) .Select((l, c, t, tpl) => new { l.Id, l.LabelCode, l.LocationId, l.LabelTypeId, l.TemplateId, l.LastModificationTime, l.CreationTime, LabelCategoryName = c.CategoryName, TypeName = t.TypeName, TemplateCode = tpl.TemplateCode, TemplateWidth = tpl.Width, TemplateHeight = tpl.Height, TemplateUnit = tpl.Unit }) .FirstAsync(); if (labelRow is null) { throw new UserFriendlyException("标签不存在或不可用"); } if (!string.Equals(labelRow.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase)) { throw new UserFriendlyException("该标签不属于当前门店"); } var previewProductId = await ResolvePreviewProductIdAsync(labelRow.Id, input.ProductId); var template = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo { LabelCode = labelCode, ProductId = previewProductId, BaseTime = input.BaseTime, PrintInputJson = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value) }); Dictionary? templateProductDefaultValues = null; if (!string.IsNullOrWhiteSpace(previewProductId)) { var productDefault = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.TemplateId == labelRow.TemplateId) .Where(x => x.ProductId == previewProductId) .Where(x => x.LabelTypeId == labelRow.LabelTypeId) .OrderBy(x => x.OrderNum) .FirstAsync(); if (!string.IsNullOrWhiteSpace(productDefault?.DefaultValuesJson)) { try { templateProductDefaultValues = JsonSerializer.Deserialize>(productDefault.DefaultValuesJson!); } catch { templateProductDefaultValues = null; } } } var productName = string.Empty; var productCategoryName = "无"; if (!string.IsNullOrWhiteSpace(previewProductId)) { var p = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.State && x.Id == previewProductId); if (p is not null) { productName = p.ProductName ?? string.Empty; if (!string.IsNullOrWhiteSpace(p.CategoryId)) { var pc = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.State && x.Id == p.CategoryId); productCategoryName = NormalizeCategoryName(pc?.CategoryName); } } } return new UsAppLabelPreviewDto { LabelId = labelRow.Id, LocationId = locationId, LabelCode = labelCode, TemplateCode = labelRow.TemplateCode, LabelSizeText = FormatLabelSize(labelRow.TemplateWidth, labelRow.TemplateHeight, labelRow.TemplateUnit), TypeName = labelRow.TypeName, ProductName = string.IsNullOrWhiteSpace(productName) ? null : productName, ProductCategoryName = productCategoryName, LabelCategoryName = labelRow.LabelCategoryName, LabelLastEdited = labelRow.LastModificationTime ?? labelRow.CreationTime, PreviewImageBase64Png = null, Template = template, TemplateProductDefaultValues = templateProductDefaultValues }; } /// /// App 打印:创建打印任务并落库打印明细(fl_label_print_task / fl_label_print_data) /// /// 打印入参 /// 任务Id [Authorize] [UnitOfWork] public virtual async Task PrintAsync(UsAppLabelPrintInputVo input) { if (input is null) { throw new UserFriendlyException("入参不能为空"); } var locationId = input.LocationId?.Trim(); if (string.IsNullOrWhiteSpace(locationId)) { throw new UserFriendlyException("门店Id不能为空"); } var labelCode = input.LabelCode?.Trim(); if (string.IsNullOrWhiteSpace(labelCode)) { throw new UserFriendlyException("labelCode不能为空"); } var quantity = input.PrintQuantity <= 0 ? 1 : input.PrintQuantity; var clientRequestId = input.ClientRequestId?.Trim(); if (!string.IsNullOrWhiteSpace(clientRequestId)) { // 幂等:同一个 clientRequestId 重复调用,直接返回首次创建的任务集合 var existed = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.ClientRequestId == clientRequestId) .OrderBy(x => x.CopyIndex) .ToListAsync(); if (existed is not null && existed.Count > 0) { var existedBatchId = existed.First().BatchId; var existedTaskIds = existed.Select(x => x.Id).ToList(); return new UsAppLabelPrintOutputDto { TaskId = existedTaskIds.FirstOrDefault() ?? string.Empty, PrintQuantity = existedTaskIds.Count, BatchId = existedBatchId, TaskIds = existedTaskIds }; } } // 校验 label + location,并补齐一些顶部字段用于任务表落库 var labelRow = await _dbContext.SqlSugarClient .Queryable( (l, t, tpl) => l.LabelTypeId == t.Id && l.TemplateId == tpl.Id) .Where((l, t, tpl) => !l.IsDeleted && l.State) .Where((l, t, tpl) => !t.IsDeleted && t.State) .Where((l, t, tpl) => !tpl.IsDeleted) .Where((l, t, tpl) => l.LabelCode == labelCode) .Select((l, t, tpl) => new { l.Id, l.LocationId, l.LabelTypeId, l.TemplateId }) .FirstAsync(); if (labelRow is null) { throw new UserFriendlyException("标签不存在或不可用"); } if (!string.Equals(labelRow.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase)) { throw new UserFriendlyException("该标签不属于当前门店"); } var previewProductId = await ResolvePreviewProductIdAsync(labelRow.Id, input.ProductId); var normalizedPrintInput = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value); // 解析模板 elements(与预览一致的渲染数据) var resolvedTemplate = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo { LabelCode = labelCode, ProductId = previewProductId, BaseTime = input.BaseTime, PrintInputJson = normalizedPrintInput }); var templateProductDefaultValuesJson = await ResolveTemplateProductDefaultValuesJsonAsync( labelRow.TemplateId, previewProductId, labelRow.LabelTypeId); var printInputJsonStr = input.PrintInputJson is null ? null : JsonSerializer.Serialize(input.PrintInputJson); var renderTemplateJsonStr = JsonSerializer.Serialize(resolvedTemplate); var now = DateTime.Now; var currentUserId = CurrentUser?.Id?.ToString(); var batchId = _guidGenerator.Create().ToString(); var taskIds = new List(); for (var i = 1; i <= quantity; i++) { var taskId = _guidGenerator.Create().ToString(); taskIds.Add(taskId); var task = new FlLabelPrintTaskDbEntity { Id = taskId, BatchId = batchId, CopyIndex = i, ClientRequestId = string.IsNullOrWhiteSpace(clientRequestId) ? null : clientRequestId, LabelId = labelRow.Id, TemplateId = labelRow.TemplateId, LabelTypeId = labelRow.LabelTypeId, ProductId = previewProductId, LocationId = locationId, BaseTime = input.BaseTime, PrintInputJson = printInputJsonStr, TemplateProductDefaultValuesJson = templateProductDefaultValuesJson, RenderTemplateJson = renderTemplateJsonStr, PrinterId = input.PrinterId?.Trim(), PrinterMac = input.PrinterMac?.Trim(), PrinterAddress = input.PrinterAddress?.Trim(), Status = "CREATED", PrintedAt = null, ErrorMessage = null, CreatedBy = currentUserId, CreationTime = now }; await _dbContext.SqlSugarClient.Insertable(task).ExecuteCommandAsync(); var rows = resolvedTemplate.Elements.Select(e => { var cfgJson = e.ConfigJson is null ? null : JsonSerializer.Serialize(e.ConfigJson); string? renderValue = null; if (e.ConfigJson is JsonElement je && je.ValueKind == JsonValueKind.Object && je.TryGetProperty("text", out var tv)) { renderValue = tv.ValueKind == JsonValueKind.String ? tv.GetString() : tv.ToString(); } else if (e.ConfigJson is Dictionary dict && dict.TryGetValue("text", out var v)) { renderValue = v?.ToString(); } return new FlLabelPrintDataDbEntity { Id = _guidGenerator.Create().ToString(), PrintTaskId = taskId, ElementId = e.Id?.Trim() ?? string.Empty, ElementName = e.ElementName?.Trim(), RenderValue = renderValue, RenderConfigJson = cfgJson }; }).Where(x => !string.IsNullOrWhiteSpace(x.ElementId)).ToList(); if (rows.Count > 0) { await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); } } return new UsAppLabelPrintOutputDto { TaskId = taskIds.FirstOrDefault() ?? string.Empty, PrintQuantity = quantity, BatchId = batchId, TaskIds = taskIds }; } /// /// App 重新打印:根据历史任务Id重打(创建新任务与明细) /// [Authorize] [UnitOfWork] public virtual async Task ReprintAsync(UsAppLabelReprintInputVo input) { if (input is null) { throw new UserFriendlyException("入参不能为空"); } var locationId = input.LocationId?.Trim(); if (string.IsNullOrWhiteSpace(locationId)) { throw new UserFriendlyException("门店Id不能为空"); } var taskId = input.TaskId?.Trim(); if (string.IsNullOrWhiteSpace(taskId)) { throw new UserFriendlyException("taskId不能为空"); } var quantity = input.PrintQuantity <= 0 ? 1 : input.PrintQuantity; var clientRequestId = input.ClientRequestId?.Trim(); if (!string.IsNullOrWhiteSpace(clientRequestId)) { var existed = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.ClientRequestId == clientRequestId) .OrderBy(x => x.CopyIndex) .ToListAsync(); if (existed is not null && existed.Count > 0) { var existedBatchId = existed.First().BatchId; var existedTaskIds = existed.Select(x => x.Id).ToList(); return new UsAppLabelPrintOutputDto { TaskId = existedTaskIds.FirstOrDefault() ?? string.Empty, PrintQuantity = existedTaskIds.Count, BatchId = existedBatchId, TaskIds = existedTaskIds }; } } var currentUserId = CurrentUser?.Id?.ToString(); if (string.IsNullOrWhiteSpace(currentUserId)) { throw new UserFriendlyException("未登录"); } var old = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => x.Id == taskId); if (old is null) { throw new UserFriendlyException("打印任务不存在"); } // 仅允许重打自己在当前门店的任务 if (!string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase)) { throw new UserFriendlyException("无权限重打该任务"); } if (!string.Equals(old.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase)) { throw new UserFriendlyException("该任务不属于当前门店"); } LabelTemplatePreviewDto? resolvedTemplate = null; try { resolvedTemplate = JsonSerializer.Deserialize(old.RenderTemplateJson); } catch { resolvedTemplate = null; } if (resolvedTemplate is null) { throw new UserFriendlyException("历史任务渲染快照解析失败,无法重打"); } var now = DateTime.Now; var batchId = _guidGenerator.Create().ToString(); var taskIds = new List(); for (var i = 1; i <= quantity; i++) { var newTaskId = _guidGenerator.Create().ToString(); taskIds.Add(newTaskId); var newTask = new FlLabelPrintTaskDbEntity { Id = newTaskId, BatchId = batchId, CopyIndex = i, ClientRequestId = string.IsNullOrWhiteSpace(clientRequestId) ? null : clientRequestId, LabelId = old.LabelId, TemplateId = old.TemplateId, LabelTypeId = old.LabelTypeId, ProductId = old.ProductId, LocationId = old.LocationId, BaseTime = old.BaseTime, PrintInputJson = old.PrintInputJson, TemplateProductDefaultValuesJson = old.TemplateProductDefaultValuesJson, RenderTemplateJson = old.RenderTemplateJson, PrinterId = string.IsNullOrWhiteSpace(input.PrinterId) ? old.PrinterId : input.PrinterId.Trim(), PrinterMac = string.IsNullOrWhiteSpace(input.PrinterMac) ? old.PrinterMac : input.PrinterMac.Trim(), PrinterAddress = string.IsNullOrWhiteSpace(input.PrinterAddress) ? old.PrinterAddress : input.PrinterAddress.Trim(), Status = "CREATED", PrintedAt = null, ErrorMessage = null, CreatedBy = currentUserId, CreationTime = now }; await _dbContext.SqlSugarClient.Insertable(newTask).ExecuteCommandAsync(); var rows = resolvedTemplate.Elements.Select(e => { var cfgJson = e.ConfigJson is null ? null : JsonSerializer.Serialize(e.ConfigJson); string? renderValue = null; if (e.ConfigJson is JsonElement je && je.ValueKind == JsonValueKind.Object && je.TryGetProperty("text", out var tv)) { renderValue = tv.ValueKind == JsonValueKind.String ? tv.GetString() : tv.ToString(); } else if (e.ConfigJson is Dictionary dict && dict.TryGetValue("text", out var v)) { renderValue = v?.ToString(); } return new FlLabelPrintDataDbEntity { Id = _guidGenerator.Create().ToString(), PrintTaskId = newTaskId, ElementId = e.Id?.Trim() ?? string.Empty, ElementName = e.ElementName?.Trim(), RenderValue = renderValue, RenderConfigJson = cfgJson }; }).Where(x => !string.IsNullOrWhiteSpace(x.ElementId)).ToList(); if (rows.Count > 0) { await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); } } return new UsAppLabelPrintOutputDto { TaskId = taskIds.FirstOrDefault() ?? string.Empty, PrintQuantity = quantity, BatchId = batchId, TaskIds = taskIds }; } /// /// App 打印日志:获取当前登录账号在当前门店打印的记录(分页,时间倒序) /// /// /// 仅返回满足: /// - CreatedBy == CurrentUser.Id /// - LocationId == input.LocationId /// 的打印任务记录(fl_label_print_task)。 /// /// 示例请求: /// ```json /// { /// "locationId": "11111111-1111-1111-1111-111111111111", /// "skipCount": 1, /// "maxResultCount": 20 /// } /// ``` /// /// 参数说明: /// - locationId: 当前门店 Id(必填) /// - skipCount: 页码(从 1 开始,遵循本项目约定) /// - maxResultCount: 每页条数 /// /// 分页查询入参 /// 分页打印日志 /// 成功 /// 参数错误/未登录 /// 服务器错误 [Authorize] [HttpPost] public virtual async Task> GetPrintLogListAsync(PrintLogGetListInputVo input) { if (input is null) { throw new UserFriendlyException("入参不能为空"); } if (!CurrentUser.Id.HasValue) { throw new UserFriendlyException("用户未登录"); } var locationId = input.LocationId?.Trim(); if (string.IsNullOrWhiteSpace(locationId)) { throw new UserFriendlyException("门店Id不能为空"); } var currentUserIdStr = CurrentUser.Id.Value.ToString(); var currentUser = await _userRepository.GetByIdAsync(CurrentUser.Id.Value); var operatorName = currentUser?.Name?.Trim() ?? string.Empty; var locationName = "无"; if (Guid.TryParse(locationId, out var locationGuid)) { var loc = await _dbContext.SqlSugarClient.Queryable() .Where(x => !x.IsDeleted && x.Id == locationGuid) .Select(x => new { x.LocationCode, x.LocationName }) .FirstAsync(); if (loc is not null) { var name = loc.LocationName?.Trim(); if (!string.IsNullOrWhiteSpace(name)) { locationName = name; } } } RefAsync total = 0; var query = _dbContext.SqlSugarClient .Queryable() .LeftJoin((t, l) => t.LabelId == l.Id) .LeftJoin((t, l, p) => t.ProductId == p.Id) .LeftJoin((t, l, p, lt) => t.LabelTypeId == lt.Id) .LeftJoin((t, l, p, lt, tpl) => t.TemplateId == tpl.Id) .Where((t, l, p, lt, tpl) => t.CreatedBy == currentUserIdStr && t.LocationId == locationId) .OrderBy((t, l, p, lt, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc) .OrderBy((t, l, p, lt, tpl) => t.CreationTime, OrderByType.Desc) .Select((t, l, p, lt, tpl) => new { t.Id, t.BatchId, t.CopyIndex, t.LabelId, LabelCode = l.LabelCode, t.ProductId, ProductName = p.ProductName, TypeName = lt.TypeName, TemplateWidth = tpl.Width, TemplateHeight = tpl.Height, TemplateUnit = tpl.Unit, t.PrintInputJson, t.PrintedAt, t.CreationTime }); var pageRows = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); var items = pageRows.Select(x => new PrintLogItemDto { TaskId = x.Id, BatchId = x.BatchId, CopyIndex = x.CopyIndex, LabelId = x.LabelId, LabelCode = x.LabelCode ?? string.Empty, ProductId = x.ProductId, ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), TypeName = x.TypeName ?? string.Empty, LabelSizeText = FormatLabelSizeWithUnit(x.TemplateWidth, x.TemplateHeight, x.TemplateUnit), PrintInputJson = x.PrintInputJson, PrintedAt = x.PrintedAt ?? x.CreationTime, OperatorName = operatorName, LocationName = locationName }).ToList(); var pageSize = input.MaxResultCount <= 0 ? items.Count : input.MaxResultCount; var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); var totalCount = (long)total; var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize); return new PagedResultWithPageDto { PageIndex = pageIndex, PageSize = pageSize, TotalCount = totalCount, TotalPages = totalPages, Items = items }; } private async Task ResolveTemplateProductDefaultValuesJsonAsync( string templateId, string? productId, string labelTypeId) { if (string.IsNullOrWhiteSpace(templateId) || string.IsNullOrWhiteSpace(productId) || string.IsNullOrWhiteSpace(labelTypeId)) { return null; } var productDefault = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.TemplateId == templateId) .Where(x => x.ProductId == productId) .Where(x => x.LabelTypeId == labelTypeId) .OrderBy(x => x.OrderNum) .FirstAsync(); return string.IsNullOrWhiteSpace(productDefault?.DefaultValuesJson) ? null : productDefault!.DefaultValuesJson; } private ISugarQueryable BuildLabelingJoinQuery( string locationId, List productIds, string? filterCategoryId, string? keyword) { var q = _dbContext.SqlSugarClient .Queryable() .InnerJoin((lp, l) => lp.LabelId == l.Id) .InnerJoin((lp, l, p) => lp.ProductId == p.Id) .InnerJoin((lp, l, p, c) => l.LabelCategoryId == c.Id) .InnerJoin((lp, l, p, c, t) => l.LabelTypeId == t.Id) .InnerJoin((lp, l, p, c, t, tpl) => l.TemplateId == tpl.Id) .LeftJoin((lp, l, p, c, t, tpl, pc) => p.CategoryId == pc.Id) .Where((lp, l, p, c, t, tpl, pc) => productIds.Contains(p.Id)) .Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId) .Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State) .Where((lp, l, p, c, t, tpl, pc) => !p.IsDeleted && p.State) .Where((lp, l, p, c, t, tpl, pc) => !c.IsDeleted && c.State) .Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State) .Where((lp, l, p, c, t, tpl, pc) => !tpl.IsDeleted) .WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId) .WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) => (l.LabelName != null && l.LabelName.Contains(keyword!)) || (p.ProductName != null && p.ProductName.Contains(keyword!)) || (pc.CategoryName != null && pc.CategoryName.Contains(keyword!)) || (c.CategoryName != null && c.CategoryName.Contains(keyword!)) || (t.TypeName != null && t.TypeName.Contains(keyword!)) || (l.LabelCode != null && l.LabelCode.Contains(keyword!))); return q; } private sealed class LabelingTreeRow { public string LabelCategoryId { get; set; } = string.Empty; public string? LabelCategoryName { get; set; } public string? LabelCategoryPhotoUrl { get; set; } public int LabelCategoryOrderNum { get; set; } public string? ProductCategoryId { get; set; } public string? ProductCategoryName { get; set; } public string? ProductCategoryPhotoUrl { get; set; } public string ProductId { get; set; } = string.Empty; public string? ProductName { get; set; } public string? ProductCode { get; set; } public string? ProductImageUrl { get; set; } public string LabelTypeId { get; set; } = string.Empty; public string? TypeName { get; set; } public int TypeOrderNum { get; set; } public string LabelCode { get; set; } = string.Empty; public string? TemplateCode { get; set; } public decimal TemplateWidth { get; set; } public decimal TemplateHeight { get; set; } public string TemplateUnit { get; set; } = "inch"; } private static string NormalizeCategoryName(string? categoryName) { var s = categoryName?.Trim(); return string.IsNullOrWhiteSpace(s) ? "无" : s; } private static string? NormalizeNullableId(string? id) { var s = id?.Trim(); return string.IsNullOrWhiteSpace(s) ? null : s; } private static string? NormalizeNullableUrl(string? url) { var s = url?.Trim(); return string.IsNullOrWhiteSpace(s) ? null : s; } private async Task ResolvePreviewProductIdAsync(string labelId, string? productId) { var resolvedProductId = productId?.Trim(); if (!string.IsNullOrWhiteSpace(resolvedProductId)) { return resolvedProductId; } return await _dbContext.SqlSugarClient.Queryable() .Where(x => x.LabelId == labelId) .Select(x => x.ProductId) .FirstAsync(); } private static UsAppLabelTypeNodeDto BuildLabelTypeNode(LabelingTreeRow r) { return new UsAppLabelTypeNodeDto { LabelTypeId = r.LabelTypeId, TypeName = r.TypeName ?? string.Empty, OrderNum = r.TypeOrderNum, LabelCode = r.LabelCode ?? string.Empty, TemplateCode = r.TemplateCode, LabelSizeText = FormatLabelSize(r.TemplateWidth, r.TemplateHeight, r.TemplateUnit) }; } private static string? FormatLabelSize(decimal w, decimal h, string unit) { var u = (unit ?? "inch").Trim().ToLowerInvariant(); var ws = w.ToString(CultureInfo.InvariantCulture); var hs = h.ToString(CultureInfo.InvariantCulture); return u is "inch" or "in" ? $"{ws}\"x{hs}\"" : $"{ws}x{hs}{u}"; } private static string? FormatLabelSizeWithUnit(decimal w, decimal h, string unit) { var u = (unit ?? "inch").Trim().ToLowerInvariant(); var ws = w.ToString(CultureInfo.InvariantCulture); var hs = h.ToString(CultureInfo.InvariantCulture); var normalizedUnit = u is "in" ? "inch" : u; return $"{ws}x{hs}{normalizedUnit}"; } }