diff --git a/.env.example b/.env.example index 32a49b8..54347bf 100644 --- a/.env.example +++ b/.env.example @@ -27,6 +27,13 @@ WELINK_CARD_INI_PATH= # 对外访问根地址(无尾部 /),一键通知卡片内链接依赖,示例:https://report.example.com PUBLIC_APP_URL= +# 详细执行历史 Drawer 外部链接(留空则前端展示「暂无」) +TEST_CODE_REPO_URL= +# 取包地址:模板 URL,占位符 code_branch / start_time / package_name(测试/开发环境可配不同值) +PACKAGE_INIT_URL= +PACKAGE_NAME_MAC= +PACKAGE_NAME_OH= + # LDAP 域登录(LDAP_HOST 非空时启用域账号登录,留空则使用 MVP 工号+统一密码模式) LDAP_HOST= LDAP_PORT=389 diff --git a/backend/api/router.py b/backend/api/router.py index 1496531..793041a 100644 --- a/backend/api/router.py +++ b/backend/api/router.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from backend.api.v1 import auth, dashboard, overview, history, analysis, cases, report, notification, admin, ut_gate_run +from backend.api.v1 import auth, dashboard, overview, history, analysis, cases, report, notification, admin, ut_gate_run, app api_router = APIRouter(prefix="/api/v1") @@ -14,3 +14,4 @@ api_router.include_router(notification.router) api_router.include_router(admin.router) api_router.include_router(ut_gate_run.router) +api_router.include_router(app.router) diff --git a/backend/api/v1/app.py b/backend/api/v1/app.py new file mode 100644 index 0000000..1bef28e --- /dev/null +++ b/backend/api/v1/app.py @@ -0,0 +1,29 @@ +# ============================================================ +# API 路由层 — 应用级前端配置(/api/v1/app) +# ============================================================ + +from typing import Optional + +from fastapi import APIRouter, Depends + +from backend.core.config import settings +from backend.core.dependencies import get_current_user +from backend.schemas.app_config import FrontendConfigResponse + +router = APIRouter(prefix="/app", tags=["应用配置"]) + + +def _opt_str(val: str) -> Optional[str]: + s = (val or "").strip() + return s if s else None + + +@router.get("/frontend-config", response_model=FrontendConfigResponse) +async def get_frontend_config(_: dict = Depends(get_current_user)): + """返回前端展示用只读配置(测试代码仓、取包地址模板等),值来自 .env。""" + return FrontendConfigResponse( + test_code_repo_url=_opt_str(settings.TEST_CODE_REPO_URL), + package_init_url=_opt_str(settings.PACKAGE_INIT_URL), + package_name_mac=_opt_str(settings.PACKAGE_NAME_MAC), + package_name_oh=_opt_str(settings.PACKAGE_NAME_OH), + ) diff --git a/backend/core/config.py b/backend/core/config.py index b4c07b3..12f8c28 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -31,6 +31,13 @@ class Settings(BaseSettings): # 站点对外根 URL(无尾部斜杠),用于一键通知 WeLink 卡片内 /history 绝对链接 PUBLIC_APP_URL: str = "" + # 详细执行历史 Drawer:测试代码仓链接(空则前端展示「暂无」) + TEST_CODE_REPO_URL: str = "" + # 取包地址:模板 URL,占位符 code_branch / start_time / package_name + PACKAGE_INIT_URL: str = "" + PACKAGE_NAME_MAC: str = "" + PACKAGE_NAME_OH: str = "" + # AI 失败分析:main_module → 仓库映射(YAML,模板见 config/module_repo_mapping.yaml.example) AI_MODULE_REPO_MAPPING_PATH: str = "" # AI 失败分析:AIFA 服务地址(不含 /v1/analyze) diff --git a/backend/schemas/app_config.py b/backend/schemas/app_config.py new file mode 100644 index 0000000..a842d1b --- /dev/null +++ b/backend/schemas/app_config.py @@ -0,0 +1,21 @@ +# ============================================================ +# 前端只读配置 — Schema +# ============================================================ + +from typing import Optional + +from pydantic import BaseModel, Field + + +class FrontendConfigResponse(BaseModel): + """GET /app/frontend-config:Drawer 外部链接等前端展示用配置(来自 .env)。""" + + test_code_repo_url: Optional[str] = Field( + None, description="测试代码仓地址;未配置则为 None" + ) + package_init_url: Optional[str] = Field( + None, + description="取包链接模板,占位符 code_branch / start_time / package_name", + ) + package_name_mac: Optional[str] = Field(None, description="mac 平台包名") + package_name_oh: Optional[str] = Field(None, description="oh 平台包名") diff --git a/backend/schemas/batch_report.py b/backend/schemas/batch_report.py index 9fcdb98..270f1e4 100644 --- a/backend/schemas/batch_report.py +++ b/backend/schemas/batch_report.py @@ -36,6 +36,10 @@ class BatchReportResponse(BaseModel): model_config = {"from_attributes": True} start_time: str = Field(..., description="轮次(批次)") + code_branch: Optional[str] = Field( + None, + description="构建分支;同批次多分支时用顿号拼接,无则 None", + ) total: int = Field(..., ge=0) passed: int = Field(..., ge=0) failed: int = Field(..., ge=0, description="failed + error") diff --git a/backend/services/batch_report_service.py b/backend/services/batch_report_service.py index 3e267d0..e8a5586 100644 --- a/backend/services/batch_report_service.py +++ b/backend/services/batch_report_service.py @@ -56,6 +56,21 @@ async def get_batch_report(db: AsyncSession, start_time: str) -> BatchReportResp failed = int(agg_row.failed or 0) skip = int(agg_row.skip or 0) + branch_stmt = ( + select(Ph.code_branch) + .where(Ph.start_time == batch) + .where(Ph.code_branch.isnot(None)) + .where(func.trim(Ph.code_branch) != "") + .distinct() + .order_by(Ph.code_branch) + ) + branch_rows = (await db.execute(branch_stmt)).scalars().all() + code_branch: Optional[str] = None + if branch_rows: + branches = [str(b).strip() for b in branch_rows if b and str(b).strip()] + if branches: + code_branch = "、".join(branches) + # ----- 2) bug 归因 × 主模块(单批次内数据量可控,使用 JOIN)----- bug_type = func.lower(func.trim(Pfr.failed_type)) main_mod = func.coalesce(func.nullif(func.trim(Ph.main_module), ""), "") @@ -150,6 +165,7 @@ async def get_batch_report(db: AsyncSession, start_time: str) -> BatchReportResp return BatchReportResponse( start_time=batch, + code_branch=code_branch, total=total, passed=passed, failed=failed, diff --git a/backend/tests/test_package_url.py b/backend/tests/test_package_url.py new file mode 100644 index 0000000..127dfd9 --- /dev/null +++ b/backend/tests/test_package_url.py @@ -0,0 +1,53 @@ +"""resolve_package_url 单元测试。""" + +from backend.utils.package_url import resolve_package_url + +_TEMPLATE = ( + "https://example.com/path?branch=code_branch&time=start_time&pkg=package_name" +) + + +def test_mac_platform_builds_url(): + url, hint = resolve_package_url( + _TEMPLATE, + "mac.pkg", + "oh.pkg", + "master", + "2026_0110_1200", + "MAC", + ) + assert hint is None + assert url == "https://example.com/path?branch=master&time=202601101200&pkg=mac.pkg" + + +def test_oh_platform_builds_url(): + url, hint = resolve_package_url( + _TEMPLATE, + "mac.pkg", + "oh.hap", + "930bugfix", + "202601211000", + "oh", + ) + assert hint is None + assert url == "https://example.com/path?branch=930bugfix&time=202601211000&pkg=oh.hap" + + +def test_unknown_platform(): + url, hint = resolve_package_url( + _TEMPLATE, + "mac.pkg", + "oh.pkg", + "master", + "202601211000", + "android", + ) + assert url is None + assert hint == "unknown_platform" + + +def test_missing_config_or_fields(): + assert resolve_package_url("", "a", "b", "master", "t", "mac") == (None, None) + assert resolve_package_url(_TEMPLATE, "", "b", "master", "t", "mac") == (None, None) + assert resolve_package_url(_TEMPLATE, "a", "b", "", "t", "mac") == (None, None) + assert resolve_package_url(_TEMPLATE, "a", "b", "master", "", "mac") == (None, None) diff --git a/backend/utils/package_url.py b/backend/utils/package_url.py new file mode 100644 index 0000000..dfdff6f --- /dev/null +++ b/backend/utils/package_url.py @@ -0,0 +1,46 @@ +# ============================================================ +# 取包地址拼接(占位符:code_branch、start_time、package_name) +# ============================================================ + +from typing import Optional, Tuple + + +def resolve_package_url( + init_url: str, + package_name_mac: str, + package_name_oh: str, + code_branch: Optional[str], + start_time: Optional[str], + platform: Optional[str], +) -> Tuple[Optional[str], Optional[str]]: + """ + 生成取包链接。 + + :return: (url, hint)。hint 为 ``unknown_platform`` 时表示平台不支持;其余失败为 (None, None)。 + """ + template = (init_url or "").strip() + if not template: + return None, None + + branch = (code_branch or "").strip() + batch = (start_time or "").strip().replace("_", "") + if not branch or not batch: + return None, None + + plat = (platform or "").strip().lower() + if plat == "mac": + package_name = (package_name_mac or "").strip() + elif plat == "oh": + package_name = (package_name_oh or "").strip() + else: + return None, "unknown_platform" + + if not package_name: + return None, None + + url = ( + template.replace("code_branch", branch) + .replace("start_time", batch) + .replace("package_name", package_name) + ) + return url, None diff --git a/docs/02_prd.md b/docs/02_prd.md index 96d78a9..ee24f16 100644 --- a/docs/02_prd.md +++ b/docs/02_prd.md @@ -83,6 +83,7 @@ - **Given** 图表进行数据渲染,**When** 绘制趋势线,**Then** 提供多条折线(或双 Y 轴折线+柱状图组合),明确展示关键指标的走势:失败用例数(折线A)、总用例数(折线B)、执行耗时/执行时间(折线C)。 - **Given** 用户想要查看某个具体批次的详细数据,**When** 将鼠标悬停在 X 轴的某个"批次"节点上(触发 Hover),**Then** 出现提示框(Tooltip),完整展示该批次的所有次要数据,包含:具体的执行发生时间(Timestamp)、成功数、未处理数、以及具体的执行耗时。 - **Given** 底层 MySQL 中存在海量的历史批次数据,**When** 渲染折线图时,**Then** 系统默认仅查询并展示最近 N 个(如最近 30 个)批次的数据点,以避免 X 轴刻度过于拥挤,并保证图表渲染性能。 + - **Given** 用户在趋势图上点击某批次的「失败用例数」折线数据点,**When** 跳转至详细执行历史,**Then** URL 自动带入该批次(`start_time`)及执行结果筛选(`case_result=failed` 与 `error`);点击「总用例数」折线数据点则仅带入批次筛选。 - **数据来源:** 以 `pipeline_overview` 为主表,按 `batch` 聚合:`SUM(case_num)` 总用例数、`SUM(failed_num)` 失败数、`SUM(passed_num)` 通过数;执行耗时通过 `MAX(batch_end) - MIN(batch_start)` 计算。 #### Story 1.2: 多层级失败明细下钻 @@ -297,7 +298,7 @@ | 区域 | 内容 | |------|------| | **基本信息** | 用例名、批次、分组、主模块、用例级别、平台、代码分支 | -| **外部链接** | 日志URL、截图URL、测试报告URL、流水线URL(均以可点击链接形式展示) | +| **外部链接** | 日志URL、截图URL、测试报告URL、流水线URL、**测试代码仓**(`.env`)、**取包地址**(按行字段 + `.env` 模板拼接) | | **归因分析区** | 失败类型下拉选择(来源 `case_failed_type`)、详细原因文本框、DTS 单号输入框、恢复批次输入框 | | **流转操作区** | 当前负责人展示、指派给其他人(下拉选择来源 `ums_email`) | | **操作时间线** | 按时间倒序展示历次操作记录(来源 `owner_history` 及审计日志) | diff --git a/docs/03_deployment_guide.md b/docs/03_deployment_guide.md index d839b8a..af8d4cd 100644 --- a/docs/03_deployment_guide.md +++ b/docs/03_deployment_guide.md @@ -198,6 +198,14 @@ WELINK_CARD_INI_PATH= # 一键通知 WeLink 卡片中的「详细执行历史」链接:填写用户浏览器可访问的根地址(无尾部 /) PUBLIC_APP_URL=https://your-report-host.example.com + +# 详细执行历史 Drawer:测试代码仓(留空则展示「暂无」) +TEST_CODE_REPO_URL=https://your-code-repo.example.com + +# 取包地址:模板与 mac/oh 包名(测试、开发环境可分别配置) +PACKAGE_INIT_URL=https://clouddragon.huawei.com/artifact/artifactcenter/product?repoKey=product_generic&path=vnext%2Fdaily%2Frolling_test%2Fsmoke%2Fcode_branch%2Fstart_time%2Fpackage_name&coordinate=vnext&packageType=Generic&fileType=file&from=link +PACKAGE_NAME_MAC=bitfun_ide_smoke_test_macos_arm64.dmg +PACKAGE_NAME_OH=bitfun_ide_uitest_ohos_aarch64.hap ``` ### 4.4 启动后端 diff --git a/docs/04_project_structure.md b/docs/04_project_structure.md index ee39b9c..915ae22 100644 --- a/docs/04_project_structure.md +++ b/docs/04_project_structure.md @@ -41,7 +41,7 @@ dt-report/ | 文件 | 说明 | |------|------| -| `config.py` | 全局配置。使用 Pydantic `BaseSettings` 从 `.env` 文件读取 `DATABASE_URL`、`SECRET_KEY`、`ADMIN_EMPLOYEE_IDS`、WeLink API 配置、`WELINK_CARD_INI_PATH`(WeLink 卡片 INI 路径)、`PUBLIC_APP_URL`(一键通知卡片内绝对链接根地址)、日志配置(ENV、LOG_LEVEL、LOG_DIR 等) | +| `config.py` | 全局配置。使用 Pydantic `BaseSettings` 从 `.env` 文件读取 `DATABASE_URL`、`SECRET_KEY`、`ADMIN_EMPLOYEE_IDS`、WeLink API 配置、`WELINK_CARD_INI_PATH`(WeLink 卡片 INI 路径)、`PUBLIC_APP_URL`(一键通知卡片内绝对链接根地址)、`TEST_CODE_REPO_URL` / `PACKAGE_INIT_URL` / `PACKAGE_NAME_MAC` / `PACKAGE_NAME_OH`(详细执行历史 Drawer 外部链接)、日志配置(ENV、LOG_LEVEL、LOG_DIR 等) | | `dashboard_defaults.py` | 首页看板代码内默认策略(非环境变量):如是否按 `pipeline_overview.batch` 前缀过滤轮次;后续可迁到 `Settings` / `.env` | | `database.py` | 数据库连接层。创建 SQLAlchemy 异步引擎(`create_async_engine`)和会话工厂(`async_sessionmaker`),提供 `get_db()` 异步生成器用于 FastAPI 依赖注入 | | `security.py` | 认证鉴权工具。JWT Token 的生成(`create_access_token`)与验证(`verify_token`),管理员权限校验依赖项(`require_admin`) | @@ -118,6 +118,7 @@ Schema 定义 API 的请求参数格式和响应 JSON 格式,由 FastAPI 自 |------|---------|------|---------| | `router.py` | `/api/v1` | 总路由注册,将所有子模块路由挂载到 `/api/v1` 下 | ✅ 已实现 | | `v1/history.py` | `/api/v1/history` | 执行明细:`GET /history` 分页筛选(未选批次时默认最近 30 批;已传非空 `start_time_contains` 或已选非空 `case_name` / `case_name_contains` 且无批次 IN 时不注入,见 Spec 08 §3.1 / §3.1.1;各维度可选 `*_contains` 子串);`GET /history/batch-report` 轮次通报;`GET /history/oh-daily-export` OH 日报 TSV(格式见 `constants/oh_daily_export_table.py`);`POST /history/failure-process` 失败标注(仍为 bug 且跟踪人变更时可向新跟踪人发 WeLink);`POST /history/inherit-failure-reason` 继承;`POST /history/one-click-analyze` 一键分析(整批 bug);`POST /history/one-click-bug-notify` 一键通知(WeLink,spec/13) | ✅ 已实现 | +| `v1/app.py` | `/api/v1/app` | 前端只读配置:`GET /app/frontend-config`(测试代码仓、取包地址模板等,来自 `.env`) | ✅ 已实现 | | `v1/auth.py` | `/api/v1/auth` | 认证接口(登录/登出) | 🔲 占位 | | `v1/dashboard.py` | `/api/v1/dashboard` | 数据看板接口 | 🔲 占位 | | `v1/overview.py` | `/api/v1/overview` | 分组概览接口 | 🔲 占位 | @@ -223,7 +224,7 @@ Schema 定义 API 的请求参数格式和响应 JSON 格式,由 FastAPI 自 | `history/HistoryPage.tsx` | 详细执行历史页面。Table 展示 pipeline_history 数据(含跟踪人、失败原因列),支持分页与多维度筛选(字符串维度下拉可搜索;有匹配候选项且搜索非空时首行「全部」应用子串筛选,与 URL `*_contains` 同步;`allowClear` 清除该维度 IN 与子串),Drawer 含基本信息区、失败归因区(仅 failed 时展示)、外部链接区;用例名链至钻取页;工具栏含分析处理、继承、一键分析、**一键通知**(spec/13)、一键生成通报、**日报数据**(`GET /history/oh-daily-export`,批次子串搜索弹窗 + TSV 复制);**搜索模板**:「保存模板」在「日报数据」之后,仅「筛选确认」后可保存(`historyApi` 调 `GET/POST/DELETE /history/search-templates`,每用户最多 10 条,交互与校验见 `docs/02_prd.md` Story 1.3);模板条展示于通报按钮同区域后部,点击应用查询、× 删除确认,删除当前所用模板时不改 URL;钻取模式应用模板时保留用例/平台/分支锚定。“已分析”列新增行级「分析」按钮,点击后复用与工具栏「分析处理」相同的弹窗与提交流程;**分析处理**弹窗在失败类型为 bug 时跟踪人为可编辑输入,默认按模块带出「姓名 工号」(spec/04);「详细原因」为多行 `Input.TextArea`,有本地缓存时上方提供可搜索 `Select`(`localStorage`)一键写入历史文案;失败标注 Modal 宽度约 580px、`body` 设 `maxHeight`+纵向滚动,避免内容与页脚重叠。布局:主内容区内占满剩余高度,筛选区与表头、分页固定,**仅表体区域纵向滚动**(`ResizeObserver` + `scroll.y`);样式见 `history-table.css` | ✅ 已实现 | | `history/HistoryStringMultiFilter.tsx` | 历史页字符串多选筛选项:搜索、`dropdownRender` 内「全部」(不展示数量)、子串条件以灰色 `Tag` 与多选同框展示(`history-table.css`)、隐藏 `*_contains` 与 `Select` 的 Form 联动 | ✅ 已实现 | | `history/CaseExecutionsHistoryPage.tsx` | 用例执行历史钻取壳组件,渲染 `HistoryPage drilldown`(`/history/case-executions`),规约 spec/12 | ✅ 已实现 | -| `dashboard/DashboardPage.tsx` | 首页大盘 | 🔲 占位 | +| `dashboard/DashboardPage.tsx` | 首页大盘:最新批次统计卡片(点击跳转 History 带 `start_time`);master / bugfix 双趋势折线图(ECharts);点击「失败用例数」折线点跳转 History 并预填 `case_result=failed&error`,点击「总用例数」折线点仅带 `start_time`(见 spec/09 §3.7) | ✅ 已实现 | | `overview/OverviewPage.tsx` | 分组执行历史 | 🔲 占位 | | `cases/CasesPage.tsx` | 用例管理 | 🔲 占位 | | `report/ReportPage.tsx` | 总结报告 | 🔲 占位 | diff --git a/frontend/src/pages/dashboard/DashboardPage.tsx b/frontend/src/pages/dashboard/DashboardPage.tsx index f661665..cb18097 100644 --- a/frontend/src/pages/dashboard/DashboardPage.tsx +++ b/frontend/src/pages/dashboard/DashboardPage.tsx @@ -9,6 +9,15 @@ import { type LatestBatchItem, } from "../../services"; +const FAILED_CASE_RESULTS = ["failed", "error"] as const; + +function buildHistoryHref(batch: string, caseResults?: readonly string[]): string { + const q = new URLSearchParams(); + q.append("start_time", batch); + caseResults?.forEach((r) => q.append("case_result", r)); + return `/history?${q.toString()}`; +} + function buildChartOption(items: BatchTrendItem[]) { return { tooltip: { @@ -123,15 +132,24 @@ export default function DashboardPage() { }, []); const handleCardClick = (batch: string) => { - navigate(`/history?start_time=${encodeURIComponent(batch)}`); + navigate(buildHistoryHref(batch)); }; - const createChartClickHandler = (items: BatchTrendItem[]) => (params: { dataIndex: number }) => { - const item = items[params.dataIndex]; - if (item?.batch) { - navigate(`/history?start_time=${encodeURIComponent(item.batch)}`); - } - }; + const createChartClickHandler = + (items: BatchTrendItem[]) => + (params: { dataIndex?: number; seriesName?: string }) => { + const idx = params.dataIndex; + if (idx == null || idx < 0) return; + const item = items[idx]; + if (!item?.batch) return; + const isFailedSeries = params.seriesName === "失败用例数"; + navigate( + buildHistoryHref( + item.batch, + isFailedSeries ? FAILED_CASE_RESULTS : undefined, + ), + ); + }; const hasLatestBatch = latestBatch && Object.keys(latestBatch).length > 0; diff --git a/frontend/src/pages/history/HistoryPage.tsx b/frontend/src/pages/history/HistoryPage.tsx index d4472b6..87f120c 100644 --- a/frontend/src/pages/history/HistoryPage.tsx +++ b/frontend/src/pages/history/HistoryPage.tsx @@ -40,7 +40,10 @@ import { type InheritSourceRecordItem, type BatchReportResponse, type HistorySearchTemplateItem, + appApi, + type FrontendConfig, } from "../../services"; +import { resolvePackageUrl } from "../../utils/packageUrl"; import AIFailureAnalysisTab from "./components/ai_analysis/AIFailureAnalysisTab"; const { Text, Title, Paragraph } = Typography; @@ -64,6 +67,7 @@ function buildRollingReportMarkdown(data: BatchReportResponse): string { const lines: string[] = []; lines.push("【rolling线看护进展通告】"); lines.push(`batch:${data.start_time}`); + lines.push(`构建分支:${data.code_branch ?? "—"}`); lines.push( `用例总数:${data.total},成功:${data.passed},失败:${data.failed},跳过:${data.skip}`, ); @@ -142,6 +146,28 @@ function UrlLink({ return 暂无; } +function PackageUrlLink({ + config, + record, +}: { + config: FrontendConfig | null; + record: HistoryItem; +}) { + const resolved = resolvePackageUrl( + config, + record.code_branch, + record.start_time, + record.platform, + ); + if (resolved.kind === "url") { + return ; + } + if (resolved.kind === "unknown_platform") { + return 未知平台; + } + return 暂无; +} + /** 仅当内容被遮挡时悬停显示 */ function EllipsisTooltip({ title, @@ -268,6 +294,7 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) { const [loading, setLoading] = useState(false); const [options, setOptions] = useState(null); const [optionsLoading, setOptionsLoading] = useState(false); + const [frontendConfig, setFrontendConfig] = useState(null); const [drawerVisible, setDrawerVisible] = useState(false); const [drawerRecord, setDrawerRecord] = useState(null); const [drawerFailureTabKey, setDrawerFailureTabKey] = useState("failure"); @@ -463,8 +490,18 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) { } }; + const fetchFrontendConfig = async () => { + try { + const cfg = await appApi.frontendConfig(); + setFrontendConfig(cfg); + } catch { + setFrontendConfig(null); + } + }; + useEffect(() => { fetchOptions(); + void fetchFrontendConfig(); }, []); const loadSearchTemplates = useCallback(async () => { @@ -2596,10 +2633,18 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) { 日志: -
+
流水线:
+
+ 测试代码仓: + +
+
+ 取包地址: + +
)} diff --git a/frontend/src/services/index.ts b/frontend/src/services/index.ts index 3872eb8..5720d12 100644 --- a/frontend/src/services/index.ts +++ b/frontend/src/services/index.ts @@ -202,6 +202,7 @@ export interface BatchReportPlatformGroup { export interface BatchReportResponse { start_time: string; + code_branch: string | null; total: number; passed: number; failed: number; @@ -349,6 +350,19 @@ export const historyApi = { // --- Dashboard API --- +/** GET /app/frontend-config 前端只读配置 */ +export interface FrontendConfig { + test_code_repo_url: string | null; + package_init_url: string | null; + package_name_mac: string | null; + package_name_oh: string | null; +} + +export const appApi = { + frontendConfig: (): Promise => + request.get("/app/frontend-config") as Promise, +}; + export interface LatestBatchItem { batch: string; total_case_num: number; diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts index 1c87180..e09fd15 100644 --- a/frontend/src/utils/index.ts +++ b/frontend/src/utils/index.ts @@ -1 +1,2 @@ export { getSafeRedirectPath } from "./safeRedirect"; +export { resolvePackageUrl, type PackageUrlResolve } from "./packageUrl"; diff --git a/frontend/src/utils/packageUrl.ts b/frontend/src/utils/packageUrl.ts new file mode 100644 index 0000000..a5bb982 --- /dev/null +++ b/frontend/src/utils/packageUrl.ts @@ -0,0 +1,43 @@ +import type { FrontendConfig } from "../services"; + +export type PackageUrlResolve = + | { kind: "url"; url: string } + | { kind: "unknown_platform" } + | { kind: "unavailable" }; + +/** 根据环境配置与行数据拼接取包地址(platform 忽略大小写) */ +export function resolvePackageUrl( + config: Pick< + FrontendConfig, + "package_init_url" | "package_name_mac" | "package_name_oh" + > | null | undefined, + codeBranch: string | null | undefined, + startTime: string | null | undefined, + platform: string | null | undefined, +): PackageUrlResolve { + const template = config?.package_init_url?.trim(); + if (!template) return { kind: "unavailable" }; + + const branch = codeBranch?.trim(); + const batch = startTime?.trim().replace(/_/g, ""); + if (!branch || !batch) return { kind: "unavailable" }; + + const plat = platform?.trim().toLowerCase(); + let packageName: string | undefined; + if (plat === "mac") { + packageName = config?.package_name_mac?.trim(); + } else if (plat === "oh") { + packageName = config?.package_name_oh?.trim(); + } else { + return { kind: "unknown_platform" }; + } + + if (!packageName) return { kind: "unavailable" }; + + const url = template + .replace(/code_branch/g, branch) + .replace(/start_time/g, batch) + .replace(/package_name/g, packageName); + + return { kind: "url", url }; +} diff --git a/spec/02_history_fields_spec.md b/spec/02_history_fields_spec.md index 96e8b5d..08cb4bc 100644 --- a/spec/02_history_fields_spec.md +++ b/spec/02_history_fields_spec.md @@ -48,6 +48,10 @@ |--------|----------| | log_url | 可点击链接(新窗口打开),无值则显示「暂无」 | | pipeline_url | 可点击链接(新窗口打开),无值则显示「暂无」 | +| test_code_repo_url | 可点击链接;值来自环境变量 `TEST_CODE_REPO_URL`,未配置则「暂无」 | +| package_url(取包地址) | 由当前行 `code_branch`、`start_time`、`platform` 与 `.env` 中 `PACKAGE_INIT_URL`、`PACKAGE_NAME_MAC`、`PACKAGE_NAME_OH` 拼接;`platform` 忽略大小写,仅支持 `mac` / `oh`;不支持的平台显示「未知平台」;配置或行字段缺失则「暂无」 | + +**说明:** 测试代码仓与取包模板通过 `GET /api/v1/app/frontend-config` 下发(需登录),详见部署文档。 **说明:** API 响应字段 `owner` 在 Drawer 基本信息区展示为「用例开发责任人」,值为按 `main_module` 与 `ums_module_owner.module` 关联解析后的「姓名+工号」展示串;**模块匹配按小写等价**(`LOWER(main_module)` 与 `LOWER(module)`),避免流水线写入大小写不一致导致无负责人。`owner_history` 本次实现不考虑。 @@ -90,5 +94,7 @@ | reports_url | ✓ | ✓ 基本信息(链接) | — | | log_url | — | ✓ 外部链接 | — | | pipeline_url | — | ✓ 外部链接 | — | +| test_code_repo_url | — | ✓ 外部链接(配置) | — | +| package_url | — | ✓ 外部链接(拼接) | — | | owner_history | — | — | — | | id / created_at / updated_at | — | — | — | diff --git a/spec/09_homepage_batch_status_trend_spec.md b/spec/09_homepage_batch_status_trend_spec.md index 4f7aef4..235b116 100644 --- a/spec/09_homepage_batch_status_trend_spec.md +++ b/spec/09_homepage_batch_status_trend_spec.md @@ -127,7 +127,9 @@ ### 3.7 交互 - **图例**:支持点击图例切换显示/隐藏对应折线系列。 -- **点击数据点**:点击某批次的数据点,跳转到 History 页(`/history`),URL 带筛选参数 `?start_time=批次值`(可附加 `code_branch` 以保持筛选一致性),直接展示该批次的用例明细。 +- **点击数据点**:按所点折线系列区分跳转目标(同一 `start_time` 仅对应一个 `code_branch`,跳转**不带** `code_branch` 参数): + - **失败用例数**折线:跳转 History(`/history`),URL 为 `?start_time=批次值&case_result=failed&case_result=error`,预填「执行结果」为失败与异常,直接展示该批次待跟进用例。 + - **总用例数**折线:跳转 History,URL 为 `?start_time=批次值`,展示该批次全部用例明细。 ### 3.8 数据范围