LabelTemplateContentsHelper.cs 8.15 KB
using FoodLabeling.Application.Contracts.Dtos.LabelTemplate;
using FoodLabeling.Application.Services.DbModels;
using SqlSugar;

namespace FoodLabeling.Application.Helpers;

/// <summary>
/// 标签模板 <c>Contents</c>:汇总各控件展示名(编辑器 Element name / typeAdd 推导),逗号拼接。
/// </summary>
public static class LabelTemplateContentsHelper
{
    /// <summary>
    /// 保存时解析 Contents:优先入参 <paramref name="inputContents"/>,否则从 <paramref name="elements"/> 推导。
    /// </summary>
    public static string ResolveForSave(string? inputContents, IReadOnlyList<LabelTemplateElementDto>? elements)
    {
        var trimmed = inputContents?.Trim();
        if (!string.IsNullOrWhiteSpace(trimmed))
        {
            return trimmed;
        }

        return BuildFromElements(elements);
    }

    /// <summary>
    /// 从 elements 推导 Contents(去重、按 OrderNum 排序)。
    /// </summary>
    public static string BuildFromElements(IReadOnlyList<LabelTemplateElementDto>? elements)
    {
        if (elements is null || elements.Count == 0)
        {
            return string.Empty;
        }

        var names = CollectOrderedDistinctLabels(
            elements
                .OrderBy(e => e.OrderNum)
                .ThenBy(e => e.Id, StringComparer.Ordinal)
                .Select(e => ResolveElementContentsLabel(e.ElementName, e.TypeAdd, e.ElementType)));

        return names.Count > 0 ? string.Join(", ", names) : string.Empty;
    }

    /// <summary>
    /// 批量从 <c>fl_label_template_element</c> 推导 Contents。Key = 模板主键 Id。
    /// </summary>
    public static async Task<Dictionary<string, string>> ResolveContentsMapFromElementsAsync(
        ISqlSugarClient db,
        IReadOnlyList<string> templateIds)
    {
        var result = new Dictionary<string, string>(StringComparer.Ordinal);
        if (templateIds.Count == 0)
        {
            return result;
        }

        var rows = await db.Queryable<FlLabelTemplateElementDbEntity>()
            .Where(x => templateIds.Contains(x.TemplateId))
            .Select(x => new ElementContentsRow
            {
                TemplateId = x.TemplateId,
                ElementName = x.ElementName,
                TypeAdd = x.TypeAdd,
                ElementType = x.ElementType,
                OrderNum = x.OrderNum,
                ElementKey = x.ElementKey
            })
            .ToListAsync();

        foreach (var group in rows.GroupBy(x => x.TemplateId))
        {
            var names = CollectOrderedDistinctLabels(
                group
                    .OrderBy(x => x.OrderNum)
                    .ThenBy(x => x.ElementKey, StringComparer.Ordinal)
                    .Select(x => ResolveElementContentsLabel(x.ElementName, x.TypeAdd, x.ElementType)));

            if (names.Count > 0)
            {
                result[group.Key] = string.Join(", ", names);
            }
        }

        return result;
    }

    /// <summary>
    /// 单控件 Contents 展示名:优先用户编辑的 Element name;旧 slug 则按 typeAdd 推导「控件名 (For Template)」。
    /// </summary>
    public static string ResolveElementContentsLabel(string? elementName, string? typeAdd, string? elementType)
    {
        var derived = DeriveLabelFromTypeAdd(typeAdd, elementType);
        var name = elementName?.Trim();
        if (string.IsNullOrWhiteSpace(name))
        {
            return derived;
        }

        if (IsLegacySlugElementName(name, typeAdd, elementType))
        {
            return string.IsNullOrWhiteSpace(derived) ? name : derived;
        }

        return name;
    }

    /// <summary>
    /// 从 typeAdd(如 <c>template_Text</c>、<c>label_Nutrition Facts</c>)推导展示名。
    /// </summary>
    public static string DeriveLabelFromTypeAdd(string? typeAdd, string? elementType)
    {
        var ta = typeAdd?.Trim();
        if (!string.IsNullOrWhiteSpace(ta))
        {
            var idx = ta.IndexOf('_');
            if (idx >= 0 && idx < ta.Length - 1)
            {
                var prefix = ta[..idx].Trim().ToLowerInvariant();
                var field = ta[(idx + 1)..].Trim();
                if (!string.IsNullOrWhiteSpace(field))
                {
                    var category = MapPrefixToCategoryTitle(prefix);
                    return category != null ? $"{field} ({category})" : field;
                }
            }
        }

        return MapElementTypeToDisplayLabel(elementType);
    }

    private static bool IsLegacySlugElementName(string name, string? typeAdd, string? elementType)
    {
        if (name.Contains(' ') || name.Contains('('))
        {
            return false;
        }

        if (!LegacySlugPattern.IsMatch(name))
        {
            return false;
        }

        var derived = DeriveLabelFromTypeAdd(typeAdd, elementType);
        if (string.IsNullOrWhiteSpace(derived))
        {
            return true;
        }

        var field = ExtractTypeAddField(typeAdd);
        if (string.IsNullOrWhiteSpace(field))
        {
            return true;
        }

        var slugField = SlugPaletteLabel(field);
        var slugName = name.Trim().ToLowerInvariant();
        if (slugName == slugField)
        {
            return true;
        }

        if (slugName.StartsWith(slugField, StringComparison.Ordinal)
            && slugName.Length > slugField.Length
            && int.TryParse(slugName[slugField.Length..], out _))
        {
            return true;
        }

        return false;
    }

    private static string? ExtractTypeAddField(string? typeAdd)
    {
        var ta = typeAdd?.Trim();
        if (string.IsNullOrWhiteSpace(ta))
        {
            return null;
        }

        var idx = ta.IndexOf('_');
        return idx >= 0 && idx < ta.Length - 1 ? ta[(idx + 1)..].Trim() : null;
    }

    private static string SlugPaletteLabel(string label) =>
        new string(label.Trim().ToLowerInvariant().Where(char.IsLetterOrDigit).ToArray());

    private static string? MapPrefixToCategoryTitle(string prefix) =>
        prefix switch
        {
            "template" => "For Template",
            "label" => "For Label",
            "auto" => "Entered Automatically",
            "print" => "Entered When Printing",
            _ => null
        };

    private static string MapElementTypeToDisplayLabel(string? elementType)
    {
        var t = (elementType ?? string.Empty).Trim().ToUpperInvariant();
        return t switch
        {
            "TEXT_PRODUCT" => "Label Name (For Label)",
            "TEXT_PRICE" => "Price (For Label)",
            "NUTRITION" or "NUTRITIONFACTS" => "Nutrition Facts (For Label)",
            "DURATION" => "Duration (For Label)",
            "DATE" => "Current Date (Entered Automatically)",
            "TIME" => "Current Time (Entered Automatically)",
            "BARCODE" => "Barcode (For Label)",
            "QRCODE" => "QR Code (For Label)",
            "BLANK" => "Blank Space (For Template)",
            "IMAGE" => "Image (For Label)",
            "TEXT_STATIC" => "Text (For Template)",
            _ => string.IsNullOrWhiteSpace(t) ? string.Empty : t.Replace('_', ' ')
        };
    }

    private static List<string> CollectOrderedDistinctLabels(IEnumerable<string> labels)
    {
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var names = new List<string>();
        foreach (var label in labels)
        {
            var text = label?.Trim();
            if (string.IsNullOrWhiteSpace(text) || !seen.Add(text))
            {
                continue;
            }

            names.Add(text);
        }

        return names;
    }

    private static readonly System.Text.RegularExpressions.Regex LegacySlugPattern =
        new("^[a-z0-9]+$", System.Text.RegularExpressions.RegexOptions.Compiled);

    private sealed class ElementContentsRow
    {
        public string TemplateId { get; init; } = string.Empty;

        public string ElementName { get; init; } = string.Empty;

        public string? TypeAdd { get; init; }

        public string ElementType { get; init; } = string.Empty;

        public int OrderNum { get; init; }

        public string ElementKey { get; init; } = string.Empty;
    }
}