tests run: passed, skipped`
+ 若存在 **`, failed`** 或 **`0 failed` 以外的 failed 片段**,且 **failed > 0**,则 **`is_intercepted=true`**。
+ **其它任意情况**(含:合法 Summary 且无失败、无合法 Summary、仅凭退出码非 0、解析失败等)一律 **`is_intercepted=false`**。
+- **本期业务约定**:UT 门禁日志中 **Summary 仅一行**(**不出现**多 crate / 多模块导致的**多行** Summary;若将来工具链变化出现多行,须修订 §7.2 解析策略)。
+- **本期解析目标**:产出 **`is_intercepted`(布尔)** 即可;**不得**仅凭「`cargo make test` 退出码非 0」置 **`true`**(须以 Summary 可判定且存在失败用例为准)。
+- **`ut_exit_code`**(见 §5.2)建议随请求上报并落库,便于在 **`is_intercepted=false`** 时结合 Jenkins 排查。
+
+---
+
+## 4. 问题 2:如何持久化存储结果?
+
+| 方案 | 说明 | 推荐 |
+|------|------|------|
+| **A. 调用后端 API 写入数据库** | Jenkins 脚本 `curl` POST JSON;服务端校验后 INSERT | **推荐**;与现有 dt-report 架构一致,查询简单 |
+| **B. 写文件由外部同步** | 写 NDJSON/SQLite,由 Filebeat/定时任务导入 | 适合强隔离网络;增加同步组件与延迟 |
+| **C. 消息队列** | 推 Kafka/RabbitMQ,消费者落库 | 适合极大规模;本期通常过重 |
+
+**本期规约**:采用 **A**;若网络不可靠,可在 B 中 **仅作本地落盘备份**(同一路径 append 一行 JSON),不替代 API 成功路径的定义。
+
+---
+
+## 5. 问题 3:数据表结构设计
+
+### 5.1 表名(建议)
+
+`ut_gate_run`(新建表;DDL 单独迁移文件,与 ORM 字段一致)。
+
+### 5.2 字段(建议)
+
+**本期持久化**:Jenkins 构建维度 + **`mr_url`**(**唯一 MR 相关字段**)+ **`idempotency_key`** + **`is_intercepted`** + **`ut_exit_code`**;**不存** `git_remote_url`、`git_commit_sha`、`mr_id`、`source_branch`、`target_branch`、`mr_title`、`summary_line`、各 `tests_*`、`duration_sec`、`reporter_version` 等。
+
+| 字段名 | 类型(MySQL 5.7) | 可空 | 说明 |
+|--------|-------------------|------|------|
+| `id` | BIGINT UNSIGNED AI PK | 否 | 主键 |
+| `created_at` | DATETIME | 否 | 记录创建时间(默认 CURRENT_TIMESTAMP) |
+| `updated_at` | DATETIME | 否 | 更新时间 ON UPDATE CURRENT_TIMESTAMP |
+| `reported_at` | DATETIME | 否 | 门禁结束上报时间(可由服务端写 `NOW()`,或与客户端 `finished_at` 二选一) |
+| `jenkins_base_url` | VARCHAR(512) | 是 | Jenkins 根 URL;**由 `BUILD_URL` 解析**,不依赖 `JENKINS_URL`(见 §5.2.1) |
+| `job_name` | VARCHAR(256) | 否 | Job 名称(或 fullName,约定一种) |
+| `build_number` | INT UNSIGNED | 否 | 构建号 |
+| `build_url` | VARCHAR(1024) | 是 | 本次构建页 URL |
+| `mr_url` | VARCHAR(1024) | 是 | **MR 页面完整 URL**(如 CodeHub `.../merge_requests/4647`);作为**逻辑 MR 唯一标识**用于去重与列表跳转;**无 MR 场景**(非 MR 触发)可空 |
+| `idempotency_key` | VARCHAR(128) | 否 | **幂等键**:标识「同一次 Jenkins 构建」的唯一键,防止网络重试或脚本重复执行导致**同一次构建写入多行**(详见下文 **idempotency_key 说明**) |
+| `is_intercepted` | TINYINT(1) | 否 | **`1`(true)**:Summary **可判定**且**存在失败用例**;**`0`(false)**:其它**所有**情况(含未拦截、无 Summary、前置失败等) |
+| `ut_exit_code` | INT | 是 | `cargo make test` 退出码,便于排查;与 `is_intercepted` 无简单一一对应 |
+
+**为何表内没有 `WORKSPACE`?**
+**§5.2 仅列落库字段**。**`WORKSPACE`** 在 **§5.2.1 A 类**中出现,是因为上报脚本需要用它(或 **`${WORKSPACE:-.}`**)拼 **`tee` 日志路径**(见 §7),属于**运行时路径**,随 Agent/任务变化,对看板无稳定业务语义;**`build_url` 已能唯一定位本次构建**,故**不**把 `WORKSPACE` 设计成表字段。若二期要做「工作区审计」再单独加列。
+
+**唯一约束**:`UNIQUE KEY uk_idempotency (idempotency_key)`,避免同一构建重复 INSERT。
+
+**`idempotency_key` 说明(做什么用)**
+
+- Jenkins 上报可能因 **超时重试、网络抖动、脚本重复调用** 而多次 `POST` **同一构建**;若无幂等设计,库内会出现多条「同一 `job_name` + `build_number`」的记录,统计会被放大。
+- 客户端为**每一次构建**生成一个稳定字符串:**优先**使用 Jenkins **`BUILD_TAG`**(通常为 `job_name-build_number`,在单控制器内可唯一标识一次构建);或使用 `sha256(job_name + "\0" + str(build_number))` 的 hex(**不**依赖 commit,与本期表结构一致)。
+- 服务端以 **`idempotency_key`** 做 **UNIQUE**:第二次相同 key 的请求返回 **200** 且返回已有记录(或 **409**,见 §6.1),**不**再插入新行。
+
+**`is_intercepted` 与业务用语**
+
+| `is_intercepted` | 含义 |
+|------------------|------|
+| **true**(1) | **拦截到**:合法 Summary,且存在失败用例 |
+| **false**(0) | **非拦截**(混合):含「未拦截」、无 Summary、前置失败、解析失败等,**库内不区分** |
+
+### 5.2.1 数据来源规约:100% 不依赖「未定义 / 未约定」环境变量(本期)
+
+**目标**:上报脚本在任何 Agent 上不因「变量未注入」而报错或写入脏数据;**拿不到就不传或可空**,**绝不**假设 `VAR` 一定存在。
+
+**A 类——Jenkins 核心变量(视为默认可用;若极端环境缺失须有降级)**
+
+| 变量 | 用途 | 缺失时 |
+|------|------|--------|
+| `JOB_NAME` | `job_name` | 视为异常,不应继续上报(或记录错误日志) |
+| `BUILD_NUMBER` | `build_number` | 同上 |
+| `BUILD_URL` | `build_url`;并可**解析**出 `jenkins_base_url`(见下) | 同上 |
+| `BUILD_TAG` | **`idempotency_key` 首选** | 改用 `sha256(JOB_NAME + "\0" + BUILD_NUMBER)` 等**纯 A 类变量**计算 |
+| `WORKSPACE` | `tee` 日志路径(如 `"$WORKSPACE/.ci/ut_console.log"`) | 降级为 `"${WORKSPACE:-.}/.ci/..."` 或当前目录(须在试点验证) |
+
+**禁止**将下列变量当作「一定存在」写入上报逻辑(除非落入 B 类且已判空):`JENKINS_URL`、`GIT_*`、`CI_*` 等未列入本节的通用名。
+
+**B 类——CodeHub 插件变量(本期已固化,仅此一项)**
+
+| Jenkins 变量名 | 映射到请求体 / 表字段 | 使用前 |
+|----------------|----------------------|--------|
+| **`codehubMergeRequestUrl`** | **`mr_url`** | **`[ -n "${codehubMergeRequestUrl:-}" ]`** 为真则赋值;否则 **省略 `mr_url` 键** 或 **`null`**(非 MR 触发等) |
+
+- **本期**:**仅**允许通过 **`codehubMergeRequestUrl`** 填充 **`mr_url`**;**不采用** `.ci/mr_url.txt`、**不采用**从 Console / `consoleText` 解析 MR 链接作为默认路径(二期若变更须改本节)。
+- **禁止**在脚本中再引用其它未列入 **A / B 类**的变量名填充 `mr_url`。
+
+**`jenkins_base_url`(可空)**
+
+- **不得**依赖 `JENKINS_URL`。
+- **推荐**:从 **`BUILD_URL`** 用 shell 解析出 **scheme + host(+ 固定 port)**(例如 `https://jenkins.example.com`),解析失败则 **JSON 中省略 `jenkins_base_url`** 或显式 `null`。
+
+**`mr_url`(可空)**
+
+- **本期**:**仅**来自 **`codehubMergeRequestUrl`**(见上表),与 Webhook **`object_attributes.url`** 语义一致,由 CodeHub 插件注入。
+- **非 MR / 变量为空**:不传 `mr_url` 或 `null`。
+
+**`is_intercepted` / `ut_exit_code`**
+
+- **仅**依赖 **`tee` 落盘日志**与 **`PIPESTATUS[0]`**(或等价),**不**依赖任何 MR/Git 环境变量。
+
+**API / 后端**
+
+- `id`、`created_at`、`updated_at`、`reported_at` 由服务端或数据库生成,**不要求**客户端从环境变量推导。
+
+### 5.3 索引(支持看板查询)
+
+| 索引 | 字段 | 用途 |
+|------|------|------|
+| `idx_created_at` | `created_at` | 时间范围筛选、列表排序;**二期**若做趋势统计可复用 |
+| `idx_mr_url_created` | `mr_url`(191), `created_at` | 按 MR 链接聚合、列表筛选;**`mr_url` 为前缀索引**(MySQL 5.7 utf8mb4 单索引字节上限 3072,全列 `VARCHAR(1024)` 会报 ERROR 1071) |
+| `idx_is_intercepted_created` | `is_intercepted`, `created_at` | 列表按拦截状态筛选;**二期**若做分布/趋势可复用 |
+| `idx_job_build` | `job_name`, `build_number` | 对账、去重辅助 |
+
+### 5.4 MR 标识(规约)
+
+- **本期仅使用 `mr_url`**:完整 MR 页面 URL 一般已包含 **项目路径 + `merge_requests/`**,**全局可区分不同 MR**,无需再存 `mr_id`、源/目标分支。
+- **多仓(已确认)**:不同仓库、不同 MR 的 URL **路径不同**,**仅凭 `mr_url` 即可区分多仓与多 MR**,**不需要**再增加仓库键、`job_name` 等与「仓」绑定的额外字段作区分;每个仓库各自 MR → **各自一条 `mr_url`**,多条构建多行上报;看板按 **`mr_url` 去重** 即按「单仓 MR」统计;若需把多个 MR URL 合成「同一需求」,属 **二期或平台侧关联**,本期不存额外字段。
+- **取值来源**:**仅** §5.2.1 **B 类** **`codehubMergeRequestUrl`**(判非空后写入 `mr_url`)。
+- **`mr_url` 为空**:非 MR 触发的构建可不报;看板「按 MR」统计时 **排除** `mr_url` 为空的记录,或单独展示「无 MR 关联」。
+
+---
+
+## 6. 问题 4:API 接口设计
+
+### 6.1 写入:上报单次门禁结果
+
+**POST 实现细则**(请求/响应字段、幂等 200/409、鉴权配置、日志)见 **`spec/16_ut_gate_report_post_api_spec.md`**。
+
+| 项目 | 规约 |
+|------|------|
+| 路径 | `POST /api/v1/ut-gate-runs` |
+| Content-Type | `application/json` |
+| 认证 | **必须**;**本期仅采用** **固定集成 Token**:HTTP 头 **`Authorization: Bearer `**。**`token`** 由 QualityBoard 配置(如环境变量),Jenkins 经 **Credentials** 注入为环境变量后写入请求头;**禁止**硬编码进仓库脚本。**本期不采用** HMAC + 时间戳、OAuth 用户态;**禁止**将 UT 门禁写入接口与用户登录 Cookie 混用。 |
+| 幂等 | 请求体带 `idempotency_key`(**含义与生成**见 §5.2);**实现口径**见 **`spec/16_ut_gate_report_post_api_spec.md` §5**(重复且一致 → **200**;同键不同内容 → **409**)。 |
+
+**`idempotency_key` 生成建议**:**首选** Jenkins **`BUILD_TAG`**;否则使用 `sha256(job_name + "\0" + str(build_number))` 的 hex(同一 Job 下 **`build_number` 单调递增**,与 `job_name` 组合可区分每次构建)。
+
+**请求体(JSON)示例字段**(与表字段对应,蛇形命名,与前端/后端 Schema 一致):
+
+- 必填:`idempotency_key`, `job_name`, `build_number`, `is_intercepted`(布尔:`true` / `false`)
+- **建议**:`mr_url`(**仅当** **`codehubMergeRequestUrl`** 非空时**等于该变量值**)、`ut_exit_code`(整数,可空)、`build_url`(来自 `BUILD_URL`)、`jenkins_base_url`(来自 **`BUILD_URL` 解析**,失败则省略);**须满足 R5 / §5.2.1**
+
+**响应**:`201 Created` 返回写入记录 ID 与主要字段;幂等命中返回 `200 OK`。
+
+### 6.2 查询:列表与聚合(看板)
+
+**列表 GET 实现细则**(查询参数、时间语义、`reported_at` 筛选、`mr_url`/`job_name` 匹配、分页、权限)见 **`spec/17_ut_gate_runs_get_api_spec.md`**。
+
+**认证(分场景)**:**Jenkins → `POST`** 使用 **`Authorization: Bearer <固定集成Token>`**(见 §6.1、**`spec/16`**)。**浏览器 → `GET` 列表** 使用 **用户登录态(JWT)**,**不得**在浏览器持有集成 Token;细则见 **`spec/17` §3**。**本期不采用** HMAC。
+**看板前端**:列表数据经 **`GET /api/v1/ut-gate-runs`** 由已登录前端调用后端,后端直读库;与主 spec 原「服务端代调」表述等价(**不**经浏览器携带集成 Token)。
+**「UT门禁历史」权限(已确认)**:**全员可见**——凡**已登录**本系统的用户均可访问该菜单及列表数据(不因角色隐藏);仍依赖应用整体登录与内网部署。
+
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| `/api/v1/ut-gate-runs` | GET | 分页列表;筛选与排序见 **`spec/17_ut_gate_runs_get_api_spec.md` §4~§5**(`start_time`/`end_time` 绑定 **`reported_at`**;`is_intercepted`;`mr_url` 精确与 `mr_url_contains` 子串互斥;`job_name_contains`) |
+| `/api/v1/ut-gate-runs/stats` | GET | 聚合:按日/周 **`is_intercepted=true` 次数**、**`false` 次数**(`false` 为混合口径);可选 **按 `job_name`** 分布(参数:`granularity`, `start_time`, `end_time`);**本期不按仓库 URL / commit / MR 分支** 维度存库(无 `git_remote_url`、`git_commit_sha`、`source_branch`/`target_branch`);**stats 首期可不实现**,见 **`spec/17` §11** 与 §8.2 |
+
+### 6.3 异常与错误码
+
+| HTTP | 场景 |
+|------|------|
+| 400 | JSON 非法、缺少必填字段、枚举非法 |
+| 401/403 | 认证失败 |
+| 409 | 幂等键冲突且载荷不一致(若采用严格策略) |
+| 422 | 业务校验失败(如字段类型非法、与服务端约定规则冲突等;**本期**对 `is_intercepted` 的真假以 **Jenkins 脚本解析结果为准**,服务端可不做强校验) |
+| 500 | 服务端/数据库异常(须 `logger.exception`,不落敏感信息) |
+
+**Jenkins 侧**:任意 4xx/5xx **仅记录日志**,不改变门禁退出码。
+
+---
+
+## 7. 问题 5:root.sh 改造要点
+
+### 7.1 插入位置(逻辑顺序)
+
+1. 在调用 `cargo make test` **之前**:确保目录存在,准备日志文件路径。
+2. **执行测试**:`cargo make test 2>&1 | tee "${WORKSPACE:-.}/.ci/ut_console.log"`(路径须与 §5.2.1 **`WORKSPACE` 降级**一致);**同时**保存 **`PIPESTATUS[0]`**(或等价)为 `UT_EXIT_CODE`。
+3. **在现有成功/失败判定与 `exit` 之前或之后**(须在**同一 shell** 可拿到退出码处):调用上报函数 `report_ut_gate`(内部 `curl`,`|| true`)。
+4. **保持原有** `exit` 逻辑**不变**(仍以门禁规则为准)。
+
+### 7.2 Summary 解析
+
+- 从 **`${WORKSPACE:-.}/.ci/ut_console.log`**(与 §7.1 路径一致)读取匹配 **`Summary [`** 的行(**本期约定**:日志中**仅一行** Summary,直接取该行即可;实现上亦可保留「取最后一行匹配」以防御偶然重复输出)。
+- 根据该行:**存在失败用例** → 上报 **`is_intercepted: true`**;**否则**(含合法 Summary 无失败)→ **`is_intercepted: false`**。
+- 若**无合法 Summary** 或无法解析:**一律** **`is_intercepted: false`**;**不得**仅凭退出码非 0 置 **`true`**。同时上报 **`ut_exit_code`**(`PIPESTATUS[0]`)便于排查。
+
+### 7.3 失败时是否上报
+
+**是**。只要门禁流程走到「测试已执行完毕」,均应上报 **`is_intercepted`**(及 **`idempotency_key`**、**`job_name`**、**`build_number`** 等必填项)与建议的 **`ut_exit_code`**;**`mr_url`** 仅当 **`codehubMergeRequestUrl`** 非空时填写。
+**未执行测试就中断**(如克隆失败):是否仍 `POST` 由 **Jenkins 侧策略**自定(**不影响**门禁结论);**本期不提供** **`error_message`** 上报字段,亦**不在** `ut_gate_run` 表扩展该列。
+
+---
+
+## 8. 问题 6:看板展示维度
+
+### 8.1 维度(本期以列表呈现为主)
+
+| 维度 | 本期在「UT门禁历史」页的用法 |
+|------|------------------------------|
+| 时间 | 表格列 **`created_at` / `reported_at`**(与 §5.2 一致),支持时间范围 **筛选**、分页排序;**不做**按日/周聚合图表 |
+| MR | 列 **`mr_url`**(可外链 CodeHub)、列 **`is_intercepted`**;同一 MR 多行构建以多行展示,**不做** MR 维度合并小计图表 |
+| Job | 列 **`job_name`**、**`build_number`** 等;**不做** Job 占比饼图 |
+
+### 8.2 图表(本期不做)
+
+- **本期**:**不实现** ECharts(**无**趋势图、柱状图、饼图等);**不在系统首页**展示 UT 门禁相关图表或汇总卡片。
+- **二期(可选)**:可恢复趋势/分布类图表(例如按日拦截次数、`job_name` 占比等),与 §6.2 `stats` 接口能力配套后再做。
+
+### 8.3 与现有系统关系:菜单与路由
+
+- **菜单位置**:与 **「详细执行历史」**(现有 `/history` 所在主导航层级)**同级**,新增一项,菜单文案:**「UT门禁历史」**。
+- **路由**:建议 **`/ut-gate-history`**(与 **`/history`** 并列顶层路径;实现时若需微调须保持「与详细执行历史同级」语义,并在路由表中登记)。
+- **页面内容**:**仅** Ant Design **``** + 筛选条件 + 分页,对接 **`GET /api/v1/ut-gate-runs`**;行内可链 **`build_url`** / **`mr_url`** 跳转 Jenkins / CodeHub。**本期页面不引入 ECharts**。**前端实现级规约**见 **`spec/18_ut_gate_history_frontend_spec.md`**。
+- **首页**:**不**增加 UT 门禁图表或专用卡片;用户经 **「UT门禁历史」** 菜单进入列表即可。
+
+### 8.4 核心指标:按 MR 去重(不依赖是否合入)
+
+- **指标语义(推荐)**:时间窗内,**至少出现过一次 `is_intercepted=true`** 的 MR 占比——**分子** = 曾 **`true`** 的 **不同 `mr_url`** 数(**须** `mr_url` 非空);**分母** = 时间窗内 **`mr_url` 非空** 且至少有一条上报记录的 **不同 `mr_url`** 数。
+- **局限**:因 **`is_intercepted=false` 混合多种语义**,分母**包含**「从未 Summary 可判定仅 false」的 MR 时,比例解读偏「宽」;若业务要求分母仅限「Summary 可判定」的 MR,须**二期**增加「可判定」标志字段,或**约定**仅在测试跑完且可解析时上报。
+- **逻辑 MR 键**:**仅** **`mr_url`**(建议服务端或上报端做 **URL 规范化**:去 fragment、统一 host 大小写规则等,**待实现约定**)。
+- **比例**:分子 ÷ 分母。同一 **`mr_url`** 多次构建仅影响「是否曾 **`true`**」,**每个 `mr_url` 在分母中最多计一次**。
+- **`mr_url` 为空** 的构建:不参与本 MR 指标分子/分母,或单独统计「无 MR 关联构建」。
+
+---
+
+## 9. 安全与运维
+
+- **鉴权方式**:**固定集成 Token** + **`Authorization: Bearer`**(见 §6.1、§6.2);**本期不采用** HMAC。
+- **「UT门禁历史」**:**全员可见**(见 §6.2);应用仍须登录、内网访问。
+- Token 存放在 **Jenkins Credentials**,注入为环境变量(如 `QUALITYBOARD_UT_REPORT_TOKEN`)。
+- 服务端对 Token **轮换**友好:支持双 Token 过渡期(**可选实现**)。
+- 限流:按 IP 或 Token **QPS 限制**,防止误配置死循环打满服务。
+- 审计:上报成功打 **INFO**(含 `idempotency_key`、`job_name`、`build_number`,不含 Token)。
+
+---
+
+## 10. 已确认决策(原开放问题关闭)
+
+| 原编号 | 决策 |
+|--------|------|
+| 1(B 类 / `mr_url`) | **`mr_url` 仅由 `codehubMergeRequestUrl` 赋值**(判非空),已写入 **§5.2.1**、§5.4、§6.1、§7.3。 |
+| 2(`mr_url` 备选) | **不采用** `.ci/mr_url.txt` 等文件备选;**仅用 B 类变量**。 |
+| 3(多仓) | **仅凭 `mr_url` 区分多仓 / 多 MR**即可,**不增加**其它区分字段(见 §5.4)。 |
+| 4(Summary) | **约定仅一行 Summary**,**不出现多行**;解析见 §3.3、§7.2。 |
+| 5(权限) | **「UT门禁历史」全员可见**(已登录用户均可访问菜单与数据);见 §6.2、§9。 |
+| 6(`error_message`) | **不需要**;**本期不提供**该上报字段,**不扩展**表字段(见 §7.3)。 |
+
+**本期无未决开放项**;若工具链或 CodeHub 插件行为变更,须修订 §3.3、§5.2.1、§7.2 并更新本表。
+
+---
+
+## 11. 文档与实现检查清单(后续迭代用)
+
+**推荐实现顺序**见 **§12**(分阶段计划与 PR 切分)。
+
+- [ ] 新增 `database/V*.*.*__create_ut_gate_run.sql` 与 ORM/Schema/Service/API
+- [x] **`GET /api/v1/ut-gate-runs`** 分页列表:见 **`spec/17_ut_gate_runs_get_api_spec.md`**
+- [ ] Jenkins 侧:Credentials、`curl` 示例、`tee` + `PIPESTATUS` 试点;**`codehubMergeRequestUrl` → `mr_url`**(§5.2.1 **B 类**)按规约接入
+- [ ] 联调:幂等、超时(`curl --max-time`)、DNS
+- [x] 前端:**「UT门禁历史」**菜单(与详细执行历史同级)+ 路由 **`/ut-gate-history`** + 列表页(**无图表**);**全员可见**(已登录用户);`utGateApi` 服务封装(**细则见 `spec/18_ut_gate_history_frontend_spec.md`**,**§10 已实现**)
+- [ ] 更新 `docs/` 中架构/接口说明(若有对外部署)
+
+---
+
+## 12. 分阶段实现计划(推荐)
+
+本节约定 **QualityBoard 与 Jenkins 侧** 的落地顺序,与项目分层 **Model → Schema → Service → API** 一致;**不必**引入新的顶层工程子项目,在现有 `backend/models`、`schemas`、`services`、`api/v1` 下为本需求新增一组文件并注册路由即可。
+
+### 12.1 为何分阶段
+
+- **依赖链固定**:须先有迁移 SQL 与 ORM,再写 Service/API;前端依赖稳定接口。
+- **验收清晰**:每阶段有可独立验证的交付物(库表、POST 幂等、GET 分页、页面、端到端联调)。
+- **与检查清单对应**:§11 勾选项可按 §12 阶段逐项完成。
+
+### 12.2 阶段与验收标准
+
+| 阶段 | 内容 | 验收标准(建议) |
+|------|------|------------------|
+| **1. 数据层** | `database/V*.*.*__create_ut_gate_run.sql`;`UtGateRun`(或等价命名)ORM;字段与本文 **§5** 一致;**禁止** `create_all` | 迁移在目标环境执行成功 |
+| **2. 上报 API** | 请求 Schema、Service(含 **`idempotency_key` 幂等**)、`POST` 路由;**集成 Bearer** 校验;日志符合 `docs/06_logging_guide.md` | 同 key 重复上报不产生重复行;未授权/参数错误返回 4xx |
+| **3. 查询 API** | 列表 Query Schema、`select` 分页与计数、`GET` + `PageResponse` | 与项目内其它列表接口行为一致 |
+| **4. 前端** | `frontend/src/services` 下 **`utGateApi`**(字段 **snake_case** 与后端一致);路由 **`/ut-gate-history`**;**「UT门禁历史」**菜单(全员可见,见 §6.2、§9);列表页,**本期无图表** | 已登录用户可访问列表与分页;**实现见 `spec/18_ut_gate_history_frontend_spec.md` §10(v1.1)** |
+| **5. Jenkins 侧** | Credentials、**`curl --max-time`**、**`tee` + `PIPESTATUS`**(或等价);**`codehubMergeRequestUrl` → `mr_url`**(§5.2.1 **B 类**) | 试点 Job 端到端产生一条符合预期的库记录 |
+
+**文档**:`docs/` 中架构/接口说明在**功能对外可用**的版本与代码同步更新即可,无需每个小改动都改文档。
+
+### 12.3 PR 切分建议(可压缩)
+
+| PR | 范围 |
+|----|------|
+| **PR1** | 迁移 SQL + ORM(+ 若需 Pydantic 仅用于内部校验可随 PR2,避免空转) |
+| **PR2** | POST 上报(认证、幂等、日志) |
+| **PR3** | GET 分页列表 |
+| **PR4** | 前端 + `pnpm build` / 部署流程按仓库脚本执行 |
+
+单人开发时可合并为 **「数据库 + 后端」** 与 **「前端」** 两个 PR,但**不建议**省略迁移或把迁移与大量无关逻辑混在同一提交。
+
+### 12.4 不必单独成「模块」的部分
+
+- **Summary 解析**:放在 **Service** 层(或同目录下小工具函数文件),无需单独 Python 包。
+- **Jenkins 流水线脚本**:若不在本仓,以本文 **§7** 与运维侧仓库/片段为准,QualityBoard 仓内不强制新建脚本目录。
+
+---
+
+## 修订记录
+
+| 版本 | 日期 | 说明 |
+|------|------|------|
+| v0.1 | 2026-05-07 | 初稿:采集、存储、API、表结构、看板与 root.sh 集成要点 |
+| v0.2 | 2026-05-07 | §1.1:无合法 Summary 且退出码非 0 不等同于 UT 未通过;同步 §1.2、§3.3、§5.2、§6.3、§7.2、§8 口径 |
+| v0.3 | 2026-05-07 | 新增 §5.4(多仓与 MR ID 作用域)、§8.4(按逻辑 MR 去重指标);`mr_id` 字段说明与 §10 引用更新 |
+| v0.4 | 2026-05-07 | §1.2/§1.3:业务以「是否出现失败用例」为主,用例计数改为可选;同步 §3.3、§5.2、§6.1、§6.3、§7 |
+| v0.5 | 2026-05-07 | 业务语义改为「拦截/未拦截」:`ut_status` 枚举 **`intercepted`** / **`not_intercepted`** 替代 passed/failed;全文与 §8.4 指标语义对齐 |
+| v0.6 | 2026-05-07 | §3:本期**仅采用方案 B**,取消与方案 A 的配合及对账同步;§3.1 增「本期」列、§3.2 改为实现要点 |
+| v0.7 | 2026-05-07 | §5.2:`mr_title` 及 `ut_exit_code` 之后字段删除;`ut_status` 改为布尔 **`is_intercepted`**(仅 Summary 可判定且存在失败为 true,其余 false);**`idempotency_key`** 前移并专段说明;全文与 §8 统计局限同步 |
+| v0.8 | 2026-05-07 | MR 维度**仅保留 `mr_url`**;删除 `mr_id`、`source_branch`、`target_branch`;§5.3/§5.4、§6、§8、§10 同步 |
+| v0.9 | 2026-05-07 | 删除 **`git_remote_url`**、**`git_commit_sha`**;**`idempotency_key`** 改为依赖 **`BUILD_TAG`** 或 **`job_name`+`build_number`**;§1.2、§6、§8 与幂等说明同步 |
+| v1.0 | 2026-05-07 | 新增 **R5**、**§5.2.1**:**100% 不依赖未定义环境变量**;`jenkins_base_url` 从 **`BUILD_URL` 解析**;**B 类**契约白名单;§5.4、§6.1、§7、§10、§11 同步 |
+| v1.0.1 | 2026-05-07 | §5.2:补充说明 **`WORKSPACE` 仅 §5.2.1 / §7 使用、不入库** |
+| v1.1 | 2026-05-07 | **认证定稿**:**固定集成 Token** + **`Authorization: Bearer`**;写入/查询一致;**不采用** HMAC;§6.1、§6.2、§9 同步 |
+| v1.2 | 2026-05-07 | §8:**本期不做图表**、**首页不展示** UT 图表;菜单 **「UT门禁历史」** 与详细执行历史**同级**,路由建议 **`/ut-gate-history`**;§8.1–§8.3 重写;§1.2、§5.3 索引说明、§11 同步 |
+| v1.3 | 2026-05-07 | **B 类**固化为 **`codehubMergeRequestUrl`→`mr_url`**;多仓仅 **`mr_url`**;Summary **单行**;**全员可见**;**无 `error_message`**;§10 改为已确认表;§3.2–§3.3、§5.2.1、§5.4、§6–§9、§11 同步 |
+| v1.3.1 | 2026-05-07 | **R5**、表字段 `jenkins_base_url` 与 §5.2.1 对齐(不引用外部「CI 契约」);§11 前端项补充 **全员可见** |
+| v1.4 | 2026-05-07 | 新增 **§12 分阶段实现计划**(阶段表、验收、PR 切分、非单独模块说明);§11 增加对 §12 的引用 |
+| v1.5 | 2026-05-07 | §6.1:POST 细则引用 **`spec/16_ut_gate_report_post_api_spec.md`**;幂等行为与 §16 对齐 |
+| v1.6 | 2026-05-07 | 新增 **`spec/17_ut_gate_runs_get_api_spec.md`**(GET 列表);§6.2 认证分场景(POST 集成 Token / GET 用户 JWT)、筛选与 stats 说明对齐 §17 |
+| v1.7 | 2026-05-07 | §11:`GET` 列表检查项已落地;与 **`spec/17` v1.1** 同步 |
+| v1.8 | 2026-05-07 | §8.3、§11、§12.2:**前端**细则引用 **`spec/18_ut_gate_history_frontend_spec.md`** |
+| v1.9 | 2026-05-07 | §11:前端项注明 **`spec/18` §10** 已落地代码;部署侧仍需 `pnpm build` |
+| v1.10 | 2026-05-12 | §5.3、`V1.1.2` DDL:**`idx_mr_url_created`** 改为 **`mr_url`(191)** 前缀索引,避免 MySQL 5.7 utf8mb4 **ERROR 1071** |
diff --git a/spec/16_ut_gate_report_post_api_spec.md b/spec/16_ut_gate_report_post_api_spec.md
new file mode 100644
index 0000000..c0fda1a
--- /dev/null
+++ b/spec/16_ut_gate_report_post_api_spec.md
@@ -0,0 +1,213 @@
+# UT 门禁结果上报 API(POST)实现规约
+
+本文档为 **`POST /api/v1/ut-gate-runs`** 的**实现级**规约,供后端开发、联调与 Jenkins 脚本对照。上位需求、业务语义、表结构字段含义见 **`spec/15_ut_gate_jenkins_report_spec.md`**;数据库 DDL 见 **`database/V1.1.2__create_ut_gate_run.sql`**。
+
+**本文档范围**:仅 **写入(上报)** 接口;**不包含** `GET` 列表/统计、前端页面(**`GET` 见 `spec/17_ut_gate_runs_get_api_spec.md`**)。
+
+---
+
+## 1. 接口概要
+
+| 项目 | 规约 |
+|------|------|
+| 方法 / 路径 | **`POST /api/v1/ut-gate-runs`** |
+| Content-Type | **`application/json`**(UTF-8) |
+| FastAPI `tags` | **`["UT门禁上报"]`**(或等价中文标签,与项目其它路由风格一致) |
+| 鉴权 | 见 **§3** |
+
+---
+
+## 2. 关联与约束
+
+- **分层**:`Schema`(请求/响应体校验)→ `Service`(幂等、INSERT)→ `API`(路由、依赖注入)。禁止在路由函数内手写 SQL 大块逻辑。
+- **Python**:类型标注使用 `Optional[X]`(Python 3.8);**禁止** `X | None`。
+- **日志**:遵循 `docs/06_logging_guide.md`;**禁止**在日志中输出 Token 或 `Authorization` 头全文。
+- **ORM**:写入目标表 **`ut_gate_run`**,模型 **`UtGateRun`**(`backend/models/ut_gate_run.py`),字段与 DDL **逐列一致**。
+
+---
+
+## 3. 认证与安全
+
+### 3.1 方式(本期定稿)
+
+- 请求头 **`Authorization: Bearer `**。
+- **``** 与服务器配置项一致;部署时由运维配置 **QualityBoard `.env`**,与 **Jenkins Credentials** 注入的变量**共用同一密钥值**(Jenkins 侧变量名可为 `QUALITYBOARD_UT_REPORT_TOKEN` 等,**不在本文档强制 Jenkins 变量名**,仅要求请求头格式正确)。
+
+### 3.2 服务端配置项(建议)
+
+| 环境变量 / Settings 字段 | 说明 |
+|---------------------------|------|
+| **`UT_GATE_INTEGRATION_TOKEN`**(建议命名;实现时写入 `backend/core/config.py`) | 非空字符串时启用校验:与 `Authorization` 中 Bearer 值**按常量时间比较**(或框架推荐方式),相等则通过。 |
+| **未配置或为空** | **实现二选一须在 PR 描述中写明**:**(A)** 拒绝所有上报(`401`/`503` + 明确 `detail`);**(B)** 仅开发环境放行(**禁止**生产默认可匿名写入)。**推荐 (A)**,避免误部署空 Token。 |
+
+### 3.3 失败响应
+
+| HTTP | 场景 |
+|------|------|
+| **401** | 缺失 `Authorization`、非 `Bearer`、Token 不匹配或配置未启用有效 Token。 |
+| **403** | 本期可与 **401** 合并实现(统一 **401** 即可),**不**单独引入 RBAC。 |
+
+---
+
+## 4. 请求体(JSON)
+
+### 4.1 字段总表
+
+所有键名 **蛇形命名(snake_case)**,与表字段、前端后续对接一致。
+
+| JSON 键 | 必填 | 类型 | 长度 / 范围 | 映射列 | 说明 |
+|---------|------|------|---------------|--------|------|
+| `idempotency_key` | **是** | `string` | `1~128` | `idempotency_key` | 同 §5.2 主 spec;**禁止**仅空白字符。 |
+| `job_name` | **是** | `string` | `1~256` | `job_name` | Jenkins `JOB_NAME` 等,与主 spec 一致。 |
+| `build_number` | **是** | `integer` | `≥ 0` 且与 DB **`INT UNSIGNED`** 一致 | `build_number` | 超出 unsigned 范围则 **422**。 |
+| `is_intercepted` | **是** | `boolean` | — | `is_intercepted` | **`true`/`false`**;服务端落库为 `1`/`0`(tinyint)。 |
+| `ut_exit_code` | 否 | `integer` 或省略 / `null` | 与 MySQL **`INT`** 一致 | `ut_exit_code` | 建议 Jenkins 始终上报;缺省则 **`NULL`**。 |
+| `build_url` | 否 | `string` 或 `null` | `≤ 1024` | `build_url` | 通常 `BUILD_URL`。 |
+| `jenkins_base_url` | 否 | `string` 或 `null` | `≤ 512` | `jenkins_base_url` | 由 `BUILD_URL` 解析;缺省 **`NULL`**。 |
+| `mr_url` | 否 | `string` 或 `null` | `≤ 1024` | `mr_url` | **仅当** Jenkins **`codehubMergeRequestUrl`** 非空时等于该值;否则省略或 **`null`**。 |
+
+**禁止**出现的键(本期):`error_message`、`git_remote_url`、`git_commit_sha`、`mr_id`、`summary_line` 等未在 **`ut_gate_run`** 表定义的字段;若客户端传入,**建议**服务端 **忽略**(不报错)或 **422**——实现时选一种并在 OpenAPI 说明中写死;**推荐忽略未知键**(Pydantic 默认 `extra="ignore"`)。
+
+### 4.2 服务端校验规则
+
+1. **Content-Type** 非 JSON 或 body 非法 JSON → **400**(可由框架抛出,**detail** 中文简述即可)。
+2. **必填缺失 / 类型错误 / 超长度** → **422**(Pydantic `RequestValidationError` 统一处理时,对外仍应为可读中文或结构化 `detail`,与项目现有全局异常处理一致)。
+3. **`idempotency_key` / `job_name` 去首尾空白**后若为空 → **422**。
+4. **`mr_url` / `build_url` / `jenkins_base_url`**:可选 **去首尾空白**;全空白视为 **`null`**。
+5. **不在服务端重算 `is_intercepted`**:以请求体为准(与主 spec §6.3「以 Jenkins 脚本解析结果为准」一致)。
+
+### 4.3 请求示例
+
+```json
+{
+ "idempotency_key": "my-job-123",
+ "job_name": "folder/my-job",
+ "build_number": 123,
+ "is_intercepted": false,
+ "ut_exit_code": 101,
+ "build_url": "https://jenkins.example.com/job/folder/job/my-job/123/",
+ "jenkins_base_url": "https://jenkins.example.com",
+ "mr_url": "https://codehub.example.com/group/project/-/merge_requests/4647"
+}
+```
+
+---
+
+## 5. 幂等语义(本期固化)
+
+以 **`idempotency_key`** 对应 **`UNIQUE KEY uk_idempotency`**。
+
+### 5.1 比较字段集(客户端可写、参与幂等比较)
+
+以下字段若与库中已有行**全部相同**,视为**同一语义重复上报**:
+
+`job_name`, `build_number`, `build_url`, `jenkins_base_url`, `mr_url`, `is_intercepted`, `ut_exit_code`
+
+**不参与比较**:`id`, `created_at`, `updated_at`, `reported_at`(由库或服务端维护)。
+
+### 5.2 行为
+
+| 条件 | HTTP | 响应体 |
+|------|------|--------|
+| `idempotency_key` **不存在** | **201 Created** | 新建行,返回 **完整记录**(见 **§6**),含生成的 `id` 与时间字段。 |
+| `idempotency_key` **已存在**,且 §5.1 字段与已存行 **逐项相等**(`NULL` 与缺失均与 **`NULL`** 等价) | **200 OK** | 返回 **当前库中该行完整记录**(**不**修改 `updated_at`/`reported_at` 亦可,**不**再 INSERT)。 |
+| `idempotency_key` **已存在**,且 §5.1 中 **任一项不等** | **409 Conflict** | `detail` 中文说明「幂等键已存在且请求内容不一致」;**不**改库中已有行。 |
+
+> 说明:主 spec §6.1 中「200 或 409 二选一」本期按上表 **同时采用**:重复且一致 → **200**;冲突 → **409**。
+
+---
+
+## 6. 成功响应体(201 / 200)
+
+### 6.1 格式
+
+- **与表字段一致**的 **JSON 对象**(蛇形命名),至少包含:
+
+`id`, `created_at`, `updated_at`, `reported_at`, `jenkins_base_url`, `job_name`, `build_number`, `build_url`, `mr_url`, `idempotency_key`, `is_intercepted`, `ut_exit_code`
+
+### 6.2 类型与序列化
+
+| 字段 | JSON 类型 | 说明 |
+|------|-------------|------|
+| `id` | `number` | 大整数;前端若用 JS 需注意精度,本期列表可用字符串化策略 **留待 GET spec**。 |
+| `is_intercepted` | `boolean` | 由 ORM `tinyint` 映射为 `true`/`false`。 |
+| `ut_exit_code` | `number` 或 `null` | |
+| `*_at` | `string`(ISO 8601) | 与时区策略一致:推荐 **UTC** 带 `Z` 或显式 offset,与项目其它 API 一致。 |
+| 可空字符串列 | `string` 或 `null` | |
+
+### 6.3 Pydantic 响应模型
+
+- 须设置 **`model_config = {"from_attributes": True}`**(与项目 Schema 契约一致)。
+
+---
+
+## 7. 错误响应与 HTTP 表
+
+| HTTP | 场景 |
+|------|------|
+| **400** | Body 非合法 JSON。 |
+| **401** | 鉴权失败(见 §3)。 |
+| **409** | 幂等键冲突且载荷不一致(§5.2)。 |
+| **422** | 参数校验失败(§4.2)。 |
+| **500** | 未预期异常;**必须** `logger.exception()`,**detail** 对用户简短中文,**不**返回栈信息。 |
+
+**Jenkins 侧**:主 spec 约定 4xx/5xx **不改变**门禁退出码;与本文档无关但联调时需知晓。
+
+---
+
+## 8. 服务端写库规则
+
+| 列 | 规则 |
+|----|------|
+| `id` | 自增,插入后返回。 |
+| `created_at` / `updated_at` | 以 **数据库默认值 / ON UPDATE** 为准;**不在应用层覆盖**(除非项目统一用 `server_default` 已对齐)。 |
+| `reported_at` | **插入时**写 **`NOW()`**(或服务端等价 UTC);**幂等 200** 时**不更新**该列。 |
+
+**事务**:单条 INSERT 或「SELECT by key + 比较 + INSERT/返回」须在**同一事务**内完成,避免并发双插;并发下第二条应命中唯一键异常后转为「读已有行 + 比较」分支(实现细节由 Service 层处理)。
+
+---
+
+## 9. 日志
+
+| 级别 | 时机 | 内容 |
+|------|------|------|
+| **INFO** | **201** 或 **200**(幂等命中)成功落库/返回 | **`idempotency_key`、`job_name`、`build_number`**;可含 **`id`**;**不含** Token。 |
+| **WARNING** | **401**、**422**、**409** | 原因简述(**不**打完整 body)。 |
+| **ERROR** | **500** | `logger.exception()`,**不**记录敏感信息。 |
+
+---
+
+## 10. 路由注册与 OpenAPI
+
+- 在 `backend/api/v1/` 新增模块(如 **`ut_gate_run.py`**),`router = APIRouter(prefix="/ut-gate-runs", tags=["UT门禁上报"])`。
+- 在 `backend/main.py`(或统一 `api/v1/__init__.py`)**include_router**,前缀与现有 **`/api/v1`** 拼接后为 **`/api/v1/ut-gate-runs`**。
+- OpenAPI 中本接口 **description** 可简短引用本文档路径。
+
+---
+
+## 11. 非目标(本期)
+
+- **GET** / 分页 / 筛选:见后续专项或主 spec §6.2。
+- **限流**:主 spec §9 建议按 IP/Token QPS;可在首版 **TODO** 或二期实现;**不阻塞** POST 首版合入。
+- **双 Token 过渡期**:主 spec 为可选;首版单 Token 即可。
+
+---
+
+## 12. 实现检查清单
+
+- [x] `Settings` 增加 **`UT_GATE_INTEGRATION_TOKEN`**(或最终命名)及 `.env.example` 说明
+- [x] 依赖:`verify_ut_gate_integration_token`(或等价)仅作用于本路由
+- [x] `UtGateRunCreate` / `UtGateRunItem` Schema(命名以代码为准,字段与 §4、§6 一致)
+- [x] `create_ut_gate_run`(或等价)`async` Service:`INSERT` / 幂等分支 / 事务与唯一键冲突处理
+- [x] 路由:`POST`、`response_model`、**201** 与 **200** 用 `JSONResponse`/`Response` 区分状态码(FastAPI 默认单 `response_model` 时需显式处理多状态码)
+- [x] 单元测试:幂等比较逻辑、OpenAPI 路径登记(**鉴权 / 422 / 201 / 200 / 409** 的 HTTP 联测可在有 MySQL 的 CI 或本地库上补全)
+
+---
+
+## 修订记录
+
+| 版本 | 日期 | 说明 |
+|------|------|------|
+| v1.0 | 2026-05-07 | 初稿:`POST /api/v1/ut-gate-runs`、鉴权、请求/响应、幂等 200/409、日志与检查清单 |
+| v1.1 | 2026-05-07 | 后端已按 §12 落地;§12 勾选同步;补充联测说明 |
+| v1.2 | 2026-05-07 | 文首范围补充 **`GET` 见 `spec/17_ut_gate_runs_get_api_spec.md`** |
diff --git a/spec/17_ut_gate_runs_get_api_spec.md b/spec/17_ut_gate_runs_get_api_spec.md
new file mode 100644
index 0000000..e9064a8
--- /dev/null
+++ b/spec/17_ut_gate_runs_get_api_spec.md
@@ -0,0 +1,174 @@
+# UT 门禁记录查询 API(GET 列表)实现规约
+
+本文档为 **`GET /api/v1/ut-gate-runs`** 的**实现级**规约,供后端与「UT门禁历史」前端联调对照。上位需求见 **`spec/15_ut_gate_jenkins_report_spec.md`**(§6.2、§8);表结构与字段含义见 **§5** 及 **`database/V1.1.2__create_ut_gate_run.sql`**;单条记录 JSON 形状与 **`spec/16_ut_gate_report_post_api_spec.md` §6** 的 **`UtGateRunItem`** 对齐。
+
+**本文档范围**:**分页列表** `GET /api/v1/ut-gate-runs`。**不包含** `GET /api/v1/ut-gate-runs/stats`(见 **§11**);**不包含** Jenkins `POST`(见 **`spec/16_ut_gate_report_post_api_spec.md`**)。**浏览器列表页**实现见 **`spec/18_ut_gate_history_frontend_spec.md`**。
+
+---
+
+## 1. 接口概要
+
+| 项目 | 规约 |
+|------|------|
+| 方法 / 路径 | **`GET /api/v1/ut-gate-runs`** |
+| FastAPI `tags` | **`["UT门禁上报"]`**(与 POST 同路由模块时可复用,或 **`["UT门禁历史"]`**,实现时二选一保持 OpenAPI 分组清晰) |
+| 鉴权 | 见 **§3** |
+| 响应包装 | **`PageResponse[UtGateRunItem]`**(与 `backend/schemas/common.py` 一致:`items`, `total`, `page`, `page_size`) |
+
+---
+
+## 2. 关联与约束
+
+- **分层**:`Schema`(`UtGateRunQuery` 等)→ `Service`(`select` + 动态 `where` + 分页 + count)→ `API`(`Depends(get_db)`、`response_model=PageResponse[UtGateRunItem]`)。
+- **Python**:类型标注使用 `Optional[X]`(Python 3.8);**禁止** `X | None`。
+- **查询策略**:**默认禁止 JOIN**(项目规则);本接口仅查 **`ut_gate_run`** 单表。
+- **日志**:列表接口**不在每条查询后打 INFO**(见项目日志规范);**可**在参数明显非法时 **WARNING**;未预期异常 **ERROR** + `logger.exception()`。
+- **ORM → Schema**:`UtGateRunItem.model_validate(row)`,`model_config = {"from_attributes": True}`。
+
+---
+
+## 3. 认证与权限
+
+### 3.1 与集成 Token、浏览器的关系
+
+- **`UT_GATE_INTEGRATION_TOKEN` / `Authorization: Bearer`**:用于 **Jenkins → `POST /api/v1/ut-gate-runs`**(见 **`spec/16`**)。**浏览器不得持有该 Token。**
+- **本 `GET` 接口**:使用现有用户登录态 **`Depends(get_current_user)`**(JWT),与 **`/api/v1/history`** 等列表接口一致;由后端直接读库,**不要求**再用集成 Token「代调」第二跳。
+
+### 3.2 「全员可见」(已确认)
+
+- 凡 **已登录** 用户(`user` / `admin` 等现有角色)均可调用本接口;**不得**因非管理员返回 **403**(除非未登录 **401**)。
+- 与主 spec §6.2、§9 一致:菜单与数据对登录用户开放,**不**做额外 RBAC 裁剪。
+
+### 3.3 失败响应
+
+| HTTP | 场景 |
+|------|------|
+| **401** | 未登录或 JWT 无效 |
+| **422** | 查询参数校验失败(时间格式、互斥参数、非法 `sort_order` 等) |
+| **500** | 数据库或其它未预期异常;`logger.exception()`,响应 **detail** 简短中文 |
+
+---
+
+## 4. 查询参数(Query)
+
+继承 **`PageRequest`**:`page`(默认 `1`)、`page_size`(默认 `20`,**建议上限** `100`,与项目其它列表一致)。
+
+| 参数名 | 必填 | 类型 | 说明 |
+|--------|------|------|------|
+| `start_time` | 否 | `string` | **闭区间左端**,作用于 **`reported_at`**(见 §4.1)。 |
+| `end_time` | 否 | `string` | **闭区间右端**,作用于 **`reported_at`**。 |
+| `is_intercepted` | 否 | `boolean` | 为 `true` / `false` 时仅返回对应行;**省略**则不过滤。 |
+| `mr_url` | 否 | `string` | **精确匹配**:`TRIM(mr_url 参数)` 与列 **`mr_url`** 相等;**不**使用 `LIKE`。 |
+| `mr_url_contains` | 否 | `string` | **子串匹配**:`mr_url IS NOT NULL AND mr_url LIKE '%…%'`;**转义**字面量 `%`、`_`、`!`(与 `history_service._like_substring` 语义一致,防通配符注入)。最大长度 **200**(与 History 子串参数一致)。 |
+| `job_name_contains` | 否 | `string` | **`job_name`** 子串匹配,同上转义规则;最大长度 **200**。 |
+| `sort_field` | 否 | `string` | 允许值:`reported_at`(默认)、`created_at`、`id`。其它值 → **422**。 |
+| `sort_order` | 否 | `string` | `asc` / `desc`(大小写不敏感),默认 **`desc`**。其它值 → **422**。 |
+
+### 4.1 时间参数语义(固化)
+
+- 筛选列固定为 **`reported_at`**(门禁结束上报时间,与主 spec §5.2、§8.1 列表语义一致)。
+- **`start_time` / `end_time`** 与主 spec §6.2 命名对齐;**语义**为本接口上的 **`reported_at` 范围**,**不是** Jenkins `JOB_NAME` 的 `start_time`。
+- **格式**(二选一,实现须统一解析):
+ 1. **ISO 8601** 日期时间字符串(推荐带时区;若无时区则按服务器本地或统一按 **UTC** 解析,**须在实现 PR 中写死一种**);或
+ 2. **`YYYY-MM-DD`**:视为该日 **00:00:00**~**23:59:59** 的本地日界(仅当 `start_time`、`end_time` **均为**日期格式时适用;与 ISO 混用规则在实现中 **422** 或按文档写死)。
+- **闭区间**:`reported_at >= 解析(start_time)` 且 `reported_at <= 解析(end_time)`;若只传一端则只施加一端条件。
+- **`start_time` 晚于 `end_time`** → **422**。
+
+### 4.2 `mr_url` 与 `mr_url_contains` 互斥
+
+- 若 **`mr_url` 与 `mr_url_contains` 同时非空**(去空白后)→ **422**,`detail` 说明二者互斥。
+- 若均为空,则不对 `mr_url` 列加条件。
+
+### 4.3 多条件组合
+
+- 所有条件 **AND** 关系。
+
+---
+
+## 5. 排序与分页
+
+- **默认排序**:`reported_at DESC`,`id DESC`(第二排序键保证稳定顺序,**建议**写入 Service)。
+- **`sort_field` + `sort_order`** 覆盖第一排序键;**第二键**仍为 **`id DESC`**(推荐)。
+
+### 5.1 计数
+
+- `total` 使用 **`select(func.count()).select_from(与列表相同 where 条件的子查询/别名)`** 或与项目现有 `history_service` 等一致的 **count 语句**,**禁止**对大结果集仅 `LIMIT` 后数行当 `total`。
+
+---
+
+## 6. 响应体
+
+- 顶层:`PageResponse[UtGateRunItem]`。
+- **`UtGateRunItem`** 字段与 **`spec/16` §6**、表 **`ut_gate_run`** 一致;若 POST 已定义该 Schema,**GET 直接复用**,避免两套模型漂移。
+
+### 6.1 日期时间序列化
+
+- 与 **`spec/16` §6.2** 及项目现有列表 API 一致(推荐 **ISO 8601** 字符串)。
+
+### 6.2 大整数 `id`
+
+- JSON **number** 与 POST 一致;若前端需字符串化,在 **前端类型** 或 **二期** 扩展字段中处理,**本期**不强制改 Schema。
+
+---
+
+## 7. 索引与性能
+
+- 已有索引:`idx_created_at`、`idx_mr_url_created`、`idx_is_intercepted_created`、`idx_job_build`(见 `V1.1.2` DDL)。
+- **`reported_at` 范围 + `ORDER BY reported_at`**:若线上执行计划不佳,**二期**可加 **`idx_reported_at`**(须新迁移,**禁止** ALTER 保护表;仅允许**新增**迁移加索引)。
+
+---
+
+## 8. 路由与模块组织
+
+- 与 **`POST /api/v1/ut-gate-runs`** 同属 **`backend/api/v1/ut_gate_run.py`**(推荐),或拆文件但 **同一 `prefix="/ut-gate-runs"`** 的 `APIRouter`。
+- 在 **`backend/api/router.py`** 已 `include_router` 的前提下,仅新增 **`GET`** 处理函数即可。
+
+---
+
+## 9. OpenAPI
+
+- 为各 Query 参数补充 **description**(中文):尤其说明 **`start_time`/`end_time` 绑定 `reported_at`**,避免与 History 的「批次 start_time」混淆。
+
+---
+
+## 10. 错误与校验小结
+
+| 场景 | HTTP |
+|------|--------|
+| 未登录 | **401** |
+| 互斥参数、`sort_field`/`sort_order` 非法、时间解析失败、start>end | **422** |
+| 数据库异常 | **500** |
+
+---
+
+## 11. `GET /api/v1/ut-gate-runs/stats`(非本期必做)
+
+主 spec §6.2 已列出 **stats** 接口(按日/周聚合 `is_intercepted`、可选 `job_name`)。**主 spec §8.2** 明确 **本期前端不做图表**。
+
+| 项 | 规约 |
+|----|------|
+| **本期实现** | **可不实现** stats;若实现,**不得**被「UT门禁历史」列表页 **v1** 强依赖。 |
+| **鉴权** | 与列表 **GET** 相同:**`get_current_user`**。 |
+| **二期** | 与 ECharts 或看板汇总一并对接时再固化请求/响应字段表。 |
+
+若本期跳过 stats,**OpenAPI** 可不登记该路径,直至二期 spec 更新。
+
+---
+
+## 12. 实现检查清单
+
+- [x] `UtGateRunQuery`(或等价)继承 `PageRequest`,含 §4 字段与校验器
+- [x] `list_ut_gate_runs(db, query) -> Tuple[List[UtGateRun], int]`(与项目 Service 签名风格一致)
+- [x] `GET` 路由:`Depends(get_current_user)`、`response_model=PageResponse[UtGateRunItem]`
+- [x] `mr_url_contains` / `job_name_contains` 与 History 一致的 **LIKE 转义**
+- [x] 单测:Schema(互斥、时间、排序)、OpenAPI 含 `GET`(DB 联测可后续补)
+
+---
+
+## 修订记录
+
+| 版本 | 日期 | 说明 |
+|------|------|------|
+| v1.0 | 2026-05-07 | 初稿:`GET` 列表、鉴权、筛选、排序、分页、`stats` 非必做说明 |
+| v1.1 | 2026-05-07 | 后端已按 §12 实现列表接口;§12 勾选同步 |
+| v1.2 | 2026-05-07 | 文首补充 **`spec/18`** 浏览器列表页引用 |
diff --git a/spec/18_ut_gate_history_frontend_spec.md b/spec/18_ut_gate_history_frontend_spec.md
new file mode 100644
index 0000000..11ba3f6
--- /dev/null
+++ b/spec/18_ut_gate_history_frontend_spec.md
@@ -0,0 +1,170 @@
+# 「UT门禁历史」前端实现规约
+
+本文档对应 **`spec/15_ut_gate_jenkins_report_spec.md` §12.2 阶段 4(前端)** 的实现级说明,与 **§8.3** 菜单/路由/页面形态、**§6.2**(全员可见)、**`spec/17_ut_gate_runs_get_api_spec.md`**(列表 GET)对齐。**不包含** Jenkins 脚本、后端 POST、**`stats`** 图表。
+
+---
+
+## 1. 文档范围与关联
+
+| 关联 | 说明 |
+|------|------|
+| 上位需求 | **`spec/15_ut_gate_jenkins_report_spec.md`** §8.1~§8.3、§9、§11 前端勾选项 |
+| 列表 API | **`GET /api/v1/ut-gate-runs`**,见 **`spec/17_ut_gate_runs_get_api_spec.md`** |
+| 项目前端契约 | `.cursor/rules/project.mdc`:`frontend/src/services/`、`frontend/src/pages/`、`request.ts`、`pnpm build` |
+
+**非目标(本期)**:ECharts、首页 UT 卡片、`GET /api/v1/ut-gate-runs/stats`、在浏览器中持有 **`UT_GATE_INTEGRATION_TOKEN`**(仅 Jenkins 使用 POST)。
+
+---
+
+## 2. 路由与导航
+
+### 2.1 路由
+
+| 项 | 规约 |
+|----|------|
+| **浏览器路径** | **`/ut-gate-history`**(与 **`/history`** 同级顶层路径,见 §15 §8.3) |
+| **注册位置** | `frontend/src/routes/index.tsx`:在 **`RequireAuth`** 包裹的 `MainLayout` 子路由中新增一条 **`{ path: "ut-gate-history", element: }`** |
+| **鉴权** | **仅** `RequireAuth`(与「详细执行历史」一致);**不得**使用 `RequireAdmin`(全员可见) |
+
+### 2.2 侧栏菜单
+
+| 项 | 规约 |
+|----|------|
+| **文案** | **「UT门禁历史」** |
+| **位置** | 与 **「详细执行历史」**(`/history`)**同级**:紧挨在 **`/history`** 项之后(或之前,二选一固定即可) |
+| **`key`** | **`/ut-gate-history`**(与路由 path 一致,便于 `navigate`) |
+| **图标** | 选用 Ant Design Icons 中与「门禁/检查」语义接近且与 `HistoryOutlined` 可区分的图标(如 **`SafetyCertificateOutlined`** 或 **`AuditOutlined`**,实现时选定一种) |
+| **全员可见** | **`getMenuItemsByRole`**(`MainLayout.tsx`)中:**`user` 与 `admin` 均须包含本菜单项**;**不得**随「用例管理 / 管理员后台」等非管理员隐藏逻辑一并过滤掉 |
+
+---
+
+## 3. 页面与组件
+
+### 3.1 文件与导出
+
+| 项 | 规约 |
+|----|------|
+| **页面文件** | `frontend/src/pages/ut-gate-history/UtGateHistoryPage.tsx`(目录名 **kebab-case** 与路由语义一致) |
+| **组件形式** | **默认导出**函数组件:`export default function UtGateHistoryPage()` |
+| **数据加载** | `useState` + `useEffect` + 异步函数调用 `utGateApi.list`(与项目其它列表页习惯一致) |
+
+### 3.2 页面结构(本期)
+
+- **仅** Ant Design **``** + **筛选区** + **分页**;**不**引入 ECharts、**不**做首页嵌入。
+- **表格 `rowKey`**:**`"id"`**(与项目规则一致)。
+- **分页**:对接后端 **`PageResponse`**:`pagination.current`、`pagination.pageSize`、`pagination.total`,与 **`spec/17`** 的 `page` / `page_size` / `total` 一致;翻页时重新请求列表。
+
+### 3.3 建议列(与 §15 §5 / §8.1 对齐)
+
+以下列名均为 **中文表头**;数据字段 **snake_case** 与后端一致。
+
+| 列(建议顺序) | 字段 | 说明 |
+|----------------|------|------|
+| ID | `id` | 大整数;若担心 JS 精度,**可**格式化为 **字符串** 展示(见 §6.2) |
+| 上报时间 | `reported_at` | 可格式化为本地可读时间字符串 |
+| 创建时间 | `created_at` | 可选列 |
+| Job | `job_name` | — |
+| 构建号 | `build_number` | — |
+| 是否拦截 | `is_intercepted` | **`Tag`** 等:`true` → 建议文案 **「已拦截」**(或「是」);`false` → **「未拦截」**(或「否」),并在列说明或 Tooltip 中注明 **false 为混合语义**(见 §15 §1.2 / §8.4,简短即可) |
+| MR 链接 | `mr_url` | 可空;非空时 **`Link`/`Typography.Link`** `target="_blank"` `rel="noopener noreferrer"` |
+| 构建链接 | `build_url` | 可空;非空时同上外链 Jenkins |
+| 退出码 | `ut_exit_code` | 可空,展示 `-` 或 `—` |
+| 幂等键 | `idempotency_key` | 可选列;过长可 `ellipsis` + `Tooltip` |
+
+**不要求**本期展示:`jenkins_base_url`、`updated_at`(除非产品希望对齐 DBA 排障,可列为可选隐藏列二期再做)。
+
+---
+
+## 4. 筛选与查询参数
+
+筛选条件映射 **`spec/17` §4** Query 参数;请求时 **snake_case** 与后端一致。
+
+| UI 建议 | 对应 Query 参数 | 说明 |
+|---------|------------------|------|
+| **上报时间**范围(`RangePicker` 或两个日期选择) | `start_time`、`end_time` | 绑定 **`reported_at`**;推荐传 **`YYYY-MM-DD`**(与 §17 日期模式一致);**须在页面旁用中文提示**:「时间筛选对应上报时间 `reported_at`,与详细执行历史的批次时间不同」 |
+| **是否拦截**下拉(全部 / 是 / 否) | `is_intercepted` | 选「全部」则**不传**该参数 |
+| **MR 精确**输入框 | `mr_url` | 与 **MR 子串**互斥(见下) |
+| **MR 子串**输入框 | `mr_url_contains` | 与 **MR 精确**互斥;若两者均有非空值,**提交前校验**并 **`message.warning`** 或表单错误提示,**不**发请求 |
+| **Job 子串**输入框 | `job_name_contains` | 可选 |
+| **排序**(可选简化) | `sort_field`、`sort_order` | 默认 **`reported_at` + `desc`**;可提供简单下拉或固定不写死由后端默认 |
+
+**查询按钮**:点击后 `setState` 页码为 **1** 再请求,避免筛选后仍停留在超大页码无数据。
+
+**重置按钮**:清空筛选、`page=1`、重新拉取。
+
+---
+
+## 5. Service 层(`utGateApi`)
+
+### 5.1 文件位置
+
+- **推荐**:新建 **`frontend/src/services/utGate.ts`**,导出 **`utGateApi`** 对象;在 **`frontend/src/services/index.ts`** 增加 **`export { utGateApi } from "./utGate"`**(或等价聚合导出),便于其它模块按需引用。
+- **禁止**:在页面内手写完整 `axios` URL 字符串散落多处;**须**通过 **`request`**(`./request.ts`)实例调用。
+
+### 5.2 `request` 与鉴权
+
+- 使用 **`import request from "./request"`**(或与 `historyApi` 相同的 project's request 路径)。
+- **JWT**:依赖现有 **`request` 拦截器**从 `localStorage` 注入 **`Authorization: Bearer`**;**禁止**把 **`UT_GATE_INTEGRATION_TOKEN`** 写入前端代码或 `localStorage`。
+
+### 5.3 TypeScript 类型(与后端一致 **snake_case**)
+
+在 **`utGate.ts`**(或紧邻的 `types` 片段)中定义,字段与 **`UtGateRunItem`** / **`UtGateRunQuery`** 对齐,例如:
+
+- **`UtGateRunItem`**:`id`, `created_at`, `updated_at`, `reported_at`, `jenkins_base_url`, `job_name`, `build_number`, `build_url`, `mr_url`, `idempotency_key`, `is_intercepted`, `ut_exit_code`(日期时间字段类型为 **`string | null`** 等与 `HistoryItem` 风格一致即可)。
+- **`UtGateRunListParams`**(可选命名):`page`, `page_size`, `start_time`, `end_time`, `is_intercepted`, `mr_url`, `mr_url_contains`, `job_name_contains`, `sort_field`, `sort_order`;全部为 **可选**除分页默认值由页面传入。
+
+### 5.4 API 方法
+
+```ts
+utGateApi.list(params?: UtGateRunListParams): Promise>
+```
+
+- **HTTP**:`request.get("/ut-gate-runs", { params })`。
+- **Query 序列化**:与 `historyApi` 类似,**仅附加非 `undefined` / 非空字符串** 的键;`boolean` 的 `is_intercepted` 需能序列化为后端可解析形式(与 FastAPI 行为一致,一般为 `true`/`false` 字符串)。
+
+---
+
+## 6. 大整数 `id` 与日期
+
+- **`id`**:`BIGINT` 可能超过 **`Number.MAX_SAFE_INTEGER`**。表格展示推荐 **`String(row.id)`** 或使用 **`BigInt`** 再转字符串;**避免**对大 `id` 做依赖精度的数值运算。
+- **日期时间**:后端返回 ISO 字符串;展示层可用 **`dayjs`**(若项目已用)或 **`toLocaleString`** 格式化,**不**强制与时区策略改动后端。
+
+---
+
+## 7. 错误与空态
+
+- **401**:由 **`request` 响应拦截器**统一跳转登录(现有行为),页面无需重复实现跳转逻辑。
+- **列表为空**:`` 或表格 `locale.emptyText`。
+- **非 401 错误**:`message.error` 展示简短中文(可从 `error.response?.data?.detail` 读取数组或字符串)。
+
+---
+
+## 8. 构建与部署
+
+- 修改前端后须执行 **`pnpm build`**(或通过 **`scripts/deploy.sh`**),并**重启**后端以托管新静态资源(见项目规则 §五)。
+- **不**新增 `npm`/`yarn` 依赖除非产品必须;若新增须 **`pnpm add`** 并说明用途。
+
+---
+
+## 9. 文档与 Epic 表
+
+- 功能对用户可用后,同步 **`docs/05_technical_architecture.md`** 中 Epic 表「UT 门禁」一行:前端路由 **`/ut-gate-history`** 与页面组件名(与 §15 §11「更新 docs」一致)。
+
+---
+
+## 10. 实现检查清单
+
+- [x] `frontend/src/services/utGate.ts`:`utGateApi` + **`UtGateRunItem`** / 列表请求参数类型
+- [x] `frontend/src/pages/ut-gate-history/UtGateHistoryPage.tsx`:表格 + 筛选 + 分页 + 外链
+- [x] `routes/index.tsx`:注册 **`/ut-gate-history`**,**仅** `RequireAuth`
+- [x] `MainLayout.tsx`:菜单项 **「UT门禁历史」**,**`user`/`admin` 均可见**
+- [ ] 部署环境执行 **`pnpm build`** 并重启后端(见项目规则)
+
+---
+
+## 修订记录
+
+| 版本 | 日期 | 说明 |
+|------|------|------|
+| v1.0 | 2026-05-07 | 初稿:对应 §15 §12.2 阶段 4;路由、菜单全员可见、`utGateApi`、表格与筛选、非目标范围 |
+| v1.1 | 2026-05-07 | 仓库已按 §10 落地前端实现;构建请在本地执行 `pnpm build` |