抽象简历查询

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
+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