Files
resume-agent/backend/app/validators.py
T
hypandClaude ae2d9b128d feat: builder 简历生成 + 轻度优化 + 简历导入交付副本
自内部仓库剥离深度优化与 RAG 知识库后的交付版本:
- Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强)
- 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护
- 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏
- PostgreSQL 运行时 + Alembic 迁移链

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 11:08:30 +08:00

121 lines
4.1 KiB
Python

from __future__ import annotations
import re
from typing import Any
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
def strict_phone(value: str) -> bool:
return (
len(value) == 11
and value.isascii()
and value.isdigit()
and value[0] == "1"
and value[1] in "3456789"
)
def mask_phone(value: Any) -> str | None:
if not isinstance(value, str) or len(value) != 11:
return None
return f"{value[:3]}****{value[-4:]}"
def valid_month(value: Any) -> bool:
if not isinstance(value, str) or len(value) != 7 or value[4] != "-":
return False
year, month = value.split("-", 1)
return (
year.isdigit()
and month.isdigit()
and 1900 <= int(year) <= 2100
and 1 <= int(month) <= 12
)
def anchor_missing_fields(
profile: dict[str, Any], required: list[str]
) -> list[str]:
anchor = profile.get("anchor", {})
missing = [field for field in required if not _present(anchor.get(field))]
start = anchor.get("start_date")
end = anchor.get("end_date_or_present")
if start and not valid_month(start) and "start_date" not in missing:
missing.append("start_date")
if end and end != "present" and not valid_month(end) and "end_date_or_present" not in missing:
missing.append("end_date_or_present")
if valid_month(start) and valid_month(end) and end < start and "end_date_or_present" not in missing:
missing.append("end_date_or_present")
return missing
def can_create_resume(profile: dict[str, Any], missing: list[str]) -> bool:
base_ready = bool(
profile.get("privacy_accepted")
and strict_phone(str(profile.get("phone") or ""))
and str(profile.get("name") or "").strip()
and profile.get("job_type") in {"campus", "social", "internship"}
)
return base_ready and not missing
def _present(value: Any) -> bool:
return bool(value.strip()) if isinstance(value, str) else value is not None
def valid_email(value: Any) -> bool:
return isinstance(value, str) and bool(_EMAIL_RE.match(value))
def valid_url(value: Any) -> bool:
if not isinstance(value, str):
return False
return value.startswith(("https://", "http://")) and len(value) > 8
def normalize_tags(values: Any, *, max_items: int = 20, max_length: int = 32) -> list[str]:
"""标签规范化:去空白、去空、去重(大小写不敏感保留首个写法)、限长限量。"""
if not isinstance(values, list):
return []
seen: set[str] = set()
result: list[str] = []
for value in values:
if not isinstance(value, str):
continue
item = value.strip()
if not item or len(item) > max_length:
continue
key = item.casefold()
if key in seen:
continue
seen.add(key)
result.append(item)
if len(result) >= max_items:
break
return result
def competition_entry_errors(entry: dict[str, Any]) -> list[str]:
"""竞赛条目核心字段校验,返回问题字段名列表(空 = 合法)。"""
errors: list[str] = []
if not str(entry.get("name") or "").strip():
errors.append("name")
if not str(entry.get("award") or "").strip():
errors.append("award")
if not valid_month(entry.get("date")):
errors.append("date")
return errors
def record_entry_errors(entry: dict[str, Any], required: list[str] | tuple[str, ...]) -> list[str]:
"""经历卡片核心字段校验:非空 + YYYY-MM/present + 结束不早于开始。"""
errors = [field for field in required if not str(entry.get(field) or "").strip()]
start = str(entry.get("start_date") or "")
end = str(entry.get("end_date_or_present") or "")
if start and not valid_month(start) and "start_date" not in errors:
errors.append("start_date")
if end and end != "present" and not valid_month(end) and "end_date_or_present" not in errors:
errors.append("end_date_or_present")
if valid_month(start) and valid_month(end) and end < start and "end_date_or_present" not in errors:
errors.append("end_date_or_present")
return errors