238 lines
7.8 KiB
Python
238 lines
7.8 KiB
Python
|
|
"""
|
|||
|
|
Predefined Scheduled Tasks
|
|||
|
|
All registered tasks for scheduled execution
|
|||
|
|
"""
|
|||
|
|
import logging
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
from typing import Dict, Any, List, Optional
|
|||
|
|
from sqlalchemy import select
|
|||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
|
from sqlalchemy.dialects.postgresql import insert
|
|||
|
|
|
|||
|
|
from app.jobs.registry import task_registry
|
|||
|
|
from app.models.db.celestial_body import CelestialBody
|
|||
|
|
from app.models.db.position import Position
|
|||
|
|
from app.services.horizons import HorizonsService
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@task_registry.register(
|
|||
|
|
name="sync_solar_system_positions",
|
|||
|
|
description="同步太阳系天体位置数据,从NASA Horizons API获取指定天体的位置数据并保存到数据库",
|
|||
|
|
category="data_sync",
|
|||
|
|
parameters=[
|
|||
|
|
{
|
|||
|
|
"name": "body_ids",
|
|||
|
|
"type": "array",
|
|||
|
|
"description": "要同步的天体ID列表,例如['10', '199', '299']。如果不指定,则同步所有活跃的太阳系天体",
|
|||
|
|
"required": False,
|
|||
|
|
"default": None
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"name": "days",
|
|||
|
|
"type": "integer",
|
|||
|
|
"description": "同步天数,从今天开始向未来延伸的天数",
|
|||
|
|
"required": False,
|
|||
|
|
"default": 7
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"name": "source",
|
|||
|
|
"type": "string",
|
|||
|
|
"description": "数据源标记,用于标识数据来源",
|
|||
|
|
"required": False,
|
|||
|
|
"default": "nasa_horizons_cron"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
)
|
|||
|
|
async def sync_solar_system_positions(
|
|||
|
|
db: AsyncSession,
|
|||
|
|
logger: logging.Logger,
|
|||
|
|
params: Dict[str, Any]
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
"""
|
|||
|
|
Sync solar system body positions from NASA Horizons
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
db: Database session
|
|||
|
|
logger: Logger instance
|
|||
|
|
params: Task parameters
|
|||
|
|
- body_ids: List of body IDs to sync (optional, defaults to all active)
|
|||
|
|
- days: Number of days to sync (default: 7)
|
|||
|
|
- source: Source tag for the data (default: "nasa_horizons_cron")
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Summary of sync operation
|
|||
|
|
"""
|
|||
|
|
body_ids = params.get("body_ids")
|
|||
|
|
days = params.get("days", 7)
|
|||
|
|
source = params.get("source", "nasa_horizons_cron")
|
|||
|
|
|
|||
|
|
logger.info(f"Starting solar system position sync: days={days}, source={source}")
|
|||
|
|
|
|||
|
|
# Get list of bodies to sync
|
|||
|
|
if body_ids:
|
|||
|
|
# Use specified body IDs
|
|||
|
|
result = await db.execute(
|
|||
|
|
select(CelestialBody).where(
|
|||
|
|
CelestialBody.id.in_(body_ids),
|
|||
|
|
CelestialBody.is_active == True
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
bodies = result.scalars().all()
|
|||
|
|
logger.info(f"Syncing {len(bodies)} specified bodies")
|
|||
|
|
else:
|
|||
|
|
# Get all active solar system bodies
|
|||
|
|
# Typically solar system bodies include planets, dwarf planets, and major satellites
|
|||
|
|
result = await db.execute(
|
|||
|
|
select(CelestialBody).where(
|
|||
|
|
CelestialBody.is_active == True,
|
|||
|
|
CelestialBody.system_id == 1,
|
|||
|
|
CelestialBody.type.in_(['planet', 'dwarf_planet', 'satellite'])
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
bodies = result.scalars().all()
|
|||
|
|
logger.info(f"Syncing all {len(bodies)} active solar system bodies")
|
|||
|
|
|
|||
|
|
if not bodies:
|
|||
|
|
logger.warning("No bodies found to sync")
|
|||
|
|
return {
|
|||
|
|
"success": True,
|
|||
|
|
"bodies_synced": 0,
|
|||
|
|
"total_positions": 0,
|
|||
|
|
"message": "No bodies found"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Initialize services
|
|||
|
|
horizons = HorizonsService()
|
|||
|
|
|
|||
|
|
# Sync positions for each body
|
|||
|
|
total_positions = 0
|
|||
|
|
synced_bodies = []
|
|||
|
|
failed_bodies = []
|
|||
|
|
|
|||
|
|
start_time = datetime.utcnow()
|
|||
|
|
end_time = start_time + timedelta(days=days)
|
|||
|
|
|
|||
|
|
for body in bodies:
|
|||
|
|
# Use savepoint for this body's operations
|
|||
|
|
async with db.begin_nested(): # Creates a SAVEPOINT
|
|||
|
|
try:
|
|||
|
|
logger.debug(f"Fetching positions for {body.name} ({body.id})")
|
|||
|
|
|
|||
|
|
# Fetch positions from NASA Horizons
|
|||
|
|
positions = await horizons.get_body_positions(
|
|||
|
|
body_id=body.id,
|
|||
|
|
start_time=start_time,
|
|||
|
|
end_time=end_time,
|
|||
|
|
step="1d" # Daily positions
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Save positions to database (upsert logic)
|
|||
|
|
count = 0
|
|||
|
|
for pos in positions:
|
|||
|
|
# Use PostgreSQL's INSERT ... ON CONFLICT to handle duplicates
|
|||
|
|
stmt = insert(Position).values(
|
|||
|
|
body_id=body.id,
|
|||
|
|
time=pos.time,
|
|||
|
|
x=pos.x,
|
|||
|
|
y=pos.y,
|
|||
|
|
z=pos.z,
|
|||
|
|
vx=getattr(pos, 'vx', None),
|
|||
|
|
vy=getattr(pos, 'vy', None),
|
|||
|
|
vz=getattr(pos, 'vz', None),
|
|||
|
|
source=source
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# On conflict (body_id, time), update the existing record
|
|||
|
|
stmt = stmt.on_conflict_do_update(
|
|||
|
|
index_elements=['body_id', 'time'],
|
|||
|
|
set_={
|
|||
|
|
'x': pos.x,
|
|||
|
|
'y': pos.y,
|
|||
|
|
'z': pos.z,
|
|||
|
|
'vx': getattr(pos, 'vx', None),
|
|||
|
|
'vy': getattr(pos, 'vy', None),
|
|||
|
|
'vz': getattr(pos, 'vz', None),
|
|||
|
|
'source': source
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
await db.execute(stmt)
|
|||
|
|
count += 1
|
|||
|
|
|
|||
|
|
# Savepoint will auto-commit if no exception
|
|||
|
|
total_positions += count
|
|||
|
|
synced_bodies.append(body.name)
|
|||
|
|
logger.debug(f"Saved {count} positions for {body.name}")
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
# Savepoint will auto-rollback on exception
|
|||
|
|
logger.error(f"Failed to sync {body.name}: {str(e)}")
|
|||
|
|
failed_bodies.append({"body": body.name, "error": str(e)})
|
|||
|
|
# Continue to next body
|
|||
|
|
|
|||
|
|
# Summary
|
|||
|
|
result = {
|
|||
|
|
"success": len(failed_bodies) == 0,
|
|||
|
|
"bodies_synced": len(synced_bodies),
|
|||
|
|
"total_positions": total_positions,
|
|||
|
|
"synced_bodies": synced_bodies,
|
|||
|
|
"failed_bodies": failed_bodies,
|
|||
|
|
"time_range": f"{start_time.date()} to {end_time.date()}",
|
|||
|
|
"source": source
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.info(f"Sync completed: {len(synced_bodies)} bodies, {total_positions} positions")
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
|
|||
|
|
@task_registry.register(
|
|||
|
|
name="sync_celestial_events",
|
|||
|
|
description="同步天体事件数据(预留功能,暂未实现)",
|
|||
|
|
category="data_sync",
|
|||
|
|
parameters=[
|
|||
|
|
{
|
|||
|
|
"name": "event_types",
|
|||
|
|
"type": "array",
|
|||
|
|
"description": "事件类型列表,如['eclipse', 'conjunction', 'opposition']",
|
|||
|
|
"required": False,
|
|||
|
|
"default": None
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"name": "days_ahead",
|
|||
|
|
"type": "integer",
|
|||
|
|
"description": "向未来查询的天数",
|
|||
|
|
"required": False,
|
|||
|
|
"default": 30
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
)
|
|||
|
|
async def sync_celestial_events(
|
|||
|
|
db: AsyncSession,
|
|||
|
|
logger: logging.Logger,
|
|||
|
|
params: Dict[str, Any]
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
"""
|
|||
|
|
Sync celestial events (PLACEHOLDER - NOT IMPLEMENTED YET)
|
|||
|
|
|
|||
|
|
This is a reserved task for future implementation.
|
|||
|
|
It will sync astronomical events like eclipses, conjunctions, oppositions, etc.
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
db: Database session
|
|||
|
|
logger: Logger instance
|
|||
|
|
params: Task parameters
|
|||
|
|
- event_types: Types of events to sync
|
|||
|
|
- days_ahead: Number of days ahead to query
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Summary of sync operation
|
|||
|
|
"""
|
|||
|
|
logger.warning("sync_celestial_events is not implemented yet")
|
|||
|
|
return {
|
|||
|
|
"success": False,
|
|||
|
|
"message": "This task is reserved for future implementation",
|
|||
|
|
"events_synced": 0
|
|||
|
|
}
|