205 lines
6.8 KiB
TypeScript
205 lines
6.8 KiB
TypeScript
import { Controller, Post, Get, Body, Param, UseGuards, Logger } from '@nestjs/common'
|
|
import { InjectModel } from '@nestjs/mongoose'
|
|
import { Model } from 'mongoose'
|
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'
|
|
import { Public } from '../../common/decorators/public.decorator'
|
|
import { CurrentUser } from '../../common/decorators/current-user.decorator'
|
|
import { AiService } from '../ai/ai.service'
|
|
import { Contribution, ContributionDocument } from '../schemas/contribution.schema'
|
|
import { CompanyBank, CompanyBankDocument } from '../schemas/company-bank.schema'
|
|
|
|
@Controller('contribution')
|
|
@UseGuards(JwtAuthGuard)
|
|
export class ContributionController {
|
|
private readonly logger = new Logger(ContributionController.name)
|
|
|
|
constructor(
|
|
@InjectModel(Contribution.name) private contributionModel: Model<ContributionDocument>,
|
|
@InjectModel(CompanyBank.name) private companyBankModel: Model<CompanyBankDocument>,
|
|
private readonly aiService: AiService,
|
|
) {}
|
|
|
|
@Post()
|
|
async create(
|
|
@CurrentUser('userId') userId: string,
|
|
@Body() body: {
|
|
interviewId: string
|
|
company: string
|
|
position: string
|
|
rounds?: string
|
|
questions?: string[]
|
|
experience?: string
|
|
tags?: string[]
|
|
},
|
|
) {
|
|
const contribution = await this.contributionModel.create({
|
|
userId,
|
|
interviewId: body.interviewId,
|
|
company: body.company,
|
|
position: body.position,
|
|
rounds: body.rounds || '',
|
|
questions: body.questions || [],
|
|
experience: body.experience || '',
|
|
tags: body.tags || [],
|
|
verified: false,
|
|
})
|
|
|
|
// Async AI processing (non-blocking)
|
|
this.aiProcessContribution(contribution, body).catch(e => {
|
|
this.logger.error(`AI processing failed for contribution ${contribution._id}: ${e.message}`)
|
|
})
|
|
|
|
// Update company bank
|
|
if (body.questions && body.questions.length > 0) {
|
|
let bank = await this.companyBankModel.findOne({
|
|
company: body.company,
|
|
position: body.position,
|
|
}).exec()
|
|
|
|
if (!bank) {
|
|
bank = await this.companyBankModel.create({
|
|
company: body.company,
|
|
position: body.position,
|
|
questions: [],
|
|
contributionCount: 0,
|
|
viewCount: 0,
|
|
})
|
|
}
|
|
|
|
// Add new questions (avoid duplicates)
|
|
for (const q of body.questions) {
|
|
const exists = bank.questions.some(eq => eq.content === q)
|
|
if (!exists) {
|
|
bank.questions.push({
|
|
content: q,
|
|
type: 'general',
|
|
referenceAnswer: '',
|
|
difficulty: 'medium',
|
|
frequency: 1,
|
|
tags: body.tags || [],
|
|
})
|
|
} else {
|
|
const existing = bank.questions.find(eq => eq.content === q)
|
|
if (existing) existing.frequency += 1
|
|
}
|
|
}
|
|
bank.contributionCount += 1
|
|
await bank.save()
|
|
}
|
|
|
|
return {
|
|
id: contribution._id.toString(),
|
|
company: contribution.company,
|
|
position: contribution.position,
|
|
message: '感谢你的分享!你的面经将帮助更多同学准备面试',
|
|
}
|
|
}
|
|
|
|
private async aiProcessContribution(contribution: ContributionDocument, body: any) {
|
|
const prompt = `你是一个面试题分析专家。请分析以下面经内容,返回 JSON(不要 Markdown 包裹):
|
|
|
|
公司: ${body.company}
|
|
岗位: ${body.position}
|
|
轮次: ${body.rounds || '未说明'}
|
|
面试题: ${(body.questions || []).join('\n')}
|
|
面试经验: ${body.experience || ''}
|
|
用户标签: ${(body.tags || []).join(', ')}
|
|
|
|
请返回严格 JSON:
|
|
{
|
|
"structuredQuestions": [
|
|
{
|
|
"content": "原题",
|
|
"type": "技术题|行为题|场景题|算法题|系统设计题|HR题",
|
|
"difficulty": "easy|medium|hard",
|
|
"referenceAnswer": "参考答案",
|
|
"tags": ["标签1", "标签2"]
|
|
}
|
|
],
|
|
"aiSummary": "面试总结,包含难度评估、重点方向、准备建议(50字以内)"
|
|
}`
|
|
|
|
const result = await this.aiService.call({ systemPrompt: prompt, userMessage: '请分析以上面经', maxTokens: 3000 })
|
|
const parsed = JSON.parse(result.replace(/```json\s*|\s*```/g, '').trim())
|
|
|
|
await this.contributionModel.findByIdAndUpdate(contribution._id, {
|
|
$set: {
|
|
aiProcessed: true,
|
|
structuredQuestions: parsed.structuredQuestions || [],
|
|
aiSummary: parsed.aiSummary || '',
|
|
},
|
|
}).exec()
|
|
|
|
// Update company bank with structured data
|
|
if (parsed.structuredQuestions?.length > 0) {
|
|
const bank = await this.companyBankModel.findOne({
|
|
company: body.company,
|
|
position: body.position,
|
|
}).exec()
|
|
|
|
if (bank) {
|
|
for (const sq of parsed.structuredQuestions) {
|
|
const existing = bank.questions.find(eq => eq.content === sq.content)
|
|
if (existing) {
|
|
existing.type = sq.type || existing.type
|
|
existing.difficulty = sq.difficulty || existing.difficulty
|
|
existing.referenceAnswer = sq.referenceAnswer || existing.referenceAnswer
|
|
if (sq.tags) existing.tags = [...new Set([...existing.tags, ...sq.tags])]
|
|
}
|
|
}
|
|
await bank.save()
|
|
}
|
|
}
|
|
}
|
|
|
|
@Get('company/:company/position/:position')
|
|
async getBank(@Param('company') company: string, @Param('position') position: string) {
|
|
const bank = await this.companyBankModel.findOne({ company, position }).exec()
|
|
if (!bank) return { company, position, questions: [], contributionCount: 0 }
|
|
|
|
bank.viewCount += 1
|
|
await bank.save()
|
|
|
|
return {
|
|
company: bank.company,
|
|
position: bank.position,
|
|
questions: bank.questions.sort((a, b) => b.frequency - a.frequency),
|
|
contributionCount: bank.contributionCount,
|
|
viewCount: bank.viewCount,
|
|
}
|
|
}
|
|
|
|
@Get('company/:company')
|
|
async getCompanyBanks(@Param('company') company: string) {
|
|
const banks = await this.companyBankModel.find({ company }).exec()
|
|
return banks.map(b => ({
|
|
position: b.position,
|
|
questionCount: b.questions.length,
|
|
contributionCount: b.contributionCount,
|
|
}))
|
|
}
|
|
|
|
@Get('my')
|
|
async getMyContributions(@CurrentUser('userId') userId: string) {
|
|
return this.contributionModel
|
|
.find({ userId })
|
|
.sort({ createdAt: -1 })
|
|
.select('company position rounds experience createdAt')
|
|
.exec()
|
|
}
|
|
|
|
@Public()
|
|
@Get('companies/hot')
|
|
async getHotCompanies() {
|
|
const banks = await this.companyBankModel.aggregate([
|
|
{ $group: { _id: '$company', positionCount: { $sum: 1 }, totalContributions: { $sum: '$contributionCount' } } },
|
|
{ $sort: { totalContributions: -1, positionCount: -1 } },
|
|
{ $project: { _id: 0, name: '$_id', positionCount: 1 } },
|
|
]).exec()
|
|
|
|
if (banks.length > 0) return banks
|
|
|
|
const DEFAULT_COMPANIES = ['腾讯', '字节跳动', '阿里巴巴', '美团', '百度', '京东', '网易', '小红书']
|
|
return DEFAULT_COMPANIES.map((name, i) => ({ name, positionCount: 0, sort: i }))
|
|
}
|
|
} |