from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from apscheduler.schedulers.asyncio import AsyncIOScheduler from app.config import settings from app.database import engine, Base, async_session from app.api import router as api_router from app.api import auth, users, customers, visits, work_plans, mini_business, key_visits from app.api import dashboard, upload, export, import_data, wecom, daily_notes, ai_summary from app.services.scheduler import check_daily_reporting _scheduler = AsyncIOScheduler() async def _scheduled_check(): """Wrapper for APScheduler: create a fresh session and run the daily check.""" async with async_session() as db: await check_daily_reporting(db) @asynccontextmanager async def lifespan(app: FastAPI): # Startup: create tables if not exists (for dev convenience) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) # Add columns that may be missing from older tables await conn.run_sync(lambda c: c.exec_driver_sql( "ALTER TABLE customers ADD COLUMN IF NOT EXISTS remarks TEXT DEFAULT ''" )) await conn.run_sync(lambda c: c.exec_driver_sql( "ALTER TABLE visits ADD COLUMN IF NOT EXISTS visitor_name VARCHAR(50) DEFAULT ''" )) await conn.run_sync(lambda c: c.exec_driver_sql( "ALTER TABLE visits ADD COLUMN IF NOT EXISTS visitor_phone VARCHAR(20) DEFAULT ''" )) # edit_log columns for change tracking for tbl in ["visits", "daily_notes", "work_plans", "mini_business", "key_visits"]: await conn.run_sync(lambda c, t=tbl: c.exec_driver_sql( f"ALTER TABLE {t} ADD COLUMN IF NOT EXISTS edit_log JSONB DEFAULT '[]'" )) # Start daily reporting scheduler (17:30 CST = 09:30 UTC) _scheduler.add_job(_scheduled_check, "cron", hour=17, minute=30, id="daily_check") _scheduler.start() yield # Shutdown _scheduler.shutdown(wait=False) await engine.dispose() app = FastAPI( title=settings.APP_NAME, version="0.1.0", lifespan=lifespan, ) # CORS app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Mount all routers app.include_router(auth.router, prefix="/api") app.include_router(users.router, prefix="/api") app.include_router(customers.router, prefix="/api") app.include_router(visits.router, prefix="/api") app.include_router(work_plans.router, prefix="/api") app.include_router(mini_business.router, prefix="/api") app.include_router(key_visits.router, prefix="/api") app.include_router(dashboard.router, prefix="/api") app.include_router(upload.router, prefix="/api") app.include_router(export.router, prefix="/api") app.include_router(import_data.router, prefix="/api") app.include_router(wecom.router, prefix="/api") app.include_router(daily_notes.router, prefix="/api") app.include_router(ai_summary.router, prefix="/api") @app.get("/health") async def health_check(): return {"status": "ok", "app": settings.APP_NAME}