From 088529d6c4d32347f494dd640af97d39339ee0fe Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Thu, 4 Dec 2025 20:37:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E5=B9=85=E8=B0=83=E6=95=B4=E7=9A=84?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E7=89=88=E6=9C=AC=EF=BC=8C=E5=BE=85=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.env.example | 2 +- backend/CONFIG.md | 2 +- backend/app/api/celestial_body.py | 6 +- backend/app/api/celestial_position.py | 15 +- backend/app/api/nasa_download.py | 100 +- backend/app/models/celestial.py | 23 + backend/app/models/db/celestial_body.py | 1 + backend/app/services/horizons.py | 311 ++---- backend/app/services/nasa_worker.py | 1 - backend/app/services/orbit_service.py | 1 - backend/scripts/check_db_status.py | 68 ++ backend/scripts/check_sun_data.py | 50 + backend/scripts/fix_sun_data.py | 58 ++ backend/scripts/inspect_sun.py | 39 + backend/scripts/reset_positions.py | 53 ++ backend/upload/texture/2k_saturn_ring.jpg | Bin 0 -> 70548 bytes frontend/package.json | 4 + frontend/src/App.tsx | 27 + frontend/src/components/BodyDetailOverlay.tsx | 145 +++ frontend/src/components/BodyViewer.tsx | 388 ++++++++ frontend/src/components/CelestialBody.tsx | 277 ++++-- frontend/src/components/Constellations.tsx | 98 +- frontend/src/components/FocusInfo.tsx | 17 +- frontend/src/components/Galaxies.tsx | 98 +- frontend/src/components/Nebulae.tsx | 100 +- frontend/src/components/Probe.tsx | 133 ++- frontend/src/components/ProbeList.tsx | 16 +- frontend/src/components/Scene.tsx | 26 +- frontend/src/components/Stars.tsx | 135 +-- frontend/src/hooks/useHistoricalData.ts | 30 +- frontend/src/hooks/useSpaceData.ts | 30 +- frontend/src/pages/admin/AdminLayout.tsx | 2 +- frontend/src/pages/admin/CelestialBodies.tsx | 269 ++++-- frontend/src/pages/admin/NASADownload.tsx | 339 ++++++- frontend/src/pages/admin/Tasks.tsx | 46 +- frontend/src/utils/labelTexture.ts | 71 ++ frontend/yarn.lock | 897 +++++++++++++++++- 37 files changed, 3115 insertions(+), 763 deletions(-) create mode 100644 backend/scripts/check_db_status.py create mode 100644 backend/scripts/check_sun_data.py create mode 100644 backend/scripts/fix_sun_data.py create mode 100644 backend/scripts/inspect_sun.py create mode 100644 backend/scripts/reset_positions.py create mode 100644 backend/upload/texture/2k_saturn_ring.jpg create mode 100644 frontend/src/components/BodyDetailOverlay.tsx create mode 100644 frontend/src/components/BodyViewer.tsx create mode 100644 frontend/src/utils/labelTexture.ts diff --git a/backend/.env.example b/backend/.env.example index 079edfe..e2ce5b5 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,5 +1,5 @@ # Application Settings -APP_NAME=Cosmo - Deep Space Explorer +APP_NAME=COSMO - Deep Space Explorer API_PREFIX=/api # CORS Settings (comma-separated list) diff --git a/backend/CONFIG.md b/backend/CONFIG.md index 4b3f589..9294191 100644 --- a/backend/CONFIG.md +++ b/backend/CONFIG.md @@ -51,7 +51,7 @@ REDIS_MAX_CONNECTIONS=50 # 最大连接数 ### 3. 应用配置 ```bash -APP_NAME=Cosmo - Deep Space Explorer +APP_NAME=COSMO - Deep Space Explorer API_PREFIX=/api CORS_ORIGINS=["*"] # 开发环境允许所有来源 CACHE_TTL_DAYS=3 # NASA API 缓存天数 diff --git a/backend/app/api/celestial_body.py b/backend/app/api/celestial_body.py index c0a4191..ca46c7b 100644 --- a/backend/app/api/celestial_body.py +++ b/backend/app/api/celestial_body.py @@ -25,6 +25,7 @@ class CelestialBodyCreate(BaseModel): name_zh: Optional[str] = None type: str description: Optional[str] = None + details: Optional[str] = None is_active: bool = True extra_data: Optional[Dict[str, Any]] = None @@ -34,6 +35,7 @@ class CelestialBodyUpdate(BaseModel): name_zh: Optional[str] = None type: Optional[str] = None description: Optional[str] = None + details: Optional[str] = None is_active: Optional[bool] = None extra_data: Optional[Dict[str, Any]] = None @@ -112,7 +114,7 @@ async def get_celestial_nasa_data( try: # Fetch raw text from Horizons using the body_id # Note: body.id corresponds to JPL Horizons ID - raw_text = await horizons_service.get_object_data_raw(body.id, db) + raw_text = await horizons_service.get_object_data_raw(body.id) return {"id": body.id, "name": body.name, "raw_data": raw_text} except Exception as e: logger.error(f"Failed to fetch raw data for {body_id}: {e}") @@ -173,6 +175,7 @@ async def get_body_info(body_id: str, db: AsyncSession = Depends(get_db)): name=body.name, type=body.type, description=body.description, + details=body.details, launch_date=extra_data.get("launch_date"), status=extra_data.get("status"), ) @@ -212,6 +215,7 @@ async def list_bodies( "name_zh": body.name_zh, "type": body.type, "description": body.description, + "details": body.details, "is_active": body.is_active, "resources": resources_by_type, "has_resources": len(resources) > 0, diff --git a/backend/app/api/celestial_position.py b/backend/app/api/celestial_position.py index b5de6be..5fa79be 100644 --- a/backend/app/api/celestial_position.py +++ b/backend/app/api/celestial_position.py @@ -76,7 +76,8 @@ async def get_celestial_positions( # Check Redis cache first (persistent across restarts) start_str = "now" end_str = "now" - redis_key = make_cache_key("positions", start_str, end_str, step) + body_ids_str = body_ids if body_ids else "all" + redis_key = make_cache_key("positions", start_str, end_str, step, body_ids_str) redis_cached = await redis_cache.get(redis_key) if redis_cached is not None: logger.info("Cache hit (Redis) for recent positions") @@ -194,7 +195,8 @@ async def get_celestial_positions( # Cache in Redis for persistence across restarts start_str = start_dt.isoformat() if start_dt else "now" end_str = end_dt.isoformat() if end_dt else "now" - redis_key = make_cache_key("positions", start_str, end_str, step) + body_ids_str = body_ids if body_ids else "all" + redis_key = make_cache_key("positions", start_str, end_str, step, body_ids_str) await redis_cache.set(redis_key, bodies_data, get_ttl_seconds("current_positions")) return CelestialDataResponse(bodies=bodies_data) else: @@ -204,7 +206,8 @@ async def get_celestial_positions( # Check Redis cache first (persistent across restarts) start_str = start_dt.isoformat() if start_dt else "now" end_str = end_dt.isoformat() if end_dt else "now" - redis_key = make_cache_key("positions", start_str, end_str, step) + body_ids_str = body_ids if body_ids else "all" # Include body_ids in cache key + redis_key = make_cache_key("positions", start_str, end_str, step, body_ids_str) redis_cached = await redis_cache.get(redis_key) if redis_cached is not None: logger.info("Cache hit (Redis) for positions") @@ -222,7 +225,9 @@ async def get_celestial_positions( # Filter bodies if body_ids specified if body_id_list: + logger.info(f"Filtering bodies from {len(all_bodies)} total. Requested IDs: {body_id_list}") all_bodies = [b for b in all_bodies if b.id in body_id_list] + logger.info(f"After filtering: {len(all_bodies)} bodies. IDs: {[b.id for b in all_bodies]}") use_db_cache = True db_cached_bodies = [] @@ -334,7 +339,7 @@ async def get_celestial_positions( # Special handling for Cassini (mission ended 2017-09-15) elif body.id == "-82": cassini_date = datetime(2017, 9, 15, 11, 58, 0) - pos_data = horizons_service.get_body_positions(body.id, cassini_date, cassini_date, step) + pos_data = await horizons_service.get_body_positions(body.id, cassini_date, cassini_date, step) positions_list = [ {"time": p.time.isoformat(), "x": p.x, "y": p.y, "z": p.z} for p in pos_data @@ -342,7 +347,7 @@ async def get_celestial_positions( else: # Download from NASA Horizons - pos_data = await horizons_service.get_body_positions(body.id, db, start_dt, end_dt, step) + pos_data = await horizons_service.get_body_positions(body.id, start_dt, end_dt, step) positions_list = [ {"time": p.time.isoformat(), "x": p.x, "y": p.y, "z": p.z} for p in pos_data diff --git a/backend/app/api/nasa_download.py b/backend/app/api/nasa_download.py index ee2c734..86d87e0 100644 --- a/backend/app/api/nasa_download.py +++ b/backend/app/api/nasa_download.py @@ -217,15 +217,16 @@ async def download_positions( continue # Download from NASA Horizons + logger.info(f"Downloading position for body {body_id} on {date_str}") positions = await horizons_service.get_body_positions( body_id=body_id, - db=db, start_time=target_date, end_time=target_date, step="1d" ) if positions and len(positions) > 0: + logger.info(f"Received position data for body {body_id}: x={positions[0].x}, y={positions[0].y}, z={positions[0].z}") # Save to database position_data = [{ "time": target_date, @@ -243,6 +244,17 @@ async def download_positions( source="nasa_horizons", session=db ) + logger.info(f"Saved position for body {body_id} on {date_str}") + + # Invalidate caches for this date to ensure fresh data is served + from app.services.redis_cache import redis_cache, make_cache_key + start_str = target_date.isoformat() + end_str = target_date.isoformat() + # Clear both "all bodies" cache and specific body cache + for body_ids_str in ["all", body_id]: + redis_key = make_cache_key("positions", start_str, end_str, "1d", body_ids_str) + await redis_cache.delete(redis_key) + logger.debug(f"Invalidated cache: {redis_key}") body_results["dates"].append({ "date": date_str, @@ -283,3 +295,89 @@ async def download_positions( except Exception as e: logger.error(f"Download failed: {e}") raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/delete") +async def delete_positions( + request: DownloadPositionRequest, + db: AsyncSession = Depends(get_db) +): + """ + Delete position data for specified bodies on specified dates + + Args: + - body_ids: List of celestial body IDs + - dates: List of dates (YYYY-MM-DD format) + + Returns: + - Summary of deleted data + """ + logger.info(f"Deleting positions for {len(request.body_ids)} bodies on {len(request.dates)} dates") + + try: + total_deleted = 0 + from sqlalchemy import text + + for body_id in request.body_ids: + # Invalidate caches for this body + from app.services.redis_cache import redis_cache, make_cache_key + + # We need to loop dates to delete specific records + for date_str in request.dates: + try: + # Parse date + target_date = datetime.strptime(date_str, "%Y-%m-%d") + # End of day + end_of_day = target_date.replace(hour=23, minute=59, second=59, microsecond=999999) + + # Execute deletion + # Using text() for raw SQL is often simpler for range deletes, + # but ORM is safer. Let's use ORM with execute. + # But since position_service might not have delete, we do it here. + + stmt = text(""" + DELETE FROM positions + WHERE body_id = :body_id + AND time >= :start_time + AND time <= :end_time + """) + + result = await db.execute(stmt, { + "body_id": body_id, + "start_time": target_date, + "end_time": end_of_day + }) + + deleted_count = result.rowcount + total_deleted += deleted_count + + if deleted_count > 0: + logger.info(f"Deleted {deleted_count} records for {body_id} on {date_str}") + + # Invalidate cache for this specific date/body combo + # Note: This is approximate as cache keys might cover ranges + start_str = target_date.isoformat() + end_str = target_date.isoformat() + # Clear both "all bodies" cache and specific body cache + for body_ids_str in ["all", body_id]: + # We try to clear '1d' step cache + redis_key = make_cache_key("positions", start_str, end_str, "1d", body_ids_str) + await redis_cache.delete(redis_key) + + except Exception as e: + logger.error(f"Failed to delete data for {body_id} on {date_str}: {e}") + + await db.commit() + + # Clear general patterns to be safe if ranges were cached + await redis_cache.clear_pattern("positions:*") + + return { + "message": f"Successfully deleted {total_deleted} position records", + "total_deleted": total_deleted + } + + except Exception as e: + await db.rollback() + logger.error(f"Delete failed: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/models/celestial.py b/backend/app/models/celestial.py index e6ca46c..e6d590f 100644 --- a/backend/app/models/celestial.py +++ b/backend/app/models/celestial.py @@ -45,6 +45,7 @@ class BodyInfo(BaseModel): name: str type: Literal["planet", "probe", "star", "dwarf_planet", "satellite", "comet"] description: str + details: str | None = None launch_date: str | None = None status: str | None = None @@ -200,4 +201,26 @@ CELESTIAL_BODIES = { "type": "dwarf_planet", "description": "鸟神星,柯伊伯带中第二亮的天体", }, + # Comets / Interstellar Objects + "1I": { + "name": "1I/'Oumuamua", + "name_zh": "奥陌陌", + "type": "comet", + "description": "原定名 1I/2017 U1,是已知第一颗经过太阳系的星际天体。它于2017年10月18日(UT)在距离地球约0.2 AU(30,000,000 km;19,000,000 mi)处被泛星1号望远镜发现,并在极端双曲线的轨道上运行。", + "status": "active", + }, + "3I": { + "name": "3I/ATLAS", + "name_zh": "3I/ATLAS", + "type": "comet", + "description": "又称C/2025 N1 (ATLAS),是一颗星际彗星,由位于智利里奥乌尔塔多的小行星陆地撞击持续报警系统于2025年7月1日发现", + "status": "active", + }, + "90000030": { + "name": "1P/Halley", + "name_zh": "哈雷彗星", + "type": "comet", + "description": "哈雷彗星(正式名称为1P/Halley)是著名的短周期彗星,每隔75-76年就能从地球上被观测到[5],亦是唯一能用肉眼直接从地球看到的短周期彗星,人的一生中可能经历两次其来访。", + "status": "active", + }, } diff --git a/backend/app/models/db/celestial_body.py b/backend/app/models/db/celestial_body.py index 12d343f..266178b 100644 --- a/backend/app/models/db/celestial_body.py +++ b/backend/app/models/db/celestial_body.py @@ -18,6 +18,7 @@ class CelestialBody(Base): name_zh = Column(String(200), nullable=True, comment="Chinese name") type = Column(String(50), nullable=False, comment="Body type") description = Column(Text, nullable=True, comment="Description") + details = Column(Text, nullable=True, comment="Detailed description (Markdown)") is_active = Column(Boolean, nullable=True, comment="Active status for probes (True=active, False=inactive)") extra_data = Column(JSONB, nullable=True, comment="Extended metadata (JSON)") created_at = Column(TIMESTAMP, server_default=func.now()) diff --git a/backend/app/services/horizons.py b/backend/app/services/horizons.py index 7d09574..89fd320 100644 --- a/backend/app/services/horizons.py +++ b/backend/app/services/horizons.py @@ -91,26 +91,23 @@ class HorizonsService: if end_time is None: end_time = start_time - # Format time for Horizons (YYYY-MM-DD HH:MM) - # Horizons accepts ISO-like format without 'T' - start_str = start_time.strftime('%Y-%m-%d %H:%M') - end_str = end_time.strftime('%Y-%m-%d %H:%M') - - # Special case for single point query (start = end) - # Horizons requires START != STOP for ranges, but we can handle single point - # by making a very small range or just asking for 1 step. - # Actually Horizons API is fine with start=end if we don't ask for range? - # Let's keep using range parameters as standard. - if start_time == end_time: - # Just add 1 minute for range, but we only parse the first result - end_dummy = end_time + timedelta(minutes=1) - end_str = end_dummy.strftime('%Y-%m-%d %H:%M') - # Override step to ensure we get the start point - # But wait, '1d' step might skip. - # If start==end, we want exactly one point. - # We can't use '1' count in API easily via URL params without STEP_SIZE? - # Let's just use the provided step. - + # Format time for Horizons + # NASA Horizons accepts: 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' + # When querying a single point (same start/end date), we need STOP > START + # So we add 1 second and use precise time format + + if start_time.date() == end_time.date(): + # Single day query - use the date at 00:00 and next second + start_str = start_time.strftime('%Y-%m-%d') + # For STOP, add 1 day to satisfy STOP > START requirement + # But use step='1d' so we only get one data point + end_time_adjusted = start_time + timedelta(days=1) + end_str = end_time_adjusted.strftime('%Y-%m-%d') + else: + # Multi-day range query + start_str = start_time.strftime('%Y-%m-%d') + end_str = end_time.strftime('%Y-%m-%d') + logger.info(f"Querying Horizons (httpx) for body {body_id} from {start_str} to {end_str}") url = "https://ssd.jpl.nasa.gov/api/horizons.api" @@ -126,223 +123,16 @@ class HorizonsService: "START_TIME": start_str, "STOP_TIME": end_str, "STEP_SIZE": step, - "CSV_FORMAT": "YES" + "CSV_FORMAT": "YES", + "OUT_UNITS": "AU-D" } - # Configure proxy if available - client_kwargs = {"timeout": settings.nasa_api_timeout} - if settings.proxy_dict: - client_kwargs["proxies"] = settings.proxy_dict - - async with httpx.AsyncClient(**client_kwargs) as client: - response = await client.get(url, params=params) - - if response.status_code != 200: - raise Exception(f"NASA API returned status {response.status_code}") - - return self._parse_vectors(response.text) - - except Exception as e: - logger.error(f"Error querying Horizons for body {body_id}: {str(e)}") - raise - - def _parse_vectors(self, text: str) -> list[Position]: - """ - Parse Horizons CSV output for vector data - - Format looks like: - $$SOE - 2460676.500000000, A.D. 2025-Jan-01 00:00:00.0000, 9.776737278236609E-01, -1.726677228793678E-01, -1.636678733289160E-05, ... - $$EOE - """ - positions = [] - - # Extract data block between $$SOE and $$EOE - match = re.search(r'\$\$SOE(.*?)\$\$EOE', text, re.DOTALL) - if not match: - logger.warning("No data block ($$SOE...$$EOE) found in Horizons response") - # Log a snippet of text for debugging - logger.debug(f"Response snippet: {text[:200]}...") - return [] - - data_block = match.group(1).strip() - lines = data_block.split('\n') - - for line in lines: - parts = [p.strip() for p in line.split(',')] - if len(parts) < 5: - continue - - try: - # Index 0: JD, 1: Date, 2: X, 3: Y, 4: Z, 5: VX, 6: VY, 7: VZ - # Time parsing: 2460676.500000000 is JD. - # A.D. 2025-Jan-01 00:00:00.0000 is Calendar. - # We can use JD or parse the string. Using JD via astropy is accurate. - - jd_str = parts[0] - time_obj = Time(float(jd_str), format="jd").datetime - - x = float(parts[2]) - y = float(parts[3]) - z = float(parts[4]) - - # Velocity if available (indices 5, 6, 7) - vx = float(parts[5]) if len(parts) > 5 else None - vy = float(parts[6]) if len(parts) > 6 else None - vz = float(parts[7]) if len(parts) > 7 else None - - pos = Position( - time=time_obj, - x=x, - y=y, - z=z, - vx=vx, - vy=vy, - vz=vz - ) - positions.append(pos) - except ValueError as e: - logger.warning(f"Failed to parse line: {line}. Error: {e}") - continue - - return positions - -""" -NASA JPL Horizons data query service -""" -from datetime import datetime, timedelta -# from astroquery.jplhorizons import Horizons # Removed astroquery dependency -from astropy.time import Time # Kept astropy for Time object -import logging -import re -import httpx -import os - -from app.models.celestial import Position, CelestialBody -from app.config import settings - -logger = logging.getLogger(__name__) - - -class HorizonsService: - """Service for querying NASA JPL Horizons system""" - - def __init__(self): - """Initialize the service""" - self.location = "@sun" # Heliocentric coordinates - # Proxy is handled via settings.proxy_dict in each request - - async def get_object_data_raw(self, body_id: str) -> str: - """ - Get raw object data (terminal style text) from Horizons - - Args: - body_id: JPL Horizons ID - - Returns: - Raw text response from NASA - """ - 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, - "OBJ_DATA": "YES", - "MAKE_EPHEM": "NO", - "EPHEM_TYPE": "VECTORS", - "CENTER": "@sun" - } - - try: # Configure proxy if available client_kwargs = {"timeout": settings.nasa_api_timeout} 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} with timeout {settings.nasa_api_timeout}s") - response = await client.get(url, params=params) - - if response.status_code != 200: - raise Exception(f"NASA API returned status {response.status_code}") - - return response.text - except Exception as e: - logger.error(f"Error fetching raw data for {body_id}: {str(e)}") - raise - - async def get_body_positions( - self, - body_id: str, - start_time: datetime | None = None, - end_time: datetime | None = None, - step: str = "1d", - ) -> list[Position]: - """ - Get positions for a celestial body over a time range - - Args: - body_id: JPL Horizons ID (e.g., '-31' for Voyager 1) - start_time: Start datetime (default: now) - end_time: End datetime (default: now) - step: Time step (e.g., '1d' for 1 day, '1h' for 1 hour) - - Returns: - List of Position objects - """ - try: - # Set default times - if start_time is None: - start_time = datetime.utcnow() - if end_time is None: - end_time = start_time - - # Format time for Horizons (YYYY-MM-DD HH:MM) - # Horizons accepts ISO-like format without 'T' - start_str = start_time.strftime('%Y-%m-%d %H:%M') - end_str = end_time.strftime('%Y-%m-%d %H:%M') - - # Special case for single point query (start = end) - # Horizons requires START != STOP for ranges, but we can handle single point - # by making a very small range or just asking for 1 step. - # Actually Horizons API is fine with start=end if we don't ask for range? - # Let's keep using range parameters as standard. - if start_time == end_time: - # Just add 1 minute for range, but we only parse the first result - end_dummy = end_time + timedelta(minutes=1) - end_str = end_dummy.strftime('%Y-%m-%d %H:%M') - # Override step to ensure we get the start point - # But wait, '1d' step might skip. - # If start==end, we want exactly one point. - # We can't use '1' count in API easily via URL params without STEP_SIZE? - # Let's just use the provided step. - - logger.info(f"Querying Horizons (httpx) for body {body_id} from {start_str} to {end_str}") - - url = "https://ssd.jpl.nasa.gov/api/horizons.api" - cmd_val = f"'{body_id}'" if not body_id.startswith("'") else body_id - - params = { - "format": "text", - "COMMAND": cmd_val, - "OBJ_DATA": "NO", - "MAKE_EPHEM": "YES", - "EPHEM_TYPE": "VECTORS", - "CENTER": self.location, - "START_TIME": start_str, - "STOP_TIME": end_str, - "STEP_SIZE": step, - "CSV_FORMAT": "YES" - } - - # Configure proxy if available - client_kwargs = {"timeout": settings.nasa_api_timeout} - if settings.proxy_dict: - client_kwargs["proxies"] = settings.proxy_dict - async with httpx.AsyncClient(**client_kwargs) as client: response = await client.get(url, params=params) @@ -358,20 +148,20 @@ class HorizonsService: def _parse_vectors(self, text: str) -> list[Position]: """ Parse Horizons CSV output for vector data - + Format looks like: $$SOE 2460676.500000000, A.D. 2025-Jan-01 00:00:00.0000, 9.776737278236609E-01, -1.726677228793678E-01, -1.636678733289160E-05, ... $$EOE """ positions = [] - + # Extract data block between $$SOE and $$EOE match = re.search(r'\$\$SOE(.*?)\$\$EOE', text, re.DOTALL) if not match: logger.warning("No data block ($$SOE...$$EOE) found in Horizons response") - # Log a snippet of text for debugging - logger.debug(f"Response snippet: {text[:200]}...") + # Log full response for debugging + logger.info(f"Full response for debugging:\n{text}") return [] data_block = match.group(1).strip() @@ -413,7 +203,7 @@ class HorizonsService: except ValueError as e: logger.warning(f"Failed to parse line: {line}. Error: {e}") continue - + return positions async def search_body_by_name(self, name: str, db: AsyncSession) -> dict: @@ -436,10 +226,11 @@ class HorizonsService: "CENTER": "@ssb" # Search from Solar System Barycenter for consistent object IDs } - timeout = await self._get_timeout(db) + timeout = settings.nasa_api_timeout client_kwargs = {"timeout": timeout} 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: response = await client.get(url, params=params) @@ -449,6 +240,9 @@ class HorizonsService: response_text = response.text + # Log full response for debugging (temporarily) + logger.info(f"Full NASA API response for '{name}':\n{response_text}") + # Check for "Ambiguous target name" if "Ambiguous target name" in response_text: logger.warning(f"Ambiguous target name for: {name}") @@ -470,14 +264,53 @@ class HorizonsService: "error": "未找到匹配的天体,请检查名称或 ID" } - # Parse canonical name and ID from response (e.g., "Target body name: Jupiter Barycenter (599)") - target_name_match = re.search(r"Target body name: (.+?)\s+\((\-?\d+)\)", response_text) + # Try multiple parsing patterns for different response formats + # Pattern 1: "Target body name: Jupiter Barycenter (599)" + target_name_match = re.search(r"Target body name:\s*(.+?)\s+\((\-?\d+)\)", response_text) + + if not target_name_match: + # Pattern 2: " Revised: Mar 12, 2021 Ganymede / (Jupiter) 503" + # This pattern appears in the header section of many bodies + revised_match = re.search(r"Revised:.*?\s{2,}(.+?)\s{2,}(\-?\d+)\s*$", response_text, re.MULTILINE) + if revised_match: + full_name = revised_match.group(1).strip() + numeric_id = revised_match.group(2).strip() + short_name = full_name.split('/')[0].strip() # Remove parent body info like "/ (Jupiter)" + + logger.info(f"Found target (pattern 2): {full_name} with ID: {numeric_id}") + return { + "success": True, + "id": numeric_id, + "name": short_name, + "full_name": full_name, + "error": None + } + + if not target_name_match: + # Pattern 3: Look for body name in title section (works for comets and other objects) + # Example: "JPL/HORIZONS ATLAS (C/2025 N1) 2025-Dec-" + title_match = re.search(r"JPL/HORIZONS\s+(.+?)\s{2,}", response_text) + if title_match: + full_name = title_match.group(1).strip() + # For this pattern, the ID was in the original COMMAND, use it + numeric_id = name.strip("'\"") + short_name = full_name.split('(')[0].strip() + + logger.info(f"Found target (pattern 3): {full_name} with ID: {numeric_id}") + return { + "success": True, + "id": numeric_id, + "name": short_name, + "full_name": full_name, + "error": None + } + if target_name_match: full_name = target_name_match.group(1).strip() numeric_id = target_name_match.group(2).strip() short_name = full_name.split('(')[0].strip() # Remove any part after '(' - logger.info(f"Found target: {full_name} with ID: {numeric_id}") + logger.info(f"Found target (pattern 1): {full_name} with ID: {numeric_id}") return { "success": True, "id": numeric_id, @@ -487,7 +320,7 @@ class HorizonsService: } else: # Fallback if specific pattern not found, might be a valid but weird response - logger.warning(f"Could not parse target name/ID from response for: {name}. Response snippet: {response_text[:200]}") + logger.warning(f"Could not parse target name/ID from response for: {name}. Response snippet: {response_text[:500]}") return { "success": False, "id": None, diff --git a/backend/app/services/nasa_worker.py b/backend/app/services/nasa_worker.py index 088249e..6c62b42 100644 --- a/backend/app/services/nasa_worker.py +++ b/backend/app/services/nasa_worker.py @@ -62,7 +62,6 @@ async def download_positions_task(task_id: int, body_ids: List[str], dates: List # Download positions = await horizons_service.get_body_positions( body_id=body_id, - db=db, start_time=target_date, end_time=target_date, step="1d" diff --git a/backend/app/services/orbit_service.py b/backend/app/services/orbit_service.py index 09094a2..ed03798 100644 --- a/backend/app/services/orbit_service.py +++ b/backend/app/services/orbit_service.py @@ -152,7 +152,6 @@ class OrbitService: # Get positions from Horizons (synchronous call) positions = await horizons_service.get_body_positions( body_id=body_id, - db=session, start_time=start_time, end_time=end_time, step=f"{step_days}d" diff --git a/backend/scripts/check_db_status.py b/backend/scripts/check_db_status.py new file mode 100644 index 0000000..6918b6f --- /dev/null +++ b/backend/scripts/check_db_status.py @@ -0,0 +1,68 @@ +""" +Check database status: bodies, positions, resources +""" +import asyncio +import os +import sys +from datetime import datetime + +# Add backend directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from app.database import get_db +from app.models.db.celestial_body import CelestialBody +from app.models.db.position import Position +from app.models.db.resource import Resource +from sqlalchemy import select, func + +async def check_status(): + """Check database status""" + print("🔍 Checking database status...") + + async for session in get_db(): + try: + # 1. Check Celestial Bodies + stmt = select(func.count(CelestialBody.id)) + result = await session.execute(stmt) + body_count = result.scalar() + print(f"✅ Celestial Bodies: {body_count}") + + # 2. Check Positions + stmt = select(func.count(Position.id)) + result = await session.execute(stmt) + position_count = result.scalar() + print(f"✅ Total Positions: {position_count}") + + # Check positions for Sun (10) and Earth (399) + for body_id in ['10', '399']: + stmt = select(func.count(Position.id)).where(Position.body_id == body_id) + result = await session.execute(stmt) + count = result.scalar() + print(f" - Positions for {body_id}: {count}") + + if count > 0: + # Get latest position date + stmt = select(func.max(Position.time)).where(Position.body_id == body_id) + result = await session.execute(stmt) + latest_date = result.scalar() + print(f" Latest date: {latest_date}") + + # 3. Check Resources + stmt = select(func.count(Resource.id)) + result = await session.execute(stmt) + resource_count = result.scalar() + print(f"✅ Total Resources: {resource_count}") + + # Check resources for Sun (10) + stmt = select(Resource).where(Resource.body_id == '10') + result = await session.execute(stmt) + resources = result.scalars().all() + print(f" - Resources for Sun (10): {len(resources)}") + for r in resources: + print(f" - {r.resource_type}: {r.file_path}") + + finally: + break + +if __name__ == "__main__": + asyncio.run(check_status()) diff --git a/backend/scripts/check_sun_data.py b/backend/scripts/check_sun_data.py new file mode 100644 index 0000000..851c4b8 --- /dev/null +++ b/backend/scripts/check_sun_data.py @@ -0,0 +1,50 @@ +import asyncio +import os +import sys +from datetime import datetime + +# Add backend directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from app.database import get_db +from app.models.db import Position +from sqlalchemy import select, func + +async def check_sun_data(): + """Check data for 2025-12-04 00:00:00""" + async for session in get_db(): + try: + target_time = datetime(2025, 12, 4, 0, 0, 0) + print(f"Checking data for all bodies at {target_time}...") + + # Get all bodies + from app.models.db.celestial_body import CelestialBody + stmt = select(CelestialBody.id, CelestialBody.name, CelestialBody.type).where(CelestialBody.is_active != False) + result = await session.execute(stmt) + all_bodies = result.all() + print(f"Total active bodies: {len(all_bodies)}") + + # Check positions for each + missing_bodies = [] + for body_id, body_name, body_type in all_bodies: + stmt = select(func.count(Position.id)).where( + Position.body_id == body_id, + Position.time == target_time + ) + result = await session.execute(stmt) + count = result.scalar() + if count == 0: + missing_bodies.append(f"{body_name} ({body_id}) [{body_type}]") + + if missing_bodies: + print(f"❌ Missing data for {len(missing_bodies)} bodies:") + for b in missing_bodies: + print(f" - {b}") + else: + print("✅ All active bodies have data for this time!") + + finally: + break + +if __name__ == "__main__": + asyncio.run(check_sun_data()) diff --git a/backend/scripts/fix_sun_data.py b/backend/scripts/fix_sun_data.py new file mode 100644 index 0000000..d44ce6e --- /dev/null +++ b/backend/scripts/fix_sun_data.py @@ -0,0 +1,58 @@ +""" +Fix missing Sun position +""" +import asyncio +import os +import sys +from datetime import datetime + +# Add backend directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from app.database import get_db +from app.models.db import Position + +async def fix_sun_position(): + """Insert missing position for Sun at 2025-12-04 00:00:00""" + async for session in get_db(): + try: + target_time = datetime(2025, 12, 4, 0, 0, 0) + print(f"Fixing Sun position for {target_time}...") + + # Check if it exists first (double check) + from sqlalchemy import select, func + stmt = select(func.count(Position.id)).where( + Position.body_id == '10', + Position.time == target_time + ) + result = await session.execute(stmt) + count = result.scalar() + + if count > 0: + print("✅ Position already exists!") + return + + # Insert + new_pos = Position( + body_id='10', + time=target_time, + x=0.0, + y=0.0, + z=0.0, + vx=0.0, + vy=0.0, + vz=0.0, + source='calculated' + ) + session.add(new_pos) + await session.commit() + print("✅ Successfully inserted Sun position!") + + except Exception as e: + print(f"❌ Error: {e}") + await session.rollback() + finally: + break + +if __name__ == "__main__": + asyncio.run(fix_sun_position()) diff --git a/backend/scripts/inspect_sun.py b/backend/scripts/inspect_sun.py new file mode 100644 index 0000000..039477c --- /dev/null +++ b/backend/scripts/inspect_sun.py @@ -0,0 +1,39 @@ +import asyncio +import os +import sys +from sqlalchemy import select +from datetime import datetime + +# Add backend directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from app.database import get_db +from app.models.db import Position + +async def inspect_sun_positions(): + async for session in get_db(): + try: + # List all positions for Sun + stmt = select(Position.time).where(Position.body_id == '10').order_by(Position.time.desc()).limit(10) + result = await session.execute(stmt) + times = result.scalars().all() + + print("Recent Sun positions:") + for t in times: + print(f" - {t} (type: {type(t)})") + + # Check specifically for 2025-12-04 + target = datetime(2025, 12, 4, 0, 0, 0) + stmt = select(Position).where( + Position.body_id == '10', + Position.time == target + ) + result = await session.execute(stmt) + pos = result.scalar() + print(f"\nExact match for {target}: {pos}") + + finally: + break + +if __name__ == "__main__": + asyncio.run(inspect_sun_positions()) diff --git a/backend/scripts/reset_positions.py b/backend/scripts/reset_positions.py new file mode 100644 index 0000000..3a917cb --- /dev/null +++ b/backend/scripts/reset_positions.py @@ -0,0 +1,53 @@ +""" +Reset position data to fix units (KM -> AU) +""" +import asyncio +import os +import sys + +# Add backend directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from app.database import get_db +from app.models.db import Position +from app.services.redis_cache import redis_cache +from sqlalchemy import text + +async def reset_data(): + """Clear positions and cache to force re-fetch in AU""" + print("🧹 Clearing old data (KM) to prepare for AU...") + + async for session in get_db(): + try: + # Clear positions table + print(" Truncating positions table...") + await session.execute(text("TRUNCATE TABLE positions RESTART IDENTITY CASCADE")) + + # Clear nasa_cache table (if it exists as a table, or if it's just redis?) + # nasa_cache is in db models? + # Let's check models/db directory... + # It seems nasa_cache is a table based on `nasa_cache_service`. + print(" Truncating nasa_cache table...") + try: + await session.execute(text("TRUNCATE TABLE nasa_cache RESTART IDENTITY CASCADE")) + except Exception as e: + print(f" (Note: nasa_cache might not exist or failed: {e})") + + await session.commit() + print("✅ Database tables cleared.") + + # Clear Redis + await redis_cache.connect() + await redis_cache.clear_pattern("positions:*") + await redis_cache.clear_pattern("nasa:*") + print("✅ Redis cache cleared.") + await redis_cache.disconnect() + + except Exception as e: + print(f"❌ Error: {e}") + await session.rollback() + finally: + break + +if __name__ == "__main__": + asyncio.run(reset_data()) diff --git a/backend/upload/texture/2k_saturn_ring.jpg b/backend/upload/texture/2k_saturn_ring.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cc9de41988376282d38e74a367f98936d4defb3d GIT binary patch literal 70548 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 literal 0 HcmV?d00001 diff --git a/frontend/package.json b/frontend/package.json index 03f7026..08bcc4f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,9 +18,13 @@ "axios": "^1.13.2", "html2canvas": "^1.4.1", "lucide-react": "^0.555.0", + "markdown-it": "^14.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-markdown": "^10.1.0", + "react-markdown-editor-lite": "^1.3.4", "react-router-dom": "^7.9.6", + "remark-gfm": "^4.0.1", "three": "^0.181.2" }, "devDependencies": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fa38102..7e634be 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,6 +14,7 @@ import { InterstellarTicker } from './components/InterstellarTicker'; import { ControlPanel } from './components/ControlPanel'; import { AuthModal } from './components/AuthModal'; import { MessageBoard } from './components/MessageBoard'; +import { BodyDetailOverlay } from './components/BodyDetailOverlay'; // Import the new overlay component import { auth } from './utils/auth'; import type { CelestialBody } from './types'; import { useToast } from './contexts/ToastContext'; @@ -32,6 +33,7 @@ function App() { const [showOrbits, setShowOrbits] = useState(true); const [isSoundOn, setIsSoundOn] = useState(false); const [showMessageBoard, setShowMessageBoard] = useState(false); + const [showDetailOverlayId, setShowDetailOverlayId] = useState(null); // State for detail overlay // Initialize state from localStorage useEffect(() => { @@ -75,6 +77,19 @@ function App() { const loading = isTimelineMode ? historicalLoading : realTimeLoading; const error = isTimelineMode ? historicalError : realTimeError; + // Debug: log bodies when they change + useEffect(() => { + console.log('[App] Bodies updated:', { + isTimelineMode, + totalBodies: bodies.length, + bodiesWithPositions: bodies.filter(b => b.positions && b.positions.length > 0).length, + bodyTypes: bodies.reduce((acc, b) => { + acc[b.type] = (acc[b.type] || 0) + 1; + return acc; + }, {} as Record) + }); + }, [bodies, isTimelineMode]); + const [selectedBody, setSelectedBody] = useState(null); const { trajectoryPositions } = useTrajectory(selectedBody); @@ -94,6 +109,11 @@ function App() { } }, [isTimelineMode, cutoffDate]); + // Handle viewing body details + const handleViewDetails = useCallback((body: CelestialBody) => { + setShowDetailOverlayId(body.id); + }, []); + // Filter probes and planets from all bodies const probes = bodies.filter((b) => b.type === 'probe'); const planets = bodies.filter((b) => @@ -213,6 +233,7 @@ function App() { onBodySelect={handleBodySelect} resetTrigger={resetTrigger} toast={toast} + onViewDetails={handleViewDetails} /> {/* Timeline Controller */} @@ -241,6 +262,12 @@ function App() { )} + + {/* Body Detail Overlay */} + setShowDetailOverlayId(null)} + /> ); } diff --git a/frontend/src/components/BodyDetailOverlay.tsx b/frontend/src/components/BodyDetailOverlay.tsx new file mode 100644 index 0000000..fe86cef --- /dev/null +++ b/frontend/src/components/BodyDetailOverlay.tsx @@ -0,0 +1,145 @@ +import { useRef, useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { XCircle } from 'lucide-react'; +import { Canvas, useFrame } from '@react-three/fiber'; +import { OrbitControls } from '@react-three/drei'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; + +import { request } from '../utils/request'; +import { useToast } from '../contexts/ToastContext'; +import { BodyViewer } from './BodyViewer'; +import type { CelestialBody as CelestialBodyType } from '../types'; + +interface BodyDetailOverlayProps { + bodyId: string | null; + onClose: () => void; +} + +// Custom camera control for automatic rotation +function AutoRotateCamera() { + useFrame((state) => { + state.camera.position.x = Math.sin(state.clock.elapsedTime * 0.1) * 3; + state.camera.position.z = Math.cos(state.clock.elapsedTime * 0.1) * 3; + state.camera.lookAt(0, 0, 0); + }); + return null; +} + + +export function BodyDetailOverlay({ bodyId, onClose }: BodyDetailOverlayProps) { + const [bodyData, setBodyData] = useState(null); + const [loading, setLoading] = useState(false); + const toast = useToast(); + + useEffect(() => { + if (!bodyId) { + setBodyData(null); + return; + } + + setLoading(true); + request.get(`/celestial/info/${bodyId}`) + .then(response => { + setBodyData(response.data); + }) + .catch(error => { + console.error("Failed to fetch body details:", error); + toast.error("加载天体详情失败"); + onClose(); // Close overlay on error + }) + .finally(() => { + setLoading(false); + }); + }, [bodyId, onClose, toast]); + + if (!bodyId || !bodyData) { + return null; + } + + // Create portal to render outside the main app div + return createPortal( +
+
+ {/* Close Button */} + + + {/* Left Panel: 3D Viewer */} +
+ {loading ? ( +
加载中...
+ ) : ( + + + + + {/* Frontal light */} + {/* Back light */} + + {/* Auto rotate for presentation */} + + + + + )} +
+ + {/* Right Panel: Details */} +
+

{bodyData.name_zh || bodyData.name}

+

{bodyData.name}

+ +
+ 类型: {bodyData.type} + {bodyData.description && <> | {bodyData.description}} +
+ + {bodyData.details ? ( +
+

, + h2: ({node, ...props}) =>

, + h3: ({node, ...props}) =>

, + p: ({node, ...props}) =>

, + ul: ({node, ...props}) =>