2 Commits

Author SHA1 Message Date
yuzhiran d8a7872cd2 fix: interview create returns HTTP 201, frontend only accepted 200
NestJS POST 默认返回 201 Created,前端 startInterview() 和
sendAnswer() 只判断 statusCode === 200 导致成功创建也落入
else 分支显示创建面试失败。改为 >= 200 && < 300。
2026-07-03 15:52:35 +08:00
yuzhiran 59fc35dad9 fix: interview creation show real error msg instead of generic failure
- Backend: wrap AI call in try/catch, throw HttpException(503)
  with msg 'AI 服务暂时不可用,请稍后重试'
- Backend: AllExceptionsFilter preserve original error message
  for non-HttpException errors
- Frontend: handle string/object res.data, show statusCode
  as fallback when message is unavailable
2026-07-03 15:32:22 +08:00
3 changed files with 19 additions and 10 deletions
@@ -16,7 +16,7 @@ export class AllExceptionsFilter implements ExceptionFilter {
const message = exception instanceof HttpException const message = exception instanceof HttpException
? exception.getResponse() ? exception.getResponse()
: '服务器内部错误'; : (exception as Error)?.message || '服务器内部错误';
const errorResponse = { const errorResponse = {
code: status, code: status,
@@ -1,4 +1,4 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common' import { Injectable, HttpException, HttpStatus, Logger } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose' import { InjectModel } from '@nestjs/mongoose'
import { Model } from 'mongoose' import { Model } from 'mongoose'
import { Interview, InterviewDocument } from './interview.schema' import { Interview, InterviewDocument } from './interview.schema'
@@ -11,6 +11,8 @@ import { analyzeSpeech } from '../../common/utils/filler-words'
@Injectable() @Injectable()
export class InterviewService { export class InterviewService {
private readonly logger = new Logger(InterviewService.name)
constructor( constructor(
@InjectModel(Interview.name) private interviewModel: Model<InterviewDocument>, @InjectModel(Interview.name) private interviewModel: Model<InterviewDocument>,
@InjectModel(Progress.name) private progressModel: Model<ProgressDocument>, @InjectModel(Progress.name) private progressModel: Model<ProgressDocument>,
@@ -25,11 +27,17 @@ export class InterviewService {
const cost = await this.quotaService.checkInterview(userId) const cost = await this.quotaService.checkInterview(userId)
// Step 2: AI 生成第一个问题 // Step 2: AI 生成第一个问题
const firstQuestion = await this.aiService.call({ let firstQuestion: string
systemPrompt: `你是一位专业的${position}面试官。请针对校招该岗位提出第一个面试问题,要求具体且有针对性。直接输出问题,不要多余内容。`, try {
userMessage: `请为${position}岗位的校招候选人提出第一个面试问题。`, firstQuestion = await this.aiService.call({
temperature: 0.8, systemPrompt: `你是一位专业的${position}面试官。请针对校招该岗位提出第一个面试问题,要求具体且有针对性。直接输出问题,不要多余内容。`,
}) userMessage: `请为${position}岗位的校招候选人提出第一个面试问题。`,
temperature: 0.8,
})
} catch (e) {
this.logger.error(`AI call failed for interview create: ${(e as Error).message}`)
throw new HttpException('AI 服务暂时不可用,请稍后重试', HttpStatus.SERVICE_UNAVAILABLE)
}
// Step 3: AI 调用成功后,扣除引力值并创建面试记录 // Step 3: AI 调用成功后,扣除引力值并创建面试记录
await this.quotaService.deductInterview(userId, cost) await this.quotaService.deductInterview(userId, cost)
+4 -3
View File
@@ -224,7 +224,7 @@ const startInterview = async () => {
header: { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' }, header: { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' },
data: { position: position.value }, data: { position: position.value },
}) })
if (res.statusCode === 200 && res.data) { if (res.statusCode >= 200 && res.statusCode < 300 && res.data) {
interviewId.value = res.data.id interviewId.value = res.data.id
messages.value = res.data.messages || messages.value messages.value = res.data.messages || messages.value
answeredCount.value = res.data.questionCount || 0 answeredCount.value = res.data.questionCount || 0
@@ -242,7 +242,8 @@ const startInterview = async () => {
} else if (checkAuth(res)) { } else if (checkAuth(res)) {
return // token 过期,已清除并跳转登录 return // token 过期,已清除并跳转登录
} else { } else {
const msg = res.data?.message || '创建面试失败' const errMsg = typeof res.data === 'string' ? res.data : (res.data?.message || '')
const msg = errMsg || `创建面试失败(${res.statusCode}`
messages.value.push({ role: 'ai', content: msg }) messages.value.push({ role: 'ai', content: msg })
} }
} catch { } catch {
@@ -273,7 +274,7 @@ const sendAnswer = async () => {
header: { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' }, header: { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' },
data: avatarMode.value ? { answer, avatar: true } : { answer }, data: avatarMode.value ? { answer, avatar: true } : { answer },
}) })
if (res.statusCode === 200 && res.data?.messages) { if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.messages) {
const aiMsg = res.data.messages.find(m => m.role === 'ai') const aiMsg = res.data.messages.find(m => m.role === 'ai')
// Only push AI messages from response to avoid duplicating the user message already added above // Only push AI messages from response to avoid duplicating the user message already added above
const newAiMessages = res.data.messages.filter(m => m.role === 'ai') const newAiMessages = res.data.messages.filter(m => m.role === 'ai')