合并相同属地的IP统计
This commit is contained in:
@@ -632,7 +632,7 @@ async def get_ip_locations(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
result = await db.execute(_apply_monitoring_scope_to_log_query(query, scope))
|
result = await db.execute(_apply_monitoring_scope_to_log_query(query, scope))
|
||||||
buckets: dict[tuple[str, str, str, str], dict] = {}
|
buckets: dict[tuple[str, str, str], dict] = {}
|
||||||
all_ip_addresses: set[str] = set()
|
all_ip_addresses: set[str] = set()
|
||||||
all_user_ids: set[uuid.UUID] = set()
|
all_user_ids: set[uuid.UUID] = set()
|
||||||
total_count = 0
|
total_count = 0
|
||||||
@@ -641,15 +641,16 @@ async def get_ip_locations(
|
|||||||
|
|
||||||
for ip_address, user_id, allowed in result.all():
|
for ip_address, user_id, allowed in result.all():
|
||||||
ip_info = resolve_ip_location(ip_address)
|
ip_info = resolve_ip_location(ip_address)
|
||||||
key = (ip_info.country, ip_info.province, ip_info.city, ip_info.isp)
|
key = (ip_info.country, ip_info.province, ip_info.city)
|
||||||
|
location = " / ".join(part for part in [ip_info.country, ip_info.province, ip_info.city] if part) or ip_info.location or "未知"
|
||||||
bucket = buckets.setdefault(
|
bucket = buckets.setdefault(
|
||||||
key,
|
key,
|
||||||
{
|
{
|
||||||
"country": ip_info.country,
|
"country": ip_info.country,
|
||||||
"province": ip_info.province,
|
"province": ip_info.province,
|
||||||
"city": ip_info.city,
|
"city": ip_info.city,
|
||||||
"isp": ip_info.isp,
|
"isp": "",
|
||||||
"location": ip_info.location,
|
"location": location,
|
||||||
"total_count": 0,
|
"total_count": 0,
|
||||||
"allowed_count": 0,
|
"allowed_count": 0,
|
||||||
"denied_count": 0,
|
"denied_count": 0,
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ from app.api.v1 import permission_monitoring
|
|||||||
|
|
||||||
|
|
||||||
class FakeIpInfo:
|
class FakeIpInfo:
|
||||||
def __init__(self, province: str, city: str) -> None:
|
def __init__(self, province: str, city: str, isp: str = "电信") -> None:
|
||||||
self.country = "中国"
|
self.country = "中国"
|
||||||
self.province = province
|
self.province = province
|
||||||
self.city = city
|
self.city = city
|
||||||
self.isp = "电信"
|
self.isp = isp
|
||||||
self.location = f"中国 / {province} / {city} / 电信"
|
self.location = f"中国 / {province} / {city} / {isp}"
|
||||||
|
|
||||||
|
|
||||||
async def _seed_permission_log(db_session, study_id: uuid.UUID, user_id: uuid.UUID, *, allowed: bool, elapsed_ms: float) -> None:
|
async def _seed_permission_log(db_session, study_id: uuid.UUID, user_id: uuid.UUID, *, allowed: bool, elapsed_ms: float) -> None:
|
||||||
@@ -334,6 +334,163 @@ async def test_ip_locations_counts_unique_users_per_location(db_session, monkeyp
|
|||||||
assert result["summary"]["unique_user_count"] == 2
|
assert result["summary"]["unique_user_count"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ip_locations_merges_same_region_with_different_isp(db_session, monkeypatch):
|
||||||
|
"""同一省市的 IP 属地统计不应因运营商不同拆分。"""
|
||||||
|
await db_session.execute(text("DELETE FROM permission_access_logs"))
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
study_id = "00000000-0000-0000-0000-000000000401"
|
||||||
|
user_id = "00000000-0000-0000-0000-000000000501"
|
||||||
|
|
||||||
|
await db_session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
INSERT INTO studies (id, code, name, status, is_locked, visit_schedule, active_roles)
|
||||||
|
VALUES (:id, :code, :name, :status, :is_locked, :visit_schedule, :active_roles)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"id": study_id,
|
||||||
|
"code": "IP-LOCATION-MERGE-STUDY",
|
||||||
|
"name": "IP Location Merge Study",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"is_locked": False,
|
||||||
|
"visit_schedule": "[]",
|
||||||
|
"active_roles": "[]",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await db_session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
INSERT INTO users (id, email, password_hash, full_name, role, clinical_department, status)
|
||||||
|
VALUES (:id, :email, :password_hash, :full_name, :role, :clinical_department, :status)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"id": user_id,
|
||||||
|
"email": "region-merge@example.com",
|
||||||
|
"password_hash": "hash",
|
||||||
|
"full_name": "属地合并用户",
|
||||||
|
"role": "PM",
|
||||||
|
"clinical_department": "临床运营",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for ip_address in ["10.2.1.1", "10.2.1.2"]:
|
||||||
|
await db_session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
INSERT INTO permission_access_logs
|
||||||
|
(id, study_id, user_id, endpoint_key, role, allowed, elapsed_ms, ip_address, created_at)
|
||||||
|
VALUES
|
||||||
|
(:id, :study_id, :user_id, :endpoint_key, :role, :allowed, :elapsed_ms, :ip_address, CURRENT_TIMESTAMP)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"study_id": study_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"endpoint_key": "admin.permissions.read",
|
||||||
|
"role": "PM",
|
||||||
|
"allowed": True,
|
||||||
|
"elapsed_ms": 3.2,
|
||||||
|
"ip_address": ip_address,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
ip_info_by_address = {
|
||||||
|
"10.2.1.1": FakeIpInfo("重庆", "重庆市", "电信"),
|
||||||
|
"10.2.1.2": FakeIpInfo("重庆", "重庆市", "联通"),
|
||||||
|
}
|
||||||
|
monkeypatch.setattr(
|
||||||
|
permission_monitoring,
|
||||||
|
"resolve_ip_location",
|
||||||
|
lambda ip: ip_info_by_address[ip],
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await permission_monitoring.get_ip_locations(db=db_session, _=object(), days=7, limit=10)
|
||||||
|
|
||||||
|
assert len(result["items"]) == 1
|
||||||
|
assert result["items"][0]["province"] == "重庆"
|
||||||
|
assert result["items"][0]["city"] == "重庆市"
|
||||||
|
assert result["items"][0]["total_count"] == 2
|
||||||
|
assert result["items"][0]["unique_ip_count"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ip_locations_keeps_private_network_location_label(db_session, monkeypatch):
|
||||||
|
"""局域网 IP 无省市信息时,属地统计仍应保留可展示标签。"""
|
||||||
|
await db_session.execute(text("DELETE FROM permission_access_logs"))
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
study_id = "00000000-0000-0000-0000-000000000601"
|
||||||
|
user_id = "00000000-0000-0000-0000-000000000701"
|
||||||
|
|
||||||
|
await db_session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
INSERT INTO studies (id, code, name, status, is_locked, visit_schedule, active_roles)
|
||||||
|
VALUES (:id, :code, :name, :status, :is_locked, :visit_schedule, :active_roles)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"id": study_id,
|
||||||
|
"code": "IP-LOCATION-PRIVATE-STUDY",
|
||||||
|
"name": "IP Location Private Study",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"is_locked": False,
|
||||||
|
"visit_schedule": "[]",
|
||||||
|
"active_roles": "[]",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await db_session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
INSERT INTO users (id, email, password_hash, full_name, role, clinical_department, status)
|
||||||
|
VALUES (:id, :email, :password_hash, :full_name, :role, :clinical_department, :status)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"id": user_id,
|
||||||
|
"email": "private-network@example.com",
|
||||||
|
"password_hash": "hash",
|
||||||
|
"full_name": "局域网用户",
|
||||||
|
"role": "PM",
|
||||||
|
"clinical_department": "临床运营",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await db_session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
INSERT INTO permission_access_logs
|
||||||
|
(id, study_id, user_id, endpoint_key, role, allowed, elapsed_ms, ip_address, created_at)
|
||||||
|
VALUES
|
||||||
|
(:id, :study_id, :user_id, :endpoint_key, :role, :allowed, :elapsed_ms, :ip_address, CURRENT_TIMESTAMP)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"study_id": study_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"endpoint_key": "admin.permissions.read",
|
||||||
|
"role": "PM",
|
||||||
|
"allowed": True,
|
||||||
|
"elapsed_ms": 3.2,
|
||||||
|
"ip_address": "192.168.1.10",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
result = await permission_monitoring.get_ip_locations(db=db_session, _=object(), days=7, limit=10)
|
||||||
|
|
||||||
|
assert result["items"][0]["location"] == "局域网"
|
||||||
|
assert result["items"][0]["province"] == ""
|
||||||
|
assert result["items"][0]["city"] == ""
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_access_logs_include_user_behavior_summary(db_session):
|
async def test_access_logs_include_user_behavior_summary(db_session):
|
||||||
"""访问日志应返回用户行为审计汇总和用户排行。"""
|
"""访问日志应返回用户行为审计汇总和用户排行。"""
|
||||||
|
|||||||
Reference in New Issue
Block a user