generated from kgod/ai-review-template
121 lines
4.1 KiB
Python
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
|