抽象简历查询

This commit is contained in:
zk
2026-04-24 19:04:57 +08:00
parent 7efa402e9c
commit f795c2fd5d
3 changed files with 112 additions and 108 deletions
+17 -40
View File
@@ -1,20 +1,14 @@
"""求职助手 Agent 对话 Service
主要功能:查询简历数据,调用 AI 模块完成对话。
依赖:UserResume + 5张子表 ORM、job_agent.chat AI 模块
使用表:bg_user_resume、bg_user_resume_education/work/internship/project/competition(查询简历
依赖:resume_loader(简历统一查询)、job_agent.chat AI 模块
使用表:bg_user_resume + 5张子表(通过 resume_loader 查询
"""
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.ai.job_agent.chat import agent_chat
from app.models.user_resume import UserResume
from app.models.user_resume_education import UserResumeEducation
from app.models.user_resume_work import UserResumeWork
from app.models.user_resume_internship import UserResumeInternship
from app.models.user_resume_project import UserResumeProject
from app.models.user_resume_competition import UserResumeCompetition
from app.services.resume_loader import ResumeDetail, load_resume_detail
class JobAgentChatService:
@@ -26,31 +20,14 @@ class JobAgentChatService:
history: list[dict], job_categories: list[str],
regions: list[str], industries: list[str]) -> dict:
"""求职助手对话:查简历 → 序列化 → 调 AI 模块"""
resume = await self._get_resume(resume_id, user_id)
edu, work, intern, proj, comp = await self._load_sub_tables(resume_id)
resume_text = self._build_resume_text(resume, edu, work, intern, proj, comp)
detail = await load_resume_detail(self.session, resume_id, user_id)
resume_text = self._build_resume_text(detail)
return await agent_chat(resume_text, message, history, job_categories, regions, industries)
async def _get_resume(self, resume_id: int, user_id: int) -> UserResume:
"""查指定简历"""
result = await self.session.execute(select(UserResume).where(UserResume.id == resume_id, UserResume.user_id == user_id))
resume = result.scalar_one_or_none()
if not resume:
raise ValueError("简历不存在")
return resume
async def _load_sub_tables(self, resume_id: int):
"""查询简历5张子表"""
edu = (await self.session.execute(select(UserResumeEducation).where(UserResumeEducation.resume_id == resume_id))).scalars().all()
work = (await self.session.execute(select(UserResumeWork).where(UserResumeWork.resume_id == resume_id))).scalars().all()
intern = (await self.session.execute(select(UserResumeInternship).where(UserResumeInternship.resume_id == resume_id))).scalars().all()
proj = (await self.session.execute(select(UserResumeProject).where(UserResumeProject.resume_id == resume_id))).scalars().all()
comp = (await self.session.execute(select(UserResumeCompetition).where(UserResumeCompetition.resume_id == resume_id))).scalars().all()
return edu, work, intern, proj, comp
@staticmethod
def _build_resume_text(resume: UserResume, edu_list, work_list, intern_list, proj_list, comp_list) -> str:
def _build_resume_text(detail: ResumeDetail) -> str:
"""将简历数据序列化为文本供 AI 使用"""
resume = detail.resume
parts = []
if resume.name:
parts.append(f"姓名:{resume.name}")
@@ -62,24 +39,24 @@ class JobAgentChatService:
parts.append(f"证书:{''.join(resume.certificates)}")
if resume.summary:
parts.append(f"个人概述:{resume.summary}")
if edu_list:
if detail.education:
parts.append("教育经历:")
for r in edu_list:
for r in detail.education:
parts.append(f" - {r.school or ''} {r.major or ''} {r.degree or ''}")
if work_list:
if detail.work:
parts.append("工作经历:")
for r in work_list:
for r in detail.work:
parts.append(f" - {r.company_name or ''} {r.position or ''}")
if intern_list:
if detail.internship:
parts.append("实习经历:")
for r in intern_list:
for r in detail.internship:
parts.append(f" - {r.company_name or ''} {r.position or ''}")
if proj_list:
if detail.project:
parts.append("项目经历:")
for r in proj_list:
for r in detail.project:
parts.append(f" - {r.project_name or ''} {r.role or ''}")
if comp_list:
if detail.competition:
parts.append("竞赛经历:")
for r in comp_list:
for r in detail.competition:
parts.append(f" - {r.competition_name or ''} {r.award or ''}")
return "\n".join(parts) if parts else "暂无简历信息"
+65
View File
@@ -0,0 +1,65 @@
"""简历统一查询模块
提供简历主表 + 5张子表的统一查询能力,返回脱离 session 的 ResumeDetail dataclass。
各 Service 统一复用,避免重复查询逻辑。
"""
from dataclasses import dataclass, field
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.user_resume import UserResume
from app.models.user_resume_education import UserResumeEducation
from app.models.user_resume_work import UserResumeWork
from app.models.user_resume_internship import UserResumeInternship
from app.models.user_resume_project import UserResumeProject
from app.models.user_resume_competition import UserResumeCompetition
@dataclass
class ResumeDetail:
"""简历完整数据,主表 + 5张子表"""
resume: UserResume
education: list[UserResumeEducation] = field(default_factory=list)
work: list[UserResumeWork] = field(default_factory=list)
internship: list[UserResumeInternship] = field(default_factory=list)
project: list[UserResumeProject] = field(default_factory=list)
competition: list[UserResumeCompetition] = field(default_factory=list)
async def load_resume_detail(session: AsyncSession, resume_id: int, user_id: int) -> ResumeDetail:
"""按ID查简历主表(校验归属)+ 5张子表,返回 ResumeDetail"""
result = await session.execute(select(UserResume).where(UserResume.id == resume_id, UserResume.user_id == user_id))
resume = result.scalar_one_or_none()
if not resume:
raise ValueError("简历不存在")
edu, work, intern, proj, comp = await _load_sub_tables(session, resume_id)
return ResumeDetail(resume=resume, education=edu, work=work, internship=intern, project=proj, competition=comp)
async def load_default_resume_detail(session: AsyncSession, user_id: int) -> ResumeDetail:
"""自动选默认简历(先默认再最新)+ 5张子表,返回 ResumeDetail"""
result = await session.execute(
select(UserResume).where(UserResume.user_id == user_id, UserResume.is_default == 1)
.order_by(desc(UserResume.update_time)).limit(1))
resume = result.scalar_one_or_none()
if not resume:
result = await session.execute(
select(UserResume).where(UserResume.user_id == user_id)
.order_by(desc(UserResume.update_time)).limit(1))
resume = result.scalar_one_or_none()
if not resume:
raise ValueError("请先创建简历")
edu, work, intern, proj, comp = await _load_sub_tables(session, resume.id)
return ResumeDetail(resume=resume, education=edu, work=work, internship=intern, project=proj, competition=comp)
async def _load_sub_tables(session: AsyncSession, resume_id: int):
"""查询简历5张子表"""
edu = (await session.execute(select(UserResumeEducation).where(UserResumeEducation.resume_id == resume_id))).scalars().all()
work = (await session.execute(select(UserResumeWork).where(UserResumeWork.resume_id == resume_id))).scalars().all()
intern = (await session.execute(select(UserResumeInternship).where(UserResumeInternship.resume_id == resume_id))).scalars().all()
proj = (await session.execute(select(UserResumeProject).where(UserResumeProject.resume_id == resume_id))).scalars().all()
comp = (await session.execute(select(UserResumeCompetition).where(UserResumeCompetition.resume_id == resume_id))).scalars().all()
return edu, work, intern, proj, comp
+30 -68
View File
@@ -11,7 +11,7 @@ import json
import random
import string
from sqlalchemy import select, desc
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.ai.skill_gap_analyzer.analyzer import (
@@ -26,11 +26,7 @@ from app.schemas.skill_gap import (
)
from app.models.job import Job
from app.models.user_resume import UserResume
from app.models.user_resume_competition import UserResumeCompetition
from app.models.user_resume_education import UserResumeEducation
from app.models.user_resume_internship import UserResumeInternship
from app.models.user_resume_project import UserResumeProject
from app.models.user_resume_work import UserResumeWork
from app.services.resume_loader import ResumeDetail, load_resume_detail, load_default_resume_detail
# Redis 常量
CUSTOMIZE_RESUME_KEY_PREFIX = "customize:resume:"
@@ -63,24 +59,25 @@ def _build_paragraphs(description: list[dict] | None) -> list[Paragraph]:
return [Paragraph(id=_rand_id(), text=item.get("text", "")) for item in description if isinstance(item, dict)]
def _build_resume_json(resume: UserResume, edu_list, work_list, intern_list, proj_list, comp_list) -> str:
def _build_resume_json(detail: ResumeDetail) -> str:
"""拼装简历 JSON 字符串供 AI 使用"""
resume = detail.resume
data = {
"skills": resume.skills or [],
"certificates": resume.certificates or [],
"summary": resume.summary or "",
"targetPosition": resume.target_position or "",
}
if edu_list:
data["education"] = [{"school": r.school, "major": r.major, "degree": r.degree, "description": r.description} for r in edu_list]
if work_list:
data["work"] = [{"companyName": r.company_name, "position": r.position, "description": r.description} for r in work_list]
if intern_list:
data["internship"] = [{"companyName": r.company_name, "position": r.position, "description": r.description} for r in intern_list]
if proj_list:
data["project"] = [{"companyName": r.company_name, "projectName": r.project_name, "role": r.role, "description": r.description} for r in proj_list]
if comp_list:
data["competition"] = [{"competitionName": r.competition_name, "award": r.award, "description": r.description} for r in comp_list]
if detail.education:
data["education"] = [{"school": r.school, "major": r.major, "degree": r.degree, "description": r.description} for r in detail.education]
if detail.work:
data["work"] = [{"companyName": r.company_name, "position": r.position, "description": r.description} for r in detail.work]
if detail.internship:
data["internship"] = [{"companyName": r.company_name, "position": r.position, "description": r.description} for r in detail.internship]
if detail.project:
data["project"] = [{"companyName": r.company_name, "projectName": r.project_name, "role": r.role, "description": r.description} for r in detail.project]
if detail.competition:
data["competition"] = [{"competitionName": r.competition_name, "award": r.award, "description": r.description} for r in detail.competition]
return json.dumps(data, ensure_ascii=False)
@@ -94,21 +91,20 @@ class SkillGapService:
async def analyze_skill_gap(self, user_id: int, job_id: int) -> dict:
"""差距分析完整流程:查简历 → 查岗位 → AI分析 → 计算匹配分"""
# 1. 自动选择简历
resume = await self._pick_resume(user_id)
detail = await load_default_resume_detail(self.session, user_id)
# 2. 查岗位
job = await self._get_job(job_id)
skill_tags: list[str] = job.skill_tags or []
# 3. skill_tags 为空 → 满分
if not skill_tags:
return self._gap_result(10.0, job, resume, [])
# 4. 查子表拼 AI 输入
edu, work, intern, proj, comp = await self._load_sub_tables(resume.id)
resume_json = _build_resume_json(resume, edu, work, intern, proj, comp)
return self._gap_result(10.0, job, detail.resume, [])
# 4. 拼 AI 输入
resume_json = _build_resume_json(detail)
# 5. AI 分析
missing = await analyze_skill_gap(skill_tags, resume_json)
# 6. 计算匹配分
score = round((len(skill_tags) - len(missing)) / len(skill_tags) * 10, 1)
return self._gap_result(score, job, resume, missing)
return self._gap_result(score, job, detail.resume, missing)
@staticmethod
def _gap_result(score: float, job: Job, resume: UserResume, missing: list[str]) -> dict:
@@ -127,16 +123,15 @@ class SkillGapService:
if not optimize_modules:
raise ValueError("请至少选择一个优化模块")
# 1. 查简历 + 岗位
resume = await self._get_resume(resume_id, user_id)
detail = await load_resume_detail(self.session, resume_id, user_id)
job = await self._get_job(job_id)
edu_rows, work_rows, intern_rows, proj_rows, comp_rows = await self._load_sub_tables(resume.id)
# 2. 组装基础定制简历
cr = self._build_customize_resume(resume, edu_rows, work_rows, intern_rows, proj_rows, comp_rows)
cr = self._build_customize_resume(detail)
# 3. 并发 AI 优化
tasks = []
job_desc = f"{job.description or ''}\n{job.requirement or ''}"
if "summary" in optimize_modules:
tasks.append(("summary", optimize_summary(job.title or "", add_skills, resume.summary or "")))
tasks.append(("summary", optimize_summary(job.title or "", add_skills, detail.resume.summary or "")))
if "experience" in optimize_modules:
for module_name, rows_json in self._experience_tasks(cr, job.title or "", job_desc):
tasks.append((module_name, optimize_module(job.title or "", job_desc, rows_json)))
@@ -371,30 +366,6 @@ class SkillGapService:
# ===== 内部工具方法 =====
async def _pick_resume(self, user_id: int) -> UserResume:
"""自动选择简历:先查默认,再查最新"""
result = await self.session.execute(
select(UserResume).where(UserResume.user_id == user_id, UserResume.is_default == 1)
.order_by(desc(UserResume.update_time)).limit(1))
resume = result.scalar_one_or_none()
if not resume:
result = await self.session.execute(
select(UserResume).where(UserResume.user_id == user_id)
.order_by(desc(UserResume.update_time)).limit(1))
resume = result.scalar_one_or_none()
if not resume:
raise ValueError("请先创建简历")
return resume
async def _get_resume(self, resume_id: int, user_id: int) -> UserResume:
"""查指定简历"""
result = await self.session.execute(
select(UserResume).where(UserResume.id == resume_id, UserResume.user_id == user_id))
resume = result.scalar_one_or_none()
if not resume:
raise ValueError("简历不存在")
return resume
async def _get_job(self, job_id: int) -> Job:
"""查岗位"""
result = await self.session.execute(select(Job).where(Job.id == job_id))
@@ -403,18 +374,9 @@ class SkillGapService:
raise ValueError("岗位不存在")
return job
async def _load_sub_tables(self, resume_id: int):
"""查询简历5张子表"""
edu = (await self.session.execute(select(UserResumeEducation).where(UserResumeEducation.resume_id == resume_id))).scalars().all()
work = (await self.session.execute(select(UserResumeWork).where(UserResumeWork.resume_id == resume_id))).scalars().all()
intern = (await self.session.execute(select(UserResumeInternship).where(UserResumeInternship.resume_id == resume_id))).scalars().all()
proj = (await self.session.execute(select(UserResumeProject).where(UserResumeProject.resume_id == resume_id))).scalars().all()
comp = (await self.session.execute(select(UserResumeCompetition).where(UserResumeCompetition.resume_id == resume_id))).scalars().all()
return edu, work, intern, proj, comp
def _build_customize_resume(self, resume: UserResume, edu_rows, work_rows,
intern_rows, proj_rows, comp_rows) -> CustomizeResume:
"""从数据库记录组装 CustomizeResume"""
def _build_customize_resume(self, detail: ResumeDetail) -> CustomizeResume:
"""从 ResumeDetail 组装 CustomizeResume"""
resume = detail.resume
profile = ResumeProfile(
avatarUrl=resume.avatar_url or "", name=resume.name or "", email=resume.email or "",
mobileNumber=resume.mobile_number or "", city=resume.city or "",
@@ -427,19 +389,19 @@ class SkillGapService:
education=[Education(id=_rand_id(), school=r.school or "", major=r.major or "",
degree=r.degree or "", studyType=r.study_type or "",
startDate=r.start_date or "", endDate=r.end_date or "",
description=_build_paragraphs(r.description)) for r in edu_rows],
description=_build_paragraphs(r.description)) for r in detail.education],
work=[Work(id=_rand_id(), companyName=r.company_name or "", position=r.position or "",
startDate=r.start_date or "", endDate=r.end_date or "",
description=_build_paragraphs(r.description)) for r in work_rows],
description=_build_paragraphs(r.description)) for r in detail.work],
internship=[Internship(id=_rand_id(), companyName=r.company_name or "", position=r.position or "",
startDate=r.start_date or "", endDate=r.end_date or "",
description=_build_paragraphs(r.description)) for r in intern_rows],
description=_build_paragraphs(r.description)) for r in detail.internship],
project=[Project(id=_rand_id(), companyName=r.company_name or "", projectName=r.project_name or "",
role=r.role or "", startDate=r.start_date or "", endDate=r.end_date or "",
description=_build_paragraphs(r.description)) for r in proj_rows],
description=_build_paragraphs(r.description)) for r in detail.project],
competition=[Competition(id=_rand_id(), competitionName=r.competition_name or "", award=r.award or "",
awardDate=r.award_date or "",
description=_build_paragraphs(r.description)) for r in comp_rows],
description=_build_paragraphs(r.description)) for r in detail.competition],
)
@staticmethod