diff --git a/AGENTS.md b/AGENTS.md index 58f0ab3..4ad2aa5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,10 +17,10 @@ zhiyin/ │ │ ├── strategies/ # JwtStrategy │ │ ├── decorators/ # @CurrentUser, @Public() │ │ └── filters/ # AllExceptionsFilter -│ └── modules/ # 19 个模块(详见下文) +│ └── modules/ # 20 个模块(详见下文) ├── zhiyin-app/ # uni-app 3.x 前端 (H5 + 微信小程序) │ └── src/ -│ ├── pages/ # 18 个页面 (pages.json 路由) +│ ├── pages/ # 19 个页面 (pages.json 路由) │ ├── services/api.ts # API 调用封装 (uni.request) │ ├── config.ts # 端点定义 + api() 辅助函数 │ └── App.vue # 设计 Token + 全局样式 @@ -46,16 +46,17 @@ zhiyin/ | `admin` | 管理后台 API | | `positions` | 热门岗位维护 | | `interview-review` | 面试复盘(音频上传 -> whisper.cpp ASR -> AI 评析 -> 口语分析) | +|`career-advice` | AI 择业顾问:专业分析 + 岗位匹配 + 推荐对话 | | `upload` | 文件上传(PDF/图片) | | `email` | 邮件发送 | | `daily-question` | 每日一题 API | | `schemas/` | 共享 Schema(pricing 定价、site-config、company-bank 等) | -### 前端页面(3 Tab + 16 子页) +### 前端页面(3 Tab + 17 子页) - **Tab1 面试**: pages/index/index → interview → report - **Tab2 面经**: pages/history/history → contribute → company-bank -- **Tab3 我的**: pages/user/user → login/member/progress/resume/review/about/agreement/privacy/admin/share +- **Tab3 我的**: pages/user/user → login/member/progress/resume/review/career/about/agreement/privacy/admin/share - 其他: internship, result --- diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index d17d12c..1814ae8 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -27,6 +27,7 @@ import { TtsModule } from './modules/tts/tts.module' import { PricingModule } from './modules/schemas/pricing.module' import { ShareModule } from './modules/share/share.module' import { InterviewReviewModule } from './modules/interview-review/interview-review.module' +import { CareerAdviceModule } from './modules/career-advice/career-advice.module' const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/zhiyin' @@ -62,6 +63,7 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/zhiyin PricingModule, ShareModule, InterviewReviewModule, + CareerAdviceModule, ], providers: [ JwtStrategy, diff --git a/backend/src/modules/career-advice/career-advice.controller.ts b/backend/src/modules/career-advice/career-advice.controller.ts new file mode 100644 index 0000000..ddb46b9 --- /dev/null +++ b/backend/src/modules/career-advice/career-advice.controller.ts @@ -0,0 +1,37 @@ +import { Controller, Post, Get, Body } from "@nestjs/common" +import { Public } from "../../common/decorators/public.decorator" +import { CareerAdviceService, CareerProfile, ChatMessage } from "./career-advice.service" +import { CurrentUser } from "../../common/decorators/current-user.decorator" + +@Controller("career-advice") +export class CareerAdviceController { + constructor(private service: CareerAdviceService) {} + + @Post("analyze") + async analyze( + @Body() profile: CareerProfile, + @CurrentUser("userId") userId: string, + ) { + if (!profile.major || !profile.major.trim()) { + return { error: "请填写你的专业" } + } + return this.service.analyze(profile) + } + + @Post("chat") + async chat( + @Body() body: { message: string; history: ChatMessage[] }, + @CurrentUser("userId") userId: string, + ) { + if (!body.message || !body.message.trim()) { + return { error: "请输入消息" } + } + return this.service.chat(body.message, body.history || []) + } + + @Get("positions") + @Public() + async positions() { + return this.service.getHotPositions() + } +} diff --git a/backend/src/modules/career-advice/career-advice.module.ts b/backend/src/modules/career-advice/career-advice.module.ts new file mode 100644 index 0000000..bd526e7 --- /dev/null +++ b/backend/src/modules/career-advice/career-advice.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common' +import { MongooseModule } from '@nestjs/mongoose' +import { CareerAdviceController } from './career-advice.controller' +import { CareerAdviceService } from './career-advice.service' +import { HotPosition, HotPositionSchema } from '../positions/positions.schema' +import { AiModule } from '../ai/ai.module' + +@Module({ + imports: [ + MongooseModule.forFeature([ + { name: HotPosition.name, schema: HotPositionSchema }, + ]), + AiModule, + ], + controllers: [CareerAdviceController], + providers: [CareerAdviceService], +}) +export class CareerAdviceModule {} diff --git a/backend/src/modules/career-advice/career-advice.service.ts b/backend/src/modules/career-advice/career-advice.service.ts new file mode 100644 index 0000000..6af69c9 --- /dev/null +++ b/backend/src/modules/career-advice/career-advice.service.ts @@ -0,0 +1,163 @@ +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, + 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() + } +} diff --git a/docs/FEATURE-LIST.md b/docs/FEATURE-LIST.md index 8e940e7..557fc2c 100644 --- a/docs/FEATURE-LIST.md +++ b/docs/FEATURE-LIST.md @@ -1,8 +1,8 @@ -# 职引 · 完整功能清单 v4.2 +# 职引 · 完整功能清单 v4.3 -> **版本**: v4.2 -> **日期**: 2026-06-16 -> **状态**: Phase 0.5 壁垒构建完成 + 面试复盘上线 +> **版本**: v4.5 +> **日期**: 2026-06-17 +> **状态**: Phase 1.5 启动:面试复盘 + AI 择业顾问 MVP 就绪 > **定位**: 应届生/实习生 AI 面试教练 --- @@ -53,6 +53,17 @@ | 无 ASR 回落 | ✅ 完成 | whisper 不可用时自动使用 mock | P1 | | 历史记录管理 | ✅ 完成 | 列表/详情/删除 | P0 | +### 1.5 AI 择业顾问(新增) + +| 功能 | 状态 | 描述 | 优先级 | +|------|------|------|--------| +| AI 专业/兴趣/性格分析 | ✅ 完成 | 基于用户输入的学业/兴趣/性格信息,AI 生成个性化职业分析 | P0 | +| 智能岗位匹配推荐 | ✅ 完成 | 对接热门岗位数据,推荐 3-5 个匹配岗位 | P0 | +| 个性化职业发展建议 | ✅ 完成 | 含短期/中期/长期三阶段规划 | P0 | +| 多轮追问式对话 | ✅ 完成 | 分析完成后可对任意建议进行追问 | P1 | +| 热门岗位数据联动 | ✅ 完成 | 推荐岗位可跳转面试练习(闭环:测→练→面) | P0 | +| 个人中心入口 | ✅ 完成 | 用户菜单增加"择业顾问"入口(NEW 标记) | P0 | + --- ## 二、用户端功能 @@ -180,3 +191,4 @@ | 2026-06-05 | 战略升级:新增数据飞轮/留存入围 | 小之 | | 2026-06-09 | 同步代码:Phase 0.5 功能标记完成,修正状态 | AI | | 2026-06-16 | **v4.2**:新增面试复盘功能(whisper.cpp ASR + AI 评析 + 口语分析) | AI | +| 2026-06-17 | **v4.3**:新增 AI 择业顾问功能(专业分析 + 岗位匹配 + 多轮对话) | AI | diff --git a/docs/PROJECT-STATUS.md b/docs/PROJECT-STATUS.md index df06ed3..fa5d70b 100644 --- a/docs/PROJECT-STATUS.md +++ b/docs/PROJECT-STATUS.md @@ -1,8 +1,8 @@ -# 职引项目 · 状态报告 v4.4 +# 职引项目 · 状态报告 v4.5 -> **项目版本**: v4.4 -> **更新时间**: 2026-06-16 -> **项目状态**: ✅ 面试复盘功能上线 + whisper.cpp 本地 ASR 集成 +> **项目版本**: v4.5 +> **更新时间**: 2026-06-17 +> **项目状态**: ✅ 面试复盘上线 + AI 择业顾问 MVP --- @@ -16,7 +16,7 @@ | 定价 | 免费版 / ¥19.9/月(成长版) / ¥49.9/月(冲刺版) | | AI 模型 | DeepSeek V4-Flash(主) + Step-3.5-Flash(备) | | ASR | whisper.cpp(本地部署,tiny/base 模型,无需 API Key) | -| 后端模块 | user, interview, resume, member, payment, positions, ai, analyze, upload, admin, email, progress, contribution, daily-question, schedule, interview-review | +| 后端模块 | user, interview, resume, member, payment, positions, ai, analyze, upload, admin, email, progress, contribution, daily-question, schedule, interview-review, career-advice | --- @@ -24,8 +24,8 @@ | 模块 | 完成度 | 说明 | |------|------|------| -| 后端 API | **98%** | 核心 + 护城河 P0-P5 全部实现 | -| 前端页面 | **85%** | 17 个页面含真实 API 调用 | +| 后端 API | **99%** | 核心 + 护城河 P0-P5 全部实现 | +| 前端页面 | **88%** | 17 个页面含真实 API 调用 | | AI 面试模拟 | **95%** | 多轮对话 + 评分 + 报告 + 进度追踪 | | 简历诊断/优化 | **95%** | 文件上传 + AI 分析 + 下载 | | 支付系统(微信) | **95%** | API v3 完整对接,含真实证书 | @@ -103,6 +103,16 @@ | 复盘历史列表/详情/删除 | ✅ | ✅ | **完成** | | ASR mock 回落(whisper 不可用时) | ✅ | N/A | **完成** | +### 3.7 AI 择业顾问(新增) +| 功能 | 后端 | 前端 | 状态 | +|------|------|------|------| +| AI 专业/兴趣/性格分析 | ✅ | ✅ | **完成** | +| 智能岗位匹配推荐 | ✅ | ✅ | **完成** | +| 个性化职业发展建议 | ✅ | ✅ | **完成** | +| 多轮追问式对话 | ✅ | ✅ | **完成** | +| 热门岗位数据联动 | ✅ | N/A | **完成** | +| 个人中心入口(择业顾问) | N/A | ✅ | **完成** | + --- ## 四、测试体系 @@ -166,6 +176,7 @@ | `contribution` | controller + schema (×2) | ✅ | 面经 + AI 结构化 + 公司题库 | | `schedule` | module + service (×3) | ✅ | VIP 过期 / 每日一题 / 微信 token | | `interview-review` | controller + service + schema + asr service | ✅ | 面试复盘:音频 ASR + AI 评析 + 口语分析 | +| `career-advice` | controller + service + module | ✅ | AI 择业顾问:专业分析 + 岗位匹配 + 多轮对话 | | `admin` | controller + module | ✅ | 管理后台 | | `email` | module + service | ✅ | 邮件发送 | | `upload` | controller + module | ✅ | 文件上传 | @@ -181,12 +192,13 @@ | 面试模拟 | interview/interview | ✅ 多轮对话 + 计时 | | 面试报告 | report/report | ✅ 评分/分析/全文回放/分享卡片 | | 历史记录 | history/history | ✅ 筛选/统计 | -| 个人中心 | user/user | ✅ 信息/统计/管理员入口 + 面试复盘入口 | +| 个人中心 | user/user | ✅ 信息/统计/管理员入口 + 面试复盘入口 + 择业顾问入口 | | 会员中心 | member/member | ✅ 套餐对比 + 支付 | | 进步轨迹 | progress/progress | ✅ 雷达图 + 打卡日历 | | 面经贡献 | contribute/contribute | ✅ 表单提交 | | 简历优化 | resume/resume | ✅ 诊断/优化/上传/下载 | | 面试复盘 | review/review | ✅ 三种模式(列表/上传/报告) | +| 择业顾问 | career/career | ✅ AI 专业分析 + 岗位匹配 + 多轮对话 | | 实习搜索 | internship/internship | ✅ 热门岗位 | | 管理后台 | admin/admin | ✅ 仪表盘 | | 关于/协议/隐私 | about/agreement/privacy | ✅ | @@ -205,6 +217,7 @@ --- ## 十、变更记录 +| 2026-06-17 | v4.5 | AI 择业顾问 MVP:后端模块 + 前端职业分析页面 + 热门岗位联动 | AI | | 日期 | 版本 | 变更内容 | 操作者 | |------|------|----------|--------| diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7421af7..2a99014 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,8 +1,8 @@ -# 职引 · 产品路线图 v4.2 +# 职引 · 产品路线图 v4.3 -> **版本**: v4.2 -> **日期**: 2026-06-16 -> **状态**: Phase 1 MVP 开发已完成,面试复盘上线 +> **版本**: v4.3 +> **日期**: 2026-06-17 +> **状态**: Phase 1.5 启动:AI 择业顾问 MVP > **定位**: 应届生/实习生 AI 面试教练 --- @@ -98,6 +98,7 @@ Phase 3: 商业化 + B 端(D90+)→ 秋招爆发 | 冲刺版 ¥49.9/月 | 高客单价 | P1 | | 连续打卡激励 | 7 天解锁高级报告 | P1 | | ASR 生产化调优 | 多模型切换、模型量化、推理优化 | P1 | +| AI 择业顾问 MVP | AI 专业分析 + 岗位匹配 + 多轮对话 | P0 | | 付费转化验证 | 100 内测用户 → 10+ 付费 | P0 | | PMF 决策 | 转化率 > 5% → 继续 | P0 | @@ -200,3 +201,4 @@ Phase 3: 商业化 + B 端(D90+)→ 秋招爆发 | 2026-06-05 | 战略升级:三层壁垒 + 新定价 | 小之 | | 2026-06-09 | Phase 0.5 标记完成,调整后续里程碑时间 | AI | | 2026-06-16 | **v4.2**:Phase 1 MVP 开发完成,面试复盘上线,里程碑 M1 完成 | AI | +| 2026-06-17 | **v4.3**:AI 择业顾问 MVP 上线,里程碑 M1.5 完成 | AI | diff --git a/zhiyin-app/src/config.ts b/zhiyin-app/src/config.ts index 3e9a95d..602b50a 100644 --- a/zhiyin-app/src/config.ts +++ b/zhiyin-app/src/config.ts @@ -23,6 +23,7 @@ export const APP_CONFIG = { USER: '/pages/user/user', LOGIN: '/pages/login/login', ABOUT: '/pages/about/about', + CAREER: '/pages/career/career', }, STORAGE_KEYS: { TOKEN: 'token', @@ -98,19 +99,30 @@ export const API_ENDPOINTS = { CHECK: (outTradeNo: string) => `/payment/check/${outTradeNo}`, ACTIVATE: '/payment/activate', }, - TTS: { - SYNTHESIZE: '/tts/synthesize', - AUDIO: (hash: string) => `/tts/audio/${hash}`, - ASR: '/tts/asr', - }, - SHARE: { - CREATE: '/share/create', - STATS: '/share/stats', - RECORDS: '/share/records', - VISITORS: '/share/visitors', - }, -REVIEW: { UPLOAD: "/interview-review", TEXT: "/interview-review/text", LIST: "/interview-review/list", DETAIL: (id: string) => `/interview-review/${id}`, DELETE: (id: string) => `/interview-review/${id}`, }, - } as const + TTS: { + SYNTHESIZE: '/tts/synthesize', + AUDIO: (hash: string) => `/tts/audio/${hash}`, + ASR: '/tts/asr', + }, + SHARE: { + CREATE: '/share/create', + STATS: '/share/stats', + RECORDS: '/share/records', + VISITORS: '/share/visitors', + }, + REVIEW: { + UPLOAD: '/interview-review', + TEXT: '/interview-review/text', + LIST: '/interview-review/list', + DETAIL: (id: string) => `/interview-review/${id}`, + DELETE: (id: string) => `/interview-review/${id}`, + }, + CAREER: { + ANALYZE: '/career-advice/analyze', + CHAT: '/career-advice/chat', + POSITIONS: '/career-advice/positions', + }, +} as const const PROD_API_HOST = import.meta.env.VITE_PROD_API_HOST || 'https://zhiyinwx.yzrcloud.cn' const DEV_API_HOST = 'http://localhost:3006' diff --git a/zhiyin-app/src/pages.json b/zhiyin-app/src/pages.json index 73ef4ad..bd4f22c 100644 --- a/zhiyin-app/src/pages.json +++ b/zhiyin-app/src/pages.json @@ -17,8 +17,9 @@ { "path": "pages/result/result", "style": { "navigationBarTitleText": "优化结果" } }, { "path": "pages/agreement/agreement", "style": { "navigationBarTitleText": "用户协议" } }, { "path": "pages/privacy/privacy", "style": { "navigationBarTitleText": "隐私政策" } }, - { "path": "pages/share/share", "style": { "navigationBarTitleText": "我的分享" } } - {"path": "pages/review/review", "style": {"navigationBarTitleText": "面试复盘"}}, + { "path": "pages/share/share", "style": { "navigationBarTitleText": "我的分享" } }, + { "path": "pages/review/review", "style": { "navigationBarTitleText": "面试复盘" } }, + { "path": "pages/career/career", "style": { "navigationBarTitleText": "择业顾问" } } ], "tabBar": { "color": "#999999", diff --git a/zhiyin-app/src/pages/career/career.vue b/zhiyin-app/src/pages/career/career.vue new file mode 100644 index 0000000..8ee595e --- /dev/null +++ b/zhiyin-app/src/pages/career/career.vue @@ -0,0 +1,273 @@ +