From 17a0218b32f18313456e3bd5a95533ff6431bc42 Mon Sep 17 00:00:00 2001 From: v6ole Date: Sun, 12 Jul 2026 18:51:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=95=86=E6=9C=BA=E8=B7=9F=E5=8D=95?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=B7=9F=E8=BF=9B=E8=AE=B0=E5=BD=95=E5=AD=90?= =?UTF-8?q?=E8=A1=A8=EF=BC=88=E5=90=8E=E7=AB=AF+API=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 mini_business_logs 表(business_id/log_date/method/content/created_by) - MiniBusiness 模型添加 logs relationship(cascade delete) - 5个端点: GET列表/POST新增/PUT编辑/DELETE删除 - 跟进方式: 电话/微信/上门/邮件/其他 - lifespan 自动建表+索引 Co-Authored-By: Claude --- backend/app/api/mini_business_logs.py | 143 +++++++++++++++++++++++ backend/app/main.py | 21 +++- backend/app/models/__init__.py | 2 + backend/app/models/mini_business.py | 8 +- backend/app/models/mini_business_log.py | 30 +++++ backend/app/schemas/mini_business_log.py | 33 ++++++ frontend/src/api/miniBusiness.ts | 13 +++ 7 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 backend/app/api/mini_business_logs.py create mode 100644 backend/app/models/mini_business_log.py create mode 100644 backend/app/schemas/mini_business_log.py diff --git a/backend/app/api/mini_business_logs.py b/backend/app/api/mini_business_logs.py new file mode 100644 index 0000000..a35b7e6 --- /dev/null +++ b/backend/app/api/mini_business_logs.py @@ -0,0 +1,143 @@ +"""MiniBusinessLog CRUD API — follow-up log entries for mini business opportunities.""" + +import uuid +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from app.database import get_db +from app.middleware.auth import get_current_user, require_any_role +from app.models.mini_business import MiniBusiness +from app.models.mini_business_log import MiniBusinessLog +from app.models.user import User +from app.schemas.mini_business_log import ( + MiniBusinessLogCreate, MiniBusinessLogUpdate, MiniBusinessLogOut, +) + +router = APIRouter(prefix="/mini-business", tags=["MiniBusiness Logs"]) + + +async def _get_log(db: AsyncSession, log_id: str) -> MiniBusinessLog: + result = await db.execute(select(MiniBusinessLog).where(MiniBusinessLog.id == log_id)) + log = result.scalar_one_or_none() + if not log: + raise HTTPException(status_code=404, detail="跟进记录不存在") + return log + + +@router.get("/{business_id}/logs") +async def list_logs( + business_id: str, + current_user: dict = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """List follow-up logs for a mini business, newest first.""" + # Verify business exists and user has access + mb = await db.get(MiniBusiness, business_id) + if not mb: + raise HTTPException(status_code=404, detail="商机不存在") + if current_user["role"] == "manager" and str(mb.manager_id) != current_user["user_id"]: + raise HTTPException(status_code=403, detail="Access denied") + + result = await db.execute( + select(MiniBusinessLog) + .where(MiniBusinessLog.business_id == business_id) + .order_by(MiniBusinessLog.log_date.desc(), MiniBusinessLog.created_at.desc()) + ) + logs = result.scalars().all() + + # Resolve creator names + user_ids = {log.created_by for log in logs} + users = (await db.execute(select(User).where(User.id.in_(user_ids)))).scalars().all() + user_map = {u.id: u.name for u in users} + + return [ + { + "id": log.id, + "business_id": log.business_id, + "log_date": str(log.log_date), + "method": log.method, + "content": log.content, + "created_by": log.created_by, + "created_by_name": user_map.get(log.created_by, ""), + "created_at": str(log.created_at) if log.created_at else None, + } + for log in logs + ] + + +@router.post("/{business_id}/logs") +async def create_log( + business_id: str, + data: MiniBusinessLogCreate, + current_user: dict = Depends(require_any_role), + db: AsyncSession = Depends(get_db), +): + """Add a follow-up log entry.""" + mb = await db.get(MiniBusiness, business_id) + if not mb: + raise HTTPException(status_code=404, detail="商机不存在") + if current_user["role"] == "manager" and str(mb.manager_id) != current_user["user_id"]: + raise HTTPException(status_code=403, detail="Access denied") + + log = MiniBusinessLog( + business_id=uuid.UUID(business_id), + log_date=data.log_date, + method=data.method, + content=data.content, + created_by=uuid.UUID(current_user["user_id"]), + ) + db.add(log) + await db.commit() + await db.refresh(log) + + creator = await db.get(User, log.created_by) + return { + "id": log.id, + "business_id": log.business_id, + "log_date": str(log.log_date), + "method": log.method, + "content": log.content, + "created_by": log.created_by, + "created_by_name": creator.name if creator else "", + "created_at": str(log.created_at) if log.created_at else None, + } + + +@router.put("/logs/{log_id}") +async def update_log( + log_id: str, + data: MiniBusinessLogUpdate, + current_user: dict = Depends(require_any_role), + db: AsyncSession = Depends(get_db), +): + """Update a follow-up log entry.""" + log = await _get_log(db, log_id) + + # Managers can only edit their own logs; directors/leaders can edit any + if current_user["role"] == "manager" and str(log.created_by) != current_user["user_id"]: + raise HTTPException(status_code=403, detail="Access denied") + + update_data = data.model_dump(exclude_unset=True) + for k, v in update_data.items(): + setattr(log, k, v) + + await db.commit() + await db.refresh(log) + return {"id": log.id, "log_date": str(log.log_date), "method": log.method, "content": log.content} + + +@router.delete("/logs/{log_id}") +async def delete_log( + log_id: str, + current_user: dict = Depends(require_any_role), + db: AsyncSession = Depends(get_db), +): + """Delete a follow-up log entry.""" + log = await _get_log(db, log_id) + + if current_user["role"] == "manager" and str(log.created_by) != current_user["user_id"]: + raise HTTPException(status_code=403, detail="Access denied") + + await db.delete(log) + await db.commit() + return {"status": "deleted"} diff --git a/backend/app/main.py b/backend/app/main.py index 7d11e4b..028c2ef 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -5,7 +5,7 @@ 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 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 @@ -74,6 +74,24 @@ async def lifespan(app: FastAPI): 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 @@ -123,6 +141,7 @@ 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") diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index f4ad82c..0669523 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -10,6 +10,7 @@ from app.models.daily_note import DailyNote from app.models.ai_summary import AISummary from app.models.system_config import SystemConfig from app.models.leave import Leave +from app.models.mini_business_log import MiniBusinessLog __all__ = [ "User", @@ -19,6 +20,7 @@ __all__ = [ "Visit", "WorkPlan", "MiniBusiness", + "MiniBusinessLog", "KeyVisit", "DailyNote", "AISummary", diff --git a/backend/app/models/mini_business.py b/backend/app/models/mini_business.py index fb242dd..1bf7d9e 100644 --- a/backend/app/models/mini_business.py +++ b/backend/app/models/mini_business.py @@ -1,9 +1,13 @@ import uuid +from typing import TYPE_CHECKING from sqlalchemy import String, Text, ForeignKey -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.dialects.postgresql import UUID, JSONB from app.database import Base +if TYPE_CHECKING: + from app.models.mini_business_log import MiniBusinessLog + class MiniBusiness(Base): __tablename__ = "mini_business" @@ -17,3 +21,5 @@ class MiniBusiness(Base): manager_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), index=True) expected_revenue_date: Mapped[str] = mapped_column(String(50), default="") edit_log: Mapped[list] = mapped_column(JSONB, default=list) + + logs: Mapped[list["MiniBusinessLog"]] = relationship(back_populates="business", cascade="all, delete-orphan") diff --git a/backend/app/models/mini_business_log.py b/backend/app/models/mini_business_log.py new file mode 100644 index 0000000..0d9bccf --- /dev/null +++ b/backend/app/models/mini_business_log.py @@ -0,0 +1,30 @@ +"""MiniBusinessLog — follow-up log entries for mini business opportunities.""" + +import uuid +from datetime import date, datetime +from sqlalchemy import String, Date, DateTime, ForeignKey, Text, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column, relationship +from app.database import Base + + +class MiniBusinessLog(Base): + __tablename__ = "mini_business_logs" + + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 + ) + business_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("mini_business.id", ondelete="CASCADE"), index=True + ) + log_date: Mapped[date] = mapped_column(Date) + method: Mapped[str] = mapped_column(String(20), default="电话") + content: Mapped[str] = mapped_column(Text, default="") + created_by: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("users.id") + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now() + ) + + business: Mapped["MiniBusiness"] = relationship(back_populates="logs") diff --git a/backend/app/schemas/mini_business_log.py b/backend/app/schemas/mini_business_log.py new file mode 100644 index 0000000..b14125a --- /dev/null +++ b/backend/app/schemas/mini_business_log.py @@ -0,0 +1,33 @@ +"""MiniBusinessLog Pydantic schemas.""" + +from datetime import date, datetime +from uuid import UUID +from typing import Optional +from pydantic import BaseModel, Field + +FOLLOW_UP_METHODS = ["电话", "微信", "上门", "邮件", "其他"] + + +class MiniBusinessLogCreate(BaseModel): + log_date: date + method: str = Field(default="电话", pattern="^(电话|微信|上门|邮件|其他)$") + content: str = "" + + +class MiniBusinessLogUpdate(BaseModel): + log_date: Optional[date] = None + method: Optional[str] = Field(default=None, pattern="^(电话|微信|上门|邮件|其他)$") + content: Optional[str] = None + + +class MiniBusinessLogOut(BaseModel): + id: UUID + business_id: UUID + log_date: date + method: str + content: str + created_by: UUID + created_by_name: str = "" + created_at: datetime | None = None + + model_config = {"from_attributes": True} diff --git a/frontend/src/api/miniBusiness.ts b/frontend/src/api/miniBusiness.ts index 7c12684..11112d7 100644 --- a/frontend/src/api/miniBusiness.ts +++ b/frontend/src/api/miniBusiness.ts @@ -13,4 +13,17 @@ export const miniBusinessApi = { delete(id: string) { return api.delete(`/mini-business/${id}`) }, + // Follow-up logs + getLogs(businessId: string) { + return api.get(`/mini-business/${businessId}/logs`) + }, + createLog(businessId: string, data: any) { + return api.post(`/mini-business/${businessId}/logs`, data) + }, + updateLog(logId: string, data: any) { + return api.put(`/mini-business/logs/${logId}`, data) + }, + deleteLog(logId: string) { + return api.delete(`/mini-business/logs/${logId}`) + }, }