Files
zhiyin/backend/src/modules/contribution/contribution.controller.ts
T
2026-06-16 13:18:36 +08:00

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 }))
}
}