Files
qiji/backend/app/api/visits.py
T
v6ole 6504a393e3 feat: 个人用户聚合视图 — 客户管理双Tab+四模块数据聚合
- 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>
2026-07-12 22:14:44 +08:00

312 lines
12 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
from app.utils.edit_log import compute_diff, append_entry, init_entry
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()
# Resolve companion UUIDs to names
companion_names_resolved = list(visit.companion_names or [])
if visit.companions:
comp_result = await db.execute(select(User.name).where(User.id.in_(visit.companions)))
companion_names_resolved = [n for n, in comp_result.all()] + companion_names_resolved
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,
"visitor_name": visit.visitor_name or "",
"visitor_phone": visit.visitor_phone or "",
"communication_content": visit.communication_content,
"customer_demand": visit.customer_demand,
"companions": visit.companions,
"companion_names": visit.companion_names or [],
"companion_names_resolved": companion_names_resolved,
"photos": visit.photos,
"manager_id": str(visit.manager_id),
"manager_name": manager_name,
"edit_log": visit.edit_log or [],
"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),
customer_type: 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))
if customer_type:
query = query.join(Customer, Visit.customer_id == Customer.id).where(Customer.customer_type == customer_type)
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,
companion_names=data.companion_names,
photos=data.photos,
manager_id=data.manager_id if (current_user["role"] in ("director", "leader") and data.manager_id) else uuid.UUID(current_user["user_id"]),
)
init_entry(visit, current_user["name"])
db.add(visit)
# Update customer's last visit tracking
cust_result = await db.execute(select(Customer).where(Customer.id == data.customer_id))
customer = cust_result.scalar_one_or_none()
if customer:
customer.last_visit_date = parse_date(data.visit_date)
customer.last_visit_manager_id = uuid.UUID(current_user["user_id"])
db.add(customer)
# Auto-complete matching work plans for this customer
from app.services.visits import auto_complete_work_plans
await auto_complete_work_plans(
db, data.customer_id, parse_date(data.visit_date),
current_user["name"], "拜访自动完成",
)
# Auto-create follow-up logs for active mini business opportunities
from app.models.mini_business import MiniBusiness
from app.models.mini_business_log import MiniBusinessLog
method_map = {"上门": "上门", "电话": "电话", "微信": "微信", "出差": "其他"}
active_biz = await db.execute(
select(MiniBusiness).where(
MiniBusiness.customer_id == data.customer_id,
MiniBusiness.status == "跟进中",
)
)
for biz in active_biz.scalars().all():
parts = []
if data.communication_content and data.communication_content.strip():
parts.append(f"沟通内容:{data.communication_content.strip()}")
if data.customer_demand and data.customer_demand.strip():
parts.append(f"客户需求:{data.customer_demand.strip()}")
log_content = "\n".join(parts) if parts else "拜访跟进"
follow_method = method_map.get(data.visit_method, "其他")
log = MiniBusinessLog(
business_id=biz.id,
log_date=parse_date(data.visit_date),
method=follow_method,
content=log_content,
created_by=uuid.UUID(current_user["user_id"]),
)
db.add(log)
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")
# Snapshot old values for diff
old_snapshot = {
"customer_id": str(visit.customer_id), "visit_date": str(visit.visit_date),
"visit_method": visit.visit_method, "time_range": visit.time_range,
"visitor_name": visit.visitor_name or "", "visitor_phone": visit.visitor_phone or "",
"communication_content": visit.communication_content, "customer_demand": visit.customer_demand,
}
update_data = data.model_dump(exclude_unset=True)
if current_user["role"] not in ("director", "leader"):
update_data.pop("manager_id", None)
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)
# Compute diff and append to edit_log
new_snapshot = {
"customer_id": str(visit.customer_id), "visit_date": str(visit.visit_date),
"visit_method": visit.visit_method, "time_range": visit.time_range,
"visitor_name": visit.visitor_name or "", "visitor_phone": visit.visitor_phone or "",
"communication_content": visit.communication_content, "customer_demand": visit.customer_demand,
}
changes = compute_diff(old_snapshot, new_snapshot)
if changes:
append_entry(visit, current_user["name"], changes, getattr(data, "edit_reason", None))
await db.commit()
await db.refresh(visit)
# Auto-complete matching work plans after visit update
from app.services.visits import auto_complete_work_plans
await auto_complete_work_plans(
db, visit.customer_id, visit.visit_date,
current_user["name"], "拜访更新自动完成",
)
await db.commit()
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")
customer_id = visit.customer_id
manager_id = visit.manager_id
# Clean up photos in MinIO
if visit.photos:
delete_objects(visit.photos)
await db.delete(visit)
# Recalculate customer's last_visit_date from remaining visits
latest = await db.execute(
select(func.max(Visit.visit_date)).where(
Visit.customer_id == customer_id,
)
)
new_latest = latest.scalar()
cust = await db.get(Customer, customer_id)
if cust:
if new_latest:
cust.last_visit_date = new_latest
# Keep the existing manager if date unchanged, or find who made the latest visit
latest_visit = await db.execute(
select(Visit).where(
Visit.customer_id == customer_id,
Visit.visit_date == new_latest,
).order_by(Visit.created_at.desc()).limit(1)
)
lv = latest_visit.scalar_one_or_none()
if lv:
cust.last_visit_manager_id = lv.manager_id
else:
cust.last_visit_date = None
cust.last_visit_manager_id = None
await db.commit()
return {"detail": "deleted"}