添加简历诊断功能
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""简历诊断 AI 引擎:并行诊断 + 汇总评价"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
|
||||
from app.ai.models import LLM
|
||||
from app.ai.resume_diagnoser.prompts import DIAGNOSE_MODULE_PROMPT, SUMMARY_PROMPT
|
||||
from app.core.logger import log
|
||||
|
||||
|
||||
def _parse_json(text: str) -> dict:
|
||||
"""解析 AI 输出的 JSON,自动去除 markdown 代码块包裹,容错处理"""
|
||||
cleaned = re.sub(r"^```(?:json)?\s*\n?", "", text.strip())
|
||||
cleaned = re.sub(r"\n?```\s*$", "", cleaned)
|
||||
try:
|
||||
return json.loads(cleaned)
|
||||
except json.JSONDecodeError:
|
||||
# AI 可能在 JSON 字符串值中嵌入了未转义的引号,尝试提取最外层 { }
|
||||
match = re.search(r"\{[\s\S]*\}", cleaned)
|
||||
if match:
|
||||
return json.loads(match.group())
|
||||
raise
|
||||
|
||||
|
||||
# 诊断链(StrOutputParser 拿原始文本,再手动解析 JSON,避免 markdown 代码块导致解析失败)
|
||||
_diagnose_chain = (
|
||||
ChatPromptTemplate.from_messages([("system", DIAGNOSE_MODULE_PROMPT), ("human", "请开始诊断。")])
|
||||
| LLM.CLAUDE_SONNET_4.create(temperature=0)
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
# 汇总评价链(纯文本输出)
|
||||
_summary_chain = (
|
||||
ChatPromptTemplate.from_messages([("system", SUMMARY_PROMPT), ("human", "请生成整体评价。")])
|
||||
| LLM.CLAUDE_SONNET_4.create(temperature=0.3)
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
|
||||
async def diagnose_all(tasks: list[dict]) -> list[dict]:
|
||||
"""并行诊断所有模块记录
|
||||
|
||||
tasks: [{"module_type": ..., "target_position": ..., "context": ..., "description_text": ...}, ...]
|
||||
返回: 与 tasks 一一对应的诊断结果列表
|
||||
"""
|
||||
log.info(f"开始{len(tasks)}路并行AI诊断")
|
||||
results = await asyncio.gather(*[_safe_invoke(task) for task in tasks])
|
||||
log.info("并行AI诊断完成")
|
||||
return results
|
||||
|
||||
|
||||
async def generate_summary(grade: str, urgent_total: int, important_total: int,
|
||||
expression_total: int, target_position: str, all_findings: str) -> str:
|
||||
"""AI 生成整体评价文本"""
|
||||
inp = {
|
||||
"grade": grade, "urgent_total": str(urgent_total),
|
||||
"important_total": str(important_total), "expression_total": str(expression_total),
|
||||
"target_position": target_position or "未指定", "all_findings": all_findings,
|
||||
}
|
||||
try:
|
||||
return await _summary_chain.ainvoke(inp)
|
||||
except Exception as e:
|
||||
log.warning(f"AI生成整体评价失败: {e}")
|
||||
return "简历诊断已完成,请查看各模块的详细诊断结果。"
|
||||
|
||||
|
||||
async def _safe_invoke(task: dict) -> dict:
|
||||
"""单条记录诊断,失败返回空结果"""
|
||||
raw = ""
|
||||
try:
|
||||
raw = await _diagnose_chain.ainvoke(task)
|
||||
return _parse_json(raw)
|
||||
except Exception as e:
|
||||
log.warning(f"AI诊断[{task.get('module_type', '')}]失败: {e}\n原始输出: {raw[:500]}")
|
||||
return _empty_result()
|
||||
|
||||
|
||||
def _empty_result() -> dict:
|
||||
return {
|
||||
"finding": "", "importance": "", "suggestion": "",
|
||||
"urgent_issues": {"typo": 0},
|
||||
"important_issues": {"no_result": 0, "no_quantify": 0, "weak_relevance": 0},
|
||||
"expression_issues": {"not_concise": 0, "format_inconsistent": 0},
|
||||
"optimized_content": None,
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"""简历诊断 Prompt 模板
|
||||
|
||||
注意:prompt 中的 JSON 示例花括号必须用 {{ }} 转义,避免被 ChatPromptTemplate 当作变量。
|
||||
"""
|
||||
|
||||
DIAGNOSE_MODULE_PROMPT = """你是一位资深简历顾问和求职专家。请对以下简历模块的描述文本进行专业诊断。
|
||||
|
||||
## 模块信息
|
||||
- 模块类型:{module_type}
|
||||
- 目标岗位:{target_position}
|
||||
- 模块上下文:{context}
|
||||
|
||||
## 待诊断文本
|
||||
{description_text}
|
||||
|
||||
## 诊断维度
|
||||
|
||||
### 紧急修复
|
||||
- typo:错别字、语法错误、语病、标点符号使用错误、中英文标点混用、用词不当
|
||||
|
||||
### 重点优化
|
||||
- no_result:只描述了做了什么(任务/职责),但没有体现最终结果、产出或影响,像流水账
|
||||
- no_quantify:有成果描述但缺少具体数字支撑,使用了"大幅""显著""有效"等模糊表达,缺少人数、金额、百分比、时间等量化数据
|
||||
- weak_relevance:描述内容与目标岗位的核心职责关联度低,花大量篇幅描述与目标岗位无关的内容(注意:如果目标岗位为"未指定",此项必须为0)
|
||||
|
||||
### 表达提升
|
||||
- not_concise:句子偏长信息密度低,存在赘词重复表达(如"进行了开发"应简化为"开发了"),使用空泛修饰词("充分""积极""认真")
|
||||
- format_inconsistent:时间格式、标点风格、数字写法、项目符号、人称使用不统一
|
||||
|
||||
## 输出要求
|
||||
严格输出以下JSON格式,每个问题类别的值为该类问题出现的次数(0表示无此问题):
|
||||
```json
|
||||
{{
|
||||
"finding": "用2-3句话概述发现的主要问题",
|
||||
"importance": "用1-2句话说明为什么这些问题对简历质量很重要",
|
||||
"suggestion": "给出具体可执行的改进建议",
|
||||
"urgent_issues": {{"typo": 0}},
|
||||
"important_issues": {{"no_result": 0, "no_quantify": 0, "weak_relevance": 0}},
|
||||
"expression_issues": {{"not_concise": 0, "format_inconsistent": 0}},
|
||||
"optimized_content": ["改写后的段落1", "改写后的段落2"]
|
||||
}}
|
||||
```
|
||||
|
||||
## 关于 optimized_content 的格式要求
|
||||
- optimized_content 必须是一个纯文本字符串数组
|
||||
- 如果原文是 JSON 数组格式(如 [{{"id": "xxx", "text": "段落内容"}}]),则只提取每个元素的 text 内容进行改写,返回改写后的纯文本数组,段落数量必须与原文一一对应
|
||||
- 如果原文是纯文本(非JSON),则返回包含一个元素的数组:["改写后的完整文本"]
|
||||
- 如果原文没有明显问题,返回原文内容不做修改
|
||||
|
||||
只输出JSON,不要输出其他内容。"""
|
||||
|
||||
SUMMARY_PROMPT = """你是一位资深简历顾问。请根据以下简历诊断结果,生成一段整体评价。
|
||||
|
||||
## 诊断统计
|
||||
- 评级:{grade}
|
||||
- 紧急修复问题:{urgent_total} 个
|
||||
- 重点优化问题:{important_total} 个
|
||||
- 表达提升问题:{expression_total} 个
|
||||
|
||||
## 目标岗位
|
||||
{target_position}
|
||||
|
||||
## 各模块诊断发现
|
||||
{all_findings}
|
||||
|
||||
## 评级含义
|
||||
- A(优秀):简历相当出彩,在求职市场中格外抢眼
|
||||
- B(良好):简历已经很棒,但还有提升潜力
|
||||
- C(一般):简历还有打磨空间,需要推敲细节
|
||||
- D(待提升):简历有较大提升空间,需要尽快完善
|
||||
|
||||
## 输出要求
|
||||
请用3-5句话生成简历整体评价,包括:
|
||||
1. 背景概括(基于模块内容简要描述求职者背景)
|
||||
2. 优势总结(如果有值得肯定的地方)
|
||||
3. 主要问题(最需要改进的方面)
|
||||
4. 一句鼓励或行动建议
|
||||
|
||||
直接输出评价文本,不要输出JSON或其他格式标记。控制在300字以内。"""
|
||||
@@ -0,0 +1,76 @@
|
||||
"""简历诊断接口"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.ai.resume_diagnoser.diagnoser import diagnose_all, generate_summary
|
||||
from app.core.context import RequestContext
|
||||
from app.core.database import get_db
|
||||
from app.services.resume_diagnose_service import ResumeDiagnoseService, aggregate_results
|
||||
|
||||
router = APIRouter(prefix="/resume/diagnose", tags=["简历诊断"])
|
||||
|
||||
|
||||
class DiagnoseParam(BaseModel):
|
||||
resume_id: int = Field(..., alias="resumeId")
|
||||
|
||||
|
||||
class ResolveParam(BaseModel):
|
||||
user_feedback: int = Field(..., alias="userFeedback")
|
||||
|
||||
|
||||
@router.post("", summary="触发简历诊断")
|
||||
async def diagnose_resume(param: DiagnoseParam):
|
||||
"""触发简历AI诊断,返回报告ID"""
|
||||
user_id = RequestContext.user_id.get()
|
||||
|
||||
# 1. 短事务:加载简历数据
|
||||
async for session in get_db():
|
||||
service = ResumeDiagnoseService(session)
|
||||
resume, tasks = await service.load_resume_data(param.resume_id, user_id)
|
||||
|
||||
if not tasks:
|
||||
raise ValueError("简历没有可诊断的描述内容")
|
||||
|
||||
# 2. 并行 AI 诊断(不持有数据库连接)
|
||||
ai_tasks = [{k: v for k, v in t.items() if not k.startswith("_")} for t in tasks]
|
||||
ai_results = await diagnose_all(ai_tasks)
|
||||
|
||||
# 3. 统计 + 评级(纯计算)
|
||||
stats = aggregate_results(tasks, ai_results)
|
||||
|
||||
# 4. AI 生成整体评价(不持有数据库连接)
|
||||
summary = await generate_summary(
|
||||
grade=stats["grade"], urgent_total=stats["urgent_total"],
|
||||
important_total=stats["important_total"], expression_total=stats["expression_total"],
|
||||
target_position=resume.target_position or "", all_findings=stats["all_findings"],
|
||||
)
|
||||
|
||||
# 5. 短事务:纯写入
|
||||
async for session in get_db():
|
||||
service = ResumeDiagnoseService(session)
|
||||
report_id = await service.save_report(
|
||||
param.resume_id, user_id, stats["grade"], summary,
|
||||
stats["urgent_total"], stats["important_total"], stats["expression_total"],
|
||||
tasks, ai_results,
|
||||
)
|
||||
|
||||
return {"reportId": report_id}
|
||||
|
||||
|
||||
@router.get("/{resume_id}", summary="查询最近一次诊断报告")
|
||||
async def get_diagnosis_report(resume_id: int):
|
||||
"""查询指定简历的最近一次诊断报告 + 所有诊断问题"""
|
||||
user_id = RequestContext.user_id.get()
|
||||
async for session in get_db():
|
||||
service = ResumeDiagnoseService(session)
|
||||
return await service.get_latest_report(resume_id, user_id)
|
||||
|
||||
|
||||
@router.put("/issue/{issue_id}/resolve", summary="标记问题已处理")
|
||||
async def resolve_issue(issue_id: int, param: ResolveParam):
|
||||
"""标记诊断问题已处理 + 用户评价"""
|
||||
user_id = RequestContext.user_id.get()
|
||||
async for session in get_db():
|
||||
service = ResumeDiagnoseService(session)
|
||||
await service.resolve_issue(issue_id, user_id, param.user_feedback)
|
||||
@@ -32,9 +32,11 @@ app.add_middleware(
|
||||
# ========== 路由注册 ==========
|
||||
from app.api.health import router as health_router
|
||||
from app.api.resume import router as resume_router
|
||||
from app.api.resume_diagnose import router as resume_diagnose_router
|
||||
|
||||
app.include_router(health_router)
|
||||
app.include_router(resume_router)
|
||||
app.include_router(resume_diagnose_router)
|
||||
# ==============================
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""简历诊断问题表(bg_resume_diagnosis_issue)"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import BigInteger, Integer, String, Text, DateTime, JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class ResumeDiagnosisIssue(Base):
|
||||
"""简历诊断问题表 bg_resume_diagnosis_issue"""
|
||||
__tablename__ = "bg_resume_diagnosis_issue"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
report_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="关联report.id")
|
||||
resume_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="关联bg_user_resume.id")
|
||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="用户ID")
|
||||
module_type: Mapped[str] = mapped_column(String(32), nullable=False, comment="模块类型: summary/education/work/internship/project/competition")
|
||||
module_record_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="模块记录ID,summary时为resume_id")
|
||||
finding: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="诊断发现")
|
||||
importance: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="为什么重要")
|
||||
suggestion: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="改进建议")
|
||||
urgent_issues: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment='紧急修复子类型计数 {"typo": 0}')
|
||||
important_issues: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment='重点优化子类型计数 {"no_result": 0, "no_quantify": 0, "weak_relevance": 0}')
|
||||
expression_issues: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment='表达提升子类型计数 {"not_concise": 0, "format_inconsistent": 0}')
|
||||
optimized_content: Mapped[Optional[list | str]] = mapped_column(JSON, nullable=True, comment="AI改写后的内容,子表模块与原description格式一致[{id,text}]保持原id只改写text,summary模块为纯文本字符串")
|
||||
status: Mapped[int] = mapped_column(Integer, default=0, comment="0=待处理 1=已处理")
|
||||
user_feedback: Mapped[int] = mapped_column(Integer, default=0, comment="0=未评价 1=符合 2=不符合")
|
||||
create_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, comment="创建时间")
|
||||
update_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||
@@ -0,0 +1,25 @@
|
||||
"""简历诊断报告表(bg_resume_diagnosis_report)"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import BigInteger, Integer, String, Text, DateTime
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class ResumeDiagnosisReport(Base):
|
||||
"""简历诊断报告表 bg_resume_diagnosis_report"""
|
||||
__tablename__ = "bg_resume_diagnosis_report"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
resume_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="关联bg_user_resume.id")
|
||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="用户ID")
|
||||
grade: Mapped[Optional[str]] = mapped_column(String(1), nullable=True, comment="评级 A/B/C/D")
|
||||
summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="AI生成的整体评价")
|
||||
urgent_total: Mapped[int] = mapped_column(Integer, default=0, comment="紧急修复总数")
|
||||
important_total: Mapped[int] = mapped_column(Integer, default=0, comment="重点优化总数")
|
||||
expression_total: Mapped[int] = mapped_column(Integer, default=0, comment="表达提升总数")
|
||||
create_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, comment="创建时间")
|
||||
update_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||
@@ -0,0 +1,257 @@
|
||||
"""简历诊断 Service
|
||||
|
||||
加载简历描述数据 → 并行 AI 诊断 → 统计评级 → AI 汇总评价 → 写入数据库。
|
||||
依赖:resume_diagnoser(AI诊断引擎)
|
||||
使用表:bg_user_resume + 5张子表(读)、bg_resume_diagnosis_report + issue(写)
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import shortuuid
|
||||
from sqlalchemy import select, desc
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.logger import log
|
||||
from app.models.resume_diagnosis_issue import ResumeDiagnosisIssue
|
||||
from app.models.resume_diagnosis_report import ResumeDiagnosisReport
|
||||
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.tool.snowflake import next_id
|
||||
|
||||
# 模块中文名映射
|
||||
_MODULE_LABELS = {
|
||||
"summary": "个人概述", "education": "教育经历", "work": "工作经历",
|
||||
"internship": "实习经历", "project": "项目经历", "competition": "竞赛经历",
|
||||
}
|
||||
|
||||
|
||||
class ResumeDiagnoseService:
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def load_resume_data(self, resume_id: int, user_id: int) -> tuple[UserResume, list[dict]]:
|
||||
"""加载简历主表 + 5 张子表数据,组装 AI 任务列表"""
|
||||
result = await self.session.execute(
|
||||
select(UserResume).where(UserResume.id == resume_id, UserResume.user_id == user_id))
|
||||
resume = result.scalar_one_or_none()
|
||||
if resume is None:
|
||||
raise ValueError("简历不存在")
|
||||
|
||||
target_position = resume.target_position or ""
|
||||
tasks: list[dict] = []
|
||||
|
||||
# summary
|
||||
if resume.summary and resume.summary.strip():
|
||||
tasks.append({
|
||||
"module_type": "个人概述", "target_position": target_position or "未指定",
|
||||
"context": f"姓名: {resume.name or '未填写'}",
|
||||
"description_text": resume.summary,
|
||||
"_module_type_key": "summary", "_module_record_id": resume_id,
|
||||
})
|
||||
|
||||
# 子表
|
||||
await self._collect_tasks(tasks, target_position, "education", UserResumeEducation, resume_id,
|
||||
lambda r: f"学校: {r.school or ''}, 专业: {r.major or ''}, 学历: {r.degree or ''}")
|
||||
await self._collect_tasks(tasks, target_position, "work", UserResumeWork, resume_id,
|
||||
lambda r: f"公司: {r.company_name or ''}, 职位: {r.position or ''}")
|
||||
await self._collect_tasks(tasks, target_position, "internship", UserResumeInternship, resume_id,
|
||||
lambda r: f"公司: {r.company_name or ''}, 职位: {r.position or ''}")
|
||||
await self._collect_tasks(tasks, target_position, "project", UserResumeProject, resume_id,
|
||||
lambda r: f"公司: {r.company_name or ''}, 项目: {r.project_name or ''}, 角色: {r.role or ''}")
|
||||
await self._collect_tasks(tasks, target_position, "competition", UserResumeCompetition, resume_id,
|
||||
lambda r: f"竞赛: {r.competition_name or ''}, 获奖: {r.award or ''}")
|
||||
return resume, tasks
|
||||
|
||||
async def _collect_tasks(self, tasks: list[dict], target_position: str,
|
||||
module_type: str, model_cls, resume_id: int, context_fn):
|
||||
"""查询子表记录,将有 description 的记录加入 tasks"""
|
||||
result = await self.session.execute(select(model_cls).where(model_cls.resume_id == resume_id))
|
||||
for record in result.scalars().all():
|
||||
desc_text = _build_description_text(record.description)
|
||||
if not desc_text:
|
||||
continue
|
||||
tasks.append({
|
||||
"module_type": _MODULE_LABELS[module_type],
|
||||
"target_position": target_position or "未指定",
|
||||
"context": context_fn(record),
|
||||
"description_text": desc_text,
|
||||
"_module_type_key": module_type, "_module_record_id": record.id,
|
||||
"_original_description": record.description, # 原始 [{id,text}],用于映射 optimized_content
|
||||
})
|
||||
|
||||
async def save_report(self, resume_id: int, user_id: int, grade: str, summary: str,
|
||||
urgent_total: int, important_total: int, expression_total: int,
|
||||
tasks: list[dict], ai_results: list[dict]) -> int:
|
||||
"""纯写入:接收已算好的 grade、summary、统计数据,写入 report + issues"""
|
||||
report_id = next_id()
|
||||
|
||||
self.session.add(ResumeDiagnosisReport(
|
||||
id=report_id, resume_id=resume_id, user_id=user_id,
|
||||
grade=grade, summary=summary,
|
||||
urgent_total=urgent_total, important_total=important_total, expression_total=expression_total,
|
||||
))
|
||||
|
||||
for task, ai_result in zip(tasks, ai_results):
|
||||
if not _has_issues(ai_result):
|
||||
continue
|
||||
self.session.add(ResumeDiagnosisIssue(
|
||||
id=next_id(), report_id=report_id, resume_id=resume_id, user_id=user_id,
|
||||
module_type=task["_module_type_key"], module_record_id=task["_module_record_id"],
|
||||
finding=ai_result.get("finding", ""), importance=ai_result.get("importance", ""),
|
||||
suggestion=ai_result.get("suggestion", ""),
|
||||
urgent_issues=ai_result.get("urgent_issues"), important_issues=ai_result.get("important_issues"),
|
||||
expression_issues=ai_result.get("expression_issues"),
|
||||
optimized_content=_build_optimized_content(task, ai_result.get("optimized_content")),
|
||||
status=0, user_feedback=0,
|
||||
))
|
||||
|
||||
await self.session.flush()
|
||||
log.info(f"诊断报告保存完成 reportId:{report_id} grade:{grade}")
|
||||
return report_id
|
||||
|
||||
async def get_latest_report(self, resume_id: int, user_id: int) -> dict | None:
|
||||
"""查询最近一次诊断报告 + 所有 issues"""
|
||||
result = await self.session.execute(
|
||||
select(ResumeDiagnosisReport).where(
|
||||
ResumeDiagnosisReport.resume_id == resume_id, ResumeDiagnosisReport.user_id == user_id,
|
||||
).order_by(desc(ResumeDiagnosisReport.create_time)).limit(1))
|
||||
report = result.scalar_one_or_none()
|
||||
if report is None:
|
||||
return None
|
||||
|
||||
result = await self.session.execute(
|
||||
select(ResumeDiagnosisIssue).where(ResumeDiagnosisIssue.report_id == report.id))
|
||||
issues = result.scalars().all()
|
||||
|
||||
return {
|
||||
"report": {
|
||||
"id": str(report.id), "resumeId": str(report.resume_id),
|
||||
"grade": report.grade, "summary": report.summary,
|
||||
"urgentTotal": report.urgent_total, "importantTotal": report.important_total,
|
||||
"expressionTotal": report.expression_total,
|
||||
"createTime": report.create_time.strftime("%Y-%m-%d %H:%M:%S") if report.create_time else None,
|
||||
},
|
||||
"issues": [_issue_to_dict(i) for i in issues],
|
||||
}
|
||||
|
||||
async def resolve_issue(self, issue_id: int, user_id: int, user_feedback: int) -> None:
|
||||
"""标记问题已处理 + 用户评价"""
|
||||
result = await self.session.execute(
|
||||
select(ResumeDiagnosisIssue).where(
|
||||
ResumeDiagnosisIssue.id == issue_id, ResumeDiagnosisIssue.user_id == user_id))
|
||||
issue = result.scalar_one_or_none()
|
||||
if issue is None:
|
||||
raise ValueError("诊断问题不存在")
|
||||
issue.status = 1
|
||||
issue.user_feedback = user_feedback
|
||||
await self.session.flush()
|
||||
|
||||
|
||||
# ===== 工具函数 =====
|
||||
|
||||
def _build_optimized_content(task: dict, ai_texts: list[str] | None):
|
||||
"""将 AI 返回的纯文本数组映射回存储格式
|
||||
- summary 模块:取第一个元素作为纯文本字符串
|
||||
- 子表模块:用原始 description 的 id + AI 改写的 text 组合成 [{id, text}]
|
||||
"""
|
||||
if not ai_texts or not isinstance(ai_texts, list):
|
||||
return None
|
||||
original = task.get("_original_description")
|
||||
if original is None:
|
||||
# summary 模块,存纯文本
|
||||
return ai_texts[0] if ai_texts else None
|
||||
# 子表模块,映射回 [{id, text}]
|
||||
result = []
|
||||
for i, item in enumerate(original):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
text = ai_texts[i] if i < len(ai_texts) else item.get("text", "")
|
||||
result.append({"id": item.get("id"), "text": text})
|
||||
return result
|
||||
|
||||
|
||||
def _build_description_text(description: list[dict] | None) -> str:
|
||||
"""子表 description [{id, text}] → JSON 字符串传给 AI(保留 id 以便 AI 返回同格式)"""
|
||||
if not description:
|
||||
return ""
|
||||
valid = [item for item in description if isinstance(item, dict) and item.get("text")]
|
||||
if not valid:
|
||||
return ""
|
||||
return json.dumps(valid, ensure_ascii=False)
|
||||
|
||||
|
||||
def aggregate_results(tasks: list[dict], ai_results: list[dict]) -> dict:
|
||||
"""统计汇总 + 评级,返回 {grade, urgent_total, important_total, expression_total, has_weak_relevance, all_findings}"""
|
||||
urgent_total = 0
|
||||
important_total = 0
|
||||
expression_total = 0
|
||||
has_weak_relevance = False
|
||||
all_findings: list[str] = []
|
||||
|
||||
for task, ai_result in zip(tasks, ai_results):
|
||||
urgent = ai_result.get("urgent_issues", {})
|
||||
important = ai_result.get("important_issues", {})
|
||||
expression = ai_result.get("expression_issues", {})
|
||||
|
||||
urgent_total += sum(v for v in urgent.values() if isinstance(v, int))
|
||||
important_total += sum(v for v in important.values() if isinstance(v, int))
|
||||
expression_total += sum(v for v in expression.values() if isinstance(v, int))
|
||||
|
||||
if important.get("weak_relevance", 0) > 0:
|
||||
has_weak_relevance = True
|
||||
|
||||
finding = ai_result.get("finding", "")
|
||||
if finding and _has_issues(ai_result):
|
||||
label = _MODULE_LABELS.get(task["_module_type_key"], task["_module_type_key"])
|
||||
all_findings.append(f"【{label}】{finding}")
|
||||
|
||||
grade = _calc_grade(urgent_total, important_total, expression_total, has_weak_relevance)
|
||||
return {
|
||||
"grade": grade, "urgent_total": urgent_total,
|
||||
"important_total": important_total, "expression_total": expression_total,
|
||||
"all_findings": "\n".join(all_findings),
|
||||
}
|
||||
|
||||
|
||||
def _calc_grade(urgent: int, important: int, expression: int, has_weak_relevance: bool) -> str:
|
||||
"""评级硬算:D → C → B → A
|
||||
A:urgent=0, important<=1, expression<=1
|
||||
B:urgent=0, important<=3, expression<=2(且不满足A)
|
||||
C:urgent=1, 或 important 3-4
|
||||
D:urgent>=2, 或 (important>=4 且 has_weak_relevance)
|
||||
"""
|
||||
if urgent >= 2 or (important >= 4 and has_weak_relevance):
|
||||
return "D"
|
||||
if urgent == 1 or 3 <= important <= 4:
|
||||
return "C"
|
||||
if urgent == 0 and important <= 1 and expression <= 1:
|
||||
return "A"
|
||||
if urgent == 0 and important <= 3 and expression <= 2:
|
||||
return "B"
|
||||
return "C"
|
||||
|
||||
|
||||
def _has_issues(ai_result: dict) -> bool:
|
||||
"""判断诊断结果是否存在问题(所有计数都为 0 则无问题)"""
|
||||
for key in ("urgent_issues", "important_issues", "expression_issues"):
|
||||
counts = ai_result.get(key, {})
|
||||
if any(v > 0 for v in counts.values() if isinstance(v, int)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _issue_to_dict(issue: ResumeDiagnosisIssue) -> dict:
|
||||
"""ORM → API 响应字典"""
|
||||
return {
|
||||
"id": str(issue.id), "moduleType": issue.module_type,
|
||||
"moduleRecordId": str(issue.module_record_id),
|
||||
"finding": issue.finding, "importance": issue.importance, "suggestion": issue.suggestion,
|
||||
"urgentIssues": issue.urgent_issues, "importantIssues": issue.important_issues,
|
||||
"expressionIssues": issue.expression_issues, "optimizedContent": issue.optimized_content,
|
||||
"status": issue.status, "userFeedback": issue.user_feedback,
|
||||
}
|
||||
Reference in New Issue
Block a user