LabelAlertTimerAppService.cs
14.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
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;
/// <summary>
/// 标签告警计时器(App):跟踪已打印标签的过期倒计时;与可否打印无关。
/// </summary>
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;
}
/// <summary>
/// 分页查询当前门店告警计时器列表(含倒计时)
/// </summary>
/// <remarks>
/// 仅展示已打印标签的过期倒计时,<b>不</b>用于判断能否打印。
/// 过期时刻与 Print Log「Expiration」列同源(<c>ReportsPrintLogExpiryHelper</c>)。
/// 同一打印批次(<c>BatchId</c>)无论打印多少张标签,仅一条计时器(取 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)
/// </remarks>
/// <param name="input">分页查询入参</param>
/// <returns>分页计时器列表(含 remainingTime 倒计时秒数)</returns>
/// <response code="200">成功返回分页列表</response>
/// <response code="400">参数错误或未登录/无门店权限</response>
/// <response code="500">服务器错误</response>
[Authorize]
[HttpPost("label-alert-timer/list")]
public virtual async Task<PagedResultWithPageDto<LabelAlertTimerListItemDto>> 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);
}
/// <summary>
/// App:当前账号当前门店告警列表(含倒计时)
/// </summary>
/// <remarks>
/// 供 App 警告页使用:按当前登录账号可访问的门店查询已打印标签的告警倒计时。
/// <c>locationId</c> 可省略,省略时使用管理员已选门店缓存(<c>select-admin-scope-location</c>);
/// 仍无门店时返回友好错误。过期状态仅用于展示,与可否打印无关。
///
/// 示例请求:
/// ```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: 过期时刻
/// </remarks>
/// <param name="input">分页查询入参</param>
/// <returns>分页告警列表(含倒计时)</returns>
/// <response code="200">成功返回分页列表</response>
/// <response code="400">未登录、无门店或无权限</response>
/// <response code="500">服务器错误</response>
[Authorize]
[HttpPost("label-alert-timer/app-list")]
public virtual async Task<PagedResultWithPageDto<LabelAlertTimerListItemDto>> 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);
}
/// <summary>
/// 软删除告警计时器
/// </summary>
/// <remarks>
/// 删除前校验当前用户可访问该计时器所属门店。
/// </remarks>
/// <param name="id">计时器 Id</param>
/// <response code="200">删除成功</response>
/// <response code="400">记录不存在或无门店权限</response>
/// <response code="500">服务器错误</response>
[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<FlLabelAlertTimerDbEntity>()
.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<FlLabelAlertTimerDbEntity>()
.SetColumns(x => x.IsDeleted == true)
.SetColumns(x => x.DeletionTime == now)
.Where(x => x.Id == timerId && !x.IsDeleted)
.ExecuteCommandAsync();
}
/// <summary>
/// 查询已打印标签告警的过期/倒计时状态(不拦截打印)
/// </summary>
/// <remarks>
/// 至少提供 <c>timerId</c>、<c>batchId</c>、<c>printTaskId</c> 之一。
/// 本接口仅返回已打印批次的过期状态与剩余秒数,供展示倒计时;
/// <b>绝不</b>用于判断「能不能打印」——打印流程不得依赖本接口结果做拦截。
///
/// 示例请求:
/// ```json
/// {
/// "batchId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
/// }
/// ```
///
/// 参数说明:
/// - timerId: 计时器 Id
/// - batchId: 打印批次 Id
/// - printTaskId: 打印任务 Id(同批次任意任务均可)
/// </remarks>
/// <param name="input">查询入参</param>
/// <returns>过期/倒计时状态(展示用)</returns>
/// <response code="200">成功(含未找到记录的情况)</response>
/// <response code="400">未提供任何标识</response>
/// <response code="500">服务器错误</response>
[Authorize]
[HttpPost("label-alert-timer/check-expired")]
public virtual async Task<LabelAlertTimerCheckExpiredOutputDto> 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<FlLabelAlertTimerDbEntity>()
.Where(x => x.Id == timerId && !x.IsDeleted)
.Take(1)
.ToListAsync())
.FirstOrDefault();
}
else if (!string.IsNullOrWhiteSpace(batchId))
{
row = (await db.Queryable<FlLabelAlertTimerDbEntity>()
.Where(x => x.BatchId == batchId && !x.IsDeleted)
.Take(1)
.ToListAsync())
.FirstOrDefault();
}
else if (!string.IsNullOrWhiteSpace(printTaskId))
{
var task = (await db.Queryable<FlLabelPrintTaskDbEntity>()
.Where(x => x.Id == printTaskId)
.Take(1)
.ToListAsync())
.FirstOrDefault();
if (task is not null && !string.IsNullOrWhiteSpace(task.BatchId))
{
row = (await db.Queryable<FlLabelAlertTimerDbEntity>()
.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<PagedResultWithPageDto<LabelAlertTimerListItemDto>> QueryListByLocationAsync(
string locationId,
LabelAlertTimerGetListInputVo input)
{
await UsAppPrintLogScopeHelper.EnsureUserCanAccessLocationAsync(
CurrentUser, _dbContext.SqlSugarClient, locationId);
var db = _dbContext.SqlSugarClient;
RefAsync<int> total = 0;
var query = db.Queryable<FlLabelAlertTimerDbEntity>()
.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<LabelAlertTimerListItemDto>
{
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);
}
}