chawin.chen Qwen-Coder commited on
Commit ·
606732d
1
Parent(s): 102bb27
feat: 新增用户设备列表API、region筛选和配置接口
Browse files- 新增 /facescore/user_devices 接口,支持分页、device_id/region 筛选
- 新增 /facescore/config 配置接口,返回 region_codes 等字典配置
- 新增 /facescore/clean_debug_data 清理调试数据接口
- 新增 region_code_utils.py 地区代码转国家名称工具类(完整250+国家)
- database.py 新增 fetch_user_devices/count_user_devices/clean_debug_data
- models.py 新增 UserDeviceItem/UserDeviceListResponse,ImageScoreItem 增加 region/region_country
- 修改 outputs 接口支持 region 筛选并返回 region/region_country
- 所有 App 端接口(/app/analyze, /restore, /upscale, /face_verify, /upcolor, /anime_style, /celebrity/match)添加 region 获取和存储逻辑
- 设备列表支持检测总量统计(子查询关联 tpl_app_processed_images)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- api_routes.py +150 -17
- database.py +118 -3
- models.py +26 -0
- region_code_utils.py +294 -0
api_routes.py
CHANGED
|
@@ -76,10 +76,42 @@ from database import (
|
|
| 76 |
infer_category_from_filename,
|
| 77 |
fetch_today_category_counts,
|
| 78 |
upsert_device_record,
|
|
|
|
|
|
|
|
|
|
| 79 |
)
|
|
|
|
| 80 |
|
| 81 |
SERVER_HOSTNAME = os.environ.get("HOSTNAME", "")
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
# 尝试导入DeepFace
|
| 84 |
deepface_module = None
|
| 85 |
if DEEPFACE_AVAILABLE:
|
|
@@ -339,6 +371,8 @@ from models import (
|
|
| 339 |
CelebrityMatchResponse,
|
| 340 |
CategoryStatsResponse,
|
| 341 |
CategoryStatItem,
|
|
|
|
|
|
|
| 342 |
)
|
| 343 |
|
| 344 |
from face_analyzer import EnhancedFaceAnalyzer
|
|
@@ -743,6 +777,7 @@ async def _record_output_file(
|
|
| 743 |
category: Optional[str] = None,
|
| 744 |
bos_uploaded: bool = False,
|
| 745 |
score: Optional[float] = None,
|
|
|
|
| 746 |
extra: Optional[Dict[str, Any]] = None,
|
| 747 |
) -> None:
|
| 748 |
"""封装的图片记录写入,避免影响主流程"""
|
|
@@ -762,6 +797,7 @@ async def _record_output_file(
|
|
| 762 |
category=category,
|
| 763 |
bos_uploaded=bos_uploaded,
|
| 764 |
score=score_value,
|
|
|
|
| 765 |
extra_metadata=extra,
|
| 766 |
)
|
| 767 |
# 使用与数据库一致的路径转换
|
|
@@ -1602,23 +1638,7 @@ async def app_analyze_face(
|
|
| 1602 |
App专用人脸分析接口,参数与原 /analyze 保持完全一致并透传。
|
| 1603 |
"""
|
| 1604 |
# 异步记录设备信息
|
| 1605 |
-
|
| 1606 |
-
if device_id:
|
| 1607 |
-
try:
|
| 1608 |
-
device_info = {
|
| 1609 |
-
"device_id": device_id,
|
| 1610 |
-
"device_type": request.headers.get("x-device-type"),
|
| 1611 |
-
"device_model": request.headers.get("x-device-model"),
|
| 1612 |
-
"os_version": request.headers.get("x-os-version"),
|
| 1613 |
-
"app_version": request.headers.get("x-app-version"),
|
| 1614 |
-
"region": request.headers.get("x-region"),
|
| 1615 |
-
"timezone": request.headers.get("x-timezone"),
|
| 1616 |
-
"language": request.headers.get("Accept-Language"),
|
| 1617 |
-
}
|
| 1618 |
-
# 创建异步任务,不阻塞当前请求
|
| 1619 |
-
asyncio.create_task(upsert_device_record(**device_info))
|
| 1620 |
-
except Exception as e:
|
| 1621 |
-
logger.warning(f"Failed to record device info for {device_id}: {e}")
|
| 1622 |
|
| 1623 |
return await analyze_face(
|
| 1624 |
request=request,
|
|
@@ -1877,6 +1897,87 @@ async def get_daily_category_stats():
|
|
| 1877 |
return CategoryStatsResponse(stats=stats, total=total)
|
| 1878 |
|
| 1879 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1880 |
@api_router.post("/outputs", response_model=PagedImageFileList, tags=["检测列表"])
|
| 1881 |
@log_api_params
|
| 1882 |
async def list_outputs(
|
|
@@ -1951,6 +2052,8 @@ async def list_outputs(
|
|
| 1951 |
is_cropped = False
|
| 1952 |
nickname_value = record.get("nickname") if record else None
|
| 1953 |
hostname_value = record.get("hostname") if record else None
|
|
|
|
|
|
|
| 1954 |
last_modified_dt = None
|
| 1955 |
|
| 1956 |
if record:
|
|
@@ -1987,6 +2090,8 @@ async def list_outputs(
|
|
| 1987 |
"last_modified": last_modified_str,
|
| 1988 |
"nickname": nickname_value,
|
| 1989 |
"hostname": hostname_value,
|
|
|
|
|
|
|
| 1990 |
}
|
| 1991 |
all_files.append(file_info)
|
| 1992 |
|
|
@@ -2012,9 +2117,11 @@ async def list_outputs(
|
|
| 2012 |
# 普通文件列表模式(无关键词或CLIP不可用)
|
| 2013 |
logger.info("Returning regular file list")
|
| 2014 |
try:
|
|
|
|
| 2015 |
total_count = await count_image_records(
|
| 2016 |
category=category,
|
| 2017 |
nickname=nickname_filter,
|
|
|
|
| 2018 |
is_cropped_face=is_cropped_filter,
|
| 2019 |
)
|
| 2020 |
if total_count > 0:
|
|
@@ -2022,6 +2129,7 @@ async def list_outputs(
|
|
| 2022 |
rows = await fetch_paged_image_records(
|
| 2023 |
category=category,
|
| 2024 |
nickname=nickname_filter,
|
|
|
|
| 2025 |
offset=offset,
|
| 2026 |
limit=page_size,
|
| 2027 |
is_cropped_face=is_cropped_filter,
|
|
@@ -2038,6 +2146,9 @@ async def list_outputs(
|
|
| 2038 |
else:
|
| 2039 |
last_modified_dt = last_modified
|
| 2040 |
size_bytes = int(row.get("size_bytes") or 0)
|
|
|
|
|
|
|
|
|
|
| 2041 |
paged_results.append({
|
| 2042 |
"file_path": row.get("file_path"),
|
| 2043 |
"score": float(row.get("score") or 0.0),
|
|
@@ -2048,6 +2159,8 @@ async def list_outputs(
|
|
| 2048 |
"%Y-%m-%d %H:%M:%S") if last_modified_dt else "",
|
| 2049 |
"nickname": row.get("nickname"),
|
| 2050 |
"hostname": row.get("hostname"),
|
|
|
|
|
|
|
| 2051 |
})
|
| 2052 |
total_pages = (total_count + page_size - 1) // page_size
|
| 2053 |
return PagedImageFileList(
|
|
@@ -2097,6 +2210,8 @@ async def list_outputs(
|
|
| 2097 |
),
|
| 2098 |
"nickname": None,
|
| 2099 |
"hostname": None,
|
|
|
|
|
|
|
| 2100 |
}
|
| 2101 |
all_files.append(file_info)
|
| 2102 |
|
|
@@ -2336,6 +2451,7 @@ async def sync_bos_resources(
|
|
| 2336 |
@api_router.post("/restore")
|
| 2337 |
@log_api_params
|
| 2338 |
async def restore_old_photo(
|
|
|
|
| 2339 |
file: UploadFile = File(...),
|
| 2340 |
md5: str = Query(None, description="前端传递的文件md5,用于提前保存记录"),
|
| 2341 |
colorize: bool = Query(False, description="是否对黑白照片进行上色"),
|
|
@@ -2453,6 +2569,7 @@ async def restore_old_photo(
|
|
| 2453 |
nickname=nickname,
|
| 2454 |
category="restore",
|
| 2455 |
bos_uploaded=True,
|
|
|
|
| 2456 |
extra={
|
| 2457 |
"source": "restore",
|
| 2458 |
"colorize": colorize,
|
|
@@ -2484,6 +2601,7 @@ async def restore_old_photo(
|
|
| 2484 |
@api_router.post("/upcolor")
|
| 2485 |
@log_api_params
|
| 2486 |
async def colorize_photo(
|
|
|
|
| 2487 |
file: UploadFile = File(...),
|
| 2488 |
md5: str = Query(None, description="前端传递的文件md5,用于提前保存记录"),
|
| 2489 |
nickname: str = Form(None, description="操作者昵称"),
|
|
@@ -2563,6 +2681,7 @@ async def colorize_photo(
|
|
| 2563 |
nickname=nickname,
|
| 2564 |
category="upcolor",
|
| 2565 |
bos_uploaded=True,
|
|
|
|
| 2566 |
extra={
|
| 2567 |
"source": "upcolor",
|
| 2568 |
"md5": actual_md5,
|
|
@@ -2663,6 +2782,7 @@ async def preload_anime_models(
|
|
| 2663 |
@api_router.post("/anime_style")
|
| 2664 |
@log_api_params
|
| 2665 |
async def anime_stylize_photo(
|
|
|
|
| 2666 |
file: UploadFile = File(...),
|
| 2667 |
style_type: str = Form("handdrawn",
|
| 2668 |
description="动漫风格类型: handdrawn=手绘风格, disney=迪士尼风格, illustration=插画风格, artstyle=艺术风格, anime=二次元风格, sketch=素描风格"),
|
|
@@ -2755,6 +2875,7 @@ async def anime_stylize_photo(
|
|
| 2755 |
nickname=nickname,
|
| 2756 |
category="anime_style",
|
| 2757 |
bos_uploaded=original_bos_uploaded,
|
|
|
|
| 2758 |
extra={
|
| 2759 |
"source": "anime_style",
|
| 2760 |
"style_type": style_type,
|
|
@@ -2793,6 +2914,7 @@ async def anime_stylize_photo(
|
|
| 2793 |
nickname=nickname,
|
| 2794 |
category="anime_style",
|
| 2795 |
bos_uploaded=bos_uploaded,
|
|
|
|
| 2796 |
extra={
|
| 2797 |
"source": "anime_style",
|
| 2798 |
"style_type": style_type,
|
|
@@ -2922,6 +3044,7 @@ async def grayscale_photo(
|
|
| 2922 |
@api_router.post("/upscale")
|
| 2923 |
@log_api_params
|
| 2924 |
async def upscale_photo(
|
|
|
|
| 2925 |
file: UploadFile = File(...),
|
| 2926 |
md5: str = Query(None, description="前端传递的文件md5,用于提前保存记录"),
|
| 2927 |
scale: int = Query(UPSCALE_SIZE, description="放大倍数,支持2或4倍"),
|
|
@@ -3010,6 +3133,7 @@ async def upscale_photo(
|
|
| 3010 |
nickname=nickname,
|
| 3011 |
category="upscale",
|
| 3012 |
bos_uploaded=True,
|
|
|
|
| 3013 |
extra={
|
| 3014 |
"source": "upscale",
|
| 3015 |
"md5": actual_md5,
|
|
@@ -4283,6 +4407,7 @@ async def load_celebrity_database():
|
|
| 4283 |
@api_router.post("/celebrity/match", tags=["Face Recognition"])
|
| 4284 |
@log_api_params
|
| 4285 |
async def match_celebrity_face(
|
|
|
|
| 4286 |
file: UploadFile = File(..., description="待匹配的用户图片"),
|
| 4287 |
nickname: str = Form(None, description="操作者昵称"),
|
| 4288 |
):
|
|
@@ -4346,6 +4471,7 @@ async def match_celebrity_face(
|
|
| 4346 |
file_path=temp_path,
|
| 4347 |
nickname=nickname,
|
| 4348 |
category="celebrity",
|
|
|
|
| 4349 |
extra={
|
| 4350 |
"source": "celebrity_match",
|
| 4351 |
"role": "query",
|
|
@@ -4502,6 +4628,7 @@ async def match_celebrity_face(
|
|
| 4502 |
file_path=face_path,
|
| 4503 |
nickname=nickname,
|
| 4504 |
category="face",
|
|
|
|
| 4505 |
extra={
|
| 4506 |
"source": "celebrity_match",
|
| 4507 |
"role": "face_crop",
|
|
@@ -4522,6 +4649,7 @@ async def match_celebrity_face(
|
|
| 4522 |
file_path=temp_path,
|
| 4523 |
nickname=nickname,
|
| 4524 |
category="celebrity",
|
|
|
|
| 4525 |
extra={
|
| 4526 |
"source": "celebrity_match",
|
| 4527 |
"role": "annotated",
|
|
@@ -4571,6 +4699,7 @@ async def match_celebrity_face(
|
|
| 4571 |
@api_router.post("/face_verify")
|
| 4572 |
@log_api_params
|
| 4573 |
async def face_similarity_verification(
|
|
|
|
| 4574 |
file1: UploadFile = File(..., description="第一张人脸图片"),
|
| 4575 |
file2: UploadFile = File(..., description="第二张人脸图片"),
|
| 4576 |
nickname: str = Form(None, description="操作者昵称"),
|
|
@@ -4773,6 +4902,7 @@ async def face_similarity_verification(
|
|
| 4773 |
file_path=original_path1,
|
| 4774 |
nickname=nickname,
|
| 4775 |
category="original",
|
|
|
|
| 4776 |
extra={
|
| 4777 |
"source": "face_verify",
|
| 4778 |
"role": "original1",
|
|
@@ -4784,6 +4914,7 @@ async def face_similarity_verification(
|
|
| 4784 |
file_path=original_path2,
|
| 4785 |
nickname=nickname,
|
| 4786 |
category="original",
|
|
|
|
| 4787 |
extra={
|
| 4788 |
"source": "face_verify",
|
| 4789 |
"role": "original2",
|
|
@@ -4816,6 +4947,7 @@ async def face_similarity_verification(
|
|
| 4816 |
file_path=face_path1,
|
| 4817 |
nickname=nickname,
|
| 4818 |
category="face",
|
|
|
|
| 4819 |
extra={
|
| 4820 |
"source": "face_verify",
|
| 4821 |
"role": "face1",
|
|
@@ -4826,6 +4958,7 @@ async def face_similarity_verification(
|
|
| 4826 |
file_path=face_path2,
|
| 4827 |
nickname=nickname,
|
| 4828 |
category="face",
|
|
|
|
| 4829 |
extra={
|
| 4830 |
"source": "face_verify",
|
| 4831 |
"role": "face2",
|
|
|
|
| 76 |
infer_category_from_filename,
|
| 77 |
fetch_today_category_counts,
|
| 78 |
upsert_device_record,
|
| 79 |
+
fetch_user_devices,
|
| 80 |
+
count_user_devices,
|
| 81 |
+
clean_debug_data,
|
| 82 |
)
|
| 83 |
+
from region_code_utils import get_country_name_by_region_code, REGION_CODE_TO_NAME
|
| 84 |
|
| 85 |
SERVER_HOSTNAME = os.environ.get("HOSTNAME", "")
|
| 86 |
|
| 87 |
+
|
| 88 |
+
async def _record_device_info(request: Request) -> None:
|
| 89 |
+
"""
|
| 90 |
+
从请求头中提取设备信息并异步记录到数据库
|
| 91 |
+
获取不到 region 时存空串
|
| 92 |
+
"""
|
| 93 |
+
device_id = request.headers.get("x-device-id")
|
| 94 |
+
if device_id:
|
| 95 |
+
try:
|
| 96 |
+
device_info = {
|
| 97 |
+
"device_id": device_id,
|
| 98 |
+
"device_type": request.headers.get("x-device-type"),
|
| 99 |
+
"device_model": request.headers.get("x-device-model"),
|
| 100 |
+
"os_version": request.headers.get("x-os-version"),
|
| 101 |
+
"app_version": request.headers.get("x-app-version"),
|
| 102 |
+
"region": request.headers.get("x-region") or "",
|
| 103 |
+
"timezone": request.headers.get("x-timezone"),
|
| 104 |
+
"language": request.headers.get("Accept-Language"),
|
| 105 |
+
}
|
| 106 |
+
asyncio.create_task(upsert_device_record(**device_info))
|
| 107 |
+
except Exception as e:
|
| 108 |
+
logger.warning(f"Failed to record device info for {device_id}: {e}")
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _get_region_from_headers(request: Request) -> str:
|
| 112 |
+
"""从请求头获取 region,获取不到返回空串"""
|
| 113 |
+
return request.headers.get("x-region") or ""
|
| 114 |
+
|
| 115 |
# 尝试导入DeepFace
|
| 116 |
deepface_module = None
|
| 117 |
if DEEPFACE_AVAILABLE:
|
|
|
|
| 371 |
CelebrityMatchResponse,
|
| 372 |
CategoryStatsResponse,
|
| 373 |
CategoryStatItem,
|
| 374 |
+
UserDeviceItem,
|
| 375 |
+
UserDeviceListResponse,
|
| 376 |
)
|
| 377 |
|
| 378 |
from face_analyzer import EnhancedFaceAnalyzer
|
|
|
|
| 777 |
category: Optional[str] = None,
|
| 778 |
bos_uploaded: bool = False,
|
| 779 |
score: Optional[float] = None,
|
| 780 |
+
region: Optional[str] = None,
|
| 781 |
extra: Optional[Dict[str, Any]] = None,
|
| 782 |
) -> None:
|
| 783 |
"""封装的图片记录写入,避免影响主流程"""
|
|
|
|
| 797 |
category=category,
|
| 798 |
bos_uploaded=bos_uploaded,
|
| 799 |
score=score_value,
|
| 800 |
+
region=region,
|
| 801 |
extra_metadata=extra,
|
| 802 |
)
|
| 803 |
# 使用与数据库一致的路径转换
|
|
|
|
| 1638 |
App专用人脸分析接口,参数与原 /analyze 保持完全一致并透传。
|
| 1639 |
"""
|
| 1640 |
# 异步记录设备信息
|
| 1641 |
+
await _record_device_info(request)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1642 |
|
| 1643 |
return await analyze_face(
|
| 1644 |
request=request,
|
|
|
|
| 1897 |
return CategoryStatsResponse(stats=stats, total=total)
|
| 1898 |
|
| 1899 |
|
| 1900 |
+
@api_router.post("/user_devices", response_model=UserDeviceListResponse, tags=["用户设备列表"])
|
| 1901 |
+
@log_api_params
|
| 1902 |
+
async def list_user_devices(
|
| 1903 |
+
device_id: Optional[str] = Query(None, description="设备 ID 筛选"),
|
| 1904 |
+
region: Optional[str] = Query(None, description="地区筛选"),
|
| 1905 |
+
page: int = Query(1, ge=1, description="页码"),
|
| 1906 |
+
page_size: int = Query(10, ge=1, le=100, description="每页数量"),
|
| 1907 |
+
):
|
| 1908 |
+
"""查询用户设备列表,支持按 device_id 和 region 筛选,按 created_at 倒序"""
|
| 1909 |
+
try:
|
| 1910 |
+
offset = (page - 1) * page_size
|
| 1911 |
+
|
| 1912 |
+
devices = await fetch_user_devices(
|
| 1913 |
+
device_id=device_id,
|
| 1914 |
+
region=region,
|
| 1915 |
+
offset=offset,
|
| 1916 |
+
limit=page_size,
|
| 1917 |
+
)
|
| 1918 |
+
|
| 1919 |
+
total_count = await count_user_devices(device_id=device_id, region=region)
|
| 1920 |
+
total_pages = (total_count + page_size - 1) // page_size
|
| 1921 |
+
|
| 1922 |
+
results = []
|
| 1923 |
+
for device in devices:
|
| 1924 |
+
region_code = device.get("region") or ""
|
| 1925 |
+
region_country = get_country_name_by_region_code(region_code) if region_code else ""
|
| 1926 |
+
|
| 1927 |
+
results.append(UserDeviceItem(
|
| 1928 |
+
device_id=device.get("device_id", ""),
|
| 1929 |
+
device_type=device.get("device_type"),
|
| 1930 |
+
device_model=device.get("device_model"),
|
| 1931 |
+
os_version=device.get("os_version"),
|
| 1932 |
+
app_version=device.get("app_version"),
|
| 1933 |
+
timezone=device.get("timezone"),
|
| 1934 |
+
region=region_code,
|
| 1935 |
+
region_country=region_country,
|
| 1936 |
+
language=device.get("language"),
|
| 1937 |
+
created_at=device.get("created_at", "").strftime("%Y-%m-%d %H:%M:%S") if device.get("created_at") else "",
|
| 1938 |
+
updated_at=device.get("updated_at", "").strftime("%Y-%m-%d %H:%M:%S") if device.get("updated_at") else "",
|
| 1939 |
+
))
|
| 1940 |
+
# 添加 detection_count 字段
|
| 1941 |
+
results[-1].detection_count = device.get("detection_count", 0)
|
| 1942 |
+
|
| 1943 |
+
return UserDeviceListResponse(
|
| 1944 |
+
results=results,
|
| 1945 |
+
count=total_count,
|
| 1946 |
+
page=page,
|
| 1947 |
+
page_size=page_size,
|
| 1948 |
+
total_pages=total_pages,
|
| 1949 |
+
)
|
| 1950 |
+
except Exception as e:
|
| 1951 |
+
logger.error(f"Failed to get user devices list: {str(e)}")
|
| 1952 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 1953 |
+
|
| 1954 |
+
|
| 1955 |
+
@api_router.post("/clean_debug_data", tags=["系统维护"])
|
| 1956 |
+
async def clean_debug_data_api():
|
| 1957 |
+
"""清理调试数据接口"""
|
| 1958 |
+
try:
|
| 1959 |
+
results = await clean_debug_data()
|
| 1960 |
+
return {
|
| 1961 |
+
"success": True,
|
| 1962 |
+
"message": "清理成功",
|
| 1963 |
+
"details": results,
|
| 1964 |
+
}
|
| 1965 |
+
except Exception as e:
|
| 1966 |
+
logger.error(f"Failed to clean debug data: {str(e)}")
|
| 1967 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 1968 |
+
|
| 1969 |
+
|
| 1970 |
+
@api_router.get("/config", tags=["系统配置"])
|
| 1971 |
+
async def get_app_config():
|
| 1972 |
+
"""
|
| 1973 |
+
获取前端应用配置字典
|
| 1974 |
+
返回地区代码映射表等配置项,支持后续扩展
|
| 1975 |
+
"""
|
| 1976 |
+
return {
|
| 1977 |
+
"region_codes": REGION_CODE_TO_NAME,
|
| 1978 |
+
}
|
| 1979 |
+
|
| 1980 |
+
|
| 1981 |
@api_router.post("/outputs", response_model=PagedImageFileList, tags=["检测列表"])
|
| 1982 |
@log_api_params
|
| 1983 |
async def list_outputs(
|
|
|
|
| 2052 |
is_cropped = False
|
| 2053 |
nickname_value = record.get("nickname") if record else None
|
| 2054 |
hostname_value = record.get("hostname") if record else None
|
| 2055 |
+
region_code = record.get("region") or "" if record else ""
|
| 2056 |
+
region_country = get_country_name_by_region_code(region_code) if region_code else ""
|
| 2057 |
last_modified_dt = None
|
| 2058 |
|
| 2059 |
if record:
|
|
|
|
| 2090 |
"last_modified": last_modified_str,
|
| 2091 |
"nickname": nickname_value,
|
| 2092 |
"hostname": hostname_value,
|
| 2093 |
+
"region": region_code,
|
| 2094 |
+
"region_country": region_country,
|
| 2095 |
}
|
| 2096 |
all_files.append(file_info)
|
| 2097 |
|
|
|
|
| 2117 |
# 普通文件列表模式(无关键词或CLIP不可用)
|
| 2118 |
logger.info("Returning regular file list")
|
| 2119 |
try:
|
| 2120 |
+
region_filter = request.region if hasattr(request, 'region') else None
|
| 2121 |
total_count = await count_image_records(
|
| 2122 |
category=category,
|
| 2123 |
nickname=nickname_filter,
|
| 2124 |
+
region=region_filter,
|
| 2125 |
is_cropped_face=is_cropped_filter,
|
| 2126 |
)
|
| 2127 |
if total_count > 0:
|
|
|
|
| 2129 |
rows = await fetch_paged_image_records(
|
| 2130 |
category=category,
|
| 2131 |
nickname=nickname_filter,
|
| 2132 |
+
region=region_filter,
|
| 2133 |
offset=offset,
|
| 2134 |
limit=page_size,
|
| 2135 |
is_cropped_face=is_cropped_filter,
|
|
|
|
| 2146 |
else:
|
| 2147 |
last_modified_dt = last_modified
|
| 2148 |
size_bytes = int(row.get("size_bytes") or 0)
|
| 2149 |
+
region_code = row.get("region") or ""
|
| 2150 |
+
region_country = get_country_name_by_region_code(region_code) if region_code else ""
|
| 2151 |
+
|
| 2152 |
paged_results.append({
|
| 2153 |
"file_path": row.get("file_path"),
|
| 2154 |
"score": float(row.get("score") or 0.0),
|
|
|
|
| 2159 |
"%Y-%m-%d %H:%M:%S") if last_modified_dt else "",
|
| 2160 |
"nickname": row.get("nickname"),
|
| 2161 |
"hostname": row.get("hostname"),
|
| 2162 |
+
"region": region_code,
|
| 2163 |
+
"region_country": region_country,
|
| 2164 |
})
|
| 2165 |
total_pages = (total_count + page_size - 1) // page_size
|
| 2166 |
return PagedImageFileList(
|
|
|
|
| 2210 |
),
|
| 2211 |
"nickname": None,
|
| 2212 |
"hostname": None,
|
| 2213 |
+
"region": None,
|
| 2214 |
+
"region_country": None,
|
| 2215 |
}
|
| 2216 |
all_files.append(file_info)
|
| 2217 |
|
|
|
|
| 2451 |
@api_router.post("/restore")
|
| 2452 |
@log_api_params
|
| 2453 |
async def restore_old_photo(
|
| 2454 |
+
request: Request,
|
| 2455 |
file: UploadFile = File(...),
|
| 2456 |
md5: str = Query(None, description="前端传递的文件md5,用于提前保存记录"),
|
| 2457 |
colorize: bool = Query(False, description="是否对黑白照片进行上色"),
|
|
|
|
| 2569 |
nickname=nickname,
|
| 2570 |
category="restore",
|
| 2571 |
bos_uploaded=True,
|
| 2572 |
+
region=_get_region_from_headers(request),
|
| 2573 |
extra={
|
| 2574 |
"source": "restore",
|
| 2575 |
"colorize": colorize,
|
|
|
|
| 2601 |
@api_router.post("/upcolor")
|
| 2602 |
@log_api_params
|
| 2603 |
async def colorize_photo(
|
| 2604 |
+
request: Request,
|
| 2605 |
file: UploadFile = File(...),
|
| 2606 |
md5: str = Query(None, description="前端传递的文件md5,用于提前保存记录"),
|
| 2607 |
nickname: str = Form(None, description="操作者昵称"),
|
|
|
|
| 2681 |
nickname=nickname,
|
| 2682 |
category="upcolor",
|
| 2683 |
bos_uploaded=True,
|
| 2684 |
+
region=_get_region_from_headers(request),
|
| 2685 |
extra={
|
| 2686 |
"source": "upcolor",
|
| 2687 |
"md5": actual_md5,
|
|
|
|
| 2782 |
@api_router.post("/anime_style")
|
| 2783 |
@log_api_params
|
| 2784 |
async def anime_stylize_photo(
|
| 2785 |
+
request: Request,
|
| 2786 |
file: UploadFile = File(...),
|
| 2787 |
style_type: str = Form("handdrawn",
|
| 2788 |
description="动漫风格类型: handdrawn=手绘风格, disney=迪士尼风格, illustration=插画风格, artstyle=艺术风格, anime=二次元风格, sketch=素描风格"),
|
|
|
|
| 2875 |
nickname=nickname,
|
| 2876 |
category="anime_style",
|
| 2877 |
bos_uploaded=original_bos_uploaded,
|
| 2878 |
+
region=_get_region_from_headers(request),
|
| 2879 |
extra={
|
| 2880 |
"source": "anime_style",
|
| 2881 |
"style_type": style_type,
|
|
|
|
| 2914 |
nickname=nickname,
|
| 2915 |
category="anime_style",
|
| 2916 |
bos_uploaded=bos_uploaded,
|
| 2917 |
+
region=_get_region_from_headers(request),
|
| 2918 |
extra={
|
| 2919 |
"source": "anime_style",
|
| 2920 |
"style_type": style_type,
|
|
|
|
| 3044 |
@api_router.post("/upscale")
|
| 3045 |
@log_api_params
|
| 3046 |
async def upscale_photo(
|
| 3047 |
+
request: Request,
|
| 3048 |
file: UploadFile = File(...),
|
| 3049 |
md5: str = Query(None, description="前端传递的文件md5,用于提前保存记录"),
|
| 3050 |
scale: int = Query(UPSCALE_SIZE, description="放大倍数,支持2或4倍"),
|
|
|
|
| 3133 |
nickname=nickname,
|
| 3134 |
category="upscale",
|
| 3135 |
bos_uploaded=True,
|
| 3136 |
+
region=_get_region_from_headers(request),
|
| 3137 |
extra={
|
| 3138 |
"source": "upscale",
|
| 3139 |
"md5": actual_md5,
|
|
|
|
| 4407 |
@api_router.post("/celebrity/match", tags=["Face Recognition"])
|
| 4408 |
@log_api_params
|
| 4409 |
async def match_celebrity_face(
|
| 4410 |
+
request: Request,
|
| 4411 |
file: UploadFile = File(..., description="待匹配的用户图片"),
|
| 4412 |
nickname: str = Form(None, description="操作者昵称"),
|
| 4413 |
):
|
|
|
|
| 4471 |
file_path=temp_path,
|
| 4472 |
nickname=nickname,
|
| 4473 |
category="celebrity",
|
| 4474 |
+
region=_get_region_from_headers(request),
|
| 4475 |
extra={
|
| 4476 |
"source": "celebrity_match",
|
| 4477 |
"role": "query",
|
|
|
|
| 4628 |
file_path=face_path,
|
| 4629 |
nickname=nickname,
|
| 4630 |
category="face",
|
| 4631 |
+
region=_get_region_from_headers(request),
|
| 4632 |
extra={
|
| 4633 |
"source": "celebrity_match",
|
| 4634 |
"role": "face_crop",
|
|
|
|
| 4649 |
file_path=temp_path,
|
| 4650 |
nickname=nickname,
|
| 4651 |
category="celebrity",
|
| 4652 |
+
region=_get_region_from_headers(request),
|
| 4653 |
extra={
|
| 4654 |
"source": "celebrity_match",
|
| 4655 |
"role": "annotated",
|
|
|
|
| 4699 |
@api_router.post("/face_verify")
|
| 4700 |
@log_api_params
|
| 4701 |
async def face_similarity_verification(
|
| 4702 |
+
request: Request,
|
| 4703 |
file1: UploadFile = File(..., description="第一张人脸图片"),
|
| 4704 |
file2: UploadFile = File(..., description="第二张人脸图片"),
|
| 4705 |
nickname: str = Form(None, description="操作者昵称"),
|
|
|
|
| 4902 |
file_path=original_path1,
|
| 4903 |
nickname=nickname,
|
| 4904 |
category="original",
|
| 4905 |
+
region=_get_region_from_headers(request),
|
| 4906 |
extra={
|
| 4907 |
"source": "face_verify",
|
| 4908 |
"role": "original1",
|
|
|
|
| 4914 |
file_path=original_path2,
|
| 4915 |
nickname=nickname,
|
| 4916 |
category="original",
|
| 4917 |
+
region=_get_region_from_headers(request),
|
| 4918 |
extra={
|
| 4919 |
"source": "face_verify",
|
| 4920 |
"role": "original2",
|
|
|
|
| 4947 |
file_path=face_path1,
|
| 4948 |
nickname=nickname,
|
| 4949 |
category="face",
|
| 4950 |
+
region=_get_region_from_headers(request),
|
| 4951 |
extra={
|
| 4952 |
"source": "face_verify",
|
| 4953 |
"role": "face1",
|
|
|
|
| 4958 |
file_path=face_path2,
|
| 4959 |
nickname=nickname,
|
| 4960 |
category="face",
|
| 4961 |
+
region=_get_region_from_headers(request),
|
| 4962 |
extra={
|
| 4963 |
"source": "face_verify",
|
| 4964 |
"role": "face2",
|
database.py
CHANGED
|
@@ -125,6 +125,7 @@ async def upsert_image_record(
|
|
| 125 |
last_modified: datetime,
|
| 126 |
bos_uploaded: bool,
|
| 127 |
hostname: Optional[str] = None,
|
|
|
|
| 128 |
extra_metadata: Optional[Dict[str, Any]] = None,
|
| 129 |
) -> None:
|
| 130 |
"""写入或更新图片记录"""
|
|
@@ -139,8 +140,9 @@ async def upsert_image_record(
|
|
| 139 |
last_modified,
|
| 140 |
bos_uploaded,
|
| 141 |
hostname,
|
|
|
|
| 142 |
extra_metadata
|
| 143 |
-
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
| 144 |
ON DUPLICATE KEY UPDATE
|
| 145 |
category = VALUES(category),
|
| 146 |
nickname = VALUES(nickname),
|
|
@@ -150,6 +152,7 @@ async def upsert_image_record(
|
|
| 150 |
last_modified = VALUES(last_modified),
|
| 151 |
bos_uploaded = VALUES(bos_uploaded),
|
| 152 |
hostname = VALUES(hostname),
|
|
|
|
| 153 |
extra_metadata = VALUES(extra_metadata),
|
| 154 |
updated_at = CURRENT_TIMESTAMP
|
| 155 |
"""
|
|
@@ -166,6 +169,7 @@ async def upsert_image_record(
|
|
| 166 |
last_modified,
|
| 167 |
1 if bos_uploaded else 0,
|
| 168 |
hostname,
|
|
|
|
| 169 |
extra_value,
|
| 170 |
),
|
| 171 |
)
|
|
@@ -175,6 +179,7 @@ async def fetch_paged_image_records(
|
|
| 175 |
*,
|
| 176 |
category: Optional[str],
|
| 177 |
nickname: Optional[str],
|
|
|
|
| 178 |
offset: int,
|
| 179 |
limit: int,
|
| 180 |
is_cropped_face: Optional[int] = None,
|
|
@@ -188,6 +193,9 @@ async def fetch_paged_image_records(
|
|
| 188 |
if nickname:
|
| 189 |
where_clauses.append("nickname LIKE %s")
|
| 190 |
params.append(f"{nickname}%")
|
|
|
|
|
|
|
|
|
|
| 191 |
if is_cropped_face is not None:
|
| 192 |
where_clauses.append("is_cropped_face = %s")
|
| 193 |
params.append(is_cropped_face)
|
|
@@ -202,7 +210,8 @@ async def fetch_paged_image_records(
|
|
| 202 |
size_bytes,
|
| 203 |
last_modified,
|
| 204 |
bos_uploaded,
|
| 205 |
-
hostname
|
|
|
|
| 206 |
FROM tpl_app_processed_images
|
| 207 |
{where_sql}
|
| 208 |
ORDER BY last_modified DESC, id DESC
|
|
@@ -216,6 +225,7 @@ async def count_image_records(
|
|
| 216 |
*,
|
| 217 |
category: Optional[str],
|
| 218 |
nickname: Optional[str],
|
|
|
|
| 219 |
is_cropped_face: Optional[int] = None,
|
| 220 |
) -> int:
|
| 221 |
"""按条件统计图片记录数量"""
|
|
@@ -227,6 +237,9 @@ async def count_image_records(
|
|
| 227 |
if nickname:
|
| 228 |
where_clauses.append("nickname LIKE %s")
|
| 229 |
params.append(f"{nickname}%")
|
|
|
|
|
|
|
|
|
|
| 230 |
if is_cropped_face is not None:
|
| 231 |
where_clauses.append("is_cropped_face = %s")
|
| 232 |
params.append(is_cropped_face)
|
|
@@ -277,7 +290,8 @@ async def fetch_records_by_paths(file_paths: Iterable[str]) -> Dict[
|
|
| 277 |
size_bytes,
|
| 278 |
last_modified,
|
| 279 |
bos_uploaded,
|
| 280 |
-
hostname
|
|
|
|
| 281 |
FROM tpl_app_processed_images
|
| 282 |
WHERE file_path IN ({placeholders})
|
| 283 |
"""
|
|
@@ -389,6 +403,79 @@ async def upsert_device_record(
|
|
| 389 |
logger.warning(f"写入设备记录失败: {exc}")
|
| 390 |
|
| 391 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
async def record_image_creation(
|
| 393 |
*,
|
| 394 |
file_path: str,
|
|
@@ -396,6 +483,7 @@ async def record_image_creation(
|
|
| 396 |
score: float = 0.0,
|
| 397 |
category: Optional[str] = None,
|
| 398 |
bos_uploaded: bool = False,
|
|
|
|
| 399 |
extra_metadata: Optional[Dict[str, Any]] = None,
|
| 400 |
) -> None:
|
| 401 |
"""
|
|
@@ -437,7 +525,34 @@ async def record_image_creation(
|
|
| 437 |
last_modified=last_modified,
|
| 438 |
bos_uploaded=bos_uploaded,
|
| 439 |
hostname=HOSTNAME,
|
|
|
|
| 440 |
extra_metadata=extra_metadata,
|
| 441 |
)
|
| 442 |
except Exception as exc:
|
| 443 |
logger.warning(f"写入图片记录失败: {exc}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
last_modified: datetime,
|
| 126 |
bos_uploaded: bool,
|
| 127 |
hostname: Optional[str] = None,
|
| 128 |
+
region: Optional[str] = None,
|
| 129 |
extra_metadata: Optional[Dict[str, Any]] = None,
|
| 130 |
) -> None:
|
| 131 |
"""写入或更新图片记录"""
|
|
|
|
| 140 |
last_modified,
|
| 141 |
bos_uploaded,
|
| 142 |
hostname,
|
| 143 |
+
region,
|
| 144 |
extra_metadata
|
| 145 |
+
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
| 146 |
ON DUPLICATE KEY UPDATE
|
| 147 |
category = VALUES(category),
|
| 148 |
nickname = VALUES(nickname),
|
|
|
|
| 152 |
last_modified = VALUES(last_modified),
|
| 153 |
bos_uploaded = VALUES(bos_uploaded),
|
| 154 |
hostname = VALUES(hostname),
|
| 155 |
+
region = VALUES(region),
|
| 156 |
extra_metadata = VALUES(extra_metadata),
|
| 157 |
updated_at = CURRENT_TIMESTAMP
|
| 158 |
"""
|
|
|
|
| 169 |
last_modified,
|
| 170 |
1 if bos_uploaded else 0,
|
| 171 |
hostname,
|
| 172 |
+
region,
|
| 173 |
extra_value,
|
| 174 |
),
|
| 175 |
)
|
|
|
|
| 179 |
*,
|
| 180 |
category: Optional[str],
|
| 181 |
nickname: Optional[str],
|
| 182 |
+
region: Optional[str] = None,
|
| 183 |
offset: int,
|
| 184 |
limit: int,
|
| 185 |
is_cropped_face: Optional[int] = None,
|
|
|
|
| 193 |
if nickname:
|
| 194 |
where_clauses.append("nickname LIKE %s")
|
| 195 |
params.append(f"{nickname}%")
|
| 196 |
+
if region:
|
| 197 |
+
where_clauses.append("region = %s")
|
| 198 |
+
params.append(region)
|
| 199 |
if is_cropped_face is not None:
|
| 200 |
where_clauses.append("is_cropped_face = %s")
|
| 201 |
params.append(is_cropped_face)
|
|
|
|
| 210 |
size_bytes,
|
| 211 |
last_modified,
|
| 212 |
bos_uploaded,
|
| 213 |
+
hostname,
|
| 214 |
+
region
|
| 215 |
FROM tpl_app_processed_images
|
| 216 |
{where_sql}
|
| 217 |
ORDER BY last_modified DESC, id DESC
|
|
|
|
| 225 |
*,
|
| 226 |
category: Optional[str],
|
| 227 |
nickname: Optional[str],
|
| 228 |
+
region: Optional[str] = None,
|
| 229 |
is_cropped_face: Optional[int] = None,
|
| 230 |
) -> int:
|
| 231 |
"""按条件统计图片记录数量"""
|
|
|
|
| 237 |
if nickname:
|
| 238 |
where_clauses.append("nickname LIKE %s")
|
| 239 |
params.append(f"{nickname}%")
|
| 240 |
+
if region:
|
| 241 |
+
where_clauses.append("region = %s")
|
| 242 |
+
params.append(region)
|
| 243 |
if is_cropped_face is not None:
|
| 244 |
where_clauses.append("is_cropped_face = %s")
|
| 245 |
params.append(is_cropped_face)
|
|
|
|
| 290 |
size_bytes,
|
| 291 |
last_modified,
|
| 292 |
bos_uploaded,
|
| 293 |
+
hostname,
|
| 294 |
+
region
|
| 295 |
FROM tpl_app_processed_images
|
| 296 |
WHERE file_path IN ({placeholders})
|
| 297 |
"""
|
|
|
|
| 403 |
logger.warning(f"写入设备记录失败: {exc}")
|
| 404 |
|
| 405 |
|
| 406 |
+
async def fetch_user_devices(
|
| 407 |
+
*,
|
| 408 |
+
device_id: Optional[str] = None,
|
| 409 |
+
region: Optional[str] = None,
|
| 410 |
+
offset: int,
|
| 411 |
+
limit: int,
|
| 412 |
+
) -> List[Dict[str, Any]]:
|
| 413 |
+
"""查询用户设备列表,支持按 device_id 和 region 筛选,按 created_at 倒序"""
|
| 414 |
+
where_clauses: List[str] = []
|
| 415 |
+
params: List[Any] = []
|
| 416 |
+
|
| 417 |
+
if device_id:
|
| 418 |
+
where_clauses.append("d.device_id LIKE %s")
|
| 419 |
+
params.append(f"{device_id}%")
|
| 420 |
+
|
| 421 |
+
if region:
|
| 422 |
+
where_clauses.append("d.region = %s")
|
| 423 |
+
params.append(region)
|
| 424 |
+
|
| 425 |
+
where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
|
| 426 |
+
|
| 427 |
+
query = f"""
|
| 428 |
+
SELECT
|
| 429 |
+
d.device_id,
|
| 430 |
+
d.device_type,
|
| 431 |
+
d.device_model,
|
| 432 |
+
d.os_version,
|
| 433 |
+
d.app_version,
|
| 434 |
+
d.timezone,
|
| 435 |
+
d.region,
|
| 436 |
+
d.language,
|
| 437 |
+
d.created_at,
|
| 438 |
+
d.updated_at,
|
| 439 |
+
COALESCE((
|
| 440 |
+
SELECT COUNT(*)
|
| 441 |
+
FROM tpl_app_processed_images p
|
| 442 |
+
WHERE p.nickname LIKE CONCAT('android_release_', d.device_id, '%%')
|
| 443 |
+
), 0) AS detection_count
|
| 444 |
+
FROM tpl_app_user_devices d
|
| 445 |
+
{where_sql}
|
| 446 |
+
ORDER BY d.created_at DESC
|
| 447 |
+
LIMIT %s OFFSET %s
|
| 448 |
+
"""
|
| 449 |
+
params.extend([limit, offset])
|
| 450 |
+
return await fetch_all(query, params)
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
async def count_user_devices(
|
| 454 |
+
*,
|
| 455 |
+
device_id: Optional[str] = None,
|
| 456 |
+
region: Optional[str] = None,
|
| 457 |
+
) -> int:
|
| 458 |
+
"""统计用户设备数量"""
|
| 459 |
+
where_clauses: List[str] = []
|
| 460 |
+
params: List[Any] = []
|
| 461 |
+
|
| 462 |
+
if device_id:
|
| 463 |
+
where_clauses.append("d.device_id LIKE %s")
|
| 464 |
+
params.append(f"{device_id}%")
|
| 465 |
+
|
| 466 |
+
if region:
|
| 467 |
+
where_clauses.append("d.region = %s")
|
| 468 |
+
params.append(region)
|
| 469 |
+
|
| 470 |
+
where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
|
| 471 |
+
|
| 472 |
+
query = f"SELECT COUNT(*) AS total FROM tpl_app_user_devices d {where_sql}"
|
| 473 |
+
rows = await fetch_all(query, params)
|
| 474 |
+
if not rows:
|
| 475 |
+
return 0
|
| 476 |
+
return int(rows[0].get("total", 0) or 0)
|
| 477 |
+
|
| 478 |
+
|
| 479 |
async def record_image_creation(
|
| 480 |
*,
|
| 481 |
file_path: str,
|
|
|
|
| 483 |
score: float = 0.0,
|
| 484 |
category: Optional[str] = None,
|
| 485 |
bos_uploaded: bool = False,
|
| 486 |
+
region: Optional[str] = None,
|
| 487 |
extra_metadata: Optional[Dict[str, Any]] = None,
|
| 488 |
) -> None:
|
| 489 |
"""
|
|
|
|
| 525 |
last_modified=last_modified,
|
| 526 |
bos_uploaded=bos_uploaded,
|
| 527 |
hostname=HOSTNAME,
|
| 528 |
+
region=region,
|
| 529 |
extra_metadata=extra_metadata,
|
| 530 |
)
|
| 531 |
except Exception as exc:
|
| 532 |
logger.warning(f"写入图片记录失败: {exc}")
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
# 清理调试数据 SQL 列表,可在此处增删 SQL
|
| 536 |
+
CLEAN_DEBUG_SQLS = [
|
| 537 |
+
"DELETE FROM tpl_app_user_devices WHERE device_model='Xiaomi 23127PN0CC' AND region='CN'",
|
| 538 |
+
"DELETE FROM tpl_app_processed_images WHERE nickname LIKE 'android_debug%'",
|
| 539 |
+
"DELETE FROM tpl_app_processed_images WHERE nickname='android_release_1b708ef79ac4f484'",
|
| 540 |
+
"DELETE FROM tpl_app_processed_images WHERE nickname='9527'",
|
| 541 |
+
]
|
| 542 |
+
|
| 543 |
+
|
| 544 |
+
async def clean_debug_data() -> Dict[str, int]:
|
| 545 |
+
"""
|
| 546 |
+
清理调试数据
|
| 547 |
+
:return: 返回每个 SQL 影响的行数
|
| 548 |
+
"""
|
| 549 |
+
results = {}
|
| 550 |
+
for idx, sql in enumerate(CLEAN_DEBUG_SQLS):
|
| 551 |
+
try:
|
| 552 |
+
affected = await execute(sql)
|
| 553 |
+
results[f"sql_{idx + 1}"] = affected
|
| 554 |
+
logger.info(f"Clean debug SQL #{idx + 1} executed, affected: {affected}")
|
| 555 |
+
except Exception as exc:
|
| 556 |
+
logger.warning(f"Clean debug SQL #{idx + 1} failed: {exc}")
|
| 557 |
+
results[f"sql_{idx + 1}"] = -1
|
| 558 |
+
return results
|
models.py
CHANGED
|
@@ -21,6 +21,8 @@ class ImageScoreItem(BaseModel):
|
|
| 21 |
last_modified: str
|
| 22 |
nickname: Optional[str] = None
|
| 23 |
hostname: Optional[str] = None
|
|
|
|
|
|
|
| 24 |
|
| 25 |
|
| 26 |
class SearchRequest(BaseModel):
|
|
@@ -29,6 +31,7 @@ class SearchRequest(BaseModel):
|
|
| 29 |
top_k: Optional[int] = 5
|
| 30 |
score_threshold: float = 0.0
|
| 31 |
nickname: Optional[str] = None
|
|
|
|
| 32 |
|
| 33 |
|
| 34 |
class ImageSearchRequest(BaseModel):
|
|
@@ -68,3 +71,26 @@ class CategoryStatItem(BaseModel):
|
|
| 68 |
class CategoryStatsResponse(BaseModel):
|
| 69 |
stats: List[CategoryStatItem]
|
| 70 |
total: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
last_modified: str
|
| 22 |
nickname: Optional[str] = None
|
| 23 |
hostname: Optional[str] = None
|
| 24 |
+
region: Optional[str] = None
|
| 25 |
+
region_country: Optional[str] = None
|
| 26 |
|
| 27 |
|
| 28 |
class SearchRequest(BaseModel):
|
|
|
|
| 31 |
top_k: Optional[int] = 5
|
| 32 |
score_threshold: float = 0.0
|
| 33 |
nickname: Optional[str] = None
|
| 34 |
+
region: Optional[str] = None
|
| 35 |
|
| 36 |
|
| 37 |
class ImageSearchRequest(BaseModel):
|
|
|
|
| 71 |
class CategoryStatsResponse(BaseModel):
|
| 72 |
stats: List[CategoryStatItem]
|
| 73 |
total: int
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class UserDeviceItem(BaseModel):
|
| 77 |
+
device_id: str
|
| 78 |
+
device_type: Optional[str] = None
|
| 79 |
+
device_model: Optional[str] = None
|
| 80 |
+
os_version: Optional[str] = None
|
| 81 |
+
app_version: Optional[str] = None
|
| 82 |
+
timezone: Optional[str] = None
|
| 83 |
+
region: Optional[str] = None
|
| 84 |
+
region_country: Optional[str] = None
|
| 85 |
+
language: Optional[str] = None
|
| 86 |
+
detection_count: int = 0
|
| 87 |
+
created_at: str
|
| 88 |
+
updated_at: str
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class UserDeviceListResponse(BaseModel):
|
| 92 |
+
results: List[UserDeviceItem]
|
| 93 |
+
count: int
|
| 94 |
+
page: int
|
| 95 |
+
page_size: int
|
| 96 |
+
total_pages: int
|
region_code_utils.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# region_code_utils.py
|
| 2 |
+
"""
|
| 3 |
+
地区代码转换工具类
|
| 4 |
+
将 ISO 3166-1 alpha-2 两位字母代码转换为完整国家名称
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
# ISO 3166-1 alpha-2 国家代码映射表 (完整)
|
| 8 |
+
REGION_CODE_TO_NAME = {
|
| 9 |
+
# A
|
| 10 |
+
"AF": "阿富汗",
|
| 11 |
+
"AL": "阿尔巴尼亚",
|
| 12 |
+
"DZ": "阿尔及利亚",
|
| 13 |
+
"AS": "美属萨摩亚",
|
| 14 |
+
"AD": "安道尔",
|
| 15 |
+
"AO": "安哥拉",
|
| 16 |
+
"AI": "安圭拉",
|
| 17 |
+
"AQ": "南极洲",
|
| 18 |
+
"AG": "安提瓜和巴布达",
|
| 19 |
+
"AR": "阿根廷",
|
| 20 |
+
"AM": "亚美尼亚",
|
| 21 |
+
"AW": "阿鲁巴",
|
| 22 |
+
"AU": "澳大利亚",
|
| 23 |
+
"AT": "奥地利",
|
| 24 |
+
"AZ": "阿塞拜疆",
|
| 25 |
+
# B
|
| 26 |
+
"BS": "巴哈马",
|
| 27 |
+
"BH": "巴林",
|
| 28 |
+
"BD": "孟加拉国",
|
| 29 |
+
"BB": "巴巴多斯",
|
| 30 |
+
"BY": "白俄罗斯",
|
| 31 |
+
"BE": "比利时",
|
| 32 |
+
"BZ": "伯利兹",
|
| 33 |
+
"BJ": "贝宁",
|
| 34 |
+
"BM": "百慕大",
|
| 35 |
+
"BT": "不丹",
|
| 36 |
+
"BO": "玻利维亚",
|
| 37 |
+
"BA": "波黑",
|
| 38 |
+
"BW": "博茨瓦纳",
|
| 39 |
+
"BV": "布韦岛",
|
| 40 |
+
"BR": "巴西",
|
| 41 |
+
"IO": "英属印度洋领地",
|
| 42 |
+
"BN": "文莱",
|
| 43 |
+
"BG": "保加利亚",
|
| 44 |
+
"BF": "布基纳法索",
|
| 45 |
+
"BI": "布隆迪",
|
| 46 |
+
# C
|
| 47 |
+
"KH": "柬埔寨",
|
| 48 |
+
"CM": "喀麦隆",
|
| 49 |
+
"CA": "加拿大",
|
| 50 |
+
"CV": "佛得角",
|
| 51 |
+
"KY": "开曼群岛",
|
| 52 |
+
"CF": "中非",
|
| 53 |
+
"TD": "乍得",
|
| 54 |
+
"CL": "智利",
|
| 55 |
+
"CN": "中国",
|
| 56 |
+
"CX": "圣诞岛",
|
| 57 |
+
"CC": "科科斯群岛",
|
| 58 |
+
"CO": "哥伦比亚",
|
| 59 |
+
"KM": "科摩罗",
|
| 60 |
+
"CG": "刚果",
|
| 61 |
+
"CD": "刚果民主共和国",
|
| 62 |
+
"CK": "库克群岛",
|
| 63 |
+
"CR": "哥斯达黎加",
|
| 64 |
+
"CI": "科特迪瓦",
|
| 65 |
+
"HR": "克罗地亚",
|
| 66 |
+
"CU": "古巴",
|
| 67 |
+
"CY": "塞浦路斯",
|
| 68 |
+
"CZ": "捷克",
|
| 69 |
+
# D
|
| 70 |
+
"DK": "丹麦",
|
| 71 |
+
"DJ": "吉布提",
|
| 72 |
+
"DM": "多米尼加",
|
| 73 |
+
"DO": "多米尼加共和国",
|
| 74 |
+
# E
|
| 75 |
+
"EC": "厄瓜多尔",
|
| 76 |
+
"EG": "埃及",
|
| 77 |
+
"SV": "萨尔瓦多",
|
| 78 |
+
"GQ": "赤道几内亚",
|
| 79 |
+
"ER": "厄立特里亚",
|
| 80 |
+
"EE": "爱沙尼亚",
|
| 81 |
+
"ET": "埃塞俄比亚",
|
| 82 |
+
# F
|
| 83 |
+
"FK": "福克兰群岛",
|
| 84 |
+
"FO": "法罗群岛",
|
| 85 |
+
"FJ": "斐济",
|
| 86 |
+
"FI": "芬兰",
|
| 87 |
+
"FR": "法国",
|
| 88 |
+
"GF": "法属圭亚那",
|
| 89 |
+
"PF": "法属波利尼西亚",
|
| 90 |
+
"TF": "法属南部领地",
|
| 91 |
+
"GA": "加蓬",
|
| 92 |
+
"GM": "冈比亚",
|
| 93 |
+
"GE": "格鲁吉亚",
|
| 94 |
+
"DE": "德国",
|
| 95 |
+
"GH": "加纳",
|
| 96 |
+
"GI": "直布罗陀",
|
| 97 |
+
"GR": "希腊",
|
| 98 |
+
"GL": "格陵兰",
|
| 99 |
+
"GD": "格林纳达",
|
| 100 |
+
"GP": "瓜德罗普",
|
| 101 |
+
"GU": "关岛",
|
| 102 |
+
"GT": "危地马拉",
|
| 103 |
+
"GN": "几内亚",
|
| 104 |
+
"GW": "几内亚比绍",
|
| 105 |
+
"GY": "圭亚那",
|
| 106 |
+
# H
|
| 107 |
+
"HT": "海地",
|
| 108 |
+
"HM": "赫德岛和麦克唐纳岛",
|
| 109 |
+
"HN": "洪都拉斯",
|
| 110 |
+
"HK": "中国香港",
|
| 111 |
+
"HU": "匈牙利",
|
| 112 |
+
# I
|
| 113 |
+
"IS": "冰岛",
|
| 114 |
+
"IN": "印度",
|
| 115 |
+
"ID": "印度尼西亚",
|
| 116 |
+
"IR": "伊朗",
|
| 117 |
+
"IQ": "伊拉克",
|
| 118 |
+
"IE": "爱尔兰",
|
| 119 |
+
"IL": "以色列",
|
| 120 |
+
"IT": "意大利",
|
| 121 |
+
"JM": "牙买加",
|
| 122 |
+
"JP": "日本",
|
| 123 |
+
"JO": "约旦",
|
| 124 |
+
# K
|
| 125 |
+
"KZ": "哈萨克斯坦",
|
| 126 |
+
"KE": "肯尼亚",
|
| 127 |
+
"KI": "基里巴斯",
|
| 128 |
+
"KP": "朝鲜",
|
| 129 |
+
"KR": "韩国",
|
| 130 |
+
"KW": "科威特",
|
| 131 |
+
"KG": "吉尔吉斯斯坦",
|
| 132 |
+
"LA": "老挝",
|
| 133 |
+
"LV": "拉脱维亚",
|
| 134 |
+
"LB": "黎巴嫩",
|
| 135 |
+
"LS": "莱索托",
|
| 136 |
+
"LR": "利比里亚",
|
| 137 |
+
"LY": "利比亚",
|
| 138 |
+
"LI": "列支敦士登",
|
| 139 |
+
"LT": "立陶宛",
|
| 140 |
+
"LU": "卢森堡",
|
| 141 |
+
"MO": "中国澳门",
|
| 142 |
+
# M
|
| 143 |
+
"MK": "北马其顿",
|
| 144 |
+
"MG": "马达加斯加",
|
| 145 |
+
"MW": "马拉维",
|
| 146 |
+
"MY": "马来西亚",
|
| 147 |
+
"MV": "马尔代夫",
|
| 148 |
+
"ML": "马里",
|
| 149 |
+
"MT": "马耳他",
|
| 150 |
+
"MH": "马绍尔群岛",
|
| 151 |
+
"MQ": "马提尼克",
|
| 152 |
+
"MR": "毛里塔尼亚",
|
| 153 |
+
"MU": "毛里求斯",
|
| 154 |
+
"YT": "马约特",
|
| 155 |
+
"MX": "墨西哥",
|
| 156 |
+
"FM": "密克罗尼西亚",
|
| 157 |
+
"MD": "摩尔多瓦",
|
| 158 |
+
"MC": "摩纳哥",
|
| 159 |
+
"MN": "蒙古",
|
| 160 |
+
"ME": "黑山",
|
| 161 |
+
"MS": "蒙特塞拉特",
|
| 162 |
+
"MA": "摩洛哥",
|
| 163 |
+
"MZ": "莫桑比克",
|
| 164 |
+
"MM": "缅甸",
|
| 165 |
+
# N
|
| 166 |
+
"NA": "纳米比亚",
|
| 167 |
+
"NR": "瑙鲁",
|
| 168 |
+
"NP": "尼泊尔",
|
| 169 |
+
"NL": "荷兰",
|
| 170 |
+
"NC": "新喀里多尼亚",
|
| 171 |
+
"NZ": "新西兰",
|
| 172 |
+
"NI": "尼加拉瓜",
|
| 173 |
+
"NE": "尼日尔",
|
| 174 |
+
"NG": "尼日利亚",
|
| 175 |
+
"NU": "纽埃",
|
| 176 |
+
"NF": "诺福克岛",
|
| 177 |
+
"MP": "北马里亚纳群岛",
|
| 178 |
+
"NO": "挪威",
|
| 179 |
+
# O
|
| 180 |
+
"OM": "阿曼",
|
| 181 |
+
# P
|
| 182 |
+
"PK": "巴基斯坦",
|
| 183 |
+
"PW": "帕劳",
|
| 184 |
+
"PS": "巴勒斯坦",
|
| 185 |
+
"PA": "巴拿马",
|
| 186 |
+
"PG": "巴布亚新几内亚",
|
| 187 |
+
"PY": "巴拉圭",
|
| 188 |
+
"PE": "秘鲁",
|
| 189 |
+
"PH": "菲律宾",
|
| 190 |
+
"PN": "皮特凯恩群岛",
|
| 191 |
+
"PL": "波兰",
|
| 192 |
+
"PT": "葡萄牙",
|
| 193 |
+
"PR": "波多黎各",
|
| 194 |
+
"QA": "卡塔尔",
|
| 195 |
+
# R
|
| 196 |
+
"RE": "留尼汪",
|
| 197 |
+
"RO": "罗马尼亚",
|
| 198 |
+
"RU": "俄罗斯",
|
| 199 |
+
"RW": "卢旺达",
|
| 200 |
+
# S
|
| 201 |
+
"BL": "圣巴泰勒米",
|
| 202 |
+
"SH": "圣赫勒拿",
|
| 203 |
+
"KN": "圣基茨和尼维斯",
|
| 204 |
+
"LC": "圣卢西亚",
|
| 205 |
+
"MF": "法属圣马丁",
|
| 206 |
+
"PM": "圣皮埃尔和密克隆",
|
| 207 |
+
"VC": "圣文森特和格林纳丁斯",
|
| 208 |
+
"WS": "萨摩亚",
|
| 209 |
+
"SM": "圣马力诺",
|
| 210 |
+
"ST": "圣多美和普林西比",
|
| 211 |
+
"SA": "沙特阿拉伯",
|
| 212 |
+
"SN": "塞内加尔",
|
| 213 |
+
"RS": "塞尔维亚",
|
| 214 |
+
"SC": "塞舌尔",
|
| 215 |
+
"SL": "塞拉利昂",
|
| 216 |
+
"SG": "新加坡",
|
| 217 |
+
"SX": "荷属圣马丁",
|
| 218 |
+
"SK": "斯洛伐克",
|
| 219 |
+
"SI": "斯洛文尼亚",
|
| 220 |
+
"SB": "所罗门群岛",
|
| 221 |
+
"SO": "索马里",
|
| 222 |
+
"ZA": "南非",
|
| 223 |
+
"GS": "南乔治亚和南桑威奇群岛",
|
| 224 |
+
"SS": "南苏丹",
|
| 225 |
+
"ES": "西班牙",
|
| 226 |
+
"LK": "斯里兰卡",
|
| 227 |
+
"SD": "苏丹",
|
| 228 |
+
"SR": "苏里南",
|
| 229 |
+
"SJ": "斯瓦尔巴和扬马延",
|
| 230 |
+
"SZ": "斯威士兰",
|
| 231 |
+
"SE": "瑞典",
|
| 232 |
+
"CH": "瑞士",
|
| 233 |
+
"SY": "叙利亚",
|
| 234 |
+
# T
|
| 235 |
+
"TW": "中国台湾",
|
| 236 |
+
"TJ": "塔吉克斯坦",
|
| 237 |
+
"TZ": "坦桑尼亚",
|
| 238 |
+
"TH": "泰国",
|
| 239 |
+
"TL": "东帝汶",
|
| 240 |
+
"TG": "多哥",
|
| 241 |
+
"TK": "托克劳",
|
| 242 |
+
"TO": "汤加",
|
| 243 |
+
"TT": "特立尼达和多巴哥",
|
| 244 |
+
"TN": "突尼斯",
|
| 245 |
+
"TR": "土耳其",
|
| 246 |
+
"TM": "土库曼斯坦",
|
| 247 |
+
"TC": "特克斯和凯科斯群岛",
|
| 248 |
+
"TV": "图瓦卢",
|
| 249 |
+
"UG": "乌干达",
|
| 250 |
+
"UA": "乌克兰",
|
| 251 |
+
"AE": "阿联酋",
|
| 252 |
+
"GB": "英国",
|
| 253 |
+
"US": "美国",
|
| 254 |
+
"UM": "美国本土外小岛屿",
|
| 255 |
+
"UY": "乌拉圭",
|
| 256 |
+
"UZ": "乌兹别克斯坦",
|
| 257 |
+
# V
|
| 258 |
+
"VU": "瓦努阿图",
|
| 259 |
+
"VA": "梵蒂冈",
|
| 260 |
+
"VE": "委内瑞拉",
|
| 261 |
+
"VN": "越南",
|
| 262 |
+
"VG": "英属维尔京群岛",
|
| 263 |
+
"VI": "美属维尔京群岛",
|
| 264 |
+
# W
|
| 265 |
+
"WF": "瓦利斯和富图纳",
|
| 266 |
+
"EH": "西撒哈拉",
|
| 267 |
+
"YE": "也门",
|
| 268 |
+
"ZM": "赞比亚",
|
| 269 |
+
"ZW": "津巴布韦",
|
| 270 |
+
"AX": "奥兰群岛",
|
| 271 |
+
"BQ": "荷兰加勒比区",
|
| 272 |
+
"CW": "库拉索",
|
| 273 |
+
"GG": "根西岛",
|
| 274 |
+
"IM": "马恩岛",
|
| 275 |
+
"JE": "泽西岛",
|
| 276 |
+
"XK": "科索沃",
|
| 277 |
+
"AQ": "南极洲",
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def get_country_name_by_region_code(region_code: str) -> str:
|
| 282 |
+
"""
|
| 283 |
+
根据地区代码获取国家名称
|
| 284 |
+
:param region_code: ISO 3166-1 alpha-2 两位字母代码,如 "CN", "US"
|
| 285 |
+
:return: 完整国家名称,如 "中国", "美国";如果代码不存在则返回原始代码
|
| 286 |
+
"""
|
| 287 |
+
if not region_code:
|
| 288 |
+
return ""
|
| 289 |
+
|
| 290 |
+
upper_code = region_code.strip().upper()
|
| 291 |
+
if not upper_code:
|
| 292 |
+
return ""
|
| 293 |
+
|
| 294 |
+
return REGION_CODE_TO_NAME.get(upper_code, region_code)
|