from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from sqlalchemy import select 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, mini_business_logs, key_visits from app.api import dashboard, upload, export, import_data, wecom, daily_notes, ai_summary, system_config, leaves from app.models.system_config import SystemConfig from app.services.holidays import refresh_holidays from app.services.scheduler_manager import start_scheduler, shutdown_scheduler @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 '[]'" )) # New columns for v0.3 await conn.run_sync(lambda c: c.exec_driver_sql( "ALTER TABLE users ADD COLUMN IF NOT EXISTS require_report BOOLEAN DEFAULT TRUE" )) await conn.run_sync(lambda c: c.exec_driver_sql( "ALTER TABLE customers ADD COLUMN IF NOT EXISTS last_visit_date DATE" )) await conn.run_sync(lambda c: c.exec_driver_sql( "ALTER TABLE customers ADD COLUMN IF NOT EXISTS last_visit_manager_id UUID" )) await conn.run_sync(lambda c: c.exec_driver_sql( "ALTER TABLE customers ADD COLUMN IF NOT EXISTS customer_type VARCHAR(20) DEFAULT 'unit'" )) # New columns for v0.4 await conn.run_sync(lambda c: c.exec_driver_sql( "ALTER TABLE visits ADD COLUMN IF NOT EXISTS companion_names TEXT[] DEFAULT '{}'" )) # v0.6 — manager color tag await conn.run_sync(lambda c: c.exec_driver_sql( "ALTER TABLE users ADD COLUMN IF NOT EXISTS color VARCHAR(7)" )) # system_config table for v0.5 await conn.run_sync(lambda c: c.exec_driver_sql( "CREATE TABLE IF NOT EXISTS system_config (key VARCHAR(64) PRIMARY KEY, value TEXT DEFAULT '')" )) # leaves table for v0.7 await conn.run_sync(lambda c: c.exec_driver_sql( "CREATE TABLE IF NOT EXISTS leaves (" " id UUID PRIMARY KEY DEFAULT gen_random_uuid()," " manager_id UUID NOT NULL REFERENCES users(id)," " leave_type VARCHAR(20) NOT NULL DEFAULT '事假'," " start_date DATE NOT NULL," " end_date DATE NOT NULL," " reason VARCHAR(500) DEFAULT ''," " submitted_by UUID NOT NULL REFERENCES users(id)," " created_at TIMESTAMPTZ DEFAULT now()," " updated_at TIMESTAMPTZ DEFAULT now()" ")" )) await conn.run_sync(lambda c: c.exec_driver_sql( "CREATE INDEX IF NOT EXISTS idx_leaves_manager_id ON leaves(manager_id)" )) await conn.run_sync(lambda c: c.exec_driver_sql( "CREATE INDEX IF NOT EXISTS idx_leaves_dates ON leaves(start_date, end_date)" )) # mini_business_logs table for v0.8 await conn.run_sync(lambda c: c.exec_driver_sql( "CREATE TABLE IF NOT EXISTS mini_business_logs (" " id UUID PRIMARY KEY DEFAULT gen_random_uuid()," " business_id UUID NOT NULL REFERENCES mini_business(id) ON DELETE CASCADE," " log_date DATE NOT NULL," " method VARCHAR(20) NOT NULL DEFAULT '电话'," " content TEXT NOT NULL DEFAULT ''," " created_by UUID NOT NULL REFERENCES users(id)," " created_at TIMESTAMPTZ DEFAULT now()" ")" )) await conn.run_sync(lambda c: c.exec_driver_sql( "CREATE INDEX IF NOT EXISTS idx_mbl_business_id ON mini_business_logs(business_id)" )) await conn.run_sync(lambda c: c.exec_driver_sql( "CREATE INDEX IF NOT EXISTS idx_mbl_log_date ON mini_business_logs(log_date)" )) # Read notification_time from DB (or use default 17:30) notification_hour, notification_minute = 17, 30 async with async_session() as db: row = await db.get(SystemConfig, "notification_time") if row and row.value: try: parts = row.value.strip().split(":") notification_hour, notification_minute = int(parts[0]), int(parts[1]) except (ValueError, IndexError): pass # Auto-refresh Chinese holiday data from API on startup try: await refresh_holidays(db) except Exception: pass # Use cached data if API unavailable # Start daily reporting scheduler start_scheduler(notification_hour, notification_minute) yield # Shutdown shutdown_scheduler() 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(mini_business_logs.router, prefix="/api") app.include_router(key_visits.router, prefix="/api") app.include_router(dashboard.router, prefix="/api") app.include_router(leaves.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.include_router(system_config.router, prefix="/api") @app.get("/health") async def health_check(): return {"status": "ok", "app": settings.APP_NAME}