权限监控与管理界面全面美化
重新设计权限系统所有 UI 组件的视觉风格,统一配色、圆角、阴影和交互动效: - 实时概览:统计卡片加图标和渐变色条,健康评分改为环形进度,告警改为卡片式 - 趋势分析:图表卡片加彩色图标标识,ECharts 配色升级为渐变面积填充 - 访问日志:指标卡片带图标,日志审计改为卡片式入口,IP排行前三高亮 - IP属地:工具栏重设计,排行列表前三渐变高亮,指标卡片统一新风格 - 系统级权限:从 el-table 改为自定义卡片列表,模块块独立圆角卡片 - 项目权限配置:空状态引导优化,成员表格加头像,工具栏加背景容器 - 角色概览卡片:加进度条可视化,hover 微动效 - 接口权限矩阵:工具栏分离布局,表格圆角包裹 - 角色管理抽屉:侧边栏选中态渐变,操作行 hover 高亮 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
"""IP 属地解析服务。
|
||||
|
||||
优先使用 ip2region xdb 离线库;缺少依赖或数据文件时降级为基础地址分类。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import logging
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger("ctms.ip_location")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IpLocation:
|
||||
location: str
|
||||
country: str = ""
|
||||
province: str = ""
|
||||
city: str = ""
|
||||
isp: str = ""
|
||||
|
||||
|
||||
def _default_xdb_path(version: int) -> Path:
|
||||
filename = "ip2region_v6.xdb" if version == 6 else "ip2region_v4.xdb"
|
||||
return Path(__file__).resolve().parents[1] / "data" / filename
|
||||
|
||||
|
||||
def _normalize_part(value: str | None) -> str:
|
||||
part = (value or "").strip()
|
||||
return "" if part in {"", "0", "-"} else part
|
||||
|
||||
|
||||
def _format_location(country: str, province: str, city: str, isp: str) -> str:
|
||||
parts = [part for part in [country, province, city, isp] if part]
|
||||
return " / ".join(parts) if parts else "公网"
|
||||
|
||||
|
||||
class Ip2RegionResolver:
|
||||
def __init__(self, db_path: Optional[str] = None, ipv6_db_path: Optional[str] = None) -> None:
|
||||
self.db_paths = {
|
||||
4: Path(db_path or settings.IP2REGION_XDB_PATH or _default_xdb_path(4)),
|
||||
6: Path(ipv6_db_path or settings.IP2REGION_IPV6_XDB_PATH or _default_xdb_path(6)),
|
||||
}
|
||||
self._searchers = {}
|
||||
self._load_failed: set[int] = set()
|
||||
|
||||
def lookup(self, ip: str | None) -> IpLocation:
|
||||
addr = self._classify_address(ip)
|
||||
if addr is None:
|
||||
return IpLocation(location="未知")
|
||||
if addr.is_loopback:
|
||||
return IpLocation(location="本机")
|
||||
if addr.is_private or addr.is_link_local:
|
||||
return IpLocation(location="局域网")
|
||||
|
||||
searcher = self._get_searcher(addr.version)
|
||||
if not searcher:
|
||||
return IpLocation(location="公网")
|
||||
|
||||
try:
|
||||
region = searcher.search(str(addr))
|
||||
except Exception:
|
||||
logger.exception("Failed to lookup IP location: %s", addr)
|
||||
return IpLocation(location="公网")
|
||||
return self._parse_region(region)
|
||||
|
||||
@staticmethod
|
||||
def _classify_address(ip: str | None):
|
||||
if not ip:
|
||||
return None
|
||||
try:
|
||||
return ipaddress.ip_address(ip)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _get_searcher(self, version: int):
|
||||
if version in self._searchers or version in self._load_failed:
|
||||
return self._searchers.get(version)
|
||||
db_path = self.db_paths[version]
|
||||
if not db_path.exists():
|
||||
self._load_failed.add(version)
|
||||
logger.info("ip2region xdb file not found: %s", db_path)
|
||||
return None
|
||||
try:
|
||||
try:
|
||||
import ip2region.searcher as searcher
|
||||
import ip2region.util as util
|
||||
except ModuleNotFoundError:
|
||||
vendor_path = Path(__file__).resolve().parents[1] / "vendor"
|
||||
if str(vendor_path) not in sys.path:
|
||||
sys.path.insert(0, str(vendor_path))
|
||||
import ip2region.searcher as searcher
|
||||
import ip2region.util as util
|
||||
|
||||
ip_version = util.IPv6 if version == 6 else util.IPv4
|
||||
buffer = util.load_content_from_file(str(db_path))
|
||||
self._searchers[version] = searcher.new_with_buffer(ip_version, buffer)
|
||||
except Exception:
|
||||
self._load_failed.add(version)
|
||||
logger.exception("Failed to initialize ip2region searcher: %s", db_path)
|
||||
return self._searchers.get(version)
|
||||
|
||||
@staticmethod
|
||||
def _parse_region(region: str | None) -> IpLocation:
|
||||
parts = [*str(region or "").split("|"), "", "", "", ""]
|
||||
country = _normalize_part(parts[0])
|
||||
province = _normalize_part(parts[1])
|
||||
city = _normalize_part(parts[2])
|
||||
isp = _normalize_part(parts[3])
|
||||
return IpLocation(
|
||||
location=_format_location(country, province, city, isp),
|
||||
country=country,
|
||||
province=province,
|
||||
city=city,
|
||||
isp=isp,
|
||||
)
|
||||
|
||||
|
||||
_resolver = Ip2RegionResolver()
|
||||
|
||||
|
||||
def resolve_ip_location(ip: str | None) -> IpLocation:
|
||||
return _resolver.lookup(ip)
|
||||
Reference in New Issue
Block a user