feat(桌面与监控): 完善工作台导航和登录活动定位
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled

- 优化桌面标签、上下文标题、前进后退、导航栏隐藏和原生菜单体验

- 补充登录会话 IP 采集、地理位置回退、管理端展示及数据库迁移

- 更新桌面发布检查、运维文档和前后端测试覆盖
This commit is contained in:
Cheng Zhou
2026-07-13 16:03:20 +08:00
parent b26ebdda02
commit ab59476d10
50 changed files with 2086 additions and 191 deletions
@@ -0,0 +1,58 @@
import pytest
from app.core.config import settings
from app.services import ip_geolocation_fallback
from app.services.ip_geolocation_fallback import ExternalIpLocation
def test_ip2location_response_parser_requires_matching_public_ip_and_coordinates():
result = ip_geolocation_fallback._parse_response(
"8.8.8.8",
{
"ip": "8.8.8.8",
"country_code": "US",
"country_name": "United States of America",
"region_name": "California",
"city_name": "Mountain View",
"latitude": 37.38605,
"longitude": -122.08385,
"isp": "Google LLC",
},
)
assert result is not None
assert result.country_code == "US"
assert result.longitude == -122.08385
assert ip_geolocation_fallback._parse_response("8.8.8.8", {"ip": "1.1.1.1"}) is None
assert ip_geolocation_fallback._global_ip("192.168.1.8") is None
@pytest.mark.asyncio
async def test_external_fallback_caches_public_ip_results(monkeypatch):
calls = []
async def fake_fetch(_client, _semaphore, ip_address):
calls.append(ip_address)
return ExternalIpLocation(
ip_address=ip_address,
country="United States of America",
country_code="US",
region="California",
city="Mountain View",
isp="Google LLC",
longitude=-122.08385,
latitude=37.38605,
)
monkeypatch.setattr(settings, "ENV", "development")
monkeypatch.setattr(settings, "MONITORING_IP_GEO_FALLBACK_ENABLED", True)
monkeypatch.setattr(ip_geolocation_fallback, "_fetch_one", fake_fetch)
ip_geolocation_fallback.reset_ip_geolocation_fallback_cache()
first = await ip_geolocation_fallback.resolve_external_ip_locations(["8.8.8.8", "192.168.1.8"])
second = await ip_geolocation_fallback.resolve_external_ip_locations(["8.8.8.8"])
assert first["8.8.8.8"].city == "Mountain View"
assert second["8.8.8.8"].latitude == 37.38605
assert calls == ["8.8.8.8"]
ip_geolocation_fallback.reset_ip_geolocation_fallback_cache()
@@ -4,6 +4,7 @@ import pytest
from app.core.config import settings
from app.services import monitoring_server_location
from app.services.ip_geolocation_fallback import ExternalIpLocation
def test_public_ip_normalization_rejects_private_addresses():
@@ -51,3 +52,45 @@ async def test_server_location_returns_none_instead_of_a_hardcoded_fallback(monk
assert location is None
monitoring_server_location.reset_monitoring_server_location_cache()
@pytest.mark.asyncio
async def test_server_location_uses_external_coordinates_when_local_coordinates_are_missing(monkeypatch):
monkeypatch.setattr(settings, "MONITORING_SERVER_PUBLIC_IP", "5.34.216.210")
monkeypatch.setattr(
monitoring_server_location,
"resolve_ip_location",
lambda _ip: SimpleNamespace(
location="公网",
country="",
province="",
city="",
isp="",
),
)
async def fake_external_lookup(ip_addresses):
assert ip_addresses == ["5.34.216.210"]
return {
"5.34.216.210": ExternalIpLocation(
ip_address="5.34.216.210",
country="United States of America",
country_code="US",
region="California",
city="Los Angeles",
isp="Example ISP",
longitude=-118.2439,
latitude=34.05257,
)
}
monkeypatch.setattr(monitoring_server_location, "resolve_external_ip_locations", fake_external_lookup)
monitoring_server_location.reset_monitoring_server_location_cache()
location = await monitoring_server_location.resolve_monitoring_server_location()
assert location is not None
assert location.name == "United States of America / California / Los Angeles"
assert location.longitude == -118.2439
assert location.latitude == 34.05257
monitoring_server_location.reset_monitoring_server_location_cache()
@@ -13,6 +13,7 @@ from app.core.permission_monitor import set_permission_monitor, PermissionMonito
from app.api.v1 import permission_monitoring
from app.api.v1.system_permissions import list_system_permissions
from app.services.monitoring_server_location import MonitoringServerLocation
from app.services.ip_geolocation_fallback import ExternalIpLocation
class FakeIpInfo:
@@ -583,6 +584,57 @@ async def test_ip_locations_counts_unique_users_per_location(db_session, monkeyp
assert sorted(resolved_ips) == ["10.1.1.1", "10.1.1.2", "10.1.1.3"]
@pytest.mark.asyncio
async def test_ip_locations_uses_external_coordinates_only_when_local_coordinates_are_missing(db_session, monkeypatch):
await db_session.execute(text("DELETE FROM permission_access_logs"))
await db_session.execute(text("DELETE FROM security_access_logs"))
await db_session.commit()
study_id = uuid.uuid4()
user_id = uuid.uuid4()
await _seed_permission_log(db_session, study_id, user_id, allowed=True, elapsed_ms=3.2)
await db_session.execute(
text("UPDATE permission_access_logs SET ip_address = :ip_address"),
{"ip_address": "8.8.8.8"},
)
await db_session.commit()
monkeypatch.setattr(
permission_monitoring,
"resolve_ip_location",
lambda _ip: FakeIpInfo("", "", isp="", country="未知国家"),
)
async def fake_external_lookup(ip_addresses):
assert ip_addresses == ["8.8.8.8"]
return {
"8.8.8.8": ExternalIpLocation(
ip_address="8.8.8.8",
country="United States of America",
country_code="US",
region="California",
city="Mountain View",
isp="Google LLC",
longitude=-122.08385,
latitude=37.38605,
)
}
monkeypatch.setattr(permission_monitoring, "resolve_external_ip_locations", fake_external_lookup)
result = await permission_monitoring.get_ip_locations(
db=db_session,
_=AdminUserStub(),
days=7,
limit=10,
)
assert result["items"][0]["longitude"] == -122.08385
assert result["items"][0]["latitude"] == 37.38605
assert result["items"][0]["city"] == "Mountain View"
assert result["data_quality"]["external_fallback_ip_count"] == 1
assert result["data_quality"]["resolver"] == "ip2region+ip2location.io"
@pytest.mark.asyncio
async def test_ip_locations_merges_same_region_with_different_isp(db_session, monkeypatch):
"""同一省市的 IP 属地统计不应因运营商不同拆分。"""
+66 -2
View File
@@ -1,6 +1,7 @@
from datetime import datetime, timedelta, timezone
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import SimpleNamespace
import pytest
@@ -8,7 +9,13 @@ from app.core.config import settings
from app.crud import user as user_crud
from app.models.user import User, UserStatus
from app.models.user_login_session import UserLoginSession
from app.services.user_login_sessions import get_login_summaries, session_id_from_payload
from app.schemas.user import UserLoginActivityRead
from app.services.user_login_sessions import (
create_login_session,
get_login_summaries,
login_activity_payload,
session_id_from_payload,
)
def test_legacy_session_id_is_stable_without_storing_a_token():
@@ -28,6 +35,63 @@ def test_session_heartbeat_returns_server_observed_client_ip():
assert '"client_ip": resolve_client_ip(request)' in auth_source
@pytest.mark.asyncio
async def test_login_session_stores_server_observed_login_ip(db_session):
user = User(
id=uuid.uuid4(),
email="login-ip@example.com",
password_hash="hash",
full_name="Login IP",
clinical_department="IT",
status=UserStatus.ACTIVE,
)
db_session.add(user)
await db_session.commit()
request = SimpleNamespace(
headers={
"x-ctms-client-type": "desktop",
"x-ctms-client-platform": "macos",
"x-ctms-client-version": "0.1.0",
},
client=SimpleNamespace(host="203.0.113.18"),
)
session = await create_login_session(
db_session,
session_id=uuid.uuid4(),
user_id=user.id,
request=request,
)
assert session.login_ip == "203.0.113.18"
assert session.client_type == "desktop"
def test_login_activity_payload_uses_server_authoritative_status_and_local_ip_location(monkeypatch):
now = datetime.now(timezone.utc)
monkeypatch.setattr(settings, "USER_SESSION_ONLINE_SECONDS", 300)
session = UserLoginSession(
id=uuid.uuid4(),
user_id=uuid.uuid4(),
client_type="web",
login_ip="127.0.0.1",
login_at=now - timedelta(minutes=10),
last_seen_at=now - timedelta(seconds=30),
)
online = login_activity_payload(session, reference_time=now)
assert online["activity_status"] == "ONLINE"
assert online["login_ip"] == "127.0.0.1"
assert online["ip_location"] == "本机"
assert UserLoginActivityRead.model_validate(online).activity_status == "ONLINE"
session.last_seen_at = now - timedelta(minutes=6)
assert login_activity_payload(session, reference_time=now)["activity_status"] == "OFFLINE"
session.ended_at = now - timedelta(minutes=1)
assert login_activity_payload(session, reference_time=now)["activity_status"] == "ENDED"
@pytest.mark.asyncio
async def test_login_summary_marks_recent_unended_sessions_online(db_session, monkeypatch):
now = datetime.now(timezone.utc)