Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8051933dcf |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"}] }]',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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="模块记录ID,summary时为resume_id")
|
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="诊断发现")
|
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="为什么重要")
|
||||||
|
|||||||
@@ -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="更新时间")
|
||||||
|
|||||||
@@ -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="更新时间")
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 "暂无简历信息"
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 "暂无简历信息"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
加载简历描述数据 → 并行 AI 诊断 → 统计评级 → AI 汇总评价 → 写入数据库。
|
加载简历描述数据 → 并行 AI 诊断 → 统计评级 → AI 汇总评价 → 写入数据库。
|
||||||
依赖:resume_diagnoser(AI诊断引擎)
|
依赖:resume_diagnoser(AI诊断引擎)
|
||||||
使用表: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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
上传简历文件 → 解析为纯文本 → AI 两阶段并行结构化 → 写入数据库。
|
上传简历文件 → 解析为纯文本 → AI 两阶段并行结构化 → 写入数据库。
|
||||||
依赖:resume_text_extractor(文件文本提取)、resume_extractor(AI两阶段并行提取)
|
依赖:resume_text_extractor(文件文本提取)、resume_extractor(AI两阶段并行提取)
|
||||||
使用表:bg_user_resume(主表)、bg_user_resume_education/work/internship/project/competition(5张子表)
|
使用表:bg_user_resume(主表)、bg_user_resume_education/work/internship/project/competition/organization(6张子表)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
岗位技能差距分析 → 定制简历生成/查询/编辑/回滚 → AI 对话式编辑。
|
岗位技能差距分析 → 定制简历生成/查询/编辑/回滚 → AI 对话式编辑。
|
||||||
依赖:skill_gap_analyzer(AI引擎)
|
依赖:skill_gap_analyzer(AI引擎)
|
||||||
使用表: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):
|
||||||
|
|||||||
Reference in New Issue
Block a user