Commit 0a93339e275608d10273cdc6f74176df31e1124e
1 parent
aa01fd36
2026-08-07代码提交
Showing
5 changed files
with
301 additions
and
82 deletions
泰额版/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 | 8 | /// <summary>是否找到计时器记录</summary> |
| 9 | 9 | public bool Found { get; set; } |
| 10 | 10 | |
| 11 | - /// <summary>是否已过期;无记录时为 false(允许继续打印)</summary> | |
| 11 | + /// <summary>是否已过期;无记录时为 false(仅展示用,不用于拦截打印)</summary> | |
| 12 | 12 | public bool IsExpired { get; set; } |
| 13 | 13 | |
| 14 | 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 | 8 | public class LabelAlertTimerGetListInputVo : PagedAndSortedResultRequestDto |
| 9 | 9 | { |
| 10 | 10 | /// <summary> |
| 11 | - /// 当前门店 Id(location.Id,Guid 字符串,必填) | |
| 11 | + /// 当前门店 Id(location.Id,Guid 字符串)。 | |
| 12 | + /// <c>list</c> 必填;<c>app-list</c> 可空(空则取已选门店缓存)。 | |
| 12 | 13 | /// </summary> |
| 13 | 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 | 11 | { |
| 12 | 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 | 19 | Task DeleteAsync(string id); |
| 15 | 20 | |
| 16 | 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 | 5 | using FoodLabeling.Application.Services.DbModels; |
| 6 | 6 | using Microsoft.AspNetCore.Authorization; |
| 7 | 7 | using Microsoft.AspNetCore.Mvc; |
| 8 | +using Microsoft.Extensions.Caching.Distributed; | |
| 8 | 9 | using SqlSugar; |
| 9 | 10 | using Volo.Abp; |
| 10 | 11 | using Volo.Abp.Application.Services; |
| ... | ... | @@ -13,7 +14,7 @@ using Yi.Framework.SqlSugarCore.Abstractions; |
| 13 | 14 | namespace FoodLabeling.Application.Services; |
| 14 | 15 | |
| 15 | 16 | /// <summary> |
| 16 | -/// 标签告警计时器(App):按打印批次展示过期倒计时,过期时刻与 Print Log Expiration 同源。 | |
| 17 | +/// 标签告警计时器(App):跟踪已打印标签的过期倒计时;与可否打印无关。 | |
| 17 | 18 | /// </summary> |
| 18 | 19 | public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerAppService |
| 19 | 20 | { |
| ... | ... | @@ -21,16 +22,19 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp |
| 21 | 22 | private const string StatusExpired = "expired"; |
| 22 | 23 | |
| 23 | 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 | 29 | _dbContext = dbContext; |
| 30 | + _distributedCache = distributedCache; | |
| 28 | 31 | } |
| 29 | 32 | |
| 30 | 33 | /// <summary> |
| 31 | - /// 分页查询当前门店告警计时器列表 | |
| 34 | + /// 分页查询当前门店告警计时器列表(含倒计时) | |
| 32 | 35 | /// </summary> |
| 33 | 36 | /// <remarks> |
| 37 | + /// 仅展示已打印标签的过期倒计时,<b>不</b>用于判断能否打印。 | |
| 34 | 38 | /// 过期时刻与 Print Log「Expiration」列同源(<c>ReportsPrintLogExpiryHelper</c>)。 |
| 35 | 39 | /// 同一打印批次(<c>BatchId</c>)无论打印多少张标签,仅一条计时器(取 CopyIndex 最小任务)。 |
| 36 | 40 | /// |
| ... | ... | @@ -51,7 +55,7 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp |
| 51 | 55 | /// - dateDay: 可选,按 PrintedAt 自然日筛选(yyyy-MM-dd) |
| 52 | 56 | /// </remarks> |
| 53 | 57 | /// <param name="input">分页查询入参</param> |
| 54 | - /// <returns>分页计时器列表</returns> | |
| 58 | + /// <returns>分页计时器列表(含 remainingTime 倒计时秒数)</returns> | |
| 55 | 59 | /// <response code="200">成功返回分页列表</response> |
| 56 | 60 | /// <response code="400">参数错误或未登录/无门店权限</response> |
| 57 | 61 | /// <response code="500">服务器错误</response> |
| ... | ... | @@ -76,43 +80,73 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp |
| 76 | 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 | 152 | /// <summary> |
| ... | ... | @@ -163,11 +197,12 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp |
| 163 | 197 | } |
| 164 | 198 | |
| 165 | 199 | /// <summary> |
| 166 | - /// 检查告警计时器是否已过期 | |
| 200 | + /// 查询已打印标签告警的过期/倒计时状态(不拦截打印) | |
| 167 | 201 | /// </summary> |
| 168 | 202 | /// <remarks> |
| 169 | 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 | 208 | /// ```json |
| ... | ... | @@ -182,7 +217,7 @@ public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerApp |
| 182 | 217 | /// - printTaskId: 打印任务 Id(同批次任意任务均可) |
| 183 | 218 | /// </remarks> |
| 184 | 219 | /// <param name="input">查询入参</param> |
| 185 | - /// <returns>过期检查结果</returns> | |
| 220 | + /// <returns>过期/倒计时状态(展示用)</returns> | |
| 186 | 221 | /// <response code="200">成功(含未找到记录的情况)</response> |
| 187 | 222 | /// <response code="400">未提供任何标识</response> |
| 188 | 223 | /// <response code="500">服务器错误</response> |
| ... | ... | @@ -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 | 359 | private static LabelAlertTimerListItemDto MapListItem(FlLabelAlertTimerDbEntity row, DateTime now) |
| 282 | 360 | { |
| 283 | 361 | var remaining = Math.Max(0, (int)(row.ExpiresAt - now).TotalSeconds); | ... | ... |
项目相关文档/2026-08-07告警接口文档.md
| 1 | 1 | # 告警计时器接口文档(泰额版) |
| 2 | 2 | |
| 3 | 3 | > 模块:标签告警计时器(Label Alert Timer) |
| 4 | -> 范围:泰额版后端(租户业务库,非 `antis-foodlabeling-host`) | |
| 4 | +> 服务:`LabelAlertTimerAppService` | |
| 5 | +> 范围:泰额版后端,数据在**租户业务库**(非 `antis-foodlabeling-host`) | |
| 5 | 6 | > 认证:`Authorization: Bearer {token}`;业务请求建议带 `__tenant: {tenantId}` |
| 6 | 7 | > JSON:camelCase |
| 7 | 8 | > Base URL 示例:`http://127.0.0.1:19002`(以实际部署为准) |
| ... | ... | @@ -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 | 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 | 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 | 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 | 98 | ```json |
| 72 | 99 | { |
| ... | ... | @@ -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 | 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 | 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 | 215 | ```json |
| 124 | 216 | { |
| ... | ... | @@ -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 | 231 | ```json |
| ... | ... | @@ -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 | 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 | 296 | curl -s -X POST "http://127.0.0.1:19002/api/app/label-alert-timer/list" \ |
| 165 | 297 | -H "Authorization: Bearer TOKEN" \ |
| 166 | - -H "__tenant: TENANT" \ | |
| 298 | + -H "__tenant: TENANT_ID" \ | |
| 167 | 299 | -H "Content-Type: application/json" \ |
| 168 | 300 | -d '{"locationId":"LOCATION_ID","skipCount":1,"maxResultCount":20}' |
| 169 | 301 | |
| 302 | +# 4) 单条过期/倒计时查询(仅展示) | |
| 170 | 303 | curl -s -X POST "http://127.0.0.1:19002/api/app/label-alert-timer/check-expired" \ |
| 171 | 304 | -H "Authorization: Bearer TOKEN" \ |
| 172 | - -H "__tenant: TENANT" \ | |
| 305 | + -H "__tenant: TENANT_ID" \ | |
| 173 | 306 | -H "Content-Type: application/json" \ |
| 174 | 307 | -d '{"batchId":"YOUR_BATCH_ID"}' |
| 175 | 308 | |
| 309 | +# 5) 软删除 | |
| 176 | 310 | curl -s -X DELETE "http://127.0.0.1:19002/api/app/label-alert-timer/TIMER_ID" \ |
| 177 | 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`,避免本地倒计时漂移。 | ... | ... |