# 告警计时器接口文档(泰额版) > 模块:标签告警计时器(Label Alert Timer) > 服务:`LabelAlertTimerAppService` > 范围:泰额版后端,数据在**租户业务库**(非 `antis-foodlabeling-host`) > 认证:`Authorization: Bearer {token}`;业务请求建议带 `__tenant: {tenantId}` > JSON:camelCase > Base URL 示例:`http://127.0.0.1:19002`(以实际部署为准) > 更新日期:2026-08-07 --- ## 1. 业务说明 | 概念 | 说明 | |------|------| | 用途 | 跟踪**已经打印**的标签过期时间,供 App / 前端做**倒计时与警告列表** | | 与打印关系 | **过期与「能不能打印」无关**;打印接口**不会**因计时器已过期而拒绝打印 | | 过期时刻 | 与 Print Log「Expiration」列**同源**(`ReportsPrintLogExpiryHelper.TryResolveExpiryDateTime`) | | 批次维度 | 同一 `BatchId` 无论打印多少张,**仅一条**计时器(取 `CopyIndex` 最小的 `fl_label_print_task`) | | 无过期不上列表 | 模板无法解析过期时刻时,不写入计时器、不出现在列表 | | 列表范围 | **门店级**:该 `locationId` 下未软删的全部计时器(不限当前用户自己打印) | | 状态 | `running`(未过期)/ `expired`(已过期) | | 软删 | 用户可删除计时器;删除后不再出现在列表,不影响历史打印任务 | ### 写入时机 - `UsAppLabelingAppService.PrintAsync` / `ReprintAsync` 成功创建批次后自动写入(`LabelAlertTimerWriteHelper.TryCreateFromPrintBatchAsync`) - 幂等重试(相同 `clientRequestId`)返回前也会补写一次(按 `BatchId` 唯一索引去重;含软删记录也不再插入) ### 建表 | 项 | 说明 | |----|------| | 脚本 | `泰额版/.../module/food-labeling-us/scripts/fl_label_alert_timer.sql` | | 表名 | `fl_label_alert_timer` | | 新租户 | 开通时嵌入资源自动执行 | | 已有租户 | 需用具备 CREATE 权限的账号在业务库手动执行(业务账号 `netteam` 通常无建表权限) | 主要字段:`BatchId`(唯一)、`PrintTaskId`、`LabelId`、`LocationId`、`PrintedAt`、`ExpiresAt`、`DurationSeconds`、`Title`、`Subtitle`、`IsDeleted`。 --- ## 2. 接口一览 | 功能 | 方法 | 路由 | 说明 | |------|------|------|------| | 分页列表 | POST | `/api/app/label-alert-timer/list` | `locationId` **必填** | | App 警告列表 | POST | `/api/app/label-alert-timer/app-list` | **推荐 App 使用**;`locationId` 可空(走已选门店缓存) | | 软删除 | DELETE | `/api/app/label-alert-timer/{id}` | 软删计时器 | | 查询过期/倒计时 | POST | `/api/app/label-alert-timer/check-expired` | 单条状态查询;**仅展示,不拦打印** | > 全部需登录。列表 / 删除 / 查询均校验当前账号可访问对应门店。 ### 获取 Token(App) ```http POST /api/app/th-app-auth/login Content-Type: application/json { "email": "mai@123.com", "password": "123456", "tenantId": "可选-租户Id" } ``` 后续请求头: ```http Authorization: Bearer {token} __tenant: {tenantId} Content-Type: application/json ``` --- ## 3. 分页列表 **POST** `/api/app/label-alert-timer/list` ### 请求 ```json { "locationId": "3a21220f-db37-3e32-7390-d55f64cd62a8", "skipCount": 1, "maxResultCount": 20, "dateDay": "2026-08-07" } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | locationId | string | 是 | 当前门店 Id;空则报错「门店Id不能为空」 | | skipCount | number | 是 | **页码**(从 1 开始,与项目分页约定一致) | | maxResultCount | number | 是 | 每页条数 | | dateDay | string | 否 | `yyyy-MM-dd`,按 `PrintedAt` 自然日筛选 | ### 响应 ```json { "pageIndex": 1, "pageSize": 20, "totalCount": 1, "totalPages": 1, "items": [ { "id": "1987654321000123456", "batchId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "printTaskId": "bbbbbbbb-cccc-dddd-eeee-ffffffffffff", "labelId": "label-id-001", "labelCode": "LB_001", "title": "Chicken Prep (4 hours)", "subtitle": "4 hours Completes at 2:30 PM", "totalTime": 14400, "remainingTime": 7200, "status": "running", "expiresAt": "2026-08-07T14:30:00", "printedAt": "2026-08-07T10:30:00", "locationId": "3a21220f-db37-3e32-7390-d55f64cd62a8", "productName": "Grilled Chicken" } ] } ``` ### 列表项字段(`items[]`) | 字段 | 类型 | 说明 | |------|------|------| | id | string | 计时器主键 | | batchId | string | 打印批次 Id | | printTaskId | string | 代表任务 Id(CopyIndex 最小) | | labelId | string | 标签 Id | | labelCode | string \| null | 标签编码 | | title | string | 标题(含时长文案) | | subtitle | string | 副标题(含完成时刻文案) | | totalTime | number | 总时长(秒),同库字段 `DurationSeconds` | | remainingTime | number | **剩余秒数**,`max(0, ExpiresAt - now)`;**App 倒计时用此字段** | | status | string | `running` / `expired` | | expiresAt | string | 过期时刻 | | printedAt | string | 打印时刻 | | locationId | string | 门店 Id | | productName | string \| null | 产品名称 | 排序:先 `ExpiresAt` 降序,再 `PrintedAt` 降序。 --- ## 4. App 警告列表(推荐) **POST** `/api/app/label-alert-timer/app-list` 当前登录账号可访问的门店下警告列表;出参结构与第 3 节 `list` **完全相同**(含 `remainingTime` 倒计时)。 ### 请求 ```json { "locationId": "3a21220f-db37-3e32-7390-d55f64cd62a8", "skipCount": 1, "maxResultCount": 50 } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | locationId | string | 否 | 当前门店;**为空**时取 `POST /api/app/us-app-auth/select-admin-scope-location` 写入的已选门店缓存 | | skipCount | number | 是 | 页码(从 1 开始) | | maxResultCount | number | 是 | 每页条数 | | dateDay | string | 否 | `yyyy-MM-dd`,按 `PrintedAt` 筛选 | ### 门店解析规则 1. 入参 `locationId` 有值 → 用入参 2. 入参为空 → 读管理员已选门店缓存 3. 仍无门店 → 400:「请先选择门店或传入 locationId」 4. 有门店但当前账号不可访问 → 权限校验失败(与 `list` 相同) ### 倒计时对接 | 字段 | App 用法 | |------|----------| | remainingTime | 初始剩余秒数;进入页面后可本地每秒 `-1`,或定时重新拉列表校正 | | totalTime | 进度条分母:`progress = (totalTime - remainingTime) / totalTime` | | status | `expired` 时 remainingTime 为 0,可高亮/置顶 | | expiresAt | 展示绝对过期时间;与 Print Log Expiration 对齐 | --- ## 5. 软删除 **DELETE** `/api/app/label-alert-timer/{id}` | 项 | 说明 | |----|------| | 路径参数 id | 计时器主键 | | 权限 | 校验当前用户可访问该计时器所属 `LocationId` | | 行为 | `IsDeleted=1`,`DeletionTime=now` | | 不存在/已删 | 报错「计时器不存在或已删除」 | 无响应体(成功即可)。 --- ## 6. 查询过期/倒计时状态 **POST** `/api/app/label-alert-timer/check-expired` 仅查询**已打印**批次的过期状态与剩余秒数,供单条展示。 **不得**用于拦截打印;打印流程不要依赖本接口结果做「禁止打印」。 ### 请求 至少提供 `timerId`、`batchId`、`printTaskId` 之一(优先级:`timerId` > `batchId` > `printTaskId`): ```json { "batchId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" } ``` | 字段 | 说明 | |------|------| | timerId | 计时器 Id | | batchId | 打印批次 Id | | printTaskId | 打印任务 Id(同批次任意任务均可,会反查 BatchId) | 未提供任一标识 → 「请至少提供 timerId、batchId 或 printTaskId 之一」。 ### 响应(找到记录) ```json { "found": true, "isExpired": false, "expiresAt": "2026-08-07T14:30:00", "remainingSeconds": 7200, "status": "running", "title": "Chicken Prep (4 hours)", "subtitle": "4 hours Completes at 2:30 PM", "timerId": "1987654321000123456", "batchId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" } ``` ### 响应(无记录) ```json { "found": false, "isExpired": false, "remainingSeconds": 0 } ``` | 字段 | 说明 | |------|------| | found | 是否找到未删除的计时器 | | isExpired | 是否已过期;无记录时为 `false` | | remainingSeconds | 剩余秒数(已过期或无记录为 0);与列表的 `remainingTime` 含义相同 | | status | `running` / `expired`;无记录时可能为空 | | expiresAt / title / subtitle / timerId / batchId | 找到记录时有值 | --- ## 7. 常见错误文案 | 场景 | 文案 | |------|------| | 入参为空 | 入参不能为空 | | 未登录 | 用户未登录 | | list 未传门店 | 门店Id不能为空 | | app-list 无门店且无缓存 | 请先选择门店或传入 locationId | | 删除 Id 为空 | 计时器Id不能为空 | | 记录不存在/已删 | 计时器不存在或已删除 | | check-expired 无标识 | 请至少提供 timerId、batchId 或 printTaskId 之一 | | 无门店权限 | 由门店权限校验抛出(与打印日志门店校验一致) | --- ## 8. curl 示例 ```bash # 1) 登录拿 Token(按实际环境替换) curl -s -X POST "http://127.0.0.1:19002/api/app/th-app-auth/login" \ -H "Content-Type: application/json" \ -d '{"email":"mai@123.com","password":"123456","tenantId":"TENANT_ID"}' # 2) App 警告列表(含倒计时) curl -s -X POST "http://127.0.0.1:19002/api/app/label-alert-timer/app-list" \ -H "Authorization: Bearer TOKEN" \ -H "__tenant: TENANT_ID" \ -H "Content-Type: application/json" \ -d '{"locationId":"LOCATION_ID","skipCount":1,"maxResultCount":50}' # 3) 通用分页列表 curl -s -X POST "http://127.0.0.1:19002/api/app/label-alert-timer/list" \ -H "Authorization: Bearer TOKEN" \ -H "__tenant: TENANT_ID" \ -H "Content-Type: application/json" \ -d '{"locationId":"LOCATION_ID","skipCount":1,"maxResultCount":20}' # 4) 单条过期/倒计时查询(仅展示) curl -s -X POST "http://127.0.0.1:19002/api/app/label-alert-timer/check-expired" \ -H "Authorization: Bearer TOKEN" \ -H "__tenant: TENANT_ID" \ -H "Content-Type: application/json" \ -d '{"batchId":"YOUR_BATCH_ID"}' # 5) 软删除 curl -s -X DELETE "http://127.0.0.1:19002/api/app/label-alert-timer/TIMER_ID" \ -H "Authorization: Bearer TOKEN" \ -H "__tenant: TENANT_ID" ``` --- ## 9. 前端 / App 对接建议 1. **警告页**:优先调 `app-list`;用 `remainingTime`(秒)做倒计时;`status=expired` 可高亮。 2. **不要做打印前过期拦截**:过期与可否打印无关;计时器在打印成功后才写入。 3. **与 Print Log 一致**:展示过期时间时与 Print Log Expiration 列对齐,避免两套算法。 4. **删除**:左滑/长按调 DELETE;仅隐藏计时器,不影响历史打印任务。 5. **校正**:长时间停留页面时,可定时重拉 `app-list`,避免本地倒计时漂移。