diff --git a/Dockerfile b/Dockerfile index b24aeac..4637953 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,13 @@ # Backend Dockerfile for Cosmo -FROM python:3.12-slim +FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/python:3.12-slim # Set working directory WORKDIR /app +# Configure Debian mirrors (Aliyun) +RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \ + sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources + # Install system dependencies RUN apt-get update && apt-get install -y \ gcc \ @@ -11,6 +15,10 @@ RUN apt-get update && apt-get install -y \ curl \ && rm -rf /var/lib/apt/lists/* +# Configure pip to use Aliyun mirror +RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \ + pip config set install.trusted-host mirrors.aliyun.com + # Copy requirements first for better caching COPY requirements.txt . diff --git a/app/config.py b/app/config.py index 2c976d3..7f1358e 100644 --- a/app/config.py +++ b/app/config.py @@ -1,19 +1,40 @@ """ Application configuration """ -from pydantic_settings import BaseSettings -from pydantic import Field +from typing import Union, Any +from pydantic_settings import BaseSettings, SettingsConfigDict +from pydantic import Field, field_validator, ValidationInfo class Settings(BaseSettings): """Application settings""" + model_config = SettingsConfigDict( + env_file=".env", + # Don't try to parse json for environment variables + env_parse_none_str=None, + ) + # Application app_name: str = "Cosmo - Deep Space Explorer" api_prefix: str = "/api" - # CORS settings - allow all origins for development (IP access support) - cors_origins: list[str] = ["*"] + # CORS settings - stored as string in env, converted to list + cors_origins: Union[str, list[str]] = "*" + + @field_validator('cors_origins', mode='before') + @classmethod + def validate_cors_origins(cls, v: Any) -> list[str]: + """Parse CORS origins from comma-separated string or JSON array""" + if v is None: + return ["*"] + if isinstance(v, str): + # Parse comma-separated string + origins = [origin.strip() for origin in v.split(',') if origin.strip()] + return origins if origins else ["*"] + if isinstance(v, list): + return v + return ["*"] # Cache settings cache_ttl_days: int = 3 @@ -43,6 +64,22 @@ class Settings(BaseSettings): upload_dir: str = "upload" max_upload_size: int = 10485760 # 10MB + # Proxy settings (for accessing NASA JPL Horizons API in China) + http_proxy: str = "" + https_proxy: str = "" + + @property + def proxy_dict(self) -> dict[str, str] | None: + """Get proxy configuration as a dictionary for httpx""" + if self.http_proxy or self.https_proxy: + proxies = {} + if self.http_proxy: + proxies["http://"] = self.http_proxy + if self.https_proxy: + proxies["https://"] = self.https_proxy + return proxies + return None + @property def database_url(self) -> str: """Construct database URL for SQLAlchemy""" @@ -58,8 +95,5 @@ class Settings(BaseSettings): return f"redis://:{self.redis_password}@{self.redis_host}:{self.redis_port}/{self.redis_db}" return f"redis://{self.redis_host}:{self.redis_port}/{self.redis_db}" - class Config: - env_file = ".env" - settings = Settings() diff --git a/app/main.py b/app/main.py index ac1ab9a..687eab9 100644 --- a/app/main.py +++ b/app/main.py @@ -34,11 +34,19 @@ from app.services.cache_preheat import preheat_all_caches from app.database import close_db # Configure logging +# Set root logger to WARNING in production, INFO in development +log_level = logging.INFO if settings.jwt_secret_key == "your-secret-key-change-this-in-production" else logging.WARNING logging.basicConfig( - level=logging.INFO, + level=log_level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) +# Reduce noise from specific loggers in production +if log_level == logging.WARNING: + logging.getLogger("app.services.cache").setLevel(logging.ERROR) + logging.getLogger("app.services.redis_cache").setLevel(logging.ERROR) + logging.getLogger("app.api.celestial_position").setLevel(logging.WARNING) + logger = logging.getLogger(__name__) diff --git a/app/services/horizons.py b/app/services/horizons.py index c9abca7..ed678be 100644 --- a/app/services/horizons.py +++ b/app/services/horizons.py @@ -7,8 +7,10 @@ from astropy.time import Time import logging import re import httpx +import os from app.models.celestial import Position, CelestialBody +from app.config import settings logger = logging.getLogger(__name__) @@ -20,6 +22,15 @@ class HorizonsService: """Initialize the service""" self.location = "@sun" # Heliocentric coordinates + # Set proxy for astroquery if configured + # astroquery uses standard HTTP_PROXY and HTTPS_PROXY environment variables + if settings.http_proxy: + os.environ['HTTP_PROXY'] = settings.http_proxy + logger.info(f"Set HTTP_PROXY for astroquery: {settings.http_proxy}") + if settings.https_proxy: + os.environ['HTTPS_PROXY'] = settings.https_proxy + logger.info(f"Set HTTPS_PROXY for astroquery: {settings.https_proxy}") + async def get_object_data_raw(self, body_id: str) -> str: """ Get raw object data (terminal style text) from Horizons @@ -33,7 +44,7 @@ class HorizonsService: url = "https://ssd.jpl.nasa.gov/api/horizons.api" # Ensure ID is quoted for COMMAND cmd_val = f"'{body_id}'" if not body_id.startswith("'") else body_id - + params = { "format": "text", "COMMAND": cmd_val, @@ -44,9 +55,15 @@ class HorizonsService: } try: - async with httpx.AsyncClient() as client: + # Configure proxy if available + client_kwargs = {"timeout": 5.0} + if settings.proxy_dict: + client_kwargs["proxies"] = settings.proxy_dict + logger.info(f"Using proxy for NASA API: {settings.proxy_dict}") + + async with httpx.AsyncClient(**client_kwargs) as client: logger.info(f"Fetching raw data for body {body_id}") - response = await client.get(url, params=params, timeout=30.0) + response = await client.get(url, params=params) if response.status_code != 200: raise Exception(f"NASA API returned status {response.status_code}") diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py index 8c6dc65..1d5a3b8 100644 --- a/app/services/system_settings_service.py +++ b/app/services/system_settings_service.py @@ -211,8 +211,16 @@ class SystemSettingsService: for default in defaults: existing = await self.get_setting(default["key"], session) if not existing: - await self.create_setting(default, session) - logger.info(f"Created default setting: {default['key']}") + try: + await self.create_setting(default, session) + logger.info(f"Created default setting: {default['key']}") + except Exception as e: + # Ignore duplicate key errors (race condition between workers) + if "duplicate key" in str(e).lower() or "unique constraint" in str(e).lower(): + logger.debug(f"Setting {default['key']} already exists (created by another worker)") + else: + logger.error(f"Error creating default setting {default['key']}: {e}") + raise # Singleton instance diff --git a/pip.conf b/pip.conf new file mode 100644 index 0000000..3a34e37 --- /dev/null +++ b/pip.conf @@ -0,0 +1,9 @@ +# pip configuration for Aliyun mirror +# This file speeds up pip install in development + +[global] +index-url = https://mirrors.aliyun.com/pypi/simple/ +trusted-host = mirrors.aliyun.com + +[install] +trusted-host = mirrors.aliyun.com diff --git a/requirements.txt b/requirements.txt index 6603b8d..9d105a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ astroquery==0.4.7 astropy==6.0.0 pydantic==2.5.0 pydantic-settings==2.1.0 +email-validator==2.1.0 python-dotenv==1.0.0 httpx==0.25.2 diff --git a/scripts/init_db.sql b/scripts/init_db.sql index c519efc..7658de7 100644 --- a/scripts/init_db.sql +++ b/scripts/init_db.sql @@ -12,7 +12,7 @@ Target Server Version : 140008 (140008) File Encoding : 65001 - Date: 02/12/2025 16:02:50 + Date: 02/12/2025 23:57:44 */ @@ -5350,6 +5350,7 @@ INSERT INTO "public"."system_settings" ("id", "key", "value", "value_type", "cat INSERT INTO "public"."system_settings" ("id", "key", "value", "value_type", "category", "label", "description", "is_public", "created_at", "updated_at") VALUES (6, 'orbit_points', '200', 'int', 'visualization', '轨道线点数', '生成轨道线时使用的点数,越多越平滑但性能越低', 't', '2025-11-30 05:10:43.416174+00', NULL); INSERT INTO "public"."system_settings" ("id", "key", "value", "value_type", "category", "label", "description", "is_public", "created_at", "updated_at") VALUES (1, 'timeline_interval_days', '30', 'int', 'visualization', '时间轴播放间隔(天)', '星图时间轴播放时每次跳转的天数间隔', 't', '2025-11-30 05:10:43.416174+00', '2025-11-30 15:21:56.563851+00'); INSERT INTO "public"."system_settings" ("id", "key", "value", "value_type", "category", "label", "description", "is_public", "created_at", "updated_at") VALUES (7, 'danmaku_ttl', '86400', 'int', 'platform', '弹幕保留时间', '用户发送的弹幕在系统中保留的时间(秒)', 't', '2025-11-30 16:12:15.94262+00', NULL); +INSERT INTO "public"."system_settings" ("id", "key", "value", "value_type", "category", "label", "description", "is_public", "created_at", "updated_at") VALUES (8, 'default_password', 'cosmo', 'string', 'general', '初始化密码', '重置的默认密码', 'f', '2025-12-02 10:35:08.508068+00', NULL); COMMIT; -- ---------------------------- @@ -5434,8 +5435,8 @@ COMMENT ON COLUMN "public"."users"."last_login_at" IS 'Last login time'; -- Records of users -- ---------------------------- BEGIN; -INSERT INTO "public"."users" ("id", "username", "password_hash", "email", "full_name", "is_active", "created_at", "updated_at", "last_login_at") VALUES (3, 'pepperdog', '$2b$12$KqTNQKf13L5rDUkt6yabNuh7fbmRsxME3W5OoVXj/C2.CLNcHvAz.', 'mula.liu@163.com', '胡椒狗', 't', '2025-11-29 18:30:52.045915', '2025-12-01 09:22:53.233616', '2025-12-01 09:22:53.516333'); -INSERT INTO "public"."users" ("id", "username", "password_hash", "email", "full_name", "is_active", "created_at", "updated_at", "last_login_at") VALUES (2, 'cosmo', '$2b$12$42d8/NAaYJlK8w/1yBd5uegdHlDkpC9XFtXYu2sWq0EXj48KAMZ0i', 'admin@cosmo.com', 'COSMO零号', 't', '2025-11-28 18:07:11.767382', '2025-12-02 03:27:52.709283', '2025-12-02 03:27:52.961435'); +INSERT INTO "public"."users" ("id", "username", "password_hash", "email", "full_name", "is_active", "created_at", "updated_at", "last_login_at") VALUES (3, 'pepperdog', '$2b$12$zgLRor/cbpt0TF2aqeuuDupeXRRXrtBL57rFgfICzPjRYync6zYIq', 'mula.liu@163.com', '胡椒狗', 't', '2025-11-29 18:30:52.045915', '2025-12-02 10:42:10.298484', '2025-12-02 10:42:10.556614'); +INSERT INTO "public"."users" ("id", "username", "password_hash", "email", "full_name", "is_active", "created_at", "updated_at", "last_login_at") VALUES (2, 'cosmo', '$2b$12$42d8/NAaYJlK8w/1yBd5uegdHlDkpC9XFtXYu2sWq0EXj48KAMZ0i', 'admin@cosmo.com', 'COSMO零号', 't', '2025-11-28 18:07:11.767382', '2025-12-02 10:42:52.277596', '2025-12-02 10:42:52.561184'); COMMIT; -- ---------------------------- @@ -5492,7 +5493,7 @@ SELECT setval('"public"."static_data_id_seq"', 64, true); -- ---------------------------- ALTER SEQUENCE "public"."system_settings_id_seq" OWNED BY "public"."system_settings"."id"; -SELECT setval('"public"."system_settings_id_seq"', 7, true); +SELECT setval('"public"."system_settings_id_seq"', 8, true); -- ---------------------------- -- Alter sequences owned by