Commit 0a93339e275608d10273cdc6f74176df31e1124e

Authored by 李曜臣
1 parent aa01fd36

2026-08-07代码提交

泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelAlertTimer/LabelAlertTimerCheckExpiredOutputDto.cs
@@ -8,7 +8,7 @@ public class LabelAlertTimerCheckExpiredOutputDto @@ -8,7 +8,7 @@ public class LabelAlertTimerCheckExpiredOutputDto
8 /// <summary>是否找到计时器记录</summary> 8 /// <summary>是否找到计时器记录</summary>
9 public bool Found { get; set; } 9 public bool Found { get; set; }
10 10
11 - /// <summary>是否已过期;无记录时为 false(允许继续打印)</summary> 11 + /// <summary>是否已过期;无记录时为 false(仅展示用,不用于拦截打印)</summary>
12 public bool IsExpired { get; set; } 12 public bool IsExpired { get; set; }
13 13
14 public DateTime? ExpiresAt { get; set; } 14 public DateTime? ExpiresAt { get; set; }
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelAlertTimer/LabelAlertTimerGetListInputVo.cs
@@ -8,7 +8,8 @@ namespace FoodLabeling.Application.Contracts.Dtos.LabelAlertTimer; @@ -8,7 +8,8 @@ namespace FoodLabeling.Application.Contracts.Dtos.LabelAlertTimer;
8 public class LabelAlertTimerGetListInputVo : PagedAndSortedResultRequestDto 8 public class LabelAlertTimerGetListInputVo : PagedAndSortedResultRequestDto
9 { 9 {
10 /// <summary> 10 /// <summary>
11 - /// 当前门店 Id(location.Id,Guid 字符串,必填) 11 + /// 当前门店 Id(location.Id,Guid 字符串)。
  12 + /// <c>list</c> 必填;<c>app-list</c> 可空(空则取已选门店缓存)。
12 /// </summary> 13 /// </summary>
13 public string? LocationId { get; set; } 14 public string? LocationId { get; set; }
14 15
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelAlertTimerAppService.cs
@@ -11,6 +11,11 @@ public interface ILabelAlertTimerAppService : IApplicationService @@ -11,6 +11,11 @@ public interface ILabelAlertTimerAppService : IApplicationService
11 { 11 {
12 Task<PagedResultWithPageDto<LabelAlertTimerListItemDto>> GetListAsync(LabelAlertTimerGetListInputVo input); 12 Task<PagedResultWithPageDto<LabelAlertTimerListItemDto>> GetListAsync(LabelAlertTimerGetListInputVo input);
13 13
  14 + /// <summary>
  15 + /// App:当前账号当前门店告警列表(含倒计时;locationId 可省略,走已选门店缓存)
  16 + /// </summary>
  17 + Task<PagedResultWithPageDto<LabelAlertTimerListItemDto>> GetAppListAsync(LabelAlertTimerGetListInputVo input);
  18 +
14 Task DeleteAsync(string id); 19 Task DeleteAsync(string id);
15 20
16 Task<LabelAlertTimerCheckExpiredOutputDto> CheckExpiredAsync(LabelAlertTimerCheckExpiredInputVo input); 21 Task<LabelAlertTimerCheckExpiredOutputDto> CheckExpiredAsync(LabelAlertTimerCheckExpiredInputVo input);
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAlertTimerAppService.cs
@@ -5,6 +5,7 @@ using FoodLabeling.Application.Helpers; @@ -5,6 +5,7 @@ using FoodLabeling.Application.Helpers;
5 using FoodLabeling.Application.Services.DbModels; 5 using FoodLabeling.Application.Services.DbModels;
6 using Microsoft.AspNetCore.Authorization; 6 using Microsoft.AspNetCore.Authorization;
7 using Microsoft.AspNetCore.Mvc; 7 using Microsoft.AspNetCore.Mvc;
  8 +using Microsoft.Extensions.Caching.Distributed;
8 using SqlSugar; 9 using SqlSugar;
9 using Volo.Abp; 10 using Volo.Abp;
10 using Volo.Abp.Application.Services; 11 using Volo.Abp.Application.Services;
@@ -13,7 +14,7 @@ using Yi.Framework.SqlSugarCore.Abstractions; @@ -13,7 +14,7 @@ using Yi.Framework.SqlSugarCore.Abstractions;
13 namespace FoodLabeling.Application.Services; 14 namespace FoodLabeling.Application.Services;
14 15
15 /// <summary> 16 /// <summary>
16 -/// 标签告警计时器(App):按打印批次展示过期倒计时,过期时刻与 Print Log Expiration 同源 17 +/// 标签告警计时器(App):跟踪已打印标签的过期倒计时;与可否打印无关
17 /// </summary> 18 /// </summary>
18 public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerAppService 19 public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerAppService
19 { 20 {
@@ -21,16 +22,19 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp @@ -21,16 +22,19 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp
21 private const string StatusExpired = "expired"; 22 private const string StatusExpired = "expired";
22 23
23 private readonly ISqlSugarDbContext _dbContext; 24 private readonly ISqlSugarDbContext _dbContext;
  25 + private readonly IDistributedCache _distributedCache;
24 26
25 - public LabelAlertTimerAppService(ISqlSugarDbContext dbContext) 27 + public LabelAlertTimerAppService(ISqlSugarDbContext dbContext, IDistributedCache distributedCache)
26 { 28 {
27 _dbContext = dbContext; 29 _dbContext = dbContext;
  30 + _distributedCache = distributedCache;
28 } 31 }
29 32
30 /// <summary> 33 /// <summary>
31 - /// 分页查询当前门店告警计时器列表 34 + /// 分页查询当前门店告警计时器列表(含倒计时)
32 /// </summary> 35 /// </summary>
33 /// <remarks> 36 /// <remarks>
  37 + /// 仅展示已打印标签的过期倒计时,<b>不</b>用于判断能否打印。
34 /// 过期时刻与 Print Log「Expiration」列同源(<c>ReportsPrintLogExpiryHelper</c>)。 38 /// 过期时刻与 Print Log「Expiration」列同源(<c>ReportsPrintLogExpiryHelper</c>)。
35 /// 同一打印批次(<c>BatchId</c>)无论打印多少张标签,仅一条计时器(取 CopyIndex 最小任务)。 39 /// 同一打印批次(<c>BatchId</c>)无论打印多少张标签,仅一条计时器(取 CopyIndex 最小任务)。
36 /// 40 ///
@@ -51,7 +55,7 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp @@ -51,7 +55,7 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp
51 /// - dateDay: 可选,按 PrintedAt 自然日筛选(yyyy-MM-dd) 55 /// - dateDay: 可选,按 PrintedAt 自然日筛选(yyyy-MM-dd)
52 /// </remarks> 56 /// </remarks>
53 /// <param name="input">分页查询入参</param> 57 /// <param name="input">分页查询入参</param>
54 - /// <returns>分页计时器列表</returns> 58 + /// <returns>分页计时器列表(含 remainingTime 倒计时秒数)</returns>
55 /// <response code="200">成功返回分页列表</response> 59 /// <response code="200">成功返回分页列表</response>
56 /// <response code="400">参数错误或未登录/无门店权限</response> 60 /// <response code="400">参数错误或未登录/无门店权限</response>
57 /// <response code="500">服务器错误</response> 61 /// <response code="500">服务器错误</response>
@@ -76,43 +80,73 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp @@ -76,43 +80,73 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp
76 throw new UserFriendlyException("门店Id不能为空"); 80 throw new UserFriendlyException("门店Id不能为空");
77 } 81 }
78 82
79 - await UsAppPrintLogScopeHelper.EnsureUserCanAccessLocationAsync(  
80 - CurrentUser, _dbContext.SqlSugarClient, locationId);  
81 -  
82 - var db = _dbContext.SqlSugarClient;  
83 - RefAsync<int> total = 0;  
84 - var query = db.Queryable<FlLabelAlertTimerDbEntity>()  
85 - .Where(x => !x.IsDeleted && x.LocationId == locationId); 83 + return await QueryListByLocationAsync(locationId, input);
  84 + }
86 85
87 - var (dayStart, dayEndExcl) = ResolveDateDayFilter(input.DateDay);  
88 - if (dayStart.HasValue && dayEndExcl.HasValue) 86 + /// <summary>
  87 + /// App:当前账号当前门店告警列表(含倒计时)
  88 + /// </summary>
  89 + /// <remarks>
  90 + /// 供 App 警告页使用:按当前登录账号可访问的门店查询已打印标签的告警倒计时。
  91 + /// <c>locationId</c> 可省略,省略时使用管理员已选门店缓存(<c>select-admin-scope-location</c>);
  92 + /// 仍无门店时返回友好错误。过期状态仅用于展示,与可否打印无关。
  93 + ///
  94 + /// 示例请求:
  95 + /// ```json
  96 + /// {
  97 + /// "locationId": "11111111-1111-1111-1111-111111111111",
  98 + /// "skipCount": 1,
  99 + /// "maxResultCount": 50
  100 + /// }
  101 + /// ```
  102 + ///
  103 + /// 参数说明:
  104 + /// - locationId: 当前门店 Id(可选;空则取已选门店缓存)
  105 + /// - skipCount: 页码(从 1 开始)
  106 + /// - maxResultCount: 每页条数
  107 + /// - dateDay: 可选,按 PrintedAt 自然日筛选(yyyy-MM-dd)
  108 + ///
  109 + /// 出参倒计时字段:
  110 + /// - remainingTime: 剩余秒数,App 可直接做倒计时
  111 + /// - totalTime: 总时长(秒)
  112 + /// - status: running / expired
  113 + /// - expiresAt: 过期时刻
  114 + /// </remarks>
  115 + /// <param name="input">分页查询入参</param>
  116 + /// <returns>分页告警列表(含倒计时)</returns>
  117 + /// <response code="200">成功返回分页列表</response>
  118 + /// <response code="400">未登录、无门店或无权限</response>
  119 + /// <response code="500">服务器错误</response>
  120 + [Authorize]
  121 + [HttpPost("label-alert-timer/app-list")]
  122 + public virtual async Task<PagedResultWithPageDto<LabelAlertTimerListItemDto>> GetAppListAsync(
  123 + LabelAlertTimerGetListInputVo input)
  124 + {
  125 + if (input is null)
89 { 126 {
90 - var start = dayStart.Value;  
91 - var endExcl = dayEndExcl.Value;  
92 - query = query.Where(x => x.PrintedAt >= start && x.PrintedAt < endExcl); 127 + throw new UserFriendlyException("入参不能为空");
93 } 128 }
94 129
95 - var pageRows = await query  
96 - .OrderBy(x => x.ExpiresAt, OrderByType.Desc)  
97 - .OrderBy(x => x.PrintedAt, OrderByType.Desc)  
98 - .ToPageListAsync(input.SkipCount, input.MaxResultCount, total);  
99 -  
100 - var now = DateTime.Now;  
101 - var items = pageRows.Select(x => MapListItem(x, now)).ToList(); 130 + if (!CurrentUser.Id.HasValue)
  131 + {
  132 + throw new UserFriendlyException("用户未登录");
  133 + }
102 134
103 - var pageSize = input.MaxResultCount <= 0 ? items.Count : input.MaxResultCount;  
104 - var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount);  
105 - var totalCount = (long)total;  
106 - var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize); 135 + var locationId = input.LocationId?.Trim();
  136 + if (string.IsNullOrWhiteSpace(locationId))
  137 + {
  138 + var cache = await UsAppAuthScopeHelper.GetAdminScopeCacheAsync(
  139 + _distributedCache,
  140 + CurrentUser.Id.Value);
  141 + locationId = cache?.Location?.Id?.Trim();
  142 + }
107 143
108 - return new PagedResultWithPageDto<LabelAlertTimerListItemDto> 144 + if (string.IsNullOrWhiteSpace(locationId))
109 { 145 {
110 - PageIndex = pageIndex,  
111 - PageSize = pageSize,  
112 - TotalCount = totalCount,  
113 - TotalPages = totalPages,  
114 - Items = items  
115 - }; 146 + throw new UserFriendlyException("请先选择门店或传入 locationId");
  147 + }
  148 +
  149 + return await QueryListByLocationAsync(locationId, input);
116 } 150 }
117 151
118 /// <summary> 152 /// <summary>
@@ -163,11 +197,12 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp @@ -163,11 +197,12 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp
163 } 197 }
164 198
165 /// <summary> 199 /// <summary>
166 - /// 检查告警计时器是否已过期 200 + /// 查询已打印标签告警的过期/倒计时状态(不拦截打印)
167 /// </summary> 201 /// </summary>
168 /// <remarks> 202 /// <remarks>
169 /// 至少提供 <c>timerId</c>、<c>batchId</c>、<c>printTaskId</c> 之一。 203 /// 至少提供 <c>timerId</c>、<c>batchId</c>、<c>printTaskId</c> 之一。
170 - /// 无匹配记录时 <c>found=false</c>、<c>isExpired=false</c>(允许继续打印)。 204 + /// 本接口仅返回已打印批次的过期状态与剩余秒数,供展示倒计时;
  205 + /// <b>绝不</b>用于判断「能不能打印」——打印流程不得依赖本接口结果做拦截。
171 /// 206 ///
172 /// 示例请求: 207 /// 示例请求:
173 /// ```json 208 /// ```json
@@ -182,7 +217,7 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp @@ -182,7 +217,7 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp
182 /// - printTaskId: 打印任务 Id(同批次任意任务均可) 217 /// - printTaskId: 打印任务 Id(同批次任意任务均可)
183 /// </remarks> 218 /// </remarks>
184 /// <param name="input">查询入参</param> 219 /// <param name="input">查询入参</param>
185 - /// <returns>过期检查结果</returns> 220 + /// <returns>过期/倒计时状态(展示用)</returns>
186 /// <response code="200">成功(含未找到记录的情况)</response> 221 /// <response code="200">成功(含未找到记录的情况)</response>
187 /// <response code="400">未提供任何标识</response> 222 /// <response code="400">未提供任何标识</response>
188 /// <response code="500">服务器错误</response> 223 /// <response code="500">服务器错误</response>
@@ -278,6 +313,49 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp @@ -278,6 +313,49 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp
278 }; 313 };
279 } 314 }
280 315
  316 + private async Task<PagedResultWithPageDto<LabelAlertTimerListItemDto>> QueryListByLocationAsync(
  317 + string locationId,
  318 + LabelAlertTimerGetListInputVo input)
  319 + {
  320 + await UsAppPrintLogScopeHelper.EnsureUserCanAccessLocationAsync(
  321 + CurrentUser, _dbContext.SqlSugarClient, locationId);
  322 +
  323 + var db = _dbContext.SqlSugarClient;
  324 + RefAsync<int> total = 0;
  325 + var query = db.Queryable<FlLabelAlertTimerDbEntity>()
  326 + .Where(x => !x.IsDeleted && x.LocationId == locationId);
  327 +
  328 + var (dayStart, dayEndExcl) = ResolveDateDayFilter(input.DateDay);
  329 + if (dayStart.HasValue && dayEndExcl.HasValue)
  330 + {
  331 + var start = dayStart.Value;
  332 + var endExcl = dayEndExcl.Value;
  333 + query = query.Where(x => x.PrintedAt >= start && x.PrintedAt < endExcl);
  334 + }
  335 +
  336 + var pageRows = await query
  337 + .OrderBy(x => x.ExpiresAt, OrderByType.Desc)
  338 + .OrderBy(x => x.PrintedAt, OrderByType.Desc)
  339 + .ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
  340 +
  341 + var now = DateTime.Now;
  342 + var items = pageRows.Select(x => MapListItem(x, now)).ToList();
  343 +
  344 + var pageSize = input.MaxResultCount <= 0 ? items.Count : input.MaxResultCount;
  345 + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount);
  346 + var totalCount = (long)total;
  347 + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize);
  348 +
  349 + return new PagedResultWithPageDto<LabelAlertTimerListItemDto>
  350 + {
  351 + PageIndex = pageIndex,
  352 + PageSize = pageSize,
  353 + TotalCount = totalCount,
  354 + TotalPages = totalPages,
  355 + Items = items
  356 + };
  357 + }
  358 +
281 private static LabelAlertTimerListItemDto MapListItem(FlLabelAlertTimerDbEntity row, DateTime now) 359 private static LabelAlertTimerListItemDto MapListItem(FlLabelAlertTimerDbEntity row, DateTime now)
282 { 360 {
283 var remaining = Math.Max(0, (int)(row.ExpiresAt - now).TotalSeconds); 361 var remaining = Math.Max(0, (int)(row.ExpiresAt - now).TotalSeconds);
项目相关文档/2026-08-07告警接口文档.md
1 # 告警计时器接口文档(泰额版) 1 # 告警计时器接口文档(泰额版)
2 2
3 > 模块:标签告警计时器(Label Alert Timer) 3 > 模块:标签告警计时器(Label Alert Timer)
4 -> 范围:泰额版后端(租户业务库,非 `antis-foodlabeling-host`) 4 +> 服务:`LabelAlertTimerAppService`
  5 +> 范围:泰额版后端,数据在**租户业务库**(非 `antis-foodlabeling-host`)
5 > 认证:`Authorization: Bearer {token}`;业务请求建议带 `__tenant: {tenantId}` 6 > 认证:`Authorization: Bearer {token}`;业务请求建议带 `__tenant: {tenantId}`
6 > JSON:camelCase 7 > JSON:camelCase
7 > Base URL 示例:`http://127.0.0.1:19002`(以实际部署为准) 8 > Base URL 示例:`http://127.0.0.1:19002`(以实际部署为准)
@@ -13,34 +14,60 @@ @@ -13,34 +14,60 @@
13 14
14 | 概念 | 说明 | 15 | 概念 | 说明 |
15 |------|------| 16 |------|------|
16 -| 过期时刻 | 与 Print Log「Expiration」列**同源**,由 `ReportsPrintLogExpiryHelper.TryResolveExpiryDateTime` 解析 |  
17 -| 批次维度 | 一次打印(同一 `BatchId`)无论打印多少张,**仅一条**计时器(取 `CopyIndex` 最小的 `fl_label_print_task`) | 17 +| 用途 | 跟踪**已经打印**的标签过期时间,供 App / 前端做**倒计时与警告列表** |
  18 +| 与打印关系 | **过期与「能不能打印」无关**;打印接口**不会**因计时器已过期而拒绝打印 |
  19 +| 过期时刻 | 与 Print Log「Expiration」列**同源**(`ReportsPrintLogExpiryHelper.TryResolveExpiryDateTime`) |
  20 +| 批次维度 | 同一 `BatchId` 无论打印多少张,**仅一条**计时器(取 `CopyIndex` 最小的 `fl_label_print_task`) |
18 | 无过期不上列表 | 模板无法解析过期时刻时,不写入计时器、不出现在列表 | 21 | 无过期不上列表 | 模板无法解析过期时刻时,不写入计时器、不出现在列表 |
  22 +| 列表范围 | **门店级**:该 `locationId` 下未软删的全部计时器(不限当前用户自己打印) |
19 | 状态 | `running`(未过期)/ `expired`(已过期) | 23 | 状态 | `running`(未过期)/ `expired`(已过期) |
20 -| 软删 | 用户可删除计时器;删除后不再出现在列表 | 24 +| 软删 | 用户可删除计时器;删除后不再出现在列表,不影响历史打印任务 |
21 25
22 ### 写入时机 26 ### 写入时机
23 27
24 -- `UsAppLabelingAppService.PrintAsync` / `ReprintAsync` 成功创建批次后自动写入  
25 -- 幂等重试(相同 `clientRequestId`)返回前也会补写一次(Helper 内部按 `BatchId` 去重) 28 +- `UsAppLabelingAppService.PrintAsync` / `ReprintAsync` 成功创建批次后自动写入(`LabelAlertTimerWriteHelper.TryCreateFromPrintBatchAsync`)
  29 +- 幂等重试(相同 `clientRequestId`)返回前也会补写一次(按 `BatchId` 唯一索引去重;含软删记录也不再插入)
26 30
27 ### 建表 31 ### 建表
28 32
29 -- 脚本:`module/food-labeling-us/scripts/fl_label_alert_timer.sql`  
30 -- 表名:`fl_label_alert_timer`  
31 -- 新租户开通时嵌入资源自动执行;**已有租户需用具备 CREATE 权限的账号**在业务库手动执行脚本(业务账号 `netteam` 通常无建表权限,可用运维高权限账号执行) 33 +| 项 | 说明 |
  34 +|----|------|
  35 +| 脚本 | `泰额版/.../module/food-labeling-us/scripts/fl_label_alert_timer.sql` |
  36 +| 表名 | `fl_label_alert_timer` |
  37 +| 新租户 | 开通时嵌入资源自动执行 |
  38 +| 已有租户 | 需用具备 CREATE 权限的账号在业务库手动执行(业务账号 `netteam` 通常无建表权限) |
  39 +
  40 +主要字段:`BatchId`(唯一)、`PrintTaskId`、`LabelId`、`LocationId`、`PrintedAt`、`ExpiresAt`、`DurationSeconds`、`Title`、`Subtitle`、`IsDeleted`。
32 41
33 --- 42 ---
34 43
35 ## 2. 接口一览 44 ## 2. 接口一览
36 45
37 -| 功能 | 方法 | 路由 |  
38 -|------|------|------|  
39 -| 分页列表 | POST | `/api/app/label-alert-timer/list` |  
40 -| 软删除 | DELETE | `/api/app/label-alert-timer/{id}` |  
41 -| 检查是否过期 | POST | `/api/app/label-alert-timer/check-expired` | 46 +| 功能 | 方法 | 路由 | 说明 |
  47 +|------|------|------|------|
  48 +| 分页列表 | POST | `/api/app/label-alert-timer/list` | `locationId` **必填** |
  49 +| App 警告列表 | POST | `/api/app/label-alert-timer/app-list` | **推荐 App 使用**;`locationId` 可空(走已选门店缓存) |
  50 +| 软删除 | DELETE | `/api/app/label-alert-timer/{id}` | 软删计时器 |
  51 +| 查询过期/倒计时 | POST | `/api/app/label-alert-timer/check-expired` | 单条状态查询;**仅展示,不拦打印** |
  52 +
  53 +> 全部需登录。列表 / 删除 / 查询均校验当前账号可访问对应门店。
  54 +
  55 +### 获取 Token(App)
  56 +
  57 +```http
  58 +POST /api/app/th-app-auth/login
  59 +Content-Type: application/json
  60 +
  61 +{ "email": "mai@123.com", "password": "123456", "tenantId": "可选-租户Id" }
  62 +```
  63 +
  64 +后续请求头:
42 65
43 -> 所有接口需登录;列表与删除需已绑定 `locationId` 对应门店。 66 +```http
  67 +Authorization: Bearer {token}
  68 +__tenant: {tenantId}
  69 +Content-Type: application/json
  70 +```
44 71
45 --- 72 ---
46 73
@@ -59,14 +86,14 @@ @@ -59,14 +86,14 @@
59 } 86 }
60 ``` 87 ```
61 88
62 -| 字段 | 必填 | 说明 |  
63 -|------|------|------|  
64 -| locationId | 是 | 当前门店 Id |  
65 -| skipCount | 是 | 页码(从 1 开始) |  
66 -| maxResultCount | 是 | 每页条数 |  
67 -| dateDay | 否 | `yyyy-MM-dd`,按 PrintedAt 自然日筛选 | 89 +| 字段 | 类型 | 必填 | 说明 |
  90 +|------|------|------|------|
  91 +| locationId | string | 是 | 当前门店 Id;空则报错「门店Id不能为空」 |
  92 +| skipCount | number | 是 | **页码**(从 1 开始,与项目分页约定一致) |
  93 +| maxResultCount | number | 是 | 每页条数 |
  94 +| dateDay | string | 否 | `yyyy-MM-dd`,按 `PrintedAt` 自然日筛选 |
68 95
69 -### 响应示例 96 +### 响应
70 97
71 ```json 98 ```json
72 { 99 {
@@ -95,30 +122,95 @@ @@ -95,30 +122,95 @@
95 } 122 }
96 ``` 123 ```
97 124
98 -| 出参字段 | 说明 |  
99 -|----------|------|  
100 -| totalTime | 总时长(秒),等于库中 DurationSeconds |  
101 -| remainingTime | 剩余秒数,`max(0, ExpiresAt - now)` |  
102 -| status | `running` 或 `expired` | 125 +### 列表项字段(`items[]`)
  126 +
  127 +| 字段 | 类型 | 说明 |
  128 +|------|------|------|
  129 +| id | string | 计时器主键 |
  130 +| batchId | string | 打印批次 Id |
  131 +| printTaskId | string | 代表任务 Id(CopyIndex 最小) |
  132 +| labelId | string | 标签 Id |
  133 +| labelCode | string \| null | 标签编码 |
  134 +| title | string | 标题(含时长文案) |
  135 +| subtitle | string | 副标题(含完成时刻文案) |
  136 +| totalTime | number | 总时长(秒),同库字段 `DurationSeconds` |
  137 +| remainingTime | number | **剩余秒数**,`max(0, ExpiresAt - now)`;**App 倒计时用此字段** |
  138 +| status | string | `running` / `expired` |
  139 +| expiresAt | string | 过期时刻 |
  140 +| printedAt | string | 打印时刻 |
  141 +| locationId | string | 门店 Id |
  142 +| productName | string \| null | 产品名称 |
  143 +
  144 +排序:先 `ExpiresAt` 降序,再 `PrintedAt` 降序。
  145 +
  146 +---
  147 +
  148 +## 4. App 警告列表(推荐)
  149 +
  150 +**POST** `/api/app/label-alert-timer/app-list`
  151 +
  152 +当前登录账号可访问的门店下警告列表;出参结构与第 3 节 `list` **完全相同**(含 `remainingTime` 倒计时)。
  153 +
  154 +### 请求
  155 +
  156 +```json
  157 +{
  158 + "locationId": "3a21220f-db37-3e32-7390-d55f64cd62a8",
  159 + "skipCount": 1,
  160 + "maxResultCount": 50
  161 +}
  162 +```
  163 +
  164 +| 字段 | 类型 | 必填 | 说明 |
  165 +|------|------|------|------|
  166 +| locationId | string | 否 | 当前门店;**为空**时取 `POST /api/app/us-app-auth/select-admin-scope-location` 写入的已选门店缓存 |
  167 +| skipCount | number | 是 | 页码(从 1 开始) |
  168 +| maxResultCount | number | 是 | 每页条数 |
  169 +| dateDay | string | 否 | `yyyy-MM-dd`,按 `PrintedAt` 筛选 |
  170 +
  171 +### 门店解析规则
  172 +
  173 +1. 入参 `locationId` 有值 → 用入参
  174 +2. 入参为空 → 读管理员已选门店缓存
  175 +3. 仍无门店 → 400:「请先选择门店或传入 locationId」
  176 +4. 有门店但当前账号不可访问 → 权限校验失败(与 `list` 相同)
  177 +
  178 +### 倒计时对接
  179 +
  180 +| 字段 | App 用法 |
  181 +|------|----------|
  182 +| remainingTime | 初始剩余秒数;进入页面后可本地每秒 `-1`,或定时重新拉列表校正 |
  183 +| totalTime | 进度条分母:`progress = (totalTime - remainingTime) / totalTime` |
  184 +| status | `expired` 时 remainingTime 为 0,可高亮/置顶 |
  185 +| expiresAt | 展示绝对过期时间;与 Print Log Expiration 对齐 |
103 186
104 --- 187 ---
105 188
106 -## 4. 软删除 189 +## 5. 软删除
107 190
108 **DELETE** `/api/app/label-alert-timer/{id}` 191 **DELETE** `/api/app/label-alert-timer/{id}`
109 192
110 -- 校验当前用户可访问该计时器所属门店  
111 -- 设置 `IsDeleted=1`、`DeletionTime=now` 193 +| 项 | 说明 |
  194 +|----|------|
  195 +| 路径参数 id | 计时器主键 |
  196 +| 权限 | 校验当前用户可访问该计时器所属 `LocationId` |
  197 +| 行为 | `IsDeleted=1`,`DeletionTime=now` |
  198 +| 不存在/已删 | 报错「计时器不存在或已删除」 |
  199 +
  200 +无响应体(成功即可)。
112 201
113 --- 202 ---
114 203
115 -## 5. 检查是否过期 204 +## 6. 查询过期/倒计时状态
116 205
117 **POST** `/api/app/label-alert-timer/check-expired` 206 **POST** `/api/app/label-alert-timer/check-expired`
118 207
  208 +仅查询**已打印**批次的过期状态与剩余秒数,供单条展示。
  209 +**不得**用于拦截打印;打印流程不要依赖本接口结果做「禁止打印」。
  210 +
119 ### 请求 211 ### 请求
120 212
121 -至少提供 `timerId`、`batchId`、`printTaskId` 之一 213 +至少提供 `timerId`、`batchId`、`printTaskId` 之一(优先级:`timerId` > `batchId` > `printTaskId`)
122 214
123 ```json 215 ```json
124 { 216 {
@@ -126,6 +218,14 @@ @@ -126,6 +218,14 @@
126 } 218 }
127 ``` 219 ```
128 220
  221 +| 字段 | 说明 |
  222 +|------|------|
  223 +| timerId | 计时器 Id |
  224 +| batchId | 打印批次 Id |
  225 +| printTaskId | 打印任务 Id(同批次任意任务均可,会反查 BatchId) |
  226 +
  227 +未提供任一标识 → 「请至少提供 timerId、batchId 或 printTaskId 之一」。
  228 +
129 ### 响应(找到记录) 229 ### 响应(找到记录)
130 230
131 ```json 231 ```json
@@ -152,37 +252,72 @@ @@ -152,37 +252,72 @@
152 } 252 }
153 ``` 253 ```
154 254
155 -> **对接建议**:`found=false` 时视为无告警限制,**允许继续打印**;`found=true && isExpired=true` 时可提示用户标签已过期。 255 +| 字段 | 说明 |
  256 +|------|------|
  257 +| found | 是否找到未删除的计时器 |
  258 +| isExpired | 是否已过期;无记录时为 `false` |
  259 +| remainingSeconds | 剩余秒数(已过期或无记录为 0);与列表的 `remainingTime` 含义相同 |
  260 +| status | `running` / `expired`;无记录时可能为空 |
  261 +| expiresAt / title / subtitle / timerId / batchId | 找到记录时有值 |
156 262
157 --- 263 ---
158 264
159 -## 6. curl 示例 265 +## 7. 常见错误文案
  266 +
  267 +| 场景 | 文案 |
  268 +|------|------|
  269 +| 入参为空 | 入参不能为空 |
  270 +| 未登录 | 用户未登录 |
  271 +| list 未传门店 | 门店Id不能为空 |
  272 +| app-list 无门店且无缓存 | 请先选择门店或传入 locationId |
  273 +| 删除 Id 为空 | 计时器Id不能为空 |
  274 +| 记录不存在/已删 | 计时器不存在或已删除 |
  275 +| check-expired 无标识 | 请至少提供 timerId、batchId 或 printTaskId 之一 |
  276 +| 无门店权限 | 由门店权限校验抛出(与打印日志门店校验一致) |
  277 +
  278 +---
  279 +
  280 +## 8. curl 示例
160 281
161 ```bash 282 ```bash
162 -# 获取 Token 后替换 TOKEN、TENANT、LOCATION_ID 283 +# 1) 登录拿 Token(按实际环境替换)
  284 +curl -s -X POST "http://127.0.0.1:19002/api/app/th-app-auth/login" \
  285 + -H "Content-Type: application/json" \
  286 + -d '{"email":"mai@123.com","password":"123456","tenantId":"TENANT_ID"}'
  287 +
  288 +# 2) App 警告列表(含倒计时)
  289 +curl -s -X POST "http://127.0.0.1:19002/api/app/label-alert-timer/app-list" \
  290 + -H "Authorization: Bearer TOKEN" \
  291 + -H "__tenant: TENANT_ID" \
  292 + -H "Content-Type: application/json" \
  293 + -d '{"locationId":"LOCATION_ID","skipCount":1,"maxResultCount":50}'
163 294
  295 +# 3) 通用分页列表
164 curl -s -X POST "http://127.0.0.1:19002/api/app/label-alert-timer/list" \ 296 curl -s -X POST "http://127.0.0.1:19002/api/app/label-alert-timer/list" \
165 -H "Authorization: Bearer TOKEN" \ 297 -H "Authorization: Bearer TOKEN" \
166 - -H "__tenant: TENANT" \ 298 + -H "__tenant: TENANT_ID" \
167 -H "Content-Type: application/json" \ 299 -H "Content-Type: application/json" \
168 -d '{"locationId":"LOCATION_ID","skipCount":1,"maxResultCount":20}' 300 -d '{"locationId":"LOCATION_ID","skipCount":1,"maxResultCount":20}'
169 301
  302 +# 4) 单条过期/倒计时查询(仅展示)
170 curl -s -X POST "http://127.0.0.1:19002/api/app/label-alert-timer/check-expired" \ 303 curl -s -X POST "http://127.0.0.1:19002/api/app/label-alert-timer/check-expired" \
171 -H "Authorization: Bearer TOKEN" \ 304 -H "Authorization: Bearer TOKEN" \
172 - -H "__tenant: TENANT" \ 305 + -H "__tenant: TENANT_ID" \
173 -H "Content-Type: application/json" \ 306 -H "Content-Type: application/json" \
174 -d '{"batchId":"YOUR_BATCH_ID"}' 307 -d '{"batchId":"YOUR_BATCH_ID"}'
175 308
  309 +# 5) 软删除
176 curl -s -X DELETE "http://127.0.0.1:19002/api/app/label-alert-timer/TIMER_ID" \ 310 curl -s -X DELETE "http://127.0.0.1:19002/api/app/label-alert-timer/TIMER_ID" \
177 -H "Authorization: Bearer TOKEN" \ 311 -H "Authorization: Bearer TOKEN" \
178 - -H "__tenant: TENANT" 312 + -H "__tenant: TENANT_ID"
179 ``` 313 ```
180 314
181 --- 315 ---
182 316
183 -## 7. 前端对接建议 317 +## 9. 前端 / App 对接建议
184 318
185 -1. **列表页**:进入门店后拉取列表,用 `remainingTime` 做倒计时;`status=expired` 可高亮或置顶。  
186 -2. **打印前校验**(可选):重打历史批次时调 `check-expired`,`isExpired=true` 时二次确认。  
187 -3. **与 Print Log 一致**:展示过期时间时请与 Print Log Expiration 列对齐,避免两套算法。  
188 -4. **删除**:用户左滑/长按删除时调 DELETE;删除仅隐藏计时器,不影响历史打印任务。 319 +1. **警告页**:优先调 `app-list`;用 `remainingTime`(秒)做倒计时;`status=expired` 可高亮。
  320 +2. **不要做打印前过期拦截**:过期与可否打印无关;计时器在打印成功后才写入。
  321 +3. **与 Print Log 一致**:展示过期时间时与 Print Log Expiration 列对齐,避免两套算法。
  322 +4. **删除**:左滑/长按调 DELETE;仅隐藏计时器,不影响历史打印任务。
  323 +5. **校正**:长时间停留页面时,可定时重拉 `app-list`,避免本地倒计时漂移。