using System.Text.Json; using FoodLabeling.Application.Helpers; using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Contracts.Dtos.Label; 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; /// /// 标签管理(一个产品展示多个标签) /// public class LabelAppService : ApplicationService, ILabelAppService { private readonly ISqlSugarDbContext _dbContext; private readonly IGuidGenerator _guidGenerator; public LabelAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator) { _dbContext = dbContext; _guidGenerator = guidGenerator; } public async Task> GetListAsync(LabelGetListInputVo input) { RefAsync total = 0; var productId = input.ProductId?.Trim(); var groupId = input.GroupId?.Trim(); var locationId = input.LocationId?.Trim(); var keyword = input.Keyword?.Trim(); var labelCategoryId = input.LabelCategoryId?.Trim(); var labelTypeId = input.LabelTypeId?.Trim(); var templateCode = input.TemplateCode?.Trim(); // 目标:列表每行是“标签”,同一个标签下的 products 以 “,” 拼接展示 // 因此需要按 label 维度分页(避免 label-product join 导致重复行与分页错乱)。 var labelIdsQuery = _dbContext.SqlSugarClient.Queryable() .Where(l => !l.IsDeleted) .WhereIF(!string.IsNullOrWhiteSpace(labelCategoryId), l => l.LabelCategoryId == labelCategoryId) .WhereIF(!string.IsNullOrWhiteSpace(labelTypeId), l => l.LabelTypeId == labelTypeId) .WhereIF(input.State != null, l => l.State == input.State); if (!string.IsNullOrWhiteSpace(templateCode)) { labelIdsQuery = labelIdsQuery .InnerJoin((l, tpl) => l.TemplateId == tpl.Id) .Where((l, tpl) => !tpl.IsDeleted && tpl.TemplateCode == templateCode) .Select((l, tpl) => l); } var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( _dbContext.SqlSugarClient, groupId, locationId); if (scopedLocationIds is not null) { labelIdsQuery = scopedLocationIds.Count == 0 ? labelIdsQuery.Where(_ => false) : labelIdsQuery.Where(l => scopedLocationIds.Contains(l.LocationId)); } // 按产品筛选:存在 label-product 关联即可 if (!string.IsNullOrWhiteSpace(productId)) { labelIdsQuery = labelIdsQuery .InnerJoin((l, lp) => lp.LabelId == l.Id) .Where((l, lp) => lp.ProductId == productId) .Select((l, lp) => l); } // 关键字:匹配 labelName/categoryName/typeName/templateName/productName if (!string.IsNullOrWhiteSpace(keyword)) { labelIdsQuery = labelIdsQuery .LeftJoin((l, c) => l.LabelCategoryId == c.Id) .LeftJoin((l, c, t) => l.LabelTypeId == t.Id) .LeftJoin((l, c, t, tpl) => l.TemplateId == tpl.Id) .LeftJoin((l, c, t, tpl, lp) => lp.LabelId == l.Id) .LeftJoin((l, c, t, tpl, lp, p) => lp.ProductId == p.Id) .Where((l, c, t, tpl, lp, p) => l.LabelName.Contains(keyword!) || (c.CategoryName != null && c.CategoryName.Contains(keyword!)) || (t.TypeName != null && t.TypeName.Contains(keyword!)) || (tpl.TemplateName != null && tpl.TemplateName.Contains(keyword!)) || (p.ProductName != null && p.ProductName.Contains(keyword!))) .Select((l, c, t, tpl, lp, p) => l); } // 排序(优先外部 Sorting,否则按最后编辑倒序) if (!string.IsNullOrWhiteSpace(input.Sorting)) { labelIdsQuery = labelIdsQuery.OrderBy(input.Sorting); } else { labelIdsQuery = labelIdsQuery.OrderByDescending(l => l.LastModificationTime ?? l.CreationTime); } var pageLabelIds = await labelIdsQuery .Select(l => l.Id) .Distinct() .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); if (pageLabelIds.Count == 0) { return new PagedResultWithPageDto { PageIndex = 1, PageSize = input.MaxResultCount, TotalCount = total, TotalPages = 0, Items = new List() }; } // 查询标签基础信息(分类/类型/模板) var labelRows = 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) => pageLabelIds.Contains(l.Id)) .Where((l, c, t, tpl) => !l.IsDeleted && !c.IsDeleted && !t.IsDeleted && !tpl.IsDeleted) .Select((l, c, t, tpl) => new { l.Id, l.LabelCode, l.LabelName, l.LocationId, LabelCategoryName = c.CategoryName, LabelTypeName = t.TypeName, TemplateName = tpl.TemplateName, TemplateCode = tpl.TemplateCode, l.State, LastEdited = l.LastModificationTime ?? l.CreationTime }) .ToListAsync(); // 按分页顺序输出 var labelMap = labelRows.ToDictionary(x => x.Id, x => x); var orderedLabels = pageLabelIds.Where(id => labelMap.ContainsKey(id)).Select(id => labelMap[id]).ToList(); // 查询 products 并拼接 var productRows = await _dbContext.SqlSugarClient .Queryable() .InnerJoin((lp, l) => lp.LabelId == l.Id) .InnerJoin((lp, l, p) => lp.ProductId == p.Id) .LeftJoin((lp, l, p, pc) => p.CategoryId == pc.Id) .Where((lp, l, p, pc) => pageLabelIds.Contains(lp.LabelId)) .Where((lp, l, p, pc) => !l.IsDeleted && !p.IsDeleted) .Select((lp, l, p, pc) => new { lp.LabelId, p.ProductName, ProductCategoryName = pc.CategoryName }) .ToListAsync(); var productsMap = productRows .GroupBy(x => x.LabelId) .ToDictionary( g => g.Key, g => new { Products = string.Join(",", g.Select(x => x.ProductName ?? string.Empty).Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()), ProductCategoryName = string.Join(",", g.Select(x => x.ProductCategoryName ?? string.Empty).Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()) }); var locationIds = orderedLabels .Select(x => x.LocationId) .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x!.Trim()) .Distinct() .ToList(); var locationMap = new Dictionary(); if (locationIds.Count > 0) { var locGuids = locationIds.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); if (locGuids.Count > 0) { var locs = await _dbContext.SqlSugarClient.Queryable() .Where(x => !x.IsDeleted) .Where(x => locGuids.Contains(x.Id)) .ToListAsync(); locationMap = locs.ToDictionary(x => x.Id.ToString(), x => x); } } var items = orderedLabels.Select(x => { var locationName = string.Empty; if (!string.IsNullOrWhiteSpace(x.LocationId) && locationMap.TryGetValue(x.LocationId!, out var loc)) { locationName = loc.LocationName ?? loc.LocationCode; } var products = productsMap.TryGetValue(x.Id, out var prod) ? prod.Products : string.Empty; var productCategoryNameValue = productsMap.TryGetValue(x.Id, out var prod2) ? prod2.ProductCategoryName : string.Empty; return new LabelGetListOutputDto { Id = x.LabelCode ?? string.Empty, LabelName = x.LabelName ?? string.Empty, LocationName = string.IsNullOrWhiteSpace(locationName) ? "无" : locationName, LabelCategoryName = x.LabelCategoryName ?? string.Empty, ProductCategoryName = string.IsNullOrWhiteSpace(productCategoryNameValue) ? "无" : productCategoryNameValue, Products = products, TemplateName = x.TemplateName ?? string.Empty, TemplateCode = x.TemplateCode ?? string.Empty, LabelTypeName = x.LabelTypeName ?? string.Empty, State = x.State, LastEdited = x.LastEdited, HasError = false }; }).ToList(); var pageSize = input.MaxResultCount <= 0 ? items.Count : input.MaxResultCount; var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.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 }; } public async Task GetAsync(string id) { var labelCode = id?.Trim(); if (string.IsNullOrWhiteSpace(labelCode)) { throw new UserFriendlyException("标签Code不能为空"); } var label = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.LabelCode == labelCode); if (label is null) { throw new UserFriendlyException("标签不存在"); } var template = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => x.Id == label.TemplateId); var category = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => x.Id == label.LabelCategoryId); var type = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => x.Id == label.LabelTypeId); LocationAggregateRoot? location = null; if (!string.IsNullOrWhiteSpace(label.LocationId) && Guid.TryParse(label.LocationId, out var locId)) { location = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == locId); } var productIds = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.LabelId == label.Id) .Select(x => x.ProductId) .ToListAsync(); object? labelInfo = null; if (!string.IsNullOrWhiteSpace(label.LabelInfoJson)) { labelInfo = JsonSerializer.Deserialize(label.LabelInfoJson); } var locationId = label.LocationId ?? string.Empty; var locationIdList = string.IsNullOrWhiteSpace(locationId) ? new List() : new List { locationId.Trim() }; var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( _dbContext.SqlSugarClient, locationIdList); var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( _dbContext.SqlSugarClient, locationIdList); return new LabelGetOutputDto { Id = label.LabelCode ?? string.Empty, LabelName = label.LabelName, LocationId = locationId, LocationName = location?.LocationName ?? location?.LocationCode ?? "无", PartnerId = partnerIds.Count > 0 ? partnerIds[0] : null, PartnerIds = partnerIds, RegionIds = regionIds, GroupIds = regionIds, LabelCategoryId = label.LabelCategoryId ?? string.Empty, LabelCategoryName = category?.CategoryName ?? "无", LabelTypeId = label.LabelTypeId ?? string.Empty, LabelTypeName = type?.TypeName ?? "无", TemplateCode = template?.TemplateCode ?? string.Empty, TemplateName = template?.TemplateName ?? string.Empty, State = label.State, LabelInfoJson = labelInfo, ProductIds = productIds }; } [UnitOfWork] public async Task CreateAsync(LabelCreateInputVo input) { var labelCode = input.LabelCode?.Trim(); if (string.IsNullOrWhiteSpace(labelCode)) { labelCode = $"LBL_{_guidGenerator.Create():N}"; } var labelName = input.LabelName?.Trim(); if (string.IsNullOrWhiteSpace(labelName)) { throw new UserFriendlyException("标签名称不能为空"); } if (input.ProductIds is null || input.ProductIds.Count == 0) { throw new UserFriendlyException("标签至少需要绑定一个产品"); } if (string.IsNullOrWhiteSpace(input.TemplateCode)) { throw new UserFriendlyException("模板编码不能为空"); } if (string.IsNullOrWhiteSpace(input.LabelCategoryId)) { throw new UserFriendlyException("标签分类Id不能为空"); } if (string.IsNullOrWhiteSpace(input.LabelTypeId)) { throw new UserFriendlyException("标签类型Id不能为空"); } var resolvedLocationId = await ResolveLabelLocationIdForSaveAsync(input); var template = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.TemplateCode == input.TemplateCode.Trim()); if (template is null) { throw new UserFriendlyException("模板不存在"); } // 唯一性校验(LabelCode) var exists = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && x.LabelCode == labelCode); if (exists) { throw new UserFriendlyException("标签编码已存在"); } // 插入 fl_label var now = DateTime.Now; var labelId = _guidGenerator.Create().ToString(); var labelEntity = new FlLabelDbEntity { Id = labelId, IsDeleted = false, CreationTime = now, CreatorId = CurrentUser?.Id?.ToString(), LastModifierId = CurrentUser?.Id?.ToString(), LastModificationTime = now, ConcurrencyStamp = string.Empty, LabelCode = labelCode, LabelName = labelName, TemplateId = template.Id, LocationId = resolvedLocationId, LabelCategoryId = input.LabelCategoryId?.Trim(), LabelTypeId = input.LabelTypeId?.Trim(), State = input.State, LabelType = null, LabelInfoJson = input.LabelInfoJson == null ? null : JsonSerializer.Serialize(input.LabelInfoJson) }; await _dbContext.SqlSugarClient.Insertable(labelEntity).ExecuteCommandAsync(); // 插入 fl_label_product var productIds = input.ProductIds.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList(); var rows = productIds.Select(pid => new FlLabelProductDbEntity { Id = _guidGenerator.Create().ToString(), LabelId = labelId, ProductId = pid }).ToList(); await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); await LabelCategoryAppService.TouchLabelCategoryLastEditedAsync( _dbContext, labelEntity.LabelCategoryId, CurrentUser?.Id?.ToString()); return await GetAsync(labelCode); } [UnitOfWork] public async Task UpdateAsync(string id, LabelUpdateInputVo input) { var labelCode = id?.Trim(); if (string.IsNullOrWhiteSpace(labelCode)) { throw new UserFriendlyException("标签Code不能为空"); } var label = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.LabelCode == labelCode); if (label is null) { throw new UserFriendlyException("标签不存在"); } if (input.ProductIds is null || input.ProductIds.Count == 0) { throw new UserFriendlyException("标签至少需要绑定一个产品"); } if (string.IsNullOrWhiteSpace(input.TemplateCode)) { throw new UserFriendlyException("模板编码不能为空"); } if (string.IsNullOrWhiteSpace(input.LabelCategoryId)) { throw new UserFriendlyException("标签分类Id不能为空"); } if (string.IsNullOrWhiteSpace(input.LabelTypeId)) { throw new UserFriendlyException("标签类型Id不能为空"); } var resolvedLocationId = await ResolveLabelLocationIdForSaveAsync(input); var template = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.TemplateCode == input.TemplateCode.Trim()); if (template is null) { throw new UserFriendlyException("模板不存在"); } var oldCategoryId = label.LabelCategoryId; var now = DateTime.Now; label.LabelName = input.LabelName?.Trim() ?? label.LabelName; label.TemplateId = template.Id; label.LocationId = resolvedLocationId; label.LabelCategoryId = input.LabelCategoryId?.Trim(); label.LabelTypeId = input.LabelTypeId?.Trim(); label.State = input.State; label.LastModifierId = CurrentUser?.Id?.ToString(); label.LastModificationTime = now; label.LabelInfoJson = input.LabelInfoJson == null ? null : JsonSerializer.Serialize(input.LabelInfoJson); await _dbContext.SqlSugarClient.Updateable(label).ExecuteCommandAsync(); // 重建关联 await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.LabelId == label.Id) .ExecuteCommandAsync(); var productIds = input.ProductIds.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList(); var rows = productIds.Select(pid => new FlLabelProductDbEntity { Id = _guidGenerator.Create().ToString(), LabelId = label.Id, ProductId = pid }).ToList(); await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); var newCategoryId = label.LabelCategoryId; await LabelCategoryAppService.TouchLabelCategoryLastEditedAsync( _dbContext, newCategoryId, CurrentUser?.Id?.ToString()); if (!string.Equals(oldCategoryId, newCategoryId, StringComparison.Ordinal)) { await LabelCategoryAppService.TouchLabelCategoryLastEditedAsync( _dbContext, oldCategoryId, CurrentUser?.Id?.ToString()); } return await GetAsync(labelCode); } [UnitOfWork] public async Task DeleteAsync(string id) { var labelCode = id?.Trim(); if (string.IsNullOrWhiteSpace(labelCode)) { return; } var label = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.LabelCode == labelCode); if (label is null) { return; } label.IsDeleted = true; label.LastModifierId = CurrentUser?.Id?.ToString(); label.LastModificationTime = DateTime.Now; await _dbContext.SqlSugarClient.Updateable(label).ExecuteCommandAsync(); await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.LabelId == label.Id) .ExecuteCommandAsync(); await LabelCategoryAppService.TouchLabelCategoryLastEditedAsync( _dbContext, label.LabelCategoryId, CurrentUser?.Id?.ToString()); } /// /// 标签预览:不落库,只把 template elements 的 AUTO_DB/PRINT_INPUT 渲染进 config /// [UnitOfWork] public async Task PreviewAsync(LabelPreviewResolveInputVo input) { var labelCode = input?.LabelCode?.Trim(); if (string.IsNullOrWhiteSpace(labelCode)) { throw new UserFriendlyException("labelCode不能为空"); } var baseTime = input?.BaseTime ?? DateTime.Now; var label = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.LabelCode == labelCode); if (label is null) { throw new UserFriendlyException("标签不存在"); } // 选择预览产品 var productId = input?.ProductId?.Trim(); if (string.IsNullOrWhiteSpace(productId)) { productId = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.LabelId == label.Id) .Select(x => x.ProductId) .FirstAsync(); } if (string.IsNullOrWhiteSpace(productId)) { throw new UserFriendlyException("该标签未绑定产品,无法预览"); } var product = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == productId); if (product is null) { throw new UserFriendlyException("预览产品不存在"); } // 取模板头 & elements var template = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == label.TemplateId); if (template is null) { throw new UserFriendlyException("模板不存在"); } var elements = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.TemplateId == template.Id) // SqlSugar 不提供 ThenBy,这里用组合排序键保证 OrderNum + ZIndex 的稳定顺序 .OrderBy(x => x.OrderNum * 1000000 + x.ZIndex) .ToListAsync(); // 解析 labelInfo(可用于补充一些数值) Dictionary labelInfoDict = new(); if (!string.IsNullOrWhiteSpace(label.LabelInfoJson)) { try { labelInfoDict = JsonSerializer.Deserialize>(label.LabelInfoJson) ?? new Dictionary(); } catch { // ignore } } var printInputJson = input?.PrintInputJson ?? new Dictionary(); static int TryGetInt(object? v, int defaultValue) { if (v is null) return defaultValue; if (v is int i) return i; if (v is long l) return (int)l; if (v is decimal d) return (int)d; if (v is double db) return (int)db; if (v is JsonElement je) { if (je.ValueKind == JsonValueKind.Number && je.TryGetInt32(out var ii)) return ii; if (je.ValueKind == JsonValueKind.String && int.TryParse(je.GetString(), out var si)) return si; } if (v is string s && int.TryParse(s, out var st)) return st; return defaultValue; } static string? TryToString(object? v) { if (v is null) return null; if (v is string s) return s; if (v is JsonElement je) { if (je.ValueKind == JsonValueKind.String) return je.GetString(); if (je.ValueKind == JsonValueKind.Number || je.ValueKind == JsonValueKind.True || je.ValueKind == JsonValueKind.False) { return je.ToString(); } } return v.ToString(); } static void UpsertConfigValue(Dictionary cfg, string key, object? value) { if (cfg.ContainsKey(key)) { cfg[key] = value; return; } cfg.Add(key, value); } Dictionary ParseConfig(string? configJson) { if (string.IsNullOrWhiteSpace(configJson)) { return new Dictionary(); } try { return JsonSerializer.Deserialize>(configJson) ?? new Dictionary(); } catch { return new Dictionary(); } } foreach (var el in elements) { // FIXED:不处理(直接保留 config) // AUTO_DB:根据 type 做一个最小可用渲染(支持 TEXT_PRODUCT / BARCODE / QRCODE / DATE / TIME) // PRINT_INPUT:按 InputKey 覆盖相应字段(优先级最高) } var resolvedElements = elements.Select(el => { var cfg = ParseConfig(el.ConfigJson); // 1) 先做 AUTO_DB:按类型兜底填充 if (string.Equals(el.ValueSourceType, "AUTO_DB", StringComparison.OrdinalIgnoreCase)) { switch (el.ElementType) { case "TEXT_PRODUCT": UpsertConfigValue(cfg, "text", product.ProductName); break; case "TEXT_PRICE": { // price 可能在 labelInfo 或 printInput 里 object? priceObj = null; if (labelInfoDict.TryGetValue("price", out var p)) priceObj = p; if (priceObj is null && printInputJson.TryGetValue("price", out var pp)) priceObj = pp; var priceStr = TryToString(priceObj); if (!string.IsNullOrWhiteSpace(priceStr)) { UpsertConfigValue(cfg, "text", priceStr); } } break; case "BARCODE": if (!string.IsNullOrWhiteSpace(product.ProductCode)) { UpsertConfigValue(cfg, "data", product.ProductCode); } break; case "QRCODE": if (!string.IsNullOrWhiteSpace(product.ProductCode)) { UpsertConfigValue(cfg, "data", product.ProductCode); } break; case "DATE": { var offsetDays = cfg.TryGetValue("offsetDays", out var od) ? TryGetInt(od, 0) : 0; var dt = baseTime.Date.AddDays(offsetDays); UpsertConfigValue(cfg, "format", dt.ToString("yyyy-MM-dd")); cfg.Remove("inputType"); } break; case "TIME": { var dt = baseTime; UpsertConfigValue(cfg, "format", dt.ToString("HH:mm")); cfg.Remove("inputType"); } break; } } // 2) 再做 PRINT_INPUT:按 InputKey 覆盖(如果前端传了) if (string.Equals(el.ValueSourceType, "PRINT_INPUT", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(el.InputKey) && printInputJson.TryGetValue(el.InputKey!, out var inputValue)) { var s = TryToString(inputValue); if (!string.IsNullOrWhiteSpace(s)) { switch (el.ElementType) { case "TEXT_STATIC": case "TEXT_PRODUCT": case "TEXT_PRICE": UpsertConfigValue(cfg, "text", s); cfg.Remove("inputType"); break; case "BARCODE": case "QRCODE": UpsertConfigValue(cfg, "data", s); break; case "DATE": case "TIME": UpsertConfigValue(cfg, "format", s); cfg.Remove("inputType"); break; } } } // 3) FIXED:不做任何覆盖,保持模板/标签设计时写入的固定值(由前端/设计器落库) return new LabelTemplateElementDto { Id = el.ElementKey, ElementType = el.ElementType, TypeAdd = el.TypeAdd, ElementName = el.ElementName, PosX = el.PosX, PosY = el.PosY, Width = el.Width, Height = el.Height, Rotation = string.IsNullOrWhiteSpace(el.Rotation) ? "horizontal" : el.Rotation, BorderType = string.IsNullOrWhiteSpace(el.BorderType) ? "none" : el.BorderType, ZIndex = el.ZIndex, OrderNum = el.OrderNum, ValueSourceType = el.ValueSourceType, BindingExpr = el.BindingExpr, AutoQueryKey = el.AutoQueryKey, InputKey = el.InputKey, IsRequiredInput = el.IsRequiredInput, ConfigJson = cfg }; }).ToList(); return new LabelTemplatePreviewDto { Id = template.TemplateCode, Name = template.TemplateName, LabelType = template.LabelType ?? string.Empty, Unit = template.Unit, Width = template.Width, Height = template.Height, AppliedLocation = template.AppliedLocationType, ShowRuler = template.ShowRuler, ShowGrid = template.ShowGrid, Elements = resolvedElements }; } private Task ResolveLabelLocationIdForSaveAsync(LabelUpdateInputVo input) => ResolveLabelLocationIdForSaveAsync(new LabelCreateInputVo { PartnerId = input.PartnerId, PartnerIds = input.PartnerIds, RegionIds = input.RegionIds, GroupIds = input.GroupIds, LocationId = input.LocationId, LocationIds = input.LocationIds }); /// /// 标签仅绑定单门店:解析 Company/Region/门店入参为唯一 location.Id。 /// private async Task ResolveLabelLocationIdForSaveAsync(LabelCreateInputVo input) { var partnerIds = NormalizePartnerIds(input); var regionIds = NormalizeRegionIds(input); var explicitLocationId = input.LocationId?.Trim(); var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( _dbContext.SqlSugarClient, partnerIds, regionIds, explicitLocationIds); if (!string.IsNullOrWhiteSpace(explicitLocationId)) { if (merged.Count > 0 && !merged.Contains(explicitLocationId, StringComparer.Ordinal)) { throw new UserFriendlyException("所选门店不在指定公司/区域范围内"); } await LocationScopeBindingHelper.ValidateLocationIdsExistAsync( _dbContext.SqlSugarClient, new List { explicitLocationId }); return explicitLocationId; } if (merged.Count == 0) { throw new UserFriendlyException("须指定门店 locationId,或选择公司/区域以解析出门店"); } if (merged.Count > 1) { throw new UserFriendlyException("所选公司/区域对应多个门店,请显式指定 locationId"); } return merged[0]; } private static List NormalizePartnerIds(LabelCreateInputVo input) { var merged = new HashSet(StringComparer.Ordinal); if (!string.IsNullOrWhiteSpace(input.PartnerId)) { merged.Add(input.PartnerId.Trim()); } foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.PartnerIds)) { merged.Add(id); } return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); } private static List NormalizeRegionIds(LabelCreateInputVo input) { var merged = new HashSet(StringComparer.Ordinal); foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds)) { merged.Add(id); } foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds)) { merged.Add(id); } return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); } }