Download database.py from ethonmax/picpocket: direct link, hf CLI and curl.
- Browser
- Download file 41.2 kB
-
https://huggingface.co/spaces/ethonmax/picpocket/resolve/main/database.py
- Command line
-
hf download hf://spaces/ethonmax/picpocket/database.py
-
curl -L -o database.py https://huggingface.co/spaces/ethonmax/picpocket/resolve/main/database.py
41.2 kB
| import asyncio | |
| import json | |
| import os | |
| import time | |
| from contextlib import asynccontextmanager | |
| from datetime import datetime | |
| from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple | |
| from client_source import current_client_source, validate_client_source | |
| import aiomysql | |
| from aiomysql.cursors import DictCursor | |
| from config import ( | |
| IMAGES_DIR, | |
| logger, | |
| MYSQL_HOST, | |
| MYSQL_PORT, | |
| MYSQL_DB, | |
| MYSQL_USER, | |
| MYSQL_PASSWORD, | |
| MYSQL_POOL_MIN_SIZE, | |
| MYSQL_POOL_MAX_SIZE, | |
| LEADERBOARD_DAYS_LIMIT, | |
| ) | |
| _pool: Optional[aiomysql.Pool] = None | |
| _pool_lock = asyncio.Lock() | |
| async def init_mysql_pool() -> aiomysql.Pool: | |
| """初始化 MySQL 连接池""" | |
| global _pool | |
| if _pool is not None: | |
| return _pool | |
| async with _pool_lock: | |
| if _pool is not None: | |
| return _pool | |
| try: | |
| _pool = await aiomysql.create_pool( | |
| host=MYSQL_HOST, | |
| port=MYSQL_PORT, | |
| user=MYSQL_USER, | |
| password=MYSQL_PASSWORD, | |
| db=MYSQL_DB, | |
| minsize=MYSQL_POOL_MIN_SIZE, | |
| maxsize=MYSQL_POOL_MAX_SIZE, | |
| autocommit=True, | |
| charset="utf8mb4", | |
| cursorclass=DictCursor, | |
| ) | |
| logger.info( | |
| "MySQL 连接池初始化成功,host=%s db=%s", | |
| MYSQL_HOST, | |
| MYSQL_DB, | |
| ) | |
| except Exception as exc: | |
| logger.error(f"初始化 MySQL 连接池失败: {exc}") | |
| raise | |
| return _pool | |
| async def close_mysql_pool() -> None: | |
| """关闭 MySQL 连接池""" | |
| global _pool | |
| if _pool is None: | |
| return | |
| async with _pool_lock: | |
| if _pool is None: | |
| return | |
| _pool.close() | |
| await _pool.wait_closed() | |
| _pool = None | |
| logger.info("MySQL 连接池已关闭") | |
| async def get_connection(): | |
| """获取连接池中的连接""" | |
| if _pool is None: | |
| await init_mysql_pool() | |
| assert _pool is not None | |
| conn = await _pool.acquire() | |
| try: | |
| yield conn | |
| finally: | |
| _pool.release(conn) | |
| async def execute( | |
| query: str, params: Sequence[Any] | Dict[str, Any] | None = None | |
| ) -> int: | |
| """执行写入类 SQL""" | |
| start_time = time.perf_counter() | |
| async with get_connection() as conn: | |
| async with conn.cursor() as cursor: | |
| try: | |
| await cursor.execute(query, params or ()) | |
| affected = cursor.rowcount | |
| duration = (time.perf_counter() - start_time) * 1000 | |
| formatted_query = " ".join(query.split()) | |
| logger.info( | |
| "SQL Execute: %s | Params: %s | Affected: %d | Cost: %.2fms", | |
| formatted_query, | |
| params, | |
| affected, | |
| duration, | |
| ) | |
| return affected | |
| except Exception as exc: | |
| duration = (time.perf_counter() - start_time) * 1000 | |
| formatted_query = " ".join(query.split()) | |
| logger.error( | |
| "SQL Execute Failed: %s | Params: %s | Cost: %.2fms | Error: %s", | |
| formatted_query, | |
| params, | |
| duration, | |
| exc, | |
| ) | |
| raise | |
| async def fetch_all( | |
| query: str, params: Sequence[Any] | Dict[str, Any] | None = None | |
| ) -> List[Dict[str, Any]]: | |
| """执行查询并返回全部结果""" | |
| start_time = time.perf_counter() | |
| async with get_connection() as conn: | |
| async with conn.cursor() as cursor: | |
| try: | |
| await cursor.execute(query, params or ()) | |
| rows = await cursor.fetchall() | |
| duration = (time.perf_counter() - start_time) * 1000 | |
| formatted_query = " ".join(query.split()) | |
| logger.info( | |
| "SQL Query: %s | Params: %s | Rows: %d | Cost: %.2fms", | |
| formatted_query, | |
| params, | |
| len(rows), | |
| duration, | |
| ) | |
| return list(rows) | |
| except Exception as exc: | |
| duration = (time.perf_counter() - start_time) * 1000 | |
| formatted_query = " ".join(query.split()) | |
| logger.error( | |
| "SQL Query Failed: %s | Params: %s | Cost: %.2fms | Error: %s", | |
| formatted_query, | |
| params, | |
| duration, | |
| exc, | |
| ) | |
| raise | |
| async def fetch_one( | |
| query: str, params: Sequence[Any] | Dict[str, Any] | None = None | |
| ) -> Optional[Dict[str, Any]]: | |
| """执行查询并返回单条结果""" | |
| rows = await fetch_all(query, params) | |
| return rows[0] if rows else None | |
| def _serialize_extra(extra: Optional[Dict[str, Any]]) -> Optional[str]: | |
| if extra is None: | |
| return None | |
| try: | |
| return json.dumps(extra, ensure_ascii=False) | |
| except Exception: | |
| logger.warning("无法序列化 extra_metadata,已忽略") | |
| return None | |
| async def upsert_image_record( | |
| *, | |
| file_path: str, | |
| category: str, | |
| nickname: Optional[str], | |
| score: float, | |
| is_cropped_face: bool, | |
| size_bytes: int, | |
| last_modified: datetime, | |
| bos_uploaded: bool, | |
| hostname: Optional[str] = None, | |
| region: Optional[str] = None, | |
| extra_metadata: Optional[Dict[str, Any]] = None, | |
| client_source: Optional[str] = None, | |
| ) -> None: | |
| """写入或更新图片记录""" | |
| source = validate_client_source(client_source if client_source is not None else current_client_source.get()) | |
| query = """ | |
| INSERT INTO tpl_app_processed_images ( | |
| file_path, | |
| category, | |
| nickname, | |
| score, | |
| is_cropped_face, | |
| size_bytes, | |
| last_modified, | |
| bos_uploaded, | |
| hostname, | |
| region, | |
| extra_metadata, | |
| client_source | |
| ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) | |
| ON DUPLICATE KEY UPDATE | |
| client_source = IF(client_source = 'unknown', VALUES(client_source), client_source), | |
| category = VALUES(category), | |
| nickname = VALUES(nickname), | |
| score = VALUES(score), | |
| is_cropped_face = VALUES(is_cropped_face), | |
| size_bytes = VALUES(size_bytes), | |
| last_modified = VALUES(last_modified), | |
| bos_uploaded = VALUES(bos_uploaded), | |
| hostname = VALUES(hostname), | |
| region = VALUES(region), | |
| extra_metadata = VALUES(extra_metadata), | |
| updated_at = CURRENT_TIMESTAMP | |
| """ | |
| extra_value = _serialize_extra(extra_metadata) | |
| await execute( | |
| query, | |
| ( | |
| file_path, | |
| category, | |
| nickname, | |
| score, | |
| 1 if is_cropped_face else 0, | |
| size_bytes, | |
| last_modified, | |
| 1 if bos_uploaded else 0, | |
| hostname, | |
| region, | |
| extra_value, | |
| source, | |
| ), | |
| ) | |
| async def fetch_paged_image_records( | |
| *, | |
| category: Optional[str], | |
| nickname: Optional[str], | |
| region: Optional[str] = None, | |
| platform: Optional[str] = None, | |
| offset: int, | |
| limit: int, | |
| is_cropped_face: Optional[int] = None, | |
| ) -> List[Dict[str, Any]]: | |
| """按条件分页查询图片记录""" | |
| where_clauses: List[str] = [] | |
| params: List[Any] = [] | |
| if category and category != "all": | |
| where_clauses.append("category = %s") | |
| params.append(category) | |
| if nickname: | |
| where_clauses.append("nickname LIKE %s") | |
| params.append(f"{nickname}%") | |
| if platform in {"android", "ios", "web", "wechat", "miniprogram"}: | |
| where_clauses.append("client_source = %s") | |
| params.append("wechat" if platform == "miniprogram" else platform) | |
| if region: | |
| where_clauses.append("region = %s") | |
| params.append(region) | |
| if is_cropped_face is not None: | |
| where_clauses.append("is_cropped_face = %s") | |
| params.append(is_cropped_face) | |
| where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else "" | |
| query = f""" | |
| SELECT | |
| file_path, | |
| category, | |
| nickname, | |
| score, | |
| is_cropped_face, | |
| size_bytes, | |
| last_modified, | |
| bos_uploaded, | |
| hostname, | |
| region | |
| FROM tpl_app_processed_images | |
| {where_sql} | |
| ORDER BY last_modified DESC, id DESC | |
| LIMIT %s OFFSET %s | |
| """ | |
| params.extend([limit, offset]) | |
| return await fetch_all(query, params) | |
| async def count_image_records( | |
| *, | |
| category: Optional[str], | |
| nickname: Optional[str], | |
| region: Optional[str] = None, | |
| platform: Optional[str] = None, | |
| is_cropped_face: Optional[int] = None, | |
| ) -> int: | |
| """按条件统计图片记录数量""" | |
| where_clauses: List[str] = [] | |
| params: List[Any] = [] | |
| if category and category != "all": | |
| where_clauses.append("category = %s") | |
| params.append(category) | |
| if nickname: | |
| where_clauses.append("nickname LIKE %s") | |
| params.append(f"{nickname}%") | |
| if platform in {"android", "ios", "web", "wechat", "miniprogram"}: | |
| where_clauses.append("client_source = %s") | |
| params.append("wechat" if platform == "miniprogram" else platform) | |
| if region: | |
| where_clauses.append("region = %s") | |
| params.append(region) | |
| if is_cropped_face is not None: | |
| where_clauses.append("is_cropped_face = %s") | |
| params.append(is_cropped_face) | |
| where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else "" | |
| query = f"SELECT COUNT(*) AS total FROM tpl_app_processed_images {where_sql}" | |
| rows = await fetch_all(query, params) | |
| if not rows: | |
| return 0 | |
| return int(rows[0].get("total", 0) or 0) | |
| async def fetch_today_category_counts() -> List[Dict[str, Any]]: | |
| """统计当天按类别分组的数量""" | |
| query = """ | |
| SELECT | |
| COALESCE(category, 'unknown') AS category, | |
| COUNT(*) AS count | |
| FROM tpl_app_processed_images | |
| WHERE last_modified >= CURDATE() | |
| AND last_modified < DATE_ADD(CURDATE(), INTERVAL 1 DAY) | |
| GROUP BY COALESCE(category, 'unknown') | |
| """ | |
| rows = await fetch_all(query) | |
| return [ | |
| { | |
| "category": str(row.get("category") or "unknown"), | |
| "count": int(row.get("count") or 0), | |
| } | |
| for row in rows | |
| ] | |
| async def fetch_records_by_paths(file_paths: Iterable[str]) -> Dict[ | |
| str, Dict[str, Any]]: | |
| """根据文件名批量查询图片记录""" | |
| paths = list({path for path in file_paths if path}) | |
| if not paths: | |
| return {} | |
| placeholders = ", ".join(["%s"] * len(paths)) | |
| query = f""" | |
| SELECT | |
| file_path, | |
| category, | |
| nickname, | |
| score, | |
| is_cropped_face, | |
| size_bytes, | |
| last_modified, | |
| bos_uploaded, | |
| hostname, | |
| region | |
| FROM tpl_app_processed_images | |
| WHERE file_path IN ({placeholders}) | |
| """ | |
| rows = await fetch_all(query, paths) | |
| return {row["file_path"]: row for row in rows} | |
| _IMAGES_DIR_ABS = os.path.abspath(os.path.expanduser(IMAGES_DIR)) | |
| def _normalize_file_path(file_path: str) -> Optional[str]: | |
| """将绝对路径转换为相对 IMAGES_DIR 的文件名""" | |
| try: | |
| abs_path = os.path.abspath(os.path.expanduser(file_path)) | |
| if os.path.isdir(abs_path): | |
| return None | |
| if os.path.commonpath([_IMAGES_DIR_ABS, abs_path]) != _IMAGES_DIR_ABS: | |
| return os.path.basename(abs_path) | |
| rel_path = os.path.relpath(abs_path, _IMAGES_DIR_ABS) | |
| return rel_path.replace("\\", "/") | |
| except Exception: | |
| return None | |
| def infer_category_from_filename(filename: str, default: str = "other") -> str: | |
| """根据文件名推断类别""" | |
| lower_name = filename.lower() | |
| if "_face" in lower_name: | |
| return "face" | |
| if lower_name.endswith("_original.webp") or "_original" in lower_name: | |
| return "original" | |
| if "_restore" in lower_name: | |
| return "restore" | |
| if "_upcolor" in lower_name: | |
| return "upcolor" | |
| if "_compress" in lower_name: | |
| return "compress" | |
| if "_upscale" in lower_name: | |
| return "upscale" | |
| if "_anime_style_" in lower_name: | |
| return "anime_style" | |
| if "_grayscale" in lower_name: | |
| return "grayscale" | |
| if "_id_photo" in lower_name or "_save_id_photo" in lower_name: | |
| return "id_photo" | |
| if "_grid_" in lower_name: | |
| return "grid" | |
| if "_rvm_id_photo" in lower_name: | |
| return "rvm" | |
| if "_celebrity_" in lower_name or "_celebrity" in lower_name: | |
| return "celebrity" | |
| return default | |
| from config import HOSTNAME | |
| async def upsert_device_record( | |
| *, | |
| device_id: str, | |
| device_type: Optional[str] = None, | |
| device_model: Optional[str] = None, | |
| os_version: Optional[str] = None, | |
| app_version: Optional[str] = None, | |
| timezone: Optional[str] = None, | |
| region: Optional[str] = None, | |
| language: Optional[str] = None, | |
| client_source: str = "android", | |
| ) -> None: | |
| """写入或更新设备记录""" | |
| source = validate_client_source(client_source) | |
| query = """ | |
| INSERT INTO tpl_app_user_devices ( | |
| device_id, | |
| device_type, | |
| device_model, | |
| os_version, | |
| app_version, | |
| timezone, | |
| region, | |
| language, | |
| client_source | |
| ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) | |
| ON DUPLICATE KEY UPDATE | |
| client_source = IF(client_source = 'unknown', VALUES(client_source), client_source), | |
| device_type = VALUES(device_type), | |
| device_model = VALUES(device_model), | |
| os_version = VALUES(os_version), | |
| app_version = VALUES(app_version), | |
| timezone = VALUES(timezone), | |
| region = VALUES(region), | |
| language = VALUES(language), | |
| updated_at = CURRENT_TIMESTAMP | |
| """ | |
| import time | |
| start_time = time.perf_counter() | |
| try: | |
| await execute( | |
| query, | |
| ( | |
| device_id, | |
| device_type, | |
| device_model, | |
| os_version, | |
| app_version, | |
| timezone, | |
| region, | |
| language, | |
| source, | |
| ), | |
| ) | |
| duration = (time.perf_counter() - start_time) * 1000 | |
| logger.info(f"Device record updated for {device_id}, cost: {duration:.2f}ms") | |
| except Exception as exc: | |
| logger.warning(f"写入设备记录失败: {exc}") | |
| async def get_user_display_name(device_id: str) -> str: | |
| return (await get_user_profile(device_id))["display_name"] | |
| async def get_user_profile(device_id: str) -> Dict[str, Any]: | |
| versioned = os.getenv("ENABLE_PROFILE_REVISIONS", "false").lower() == "true" | |
| fields = "display_name, display_name_initialized" + (", profile_revision" if versioned else "") | |
| row = await fetch_one( | |
| f"SELECT {fields} FROM tpl_app_user_devices WHERE device_id = %s", | |
| (device_id,), | |
| ) | |
| name = str(row.get("display_name") or "") if row else "" | |
| result = {"display_name": name, | |
| "nickname_initialized": bool(name or (row and row.get("display_name_initialized")))} | |
| if versioned: | |
| result["client_revision"] = int(row.get("profile_revision") or 0) if row else 0 | |
| return result | |
| async def set_user_display_name( | |
| device_id: str, | |
| display_name: str, | |
| *, | |
| initialize_only: bool = False, | |
| client_revision: Optional[int] = None, | |
| device_type: Optional[str] = None, | |
| device_model: Optional[str] = None, | |
| os_version: Optional[str] = None, | |
| app_version: Optional[str] = None, | |
| timezone: Optional[str] = None, | |
| region: Optional[str] = None, | |
| language: Optional[str] = None, | |
| ) -> None: | |
| # 独立更新资料,不触碰评分记录的 updated_at、分数或原 nickname。 | |
| # 首次引导同时写入设备资料,单条语句保证昵称与设备信息一起保存。 | |
| # 旧客户端未传或传空的设备字段保留原值;空昵称仍表示主动清空。 | |
| # 条件判断与写入在同一条 SQL 内,避免初始化重试覆盖并发手动改名或清空。 | |
| name_update = ("IF(display_name_initialized = 0 AND display_name = '', VALUES(display_name), display_name)" | |
| if initialize_only else "VALUES(display_name)") | |
| versioned = client_revision is not None and os.getenv("ENABLE_PROFILE_REVISIONS", "false").lower() == "true" | |
| revision_column = ", profile_revision" if versioned else "" | |
| revision_value = ", %s" if versioned else "" | |
| revision_update = ", profile_revision = GREATEST(profile_revision, VALUES(profile_revision))" if versioned else "" | |
| if versioned and not initialize_only: | |
| name_update = "IF(VALUES(profile_revision) > profile_revision, VALUES(display_name), display_name)" | |
| await execute( | |
| f"""INSERT INTO tpl_app_user_devices ( | |
| device_id, display_name, display_name_initialized, client_source, device_type, device_model, | |
| os_version, app_version, timezone, region, language{revision_column} | |
| ) VALUES (%s, %s, 1, 'android', %s, %s, %s, %s, %s, %s, %s{revision_value}) | |
| ON DUPLICATE KEY UPDATE display_name = {name_update}, | |
| display_name_initialized = 1, | |
| client_source = IF(client_source = 'unknown', VALUES(client_source), client_source), | |
| device_type = COALESCE(NULLIF(VALUES(device_type), ''), device_type), | |
| device_model = COALESCE(NULLIF(VALUES(device_model), ''), device_model), | |
| os_version = COALESCE(NULLIF(VALUES(os_version), ''), os_version), | |
| app_version = COALESCE(NULLIF(VALUES(app_version), ''), app_version), | |
| timezone = COALESCE(NULLIF(VALUES(timezone), ''), timezone), | |
| region = COALESCE(NULLIF(VALUES(region), ''), region), | |
| language = COALESCE(NULLIF(VALUES(language), ''), language){revision_update}""", | |
| (device_id, display_name, device_type, device_model, os_version, | |
| app_version, timezone, region, language) + ((client_revision,) if versioned else ()) , | |
| ) | |
| async def fetch_user_devices( | |
| *, | |
| device_id: Optional[str] = None, | |
| region: Optional[str] = None, | |
| offset: int, | |
| limit: int, | |
| ) -> List[Dict[str, Any]]: | |
| """查询用户设备列表,支持按 device_id 和 region 筛选,按 created_at 倒序""" | |
| where_clauses: List[str] = [] | |
| params: List[Any] = [] | |
| if device_id: | |
| where_clauses.append("d.device_id LIKE %s") | |
| params.append(f"{device_id}%") | |
| if region: | |
| where_clauses.append("d.region = %s") | |
| params.append(region) | |
| where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else "" | |
| query = f""" | |
| SELECT | |
| d.device_id, | |
| d.device_type, | |
| d.device_model, | |
| d.os_version, | |
| d.app_version, | |
| d.timezone, | |
| d.region, | |
| d.language, | |
| d.created_at, | |
| d.updated_at, | |
| COALESCE(( | |
| SELECT COUNT(*) | |
| FROM tpl_app_processed_images p | |
| WHERE p.nickname LIKE CONCAT('android_release_', d.device_id, '%%') | |
| AND p.category = 'original' | |
| ), 0) AS detection_count | |
| FROM tpl_app_user_devices d | |
| {where_sql} | |
| ORDER BY d.updated_at DESC | |
| LIMIT %s OFFSET %s | |
| """ | |
| params.extend([limit, offset]) | |
| return await fetch_all(query, params) | |
| async def count_user_devices( | |
| *, | |
| device_id: Optional[str] = None, | |
| region: Optional[str] = None, | |
| ) -> int: | |
| """统计用户设备数量""" | |
| where_clauses: List[str] = [] | |
| params: List[Any] = [] | |
| if device_id: | |
| where_clauses.append("d.device_id LIKE %s") | |
| params.append(f"{device_id}%") | |
| if region: | |
| where_clauses.append("d.region = %s") | |
| params.append(region) | |
| where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else "" | |
| query = f"SELECT COUNT(*) AS total FROM tpl_app_user_devices d {where_sql}" | |
| rows = await fetch_all(query, params) | |
| if not rows: | |
| return 0 | |
| return int(rows[0].get("total", 0) or 0) | |
| async def fetch_device_region_stats() -> List[Dict[str, Any]]: | |
| """统计用户设备按地区分组的数量""" | |
| query = """ | |
| SELECT | |
| COALESCE(region, '') AS region, | |
| COUNT(*) AS count | |
| FROM tpl_app_user_devices | |
| GROUP BY COALESCE(region, '') | |
| ORDER BY count DESC | |
| """ | |
| return await fetch_all(query) | |
| async def fetch_image_region_stats( | |
| *, | |
| category: Optional[str], | |
| nickname: Optional[str], | |
| region: Optional[str] = None, | |
| platform: Optional[str] = None, | |
| is_cropped_face: Optional[int] = None, | |
| ) -> List[Dict[str, Any]]: | |
| """统计图片记录按地区分组的数量""" | |
| where_clauses: List[str] = [] | |
| params: List[Any] = [] | |
| if category and category != "all": | |
| where_clauses.append("category = %s") | |
| params.append(category) | |
| if nickname: | |
| where_clauses.append("nickname LIKE %s") | |
| params.append(f"{nickname}%") | |
| if platform in {"android", "ios", "web", "wechat", "miniprogram"}: | |
| where_clauses.append("client_source = %s") | |
| params.append("wechat" if platform == "miniprogram" else platform) | |
| if region: | |
| where_clauses.append("region = %s") | |
| params.append(region) | |
| if is_cropped_face is not None: | |
| where_clauses.append("is_cropped_face = %s") | |
| params.append(is_cropped_face) | |
| where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else "" | |
| query = f""" | |
| SELECT | |
| COALESCE(region, '') AS region, | |
| COUNT(*) AS count | |
| FROM tpl_app_processed_images | |
| {where_sql} | |
| GROUP BY COALESCE(region, '') | |
| ORDER BY count DESC | |
| """ | |
| return await fetch_all(query, params) | |
| async def record_image_creation( | |
| *, | |
| file_path: str, | |
| nickname: Optional[str], | |
| score: float = 0.0, | |
| category: Optional[str] = None, | |
| bos_uploaded: bool = False, | |
| region: Optional[str] = None, | |
| extra_metadata: Optional[Dict[str, Any]] = None, | |
| ) -> None: | |
| """ | |
| 记录图片元数据到数据库,如果数据库不可用则静默忽略。 | |
| :param file_path: 绝对或相对文件路径 | |
| :param nickname: 用户昵称 | |
| :param score: 关联得分 | |
| :param category: 文件类别,未提供时自动根据文件名推断 | |
| :param bos_uploaded: 是否已上传至 BOS | |
| :param extra_metadata: 额外信息 | |
| """ | |
| normalized = _normalize_file_path(file_path) | |
| if normalized is None: | |
| logger.info("record_image_creation: 无法计算文件名,路径=%s", file_path) | |
| return | |
| abs_path = os.path.join(_IMAGES_DIR_ABS, normalized) | |
| if not os.path.isfile(abs_path): | |
| logger.info("record_image_creation: 文件不存在,跳过记录 file=%s", abs_path) | |
| return | |
| try: | |
| stat = os.stat(abs_path) | |
| category_name = category or infer_category_from_filename(normalized) | |
| # 逻辑调整:支持 ugc_xxxx_face1.webp 这种格式 | |
| is_cropped_face = "_face" in normalized and normalized.count("_") >= 2 | |
| last_modified = datetime.fromtimestamp(stat.st_mtime) | |
| nickname_value = nickname.strip() if isinstance(nickname, | |
| str) and nickname.strip() else None | |
| # 如果 region 为空且为 Android 客户端请求,尝试通过 device_id 回填历史记录的 region | |
| region_val = region.strip() if isinstance(region, str) and region.strip() else None | |
| if not region_val and nickname_value and nickname_value.startswith("android_"): | |
| parts = nickname_value.split("_") | |
| device_id = parts[-1] if len(parts) >= 3 else None | |
| if device_id: | |
| try: | |
| row = await fetch_one( | |
| "SELECT region FROM tpl_app_user_devices WHERE device_id = %s AND region IS NOT NULL AND region != '' LIMIT 1", | |
| (device_id,), | |
| ) | |
| if row and row.get("region"): | |
| region_val = row.get("region") | |
| except Exception: | |
| pass | |
| await upsert_image_record( | |
| file_path=normalized, | |
| category=category_name, | |
| nickname=nickname_value, | |
| score=score, | |
| is_cropped_face=is_cropped_face, | |
| size_bytes=stat.st_size, | |
| last_modified=last_modified, | |
| bos_uploaded=bos_uploaded, | |
| hostname=HOSTNAME, | |
| region=region_val, | |
| extra_metadata=extra_metadata, | |
| ) | |
| except Exception as exc: | |
| logger.warning(f"写入图片记录失败: {exc}") | |
| # 排名与排行榜内存缓存 | |
| _RANK_CACHE: Dict[str, Tuple[float, Dict[str, Any]]] = {} | |
| _CACHE_TTL_RANK = 60 # 60秒 | |
| _LEADERBOARD_CACHE: Dict[str, Tuple[float, List[Dict[str, Any]]]] = {} | |
| _CACHE_TTL_LEADERBOARD = 180 # 3分钟 | |
| def _ranking_scope(source: str, alias: str = "") -> str: | |
| validate_client_source(source) | |
| prefix = alias + "." if alias else "" | |
| condition = f"{prefix}client_source = %s" | |
| if source == "android": | |
| condition += f" AND ({prefix}nickname IS NULL OR LEFT({prefix}nickname, 14) <> 'android_debug_')" | |
| return condition | |
| async def get_face_score_rank( | |
| score: float, | |
| region: Optional[str] = None, | |
| gender: Optional[str] = None, | |
| age: Optional[str] = None, | |
| client_source: str = "android", | |
| ) -> Dict[str, Any]: | |
| """ | |
| 计算颜值评分在全球和当前地区的排名及击败百分比 | |
| :param score: 颜值分数 (0.0 ~ 100.0) | |
| :param region: 用户所在地区代码 (如 "CN", "US") | |
| :param gender: 预留性别 | |
| :param age: 预留年龄 | |
| :return: 包含地区和全球排名统计的字典 | |
| """ | |
| from region_code_utils import get_country_name_by_region_code | |
| scope = _ranking_scope(client_source) | |
| score_val = max(0.0, min(100.0, float(score))) | |
| region_code = region.strip().upper() if region and region.strip() and region.strip().upper() != "GLOBAL" else "" | |
| region_name = get_country_name_by_region_code(region_code) if region_code else "全球" | |
| days_limit = max(1, int(LEADERBOARD_DAYS_LIMIT)) | |
| # 排名对小数分数敏感,不能按 0.1 分缓存,否则相邻分数会误用同一名次。 | |
| cache_key = f"{client_source}:{score_val:.4f}:{region_code}:{days_limit}" | |
| now = time.time() | |
| if cache_key in _RANK_CACHE: | |
| cached_time, cached_data = _RANK_CACHE[cache_key] | |
| if now - cached_time < _CACHE_TTL_RANK: | |
| return cached_data | |
| # 统一分数比较表达式:兼容历史库内可能存在的 10 分制 (0~10) 与 100 分制 (0~100) | |
| score_expr = "(CASE WHEN score <= 10.0 THEN score * 10.0 ELSE score END)" | |
| success = True | |
| global_rank = 0 | |
| global_total = 0 | |
| global_percentile = 0.0 | |
| region_rank = 0 | |
| region_total = 0 | |
| region_percentile = 0.0 | |
| try: | |
| # 排行榜按用户最高分去重;/rank 必须使用完全相同的用户最高分去重口径。 | |
| user_key_expr = "(CASE WHEN nickname IS NOT NULL AND nickname != '' THEN nickname ELSE CONCAT('id_', id) END)" | |
| g_stats_row = await fetch_one( | |
| f""" | |
| SELECT COUNT(*) AS t, | |
| COALESCE(SUM(CASE WHEN max_score > %s THEN 1 ELSE 0 END), 0) + 1 AS r | |
| FROM ( | |
| SELECT {user_key_expr} AS user_key, | |
| MAX({score_expr}) AS max_score | |
| FROM tpl_app_processed_images | |
| WHERE category = 'face' | |
| AND {scope} | |
| AND updated_at >= DATE_SUB(NOW(), INTERVAL {days_limit} DAY) | |
| AND score > 0 | |
| GROUP BY user_key | |
| ) AS ranked_users | |
| """, | |
| (score_val, client_source) | |
| ) | |
| if g_stats_row is not None: | |
| db_g_total = int(g_stats_row.get("t") or 0) | |
| db_g_rank = int(g_stats_row.get("r") or 1) | |
| if db_g_total > 0: | |
| global_total = db_g_total | |
| global_rank = min(global_total, max(1, db_g_rank)) | |
| # 击败百分比 = (total - rank + 1) / total * 100 | |
| global_percentile = round((global_total - global_rank + 1) / global_total * 100.0, 2) | |
| global_percentile = max(0.01, min(100.0, global_percentile)) | |
| # 地区排名计算 | |
| if region_code: | |
| r_stats_row = await fetch_one( | |
| f""" | |
| SELECT COUNT(*) AS t, | |
| COALESCE(SUM(CASE WHEN max_score > %s THEN 1 ELSE 0 END), 0) + 1 AS r | |
| FROM ( | |
| SELECT {user_key_expr} AS user_key, | |
| MAX({score_expr}) AS max_score | |
| FROM tpl_app_processed_images | |
| WHERE category = 'face' AND region = %s | |
| AND {scope} | |
| AND updated_at >= DATE_SUB(NOW(), INTERVAL {days_limit} DAY) | |
| AND score > 0 | |
| GROUP BY user_key | |
| ) AS ranked_users | |
| """, | |
| (score_val, region_code, client_source) | |
| ) | |
| if r_stats_row is not None: | |
| db_r_total = int(r_stats_row.get("t") or 0) | |
| db_r_rank = int(r_stats_row.get("r") or 1) | |
| if db_r_total > 0: | |
| region_total = db_r_total | |
| region_rank = min(region_total, max(1, db_r_rank)) | |
| region_percentile = round((region_total - region_rank + 1) / region_total * 100.0, 2) | |
| region_percentile = max(0.01, min(100.0, region_percentile)) | |
| else: | |
| region_total = global_total | |
| region_rank = global_rank | |
| region_percentile = global_percentile | |
| except Exception as exc: | |
| logger.warning(f"获取颜值排名数据库查询异常: {exc}") | |
| success = False | |
| result = { | |
| "success": success, | |
| "score": score_val, | |
| "region_code": region_code or "GLOBAL", | |
| "region_name": region_name, | |
| "region_rank": region_rank, | |
| "region_total": region_total, | |
| "region_percentile": region_percentile, | |
| "global_rank": global_rank, | |
| "global_total": global_total, | |
| "global_percentile": global_percentile, | |
| "days_limit": days_limit, | |
| } | |
| if success: | |
| _RANK_CACHE[cache_key] = (now, result) | |
| return result | |
| async def get_face_score_leaderboard( | |
| region: Optional[str] = None, | |
| limit: int = 50, | |
| current_device_id: Optional[str] = None, | |
| client_source: str = "android", | |
| ) -> Dict[str, Any]: | |
| result = await _get_cached_face_score_leaderboard(region, limit, current_device_id, client_source) | |
| if client_source != "android" or not result["success"] or not result["items"]: | |
| return result | |
| # 排名缓存只保存评分记录;昵称每次批量读取,跨 worker 改名/清空也立即生效。 | |
| # Android 上传格式为 prefix_androidId,历史 android_* 前缀同样兼容。 | |
| device_ids = sorted({item["nickname"].rsplit("_", 1)[-1] | |
| for item in result["items"] if item["nickname"].startswith("android_")}) | |
| try: | |
| names: Dict[str, str] = {} | |
| if device_ids: | |
| placeholders = ", ".join(["%s"] * len(device_ids)) | |
| rows = await fetch_all( | |
| f"SELECT device_id, display_name FROM tpl_app_user_devices WHERE client_source = 'android' AND device_id IN ({placeholders})", | |
| device_ids, | |
| ) | |
| names = {row["device_id"]: str(row.get("display_name") or "") for row in rows} | |
| for item in result["items"]: | |
| item["display_name"] = names.get(item["nickname"].rsplit("_", 1)[-1], "") if item["nickname"].startswith("android_") else "" | |
| return result | |
| except Exception as exc: | |
| logger.warning("读取排行榜昵称失败: %s", exc) | |
| return {**result, "success": False, "items": [], "total_records": 0} | |
| async def _get_cached_face_score_leaderboard( | |
| region: Optional[str] = None, | |
| limit: int = 50, | |
| current_device_id: Optional[str] = None, | |
| client_source: str = "android", | |
| ) -> Dict[str, Any]: | |
| """ | |
| 获取颜值评分记录排行榜 Top N (支持地区榜与全球榜)。 | |
| 按用户(nickname)去重,每个用户仅保留最高分记录;无昵称的匿名记录独立保留。 | |
| """ | |
| from region_code_utils import get_country_name_by_region_code | |
| scope = _ranking_scope(client_source, "candidate") | |
| region_code = region.strip().upper() if region and region.strip() and region.strip().upper() != "GLOBAL" else None | |
| days_limit = max(1, int(LEADERBOARD_DAYS_LIMIT)) | |
| # limit 会改变缓存内容,必须纳入 key,避免一次小 limit 请求污染后续 Top 50。 | |
| safe_limit = max(1, min(100, int(limit))) | |
| cache_key = f"{client_source}:{region_code or 'GLOBAL'}:{days_limit}:{safe_limit}" | |
| now = time.time() | |
| def response_items(cached_or_fresh_items: List[Dict[str, Any]]) -> List[Dict[str, Any]]: | |
| device_suffix = f"_{current_device_id}" if current_device_id and client_source == "android" else None | |
| result_items: List[Dict[str, Any]] = [] | |
| for cached_item in cached_or_fresh_items[:limit]: | |
| nickname = str(cached_item.get("nickname") or "") | |
| public_item = dict(cached_item) | |
| public_item["is_current_user"] = bool(device_suffix and nickname.endswith(device_suffix)) | |
| result_items.append(public_item) | |
| return result_items | |
| if cache_key in _LEADERBOARD_CACHE: | |
| cached_time, cached_items = _LEADERBOARD_CACHE[cache_key] | |
| if now - cached_time < _CACHE_TTL_LEADERBOARD: | |
| return { | |
| "success": True, | |
| "region_code": region_code or "GLOBAL", | |
| "region_name": get_country_name_by_region_code(region_code) if region_code else "全球", | |
| "total_records": len(cached_items), | |
| "items": response_items(cached_items), | |
| "days_limit": days_limit, | |
| } | |
| success = True | |
| items = [] | |
| candidate_score_expr = "(CASE WHEN candidate.score <= 10.0 THEN candidate.score * 10.0 ELSE candidate.score END)" | |
| better_score_expr = "(CASE WHEN better.score <= 10.0 THEN better.score * 10.0 ELSE better.score END)" | |
| better_scope = _ranking_scope(client_source, "better") | |
| try: | |
| if region_code: | |
| query = f""" | |
| SELECT candidate.id, {candidate_score_expr} AS score, candidate.region, | |
| candidate.nickname, candidate.file_path, candidate.extra_metadata, candidate.updated_at, candidate.created_at | |
| FROM tpl_app_processed_images AS candidate | |
| WHERE candidate.category = 'face' AND candidate.score > 0 AND candidate.region = %s | |
| AND {scope} | |
| AND candidate.updated_at >= DATE_SUB(NOW(), INTERVAL {days_limit} DAY) | |
| AND ( | |
| candidate.nickname IS NULL | |
| OR candidate.nickname = '' | |
| OR NOT EXISTS ( | |
| SELECT 1 | |
| FROM tpl_app_processed_images AS better | |
| WHERE better.nickname = candidate.nickname | |
| AND better.category = 'face' AND better.score > 0 AND better.region = %s | |
| AND {better_scope} | |
| AND better.updated_at >= DATE_SUB(NOW(), INTERVAL {days_limit} DAY) | |
| AND ( | |
| {better_score_expr} > {candidate_score_expr} | |
| OR ({better_score_expr} = {candidate_score_expr} AND better.id < candidate.id) | |
| ) | |
| ) | |
| ) | |
| ORDER BY score DESC, candidate.id ASC | |
| LIMIT {safe_limit} | |
| """ | |
| rows = await fetch_all(query, (region_code, client_source, region_code, client_source)) | |
| else: | |
| query = f""" | |
| SELECT candidate.id, {candidate_score_expr} AS score, candidate.region, | |
| candidate.nickname, candidate.file_path, candidate.extra_metadata, candidate.updated_at, candidate.created_at | |
| FROM tpl_app_processed_images AS candidate | |
| WHERE candidate.category = 'face' AND candidate.score > 0 | |
| AND {scope} | |
| AND candidate.updated_at >= DATE_SUB(NOW(), INTERVAL {days_limit} DAY) | |
| AND ( | |
| candidate.nickname IS NULL | |
| OR candidate.nickname = '' | |
| OR NOT EXISTS ( | |
| SELECT 1 | |
| FROM tpl_app_processed_images AS better | |
| WHERE better.nickname = candidate.nickname | |
| AND better.category = 'face' AND better.score > 0 | |
| AND {better_scope} | |
| AND better.updated_at >= DATE_SUB(NOW(), INTERVAL {days_limit} DAY) | |
| AND ( | |
| {better_score_expr} > {candidate_score_expr} | |
| OR ({better_score_expr} = {candidate_score_expr} AND better.id < candidate.id) | |
| ) | |
| ) | |
| ) | |
| ORDER BY score DESC, candidate.id ASC | |
| LIMIT {safe_limit} | |
| """ | |
| rows = await fetch_all(query, (client_source, client_source)) | |
| seen_users = set() | |
| for row in rows: | |
| raw_nick = str(row.get("nickname") or "").strip() | |
| user_key = raw_nick if raw_nick else f"id_{row.get('id')}" | |
| if user_key in seen_users: | |
| continue | |
| seen_users.add(user_key) | |
| row_id = int(row.get("id") or 0) | |
| score_val = round(float(row.get("score") or 0.0), 2) | |
| row_region = row.get("region") or region_code or "US" | |
| row_country_name = get_country_name_by_region_code(row_region) | |
| meta = row.get("extra_metadata") | |
| if isinstance(meta, str): | |
| try: | |
| meta = json.loads(meta) | |
| except Exception: | |
| meta = {} | |
| elif not isinstance(meta, dict): | |
| meta = {} | |
| raw_gender = str(meta.get("gender") or "").strip().lower() | |
| if any(w in raw_gender for w in ["female", "woman", "women", "女", "f"]): | |
| gender = "Female" | |
| elif any(w in raw_gender for w in ["male", "man", "men", "男", "m"]): | |
| gender = "Male" | |
| else: | |
| gender = "Female" if (row_id % 2 == 0) else "Male" | |
| age = str(meta.get("age") or (20 + (row_id % 12))) | |
| # 确定性假头像 ID (1..16),女 1..8,男 9..16 | |
| if gender == "Female": | |
| avatar_id = (row_id % 8) + 1 | |
| else: | |
| avatar_id = (row_id % 8) + 9 | |
| updated_at_val = row.get("updated_at") or row.get("created_at") | |
| updated_at_str = str(updated_at_val or "") | |
| items.append({ | |
| "record_id": row_id, | |
| "file_path": str(row.get("file_path") or ""), | |
| "rank": len(items) + 1, | |
| "score": score_val, | |
| "nickname": str(row.get("nickname") or ""), | |
| "region_code": row_region, | |
| "region_name": row_country_name, | |
| "gender": gender, | |
| "age": age, | |
| "avatar_id": avatar_id, | |
| "created_at": updated_at_str, | |
| "updated_at": updated_at_str, | |
| }) | |
| if len(items) >= limit: | |
| break | |
| except Exception as exc: | |
| logger.warning(f"获取颜值排行榜数据库异常: {exc}") | |
| success = False | |
| if success: | |
| _LEADERBOARD_CACHE[cache_key] = (now, items) | |
| return { | |
| "success": success, | |
| "region_code": region_code or "GLOBAL", | |
| "region_name": get_country_name_by_region_code(region_code) if region_code else "全球", | |
| "total_records": len(items), | |
| "items": response_items(items), | |
| "days_limit": days_limit, | |
| } | |
| # 清理调试数据 SQL 列表,可在此处增删 SQL | |
| CLEAN_DEBUG_SQLS = [ | |
| "DELETE FROM tpl_app_user_devices WHERE device_model='Xiaomi 23127PN0CC' AND region='CN'", | |
| "DELETE FROM tpl_app_user_devices WHERE device_id='cfcae0ec66c8b429'", | |
| "DELETE FROM tpl_app_user_devices WHERE device_id='7bfefa2d7f8c0be3'", | |
| "DELETE FROM tpl_app_user_devices WHERE device_id='782831daa5e25b35'", | |
| "DELETE FROM tpl_app_processed_images WHERE nickname LIKE 'android_debug%%'", | |
| "DELETE FROM tpl_app_processed_images WHERE nickname='string'", | |
| "DELETE FROM tpl_app_processed_images WHERE nickname='android_release_1b708ef79ac4f484'", | |
| "DELETE FROM tpl_app_processed_images WHERE nickname='android_release_3912d8c0e7f9c153'", | |
| "DELETE FROM tpl_app_processed_images WHERE nickname='android_release_782831daa5e25b35'", | |
| "DELETE FROM tpl_app_processed_images WHERE nickname='9527'", | |
| ] | |
| async def clean_debug_data() -> Dict[str, int]: | |
| """ | |
| 清理调试数据 | |
| :return: 返回每个 SQL 影响的行数 | |
| """ | |
| results = {} | |
| for idx, sql in enumerate(CLEAN_DEBUG_SQLS): | |
| try: | |
| affected = await execute(sql) | |
| results[f"sql_{idx + 1}"] = affected | |
| logger.info(f"Clean debug SQL #{idx + 1} executed, affected: {affected}") | |
| except Exception as exc: | |
| logger.warning(f"Clean debug SQL #{idx + 1} failed: {exc}") | |
| results[f"sql_{idx + 1}"] = -1 | |
| return results | |