using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Contracts.Dtos.LabelAlertTimer; using FoodLabeling.Application.Contracts.IServices; using FoodLabeling.Application.Helpers; using FoodLabeling.Application.Services.DbModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Distributed; using SqlSugar; using Volo.Abp; using Volo.Abp.Application.Services; using Yi.Framework.SqlSugarCore.Abstractions; namespace FoodLabeling.Application.Services; /// /// 标签告警计时器(App):跟踪已打印标签的过期倒计时;与可否打印无关。 /// public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerAppService { private const string StatusRunning = "running"; private const string StatusExpired = "expired"; private readonly ISqlSugarDbContext _dbContext; private readonly IDistributedCache _distributedCache; public LabelAlertTimerAppService(ISqlSugarDbContext dbContext, IDistributedCache distributedCache) { _dbContext = dbContext; _distributedCache = distributedCache; } /// /// 分页查询当前门店告警计时器列表(含倒计时) /// /// /// 仅展示已打印标签的过期倒计时,用于判断能否打印。 /// 过期时刻与 Print Log「Expiration」列同源(ReportsPrintLogExpiryHelper)。 /// 同一打印批次(BatchId)无论打印多少张标签,仅一条计时器(取 CopyIndex 最小任务)。 /// /// 示例请求: /// ```json /// { /// "locationId": "11111111-1111-1111-1111-111111111111", /// "skipCount": 1, /// "maxResultCount": 20, /// "dateDay": "2026-08-07" /// } /// ``` /// /// 参数说明: /// - locationId: 当前门店 Id(必填,须已绑定) /// - skipCount: 页码(从 1 开始) /// - maxResultCount: 每页条数 /// - dateDay: 可选,按 PrintedAt 自然日筛选(yyyy-MM-dd) /// /// 分页查询入参 /// 分页计时器列表(含 remainingTime 倒计时秒数) /// 成功返回分页列表 /// 参数错误或未登录/无门店权限 /// 服务器错误 [Authorize] [HttpPost("label-alert-timer/list")] public virtual async Task> GetListAsync( LabelAlertTimerGetListInputVo input) { if (input is null) { throw new UserFriendlyException("入参不能为空"); } if (!CurrentUser.Id.HasValue) { throw new UserFriendlyException("用户未登录"); } var locationId = input.LocationId?.Trim(); if (string.IsNullOrWhiteSpace(locationId)) { throw new UserFriendlyException("门店Id不能为空"); } return await QueryListByLocationAsync(locationId, input); } /// /// App:当前账号当前门店告警列表(含倒计时) /// /// /// 供 App 警告页使用:按当前登录账号可访问的门店查询已打印标签的告警倒计时。 /// locationId 可省略,省略时使用管理员已选门店缓存(select-admin-scope-location); /// 仍无门店时返回友好错误。过期状态仅用于展示,与可否打印无关。 /// /// 示例请求: /// ```json /// { /// "locationId": "11111111-1111-1111-1111-111111111111", /// "skipCount": 1, /// "maxResultCount": 50 /// } /// ``` /// /// 参数说明: /// - locationId: 当前门店 Id(可选;空则取已选门店缓存) /// - skipCount: 页码(从 1 开始) /// - maxResultCount: 每页条数 /// - dateDay: 可选,按 PrintedAt 自然日筛选(yyyy-MM-dd) /// /// 出参倒计时字段: /// - remainingTime: 剩余秒数,App 可直接做倒计时 /// - totalTime: 总时长(秒) /// - status: running / expired /// - expiresAt: 过期时刻 /// /// 分页查询入参 /// 分页告警列表(含倒计时) /// 成功返回分页列表 /// 未登录、无门店或无权限 /// 服务器错误 [Authorize] [HttpPost("label-alert-timer/app-list")] public virtual async Task> GetAppListAsync( LabelAlertTimerGetListInputVo input) { if (input is null) { throw new UserFriendlyException("入参不能为空"); } if (!CurrentUser.Id.HasValue) { throw new UserFriendlyException("用户未登录"); } var locationId = input.LocationId?.Trim(); if (string.IsNullOrWhiteSpace(locationId)) { var cache = await UsAppAuthScopeHelper.GetAdminScopeCacheAsync( _distributedCache, CurrentUser.Id.Value); locationId = cache?.Location?.Id?.Trim(); } if (string.IsNullOrWhiteSpace(locationId)) { throw new UserFriendlyException("请先选择门店或传入 locationId"); } return await QueryListByLocationAsync(locationId, input); } /// /// 软删除告警计时器 /// /// /// 删除前校验当前用户可访问该计时器所属门店。 /// /// 计时器 Id /// 删除成功 /// 记录不存在或无门店权限 /// 服务器错误 [Authorize] [HttpDelete("label-alert-timer/{id}")] public virtual async Task DeleteAsync(string id) { var timerId = id?.Trim(); if (string.IsNullOrWhiteSpace(timerId)) { throw new UserFriendlyException("计时器Id不能为空"); } if (!CurrentUser.Id.HasValue) { throw new UserFriendlyException("用户未登录"); } var db = _dbContext.SqlSugarClient; var row = (await db.Queryable() .Where(x => x.Id == timerId && !x.IsDeleted) .Take(1) .ToListAsync()) .FirstOrDefault(); if (row is null) { throw new UserFriendlyException("计时器不存在或已删除"); } await UsAppPrintLogScopeHelper.EnsureUserCanAccessLocationAsync( CurrentUser, db, row.LocationId); var now = DateTime.Now; await db.Updateable() .SetColumns(x => x.IsDeleted == true) .SetColumns(x => x.DeletionTime == now) .Where(x => x.Id == timerId && !x.IsDeleted) .ExecuteCommandAsync(); } /// /// 查询已打印标签告警的过期/倒计时状态(不拦截打印) /// /// /// 至少提供 timerIdbatchIdprintTaskId 之一。 /// 本接口仅返回已打印批次的过期状态与剩余秒数,供展示倒计时; /// 绝不用于判断「能不能打印」——打印流程不得依赖本接口结果做拦截。 /// /// 示例请求: /// ```json /// { /// "batchId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" /// } /// ``` /// /// 参数说明: /// - timerId: 计时器 Id /// - batchId: 打印批次 Id /// - printTaskId: 打印任务 Id(同批次任意任务均可) /// /// 查询入参 /// 过期/倒计时状态(展示用) /// 成功(含未找到记录的情况) /// 未提供任何标识 /// 服务器错误 [Authorize] [HttpPost("label-alert-timer/check-expired")] public virtual async Task CheckExpiredAsync( LabelAlertTimerCheckExpiredInputVo input) { if (input is null) { throw new UserFriendlyException("入参不能为空"); } var timerId = input.TimerId?.Trim(); var batchId = input.BatchId?.Trim(); var printTaskId = input.PrintTaskId?.Trim(); if (string.IsNullOrWhiteSpace(timerId) && string.IsNullOrWhiteSpace(batchId) && string.IsNullOrWhiteSpace(printTaskId)) { throw new UserFriendlyException("请至少提供 timerId、batchId 或 printTaskId 之一"); } var db = _dbContext.SqlSugarClient; FlLabelAlertTimerDbEntity? row = null; if (!string.IsNullOrWhiteSpace(timerId)) { row = (await db.Queryable() .Where(x => x.Id == timerId && !x.IsDeleted) .Take(1) .ToListAsync()) .FirstOrDefault(); } else if (!string.IsNullOrWhiteSpace(batchId)) { row = (await db.Queryable() .Where(x => x.BatchId == batchId && !x.IsDeleted) .Take(1) .ToListAsync()) .FirstOrDefault(); } else if (!string.IsNullOrWhiteSpace(printTaskId)) { var task = (await db.Queryable() .Where(x => x.Id == printTaskId) .Take(1) .ToListAsync()) .FirstOrDefault(); if (task is not null && !string.IsNullOrWhiteSpace(task.BatchId)) { row = (await db.Queryable() .Where(x => x.BatchId == task.BatchId && !x.IsDeleted) .Take(1) .ToListAsync()) .FirstOrDefault(); } } if (row is null) { return new LabelAlertTimerCheckExpiredOutputDto { Found = false, IsExpired = false, RemainingSeconds = 0 }; } if (!CurrentUser.Id.HasValue) { throw new UserFriendlyException("用户未登录"); } await UsAppPrintLogScopeHelper.EnsureUserCanAccessLocationAsync( CurrentUser, db, row.LocationId); var now = DateTime.Now; var remaining = Math.Max(0, (int)(row.ExpiresAt - now).TotalSeconds); var isExpired = row.ExpiresAt <= now; return new LabelAlertTimerCheckExpiredOutputDto { Found = true, IsExpired = isExpired, ExpiresAt = row.ExpiresAt, RemainingSeconds = remaining, Status = isExpired ? StatusExpired : StatusRunning, Title = row.Title, Subtitle = row.Subtitle, TimerId = row.Id, BatchId = row.BatchId }; } private async Task> QueryListByLocationAsync( string locationId, LabelAlertTimerGetListInputVo input) { await UsAppPrintLogScopeHelper.EnsureUserCanAccessLocationAsync( CurrentUser, _dbContext.SqlSugarClient, locationId); var db = _dbContext.SqlSugarClient; RefAsync total = 0; var query = db.Queryable() .Where(x => !x.IsDeleted && x.LocationId == locationId); var (dayStart, dayEndExcl) = ResolveDateDayFilter(input.DateDay); if (dayStart.HasValue && dayEndExcl.HasValue) { var start = dayStart.Value; var endExcl = dayEndExcl.Value; query = query.Where(x => x.PrintedAt >= start && x.PrintedAt < endExcl); } var pageRows = await query .OrderBy(x => x.ExpiresAt, OrderByType.Desc) .OrderBy(x => x.PrintedAt, OrderByType.Desc) .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); var now = DateTime.Now; var items = pageRows.Select(x => MapListItem(x, now)).ToList(); var pageSize = input.MaxResultCount <= 0 ? items.Count : input.MaxResultCount; var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); var totalCount = (long)total; var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize); return new PagedResultWithPageDto { PageIndex = pageIndex, PageSize = pageSize, TotalCount = totalCount, TotalPages = totalPages, Items = items }; } private static LabelAlertTimerListItemDto MapListItem(FlLabelAlertTimerDbEntity row, DateTime now) { var remaining = Math.Max(0, (int)(row.ExpiresAt - now).TotalSeconds); var isExpired = row.ExpiresAt <= now; return new LabelAlertTimerListItemDto { Id = row.Id, BatchId = row.BatchId, PrintTaskId = row.PrintTaskId, LabelId = row.LabelId, LabelCode = row.LabelCode, Title = row.Title, Subtitle = row.Subtitle, TotalTime = row.DurationSeconds, RemainingTime = remaining, Status = isExpired ? StatusExpired : StatusRunning, ExpiresAt = row.ExpiresAt, PrintedAt = row.PrintedAt, LocationId = row.LocationId, ProductName = row.ProductName }; } private static (DateTime? DayStart, DateTime? DayEndExcl) ResolveDateDayFilter(string? dateDay) { if (string.IsNullOrWhiteSpace(dateDay)) { return (null, null); } if (DateTime.TryParse(dateDay.Trim(), out var parsedDay)) { var day = parsedDay.Date; return (day, day.AddDays(1)); } return (null, null); } }