137 lines
4.2 KiB
Python
137 lines
4.2 KiB
Python
"""风控预警自然语言筛选条件解析。"""
|
||||
|
|
|
|||
|
|
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
|