Files
qiji/backend/app/main.py
T
v6ole 3285e22142 feat: 导入增强 + 客户合并 + 同伴自定义 + 性能优化
=== 导入系统全面增强 ===
- 5 Sheet 完整导入:拜访/计划/商机/要客/纪要
- 同访人智能解析:中英文逗号/顿号/分号 → 系统用户UUID + 外部人员TEXT
- 客户自动创建:Excel中不存在的客户自动入库
- 模糊名称匹配:去空格 + 包含关系纠错
- manager_id 修正:导入时用客户分配的经理(非Excel列/非导入人)
- 模板更新:Sheet5「客户经理」→「填报人」,同访人示例含逗号分隔

=== 客户合并功能 ===
- PUT 改名碰撞检测 → 409 + 合并预览
- GET merge-preview / POST merge 端点
- 事务级迁移:Visit/WorkPlan/MiniBusiness/KeyVisit/联系人/分配
- 去重逻辑:联系人(name+phone)、分配(manager+role)
- last_visit_date 取最大值

=== 拜访记录完善 ===
- visits 新增 companion_names TEXT[] 列
- 同访人支持自定义输入(外部人员),el-select allow-create
- 移动端 VisitForm + PC端 ManagerWorkspace 统一
- 周报「客户经理」→「相关人员」(创建人+同访人)
- 纪要「客户经理」→「填报人」
- 删除拜访后重新计算 customer.last_visit_date

=== 客户选择放开 ===
- 客户经理可看到全部客户(不再限自己分配的)
- 拜访时客户下拉返回全量

=== 前端性能优化 ===
- Element Plus 按需加载 (unplugin-vue-components + unplugin-element-plus)
- 周报照片URL并行请求 (Promise.all)
- onMounted 三路并行 (loadReport + dropdowns + AI)
- nginx Cache-Control: immutable for /assets/
- Google Fonts preconnect hints
- 图片上传前 Canvas 压缩 (max 1920px, quality 0.8)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 22:25:04 +08:00

109 lines
4.1 KiB
Python

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, check_overdue_plans
_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)
async def _scheduled_overdue_check():
"""Check for overdue plans and remind managers (9:00 AM)."""
async with async_session() as db:
await check_overdue_plans(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 '[]'"
))
# 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 '{}'"
))
# Start daily reporting scheduler (17:30 CST = 09:30 UTC)
_scheduler.add_job(_scheduled_check, "cron", hour=17, minute=30, id="daily_check")
_scheduler.add_job(_scheduled_overdue_check, "cron", hour=9, minute=0, id="overdue_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}