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
10 changes: 5 additions & 5 deletions backend/api/v1/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

# get_db: 数据库会话的提供者函数(定义在 database.py 中)
# FastAPI 的 Depends(get_db) 会自动调用它,创建一个 session 并在请求结束后关闭
from backend.core.database import get_db
from backend.core.database import async_session_on_pinned_connection, get_db
from backend.core.dependencies import get_current_user # 从 JWT 解析当前用户,未登录返回 401
# PageResponse: 通用分页响应模型 { items, total, page, page_size }
from backend.schemas.common import PageResponse
Expand Down Expand Up @@ -127,23 +127,23 @@ async def get_inherit_source_records_endpoint(
@router.post("/inherit-failure-reason", response_model=InheritFailureReasonResponse)
async def post_inherit_failure_reason(
req: InheritFailureReasonRequest,
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_user),
):
"""执行失败原因继承,支持批次维度和用例维度。"""
operator_employee_id = payload.get("sub", "")
return await inherit_failure_reason(db, req, operator_employee_id)
async with async_session_on_pinned_connection() as db:
return await inherit_failure_reason(db, req, operator_employee_id)


@router.post("/one-click-analyze", response_model=OneClickAnalyzeResponse)
async def post_one_click_analyze(
req: OneClickAnalyzeRequest,
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_user),
):
"""一键分析:整批未分析失败/异常用例标记为 bug,跟踪人为用例开发责任人(姓名+工号)。"""
analyzer_employee_id = payload.get("sub", "")
return await one_click_analyze(db, req, analyzer_employee_id)
async with async_session_on_pinned_connection() as db:
return await one_click_analyze(db, req, analyzer_employee_id)


@router.post("/one-click-bug-notify", response_model=OneClickBugNotifyResponse)
Expand Down
19 changes: 19 additions & 0 deletions backend/core/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import logging
import time
from contextlib import asynccontextmanager
from typing import AsyncIterator

# SQLAlchemy 是 Python 最流行的 ORM(对象关系映射)库。
# "async" 前缀表示异步版本 — 不会阻塞其他请求,性能更好。
Expand Down Expand Up @@ -57,6 +59,23 @@ def _after_cursor_execute(conn, cursor, statement, parameters, context, executem
async_session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)


@asynccontextmanager
async def async_session_on_pinned_connection() -> AsyncIterator[AsyncSession]:
"""
在单条池连接上创建 AsyncSession,直至上下文结束再归还连接。

普通 get_db 会话在 commit() 后可能把连接放回池并改用另一条连接执行后续 SQL;
而 MySQL GET_LOCK / RELEASE_LOCK 必须发生在**同一连接**上,否则 RELEASE 无效、
锁会随旧连接滞留在池中,导致后续同名的 GET_LOCK 长时间阻塞直至超时。
"""
async with engine.connect() as conn:
session = AsyncSession(bind=conn, expire_on_commit=False)
try:
yield session
finally:
await session.close()


# 这是 FastAPI 的"依赖注入"函数 — 后面 API 层会通过 Depends(get_db) 来自动调用它
# 它是一个异步生成器(async generator),用 yield 而非 return:
# 1. 请求进来时 → 创建一个数据库会话并交给 API 函数使用
Expand Down
16 changes: 15 additions & 1 deletion frontend/src/pages/history/HistoryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
const [inheritForm] = Form.useForm();
const [inheritModalVisible, setInheritModalVisible] = useState(false);
const [inheritSubmitLoading, setInheritSubmitLoading] = useState(false);
/** 继承提交进行中时用于中止 HTTP,避免点取消后仍占服务端 GET_LOCK */
const inheritAbortRef = useRef<AbortController | null>(null);
const [oneClickLoading, setOneClickLoading] = useState(false);
const [bugNotifyLoading, setBugNotifyLoading] = useState(false);
const [reportModalVisible, setReportModalVisible] = useState(false);
Expand Down Expand Up @@ -611,6 +613,8 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {

const openInheritModal = async () => {
if (!processBtnEnabled) return;
inheritAbortRef.current?.abort();
inheritAbortRef.current = null;
setInheritModalVisible(true);
setInheritSourceRecords([]);
inheritForm.setFieldsValue({
Expand Down Expand Up @@ -665,8 +669,9 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
payload.source_pfr_id = values.source_pfr_id;
payload.history_ids = failedOnlyIds.map(Number);
}
inheritAbortRef.current = new AbortController();
setInheritSubmitLoading(true);
const res = await historyApi.inheritFailureReason(payload);
const res = await historyApi.inheritFailureReason(payload, inheritAbortRef.current.signal);
message.success(res.message || `继承成功,共继承 ${res.inherited_count} 条`);
setInheritModalVisible(false);
inheritForm.resetFields();
Expand All @@ -678,6 +683,11 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
page_size: params.page_size ?? 20,
});
} catch (e: unknown) {
const code = (e as { code?: string })?.code;
const name = (e as { name?: string })?.name;
if (code === "ERR_CANCELED" || name === "CanceledError") {
return;
}
const err = e as { response?: { data?: { detail?: string } }; message?: string };
const msg = err?.response?.data?.detail || err?.message || "继承失败";
if (typeof msg === "string") {
Expand All @@ -689,11 +699,15 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
message.error("继承失败");
}
} finally {
inheritAbortRef.current = null;
setInheritSubmitLoading(false);
}
};

const handleInheritModalCancel = () => {
inheritAbortRef.current?.abort();
inheritAbortRef.current = null;
setInheritSubmitLoading(false);
setInheritModalVisible(false);
inheritForm.resetFields();
};
Expand Down
12 changes: 9 additions & 3 deletions frontend/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,15 @@ export const historyApi = {
if (batch) params.batch = batch;
return request.get("/history/inherit-source-records", { params }) as any;
},
/** 提交失败原因继承(大批量可能较慢,单独 60s 超时) */
inheritFailureReason(data: InheritFailureReasonRequest): Promise<InheritFailureReasonResponse> {
return request.post("/history/inherit-failure-reason", data, { timeout: 60000 }) as any;
/** 提交失败原因继承(大批量可能较慢,单独 60s 超时);signal 用于关闭弹窗时中止请求、尽快释放服务端锁 */
inheritFailureReason(
data: InheritFailureReasonRequest,
signal?: AbortSignal
): Promise<InheritFailureReasonResponse> {
return request.post("/history/inherit-failure-reason", data, {
timeout: 60000,
signal,
}) as any;
},
/** 一键分析:整批未分析失败/异常标记为 bug(锚点解析批次) */
oneClickAnalyze(data: OneClickAnalyzeRequest): Promise<OneClickAnalyzeResponse> {
Expand Down
Loading