@@ -20,10 +20,30 @@ def __init__(self, message: str, code: int) -> None:
2020
2121
2222class MonitorService :
23- def __init__ (self , repo : MonitorRepository , redis : AsyncRedis ) -> None :
23+ def __init__ (
24+ self ,
25+ repo : MonitorRepository | None ,
26+ redis : AsyncRedis | None ,
27+ ) -> None :
2428 self .repo = repo
2529 self .redis = redis
2630
31+ def _require_repo (self ) -> MonitorRepository :
32+ if self .repo is None :
33+ raise MonitorDomainError (
34+ "Monitor repository is not configured" ,
35+ 500 ,
36+ )
37+ return self .repo
38+
39+ def _require_redis (self ) -> AsyncRedis :
40+ if self .redis is None :
41+ raise MonitorDomainError (
42+ "Redis client is not configured" ,
43+ 500 ,
44+ )
45+ return self .redis
46+
2747 @staticmethod
2848 def _get_server_status_payload () -> dict [str , float | int | None ]:
2949 cpu_percent = psutil .cpu_percent (interval = 1 )
@@ -58,17 +78,17 @@ async def get_overview(self, days: int) -> dict:
5878 end_time = datetime .now (UTC )
5979 start_time = end_time - timedelta (days = days )
6080
61- total_visits = await self .repo . count_visits_since ( start_time )
62- unique_visitors = await self . repo . count_unique_visitors_since (
63- start_time
64- )
65- unique_visitor_ids = await self . repo .count_unique_visitor_ids_since (
81+ repo = self ._require_repo ( )
82+
83+ total_visits = await repo . count_visits_since ( start_time )
84+ unique_visitors = await repo . count_unique_visitors_since ( start_time )
85+ unique_visitor_ids = await repo .count_unique_visitor_ids_since (
6686 start_time
6787 )
68- top_pages = await self . repo .get_top_pages_since (start_time , limit = 10 )
69- browser_stats = await self . repo .get_browser_stats_since (start_time )
70- os_stats = await self . repo .get_os_stats_since (start_time )
71- daily_trend = await self . repo .get_daily_trend_since (start_time )
88+ top_pages = await repo .get_top_pages_since (start_time , limit = 10 )
89+ browser_stats = await repo .get_browser_stats_since (start_time )
90+ os_stats = await repo .get_os_stats_since (start_time )
91+ daily_trend = await repo .get_daily_trend_since (start_time )
7292
7393 return {
7494 "total_visits" : total_visits ,
@@ -85,9 +105,11 @@ async def get_visitors(self, days: int, page: int, page_size: int) -> dict:
85105 end_time = datetime .now (UTC )
86106 start_time = end_time - timedelta (days = days )
87107
88- total = await self .repo .count_visits_since (start_time )
108+ repo = self ._require_repo ()
109+
110+ total = await repo .count_visits_since (start_time )
89111 offset = (page - 1 ) * page_size
90- visitors = await self . repo .list_visitors_since (
112+ visitors = await repo .list_visitors_since (
91113 start_time ,
92114 offset = offset ,
93115 limit = page_size ,
@@ -128,7 +150,9 @@ async def get_user_logins(
128150 end_time = datetime .now (UTC )
129151 start_time = end_time - timedelta (days = days )
130152
131- users = await self .repo .list_users_with_login_records ()
153+ repo = self ._require_repo ()
154+
155+ users = await repo .list_users_with_login_records ()
132156
133157 login_logs = []
134158 for user in users :
@@ -177,20 +201,21 @@ async def get_server_status(self) -> dict[str, float | int | None]:
177201 return self ._get_server_status_payload ()
178202
179203 async def get_online_users (self , include_user_details : bool ) -> dict :
180- online_count_raw = await self .redis .get ("stats:online_count" )
204+ redis = self ._require_redis ()
205+
206+ online_count_raw = await redis .get ("stats:online_count" )
181207 online_count = int (online_count_raw ) if online_count_raw else 0
182208
183- online_user_ids_raw = await self . redis .zrange ("online_users_z" , 0 , - 1 )
209+ online_user_ids_raw = await redis .zrange ("online_users_z" , 0 , - 1 )
184210 online_user_ids = [
185211 int (uid .decode () if isinstance (uid , bytes ) else str (uid ))
186212 for uid in online_user_ids_raw
187213 ]
188214
189215 user_details : list [dict ] = []
190216 if include_user_details and online_user_ids :
191- users : list [User ] = await self .repo .list_users_by_ids (
192- online_user_ids
193- )
217+ repo = self ._require_repo ()
218+ users : list [User ] = await repo .list_users_by_ids (online_user_ids )
194219 user_details = [
195220 {
196221 "id" : user .id ,
@@ -215,3 +240,94 @@ async def stream_server_status(self) -> AsyncIterator[str]:
215240 payload = self ._get_server_status_payload ()
216241 yield self ._to_sse_event (payload )
217242 await asyncio .sleep (5 )
243+
244+ async def get_daily_summary (self , date : datetime ) -> dict :
245+ """获取指定日期的访问统计摘要。
246+
247+ Args:
248+ date: 日期(取其当天 00:00 ~ 次日 00:00)
249+
250+ Returns:
251+ 包含总访问量、独立访客、热门页面等统计信息的字典
252+ """
253+ start = date .replace (hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
254+ end = start + timedelta (days = 1 )
255+
256+ repo = self ._require_repo ()
257+
258+ total_visits = await repo .count_visits_between (start , end )
259+ unique_visitors = await repo .count_unique_visitors_between (start , end )
260+ unique_ips = await repo .count_unique_ips_between (start , end )
261+ top_pages = await repo .get_top_pages_between (start , end , limit = 5 )
262+ browser_stats = await repo .get_browser_stats_between (start , end )
263+ os_stats = await repo .get_os_stats_between (start , end )
264+ device_stats = await repo .get_device_stats_between (start , end )
265+
266+ return {
267+ "date" : start .strftime ("%Y-%m-%d" ),
268+ "total_visits" : total_visits ,
269+ "unique_visitors" : unique_visitors ,
270+ "unique_ips" : unique_ips ,
271+ "top_pages" : top_pages ,
272+ "browser_stats" : browser_stats ,
273+ "os_stats" : os_stats ,
274+ "device_stats" : device_stats ,
275+ }
276+
277+ async def cleanup_stale_heartbeats (
278+ self , * , cutoff_seconds : int = 600
279+ ) -> dict :
280+ """清理过期心跳并同步用户在线状态到数据库。
281+
282+ Args:
283+ cutoff_seconds: 超时秒数,默认 600 秒 (10 分钟)
284+
285+ Returns:
286+ 包含清理统计信息的字典
287+ """
288+ from sqlalchemy import update
289+
290+ from app .api .des .db import AsyncSessionFactory
291+ from app .models .models import User
292+
293+ redis = self ._require_redis ()
294+
295+ now = int (datetime .now (UTC ).timestamp ())
296+ cutoff_time = now - cutoff_seconds
297+
298+ removed_count = await redis .zremrangebyscore (
299+ "online_users_z" , 0 , cutoff_time
300+ )
301+ online_count = await redis .zcard ("online_users_z" )
302+
303+ await redis .set ("stats:online_count" , str (online_count ), ex = 120 )
304+
305+ online_user_ids = await redis .zrange ("online_users_z" , 0 , - 1 )
306+ online_user_ids = [
307+ int (uid .decode () if isinstance (uid , bytes ) else str (uid ))
308+ for uid in online_user_ids
309+ ]
310+
311+ if online_user_ids :
312+ async with AsyncSessionFactory () as session :
313+ await session .execute (
314+ update (User )
315+ .where (User .id .in_ (online_user_ids ))
316+ .values (active = True )
317+ .execution_options (synchronize_session = False )
318+ )
319+ await session .execute (
320+ update (User )
321+ .where (
322+ User .id .not_in (online_user_ids ),
323+ User .active ,
324+ )
325+ .values (active = False )
326+ .execution_options (synchronize_session = False )
327+ )
328+ await session .commit ()
329+
330+ return {
331+ "removed_count" : removed_count ,
332+ "online_count" : online_count ,
333+ }
0 commit comments