"""定制简历存取 + 数据转换模块 提供定制简历的保存(数据库持久化)、查询、回滚(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)