1 Commits
Author SHA1 Message Date
zk 8051933dcf 添加简历表子表 2026-08-11 17:57:47 +08:00
16 changed files with 167 additions and 47 deletions
+8 -6
View File
@@ -1,8 +1,8 @@
"""简历一步并发提取 """简历一步并发提取
6 路并发(个人信息 / 教育 / 工作 / 实习 / 项目 / 竞赛),每路独立负责一个模块, 7 路并发(个人信息 / 教育 / 工作 / 实习 / 项目 / 竞赛 / 社团组织),每路独立负责一个模块,
各自基于简历全文一次性输出该模块的全部内容(短字段 + description),无第二阶段。 各自基于简历全文一次性输出该模块的全部内容(短字段 + description),无第二阶段。
相比两阶段方案:少一个串行轮次、全文只 prefill 6 次、无标识名定位环节。 相比两阶段方案:少一个串行轮次、全文只 prefill 7 次、无标识名定位环节。
description/summary 由 AI 直接按原文结构输出为字符串数组,代码只做类型兜底与清理。 description/summary 由 AI 直接按原文结构输出为字符串数组,代码只做类型兜底与清理。
最终组装为与两阶段方案完全一致的 dict 结构(description 为 list[str]summary 为 str),上下游无感知。 最终组装为与两阶段方案完全一致的 dict 结构(description 为 list[str]summary 为 str),上下游无感知。
任意模块提取失败:记录日志后丢弃该模块(主表字段退化为空、子表退化为空数组),不影响其余模块。 任意模块提取失败:记录日志后丢弃该模块(主表字段退化为空、子表退化为空数组),不影响其余模块。
@@ -18,6 +18,7 @@ from app.ai.model_config import ResumeExtractorModel
from app.ai.resume_extractor_v2.prompts import ( from app.ai.resume_extractor_v2.prompts import (
PROFILE_PROMPT, EDUCATION_PROMPT, WORK_PROMPT, PROFILE_PROMPT, EDUCATION_PROMPT, WORK_PROMPT,
INTERNSHIP_PROMPT, PROJECT_PROMPT, COMPETITION_PROMPT, INTERNSHIP_PROMPT, PROJECT_PROMPT, COMPETITION_PROMPT,
ORGANIZATION_PROMPT,
) )
from app.core.logger import log from app.core.logger import log
from app.tool.json_helper import parse_llm_json from app.tool.json_helper import parse_llm_json
@@ -76,9 +77,10 @@ _SUB_MODULES: tuple[tuple[str, str, str], ...] = (
("internship", INTERNSHIP_PROMPT, "实习"), ("internship", INTERNSHIP_PROMPT, "实习"),
("project", PROJECT_PROMPT, "项目"), ("project", PROJECT_PROMPT, "项目"),
("competition", COMPETITION_PROMPT, "竞赛"), ("competition", COMPETITION_PROMPT, "竞赛"),
("organization", ORGANIZATION_PROMPT, "社团组织"),
) )
# 无运行时 prompt 替换,6 条链在模块加载时一次性建好,请求期只做 ainvoke # 无运行时 prompt 替换,7 条链在模块加载时一次性建好,请求期只做 ainvoke
_profile_chain = _build_chain(PROFILE_PROMPT) _profile_chain = _build_chain(PROFILE_PROMPT)
_sub_chains = tuple((module, _build_chain(prompt), label) for module, prompt, label in _SUB_MODULES) _sub_chains = tuple((module, _build_chain(prompt), label) for module, prompt, label in _SUB_MODULES)
@@ -98,11 +100,11 @@ def _assemble_profile(profile) -> dict:
# ==================== 入口 ==================== # ==================== 入口 ====================
async def extract_all(text: str) -> dict: async def extract_all(text: str) -> dict:
"""一步6路并发提取简历,返回与两阶段方案完全一致的结构化数据 """一步7路并发提取简历,返回与两阶段方案完全一致的结构化数据
text: 简历纯文本全文(PyMuPDF/docx/txt 提取,保留原始换行)。 text: 简历纯文本全文(PyMuPDF/docx/txt 提取,保留原始换行)。
""" """
log.info(f"一步6路并发提取开始,文本字符数: {len(text)}") log.info(f"一步7路并发提取开始,文本字符数: {len(text)}")
start = time.perf_counter() start = time.perf_counter()
inp = {"text": text} inp = {"text": text}
@@ -116,7 +118,7 @@ async def extract_all(text: str) -> dict:
result[module] = _clean_records(raw, label) result[module] = _clean_records(raw, label)
log.info( log.info(
f"一步6路并发提取完成,总耗时: {time.perf_counter() - start:.2f}s - " f"一步7路并发提取完成,总耗时: {time.perf_counter() - start:.2f}s - "
+ " ".join(f"{label}:{len(result[module])}" for module, _, label in _sub_chains) + " ".join(f"{label}:{len(result[module])}" for module, _, label in _sub_chains)
) )
return result return result
+31 -5
View File
@@ -1,6 +1,6 @@
"""简历一步并发提取 Prompt """简历一步并发提取 Prompt
6 路并发,每路独立负责一个模块(个人信息 / 教育 / 工作 / 实习 / 项目 / 竞赛), 7 路并发,每路独立负责一个模块(个人信息 / 教育 / 工作 / 实习 / 项目 / 竞赛 / 社团组织),
各自基于简历全文一次性输出该模块的**全部内容**(短字段 + description),无第二阶段。 各自基于简历全文一次性输出该模块的**全部内容**(短字段 + description),无第二阶段。
与两阶段方案的差异: 与两阶段方案的差异:
@@ -16,11 +16,13 @@ description/summary 输出格式:字符串数组 list[str],每个元素代
- 不主动改写、润色、扩写、编造内容;没有内容则返回空数组 []。 - 不主动改写、润色、扩写、编造内容;没有内容则返回空数组 []。
模块归属总原则(防同一内容被多个模块重复收录): 模块归属总原则(防同一内容被多个模块重复收录):
- 原文中的**每一段经历只能归属一个模块**。一条内容要么是工作、要么是实习、要么是项目、要么是竞赛,不能同时出现在两个模块。 - 原文中的**每一段经历只能归属一个模块**。一条内容要么是工作、要么是实习、要么是项目、要么是竞赛、要么是社团组织,不能同时出现在两个模块。
- 工作/实习互为补集:明确标注实习的只归实习模块,未标注实习的只归工作模块,两边都不重复收。 - 工作/实习互为补集:明确标注实习的只归实习模块,未标注实习的只归工作模块,两边都不重复收。
- 工作/实习条目内部所描述的职责、成果、子项目,属于该**工作/实习**记录本身的描述,**不要**再把它们抽成独立的"项目"记录。 - 工作/实习条目内部所描述的职责、成果、子项目,属于该**工作/实习**记录本身的描述,**不要**再把它们抽成独立的"项目"记录。
- 只有出现在**独立"项目/项目经历"板块**下、有自己标题的条目,才算项目记录。 - 只有出现在**独立"项目/项目经历"板块**下、有自己标题的条目,才算项目记录。
- "做了作品/项目 + 参加比赛获奖"这类既像项目又像竞赛的条目(如机器人竞赛、创新创业大赛作品),**只归竞赛**,不要在项目里重复。 - "做了作品/项目 + 参加比赛获奖"这类既像项目又像竞赛的条目(如机器人竞赛、创新创业大赛作品),**只归竞赛**,不要在项目里重复。
- 社团/学生组织任职、校园活动、志愿服务/公益,**只归社团组织模块**,教育/工作/实习/项目/竞赛五个模块一律不收。
- 社团名义参加的竞赛仍**只归竞赛**;社团组织模块只收非竞赛的组织与活动经历。
各模块通用铁律(防跨记录串扰/重复): 各模块通用铁律(防跨记录串扰/重复):
- 每条记录只装真正属于它本身的内容;即使原文中相邻的内容属于**另一条**记录 - 每条记录只装真正属于它本身的内容;即使原文中相邻的内容属于**另一条**记录
@@ -60,16 +62,18 @@ _DESC_RULE = """description 输出规范:为字符串数组,**每个元素
PROFILE_PROMPT = """严格根据简历原文提取,不要猜测或编造,没有的填null。 PROFILE_PROMPT = """严格根据简历原文提取,不要猜测或编造,没有的填null。
输入为简历纯文本全文。从中提取个人基本信息、技能标签、证书和自我评价,输出JSON: 输入为简历纯文本全文。从中提取个人基本信息、技能标签、证书和自我评价,输出JSON:
```json ```json
{{ "name": "姓名", "email": "邮箱", "mobileNumber": "手机号", "city": "所在城市", "wechatNumber": "微信号", "portfolioUrl": "作品集链接", "skills": ["技能1"], "certificates": ["证书1"], "summary": ["自我评价段落1", "自我评价段落2"] }} {{ "name": "姓名", "email": "邮箱", "mobileNumber": "手机号", "city": "所在城市", "wechatNumber": "微信号", "portfolioUrl": "作品集链接", "hobbies": "兴趣爱好", "languageSkills": "语言能力", "skills": ["技能1"], "certificates": ["证书1"], "summary": ["自我评价段落1", "自我评价段落2"] }}
``` ```
规则: 规则:
- 只提取以上9个字段,**不要提取任何经历**(教育/工作/实习/项目/竞赛由其它模块单独负责)。前6个字段没有的填null,后3个数组字段没有的填[]。 - 只提取以上11个字段,**不要提取任何经历**(教育/工作/实习/项目/竞赛/社团组织由其它模块单独负责)。前8个字段没有的填null,后3个数组字段没有的填[]。
- name:填完整姓名,原文怎么写就怎么填,不要截断或只取一个字。 - name:填完整姓名,原文怎么写就怎么填,不要截断或只取一个字。
- mobileNumber:只填**一个**手机号(通常11位数字),若原文出现多个手机号只取第一个,绝不能把多个号码拼接或用逗号/顿号等连接填入;微信号必须填到wechatNumber,绝不能把微信号当手机号。 - mobileNumber:只填**一个**手机号(通常11位数字),若原文出现多个手机号只取第一个,绝不能把多个号码拼接或用逗号/顿号等连接填入;微信号必须填到wechatNumber,绝不能把微信号当手机号。
- portfolioUrl:只填作品集/个人主页/GitHub 等链接。 - portfolioUrl:只填作品集/个人主页/GitHub 等链接。
- hobbies:**字符串**(不是数组)。填原文中兴趣爱好/爱好/个人兴趣板块的内容,原文怎么写就怎么填,多项之间保留原文的分隔写法。不要把技能、证书、社团经历当作兴趣爱好;没有填null。
- languageSkills**字符串**(不是数组)。填原文中明确写出的语言能力描述(如"英语(流利)、日语 N2"),原文怎么写就怎么填。标准化考试成绩(CET-4/6、雅思、托福、GRE 等)归 certificates,**不要**重复填到本字段;原文没有语言能力描述则填null。
- skills:仅当简历中有明确的"技能"/"专业技能"等**独立模块**时才提取,按原文逐字填,最多10个;如果没有专门的技能模块,填[]。**不要**从工作/项目/自我评价里归纳技能,尤其不要把"团队沟通""抗压能力""责任心"等软素质描述当作技能。**证书/语言等级类(如CET-4/6、雅思、托福、GRE、计算机等级、教师资格证等)一律不计入 skills,它们只归 certificates。** - skills:仅当简历中有明确的"技能"/"专业技能"等**独立模块**时才提取,按原文逐字填,最多10个;如果没有专门的技能模块,填[]。**不要**从工作/项目/自我评价里归纳技能,尤其不要把"团队沟通""抗压能力""责任心"等软素质描述当作技能。**证书/语言等级类(如CET-4/6、雅思、托福、GRE、计算机等级、教师资格证等)一律不计入 skills,它们只归 certificates。**
- certificates:填真正的**证书/证件/标准化考试成绩**,包括语言与等级类(如CET-4/6、雅思、托福、GRE、计算机等级、教师资格证、驾照等)。**不要**把竞赛奖项、荣誉称号、名次、奖学金,或工作描述里顺带提到的"认证"句子当作证书;没有填[]。 - certificates:填真正的**证书/证件/标准化考试成绩**,包括语言与等级类(如CET-4/6、雅思、托福、GRE、计算机等级、教师资格证、驾照等)。**不要**把竞赛奖项、荣誉称号、名次、奖学金,或工作描述里顺带提到的"认证"句子当作证书;没有填[]。
- summary:自我评价/个人概述**正文**的字符串数组,按原文分段拆成数组元素,一个段落一个元素。不要包含板块标题(如"个人优势""自我评价"),不要把技能列表、兴趣爱好、经历内容纳入;允许轻度清理(去除乱码/水印碎片),不要改写或编造;没有填[]。 - summary:自我评价/个人概述**正文**的字符串数组,按原文分段拆成数组元素,一个段落一个元素。不要包含板块标题(如"个人优势""自我评价"),不要把技能列表、兴趣爱好、语言能力、经历内容纳入(兴趣爱好归 hobbies,语言能力归 languageSkills;允许轻度清理(去除乱码/水印碎片),不要改写或编造;没有填[]。
只输出JSON,不要输出任何解释文字。""" 只输出JSON,不要输出任何解释文字。"""
# ==================== 教育 ==================== # ==================== 教育 ====================
@@ -167,3 +171,25 @@ COMPETITION_PROMPT = """严格根据简历原文提取,不要猜测或编造
字段规则:短字段直接填值,时间格式YYYY.MM,没有的填null。 字段规则:短字段直接填值,时间格式YYYY.MM,没有的填null。
description 填该竞赛/获奖的额外描述内容。 description 填该竞赛/获奖的额外描述内容。
""" + _DESC_RULE """ + _DESC_RULE
# ==================== 社团组织 ====================
ORGANIZATION_PROMPT = """严格根据简历原文提取,不要猜测或编造。输入为简历纯文本全文。
从中提取**全部社团/学生组织/校园活动/志愿服务**经历,按原文出现顺序输出JSON数组,每个元素为一条社团组织记录:
```json
[{{ "organizationName": "社团/组织名称", "role": "担任角色", "startDate": "2022.04", "endDate": "2023.06", "description": ["描述段落1", "描述段落2"] }}]
```
记录选取规则:
1. 只收**非任职、非竞赛**的组织类经历,包括:学生会/社团/协会/学生组织任职,校园活动的组织与参与,志愿服务/公益/支教,学校或院系的非学业性组织工作。
2. **绝对不要**把正式工作、实习经历当作社团经历——凡是公司/企业/机构的任职(含实习),一律归工作或实习模块,这里不收。
3. **绝对不要**把竞赛/比赛/大赛条目当作社团经历(如"XX大赛""挑战杯""创新创业大赛"等以参赛/获奖为核心的条目)——这些归竞赛模块,即使该竞赛由某社团组织或以社团名义参加。
4. **绝对不要**把出现在独立"项目/项目经历"板块下、有自己标题的项目条目当作社团经历——这些归项目模块。
5. **绝对不要**把学历教育经历当作社团经历。
6. 同一个社团/组织的同一段任职只输出一条;若同一组织有多段不同职务/时间的经历,按段分别输出多条。
7. 单纯的兴趣爱好罗列(如"爱好篮球、摄影")不算社团组织经历,不要输出。
8. 没有则输出[]。
字段规则:短字段直接填值,时间格式YYYY.MM,至今则 endDate 填null,没有的填null。
organizationName 填社团/组织/活动的名称,原文怎么写就怎么填。
role 只填原文中**明确写出**的简短角色名(如"部长""统筹""志愿者");若原文没有写明,填null,**不要臆造或默认**(如不要凭空填"成员""负责人")。
description 填该社团/组织经历的职责与成果描述。
""" + _DESC_RULE
+3 -2
View File
@@ -94,7 +94,7 @@ AGENT_PLAN_PROMPT = """你是一个简历编辑助手。你的唯一职责是根
3. 修改主表:{{"type": "update", "module": "resume", "instruction": "修改说明(30字内)"}} 3. 修改主表:{{"type": "update", "module": "resume", "instruction": "修改说明(30字内)"}}
4. 新增记录:{{"type": "add", "module": "模块名", "instruction": "新增说明(30字内)"}} 4. 新增记录:{{"type": "add", "module": "模块名", "instruction": "新增说明(30字内)"}}
模块名可选:resume(主表,包含 name、email、mobileNumber、city、wechatNumber、portfolioUrl、skills、certificates、summary、avatarUrl)、education(教育)、work(工作)、internship(实习)、project(项目)、competition(竞赛) 模块名可选:resume(主表,包含 name、email、mobileNumber、city、wechatNumber、portfolioUrl、skills、certificates、summary、avatarUrl、hobbies、languageSkills)、education(教育)、work(工作)、internship(实习)、project(项目)、competition(竞赛)、organization(社团组织)
规则: 规则:
1. 非简历修改相关的指令一律拒绝,返回固定话术,不要尝试回答或引导 1. 非简历修改相关的指令一律拒绝,返回固定话术,不要尝试回答或引导
@@ -154,10 +154,11 @@ AGENT_MODULE_ADD_PROMPT = """你是一个简历编辑助手。根据要求,生
# 各模块数据结构定义(传入 prompt 的 module_schema # 各模块数据结构定义(传入 prompt 的 module_schema
MODULE_SCHEMAS: dict[str, str] = { MODULE_SCHEMAS: dict[str, str] = {
"resume": '{ "avatarUrl": "string", "name": "string", "email": "string", "mobileNumber": "string", "city": "string", "wechatNumber": "string", "portfolioUrl": "string", "skills": ["string"], "certificates": ["string"], "summary": "string" }', "resume": '{ "avatarUrl": "string", "name": "string", "email": "string", "mobileNumber": "string", "city": "string", "wechatNumber": "string", "portfolioUrl": "string", "skills": ["string"], "certificates": ["string"], "summary": "string", "hobbies": "string", "languageSkills": "string" }',
"education": '[{ "id": "string(8位)", "school": "string", "major": "string", "degree": "大专/本科/硕士/博士", "studyType": "全日制/非全日制", "startDate": "2023.09", "endDate": "2024.06", "description": [{"id": "string(8位)", "text": "string"}] }]', "education": '[{ "id": "string(8位)", "school": "string", "major": "string", "degree": "大专/本科/硕士/博士", "studyType": "全日制/非全日制", "startDate": "2023.09", "endDate": "2024.06", "description": [{"id": "string(8位)", "text": "string"}] }]',
"work": '[{ "id": "string(8位)", "companyName": "string", "position": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }]', "work": '[{ "id": "string(8位)", "companyName": "string", "position": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }]',
"internship": '[{ "id": "string(8位)", "companyName": "string", "position": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }]', "internship": '[{ "id": "string(8位)", "companyName": "string", "position": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }]',
"project": '[{ "id": "string(8位)", "companyName": "string", "projectName": "string", "role": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }]', "project": '[{ "id": "string(8位)", "companyName": "string", "projectName": "string", "role": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }]',
"competition": '[{ "id": "string(8位)", "competitionName": "string", "award": "string", "awardDate": "2023.07", "description": [{"id": "string(8位)", "text": "string"}] }]', "competition": '[{ "id": "string(8位)", "competitionName": "string", "award": "string", "awardDate": "2023.07", "description": [{"id": "string(8位)", "text": "string"}] }]',
"organization": '[{ "id": "string(8位)", "organizationName": "string", "role": "string", "startDate": "2022.04", "endDate": "2023.06", "description": [{"id": "string(8位)", "text": "string"}] }]',
} }
+1 -1
View File
@@ -25,7 +25,7 @@ async def match_score(param: MatchScoreParam):
@router.post("/optimize-resume", summary="针对岗位优化简历") @router.post("/optimize-resume", summary="针对岗位优化简历")
async def optimize_resume(param: OptimizeResumeParam, _: None = Depends(func_permission("resume_custom"))): async def optimize_resume(param: OptimizeResumeParam, _: None = Depends(func_permission("resume_custom"))):
"""根据目标岗位,AI并发优化简历(summary + 5张子表经历),存Redis并返回""" """根据目标岗位,AI并发优化简历(summary + 6张子表经历),存Redis并返回"""
user_id = RequestContext.user_id.get() user_id = RequestContext.user_id.get()
async for session in get_db(): async for session in get_db():
service = JobAgentChatService(session) service = JobAgentChatService(session)
+1 -1
View File
@@ -17,7 +17,7 @@ class ResumeDiagnosisIssue(Base):
report_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="关联report.id") 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") 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") 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_type: Mapped[str] = mapped_column(String(32), nullable=False, comment="模块类型: summary/education/work/internship/project/competition/organization")
module_record_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="模块记录IDsummary时为resume_id") module_record_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="模块记录IDsummary时为resume_id")
finding: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="诊断发现") finding: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="诊断发现")
importance: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="为什么重要") importance: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="为什么重要")
+2
View File
@@ -35,6 +35,8 @@ class UserResume(Base):
skills: Mapped[Optional[list]] = mapped_column(JSON, nullable=True, comment="技能标签列表") skills: Mapped[Optional[list]] = mapped_column(JSON, nullable=True, comment="技能标签列表")
certificates: Mapped[Optional[list]] = mapped_column(JSON, nullable=True, comment="证书标签列表") certificates: Mapped[Optional[list]] = mapped_column(JSON, nullable=True, comment="证书标签列表")
summary: Mapped[Optional[str]] = mapped_column(String(2000), nullable=True, comment="个人概述") summary: Mapped[Optional[str]] = mapped_column(String(2000), nullable=True, comment="个人概述")
hobbies: Mapped[Optional[str]] = mapped_column(String(2000), nullable=True, comment="兴趣爱好")
language_skills: Mapped[Optional[str]] = mapped_column(String(2000), nullable=True, comment="语言能力")
create_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, 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="更新时间") update_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
+26
View File
@@ -0,0 +1,26 @@
"""简历-社团组织经历表(bg_user_resume_organization"""
from datetime import datetime
from typing import Optional
from sqlalchemy import BigInteger, Integer, String, DateTime, JSON
from sqlalchemy.orm import Mapped, mapped_column
from app.core.database import Base
class UserResumeOrganization(Base):
"""简历-社团组织经历表 bg_user_resume_organization"""
__tablename__ = "bg_user_resume_organization"
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")
organization_name: Mapped[Optional[str]] = mapped_column(String(128), nullable=True, comment="社团/组织名称")
role: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, comment="担任角色")
start_date: Mapped[Optional[str]] = mapped_column(String(16), nullable=True, comment="开始时间,格式:2023.06")
end_date: Mapped[Optional[str]] = mapped_column(String(16), nullable=True, comment="结束时间,格式:2023.09")
description: Mapped[Optional[list]] = mapped_column(JSON, nullable=True, comment="描述段落 [{id, text}]")
sort_order: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, 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="更新时间")
+12
View File
@@ -28,6 +28,8 @@ class ResumeProfile(_AliasModel):
skills: list[str] = Field(default_factory=list) skills: list[str] = Field(default_factory=list)
certificates: list[str] = Field(default_factory=list) certificates: list[str] = Field(default_factory=list)
summary: str = Field(default="") summary: str = Field(default="")
hobbies: str = Field(default="")
language_skills: str = Field(default="", alias="languageSkills")
class Education(_AliasModel): class Education(_AliasModel):
@@ -77,6 +79,15 @@ class Competition(_AliasModel):
description: list[Paragraph] = Field(default_factory=list) description: list[Paragraph] = Field(default_factory=list)
class Organization(_AliasModel):
id: str = Field(default="")
organization_name: str = Field(default="", alias="organizationName")
role: str = Field(default="")
start_date: str = Field(default="", alias="startDate")
end_date: str = Field(default="", alias="endDate")
description: list[Paragraph] = Field(default_factory=list)
class CustomizeResume(_AliasModel): class CustomizeResume(_AliasModel):
"""定制简历结构""" """定制简历结构"""
resume: ResumeProfile = Field(default_factory=ResumeProfile) resume: ResumeProfile = Field(default_factory=ResumeProfile)
@@ -85,3 +96,4 @@ class CustomizeResume(_AliasModel):
internship: list[Internship] = Field(default_factory=list) internship: list[Internship] = Field(default_factory=list)
project: list[Project] = Field(default_factory=list) project: list[Project] = Field(default_factory=list)
competition: list[Competition] = Field(default_factory=list) competition: list[Competition] = Field(default_factory=list)
organization: list[Organization] = Field(default_factory=list)
+7 -1
View File
@@ -95,7 +95,11 @@ class BrowserPlugService:
parts.append(f"证书:{''.join(resume['certificates'])}") parts.append(f"证书:{''.join(resume['certificates'])}")
if resume.get("summary"): if resume.get("summary"):
parts.append(f"个人概述:{resume['summary']}") parts.append(f"个人概述:{resume['summary']}")
for section, title in [("education", "教育经历"), ("work", "工作经历"), ("internship", "实习经历"), ("project", "项目经历"), ("competition", "竞赛经历")]: if resume.get("hobbies"):
parts.append(f"兴趣爱好:{resume['hobbies']}")
if resume.get("languageSkills"):
parts.append(f"语言能力:{resume['languageSkills']}")
for section, title in [("education", "教育经历"), ("work", "工作经历"), ("internship", "实习经历"), ("project", "项目经历"), ("competition", "竞赛经历"), ("organization", "社团组织经历")]:
items = data.get(section, []) items = data.get(section, [])
if items: if items:
parts.append(f"{title}") parts.append(f"{title}")
@@ -108,4 +112,6 @@ class BrowserPlugService:
parts.append(f" - {item.get('projectName', '')} {item.get('role', '')}") parts.append(f" - {item.get('projectName', '')} {item.get('role', '')}")
elif section == "competition": elif section == "competition":
parts.append(f" - {item.get('competitionName', '')} {item.get('award', '')}") parts.append(f" - {item.get('competitionName', '')} {item.get('award', '')}")
elif section == "organization":
parts.append(f" - {item.get('organizationName', '')} {item.get('role', '')}")
return "\n".join(parts) if parts else "暂无简历信息" return "\n".join(parts) if parts else "暂无简历信息"
+6 -2
View File
@@ -13,7 +13,7 @@ from app.core.database import get_db
from app.core.redis import RedisManager from app.core.redis import RedisManager
from app.models.user_job_customize_resume import UserJobCustomizeResume from app.models.user_job_customize_resume import UserJobCustomizeResume
from app.schemas.customize_resume import ( from app.schemas.customize_resume import (
CustomizeResume, ResumeProfile, Education, Work, Internship, Project, Competition, Paragraph, CustomizeResume, ResumeProfile, Education, Work, Internship, Project, Competition, Organization, Paragraph,
) )
from app.services.resume_loader import ResumeDetail, load_default_resume_detail from app.services.resume_loader import ResumeDetail, load_default_resume_detail
from app.tool.snowflake import next_id from app.tool.snowflake import next_id
@@ -46,6 +46,7 @@ def build_from_detail(detail: ResumeDetail) -> CustomizeResume:
wechatNumber=resume.wechat_number or "", portfolioUrl=resume.portfolio_url or "", wechatNumber=resume.wechat_number or "", portfolioUrl=resume.portfolio_url or "",
skills=resume.skills or [], certificates=resume.certificates or [], skills=resume.skills or [], certificates=resume.certificates or [],
summary=resume.summary or "", summary=resume.summary or "",
hobbies=resume.hobbies or "", languageSkills=resume.language_skills or "",
) )
return CustomizeResume( return CustomizeResume(
resume=profile, resume=profile,
@@ -65,6 +66,9 @@ def build_from_detail(detail: ResumeDetail) -> CustomizeResume:
competition=[Competition(id=_rand_id(), competitionName=r.competition_name or "", award=r.award or "", competition=[Competition(id=_rand_id(), competitionName=r.competition_name or "", award=r.award or "",
awardDate=r.award_date or "", awardDate=r.award_date or "",
description=_build_paragraphs(r.description)) for r in detail.competition], description=_build_paragraphs(r.description)) for r in detail.competition],
organization=[Organization(id=_rand_id(), organizationName=r.organization_name or "", role=r.role or "",
startDate=r.start_date or "", endDate=r.end_date or "",
description=_build_paragraphs(r.description)) for r in detail.organization],
) )
@@ -104,7 +108,7 @@ async def get_by_id(user_id: int, customize_resume_id: int) -> CustomizeResume |
async def get(user_id: int, job_id: int) -> dict | None: async def get(user_id: int, job_id: int) -> dict | None:
"""查询定制简历,查不到则加载默认简历构建返回 """查询定制简历,查不到则加载默认简历构建返回
返回结构:{id, resumeId, resumeName, resume, education, work, internship, project, competition} 返回结构:{id, resumeId, resumeName, resume, education, work, internship, project, competition, organization}
fallback 默认简历时:id=None, resumeId=默认简历id, resumeName=原简历名 fallback 默认简历时:id=None, resumeId=默认简历id, resumeName=原简历名
""" """
async for session in get_db(): async for session in get_db():
+15 -5
View File
@@ -2,7 +2,7 @@
主要功能:针对岗位并发优化简历;岗位匹配度评分。 主要功能:针对岗位并发优化简历;岗位匹配度评分。
依赖:resume_loader(简历统一查询)、customize_resume_store(定制简历存取+构建)、job_agent.resume_optimizer(岗位简历优化)、job_agent.match_scorer(匹配评分) 依赖:resume_loader(简历统一查询)、customize_resume_store(定制简历存取+构建)、job_agent.resume_optimizer(岗位简历优化)、job_agent.match_scorer(匹配评分)
使用表:bg_user_resume + 5张子表(通过 resume_loader 查询)、bg_job(查岗位)、bg_user_job_customize_resume(定制简历) 使用表:bg_user_resume + 6张子表(通过 resume_loader 查询)、bg_job(查岗位)、bg_user_job_customize_resume(定制简历)
""" """
import asyncio import asyncio
@@ -16,7 +16,7 @@ from app.ai.job_agent.resume_optimizer import optimize_summary, optimize_experie
from app.ai.job_agent.match_scorer import score_match as ai_score_match from app.ai.job_agent.match_scorer import score_match as ai_score_match
from app.core.logger import log from app.core.logger import log
from app.models.job import Job from app.models.job import Job
from app.schemas.customize_resume import CustomizeResume, Education, Work, Internship, Project, Competition from app.schemas.customize_resume import CustomizeResume, Education, Work, Internship, Project, Competition, Organization
from app.services.resume_loader import load_resume_detail from app.services.resume_loader import load_resume_detail
from app.services import customize_resume_store from app.services import customize_resume_store
@@ -54,6 +54,10 @@ class JobAgentChatService:
parts.append(f"证书:{''.join(r.certificates)}") parts.append(f"证书:{''.join(r.certificates)}")
if r.summary: if r.summary:
parts.append(f"个人概述:{r.summary}") parts.append(f"个人概述:{r.summary}")
if r.hobbies:
parts.append(f"兴趣爱好:{r.hobbies}")
if r.language_skills:
parts.append(f"语言能力:{r.language_skills}")
if cr.education: if cr.education:
parts.append("教育经历:") parts.append("教育经历:")
for e in cr.education: for e in cr.education:
@@ -77,6 +81,11 @@ class JobAgentChatService:
parts.append("竞赛经历:") parts.append("竞赛经历:")
for c in cr.competition: for c in cr.competition:
parts.append(f" - {c.competition_name} {c.award}".rstrip()) parts.append(f" - {c.competition_name} {c.award}".rstrip())
if cr.organization:
parts.append("社团组织经历:")
for o in cr.organization:
parts.append(f" - {o.organization_name} {o.role}".rstrip())
parts.extend(f" {p.text}" for p in o.description if p.text)
return "\n".join(parts) if parts else "暂无简历信息" return "\n".join(parts) if parts else "暂无简历信息"
async def optimize_resume(self, user_id: int, resume_id: int, job_id: int) -> dict: async def optimize_resume(self, user_id: int, resume_id: int, job_id: int) -> dict:
@@ -117,7 +126,8 @@ class JobAgentChatService:
"""构建各子表的 AI 优化任务列表,按单条记录拆分""" """构建各子表的 AI 优化任务列表,按单条记录拆分"""
result: list[tuple[str, int, str]] = [] result: list[tuple[str, int, str]] = []
for name, items in [("education", cr.education), ("work", cr.work), ("internship", cr.internship), for name, items in [("education", cr.education), ("work", cr.work), ("internship", cr.internship),
("project", cr.project), ("competition", cr.competition)]: ("project", cr.project), ("competition", cr.competition),
("organization", cr.organization)]:
for idx, item in enumerate(items or []): for idx, item in enumerate(items or []):
result.append((name, idx, json.dumps(item.model_dump(by_alias=True), ensure_ascii=False))) result.append((name, idx, json.dumps(item.model_dump(by_alias=True), ensure_ascii=False)))
return result return result
@@ -136,8 +146,8 @@ class JobAgentChatService:
if key == "summary" and isinstance(result, str): if key == "summary" and isinstance(result, str):
cr.resume.summary = result cr.resume.summary = result
return return
model_map = {"education": Education, "work": Work, "internship": Internship, "project": Project, "competition": Competition} model_map = {"education": Education, "work": Work, "internship": Internship, "project": Project, "competition": Competition, "organization": Organization}
list_map = {"education": cr.education, "work": cr.work, "internship": cr.internship, "project": cr.project, "competition": cr.competition} list_map = {"education": cr.education, "work": cr.work, "internship": cr.internship, "project": cr.project, "competition": cr.competition, "organization": cr.organization}
model_cls = model_map.get(key) model_cls = model_map.get(key)
items = list_map.get(key) items = list_map.get(key)
if model_cls is None or items is None: if model_cls is None or items is None:
+9 -1
View File
@@ -2,7 +2,7 @@
主要功能:查询简历数据 + 查询岗位(可选),调用 AI 模块完成对话。 主要功能:查询简历数据 + 查询岗位(可选),调用 AI 模块完成对话。
依赖:resume_loader(简历统一查询)、nova_chat AI 模块 依赖:resume_loader(简历统一查询)、nova_chat AI 模块
使用表:bg_user_resume + 5张子表(通过 resume_loader 查询)、bg_job(查岗位,可选) 使用表:bg_user_resume + 6张子表(通过 resume_loader 查询)、bg_job(查岗位,可选)
""" """
from typing import Optional from typing import Optional
@@ -60,6 +60,10 @@ class NovaChatService:
parts.append(f"证书:{''.join(resume.certificates)}") parts.append(f"证书:{''.join(resume.certificates)}")
if resume.summary: if resume.summary:
parts.append(f"个人概述:{resume.summary}") parts.append(f"个人概述:{resume.summary}")
if resume.hobbies:
parts.append(f"兴趣爱好:{resume.hobbies}")
if resume.language_skills:
parts.append(f"语言能力:{resume.language_skills}")
if detail.education: if detail.education:
parts.append("教育经历:") parts.append("教育经历:")
for r in detail.education: for r in detail.education:
@@ -80,4 +84,8 @@ class NovaChatService:
parts.append("竞赛经历:") parts.append("竞赛经历:")
for r in detail.competition: for r in detail.competition:
parts.append(f" - {r.competition_name or ''} {r.award or ''}") parts.append(f" - {r.competition_name or ''} {r.award or ''}")
if detail.organization:
parts.append("社团组织经历:")
for r in detail.organization:
parts.append(f" - {r.organization_name or ''} {r.role or ''}")
return "\n".join(parts) if parts else "暂无简历信息" return "\n".join(parts) if parts else "暂无简历信息"
+5 -2
View File
@@ -2,7 +2,7 @@
加载简历描述数据 → 并行 AI 诊断 → 统计评级 → AI 汇总评价 → 写入数据库。 加载简历描述数据 → 并行 AI 诊断 → 统计评级 → AI 汇总评价 → 写入数据库。
依赖:resume_diagnoserAI诊断引擎) 依赖:resume_diagnoserAI诊断引擎)
使用表:bg_user_resume + 5张子表(读)、bg_resume_diagnosis_report + issue(写) 使用表:bg_user_resume + 6张子表(读)、bg_resume_diagnosis_report + issue(写)
""" """
import json import json
@@ -21,6 +21,7 @@ from app.tool.snowflake import next_id
_MODULE_LABELS = { _MODULE_LABELS = {
"summary": "个人概述", "education": "教育经历", "work": "工作经历", "summary": "个人概述", "education": "教育经历", "work": "工作经历",
"internship": "实习经历", "project": "项目经历", "competition": "竞赛经历", "internship": "实习经历", "project": "项目经历", "competition": "竞赛经历",
"organization": "社团组织经历",
} }
@@ -30,7 +31,7 @@ class ResumeDiagnoseService:
self.session = session self.session = session
async def load_resume_data(self, resume_id: int, user_id: int) -> tuple[UserResume, list[dict]]: async def load_resume_data(self, resume_id: int, user_id: int) -> tuple[UserResume, list[dict]]:
"""加载简历主表 + 5 张子表数据,组装 AI 任务列表""" """加载简历主表 + 6 张子表数据,组装 AI 任务列表"""
detail = await load_resume_detail(self.session, resume_id, user_id) detail = await load_resume_detail(self.session, resume_id, user_id)
resume = detail.resume resume = detail.resume
@@ -57,6 +58,8 @@ class ResumeDiagnoseService:
lambda r: f"公司: {r.company_name or ''}, 项目: {r.project_name or ''}, 角色: {r.role or ''}") lambda r: f"公司: {r.company_name or ''}, 项目: {r.project_name or ''}, 角色: {r.role or ''}")
self._collect_tasks(tasks, target_position, "competition", detail.competition, self._collect_tasks(tasks, target_position, "competition", detail.competition,
lambda r: f"竞赛: {r.competition_name or ''}, 获奖: {r.award or ''}") lambda r: f"竞赛: {r.competition_name or ''}, 获奖: {r.award or ''}")
self._collect_tasks(tasks, target_position, "organization", detail.organization,
lambda r: f"社团/组织: {r.organization_name or ''}, 角色: {r.role or ''}")
return resume, tasks return resume, tasks
@staticmethod @staticmethod
+13 -10
View File
@@ -1,6 +1,6 @@
"""简历统一查询模块 """简历统一查询模块
提供简历主表 + 5张子表的统一查询能力,返回脱离 session 的 ResumeDetail dataclass。 提供简历主表 + 6张子表的统一查询能力,返回脱离 session 的 ResumeDetail dataclass。
各 Service 统一复用,避免重复查询逻辑。 各 Service 统一复用,避免重复查询逻辑。
""" """
@@ -15,31 +15,33 @@ from app.models.user_resume_work import UserResumeWork
from app.models.user_resume_internship import UserResumeInternship from app.models.user_resume_internship import UserResumeInternship
from app.models.user_resume_project import UserResumeProject from app.models.user_resume_project import UserResumeProject
from app.models.user_resume_competition import UserResumeCompetition from app.models.user_resume_competition import UserResumeCompetition
from app.models.user_resume_organization import UserResumeOrganization
@dataclass @dataclass
class ResumeDetail: class ResumeDetail:
"""简历完整数据,主表 + 5张子表""" """简历完整数据,主表 + 6张子表"""
resume: UserResume resume: UserResume
education: list[UserResumeEducation] = field(default_factory=list) education: list[UserResumeEducation] = field(default_factory=list)
work: list[UserResumeWork] = field(default_factory=list) work: list[UserResumeWork] = field(default_factory=list)
internship: list[UserResumeInternship] = field(default_factory=list) internship: list[UserResumeInternship] = field(default_factory=list)
project: list[UserResumeProject] = field(default_factory=list) project: list[UserResumeProject] = field(default_factory=list)
competition: list[UserResumeCompetition] = field(default_factory=list) competition: list[UserResumeCompetition] = field(default_factory=list)
organization: list[UserResumeOrganization] = field(default_factory=list)
async def load_resume_detail(session: AsyncSession, resume_id: int, user_id: int) -> ResumeDetail: async def load_resume_detail(session: AsyncSession, resume_id: int, user_id: int) -> ResumeDetail:
"""按ID查简历主表(校验归属)+ 5张子表,返回 ResumeDetail""" """按ID查简历主表(校验归属)+ 6张子表,返回 ResumeDetail"""
result = await session.execute(select(UserResume).where(UserResume.id == resume_id, UserResume.user_id == user_id)) result = await session.execute(select(UserResume).where(UserResume.id == resume_id, UserResume.user_id == user_id))
resume = result.scalar_one_or_none() resume = result.scalar_one_or_none()
if not resume: if not resume:
raise ValueError("简历不存在") raise ValueError("简历不存在")
edu, work, intern, proj, comp = await _load_sub_tables(session, resume_id) edu, work, intern, proj, comp, org = await _load_sub_tables(session, resume_id)
return ResumeDetail(resume=resume, education=edu, work=work, internship=intern, project=proj, competition=comp) return ResumeDetail(resume=resume, education=edu, work=work, internship=intern, project=proj, competition=comp, organization=org)
async def load_default_resume_detail(session: AsyncSession, user_id: int) -> ResumeDetail: async def load_default_resume_detail(session: AsyncSession, user_id: int) -> ResumeDetail:
"""自动选默认简历(先默认再最新)+ 5张子表,返回 ResumeDetail""" """自动选默认简历(先默认再最新)+ 6张子表,返回 ResumeDetail"""
result = await session.execute( result = await session.execute(
select(UserResume).where(UserResume.user_id == user_id, UserResume.is_default == 1) select(UserResume).where(UserResume.user_id == user_id, UserResume.is_default == 1)
.order_by(desc(UserResume.update_time)).limit(1)) .order_by(desc(UserResume.update_time)).limit(1))
@@ -51,15 +53,16 @@ async def load_default_resume_detail(session: AsyncSession, user_id: int) -> Res
resume = result.scalar_one_or_none() resume = result.scalar_one_or_none()
if not resume: if not resume:
raise ValueError("请先创建简历") raise ValueError("请先创建简历")
edu, work, intern, proj, comp = await _load_sub_tables(session, resume.id) edu, work, intern, proj, comp, org = await _load_sub_tables(session, resume.id)
return ResumeDetail(resume=resume, education=edu, work=work, internship=intern, project=proj, competition=comp) return ResumeDetail(resume=resume, education=edu, work=work, internship=intern, project=proj, competition=comp, organization=org)
async def _load_sub_tables(session: AsyncSession, resume_id: int): async def _load_sub_tables(session: AsyncSession, resume_id: int):
"""查询简历5张子表""" """查询简历6张子表"""
edu = (await session.execute(select(UserResumeEducation).where(UserResumeEducation.resume_id == resume_id))).scalars().all() 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() 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() 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() 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() comp = (await session.execute(select(UserResumeCompetition).where(UserResumeCompetition.resume_id == resume_id))).scalars().all()
return edu, work, intern, proj, comp org = (await session.execute(select(UserResumeOrganization).where(UserResumeOrganization.resume_id == resume_id))).scalars().all()
return edu, work, intern, proj, comp, org
+12 -2
View File
@@ -2,7 +2,7 @@
上传简历文件 → 解析为纯文本 → AI 两阶段并行结构化 → 写入数据库。 上传简历文件 → 解析为纯文本 → AI 两阶段并行结构化 → 写入数据库。
依赖:resume_text_extractor(文件文本提取)、resume_extractorAI两阶段并行提取) 依赖:resume_text_extractor(文件文本提取)、resume_extractorAI两阶段并行提取)
使用表:bg_user_resume(主表)、bg_user_resume_education/work/internship/project/competition5张子表) 使用表:bg_user_resume(主表)、bg_user_resume_education/work/internship/project/competition/organization6张子表)
""" """
import shortuuid import shortuuid
@@ -15,6 +15,7 @@ from app.models.user_resume import UserResume
from app.models.user_resume_competition import UserResumeCompetition from app.models.user_resume_competition import UserResumeCompetition
from app.models.user_resume_education import UserResumeEducation from app.models.user_resume_education import UserResumeEducation
from app.models.user_resume_internship import UserResumeInternship from app.models.user_resume_internship import UserResumeInternship
from app.models.user_resume_organization import UserResumeOrganization
from app.models.user_resume_project import UserResumeProject from app.models.user_resume_project import UserResumeProject
from app.models.user_resume_work import UserResumeWork from app.models.user_resume_work import UserResumeWork
from app.tool.resume_text_extractor import extract_text from app.tool.resume_text_extractor import extract_text
@@ -45,7 +46,7 @@ class ResumeService:
return result return result
async def save_resume(self, session: AsyncSession, user_id: int, filename: str, parsed: dict) -> int: async def save_resume(self, session: AsyncSession, user_id: int, filename: str, parsed: dict) -> int:
"""将解析结果写入主表 + 5张子表,返回简历ID""" """将解析结果写入主表 + 6张子表,返回简历ID"""
resume_id = next_id() resume_id = next_id()
session.add(UserResume( session.add(UserResume(
@@ -57,6 +58,7 @@ class ResumeService:
wechat_number=parsed.get("wechatNumber"), portfolio_url=parsed.get("portfolioUrl"), wechat_number=parsed.get("wechatNumber"), portfolio_url=parsed.get("portfolioUrl"),
skills=parsed.get("skills") or [], certificates=parsed.get("certificates") or [], skills=parsed.get("skills") or [], certificates=parsed.get("certificates") or [],
summary=parsed.get("summary"), summary=parsed.get("summary"),
hobbies=parsed.get("hobbies"), language_skills=parsed.get("languageSkills"),
)) ))
for i, edu in enumerate(parsed.get("education") or []): for i, edu in enumerate(parsed.get("education") or []):
@@ -101,6 +103,14 @@ class ResumeService:
description=_to_paragraphs(comp.get("description")), sort_order=i, description=_to_paragraphs(comp.get("description")), sort_order=i,
)) ))
for i, org in enumerate(parsed.get("organization") or []):
session.add(UserResumeOrganization(
id=next_id(), resume_id=resume_id, user_id=user_id,
organization_name=org.get("organizationName"), role=org.get("role"),
start_date=org.get("startDate"), end_date=org.get("endDate"),
description=_to_paragraphs(org.get("description")), sort_order=i,
))
await session.flush() await session.flush()
log.info(f"简历保存完成,resumeId: {resume_id}") log.info(f"简历保存完成,resumeId: {resume_id}")
return resume_id return resume_id
+16 -9
View File
@@ -2,7 +2,7 @@
岗位技能差距分析 → 定制简历生成/查询/编辑/回滚 → AI 对话式编辑。 岗位技能差距分析 → 定制简历生成/查询/编辑/回滚 → AI 对话式编辑。
依赖:skill_gap_analyzerAI引擎) 依赖:skill_gap_analyzerAI引擎)
使用表:bg_job(读)、bg_user_resume + 5张子表(读) 使用表:bg_job(读)、bg_user_resume + 6张子表(读)
存储:Redis(定制简历 + 回滚数据) 存储:Redis(定制简历 + 回滚数据)
""" """
@@ -20,7 +20,7 @@ from app.ai.skill_gap_analyzer.analyzer import (
from app.ai.skill_gap_analyzer.prompts import MODULE_SCHEMAS from app.ai.skill_gap_analyzer.prompts import MODULE_SCHEMAS
from app.core.logger import log from app.core.logger import log
from app.schemas.customize_resume import ( from app.schemas.customize_resume import (
CustomizeResume, ResumeProfile, Education, Work, Internship, Project, Competition, CustomizeResume, ResumeProfile, Education, Work, Internship, Project, Competition, Organization,
) )
from app.models.job import Job from app.models.job import Job
from app.models.user_resume import UserResume from app.models.user_resume import UserResume
@@ -35,6 +35,7 @@ _MODULE_LABELS = {
"internship": "实习经历", "internship": "实习经历",
"project": "项目经历", "project": "项目经历",
"competition": "竞赛经历", "competition": "竞赛经历",
"organization": "社团组织经历",
} }
@@ -45,6 +46,8 @@ def _build_resume_json(detail: ResumeDetail) -> str:
"skills": resume.skills or [], "skills": resume.skills or [],
"certificates": resume.certificates or [], "certificates": resume.certificates or [],
"summary": resume.summary or "", "summary": resume.summary or "",
"hobbies": resume.hobbies or "",
"languageSkills": resume.language_skills or "",
"targetPosition": resume.target_position or "", "targetPosition": resume.target_position or "",
} }
if detail.education: if detail.education:
@@ -57,6 +60,8 @@ def _build_resume_json(detail: ResumeDetail) -> str:
data["project"] = [{"companyName": r.company_name, "projectName": r.project_name, "role": r.role, "description": r.description} for r in 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: if detail.competition:
data["competition"] = [{"competitionName": r.competition_name, "award": r.award, "description": r.description} for r in detail.competition] data["competition"] = [{"competitionName": r.competition_name, "award": r.award, "description": r.description} for r in detail.competition]
if detail.organization:
data["organization"] = [{"organizationName": r.organization_name, "role": r.role, "description": r.description} for r in detail.organization]
return json.dumps(data, ensure_ascii=False) return json.dumps(data, ensure_ascii=False)
@@ -141,7 +146,8 @@ class SkillGapService:
"""构建各子表的 AI 优化任务列表,按单条记录拆分""" """构建各子表的 AI 优化任务列表,按单条记录拆分"""
result: list[tuple[str, int, str]] = [] result: list[tuple[str, int, str]] = []
for name, items in [("education", cr.education), ("work", cr.work), ("internship", cr.internship), for name, items in [("education", cr.education), ("work", cr.work), ("internship", cr.internship),
("project", cr.project), ("competition", cr.competition)]: ("project", cr.project), ("competition", cr.competition),
("organization", cr.organization)]:
for idx, item in enumerate(items or []): for idx, item in enumerate(items or []):
result.append((name, idx, json.dumps(item.model_dump(by_alias=True), ensure_ascii=False))) result.append((name, idx, json.dumps(item.model_dump(by_alias=True), ensure_ascii=False)))
return result return result
@@ -152,8 +158,8 @@ class SkillGapService:
if key == "summary" and isinstance(result, str): if key == "summary" and isinstance(result, str):
cr.resume.summary = result cr.resume.summary = result
return return
model_map = {"education": Education, "work": Work, "internship": Internship, "project": Project, "competition": Competition} model_map = {"education": Education, "work": Work, "internship": Internship, "project": Project, "competition": Competition, "organization": Organization}
list_map = {"education": cr.education, "work": cr.work, "internship": cr.internship, "project": cr.project, "competition": cr.competition} list_map = {"education": cr.education, "work": cr.work, "internship": cr.internship, "project": cr.project, "competition": cr.competition, "organization": cr.organization}
model_cls = model_map.get(key) model_cls = model_map.get(key)
items = list_map.get(key) items = list_map.get(key)
if model_cls is None or items is None: if model_cls is None or items is None:
@@ -272,7 +278,7 @@ class SkillGapService:
return cr.resume.model_dump_json(by_alias=True) return cr.resume.model_dump_json(by_alias=True)
mapping = { mapping = {
"education": cr.education, "work": cr.work, "internship": cr.internship, "education": cr.education, "work": cr.work, "internship": cr.internship,
"project": cr.project, "competition": cr.competition, "project": cr.project, "competition": cr.competition, "organization": cr.organization,
} }
items = mapping.get(mod_name, []) items = mapping.get(mod_name, [])
if not record_id: if not record_id:
@@ -290,7 +296,7 @@ class SkillGapService:
return return
mapping = { mapping = {
"education": cr.education, "work": cr.work, "internship": cr.internship, "education": cr.education, "work": cr.work, "internship": cr.internship,
"project": cr.project, "competition": cr.competition, "project": cr.project, "competition": cr.competition, "organization": cr.organization,
} }
items = mapping.get(mod_name) items = mapping.get(mod_name)
if items is not None: if items is not None:
@@ -308,14 +314,14 @@ class SkillGapService:
return return
model_map = { model_map = {
"education": Education, "work": Work, "internship": Internship, "education": Education, "work": Work, "internship": Internship,
"project": Project, "competition": Competition, "project": Project, "competition": Competition, "organization": Organization,
} }
model_cls = model_map.get(mod_name) model_cls = model_map.get(mod_name)
if not model_cls or not isinstance(result, dict) or not record_id: if not model_cls or not isinstance(result, dict) or not record_id:
return return
list_map = { list_map = {
"education": cr.education, "work": cr.work, "internship": cr.internship, "education": cr.education, "work": cr.work, "internship": cr.internship,
"project": cr.project, "competition": cr.competition, "project": cr.project, "competition": cr.competition, "organization": cr.organization,
} }
items = list_map.get(mod_name, []) items = list_map.get(mod_name, [])
new_item = model_cls.model_validate(result) new_item = model_cls.model_validate(result)
@@ -336,6 +342,7 @@ class SkillGapService:
"internship": (Internship, cr.internship), "internship": (Internship, cr.internship),
"project": (Project, cr.project), "project": (Project, cr.project),
"competition": (Competition, cr.competition), "competition": (Competition, cr.competition),
"organization": (Organization, cr.organization),
} }
entry = model_map.get(mod_name) entry = model_map.get(mod_name)
if not entry or not isinstance(result, dict): if not entry or not isinstance(result, dict):