添加了nasa接口的代理服务
parent
f3f8251f47
commit
de5447c5e5
10
Dockerfile
10
Dockerfile
|
|
@ -1,9 +1,13 @@
|
||||||
# Backend Dockerfile for Cosmo
|
# 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
|
# Set working directory
|
||||||
WORKDIR /app
|
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
|
# Install system dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
gcc \
|
gcc \
|
||||||
|
|
@ -11,6 +15,10 @@ RUN apt-get update && apt-get install -y \
|
||||||
curl \
|
curl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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 first for better caching
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,40 @@
|
||||||
"""
|
"""
|
||||||
Application configuration
|
Application configuration
|
||||||
"""
|
"""
|
||||||
from pydantic_settings import BaseSettings
|
from typing import Union, Any
|
||||||
from pydantic import Field
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
from pydantic import Field, field_validator, ValidationInfo
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
"""Application settings"""
|
"""Application settings"""
|
||||||
|
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
# Don't try to parse json for environment variables
|
||||||
|
env_parse_none_str=None,
|
||||||
|
)
|
||||||
|
|
||||||
# Application
|
# Application
|
||||||
app_name: str = "Cosmo - Deep Space Explorer"
|
app_name: str = "Cosmo - Deep Space Explorer"
|
||||||
api_prefix: str = "/api"
|
api_prefix: str = "/api"
|
||||||
|
|
||||||
# CORS settings - allow all origins for development (IP access support)
|
# CORS settings - stored as string in env, converted to list
|
||||||
cors_origins: list[str] = ["*"]
|
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 settings
|
||||||
cache_ttl_days: int = 3
|
cache_ttl_days: int = 3
|
||||||
|
|
@ -43,6 +64,22 @@ class Settings(BaseSettings):
|
||||||
upload_dir: str = "upload"
|
upload_dir: str = "upload"
|
||||||
max_upload_size: int = 10485760 # 10MB
|
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
|
@property
|
||||||
def database_url(self) -> str:
|
def database_url(self) -> str:
|
||||||
"""Construct database URL for SQLAlchemy"""
|
"""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_password}@{self.redis_host}:{self.redis_port}/{self.redis_db}"
|
||||||
return f"redis://{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()
|
settings = Settings()
|
||||||
|
|
|
||||||
10
app/main.py
10
app/main.py
|
|
@ -34,11 +34,19 @@ from app.services.cache_preheat import preheat_all_caches
|
||||||
from app.database import close_db
|
from app.database import close_db
|
||||||
|
|
||||||
# Configure logging
|
# 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(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=log_level,
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@ from astropy.time import Time
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import httpx
|
import httpx
|
||||||
|
import os
|
||||||
|
|
||||||
from app.models.celestial import Position, CelestialBody
|
from app.models.celestial import Position, CelestialBody
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -20,6 +22,15 @@ class HorizonsService:
|
||||||
"""Initialize the service"""
|
"""Initialize the service"""
|
||||||
self.location = "@sun" # Heliocentric coordinates
|
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:
|
async def get_object_data_raw(self, body_id: str) -> str:
|
||||||
"""
|
"""
|
||||||
Get raw object data (terminal style text) from Horizons
|
Get raw object data (terminal style text) from Horizons
|
||||||
|
|
@ -33,7 +44,7 @@ class HorizonsService:
|
||||||
url = "https://ssd.jpl.nasa.gov/api/horizons.api"
|
url = "https://ssd.jpl.nasa.gov/api/horizons.api"
|
||||||
# Ensure ID is quoted for COMMAND
|
# Ensure ID is quoted for COMMAND
|
||||||
cmd_val = f"'{body_id}'" if not body_id.startswith("'") else body_id
|
cmd_val = f"'{body_id}'" if not body_id.startswith("'") else body_id
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"format": "text",
|
"format": "text",
|
||||||
"COMMAND": cmd_val,
|
"COMMAND": cmd_val,
|
||||||
|
|
@ -44,9 +55,15 @@ class HorizonsService:
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
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}")
|
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:
|
if response.status_code != 200:
|
||||||
raise Exception(f"NASA API returned status {response.status_code}")
|
raise Exception(f"NASA API returned status {response.status_code}")
|
||||||
|
|
|
||||||
|
|
@ -211,8 +211,16 @@ class SystemSettingsService:
|
||||||
for default in defaults:
|
for default in defaults:
|
||||||
existing = await self.get_setting(default["key"], session)
|
existing = await self.get_setting(default["key"], session)
|
||||||
if not existing:
|
if not existing:
|
||||||
await self.create_setting(default, session)
|
try:
|
||||||
logger.info(f"Created default setting: {default['key']}")
|
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
|
# Singleton instance
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -4,6 +4,7 @@ astroquery==0.4.7
|
||||||
astropy==6.0.0
|
astropy==6.0.0
|
||||||
pydantic==2.5.0
|
pydantic==2.5.0
|
||||||
pydantic-settings==2.1.0
|
pydantic-settings==2.1.0
|
||||||
|
email-validator==2.1.0
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
httpx==0.25.2
|
httpx==0.25.2
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
Target Server Version : 140008 (140008)
|
Target Server Version : 140008 (140008)
|
||||||
File Encoding : 65001
|
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 (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 (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 (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;
|
COMMIT;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
|
|
@ -5434,8 +5435,8 @@ COMMENT ON COLUMN "public"."users"."last_login_at" IS 'Last login time';
|
||||||
-- Records of users
|
-- Records of users
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
BEGIN;
|
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 (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 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 (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;
|
COMMIT;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
|
|
@ -5492,7 +5493,7 @@ SELECT setval('"public"."static_data_id_seq"', 64, true);
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
ALTER SEQUENCE "public"."system_settings_id_seq"
|
ALTER SEQUENCE "public"."system_settings_id_seq"
|
||||||
OWNED BY "public"."system_settings"."id";
|
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
|
-- Alter sequences owned by
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue