v4.3 安全修复+代码质量+测试体系+护城河验证

## 安全修复 (5项)
- CRITICAL JWT 硬编码 fallback(jwt.strategy / app.module / user.module)
- HIGH seed_admin.js MongoDB 凭据泄漏
- MEDIUM 邮箱验证码泄漏
- MEDIUM 支付订单查询 IDOR
- MEDIUM 管理后台 NoSQL 注入

## 代码质量 (14处)
- console.log→Logger(user.service.ts)
- as any 类型化(11处跨7个文件)
- Schema 联合类型修复(progress.schema)
- Module 依赖缺失修复(progress.module)

## 测试体系 (61项)
- 后端单元测试 Jest(43项):BenchmarkService/UserService/PaymentController
- 后端集成测试 Supertest(11项):API 认证/支付/进度/管理
- 前端单元测试 Vitest(7项):配置文件/API端点
- 浏览器自动化 Playwright(7项):API smoke test
- 覆盖率报告 + e2e 配置

## 护城河 P0-P5 启动验证通过 + 编译通过
This commit is contained in:
yuzhiran
2026-06-11 10:27:35 +08:00
parent 9276ab9028
commit e6b79ddb21
39 changed files with 4576 additions and 246 deletions
@@ -1,17 +1,21 @@
import { Controller, Post, Get, Body, Param, UseGuards } from '@nestjs/common'
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 { 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()
@@ -39,6 +43,11 @@ export class ContributionController {
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({
@@ -69,7 +78,6 @@ export class ContributionController {
tags: body.tags || [],
})
} else {
// Increment frequency
const existing = bank.questions.find(eq => eq.content === q)
if (existing) existing.frequency += 1
}
@@ -79,13 +87,70 @@ export class ContributionController {
}
return {
id: (contribution as any)._id.toString(),
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()