a5c4bcb821
- backend: career-advice module with analyze/chat/positions endpoints - frontend: career.vue page with profile form, AI advice, recommendation cards - config/api/pages/user.vue: full integration into existing flow - docs: PROJECT-STATUS v4.5, FEATURE-LIST v4.3, ROADMAP v4.3 - AGENTS.md: updated module count and career link paths
164 lines
4.8 KiB
TypeScript
164 lines
4.8 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common'
|
|
import { InjectModel } from '@nestjs/mongoose'
|
|
import { Model } from 'mongoose'
|
|
import { AiService } from '../ai/ai.service'
|
|
import { HotPosition } from '../positions/positions.schema'
|
|
|
|
export interface CareerProfile {
|
|
major: string
|
|
grade?: string
|
|
interests?: string
|
|
gpa?: string
|
|
goal?: string
|
|
}
|
|
|
|
export interface CareerPath {
|
|
name: string
|
|
reason: string
|
|
matchScore: number
|
|
salary?: string
|
|
}
|
|
|
|
export interface ChatMessage {
|
|
role: 'user' | 'assistant'
|
|
content: string
|
|
}
|
|
|
|
@Injectable()
|
|
export class CareerAdviceService {
|
|
private readonly logger = new Logger(CareerAdviceService.name)
|
|
|
|
constructor(
|
|
@InjectModel(HotPosition.name) private positionModel: Model<HotPosition>,
|
|
private aiService: AiService,
|
|
) {}
|
|
|
|
/**
|
|
* Analyze user profile and return career advice with position recommendations.
|
|
*/
|
|
async analyze(profile: CareerProfile) {
|
|
const positions = await this.positionModel.find({ active: true })
|
|
.sort({ sort: 1 })
|
|
.lean()
|
|
.exec()
|
|
|
|
const positionsContext = positions.map(p =>
|
|
`- ${p.name}${p.salary ? ` (薪资: ${p.salary})` : ''}${p.company ? ` - ${p.company}` : ''}`
|
|
).join('\n')
|
|
|
|
const systemPrompt = `你是一位资深的中国大学生职业规划顾问。你的任务是根据学生的专业背景和个人情况,给出个性化的择业建议。
|
|
|
|
你的建议需要涵盖:
|
|
1. 该专业的典型职业方向和发展路径
|
|
2. "考研vs就业vs考公"的决策分析(基于学生具体情况)
|
|
3. 当前就业市场的真实形势(如AI对各行业的影响)
|
|
4. 具体可行动的建议(实习、技能提升等)
|
|
|
|
你需要从以下可用岗位中,推荐最匹配该学生的3-5个方向:
|
|
|
|
${positionsContext}
|
|
|
|
注意:
|
|
- 你的回答要诚恳务实,不要画大饼
|
|
- 指出每个选择的利弊和风险
|
|
- 结合AI时代对各行业的影响给出建议
|
|
- 最后以JSON格式输出推荐岗位列表
|
|
|
|
回复格式:
|
|
先输出一段详细的个性化分析建议(中文,300-500字)。
|
|
然后在最后单独一行输出JSON数组(不要任何其他内容):
|
|
---JSON---
|
|
[{"name":"岗位名","reason":"推荐理由简要说明","matchScore":85,"salary":"薪资范围"},...]
|
|
---JSON---`
|
|
|
|
const userMessage = this.buildProfileMessage(profile)
|
|
const rawReply = await this.aiService.call({
|
|
systemPrompt,
|
|
userMessage,
|
|
temperature: 0.7,
|
|
maxTokens: 2048,
|
|
})
|
|
|
|
return this.parseResponse(rawReply, positions)
|
|
}
|
|
|
|
/**
|
|
* Continue a chat conversation about career choices.
|
|
*/
|
|
async chat(message: string, history: ChatMessage[]) {
|
|
const positions = await this.positionModel.find({ active: true })
|
|
.sort({ sort: 1 })
|
|
.lean()
|
|
.exec()
|
|
|
|
const positionsContext = positions.map(p =>
|
|
`- ${p.name}${p.salary ? ` (薪资: ${p.salary})` : ''}`
|
|
).join('\n')
|
|
|
|
const systemPrompt = `你是一位资深的中国大学生职业规划顾问。继续与学生的对话,回答他们的择业相关问题。
|
|
|
|
可推荐的岗位列表:
|
|
${positionsContext}
|
|
|
|
注意:
|
|
- 回答诚恳务实,结合AI时代背景
|
|
- 给出具体可操作的建议
|
|
- 如果学生提到具体岗位,可以从推荐列表中选择匹配的
|
|
- 保持对话亲切自然,用中文回复`
|
|
|
|
const historyMessages = history.map(h =>
|
|
`${h.role === 'user' ? '学生' : '顾问'}: ${h.content}`
|
|
).join('\n')
|
|
|
|
const userMessage = `对话历史:\n${historyMessages}\n\n学生最新提问: ${message}`
|
|
|
|
const reply = await this.aiService.call({
|
|
systemPrompt,
|
|
userMessage,
|
|
temperature: 0.7,
|
|
maxTokens: 1024,
|
|
})
|
|
|
|
return { reply }
|
|
}
|
|
|
|
private buildProfileMessage(profile: CareerProfile): string {
|
|
const parts: string[] = [`学生专业: ${profile.major}`]
|
|
if (profile.grade) parts.push(`年级: ${profile.grade}`)
|
|
if (profile.interests) parts.push(`兴趣方向: ${profile.interests}`)
|
|
if (profile.gpa) parts.push(`GPA/成绩: ${profile.gpa}`)
|
|
if (profile.goal) parts.push(`目标/困惑: ${profile.goal}`)
|
|
return parts.join('\n')
|
|
}
|
|
|
|
private parseResponse(rawReply: string, allPositions: any[]): {
|
|
reply: string
|
|
careerPaths: CareerPath[]
|
|
} {
|
|
// Extract JSON array from ---JSON--- markers
|
|
const jsonMatch = rawReply.match(/---JSON---\s*(\[[\s\S]*?\])\s*---JSON---/)
|
|
let careerPaths: CareerPath[] = []
|
|
|
|
if (jsonMatch) {
|
|
try {
|
|
careerPaths = JSON.parse(jsonMatch[1])
|
|
} catch {
|
|
this.logger.warn('Failed to parse career paths JSON')
|
|
}
|
|
}
|
|
|
|
// Clean reply (remove --JSON-- markers)
|
|
const reply = rawReply.replace(/---JSON---[\s\S]*?---JSON---/g, '').trim()
|
|
|
|
return { reply, careerPaths }
|
|
}
|
|
|
|
/** Get hot positions (delegated from positions module) */
|
|
async getHotPositions() {
|
|
return this.positionModel.find({ active: true })
|
|
.sort({ sort: 1 })
|
|
.lean()
|
|
.exec()
|
|
}
|
|
}
|