Files
qiji/backend/app/main.py
T
v6ole 7dcb22f573 feat: 集成 apisbo.com 中国节假日 API 自动获取+缓存
- 新增 holidays.py 服务:按年获取节假日数据,缓存到 system_config
- 区分 holiday(工作日假日)和 workday(调休上班日)两种类型
- is_working_day() 支持调休判断:周末在 workdays 集合中视为工作日
- 启动时自动从 API 刷新当年+明年数据,失败则使用缓存
- 系统设置新增「从 API 刷新」按钮,支局长可手动触发
- 支持 2010-2026 年数据,API 免费免鉴权

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-12 14:17:31 +08:00

141 lines
5.7 KiB
Python

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, 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"
))
# 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)"
))
# 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(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}