Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ Thumbs.db
backend/data/*
!backend/data/.gitkeep

# 日报导出表格格式已固化在 backend/constants/oh_daily_export_table.py,不提交本地 Excel 模板
数据导出模板.xlsx
~$数据导出模板.xlsx

# Runtime
.pid
nohup.out
Expand Down
51 changes: 51 additions & 0 deletions backend/api/v1/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
InheritSourceRecordsResponse,
)
from backend.schemas.batch_report import BatchReportResponse
from backend.schemas.oh_daily_export import OhDailyExportResponse
from backend.schemas.one_click_analyze import OneClickAnalyzeRequest, OneClickAnalyzeResponse
from backend.schemas.one_click_bug_notify import (
OneClickBugNotifyRequest,
Expand All @@ -54,8 +55,15 @@
inherit_failure_reason,
)
from backend.services.batch_report_service import get_batch_report
from backend.services.oh_daily_export_service import get_oh_daily_export
from backend.services.one_click_analyze_service import one_click_analyze
from backend.services.one_click_bug_notify_service import one_click_bug_notify
from backend.schemas.history_search_template import HistorySearchTemplateCreate, HistorySearchTemplateItem
from backend.services.history_search_template_service import (
create_search_template,
delete_search_template,
list_search_templates,
)

# 创建一个路由器实例:
# - prefix="/history": 这个路由器下的所有端点都自动加上 /history 前缀
Expand Down Expand Up @@ -167,6 +175,49 @@ async def get_batch_report_endpoint(
return await get_batch_report(db, start_time)


@router.get("/oh-daily-export", response_model=OhDailyExportResponse)
async def get_oh_daily_export_endpoint(
start_time: str = Query(..., description="轮次(批次),与日报弹窗所选批次一致"),
db: AsyncSession = Depends(get_db),
_: dict = Depends(get_current_user),
):
"""OH 平台(白名单 platform)日报:按模板维度统计各主模块分类用例数与通过率,返回可复制 TSV。"""
return await get_oh_daily_export(db, start_time)


@router.get("/search-templates", response_model=List[HistorySearchTemplateItem])
async def get_history_search_templates(
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_user),
):
"""当前登录用户的历史页搜索模板列表(按更新时间倒序)。"""
employee_id = payload.get("sub", "")
return await list_search_templates(db, employee_id)


@router.post("/search-templates", response_model=HistorySearchTemplateItem)
async def post_history_search_template(
body: HistorySearchTemplateCreate,
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_user),
):
"""保存当前筛选条件为搜索模板(每用户最多 10 条)。"""
employee_id = payload.get("sub", "")
return await create_search_template(db, employee_id, body)


@router.delete("/search-templates/{template_id}")
async def delete_history_search_template(
template_id: int,
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_user),
):
"""删除指定搜索模板(仅本人)。"""
employee_id = payload.get("sub", "")
await delete_search_template(db, employee_id, template_id)
return {"success": True, "message": "删除成功"}


# @router.get("") 定义一个 GET 请求的端点
# 完整路径: /api/v1/history(prefix="/history" + "" = "/history")
# response_model=PageResponse[HistoryItem] 的作用:
Expand Down
1 change: 1 addition & 0 deletions backend/constants/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# 业务常量包(无运行时副作用,供 Service 等引用)
51 changes: 51 additions & 0 deletions backend/constants/oh_daily_export_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
"""
OH 平台「日报数据」导出 — 表格格式定义(唯一权威来源)。

仓库不依赖「数据导出模板.xlsx」;若需调整列名、行顺序或主模块归属,只改本文件。

**逻辑布局(与 Excel 粘贴后一致,共 7 行 × 6 列)**

- 第 1 行(表头):`OH_DAILY_EXPORT_HEADER_ROW` — 组件、Total、Success、Fail、NewFail、通过率。
- 第 2~7 行:A 列为组件分类中文名;B~F 列为该分类的 total、success、fail、newFail、通过率(文本)。

**导出形态**:制表符分隔(TSV),每行 6 个字段。

**统计口径**(由 Service 实现,与本文件分工):单批次 + OH 平台白名单 + 本文件列出的 main_module;
按 case_name 去重聚合。**NewFail**:在「小于当前批次且最大的上一批次 B」中该用例为成功,在当前批次 A 中为失败的去重用例数(无满足条件的 B 时为 0)。
"""

from typing import List, Tuple

# ----- 表头:第 1 行 A1~F1 -----
OH_DAILY_EXPORT_HEADER_ROW: Tuple[str, ...] = (
"组件",
"Total",
"Success",
"Fail",
"NewFail",
"通过率",
)

# ----- 数据行:A 列展示名 + 纳入该行的 main_module 精确匹配列表(顺序即导出行序) -----
OH_DAILY_EXPORT_ROWS: List[Tuple[str, Tuple[str, ...]]] = [
("后端 & DFX", ("App", "Terminal", "LOG", "Git", "GIT", "TrustWorkspace")),
("编辑器 & 前端组件", ("Problem", "Hover", "Other", "Settings", "Editor", "Explorer", "FileExplorer")),
("IDE框架", ("Workbench", "Window", "webview", "Scaffold", "Notification")),
(
"插件生态 & 调试",
(
"AIChatView",
"LSP",
"PluginLSP",
"PluginDebug",
"PluginAPI",
"PluginE",
"PluginC",
"Output",
"Debug",
),
),
("智能辅助编写", ("AIFE",)),
("智能辅助阅读", ("AIPI",)),
]
2 changes: 2 additions & 0 deletions backend/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from backend.models.case_offline_type import CaseOfflineType
from backend.models.sys_audit_log import SysAuditLog
from backend.models.report_snapshot import ReportSnapshot
from backend.models.history_search_template import HistorySearchTemplate

__all__ = [
"Base",
Expand All @@ -22,4 +23,5 @@
"CaseOfflineType",
"SysAuditLog",
"ReportSnapshot",
"HistorySearchTemplate",
]
39 changes: 39 additions & 0 deletions backend/models/history_search_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from datetime import datetime
from typing import Optional

from sqlalchemy import BigInteger, DateTime, ForeignKey, Index, String, Text, UniqueConstraint, text
from sqlalchemy.orm import Mapped, mapped_column

from backend.models.base import Base


class HistorySearchTemplate(Base):
__tablename__ = "history_search_template"
__table_args__ = (
UniqueConstraint("employee_id", "name", name="uk_hst_employee_name"),
Index("idx_hst_employee_id", "employee_id"),
{"extend_existing": True},
)

id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
employee_id: Mapped[str] = mapped_column(
String(20),
ForeignKey("ums_email.employee_id", name="fk_hst_employee"),
nullable=False,
comment="工号",
)
name: Mapped[str] = mapped_column(String(100), nullable=False, comment="模板名称")
query_json: Mapped[str] = mapped_column(Text, nullable=False, comment="筛选条件 JSON")
created_at: Mapped[Optional[datetime]] = mapped_column(
DateTime,
nullable=True,
server_default=text("CURRENT_TIMESTAMP"),
comment="创建时间",
)
updated_at: Mapped[Optional[datetime]] = mapped_column(
DateTime,
nullable=True,
server_default=text("CURRENT_TIMESTAMP"),
server_onupdate=text("CURRENT_TIMESTAMP"),
comment="更新时间",
)
21 changes: 21 additions & 0 deletions backend/schemas/history_search_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from datetime import datetime
from typing import Optional

from pydantic import BaseModel, Field

from backend.schemas.history import HistoryQuery


class HistorySearchTemplateCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100, description="模板名称")
query_params: HistoryQuery = Field(..., description="与列表筛选一致的查询参数快照")


class HistorySearchTemplateItem(BaseModel):
id: int
name: str
query_params: HistoryQuery
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None

model_config = {"from_attributes": True}
20 changes: 20 additions & 0 deletions backend/schemas/oh_daily_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# ============================================================
# OH 平台日报数据导出 — Schema
# ============================================================

from typing import List

from pydantic import BaseModel, Field


class OhDailyExportResponse(BaseModel):
"""GET /history/oh-daily-export 响应;export_text 布局见 `backend.constants.oh_daily_export_table`。"""

model_config = {"from_attributes": True}

start_time: str = Field(..., description="轮次(批次)")
platform_filter: List[str] = Field(
...,
description="参与统计的 pipeline_history.platform 取值(白名单)",
)
export_text: str = Field(..., description="制表符分隔文本,可直接粘贴到 Excel")
116 changes: 116 additions & 0 deletions backend/services/history_search_template_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import json
import logging
from typing import List

from fastapi import HTTPException, status
from sqlalchemy import and_, delete, func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from backend.models.history_search_template import HistorySearchTemplate
from backend.schemas.history import HistoryQuery
from backend.schemas.history_search_template import HistorySearchTemplateCreate, HistorySearchTemplateItem

logger = logging.getLogger(__name__)

MAX_TEMPLATES_PER_USER = 10


async def list_search_templates(db: AsyncSession, employee_id: str) -> List[HistorySearchTemplateItem]:
if not employee_id or not str(employee_id).strip():
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="未登录")
eid = str(employee_id).strip()
stmt = (
select(HistorySearchTemplate)
.where(HistorySearchTemplate.employee_id == eid)
.order_by(HistorySearchTemplate.updated_at.desc(), HistorySearchTemplate.id.desc())
)
result = await db.execute(stmt)
rows = result.scalars().all()
items: List[HistorySearchTemplateItem] = []
for row in rows:
try:
raw = json.loads(row.query_json)
q = HistoryQuery.model_validate(raw)
except Exception:
logger.exception("搜索模板 query_json 解析失败 template_id=%s", row.id)
continue
items.append(
HistorySearchTemplateItem(
id=int(row.id),
name=row.name,
query_params=q,
created_at=row.created_at,
updated_at=row.updated_at,
)
)
return items


async def create_search_template(
db: AsyncSession, employee_id: str, body: HistorySearchTemplateCreate
) -> HistorySearchTemplateItem:
if not employee_id or not str(employee_id).strip():
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="未登录")
eid = str(employee_id).strip()
name = body.name.strip()
if not name:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="模板名称不能为空")

cnt_stmt = select(func.count()).select_from(HistorySearchTemplate).where(HistorySearchTemplate.employee_id == eid)
cnt_result = await db.execute(cnt_stmt)
count_val = int(cnt_result.scalar_one() or 0)
if count_val >= MAX_TEMPLATES_PER_USER:
logger.warning("用户搜索模板已达上限 employee_id=%s count=%s", eid, count_val)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="模板已达上限,请先删除模板",
)

payload = body.query_params.model_dump(mode="json")
row = HistorySearchTemplate(
employee_id=eid,
name=name,
query_json=json.dumps(payload, ensure_ascii=False),
)
db.add(row)
try:
await db.commit()
await db.refresh(row)
except IntegrityError:
await db.rollback()
logger.warning("搜索模板名称重复 employee_id=%s name=%s", eid, name)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="模板名称已存在!",
)
except Exception:
await db.rollback()
logger.exception("创建搜索模板失败 employee_id=%s", eid)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="保存失败")

logger.info("已创建历史搜索模板 id=%s employee_id=%s name=%s", row.id, eid, name)
return HistorySearchTemplateItem(
id=int(row.id),
name=row.name,
query_params=body.query_params,
created_at=row.created_at,
updated_at=row.updated_at,
)


async def delete_search_template(db: AsyncSession, employee_id: str, template_id: int) -> None:
if not employee_id or not str(employee_id).strip():
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="未登录")
eid = str(employee_id).strip()
stmt = delete(HistorySearchTemplate).where(
and_(
HistorySearchTemplate.id == template_id,
HistorySearchTemplate.employee_id == eid,
)
)
result = await db.execute(stmt)
if result.rowcount == 0:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模板不存在")
await db.commit()
logger.info("已删除历史搜索模板 id=%s employee_id=%s", template_id, eid)
Loading
Loading