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, @InjectModel(CompanyBank.name) private companyBankModel: Model, 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 })) } }