Files
qiji/backend/app/api/visits.py
T
v6ole a1886074dd 企迹(qiji) 政企周报管理系统 — v0.1
后端: FastAPI + SQLAlchemy 2.0 (async) + Alembic + MinIO + Casdoor + 企微
前端: Vue 3 + Vite + TypeScript + Element Plus + Pinia

功能清单:
- 8 张数据表自动建表 / Casdoor OIDC 登录 / 企微静默登录
- 双布局: 移动端(填报) + PC端(汇总管理)
- 拜访记录 CRUD + MinIO 照片直传 + 缩略图预览 + 同访人草稿
- 今日纪要 (6 分类) / 工作计划 / 小微商机 / 要客拜访 CRUD
- 客户档案: 备注/收支费用/联系人/归属分配/批量转移
- 客户导入导出 + 模板下载 + 搜索/分页/筛选
- 仪表盘: 四卡统计 + 填报进度 (拜访+纪要双维度)
- 周报详情: 五 Tab + 按人/客户筛选 + 时间轴
- 用户管理 / 客户经理 PC 端工作台
- 企微: 催办/公告/定时提醒 / 时区修正
- Docker 部署配置

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-24 08:30:12 +08:00

210 lines
7.0 KiB
Python

import uuid
from datetime import date, datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.database import get_db
from app.middleware.auth import get_current_user, require_any_role
from app.models.visit import Visit
from app.models.customer import Customer
from app.models.user import User
from app.schemas.visit import VisitCreate, VisitUpdate, VisitOut, VisitListOut
from app.utils.timezone import today_cst, parse_date
from app.services.minio_client import delete_objects
router = APIRouter(prefix="/visits", tags=["Visits"])
async def _enrich_visit(visit: Visit, db: AsyncSession) -> dict:
"""Enrich a visit record with customer/manager names."""
customer_name = None
manager_name = None
if visit.customer_id:
cust_result = await db.execute(select(Customer.name).where(Customer.id == visit.customer_id))
customer_name = cust_result.scalar_one_or_none()
if visit.manager_id:
mgr_result = await db.execute(select(User.name).where(User.id == visit.manager_id))
manager_name = mgr_result.scalar_one_or_none()
return {
"id": str(visit.id),
"customer_id": str(visit.customer_id),
"customer_name": customer_name,
"visit_date": visit.visit_date,
"visit_method": visit.visit_method,
"time_range": visit.time_range,
"communication_content": visit.communication_content,
"customer_demand": visit.customer_demand,
"companions": visit.companions,
"photos": visit.photos,
"manager_id": str(visit.manager_id),
"manager_name": manager_name,
"created_at": str(visit.created_at),
"updated_at": str(visit.updated_at),
}
@router.get("/")
async def list_visits(
date_from: Optional[str] = Query(None),
date_to: Optional[str] = Query(None),
customer_id: Optional[str] = Query(None),
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""List visits. Managers see only their own, directors/leaders see all."""
query = select(Visit)
if current_user["role"] == "manager":
query = query.where(Visit.manager_id == uuid.UUID(current_user["user_id"]))
if date_from:
query = query.where(Visit.visit_date >= parse_date(date_from))
if date_to:
query = query.where(Visit.visit_date <= parse_date(date_to))
if customer_id:
query = query.where(Visit.customer_id == uuid.UUID(customer_id))
query = query.order_by(Visit.visit_date.desc(), Visit.created_at.desc()).limit(200)
result = await db.execute(query)
visits = result.scalars().all()
# Enrich
enriched = []
for v in visits:
enriched.append(await _enrich_visit(v, db))
return enriched
@router.get("/today")
async def list_today_visits(
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get today's visits for the current user's mobile home screen."""
query = select(Visit).where(Visit.visit_date == today_cst())
if current_user["role"] == "manager":
query = query.where(Visit.manager_id == uuid.UUID(current_user["user_id"]))
query = query.order_by(Visit.created_at.desc())
result = await db.execute(query)
visits = result.scalars().all()
enriched = []
for v in visits:
enriched.append(await _enrich_visit(v, db))
count = len(enriched)
return {"count": count, "visits": enriched}
@router.get("/{visit_id}")
async def get_visit(
visit_id: str,
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Visit).where(Visit.id == visit_id))
visit = result.scalar_one_or_none()
if not visit:
raise HTTPException(status_code=404, detail="Visit not found")
# Permission check
if current_user["role"] == "manager" and str(visit.manager_id) != current_user["user_id"]:
raise HTTPException(status_code=403, detail="Access denied")
return await _enrich_visit(visit, db)
@router.post("/")
async def create_visit(
data: VisitCreate,
current_user: dict = Depends(require_any_role),
db: AsyncSession = Depends(get_db),
):
"""Create a visit record. If companions are selected, creates draft copies for them."""
visit = Visit(
customer_id=data.customer_id,
visit_date=parse_date(data.visit_date),
visit_method=data.visit_method,
time_range=data.time_range,
communication_content=data.communication_content,
customer_demand=data.customer_demand,
companions=data.companions,
photos=data.photos,
manager_id=uuid.UUID(current_user["user_id"]),
)
db.add(visit)
# Create draft copies for companions
for companion_id in data.companions:
if companion_id != uuid.UUID(current_user["user_id"]):
draft = Visit(
customer_id=data.customer_id,
visit_date=parse_date(data.visit_date),
visit_method=data.visit_method,
time_range=data.time_range,
communication_content="", # Leave blank for companion to fill
customer_demand="",
companions=[],
photos=[],
manager_id=companion_id,
)
db.add(draft)
await db.commit()
await db.refresh(visit)
return await _enrich_visit(visit, db)
@router.put("/{visit_id}")
async def update_visit(
visit_id: str,
data: VisitUpdate,
current_user: dict = Depends(require_any_role),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Visit).where(Visit.id == visit_id))
visit = result.scalar_one_or_none()
if not visit:
raise HTTPException(status_code=404, detail="Visit not found")
if current_user["role"] == "manager" and str(visit.manager_id) != current_user["user_id"]:
raise HTTPException(status_code=403, detail="Access denied")
update_data = data.model_dump(exclude_unset=True)
if "visit_date" in update_data and update_data["visit_date"]:
update_data["visit_date"] = parse_date(update_data["visit_date"])
for key, value in update_data.items():
setattr(visit, key, value)
await db.commit()
await db.refresh(visit)
return await _enrich_visit(visit, db)
@router.delete("/{visit_id}")
async def delete_visit(
visit_id: str,
current_user: dict = Depends(require_any_role),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Visit).where(Visit.id == visit_id))
visit = result.scalar_one_or_none()
if not visit:
raise HTTPException(status_code=404, detail="Visit not found")
if current_user["role"] == "manager" and str(visit.manager_id) != current_user["user_id"]:
raise HTTPException(status_code=403, detail="Access denied")
# Clean up photos in MinIO
if visit.photos:
delete_objects(visit.photos)
await db.delete(visit)
await db.commit()
return {"detail": "deleted"}