6504a393e3
- customers 表新增 customer_type 列 (unit/individual) - 客户管理页:单位客户 | 个人用户 Tab 切换 - 个人用户 Tab: 直接内嵌4 Tab 显示所有个人用户的聚合数据 - 个人用户通过业务模块快速新建产生 (customer_type=individual) - 4个业务API新增 customer_type 筛选参数(JOIN customers) - 客户列表排序: 个人用户置顶 Co-Authored-By: Claude <noreply@anthropic.com>
163 lines
6.8 KiB
Python
163 lines
6.8 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, 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}
|