using System.Text.Json; using FoodLabeling.Application.Helpers; using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Contracts.Dtos.LabelTemplate; using FoodLabeling.Application.Contracts.IServices; using FoodLabeling.Application.Services.DbModels; using SqlSugar; using Volo.Abp; using Volo.Abp.Application.Services; using Volo.Abp.Guids; using Volo.Abp.Uow; using Yi.Framework.SqlSugarCore.Abstractions; namespace FoodLabeling.Application.Services; /// /// 标签模板管理(Label Templates) /// public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppService { private readonly ISqlSugarDbContext _dbContext; private readonly IGuidGenerator _guidGenerator; public LabelTemplateAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator) { _dbContext = dbContext; _guidGenerator = guidGenerator; } public async Task> GetListAsync(LabelTemplateGetListInputVo input) { input ??= new LabelTemplateGetListInputVo(); RefAsync total = 0; var keyword = input.Keyword?.Trim(); var scopedLocationIds = await LocationScopeBindingHelper.ResolveFilteredLocationIdsForListAsync( _dbContext.SqlSugarClient, input.PartnerId, input.GroupId, input.LocationId); var query = _dbContext.SqlSugarClient.Queryable() .Where(x => !x.IsDeleted) .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.TemplateName.Contains(keyword!) || x.TemplateCode.Contains(keyword!)) .WhereIF(!string.IsNullOrWhiteSpace(input.LabelType), x => x.LabelType == input.LabelType) .WhereIF(input.State != null, x => x.State == input.State); query = await LabelTemplateScopeHelper.ApplyTemplateScopeFilterAsync( _dbContext.SqlSugarClient, query, scopedLocationIds); query = ApplyLabelTemplateListSorting(query, input.Sorting); query = LabelTemplateQueryHelper.ProjectListColumns(query); var pageSize = input.MaxResultCount <= 0 ? 10 : input.MaxResultCount; var pageEntities = await query.ToPageListAsync(input.SkipCount, pageSize, total); var templateIds = pageEntities.Select(x => x.Id).ToList(); var elementCountMap = new Dictionary(StringComparer.Ordinal); if (templateIds.Count > 0) { var elementCounts = await _dbContext.SqlSugarClient.Queryable() .Where(x => templateIds.Contains(x.TemplateId)) .GroupBy(x => x.TemplateId) .Select(x => new { TemplateId = x.TemplateId, Count = SqlFunc.AggregateCount(x.Id) }) .ToListAsync(); elementCountMap = elementCounts.ToDictionary(x => x.TemplateId, x => (int)x.Count); } var scopeMap = await LabelTemplateScopeHelper.BuildScopeDisplayMapAsync( _dbContext.SqlSugarClient, pageEntities); var itemsMap = templateIds.Count > 0 ? await LabelTemplateListItemsHelper.ResolveTemplateItemsMapAsync( _dbContext.SqlSugarClient, templateIds) : new Dictionary(StringComparer.Ordinal); var items = pageEntities.Select(x => { scopeMap.TryGetValue(x.Id, out var scope); itemsMap.TryGetValue(x.Id, out var itemsDisplay); var lastEdited = x.LastModificationTime ?? x.CreationTime; var contentsCount = elementCountMap.TryGetValue(x.Id, out var c) ? c : 0; var locationDisplay = scope?.Location ?? EmptyDisplay; var sizeText = $"{x.Width}x{x.Height}{x.Unit}"; return new LabelTemplateGetListOutputDto { Id = x.TemplateCode, // front-end uses templateCode as identifier TemplateCode = x.TemplateCode, TemplateName = x.TemplateName, LabelType = x.LabelType, LocationText = locationDisplay, Company = scope?.Company ?? string.Empty, Region = scope?.Region ?? string.Empty, Location = locationDisplay, PartnerIds = scope?.PartnerIds ?? new List(), CompanyIds = scope?.PartnerIds ?? new List(), RegionIds = scope?.RegionIds ?? new List(), GroupIds = scope?.RegionIds ?? new List(), LocationIds = scope?.LocationIds ?? new List(), ContentsCount = contentsCount, Items = itemsDisplay?.Items ?? "无", ItemNames = itemsDisplay?.ItemNames ?? new List(), SizeText = sizeText, VersionNo = x.VersionNo, LastEdited = lastEdited }; }).ToList(); return BuildPagedResult(input.SkipCount, pageSize, total, items); } /// /// 列表排序:白名单字段 + IFNULL(LastModificationTime, CreationTime),避免 ?? 与原始 Sorting 拼接导致 SQL 异常。 /// private static ISugarQueryable ApplyLabelTemplateListSorting( ISugarQueryable query, string? sorting) { if (!string.IsNullOrWhiteSpace(sorting)) { var s = sorting.Trim(); if (s.Equals("TemplateName asc", StringComparison.OrdinalIgnoreCase)) { return query.OrderBy(x => x.TemplateName); } if (s.Equals("TemplateName desc", StringComparison.OrdinalIgnoreCase)) { return query.OrderByDescending(x => x.TemplateName); } if (s.Equals("TemplateCode asc", StringComparison.OrdinalIgnoreCase)) { return query.OrderBy(x => x.TemplateCode); } if (s.Equals("TemplateCode desc", StringComparison.OrdinalIgnoreCase)) { return query.OrderByDescending(x => x.TemplateCode); } if (s.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase)) { return query.OrderBy(x => x.CreationTime); } if (s.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase)) { return query.OrderByDescending(x => x.CreationTime); } if (s.Equals("LastModificationTime asc", StringComparison.OrdinalIgnoreCase)) { return query.OrderBy(x => x.LastModificationTime); } if (s.Equals("LastModificationTime desc", StringComparison.OrdinalIgnoreCase)) { return query.OrderByDescending(x => x.LastModificationTime); } } return query .OrderBy(x => SqlFunc.IsNull(x.LastModificationTime, x.CreationTime), OrderByType.Desc) .OrderBy(x => x.TemplateCode, OrderByType.Asc); } public async Task GetAsync(string id) { var template = await LabelTemplateQueryHelper.ProjectListColumns( _dbContext.SqlSugarClient.Queryable()) .FirstAsync(x => !x.IsDeleted && x.TemplateCode == id); if (template is null) { throw new UserFriendlyException("模板不存在"); } var elements = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.TemplateId == template.Id) .OrderBy(x => x.OrderNum) .ToListAsync(); List MapElements() { return elements.Select(e => { object? cfg = null; if (!string.IsNullOrWhiteSpace(e.ConfigJson)) { cfg = JsonSerializer.Deserialize(e.ConfigJson); } return new LabelTemplateElementDto { Id = e.ElementKey, ElementType = e.ElementType, TypeAdd = e.TypeAdd, ElementName = e.ElementName, PosX = e.PosX, PosY = e.PosY, Width = e.Width, Height = e.Height, Rotation = e.Rotation, BorderType = e.BorderType, ZIndex = e.ZIndex, OrderNum = e.OrderNum, ValueSourceType = e.ValueSourceType, BindingExpr = e.BindingExpr, AutoQueryKey = e.AutoQueryKey, InputKey = e.InputKey, IsRequiredInput = e.IsRequiredInput, ConfigJson = cfg }; }).ToList(); } var defaultRows = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.TemplateId == template.Id) .OrderBy(x => x.OrderNum) .ToListAsync(); var productDefaults = defaultRows.Select(x => { object? defaults = null; if (!string.IsNullOrWhiteSpace(x.DefaultValuesJson)) { defaults = JsonSerializer.Deserialize(x.DefaultValuesJson); } return new LabelTemplateProductDefaultDto { ProductId = x.ProductId, LabelTypeId = x.LabelTypeId, DefaultValues = defaults, OrderNum = x.OrderNum }; }).ToList(); var dto = new LabelTemplateGetOutputDto { Id = template.TemplateCode, TemplateCode = template.TemplateCode, TemplateName = template.TemplateName, LabelType = template.LabelType, Unit = template.Unit, Width = template.Width, Height = template.Height, AppliedLocationType = template.AppliedLocationType, AppliedPartnerType = LabelTemplateScopeHelper.ScopeAll, AppliedRegionType = LabelTemplateScopeHelper.ScopeAll, ShowRuler = template.ShowRuler, ShowGrid = template.ShowGrid, BorderType = NormalizeTemplateBorderType(template.BorderType), PrintOrientationType = NormalizePrintOrientationType(template.PrintOrientationType), VersionNo = template.VersionNo, State = template.State, Elements = MapElements(), TemplateProductDefaults = productDefaults }; await FillTemplateScopeOnDtoAsync(dto, template); return dto; } [UnitOfWork] public async Task CreateAsync(LabelTemplateCreateInputVo input) { var code = input.TemplateCode?.Trim(); var name = input.TemplateName?.Trim(); if (string.IsNullOrWhiteSpace(code)) { throw new UserFriendlyException("模板编码不能为空"); } if (string.IsNullOrWhiteSpace(name)) { throw new UserFriendlyException("模板名称不能为空"); } var duplicated = await LabelTemplateQueryHelper.ProjectListColumns( _dbContext.SqlSugarClient.Queryable()) .AnyAsync(x => !x.IsDeleted && x.TemplateCode == code); if (duplicated) { throw new UserFriendlyException("模板编码已存在"); } var scope = await LabelTemplateScopeHelper.ResolveScopeForSaveAsync(_dbContext.SqlSugarClient, input); var now = DateTime.Now; var templateId = _guidGenerator.Create().ToString(); var entity = new FlLabelTemplateDbEntity { Id = templateId, IsDeleted = false, CreationTime = now, CreatorId = CurrentUser?.Id?.ToString(), LastModifierId = CurrentUser?.Id?.ToString(), LastModificationTime = now, ConcurrencyStamp = string.Empty, TemplateCode = code, TemplateName = name, LabelType = input.LabelType, Unit = string.IsNullOrWhiteSpace(input.Unit) ? "inch" : input.Unit.Trim(), Width = input.Width, Height = input.Height, AppliedLocationType = scope.AppliedLocationType, ShowRuler = input.ShowRuler, ShowGrid = input.ShowGrid, BorderType = NormalizeTemplateBorderType(input.BorderType), PrintOrientationType = NormalizePrintOrientationType(input.PrintOrientationType), VersionNo = 1, State = input.State }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); await RebuildTemplateElementsAndDefaultsAsync( entity.Id, input.Elements, new List()); await LabelTemplateScopeHelper.SaveTemplateScopeAsync( _dbContext.SqlSugarClient, _guidGenerator, entity.Id, scope, CurrentUser?.Id?.ToString(), now); await LabelTemplateScopeSchemaHelper.SetAppliedScopeTypesAsync( _dbContext.SqlSugarClient, entity.Id, scope.AppliedPartnerType, scope.AppliedRegionType); return await GetAsync(code); } [UnitOfWork] public async Task UpdateAsync(string id, LabelTemplateUpdateInputVo input) { var template = await LabelTemplateQueryHelper.ProjectListColumns( _dbContext.SqlSugarClient.Queryable()) .FirstAsync(x => !x.IsDeleted && x.TemplateCode == id); if (template is null) { throw new UserFriendlyException("模板不存在"); } var code = input.TemplateCode?.Trim(); var name = input.TemplateName?.Trim(); if (!string.IsNullOrWhiteSpace(code) && !string.Equals(code, template.TemplateCode, StringComparison.OrdinalIgnoreCase)) { var duplicated = await LabelTemplateQueryHelper.ProjectListColumns( _dbContext.SqlSugarClient.Queryable()) .AnyAsync(x => !x.IsDeleted && x.TemplateCode == code); if (duplicated) { throw new UserFriendlyException("模板编码已存在"); } } var scope = await LabelTemplateScopeHelper.ResolveScopeForSaveAsync(_dbContext.SqlSugarClient, input); template.TemplateName = name ?? template.TemplateName; template.LabelType = input.LabelType; template.Unit = string.IsNullOrWhiteSpace(input.Unit) ? template.Unit : input.Unit.Trim(); template.Width = input.Width; template.Height = input.Height; template.AppliedLocationType = scope.AppliedLocationType; template.ShowRuler = input.ShowRuler; template.ShowGrid = input.ShowGrid; template.BorderType = NormalizeTemplateBorderType(input.BorderType); template.PrintOrientationType = NormalizePrintOrientationType(input.PrintOrientationType); template.State = input.State; template.VersionNo = template.VersionNo + 1; template.LastModifierId = CurrentUser?.Id?.ToString(); template.LastModificationTime = DateTime.Now; if (!string.IsNullOrWhiteSpace(code)) { template.TemplateCode = code; } await _dbContext.SqlSugarClient.Updateable(template).ExecuteCommandAsync(); await RebuildTemplateElementsAndDefaultsAsync( template.Id, input.Elements, input.TemplateProductDefaults); await LabelTemplateScopeHelper.SaveTemplateScopeAsync( _dbContext.SqlSugarClient, _guidGenerator, template.Id, scope, CurrentUser?.Id?.ToString(), template.LastModificationTime ?? DateTime.Now); await LabelTemplateScopeSchemaHelper.SetAppliedScopeTypesAsync( _dbContext.SqlSugarClient, template.Id, scope.AppliedPartnerType, scope.AppliedRegionType); return await GetAsync(template.TemplateCode); } [UnitOfWork] public async Task DeleteAsync(string id) { var template = await LabelTemplateQueryHelper.ProjectListColumns( _dbContext.SqlSugarClient.Queryable()) .FirstAsync(x => !x.IsDeleted && x.TemplateCode == id); if (template is null) { return; } var used = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && x.TemplateId == template.Id); if (used) { throw new UserFriendlyException("该模板已被标签引用,无法删除"); } template.IsDeleted = true; template.LastModifierId = CurrentUser?.Id?.ToString(); template.LastModificationTime = DateTime.Now; await _dbContext.SqlSugarClient.Updateable(template).ExecuteCommandAsync(); // 删除子表数据(子表无 IsDeleted 字段) await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.TemplateId == template.Id) .ExecuteCommandAsync(); await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.TemplateId == template.Id) .ExecuteCommandAsync(); if (await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(_dbContext.SqlSugarClient)) { await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.TemplateId == template.Id) .ExecuteCommandAsync(); await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.TemplateId == template.Id) .ExecuteCommandAsync(); } await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.TemplateId == template.Id) .ExecuteCommandAsync(); } private async Task RebuildTemplateElementsAndDefaultsAsync( string templateDbId, List elements, List? templateProductDefaults) { // elements 重建 await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.TemplateId == templateDbId) .ExecuteCommandAsync(); if (elements is not null && elements.Count > 0) { var rows = elements.Select(e => { var elementName = EnsureElementName(e.ElementName); object? cfg = e.ConfigJson; var configJson = cfg == null ? null : JsonSerializer.Serialize(cfg); return new FlLabelTemplateElementDbEntity { Id = _guidGenerator.Create().ToString(), TemplateId = templateDbId, ElementKey = e.Id, ElementType = e.ElementType, TypeAdd = string.IsNullOrWhiteSpace(e.TypeAdd) ? null : e.TypeAdd.Trim(), ElementName = elementName, PosX = e.PosX, PosY = e.PosY, Width = e.Width, Height = e.Height, Rotation = string.IsNullOrWhiteSpace(e.Rotation) ? "horizontal" : e.Rotation, BorderType = string.IsNullOrWhiteSpace(e.BorderType) ? "none" : e.BorderType, ZIndex = e.ZIndex, OrderNum = e.OrderNum, ValueSourceType = string.IsNullOrWhiteSpace(e.ValueSourceType) ? "FIXED" : e.ValueSourceType, BindingExpr = e.BindingExpr, AutoQueryKey = e.AutoQueryKey, InputKey = e.InputKey, IsRequiredInput = e.IsRequiredInput, ConfigJson = configJson }; }).ToList(); await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); } // 模板-产品-标签类型默认值:仅在显式传入时重建,避免普通编辑误清空 if (templateProductDefaults is not null) { var duplicateCheckSet = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var row in templateProductDefaults) { var productId = row.ProductId?.Trim(); var labelTypeId = row.LabelTypeId?.Trim(); if (string.IsNullOrWhiteSpace(productId) || string.IsNullOrWhiteSpace(labelTypeId)) { continue; } var key = $"{productId}::{labelTypeId}"; if (!duplicateCheckSet.Add(key)) { throw new UserFriendlyException($"模板默认值绑定重复:产品[{productId}]与标签类型[{labelTypeId}]只能存在一条"); } } await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.TemplateId == templateDbId) .ExecuteCommandAsync(); if (templateProductDefaults.Count > 0) { var rows = templateProductDefaults.Select((x, idx) => { var productId = x.ProductId?.Trim(); var labelTypeId = x.LabelTypeId?.Trim(); if (string.IsNullOrWhiteSpace(productId)) { throw new UserFriendlyException("模板默认值绑定中,产品Id不能为空"); } if (string.IsNullOrWhiteSpace(labelTypeId)) { throw new UserFriendlyException("模板默认值绑定中,标签类型Id不能为空"); } var json = x.DefaultValues is null ? null : JsonSerializer.Serialize(x.DefaultValues); return new FlLabelTemplateProductDefaultDbEntity { Id = _guidGenerator.Create().ToString(), TemplateId = templateDbId, ProductId = productId, LabelTypeId = labelTypeId, DefaultValuesJson = json, OrderNum = x.OrderNum <= 0 ? idx + 1 : x.OrderNum }; }).ToList(); await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); } } } private const string EmptyDisplay = "无"; private async Task FillTemplateScopeOnDtoAsync(LabelTemplateGetOutputDto dto, FlLabelTemplateDbEntity template) { var scopeMap = await LabelTemplateScopeHelper.BuildScopeDisplayMapAsync( _dbContext.SqlSugarClient, new List { template }); scopeMap.TryGetValue(template.Id, out var scope); dto.Company = scope?.Company ?? string.Empty; dto.Region = scope?.Region ?? string.Empty; dto.Location = scope?.Location ?? string.Empty; dto.AppliedPartnerType = scope?.AppliedPartnerType ?? LabelTemplateScopeHelper.ScopeAll; dto.AppliedRegionType = scope?.AppliedRegionType ?? LabelTemplateScopeHelper.ScopeAll; dto.AppliedLocationType = scope?.AppliedLocationType ?? template.AppliedLocationType; dto.PartnerIds = scope?.PartnerIds ?? new List(); dto.CompanyIds = scope?.PartnerIds ?? new List(); dto.RegionIds = scope?.RegionIds ?? new List(); dto.GroupIds = scope?.RegionIds ?? new List(); dto.LocationIds = scope?.LocationIds ?? new List(); dto.AppliedLocationIds = dto.LocationIds; } private static string EnsureElementName(string? elementName) { var normalizedName = elementName?.Trim(); if (string.IsNullOrWhiteSpace(normalizedName)) { throw new UserFriendlyException("组件名字不能为空"); } return normalizedName; } private static PagedResultWithPageDto BuildPagedResult(int skipCount, int maxResultCount, int total, List items) { var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount); var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize); return new PagedResultWithPageDto { PageIndex = pageIndex, PageSize = pageSize, TotalCount = total, TotalPages = totalPages, Items = items }; } private static PagedResultWithPageDto BuildPagedResult(int skipCount, int maxResultCount, RefAsync total, List items) { var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount); var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total.Value / (double)pageSize); return new PagedResultWithPageDto { PageIndex = pageIndex, PageSize = pageSize, TotalCount = total.Value, TotalPages = totalPages, Items = items }; } private static string NormalizeTemplateBorderType(string? raw) { var v = (raw ?? string.Empty).Trim().ToLowerInvariant(); return v is "line" or "dotted" ? v : "none"; } private static string NormalizePrintOrientationType(string? raw) { var v = (raw ?? string.Empty).Trim().ToLowerInvariant(); return v == "horizontal" ? "horizontal" : "vertical"; } }