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();
}
///
/// 查询已打印标签告警的过期/倒计时状态(不拦截打印)
///
///
/// 至少提供 timerId、batchId、printTaskId 之一。
/// 本接口仅返回已打印批次的过期状态与剩余秒数,供展示倒计时;
/// 绝不用于判断「能不能打印」——打印流程不得依赖本接口结果做拦截。
///
/// 示例请求:
/// ```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);
}
}