"""风控预警自然语言筛选条件解析。""" from __future__ import annotations import re from datetime import UTC, date, datetime, time, timedelta from zoneinfo import ZoneInfo from app.core.risk_contracts import RiskAlertQuery SHANGHAI = ZoneInfo("Asia/Shanghai") def parse_risk_alert_filters( message: str, *, now: datetime | None = None, ) -> dict[str, str]: """把常见中文筛选表达转换为风控查询工具参数。""" current = now or datetime.now(SHANGHAI) if current.tzinfo is None: current = current.replace(tzinfo=SHANGHAI) filters: dict[str, str] = {} level = re.search(r"(低|中|高)(?:风险)?", message) if level: filters["risk_level"] = level.group(1) rule = re.search(r"RW-\d{3}", message, re.IGNORECASE) if rule: filters["rule_code"] = rule.group(0).upper() customer = re.search( r"(?:客户编号|客户号|客户账户|客户)\s*[::#]?\s*" r"([A-Za-z0-9_-]{1,64})", message, ) if customer: filters["customer_no"] = customer.group(1) elif matched := re.search(r"\b(CUST-\d+)\b", message, re.IGNORECASE): filters["customer_no"] = matched.group(1).upper() product_code = re.search( r"(?:产品编号|产品代码|产品编码)\s*[::#]?\s*" r"([A-Za-z0-9_-]{1,64})", message, ) if product_code: filters["product_code"] = product_code.group(1) elif matched := re.search(r"\b(P-[A-Za-z0-9_-]{1,60})\b", message): filters["product_code"] = matched.group(1) product_name = re.search( r"产品名称\s*[::#]?\s*([^\s,,。;;?!]{1,128})", message, ) if product_name is None: product_name = re.search( r"产品\s*[::]\s*([^\s,,。;;?!]{1,128})", message, ) if product_name: candidate = product_name.group(1).strip() if candidate not in {"编号", "代码", "编码", "名称"}: filters["product_name"] = candidate start_time, end_time = _parse_time_range(message, current) if start_time is not None: filters["start_time"] = _to_utc_iso(start_time) if end_time is not None: filters["end_time"] = _to_utc_iso(end_time) validated = RiskAlertQuery.model_validate(filters) return validated.model_dump(mode="json", exclude_none=True) def _parse_time_range( message: str, current: datetime, ) -> tuple[datetime | None, datetime | None]: explicit = re.search( r"(\d{4}-\d{2}-\d{2})\s*(?:至|到|~|—)\s*(\d{4}-\d{2}-\d{2})", message, ) if explicit: start_date = _parse_date(explicit.group(1)) end_date = _parse_date(explicit.group(2)) if start_date is not None and end_date is not None: return ( datetime.combine(start_date, time.min, tzinfo=SHANGHAI), datetime.combine(end_date, time.max, tzinfo=SHANGHAI), ) if "今天" in message or "今日" in message: start = current.replace(hour=0, minute=0, second=0, microsecond=0) return start, current if "本月" in message: start = current.replace( day=1, hour=0, minute=0, second=0, microsecond=0, ) return start, current recent = re.search(r"(?:近|最近)\s*(\d+)\s*(天|日)", message) if recent: days = int(recent.group(1)) return current - timedelta(days=days), current since = re.search(r"(\d{4}-\d{2}-\d{2})\s*(?:以来|之后|起)", message) if since: start_date = _parse_date(since.group(1)) if start_date is not None: return datetime.combine(start_date, time.min, tzinfo=SHANGHAI), current until = re.search(r"(?:截至|截止)\s*(\d{4}-\d{2}-\d{2})", message) if until: end_date = _parse_date(until.group(1)) if end_date is not None: return None, datetime.combine(end_date, time.max, tzinfo=SHANGHAI) return None, None def _to_utc_iso(value: datetime) -> str: return value.astimezone(UTC).replace(tzinfo=None).isoformat() def _parse_date(value: str) -> date | None: try: return date.fromisoformat(value) except ValueError: return None