修改定制简历保存逻辑
This commit is contained in:
@@ -71,7 +71,8 @@ offerpie_python_ai/
|
|||||||
│ ├─ resume_diagnosis_report.py # 简历诊断报告表(bg_resume_diagnosis_report)
|
│ ├─ resume_diagnosis_report.py # 简历诊断报告表(bg_resume_diagnosis_report)
|
||||||
│ ├─ resume_diagnosis_issue.py # 简历诊断问题表(bg_resume_diagnosis_issue)
|
│ ├─ resume_diagnosis_issue.py # 简历诊断问题表(bg_resume_diagnosis_issue)
|
||||||
│ ├─ job.py # 岗位表(bg_job,只读,用于技能差距分析)
|
│ ├─ job.py # 岗位表(bg_job,只读,用于技能差距分析)
|
||||||
│ └─ job_agent_config.py # 求职助手配置表(bg_job_agent_config)
|
│ ├─ job_agent_config.py # 求职助手配置表(bg_job_agent_config)
|
||||||
|
│ └─ user_job_customize_resume.py # 用户岗位定制简历表(bg_user_job_customize_resume)
|
||||||
│
|
│
|
||||||
├─ tool/ # **工具层**(无状态、无业务依赖的通用工具)
|
├─ tool/ # **工具层**(无状态、无业务依赖的通用工具)
|
||||||
│ ├─ file_parser.py # 文件解析工具(PDF/Word/TXT → 纯文本,parse_to_text 入口方法)
|
│ ├─ file_parser.py # 文件解析工具(PDF/Word/TXT → 纯文本,parse_to_text 入口方法)
|
||||||
@@ -90,7 +91,7 @@ offerpie_python_ai/
|
|||||||
├─ resume_diagnose_service.py # 简历诊断服务(加载简历→AI并行诊断→统计评级→写入报告)
|
├─ resume_diagnose_service.py # 简历诊断服务(加载简历→AI并行诊断→统计评级→写入报告)
|
||||||
├─ skill_gap_service.py # 技能差距分析服务(差距分析→定制简历生成→AI对话编辑)
|
├─ skill_gap_service.py # 技能差距分析服务(差距分析→定制简历生成→AI对话编辑)
|
||||||
├─ resume_loader.py # 简历统一查询模块(按ID查/自动选默认+5张子表,返回 ResumeDetail dataclass)
|
├─ resume_loader.py # 简历统一查询模块(按ID查/自动选默认+5张子表,返回 ResumeDetail dataclass)
|
||||||
├─ customize_resume_store.py # 定制简历 Redis 存取模块(保存自动回滚备份、查询、回滚)
|
├─ customize_resume_store.py # 定制简历存取模块(数据库持久化 + Redis回滚备份、按用户+岗位维度存取)
|
||||||
├─ job_agent_chat_service.py # 求职助手对话服务(查简历→序列化→调AI模块完成对话)
|
├─ job_agent_chat_service.py # 求职助手对话服务(查简历→序列化→调AI模块完成对话)
|
||||||
└─ nova_chat_service.py # Nova 对话助手服务(查简历+查岗位(可选)→调AI,纯对话不持久化)
|
└─ nova_chat_service.py # Nova 对话助手服务(查简历+查岗位(可选)→调AI,纯对话不持久化)
|
||||||
```
|
```
|
||||||
@@ -102,9 +103,9 @@ offerpie_python_ai/
|
|||||||
| **core** | 核心基础设施:数据库连接、Redis连接、鉴权、日志、中间件、异常处理、统一响应 | `database.py`、`redis.py`、`auth.py`、`middleware.py`、`exceptions.py`、`logger.py`、`StandardResponse` |
|
| **core** | 核心基础设施:数据库连接、Redis连接、鉴权、日志、中间件、异常处理、统一响应 | `database.py`、`redis.py`、`auth.py`、`middleware.py`、`exceptions.py`、`logger.py`、`StandardResponse` |
|
||||||
| **ai** | AI 模型管理 + 业务 AI 能力 | `LLM` 枚举、`resume_extractor/`(简历并行提取)、`resume_diagnoser/`(简历诊断)、`skill_gap_analyzer/`(技能差距分析 + 定制简历优化 + Agent 原子化规划 + 单条记录修改/新增)、`job_agent/`(求职助手对话 + 岗位简历优化)、`nova_chat/`(Nova 对话助手,纯对话) |
|
| **ai** | AI 模型管理 + 业务 AI 能力 | `LLM` 枚举、`resume_extractor/`(简历并行提取)、`resume_diagnoser/`(简历诊断)、`skill_gap_analyzer/`(技能差距分析 + 定制简历优化 + Agent 原子化规划 + 单条记录修改/新增)、`job_agent/`(求职助手对话 + 岗位简历优化)、`nova_chat/`(Nova 对话助手,纯对话) |
|
||||||
| **api** | REST API 路由定义 | `health.py`(健康检查)、`resume.py`(简历上传解析)、`resume_diagnose.py`(简历诊断)、`skill_gap.py`(技能差距分析 + 生成定制简历 + AI对话编辑)、`customize_resume.py`(定制简历查询/修改/回滚)、`job_agent_chat.py`(求职助手对话 + 岗位简历优化)、`nova_chat.py`(Nova 对话助手) |
|
| **api** | REST API 路由定义 | `health.py`(健康检查)、`resume.py`(简历上传解析)、`resume_diagnose.py`(简历诊断)、`skill_gap.py`(技能差距分析 + 生成定制简历 + AI对话编辑)、`customize_resume.py`(定制简历查询/修改/回滚)、`job_agent_chat.py`(求职助手对话 + 岗位简历优化)、`nova_chat.py`(Nova 对话助手) |
|
||||||
| **models** | SQLAlchemy ORM 模型,与 Java 端共享同一数据库 | `FuncPermission`、`UserFuncPermissionStock`、`UserFuncUsageLog`、`UserResume`、`UserResumeEducation`/`Work`/`Internship`/`Project`/`Competition`、`ResumeDiagnosisReport`、`ResumeDiagnosisIssue`、`Job`(只读)、`JobAgentConfig` |
|
| **models** | SQLAlchemy ORM 模型,与 Java 端共享同一数据库 | `FuncPermission`、`UserFuncPermissionStock`、`UserFuncUsageLog`、`UserResume`、`UserResumeEducation`/`Work`/`Internship`/`Project`/`Competition`、`ResumeDiagnosisReport`、`ResumeDiagnosisIssue`、`Job`(只读)、`JobAgentConfig`、`UserJobCustomizeResume` |
|
||||||
| **tool** | 无状态通用工具,不依赖数据库/Redis/用户上下文 | `file_parser.py`(PDF/Word/TXT 文件解析为纯文本)、`json_helper.py`(AI 输出 JSON 解析,去 markdown 代码块 + json_repair 容错)、`snowflake.py`(雪花ID生成) |
|
| **tool** | 无状态通用工具,不依赖数据库/Redis/用户上下文 | `file_parser.py`(PDF/Word/TXT 文件解析为纯文本)、`json_helper.py`(AI 输出 JSON 解析,去 markdown 代码块 + json_repair 容错)、`snowflake.py`(雪花ID生成) |
|
||||||
| **services** | 业务逻辑实现 | `FuncPermissionService`(功能权限校验、扣减、回退)、`ResumeParseService`(简历文件解析→AI结构化→入库)、`ResumeDiagnoseService`(简历诊断→AI并行分析→评级→入库)、`SkillGapService`(技能差距分析→定制简历生成→AI对话编辑)、`resume_loader`(简历统一查询,返回ResumeDetail)、`customize_resume_store`(定制简历Redis存取+数据构建,自动回滚备份)、`JobAgentChatService`(求职助手对话+岗位简历优化)、`NovaChatService`(Nova对话助手,查简历+查岗位→调AI) |
|
| **services** | 业务逻辑实现 | `FuncPermissionService`(功能权限校验、扣减、回退)、`ResumeParseService`(简历文件解析→AI结构化→入库)、`ResumeDiagnoseService`(简历诊断→AI并行分析→评级→入库)、`SkillGapService`(技能差距分析→定制简历生成→AI对话编辑)、`resume_loader`(简历统一查询,返回ResumeDetail)、`customize_resume_store`(定制简历数据库存取+数据构建,按用户+岗位维度,Redis回滚备份)、`JobAgentChatService`(求职助手对话+岗位简历优化)、`NovaChatService`(Nova对话助手,查简历+查岗位→调AI) |
|
||||||
|
|
||||||
## 3️⃣ 技术栈
|
## 3️⃣ 技术栈
|
||||||
| 类别 | 技术选型 | 说明 |
|
| 类别 | 技术选型 | 说明 |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""定制简历接口(查询/修改/回滚)"""
|
"""定制简历接口(查询/修改/回滚)"""
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Query
|
||||||
|
|
||||||
from app.core.context import RequestContext
|
from app.core.context import RequestContext
|
||||||
from app.schemas.customize_resume import CustomizeResume
|
from app.schemas.customize_resume import CustomizeResume
|
||||||
@@ -10,21 +10,21 @@ router = APIRouter(prefix="/job", tags=["定制简历"])
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/customize-resume", summary="查询定制简历")
|
@router.get("/customize-resume", summary="查询定制简历")
|
||||||
async def get_customize_resume():
|
async def get_customize_resume(job_id: int = Query(..., description="岗位ID")):
|
||||||
"""查询当前用户的定制简历"""
|
"""查询当前用户针对某岗位的定制简历"""
|
||||||
user_id = RequestContext.user_id.get()
|
user_id = RequestContext.user_id.get()
|
||||||
return await customize_resume_store.get(user_id)
|
return await customize_resume_store.get(user_id, job_id)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/customize-resume", summary="修改定制简历")
|
@router.put("/customize-resume", summary="修改定制简历")
|
||||||
async def update_customize_resume(data: CustomizeResume):
|
async def update_customize_resume(job_id: int = Query(..., description="岗位ID"), data: CustomizeResume = ...):
|
||||||
"""手动编辑定制简历(整体覆盖)"""
|
"""手动编辑定制简历(整体覆盖)"""
|
||||||
user_id = RequestContext.user_id.get()
|
user_id = RequestContext.user_id.get()
|
||||||
await customize_resume_store.save(user_id, data)
|
await customize_resume_store.save(user_id, job_id, data)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/customize-resume/rollback", summary="回滚定制简历")
|
@router.post("/customize-resume/rollback", summary="回滚定制简历")
|
||||||
async def rollback_customize_resume():
|
async def rollback_customize_resume(job_id: int = Query(..., description="岗位ID")):
|
||||||
"""回滚到上一版本的定制简历"""
|
"""回滚到上一版本的定制简历"""
|
||||||
user_id = RequestContext.user_id.get()
|
user_id = RequestContext.user_id.get()
|
||||||
await customize_resume_store.rollback(user_id)
|
await customize_resume_store.rollback(user_id, job_id)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""用户岗位定制简历表(bg_user_job_customize_resume)
|
||||||
|
|
||||||
|
一个用户 + 一个岗位 = 一份定制简历,content 字段存完整 CustomizeResume JSON。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import BigInteger, DateTime, JSON
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.core.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class UserJobCustomizeResume(Base):
|
||||||
|
"""用户岗位定制简历表 bg_user_job_customize_resume"""
|
||||||
|
__tablename__ = "bg_user_job_customize_resume"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||||
|
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="用户ID")
|
||||||
|
job_id: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="岗位ID,关联 bg_job.id")
|
||||||
|
content: Mapped[Optional[dict]] = mapped_column(JSON, nullable=False, comment="定制简历完整 JSON(CustomizeResume 结构)")
|
||||||
|
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="更新时间")
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
"""定制简历 Redis 存取 + 数据转换模块
|
"""定制简历存取 + 数据转换模块
|
||||||
|
|
||||||
提供定制简历的保存(自动回滚备份)、查询、回滚、从 ResumeDetail 构建能力。
|
提供定制简历的保存(数据库持久化)、查询、回滚(Redis临时备份)、从 ResumeDetail 构建能力。
|
||||||
各 Service 统一复用,不直接操作 Redis key。
|
各 Service 统一复用,不直接操作数据库表和 Redis key。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
|
||||||
|
from sqlalchemy import select, update
|
||||||
|
|
||||||
|
from app.core.database import async_session_factory
|
||||||
from app.core.redis import RedisManager
|
from app.core.redis import RedisManager
|
||||||
|
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, Paragraph,
|
||||||
)
|
)
|
||||||
from app.services.resume_loader import ResumeDetail
|
from app.services.resume_loader import ResumeDetail, load_default_resume_detail
|
||||||
|
from app.tool.snowflake import next_id
|
||||||
|
|
||||||
# Redis 常量
|
# Redis 回滚常量
|
||||||
KEY_PREFIX = "customize:resume:"
|
ROLLBACK_PREFIX = "customize:resume:rollback:"
|
||||||
EXPIRE = 12 * 60 * 60 # 12小时
|
|
||||||
ROLLBACK_KEY_PREFIX = "customize:resume:rollback:"
|
|
||||||
ROLLBACK_EXPIRE = 30 * 60 # 30分钟
|
ROLLBACK_EXPIRE = 30 * 60 # 30分钟
|
||||||
|
|
||||||
_CHARS = string.ascii_letters + string.digits
|
_CHARS = string.ascii_letters + string.digits
|
||||||
@@ -65,31 +68,57 @@ def build_from_detail(detail: ResumeDetail) -> CustomizeResume:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def save(user_id: int, cr: CustomizeResume) -> None:
|
def _rollback_key(user_id: int, job_id: int) -> str:
|
||||||
"""保存定制简历,自动备份旧版本到回滚 key"""
|
return f"{ROLLBACK_PREFIX}{user_id}:{job_id}"
|
||||||
key = f"{KEY_PREFIX}{user_id}"
|
|
||||||
rollback_key = f"{ROLLBACK_KEY_PREFIX}{user_id}"
|
|
||||||
old_data = await RedisManager.client.get(key)
|
|
||||||
if old_data:
|
|
||||||
await RedisManager.client.set(rollback_key, old_data, ex=ROLLBACK_EXPIRE)
|
|
||||||
await RedisManager.client.set(key, cr.model_dump_json(by_alias=True), ex=EXPIRE)
|
|
||||||
|
|
||||||
|
|
||||||
async def get(user_id: int) -> dict | None:
|
async def save(user_id: int, job_id: int, cr: CustomizeResume) -> None:
|
||||||
"""查询定制简历,返回 dict 或 None"""
|
"""保存定制简历:备份旧版本到 Redis 用于回滚 + 写数据库"""
|
||||||
key = f"{KEY_PREFIX}{user_id}"
|
content = cr.model_dump(by_alias=True)
|
||||||
data = await RedisManager.client.get(key)
|
|
||||||
if data:
|
async with async_session_factory() as session:
|
||||||
return CustomizeResume.model_validate_json(data).model_dump(by_alias=True)
|
# 查已有记录,备份旧版本到 Redis 用于回滚
|
||||||
return None
|
result = await session.execute(select(UserJobCustomizeResume).where(UserJobCustomizeResume.user_id == user_id, UserJobCustomizeResume.job_id == job_id))
|
||||||
|
record = result.scalar_one_or_none()
|
||||||
|
if record:
|
||||||
|
old_cr = CustomizeResume.model_validate(record.content)
|
||||||
|
await RedisManager.client.set(_rollback_key(user_id, job_id), old_cr.model_dump_json(by_alias=True), ex=ROLLBACK_EXPIRE)
|
||||||
|
await session.execute(update(UserJobCustomizeResume).where(
|
||||||
|
UserJobCustomizeResume.id == record.id).values(content=content))
|
||||||
|
else:
|
||||||
|
session.add(UserJobCustomizeResume(id=next_id(), user_id=user_id, job_id=job_id, content=content))
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
async def rollback(user_id: int) -> None:
|
async def get(user_id: int, job_id: int) -> dict | None:
|
||||||
"""回滚定制简历到上一版本"""
|
"""查询定制简历,查不到则加载默认简历构建返回"""
|
||||||
rollback_key = f"{ROLLBACK_KEY_PREFIX}{user_id}"
|
async with async_session_factory() as session:
|
||||||
data = await RedisManager.client.get(rollback_key)
|
result = await session.execute(select(UserJobCustomizeResume).where(
|
||||||
|
UserJobCustomizeResume.user_id == user_id, UserJobCustomizeResume.job_id == job_id))
|
||||||
|
record = result.scalar_one_or_none()
|
||||||
|
if record:
|
||||||
|
return CustomizeResume.model_validate(record.content).model_dump(by_alias=True)
|
||||||
|
# 没有定制简历,加载默认简历构建
|
||||||
|
detail = await load_default_resume_detail(session, user_id)
|
||||||
|
return build_from_detail(detail).model_dump(by_alias=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def rollback(user_id: int, job_id: int) -> None:
|
||||||
|
"""回滚定制简历到上一版本:从 Redis 取回滚数据写入数据库"""
|
||||||
|
rollback_k = _rollback_key(user_id, job_id)
|
||||||
|
data = await RedisManager.client.get(rollback_k)
|
||||||
if not data:
|
if not data:
|
||||||
raise ValueError("没有可回滚的版本")
|
raise ValueError("没有可回滚的版本")
|
||||||
key = f"{KEY_PREFIX}{user_id}"
|
content = CustomizeResume.model_validate_json(data).model_dump(by_alias=True)
|
||||||
await RedisManager.client.set(key, data, ex=EXPIRE)
|
|
||||||
await RedisManager.client.delete(rollback_key)
|
async with async_session_factory() as session:
|
||||||
|
result = await session.execute(select(UserJobCustomizeResume).where(
|
||||||
|
UserJobCustomizeResume.user_id == user_id, UserJobCustomizeResume.job_id == job_id))
|
||||||
|
record = result.scalar_one_or_none()
|
||||||
|
if record:
|
||||||
|
await session.execute(update(UserJobCustomizeResume).where(
|
||||||
|
UserJobCustomizeResume.id == record.id).values(content=content))
|
||||||
|
else:
|
||||||
|
session.add(UserJobCustomizeResume(id=next_id(), user_id=user_id, job_id=job_id, content=content))
|
||||||
|
await session.commit()
|
||||||
|
await RedisManager.client.delete(rollback_k)
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ class JobAgentChatService:
|
|||||||
log.warning(f"岗位简历优化[{key}]失败: {result}")
|
log.warning(f"岗位简历优化[{key}]失败: {result}")
|
||||||
continue
|
continue
|
||||||
self._apply_optimize_result(cr, key, result)
|
self._apply_optimize_result(cr, key, result)
|
||||||
# 4. 存 Redis
|
# 4. 存数据库
|
||||||
await customize_resume_store.save(user_id, cr)
|
await customize_resume_store.save(user_id, job_id, cr)
|
||||||
# 5. 返回
|
# 5. 返回
|
||||||
return cr.model_dump(by_alias=True)
|
return cr.model_dump(by_alias=True)
|
||||||
|
|
||||||
|
|||||||
@@ -126,8 +126,8 @@ class SkillGapService:
|
|||||||
if "skills" in optimize_modules and add_skills:
|
if "skills" in optimize_modules and add_skills:
|
||||||
existing = set(cr.resume.skills)
|
existing = set(cr.resume.skills)
|
||||||
cr.resume.skills.extend([s for s in add_skills if s not in existing])
|
cr.resume.skills.extend([s for s in add_skills if s not in existing])
|
||||||
# 5. 存 Redis
|
# 5. 存数据库
|
||||||
await customize_resume_store.save(user_id, cr)
|
await customize_resume_store.save(user_id, job_id, cr)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _experience_tasks(cr: CustomizeResume, job_title: str, job_desc: str) -> list[tuple[str, str]]:
|
def _experience_tasks(cr: CustomizeResume, job_title: str, job_desc: str) -> list[tuple[str, str]]:
|
||||||
@@ -161,7 +161,7 @@ class SkillGapService:
|
|||||||
instruction: str, chat_history: list) -> dict:
|
instruction: str, chat_history: list) -> dict:
|
||||||
"""AI 对话式编辑定制简历(原子化操作版)"""
|
"""AI 对话式编辑定制简历(原子化操作版)"""
|
||||||
# 1. 取当前定制简历
|
# 1. 取当前定制简历
|
||||||
cr_data = await customize_resume_store.get(user_id)
|
cr_data = await customize_resume_store.get(user_id, job_id)
|
||||||
if not cr_data:
|
if not cr_data:
|
||||||
raise ValueError("定制简历不存在,请先生成")
|
raise ValueError("定制简历不存在,请先生成")
|
||||||
cr = CustomizeResume.model_validate(cr_data)
|
cr = CustomizeResume.model_validate(cr_data)
|
||||||
@@ -228,7 +228,7 @@ class SkillGapService:
|
|||||||
elif op_type == "add":
|
elif op_type == "add":
|
||||||
self._apply_record_add(cr, mod_name, result)
|
self._apply_record_add(cr, mod_name, result)
|
||||||
# 6. 保存(自动备份回滚)
|
# 6. 保存(自动备份回滚)
|
||||||
await customize_resume_store.save(user_id, cr)
|
await customize_resume_store.save(user_id, job_id, cr)
|
||||||
# 拼接更新模块标签
|
# 拼接更新模块标签
|
||||||
updated_modules = list(dict.fromkeys(op.get("module", "") for op in operations))
|
updated_modules = list(dict.fromkeys(op.get("module", "") for op in operations))
|
||||||
label = "、".join(_MODULE_LABELS.get(m, m) for m in updated_modules if m)
|
label = "、".join(_MODULE_LABELS.get(m, m) for m in updated_modules if m)
|
||||||
|
|||||||
Reference in New Issue
Block a user