Files
offerpai_python_ai/app/services/customize_resume_store.py
T
2026-04-29 18:42:40 +08:00

126 lines
6.1 KiB
Python

"""定制简历存取 + 数据转换模块
提供定制简历的保存(数据库持久化)、查询、回滚(Redis临时备份)、从 ResumeDetail 构建能力。
各 Service 统一复用,不直接操作数据库表和 Redis key。
"""
import random
import string
from sqlalchemy import select, update
from app.core.database import get_db
from app.core.redis import RedisManager
from app.models.user_job_customize_resume import UserJobCustomizeResume
from app.schemas.customize_resume import (
CustomizeResume, ResumeProfile, Education, Work, Internship, Project, Competition, Paragraph,
)
from app.services.resume_loader import ResumeDetail, load_default_resume_detail
from app.tool.snowflake import next_id
# Redis 回滚常量
ROLLBACK_PREFIX = "customize:resume:rollback:"
ROLLBACK_EXPIRE = 30 * 60 # 30分钟
_CHARS = string.ascii_letters + string.digits
def _rand_id() -> str:
"""生成随机8位字符串标识"""
return "".join(random.choices(_CHARS, k=8))
def _build_paragraphs(description: list[dict] | None) -> list[Paragraph]:
"""将数据库 description [{id, text}] 转为 Paragraph 列表"""
if not description:
return []
return [Paragraph(id=_rand_id(), text=item.get("text", "")) for item in description if isinstance(item, dict)]
def build_from_detail(detail: ResumeDetail) -> CustomizeResume:
"""从 ResumeDetail 组装 CustomizeResume"""
resume = detail.resume
profile = ResumeProfile(
avatarUrl=resume.avatar_url or "", name=resume.name or "", email=resume.email or "",
mobileNumber=resume.mobile_number or "", city=resume.city or "",
wechatNumber=resume.wechat_number or "", portfolioUrl=resume.portfolio_url or "",
skills=resume.skills or [], certificates=resume.certificates or [],
summary=resume.summary or "",
)
return CustomizeResume(
resume=profile,
education=[Education(id=_rand_id(), school=r.school or "", major=r.major or "",
degree=r.degree or "", studyType=r.study_type or "",
startDate=r.start_date or "", endDate=r.end_date or "",
description=_build_paragraphs(r.description)) for r in detail.education],
work=[Work(id=_rand_id(), companyName=r.company_name or "", position=r.position or "",
startDate=r.start_date or "", endDate=r.end_date or "",
description=_build_paragraphs(r.description)) for r in detail.work],
internship=[Internship(id=_rand_id(), companyName=r.company_name or "", position=r.position or "",
startDate=r.start_date or "", endDate=r.end_date or "",
description=_build_paragraphs(r.description)) for r in detail.internship],
project=[Project(id=_rand_id(), companyName=r.company_name or "", projectName=r.project_name or "",
role=r.role or "", startDate=r.start_date or "", endDate=r.end_date or "",
description=_build_paragraphs(r.description)) for r in detail.project],
competition=[Competition(id=_rand_id(), competitionName=r.competition_name or "", award=r.award or "",
awardDate=r.award_date or "",
description=_build_paragraphs(r.description)) for r in detail.competition],
)
def _rollback_key(user_id: int, job_id: int) -> str:
return f"{ROLLBACK_PREFIX}{user_id}:{job_id}"
async def save(user_id: int, job_id: int, cr: CustomizeResume) -> None:
"""保存定制简历:备份旧版本到 Redis 用于回滚 + 写数据库"""
content = cr.model_dump(by_alias=True)
async for session in get_db():
# 查已有记录,备份旧版本到 Redis 用于回滚
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))
async def get(user_id: int, job_id: int) -> dict | None:
"""查询定制简历,查不到则加载默认简历构建返回"""
data = None
async for session in get_db():
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:
data = CustomizeResume.model_validate(record.content).model_dump(by_alias=True)
else:
# 没有定制简历,加载默认简历构建
detail = await load_default_resume_detail(session, user_id)
data = build_from_detail(detail).model_dump(by_alias=True)
return data
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:
raise ValueError("没有可回滚的版本")
content = CustomizeResume.model_validate_json(data).model_dump(by_alias=True)
async for session in get_db():
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 RedisManager.client.delete(rollback_k)