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