diff --git a/backend/app/services/course_service.py b/backend/app/services/course_service.py index b0a825a..ba175f4 100644 --- a/backend/app/services/course_service.py +++ b/backend/app/services/course_service.py @@ -195,8 +195,12 @@ class CourseService(BaseService[Course]): and_(Course.name == course_in.name, Course.is_deleted == False) ) ) - if existing.scalar_one_or_none(): - raise ConflictError(f"课程名称 '{course_in.name}' 已存在") + existing_course = existing.scalar_one_or_none() + if existing_course: + raise ConflictError( + f"课程名称 '{course_in.name}' 已存在", + detail={"existing_id": existing_course.id, "existing_name": existing_course.name} + ) # 创建课程 course_data = course_in.model_dump() @@ -260,8 +264,12 @@ class CourseService(BaseService[Course]): ) ) ) - if existing.scalar_one_or_none(): - raise ConflictError(f"课程名称 '{course_in.name}' 已存在") + existing_course = existing.scalar_one_or_none() + if existing_course: + raise ConflictError( + f"课程名称 '{course_in.name}' 已存在", + detail={"existing_id": existing_course.id, "existing_name": existing_course.name} + ) # 记录状态变更 old_status = course.status @@ -800,8 +808,12 @@ class GrowthPathService(BaseService[GrowthPath]): and_(GrowthPath.name == path_in.name, GrowthPath.is_deleted == False) ) ) - if existing.scalar_one_or_none(): - raise ConflictError(f"成长路径名称 '{path_in.name}' 已存在") + existing_path = existing.scalar_one_or_none() + if existing_path: + raise ConflictError( + f"成长路径名称 '{path_in.name}' 已存在", + detail={"existing_id": existing_path.id, "existing_name": existing_path.name, "type": "growth_path"} + ) # 验证课程是否存在 if path_in.courses: diff --git a/frontend/src/utils/errorHandler.ts b/frontend/src/utils/errorHandler.ts index 4b2a5c8..44f301a 100644 --- a/frontend/src/utils/errorHandler.ts +++ b/frontend/src/utils/errorHandler.ts @@ -365,6 +365,16 @@ export const handleHttpError = (error: any): ErrorInfo => { type = ErrorType.API message = '请求的资源不存在' break + case 409: + type = ErrorType.VALIDATION + level = ErrorLevel.MEDIUM + // 处理冲突错误,通常是资源重复 + if (typeof data?.detail === 'object') { + message = data.detail.message || '资源已存在' + } else { + message = data?.detail || data?.message || '资源已存在' + } + break case 429: type = ErrorType.API level = ErrorLevel.HIGH @@ -487,4 +497,48 @@ export const handleWebSocketError = (error: Event, url: string): void => { errorHandler.handleError(errorInfo) } +// 冲突错误详情接口 +export interface ConflictDetail { + existing_id?: number + existing_name?: string + type?: string // 'course', 'growth_path', 'user' 等 +} + +// 检查是否为冲突错误 +export const isConflictError = (error: any): boolean => { + const status = error?.status || error?.response?.status + return status === 409 +} + +// 获取冲突错误详情 +export const getConflictDetail = (error: any): ConflictDetail | null => { + if (!isConflictError(error)) return null + + // 尝试从不同位置获取详情 + const detail = error?.detail?.detail + || error?.response?.data?.detail?.detail + || error?.response?.data?.detail + || null + + if (detail && typeof detail === 'object') { + return { + existing_id: detail.existing_id, + existing_name: detail.existing_name, + type: detail.type + } + } + + return null +} + +// 获取冲突错误消息 +export const getConflictMessage = (error: any): string => { + const detail = error?.detail?.message + || error?.response?.data?.detail?.message + || error?.message + || '资源已存在' + + return typeof detail === 'string' ? detail : '资源已存在' +} + // 导出类型定义 diff --git a/frontend/src/views/admin/position-management.vue b/frontend/src/views/admin/position-management.vue index 791d4de..f804ad6 100644 --- a/frontend/src/views/admin/position-management.vue +++ b/frontend/src/views/admin/position-management.vue @@ -640,9 +640,14 @@ const handleSubmit = async () => { loadPositionList() } } - } catch (error) { + } catch (error: any) { console.error('提交失败:', error) - ElMessage.error(isEdit.value ? '岗位编辑失败' : '岗位创建失败') + // 提取详细错误信息 + const message = error?.detail?.message + || error?.response?.data?.detail?.message + || error?.message + || (isEdit.value ? '岗位编辑失败' : '岗位创建失败') + ElMessage.error(message) } finally { submitLoading.value = false } diff --git a/frontend/src/views/admin/user-management.vue b/frontend/src/views/admin/user-management.vue index 628886a..1d07d73 100644 --- a/frontend/src/views/admin/user-management.vue +++ b/frontend/src/views/admin/user-management.vue @@ -1026,9 +1026,18 @@ const handleEdit = async () => { ElMessage.success('用户信息修改成功') loadUserList() } - } catch (error) { + } catch (error: any) { console.error('编辑用户失败:', error) - ElMessage.error('编辑用户失败') + // 提取详细错误信息 + let errorMsg = '编辑用户失败' + if (error?.detail?.message) { + errorMsg = error.detail.message + } else if (error?.response?.data?.detail?.message) { + errorMsg = error.response.data.detail.message + } else if (error.message) { + errorMsg = error.message + } + ElMessage.error(errorMsg) } finally { editLoading.value = false } diff --git a/frontend/src/views/manager/edit-course.vue b/frontend/src/views/manager/edit-course.vue index fe5ae86..115209f 100644 --- a/frontend/src/views/manager/edit-course.vue +++ b/frontend/src/views/manager/edit-course.vue @@ -988,7 +988,33 @@ const handleSave = async () => { router.push('/manager/course-management') } catch (error: any) { console.error('保存课程失败:', error) - ElMessage.error(error.message || '保存课程失败') + + // 处理课程名重复的409冲突错误 + const status = error?.status || error?.response?.status + const detail = error?.detail?.detail || error?.response?.data?.detail?.detail + + if (status === 409 && detail?.existing_id) { + // 课程名重复,提供跳转选项 + ElMessageBox.confirm( + `课程名称"${courseForm.name}"已存在,您可以点击下方按钮查看已有课程。`, + '课程名称重复', + { + confirmButtonText: '查看已有课程', + cancelButtonText: '修改名称', + type: 'warning' + } + ).then(() => { + // 跳转到已存在的课程 + router.push(`/manager/edit-course/${detail.existing_id}`) + }).catch(() => { + // 用户选择修改名称,聚焦到名称输入框 + activeTab.value = 'basic' + }) + } else { + // 其他错误 + const message = error?.detail?.message || error?.message || '保存课程失败' + ElMessage.error(message) + } } }