chawin.chen commited on
Commit
640e3c9
·
1 Parent(s): 3183d9b

feat: 新增按国家/地区分组的统计功能

Browse files

- 后端: 添加 device_region_stats 和 image_region_stats API,支持按地区GROUP BY统计
- 前端: 在用户设备列表和检测历史页面添加国家分布统计条,显示Top10国家占比
- 支持点击国家行进行筛选

Files changed (3) hide show
  1. api_routes.py +58 -0
  2. database.py +58 -0
  3. models.py +11 -0
api_routes.py CHANGED
@@ -80,6 +80,8 @@ from database import (
80
  upsert_device_record,
81
  fetch_user_devices,
82
  count_user_devices,
 
 
83
  clean_debug_data,
84
  fetch_all,
85
  )
@@ -535,6 +537,8 @@ from models import (
535
  CelebrityMatchResponse,
536
  CategoryStatsResponse,
537
  CategoryStatItem,
 
 
538
  UserDeviceItem,
539
  UserDeviceListResponse,
540
  )
@@ -2320,6 +2324,60 @@ async def list_user_devices(
2320
  raise HTTPException(status_code=500, detail=str(e))
2321
 
2322
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2323
  @api_router.post("/clean_debug_data", tags=["系统维护"])
2324
  async def clean_debug_data_api():
2325
  """清理调试数据接口"""
 
80
  upsert_device_record,
81
  fetch_user_devices,
82
  count_user_devices,
83
+ fetch_device_region_stats,
84
+ fetch_image_region_stats,
85
  clean_debug_data,
86
  fetch_all,
87
  )
 
537
  CelebrityMatchResponse,
538
  CategoryStatsResponse,
539
  CategoryStatItem,
540
+ RegionStatsResponse,
541
+ RegionStatItem,
542
  UserDeviceItem,
543
  UserDeviceListResponse,
544
  )
 
2324
  raise HTTPException(status_code=500, detail=str(e))
2325
 
2326
 
2327
+ @api_router.get("/device_region_stats", response_model=RegionStatsResponse, tags=["用户设备列表"])
2328
+ async def get_device_region_stats():
2329
+ """查询用户设备按国家/地区分组的统计信息"""
2330
+ try:
2331
+ rows = await fetch_device_region_stats()
2332
+ stats: List[RegionStatItem] = []
2333
+ for row in rows:
2334
+ region_code = str(row.get("region") or "")
2335
+ region_country = get_country_name_by_region_code(region_code) if region_code else "未知地区"
2336
+ stats.append(RegionStatItem(
2337
+ region=region_code,
2338
+ region_country=region_country,
2339
+ count=int(row.get("count", 0) or 0),
2340
+ ))
2341
+ total = sum(item.count for item in stats)
2342
+ return RegionStatsResponse(stats=stats, total=total)
2343
+ except Exception as e:
2344
+ logger.error(f"Failed to get device region stats: {str(e)}")
2345
+ raise HTTPException(status_code=500, detail=str(e))
2346
+
2347
+
2348
+ @api_router.post("/image_region_stats", response_model=RegionStatsResponse, tags=["图片记录"])
2349
+ async def get_image_region_stats(
2350
+ category: Optional[str] = Query(None, description="类别筛选"),
2351
+ nickname: Optional[str] = Query(None, description="昵称筛选"),
2352
+ region: Optional[str] = Query(None, description="地区筛选"),
2353
+ platform: Optional[str] = Query(None, description="平台筛选"),
2354
+ is_cropped_face: Optional[int] = Query(None, description="是否裁剪人脸"),
2355
+ ):
2356
+ """查询图片记录按国家/地区分组的统计信息"""
2357
+ try:
2358
+ rows = await fetch_image_region_stats(
2359
+ category=category,
2360
+ nickname=nickname,
2361
+ region=region,
2362
+ platform=platform,
2363
+ is_cropped_face=is_cropped_face,
2364
+ )
2365
+ stats: List[RegionStatItem] = []
2366
+ for row in rows:
2367
+ region_code = str(row.get("region") or "")
2368
+ region_country = get_country_name_by_region_code(region_code) if region_code else "未知地区"
2369
+ stats.append(RegionStatItem(
2370
+ region=region_code,
2371
+ region_country=region_country,
2372
+ count=int(row.get("count", 0) or 0),
2373
+ ))
2374
+ total = sum(item.count for item in stats)
2375
+ return RegionStatsResponse(stats=stats, total=total)
2376
+ except Exception as e:
2377
+ logger.error(f"Failed to get image region stats: {str(e)}")
2378
+ raise HTTPException(status_code=500, detail=str(e))
2379
+
2380
+
2381
  @api_router.post("/clean_debug_data", tags=["系统维护"])
2382
  async def clean_debug_data_api():
2383
  """清理调试数据接口"""
database.py CHANGED
@@ -496,6 +496,64 @@ async def count_user_devices(
496
  return int(rows[0].get("total", 0) or 0)
497
 
498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  async def record_image_creation(
500
  *,
501
  file_path: str,
 
496
  return int(rows[0].get("total", 0) or 0)
497
 
498
 
499
+ async def fetch_device_region_stats() -> List[Dict[str, Any]]:
500
+ """统计用户设备按地区分组的数量"""
501
+ query = """
502
+ SELECT
503
+ COALESCE(region, '') AS region,
504
+ COUNT(*) AS count
505
+ FROM tpl_app_user_devices
506
+ GROUP BY COALESCE(region, '')
507
+ ORDER BY count DESC
508
+ """
509
+ return await fetch_all(query)
510
+
511
+
512
+ async def fetch_image_region_stats(
513
+ *,
514
+ category: Optional[str],
515
+ nickname: Optional[str],
516
+ region: Optional[str] = None,
517
+ platform: Optional[str] = None,
518
+ is_cropped_face: Optional[int] = None,
519
+ ) -> List[Dict[str, Any]]:
520
+ """统计图片记录按地区分组的数量"""
521
+ where_clauses: List[str] = []
522
+ params: List[Any] = []
523
+ if category and category != "all":
524
+ where_clauses.append("category = %s")
525
+ params.append(category)
526
+ if nickname:
527
+ where_clauses.append("nickname LIKE %s")
528
+ params.append(f"{nickname}%")
529
+ if platform == "android":
530
+ where_clauses.append("nickname LIKE %s")
531
+ params.append("android%")
532
+ elif platform == "ios":
533
+ where_clauses.append("nickname LIKE %s")
534
+ params.append("ios%")
535
+ elif platform == "miniprogram":
536
+ where_clauses.append("region = %s")
537
+ params.append("")
538
+ if region:
539
+ where_clauses.append("region = %s")
540
+ params.append(region)
541
+ if is_cropped_face is not None:
542
+ where_clauses.append("is_cropped_face = %s")
543
+ params.append(is_cropped_face)
544
+ where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
545
+ query = f"""
546
+ SELECT
547
+ COALESCE(region, '') AS region,
548
+ COUNT(*) AS count
549
+ FROM tpl_app_processed_images
550
+ {where_sql}
551
+ GROUP BY COALESCE(region, '')
552
+ ORDER BY count DESC
553
+ """
554
+ return await fetch_all(query, params)
555
+
556
+
557
  async def record_image_creation(
558
  *,
559
  file_path: str,
models.py CHANGED
@@ -74,6 +74,17 @@ class CategoryStatsResponse(BaseModel):
74
  total: int
75
 
76
 
 
 
 
 
 
 
 
 
 
 
 
77
  class UserDeviceItem(BaseModel):
78
  device_id: str
79
  device_type: Optional[str] = None
 
74
  total: int
75
 
76
 
77
+ class RegionStatItem(BaseModel):
78
+ region: str
79
+ region_country: str
80
+ count: int
81
+
82
+
83
+ class RegionStatsResponse(BaseModel):
84
+ stats: List[RegionStatItem]
85
+ total: int
86
+
87
+
88
  class UserDeviceItem(BaseModel):
89
  device_id: str
90
  device_type: Optional[str] = None