feat: 添加请求验证错误详细日志
All checks were successful
continuous-integration/drone/push Build is passing

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yuliang_guo
2026-01-31 10:03:54 +08:00
parent fadeaadd65
commit 0b7c07eb7f
11 changed files with 2282 additions and 2267 deletions

View File

@@ -2,10 +2,11 @@
import logging import logging
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.exceptions import RequestValidationError
import json import json
import os import os
@@ -131,6 +132,20 @@ os.makedirs(upload_path, exist_ok=True)
app.mount("/static/uploads", StaticFiles(directory=upload_path), name="uploads") app.mount("/static/uploads", StaticFiles(directory=upload_path), name="uploads")
# 请求验证错误处理 (422)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""处理请求验证错误,记录详细日志"""
logger.error(f"请求验证错误 [{request.method} {request.url.path}]: {exc.errors()}")
return JSONResponse(
status_code=422,
content={
"detail": exc.errors(),
"body": exc.body if hasattr(exc, 'body') else None,
},
)
# 全局异常处理 # 全局异常处理
@app.exception_handler(Exception) @app.exception_handler(Exception)
async def global_exception_handler(request, exc): async def global_exception_handler(request, exc):

View File

@@ -2,9 +2,9 @@
<div class="growth-path-management-container"> <div class="growth-path-management-container">
<!-- 路径列表视图 --> <!-- 路径列表视图 -->
<template v-if="!editingPath"> <template v-if="!editingPath">
<div class="page-header"> <div class="page-header">
<h1 class="page-title">成长路径管理</h1> <h1 class="page-title">成长路径管理</h1>
<div class="header-actions"> <div class="header-actions">
<el-select <el-select
v-model="filters.position_id" v-model="filters.position_id"
placeholder="筛选岗位" placeholder="筛选岗位"
@@ -19,7 +19,7 @@
:label="pos.name" :label="pos.name"
:value="pos.id" :value="pos.id"
/> />
</el-select> </el-select>
<el-select <el-select
v-model="filters.is_active" v-model="filters.is_active"
placeholder="状态" placeholder="状态"
@@ -34,9 +34,9 @@
<el-button type="primary" @click="handleCreatePath"> <el-button type="primary" @click="handleCreatePath">
<el-icon class="el-icon--left"><Plus /></el-icon> <el-icon class="el-icon--left"><Plus /></el-icon>
新建路径 新建路径
</el-button> </el-button>
</div>
</div> </div>
</div>
<!-- 路径列表 --> <!-- 路径列表 -->
<div class="path-list-card"> <div class="path-list-card">
@@ -157,7 +157,7 @@
multiple multiple
collapse-tags collapse-tags
collapse-tags-tooltip collapse-tags-tooltip
clearable clearable
style="width: 100%" style="width: 100%"
> >
<el-option <el-option
@@ -170,7 +170,7 @@
<el-button <el-button
link link
type="primary" type="primary"
size="small" size="small"
@click="handleSelectAllPositions" @click="handleSelectAllPositions"
class="select-all-btn" class="select-all-btn"
> >
@@ -228,11 +228,11 @@
placeholder="阶段名称" placeholder="阶段名称"
size="small" size="small"
style="width: 100px" style="width: 100px"
> >
<template #prefix> <template #prefix>
<span class="stage-order">{{ index + 1 }}</span> <span class="stage-order">{{ index + 1 }}</span>
</template> </template>
</el-input> </el-input>
<el-button <el-button
link link
type="danger" type="danger"
@@ -244,7 +244,7 @@
</el-button> </el-button>
</div> </div>
</div> </div>
</div> </div>
<!-- 路径统计 --> <!-- 路径统计 -->
<div class="top-section stats-section"> <div class="top-section stats-section">
@@ -253,7 +253,7 @@
<div class="stat-box"> <div class="stat-box">
<span class="stat-value">{{ editingPath.nodes?.length || 0 }}</span> <span class="stat-value">{{ editingPath.nodes?.length || 0 }}</span>
<span class="stat-label">课程总数</span> <span class="stat-label">课程总数</span>
</div> </div>
<div class="stat-box"> <div class="stat-box">
<span class="stat-value">{{ requiredCount }}</span> <span class="stat-value">{{ requiredCount }}</span>
<span class="stat-label">必修课程</span> <span class="stat-label">必修课程</span>
@@ -261,10 +261,10 @@
<div class="stat-box"> <div class="stat-box">
<span class="stat-value">{{ totalDuration }}</span> <span class="stat-value">{{ totalDuration }}</span>
<span class="stat-label">总学时(h)</span> <span class="stat-label">总学时(h)</span>
</div>
</div>
</div>
</div> </div>
</div>
</div>
</div>
<!-- 下方左右分栏 --> <!-- 下方左右分栏 -->
<div class="editor-bottom"> <div class="editor-bottom">
@@ -303,11 +303,11 @@
<div class="course-list" v-loading="coursesLoading"> <div class="course-list" v-loading="coursesLoading">
<div <div
v-for="course in filteredCourses" v-for="course in filteredCourses"
:key="course.id" :key="course.id"
class="course-item" class="course-item"
:class="{ 'is-added': isNodeAdded(course.id) }" :class="{ 'is-added': isNodeAdded(course.id) }"
draggable="true" draggable="true"
@dragstart="handleDragStart($event, course)" @dragstart="handleDragStart($event, course)"
@click="handleAddCourse(course)" @click="handleAddCourse(course)"
> >
<div class="course-info"> <div class="course-info">
@@ -320,21 +320,21 @@
<el-icon v-else><Plus /></el-icon> <el-icon v-else><Plus /></el-icon>
</div> </div>
<el-empty v-if="filteredCourses.length === 0" description="暂无课程" :image-size="50" /> <el-empty v-if="filteredCourses.length === 0" description="暂无课程" :image-size="50" />
</div> </div>
</div> </div>
<!-- 右侧 2/3已选课程配置 --> <!-- 右侧 2/3已选课程配置 -->
<div class="selected-courses-panel"> <div class="selected-courses-panel">
<div class="panel-header"> <div class="panel-header">
<span>已选课程配置</span> <span>已选课程配置</span>
<el-tag size="small" type="success">{{ editingPath.nodes?.length || 0 }} </el-tag> <el-tag size="small" type="success">{{ editingPath.nodes?.length || 0 }} </el-tag>
</div> </div>
<div <div
class="selected-content" class="selected-content"
:class="{ 'is-dragging-over': isDraggingOver }" :class="{ 'is-dragging-over': isDraggingOver }"
@dragover.prevent="handleDragOver" @dragover.prevent="handleDragOver"
@dragleave="handleDragLeave" @dragleave="handleDragLeave"
@drop="handleDrop" @drop="handleDrop"
> >
<template v-if="editingPath.nodes && editingPath.nodes.length > 0"> <template v-if="editingPath.nodes && editingPath.nodes.length > 0">
<div <div
@@ -347,7 +347,7 @@
<el-tag size="small" type="info"> <el-tag size="small" type="info">
{{ getStageNodes(stage.name).length }} {{ getStageNodes(stage.name).length }}
</el-tag> </el-tag>
</div> </div>
<div class="stage-nodes"> <div class="stage-nodes">
<div <div
v-for="(node, nodeIndex) in getStageNodes(stage.name)" v-for="(node, nodeIndex) in getStageNodes(stage.name)"
@@ -356,7 +356,7 @@
> >
<div class="node-drag-handle"> <div class="node-drag-handle">
<el-icon><Rank /></el-icon> <el-icon><Rank /></el-icon>
</div> </div>
<div class="node-content"> <div class="node-content">
<span class="node-title">{{ node.title }}</span> <span class="node-title">{{ node.title }}</span>
<div class="node-meta"> <div class="node-meta">
@@ -367,10 +367,10 @@
style="cursor: pointer" style="cursor: pointer"
> >
{{ node.is_required ? '必修' : '选修' }} {{ node.is_required ? '必修' : '选修' }}
</el-tag> </el-tag>
<span>{{ node.estimated_days || 1 }}</span> <span>{{ node.estimated_days || 1 }}</span>
</div> </div>
</div> </div>
<div class="node-actions"> <div class="node-actions">
<el-select <el-select
v-model="node.stage_name" v-model="node.stage_name"
@@ -393,16 +393,16 @@
> >
<el-icon><Delete /></el-icon> <el-icon><Delete /></el-icon>
</el-button> </el-button>
</div> </div>
</div> </div>
<div <div
v-if="getStageNodes(stage.name).length === 0" v-if="getStageNodes(stage.name).length === 0"
class="stage-empty" class="stage-empty"
> >
拖拽或点击课程添加到此阶段 拖拽或点击课程添加到此阶段
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<el-empty v-else description="请从左侧添加课程" :image-size="60"> <el-empty v-else description="请从左侧添加课程" :image-size="60">
<template #description> <template #description>
@@ -413,7 +413,7 @@
</div> </div>
</div> </div>
</div> </div>
</template> </template>
</div> </div>
</template> </template>
@@ -768,10 +768,10 @@ const handleDeletePath = async (row: GrowthPathListItem) => {
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
`确定要删除路径"${row.name}"吗?此操作不可恢复。`, `确定要删除路径"${row.name}"吗?此操作不可恢复。`,
'删除确认', '删除确认',
{ {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
} }
) )
@@ -1040,9 +1040,9 @@ onMounted(() => {
flex-shrink: 0; flex-shrink: 0;
.top-section { .top-section {
background: #fff; background: #fff;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px; padding: 16px;
h3 { h3 {
@@ -1059,7 +1059,7 @@ onMounted(() => {
flex: 2; flex: 2;
.form-row { .form-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
margin-bottom: 8px; margin-bottom: 8px;
@@ -1113,9 +1113,9 @@ onMounted(() => {
align-items: center; align-items: center;
margin-bottom: 12px; margin-bottom: 12px;
padding-bottom: 8px; padding-bottom: 8px;
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5;
h3 { h3 {
margin: 0; margin: 0;
padding: 0; padding: 0;
border: none; border: none;
@@ -1134,7 +1134,7 @@ onMounted(() => {
.stage-order { .stage-order {
color: #667eea; color: #667eea;
font-weight: 600; font-weight: 600;
font-size: 12px; font-size: 12px;
} }
} }
@@ -1150,7 +1150,7 @@ onMounted(() => {
gap: 8px; gap: 8px;
.stat-box { .stat-box {
flex: 1; flex: 1;
text-align: center; text-align: center;
padding: 8px 4px; padding: 8px 4px;
background: #f5f7fa; background: #f5f7fa;
@@ -1188,8 +1188,8 @@ onMounted(() => {
background: #fff; background: #fff;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.panel-header { .panel-header {
display: flex; display: flex;
@@ -1254,7 +1254,7 @@ onMounted(() => {
.course-name { .course-name {
display: block; display: block;
font-size: 13px; font-size: 13px;
color: #333; color: #333;
font-weight: 500; font-weight: 500;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -1266,34 +1266,34 @@ onMounted(() => {
font-size: 11px; font-size: 11px;
color: #909399; color: #909399;
margin-top: 2px; margin-top: 2px;
}
} }
} }
} }
} }
}
// 右侧已选课程 2/3 // 右侧已选课程 2/3
.selected-courses-panel { .selected-courses-panel {
flex: 1; flex: 1;
background: #fff; background: #fff;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.panel-header { .panel-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 12px 16px; padding: 12px 16px;
background: #fafafa; background: #fafafa;
border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0;
font-weight: 500; font-weight: 500;
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5;
} }
.selected-content { .selected-content {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 12px; padding: 12px;
border: 2px dashed transparent; border: 2px dashed transparent;
@@ -1341,7 +1341,7 @@ onMounted(() => {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 10px; padding: 10px;
background: #fff; background: #fff;
border: 1px solid #ebeef5; border: 1px solid #ebeef5;
border-radius: 6px; border-radius: 6px;
margin-bottom: 8px; margin-bottom: 8px;
@@ -1359,28 +1359,28 @@ onMounted(() => {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
.node-title { .node-title {
display: block; display: block;
font-weight: 500; font-weight: 500;
color: #333; color: #333;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.node-meta { .node-meta {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-top: 4px; margin-top: 4px;
font-size: 12px; font-size: 12px;
color: #909399; color: #909399;
} }
} }
.node-actions { .node-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
} }
@@ -1390,12 +1390,12 @@ onMounted(() => {
padding: 16px; padding: 16px;
color: #909399; color: #909399;
font-size: 13px; font-size: 13px;
}
} }
} }
} }
} }
} }
}
@keyframes pulse { @keyframes pulse {
0%, 100% { opacity: 1; } 0%, 100% { opacity: 1; }
@@ -1424,10 +1424,10 @@ onMounted(() => {
} }
.editor-bottom { .editor-bottom {
flex-direction: column; flex-direction: column;
.course-library-panel { .course-library-panel {
width: 100%; width: 100%;
max-height: 300px; max-height: 300px;
} }
} }