ReportsPrintLogDailyLabelIdHelper.cs 7.95 KB
using System.Globalization;
using FoodLabeling.Application.Services.DbModels;
using SqlSugar;

namespace FoodLabeling.Application.Helpers;

/// <summary>
/// Print Log「Label ID」:按门店 + 自然日对打印任务排序,格式 <c>yyyyMMdd-n</c>(非 fl_label.LabelCode)。
/// </summary>
public static class ReportsPrintLogDailyLabelIdHelper
{
    /// <summary>
    /// 为列表/导出行计算门店当日打印序号。
    /// </summary>
    public static async Task<Dictionary<string, string>> ResolveDailyLabelIdsAsync(
        ISqlSugarClient db,
        IReadOnlyList<PrintTaskScopeKey> tasks)
    {
        var result = new Dictionary<string, string>(StringComparer.Ordinal);
        if (tasks.Count == 0)
        {
            return result;
        }

        foreach (var task in tasks)
        {
            if (string.IsNullOrWhiteSpace(task.TaskId))
            {
                continue;
            }

            result.TryAdd(task.TaskId.Trim(), "无");
        }

        var buckets = tasks
            .Where(t =>
                !string.IsNullOrWhiteSpace(t.TaskId) &&
                !string.IsNullOrWhiteSpace(t.LocationId) &&
                t.PrintedAt != default)
            .GroupBy(t => (LocationId: t.LocationId!.Trim(), Day: t.PrintedAt.Date))
            .ToList();

        foreach (var bucket in buckets)
        {
            var locId = bucket.Key.LocationId;
            var dayStart = bucket.Key.Day;
            var dayEnd = dayStart.AddDays(1);

            var dayTasks = await db.Queryable<FlLabelPrintTaskDbEntity>()
                .Where(t => t.LocationId == locId)
                .Where(t =>
                    SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= dayStart &&
                    SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < dayEnd)
                .OrderBy(t => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Asc)
                .OrderBy(t => t.Id, OrderByType.Asc)
                .Select(t => new { t.Id, PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime) })
                .ToListAsync();

            for (var i = 0; i < dayTasks.Count; i++)
            {
                result[dayTasks[i].Id] = Format(dayStart, i + 1);
            }
        }

        return result;
    }

    /// <summary>
    /// 预览/即将打印:取门店在指定自然日的下一个打印序号(不落库)。
    /// </summary>
    public static async Task<string> ResolveNextDailyLabelIdAsync(
        ISqlSugarClient db,
        string locationId,
        DateTime referenceTime)
    {
        var locId = locationId?.Trim();
        if (string.IsNullOrWhiteSpace(locId))
        {
            return "无";
        }

        var dayStart = referenceTime.Date;
        var dayEnd = dayStart.AddDays(1);

        var count = await db.Queryable<FlLabelPrintTaskDbEntity>()
            .Where(t => t.LocationId == locId)
            .Where(t =>
                SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= dayStart &&
                SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < dayEnd)
            .CountAsync();

        return Format(dayStart, count + 1);
    }

    public static string Format(DateTime printDay, int sequence) =>
        $"{printDay:yyyyMMdd}-{sequence}";

    /// <summary>
    /// 为门店当日后续打印任务预留连续序号(不含已存在任务之外的并发锁,与列表展示规则一致)。
    /// </summary>
    public static async Task<List<string>> ResolveNextDailyLabelIdsAsync(
        ISqlSugarClient db,
        string locationId,
        DateTime day,
        int count)
    {
        var result = new List<string>();
        if (count <= 0 || string.IsNullOrWhiteSpace(locationId))
        {
            return result;
        }

        var locId = locationId.Trim();
        var dayStart = day.Date;
        var dayEnd = dayStart.AddDays(1);

        var existingCount = await db.Queryable<FlLabelPrintTaskDbEntity>()
            .Where(t => t.LocationId == locId)
            .Where(t =>
                SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= dayStart &&
                SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < dayEnd)
            .CountAsync();

        for (var i = 1; i <= count; i++)
        {
            result.Add(Format(dayStart, existingCount + i));
        }

        return result;
    }

    /// <summary>
    /// App Print Log 单日筛选:解析 <paramref name="printDate"/> / <paramref name="printDateDay"/> 为服务器本地自然日。
    /// 两者均未传时返回 null(不按日过滤,兼容 App 未传 printDate 的历史行为)。
    /// </summary>
    public static DateTime? ResolvePrintLogFilterCalendarDay(DateTime? printDate, string? printDateDay)
    {
        var text = printDateDay?.Trim();
        if (!string.IsNullOrWhiteSpace(text))
        {
            if (DateTime.TryParseExact(
                    text,
                    "yyyy-MM-dd",
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.None,
                    out var parsedExact))
            {
                return parsedExact.Date;
            }

            if (DateTime.TryParse(
                    text,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.AllowWhiteSpaces,
                    out var parsed))
            {
                return parsed.Date;
            }

            throw new Volo.Abp.UserFriendlyException("printDate 格式不正确,请使用 yyyy-MM-dd");
        }

        if (!printDate.HasValue)
        {
            return null;
        }

        var value = printDate.Value;
        return value.Kind == DateTimeKind.Utc ? value.ToLocalTime().Date : value.Date;
    }

    /// <summary>
    /// 按自然日过滤打印任务(MySQL <c>DATE(IFNULL(PrintedAt, CreationTime))</c>,避免 DateTime 区间比较时区偏差)。
    /// </summary>
    public static ISugarQueryable<FlLabelPrintTaskDbEntity> ApplyPrintTaskCalendarDayFilter(
        ISugarQueryable<FlLabelPrintTaskDbEntity> query,
        DateTime filterDay)
    {
        var dayText = filterDay.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
        return query.Where("DATE(IFNULL(PrintedAt, CreationTime)) = @filterDay", new { filterDay = dayText });
    }

    /// <summary>
    /// 多表 Join 查询按自然日过滤(首表别名 <c>t</c>)。
    /// </summary>
    public static ISugarQueryable<FlLabelPrintTaskDbEntity, T2> ApplyPrintTaskCalendarDayFilter<T2>(
        ISugarQueryable<FlLabelPrintTaskDbEntity, T2> query,
        DateTime filterDay)
    {
        var dayText = filterDay.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
        return query.Where("DATE(IFNULL(t.PrintedAt, t.CreationTime)) = @filterDay", new { filterDay = dayText });
    }

    /// <summary>
    /// 五表 Join(首表别名 <c>t</c>)。
    /// </summary>
    public static ISugarQueryable<FlLabelPrintTaskDbEntity, T2, T3, T4, T5> ApplyPrintTaskCalendarDayFilter<T2, T3, T4, T5>(
        ISugarQueryable<FlLabelPrintTaskDbEntity, T2, T3, T4, T5> query,
        DateTime filterDay)
    {
        var dayText = filterDay.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
        return query.Where("DATE(IFNULL(t.PrintedAt, t.CreationTime)) = @filterDay", new { filterDay = dayText });
    }

    /// <summary>
    /// App Print Log 单日筛选:<c>printDate</c> 的自然日区间;未传则默认服务器当天。
    /// </summary>
    public static (DateTime dayStart, DateTime dayEndExcl) ResolvePrintLogFilterDayRange(DateTime? printDate)
    {
        var day = ResolvePrintLogFilterCalendarDay(printDate, null) ?? DateTime.Today;
        return (day, day.AddDays(1));
    }

    public readonly struct PrintTaskScopeKey
    {
        public PrintTaskScopeKey(string taskId, string? locationId, DateTime printedAt)
        {
            TaskId = taskId;
            LocationId = locationId;
            PrintedAt = printedAt;
        }

        public string TaskId { get; }

        public string? LocationId { get; }

        public DateTime PrintedAt { get; }
    }
}