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 FoodLabeling.Domain.Entities; 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) { RefAsync total = 0; var keyword = input.Keyword?.Trim(); var locationId = input.LocationId?.Trim(); var specifiedTemplateIds = new HashSet(); if (!string.IsNullOrWhiteSpace(locationId)) { var rows = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.LocationId == locationId) .Select(x => x.TemplateId) .ToListAsync(); specifiedTemplateIds = rows.ToHashSet(); } 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); if (!string.IsNullOrWhiteSpace(locationId)) { query = query.Where(x => x.AppliedLocationType == "ALL" || specifiedTemplateIds.Contains(x.Id)); } query = !string.IsNullOrWhiteSpace(input.Sorting) ? query.OrderBy(input.Sorting) : query.OrderByDescending(x => x.LastModificationTime ?? x.CreationTime); var pageEntities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); var templateIds = pageEntities.Select(x => x.Id).ToList(); // element count (Contents) 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(); var elementCountMap = elementCounts.ToDictionary(x => x.TemplateId, x => (int)x.Count); // applied locations (for LocationText) var templateLocationRows = await _dbContext.SqlSugarClient.Queryable() .Where(x => templateIds.Contains(x.TemplateId)) .ToListAsync(); var locationIdSet = templateLocationRows.Select(x => x.LocationId).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList(); var locationGuidList = locationIdSet .Where(x => Guid.TryParse(x, out _)) .Select(Guid.Parse) .Distinct() .ToList(); var locationMap = new Dictionary(); if (locationGuidList.Count > 0) { var locations = await _dbContext.SqlSugarClient.Queryable() .Where(x => !x.IsDeleted) .Where(x => locationGuidList.Contains(x.Id)) .ToListAsync(); locationMap = locations.ToDictionary(x => x.Id, x => x); } string GetFirstLocationName(string templateDbId) { var first = templateLocationRows.FirstOrDefault(x => x.TemplateId == templateDbId); if (first is null) return "Specified"; if (Guid.TryParse(first.LocationId, out var gid) && locationMap.TryGetValue(gid, out var loc)) { return loc.LocationName ?? loc.LocationCode; } return "Specified"; } var items = pageEntities.Select(x => { var lastEdited = x.LastModificationTime ?? x.CreationTime; var contentsCount = elementCountMap.TryGetValue(x.Id, out var c) ? c : 0; var locationText = x.AppliedLocationType == "ALL" ? "All Locations" : GetFirstLocationName(x.Id); 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 = locationText, ContentsCount = contentsCount, SizeText = sizeText, VersionNo = x.VersionNo, LastEdited = lastEdited }; }).ToList(); return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); } public async Task GetAsync(string id) { var template = await _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 appliedLocationIds = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.TemplateId == template.Id) .Select(x => x.LocationId) .ToListAsync(); 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(); return 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, ShowRuler = template.ShowRuler, ShowGrid = template.ShowGrid, VersionNo = template.VersionNo, State = template.State, Elements = MapElements(), AppliedLocationIds = appliedLocationIds, TemplateProductDefaults = productDefaults }; } [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 _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && x.TemplateCode == code); if (duplicated) { throw new UserFriendlyException("模板编码已存在"); } 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 = string.IsNullOrWhiteSpace(input.AppliedLocationType) ? "ALL" : input.AppliedLocationType.Trim(), ShowRuler = input.ShowRuler, ShowGrid = input.ShowGrid, VersionNo = 1, State = input.State }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); await RebuildTemplateElementsLocationsAndDefaultsAsync( entity.Id, input.Elements, entity.AppliedLocationType, input.AppliedLocationIds, new List()); return await GetAsync(code); } [UnitOfWork] public async Task UpdateAsync(string id, LabelTemplateUpdateInputVo input) { var template = await _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 _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && x.TemplateCode == code); if (duplicated) { throw new UserFriendlyException("模板编码已存在"); } } 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 = string.IsNullOrWhiteSpace(input.AppliedLocationType) ? template.AppliedLocationType : input.AppliedLocationType.Trim(); template.ShowRuler = input.ShowRuler; template.ShowGrid = input.ShowGrid; 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 RebuildTemplateElementsLocationsAndDefaultsAsync( template.Id, input.Elements, template.AppliedLocationType, input.AppliedLocationIds, input.TemplateProductDefaults); return await GetAsync(template.TemplateCode); } [UnitOfWork] public async Task DeleteAsync(string id) { var template = await _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(); await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.TemplateId == template.Id) .ExecuteCommandAsync(); } private async Task RebuildTemplateElementsLocationsAndDefaultsAsync( string templateDbId, List elements, string appliedLocationType, List appliedLocationIds, 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(); } // applied locations 重建 await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.TemplateId == templateDbId) .ExecuteCommandAsync(); if (string.Equals(appliedLocationType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) { var ids = appliedLocationIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList() ?? new(); if (ids.Count == 0) { throw new UserFriendlyException("指定门店模板必须至少选择一个门店"); } var locRows = ids.Select(locId => new FlLabelTemplateLocationDbEntity { Id = _guidGenerator.Create().ToString(), TemplateId = templateDbId, LocationId = locId }).ToList(); await _dbContext.SqlSugarClient.Insertable(locRows).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 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 }; } }